File indexing completed on 2024-05-19 04:50:12
0001 /**************************************************************************************** 0002 * Copyright (c) 2011 Stefan Derkits <stefan@derkits.at> * 0003 * Copyright (c) 2011 Christian Wagner <christian.wagner86@gmx.at> * 0004 * Copyright (c) 2011 Felix Winter <ixos01@gmail.com> * 0005 * Copyright (c) 2011 Lucas Lira Gomes <x8lucas8x@gmail.com> * 0006 * * 0007 * This program is free software; you can redistribute it and/or modify it under * 0008 * the terms of the GNU General Public License as published by the Free Software * 0009 * Foundation; either version 2 of the License, or (at your option) any later * 0010 * version. * 0011 * * 0012 * This program is distributed in the hope that it will be useful, but WITHOUT ANY * 0013 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * 0014 * PARTICULAR PURPOSE. See the GNU General Public License for more details. * 0015 * * 0016 * You should have received a copy of the GNU General Public License along with * 0017 * this program. If not, see <http://www.gnu.org/licenses/>. * 0018 ****************************************************************************************/ 0019 0020 #define DEBUG_PREFIX "GpodderProvider" 0021 0022 #include "GpodderProvider.h" 0023 0024 #include "core-impl/capabilities/timecode/TimecodeWriteCapability.h" 0025 #include "core-impl/podcasts/sql/SqlPodcastProvider.h" 0026 #include "core/logger/Logger.h" 0027 #include "core/support/Amarok.h" 0028 #include "core/support/Components.h" 0029 #include "core/support/Debug.h" 0030 #include "EngineController.h" 0031 #include "gpodder/GpodderServiceConfig.h" 0032 #include "NetworkAccessManagerProxy.h" 0033 #include "PodcastModel.h" 0034 0035 #include <QAction> 0036 #include <QLabel> 0037 #include <QNetworkConfigurationManager> 0038 #include <QTimer> 0039 0040 #include <KIO/TransferJob> 0041 0042 0043 using namespace Podcasts; 0044 0045 GpodderProvider::GpodderProvider( const QString& username, 0046 const QString& devicename, 0047 ApiRequest *apiRequest ) 0048 : m_apiRequest( apiRequest ) 0049 , m_username( username ) 0050 , m_deviceName( devicename ) 0051 , m_channels() 0052 , m_addRemoveResult() 0053 , m_deviceUpdatesResult() 0054 , m_episodeActionListResult() 0055 , m_timestampStatus( 0 ) 0056 , m_timestampSubscription( subscriptionTimestamp() ) 0057 , m_removeAction( nullptr ) 0058 , m_addList() 0059 , m_removeList() 0060 , m_timerGeneratePlayAction( new QTimer( this ) ) 0061 , m_timerSynchronizeStatus( new QTimer( this ) ) 0062 , m_timerSynchronizeSubscriptions( new QTimer( this ) ) 0063 { 0064 //We have to load episode actions and podcasts subscriptions changes 0065 //that weren't uploaded before the time we closed amarok 0066 loadCachedEpisodeActions(); 0067 loadCachedPodcastsChanges(); 0068 0069 //Request all channels and episodes from m_devicename device and after it 0070 //request episode actions too 0071 requestDeviceUpdates(); 0072 0073 //Connect default podcasts signals to make possible to ask the user if he wants 0074 //to upload a new local podcast to gpodder.net 0075 connect( The::playlistManager()->defaultPodcasts(), &PodcastProvider::playlistAdded, 0076 this, &GpodderProvider::slotSyncPlaylistAdded ); 0077 connect( The::playlistManager()->defaultPodcasts(), &PodcastProvider::playlistRemoved, 0078 this, &GpodderProvider::slotSyncPlaylistRemoved ); 0079 0080 Podcasts::SqlPodcastProvider *sqlPodcastProvider; 0081 0082 sqlPodcastProvider = dynamic_cast<Podcasts::SqlPodcastProvider *> 0083 ( The::playlistManager()->defaultPodcasts() ); 0084 0085 connect( The::podcastModel(), &PlaylistBrowserNS::PodcastModel::episodeMarkedAsNew, 0086 this, &GpodderProvider::slotEpisodeMarkedAsNew ); 0087 0088 if( sqlPodcastProvider ) 0089 { 0090 connect( sqlPodcastProvider, &SqlPodcastProvider::episodeDeleted, 0091 this, &GpodderProvider::slotEpisodeDeleted ); 0092 connect( sqlPodcastProvider,&SqlPodcastProvider::episodeDownloaded, 0093 this, &GpodderProvider::slotEpisodeDownloaded ); 0094 } 0095 0096 //Connect engine controller signals to make possible to synchronize podcast status 0097 connect( The::engineController(), &EngineController::trackChanged, 0098 this, &GpodderProvider::slotTrackChanged ); 0099 connect( The::engineController(), &EngineController::trackPositionChanged, 0100 this, &GpodderProvider::slotTrackPositionChanged ); 0101 connect( The::engineController(), &EngineController::paused, 0102 this, &GpodderProvider::slotPaused ); 0103 0104 //These timers will periodically synchronize data between local podcasts and gpodder.net 0105 connect( m_timerSynchronizeStatus, &QTimer::timeout, 0106 this, &GpodderProvider::timerSynchronizeStatus ); 0107 connect( m_timerSynchronizeSubscriptions, &QTimer::timeout, 0108 this, &GpodderProvider::timerSynchronizeSubscriptions ); 0109 connect( m_timerGeneratePlayAction, &QTimer::timeout, 0110 this, &GpodderProvider::timerGenerateEpisodeAction ); 0111 0112 m_timerGeneratePlayAction->stop(); 0113 m_timerSynchronizeStatus->stop(); 0114 m_timerSynchronizeSubscriptions->stop(); 0115 } 0116 0117 GpodderProvider::~GpodderProvider() 0118 { 0119 delete m_timerGeneratePlayAction; 0120 delete m_timerSynchronizeStatus; 0121 delete m_timerSynchronizeSubscriptions; 0122 0123 //Save cached episode actions and Podcast changes, in order to 0124 //upload them to gpodder.net in the next 0125 saveCachedEpisodeActions(); 0126 saveCachedPodcastsChanges(); 0127 0128 m_uploadEpisodeStatusMap.clear(); 0129 m_episodeStatusMap.clear(); 0130 m_redirectionUrlMap.clear(); 0131 0132 m_channels.clear(); 0133 } 0134 0135 bool 0136 GpodderProvider::possiblyContainsTrack( const QUrl &url ) const 0137 { 0138 DEBUG_BLOCK 0139 0140 foreach( PodcastChannelPtr ptr, m_channels ) 0141 { 0142 foreach( PodcastEpisodePtr episode, ptr->episodes() ) 0143 { 0144 if( episode->uidUrl() == url.url() ) 0145 return true; 0146 } 0147 } 0148 0149 return false; 0150 } 0151 0152 Meta::TrackPtr 0153 GpodderProvider::trackForUrl( const QUrl &url ) 0154 { 0155 DEBUG_BLOCK 0156 0157 if( url.isEmpty() ) 0158 return Meta::TrackPtr(); 0159 0160 foreach( PodcastChannelPtr podcast, m_channels ) 0161 { 0162 foreach( PodcastEpisodePtr episode, podcast->episodes() ) 0163 { 0164 if( episode->uidUrl() == url.url() ) 0165 { 0166 return Meta::TrackPtr::dynamicCast( episode ); 0167 } 0168 } 0169 } 0170 0171 return Meta::TrackPtr(); 0172 } 0173 0174 PodcastEpisodePtr 0175 GpodderProvider::episodeForGuid( const QString &guid ) 0176 { 0177 foreach( PodcastChannelPtr ptr, m_channels ) 0178 { 0179 foreach( PodcastEpisodePtr episode, ptr->episodes() ) 0180 { 0181 if( episode->guid() == guid ) 0182 return episode; 0183 } 0184 } 0185 0186 return PodcastEpisodePtr(); 0187 } 0188 0189 void 0190 GpodderProvider::addPodcast( const QUrl &url ) 0191 { 0192 Q_UNUSED( url ) 0193 } 0194 0195 Playlists::PlaylistPtr 0196 GpodderProvider::addPlaylist( Playlists::PlaylistPtr playlist ) 0197 { 0198 DEBUG_BLOCK 0199 0200 PodcastChannelPtr channel = PodcastChannelPtr::dynamicCast( playlist ); 0201 if( channel.isNull() ) 0202 return Playlists::PlaylistPtr(); 0203 0204 //This function is executed every time a new channel is found on gpodder.net 0205 PodcastChannelPtr master; 0206 PodcastChannelPtr slave; 0207 0208 foreach( PodcastChannelPtr tempChannel, 0209 The::playlistManager()->defaultPodcasts()->channels() ) 0210 if( tempChannel->url() == channel->url() ) 0211 master = tempChannel; 0212 0213 foreach( PodcastChannelPtr tempChannel, this->channels() ) 0214 if( tempChannel->url() == channel->url() ) 0215 slave = tempChannel; 0216 0217 if( !master ) 0218 master = The::playlistManager()->defaultPodcasts()->addChannel( channel ); 0219 0220 if( !slave ) 0221 { 0222 slave = this->addChannel( master ); 0223 0224 //If playlist is not a GpodderPodcastChannelPtr then we must subscribe 0225 //it in gpodder.net 0226 if( !GpodderPodcastChannelPtr::dynamicCast( playlist ) ) 0227 { 0228 //The service will try to subscribe this podcast in gpodder.net in 0229 //the next synchronization 0230 QUrl url = QUrl( slave->url().url() ); 0231 m_removeList.removeAll( url ); 0232 m_addList << url; 0233 } 0234 } 0235 0236 //Create a playlist synchronization between master and slave 0237 The::playlistManager()->setupSync( Playlists::PlaylistPtr::dynamicCast( master ), 0238 Playlists::PlaylistPtr::dynamicCast( slave ) 0239 ); 0240 0241 return Playlists::PlaylistPtr::dynamicCast( slave ); 0242 } 0243 0244 0245 PodcastChannelPtr 0246 GpodderProvider::addChannel( const PodcastChannelPtr &channel ) 0247 { 0248 DEBUG_BLOCK 0249 0250 GpodderPodcastChannelPtr gpodderChannel( new GpodderPodcastChannel( this, channel ) ); 0251 0252 m_channels << PodcastChannelPtr::dynamicCast( gpodderChannel ); 0253 0254 emit playlistAdded( Playlists::PlaylistPtr::dynamicCast( gpodderChannel ) ); 0255 0256 return PodcastChannelPtr::dynamicCast( gpodderChannel ); 0257 } 0258 0259 PodcastEpisodePtr 0260 GpodderProvider::addEpisode( PodcastEpisodePtr episode ) 0261 { 0262 if( episode.isNull() ) 0263 return PodcastEpisodePtr(); 0264 0265 if( episode->channel().isNull() ) 0266 { 0267 debug() << "channel is null"; 0268 0269 return PodcastEpisodePtr(); 0270 } 0271 0272 return episode; 0273 } 0274 0275 PodcastChannelList 0276 GpodderProvider::channels() 0277 { 0278 DEBUG_BLOCK 0279 0280 PodcastChannelList list; 0281 0282 foreach( PodcastChannelPtr channel, m_channels ) 0283 list << PodcastChannelPtr::dynamicCast( channel ); 0284 0285 return list; 0286 } 0287 0288 QString 0289 GpodderProvider::prettyName() const 0290 { 0291 return i18n( "Gpodder Podcasts" ); 0292 } 0293 0294 QIcon 0295 GpodderProvider::icon() const 0296 { 0297 return QIcon::fromTheme( "view-services-gpodder-amarok" ); 0298 } 0299 0300 Playlists::PlaylistList 0301 GpodderProvider::playlists() 0302 { 0303 Playlists::PlaylistList playlists; 0304 0305 foreach( PodcastChannelPtr channel, m_channels ) 0306 playlists << Playlists::PlaylistPtr::staticCast( channel ); 0307 0308 return playlists; 0309 } 0310 0311 void 0312 GpodderProvider::completePodcastDownloads() 0313 { 0314 } 0315 0316 void 0317 GpodderProvider::removeChannel( const QUrl &url ) 0318 { 0319 for( int i = 0; i < m_channels.size(); i++ ) 0320 { 0321 if( m_channels.at( i )->url() == url ) 0322 { 0323 PodcastChannelPtr channel = m_channels.at( i ); 0324 QUrl url = QUrl( channel->url().url() ); 0325 0326 m_channels.removeAll( channel ); 0327 m_episodeStatusMap.remove( url ); 0328 m_uploadEpisodeStatusMap.remove( url ); 0329 m_addList.removeAll( url ); 0330 0331 emit playlistRemoved( 0332 Playlists::PlaylistPtr::dynamicCast( channel ) ); 0333 0334 return; 0335 } 0336 } 0337 } 0338 0339 QActionList 0340 GpodderProvider::channelActions( PodcastChannelList channels ) 0341 { 0342 QActionList actions; 0343 if( channels.isEmpty() ) 0344 return actions; 0345 0346 if( m_removeAction == nullptr ) 0347 { 0348 m_removeAction = new QAction( QIcon::fromTheme( "edit-delete" ), 0349 i18n( "&Delete Channel and Episodes" ), this ); 0350 m_removeAction->setProperty( "popupdropper_svg_id", "delete" ); 0351 connect( m_removeAction, SIGNAL(triggered()), SLOT(slotRemoveChannels()) ); 0352 } 0353 //Set the episode list as data that we'll retrieve in the slot 0354 m_removeAction->setData( QVariant::fromValue( channels ) ); 0355 actions << m_removeAction; 0356 0357 return actions; 0358 } 0359 0360 QActionList 0361 GpodderProvider::playlistActions( const Playlists::PlaylistList &playlists ) 0362 { 0363 PodcastChannelList channels; 0364 foreach( const Playlists::PlaylistPtr &playlist, playlists ) 0365 { 0366 PodcastChannelPtr channel = PodcastChannelPtr::dynamicCast( playlist ); 0367 if( channel ) 0368 channels << channel; 0369 } 0370 0371 return channelActions( channels ); 0372 } 0373 0374 void 0375 GpodderProvider::slotRemoveChannels() 0376 { 0377 DEBUG_BLOCK 0378 0379 QAction *action = qobject_cast<QAction *>( QObject::sender() ); 0380 0381 if( action == nullptr ) 0382 return; 0383 0384 PodcastChannelList channels = action->data().value<PodcastChannelList>(); 0385 action->setData( QVariant() ); //Clear data 0386 0387 foreach( PodcastChannelPtr channel, channels ) 0388 { 0389 removeChannel( channel->url() ); 0390 0391 //The service will try to unsubscribe this podcast from gpodder.net 0392 //in the next synchronization 0393 m_removeList << channel->url(); 0394 } 0395 } 0396 0397 void 0398 GpodderProvider::slotSyncPlaylistAdded( Playlists::PlaylistPtr playlist ) 0399 { 0400 PodcastChannelPtr channel = Podcasts::PodcastChannelPtr::dynamicCast( playlist ); 0401 //If the new channel already exist in gpodder channels, then 0402 //we don't have to add it to gpodder.net again 0403 foreach( PodcastChannelPtr tempChannel, m_channels ) 0404 if( channel->url() == tempChannel->url() ) 0405 return; 0406 0407 addPlaylist( playlist ); 0408 m_timerSynchronizeSubscriptions->start( 60000 ); 0409 } 0410 0411 void 0412 GpodderProvider::slotSyncPlaylistRemoved( Playlists::PlaylistPtr playlist ) 0413 { 0414 Podcasts::PodcastChannelPtr channel = Podcasts::PodcastChannelPtr::dynamicCast( playlist ); 0415 //If gpodder channels doesn't contains the removed channel from default 0416 //podcast provider, then we don't have to remove it from gpodder.net 0417 foreach( PodcastChannelPtr tempChannel, m_channels ) 0418 if( channel->url() == tempChannel->url() ) 0419 { 0420 removeChannel( tempChannel->url() ); 0421 0422 //The service will try to unsubscribe this podcast from gpodder.net 0423 //in the next synchronization 0424 m_removeList << tempChannel->url(); 0425 m_timerSynchronizeSubscriptions->start( 60000 ); 0426 return; 0427 } 0428 } 0429 0430 qulonglong 0431 GpodderProvider::subscriptionTimestamp() 0432 { 0433 KConfigGroup config = Amarok::config( GpodderServiceConfig::configSectionName() ); 0434 return config.readEntry( "subscriptionTimestamp", 0 ); 0435 } 0436 0437 void 0438 GpodderProvider::setSubscriptionTimestamp( qulonglong newTimestamp ) 0439 { 0440 KConfigGroup config = Amarok::config( GpodderServiceConfig::configSectionName() ); 0441 config.writeEntry( "subscriptionTimestamp", newTimestamp ); 0442 } 0443 0444 void GpodderProvider::synchronizeStatus() 0445 { 0446 DEBUG_BLOCK 0447 0448 debug() << "new episodes status: " << m_uploadEpisodeStatusMap.size(); 0449 0450 if( !QNetworkConfigurationManager().isOnline() ) 0451 return; 0452 0453 if( !m_uploadEpisodeStatusMap.isEmpty() ) 0454 { 0455 m_episodeActionsResult = 0456 m_apiRequest->uploadEpisodeActions( m_username, 0457 m_uploadEpisodeStatusMap.values() ); 0458 0459 //Only clear m_episodeStatusList if the synchronization with gpodder.net really worked 0460 connect( m_episodeActionsResult.data(), SIGNAL(finished()), 0461 SLOT(slotSuccessfulStatusSynchronisation()) ); 0462 connect( m_episodeActionsResult.data(), 0463 SIGNAL(requestError(QNetworkReply::NetworkError)), 0464 SLOT(synchronizeStatusRequestError(QNetworkReply::NetworkError)) ); 0465 connect( m_episodeActionsResult.data(), SIGNAL(parseError()), 0466 SLOT(synchronizeStatusParseError()) ); 0467 0468 Amarok::Logger::shortMessage( i18n( "Trying to synchronize statuses with gpodder.net" ) ); 0469 } 0470 else 0471 m_timerSynchronizeStatus->stop(); 0472 } 0473 0474 void GpodderProvider::slotSuccessfulStatusSynchronisation() 0475 { 0476 DEBUG_BLOCK 0477 0478 m_timestampStatus = QDateTime::currentMSecsSinceEpoch(); 0479 0480 m_uploadEpisodeStatusMap.clear(); 0481 0482 //In addition, the server MUST send any URLs that have been rewritten (sanitized, see bug:747) 0483 //as a list of tuples with the key "update_urls". The client SHOULD parse this list and update 0484 //the local subscription list accordingly (the server only sanitizes the URL, so the semantic 0485 //"content" should stay the same and therefore the client can simply update the URL value 0486 //locally and use it for future updates 0487 updateLocalPodcasts( m_episodeActionsResult->updateUrlsList() ); 0488 } 0489 0490 void GpodderProvider::synchronizeStatusParseError() 0491 { 0492 DEBUG_BLOCK 0493 0494 QTimer::singleShot( 20000, this, SLOT(timerSynchronizeStatus()) ); 0495 0496 debug() << "synchronizeStatus [Status Synchronization] - Parse error"; 0497 } 0498 0499 void GpodderProvider::synchronizeStatusRequestError(QNetworkReply::NetworkError error) 0500 { 0501 DEBUG_BLOCK 0502 0503 QTimer::singleShot( 20000, this, SLOT(timerSynchronizeStatus()) ); 0504 0505 debug() << "synchronizeStatus [Status Synchronization] - Request error nr.: " << error; 0506 } 0507 0508 void 0509 GpodderProvider::synchronizeSubscriptions() 0510 { 0511 DEBUG_BLOCK 0512 0513 debug() << "add: " << m_addList.size(); 0514 debug() << "remove: " << m_removeList.size(); 0515 0516 if( !QNetworkConfigurationManager().isOnline() ) 0517 return; 0518 0519 if( !m_removeList.isEmpty() || !m_addList.isEmpty() ) 0520 { 0521 m_addRemoveResult = 0522 m_apiRequest->addRemoveSubscriptions( m_username, m_deviceName, m_addList, m_removeList ); 0523 0524 //Only clear m_addList and m_removeList if the synchronization with gpodder.net really worked 0525 connect( m_addRemoveResult.data(), SIGNAL(finished()), this, 0526 SLOT(slotSuccessfulSubscriptionSynchronisation()) ); 0527 0528 Amarok::Logger::shortMessage( i18n( "Trying to synchronize subscriptions with gpodder.net" ) ); 0529 } 0530 else 0531 m_timerSynchronizeSubscriptions->stop(); 0532 } 0533 0534 void 0535 GpodderProvider::slotSuccessfulSubscriptionSynchronisation() 0536 { 0537 DEBUG_BLOCK 0538 0539 m_timestampSubscription = QDateTime::currentMSecsSinceEpoch(); 0540 setSubscriptionTimestamp( m_timestampSubscription ); 0541 0542 m_addList.clear(); 0543 m_removeList.clear(); 0544 0545 //In addition, the server MUST send any URLs that have been rewritten (sanitized, see bug:747) 0546 //as a list of tuples with the key "update_urls". The client SHOULD parse this list and update 0547 //the local subscription list accordingly (the server only sanitizes the URL, so the semantic 0548 //"content" should stay the same and therefore the client can simply update the URL value 0549 //locally and use it for future updates 0550 updateLocalPodcasts( m_addRemoveResult->updateUrlsList() ); 0551 } 0552 0553 void 0554 GpodderProvider::slotTrackChanged( Meta::TrackPtr track ) 0555 { 0556 m_trackToSyncStatus = nullptr; 0557 0558 if( track != Meta::TrackPtr( nullptr ) ) 0559 { 0560 //If the episode is from one of the gpodder subscribed podcasts, then we must keep looking it 0561 if( ( this->possiblyContainsTrack( QUrl( track->uidUrl() ) ) ) ) 0562 { 0563 m_trackToSyncStatus = track; 0564 0565 QTimer::singleShot( 10000, this, SLOT(timerPrepareToSyncPodcastStatus()) ); 0566 0567 //A bookmark will be created if we have a play status available, 0568 //for current track, at m_episodeStatusMap 0569 createPlayStatusBookmark(); 0570 0571 return; 0572 } 0573 } 0574 0575 m_timerGeneratePlayAction->stop(); 0576 //EpisodeActions should be sent when the user clicks 0577 //stops and doesn't resume listening in e.g. 1 minute 0578 //Or when the user is not listening a podcast in e.g. 1 minute 0579 m_timerSynchronizeStatus->start( 60000 ); 0580 } 0581 0582 void 0583 GpodderProvider::slotTrackPositionChanged( qint64 position, bool userSeek ) 0584 { 0585 Q_UNUSED( position ) 0586 0587 //If the current track is in one of the subscribed gpodder channels and it's position 0588 //is not at the beginning of the track, then we probably should sync it status. 0589 if( m_trackToSyncStatus ) 0590 { 0591 if( userSeek ) 0592 { 0593 //Test if this track still playing after 10 seconds to avoid accidentally user changes 0594 QTimer::singleShot( 10000, this, SLOT(timerPrepareToSyncPodcastStatus()) ); 0595 } 0596 } 0597 } 0598 0599 void GpodderProvider::slotPaused() 0600 { 0601 m_timerGeneratePlayAction->stop(); 0602 //EpisodeActions should be sent when the user clicks pause 0603 //or stop and doesn't resume listening in e.g. 1 minute 0604 m_timerSynchronizeStatus->start( 60000 ); 0605 } 0606 0607 void 0608 GpodderProvider::timerSynchronizeSubscriptions() 0609 { 0610 synchronizeSubscriptions(); 0611 } 0612 0613 void 0614 GpodderProvider::timerSynchronizeStatus() 0615 { 0616 synchronizeStatus(); 0617 } 0618 0619 void 0620 GpodderProvider::timerPrepareToSyncPodcastStatus() 0621 { 0622 if( The::engineController()->currentTrack() == m_trackToSyncStatus ) 0623 { 0624 EpisodeActionPtr tempEpisodeAction; 0625 PodcastEpisodePtr tempEpisode = PodcastEpisodePtr::dynamicCast( m_trackToSyncStatus ); 0626 0627 if( tempEpisode ) 0628 { 0629 qulonglong positionSeconds = The::engineController()->trackPosition(); 0630 qulonglong lengthSeconds = The::engineController()->trackLength() / 1000; 0631 0632 QString podcastUrl = resolvedPodcastUrl( tempEpisode ).url(); 0633 0634 tempEpisodeAction = EpisodeActionPtr( 0635 new EpisodeAction( QUrl( podcastUrl ), 0636 QUrl( tempEpisode->uidUrl() ), 0637 m_deviceName, 0638 EpisodeAction::Play, 0639 QDateTime::currentMSecsSinceEpoch(), 0640 1, 0641 positionSeconds + 1, 0642 lengthSeconds 0643 ) ); 0644 0645 //Any previous episodeAction, from the same podcast, will be replaced 0646 m_uploadEpisodeStatusMap.insert( QUrl( tempEpisode->uidUrl() ), tempEpisodeAction ); 0647 } 0648 0649 //Starts to generate EpisodeActions 0650 m_timerGeneratePlayAction->start( 30000 ); 0651 } 0652 } 0653 0654 void GpodderProvider::timerGenerateEpisodeAction() 0655 { 0656 //Create and update episode actions 0657 if( The::engineController()->currentTrack() == m_trackToSyncStatus ) 0658 { 0659 EpisodeActionPtr tempEpisodeAction; 0660 PodcastEpisodePtr tempEpisode = PodcastEpisodePtr::dynamicCast( m_trackToSyncStatus ); 0661 0662 if( tempEpisode ) 0663 { 0664 qulonglong positionSeconds = The::engineController()->trackPosition(); 0665 qulonglong lengthSeconds = The::engineController()->trackLength() / 1000; 0666 0667 QString podcastUrl = resolvedPodcastUrl( tempEpisode ).url(); 0668 0669 tempEpisodeAction = EpisodeActionPtr( 0670 new EpisodeAction( QUrl( podcastUrl ), 0671 QUrl( tempEpisode->uidUrl() ), 0672 m_deviceName, 0673 EpisodeAction::Play, 0674 QDateTime::currentMSecsSinceEpoch(), 0675 1, 0676 positionSeconds + 1, 0677 lengthSeconds 0678 ) ); 0679 0680 //Any previous episodeAction, from the same podcast, will be replaced 0681 m_uploadEpisodeStatusMap.insert( QUrl( tempEpisode->uidUrl() ), tempEpisodeAction ); 0682 //Make local podcasts aware of new episodeActions 0683 m_episodeStatusMap.insert( QUrl( tempEpisode->uidUrl() ), tempEpisodeAction ); 0684 } 0685 } 0686 } 0687 0688 void 0689 GpodderProvider::requestDeviceUpdates() 0690 { 0691 DEBUG_BLOCK 0692 0693 if( !QNetworkConfigurationManager().isOnline() ) 0694 { 0695 QTimer::singleShot( 10000, this, SLOT(requestDeviceUpdates()) ); 0696 return; 0697 } 0698 0699 m_deviceUpdatesResult = 0700 m_apiRequest->deviceUpdates( m_username, 0701 m_deviceName, 0702 0 ); 0703 0704 connect( m_deviceUpdatesResult.data(), SIGNAL(finished()), 0705 SLOT(deviceUpdatesFinished()) ); 0706 connect( m_deviceUpdatesResult.data(), 0707 SIGNAL(requestError(QNetworkReply::NetworkError)), 0708 SLOT(deviceUpdatesRequestError(QNetworkReply::NetworkError)) ); 0709 connect( m_deviceUpdatesResult.data(), SIGNAL(parseError()), 0710 SLOT(deviceUpdatesParseError()) ); 0711 } 0712 0713 void 0714 GpodderProvider::deviceUpdatesFinished() 0715 { 0716 DEBUG_BLOCK 0717 0718 debug() << "DeviceUpdate timestamp: " << m_deviceUpdatesResult->timestamp(); 0719 0720 //Channels to subscribe locally 0721 foreach( mygpo::PodcastPtr podcast, m_deviceUpdatesResult->addList() ) 0722 { 0723 debug() << "Subscribing GPO channel: " << podcast->title() << ": " << podcast->url(); 0724 0725 GpodderPodcastChannelPtr channel = 0726 GpodderPodcastChannelPtr( new GpodderPodcastChannel( this, podcast ) ); 0727 0728 //First we need to resolve redirection url's if there is any 0729 requestUrlResolve( channel ); 0730 } 0731 0732 //Request the last episode status for every episode in gpodder.net 0733 //subscribed podcasts 0734 QTimer::singleShot( 1000, this, SLOT(requestEpisodeActionsInCascade()) ); 0735 0736 //Only after all subscription changes are committed should we save the timestamp 0737 m_timestampSubscription = m_deviceUpdatesResult->timestamp(); 0738 setSubscriptionTimestamp( m_timestampSubscription ); 0739 } 0740 0741 void 0742 GpodderProvider::continueDeviceUpdatesFinished() 0743 { 0744 foreach( GpodderPodcastChannelPtr channel, m_resolvedChannelsToBeAdded ) 0745 { 0746 m_channelsToRequestActions.enqueue( channel->url() ); 0747 0748 PodcastChannelPtr master; 0749 PodcastChannelPtr slave; 0750 0751 slave = this->addChannel( PodcastChannelPtr::dynamicCast( channel ) ); 0752 0753 foreach( PodcastChannelPtr tempChannel, The::playlistManager()->defaultPodcasts()->channels() ) 0754 if( tempChannel->url() == channel->url() ) 0755 master = tempChannel; 0756 0757 if( !master ) 0758 master = The::playlistManager()->defaultPodcasts()->addChannel( slave ); 0759 0760 //Create a playlist synchronization between master and slave 0761 The::playlistManager()->setupSync( Playlists::PlaylistPtr::dynamicCast( master ), 0762 Playlists::PlaylistPtr::dynamicCast( slave ) 0763 ); 0764 } 0765 0766 m_resolvedChannelsToBeAdded.clear(); 0767 } 0768 0769 void 0770 GpodderProvider::deviceUpdatesParseError() 0771 { 0772 DEBUG_BLOCK 0773 0774 QTimer::singleShot( 10000, this, SLOT(requestDeviceUpdates()) ); 0775 0776 debug() << "deviceUpdates [Subscription Synchronization] - Parse error"; 0777 Amarok::Logger::shortMessage( i18n( "GPodder Service failed to get data from the server. Will retry in 10 seconds..." ) ); 0778 } 0779 0780 void 0781 GpodderProvider::deviceUpdatesRequestError( QNetworkReply::NetworkError error ) 0782 { 0783 DEBUG_BLOCK 0784 0785 QTimer::singleShot( 10000, this, SLOT(requestDeviceUpdates()) ); 0786 0787 debug() << "deviceUpdates [Subscription Synchronization] - Request error nr.: " << error; 0788 Amarok::Logger::shortMessage( i18n( "GPodder Service failed to get data from the server. Will retry in 10 seconds..." ) ); 0789 } 0790 0791 void 0792 GpodderProvider::requestEpisodeActionsInCascade() 0793 { 0794 DEBUG_BLOCK 0795 0796 if( !QNetworkConfigurationManager().isOnline() ) 0797 { 0798 QTimer::singleShot( 10000, this, SLOT(requestEpisodeActionsInCascade()) ); 0799 return; 0800 } 0801 0802 //This function will download all episode actions for 0803 //every podcast contained in m_channelsToRequestActions 0804 if( !m_channelsToRequestActions.isEmpty() ) 0805 { 0806 QUrl url = m_channelsToRequestActions.head(); 0807 m_episodeActionListResult = m_apiRequest->episodeActionsByPodcast( m_username, url.toString(), true ); 0808 debug() << "Requesting actions for " << url.toString(); 0809 connect( m_episodeActionListResult.data(), SIGNAL(finished()), 0810 SLOT(episodeActionsInCascadeFinished()) ); 0811 connect( m_episodeActionListResult.data(), 0812 SIGNAL(requestError(QNetworkReply::NetworkError)), 0813 SLOT(episodeActionsInCascadeRequestError(QNetworkReply::NetworkError)) ); 0814 connect( m_episodeActionListResult.data(), SIGNAL(parseError()), 0815 SLOT(episodeActionsInCascadeParseError()) ); 0816 } 0817 else 0818 { 0819 //We should try to upload cached EpisodeActions to gpodder.net 0820 synchronizeStatus(); 0821 } 0822 } 0823 0824 void 0825 GpodderProvider::episodeActionsInCascadeFinished() 0826 { 0827 DEBUG_BLOCK 0828 0829 m_timestampStatus = m_episodeActionListResult->timestamp(); 0830 0831 foreach( EpisodeActionPtr tempEpisodeAction, m_episodeActionListResult->list() ) 0832 { 0833 if( tempEpisodeAction->action() == EpisodeAction::Play ) 0834 { 0835 debug() << QString( "Adding a new play status to episode: %1" ) 0836 .arg( tempEpisodeAction->episodeUrl().toString() ); 0837 0838 m_episodeStatusMap.insert( tempEpisodeAction->episodeUrl(), tempEpisodeAction ); 0839 0840 //A bookmark will be created if we have a play status available, 0841 //for current track, at m_episodeStatusMap 0842 createPlayStatusBookmark(); 0843 } 0844 else 0845 { 0846 PodcastChannelPtr channel; 0847 PodcastEpisodePtr episode; 0848 0849 foreach( PodcastChannelPtr tempChannel, m_channels ) 0850 if( tempChannel->url() == tempEpisodeAction->podcastUrl() ) 0851 { 0852 channel = tempChannel; 0853 0854 foreach( PodcastEpisodePtr tempEpisode, channel->episodes() ) 0855 if( tempEpisode->uidUrl() == tempEpisodeAction->episodeUrl().toString() ) 0856 episode = tempEpisode; 0857 } 0858 0859 if( channel && episode ) 0860 { 0861 if( tempEpisodeAction->action() == EpisodeAction::New ) 0862 { 0863 if( !episode ) 0864 { 0865 debug() << QString( "New episode to be added found: %1" ) 0866 .arg( tempEpisodeAction->episodeUrl().toString() ); 0867 0868 PodcastEpisodePtr tempEpisode; 0869 tempEpisode = PodcastEpisodePtr( new PodcastEpisode() ); 0870 tempEpisode->setUidUrl( tempEpisodeAction->episodeUrl() ); 0871 tempEpisode->setChannel( PodcastChannelPtr::dynamicCast( channel ) ); 0872 0873 channel->addEpisode( tempEpisode ); 0874 } 0875 else 0876 { 0877 debug() << QString( "Marking an existent episode as new: %1" ) 0878 .arg( tempEpisodeAction->episodeUrl().toString() ); 0879 0880 episode->setNew( true ); 0881 } 0882 } 0883 else if( tempEpisodeAction->action() == EpisodeAction::Download ) 0884 { 0885 debug() << QString( "Adding a new download status to episode: %1" ) 0886 .arg( tempEpisodeAction->episodeUrl().toString() ); 0887 0888 } 0889 else if( tempEpisodeAction->action() == EpisodeAction::Delete ) 0890 { 0891 debug() << QString( "Adding a new delete status to episode: %1" ) 0892 .arg( tempEpisodeAction->episodeUrl().toString() ); 0893 0894 } 0895 0896 m_episodeStatusMap.insert( tempEpisodeAction->episodeUrl(), tempEpisodeAction ); 0897 } 0898 else 0899 { 0900 //For some reason the podcast and/or episode for this action 0901 //wasn't found 0902 debug() << QString( "Episode and/or channel not found" ); 0903 } 0904 0905 } 0906 } 0907 0908 //We must remove this podcast url and continue with the others 0909 m_channelsToRequestActions.dequeue(); 0910 0911 QTimer::singleShot( 100, this, SLOT(requestEpisodeActionsInCascade()) ); 0912 } 0913 0914 void 0915 GpodderProvider::episodeActionsInCascadeParseError() 0916 { 0917 DEBUG_BLOCK 0918 0919 QTimer::singleShot( 10000, this, SLOT(requestEpisodeActionsInCascade()) ); 0920 //If we fail to get EpisodeActions for this channel then we must put it 0921 //at the end of the list. In order to be synced later on. 0922 m_channelsToRequestActions.enqueue( m_channelsToRequestActions.dequeue() ); 0923 0924 debug() << "episodeActionsInCascade [Status Synchronization] - Parse Error"; 0925 } 0926 0927 void 0928 GpodderProvider::episodeActionsInCascadeRequestError( QNetworkReply::NetworkError error ) 0929 { 0930 DEBUG_BLOCK 0931 0932 QTimer::singleShot( 10000, this, SLOT(requestEpisodeActionsInCascade()) ); 0933 //If we fail to get EpisodeActions for this channel then we must put it 0934 //at the end of the list. In order to be synced later on. 0935 m_channelsToRequestActions.enqueue( m_channelsToRequestActions.dequeue() ); 0936 0937 debug() << "episodeActionsInCascade [Status Synchronization] - Request error nr.: " << error; 0938 } 0939 0940 void 0941 GpodderProvider::updateLocalPodcasts( const QList<QPair<QUrl,QUrl> > updatedUrls ) 0942 { 0943 QList< QPair<QUrl,QUrl> >::const_iterator tempUpdatedUrl = updatedUrls.begin(); 0944 0945 for(; tempUpdatedUrl != updatedUrls.end(); ++tempUpdatedUrl ) 0946 { 0947 foreach( PodcastChannelPtr tempChannel, The::playlistManager()->defaultPodcasts()->channels() ) 0948 { 0949 if( tempChannel->url() == (*tempUpdatedUrl).first ) 0950 tempChannel->setUrl( (*tempUpdatedUrl).second ); 0951 } 0952 0953 foreach( PodcastChannelPtr tempGpodderChannel, m_channels ) 0954 { 0955 if( tempGpodderChannel->url() == (*tempUpdatedUrl).first ) 0956 tempGpodderChannel->setUrl( (*tempUpdatedUrl).second ); 0957 } 0958 } 0959 } 0960 0961 void 0962 GpodderProvider::createPlayStatusBookmark() 0963 { 0964 Meta::TrackPtr track = The::engineController()->currentTrack(); 0965 0966 if( track ) 0967 { 0968 EpisodeActionPtr tempEpisodeAction = m_episodeStatusMap.value( QUrl( track->uidUrl() ) ); 0969 0970 //Create an AutoTimecode at the last position position, so the user always know where he stopped to listen 0971 if( tempEpisodeAction && ( tempEpisodeAction->action() == EpisodeAction::Play ) ) 0972 { 0973 if( track && track->has<Capabilities::TimecodeWriteCapability>() ) 0974 { 0975 QScopedPointer<Capabilities::TimecodeWriteCapability> tcw( track->create<Capabilities::TimecodeWriteCapability>() ); 0976 qint64 positionMiliSeconds = tempEpisodeAction->position() * 1000; 0977 0978 tcw->writeAutoTimecode( positionMiliSeconds ); 0979 } 0980 } 0981 } 0982 } 0983 0984 void 0985 GpodderProvider::requestUrlResolve( Podcasts::GpodderPodcastChannelPtr channel ) 0986 { 0987 if( !channel ) 0988 return; 0989 0990 m_resolveUrlJob = KIO::get( channel->url(), KIO::Reload, KIO::HideProgressInfo ); 0991 0992 connect( m_resolveUrlJob, &KJob::result, 0993 this, &GpodderProvider::urlResolveFinished ); 0994 connect( m_resolveUrlJob, 0995 &KIO::TransferJob::permanentRedirection, 0996 this, &GpodderProvider::urlResolvePermanentRedirection ); 0997 0998 m_resolvedPodcasts.insert( m_resolveUrlJob, channel ); 0999 } 1000 1001 void 1002 GpodderProvider::urlResolvePermanentRedirection( KIO::Job *job, const QUrl &fromUrl, const QUrl &toUrl ) 1003 { 1004 DEBUG_BLOCK 1005 1006 KIO::TransferJob *transferJob = dynamic_cast<KIO::TransferJob *>( job ); 1007 GpodderPodcastChannelPtr channel = m_resolvedPodcasts.value( transferJob ); 1008 1009 m_redirectionUrlMap.insert( toUrl, channel->url() ); 1010 1011 channel->setUrl( toUrl ); 1012 1013 debug() << fromUrl.url() << " was redirected to " << toUrl.url(); 1014 1015 requestUrlResolve( channel ); 1016 } 1017 1018 void 1019 GpodderProvider::urlResolveFinished( KJob * job ) 1020 { 1021 KIO::TransferJob *transferJob = dynamic_cast<KIO::TransferJob *>( job ); 1022 1023 if( transferJob && ( !( transferJob->isErrorPage() || job->error() ) ) ) 1024 { 1025 m_resolvedChannelsToBeAdded.push_back( m_resolvedPodcasts.value( transferJob ) ); 1026 m_resolvedPodcasts.remove( transferJob ); 1027 } 1028 else 1029 requestUrlResolve( m_resolvedPodcasts.value( transferJob ) ); 1030 1031 if( m_resolvedPodcasts.empty() ) 1032 continueDeviceUpdatesFinished(); 1033 1034 m_resolveUrlJob = nullptr; 1035 } 1036 1037 void GpodderProvider::slotEpisodeDownloaded( PodcastEpisodePtr episode ) 1038 { 1039 EpisodeActionPtr tempEpisodeAction; 1040 1041 QString podcastUrl = resolvedPodcastUrl( episode ).url(); 1042 1043 tempEpisodeAction = EpisodeActionPtr( 1044 new EpisodeAction( QUrl( podcastUrl ), 1045 QUrl( episode->uidUrl() ), 1046 m_deviceName, 1047 EpisodeAction::Download, 1048 QDateTime::currentMSecsSinceEpoch(), 1049 0, 1050 0, 1051 0 1052 ) ); 1053 1054 //Any previous episodeAction, from the same podcast, will be replaced 1055 m_uploadEpisodeStatusMap.insert( QUrl( episode->uidUrl() ), tempEpisodeAction ); 1056 1057 m_timerSynchronizeStatus->start( 60000 ); 1058 } 1059 1060 void GpodderProvider::slotEpisodeDeleted( PodcastEpisodePtr episode ) 1061 { 1062 EpisodeActionPtr tempEpisodeAction; 1063 1064 QString podcastUrl = resolvedPodcastUrl( episode ).url(); 1065 1066 tempEpisodeAction = EpisodeActionPtr( 1067 new EpisodeAction( QUrl( podcastUrl ), 1068 QUrl( episode->uidUrl() ), 1069 m_deviceName, 1070 EpisodeAction::Delete, 1071 QDateTime::currentMSecsSinceEpoch(), 1072 0, 1073 0, 1074 0 1075 ) ); 1076 1077 //Any previous episodeAction, from the same podcast, will be replaced 1078 m_uploadEpisodeStatusMap.insert( QUrl( episode->uidUrl() ), tempEpisodeAction ); 1079 1080 m_timerSynchronizeStatus->start( 60000 ); 1081 } 1082 1083 void GpodderProvider::slotEpisodeMarkedAsNew( PodcastEpisodePtr episode ) 1084 { 1085 EpisodeActionPtr tempEpisodeAction; 1086 1087 QString podcastUrl = resolvedPodcastUrl( episode ).url(); 1088 1089 tempEpisodeAction = EpisodeActionPtr( 1090 new EpisodeAction( QUrl( podcastUrl ), 1091 QUrl( episode->uidUrl() ), 1092 m_deviceName, 1093 EpisodeAction::New, 1094 QDateTime::currentMSecsSinceEpoch(), 1095 0, 1096 0, 1097 0 1098 ) ); 1099 1100 //Any previous episodeAction, from the same podcast, will be replaced 1101 m_uploadEpisodeStatusMap.insert( QUrl( episode->uidUrl() ), tempEpisodeAction ); 1102 1103 m_timerSynchronizeStatus->start( 60000 ); 1104 } 1105 1106 inline KConfigGroup 1107 GpodderProvider::gpodderActionsConfig() const 1108 { 1109 return Amarok::config( "GPodder Cached Episode Actions" ); 1110 } 1111 1112 void GpodderProvider::loadCachedEpisodeActions() 1113 { 1114 DEBUG_BLOCK 1115 1116 if( !gpodderActionsConfig().exists() ) 1117 return; 1118 1119 int action; 1120 bool validActionType; 1121 bool actionTypeConversion; 1122 qulonglong timestamp = 0; 1123 qulonglong started = 0; 1124 qulonglong position = 0; 1125 qulonglong total = 0; 1126 QStringList actionsDetails; 1127 EpisodeAction::ActionType actionType; 1128 1129 foreach( QString episodeUrl, gpodderActionsConfig().keyList() ) 1130 { 1131 actionsDetails.clear(); 1132 actionsDetails = gpodderActionsConfig().readEntry( episodeUrl ).split( ',' ); 1133 1134 if( actionsDetails.count() != 6 ) 1135 debug() << "There are less/more fields than expected."; 1136 else 1137 { 1138 action = actionsDetails[1].toInt( &actionTypeConversion ); 1139 1140 if( !actionTypeConversion ) 1141 debug() << "Failed to convert actionType field to int."; 1142 else 1143 { 1144 validActionType = true; 1145 timestamp = actionsDetails[2].toULongLong(); 1146 started = actionsDetails[3].toULongLong(); 1147 position = actionsDetails[4].toULongLong(); 1148 total = actionsDetails[5].toULongLong(); 1149 1150 switch( action ) 1151 { 1152 case 0: actionType = EpisodeAction::Download; break; 1153 case 1: actionType = EpisodeAction::Play; break; 1154 case 2: actionType = EpisodeAction::Delete; break; 1155 case 3: actionType = EpisodeAction::New; break; 1156 default: validActionType = false; break; 1157 } 1158 1159 //We can't create a EpisodeAction if action isn't a valid alternative 1160 if( !validActionType ) 1161 debug() << "Action isn't a valid alternative."; 1162 else 1163 { 1164 debug() << QString( "Loaded %1 action." ).arg( episodeUrl ); 1165 1166 EpisodeActionPtr tempEpisodeAction = EpisodeActionPtr( 1167 new EpisodeAction( QUrl( actionsDetails[0] ), 1168 QUrl( episodeUrl ), 1169 m_deviceName, 1170 actionType, 1171 timestamp, 1172 started, 1173 position, 1174 total 1175 ) ); 1176 1177 //Any previous episodeAction, from the same podcast, will be replaced 1178 m_uploadEpisodeStatusMap.insert( tempEpisodeAction->episodeUrl(), tempEpisodeAction ); 1179 m_episodeStatusMap.insert( tempEpisodeAction->episodeUrl(), tempEpisodeAction ); 1180 } 1181 } 1182 } 1183 } 1184 1185 //We should delete cached EpisodeActions, since we already loaded them 1186 gpodderActionsConfig().deleteGroup(); 1187 1188 synchronizeStatus(); 1189 } 1190 1191 void GpodderProvider::saveCachedEpisodeActions() 1192 { 1193 DEBUG_BLOCK 1194 1195 if( m_uploadEpisodeStatusMap.isEmpty() ) 1196 return; 1197 1198 int actionType; 1199 QList<QString> actionsDetails; 1200 1201 foreach( EpisodeActionPtr action, m_uploadEpisodeStatusMap.values() ) 1202 { 1203 actionsDetails.clear(); 1204 actionsDetails.append( action->podcastUrl().toString() ); 1205 1206 switch( action->action() ) 1207 { 1208 case EpisodeAction::Download: actionType = 0; break; 1209 case EpisodeAction::Play: actionType = 1; break; 1210 case EpisodeAction::Delete: actionType = 2; break; 1211 case EpisodeAction::New: actionType = 3; break; 1212 default: actionType = -1; break; 1213 } 1214 1215 actionsDetails.append( QString::number( actionType ) ); 1216 actionsDetails.append( QString::number( action->timestamp() ) ); 1217 actionsDetails.append( QString::number( action->started() ) ); 1218 actionsDetails.append( QString::number( action->position() ) ); 1219 actionsDetails.append( QString::number( action->total() ) ); 1220 1221 gpodderActionsConfig().writeEntry( action->episodeUrl().toString(), actionsDetails ); 1222 } 1223 } 1224 1225 inline KConfigGroup 1226 GpodderProvider::gpodderPodcastsConfig() const 1227 { 1228 return Amarok::config( "GPodder Cached Podcast Changes" ); 1229 } 1230 1231 void GpodderProvider::loadCachedPodcastsChanges() 1232 { 1233 DEBUG_BLOCK 1234 1235 if( !gpodderPodcastsConfig().exists() ) 1236 return; 1237 1238 QStringList podcastsUrlsToAdd; 1239 QStringList podcastsUrlsToRemove; 1240 1241 podcastsUrlsToAdd = gpodderPodcastsConfig().readEntry( "addList" ).split( ',' ); 1242 podcastsUrlsToRemove = gpodderPodcastsConfig().readEntry( "removeList" ).split( ',' ); 1243 1244 foreach( QString podcastUrl, podcastsUrlsToAdd ) 1245 { 1246 debug() << QString( "New channel to subscribe: %1" ).arg( podcastUrl ); 1247 1248 m_addList.append( QUrl( podcastUrl ) ); 1249 } 1250 1251 foreach( QString podcastUrl, podcastsUrlsToRemove ) 1252 { 1253 debug() << QString( "New channel to unsubscribe: %1 action." ).arg( podcastUrl ); 1254 1255 m_removeList.append( QUrl( podcastUrl ) ); 1256 } 1257 1258 //We should delete cached podcasts changes, since we already loaded them 1259 gpodderPodcastsConfig().deleteGroup(); 1260 1261 synchronizeSubscriptions(); 1262 } 1263 1264 void GpodderProvider::saveCachedPodcastsChanges() 1265 { 1266 DEBUG_BLOCK 1267 1268 if( !m_addList.isEmpty() ) 1269 { 1270 QStringList podcastUrlsToAdd; 1271 1272 foreach( QUrl podcastUrl, m_addList ) 1273 podcastUrlsToAdd.append( podcastUrl.toString() ); 1274 1275 gpodderPodcastsConfig().writeEntry( "addList", podcastUrlsToAdd ); 1276 } 1277 1278 if( !m_removeList.isEmpty() ) 1279 { 1280 QStringList podcastsUrlsToRemove; 1281 1282 foreach( QUrl podcastUrl, m_removeList ) 1283 podcastsUrlsToRemove.append( podcastUrl.toString() ); 1284 1285 gpodderPodcastsConfig().writeEntry( "removeList", podcastsUrlsToRemove ); 1286 } 1287 } 1288 1289 QUrl GpodderProvider::resolvedPodcastUrl( const PodcastEpisodePtr episode ) 1290 { 1291 QUrl podcastUrl = episode->channel()->url(); 1292 1293 if( m_redirectionUrlMap.contains( podcastUrl ) ) 1294 podcastUrl = m_redirectionUrlMap.value( podcastUrl ); 1295 1296 return podcastUrl; 1297 }