/* * Strawberry Music Player * This file was part of Clementine. * Copyright 2012, David Sansome * Copyright 2014, John Maguire * Copyright 2014, Krzysztof Sobiecki * Copyright 2019-2021, Jonas Kvinge * * Strawberry 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 3 of the License, or * (at your option) any later version. * * Strawberry 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 Strawberry. If not, see . * */ #include #include #include #include #include #include #include #include #include #include #include #include "core/application.h" #include "core/logging.h" #include "core/networkaccessmanager.h" #include "core/timeconstants.h" #include "core/utilities.h" #include "podcastbackend.h" #include "podcasturlloader.h" #include "gpoddersync.h" const char *GPodderSync::kSettingsGroup = "Podcasts"; const int GPodderSync::kFlushUpdateQueueDelay = 30 * kMsecPerSec; // 30 seconds const int GPodderSync::kGetUpdatesInterval = 30 * 60 * kMsecPerSec; // 30 minutes const int GPodderSync::kRequestTimeout = 30 * kMsecPerSec; // 30 seconds GPodderSync::GPodderSync(Application *app, QObject *parent) : QObject(parent), app_(app), network_(new NetworkAccessManager(this)), backend_(app_->podcast_backend()), loader_(new PodcastUrlLoader(this)), get_updates_timer_(new QTimer(this)), flush_queue_timer_(new QTimer(this)), flushing_queue_(false) { ReloadSettings(); LoadQueue(); QObject::connect(app_, &Application::SettingsChanged, this, &GPodderSync::ReloadSettings); QObject::connect(backend_, &PodcastBackend::SubscriptionAdded, this, &GPodderSync::SubscriptionAdded); QObject::connect(backend_, &PodcastBackend::SubscriptionRemoved, this, &GPodderSync::SubscriptionRemoved); get_updates_timer_->setInterval(kGetUpdatesInterval); connect(get_updates_timer_, &QTimer::timeout, this, &GPodderSync::GetUpdatesNow); flush_queue_timer_->setInterval(kFlushUpdateQueueDelay); flush_queue_timer_->setSingleShot(true); QObject::connect(flush_queue_timer_, &QTimer::timeout, this, &GPodderSync::FlushUpdateQueue); if (is_logged_in()) { GetUpdatesNow(); flush_queue_timer_->start(); get_updates_timer_->start(); } } GPodderSync::~GPodderSync() {} QString GPodderSync::DeviceId() { return QString("%1-%2").arg(qApp->applicationName(), QHostInfo::localHostName()).toLower(); } QString GPodderSync::DefaultDeviceName() { return tr("%1 on %2").arg(qApp->applicationName(), QHostInfo::localHostName()); } bool GPodderSync::is_logged_in() const { return !username_.isEmpty() && !password_.isEmpty() && api_; } void GPodderSync::ReloadSettings() { QSettings s; s.beginGroup(kSettingsGroup); username_ = s.value("gpodder_username").toString(); password_ = s.value("gpodder_password").toString(); last_successful_get_ = s.value("gpodder_last_get").toDateTime(); s.endGroup(); if (!username_.isEmpty() && !password_.isEmpty()) { api_.reset(new mygpo::ApiRequest(username_, password_, network_)); } } void GPodderSync::Login(const QString &username, const QString &password, const QString &device_name) { api_.reset(new mygpo::ApiRequest(username, password, network_)); QNetworkReply *reply = api_->renameDevice(username, DeviceId(), device_name, mygpo::Device::DESKTOP); QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, username, password]() { LoginFinished(reply, username, password); }); } void GPodderSync::LoginFinished(QNetworkReply *reply, const QString &username, const QString &password) { reply->deleteLater(); if (reply->error() == QNetworkReply::NoError) { username_ = username; password_ = password; QSettings s; s.beginGroup(kSettingsGroup); s.setValue("gpodder_username", username); s.setValue("gpodder_password", password); s.endGroup(); DoInitialSync(); emit LoginSuccess(); } else { api_.reset(); emit LoginFailure(reply->errorString()); } } void GPodderSync::Logout() { QSettings s; s.beginGroup(kSettingsGroup); s.remove("gpodder_username"); s.remove("gpodder_password"); s.remove("gpodder_last_get"); s.endGroup(); api_.reset(); // Remove session cookies. QNetworkAccessManager takes ownership of the new object and frees the previous. network_->setCookieJar(new QNetworkCookieJar()); } void GPodderSync::GetUpdatesNow() { if (!is_logged_in()) return; qlonglong timestamp = 0; if (last_successful_get_.isValid()) { timestamp = last_successful_get_.toSecsSinceEpoch(); } mygpo::DeviceUpdatesPtr reply(api_->deviceUpdates(username_, DeviceId(), timestamp)); QObject::connect(reply.data(), &mygpo::DeviceUpdates::finished, this, [this, reply]() { DeviceUpdatesFinished(reply); }); QObject::connect(reply.data(), &mygpo::DeviceUpdates::parseError, this, &GPodderSync::DeviceUpdatesParseError); QObject::connect(reply.data(), &mygpo::DeviceUpdates::requestError, this, &GPodderSync::DeviceUpdatesRequestError); } void GPodderSync::DeviceUpdatesParseError() { qLog(Warning) << "Failed to get gpodder device updates: parse error"; } void GPodderSync::DeviceUpdatesRequestError(QNetworkReply::NetworkError error) { qLog(Warning) << "Failed to get gpodder device updates:" << error; } void GPodderSync::DeviceUpdatesFinished(mygpo::DeviceUpdatesPtr reply) { // Remember episode actions for each podcast, so when we add a new podcast // we can apply the actions immediately. QMap> episodes_by_podcast; for (mygpo::EpisodePtr episode : reply->updateList()) { episodes_by_podcast[episode->podcastUrl()].append(episode); } for (mygpo::PodcastPtr podcast : reply->addList()) { const QUrl url(podcast->url()); // Are we subscribed to this podcast already? Podcast existing_podcast = backend_->GetSubscriptionByUrl(url); if (existing_podcast.is_valid()) { // Just apply actions to this existing podcast ApplyActions(episodes_by_podcast[url], existing_podcast.mutable_episodes()); backend_->UpdateEpisodes(existing_podcast.episodes()); continue; } // Start loading the podcast. Remember actions and apply them after we have a list of the episodes. PodcastUrlLoaderReply *loader_reply = loader_->Load(url); QObject::connect(loader_reply, &PodcastUrlLoaderReply::Finished, this, [this, loader_reply, url, episodes_by_podcast]() { NewPodcastLoaded(loader_reply, url, episodes_by_podcast[url]); }); } // Unsubscribe from podcasts that were removed. for (const QUrl &url : reply->removeList()) { backend_->Unsubscribe(backend_->GetSubscriptionByUrl(url)); } last_successful_get_ = QDateTime::currentDateTime(); QSettings s; s.beginGroup(kSettingsGroup); s.setValue("gpodder_last_get", last_successful_get_); s.endGroup(); } void GPodderSync::NewPodcastLoaded(PodcastUrlLoaderReply *reply, const QUrl &url, const QList &actions) { reply->deleteLater(); if (!reply->is_success()) { qLog(Warning) << "Error fetching podcast at" << url << ":" << reply->error_text(); return; } if (reply->result_type() != PodcastUrlLoaderReply::Type_Podcast) { qLog(Warning) << "The URL" << url << "no longer contains a podcast"; return; } // Apply the actions to the episodes in the podcast. for (Podcast podcast : reply->podcast_results()) { ApplyActions(actions, podcast.mutable_episodes()); // Add the subscription backend_->Subscribe(&podcast); } } void GPodderSync::ApplyActions(const QList> &actions, PodcastEpisodeList *episodes) { for (PodcastEpisodeList::iterator it = episodes->begin(); it != episodes->end(); ++it) { // Find an action for this episode for (mygpo::EpisodePtr action : actions) { if (action->url() != it->url()) continue; switch (action->status()) { case mygpo::Episode::PLAY: case mygpo::Episode::DOWNLOAD: it->set_listened(true); break; default: break; } break; } } } void GPodderSync::SubscriptionAdded(const Podcast &podcast) { if (!is_logged_in()) return; const QUrl &url = podcast.url(); queued_remove_subscriptions_.remove(url); queued_add_subscriptions_.insert(url); SaveQueue(); flush_queue_timer_->start(); } void GPodderSync::SubscriptionRemoved(const Podcast &podcast) { if (!is_logged_in()) return; const QUrl &url = podcast.url(); queued_remove_subscriptions_.insert(url); queued_add_subscriptions_.remove(url); SaveQueue(); flush_queue_timer_->start(); } namespace { template void WriteContainer(const T &container, QSettings *s, const char *array_name, const char *item_name) { s->beginWriteArray(array_name, container.count()); int index = 0; for (const auto &item : container) { s->setArrayIndex(index++); s->setValue(item_name, item); } s->endArray(); } template void ReadContainer(T *container, QSettings *s, const char *array_name, const char *item_name) { container->clear(); const int count = s->beginReadArray(array_name); for (int i = 0; i < count; ++i) { s->setArrayIndex(i); *container << s->value(item_name).value(); } s->endArray(); } } // namespace void GPodderSync::SaveQueue() { QSettings s; s.beginGroup(kSettingsGroup); WriteContainer(queued_add_subscriptions_, &s, "gpodder_queued_add_subscriptions", "url"); WriteContainer(queued_remove_subscriptions_, &s, "gpodder_queued_remove_subscriptions", "url"); s.endGroup(); } void GPodderSync::LoadQueue() { QSettings s; s.beginGroup(kSettingsGroup); ReadContainer(&queued_add_subscriptions_, &s, "gpodder_queued_add_subscriptions", "url"); ReadContainer(&queued_remove_subscriptions_, &s, "gpodder_queued_remove_subscriptions", "url"); s.endGroup(); } void GPodderSync::FlushUpdateQueue() { if (!is_logged_in() || flushing_queue_) return; QSet all_urls = queued_add_subscriptions_ + queued_remove_subscriptions_; if (all_urls.isEmpty()) return; flushing_queue_ = true; mygpo::AddRemoveResultPtr reply(api_->addRemoveSubscriptions(username_, DeviceId(), queued_add_subscriptions_.values(), queued_remove_subscriptions_.values())); qLog(Info) << "Sending" << all_urls.count() << "changes to gpodder.net"; QObject::connect(reply.data(), &mygpo::AddRemoveResult::finished, this, [this, all_urls]() { AddRemoveFinished(all_urls.values()); }); QObject::connect(reply.data(), &mygpo::AddRemoveResult::parseError, this, &GPodderSync::AddRemoveParseError); QObject::connect(reply.data(), &mygpo::AddRemoveResult::requestError, this, &GPodderSync::AddRemoveRequestError); } void GPodderSync::AddRemoveParseError() { flushing_queue_ = false; qLog(Warning) << "Failed to update gpodder subscriptions: parse error"; } void GPodderSync::AddRemoveRequestError(QNetworkReply::NetworkError err) { flushing_queue_ = false; qLog(Warning) << "Failed to update gpodder subscriptions:" << err; } void GPodderSync::AddRemoveFinished(const QList &affected_urls) { flushing_queue_ = false; // Remove the URLs from the queue. for (const QUrl &url : affected_urls) { queued_add_subscriptions_.remove(url); queued_remove_subscriptions_.remove(url); } SaveQueue(); // Did more change in the mean time? if (!queued_add_subscriptions_.isEmpty() || !queued_remove_subscriptions_.isEmpty()) { flush_queue_timer_->start(); } } void GPodderSync::DoInitialSync() { // Get updates from the server GetUpdatesNow(); get_updates_timer_->start(); // Send our complete list of subscriptions queued_remove_subscriptions_.clear(); queued_add_subscriptions_.clear(); for (const Podcast &podcast : backend_->GetAllSubscriptions()) { queued_add_subscriptions_.insert(podcast.url()); } SaveQueue(); FlushUpdateQueue(); }