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