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 }