1 /****************************************************************************************
2  * Copyright (c) 2008 Bart Cerneels <bart.cerneels@kde.org>                             *
3  *                                                                                      *
4  * This program is free software; you can redistribute it and/or modify it under        *
5  * the terms of the GNU General Public License as published by the Free Software        *
6  * Foundation; either version 2 of the License, or (at your option) any later           *
7  * version.                                                                             *
8  *                                                                                      *
9  * This program is distributed in the hope that it will be useful, but WITHOUT ANY      *
10  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A      *
11  * PARTICULAR PURPOSE. See the GNU General Public License for more details.             *
12  *                                                                                      *
13  * You should have received a copy of the GNU General Public License along with         *
14  * this program.  If not, see <http://www.gnu.org/licenses/>.                           *
15  ****************************************************************************************/
16 
17 #include "SqlPodcastMeta.h"
18 
19 #include "amarokurls/BookmarkMetaActions.h"
20 #include "amarokurls/PlayUrlRunner.h"
21 #include "core/capabilities/ActionsCapability.h"
22 #include <core/storage/SqlStorage.h>
23 #include "core/meta/TrackEditor.h"
24 #include "core/support/Debug.h"
25 #include "core-impl/capabilities/timecode/TimecodeLoadCapability.h"
26 #include "core-impl/capabilities/timecode/TimecodeWriteCapability.h"
27 #include "core-impl/collections/support/CollectionManager.h"
28 #include "core-impl/storage/StorageManager.h"
29 #include "core-impl/meta/proxy/MetaProxy.h"
30 #include "core-impl/meta/file/FileTrackProvider.h"
31 #include "core-impl/podcasts/sql/SqlPodcastProvider.h"
32 
33 #include <QDate>
34 #include <QFile>
35 
36 using namespace Podcasts;
37 
38 static FileTrackProvider myFileTrackProvider; // we need it to be available for lookups
39 
40 class TimecodeWriteCapabilityPodcastImpl : public Capabilities::TimecodeWriteCapability
41 {
42     public:
TimecodeWriteCapabilityPodcastImpl(Podcasts::PodcastEpisode * episode)43         TimecodeWriteCapabilityPodcastImpl( Podcasts::PodcastEpisode *episode )
44             : Capabilities::TimecodeWriteCapability()
45             , m_episode( episode )
46         {}
47 
writeTimecode(qint64 miliseconds)48     bool writeTimecode ( qint64 miliseconds ) override
49     {
50         DEBUG_BLOCK
51         return Capabilities::TimecodeWriteCapability::writeTimecode( miliseconds,
52                 Meta::TrackPtr::dynamicCast( m_episode ) );
53     }
54 
writeAutoTimecode(qint64 miliseconds)55     bool writeAutoTimecode ( qint64 miliseconds ) override
56     {
57         DEBUG_BLOCK
58         return Capabilities::TimecodeWriteCapability::writeAutoTimecode( miliseconds,
59                 Meta::TrackPtr::dynamicCast( m_episode ) );
60     }
61 
62     private:
63         Podcasts::PodcastEpisodePtr m_episode;
64 };
65 
66 class TimecodeLoadCapabilityPodcastImpl : public Capabilities::TimecodeLoadCapability
67 {
68     public:
TimecodeLoadCapabilityPodcastImpl(Podcasts::PodcastEpisode * episode)69         TimecodeLoadCapabilityPodcastImpl( Podcasts::PodcastEpisode *episode )
70         : Capabilities::TimecodeLoadCapability()
71         , m_episode( episode )
72         {
73             DEBUG_BLOCK
74             debug() << "episode: " << m_episode->name();
75         }
76 
hasTimecodes()77         bool hasTimecodes() override
78         {
79             if ( loadTimecodes().size() > 0 )
80                 return true;
81             return false;
82         }
83 
loadTimecodes()84         BookmarkList loadTimecodes() override
85         {
86             DEBUG_BLOCK
87             if ( m_episode && m_episode->playableUrl().isValid() )
88             {
89                 BookmarkList list = PlayUrlRunner::bookmarksFromUrl( m_episode->playableUrl() );
90                 return list;
91             }
92             else
93                 return BookmarkList();
94         }
95 
96     private:
97         Podcasts::PodcastEpisodePtr m_episode;
98 };
99 
100 Meta::TrackList
toTrackList(Podcasts::SqlPodcastEpisodeList episodes)101 SqlPodcastEpisode::toTrackList( Podcasts::SqlPodcastEpisodeList episodes )
102 {
103     Meta::TrackList tracks;
104     foreach( SqlPodcastEpisodePtr sqlEpisode, episodes )
105         tracks << Meta::TrackPtr::dynamicCast( sqlEpisode );
106 
107     return tracks;
108 }
109 
110 Podcasts::PodcastEpisodeList
toPodcastEpisodeList(SqlPodcastEpisodeList episodes)111 SqlPodcastEpisode::toPodcastEpisodeList( SqlPodcastEpisodeList episodes )
112 {
113     Podcasts::PodcastEpisodeList sqlEpisodes;
114     foreach( SqlPodcastEpisodePtr sqlEpisode, episodes )
115         sqlEpisodes << Podcasts::PodcastEpisodePtr::dynamicCast( sqlEpisode );
116 
117     return sqlEpisodes;
118 }
119 
SqlPodcastEpisode(const QStringList & result,const SqlPodcastChannelPtr & sqlChannel)120 SqlPodcastEpisode::SqlPodcastEpisode( const QStringList &result, const SqlPodcastChannelPtr &sqlChannel )
121     : Podcasts::PodcastEpisode( Podcasts::PodcastChannelPtr::staticCast( sqlChannel ) )
122     , m_channel( sqlChannel )
123 {
124     auto sqlStorage = StorageManager::instance()->sqlStorage();
125     QStringList::ConstIterator iter = result.constBegin();
126     m_dbId = (*(iter++)).toInt();
127     m_url = QUrl( *(iter++) );
128     int channelId = (*(iter++)).toInt();
129     Q_UNUSED( channelId );
130     m_localUrl = QUrl( *(iter++) );
131     m_guid = *(iter++);
132     m_title = *(iter++);
133     m_subtitle = *(iter++);
134     m_sequenceNumber = (*(iter++)).toInt();
135     m_description = *(iter++);
136     m_mimeType = *(iter++);
137     m_pubDate = QDateTime::fromString( *(iter++), Qt::ISODate );
138     m_duration = (*(iter++)).toInt();
139     m_fileSize = (*(iter++)).toInt();
140     m_isNew = sqlStorage->boolTrue() == (*(iter++));
141     m_isKeep = sqlStorage->boolTrue() == (*(iter++));
142 
143     Q_ASSERT_X( iter == result.constEnd(), "SqlPodcastEpisode( PodcastCollection*, QStringList )", "number of expected fields did not match number of actual fields" );
144 
145     setupLocalFile();
146 }
147 
148 //TODO: why do PodcastMetaCommon and PodcastEpisode not have an appropriate copy constructor?
SqlPodcastEpisode(Podcasts::PodcastEpisodePtr episode)149 SqlPodcastEpisode::SqlPodcastEpisode( Podcasts::PodcastEpisodePtr episode )
150     : Podcasts::PodcastEpisode()
151     , m_dbId( 0 )
152     , m_isKeep( false )
153 {
154     m_channel = SqlPodcastChannelPtr::dynamicCast( episode->channel() );
155 
156     if( !m_channel && episode->channel() )
157     {
158         debug() << "BUG: creating SqlEpisode but not an sqlChannel!!!";
159         debug() <<  episode->channel()->title();
160         debug() <<  m_channel->title();
161     }
162 
163     // PodcastMetaCommon
164     m_title = episode->title();
165     m_description = episode->description();
166     m_keywords = episode->keywords();
167     m_subtitle = episode->subtitle();
168     m_summary = episode->summary();
169     m_author = episode->author();
170 
171     // PodcastEpisode
172     m_guid = episode->guid();
173     m_url = QUrl( episode->uidUrl() );
174     m_localUrl = episode->localUrl();
175     m_mimeType = episode->mimeType();
176     m_pubDate = episode->pubDate();
177     m_duration = episode->duration();
178     m_fileSize = episode->filesize();
179     m_sequenceNumber = episode->sequenceNumber();
180     m_isNew = episode->isNew();
181 
182     // The album, artist, composer, genre and year fields
183     // contain proxy objects with internal references to this.
184     // These proxies are created by Podcasts::PodcastEpisode(), so
185     // these fields don't have to be set here.
186 
187     //commit to the database
188     updateInDb();
189     setupLocalFile();
190 }
191 
SqlPodcastEpisode(const PodcastChannelPtr & channel,Podcasts::PodcastEpisodePtr episode)192 SqlPodcastEpisode::SqlPodcastEpisode( const PodcastChannelPtr &channel, Podcasts::PodcastEpisodePtr episode )
193     : Podcasts::PodcastEpisode()
194     , m_dbId( 0 )
195     , m_isKeep( false )
196 {
197     m_channel = SqlPodcastChannelPtr::dynamicCast( channel );
198 
199     if( !m_channel && episode->channel() )
200     {
201         debug() << "BUG: creating SqlEpisode but not an sqlChannel!!!";
202         debug() <<  episode->channel()->title();
203         debug() <<  m_channel->title();
204     }
205 
206     // PodcastMetaCommon
207     m_title = episode->title();
208     m_description = episode->description();
209     m_keywords = episode->keywords();
210     m_subtitle = episode->subtitle();
211     m_summary = episode->summary();
212     m_author = episode->author();
213 
214     // PodcastEpisode
215     m_guid = episode->guid();
216     m_url = QUrl( episode->uidUrl() );
217     m_localUrl = episode->localUrl();
218     m_mimeType = episode->mimeType();
219     m_pubDate = episode->pubDate();
220     m_duration = episode->duration();
221     m_fileSize = episode->filesize();
222     m_sequenceNumber = episode->sequenceNumber();
223     m_isNew = episode->isNew();
224 
225     // The album, artist, composer, genre and year fields
226     // contain proxy objects with internal references to this.
227     // These proxies are created by Podcasts::PodcastEpisode(), so
228     // these fields don't have to be set here.
229 
230     //commit to the database
231     updateInDb();
232     setupLocalFile();
233 }
234 
235 void
setupLocalFile()236 SqlPodcastEpisode::setupLocalFile()
237 {
238     if( m_localUrl.isEmpty() || !QFileInfo( m_localUrl.toLocalFile() ).exists() )
239         return;
240 
241     MetaProxy::TrackPtr proxyTrack( new MetaProxy::Track( m_localUrl, MetaProxy::Track::ManualLookup ) );
242     m_localFile = Meta::TrackPtr( proxyTrack.data() ); // avoid static_cast
243     /* following won't write to actual file, because MetaProxy::Track hasn't yet looked
244      * up the underlying track. It will just set some cached values. */
245     writeTagsToFile();
246     proxyTrack->lookupTrack( &myFileTrackProvider );
247 }
248 
~SqlPodcastEpisode()249 SqlPodcastEpisode::~SqlPodcastEpisode()
250 {
251 }
252 
253 void
setNew(bool isNew)254 SqlPodcastEpisode::setNew( bool isNew )
255 {
256     PodcastEpisode::setNew( isNew );
257     updateInDb();
258 }
259 
setKeep(bool isKeep)260 void SqlPodcastEpisode::setKeep( bool isKeep )
261 {
262     m_isKeep = isKeep;
263     updateInDb();
264 }
265 
266 void
setLocalUrl(const QUrl & url)267 SqlPodcastEpisode::setLocalUrl( const QUrl &url )
268 {
269     m_localUrl = url;
270     updateInDb();
271 
272     if( m_localUrl.isEmpty() && !m_localFile.isNull() )
273     {
274         m_localFile.clear();
275         notifyObservers();
276     }
277     else
278     {
279         //if we had a local file previously it should get deleted by the AmarokSharedPointer.
280         m_localFile = new MetaFile::Track( m_localUrl );
281         if( m_channel->writeTags() )
282             writeTagsToFile();
283     }
284 }
285 
286 qint64
length() const287 SqlPodcastEpisode::length() const
288 {
289     //if downloaded get the duration from the file, else use the value read from the feed
290     if( m_localFile.isNull() )
291         return m_duration * 1000;
292 
293     return m_localFile->length();
294 }
295 
296 bool
hasCapabilityInterface(Capabilities::Capability::Type type) const297 SqlPodcastEpisode::hasCapabilityInterface( Capabilities::Capability::Type type ) const
298 {
299     switch( type )
300     {
301         case Capabilities::Capability::Actions:
302         case Capabilities::Capability::WriteTimecode:
303         case Capabilities::Capability::LoadTimecode:
304             //only downloaded episodes can be position marked
305 //            return !localUrl().isEmpty();
306             return true;
307         default:
308             return false;
309     }
310 }
311 
312 Capabilities::Capability*
createCapabilityInterface(Capabilities::Capability::Type type)313 SqlPodcastEpisode::createCapabilityInterface( Capabilities::Capability::Type type )
314 {
315     switch( type )
316     {
317         case Capabilities::Capability::Actions:
318         {
319             QList< QAction * > actions;
320             actions << new BookmarkCurrentTrackPositionAction( 0 );
321             return new Capabilities::ActionsCapability( actions );
322         }
323         case Capabilities::Capability::WriteTimecode:
324             return new TimecodeWriteCapabilityPodcastImpl( this );
325         case Capabilities::Capability::LoadTimecode:
326             return new TimecodeLoadCapabilityPodcastImpl( this );
327         default:
328             return 0;
329     }
330 }
331 
332 void
finishedPlaying(double playedFraction)333 SqlPodcastEpisode::finishedPlaying( double playedFraction )
334 {
335     if( length() <= 0 || playedFraction >= 0.1 )
336         setNew( false );
337 
338     PodcastEpisode::finishedPlaying( playedFraction );
339 }
340 
341 QString
name() const342 SqlPodcastEpisode::name() const
343 {
344     if( m_localFile.isNull() )
345         return m_title;
346 
347     return m_localFile->name();
348 }
349 
350 QString
prettyName() const351 SqlPodcastEpisode::prettyName() const
352 {
353     /*for now just do the same as name, but in the future we might want to used a cleaned
354       up string using some sort of regex tag rewrite for podcasts. decapitateString on
355       steroides. */
356     return name();
357 }
358 
359 void
setTitle(const QString & title)360 SqlPodcastEpisode::setTitle( const QString &title )
361 {
362     m_title = title;
363 
364     Meta::TrackEditorPtr ec = m_localFile ? m_localFile->editor() : Meta::TrackEditorPtr();
365     if( ec  )
366         ec->setTitle( title );
367 }
368 
369 Meta::ArtistPtr
artist() const370 SqlPodcastEpisode::artist() const
371 {
372     if( m_localFile.isNull() )
373         return m_artistPtr;
374 
375     return m_localFile->artist();
376 }
377 
378 Meta::ComposerPtr
composer() const379 SqlPodcastEpisode::composer() const
380 {
381     if( m_localFile.isNull() )
382         return m_composerPtr;
383 
384     return m_localFile->composer();
385 }
386 
387 Meta::GenrePtr
genre() const388 SqlPodcastEpisode::genre() const
389 {
390     if( m_localFile.isNull() )
391         return m_genrePtr;
392 
393     return m_localFile->genre();
394 }
395 
396 Meta::YearPtr
year() const397 SqlPodcastEpisode::year() const
398 {
399     if( m_localFile.isNull() )
400         return m_yearPtr;
401 
402     return m_localFile->year();
403 }
404 
405 Meta::TrackEditorPtr
editor()406 SqlPodcastEpisode::editor()
407 {
408     if( m_localFile )
409         return m_localFile->editor();
410     else
411         return Meta::TrackEditorPtr();
412 }
413 
414 bool
writeTagsToFile()415 SqlPodcastEpisode::writeTagsToFile()
416 {
417     if( !m_localFile )
418         return false;
419 
420     Meta::TrackEditorPtr ec = m_localFile->editor();
421     if( !ec )
422         return false;
423 
424     debug() << "writing tags for podcast episode " << title() << "to " << m_localUrl.url();
425     ec->beginUpdate();
426     ec->setTitle( m_title );
427     ec->setAlbum( m_channel->title() );
428     ec->setArtist( m_channel->author() );
429     ec->setGenre( i18n( "Podcast" ) );
430     ec->setYear( m_pubDate.date().year() );
431     ec->endUpdate();
432 
433     notifyObservers();
434     return true;
435 }
436 
437 void
updateInDb()438 SqlPodcastEpisode::updateInDb()
439 {
440     auto sqlStorage = StorageManager::instance()->sqlStorage();
441 
442     QString boolTrue = sqlStorage->boolTrue();
443     QString boolFalse = sqlStorage->boolFalse();
444     #define escape(x) sqlStorage->escape(x)
445     QString command;
446     QTextStream stream( &command );
447     if( m_dbId )
448     {
449         stream << "UPDATE podcastepisodes ";
450         stream << "SET url='";
451         stream << escape(m_url.url());
452         stream << "', channel=";
453         stream << m_channel->dbId();
454         stream << ", localurl='";
455         stream << escape(m_localUrl.url());
456         stream << "', guid='";
457         stream << escape(m_guid);
458         stream << "', title='";
459         stream << escape(m_title);
460         stream << "', subtitle='";
461         stream << escape(m_subtitle);
462         stream << "', sequencenumber=";
463         stream << m_sequenceNumber;
464         stream << ", description='";
465         stream << escape(m_description);
466         stream << "', mimetype='";
467         stream << escape(m_mimeType);
468         stream << "', pubdate='";
469         stream << escape(m_pubDate.toString(Qt::ISODate));
470         stream << "', duration=";
471         stream << m_duration;
472         stream << ", filesize=";
473         stream << m_fileSize;
474         stream << ", isnew=";
475         stream << (isNew() ? boolTrue : boolFalse);
476         stream << ", iskeep=";
477         stream << (isKeep() ? boolTrue : boolFalse);
478         stream << " WHERE id=";
479         stream << m_dbId;
480         stream << ";";
481         sqlStorage->query( command );
482     }
483     else
484     {
485         stream << "INSERT INTO podcastepisodes (";
486         stream << "url,channel,localurl,guid,title,subtitle,sequencenumber,description,";
487         stream << "mimetype,pubdate,duration,filesize,isnew,iskeep) ";
488         stream << "VALUES ( '";
489         stream << escape(m_url.url()) << "', ";
490         stream << m_channel->dbId() << ", '";
491         stream << escape(m_localUrl.url()) << "', '";
492         stream << escape(m_guid) << "', '";
493         stream << escape(m_title) << "', '";
494         stream << escape(m_subtitle) << "', ";
495         stream << m_sequenceNumber << ", '";
496         stream << escape(m_description) << "', '";
497         stream << escape(m_mimeType) << "', '";
498         stream << escape(m_pubDate.toString(Qt::ISODate)) << "', ";
499         stream << m_duration << ", ";
500         stream << m_fileSize << ", ";
501         stream << (isNew() ? boolTrue : boolFalse) << ", ";
502         stream << (isKeep() ? boolTrue : boolFalse);
503         stream << ");";
504         m_dbId = sqlStorage->insert( command, QStringLiteral("podcastepisodes") );
505     }
506 }
507 
508 void
deleteFromDb()509 SqlPodcastEpisode::deleteFromDb()
510 {
511     auto sqlStorage = StorageManager::instance()->sqlStorage();
512     sqlStorage->query(
513         QStringLiteral( "DELETE FROM podcastepisodes WHERE id = %1;" ).arg( dbId() ) );
514 }
515 
516 Playlists::PlaylistPtr
toPlaylistPtr(const SqlPodcastChannelPtr & sqlChannel)517 SqlPodcastChannel::toPlaylistPtr( const SqlPodcastChannelPtr &sqlChannel )
518 {
519     Playlists::PlaylistPtr playlist = Playlists::PlaylistPtr::dynamicCast( sqlChannel );
520     return playlist;
521 }
522 
523 SqlPodcastChannelPtr
fromPlaylistPtr(const Playlists::PlaylistPtr & playlist)524 SqlPodcastChannel::fromPlaylistPtr( const Playlists::PlaylistPtr &playlist )
525 {
526     SqlPodcastChannelPtr sqlChannel = SqlPodcastChannelPtr::dynamicCast( playlist );
527     return sqlChannel;
528 }
529 
SqlPodcastChannel(SqlPodcastProvider * provider,const QStringList & result)530 SqlPodcastChannel::SqlPodcastChannel( SqlPodcastProvider *provider,
531                                             const QStringList &result )
532     : Podcasts::PodcastChannel()
533     , m_episodesLoaded( false )
534     , m_trackCacheIsValid( false )
535     , m_provider( provider )
536 {
537     auto sqlStorage = StorageManager::instance()->sqlStorage();
538     QStringList::ConstIterator iter = result.constBegin();
539     m_dbId = (*(iter++)).toInt();
540     m_url = QUrl( *(iter++) );
541     m_title = *(iter++);
542     m_webLink = QUrl::fromUserInput(*(iter++));
543     m_imageUrl = QUrl::fromUserInput(*(iter++));
544     m_description = *(iter++);
545     m_copyright = *(iter++);
546     m_directory = QUrl( *(iter++) );
547     m_labels = QStringList( QString( *(iter++) ).split( QLatin1Char(','), QString::SkipEmptyParts ) );
548     m_subscribeDate = QDate::fromString( *(iter++) );
549     m_autoScan = sqlStorage->boolTrue() == *(iter++);
550     m_fetchType = (*(iter++)).toInt() == DownloadWhenAvailable ? DownloadWhenAvailable : StreamOrDownloadOnDemand;
551     m_purge = sqlStorage->boolTrue() == *(iter++);
552     m_purgeCount = (*(iter++)).toInt();
553     m_writeTags = sqlStorage->boolTrue() == *(iter++);
554     m_filenameLayout = *(iter++);
555 }
556 
SqlPodcastChannel(Podcasts::SqlPodcastProvider * provider,Podcasts::PodcastChannelPtr channel)557 SqlPodcastChannel::SqlPodcastChannel( Podcasts::SqlPodcastProvider *provider,
558                                             Podcasts::PodcastChannelPtr channel )
559     : Podcasts::PodcastChannel()
560     , m_dbId( 0 )
561     , m_trackCacheIsValid( false )
562     , m_provider( provider )
563     , m_filenameLayout( QStringLiteral("%default%") )
564 {
565     // PodcastMetaCommon
566     m_title = channel->title();
567     m_description = channel->description();
568     m_keywords = channel->keywords();
569     m_subtitle = channel->subtitle();
570     m_summary = channel->summary();
571     m_author = channel->author();
572 
573     // PodcastChannel
574     m_url = channel->url();
575     m_webLink = channel->webLink();
576     m_imageUrl = channel->imageUrl();
577     m_labels = channel->labels();
578     m_subscribeDate = channel->subscribeDate();
579     m_copyright = channel->copyright();
580 
581     if( channel->hasImage() )
582         m_image = channel->image();
583 
584     //Default Settings
585 
586     m_directory = QUrl( m_provider->baseDownloadDir() );
587     m_directory = m_directory.adjusted(QUrl::StripTrailingSlash);
588     m_directory.setPath( QDir::toNativeSeparators(m_directory.path() + QLatin1Char('/') + Amarok::vfatPath( m_title )) );
589 
590     m_autoScan = true;
591     m_fetchType = StreamOrDownloadOnDemand;
592     m_purge = false;
593     m_purgeCount = 10;
594     m_writeTags = true;
595 
596     updateInDb();
597 
598     foreach( Podcasts::PodcastEpisodePtr episode, channel->episodes() )
599     {
600         episode->setChannel( PodcastChannelPtr( this ) );
601         SqlPodcastEpisode *sqlEpisode = new SqlPodcastEpisode( episode );
602 
603         m_episodes << SqlPodcastEpisodePtr( sqlEpisode );
604     }
605     m_episodesLoaded = true;
606 }
607 
608 int
trackCount() const609 SqlPodcastChannel::trackCount() const
610 {
611     if( m_episodesLoaded )
612         return m_episodes.count();
613     else
614         return -1;
615 }
616 
617 void
triggerTrackLoad()618 SqlPodcastChannel::triggerTrackLoad()
619 {
620     if( !m_episodesLoaded )
621         loadEpisodes();
622     notifyObserversTracksLoaded();
623 }
624 
625 Playlists::PlaylistProvider *
provider() const626 SqlPodcastChannel::provider() const
627 {
628     return dynamic_cast<Playlists::PlaylistProvider *>( m_provider );
629 }
630 
631 QStringList
groups()632 SqlPodcastChannel::groups()
633 {
634     return m_labels;
635 }
636 
637 void
setGroups(const QStringList & groups)638 SqlPodcastChannel::setGroups( const QStringList &groups )
639 {
640     m_labels = groups;
641 }
642 
643 QUrl
uidUrl() const644 SqlPodcastChannel::uidUrl() const
645 {
646     return QUrl( QStringLiteral( "amarok-sqlpodcastuid://%1").arg( m_dbId ) );
647 }
648 
~SqlPodcastChannel()649 SqlPodcastChannel::~SqlPodcastChannel()
650 {
651     m_episodes.clear();
652 }
653 
654 void
setTitle(const QString & title)655 SqlPodcastChannel::setTitle( const QString &title )
656 {
657     /* also change the savelocation if a title is not set yet.
658        This is a special condition that can happen when first fetching a podcast feed */
659     if( m_title.isEmpty() )
660     {
661         m_directory = m_directory.adjusted(QUrl::StripTrailingSlash);
662         m_directory.setPath( QDir::toNativeSeparators(m_directory.path() + QLatin1Char('/') + Amarok::vfatPath( title )) );
663     }
664     m_title = title;
665 }
666 
667 Podcasts::PodcastEpisodeList
episodes() const668 SqlPodcastChannel::episodes() const
669 {
670     return SqlPodcastEpisode::toPodcastEpisodeList( m_episodes );
671 }
672 
673 void
setImage(const QImage & image)674 SqlPodcastChannel::setImage( const QImage &image )
675 {
676     DEBUG_BLOCK
677 
678     m_image = image;
679 }
680 
681 void
setImageUrl(const QUrl & imageUrl)682 SqlPodcastChannel::setImageUrl( const QUrl &imageUrl )
683 {
684     DEBUG_BLOCK
685     debug() << imageUrl;
686     m_imageUrl = imageUrl;
687 
688     if( imageUrl.isLocalFile() )
689     {
690         m_image = QImage( imageUrl.path() );
691         return;
692     }
693 
694     debug() << "Image is remote, handled by podcastImageFetcher.";
695 }
696 
697 Podcasts::PodcastEpisodePtr
addEpisode(const PodcastEpisodePtr & episode)698 SqlPodcastChannel::addEpisode(const PodcastEpisodePtr &episode )
699 {
700     if( !m_provider )
701         return PodcastEpisodePtr();
702 
703     QUrl checkUrl;
704     //searched in the database for guid or enclosure url
705     if( !episode->guid().isEmpty() )
706         checkUrl = QUrl::fromUserInput(episode->guid());
707     else if( !episode->uidUrl().isEmpty() )
708         checkUrl = QUrl::fromUserInput(episode->uidUrl());
709     else
710         return PodcastEpisodePtr(); //noting to check for
711 
712     if( m_provider->possiblyContainsTrack( checkUrl ) )
713         return PodcastEpisodePtr::dynamicCast( m_provider->trackForUrl( QUrl::fromUserInput(episode->guid()) ) );
714 
715     //force episodes load.
716     if( !m_episodesLoaded )
717         loadEpisodes();
718 
719     SqlPodcastEpisodePtr sqlEpisode;
720 
721     if (SqlPodcastEpisodePtr::dynamicCast( episode ))
722         sqlEpisode = SqlPodcastEpisodePtr( new SqlPodcastEpisode( episode ) );
723     else
724         sqlEpisode = SqlPodcastEpisodePtr( new SqlPodcastEpisode( PodcastChannelPtr(this) , episode ) );
725 
726 
727     //episodes are sorted on pubDate high to low
728     int i;
729     for( i = 0; i < m_episodes.count() ; i++ )
730     {
731         if( sqlEpisode->pubDate() > m_episodes[i]->pubDate() )
732         {
733             m_episodes.insert( i, sqlEpisode );
734             break;
735         }
736     }
737 
738     //insert in case the list is empty or at the end of the list
739     if( i == m_episodes.count() )
740         m_episodes << sqlEpisode;
741 
742     notifyObserversTrackAdded( Meta::TrackPtr::dynamicCast( sqlEpisode ), i );
743 
744     applyPurge();
745     m_trackCacheIsValid = false;
746     return PodcastEpisodePtr::dynamicCast( sqlEpisode );
747 }
748 
749 void
applyPurge()750 SqlPodcastChannel::applyPurge()
751 {
752     DEBUG_BLOCK
753     if( !hasPurge() )
754         return;
755 
756     if( m_episodes.count() > purgeCount() )
757     {
758         int purgeIndex = 0;
759 
760         foreach( SqlPodcastEpisodePtr episode, m_episodes )
761         {
762             if ( !episode->isKeep() )
763             {
764                 if( purgeIndex >= purgeCount() )
765                 {
766                     m_provider->deleteDownloadedEpisode( episode );
767                     m_episodes.removeOne( episode );
768                 }
769                 else
770                     purgeIndex++;
771             }
772         }
773         m_trackCacheIsValid = false;
774     }
775 }
776 
777 void
updateInDb()778 SqlPodcastChannel::updateInDb()
779 {
780     auto sqlStorage = StorageManager::instance()->sqlStorage();
781 
782     QString boolTrue = sqlStorage->boolTrue();
783     QString boolFalse = sqlStorage->boolFalse();
784     #define escape(x) sqlStorage->escape(x)
785     QString command;
786     QTextStream stream( &command );
787     if( m_dbId )
788     {
789         stream << "UPDATE podcastchannels ";
790         stream << "SET url='";
791         stream << escape(m_url.url());
792         stream << "', title='";
793         stream << escape(m_title);
794         stream << "', weblink='";
795         stream << escape(m_webLink.url());
796         stream << "', image='";
797         stream << escape(m_imageUrl.url());
798         stream << "', description='";
799         stream << escape(m_description);
800         stream << "', copyright='";
801         stream << escape(m_copyright);
802         stream << "', directory='";
803         stream << escape(m_directory.url());
804         stream << "', labels='";
805         stream << escape(m_labels.join( QLatin1Char(',') ));
806         stream << "', subscribedate='";
807         stream << escape(m_subscribeDate.toString());
808         stream << "', autoscan=";
809         stream << (m_autoScan ? boolTrue : boolFalse);
810         stream << ", fetchtype=";
811         stream << QString::number(m_fetchType);
812         stream << ", haspurge=";
813         stream << (m_purge ? boolTrue : boolFalse);
814         stream << ", purgecount=";
815         stream << QString::number(m_purgeCount);
816         stream << ", writetags=";
817         stream << (m_writeTags ? boolTrue : boolFalse);
818         stream << ", filenamelayout='";
819         stream << escape(m_filenameLayout);
820         stream << "' WHERE id=";
821         stream << m_dbId;
822         stream << ";";
823         debug() << command;
824         sqlStorage->query( command );
825     }
826     else
827     {
828         stream << "INSERT INTO podcastchannels(";
829         stream << "url,title,weblink,image,description,copyright,directory,labels,";
830         stream << "subscribedate,autoscan,fetchtype,haspurge,purgecount,writetags,filenamelayout) ";
831         stream << "VALUES ( '";
832         stream << escape(m_url.url()) << "', '";
833         stream << escape(m_title) << "', '";
834         stream << escape(m_webLink.url()) << "', '";
835         stream << escape(m_imageUrl.url()) << "', '";
836         stream << escape(m_description) << "', '";
837         stream << escape(m_copyright) << "', '";
838         stream << escape(m_directory.url()) << "', '";
839         stream << escape(m_labels.join( QLatin1Char(',') )) << "', '";
840         stream << escape(m_subscribeDate.toString()) << "', ";
841         stream << (m_autoScan ? boolTrue : boolFalse) << ", ";
842         stream << QString::number(m_fetchType) << ", ";
843         stream << (m_purge ? boolTrue : boolFalse) << ", ";
844         stream << QString::number(m_purgeCount) << ", ";
845         stream << (m_writeTags ? boolTrue : boolFalse) << ", '";
846         stream << escape(m_filenameLayout);
847         stream << "');";
848         debug() << command;
849         m_dbId = sqlStorage->insert( command, QStringLiteral("podcastchannels") );
850     }
851 }
852 
853 void
deleteFromDb()854 SqlPodcastChannel::deleteFromDb()
855 {
856     auto sqlStorage = StorageManager::instance()->sqlStorage();
857     foreach( SqlPodcastEpisodePtr sqlEpisode, m_episodes )
858     {
859        sqlEpisode->deleteFromDb();
860        m_episodes.removeOne( sqlEpisode );
861     }
862     m_trackCacheIsValid = false;
863 
864     sqlStorage->query(
865         QStringLiteral( "DELETE FROM podcastchannels WHERE id = %1;" ).arg( dbId() ) );
866 }
867 
868 void
loadEpisodes()869 SqlPodcastChannel::loadEpisodes()
870 {
871     m_episodes.clear();
872 
873     auto sqlStorage = StorageManager::instance()->sqlStorage();
874 
875     //If purge is enabled we must limit the number of results
876     QString command;
877 
878     int rowLength = 15;
879 
880     //If purge is enabled we must limit the number of results, though there are some files
881     //the user want to be shown even if there is no more slot
882     if( hasPurge() )
883     {
884         command = QString( "(SELECT id, url, channel, localurl, guid, "
885                            "title, subtitle, sequencenumber, description, mimetype, pubdate, "
886                            "duration, filesize, isnew, iskeep FROM podcastepisodes WHERE channel = %1 "
887                            "AND iskeep IS FALSE ORDER BY pubdate DESC LIMIT " + QString::number( purgeCount() ) + ") "
888                            "UNION "
889                            "(SELECT id, url, channel, localurl, guid, "
890                            "title, subtitle, sequencenumber, description, mimetype, pubdate, "
891                            "duration, filesize, isnew, iskeep FROM podcastepisodes WHERE channel = %1 "
892                            "AND iskeep IS TRUE) "
893                            "ORDER BY pubdate DESC;"
894                            );
895     }
896     else
897     {
898         command = QString( "SELECT id, url, channel, localurl, guid, "
899                            "title, subtitle, sequencenumber, description, mimetype, pubdate, "
900                            "duration, filesize, isnew, iskeep FROM podcastepisodes WHERE channel = %1 "
901                            "ORDER BY pubdate DESC;"
902                            );
903     }
904 
905     QStringList results = sqlStorage->query( command.arg( m_dbId ) );
906 
907     for( int i = 0; i < results.size(); i += rowLength )
908     {
909         QStringList episodesResult = results.mid( i, rowLength );
910         SqlPodcastEpisodePtr sqlEpisode = SqlPodcastEpisodePtr(
911                                               new SqlPodcastEpisode(
912                                                   episodesResult,
913                                                   SqlPodcastChannelPtr( this ) ) );
914         m_episodes << sqlEpisode;
915     }
916 
917     m_episodesLoaded = true;
918     m_trackCacheIsValid = false;
919 }
920 
921 Meta::TrackList
tracks()922 Podcasts::SqlPodcastChannel::tracks()
923 {
924     if ( !m_trackCacheIsValid ) {
925         m_episodesAsTracksCache = Podcasts::SqlPodcastEpisode::toTrackList( m_episodes );
926         m_trackCacheIsValid = true;
927     }
928     return m_episodesAsTracksCache;
929 }
930 
931 void
syncTrackStatus(int position,const Meta::TrackPtr & otherTrack)932 Podcasts::SqlPodcastChannel::syncTrackStatus(int position, const Meta::TrackPtr &otherTrack )
933 {
934     Q_UNUSED( position );
935 
936     Podcasts::PodcastEpisodePtr master =
937             Podcasts::PodcastEpisodePtr::dynamicCast( otherTrack );
938 
939     if ( master )
940     {
941         this->setName( master->channel()->name() );
942         this->setTitle( master->channel()->title() );
943         this->setUrl( master->channel()->url() );
944     }
945 }
946 
947 void
addTrack(const Meta::TrackPtr & track,int position)948 Podcasts::SqlPodcastChannel::addTrack( const Meta::TrackPtr &track, int position )
949 {
950     Q_UNUSED( position );
951 
952     addEpisode( Podcasts::PodcastEpisodePtr::dynamicCast( track ) );
953     notifyObserversTrackAdded( track, position );
954 }
955