From 5c57c923d9335ddc007a5f3a75912a3da5777efa Mon Sep 17 00:00:00 2001 From: Jonas Kvinge Date: Sun, 26 Jan 2025 11:05:07 +0100 Subject: [PATCH] Add Dropbox --- CMakeLists.txt | 28 ++ data/icons.qrc | 5 + data/icons/128x128/dropbox.png | Bin 0 -> 2255 bytes data/icons/22x22/dropbox.png | Bin 0 -> 638 bytes data/icons/32x32/dropbox.png | Bin 0 -> 746 bytes data/icons/48x48/dropbox.png | Bin 0 -> 1011 bytes data/icons/64x64/dropbox.png | Bin 0 -> 1280 bytes data/icons/full/dropbox.png | Bin 0 -> 11345 bytes data/schema/schema-22.sql | 82 ++++++ data/schema/schema.sql | 81 ++++++ src/collection/collectionplaylistitem.cpp | 5 +- src/config.h.in | 2 + src/constants/dropboxconstants.h | 30 +++ src/constants/dropboxsettings.h | 46 ++++ src/core/application.cpp | 7 + src/core/mainwindow.cpp | 39 ++- src/core/mainwindow.h | 3 + src/core/song.cpp | 13 +- src/core/song.h | 4 +- .../albumcoverchoicecontroller.cpp | 2 + src/dropbox/dropboxbaserequest.cpp | 132 ++++++++++ src/dropbox/dropboxbaserequest.h | 59 +++++ src/dropbox/dropboxservice.cpp | 190 ++++++++++++++ src/dropbox/dropboxservice.h | 93 +++++++ src/dropbox/dropboxsongsrequest.cpp | 244 ++++++++++++++++++ src/dropbox/dropboxsongsrequest.h | 67 +++++ src/dropbox/dropboxstreamurlrequest.cpp | 129 +++++++++ src/dropbox/dropboxstreamurlrequest.h | 71 +++++ src/dropbox/dropboxurlhandler.cpp | 76 ++++++ src/dropbox/dropboxurlhandler.h | 56 ++++ src/playlist/playlistbackend.cpp | 6 +- src/playlist/playlistitem.cpp | 4 + src/playlist/songplaylistitem.cpp | 2 +- src/playlist/streamplaylistitem.cpp | 2 +- src/settings/dropboxsettingspage.cpp | 144 +++++++++++ src/settings/dropboxsettingspage.h | 58 +++++ src/settings/dropboxsettingspage.ui | 125 +++++++++ src/settings/settingsdialog.cpp | 9 +- src/settings/settingsdialog.h | 2 + .../cloudstoragestreamingservice.cpp | 134 ++++++++++ src/streaming/cloudstoragestreamingservice.h | 89 +++++++ src/utilities/coverutils.cpp | 2 + 42 files changed, 2032 insertions(+), 9 deletions(-) create mode 100644 data/icons/128x128/dropbox.png create mode 100644 data/icons/22x22/dropbox.png create mode 100644 data/icons/32x32/dropbox.png create mode 100644 data/icons/48x48/dropbox.png create mode 100644 data/icons/64x64/dropbox.png create mode 100644 data/icons/full/dropbox.png create mode 100644 data/schema/schema-22.sql create mode 100644 src/constants/dropboxconstants.h create mode 100644 src/constants/dropboxsettings.h create mode 100644 src/dropbox/dropboxbaserequest.cpp create mode 100644 src/dropbox/dropboxbaserequest.h create mode 100644 src/dropbox/dropboxservice.cpp create mode 100644 src/dropbox/dropboxservice.h create mode 100644 src/dropbox/dropboxsongsrequest.cpp create mode 100644 src/dropbox/dropboxsongsrequest.h create mode 100644 src/dropbox/dropboxstreamurlrequest.cpp create mode 100644 src/dropbox/dropboxstreamurlrequest.h create mode 100644 src/dropbox/dropboxurlhandler.cpp create mode 100644 src/dropbox/dropboxurlhandler.h create mode 100644 src/settings/dropboxsettingspage.cpp create mode 100644 src/settings/dropboxsettingspage.h create mode 100644 src/settings/dropboxsettingspage.ui create mode 100644 src/streaming/cloudstoragestreamingservice.cpp create mode 100644 src/streaming/cloudstoragestreamingservice.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 9db5a974a..78225a4c7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -379,6 +379,13 @@ optional_component(STREAMTAGREADER ON "Stream tagreader" optional_component(DISCORD_RPC ON "Discord Rich Presence" DEPENDS "RapidJSON" RapidJSON_FOUND + +optional_component(DROPBOX ON "Streaming: Dropbox" + DEPENDS "Stream tagreader" HAVE_STREAMTAGREADER +) + +optional_component(ONEDRIVE ON "Streaming: OneDrive" + DEPENDS "Stream tagreader" HAVE_STREAMTAGREADER ) if(HAVE_SONGFINGERPRINTING OR HAVE_MUSICBRAINZ) @@ -776,6 +783,7 @@ set(SOURCES src/streaming/streamingcollectionviewcontainer.cpp src/streaming/streamingsearchview.cpp src/streaming/streamsongmimedata.cpp + src/streaming/cloudstoragestreamingservice.cpp src/radios/radioservices.cpp src/radios/radiobackend.cpp @@ -1072,6 +1080,7 @@ set(HEADERS src/streaming/streamingtabsview.h src/streaming/streamingcollectionview.h src/streaming/streamingcollectionviewcontainer.h + src/streaming/cloudstoragestreamingservice.h src/radios/radioservices.h src/radios/radiobackend.h @@ -1480,6 +1489,25 @@ optional_source(HAVE_QOBUZ src/settings/qobuzsettingspage.ui ) +optional_source(HAVE_DROPBOX + SOURCES + src/dropbox/dropboxservice.cpp + src/dropbox/dropboxurlhandler.cpp + src/dropbox/dropboxbaserequest.cpp + src/dropbox/dropboxsongsrequest.cpp + src/dropbox/dropboxstreamurlrequest.cpp + src/settings/dropboxsettingspage.cpp + HEADERS + src/dropbox/dropboxservice.h + src/dropbox/dropboxurlhandler.h + src/dropbox/dropboxbaserequest.h + src/dropbox/dropboxsongsrequest.h + src/dropbox/dropboxstreamurlrequest.h + src/settings/dropboxsettingspage.h + UI + src/settings/dropboxsettingspage.ui +) + qt_wrap_cpp(SOURCES ${HEADERS}) qt_wrap_ui(SOURCES ${UI}) qt_add_resources(SOURCES data/data.qrc data/icons.qrc) diff --git a/data/icons.qrc b/data/icons.qrc index ebbfe2f3b..863717f80 100644 --- a/data/icons.qrc +++ b/data/icons.qrc @@ -98,6 +98,7 @@ icons/128x128/somafm.png icons/128x128/radioparadise.png icons/128x128/musicbrainz.png + icons/128x128/dropbox.png icons/64x64/albums.png icons/64x64/alsa.png icons/64x64/application-exit.png @@ -197,6 +198,7 @@ icons/64x64/somafm.png icons/64x64/radioparadise.png icons/64x64/musicbrainz.png + icons/64x64/dropbox.png icons/48x48/albums.png icons/48x48/alsa.png icons/48x48/application-exit.png @@ -300,6 +302,7 @@ icons/48x48/somafm.png icons/48x48/radioparadise.png icons/48x48/musicbrainz.png + icons/48x48/dropbox.png icons/32x32/albums.png icons/32x32/alsa.png icons/32x32/application-exit.png @@ -403,6 +406,7 @@ icons/32x32/somafm.png icons/32x32/radioparadise.png icons/32x32/musicbrainz.png + icons/32x32/dropbox.png icons/22x22/albums.png icons/22x22/alsa.png icons/22x22/application-exit.png @@ -506,5 +510,6 @@ icons/22x22/somafm.png icons/22x22/radioparadise.png icons/22x22/musicbrainz.png + icons/22x22/dropbox.png diff --git a/data/icons/128x128/dropbox.png b/data/icons/128x128/dropbox.png new file mode 100644 index 0000000000000000000000000000000000000000..2c8f4a56a76278f0f7c38a7a09510e125af7300d GIT binary patch literal 2255 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 W5iB$9nR`F53`r#c;v1eZS$_a!;ZAt~ literal 0 HcmV?d00001 diff --git a/data/icons/48x48/dropbox.png b/data/icons/48x48/dropbox.png new file mode 100644 index 0000000000000000000000000000000000000000..a105e85e8fe54fddd262408008e08bf087869e75 GIT binary patch literal 1011 zcmZ9L3ry2j6o+qxg@-VSzzB>XQ^0N@{@Vhhr67Z{Qn3h(iNeGd3Y8H;={lusaUFy5 zNJfysU<8+`yew*vhf@&=9XNSJx9P^q5y?7gWk6g28Bl)vFD6UoP0syt?!8}b&bhf| zaj`;sTQ^%mh`lIUkU*3AH>@q_S@O`Pn}%n4bb^?W0v|#&MTD%;OPa@od_*67A|ZrV zM~I7};YK{45Jq6!fy5{x1tSfX=^zqVq{C7Mk_78aki#IyKn@{9iwPq_iow@nOB!tD zsObegs(ZnZg1nCdl`t&8yon7nVk$vWP<|DwYtXbqa|N#|QF{w(8`uPD`{0^~x;~Jj z*jI|NS^PSSqE0NWV)<`qyFgMwgebU#t;b<|90#lL`vPLC;G;(C5WMr@laC9-SY3y* z1CBM8-E+z4g?cB|oa5}$YAqz;M}WS_?g9msp|{RG3)_ygVI{DUgKnwy=7^aF zBP~WK$n<+K;Y}x2Dv>zK%EWN9-{~OM_FSd6Yej;$6Qh5aua+n2DJQKcCNkwm{Vx$$ zTK{~VSBbld+pe_k;@HCk18#^19(FRf|BUw?7AQ#WN%Ki2k{Ja)b_ zH%g%1gn)2*S`$OTQ%aZP%b<)ojop;rbI+}v#&Kjr3 z2XvFtgRjh842ikU7d40eReWjQ!`L_z;uL7p9(i=At7pvEI2`Qs=2yccb#J)3PZ=jt zT}zbRmO01Q-0(QJOOw7?q;xJjwDn#0V{LEMx5$kuQ`AhyMC;f)-&wMg6AZd_rfw`@ zgv#bc_L}%TDVmVq+Uslw7j}f-_Xx}{sQBWi`!cIj>L%wqLXYgwth-$sE1OTb9bA;( zy0G`ll^vr=Y;(U|s$t)Xqgb$Hk1BTmv!+ZA{c7H-sKY9mRF%O?S7y*4Y>FMYhhptv z2PLvNJPw;jv3x0tM^SbdSN~7Q$(3bgs$UdXya3K~p*VcNnHHG;-Jr_K$@owu%~8DQ Z1n^iv&mI59ZS?OYL=+V(xEvuV{0ERd%VTOPsM`jlpabx4s{cI5t2BMkmP-Yj8d)S7liC!67n*b5U!dKD|!AU zUl&4XY9CLDkc2=#6ndLL)`PYQ1~QO9kah6N0`Y@cBnqyAviz%<90}r&3Dm&n2vFnY zJB*E^`z`!(Knnp0#_TwpYr^Oler$wwEFw-~@Na0xF|GsIh(i}KJc_hR7;iy*8MI^2 z=@9<|j5dP=!9D?+X8hC)*$KQI!n;0!(eVZznIK+p z&xG)65Knk~gBNd+T?L z8T%@cau!5_pIcCK9~My{YY|rp9S~Ivl?DfEKo3J~DQuLu^Z@7YVy*%q1?U?@Pd_&1 z!g3o*ZlJIp=27s?26eT@*AsP=&^CFj7JR8~HEy5^xr>%Zk|4w1H>V41MQ*yiLY! zSUZ(k?#-$BZbrXd(Jb*+YtJc<9@+8Dw%0BVPG6kr%%xfIoE6iJSM7IpW}a0KDR=O_ zElLm1Qz(l*8XmcwSJ!U*8{O`1rcM|6=KL#da8?R$iue#NXY0kyFPLN0|CDTU9s*nW?QQcTAhY8TZb32hd#{Ys&E}Sqo~Te&W$8`U&Gzv+iD`n;?*{iY!lX zxR6L)eDuWNtbDaUZ+l5zH1%J03Sodk7NQ8_hQ@|bLfB09(j`pR61Jm+#o=<;TqbJ~ zlgVW=P0*6?KOj0r77?ECA;97~aNYxHuJx7_F#o>|iiqg2I7LXb{6mJrbzr||ZwiDCu|m literal 0 HcmV?d00001 diff --git a/data/icons/full/dropbox.png b/data/icons/full/dropbox.png new file mode 100644 index 0000000000000000000000000000000000000000..728ecc18452778fc398d17acdbde0c326cf78af3 GIT binary patch literal 11345 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 literal 0 HcmV?d00001 diff --git a/data/schema/schema-22.sql b/data/schema/schema-22.sql new file mode 100644 index 000000000..73f4f22a5 --- /dev/null +++ b/data/schema/schema-22.sql @@ -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; diff --git a/data/schema/schema.sql b/data/schema/schema.sql index 919fe7974..d2306a439 100644 --- a/data/schema/schema.sql +++ b/data/schema/schema.sql @@ -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 ( name TEXT NOT NULL, diff --git a/src/collection/collectionplaylistitem.cpp b/src/collection/collectionplaylistitem.cpp index 29363f206..46f5f8512 100644 --- a/src/collection/collectionplaylistitem.cpp +++ b/src/collection/collectionplaylistitem.cpp @@ -41,9 +41,12 @@ bool CollectionPlaylistItem::InitFromQuery(const SqlRow &query) { case Song::Source::Collection: col = 0; break; - default: + case Song::Source::Dropbox: col = static_cast(Song::kRowIdColumns.count()); break; + default: + col = static_cast(Song::kRowIdColumns.count() * 2); + break; } song_.InitFromQuery(query, true, col); diff --git a/src/config.h.in b/src/config.h.in index 84b474d01..6f6352385 100644 --- a/src/config.h.in +++ b/src/config.h.in @@ -33,6 +33,8 @@ #cmakedefine HAVE_SPOTIFY #cmakedefine HAVE_QOBUZ #cmakedefine HAVE_DISCORD_RPC +#cmakedefine HAVE_DROPBOX +#cmakedefine HAVE_ONEDRIVE #cmakedefine HAVE_TAGLIB_DSFFILE #cmakedefine HAVE_TAGLIB_DSDIFFFILE diff --git a/src/constants/dropboxconstants.h b/src/constants/dropboxconstants.h new file mode 100644 index 000000000..9d7576db5 --- /dev/null +++ b/src/constants/dropboxconstants.h @@ -0,0 +1,30 @@ +/* +* Strawberry Music Player +* Copyright 2025, Jonas Kvinge +* +* Strawberry is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* Strawberry is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with Strawberry. If not, see . +* +*/ + +#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 diff --git a/src/constants/dropboxsettings.h b/src/constants/dropboxsettings.h new file mode 100644 index 000000000..8d3454de6 --- /dev/null +++ b/src/constants/dropboxsettings.h @@ -0,0 +1,46 @@ +/* +* Strawberry Music Player +* Copyright 2025, Jonas Kvinge +* +* Strawberry is free software: you can redistribute it and/or modify +* it under the terms of the GNU General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. +* +* Strawberry is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU General Public License for more details. +* +* You should have received a copy of the GNU General Public License +* along with Strawberry. If not, see . +* +*/ + +#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 diff --git a/src/core/application.cpp b/src/core/application.cpp index c08eed78d..91a6ae9c6 100644 --- a/src/core/application.cpp +++ b/src/core/application.cpp @@ -105,6 +105,10 @@ # include "covermanager/qobuzcoverprovider.h" #endif +#ifdef HAVE_DROPBOX +# include "dropbox/dropboxservice.h" +#endif + #ifdef HAVE_MOODBAR # include "moodbar/moodbarcontroller.h" # include "moodbar/moodbarloader.h" @@ -200,6 +204,9 @@ class ApplicationImpl { #endif #ifdef HAVE_QOBUZ streaming_services->AddService(make_shared(app->task_manager(), app->database(), app->network(), app->url_handlers(), app->albumcover_loader())); +#endif +#ifdef HAVE_DROPBOX + streaming_services->AddService(make_shared(app->task_manager(), app->database(), app->network(), app->url_handlers(), app->tagreader_client(), app->albumcover_loader())); #endif return streaming_services; }), diff --git a/src/core/mainwindow.cpp b/src/core/mainwindow.cpp index ea66e1b85..efb30a540 100644 --- a/src/core/mainwindow.cpp +++ b/src/core/mainwindow.cpp @@ -178,6 +178,9 @@ #ifdef HAVE_QOBUZ # include "constants/qobuzsettings.h" #endif +#ifdef HAVE_DROPBOX +# include "constants/dropboxsettings.h" +#endif #include "streaming/streamingservices.h" #include "streaming/streamingservice.h" @@ -355,6 +358,9 @@ MainWindow::MainWindow(Application *app, #endif #ifdef HAVE_QOBUZ 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 radio_view_(new RadioViewContainer(this)), lastfm_import_dialog_(new LastFMImportDialog(app_->lastfm_import(), this)), @@ -441,6 +447,9 @@ MainWindow::MainWindow(Application *app, #ifdef HAVE_QOBUZ ui_->tabs->AddTab(qobuz_view_, u"qobuz"_s, IconLoader::Load(u"qobuz"_s, true, 0, 32), tr("Qobuz")); #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 ui_->tabs->AddBottomWidget(ui_->widget_playing); @@ -782,6 +791,12 @@ MainWindow::MainWindow(Application *app, } #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_->view(), &RadioView::GetChannels, &*app_->radio_services(), &RadioServices::GetChannels); QObject::connect(radio_view_->view(), &RadioView::AddToPlaylistSignal, this, &MainWindow::AddToPlaylist); @@ -1280,6 +1295,18 @@ void MainWindow::ReloadSettings() { } #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(); } @@ -1326,10 +1353,12 @@ void MainWindow::ReloadAllSettings() { qobuz_view_->ReloadSettings(); qobuz_view_->search_view()->ReloadSettings(); #endif +#ifdef HAVE_DROPBOX + dropbox_view_->ReloadSettings(); +#endif #ifdef HAVE_DISCORD_RPC discord_rich_presence_->ReloadSettings(); #endif - } void MainWindow::RefreshStyleSheet() { @@ -2717,6 +2746,9 @@ void MainWindow::OpenServiceSettingsDialog(const Song::Source source) { case Song::Source::Spotify: settings_dialog_->OpenAtPage(SettingsDialog::Page::Spotify); break; + case Song::Source::Dropbox: + settings_dialog_->OpenAtPage(SettingsDialog::Page::Dropbox); + break; default: break; } @@ -3398,6 +3430,11 @@ void MainWindow::FocusSearchField() { else if (ui_->tabs->currentIndex() == ui_->tabs->IndexOfTab(qobuz_view_) && !qobuz_view_->SearchFieldHasFocus()) { qobuz_view_->FocusSearchField(); } +#endif +#ifdef HAVE_DROPBOX + else if (ui_->tabs->currentIndex() == ui_->tabs->IndexOfTab(dropbox_view_) && !dropbox_view_->SearchFieldHasFocus()) { + dropbox_view_->FocusSearchField(); + } #endif else if (!ui_->playlist->SearchFieldHasFocus()) { ui_->playlist->FocusSearchField(); diff --git a/src/core/mainwindow.h b/src/core/mainwindow.h index ed4fd85d2..a9a432de0 100644 --- a/src/core/mainwindow.h +++ b/src/core/mainwindow.h @@ -355,6 +355,9 @@ class MainWindow : public QMainWindow, public PlatformInterface { #ifdef HAVE_QOBUZ StreamingTabsView *qobuz_view_; #endif +#ifdef HAVE_DROPBOX + StreamingSongsView *dropbox_view_; +#endif RadioViewContainer *radio_view_; diff --git a/src/core/song.cpp b/src/core/song.cpp index cd8190f93..e0b219565 100644 --- a/src/core/song.cpp +++ b/src/core/song.cpp @@ -1163,6 +1163,8 @@ QString Song::TextForSource(const Source source) { case Source::Qobuz: return u"qobuz"_s; case Source::SomaFM: return u"somafm"_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; } return u"unknown"_s; @@ -1183,6 +1185,8 @@ QString Song::DescriptionForSource(const Source source) { case Source::Qobuz: return u"Qobuz"_s; case Source::SomaFM: return u"SomaFM"_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; } 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("somafm"_L1, Qt::CaseInsensitive) == 0) return Source::SomaFM; 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; @@ -1221,6 +1227,8 @@ QIcon Song::IconForSource(const Source source) { case Source::Qobuz: return IconLoader::Load(u"qobuz"_s); case Source::SomaFM: return IconLoader::Load(u"somafm"_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); } return IconLoader::Load(u"edit-delete"_s); @@ -1470,7 +1478,7 @@ Song::FileType Song::FiletypeByExtension(const QString &ext) { 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; case Source::Device: 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::CDDA: case Source::Stream: case Source::SomaFM: case Source::RadioParadise: + case Source::OneDrive: case Source::Unknown: return StandardPaths::WritableLocation(StandardPaths::StandardLocation::AppLocalDataLocation) + u"/albumcovers"_s; } diff --git a/src/core/song.h b/src/core/song.h index 9c06e54c7..f77a39444 100644 --- a/src/core/song.h +++ b/src/core/song.h @@ -76,7 +76,9 @@ class Song { Qobuz = 8, SomaFM = 9, RadioParadise = 10, - Spotify = 11 + Spotify = 11, + Dropbox = 12, + OneDrive = 13, }; static const int kSourceCount = 16; diff --git a/src/covermanager/albumcoverchoicecontroller.cpp b/src/covermanager/albumcoverchoicecontroller.cpp index a61ec9bc0..952137f85 100644 --- a/src/covermanager/albumcoverchoicecontroller.cpp +++ b/src/covermanager/albumcoverchoicecontroller.cpp @@ -589,6 +589,8 @@ void AlbumCoverChoiceController::SaveArtManualToSong(Song *song, const QUrl &art case Song::Source::Tidal: case Song::Source::Spotify: case Song::Source::Qobuz: + case Song::Source::Dropbox: + case Song::Source::OneDrive: StreamingServicePtr service = streaming_services_->ServiceBySource(song->source()); if (!service) break; if (service->artists_collection_backend()) { diff --git a/src/dropbox/dropboxbaserequest.cpp b/src/dropbox/dropboxbaserequest.cpp new file mode 100644 index 000000000..d3ecbfc2d --- /dev/null +++ b/src/dropbox/dropboxbaserequest.cpp @@ -0,0 +1,132 @@ +/* + * Strawberry Music Player + * Copyright 2025, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include "config.h" + +#include +#include +#include +#include +#include + +#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 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; + +} diff --git a/src/dropbox/dropboxbaserequest.h b/src/dropbox/dropboxbaserequest.h new file mode 100644 index 000000000..62277be2b --- /dev/null +++ b/src/dropbox/dropboxbaserequest.h @@ -0,0 +1,59 @@ +/* + * Strawberry Music Player + * Copyright 2025, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef DROPBOXBASEREQUEST_H +#define DROPBOXBASEREQUEST_H + +#include "config.h" + +#include +#include +#include + +#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 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 diff --git a/src/dropbox/dropboxservice.cpp b/src/dropbox/dropboxservice.cpp new file mode 100644 index 000000000..9231541dc --- /dev/null +++ b/src/dropbox/dropboxservice.cpp @@ -0,0 +1,190 @@ +/* + * Strawberry Music Player + * Copyright 2025, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include +#include +#include + +#include "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 task_manager, + const SharedPtr database, + const SharedPtr network, + const SharedPtr url_handlers, + const SharedPtr tagreader_client, + const SharedPtr 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); + +} diff --git a/src/dropbox/dropboxservice.h b/src/dropbox/dropboxservice.h new file mode 100644 index 000000000..b7f386a59 --- /dev/null +++ b/src/dropbox/dropboxservice.h @@ -0,0 +1,93 @@ +/* + * Strawberry Music Player + * Copyright 2025, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef DROPBOXSERVICE_H +#define DROPBOXSERVICE_H + +#include +#include +#include +#include + +#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 task_manager, + const SharedPtr database, + const SharedPtr network, + const SharedPtr url_handlers, + const SharedPtr tagreader_client, + const SharedPtr 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 network_; + OAuthenticator *oauth_; + DropboxSongsRequest *songs_request_; + bool enabled_; + QList wait_for_exit_; + bool finished_; + uint next_stream_url_request_id_; + QMap> stream_url_requests_; +}; + +#endif // DROPBOXSERVICE_H diff --git a/src/dropbox/dropboxsongsrequest.cpp b/src/dropbox/dropboxsongsrequest.cpp new file mode 100644 index 000000000..c6e3f8517 --- /dev/null +++ b/src/dropbox/dropboxsongsrequest.cpp @@ -0,0 +1,244 @@ +/* + * Strawberry Music Player + * Copyright 2025, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 network, const SharedPtr 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 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); + +} diff --git a/src/dropbox/dropboxsongsrequest.h b/src/dropbox/dropboxsongsrequest.h new file mode 100644 index 000000000..350a14eaa --- /dev/null +++ b/src/dropbox/dropboxsongsrequest.h @@ -0,0 +1,67 @@ +/* + * Strawberry Music Player + * Copyright 2025, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef DROPBOXSONGSREQUEST_H +#define DROPBOXSONGSREQUEST_H + +#include "config.h" + +#include +#include +#include + +#include "dropboxbaserequest.h" + +class NetworkAccessManager; +class CollectionBackend; +class QNetworkReply; +class DropboxService; + +class DropboxSongsRequest : public DropboxBaseRequest { + Q_OBJECT + + public: + explicit DropboxSongsRequest(const SharedPtr network, const SharedPtr 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 network_; + const SharedPtr collection_backend_; + DropboxService *service_; +}; + +#endif // DROPBOXSONGSREQUEST_H diff --git a/src/dropbox/dropboxstreamurlrequest.cpp b/src/dropbox/dropboxstreamurlrequest.cpp new file mode 100644 index 000000000..1c523dadf --- /dev/null +++ b/src/dropbox/dropboxstreamurlrequest.cpp @@ -0,0 +1,129 @@ +/* + * Strawberry Music Player + * Copyright 2025, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include "config.h" + +#include +#include +#include +#include +#include + +#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 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_); + +} diff --git a/src/dropbox/dropboxstreamurlrequest.h b/src/dropbox/dropboxstreamurlrequest.h new file mode 100644 index 000000000..d3542ebe0 --- /dev/null +++ b/src/dropbox/dropboxstreamurlrequest.h @@ -0,0 +1,71 @@ +/* + * Strawberry Music Player + * Copyright 2025, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef DROPBOXSTREAMURLREQUEST_H +#define DROPBOXSTREAMURLREQUEST_H + +#include "config.h" + +#include +#include +#include +#include + +#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 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 network_; + DropboxService *service_; + uint id_; + QUrl media_url_; + QUrl stream_url_; + QNetworkReply *reply_; + bool success_; + QString error_; +}; + +using DropboxStreamURLRequestPtr = QSharedPointer; + +#endif // DROPBOXSTREAMURLREQUEST_H diff --git a/src/dropbox/dropboxurlhandler.cpp b/src/dropbox/dropboxurlhandler.cpp new file mode 100644 index 000000000..96d7a1f60 --- /dev/null +++ b/src/dropbox/dropboxurlhandler.cpp @@ -0,0 +1,76 @@ +/* + * Strawberry Music Player + * Copyright 2025, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include +#include + +#include "includes/shared_ptr.h" +#include "core/taskmanager.h" +#include "dropboxurlhandler.h" +#include "dropboxservice.h" + +DropboxUrlHandler::DropboxUrlHandler(const SharedPtr 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); +} diff --git a/src/dropbox/dropboxurlhandler.h b/src/dropbox/dropboxurlhandler.h new file mode 100644 index 000000000..a3155d18b --- /dev/null +++ b/src/dropbox/dropboxurlhandler.h @@ -0,0 +1,56 @@ +/* + * Strawberry Music Player + * Copyright 2025, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#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 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 task_manager_; + DropboxService *service_; + QMap requests_; +}; + +#endif // DROPBOXURLHANDLER_H diff --git a/src/playlist/playlistbackend.cpp b/src/playlist/playlistbackend.cpp index 4417d3cb3..3c32129cd 100644 --- a/src/playlist/playlistbackend.cpp +++ b/src/playlist/playlistbackend.cpp @@ -56,7 +56,7 @@ using namespace Qt::Literals::StringLiterals; using std::make_shared; namespace { -constexpr int kSongTableJoins = 2; +constexpr int kSongTableJoins = 3; } PlaylistBackend::PlaylistBackend(const SharedPtr database, @@ -186,10 +186,12 @@ PlaylistBackend::Playlist PlaylistBackend::GetPlaylist(const int id) { 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 dropbox_songs ON p.type = dropbox_songs.source AND p.collection_id = dropbox_songs.ROWID " "WHERE p.playlist = :playlist" ).arg(Song::JoinSpec(u"songs"_s), + Song::JoinSpec(u"dropbox_songs"_s), Song::JoinSpec(u"p"_s)); } diff --git a/src/playlist/playlistitem.cpp b/src/playlist/playlistitem.cpp index 928f560e0..0c8497b97 100644 --- a/src/playlist/playlistitem.cpp +++ b/src/playlist/playlistitem.cpp @@ -47,6 +47,8 @@ PlaylistItemPtr PlaylistItem::NewFromSource(const Song::Source source) { switch (source) { case Song::Source::Collection: + case Song::Source::Dropbox: + case Song::Source::OneDrive: return make_shared(source); case Song::Source::Subsonic: case Song::Source::Tidal: @@ -72,6 +74,8 @@ PlaylistItemPtr PlaylistItem::NewFromSong(const Song &song) { switch (song.source()) { case Song::Source::Collection: + case Song::Source::Dropbox: + case Song::Source::OneDrive: return make_shared(song); case Song::Source::Subsonic: case Song::Source::Tidal: diff --git a/src/playlist/songplaylistitem.cpp b/src/playlist/songplaylistitem.cpp index c169e51b9..f8a52ce03 100644 --- a/src/playlist/songplaylistitem.cpp +++ b/src/playlist/songplaylistitem.cpp @@ -34,7 +34,7 @@ SongPlaylistItem::SongPlaylistItem(const Song::Source source) : PlaylistItem(sou SongPlaylistItem::SongPlaylistItem(const Song &song) : PlaylistItem(song.source()), song_(song) {} bool SongPlaylistItem::InitFromQuery(const SqlRow &query) { - song_.InitFromQuery(query, false, static_cast(Song::kRowIdColumns.count())); + song_.InitFromQuery(query, false, static_cast(Song::kRowIdColumns.count() * 2)); return true; } diff --git a/src/playlist/streamplaylistitem.cpp b/src/playlist/streamplaylistitem.cpp index b63002e9b..c0b5f71ef 100644 --- a/src/playlist/streamplaylistitem.cpp +++ b/src/playlist/streamplaylistitem.cpp @@ -47,7 +47,7 @@ void StreamPlaylistItem::InitMetadata() { bool StreamPlaylistItem::InitFromQuery(const SqlRow &query) { - song_.InitFromQuery(query, false, static_cast(Song::kRowIdColumns.count())); + song_.InitFromQuery(query, false, static_cast(Song::kRowIdColumns.count() * 2)); InitMetadata(); return true; diff --git a/src/settings/dropboxsettingspage.cpp b/src/settings/dropboxsettingspage.cpp new file mode 100644 index 000000000..545ce23b3 --- /dev/null +++ b/src/settings/dropboxsettingspage.cpp @@ -0,0 +1,144 @@ +/* + * Strawberry Music Player + * Copyright 2025, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 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(); + +} diff --git a/src/settings/dropboxsettingspage.h b/src/settings/dropboxsettingspage.h new file mode 100644 index 000000000..a5796bf9b --- /dev/null +++ b/src/settings/dropboxsettingspage.h @@ -0,0 +1,58 @@ +/* + * Strawberry Music Player + * Copyright 2025, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef DROPBOXSETTINGSPAGE_H +#define DROPBOXSETTINGSPAGE_H + +#include + +#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 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 service_; +}; + +#endif // DROPBOXSETTINGSPAGE_H diff --git a/src/settings/dropboxsettingspage.ui b/src/settings/dropboxsettingspage.ui new file mode 100644 index 000000000..b5c3f33f8 --- /dev/null +++ b/src/settings/dropboxsettingspage.ui @@ -0,0 +1,125 @@ + + + DropboxSettingsPage + + + + 0 + 0 + 569 + 491 + + + + Dropbox + + + + + + Strawberry can play music that you have uploaded to Dropbox + + + true + + + + + + + Enable + + + + + + + + + + + 28 + + + 0 + + + 0 + + + + + + + Login + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + Reset cursor and songs + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Vertical + + + + 20 + 357 + + + + + + + + + LoginStateWidget + QWidget +
widgets/loginstatewidget.h
+ 1 +
+
+ + +
diff --git a/src/settings/settingsdialog.cpp b/src/settings/settingsdialog.cpp index 33f277cb2..ff142af53 100644 --- a/src/settings/settingsdialog.cpp +++ b/src/settings/settingsdialog.cpp @@ -90,6 +90,10 @@ # include "qobuz/qobuzservice.h" # include "qobuzsettingspage.h" #endif +#ifdef HAVE_DROPBOX +# include "dropbox/dropboxservice.h" +# include "dropboxsettingspage.h" +#endif #include "ui_settingsdialog.h" @@ -144,7 +148,7 @@ SettingsDialog::SettingsDialog(const SharedPtr player, AddPage(Page::Moodbar, new MoodbarSettingsPage(this, this), iface); #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")); #endif @@ -160,6 +164,9 @@ SettingsDialog::SettingsDialog(const SharedPtr player, #ifdef HAVE_QOBUZ AddPage(Page::Qobuz, new QobuzSettingsPage(this, streaming_services->Service(), this), streaming); #endif +#ifdef HAVE_DROPBOX + AddPage(Page::Dropbox, new DropboxSettingsPage(this, streaming_services->Service(), this), streaming); +#endif // List box QObject::connect(ui_->list, &QTreeWidget::currentItemChanged, this, &SettingsDialog::CurrentItemChanged); diff --git a/src/settings/settingsdialog.h b/src/settings/settingsdialog.h index 8723ca193..8760a8a62 100644 --- a/src/settings/settingsdialog.h +++ b/src/settings/settingsdialog.h @@ -93,6 +93,8 @@ class SettingsDialog : public QDialog { Tidal, Qobuz, Spotify, + Dropbox, + OneDrive, }; enum Role { diff --git a/src/streaming/cloudstoragestreamingservice.cpp b/src/streaming/cloudstoragestreamingservice.cpp new file mode 100644 index 000000000..bcdb88912 --- /dev/null +++ b/src/streaming/cloudstoragestreamingservice.cpp @@ -0,0 +1,134 @@ +/* + * Strawberry Music Player + * Copyright 2025, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#include + +#include +#include +#include + +#include "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 task_manager, + const SharedPtr database, + const SharedPtr tagreader_client, + const SharedPtr 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(); + 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 connection = make_shared(); + *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(); + +} diff --git a/src/streaming/cloudstoragestreamingservice.h b/src/streaming/cloudstoragestreamingservice.h new file mode 100644 index 000000000..0975484a0 --- /dev/null +++ b/src/streaming/cloudstoragestreamingservice.h @@ -0,0 +1,89 @@ +/* + * Strawberry Music Player + * Copyright 2025, Jonas Kvinge + * + * Strawberry is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Strawberry is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Strawberry. If not, see . + * + */ + +#ifndef CLOUDSTORAGESTREAMINGSERVICE_H +#define CLOUDSTORAGESTREAMINGSERVICE_H + +#include + +#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 task_manager, + const SharedPtr database, + const SharedPtr tagreader_client, + const SharedPtr 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 collection_backend() const { return collection_backend_; } + CollectionModel *collection_model() const { return collection_model_; } + CollectionFilter *collection_filter_model() const { return collection_model_->filter(); } + + SharedPtr 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 task_manager_; + const SharedPtr tagreader_client_; + SharedPtr collection_backend_; + CollectionModel *collection_model_; + QList pending_tagreader_replies_; + + private: + Song::Source source_; + int indexing_task_id_; + int indexing_task_progress_; + int indexing_task_max_; +}; + +#endif // CLOUDSTORAGESTREAMINGSERVICE_H diff --git a/src/utilities/coverutils.cpp b/src/utilities/coverutils.cpp index 4b3deaf4e..a0cfba920 100644 --- a/src/utilities/coverutils.cpp +++ b/src/utilities/coverutils.cpp @@ -142,6 +142,8 @@ QString CoverUtils::CoverFilenameFromSource(const Song::Source source, const QUr case Song::Source::Stream: case Song::Source::SomaFM: case Song::Source::RadioParadise: + case Song::Source::Dropbox: + case Song::Source::OneDrive: case Song::Source::Unknown: filename = QString::fromLatin1(Sha1CoverHash(artist, album).toHex()); break;