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 000000000..2c8f4a56a Binary files /dev/null and b/data/icons/128x128/dropbox.png differ diff --git a/data/icons/22x22/dropbox.png b/data/icons/22x22/dropbox.png new file mode 100644 index 000000000..5ea711c59 Binary files /dev/null and b/data/icons/22x22/dropbox.png differ diff --git a/data/icons/32x32/dropbox.png b/data/icons/32x32/dropbox.png new file mode 100644 index 000000000..96e051f51 Binary files /dev/null and b/data/icons/32x32/dropbox.png differ diff --git a/data/icons/48x48/dropbox.png b/data/icons/48x48/dropbox.png new file mode 100644 index 000000000..a105e85e8 Binary files /dev/null and b/data/icons/48x48/dropbox.png differ diff --git a/data/icons/64x64/dropbox.png b/data/icons/64x64/dropbox.png new file mode 100644 index 000000000..7042c3fd4 Binary files /dev/null and b/data/icons/64x64/dropbox.png differ diff --git a/data/icons/full/dropbox.png b/data/icons/full/dropbox.png new file mode 100644 index 000000000..728ecc184 Binary files /dev/null and b/data/icons/full/dropbox.png differ 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;