/* * Cantata * * Copyright (c) 2011-2013 Craig Drummond * * ---- * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "podcastservice.h" #include "networkaccessmanager.h" #include "onlineservicesmodel.h" #include "musiclibraryitempodcast.h" #include "musiclibraryitemsong.h" #include "utils.h" #include "settings.h" #include "dialog.h" #include "buddylabel.h" #include "mpdconnection.h" #include "config.h" #include #include #include #include #include #include #include #if QT_VERSION >= 0x050000 #include #endif #include class PodcastSettingsDialog : public Dialog { public: PodcastSettingsDialog(QWidget *p) : Dialog(p) { QWidget *mw=new QWidget(this); QFormLayout * lay=new QFormLayout(mw); BuddyLabel *label=new BuddyLabel(i18n("Check for new updates:"), mw); combo = new QComboBox(this); label->setBuddy(combo); lay->setWidget(0, QFormLayout::LabelRole, label); lay->setWidget(0, QFormLayout::FieldRole, combo); setButtons(Ok|Cancel); setMainWidget(mw); setCaption(i18n("Podcast Update")); combo->addItem(i18n("Manually"), 0); combo->addItem(i18n("Every 15 minutes"), 15); combo->addItem(i18n("Every 30 minutes"), 30); combo->addItem(i18n("Every hour"), 60); combo->addItem(i18n("Every 2 hours"), 2*60); combo->addItem(i18n("Every 6 hours"), 6*60); combo->addItem(i18n("Every 12 hours"), 12*60); combo->addItem(i18n("Every day"), 24*60); combo->addItem(i18n("Every week"), 7*24*60); int val=Settings::self()->rssUpdate(); int possible=0; for (int i=0; icount(); ++i) { int cval=combo->itemData(i).toInt(); if (cval==val) { combo->setCurrentIndex(i); possible=-1; break; } if (cval=0) { combo->setCurrentIndex(possible); } } int update() const { return combo->itemData(combo->currentIndex()).toInt(); } private: QComboBox *combo; }; const QString PodcastService::constName=QLatin1String("Podcasts"); QString PodcastService::iconFile; static const char * constNewFeedProperty="new-feed"; // Move files from previous ~/.config/cantata to ~/.local/share/cantata static void moveToNewLocation() { #if !defined Q_OS_WIN && !defined Q_OS_MAC // Not required for windows - as already stored in data location! if (Settings::self()->version() prefixMap; if (prefixMap.isEmpty()) { prefixMap.insert(QLatin1String("fb:"), QLatin1String("http://feeds.feedburner.com/%1")); prefixMap.insert(QLatin1String("yt:"), QLatin1String("http://www.youtube.com/rss/user/%1/videos.rss")); prefixMap.insert(QLatin1String("sc:"), QLatin1String("http://soundcloud.com/%1")); prefixMap.insert(QLatin1String("fm4od:"), QLatin1String("http://onapp1.orf.at/webcam/fm4/fod/%1.xspf")); prefixMap.insert(QLatin1String("ytpl:"), QLatin1String("http://gdata.youtube.com/feeds/api/playlists/%1")); } QMap::ConstIterator it(prefixMap.constBegin()); QMap::ConstIterator end(prefixMap.constEnd()); for (; it!=end; ++it) { if (trimmed.startsWith(it.key())) { trimmed=it.value().arg(trimmed.mid(it.key().length())); } } if (!trimmed.contains(QLatin1String("://"))) { trimmed.prepend(QLatin1String("http://")); } return fixUrl(QUrl(trimmed)); } QUrl PodcastService::fixUrl(const QUrl &orig) { QUrl u=orig; if (u.scheme().isEmpty() || QLatin1String("itpc")==u.scheme() || QLatin1String("pcast")==u.scheme() || QLatin1String("feed")==u.scheme() || QLatin1String("itms")==u.scheme()) { u.setScheme(QLatin1String("http")); } return u; } PodcastService::PodcastService(MusicModel *m) : OnlineService(m, i18n("Podcasts")) , updateTimer(0) { moveToNewLocation(); loaded=true; setUseArtistImages(false); setUseAlbumImages(false); loadAll(); connect(MPDConnection::self(), SIGNAL(currentSongUpdated(const Song &)), this, SLOT(currentMpdSong(const Song &))); if (iconFile.isEmpty()) { #ifdef Q_OS_WIN iconFile=QCoreApplication::applicationDirPath()+"/icons/podcasts.png"; #else iconFile=QString(INSTALL_PREFIX"/share/")+QCoreApplication::applicationName()+"/icons/podcasts.png"; #endif } } Song PodcastService::fixPath(const Song &orig, bool) const { Song song=orig; song.setIsFromOnlineService(constName); return encode(song); } void PodcastService::clear() { cancelAll(); ::OnlineService::clear(); } void PodcastService::loadAll() { QString dir=Utils::dataDir(MusicLibraryItemPodcast::constDir); if (!dir.isEmpty()) { QDir d(dir); QStringList entries=d.entryList(QStringList() << "*"+MusicLibraryItemPodcast::constExt, QDir::Files|QDir::Readable|QDir::NoDot|QDir::NoDotDot); foreach (const QString &e, entries) { if (!update) { update=new MusicLibraryItemRoot(); } MusicLibraryItemPodcast *podcast=new MusicLibraryItemPodcast(dir+e, update); if (podcast->load()) { update->append(podcast); } else { delete podcast; } } if (update) { if (update->childItems().isEmpty()) { delete update; } else { applyUpdate(); } } startTimer(); } } void PodcastService::cancelAll() { foreach (NetworkJob *j, jobs) { disconnect(j, SIGNAL(finished()), this, SLOT(jobFinished())); j->abort(); j->deleteLater(); } jobs.clear(); setBusy(false); } void PodcastService::jobFinished() { NetworkJob *j=dynamic_cast(sender()); if (!j || !jobs.contains(j)) { return; } j->deleteLater(); jobs.removeAll(j); if (!j->ok()) { emitError(i18n("Failed to download %1", j->url().toString())); return; } if (updateUrls.contains(j->url())){ updateUrls.remove(j->url()); if (updateUrls.isEmpty()) { lastRssUpdate=QDateTime::currentDateTime(); Settings::self()->saveLastRssUpdate(lastRssUpdate); startTimer(); } } bool isNew=j->property(constNewFeedProperty).toBool(); MusicLibraryItemPodcast *podcast=new MusicLibraryItemPodcast(QString(), this); if (podcast->loadRss(j->actualJob())) { if (isNew) { podcast->save(); beginInsertRows(index(), childCount(), childCount()); m_childItems.append(podcast); endInsertRows(); emitNeedToSort(); } else { MusicLibraryItemPodcast *orig = getPodcast(j->url()); if (!orig) { delete podcast; return; } QSet origSongs; QSet newSongs; QSet playedSongs; foreach (MusicLibraryItem *i, orig->childItems()) { MusicLibraryItemSong *song=static_cast(i); if (!song->song().podcastPublishedDate().isEmpty()) { origSongs.insert(song->file()); } if (song->song().hasbeenPlayed()) { playedSongs.insert(song->file()); } } foreach (MusicLibraryItem *i, podcast->childItems()) { MusicLibraryItemSong *song=static_cast(i); newSongs.insert(song->file()); } QSet added=newSongs-origSongs; QSet removed=origSongs-newSongs; if (added.count() || removed.count()) { QModelIndex origIndex=createIndex(orig); if (orig->childCount()) { beginRemoveRows(origIndex, 0, orig->childCount()-1); orig->clear(); endRemoveRows(); } if (added.count()) { beginInsertRows(origIndex, 0, podcast->childCount()-1); orig->addAll(podcast); endInsertRows(); } // Restore played status... quint32 played=0; foreach (MusicLibraryItem *i, orig->childItems()) { MusicLibraryItemSong *song=static_cast(i); if (playedSongs.contains(song->file())) { played++; orig->setPlayed(song); playedSongs.remove(song->file()); if (playedSongs.isEmpty()) { break; } } } orig->setUnplayedCount(orig->childCount()-played); orig->save(); emitNeedToSort(); } delete podcast; } } else if (isNew) { delete podcast; emitError(i18n("Failed to parse %1", j->url().toString())); } if (jobs.isEmpty()) { setBusy(false); } } void PodcastService::configure(QWidget *p) { PodcastSettingsDialog dlg(p); if (QDialog::Accepted==dlg.exec()) { int current=Settings::self()->rssUpdate(); if (current!=dlg.update()) { Settings::self()->saveRssUpdate(dlg.update()); startTimer(); } } } MusicLibraryItemPodcast * PodcastService::getPodcast(const QUrl &url) const { foreach (MusicLibraryItem *i, m_childItems) { if (static_cast(i)->rssUrl()==url) { return static_cast(i); } } return 0; } void PodcastService::unSubscribe(MusicLibraryItem *item) { int row=m_childItems.indexOf(item); if (row>=0) { beginRemoveRows(index(), row, row); static_cast(item)->removeFiles(); delete m_childItems.takeAt(row); resetRows(); endRemoveRows(); if (m_childItems.isEmpty()) { stopTimer(); } } } void PodcastService::refreshSubscription(MusicLibraryItem *item) { if (item) { QUrl url=static_cast(item)->rssUrl(); if (processingUrl(url)) { return; } addUrl(url, false); } else { updateRss(); } } bool PodcastService::processingUrl(const QUrl &url) { foreach (NetworkJob *j, jobs) { if (j->url()==url) { return true; } } return false; } void PodcastService::addUrl(const QUrl &url, bool isNew) { setBusy(true); NetworkJob *job=NetworkAccessManager::self()->get(QUrl(url)); connect(job, SIGNAL(finished()), this, SLOT(jobFinished())); job->setProperty(constNewFeedProperty, isNew); jobs.append(job); } void PodcastService::startTimer() { if (0==Settings::self()->rssUpdate() || m_childItems.isEmpty()) { stopTimer(); return; } if (!updateTimer) { updateTimer=new QTimer(this); updateTimer->setSingleShot(true); connect(updateTimer, SIGNAL(timeout()), this, SLOT(updateRss())); } if (!lastRssUpdate.isValid()) { lastRssUpdate=Settings::self()->lastRssUpdate(); } if (!lastRssUpdate.isValid()) { updateRss(); } else { QDateTime nextUpdate = lastRssUpdate.addSecs(Settings::self()->rssUpdate()*60); int secsUntilNextUpdate = QDateTime::currentDateTime().secsTo(nextUpdate); if (secsUntilNextUpdate<0) { // Oops, missed update time!!! updateRss(); } else { updateTimer->start(secsUntilNextUpdate*1000ll); } } } void PodcastService::stopTimer() { if (updateTimer) { updateTimer->stop(); } } void PodcastService::updateRss() { foreach (MusicLibraryItem *i, m_childItems) { QUrl url=static_cast(i)->rssUrl(); updateUrls.insert(url); if (!processingUrl(url)) { addUrl(url, false); } } } void PodcastService::currentMpdSong(const Song &s) { if (s.isFromOnlineService() && s.album==constName) { foreach (MusicLibraryItem *p, m_childItems) { MusicLibraryItemPodcast *podcast=static_cast(p); foreach (MusicLibraryItem *i, podcast->childItems()) { MusicLibraryItemSong *song=static_cast(i); if (song->file()==s.file) { if (!song->song().hasbeenPlayed()) { podcast->setPlayed(song); emitDataChanged(createIndex(song)); emitDataChanged(createIndex(podcast)); podcast->save(); } return; } } } } }