You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
tdemultimedia/noatun/library/playlistsaver.cpp

772 lines
18 KiB

#include <noatun/playlistsaver.h>
#include <tqdom.h>
#include <tdeio/netaccess.h>
#include <tqfile.h>
#include <tqtextstream.h>
#include <noatun/app.h>
#include "ksaver.h"
#include <ksimpleconfig.h>
#include <kmimetype.h>
#include <tdelocale.h>
#include <tqregexp.h>
#include <tqxml.h>
#include <kdebug.h>
PlaylistSaver::PlaylistSaver()
{
}
PlaylistSaver::~PlaylistSaver()
{
}
bool PlaylistSaver::save(const KURL &file, int opt)
{
// kdDebug(66666) << k_funcinfo << "opt=" << opt << endl;
if(file.isEmpty() || !file.isValid())
return false;
switch (opt)
{
default:
case 0:
case XMLPlaylist:
return saveXML(file, opt);
case M3U:
case EXTM3U:
return saveM3U(file, opt);
case PLS:
return savePLS(file, opt);
case ASX:
return false; // No, I won't code that! [mETz]
}
}
bool PlaylistSaver::load(const KURL &file, int opt)
{
// kdDebug(66666) << k_funcinfo << "opt=" << opt << endl;
switch (opt)
{
default:
case 0:
case XMLPlaylist:
case ASX:
return loadXML(file, opt);
case M3U:
case EXTM3U:
return loadM3U(file, opt);
case PLS:
return loadPLS(file, opt);
}
}
bool PlaylistSaver::metalist(const KURL &url)
{
kdDebug(66666) << k_funcinfo << "url=" << url.url() << endl;
TQString end=url.filename().right(3).lower();
/*
if (end=="mp3" || end=="ogg") // we want to download streams only
{
kdDebug(66666) << k_funcinfo << "I can only load playlists" << endl;
return false;
}
*/
/*
.wax audio/x-ms-wax Metafiles that reference Windows Media files with the
.asf, .wma or .wax file extensions.
.wvx video/x-ms-wvx Metafiles that reference Windows Media files with the
.wma, .wmv, .wvx or .wax file extensions.
.asx video/x-ms-asf Metafiles that reference Windows Media files with the
.wma, .wax, .wmv, .wvx, .asf, or .asx file extensions.
*/
// it's actually a stream!
if (end!="pls" &&
end!="m3u" &&
end!="wax" && // windows mediaplayer metafile
end!="wvx" && // windows mediaplayer metafile
end!="asx" && // windows mediaplayer metafile
url.protocol().lower()=="http")
{
KMimeType::Ptr mimetype = KMimeType::findByURL(url);
TQString type=mimetype->name();
if (type!="application/octet-stream")
return false;
TQMap<TQString,TQString> map;
map["playObject"]="Arts::StreamPlayObject";
map["title"] = i18n("Stream from %1").arg(url.host());
KURL u(url);
if (!u.hasPath())
u.setPath("/");
map["stream_"] = map["url"] = u.url();
reset();
readItem(map);
return true;
}
// it is a pls, m3u or ms-media-player file by now
if(loadXML(url, XMLPlaylist))
return true;
if(loadXML(url,ASX))
return true;
if(loadPLS(url))
return true;
if(loadM3U(url))
return true;
return false;
}
bool PlaylistSaver::saveXML(const KURL &file, int )
{
TQString localFile;
if (file.isLocalFile())
localFile = TQFile::encodeName(file.path());
else
localFile = napp->tempSaveName(file.path());
// TQDom is a pain :)
TQDomDocument doc("playlist");
doc.setContent(TQString("<!DOCTYPE XMLPlaylist><playlist version=\"1.0\" client=\"noatun\"/>"));
TQDomElement docElem=doc.documentElement();
reset();
PlaylistItem i;
TQStringList props;
while ((i=writeItem()))
{
// write all properties
props=i.properties();
TQDomElement elem=doc.createElement("item");
for (TQStringList::Iterator pi(props.begin()); pi!=props.end(); ++pi)
{
TQString val=i.property(*pi);
elem.setAttribute(*pi, val);
if ((*pi)=="url")
{
KURL u(val);
if (u.isLocalFile())
{
elem.setAttribute("local", u.path());
}
}
}
docElem.appendChild(elem);
props.clear();
}
Noatun::KSaver saver(localFile);
if (!saver.open())
return false;
saver.textStream().setEncoding(TQTextStream::UnicodeUTF8);
saver.textStream() << doc.toString();
saver.close();
return true;
}
class NoatunXMLStructure : public TQXmlDefaultHandler
{
public:
PlaylistSaver *saver;
bool fresh;
NoatunXMLStructure(PlaylistSaver *s)
: saver(s), fresh(true)
{
}
bool startElement(
const TQString&, const TQString &,
const TQString &name, const TQXmlAttributes &a
)
{
if (fresh)
{
if (name=="playlist")
{
fresh=false;
return true;
}
else
{
return false;
}
}
if (name != "item")
return true;
TQMap<TQString,TQString> propMap;
for (int i=0; i<a.count(); i++)
{
propMap[a.qName(i)] = a.value(i);
}
saver->readItem(propMap);
return true;
}
};
class MSASXStructure : public TQXmlDefaultHandler
{
public:
PlaylistSaver *saver;
bool fresh;
bool inEntry, inTitle;
TQMap<TQString,TQString> propMap;
TQString mAbsPath;
MSASXStructure(PlaylistSaver *s, const TQString &absPath)
: saver(s), fresh(true), inEntry(false),
inTitle(false), mAbsPath(absPath)
{
//kdDebug(66666) << k_funcinfo << endl;
}
bool startElement(const TQString&, const TQString &,
const TQString &name, const TQXmlAttributes &a)
{
if (fresh)
{
if (name.lower()=="asx")
{
//kdDebug(66666) << "found ASX format" << endl;
fresh=false;
return true;
}
else
{
kdDebug(66666) << "This is NOT an ASX style playlist!" << endl;
return false;
}
}
if (name.lower()=="entry")
{
if(inEntry) // WHOOPS, we are already in an entry, this should NEVER happen
{
kdDebug(66666) << "STOP, ENTRY INSIDE ENTRY!" << endl;
return false;
}
// kdDebug(66666) << "<ENTRY> =====================" << endl;
inEntry=true;
}
else
{
if (inEntry) // inside entry block
{
// known stuff inside an <entry> ... </entry> block:
// <title>blah</title>
// <param album="blub" />
// <param artist="blah" />
// <ref HREF="file:/something" />
if(name.lower()=="ref")
{
for (int i=0; i<a.count(); i++)
{
if(a.qName(i).lower()=="href")
{
TQString filename=a.value(i);
if (filename.find(TQRegExp("^[a-zA-Z0-9]+:/"))==0)
{
KURL url(filename);
KMimeType::Ptr mimetype = KMimeType::findByURL(url);
TQString type=mimetype->name();
if (type != "application/octet-stream")
{
propMap["url"]=filename;
}
else
{
propMap["playObject"]="SplayPlayObject";
propMap["title"] = i18n("Stream from %1").arg(url.host());
if (!url.hasPath())
url.setPath("/");
propMap["url"] = url.url();
propMap["stream_"]=propMap["url"];
// readItem(propMap);
// continue;
}
}
else
{
KURL u1;
// we have to deal with a relative path
if (filename.find('/'))
{
u1.setPath(mAbsPath); //FIXME: how to get the path in this place?
u1.setFileName(filename);
}
else
{
u1.setPath(filename);
}
propMap["url"]=u1.url();
}
// kdDebug(66666) << "adding property url, value='" << propMap["url"] << "'" << endl;
}
}
}
else if(name.lower()=="param")
{
TQString keyName="", keyValue="";
for (int i=0; i<a.count(); i++)
{
if(a.value(i).lower()=="album")
keyName="album";
else if(a.value(i).lower()=="artist")
keyName="author";
else if(!keyName.isEmpty()) // successfully found a key, the next key=value pair has to be the value
{
// kdDebug(66666) << "keyName=" << keyName << ", next value is '" << a.value(i) << "'" << endl;
keyValue=a.value(i);
}
}
if (!keyName.isEmpty() && !keyValue.isEmpty())
{
// kdDebug(66666) << "adding property; key='" << keyName << "', value='" << keyValue << "'" << endl;
propMap[keyName]=keyValue;
}
}
else if(name.lower()=="title")
{
if(inTitle) // WHOOPS, we are already in an entry, this should NEVER happen
{
kdDebug(66666) << "STOP, TITLE INSIDE TITLE!" << endl;
return false;
}
// kdDebug(66666) << "<TITLE> ======" << endl;
inTitle=true;
}
/* else
{
kdDebug(66666) << "Unknown/unused element inside ENTRY block, NAME='" << name << "'" << endl;
for (int i=0; i<a.count(); i++)
kdDebug(66666) << " | " << a.qName(i) << " = '" << a.value(i) << "'" << endl;
}*/
}
/* else
{
kdDebug(66666) << "Uninteresting element, NAME='" << name << "'" << endl;
for (int i=0; i<a.count(); i++)
kdDebug(66666) << " | " << a.qName(i) << " = '" << a.value(i) << "'" << endl;
}*/
}
return true;
}
bool endElement(const TQString&,const TQString&, const TQString& name)
{
// kdDebug(66666) << k_funcinfo << "name='" << name << "'" << endl;
if (name.lower()=="entry")
{
if(inEntry)
{
/* kdDebug(66666) << "</ENTRY> =====================" << endl;
for (TQMap<TQString,TQString>::ConstIterator it=propMap.begin(); it!=propMap.end(); ++it )
kdDebug(66666) << "key='" << it.key() << "', val='" << it.data() << "'" << endl;
*/
saver->readItem(propMap);
propMap.clear();
inEntry=false;
}
else // found </entry> without a start
{
kdDebug(66666) << "STOP, </ENTRY> without a start" << endl;
return false;
}
}
else if (name.lower()=="title")
{
if(inTitle && inEntry)
{
// kdDebug(66666) << "</TITLE> ======" << endl;
inTitle=false;
}
else if (inTitle) // found </title> without a start or not inside an <entry> ... </entry>
{
kdDebug(66666) << "STOP, </TITLE> without a start" << endl;
return false;
}
}
return true;
}
bool characters(const TQString &ch)
{
if(inTitle)
{
if (!ch.isEmpty())
{
propMap["title"]=ch;
// kdDebug(66666) << "adding property; key='title', value='" << ch << "'" << endl;
}
}
return true;
}
};
bool PlaylistSaver::loadXML(const KURL &url, int opt)
{
kdDebug(66666) << k_funcinfo <<
"file='" << url.url() << "', opt=" << opt << endl;
TQString dest;
if(TDEIO::NetAccess::download(url, dest, 0L))
{
TQFile file(dest);
if (!file.open(IO_ReadOnly))
return false;
reset();
// TQXml is horribly documented
TQXmlInputSource source(&file);
TQXmlSimpleReader reader;
if (opt == ASX ||
url.path().right(4).lower()==".wax" ||
url.path().right(4).lower()==".asx" ||
url.path().right(4).lower()==".wvx")
{
MSASXStructure ASXparser(this, url.path(0));
reader.setContentHandler(&ASXparser);
reader.parse(source);
return !ASXparser.fresh;
}
else
{
NoatunXMLStructure parser(this);
reader.setContentHandler(&parser);
reader.parse(source);
return !parser.fresh;
}
} // END download()
return false;
}
bool PlaylistSaver::loadM3U(const KURL &file, int /*opt*/)
{
kdDebug(66666) << k_funcinfo << "file='" << file.path() << endl;
TQString localFile;
if(!TDEIO::NetAccess::download(file, localFile, 0L))
return false;
// if it's a PLS, transfer control, again (TDEIO bug?)
#if 0
{
KSimpleConfig list(local, true);
list.setGroup("playlist");
// some stupid Windows lusers like to be case insensitive
TQStringList groups=list.groupList().grep(TQRegExp("^playlist$", false));
if (groups.count())
{
KURL l;
l.setPath(local);
return loadPLS(l);
}
}
#endif
TQFile saver(localFile);
saver.open(IO_ReadOnly);
TQTextStream t(&saver);
bool isExt = false; // flag telling if we load an EXTM3U file
TQString filename;
TQString extinf;
TQMap<TQString,TQString> prop;
reset();
while (!t.eof())
{
if (isExt)
{
extinf = t.readLine();
if (!extinf.startsWith("#EXTINF:"))
{
// kdDebug(66666) << "EXTM3U extinf line != extinf, assuming it's a filename." << endl;
filename = extinf;
extinf="";
}
else
{
filename = t.readLine(); // read in second line containing the filename
}
//kdDebug(66666) << "EXTM3U filename = '" << filename << "'" << endl;
}
else // old style m3u
{
filename = t.readLine();
}
if (filename == "#EXTM3U") // on first line
{
// kdDebug(66666) << "FOUND '#EXTM3U' @ " << saver.at() << "." << endl;
isExt=true;
continue; // skip parsing the first (i.e. this) line
}
if (filename.isEmpty())
continue;
if (filename.find(TQRegExp("^[a-zA-Z0-9]+:/"))==0)
{
//kdDebug(66666) << k_funcinfo << "url filename = " << filename << endl;
KURL protourl(filename);
KMimeType::Ptr mimetype = KMimeType::findByURL(protourl);
if (mimetype->name() != "application/octet-stream")
{
prop["url"] = filename;
}
else
{
prop["playObject"]="SplayPlayObject";
// Default title, might be overwritten by #EXTINF later
prop["title"] = i18n("Stream from %1").arg(protourl.host());
if (!protourl.hasPath())
protourl.setPath("/");
prop["url"] = protourl.url();
prop["stream_"] = prop["url"];
}
}
else // filename that is not of URL style (i.e. NOT "proto:/path/somefile")
{
KURL u1;
// we have to deal with a relative path
if (filename.find('/'))
{
u1.setPath(file.path(0));
u1.setFileName(filename);
}
else
{
u1.setPath(filename);
}
prop["url"] = u1.url();
}
// parse line of the following format:
//#EXTINF:length,displayed_title
if (isExt)
{
extinf.remove(0,8); // remove "#EXTINF:"
//kdDebug(66666) << "EXTM3U extinf = '" << extinf << "'" << endl;
int timeTitleSep = extinf.find(',', 0);
int length = (extinf.left(timeTitleSep)).toInt();
if (length>0)
prop["length"]=TQString::number(length*1000);
TQString displayTitle=extinf.mid(timeTitleSep+1);
if (!displayTitle.isEmpty())
{
int artistTitleSep = displayTitle.find(" - ",0);
if (artistTitleSep == -1) // no "artist - title" like format, just set it as title
{
prop["title"] = displayTitle;
}
else
{
prop["author"] = displayTitle.left(artistTitleSep);
prop["title"] = displayTitle.mid(artistTitleSep+3);
}
/*kdDebug(66666) << "EXTM3U author/artist='" << prop["author"] <<
"', title='" << prop["title"] << "'" << endl;*/
} // END !displayTitle.isEmpty()
} // END if(isExt)
// kdDebug(66666) << k_funcinfo << "adding file '" << prop["url"] << "' to playlist" << endl;
readItem(prop);
prop.clear();
} // END while()
TDEIO::NetAccess::removeTempFile(localFile);
// kdDebug(66666) << k_funcinfo << "END" << endl;
return true;
}
bool PlaylistSaver::saveM3U(const KURL &file, int opt)
{
// kdDebug(66666) << k_funcinfo << "file='" << file.path() << "', opt=" << opt << endl;
bool isExt=(opt==EXTM3U); // easier ;)
TQString local(napp->tempSaveName(file.path()));
TQFile saver(local);
saver.open(IO_ReadWrite | IO_Truncate);
TQTextStream t(&saver);
reset();
PlaylistItem i;
TQStringList props;
// this is more code but otoh faster than checking for isExt inside the loop
if(isExt)
{
t << "#EXTM3U" << '\n';
while ((i=writeItem()))
{
int length = static_cast<int>(((i.property("length")).toInt())/1000);
if(length==0) length=-1; // special value in an EXTM3U file, means "unknown"
KURL u(i.property("url"));
TQString title;
// if a playlistitem is without a tag or ONLY title is set
if((i.property("author").isEmpty() && i.property("title").isEmpty())
|| (i.property("author").isEmpty() && !i.property("title").isEmpty()) )
title = u.filename().left(u.filename().length()-4);
else
title = i.property("author") + " - " + i.property("title");
// kdDebug(66666) << "#EXTINF:"<< TQString::number(length) << "," << title << endl;
t << "#EXTINF:"<< TQString::number(length) << "," << title << '\n';
if (u.isLocalFile())
t << u.path() << '\n';
else
t << u.url() << '\n';
}
}
else
{
while ((i=writeItem()))
{
KURL u(i.property("url"));
if (u.isLocalFile())
t << u.path() << '\n';
else
t << u.url() << '\n';
}
}
saver.close();
TDEIO::NetAccess::upload(local, file, 0L);
saver.remove();
return true;
}
static TQString findNoCase(const TQMap<TQString,TQString> &map, const TQString &key)
{
for (TQMap<TQString,TQString>::ConstIterator i=map.begin(); i!=map.end(); ++i)
{
if (i.key().lower() == key.lower())
return i.data();
}
return 0;
}
bool PlaylistSaver::loadPLS(const KURL &file, int /*opt*/)
{
kdDebug(66666) << k_funcinfo << "file='" << file.path() << endl;
TQString localFile;
if(!TDEIO::NetAccess::download(file, localFile, 0L))
return false;
TQFile checkFile(localFile);
checkFile.open(IO_ReadOnly);
TQTextStream t(&checkFile);
TQString firstLine = t.readLine();
if(firstLine.lower() != "[playlist]")
{
kdDebug(66666) << k_funcinfo << "PLS didn't start with '[playlist]', aborting" << endl;
return false;
}
KSimpleConfig list(localFile, true);
//list.setGroup("playlist");
// some stupid Windows lusers like to be case insensitive
TQStringList groups = list.groupList().grep(TQRegExp("^playlist$", false));
/*
if (!groups.count()) // didn't find "[playlist]", it's not a .pls file
return false;
*/
TQMap<TQString,TQString> group = list.entryMap(groups[0]);
TQString numOfEntries = findNoCase(group, "numberofentries");
if(numOfEntries.isEmpty())
return false;
reset();
unsigned int nEntries = numOfEntries.toInt();
for(unsigned int entry = 1; entry <= nEntries; ++entry )
{
TQString str;
str.sprintf("file%d", entry);
TQString cast = findNoCase(group, str.utf8());
str.sprintf("title%d", entry);
TQString title = findNoCase(group, str.utf8());
// assume that everything in a pls is a streamable file
TQMap<TQString,TQString> map;
KURL url(cast);
if (!url.hasPath())
url.setPath("/");
map["playObject"]="SplayPlayObject";
if (title.isEmpty())
map["title"] = i18n("Stream from %1 (port: %2)").arg( url.host() ).arg( url.port() );
else
map["title"] = i18n("Stream from %1, (ip: %2, port: %3)").arg( title ).arg( url.host() ).arg(url.port() );
map["url"] = map["stream_"]= url.url();
readItem(map);
}
return true;
}
bool PlaylistSaver::savePLS(const KURL &, int)
{
return false;
}
void PlaylistSaver::setGroup(const TQString &)
{
}