920 lines
29 KiB
C++
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));
|
|
|
|
}
|