Compare commits
1 Commits
podcasts
...
cloud_stor
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c57c923d9 |
@@ -208,8 +208,6 @@ else()
|
|||||||
pkg_check_modules(TAGLIB REQUIRED IMPORTED_TARGET taglib>=1.12)
|
pkg_check_modules(TAGLIB REQUIRED IMPORTED_TARGET taglib>=1.12)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
pkg_check_modules(LIBMYGPO libmygpo-qt6)
|
|
||||||
|
|
||||||
find_package(GTest)
|
find_package(GTest)
|
||||||
|
|
||||||
pkg_check_modules(LIBSPARSEHASH IMPORTED_TARGET libsparsehash)
|
pkg_check_modules(LIBSPARSEHASH IMPORTED_TARGET libsparsehash)
|
||||||
@@ -381,9 +379,14 @@ optional_component(STREAMTAGREADER ON "Stream tagreader"
|
|||||||
|
|
||||||
optional_component(DISCORD_RPC ON "Discord Rich Presence"
|
optional_component(DISCORD_RPC ON "Discord Rich Presence"
|
||||||
DEPENDS "RapidJSON" RapidJSON_FOUND
|
DEPENDS "RapidJSON" RapidJSON_FOUND
|
||||||
|
|
||||||
|
optional_component(DROPBOX ON "Streaming: Dropbox"
|
||||||
|
DEPENDS "Stream tagreader" HAVE_STREAMTAGREADER
|
||||||
)
|
)
|
||||||
|
|
||||||
optional_component(PODCASTS ON "Podcasts support" DEPENDS "libmygpo" LIBMYGPO_FOUND)
|
optional_component(ONEDRIVE ON "Streaming: OneDrive"
|
||||||
|
DEPENDS "Stream tagreader" HAVE_STREAMTAGREADER
|
||||||
|
)
|
||||||
|
|
||||||
if(HAVE_SONGFINGERPRINTING OR HAVE_MUSICBRAINZ)
|
if(HAVE_SONGFINGERPRINTING OR HAVE_MUSICBRAINZ)
|
||||||
set(HAVE_CHROMAPRINT ON)
|
set(HAVE_CHROMAPRINT ON)
|
||||||
@@ -780,6 +783,7 @@ set(SOURCES
|
|||||||
src/streaming/streamingcollectionviewcontainer.cpp
|
src/streaming/streamingcollectionviewcontainer.cpp
|
||||||
src/streaming/streamingsearchview.cpp
|
src/streaming/streamingsearchview.cpp
|
||||||
src/streaming/streamsongmimedata.cpp
|
src/streaming/streamsongmimedata.cpp
|
||||||
|
src/streaming/cloudstoragestreamingservice.cpp
|
||||||
|
|
||||||
src/radios/radioservices.cpp
|
src/radios/radioservices.cpp
|
||||||
src/radios/radiobackend.cpp
|
src/radios/radiobackend.cpp
|
||||||
@@ -1076,6 +1080,7 @@ set(HEADERS
|
|||||||
src/streaming/streamingtabsview.h
|
src/streaming/streamingtabsview.h
|
||||||
src/streaming/streamingcollectionview.h
|
src/streaming/streamingcollectionview.h
|
||||||
src/streaming/streamingcollectionviewcontainer.h
|
src/streaming/streamingcollectionviewcontainer.h
|
||||||
|
src/streaming/cloudstoragestreamingservice.h
|
||||||
|
|
||||||
src/radios/radioservices.h
|
src/radios/radioservices.h
|
||||||
src/radios/radiobackend.h
|
src/radios/radiobackend.h
|
||||||
@@ -1484,67 +1489,23 @@ optional_source(HAVE_QOBUZ
|
|||||||
src/settings/qobuzsettingspage.ui
|
src/settings/qobuzsettingspage.ui
|
||||||
)
|
)
|
||||||
|
|
||||||
optional_source(HAVE_PODCASTS
|
optional_source(HAVE_DROPBOX
|
||||||
SOURCES
|
SOURCES
|
||||||
podcasts/gpoddersync.cpp
|
src/dropbox/dropboxservice.cpp
|
||||||
podcasts/gpoddertoptagsmodel.cpp
|
src/dropbox/dropboxurlhandler.cpp
|
||||||
podcasts/gpoddertoptagspage.cpp
|
src/dropbox/dropboxbaserequest.cpp
|
||||||
podcasts/itunessearchpage.cpp
|
src/dropbox/dropboxsongsrequest.cpp
|
||||||
podcasts/podcastbackend.cpp
|
src/dropbox/dropboxstreamurlrequest.cpp
|
||||||
podcasts/podcastservice.cpp
|
src/settings/dropboxsettingspage.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
|
HEADERS
|
||||||
podcasts/addpodcastbyurl.h
|
src/dropbox/dropboxservice.h
|
||||||
podcasts/addpodcastdialog.h
|
src/dropbox/dropboxurlhandler.h
|
||||||
podcasts/addpodcastpage.h
|
src/dropbox/dropboxbaserequest.h
|
||||||
podcasts/episodeinfowidget.h
|
src/dropbox/dropboxsongsrequest.h
|
||||||
podcasts/fixedopmlpage.h
|
src/dropbox/dropboxstreamurlrequest.h
|
||||||
podcasts/gpoddersync.h
|
src/settings/dropboxsettingspage.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
|
UI
|
||||||
podcasts/addpodcastbyurl.ui
|
src/settings/dropboxsettingspage.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_cpp(SOURCES ${HEADERS})
|
||||||
|
|||||||
@@ -98,6 +98,7 @@
|
|||||||
<file>icons/128x128/somafm.png</file>
|
<file>icons/128x128/somafm.png</file>
|
||||||
<file>icons/128x128/radioparadise.png</file>
|
<file>icons/128x128/radioparadise.png</file>
|
||||||
<file>icons/128x128/musicbrainz.png</file>
|
<file>icons/128x128/musicbrainz.png</file>
|
||||||
|
<file>icons/128x128/dropbox.png</file>
|
||||||
<file>icons/64x64/albums.png</file>
|
<file>icons/64x64/albums.png</file>
|
||||||
<file>icons/64x64/alsa.png</file>
|
<file>icons/64x64/alsa.png</file>
|
||||||
<file>icons/64x64/application-exit.png</file>
|
<file>icons/64x64/application-exit.png</file>
|
||||||
@@ -197,6 +198,7 @@
|
|||||||
<file>icons/64x64/somafm.png</file>
|
<file>icons/64x64/somafm.png</file>
|
||||||
<file>icons/64x64/radioparadise.png</file>
|
<file>icons/64x64/radioparadise.png</file>
|
||||||
<file>icons/64x64/musicbrainz.png</file>
|
<file>icons/64x64/musicbrainz.png</file>
|
||||||
|
<file>icons/64x64/dropbox.png</file>
|
||||||
<file>icons/48x48/albums.png</file>
|
<file>icons/48x48/albums.png</file>
|
||||||
<file>icons/48x48/alsa.png</file>
|
<file>icons/48x48/alsa.png</file>
|
||||||
<file>icons/48x48/application-exit.png</file>
|
<file>icons/48x48/application-exit.png</file>
|
||||||
@@ -300,6 +302,7 @@
|
|||||||
<file>icons/48x48/somafm.png</file>
|
<file>icons/48x48/somafm.png</file>
|
||||||
<file>icons/48x48/radioparadise.png</file>
|
<file>icons/48x48/radioparadise.png</file>
|
||||||
<file>icons/48x48/musicbrainz.png</file>
|
<file>icons/48x48/musicbrainz.png</file>
|
||||||
|
<file>icons/48x48/dropbox.png</file>
|
||||||
<file>icons/32x32/albums.png</file>
|
<file>icons/32x32/albums.png</file>
|
||||||
<file>icons/32x32/alsa.png</file>
|
<file>icons/32x32/alsa.png</file>
|
||||||
<file>icons/32x32/application-exit.png</file>
|
<file>icons/32x32/application-exit.png</file>
|
||||||
@@ -403,6 +406,7 @@
|
|||||||
<file>icons/32x32/somafm.png</file>
|
<file>icons/32x32/somafm.png</file>
|
||||||
<file>icons/32x32/radioparadise.png</file>
|
<file>icons/32x32/radioparadise.png</file>
|
||||||
<file>icons/32x32/musicbrainz.png</file>
|
<file>icons/32x32/musicbrainz.png</file>
|
||||||
|
<file>icons/32x32/dropbox.png</file>
|
||||||
<file>icons/22x22/albums.png</file>
|
<file>icons/22x22/albums.png</file>
|
||||||
<file>icons/22x22/alsa.png</file>
|
<file>icons/22x22/alsa.png</file>
|
||||||
<file>icons/22x22/application-exit.png</file>
|
<file>icons/22x22/application-exit.png</file>
|
||||||
@@ -506,5 +510,6 @@
|
|||||||
<file>icons/22x22/somafm.png</file>
|
<file>icons/22x22/somafm.png</file>
|
||||||
<file>icons/22x22/radioparadise.png</file>
|
<file>icons/22x22/radioparadise.png</file>
|
||||||
<file>icons/22x22/musicbrainz.png</file>
|
<file>icons/22x22/musicbrainz.png</file>
|
||||||
|
<file>icons/22x22/dropbox.png</file>
|
||||||
</qresource>
|
</qresource>
|
||||||
</RCC>
|
</RCC>
|
||||||
|
|||||||
BIN
data/icons/128x128/dropbox.png
Normal file
BIN
data/icons/128x128/dropbox.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
BIN
data/icons/22x22/dropbox.png
Normal file
BIN
data/icons/22x22/dropbox.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 638 B |
BIN
data/icons/32x32/dropbox.png
Normal file
BIN
data/icons/32x32/dropbox.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 746 B |
BIN
data/icons/48x48/dropbox.png
Normal file
BIN
data/icons/48x48/dropbox.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1011 B |
BIN
data/icons/64x64/dropbox.png
Normal file
BIN
data/icons/64x64/dropbox.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
BIN
data/icons/full/dropbox.png
Normal file
BIN
data/icons/full/dropbox.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
82
data/schema/schema-22.sql
Normal file
82
data/schema/schema-22.sql
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS dropbox_songs (
|
||||||
|
|
||||||
|
title TEXT,
|
||||||
|
album TEXT,
|
||||||
|
artist TEXT,
|
||||||
|
albumartist TEXT,
|
||||||
|
track INTEGER NOT NULL DEFAULT -1,
|
||||||
|
disc INTEGER NOT NULL DEFAULT -1,
|
||||||
|
year INTEGER NOT NULL DEFAULT -1,
|
||||||
|
originalyear INTEGER NOT NULL DEFAULT -1,
|
||||||
|
genre TEXT,
|
||||||
|
compilation INTEGER NOT NULL DEFAULT 0,
|
||||||
|
composer TEXT,
|
||||||
|
performer TEXT,
|
||||||
|
grouping TEXT,
|
||||||
|
comment TEXT,
|
||||||
|
lyrics TEXT,
|
||||||
|
|
||||||
|
artist_id TEXT,
|
||||||
|
album_id TEXT,
|
||||||
|
song_id TEXT,
|
||||||
|
|
||||||
|
beginning INTEGER NOT NULL DEFAULT 0,
|
||||||
|
length INTEGER NOT NULL DEFAULT 0,
|
||||||
|
|
||||||
|
bitrate INTEGER NOT NULL DEFAULT -1,
|
||||||
|
samplerate INTEGER NOT NULL DEFAULT -1,
|
||||||
|
bitdepth INTEGER NOT NULL DEFAULT -1,
|
||||||
|
|
||||||
|
source INTEGER NOT NULL DEFAULT 0,
|
||||||
|
directory_id INTEGER NOT NULL DEFAULT -1,
|
||||||
|
url TEXT NOT NULL,
|
||||||
|
filetype INTEGER NOT NULL DEFAULT 0,
|
||||||
|
filesize INTEGER NOT NULL DEFAULT -1,
|
||||||
|
mtime INTEGER NOT NULL DEFAULT -1,
|
||||||
|
ctime INTEGER NOT NULL DEFAULT -1,
|
||||||
|
unavailable INTEGER DEFAULT 0,
|
||||||
|
|
||||||
|
fingerprint TEXT,
|
||||||
|
|
||||||
|
playcount INTEGER NOT NULL DEFAULT 0,
|
||||||
|
skipcount INTEGER NOT NULL DEFAULT 0,
|
||||||
|
lastplayed INTEGER NOT NULL DEFAULT -1,
|
||||||
|
lastseen INTEGER NOT NULL DEFAULT -1,
|
||||||
|
|
||||||
|
compilation_detected INTEGER DEFAULT 0,
|
||||||
|
compilation_on INTEGER NOT NULL DEFAULT 0,
|
||||||
|
compilation_off INTEGER NOT NULL DEFAULT 0,
|
||||||
|
compilation_effective INTEGER NOT NULL DEFAULT 0,
|
||||||
|
|
||||||
|
art_embedded INTEGER DEFAULT 0,
|
||||||
|
art_automatic TEXT,
|
||||||
|
art_manual TEXT,
|
||||||
|
art_unset INTEGER DEFAULT 0,
|
||||||
|
|
||||||
|
effective_albumartist TEXT,
|
||||||
|
effective_originalyear INTEGER NOT NULL DEFAULT 0,
|
||||||
|
|
||||||
|
cue_path TEXT,
|
||||||
|
|
||||||
|
rating INTEGER DEFAULT -1,
|
||||||
|
|
||||||
|
acoustid_id TEXT,
|
||||||
|
acoustid_fingerprint TEXT,
|
||||||
|
|
||||||
|
musicbrainz_album_artist_id TEXT,
|
||||||
|
musicbrainz_artist_id TEXT,
|
||||||
|
musicbrainz_original_artist_id TEXT,
|
||||||
|
musicbrainz_album_id TEXT,
|
||||||
|
musicbrainz_original_album_id TEXT,
|
||||||
|
musicbrainz_recording_id TEXT,
|
||||||
|
musicbrainz_track_id TEXT,
|
||||||
|
musicbrainz_disc_id TEXT,
|
||||||
|
musicbrainz_release_group_id TEXT,
|
||||||
|
musicbrainz_work_id TEXT,
|
||||||
|
|
||||||
|
ebur128_integrated_loudness_lufs REAL,
|
||||||
|
ebur128_loudness_range_lu REAL
|
||||||
|
|
||||||
|
);
|
||||||
|
|
||||||
|
UPDATE schema_version SET version=22;
|
||||||
@@ -1018,6 +1018,87 @@ CREATE TABLE IF NOT EXISTS qobuz_songs (
|
|||||||
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS dropbox_songs (
|
||||||
|
|
||||||
|
title TEXT,
|
||||||
|
album TEXT,
|
||||||
|
artist TEXT,
|
||||||
|
albumartist TEXT,
|
||||||
|
track INTEGER NOT NULL DEFAULT -1,
|
||||||
|
disc INTEGER NOT NULL DEFAULT -1,
|
||||||
|
year INTEGER NOT NULL DEFAULT -1,
|
||||||
|
originalyear INTEGER NOT NULL DEFAULT -1,
|
||||||
|
genre TEXT,
|
||||||
|
compilation INTEGER NOT NULL DEFAULT 0,
|
||||||
|
composer TEXT,
|
||||||
|
performer TEXT,
|
||||||
|
grouping TEXT,
|
||||||
|
comment TEXT,
|
||||||
|
lyrics TEXT,
|
||||||
|
|
||||||
|
artist_id TEXT,
|
||||||
|
album_id TEXT,
|
||||||
|
song_id TEXT,
|
||||||
|
|
||||||
|
beginning INTEGER NOT NULL DEFAULT 0,
|
||||||
|
length INTEGER NOT NULL DEFAULT 0,
|
||||||
|
|
||||||
|
bitrate INTEGER NOT NULL DEFAULT -1,
|
||||||
|
samplerate INTEGER NOT NULL DEFAULT -1,
|
||||||
|
bitdepth INTEGER NOT NULL DEFAULT -1,
|
||||||
|
|
||||||
|
source INTEGER NOT NULL DEFAULT 0,
|
||||||
|
directory_id INTEGER NOT NULL DEFAULT -1,
|
||||||
|
url TEXT NOT NULL,
|
||||||
|
filetype INTEGER NOT NULL DEFAULT 0,
|
||||||
|
filesize INTEGER NOT NULL DEFAULT -1,
|
||||||
|
mtime INTEGER NOT NULL DEFAULT -1,
|
||||||
|
ctime INTEGER NOT NULL DEFAULT -1,
|
||||||
|
unavailable INTEGER DEFAULT 0,
|
||||||
|
|
||||||
|
fingerprint TEXT,
|
||||||
|
|
||||||
|
playcount INTEGER NOT NULL DEFAULT 0,
|
||||||
|
skipcount INTEGER NOT NULL DEFAULT 0,
|
||||||
|
lastplayed INTEGER NOT NULL DEFAULT -1,
|
||||||
|
lastseen INTEGER NOT NULL DEFAULT -1,
|
||||||
|
|
||||||
|
compilation_detected INTEGER DEFAULT 0,
|
||||||
|
compilation_on INTEGER NOT NULL DEFAULT 0,
|
||||||
|
compilation_off INTEGER NOT NULL DEFAULT 0,
|
||||||
|
compilation_effective INTEGER NOT NULL DEFAULT 0,
|
||||||
|
|
||||||
|
art_embedded INTEGER DEFAULT 0,
|
||||||
|
art_automatic TEXT,
|
||||||
|
art_manual TEXT,
|
||||||
|
art_unset INTEGER DEFAULT 0,
|
||||||
|
|
||||||
|
effective_albumartist TEXT,
|
||||||
|
effective_originalyear INTEGER NOT NULL DEFAULT 0,
|
||||||
|
|
||||||
|
cue_path TEXT,
|
||||||
|
|
||||||
|
rating INTEGER DEFAULT -1,
|
||||||
|
|
||||||
|
acoustid_id TEXT,
|
||||||
|
acoustid_fingerprint TEXT,
|
||||||
|
|
||||||
|
musicbrainz_album_artist_id TEXT,
|
||||||
|
musicbrainz_artist_id TEXT,
|
||||||
|
musicbrainz_original_artist_id TEXT,
|
||||||
|
musicbrainz_album_id TEXT,
|
||||||
|
musicbrainz_original_album_id TEXT,
|
||||||
|
musicbrainz_recording_id TEXT,
|
||||||
|
musicbrainz_track_id TEXT,
|
||||||
|
musicbrainz_disc_id TEXT,
|
||||||
|
musicbrainz_release_group_id TEXT,
|
||||||
|
musicbrainz_work_id TEXT,
|
||||||
|
|
||||||
|
ebur128_integrated_loudness_lufs REAL,
|
||||||
|
ebur128_loudness_range_lu REAL
|
||||||
|
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS playlists (
|
CREATE TABLE IF NOT EXISTS playlists (
|
||||||
|
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
|
|||||||
@@ -41,9 +41,12 @@ bool CollectionPlaylistItem::InitFromQuery(const SqlRow &query) {
|
|||||||
case Song::Source::Collection:
|
case Song::Source::Collection:
|
||||||
col = 0;
|
col = 0;
|
||||||
break;
|
break;
|
||||||
default:
|
case Song::Source::Dropbox:
|
||||||
col = static_cast<int>(Song::kRowIdColumns.count());
|
col = static_cast<int>(Song::kRowIdColumns.count());
|
||||||
break;
|
break;
|
||||||
|
default:
|
||||||
|
col = static_cast<int>(Song::kRowIdColumns.count() * 2);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
song_.InitFromQuery(query, true, col);
|
song_.InitFromQuery(query, true, col);
|
||||||
|
|||||||
@@ -33,7 +33,8 @@
|
|||||||
#cmakedefine HAVE_SPOTIFY
|
#cmakedefine HAVE_SPOTIFY
|
||||||
#cmakedefine HAVE_QOBUZ
|
#cmakedefine HAVE_QOBUZ
|
||||||
#cmakedefine HAVE_DISCORD_RPC
|
#cmakedefine HAVE_DISCORD_RPC
|
||||||
#cmakedefine HAVE_PODCASTS
|
#cmakedefine HAVE_DROPBOX
|
||||||
|
#cmakedefine HAVE_ONEDRIVE
|
||||||
|
|
||||||
#cmakedefine HAVE_TAGLIB_DSFFILE
|
#cmakedefine HAVE_TAGLIB_DSFFILE
|
||||||
#cmakedefine HAVE_TAGLIB_DSDIFFFILE
|
#cmakedefine HAVE_TAGLIB_DSDIFFFILE
|
||||||
|
|||||||
30
src/constants/dropboxconstants.h
Normal file
30
src/constants/dropboxconstants.h
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* 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 DROPBOXCONSTANTS_H
|
||||||
|
#define DROPBOXCONSTANTS_H
|
||||||
|
|
||||||
|
namespace DropboxConstants {
|
||||||
|
|
||||||
|
constexpr char kApiUrl[] = "https://api.dropboxapi.com";
|
||||||
|
constexpr char kNotifyApiUrl[] = "https://notify.dropboxapi.com";
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
#endif // DROPBOXCONSTANTS_H
|
||||||
46
src/constants/dropboxsettings.h
Normal file
46
src/constants/dropboxsettings.h
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* 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 DROPBOXSETTINGS_H
|
||||||
|
#define DROPBOXSETTINGS_H
|
||||||
|
|
||||||
|
namespace DropboxSettings {
|
||||||
|
|
||||||
|
constexpr char kSettingsGroup[] = "Dropbox";
|
||||||
|
|
||||||
|
constexpr char kEnabled[] = "enabled";
|
||||||
|
constexpr char kSearchDelay[] = "searchdelay";
|
||||||
|
constexpr char kArtistsSearchLimit[] = "artistssearchlimit";
|
||||||
|
constexpr char kAlbumsSearchLimit[] = "albumssearchlimit";
|
||||||
|
constexpr char kSongsSearchLimit[] = "songssearchlimit";
|
||||||
|
constexpr char kFetchAlbums[] = "fetchalbums";
|
||||||
|
constexpr char kDownloadAlbumCovers[] = "downloadalbumcovers";
|
||||||
|
|
||||||
|
constexpr char kTokenType[] = "token_type";
|
||||||
|
constexpr char kAccessToken[] = "access_token";
|
||||||
|
constexpr char kRefreshToken[] = "refresh_token";
|
||||||
|
constexpr char kExpiresIn[] = "expires_in";
|
||||||
|
constexpr char kLoginTime[] = "login_time";
|
||||||
|
|
||||||
|
constexpr char kApiUrl[] = "https://api.dropboxapi.com";
|
||||||
|
constexpr char kNotifyApiUrl[] = "https://notify.dropboxapi.com";
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
#endif // DROPBOXSETTINGS_H
|
||||||
@@ -105,6 +105,10 @@
|
|||||||
# include "covermanager/qobuzcoverprovider.h"
|
# include "covermanager/qobuzcoverprovider.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef HAVE_DROPBOX
|
||||||
|
# include "dropbox/dropboxservice.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef HAVE_MOODBAR
|
#ifdef HAVE_MOODBAR
|
||||||
# include "moodbar/moodbarcontroller.h"
|
# include "moodbar/moodbarcontroller.h"
|
||||||
# include "moodbar/moodbarloader.h"
|
# include "moodbar/moodbarloader.h"
|
||||||
@@ -113,16 +117,7 @@
|
|||||||
#include "radios/radioservices.h"
|
#include "radios/radioservices.h"
|
||||||
#include "radios/radiobackend.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 std::make_shared;
|
||||||
|
|
||||||
using namespace std::chrono_literals;
|
using namespace std::chrono_literals;
|
||||||
|
|
||||||
class ApplicationImpl {
|
class ApplicationImpl {
|
||||||
@@ -209,6 +204,9 @@ class ApplicationImpl {
|
|||||||
#endif
|
#endif
|
||||||
#ifdef HAVE_QOBUZ
|
#ifdef HAVE_QOBUZ
|
||||||
streaming_services->AddService(make_shared<QobuzService>(app->task_manager(), app->database(), app->network(), app->url_handlers(), app->albumcover_loader()));
|
streaming_services->AddService(make_shared<QobuzService>(app->task_manager(), app->database(), app->network(), app->url_handlers(), app->albumcover_loader()));
|
||||||
|
#endif
|
||||||
|
#ifdef HAVE_DROPBOX
|
||||||
|
streaming_services->AddService(make_shared<DropboxService>(app->task_manager(), app->database(), app->network(), app->url_handlers(), app->tagreader_client(), app->albumcover_loader()));
|
||||||
#endif
|
#endif
|
||||||
return streaming_services;
|
return streaming_services;
|
||||||
}),
|
}),
|
||||||
@@ -225,21 +223,6 @@ class ApplicationImpl {
|
|||||||
#ifdef HAVE_MOODBAR
|
#ifdef HAVE_MOODBAR
|
||||||
moodbar_loader_([app]() { return new MoodbarLoader(app); }),
|
moodbar_loader_([app]() { return new MoodbarLoader(app); }),
|
||||||
moodbar_controller_([app]() { return new MoodbarController(app->player(), app->moodbar_loader()); }),
|
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
|
#endif
|
||||||
lastfm_import_([app]() { return new LastFMImport(app->network()); })
|
lastfm_import_([app]() { return new LastFMImport(app->network()); })
|
||||||
{}
|
{}
|
||||||
@@ -265,13 +248,6 @@ class ApplicationImpl {
|
|||||||
#ifdef HAVE_MOODBAR
|
#ifdef HAVE_MOODBAR
|
||||||
Lazy<MoodbarLoader> moodbar_loader_;
|
Lazy<MoodbarLoader> moodbar_loader_;
|
||||||
Lazy<MoodbarController> moodbar_controller_;
|
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
|
#endif
|
||||||
Lazy<LastFMImport> lastfm_import_;
|
Lazy<LastFMImport> lastfm_import_;
|
||||||
|
|
||||||
@@ -421,10 +397,3 @@ SharedPtr<LastFMImport> Application::lastfm_import() const { return p_->lastfm_i
|
|||||||
SharedPtr<MoodbarController> Application::moodbar_controller() const { return p_->moodbar_controller_.ptr(); }
|
SharedPtr<MoodbarController> Application::moodbar_controller() const { return p_->moodbar_controller_.ptr(); }
|
||||||
SharedPtr<MoodbarLoader> Application::moodbar_loader() const { return p_->moodbar_loader_.ptr(); }
|
SharedPtr<MoodbarLoader> Application::moodbar_loader() const { return p_->moodbar_loader_.ptr(); }
|
||||||
#endif
|
#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
|
|
||||||
|
|||||||
@@ -63,14 +63,6 @@ class RadioServices;
|
|||||||
class MoodbarController;
|
class MoodbarController;
|
||||||
class MoodbarLoader;
|
class MoodbarLoader;
|
||||||
#endif
|
#endif
|
||||||
#ifdef HAVE_PODCASTS
|
|
||||||
class PodcastBackend;
|
|
||||||
class GPodderSync;
|
|
||||||
class PodcastDownloader;
|
|
||||||
class PodcastUpdater;
|
|
||||||
class PodcastDeleter;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
|
|
||||||
class Application : public QObject {
|
class Application : public QObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -110,13 +102,6 @@ class Application : public QObject {
|
|||||||
SharedPtr<MoodbarController> moodbar_controller() const;
|
SharedPtr<MoodbarController> moodbar_controller() const;
|
||||||
SharedPtr<MoodbarLoader> moodbar_loader() const;
|
SharedPtr<MoodbarLoader> moodbar_loader() const;
|
||||||
#endif
|
#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;
|
SharedPtr<LastFMImport> lastfm_import() const;
|
||||||
|
|
||||||
|
|||||||
@@ -178,6 +178,9 @@
|
|||||||
#ifdef HAVE_QOBUZ
|
#ifdef HAVE_QOBUZ
|
||||||
# include "constants/qobuzsettings.h"
|
# include "constants/qobuzsettings.h"
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef HAVE_DROPBOX
|
||||||
|
# include "constants/dropboxsettings.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
#include "streaming/streamingservices.h"
|
#include "streaming/streamingservices.h"
|
||||||
#include "streaming/streamingservice.h"
|
#include "streaming/streamingservice.h"
|
||||||
@@ -355,6 +358,9 @@ MainWindow::MainWindow(Application *app,
|
|||||||
#endif
|
#endif
|
||||||
#ifdef HAVE_QOBUZ
|
#ifdef HAVE_QOBUZ
|
||||||
qobuz_view_(new StreamingTabsView(app->streaming_services()->ServiceBySource(Song::Source::Qobuz), app->albumcover_loader(), QLatin1String(QobuzSettings::kSettingsGroup), this)),
|
qobuz_view_(new StreamingTabsView(app->streaming_services()->ServiceBySource(Song::Source::Qobuz), app->albumcover_loader(), QLatin1String(QobuzSettings::kSettingsGroup), this)),
|
||||||
|
#endif
|
||||||
|
#ifdef HAVE_DROPBOX
|
||||||
|
dropbox_view_(new StreamingSongsView(app->streaming_services()->ServiceBySource(Song::Source::Dropbox), QLatin1String(DropboxSettings::kSettingsGroup), this)),
|
||||||
#endif
|
#endif
|
||||||
radio_view_(new RadioViewContainer(this)),
|
radio_view_(new RadioViewContainer(this)),
|
||||||
lastfm_import_dialog_(new LastFMImportDialog(app_->lastfm_import(), this)),
|
lastfm_import_dialog_(new LastFMImportDialog(app_->lastfm_import(), this)),
|
||||||
@@ -441,6 +447,9 @@ MainWindow::MainWindow(Application *app,
|
|||||||
#ifdef HAVE_QOBUZ
|
#ifdef HAVE_QOBUZ
|
||||||
ui_->tabs->AddTab(qobuz_view_, u"qobuz"_s, IconLoader::Load(u"qobuz"_s, true, 0, 32), tr("Qobuz"));
|
ui_->tabs->AddTab(qobuz_view_, u"qobuz"_s, IconLoader::Load(u"qobuz"_s, true, 0, 32), tr("Qobuz"));
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef HAVE_DROPBOX
|
||||||
|
ui_->tabs->AddTab(dropbox_view_, u"dropbox"_s, IconLoader::Load(u"dropbox"_s, true, 0, 32), tr("Dropbox"));
|
||||||
|
#endif
|
||||||
|
|
||||||
// Add the playing widget to the fancy tab widget
|
// Add the playing widget to the fancy tab widget
|
||||||
ui_->tabs->AddBottomWidget(ui_->widget_playing);
|
ui_->tabs->AddBottomWidget(ui_->widget_playing);
|
||||||
@@ -782,6 +791,12 @@ MainWindow::MainWindow(Application *app,
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef HAVE_DROPBOX
|
||||||
|
QObject::connect(dropbox_view_, &StreamingSongsView::ShowErrorDialog, this, &MainWindow::ShowErrorDialog);
|
||||||
|
QObject::connect(dropbox_view_, &StreamingSongsView::OpenSettingsDialog, this, &MainWindow::OpenServiceSettingsDialog);
|
||||||
|
QObject::connect(dropbox_view_->view(), &StreamingCollectionView::AddToPlaylistSignal, this, &MainWindow::AddToPlaylist);
|
||||||
|
#endif
|
||||||
|
|
||||||
QObject::connect(radio_view_, &RadioViewContainer::Refresh, &*app_->radio_services(), &RadioServices::RefreshChannels);
|
QObject::connect(radio_view_, &RadioViewContainer::Refresh, &*app_->radio_services(), &RadioServices::RefreshChannels);
|
||||||
QObject::connect(radio_view_->view(), &RadioView::GetChannels, &*app_->radio_services(), &RadioServices::GetChannels);
|
QObject::connect(radio_view_->view(), &RadioView::GetChannels, &*app_->radio_services(), &RadioServices::GetChannels);
|
||||||
QObject::connect(radio_view_->view(), &RadioView::AddToPlaylistSignal, this, &MainWindow::AddToPlaylist);
|
QObject::connect(radio_view_->view(), &RadioView::AddToPlaylistSignal, this, &MainWindow::AddToPlaylist);
|
||||||
@@ -1280,6 +1295,18 @@ void MainWindow::ReloadSettings() {
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef HAVE_DROPBOX
|
||||||
|
s.beginGroup(DropboxSettings::kSettingsGroup);
|
||||||
|
const bool enable_dropbox = s.value(DropboxSettings::kEnabled, false).toBool();
|
||||||
|
s.endGroup();
|
||||||
|
if (enable_dropbox) {
|
||||||
|
ui_->tabs->EnableTab(dropbox_view_);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ui_->tabs->DisableTab(dropbox_view_);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
ui_->tabs->ReloadSettings();
|
ui_->tabs->ReloadSettings();
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1326,10 +1353,12 @@ void MainWindow::ReloadAllSettings() {
|
|||||||
qobuz_view_->ReloadSettings();
|
qobuz_view_->ReloadSettings();
|
||||||
qobuz_view_->search_view()->ReloadSettings();
|
qobuz_view_->search_view()->ReloadSettings();
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef HAVE_DROPBOX
|
||||||
|
dropbox_view_->ReloadSettings();
|
||||||
|
#endif
|
||||||
#ifdef HAVE_DISCORD_RPC
|
#ifdef HAVE_DISCORD_RPC
|
||||||
discord_rich_presence_->ReloadSettings();
|
discord_rich_presence_->ReloadSettings();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::RefreshStyleSheet() {
|
void MainWindow::RefreshStyleSheet() {
|
||||||
@@ -2717,6 +2746,9 @@ void MainWindow::OpenServiceSettingsDialog(const Song::Source source) {
|
|||||||
case Song::Source::Spotify:
|
case Song::Source::Spotify:
|
||||||
settings_dialog_->OpenAtPage(SettingsDialog::Page::Spotify);
|
settings_dialog_->OpenAtPage(SettingsDialog::Page::Spotify);
|
||||||
break;
|
break;
|
||||||
|
case Song::Source::Dropbox:
|
||||||
|
settings_dialog_->OpenAtPage(SettingsDialog::Page::Dropbox);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -3398,6 +3430,11 @@ void MainWindow::FocusSearchField() {
|
|||||||
else if (ui_->tabs->currentIndex() == ui_->tabs->IndexOfTab(qobuz_view_) && !qobuz_view_->SearchFieldHasFocus()) {
|
else if (ui_->tabs->currentIndex() == ui_->tabs->IndexOfTab(qobuz_view_) && !qobuz_view_->SearchFieldHasFocus()) {
|
||||||
qobuz_view_->FocusSearchField();
|
qobuz_view_->FocusSearchField();
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
#ifdef HAVE_DROPBOX
|
||||||
|
else if (ui_->tabs->currentIndex() == ui_->tabs->IndexOfTab(dropbox_view_) && !dropbox_view_->SearchFieldHasFocus()) {
|
||||||
|
dropbox_view_->FocusSearchField();
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
else if (!ui_->playlist->SearchFieldHasFocus()) {
|
else if (!ui_->playlist->SearchFieldHasFocus()) {
|
||||||
ui_->playlist->FocusSearchField();
|
ui_->playlist->FocusSearchField();
|
||||||
|
|||||||
@@ -355,6 +355,9 @@ class MainWindow : public QMainWindow, public PlatformInterface {
|
|||||||
#ifdef HAVE_QOBUZ
|
#ifdef HAVE_QOBUZ
|
||||||
StreamingTabsView *qobuz_view_;
|
StreamingTabsView *qobuz_view_;
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef HAVE_DROPBOX
|
||||||
|
StreamingSongsView *dropbox_view_;
|
||||||
|
#endif
|
||||||
|
|
||||||
RadioViewContainer *radio_view_;
|
RadioViewContainer *radio_view_;
|
||||||
|
|
||||||
|
|||||||
@@ -1163,6 +1163,8 @@ QString Song::TextForSource(const Source source) {
|
|||||||
case Source::Qobuz: return u"qobuz"_s;
|
case Source::Qobuz: return u"qobuz"_s;
|
||||||
case Source::SomaFM: return u"somafm"_s;
|
case Source::SomaFM: return u"somafm"_s;
|
||||||
case Source::RadioParadise: return u"radioparadise"_s;
|
case Source::RadioParadise: return u"radioparadise"_s;
|
||||||
|
case Source::Dropbox: return u"dropbox"_s;
|
||||||
|
case Source::OneDrive: return u"onedrive"_s;
|
||||||
case Source::Unknown: return u"unknown"_s;
|
case Source::Unknown: return u"unknown"_s;
|
||||||
}
|
}
|
||||||
return u"unknown"_s;
|
return u"unknown"_s;
|
||||||
@@ -1183,6 +1185,8 @@ QString Song::DescriptionForSource(const Source source) {
|
|||||||
case Source::Qobuz: return u"Qobuz"_s;
|
case Source::Qobuz: return u"Qobuz"_s;
|
||||||
case Source::SomaFM: return u"SomaFM"_s;
|
case Source::SomaFM: return u"SomaFM"_s;
|
||||||
case Source::RadioParadise: return u"Radio Paradise"_s;
|
case Source::RadioParadise: return u"Radio Paradise"_s;
|
||||||
|
case Source::Dropbox: return u"Dropbox"_s;
|
||||||
|
case Source::OneDrive: return u"OneDrive"_s;
|
||||||
case Source::Unknown: return u"Unknown"_s;
|
case Source::Unknown: return u"Unknown"_s;
|
||||||
}
|
}
|
||||||
return u"unknown"_s;
|
return u"unknown"_s;
|
||||||
@@ -1202,6 +1206,8 @@ Song::Source Song::SourceFromText(const QString &source) {
|
|||||||
if (source.compare("qobuz"_L1, Qt::CaseInsensitive) == 0) return Source::Qobuz;
|
if (source.compare("qobuz"_L1, Qt::CaseInsensitive) == 0) return Source::Qobuz;
|
||||||
if (source.compare("somafm"_L1, Qt::CaseInsensitive) == 0) return Source::SomaFM;
|
if (source.compare("somafm"_L1, Qt::CaseInsensitive) == 0) return Source::SomaFM;
|
||||||
if (source.compare("radioparadise"_L1, Qt::CaseInsensitive) == 0) return Source::RadioParadise;
|
if (source.compare("radioparadise"_L1, Qt::CaseInsensitive) == 0) return Source::RadioParadise;
|
||||||
|
if (source.compare("dropbox"_L1, Qt::CaseInsensitive) == 0) return Source::Dropbox;
|
||||||
|
if (source.compare("onedrive"_L1, Qt::CaseInsensitive) == 0) return Source::OneDrive;
|
||||||
|
|
||||||
return Source::Unknown;
|
return Source::Unknown;
|
||||||
|
|
||||||
@@ -1221,6 +1227,8 @@ QIcon Song::IconForSource(const Source source) {
|
|||||||
case Source::Qobuz: return IconLoader::Load(u"qobuz"_s);
|
case Source::Qobuz: return IconLoader::Load(u"qobuz"_s);
|
||||||
case Source::SomaFM: return IconLoader::Load(u"somafm"_s);
|
case Source::SomaFM: return IconLoader::Load(u"somafm"_s);
|
||||||
case Source::RadioParadise: return IconLoader::Load(u"radioparadise"_s);
|
case Source::RadioParadise: return IconLoader::Load(u"radioparadise"_s);
|
||||||
|
case Source::Dropbox: return IconLoader::Load(u"dropbox"_s);
|
||||||
|
case Source::OneDrive: return IconLoader::Load(u"onedrive"_s);
|
||||||
case Source::Unknown: return IconLoader::Load(u"edit-delete"_s);
|
case Source::Unknown: return IconLoader::Load(u"edit-delete"_s);
|
||||||
}
|
}
|
||||||
return IconLoader::Load(u"edit-delete"_s);
|
return IconLoader::Load(u"edit-delete"_s);
|
||||||
@@ -1470,7 +1478,7 @@ Song::FileType Song::FiletypeByExtension(const QString &ext) {
|
|||||||
|
|
||||||
bool Song::IsLinkedCollectionSource(const Source source) {
|
bool Song::IsLinkedCollectionSource(const Source source) {
|
||||||
|
|
||||||
return source == Source::Collection;
|
return source == Source::Collection || source == Source::Dropbox;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1489,11 +1497,14 @@ QString Song::ImageCacheDir(const Source source) {
|
|||||||
return StandardPaths::WritableLocation(StandardPaths::StandardLocation::AppLocalDataLocation) + u"/qobuzalbumcovers"_s;
|
return StandardPaths::WritableLocation(StandardPaths::StandardLocation::AppLocalDataLocation) + u"/qobuzalbumcovers"_s;
|
||||||
case Source::Device:
|
case Source::Device:
|
||||||
return StandardPaths::WritableLocation(StandardPaths::StandardLocation::AppLocalDataLocation) + u"/devicealbumcovers"_s;
|
return StandardPaths::WritableLocation(StandardPaths::StandardLocation::AppLocalDataLocation) + u"/devicealbumcovers"_s;
|
||||||
|
case Source::Dropbox:
|
||||||
|
return StandardPaths::WritableLocation(StandardPaths::StandardLocation::AppLocalDataLocation) + u"/dropboxalbumcovers"_s;
|
||||||
case Source::LocalFile:
|
case Source::LocalFile:
|
||||||
case Source::CDDA:
|
case Source::CDDA:
|
||||||
case Source::Stream:
|
case Source::Stream:
|
||||||
case Source::SomaFM:
|
case Source::SomaFM:
|
||||||
case Source::RadioParadise:
|
case Source::RadioParadise:
|
||||||
|
case Source::OneDrive:
|
||||||
case Source::Unknown:
|
case Source::Unknown:
|
||||||
return StandardPaths::WritableLocation(StandardPaths::StandardLocation::AppLocalDataLocation) + u"/albumcovers"_s;
|
return StandardPaths::WritableLocation(StandardPaths::StandardLocation::AppLocalDataLocation) + u"/albumcovers"_s;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,7 +76,9 @@ class Song {
|
|||||||
Qobuz = 8,
|
Qobuz = 8,
|
||||||
SomaFM = 9,
|
SomaFM = 9,
|
||||||
RadioParadise = 10,
|
RadioParadise = 10,
|
||||||
Spotify = 11
|
Spotify = 11,
|
||||||
|
Dropbox = 12,
|
||||||
|
OneDrive = 13,
|
||||||
};
|
};
|
||||||
static const int kSourceCount = 16;
|
static const int kSourceCount = 16;
|
||||||
|
|
||||||
|
|||||||
@@ -589,6 +589,8 @@ void AlbumCoverChoiceController::SaveArtManualToSong(Song *song, const QUrl &art
|
|||||||
case Song::Source::Tidal:
|
case Song::Source::Tidal:
|
||||||
case Song::Source::Spotify:
|
case Song::Source::Spotify:
|
||||||
case Song::Source::Qobuz:
|
case Song::Source::Qobuz:
|
||||||
|
case Song::Source::Dropbox:
|
||||||
|
case Song::Source::OneDrive:
|
||||||
StreamingServicePtr service = streaming_services_->ServiceBySource(song->source());
|
StreamingServicePtr service = streaming_services_->ServiceBySource(song->source());
|
||||||
if (!service) break;
|
if (!service) break;
|
||||||
if (service->artists_collection_backend()) {
|
if (service->artists_collection_backend()) {
|
||||||
|
|||||||
132
src/dropbox/dropboxbaserequest.cpp
Normal file
132
src/dropbox/dropboxbaserequest.cpp
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
/*
|
||||||
|
* 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 "config.h"
|
||||||
|
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QString>
|
||||||
|
#include <QUrl>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
#include <QJsonObject>
|
||||||
|
|
||||||
|
#include "constants/dropboxconstants.h"
|
||||||
|
#include "core/networkaccessmanager.h"
|
||||||
|
#include "dropboxservice.h"
|
||||||
|
#include "dropboxbaserequest.h"
|
||||||
|
|
||||||
|
using namespace Qt::Literals::StringLiterals;
|
||||||
|
using namespace DropboxConstants;
|
||||||
|
|
||||||
|
DropboxBaseRequest::DropboxBaseRequest(const SharedPtr<NetworkAccessManager> network, DropboxService *service, QObject *parent)
|
||||||
|
: JsonBaseRequest(network, parent),
|
||||||
|
service_(service) {}
|
||||||
|
|
||||||
|
QString DropboxBaseRequest::service_name() const {
|
||||||
|
|
||||||
|
return service_->name();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DropboxBaseRequest::authentication_required() const {
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DropboxBaseRequest::authenticated() const {
|
||||||
|
|
||||||
|
return service_->authenticated();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DropboxBaseRequest::use_authorization_header() const {
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray DropboxBaseRequest::authorization_header() const {
|
||||||
|
|
||||||
|
return service_->authorization_header();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
QNetworkReply *DropboxBaseRequest::GetTemporaryLink(const QUrl &url) {
|
||||||
|
|
||||||
|
QJsonObject json_object;
|
||||||
|
json_object.insert("path"_L1, url.path());
|
||||||
|
return CreatePostRequest(QUrl(QLatin1String(kApiUrl) + "/2/files/get_temporary_link"_L1), json_object);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonBaseRequest::JsonObjectResult DropboxBaseRequest::ParseJsonObject(QNetworkReply *reply) {
|
||||||
|
|
||||||
|
if (reply->error() != QNetworkReply::NoError && reply->error() < 200) {
|
||||||
|
return ReplyDataResult(ErrorCode::NetworkError, QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error()));
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonObjectResult result(ErrorCode::Success);
|
||||||
|
result.network_error = reply->error();
|
||||||
|
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).isValid()) {
|
||||||
|
result.http_status_code = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||||
|
}
|
||||||
|
|
||||||
|
const QByteArray data = reply->readAll();
|
||||||
|
if (!data.isEmpty()) {
|
||||||
|
QJsonParseError json_parse_error;
|
||||||
|
const QJsonDocument json_document = QJsonDocument::fromJson(data, &json_parse_error);
|
||||||
|
if (json_parse_error.error == QJsonParseError::NoError) {
|
||||||
|
const QJsonObject json_object = json_document.object();
|
||||||
|
if (json_object.contains("error"_L1) && json_object["error"_L1].isObject()) {
|
||||||
|
const QJsonObject object_error = json_object["error"_L1].toObject();
|
||||||
|
if (object_error.contains("status"_L1) && object_error.contains("message"_L1)) {
|
||||||
|
const int status = object_error["status"_L1].toInt();
|
||||||
|
const QString message = object_error["message"_L1].toString();
|
||||||
|
result.error_code = ErrorCode::APIError;
|
||||||
|
result.error_message = QStringLiteral("%1 (%2)").arg(message).arg(status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
result.json_object = json_document.object();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
result.error_code = ErrorCode::ParseError;
|
||||||
|
result.error_message = json_parse_error.errorString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.error_code != ErrorCode::APIError) {
|
||||||
|
if (reply->error() != QNetworkReply::NoError) {
|
||||||
|
result.error_code = ErrorCode::NetworkError;
|
||||||
|
result.error_message = QStringLiteral("%1 (%2)").arg(reply->errorString()).arg(reply->error());
|
||||||
|
}
|
||||||
|
else if (result.http_status_code != 200) {
|
||||||
|
result.error_code = ErrorCode::HttpError;
|
||||||
|
result.error_message = QStringLiteral("Received HTTP code %1").arg(result.http_status_code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reply->error() == QNetworkReply::AuthenticationRequiredError) {
|
||||||
|
service_->ClearSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
|
||||||
|
}
|
||||||
59
src/dropbox/dropboxbaserequest.h
Normal file
59
src/dropbox/dropboxbaserequest.h
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
/*
|
||||||
|
* 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 DROPBOXBASEREQUEST_H
|
||||||
|
#define DROPBOXBASEREQUEST_H
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QString>
|
||||||
|
#include <QUrl>
|
||||||
|
|
||||||
|
#include "includes/shared_ptr.h"
|
||||||
|
#include "core/jsonbaserequest.h"
|
||||||
|
|
||||||
|
class QNetworkReply;
|
||||||
|
class NetworkAccessManager;
|
||||||
|
class DropboxService;
|
||||||
|
|
||||||
|
class DropboxBaseRequest : public JsonBaseRequest {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit DropboxBaseRequest(const SharedPtr<NetworkAccessManager> network, DropboxService *service, QObject *parent = nullptr);
|
||||||
|
|
||||||
|
QString service_name() const override;
|
||||||
|
bool authentication_required() const override;
|
||||||
|
bool authenticated() const override;
|
||||||
|
bool use_authorization_header() const override;
|
||||||
|
QByteArray authorization_header() const override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
QNetworkReply *GetTemporaryLink(const QUrl &url);
|
||||||
|
JsonObjectResult ParseJsonObject(QNetworkReply *reply);
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void ShowErrorDialog(const QString &error);
|
||||||
|
|
||||||
|
private:
|
||||||
|
DropboxService *service_;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // DROPBOXBASEREQUEST_H
|
||||||
190
src/dropbox/dropboxservice.cpp
Normal file
190
src/dropbox/dropboxservice.cpp
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
/*
|
||||||
|
* 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 <QString>
|
||||||
|
#include <QUrl>
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
|
#include "constants/dropboxsettings.h"
|
||||||
|
#include "core/logging.h"
|
||||||
|
#include "core/settings.h"
|
||||||
|
#include "core/database.h"
|
||||||
|
#include "core/urlhandlers.h"
|
||||||
|
#include "core/networkaccessmanager.h"
|
||||||
|
#include "core/oauthenticator.h"
|
||||||
|
#include "collection/collectionbackend.h"
|
||||||
|
#include "collection/collectionmodel.h"
|
||||||
|
#include "streaming/cloudstoragestreamingservice.h"
|
||||||
|
#include "dropboxservice.h"
|
||||||
|
#include "dropboxurlhandler.h"
|
||||||
|
#include "dropboxsongsrequest.h"
|
||||||
|
#include "dropboxstreamurlrequest.h"
|
||||||
|
|
||||||
|
using namespace Qt::Literals::StringLiterals;
|
||||||
|
using namespace DropboxSettings;
|
||||||
|
|
||||||
|
const Song::Source DropboxService::kSource = Song::Source::Dropbox;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr char kClientIDB64[] = "Zmx0b2EyYzRwaGo2eHlw";
|
||||||
|
constexpr char kClientSecretB64[] = "emo3em5jNnNpM3Ftd2s3";
|
||||||
|
constexpr char kOAuthRedirectUrl[] = "http://localhost/";
|
||||||
|
constexpr char kOAuthAuthorizeUrl[] = "https://www.dropbox.com/1/oauth2/authorize";
|
||||||
|
constexpr char kOAuthAccessTokenUrl[] = "https://api.dropboxapi.com/1/oauth2/token";
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
DropboxService::DropboxService(const SharedPtr<TaskManager> task_manager,
|
||||||
|
const SharedPtr<Database> database,
|
||||||
|
const SharedPtr<NetworkAccessManager> network,
|
||||||
|
const SharedPtr<UrlHandlers> url_handlers,
|
||||||
|
const SharedPtr<TagReaderClient> tagreader_client,
|
||||||
|
const SharedPtr<AlbumCoverLoader> albumcover_loader,
|
||||||
|
QObject *parent)
|
||||||
|
: CloudStorageStreamingService(task_manager, database, tagreader_client, albumcover_loader, Song::Source::Dropbox, u"Dropbox"_s, u"dropbox"_s, QLatin1String(kSettingsGroup), parent),
|
||||||
|
network_(network),
|
||||||
|
oauth_(new OAuthenticator(network, this)),
|
||||||
|
songs_request_(new DropboxSongsRequest(network, collection_backend_, this, this)),
|
||||||
|
enabled_(false),
|
||||||
|
next_stream_url_request_id_(0) {
|
||||||
|
|
||||||
|
url_handlers->Register(new DropboxUrlHandler(task_manager, this, this));
|
||||||
|
|
||||||
|
oauth_->set_settings_group(QLatin1String(kSettingsGroup));
|
||||||
|
oauth_->set_type(OAuthenticator::Type::Authorization_Code);
|
||||||
|
oauth_->set_authorize_url(QUrl(QLatin1String(kOAuthAuthorizeUrl)));
|
||||||
|
oauth_->set_redirect_url(QUrl(QLatin1String(kOAuthRedirectUrl)));
|
||||||
|
oauth_->set_access_token_url(QUrl(QLatin1String(kOAuthAccessTokenUrl)));
|
||||||
|
oauth_->set_client_id(QString::fromLatin1(QByteArray::fromBase64(kClientIDB64)));
|
||||||
|
oauth_->set_client_secret(QString::fromLatin1(QByteArray::fromBase64(kClientSecretB64)));
|
||||||
|
oauth_->set_use_local_redirect_server(true);
|
||||||
|
oauth_->set_random_port(true);
|
||||||
|
|
||||||
|
QObject::connect(oauth_, &OAuthenticator::AuthenticationFinished, this, &DropboxService::OAuthFinished);
|
||||||
|
|
||||||
|
DropboxService::ReloadSettings();
|
||||||
|
oauth_->LoadSession();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DropboxService::authenticated() const {
|
||||||
|
|
||||||
|
return oauth_->authenticated();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void DropboxService::Exit() {
|
||||||
|
|
||||||
|
wait_for_exit_ << &*collection_backend_;
|
||||||
|
QObject::connect(&*collection_backend_, &CollectionBackend::ExitFinished, this, &DropboxService::ExitReceived);
|
||||||
|
collection_backend_->ExitAsync();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void DropboxService::ExitReceived() {
|
||||||
|
|
||||||
|
QObject *obj = sender();
|
||||||
|
QObject::disconnect(obj, nullptr, this, nullptr);
|
||||||
|
qLog(Debug) << obj << "successfully exited.";
|
||||||
|
wait_for_exit_.removeAll(obj);
|
||||||
|
if (wait_for_exit_.isEmpty()) Q_EMIT ExitFinished();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void DropboxService::ReloadSettings() {
|
||||||
|
|
||||||
|
Settings s;
|
||||||
|
s.beginGroup(kSettingsGroup);
|
||||||
|
enabled_ = s.value(kEnabled, false).toBool();
|
||||||
|
s.endGroup();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void DropboxService::Authenticate() {
|
||||||
|
|
||||||
|
oauth_->Authenticate();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void DropboxService::ClearSession() {
|
||||||
|
|
||||||
|
oauth_->ClearSession();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DropboxService::OAuthFinished(const bool success, const QString &error) {
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
Q_EMIT LoginFinished(true);
|
||||||
|
Q_EMIT LoginSuccess();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Q_EMIT LoginFailure(error);
|
||||||
|
Q_EMIT LoginFinished(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray DropboxService::authorization_header() const {
|
||||||
|
return oauth_->authorization_header();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DropboxService::Start() {
|
||||||
|
songs_request_->GetFolderList();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DropboxService::Reset() {
|
||||||
|
|
||||||
|
collection_backend_->DeleteAll();
|
||||||
|
|
||||||
|
Settings s;
|
||||||
|
s.beginGroup(kSettingsGroup);
|
||||||
|
s.remove("cursor");
|
||||||
|
s.endGroup();
|
||||||
|
|
||||||
|
if (authenticated()) {
|
||||||
|
Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
uint DropboxService::GetStreamURL(const QUrl &url, QString &error) {
|
||||||
|
|
||||||
|
if (!authenticated()) {
|
||||||
|
error = tr("Not authenticated with Dropbox.");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint id = 0;
|
||||||
|
while (id == 0) id = ++next_stream_url_request_id_;
|
||||||
|
DropboxStreamURLRequestPtr stream_url_request = DropboxStreamURLRequestPtr(new DropboxStreamURLRequest(network_, this, id, url));
|
||||||
|
stream_url_requests_.insert(id, stream_url_request);
|
||||||
|
QObject::connect(&*stream_url_request, &DropboxStreamURLRequest::StreamURLRequestFinished, this, &DropboxService::StreamURLRequestFinishedSlot);
|
||||||
|
stream_url_request->Process();
|
||||||
|
|
||||||
|
return id;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void DropboxService::StreamURLRequestFinishedSlot(const uint id, const QUrl &media_url, const bool success, const QUrl &stream_url, const QString &error) {
|
||||||
|
|
||||||
|
if (!stream_url_requests_.contains(id)) return;
|
||||||
|
DropboxStreamURLRequestPtr stream_url_request = stream_url_requests_.take(id);
|
||||||
|
|
||||||
|
Q_EMIT StreamURLRequestFinished(id, media_url, success, stream_url, error);
|
||||||
|
|
||||||
|
}
|
||||||
93
src/dropbox/dropboxservice.h
Normal file
93
src/dropbox/dropboxservice.h
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
/*
|
||||||
|
* 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 DROPBOXSERVICE_H
|
||||||
|
#define DROPBOXSERVICE_H
|
||||||
|
|
||||||
|
#include <QList>
|
||||||
|
#include <QString>
|
||||||
|
#include <QUrl>
|
||||||
|
#include <QSharedPointer>
|
||||||
|
|
||||||
|
#include "core/song.h"
|
||||||
|
#include "streaming/cloudstoragestreamingservice.h"
|
||||||
|
|
||||||
|
class QNetworkReply;
|
||||||
|
|
||||||
|
class TaskManager;
|
||||||
|
class Database;
|
||||||
|
class NetworkAccessManager;
|
||||||
|
class UrlHandlers;
|
||||||
|
class TagReaderClient;
|
||||||
|
class AlbumCoverLoader;
|
||||||
|
class OAuthenticator;
|
||||||
|
class DropboxSongsRequest;
|
||||||
|
class DropboxStreamURLRequest;
|
||||||
|
|
||||||
|
class DropboxService : public CloudStorageStreamingService {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit DropboxService(const SharedPtr<TaskManager> task_manager,
|
||||||
|
const SharedPtr<Database> database,
|
||||||
|
const SharedPtr<NetworkAccessManager> network,
|
||||||
|
const SharedPtr<UrlHandlers> url_handlers,
|
||||||
|
const SharedPtr<TagReaderClient> tagreader_client,
|
||||||
|
const SharedPtr<AlbumCoverLoader> albumcover_loader,
|
||||||
|
QObject *parent = nullptr);
|
||||||
|
|
||||||
|
static const Song::Source kSource;
|
||||||
|
|
||||||
|
bool oauth() const override { return true; }
|
||||||
|
bool authenticated() const override;
|
||||||
|
bool show_progress() const override { return false; }
|
||||||
|
bool enable_refresh_button() const override { return false; }
|
||||||
|
|
||||||
|
void Exit() override;
|
||||||
|
void ReloadSettings() override;
|
||||||
|
|
||||||
|
void Authenticate();
|
||||||
|
void ClearSession();
|
||||||
|
|
||||||
|
void Start();
|
||||||
|
void Reset();
|
||||||
|
uint GetStreamURL(const QUrl &url, QString &error);
|
||||||
|
|
||||||
|
QByteArray authorization_header() const;
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void StreamURLRequestFinished(const uint id, const QUrl &media_url, const bool success, const QUrl &stream_url, const QString &error = QString());
|
||||||
|
|
||||||
|
private Q_SLOTS:
|
||||||
|
void ExitReceived();
|
||||||
|
void OAuthFinished(const bool success, const QString &error = QString());
|
||||||
|
void StreamURLRequestFinishedSlot(const uint id, const QUrl &media_url, const bool success, const QUrl &stream_url, const QString &error = QString());
|
||||||
|
|
||||||
|
private:
|
||||||
|
const SharedPtr<NetworkAccessManager> network_;
|
||||||
|
OAuthenticator *oauth_;
|
||||||
|
DropboxSongsRequest *songs_request_;
|
||||||
|
bool enabled_;
|
||||||
|
QList<QObject*> wait_for_exit_;
|
||||||
|
bool finished_;
|
||||||
|
uint next_stream_url_request_id_;
|
||||||
|
QMap<uint, QSharedPointer<DropboxStreamURLRequest>> stream_url_requests_;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // DROPBOXSERVICE_H
|
||||||
244
src/dropbox/dropboxsongsrequest.cpp
Normal file
244
src/dropbox/dropboxsongsrequest.cpp
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
/*
|
||||||
|
* 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 "config.h"
|
||||||
|
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QString>
|
||||||
|
#include <QUrl>
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QTimer>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonValue>
|
||||||
|
|
||||||
|
#include "constants/dropboxsettings.h"
|
||||||
|
#include "core/logging.h"
|
||||||
|
#include "core/settings.h"
|
||||||
|
#include "core/networkaccessmanager.h"
|
||||||
|
#include "collection/collectionbackend.h"
|
||||||
|
#include "dropboxservice.h"
|
||||||
|
#include "dropboxbaserequest.h"
|
||||||
|
#include "dropboxsongsrequest.h"
|
||||||
|
|
||||||
|
using namespace Qt::Literals::StringLiterals;
|
||||||
|
using namespace DropboxSettings;
|
||||||
|
|
||||||
|
DropboxSongsRequest::DropboxSongsRequest(const SharedPtr<NetworkAccessManager> network, const SharedPtr<CollectionBackend> collection_backend, DropboxService *service, QObject *parent)
|
||||||
|
: DropboxBaseRequest(network, service, parent),
|
||||||
|
network_(network),
|
||||||
|
collection_backend_(collection_backend),
|
||||||
|
service_(service) {}
|
||||||
|
|
||||||
|
void DropboxSongsRequest::GetFolderList() {
|
||||||
|
|
||||||
|
Settings s;
|
||||||
|
s.beginGroup(kSettingsGroup);
|
||||||
|
QString cursor = s.value("cursor").toString();
|
||||||
|
s.endGroup();
|
||||||
|
|
||||||
|
QUrl url(QLatin1String(kApiUrl) + "/2/files/list_folder"_L1);
|
||||||
|
QJsonObject json_object;
|
||||||
|
|
||||||
|
if (cursor.isEmpty()) {
|
||||||
|
json_object.insert("path"_L1, ""_L1);
|
||||||
|
json_object.insert("recursive"_L1, true);
|
||||||
|
json_object.insert("include_deleted"_L1, true);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
url.setUrl(QLatin1String(kApiUrl) + "/2/files/list_folder/continue"_L1);
|
||||||
|
json_object.insert("cursor"_L1, cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
QNetworkReply *reply = CreatePostRequest(url, json_object);
|
||||||
|
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply] { GetFolderListFinished(reply); });
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void DropboxSongsRequest::GetFolderListFinished(QNetworkReply *reply) {
|
||||||
|
|
||||||
|
reply->deleteLater();
|
||||||
|
|
||||||
|
const JsonObjectResult json_object_result = ParseJsonObject(reply);
|
||||||
|
if (json_object_result.success()) {
|
||||||
|
Error(json_object_result.error_message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QJsonObject &json_object = json_object_result.json_object;
|
||||||
|
if (json_object.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json_object.contains("reset"_L1) && json_object["reset"_L1].toBool()) {
|
||||||
|
qLog(Debug) << "Resetting Dropbox database";
|
||||||
|
collection_backend_->DeleteAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
Settings s;
|
||||||
|
s.beginGroup(kSettingsGroup);
|
||||||
|
s.setValue("cursor", json_object["cursor"_L1].toString());
|
||||||
|
s.endGroup();
|
||||||
|
}
|
||||||
|
|
||||||
|
const QJsonArray entires = json_object["entries"_L1].toArray();
|
||||||
|
qLog(Debug) << "File list found:" << entires.size();
|
||||||
|
|
||||||
|
QList<QUrl> urls_deleted;
|
||||||
|
for (const QJsonValue &value_entry : entires) {
|
||||||
|
if (!value_entry.isObject()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const QJsonObject object_entry = value_entry.toObject();
|
||||||
|
const QString tag = object_entry[".tag"_L1].toString();
|
||||||
|
const QString path = object_entry["path_lower"_L1].toString();
|
||||||
|
const qint64 size = object_entry["size"_L1].toInt();
|
||||||
|
const QString server_modified = object_entry["server_modified"_L1].toString();
|
||||||
|
|
||||||
|
QUrl url;
|
||||||
|
url.setScheme(service_->url_scheme());
|
||||||
|
url.setPath(path);
|
||||||
|
|
||||||
|
if (tag == "deleted"_L1) {
|
||||||
|
qLog(Debug) << "Deleting song with URL" << url;
|
||||||
|
urls_deleted << url;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tag == "folder"_L1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DropboxService::IsSupportedFiletype(path)) {
|
||||||
|
GetStreamURL(url, path, size, QDateTime::fromString(server_modified, Qt::ISODate).toSecsSinceEpoch());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!urls_deleted.isEmpty()) {
|
||||||
|
collection_backend_->DeleteSongsByUrlsAsync(urls_deleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (json_object.contains("has_more"_L1) && json_object["has_more"_L1].isBool() && json_object["has_more"_L1].toBool()) {
|
||||||
|
Settings s;
|
||||||
|
s.beginGroup(kSettingsGroup);
|
||||||
|
s.setValue("cursor", json_object["cursor"_L1].toVariant());
|
||||||
|
s.endGroup();
|
||||||
|
GetFolderList();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Long-poll wait for changes.
|
||||||
|
LongPollDelta();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void DropboxSongsRequest::LongPollDelta() {
|
||||||
|
|
||||||
|
if (!service_->authenticated()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Settings s;
|
||||||
|
s.beginGroup(kSettingsGroup);
|
||||||
|
const QString cursor = s.value("cursor").toString();
|
||||||
|
s.endGroup();
|
||||||
|
|
||||||
|
QJsonObject json_object;
|
||||||
|
json_object.insert("cursor"_L1, cursor);
|
||||||
|
json_object.insert("timeout"_L1, 30);
|
||||||
|
|
||||||
|
QNetworkReply *reply = CreatePostRequest(QUrl(QLatin1String(kNotifyApiUrl) + "/2/files/list_folder/longpoll"_L1), json_object);
|
||||||
|
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply] { LongPollDeltaFinished(reply); });
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void DropboxSongsRequest::LongPollDeltaFinished(QNetworkReply *reply) {
|
||||||
|
|
||||||
|
reply->deleteLater();
|
||||||
|
|
||||||
|
const JsonObjectResult json_object_result = ParseJsonObject(reply);
|
||||||
|
if (json_object_result.success()) {
|
||||||
|
Error(json_object_result.error_message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QJsonObject &json_object = json_object_result.json_object;
|
||||||
|
if (json_object["changes"_L1].toBool()) {
|
||||||
|
qLog(Debug) << "Dropbox: Received changes...";
|
||||||
|
GetFolderList();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
bool ok = false;
|
||||||
|
int backoff = json_object["backoff"_L1].toString().toInt(&ok);
|
||||||
|
if (!ok) {
|
||||||
|
backoff = 10;
|
||||||
|
}
|
||||||
|
QTimer::singleShot(backoff * 1000, this, &DropboxSongsRequest::LongPollDelta);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void DropboxSongsRequest::GetStreamURL(const QUrl &url, const QString &path, const qint64 size, const qint64 mtime) {
|
||||||
|
|
||||||
|
QNetworkReply *reply = GetTemporaryLink(url);
|
||||||
|
QObject::connect(reply, &QNetworkReply::finished, this, [this, reply, path, size, mtime]() {
|
||||||
|
GetStreamUrlFinished(reply, path, size, mtime);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void DropboxSongsRequest::GetStreamUrlFinished(QNetworkReply *reply, const QString &filename, const qint64 size, const qint64 mtime) {
|
||||||
|
|
||||||
|
reply->deleteLater();
|
||||||
|
|
||||||
|
const JsonObjectResult json_object_result = ParseJsonObject(reply);
|
||||||
|
if (!json_object_result.success()) {
|
||||||
|
Error(json_object_result.error_message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QJsonObject &json_object = json_object_result.json_object;
|
||||||
|
if (json_object.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!json_object.contains("link"_L1)) {
|
||||||
|
Error(u"Missing link"_s);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QUrl url = QUrl::fromEncoded(json_object["link"_L1].toVariant().toByteArray());
|
||||||
|
|
||||||
|
service_->MaybeAddFileToDatabase(url, filename, size, mtime);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void DropboxSongsRequest::Error(const QString &error_message, const QVariant &debug_output) {
|
||||||
|
|
||||||
|
qLog(Error) << service_name() << error_message;
|
||||||
|
if (debug_output.isValid()) {
|
||||||
|
qLog(Debug) << debug_output;
|
||||||
|
}
|
||||||
|
|
||||||
|
Q_EMIT ShowErrorDialog(error_message);
|
||||||
|
|
||||||
|
}
|
||||||
67
src/dropbox/dropboxsongsrequest.h
Normal file
67
src/dropbox/dropboxsongsrequest.h
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
* 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 DROPBOXSONGSREQUEST_H
|
||||||
|
#define DROPBOXSONGSREQUEST_H
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
#include <QList>
|
||||||
|
#include <QString>
|
||||||
|
#include <QUrl>
|
||||||
|
|
||||||
|
#include "dropboxbaserequest.h"
|
||||||
|
|
||||||
|
class NetworkAccessManager;
|
||||||
|
class CollectionBackend;
|
||||||
|
class QNetworkReply;
|
||||||
|
class DropboxService;
|
||||||
|
|
||||||
|
class DropboxSongsRequest : public DropboxBaseRequest {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit DropboxSongsRequest(const SharedPtr<NetworkAccessManager> network, const SharedPtr<CollectionBackend> collection_backend, DropboxService *service, QObject *parent = nullptr);
|
||||||
|
|
||||||
|
void ReloadSettings();
|
||||||
|
|
||||||
|
void GetFolderList();
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void ShowErrorDialog(const QString &error);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void LongPollDelta();
|
||||||
|
void GetStreamURL(const QUrl &url, const QString &path, const qint64 size, const qint64 mtime);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void Error(const QString &error_message, const QVariant &debug_output = QVariant()) override;
|
||||||
|
|
||||||
|
private Q_SLOTS:
|
||||||
|
void GetFolderListFinished(QNetworkReply *reply);
|
||||||
|
void LongPollDeltaFinished(QNetworkReply *reply);
|
||||||
|
void GetStreamUrlFinished(QNetworkReply *reply, const QString &filename, const qint64 size, const qint64 mtime);
|
||||||
|
|
||||||
|
private:
|
||||||
|
const SharedPtr<NetworkAccessManager> network_;
|
||||||
|
const SharedPtr<CollectionBackend> collection_backend_;
|
||||||
|
DropboxService *service_;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // DROPBOXSONGSREQUEST_H
|
||||||
129
src/dropbox/dropboxstreamurlrequest.cpp
Normal file
129
src/dropbox/dropboxstreamurlrequest.cpp
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
/*
|
||||||
|
* 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 "config.h"
|
||||||
|
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QString>
|
||||||
|
#include <QUrl>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
#include <QJsonObject>
|
||||||
|
|
||||||
|
#include "includes/shared_ptr.h"
|
||||||
|
#include "core/logging.h"
|
||||||
|
#include "core/networkaccessmanager.h"
|
||||||
|
#include "dropboxservice.h"
|
||||||
|
#include "dropboxbaserequest.h"
|
||||||
|
#include "dropboxstreamurlrequest.h"
|
||||||
|
|
||||||
|
using namespace Qt::Literals::StringLiterals;
|
||||||
|
|
||||||
|
DropboxStreamURLRequest::DropboxStreamURLRequest(const SharedPtr<NetworkAccessManager> network, DropboxService *service, const uint id, const QUrl &media_url, QObject *parent)
|
||||||
|
: DropboxBaseRequest(network, service, parent),
|
||||||
|
network_(network),
|
||||||
|
service_(service),
|
||||||
|
id_(id),
|
||||||
|
media_url_(media_url),
|
||||||
|
reply_(nullptr) {}
|
||||||
|
|
||||||
|
DropboxStreamURLRequest::~DropboxStreamURLRequest() {
|
||||||
|
|
||||||
|
if (reply_) {
|
||||||
|
QObject::disconnect(reply_, nullptr, this, nullptr);
|
||||||
|
if (reply_->isRunning()) reply_->abort();
|
||||||
|
reply_->deleteLater();
|
||||||
|
reply_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void DropboxStreamURLRequest::Cancel() {
|
||||||
|
|
||||||
|
if (reply_ && reply_->isRunning()) {
|
||||||
|
reply_->abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void DropboxStreamURLRequest::Process() {
|
||||||
|
|
||||||
|
GetStreamURL();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void DropboxStreamURLRequest::GetStreamURL() {
|
||||||
|
|
||||||
|
if (reply_) {
|
||||||
|
QObject::disconnect(reply_, nullptr, this, nullptr);
|
||||||
|
if (reply_->isRunning()) reply_->abort();
|
||||||
|
reply_->deleteLater();
|
||||||
|
}
|
||||||
|
|
||||||
|
reply_ = GetTemporaryLink(media_url_);
|
||||||
|
QObject::connect(reply_, &QNetworkReply::finished, this, &DropboxStreamURLRequest::StreamURLReceived);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void DropboxStreamURLRequest::StreamURLReceived() {
|
||||||
|
|
||||||
|
const QScopeGuard finish = qScopeGuard([this]() { Finish(); });
|
||||||
|
|
||||||
|
if (!reply_) return;
|
||||||
|
|
||||||
|
Q_ASSERT(replies_.contains(reply_));
|
||||||
|
replies_.removeAll(reply_);
|
||||||
|
|
||||||
|
const JsonObjectResult json_object_result = ParseJsonObject(reply_).json_object;
|
||||||
|
|
||||||
|
QObject::disconnect(reply_, nullptr, this, nullptr);
|
||||||
|
reply_->deleteLater();
|
||||||
|
reply_ = nullptr;
|
||||||
|
|
||||||
|
if (!json_object_result.success()) {
|
||||||
|
Error(json_object_result.error_message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QJsonObject &json_object = json_object_result.json_object;
|
||||||
|
if (json_object.isEmpty() || !json_object.contains("link"_L1)) {
|
||||||
|
Error(u"Could not parse stream URL"_s);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
stream_url_ = QUrl::fromEncoded(json_object["link"_L1].toVariant().toByteArray());
|
||||||
|
success_ = stream_url_.isValid();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void DropboxStreamURLRequest::Error(const QString &error_message, const QVariant &debug_output) {
|
||||||
|
|
||||||
|
qLog(Error) << service_name() << error_message;
|
||||||
|
if (debug_output.isValid()) {
|
||||||
|
qLog(Debug) << debug_output;
|
||||||
|
}
|
||||||
|
|
||||||
|
error_ = error_message;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void DropboxStreamURLRequest::Finish() {
|
||||||
|
|
||||||
|
Q_EMIT StreamURLRequestFinished(id_, media_url_, success_, stream_url_, error_);
|
||||||
|
|
||||||
|
}
|
||||||
71
src/dropbox/dropboxstreamurlrequest.h
Normal file
71
src/dropbox/dropboxstreamurlrequest.h
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
/*
|
||||||
|
* 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 DROPBOXSTREAMURLREQUEST_H
|
||||||
|
#define DROPBOXSTREAMURLREQUEST_H
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
#include <QVariant>
|
||||||
|
#include <QString>
|
||||||
|
#include <QUrl>
|
||||||
|
#include <QSharedPointer>
|
||||||
|
|
||||||
|
#include "includes/shared_ptr.h"
|
||||||
|
#include "dropboxservice.h"
|
||||||
|
#include "dropboxbaserequest.h"
|
||||||
|
|
||||||
|
class QNetworkReply;
|
||||||
|
class NetworkAccessManager;
|
||||||
|
|
||||||
|
class DropboxStreamURLRequest : public DropboxBaseRequest {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit DropboxStreamURLRequest(const SharedPtr<NetworkAccessManager> network, DropboxService *service, const uint id, const QUrl &media_url, QObject *parent = nullptr);
|
||||||
|
~DropboxStreamURLRequest() override;
|
||||||
|
|
||||||
|
void Process();
|
||||||
|
void Cancel();
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void StreamURLRequestFinished(const uint id, const QUrl &media_url, const bool success, const QUrl &stream_url, const QString &error = QString());
|
||||||
|
|
||||||
|
private Q_SLOTS:
|
||||||
|
void StreamURLReceived();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void GetStreamURL();
|
||||||
|
void Error(const QString &error_message, const QVariant &debug_output = QVariant()) override;
|
||||||
|
void Finish();
|
||||||
|
|
||||||
|
private:
|
||||||
|
const SharedPtr<NetworkAccessManager> network_;
|
||||||
|
DropboxService *service_;
|
||||||
|
uint id_;
|
||||||
|
QUrl media_url_;
|
||||||
|
QUrl stream_url_;
|
||||||
|
QNetworkReply *reply_;
|
||||||
|
bool success_;
|
||||||
|
QString error_;
|
||||||
|
};
|
||||||
|
|
||||||
|
using DropboxStreamURLRequestPtr = QSharedPointer<DropboxStreamURLRequest>;
|
||||||
|
|
||||||
|
#endif // DROPBOXSTREAMURLREQUEST_H
|
||||||
76
src/dropbox/dropboxurlhandler.cpp
Normal file
76
src/dropbox/dropboxurlhandler.cpp
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
* 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 <QString>
|
||||||
|
#include <QUrl>
|
||||||
|
|
||||||
|
#include "includes/shared_ptr.h"
|
||||||
|
#include "core/taskmanager.h"
|
||||||
|
#include "dropboxurlhandler.h"
|
||||||
|
#include "dropboxservice.h"
|
||||||
|
|
||||||
|
DropboxUrlHandler::DropboxUrlHandler(const SharedPtr<TaskManager> task_manager, DropboxService *service, QObject *parent)
|
||||||
|
: UrlHandler(parent),
|
||||||
|
task_manager_(task_manager),
|
||||||
|
service_(service) {
|
||||||
|
|
||||||
|
QObject::connect(service, &DropboxService::StreamURLRequestFinished, this, &DropboxUrlHandler::StreamURLRequestFinished);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
QString DropboxUrlHandler::scheme() const { return service_->url_scheme(); }
|
||||||
|
|
||||||
|
UrlHandler::LoadResult DropboxUrlHandler::StartLoading(const QUrl &url) {
|
||||||
|
|
||||||
|
Request request;
|
||||||
|
request.task_id = task_manager_->StartTask(QStringLiteral("Loading %1 stream...").arg(url.scheme()));
|
||||||
|
QString error;
|
||||||
|
request.id = service_->GetStreamURL(url, error);
|
||||||
|
if (request.id == 0) {
|
||||||
|
CancelTask(request.task_id);
|
||||||
|
return LoadResult(url, LoadResult::Type::Error, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
requests_.insert(request.id, request);
|
||||||
|
|
||||||
|
LoadResult load_result(url);
|
||||||
|
load_result.type_ = LoadResult::Type::WillLoadAsynchronously;
|
||||||
|
|
||||||
|
return load_result;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void DropboxUrlHandler::StreamURLRequestFinished(const uint id, const QUrl &media_url, const bool success, const QUrl &stream_url, const QString &error) {
|
||||||
|
|
||||||
|
if (!requests_.contains(id)) return;
|
||||||
|
const Request request = requests_.take(id);
|
||||||
|
CancelTask(request.task_id);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
Q_EMIT AsyncLoadComplete(LoadResult(media_url, LoadResult::Type::TrackAvailable, stream_url));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Q_EMIT AsyncLoadComplete(LoadResult(media_url, LoadResult::Type::Error, error));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void DropboxUrlHandler::CancelTask(const int task_id) {
|
||||||
|
task_manager_->SetTaskFinished(task_id);
|
||||||
|
}
|
||||||
56
src/dropbox/dropboxurlhandler.h
Normal file
56
src/dropbox/dropboxurlhandler.h
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* 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 DROPBOXURLHANDLER_H
|
||||||
|
#define DROPBOXURLHANDLER_H
|
||||||
|
|
||||||
|
#include "includes/shared_ptr.h"
|
||||||
|
#include "core/urlhandler.h"
|
||||||
|
|
||||||
|
class TaskManager;
|
||||||
|
class DropboxService;
|
||||||
|
|
||||||
|
class DropboxUrlHandler : public UrlHandler {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit DropboxUrlHandler(const SharedPtr<TaskManager> task_manager, DropboxService *service, QObject *parent = nullptr);
|
||||||
|
|
||||||
|
QString scheme() const override;
|
||||||
|
LoadResult StartLoading(const QUrl &url) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void CancelTask(const int task_id);
|
||||||
|
|
||||||
|
private Q_SLOTS:
|
||||||
|
void StreamURLRequestFinished(const uint id, const QUrl &media_url, const bool success, const QUrl &stream_url, const QString &error = QString());
|
||||||
|
|
||||||
|
private:
|
||||||
|
class Request {
|
||||||
|
public:
|
||||||
|
explicit Request() : id(0), task_id(-1) {}
|
||||||
|
uint id;
|
||||||
|
int task_id;
|
||||||
|
};
|
||||||
|
const SharedPtr<TaskManager> task_manager_;
|
||||||
|
DropboxService *service_;
|
||||||
|
QMap<uint, Request> requests_;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // DROPBOXURLHANDLER_H
|
||||||
@@ -56,7 +56,7 @@ using namespace Qt::Literals::StringLiterals;
|
|||||||
using std::make_shared;
|
using std::make_shared;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr int kSongTableJoins = 2;
|
constexpr int kSongTableJoins = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
PlaylistBackend::PlaylistBackend(const SharedPtr<Database> database,
|
PlaylistBackend::PlaylistBackend(const SharedPtr<Database> database,
|
||||||
@@ -186,10 +186,12 @@ PlaylistBackend::Playlist PlaylistBackend::GetPlaylist(const int id) {
|
|||||||
|
|
||||||
QString PlaylistBackend::PlaylistItemsQuery() {
|
QString PlaylistBackend::PlaylistItemsQuery() {
|
||||||
|
|
||||||
return QStringLiteral("SELECT %1, %2, p.type FROM playlist_items AS p "
|
return QStringLiteral("SELECT %1, %2, %3, p.type FROM playlist_items AS p "
|
||||||
"LEFT JOIN songs ON p.type = songs.source AND p.collection_id = songs.ROWID "
|
"LEFT JOIN songs ON p.type = songs.source AND p.collection_id = songs.ROWID "
|
||||||
|
"LEFT JOIN dropbox_songs ON p.type = dropbox_songs.source AND p.collection_id = dropbox_songs.ROWID "
|
||||||
"WHERE p.playlist = :playlist"
|
"WHERE p.playlist = :playlist"
|
||||||
).arg(Song::JoinSpec(u"songs"_s),
|
).arg(Song::JoinSpec(u"songs"_s),
|
||||||
|
Song::JoinSpec(u"dropbox_songs"_s),
|
||||||
Song::JoinSpec(u"p"_s));
|
Song::JoinSpec(u"p"_s));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ PlaylistItemPtr PlaylistItem::NewFromSource(const Song::Source source) {
|
|||||||
|
|
||||||
switch (source) {
|
switch (source) {
|
||||||
case Song::Source::Collection:
|
case Song::Source::Collection:
|
||||||
|
case Song::Source::Dropbox:
|
||||||
|
case Song::Source::OneDrive:
|
||||||
return make_shared<CollectionPlaylistItem>(source);
|
return make_shared<CollectionPlaylistItem>(source);
|
||||||
case Song::Source::Subsonic:
|
case Song::Source::Subsonic:
|
||||||
case Song::Source::Tidal:
|
case Song::Source::Tidal:
|
||||||
@@ -72,6 +74,8 @@ PlaylistItemPtr PlaylistItem::NewFromSong(const Song &song) {
|
|||||||
|
|
||||||
switch (song.source()) {
|
switch (song.source()) {
|
||||||
case Song::Source::Collection:
|
case Song::Source::Collection:
|
||||||
|
case Song::Source::Dropbox:
|
||||||
|
case Song::Source::OneDrive:
|
||||||
return make_shared<CollectionPlaylistItem>(song);
|
return make_shared<CollectionPlaylistItem>(song);
|
||||||
case Song::Source::Subsonic:
|
case Song::Source::Subsonic:
|
||||||
case Song::Source::Tidal:
|
case Song::Source::Tidal:
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ SongPlaylistItem::SongPlaylistItem(const Song::Source source) : PlaylistItem(sou
|
|||||||
SongPlaylistItem::SongPlaylistItem(const Song &song) : PlaylistItem(song.source()), song_(song) {}
|
SongPlaylistItem::SongPlaylistItem(const Song &song) : PlaylistItem(song.source()), song_(song) {}
|
||||||
|
|
||||||
bool SongPlaylistItem::InitFromQuery(const SqlRow &query) {
|
bool SongPlaylistItem::InitFromQuery(const SqlRow &query) {
|
||||||
song_.InitFromQuery(query, false, static_cast<int>(Song::kRowIdColumns.count()));
|
song_.InitFromQuery(query, false, static_cast<int>(Song::kRowIdColumns.count() * 2));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ void StreamPlaylistItem::InitMetadata() {
|
|||||||
|
|
||||||
bool StreamPlaylistItem::InitFromQuery(const SqlRow &query) {
|
bool StreamPlaylistItem::InitFromQuery(const SqlRow &query) {
|
||||||
|
|
||||||
song_.InitFromQuery(query, false, static_cast<int>(Song::kRowIdColumns.count()));
|
song_.InitFromQuery(query, false, static_cast<int>(Song::kRowIdColumns.count() * 2));
|
||||||
InitMetadata();
|
InitMetadata();
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -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>
|
|
||||||
@@ -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 ¤t) {
|
|
||||||
|
|
||||||
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(¤t_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(¤t_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_));
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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 ¤t);
|
|
||||||
|
|
||||||
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
|
|
||||||
@@ -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>
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -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()));
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -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>
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -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(); }
|
|
||||||
@@ -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
|
|
||||||
@@ -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>
|
|
||||||
@@ -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();
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -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);
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -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();
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -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(); }
|
|
||||||
@@ -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
|
|
||||||
@@ -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>
|
|
||||||
@@ -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
|
|
||||||
@@ -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());
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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 ©right() 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
|
|
||||||
@@ -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;
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -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();
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -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;
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -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;
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -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();
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -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>
|
|
||||||
@@ -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();
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -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>
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -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));
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -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();
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -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);
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
144
src/settings/dropboxsettingspage.cpp
Normal file
144
src/settings/dropboxsettingspage.cpp
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
/*
|
||||||
|
* 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 <QByteArray>
|
||||||
|
#include <QString>
|
||||||
|
#include <QSettings>
|
||||||
|
#include <QCheckBox>
|
||||||
|
#include <QComboBox>
|
||||||
|
#include <QLineEdit>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QSpinBox>
|
||||||
|
#include <QMessageBox>
|
||||||
|
#include <QEvent>
|
||||||
|
|
||||||
|
#include "constants/dropboxsettings.h"
|
||||||
|
#include "core/settings.h"
|
||||||
|
#include "core/iconloader.h"
|
||||||
|
#include "widgets/loginstatewidget.h"
|
||||||
|
#include "dropbox/dropboxservice.h"
|
||||||
|
#include "settingsdialog.h"
|
||||||
|
#include "dropboxsettingspage.h"
|
||||||
|
#include "ui_dropboxsettingspage.h"
|
||||||
|
|
||||||
|
using namespace Qt::Literals::StringLiterals;
|
||||||
|
using namespace DropboxSettings;
|
||||||
|
|
||||||
|
DropboxSettingsPage::DropboxSettingsPage(SettingsDialog *dialog, const SharedPtr<DropboxService> service, QWidget *parent)
|
||||||
|
: SettingsPage(dialog, parent),
|
||||||
|
ui_(new Ui_DropboxSettingsPage),
|
||||||
|
service_(service) {
|
||||||
|
|
||||||
|
Q_ASSERT(service);
|
||||||
|
|
||||||
|
ui_->setupUi(this);
|
||||||
|
|
||||||
|
setWindowIcon(IconLoader::Load(u"dropbox"_s));
|
||||||
|
|
||||||
|
ui_->login_state->AddCredentialGroup(ui_->widget_authorization);
|
||||||
|
|
||||||
|
QObject::connect(ui_->button_login, &QPushButton::clicked, this, &DropboxSettingsPage::LoginClicked);
|
||||||
|
QObject::connect(ui_->button_reset, &QPushButton::clicked, this, &DropboxSettingsPage::ResetClicked);
|
||||||
|
QObject::connect(ui_->login_state, &LoginStateWidget::LogoutClicked, this, &DropboxSettingsPage::LogoutClicked);
|
||||||
|
|
||||||
|
QObject::connect(this, &DropboxSettingsPage::Authorize, &*service_, &DropboxService::Authenticate);
|
||||||
|
QObject::connect(&*service_, &StreamingService::LoginFailure, this, &DropboxSettingsPage::LoginFailure);
|
||||||
|
QObject::connect(&*service_, &StreamingService::LoginSuccess, this, &DropboxSettingsPage::LoginSuccess);
|
||||||
|
|
||||||
|
dialog->installEventFilter(this);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
DropboxSettingsPage::~DropboxSettingsPage() {
|
||||||
|
delete ui_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DropboxSettingsPage::Load() {
|
||||||
|
|
||||||
|
Settings s;
|
||||||
|
s.beginGroup(kSettingsGroup);
|
||||||
|
ui_->enable->setChecked(s.value(kEnabled, false).toBool());
|
||||||
|
s.endGroup();
|
||||||
|
|
||||||
|
if (service_->authenticated()) ui_->login_state->SetLoggedIn(LoginStateWidget::State::LoggedIn);
|
||||||
|
|
||||||
|
Init(ui_->layout_dropboxsettingspage->parentWidget());
|
||||||
|
|
||||||
|
if (!Settings().childGroups().contains(QLatin1String(kSettingsGroup))) set_changed();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void DropboxSettingsPage::Save() {
|
||||||
|
|
||||||
|
Settings s;
|
||||||
|
s.beginGroup(kSettingsGroup);
|
||||||
|
s.setValue(kEnabled, ui_->enable->isChecked());
|
||||||
|
s.endGroup();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void DropboxSettingsPage::LoginClicked() {
|
||||||
|
|
||||||
|
Q_EMIT Authorize();
|
||||||
|
|
||||||
|
ui_->button_login->setEnabled(false);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DropboxSettingsPage::eventFilter(QObject *object, QEvent *event) {
|
||||||
|
|
||||||
|
if (object == dialog() && event->type() == QEvent::Enter) {
|
||||||
|
ui_->button_login->setEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SettingsPage::eventFilter(object, event);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void DropboxSettingsPage::LogoutClicked() {
|
||||||
|
|
||||||
|
service_->ClearSession();
|
||||||
|
ui_->button_login->setEnabled(true);
|
||||||
|
ui_->login_state->SetLoggedIn(LoginStateWidget::State::LoggedOut);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void DropboxSettingsPage::LoginSuccess() {
|
||||||
|
|
||||||
|
if (!isVisible()) return;
|
||||||
|
ui_->login_state->SetLoggedIn(LoginStateWidget::State::LoggedIn);
|
||||||
|
ui_->button_login->setEnabled(true);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void DropboxSettingsPage::LoginFailure(const QString &failure_reason) {
|
||||||
|
|
||||||
|
if (!isVisible()) return;
|
||||||
|
QMessageBox::warning(this, tr("Authentication failed"), failure_reason);
|
||||||
|
ui_->button_login->setEnabled(true);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void DropboxSettingsPage::ResetClicked() {
|
||||||
|
|
||||||
|
service_->Reset();
|
||||||
|
|
||||||
|
}
|
||||||
58
src/settings/dropboxsettingspage.h
Normal file
58
src/settings/dropboxsettingspage.h
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* 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 DROPBOXSETTINGSPAGE_H
|
||||||
|
#define DROPBOXSETTINGSPAGE_H
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
|
||||||
|
#include "includes/shared_ptr.h"
|
||||||
|
#include "settingspage.h"
|
||||||
|
|
||||||
|
class DropboxService;
|
||||||
|
class Ui_DropboxSettingsPage;
|
||||||
|
|
||||||
|
class DropboxSettingsPage : public SettingsPage {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit DropboxSettingsPage(SettingsDialog *dialog, const SharedPtr<DropboxService> service, QWidget *parent);
|
||||||
|
~DropboxSettingsPage();
|
||||||
|
|
||||||
|
void Load() override;
|
||||||
|
void Save() override;
|
||||||
|
|
||||||
|
bool eventFilter(QObject *object, QEvent *event) override;
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void Authorize();
|
||||||
|
|
||||||
|
private Q_SLOTS:
|
||||||
|
void LoginClicked();
|
||||||
|
void LogoutClicked();
|
||||||
|
void LoginSuccess();
|
||||||
|
void LoginFailure(const QString &failure_reason);
|
||||||
|
void ResetClicked();
|
||||||
|
|
||||||
|
private:
|
||||||
|
Ui_DropboxSettingsPage *ui_;
|
||||||
|
const SharedPtr<DropboxService> service_;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // DROPBOXSETTINGSPAGE_H
|
||||||
125
src/settings/dropboxsettingspage.ui
Normal file
125
src/settings/dropboxsettingspage.ui
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>DropboxSettingsPage</class>
|
||||||
|
<widget class="QWidget" name="DropboxSettingsPage">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>569</width>
|
||||||
|
<height>491</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Dropbox</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="layout_dropboxsettingspage">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_info">
|
||||||
|
<property name="text">
|
||||||
|
<string>Strawberry can play music that you have uploaded to Dropbox</string>
|
||||||
|
</property>
|
||||||
|
<property name="wordWrap">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QCheckBox" name="enable">
|
||||||
|
<property name="text">
|
||||||
|
<string>Enable</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="LoginStateWidget" name="login_state" native="true"/>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QWidget" name="widget_authorization" native="true">
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
|
<property name="leftMargin">
|
||||||
|
<number>28</number>
|
||||||
|
</property>
|
||||||
|
<property name="topMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="bottomMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="button_login">
|
||||||
|
<property name="text">
|
||||||
|
<string>Login</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="spacer_login">
|
||||||
|
<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>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="layout_buttons">
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="button_reset">
|
||||||
|
<property name="text">
|
||||||
|
<string>Reset cursor and songs</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="spacer_buttons">
|
||||||
|
<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="spacer_bottom">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>20</width>
|
||||||
|
<height>357</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>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
||||||
@@ -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));
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -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. <a href="https://gpodder.net/register/">Create an account</a>.</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>
|
|
||||||
@@ -90,6 +90,10 @@
|
|||||||
# include "qobuz/qobuzservice.h"
|
# include "qobuz/qobuzservice.h"
|
||||||
# include "qobuzsettingspage.h"
|
# include "qobuzsettingspage.h"
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef HAVE_DROPBOX
|
||||||
|
# include "dropbox/dropboxservice.h"
|
||||||
|
# include "dropboxsettingspage.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
#include "ui_settingsdialog.h"
|
#include "ui_settingsdialog.h"
|
||||||
|
|
||||||
@@ -144,7 +148,7 @@ SettingsDialog::SettingsDialog(const SharedPtr<Player> player,
|
|||||||
AddPage(Page::Moodbar, new MoodbarSettingsPage(this, this), iface);
|
AddPage(Page::Moodbar, new MoodbarSettingsPage(this, this), iface);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if defined(HAVE_SUBSONIC) || defined(HAVE_TIDAL) || defined(HAVE_SPOTIFY) || defined(HAVE_QOBUZ)
|
#if defined(HAVE_SUBSONIC) || defined(HAVE_TIDAL) || defined(HAVE_SPOTIFY) || defined(HAVE_QOBUZ) || defined(HAVE_DROPBOX)
|
||||||
QTreeWidgetItem *streaming = AddCategory(tr("Streaming"));
|
QTreeWidgetItem *streaming = AddCategory(tr("Streaming"));
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -160,6 +164,9 @@ SettingsDialog::SettingsDialog(const SharedPtr<Player> player,
|
|||||||
#ifdef HAVE_QOBUZ
|
#ifdef HAVE_QOBUZ
|
||||||
AddPage(Page::Qobuz, new QobuzSettingsPage(this, streaming_services->Service<QobuzService>(), this), streaming);
|
AddPage(Page::Qobuz, new QobuzSettingsPage(this, streaming_services->Service<QobuzService>(), this), streaming);
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef HAVE_DROPBOX
|
||||||
|
AddPage(Page::Dropbox, new DropboxSettingsPage(this, streaming_services->Service<DropboxService>(), this), streaming);
|
||||||
|
#endif
|
||||||
|
|
||||||
// List box
|
// List box
|
||||||
QObject::connect(ui_->list, &QTreeWidget::currentItemChanged, this, &SettingsDialog::CurrentItemChanged);
|
QObject::connect(ui_->list, &QTreeWidget::currentItemChanged, this, &SettingsDialog::CurrentItemChanged);
|
||||||
|
|||||||
@@ -93,6 +93,8 @@ class SettingsDialog : public QDialog {
|
|||||||
Tidal,
|
Tidal,
|
||||||
Qobuz,
|
Qobuz,
|
||||||
Spotify,
|
Spotify,
|
||||||
|
Dropbox,
|
||||||
|
OneDrive,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum Role {
|
enum Role {
|
||||||
|
|||||||
134
src/streaming/cloudstoragestreamingservice.cpp
Normal file
134
src/streaming/cloudstoragestreamingservice.cpp
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
/*
|
||||||
|
* 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 <memory>
|
||||||
|
|
||||||
|
#include <QString>
|
||||||
|
#include <QUrl>
|
||||||
|
#include <QFileInfo>
|
||||||
|
|
||||||
|
#include "core/logging.h"
|
||||||
|
#include "core/database.h"
|
||||||
|
#include "core/taskmanager.h"
|
||||||
|
#include "core/song.h"
|
||||||
|
#include "collection/collectionbackend.h"
|
||||||
|
#include "collection/collectionmodel.h"
|
||||||
|
#include "playlist/playlist.h"
|
||||||
|
#include "cloudstoragestreamingservice.h"
|
||||||
|
|
||||||
|
using namespace Qt::Literals::StringLiterals;
|
||||||
|
using std::make_shared;
|
||||||
|
|
||||||
|
CloudStorageStreamingService::CloudStorageStreamingService(const SharedPtr<TaskManager> task_manager,
|
||||||
|
const SharedPtr<Database> database,
|
||||||
|
const SharedPtr<TagReaderClient> tagreader_client,
|
||||||
|
const SharedPtr<AlbumCoverLoader> albumcover_loader,
|
||||||
|
const Song::Source source,
|
||||||
|
const QString &name,
|
||||||
|
const QString &url_scheme,
|
||||||
|
const QString &settings_group,
|
||||||
|
QObject *parent)
|
||||||
|
: StreamingService(source, name, url_scheme, settings_group, parent),
|
||||||
|
task_manager_(task_manager),
|
||||||
|
tagreader_client_(tagreader_client),
|
||||||
|
source_(source),
|
||||||
|
indexing_task_id_(-1),
|
||||||
|
indexing_task_progress_(0),
|
||||||
|
indexing_task_max_(0) {
|
||||||
|
|
||||||
|
collection_backend_ = make_shared<CollectionBackend>();
|
||||||
|
collection_backend_->moveToThread(database->thread());
|
||||||
|
collection_backend_->Init(database, task_manager, source, name + "_songs"_L1);
|
||||||
|
collection_model_ = new CollectionModel(collection_backend_, albumcover_loader, this);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void CloudStorageStreamingService::MaybeAddFileToDatabase(const QUrl &url, const QString &filename, const size_t size, const quint64 mtime, const QString &token_type, const QString &access_token) {
|
||||||
|
|
||||||
|
if (!IsSupportedFiletype(filename)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (indexing_task_id_ == -1) {
|
||||||
|
indexing_task_id_ = task_manager_->StartTask(tr("Indexing %1").arg(name()));
|
||||||
|
indexing_task_progress_ = 0;
|
||||||
|
indexing_task_max_ = 0;
|
||||||
|
}
|
||||||
|
indexing_task_max_++;
|
||||||
|
task_manager_->SetTaskProgress(indexing_task_id_, indexing_task_progress_, indexing_task_max_);
|
||||||
|
|
||||||
|
TagReaderReadStreamReplyPtr reply = tagreader_client_->ReadStreamAsync(url, filename, size, mtime, token_type, access_token);
|
||||||
|
pending_tagreader_replies_.append(reply);
|
||||||
|
|
||||||
|
SharedPtr<QMetaObject::Connection> connection = make_shared<QMetaObject::Connection>();
|
||||||
|
*connection = QObject::connect(&*reply, &TagReaderReadStreamReply::Finished, this, [this, reply, url, filename, connection]() {
|
||||||
|
ReadStreamFinished(reply, url, filename);
|
||||||
|
QObject::disconnect(*connection);
|
||||||
|
}, Qt::QueuedConnection);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void CloudStorageStreamingService::ReadStreamFinished(TagReaderReadStreamReplyPtr reply, const QUrl url, const QString filename) {
|
||||||
|
|
||||||
|
++indexing_task_progress_;
|
||||||
|
if (indexing_task_progress_ >= indexing_task_max_) {
|
||||||
|
task_manager_->SetTaskFinished(indexing_task_id_);
|
||||||
|
indexing_task_id_ = -1;
|
||||||
|
Q_EMIT AllIndexingTasksFinished();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
task_manager_->SetTaskProgress(indexing_task_id_, indexing_task_progress_, indexing_task_max_);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!reply->result().success()) {
|
||||||
|
qLog(Error) << "Failed to read tags from stream, URL" << url << reply->result().error_string();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Song song = reply->song();
|
||||||
|
song.set_source(source_);
|
||||||
|
song.set_directory_id(0);
|
||||||
|
QUrl song_url;
|
||||||
|
song_url.setScheme(url_scheme());
|
||||||
|
song_url.setPath(filename);
|
||||||
|
song.set_url(song_url);
|
||||||
|
|
||||||
|
collection_backend_->AddOrUpdateSongs(SongList() << song);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CloudStorageStreamingService::IsSupportedFiletype(const QString &filename) {
|
||||||
|
|
||||||
|
const QFileInfo fileinfo(filename);
|
||||||
|
return Song::kAcceptedExtensions.contains(fileinfo.suffix(), Qt::CaseInsensitive) && !Song::kRejectedExtensions.contains(fileinfo.suffix(), Qt::CaseInsensitive);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void CloudStorageStreamingService::AbortReadTagsReplies() {
|
||||||
|
|
||||||
|
qLog(Debug) << "Aborting the read tags replies";
|
||||||
|
|
||||||
|
pending_tagreader_replies_.clear();
|
||||||
|
|
||||||
|
task_manager_->SetTaskFinished(indexing_task_id_);
|
||||||
|
indexing_task_id_ = -1;
|
||||||
|
|
||||||
|
Q_EMIT AllIndexingTasksFinished();
|
||||||
|
|
||||||
|
}
|
||||||
89
src/streaming/cloudstoragestreamingservice.h
Normal file
89
src/streaming/cloudstoragestreamingservice.h
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
/*
|
||||||
|
* 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 CLOUDSTORAGESTREAMINGSERVICE_H
|
||||||
|
#define CLOUDSTORAGESTREAMINGSERVICE_H
|
||||||
|
|
||||||
|
#include <QList>
|
||||||
|
|
||||||
|
#include "includes/shared_ptr.h"
|
||||||
|
#include "tagreader/tagreaderclient.h"
|
||||||
|
#include "streamingservice.h"
|
||||||
|
#include "covermanager/albumcovermanager.h"
|
||||||
|
#include "collection/collectionmodel.h"
|
||||||
|
|
||||||
|
class TaskManager;
|
||||||
|
class Database;
|
||||||
|
class TagReaderClient;
|
||||||
|
class AlbumCoverLoader;
|
||||||
|
class CollectionBackend;
|
||||||
|
class CollectionModel;
|
||||||
|
class NetworkAccessManager;
|
||||||
|
|
||||||
|
class CloudStorageStreamingService : public StreamingService {
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit CloudStorageStreamingService(const SharedPtr<TaskManager> task_manager,
|
||||||
|
const SharedPtr<Database> database,
|
||||||
|
const SharedPtr<TagReaderClient> tagreader_client,
|
||||||
|
const SharedPtr<AlbumCoverLoader> albumcover_loader,
|
||||||
|
const Song::Source source,
|
||||||
|
const QString &name,
|
||||||
|
const QString &url_scheme,
|
||||||
|
const QString &settings_group,
|
||||||
|
QObject *parent = nullptr);
|
||||||
|
|
||||||
|
bool is_indexing() const { return indexing_task_id_ != -1; }
|
||||||
|
|
||||||
|
SharedPtr<CollectionBackend> collection_backend() const { return collection_backend_; }
|
||||||
|
CollectionModel *collection_model() const { return collection_model_; }
|
||||||
|
CollectionFilter *collection_filter_model() const { return collection_model_->filter(); }
|
||||||
|
|
||||||
|
SharedPtr<CollectionBackend> songs_collection_backend() override { return collection_backend_; }
|
||||||
|
CollectionModel *songs_collection_model() override { return collection_model_; }
|
||||||
|
CollectionFilter *songs_collection_filter_model() override { return collection_model_->filter(); }
|
||||||
|
|
||||||
|
virtual void MaybeAddFileToDatabase(const QUrl &url, const QString &filename, const size_t size, const quint64 mtime, const QString &token_type = QString(), const QString &access_token = QString());
|
||||||
|
static bool IsSupportedFiletype(const QString &filename);
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void AllIndexingTasksFinished();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void AbortReadTagsReplies();
|
||||||
|
|
||||||
|
protected Q_SLOTS:
|
||||||
|
void ReadStreamFinished(TagReaderReadStreamReplyPtr reply, const QUrl url, const QString filename);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
const SharedPtr<TaskManager> task_manager_;
|
||||||
|
const SharedPtr<TagReaderClient> tagreader_client_;
|
||||||
|
SharedPtr<CollectionBackend> collection_backend_;
|
||||||
|
CollectionModel *collection_model_;
|
||||||
|
QList<TagReaderReplyPtr> pending_tagreader_replies_;
|
||||||
|
|
||||||
|
private:
|
||||||
|
Song::Source source_;
|
||||||
|
int indexing_task_id_;
|
||||||
|
int indexing_task_progress_;
|
||||||
|
int indexing_task_max_;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // CLOUDSTORAGESTREAMINGSERVICE_H
|
||||||
@@ -142,6 +142,8 @@ QString CoverUtils::CoverFilenameFromSource(const Song::Source source, const QUr
|
|||||||
case Song::Source::Stream:
|
case Song::Source::Stream:
|
||||||
case Song::Source::SomaFM:
|
case Song::Source::SomaFM:
|
||||||
case Song::Source::RadioParadise:
|
case Song::Source::RadioParadise:
|
||||||
|
case Song::Source::Dropbox:
|
||||||
|
case Song::Source::OneDrive:
|
||||||
case Song::Source::Unknown:
|
case Song::Source::Unknown:
|
||||||
filename = QString::fromLatin1(Sha1CoverHash(artist, album).toHex());
|
filename = QString::fromLatin1(Sha1CoverHash(artist, album).toHex());
|
||||||
break;
|
break;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user