add mp3 and ogg torrent url info to JamendoAlbum
[amarok.git] / src / PlaylistHandler.cpp
blob416f7a1fbbb9cfe215d1dbd571dee737eea3e7fc
1 // Author: Max Howell (C) Copyright 2003-4
2 // Author: Mark Kretschmann (C) Copyright 2004
3 // Author: Nikolaj Hald Nielsen (C) Copyright 2007
4 // .ram file support from Kaffeine 0.5, Copyright (C) 2004 by Jürgen Kofler (GPL 2 or later)
5 // .asx file support added by Michael Seiwert Copyright (C) 2006
6 // .asx file support from Kaffeine, Copyright (C) 2004-2005 by Jürgen Kofler (GPL 2 or later)
7 // .smil file support from Kaffeine 0.7
8 // .pls parser (C) Copyright 2005 by Michael Buesch <mbuesch@freenet.de>
9 // .xspf file support added by Mattias Fliesberg <mattias.fliesberg@gmail.com> Copyright (C) 2006
10 // Copyright: See COPYING file that comes with this distribution
16 #define DEBUG_PREFIX "PlaylistHandler"
18 #include "amarokconfig.h"
19 #include "app.h"
20 #include "CollectionManager.h"
21 #include "MainWindow.h"
22 #include "meta/EditCapability.h"
23 #include "meta/proxy/MetaProxy.h"
24 #include "PlaylistHandler.h"
25 #include "playlist/PlaylistModel.h"
26 #include "statusbar.h"
27 #include "TheInstances.h"
28 #include "xspfplaylist.h"
30 #include <KMessageBox>
31 #include <KUrl>
33 #include <QFile>
34 #include <QtXml>
37 using namespace Meta;
40 PlaylistHandler::PlaylistHandler()
41 : QObject( 0 )
42 , m_format( Unknown )
47 bool PlaylistHandler::isPlaylist(const KUrl & path)
49 return getFormat( path ) != Unknown;
52 void PlaylistHandler::load(const QString & path)
54 DEBUG_BLOCK
55 debug() << "file: " << path;
57 //check if file is local or remote
58 KUrl url( path );
59 m_path = url.path();
60 m_format = getFormat( url );
62 if ( url.isLocalFile() ) {
64 QFile file( url.path() );
65 if( !file.open( QIODevice::ReadOnly ) ) {
66 debug() << "cannot open file";
67 return;
70 m_contents = QString( file.readAll() );
71 file.close();
73 QTextStream stream;
74 stream.setString( &m_contents );
75 handleByFormat( stream, m_format);
77 } else {
78 downloadPlaylist( url );
83 bool PlaylistHandler::save( Meta::TrackList tracks,
84 const QString &location )
86 KUrl url( location );
87 Format playlistFormat = getFormat( url );
88 switch (playlistFormat) {
89 case M3U:
90 return saveM3u( tracks, location );
91 break;
92 case PLS:
93 return savePls( tracks, location );
94 break;
95 case XSPF:
96 return saveXSPF( tracks, location );
97 break;
98 default:
99 debug() << "Currently unhandled type!";
100 return false;
101 break;
103 return false;
107 PlaylistHandler::Format PlaylistHandler::getFormat( const KUrl &path )
110 const QString ext = Amarok::extension( path.fileName() );
112 if( ext == "m3u" ) return M3U;
113 if( ext == "pls" ) return PLS;
114 if( ext == "ram" ) return RAM;
115 if( ext == "smil") return SMIL;
116 if( ext == "asx" || ext == "wax" ) return ASX;
117 if( ext == "xml" ) return XML;
118 if( ext == "xspf" ) return XSPF;
120 return Unknown;
124 void PlaylistHandler::handleByFormat( QTextStream &stream, Format format)
126 DEBUG_BLOCK
128 switch( format ) {
130 case PLS:
131 loadPls( stream );
132 break;
133 case M3U:
134 loadM3u( stream );
135 break;
136 case RAM:
137 loadRealAudioRam( stream );
138 break;
139 case ASX:
140 loadASX( stream );
141 break;
142 case SMIL:
143 loadSMIL( stream );
144 break;
145 case XSPF:
146 loadXSPF( stream );
147 break;
149 default:
150 debug() << "unknown type!";
151 break;
158 void PlaylistHandler::downloadPlaylist(const KUrl & path)
160 DEBUG_BLOCK
161 m_downloadJob = KIO::storedGet( path );
163 connect( m_downloadJob, SIGNAL( result( KJob * ) ),
164 this, SLOT( downloadComplete( KJob * ) ) );
166 Amarok::StatusBar::instance() ->newProgressOperation( m_downloadJob )
167 .setDescription( i18n( "Downloading Playlist" ) );
171 void PlaylistHandler::downloadComplete(KJob * job)
173 DEBUG_BLOCK
175 if ( !m_downloadJob->error() == 0 )
177 //TODO: error handling here
178 return ;
181 m_contents = m_downloadJob->data();
182 QTextStream stream;
183 stream.setString( &m_contents );
186 handleByFormat( stream, m_format );
188 m_downloadJob->deleteLater();
193 bool
194 PlaylistHandler::loadPls( QTextStream &stream )
196 DEBUG_BLOCK
199 TrackList tracks;
200 TrackPtr currentTrack;
202 // Counted number of "File#=" lines.
203 unsigned int entryCnt = 0;
204 // Value of the "NumberOfEntries=#" line.
205 unsigned int numberOfEntries = 0;
206 // Does the file have a "[playlist]" section? (as it's required by the standard)
207 bool havePlaylistSection = false;
208 QString tmp;
209 QStringList lines;
211 const QRegExp regExp_NumberOfEntries("^NumberOfEntries\\s*=\\s*\\d+$");
212 const QRegExp regExp_File("^File\\d+\\s*=");
213 const QRegExp regExp_Title("^Title\\d+\\s*=");
214 const QRegExp regExp_Length("^Length\\d+\\s*=\\s*\\d+$");
215 const QRegExp regExp_Version("^Version\\s*=\\s*\\d+$");
216 const QString section_playlist("[playlist]");
218 /* Preprocess the input data.
219 * Read the lines into a buffer; Cleanup the line strings;
220 * Count the entries manually and read "NumberOfEntries".
222 while (!stream.atEnd()) {
223 tmp = stream.readLine();
224 tmp = tmp.trimmed();
225 if (tmp.isEmpty())
226 continue;
227 lines.append(tmp);
229 if (tmp.contains(regExp_File)) {
230 entryCnt++;
231 continue;
233 if (tmp == section_playlist) {
234 havePlaylistSection = true;
235 continue;
237 if (tmp.contains(regExp_NumberOfEntries)) {
238 numberOfEntries = tmp.section('=', -1).trimmed().toUInt();
239 continue;
242 if (numberOfEntries != entryCnt) {
243 warning() << ".pls playlist: Invalid \"NumberOfEntries\" value. "
244 << "NumberOfEntries=" << numberOfEntries << " counted="
245 << entryCnt << endl;
246 /* Corrupt file. The "NumberOfEntries" value is
247 * not correct. Fix it by setting it to the manually
248 * counted number and go on parsing.
250 numberOfEntries = entryCnt;
252 if (!numberOfEntries)
253 return true;
255 unsigned int index;
256 bool ok = false;
257 bool inPlaylistSection = false;
259 /* Now iterate through all beautified lines in the buffer
260 * and parse the playlist data.
262 QStringList::const_iterator i = lines.begin(), end = lines.end();
263 for ( ; i != end; ++i) {
264 if (!inPlaylistSection && havePlaylistSection) {
265 /* The playlist begins with the "[playlist]" tag.
266 * Skip everything before this.
268 if ((*i) == section_playlist)
269 inPlaylistSection = true;
270 continue;
272 if ((*i).contains(regExp_File)) {
273 // Have a "File#=XYZ" line.
274 index = loadPls_extractIndex(*i);
275 if (index > numberOfEntries || index == 0)
276 continue;
277 tmp = (*i).section('=', 1).trimmed();
278 currentTrack = Meta::TrackPtr( new MetaProxy::Track( KUrl( tmp ) ) );
279 tracks.append( currentTrack );
280 continue;
282 if ((*i).contains(regExp_Title)) {
283 // Have a "Title#=XYZ" line.
284 index = loadPls_extractIndex(*i);
285 if (index > numberOfEntries || index == 0)
286 continue;
287 tmp = (*i).section('=', 1).trimmed();
289 if ( currentTrack.data() != 0 && currentTrack->is<Meta::EditCapability>() )
291 Meta::EditCapability *ec = currentTrack->as<Meta::EditCapability>();
292 if( ec )
293 ec->setTitle( tmp );
294 delete ec;
296 continue;
298 if ((*i).contains(regExp_Length)) {
299 // Have a "Length#=XYZ" line.
300 index = loadPls_extractIndex(*i);
301 if (index > numberOfEntries || index == 0)
302 continue;
303 tmp = (*i).section('=', 1).trimmed();
304 //tracks.append( KUrl(tmp) );
305 // Q_ASSERT(ok);
306 continue;
308 if ((*i).contains(regExp_NumberOfEntries)) {
309 // Have the "NumberOfEntries=#" line.
310 continue;
312 if ((*i).contains(regExp_Version)) {
313 // Have the "Version=#" line.
314 tmp = (*i).section('=', 1).trimmed();
315 // We only support Version=2
316 if (tmp.toUInt(&ok) != 2)
317 warning() << ".pls playlist: Unsupported version." << endl;
318 // Q_ASSERT(ok);
319 continue;
321 warning() << ".pls playlist: Unrecognized line: \"" << *i << "\"" << endl;
324 debug() << QString( "inserting %1 tracks from playlist" ).arg( tracks.count() );
325 The::playlistModel()->insertOptioned( tracks, Playlist::Append );
327 return true;
330 bool
331 PlaylistHandler::savePls( Meta::TrackList tracks, const QString &location )
333 QFile file( location );
335 if( !file.open( QIODevice::WriteOnly ) )
337 KMessageBox::sorry( MainWindow::self(), i18n( "Cannot write playlist (%1).").arg(location) );
338 return false;
341 KUrl::List urls;
342 QStringList titles;
343 QList<int> lengths;
344 foreach( TrackPtr track, tracks )
346 urls << track->url();
347 titles << track->name();
348 lengths << track->length();
351 QTextStream stream( &file );
352 stream << "[Playlist]\n";
353 stream << "NumberOfEntries=" << tracks.count() << endl;
354 for( int i = 1, n = urls.count(); i < n; ++i )
356 stream << "File" << i << "=";
357 stream << urls[i].path();
358 stream << "\nTitle" << i << "=";
359 stream << titles[i];
360 stream << "\nLength" << i << "=";
361 stream << lengths[i];
362 stream << "\n";
365 stream << "Version=2\n";
366 file.close();
367 return true;
370 unsigned int
371 PlaylistHandler::loadPls_extractIndex( const QString &str ) const
373 /* Extract the index number out of a .pls line.
374 * Example:
375 * loadPls_extractIndex("File2=foobar") == 2
377 bool ok = false;
378 unsigned int ret;
379 QString tmp(str.section('=', 0, 0));
380 tmp.remove(QRegExp("^\\D*"));
381 ret = tmp.trimmed().toUInt(&ok);
382 Q_ASSERT(ok);
383 return ret;
386 bool
387 PlaylistHandler::loadM3u( QTextStream &stream )
389 DEBUG_BLOCK
391 TrackList tracks;
393 const QString directory = m_path.left( m_path.lastIndexOf( '/' ) + 1 );
395 for( QString line; !stream.atEnd(); )
397 line = stream.readLine();
399 if( line.startsWith( "#EXTINF" ) ) {
400 const QString extinf = line.section( ':', 1 );
401 const int length = extinf.section( ',', 0, 0 ).toInt();
402 //b.setTitle( extinf.section( ',', 1 ) );
403 //b.setLength( length <= 0 ? /*MetaBundle::Undetermined HACK*/ -2 : length );
406 else if( !line.startsWith( "#" ) && !line.isEmpty() )
408 // KUrl::isRelativeUrl() expects absolute URLs to start with a protocol, so prepend it if missing
409 QString url = line;
410 if( url.startsWith( "/" ) )
411 url.prepend( "file://" );
413 if( KUrl::isRelativeUrl( url ) ) {
414 KUrl kurl( KUrl( directory + line ) );
415 kurl.cleanPath();
416 debug() << "found track: " << kurl.path();
417 tracks.append( Meta::TrackPtr( new MetaProxy::Track( kurl ) ) );
419 else {
420 tracks.append( Meta::TrackPtr( new MetaProxy::Track( KUrl( line ) ) ) );
421 debug() << "found track: " << line;
424 // Ensure that we always have a title: use the URL as fallback
425 //if( b.title().isEmpty() )
426 // b.setTitle( url );
431 debug() << QString( "inserting %1 tracks from playlist" ).arg( tracks.count() );
432 The::playlistModel()->insertOptioned( tracks, Playlist::Append );
435 return true;
438 bool
439 PlaylistHandler::saveM3u( Meta::TrackList tracks, const QString &location )
441 const bool relative = AmarokConfig::relativePlaylist();
442 if( location.isEmpty() )
443 return false;
445 QFile file( location );
447 if( !file.open( QIODevice::WriteOnly ) )
449 KMessageBox::sorry( MainWindow::self(), i18n( "Cannot write playlist (%1).").arg(location) );
450 return false;
453 QTextStream stream( &file );
454 stream << "#EXTM3U\n";
456 KUrl::List urls;
457 QStringList titles;
458 QList<int> lengths;
459 foreach( TrackPtr track, tracks )
461 urls << track->url();
462 titles << track->name();
463 lengths << track->length();
466 //Port 2.0 is this still necessary?
467 // foreach( KUrl url, urls)
468 // {
469 // if( url.isLocalFile() && QFileInfo( url.path() ).isDir() )
470 // urls += recurse( url );
471 // else
472 // urls += url;
473 // }
475 for( int i = 0, n = urls.count(); i < n; ++i )
477 const KUrl &url = urls[i];
479 if( !titles.isEmpty() && !lengths.isEmpty() )
481 stream << "#EXTINF:";
482 stream << QString::number( lengths[i] );
483 stream << ',';
484 stream << titles[i];
485 stream << '\n';
487 if (url.protocol() == "file" ) {
488 if ( relative ) {
489 const QFileInfo fi(file);
490 stream << KUrl::relativePath(fi.path(), url.path());
491 } else
492 stream << url.path();
493 } else {
494 stream << url.url();
496 stream << "\n";
499 file.close(); // Flushes the file, before we read it
500 // PlaylistBrowser::instance()->addPlaylist( path, 0, true ); //Port 2.0: re add when we have a playlistbrowser
502 return true;
505 bool
506 PlaylistHandler::loadRealAudioRam( QTextStream &stream )
508 DEBUG_BLOCK
510 TrackList tracks;
511 QString url;
512 //while loop adapted from Kaffeine 0.5
513 while (!stream.atEnd())
515 url = stream.readLine();
516 if (url[0] == '#') continue; /* ignore comments */
517 if (url == "--stop--") break; /* stop line */
518 if ((url.left(7) == "rtsp://") || (url.left(6) == "pnm://") || (url.left(7) == "http://"))
520 tracks.append( Meta::TrackPtr( new MetaProxy::Track( KUrl( url ) ) ) );
524 The::playlistModel()->insertOptioned( tracks, Playlist::Append );
525 return true;
528 bool
529 PlaylistHandler::loadSMIL( QTextStream &stream )
531 // adapted from Kaffeine 0.7
532 QDomDocument doc;
533 if( !doc.setContent( stream.readAll() ) )
535 debug() << "Could now read smil playlist";
536 return false;
539 QDomElement root = doc.documentElement();
540 stream.setAutoDetectUnicode( true );
541 stream.setCodec( QTextCodec::codecForName( "UTF-8" ) );
543 if( root.nodeName().toLower() != "smil" )
544 return false;
546 KUrl kurl;
547 QString url;
548 QDomNodeList nodeList;
549 QDomNode node;
550 QDomElement element;
552 TrackList tracks;
554 //audio sources...
555 nodeList = doc.elementsByTagName( "audio" );
556 for( uint i = 0; i < nodeList.count(); i++ )
558 node = nodeList.item(i);
559 url.clear();
560 if( (node.nodeName().toLower() == "audio") && (node.isElement()) )
562 element = node.toElement();
563 if( element.hasAttribute("src") )
564 url = element.attribute("src");
566 else if( element.hasAttribute("Src") )
567 url = element.attribute("Src");
569 else if( element.hasAttribute("SRC") )
570 url = element.attribute("SRC");
572 if( !url.isNull() )
574 tracks.append( Meta::TrackPtr( new MetaProxy::Track( KUrl( url ) ) ) );
578 The::playlistModel()->insertOptioned( tracks, Playlist::Append );
579 return true;
583 bool
584 PlaylistHandler::loadASX( QTextStream &stream )
586 //adapted from Kaffeine 0.7
587 TrackList tracks;
588 QDomDocument doc;
589 QString errorMsg;
590 int errorLine, errorColumn;
591 stream.setCodec( "UTF8" );
593 QString content = stream.readAll();
595 //ASX looks a lot like xml, but doesn't require tags to be case sensitive,
596 //meaning we have to accept things like: <Abstract>...</abstract>
597 //We use a dirty way to achieve this: we make all tags lower case
598 QRegExp ex("(<[/]?[^>]*[A-Z]+[^>]*>)");
599 ex.setCaseSensitivity( Qt::CaseSensitive );
600 while ( (ex.indexIn(content)) != -1 )
601 content.replace(ex.cap( 1 ), ex.cap( 1 ).toLower());
604 if (!doc.setContent(content, &errorMsg, &errorLine, &errorColumn))
606 debug() << "Error loading xml file: " "(" << errorMsg << ")"
607 << " at line " << errorLine << ", column " << errorColumn << endl;
608 return false;
611 QDomElement root = doc.documentElement();
613 QString url;
614 QString title;
615 QString author;
616 QTime length;
617 QString duration;
619 if (root.nodeName().toLower() != "asx") return false;
621 QDomNode node = root.firstChild();
622 QDomNode subNode;
623 QDomElement element;
625 while (!node.isNull())
627 url.clear();
628 title.clear();
629 author.clear();
630 length = QTime();
631 if (node.nodeName().toLower() == "entry")
633 subNode = node.firstChild();
634 while (!subNode.isNull())
636 if ((subNode.nodeName().toLower() == "ref") && (subNode.isElement()) && (url.isNull()))
638 element = subNode.toElement();
639 if (element.hasAttribute("href"))
640 url = element.attribute("href");
641 if (element.hasAttribute("HREF"))
642 url = element.attribute("HREF");
643 if (element.hasAttribute("Href"))
644 url = element.attribute("Href");
645 if (element.hasAttribute("HRef"))
646 url = element.attribute("HRef");
648 if ((subNode.nodeName().toLower() == "duration") && (subNode.isElement()))
650 duration.clear();
651 element = subNode.toElement();
652 if (element.hasAttribute("value"))
653 duration = element.attribute("value");
654 if (element.hasAttribute("Value"))
655 duration = element.attribute("Value");
656 if (element.hasAttribute("VALUE"))
657 duration = element.attribute("VALUE");
659 if (!duration.isNull())
660 length = stringToTime(duration);
663 if ((subNode.nodeName().toLower() == "title") && (subNode.isElement()))
665 title = subNode.toElement().text();
667 if ((subNode.nodeName().toLower() == "author") && (subNode.isElement()))
669 author = subNode.toElement().text();
671 subNode = subNode.nextSibling();
673 if (!url.isEmpty())
675 TrackPtr trackPtr = Meta::TrackPtr( new MetaProxy::Track( KUrl( url ) ) );
676 Meta::EditCapability *ec = trackPtr->as<Meta::EditCapability>();
677 if( ec )
678 ec->setTitle( title );
679 delete ec;
680 tracks.append( trackPtr );
683 node = node.nextSibling();
686 The::playlistModel()->insertOptioned( tracks, Playlist::Append );
687 return true;
690 QTime PlaylistHandler::stringToTime(const QString& timeString)
692 int sec = 0;
693 bool ok = false;
694 QStringList tokens = timeString.split( ':' );
696 sec += tokens[0].toInt(&ok)*3600; //hours
697 sec += tokens[1].toInt(&ok)*60; //minutes
698 sec += tokens[2].toInt(&ok); //secs
700 if (ok)
701 return QTime().addSecs(sec);
702 else
703 return QTime();
706 bool
707 PlaylistHandler::loadXSPF( QTextStream &stream )
709 XSPFPlaylist* doc = new XSPFPlaylist( stream );
711 XSPFtrackList trackList = doc->trackList();
713 TrackList tracks;
714 foreach( XSPFtrack track, trackList )
716 KUrl location = track.location;
717 QString artist = track.creator;
718 QString title = track.title;
719 QString album = track.album;
721 if( location.isEmpty() || ( location.isLocalFile() && !QFile::exists( location.url() ) ) )
724 TrackPtr trackPtr = Meta::TrackPtr( new MetaProxy::Track( KUrl( location ) ) );
725 tracks.append( trackPtr );
727 else
729 debug() << location << ' ' << artist << ' ' << title << ' ' << album;
731 TrackPtr trackPtr = Meta::TrackPtr( new MetaProxy::Track( KUrl( location ) ) );
732 Meta::EditCapability *ec = trackPtr->as<Meta::EditCapability>();
733 if( ec )
735 ec->setTitle( title );
736 ec->setArtist( artist );
737 ec->setAlbum( album );
738 ec->setComment( track.annotation );
740 delete ec;
741 tracks.append( trackPtr );
746 delete doc;
748 The::playlistModel()->insertOptioned( tracks, Playlist::Append );
749 return true;
752 bool
753 PlaylistHandler::saveXSPF( Meta::TrackList tracks, const QString &location )
755 if( tracks.isEmpty() )
756 return false;
757 XSPFPlaylist playlist;
759 playlist.setCreator( "Amarok" );
760 playlist.setTitle( tracks[0]->artist()->name() );
762 playlist.setTrackList( tracks );
764 QFile file( location );
765 if( !file.open( QIODevice::WriteOnly ) )
767 KMessageBox::sorry( MainWindow::self(), i18n( "Cannot write playlist (%1).").arg(location) );
768 return false;
771 QTextStream stream ( &file );
773 playlist.save( stream, 2 );
775 file.close();
777 return true;
780 #include <kdirlister.h>
781 #include <QEventLoop>
782 //this function (C) Copyright 2003-4 Max Howell, (C) Copyright 2004 Mark Kretschmann
783 KUrl::List
784 PlaylistHandler::recurse( const KUrl &url )
786 typedef QMap<QString, KUrl> FileMap;
788 KDirLister lister( false );
789 lister.setAutoUpdate( false );
790 lister.setAutoErrorHandlingEnabled( false, 0 );
791 lister.openUrl( url );
793 while( !lister.isFinished() )
794 kapp->processEvents( QEventLoop::ExcludeUserInput );
796 KFileItemList items = lister.items();
797 KUrl::List urls;
798 FileMap files;
799 foreach( const KFileItem& it, items ) {
800 if( it.isFile() ) { files[it.name()] = it.url(); continue; }
801 if( it.isDir() ) urls += recurse( it.url() );
804 oldForeachType( FileMap, files )
805 // users often have playlist files that reflect directories
806 // higher up, or stuff in this directory. Don't add them as
807 // it produces double entries
808 if( !isPlaylist( (*it).fileName() ) )
809 urls += *it;
811 return urls;
814 #include "PlaylistHandler.moc"