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 }