File indexing completed on 2025-01-19 04:24:36

0001 /****************************************************************************************
0002  * Copyright (c) 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 #include "UmsPodcastProvider.h"
0017 #include "core/support/Debug.h"
0018 
0019 #include <KConfigGroup>
0020 #include <KIO/DeleteJob>
0021 #include <KIO/FileCopyJob>
0022 #include <KIO/Job>
0023 
0024 #include <QAction>
0025 #include <QDialog>
0026 #include <QDialogButtonBox>
0027 #include <QDirIterator>
0028 #include <QLabel>
0029 #include <QListWidget>
0030 #include <QObject>
0031 #include <QPushButton>
0032 #include <QVBoxLayout>
0033 #include <QMimeDatabase>
0034 #include <QMimeType>
0035 
0036 #include <algorithm>
0037 
0038 using namespace Podcasts;
0039 
0040 UmsPodcastProvider::UmsPodcastProvider( const QUrl &scanDirectory )
0041         : m_scanDirectory( scanDirectory )
0042         , m_deleteEpisodeAction( nullptr )
0043         , m_deleteChannelAction( nullptr )
0044 {
0045 
0046 }
0047 
0048 UmsPodcastProvider::~UmsPodcastProvider()
0049 {
0050 
0051 }
0052 
0053 bool
0054 UmsPodcastProvider::possiblyContainsTrack( const QUrl &url ) const
0055 {
0056     Q_UNUSED( url )
0057     return false;
0058 }
0059 
0060 Meta::TrackPtr
0061 UmsPodcastProvider::trackForUrl( const QUrl &url )
0062 {
0063     Q_UNUSED( url )
0064     return Meta::TrackPtr();
0065 }
0066 
0067 PodcastEpisodePtr
0068 UmsPodcastProvider::episodeForGuid( const QString &guid )
0069 {
0070     Q_UNUSED( guid )
0071     return PodcastEpisodePtr();
0072 }
0073 
0074 void
0075 UmsPodcastProvider::addPodcast( const QUrl &url )
0076 {
0077     Q_UNUSED( url );
0078 }
0079 
0080 PodcastChannelPtr
0081 UmsPodcastProvider::addChannel( const PodcastChannelPtr &channel )
0082 {
0083     UmsPodcastChannelPtr umsChannel = UmsPodcastChannelPtr(
0084             new UmsPodcastChannel( channel, this ) );
0085     m_umsChannels << umsChannel;
0086 
0087     Q_EMIT playlistAdded( Playlists::PlaylistPtr( umsChannel.data() ) );
0088     return PodcastChannelPtr( umsChannel.data() );
0089 }
0090 
0091 PodcastEpisodePtr
0092 UmsPodcastProvider::addEpisode( PodcastEpisodePtr episode )
0093 {
0094     QUrl localFilePath = episode->playableUrl();
0095     if( !localFilePath.isLocalFile() )
0096         return PodcastEpisodePtr();
0097 
0098     QUrl destination = m_scanDirectory;
0099     destination = destination.adjusted(QUrl::StripTrailingSlash);
0100     destination.setPath(destination.path() + QLatin1Char('/') + ( Amarok::vfatPath( episode->channel()->prettyName() ) ));
0101     KIO::mkdir( destination );
0102     destination = destination.adjusted(QUrl::StripTrailingSlash);
0103     destination.setPath(destination.path() + QLatin1Char('/') + ( Amarok::vfatPath( localFilePath.fileName() ) ));
0104 
0105     debug() << QString( "Copy episode \"%1\" to %2" ).arg( localFilePath.path(),
0106             destination.path() );
0107     KIO::FileCopyJob *copyJob = KIO::file_copy( localFilePath, destination );
0108     connect( copyJob, &KJob::result, this, &UmsPodcastProvider::slotCopyComplete );
0109     copyJob->start();
0110     //we have not copied the data over yet so we can't return an episode yet
0111     //TODO: return a proxy for the episode we are still copying.
0112     return PodcastEpisodePtr();
0113 }
0114 
0115 void
0116 UmsPodcastProvider::slotCopyComplete( KJob *job )
0117 {
0118     KIO::FileCopyJob *copyJob = dynamic_cast<KIO::FileCopyJob *>( job );
0119     if( !copyJob )
0120         return;
0121 
0122     QUrl localFilePath = copyJob->destUrl();
0123     MetaFile::Track *fileTrack = new MetaFile::Track( localFilePath );
0124 
0125     UmsPodcastEpisodePtr umsEpisode = addFile( MetaFile::TrackPtr( fileTrack ) );
0126 }
0127 
0128 PodcastChannelList
0129 UmsPodcastProvider::channels()
0130 {
0131     return UmsPodcastChannel::toPodcastChannelList( m_umsChannels );
0132 }
0133 
0134 void
0135 UmsPodcastProvider::removeSubscription( const PodcastChannelPtr &channel )
0136 {
0137     UmsPodcastChannelPtr umsChannel = UmsPodcastChannelPtr::dynamicCast( channel );
0138     if( umsChannel.isNull() )
0139     {
0140         error() << "trying to remove a podcast channel of the wrong type";
0141         return;
0142     }
0143 
0144     if( !m_umsChannels.contains( umsChannel ) )
0145     {
0146         error() << "trying to remove a podcast channel that is not in the list";
0147         return;
0148     }
0149 
0150     m_umsChannels.removeAll( umsChannel );
0151 }
0152 
0153 void
0154 UmsPodcastProvider::configureProvider()
0155 {
0156 }
0157 
0158 void
0159 UmsPodcastProvider::configureChannel( const PodcastChannelPtr &channel )
0160 {
0161     Q_UNUSED( channel );
0162 }
0163 
0164 QString
0165 UmsPodcastProvider::prettyName() const
0166 {
0167     return i18nc( "Podcasts on a media device", "Podcasts on %1", QStringLiteral("TODO: replace me") );
0168 }
0169 
0170 QIcon
0171 UmsPodcastProvider::icon() const
0172 {
0173     return QIcon::fromTheme("drive-removable-media-usb-pendrive");
0174 }
0175 
0176 Playlists::PlaylistList
0177 UmsPodcastProvider::playlists()
0178 {
0179     Playlists::PlaylistList playlists;
0180     foreach( UmsPodcastChannelPtr channel, m_umsChannels )
0181         playlists << Playlists::PlaylistPtr::dynamicCast( channel );
0182     return playlists;
0183 }
0184 
0185 QActionList
0186 UmsPodcastProvider::episodeActions( const PodcastEpisodeList &episodes )
0187 {
0188     QActionList actions;
0189     if( episodes.isEmpty() )
0190         return actions;
0191 
0192     if( m_deleteEpisodeAction == nullptr )
0193     {
0194         m_deleteEpisodeAction = new QAction( QIcon::fromTheme( "edit-delete" ), i18n( "&Delete Episode" ), this );
0195         m_deleteEpisodeAction->setProperty( "popupdropper_svg_id", "delete" );
0196         connect( m_deleteEpisodeAction, &QAction::triggered, this, &UmsPodcastProvider::slotDeleteEpisodes );
0197     }
0198     // set the episode list as data that we'll retrieve in the slot
0199     m_deleteEpisodeAction->setData( QVariant::fromValue( episodes ) );
0200     actions << m_deleteEpisodeAction;
0201 
0202     return actions;
0203 }
0204 
0205 void
0206 UmsPodcastProvider::slotDeleteEpisodes()
0207 {
0208     DEBUG_BLOCK
0209     QAction *action = qobject_cast<QAction *>( QObject::sender() );
0210     if( action == nullptr )
0211         return;
0212 
0213     //get the list of episodes to apply to, then clear that data.
0214     PodcastEpisodeList episodes =
0215             action->data().value<PodcastEpisodeList>();
0216     action->setData( QVariant() );
0217 
0218     UmsPodcastEpisodeList umsEpisodes;
0219     foreach( PodcastEpisodePtr episode, episodes )
0220     {
0221         UmsPodcastEpisodePtr umsEpisode =
0222                 UmsPodcastEpisode::fromPodcastEpisodePtr( episode );
0223         if( !umsEpisode )
0224         {
0225             error() << "Could not cast to UmsPodcastEpisode";
0226             continue;
0227         }
0228 
0229         PodcastChannelPtr channel = umsEpisode->channel();
0230         if( !channel )
0231         {
0232             error() << "episode did not have a valid channel";
0233             continue;
0234         }
0235 
0236         UmsPodcastChannelPtr umsChannel =
0237                 UmsPodcastChannel::fromPodcastChannelPtr( channel );
0238         if( !umsChannel )
0239         {
0240             error() << "Could not cast to UmsPodcastChannel";
0241             continue;
0242         }
0243 
0244         umsEpisodes << umsEpisode;
0245     }
0246 
0247     deleteEpisodes( umsEpisodes );
0248 }
0249 
0250 void
0251 UmsPodcastProvider::deleteEpisodes( UmsPodcastEpisodeList umsEpisodes )
0252 {
0253     QList<QUrl> urlsToDelete;
0254     foreach( UmsPodcastEpisodePtr umsEpisode, umsEpisodes )
0255         urlsToDelete << umsEpisode->playableUrl();
0256 
0257     QDialog dialog;
0258     dialog.setWindowTitle( i18n( "Confirm Delete" ) );
0259 
0260     QLabel *label = new QLabel( i18np( "Are you sure you want to delete this episode?",
0261                                        "Are you sure you want to delete these %1 episodes?",
0262                                        urlsToDelete.count() ),
0263                                 &dialog );
0264     QListWidget *listWidget = new QListWidget( &dialog );
0265     listWidget->setSelectionMode( QAbstractItemView::NoSelection );
0266     foreach( const QUrl &url, urlsToDelete )
0267     {
0268         new QListWidgetItem( url.toLocalFile(), listWidget );
0269     }
0270 
0271     QWidget *widget = new QWidget( &dialog );
0272     QDialogButtonBox *buttonBox = new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Cancel );
0273     QVBoxLayout *layout = new QVBoxLayout( widget );
0274     layout->addWidget( label );
0275     layout->addWidget( listWidget );
0276     layout->addWidget( buttonBox );
0277 
0278     buttonBox->button( QDialogButtonBox::Ok )->setText( i18n( "Yes, delete from %1.",
0279                                                         QStringLiteral("TODO: replace me") ) );
0280 
0281     if( dialog.exec() != QDialog::Accepted )
0282         return;
0283 
0284     KIO::DeleteJob *deleteJob = KIO::del( urlsToDelete, KIO::HideProgressInfo );
0285 
0286     //keep track of these episodes until the job is done
0287     m_deleteJobMap.insert( deleteJob, umsEpisodes );
0288 
0289     connect( deleteJob, &KJob::result, this, &UmsPodcastProvider::deleteJobComplete );
0290 }
0291 
0292 void
0293 UmsPodcastProvider::deleteJobComplete( KJob *job )
0294 {
0295     DEBUG_BLOCK
0296     if( job->error() )
0297     {
0298         error() << "problem deleting episode(s): " << job->errorString();
0299         return;
0300     }
0301 
0302     UmsPodcastEpisodeList deletedEpisodes = m_deleteJobMap.take( job );
0303     foreach( UmsPodcastEpisodePtr deletedEpisode, deletedEpisodes )
0304     {
0305         PodcastChannelPtr channel = deletedEpisode->channel();
0306         UmsPodcastChannelPtr umsChannel =
0307                 UmsPodcastChannel::fromPodcastChannelPtr( channel );
0308         if( !umsChannel )
0309         {
0310             error() << "Could not cast to UmsPodcastChannel";
0311             continue;
0312         }
0313 
0314         umsChannel->removeEpisode( deletedEpisode );
0315         if( umsChannel->m_umsEpisodes.isEmpty() )
0316         {
0317             debug() << "channel is empty now, remove it";
0318             m_umsChannels.removeAll( umsChannel );
0319             Q_EMIT( playlistRemoved( Playlists::PlaylistPtr::dynamicCast( umsChannel ) ) );
0320         }
0321     }
0322 }
0323 
0324 QActionList
0325 UmsPodcastProvider::channelActions( const PodcastChannelList &channels )
0326 {
0327     QActionList actions;
0328     if( channels.isEmpty() )
0329         return actions;
0330 
0331     if( m_deleteChannelAction == nullptr )
0332     {
0333         m_deleteChannelAction = new QAction( QIcon::fromTheme( "edit-delete" ), i18n( "&Delete "
0334                 "Channel and Episodes" ), this );
0335         m_deleteChannelAction->setProperty( "popupdropper_svg_id", "delete" );
0336         connect( m_deleteChannelAction, &QAction::triggered, this, &UmsPodcastProvider::slotDeleteChannels );
0337     }
0338     // set the episode list as data that we'll retrieve in the slot
0339     m_deleteChannelAction->setData( QVariant::fromValue( channels ) );
0340     actions << m_deleteChannelAction;
0341 
0342     return actions;
0343 }
0344 
0345 void
0346 UmsPodcastProvider::slotDeleteChannels()
0347 {
0348     DEBUG_BLOCK
0349     QAction *action = qobject_cast<QAction *>( QObject::sender() );
0350     if( action == nullptr )
0351         return;
0352 
0353     //get the list of episodes to apply to, then clear that data.
0354     PodcastChannelList channels =
0355             action->data().value<PodcastChannelList>();
0356     action->setData( QVariant() );
0357 
0358     foreach( PodcastChannelPtr channel, channels )
0359     {
0360         UmsPodcastChannelPtr umsChannel =
0361                 UmsPodcastChannel::fromPodcastChannelPtr( channel );
0362         if( !umsChannel )
0363         {
0364             error() << "Could not cast to UmsPodcastChannel";
0365             continue;
0366         }
0367 
0368         deleteEpisodes( umsChannel->m_umsEpisodes );
0369         //slot deleteJobComplete() will Q_EMIT signal once all tracks are gone.
0370     }
0371 }
0372 
0373 QActionList
0374 UmsPodcastProvider::playlistActions( const Playlists::PlaylistList &playlists )
0375 {
0376     PodcastChannelList channels;
0377     foreach( const Playlists::PlaylistPtr &playlist, playlists )
0378     {
0379         PodcastChannelPtr channel = PodcastChannelPtr::dynamicCast( playlist );
0380         if( channel )
0381             channels << channel;
0382     }
0383 
0384     return channelActions( channels );
0385 }
0386 
0387 QActionList
0388 UmsPodcastProvider::trackActions( const QMultiHash<Playlists::PlaylistPtr, int> &playlistTracks )
0389 {
0390     PodcastEpisodeList episodes;
0391     foreach( const Playlists::PlaylistPtr &playlist, playlistTracks.uniqueKeys() )
0392     {
0393         PodcastChannelPtr channel = PodcastChannelPtr::dynamicCast( playlist );
0394         if( !channel )
0395             continue;
0396 
0397         PodcastEpisodeList channelEpisodes = channel->episodes();
0398         QList<int> trackPositions = playlistTracks.values( playlist );
0399         std::sort( trackPositions.begin(), trackPositions.end() );
0400         foreach( int trackPosition, trackPositions )
0401         {
0402             if( trackPosition >= 0 && trackPosition < channelEpisodes.count() )
0403                 episodes << channelEpisodes.at( trackPosition );
0404         }
0405     }
0406 
0407     return episodeActions( episodes );
0408 }
0409 
0410 void
0411 UmsPodcastProvider::completePodcastDownloads()
0412 {
0413 
0414 }
0415 
0416 void
0417 UmsPodcastProvider::updateAll() //slot
0418 {
0419 }
0420 
0421 void
0422 UmsPodcastProvider::update( const Podcasts::PodcastChannelPtr &channel ) //slot
0423 {
0424     Q_UNUSED( channel );
0425 }
0426 
0427 void
0428 UmsPodcastProvider::downloadEpisode( const Podcasts::PodcastEpisodePtr &episode ) //slot
0429 {
0430     Q_UNUSED( episode );
0431 }
0432 
0433 void
0434 UmsPodcastProvider::deleteDownloadedEpisode( const Podcasts::PodcastEpisodePtr &episode ) //slot
0435 {
0436     Q_UNUSED( episode );
0437 }
0438 
0439 void
0440 UmsPodcastProvider::slotUpdated() //slot
0441 {
0442 
0443 }
0444 
0445 void
0446 UmsPodcastProvider::scan()
0447 {
0448     if( m_scanDirectory.isEmpty() )
0449         return;
0450     m_dirList.clear();
0451     debug() << "scan directory for podcasts: " <<
0452             m_scanDirectory.toLocalFile();
0453     QDirIterator it( m_scanDirectory.toLocalFile(), QDirIterator::Subdirectories );
0454     while( it.hasNext() )
0455         addPath( it.next() );
0456 }
0457 
0458 int
0459 UmsPodcastProvider::addPath( const QString &path )
0460 {
0461     DEBUG_BLOCK
0462     int acc = 0;
0463     QMimeDatabase db;
0464     debug() << path;
0465     QMimeType mime = db.mimeTypeForFile( path, QMimeDatabase::MatchContent );
0466     if( !mime.isValid() || mime.isDefault() )
0467     {
0468         debug() << "Trying again with findByPath:" ;
0469         mime = db.mimeTypeForFile( path, QMimeDatabase::MatchExtension);
0470         if( mime.isDefault() )
0471             return 0;
0472     }
0473     debug() << "Got type: " << mime.name() << ", with accuracy: " << acc;
0474 
0475     QFileInfo info( path );
0476     if( info.isDir() )
0477     {
0478         if( m_dirList.contains( path ) )
0479             return 0;
0480         m_dirList << info.canonicalPath();
0481         return 1;
0482     }
0483     else if( info.isFile() )
0484     {
0485 //        foreach( const QString &mimetype, m_handler->mimetypes() )
0486 //        {
0487 //            if( mime.inherits( mimetype ) )
0488 //            {
0489                 addFile( MetaFile::TrackPtr( new MetaFile::Track(
0490                     QUrl::fromLocalFile( info.canonicalFilePath() ) ) ) );
0491                 return 2;
0492 //            }
0493 //        }
0494     }
0495 
0496     return 0;
0497 }
0498 
0499 UmsPodcastEpisodePtr
0500 UmsPodcastProvider::addFile( MetaFile::TrackPtr metafileTrack )
0501 {
0502     DEBUG_BLOCK
0503     debug() << metafileTrack->playableUrl().url();
0504     debug() << "album: " << metafileTrack->album()->name();
0505     debug() << "title: " << metafileTrack->name();
0506     if( metafileTrack->album()->name().isEmpty() )
0507     {
0508         debug() << "Can't figure out channel without album tag.";
0509         return UmsPodcastEpisodePtr();
0510     }
0511 
0512     if( metafileTrack->name().isEmpty() )
0513     {
0514         debug() << "Can not use a track without a title.";
0515         return UmsPodcastEpisodePtr();
0516     }
0517 
0518     //see if there is already a UmsPodcastEpisode for this track
0519     UmsPodcastChannelPtr channel;
0520     UmsPodcastEpisodePtr episode;
0521 
0522     foreach( UmsPodcastChannelPtr c, m_umsChannels )
0523     {
0524         if( c->name() == metafileTrack->album()->name() )
0525         {
0526             channel = c;
0527             break;
0528         }
0529     }
0530 
0531     if( channel )
0532     {
0533         foreach( UmsPodcastEpisodePtr e, channel->umsEpisodes() )
0534         {
0535             if( e->title() == metafileTrack->name() )
0536             {
0537                 episode = e;
0538                 break;
0539             }
0540         }
0541     }
0542     else
0543     {
0544         debug() << "there is no channel for this episode yet";
0545         channel = UmsPodcastChannelPtr( new UmsPodcastChannel( this ) );
0546         channel->setTitle( metafileTrack->album()->name() );
0547         m_umsChannels << channel;
0548         Q_EMIT playlistAdded( Playlists::PlaylistPtr( channel.data() ) );
0549     }
0550 
0551     if( episode.isNull() )
0552     {
0553         debug() << "this episode was not found in an existing channel";
0554         episode = UmsPodcastEpisodePtr( new UmsPodcastEpisode( channel ) );
0555         episode->setLocalFile( metafileTrack );
0556 
0557         channel->addUmsEpisode( episode );
0558     }
0559 
0560     episode->setLocalFile( metafileTrack );
0561 
0562     return episode;
0563 }