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 }