You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
amarok/amarok/src/playlistbrowseritem.cpp

3747 lines
122 KiB

/***************************************************************************
* 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> //KDE_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").arg(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 children, 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 children. 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 children
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.arg( i18n( "Playlist" ), text(0) );
str += body.arg( i18n( "Number of tracks" ), TQString::number(m_trackCount) );
str += body.arg( i18n( "Length" ), MetaBundle::prettyTime( m_length ) );
str += body.arg( 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, QUEUE, 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" ), QUEUE );
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 QUEUE:
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 KDE_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").arg( 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, QUEUE, 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" ), QUEUE );
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 QUEUE:
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" ).arg( 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.arg( 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 KDE_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, QUEUE, 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" ), QUEUE );
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 QUEUE:
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';")
.arg( 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';")
.arg( 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';")
.arg( 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';")
.arg( 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.arg( i18n( "Description" ), description() );
str += body.arg( i18n( "Website" ), link().prettyURL() );
str += body.arg( i18n( "Copyright" ), copyright() );
str += body.arg( i18n( "URL" ), m_url.prettyURL() );
str += "</table>";
str += i18n( "<p>&nbsp;<b>Episodes</b></p><ul>" );
for( TQListViewItem *c = firstChild(); c; c = c->nextSibling() )
{
str += TQString("<li>%1</li>").arg( 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, QUEUE, 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" ), QUEUE );
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 QUEUE:
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
{