File indexing completed on 2024-05-19 04:48:41

0001 /****************************************************************************************
0002  * Copyright (c) 2007-2010 Bart Cerneels <bart.cerneels@kde.org>                        *
0003  *                                                                                      *
0004  * This program is free software; you can redistribute it and/or modify it under        *
0005  * the terms of the GNU General Public License as published by the Free Software        *
0006  * Foundation; either version 2 of the License, or (at your option) any later           *
0007  * version.                                                                             *
0008  *                                                                                      *
0009  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
0010  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
0011  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
0012  *                                                                                      *
0013  * You should have received a copy of the GNU General Public License along with         *
0014  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
0015  ****************************************************************************************/
0016 
0017 #include "PodcastModel.h"
0018 
0019 #include "AmarokMimeData.h"
0020 // #include "context/popupdropper/libpud/PopupDropper.h"
0021 // #include "context/popupdropper/libpud/PopupDropperItem.h"
0022 #include "core/podcasts/PodcastImageFetcher.h"
0023 #include "core/podcasts/PodcastMeta.h"
0024 #include "core/support/Debug.h"
0025 #include "playlistmanager/PlaylistManager.h"
0026 #include "playlistmanager/SyncedPodcast.h"
0027 #include "PodcastCategory.h"
0028 #include "SvgHandler.h"
0029 #include "widgets/PrettyTreeRoles.h"
0030 
0031 #include <QAction>
0032 #include <QIcon>
0033 #include <QInputDialog>
0034 #include <QListIterator>
0035 #include <QPainter>
0036 
0037 #include <KIconEngine>
0038 
0039 using namespace Podcasts;
0040 
0041 namespace The
0042 {
0043     PlaylistBrowserNS::PodcastModel* podcastModel()
0044     {
0045         return PlaylistBrowserNS::PodcastModel::instance();
0046     }
0047 }
0048 
0049 PlaylistBrowserNS::PodcastModel* PlaylistBrowserNS::PodcastModel::s_instance = nullptr;
0050 
0051 PlaylistBrowserNS::PodcastModel*
0052 PlaylistBrowserNS::PodcastModel::instance()
0053 {
0054     return s_instance ? s_instance : new PodcastModel();
0055 }
0056 
0057 void
0058 PlaylistBrowserNS::PodcastModel::destroy()
0059 {
0060     if ( s_instance )
0061     {
0062         delete s_instance;
0063         s_instance = nullptr;
0064     }
0065 }
0066 
0067 PlaylistBrowserNS::PodcastModel::PodcastModel()
0068     : PlaylistBrowserModel( PlaylistManager::PodcastChannel )
0069 {
0070     s_instance = this;
0071 }
0072 
0073 bool
0074 PlaylistBrowserNS::PodcastModel::isOnDisk( PodcastEpisodePtr episode ) const
0075 {
0076     bool isOnDisk = false;
0077     QUrl episodeFile( episode->localUrl() );
0078 
0079     if( !episodeFile.isEmpty() )
0080     {
0081         isOnDisk = QFileInfo( episodeFile.toLocalFile() ).exists();
0082         // reset localUrl because the file is not there.
0083         // FIXME: changing a podcast in innocent-looking getter method is convoluted
0084         if( !isOnDisk )
0085             episode->setLocalUrl( QUrl() );
0086     }
0087 
0088     return isOnDisk;
0089 }
0090 
0091 QVariant
0092 PlaylistBrowserNS::PodcastModel::icon( const PodcastChannelPtr &channel ) const
0093 {
0094     QStringList emblems;
0095     //TODO: only check visible episodes. For now those are all returned by episodes().
0096     foreach( const Podcasts::PodcastEpisodePtr ep, channel->episodes() )
0097     {
0098         if( ep->isNew() )
0099         {
0100             emblems << QStringLiteral("rating");
0101             break;
0102         }
0103     }
0104 
0105     if( channel->hasImage() )
0106     {
0107         QSize size( channel->image().size() );
0108         QPixmap pixmap( 32, 32 );
0109         pixmap.fill( Qt::transparent );
0110 
0111         size.scale( 32, 32, Qt::KeepAspectRatio );
0112 
0113         int x = 32 / 2 - size.width()  / 2;
0114         int y = 32 / 2 - size.height() / 2;
0115 
0116         QPainter p( &pixmap );
0117         p.drawPixmap( x, y, QPixmap::fromImage( channel->image().scaled( size,
0118                 Qt::KeepAspectRatio, Qt::SmoothTransformation ) ) );
0119 
0120         // if it's a new episode draw the overlay:
0121         if( !emblems.isEmpty() )
0122             // draw the overlay the same way KIconLoader does:
0123             p.drawPixmap( 2, 32 - 16 - 2, QIcon::fromTheme( QStringLiteral("rating") ).pixmap( 16, 16 ) );
0124         p.end();
0125 
0126         return pixmap;
0127     }
0128     else
0129         return ( QIcon(new KIconEngine( "podcast-amarok", nullptr, emblems )).pixmap( 32, 32 ) );
0130 }
0131 
0132 QVariant
0133 PlaylistBrowserNS::PodcastModel::icon( const PodcastEpisodePtr &episode ) const
0134 {
0135     QStringList emblems;
0136     if( isOnDisk( episode ) )
0137         emblems << QStringLiteral("go-down");
0138 
0139     if( episode->isNew() )
0140         return ( QIcon( new KIconEngine( "rating", nullptr, emblems )).pixmap( 24, 24 ) );
0141     else
0142         return ( QIcon( new KIconEngine( "podcast-amarok", nullptr, emblems )).pixmap( 24, 24 ));
0143 }
0144 
0145 QVariant
0146 PlaylistBrowserNS::PodcastModel::data( const QModelIndex &idx, int role ) const
0147 {
0148     if( !idx.isValid() )
0149         return PlaylistBrowserModel::data( idx, role );
0150 
0151     if( IS_TRACK(idx) )
0152         return episodeData( episodeForIndex( idx ), idx, role );
0153     else
0154         return channelData( channelForIndex( idx ), idx, role );
0155 }
0156 
0157 QVariant
0158 PlaylistBrowserNS::PodcastModel::channelData( const PodcastChannelPtr &channel,
0159                                               const QModelIndex &idx, int role ) const
0160 {
0161     if( !channel )
0162         return QVariant();
0163 
0164     switch( role )
0165     {
0166         case Qt::DisplayRole:
0167         case Qt::ToolTipRole:
0168             switch( idx.column() )
0169             {
0170                 case PlaylistBrowserModel::PlaylistItemColumn:
0171                     return channel->title();
0172                 case SubtitleColumn:
0173                     return channel->subtitle();
0174                 case AuthorColumn:
0175                     return channel->author();
0176                 case KeywordsColumn:
0177                     return channel->keywords();
0178                 case ImageColumn:
0179                 {
0180                     QUrl imageUrl( PodcastImageFetcher::cachedImagePath( channel ) );
0181                     if( !QFile( imageUrl.toLocalFile() ).exists() )
0182                         imageUrl = channel->imageUrl();
0183                     return imageUrl;
0184                 }
0185                 case DateColumn:
0186                     return channel->subscribeDate();
0187                 case IsEpisodeColumn:
0188                     return false;
0189             }
0190             break;
0191         case PrettyTreeRoles::ByLineRole:
0192             if( idx.column() == PlaylistBrowserModel::ProviderColumn )
0193             {
0194                 Playlists::PlaylistProvider *provider = providerForIndex( idx );
0195                 if( provider )
0196                     return i18ncp( "number of podcasts from one source",
0197                                     "One Channel", "%1 channels",
0198                                     provider->playlists().count() );
0199             }
0200             if( idx.column() == PlaylistBrowserModel::PlaylistItemColumn )
0201                 return channel->description();
0202             break;
0203         case PrettyTreeRoles::HasCoverRole:
0204             return idx.column() == PlaylistBrowserModel::PlaylistItemColumn;
0205         case Qt::DecorationRole:
0206             if( idx.column() == PlaylistBrowserModel::PlaylistItemColumn )
0207                 return icon( channel );
0208             break;
0209     }
0210 
0211     return PlaylistBrowserModel::data( idx, role );
0212 }
0213 
0214 QVariant
0215 PlaylistBrowserNS::PodcastModel::episodeData( const PodcastEpisodePtr &episode,
0216                                               const QModelIndex &idx, int role ) const
0217 {
0218     if( !episode )
0219         return QVariant();
0220 
0221     switch( role )
0222     {
0223         case Qt::DisplayRole:
0224         case Qt::ToolTipRole:
0225             switch( idx.column() )
0226             {
0227                 case PlaylistBrowserModel::PlaylistItemColumn:
0228                     return episode->title();
0229                 case SubtitleColumn:
0230                     return episode->subtitle();
0231                 case AuthorColumn:
0232                     return episode->author();
0233                 case KeywordsColumn:
0234                     return episode->keywords();
0235                 case FilesizeColumn:
0236                     return episode->filesize();
0237                 case DateColumn:
0238                     return episode->pubDate();
0239                 case IsEpisodeColumn:
0240                     return true;
0241             }
0242             break;
0243         case PrettyTreeRoles::ByLineRole:
0244             if( idx.column() == PlaylistBrowserModel::ProviderColumn )
0245             {
0246                 Playlists::PlaylistProvider *provider = providerForIndex( idx );
0247                 if( provider )
0248                     return i18ncp( "number of podcasts from one source",
0249                                     "One Channel", "%1 channels",
0250                                     provider->playlists().count() );
0251             }
0252             if( idx.column() == PlaylistBrowserModel::PlaylistItemColumn )
0253                 return episode->description();
0254             break;
0255         case PrettyTreeRoles::HasCoverRole:
0256             return ( idx.column() == PlaylistBrowserModel::PlaylistItemColumn );
0257         case Qt::DecorationRole:
0258             if( idx.column() == PlaylistBrowserModel::PlaylistItemColumn )
0259                 return icon( episode );
0260             break;
0261         case EpisodeIsNewRole:
0262             return episode->isNew();
0263     }
0264 
0265     return PlaylistBrowserModel::data( idx, role );
0266 }
0267 
0268 bool
0269 PlaylistBrowserNS::PodcastModel::setData( const QModelIndex &idx, const QVariant &value, int role )
0270 {
0271     PodcastEpisodePtr episode = episodeForIndex( idx );
0272     if( !episode || !value.canConvert<bool>() || role != EpisodeIsNewRole )
0273     {
0274         return PlaylistBrowserModel::setData( idx, value, role );
0275     }
0276 
0277     bool checked = value.toBool();
0278     episode->setNew( checked );
0279     if( checked )
0280         Q_EMIT episodeMarkedAsNew( episode );
0281     Q_EMIT dataChanged( idx, idx );
0282     return true;
0283 }
0284 
0285 int
0286 PlaylistBrowserNS::PodcastModel::columnCount( const QModelIndex &parent ) const
0287 {
0288     Q_UNUSED( parent )
0289     return ColumnCount;
0290 }
0291 
0292 QVariant
0293 PlaylistBrowserNS::PodcastModel::headerData( int section, Qt::Orientation orientation,
0294                                              int role) const
0295 {
0296     if( orientation == Qt::Horizontal && role == Qt::DisplayRole )
0297     {
0298         switch( section )
0299         {
0300             case 0: return i18n("Type");
0301             case 1: return i18n("Title");
0302             case 2: return i18n("Summary");
0303             default: return QVariant();
0304         }
0305     }
0306 
0307     return QVariant();
0308 }
0309 
0310 void
0311 PlaylistBrowserNS::PodcastModel::addPodcast()
0312 {
0313     debug() << "adding Podcast";
0314 
0315     //TODO: request the user to which PodcastProvider he wants to add it in case
0316     // of multiple (enabled) Podcast Providers.
0317     Podcasts::PodcastProvider *podcastProvider = The::playlistManager()->defaultPodcasts();
0318     if( podcastProvider )
0319     {
0320         bool ok;
0321         QString url = QInputDialog::getText( nullptr,
0322                             i18n("Add Podcast"),
0323                             i18n("Enter RSS 1.0/2.0 or Atom feed URL:"),
0324                             QLineEdit::Normal,
0325                             QString(),
0326                             &ok );
0327         if( ok && !url.isEmpty() )
0328         {
0329             // user entered something and pressed OK
0330             podcastProvider->addPodcast( Podcasts::PodcastProvider::toFeedUrl( url.trimmed() ) );
0331         }
0332         else
0333         {
0334             // user entered nothing or pressed Cancel
0335             debug() << "invalid input or cancel";
0336         }
0337     }
0338     else
0339     {
0340         debug() << "PodcastChannel provider is null";
0341     }
0342 
0343 }
0344 
0345 void
0346 PlaylistBrowserNS::PodcastModel::refreshPodcasts()
0347 {
0348     foreach( Playlists::PlaylistProvider *provider,
0349              The::playlistManager()->providersForCategory( PlaylistManager::PodcastChannel ) )
0350     {
0351         PodcastProvider *podcastProvider = dynamic_cast<PodcastProvider *>( provider );
0352         if( podcastProvider )
0353             podcastProvider->updateAll();
0354     }
0355 }
0356 
0357 Podcasts::PodcastChannelPtr
0358 PlaylistBrowserNS::PodcastModel::channelForIndex( const QModelIndex &idx ) const
0359 {
0360     return Podcasts::PodcastChannelPtr::dynamicCast( playlistFromIndex( idx ) );
0361 }
0362 
0363 Podcasts::PodcastEpisodePtr
0364 PlaylistBrowserNS::PodcastModel::episodeForIndex( const QModelIndex &idx ) const
0365 {
0366     return Podcasts::PodcastEpisodePtr::dynamicCast( trackFromIndex( idx ) );
0367 }
0368 
0369 Meta::TrackList
0370 PlaylistBrowserNS::PodcastModel::podcastEpisodesToTracks( Podcasts::PodcastEpisodeList episodes )
0371 {
0372     Meta::TrackList tracks;
0373     foreach( Podcasts::PodcastEpisodePtr episode, episodes )
0374         tracks << Meta::TrackPtr::staticCast( episode );
0375     return tracks;
0376 }