/*************************************************************************** * copyright : (c) 2004 Pierpaolo Di Panfilo * * (c) 2004 Mark Kretschmann * * (c) 2005-2006 Seb Ruiz * * (c) 2005 Christian Muehlhaeuser * * (c) 2006 Bart Cerneels * * (c) 2006 Ian Monroe * * (c) 2006 Alexandre Oliveira * * (c) 2006 Adam Pigg * * (c) 2006 Bonne Eggleston * * 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 #include #include #include //paintCell() #include //paintCell() #include #include //Used for Shoutcast random name generation #include //KDE_VERSION ifndefs. Remove this once we reach a kde 4 dep #include //smallIcon #include //podcast retrieval #include //podcast retrieval #include #include //podcast info box #include #include #include #include //podcast loading icons #include #include #include #include //rename ///////////////////////////////////////////////////////////////////////////// /// CLASS PlaylistReader //////////////////////////////////////////////////////////////////////////// class PlaylistReader : public ThreadManager::DependentJob { public: PlaylistReader( TQObject *recipient, const TQString &path ) : ThreadManager::DependentJob( recipient, "PlaylistReader" ) , m_path( TQDeepCopy( 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( bundles ); title = TQDeepCopy( title ); for( BundleList::iterator it = bundles.begin(); it != bundles.end(); ++it ) *it = TQDeepCopy( *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 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( parent )->isKept() ) return; if( !parent->parent() ) break; parent = parent->parent(); } setRenameEnabled( 0, true ); static_cast( 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(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(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( firstChild() ); it; it = static_cast( 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( 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( 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(PlaylistBrowser::instance()->m_smartCategory) ) menu.insertItem( SmallIconSet(Amarok::icon( "add_playlist" )), i18n("New Smart Playlist..."), SMART ); else if( parentCat == static_cast(PlaylistBrowser::instance()->m_dynamicCategory) ) menu.insertItem( SmallIconSet(Amarok::icon( "add_playlist" )), i18n("New Dynamic Playlist..."), DYNAMIC ); else if( parentCat == static_cast(PlaylistBrowser::instance()->m_streamsCategory) ) menu.insertItem( SmallIconSet(Amarok::icon( "add_playlist" )), i18n("Add Radio Stream..."), STREAM ); else if( parentCat == static_cast(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(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 bundles; foreachType( KURL::List, list ) bundles += MetaBundle( *it ); insertTracks( after, bundles ); } void PlaylistEntry::insertTracks( TQListViewItem *after, TQValueList bundles ) { int pos = 0; if( after ) { pos = m_trackList.find( static_cast(after)->trackInfo() ) + 1; if( pos == -1 ) return; } uint k = 0; foreachType( TQValueList, 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(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( 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(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(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 = "%1%2"; TQString str = ""; 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 += "
"; 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( 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(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 = "%1%2"; TQString str = ""; str += body.arg( i18n( "URL" ), m_url.prettyURL() ); str += "
"; 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( 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(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(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(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(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(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(child)->isNew() ) { setNew( true ); return; } } setNew( false ); } void PodcastChannel::setListened( const bool n /*true*/ ) { if( !isPolished() ) load(); TQListViewItem *child = firstChild(); while( child ) { static_cast(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 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( 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( 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(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( 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 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, 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 = "%1%2"; TQString str = ""; 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 += "
"; str += i18n( "

 Episodes

    " ); for( TQListViewItem *c = firstChild(); c; c = c->nextSibling() ) { str += TQString("
  • %1
  • ").arg( static_cast(c)->title() ); } str += "
"; PlaylistBrowser::instance()->setInfo( text(0), str ); } void PodcastChannel::slotDoubleClicked() { if( !isPolished() ) load(); KURL::List list; TQListViewItem *child = firstChild(); while( child ) { #define child static_cast(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 purgedItems; TQListViewItem *current = firstChild(); for( int i=0; current && i < childCount(); current = current->nextSibling(), i++ ) { if( i < purgeCount() ) continue; purgedItems.append( current ); } foreachType( TQValueList, purgedItems ) { TQListViewItem *item = *it; #define item static_cast(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 episodes; episodes = CollectionDB::instance()->getPodcastEpisodes( url() ); TQValueList possibleEntries; int i = 0; // qvaluelist has no reverse iterator :-( for( ; !episodes.isEmpty(); ) { PodcastEpisodeBundle episode = episodes.last(); if ( i >= restoreCount ) break; PodcastEpisode *existingItem = static_cast( 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( 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::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(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(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(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\"" ).arg( 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( 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( 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(m_parent)->setNew( true ); else static_cast(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 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( 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(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 = "%1%2"; TQString str = ""; //str += body.arg( i18n( "Title" ), m_bundle.title() ); str += body.arg( i18n( "Description" ), m_bundle.description() ); str += body.arg( i18n( "Date" ), m_bundle.date() ); str += body.arg( i18n( "Author" ), m_bundle.author() ); str += body.arg( i18n( "Type" ), m_bundle.type() ); str += body.arg( i18n( "URL" ), m_bundle.url().prettyURL() ); str += body.arg( i18n( "Local URL" ), isOnDisk() ? localUrl().prettyURL() : i18n( "n/a" ) ); str += "
"; 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, QUEUE, 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" ), QUEUE ); 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 QUEUE: 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(*it)->setListened(); } break; case NEW: for ( TQListViewItemIterator it( listView(), TQListViewItemIterator::Selected); *it; ++it ) { if ( isPodcastEpisode( *it ) ) static_cast(*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(*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").arg(item->title()), Ok|Cancel, Ok, false ) { TQVBox* vbox = makeVBoxMainWidget(); vbox->setSpacing( KDialog::spacingHint() ); m_urlRequester = new KURLRequester( vbox ); if( dynamic_cast(item->parent()) ) m_urlRequester->setURL( static_cast(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 children 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" ).arg( *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" ).arg( *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" ).arg( *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" ).arg( *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" ).arg( *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" ).arg( *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.unicode(), m_sqlForTags.length() ) .replace( "(*CurrentTimeT*)" , TQString::number(TQDateTime::currentDateTime().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 children the XML // and what to expand by, then have the children 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, QUEUE, 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" ), 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 ); } // 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 QUEUE: 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 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 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::iterator it = genreMapping.begin(); it != genreMapping.end(); ++it ) { // Find the target genre ShoutcastGenre *existingGenre = dynamic_cast ( 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"