|
|
|
// 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 <mbuesch@freenet.de>
|
|
|
|
// .xspf file support added by Mattias Fliesberg <mattias.fliesberg@gmail.com> 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 <tqdatetime.h> //::recurse()
|
|
|
|
#include <tqeventloop.h> //::recurse()
|
|
|
|
#include <tqfile.h> //::loadPlaylist()
|
|
|
|
#include <tqlistview.h>
|
|
|
|
#include <tqregexp.h>
|
|
|
|
#include <tqstringlist.h>
|
|
|
|
#include <tqtextstream.h> //::loadPlaylist()
|
|
|
|
|
|
|
|
#include <dcopref.h>
|
|
|
|
#include <kapplication.h>
|
|
|
|
#include <kurl.h>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//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<XMLData> &x ) : TQCustomEvent( 1001 ), xml( TQDeepCopy<TQValueList<XMLData> >( x ) ) { }
|
|
|
|
TagsEvent( const BundleList &bees ) : TQCustomEvent( 1000 ), bundles( TQDeepCopy<BundleList>( 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<XMLData> 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<TagsEvent*>(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<XMLData>, 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<PlaylistItem> 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("<br>%1").arg( m_badURLs[it].prettyURL() );
|
|
|
|
else if( it == 5 )
|
|
|
|
text += TQString("<br>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<TQString, KURL> 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<TQString, KURL> 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<MetaBundle>( 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<MyXmlLoader*>( const_cast<TQT_BASE_OBJECT_NAME*>( 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: <Abstract>...</abstract>
|
|
|
|
//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();
|
|
|
|
|