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"
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>
40 PlaylistHandler::PlaylistHandler()
47 bool PlaylistHandler::isPlaylist(const KUrl
& path
)
49 return getFormat( path
) != Unknown
;
52 void PlaylistHandler::load(const QString
& path
)
55 debug() << "file: " << path
;
57 //check if file is local or remote
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";
70 m_contents
= QString( file
.readAll() );
74 stream
.setString( &m_contents
);
75 handleByFormat( stream
, m_format
);
78 downloadPlaylist( url
);
83 bool PlaylistHandler::save( Meta::TrackList tracks
,
84 const QString
&location
)
87 Format playlistFormat
= getFormat( url
);
88 switch (playlistFormat
) {
90 return saveM3u( tracks
, location
);
93 return savePls( tracks
, location
);
96 return saveXSPF( tracks
, location
);
99 debug() << "Currently unhandled type!";
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
;
124 void PlaylistHandler::handleByFormat( QTextStream
&stream
, Format format
)
137 loadRealAudioRam( stream
);
150 debug() << "unknown type!";
158 void PlaylistHandler::downloadPlaylist(const KUrl
& path
)
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
)
175 if ( !m_downloadJob
->error() == 0 )
177 //TODO: error handling here
181 m_contents
= m_downloadJob
->data();
183 stream
.setString( &m_contents
);
186 handleByFormat( stream
, m_format
);
188 m_downloadJob
->deleteLater();
194 PlaylistHandler::loadPls( QTextStream
&stream
)
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;
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();
229 if (tmp
.contains(regExp_File
)) {
233 if (tmp
== section_playlist
) {
234 havePlaylistSection
= true;
237 if (tmp
.contains(regExp_NumberOfEntries
)) {
238 numberOfEntries
= tmp
.section('=', -1).trimmed().toUInt();
242 if (numberOfEntries
!= entryCnt
) {
243 warning() << ".pls playlist: Invalid \"NumberOfEntries\" value. "
244 << "NumberOfEntries=" << numberOfEntries
<< " counted="
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
)
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;
272 if ((*i
).contains(regExp_File
)) {
273 // Have a "File#=XYZ" line.
274 index
= loadPls_extractIndex(*i
);
275 if (index
> numberOfEntries
|| index
== 0)
277 tmp
= (*i
).section('=', 1).trimmed();
278 currentTrack
= Meta::TrackPtr( new MetaProxy::Track( KUrl( tmp
) ) );
279 tracks
.append( currentTrack
);
282 if ((*i
).contains(regExp_Title
)) {
283 // Have a "Title#=XYZ" line.
284 index
= loadPls_extractIndex(*i
);
285 if (index
> numberOfEntries
|| index
== 0)
287 tmp
= (*i
).section('=', 1).trimmed();
289 if ( currentTrack
.data() != 0 && currentTrack
->is
<Meta::EditCapability
>() )
291 Meta::EditCapability
*ec
= currentTrack
->as
<Meta::EditCapability
>();
298 if ((*i
).contains(regExp_Length
)) {
299 // Have a "Length#=XYZ" line.
300 index
= loadPls_extractIndex(*i
);
301 if (index
> numberOfEntries
|| index
== 0)
303 tmp
= (*i
).section('=', 1).trimmed();
304 //tracks.append( KUrl(tmp) );
308 if ((*i
).contains(regExp_NumberOfEntries
)) {
309 // Have the "NumberOfEntries=#" line.
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
;
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
);
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
) );
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
<< "=";
360 stream
<< "\nLength" << i
<< "=";
361 stream
<< lengths
[i
];
365 stream
<< "Version=2\n";
371 PlaylistHandler::loadPls_extractIndex( const QString
&str
) const
373 /* Extract the index number out of a .pls line.
375 * loadPls_extractIndex("File2=foobar") == 2
379 QString
tmp(str
.section('=', 0, 0));
380 tmp
.remove(QRegExp("^\\D*"));
381 ret
= tmp
.trimmed().toUInt(&ok
);
387 PlaylistHandler::loadM3u( QTextStream
&stream
)
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
410 if( url
.startsWith( "/" ) )
411 url
.prepend( "file://" );
413 if( KUrl::isRelativeUrl( url
) ) {
414 KUrl
kurl( KUrl( directory
+ line
) );
416 debug() << "found track: " << kurl
.path();
417 tracks
.append( Meta::TrackPtr( new MetaProxy::Track( kurl
) ) );
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
);
439 PlaylistHandler::saveM3u( Meta::TrackList tracks
, const QString
&location
)
441 const bool relative
= AmarokConfig::relativePlaylist();
442 if( location
.isEmpty() )
445 QFile
file( location
);
447 if( !file
.open( QIODevice::WriteOnly
) )
449 KMessageBox::sorry( MainWindow::self(), i18n( "Cannot write playlist (%1).").arg(location
) );
453 QTextStream
stream( &file
);
454 stream
<< "#EXTM3U\n";
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)
469 // if( url.isLocalFile() && QFileInfo( url.path() ).isDir() )
470 // urls += recurse( url );
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
] );
487 if (url
.protocol() == "file" ) {
489 const QFileInfo
fi(file
);
490 stream
<< KUrl::relativePath(fi
.path(), url
.path());
492 stream
<< url
.path();
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
506 PlaylistHandler::loadRealAudioRam( QTextStream
&stream
)
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
);
529 PlaylistHandler::loadSMIL( QTextStream
&stream
)
531 // adapted from Kaffeine 0.7
533 if( !doc
.setContent( stream
.readAll() ) )
535 debug() << "Could now read smil playlist";
539 QDomElement root
= doc
.documentElement();
540 stream
.setAutoDetectUnicode( true );
541 stream
.setCodec( QTextCodec::codecForName( "UTF-8" ) );
543 if( root
.nodeName().toLower() != "smil" )
548 QDomNodeList nodeList
;
555 nodeList
= doc
.elementsByTagName( "audio" );
556 for( uint i
= 0; i
< nodeList
.count(); i
++ )
558 node
= nodeList
.item(i
);
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");
574 tracks
.append( Meta::TrackPtr( new MetaProxy::Track( KUrl( url
) ) ) );
578 The::playlistModel()->insertOptioned( tracks
, Playlist::Append
);
584 PlaylistHandler::loadASX( QTextStream
&stream
)
586 //adapted from Kaffeine 0.7
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
;
611 QDomElement root
= doc
.documentElement();
619 if (root
.nodeName().toLower() != "asx") return false;
621 QDomNode node
= root
.firstChild();
625 while (!node
.isNull())
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()))
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();
675 TrackPtr trackPtr
= Meta::TrackPtr( new MetaProxy::Track( KUrl( url
) ) );
676 Meta::EditCapability
*ec
= trackPtr
->as
<Meta::EditCapability
>();
678 ec
->setTitle( title
);
680 tracks
.append( trackPtr
);
683 node
= node
.nextSibling();
686 The::playlistModel()->insertOptioned( tracks
, Playlist::Append
);
690 QTime
PlaylistHandler::stringToTime(const QString
& timeString
)
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
701 return QTime().addSecs(sec
);
707 PlaylistHandler::loadXSPF( QTextStream
&stream
)
709 XSPFPlaylist
* doc
= new XSPFPlaylist( stream
);
711 XSPFtrackList trackList
= doc
->trackList();
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
);
729 debug() << location
<< ' ' << artist
<< ' ' << title
<< ' ' << album
;
731 TrackPtr trackPtr
= Meta::TrackPtr( new MetaProxy::Track( KUrl( location
) ) );
732 Meta::EditCapability
*ec
= trackPtr
->as
<Meta::EditCapability
>();
735 ec
->setTitle( title
);
736 ec
->setArtist( artist
);
737 ec
->setAlbum( album
);
738 ec
->setComment( track
.annotation
);
741 tracks
.append( trackPtr
);
748 The::playlistModel()->insertOptioned( tracks
, Playlist::Append
);
753 PlaylistHandler::saveXSPF( Meta::TrackList tracks
, const QString
&location
)
755 if( tracks
.isEmpty() )
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
) );
771 QTextStream
stream ( &file
);
773 playlist
.save( stream
, 2 );
780 #include <kdirlister.h>
781 #include <QEventLoop>
782 //this function (C) Copyright 2003-4 Max Howell, (C) Copyright 2004 Mark Kretschmann
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();
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() ) )
814 #include "PlaylistHandler.moc"