these connections work better with the phonon engine
[amarok.git] / src / PlaylistHandler.cpp
bloba2f4aa340cf1a6dec235ba87e479f4c721db25c2
1 // Author: Max Howell (C) Copyright 2003-4
2 // Author: Mark Kretschmann (C) Copyright 2004
3 // Author: Nikolaj Hald Nielsen (C) Copyright 2007
4 // Author: Casey Link (C) Copyright 2007
5 // .ram file support from Kaffeine 0.5, Copyright (C) 2004 by Jürgen Kofler (GPL 2 or later)
6 // .asx file support added by Michael Seiwert Copyright (C) 2006
7 // .asx file support from Kaffeine, Copyright (C) 2004-2005 by Jürgen Kofler (GPL 2 or later)
8 // .smil file support from Kaffeine 0.7
9 // .pls parser (C) Copyright 2005 by Michael Buesch <mbuesch@freenet.de>
10 // .xspf file support added by Mattias Fliesberg <mattias.fliesberg@gmail.com> Copyright (C) 2006
11 // Copyright: See COPYING file that comes with this distribution
17 #define DEBUG_PREFIX "PlaylistHandler"
19 #include "amarokconfig.h"
20 #include "amarok.h"
21 #include "app.h"
22 #include "CollectionManager.h"
23 #include "MainWindow.h"
24 #include "meta/EditCapability.h"
25 #include "meta/proxy/MetaProxy.h"
26 #include "PlaylistHandler.h"
27 #include "playlist/PlaylistModel.h"
28 #include "ContextStatusBar.h"
29 #include "TheInstances.h"
30 #include "xspfplaylist.h"
32 #include <kdirlister.h>
33 #include <KMessageBox>
34 #include <KUrl>
36 #include <QEventLoop>
37 #include <QFile>
38 #include <QtXml>
42 using namespace Meta;
45 PlaylistHandler::PlaylistHandler()
46 : QObject( 0 )
47 , m_format( Unknown )
52 bool PlaylistHandler::isPlaylist(const KUrl & path)
54 const QString ext = Amarok::extension( path.fileName() );
56 if( ext == "m3u" ) return true;
57 if( ext == "pls" ) return true;
58 if( ext == "ram" ) return true;
59 if( ext == "smil") return true;
60 if( ext == "asx" || ext == "wax" ) return true;
61 if( ext == "xml" ) return true;
62 if( ext == "xspf" ) return true;
64 return false;
67 void PlaylistHandler::load(const QString & path)
69 DEBUG_BLOCK
70 debug() << "file: " << path;
72 //check if file is local or remote
73 KUrl url( path );
74 m_path = url.path();
75 m_format = getFormat( url );
77 if ( url.isLocalFile() ) {
79 QFile file( url.path() );
80 if( !file.open( QIODevice::ReadOnly ) ) {
81 debug() << "cannot open file";
82 return;
85 m_contents = QString( file.readAll() );
86 file.close();
88 QTextStream stream;
89 stream.setString( &m_contents );
90 handleByFormat( stream, m_format);
92 } else {
93 downloadPlaylist( url );
98 bool PlaylistHandler::save( Meta::TrackList tracks,
99 const QString &location )
101 KUrl url( location );
102 Format playlistFormat = getFormat( url );
103 switch (playlistFormat) {
104 case M3U:
105 return saveM3u( tracks, location );
106 break;
107 case PLS:
108 return savePls( tracks, location );
109 break;
110 case XSPF:
111 return saveXSPF( tracks, location );
112 break;
113 default:
114 debug() << "Currently unhandled type!";
115 return false;
116 break;
118 return false;
122 PlaylistHandler::Format PlaylistHandler::getFormat( const KUrl &path )
125 const QString ext = Amarok::extension( path.fileName() );
127 if( ext == "m3u" ) return M3U;
128 if( ext == "pls" ) return PLS;
129 if( ext == "ram" ) return RAM;
130 if( ext == "smil") return SMIL;
131 if( ext == "asx" || ext == "wax" ) return ASX;
132 if( ext == "xml" ) return XML;
133 if( ext == "xspf" ) return XSPF;
135 return Unknown;
139 void PlaylistHandler::handleByFormat( QTextStream &stream, Format format)
141 DEBUG_BLOCK
143 switch( format ) {
145 case PLS:
146 loadPls( stream );
147 break;
148 case M3U:
149 loadM3u( stream );
150 break;
151 case RAM:
152 loadRealAudioRam( stream );
153 break;
154 case ASX:
155 loadASX( stream );
156 break;
157 case SMIL:
158 loadSMIL( stream );
159 break;
160 case XSPF:
161 loadXSPF( stream );
162 break;
164 default:
165 debug() << "unknown type!";
166 break;
173 void PlaylistHandler::downloadPlaylist(const KUrl & path)
175 DEBUG_BLOCK
176 m_downloadJob = KIO::storedGet( path );
178 connect( m_downloadJob, SIGNAL( result( KJob * ) ),
179 this, SLOT( downloadComplete( KJob * ) ) );
181 Amarok::ContextStatusBar::instance() ->newProgressOperation( m_downloadJob )
182 .setDescription( i18n( "Downloading Playlist" ) );
186 void PlaylistHandler::downloadComplete(KJob * job)
188 DEBUG_BLOCK
190 if ( !m_downloadJob->error() == 0 )
192 //TODO: error handling here
193 return ;
196 m_contents = m_downloadJob->data();
197 QTextStream stream;
198 stream.setString( &m_contents );
201 handleByFormat( stream, m_format );
203 m_downloadJob->deleteLater();
208 bool
209 PlaylistHandler::loadPls( QTextStream &stream )
211 DEBUG_BLOCK
214 TrackList tracks;
215 TrackPtr currentTrack;
217 // Counted number of "File#=" lines.
218 unsigned int entryCnt = 0;
219 // Value of the "NumberOfEntries=#" line.
220 unsigned int numberOfEntries = 0;
221 // Does the file have a "[playlist]" section? (as it's required by the standard)
222 bool havePlaylistSection = false;
223 QString tmp;
224 QStringList lines;
226 const QRegExp regExp_NumberOfEntries("^NumberOfEntries\\s*=\\s*\\d+$");
227 const QRegExp regExp_File("^File\\d+\\s*=");
228 const QRegExp regExp_Title("^Title\\d+\\s*=");
229 const QRegExp regExp_Length("^Length\\d+\\s*=\\s*\\d+$");
230 const QRegExp regExp_Version("^Version\\s*=\\s*\\d+$");
231 const QString section_playlist("[playlist]");
233 /* Preprocess the input data.
234 * Read the lines into a buffer; Cleanup the line strings;
235 * Count the entries manually and read "NumberOfEntries".
237 while (!stream.atEnd()) {
238 tmp = stream.readLine();
239 tmp = tmp.trimmed();
240 if (tmp.isEmpty())
241 continue;
242 lines.append(tmp);
244 if (tmp.contains(regExp_File)) {
245 entryCnt++;
246 continue;
248 if (tmp == section_playlist) {
249 havePlaylistSection = true;
250 continue;
252 if (tmp.contains(regExp_NumberOfEntries)) {
253 numberOfEntries = tmp.section('=', -1).trimmed().toUInt();
254 continue;
257 if (numberOfEntries != entryCnt) {
258 warning() << ".pls playlist: Invalid \"NumberOfEntries\" value. "
259 << "NumberOfEntries=" << numberOfEntries << " counted="
260 << entryCnt << endl;
261 /* Corrupt file. The "NumberOfEntries" value is
262 * not correct. Fix it by setting it to the manually
263 * counted number and go on parsing.
265 numberOfEntries = entryCnt;
267 if (!numberOfEntries)
268 return true;
270 unsigned int index;
271 bool ok = false;
272 bool inPlaylistSection = false;
274 /* Now iterate through all beautified lines in the buffer
275 * and parse the playlist data.
277 QStringList::const_iterator i = lines.begin(), end = lines.end();
278 for ( ; i != end; ++i) {
279 if (!inPlaylistSection && havePlaylistSection) {
280 /* The playlist begins with the "[playlist]" tag.
281 * Skip everything before this.
283 if ((*i) == section_playlist)
284 inPlaylistSection = true;
285 continue;
287 if ((*i).contains(regExp_File)) {
288 // Have a "File#=XYZ" line.
289 index = loadPls_extractIndex(*i);
290 if (index > numberOfEntries || index == 0)
291 continue;
292 tmp = (*i).section('=', 1).trimmed();
293 currentTrack = Meta::TrackPtr( new MetaProxy::Track( KUrl( tmp ) ) );
294 tracks.append( currentTrack );
295 continue;
297 if ((*i).contains(regExp_Title)) {
298 // Have a "Title#=XYZ" line.
299 index = loadPls_extractIndex(*i);
300 if (index > numberOfEntries || index == 0)
301 continue;
302 tmp = (*i).section('=', 1).trimmed();
304 if ( currentTrack.data() != 0 && currentTrack->is<Meta::EditCapability>() )
306 Meta::EditCapability *ec = currentTrack->as<Meta::EditCapability>();
307 if( ec )
308 ec->setTitle( tmp );
309 delete ec;
311 continue;
313 if ((*i).contains(regExp_Length)) {
314 // Have a "Length#=XYZ" line.
315 index = loadPls_extractIndex(*i);
316 if (index > numberOfEntries || index == 0)
317 continue;
318 tmp = (*i).section('=', 1).trimmed();
319 //tracks.append( KUrl(tmp) );
320 // Q_ASSERT(ok);
321 continue;
323 if ((*i).contains(regExp_NumberOfEntries)) {
324 // Have the "NumberOfEntries=#" line.
325 continue;
327 if ((*i).contains(regExp_Version)) {
328 // Have the "Version=#" line.
329 tmp = (*i).section('=', 1).trimmed();
330 // We only support Version=2
331 if (tmp.toUInt(&ok) != 2)
332 warning() << ".pls playlist: Unsupported version." << endl;
333 // Q_ASSERT(ok);
334 continue;
336 warning() << ".pls playlist: Unrecognized line: \"" << *i << "\"" << endl;
339 debug() << QString( "inserting %1 tracks from playlist" ).arg( tracks.count() );
340 The::playlistModel()->insertOptioned( tracks, Playlist::Append );
342 return true;
345 bool
346 PlaylistHandler::savePls( Meta::TrackList tracks, const QString &location )
348 QFile file( location );
350 if( !file.open( QIODevice::WriteOnly ) )
352 KMessageBox::sorry( MainWindow::self(), i18n( "Cannot write playlist (%1).").arg(location) );
353 return false;
356 KUrl::List urls;
357 QStringList titles;
358 QList<int> lengths;
359 foreach( TrackPtr track, tracks )
361 urls << track->url();
362 titles << track->name();
363 lengths << track->length();
366 QTextStream stream( &file );
367 stream << "[Playlist]\n";
368 stream << "NumberOfEntries=" << tracks.count() << endl;
369 for( int i = 1, n = urls.count(); i < n; ++i )
371 stream << "File" << i << "=";
372 stream << urls[i].path();
373 stream << "\nTitle" << i << "=";
374 stream << titles[i];
375 stream << "\nLength" << i << "=";
376 stream << lengths[i];
377 stream << "\n";
380 stream << "Version=2\n";
381 file.close();
382 return true;
385 unsigned int
386 PlaylistHandler::loadPls_extractIndex( const QString &str ) const
388 /* Extract the index number out of a .pls line.
389 * Example:
390 * loadPls_extractIndex("File2=foobar") == 2
392 bool ok = false;
393 unsigned int ret;
394 QString tmp(str.section('=', 0, 0));
395 tmp.remove(QRegExp("^\\D*"));
396 ret = tmp.trimmed().toUInt(&ok);
397 Q_ASSERT(ok);
398 return ret;
401 bool
402 PlaylistHandler::loadM3u( QTextStream &stream )
404 DEBUG_BLOCK
406 TrackList tracks;
408 const QString directory = m_path.left( m_path.lastIndexOf( '/' ) + 1 );
410 for( QString line; !stream.atEnd(); )
412 line = stream.readLine();
414 if( line.startsWith( "#EXTINF" ) ) {
415 const QString extinf = line.section( ':', 1 );
416 const int length = extinf.section( ',', 0, 0 ).toInt();
417 //b.setTitle( extinf.section( ',', 1 ) );
418 //b.setLength( length <= 0 ? /*MetaBundle::Undetermined HACK*/ -2 : length );
421 else if( !line.startsWith( "#" ) && !line.isEmpty() )
423 // KUrl::isRelativeUrl() expects absolute URLs to start with a protocol, so prepend it if missing
424 QString url = line;
425 if( url.startsWith( "/" ) )
426 url.prepend( "file://" );
428 if( KUrl::isRelativeUrl( url ) ) {
429 KUrl kurl( KUrl( directory + line ) );
430 kurl.cleanPath();
431 debug() << "found track: " << kurl.path();
432 tracks.append( Meta::TrackPtr( new MetaProxy::Track( kurl ) ) );
434 else {
435 tracks.append( Meta::TrackPtr( new MetaProxy::Track( KUrl( line ) ) ) );
436 debug() << "found track: " << line;
439 // Ensure that we always have a title: use the URL as fallback
440 //if( b.title().isEmpty() )
441 // b.setTitle( url );
446 debug() << QString( "inserting %1 tracks from playlist" ).arg( tracks.count() );
447 The::playlistModel()->insertOptioned( tracks, Playlist::Append );
450 return true;
453 bool
454 PlaylistHandler::saveM3u( Meta::TrackList tracks, const QString &location )
456 const bool relative = AmarokConfig::relativePlaylist();
457 if( location.isEmpty() )
458 return false;
460 QFile file( location );
462 if( !file.open( QIODevice::WriteOnly ) )
464 KMessageBox::sorry( MainWindow::self(), i18n( "Cannot write playlist (%1).").arg(location) );
465 return false;
468 QTextStream stream( &file );
469 stream << "#EXTM3U\n";
471 KUrl::List urls;
472 QStringList titles;
473 QList<int> lengths;
474 foreach( TrackPtr track, tracks )
476 urls << track->url();
477 titles << track->name();
478 lengths << track->length();
481 //Port 2.0 is this still necessary?
482 // foreach( KUrl url, urls)
483 // {
484 // if( url.isLocalFile() && QFileInfo( url.path() ).isDir() )
485 // urls += recurse( url );
486 // else
487 // urls += url;
488 // }
490 for( int i = 0, n = urls.count(); i < n; ++i )
492 const KUrl &url = urls[i];
494 if( !titles.isEmpty() && !lengths.isEmpty() )
496 stream << "#EXTINF:";
497 stream << QString::number( lengths[i] );
498 stream << ',';
499 stream << titles[i];
500 stream << '\n';
502 if (url.protocol() == "file" ) {
503 if ( relative ) {
504 const QFileInfo fi(file);
505 stream << KUrl::relativePath(fi.path(), url.path());
506 } else
507 stream << url.path();
508 } else {
509 stream << url.url();
511 stream << "\n";
514 file.close(); // Flushes the file, before we read it
515 // PlaylistBrowser::instance()->addPlaylist( path, 0, true ); //Port 2.0: re add when we have a playlistbrowser
517 return true;
520 bool
521 PlaylistHandler::loadRealAudioRam( QTextStream &stream )
523 DEBUG_BLOCK
525 TrackList tracks;
526 QString url;
527 //while loop adapted from Kaffeine 0.5
528 while (!stream.atEnd())
530 url = stream.readLine();
531 if (url[0] == '#') continue; /* ignore comments */
532 if (url == "--stop--") break; /* stop line */
533 if ((url.left(7) == "rtsp://") || (url.left(6) == "pnm://") || (url.left(7) == "http://"))
535 tracks.append( Meta::TrackPtr( new MetaProxy::Track( KUrl( url ) ) ) );
539 The::playlistModel()->insertOptioned( tracks, Playlist::Append );
540 return true;
543 bool
544 PlaylistHandler::loadSMIL( QTextStream &stream )
546 // adapted from Kaffeine 0.7
547 QDomDocument doc;
548 if( !doc.setContent( stream.readAll() ) )
550 debug() << "Could now read smil playlist";
551 return false;
554 QDomElement root = doc.documentElement();
555 stream.setAutoDetectUnicode( true );
556 stream.setCodec( QTextCodec::codecForName( "UTF-8" ) );
558 if( root.nodeName().toLower() != "smil" )
559 return false;
561 KUrl kurl;
562 QString url;
563 QDomNodeList nodeList;
564 QDomNode node;
565 QDomElement element;
567 TrackList tracks;
569 //audio sources...
570 nodeList = doc.elementsByTagName( "audio" );
571 for( uint i = 0; i < nodeList.count(); i++ )
573 node = nodeList.item(i);
574 url.clear();
575 if( (node.nodeName().toLower() == "audio") && (node.isElement()) )
577 element = node.toElement();
578 if( element.hasAttribute("src") )
579 url = element.attribute("src");
581 else if( element.hasAttribute("Src") )
582 url = element.attribute("Src");
584 else if( element.hasAttribute("SRC") )
585 url = element.attribute("SRC");
587 if( !url.isNull() )
589 tracks.append( Meta::TrackPtr( new MetaProxy::Track( KUrl( url ) ) ) );
593 The::playlistModel()->insertOptioned( tracks, Playlist::Append );
594 return true;
598 bool
599 PlaylistHandler::loadASX( QTextStream &stream )
601 //adapted from Kaffeine 0.7
602 TrackList tracks;
603 QDomDocument doc;
604 QString errorMsg;
605 int errorLine, errorColumn;
606 stream.setCodec( "UTF8" );
608 QString content = stream.readAll();
610 //ASX looks a lot like xml, but doesn't require tags to be case sensitive,
611 //meaning we have to accept things like: <Abstract>...</abstract>
612 //We use a dirty way to achieve this: we make all tags lower case
613 QRegExp ex("(<[/]?[^>]*[A-Z]+[^>]*>)");
614 ex.setCaseSensitivity( Qt::CaseSensitive );
615 while ( (ex.indexIn(content)) != -1 )
616 content.replace(ex.cap( 1 ), ex.cap( 1 ).toLower());
619 if (!doc.setContent(content, &errorMsg, &errorLine, &errorColumn))
621 debug() << "Error loading xml file: " "(" << errorMsg << ")"
622 << " at line " << errorLine << ", column " << errorColumn << endl;
623 return false;
626 QDomElement root = doc.documentElement();
628 QString url;
629 QString title;
630 QString author;
631 QTime length;
632 QString duration;
634 if (root.nodeName().toLower() != "asx") return false;
636 QDomNode node = root.firstChild();
637 QDomNode subNode;
638 QDomElement element;
640 while (!node.isNull())
642 url.clear();
643 title.clear();
644 author.clear();
645 length = QTime();
646 if (node.nodeName().toLower() == "entry")
648 subNode = node.firstChild();
649 while (!subNode.isNull())
651 if ((subNode.nodeName().toLower() == "ref") && (subNode.isElement()) && (url.isNull()))
653 element = subNode.toElement();
654 if (element.hasAttribute("href"))
655 url = element.attribute("href");
656 if (element.hasAttribute("HREF"))
657 url = element.attribute("HREF");
658 if (element.hasAttribute("Href"))
659 url = element.attribute("Href");
660 if (element.hasAttribute("HRef"))
661 url = element.attribute("HRef");
663 if ((subNode.nodeName().toLower() == "duration") && (subNode.isElement()))
665 duration.clear();
666 element = subNode.toElement();
667 if (element.hasAttribute("value"))
668 duration = element.attribute("value");
669 if (element.hasAttribute("Value"))
670 duration = element.attribute("Value");
671 if (element.hasAttribute("VALUE"))
672 duration = element.attribute("VALUE");
674 if (!duration.isNull())
675 length = stringToTime(duration);
678 if ((subNode.nodeName().toLower() == "title") && (subNode.isElement()))
680 title = subNode.toElement().text();
682 if ((subNode.nodeName().toLower() == "author") && (subNode.isElement()))
684 author = subNode.toElement().text();
686 subNode = subNode.nextSibling();
688 if (!url.isEmpty())
690 TrackPtr trackPtr = Meta::TrackPtr( new MetaProxy::Track( KUrl( url ) ) );
691 Meta::EditCapability *ec = trackPtr->as<Meta::EditCapability>();
692 if( ec )
693 ec->setTitle( title );
694 delete ec;
695 tracks.append( trackPtr );
698 node = node.nextSibling();
701 The::playlistModel()->insertOptioned( tracks, Playlist::Append );
702 return true;
705 QTime PlaylistHandler::stringToTime(const QString& timeString)
707 int sec = 0;
708 bool ok = false;
709 QStringList tokens = timeString.split( ':' );
711 sec += tokens[0].toInt(&ok)*3600; //hours
712 sec += tokens[1].toInt(&ok)*60; //minutes
713 sec += tokens[2].toInt(&ok); //secs
715 if (ok)
716 return QTime().addSecs(sec);
717 else
718 return QTime();
721 bool
722 PlaylistHandler::loadXSPF( QTextStream &stream )
724 DEBUG_BLOCK
725 XSPFPlaylist* doc = new XSPFPlaylist( stream );
727 XSPFtrackList trackList = doc->trackList();
729 TrackList tracks;
730 foreach( const XSPFtrack &track, trackList )
732 KUrl location = track.location;
733 QString artist = track.creator;
734 debug() << "TRACK CREATOR IS: " << track.creator;
735 QString title = track.title;
736 QString album = track.album;
739 if( location.isEmpty() || ( location.isLocalFile() && !QFile::exists( location.path() ) ) )
741 TrackPtr trackPtr = Meta::TrackPtr( new MetaProxy::Track( KUrl( location ) ) );
742 tracks.append( trackPtr );
744 else
746 debug() << location << ' ' << artist << ' ' << title << ' ' << album;
748 TrackPtr trackPtr = Meta::TrackPtr( new MetaProxy::Track( KUrl( location ) ) );
749 Meta::EditCapability *ec = trackPtr->as<Meta::EditCapability>();
750 if( ec )
752 debug() << "Have EditCapability";
753 ec->setTitle( title );
754 ec->setArtist( artist );
755 ec->setAlbum( album );
756 ec->setComment( track.annotation );
758 delete ec;
759 tracks.append( trackPtr );
764 delete doc;
766 The::playlistModel()->insertOptioned( tracks, Playlist::Append );
767 return true;
770 bool
771 PlaylistHandler::saveXSPF( Meta::TrackList tracks, const QString &location )
773 if( tracks.isEmpty() )
774 return false;
775 XSPFPlaylist playlist;
777 playlist.setCreator( "Amarok" );
778 playlist.setTitle( tracks[0]->artist()->name() );
780 playlist.setTrackList( tracks );
782 QFile file( location );
783 if( !file.open( QIODevice::WriteOnly ) )
785 KMessageBox::sorry( MainWindow::self(), i18n( "Cannot write playlist (%1).").arg(location) );
786 return false;
789 QTextStream stream ( &file );
791 playlist.save( stream, 2 );
793 file.close();
795 return true;
798 KUrl::List
799 recurse( const KUrl &url )
801 return Amarok::recursiveUrlExpand( url );
804 namespace Amarok
807 //this function (C) Copyright 2003-4 Max Howell, (C) Copyright 2004 Mark Kretschmann
808 KUrl::List
809 recursiveUrlExpand ( const KUrl &url )
811 typedef QMap<QString, KUrl> FileMap;
813 KDirLister lister ( false );
814 lister.setAutoUpdate ( false );
815 lister.setAutoErrorHandlingEnabled ( false, 0 );
816 lister.openUrl ( url );
818 while ( !lister.isFinished() )
819 kapp->processEvents ( QEventLoop::ExcludeUserInput );
821 KFileItemList items = lister.items();
822 KUrl::List urls;
823 FileMap files;
824 foreach ( const KFileItem& it, items )
826 if ( it.isFile() ) { files[it.name() ] = it.url(); continue; }
827 if ( it.isDir() ) urls += recurse ( it.url() );
830 oldForeachType ( FileMap, files )
831 // users often have playlist files that reflect directories
832 // higher up, or stuff in this directory. Don't add them as
833 // it produces double entries
834 if ( !PlaylistHandler::isPlaylist( ( *it ).fileName() ) )
835 urls += *it;
836 return urls;
839 KUrl::List
840 recursiveUrlExpand ( const KUrl::List &list )
842 KUrl::List urls;
843 oldForeachType ( KUrl::List, list )
845 urls += recursiveUrlExpand ( *it );
848 return urls;
851 #include "PlaylistHandler.moc"