From 70e847ce3626a1cfd29acdc01f258e3175bfefa5 Mon Sep 17 00:00:00 2001 From: emeric Date: Wed, 10 Jun 2020 16:35:13 +0200 Subject: [PATCH 01/12] Provide fonts to prevent them from downloading. fixes #64 --- docroot/css/S6u8w4BMUTPHjxsAUi-qJCY.woff2 | Bin 0 -> 5536 bytes docroot/css/S6u8w4BMUTPHjxsAXC-q.woff2 | Bin 0 -> 24440 bytes docroot/css/S6u9w4BMUTPHh6UVSwaPGR_p.woff2 | Bin 0 -> 5360 bytes docroot/css/S6u9w4BMUTPHh6UVSwiPGQ.woff2 | Bin 0 -> 22992 bytes docroot/css/S6uyw4BMUTPHjx4wXg.woff2 | Bin 0 -> 23484 bytes docroot/css/S6uyw4BMUTPHjxAwXjeu.woff2 | Bin 0 -> 5480 bytes docroot/css/bootstrap-darkly.min.css | 2 +- docroot/css/bootstrap-flatly.min.css | 2 +- docroot/css/fonts.css | 54 +++++++++++++++++++++ src/lms/ui/LmsTheme.cpp | 2 + 10 files changed, 58 insertions(+), 2 deletions(-) create mode 100755 docroot/css/S6u8w4BMUTPHjxsAUi-qJCY.woff2 create mode 100755 docroot/css/S6u8w4BMUTPHjxsAXC-q.woff2 create mode 100755 docroot/css/S6u9w4BMUTPHh6UVSwaPGR_p.woff2 create mode 100755 docroot/css/S6u9w4BMUTPHh6UVSwiPGQ.woff2 create mode 100755 docroot/css/S6uyw4BMUTPHjx4wXg.woff2 create mode 100755 docroot/css/S6uyw4BMUTPHjxAwXjeu.woff2 create mode 100755 docroot/css/fonts.css diff --git a/docroot/css/S6u8w4BMUTPHjxsAUi-qJCY.woff2 b/docroot/css/S6u8w4BMUTPHjxsAUi-qJCY.woff2 new file mode 100755 index 0000000000000000000000000000000000000000..69af6c080dc691492934245dcb2e51e9d97fcd16 GIT binary patch literal 5536 zcmV;R6<_LiPew8T0RR9102QDB5C8xG050$V02N070RR9100000000000000000000 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 literal 0 HcmV?d00001 diff --git a/docroot/css/S6u8w4BMUTPHjxsAXC-q.woff2 b/docroot/css/S6u8w4BMUTPHjxsAXC-q.woff2 new file mode 100755 index 0000000000000000000000000000000000000000..3246c1286f3cf243137975c5108ce2241aa91792 GIT binary patch literal 24440 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 OPVO#>#@zM-0000qd`Ywb literal 0 HcmV?d00001 diff --git a/docroot/css/S6u9w4BMUTPHh6UVSwiPGQ.woff2 b/docroot/css/S6u9w4BMUTPHh6UVSwiPGQ.woff2 new file mode 100755 index 0000000000000000000000000000000000000000..f8c37ecde4dafa44bb57a4f73937af5a94b18284 GIT binary patch literal 22992 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 literal 0 HcmV?d00001 diff --git a/docroot/css/S6uyw4BMUTPHjx4wXg.woff2 b/docroot/css/S6uyw4BMUTPHjx4wXg.woff2 new file mode 100755 index 0000000000000000000000000000000000000000..6904b6649ffa9ae838ec975c22159a5352ab92a9 GIT binary patch literal 23484 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 literal 0 HcmV?d00001 diff --git a/docroot/css/bootstrap-darkly.min.css b/docroot/css/bootstrap-darkly.min.css index 68048ef2..afed1509 100755 --- a/docroot/css/bootstrap-darkly.min.css +++ b/docroot/css/bootstrap-darkly.min.css @@ -1,4 +1,4 @@ -@import url("https://fonts.googleapis.com/css?family=Lato:400,700,400italic&display=swap");/*! +/*! * bootswatch v3.4.1 * Homepage: http://bootswatch.com * Copyright 2012-2019 Thomas Park diff --git a/docroot/css/bootstrap-flatly.min.css b/docroot/css/bootstrap-flatly.min.css index 7d99e1b6..14ea2fc1 100755 --- a/docroot/css/bootstrap-flatly.min.css +++ b/docroot/css/bootstrap-flatly.min.css @@ -1,4 +1,4 @@ -@import url("https://fonts.googleapis.com/css?family=Lato:400,700,400italic&display=swap");/*! +/*! * bootswatch v3.4.1 * Homepage: http://bootswatch.com * Copyright 2012-2019 Thomas Park diff --git a/docroot/css/fonts.css b/docroot/css/fonts.css new file mode 100755 index 00000000..f2f53989 --- /dev/null +++ b/docroot/css/fonts.css @@ -0,0 +1,54 @@ +/* latin-ext */ +@font-face { + font-family: 'Lato'; + font-style: italic; + font-weight: 400; + font-display: swap; + src: local('Lato Italic'), local('Lato-Italic'), url(S6u8w4BMUTPHjxsAUi-qJCY.woff2) format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Lato'; + font-style: italic; + font-weight: 400; + font-display: swap; + src: local('Lato Italic'), local('Lato-Italic'), url(S6u8w4BMUTPHjxsAXC-q.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* latin-ext */ +@font-face { + font-family: 'Lato'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: local('Lato Regular'), local('Lato-Regular'), url(S6uyw4BMUTPHjxAwXjeu.woff2) format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Lato'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: local('Lato Regular'), local('Lato-Regular'), url(S6uyw4BMUTPHjx4wXg.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} +/* latin-ext */ +@font-face { + font-family: 'Lato'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: local('Lato Bold'), local('Lato-Bold'), url(S6u9w4BMUTPHh6UVSwaPGR_p.woff2) format('woff2'); + unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} +/* latin */ +@font-face { + font-family: 'Lato'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: local('Lato Bold'), local('Lato-Bold'), url(S6u9w4BMUTPHh6UVSwiPGQ.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} diff --git a/src/lms/ui/LmsTheme.cpp b/src/lms/ui/LmsTheme.cpp index 6c6c97a2..d56cbe5a 100644 --- a/src/lms/ui/LmsTheme.cpp +++ b/src/lms/ui/LmsTheme.cpp @@ -66,6 +66,7 @@ LmsTheme::getStyleSheets(Database::User::UITheme theme) case Database::User::UITheme::Dark: return { + {"css/fonts.css"}, {"css/bootstrap-darkly.min.css"}, {"resources/themes/bootstrap/3/wt.css"}, {"css/lms.css"}, @@ -75,6 +76,7 @@ LmsTheme::getStyleSheets(Database::User::UITheme theme) case Database::User::UITheme::Light: return { + {"css/fonts.css"}, {"css/bootstrap-flatly.min.css"}, {"resources/themes/bootstrap/3/wt.css"}, {"css/lms.css"}, From 73acd65bb35feabecd9057db0c7ffe717beec2f0 Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 20 Jun 2020 14:31:53 +0200 Subject: [PATCH 02/12] Subsonic API: added the getScanStatus and startScan commands. fixes #62 --- src/libs/scanner/impl/MediaScanner.cpp | 22 ++++--- src/libs/scanner/impl/MediaScanner.hpp | 28 ++++---- .../scanner/include/scanner/IMediaScanner.hpp | 3 + src/libs/subsonic/CMakeLists.txt | 2 + src/libs/subsonic/impl/Scan.cpp | 65 +++++++++++++++++++ src/libs/subsonic/impl/Scan.hpp | 30 +++++++++ src/libs/subsonic/impl/SubsonicResource.cpp | 5 +- src/lms/ui/LmsApplication.cpp | 10 +++ src/lms/ui/LmsApplication.hpp | 1 + src/lms/ui/admin/DatabaseSettingsView.cpp | 1 - src/lms/ui/admin/DatabaseStatus.cpp | 4 ++ 11 files changed, 146 insertions(+), 25 deletions(-) create mode 100644 src/libs/subsonic/impl/Scan.cpp create mode 100644 src/libs/subsonic/impl/Scan.hpp diff --git a/src/libs/scanner/impl/MediaScanner.cpp b/src/libs/scanner/impl/MediaScanner.cpp index 36a3e659..f2b5a70a 100644 --- a/src/libs/scanner/impl/MediaScanner.cpp +++ b/src/libs/scanner/impl/MediaScanner.cpp @@ -310,7 +310,7 @@ MediaScanner::getStatus() { Status res; - std::unique_lock lock {_statusMutex}; + std::shared_lock lock {_statusMutex}; res.currentState = _curState; res.nextScheduledScan = _nextScheduledScan; @@ -367,7 +367,7 @@ MediaScanner::scheduleNextScan() } { - std::unique_lock lock {_statusMutex}; + std::unique_lock lock {_statusMutex}; _curState = nextScanDateTime.isValid() ? State::Scheduled : State::NotScheduled; _nextScheduledScan = nextScanDateTime; } @@ -421,8 +421,10 @@ MediaScanner::scan(boost::system::error_code err) if (err) return; + scanStarted().emit(); + { - std::unique_lock lock {_statusMutex}; + std::unique_lock lock {_statusMutex}; _curState = State::InProgress; _nextScheduledScan = {}; } @@ -464,7 +466,7 @@ MediaScanner::scan(boost::system::error_code err) { stats.stopTime = Wt::WLocalDateTime::currentDateTime().toUTC(); { - std::unique_lock lock {_statusMutex}; + std::unique_lock lock {_statusMutex}; _lastCompleteScanStats = std::move(stats); _inProgressScanStats.reset(); @@ -476,7 +478,7 @@ MediaScanner::scan(boost::system::error_code err) } else { - std::unique_lock lock {_statusMutex}; + std::unique_lock lock {_statusMutex}; _curState = State::NotScheduled; _inProgressScanStats.reset(); @@ -584,13 +586,15 @@ MediaScanner::refreshScanSettings() void MediaScanner::notifyInProgress(const ScanStats& stats) { + const ScanProgressStats progressStats {stats.toProgressStats()}; + { - std::unique_lock lock {_statusMutex}; - _inProgressScanStats = stats.toProgressStats(); + std::unique_lock lock {_statusMutex}; + _inProgressScanStats = progressStats; } - std::chrono::system_clock::time_point now {std::chrono::system_clock::now()}; - _sigScanInProgress(*_inProgressScanStats); + const std::chrono::system_clock::time_point now {std::chrono::system_clock::now()}; + _sigScanInProgress(progressStats); _lastScanInProgressEmit = now; } diff --git a/src/libs/scanner/impl/MediaScanner.hpp b/src/libs/scanner/impl/MediaScanner.hpp index a04ceb39..8c19defb 100644 --- a/src/libs/scanner/impl/MediaScanner.hpp +++ b/src/libs/scanner/impl/MediaScanner.hpp @@ -20,7 +20,7 @@ #pragma once #include -#include +#include #include #include @@ -59,6 +59,7 @@ class MediaScanner : public IMediaScanner Status getStatus() override; + Wt::Signal<>& scanStarted() override { return _sigScanStarted; } Wt::Signal<>& scanComplete() override { return _sigScanComplete; } Wt::Signal& scanInProgress() override { return _sigScanInProgress; } Wt::Signal& scheduled() override { return _sigScheduled; } @@ -88,21 +89,22 @@ class MediaScanner : public IMediaScanner void notifyInProgressIfNeeded(const ScanStats& stats); void notifyInProgress(const ScanStats& stats); - bool _running {}; - Wt::WIOService _ioService; - boost::asio::system_timer _scheduleTimer {_ioService}; - Wt::Signal<> _sigScanComplete; - Wt::Signal _sigScanInProgress; + bool _running {}; + Wt::WIOService _ioService; + boost::asio::system_timer _scheduleTimer {_ioService}; + Wt::Signal<> _sigScanStarted; + Wt::Signal<> _sigScanComplete; + Wt::Signal _sigScanInProgress; std::chrono::system_clock::time_point _lastScanInProgressEmit {}; - Wt::Signal _sigScheduled; - Database::Session _dbSession; - std::unique_ptr _metadataParser; + Wt::Signal _sigScheduled; + Database::Session _dbSession; + std::unique_ptr _metadataParser; - std::mutex _statusMutex; - State _curState {State::NotScheduled}; - std::optional _lastCompleteScanStats; + std::shared_mutex _statusMutex; + State _curState {State::NotScheduled}; + std::optional _lastCompleteScanStats; std::optional _inProgressScanStats; - Wt::WDateTime _nextScheduledScan; + Wt::WDateTime _nextScheduledScan; // Current scan settings std::size_t _scanVersion {}; diff --git a/src/libs/scanner/include/scanner/IMediaScanner.hpp b/src/libs/scanner/include/scanner/IMediaScanner.hpp index e50be005..1c0d3fdc 100644 --- a/src/libs/scanner/include/scanner/IMediaScanner.hpp +++ b/src/libs/scanner/include/scanner/IMediaScanner.hpp @@ -64,6 +64,9 @@ class IMediaScanner virtual Status getStatus() = 0; + // Called just after scan start + virtual Wt::Signal<>& scanStarted() = 0; + // Called just after scan complete virtual Wt::Signal<>& scanComplete() = 0; diff --git a/src/libs/subsonic/CMakeLists.txt b/src/libs/subsonic/CMakeLists.txt index 34d3d68d..439e6289 100644 --- a/src/libs/subsonic/CMakeLists.txt +++ b/src/libs/subsonic/CMakeLists.txt @@ -1,6 +1,7 @@ add_library(lmssubsonic SHARED impl/ParameterParsing.cpp + impl/Scan.cpp impl/Stream.cpp impl/SubsonicId.cpp impl/SubsonicResource.cpp @@ -26,6 +27,7 @@ target_link_libraries(lmssubsonic PRIVATE target_link_libraries(lmssubsonic PUBLIC lmsdatabase + lmsscanner wt ) diff --git a/src/libs/subsonic/impl/Scan.cpp b/src/libs/subsonic/impl/Scan.cpp new file mode 100644 index 00000000..2e359c5a --- /dev/null +++ b/src/libs/subsonic/impl/Scan.cpp @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2020 Emeric Poupon + * + * This file is part of LMS. + * + * LMS 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. + * + * LMS 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 LMS. If not, see . + */ + +#include "Scan.hpp" + +#include "scanner/IMediaScanner.hpp" +#include "utils/Service.hpp" + +namespace API::Subsonic::Scan +{ + using namespace Scanner; + + static + Response::Node + createStatusResponseNode() + { + Response::Node statusResponse; + + const IMediaScanner::Status scanStatus {ServiceProvider::get()->getStatus()}; + + statusResponse.setAttribute("scanning", scanStatus.currentState == IMediaScanner::State::InProgress ? "true" : "false"); + if (scanStatus.currentState == IMediaScanner::State::InProgress && scanStatus.inProgressScanStats) + statusResponse.setAttribute("count", std::to_string(scanStatus.inProgressScanStats->processedFiles)); + + return statusResponse; + } + + + Response + handleGetScanStatus(RequestContext&) + { + Response response {Response::createOkResponse()}; + response.addNode("scanStatus", createStatusResponseNode()); + + return response; + } + + Response + handleStartScan(RequestContext&) + { + ServiceProvider::get()->requestImmediateScan(); + + Response response {Response::createOkResponse()}; + response.addNode("scanStatus", createStatusResponseNode()); + + return response; + } +} + diff --git a/src/libs/subsonic/impl/Scan.hpp b/src/libs/subsonic/impl/Scan.hpp new file mode 100644 index 00000000..3074a7b5 --- /dev/null +++ b/src/libs/subsonic/impl/Scan.hpp @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2020 Emeric Poupon + * + * This file is part of LMS. + * + * LMS 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. + * + * LMS 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 LMS. If not, see . + */ + +#pragma once + +#include "RequestContext.hpp" +#include "SubsonicResponse.hpp" + +namespace API::Subsonic::Scan +{ + Response handleGetScanStatus(RequestContext& context); + Response handleStartScan(RequestContext& context); +} + diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index a78d028a..0adefb39 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -42,6 +42,7 @@ #include "utils/Utils.hpp" #include "ParameterParsing.hpp" #include "RequestContext.hpp" +#include "Scan.hpp" #include "Stream.hpp" #include "SubsonicResponse.hpp" @@ -1860,8 +1861,8 @@ static std::unordered_map requestEntryPoints {"savePlayQueue", {handleNotImplemented, false}}, // Media library scanning - {"getScanStatus", {handleNotImplemented, true}}, - {"startScan", {handleNotImplemented, true}}, + {"getScanStatus", {Scan::handleGetScanStatus, true}}, + {"startScan", {Scan::handleStartScan, true}}, }; using MediaRetrievalHandlerFunc = std::function; diff --git a/src/lms/ui/LmsApplication.cpp b/src/lms/ui/LmsApplication.cpp index 89cebb24..86fc7a15 100644 --- a/src/lms/ui/LmsApplication.cpp +++ b/src/lms/ui/LmsApplication.cpp @@ -523,6 +523,16 @@ LmsApplication::createHome() // Events from MediaScanner { const std::string sessionId {LmsApp->sessionId()}; + + ServiceProvider::get()->scanStarted().connect(this, [=] () + { + Wt::WServer::instance()->post(sessionId, [=] + { + _events.dbScanStarted.emit(); + triggerUpdate(); + }); + }); + ServiceProvider::get()->scanComplete().connect(this, [=] () { Wt::WServer::instance()->post(sessionId, [=] diff --git a/src/lms/ui/LmsApplication.hpp b/src/lms/ui/LmsApplication.hpp index 20cac9b7..2b3b4534 100644 --- a/src/lms/ui/LmsApplication.hpp +++ b/src/lms/ui/LmsApplication.hpp @@ -55,6 +55,7 @@ struct Events Wt::Signal appClosed; // Database events + Wt::Signal<> dbScanStarted; Wt::Signal<> dbScanned; Wt::Signal dbScanInProgress; Wt::Signal dbScanScheduled; diff --git a/src/lms/ui/admin/DatabaseSettingsView.cpp b/src/lms/ui/admin/DatabaseSettingsView.cpp index 323dfa7c..5b612ff6 100644 --- a/src/lms/ui/admin/DatabaseSettingsView.cpp +++ b/src/lms/ui/admin/DatabaseSettingsView.cpp @@ -248,7 +248,6 @@ DatabaseSettingsView::refreshView() immScanBtn->clicked().connect([=] () { ServiceProvider::get()->requestImmediateScan(); - LmsApp->notifyMsg(MsgType::Info, Wt::WString::tr("Lms.Admin.Database.scan-launched")); }); t->updateView(model.get()); diff --git a/src/lms/ui/admin/DatabaseStatus.cpp b/src/lms/ui/admin/DatabaseStatus.cpp index 34133e01..1030c452 100644 --- a/src/lms/ui/admin/DatabaseStatus.cpp +++ b/src/lms/ui/admin/DatabaseStatus.cpp @@ -121,6 +121,10 @@ DatabaseStatus::DatabaseStatus() auto onDbEvent = [&]() { refreshContents(); }; + LmsApp->getEvents().dbScanStarted.connect(this, []() + { + LmsApp->notifyMsg(MsgType::Info, Wt::WString::tr("Lms.Admin.Database.scan-launched")); + }); LmsApp->getEvents().dbScanned.connect(this, onDbEvent); LmsApp->getEvents().dbScanInProgress.connect(this, onDbEvent); LmsApp->getEvents().dbScanScheduled.connect(this, onDbEvent); From 03446188ba6ab14b5ccd5322292f0868f3ccacb0 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 21 Jun 2020 14:25:23 +0200 Subject: [PATCH 03/12] Subsonic API: do not report numeric values as strings --- src/libs/subsonic/impl/Scan.cpp | 2 +- src/libs/subsonic/impl/SubsonicResource.cpp | 26 +++++------ src/libs/subsonic/impl/SubsonicResponse.cpp | 51 +++++++++++++++++---- src/libs/subsonic/impl/SubsonicResponse.hpp | 7 ++- 4 files changed, 60 insertions(+), 26 deletions(-) diff --git a/src/libs/subsonic/impl/Scan.cpp b/src/libs/subsonic/impl/Scan.cpp index 2e359c5a..544f71dd 100644 --- a/src/libs/subsonic/impl/Scan.cpp +++ b/src/libs/subsonic/impl/Scan.cpp @@ -36,7 +36,7 @@ namespace API::Subsonic::Scan statusResponse.setAttribute("scanning", scanStatus.currentState == IMediaScanner::State::InProgress ? "true" : "false"); if (scanStatus.currentState == IMediaScanner::State::InProgress && scanStatus.inProgressScanStats) - statusResponse.setAttribute("count", std::to_string(scanStatus.inProgressScanStats->processedFiles)); + statusResponse.setAttribute("count", scanStatus.inProgressScanStats->processedFiles); return statusResponse; } diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index 0adefb39..ad53f93d 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -290,18 +290,18 @@ trackToResponseNode(const Track::pointer& track, Session& dbSession, const User: trackResponse.setAttribute("isDir", "false"); trackResponse.setAttribute("title", track->getName()); if (track->getTrackNumber()) - trackResponse.setAttribute("track", std::to_string(*track->getTrackNumber())); + trackResponse.setAttribute("track", *track->getTrackNumber()); if (track->getDiscNumber()) - trackResponse.setAttribute("discNumber", std::to_string(*track->getDiscNumber())); + trackResponse.setAttribute("discNumber", *track->getDiscNumber()); if (track->getYear()) - trackResponse.setAttribute("year", std::to_string(*track->getYear())); + trackResponse.setAttribute("year", *track->getYear()); trackResponse.setAttribute("path", getTrackPath(track)); { std::error_code ec; const auto fileSize {std::filesystem::file_size(track->getPath(), ec)}; if (!ec) - trackResponse.setAttribute("size", std::to_string(fileSize)); + trackResponse.setAttribute("size", fileSize); } if (track->getPath().has_extension()) @@ -331,7 +331,7 @@ trackToResponseNode(const Track::pointer& track, Session& dbSession, const User: trackResponse.setAttribute("parent", IdToString({Id::Type::Release, track->getRelease().id()})); } - trackResponse.setAttribute("duration", std::to_string(std::chrono::duration_cast(track->getDuration()).count())); + trackResponse.setAttribute("duration", std::chrono::duration_cast(track->getDuration()).count()); trackResponse.setAttribute("type", "music"); if (user->hasStarredTrack(track)) @@ -355,7 +355,7 @@ trackBookmarkToResponseNode(const TrackBookmark::pointer& trackBookmark) { Response::Node trackBookmarkNode; - trackBookmarkNode.setAttribute("position", std::to_string(trackBookmark->getOffset().count())); + trackBookmarkNode.setAttribute("position", trackBookmark->getOffset().count()); if (!trackBookmark->getComment().empty()) trackBookmarkNode.setAttribute("comment", trackBookmark->getComment()); trackBookmarkNode.setAttribute("created", reportedCreatedBookmarkDate); @@ -374,7 +374,7 @@ releaseToResponseNode(const Release::pointer& release, Session& dbSession, const if (id3) { albumNode.setAttribute("name", release->getName()); - albumNode.setAttribute("songCount", std::to_string(release->getTracksCount())); + albumNode.setAttribute("songCount", release->getTracksCount()); albumNode.setAttribute("duration", std::to_string(std::chrono::duration_cast(release->getDuration()).count())); } else @@ -387,7 +387,7 @@ releaseToResponseNode(const Release::pointer& release, Session& dbSession, const albumNode.setAttribute("coverArt", IdToString({Id::Type::Release, release.id()})); auto releaseYear {release->getReleaseYear()}; if (releaseYear) - albumNode.setAttribute("year", std::to_string(*releaseYear)); + albumNode.setAttribute("year", *releaseYear); auto artists {release->getReleaseArtists()}; if (artists.empty()) @@ -443,7 +443,7 @@ artistToResponseNode(const User::pointer& user, const Artist::pointer& artist, b artistNode.setAttribute("name", artist->getName()); if (id3) - artistNode.setAttribute("albumCount", std::to_string(artist->getReleaseCount())); + artistNode.setAttribute("albumCount", artist->getReleaseCount()); if (user->hasStarredArtist(artist)) artistNode.setAttribute("starred", reportedStarredDate); @@ -458,8 +458,8 @@ clusterToResponseNode(const Cluster::pointer& cluster) Response::Node clusterNode; clusterNode.setValue(cluster->getName()); - clusterNode.setAttribute("songCount", std::to_string(cluster->getTracksCount())); - clusterNode.setAttribute("albumCount", std::to_string(cluster->getReleasesCount())); + clusterNode.setAttribute("songCount", cluster->getTracksCount()); + clusterNode.setAttribute("albumCount", cluster->getReleasesCount()); return clusterNode; } @@ -1208,8 +1208,8 @@ tracklistToResponseNode(const TrackList::pointer& tracklist, Session&) playlistNode.setAttribute("id", IdToString({Id::Type::Playlist, tracklist.id()})); playlistNode.setAttribute("name", tracklist->getName()); - playlistNode.setAttribute("songCount", std::to_string(tracklist->getCount())); - playlistNode.setAttribute("duration", std::to_string(std::chrono::duration_cast(tracklist->getDuration()).count())); + playlistNode.setAttribute("songCount", tracklist->getCount()); + playlistNode.setAttribute("duration", std::chrono::duration_cast(tracklist->getDuration()).count()); playlistNode.setAttribute("public", tracklist->isPublic() ? "true" : "false"); playlistNode.setAttribute("created", ""); playlistNode.setAttribute("owner", tracklist->getUser()->getLoginName()); diff --git a/src/libs/subsonic/impl/SubsonicResponse.cpp b/src/libs/subsonic/impl/SubsonicResponse.cpp index b6b2431d..893be143 100644 --- a/src/libs/subsonic/impl/SubsonicResponse.cpp +++ b/src/libs/subsonic/impl/SubsonicResponse.cpp @@ -46,6 +46,15 @@ ResponseFormatToMimeType(ResponseFormat format) void Response::Node::setValue(std::string_view value) +{ + if (!_children.empty() || !_childrenArrays.empty()) + throw LmsException {"Node already has children"}; + + _value = std::string {value}; +} + +void +Response::Node::setValue(long long value) { if (!_children.empty() || !_childrenArrays.empty()) throw LmsException {"Node already has children"}; @@ -55,6 +64,12 @@ Response::Node::setValue(std::string_view value) void Response::Node::setAttribute(std::string_view key, std::string_view value) +{ + _attributes[std::string {key}] = std::string {value}; +} + +void +Response::Node::setAttribute(std::string_view key, long long value) { _attributes[std::string {key}] = value; } @@ -62,7 +77,7 @@ Response::Node::setAttribute(std::string_view key, std::string_view value) void Response::Node::addChild(const std::string& key, Node node) { - if (!_value.empty()) + if (_value.valueless_by_exception()) throw LmsException {"Node already has a value"}; _children[key].emplace_back(std::move(node)); @@ -71,7 +86,7 @@ Response::Node::addChild(const std::string& key, Node node) void Response::Node::addArrayChild(const std::string& key, Node node) { - if (!_value.empty()) + if (_value.valueless_by_exception()) throw LmsException {"Node already has a value"}; _childrenArrays[key].emplace_back(std::move(node)); @@ -160,11 +175,19 @@ Response::writeXML(std::ostream& os) boost::property_tree::ptree res; for (auto itAttribute : node._attributes) - res.put("." + itAttribute.first, itAttribute.second); - - if (!node._value.empty()) { - res.put_value(node._value); + if (std::holds_alternative(itAttribute.second)) + res.put("." + itAttribute.first, std::get(itAttribute.second)); + else if (std::holds_alternative(itAttribute.second)) + res.put("." + itAttribute.first, std::get(itAttribute.second)); + } + + if (node._value.valueless_by_exception()) + { + if (std::holds_alternative(node._value)) + res.put_value(std::get(node._value)); + else if (std::holds_alternative(node._value)) + res.put_value(std::get(node._value)); } else { @@ -200,11 +223,19 @@ Response::writeJSON(std::ostream& os) Json::Object res; for (auto itAttribute : node._attributes) - res[itAttribute.first] = Json::Value {itAttribute.second}; - - if (!node._value.empty()) { - res["value"] = Json::Value {node._value}; + if (std::holds_alternative(itAttribute.second)) + res[itAttribute.first] = Json::Value {std::get(itAttribute.second)}; + else if (std::holds_alternative(itAttribute.second)) + res[itAttribute.first] = Json::Value {std::get(itAttribute.second)}; + } + + if (node._value.valueless_by_exception()) + { + if (std::holds_alternative(node._value)) + res["value"] = Json::Value {std::get(node._value)}; + else if (std::holds_alternative(node._value)) + res["value"] = Json::Value {std::get(node._value)}; } else { diff --git a/src/libs/subsonic/impl/SubsonicResponse.hpp b/src/libs/subsonic/impl/SubsonicResponse.hpp index a88217ad..8354cbf2 100644 --- a/src/libs/subsonic/impl/SubsonicResponse.hpp +++ b/src/libs/subsonic/impl/SubsonicResponse.hpp @@ -21,6 +21,7 @@ #include #include #include +#include #include @@ -181,9 +182,11 @@ class Response { public: void setAttribute(std::string_view key, std::string_view value); + void setAttribute(std::string_view key, long long value); // A Node has either a value or some children void setValue(std::string_view value); + void setValue(long long value); Node& createChild(const std::string& key); Node& createArrayChild(const std::string& key); @@ -192,8 +195,8 @@ class Response private: friend class Response; - std::map _attributes; - std::string _value; + std::map> _attributes; + std::variant _value; std::map> _children; std::map> _childrenArrays; }; From bd84f41b07c4db4b82be32aeb2ddfd40679f086c Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 21 Jun 2020 14:29:36 +0200 Subject: [PATCH 04/12] Added missing license file. fixes #70 --- COPYING | 674 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 674 insertions(+) create mode 100755 COPYING diff --git a/COPYING b/COPYING new file mode 100755 index 00000000..f288702d --- /dev/null +++ b/COPYING @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program 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. + + This program 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 this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. From a293273940923e8c5b988c794805a0d13244c45f Mon Sep 17 00:00:00 2001 From: emeric Date: Tue, 23 Jun 2020 11:26:48 +0200 Subject: [PATCH 05/12] Bring some more diversity in the radio mode when using tags --- src/libs/database/impl/Artist.cpp | 2 +- src/libs/database/impl/Release.cpp | 2 +- src/libs/database/impl/Track.cpp | 2 +- src/libs/database/impl/TrackList.cpp | 2 +- src/test/database/DatabaseTest.cpp | 8 +++++++- 5 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/libs/database/impl/Artist.cpp b/src/libs/database/impl/Artist.cpp index 7f2302f7..267c0eee 100644 --- a/src/libs/database/impl/Artist.cpp +++ b/src/libs/database/impl/Artist.cpp @@ -464,7 +464,7 @@ Artist::getSimilarArtists(std::optional offset, std::optionalid()) .bind(self()->id()) .groupBy("a.id") - .orderBy("COUNT(*) DESC") + .orderBy("COUNT(*) DESC, RANDOM()") .limit(count ? static_cast(*count) : -1) .offset(offset ? static_cast(*offset) : -1)}; diff --git a/src/libs/database/impl/Release.cpp b/src/libs/database/impl/Release.cpp index 23646eaa..a50262ae 100644 --- a/src/libs/database/impl/Release.cpp +++ b/src/libs/database/impl/Release.cpp @@ -426,7 +426,7 @@ Release::getSimilarReleases(std::optional offset, std::optionalid()) .bind(self()->id()) .groupBy("r.id") - .orderBy("COUNT(*) DESC") + .orderBy("COUNT(*) DESC, RANDOM()") .limit(count ? static_cast(*count) : -1) .offset(offset ? static_cast(*offset) : -1)}; diff --git a/src/libs/database/impl/Track.cpp b/src/libs/database/impl/Track.cpp index 26dcc37f..04a16f1f 100644 --- a/src/libs/database/impl/Track.cpp +++ b/src/libs/database/impl/Track.cpp @@ -341,7 +341,7 @@ Track::getSimilarTracks(Session& session, " AND t_c.cluster_id IN (SELECT c.id FROM cluster c INNER JOIN track_cluster t_c ON t_c.cluster_id = c.id WHERE t_c.track_id IN (" + oss.str() + "))" " AND t.id NOT IN (" + oss.str() + ")") .groupBy("t.id") - .orderBy("COUNT(*) DESC") + .orderBy("COUNT(*) DESC, RANDOM()") .limit(size ? static_cast(*size) : -1) .offset(offset ? static_cast(*offset) : -1)}; diff --git a/src/libs/database/impl/TrackList.cpp b/src/libs/database/impl/TrackList.cpp index 47fb5a22..03a20d97 100644 --- a/src/libs/database/impl/TrackList.cpp +++ b/src/libs/database/impl/TrackList.cpp @@ -382,7 +382,7 @@ TrackList::getSimilarTracks(std::optional offset, std::optionalid()) .bind(self()->id()) .groupBy("t.id") - .orderBy("COUNT(*) DESC") + .orderBy("COUNT(*) DESC, RANDOM()") .limit(size ? static_cast(*size) : -1) .offset(offset ? static_cast(*offset) : -1)}; diff --git a/src/test/database/DatabaseTest.cpp b/src/test/database/DatabaseTest.cpp index b23a19a1..c5edcc1f 100644 --- a/src/test/database/DatabaseTest.cpp +++ b/src/test/database/DatabaseTest.cpp @@ -811,7 +811,13 @@ testMultipleTracksSingleClusterSimilarity(Session& session) const auto similarTracks {Track::getSimilarTracks(session, {tracks.front().getId()})}; CHECK(similarTracks.size() == tracks.size() - 1); for (auto similarTrack : similarTracks) - CHECK(std::find_if(std::next(std::cbegin(tracks), 1), std::cend(tracks), [&](const ScopedTrack& track) { return similarTrack.id() == track.getId(); }) != std::cend(tracks)); + { + CHECK(std::find_if(std::next(std::cbegin(tracks), 1), std::cend(tracks), + [&](const ScopedTrack& track) + { + return similarTrack.id() == track.getId(); + }) != std::cend(tracks)); + } } } From 2ca3e6a237aaf72ad97296402d63621730c2793a Mon Sep 17 00:00:00 2001 From: emeric Date: Tue, 23 Jun 2020 14:41:38 +0200 Subject: [PATCH 06/12] Added some form elements + autocomplete attributes --- approot/admin-database.xml | 104 ++++++----- approot/admin-initwizard.xml | 62 +++--- approot/admin-user.xml | 34 ++-- approot/login.xml | 79 ++++---- approot/settings.xml | 280 ++++++++++++++-------------- src/lms/ui/Auth.cpp | 5 +- src/lms/ui/SettingsView.cpp | 3 + src/lms/ui/admin/InitWizardView.cpp | 24 ++- src/lms/ui/admin/UserView.cpp | 1 + 9 files changed, 309 insertions(+), 283 deletions(-) diff --git a/approot/admin-database.xml b/approot/admin-database.xml index e2d504a1..4e01c81b 100644 --- a/approot/admin-database.xml +++ b/approot/admin-database.xml @@ -6,62 +6,64 @@
- ${tr:Lms.Admin.Database.database} -
-
- -
- ${media-directory} - ${media-directory-info class="help-block"} +
+ ${tr:Lms.Admin.Database.database} +
+
+ +
+ ${media-directory} + ${media-directory-info class="help-block"} +
+
+
+ +
+ ${update-period} + ${update-period-info class="help-block"} +
+
+
+ +
+ ${update-start-time} + ${update-start-time-info class="help-block"} +
+
+
+ ${tr:Lms.Admin.Database.scan-options} +
+
+ +
+ ${recommendation-engine-type} + ${recommendation-engine-type-info class="help-block"} +
+
+
+ +
+ ${tags} + ${tags-info class="help-block"} +
- -
- ${update-period} - ${update-period-info class="help-block"} +
+ ${apply-btn class="btn-primary"} ${discard-btn} ${immediate-scan-btn class="btn-info"}
-
- -
- ${update-start-time} - ${update-start-time-info class="help-block"} -
-
-
- ${tr:Lms.Admin.Database.scan-options} -
-
- -
- ${recommendation-engine-type} - ${recommendation-engine-type-info class="help-block"} -
-
-
- -
- ${tags} - ${tags-info class="help-block"} -
-
-
-
-
- ${apply-btn class="btn-primary"} ${discard-btn} ${immediate-scan-btn class="btn-info"} -
-
+

diff --git a/approot/admin-initwizard.xml b/approot/admin-initwizard.xml index d58c2bb7..d9ca837a 100644 --- a/approot/admin-initwizard.xml +++ b/approot/admin-initwizard.xml @@ -9,42 +9,44 @@ -
-
- -
- ${admin-login} - ${admin-login-info class="help-block"} +
+
+
+ +
+ ${admin-login} + ${admin-login-info class="help-block"} +
-
-
- -
- ${password} - ${password-info class="help-block"} +
+ +
+ ${password} + ${password-info class="help-block"} +
-
-
- -
- ${password-confirm} - ${password-confirm-info class="help-block"} +
+ +
+ ${password-confirm} + ${password-confirm-info class="help-block"} +
+
+
+
+ ${create-btn class="btn-primary"} +
-
-
- ${create-btn class="btn-primary"} -
-
-
+
diff --git a/approot/admin-user.xml b/approot/admin-user.xml index 9ce8c4f4..c3adc97a 100644 --- a/approot/admin-user.xml +++ b/approot/admin-user.xml @@ -5,8 +5,9 @@
${title} -
- ${} +
+
+ ${}
${} - ${} + ${}
${} -
- -
- ${password} - ${password-info class="help-block"} +
+ +
+ ${password} + ${password-info class="help-block"} +
-
- ${} + ${}
@@ -48,12 +49,13 @@
${} -
-
- ${save-btn class="btn-primary"} +
+
+ ${save-btn class="btn-primary"} +
-
+
diff --git a/approot/login.xml b/approot/login.xml index 175b7df8..7dab178c 100644 --- a/approot/login.xml +++ b/approot/login.xml @@ -4,50 +4,51 @@
- -
-
- -
- ${login-name} -
-
- ${login-name-info} -
+ +
+
+
+ +
+ ${login-name} +
+
+ ${login-name-info} +
+
-
- -
- ${password} -
-
- ${password-info} -
-
+
+ +
+ ${password} +
+
+ ${password-info} +
+
-
- -
-
+
+ +
+
+
+
+
+
+ ${login-btn class="btn-primary"} +
+
-
-
-
- ${login-btn class="btn-primary"} -
-
-
+
- diff --git a/approot/settings.xml b/approot/settings.xml index 239b745e..6b52ee30 100644 --- a/approot/settings.xml +++ b/approot/settings.xml @@ -6,168 +6,170 @@
- ${tr:Lms.Settings.appearance} -
-
-
-
- - ${dark-mode-info clas="help-block"} +
+ ${tr:Lms.Settings.appearance} +
+
+
+
+ + ${dark-mode-info clas="help-block"} +
-
- ${tr:Lms.Settings.audio} -
- ${tr:Lms.Settings.audio-settings-are-local} -
-
-
- -
- ${transcode-mode} - ${transcode-mode-info class="help-block"} -
+ ${tr:Lms.Settings.audio} +
+ ${tr:Lms.Settings.audio-settings-are-local}
-
- -
- ${transcode-format} - ${transcode-format-info class="help-block"} -
-
-
- -
-
- ${transcode-bitrate} - kbps +
+
+ +
+ ${transcode-mode} + ${transcode-mode-info class="help-block"} +
+
+
+ +
+ ${transcode-format} + ${transcode-format-info class="help-block"} +
+
+
+ +
+
+ ${transcode-bitrate} + kbps +
+ ${transcode-bitrate-info class="help-block"}
- ${transcode-bitrate-info class="help-block"}
-
-
- -
- ${replaygain-mode} - ${replaygain-mode-info class="help-block"} -
-
-
- -
-
- ${replaygain-preamp} - dB +
+ +
+ ${replaygain-mode} + ${replaygain-mode-info class="help-block"}
- ${replaygain-preamp-info class="help-block"}
-
-
- -
-
- ${replaygain-preamp-no-rg-info} - dB +
+ +
+
+ ${replaygain-preamp} + dB +
+ ${replaygain-preamp-info class="help-block"}
- ${replaygain-preamp-no-rg-info-info class="help-block"}
-
-
- ${} - ${tr:Lms.Settings.subsonic-api} -
-
-
-
- - ${subsonic-transcode-enable-info class="help-block"} +
+ +
+
+ ${replaygain-preamp-no-rg-info} + dB +
+ ${replaygain-preamp-no-rg-info-info class="help-block"}
-
- -
- ${subsonic-transcode-format} - ${subsonic-transcode-format-info class="help-block"} -
-
-
- -
-
- ${subsonic-transcode-bitrate} - kbps + ${} + ${tr:Lms.Settings.subsonic-api} +
+
+
+
+ + ${subsonic-transcode-enable-info class="help-block"} +
+
+
+
+ +
+ ${subsonic-transcode-format} + ${subsonic-transcode-format-info class="help-block"} +
+
+
+ +
+
+ ${subsonic-transcode-bitrate} + kbps +
+ ${subsonic-transcode-bitrate-info class="help-block"} +
+
+
+ +
+ ${subsonic-artist-list-mode} + ${subsonic-artist-list-mode-info class="help-block"}
- ${subsonic-transcode-bitrate-info class="help-block"}
-
- -
- ${subsonic-artist-list-mode} - ${subsonic-artist-list-mode-info class="help-block"} + ${} + ${tr:Lms.Settings.change-password} +
+ ${} +
+ +
+ ${password-old} + ${password-old-info class="help-block"} +
-
-
- ${} - ${tr:Lms.Settings.change-password} -
- ${} -
- -
- ${password-old} - ${password-old-info class="help-block"} + ${} +
+ +
+ ${password} + ${password-info class="help-block"} +
-
- ${} -
- -
- ${password} - ${password-info class="help-block"} +
+ +
+ ${password-confirm} + ${password-confirm-info class="help-block"} +
-
-
- -
- ${password-confirm} - ${password-confirm-info class="help-block"} -
-
-
-
- ${apply-btn class="btn-primary"} ${discard-btn} +
+
+ ${apply-btn class="btn-primary"} ${discard-btn} +
-
+
diff --git a/src/lms/ui/Auth.cpp b/src/lms/ui/Auth.cpp index d450b0c7..cae9ee16 100644 --- a/src/lms/ui/Auth.cpp +++ b/src/lms/ui/Auth.cpp @@ -181,12 +181,15 @@ Auth::Auth() }; // LoginName - setFormWidget(AuthModel::LoginNameField, std::make_unique()); + auto loginName {std::make_unique()}; + loginName->setAttributeValue("autocomplete", "username"); + setFormWidget(AuthModel::LoginNameField, std::move(loginName)); // Password auto password = std::make_unique(); password->setEchoMode(Wt::EchoMode::Password); password->enterPressed().connect(this, processAuth); + password->setAttributeValue("autocomplete", "current-password"); setFormWidget(AuthModel::PasswordField, std::move(password)); // Remember me diff --git a/src/lms/ui/SettingsView.cpp b/src/lms/ui/SettingsView.cpp index 30d8a401..33cfd766 100644 --- a/src/lms/ui/SettingsView.cpp +++ b/src/lms/ui/SettingsView.cpp @@ -387,17 +387,20 @@ SettingsView::refreshView() auto oldPassword {std::make_unique()}; oldPassword->setEchoMode(Wt::EchoMode::Password); + oldPassword->setAttributeValue("autocomplete", "current-password"); t->setFormWidget(SettingsModel::PasswordOldField, std::move(oldPassword)); } // Password auto password {std::make_unique()}; password->setEchoMode(Wt::EchoMode::Password); + password->setAttributeValue("autocomplete", "new-password"); t->setFormWidget(SettingsModel::PasswordField, std::move(password)); // Password confirm auto passwordConfirm {std::make_unique()}; passwordConfirm->setEchoMode(Wt::EchoMode::Password); + passwordConfirm->setAttributeValue("autocomplete", "new-password"); t->setFormWidget(SettingsModel::PasswordConfirmField, std::move(passwordConfirm)); // Audio diff --git a/src/lms/ui/admin/InitWizardView.cpp b/src/lms/ui/admin/InitWizardView.cpp index 34d7c5fb..e62820b0 100644 --- a/src/lms/ui/admin/InitWizardView.cpp +++ b/src/lms/ui/admin/InitWizardView.cpp @@ -113,17 +113,27 @@ InitWizardView::InitWizardView() auto model = std::make_shared(); // AdminLogin - setFormWidget(InitWizardModel::AdminLoginField, std::make_unique()); + { + auto adminLogin {std::make_unique()}; + adminLogin->setAttributeValue("autocomplete", "username"); + setFormWidget(InitWizardModel::AdminLoginField, std::move(adminLogin)); + } // Password - auto passwordEdit = std::make_unique(); - passwordEdit->setEchoMode(Wt::EchoMode::Password); - setFormWidget(InitWizardModel::PasswordField, std::move(passwordEdit) ); + { + auto passwordEdit = std::make_unique(); + passwordEdit->setEchoMode(Wt::EchoMode::Password); + passwordEdit->setAttributeValue("autocomplete", "current-password"); + setFormWidget(InitWizardModel::PasswordField, std::move(passwordEdit) ); + } // Password confirmation - auto passwordConfirmEdit = std::make_unique(); - passwordConfirmEdit->setEchoMode(Wt::EchoMode::Password); - setFormWidget(InitWizardModel::PasswordConfirmField, std::move(passwordConfirmEdit)); + { + auto passwordConfirmEdit = std::make_unique(); + passwordConfirmEdit->setEchoMode(Wt::EchoMode::Password); + passwordConfirmEdit->setAttributeValue("autocomplete", "current-password"); + setFormWidget(InitWizardModel::PasswordConfirmField, std::move(passwordConfirmEdit)); + } Wt::WPushButton* saveButton = bindNew("create-btn", Wt::WString::tr("Lms.create")); saveButton->clicked().connect([=] diff --git a/src/lms/ui/admin/UserView.cpp b/src/lms/ui/admin/UserView.cpp index 3bb52e6d..53c27e82 100644 --- a/src/lms/ui/admin/UserView.cpp +++ b/src/lms/ui/admin/UserView.cpp @@ -227,6 +227,7 @@ UserView::refreshView() // Password auto passwordEdit = std::make_unique(); passwordEdit->setEchoMode(Wt::EchoMode::Password); + passwordEdit->setAttributeValue("autocomplete", "off"); t->setFormWidget(UserModel::PasswordField, std::move(passwordEdit)); // Demo account From a01b8f28af2546df9bce8a433dd4f0ca42079514 Mon Sep 17 00:00:00 2001 From: emeric Date: Thu, 25 Jun 2020 15:41:14 +0200 Subject: [PATCH 07/12] Better detection of embedded covers for vorbis/opus ogg files. ref #71 --- src/libs/metadata/impl/TagLibParser.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/libs/metadata/impl/TagLibParser.cpp b/src/libs/metadata/impl/TagLibParser.cpp index 5fa1bfea..2a83651a 100644 --- a/src/libs/metadata/impl/TagLibParser.cpp +++ b/src/libs/metadata/impl/TagLibParser.cpp @@ -26,8 +26,10 @@ #include #include #include +#include #include #include +#include #include #include "utils/Logger.hpp" @@ -399,6 +401,16 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug) if (!flacFile->pictureList().isEmpty()) track.hasCover = true; } + else if (TagLib::Ogg::Vorbis::File* vorbisFile {dynamic_cast(f.file())}) + { + if (!vorbisFile->tag()->pictureList().isEmpty()) + track.hasCover = true; + } + else if (TagLib::Ogg::Opus::File* opusFile {dynamic_cast(f.file())}) + { + if (!opusFile->tag()->pictureList().isEmpty()) + track.hasCover = true; + } for (const auto& property : properties) { From 6546f44cb24756e89fd748938884add989362051 Mon Sep 17 00:00:00 2001 From: emeric Date: Thu, 25 Jun 2020 16:36:07 +0200 Subject: [PATCH 08/12] Force rescan for Better detection of embedded covers for vorbis/opus ogg files. ref #71 --- src/libs/database/impl/Session.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/libs/database/impl/Session.cpp b/src/libs/database/impl/Session.cpp index 7cb92cbe..7cf91fcf 100644 --- a/src/libs/database/impl/Session.cpp +++ b/src/libs/database/impl/Session.cpp @@ -40,7 +40,7 @@ namespace Database { -#define LMS_DATABASE_VERSION 23 +#define LMS_DATABASE_VERSION 24 using Version = std::size_t; @@ -261,6 +261,12 @@ CREATE TABLE "user_backup" ( // Just increment the scan version of the settings to make the next scheduled scan rescan everything ScanSettings::get(*this).modify()->incScanVersion(); } + else if (version == 23) + { + // Better cover detection + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + ScanSettings::get(*this).modify()->incScanVersion(); + } else { LMS_LOG(DB, ERROR) << "Database version " << version << " cannot be handled using migration"; From 8a38d130da6349cdbe38a60ff7ea0b330637163b Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 26 Jun 2020 10:56:04 +0200 Subject: [PATCH 09/12] Handled preferred cover file names (same as track's filename, then cover, then front). fixes #71 --- src/libs/av/impl/AvInfo.cpp | 2 +- src/libs/cover/impl/CoverArtGrabber.cpp | 59 ++++++++++++++++++------- src/libs/cover/impl/CoverArtGrabber.hpp | 12 ++--- 3 files changed, 52 insertions(+), 21 deletions(-) diff --git a/src/libs/av/impl/AvInfo.cpp b/src/libs/av/impl/AvInfo.cpp index b8fc392a..ff4d010d 100644 --- a/src/libs/av/impl/AvInfo.cpp +++ b/src/libs/av/impl/AvInfo.cpp @@ -215,7 +215,7 @@ MediaFile::getAttachedPictures(std::size_t nbMaxPictures) const LMS_LOG(AV, ERROR) << "CODEC ID " << avstream->codecpar->codec_id << " not handled in mime type conversion"; } - AVPacket pkt = avstream->attached_pic; + const AVPacket& pkt {avstream->attached_pic}; std::copy(pkt.data, pkt.data + pkt.size, std::back_inserter(picture.data)); diff --git a/src/libs/cover/impl/CoverArtGrabber.cpp b/src/libs/cover/impl/CoverArtGrabber.cpp index efd1c73c..64387d5a 100644 --- a/src/libs/cover/impl/CoverArtGrabber.cpp +++ b/src/libs/cover/impl/CoverArtGrabber.cpp @@ -78,30 +78,59 @@ getFromAvMediaFile(const Av::MediaFile& input) } std::optional -Grabber::getFromDirectory(const std::filesystem::path& p) const +Grabber::getFromDirectory(const std::filesystem::path& p, std::string_view preferredFileName) const { - for (auto coverPath : getCoverPaths(p)) - { - Image image; + const std::multimap coverPaths {getCoverPaths(p)}; - if (image.load(coverPath)) + auto tryLoadImage = [](const std::filesystem::path& p, Image& image) + { + if (!image.load(p)) + { + LMS_LOG(COVER, ERROR) << "Cannot load image in file '" << p.string() << "'"; + return false; + } + + return true; + }; + + auto tryLoadImageFromFilename = [&](std::string_view fileName, Image& image) + { + auto range {coverPaths.equal_range(std::string {fileName})}; + for (auto it {range.first}; it != range.second; ++it) + { + if (tryLoadImage(it->second, image)) + return true; + } + return false; + }; + + Image image; + + if (!preferredFileName.empty() && tryLoadImageFromFilename(preferredFileName, image)) + return image; + + for (std::string_view filename : _preferredFileNames) + { + if (tryLoadImageFromFilename(filename, image)) + return image; + } + + // Just pick one + for (const auto& [filename, coverPath] : coverPaths) + { + if (tryLoadImage(coverPath, image)) return image; - else - LMS_LOG(COVER, ERROR) << "Cannot load image in file '" << coverPath.string() << "'"; } - LMS_LOG(COVER, DEBUG) << "No cover found in directory '" << p.string() << "'"; return std::nullopt; } -std::vector +std::multimap Grabber::getCoverPaths(const std::filesystem::path& directoryPath) const { - std::vector res; + std::multimap res; std::error_code ec; - // TODO handle preferred file names - std::filesystem::directory_iterator itPath(directoryPath, ec); std::filesystem::directory_iterator itEnd; while (!ec && itPath != itEnd) @@ -121,7 +150,7 @@ Grabber::getCoverPaths(const std::filesystem::path& directoryPath) const continue; } - res.push_back(path); + res.emplace(std::filesystem::path{path}.filename().replace_extension("").string(), path); } return res; @@ -177,12 +206,12 @@ Grabber::getFromTrack(Database::Session& dbSession, Database::IdType trackId, st cover = getFromTrack(trackPath); if (!cover) - cover = getFromDirectory(trackPath.parent_path()); + cover = getFromDirectory(trackPath.parent_path(), trackPath.filename().replace_extension("").string()); if (!cover && isMultiDisc) { if (trackPath.parent_path().has_parent_path()) - cover = getFromDirectory(trackPath.parent_path().parent_path()); + cover = getFromDirectory(trackPath.parent_path().parent_path(), {}); } if (!cover) diff --git a/src/libs/cover/impl/CoverArtGrabber.hpp b/src/libs/cover/impl/CoverArtGrabber.hpp index a153b8b4..25eac8cc 100644 --- a/src/libs/cover/impl/CoverArtGrabber.hpp +++ b/src/libs/cover/impl/CoverArtGrabber.hpp @@ -20,10 +20,12 @@ #pragma once #include -#include +#include #include -#include +#include +#include #include +#include #include "cover/ICoverArtGrabber.hpp" #include "database/Types.hpp" @@ -101,8 +103,8 @@ namespace CoverArt Image getFromRelease(Database::Session& dbSession, Database::IdType releaseId, std::size_t size); std::optional getFromTrack(const std::filesystem::path& path) const; - std::vector getCoverPaths(const std::filesystem::path& directoryPath) const; - std::optional getFromDirectory(const std::filesystem::path& path) const; + std::multimap getCoverPaths(const std::filesystem::path& directoryPath) const; + std::optional getFromDirectory(const std::filesystem::path& path, std::string_view preferredFileName) const; std::unique_ptr _defaultCover; // unique_ptr to defer initializing @@ -117,7 +119,7 @@ namespace CoverArt static inline constexpr std::size_t _maxCacheEntries {1000}; static inline const std::vector _fileExtensions {".jpg", ".jpeg", ".png", ".bmp"}; // TODO parametrize static inline constexpr std::size_t _maxFileSize {10000000}; - static inline const std::vector _preferredFileNames {"cover", "front"}; // TODO parametrize + static inline const std::vector _preferredFileNames {"cover", "front"}; // TODO parametrize }; } // namespace CoverArt From c1804c1f2e4c00eb34d19da3af5686611048c577 Mon Sep 17 00:00:00 2001 From: emeric Date: Wed, 1 Jul 2020 09:23:35 +0200 Subject: [PATCH 10/12] Resume audio context on seek --- docroot/js/mediaplayer.js | 1 + 1 file changed, 1 insertion(+) diff --git a/docroot/js/mediaplayer.js b/docroot/js/mediaplayer.js index 9022e786..a5c553f1 100644 --- a/docroot/js/mediaplayer.js +++ b/docroot/js/mediaplayer.js @@ -163,6 +163,7 @@ LMS.mediaplayer = function () { _requestNextTrack(); }); _elems.seek.addEventListener("change", function() { + _audioCtx.resume(); let mode = _getAudioMode(); if (!mode) return; From b2863e927c4a0c973df70fb5e7b3c5082466c98f Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 3 Jul 2020 10:03:26 +0200 Subject: [PATCH 11/12] Scan files which extensions is not lowercase --- src/libs/scanner/impl/MediaScanner.cpp | 11 +++++++++-- src/libs/utils/impl/String.cpp | 8 +++++++- src/libs/utils/include/utils/String.hpp | 3 +++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/libs/scanner/impl/MediaScanner.cpp b/src/libs/scanner/impl/MediaScanner.cpp index f2b5a70a..0529ff4a 100644 --- a/src/libs/scanner/impl/MediaScanner.cpp +++ b/src/libs/scanner/impl/MediaScanner.cpp @@ -65,7 +65,9 @@ getNextFirstOfMonth(Wt::WDate current) bool isFileSupported(const std::filesystem::path& file, const std::unordered_set& extensions) { - return (extensions.find(file.extension()) != extensions.end()); + const std::filesystem::path extension {StringUtils::stringToLower(file.extension().string())}; + + return (extensions.find(extension) != extensions.end()); } bool @@ -568,7 +570,12 @@ MediaScanner::refreshScanSettings() _startTime = scanSettings->getUpdateStartTime(); _updatePeriod = scanSettings->getUpdatePeriod(); - _fileExtensions = scanSettings->getAudioFileExtensions(); + { + const auto fileExtensions {scanSettings->getAudioFileExtensions()}; + _fileExtensions.clear(); + std::transform(std::cbegin(fileExtensions), std::end(fileExtensions), std::inserter(_fileExtensions, std::begin(_fileExtensions)), + [](const std::filesystem::path& extension) { return std::filesystem::path{ StringUtils::stringToLower(extension.string()) }; }); + } _mediaDirectory = scanSettings->getMediaDirectory(); _recommendationEngineType = scanSettings->getRecommendationEngineType(); diff --git a/src/libs/utils/impl/String.cpp b/src/libs/utils/impl/String.cpp index dbcb82d2..edef6744 100644 --- a/src/libs/utils/impl/String.cpp +++ b/src/libs/utils/impl/String.cpp @@ -104,11 +104,17 @@ stringToLower(std::string_view str) std::string res; res.reserve(str.size()); - std::transform(std::cbegin(str), std::cend(str), std::back_inserter(res), [](char c) { return std::tolower(c);}); + std::transform(std::cbegin(str), std::cend(str), std::back_inserter(res), [](unsigned char c) { return std::tolower(c);}); return res; } +void +stringToLower(std::string& str) +{ + std::transform(std::cbegin(str), std::cend(str), std::begin(str), [](unsigned char c) { return std::tolower(c);}); +} + std::string stringToUpper(const std::string& str) { diff --git a/src/libs/utils/include/utils/String.hpp b/src/libs/utils/include/utils/String.hpp index 62d6fba4..2c4e6724 100644 --- a/src/libs/utils/include/utils/String.hpp +++ b/src/libs/utils/include/utils/String.hpp @@ -43,6 +43,9 @@ stringTrimEnd(const std::string& str, const std::string& whitespaces = " \t"); std::string stringToLower(std::string_view str); +void +stringToLower(std::string& str); + std::string stringToUpper(const std::string& str); From 718d2ee6d717099db0b28dbe2700bf26271af395 Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 3 Jul 2020 21:03:16 +0200 Subject: [PATCH 12/12] Made Audinaut work. fixes #76 --- src/libs/subsonic/impl/RequestContext.hpp | 1 + src/libs/subsonic/impl/Scan.cpp | 8 +- src/libs/subsonic/impl/SubsonicResource.cpp | 92 ++++++++++----------- src/libs/subsonic/impl/SubsonicResponse.cpp | 19 ++++- src/libs/subsonic/impl/SubsonicResponse.hpp | 14 ++-- src/libs/utils/include/utils/String.hpp | 4 +- 6 files changed, 73 insertions(+), 65 deletions(-) diff --git a/src/libs/subsonic/impl/RequestContext.hpp b/src/libs/subsonic/impl/RequestContext.hpp index ec10a226..cd41100b 100644 --- a/src/libs/subsonic/impl/RequestContext.hpp +++ b/src/libs/subsonic/impl/RequestContext.hpp @@ -38,6 +38,7 @@ namespace API::Subsonic const Wt::Http::ParameterMap& parameters; Database::Session& dbSession; std::string userName; + std::string clientName; }; } diff --git a/src/libs/subsonic/impl/Scan.cpp b/src/libs/subsonic/impl/Scan.cpp index 544f71dd..42716069 100644 --- a/src/libs/subsonic/impl/Scan.cpp +++ b/src/libs/subsonic/impl/Scan.cpp @@ -43,20 +43,20 @@ namespace API::Subsonic::Scan Response - handleGetScanStatus(RequestContext&) + handleGetScanStatus(RequestContext& context) { - Response response {Response::createOkResponse()}; + Response response {Response::createOkResponse(context)}; response.addNode("scanStatus", createStatusResponseNode()); return response; } Response - handleStartScan(RequestContext&) + handleStartScan(RequestContext& context) { ServiceProvider::get()->requestImmediateScan(); - Response response {Response::createOkResponse()}; + Response response {Response::createOkResponse(context)}; response.addNode("scanStatus", createStatusResponseNode()); return response; diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index ad53f93d..6d793f20 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -141,15 +141,15 @@ getClientInfo(const Wt::Http::ParameterMap& parameters) ClientInfo res; // Mandatory parameters + res.name = getMandatoryParameterAs(parameters, "c"); res.version = getMandatoryParameterAs(parameters, "v"); if (res.version.major > API_VERSION_MAJOR) throw ServerMustUpgradeError {}; if (res.version.major < API_VERSION_MAJOR) throw ClientMustUpgradeError {}; - if (res.version.minor > API_VERSION_MINOR) + if (res.version.minor > Response::getAPIMinorVersion(res.name)) throw ServerMustUpgradeError {}; - res.name = getMandatoryParameterAs(parameters, "c"); res.user = getMandatoryParameterAs(parameters, "u"); res.password = decodePasswordIfNeeded(getMandatoryParameterAs(parameters, "p")); @@ -493,9 +493,9 @@ userToResponseNode(const User::pointer& user) static Response -handlePingRequest(RequestContext&) +handlePingRequest(RequestContext& context) { - return Response::createOkResponse(); + return Response::createOkResponse(context); } static @@ -521,7 +521,7 @@ handleChangePassword(RequestContext& context) user.modify()->setPasswordHash(hash); user.modify()->clearAuthTokens(); - return Response::createOkResponse(); + return Response::createOkResponse(context); } static @@ -576,7 +576,7 @@ handleCreatePlaylistRequest(RequestContext& context) TrackListEntry::create(context.dbSession, track, tracklist ); } - return Response::createOkResponse(); + return Response::createOkResponse(context); } static @@ -599,7 +599,7 @@ handleCreateUserRequest(RequestContext& context) User::pointer user {User::create(context.dbSession, username, hash)}; - return Response::createOkResponse(); + return Response::createOkResponse(context); } static @@ -626,7 +626,7 @@ handleDeletePlaylistRequest(RequestContext& context) tracklist.remove(); - return Response::createOkResponse(); + return Response::createOkResponse(context); } static @@ -647,14 +647,14 @@ handleDeleteUserRequest(RequestContext& context) user.remove(); - return Response::createOkResponse(); + return Response::createOkResponse(context); } static Response -handleGetLicenseRequest(RequestContext&) +handleGetLicenseRequest(RequestContext& context) { - Response response {Response::createOkResponse()}; + Response response {Response::createOkResponse(context)}; Response::Node& licenseNode {response.createNode("license")}; licenseNode.setAttribute("licenseExpires", "2025-09-03T14:46:43"); @@ -680,7 +680,7 @@ handleGetRandomSongsRequest(RequestContext& context) auto tracks {Track::getAllRandom(context.dbSession, {}, size)}; - Response response {Response::createOkResponse()}; + Response response {Response::createOkResponse(context)}; Response::Node& randomSongsNode {response.createNode("randomSongs")}; for (const Track::pointer& track : tracks) @@ -757,7 +757,7 @@ handleGetAlbumListRequestCommon(const RequestContext& context, bool id3) else throw NotImplementedGenericError {}; - Response response {Response::createOkResponse()}; + Response response {Response::createOkResponse(context)}; Response::Node& albumListNode {response.createNode(id3 ? "albumList2" : "albumList")}; for (const Release::pointer& release : releases) @@ -800,7 +800,7 @@ handleGetAlbumRequest(RequestContext& context) if (!user) throw UserNotAuthorizedError {}; - Response response {Response::createOkResponse()}; + Response response {Response::createOkResponse(context)}; Response::Node releaseNode {releaseToResponseNode(release, context.dbSession, user, true /* id3 */)}; auto tracks {release->getTracks()}; @@ -832,7 +832,7 @@ handleGetArtistRequest(RequestContext& context) if (!user) throw UserNotAuthorizedError {}; - Response response {Response::createOkResponse()}; + Response response {Response::createOkResponse(context)}; Response::Node artistNode {artistToResponseNode(user, artist, true /* id3 */)}; auto releases {artist->getReleases()}; @@ -856,7 +856,7 @@ handleGetArtistInfoRequestCommon(RequestContext& context, bool id3) // Optional params std::size_t count {getParameterAs(context.parameters, "count").value_or(20)}; - Response response {Response::createOkResponse()}; + Response response {Response::createOkResponse(context)}; Response::Node& artistInfoNode {response.createNode(id3 ? "artistInfo2" : "artistInfo")}; { @@ -909,7 +909,7 @@ static Response handleGetArtistsRequest(RequestContext& context) { - Response response {Response::createOkResponse()}; + Response response {Response::createOkResponse(context)}; Response::Node& artistsNode {response.createNode("artists")}; Response::Node& indexNode {artistsNode.createArrayChild("index")}; @@ -951,7 +951,7 @@ handleGetMusicDirectoryRequest(RequestContext& context) // Mandatory params Id id {getMandatoryParameterAs(context.parameters, "id")}; - Response response {Response::createOkResponse()}; + Response response {Response::createOkResponse(context)}; Response::Node& directoryNode {response.createNode("directory")}; directoryNode.setAttribute("id", IdToString(id)); @@ -1015,9 +1015,9 @@ handleGetMusicDirectoryRequest(RequestContext& context) static Response -handleGetMusicFoldersRequest(RequestContext&) +handleGetMusicFoldersRequest(RequestContext& context) { - Response response {Response::createOkResponse()}; + Response response {Response::createOkResponse(context)}; Response::Node& musicFoldersNode {response.createNode("musicFolders")}; Response::Node& musicFolderNode {musicFoldersNode.createArrayChild("musicFolder")}; @@ -1031,7 +1031,7 @@ static Response handleGetGenresRequest(RequestContext& context) { - Response response {Response::createOkResponse()}; + Response response {Response::createOkResponse(context)}; Response::Node& genresNode {response.createNode("genres")}; @@ -1053,7 +1053,7 @@ static Response handleGetIndexesRequest(RequestContext& context) { - Response response {Response::createOkResponse()}; + Response response {Response::createOkResponse(context)}; Response::Node& artistsNode {response.createNode("indexes")}; Response::Node& indexNode {artistsNode.createArrayChild("index")}; @@ -1129,7 +1129,7 @@ handleGetSimilarSongsRequestCommon(RequestContext& context, bool id3) Random::shuffleContainer(tracks); - Response response {Response::createOkResponse()}; + Response response {Response::createOkResponse(context)}; Response::Node& similarSongsNode {response.createNode(id3 ? "similarSongs2" : "similarSongs")}; for (const Track::pointer& track : tracks) similarSongsNode.addArrayChild("song", trackToResponseNode(track, context.dbSession, user)); @@ -1161,7 +1161,7 @@ handleGetStarredRequestCommon(RequestContext& context, bool id3) if (!user) throw UserNotAuthorizedError {}; - Response response {Response::createOkResponse()}; + Response response {Response::createOkResponse(context)}; Response::Node& starredNode {response.createNode(id3 ? "starred2" : "starred")}; { @@ -1236,7 +1236,7 @@ handleGetPlaylistRequest(RequestContext& context) if (!tracklist) throw RequestedDataNotFoundError {}; - Response response {Response::createOkResponse()}; + Response response {Response::createOkResponse(context)}; Response::Node playlistNode {tracklistToResponseNode(tracklist, context.dbSession)}; auto entries {tracklist->getEntries()}; @@ -1258,7 +1258,7 @@ handleGetPlaylistsRequest(RequestContext& context) if (!user) throw UserNotAuthorizedError {}; - Response response {Response::createOkResponse()}; + Response response {Response::createOkResponse(context)}; Response::Node& playlistsNode {response.createNode("playlists")}; auto tracklists {TrackList::getAll(context.dbSession, user, TrackList::Type::Playlist)}; @@ -1295,7 +1295,7 @@ handleGetSongsByGenreRequest(RequestContext& context) if (!user) throw UserNotAuthorizedError {}; - Response response {Response::createOkResponse()}; + Response response {Response::createOkResponse(context)}; Response::Node& songsByGenreNode {response.createNode("songsByGenre")}; bool more; @@ -1320,7 +1320,7 @@ handleGetUserRequest(RequestContext& context) if (!user) throw RequestedDataNotFoundError {}; - Response response {Response::createOkResponse()}; + Response response {Response::createOkResponse(context)}; response.addNode("user", userToResponseNode(user)); return response; @@ -1332,7 +1332,7 @@ handleGetUsersRequest(RequestContext& context) { auto transaction {context.dbSession.createSharedTransaction()}; - Response response {Response::createOkResponse()}; + Response response {Response::createOkResponse(context)}; Response::Node& usersNode {response.createNode("users")}; const auto users {User::getAll(context.dbSession)}; @@ -1365,7 +1365,7 @@ handleSearchRequestCommon(RequestContext& context, bool id3) if (!user) throw UserNotAuthorizedError {}; - Response response {Response::createOkResponse()}; + Response response {Response::createOkResponse(context)}; Response::Node& searchResult2Node {response.createNode(id3 ? "searchResult3" : "searchResult2")}; bool more; @@ -1474,7 +1474,7 @@ handleStarRequest(RequestContext& context) user.modify()->starTrack(track); } - return Response::createOkResponse(); + return Response::createOkResponse(context); } static @@ -1531,7 +1531,7 @@ handleUnstarRequest(RequestContext& context) } - return Response::createOkResponse(); + return Response::createOkResponse(context); } static @@ -1563,7 +1563,7 @@ handleUpdateUserRequest(RequestContext& context) user.modify()->clearAuthTokens(); } - return Response::createOkResponse(); + return Response::createOkResponse(context); } static @@ -1627,7 +1627,7 @@ handleUpdatePlaylistRequest(RequestContext& context) TrackListEntry::create(context.dbSession, track, tracklist ); } - return Response::createOkResponse(); + return Response::createOkResponse(context); } static @@ -1642,7 +1642,7 @@ handleGetBookmarks(RequestContext& context) const auto bookmarks {TrackBookmark::getByUser(context.dbSession, user)}; - Response response {Response::createOkResponse()}; + Response response {Response::createOkResponse(context)}; Response::Node& bookmarksNode {response.createNode("bookmarks")}; for (const TrackBookmark::pointer& bookmark : bookmarks) @@ -1687,7 +1687,7 @@ handleCreateBookmark(RequestContext& context) if (comment) bookmark.modify()->setComment(*comment); - return Response::createOkResponse(); + return Response::createOkResponse(context); } static @@ -1715,7 +1715,7 @@ handleDeleteBookmark(RequestContext& context) bookmark.remove(); - return Response::createOkResponse(); + return Response::createOkResponse(context); } static @@ -1891,12 +1891,15 @@ SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Resp // Optional parameters const ResponseFormat format {getParameterAs(parameters, "f").value_or("xml") == "json" ? ResponseFormat::json : ResponseFormat::xml}; + std::string clientName; + try { - // Mandatory parameters const ClientInfo clientInfo {getClientInfo(parameters)}; + clientName = clientInfo.name; + SessionPool::ScopedSession dbSession {_sessionPool}; switch (ServiceProvider::get()->checkUserPassword(dbSession.get(), @@ -1911,7 +1914,7 @@ SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Resp throw LoginThrottledGenericError {}; } - RequestContext requestContext {parameters, dbSession.get(), clientInfo.user}; + RequestContext requestContext {parameters, dbSession.get(), clientInfo.user, clientInfo.name}; auto itEntryPoint {requestEntryPoints.find(requestPath)}; if (itEntryPoint != requestEntryPoints.end()) @@ -1945,21 +1948,12 @@ SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Resp LMS_LOG(API_SUBSONIC, ERROR) << "Unhandled command '" << requestPath << "'"; throw UnknownEntryPointGenericError {}; } - catch (const NotImplementedGenericError& e) - { - LMS_LOG(API_SUBSONIC, INFO) << "Command '" << requestPath << "'" - << ", params = [" << parameterMapToDebugString(request.getParameterMap()) << "]" - << ", code = " << static_cast(e.getCode()) << ", msg = '" << static_cast(e).getMessage() << "'"; - Response resp {Response::createFailedResponse(e)}; - resp.write(response.out(), format); - response.setMimeType(ResponseFormatToMimeType(format)); - } catch (const Error& e) { LMS_LOG(API_SUBSONIC, ERROR) << "Error while processing request '" << requestPath << "'" << ", params = [" << parameterMapToDebugString(request.getParameterMap()) << "]" << ", code = " << static_cast(e.getCode()) << ", msg = '" << e.getMessage() << "'"; - Response resp {Response::createFailedResponse(e)}; + Response resp {Response::createFailedResponse(clientName, e)}; resp.write(response.out(), format); response.setMimeType(ResponseFormatToMimeType(format)); } diff --git a/src/libs/subsonic/impl/SubsonicResponse.cpp b/src/libs/subsonic/impl/SubsonicResponse.cpp index 893be143..9572d18e 100644 --- a/src/libs/subsonic/impl/SubsonicResponse.cpp +++ b/src/libs/subsonic/impl/SubsonicResponse.cpp @@ -28,6 +28,7 @@ #include #include "utils/Exception.hpp" +#include "utils/String.hpp" namespace API::Subsonic { @@ -108,25 +109,25 @@ Response::Node::createArrayChild(const std::string& key) } Response -Response::createOkResponse() +Response::createOkResponse(const RequestContext& context) { Response response; Node& responseNode {response._root.createChild("subsonic-response")}; responseNode.setAttribute("status", "ok"); - responseNode.setAttribute("version", API_VERSION_STR); + responseNode.setAttribute("version", std::string {QUOTEME(API_VERSION_MAJOR) "."} + std::to_string(getAPIMinorVersion(context.clientName)) + ".0"); return response; } Response -Response::createFailedResponse(const Error& error) +Response::createFailedResponse(std::string_view clientName, const Error& error) { Response response; Node& responseNode {response._root.createChild("subsonic-response")}; responseNode.setAttribute("status", "failed"); - responseNode.setAttribute("version", API_VERSION_STR); + responseNode.setAttribute("version", std::string {QUOTEME(API_VERSION_MAJOR) "."} + std::to_string(getAPIMinorVersion(clientName)) + ".0"); Node& errorNode {responseNode.createChild("error")}; errorNode.setAttribute("code", std::to_string(static_cast(error.getCode()))); @@ -213,6 +214,16 @@ Response::writeXML(std::ostream& os) boost::property_tree::write_xml(os, root); } +unsigned +Response::getAPIMinorVersion(std::string_view clientName) +{ + // Audinaut does not lower its client API version in plain text authentication mode + if (clientName == "Audinaut") + return 13; + else + return 12; +} + void Response::writeJSON(std::ostream& os) { diff --git a/src/libs/subsonic/impl/SubsonicResponse.hpp b/src/libs/subsonic/impl/SubsonicResponse.hpp index 8354cbf2..811d22c9 100644 --- a/src/libs/subsonic/impl/SubsonicResponse.hpp +++ b/src/libs/subsonic/impl/SubsonicResponse.hpp @@ -24,15 +24,13 @@ #include #include +#include "RequestContext.hpp" + +#define API_VERSION_MAJOR 1 namespace API::Subsonic { -#define API_VERSION_MAJOR 1 -#define API_VERSION_MINOR 12 -#define API_VERSION_PATCH 0 -#define API_VERSION_STR "1.12.0" - enum class ResponseFormat { xml, @@ -201,8 +199,8 @@ class Response std::map> _childrenArrays; }; - static Response createOkResponse(); - static Response createFailedResponse(const Error& error); + static Response createOkResponse(const RequestContext& context); + static Response createFailedResponse(std::string_view clientName, const Error& error); virtual ~Response() {} Response(const Response&) = delete; @@ -215,6 +213,8 @@ class Response Node& createArrayNode(const std::string& key); void write(std::ostream& os, ResponseFormat format); + + static unsigned getAPIMinorVersion(std::string_view clientName); private: void writeJSON(std::ostream& os); diff --git a/src/libs/utils/include/utils/String.hpp b/src/libs/utils/include/utils/String.hpp index 2c4e6724..d4655931 100644 --- a/src/libs/utils/include/utils/String.hpp +++ b/src/libs/utils/include/utils/String.hpp @@ -25,8 +25,10 @@ #include #include -namespace StringUtils { +#define QUOTEME(x) QUOTEME_1(x) +#define QUOTEME_1(x) #x +namespace StringUtils { std::vector splitString(const std::string& string, const std::string& separators);