Compare commits

..

16 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
07900f1265 Extract retry logging into helper method and fix formatting
- Add LogRetryAttempt() helper method for consistent logging
- Fix formatting in ShouldRetryRequest() for better readability
- Use helper method in both GetRecentTracksRequestFinished and GetTopTracksRequestFinished
- Eliminates duplicate logging code

Co-authored-by: jonaski <10343810+jonaski@users.noreply.github.com>
2026-01-03 21:30:38 +00:00
copilot-swe-agent[bot]
c25d8a5e6c Improve retry logic safety and readability
- Add named constants for retry-eligible HTTP status codes (500, 503)
- Add bounds checking in backoff calculation to prevent integer overflow
- Use kMaxBackoffShift constant to limit bit shift operations
- Improves code safety and readability

Co-authored-by: jonaski <10343810+jonaski@users.noreply.github.com>
2026-01-03 21:29:11 +00:00
copilot-swe-agent[bot]
8bdbeeb5a8 Refactor retry logic to reduce code duplication
- Extract retry condition check into ShouldRetryRequest() helper
- Extract backoff delay calculation into CalculateBackoffDelay() helper
- Use helper methods in both GetRecentTracksRequestFinished and GetTopTracksRequestFinished
- Improves code maintainability and consistency

Co-authored-by: jonaski <10343810+jonaski@users.noreply.github.com>
2026-01-03 21:27:23 +00:00
copilot-swe-agent[bot]
4c8103ef6d Add custom API key support and retry logic for Last.fm import
- Add API key field to Last.fm settings UI with helpful info text
- Store and load custom API key from settings
- Use custom API key in lastfmimport if provided, fall back to default
- Implement exponential backoff retry logic (up to 5 retries)
- Retry on HTTP 500/503 errors with increasing delays (5s, 10s, 20s, 40s, 80s)
- Add retry count tracking to request structures

Co-authored-by: jonaski <10343810+jonaski@users.noreply.github.com>
2026-01-03 21:24:41 +00:00
copilot-swe-agent[bot]
8a7a22e9bd Initial plan 2026-01-03 21:12:16 +00:00
Marcus Müller
17519076f5 Include .webp in allowed extensions
Modern Qt can read and write webp out of the box, no use excluding that.

Signed-off-by: Marcus Müller <mueller@baseband.digital>
2026-01-03 16:55:29 +01:00
Jonas Kvinge
e8d9e1172f FileViewTreeModel: Add const 2026-01-03 16:09:56 +01:00
Alexopus
aac8d4e68b Add file tree view 2026-01-03 15:11:56 +01:00
dependabot[bot]
0e28e800b3 Bump vmactions/freebsd-vm from 1.3.4 to 1.3.5
Bumps [vmactions/freebsd-vm](https://github.com/vmactions/freebsd-vm) from 1.3.4 to 1.3.5.
- [Release notes](https://github.com/vmactions/freebsd-vm/releases)
- [Commits](https://github.com/vmactions/freebsd-vm/compare/v1.3.4...v1.3.5)

---
updated-dependencies:
- dependency-name: vmactions/freebsd-vm
  dependency-version: 1.3.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-02 17:33:02 +01:00
Jonas Kvinge
cf84bc29ab CI: Manually codesign 2026-01-01 01:51:10 +01:00
Jonas Kvinge
afc3effc9d CI: Switch macOS dependencies repo 2025-12-30 20:01:34 +01:00
Jonas Kvinge
370bebff5f CollectionView: Fix Enter/Return behavior to respect double-click settings
Fixes #1691
2025-12-30 19:08:52 +01:00
Jonas Kvinge
db410cc257 MainWindow: Remove unused declaration 2025-12-29 22:14:08 +01:00
Jonas Kvinge
20a9946e51 Song: Prefer filenames with "front" or "cover" for art automatic
Fixes #1745
2025-12-29 21:16:06 +01:00
dependabot[bot]
b6c8ff19af Bump vmactions/freebsd-vm from 1.3.2 to 1.3.4
Bumps [vmactions/freebsd-vm](https://github.com/vmactions/freebsd-vm) from 1.3.2 to 1.3.4.
- [Release notes](https://github.com/vmactions/freebsd-vm/releases)
- [Commits](https://github.com/vmactions/freebsd-vm/compare/v1.3.2...v1.3.4)

---
updated-dependencies:
- dependency-name: vmactions/freebsd-vm
  dependency-version: 1.3.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-29 18:18:55 +01:00
dependabot[bot]
80d058af10 Bump vmactions/openbsd-vm from 1.2.9 to 1.3.1
Bumps [vmactions/openbsd-vm](https://github.com/vmactions/openbsd-vm) from 1.2.9 to 1.3.1.
- [Release notes](https://github.com/vmactions/openbsd-vm/releases)
- [Commits](https://github.com/vmactions/openbsd-vm/compare/v1.2.9...v1.3.1)

---
updated-dependencies:
- dependency-name: vmactions/openbsd-vm
  dependency-version: 1.3.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-29 17:23:16 +01:00
84 changed files with 1169 additions and 8514 deletions

View File

@@ -747,7 +747,7 @@ jobs:
df -h
- name: Build FreeBSD
id: build-freebsd
uses: vmactions/freebsd-vm@v1.3.2
uses: vmactions/freebsd-vm@v1.3.5
with:
usesh: true
mem: 8192
@@ -772,7 +772,7 @@ jobs:
submodules: recursive
- name: Build OpenBSD
id: build-openbsd
uses: vmactions/openbsd-vm@v1.2.9
uses: vmactions/openbsd-vm@v1.3.1
with:
usesh: true
mem: 4096
@@ -845,7 +845,7 @@ jobs:
p12-password: ${{ secrets.APPLE_DEVELOPER_ID_CERTIFICATE_PASSWORD }}
- name: Download macOS dependencies
run: curl -f -O -L https://github.com/strawberrymusicplayer/strawberry-macos-dependencies$(test "${{env.arch}}" = "x86_64" && echo "-intel" || echo "")/releases/latest/download/strawberry-macos-${{env.arch}}-${{env.buildtype}}.tar.xz
run: curl -f -O -L https://github.com/strawberrymusicplayer/strawberry-macos-dependencies/releases/latest/download/strawberry-macos-${{env.arch}}-${{env.buildtype}}.tar.xz
- name: Extract macOS dependencies
run: sudo tar -C / -xf strawberry-macos-${{env.arch}}-${{env.buildtype}}.tar.xz
@@ -898,7 +898,7 @@ jobs:
- name: Manually Codesign
if: github.repository == 'strawberrymusicplayer/strawberry' && github.event.pull_request.head.repo.fork == false && matrix.runner == 'macos-15-intel'
working-directory: build
run: codesign -s 383J84DVB6 -f strawberry.app/Contents/Frameworks/{libpcre2-8.0.dylib,libpcre2-16.0.dylib,libpng16.16.dylib,libfreetype.6.dylib,libzstd.1.dylib,libbrotlicommon.1.dylib,libbrotlienc.1.dylib} strawberry.app/Contents/Frameworks/png.framework/png strawberry.app
run: codesign -s 383J84DVB6 -f strawberry.app/Contents/Frameworks/{libpcre2-8.0.dylib,libpcre2-16.0.dylib,libpng16.16.dylib,libfreetype.6.dylib,libzstd.1.dylib,libbrotlicommon.1.dylib,libbrotlienc.1.dylib,libbrotlidec.1.dylib,libsoup-3.0.0.dylib,libnghttp2.14.dylib,libpsl.5.dylib} strawberry.app/Contents/Frameworks/png.framework/png strawberry.app
- name: Manually Codesign
if: github.repository == 'strawberrymusicplayer/strawberry' && github.event.pull_request.head.repo.fork == false && matrix.runner == 'macos-15'

View File

@@ -208,8 +208,6 @@ else()
pkg_check_modules(TAGLIB REQUIRED IMPORTED_TARGET taglib>=1.12)
endif()
pkg_check_modules(LIBMYGPO libmygpo-qt6)
find_package(GTest)
pkg_check_modules(LIBSPARSEHASH IMPORTED_TARGET libsparsehash)
@@ -383,8 +381,6 @@ optional_component(DISCORD_RPC ON "Discord Rich Presence"
DEPENDS "RapidJSON" RapidJSON_FOUND
)
optional_component(PODCASTS ON "Podcasts support" DEPENDS "libmygpo" LIBMYGPO_FOUND)
if(HAVE_SONGFINGERPRINTING OR HAVE_MUSICBRAINZ)
set(HAVE_CHROMAPRINT ON)
endif()
@@ -827,6 +823,8 @@ set(SOURCES
src/fileview/fileview.cpp
src/fileview/fileviewlist.cpp
src/fileview/fileviewtree.cpp
src/fileview/fileviewtreemodel.cpp
src/device/devicemanager.cpp
src/device/devicelister.cpp
@@ -1116,6 +1114,8 @@ set(HEADERS
src/fileview/fileview.h
src/fileview/fileviewlist.h
src/fileview/fileviewtree.h
src/fileview/fileviewtreemodel.h
src/device/devicemanager.h
src/device/devicelister.h
@@ -1484,69 +1484,6 @@ optional_source(HAVE_QOBUZ
src/settings/qobuzsettingspage.ui
)
optional_source(HAVE_PODCASTS
SOURCES
podcasts/gpoddersync.cpp
podcasts/gpoddertoptagsmodel.cpp
podcasts/gpoddertoptagspage.cpp
podcasts/itunessearchpage.cpp
podcasts/podcastbackend.cpp
podcasts/podcastservice.cpp
podcasts/podcast.cpp
podcasts/podcastdownloader.cpp
podcasts/podcastupdater.cpp
podcasts/podcastdeleter.cpp
podcasts/podcastdiscoverymodel.cpp
podcasts/podcastepisode.cpp
podcasts/podcastinfodialog.cpp
podcasts/podcastinfowidget.cpp
podcasts/podcastparser.cpp
podcasts/podcastservicemodel.cpp
podcasts/podcasturlloader.cpp
podcasts/gpoddersearchpage.cpp
podcasts/addpodcastbyurl.cpp
podcasts/addpodcastdialog.cpp
podcasts/addpodcastpage.cpp
podcasts/episodeinfowidget.cpp
podcasts/fixedopmlpage.cpp
settings/podcastsettingspage.cpp
HEADERS
podcasts/addpodcastbyurl.h
podcasts/addpodcastdialog.h
podcasts/addpodcastpage.h
podcasts/episodeinfowidget.h
podcasts/fixedopmlpage.h
podcasts/gpoddersync.h
podcasts/gpoddertoptagsmodel.h
podcasts/gpoddertoptagspage.h
podcasts/itunessearchpage.h
podcasts/opmlcontainer.h
podcasts/podcastbackend.h
podcasts/podcastdeleter.h
podcasts/podcastdiscoverymodel.h
podcasts/podcastdownloader.h
podcasts/podcastepisode.h
podcasts/podcast.h
podcasts/podcastinfodialog.h
podcasts/podcastinfowidget.h
podcasts/podcastparser.h
podcasts/podcastservice.h
podcasts/podcastservicemodel.h
podcasts/podcastupdater.h
podcasts/podcasturlloader.h
podcasts/gpoddersearchpage.h
settings/podcastsettingspage.h
UI
podcasts/addpodcastbyurl.ui
podcasts/addpodcastdialog.ui
podcasts/episodeinfowidget.ui
podcasts/itunessearchpage.ui
podcasts/podcastinfodialog.ui
podcasts/podcastinfowidget.ui
podcasts/gpoddersearchpage.ui
settings/podcastsettingspage.ui
)
qt_wrap_cpp(SOURCES ${HEADERS})
qt_wrap_ui(SOURCES ${UI})
qt_add_resources(SOURCES data/data.qrc data/icons.qrc)

View File

@@ -383,7 +383,7 @@ void CollectionView::keyPressEvent(QKeyEvent *e) {
case Qt::Key_Enter:
case Qt::Key_Return:
if (currentIndex().isValid()) {
AddToPlaylist();
Q_EMIT doubleClicked(currentIndex());
}
e->accept();
break;

View File

@@ -33,7 +33,6 @@
#cmakedefine HAVE_SPOTIFY
#cmakedefine HAVE_QOBUZ
#cmakedefine HAVE_DISCORD_RPC
#cmakedefine HAVE_PODCASTS
#cmakedefine HAVE_TAGLIB_DSFFILE
#cmakedefine HAVE_TAGLIB_DSDIFFFILE

View File

@@ -33,7 +33,7 @@ constexpr char kFileFilter[] =
"*.mod *.s3m *.xm *.it "
"*.spc *.vgm";
constexpr char kLoadImageFileFilter[] = QT_TRANSLATE_NOOP("FileFilter", "Images (*.png *.jpg *.jpeg *.bmp *.gif *.xpm *.pbm *.pgm *.ppm *.xbm)");
constexpr char kSaveImageFileFilter[] = QT_TRANSLATE_NOOP("FileFilter", "Images (*.png *.jpg *.jpeg *.bmp *.xpm *.pbm *.ppm *.xbm)");
constexpr char kLoadImageFileFilter[] = QT_TRANSLATE_NOOP("FileFilter", "Images (*.png *.jpg *.jpeg *.bmp *.gif *.xpm *.pbm *.pgm *.ppm *.xbm *.webp)");
constexpr char kSaveImageFileFilter[] = QT_TRANSLATE_NOOP("FileFilter", "Images (*.png *.jpg *.jpeg *.bmp *.xpm *.pbm *.ppm *.xbm *.webp)");
#endif // FILEFILTERCONSTANTS_H

View File

@@ -34,6 +34,7 @@ constexpr char kShowErrorDialog[] = "show_error_dialog";
constexpr char kStripRemastered[] = "strip_remastered";
constexpr char kSources[] = "sources";
constexpr char kUserToken[] = "user_token";
constexpr char kApiKey[] = "api_key";
} // namespace ScrobblerSettings

View File

@@ -113,16 +113,7 @@
#include "radios/radioservices.h"
#include "radios/radiobackend.h"
#ifdef HAVE_PODCASTS
# include "podcasts/podcastbackend.h"
# include "podcasts/gpoddersync.h"
# include "podcasts/podcastdownloader.h"
# include "podcasts/podcastupdater.h"
# include "podcasts/podcastdeleter.h"
#endif
using std::make_shared;
using namespace std::chrono_literals;
class ApplicationImpl {
@@ -225,21 +216,6 @@ class ApplicationImpl {
#ifdef HAVE_MOODBAR
moodbar_loader_([app]() { return new MoodbarLoader(app); }),
moodbar_controller_([app]() { return new MoodbarController(app->player(), app->moodbar_loader()); }),
#endif
#ifdef HAVE_PODCASTS
podcast_backend_([app]() {
PodcastBackend* backend = new PodcastBackend(app, app);
app->MoveToThread(backend, database_->thread());
return backend;
}),
gpodder_sync_([app]() { return new GPodderSync(app, app); }),
podcast_downloader_([app]() { return new PodcastDownloader(app, app); }),
podcast_updater_([app]() { return new PodcastUpdater(app, app); }),
podcast_deleter_([app]() {
PodcastDeleter* deleter = new PodcastDeleter(app, app);
app->MoveToNewThread(deleter);
return deleter;
}),
#endif
lastfm_import_([app]() { return new LastFMImport(app->network()); })
{}
@@ -265,13 +241,6 @@ class ApplicationImpl {
#ifdef HAVE_MOODBAR
Lazy<MoodbarLoader> moodbar_loader_;
Lazy<MoodbarController> moodbar_controller_;
#endif
#ifdef HAVE_PODCASTS
Lazy<PodcastBackend> podcast_backend_;
Lazy<GPodderSync> gpodder_sync_;
Lazy<PodcastDownloader> podcast_downloader_;
Lazy<PodcastUpdater> podcast_updater_;
Lazy<PodcastDeleter> podcast_deleter_;
#endif
Lazy<LastFMImport> lastfm_import_;
@@ -421,10 +390,3 @@ SharedPtr<LastFMImport> Application::lastfm_import() const { return p_->lastfm_i
SharedPtr<MoodbarController> Application::moodbar_controller() const { return p_->moodbar_controller_.ptr(); }
SharedPtr<MoodbarLoader> Application::moodbar_loader() const { return p_->moodbar_loader_.ptr(); }
#endif
#ifdef HAVE_PODCASTS
PodcastBackend *Application::podcast_backend() const { return p_->podcast_backend_.get(); }
GPodderSync *Application::gpodder_sync() const { return p_->gpodder_sync_.get(); }
PodcastDownloader *Application::podcast_downloader() const { return p_->podcast_downloader_.get(); }
PodcastUpdater *Application::podcast_updater() const { return p_->podcast_updater_.get(); }
PodcastDeleter *Application::podcast_deleter() const { return p_->podcast_deleter_.get(); }
#endif

View File

@@ -63,14 +63,6 @@ class RadioServices;
class MoodbarController;
class MoodbarLoader;
#endif
#ifdef HAVE_PODCASTS
class PodcastBackend;
class GPodderSync;
class PodcastDownloader;
class PodcastUpdater;
class PodcastDeleter;
#endif
class Application : public QObject {
Q_OBJECT
@@ -110,13 +102,6 @@ class Application : public QObject {
SharedPtr<MoodbarController> moodbar_controller() const;
SharedPtr<MoodbarLoader> moodbar_loader() const;
#endif
#ifdef HAVE_PODCASTS
PodcastBackend *podcast_backend() const;
GPodderSync *gpodder_sync() const;
PodcastDownloader *podcast_downloader() const;
PodcastUpdater *podcast_updater() const;
PodcastDeleter *podcast_deleter() const;
#endif
SharedPtr<LastFMImport> lastfm_import() const;

View File

@@ -43,7 +43,6 @@
#include <QString>
#include <QUrl>
#include <QImage>
#include <QPixmap>
#include <QTimer>
#include <QtEvents>
@@ -290,9 +289,6 @@ class MainWindow : public QMainWindow, public PlatformInterface {
void CheckFullRescanRevisions();
// creates the icon by painting the full one depending on the current position
QPixmap CreateOverlayedIcon(const int position, const int scrobble_point);
void GetCoverAutomatically();
void SetToggleScrobblingIcon(const bool value);

View File

@@ -1668,12 +1668,24 @@ void Song::InitArtManual() {
void Song::InitArtAutomatic() {
if (d->art_automatic_.isEmpty() && d->source_ == Source::LocalFile && d->url_.isLocalFile()) {
// Pick the first image file in the album directory.
QFileInfo file(d->url_.toLocalFile());
QDir dir(file.path());
QStringList files = dir.entryList(QStringList() << u"*.jpg"_s << u"*.png"_s << u"*.gif"_s << u"*.jpeg"_s, QDir::Files|QDir::Readable, QDir::Name);
if (files.count() > 0) {
d->art_automatic_ = QUrl::fromLocalFile(file.path() + QDir::separator() + files.first());
const QFileInfo fileinfo(d->url_.toLocalFile());
const QDir dir(fileinfo.path());
const QStringList cover_files = dir.entryList(QStringList() << u"*.jpg"_s << u"*.png"_s << u"*.gif"_s << u"*.jpeg"_s, QDir::Files|QDir::Readable, QDir::Name);
QString best_cover_file;
for (const QString &cover_file : cover_files) {
if (cover_file.contains("back"_L1, Qt::CaseInsensitive)) {
continue;
}
if (cover_file.contains("front"_L1, Qt::CaseInsensitive) || cover_file.startsWith("cover"_L1, Qt::CaseInsensitive)) {
best_cover_file = cover_file;
break;
}
if (best_cover_file.isEmpty()) {
best_cover_file = cover_file;
}
}
if (!best_cover_file.isEmpty()) {
d->art_automatic_ = QUrl::fromLocalFile(fileinfo.path() + QDir::separator() + best_cover_file);
}
}

View File

@@ -29,13 +29,17 @@
#include <QString>
#include <QStringList>
#include <QUrl>
#include <QSettings>
#include <QStandardPaths>
#include <QMessageBox>
#include <QScrollBar>
#include <QLineEdit>
#include <QToolButton>
#include <QFileDialog>
#include <QSpacerItem>
#include <QtEvents>
#include "constants/appearancesettings.h"
#include "constants/filefilterconstants.h"
#include "includes/shared_ptr.h"
#include "core/deletefiles.h"
#include "core/filesystemmusicstorage.h"
@@ -45,10 +49,11 @@
#include "dialogs/deleteconfirmationdialog.h"
#include "fileview.h"
#include "fileviewlist.h"
#include "fileviewtree.h"
#include "fileviewtreemodel.h"
#include "fileviewtreeitem.h"
#include "ui_fileview.h"
#include "organize/organizeerrordialog.h"
#include "constants/appearancesettings.h"
#include "constants/filefilterconstants.h"
using std::make_unique;
using namespace Qt::Literals::StringLiterals;
@@ -57,9 +62,12 @@ FileView::FileView(QWidget *parent)
: QWidget(parent),
ui_(new Ui_FileView),
model_(nullptr),
tree_model_(nullptr),
undo_stack_(new QUndoStack(this)),
task_manager_(nullptr),
storage_(new FilesystemMusicStorage(Song::Source::LocalFile, u"/"_s)) {
storage_(new FilesystemMusicStorage(Song::Source::LocalFile, u"/"_s)),
tree_view_active_(false),
view_mode_spacer_(nullptr) {
ui_->setupUi(this);
@@ -68,12 +76,14 @@ FileView::FileView(QWidget *parent)
ui_->forward->setIcon(IconLoader::Load(u"go-next"_s));
ui_->home->setIcon(IconLoader::Load(u"go-home"_s));
ui_->up->setIcon(IconLoader::Load(u"go-up"_s));
ui_->toggle_view->setIcon(IconLoader::Load(u"view-choose"_s));
QObject::connect(ui_->back, &QToolButton::clicked, undo_stack_, &QUndoStack::undo);
QObject::connect(ui_->forward, &QToolButton::clicked, undo_stack_, &QUndoStack::redo);
QObject::connect(ui_->home, &QToolButton::clicked, this, &FileView::FileHome);
QObject::connect(ui_->up, &QToolButton::clicked, this, &FileView::FileUp);
QObject::connect(ui_->path, &QLineEdit::textChanged, this, &FileView::ChangeFilePath);
QObject::connect(ui_->toggle_view, &QToolButton::clicked, this, &FileView::ToggleViewMode);
QObject::connect(undo_stack_, &QUndoStack::canUndoChanged, ui_->back, &FileView::setEnabled);
QObject::connect(undo_stack_, &QUndoStack::canRedoChanged, ui_->forward, &FileView::setEnabled);
@@ -87,6 +97,22 @@ FileView::FileView(QWidget *parent)
QObject::connect(ui_->list, &FileViewList::Delete, this, &FileView::Delete);
QObject::connect(ui_->list, &FileViewList::EditTags, this, &FileView::EditTags);
// Connect tree view signals
QObject::connect(ui_->tree, &FileViewTree::AddToPlaylist, this, &FileView::AddToPlaylist);
QObject::connect(ui_->tree, &FileViewTree::CopyToCollection, this, &FileView::CopyToCollection);
QObject::connect(ui_->tree, &FileViewTree::MoveToCollection, this, &FileView::MoveToCollection);
QObject::connect(ui_->tree, &FileViewTree::CopyToDevice, this, &FileView::CopyToDevice);
QObject::connect(ui_->tree, &FileViewTree::Delete, this, &FileView::Delete);
QObject::connect(ui_->tree, &FileViewTree::EditTags, this, &FileView::EditTags);
QObject::connect(ui_->tree, &FileViewTree::activated, this, &FileView::ItemActivated);
QObject::connect(ui_->tree, &FileViewTree::doubleClicked, this, &FileView::ItemDoubleClick);
// Setup tree root management buttons
ui_->add_tree_root->setIcon(IconLoader::Load(u"folder-new"_s));
ui_->remove_tree_root->setIcon(IconLoader::Load(u"list-remove"_s));
QObject::connect(ui_->add_tree_root, &QToolButton::clicked, this, &FileView::AddRootButtonClicked);
QObject::connect(ui_->remove_tree_root, &QToolButton::clicked, this, &FileView::RemoveRootButtonClicked);
QString filter = QLatin1String(kFileFilter);
filter_list_ << filter.split(u' ');
@@ -109,6 +135,19 @@ void FileView::ReloadSettings() {
ui_->forward->setIconSize(QSize(iconsize, iconsize));
ui_->home->setIconSize(QSize(iconsize, iconsize));
ui_->up->setIconSize(QSize(iconsize, iconsize));
ui_->toggle_view->setIconSize(QSize(iconsize, iconsize));
ui_->add_tree_root->setIconSize(QSize(iconsize, iconsize));
ui_->remove_tree_root->setIconSize(QSize(iconsize, iconsize));
// Load tree root paths setting
Settings file_settings;
file_settings.beginGroup(u"FileView"_s);
tree_root_paths_ = file_settings.value(u"tree_root_paths"_s, QStandardPaths::standardLocations(QStandardPaths::StandardLocation::MusicLocation)).toStringList();
tree_view_active_ = file_settings.value(u"tree_view_active"_s, false).toBool();
file_settings.endGroup();
// Set initial view mode
UpdateViewModeUI();
}
@@ -180,24 +219,46 @@ void FileView::ChangeFilePathWithoutUndo(const QString &new_path) {
}
void FileView::ItemActivated(const QModelIndex &idx) {
if (model_->isDir(idx))
// Only handle activation for list view (not tree view)
if (!tree_view_active_ && model_->isDir(idx)) {
ChangeFilePath(model_->filePath(idx));
}
}
void FileView::ItemDoubleClick(const QModelIndex &idx) {
if (model_->isDir(idx)) {
return;
QString file_path;
bool is_file = false;
// Handle tree view with virtual roots
if (tree_view_active_ && tree_model_) {
QVariant type_var = tree_model_->data(idx, FileViewTreeModel::Role_Type);
if (type_var.isValid()) {
FileViewTreeItem::Type item_type = type_var.value<FileViewTreeItem::Type>();
// Only handle files, ignore directories and virtual roots
if (item_type == FileViewTreeItem::Type::File) {
file_path = tree_model_->data(idx, FileViewTreeModel::Role_FilePath).toString();
is_file = true;
}
}
}
// Handle list view with filesystem model
else if (!tree_view_active_ && model_) {
if (!model_->isDir(idx)) {
file_path = model_->filePath(idx);
is_file = true;
}
}
QString file_path = model_->filePath(idx);
// Add file to playlist if it's a valid file
if (is_file && !file_path.isEmpty()) {
MimeData *mimedata = new MimeData;
mimedata->from_doubleclick_ = true;
mimedata->setUrls(QList<QUrl>() << QUrl::fromLocalFile(file_path));
mimedata->name_for_new_playlist_ = file_path;
MimeData *mimedata = new MimeData;
mimedata->from_doubleclick_ = true;
mimedata->setUrls(QList<QUrl>() << QUrl::fromLocalFile(file_path));
mimedata->name_for_new_playlist_ = file_path;
Q_EMIT AddToPlaylist(mimedata);
Q_EMIT AddToPlaylist(mimedata);
}
}
@@ -272,12 +333,156 @@ void FileView::showEvent(QShowEvent *e) {
model_->setNameFilterDisables(false);
ui_->list->setModel(model_);
// Create tree model
tree_model_ = new FileViewTreeModel(this);
tree_model_->SetNameFilters(filter_list_);
SetupTreeView();
ChangeFilePathWithoutUndo(QDir::homePath());
if (!lazy_set_path_.isEmpty()) ChangeFilePathWithoutUndo(lazy_set_path_);
}
void FileView::SetupTreeView() {
// Use the new tree model with virtual roots
ui_->tree->setModel(tree_model_);
// Set the root paths in the model
tree_model_->SetRootPaths(tree_root_paths_);
// No need to set root index - the model handles virtual roots
}
void FileView::ToggleViewMode() {
tree_view_active_ = !tree_view_active_;
UpdateViewModeUI();
// Save the preference
Settings s;
s.beginGroup(u"FileView"_s);
s.setValue(u"tree_view_active"_s, tree_view_active_);
s.endGroup();
}
void FileView::UpdateViewModeUI() {
if (tree_view_active_) {
ui_->view_stack->setCurrentWidget(ui_->tree_page);
// Hide navigation controls in tree view mode
ui_->back->setVisible(false);
ui_->forward->setVisible(false);
ui_->up->setVisible(false);
ui_->home->setVisible(false);
ui_->path->setVisible(false);
// Show tree root management buttons
ui_->add_tree_root->setVisible(true);
ui_->remove_tree_root->setVisible(true);
// Insert spacer in tree view if not already present
if (!view_mode_spacer_) {
view_mode_spacer_ = new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Minimum);
ui_->horizontalLayout->insertSpacerItem(ui_->horizontalLayout->indexOf(ui_->toggle_view), view_mode_spacer_);
}
}
else {
ui_->view_stack->setCurrentWidget(ui_->list_page);
// Show navigation controls in list view mode
ui_->back->setVisible(true);
ui_->forward->setVisible(true);
ui_->up->setVisible(true);
ui_->home->setVisible(true);
ui_->path->setVisible(true);
// Hide tree root management buttons in list view
ui_->add_tree_root->setVisible(false);
ui_->remove_tree_root->setVisible(false);
// Remove spacer in list view
if (view_mode_spacer_) {
ui_->horizontalLayout->removeItem(view_mode_spacer_);
delete view_mode_spacer_;
view_mode_spacer_ = nullptr;
}
}
}
void FileView::AddTreeRootPath(const QString &path) {
if (!tree_root_paths_.contains(path)) {
tree_root_paths_.append(path);
SaveTreeRootPaths();
// Refresh the tree view to show the new root
if (tree_model_) {
SetupTreeView();
}
}
}
void FileView::RemoveTreeRootPath(const QString &path) {
tree_root_paths_.removeAll(path);
SaveTreeRootPaths();
// Refresh the tree view
if (tree_model_) {
SetupTreeView();
}
}
void FileView::SaveTreeRootPaths() {
Settings s;
s.beginGroup(u"FileView"_s);
s.setValue(u"tree_root_paths"_s, tree_root_paths_);
s.endGroup();
}
void FileView::AddRootButtonClicked() {
const QString dir = QFileDialog::getExistingDirectory(this, tr("Select folder to add as tree root"), tree_root_paths_.isEmpty() ? QDir::homePath() : tree_root_paths_.first(), QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);
if (!dir.isEmpty()) {
AddTreeRootPath(dir);
}
}
void FileView::RemoveRootButtonClicked() {
// Get currently selected item in tree
QModelIndex current = ui_->tree->currentIndex();
if (!current.isValid()) return;
QString path;
// Get the file path from the appropriate model
if (tree_model_) {
path = tree_model_->data(current, FileViewTreeModel::Role_FilePath).toString();
}
if (path.isEmpty()) return;
const QString clean_path = QDir::cleanPath(path);
// Check if this path or any parent is a configured root
for (const QString &root : std::as_const(tree_root_paths_)) {
const QString clean_root = QDir::cleanPath(root);
if (clean_path == clean_root || clean_path.startsWith(clean_root + QDir::separator())) {
RemoveTreeRootPath(root);
return;
}
}
}
void FileView::keyPressEvent(QKeyEvent *e) {
switch (e->key()) {

View File

@@ -40,10 +40,12 @@ class QFileIconProvider;
class QUndoStack;
class QKeyEvent;
class QShowEvent;
class QSpacerItem;
class MusicStorage;
class TaskManager;
class Ui_FileView;
class FileViewTreeModel;
class FileView : public QWidget {
Q_OBJECT
@@ -76,12 +78,22 @@ class FileView : public QWidget {
void ChangeFilePath(const QString &new_path);
void ItemActivated(const QModelIndex &idx);
void ItemDoubleClick(const QModelIndex &idx);
void ToggleViewMode();
void Delete(const QStringList &filenames);
void DeleteFinished(const SongList &songs_with_errors);
public Q_SLOTS:
void AddTreeRootPath(const QString &path);
void RemoveTreeRootPath(const QString &path);
private:
void ChangeFilePathWithoutUndo(const QString &new_path);
void SetupTreeView();
void SaveTreeRootPaths();
void AddRootButtonClicked();
void RemoveRootButtonClicked();
void UpdateViewModeUI();
private:
class UndoCommand : public QUndoCommand {
@@ -110,16 +122,21 @@ class FileView : public QWidget {
Ui_FileView *ui_;
QFileSystemModel *model_;
FileViewTreeModel *tree_model_;
QUndoStack *undo_stack_;
SharedPtr<TaskManager> task_manager_;
SharedPtr<MusicStorage> storage_;
QString lazy_set_path_;
QStringList tree_root_paths_;
QStringList filter_list_;
ScopedPtr<QFileIconProvider> file_icon_provider_;
bool tree_view_active_;
QSpacerItem *view_mode_spacer_;
};
#endif // FILEVIEW_H

View File

@@ -95,28 +95,143 @@
<item>
<widget class="QLineEdit" name="path"/>
</item>
<item>
<widget class="QToolButton" name="add_tree_root">
<property name="toolTip">
<string>Add root directory</string>
</property>
<property name="iconSize">
<size>
<width>22</width>
<height>22</height>
</size>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="remove_tree_root">
<property name="toolTip">
<string>Remove selected root directory</string>
</property>
<property name="iconSize">
<size>
<width>22</width>
<height>22</height>
</size>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="toggle_view">
<property name="iconSize">
<size>
<width>22</width>
<height>22</height>
</size>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
<property name="toolTip">
<string>Toggle between list and tree view</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="FileViewList" name="list">
<property name="dragEnabled">
<bool>true</bool>
</property>
<property name="dragDropMode">
<enum>QAbstractItemView::DragOnly</enum>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
<widget class="QStackedWidget" name="view_stack">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="list_page">
<layout class="QVBoxLayout" name="list_layout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="FileViewList" name="list">
<property name="dragEnabled">
<bool>true</bool>
</property>
<property name="dragDropMode">
<enum>QAbstractItemView::DragOnly</enum>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tree_page">
<layout class="QVBoxLayout" name="tree_layout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="FileViewTree" name="tree">
<property name="dragEnabled">
<bool>true</bool>
</property>
<property name="dragDropMode">
<enum>QAbstractItemView::DragOnly</enum>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="iconSize">
<size>
<width>16</width>
<height>16</height>
</size>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
@@ -127,6 +242,11 @@
<extends>QListView</extends>
<header>fileview/fileviewlist.h</header>
</customwidget>
<customwidget>
<class>FileViewTree</class>
<extends>QTreeView</extends>
<header>fileview/fileviewtree.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>

View File

@@ -99,7 +99,7 @@ MimeData *FileViewList::MimeDataFromSelection() const {
const QStringList filenames = FilenamesFromSelection();
// if just one folder selected - use its path as the new playlist's name
// If just one folder selected - use its path as the new playlist's name
if (filenames.size() == 1 && QFileInfo(filenames.first()).isDir()) {
if (filenames.first().length() > 20) {
mimedata->name_for_new_playlist_ = QDir(filenames.first()).dirName();
@@ -108,7 +108,7 @@ MimeData *FileViewList::MimeDataFromSelection() const {
mimedata->name_for_new_playlist_ = filenames.first();
}
}
// otherwise, use the current root path
// Otherwise, use the current root path
else {
QString path = qobject_cast<QFileSystemModel*>(model())->rootPath();
if (path.length() > 20) {
@@ -196,11 +196,11 @@ void FileViewList::mousePressEvent(QMouseEvent *e) {
case Qt::XButton2:
Q_EMIT Forward();
break;
// enqueue to playlist with middleClick
// Enqueue to playlist with middleClick
case Qt::MiddleButton:{
QListView::mousePressEvent(e);
// we need to update the menu selection
// We need to update the menu selection
menu_selection_ = selectionModel()->selection();
MimeData *mimedata = new MimeData;

View File

@@ -0,0 +1,205 @@
/*
* Strawberry Music Player
* Copyright 2025, 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 <algorithm>
#include <utility>
#include <QWidget>
#include <QAbstractItemModel>
#include <QFileInfo>
#include <QDir>
#include <QMenu>
#include <QUrl>
#include <QCollator>
#include <QtEvents>
#include "core/iconloader.h"
#include "core/mimedata.h"
#include "utilities/filemanagerutils.h"
#include "fileviewtree.h"
#include "fileviewtreemodel.h"
using namespace Qt::Literals::StringLiterals;
FileViewTree::FileViewTree(QWidget *parent)
: QTreeView(parent),
menu_(new QMenu(this)) {
menu_->addAction(IconLoader::Load(u"media-playback-start"_s), tr("Append to current playlist"), this, &FileViewTree::AddToPlaylistSlot);
menu_->addAction(IconLoader::Load(u"media-playback-start"_s), tr("Replace current playlist"), this, &FileViewTree::LoadSlot);
menu_->addAction(IconLoader::Load(u"document-new"_s), tr("Open in new playlist"), this, &FileViewTree::OpenInNewPlaylistSlot);
menu_->addSeparator();
menu_->addAction(IconLoader::Load(u"edit-copy"_s), tr("Copy to collection..."), this, &FileViewTree::CopyToCollectionSlot);
menu_->addAction(IconLoader::Load(u"go-jump"_s), tr("Move to collection..."), this, &FileViewTree::MoveToCollectionSlot);
menu_->addAction(IconLoader::Load(u"device"_s), tr("Copy to device..."), this, &FileViewTree::CopyToDeviceSlot);
menu_->addAction(IconLoader::Load(u"edit-delete"_s), tr("Delete from disk..."), this, &FileViewTree::DeleteSlot);
menu_->addSeparator();
menu_->addAction(IconLoader::Load(u"edit-rename"_s), tr("Edit track information..."), this, &FileViewTree::EditTagsSlot);
menu_->addAction(IconLoader::Load(u"document-open-folder"_s), tr("Show in file browser..."), this, &FileViewTree::ShowInBrowser);
setAttribute(Qt::WA_MacShowFocusRect, false);
setHeaderHidden(true);
setUniformRowHeights(true);
}
void FileViewTree::contextMenuEvent(QContextMenuEvent *e) {
menu_selection_ = selectionModel()->selection();
menu_->popup(e->globalPos());
e->accept();
}
QStringList FileViewTree::FilenamesFromSelection() const {
QStringList filenames;
const QModelIndexList indexes = menu_selection_.indexes();
FileViewTreeModel *tree_model = qobject_cast<FileViewTreeModel*>(model());
if (tree_model) {
for (const QModelIndex &index : indexes) {
if (index.column() == 0) {
QString path = tree_model->data(index, FileViewTreeModel::Role_FilePath).toString();
if (!path.isEmpty()) {
filenames << path;
}
}
}
}
QCollator collator;
collator.setNumericMode(true);
std::sort(filenames.begin(), filenames.end(), collator);
return filenames;
}
QList<QUrl> FileViewTree::UrlListFromSelection() const {
QList<QUrl> urls;
const QStringList filenames = FilenamesFromSelection();
urls.reserve(filenames.count());
for (const QString &filename : std::as_const(filenames)) {
urls << QUrl::fromLocalFile(filename);
}
return urls;
}
MimeData *FileViewTree::MimeDataFromSelection() const {
MimeData *mimedata = new MimeData;
mimedata->setUrls(UrlListFromSelection());
const QStringList filenames = FilenamesFromSelection();
// if just one folder selected - use its path as the new playlist's name
if (filenames.size() == 1 && QFileInfo(filenames.first()).isDir()) {
if (filenames.first().length() > 20) {
mimedata->name_for_new_playlist_ = QDir(filenames.first()).dirName();
}
else {
mimedata->name_for_new_playlist_ = filenames.first();
}
}
// otherwise, use "Files" as default
else {
mimedata->name_for_new_playlist_ = tr("Files");
}
return mimedata;
}
void FileViewTree::LoadSlot() {
MimeData *mimedata = MimeDataFromSelection();
mimedata->clear_first_ = true;
Q_EMIT AddToPlaylist(mimedata);
}
void FileViewTree::AddToPlaylistSlot() {
Q_EMIT AddToPlaylist(MimeDataFromSelection());
}
void FileViewTree::OpenInNewPlaylistSlot() {
MimeData *mimedata = MimeDataFromSelection();
mimedata->open_in_new_playlist_ = true;
Q_EMIT AddToPlaylist(mimedata);
}
void FileViewTree::CopyToCollectionSlot() {
Q_EMIT CopyToCollection(UrlListFromSelection());
}
void FileViewTree::MoveToCollectionSlot() {
Q_EMIT MoveToCollection(UrlListFromSelection());
}
void FileViewTree::CopyToDeviceSlot() {
Q_EMIT CopyToDevice(UrlListFromSelection());
}
void FileViewTree::DeleteSlot() {
Q_EMIT Delete(FilenamesFromSelection());
}
void FileViewTree::EditTagsSlot() {
Q_EMIT EditTags(UrlListFromSelection());
}
void FileViewTree::mousePressEvent(QMouseEvent *e) {
switch (e->button()) {
// Enqueue to playlist with middleClick
case Qt::MiddleButton:{
QTreeView::mousePressEvent(e);
// We need to update the menu selection
QItemSelectionModel *selection_model = selectionModel();
if (!selection_model) {
e->ignore();
return;
}
menu_selection_ = selection_model->selection();
MimeData *mimedata = new MimeData;
mimedata->setUrls(UrlListFromSelection());
mimedata->enqueue_now_ = true;
Q_EMIT AddToPlaylist(mimedata);
break;
}
default:
QTreeView::mousePressEvent(e);
break;
}
}
void FileViewTree::ShowInBrowser() {
Utilities::OpenInFileBrowser(UrlListFromSelection());
}

View File

@@ -0,0 +1,78 @@
/*
* Strawberry Music Player
* Copyright 2025, 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/>.
*
*/
#ifndef FILEVIEWTREE_H
#define FILEVIEWTREE_H
#include <QObject>
#include <QTreeView>
#include <QList>
#include <QUrl>
#include <QString>
#include <QStringList>
class QWidget;
class QMimeData;
class QMenu;
class QMouseEvent;
class QContextMenuEvent;
class MimeData;
class FileViewTree : public QTreeView {
Q_OBJECT
public:
explicit FileViewTree(QWidget *parent = nullptr);
void mousePressEvent(QMouseEvent *e) override;
Q_SIGNALS:
void AddToPlaylist(QMimeData *data);
void CopyToCollection(const QList<QUrl> &urls);
void MoveToCollection(const QList<QUrl> &urls);
void CopyToDevice(const QList<QUrl> &urls);
void Delete(const QStringList &filenames);
void EditTags(const QList<QUrl> &urls);
protected:
void contextMenuEvent(QContextMenuEvent *e) override;
private:
QStringList FilenamesFromSelection() const;
QList<QUrl> UrlListFromSelection() const;
MimeData *MimeDataFromSelection() const;
private Q_SLOTS:
void LoadSlot();
void AddToPlaylistSlot();
void OpenInNewPlaylistSlot();
void CopyToCollectionSlot();
void MoveToCollectionSlot();
void CopyToDeviceSlot();
void DeleteSlot();
void EditTagsSlot();
void ShowInBrowser();
private:
QMenu *menu_;
QItemSelection menu_selection_;
};
#endif // FILEVIEWTREE_H

View File

@@ -0,0 +1,52 @@
/*
* Strawberry Music Player
* Copyright 2025, 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/>.
*
*/
#ifndef FILEVIEWTREEITEM_H
#define FILEVIEWTREEITEM_H
#include "config.h"
#include <QFileInfo>
#include "core/simpletreeitem.h"
class FileViewTreeItem : public SimpleTreeItem<FileViewTreeItem> {
public:
enum class Type {
Root, // Hidden root
VirtualRoot, // User-configured root paths
Directory, // File system directory
File // File system file
};
explicit FileViewTreeItem(SimpleTreeModel<FileViewTreeItem> *_model) : SimpleTreeItem<FileViewTreeItem>(_model), type(Type::Root), lazy_loaded(false) {}
explicit FileViewTreeItem(const Type _type, FileViewTreeItem *_parent = nullptr) : SimpleTreeItem<FileViewTreeItem>(_parent), type(_type), lazy_loaded(false) {}
Type type;
QString file_path; // Absolute file system path
QFileInfo file_info; // Cached file info
bool lazy_loaded; // Whether children have been loaded
private:
Q_DISABLE_COPY(FileViewTreeItem)
};
Q_DECLARE_METATYPE(FileViewTreeItem::Type)
#endif // FILEVIEWTREEITEM_H

View File

@@ -0,0 +1,246 @@
/*
* Strawberry Music Player
* Copyright 2025, 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 <QObject>
#include <QVariant>
#include <QString>
#include <QStringList>
#include <QList>
#include <QMap>
#include <QDir>
#include <QFileInfo>
#include <QFileIconProvider>
#include <QMimeData>
#include <QUrl>
#include <QIcon>
#include "core/simpletreemodel.h"
#include "core/logging.h"
#include "fileviewtreemodel.h"
#include "fileviewtreeitem.h"
using namespace Qt::Literals::StringLiterals;
FileViewTreeModel::FileViewTreeModel(QObject *parent)
: SimpleTreeModel<FileViewTreeItem>(new FileViewTreeItem(this), parent),
icon_provider_(new QFileIconProvider()) {
}
FileViewTreeModel::~FileViewTreeModel() {
delete root_;
delete icon_provider_;
}
Qt::ItemFlags FileViewTreeModel::flags(const QModelIndex &idx) const {
const FileViewTreeItem *item = IndexToItem(idx);
if (!item) return Qt::NoItemFlags;
switch (item->type) {
case FileViewTreeItem::Type::VirtualRoot:
case FileViewTreeItem::Type::Directory:
case FileViewTreeItem::Type::File:
return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled;
case FileViewTreeItem::Type::Root:
default:
return Qt::ItemIsEnabled;
}
}
QVariant FileViewTreeModel::data(const QModelIndex &idx, const int role) const {
if (!idx.isValid()) return QVariant();
const FileViewTreeItem *item = IndexToItem(idx);
if (!item) return QVariant();
switch (role) {
case Qt::DisplayRole:
if (item->type == FileViewTreeItem::Type::VirtualRoot) {
return item->display_text.isEmpty() ? item->file_path : item->display_text;
}
return item->file_info.fileName();
case Qt::DecorationRole:
return GetIcon(item);
case Role_Type:
return QVariant::fromValue(item->type);
case Role_FilePath:
return item->file_path;
case Role_FileName:
return item->file_info.fileName();
default:
return QVariant();
}
}
bool FileViewTreeModel::hasChildren(const QModelIndex &parent) const {
const FileViewTreeItem *item = IndexToItem(parent);
if (!item) return false;
// Root and VirtualRoot always have children (or can have them)
if (item->type == FileViewTreeItem::Type::Root) return true;
if (item->type == FileViewTreeItem::Type::VirtualRoot) return true;
// Directories can have children
if (item->type == FileViewTreeItem::Type::Directory) {
return true;
}
// Files don't have children
return false;
}
bool FileViewTreeModel::canFetchMore(const QModelIndex &parent) const {
const FileViewTreeItem *item = IndexToItem(parent);
if (!item) return false;
// Can fetch more if not yet lazy loaded
return !item->lazy_loaded && (item->type == FileViewTreeItem::Type::VirtualRoot || item->type == FileViewTreeItem::Type::Directory);
}
void FileViewTreeModel::fetchMore(const QModelIndex &parent) {
FileViewTreeItem *item = IndexToItem(parent);
if (!item || item->lazy_loaded) return;
LazyLoad(item);
}
void FileViewTreeModel::LazyLoad(FileViewTreeItem *item) {
if (item->lazy_loaded) return;
QDir dir(item->file_path);
if (!dir.exists()) {
item->lazy_loaded = true;
return;
}
// Apply name filters
const QDir::Filters filters = QDir::AllDirs | QDir::Files | QDir::NoDotAndDotDot;
if (!name_filters_.isEmpty()) {
dir.setNameFilters(name_filters_);
}
const QFileInfoList entries = dir.entryInfoList(filters, QDir::Name | QDir::DirsFirst);
if (!entries.isEmpty()) {
BeginInsert(item, 0, static_cast<int>(entries.count()) - 1);
for (const QFileInfo &entry : entries) {
FileViewTreeItem *child = new FileViewTreeItem(
entry.isDir() ? FileViewTreeItem::Type::Directory : FileViewTreeItem::Type::File,
item
);
child->file_path = entry.absoluteFilePath();
child->file_info = entry;
child->lazy_loaded = false;
child->display_text = entry.fileName();
}
EndInsert();
}
item->lazy_loaded = true;
}
QIcon FileViewTreeModel::GetIcon(const FileViewTreeItem *item) const {
if (!item) return QIcon();
switch (item->type) {
case FileViewTreeItem::Type::VirtualRoot:
case FileViewTreeItem::Type::Directory:
return icon_provider_->icon(QFileIconProvider::Folder);
case FileViewTreeItem::Type::File:
return icon_provider_->icon(item->file_info);
default:
return QIcon();
}
}
QStringList FileViewTreeModel::mimeTypes() const {
return QStringList() << u"text/uri-list"_s;
}
QMimeData *FileViewTreeModel::mimeData(const QModelIndexList &indexes) const {
if (indexes.isEmpty()) return nullptr;
QList<QUrl> urls;
for (const QModelIndex &idx : indexes) {
const FileViewTreeItem *item = IndexToItem(idx);
if (item && (item->type == FileViewTreeItem::Type::File || item->type == FileViewTreeItem::Type::Directory || item->type == FileViewTreeItem::Type::VirtualRoot)) {
urls << QUrl::fromLocalFile(item->file_path);
}
}
if (urls.isEmpty()) return nullptr;
QMimeData *data = new QMimeData();
data->setUrls(urls);
return data;
}
void FileViewTreeModel::SetRootPaths(const QStringList &paths) {
Reset();
for (const QString &path : paths) {
QFileInfo info(path);
if (!info.exists() || !info.isDir()) continue;
FileViewTreeItem *virtual_root = new FileViewTreeItem(FileViewTreeItem::Type::VirtualRoot, root_);
virtual_root->file_path = info.absoluteFilePath();
virtual_root->file_info = info;
virtual_root->display_text = info.absoluteFilePath();
virtual_root->lazy_loaded = false;
}
}
void FileViewTreeModel::SetNameFilters(const QStringList &filters) {
name_filters_ = filters;
}
void FileViewTreeModel::Reset() {
beginResetModel();
// Clear children without notifications since we're in a reset
qDeleteAll(root_->children);
root_->children.clear();
endResetModel();
}

View File

@@ -0,0 +1,72 @@
/*
* Strawberry Music Player
* Copyright 2025, 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/>.
*
*/
#ifndef FILEVIEWTREEMODEL_H
#define FILEVIEWTREEMODEL_H
#include "config.h"
#include <QObject>
#include <QVariant>
#include <QStringList>
#include <QIcon>
#include "core/simpletreemodel.h"
#include "fileviewtreeitem.h"
class QFileIconProvider;
class QMimeData;
class FileViewTreeModel : public SimpleTreeModel<FileViewTreeItem> {
Q_OBJECT
public:
explicit FileViewTreeModel(QObject *parent = nullptr);
~FileViewTreeModel() override;
enum Role {
Role_Type = Qt::UserRole + 1,
Role_FilePath,
Role_FileName,
RoleCount
};
// QAbstractItemModel
Qt::ItemFlags flags(const QModelIndex &idx) const override;
QVariant data(const QModelIndex &idx, const int role) const override;
bool hasChildren(const QModelIndex &parent) const override;
bool canFetchMore(const QModelIndex &parent) const override;
void fetchMore(const QModelIndex &parent) override;
QStringList mimeTypes() const override;
QMimeData *mimeData(const QModelIndexList &indexes) const override;
void SetRootPaths(const QStringList &paths);
void SetNameFilters(const QStringList &filters);
private:
void Reset();
void LazyLoad(FileViewTreeItem *item);
QIcon GetIcon(const FileViewTreeItem *item) const;
private:
QFileIconProvider *icon_provider_;
QStringList name_filters_;
};
#endif // FILEVIEWTREEMODEL_H

View File

@@ -1,116 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* 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 <QObject>
#include <QString>
#include <QUrl>
#include <QClipboard>
#include <QMessageBox>
#include "core/iconloader.h"
#include "podcastdiscoverymodel.h"
#include "podcasturlloader.h"
#include "addpodcastbyurl.h"
#include "ui_addpodcastbyurl.h"
AddPodcastByUrl::AddPodcastByUrl(Application *app, QWidget *parent)
: AddPodcastPage(app, parent),
ui_(new Ui_AddPodcastByUrl),
loader_(new PodcastUrlLoader(this)) {
ui_->setupUi(this);
QObject::connect(ui_->go, &QPushButton::clicked, this, &AddPodcastByUrl::GoClicked);
setWindowIcon(IconLoader::Load("podcast"));
}
AddPodcastByUrl::~AddPodcastByUrl() { delete ui_; }
void AddPodcastByUrl::SetUrlAndGo(const QUrl &url) {
ui_->url->setText(url.toString());
GoClicked();
}
void AddPodcastByUrl::SetOpml(const OpmlContainer &opml) {
ui_->url->setText(opml.url.toString());
model()->clear();
model()->CreateOpmlContainerItems(opml, model()->invisibleRootItem());
}
void AddPodcastByUrl::GoClicked() {
emit Busy(true);
model()->clear();
PodcastUrlLoaderReply* reply = loader_->Load(ui_->url->text());
ui_->url->setText(reply->url().toString());
QObject::connect(reply, &PodcastUrlLoaderReply::Finished, this, [this, reply]() { RequestFinished(reply); });
}
void AddPodcastByUrl::RequestFinished(PodcastUrlLoaderReply *reply) {
reply->deleteLater();
emit Busy(false);
if (!reply->is_success()) {
QMessageBox::warning(this, tr("Failed to load podcast"), reply->error_text(), QMessageBox::Close);
return;
}
switch (reply->result_type()) {
case PodcastUrlLoaderReply::Type_Podcast:
for (const Podcast& podcast : reply->podcast_results()) {
model()->appendRow(model()->CreatePodcastItem(podcast));
}
break;
case PodcastUrlLoaderReply::Type_Opml:
model()->CreateOpmlContainerItems(reply->opml_results(), model()->invisibleRootItem());
break;
}
}
void AddPodcastByUrl::Show() {
ui_->url->setFocus();
const QClipboard *clipboard = QApplication::clipboard();
QStringList contents;
contents << clipboard->text(QClipboard::Selection) << clipboard->text(QClipboard::Clipboard);
for (const QString &content : contents) {
if (content.contains("://")) {
ui_->url->setText(content);
return;
}
}
}

View File

@@ -1,60 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* 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/>.
*
*/
#ifndef ADDPODCASTBYURL_H
#define ADDPODCASTBYURL_H
#include <QObject>
#include <QUrl>
#include "addpodcastpage.h"
#include "opmlcontainer.h"
class Application;
class AddPodcastPage;
class PodcastUrlLoader;
class PodcastUrlLoaderReply;
class Ui_AddPodcastByUrl;
class AddPodcastByUrl : public AddPodcastPage {
Q_OBJECT
public:
explicit AddPodcastByUrl(Application *app, QWidget *parent = nullptr);
~AddPodcastByUrl();
void Show();
void SetOpml(const OpmlContainer &opml);
void SetUrlAndGo(const QUrl &url);
private slots:
void GoClicked();
void RequestFinished(PodcastUrlLoaderReply *reply);
private:
Ui_AddPodcastByUrl *ui_;
PodcastUrlLoader *loader_;
};
#endif // ADDPODCASTBYURL_H

View File

@@ -1,61 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AddPodcastByUrl</class>
<widget class="QWidget" name="AddPodcastByUrl">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>431</width>
<height>51</height>
</rect>
</property>
<property name="windowTitle">
<string>Enter a URL</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>If you know the URL of a podcast, enter it below and press Go.</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLineEdit" name="url"/>
</item>
<item>
<widget class="QPushButton" name="go">
<property name="text">
<string>Go</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<connections>
<connection>
<sender>url</sender>
<signal>returnPressed()</signal>
<receiver>go</receiver>
<slot>click()</slot>
<hints>
<hint type="sourcelabel">
<x>109</x>
<y>24</y>
</hint>
<hint type="destinationlabel">
<x>429</x>
<y>49</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -1,270 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2012, 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* 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 <QDialog>
#include <QVariant>
#include <QString>
#include <QUrl>
#include <QDir>
#include <QFileDialog>
#include <QTimer>
#include <QPushButton>
#include <QListWidget>
#include <QItemSelectionModel>
#include "core/application.h"
#include "core/iconloader.h"
#include "widgets/widgetfadehelper.h"
#include "fixedopmlpage.h"
#include "gpoddersearchpage.h"
#include "gpoddertoptagspage.h"
#include "itunessearchpage.h"
#include "podcastbackend.h"
#include "podcastdiscoverymodel.h"
#include "addpodcastbyurl.h"
#include "podcastinfowidget.h"
#include "addpodcastdialog.h"
#include "ui_addpodcastdialog.h"
const char *AddPodcastDialog::kBbcOpmlUrl = "http://www.bbc.co.uk/podcasts.opml";
const char *AddPodcastDialog::kCbcOpmlUrl = "http://cbc.ca/podcasts.opml";
AddPodcastDialog::AddPodcastDialog(Application *app, QWidget *parent)
: QDialog(parent),
app_(app),
ui_(new Ui_AddPodcastDialog),
last_opml_path_(QDir::homePath()) {
ui_->setupUi(this);
ui_->details->SetApplication(app);
ui_->results->SetExpandOnReset(false);
ui_->results->SetAddOnDoubleClick(false);
ui_->results_stack->setCurrentWidget(ui_->results_page);
fader_ = new WidgetFadeHelper(ui_->details_scroll_area);
QObject::connect(ui_->provider_list, &QListWidget::currentRowChanged, this, &AddPodcastDialog::ChangePage);
QObject::connect(ui_->details, &PodcastInfoWidget::LoadingFinished, fader_, &WidgetFadeHelper::StartFade);
QObject::connect(ui_->results, &AutoExpandingTreeView::doubleClicked, this, &AddPodcastDialog::PodcastDoubleClicked);
// Create Add and Remove Podcast buttons
add_button_ = new QPushButton(IconLoader::Load("list-add"), tr("Add Podcast"), this);
add_button_->setEnabled(false);
connect(add_button_, &QPushButton::clicked, this, &AddPodcastDialog::AddPodcast);
ui_->button_box->addButton(add_button_, QDialogButtonBox::ActionRole);
remove_button_ = new QPushButton(IconLoader::Load("list-remove"), tr("Unsubscribe"), this);
remove_button_->setEnabled(false);
connect(remove_button_, &QPushButton::clicked, this, &AddPodcastDialog::RemovePodcast);
ui_->button_box->addButton(remove_button_, QDialogButtonBox::ActionRole);
QPushButton *settings_button = new QPushButton(IconLoader::Load("configure"), tr("Configure podcasts..."), this);
connect(settings_button, &QPushButton::clicked, this, &AddPodcastDialog::OpenSettingsPage);
ui_->button_box->addButton(settings_button, QDialogButtonBox::ResetRole);
// Create an Open OPML file button
QPushButton *open_opml_button = new QPushButton(IconLoader::Load("document-open"), tr("Open OPML file..."), this);
QObject::connect(open_opml_button, &QPushButton::clicked, this, &AddPodcastDialog::OpenOPMLFile);
ui_->button_box->addButton(open_opml_button, QDialogButtonBox::ResetRole);
// Add providers
by_url_page_ = new AddPodcastByUrl(app, this);
AddPage(by_url_page_);
AddPage(new FixedOpmlPage(QUrl(kBbcOpmlUrl), tr("BBC Podcasts"), IconLoader::Load("bbc"), app, this));
AddPage(new FixedOpmlPage(QUrl(kCbcOpmlUrl), tr("CBC Podcasts"), IconLoader::Load("cbc"), app, this));
AddPage(new GPodderTopTagsPage(app, this));
AddPage(new GPodderSearchPage(app, this));
AddPage(new ITunesSearchPage(app, this));
ui_->provider_list->setCurrentRow(0);
}
AddPodcastDialog::~AddPodcastDialog() { delete ui_; }
void AddPodcastDialog::ShowWithUrl(const QUrl& url) {
by_url_page_->SetUrlAndGo(url);
ui_->provider_list->setCurrentRow(0);
show();
}
void AddPodcastDialog::ShowWithOpml(const OpmlContainer& opml) {
by_url_page_->SetOpml(opml);
ui_->provider_list->setCurrentRow(0);
show();
}
void AddPodcastDialog::AddPage(AddPodcastPage *page) {
pages_.append(page);
page_is_busy_.append(false);
ui_->stack->addWidget(page);
new QListWidgetItem(page->windowIcon(), page->windowTitle(), ui_->provider_list);
QObject::connect(page, &AddPodcastPage::Busy, this, &AddPodcastDialog::PageBusyChanged);
}
void AddPodcastDialog::ChangePage(const int index) {
AddPodcastPage *page = pages_[index];
ui_->stack->setCurrentIndex(index);
ui_->stack->setVisible(page->has_visible_widget());
ui_->results->setModel(page->model());
ui_->results_stack->setCurrentWidget(page_is_busy_[index] ? ui_->busy_page : ui_->results_page);
QObject::connect(ui_->results->selectionModel(), &QItemSelectionModel::currentRowChanged, this, &AddPodcastDialog::ChangePodcast);
ChangePodcast(QModelIndex());
CurrentPageBusyChanged(page_is_busy_[index]);
page->Show();
}
void AddPodcastDialog::ChangePodcast(const QModelIndex &current) {
QVariant podcast_variant = current.data(PodcastDiscoveryModel::Role_Podcast);
// If the selected item is invalid or not a podcast, hide the details pane.
if (podcast_variant.isNull()) {
ui_->details_scroll_area->hide();
add_button_->setEnabled(false);
remove_button_->setEnabled(false);
return;
}
current_podcast_ = podcast_variant.value<Podcast>();
// Start the blur+fade if there's already a podcast in the details pane.
if (ui_->details_scroll_area->isVisible()) {
fader_->StartBlur();
}
else {
ui_->details_scroll_area->show();
}
// Update the details pane
ui_->details->SetPodcast(current_podcast_);
// Is the user already subscribed to this podcast?
Podcast subscribed_podcast = app_->podcast_backend()->GetSubscriptionByUrl(current_podcast_.url());
const bool is_subscribed = subscribed_podcast.url().isValid();
if (is_subscribed) {
// Use the one from the database which will contain the ID.
current_podcast_ = subscribed_podcast;
}
add_button_->setEnabled(!is_subscribed);
remove_button_->setEnabled(is_subscribed);
}
void AddPodcastDialog::PageBusyChanged(const bool busy) {
const int index = pages_.indexOf(qobject_cast<AddPodcastPage*>(sender()));
if (index == -1) return;
page_is_busy_[index] = busy;
if (index == ui_->provider_list->currentRow()) {
CurrentPageBusyChanged(busy);
}
}
void AddPodcastDialog::CurrentPageBusyChanged(const bool busy) {
ui_->results_stack->setCurrentWidget(busy ? ui_->busy_page : ui_->results_page);
ui_->stack->setDisabled(busy);
QTimer::singleShot(0, this, &AddPodcastDialog::SelectFirstPodcast);
}
void AddPodcastDialog::SelectFirstPodcast() {
// Select the first item if there was one.
const PodcastDiscoveryModel *model = pages_[ui_->provider_list->currentRow()]->model();
if (model->rowCount() > 0) {
ui_->results->selectionModel()->setCurrentIndex(model->index(0, 0), QItemSelectionModel::ClearAndSelect);
}
}
void AddPodcastDialog::AddPodcast() {
app_->podcast_backend()->Subscribe(&current_podcast_);
add_button_->setEnabled(false);
remove_button_->setEnabled(true);
}
void AddPodcastDialog::PodcastDoubleClicked(const QModelIndex &idx) {
QVariant podcast_variant = idx.data(PodcastDiscoveryModel::Role_Podcast);
if (podcast_variant.isNull()) {
return;
}
current_podcast_ = podcast_variant.value<Podcast>();
app_->podcast_backend()->Subscribe(&current_podcast_);
add_button_->setEnabled(false);
remove_button_->setEnabled(true);
}
void AddPodcastDialog::RemovePodcast() {
app_->podcast_backend()->Unsubscribe(current_podcast_);
current_podcast_.set_database_id(-1);
add_button_->setEnabled(true);
remove_button_->setEnabled(false);
}
void AddPodcastDialog::OpenSettingsPage() {
//app_->OpenSettingsDialogAtPage(SettingsDialog::Page_Podcasts);
}
void AddPodcastDialog::OpenOPMLFile() {
const QString filename = QFileDialog::getOpenFileName(this, tr("Open OPML file"), last_opml_path_, "OPML files (*.opml)");
if (filename.isEmpty()) {
return;
}
last_opml_path_ = filename;
by_url_page_->SetUrlAndGo(QUrl::fromLocalFile(last_opml_path_));
ChangePage(ui_->stack->indexOf(by_url_page_));
}

View File

@@ -1,91 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2012, 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* 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/>.
*
*/
#ifndef ADDPODCASTDIALOG_H
#define ADDPODCASTDIALOG_H
#include <QDialog>
#include <QList>
#include <QString>
#include <QUrl>
#include "podcast.h"
class Application;
class AddPodcastByUrl;
class AddPodcastPage;
class OpmlContainer;
class WidgetFadeHelper;
class Ui_AddPodcastDialog;
class AddPodcastDialog : public QDialog {
Q_OBJECT
public:
explicit AddPodcastDialog(Application *app, QWidget *parent = nullptr);
~AddPodcastDialog();
// Convenience methods that open the dialog at the Add By Url page and fill it with either a URL (which is then fetched), or a pre-fetched OPML container.
void ShowWithUrl(const QUrl &url);
void ShowWithOpml(const OpmlContainer &opml);
private slots:
void OpenSettingsPage();
void AddPodcast();
void PodcastDoubleClicked(const QModelIndex &idx);
void RemovePodcast();
void ChangePage(const int index);
void ChangePodcast(const QModelIndex &current);
void PageBusyChanged(const bool busy);
void CurrentPageBusyChanged(const bool busy);
void SelectFirstPodcast();
void OpenOPMLFile();
private:
void AddPage(AddPodcastPage *page);
private:
static const char *kBbcOpmlUrl;
static const char *kCbcOpmlUrl;
Application *app_;
Ui_AddPodcastDialog *ui_;
QPushButton *add_button_;
QPushButton *remove_button_;
QList<AddPodcastPage*> pages_;
QList<bool> page_is_busy_;
AddPodcastByUrl *by_url_page_;
WidgetFadeHelper *fader_;
Podcast current_podcast_;
QString last_opml_path_;
};
#endif // ADDPODCASTDIALOG_H

View File

@@ -1,276 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AddPodcastDialog</class>
<widget class="QDialog" name="AddPodcastDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>941</width>
<height>473</height>
</rect>
</property>
<property name="windowTitle">
<string>Add podcast</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QListWidget" name="provider_list">
<property name="maximumSize">
<size>
<width>200</width>
<height>16777215</height>
</size>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="iconSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="uniformItemSizes">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QStackedWidget" name="stack">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<widget class="QStackedWidget" name="results_stack">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="results_page">
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="AutoExpandingTreeView" name="results">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="uniformRowHeights">
<bool>true</bool>
</property>
<attribute name="headerVisible">
<bool>false</bool>
</attribute>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="busy_page">
<layout class="QVBoxLayout" name="verticalLayout_4">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>192</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="BusyIndicator" name="widget" native="true">
<property name="text" stdset="0">
<string>Loading...</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QScrollArea" name="details_scroll_area">
<property name="minimumSize">
<size>
<width>250</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>250</width>
<height>16777215</height>
</size>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="PodcastInfoWidget" name="details">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>248</width>
<height>415</height>
</rect>
</property>
</widget>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QDialogButtonBox" name="button_box">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Close</set>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>BusyIndicator</class>
<extends>QWidget</extends>
<header>widgets/busyindicator.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>PodcastInfoWidget</class>
<extends>QWidget</extends>
<header>podcasts/podcastinfowidget.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>AutoExpandingTreeView</class>
<extends>QTreeView</extends>
<header>widgets/autoexpandingtreeview.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections>
<connection>
<sender>button_box</sender>
<signal>accepted()</signal>
<receiver>AddPodcastDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>836</x>
<y>463</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>button_box</sender>
<signal>rejected()</signal>
<receiver>AddPodcastDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>885</x>
<y>463</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -1,36 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* 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 <QWidget>
#include "addpodcastpage.h"
#include "podcastdiscoverymodel.h"
AddPodcastPage::AddPodcastPage(Application *app, QWidget *parent)
: QWidget(parent), model_(new PodcastDiscoveryModel(app, this)) {}
void AddPodcastPage::SetModel(PodcastDiscoveryModel *model) {
delete model_;
model_ = model;
}

View File

@@ -1,53 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* 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/>.
*
*/
#ifndef ADDPODCASTPAGE_H
#define ADDPODCASTPAGE_H
#include <QWidget>
class Application;
class PodcastDiscoveryModel;
class AddPodcastPage : public QWidget {
Q_OBJECT
public:
explicit AddPodcastPage(Application *app, QWidget *parent = nullptr);
PodcastDiscoveryModel *model() const { return model_; }
virtual bool has_visible_widget() const { return true; }
virtual void Show() {}
signals:
void Busy(bool busy);
protected:
void SetModel(PodcastDiscoveryModel *model);
private:
PodcastDiscoveryModel *model_;
};
#endif // ADDPODCASTPAGE_H

View File

@@ -1,49 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
Copyright 2018, Jim Broadus <jbroadus@gmail.com>
* 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 <QWidget>
#include "core/utilities.h"
#include "episodeinfowidget.h"
#include "ui_episodeinfowidget.h"
EpisodeInfoWidget::EpisodeInfoWidget(QWidget *parent)
: QWidget(parent), ui_(new Ui_EpisodeInfoWidget), app_(nullptr) {
ui_->setupUi(this);
}
EpisodeInfoWidget::~EpisodeInfoWidget() { delete ui_; }
void EpisodeInfoWidget::SetApplication(Application *app) { app_ = app; }
void EpisodeInfoWidget::SetEpisode(const PodcastEpisode &episode) {
episode_ = episode;
ui_->title->setText(episode.title());
ui_->description->setText(episode.description());
ui_->author->setText(episode.author());
ui_->date->setText(episode.publication_date().toString("d MMMM yyyy"));
ui_->duration->setText(Utilities::PrettyTime(episode.duration_secs()));
}

View File

@@ -1,50 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
Copyright 2018, Jim Broadus <jbroadus@gmail.com>
* 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/>.
*
*/
#ifndef EPISODEINFOWIDGET_H
#define EPISODEINFOWIDGET_H
#include <QWidget>
#include "podcastepisode.h"
class Application;
class Ui_EpisodeInfoWidget;
class EpisodeInfoWidget : public QWidget {
Q_OBJECT
public:
explicit EpisodeInfoWidget(QWidget *parent = nullptr);
~EpisodeInfoWidget();
void SetApplication(Application *app);
void SetEpisode(const PodcastEpisode &episode);
private:
Ui_EpisodeInfoWidget *ui_;
Application *app_;
PodcastEpisode episode_;
};
#endif // EPISODEINFOWIDGET_H

View File

@@ -1,137 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>EpisodeInfoWidget</class>
<widget class="QWidget" name="EpisodeInfoWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>398</width>
<height>551</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<property name="styleSheet">
<string notr="true">#title {
font-weight: bold;
}
#description {
font-size: smaller;
}
QLineEdit {
background: transparent;
}</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="sizeConstraint">
<enum>QLayout::SetMinAndMaxSize</enum>
</property>
<item>
<widget class="QLabel" name="title">
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="description">
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item>
<layout class="QGridLayout" name="gridLayout">
<property name="sizeConstraint">
<enum>QLayout::SetMinAndMaxSize</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="author_label">
<property name="text">
<string>Author</string>
</property>
<property name="field_label" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="date">
<property name="frame">
<bool>false</bool>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="author">
<property name="frame">
<bool>false</bool>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="date_label">
<property name="text">
<string>Date</string>
</property>
<property name="field_label" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="duration_label">
<property name="text">
<string>Duration</string>
</property>
<property name="field_label" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="duration">
<property name="frame">
<bool>false</bool>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -1,82 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* 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 <QObject>
#include <QString>
#include <QUrl>
#include <QIcon>
#include <QMessageBox>
#include "podcastdiscoverymodel.h"
#include "podcasturlloader.h"
#include "fixedopmlpage.h"
FixedOpmlPage::FixedOpmlPage(const QUrl &opml_url, const QString &title, const QIcon &icon, Application *app, QWidget *parent)
: AddPodcastPage(app, parent),
loader_(new PodcastUrlLoader(this)),
opml_url_(opml_url),
done_initial_load_(false) {
setWindowTitle(title);
setWindowIcon(icon);
}
void FixedOpmlPage::Show() {
if (!done_initial_load_) {
emit Busy(true);
done_initial_load_ = true;
PodcastUrlLoaderReply *reply = loader_->Load(opml_url_);
QObject::connect(reply, &PodcastUrlLoaderReply::Finished, this, [this, reply]() { LoadFinished(reply); });
}
}
void FixedOpmlPage::LoadFinished(PodcastUrlLoaderReply *reply) {
reply->deleteLater();
emit Busy(false);
if (!reply->is_success()) {
QMessageBox::warning(this, tr("Failed to load podcast"), reply->error_text(), QMessageBox::Close);
return;
}
switch (reply->result_type()) {
case PodcastUrlLoaderReply::Type_Podcast:{
const PodcastList podcasts = reply->podcast_results();
for (const Podcast &podcast : podcasts) {
model()->appendRow(model()->CreatePodcastItem(podcast));
}
break;
}
case PodcastUrlLoaderReply::Type_Opml:
model()->CreateOpmlContainerItems(reply->opml_results(), model()->invisibleRootItem());
break;
}
}

View File

@@ -1,56 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* 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/>.
*
*/
#ifndef FIXEDOPMLPAGE_H
#define FIXEDOPMLPAGE_H
#include <QObject>
#include <QUrl>
#include <QIcon>
#include "addpodcastpage.h"
class Application;
class PodcastUrlLoader;
class PodcastUrlLoaderReply;
class FixedOpmlPage : public AddPodcastPage {
Q_OBJECT
public:
FixedOpmlPage(const QUrl &opml_url, const QString &title, const QIcon &icon, Application *app, QWidget *parent = nullptr);
bool has_visible_widget() const { return false; }
void Show();
private slots:
void LoadFinished(PodcastUrlLoaderReply *reply);
private:
PodcastUrlLoader *loader_;
QUrl opml_url_;
bool done_initial_load_;
};
#endif // FIXEDOPMLPAGE_H

View File

@@ -1,100 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* 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 <QMessageBox>
#include <QPushButton>
#include "core/application.h"
#include "core/iconloader.h"
#include "core/networkaccessmanager.h"
#include "podcast.h"
#include "podcastdiscoverymodel.h"
#include "gpoddersearchpage.h"
#include "ui_gpoddersearchpage.h"
GPodderSearchPage::GPodderSearchPage(Application *app, QWidget *parent)
: AddPodcastPage(app, parent),
ui_(new Ui_GPodderSearchPage),
network_(new NetworkAccessManager(this)),
api_(new mygpo::ApiRequest(network_)) {
ui_->setupUi(this);
QObject::connect(ui_->search, &QPushButton::clicked, this, &GPodderSearchPage::SearchClicked);
setWindowIcon(IconLoader::Load("mygpo"));
}
GPodderSearchPage::~GPodderSearchPage() {
delete ui_;
delete api_;
}
void GPodderSearchPage::SearchClicked() {
emit Busy(true);
mygpo::PodcastListPtr list(api_->search(ui_->query->text()));
QObject::connect(list.data(), &mygpo::PodcastList::finished, this, [this, list]() { SearchFinished(list); });
QObject::connect(list.data(), &mygpo::PodcastList::parseError, this, [this, list]() { SearchFailed(list); });
QObject::connect(list.data(), &mygpo::PodcastList::requestError, this, [this, list]() { SearchFailed(list); });
}
void GPodderSearchPage::SearchFinished(mygpo::PodcastListPtr list) {
emit Busy(false);
model()->clear();
for (mygpo::PodcastPtr gpo_podcast : list->list()) {
Podcast podcast;
podcast.InitFromGpo(gpo_podcast.data());
model()->appendRow(model()->CreatePodcastItem(podcast));
}
}
void GPodderSearchPage::SearchFailed(mygpo::PodcastListPtr list) {
emit Busy(false);
model()->clear();
if (QMessageBox::warning(
nullptr, tr("Failed to fetch podcasts"),
tr("There was a problem communicating with gpodder.net"),
QMessageBox::Retry | QMessageBox::Close,
QMessageBox::Retry) != QMessageBox::Retry) {
return;
}
// Try doing the search again.
SearchClicked();
}
void GPodderSearchPage::Show() { ui_->query->setFocus(); }

View File

@@ -1,57 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* 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/>.
*
*/
#ifndef GPODDERSEARCHPAGE_H
#define GPODDERSEARCHPAGE_H
#include <ApiRequest.h>
#include "addpodcastpage.h"
class QNetworkAccessManager;
class Application;
class Ui_GPodderSearchPage;
class GPodderSearchPage : public AddPodcastPage {
Q_OBJECT
public:
explicit GPodderSearchPage(Application *app, QWidget *parent = nullptr);
~GPodderSearchPage();
void Show();
private slots:
void SearchClicked();
void SearchFinished(mygpo::PodcastListPtr list);
void SearchFailed(mygpo::PodcastListPtr list);
private:
Ui_GPodderSearchPage *ui_;
QNetworkAccessManager *network_;
mygpo::ApiRequest *api_;
};
#endif // GPODDERSEARCHPAGE_H

View File

@@ -1,61 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>GPodderSearchPage</class>
<widget class="QWidget" name="GPodderSearchPage">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>538</width>
<height>69</height>
</rect>
</property>
<property name="windowTitle">
<string>Search gpodder.net</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Enter search terms below to find podcasts on gpodder.net</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLineEdit" name="query"/>
</item>
<item>
<widget class="QPushButton" name="search">
<property name="text">
<string>Search</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<connections>
<connection>
<sender>query</sender>
<signal>returnPressed()</signal>
<receiver>search</receiver>
<slot>click()</slot>
<hints>
<hint type="sourcelabel">
<x>130</x>
<y>45</y>
</hint>
<hint type="destinationlabel">
<x>198</x>
<y>46</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -1,415 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* 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 <QCoreApplication>
#include <QSet>
#include <QList>
#include <QMap>
#include <QString>
#include <QUrl>
#include <QHostInfo>
#include <QNetworkReply>
#include <QNetworkCookieJar>
#include <QSettings>
#include <QTimer>
#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<QUrl, QList<mygpo::EpisodePtr>> 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<mygpo::EpisodePtr> &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<QSharedPointer<mygpo::Episode>> &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<typename T>
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<typename T>
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<typename T::value_type>();
}
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<QUrl> 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<QUrl> &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();
}

View File

@@ -1,125 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* 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/>.
*
*/
#ifndef GPODDERSYNC_H
#define GPODDERSYNC_H
#include <QObject>
#include <QScopedPointer>
#include <QSet>
#include <QList>
#include <QString>
#include <QDateTime>
#include <QUrl>
#include <QNetworkReply>
#include <ApiRequest.h>
#include "podcastepisode.h"
class QTimer;
class Application;
class NetworkAccessManager;
class Podcast;
class PodcastBackend;
class PodcastUrlLoader;
class PodcastUrlLoaderReply;
class GPodderSync : public QObject {
Q_OBJECT
public:
explicit GPodderSync(Application *app, QObject *parent = nullptr);
~GPodderSync();
static const char *kSettingsGroup;
static const int kFlushUpdateQueueDelay;
static const int kGetUpdatesInterval;
static const int kRequestTimeout;
static QString DefaultDeviceName();
static QString DeviceId();
bool is_logged_in() const;
// Tries to login using the given username and password. Also sets the device name and type on gpodder.net.
// If login succeeds the username and password will be saved in QSettings.
void Login(const QString &username, const QString &password, const QString &device_name);
// Clears any saved username and password from QSettings.
void Logout();
signals:
void LoginSuccess();
void LoginFailure(const QString &error);
public slots:
void GetUpdatesNow();
private slots:
void ReloadSettings();
void LoginFinished(QNetworkReply *reply, const QString &username, const QString &password);
void DeviceUpdatesFinished(mygpo::DeviceUpdatesPtr reply);
void DeviceUpdatesParseError();
void DeviceUpdatesRequestError(QNetworkReply::NetworkError error);
void NewPodcastLoaded(PodcastUrlLoaderReply *reply, const QUrl &url, const QList<mygpo::EpisodePtr> &actions);
void ApplyActions(const QList<mygpo::EpisodePtr> &actions, PodcastEpisodeList *episodes);
void SubscriptionAdded(const Podcast &podcast);
void SubscriptionRemoved(const Podcast &podcast);
void FlushUpdateQueue();
void AddRemoveFinished(const QList<QUrl> &affected_urls);
void AddRemoveParseError();
void AddRemoveRequestError(QNetworkReply::NetworkError error);
private:
void LoadQueue();
void SaveQueue();
void DoInitialSync();
private:
Application *app_;
NetworkAccessManager *network_;
QScopedPointer<mygpo::ApiRequest> api_;
PodcastBackend *backend_;
PodcastUrlLoader *loader_;
QString username_;
QString password_;
QDateTime last_successful_get_;
QTimer *get_updates_timer_;
QTimer *flush_queue_timer_;
QSet<QUrl> queued_add_subscriptions_;
QSet<QUrl> queued_remove_subscriptions_;
bool flushing_queue_;
};
#endif // GPODDERSYNC_H

View File

@@ -1,115 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* 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 <QObject>
#include <QMessageBox>
#include <ApiRequest.h>
#include "core/application.h"
#include "gpoddertoptagsmodel.h"
#include "gpoddertoptagspage.h"
#include "podcast.h"
GPodderTopTagsModel::GPodderTopTagsModel(mygpo::ApiRequest *api, Application *app, QObject *parent)
: PodcastDiscoveryModel(app, parent), api_(api) {}
bool GPodderTopTagsModel::hasChildren(const QModelIndex &parent) const {
if (parent.isValid() && parent.data(Role_Type).toInt() == Type_Folder) {
return true;
}
return PodcastDiscoveryModel::hasChildren(parent);
}
bool GPodderTopTagsModel::canFetchMore(const QModelIndex &parent) const {
if (parent.isValid() && parent.data(Role_Type).toInt() == Type_Folder &&
!parent.data(Role_HasLazyLoaded).toBool()) {
return true;
}
return PodcastDiscoveryModel::canFetchMore(parent);
}
void GPodderTopTagsModel::fetchMore(const QModelIndex &parent) {
if (!parent.isValid() || parent.data(Role_Type).toInt() != Type_Folder ||
parent.data(Role_HasLazyLoaded).toBool()) {
return;
}
setData(parent, true, Role_HasLazyLoaded);
// Create a little Loading... item.
itemFromIndex(parent)->appendRow(CreateLoadingIndicator());
mygpo::PodcastListPtr list(api_->podcastsOfTag(GPodderTopTagsPage::kMaxTagCount, parent.data().toString()));
QObject::connect(list.get(), &mygpo::PodcastList::finished, this, [this, parent, list]() { PodcastsOfTagFinished(parent, list.data()); });
QObject::connect(list.get(), &mygpo::PodcastList::parseError, this, [this, parent, list]() { PodcastsOfTagFailed(parent, list.data()); });
QObject::connect(list.get(), &mygpo::PodcastList::requestError, this, [this, parent, list]() { PodcastsOfTagFailed(parent, list.data()); });
}
void GPodderTopTagsModel::PodcastsOfTagFinished(const QModelIndex &parent, mygpo::PodcastList *list) {
QStandardItem *parent_item = itemFromIndex(parent);
if (!parent_item) return;
// Remove the Loading... item.
while (parent_item->hasChildren()) {
parent_item->removeRow(0);
}
for (mygpo::PodcastPtr gpo_podcast : list->list()) {
Podcast podcast;
podcast.InitFromGpo(gpo_podcast.data());
parent_item->appendRow(CreatePodcastItem(podcast));
}
}
void GPodderTopTagsModel::PodcastsOfTagFailed(const QModelIndex &parent, mygpo::PodcastList*) {
QStandardItem *parent_item = itemFromIndex(parent);
if (!parent_item) return;
// Remove the Loading... item.
while (parent_item->hasChildren()) {
parent_item->removeRow(0);
}
if (QMessageBox::warning(nullptr, tr("Failed to fetch podcasts"), tr("There was a problem communicating with gpodder.net"), QMessageBox::Retry | QMessageBox::Close, QMessageBox::Retry) != QMessageBox::Retry) {
return;
}
// Try fetching the list again.
setData(parent, false, Role_HasLazyLoaded);
fetchMore(parent);
}

View File

@@ -1,61 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* 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/>.
*
*/
#ifndef GPODDERTOPTAGSMODEL_H
#define GPODDERTOPTAGSMODEL_H
#include <QObject>
#include "podcastdiscoverymodel.h"
namespace mygpo {
class ApiRequest;
class PodcastList;
} // namespace mygpo
class Application;
class GPodderTopTagsModel : public PodcastDiscoveryModel {
Q_OBJECT
public:
GPodderTopTagsModel(mygpo::ApiRequest *api, Application *app, QObject *parent = nullptr);
enum Role {
Role_HasLazyLoaded = PodcastDiscoveryModel::RoleCount,
RoleCount
};
bool hasChildren(const QModelIndex &parent) const;
bool canFetchMore(const QModelIndex &parent) const;
void fetchMore(const QModelIndex &parent);
private slots:
void PodcastsOfTagFinished(const QModelIndex &parent, mygpo::PodcastList *list);
void PodcastsOfTagFailed(const QModelIndex &parent, mygpo::PodcastList *list);
private:
mygpo::ApiRequest *api_;
};
#endif // GPODDERTOPTAGSMODEL_H

View File

@@ -1,93 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* 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 <QMessageBox>
#include "TagList.h"
#include "core/application.h"
#include "core/networkaccessmanager.h"
#include "core/iconloader.h"
#include "gpoddertoptagsmodel.h"
#include "gpoddertoptagspage.h"
const int GPodderTopTagsPage::kMaxTagCount = 100;
GPodderTopTagsPage::GPodderTopTagsPage(Application *app, QWidget *parent)
: AddPodcastPage(app, parent),
network_(new NetworkAccessManager(this)),
api_(new mygpo::ApiRequest(network_)),
done_initial_load_(false) {
setWindowTitle(tr("gpodder.net directory"));
setWindowIcon(IconLoader::Load("mygpo"));
SetModel(new GPodderTopTagsModel(api_, app, this));
}
GPodderTopTagsPage::~GPodderTopTagsPage() { delete api_; }
void GPodderTopTagsPage::Show() {
if (!done_initial_load_) {
// Start the request for list of top-level tags
emit Busy(true);
done_initial_load_ = true;
mygpo::TagListPtr tag_list(api_->topTags(kMaxTagCount));
QObject::connect(tag_list.get(), &mygpo::TagList::finished, this, [this, tag_list]() { TagListLoaded(tag_list); });
QObject::connect(tag_list.get(), &mygpo::TagList::parseError, this, [this]() { TagListFailed(); });
QObject::connect(tag_list.get(), &mygpo::TagList::requestError, this, [this]() { TagListFailed(); });
}
}
void GPodderTopTagsPage::TagListLoaded(mygpo::TagListPtr tag_list) {
emit Busy(false);
for (mygpo::TagPtr tag : tag_list->list()) {
model()->appendRow(model()->CreateFolder(tag->tag()));
}
}
void GPodderTopTagsPage::TagListFailed() {
emit Busy(false);
done_initial_load_ = false;
if (QMessageBox::warning(
nullptr, tr("Failed to fetch directory"),
tr("There was a problem communicating with gpodder.net"),
QMessageBox::Retry | QMessageBox::Close,
QMessageBox::Retry) != QMessageBox::Retry) {
return;
}
// Try doing the search again.
Show();
}

View File

@@ -1,59 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* 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/>.
*
*/
#ifndef GPODDERTOPTAGSPAGE_H
#define GPODDERTOPTAGSPAGE_H
#include <QObject>
#include <ApiRequest.h>
#include "addpodcastpage.h"
class Application;
class NetworkAccessManager;
class GPodderTopTagsPage : public AddPodcastPage {
Q_OBJECT
public:
explicit GPodderTopTagsPage(Application *app, QWidget *parent = nullptr);
~GPodderTopTagsPage();
static const int kMaxTagCount;
virtual bool has_visible_widget() const { return false; }
virtual void Show();
private slots:
void TagListLoaded(mygpo::TagListPtr tag_list);
void TagListFailed();
private:
NetworkAccessManager *network_;
mygpo::ApiRequest *api_;
bool done_initial_load_;
};
#endif // GPODDERTOPTAGSPAGE_H

View File

@@ -1,133 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2012, 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* 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 <QObject>
#include <QJsonDocument>
#include <QJsonParseError>
#include <QJsonValue>
#include <QJsonObject>
#include <QJsonArray>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QUrl>
#include <QUrlQuery>
#include <QMessageBox>
#include <QPushButton>
#include "core/networkaccessmanager.h"
#include "core/iconloader.h"
#include "podcast.h"
#include "podcastdiscoverymodel.h"
#include "itunessearchpage.h"
#include "ui_itunessearchpage.h"
const char* ITunesSearchPage::kUrlBase = "http://ax.phobos.apple.com.edgesuite.net/WebObjects/MZStoreServices.woa/wa/wsSearch?country=US&media=podcast";
ITunesSearchPage::ITunesSearchPage(Application* app, QWidget* parent)
: AddPodcastPage(app, parent),
ui_(new Ui_ITunesSearchPage),
network_(new NetworkAccessManager(this)) {
ui_->setupUi(this);
QObject::connect(ui_->search, &QPushButton::clicked, this, &ITunesSearchPage::SearchClicked);
setWindowIcon(IconLoader::Load("itunes"));
}
ITunesSearchPage::~ITunesSearchPage() { delete ui_; }
void ITunesSearchPage::SearchClicked() {
emit Busy(true);
QUrl url(QUrl::fromEncoded(kUrlBase));
QUrlQuery url_query;
url_query.addQueryItem("term", ui_->query->text());
url.setQuery(url_query);
QNetworkReply *reply = network_->get(QNetworkRequest(url));
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply]() { SearchFinished(reply); });
}
void ITunesSearchPage::SearchFinished(QNetworkReply* reply) {
reply->deleteLater();
emit Busy(false);
model()->clear();
// Was there a network error?
if (reply->error() != QNetworkReply::NoError) {
QMessageBox::warning(this, tr("Failed to fetch podcasts"), reply->errorString());
return;
}
QJsonParseError error;
QJsonDocument json_document = QJsonDocument::fromJson(reply->readAll(), &error);
if (error.error != QJsonParseError::NoError) {
QMessageBox::warning(this, tr("Failed to fetch podcasts"), tr("There was a problem parsing the response from the iTunes Store"));
return;
}
QJsonObject json_data = json_document.object();
// Was there an error message in the JSON?
if (json_data.contains("errorMessage")) {
QMessageBox::warning(this, tr("Failed to fetch podcasts"), json_data["errorMessage"].toString());
return;
}
QJsonArray array = json_data["results"].toArray();
for (const QJsonValueRef &result : array) {
if (!result.isObject()) continue;
QJsonObject json_result = result.toObject();
if (json_result["kind"].toString() != "podcast") {
continue;
}
if (!json_result.contains("artistName") ||
!json_result.contains("trackName") ||
!json_result.contains("feedUrl") ||
!json_result.contains("trackViewUrl") ||
!json_result.contains("artworkUrl30") ||
!json_result.contains("artworkUrl100")) {
continue;
}
Podcast podcast;
podcast.set_author(json_result["artistName"].toString());
podcast.set_title(json_result["trackName"].toString());
podcast.set_url(QUrl(json_result["feedUrl"].toString()));
podcast.set_link(QUrl(json_result["trackViewUrl"].toString()));
podcast.set_image_url_small(QUrl(json_result["artworkUrl30"].toString()));
podcast.set_image_url_large(QUrl(json_result["artworkUrl100"].toString()));
model()->appendRow(model()->CreatePodcastItem(podcast));
}
}
void ITunesSearchPage::Show() { ui_->query->setFocus(); }

View File

@@ -1,56 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2012, 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* 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/>.
*
*/
#ifndef ITUNESSEARCHPAGE_H
#define ITUNESSEARCHPAGE_H
#include "addpodcastpage.h"
class Ui_ITunesSearchPage;
class QNetworkReply;
class NetworkAccessManager;
class ITunesSearchPage : public AddPodcastPage {
Q_OBJECT
public:
ITunesSearchPage(Application *app, QWidget *parent);
~ITunesSearchPage();
void Show();
private slots:
void SearchClicked();
void SearchFinished(QNetworkReply *reply);
private:
static const char *kUrlBase;
Ui_ITunesSearchPage *ui_;
NetworkAccessManager *network_;
};
#endif // ITUNESSEARCHPAGE_H

View File

@@ -1,61 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ITunesSearchPage</class>
<widget class="QWidget" name="ITunesSearchPage">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>516</width>
<height>69</height>
</rect>
</property>
<property name="windowTitle">
<string>Search iTunes</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="margin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Enter search terms below to find podcasts in the iTunes Store</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLineEdit" name="query"/>
</item>
<item>
<widget class="QPushButton" name="search">
<property name="text">
<string>Search</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<connections>
<connection>
<sender>query</sender>
<signal>returnPressed()</signal>
<receiver>search</receiver>
<slot>click()</slot>
<hints>
<hint type="sourcelabel">
<x>237</x>
<y>52</y>
</hint>
<hint type="destinationlabel">
<x>461</x>
<y>55</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -1,45 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* 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/>.
*
*/
#ifndef OPMLCONTAINER_H
#define OPMLCONTAINER_H
#include <QList>
#include <QString>
#include <QUrl>
#include "podcast.h"
class OpmlContainer {
public:
// Only set for the top-level container
QUrl url;
QString name;
QList<OpmlContainer> containers;
PodcastList feeds;
};
Q_DECLARE_METATYPE(OpmlContainer)
#endif // OPMLCONTAINER_H

View File

@@ -1,194 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* 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 <QSharedData>
#include <QVariant>
#include <QVariantMap>
#include <QString>
#include <QStringList>
#include <QUrl>
#include <QDataStream>
#include <QDateTime>
#include <QSqlQuery>
#include "core/utilities.h"
#include "podcast.h"
#include <Podcast.h>
const QStringList Podcast::kColumns = QStringList() << "url"
<< "title"
<< "description"
<< "copyright"
<< "link"
<< "image_url_large"
<< "image_url_small"
<< "author"
<< "owner_name"
<< "owner_email"
<< "last_updated"
<< "last_update_error"
<< "extra";
const QString Podcast::kColumnSpec = Podcast::kColumns.join(", ");
const QString Podcast::kJoinSpec = Utilities::Prepend("p.", Podcast::kColumns).join(", ");
const QString Podcast::kBindSpec = Utilities::Prepend(":", Podcast::kColumns).join(", ");
const QString Podcast::kUpdateSpec = Utilities::Updateify(Podcast::kColumns).join(", ");
struct Podcast::Private : public QSharedData {
Private();
int database_id_;
QUrl url_;
QString title_;
QString description_;
QString copyright_;
QUrl link_;
QUrl image_url_large_;
QUrl image_url_small_;
// iTunes extensions
QString author_;
QString owner_name_;
QString owner_email_;
QDateTime last_updated_;
QString last_update_error_;
QVariantMap extra_;
// These are stored in a different table
PodcastEpisodeList episodes_;
};
Podcast::Private::Private() : database_id_(-1) {}
Podcast::Podcast() : d(new Private) {}
Podcast::Podcast(const Podcast &other) : d(other.d) {}
Podcast::~Podcast() {}
Podcast &Podcast::operator=(const Podcast &other) {
d = other.d;
return *this;
}
int Podcast::database_id() const { return d->database_id_; }
const QUrl &Podcast::url() const { return d->url_; }
const QString &Podcast::title() const { return d->title_; }
const QString &Podcast::description() const { return d->description_; }
const QString &Podcast::copyright() const { return d->copyright_; }
const QUrl &Podcast::link() const { return d->link_; }
const QUrl &Podcast::image_url_large() const { return d->image_url_large_; }
const QUrl &Podcast::image_url_small() const { return d->image_url_small_; }
const QString &Podcast::author() const { return d->author_; }
const QString &Podcast::owner_name() const { return d->owner_name_; }
const QString &Podcast::owner_email() const { return d->owner_email_; }
const QDateTime &Podcast::last_updated() const { return d->last_updated_; }
const QString &Podcast::last_update_error() const {
return d->last_update_error_;
}
const QVariantMap &Podcast::extra() const { return d->extra_; }
QVariant Podcast::extra(const QString &key) const { return d->extra_[key]; }
void Podcast::set_database_id(const int v) { d->database_id_ = v; }
void Podcast::set_url(const QUrl &v) { d->url_ = v; }
void Podcast::set_title(const QString &v) { d->title_ = v; }
void Podcast::set_description(const QString &v) { d->description_ = v; }
void Podcast::set_copyright(const QString &v) { d->copyright_ = v; }
void Podcast::set_link(const QUrl &v) { d->link_ = v; }
void Podcast::set_image_url_large(const QUrl &v) { d->image_url_large_ = v; }
void Podcast::set_image_url_small(const QUrl &v) { d->image_url_small_ = v; }
void Podcast::set_author(const QString &v) { d->author_ = v; }
void Podcast::set_owner_name(const QString &v) { d->owner_name_ = v; }
void Podcast::set_owner_email(const QString &v) { d->owner_email_ = v; }
void Podcast::set_last_updated(const QDateTime &v) { d->last_updated_ = v; }
void Podcast::set_last_update_error(const QString &v) { d->last_update_error_ = v; }
void Podcast::set_extra(const QVariantMap &v) { d->extra_ = v; }
void Podcast::set_extra(const QString &key, const QVariant &value) { d->extra_[key] = value; }
const PodcastEpisodeList &Podcast::episodes() const { return d->episodes_; }
PodcastEpisodeList* Podcast::mutable_episodes() { return &d->episodes_; }
void Podcast::set_episodes(const PodcastEpisodeList &v) { d->episodes_ = v; }
void Podcast::add_episode(const PodcastEpisode &episode) { d->episodes_.append(episode); }
void Podcast::InitFromQuery(const QSqlQuery &query) {
d->database_id_ = query.value(0).toInt();
d->url_ = QUrl::fromEncoded(query.value(1).toByteArray());
d->title_ = query.value(2).toString();
d->description_ = query.value(3).toString();
d->copyright_ = query.value(4).toString();
d->link_ = QUrl::fromEncoded(query.value(5).toByteArray());
d->image_url_large_ = QUrl::fromEncoded(query.value(6).toByteArray());
d->image_url_small_ = QUrl::fromEncoded(query.value(7).toByteArray());
d->author_ = query.value(8).toString();
d->owner_name_ = query.value(9).toString();
d->owner_email_ = query.value(10).toString();
d->last_updated_ = QDateTime::fromSecsSinceEpoch(query.value(11).toUInt());
d->last_update_error_ = query.value(12).toString();
QDataStream extra_stream(query.value(13).toByteArray());
extra_stream >> d->extra_;
}
void Podcast::BindToQuery(QSqlQuery* query) const {
query->bindValue(":url", d->url_.toEncoded());
query->bindValue(":title", d->title_);
query->bindValue(":description", d->description_);
query->bindValue(":copyright", d->copyright_);
query->bindValue(":link", d->link_.toEncoded());
query->bindValue(":image_url_large", d->image_url_large_.toEncoded());
query->bindValue(":image_url_small", d->image_url_small_.toEncoded());
query->bindValue(":author", d->author_);
query->bindValue(":owner_name", d->owner_name_);
query->bindValue(":owner_email", d->owner_email_);
query->bindValue(":last_updated", d->last_updated_.toSecsSinceEpoch());
query->bindValue(":last_update_error", d->last_update_error_);
QByteArray extra;
QDataStream extra_stream(&extra, QIODevice::WriteOnly);
extra_stream << d->extra_;
query->bindValue(":extra", extra);
}
void Podcast::InitFromGpo(const mygpo::Podcast* podcast) {
d->url_ = podcast->url();
d->title_ = podcast->title();
d->description_ = podcast->description();
d->link_ = podcast->website();
d->image_url_large_ = podcast->logoUrl();
set_extra("gpodder:subscribers", podcast->subscribers());
set_extra("gpodder:subscribers_last_week", podcast->subscribersLastWeek());
set_extra("gpodder:page", podcast->mygpoUrl());
}

View File

@@ -1,114 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* 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/>.
*
*/
#ifndef PODCAST_H
#define PODCAST_H
#include <QSharedData>
#include <QSharedDataPointer>
#include <QVariant>
#include <QVariantMap>
#include <QString>
#include <QStringList>
#include <QUrl>
#include <QSqlQuery>
#include "podcastepisode.h"
namespace mygpo {
class Podcast;
} // namespace mygpo
class Podcast {
public:
Podcast();
Podcast(const Podcast &other);
~Podcast();
static const QStringList kColumns;
static const QString kColumnSpec;
static const QString kJoinSpec;
static const QString kBindSpec;
static const QString kUpdateSpec;
void InitFromQuery(const QSqlQuery &query);
void InitFromGpo(const mygpo::Podcast *podcast);
void BindToQuery(QSqlQuery *query) const;
bool is_valid() const { return database_id() != -1; }
int database_id() const;
const QUrl &url() const;
const QString &title() const;
const QString &description() const;
const QString &copyright() const;
const QUrl &link() const;
const QUrl &image_url_large() const;
const QUrl &image_url_small() const;
const QString &author() const;
const QString &owner_name() const;
const QString &owner_email() const;
const QDateTime &last_updated() const;
const QString &last_update_error() const;
const QVariantMap &extra() const;
QVariant extra(const QString &key) const;
void set_database_id(const int v);
void set_url(const QUrl &v);
void set_title(const QString &v);
void set_description(const QString &v);
void set_copyright(const QString &v);
void set_link(const QUrl &v);
void set_image_url_large(const QUrl &v);
void set_image_url_small(const QUrl &v);
void set_author(const QString &v);
void set_owner_name(const QString &v);
void set_owner_email(const QString &v);
void set_last_updated(const QDateTime &v);
void set_last_update_error(const QString &v);
void set_extra(const QVariantMap &v);
void set_extra(const QString &key, const QVariant &value);
// Small images are suitable for 16x16 icons in lists. Large images are used in detailed information displays.
const QUrl &ImageUrlLarge() const { return image_url_large().isValid() ? image_url_large() : image_url_small(); }
const QUrl &ImageUrlSmall() const { return image_url_small().isValid() ? image_url_small() : image_url_large(); }
// These are stored in a different database table, and aren't loaded or persisted by InitFromQuery or BindToQuery.
const PodcastEpisodeList &episodes() const;
PodcastEpisodeList *mutable_episodes();
void set_episodes(const PodcastEpisodeList &v);
void add_episode(const PodcastEpisode &episode);
Podcast &operator=(const Podcast &other);
private:
struct Private;
QSharedDataPointer<Private> d;
};
Q_DECLARE_METATYPE(Podcast)
typedef QList<Podcast> PodcastList;
Q_DECLARE_METATYPE(QList<Podcast>)
#endif // PODCAST_H

View File

@@ -1,368 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* 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 <QMutexLocker>
#include <QSqlDatabase>
#include <QSqlQuery>
#include <QUrl>
#include "core/application.h"
#include "core/database.h"
#include "core/logging.h"
#include "core/scopedtransaction.h"
#include "podcastbackend.h"
PodcastBackend::PodcastBackend(Application *app, QObject *parent)
: QObject(parent), app_(app), db_(app->database()) {}
void PodcastBackend::Subscribe(Podcast *podcast) {
// If this podcast is already in the database, do nothing
if (podcast->is_valid()) {
return;
}
// If there's an entry in the database with the same URL, take its data.
Podcast existing_podcast = GetSubscriptionByUrl(podcast->url());
if (existing_podcast.is_valid()) {
*podcast = existing_podcast;
return;
}
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
ScopedTransaction t(&db);
// Insert the podcast.
QSqlQuery q(db);
q.prepare("INSERT INTO podcasts (" + Podcast::kColumnSpec + ") VALUES (" + Podcast::kBindSpec + ")");
podcast->BindToQuery(&q);
q.exec();
if (db_->CheckErrors(q)) return;
// Update the database ID.
const int database_id = q.lastInsertId().toInt();
podcast->set_database_id(database_id);
// Update the IDs of any episodes.
PodcastEpisodeList *episodes = podcast->mutable_episodes();
for (auto it = episodes->begin(); it != episodes->end(); ++it) {
it->set_podcast_database_id(database_id);
}
// Add those episodes to the database.
AddEpisodes(episodes, &db);
t.Commit();
emit SubscriptionAdded(*podcast);
}
void PodcastBackend::Unsubscribe(const Podcast &podcast) {
// If this podcast is not already in the database, do nothing
if (!podcast.is_valid()) {
return;
}
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
ScopedTransaction t(&db);
// Remove the podcast.
QSqlQuery q(db);
q.prepare("DELETE FROM podcasts WHERE ROWID = :id");
q.bindValue(":id", podcast.database_id());
q.exec();
if (db_->CheckErrors(q)) return;
// Remove all episodes in the podcast
q.prepare("DELETE FROM podcast_episodes WHERE podcast_id = :id");
q.bindValue(":id", podcast.database_id());
q.exec();
if (db_->CheckErrors(q)) return;
t.Commit();
emit SubscriptionRemoved(podcast);
}
void PodcastBackend::AddEpisodes(PodcastEpisodeList *episodes, QSqlDatabase *db) {
QSqlQuery q(*db);
q.prepare("INSERT INTO podcast_episodes (" + PodcastEpisode::kColumnSpec + ") VALUES (" + PodcastEpisode::kBindSpec + ")");
for (auto it = episodes->begin(); it != episodes->end(); ++it) {
it->BindToQuery(&q);
q.exec();
if (db_->CheckErrors(q)) continue;
const int database_id = q.lastInsertId().toInt();
it->set_database_id(database_id);
}
}
void PodcastBackend::AddEpisodes(PodcastEpisodeList *episodes) {
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
ScopedTransaction t(&db);
AddEpisodes(episodes, &db);
t.Commit();
emit EpisodesAdded(*episodes);
}
void PodcastBackend::UpdateEpisodes(const PodcastEpisodeList &episodes) {
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
ScopedTransaction t(&db);
QSqlQuery q(db);
q.prepare("UPDATE podcast_episodes SET listened = :listened, listened_date = :listened_date, downloaded = :downloaded, local_url = :local_url WHERE ROWID = :id");
for (const PodcastEpisode &episode : episodes) {
q.bindValue(":listened", episode.listened());
q.bindValue(":listened_date", episode.listened_date().toSecsSinceEpoch());
q.bindValue(":downloaded", episode.downloaded());
q.bindValue(":local_url", episode.local_url().toEncoded());
q.bindValue(":id", episode.database_id());
q.exec();
db_->CheckErrors(q);
}
t.Commit();
emit EpisodesUpdated(episodes);
}
PodcastList PodcastBackend::GetAllSubscriptions() {
PodcastList ret;
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
QSqlQuery q(db);
q.prepare("SELECT ROWID, " + Podcast::kColumnSpec + " FROM podcasts");
q.exec();
if (db_->CheckErrors(q)) return ret;
while (q.next()) {
Podcast podcast;
podcast.InitFromQuery(q);
ret << podcast;
}
return ret;
}
Podcast PodcastBackend::GetSubscriptionById(const int id) {
Podcast ret;
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
QSqlQuery q(db);
q.prepare("SELECT ROWID, " + Podcast::kColumnSpec + " FROM podcasts WHERE ROWID = :id");
q.bindValue(":id", id);
q.exec();
if (!db_->CheckErrors(q) && q.next()) {
ret.InitFromQuery(q);
}
return ret;
}
Podcast PodcastBackend::GetSubscriptionByUrl(const QUrl &url) {
Podcast ret;
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
QSqlQuery q(db);
q.prepare("SELECT ROWID, " + Podcast::kColumnSpec + " FROM podcasts WHERE url = :url");
q.bindValue(":url", url.toEncoded());
q.exec();
if (!db_->CheckErrors(q) && q.next()) {
ret.InitFromQuery(q);
}
return ret;
}
PodcastEpisodeList PodcastBackend::GetEpisodes(const int podcast_id) {
PodcastEpisodeList ret;
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
QSqlQuery q(db);
q.prepare("SELECT ROWID, " + PodcastEpisode::kColumnSpec + " FROM podcast_episodes WHERE podcast_id = :id ORDER BY publication_date DESC");
q.bindValue(":id", podcast_id);
q.exec();
if (db_->CheckErrors(q)) return ret;
while (q.next()) {
PodcastEpisode episode;
episode.InitFromQuery(q);
ret << episode;
}
return ret;
}
PodcastEpisode PodcastBackend::GetEpisodeById(const int id) {
PodcastEpisode ret;
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
QSqlQuery q(db);
q.prepare("SELECT ROWID, " + PodcastEpisode::kColumnSpec + " FROM podcast_episodes WHERE ROWID = :id");
q.bindValue(":id", id);
q.exec();
if (!db_->CheckErrors(q) && q.next()) {
ret.InitFromQuery(q);
}
return ret;
}
PodcastEpisode PodcastBackend::GetEpisodeByUrl(const QUrl &url) {
PodcastEpisode ret;
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
QSqlQuery q(db);
q.prepare("SELECT ROWID, " + PodcastEpisode::kColumnSpec + " FROM podcast_episodes WHERE url = :url");
q.bindValue(":url", url.toEncoded());
q.exec();
if (!db_->CheckErrors(q) && q.next()) {
ret.InitFromQuery(q);
}
return ret;
}
PodcastEpisode PodcastBackend::GetEpisodeByUrlOrLocalUrl(const QUrl &url) {
PodcastEpisode ret;
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
QSqlQuery q(db);
q.prepare("SELECT ROWID, " + PodcastEpisode::kColumnSpec + " FROM podcast_episodes WHERE url = :url OR local_url = :url");
q.bindValue(":url", url.toEncoded());
q.exec();
if (!db_->CheckErrors(q) && q.next()) {
ret.InitFromQuery(q);
}
return ret;
}
PodcastEpisodeList PodcastBackend::GetOldDownloadedEpisodes(const QDateTime &max_listened_date) {
PodcastEpisodeList ret;
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
QSqlQuery q(db);
q.prepare("SELECT ROWID, " + PodcastEpisode::kColumnSpec + " FROM podcast_episodes WHERE downloaded = 'true' AND listened_date <= :max_listened_date");
q.bindValue(":max_listened_date", max_listened_date.toSecsSinceEpoch());
q.exec();
if (db_->CheckErrors(q)) return ret;
while (q.next()) {
PodcastEpisode episode;
episode.InitFromQuery(q);
ret << episode;
}
return ret;
}
PodcastEpisode PodcastBackend::GetOldestDownloadedListenedEpisode() {
PodcastEpisode ret;
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
QSqlQuery q(db);
q.prepare("SELECT ROWID, " + PodcastEpisode::kColumnSpec + " FROM podcast_episodes WHERE downloaded = 'true' AND listened = 'true' ORDER BY listened_date ASC");
q.exec();
if (db_->CheckErrors(q)) return ret;
q.next();
ret.InitFromQuery(q);
return ret;
}
PodcastEpisodeList PodcastBackend::GetNewDownloadedEpisodes() {
PodcastEpisodeList ret;
QMutexLocker l(db_->Mutex());
QSqlDatabase db(db_->Connect());
QSqlQuery q(db);
q.prepare("SELECT ROWID, " + PodcastEpisode::kColumnSpec + " FROM podcast_episodes WHERE downloaded = 'true' AND listened = 'false'");
q.exec();
if (db_->CheckErrors(q)) return ret;
while (q.next()) {
PodcastEpisode episode;
episode.InitFromQuery(q);
ret << episode;
}
return ret;
}

View File

@@ -1,98 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* 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/>.
*
*/
#ifndef PODCASTBACKEND_H
#define PODCASTBACKEND_H
#include <QObject>
#include <QDateTime>
#include <QUrl>
#include "podcast.h"
class QSqlDatabase;
class Application;
class Database;
class PodcastBackend : public QObject {
Q_OBJECT
public:
explicit PodcastBackend(Application *app, QObject *parent = nullptr);
// Adds the podcast and any included Episodes to the database.
// Updates the podcast with a database ID.
// If this podcast already has an ID set, this function does nothing.
// If a podcast with this URL already exists in the database,
// this function just updates the ID field in the provided podcast.
void Subscribe(Podcast *podcast);
// Removes the Podcast with the given ID from the database.
// Also removes any episodes associated with this podcast.
void Unsubscribe(const Podcast &podcast);
// Returns a list of all the subscribed podcasts.
// For efficiency the Podcast objects returned won't contain any PodcastEpisode objects - get them separately if you want them.
PodcastList GetAllSubscriptions();
Podcast GetSubscriptionById(const int id);
Podcast GetSubscriptionByUrl(const QUrl &url);
// Returns podcast episodes that match various keys. All these queries are indexed.
PodcastEpisodeList GetEpisodes(const int podcast_id);
PodcastEpisode GetEpisodeById(const int id);
PodcastEpisode GetEpisodeByUrl(const QUrl &url);
PodcastEpisode GetEpisodeByUrlOrLocalUrl(const QUrl &url);
PodcastEpisode GetOldestDownloadedListenedEpisode();
// Returns a list of episodes that have local data (downloaded=true) but were last listened to before the given QDateTime.
// This query is NOT indexed so it involves a full search of the table.
PodcastEpisodeList GetOldDownloadedEpisodes(const QDateTime &max_listened_date);
PodcastEpisodeList GetNewDownloadedEpisodes();
// Adds episodes to the database. Every episode must have a valid podcast_database_id set already.
void AddEpisodes(PodcastEpisodeList *episodes);
// Updates the editable fields (listened, listened_date, downloaded, and local_url) on episodes that must already exist in the database.
void UpdateEpisodes(const PodcastEpisodeList &episodes);
signals:
void SubscriptionAdded(const Podcast &podcast);
void SubscriptionRemoved(const Podcast &podcast);
// Emitted when episodes are added to a subscription that *already exists*.
void EpisodesAdded(const PodcastEpisodeList &episodes);
// Emitted when existing episodes are updated.
void EpisodesUpdated(const PodcastEpisodeList &episodes);
private:
// Adds each episode to the database, setting their IDs after inserting each one.
void AddEpisodes(PodcastEpisodeList *episodes, QSqlDatabase *db);
private:
Application *app_;
Database *db_;
};
#endif // PODCASTBACKEND_H

View File

@@ -1,124 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* 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 <QObject>
#include <QFile>
#include <QDateTime>
#include <QTimer>
#include <QSettings>
#include "core/application.h"
#include "core/logging.h"
#include "core/timeconstants.h"
#include "core/utilities.h"
#include "collection/collectiondirectorymodel.h"
#include "collection/collectionmodel.h"
#include "podcastbackend.h"
#include "podcastdeleter.h"
const char *PodcastDeleter::kSettingsGroup = "Podcasts";
const int PodcastDeleter::kAutoDeleteCheckIntervalMsec = 60 * 6 * 60 * kMsecPerSec;
PodcastDeleter::PodcastDeleter(Application *app, QObject *parent)
: QObject(parent),
app_(app),
backend_(app_->podcast_backend()),
delete_after_secs_(0),
auto_delete_timer_(new QTimer(this)) {
ReloadSettings();
auto_delete_timer_->setSingleShot(true);
AutoDelete();
QObject::connect(auto_delete_timer_, &QTimer::timeout, this, &PodcastDeleter::AutoDelete);
QObject::connect(app_, &Application::SettingsChanged, this, &PodcastDeleter::ReloadSettings);
}
void PodcastDeleter::DeleteEpisode(const PodcastEpisode &episode) {
// Delete the local file
if (!QFile::remove(episode.local_url().toLocalFile())) {
qLog(Warning) << "The local file" << episode.local_url().toLocalFile() << "could not be removed";
}
// Update the episode in the DB
PodcastEpisode episode_copy(episode);
episode_copy.set_downloaded(false);
episode_copy.set_local_url(QUrl());
episode_copy.set_listened_date(QDateTime());
backend_->UpdateEpisodes(PodcastEpisodeList() << episode_copy);
}
void PodcastDeleter::ReloadSettings() {
QSettings s;
s.beginGroup(kSettingsGroup);
delete_after_secs_ = s.value("delete_after", 0).toInt();
s.endGroup();
AutoDelete();
}
void PodcastDeleter::AutoDelete() {
if (delete_after_secs_ <= 0) {
return;
}
auto_delete_timer_->stop();
QDateTime max_date = QDateTime::currentDateTime();
qint64 timeout_ms;
PodcastEpisode oldest_episode;
QDateTime oldest_episode_time;
max_date = max_date.addSecs(-delete_after_secs_);
PodcastEpisodeList old_episodes = backend_->GetOldDownloadedEpisodes(max_date);
qLog(Info) << "Deleting" << old_episodes.count()
<< "episodes because they were last listened to"
<< (delete_after_secs_ / kSecsPerDay) << "days ago";
for (const PodcastEpisode& episode : old_episodes) {
DeleteEpisode(episode);
}
oldest_episode = backend_->GetOldestDownloadedListenedEpisode();
if (!oldest_episode.listened_date().isValid()) {
oldest_episode_time = QDateTime::currentDateTime();
}
else {
oldest_episode_time = oldest_episode.listened_date();
}
timeout_ms = QDateTime::currentDateTime().toMSecsSinceEpoch();
timeout_ms -= oldest_episode_time.toMSecsSinceEpoch();
timeout_ms = (delete_after_secs_ * kMsecPerSec) - timeout_ms;
qLog(Info) << "Timeout for autodelete set to:" << timeout_ms << "ms";
if (timeout_ms >= 0) {
auto_delete_timer_->setInterval(timeout_ms);
}
else {
auto_delete_timer_->setInterval(kAutoDeleteCheckIntervalMsec);
}
auto_delete_timer_->start();
}

View File

@@ -1,56 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* 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/>.
*
*/
#ifndef PODCASTDELETER_H
#define PODCASTDELETER_H
#include <QObject>
#include "podcast.h"
#include "podcastepisode.h"
class QTimer;
class Application;
class PodcastBackend;
class PodcastDeleter : public QObject {
Q_OBJECT
public:
explicit PodcastDeleter(Application *app, QObject *parent = nullptr);
static const char *kSettingsGroup;
static const int kAutoDeleteCheckIntervalMsec;
public slots:
// Deletes downloaded data for this episode
void DeleteEpisode(const PodcastEpisode &episode);
void AutoDelete();
void ReloadSettings();
private:
Application *app_;
PodcastBackend *backend_;
int delete_after_secs_;
QTimer *auto_delete_timer_;
};
#endif // PODCASTDELETER_H

View File

@@ -1,125 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* 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 "podcastdiscoverymodel.h"
#include <QStandardItemModel>
#include <QStandardItem>
#include <QSet>
#include <QString>
#include <QUrl>
#include <QIcon>
#include "core/application.h"
#include "core/iconloader.h"
#include "core/standarditemiconloader.h"
#include "opmlcontainer.h"
#include "podcast.h"
PodcastDiscoveryModel::PodcastDiscoveryModel(Application *app, QObject *parent)
: QStandardItemModel(parent),
app_(app),
icon_loader_(new StandardItemIconLoader(app->album_cover_loader(), this)),
default_icon_(IconLoader::Load("podcast")) {
icon_loader_->SetModel(this);
}
QVariant PodcastDiscoveryModel::data(const QModelIndex &idx, int role) const {
if (idx.isValid() && role == Qt::DecorationRole && !QStandardItemModel::data(idx, Role_StartedLoadingImage).toBool()) {
const QUrl image_url = QStandardItemModel::data(idx, Role_ImageUrl).toUrl();
if (image_url.isValid()) {
const_cast<PodcastDiscoveryModel*>(this)->LazyLoadImage(image_url, idx);
}
}
return QStandardItemModel::data(idx, role);
}
QStandardItem *PodcastDiscoveryModel::CreatePodcastItem(const Podcast &podcast) {
QStandardItem *item = new QStandardItem;
item->setIcon(default_icon_);
item->setText(podcast.title());
item->setData(QVariant::fromValue(podcast), Role_Podcast);
item->setData(Type_Podcast, Role_Type);
item->setData(podcast.ImageUrlSmall(), Role_ImageUrl);
return item;
}
QStandardItem *PodcastDiscoveryModel::CreateFolder(const QString &name) {
if (folder_icon_.isNull()) {
folder_icon_ = IconLoader::Load("folder");
}
QStandardItem *item = new QStandardItem;
item->setIcon(folder_icon_);
item->setText(name);
item->setData(Type_Folder, Role_Type);
return item;
}
QStandardItem *PodcastDiscoveryModel::CreateOpmlContainerItem(const OpmlContainer &container) {
QStandardItem *item = CreateFolder(container.name);
CreateOpmlContainerItems(container, item);
return item;
}
void PodcastDiscoveryModel::CreateOpmlContainerItems(const OpmlContainer &container, QStandardItem *parent) {
for (const OpmlContainer &child : container.containers) {
QStandardItem *child_item = CreateOpmlContainerItem(child);
parent->appendRow(child_item);
}
for (const Podcast &child : container.feeds) {
QStandardItem *child_item = CreatePodcastItem(child);
parent->appendRow(child_item);
}
}
void PodcastDiscoveryModel::LazyLoadImage(const QUrl &url, const QModelIndex &idx) {
QStandardItem *item = itemFromIndex(idx);
item->setData(true, Role_StartedLoadingImage);
icon_loader_->LoadIcon(url, QUrl(), item);
}
QStandardItem *PodcastDiscoveryModel::CreateLoadingIndicator() {
QStandardItem *item = new QStandardItem;
item->setText(tr("Loading..."));
item->setData(Type_LoadingIndicator, Role_Type);
return item;
}

View File

@@ -1,79 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* 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/>.
*
*/
#ifndef PODCASTDISCOVERYMODEL_H
#define PODCASTDISCOVERYMODEL_H
#include <QStandardItemModel>
#include <QString>
#include <QUrl>
#include <QIcon>
#include "covermanager/albumcoverloaderoptions.h"
class Application;
class OpmlContainer;
class OpmlFeed;
class Podcast;
class StandardItemIconLoader;
class PodcastDiscoveryModel : public QStandardItemModel {
Q_OBJECT
public:
explicit PodcastDiscoveryModel(Application *app, QObject *parent = nullptr);
enum Type {
Type_Folder,
Type_Podcast,
Type_LoadingIndicator
};
enum Role {
Role_Podcast = Qt::UserRole,
Role_Type,
Role_ImageUrl,
Role_StartedLoadingImage,
RoleCount
};
void CreateOpmlContainerItems(const OpmlContainer &container, QStandardItem *parent);
QStandardItem *CreateOpmlContainerItem(const OpmlContainer &container);
QStandardItem *CreatePodcastItem(const Podcast &podcast);
QStandardItem *CreateFolder(const QString &name);
QStandardItem *CreateLoadingIndicator();
QVariant data(const QModelIndex &idx, int role) const override;
private:
void LazyLoadImage(const QUrl &url, const QModelIndex &idx);
private:
Application *app_;
StandardItemIconLoader *icon_loader_;
QIcon default_icon_;
QIcon folder_icon_;
};
#endif // PODCASTDISCOVERYMODEL_H

View File

@@ -1,288 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* 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 "podcastdownloader.h"
#include <QString>
#include <QDateTime>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QList>
#include <QString>
#include <QNetworkRequest>
#include <QNetworkReply>
#include <QSettings>
#include <QTimer>
#include "core/application.h"
#include "core/logging.h"
#include "core/networkaccessmanager.h"
#include "core/tagreaderclient.h"
#include "core/timeconstants.h"
#include "core/utilities.h"
#include "podcastbackend.h"
const char *PodcastDownloader::kSettingsGroup = "Podcasts";
Task::Task(const PodcastEpisode &episode, QFile *file, PodcastBackend *backend)
: file_(file),
episode_(episode),
backend_(backend),
network_(new NetworkAccessManager(this)),
req_(QNetworkRequest(episode.url())),
reply_(network_->get(req_)) {
QObject::connect(reply_, &QNetworkReply::readyRead, this, &Task::reading);
QObject::connect(reply_, &QNetworkReply::finished, this, &Task::finishedInternal);
QObject::connect(reply_, &QNetworkReply::downloadProgress, this, &Task::downloadProgressInternal);
emit ProgressChanged(episode_, PodcastDownload::Queued, 0);
}
PodcastEpisode Task::episode() const { return episode_; }
void Task::reading() {
qint64 bytes = 0;
forever {
bytes = reply_->bytesAvailable();
if (bytes <= 0) break;
file_->write(reply_->read(bytes));
}
}
void Task::finishedPublic() {
disconnect(reply_, &QNetworkReply::readyRead, nullptr, nullptr);
disconnect(reply_, &QNetworkReply::downloadProgress, nullptr, nullptr);
disconnect(reply_, &QNetworkReply::finished, nullptr, nullptr);
emit ProgressChanged(episode_, PodcastDownload::NotDownloading, 0);
// Delete the file
file_->remove();
emit finished(this);
}
void Task::finishedInternal() {
reply_->deleteLater();
if (reply_->error() != QNetworkReply::NoError) {
qLog(Warning) << "Error downloading episode:" << reply_->errorString();
emit ProgressChanged(episode_, PodcastDownload::NotDownloading, 0);
// Delete the file
file_->remove();
emit finished(this);
reply_ = nullptr;
return;
}
qLog(Info) << "Download of" << file_->fileName() << "finished";
// Tell the database the episode has been updated. Get it from the DB again in case the listened field changed in the mean time.
PodcastEpisode episode = episode_;
episode.set_downloaded(true);
episode.set_local_url(QUrl::fromLocalFile(file_->fileName()));
backend_->UpdateEpisodes(PodcastEpisodeList() << episode);
Podcast podcast = backend_->GetSubscriptionById(episode.podcast_database_id());
Song song = episode_.ToSong(podcast);
emit ProgressChanged(episode_, PodcastDownload::Finished, 0);
// I didn't ecountered even a single podcast with a correct metadata
TagReaderClient::Instance()->SaveFileBlocking(file_->fileName(), song);
emit finished(this);
reply_ = nullptr;
}
void Task::downloadProgressInternal(qint64 received, qint64 total) {
if (total <= 0) {
emit ProgressChanged(episode_, PodcastDownload::Downloading, 0);
}
else {
emit ProgressChanged(episode_, PodcastDownload::Downloading, static_cast<float>(received) / total * 100);
}
}
PodcastDownloader::PodcastDownloader(Application *app, QObject *parent)
: QObject(parent),
app_(app),
backend_(app_->podcast_backend()),
network_(new NetworkAccessManager(this)),
disallowed_filename_characters_("[^a-zA-Z0-9_~ -]"),
auto_download_(false) {
QObject::connect(backend_, &PodcastBackend::EpisodesAdded, this, &PodcastDownloader::EpisodesAdded);
QObject::connect(backend_, &PodcastBackend::SubscriptionAdded, this, &PodcastDownloader::SubscriptionAdded);
QObject::connect(app_, &Application::SettingsChanged, this, &PodcastDownloader::ReloadSettings);
ReloadSettings();
}
QString PodcastDownloader::DefaultDownloadDir() const {
return QDir::homePath() + "/Podcasts";
}
void PodcastDownloader::ReloadSettings() {
QSettings s;
s.beginGroup(kSettingsGroup);
auto_download_ = s.value("auto_download", false).toBool();
download_dir_ = s.value("download_dir", DefaultDownloadDir()).toString();
}
QString PodcastDownloader::FilenameForEpisode(const QString &directory, const PodcastEpisode &episode) const {
const QString file_extension = QFileInfo(episode.url().path()).suffix();
int count = 0;
// The file name contains the publication date and episode title
QString base_filename = episode.publication_date().date().toString(Qt::ISODate) + "-" + SanitiseFilenameComponent(episode.title());
// Add numbers on to the end of the filename until we find one that doesn't exist.
forever {
QString filename;
if (count == 0) {
filename = QString("%1/%2.%3").arg(directory, base_filename, file_extension);
}
else {
filename = QString("%1/%2 (%3).%4").arg(directory, base_filename, QString::number(count), file_extension);
}
if (!QFile::exists(filename)) {
return filename;
}
++count;
}
}
void PodcastDownloader::DownloadEpisode(const PodcastEpisode &episode) {
for (Task *tas : list_tasks_) {
if (tas->episode().database_id() == episode.database_id()) {
return;
}
}
Podcast podcast = backend_->GetSubscriptionById(episode.podcast_database_id());
if (!podcast.is_valid()) {
qLog(Warning) << "The podcast that contains episode" << episode.url() << "doesn't exist any more";
return;
}
const QString directory = download_dir_ + "/" + SanitiseFilenameComponent(podcast.title());
const QString filepath = FilenameForEpisode(directory, episode);
// Open the output file
if (!QDir(directory).exists()) QDir().mkpath(directory);
QFile *file = new QFile(filepath);
if (!file->open(QIODevice::WriteOnly)) {
qLog(Warning) << "Could not open the file" << filepath << "for writing";
return;
}
Task *task = new Task(episode, file, backend_);
list_tasks_ << task;
qLog(Info) << "Downloading" << task->episode().url() << "to" << filepath;
QObject::connect(task, &Task::finished, this, &PodcastDownloader::ReplyFinished);
QObject::connect(task, &Task::ProgressChanged, this, &PodcastDownloader::ProgressChanged);
}
void PodcastDownloader::ReplyFinished(Task *task) {
list_tasks_.removeAll(task);
delete task;
}
QString PodcastDownloader::SanitiseFilenameComponent(const QString &text) const {
return QString(text).replace(disallowed_filename_characters_, " ") .simplified();
}
void PodcastDownloader::SubscriptionAdded(const Podcast &podcast) {
EpisodesAdded(podcast.episodes());
}
void PodcastDownloader::EpisodesAdded(const PodcastEpisodeList &episodes) {
if (auto_download_) {
for (const PodcastEpisode &episode : episodes) {
DownloadEpisode(episode);
}
}
}
PodcastEpisodeList PodcastDownloader::EpisodesDownloading(const PodcastEpisodeList &episodes) {
PodcastEpisodeList ret;
for (Task *tas : list_tasks_) {
for (const PodcastEpisode &episode : episodes) {
if (tas->episode().database_id() == episode.database_id()) {
ret << episode;
}
}
}
return ret;
}
void PodcastDownloader::cancelDownload(const PodcastEpisodeList &episodes) {
QList<Task*> ta;
for (Task *tas : list_tasks_) {
for (const PodcastEpisode &episode : episodes) {
if (tas->episode().database_id() == episode.database_id()) {
ta << tas;
}
}
}
for (Task *tas : ta) {
tas->finishedPublic();
list_tasks_.removeAll(tas);
}
}

View File

@@ -1,129 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* 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/>.
*
*/
#ifndef PODCASTDOWNLOADER_H
#define PODCASTDOWNLOADER_H
#include <memory>
#include <QObject>
#include <QFile>
#include <QSet>
#include <QList>
#include <QQueue>
#include <QString>
#include <QRegularExpression>
#include <QNetworkRequest>
#include "core/networkaccessmanager.h"
#include "podcast.h"
#include "podcastepisode.h"
class Application;
class PodcastBackend;
class NetworkAccessManager;
class QNetworkReply;
namespace PodcastDownload {
enum State {
NotDownloading,
Queued,
Downloading,
Finished
};
}
class Task : public QObject {
Q_OBJECT
public:
Task(const PodcastEpisode &episode, QFile *file, PodcastBackend *backend);
PodcastEpisode episode() const;
signals:
void ProgressChanged(const PodcastEpisode &episode, const PodcastDownload::State state, const int percent);
void finished(Task *task);
public slots:
void finishedPublic();
private slots:
void reading();
void downloadProgressInternal(qint64 received, qint64 total);
void finishedInternal();
private:
std::unique_ptr<QFile> file_;
PodcastEpisode episode_;
PodcastBackend *backend_;
std::unique_ptr<NetworkAccessManager> network_;
QNetworkRequest req_;
QNetworkReply *reply_;
};
class PodcastDownloader : public QObject {
Q_OBJECT
public:
explicit PodcastDownloader(Application *app, QObject *parent = nullptr);
PodcastEpisodeList EpisodesDownloading(const PodcastEpisodeList &episodes);
QString DefaultDownloadDir() const;
public slots:
// Adds the episode to the download queue
void DownloadEpisode(const PodcastEpisode &episode);
void cancelDownload(const PodcastEpisodeList &episodes);
signals:
void ProgressChanged(const PodcastEpisode &episode, const PodcastDownload::State state, const int percent);
private slots:
void ReloadSettings();
void SubscriptionAdded(const Podcast &podcast);
void EpisodesAdded(const PodcastEpisodeList &episodes);
void ReplyFinished(Task *task);
private:
QString FilenameForEpisode(const QString &directory, const PodcastEpisode &episode) const;
QString SanitiseFilenameComponent(const QString &text) const;
private:
static const char *kSettingsGroup;
Application *app_;
PodcastBackend *backend_;
NetworkAccessManager *network_;
QRegularExpression disallowed_filename_characters_;
bool auto_download_;
QString download_dir_;
QList<Task*> list_tasks_;
};
#endif // PODCASTDOWNLOADER_H

View File

@@ -1,231 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* 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 <limits>
#include <QSharedData>
#include <QIODevice>
#include <QFile>
#include <QFileInfo>
#include <QDataStream>
#include <QVariant>
#include <QVariantMap>
#include <QByteArray>
#include <QString>
#include <QStringList>
#include <QUrl>
#include <QDateTime>
#include <QSqlQuery>
#include "core/logging.h"
#include "core/timeconstants.h"
#include "core/utilities.h"
#include "podcast.h"
#include "podcastepisode.h"
const QStringList PodcastEpisode::kColumns = QStringList() << "podcast_id"
<< "title"
<< "description"
<< "author"
<< "publication_date"
<< "duration_secs"
<< "url"
<< "listened"
<< "listened_date"
<< "downloaded"
<< "local_url"
<< "extra";
const QString PodcastEpisode::kColumnSpec = PodcastEpisode::kColumns.join(", ");
const QString PodcastEpisode::kJoinSpec = Utilities::Prepend("e.", PodcastEpisode::kColumns).join(", ");
const QString PodcastEpisode::kBindSpec = Utilities::Prepend(":", PodcastEpisode::kColumns).join(", ");
const QString PodcastEpisode::kUpdateSpec = Utilities::Updateify(PodcastEpisode::kColumns).join(", ");
struct PodcastEpisode::Private : public QSharedData {
Private();
int database_id_;
int podcast_database_id_;
QString title_;
QString description_;
QString author_;
QDateTime publication_date_;
int duration_secs_;
QUrl url_;
bool listened_;
QDateTime listened_date_;
bool downloaded_;
QUrl local_url_;
QVariantMap extra_;
};
PodcastEpisode::Private::Private()
: database_id_(-1),
podcast_database_id_(-1),
duration_secs_(-1),
listened_(false),
downloaded_(false) {}
PodcastEpisode::PodcastEpisode() : d(new Private) {}
PodcastEpisode::PodcastEpisode(const PodcastEpisode &other) : d(other.d) {}
PodcastEpisode::~PodcastEpisode() {}
PodcastEpisode &PodcastEpisode::operator=(const PodcastEpisode &other) {
d = other.d;
return *this;
}
int PodcastEpisode::database_id() const { return d->database_id_; }
int PodcastEpisode::podcast_database_id() const {
return d->podcast_database_id_;
}
const QString &PodcastEpisode::title() const { return d->title_; }
const QString &PodcastEpisode::description() const { return d->description_; }
const QString &PodcastEpisode::author() const { return d->author_; }
const QDateTime &PodcastEpisode::publication_date() const { return d->publication_date_; }
int PodcastEpisode::duration_secs() const { return d->duration_secs_; }
const QUrl &PodcastEpisode::url() const { return d->url_; }
bool PodcastEpisode::listened() const { return d->listened_; }
const QDateTime &PodcastEpisode::listened_date() const { return d->listened_date_; }
bool PodcastEpisode::downloaded() const { return d->downloaded_; }
const QUrl &PodcastEpisode::local_url() const { return d->local_url_; }
const QVariantMap &PodcastEpisode::extra() const { return d->extra_; }
QVariant PodcastEpisode::extra(const QString &key) const { return d->extra_[key]; }
void PodcastEpisode::set_database_id(const int v) { d->database_id_ = v; }
void PodcastEpisode::set_podcast_database_id(const int v) { d->podcast_database_id_ = v; }
void PodcastEpisode::set_title(const QString &v) { d->title_ = v; }
void PodcastEpisode::set_description(const QString &v) { d->description_ = v; }
void PodcastEpisode::set_author(const QString &v) { d->author_ = v; }
void PodcastEpisode::set_publication_date(const QDateTime &v) { d->publication_date_ = v; }
void PodcastEpisode::set_duration_secs(int v) { d->duration_secs_ = v; }
void PodcastEpisode::set_url(const QUrl &v) { d->url_ = v; }
void PodcastEpisode::set_listened(const bool v) { d->listened_ = v; }
void PodcastEpisode::set_listened_date(const QDateTime &v) { d->listened_date_ = v; }
void PodcastEpisode::set_downloaded(const bool v) { d->downloaded_ = v; }
void PodcastEpisode::set_local_url(const QUrl &v) { d->local_url_ = v; }
void PodcastEpisode::set_extra(const QVariantMap &v) { d->extra_ = v; }
void PodcastEpisode::set_extra(const QString &key, const QVariant &value) { d->extra_[key] = value; }
void PodcastEpisode::InitFromQuery(const QSqlQuery &query) {
d->database_id_ = query.value(0).toInt();
d->podcast_database_id_ = query.value(1).toInt();
d->title_ = query.value(2).toString();
d->description_ = query.value(3).toString();
d->author_ = query.value(4).toString();
d->publication_date_ = QDateTime::fromSecsSinceEpoch(query.value(5).toUInt());
d->duration_secs_ = query.value(6).toInt();
d->url_ = QUrl::fromEncoded(query.value(7).toByteArray());
d->listened_ = query.value(8).toBool();
// After setting QDateTime to invalid state, it's saved into database as time_t,
// when this number std::numeric_limits<unsigned int>::max() (4294967295) is read back from database, it creates a valid QDateTime.
// So to make it behave consistently, this change is needed.
if (query.value(9).toUInt() == std::numeric_limits<unsigned int>::max()) {
d->listened_date_ = QDateTime();
}
else {
d->listened_date_ = QDateTime::fromSecsSinceEpoch(query.value(9).toUInt());
}
d->downloaded_ = query.value(10).toBool();
d->local_url_ = QUrl::fromEncoded(query.value(11).toByteArray());
QDataStream extra_stream(query.value(12).toByteArray());
extra_stream >> d->extra_;
}
void PodcastEpisode::BindToQuery(QSqlQuery* query) const {
query->bindValue(":podcast_id", d->podcast_database_id_);
query->bindValue(":title", d->title_);
query->bindValue(":description", d->description_);
query->bindValue(":author", d->author_);
query->bindValue(":publication_date", d->publication_date_.toSecsSinceEpoch());
query->bindValue(":duration_secs", d->duration_secs_);
query->bindValue(":url", d->url_.toEncoded());
query->bindValue(":listened", d->listened_);
query->bindValue(":listened_date", d->listened_date_.toSecsSinceEpoch());
query->bindValue(":downloaded", d->downloaded_);
query->bindValue(":local_url", d->local_url_.toEncoded());
QByteArray extra;
QDataStream extra_stream(&extra, QIODevice::WriteOnly);
extra_stream << d->extra_;
query->bindValue(":extra", extra);
}
Song PodcastEpisode::ToSong(const Podcast &podcast) const {
Song ret;
ret.set_valid(true);
ret.set_title(title().simplified());
ret.set_artist(author().simplified());
ret.set_length_nanosec(kNsecPerSec * duration_secs());
ret.set_year(publication_date().date().year());
ret.set_comment(description());
ret.set_id(database_id());
ret.set_ctime(publication_date().toSecsSinceEpoch());
ret.set_genre(QString("Podcast"));
//ret.set_genre_id3(186);
if (listened() && listened_date().isValid()) {
ret.set_mtime(listened_date().toSecsSinceEpoch());
}
else {
ret.set_mtime(publication_date().toSecsSinceEpoch());
}
if (ret.length_nanosec() < 0) {
ret.set_length_nanosec(-1);
}
if (downloaded() && QFile::exists(local_url().toLocalFile())) {
ret.set_url(local_url());
}
else {
ret.set_url(url());
}
ret.set_basefilename(QFileInfo(ret.url().path()).fileName());
// Use information from the podcast if it's set
if (podcast.is_valid()) {
ret.set_album(podcast.title().simplified());
ret.set_art_automatic(podcast.ImageUrlLarge());
if (author().isEmpty()) ret.set_artist(podcast.title().simplified());
}
return ret;
}

View File

@@ -1,100 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* 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/>.
*
*/
#ifndef PODCASTEPISODE_H
#define PODCASTEPISODE_H
#include <QSharedDataPointer>
#include <QVariant>
#include <QVariantMap>
#include <QString>
#include <QStringList>
#include <QUrl>
#include <QDateTime>
#include <QSqlQuery>
#include "core/song.h"
class Podcast;
class PodcastEpisode {
public:
PodcastEpisode();
PodcastEpisode(const PodcastEpisode &other);
~PodcastEpisode();
static const QStringList kColumns;
static const QString kColumnSpec;
static const QString kJoinSpec;
static const QString kBindSpec;
static const QString kUpdateSpec;
void InitFromQuery(const QSqlQuery &query);
void BindToQuery(QSqlQuery *query) const;
Song ToSong(const Podcast &podcast) const;
bool is_valid() const { return database_id() != -1; }
int database_id() const;
int podcast_database_id() const;
const QString &title() const;
const QString &description() const;
const QString &author() const;
const QDateTime &publication_date() const;
int duration_secs() const;
const QUrl &url() const;
bool listened() const;
const QDateTime &listened_date() const;
bool downloaded() const;
const QUrl &local_url() const;
const QVariantMap &extra() const;
QVariant extra(const QString &key) const;
void set_database_id(const int v);
void set_podcast_database_id(int v);
void set_title(const QString &v);
void set_description(const QString &v);
void set_author(const QString &v);
void set_publication_date(const QDateTime &v);
void set_duration_secs(int v);
void set_url(const QUrl &v);
void set_listened(const bool v);
void set_listened_date(const QDateTime &v);
void set_downloaded(const bool v);
void set_local_url(const QUrl &v);
void set_extra(const QVariantMap &v);
void set_extra(const QString &key, const QVariant &value);
PodcastEpisode &operator=(const PodcastEpisode &other);
private:
struct Private;
QSharedDataPointer<Private> d;
};
Q_DECLARE_METATYPE(PodcastEpisode)
typedef QList<PodcastEpisode> PodcastEpisodeList;
Q_DECLARE_METATYPE(QList<PodcastEpisode>)
#endif // PODCASTEPISODE_H

View File

@@ -1,59 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
Copyright 2018, Jim Broadus <jbroadus@gmail.com>
* 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 <QDialog>
#include "core/application.h"
#include "podcastepisode.h"
#include "podcastinfodialog.h"
#include "ui_podcastinfodialog.h"
PodcastInfoDialog::PodcastInfoDialog(Application *app, QWidget *parent)
: QDialog(parent), app_(app), ui_(new Ui_PodcastInfoDialog) {
ui_->setupUi(this);
ui_->podcast_details->SetApplication(app);
ui_->episode_details->SetApplication(app);
}
PodcastInfoDialog::~PodcastInfoDialog() { delete ui_; }
void PodcastInfoDialog::ShowPodcast(const Podcast &podcast) {
ui_->episode_info_scroll_area->hide();
ui_->podcast_url->setText(podcast.url().toString());
ui_->podcast_url->setReadOnly(true);
ui_->podcast_details->SetPodcast(podcast);
show();
}
void PodcastInfoDialog::ShowEpisode(const PodcastEpisode &episode, const Podcast &podcast) {
ui_->episode_info_scroll_area->show();
ui_->podcast_url->setText(episode.url().toString());
ui_->podcast_url->setReadOnly(true);
ui_->podcast_details->SetPodcast(podcast);
ui_->episode_details->SetEpisode(episode);
show();
}

View File

@@ -1,48 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
Copyright 2018, Jim Broadus <jbroadus@gmail.com>
* 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/>.
*
*/
#ifndef PODCASTINFODIALOG_H
#define PODCASTINFODIALOG_H
#include <QDialog>
class Application;
class Podcast;
class PodcastEpisode;
class Ui_PodcastInfoDialog;
class PodcastInfoDialog : public QDialog {
Q_OBJECT
public:
explicit PodcastInfoDialog(Application *app, QWidget *parent = nullptr);
~PodcastInfoDialog();
void ShowPodcast(const Podcast &podcast);
void ShowEpisode(const PodcastEpisode &episode, const Podcast &podcast);
private:
Application *app_;
Ui_PodcastInfoDialog *ui_;
};
#endif // PODCASTINFODIALOG_H

View File

@@ -1,142 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PodcastInfoDialog</class>
<widget class="QDialog" name="PodcastInfoDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>493</width>
<height>415</height>
</rect>
</property>
<property name="windowTitle">
<string>Podcast Information</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QLineEdit" name="podcast_url"/>
</item>
<item>
<widget class="QScrollArea" name="episode_info_scroll_area">
<property name="enabled">
<bool>true</bool>
</property>
<property name="minimumSize">
<size>
<width>250</width>
<height>100</height>
</size>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="EpisodeInfoWidget" name="episode_details">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>473</width>
<height>158</height>
</rect>
</property>
</widget>
</widget>
</item>
<item>
<widget class="QScrollArea" name="podcast_info_scroll_area">
<property name="minimumSize">
<size>
<width>250</width>
<height>100</height>
</size>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAlwaysOff</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="PodcastInfoWidget" name="podcast_details">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>473</width>
<height>157</height>
</rect>
</property>
</widget>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Close</set>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>PodcastInfoWidget</class>
<extends>QWidget</extends>
<header>podcasts/podcastinfowidget.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>EpisodeInfoWidget</class>
<extends>QWidget</extends>
<header location="global">podcasts/episodeinfowidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>PodcastInfoDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>PodcastInfoDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@@ -1,134 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* 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 <QWidget>
#include <QImage>
#include <QPixmap>
#include <QColor>
#include <QPalette>
#include <QLabel>
#include "core/application.h"
#include "covermanager/albumcoverloader.h"
#include "covermanager/albumcoverloaderoptions.h"
#include "covermanager/albumcoverloaderresult.h"
#include "podcastinfowidget.h"
#include "ui_podcastinfowidget.h"
PodcastInfoWidget::PodcastInfoWidget(QWidget *parent)
: QWidget(parent),
ui_(new Ui_PodcastInfoWidget),
app_(nullptr),
image_id_(0) {
ui_->setupUi(this);
cover_options_.desired_height_ = 180;
ui_->image->setFixedSize(cover_options_.desired_height_, cover_options_.desired_height_);
// Set the colour of all the labels
const bool light = palette().color(QPalette::Base).value() > 128;
const QColor color = palette().color(QPalette::Dark);
QPalette label_palette(palette());
label_palette.setColor(QPalette::WindowText, light ? color.darker(150) : color.lighter(125));
for (QLabel* label : findChildren<QLabel*>()) {
if (label->property("field_label").toBool()) {
label->setPalette(label_palette);
}
}
}
PodcastInfoWidget::~PodcastInfoWidget() { delete ui_; }
void PodcastInfoWidget::SetApplication(Application *app) {
app_ = app;
connect(app_->album_cover_loader(), &AlbumCoverLoader::AlbumCoverLoaded, this, &PodcastInfoWidget::AlbumCoverLoaded);
}
namespace {
template<typename T>
void SetText(const QString& value, T* label, QLabel* buddy_label = nullptr) {
const bool visible = !value.isEmpty();
label->setVisible(visible);
if (buddy_label) {
buddy_label->setVisible(visible);
}
if (visible) {
label->setText(value);
}
}
} // namespace
void PodcastInfoWidget::SetPodcast(const Podcast &podcast) {
if (image_id_) {
app_->album_cover_loader()->CancelTask(image_id_);
image_id_ = 0;
}
podcast_ = podcast;
if (podcast.ImageUrlLarge().isValid()) {
// Start loading an image for this item.
image_id_ = app_->album_cover_loader()->LoadImageAsync(cover_options_, podcast.ImageUrlLarge(), QUrl());
}
ui_->image->hide();
SetText(podcast.title(), ui_->title);
SetText(podcast.description(), ui_->description);
SetText(podcast.copyright(), ui_->copyright, ui_->copyright_label);
SetText(podcast.author(), ui_->author, ui_->author_label);
SetText(podcast.owner_name(), ui_->owner, ui_->owner_label);
SetText(podcast.link().toString(), ui_->website, ui_->website_label);
SetText(podcast.extra("gpodder:subscribers").toString(), ui_->subscribers, ui_->subscribers_label);
if (!image_id_) {
emit LoadingFinished();
}
}
void PodcastInfoWidget::AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &result) {
if (id != image_id_) {
return;
}
image_id_ = 0;
if (result.success && !result.image_scaled.isNull()) {
ui_->image->setPixmap(QPixmap::fromImage(result.image_scaled));
ui_->image->show();
}
emit LoadingFinished();
}

View File

@@ -1,65 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* 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/>.
*
*/
#ifndef PODCASTINFOWIDGET_H
#define PODCASTINFOWIDGET_H
#include <QWidget>
#include "podcast.h"
#include "covermanager/albumcoverloaderoptions.h"
#include "covermanager/albumcoverloaderresult.h"
class Application;
class Ui_PodcastInfoWidget;
class QLabel;
class PodcastInfoWidget : public QWidget {
Q_OBJECT
public:
explicit PodcastInfoWidget(QWidget *parent = nullptr);
~PodcastInfoWidget();
void SetApplication(Application *app);
void SetPodcast(const Podcast& podcast);
signals:
void LoadingFinished();
private slots:
void AlbumCoverLoaded(const quint64 id, const AlbumCoverLoaderResult &result);
private:
Ui_PodcastInfoWidget *ui_;
AlbumCoverLoaderOptions cover_options_;
Application *app_;
Podcast podcast_;
quint64 image_id_;
};
#endif // PODCASTINFOWIDGET_H

View File

@@ -1,220 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PodcastInfoWidget</class>
<widget class="QWidget" name="PodcastInfoWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>398</width>
<height>551</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<property name="styleSheet">
<string notr="true">#title {
font-weight: bold;
}
#description {
font-size: smaller;
}
QLineEdit {
background: transparent;
}</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<property name="sizeConstraint">
<enum>QLayout::SetMinAndMaxSize</enum>
</property>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>0</number>
</property>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QLabel" name="image">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="title">
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="description">
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item>
<layout class="QGridLayout" name="gridLayout">
<property name="sizeConstraint">
<enum>QLayout::SetMinAndMaxSize</enum>
</property>
<item row="4" column="1">
<widget class="QLineEdit" name="website">
<property name="frame">
<bool>false</bool>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="author_label">
<property name="text">
<string>Author</string>
</property>
<property name="field_label" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="owner">
<property name="frame">
<bool>false</bool>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="website_label">
<property name="text">
<string>Website</string>
</property>
<property name="field_label" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="copyright">
<property name="frame">
<bool>false</bool>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="copyright_label">
<property name="text">
<string>Copyright</string>
</property>
<property name="field_label" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="author">
<property name="frame">
<bool>false</bool>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="owner_label">
<property name="text">
<string>Owner</string>
</property>
<property name="field_label" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="subscribers_label">
<property name="text">
<string>Subscribers</string>
</property>
<property name="field_label" stdset="0">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLineEdit" name="subscribers">
<property name="frame">
<bool>false</bool>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@@ -1,376 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* 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 <QByteArray>
#include <QString>
#include <QUrl>
#include <QDateTime>
#include <QXmlStreamReader>
#include <QRegularExpression>
#include "core/logging.h"
#include "core/utilities.h"
#include "podcastparser.h"
#include "opmlcontainer.h"
// Namespace constants must be lower case.
const char *PodcastParser::kAtomNamespace = "http://www.w3.org/2005/atom";
const char *PodcastParser::kItunesNamespace = "http://www.itunes.com/dtds/podcast-1.0.dtd";
PodcastParser::PodcastParser() {
supported_mime_types_ << "application/rss+xml"
<< "application/xml"
<< "text/x-opml"
<< "text/xml";
}
bool PodcastParser::SupportsContentType(const QString &content_type) const {
if (content_type.isEmpty()) {
// Why not have a go.
return true;
}
for (const QString &mime_type : supported_mime_types()) {
if (content_type.contains(mime_type)) {
return true;
}
}
return false;
}
bool PodcastParser::TryMagic(const QByteArray &data) const {
QString str(QString::fromUtf8(data));
return str.contains(QRegularExpression("<rss\\b")) || str.contains(QRegularExpression("<opml\\b"));
}
QVariant PodcastParser::Load(QIODevice *device, const QUrl &url) const {
QXmlStreamReader reader(device);
while (!reader.atEnd()) {
switch (reader.readNext()) {
case QXmlStreamReader::StartElement: {
const QString name = reader.name().toString();
if (name == "rss") {
Podcast podcast;
if (!ParseRss(&reader, &podcast)) {
return QVariant();
}
else {
podcast.set_url(url);
return QVariant::fromValue(podcast);
}
}
else if (name == "opml") {
OpmlContainer container;
if (!ParseOpml(&reader, &container)) {
return QVariant();
}
else {
container.url = url;
return QVariant::fromValue(container);
}
}
return QVariant();
}
default:
break;
}
}
return QVariant();
}
bool PodcastParser::ParseRss(QXmlStreamReader *reader, Podcast *ret) const {
if (!Utilities::ParseUntilElement(reader, "channel")) {
return false;
}
ParseChannel(reader, ret);
return true;
}
void PodcastParser::ParseChannel(QXmlStreamReader *reader, Podcast *ret) const {
while (!reader->atEnd()) {
QXmlStreamReader::TokenType type = reader->readNext();
switch (type) {
case QXmlStreamReader::StartElement: {
const QString name = reader->name().toString();
const QString lower_namespace = reader->namespaceUri().toString().toLower();
if (name == "title") {
ret->set_title(reader->readElementText());
}
else if (name == "link" && lower_namespace.isEmpty()) {
ret->set_link(QUrl::fromEncoded(reader->readElementText().toLatin1()));
}
else if (name == "description") {
ret->set_description(reader->readElementText());
}
else if (name == "owner" && lower_namespace == kItunesNamespace) {
ParseItunesOwner(reader, ret);
}
else if (name == "image") {
ParseImage(reader, ret);
}
else if (name == "copyright") {
ret->set_copyright(reader->readElementText());
}
else if (name == "link" && lower_namespace == kAtomNamespace && ret->url().isEmpty() && reader->attributes().value("rel").toString() == "self") {
ret->set_url(QUrl::fromEncoded(reader->readElementText().toLatin1()));
}
else if (name == "item") {
ParseItem(reader, ret);
}
else {
Utilities::ConsumeCurrentElement(reader);
}
break;
}
case QXmlStreamReader::EndElement:
return;
default:
break;
}
}
}
void PodcastParser::ParseImage(QXmlStreamReader *reader, Podcast *ret) const {
while (!reader->atEnd()) {
QXmlStreamReader::TokenType type = reader->readNext();
switch (type) {
case QXmlStreamReader::StartElement: {
const QString name = reader->name().toString();
if (name == "url") {
ret->set_image_url_large(
QUrl::fromEncoded(reader->readElementText().toLatin1()));
}
else {
Utilities::ConsumeCurrentElement(reader);
}
break;
}
case QXmlStreamReader::EndElement:
return;
default:
break;
}
}
}
void PodcastParser::ParseItunesOwner(QXmlStreamReader *reader, Podcast *ret) const {
while (!reader->atEnd()) {
QXmlStreamReader::TokenType type = reader->readNext();
switch (type) {
case QXmlStreamReader::StartElement: {
const QString name = reader->name().toString();
if (name == "name") {
ret->set_owner_name(reader->readElementText());
}
else if (name == "email") {
ret->set_owner_email(reader->readElementText());
}
else {
Utilities::ConsumeCurrentElement(reader);
}
break;
}
case QXmlStreamReader::EndElement:
return;
default:
break;
}
}
}
void PodcastParser::ParseItem(QXmlStreamReader *reader, Podcast *ret) const {
PodcastEpisode episode;
while (!reader->atEnd()) {
QXmlStreamReader::TokenType type = reader->readNext();
switch (type) {
case QXmlStreamReader::StartElement: {
const QString name = reader->name().toString();
const QString lower_namespace = reader->namespaceUri().toString().toLower();
if (name == "title") {
episode.set_title(reader->readElementText());
}
else if (name == "description") {
episode.set_description(reader->readElementText());
}
else if (name == "pubDate") {
QString date = reader->readElementText();
episode.set_publication_date(Utilities::ParseRFC822DateTime(date));
if (!episode.publication_date().isValid()) {
qLog(Error) << "Unable to parse date:" << date;
}
}
else if (name == "duration" && lower_namespace == kItunesNamespace) {
// http://www.apple.com/itunes/podcasts/specs.html
QStringList parts = reader->readElementText().split(':');
if (parts.count() == 2) {
episode.set_duration_secs(parts[0].toInt() * 60 + parts[1].toInt());
}
else if (parts.count() >= 3) {
episode.set_duration_secs(parts[0].toInt() * 60 * 60 + parts[1].toInt() * 60 + parts[2].toInt());
}
}
else if (name == "enclosure") {
const QString type2 = reader->attributes().value("type").toString();
const QUrl url = QUrl::fromEncoded(reader->attributes().value("url").toString().toLatin1());
if (type2.startsWith("audio/") || type2.startsWith("x-audio/")) {
episode.set_url(url);
}
// If the URL doesn't have a type, see if it's one of the obvious types
else if (type2.isEmpty() && (url.path().endsWith(".mp3", Qt::CaseInsensitive) || url.path().endsWith(".m4a", Qt::CaseInsensitive) || url.path().endsWith(".wav", Qt::CaseInsensitive))) {
episode.set_url(url);
}
Utilities::ConsumeCurrentElement(reader);
}
else if (name == "author" && lower_namespace == kItunesNamespace) {
episode.set_author(reader->readElementText());
}
else {
Utilities::ConsumeCurrentElement(reader);
}
break;
}
case QXmlStreamReader::EndElement:
if (!episode.publication_date().isValid()) {
episode.set_publication_date(QDateTime::currentDateTime());
}
if (!episode.url().isEmpty()) {
ret->add_episode(episode);
}
return;
default:
break;
}
}
}
bool PodcastParser::ParseOpml(QXmlStreamReader *reader, OpmlContainer *ret) const {
if (!Utilities::ParseUntilElement(reader, "body")) {
return false;
}
ParseOutline(reader, ret);
// OPML files sometimes consist of a single top level container.
OpmlContainer *top = ret;
while (top->feeds.count() == 0 && top->containers.count() == 1) {
top = &top->containers[0];
}
if (top != ret) {
// Copy the sub-container to a temporary location first.
OpmlContainer tmp = *top;
*ret = tmp;
}
return true;
}
void PodcastParser::ParseOutline(QXmlStreamReader *reader, OpmlContainer *ret) const {
while (!reader->atEnd()) {
QXmlStreamReader::TokenType type = reader->readNext();
switch (type) {
case QXmlStreamReader::StartElement: {
const QString name = reader->name().toString();
if (name != "outline") {
Utilities::ConsumeCurrentElement(reader);
continue;
}
QXmlStreamAttributes attributes = reader->attributes();
if (attributes.value("type").toString() == "rss") {
// Parse the feed and add it to this container
Podcast podcast;
podcast.set_description(attributes.value("description").toString());
QString title = attributes.value("title").toString();
if (title.isEmpty()) {
title = attributes.value("text").toString();
}
podcast.set_title(title);
podcast.set_image_url_large(QUrl::fromEncoded(attributes.value("imageHref").toString().toLatin1()));
podcast.set_url(QUrl::fromEncoded(attributes.value("xmlUrl").toString().toLatin1()));
ret->feeds.append(podcast);
// Consume any children and the EndElement.
Utilities::ConsumeCurrentElement(reader);
}
else {
// Create a new child container
OpmlContainer child;
// Take the name from the fullname attribute first if it exists.
child.name = attributes.value("fullname").toString();
if (child.name.isEmpty()) {
child.name = attributes.value("text").toString();
}
// Parse its contents and add it to this container
ParseOutline(reader, &child);
ret->containers.append(child);
}
break;
}
case QXmlStreamReader::EndElement:
return;
default:
break;
}
}
}

View File

@@ -1,72 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* 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/>.
*
*/
#ifndef PODCASTPARSER_H
#define PODCASTPARSER_H
#include <QByteArray>
#include <QString>
#include <QStringList>
#include <QUrl>
#include "podcast.h"
class QIODevice;
class QXmlStreamReader;
class OpmlContainer;
// Reads XML data from a QIODevice.
// Returns either a Podcast or an OpmlContainer depending on what was inside the XML document.
class PodcastParser {
public:
PodcastParser();
static const char *kAtomNamespace;
static const char *kItunesNamespace;
const QStringList &supported_mime_types() const { return supported_mime_types_; }
bool SupportsContentType(const QString &content_type) const;
// You should check the type of the returned QVariant to see whether it contains a Podcast or an OpmlContainer.
// If the QVariant isNull then an error occurred parsing the XML.
QVariant Load(QIODevice *device, const QUrl &url) const;
// Really quick test to see if some data might be supported. Load() might still return a null QVariant.
bool TryMagic(const QByteArray &data) const;
private:
bool ParseRss(QXmlStreamReader *reader, Podcast *ret) const;
void ParseChannel(QXmlStreamReader *reader, Podcast *ret) const;
void ParseImage(QXmlStreamReader *reader, Podcast *ret) const;
void ParseItunesOwner(QXmlStreamReader *reader, Podcast *ret) const;
void ParseItem(QXmlStreamReader *reader, Podcast *ret) const;
bool ParseOpml(QXmlStreamReader *reader, OpmlContainer *ret) const;
void ParseOutline(QXmlStreamReader *reader, OpmlContainer *ret) const;
private:
QStringList supported_mime_types_;
};
#endif // PODCASTPARSER_H

View File

@@ -1,919 +0,0 @@
/*
* 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));
}

View File

@@ -1,178 +0,0 @@
/*
* 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/>.
*
*/
#ifndef PODCASTSERVICE_H
#define PODCASTSERVICE_H
#include <memory>
#include <QMap>
#include <QIcon>
#include <QScopedPointer>
//#include "internet/internetmodel.h"
#include "internet/internetservice.h"
#include "podcastdeleter.h"
#include "podcastdownloader.h"
class QMenu;
class QAction;
class AddPodcastDialog;
class PodcastInfoDialog;
class OrganizeDialog;
class Podcast;
class PodcastBackend;
class PodcastEpisode;
class StandardItemIconLoader;
class QStandardItemModel;
class QStandardItem;
class QSortFilterProxyModel;
class PodcastService : public InternetService {
Q_OBJECT
public:
PodcastService(Application *app, QObject *parent);
~PodcastService();
static const char *kServiceName;
static const char *kSettingsGroup;
enum Type {
Type_AddPodcast = 0,
Type_Podcast,
Type_Episode
};
enum Role {
Role_Podcast = 0,
Role_Episode
};
QStandardItem *CreateRootItem();
void LazyPopulate(QStandardItem *parent);
bool has_initial_load_settings() const { return true; }
void ShowContextMenu(const QPoint &global_pos);
void ReloadSettings();
void InitialLoadSettings();
// Called by SongLoader when the user adds a Podcast URL directly.
// Adds a subscription to the podcast and displays it in the UI.
// If the QVariant contains an OPML file then this displays it in the Add Podcast dialog.
void SubscribeAndShow(const QVariant &podcast_or_opml);
public slots:
void AddPodcast();
void FileCopied(const int database_id);
private slots:
void UpdateSelectedPodcast();
void ReloadPodcast(const Podcast &podcast);
void RemoveSelectedPodcast();
void DownloadSelectedEpisode();
void PodcastInfo();
void DeleteDownloadedData();
void SetNew();
void SetListened();
void ShowConfig();
void SubscriptionAdded(const Podcast &podcast);
void SubscriptionRemoved(const Podcast &podcast);
void EpisodesAdded(const PodcastEpisodeList &episodes);
void EpisodesUpdated(const PodcastEpisodeList &episodes);
void DownloadProgressChanged(const PodcastEpisode &episode, PodcastDownload::State state, int percent);
void CurrentSongChanged(const Song &metadata);
void CopyToDevice();
void CopyToDevice(const PodcastEpisodeList &episodes_list);
void CopyToDevice(const QModelIndexList &episode_indexes, const QModelIndexList &podcast_indexes);
void CancelDownload();
void CancelDownload(const QModelIndexList &episode_indexes, const QModelIndexList &podcast_indexes);
private:
void EnsureAddPodcastDialogCreated();
void UpdatePodcastListenedStateAsync(const Song &metadata);
void PopulatePodcastList(QStandardItem *parent);
void ClearPodcastList(QStandardItem *parent);
void UpdatePodcastText(QStandardItem *item, const int unlistened_count) const;
void UpdateEpisodeText(QStandardItem *item, const PodcastDownload::State state = PodcastDownload::NotDownloading, const int percent = 0);
void UpdatePodcastText(QStandardItem *item, const PodcastDownload::State state = PodcastDownload::NotDownloading, const int percent = 0);
QStandardItem *CreatePodcastItem(const Podcast &podcast);
QStandardItem *CreatePodcastEpisodeItem(const PodcastEpisode &episode);
QModelIndex MapToMergedModel(const QModelIndex &idx) const;
void SetListened(const QModelIndexList &episode_indexes, const QModelIndexList &podcast_indexes, const bool listened);
void SetListened(const PodcastEpisodeList &episodes_list, bool listened);
void LazyLoadRoot();
private:
bool use_pretty_covers_;
bool hide_listened_;
qint64 show_episodes_;
StandardItemIconLoader *icon_loader_;
// The podcast icon
QIcon default_icon_;
// Episodes get different icons depending on their state
QIcon queued_icon_;
QIcon downloading_icon_;
QIcon downloaded_icon_;
PodcastBackend *backend_;
QStandardItemModel *model_;
QSortFilterProxyModel *proxy_;
QMenu *context_menu_;
QAction *update_selected_action_;
QAction *remove_selected_action_;
QAction *download_selected_action_;
QAction *info_selected_action_;
QAction *delete_downloaded_action_;
QAction *set_new_action_;
QAction *set_listened_action_;
QAction *copy_to_device_;
QAction *cancel_download_;
QStandardItem *root_;
std::unique_ptr<OrganizeDialog> organize_dialog_;
QModelIndexList explicitly_selected_podcasts_;
QModelIndexList selected_podcasts_;
QModelIndexList selected_episodes_;
QMap<int, QStandardItem*> podcasts_by_database_id_;
QMap<int, QStandardItem*> episodes_by_database_id_;
std::unique_ptr<AddPodcastDialog> add_podcast_dialog_;
std::unique_ptr<PodcastInfoDialog> podcast_info_dialog_;
};
#endif // PODCASTSERVICE_H

View File

@@ -1,101 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* 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 <QList>
#include <QVariant>
#include <QUrl>
#include <QMimeData>
#include "podcastservicemodel.h"
#include "podcastservice.h"
#include "playlist/songmimedata.h"
PodcastServiceModel::PodcastServiceModel(QObject* parent) : QStandardItemModel(parent) {}
QMimeData* PodcastServiceModel::mimeData(const QModelIndexList &indexes) const {
SongMimeData *data = new SongMimeData;
QList<QUrl> urls;
#if 0
for (const QModelIndex& index : indexes) {
switch (index.data(InternetModel::Role_Type).toInt()) {
case PodcastService::Type_Episode:
MimeDataForEpisode(index, data, &urls);
break;
case PodcastService::Type_Podcast:
MimeDataForPodcast(index, data, &urls);
break;
}
}
#endif
data->setUrls(urls);
return data;
}
void PodcastServiceModel::MimeDataForEpisode(const QModelIndex &idx, SongMimeData *data, QList<QUrl>* urls) const {
QVariant episode_variant = idx.data(PodcastService::Role_Episode);
if (!episode_variant.isValid()) return;
PodcastEpisode episode(episode_variant.value<PodcastEpisode>());
// Get the podcast from the index's parent
Podcast podcast;
QVariant podcast_variant = idx.parent().data(PodcastService::Role_Podcast);
if (podcast_variant.isValid()) {
podcast = podcast_variant.value<Podcast>();
}
Song song = episode.ToSong(podcast);
data->songs << song;
*urls << song.url();
}
void PodcastServiceModel::MimeDataForPodcast(const QModelIndex &idx, SongMimeData *data, QList<QUrl> *urls) const {
// Get the podcast
Podcast podcast;
QVariant podcast_variant = idx.data(PodcastService::Role_Podcast);
if (podcast_variant.isValid()) {
podcast = podcast_variant.value<Podcast>();
}
// Add each child episode
const int children = idx.model()->rowCount(idx);
for (int i = 0; i < children; ++i) {
QVariant episode_variant = idx.model()->index(i, 0, idx).data(PodcastService::Role_Episode);
if (!episode_variant.isValid()) continue;
PodcastEpisode episode(episode_variant.value<PodcastEpisode>());
Song song = episode.ToSong(podcast);
data->songs << song;
*urls << song.url();
}
}

View File

@@ -1,46 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* 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/>.
*
*/
#ifndef PODCASTSERVICEMODEL_H
#define PODCASTSERVICEMODEL_H
#include <QStandardItemModel>
#include <QList>
#include <QUrl>
class SongMimeData;
class PodcastServiceModel : public QStandardItemModel {
Q_OBJECT
public:
explicit PodcastServiceModel(QObject *parent = nullptr);
QMimeData* mimeData(const QModelIndexList &indexes) const;
private:
void MimeDataForPodcast(const QModelIndex &idx, SongMimeData* data, QList<QUrl> *urls) const;
void MimeDataForEpisode(const QModelIndex &idx, SongMimeData* data, QList<QUrl> *urls) const;
};
#endif // PODCASTSERVICEMODEL_H

View File

@@ -1,194 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* 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 "podcastupdater.h"
#include <QObject>
#include <QSet>
#include <QList>
#include <QUrl>
#include <QDateTime>
#include <QTimer>
#include <QSettings>
#include "core/application.h"
#include "core/logging.h"
#include "core/timeconstants.h"
#include "podcastbackend.h"
#include "podcasturlloader.h"
const char *PodcastUpdater::kSettingsGroup = "Podcasts";
PodcastUpdater::PodcastUpdater(Application *app, QObject *parent)
: QObject(parent),
app_(app),
update_interval_secs_(0),
update_timer_(new QTimer(this)),
loader_(new PodcastUrlLoader(this)),
pending_replies_(0) {
update_timer_->setSingleShot(true);
QObject::connect(app_, &Application::SettingsChanged, this, &PodcastUpdater::ReloadSettings);
QObject::connect(update_timer_, &QTimer::timeout, this, &PodcastUpdater::UpdateAllPodcastsNow);
QObject::connect(app_->podcast_backend(), &PodcastBackend::SubscriptionAdded, this, &PodcastUpdater::SubscriptionAdded);
ReloadSettings();
}
void PodcastUpdater::ReloadSettings() {
QSettings s;
s.beginGroup(kSettingsGroup);
last_full_update_ = s.value("last_full_update").toDateTime();
update_interval_secs_ = s.value("update_interval_secs").toInt();
s.endGroup();
RestartTimer();
}
void PodcastUpdater::SaveSettings() {
QSettings s;
s.beginGroup(kSettingsGroup);
s.setValue("last_full_update", last_full_update_);
s.endGroup();
}
void PodcastUpdater::RestartTimer() {
// Stop any existing timer
update_timer_->stop();
if (pending_replies_ > 0) {
// We're still waiting for replies from the last update - don't do anything.
return;
}
if (update_interval_secs_ > 0) {
if (!last_full_update_.isValid()) {
// Updates are enabled and we've never updated before. Do it now.
qLog(Info) << "Updating podcasts for the first time";
UpdateAllPodcastsNow();
}
else {
const QDateTime next_update = last_full_update_.addSecs(update_interval_secs_);
const int secs_until_next_update = QDateTime::currentDateTime().secsTo(next_update);
if (secs_until_next_update < 0) {
qLog(Info) << "Updating podcasts" << (-secs_until_next_update) << "seconds late";
UpdateAllPodcastsNow();
}
else {
qLog(Info) << "Updating podcasts at" << next_update << "(in" << secs_until_next_update << "seconds)";
update_timer_->start(secs_until_next_update * kMsecPerSec);
}
}
}
}
void PodcastUpdater::SubscriptionAdded(const Podcast& podcast) {
// Only update a new podcast immediately if it doesn't have an episode list.
// We assume that the episode list has already been fetched recently otherwise.
if (podcast.episodes().isEmpty()) {
UpdatePodcastNow(podcast);
}
}
void PodcastUpdater::UpdatePodcastNow(const Podcast& podcast) {
PodcastUrlLoaderReply *reply = loader_->Load(podcast.url());
QObject::connect(reply, &PodcastUrlLoaderReply::Finished, this, [this, reply, podcast]() { PodcastLoaded(reply, podcast, false); });
}
void PodcastUpdater::UpdateAllPodcastsNow() {
PodcastList podcasts = app_->podcast_backend()->GetAllSubscriptions();
for (const Podcast &podcast : podcasts) {
PodcastUrlLoaderReply *reply = loader_->Load(podcast.url());
QObject::connect(reply, &PodcastUrlLoaderReply::Finished, this, [this, reply, podcast]() { PodcastLoaded(reply, podcast, true); });
++pending_replies_;
}
}
void PodcastUpdater::PodcastLoaded(PodcastUrlLoaderReply *reply, const Podcast& podcast, bool one_of_many) {
reply->deleteLater();
if (one_of_many) {
--pending_replies_;
if (pending_replies_ == 0) {
// This was the last reply we were waiting for. Save this time as being
// the last successful update and restart the timer.
last_full_update_ = QDateTime::currentDateTime();
SaveSettings();
RestartTimer();
}
}
if (!reply->is_success()) {
qLog(Warning) << "Error fetching podcast at" << podcast.url() << ":" << reply->error_text();
return;
}
if (reply->result_type() != PodcastUrlLoaderReply::Type_Podcast) {
qLog(Warning) << "The URL" << podcast.url() << "no longer contains a podcast";
return;
}
// Get the episode URLs we had for this podcast already.
QSet<QUrl> existing_urls;
for (const PodcastEpisode &episode :
app_->podcast_backend()->GetEpisodes(podcast.database_id())) {
existing_urls.insert(episode.url());
}
// Add any new episodes
PodcastEpisodeList new_episodes;
PodcastList reply_podcasts = reply->podcast_results();
for (const Podcast &reply_podcast : reply_podcasts) {
PodcastEpisodeList episodes = reply_podcast.episodes();
for (const PodcastEpisode &episode : episodes) {
if (!existing_urls.contains(episode.url())) {
PodcastEpisode episode_copy(episode);
episode_copy.set_podcast_database_id(podcast.database_id());
new_episodes.append(episode_copy);
}
}
}
app_->podcast_backend()->AddEpisodes(&new_episodes);
qLog(Info) << "Added" << new_episodes.count() << "new episodes for" << podcast.url();
}

View File

@@ -1,71 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* 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/>.
*
*/
#ifndef PODCASTUPDATER_H
#define PODCASTUPDATER_H
#include <QObject>
#include <QDateTime>
class Application;
class Podcast;
class PodcastUrlLoader;
class PodcastUrlLoaderReply;
class QTimer;
// Responsible for updating podcasts when they're first subscribed to, and then updating them at regular intervals afterwards.
class PodcastUpdater : public QObject {
Q_OBJECT
public:
explicit PodcastUpdater(Application *app, QObject *parent = nullptr);
public slots:
void UpdateAllPodcastsNow();
void UpdatePodcastNow(const Podcast &podcast);
private slots:
void ReloadSettings();
void SubscriptionAdded(const Podcast &podcast);
void PodcastLoaded(PodcastUrlLoaderReply *reply, const Podcast &podcast, const bool one_of_many);
private:
void RestartTimer();
void SaveSettings();
private:
static const char *kSettingsGroup;
Application *app_;
QDateTime last_full_update_;
int update_interval_secs_;
QTimer *update_timer_;
PodcastUrlLoader *loader_;
int pending_replies_;
};
#endif // PODCASTUPDATER_H

View File

@@ -1,250 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2012, 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* 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 <QObject>
#include <QUrl>
#include <QUrlQuery>
#include <QNetworkRequest>
#include <QNetworkReply>
#include "core/logging.h"
#include "core/networkaccessmanager.h"
#include "core/utilities.h"
#include "podcasturlloader.h"
#include "podcastparser.h"
const int PodcastUrlLoader::kMaxRedirects = 5;
PodcastUrlLoader::PodcastUrlLoader(QObject* parent)
: QObject(parent),
network_(new NetworkAccessManager(this)),
parser_(new PodcastParser),
html_link_re_("<link (.*)>"),
html_link_rel_re_("rel\\s*=\\s*['\"]?\\s*alternate"),
html_link_type_re_("type\\s*=\\s*['\"]?([^'\" ]+)"),
html_link_href_re_("href\\s*=\\s*['\"]?([^'\" ]+)") {
//html_link_re_.setMinimal(true);
//html_link_re_.setCaseSensitivity(Qt::CaseInsensitive);
}
PodcastUrlLoader::~PodcastUrlLoader() { delete parser_; }
QUrl PodcastUrlLoader::FixPodcastUrl(const QString& url_text) {
QString url_text_copy(url_text.trimmed());
// Thanks gpodder!
QuickPrefixList quick_prefixes = QuickPrefixList()
<< QuickPrefix("fb:", "http://feeds.feedburner.com/%1")
<< QuickPrefix("yt:", "https://www.youtube.com/rss/user/%1/videos.rss")
<< QuickPrefix("sc:", "https://soundcloud.com/%1")
<< QuickPrefix("fm4od:", "http://onapp1.orf.at/webcam/fm4/fod/%1.xspf")
<< QuickPrefix("ytpl:", "https://gdata.youtube.com/feeds/api/playlists/%1");
// Check if it matches one of the quick prefixes.
for (QuickPrefixList::const_iterator it = quick_prefixes.constBegin(); it != quick_prefixes.constEnd(); ++it) {
if (url_text_copy.startsWith(it->first)) {
url_text_copy = it->second.arg(url_text_copy.mid(it->first.length()));
}
}
if (!url_text_copy.contains("://")) {
url_text_copy.prepend("http://");
}
return FixPodcastUrl(QUrl(url_text_copy));
}
QUrl PodcastUrlLoader::FixPodcastUrl(const QUrl& url_orig) {
QUrl url(url_orig);
QUrlQuery url_query(url);
// Replace schemes
if (url.scheme().isEmpty() || url.scheme() == "feed" || url.scheme() == "itpc" || url.scheme() == "itms") {
url.setScheme("http");
}
else if (url.scheme() == "zune" && url.host() == "subscribe" &&
!url_query.queryItems().isEmpty()) {
url = QUrl(url_query.queryItems()[0].second);
}
return url;
}
PodcastUrlLoaderReply* PodcastUrlLoader::Load(const QString& url_text) {
return Load(FixPodcastUrl(url_text));
}
PodcastUrlLoaderReply* PodcastUrlLoader::Load(const QUrl& url) {
// Create a reply
PodcastUrlLoaderReply* reply = new PodcastUrlLoaderReply(url, this);
// Create a state object to track this request
RequestState* state = new RequestState;
state->redirects_remaining_ = kMaxRedirects + 1;
state->reply_ = reply;
// Start the first request
NextRequest(url, state);
return reply;
}
void PodcastUrlLoader::SendErrorAndDelete(const QString& error_text, RequestState* state) {
state->reply_->SetFinished(error_text);
delete state;
}
void PodcastUrlLoader::NextRequest(const QUrl& url, RequestState* state) {
// Stop the request if there have been too many redirects already.
if (state->redirects_remaining_-- == 0) {
SendErrorAndDelete(tr("Too many redirects"), state);
return;
}
qLog(Debug) << "Loading URL" << url;
QNetworkRequest req(url);
req.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork);
QNetworkReply* network_reply = network_->get(req);
QObject::connect(network_reply, &QNetworkReply::finished, this, [this, state, network_reply]() { RequestFinished(state, network_reply); });
}
void PodcastUrlLoader::RequestFinished(RequestState* state, QNetworkReply* reply) {
reply->deleteLater();
if (reply->attribute(QNetworkRequest::RedirectionTargetAttribute).isValid()) {
const QUrl next_url = reply->url().resolved(reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl());
NextRequest(next_url, state);
return;
}
// Check for errors.
if (reply->error() != QNetworkReply::NoError) {
SendErrorAndDelete(reply->errorString(), state);
return;
}
const QVariant http_status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
if (http_status.isValid() && http_status.toInt() != 200) {
SendErrorAndDelete(
QString("HTTP %1: %2").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute) .toString(), reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString()),
state);
return;
}
// Check the mime type.
const QString content_type = reply->header(QNetworkRequest::ContentTypeHeader).toString();
if (parser_->SupportsContentType(content_type)) {
const QVariant ret = parser_->Load(reply, reply->url());
if (ret.canConvert<Podcast>()) {
state->reply_->SetFinished(PodcastList() << ret.value<Podcast>());
}
else if (ret.canConvert<OpmlContainer>()) {
state->reply_->SetFinished(ret.value<OpmlContainer>());
}
else {
SendErrorAndDelete(tr("Failed to parse the XML for this RSS feed"),
state);
return;
}
delete state;
return;
}
else if (content_type.contains("text/html")) {
// I don't want a full HTML parser here, so do this the dirty way.
const QString page_text = QString::fromUtf8(reply->readAll());
//int pos = 0;
#if 0
while ((pos = html_link_re_.indexIn(page_text, pos)) != -1) {
const QString link = html_link_re_.cap(1).toLower();
pos += html_link_re_.matchedLength();
if (html_link_rel_re_.indexIn(link) == -1 ||
html_link_type_re_.indexIn(link) == -1 ||
html_link_href_re_.indexIn(link) == -1) {
continue;
}
const QString link_type = html_link_type_re_.cap(1);
const QString href = Utilities::DecodeHtmlEntities(html_link_href_re_.cap(1));
if (parser_->supported_mime_types().contains(link_type)) {
NextRequest(QUrl(href), state);
return;
}
}
#endif
SendErrorAndDelete(tr("HTML page did not contain any RSS feeds"), state);
}
else {
SendErrorAndDelete(tr("Unknown content-type") + ": " + content_type, state);
}
}
PodcastUrlLoaderReply::PodcastUrlLoaderReply(const QUrl& url, QObject* parent)
: QObject(parent), url_(url), finished_(false) {}
void PodcastUrlLoaderReply::SetFinished(const PodcastList& results) {
result_type_ = Type_Podcast;
podcast_results_ = results;
finished_ = true;
emit Finished(true);
}
void PodcastUrlLoaderReply::SetFinished(const OpmlContainer& results) {
result_type_ = Type_Opml;
opml_results_ = results;
finished_ = true;
emit Finished(true);
}
void PodcastUrlLoaderReply::SetFinished(const QString& error_text) {
error_text_ = error_text;
finished_ = true;
emit Finished(false);
}

View File

@@ -1,119 +0,0 @@
/*
* Strawberry Music Player
* This file was part of Clementine.
* Copyright 2012, David Sansome <me@davidsansome.com>
* Copyright 2012, 2014, John Maguire <john.maguire@gmail.com>
* Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
* 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/>.
*
*/
#ifndef PODCASTURLLOADER_H
#define PODCASTURLLOADER_H
#include <QObject>
#include <QRegularExpression>
#include "opmlcontainer.h"
#include "podcast.h"
class PodcastParser;
class QNetworkAccessManager;
class QNetworkReply;
class PodcastUrlLoaderReply : public QObject {
Q_OBJECT
public:
PodcastUrlLoaderReply(const QUrl& url, QObject* parent);
enum ResultType { Type_Podcast,
Type_Opml };
const QUrl& url() const { return url_; }
bool is_finished() const { return finished_; }
bool is_success() const { return error_text_.isEmpty(); }
const QString& error_text() const { return error_text_; }
ResultType result_type() const { return result_type_; }
const PodcastList& podcast_results() const { return podcast_results_; }
const OpmlContainer& opml_results() const { return opml_results_; }
void SetFinished(const QString& error_text);
void SetFinished(const PodcastList& results);
void SetFinished(const OpmlContainer& results);
signals:
void Finished(bool success);
private:
QUrl url_;
bool finished_;
QString error_text_;
ResultType result_type_;
PodcastList podcast_results_;
OpmlContainer opml_results_;
};
class PodcastUrlLoader : public QObject {
Q_OBJECT
public:
explicit PodcastUrlLoader(QObject* parent = nullptr);
~PodcastUrlLoader();
static const int kMaxRedirects;
PodcastUrlLoaderReply* Load(const QString& url_text);
PodcastUrlLoaderReply* Load(const QUrl& url);
// Both the FixPodcastUrl functions replace common podcatcher URL schemes
// like itpc:// or zune:// with their http:// equivalents. The QString
// overload also cleans up user-entered text a bit - stripping whitespace and
// applying shortcuts like sc:tag.
static QUrl FixPodcastUrl(const QString& url_text);
static QUrl FixPodcastUrl(const QUrl& url);
private:
struct RequestState {
int redirects_remaining_;
PodcastUrlLoaderReply* reply_;
};
typedef QPair<QString, QString> QuickPrefix;
typedef QList<QuickPrefix> QuickPrefixList;
private slots:
void RequestFinished(RequestState* state, QNetworkReply* reply);
private:
void SendErrorAndDelete(const QString& error_text, RequestState* state);
void NextRequest(const QUrl& url, RequestState* state);
private:
QNetworkAccessManager* network_;
PodcastParser* parser_;
QRegularExpression html_link_re_;
QRegularExpression whitespace_re_;
QRegularExpression html_link_rel_re_;
QRegularExpression html_link_type_re_;
QRegularExpression html_link_href_re_;
};
#endif // PODCASTURLLOADER_H

View File

@@ -42,15 +42,22 @@
#include "core/logging.h"
#include "core/networkaccessmanager.h"
#include "core/settings.h"
#include "constants/scrobblersettings.h"
#include "lastfmimport.h"
#include "lastfmscrobbler.h"
using namespace Qt::Literals::StringLiterals;
using namespace ScrobblerSettings;
namespace {
constexpr int kRequestsDelay = 2000;
constexpr int kMaxRetries = 5;
constexpr int kInitialBackoffMs = 5000;
constexpr int kMaxBackoffShift = 10; // Maximum shift value to prevent overflow
constexpr int kRetryHttpStatusCode1 = 500; // Internal Server Error
constexpr int kRetryHttpStatusCode2 = 503; // Service Unavailable
}
LastFMImport::LastFMImport(const SharedPtr<NetworkAccessManager> network, QObject *parent)
@@ -101,14 +108,17 @@ void LastFMImport::ReloadSettings() {
Settings s;
s.beginGroup(LastFMScrobbler::kSettingsGroup);
username_ = s.value("username").toString();
api_key_ = s.value(ScrobblerSettings::kApiKey).toString();
s.endGroup();
}
QNetworkReply *LastFMImport::CreateRequest(const ParamList &request_params) {
const QString api_key = !api_key_.isEmpty() ? api_key_ : QLatin1String(LastFMScrobbler::kApiKey);
ParamList params = ParamList()
<< Param(u"api_key"_s, QLatin1String(LastFMScrobbler::kApiKey))
<< Param(u"api_key"_s, api_key)
<< Param(u"user"_s, username_)
<< Param(u"lang"_s, QLocale().name().left(2).toLower())
<< Param(u"format"_s, u"json"_s)
@@ -234,11 +244,11 @@ void LastFMImport::SendGetRecentTracksRequest(GetRecentTracksRequest request) {
}
QNetworkReply *reply = CreateRequest(params);
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, request]() { GetRecentTracksRequestFinished(reply, request.page); });
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, request]() { GetRecentTracksRequestFinished(reply, request); });
}
void LastFMImport::GetRecentTracksRequestFinished(QNetworkReply *reply, const int page) {
void LastFMImport::GetRecentTracksRequestFinished(QNetworkReply *reply, GetRecentTracksRequest request) {
if (!replies_.contains(reply)) return;
replies_.removeAll(reply);
@@ -247,10 +257,21 @@ void LastFMImport::GetRecentTracksRequestFinished(QNetworkReply *reply, const in
const JsonObjectResult json_object_result = ParseJsonObject(reply);
if (!json_object_result.success()) {
if (ShouldRetryRequest(json_object_result) && request.retry_count < kMaxRetries) {
const int delay_ms = CalculateBackoffDelay(request.retry_count);
LogRetryAttempt(json_object_result.http_status_code, request.retry_count, delay_ms);
QTimer::singleShot(delay_ms, this, [this, request]() {
GetRecentTracksRequest retry_request(request.page, request.retry_count + 1);
SendGetRecentTracksRequest(retry_request);
});
return;
}
Error(json_object_result.error_message);
return;
}
const int page = request.page;
QJsonObject json_object = json_object_result.json_object;
if (json_object.isEmpty()) {
return;
@@ -390,11 +411,11 @@ void LastFMImport::SendGetTopTracksRequest(GetTopTracksRequest request) {
}
QNetworkReply *reply = CreateRequest(params);
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, request]() { GetTopTracksRequestFinished(reply, request.page); });
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, request]() { GetTopTracksRequestFinished(reply, request); });
}
void LastFMImport::GetTopTracksRequestFinished(QNetworkReply *reply, const int page) {
void LastFMImport::GetTopTracksRequestFinished(QNetworkReply *reply, GetTopTracksRequest request) {
if (!replies_.contains(reply)) return;
replies_.removeAll(reply);
@@ -403,10 +424,21 @@ void LastFMImport::GetTopTracksRequestFinished(QNetworkReply *reply, const int p
const JsonObjectResult json_object_result = ParseJsonObject(reply);
if (!json_object_result.success()) {
if (ShouldRetryRequest(json_object_result) && request.retry_count < kMaxRetries) {
const int delay_ms = CalculateBackoffDelay(request.retry_count);
LogRetryAttempt(json_object_result.http_status_code, request.retry_count, delay_ms);
QTimer::singleShot(delay_ms, this, [this, request]() {
GetTopTracksRequest retry_request(request.page, request.retry_count + 1);
SendGetTopTracksRequest(retry_request);
});
return;
}
Error(json_object_result.error_message);
return;
}
const int page = request.page;
QJsonObject json_object = json_object_result.json_object;
if (json_object.isEmpty()) {
return;
@@ -516,6 +548,23 @@ void LastFMImport::GetTopTracksRequestFinished(QNetworkReply *reply, const int p
}
bool LastFMImport::ShouldRetryRequest(const JsonObjectResult &result) const {
return result.http_status_code == kRetryHttpStatusCode1 ||
result.http_status_code == kRetryHttpStatusCode2 ||
result.network_error == QNetworkReply::TemporaryNetworkFailureError;
}
void LastFMImport::LogRetryAttempt(const int http_status_code, const int retry_count, const int delay_ms) const {
qLog(Warning) << "Last.fm request failed with status" << http_status_code
<< ", retrying in" << delay_ms << "ms (attempt"
<< (retry_count + 1) << "of" << kMaxRetries << ")";
}
int LastFMImport::CalculateBackoffDelay(const int retry_count) const {
const int safe_shift = std::min(retry_count, kMaxBackoffShift);
return kInitialBackoffMs * (1 << safe_shift);
}
void LastFMImport::UpdateTotalCheck() {
Q_EMIT UpdateTotal(lastplayed_total_, playcount_total_);

View File

@@ -60,12 +60,14 @@ class LastFMImport : public JsonBaseRequest {
using ParamList = QList<Param>;
struct GetRecentTracksRequest {
explicit GetRecentTracksRequest(const int _page) : page(_page) {}
explicit GetRecentTracksRequest(const int _page, const int _retry_count = 0) : page(_page), retry_count(_retry_count) {}
int page;
int retry_count;
};
struct GetTopTracksRequest {
explicit GetTopTracksRequest(const int _page) : page(_page) {}
explicit GetTopTracksRequest(const int _page, const int _retry_count = 0) : page(_page), retry_count(_retry_count) {}
int page;
int retry_count;
};
private:
@@ -78,6 +80,10 @@ class LastFMImport : public JsonBaseRequest {
void SendGetRecentTracksRequest(GetRecentTracksRequest request);
void SendGetTopTracksRequest(GetTopTracksRequest request);
bool ShouldRetryRequest(const JsonObjectResult &result) const;
int CalculateBackoffDelay(const int retry_count) const;
void LogRetryAttempt(const int http_status_code, const int retry_count, const int delay_ms) const;
void Error(const QString &error, const QVariant &debug = QVariant()) override;
void UpdateTotalCheck();
@@ -95,14 +101,15 @@ class LastFMImport : public JsonBaseRequest {
private Q_SLOTS:
void FlushRequests();
void GetRecentTracksRequestFinished(QNetworkReply *reply, const int page);
void GetTopTracksRequestFinished(QNetworkReply *reply, const int page);
void GetRecentTracksRequestFinished(QNetworkReply *reply, GetRecentTracksRequest request);
void GetTopTracksRequestFinished(QNetworkReply *reply, GetTopTracksRequest request);
private:
SharedPtr<NetworkAccessManager> network_;
QTimer *timer_flush_requests_;
QString username_;
QString api_key_;
bool lastplayed_;
bool playcount_;
int playcount_total_;

View File

@@ -113,6 +113,7 @@ void LastFMScrobbler::ReloadSettings() {
s.beginGroup(kSettingsGroup);
enabled_ = s.value(ScrobblerSettings::kEnabled, false).toBool();
api_key_ = s.value(ScrobblerSettings::kApiKey).toString();
s.endGroup();
s.beginGroup(ScrobblerSettings::kSettingsGroup);

View File

@@ -64,6 +64,7 @@ class LastFMScrobbler : public ScrobblerService {
bool subscriber() const { return subscriber_; }
bool submitted() const override { return submitted_; }
QString username() const { return username_; }
QString api_key() const { return api_key_; }
void Authenticate();
void UpdateNowPlaying(const Song &song) override;
@@ -139,6 +140,7 @@ class LastFMScrobbler : public ScrobblerService {
bool subscriber_;
QString username_;
QString session_key_;
QString api_key_;
bool submitted_;
Song song_playing_;

View File

@@ -1,146 +0,0 @@
/* This file is part of Clementine.
Copyright 2012, David Sansome <me@davidsansome.com>
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
Copyright 2014, John Maguire <john.maguire@gmail.com>
Clementine 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.
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#include "podcastsettingspage.h"
#include <QFileDialog>
#include <QSettings>
#include "core/application.h"
#include "core/timeconstants.h"
#include "core/iconloader.h"
#include "collection/collectiondirectorymodel.h"
#include "collection/collectionmodel.h"
#include "podcasts/gpoddersync.h"
#include "podcasts/podcastdownloader.h"
#include "settingsdialog.h"
#include "ui_podcastsettingspage.h"
const char* PodcastSettingsPage::kSettingsGroup = "Podcasts";
PodcastSettingsPage::PodcastSettingsPage(SettingsDialog* dialog)
: SettingsPage(dialog), ui_(new Ui_PodcastSettingsPage) {
ui_->setupUi(this);
setWindowIcon(IconLoader::Load("podcast"));
connect(ui_->login, SIGNAL(clicked()), SLOT(LoginClicked()));
connect(ui_->login_state, SIGNAL(LoginClicked()), SLOT(LoginClicked()));
connect(ui_->login_state, SIGNAL(LogoutClicked()), SLOT(LogoutClicked()));
connect(ui_->download_dir_browse, SIGNAL(clicked()),
SLOT(DownloadDirBrowse()));
GPodderSync* gsync = dialog->app()->gpodder_sync();
connect(gsync, SIGNAL(LoginSuccess()), SLOT(GpodderLoginSuccess()));
connect(gsync, SIGNAL(LoginFailure(const QString&)), SLOT(GpodderLoginFailure(const QString&)));
ui_->login_state->AddCredentialField(ui_->username);
ui_->login_state->AddCredentialField(ui_->device_name);
ui_->login_state->AddCredentialField(ui_->password);
ui_->login_state->AddCredentialGroup(ui_->login_group);
ui_->check_interval->setItemData(0, 0); // manually
ui_->check_interval->setItemData(1, 10 * 60); // 10 minutes
ui_->check_interval->setItemData(2, 20 * 60); // 20 minutes
ui_->check_interval->setItemData(3, 30 * 60); // 30 minutes
ui_->check_interval->setItemData(4, 60 * 60); // 1 hour
ui_->check_interval->setItemData(5, 2 * 60 * 60); // 2 hours
ui_->check_interval->setItemData(6, 6 * 60 * 60); // 6 hours
ui_->check_interval->setItemData(7, 12 * 60 * 60); // 12 hours
}
PodcastSettingsPage::~PodcastSettingsPage() { delete ui_; }
void PodcastSettingsPage::Load() {
QSettings s;
s.beginGroup(kSettingsGroup);
const int update_interval = s.value("update_interval_secs", 0).toInt();
ui_->check_interval->setCurrentIndex(
ui_->check_interval->findData(update_interval));
const QString default_download_dir =
dialog()->app()->podcast_downloader()->DefaultDownloadDir();
ui_->download_dir->setText(QDir::toNativeSeparators(
s.value("download_dir", default_download_dir).toString()));
ui_->auto_download->setChecked(s.value("auto_download", false).toBool());
ui_->hide_listened->setChecked(s.value("hide_listened", false).toBool());
ui_->delete_after->setValue(s.value("delete_after", 0).toInt() / kSecsPerDay);
ui_->show_episodes->setValue(s.value("show_episodes", 0).toInt());
ui_->username->setText(s.value("gpodder_username").toString());
ui_->device_name->setText(
s.value("gpodder_device_name", GPodderSync::DefaultDeviceName())
.toString());
if (dialog()->app()->gpodder_sync()->is_logged_in()) {
ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedIn,
ui_->username->text());
}
else {
ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedOut);
}
}
void PodcastSettingsPage::Save() {
QSettings s;
s.beginGroup(kSettingsGroup);
s.setValue("update_interval_secs", ui_->check_interval->itemData(ui_->check_interval->currentIndex()));
s.setValue("download_dir",
QDir::fromNativeSeparators(ui_->download_dir->text()));
s.setValue("auto_download", ui_->auto_download->isChecked());
s.setValue("hide_listened", ui_->hide_listened->isChecked());
s.setValue("delete_after", ui_->delete_after->value() * kSecsPerDay);
s.setValue("show_episodes", ui_->show_episodes->value());
s.setValue("gpodder_device_name", ui_->device_name->text());
}
void PodcastSettingsPage::LoginClicked() {
ui_->login_state->SetLoggedIn(LoginStateWidget::LoginInProgress);
dialog()->app()->gpodder_sync()->Login(
ui_->username->text(), ui_->password->text(), ui_->device_name->text());
}
void PodcastSettingsPage::GpodderLoginSuccess() {
ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedIn,
ui_->username->text());
ui_->login_state->SetAccountTypeVisible(false);
}
void PodcastSettingsPage::GpodderLoginFailure(const QString& error) {
ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedOut,
ui_->username->text());
ui_->login_state->SetAccountTypeVisible(true);
ui_->login_state->SetAccountTypeText(tr("Login failed") + ": " + error);
}
void PodcastSettingsPage::LogoutClicked() {
ui_->login_state->SetLoggedIn(LoginStateWidget::LoggedOut);
ui_->password->clear();
dialog()->app()->gpodder_sync()->Logout();
}
void PodcastSettingsPage::DownloadDirBrowse() {
QString directory = QFileDialog::getExistingDirectory(
this, tr("Choose podcast download directory"), ui_->download_dir->text());
if (directory.isEmpty()) return;
ui_->download_dir->setText(QDir::toNativeSeparators(directory));
}

View File

@@ -1,52 +0,0 @@
/* This file is part of Clementine.
Copyright 2012, David Sansome <me@davidsansome.com>
Copyright 2014, John Maguire <john.maguire@gmail.com>
Copyright 2014, Krzysztof Sobiecki <sobkas@gmail.com>
Clementine 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.
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef PODCASTSETTINGSPAGE_H
#define PODCASTSETTINGSPAGE_H
#include "settingspage.h"
class Ui_PodcastSettingsPage;
class PodcastSettingsPage : public SettingsPage {
Q_OBJECT
public:
explicit PodcastSettingsPage(SettingsDialog* dialog);
~PodcastSettingsPage();
static const char* kSettingsGroup;
void Load();
void Save();
private slots:
void LoginClicked();
void LogoutClicked();
void GpodderLoginSuccess();
void GpodderLoginFailure(const QString& error);
void DownloadDirBrowse();
private:
Ui_PodcastSettingsPage* ui_;
};
#endif // PODCASTSETTINGSPAGE_H

View File

@@ -1,290 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>PodcastSettingsPage</class>
<widget class="QWidget" name="PodcastSettingsPage">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>616</width>
<height>656</height>
</rect>
</property>
<property name="windowTitle">
<string>Podcasts</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Updating</string>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Check for new episodes</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="check_interval">
<item>
<property name="text">
<string>Manually</string>
</property>
</item>
<item>
<property name="text">
<string>Every 10 minutes</string>
</property>
</item>
<item>
<property name="text">
<string>Every 20 minutes</string>
</property>
</item>
<item>
<property name="text">
<string>Every 30 minutes</string>
</property>
</item>
<item>
<property name="text">
<string>Every hour</string>
</property>
</item>
<item>
<property name="text">
<string>Every 2 hours</string>
</property>
</item>
<item>
<property name="text">
<string>Every 6 hours</string>
</property>
</item>
<item>
<property name="text">
<string>Every 12 hours</string>
</property>
</item>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Download episodes to</string>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<widget class="QCheckBox" name="auto_download">
<property name="text">
<string>Download new episodes automatically</string>
</property>
</widget>
</item>
<item row="1" column="1">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLineEdit" name="download_dir"/>
</item>
<item>
<widget class="QPushButton" name="download_dir_browse">
<property name="text">
<string>Browse...</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>Cleaning up</string>
</property>
<layout class="QFormLayout" name="formLayout_2">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Delete played episodes</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QSpinBox" name="delete_after">
<property name="specialValueText">
<string>Manually</string>
</property>
<property name="suffix">
<string> days</string>
</property>
<property name="prefix">
<string>After </string>
</property>
<property name="maximum">
<number>30</number>
</property>
<property name="empty_text" stdset="0">
<string/>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_4">
<property name="title">
<string>Appearance</string>
</property>
<layout class="QFormLayout" name="formLayout_4">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="0" column="0">
<widget class="QCheckBox" name="hide_listened">
<property name="text">
<string>Don't show listened episodes</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="show_episodes">
<property name="specialValueText">
<string>All</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Number of episodes to show</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_3">
<property name="title">
<string>gpodder.net</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>Clementine can synchronize your subscription list with your other computers and podcast applications. &lt;a href=&quot;https://gpodder.net/register/&quot;&gt;Create an account&lt;/a&gt;.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="LoginStateWidget" name="login_state" native="true"/>
</item>
<item>
<widget class="QWidget" name="login_group" native="true">
<layout class="QFormLayout" name="formLayout_3">
<property name="margin">
<number>0</number>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Username</string>
</property>
</widget>
</item>
<item row="0" column="1">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLineEdit" name="username"/>
</item>
<item>
<widget class="QPushButton" name="login">
<property name="text">
<string>Sign in</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Password</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="password">
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Device name</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="device_name"/>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>LoginStateWidget</class>
<extends>QWidget</extends>
<header>widgets/loginstatewidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>check_interval</tabstop>
<tabstop>download_dir</tabstop>
<tabstop>download_dir_browse</tabstop>
<tabstop>auto_download</tabstop>
<tabstop>delete_after</tabstop>
<tabstop>username</tabstop>
<tabstop>password</tabstop>
<tabstop>device_name</tabstop>
<tabstop>login</tabstop>
</tabstops>
<connections/>
</ui>

View File

@@ -106,6 +106,7 @@ void ScrobblerSettingsPage::Load() {
ui_->checkbox_source_unknown->setChecked(scrobbler_->sources().contains(Song::Source::Unknown));
ui_->checkbox_lastfm_enable->setChecked(lastfmscrobbler_->enabled());
ui_->lineedit_lastfm_api_key->setText(lastfmscrobbler_->api_key());
LastFM_RefreshControls(lastfmscrobbler_->authenticated());
ui_->checkbox_listenbrainz_enable->setChecked(listenbrainzscrobbler_->enabled());
@@ -152,6 +153,7 @@ void ScrobblerSettingsPage::Save() {
s.beginGroup(LastFMScrobbler::kSettingsGroup);
s.setValue(kEnabled, ui_->checkbox_lastfm_enable->isChecked());
s.setValue(kApiKey, ui_->lineedit_lastfm_api_key->text());
s.endGroup();
s.beginGroup(ListenBrainzScrobbler::kSettingsGroup);

View File

@@ -234,6 +234,43 @@
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="layout_lastfm_api_key">
<item>
<widget class="QLabel" name="label_lastfm_api_key">
<property name="minimumSize">
<size>
<width>80</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>API key:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineedit_lastfm_api_key">
<property name="placeholderText">
<string>Optional - your own Last.fm API key</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="label_lastfm_api_key_info">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-size:8pt;&quot;&gt;Using your own API key can help avoid rate limiting for large libraries. Get one at &lt;/span&gt;&lt;a href=&quot;https://www.last.fm/api/account/create&quot;&gt;&lt;span style=&quot; font-size:8pt; text-decoration: underline; color:#0000ff;&quot;&gt;https://www.last.fm/api/account/create&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="LoginStateWidget" name="widget_lastfm_login_state" native="true"/>
</item>
@@ -394,6 +431,7 @@
<tabstop>checkbox_source_somafm</tabstop>
<tabstop>checkbox_source_radioparadise</tabstop>
<tabstop>checkbox_lastfm_enable</tabstop>
<tabstop>lineedit_lastfm_api_key</tabstop>
<tabstop>button_lastfm_login</tabstop>
<tabstop>checkbox_listenbrainz_enable</tabstop>
<tabstop>lineedit_listenbrainz_user_token</tabstop>

View File

@@ -1,168 +0,0 @@
/* This file is part of Clementine.
Copyright 2010, David Sansome <me@davidsansome.com>
Clementine 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.
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#include "widgetfadehelper.h"
#include <QPainter>
#include <QResizeEvent>
#include <QTimeLine>
#include <QtDebug>
#include "core/qt_blurimage.h"
const int WidgetFadeHelper::kLoadingPadding = 9;
const int WidgetFadeHelper::kLoadingBorderRadius = 10;
WidgetFadeHelper::WidgetFadeHelper(QWidget* parent, const int msec)
: QWidget(parent),
parent_(parent),
blur_timeline_(new QTimeLine(msec, this)),
fade_timeline_(new QTimeLine(msec, this)) {
parent->installEventFilter(this);
connect(blur_timeline_, SIGNAL(valueChanged(qreal)), SLOT(update()));
connect(fade_timeline_, SIGNAL(valueChanged(qreal)), SLOT(update()));
connect(fade_timeline_, SIGNAL(finished()), SLOT(FadeFinished()));
hide();
}
bool WidgetFadeHelper::eventFilter(QObject* obj, QEvent* event) {
// We're only interested in our parent's resize events
if (obj != parent_ || event->type() != QEvent::Resize) return false;
// Don't care if we're hidden
if (!isVisible()) return false;
QResizeEvent* re = static_cast<QResizeEvent*>(event);
if (re->oldSize() == re->size()) {
// Ignore phoney resize events
return false;
}
// Get a new capture of the parent
hide();
CaptureParent();
show();
return false;
}
void WidgetFadeHelper::StartBlur() {
CaptureParent();
// Cover the parent
raise();
show();
// Start the timeline
blur_timeline_->stop();
blur_timeline_->start();
setAttribute(Qt::WA_TransparentForMouseEvents, false);
}
void WidgetFadeHelper::CaptureParent() {
// Take a "screenshot" of the window
original_pixmap_ = parent_->grab();
QImage original_image = original_pixmap_.toImage();
// Blur it
QImage blurred(original_image.size(), QImage::Format_ARGB32_Premultiplied);
blurred.fill(Qt::transparent);
QPainter blur_painter(&blurred);
blur_painter.save();
qt_blurImage(&blur_painter, original_image, 10.0, true, false);
blur_painter.restore();
// Draw some loading text over the top
QFont loading_font(font());
loading_font.setBold(true);
QFontMetrics loading_font_metrics(loading_font);
const QString loading_text = tr("Loading...");
const QSize loading_size(
kLoadingPadding * 2 + loading_font_metrics.width(loading_text),
kLoadingPadding * 2 + loading_font_metrics.height());
const QRect loading_rect((blurred.width() - loading_size.width()) / 2, 100,
loading_size.width(), loading_size.height());
blur_painter.setRenderHint(QPainter::Antialiasing);
blur_painter.setRenderHint(QPainter::HighQualityAntialiasing);
blur_painter.translate(0.5, 0.5);
blur_painter.setPen(QColor(200, 200, 200, 255));
blur_painter.setBrush(QColor(200, 200, 200, 192));
blur_painter.drawRoundedRect(loading_rect, kLoadingBorderRadius,
kLoadingBorderRadius);
blur_painter.setPen(palette().brush(QPalette::Text).color());
blur_painter.setFont(loading_font);
blur_painter.drawText(loading_rect.translated(-1, -1), Qt::AlignCenter,
loading_text);
blur_painter.translate(-0.5, -0.5);
blur_painter.end();
blurred_pixmap_ = QPixmap::fromImage(blurred);
resize(parent_->size());
}
void WidgetFadeHelper::StartFade() {
if (blur_timeline_->state() == QTimeLine::Running) {
// Blur timeline is still running, so we need render the current state
// into a new pixmap.
QPixmap pixmap(original_pixmap_);
QPainter painter(&pixmap);
painter.setOpacity(blur_timeline_->currentValue());
painter.drawPixmap(0, 0, blurred_pixmap_);
painter.end();
blurred_pixmap_ = pixmap;
}
blur_timeline_->stop();
original_pixmap_ = QPixmap();
// Start the timeline
fade_timeline_->stop();
fade_timeline_->start();
setAttribute(Qt::WA_TransparentForMouseEvents, true);
}
void WidgetFadeHelper::paintEvent(QPaintEvent*) {
QPainter p(this);
if (fade_timeline_->state() != QTimeLine::Running) {
// We're fading in the blur
p.drawPixmap(0, 0, original_pixmap_);
p.setOpacity(blur_timeline_->currentValue());
} else {
// Fading out the blur into the new image
p.setOpacity(1.0 - fade_timeline_->currentValue());
}
p.drawPixmap(0, 0, blurred_pixmap_);
}
void WidgetFadeHelper::FadeFinished() {
hide();
original_pixmap_ = QPixmap();
blurred_pixmap_ = QPixmap();
}

View File

@@ -1,57 +0,0 @@
/* This file is part of Clementine.
Copyright 2010, David Sansome <me@davidsansome.com>
Clementine 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.
Clementine 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 Clementine. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef WIDGETFADEHELPER_H
#define WIDGETFADEHELPER_H
#include <QWidget>
class QTimeLine;
class WidgetFadeHelper : public QWidget {
Q_OBJECT
public:
WidgetFadeHelper(QWidget* parent, const int msec = 500);
public slots:
void StartBlur();
void StartFade();
protected:
void paintEvent(QPaintEvent*);
bool eventFilter(QObject* obj, QEvent* event);
private slots:
void FadeFinished();
private:
void CaptureParent();
private:
static const int kLoadingPadding;
static const int kLoadingBorderRadius;
QWidget* parent_;
QTimeLine* blur_timeline_;
QTimeLine* fade_timeline_;
QPixmap original_pixmap_;
QPixmap blurred_pixmap_;
};
#endif // WIDGETFADEHELPER_H