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

0001 /****************************************************************************************
0002  * Copyright (c) 2008 Bart Cerneels <bart.cerneels@kde.org>                             *
0003  *                                                                                      *
0004  * This program is free software; you can redistribute it and/or modify it under        *
0005  * the terms of the GNU General Public License as published by the Free Software        *
0006  * Foundation; either version 2 of the License, or (at your option) any later           *
0007  * version.                                                                             *
0008  *                                                                                      *
0009  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
0010  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
0011  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
0012  *                                                                                      *
0013  * You should have received a copy of the GNU General Public License along with         *
0014  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
0015  ****************************************************************************************/
0016 
0017 #include "SqlPodcastMeta.h"
0018 
0019 #include "amarokurls/BookmarkMetaActions.h"
0020 #include "amarokurls/PlayUrlRunner.h"
0021 #include "core/capabilities/ActionsCapability.h"
0022 #include <core/storage/SqlStorage.h>
0023 #include "core/meta/TrackEditor.h"
0024 #include "core/support/Debug.h"
0025 #include "core-impl/capabilities/timecode/TimecodeLoadCapability.h"
0026 #include "core-impl/capabilities/timecode/TimecodeWriteCapability.h"
0027 #include "core-impl/collections/support/CollectionManager.h"
0028 #include "core-impl/storage/StorageManager.h"
0029 #include "core-impl/meta/proxy/MetaProxy.h"
0030 #include "core-impl/meta/file/FileTrackProvider.h"
0031 #include "core-impl/podcasts/sql/SqlPodcastProvider.h"
0032 
0033 #include <QDate>
0034 #include <QFile>
0035 
0036 using namespace Podcasts;
0037 
0038 static FileTrackProvider myFileTrackProvider; // we need it to be available for lookups
0039 
0040 class TimecodeWriteCapabilityPodcastImpl : public Capabilities::TimecodeWriteCapability
0041 {
0042     public:
0043         TimecodeWriteCapabilityPodcastImpl( Podcasts::PodcastEpisode *episode )
0044             : Capabilities::TimecodeWriteCapability()
0045             , m_episode( episode )
0046         {}
0047 
0048     bool writeTimecode ( qint64 miliseconds ) override
0049     {
0050         DEBUG_BLOCK
0051         return Capabilities::TimecodeWriteCapability::writeTimecode( miliseconds,
0052                 Meta::TrackPtr::dynamicCast( m_episode ) );
0053     }
0054 
0055     bool writeAutoTimecode ( qint64 miliseconds ) override
0056     {
0057         DEBUG_BLOCK
0058         return Capabilities::TimecodeWriteCapability::writeAutoTimecode( miliseconds,
0059                 Meta::TrackPtr::dynamicCast( m_episode ) );
0060     }
0061 
0062     private:
0063         Podcasts::PodcastEpisodePtr m_episode;
0064 };
0065 
0066 class TimecodeLoadCapabilityPodcastImpl : public Capabilities::TimecodeLoadCapability
0067 {
0068     public:
0069         TimecodeLoadCapabilityPodcastImpl( Podcasts::PodcastEpisode *episode )
0070         : Capabilities::TimecodeLoadCapability()
0071         , m_episode( episode )
0072         {
0073             DEBUG_BLOCK
0074             debug() << "episode: " << m_episode->name();
0075         }
0076 
0077         bool hasTimecodes() override
0078         {
0079             if ( loadTimecodes().size() > 0 )
0080                 return true;
0081             return false;
0082         }
0083 
0084         BookmarkList loadTimecodes() override
0085         {
0086             DEBUG_BLOCK
0087             if ( m_episode && m_episode->playableUrl().isValid() )
0088             {
0089                 BookmarkList list = PlayUrlRunner::bookmarksFromUrl( m_episode->playableUrl() );
0090                 return list;
0091             }
0092             else
0093                 return BookmarkList();
0094         }
0095 
0096     private:
0097         Podcasts::PodcastEpisodePtr m_episode;
0098 };
0099 
0100 Meta::TrackList
0101 SqlPodcastEpisode::toTrackList( Podcasts::SqlPodcastEpisodeList episodes )
0102 {
0103     Meta::TrackList tracks;
0104     foreach( SqlPodcastEpisodePtr sqlEpisode, episodes )
0105         tracks << Meta::TrackPtr::dynamicCast( sqlEpisode );
0106 
0107     return tracks;
0108 }
0109 
0110 Podcasts::PodcastEpisodeList
0111 SqlPodcastEpisode::toPodcastEpisodeList( SqlPodcastEpisodeList episodes )
0112 {
0113     Podcasts::PodcastEpisodeList sqlEpisodes;
0114     foreach( SqlPodcastEpisodePtr sqlEpisode, episodes )
0115         sqlEpisodes << Podcasts::PodcastEpisodePtr::dynamicCast( sqlEpisode );
0116 
0117     return sqlEpisodes;
0118 }
0119 
0120 SqlPodcastEpisode::SqlPodcastEpisode( const QStringList &result, const SqlPodcastChannelPtr &sqlChannel )
0121     : Podcasts::PodcastEpisode( Podcasts::PodcastChannelPtr::staticCast( sqlChannel ) )
0122     , m_channel( sqlChannel )
0123 {
0124     auto sqlStorage = StorageManager::instance()->sqlStorage();
0125     QStringList::ConstIterator iter = result.constBegin();
0126     m_dbId = (*(iter++)).toInt();
0127     m_url = QUrl( *(iter++) );
0128     int channelId = (*(iter++)).toInt();
0129     Q_UNUSED( channelId );
0130     m_localUrl = QUrl( *(iter++) );
0131     m_guid = *(iter++);
0132     m_title = *(iter++);
0133     m_subtitle = *(iter++);
0134     m_sequenceNumber = (*(iter++)).toInt();
0135     m_description = *(iter++);
0136     m_mimeType = *(iter++);
0137     m_pubDate = QDateTime::fromString( *(iter++), Qt::ISODate );
0138     m_duration = (*(iter++)).toInt();
0139     m_fileSize = (*(iter++)).toInt();
0140     m_isNew = sqlStorage->boolTrue() == (*(iter++));
0141     m_isKeep = sqlStorage->boolTrue() == (*(iter++));
0142 
0143     Q_ASSERT_X( iter == result.constEnd(), "SqlPodcastEpisode( PodcastCollection*, QStringList )", "number of expected fields did not match number of actual fields" );
0144 
0145     setupLocalFile();
0146 }
0147 
0148 //TODO: why do PodcastMetaCommon and PodcastEpisode not have an appropriate copy constructor?
0149 SqlPodcastEpisode::SqlPodcastEpisode( Podcasts::PodcastEpisodePtr episode )
0150     : Podcasts::PodcastEpisode()
0151     , m_dbId( 0 )
0152     , m_isKeep( false )
0153 {
0154     m_channel = SqlPodcastChannelPtr::dynamicCast( episode->channel() );
0155 
0156     if( !m_channel && episode->channel() )
0157     {
0158         debug() << "BUG: creating SqlEpisode but not an sqlChannel!!!";
0159         debug() <<  episode->channel()->title();
0160         debug() <<  m_channel->title();
0161     }
0162 
0163     // PodcastMetaCommon
0164     m_title = episode->title();
0165     m_description = episode->description();
0166     m_keywords = episode->keywords();
0167     m_subtitle = episode->subtitle();
0168     m_summary = episode->summary();
0169     m_author = episode->author();
0170 
0171     // PodcastEpisode
0172     m_guid = episode->guid();
0173     m_url = QUrl( episode->uidUrl() );
0174     m_localUrl = episode->localUrl();
0175     m_mimeType = episode->mimeType();
0176     m_pubDate = episode->pubDate();
0177     m_duration = episode->duration();
0178     m_fileSize = episode->filesize();
0179     m_sequenceNumber = episode->sequenceNumber();
0180     m_isNew = episode->isNew();
0181 
0182     // The album, artist, composer, genre and year fields
0183     // contain proxy objects with internal references to this.
0184     // These proxies are created by Podcasts::PodcastEpisode(), so
0185     // these fields don't have to be set here.
0186 
0187     //commit to the database
0188     updateInDb();
0189     setupLocalFile();
0190 }
0191 
0192 SqlPodcastEpisode::SqlPodcastEpisode( const PodcastChannelPtr &channel, Podcasts::PodcastEpisodePtr episode )
0193     : Podcasts::PodcastEpisode()
0194     , m_dbId( 0 )
0195     , m_isKeep( false )
0196 {
0197     m_channel = SqlPodcastChannelPtr::dynamicCast( channel );
0198 
0199     if( !m_channel && episode->channel() )
0200     {
0201         debug() << "BUG: creating SqlEpisode but not an sqlChannel!!!";
0202         debug() <<  episode->channel()->title();
0203         debug() <<  m_channel->title();
0204     }
0205 
0206     // PodcastMetaCommon
0207     m_title = episode->title();
0208     m_description = episode->description();
0209     m_keywords = episode->keywords();
0210     m_subtitle = episode->subtitle();
0211     m_summary = episode->summary();
0212     m_author = episode->author();
0213 
0214     // PodcastEpisode
0215     m_guid = episode->guid();
0216     m_url = QUrl( episode->uidUrl() );
0217     m_localUrl = episode->localUrl();
0218     m_mimeType = episode->mimeType();
0219     m_pubDate = episode->pubDate();
0220     m_duration = episode->duration();
0221     m_fileSize = episode->filesize();
0222     m_sequenceNumber = episode->sequenceNumber();
0223     m_isNew = episode->isNew();
0224 
0225     // The album, artist, composer, genre and year fields
0226     // contain proxy objects with internal references to this.
0227     // These proxies are created by Podcasts::PodcastEpisode(), so
0228     // these fields don't have to be set here.
0229 
0230     //commit to the database
0231     updateInDb();
0232     setupLocalFile();
0233 }
0234 
0235 void
0236 SqlPodcastEpisode::setupLocalFile()
0237 {
0238     if( m_localUrl.isEmpty() || !QFileInfo( m_localUrl.toLocalFile() ).exists() )
0239         return;
0240 
0241     MetaProxy::TrackPtr proxyTrack( new MetaProxy::Track( m_localUrl, MetaProxy::Track::ManualLookup ) );
0242     m_localFile = Meta::TrackPtr( proxyTrack.data() ); // avoid static_cast
0243     /* following won't write to actual file, because MetaProxy::Track hasn't yet looked
0244      * up the underlying track. It will just set some cached values. */
0245     writeTagsToFile();
0246     proxyTrack->lookupTrack( &myFileTrackProvider );
0247 }
0248 
0249 SqlPodcastEpisode::~SqlPodcastEpisode()
0250 {
0251 }
0252 
0253 void
0254 SqlPodcastEpisode::setNew( bool isNew )
0255 {
0256     PodcastEpisode::setNew( isNew );
0257     updateInDb();
0258 }
0259 
0260 void SqlPodcastEpisode::setKeep( bool isKeep )
0261 {
0262     m_isKeep = isKeep;
0263     updateInDb();
0264 }
0265 
0266 void
0267 SqlPodcastEpisode::setLocalUrl( const QUrl &url )
0268 {
0269     m_localUrl = url;
0270     updateInDb();
0271 
0272     if( m_localUrl.isEmpty() && !m_localFile.isNull() )
0273     {
0274         m_localFile.clear();
0275         notifyObservers();
0276     }
0277     else
0278     {
0279         //if we had a local file previously it should get deleted by the AmarokSharedPointer.
0280         m_localFile = new MetaFile::Track( m_localUrl );
0281         if( m_channel->writeTags() )
0282             writeTagsToFile();
0283     }
0284 }
0285 
0286 qint64
0287 SqlPodcastEpisode::length() const
0288 {
0289     //if downloaded get the duration from the file, else use the value read from the feed
0290     if( m_localFile.isNull() )
0291         return m_duration * 1000;
0292 
0293     return m_localFile->length();
0294 }
0295 
0296 bool
0297 SqlPodcastEpisode::hasCapabilityInterface( Capabilities::Capability::Type type ) const
0298 {
0299     switch( type )
0300     {
0301         case Capabilities::Capability::Actions:
0302         case Capabilities::Capability::WriteTimecode:
0303         case Capabilities::Capability::LoadTimecode:
0304             //only downloaded episodes can be position marked
0305 //            return !localUrl().isEmpty();
0306             return true;
0307         default:
0308             return false;
0309     }
0310 }
0311 
0312 Capabilities::Capability*
0313 SqlPodcastEpisode::createCapabilityInterface( Capabilities::Capability::Type type )
0314 {
0315     switch( type )
0316     {
0317         case Capabilities::Capability::Actions:
0318         {
0319             QList< QAction * > actions;
0320             actions << new BookmarkCurrentTrackPositionAction( nullptr );
0321             return new Capabilities::ActionsCapability( actions );
0322         }
0323         case Capabilities::Capability::WriteTimecode:
0324             return new TimecodeWriteCapabilityPodcastImpl( this );
0325         case Capabilities::Capability::LoadTimecode:
0326             return new TimecodeLoadCapabilityPodcastImpl( this );
0327         default:
0328             return nullptr;
0329     }
0330 }
0331 
0332 void
0333 SqlPodcastEpisode::finishedPlaying( double playedFraction )
0334 {
0335     if( length() <= 0 || playedFraction >= 0.1 )
0336         setNew( false );
0337 
0338     PodcastEpisode::finishedPlaying( playedFraction );
0339 }
0340 
0341 QString
0342 SqlPodcastEpisode::name() const
0343 {
0344     if( m_localFile.isNull() )
0345         return m_title;
0346 
0347     return m_localFile->name();
0348 }
0349 
0350 QString
0351 SqlPodcastEpisode::prettyName() const
0352 {
0353     /*for now just do the same as name, but in the future we might want to used a cleaned
0354       up string using some sort of regex tag rewrite for podcasts. decapitateString on
0355       steroides. */
0356     return name();
0357 }
0358 
0359 void
0360 SqlPodcastEpisode::setTitle( const QString &title )
0361 {
0362     m_title = title;
0363 
0364     Meta::TrackEditorPtr ec = m_localFile ? m_localFile->editor() : Meta::TrackEditorPtr();
0365     if( ec  )
0366         ec->setTitle( title );
0367 }
0368 
0369 Meta::ArtistPtr
0370 SqlPodcastEpisode::artist() const
0371 {
0372     if( m_localFile.isNull() )
0373         return m_artistPtr;
0374 
0375     return m_localFile->artist();
0376 }
0377 
0378 Meta::ComposerPtr
0379 SqlPodcastEpisode::composer() const
0380 {
0381     if( m_localFile.isNull() )
0382         return m_composerPtr;
0383 
0384     return m_localFile->composer();
0385 }
0386 
0387 Meta::GenrePtr
0388 SqlPodcastEpisode::genre() const
0389 {
0390     if( m_localFile.isNull() )
0391         return m_genrePtr;
0392 
0393     return m_localFile->genre();
0394 }
0395 
0396 Meta::YearPtr
0397 SqlPodcastEpisode::year() const
0398 {
0399     if( m_localFile.isNull() )
0400         return m_yearPtr;
0401 
0402     return m_localFile->year();
0403 }
0404 
0405 Meta::TrackEditorPtr
0406 SqlPodcastEpisode::editor()
0407 {
0408     if( m_localFile )
0409         return m_localFile->editor();
0410     else
0411         return Meta::TrackEditorPtr();
0412 }
0413 
0414 bool
0415 SqlPodcastEpisode::writeTagsToFile()
0416 {
0417     if( !m_localFile )
0418         return false;
0419 
0420     Meta::TrackEditorPtr ec = m_localFile->editor();
0421     if( !ec )
0422         return false;
0423 
0424     debug() << "writing tags for podcast episode " << title() << "to " << m_localUrl.url();
0425     ec->beginUpdate();
0426     ec->setTitle( m_title );
0427     ec->setAlbum( m_channel->title() );
0428     ec->setArtist( m_channel->author() );
0429     ec->setGenre( i18n( "Podcast" ) );
0430     ec->setYear( m_pubDate.date().year() );
0431     ec->endUpdate();
0432 
0433     notifyObservers();
0434     return true;
0435 }
0436 
0437 void
0438 SqlPodcastEpisode::updateInDb()
0439 {
0440     auto sqlStorage = StorageManager::instance()->sqlStorage();
0441 
0442     QString boolTrue = sqlStorage->boolTrue();
0443     QString boolFalse = sqlStorage->boolFalse();
0444     #define escape(x) sqlStorage->escape(x)
0445     QString command;
0446     QTextStream stream( &command );
0447     if( m_dbId )
0448     {
0449         stream << "UPDATE podcastepisodes ";
0450         stream << "SET url='";
0451         stream << escape(m_url.url());
0452         stream << "', channel=";
0453         stream << m_channel->dbId();
0454         stream << ", localurl='";
0455         stream << escape(m_localUrl.url());
0456         stream << "', guid='";
0457         stream << escape(m_guid);
0458         stream << "', title='";
0459         stream << escape(m_title);
0460         stream << "', subtitle='";
0461         stream << escape(m_subtitle);
0462         stream << "', sequencenumber=";
0463         stream << m_sequenceNumber;
0464         stream << ", description='";
0465         stream << escape(m_description);
0466         stream << "', mimetype='";
0467         stream << escape(m_mimeType);
0468         stream << "', pubdate='";
0469         stream << escape(m_pubDate.toString(Qt::ISODate));
0470         stream << "', duration=";
0471         stream << m_duration;
0472         stream << ", filesize=";
0473         stream << m_fileSize;
0474         stream << ", isnew=";
0475         stream << (isNew() ? boolTrue : boolFalse);
0476         stream << ", iskeep=";
0477         stream << (isKeep() ? boolTrue : boolFalse);
0478         stream << " WHERE id=";
0479         stream << m_dbId;
0480         stream << ";";
0481         sqlStorage->query( command );
0482     }
0483     else
0484     {
0485         stream << "INSERT INTO podcastepisodes (";
0486         stream << "url,channel,localurl,guid,title,subtitle,sequencenumber,description,";
0487         stream << "mimetype,pubdate,duration,filesize,isnew,iskeep) ";
0488         stream << "VALUES ( '";
0489         stream << escape(m_url.url()) << "', ";
0490         stream << m_channel->dbId() << ", '";
0491         stream << escape(m_localUrl.url()) << "', '";
0492         stream << escape(m_guid) << "', '";
0493         stream << escape(m_title) << "', '";
0494         stream << escape(m_subtitle) << "', ";
0495         stream << m_sequenceNumber << ", '";
0496         stream << escape(m_description) << "', '";
0497         stream << escape(m_mimeType) << "', '";
0498         stream << escape(m_pubDate.toString(Qt::ISODate)) << "', ";
0499         stream << m_duration << ", ";
0500         stream << m_fileSize << ", ";
0501         stream << (isNew() ? boolTrue : boolFalse) << ", ";
0502         stream << (isKeep() ? boolTrue : boolFalse);
0503         stream << ");";
0504         m_dbId = sqlStorage->insert( command, QStringLiteral("podcastepisodes") );
0505     }
0506 }
0507 
0508 void
0509 SqlPodcastEpisode::deleteFromDb()
0510 {
0511     auto sqlStorage = StorageManager::instance()->sqlStorage();
0512     sqlStorage->query(
0513         QStringLiteral( "DELETE FROM podcastepisodes WHERE id = %1;" ).arg( dbId() ) );
0514 }
0515 
0516 Playlists::PlaylistPtr
0517 SqlPodcastChannel::toPlaylistPtr( const SqlPodcastChannelPtr &sqlChannel )
0518 {
0519     Playlists::PlaylistPtr playlist = Playlists::PlaylistPtr::dynamicCast( sqlChannel );
0520     return playlist;
0521 }
0522 
0523 SqlPodcastChannelPtr
0524 SqlPodcastChannel::fromPlaylistPtr( const Playlists::PlaylistPtr &playlist )
0525 {
0526     SqlPodcastChannelPtr sqlChannel = SqlPodcastChannelPtr::dynamicCast( playlist );
0527     return sqlChannel;
0528 }
0529 
0530 SqlPodcastChannel::SqlPodcastChannel( SqlPodcastProvider *provider,
0531                                             const QStringList &result )
0532     : Podcasts::PodcastChannel()
0533     , m_episodesLoaded( false )
0534     , m_trackCacheIsValid( false )
0535     , m_provider( provider )
0536 {
0537     auto sqlStorage = StorageManager::instance()->sqlStorage();
0538     QStringList::ConstIterator iter = result.constBegin();
0539     m_dbId = (*(iter++)).toInt();
0540     m_url = QUrl( *(iter++) );
0541     m_title = *(iter++);
0542     m_webLink = QUrl::fromUserInput(*(iter++));
0543     m_imageUrl = QUrl::fromUserInput(*(iter++));
0544     m_description = *(iter++);
0545     m_copyright = *(iter++);
0546     m_directory = QUrl( *(iter++) );
0547     m_labels = QStringList( QString( *(iter++) ).split( QLatin1Char(','), Qt::SkipEmptyParts ) );
0548     m_subscribeDate = QDate::fromString( *(iter++) );
0549     m_autoScan = sqlStorage->boolTrue() == *(iter++);
0550     m_fetchType = (*(iter++)).toInt() == DownloadWhenAvailable ? DownloadWhenAvailable : StreamOrDownloadOnDemand;
0551     m_purge = sqlStorage->boolTrue() == *(iter++);
0552     m_purgeCount = (*(iter++)).toInt();
0553     m_writeTags = sqlStorage->boolTrue() == *(iter++);
0554     m_filenameLayout = *(iter++);
0555 }
0556 
0557 SqlPodcastChannel::SqlPodcastChannel( Podcasts::SqlPodcastProvider *provider,
0558                                             Podcasts::PodcastChannelPtr channel )
0559     : Podcasts::PodcastChannel()
0560     , m_dbId( 0 )
0561     , m_trackCacheIsValid( false )
0562     , m_provider( provider )
0563     , m_filenameLayout( QStringLiteral("%default%") )
0564 {
0565     // PodcastMetaCommon
0566     m_title = channel->title();
0567     m_description = channel->description();
0568     m_keywords = channel->keywords();
0569     m_subtitle = channel->subtitle();
0570     m_summary = channel->summary();
0571     m_author = channel->author();
0572 
0573     // PodcastChannel
0574     m_url = channel->url();
0575     m_webLink = channel->webLink();
0576     m_imageUrl = channel->imageUrl();
0577     m_labels = channel->labels();
0578     m_subscribeDate = channel->subscribeDate();
0579     m_copyright = channel->copyright();
0580 
0581     if( channel->hasImage() )
0582         m_image = channel->image();
0583 
0584     //Default Settings
0585 
0586     m_directory = QUrl( m_provider->baseDownloadDir() );
0587     m_directory = m_directory.adjusted(QUrl::StripTrailingSlash);
0588     m_directory.setPath( QDir::toNativeSeparators(m_directory.path() + QLatin1Char('/') + Amarok::vfatPath( m_title )) );
0589 
0590     m_autoScan = true;
0591     m_fetchType = StreamOrDownloadOnDemand;
0592     m_purge = false;
0593     m_purgeCount = 10;
0594     m_writeTags = true;
0595 
0596     updateInDb();
0597 
0598     foreach( Podcasts::PodcastEpisodePtr episode, channel->episodes() )
0599     {
0600         episode->setChannel( PodcastChannelPtr( this ) );
0601         SqlPodcastEpisode *sqlEpisode = new SqlPodcastEpisode( episode );
0602 
0603         m_episodes << SqlPodcastEpisodePtr( sqlEpisode );
0604     }
0605     m_episodesLoaded = true;
0606 }
0607 
0608 int
0609 SqlPodcastChannel::trackCount() const
0610 {
0611     if( m_episodesLoaded )
0612         return m_episodes.count();
0613     else
0614         return -1;
0615 }
0616 
0617 void
0618 SqlPodcastChannel::triggerTrackLoad()
0619 {
0620     if( !m_episodesLoaded )
0621         loadEpisodes();
0622     notifyObserversTracksLoaded();
0623 }
0624 
0625 Playlists::PlaylistProvider *
0626 SqlPodcastChannel::provider() const
0627 {
0628     return dynamic_cast<Playlists::PlaylistProvider *>( m_provider );
0629 }
0630 
0631 QStringList
0632 SqlPodcastChannel::groups()
0633 {
0634     return m_labels;
0635 }
0636 
0637 void
0638 SqlPodcastChannel::setGroups( const QStringList &groups )
0639 {
0640     m_labels = groups;
0641 }
0642 
0643 QUrl
0644 SqlPodcastChannel::uidUrl() const
0645 {
0646     return QUrl( QStringLiteral( "amarok-sqlpodcastuid://%1").arg( m_dbId ) );
0647 }
0648 
0649 SqlPodcastChannel::~SqlPodcastChannel()
0650 {
0651     m_episodes.clear();
0652 }
0653 
0654 void
0655 SqlPodcastChannel::setTitle( const QString &title )
0656 {
0657     /* also change the savelocation if a title is not set yet.
0658        This is a special condition that can happen when first fetching a podcast feed */
0659     if( m_title.isEmpty() )
0660     {
0661         m_directory = m_directory.adjusted(QUrl::StripTrailingSlash);
0662         m_directory.setPath( QDir::toNativeSeparators(m_directory.path() + QLatin1Char('/') + Amarok::vfatPath( title )) );
0663     }
0664     m_title = title;
0665 }
0666 
0667 Podcasts::PodcastEpisodeList
0668 SqlPodcastChannel::episodes() const
0669 {
0670     return SqlPodcastEpisode::toPodcastEpisodeList( m_episodes );
0671 }
0672 
0673 void
0674 SqlPodcastChannel::setImage( const QImage &image )
0675 {
0676     DEBUG_BLOCK
0677 
0678     m_image = image;
0679 }
0680 
0681 void
0682 SqlPodcastChannel::setImageUrl( const QUrl &imageUrl )
0683 {
0684     DEBUG_BLOCK
0685     debug() << imageUrl;
0686     m_imageUrl = imageUrl;
0687 
0688     if( imageUrl.isLocalFile() )
0689     {
0690         m_image = QImage( imageUrl.path() );
0691         return;
0692     }
0693 
0694     debug() << "Image is remote, handled by podcastImageFetcher.";
0695 }
0696 
0697 Podcasts::PodcastEpisodePtr
0698 SqlPodcastChannel::addEpisode(const PodcastEpisodePtr &episode )
0699 {
0700     if( !m_provider )
0701         return PodcastEpisodePtr();
0702 
0703     QUrl checkUrl;
0704     //searched in the database for guid or enclosure url
0705     if( !episode->guid().isEmpty() )
0706         checkUrl = QUrl::fromUserInput(episode->guid());
0707     else if( !episode->uidUrl().isEmpty() )
0708         checkUrl = QUrl::fromUserInput(episode->uidUrl());
0709     else
0710         return PodcastEpisodePtr(); //noting to check for
0711 
0712     if( m_provider->possiblyContainsTrack( checkUrl ) )
0713         return PodcastEpisodePtr::dynamicCast( m_provider->trackForUrl( QUrl::fromUserInput(episode->guid()) ) );
0714 
0715     //force episodes load.
0716     if( !m_episodesLoaded )
0717         loadEpisodes();
0718 
0719     SqlPodcastEpisodePtr sqlEpisode;
0720 
0721     if (SqlPodcastEpisodePtr::dynamicCast( episode ))
0722         sqlEpisode = SqlPodcastEpisodePtr( new SqlPodcastEpisode( episode ) );
0723     else
0724         sqlEpisode = SqlPodcastEpisodePtr( new SqlPodcastEpisode( PodcastChannelPtr(this) , episode ) );
0725 
0726 
0727     //episodes are sorted on pubDate high to low
0728     int i;
0729     for( i = 0; i < m_episodes.count() ; i++ )
0730     {
0731         if( sqlEpisode->pubDate() > m_episodes[i]->pubDate() )
0732         {
0733             m_episodes.insert( i, sqlEpisode );
0734             break;
0735         }
0736     }
0737 
0738     //insert in case the list is empty or at the end of the list
0739     if( i == m_episodes.count() )
0740         m_episodes << sqlEpisode;
0741 
0742     notifyObserversTrackAdded( Meta::TrackPtr::dynamicCast( sqlEpisode ), i );
0743 
0744     applyPurge();
0745     m_trackCacheIsValid = false;
0746     return PodcastEpisodePtr::dynamicCast( sqlEpisode );
0747 }
0748 
0749 void
0750 SqlPodcastChannel::applyPurge()
0751 {
0752     DEBUG_BLOCK
0753     if( !hasPurge() )
0754         return;
0755 
0756     if( m_episodes.count() > purgeCount() )
0757     {
0758         int purgeIndex = 0;
0759 
0760         foreach( SqlPodcastEpisodePtr episode, m_episodes )
0761         {
0762             if ( !episode->isKeep() )
0763             {
0764                 if( purgeIndex >= purgeCount() )
0765                 {
0766                     m_provider->deleteDownloadedEpisode( episode );
0767                     m_episodes.removeOne( episode );
0768                 }
0769                 else
0770                     purgeIndex++;
0771             }
0772         }
0773         m_trackCacheIsValid = false;
0774     }
0775 }
0776 
0777 void
0778 SqlPodcastChannel::updateInDb()
0779 {
0780     auto sqlStorage = StorageManager::instance()->sqlStorage();
0781 
0782     QString boolTrue = sqlStorage->boolTrue();
0783     QString boolFalse = sqlStorage->boolFalse();
0784     #define escape(x) sqlStorage->escape(x)
0785     QString command;
0786     QTextStream stream( &command );
0787     if( m_dbId )
0788     {
0789         stream << "UPDATE podcastchannels ";
0790         stream << "SET url='";
0791         stream << escape(m_url.url());
0792         stream << "', title='";
0793         stream << escape(m_title);
0794         stream << "', weblink='";
0795         stream << escape(m_webLink.url());
0796         stream << "', image='";
0797         stream << escape(m_imageUrl.url());
0798         stream << "', description='";
0799         stream << escape(m_description);
0800         stream << "', copyright='";
0801         stream << escape(m_copyright);
0802         stream << "', directory='";
0803         stream << escape(m_directory.url());
0804         stream << "', labels='";
0805         stream << escape(m_labels.join( QLatin1Char(',') ));
0806         stream << "', subscribedate='";
0807         stream << escape(m_subscribeDate.toString());
0808         stream << "', autoscan=";
0809         stream << (m_autoScan ? boolTrue : boolFalse);
0810         stream << ", fetchtype=";
0811         stream << QString::number(m_fetchType);
0812         stream << ", haspurge=";
0813         stream << (m_purge ? boolTrue : boolFalse);
0814         stream << ", purgecount=";
0815         stream << QString::number(m_purgeCount);
0816         stream << ", writetags=";
0817         stream << (m_writeTags ? boolTrue : boolFalse);
0818         stream << ", filenamelayout='";
0819         stream << escape(m_filenameLayout);
0820         stream << "' WHERE id=";
0821         stream << m_dbId;
0822         stream << ";";
0823         debug() << command;
0824         sqlStorage->query( command );
0825     }
0826     else
0827     {
0828         stream << "INSERT INTO podcastchannels(";
0829         stream << "url,title,weblink,image,description,copyright,directory,labels,";
0830         stream << "subscribedate,autoscan,fetchtype,haspurge,purgecount,writetags,filenamelayout) ";
0831         stream << "VALUES ( '";
0832         stream << escape(m_url.url()) << "', '";
0833         stream << escape(m_title) << "', '";
0834         stream << escape(m_webLink.url()) << "', '";
0835         stream << escape(m_imageUrl.url()) << "', '";
0836         stream << escape(m_description) << "', '";
0837         stream << escape(m_copyright) << "', '";
0838         stream << escape(m_directory.url()) << "', '";
0839         stream << escape(m_labels.join( QLatin1Char(',') )) << "', '";
0840         stream << escape(m_subscribeDate.toString()) << "', ";
0841         stream << (m_autoScan ? boolTrue : boolFalse) << ", ";
0842         stream << QString::number(m_fetchType) << ", ";
0843         stream << (m_purge ? boolTrue : boolFalse) << ", ";
0844         stream << QString::number(m_purgeCount) << ", ";
0845         stream << (m_writeTags ? boolTrue : boolFalse) << ", '";
0846         stream << escape(m_filenameLayout);
0847         stream << "');";
0848         debug() << command;
0849         m_dbId = sqlStorage->insert( command, QStringLiteral("podcastchannels") );
0850     }
0851 }
0852 
0853 void
0854 SqlPodcastChannel::deleteFromDb()
0855 {
0856     auto sqlStorage = StorageManager::instance()->sqlStorage();
0857     foreach( SqlPodcastEpisodePtr sqlEpisode, m_episodes )
0858     {
0859        sqlEpisode->deleteFromDb();
0860        m_episodes.removeOne( sqlEpisode );
0861     }
0862     m_trackCacheIsValid = false;
0863 
0864     sqlStorage->query(
0865         QStringLiteral( "DELETE FROM podcastchannels WHERE id = %1;" ).arg( dbId() ) );
0866 }
0867 
0868 void
0869 SqlPodcastChannel::loadEpisodes()
0870 {
0871     m_episodes.clear();
0872 
0873     auto sqlStorage = StorageManager::instance()->sqlStorage();
0874 
0875     //If purge is enabled we must limit the number of results
0876     QString command;
0877 
0878     int rowLength = 15;
0879 
0880     //If purge is enabled we must limit the number of results, though there are some files
0881     //the user want to be shown even if there is no more slot
0882     if( hasPurge() )
0883     {
0884         command = QString( "(SELECT id, url, channel, localurl, guid, "
0885                            "title, subtitle, sequencenumber, description, mimetype, pubdate, "
0886                            "duration, filesize, isnew, iskeep FROM podcastepisodes WHERE channel = %1 "
0887                            "AND iskeep IS FALSE ORDER BY pubdate DESC LIMIT " + QString::number( purgeCount() ) + ") "
0888                            "UNION "
0889                            "(SELECT id, url, channel, localurl, guid, "
0890                            "title, subtitle, sequencenumber, description, mimetype, pubdate, "
0891                            "duration, filesize, isnew, iskeep FROM podcastepisodes WHERE channel = %1 "
0892                            "AND iskeep IS TRUE) "
0893                            "ORDER BY pubdate DESC;"
0894                            );
0895     }
0896     else
0897     {
0898         command = QString( "SELECT id, url, channel, localurl, guid, "
0899                            "title, subtitle, sequencenumber, description, mimetype, pubdate, "
0900                            "duration, filesize, isnew, iskeep FROM podcastepisodes WHERE channel = %1 "
0901                            "ORDER BY pubdate DESC;"
0902                            );
0903     }
0904 
0905     QStringList results = sqlStorage->query( command.arg( m_dbId ) );
0906 
0907     for( int i = 0; i < results.size(); i += rowLength )
0908     {
0909         QStringList episodesResult = results.mid( i, rowLength );
0910         SqlPodcastEpisodePtr sqlEpisode = SqlPodcastEpisodePtr(
0911                                               new SqlPodcastEpisode(
0912                                                   episodesResult,
0913                                                   SqlPodcastChannelPtr( this ) ) );
0914         m_episodes << sqlEpisode;
0915     }
0916 
0917     m_episodesLoaded = true;
0918     m_trackCacheIsValid = false;
0919 }
0920 
0921 Meta::TrackList
0922 Podcasts::SqlPodcastChannel::tracks()
0923 {
0924     if ( !m_trackCacheIsValid ) {
0925         m_episodesAsTracksCache = Podcasts::SqlPodcastEpisode::toTrackList( m_episodes );
0926         m_trackCacheIsValid = true;
0927     }
0928     return m_episodesAsTracksCache;
0929 }
0930 
0931 void
0932 Podcasts::SqlPodcastChannel::syncTrackStatus(int position, const Meta::TrackPtr &otherTrack )
0933 {
0934     Q_UNUSED( position );
0935 
0936     Podcasts::PodcastEpisodePtr master =
0937             Podcasts::PodcastEpisodePtr::dynamicCast( otherTrack );
0938 
0939     if ( master )
0940     {
0941         this->setName( master->channel()->name() );
0942         this->setTitle( master->channel()->title() );
0943         this->setUrl( master->channel()->url() );
0944     }
0945 }
0946 
0947 void
0948 Podcasts::SqlPodcastChannel::addTrack( const Meta::TrackPtr &track, int position )
0949 {
0950     Q_UNUSED( position );
0951 
0952     addEpisode( Podcasts::PodcastEpisodePtr::dynamicCast( track ) );
0953     notifyObserversTrackAdded( track, position );
0954 }