// Author: Max Howell (C) Copyright 2003-4 // Author: Mark Kretschmann (C) Copyright 2004 // .ram file support from Kaffeine 0.5, Copyright (C) 2004 by Jürgen Kofler (GPL 2 or later) // .asx file support added by Michael Seiwert Copyright (C) 2006 // .asx file support from Kaffeine, Copyright (C) 2004-2005 by Jürgen Kofler (GPL 2 or later) // .smil file support from Kaffeine 0.7 // .pls parser (C) Copyright 2005 by Michael Buesch // .xspf file support added by Mattias Fliesberg Copyright (C) 2006 // Copyright: See COPYING file that comes with this distribution // ///For pls and m3u specifications see: ///http://forums.winamp.com/showthread.php?s=dbec47f3a05d10a3a77959f17926d39c&threadid=65772 #define DEBUG_PREFIX "PlaylistLoader" #include "amarok.h" #include "collectiondb.h" #include "debug.h" #include "enginecontroller.h" #include "mountpointmanager.h" #include "mydirlister.h" #include "playlist.h" #include "playlistbrowser.h" #include "playlistitem.h" #include "playlistloader.h" #include "statusbar.h" #include "contextbrowser.h" #include "xspfplaylist.h" #include //::recurse() #include //::recurse() #include //::loadPlaylist() #include #include #include #include //::loadPlaylist() #include #include #include //TODO playlists within playlists, local or remote are legal entries in m3u and pls //TODO directories from inside playlists struct XMLData { MetaBundle bundle; int queue; bool stopafter; bool dynamicdisabled; XMLData(): queue(-1), stopafter(false), dynamicdisabled(false) { } }; class TagsEvent : public TQCustomEvent { public: TagsEvent( const TQValueList &x ) : TQCustomEvent( 1001 ), xml( TQDeepCopy >( x ) ) { } TagsEvent( const BundleList &bees ) : TQCustomEvent( 1000 ), bundles( TQDeepCopy( bees ) ) { for( BundleList::Iterator it = bundles.begin(), end = bundles.end(); it != end; ++it ) { (*it).detach(); /// @see MetaBundle for explanation of audioproperties < 0 if( (*it).length() <= 0 || (*it).bitrate() <= 0 ) (*it).readTags( TagLib::AudioProperties::Fast, 0 ); } } TQValueList xml; BundleList bundles; }; UrlLoader::UrlLoader( const KURL::List &urls, TQListViewItem *after, int options ) : ThreadManager::DependentJob( TQT_TQOBJECT(Playlist::instance()), "UrlLoader" ) , m_markerListViewItem( new PlaylistItem( Playlist::instance(), after ) ) , m_playFirstUrl( options & (Playlist::StartPlay | Playlist::DirectPlay) ) , m_coloring( options & Playlist::Colorize ) , m_options( options ) , m_block( "UrlLoader" ) , m_oldQueue( Playlist::instance()->m_nextTracks ) , m_xmlSource( 0 ) { connect( this, TQT_SIGNAL( queueChanged( const PLItemList &, const PLItemList & ) ), Playlist::instance(), TQT_SIGNAL( queueChanged( const PLItemList &, const PLItemList & ) ) ); Playlist::instance()->lock(); // prevent user removing items as this could be bad Amarok::OverrideCursor cursor; setDescription( i18n("Populating playlist") ); Amarok::StatusBar::instance()->newProgressOperation( this ) .setDescription( m_description ) .setStatus( i18n("Preparing") ) .setAbortSlot( this, TQT_SLOT(abort()) ) .setTotalSteps( 100 ); foreachType( KURL::List, urls ) { const KURL url = Amarok::detachedKURL(Amarok::mostLocalURL( *it )); const TQString protocol = url.protocol(); if( protocol == "seek" ) continue; else if( ContextBrowser::hasContextProtocol( url ) ) { DEBUG_BLOCK debug() << "context expandurl" << endl; m_URLs += ContextBrowser::expandURL(Amarok::detachedKURL(url)); } else if( !MetaBundle::isKioUrl( url ) ) { m_URLs += url; } else if( protocol == "file" ) { if( TQFileInfo( url.path() ).isDir() ) m_URLs += recurse( url ); else m_URLs += url; } // Note: remove for kde 4 - we don't need to be hacking around KFileDialog, // it has been fixed for kde 3.5.3 else if( protocol == "media" || url.url().startsWith( "system:/media/" ) ) { TQString path = url.path( -1 ); if( url.url().startsWith( "system:/media/" ) ) path = path.mid( 6 ); // url looks like media:/device/path DCOPRef mediamanager( "kded", "mediamanager" ); TQString device = path.mid( 1 ); // remove first slash const int slash = device.find( '/' ); const TQString filePath = device.mid( slash ); // extract relative path device = device.left( slash ); // extract device DCOPReply reply = mediamanager.call( "properties(TQString)", device ); if( reply.isValid() ) { const TQStringList properties = reply; // properties[6] is the mount point KURL localUrl = KURL( properties[6] + filePath ); // add urls if( TQFileInfo( localUrl.path() ).isDir() ) m_URLs += recurse( localUrl ); else m_URLs += localUrl; } } else if( PlaylistFile::isPlaylistFile( url ) ) { debug() << "remote playlist" << endl; new RemotePlaylistFetcher( url, after, m_options ); m_playFirstUrl = false; } else { // this is the best way I found for recursing if required // and not recusring if not required const KURL::List urls = recurse( url ); // recurse only works on directories, else it swallows the URL if( urls.isEmpty() ) m_URLs += url; else m_URLs += urls; } } } UrlLoader::~UrlLoader() { if( Playlist::instance() ) { Playlist::instance()->unlock(); if( m_markerListViewItem ) delete m_markerListViewItem; } delete m_xmlSource; } bool UrlLoader::doJob() { setProgressTotalSteps( m_URLs.count() ); KURL::List urls; for( for_iterators( KURL::List, m_URLs ); it != end && !isAborted(); ++it ) { const KURL &url = *it; incrementProgress(); switch( PlaylistFile::format( url.fileName() ) ) { case PlaylistFile::XML: loadXml( url ); break; default: { PlaylistFile playlist( url.path() ); if( !playlist.isError() ) TQApplication::postEvent( this, new TagsEvent( playlist.bundles()) ); else m_badURLs += url; } break; case PlaylistFile::NotPlaylist: (EngineController::canDecode( url ) ? urls : m_badURLs) += url; } if( urls.count() == OPTIMUM_BUNDLE_COUNT || it == last ) { TQApplication::postEvent( this, new TagsEvent( CollectionDB::instance()->bundlesByUrls( urls ) ) ); urls.clear(); } } return true; } void UrlLoader::customEvent( TQCustomEvent *e) { //DEBUG_BLOCK #define e static_cast(e) switch( e->type() ) { case 1000: foreachType( BundleList, e->bundles ) { int alreadyOnPlaylist = 0; PlaylistItem *item = 0; if( m_options & (Playlist::Unique | Playlist::Queue) ) { item = Playlist::instance()->m_urlIndex.getFirst( (*it).url() ); } if( item ) alreadyOnPlaylist++; else item = new PlaylistItem( *it, m_markerListViewItem, (*it).exists() ); if( m_options & Playlist::Queue ) Playlist::instance()->queue( item ); if( m_playFirstUrl && (*it).exists() ) { Playlist::instance()->activate( item ); m_playFirstUrl = false; } } break; case 1001: { foreachType( TQValueList, e->xml ) { if( (*it).bundle.isEmpty() ) //safety continue; PlaylistItem* const item = new PlaylistItem( (*it).bundle, m_markerListViewItem ); item->setIsNew( m_coloring ); //TODO scrollbar position //TODO previous tracks queue //TODO current track position, even if user doesn't have resume playback turned on if( (*it).queue >= 0 ) { if( (*it).queue == 0 ) Playlist::instance()->setCurrentTrack( item ); else if( (*it).queue > 0 ) { PLItemList &m_nextTracks = Playlist::instance()->m_nextTracks; int count = m_nextTracks.count(); for( int c = count; c < (*it).queue; c++ ) // Append foo values and replace with correct values later. m_nextTracks.append( item ); m_nextTracks.replace( (*it).queue - 1, item ); } } if( (*it).stopafter ) Playlist::instance()->m_stopAfterTrack = item; item->setFilestatusEnabled( (*it).bundle.exists() ); item->setDynamicEnabled( !( (*it).dynamicdisabled ) ); } break; } default: DependentJob::customEvent( e ); return; } #undef e } void UrlLoader::completeJob() { DEBUG_BLOCK const PLItemList &newQueue = Playlist::instance()->m_nextTracks; TQPtrListIterator it( newQueue ); PLItemList added; for( it.toFirst(); *it; ++it ) if( !m_oldQueue.containsRef( *it ) ) added << (*it); if( !added.isEmpty() ) emit queueChanged( added, PLItemList() ); if ( !m_badURLs.isEmpty() ) { TQString text = i18n("These media could not be loaded into the playlist: " ); debug() << "The following urls were not suitable for the playlist:" << endl; for ( uint it = 0; it < m_badURLs.count(); it++ ) { if( it < 5 ) text += TQString("
%1").arg( m_badURLs[it].prettyURL() ); else if( it == 5 ) text += TQString("
Plus %1 more").arg( m_badURLs.count() - it ); debug() << "\t" << m_badURLs[it] << endl; } Amarok::StatusBar::instance()->shortLongMessage( i18n("Some media could not be loaded (not playable)."), text ); } if( !m_dynamicMode.isEmpty() ) Playlist::instance()->setDynamicMode( PlaylistBrowser::instance()->findDynamicModeByTitle( m_dynamicMode ) ); //synchronous, ie not using eventLoop TQApplication::sendEvent( dependent(), this ); } KURL::List UrlLoader::recurse( const KURL &url ) { typedef TQMap FileMap; KDirLister lister( false ); lister.setAutoUpdate( false ); lister.setAutoErrorHandlingEnabled( false, 0 ); if ( !lister.openURL( url ) ) return KURL::List(); // Fucking KDirLister sometimes hangs on remote media, so we add a timeout const int timeout = 3000; // ms TQTime watchdog; watchdog.start(); while( !lister.isFinished() && !isAborted() && watchdog.elapsed() < timeout ) kapp->eventLoop()->processEvents( TQEventLoop::ExcludeUserInput ); KFileItemList items = lister.items(); //returns TQPtrList, so we MUST only do it once! KURL::List urls; FileMap files; for( KFileItem *item = items.first(); item; item = items.next() ) { if( item->isFile() ) { files[item->name()] = item->url(); continue; } if( item->isDir() ) urls += recurse( item->url() ); } foreachType( FileMap, files ) // users often have playlist files that reflect directories // higher up, or stuff in this directory. Don't add them as // it produces double entries if( !PlaylistFile::isPlaylistFile( (*it).fileName() ) ) urls += *it; return urls; } namespace Amarok { // almost the same as UrlLoader::recurse, but global KURL::List recursiveUrlExpand( const KURL &url, int maxURLs ) { typedef TQMap FileMap; if( url.protocol() != "file" || !TQFileInfo( url.path() ).isDir() ) return KURL::List( url ); MyDirLister lister( false ); lister.setAutoUpdate( false ); lister.setAutoErrorHandlingEnabled( false, 0 ); if ( !lister.openURL( url ) ) return KURL::List(); // Fucking KDirLister sometimes hangs on remote media, so we add a timeout const int timeout = 3000; // ms TQTime watchdog; watchdog.start(); while( !lister.isFinished() && watchdog.elapsed() < timeout ) kapp->eventLoop()->processEvents( TQEventLoop::ExcludeUserInput ); KFileItemList items = lister.items(); //returns TQPtrList, so we MUST only do it once! KURL::List urls; FileMap files; for( KFileItem *item = items.first(); item; item = items.next() ) { if( maxURLs >= 0 && (int)(urls.count() + files.count()) >= maxURLs ) break; if( item->isFile() && !PlaylistFile::isPlaylistFile( item->url().fileName() ) ) { files[item->name()] = item->url(); continue; } if( item->isDir() ) urls += recursiveUrlExpand( item->url(), maxURLs - urls.count() - files.count() ); } foreachType( FileMap, files ) // users often have playlist files that reflect directories // higher up, or stuff in this directory. Don't add them as // it produces double entries urls += *it; return urls; } KURL::List recursiveUrlExpand( const KURL::List &list, int maxURLs ) { KURL::List urls; foreachType( KURL::List, list ) { if( maxURLs >= 0 && (int)urls.count() >= maxURLs ) break; urls += recursiveUrlExpand( *it, maxURLs - urls.count() ); } return urls; } } // Amarok void UrlLoader::loadXml( const KURL &url ) { TQFile file( url.path() ); if( !file.open( IO_ReadOnly ) ) { m_badURLs += url; return; } m_currentURL = url; delete m_xmlSource; m_xmlSource = new TQXmlInputSource( file ); MyXmlLoader loader; connect( &loader, TQT_SIGNAL( newBundle( const MetaBundle&, const XmlAttributeList& ) ), this, TQT_SLOT( slotNewBundle( const MetaBundle&, const XmlAttributeList& ) ) ); connect( &loader, TQT_SIGNAL( playlistInfo( const TQString&, const TQString&, const TQString& ) ), this, TQT_SLOT( slotPlaylistInfo( const TQString&, const TQString&, const TQString& ) ) ); loader.load( m_xmlSource ); if( !m_xml.isEmpty() ) { TQApplication::postEvent( this, new TagsEvent( m_xml ) ); m_xml.clear(); } if( !loader.lastError().isEmpty() ) { Amarok::StatusBar::instance()->longMessageThreadSafe( i18n( //TODO add a link to the path to the playlist "The XML in the playlist was invalid. Please report this as a bug to the Amarok " "developers. Thank you." ), KDE::StatusBar::Error ); ::error() << "[PLAYLISTLOADER]: Error in " << m_currentURL.prettyURL() << ": " << loader.lastError() << endl; } } void UrlLoader::slotNewBundle( const MetaBundle &bundle, const XmlAttributeList &atts ) { XMLData data; data.bundle = TQDeepCopy( bundle ); for( int i = 0, n = atts.count(); i < n; ++i ) { if( atts[i].first == "queue_index" ) { bool ok = true; data.queue = atts[i].second.toInt( &ok ); if( !ok ) data.queue = -1; } else if( atts[i].first == "stop_after" ) data.stopafter = true; else if( atts[i].first == "dynamicdisabled" ) data.dynamicdisabled = true; } data.bundle.checkExists(); m_xml.append( data ); if( m_xml.count() == OPTIMUM_BUNDLE_COUNT ) { TQApplication::postEvent( this, new TagsEvent( m_xml ) ); m_xml.clear(); } } void UrlLoader::slotPlaylistInfo( const TQString &, const TQString &version, const TQString &dynamicMode ) { if( version != Amarok::xmlVersion() ) { Amarok::StatusBar::instance()->longMessageThreadSafe( i18n( "Your last playlist was saved with a different version of Amarok than this one, " "and this version can no longer read it.\n" "You will have to create a new one.\n" "Sorry :(" ) ); static_cast( const_cast( sender() ) )->abort(); //HACK? return; } else m_dynamicMode = dynamicMode; } /// @class PlaylistFile PlaylistFile::PlaylistFile( const TQString &path ) : m_path( path ) { TQFile file( path ); if( !file.open( IO_ReadOnly ) ) { m_error = i18n( "Amarok could not open the file." ); return; } TQTextStream stream( &file ); switch( format( m_path ) ) { case M3U: loadM3u( stream ); break; case PLS: loadPls( stream ); break; case XML: m_error = i18n( "This component of Amarok cannot translate XML playlists." ); return; case RAM: loadRealAudioRam( stream ); break; case ASX: loadASX( stream ); break; case SMIL: loadSMIL( stream ); break; case XSPF: loadXSPF( stream ); break; default: m_error = i18n( "Amarok does not support this playlist format." ); return; } if( m_error.isEmpty() && m_bundles.isEmpty() ) m_error = i18n( "The playlist did not contain any references to files." ); debug() << m_error << endl; } bool PlaylistFile::loadM3u( TQTextStream &stream ) { const TQString directory = m_path.left( m_path.findRev( '/' ) + 1 ); MetaBundle b; for( TQString line; !stream.atEnd(); ) { line = stream.readLine(); if( line.startsWith( "#EXTINF" ) ) { const TQString extinf = line.section( ':', 1 ); const int length = extinf.section( ',', 0, 0 ).toInt(); b.setTitle( extinf.section( ',', 1 ) ); b.setLength( length <= 0 ? /*MetaBundle::Undetermined HACK*/ -2 : length ); } else if( !line.startsWith( "#" ) && !line.isEmpty() ) { // KURL::isRelativeURL() expects absolute URLs to start with a protocol, so prepend it if missing TQString url = line; if( url.startsWith( "/" ) ) url.prepend( "file://" ); if( KURL::isRelativeURL( url ) ) { KURL kurl( KURL::fromPathOrURL( directory + line ) ); kurl.cleanPath(); b.setPath( kurl.path() ); } else { b.setUrl( KURL::fromPathOrURL( line ) ); } // Ensure that we always have a title: use the URL as fallback if( b.title().isEmpty() ) b.setTitle( url ); m_bundles += b; b = MetaBundle(); } } return true; } bool PlaylistFile::loadPls( TQTextStream &stream ) { // Counted number of "File#=" lines. unsigned int entryCnt = 0; // Value of the "NumberOfEntries=#" line. unsigned int numberOfEntries = 0; // Does the file have a "[playlist]" section? (as it's required by the standard) bool havePlaylistSection = false; TQString tmp; TQStringList lines; const TQRegExp regExp_NumberOfEntries("^NumberOfEntries\\s*=\\s*\\d+$"); const TQRegExp regExp_File("^File\\d+\\s*="); const TQRegExp regExp_Title("^Title\\d+\\s*="); const TQRegExp regExp_Length("^Length\\d+\\s*=\\s*\\d+$"); const TQRegExp regExp_Version("^Version\\s*=\\s*\\d+$"); const TQString section_playlist("[playlist]"); /* Preprocess the input data. * Read the lines into a buffer; Cleanup the line strings; * Count the entries manually and read "NumberOfEntries". */ while (!stream.atEnd()) { tmp = stream.readLine(); tmp = tmp.stripWhiteSpace(); if (tmp.isEmpty()) continue; lines.append(tmp); if (tmp.contains(regExp_File)) { entryCnt++; continue; } if (tmp == section_playlist) { havePlaylistSection = true; continue; } if (tmp.contains(regExp_NumberOfEntries)) { numberOfEntries = TQString(tmp.section('=', -1)).stripWhiteSpace().toUInt(); continue; } } if (numberOfEntries != entryCnt) { warning() << ".pls playlist: Invalid \"NumberOfEntries\" value. " << "NumberOfEntries=" << numberOfEntries << " counted=" << entryCnt << endl; /* Corrupt file. The "NumberOfEntries" value is * not correct. Fix it by setting it to the manually * counted number and go on parsing. */ numberOfEntries = entryCnt; } if (!numberOfEntries) return true; unsigned int index; bool ok = false; bool inPlaylistSection = false; Q_ASSERT(m_bundles.isEmpty()); m_bundles.insert(m_bundles.begin(), numberOfEntries, MetaBundle()); /* Now iterate through all beautified lines in the buffer * and parse the playlist data. */ TQStringList::const_iterator i = lines.begin(), end = lines.end(); for ( ; i != end; ++i) { if (!inPlaylistSection && havePlaylistSection) { /* The playlist begins with the "[playlist]" tag. * Skip everything before this. */ if ((*i) == section_playlist) inPlaylistSection = true; continue; } if ((*i).contains(regExp_File)) { // Have a "File#=XYZ" line. index = loadPls_extractIndex(*i); if (index > numberOfEntries || index == 0) continue; tmp = TQString((*i).section('=', 1)).stripWhiteSpace(); m_bundles[index - 1].setUrl(KURL::fromPathOrURL(tmp)); // Ensure that if the entry has no title, we show at least the URL as title m_bundles[index - 1].setTitle(tmp); continue; } if ((*i).contains(regExp_Title)) { // Have a "Title#=XYZ" line. index = loadPls_extractIndex(*i); if (index > numberOfEntries || index == 0) continue; tmp = TQString((*i).section('=', 1)).stripWhiteSpace(); m_bundles[index - 1].setTitle(tmp); continue; } if ((*i).contains(regExp_Length)) { // Have a "Length#=XYZ" line. index = loadPls_extractIndex(*i); if (index > numberOfEntries || index == 0) continue; tmp = TQString((*i).section('=', 1)).stripWhiteSpace(); m_bundles[index - 1].setLength(tmp.toInt(&ok)); Q_ASSERT(ok); continue; } if ((*i).contains(regExp_NumberOfEntries)) { // Have the "NumberOfEntries=#" line. continue; } if ((*i).contains(regExp_Version)) { // Have the "Version=#" line. tmp = TQString((*i).section('=', 1)).stripWhiteSpace(); // We only support Version=2 if (tmp.toUInt(&ok) != 2) warning() << ".pls playlist: Unsupported version." << endl; Q_ASSERT(ok); continue; } warning() << ".pls playlist: Unrecognized line: \"" << *i << "\"" << endl; } return true; } bool PlaylistFile::loadXSPF( TQTextStream &stream ) { XSPFPlaylist* doc = new XSPFPlaylist( stream ); XSPFtrackList trackList = doc->trackList(); foreachType( XSPFtrackList, trackList ) { KURL location = (*it).location; TQString artist = (*it).creator; TQString title = (*it).title; TQString album = (*it).album; if( location.isEmpty() || ( location.isLocalFile() && !TQFile::exists( location.url() ) ) ) { QueryBuilder qb; qb.addMatch( QueryBuilder::tabArtist, QueryBuilder::valName, artist ); qb.addMatch( QueryBuilder::tabSong, QueryBuilder::valTitle, title ); if( !album.isEmpty() ) qb.addMatch( QueryBuilder::valName, album ); qb.addReturnValue( QueryBuilder::tabSong, QueryBuilder::valURL ); TQStringList values = qb.run(); if( values.isEmpty() ) continue; MetaBundle b( values[0] ); m_bundles += b; } else { debug() << location << ' ' << artist << ' ' << title << ' ' << album << endl; MetaBundle b; b.setUrl( location ); b.setArtist( artist ); b.setTitle( title ); b.setAlbum( album ); b.setComment( (*it).annotation ); b.setLength( (*it).duration / 1000 ); m_bundles += b; } } m_title = doc->title(); return true; } unsigned int PlaylistFile::loadPls_extractIndex( const TQString &str ) const { /* Extract the index number out of a .pls line. * Example: * loadPls_extractIndex("File2=foobar") == 2 */ bool ok = false; unsigned int ret; TQString tmp(str.section('=', 0, 0)); tmp.remove(TQRegExp("^\\D*")); ret = tmp.stripWhiteSpace().toUInt(&ok); Q_ASSERT(ok); return ret; } bool PlaylistFile::loadRealAudioRam( TQTextStream &stream ) { MetaBundle b; TQString url; //while loop adapted from Kaffeine 0.5 while (!stream.atEnd()) { url = stream.readLine(); if (url[0] == '#') continue; /* ignore comments */ if (url == "--stop--") break; /* stop line */ if ((url.left(7) == "rtsp://") || (url.left(6) == "pnm://") || (url.left(7) == "http://")) { b.setUrl(KURL(url)); m_bundles += b; b = MetaBundle(); } } return true; } bool PlaylistFile::loadASX( TQTextStream &stream ) { //adapted from Kaffeine 0.7 MetaBundle b; TQDomDocument doc; TQString errorMsg; int errorLine, errorColumn; stream.setEncoding( TQTextStream::UnicodeUTF8 ); TQString content = stream.read(); //ASX looks a lot like xml, but doesn't require tags to be case sensitive, //meaning we have to accept things like: ... //We use a dirty way to achieve this: we make all tags lower case TQRegExp ex("(<[/]?[^>]*[A-Z]+[^>]*>)"); ex.setCaseSensitive(true); while ( (ex.search(content)) != -1 ) content.replace(ex.cap( 1 ), TQString(ex.cap( 1 )).lower()); if (!doc.setContent(content, &errorMsg, &errorLine, &errorColumn)) { debug() << "Error loading xml file: " "(" << errorMsg << ")" << " at line " << errorLine << ", column " << errorColumn << endl; return false; } TQDomElement root = doc.documentElement(); TQString url; TQString title; TQString author; TQTime length; TQString duration; if (root.nodeName().lower() != "asx") return false; TQDomNode node = root.firstChild(); TQDomNode subNode; TQDomElement element; while (!node.isNull()) { url = TQString(); title = TQString(); author = TQString(); length = TQTime(); if (node.nodeName().lower() == "entry") { subNode = node.firstChild(); while (!subNode.isNull()) { if ((subNode.nodeName().lower() == "ref") && (subNode.isElement()) && (url.isNull())) { element = subNode.toElement(); if (element.hasAttribute("href")) url = element.attribute("href"); if (element.hasAttribute("HREF")) url = element.attribute("HREF"); if (element.hasAttribute("Href")) url = element.attribute("Href"); if (element.hasAttribute("HRef")) url = element.attribute("HRef"); } if ((subNode.nodeName().lower() == "duration") && (subNode.isElement())) { duration = TQString(); element = subNode.toElement(); if (element.hasAttribute("value")) duration = element.attribute("value"); if (element.hasAttribute("Value")) duration = element.attribute("Value"); if (element.hasAttribute("VALUE")) duration = element.attribute("VALUE"); if (!duration.isNull()) length = PlaylistFile::stringToTime(duration); } if ((subNode.nodeName().lower() == "title") && (subNode.isElement())) { title = subNode.toElement().text(); } if ((subNode.nodeName().lower() == "author") && (subNode.isElement())) { author = subNode.toElement().text(); } subNode = subNode.nextSibling(); } if (!url.isNull()) { if (title.isNull()) title = url; b.setUrl(KURL(url)); m_bundles += b; b = MetaBundle(); } } node = node.nextSibling(); } return true; } bool PlaylistFile::loadSMIL( TQTextStream &stream ) { // adapted from Kaffeine 0.7 TQDomDocument doc; if( !doc.setContent( stream.read() ) ) { debug() << "Could now read smil playlist" << endl; return false; } TQDomElement root = doc.documentElement(); stream.setEncoding ( TQTextStream::UnicodeUTF8 ); if( root.nodeName().lower() != "smil" ) return false; KURL kurl; TQString url; TQDomNodeList nodeList; TQDomNode node; TQDomElement element; //audio sources... nodeList = doc.elementsByTagName( "audio" ); for( uint i = 0; i < nodeList.count(); i++ ) { MetaBundle b; node = nodeList.item(i); url = TQString(); if( (node.nodeName().lower() == "audio") && (node.isElement()) ) { element = node.toElement(); if( element.hasAttribute("src") ) url = element.attribute("src"); else if( element.hasAttribute("Src") ) url = element.attribute("Src"); else if( element.hasAttribute("SRC") ) url = element.attribute("SRC"); } if( !url.isNull() ) { b.setUrl( url ); m_bundles += b; } } return true; } /// @class RemotePlaylistFetcher #include #include #include RemotePlaylistFetcher::RemotePlaylistFetcher( const KURL &source, TQListViewItem *after, int options ) : TQObject( Playlist::instance()->qscrollview() ) , m_source( source ) , m_after( after ) , m_playFirstUrl( options & (Playlist::StartPlay | Playlist::DirectPlay) ) , m_options( options ) { //We keep the extension so the UrlLoader knows what file type it is const TQString path = source.path(); m_temp = new KTempFile( TQString() /*use default prefix*/, path.mid( path.findRev( '.' ) ) ); m_temp->setAutoDelete( true ); m_destination.setPath( m_temp->name() ); KIO::Job *job = KIO::file_copy( m_source, m_destination, -1, /* permissions, this means "do what you think" */ true, /* overwrite */ false, /* resume download */ false ); /* don't show stupid UIServer dialog */ Amarok::StatusBar::instance()->newProgressOperation( job ) .setDescription( i18n("Retrieving Playlist") ); connect( job, TQT_SIGNAL(result( KIO::Job* )), TQT_SLOT(result( KIO::Job* )) ); Playlist::instance()->lock(); } RemotePlaylistFetcher::~RemotePlaylistFetcher() { Playlist::instance()->unlock(); delete m_temp; } void RemotePlaylistFetcher::result( KIO::Job *job ) { if( job->error() ) { error() << "Couldn't download remote playlist\n"; deleteLater(); } else { debug() << "Playlist was downloaded successfully\n"; UrlLoader *loader = new UrlLoader( m_destination, m_after, m_options ); ThreadManager::instance()->queueJob( loader ); // we mustn't get deleted until the loader is finished // or the playlist we downloaded will be deleted before // it can be parsed! loader->insertChild( this ); } } /// @class SqlLoader SqlLoader::SqlLoader( const TQString &sql, TQListViewItem *after, int options ) : UrlLoader( KURL::List(), after, options ) , m_sql( TQDeepCopy( sql ) ) { // Ovy: just until we make sure every SQL query from dynamic playlists is handled // correctly debug() << "Sql loader: query is: " << sql << "\n"; } bool SqlLoader::doJob() { DEBUG_BLOCK const TQStringList values = CollectionDB::instance()->query( m_sql ); setProgressTotalSteps( values.count() ); BundleList bundles; uint x = 0; for( for_iterators( TQStringList, values ); it != end && !isAborted(); ++it ) { setProgress( x += QueryBuilder::dragFieldCount ); bundles += CollectionDB::instance()->bundleFromQuery( &it ); if( bundles.count() == OPTIMUM_BUNDLE_COUNT || it == last ) { TQApplication::postEvent( this, new TagsEvent( bundles ) ); bundles.clear(); } } setProgress100Percent(); return true; } TQTime PlaylistFile::stringToTime(const TQString& timeString) { int sec = 0; bool ok = false; TQStringList tokens = TQStringList::split(':',timeString); sec += tokens[0].toInt(&ok)*3600; //hours sec += tokens[1].toInt(&ok)*60; //minutes sec += tokens[2].toInt(&ok); //secs if (ok) return TQTime().addSecs(sec); else return TQTime(); } bool MyXmlLoader::startElement( const TQString &a, const TQString &name, const TQString &b, const TQXmlAttributes &atts ) { if( name == "playlist" ) { TQString product, version, dynamic; for( int i = 0, n = atts.count(); i < n; ++i ) { if( atts.localName( i ) == "product" ) product = atts.value( i ); else if( atts.localName( i ) == "version" ) version = atts.value( i ); else if( atts.localName( i ) == "dynamicMode" ) dynamic = atts.value( i ); } emit playlistInfo( product, version, dynamic ); return !m_aborted; } else return XmlLoader::startElement( a, name, b, atts ); } #include "playlistloader.moc"