|
|
|
/***************************************************************************
|
|
|
|
* copyright : (c) 2004 Pierpaolo Di Panfilo *
|
|
|
|
* (c) 2004 Mark Kretschmann <markey@web.de> *
|
|
|
|
* (c) 2005-2006 Seb Ruiz <me@sebruiz.net> *
|
|
|
|
* (c) 2005 Christian Muehlhaeuser <chris@chris.de> *
|
|
|
|
* (c) 2006 Bart Cerneels <bart.cerneels@gmail.com> *
|
|
|
|
* (c) 2006 Ian Monroe <ian@monroe.nu> *
|
|
|
|
* (c) 2006 Alexandre Oliveira <aleprj@gmail.com> *
|
|
|
|
* (c) 2006 Adam Pigg <adam@piggz.co.uk> *
|
|
|
|
* (c) 2006 Bonne Eggleston <b.eggleston@gmail.com> *
|
|
|
|
* See COPYING file for licensing information *
|
|
|
|
***************************************************************************/
|
|
|
|
|
|
|
|
#include "amarok.h"
|
|
|
|
#include "collectiondb.h"
|
|
|
|
#include "debug.h"
|
|
|
|
#include "dynamicmode.h"
|
|
|
|
#include "k3bexporter.h"
|
|
|
|
#include "playlist.h"
|
|
|
|
#include "playlistbrowser.h"
|
|
|
|
#include "playlistbrowseritem.h"
|
|
|
|
#include "playlistloader.h" //load()
|
|
|
|
#include "playlistselection.h"
|
|
|
|
#include "podcastbundle.h"
|
|
|
|
#include "podcastsettings.h"
|
|
|
|
#include "progressBar.h"
|
|
|
|
#include "metabundle.h"
|
|
|
|
#include "statusbar.h"
|
|
|
|
#include "tagdialog.h"
|
|
|
|
#include "threadmanager.h"
|
|
|
|
#include "mediabrowser.h"
|
|
|
|
|
|
|
|
#include <tqdatetime.h>
|
|
|
|
#include <tqfileinfo.h>
|
|
|
|
#include <tqlabel.h>
|
|
|
|
#include <tqpainter.h> //paintCell()
|
|
|
|
#include <tqpixmap.h> //paintCell()
|
|
|
|
#include <tqregexp.h>
|
|
|
|
|
|
|
|
#include <kapplication.h> //Used for Shoutcast random name generation
|
|
|
|
#include <kdeversion.h> //TDE_VERSION ifndefs. Remove this once we reach a kde 4 dep
|
|
|
|
#include <kiconloader.h> //smallIcon
|
|
|
|
#include <kio/jobclasses.h> //podcast retrieval
|
|
|
|
#include <kio/job.h> //podcast retrieval
|
|
|
|
#include <klocale.h>
|
|
|
|
#include <kmessagebox.h> //podcast info box
|
|
|
|
#include <kmimetype.h>
|
|
|
|
#include <kpopupmenu.h>
|
|
|
|
#include <krun.h>
|
|
|
|
#include <kstandarddirs.h> //podcast loading icons
|
|
|
|
#include <kstringhandler.h>
|
|
|
|
#include <ktrader.h>
|
|
|
|
#include <kurlrequester.h>
|
|
|
|
|
|
|
|
#include <cstdio> //rename
|
|
|
|
|
|
|
|
|
|
|
|
/////////////////////////////////////////////////////////////////////////////
|
|
|
|
/// CLASS PlaylistReader
|
|
|
|
////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
class PlaylistReader : public ThreadManager::DependentJob
|
|
|
|
{
|
|
|
|
public:
|
|
|
|
PlaylistReader( TQObject *recipient, const TQString &path )
|
|
|
|
: ThreadManager::DependentJob( recipient, "PlaylistReader" )
|
|
|
|
, m_path( TQDeepCopy<TQString>( path ) ) {}
|
|
|
|
|
|
|
|
virtual bool doJob() {
|
|
|
|
DEBUG_BLOCK
|
|
|
|
PlaylistFile pf = PlaylistFile( m_path );
|
|
|
|
title = pf.title();
|
|
|
|
for( BundleList::iterator it = pf.bundles().begin();
|
|
|
|
it != pf.bundles().end();
|
|
|
|
++it )
|
|
|
|
bundles += MetaBundle( (*it).url() );
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
virtual void completeJob() {
|
|
|
|
DEBUG_BLOCK
|
|
|
|
PlaylistFile pf = PlaylistFile( m_path );
|
|
|
|
bundles = TQDeepCopy<BundleList>( bundles );
|
|
|
|
title = TQDeepCopy<TQString>( title );
|
|
|
|
for( BundleList::iterator it = bundles.begin();
|
|
|
|
it != bundles.end();
|
|
|
|
++it )
|
|
|
|
*it = TQDeepCopy<MetaBundle>( *it );
|
|
|
|
ThreadManager::DependentJob::completeJob();
|
|
|
|
}
|
|
|
|
|
|
|
|
BundleList bundles;
|
|
|
|
TQString title;
|
|
|
|
|
|
|
|
private:
|
|
|
|
const TQString m_path;
|
|
|
|
};
|
|
|
|
|
|
|
|
/////////////////////////////////////////////////////////////////////////////
|
|
|
|
/// CLASS PlaylistBrowserEntry
|
|
|
|
////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
int
|
|
|
|
PlaylistBrowserEntry::compare( TQListViewItem* item, int col, bool ascending ) const
|
|
|
|
{
|
|
|
|
bool i1 = rtti() == PlaylistCategory::RTTI;
|
|
|
|
bool i2 = item->rtti() == PlaylistCategory::RTTI;
|
|
|
|
|
|
|
|
// If only one of them is a category, make it show up before
|
|
|
|
if ( i1 != i2 )
|
|
|
|
return i1 ? -1 : 1;
|
|
|
|
else if ( i1 ) //both are categories
|
|
|
|
{
|
|
|
|
PlaylistBrowser * const pb = PlaylistBrowser::instance();
|
|
|
|
|
|
|
|
TQValueList<PlaylistCategory*> toplevels; //define a static order for the toplevel categories
|
|
|
|
toplevels << pb->m_playlistCategory
|
|
|
|
<< pb->m_smartCategory
|
|
|
|
<< pb->m_dynamicCategory
|
|
|
|
<< pb->m_streamsCategory
|
|
|
|
<< pb->m_podcastCategory;
|
|
|
|
|
|
|
|
for( int i = 0, n = toplevels.count(); i < n; ++i )
|
|
|
|
{
|
|
|
|
if( this == toplevels[i] )
|
|
|
|
return ascending ? -1 : 1; //same order whether or not it's ascending
|
|
|
|
if( item == toplevels[i] )
|
|
|
|
return ascending ? 1 : -1;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return KListViewItem::compare(item, col, ascending);
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
PlaylistBrowserEntry::setKept( bool k )
|
|
|
|
{
|
|
|
|
m_kept = k;
|
|
|
|
if ( !k ) //Disable renaming by two single clicks
|
|
|
|
setRenameEnabled( 0, false );
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
PlaylistBrowserEntry::updateInfo()
|
|
|
|
{
|
|
|
|
PlaylistBrowser::instance()->setInfo( TQString(), TQString() );
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
PlaylistBrowserEntry::slotDoubleClicked()
|
|
|
|
{
|
|
|
|
warning() << "No functionality for item double click implemented" << endl;
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
PlaylistBrowserEntry::slotRenameItem()
|
|
|
|
{
|
|
|
|
TQListViewItem *parent = KListViewItem::parent();
|
|
|
|
|
|
|
|
while( parent )
|
|
|
|
{
|
|
|
|
if( !static_cast<PlaylistBrowserEntry*>( parent )->isKept() )
|
|
|
|
return;
|
|
|
|
if( !parent->parent() )
|
|
|
|
break;
|
|
|
|
parent = parent->parent();
|
|
|
|
}
|
|
|
|
|
|
|
|
setRenameEnabled( 0, true );
|
|
|
|
static_cast<PlaylistBrowserView*>( listView() )->rename( this, 0 );
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
PlaylistBrowserEntry::slotPostRenameItem( const TQString /*newName*/ )
|
|
|
|
{
|
|
|
|
setRenameEnabled( 0, false );
|
|
|
|
}
|
|
|
|
|
|
|
|
/////////////////////////////////////////////////////////////////////////////
|
|
|
|
/// CLASS PlaylistCategory
|
|
|
|
////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
PlaylistCategory::PlaylistCategory( TQListView *parent, TQListViewItem *after, const TQString &t, bool isFolder )
|
|
|
|
: PlaylistBrowserEntry( parent, after )
|
|
|
|
, m_title( t )
|
|
|
|
, m_id( -1 )
|
|
|
|
, m_folder( isFolder )
|
|
|
|
{
|
|
|
|
setDragEnabled( false );
|
|
|
|
setRenameEnabled( 0, isFolder );
|
|
|
|
setPixmap( 0, SmallIcon( Amarok::icon( "files2" ) ) );
|
|
|
|
setText( 0, t );
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
PlaylistCategory::PlaylistCategory( PlaylistCategory *parent, TQListViewItem *after, const TQString &t, bool isFolder )
|
|
|
|
: PlaylistBrowserEntry( parent, after )
|
|
|
|
, m_title( t )
|
|
|
|
, m_id( -1 )
|
|
|
|
, m_folder( isFolder )
|
|
|
|
{
|
|
|
|
setDragEnabled( false );
|
|
|
|
setRenameEnabled( 0, isFolder );
|
|
|
|
setPixmap( 0, SmallIcon( Amarok::icon( "files" ) ) );
|
|
|
|
setText( 0, t );
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
PlaylistCategory::PlaylistCategory( TQListView *parent, TQListViewItem *after, const TQDomElement &xmlDefinition, bool isFolder )
|
|
|
|
: PlaylistBrowserEntry( parent, after )
|
|
|
|
, m_id( -1 )
|
|
|
|
, m_folder( isFolder )
|
|
|
|
{
|
|
|
|
setXml( xmlDefinition );
|
|
|
|
setDragEnabled( false );
|
|
|
|
setRenameEnabled( 0, isFolder );
|
|
|
|
setPixmap( 0, SmallIcon( Amarok::icon( "files2") ) );
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
PlaylistCategory::PlaylistCategory( PlaylistCategory *parent, TQListViewItem *after, const TQDomElement &xmlDefinition )
|
|
|
|
: PlaylistBrowserEntry( parent, after )
|
|
|
|
, m_id( -1 )
|
|
|
|
, m_folder( true )
|
|
|
|
{
|
|
|
|
setXml( xmlDefinition );
|
|
|
|
setDragEnabled( false );
|
|
|
|
setRenameEnabled( 0, true );
|
|
|
|
setPixmap( 0, SmallIcon( Amarok::icon( "files" ) ) );
|
|
|
|
}
|
|
|
|
|
|
|
|
PlaylistCategory::PlaylistCategory( PlaylistCategory *parent, TQListViewItem *after, const TQString &t, const int id )
|
|
|
|
: PlaylistBrowserEntry( parent, after )
|
|
|
|
, m_title( t )
|
|
|
|
, m_id( id )
|
|
|
|
, m_folder( true )
|
|
|
|
{
|
|
|
|
setDragEnabled( false );
|
|
|
|
setRenameEnabled( 0, true );
|
|
|
|
setPixmap( 0, SmallIcon( Amarok::icon( "files" ) ) );
|
|
|
|
setText( 0, t );
|
|
|
|
}
|
|
|
|
|
|
|
|
void PlaylistCategory::okRename( int col )
|
|
|
|
{
|
|
|
|
TQListViewItem::okRename( col );
|
|
|
|
|
|
|
|
if( m_id < 0 ) return;
|
|
|
|
|
|
|
|
// update the database entry to have the correct name
|
|
|
|
const int parentId = parent() ? static_cast<PlaylistCategory*>(parent())->id() : 0;
|
|
|
|
CollectionDB::instance()->updatePodcastFolder( m_id, text(0), parentId, isOpen() );
|
|
|
|
}
|
|
|
|
|
|
|
|
void PlaylistCategory::setXml( const TQDomElement &xml )
|
|
|
|
{
|
|
|
|
PlaylistBrowser *pb = PlaylistBrowser::instance();
|
|
|
|
TQString tname = xml.tagName();
|
|
|
|
if ( tname == "category" )
|
|
|
|
{
|
|
|
|
setOpen( xml.attribute( "isOpen" ) == "true" );
|
|
|
|
m_title = xml.attribute( "name" );
|
|
|
|
setText( 0, m_title );
|
|
|
|
TQListViewItem *last = 0;
|
|
|
|
for( TQDomNode n = xml.firstChild() ; !n.isNull(); n = n.nextSibling() )
|
|
|
|
{
|
|
|
|
TQDomElement e = n.toElement();
|
|
|
|
if ( e.tagName() == "category" )
|
|
|
|
last = new PlaylistCategory( this, last, e);
|
|
|
|
|
|
|
|
else if ( e.tagName() == "default" ) {
|
|
|
|
if( e.attribute( "type" ) == "stream" )
|
|
|
|
pb->m_coolStreamsOpen = (e.attribute( "isOpen" ) == "true");
|
|
|
|
if( e.attribute( "type" ) == "smartplaylist" )
|
|
|
|
pb->m_smartDefaultsOpen = (e.attribute( "isOpen" ) == "true");
|
|
|
|
if( e.attribute( "type" ) == "lastfm" )
|
|
|
|
pb->m_lastfmOpen = (e.attribute( "isOpen" ) == "true");
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
else if ( e.tagName() == "stream" )
|
|
|
|
last = new StreamEntry( this, last, e );
|
|
|
|
|
|
|
|
else if ( e.tagName() == "smartplaylist" )
|
|
|
|
last = new SmartPlaylist( this, last, e );
|
|
|
|
|
|
|
|
else if ( e.tagName() == "playlist" )
|
|
|
|
last = new PlaylistEntry( this, last, e );
|
|
|
|
|
|
|
|
else if ( e.tagName() == "lastfm" )
|
|
|
|
last = new LastFmEntry( this, last, e );
|
|
|
|
|
|
|
|
else if ( e.tagName() == "dynamic" ) {
|
|
|
|
if ( e.attribute( "name" ) == i18n("Random Mix") || e.attribute( "name" ) == i18n("Suggested Songs" ) )
|
|
|
|
continue;
|
|
|
|
last = new DynamicEntry( this, last, e );
|
|
|
|
}
|
|
|
|
else if ( e.tagName() == "podcast" )
|
|
|
|
{
|
|
|
|
const KURL url( n.namedItem( "url").toElement().text() );
|
|
|
|
TQString xmlLocation = Amarok::saveLocation( "podcasts/" );
|
|
|
|
xmlLocation += n.namedItem( "cache" ).toElement().text();
|
|
|
|
|
|
|
|
TQDomDocument xml;
|
|
|
|
TQFile xmlFile( xmlLocation );
|
|
|
|
TQTextStream stream( &xmlFile );
|
|
|
|
stream.setEncoding( TQTextStream::UnicodeUTF8 );
|
|
|
|
|
|
|
|
if( !xmlFile.open( IO_ReadOnly ) || !xml.setContent( stream.read() ) )
|
|
|
|
{
|
|
|
|
// Invalid podcasts should still be added to the browser, which means there is no cached xml.
|
|
|
|
last = new PodcastChannel( this, last, url, n );
|
|
|
|
}
|
|
|
|
else
|
|
|
|
last = new PodcastChannel( this, last, url, n, xml );
|
|
|
|
|
|
|
|
#define item static_cast<PodcastChannel*>(last)
|
|
|
|
if( item->autoscan() )
|
|
|
|
pb->m_podcastItemsToScan.append( item );
|
|
|
|
#undef item
|
|
|
|
}
|
|
|
|
else if ( e.tagName() == "settings" )
|
|
|
|
PlaylistBrowser::instance()->registerPodcastSettings( title(), new PodcastSettings( e, title() ) );
|
|
|
|
|
|
|
|
if( !e.attribute( "isOpen" ).isNull() && last )
|
|
|
|
last->setOpen( e.attribute( "isOpen" ) == "true" ); //settings doesn't have an attribute "isOpen"
|
|
|
|
}
|
|
|
|
setText( 0, xml.attribute("name") );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
TQDomElement PlaylistCategory::xml() const
|
|
|
|
{
|
|
|
|
TQDomDocument d;
|
|
|
|
TQDomElement i = d.createElement("category");
|
|
|
|
i.setAttribute( "name", text(0) );
|
|
|
|
if( isOpen() )
|
|
|
|
i.setAttribute( "isOpen", "true" );
|
|
|
|
for( PlaylistBrowserEntry *it = static_cast<PlaylistBrowserEntry*>( firstChild() ); it;
|
|
|
|
it = static_cast<PlaylistBrowserEntry*>( it->nextSibling() ) )
|
|
|
|
{
|
|
|
|
if( it == PlaylistBrowser::instance()->m_coolStreams )
|
|
|
|
{
|
|
|
|
TQDomDocument doc;
|
|
|
|
TQDomElement e = doc.createElement("default");
|
|
|
|
e.setAttribute( "type", "stream" );
|
|
|
|
if( it->isOpen() )
|
|
|
|
e.setAttribute( "isOpen", "true" );
|
|
|
|
i.appendChild( d.importNode( e, true ) );
|
|
|
|
}
|
|
|
|
else if( it == PlaylistBrowser::instance()->m_lastfmCategory )
|
|
|
|
{
|
|
|
|
TQDomDocument doc;
|
|
|
|
TQDomElement e = doc.createElement("default");
|
|
|
|
e.setAttribute( "type", "lastfm" );
|
|
|
|
if( it->isOpen() )
|
|
|
|
e.setAttribute( "isOpen", "true" );
|
|
|
|
i.appendChild( d.importNode( e, true ) );
|
|
|
|
}
|
|
|
|
else if( it == PlaylistBrowser::instance()->m_smartDefaults )
|
|
|
|
{
|
|
|
|
TQDomDocument doc;
|
|
|
|
TQDomElement e = doc.createElement("default");
|
|
|
|
e.setAttribute( "type", "smartplaylist" );
|
|
|
|
if( it->isOpen() )
|
|
|
|
e.setAttribute( "isOpen", "true" );
|
|
|
|
i.appendChild( d.importNode( e, true ) );
|
|
|
|
}
|
|
|
|
else if( it->isKept() )
|
|
|
|
i.appendChild( d.importNode( it->xml(), true ) );
|
|
|
|
}
|
|
|
|
return i;
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
PlaylistCategory::slotDoubleClicked()
|
|
|
|
{
|
|
|
|
setOpen( !isOpen() );
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
PlaylistCategory::slotRenameItem()
|
|
|
|
{
|
|
|
|
if ( isKept() ) {
|
|
|
|
setRenameEnabled( 0, true );
|
|
|
|
static_cast<PlaylistBrowserView*>( listView() )->rename( this, 0 );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void
|
|
|
|
PlaylistCategory::showContextMenu( const TQPoint &position )
|
|
|
|
{
|
|
|
|
KPopupMenu menu( listView() );
|
|
|
|
|
|
|
|
if( !isKept() ) return;
|
|
|
|
|
|
|
|
enum Actions { RENAME, REMOVE, CREATE, PLAYLIST, PLAYLIST_IMPORT, SMART, STREAM, DYNAMIC,
|
|
|
|
LASTFM, LASTFMCUSTOM, PODCAST, REFRESH, CONFIG, INTERVAL };
|
|
|
|
|
|
|
|
TQListViewItem *parentCat = this;
|
|
|
|
|
|
|
|
while( parentCat->parent() )
|
|
|
|
parentCat = parentCat->parent();
|
|
|
|
|
|
|
|
bool isPodcastFolder = false;
|
|
|
|
|
|
|
|
if( isFolder() ) {
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon("edit") ), i18n( "&Rename" ), RENAME );
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon("remove") ), i18n( "&Delete" ), REMOVE );
|
|
|
|
menu.insertSeparator();
|
|
|
|
}
|
|
|
|
|
|
|
|
if( parentCat == static_cast<TQListViewItem*>( PlaylistBrowser::instance()->m_playlistCategory) )
|
|
|
|
{
|
|
|
|
menu.insertItem( SmallIconSet(Amarok::icon( "add_playlist" )), i18n("Create Playlist..."), PLAYLIST );
|
|
|
|
menu.insertItem( SmallIconSet(Amarok::icon( "add_playlist" )), i18n("Import Playlist..."), PLAYLIST_IMPORT );
|
|
|
|
}
|
|
|
|
|
|
|
|
else if( parentCat == static_cast<TQListViewItem*>(PlaylistBrowser::instance()->m_smartCategory) )
|
|
|
|
menu.insertItem( SmallIconSet(Amarok::icon( "add_playlist" )), i18n("New Smart Playlist..."), SMART );
|
|
|
|
|
|
|
|
else if( parentCat == static_cast<TQListViewItem*>(PlaylistBrowser::instance()->m_dynamicCategory) )
|
|
|
|
menu.insertItem( SmallIconSet(Amarok::icon( "add_playlist" )), i18n("New Dynamic Playlist..."), DYNAMIC );
|
|
|
|
|
|
|
|
else if( parentCat == static_cast<TQListViewItem*>(PlaylistBrowser::instance()->m_streamsCategory) )
|
|
|
|
menu.insertItem( SmallIconSet(Amarok::icon( "add_playlist" )), i18n("Add Radio Stream..."), STREAM );
|
|
|
|
|
|
|
|
else if( parentCat == static_cast<TQListViewItem*>(PlaylistBrowser::instance()->m_lastfmCategory) )
|
|
|
|
{
|
|
|
|
menu.insertItem( SmallIconSet(Amarok::icon( "add_playlist" )), i18n("Add Last.fm Radio..."), LASTFM );
|
|
|
|
menu.insertItem( SmallIconSet(Amarok::icon( "add_playlist" )), i18n("Add Custom Last.fm Radio..."), LASTFMCUSTOM );
|
|
|
|
}
|
|
|
|
|
|
|
|
else if( parentCat == static_cast<TQListViewItem*>(PlaylistBrowser::instance()->m_podcastCategory) )
|
|
|
|
{
|
|
|
|
isPodcastFolder = true;
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon( "add_playlist" ) ), i18n("Add Podcast..."), PODCAST );
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon( "refresh" ) ), i18n("Refresh All Podcasts"), REFRESH );
|
|
|
|
menu.insertSeparator();
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon( "configure" ) ), i18n( "&Configure Podcasts..." ), CONFIG );
|
|
|
|
if( parentCat->childCount() == 0 )
|
|
|
|
menu.setItemEnabled( CONFIG, false );
|
|
|
|
if( parentCat == this )
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon( "configure" ) ), i18n("Scan Interval..."), INTERVAL );
|
|
|
|
}
|
|
|
|
|
|
|
|
menu.insertSeparator();
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon( "files" ) ), i18n("Create Sub-Folder"), CREATE );
|
|
|
|
|
|
|
|
TQListViewItem *tracker = 0;
|
|
|
|
PlaylistCategory *newFolder = 0;
|
|
|
|
int c;
|
|
|
|
TQString name;
|
|
|
|
|
|
|
|
switch( menu.exec( position ) ) {
|
|
|
|
case RENAME:
|
|
|
|
PlaylistBrowser::instance()->renameSelectedItem();
|
|
|
|
break;
|
|
|
|
|
|
|
|
case REMOVE:
|
|
|
|
PlaylistBrowser::instance()->removeSelectedItems();
|
|
|
|
break;
|
|
|
|
|
|
|
|
case PLAYLIST:
|
|
|
|
PlaylistBrowser::instance()->createPlaylist( this, false );
|
|
|
|
break;
|
|
|
|
|
|
|
|
case PLAYLIST_IMPORT:
|
|
|
|
PlaylistBrowser::instance()->openPlaylist( this );
|
|
|
|
break;
|
|
|
|
|
|
|
|
case SMART:
|
|
|
|
PlaylistBrowser::instance()->addSmartPlaylist( this );
|
|
|
|
break;
|
|
|
|
|
|
|
|
case STREAM:
|
|
|
|
PlaylistBrowser::instance()->addStream( this );
|
|
|
|
break;
|
|
|
|
|
|
|
|
case DYNAMIC:
|
|
|
|
ConfigDynamic::dynamicDialog( PlaylistBrowser::instance() );
|
|
|
|
break;
|
|
|
|
|
|
|
|
case LASTFM:
|
|
|
|
PlaylistBrowser::instance()->addLastFmRadio( this );
|
|
|
|
break;
|
|
|
|
|
|
|
|
case LASTFMCUSTOM:
|
|
|
|
PlaylistBrowser::instance()->addLastFmCustomRadio( this );
|
|
|
|
break;
|
|
|
|
|
|
|
|
case PODCAST:
|
|
|
|
PlaylistBrowser::instance()->addPodcast( this );
|
|
|
|
break;
|
|
|
|
|
|
|
|
case REFRESH:
|
|
|
|
PlaylistBrowser::instance()->refreshPodcasts( this );
|
|
|
|
break;
|
|
|
|
|
|
|
|
case CONFIG:
|
|
|
|
PlaylistBrowser::instance()->configurePodcasts( this );
|
|
|
|
break;
|
|
|
|
|
|
|
|
case CREATE:
|
|
|
|
tracker = firstChild();
|
|
|
|
|
|
|
|
for( c = 0 ; isCategory( tracker ); tracker = tracker->nextSibling() )
|
|
|
|
{
|
|
|
|
if( tracker->text(0).startsWith( i18n("Folder") ) )
|
|
|
|
c++;
|
|
|
|
if( !isCategory( tracker->nextSibling() ) )
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
name = i18n("Folder");
|
|
|
|
if( c ) name = i18n("Folder %1").tqarg(c);
|
|
|
|
if( tracker == firstChild() && !isCategory( tracker ) ) tracker = 0;
|
|
|
|
|
|
|
|
newFolder = new PlaylistCategory( this, tracker, name, true );
|
|
|
|
newFolder->startRename( 0 );
|
|
|
|
if( isPodcastFolder )
|
|
|
|
{
|
|
|
|
c = CollectionDB::instance()->addPodcastFolder( newFolder->text(0), id(), false );
|
|
|
|
newFolder->setId( c );
|
|
|
|
}
|
|
|
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
case INTERVAL:
|
|
|
|
PlaylistBrowser::instance()->changePodcastInterval();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void
|
|
|
|
PlaylistCategory::paintCell( TQPainter *p, const TQColorGroup &cg, int column, int width, int align )
|
|
|
|
{
|
|
|
|
TQFont font( p->font() );
|
|
|
|
|
|
|
|
if( !m_folder ) {
|
|
|
|
font.setBold( true );
|
|
|
|
}
|
|
|
|
|
|
|
|
p->setFont( font );
|
|
|
|
|
|
|
|
KListViewItem::paintCell( p, cg, column, width, align );
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/////////////////////////////////////////////////////////////////////////////
|
|
|
|
/// CLASS PlaylistEntry
|
|
|
|
////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
PlaylistEntry::PlaylistEntry( TQListViewItem *parent, TQListViewItem *after, const KURL &url, int tracks, int length )
|
|
|
|
: PlaylistBrowserEntry( parent, after )
|
|
|
|
, m_url( url )
|
|
|
|
, m_length( length )
|
|
|
|
, m_trackCount( tracks )
|
|
|
|
, m_loading( false )
|
|
|
|
, m_loaded( false )
|
|
|
|
, m_dynamic( false )
|
|
|
|
, m_loading1( new TQPixmap( locate("data", "amarok/images/loading1.png" ) ) )
|
|
|
|
, m_loading2( new TQPixmap( locate("data", "amarok/images/loading2.png" ) ) )
|
|
|
|
, m_lastTrack( 0 )
|
|
|
|
{
|
|
|
|
m_trackList.setAutoDelete( true );
|
|
|
|
tmp_droppedTracks.setAutoDelete( false );
|
|
|
|
|
|
|
|
setDragEnabled( true );
|
|
|
|
setRenameEnabled( 0, false );
|
|
|
|
setExpandable( true );
|
|
|
|
|
|
|
|
setPixmap( 0, SmallIcon( Amarok::icon( "playlist" ) ) );
|
|
|
|
|
|
|
|
if( !m_trackCount )
|
|
|
|
{
|
|
|
|
setText(0, i18n("Loading Playlist") );
|
|
|
|
load(); //load the playlist file
|
|
|
|
}
|
|
|
|
// set text is called from within customEvent()
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
PlaylistEntry::PlaylistEntry( TQListViewItem *parent, TQListViewItem *after, const TQDomElement &xmlDefinition )
|
|
|
|
: PlaylistBrowserEntry( parent, after )
|
|
|
|
, m_loading( false )
|
|
|
|
, m_loaded( false )
|
|
|
|
, m_dynamic( false )
|
|
|
|
, m_loading1( new TQPixmap( locate("data", "amarok/images/loading1.png" ) ) )
|
|
|
|
, m_loading2( new TQPixmap( locate("data", "amarok/images/loading2.png" ) ) )
|
|
|
|
, m_lastTrack( 0 )
|
|
|
|
{
|
|
|
|
m_url.setPath( xmlDefinition.attribute( "file" ) );
|
|
|
|
m_trackCount = xmlDefinition.namedItem( "tracks" ).toElement().text().toInt();
|
|
|
|
m_length = xmlDefinition.namedItem( "length" ).toElement().text().toInt();
|
|
|
|
|
|
|
|
TQString title = xmlDefinition.attribute( "title" );
|
|
|
|
if( title.isEmpty() )
|
|
|
|
{
|
|
|
|
title = fileBaseName( m_url.path() );
|
|
|
|
title.replace( '_', ' ' );
|
|
|
|
}
|
|
|
|
setText( 0, title );
|
|
|
|
|
|
|
|
m_trackList.setAutoDelete( true );
|
|
|
|
tmp_droppedTracks.setAutoDelete( false );
|
|
|
|
|
|
|
|
setDragEnabled( true );
|
|
|
|
setRenameEnabled( 0, false );
|
|
|
|
setExpandable( true );
|
|
|
|
|
|
|
|
setPixmap( 0, SmallIcon( Amarok::icon( "playlist" ) ) );
|
|
|
|
|
|
|
|
if( !m_trackCount )
|
|
|
|
{
|
|
|
|
setText(0, i18n("Loading Playlist") );
|
|
|
|
load(); //load the playlist file
|
|
|
|
}
|
|
|
|
// set text is called from within customEvent()
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
PlaylistEntry::~PlaylistEntry()
|
|
|
|
{
|
|
|
|
m_trackList.clear();
|
|
|
|
tmp_droppedTracks.setAutoDelete( true );
|
|
|
|
tmp_droppedTracks.clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
void PlaylistEntry::load()
|
|
|
|
{
|
|
|
|
if( m_loading ) return;
|
|
|
|
m_trackList.clear();
|
|
|
|
m_length = 0;
|
|
|
|
m_loaded = false;
|
|
|
|
m_loading = true;
|
|
|
|
|
|
|
|
//starts loading animation
|
|
|
|
m_iconCounter = 1;
|
|
|
|
startAnimation();
|
|
|
|
connect( &m_animationTimer, TQT_SIGNAL(timeout()), this, TQT_SLOT(slotAnimation()) );
|
|
|
|
|
|
|
|
//delete all tqchildren, so that we don't duplicate things
|
|
|
|
while( firstChild() )
|
|
|
|
delete firstChild();
|
|
|
|
|
|
|
|
//read the playlist file in a thread
|
|
|
|
ThreadManager::instance()->queueJob( new PlaylistReader( this, m_url.path() ) );
|
|
|
|
}
|
|
|
|
|
|
|
|
void PlaylistEntry::startAnimation()
|
|
|
|
{
|
|
|
|
if( !m_animationTimer.isActive() )
|
|
|
|
m_animationTimer.start( ANIMATION_INTERVAL );
|
|
|
|
}
|
|
|
|
|
|
|
|
void PlaylistEntry::stopAnimation()
|
|
|
|
{
|
|
|
|
m_animationTimer.stop();
|
|
|
|
m_dynamic ?
|
|
|
|
setPixmap( 0, SmallIcon( Amarok::icon( "favorites" ) ) ):
|
|
|
|
setPixmap( 0, SmallIcon( Amarok::icon( "playlist" ) ) );
|
|
|
|
}
|
|
|
|
|
|
|
|
void PlaylistEntry::slotAnimation()
|
|
|
|
{
|
|
|
|
m_iconCounter % 2 ?
|
|
|
|
setPixmap( 0, *m_loading1 ):
|
|
|
|
setPixmap( 0, *m_loading2 );
|
|
|
|
|
|
|
|
m_iconCounter++;
|
|
|
|
}
|
|
|
|
|
|
|
|
void PlaylistEntry::insertTracks( TQListViewItem *after, KURL::List list )
|
|
|
|
{
|
|
|
|
TQValueList<MetaBundle> bundles;
|
|
|
|
|
|
|
|
foreachType( KURL::List, list )
|
|
|
|
bundles += MetaBundle( *it );
|
|
|
|
|
|
|
|
insertTracks( after, bundles );
|
|
|
|
}
|
|
|
|
|
|
|
|
void PlaylistEntry::insertTracks( TQListViewItem *after, TQValueList<MetaBundle> bundles )
|
|
|
|
{
|
|
|
|
int pos = 0;
|
|
|
|
if( after ) {
|
|
|
|
pos = m_trackList.find( static_cast<PlaylistTrackItem*>(after)->trackInfo() ) + 1;
|
|
|
|
if( pos == -1 )
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
uint k = 0;
|
|
|
|
foreachType( TQValueList<MetaBundle>, bundles )
|
|
|
|
{
|
|
|
|
TrackItemInfo *newInfo = new TrackItemInfo( *it );
|
|
|
|
m_length += newInfo->length();
|
|
|
|
m_trackCount++;
|
|
|
|
|
|
|
|
if( after ) {
|
|
|
|
m_trackList.insert( pos+k, newInfo );
|
|
|
|
if( isOpen() )
|
|
|
|
after = new PlaylistTrackItem( this, after, newInfo );
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
if( m_loaded && !m_loading ) {
|
|
|
|
m_trackList.append( newInfo );
|
|
|
|
if( isOpen() ) //append the track item to the playlist
|
|
|
|
m_lastTrack = new PlaylistTrackItem( this, m_lastTrack, newInfo );
|
|
|
|
}
|
|
|
|
else
|
|
|
|
tmp_droppedTracks.append( newInfo );
|
|
|
|
}
|
|
|
|
++k;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( !m_loading ) {
|
|
|
|
PlaylistBrowser::instance()->savePlaylist( this );
|
|
|
|
if ( !m_loaded )
|
|
|
|
tmp_droppedTracks.clear(); // after saving, dropped tracks are on the file
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void PlaylistEntry::removeTrack( TQListViewItem *item, bool isLast )
|
|
|
|
{
|
|
|
|
#define item static_cast<PlaylistTrackItem*>(item)
|
|
|
|
//remove a track and update playlist stats
|
|
|
|
TrackItemInfo *info = item->trackInfo();
|
|
|
|
m_length -= info->length();
|
|
|
|
m_trackCount--;
|
|
|
|
m_trackList.remove( info );
|
|
|
|
if( item == m_lastTrack ) {
|
|
|
|
TQListViewItem *above = item->itemAbove();
|
|
|
|
m_lastTrack = above ? static_cast<PlaylistTrackItem *>( above ) : 0;
|
|
|
|
}
|
|
|
|
delete item;
|
|
|
|
|
|
|
|
#undef item
|
|
|
|
|
|
|
|
if( isLast )
|
|
|
|
PlaylistBrowser::instance()->savePlaylist( this );
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void PlaylistEntry::customEvent( TQCustomEvent *e )
|
|
|
|
{
|
|
|
|
if( e->type() != (int)PlaylistReader::JobFinishedEvent )
|
|
|
|
return;
|
|
|
|
|
|
|
|
#define playlist static_cast<PlaylistReader*>(e)
|
|
|
|
TQString str = playlist->title;
|
|
|
|
|
|
|
|
if ( str.isEmpty() )
|
|
|
|
str = fileBaseName( m_url.path() );
|
|
|
|
|
|
|
|
str.replace( '_', ' ' );
|
|
|
|
setText( 0, str );
|
|
|
|
|
|
|
|
foreachType( BundleList, playlist->bundles )
|
|
|
|
{
|
|
|
|
const MetaBundle &b = *it;
|
|
|
|
TrackItemInfo *info = new TrackItemInfo( b );
|
|
|
|
m_trackList.append( info );
|
|
|
|
m_length += info->length();
|
|
|
|
if( isOpen() )
|
|
|
|
m_lastTrack = new PlaylistTrackItem( this, m_lastTrack, info );
|
|
|
|
}
|
|
|
|
#undef playlist
|
|
|
|
|
|
|
|
//the tracks dropped on the playlist while it wasn't loaded are added to the track list
|
|
|
|
if( tmp_droppedTracks.count() ) {
|
|
|
|
|
|
|
|
for ( TrackItemInfo *info = tmp_droppedTracks.first(); info; info = tmp_droppedTracks.next() ) {
|
|
|
|
m_trackList.append( info );
|
|
|
|
}
|
|
|
|
tmp_droppedTracks.clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
m_loading = false;
|
|
|
|
m_loaded = true;
|
|
|
|
stopAnimation(); //stops the loading animation
|
|
|
|
|
|
|
|
if( m_trackCount && !m_dynamic && !isDynamic() ) setOpen( true );
|
|
|
|
else listView()->repaintItem( this );
|
|
|
|
|
|
|
|
m_trackCount = m_trackList.count();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* We destroy the tracks on collapsing the entry. However, if we are using dynamic mode, then we leave them
|
|
|
|
* because adding from a custom list is problematic if the entry has no tqchildren. Using load() is not effective
|
|
|
|
* since this is a threaded operation and would require pulling apart the entire class to make it work.
|
|
|
|
*/
|
|
|
|
|
|
|
|
void PlaylistEntry::setOpen( bool open )
|
|
|
|
{
|
|
|
|
if( open == isOpen())
|
|
|
|
return;
|
|
|
|
|
|
|
|
if( open ) { //expand
|
|
|
|
|
|
|
|
if( m_loaded ) {
|
|
|
|
//create track items
|
|
|
|
for ( TrackItemInfo *info = m_trackList.first(); info; info = m_trackList.next() )
|
|
|
|
m_lastTrack = new PlaylistTrackItem( this, m_lastTrack, info );
|
|
|
|
}
|
|
|
|
else if( !isDynamic() || !m_dynamic ) {
|
|
|
|
load();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if( !isDynamic() || !m_dynamic ) { //collapse
|
|
|
|
|
|
|
|
//delete all tqchildren
|
|
|
|
while( firstChild() )
|
|
|
|
delete firstChild();
|
|
|
|
|
|
|
|
m_lastTrack = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
TQListViewItem::setOpen( open );
|
|
|
|
PlaylistBrowser::instance()->savePlaylists();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int PlaylistEntry::compare( TQListViewItem* i, int /*col*/ ) const
|
|
|
|
{
|
|
|
|
PlaylistEntry* item = static_cast<PlaylistEntry*>(i);
|
|
|
|
|
|
|
|
// Compare case-insensitive
|
|
|
|
return TQString::localeAwareCompare( text( 0 ).lower(), item->text( 0 ).lower() );
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
KURL::List PlaylistEntry::tracksURL()
|
|
|
|
{
|
|
|
|
KURL::List list;
|
|
|
|
|
|
|
|
if( m_loaded ) { //playlist loaded
|
|
|
|
for( TrackItemInfo *info = m_trackList.first(); info; info = m_trackList.next() )
|
|
|
|
list += info->url();
|
|
|
|
}
|
|
|
|
else
|
|
|
|
list = m_url; //playlist url
|
|
|
|
|
|
|
|
return list;
|
|
|
|
}
|
|
|
|
|
|
|
|
void PlaylistEntry::updateInfo()
|
|
|
|
{
|
|
|
|
const TQString body = "<tr><td><b>%1</b></td><td>%2</td></tr>";
|
|
|
|
|
|
|
|
TQString str = "<html><body><table width=\"100%\" border=\"0\">";
|
|
|
|
|
|
|
|
str += body.tqarg( i18n( "Playlist" ), text(0) );
|
|
|
|
str += body.tqarg( i18n( "Number of tracks" ), TQString::number(m_trackCount) );
|
|
|
|
str += body.tqarg( i18n( "Length" ), MetaBundle::prettyTime( m_length ) );
|
|
|
|
str += body.tqarg( i18n( "Location" ), m_url.prettyURL() );
|
|
|
|
str += "</table></body></html>";
|
|
|
|
|
|
|
|
PlaylistBrowser::instance()->setInfo( text(0), str );
|
|
|
|
}
|
|
|
|
|
|
|
|
void PlaylistEntry::slotDoubleClicked()
|
|
|
|
{
|
|
|
|
Playlist::instance()->proposePlaylistName( text(0), true );
|
|
|
|
Playlist::instance()->insertMedia( url(), Playlist::DefaultOptions );
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void PlaylistEntry::showContextMenu( const TQPoint &position )
|
|
|
|
{
|
|
|
|
KPopupMenu menu( listView() );
|
|
|
|
|
|
|
|
enum Id { LOAD, APPEND, TQUEUE, RENAME, DELETE, MEDIADEVICE_COPY, MEDIADEVICE_SYNC };
|
|
|
|
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon( "files" ) ), i18n( "&Load" ), LOAD );
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon( "add_playlist" ) ), i18n( "&Append to Playlist" ), APPEND );
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon( "queue_track" ) ), i18n( "&Queue Tracks" ), TQUEUE );
|
|
|
|
|
|
|
|
if( MediaBrowser::isAvailable() )
|
|
|
|
{
|
|
|
|
menu.insertSeparator();
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon( "device" ) ),
|
|
|
|
i18n( "&Transfer to Media Device" ), MEDIADEVICE_COPY );
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon( "device" ) ),
|
|
|
|
i18n( "&Synchronize to Media Device" ), MEDIADEVICE_SYNC );
|
|
|
|
}
|
|
|
|
|
|
|
|
menu.insertSeparator();
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon("edit") ), i18n( "&Rename" ), RENAME );
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon("remove_from_playlist") ), i18n( "&Delete" ), DELETE );
|
|
|
|
menu.setAccel( Key_L, LOAD );
|
|
|
|
menu.setAccel( Key_F2, RENAME );
|
|
|
|
menu.setAccel( SHIFT+Key_Delete, DELETE );
|
|
|
|
|
|
|
|
switch( menu.exec( position ) )
|
|
|
|
{
|
|
|
|
case LOAD:
|
|
|
|
Playlist::instance()->clear();
|
|
|
|
Playlist::instance()->setPlaylistName( text(0), true );
|
|
|
|
//FALL THROUGH
|
|
|
|
case APPEND:
|
|
|
|
PlaylistBrowser::instance()->addSelectedToPlaylist( Playlist::Append );
|
|
|
|
break;
|
|
|
|
case TQUEUE:
|
|
|
|
PlaylistBrowser::instance()->addSelectedToPlaylist( Playlist::Queue );
|
|
|
|
break;
|
|
|
|
case RENAME:
|
|
|
|
PlaylistBrowser::instance()->renameSelectedItem();
|
|
|
|
break;
|
|
|
|
case DELETE:
|
|
|
|
PlaylistBrowser::instance()->removeSelectedItems();
|
|
|
|
break;
|
|
|
|
case MEDIADEVICE_COPY:
|
|
|
|
MediaBrowser::queue()->addURLs( tracksURL(), text(0) );
|
|
|
|
break;
|
|
|
|
case MEDIADEVICE_SYNC:
|
|
|
|
MediaBrowser::queue()->syncPlaylist( text(0), url() );
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
void PlaylistEntry::slotPostRenameItem( const TQString newName )
|
|
|
|
{
|
|
|
|
TQString oldPath = url().path();
|
|
|
|
TQString newPath = fileDirPath( oldPath ) + newName + '.' + Amarok::extension( oldPath );
|
|
|
|
|
|
|
|
if ( std::rename( TQFile::encodeName( oldPath ), TQFile::encodeName( newPath ) ) == -1 )
|
|
|
|
KMessageBox::error( listView(), i18n("Error renaming the file.") );
|
|
|
|
else
|
|
|
|
setUrl( newPath );
|
|
|
|
}
|
|
|
|
|
|
|
|
void PlaylistEntry::setDynamic( bool enable )
|
|
|
|
{
|
|
|
|
if( enable != m_dynamic )
|
|
|
|
{
|
|
|
|
if( enable )
|
|
|
|
{
|
|
|
|
if( !m_loaded ) load(); // we need to load it to ensure that we can read the contents
|
|
|
|
setPixmap( 0, SmallIcon( Amarok::icon( "favorites" ) ) );
|
|
|
|
}
|
|
|
|
else
|
|
|
|
setPixmap( 0, SmallIcon( Amarok::icon( "playlist" ) ) );
|
|
|
|
|
|
|
|
m_dynamic = enable;
|
|
|
|
}
|
|
|
|
|
|
|
|
listView()->repaintItem( this );
|
|
|
|
}
|
|
|
|
|
|
|
|
void PlaylistEntry::setup()
|
|
|
|
{
|
|
|
|
TQFontMetrics fm( listView()->font() );
|
|
|
|
int margin = listView()->itemMargin()*2;
|
|
|
|
int h = fm.lineSpacing();
|
|
|
|
if ( h % 2 > 0 ) h++;
|
|
|
|
setHeight( h + margin );
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void PlaylistEntry::paintCell( TQPainter *p, const TQColorGroup &cg, int column, int width, int align )
|
|
|
|
{
|
|
|
|
//flicker-free drawing
|
|
|
|
static TQPixmap buffer;
|
|
|
|
buffer.resize( width, height() );
|
|
|
|
|
|
|
|
if( buffer.isNull() )
|
|
|
|
{
|
|
|
|
KListViewItem::paintCell( p, cg, column, width, align );
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
TQPainter pBuf( &buffer, true );
|
|
|
|
// use alternate background
|
|
|
|
#if TDE_VERSION < KDE_MAKE_VERSION(3,3,91)
|
|
|
|
pBuf.fillRect( buffer.rect(), isSelected() ? cg.highlight() : backgroundColor() );
|
|
|
|
#else
|
|
|
|
pBuf.fillRect( buffer.rect(), isSelected() ? cg.highlight() : backgroundColor(0) );
|
|
|
|
#endif
|
|
|
|
|
|
|
|
KListView *lv = static_cast<KListView *>( listView() );
|
|
|
|
|
|
|
|
TQFont font( p->font() );
|
|
|
|
TQFontMetrics fm( p->fontMetrics() );
|
|
|
|
|
|
|
|
int text_x = 0;// lv->treeStepSize() + 3;
|
|
|
|
int textHeight;
|
|
|
|
|
|
|
|
textHeight = height();
|
|
|
|
|
|
|
|
pBuf.setPen( isSelected() ? cg.highlightedText() : cg.text() );
|
|
|
|
|
|
|
|
if( pixmap( column ) )
|
|
|
|
{
|
|
|
|
int y = (textHeight - pixmap(column)->height())/2;
|
|
|
|
pBuf.drawPixmap( text_x, y, *pixmap(column) );
|
|
|
|
text_x += pixmap(column)->width()+4;
|
|
|
|
}
|
|
|
|
|
|
|
|
pBuf.setFont( font );
|
|
|
|
TQFontMetrics fmName( font );
|
|
|
|
|
|
|
|
TQString name = text(column);
|
|
|
|
const int _width = width - text_x - lv->itemMargin()*2;
|
|
|
|
if( fmName.width( name ) > _width )
|
|
|
|
{
|
|
|
|
name = KStringHandler::rPixelSqueeze( name, pBuf.fontMetrics(), _width );
|
|
|
|
}
|
|
|
|
|
|
|
|
pBuf.drawText( text_x, 0, width - text_x, textHeight, AlignVCenter, name );
|
|
|
|
|
|
|
|
pBuf.end();
|
|
|
|
p->drawPixmap( 0, 0, buffer );
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
TQDomElement PlaylistEntry::xml() const
|
|
|
|
{
|
|
|
|
TQDomDocument doc;
|
|
|
|
TQDomElement i = doc.createElement("playlist");
|
|
|
|
i.setAttribute( "file", url().path() );
|
|
|
|
i.setAttribute( "title", text(0) );
|
|
|
|
if( isOpen() )
|
|
|
|
i.setAttribute( "isOpen", "true" );
|
|
|
|
|
|
|
|
TQDomElement attr = doc.createElement( "tracks" );
|
|
|
|
TQDomText t = doc.createTextNode( TQString::number( trackCount() ) );
|
|
|
|
attr.appendChild( t );
|
|
|
|
i.appendChild( attr );
|
|
|
|
|
|
|
|
attr = doc.createElement( "length" );
|
|
|
|
t = doc.createTextNode( TQString::number( length() ) );
|
|
|
|
attr.appendChild( t );
|
|
|
|
i.appendChild( attr );
|
|
|
|
|
|
|
|
TQFileInfo fi( url().path() );
|
|
|
|
attr = doc.createElement( "modified" );
|
|
|
|
t = doc.createTextNode( TQString::number( fi.lastModified().toTime_t() ) );
|
|
|
|
attr.appendChild( t );
|
|
|
|
i.appendChild( attr );
|
|
|
|
|
|
|
|
return i;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////
|
|
|
|
/// CLASS PlaylistTrackItem
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
PlaylistTrackItem::PlaylistTrackItem( TQListViewItem *parent, TQListViewItem *after, TrackItemInfo *info )
|
|
|
|
: PlaylistBrowserEntry( parent, after )
|
|
|
|
, m_trackInfo( info )
|
|
|
|
{
|
|
|
|
setDragEnabled( true );
|
|
|
|
setRenameEnabled( 0, false );
|
|
|
|
PlaylistEntry *p = dynamic_cast<PlaylistEntry *>(parent);
|
|
|
|
if(!p)
|
|
|
|
debug() << "parent: " << parent << " is not a PlaylistEntry" << endl;
|
|
|
|
if( p && p->text( 0 ).contains( info->artist() ) )
|
|
|
|
setText( 0, info->title() );
|
|
|
|
else
|
|
|
|
setText( 0, i18n("%1 - %2").tqarg( info->artist(), info->title() ) );
|
|
|
|
}
|
|
|
|
|
|
|
|
const KURL &PlaylistTrackItem::url()
|
|
|
|
{
|
|
|
|
return m_trackInfo->url();
|
|
|
|
}
|
|
|
|
|
|
|
|
void PlaylistTrackItem::slotDoubleClicked()
|
|
|
|
{
|
|
|
|
Playlist::instance()->insertMedia( url(), Playlist::DefaultOptions );
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void PlaylistTrackItem::showContextMenu( const TQPoint &position )
|
|
|
|
{
|
|
|
|
KPopupMenu menu( listView() );
|
|
|
|
enum Actions { LOAD, APPEND, TQUEUE, BURN, REMOVE, INFO };
|
|
|
|
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon( "files" ) ), i18n( "&Load" ), LOAD );
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon( "add_playlist" ) ), i18n( "&Append to Playlist" ), APPEND );
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon( "queue_track" ) ), i18n( "&Queue Track" ), TQUEUE );
|
|
|
|
|
|
|
|
|
|
|
|
menu.insertSeparator();
|
|
|
|
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon( "burn" ) ), i18n("Burn to CD"), BURN );
|
|
|
|
menu.setItemEnabled( BURN, K3bExporter::isAvailable() && url().isLocalFile() );
|
|
|
|
|
|
|
|
menu.insertSeparator();
|
|
|
|
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon( "remove_from_playlist" ) ), i18n( "&Remove" ), REMOVE );
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon( "info" ) ), i18n( "Edit Track &Information..." ), INFO );
|
|
|
|
|
|
|
|
switch( menu.exec( position ) ) {
|
|
|
|
case LOAD:
|
|
|
|
Playlist::instance()->clear(); //FALL THROUGH
|
|
|
|
case APPEND:
|
|
|
|
PlaylistBrowser::instance()->addSelectedToPlaylist( Playlist::Append );
|
|
|
|
break;
|
|
|
|
case TQUEUE:
|
|
|
|
PlaylistBrowser::instance()->addSelectedToPlaylist( Playlist::Queue );
|
|
|
|
break;
|
|
|
|
case BURN:
|
|
|
|
K3bExporter::instance()->exportTracks( url() );
|
|
|
|
break;
|
|
|
|
case REMOVE:
|
|
|
|
PlaylistBrowser::instance()->removeSelectedItems();
|
|
|
|
break;
|
|
|
|
case INFO:
|
|
|
|
if( !url().isLocalFile() )
|
|
|
|
KMessageBox::sorry( PlaylistBrowser::instance(), i18n( "Track information is not available for remote media." ) );
|
|
|
|
else if( TQFile::exists( url().path() ) ) {
|
|
|
|
TagDialog* dialog = new TagDialog( url() );
|
|
|
|
dialog->show();
|
|
|
|
}
|
|
|
|
else KMessageBox::sorry( PlaylistBrowser::instance(), i18n( "This file does not exist: %1" ).tqarg( url().path() ) );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////
|
|
|
|
/// CLASS TrackItemInfo
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
TrackItemInfo::TrackItemInfo( const MetaBundle &mb )
|
|
|
|
{
|
|
|
|
m_url = mb.url();
|
|
|
|
|
|
|
|
if( mb.isValidMedia() )
|
|
|
|
{
|
|
|
|
m_title = mb.title();
|
|
|
|
m_artist = mb.artist();
|
|
|
|
m_album = mb.album();
|
|
|
|
m_length = mb.length();
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
m_title = MetaBundle::prettyTitle( fileBaseName( m_url.path() ) );
|
|
|
|
m_length = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
if( m_length < 0 )
|
|
|
|
m_length = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/////////////////////////////////////////////////////////////////////////////
|
|
|
|
/// CLASS StreamEntry
|
|
|
|
////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
StreamEntry::StreamEntry( TQListViewItem *parent, TQListViewItem *after, const KURL &u, const TQString &t )
|
|
|
|
: PlaylistBrowserEntry( parent, after )
|
|
|
|
, m_title( t )
|
|
|
|
, m_url( u )
|
|
|
|
{
|
|
|
|
setDragEnabled( true );
|
|
|
|
setRenameEnabled( 0, true );
|
|
|
|
setExpandable( false );
|
|
|
|
|
|
|
|
if( m_title.isEmpty() )
|
|
|
|
m_title = fileBaseName( m_url.prettyURL() );
|
|
|
|
|
|
|
|
setPixmap( 0, SmallIcon( Amarok::icon( "playlist" ) ) );
|
|
|
|
|
|
|
|
setText( 0, m_title );
|
|
|
|
}
|
|
|
|
|
|
|
|
StreamEntry::StreamEntry( TQListViewItem *parent, TQListViewItem *after, const TQDomElement &xmlDefinition )
|
|
|
|
: PlaylistBrowserEntry( parent, after )
|
|
|
|
{
|
|
|
|
setDragEnabled( true );
|
|
|
|
setRenameEnabled( 0, true );
|
|
|
|
setExpandable( false );
|
|
|
|
|
|
|
|
m_title = xmlDefinition.attribute( "name" );
|
|
|
|
TQDomElement e = xmlDefinition.namedItem( "url" ).toElement();
|
|
|
|
m_url = KURL::fromPathOrURL( e.text() );
|
|
|
|
|
|
|
|
|
|
|
|
if( m_title.isEmpty() )
|
|
|
|
m_title = fileBaseName( m_url.prettyURL() );
|
|
|
|
|
|
|
|
setPixmap( 0, SmallIcon( Amarok::icon( "playlist" ) ) );
|
|
|
|
|
|
|
|
setText( 0, m_title );
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
TQDomElement StreamEntry::xml() const
|
|
|
|
{
|
|
|
|
TQDomDocument doc;
|
|
|
|
TQDomElement i = doc.createElement("stream");
|
|
|
|
i.setAttribute( "name", title() );
|
|
|
|
if( isOpen() )
|
|
|
|
i.setAttribute( "isOpen", "true" );
|
|
|
|
TQDomElement url = doc.createElement( "url" );
|
|
|
|
url.appendChild( doc.createTextNode( m_url.prettyURL() ));
|
|
|
|
i.appendChild( url );
|
|
|
|
return i;
|
|
|
|
}
|
|
|
|
|
|
|
|
void StreamEntry::updateInfo()
|
|
|
|
{
|
|
|
|
const TQString body = "<tr><td><b>%1</b></td><td>%2</td></tr>";
|
|
|
|
|
|
|
|
TQString str = "<html><body><table width=\"100%\" border=\"0\">";
|
|
|
|
|
|
|
|
str += body.tqarg( i18n( "URL" ), m_url.prettyURL() );
|
|
|
|
str += "</table></body></html>";
|
|
|
|
|
|
|
|
PlaylistBrowser::instance()->setInfo( text(0), str );
|
|
|
|
}
|
|
|
|
|
|
|
|
void StreamEntry::slotDoubleClicked()
|
|
|
|
{
|
|
|
|
Playlist::instance()->proposePlaylistName( text(0) );
|
|
|
|
Playlist::instance()->insertMedia( url(), Playlist::DefaultOptions );
|
|
|
|
}
|
|
|
|
|
|
|
|
void StreamEntry::setup()
|
|
|
|
{
|
|
|
|
TQFontMetrics fm( listView()->font() );
|
|
|
|
int margin = listView()->itemMargin()*2;
|
|
|
|
int h = fm.lineSpacing();
|
|
|
|
if ( h % 2 > 0 ) h++;
|
|
|
|
setHeight( h + margin );
|
|
|
|
}
|
|
|
|
|
|
|
|
void StreamEntry::paintCell( TQPainter *p, const TQColorGroup &cg, int column, int width, int align )
|
|
|
|
{
|
|
|
|
//flicker-free drawing
|
|
|
|
static TQPixmap buffer;
|
|
|
|
buffer.resize( width, height() );
|
|
|
|
|
|
|
|
if( buffer.isNull() )
|
|
|
|
{
|
|
|
|
KListViewItem::paintCell( p, cg, column, width, align );
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
TQPainter pBuf( &buffer, true );
|
|
|
|
// use alternate background
|
|
|
|
#if TDE_VERSION < KDE_MAKE_VERSION(3,3,91)
|
|
|
|
pBuf.fillRect( buffer.rect(), isSelected() ? cg.highlight() : backgroundColor() );
|
|
|
|
#else
|
|
|
|
pBuf.fillRect( buffer.rect(), isSelected() ? cg.highlight() : backgroundColor(0) );
|
|
|
|
#endif
|
|
|
|
|
|
|
|
KListView *lv = static_cast<KListView *>( listView() );
|
|
|
|
|
|
|
|
TQFont font( p->font() );
|
|
|
|
TQFontMetrics fm( p->fontMetrics() );
|
|
|
|
|
|
|
|
int text_x = 0;// lv->treeStepSize() + 3;
|
|
|
|
int textHeight;
|
|
|
|
|
|
|
|
textHeight = height();
|
|
|
|
|
|
|
|
pBuf.setPen( isSelected() ? cg.highlightedText() : cg.text() );
|
|
|
|
|
|
|
|
if( pixmap(column) ) {
|
|
|
|
int y = (textHeight - pixmap(column)->height())/2;
|
|
|
|
pBuf.drawPixmap( text_x, y, *pixmap(column) );
|
|
|
|
text_x += pixmap(column)->width()+4;
|
|
|
|
}
|
|
|
|
|
|
|
|
pBuf.setFont( font );
|
|
|
|
TQFontMetrics fmName( font );
|
|
|
|
|
|
|
|
TQString name = text(column);
|
|
|
|
const int _width = width - text_x - lv->itemMargin()*2;
|
|
|
|
if( fmName.width( name ) > _width )
|
|
|
|
{
|
|
|
|
name = KStringHandler::rPixelSqueeze( name, pBuf.fontMetrics(), _width );
|
|
|
|
}
|
|
|
|
|
|
|
|
pBuf.drawText( text_x, 0, width - text_x, textHeight, AlignVCenter, name );
|
|
|
|
|
|
|
|
pBuf.end();
|
|
|
|
p->drawPixmap( 0, 0, buffer );
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
StreamEntry::showContextMenu( const TQPoint &position )
|
|
|
|
{
|
|
|
|
KPopupMenu menu( listView() );
|
|
|
|
enum Actions { LOAD, APPEND, TQUEUE, EDIT, REMOVE };
|
|
|
|
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon( "files" ) ), i18n( "&Load" ), LOAD );
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon( "add_playlist" ) ), i18n( "&Append to Playlist" ), APPEND );
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon( "queue_track" ) ), i18n( "&Queue Tracks" ), TQUEUE );
|
|
|
|
menu.insertSeparator();
|
|
|
|
|
|
|
|
// Forbid editing non removable items
|
|
|
|
if( isKept() )
|
|
|
|
{
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon("edit") ), i18n( "E&dit" ), EDIT );
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon("remove_from_playlist") ), i18n( "&Delete" ), REMOVE );
|
|
|
|
}
|
|
|
|
else
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon( "info" ) ), i18n( "Show &Information" ), EDIT );
|
|
|
|
|
|
|
|
switch( menu.exec( position ) )
|
|
|
|
{
|
|
|
|
case LOAD:
|
|
|
|
Playlist::instance()->clear();
|
|
|
|
Playlist::instance()->setPlaylistName( text(0) );
|
|
|
|
//FALL THROUGH
|
|
|
|
case APPEND:
|
|
|
|
PlaylistBrowser::instance()->addSelectedToPlaylist( Playlist::Append );
|
|
|
|
break;
|
|
|
|
case TQUEUE:
|
|
|
|
PlaylistBrowser::instance()->addSelectedToPlaylist( Playlist::Queue );
|
|
|
|
break;
|
|
|
|
case EDIT:
|
|
|
|
PlaylistBrowser::instance()->editStreamURL( this, !isKept() ); //only editable if we keep it
|
|
|
|
if( dynamic_cast<LastFmEntry*>(this) )
|
|
|
|
PlaylistBrowser::instance()->saveLastFm();
|
|
|
|
else
|
|
|
|
PlaylistBrowser::instance()->saveStreams();
|
|
|
|
break;
|
|
|
|
case REMOVE:
|
|
|
|
PlaylistBrowser::instance()->removeSelectedItems();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/////////////////////////////////////////////////////////////////////////////
|
|
|
|
/// CLASS LastFmEntry
|
|
|
|
////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
TQDomElement LastFmEntry::xml() const
|
|
|
|
{
|
|
|
|
TQDomDocument doc;
|
|
|
|
TQDomElement i = doc.createElement("lastfm");
|
|
|
|
i.setAttribute( "name", title() );
|
|
|
|
if( isOpen() )
|
|
|
|
i.setAttribute( "isOpen", "true" );
|
|
|
|
TQDomElement url = doc.createElement( "url" );
|
|
|
|
url.appendChild( doc.createTextNode( m_url.prettyURL() ));
|
|
|
|
i.appendChild( url );
|
|
|
|
return i;
|
|
|
|
}
|
|
|
|
|
|
|
|
/////////////////////////////////////////////////////////////////////////////
|
|
|
|
/// CLASS StreamEditor
|
|
|
|
////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
StreamEditor::StreamEditor( TQWidget *parent, const TQString &title, const TQString &url, bool readonly )
|
|
|
|
: KDialogBase( parent, "StreamEditor", true, TQString(), Ok|Cancel)
|
|
|
|
{
|
|
|
|
makeGridMainWidget( 2, Qt::Horizontal );
|
|
|
|
|
|
|
|
TQLabel *nameLabel = new TQLabel( i18n("&Name:"), mainWidget() );
|
|
|
|
m_nameLineEdit = new KLineEdit( title, mainWidget() );
|
|
|
|
m_nameLineEdit->setReadOnly( readonly );
|
|
|
|
nameLabel->setBuddy( m_nameLineEdit );
|
|
|
|
|
|
|
|
TQLabel *urlLabel = new TQLabel( i18n("&Url:"), mainWidget() );
|
|
|
|
m_urlLineEdit = new KLineEdit( url, mainWidget() );
|
|
|
|
m_urlLineEdit->setReadOnly( readonly );
|
|
|
|
urlLabel->setBuddy( m_urlLineEdit );
|
|
|
|
|
|
|
|
if( !readonly )
|
|
|
|
m_nameLineEdit->setFocus();
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// In case of readonly ok button makes no sense
|
|
|
|
showButtonOK( false );
|
|
|
|
// Change Cancel to Close button
|
|
|
|
setButtonCancel( KStdGuiItem::close() );
|
|
|
|
}
|
|
|
|
|
|
|
|
TQSize min( 480, 110 );
|
|
|
|
setInitialSize( min );
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/////////////////////////////////////////////////////////////////////////////
|
|
|
|
/// CLASS DynamicEntry
|
|
|
|
////////////////////////////////////////////////////////////////////////////
|
|
|
|
DynamicEntry::DynamicEntry( TQListViewItem *parent, TQListViewItem *after, const TQString &name )
|
|
|
|
: PlaylistBrowserEntry( parent, after, name )
|
|
|
|
, DynamicMode( name )
|
|
|
|
{
|
|
|
|
setPixmap( 0, SmallIcon( Amarok::icon( "dynamic" ) ) );
|
|
|
|
setDragEnabled( true );
|
|
|
|
}
|
|
|
|
|
|
|
|
DynamicEntry::DynamicEntry( TQListViewItem *parent, TQListViewItem *after, const TQDomElement &xmlDefinition )
|
|
|
|
: PlaylistBrowserEntry( parent, after )
|
|
|
|
, DynamicMode( xmlDefinition.attribute( "name" ) )
|
|
|
|
{
|
|
|
|
setPixmap( 0, SmallIcon( Amarok::icon( "dynamic" ) ) );
|
|
|
|
setDragEnabled( true );
|
|
|
|
|
|
|
|
TQDomElement e;
|
|
|
|
|
|
|
|
setCycleTracks ( xmlDefinition.namedItem( "cycleTracks" ).toElement().text() == "true" );
|
|
|
|
setUpcomingCount( xmlDefinition.namedItem( "upcoming" ).toElement().text().toInt() );
|
|
|
|
setPreviousCount( xmlDefinition.namedItem( "previous" ).toElement().text().toInt() );
|
|
|
|
|
|
|
|
setAppendType( xmlDefinition.namedItem( "appendType" ).toElement().text().toInt() );
|
|
|
|
|
|
|
|
if ( appendType() == 2 ) {
|
|
|
|
setItems( TQStringList::split( ',', xmlDefinition.namedItem( "items" ).toElement().text() ) );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
TQString DynamicEntry::text( int column ) const
|
|
|
|
{
|
|
|
|
if( column == 0 )
|
|
|
|
return title();
|
|
|
|
return PlaylistBrowserEntry::text( column );
|
|
|
|
}
|
|
|
|
|
|
|
|
TQDomElement DynamicEntry::xml() const
|
|
|
|
{
|
|
|
|
TQDomDocument doc;
|
|
|
|
TQDomElement i;
|
|
|
|
|
|
|
|
i = doc.createElement("dynamic");
|
|
|
|
i.setAttribute( "name", title() );
|
|
|
|
if( isOpen() )
|
|
|
|
i.setAttribute( "isOpen", "true" );
|
|
|
|
|
|
|
|
TQDomElement attr = doc.createElement( "cycleTracks" );
|
|
|
|
TQDomText t = doc.createTextNode( cycleTracks() ? "true" : "false" );
|
|
|
|
attr.appendChild( t );
|
|
|
|
i.appendChild( attr );
|
|
|
|
|
|
|
|
attr = doc.createElement( "upcoming" );
|
|
|
|
t = doc.createTextNode( TQString::number( upcomingCount() ) );
|
|
|
|
attr.appendChild( t );
|
|
|
|
i.appendChild( attr );
|
|
|
|
|
|
|
|
attr = doc.createElement( "previous" );
|
|
|
|
t = doc.createTextNode( TQString::number( previousCount() ) );
|
|
|
|
attr.appendChild( t );
|
|
|
|
i.appendChild( attr );
|
|
|
|
|
|
|
|
attr = doc.createElement( "appendType" );
|
|
|
|
t = doc.createTextNode( TQString::number( appendType() ) );
|
|
|
|
attr.appendChild( t );
|
|
|
|
i.appendChild( attr );
|
|
|
|
|
|
|
|
TQString list;
|
|
|
|
if( appendType() == 2 ) {
|
|
|
|
TQStringList itemsl = items();
|
|
|
|
for( uint c = 0; c < itemsl.count(); c = c + 2 ) {
|
|
|
|
list.append( itemsl[c] );
|
|
|
|
list.append( ',' );
|
|
|
|
list.append( itemsl[c+1] );
|
|
|
|
if ( c < itemsl.count()-1 )
|
|
|
|
list.append( ',' );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
attr = doc.createElement( "items" );
|
|
|
|
t = doc.createTextNode( list );
|
|
|
|
attr.appendChild( t );
|
|
|
|
i.appendChild( attr );
|
|
|
|
return i;
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
DynamicEntry::slotDoubleClicked()
|
|
|
|
{
|
|
|
|
Playlist::instance()->loadDynamicMode( this );
|
|
|
|
Playlist::instance()->setPlaylistName( text(0) );
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void
|
|
|
|
DynamicEntry::showContextMenu( const TQPoint &position )
|
|
|
|
{
|
|
|
|
KPopupMenu menu( listView() );
|
|
|
|
|
|
|
|
enum Actions { LOAD, RENAME, REMOVE, EDIT };
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon( "files" ) ), i18n( "&Load" ), LOAD );
|
|
|
|
menu.insertSeparator();
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon("edit") ), i18n( "E&dit" ), EDIT );
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon("remove_from_playlist") ), i18n( "&Delete" ), REMOVE );
|
|
|
|
|
|
|
|
if( !isKept() )
|
|
|
|
menu.setItemEnabled( REMOVE, false );
|
|
|
|
|
|
|
|
switch( menu.exec( position ) )
|
|
|
|
{
|
|
|
|
case LOAD:
|
|
|
|
slotDoubleClicked();
|
|
|
|
break;
|
|
|
|
case EDIT:
|
|
|
|
edit();
|
|
|
|
break;
|
|
|
|
case REMOVE:
|
|
|
|
PlaylistBrowser::instance()->removeSelectedItems();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/////////////////////////////////////////////////////////////////////////////
|
|
|
|
/// CLASS PodcastChannel
|
|
|
|
////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
PodcastChannel::PodcastChannel( TQListViewItem *parent, TQListViewItem *after, const KURL &url )
|
|
|
|
: PlaylistBrowserEntry( parent, after )
|
|
|
|
, m_polished( true ) // we get the items immediately if url is given
|
|
|
|
, m_url( url )
|
|
|
|
, m_fetching( false )
|
|
|
|
, m_updating( false )
|
|
|
|
, m_new( false )
|
|
|
|
, m_hasProblem( false )
|
|
|
|
, m_parent( static_cast<PlaylistCategory*>(parent) )
|
|
|
|
, m_settingsValid( false )
|
|
|
|
{
|
|
|
|
setDragEnabled( true );
|
|
|
|
setRenameEnabled( 0, false );
|
|
|
|
|
|
|
|
setText(0, i18n("Retrieving Podcast...") ); //HACK to fill loading time space
|
|
|
|
setPixmap( 0, SmallIcon( Amarok::icon( "podcast" ) ) );
|
|
|
|
|
|
|
|
fetch();
|
|
|
|
}
|
|
|
|
|
|
|
|
PodcastChannel::PodcastChannel( TQListViewItem *parent, TQListViewItem *after, const KURL &url,
|
|
|
|
const TQDomNode &channelSettings )
|
|
|
|
: PlaylistBrowserEntry( parent, after )
|
|
|
|
, m_polished( true ) // we get the items immediately if url is given
|
|
|
|
, m_url( url )
|
|
|
|
, m_fetching( false )
|
|
|
|
, m_updating( false )
|
|
|
|
, m_new( false )
|
|
|
|
, m_hasProblem( false )
|
|
|
|
, m_parent( static_cast<PlaylistCategory*>(parent) )
|
|
|
|
, m_settingsValid( true )
|
|
|
|
{
|
|
|
|
setDragEnabled( true );
|
|
|
|
setRenameEnabled( 0, false );
|
|
|
|
|
|
|
|
setDOMSettings( channelSettings );
|
|
|
|
|
|
|
|
setText(0, i18n("Retrieving Podcast...") ); //HACK to fill loading time space
|
|
|
|
setPixmap( 0, SmallIcon( Amarok::icon( "podcast" ) ) );
|
|
|
|
|
|
|
|
fetch();
|
|
|
|
}
|
|
|
|
|
|
|
|
PodcastChannel::PodcastChannel( TQListViewItem *parent, TQListViewItem *after,
|
|
|
|
const KURL &url, const TQDomNode &channelSettings,
|
|
|
|
const TQDomDocument &xmlDefinition )
|
|
|
|
: PlaylistBrowserEntry( parent, after )
|
|
|
|
, m_polished( true ) //automatically load the channel
|
|
|
|
, m_url( url )
|
|
|
|
, m_fetching( false )
|
|
|
|
, m_updating( false )
|
|
|
|
, m_new( false )
|
|
|
|
, m_hasProblem( false )
|
|
|
|
, m_parent( static_cast<PlaylistCategory*>(parent) )
|
|
|
|
, m_settingsValid( true )
|
|
|
|
{
|
|
|
|
TQDomNode type = xmlDefinition.namedItem("rss");
|
|
|
|
if( !type.isNull() )
|
|
|
|
setXml( type.namedItem("channel"), RSS );
|
|
|
|
else
|
|
|
|
setXml( type, ATOM );
|
|
|
|
|
|
|
|
setDOMSettings( channelSettings );
|
|
|
|
|
|
|
|
setDragEnabled( true );
|
|
|
|
setRenameEnabled( 0, false );
|
|
|
|
|
|
|
|
setPixmap( 0, SmallIcon( Amarok::icon( "podcast" ) ) );
|
|
|
|
}
|
|
|
|
|
|
|
|
PodcastChannel::PodcastChannel( TQListViewItem *parent, TQListViewItem *after, const PodcastChannelBundle &pcb )
|
|
|
|
: PlaylistBrowserEntry( parent, after )
|
|
|
|
, m_bundle( pcb )
|
|
|
|
, m_polished( false )
|
|
|
|
, m_url( pcb.url() )
|
|
|
|
, m_fetching( false )
|
|
|
|
, m_updating( false )
|
|
|
|
, m_new( false )
|
|
|
|
, m_hasProblem( false )
|
|
|
|
, m_parent( static_cast<PlaylistCategory*>(parent) )
|
|
|
|
, m_settingsValid( true )
|
|
|
|
{
|
|
|
|
setText( 0, title() );
|
|
|
|
setDragEnabled( true );
|
|
|
|
setRenameEnabled( 0, false );
|
|
|
|
setPixmap( 0, SmallIcon( Amarok::icon( "podcast" ) ) );
|
|
|
|
setExpandable( true );
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
PodcastChannel::setDOMSettings( const TQDomNode &channelSettings )
|
|
|
|
{
|
|
|
|
TQString save = channelSettings.namedItem("savelocation").toElement().text();
|
|
|
|
bool scan = channelSettings.namedItem("autoscan").toElement().text() == "true";
|
|
|
|
bool hasPurge = channelSettings.namedItem("purge").toElement().text() == "true";
|
|
|
|
int purgeCount = channelSettings.namedItem("purgecount").toElement().text().toInt();
|
|
|
|
int fetchType = STREAM;
|
|
|
|
|
|
|
|
if( channelSettings.namedItem( "fetch").toElement().text() == "automatic" )
|
|
|
|
fetchType = AUTOMATIC;
|
|
|
|
|
|
|
|
KURL saveURL;
|
|
|
|
TQString t = title();
|
|
|
|
if( save.isEmpty() )
|
|
|
|
save = Amarok::saveLocation( "podcasts/" + Amarok::vfatPath( t ) );
|
|
|
|
|
|
|
|
PodcastSettings *settings = new PodcastSettings( t, save, scan, fetchType, false/*transfer*/, hasPurge, purgeCount );
|
|
|
|
m_bundle.setSettings( settings );
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
PodcastChannel::configure()
|
|
|
|
{
|
|
|
|
PodcastSettingsDialog *dialog = new PodcastSettingsDialog( m_bundle.getSettings() );
|
|
|
|
|
|
|
|
if( dialog->configure() )
|
|
|
|
{
|
|
|
|
setSettings( dialog->getSettings() );
|
|
|
|
}
|
|
|
|
|
|
|
|
delete dialog->getSettings();
|
|
|
|
delete dialog;
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
PodcastChannel::checkAndSetNew()
|
|
|
|
{
|
|
|
|
for( TQListViewItem *child = firstChild(); child; child = child->nextSibling() )
|
|
|
|
{
|
|
|
|
if( static_cast<PodcastEpisode*>(child)->isNew() )
|
|
|
|
{
|
|
|
|
setNew( true );
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
setNew( false );
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
PodcastChannel::setListened( const bool n /*true*/ )
|
|
|
|
{
|
|
|
|
if( !isPolished() )
|
|
|
|
load();
|
|
|
|
|
|
|
|
TQListViewItem *child = firstChild();
|
|
|
|
while( child )
|
|
|
|
{
|
|
|
|
static_cast<PodcastEpisode*>(child)->setListened( n );
|
|
|
|
child = child->nextSibling();
|
|
|
|
}
|
|
|
|
|
|
|
|
setNew( !n );
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
PodcastChannel::setOpen( bool b )
|
|
|
|
{
|
|
|
|
if( b == isOpen())
|
|
|
|
return;
|
|
|
|
|
|
|
|
if( isPolished() )
|
|
|
|
{
|
|
|
|
TQListViewItem::setOpen( b );
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// not polished
|
|
|
|
if( b ) load();
|
|
|
|
TQListViewItem::setOpen( b );
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
PodcastChannel::load()
|
|
|
|
{
|
|
|
|
m_polished = true;
|
|
|
|
|
|
|
|
bool hasNew = m_new;
|
|
|
|
int episodeCount = hasPurge() ? purgeCount() : -1;
|
|
|
|
TQValueList<PodcastEpisodeBundle> episodes;
|
|
|
|
episodes = CollectionDB::instance()->getPodcastEpisodes( url(), false, episodeCount );
|
|
|
|
|
|
|
|
PodcastEpisodeBundle bundle;
|
|
|
|
|
|
|
|
// podcasts are hopefully returned chronologically, insert them in reverse
|
|
|
|
while( !episodes.isEmpty() )
|
|
|
|
{
|
|
|
|
bundle = episodes.first();
|
|
|
|
new PodcastEpisode( this, 0, bundle );
|
|
|
|
|
|
|
|
if( bundle.isNew() )
|
|
|
|
hasNew = true;
|
|
|
|
|
|
|
|
episodes.pop_front();
|
|
|
|
}
|
|
|
|
sortChildItems( 0, true );
|
|
|
|
setNew( hasNew );
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
PodcastChannel::setSettings( PodcastSettings *newSettings )
|
|
|
|
{
|
|
|
|
bool downloadMedia = ( (fetchType() != newSettings->fetchType()) && (newSettings->fetchType() == AUTOMATIC) );
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Rewrite local url
|
|
|
|
* Move any downloaded media to the new location
|
|
|
|
*/
|
|
|
|
if( saveLocation() != newSettings->saveLocation() )
|
|
|
|
{
|
|
|
|
KURL::List copyList;
|
|
|
|
|
|
|
|
PodcastEpisode *item = static_cast<PodcastEpisode*>( firstChild() );
|
|
|
|
// get a list of the urls of already downloaded items
|
|
|
|
while( item )
|
|
|
|
{
|
|
|
|
if( item->isOnDisk() )
|
|
|
|
{
|
|
|
|
copyList << item->localUrl();
|
|
|
|
item->setLocalUrlBase( newSettings->saveLocation() );
|
|
|
|
}
|
|
|
|
item = static_cast<PodcastEpisode*>( item->nextSibling() );
|
|
|
|
}
|
|
|
|
// move the items
|
|
|
|
if( !copyList.isEmpty() )
|
|
|
|
{
|
|
|
|
//create the local directory first
|
|
|
|
PodcastEpisode::createLocalDir( newSettings->saveLocation() );
|
|
|
|
KIO::CopyJob* m_podcastMoveJob = KIO::move( copyList, KURL::fromPathOrURL( newSettings->saveLocation() ), false );
|
|
|
|
Amarok::StatusBar::instance()->newProgressOperation( m_podcastMoveJob )
|
|
|
|
.setDescription( i18n( "Moving Podcasts" ) );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if( newSettings->autoscan() != autoscan() )
|
|
|
|
{
|
|
|
|
if( autoscan() )
|
|
|
|
PlaylistBrowser::instance()->m_podcastItemsToScan.append( this );
|
|
|
|
else
|
|
|
|
PlaylistBrowser::instance()->m_podcastItemsToScan.remove( this );
|
|
|
|
}
|
|
|
|
|
|
|
|
m_bundle.setSettings( newSettings );
|
|
|
|
CollectionDB::instance()->updatePodcastChannel( m_bundle );
|
|
|
|
|
|
|
|
if( hasPurge() && purgeCount() != childCount() && purgeCount() != 0 )
|
|
|
|
purge();
|
|
|
|
|
|
|
|
if( downloadMedia )
|
|
|
|
downloadChildren();
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
PodcastChannel::downloadChildren()
|
|
|
|
{
|
|
|
|
TQListViewItem *item = firstChild();
|
|
|
|
while( item )
|
|
|
|
{
|
|
|
|
#define item static_cast<PodcastEpisode*>(item)
|
|
|
|
if( item->isNew() )
|
|
|
|
m_podcastDownloadQueue.append( item );
|
|
|
|
#undef item
|
|
|
|
|
|
|
|
item = item->nextSibling();
|
|
|
|
}
|
|
|
|
downloadChildQueue();
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
PodcastChannel::downloadChildQueue()
|
|
|
|
{
|
|
|
|
if( m_podcastDownloadQueue.isEmpty() ) return;
|
|
|
|
|
|
|
|
PodcastEpisode *first = m_podcastDownloadQueue.first();
|
|
|
|
first->downloadMedia();
|
|
|
|
m_podcastDownloadQueue.removeFirst();
|
|
|
|
|
|
|
|
connect( first, TQT_SIGNAL( downloadFinished() ), this, TQT_SLOT( downloadChildQueue() ) );
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
PodcastChannel::fetch()
|
|
|
|
{
|
|
|
|
setText( 0, i18n( "Retrieving Podcast..." ) );
|
|
|
|
|
|
|
|
m_iconCounter = 1;
|
|
|
|
startAnimation();
|
|
|
|
connect( &m_animationTimer, TQT_SIGNAL(timeout()), this, TQT_SLOT(slotAnimation()) );
|
|
|
|
|
|
|
|
m_podcastJob = KIO::storedGet( m_url, false, false );
|
|
|
|
|
|
|
|
Amarok::StatusBar::instance()->newProgressOperation( m_podcastJob )
|
|
|
|
.setDescription( i18n( "Fetching Podcast" ) )
|
|
|
|
.setAbortSlot( this, TQT_SLOT( abortFetch() ) );
|
|
|
|
|
|
|
|
connect( m_podcastJob, TQT_SIGNAL( result( KIO::Job* ) ), TQT_SLOT( fetchResult( KIO::Job* ) ) );
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
PodcastChannel::abortFetch()
|
|
|
|
{
|
|
|
|
m_podcastJob->kill();
|
|
|
|
|
|
|
|
stopAnimation();
|
|
|
|
title().isEmpty() ?
|
|
|
|
setText( 0, m_url.prettyURL() ) :
|
|
|
|
setText( 0, title() );
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
PodcastChannel::fetchResult( KIO::Job* job ) //SLOT
|
|
|
|
{
|
|
|
|
stopAnimation();
|
|
|
|
if ( job->error() != 0 )
|
|
|
|
{
|
|
|
|
Amarok::StatusBar::instance()->shortMessage( i18n( "Unable to connect to Podcast server." ) );
|
|
|
|
debug() << "Unable to retrieve podcast information. KIO Error: " << job->error() << endl;
|
|
|
|
|
|
|
|
title().isEmpty() ?
|
|
|
|
setText( 0, m_url.prettyURL() ) :
|
|
|
|
setText( 0, title() );
|
|
|
|
setPixmap( 0, SmallIcon("cancel") );
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
KIO::StoredTransferJob* const storedJob = static_cast<KIO::StoredTransferJob*>( job );
|
|
|
|
|
|
|
|
TQDomDocument d;
|
|
|
|
|
|
|
|
TQString data = TQString( storedJob->data() );
|
|
|
|
TQString error;
|
|
|
|
int errorline, errorcolumn;
|
|
|
|
if( !d.setContent( storedJob->data(), false /* disable namespace processing */,
|
|
|
|
&error, &errorline, &errorcolumn ) )
|
|
|
|
{
|
|
|
|
Amarok::StatusBar::instance()->shortMessage( i18n("Podcast returned invalid data.") );
|
|
|
|
debug() << "Podcast DOM failure in line " << errorline << ", column " << errorcolumn << ": " << error << endl;
|
|
|
|
|
|
|
|
title().isEmpty() ?
|
|
|
|
setText( 0, m_url.prettyURL() ) :
|
|
|
|
setText( 0, title() );
|
|
|
|
setPixmap( 0, SmallIcon("cancel") );
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
TQDomNode type = d.elementsByTagName("rss").item( 0 );
|
|
|
|
if( type.isNull() || type.toElement().attribute( "version" ) != "2.0" )
|
|
|
|
{
|
|
|
|
type = d.elementsByTagName("feed").item( 0 );
|
|
|
|
if( type.isNull() )
|
|
|
|
{
|
|
|
|
Amarok::StatusBar::instance()->shortMessage( i18n("Sorry, only RSS 2.0 or Atom feeds for podcasts!") );
|
|
|
|
|
|
|
|
if( title().isEmpty() )
|
|
|
|
setText( 0, m_url.prettyURL() );
|
|
|
|
|
|
|
|
setPixmap( 0, SmallIcon("cancel") );
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// feed is ATOM
|
|
|
|
else
|
|
|
|
{
|
|
|
|
setXml( type, ATOM );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// feed is rss 2.0
|
|
|
|
else
|
|
|
|
setXml( type.namedItem("channel"), RSS );
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
PodcastChannel::removeChildren()
|
|
|
|
{
|
|
|
|
TQListViewItem *child, *next;
|
|
|
|
if ( (child = firstChild()) )
|
|
|
|
{
|
|
|
|
while ( (next = child->nextSibling()) )
|
|
|
|
{
|
|
|
|
delete child;
|
|
|
|
child=next;
|
|
|
|
}
|
|
|
|
delete child;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
PodcastChannel::rescan()
|
|
|
|
{
|
|
|
|
m_updating = true;
|
|
|
|
fetch();
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
PodcastChannel::setNew( bool n )
|
|
|
|
{
|
|
|
|
if( n )
|
|
|
|
setPixmap( 0, SmallIcon( Amarok::icon( "podcast2" ) ) );
|
|
|
|
else if( m_hasProblem )
|
|
|
|
setPixmap( 0, SmallIcon("cancel") );
|
|
|
|
else
|
|
|
|
setPixmap( 0, SmallIcon( Amarok::icon( "podcast" ) ) );
|
|
|
|
|
|
|
|
m_new = n;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// DON'T TOUCH m_url!!! The podcast has no mention to the location of the xml file.
|
|
|
|
void
|
|
|
|
PodcastChannel::setXml( const TQDomNode &xml, const int feedType )
|
|
|
|
{
|
|
|
|
/// Podcast Channel information
|
|
|
|
const bool isAtom = ( feedType == ATOM );
|
|
|
|
|
|
|
|
TQString t = xml.namedItem( "title" ).toElement().text().remove("\n");
|
|
|
|
|
|
|
|
TQString a = xml.namedItem( "author" ).toElement().text().remove("\n");
|
|
|
|
|
|
|
|
setText( 0, t );
|
|
|
|
|
|
|
|
TQString l = TQString();
|
|
|
|
|
|
|
|
if( isAtom )
|
|
|
|
l = xml.namedItem( "link" ).toElement().attribute( "rel" );
|
|
|
|
else
|
|
|
|
l = xml.namedItem( "link" ).toElement().text();
|
|
|
|
|
|
|
|
TQString d = xml.namedItem( "description" ).toElement().text();
|
|
|
|
TQString id = xml.namedItem( "itunes:summary" ).toElement().text();
|
|
|
|
if( id.length() > d.length() )
|
|
|
|
d = id;
|
|
|
|
TQString c = xml.namedItem( "copyright" ).toElement().text();
|
|
|
|
TQString img = xml.namedItem( "image" ).toElement().namedItem( "url" ).toElement().text();
|
|
|
|
if( img.isEmpty() )
|
|
|
|
img = xml.namedItem( "itunes:image" ).toElement().namedItem( "url" ).toElement().text();
|
|
|
|
if( img.isEmpty() )
|
|
|
|
img = xml.namedItem( "itunes:image" ).toElement().attribute( "href" );
|
|
|
|
if( img.isEmpty() )
|
|
|
|
img = xml.namedItem( "itunes:image" ).toElement().text();
|
|
|
|
|
|
|
|
PodcastSettings * settings = 0;
|
|
|
|
if( m_settingsValid )
|
|
|
|
{
|
|
|
|
settings = m_bundle.getSettings();
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
settings = new PodcastSettings( t );
|
|
|
|
m_settingsValid = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
m_bundle = PodcastChannelBundle( m_url, t, a, l, d, c, settings );
|
|
|
|
delete settings;
|
|
|
|
m_bundle.setImageURL( KURL::fromPathOrURL( img ) );
|
|
|
|
|
|
|
|
m_bundle.setParentId( m_parent->id() );
|
|
|
|
if( !m_updating )
|
|
|
|
{ // don't reinsert on a refresh
|
|
|
|
debug() << "Adding podcast to database" << endl;
|
|
|
|
CollectionDB::instance()->addPodcastChannel( m_bundle );
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
debug() << "Updating podcast in database: " << endl;
|
|
|
|
CollectionDB::instance()->updatePodcastChannel( m_bundle );
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Podcast Episodes information
|
|
|
|
|
|
|
|
TQDomNode n;
|
|
|
|
if( isAtom )
|
|
|
|
n = xml.namedItem( "entry" );
|
|
|
|
else
|
|
|
|
n = xml.namedItem( "item" );
|
|
|
|
|
|
|
|
bool hasNew = false;
|
|
|
|
bool downloadMedia = ( fetchType() == AUTOMATIC );
|
|
|
|
TQDomNode node;
|
|
|
|
|
|
|
|
// We use an auto-increment id in the database, so we must insert podcasts in the reverse order
|
|
|
|
// to ensure we can pull them out reliably.
|
|
|
|
|
|
|
|
TQPtrList<TQDomElement> eList;
|
|
|
|
|
|
|
|
for( ; !n.isNull(); n = n.nextSibling() )
|
|
|
|
{
|
|
|
|
if( !n.namedItem( "enclosure" ).toElement().attribute( "url" ).isEmpty() )
|
|
|
|
{
|
|
|
|
//prepending ensures correct order in 99% of the channels, except those who use chronological order
|
|
|
|
eList.prepend( new TQDomElement( n.toElement() ) );
|
|
|
|
}
|
|
|
|
else if( isAtom )
|
|
|
|
{
|
|
|
|
// Atom feeds have multiple nodes called link, only one which has an enclosure.
|
|
|
|
TQDomNode nodes = n.namedItem("link");
|
|
|
|
for( ; !nodes.isNull(); nodes = nodes.nextSibling() )
|
|
|
|
{
|
|
|
|
if( nodes.toElement().attribute("rel") == "enclosure" )
|
|
|
|
{
|
|
|
|
eList.prepend( new TQDomElement( n.toElement() ) );
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
uint i = m_bundle.hasPurge() ? m_bundle.purgeCount() : eList.count();
|
|
|
|
foreachType( TQPtrList<TQDomElement>, eList )
|
|
|
|
{
|
|
|
|
if( !m_updating || ( ( i++ >= eList.count() ) && !episodeExists( (**it), feedType ) ) )
|
|
|
|
{
|
|
|
|
if( !isPolished() )
|
|
|
|
load();
|
|
|
|
PodcastEpisode *ep = new PodcastEpisode( this, 0, (**it), feedType, m_updating/*new*/ );
|
|
|
|
if( m_updating )
|
|
|
|
{
|
|
|
|
ep->setNew( true );
|
|
|
|
hasNew = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if( hasPurge() && purgeCount() != 0 && childCount() > purgeCount() )
|
|
|
|
purge();
|
|
|
|
|
|
|
|
//sortChildItems( 0, true ); // ensure the correct date order
|
|
|
|
|
|
|
|
if( downloadMedia )
|
|
|
|
downloadChildren();
|
|
|
|
|
|
|
|
if( m_updating && hasNew )
|
|
|
|
{
|
|
|
|
setNew();
|
|
|
|
Amarok::StatusBar::instance()->shortMessage( i18n("New podcasts have been retrieved!") );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const bool
|
|
|
|
PodcastChannel::episodeExists( const TQDomNode &xml, const int feedType )
|
|
|
|
{
|
|
|
|
TQString command;
|
|
|
|
if( feedType == RSS )
|
|
|
|
{
|
|
|
|
//check id
|
|
|
|
TQString guid = xml.namedItem( "guid" ).toElement().text();
|
|
|
|
if( !guid.isEmpty() )
|
|
|
|
{
|
|
|
|
command = TQString("SELECT id FROM podcastepisodes WHERE parent='%1' AND guid='%2';")
|
|
|
|
.tqarg( CollectionDB::instance()->escapeString( url().url() ),
|
|
|
|
CollectionDB::instance()->escapeString( guid ) );
|
|
|
|
TQStringList values = CollectionDB::instance()->query( command );
|
|
|
|
return !values.isEmpty();
|
|
|
|
}
|
|
|
|
|
|
|
|
TQString episodeTitle = xml.namedItem( "title" ).toElement().text();
|
|
|
|
KURL episodeURL = xml.namedItem( "enclosure" ).toElement().attribute( "url" );
|
|
|
|
command = TQString("SELECT id FROM podcastepisodes WHERE parent='%1' AND url='%2' AND title='%3';")
|
|
|
|
.tqarg( CollectionDB::instance()->escapeString( url().url() ),
|
|
|
|
CollectionDB::instance()->escapeString( episodeURL.url() ),
|
|
|
|
CollectionDB::instance()->escapeString( episodeTitle ) );
|
|
|
|
TQStringList values = CollectionDB::instance()->query( command );
|
|
|
|
return !values.isEmpty();
|
|
|
|
}
|
|
|
|
|
|
|
|
else if( feedType == ATOM )
|
|
|
|
{
|
|
|
|
//check id
|
|
|
|
TQString guid = xml.namedItem( "id" ).toElement().text();
|
|
|
|
if( !guid.isEmpty() )
|
|
|
|
{
|
|
|
|
command = TQString("SELECT id FROM podcastepisodes WHERE parent='%1' AND guid='%2';")
|
|
|
|
.tqarg( CollectionDB::instance()->escapeString( url().url() ),
|
|
|
|
CollectionDB::instance()->escapeString( guid ) );
|
|
|
|
TQStringList values = CollectionDB::instance()->query( command );
|
|
|
|
return !values.isEmpty();
|
|
|
|
}
|
|
|
|
|
|
|
|
TQString episodeTitle = xml.namedItem("title").toElement().text();
|
|
|
|
TQString episodeURL = TQString();
|
|
|
|
TQDomNode n = xml.namedItem("link");
|
|
|
|
for( ; !n.isNull(); n = n.nextSibling() )
|
|
|
|
{
|
|
|
|
if( n.nodeName() == "link" && n.toElement().attribute("rel") == "enclosure" )
|
|
|
|
{
|
|
|
|
episodeURL = n.toElement().attribute( "href" );
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
command = TQString("SELECT id FROM podcastepisodes WHERE parent='%1' AND url='%2' AND title='%3';")
|
|
|
|
.tqarg( CollectionDB::instance()->escapeString( url().url() ),
|
|
|
|
CollectionDB::instance()->escapeString( episodeURL ),
|
|
|
|
CollectionDB::instance()->escapeString( episodeTitle ) );
|
|
|
|
TQStringList values = CollectionDB::instance()->query( command );
|
|
|
|
|
|
|
|
return !values.isEmpty();
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
PodcastChannel::setParent( PlaylistCategory *newParent )
|
|
|
|
{
|
|
|
|
if( newParent != m_parent )
|
|
|
|
{
|
|
|
|
m_parent->takeItem( this );
|
|
|
|
newParent->insertItem( this );
|
|
|
|
newParent->sortChildItems( 0, true );
|
|
|
|
|
|
|
|
m_parent = newParent;
|
|
|
|
}
|
|
|
|
m_bundle.setParentId( m_parent->id() );
|
|
|
|
|
|
|
|
CollectionDB::instance()->updatePodcastChannel( m_bundle );
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
PodcastChannel::updateInfo()
|
|
|
|
{
|
|
|
|
if( !isPolished() )
|
|
|
|
load();
|
|
|
|
|
|
|
|
const TQString body = "<tr><td><b>%1</b></td><td>%2</td></tr>";
|
|
|
|
|
|
|
|
TQString str = "<html><body><table width=\"100%\" border=\"0\">";
|
|
|
|
|
|
|
|
str += body.tqarg( i18n( "Description" ), description() );
|
|
|
|
str += body.tqarg( i18n( "Website" ), link().prettyURL() );
|
|
|
|
str += body.tqarg( i18n( "Copyright" ), copyright() );
|
|
|
|
str += body.tqarg( i18n( "URL" ), m_url.prettyURL() );
|
|
|
|
str += "</table>";
|
|
|
|
str += i18n( "<p> <b>Episodes</b></p><ul>" );
|
|
|
|
for( TQListViewItem *c = firstChild(); c; c = c->nextSibling() )
|
|
|
|
{
|
|
|
|
str += TQString("<li>%1</li>").tqarg( static_cast<PodcastEpisode*>(c)->title() );
|
|
|
|
}
|
|
|
|
|
|
|
|
str += "</ul></body></html>";
|
|
|
|
|
|
|
|
PlaylistBrowser::instance()->setInfo( text(0), str );
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
PodcastChannel::slotDoubleClicked()
|
|
|
|
{
|
|
|
|
if( !isPolished() )
|
|
|
|
load();
|
|
|
|
KURL::List list;
|
|
|
|
TQListViewItem *child = firstChild();
|
|
|
|
while( child )
|
|
|
|
{
|
|
|
|
#define child static_cast<PodcastEpisode *>(child)
|
|
|
|
child->isOnDisk() ?
|
|
|
|
list.prepend( child->localUrl() ):
|
|
|
|
list.prepend( child->url() );
|
|
|
|
#undef child
|
|
|
|
child = child->nextSibling();
|
|
|
|
}
|
|
|
|
|
|
|
|
Playlist::instance()->proposePlaylistName( text(0) );
|
|
|
|
Playlist::instance()->insertMedia( list, Playlist::DefaultOptions );
|
|
|
|
setNew( false );
|
|
|
|
}
|
|
|
|
|
|
|
|
//maintain max items property
|
|
|
|
void
|
|
|
|
PodcastChannel::purge()
|
|
|
|
{
|
|
|
|
// if the user wants to increase the max items shown, we should find those items and add them
|
|
|
|
// back to the episode list.
|
|
|
|
if( childCount() - purgeCount() <= 0 )
|
|
|
|
{
|
|
|
|
restorePurged();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
KURL::List urlsToDelete;
|
|
|
|
TQValueList<TQListViewItem*> purgedItems;
|
|
|
|
|
|
|
|
TQListViewItem *current = firstChild();
|
|
|
|
for( int i=0; current && i < childCount(); current = current->nextSibling(), i++ )
|
|
|
|
{
|
|
|
|
if( i < purgeCount() )
|
|
|
|
continue;
|
|
|
|
|
|
|
|
purgedItems.append( current );
|
|
|
|
}
|
|
|
|
|
|
|
|
foreachType( TQValueList<TQListViewItem*>, purgedItems )
|
|
|
|
{
|
|
|
|
TQListViewItem *item = *it;
|
|
|
|
|
|
|
|
#define item static_cast<PodcastEpisode*>(item)
|
|
|
|
if( item->isOnDisk() )
|
|
|
|
urlsToDelete.append( item->localUrl() );
|
|
|
|
|
|
|
|
// CollectionDB::instance()->removePodcastEpisode( item->dBId() );
|
|
|
|
m_podcastDownloadQueue.remove( item );
|
|
|
|
#undef item
|
|
|
|
delete item;
|
|
|
|
}
|
|
|
|
|
|
|
|
if( !urlsToDelete.isEmpty() )
|
|
|
|
KIO::del( urlsToDelete );
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
PodcastChannel::restorePurged()
|
|
|
|
{
|
|
|
|
DEBUG_BLOCK
|
|
|
|
int restoreCount = purgeCount() - childCount();
|
|
|
|
|
|
|
|
if( restoreCount <= 0 ) return;
|
|
|
|
|
|
|
|
TQValueList<PodcastEpisodeBundle> episodes;
|
|
|
|
episodes = CollectionDB::instance()->getPodcastEpisodes( url() );
|
|
|
|
|
|
|
|
TQValueList<PodcastEpisodeBundle> possibleEntries;
|
|
|
|
|
|
|
|
int i = 0;
|
|
|
|
|
|
|
|
// qvaluelist has no reverse iterator :-(
|
|
|
|
for( ; !episodes.isEmpty(); )
|
|
|
|
{
|
|
|
|
PodcastEpisodeBundle episode = episodes.last();
|
|
|
|
if ( i >= restoreCount ) break;
|
|
|
|
|
|
|
|
PodcastEpisode *existingItem = static_cast<PodcastEpisode*>( firstChild() );
|
|
|
|
bool skip = false;
|
|
|
|
while ( existingItem )
|
|
|
|
{
|
|
|
|
if ( episode.url() == existingItem->url() &&
|
|
|
|
episode.title() == existingItem->title() &&
|
|
|
|
episode.date() == existingItem->date() &&
|
|
|
|
episode.guid() == existingItem->guid() ) {
|
|
|
|
skip = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
existingItem = static_cast<PodcastEpisode*>( existingItem->nextSibling() );
|
|
|
|
}
|
|
|
|
if( !skip )
|
|
|
|
{
|
|
|
|
possibleEntries.append( episode );
|
|
|
|
i++;
|
|
|
|
}
|
|
|
|
episodes.pop_back();
|
|
|
|
}
|
|
|
|
|
|
|
|
// the sorting of the channels automatically means the new episodes gets placed at the end
|
|
|
|
for( TQValueList<PodcastEpisodeBundle>::Iterator it = possibleEntries.begin(), end = possibleEntries.end();
|
|
|
|
it != end; ++it )
|
|
|
|
new PodcastEpisode( this, 0, (*it) );
|
|
|
|
|
|
|
|
sortChildItems( 0, true );
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
PodcastChannel::startAnimation()
|
|
|
|
{
|
|
|
|
if( !m_animationTimer.isActive() )
|
|
|
|
m_animationTimer.start( ANIMATION_INTERVAL );
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
PodcastChannel::stopAnimation()
|
|
|
|
{
|
|
|
|
m_animationTimer.stop();
|
|
|
|
|
|
|
|
hasNew() ?
|
|
|
|
setPixmap( 0, SmallIcon( Amarok::icon( "podcast2" ) ) ):
|
|
|
|
setPixmap( 0, SmallIcon( Amarok::icon( "podcast" ) ) );
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
PodcastChannel::slotAnimation()
|
|
|
|
{
|
|
|
|
m_iconCounter % 2 ?
|
|
|
|
setPixmap( 0, SmallIcon( Amarok::icon( "podcast" ) ) ):
|
|
|
|
setPixmap( 0, SmallIcon( Amarok::icon( "podcast2" ) ) );
|
|
|
|
|
|
|
|
m_iconCounter++;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void
|
|
|
|
PodcastChannel::showContextMenu( const TQPoint &position )
|
|
|
|
{
|
|
|
|
KPopupMenu menu( listView() );
|
|
|
|
|
|
|
|
enum Actions { LOAD, APPEND, TQUEUE, DELETE, RESCAN, LISTENED, NEW, CONFIG };
|
|
|
|
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon( "files" ) ), i18n( "&Load" ), LOAD );
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon( "add_playlist" ) ), i18n( "&Append to Playlist" ), APPEND );
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon( "queue_track" ) ), i18n( "&Queue Tracks" ), TQUEUE );
|
|
|
|
menu.insertSeparator();
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon( "remove" ) ), i18n( "&Delete" ), DELETE );
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon( "refresh" ) ), i18n( "&Check for Updates" ), RESCAN );
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon( "artist" ) ), i18n( "Mark as &Listened" ), LISTENED );
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon( "artist" ) ), i18n( "Mark as &New" ), NEW );
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon( "configure" ) ), i18n( "&Configure..." ), CONFIG );
|
|
|
|
menu.setItemEnabled( LISTENED, hasNew() );
|
|
|
|
menu.setItemEnabled( CONFIG, m_settingsValid );
|
|
|
|
|
|
|
|
switch( menu.exec( position ) )
|
|
|
|
{
|
|
|
|
case LOAD:
|
|
|
|
Playlist::instance()->clear();
|
|
|
|
Playlist::instance()->setPlaylistName( text(0) );
|
|
|
|
//FALL THROUGH
|
|
|
|
case APPEND:
|
|
|
|
PlaylistBrowser::instance()->addSelectedToPlaylist( Playlist::Append );
|
|
|
|
break;
|
|
|
|
|
|
|
|
case TQUEUE:
|
|
|
|
PlaylistBrowser::instance()->addSelectedToPlaylist( Playlist::Queue );
|
|
|
|
break;
|
|
|
|
|
|
|
|
case RESCAN:
|
|
|
|
rescan();
|
|
|
|
break;
|
|
|
|
|
|
|
|
case LISTENED:
|
|
|
|
setListened();
|
|
|
|
break;
|
|
|
|
|
|
|
|
case NEW:
|
|
|
|
setListened(false);
|
|
|
|
break;
|
|
|
|
case DELETE:
|
|
|
|
PlaylistBrowser::instance()->removeSelectedItems();
|
|
|
|
break;
|
|
|
|
|
|
|
|
case CONFIG:
|
|
|
|
{
|
|
|
|
PlaylistBrowser::instance()->configureSelectedPodcasts();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/////////////////////////////////////////////////////////////////////////////
|
|
|
|
/// CLASS PodcastEpisode
|
|
|
|
/// @note we fucking hate itunes for taking over podcasts and inserting
|
|
|
|
/// their own attributes.
|
|
|
|
////////////////////////////////////////////////////////////////////////////
|
|
|
|
PodcastEpisode::PodcastEpisode( TQListViewItem *parent, TQListViewItem *after,
|
|
|
|
const TQDomElement &xml, const int feedType, const bool &isNew )
|
|
|
|
: PlaylistBrowserEntry( parent, after )
|
|
|
|
, m_parent( parent )
|
|
|
|
, m_fetching( false )
|
|
|
|
, m_onDisk( false )
|
|
|
|
, m_localUrl( KURL() )
|
|
|
|
{
|
|
|
|
const bool isAtom = ( feedType == ATOM );
|
|
|
|
TQString title = xml.namedItem( "title" ).toElement().text().remove("\n");
|
|
|
|
TQString subtitle;
|
|
|
|
|
|
|
|
TQString description, author, date, guid, type;
|
|
|
|
int duration = 0;
|
|
|
|
uint size = 0;
|
|
|
|
KURL link;
|
|
|
|
|
|
|
|
if( isAtom )
|
|
|
|
{
|
|
|
|
for( TQDomNode n = xml.firstChild(); !n.isNull(); n = n.nextSibling() )
|
|
|
|
{
|
|
|
|
if ( n.nodeName() == "summary" ) description = n.toElement().text();
|
|
|
|
else if ( n.nodeName() == "author" ) author = n.toElement().text().remove("\n");
|
|
|
|
else if ( n.nodeName() == "published" ) date = n.toElement().text();
|
|
|
|
else if ( n.nodeName() == "id" ) guid = n.toElement().text();
|
|
|
|
else if ( n.nodeName() == "link" )
|
|
|
|
{
|
|
|
|
if( n.toElement().attribute( "rel" ) == "enclosure" )
|
|
|
|
{
|
|
|
|
const TQString weblink = n.toElement().attribute( "href" );
|
|
|
|
link = KURL::fromPathOrURL( weblink );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
description = xml.namedItem( "description" ).toElement().text();
|
|
|
|
TQString idescription = xml.namedItem( "itunes:summary" ).toElement().text();
|
|
|
|
if( idescription.length() > description.length() )
|
|
|
|
description = idescription;
|
|
|
|
|
|
|
|
if( subtitle.isEmpty() )
|
|
|
|
subtitle = xml.namedItem( "itunes:subtitle" ).toElement().text();
|
|
|
|
|
|
|
|
author = xml.namedItem( "author" ).toElement().text().remove("\n");
|
|
|
|
if( author.isEmpty() )
|
|
|
|
author = xml.namedItem( "itunes:author" ).toElement().text().remove("\n");
|
|
|
|
|
|
|
|
date = xml.namedItem( "pubDate" ).toElement().text();
|
|
|
|
if( date.isEmpty() )
|
|
|
|
date = xml.namedItem( "dc:date" ).toElement().text();
|
|
|
|
|
|
|
|
TQString ds = xml.namedItem( "itunes:duration" ).toElement().text();
|
|
|
|
TQString secs = ds.section( ":", -1, -1 );
|
|
|
|
duration = secs.toInt();
|
|
|
|
TQString min = ds.section( ":", -2, -2 );
|
|
|
|
duration += min.toInt() * 60;
|
|
|
|
TQString h = ds.section( ":", -3, -3 );
|
|
|
|
duration += h.toInt() * 3600;
|
|
|
|
|
|
|
|
size = xml.namedItem( "enclosure" ).toElement().attribute( "length" ).toInt();
|
|
|
|
type = xml.namedItem( "enclosure" ).toElement().attribute( "type" );
|
|
|
|
guid = xml.namedItem( "guid" ).toElement().text();
|
|
|
|
|
|
|
|
const TQString weblink = xml.namedItem( "enclosure" ).toElement().attribute( "url" );
|
|
|
|
|
|
|
|
link = KURL::fromPathOrURL( weblink );
|
|
|
|
}
|
|
|
|
|
|
|
|
if( title.isEmpty() )
|
|
|
|
title = link.fileName();
|
|
|
|
|
|
|
|
KURL parentUrl = static_cast<PodcastChannel*>(parent)->url();
|
|
|
|
m_bundle.setDBId( -1 );
|
|
|
|
m_bundle.setURL( link );
|
|
|
|
m_bundle.setParent( parentUrl );
|
|
|
|
m_bundle.setTitle( title );
|
|
|
|
m_bundle.setSubtitle( subtitle );
|
|
|
|
m_bundle.setAuthor( author );
|
|
|
|
m_bundle.setDescription( description );
|
|
|
|
m_bundle.setDate( date );
|
|
|
|
m_bundle.setType( type );
|
|
|
|
m_bundle.setDuration( duration );
|
|
|
|
m_bundle.setSize( size );
|
|
|
|
m_bundle.setGuid( guid );
|
|
|
|
m_bundle.setNew( isNew );
|
|
|
|
|
|
|
|
int id = CollectionDB::instance()->addPodcastEpisode( m_bundle );
|
|
|
|
m_bundle.setDBId( id );
|
|
|
|
|
|
|
|
setText( 0, title );
|
|
|
|
updatePixmap();
|
|
|
|
setDragEnabled( true );
|
|
|
|
setRenameEnabled( 0, false );
|
|
|
|
}
|
|
|
|
|
|
|
|
PodcastEpisode::PodcastEpisode( TQListViewItem *parent, TQListViewItem *after, PodcastEpisodeBundle &bundle )
|
|
|
|
: PlaylistBrowserEntry( parent, after )
|
|
|
|
, m_parent( parent )
|
|
|
|
, m_bundle( bundle )
|
|
|
|
, m_fetching( false )
|
|
|
|
, m_onDisk( false )
|
|
|
|
{
|
|
|
|
m_localUrl = m_bundle.localUrl();
|
|
|
|
isOnDisk();
|
|
|
|
|
|
|
|
setText( 0, bundle.title() );
|
|
|
|
updatePixmap();
|
|
|
|
setDragEnabled( true );
|
|
|
|
setRenameEnabled( 0, false );
|
|
|
|
}
|
|
|
|
|
|
|
|
int
|
|
|
|
PodcastEpisode::compare( TQListViewItem* item, int col, bool ascending ) const
|
|
|
|
{
|
|
|
|
if ( item->rtti() == PodcastEpisode::RTTI )
|
|
|
|
{
|
|
|
|
int ret;
|
|
|
|
#define item static_cast<PodcastEpisode*>(item)
|
|
|
|
// date is priority
|
|
|
|
bool thisHasDate = m_bundle.dateTime().isValid();
|
|
|
|
bool thatHasDate = item->m_bundle.dateTime().isValid();
|
|
|
|
if( thisHasDate && thatHasDate )
|
|
|
|
{
|
|
|
|
ret = m_bundle.dateTime() < item->m_bundle.dateTime() ? 1 : -1;
|
|
|
|
if ( !ascending ) ret *= -1;
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
// if neither has a date, then we order upon the id in the database. This
|
|
|
|
// should be the order in which it arrives in the feed.
|
|
|
|
if( !thisHasDate && !thatHasDate )
|
|
|
|
{
|
|
|
|
ret = m_bundle.dBId() < item->m_bundle.dBId() ? 1 : -1;
|
|
|
|
if ( !ascending ) ret *= -1;
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
// if one has a date, and the other doesn't, always keep non-dated at the bottom.
|
|
|
|
// hypothetically, this should never happen, but it might.
|
|
|
|
ret = thisHasDate ? 1 : -1;
|
|
|
|
if ( !ascending ) ret *= -1;
|
|
|
|
return ret;
|
|
|
|
#undef item
|
|
|
|
}
|
|
|
|
|
|
|
|
return PlaylistBrowserEntry::compare( item, col, ascending );
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
PodcastEpisode::updatePixmap()
|
|
|
|
{
|
|
|
|
if( isNew() )
|
|
|
|
setPixmap( 0, SmallIcon( Amarok::icon( "podcast2" ) ) );
|
|
|
|
else if( m_onDisk )
|
|
|
|
setPixmap( 0, SmallIcon( "down" ) );
|
|
|
|
else
|
|
|
|
setPixmap( 0, SmallIcon( Amarok::icon( "podcast" ) ) );
|
|
|
|
}
|
|
|
|
|
|
|
|
const bool
|
|
|
|
PodcastEpisode::isOnDisk()
|
|
|
|
{
|
|
|
|
if( m_localUrl.isEmpty() )
|
|
|
|
return false;
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// bool oldOnDisk = m_onDisk;
|
|
|
|
m_onDisk = TQFile::exists( m_localUrl.path() );
|
|
|
|
updatePixmap();
|
|
|
|
// m_bundle.setLocalURL( m_onDisk ? m_localUrl : KURL() );
|
|
|
|
// if( oldOnDisk != m_onDisk && dBId() )
|
|
|
|
// CollectionDB::instance()->updatePodcastEpisode( dBId(), m_bundle );
|
|
|
|
return m_onDisk;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
PodcastEpisode::downloadMedia()
|
|
|
|
{
|
|
|
|
DEBUG_BLOCK
|
|
|
|
DEBUG_THREAD_FUNC_INFO
|
|
|
|
SHOULD_BE_GUI
|
|
|
|
|
|
|
|
if( isOnDisk() )
|
|
|
|
return;
|
|
|
|
|
|
|
|
setText( 0, i18n( "Downloading Media..." ) );
|
|
|
|
|
|
|
|
m_iconCounter = 1;
|
|
|
|
startAnimation();
|
|
|
|
connect( &m_animationTimer, TQT_SIGNAL(timeout()), this, TQT_SLOT(slotAnimation()) );
|
|
|
|
|
|
|
|
KURL localDir;
|
|
|
|
PodcastChannel *channel = dynamic_cast<PodcastChannel*>(m_parent);
|
|
|
|
if( channel )
|
|
|
|
localDir = KURL::fromPathOrURL( channel->saveLocation() );
|
|
|
|
else
|
|
|
|
localDir = KURL::fromPathOrURL( PodcastSettings("Podcasts").saveLocation() );
|
|
|
|
createLocalDir( localDir );
|
|
|
|
|
|
|
|
//filename might get changed by redirects later.
|
|
|
|
m_filename = url().fileName();
|
|
|
|
m_localUrl = localDir;
|
|
|
|
m_podcastEpisodeJob = KIO::storedGet( url().url(), false, false);
|
|
|
|
|
|
|
|
Amarok::StatusBar::instance()->newProgressOperation( m_podcastEpisodeJob )
|
|
|
|
.setDescription( title().isEmpty()
|
|
|
|
? i18n( "Downloading Podcast Media" )
|
|
|
|
: i18n( "Downloading Podcast \"%1\"" ).tqarg( title() ) )
|
|
|
|
.setAbortSlot( this, TQT_SLOT( abortDownload()) )
|
|
|
|
.setProgressSignal( m_podcastEpisodeJob, TQT_SIGNAL( percent( KIO::Job *, unsigned long ) ) );
|
|
|
|
|
|
|
|
connect( m_podcastEpisodeJob, TQT_SIGNAL( result( KIO::Job * ) ), TQT_SLOT( downloadResult( KIO::Job * ) ) );
|
|
|
|
connect( m_podcastEpisodeJob, TQT_SIGNAL( redirection( KIO::Job *,const KURL& ) ), TQT_SLOT( redirected( KIO::Job *,const KURL& ) ) );
|
|
|
|
}
|
|
|
|
|
|
|
|
/* change the localurl if redirected, allows us to use the original filename to transfer to mediadevices*/
|
|
|
|
void PodcastEpisode::redirected( KIO::Job *, const KURL & redirectedUrl )
|
|
|
|
{
|
|
|
|
debug() << "redirecting to " << redirectedUrl << ". filename: " << redirectedUrl.fileName() << endl;
|
|
|
|
m_filename = redirectedUrl.fileName();
|
|
|
|
}
|
|
|
|
|
|
|
|
void PodcastEpisode::createLocalDir( const KURL &localDir )
|
|
|
|
{
|
|
|
|
if( localDir.isEmpty() ) return;
|
|
|
|
|
|
|
|
TQString localDirString = localDir.path();
|
|
|
|
if( !TQFile::exists( localDirString ) )
|
|
|
|
{
|
|
|
|
TQString parentDirString = localDir.directory( true, true );
|
|
|
|
createLocalDir( parentDirString );
|
|
|
|
TQDir dir( localDirString );
|
|
|
|
dir.mkdir( localDirString );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
PodcastEpisode::abortDownload() //SLOT
|
|
|
|
{
|
|
|
|
emit downloadAborted();
|
|
|
|
if( m_podcastEpisodeJob )
|
|
|
|
m_podcastEpisodeJob->kill( false );
|
|
|
|
|
|
|
|
//don't delete m_podcastFetcher yet, kill() is async
|
|
|
|
stopAnimation();
|
|
|
|
setText( 0, title() );
|
|
|
|
m_onDisk = false;
|
|
|
|
updatePixmap();
|
|
|
|
}
|
|
|
|
|
|
|
|
void PodcastEpisode::downloadResult( KIO::Job * transferJob )
|
|
|
|
{
|
|
|
|
emit downloadFinished();
|
|
|
|
stopAnimation();
|
|
|
|
setText( 0, title() );
|
|
|
|
|
|
|
|
if( transferJob->error() )
|
|
|
|
{
|
|
|
|
Amarok::StatusBar::instance()->shortMessage( i18n( "Media download aborted, unable to connect to server." ) );
|
|
|
|
debug() << "Unable to retrieve podcast media. KIO Error: " << transferJob->error() << endl;
|
|
|
|
|
|
|
|
m_localUrl = KURL();
|
|
|
|
setPixmap( 0, SmallIcon("cancel") );
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
|
|
|
|
m_localUrl.addPath( m_filename );
|
|
|
|
TQFile *localFile = new TQFile( m_localUrl.path() );
|
|
|
|
localFile->open( IO_WriteOnly );
|
|
|
|
localFile->writeBlock( m_podcastEpisodeJob->data() );
|
|
|
|
localFile->close();
|
|
|
|
|
|
|
|
setLocalUrl( m_localUrl );
|
|
|
|
|
|
|
|
PodcastChannel *channel = dynamic_cast<PodcastChannel *>( m_parent );
|
|
|
|
if( channel && channel->autotransfer() && MediaBrowser::isAvailable() )
|
|
|
|
{
|
|
|
|
addToMediaDevice();
|
|
|
|
MediaBrowser::queue()->URLsAdded();
|
|
|
|
}
|
|
|
|
|
|
|
|
updatePixmap();
|
|
|
|
}
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
PodcastEpisode::setLocalUrl( const KURL &localUrl )
|
|
|
|
{
|
|
|
|
m_localUrl = localUrl;
|
|
|
|
m_bundle.setLocalURL( m_localUrl );
|
|
|
|
CollectionDB::instance()->updatePodcastEpisode( dBId(), m_bundle );
|
|
|
|
isOnDisk();
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
PodcastEpisode::addToMediaDevice()
|
|
|
|
{
|
|
|
|
MetaBundle *bundle = new MetaBundle( localUrl() );
|
|
|
|
PodcastChannel *channel = dynamic_cast<PodcastChannel *>( m_parent );
|
|
|
|
if(channel && !channel->title().isEmpty())
|
|
|
|
bundle->setAlbum(channel->title());
|
|
|
|
if(!title().isEmpty())
|
|
|
|
bundle->setTitle(title());
|
|
|
|
|
|
|
|
MediaBrowser::queue()->addURL( localUrl(), bundle );
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
PodcastEpisode::setLocalUrlBase( const TQString &s )
|
|
|
|
{
|
|
|
|
if ( !m_localUrl.isEmpty() )
|
|
|
|
{
|
|
|
|
TQString filename = m_localUrl.filename();
|
|
|
|
TQString newL = s + filename;
|
|
|
|
m_localUrl = KURL::fromPathOrURL( newL );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
PodcastEpisode::setNew( const bool &n )
|
|
|
|
{
|
|
|
|
if( n == isNew() ) return;
|
|
|
|
|
|
|
|
m_bundle.setNew( n );
|
|
|
|
updatePixmap();
|
|
|
|
CollectionDB::instance()->updatePodcastEpisode( dBId(), m_bundle );
|
|
|
|
|
|
|
|
// if we mark an item as listened, we might need to update the parent
|
|
|
|
if( n == true )
|
|
|
|
static_cast<PodcastChannel*>(m_parent)->setNew( true );
|
|
|
|
else
|
|
|
|
static_cast<PodcastChannel*>(m_parent)->checkAndSetNew();
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
PodcastEpisode::startAnimation()
|
|
|
|
{
|
|
|
|
if( !m_animationTimer.isActive() )
|
|
|
|
m_animationTimer.start( ANIMATION_INTERVAL );
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
PodcastEpisode::stopAnimation()
|
|
|
|
{
|
|
|
|
m_animationTimer.stop();
|
|
|
|
updatePixmap();
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
PodcastEpisode::slotAnimation()
|
|
|
|
{
|
|
|
|
m_iconCounter % 2 ?
|
|
|
|
setPixmap( 0, SmallIcon( Amarok::icon( "podcast") ) ):
|
|
|
|
setPixmap( 0, SmallIcon( Amarok::icon( "podcast2") ) );
|
|
|
|
|
|
|
|
m_iconCounter++;
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
PodcastEpisode::setup()
|
|
|
|
{
|
|
|
|
TQFontMetrics fm( listView()->font() );
|
|
|
|
int margin = listView()->itemMargin()*2;
|
|
|
|
int h = fm.lineSpacing();
|
|
|
|
if ( h % 2 > 0 ) h++;
|
|
|
|
setHeight( h + margin );
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
PodcastEpisode::paintCell( TQPainter *p, const TQColorGroup &cg, int column, int width, int align )
|
|
|
|
{
|
|
|
|
//flicker-free drawing
|
|
|
|
static TQPixmap buffer;
|
|
|
|
buffer.resize( width, height() );
|
|
|
|
|
|
|
|
if( buffer.isNull() )
|
|
|
|
{
|
|
|
|
KListViewItem::paintCell( p, cg, column, width, align );
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
TQPainter pBuf( &buffer, true );
|
|
|
|
// use alternate background
|
|
|
|
#if TDE_VERSION < KDE_MAKE_VERSION(3,3,91)
|
|
|
|
pBuf.fillRect( buffer.rect(), isSelected() ? cg.highlight() : backgroundColor() );
|
|
|
|
#else
|
|
|
|
pBuf.fillRect( buffer.rect(), isSelected() ? cg.highlight() : backgroundColor(0) );
|
|
|
|
#endif
|
|
|
|
|
|
|
|
KListView *lv = static_cast<KListView *>( listView() );
|
|
|
|
|
|
|
|
TQFont font( p->font() );
|
|
|
|
TQFontMetrics fm( p->fontMetrics() );
|
|
|
|
|
|
|
|
int text_x = 0;// lv->treeStepSize() + 3;
|
|
|
|
int textHeight;
|
|
|
|
|
|
|
|
textHeight = height();
|
|
|
|
|
|
|
|
pBuf.setPen( isSelected() ? cg.highlightedText() : cg.text() );
|
|
|
|
|
|
|
|
if( pixmap( column ) )
|
|
|
|
{
|
|
|
|
int y = (textHeight - pixmap(column)->height())/2;
|
|
|
|
pBuf.drawPixmap( text_x, y, *pixmap(column) );
|
|
|
|
text_x += pixmap(column)->width()+4;
|
|
|
|
}
|
|
|
|
|
|
|
|
pBuf.setFont( font );
|
|
|
|
TQFontMetrics fmName( font );
|
|
|
|
|
|
|
|
TQString name = text(column);
|
|
|
|
const int _width = width - text_x - lv->itemMargin()*2;
|
|
|
|
if( fmName.width( name ) > _width )
|
|
|
|
{
|
|
|
|
//decapitateString removes the channels title from the epsiodes title
|
|
|
|
name = Amarok::decapitateString( name, static_cast<PodcastChannel *>(m_parent)->title() );
|
|
|
|
if( fmName.width( name ) > _width )
|
|
|
|
name = KStringHandler::rPixelSqueeze( name, pBuf.fontMetrics(), _width );
|
|
|
|
}
|
|
|
|
|
|
|
|
pBuf.drawText( text_x, 0, width - text_x, textHeight, AlignVCenter, name );
|
|
|
|
|
|
|
|
pBuf.end();
|
|
|
|
p->drawPixmap( 0, 0, buffer );
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
PodcastEpisode::updateInfo()
|
|
|
|
{
|
|
|
|
const TQString body = "<tr><td><b>%1</b></td><td>%2</td></tr>";
|
|
|
|
|
|
|
|
TQString str = "<html><body><table width=\"100%\" border=\"0\">";
|
|
|
|
|
|
|
|
//str += body.tqarg( i18n( "Title" ), m_bundle.title() );
|
|
|
|
str += body.tqarg( i18n( "Description" ), m_bundle.description() );
|
|
|
|
str += body.tqarg( i18n( "Date" ), m_bundle.date() );
|
|
|
|
str += body.tqarg( i18n( "Author" ), m_bundle.author() );
|
|
|
|
str += body.tqarg( i18n( "Type" ), m_bundle.type() );
|
|
|
|
str += body.tqarg( i18n( "URL" ), m_bundle.url().prettyURL() );
|
|
|
|
str += body.tqarg( i18n( "Local URL" ), isOnDisk() ? localUrl().prettyURL() : i18n( "n/a" ) );
|
|
|
|
str += "</table></body></html>";
|
|
|
|
|
|
|
|
PlaylistBrowser::instance()->setInfo( text(0), str );
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void
|
|
|
|
PodcastEpisode::slotDoubleClicked()
|
|
|
|
{
|
|
|
|
KURL::List list;
|
|
|
|
|
|
|
|
isOnDisk() ?
|
|
|
|
list.append( localUrl() ):
|
|
|
|
list.append( url() );
|
|
|
|
|
|
|
|
Playlist::instance()->insertMedia( list, Playlist::DefaultOptions );
|
|
|
|
setListened();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void
|
|
|
|
PodcastEpisode::showContextMenu( const TQPoint &position )
|
|
|
|
{
|
|
|
|
KPopupMenu menu( listView() );
|
|
|
|
|
|
|
|
enum Actions { LOAD, APPEND, TQUEUE, GET, ASSOCIATE, DELETE, MEDIA_DEVICE, LISTENED, NEW, OPEN_WITH /* has to be last */ };
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon( "files" ) ), i18n( "&Load" ), LOAD );
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon( "add_playlist" ) ), i18n( "&Append to Playlist" ), APPEND );
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon( "queue_track" ) ), i18n( "&Queue Track" ), TQUEUE );
|
|
|
|
|
|
|
|
int accuracy = 0;
|
|
|
|
KMimeType::Ptr mimetype;
|
|
|
|
if( isOnDisk() )
|
|
|
|
mimetype = KMimeType::findByFileContent( localUrl().path(), &accuracy );
|
|
|
|
if( accuracy <= 0 )
|
|
|
|
mimetype = KMimeType::findByURL( url() );
|
|
|
|
KTrader::OfferList offers = KTrader::self()->query( mimetype->name(), "Type == 'Application'" );
|
|
|
|
if( offers.empty() || (offers.size()==1 && offers.first()->name()=="Amarok") )
|
|
|
|
{
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon( "run" ) ), i18n( "&Open With..."), OPEN_WITH );
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
int i = 1;
|
|
|
|
KPopupMenu *openMenu = new KPopupMenu;
|
|
|
|
for( KTrader::OfferList::iterator it = offers.begin();
|
|
|
|
it != offers.end();
|
|
|
|
++it )
|
|
|
|
{
|
|
|
|
if( (*it)->name() != "Amarok" )
|
|
|
|
openMenu->insertItem( SmallIconSet( (*it)->icon() ), (*it)->name(), OPEN_WITH+i );
|
|
|
|
++i;
|
|
|
|
}
|
|
|
|
openMenu->insertSeparator();
|
|
|
|
openMenu->insertItem( SmallIconSet( Amarok::icon( "run" ) ), i18n( "&Other..."), OPEN_WITH );
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon( "run" ) ), i18n("&Open With"), openMenu, OPEN_WITH );
|
|
|
|
}
|
|
|
|
|
|
|
|
if( MediaBrowser::isAvailable() )
|
|
|
|
{
|
|
|
|
menu.insertSeparator();
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon( "device" ) ),
|
|
|
|
i18n( "&Transfer to Media Device" ), MEDIA_DEVICE );
|
|
|
|
menu.setItemEnabled( MEDIA_DEVICE, isOnDisk() );
|
|
|
|
}
|
|
|
|
|
|
|
|
menu.insertSeparator();
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon( "download" ) ), i18n( "&Download Media" ), GET );
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon( "attach" ) ), i18n( "&Associate with Local File" ), ASSOCIATE );
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon( "artist" ) ), i18n( "Mark as &Listened" ), LISTENED );
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon( "artist" ) ), i18n( "Mark as &New" ), NEW );
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon("remove") ), i18n( "De&lete Downloaded Podcast" ), DELETE );
|
|
|
|
|
|
|
|
menu.setItemEnabled( GET, !isOnDisk() );
|
|
|
|
menu.setItemEnabled( ASSOCIATE, !isOnDisk() );
|
|
|
|
menu.setItemEnabled( DELETE, isOnDisk() );
|
|
|
|
menu.setItemVisible( LISTENED, isNew() );
|
|
|
|
menu.setItemVisible( NEW, !isNew() );
|
|
|
|
|
|
|
|
uint id = menu.exec( position );
|
|
|
|
switch( id )
|
|
|
|
{
|
|
|
|
case LOAD:
|
|
|
|
Playlist::instance()->clear();
|
|
|
|
Playlist::instance()->setPlaylistName( text(0) );
|
|
|
|
//FALL THROUGH
|
|
|
|
case APPEND:
|
|
|
|
PlaylistBrowser::instance()->addSelectedToPlaylist( Playlist::Append );
|
|
|
|
break;
|
|
|
|
|
|
|
|
case TQUEUE:
|
|
|
|
PlaylistBrowser::instance()->addSelectedToPlaylist( Playlist::Queue );
|
|
|
|
break;
|
|
|
|
|
|
|
|
case GET:
|
|
|
|
PlaylistBrowser::instance()->downloadSelectedPodcasts();
|
|
|
|
break;
|
|
|
|
|
|
|
|
case ASSOCIATE:
|
|
|
|
associateWithLocalFile();
|
|
|
|
break;
|
|
|
|
|
|
|
|
case DELETE:
|
|
|
|
PlaylistBrowser::instance()->deleteSelectedPodcastItems();
|
|
|
|
break;
|
|
|
|
|
|
|
|
case LISTENED:
|
|
|
|
for ( TQListViewItemIterator it( listView(), TQListViewItemIterator::Selected); *it; ++it )
|
|
|
|
{
|
|
|
|
if ( isPodcastEpisode( *it ) )
|
|
|
|
static_cast<PodcastEpisode*>(*it)->setListened();
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case NEW:
|
|
|
|
for ( TQListViewItemIterator it( listView(), TQListViewItemIterator::Selected); *it; ++it )
|
|
|
|
{
|
|
|
|
if ( isPodcastEpisode( *it ) )
|
|
|
|
static_cast<PodcastEpisode*>(*it)->setListened(false);
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
case MEDIA_DEVICE:
|
|
|
|
// tags on podcasts are sometimes bad, thus use other meta information if available
|
|
|
|
if( isSelected() )
|
|
|
|
{
|
|
|
|
for ( TQListViewItemIterator it( listView(), TQListViewItemIterator::Selected); *it; ++it)
|
|
|
|
{
|
|
|
|
if( isPodcastEpisode( *it ) )
|
|
|
|
{
|
|
|
|
PodcastEpisode *podcast = static_cast<PodcastEpisode*>(*it);
|
|
|
|
if( podcast->isOnDisk() )
|
|
|
|
podcast->addToMediaDevice();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
addToMediaDevice();
|
|
|
|
|
|
|
|
MediaBrowser::queue()->URLsAdded();
|
|
|
|
break;
|
|
|
|
case OPEN_WITH:
|
|
|
|
{
|
|
|
|
KURL::List urlList;
|
|
|
|
urlList.append( isOnDisk() ? localUrl() : url() );
|
|
|
|
KRun::displayOpenWithDialog( urlList );
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
if( id >= OPEN_WITH+1 && id <= OPEN_WITH + offers.size() )
|
|
|
|
{
|
|
|
|
KTrader::OfferList::iterator it = offers.begin();
|
|
|
|
for(uint i = OPEN_WITH+1; i < id && i < OPEN_WITH+offers.size(); ++i )
|
|
|
|
{
|
|
|
|
++it;
|
|
|
|
}
|
|
|
|
KService::Ptr ptr = offers.first();
|
|
|
|
KURL::List urlList;
|
|
|
|
urlList.append( isOnDisk() ? localUrl() : url() );
|
|
|
|
if( it != offers.end() )
|
|
|
|
{
|
|
|
|
KRun::run(**it, urlList);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
class AssociatePodcastDialog : public KDialogBase
|
|
|
|
{
|
|
|
|
KURLRequester *m_urlRequester;
|
|
|
|
|
|
|
|
public:
|
|
|
|
AssociatePodcastDialog( PodcastEpisode *item )
|
|
|
|
: KDialogBase( Amarok::mainWindow(), "associatepodcastdialog", true, i18n("Select Local File for %1").tqarg(item->title()), Ok|Cancel, Ok, false )
|
|
|
|
{
|
|
|
|
TQVBox* vbox = makeVBoxMainWidget();
|
|
|
|
vbox->setSpacing( KDialog::spacingHint() );
|
|
|
|
|
|
|
|
m_urlRequester = new KURLRequester( vbox );
|
|
|
|
if( dynamic_cast<PodcastChannel *>(item->parent()) )
|
|
|
|
m_urlRequester->setURL( static_cast<PodcastChannel *>(item->parent())->saveLocation() );
|
|
|
|
}
|
|
|
|
KURL url() const { return KURL::fromPathOrURL( m_urlRequester->url() ); }
|
|
|
|
};
|
|
|
|
|
|
|
|
void
|
|
|
|
PodcastEpisode::associateWithLocalFile()
|
|
|
|
{
|
|
|
|
AssociatePodcastDialog d( this );
|
|
|
|
if( d.exec() == KDialogBase::Accepted )
|
|
|
|
{
|
|
|
|
if( !d.url().isLocalFile() || !TQFileInfo( d.url().path() ).isFile() )
|
|
|
|
Amarok::StatusBar::instance()->shortMessage( i18n( "Invalid local podcast URL." ) );
|
|
|
|
else
|
|
|
|
setLocalUrl( d.url() );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/////////////////////////////////////////////////////////////////////////////
|
|
|
|
/// CLASS SmartPlaylist
|
|
|
|
////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
SmartPlaylist::SmartPlaylist( TQListViewItem *parent, TQListViewItem *after, const TQString &name, const TQString &query )
|
|
|
|
: PlaylistBrowserEntry( parent, after, name )
|
|
|
|
, m_sqlForTags( query )
|
|
|
|
, m_title( name )
|
|
|
|
, m_dynamic( false )
|
|
|
|
{
|
|
|
|
setPixmap( 0, SmallIcon( Amarok::icon( "playlist" ) ) );
|
|
|
|
setDragEnabled( query.isEmpty() ? false : true );
|
|
|
|
|
|
|
|
setText( 0, name );
|
|
|
|
}
|
|
|
|
|
|
|
|
SmartPlaylist::SmartPlaylist( TQListViewItem *parent, TQListViewItem *after, const TQString &name, const TQString &urls, const TQString &tags )
|
|
|
|
: PlaylistBrowserEntry( parent, after, name )
|
|
|
|
, m_sqlForTags( tags )
|
|
|
|
, m_title( name )
|
|
|
|
, m_dynamic( false )
|
|
|
|
{
|
|
|
|
setPixmap( 0, SmallIcon( Amarok::icon( "playlist" ) ) );
|
|
|
|
setDragEnabled( !urls.isEmpty() && !tags.isEmpty() );
|
|
|
|
|
|
|
|
setText( 0, name );
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
SmartPlaylist::SmartPlaylist( TQListViewItem *parent, TQListViewItem *after, const TQDomElement &xmlDefinition )
|
|
|
|
: PlaylistBrowserEntry( parent, after )
|
|
|
|
, m_after( after )
|
|
|
|
, m_dynamic( false )
|
|
|
|
{
|
|
|
|
setPixmap( 0, SmallIcon( Amarok::icon( "playlist" ) ) );
|
|
|
|
setXml( xmlDefinition );
|
|
|
|
setDragEnabled( true );
|
|
|
|
}
|
|
|
|
|
|
|
|
int SmartPlaylist::length()
|
|
|
|
{
|
|
|
|
TQString sql = query();
|
|
|
|
sql.replace(TQRegExp("SELECT.*FROM"), "SELECT COUNT(*) FROM");
|
|
|
|
CollectionDB *db = CollectionDB::instance();
|
|
|
|
TQStringList result = db->query( sql );
|
|
|
|
|
|
|
|
if (! result.isEmpty())
|
|
|
|
return result.first().toInt();
|
|
|
|
else return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
void SmartPlaylist::setXml( const TQDomElement &xml )
|
|
|
|
{
|
|
|
|
m_xml = xml;
|
|
|
|
m_title = xml.attribute( "name" );
|
|
|
|
setText( 0, m_title );
|
|
|
|
// ignore query, we now compute it when needed
|
|
|
|
//m_sqlForTags = xml.namedItem( "sqlquery" ).toElement().text();
|
|
|
|
m_sqlForTags = "";
|
|
|
|
static TQStringList genres;
|
|
|
|
static TQStringList artists;
|
|
|
|
static TQStringList composers;
|
|
|
|
static TQStringList albums;
|
|
|
|
static TQStringList years;
|
|
|
|
static TQStringList labels;
|
|
|
|
|
|
|
|
//Delete all tqchildren before
|
|
|
|
while( firstChild() )
|
|
|
|
delete firstChild();
|
|
|
|
|
|
|
|
TQDomNode expandN = xml.namedItem( "expandby" );
|
|
|
|
if ( !expandN.isNull() ) {
|
|
|
|
// precompute query
|
|
|
|
TQString queryChildren = xmlToQuery( m_xml, true );
|
|
|
|
|
|
|
|
TQDomElement expand = expandN.toElement();
|
|
|
|
TQString field = expand.attribute( "field" );
|
|
|
|
SmartPlaylist *item = this;
|
|
|
|
if ( field == i18n("Genre") ) {
|
|
|
|
if ( genres.isEmpty() ) {
|
|
|
|
genres = CollectionDB::instance()->genreList();
|
|
|
|
}
|
|
|
|
foreach( genres ) {
|
|
|
|
m_after = new SmartPlaylist( item, m_after, i18n( "%1" ).tqarg( *it ),
|
|
|
|
TQString(queryChildren).replace(
|
|
|
|
"(*ExpandString*)", *it) );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if ( field == i18n("Artist") ) {
|
|
|
|
if ( artists.isEmpty() ) {
|
|
|
|
artists = CollectionDB::instance()->artistList();
|
|
|
|
}
|
|
|
|
foreach( artists ) {
|
|
|
|
m_after = new SmartPlaylist( item, m_after, i18n( "By %1" ).tqarg( *it ),
|
|
|
|
TQString(queryChildren).replace(
|
|
|
|
"(*ExpandString*)", *it) );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if ( field == i18n("Composer") ) {
|
|
|
|
if ( composers.isEmpty() ) {
|
|
|
|
composers = CollectionDB::instance()->composerList();
|
|
|
|
}
|
|
|
|
foreach( composers ) {
|
|
|
|
m_after = new SmartPlaylist( item, m_after, i18n( "By %1" ).tqarg( *it ),
|
|
|
|
TQString(queryChildren).replace(
|
|
|
|
"(*ExpandString*)", *it) );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if ( field == i18n("Album") ) {
|
|
|
|
if ( albums.isEmpty() ) {
|
|
|
|
albums = CollectionDB::instance()->albumList();
|
|
|
|
}
|
|
|
|
foreach( albums ) {
|
|
|
|
m_after = new SmartPlaylist( item, m_after, i18n( "%1" ).tqarg( *it ),
|
|
|
|
TQString(queryChildren).replace(
|
|
|
|
"(*ExpandString*)", *it) );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if ( field == i18n("Year") ) {
|
|
|
|
if ( years.isEmpty() ) {
|
|
|
|
years = CollectionDB::instance()->yearList();
|
|
|
|
}
|
|
|
|
foreach( years ) {
|
|
|
|
m_after = new SmartPlaylist( item, m_after, i18n( "%1" ).tqarg( *it ),
|
|
|
|
TQString(queryChildren).replace(
|
|
|
|
"(*ExpandString*)", *it) );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if ( field == i18n("Label") ) {
|
|
|
|
if (labels.isEmpty() ) {
|
|
|
|
labels = CollectionDB::instance()->labelList();
|
|
|
|
}
|
|
|
|
foreach( labels ) {
|
|
|
|
m_after = new SmartPlaylist( item, m_after, i18n( "%1" ).tqarg( *it ), TQString(queryChildren).replace("(*ExpandString*)", *it) );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
TQString SmartPlaylist::query()
|
|
|
|
{
|
|
|
|
if ( m_sqlForTags.isEmpty() ) m_sqlForTags = xmlToQuery( m_xml );
|
|
|
|
// duplicate string, thread-safely (TQDeepCopy is not thread-safe)
|
|
|
|
return TQString( m_sqlForTags.tqunicode(), m_sqlForTags.length() )
|
|
|
|
.replace( "(*CurrentTimeT*)" ,
|
|
|
|
TQString::number(TQDateTime::tqcurrentDateTime().toTime_t()) )
|
|
|
|
.replace( "(*ListOfFields*)" , QueryBuilder::dragSQLFields() )
|
|
|
|
.replace( "(*MountedDeviceSelection*)" ,
|
|
|
|
CollectionDB::instance()->deviceidSelection() );
|
|
|
|
}
|
|
|
|
|
|
|
|
// static
|
|
|
|
TQString
|
|
|
|
SmartPlaylist::xmlToQuery(const TQDomElement &xml, bool forExpand /* = false */) {
|
|
|
|
QueryBuilder qb;
|
|
|
|
|
|
|
|
qb.initSQLDrag();
|
|
|
|
// This code is partly copied from SmartPlaylistEditor -- but refactoring
|
|
|
|
// to have it common would involve adding an internal data structure for smart
|
|
|
|
// playlist queries. I think having the XML be that data structure is almost as good,
|
|
|
|
// it's just a little more verbose when iterating.
|
|
|
|
|
|
|
|
// Add filters
|
|
|
|
TQDomNodeList matchesList = xml.elementsByTagName( "matches" );
|
|
|
|
for ( uint i = 0; i < matchesList.count(); i++ ) {
|
|
|
|
TQDomElement matches = matchesList.item( i ).toElement();
|
|
|
|
TQDomNodeList criteriaList = matches.elementsByTagName( "criteria" );
|
|
|
|
|
|
|
|
if ( matches.attribute( "glue" ) == "OR" )
|
|
|
|
qb.beginOR();
|
|
|
|
else
|
|
|
|
qb.beginAND();
|
|
|
|
|
|
|
|
for ( uint j = 0; j < criteriaList.count(); j++ ) {
|
|
|
|
TQDomElement criteria = criteriaList.item( j ).toElement();
|
|
|
|
TQString field = criteria.attribute( "field" );
|
|
|
|
int table;
|
|
|
|
TQ_INT64 value;
|
|
|
|
if ( !qb.getField( field, &table, &value ) ) continue;
|
|
|
|
|
|
|
|
TQStringList filters;
|
|
|
|
// name conflict :) XML "value" -> QueryBuilder "filter"
|
|
|
|
TQDomNodeList domFilterList = criteria.elementsByTagName( "value" );
|
|
|
|
for ( uint k = 0 ; k < domFilterList.count(); k++ ) {
|
|
|
|
filters << domFilterList.item(k).toElement().text();
|
|
|
|
}
|
|
|
|
|
|
|
|
TQString condition = criteria.attribute( "condition" );
|
|
|
|
|
|
|
|
// Interpret dates
|
|
|
|
bool isDate = (value & (QueryBuilder::valCreateDate
|
|
|
|
| QueryBuilder::valAccessDate)) > 0;
|
|
|
|
|
|
|
|
if ( isDate ) {
|
|
|
|
TQDateTime dt1, dt2;
|
|
|
|
if ( condition == i18n( "is in the last" )
|
|
|
|
|| condition == i18n( "is not in the last" ) ) {
|
|
|
|
TQString period = criteria.attribute( "period" );
|
|
|
|
uint time = filters[0].toInt();
|
|
|
|
if ( period == "days" ) time *= 86400;
|
|
|
|
else if ( period == "months" ) time *= 86400 * 30;
|
|
|
|
else if ( period == "years" ) time *= 86400 * 365;
|
|
|
|
filters[0] = "(*CurrentTimeT*) - " + TQString::number( time );
|
|
|
|
if ( filters.count() == 1 ) filters.push_back( "" );
|
|
|
|
filters[1] = "(*CurrentTimeT*)";
|
|
|
|
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
dt1.setTime_t( filters[0].toInt() );
|
|
|
|
// truncate to midnight
|
|
|
|
if ( condition == i18n( "is after" ) )
|
|
|
|
dt1.setTime( TQTime().addSecs(-1) ); // 11:59:59 pm
|
|
|
|
else
|
|
|
|
dt1.setTime( TQTime() );
|
|
|
|
if ( filters.count() > 1 ) {
|
|
|
|
dt2.setTime_t( filters[1].toInt() );
|
|
|
|
// this is a "between", so always go till right before midnight
|
|
|
|
dt2.setTime( TQTime().addSecs( -1 ) );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( value & QueryBuilder::valLength ) {
|
|
|
|
TQString period = criteria.attribute( "period" );
|
|
|
|
uint time1 = filters[0].toInt();
|
|
|
|
if ( period == "minutes" )
|
|
|
|
time1 *= 60;
|
|
|
|
else if ( period == "hours" )
|
|
|
|
time1 *= 3600;
|
|
|
|
filters[0] = TQString::number( time1 );
|
|
|
|
if ( condition == i18n( "is between" ) )
|
|
|
|
{
|
|
|
|
uint time2 = filters[1].toInt();
|
|
|
|
if ( period == "minutes" )
|
|
|
|
time2 *= 60;
|
|
|
|
else if ( period == "hours" )
|
|
|
|
time2 *= 3600;
|
|
|
|
filters[1] = TQString::number( time2 );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if ( condition == i18n( "contains" ) )
|
|
|
|
qb.addFilter( table, value, filters[0] );
|
|
|
|
else if ( condition == i18n( "does not contain" ) )
|
|
|
|
qb.excludeFilter( table, value, filters[0]) ;
|
|
|
|
else if ( condition == i18n( "is") )
|
|
|
|
qb.addFilter( table, value, filters[0], QueryBuilder::modeNormal, true);
|
|
|
|
else if ( condition == i18n( "is not" ) )
|
|
|
|
qb.excludeFilter( table, value, filters[0], QueryBuilder::modeNormal,
|
|
|
|
true);
|
|
|
|
else if ( condition == i18n( "starts with" ) )
|
|
|
|
{
|
|
|
|
// need to take care of absolute paths
|
|
|
|
if ( field == "tags.url" )
|
|
|
|
if ( filters[0].startsWith( "/" ) )
|
|
|
|
filters[0].prepend( '.' );
|
|
|
|
else if ( !filters[0].startsWith( "./" ) )
|
|
|
|
filters[0].prepend( "./" );
|
|
|
|
qb.addFilter( table, value, filters[0], QueryBuilder::modeBeginMatch );
|
|
|
|
}
|
|
|
|
else if ( condition == i18n( "does not start with" ) )
|
|
|
|
{
|
|
|
|
// need to take care of absolute paths
|
|
|
|
if ( field == "tags.url" )
|
|
|
|
if ( filters[0].startsWith( "/" ) )
|
|
|
|
filters[0].prepend( '.' );
|
|
|
|
else if ( !filters[0].startsWith( "./" ) )
|
|
|
|
filters[0].prepend( "./" );
|
|
|
|
qb.excludeFilter( table, value, filters[0], QueryBuilder::modeBeginMatch );
|
|
|
|
}
|
|
|
|
else if ( condition == i18n( "ends with" ) )
|
|
|
|
qb.addFilter( table, value, filters[0], QueryBuilder::modeEndMatch );
|
|
|
|
else if ( condition == i18n( "does not end with" ) )
|
|
|
|
qb.excludeFilter( table, value, filters[0], QueryBuilder::modeEndMatch );
|
|
|
|
else if ( condition == i18n( "is greater than") || condition == i18n( "is after" ) )
|
|
|
|
qb.addNumericFilter( table, value, filters[0], QueryBuilder::modeGreater );
|
|
|
|
else if ( condition == i18n( "is smaller than") || condition == i18n( "is before" ) )
|
|
|
|
qb.addNumericFilter( table, value, filters[0], QueryBuilder::modeLess );
|
|
|
|
else if ( condition == i18n( "is between" )
|
|
|
|
|| condition == i18n( "is in the last" ) )
|
|
|
|
qb.addNumericFilter( table, value, filters[0], QueryBuilder::modeBetween,
|
|
|
|
filters[1] );
|
|
|
|
else if ( condition == i18n( "is not between" )
|
|
|
|
|| condition == i18n( "is not in the last" ) )
|
|
|
|
qb.addNumericFilter( table, value, filters[0],
|
|
|
|
QueryBuilder::modeNotBetween, filters[1] );
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( matches.attribute( "glue" ) == "OR" )
|
|
|
|
qb.endOR();
|
|
|
|
else
|
|
|
|
qb.endAND();
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// order by
|
|
|
|
TQDomNodeList orderbyList = xml.elementsByTagName( "orderby" );
|
|
|
|
for ( uint i = 0; i < orderbyList.count(); i++ ) {
|
|
|
|
TQDomElement orderby = orderbyList.item( i ).toElement();
|
|
|
|
TQString field = orderby.attribute( "field" );
|
|
|
|
if ( field == "random" )
|
|
|
|
{
|
|
|
|
// shuffle
|
|
|
|
if ( orderby.attribute("order" ) == "weighted" )
|
|
|
|
qb.shuffle( QueryBuilder::tabStats, QueryBuilder::valScore );
|
|
|
|
else if ( orderby.attribute("order" ) == "ratingweighted" )
|
|
|
|
qb.shuffle( QueryBuilder::tabStats, QueryBuilder::valRating );
|
|
|
|
else
|
|
|
|
qb.shuffle();
|
|
|
|
} else {
|
|
|
|
// normal sort
|
|
|
|
int table;
|
|
|
|
TQ_INT64 value;
|
|
|
|
if ( !qb.getField( field, &table, &value ) ) continue;
|
|
|
|
qb.sortBy( table, value, orderby.attribute( "order" ) == "DESC" );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( xml.hasAttribute( "maxresults" ) )
|
|
|
|
qb.setLimit(0, xml.attribute( "maxresults" ).toInt() );
|
|
|
|
|
|
|
|
// expand by, if needed
|
|
|
|
if ( forExpand ) {
|
|
|
|
// TODO: The most efficient way would be to pass the tqchildren the XML
|
|
|
|
// and what to expand by, then have the tqchildren compute the query as needed.
|
|
|
|
// This could save a few megs of RAM for queries, but this patch is getting
|
|
|
|
// too big already, right now. Ovy
|
|
|
|
TQDomNodeList expandbyList = xml.elementsByTagName( "expandby" );
|
|
|
|
for ( uint i = 0; i < expandbyList.count(); i++ ) {
|
|
|
|
TQDomElement expandby = expandbyList.item( i ).toElement();
|
|
|
|
TQString field = expandby.attribute( "field" );
|
|
|
|
int table = QueryBuilder::tabGenre; // make compiler happy
|
|
|
|
if ( field == i18n( "Genre" ) )
|
|
|
|
table = QueryBuilder::tabGenre;
|
|
|
|
else if ( field == i18n( "Artist" ) )
|
|
|
|
table = QueryBuilder::tabArtist;
|
|
|
|
else if ( field == i18n( "Composer" ) )
|
|
|
|
table = QueryBuilder::tabComposer;
|
|
|
|
else if ( field == i18n( "Album" ) )
|
|
|
|
table = QueryBuilder::tabAlbum;
|
|
|
|
else if ( field == i18n( "Year" ) )
|
|
|
|
table = QueryBuilder::tabYear;
|
|
|
|
else if ( field == i18n( "Label" ) )
|
|
|
|
table = QueryBuilder::tabLabels;
|
|
|
|
|
|
|
|
qb.addFilter( table, QueryBuilder::valName,
|
|
|
|
"(*ExpandString*)",
|
|
|
|
QueryBuilder::modeNormal, true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return qb.query( true );
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void SmartPlaylist::setDynamic( bool enable )
|
|
|
|
{
|
|
|
|
enable ?
|
|
|
|
setPixmap( 0, SmallIcon( "favorites" ) ) :
|
|
|
|
setPixmap( 0, SmallIcon( Amarok::icon( "playlist" ) ) );
|
|
|
|
m_dynamic = enable;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool SmartPlaylist::isTimeOrdered()
|
|
|
|
{
|
|
|
|
// matches statistics.createdate (firstplayed) and tags.createdate (modified date)
|
|
|
|
TQRegExp createDate( "ORDER BY.*createdate" );
|
|
|
|
// matches last played
|
|
|
|
TQRegExp accessDate( "ORDER BY.*accessdate" );
|
|
|
|
|
|
|
|
const TQString sql = query();
|
|
|
|
|
|
|
|
return ! ( ( sql.find( createDate, false ) == -1 ) /*not create ordered*/ &&
|
|
|
|
( sql.find( accessDate, false ) == -1 ) /*not access ordered*/ );
|
|
|
|
}
|
|
|
|
|
|
|
|
void SmartPlaylist::slotDoubleClicked()
|
|
|
|
{
|
|
|
|
if( !query().isEmpty() )
|
|
|
|
{
|
|
|
|
Playlist::instance()->proposePlaylistName( text(0) );
|
|
|
|
Playlist::instance()->insertMediaSql( query(), Playlist::DefaultOptions );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void SmartPlaylist::showContextMenu( const TQPoint &position )
|
|
|
|
{
|
|
|
|
KPopupMenu menu( listView() );
|
|
|
|
|
|
|
|
enum Actions { LOAD, ADD, TQUEUE, EDIT, REMOVE, MEDIADEVICE_COPY, MEDIADEVICE_SYNC };
|
|
|
|
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon( "files" ) ), i18n( "&Load" ), LOAD );
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon( "add_playlist" ) ), i18n( "&Append to Playlist" ), ADD );
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon( "queue_track" ) ), i18n( "&Queue Tracks" ), TQUEUE );
|
|
|
|
if( MediaBrowser::isAvailable() )
|
|
|
|
{
|
|
|
|
menu.insertSeparator();
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon( "device" ) ),
|
|
|
|
i18n( "&Transfer to Media Device" ), MEDIADEVICE_COPY );
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon( "device" ) ),
|
|
|
|
i18n( "&Synchronize to Media Device" ), MEDIADEVICE_SYNC );
|
|
|
|
}
|
|
|
|
|
|
|
|
// Forbid removal of Collection
|
|
|
|
if( isKept() )
|
|
|
|
{
|
|
|
|
menu.insertSeparator();
|
|
|
|
if ( isEditable() )
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon("edit") ), i18n( "E&dit..." ), EDIT );
|
|
|
|
menu.insertItem( SmallIconSet( Amarok::icon("remove_from_playlist") ), i18n( "&Delete" ), REMOVE );
|
|
|
|
}
|
|
|
|
|
|
|
|
switch( menu.exec( position ) )
|
|
|
|
{
|
|
|
|
case LOAD:
|
|
|
|
Playlist::instance()->clear();
|
|
|
|
Playlist::instance()->setPlaylistName( text(0) );
|
|
|
|
//FALL THROUGH
|
|
|
|
case ADD:
|
|
|
|
Playlist::instance()->insertMediaSql( query(), Playlist::Append );
|
|
|
|
break;
|
|
|
|
case TQUEUE:
|
|
|
|
Playlist::instance()->insertMediaSql( query(), Playlist::Queue );
|
|
|
|
break;
|
|
|
|
case EDIT:
|
|
|
|
PlaylistBrowser::instance()->editSmartPlaylist( this );
|
|
|
|
PlaylistBrowser::instance()->saveSmartPlaylists();
|
|
|
|
break;
|
|
|
|
case REMOVE:
|
|
|
|
PlaylistBrowser::instance()->removeSelectedItems();
|
|
|
|
break;
|
|
|
|
case MEDIADEVICE_COPY:
|
|
|
|
{
|
|
|
|
const TQString playlist = text(0);
|
|
|
|
const TQStringList values = CollectionDB::instance()->query( query() );
|
|
|
|
MediaBrowser::queue()->addURLs( CollectionDB::instance()->URLsFromSqlDrag( values ), playlist );
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
case MEDIADEVICE_SYNC:
|
|
|
|
MediaBrowser::queue()->syncPlaylist( text(0), query() );
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void SmartPlaylist::slotPostRenameItem( const TQString newName )
|
|
|
|
{
|
|
|
|
xml().setAttribute( "name", newName );
|
|
|
|
}
|
|
|
|
|
|
|
|
ShoutcastBrowser::ShoutcastBrowser( PlaylistCategory *parent )
|
|
|
|
: PlaylistCategory( parent, 0, i18n( "Shoutcast Streams" ) )
|
|
|
|
, m_downloading( false )
|
|
|
|
, m_cj( 0 )
|
|
|
|
, m_loading1( new TQPixmap( locate("data", "amarok/images/loading1.png" ) ) )
|
|
|
|
, m_loading2( new TQPixmap( locate("data", "amarok/images/loading2.png" ) ) )
|
|
|
|
{
|
|
|
|
setExpandable( true );
|
|
|
|
setKept( false );
|
|
|
|
}
|
|
|
|
|
|
|
|
void ShoutcastBrowser::slotDoubleClicked()
|
|
|
|
{
|
|
|
|
setOpen( !isOpen() );
|
|
|
|
}
|
|
|
|
|
|
|
|
void ShoutcastBrowser::setOpen( bool open )
|
|
|
|
{
|
|
|
|
if( open == isOpen())
|
|
|
|
return;
|
|
|
|
|
|
|
|
if ( firstChild() ) // don't redownload everything
|
|
|
|
{
|
|
|
|
TQListViewItem::setOpen( open );
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if( !m_animationTimer.isActive() )
|
|
|
|
m_animationTimer.start( ANIMATION_INTERVAL );
|
|
|
|
connect( &m_animationTimer, TQT_SIGNAL(timeout()), this, TQT_SLOT(slotAnimation()) );
|
|
|
|
|
|
|
|
TQStringList tmpdirs = KGlobal::dirs()->resourceDirs( "tmp" );
|
|
|
|
TQString tmpfile = tmpdirs[0];
|
|
|
|
tmpfile += "/amarok-genres-" + KApplication::randomString(10) + ".xml-";
|
|
|
|
|
|
|
|
//get the genre list
|
|
|
|
if ( !m_downloading )
|
|
|
|
{
|
|
|
|
m_downloading = true;
|
|
|
|
m_cj = KIO::copy( "http://www.shoutcast.com/sbin/newxml.phtml", tmpfile, false );
|
|
|
|
connect( m_cj, TQT_SIGNAL( copyingDone( KIO::Job*, const KURL&, const KURL&, bool, bool))
|
|
|
|
, this, TQT_SLOT(doneGenreDownload(KIO::Job*, const KURL&, const KURL&, bool, bool )));
|
|
|
|
connect( m_cj, TQT_SIGNAL( result( KIO::Job* )), this, TQT_SLOT( jobFinished( KIO::Job* )));
|
|
|
|
}
|
|
|
|
|
|
|
|
TQListViewItem::setOpen( open );
|
|
|
|
}
|
|
|
|
|
|
|
|
void ShoutcastBrowser::slotAnimation()
|
|
|
|
{
|
|
|
|
static int s_iconCounter = 0;
|
|
|
|
s_iconCounter % 2 ?
|
|
|
|
setPixmap( 0, *m_loading1 ):
|
|
|
|
setPixmap( 0, *m_loading2 );
|
|
|
|
|
|
|
|
s_iconCounter++;
|
|
|
|
}
|
|
|
|
|
|
|
|
void ShoutcastBrowser::doneGenreDownload( KIO::Job *job, const KURL &from, const KURL &to, bool directory, bool renamed )
|
|
|
|
{
|
|
|
|
Q_UNUSED( job ); Q_UNUSED( from ); Q_UNUSED( directory ); Q_UNUSED( renamed );
|
|
|
|
|
|
|
|
TQDomDocument doc( "genres" );
|
|
|
|
TQFile file( to.path() );
|
|
|
|
if ( !file.open( IO_ReadOnly ) )
|
|
|
|
{
|
|
|
|
warning() << "Cannot open shoutcast genre xml" << endl;
|
|
|
|
m_downloading = false;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if ( !doc.setContent( &file ) )
|
|
|
|
{
|
|
|
|
warning() << "Cannot set shoutcast genre xml" << endl;
|
|
|
|
file.close();
|
|
|
|
m_downloading = false;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
file.close();
|
|
|
|
|
|
|
|
KIO::del( to, false, false );
|
|
|
|
|
|
|
|
// We use this list to filter out some obscure genres
|
|
|
|
TQStringList bannedGenres;
|
|
|
|
bannedGenres << "alles" << "any" << "anything" << "autopilot" << "backup" << "bandas" << "beer";
|
|
|
|
bannedGenres << "catholic" << "chr" << "das" << "domaca" << "everything" << "fire" << "her" << "hollands";
|
|
|
|
bannedGenres << "http" << "just" << "lokale" << "middle" << "noticias" << "only" << "scanner" << "shqip";
|
|
|
|
bannedGenres << "good" << "super" << "wusf" << "www" << "zabavna" << "zouk" << "whatever" << "varios";
|
|
|
|
bannedGenres << "varius" << "video" << "opm" << "non" << "narodna" << "muzyka" << "muzica" << "muzika";
|
|
|
|
bannedGenres << "musique" << "music" << "multi" << "online" << "mpb" << "musica" << "musik" << "manele";
|
|
|
|
bannedGenres << "paranormal" << "todos" << "soca" << "the" << "toda" << "trova" << "italo";
|
|
|
|
bannedGenres << "auto" << "alternativo" << "best" << "clasicos" << "der" << "desi" << "die" << "emisora";
|
|
|
|
bannedGenres << "voor" << "post" << "playlist" << "ned" << "gramy" << "deportes" << "bhangra" << "exitos";
|
|
|
|
bannedGenres << "doowop" << "radio" << "radyo" << "railroad" << "program" << "mostly" << "hot";
|
|
|
|
bannedGenres << "deejay" << "cool" << "big" << "exitos" << "mp3" << "muzyczne" << "nederlandstalig";
|
|
|
|
bannedGenres << "max" << "informaci" << "halk" << "dobra" << "welcome" << "genre";
|
|
|
|
|
|
|
|
// This maps genres that should be combined together
|
|
|
|
TQMap<TQString, TQString> genreMapping;
|
|
|
|
genreMapping["Romania"] = "Romanian";
|
|
|
|
genreMapping["Turk"] = "Turkish";
|
|
|
|
genreMapping["Turkce"] = "Turkish";
|
|
|
|
genreMapping["Polskie"] = "Polska";
|
|
|
|
genreMapping["Polski"] = "Polish";
|
|
|
|
genreMapping["Greece"] = "Greek";
|
|
|
|
genreMapping["Dnb"] = "Drum&bass";
|
|
|
|
genreMapping["Classic"] = "Classical";
|
|
|
|
genreMapping["Goth"] = "Gothic";
|
|
|
|
genreMapping["Alt"] = "Alternative";
|
|
|
|
genreMapping["Italiana"] = "Italian";
|
|
|
|
genreMapping["Japan"] = "Japanese";
|
|
|
|
genreMapping["Oldie"] = "Oldies";
|
|
|
|
genreMapping["Nederlands"] = "Dutch";
|
|
|
|
genreMapping["Variety"] = "Various";
|
|
|
|
genreMapping["Soundtracks"] = "Soundtrack";
|
|
|
|
genreMapping["Gaming"] = "Game";
|
|
|
|
genreMapping["Sports"] = "Sport";
|
|
|
|
genreMapping["Spain"] = "Spanish";
|
|
|
|
|
|
|
|
TQDomElement docElem = doc.documentElement();
|
|
|
|
TQDomNode n = docElem.firstChild();
|
|
|
|
TQListViewItem *last = 0;
|
|
|
|
TQMap<TQString, TQListViewItem *> genreCache; // maps names to the listview item
|
|
|
|
while( !n.isNull() )
|
|
|
|
{
|
|
|
|
TQDomElement e = n.toElement(); // try to convert the node to an element.
|
|
|
|
const TQString name = e.attribute( "name" );
|
|
|
|
if( !name.isNull() && !bannedGenres.contains( name.lower() ) && !genreMapping.contains( name ) )
|
|
|
|
{
|
|
|
|
last = new ShoutcastGenre( this, last, name );
|
|
|
|
genreCache[ name ] = last; // so we can append genres later if needed
|
|
|
|
}
|
|
|
|
n = n.nextSibling();
|
|
|
|
}
|
|
|
|
// Process the mapped (alternate) genres
|
|
|
|
for( TQMap<TQString, TQString>::iterator it = genreMapping.begin(); it != genreMapping.end(); ++it )
|
|
|
|
{
|
|
|
|
// Find the target genre
|
|
|
|
ShoutcastGenre *existingGenre = dynamic_cast<ShoutcastGenre *> ( genreCache[ it.data() ] );
|
|
|
|
if( existingGenre != 0 )
|
|
|
|
existingGenre->appendAlternateGenre( it.key() );
|
|
|
|
}
|
|
|
|
m_downloading = false;
|
|
|
|
m_animationTimer.stop();
|
|
|
|
setPixmap( 0, SmallIcon( Amarok::icon( "files" ) ) );
|
|
|
|
setOpen( true );
|
|
|
|
}
|
|
|
|
|
|
|
|
void ShoutcastBrowser::jobFinished( KIO::Job *job )
|
|
|
|
{
|
|
|
|
m_downloading = false;
|
|
|
|
m_animationTimer.stop();
|
|
|
|
setPixmap( 0, SmallIcon( Amarok::icon( "files" ) ) );
|
|
|
|
|
|
|
|
if ( job->error() )
|
|
|
|
job->showErrorDialog( 0 );
|
|
|
|
}
|
|
|
|
|
|
|
|
ShoutcastGenre::ShoutcastGenre( ShoutcastBrowser *browser, TQListViewItem *after, TQString genre )
|
|
|
|
: PlaylistCategory( browser, after, genre )
|
|
|
|
, m_downloading( false )
|
|
|
|
, m_loading1( new TQPixmap( locate("data", "amarok/images/loading1.png" ) ) )
|
|
|
|
, m_loading2( new TQPixmap( locate("data", "amarok/images/loading2.png" ) ) )
|
|
|
|
{
|
|
|
|
setExpandable( true );
|
|
|
|
setKept( false );
|
|
|
|
m_genre = genre.replace( "&", "%26" ); //fix &
|
|
|
|
}
|
|
|
|
|
|
|
|
void ShoutcastGenre::slotDoubleClicked()
|
|
|
|
{
|
|
|
|
setOpen( !isOpen() );
|
|
|
|
}
|
|
|
|
|
|
|
|
void ShoutcastGenre::setOpen( bool open )
|
|
|
|
{
|
|
|
|
if( open == isOpen())
|
|
|
|
return;
|
|
|
|
|
|
|
|
if( firstChild() ) // don't redownload everything
|
|
|
|
{
|
|
|
|
TQListViewItem::setOpen( open );
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if( !m_animationTimer.isActive() )
|
|
|
|
m_animationTimer.start( ANIMATION_INTERVAL );
|
|
|
|
connect( &m_animationTimer, TQT_SIGNAL(timeout()), this, TQT_SLOT(slotAnimation()) );
|
|
|
|
|
|
|
|
TQStringList tmpdirs = KGlobal::dirs()->resourceDirs( "tmp" );
|
|
|
|
|
|
|
|
//get the genre list from shoutcast async, and when its done call the finish up functions to process
|
|
|
|
if( !m_downloading)
|
|
|
|
{
|
|
|
|
m_downloading = true;
|
|
|
|
m_totalJobs = 0;
|
|
|
|
m_completedJobs = 0;
|
|
|
|
startGenreDownload( m_genre, tmpdirs[0] );
|
|
|
|
for( TQStringList::iterator it = m_alternateGenres.begin(); it != m_alternateGenres.end(); ++it )
|
|
|
|
startGenreDownload( *it, tmpdirs[0] );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void ShoutcastGenre::startGenreDownload( TQString genre, TQString tmppath )
|
|
|
|
{
|
|
|
|
TQString tmpfile = tmppath + "/amarok-list-" + genre + "-" + KApplication::randomString(10) + ".xml";
|
|
|
|
KIO::CopyJob *cj = KIO::copy( "http://www.shoutcast.com/sbin/newxml.phtml?genre=" + genre, tmpfile, false );
|
|
|
|
connect( cj, TQT_SIGNAL( copyingDone ( KIO::Job*, const KURL&, const KURL&, bool, bool ) ),
|
|
|
|
this, TQT_SLOT( doneListDownload( KIO::Job*, const KURL&, const KURL&, bool, bool ) ) );
|
|
|
|
connect( cj, TQT_SIGNAL( result ( KIO::Job* ) ),
|
|
|
|
this, TQT_SLOT( jobFinished( KIO::Job* ) ) );
|
|
|
|
m_totalJobs++;
|
|
|
|
}
|
|
|
|
|
|
|
|
void ShoutcastGenre::slotAnimation()
|
|
|
|
{
|
|
|
|
static int s_iconCounter = 0;
|
|
|
|
s_iconCounter % 2 ?
|
|
|
|
setPixmap( 0, *m_loading1 ):
|
|
|
|
setPixmap( 0, *m_loading2 );
|
|
|
|
|
|
|
|
s_iconCounter++;
|
|
|
|
}
|
|
|
|
|
|
|
|
void ShoutcastGenre::doneListDownload( KIO::Job *job, const KURL &from, const KURL &to, bool directory, bool renamed )
|
|
|
|
{
|
|
|
|
Q_UNUSED( job ); Q_UNUSED( from ); Q_UNUSED( directory ); Q_UNUSED( renamed );
|
|
|
|
|
|
|
|
m_completedJobs++;
|
|
|
|
|
|
|
|
TQDomDocument doc( "list" );
|
|
|
|
TQFile file( to.path() );
|
|
|
|
if ( !file.open( IO_ReadOnly ) )
|
|
|
|
{
|
|
|
|
warning() << "Cannot open shoutcast playlist xml" << endl;
|
|
|
|
m_downloading = false;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
if ( !doc.setContent( &file ) )
|
|
|
|
{
|
|
|
|
warning() << "Cannot set shoutcast playlist xml" << endl;
|
|
|
|
file.close();
|
|
|
|
m_downloading = false;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
file.close();
|
|
|
|
|
|
|
|
KIO::del(to, false, false);
|
|
|
|
|
|
|
|
//Go through the XML file and add all the stations
|
|
|
|
TQDomElement docElem = doc.documentElement();
|
|
|
|
TQDomNode n = docElem.firstChild();
|
|
|
|
while( !n.isNull() )
|
|
|
|
{
|
|
|
|
TQDomElement e = n.toElement(); // try to convert the node to an element.
|
|
|
|
if( e.hasAttribute( "name" ) )
|
|
|
|
{
|
|
|
|
if( !e.attribute( "name" ).isNull() && ! m_stations.contains( e.attribute( "name" ) ) )
|
|
|
|
{
|
|
|
|
m_stations << e.attribute( "name" );
|
|
|
|
StreamEntry* entry = new StreamEntry( this, this,
|
|
|
|
"http://www.shoutcast.com/sbin/shoutcast-playlist.pls?rn="
|
|
|
|
+ e.attribute( "id" ) + "&file=filename.pls", e.attribute( "name" ));
|
|
|
|
|
|
|
|
entry->setKept( false );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
n = n.nextSibling();
|
|
|
|
}
|
|
|
|
if( m_completedJobs == m_totalJobs )
|
|
|
|
{
|
|
|
|
setOpen( true );
|
|
|
|
m_downloading = false;
|
|
|
|
m_animationTimer.stop();
|
|
|
|
setPixmap( 0, SmallIcon( Amarok::icon( "files" ) ) );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void ShoutcastGenre::jobFinished( KIO::Job *job )
|
|
|
|
{
|
|
|
|
m_downloading = false;
|
|
|
|
m_animationTimer.stop();
|
|
|
|
setPixmap( 0, SmallIcon( Amarok::icon( "files" ) ) );
|
|
|
|
|
|
|
|
if( job->error() )
|
|
|
|
job->showErrorDialog( 0 );
|
|
|
|
}
|
|
|
|
|
|
|
|
#include "playlistbrowseritem.moc"
|