strawberry-audio-player-win.../src/podcasts/podcastservice.cpp

920 lines
29 KiB
C++

/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012-2013, David Sansome <me@davidsansome.com>
* Copyright 2012, 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2013-2014, Krzysztof Sobiecki <sobkas@gmail.com>
* Copyright 2014, Simeon Bird <sbird@andrew.cmu.edu>
* Copyright 2019-2021, Jonas Kvinge <jonas@jkvinge.net>
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
#include <memory>
#include "podcastservice.h"
#include <QObject>
#include <QtConcurrentRun>
#include <QSet>
#include <QMap>
#include <QVariant>
#include <QString>
#include <QIcon>
#include <QDateTime>
#include <QFont>
#include <QStandardItemModel>
#include <QStandardItem>
#include <QSortFilterProxyModel>
#include <QMenu>
#include <QAction>
#include "core/application.h"
#include "core/logging.h"
#include "core/mergedproxymodel.h"
#include "core/iconloader.h"
#include "core/standarditemiconloader.h"
//#include "podcastsmodel.h"
#include "podcastservicemodel.h"
#include "collection/collectionview.h"
#include "opmlcontainer.h"
#include "podcastbackend.h"
#include "podcastdeleter.h"
#include "podcastdownloader.h"
#include "podcastinfodialog.h"
#include "podcastupdater.h"
#include "addpodcastdialog.h"
#include "organize/organizedialog.h"
#include "organize/organizeerrordialog.h"
#include "playlist/playlistmanager.h"
#include "device/devicemanager.h"
#include "device/devicestatefiltermodel.h"
#include "device/deviceview.h"
const char* PodcastService::kServiceName = "Podcasts";
const char *PodcastService::kSettingsGroup = "Podcasts";
class PodcastSortProxyModel : public QSortFilterProxyModel {
public:
explicit PodcastSortProxyModel(QObject *parent = nullptr);
protected:
bool lessThan(const QModelIndex &left, const QModelIndex &right) const;
};
PodcastService::PodcastService(Application *app, QObject *parent)
: InternetService(Song::Source_Unknown, kServiceName, QString(), QString(), SettingsDialog::Page_Appearance, app, parent),
use_pretty_covers_(true),
hide_listened_(false),
show_episodes_(0),
icon_loader_(new StandardItemIconLoader(app->album_cover_loader(), this)),
backend_(app->podcast_backend()),
model_(new PodcastServiceModel(this)),
proxy_(new PodcastSortProxyModel(this)),
root_(nullptr),
organize_dialog_(new OrganizeDialog(app_->task_manager())) {
icon_loader_->SetModel(model_);
proxy_->setSourceModel(model_);
proxy_->setDynamicSortFilter(true);
proxy_->sort(0);
QObject::connect(backend_, &PodcastBackend::SubscriptionAdded, this, &PodcastService::SubscriptionAdded);
QObject::connect(backend_, &PodcastBackend::SubscriptionRemoved, this, &PodcastService::SubscriptionRemoved);
QObject::connect(backend_, &PodcastBackend::EpisodesAdded, this, &PodcastService::EpisodesAdded);
QObject::connect(backend_, &PodcastBackend::EpisodesUpdated, this, &PodcastService::EpisodesUpdated);
QObject::connect(app_->playlist_manager(), &PlaylistManager::CurrentSongChanged, this, &PodcastService::CurrentSongChanged);
QObject::connect(organize_dialog_.get(), &OrganizeDialog::FileCopied, this, &PodcastService::FileCopied);
}
PodcastService::~PodcastService() {}
PodcastSortProxyModel::PodcastSortProxyModel(QObject *parent) : QSortFilterProxyModel(parent) {}
bool PodcastSortProxyModel::lessThan(const QModelIndex &left, const QModelIndex &right) const {
Q_UNUSED(left)
Q_UNUSED(right)
#if 0
const int left_type = left.data(InternetModel::Role_Type).toInt();
const int right_type = right.data(InternetModel::Role_Type).toInt();
// The special Add Podcast item comes first
if (left_type == PodcastService::Type_AddPodcast)
return true;
else if (right_type == PodcastService::Type_AddPodcast)
return false;
// Otherwise we only compare identical typed items.
if (left_type != right_type)
return QSortFilterProxyModel::lessThan(left, right);
switch (left_type) {
case PodcastService::Type_Podcast:
return left.data().toString().localeAwareCompare(right.data().toString()) < 0;
case PodcastService::Type_Episode: {
const PodcastEpisode left_episode = left.data(PodcastService::Role_Episode).value<PodcastEpisode>();
const PodcastEpisode right_episode = right.data(PodcastService::Role_Episode).value<PodcastEpisode>();
return left_episode.publication_date() > right_episode.publication_date();
}
default:
return QSortFilterProxyModel::lessThan(left, right);
}
#endif
return false;
}
QStandardItem *PodcastService::CreateRootItem() {
#if 0
root_ = new QStandardItem(IconLoader::Load("podcast"), tr("Podcasts"));
root_->setData(true, InternetModel::Role_CanLazyLoad);
return root_;
#endif
return nullptr;
}
void PodcastService::CopyToDevice() {
if (selected_episodes_.isEmpty() && explicitly_selected_podcasts_.isEmpty()) {
CopyToDevice(backend_->GetNewDownloadedEpisodes());
}
else {
CopyToDevice(selected_episodes_, explicitly_selected_podcasts_);
}
}
void PodcastService::CopyToDevice(const PodcastEpisodeList &episodes_list) {
SongList songs;
Podcast podcast;
for (const PodcastEpisode &episode : episodes_list) {
podcast = backend_->GetSubscriptionById(episode.podcast_database_id());
songs.append(episode.ToSong(podcast));
}
if (songs.isEmpty()) return;
organize_dialog_->SetDestinationModel(app_->device_manager()->connected_devices_model(), true);
organize_dialog_->SetCopy(true);
if (organize_dialog_->SetSongs(songs)) organize_dialog_->show();
}
void PodcastService::CopyToDevice(const QModelIndexList &episode_indexes, const QModelIndexList &podcast_indexes) {
PodcastEpisodeList episodes;
for (const QModelIndex &idx : episode_indexes) {
PodcastEpisode episode_tmp = idx.data(Role_Episode).value<PodcastEpisode>();
if (episode_tmp.downloaded()) episodes << episode_tmp;
}
for (const QModelIndex &idx : podcast_indexes) {
for (int i = 0; i < idx.model()->rowCount(idx); ++i) {
const QModelIndex &idx2 = idx.model()->index(i, 0, idx);
PodcastEpisode episode_tmp = idx2.data(Role_Episode).value<PodcastEpisode>();
if (episode_tmp.downloaded() && !episode_tmp.listened())
episodes << episode_tmp;
}
}
SongList songs;
for (const PodcastEpisode &episode : episodes) {
Podcast podcast = backend_->GetSubscriptionById(episode.podcast_database_id());
songs.append(episode.ToSong(podcast));
}
organize_dialog_->SetDestinationModel(app_->device_manager()->connected_devices_model(), true);
organize_dialog_->SetCopy(true);
if (organize_dialog_->SetSongs(songs)) organize_dialog_->show();
}
void PodcastService::CancelDownload() {
CancelDownload(selected_episodes_, explicitly_selected_podcasts_);
}
void PodcastService::CancelDownload(const QModelIndexList &episode_indexes, const QModelIndexList &podcast_indexes) {
PodcastEpisodeList episodes;
for (const QModelIndex &idx : episode_indexes) {
if (!idx.isValid()) continue;
PodcastEpisode episode_tmp = idx.data(Role_Episode).value<PodcastEpisode>();
episodes << episode_tmp;
}
for (const QModelIndex &idx : podcast_indexes) {
if (!idx.isValid()) continue;
for (int i = 0; i < idx.model()->rowCount(idx); ++i) {
const QModelIndex &idx2 = idx.model()->index(i, 0, idx);
if (!idx2.isValid()) continue;
PodcastEpisode episode_tmp = idx2.data(Role_Episode).value<PodcastEpisode>();
episodes << episode_tmp;
}
}
episodes = app_->podcast_downloader()->EpisodesDownloading(episodes);
app_->podcast_downloader()->cancelDownload(episodes);
}
void PodcastService::LazyPopulate(QStandardItem *parent) {
Q_UNUSED(parent)
#if 0
switch (parent->data(InternetModel::Role_Type).toInt()) {
case InternetModel::Type_Service:
PopulatePodcastList(model_->invisibleRootItem());
model()->merged_model()->AddSubModel(parent->index(), proxy_);
break;
}
#endif
}
void PodcastService::PopulatePodcastList(QStandardItem *parent) {
// Do this here since the downloader won't be created yet in the ctor.
QObject::connect(app_->podcast_downloader(), &PodcastDownloader::ProgressChanged, this, &PodcastService::DownloadProgressChanged);
if (default_icon_.isNull()) {
default_icon_ = IconLoader::Load("podcast");
}
PodcastList podcasts = backend_->GetAllSubscriptions();
for (const Podcast &podcast : podcasts) {
parent->appendRow(CreatePodcastItem(podcast));
}
}
void PodcastService::ClearPodcastList(QStandardItem *parent) {
parent->removeRows(0, parent->rowCount());
}
void PodcastService::UpdatePodcastText(QStandardItem *item, const int unlistened_count) const {
const Podcast podcast = item->data(Role_Podcast).value<Podcast>();
QString title = podcast.title().simplified();
QFont font;
if (unlistened_count > 0) {
// Add the number of new episodes after the title.
title.append(QString(" (%1)").arg(unlistened_count));
// Set a bold font
font.setBold(true);
}
item->setFont(font);
item->setText(title);
}
void PodcastService::UpdateEpisodeText(QStandardItem *item, const PodcastDownload::State state, const int percent) {
const PodcastEpisode episode = item->data(Role_Episode).value<PodcastEpisode>();
QString title = episode.title().simplified();
QString tooltip;
QFont font;
QIcon icon;
// Unlistened episodes are bold
if (!episode.listened()) {
font.setBold(true);
}
// Downloaded episodes get an icon
if (episode.downloaded()) {
if (downloaded_icon_.isNull()) {
downloaded_icon_ = IconLoader::Load("document-save");
}
icon = downloaded_icon_;
}
// Queued or downloading episodes get icons, tooltips, and maybe a title.
switch (state) {
case PodcastDownload::Queued:
if (queued_icon_.isNull()) {
queued_icon_ = IconLoader::Load("user-away");
}
icon = queued_icon_;
tooltip = tr("Download queued");
break;
case PodcastDownload::Downloading:
if (downloading_icon_.isNull()) {
downloading_icon_ = IconLoader::Load("go-down");
}
icon = downloading_icon_;
tooltip = tr("Downloading (%1%)...").arg(percent);
title = QString("[ %1% ] %2").arg(QString::number(percent), episode.title());
break;
case PodcastDownload::Finished:
case PodcastDownload::NotDownloading:
break;
}
item->setFont(font);
item->setText(title);
item->setIcon(icon);
}
void PodcastService::UpdatePodcastText(QStandardItem *item, const PodcastDownload::State state, const int percent) {
const Podcast podcast = item->data(Role_Podcast).value<Podcast>();
QString tooltip;
QIcon icon;
// Queued or downloading podcasts get icons, tooltips, and maybe a title.
switch (state) {
case PodcastDownload::Queued:
if (queued_icon_.isNull()) {
queued_icon_ = IconLoader::Load("user-away");
}
icon = queued_icon_;
item->setIcon(icon);
tooltip = tr("Download queued");
break;
case PodcastDownload::Downloading:
if (downloading_icon_.isNull()) {
downloading_icon_ = IconLoader::Load("go-down");
}
icon = downloading_icon_;
item->setIcon(icon);
tooltip = tr("Downloading (%1%)...").arg(percent);
break;
case PodcastDownload::Finished:
case PodcastDownload::NotDownloading:
if (podcast.ImageUrlSmall().isValid()) {
icon_loader_->LoadIcon(podcast.ImageUrlSmall(), QUrl(), item);
}
else {
item->setIcon(default_icon_);
}
break;
}
}
QStandardItem *PodcastService::CreatePodcastItem(const Podcast &podcast) {
QStandardItem *item = new QStandardItem;
// Add the episodes in this podcast and gather aggregate stats.
int unlistened_count = 0;
qint64 number = 0;
for (const PodcastEpisode &episode :
backend_->GetEpisodes(podcast.database_id())) {
if (!episode.listened()) {
unlistened_count++;
}
if (episode.listened() && hide_listened_) {
continue;
}
else {
item->appendRow(CreatePodcastEpisodeItem(episode));
++number;
}
if ((number >= show_episodes_) && (show_episodes_ != 0)) {
break;
}
}
item->setIcon(default_icon_);
//item->setData(Type_Podcast, InternetModel::Role_Type);
item->setData(QVariant::fromValue(podcast), Role_Podcast);
item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsDragEnabled | Qt::ItemIsSelectable);
UpdatePodcastText(item, unlistened_count);
// Load the podcast's image if it has one
if (podcast.ImageUrlSmall().isValid()) {
icon_loader_->LoadIcon(podcast.ImageUrlSmall(), QUrl(), item);
}
podcasts_by_database_id_[podcast.database_id()] = item;
return item;
}
QStandardItem *PodcastService::CreatePodcastEpisodeItem(const PodcastEpisode &episode) {
QStandardItem *item = new QStandardItem;
item->setText(episode.title().simplified());
//item->setData(Type_Episode, InternetModel::Role_Type);
item->setData(QVariant::fromValue(episode), Role_Episode);
//item->setData(InternetModel::PlayBehaviour_UseSongLoader, InternetModel::Role_PlayBehaviour);
item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsDragEnabled | Qt::ItemIsSelectable);
UpdateEpisodeText(item);
episodes_by_database_id_[episode.database_id()] = item;
return item;
}
void PodcastService::ShowContextMenu(const QPoint &global_pos) {
if (!context_menu_) {
context_menu_ = new QMenu;
context_menu_->addAction(IconLoader::Load("list-add"), tr("Add podcast..."), this, &PodcastService::AddPodcast);
context_menu_->addAction(IconLoader::Load("view-refresh"), tr("Update all podcasts"), app_->podcast_updater(), &PodcastUpdater::UpdateAllPodcastsNow);
context_menu_->addSeparator();
//context_menu_->addActions(GetPlaylistActions());
context_menu_->addSeparator();
update_selected_action_ = context_menu_->addAction(IconLoader::Load("view-refresh"), tr("Update this podcast"), this, &PodcastService::UpdateSelectedPodcast);
download_selected_action_ = context_menu_->addAction(IconLoader::Load("download"), "", this, &PodcastService::DownloadSelectedEpisode);
info_selected_action_ = context_menu_->addAction(IconLoader::Load("about-info"), tr("Podcast information"), this, &PodcastService::PodcastInfo);
delete_downloaded_action_ = context_menu_->addAction(IconLoader::Load("edit-delete"), tr("Delete downloaded data"), this, &PodcastService::DeleteDownloadedData);
copy_to_device_ = context_menu_->addAction(IconLoader::Load("multimedia-player-ipod-mini-blue"), tr("Copy to device..."), this, QOverload<>::of(&PodcastService::CopyToDevice));
cancel_download_ = context_menu_->addAction(IconLoader::Load("cancel"), tr("Cancel download"), this, QOverload<>::of(&PodcastService::CancelDownload));
remove_selected_action_ = context_menu_->addAction(IconLoader::Load("list-remove"), tr("Unsubscribe"), this, QOverload<>::of(&PodcastService::RemoveSelectedPodcast));
context_menu_->addSeparator();
set_new_action_ = context_menu_->addAction(tr("Mark as new"), this, &PodcastService::SetNew);
set_listened_action_ = context_menu_->addAction(tr("Mark as listened"), this, QOverload<>::of(&PodcastService::SetListened));
context_menu_->addSeparator();
context_menu_->addAction(IconLoader::Load("configure"), tr("Configure podcasts..."), this, &PodcastService::ShowConfig);
copy_to_device_->setDisabled(app_->device_manager()->connected_devices_model()->rowCount() == 0);
connect(app_->device_manager()->connected_devices_model(), &DeviceStateFilterModel::IsEmptyChanged, copy_to_device_, &QAction::setDisabled);
}
selected_episodes_.clear();
selected_podcasts_.clear();
explicitly_selected_podcasts_.clear();
QSet<int> podcast_ids;
#if 0
for (const QModelIndex &index : model()->selected_indexes()) {
switch (index.data(InternetModel::Role_Type).toInt()) {
case Type_Podcast: {
const int id = index.data(Role_Podcast).value<Podcast>().database_id();
if (!podcast_ids.contains(id)) {
selected_podcasts_.append(index);
explicitly_selected_podcasts_.append(index);
podcast_ids.insert(id);
}
break;
}
case Type_Episode: {
selected_episodes_.append(index);
// Add the parent podcast as well.
const QModelIndex parent = index.parent();
const int id = parent.data(Role_Podcast).value<Podcast>().database_id();
if (!podcast_ids.contains(id)) {
selected_podcasts_.append(parent);
podcast_ids.insert(id);
}
break;
}
}
}
#endif
const bool episodes = !selected_episodes_.isEmpty();
const bool podcasts = !selected_podcasts_.isEmpty();
update_selected_action_->setEnabled(podcasts);
remove_selected_action_->setEnabled(podcasts);
set_new_action_->setEnabled(episodes || podcasts);
set_listened_action_->setEnabled(episodes || podcasts);
cancel_download_->setEnabled(episodes || podcasts);
if (selected_episodes_.count() == 1) {
const PodcastEpisode episode = selected_episodes_[0].data(Role_Episode).value<PodcastEpisode>();
const bool downloaded = episode.downloaded();
const bool listened = episode.listened();
download_selected_action_->setEnabled(!downloaded);
delete_downloaded_action_->setEnabled(downloaded);
if (explicitly_selected_podcasts_.isEmpty()) {
set_new_action_->setEnabled(listened);
set_listened_action_->setEnabled(!listened || !episode.listened_date().isValid());
}
}
else {
download_selected_action_->setEnabled(episodes);
delete_downloaded_action_->setEnabled(episodes);
}
if (selected_podcasts_.count() == 1) {
if (selected_episodes_.count() == 1) {
info_selected_action_->setText(tr("Episode information"));
info_selected_action_->setEnabled(true);
}
else {
info_selected_action_->setText(tr("Podcast information"));
info_selected_action_->setEnabled(true);
}
}
else {
info_selected_action_->setText(tr("Podcast information"));
info_selected_action_->setEnabled(false);
}
if (explicitly_selected_podcasts_.isEmpty() && selected_episodes_.isEmpty()) {
PodcastEpisodeList epis = backend_->GetNewDownloadedEpisodes();
set_listened_action_->setEnabled(!epis.isEmpty());
}
if (selected_episodes_.count() > 1) {
download_selected_action_->setText(
tr("Download %n episodes", "", selected_episodes_.count()));
}
else {
download_selected_action_->setText(tr("Download this episode"));
}
//GetAppendToPlaylistAction()->setEnabled(episodes || podcasts);
//GetReplacePlaylistAction()->setEnabled(episodes || podcasts);
//GetOpenInNewPlaylistAction()->setEnabled(episodes || podcasts);
context_menu_->popup(global_pos);
}
void PodcastService::UpdateSelectedPodcast() {
for (const QModelIndex &index : selected_podcasts_) {
app_->podcast_updater()->UpdatePodcastNow(
index.data(Role_Podcast).value<Podcast>());
}
}
void PodcastService::RemoveSelectedPodcast() {
for (const QModelIndex &index : selected_podcasts_) {
backend_->Unsubscribe(index.data(Role_Podcast).value<Podcast>());
}
}
void PodcastService::ReloadSettings() {
InitialLoadSettings();
ClearPodcastList(model_->invisibleRootItem());
PopulatePodcastList(model_->invisibleRootItem());
}
void PodcastService::InitialLoadSettings() {
QSettings s;
s.beginGroup(CollectionSettingsPage::kSettingsGroup);
use_pretty_covers_ = s.value("pretty_covers", true).toBool();
s.endGroup();
s.beginGroup(kSettingsGroup);
hide_listened_ = s.value("hide_listened", false).toBool();
show_episodes_ = s.value("show_episodes", 0).toInt();
s.endGroup();
// TODO(notme): reload the podcast icons that are already loaded?
}
void PodcastService::EnsureAddPodcastDialogCreated() {
add_podcast_dialog_.reset(new AddPodcastDialog(app_));
}
void PodcastService::AddPodcast() {
EnsureAddPodcastDialogCreated();
add_podcast_dialog_->show();
}
void PodcastService::FileCopied(int database_id) {
SetListened(PodcastEpisodeList() << backend_->GetEpisodeById(database_id), true);
}
void PodcastService::SubscriptionAdded(const Podcast &podcast) {
// Ensure the root item is lazy loaded already
LazyLoadRoot();
// The podcast might already be in the list - maybe the LazyLoadRoot() above
// added it.
QStandardItem *item = podcasts_by_database_id_[podcast.database_id()];
if (!item) {
item = CreatePodcastItem(podcast);
model_->appendRow(item);
}
//emit ScrollToIndex(MapToMergedModel(item->index()));
}
void PodcastService::SubscriptionRemoved(const Podcast &podcast) {
QStandardItem *item = podcasts_by_database_id_.take(podcast.database_id());
if (item) {
// Remove any episode ID -> item mappings for the episodes in this podcast.
for (int i = 0; i < item->rowCount(); ++i) {
QStandardItem *episode_item = item->child(i);
const int episode_id = episode_item->data(Role_Episode).value<PodcastEpisode>().database_id();
episodes_by_database_id_.remove(episode_id);
}
// Remove this episode's row
model_->removeRow(item->row());
}
}
void PodcastService::EpisodesAdded(const PodcastEpisodeList &episodes) {
QSet<int> seen_podcast_ids;
for (const PodcastEpisode &episode : episodes) {
const int database_id = episode.podcast_database_id();
QStandardItem *parent = podcasts_by_database_id_[database_id];
if (!parent) continue;
parent->appendRow(CreatePodcastEpisodeItem(episode));
if (!seen_podcast_ids.contains(database_id)) {
// Update the unlistened count text once for each podcast
int unlistened_count = 0;
for (const PodcastEpisode &i : backend_->GetEpisodes(database_id)) {
if (!i.listened()) {
++unlistened_count;
}
}
UpdatePodcastText(parent, unlistened_count);
seen_podcast_ids.insert(database_id);
}
const Podcast podcast = parent->data(Role_Podcast).value<Podcast>();
ReloadPodcast(podcast);
}
}
void PodcastService::EpisodesUpdated(const PodcastEpisodeList &episodes) {
QSet<int> seen_podcast_ids;
QMap<int, Podcast> podcasts_map;
for (const PodcastEpisode &episode : episodes) {
const int podcast_database_id = episode.podcast_database_id();
QStandardItem *item = episodes_by_database_id_[episode.database_id()];
QStandardItem *parent = podcasts_by_database_id_[podcast_database_id];
if (!item || !parent) continue;
// Update the episode data on the item, and update the item's text.
item->setData(QVariant::fromValue(episode), Role_Episode);
UpdateEpisodeText(item);
// Update the parent podcast's text too.
if (!seen_podcast_ids.contains(podcast_database_id)) {
// Update the unlistened count text once for each podcast
int unlistened_count = 0;
for (const PodcastEpisode &i : backend_->GetEpisodes(podcast_database_id)) {
if (!i.listened()) {
++unlistened_count;
}
}
UpdatePodcastText(parent, unlistened_count);
seen_podcast_ids.insert(podcast_database_id);
}
const Podcast podcast = parent->data(Role_Podcast).value<Podcast>();
podcasts_map[podcast.database_id()] = podcast;
}
QList<Podcast> podcast_values = podcasts_map.values();
for (const Podcast &podcast_tmp : podcast_values) {
ReloadPodcast(podcast_tmp);
}
}
void PodcastService::DownloadSelectedEpisode() {
for (const QModelIndex &idx : selected_episodes_) {
app_->podcast_downloader()->DownloadEpisode(idx.data(Role_Episode).value<PodcastEpisode>());
}
}
void PodcastService::PodcastInfo() {
if (selected_podcasts_.isEmpty()) {
// Should never happen.
return;
}
const Podcast podcast = selected_podcasts_[0].data(Role_Podcast).value<Podcast>();
podcast_info_dialog_.reset(new PodcastInfoDialog(app_));
if (selected_episodes_.count() == 1) {
const PodcastEpisode episode = selected_episodes_[0].data(Role_Episode).value<PodcastEpisode>();
podcast_info_dialog_->ShowEpisode(episode, podcast);
}
else {
podcast_info_dialog_->ShowPodcast(podcast);
}
}
void PodcastService::DeleteDownloadedData() {
for (const QModelIndex &idx : selected_episodes_) {
app_->podcast_deleter()->DeleteEpisode(idx.data(Role_Episode).value<PodcastEpisode>());
}
}
void PodcastService::DownloadProgressChanged(const PodcastEpisode &episode, const PodcastDownload::State state, const int percent) {
QStandardItem *item = episodes_by_database_id_[episode.database_id()];
QStandardItem *item2 = podcasts_by_database_id_[episode.podcast_database_id()];
if (!item || !item2) return;
UpdateEpisodeText(item, state, percent);
UpdatePodcastText(item2, state, percent);
}
void PodcastService::ShowConfig() {
//app_->OpenSettingsDialogAtPage(SettingsDialog::Page_Podcasts);
}
void PodcastService::CurrentSongChanged(const Song &metadata) {
// This does two db queries, and we are called on every song change, so run this off the main thread.
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
(void)QtConcurrent::run(&PodcastService::UpdatePodcastListenedStateAsync, this, metadata);
#else
(void)QtConcurrent::run(this, &PodcastService::UpdatePodcastListenedStateAsync, metadata);
#endif
}
void PodcastService::UpdatePodcastListenedStateAsync(const Song &metadata) {
// Check whether this song is one of our podcast episodes.
PodcastEpisode episode = backend_->GetEpisodeByUrlOrLocalUrl(metadata.url());
if (!episode.is_valid()) return;
// Mark it as listened if it's not already
if (!episode.listened() || !episode.listened_date().isValid()) {
episode.set_listened(true);
episode.set_listened_date(QDateTime::currentDateTime());
backend_->UpdateEpisodes(PodcastEpisodeList() << episode);
}
}
void PodcastService::SetNew() {
SetListened(selected_episodes_, explicitly_selected_podcasts_, false);
}
void PodcastService::SetListened() {
if (selected_episodes_.isEmpty() && explicitly_selected_podcasts_.isEmpty()) {
SetListened(backend_->GetNewDownloadedEpisodes(), true);
}
else {
SetListened(selected_episodes_, explicitly_selected_podcasts_, true);
}
}
void PodcastService::SetListened(const PodcastEpisodeList &episodes_list, const bool listened) {
PodcastEpisodeList episodes;
QDateTime current_date_time = QDateTime::currentDateTime();
for (PodcastEpisode episode : episodes_list) {
episode.set_listened(listened);
if (listened) {
episode.set_listened_date(current_date_time);
}
episodes << episode;
}
backend_->UpdateEpisodes(episodes);
}
void PodcastService::SetListened(const QModelIndexList &episode_indexes, const QModelIndexList& podcast_indexes, const bool listened) {
PodcastEpisodeList episodes;
// Get all the episodes from the indexes.
for (const QModelIndex& index : episode_indexes) {
episodes << index.data(Role_Episode).value<PodcastEpisode>();
}
for (const QModelIndex& podcast : podcast_indexes) {
for (int i = 0; i < podcast.model()->rowCount(podcast); ++i) {
const QModelIndex& index = podcast.model()->index(i, 0, podcast);
episodes << index.data(Role_Episode).value<PodcastEpisode>();
}
}
// Update each one with the new state and maybe the listened time.
QDateTime current_date_time = QDateTime::currentDateTime();
for (int i = 0; i < episodes.count(); ++i) {
PodcastEpisode *episode = &episodes[i];
episode->set_listened(listened);
if (listened) {
episode->set_listened_date(current_date_time);
}
}
backend_->UpdateEpisodes(episodes);
}
QModelIndex PodcastService::MapToMergedModel(const QModelIndex &idx) const {
Q_UNUSED(idx)
//return model()->merged_model()->mapFromSource(proxy_->mapFromSource(index));
return QModelIndex();
}
void PodcastService::LazyLoadRoot() {
#if 0
if (root_->data(InternetModel::Role_CanLazyLoad).toBool()) {
root_->setData(false, InternetModel::Role_CanLazyLoad);
LazyPopulate(root_);
}
#endif
}
void PodcastService::SubscribeAndShow(const QVariant &podcast_or_opml) {
if (podcast_or_opml.canConvert<Podcast>()) {
Podcast podcast(podcast_or_opml.value<Podcast>());
backend_->Subscribe(&podcast);
// Lazy load the root item if it hasn't been already
LazyLoadRoot();
QStandardItem *item = podcasts_by_database_id_[podcast.database_id()];
if (item) {
// There will be an item already if this podcast was already there, otherwise it'll be scrolled to when the item is created.
//emit ScrollToIndex(MapToMergedModel(item->index()));
}
}
else if (podcast_or_opml.canConvert<OpmlContainer>()) {
EnsureAddPodcastDialogCreated();
add_podcast_dialog_->ShowWithOpml(podcast_or_opml.value<OpmlContainer>());
}
}
void PodcastService::ReloadPodcast(const Podcast &podcast) {
if (!(hide_listened_ || (show_episodes_ > 0))) {
return;
}
QStandardItem *item = podcasts_by_database_id_[podcast.database_id()];
model_->invisibleRootItem()->removeRow(item->row());
model_->invisibleRootItem()->appendRow(CreatePodcastItem(podcast));
}