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"
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>
45 PlaylistHandler::PlaylistHandler()
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;
67 void PlaylistHandler::load(const QString
& path
)
70 debug() << "file: " << path
;
72 //check if file is local or remote
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";
85 m_contents
= QString( file
.readAll() );
89 stream
.setString( &m_contents
);
90 handleByFormat( stream
, m_format
);
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
) {
105 return saveM3u( tracks
, location
);
108 return savePls( tracks
, location
);
111 return saveXSPF( tracks
, location
);
114 debug() << "Currently unhandled type!";
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
;
139 void PlaylistHandler::handleByFormat( QTextStream
&stream
, Format format
)
152 loadRealAudioRam( stream
);
165 debug() << "unknown type!";
173 void PlaylistHandler::downloadPlaylist(const KUrl
& path
)
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
)
190 if ( !m_downloadJob
->error() == 0 )
192 //TODO: error handling here
196 m_contents
= m_downloadJob
->data();
198 stream
.setString( &m_contents
);
201 handleByFormat( stream
, m_format
);
203 m_downloadJob
->deleteLater();
209 PlaylistHandler::loadPls( QTextStream
&stream
)
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;
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();
244 if (tmp
.contains(regExp_File
)) {
248 if (tmp
== section_playlist
) {
249 havePlaylistSection
= true;
252 if (tmp
.contains(regExp_NumberOfEntries
)) {
253 numberOfEntries
= tmp
.section('=', -1).trimmed().toUInt();
257 if (numberOfEntries
!= entryCnt
) {
258 warning() << ".pls playlist: Invalid \"NumberOfEntries\" value. "
259 << "NumberOfEntries=" << numberOfEntries
<< " counted="
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
)
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;
287 if ((*i
).contains(regExp_File
)) {
288 // Have a "File#=XYZ" line.
289 index
= loadPls_extractIndex(*i
);
290 if (index
> numberOfEntries
|| index
== 0)
292 tmp
= (*i
).section('=', 1).trimmed();
293 currentTrack
= Meta::TrackPtr( new MetaProxy::Track( KUrl( tmp
) ) );
294 tracks
.append( currentTrack
);
297 if ((*i
).contains(regExp_Title
)) {
298 // Have a "Title#=XYZ" line.
299 index
= loadPls_extractIndex(*i
);
300 if (index
> numberOfEntries
|| index
== 0)
302 tmp
= (*i
).section('=', 1).trimmed();
304 if ( currentTrack
.data() != 0 && currentTrack
->is
<Meta::EditCapability
>() )
306 Meta::EditCapability
*ec
= currentTrack
->as
<Meta::EditCapability
>();
313 if ((*i
).contains(regExp_Length
)) {
314 // Have a "Length#=XYZ" line.
315 index
= loadPls_extractIndex(*i
);
316 if (index
> numberOfEntries
|| index
== 0)
318 tmp
= (*i
).section('=', 1).trimmed();
319 //tracks.append( KUrl(tmp) );
323 if ((*i
).contains(regExp_NumberOfEntries
)) {
324 // Have the "NumberOfEntries=#" line.
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
;
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
);
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
) );
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
<< "=";
375 stream
<< "\nLength" << i
<< "=";
376 stream
<< lengths
[i
];
380 stream
<< "Version=2\n";
386 PlaylistHandler::loadPls_extractIndex( const QString
&str
) const
388 /* Extract the index number out of a .pls line.
390 * loadPls_extractIndex("File2=foobar") == 2
394 QString
tmp(str
.section('=', 0, 0));
395 tmp
.remove(QRegExp("^\\D*"));
396 ret
= tmp
.trimmed().toUInt(&ok
);
402 PlaylistHandler::loadM3u( QTextStream
&stream
)
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
425 if( url
.startsWith( "/" ) )
426 url
.prepend( "file://" );
428 if( KUrl::isRelativeUrl( url
) ) {
429 KUrl
kurl( KUrl( directory
+ line
) );
431 debug() << "found track: " << kurl
.path();
432 tracks
.append( Meta::TrackPtr( new MetaProxy::Track( kurl
) ) );
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
);
454 PlaylistHandler::saveM3u( Meta::TrackList tracks
, const QString
&location
)
456 const bool relative
= AmarokConfig::relativePlaylist();
457 if( location
.isEmpty() )
460 QFile
file( location
);
462 if( !file
.open( QIODevice::WriteOnly
) )
464 KMessageBox::sorry( MainWindow::self(), i18n( "Cannot write playlist (%1).").arg(location
) );
468 QTextStream
stream( &file
);
469 stream
<< "#EXTM3U\n";
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)
484 // if( url.isLocalFile() && QFileInfo( url.path() ).isDir() )
485 // urls += recurse( url );
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
] );
502 if (url
.protocol() == "file" ) {
504 const QFileInfo
fi(file
);
505 stream
<< KUrl::relativePath(fi
.path(), url
.path());
507 stream
<< url
.path();
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
521 PlaylistHandler::loadRealAudioRam( QTextStream
&stream
)
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
);
544 PlaylistHandler::loadSMIL( QTextStream
&stream
)
546 // adapted from Kaffeine 0.7
548 if( !doc
.setContent( stream
.readAll() ) )
550 debug() << "Could now read smil playlist";
554 QDomElement root
= doc
.documentElement();
555 stream
.setAutoDetectUnicode( true );
556 stream
.setCodec( QTextCodec::codecForName( "UTF-8" ) );
558 if( root
.nodeName().toLower() != "smil" )
563 QDomNodeList nodeList
;
570 nodeList
= doc
.elementsByTagName( "audio" );
571 for( uint i
= 0; i
< nodeList
.count(); i
++ )
573 node
= nodeList
.item(i
);
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");
589 tracks
.append( Meta::TrackPtr( new MetaProxy::Track( KUrl( url
) ) ) );
593 The::playlistModel()->insertOptioned( tracks
, Playlist::Append
);
599 PlaylistHandler::loadASX( QTextStream
&stream
)
601 //adapted from Kaffeine 0.7
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
;
626 QDomElement root
= doc
.documentElement();
634 if (root
.nodeName().toLower() != "asx") return false;
636 QDomNode node
= root
.firstChild();
640 while (!node
.isNull())
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()))
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();
690 TrackPtr trackPtr
= Meta::TrackPtr( new MetaProxy::Track( KUrl( url
) ) );
691 Meta::EditCapability
*ec
= trackPtr
->as
<Meta::EditCapability
>();
693 ec
->setTitle( title
);
695 tracks
.append( trackPtr
);
698 node
= node
.nextSibling();
701 The::playlistModel()->insertOptioned( tracks
, Playlist::Append
);
705 QTime
PlaylistHandler::stringToTime(const QString
& timeString
)
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
716 return QTime().addSecs(sec
);
722 PlaylistHandler::loadXSPF( QTextStream
&stream
)
725 XSPFPlaylist
* doc
= new XSPFPlaylist( stream
);
727 XSPFtrackList trackList
= doc
->trackList();
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
);
746 debug() << location
<< ' ' << artist
<< ' ' << title
<< ' ' << album
;
748 TrackPtr trackPtr
= Meta::TrackPtr( new MetaProxy::Track( KUrl( location
) ) );
749 Meta::EditCapability
*ec
= trackPtr
->as
<Meta::EditCapability
>();
752 debug() << "Have EditCapability";
753 ec
->setTitle( title
);
754 ec
->setArtist( artist
);
755 ec
->setAlbum( album
);
756 ec
->setComment( track
.annotation
);
759 tracks
.append( trackPtr
);
766 The::playlistModel()->insertOptioned( tracks
, Playlist::Append
);
771 PlaylistHandler::saveXSPF( Meta::TrackList tracks
, const QString
&location
)
773 if( tracks
.isEmpty() )
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
) );
789 QTextStream
stream ( &file
);
791 playlist
.save( stream
, 2 );
799 recurse( const KUrl
&url
)
801 return Amarok::recursiveUrlExpand( url
);
807 //this function (C) Copyright 2003-4 Max Howell, (C) Copyright 2004 Mark Kretschmann
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();
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() ) )
840 recursiveUrlExpand ( const KUrl::List
&list
)
843 oldForeachType ( KUrl::List
, list
)
845 urls
+= recursiveUrlExpand ( *it
);
851 #include "PlaylistHandler.moc"