From bd7561af8e5b9c11bfdfa20fa87372afd01dbff5 Mon Sep 17 00:00:00 2001 From: emeric Date: Sat, 22 Aug 2020 15:20:18 +0200 Subject: [PATCH 01/24] Changed the way services are handled --- src/libs/av/impl/AvTranscoder.cpp | 2 +- .../impl/features/FeaturesClassifierCache.cpp | 4 +- src/libs/scanner/impl/AcousticBrainzUtils.cpp | 2 +- src/libs/subsonic/impl/Scan.cpp | 4 +- src/libs/subsonic/impl/SubsonicResource.cpp | 22 ++--- src/libs/utils/include/utils/Logger.hpp | 2 +- src/libs/utils/include/utils/Service.hpp | 46 +++++----- src/lms/main.cpp | 84 ++++++++----------- src/lms/ui/Auth.cpp | 6 +- src/lms/ui/LmsApplication.cpp | 10 +-- src/lms/ui/PlayQueue.cpp | 2 +- src/lms/ui/SettingsView.cpp | 8 +- src/lms/ui/admin/DatabaseSettingsView.cpp | 4 +- src/lms/ui/admin/InitWizardView.cpp | 4 +- src/lms/ui/admin/ScannerController.cpp | 6 +- src/lms/ui/admin/UserView.cpp | 6 +- src/lms/ui/common/AuthModeModel.cpp | 4 +- src/lms/ui/explore/ArtistView.cpp | 2 +- src/lms/ui/explore/ReleaseView.cpp | 2 +- src/lms/ui/resource/ImageResource.cpp | 4 +- src/test/database/DatabaseTest.cpp | 2 +- src/tools/metadata/LmsMetadata.cpp | 2 +- .../recommendation/LmsRecommendation.cpp | 6 +- 23 files changed, 107 insertions(+), 127 deletions(-) diff --git a/src/libs/av/impl/AvTranscoder.cpp b/src/libs/av/impl/AvTranscoder.cpp index f1b9ed94..574f8299 100644 --- a/src/libs/av/impl/AvTranscoder.cpp +++ b/src/libs/av/impl/AvTranscoder.cpp @@ -38,7 +38,7 @@ static std::filesystem::path ffmpegPath; void Transcoder::init() { - ffmpegPath = ServiceProvider::get()->getPath("ffmpeg-file", "/usr/bin/ffmpeg"); + ffmpegPath = Service::get()->getPath("ffmpeg-file", "/usr/bin/ffmpeg"); if (!std::filesystem::exists(ffmpegPath)) throw LmsException {"File '" + ffmpegPath.string() + "' does not exist!"}; } diff --git a/src/libs/recommendation/impl/features/FeaturesClassifierCache.cpp b/src/libs/recommendation/impl/features/FeaturesClassifierCache.cpp index fc80098d..d184ac60 100644 --- a/src/libs/recommendation/impl/features/FeaturesClassifierCache.cpp +++ b/src/libs/recommendation/impl/features/FeaturesClassifierCache.cpp @@ -32,7 +32,7 @@ namespace Recommendation { static std::filesystem::path getCacheDirectory() { - return ServiceProvider::get()->getPath("working-dir") / "cache" / "features"; + return Service::get()->getPath("working-dir") / "cache" / "features"; } static std::filesystem::path getCacheNetworkFilePath() @@ -237,7 +237,7 @@ FeaturesClassifierCache::read() void FeaturesClassifierCache::write() const { - std::filesystem::create_directories(ServiceProvider::get()->getPath("working-dir") / "cache" / "features"); + std::filesystem::create_directories(Service::get()->getPath("working-dir") / "cache" / "features"); if (!networkToCacheFile(_network, getCacheNetworkFilePath()) || !objectPositionToCacheFile(_trackPositions, getCacheTrackPositionsFilePath())) diff --git a/src/libs/scanner/impl/AcousticBrainzUtils.cpp b/src/libs/scanner/impl/AcousticBrainzUtils.cpp index 970e04da..b10cdd46 100644 --- a/src/libs/scanner/impl/AcousticBrainzUtils.cpp +++ b/src/libs/scanner/impl/AcousticBrainzUtils.cpp @@ -40,7 +40,7 @@ getJsonData(const UUID& mbid) { static const std::string defaultAPIURL = "https://acousticbrainz.org/api/v1/"; - const std::string url {ServiceProvider::get()->getString("acousticbrainz-api-url", defaultAPIURL) + std::string {mbid.getAsString()} + "/low-level"}; + const std::string url {Service::get()->getString("acousticbrainz-api-url", defaultAPIURL) + std::string {mbid.getAsString()} + "/low-level"}; boost::asio::io_service ioService; diff --git a/src/libs/subsonic/impl/Scan.cpp b/src/libs/subsonic/impl/Scan.cpp index b663aed2..3b9325a6 100644 --- a/src/libs/subsonic/impl/Scan.cpp +++ b/src/libs/subsonic/impl/Scan.cpp @@ -32,7 +32,7 @@ namespace API::Subsonic::Scan { Response::Node statusResponse; - const IMediaScanner::Status scanStatus {ServiceProvider::get()->getStatus()}; + const IMediaScanner::Status scanStatus {Service::get()->getStatus()}; statusResponse.setAttribute("scanning", scanStatus.currentState == IMediaScanner::State::InProgress); if (scanStatus.currentState == IMediaScanner::State::InProgress) @@ -61,7 +61,7 @@ namespace API::Subsonic::Scan Response handleStartScan(RequestContext& context) { - ServiceProvider::get()->requestImmediateScan(false); + Service::get()->requestImmediateScan(false); Response response {Response::createOkResponse(context)}; response.addNode("scanStatus", createStatusResponseNode()); diff --git a/src/libs/subsonic/impl/SubsonicResource.cpp b/src/libs/subsonic/impl/SubsonicResource.cpp index 2c98cdd8..f5cbd81b 100644 --- a/src/libs/subsonic/impl/SubsonicResource.cpp +++ b/src/libs/subsonic/impl/SubsonicResource.cpp @@ -511,10 +511,10 @@ handleChangePassword(RequestContext& context) std::string username {getMandatoryParameterAs(context.parameters, "username")}; std::string password {decodePasswordIfNeeded(getMandatoryParameterAs(context.parameters, "password"))}; - if (!ServiceProvider::get()->evaluatePasswordStrength(username, password)) + if (!Service::get()->evaluatePasswordStrength(username, password)) throw PasswordTooWeakGenericError {}; - const User::PasswordHash hash {ServiceProvider::get()->hashPassword(password)}; + const User::PasswordHash hash {Service::get()->hashPassword(password)}; auto transaction {context.dbSession.createUniqueTransaction()}; @@ -593,10 +593,10 @@ handleCreateUserRequest(RequestContext& context) std::string password {decodePasswordIfNeeded(getMandatoryParameterAs(context.parameters, "password"))}; // Just ignore all the other fields as we don't handle them - if (!ServiceProvider::get()->evaluatePasswordStrength(username, password)) + if (!Service::get()->evaluatePasswordStrength(username, password)) throw PasswordTooWeakGenericError {}; - const User::PasswordHash hash {ServiceProvider::get()->hashPassword(password)}; + const User::PasswordHash hash {Service::get()->hashPassword(password)}; auto transaction {context.dbSession.createUniqueTransaction()}; @@ -878,7 +878,7 @@ handleGetArtistInfoRequestCommon(RequestContext& context, bool id3) artistInfoNode.createChild("musicBrainzId").setValue(artistMBID->getAsString()); } - auto similarArtistsId {ServiceProvider::get()->getSimilarArtists(context.dbSession, id.value, count)}; + auto similarArtistsId {Service::get()->getSimilarArtists(context.dbSession, id.value, count)}; { auto transaction {context.dbSession.createSharedTransaction()}; @@ -1107,7 +1107,7 @@ handleGetSimilarSongsRequestCommon(RequestContext& context, bool id3) // Optional params std::size_t count {getParameterAs(context.parameters, "count").value_or(50)}; - auto similarArtistsId {ServiceProvider::get()->getSimilarArtists(context.dbSession, id.value, 5)}; + auto similarArtistsId {Service::get()->getSimilarArtists(context.dbSession, id.value, 5)}; auto transaction {context.dbSession.createSharedTransaction()}; @@ -1552,10 +1552,10 @@ handleUpdateUserRequest(RequestContext& context) if (password) { *password = decodePasswordIfNeeded(*password); - if (!ServiceProvider::get()->evaluatePasswordStrength(username, *password)) + if (!Service::get()->evaluatePasswordStrength(username, *password)) throw PasswordTooWeakGenericError {}; - hash = ServiceProvider::get()->hashPassword(*password); + hash = Service::get()->hashPassword(*password); } auto transaction {context.dbSession.createUniqueTransaction()}; @@ -1746,10 +1746,10 @@ handleGetCoverArt(RequestContext& context, const Wt::Http::Request& /*request*/, switch (id.type) { case Id::Type::Track: - data = ServiceProvider::get()->getFromTrack(context.dbSession, id.value, CoverArt::Format::JPEG, size); + data = Service::get()->getFromTrack(context.dbSession, id.value, CoverArt::Format::JPEG, size); break; case Id::Type::Release: - data = ServiceProvider::get()->getFromRelease(context.dbSession, id.value, CoverArt::Format::JPEG, size); + data = Service::get()->getFromRelease(context.dbSession, id.value, CoverArt::Format::JPEG, size); break; default: throw BadParameterGenericError {"id"}; @@ -1909,7 +1909,7 @@ SubsonicResource::handleRequest(const Wt::Http::Request &request, Wt::Http::Resp SessionPool::ScopedSession dbSession {_sessionPool}; - switch (ServiceProvider::get()->checkUserPassword(dbSession.get(), + switch (Service::get()->checkUserPassword(dbSession.get(), boost::asio::ip::address::from_string(request.clientAddress()), clientInfo.user, clientInfo.password)) { diff --git a/src/libs/utils/include/utils/Logger.hpp b/src/libs/utils/include/utils/Logger.hpp index 11789b17..430beb39 100644 --- a/src/libs/utils/include/utils/Logger.hpp +++ b/src/libs/utils/include/utils/Logger.hpp @@ -82,5 +82,5 @@ class Logger virtual void processLog(const Log& log) = 0; }; -#define LMS_LOG(module, severity) Log(ServiceProvider::get(), Module::module, Severity::severity).getOstream() +#define LMS_LOG(module, severity) Log(Service::get(), Module::module, Severity::severity).getOstream() diff --git a/src/libs/utils/include/utils/Service.hpp b/src/libs/utils/include/utils/Service.hpp index c53a8c81..95a44e37 100644 --- a/src/libs/utils/include/utils/Service.hpp +++ b/src/libs/utils/include/utils/Service.hpp @@ -19,46 +19,44 @@ #pragma once +#include #include -#include template -class ServiceProvider +class Service { public: - template - static - Class& - create(Args&&... args) + Service(std::unique_ptr service) { - static_assert(std::is_base_of::value); - - assign(std::make_unique(std::forward(args)...)); - return *get(); + assign(std::move(service)); } - template - static - Class& - create(Args&&... args) + ~Service() { - assign(std::make_unique(std::forward(args)...)); - return *get(); + clear(); } - static - Class& - assign(std::unique_ptr service) - { - _service = std::move(service); - return *get(); - } + Service(const Service&) = delete; + Service(Service&&) = delete; + Service& operator=(const Service&) = delete; + Service& operator=(Service&&) = delete; - static void clear() { _service.reset(); } + Class* operator->() const + { + return Service::get(); + } static Class* get() { return _service.get(); } private: + static Class& assign(std::unique_ptr service) + { + assert(!_service); + _service = std::move(service); + return *get(); + } + static void clear() { _service.reset(); } + static inline std::unique_ptr _service; }; diff --git a/src/lms/main.cpp b/src/lms/main.cpp index 51244cc1..c70f5ca3 100644 --- a/src/lms/main.cpp +++ b/src/lms/main.cpp @@ -42,31 +42,31 @@ generateWtConfig(std::string execPath) { std::vector args; - const std::filesystem::path wtConfigPath {ServiceProvider::get()->getPath("working-dir") / "wt_config.xml"}; - const std::filesystem::path wtLogFilePath {ServiceProvider::get()->getPath("log-file", "/var/log/lms.log")}; - const std::filesystem::path wtAccessLogFilePath {ServiceProvider::get()->getPath("access-log-file", "/var/log/lms.access.log")}; - const std::filesystem::path wtResourcesPath {ServiceProvider::get()->getPath("wt-resources", "/usr/share/Wt/resources")}; + const std::filesystem::path wtConfigPath {Service::get()->getPath("working-dir") / "wt_config.xml"}; + const std::filesystem::path wtLogFilePath {Service::get()->getPath("log-file", "/var/log/lms.log")}; + const std::filesystem::path wtAccessLogFilePath {Service::get()->getPath("access-log-file", "/var/log/lms.access.log")}; + const std::filesystem::path wtResourcesPath {Service::get()->getPath("wt-resources", "/usr/share/Wt/resources")}; args.push_back(execPath); args.push_back("--config=" + wtConfigPath.string()); - args.push_back("--docroot=" + ServiceProvider::get()->getString("docroot")); - args.push_back("--approot=" + ServiceProvider::get()->getString("approot")); - args.push_back("--deploy-path=" + ServiceProvider::get()->getString("deploy-path", "/")); + args.push_back("--docroot=" + Service::get()->getString("docroot")); + args.push_back("--approot=" + Service::get()->getString("approot")); + args.push_back("--deploy-path=" + Service::get()->getString("deploy-path", "/")); if (!wtResourcesPath.empty()) args.push_back("--resources-dir=" + wtResourcesPath.string()); - if (ServiceProvider::get()->getBool("tls-enable", false)) + if (Service::get()->getBool("tls-enable", false)) { - args.push_back("--https-port=" + std::to_string( ServiceProvider::get()->getULong("listen-port", 5082))); - args.push_back("--https-address=" + ServiceProvider::get()->getString("listen-addr", "0.0.0.0")); - args.push_back("--ssl-certificate=" + ServiceProvider::get()->getString("tls-cert")); - args.push_back("--ssl-private-key=" + ServiceProvider::get()->getString("tls-key")); - args.push_back("--ssl-tmp-dh=" + ServiceProvider::get()->getString("tls-dh")); + args.push_back("--https-port=" + std::to_string( Service::get()->getULong("listen-port", 5082))); + args.push_back("--https-address=" + Service::get()->getString("listen-addr", "0.0.0.0")); + args.push_back("--ssl-certificate=" + Service::get()->getString("tls-cert")); + args.push_back("--ssl-private-key=" + Service::get()->getString("tls-key")); + args.push_back("--ssl-tmp-dh=" + Service::get()->getString("tls-dh")); } else { - args.push_back("--http-port=" + std::to_string( ServiceProvider::get()->getULong("listen-port", 5082))); - args.push_back("--http-address=" + ServiceProvider::get()->getString("listen-addr", "0.0.0.0")); + args.push_back("--http-port=" + std::to_string( Service::get()->getULong("listen-port", 5082))); + args.push_back("--http-address=" + Service::get()->getString("listen-addr", "0.0.0.0")); } if (!wtAccessLogFilePath.empty()) @@ -77,8 +77,8 @@ generateWtConfig(std::string execPath) pt.put("server.application-settings..location", "*"); pt.put("server.application-settings.log-file", wtLogFilePath.string()); - pt.put("server.application-settings.log-config", ServiceProvider::get()->getString("log-config", "* -debug -info:WebRequest")); - pt.put("server.application-settings.behind-reverse-proxy", ServiceProvider::get()->getBool("behind-reverse-proxy", false)); + pt.put("server.application-settings.log-config", Service::get()->getString("log-config", "* -debug -info:WebRequest")); + pt.put("server.application-settings.behind-reverse-proxy", Service::get()->getBool("behind-reverse-proxy", false)); { boost::property_tree::ptree viewport; @@ -132,12 +132,12 @@ int main(int argc, char* argv[]) // Make pstream work with ffmpeg close(STDIN_FILENO); - ServiceProvider::assign(createConfig(configFilePath)); - ServiceProvider::create(); + Service config {createConfig(configFilePath)}; + Service logger {std::make_unique()}; // Make sure the working directory exists - std::filesystem::create_directories(ServiceProvider::get()->getPath("working-dir")); - std::filesystem::create_directories(ServiceProvider::get()->getPath("working-dir") / "cache"); + std::filesystem::create_directories(config->getPath("working-dir")); + std::filesystem::create_directories(config->getPath("working-dir") / "cache"); // Construct WT configuration and get the argc/argv back std::vector wtServerArgs = generateWtConfig(argv[0]); @@ -156,7 +156,7 @@ int main(int argc, char* argv[]) Av::Transcoder::init(); // Initializing a connection pool to the database that will be shared along services - Database::Db database {ServiceProvider::get()->getPath("working-dir") / "lms.db"}; + Database::Db database {config->getPath("working-dir") / "lms.db"}; { Database::Session session {database}; session.prepareTables(); @@ -166,22 +166,21 @@ int main(int argc, char* argv[]) UserInterface::LmsApplicationGroupContainer appGroups; // Service initialization order is important - ServiceProvider::assign(Auth::createAuthTokenService(ServiceProvider::get()->getULong("login-throttler-max-entriees", 10000))); - ServiceProvider::assign(Auth::createPasswordService(ServiceProvider::get()->getULong("login-throttler-max-entriees", 10000))); - Scanner::IMediaScanner& mediaScanner {ServiceProvider::assign(Scanner::createMediaScanner(database))}; + Service authTokenService {Auth::createAuthTokenService(config->getULong("login-throttler-max-entriees", 10000))}; + Service passwordService {Auth::createPasswordService(config->getULong("login-throttler-max-entriees", 10000))}; + Service coverArtService {CoverArt::createGrabber(argv[0])}; + coverArtService->setDefaultCover(server.appRoot() + "/images/unknown-cover.jpg"); + Service recommendationEngineService {Recommendation::createEngine(database)}; + Service mediaScannerService {Scanner::createMediaScanner(database)}; - Recommendation::IEngine& recommendationEngine {ServiceProvider::assign(Recommendation::createEngine(database))}; - CoverArt::IGrabber& coverArtGrabber {ServiceProvider::assign(CoverArt::createGrabber(argv[0]))}; - coverArtGrabber.setDefaultCover(server.appRoot() + "/images/unknown-cover.jpg"); - - mediaScanner.scanComplete().connect([&]() + mediaScannerService->scanComplete().connect([&]() { - auto status = mediaScanner.getStatus(); + auto status = mediaScannerService->getStatus(); if (status.lastCompleteScanStats->nbChanges() > 0 || status.lastCompleteScanStats->featuresFetched > 0) { LMS_LOG(MAIN, INFO) << "Scanner changed some files, reloading the recommendation engine..."; - recommendationEngine.requestReload(); + recommendationEngineService->requestReload(); } else { @@ -189,13 +188,13 @@ int main(int argc, char* argv[]) } // Flush cover cache even if no changes: // covers may be external files that changed and we don't keep track of them - coverArtGrabber.flushCache(); + coverArtService->flushCache(); }); API::Subsonic::SubsonicResource subsonicResource {database}; // bind API resources - if (ServiceProvider::get()->getBool("api-subsonic", true)) + if (config->getBool("api-subsonic", true)) server.addResource(&subsonicResource, subsonicResource.getPath()); // bind UI entry point @@ -203,32 +202,15 @@ int main(int argc, char* argv[]) std::bind(UserInterface::LmsApplication::create, std::placeholders::_1, std::ref(database), std::ref(appGroups))); - // Start - LMS_LOG(MAIN, INFO) << "Starting recommendation engine"; - recommendationEngine.start(); - - LMS_LOG(MAIN, INFO) << "Starting media scanner..."; - mediaScanner.start(); - LMS_LOG(MAIN, INFO) << "Starting server..."; server.start(); - // Wait LMS_LOG(MAIN, INFO) << "Now running..."; Wt::WServer::waitForShutdown(); - // Stop LMS_LOG(MAIN, INFO) << "Stopping server..."; server.stop(); - LMS_LOG(MAIN, INFO) << "Stopping media scanner..."; - mediaScanner.stop(); - - LMS_LOG(MAIN, INFO) << "Stopping recommendation engine..."; - recommendationEngine.stop(); - - ServiceProvider::clear(); - LMS_LOG(MAIN, INFO) << "Clean stop!"; res = EXIT_SUCCESS; } diff --git a/src/lms/ui/Auth.cpp b/src/lms/ui/Auth.cpp index cae9ee16..e3cab312 100644 --- a/src/lms/ui/Auth.cpp +++ b/src/lms/ui/Auth.cpp @@ -43,7 +43,7 @@ static void createAuthToken(Database::IdType userId, const Wt::WDateTime& expiry) { - const std::string secret {ServiceProvider<::Auth::IAuthTokenService>::get()->createAuthToken(LmsApp->getDbSession(), userId, expiry)}; + const std::string secret {Service<::Auth::IAuthTokenService>::get()->createAuthToken(LmsApp->getDbSession(), userId, expiry)}; LmsApp->setCookie(authCookieName, secret, @@ -61,7 +61,7 @@ processAuthToken(const Wt::WEnvironment& env) if (!authCookie) return std::nullopt; - const auto res {ServiceProvider<::Auth::IAuthTokenService>::get()->processAuthToken(LmsApp->getDbSession(), boost::asio::ip::address::from_string(env.clientAddress()), *authCookie)}; + const auto res {Service<::Auth::IAuthTokenService>::get()->processAuthToken(LmsApp->getDbSession(), boost::asio::ip::address::from_string(env.clientAddress()), *authCookie)}; switch (res.state) { case ::Auth::IAuthTokenService::AuthTokenProcessResult::State::NotFound: @@ -124,7 +124,7 @@ class AuthModel : public Wt::WFormModel if (field == PasswordField) { - switch (ServiceProvider<::Auth::IPasswordService>::get()->checkUserPassword( + switch (Service<::Auth::IPasswordService>::get()->checkUserPassword( LmsApp->getDbSession(), boost::asio::ip::address::from_string(LmsApp->environment().clientAddress()), valueText(LoginNameField).toUTF8(), diff --git a/src/lms/ui/LmsApplication.cpp b/src/lms/ui/LmsApplication.cpp index de5ad4c3..adeb8ce7 100644 --- a/src/lms/ui/LmsApplication.cpp +++ b/src/lms/ui/LmsApplication.cpp @@ -530,7 +530,7 @@ LmsApplication::createHome() { const std::string sessionId {LmsApp->sessionId()}; - ServiceProvider::get()->scanStarted().connect(this, [=] () + Service::get()->scanStarted().connect(this, [=] () { Wt::WServer::instance()->post(sessionId, [=] { @@ -539,7 +539,7 @@ LmsApplication::createHome() }); }); - ServiceProvider::get()->scanComplete().connect(this, [=] () + Service::get()->scanComplete().connect(this, [=] () { Wt::WServer::instance()->post(sessionId, [=] { @@ -548,7 +548,7 @@ LmsApplication::createHome() }); }); - ServiceProvider::get()->scanInProgress().connect(this, [=] (Scanner::ScanStepStats stepStats) + Service::get()->scanInProgress().connect(this, [=] (Scanner::ScanStepStats stepStats) { Wt::WServer::instance()->post(sessionId, [=] { @@ -557,7 +557,7 @@ LmsApplication::createHome() }); }); - ServiceProvider::get()->scheduled().connect(this, [=] (Wt::WDateTime dateTime) + Service::get()->scheduled().connect(this, [=] (Wt::WDateTime dateTime) { Wt::WServer::instance()->post(sessionId, [=] { @@ -572,7 +572,7 @@ LmsApplication::createHome() { if (isUserAdmin()) { - const auto& stats {*ServiceProvider::get()->getStatus().lastCompleteScanStats}; + const auto& stats {*Service::get()->getStatus().lastCompleteScanStats}; notifyMsg(MsgType::Info, Wt::WString::tr("Lms.Admin.Database.scan-complete") .arg(static_cast(stats.nbFiles())) diff --git a/src/lms/ui/PlayQueue.cpp b/src/lms/ui/PlayQueue.cpp index ab3d06da..6d5f3765 100644 --- a/src/lms/ui/PlayQueue.cpp +++ b/src/lms/ui/PlayQueue.cpp @@ -500,7 +500,7 @@ PlayQueue::addSome() void PlayQueue::enqueueRadioTrack() { - const std::vector trackToAddIds {ServiceProvider::get()->getSimilarTracksFromTrackList(LmsApp->getDbSession(), _tracklistId, 1)}; + const std::vector trackToAddIds {Service::get()->getSimilarTracksFromTrackList(LmsApp->getDbSession(), _tracklistId, 1)}; enqueueTracks(trackToAddIds); } diff --git a/src/lms/ui/SettingsView.cpp b/src/lms/ui/SettingsView.cpp index 33cfd766..ebecb3d9 100644 --- a/src/lms/ui/SettingsView.cpp +++ b/src/lms/ui/SettingsView.cpp @@ -120,7 +120,7 @@ class SettingsModel : public Wt::WFormModel User::PasswordHash passwordHash; if (!valueText(PasswordField).empty()) - passwordHash = ServiceProvider<::Auth::IPasswordService>::get()->hashPassword(valueText(PasswordField).toUTF8()); + passwordHash = Service<::Auth::IPasswordService>::get()->hashPassword(valueText(PasswordField).toUTF8()); auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; @@ -243,7 +243,7 @@ class SettingsModel : public Wt::WFormModel { if (!valueText(PasswordOldField).empty()) { - switch (ServiceProvider<::Auth::IPasswordService>::get()->checkUserPassword( + switch (Service<::Auth::IPasswordService>::get()->checkUserPassword( LmsApp->getDbSession(), boost::asio::ip::address::from_string(LmsApp->environment().clientAddress()), LmsApp->getUserLoginName(), @@ -271,7 +271,7 @@ class SettingsModel : public Wt::WFormModel { if (!valueText(PasswordField).empty()) { - if (!ServiceProvider<::Auth::IPasswordService>::get()->evaluatePasswordStrength(LmsApp->getUserLoginName(), valueText(PasswordField).toUTF8())) + if (!Service<::Auth::IPasswordService>::get()->evaluatePasswordStrength(LmsApp->getUserLoginName(), valueText(PasswordField).toUTF8())) error = Wt::WString::tr("Lms.password-too-weak"); } else @@ -468,7 +468,7 @@ SettingsView::refreshView() // Subsonic { - t->setCondition("if-has-subsonic-api", ServiceProvider::get()->getBool("api-subsonic", true)); + t->setCondition("if-has-subsonic-api", Service::get()->getBool("api-subsonic", true)); // Transcode auto transcode {std::make_unique()}; diff --git a/src/lms/ui/admin/DatabaseSettingsView.cpp b/src/lms/ui/admin/DatabaseSettingsView.cpp index 3b734728..34ed61a2 100644 --- a/src/lms/ui/admin/DatabaseSettingsView.cpp +++ b/src/lms/ui/admin/DatabaseSettingsView.cpp @@ -230,7 +230,7 @@ DatabaseSettingsView::refreshView() { model->saveData(); - ServiceProvider::get()->requestReload(); + Service::get()->requestReload(); LmsApp->notifyMsg(MsgType::Success, Wt::WString::tr("Lms.Admin.Database.settings-saved")); } @@ -247,7 +247,7 @@ DatabaseSettingsView::refreshView() immScanBtn->clicked().connect([=] () { - ServiceProvider::get()->requestImmediateScan(false); + Service::get()->requestImmediateScan(false); }); t->updateView(model.get()); diff --git a/src/lms/ui/admin/InitWizardView.cpp b/src/lms/ui/admin/InitWizardView.cpp index a14a386e..bbec809c 100644 --- a/src/lms/ui/admin/InitWizardView.cpp +++ b/src/lms/ui/admin/InitWizardView.cpp @@ -62,7 +62,7 @@ class InitWizardModel : public Wt::WFormModel void saveData() { - const Database::User::PasswordHash passwordHash {ServiceProvider<::Auth::IPasswordService>::get()->hashPassword(valueText(PasswordField).toUTF8())}; + const Database::User::PasswordHash passwordHash {Service<::Auth::IPasswordService>::get()->hashPassword(valueText(PasswordField).toUTF8())}; auto transaction(LmsApp->getDbSession().createUniqueTransaction()); @@ -97,7 +97,7 @@ class InitWizardModel : public Wt::WFormModel if (!valueText(PasswordField).empty()) { // Evaluate the strength of the password - if (!ServiceProvider<::Auth::IPasswordService>::get()->evaluatePasswordStrength(valueText(AdminLoginField).toUTF8(), valueText(PasswordField).toUTF8())) + if (!Service<::Auth::IPasswordService>::get()->evaluatePasswordStrength(valueText(AdminLoginField).toUTF8(), valueText(PasswordField).toUTF8())) error = Wt::WString::tr("Lms.password-too-weak"); } else diff --git a/src/lms/ui/admin/ScannerController.cpp b/src/lms/ui/admin/ScannerController.cpp index 2e971064..f6193b98 100644 --- a/src/lms/ui/admin/ScannerController.cpp +++ b/src/lms/ui/admin/ScannerController.cpp @@ -146,20 +146,20 @@ ScannerController::refreshContents() actionBtn->actionButton()->setText(Wt::WString::tr("Lms.Admin.ScannerController.scan-now")); actionBtn->actionButton()->clicked().connect([] { - ServiceProvider::get()->requestImmediateScan(false); + Service::get()->requestImmediateScan(false); }); auto popup = std::make_unique(); popup->addItem(Wt::WString::tr("Lms.Admin.ScannerController.force-scan-now")); popup->itemSelected().connect([] { - ServiceProvider::get()->requestImmediateScan(true); + Service::get()->requestImmediateScan(true); }); actionBtn->dropDownButton()->setMenu(std::move(popup)); actionBtn->dropDownButton()->addStyleClass("btn-primary"); - const IMediaScanner::Status status {ServiceProvider::get()->getStatus()}; + const IMediaScanner::Status status {Service::get()->getStatus()}; if (status.lastCompleteScanStats) { bindString("last-scan", Wt::WString::tr("Lms.Admin.ScannerController.last-scan-status") diff --git a/src/lms/ui/admin/UserView.cpp b/src/lms/ui/admin/UserView.cpp index 5255cd60..63c118e3 100644 --- a/src/lms/ui/admin/UserView.cpp +++ b/src/lms/ui/admin/UserView.cpp @@ -80,7 +80,7 @@ class UserModel : public Wt::WFormModel { std::optional passwordHash; if (!valueText(PasswordField).empty()) - passwordHash = ServiceProvider<::Auth::IPasswordService>::get()->hashPassword(valueText(PasswordField).toUTF8()); + passwordHash = Service<::Auth::IPasswordService>::get()->hashPassword(valueText(PasswordField).toUTF8()); auto transaction {LmsApp->getDbSession().createUniqueTransaction()}; @@ -143,7 +143,7 @@ class UserModel : public Wt::WFormModel else { // Evaluate the strength of the password for non demo accounts - if (!ServiceProvider<::Auth::IPasswordService>::get()->evaluatePasswordStrength(getLoginName(), valueText(PasswordField).toUTF8())) + if (!Service<::Auth::IPasswordService>::get()->evaluatePasswordStrength(getLoginName(), valueText(PasswordField).toUTF8())) error = Wt::WString::tr("Lms.password-too-weak"); } } @@ -304,7 +304,7 @@ UserView::refreshView() // Demo account t->setFormWidget(UserModel::DemoField, std::make_unique()); - if (!userId && ServiceProvider::get()->getBool("demo", false)) + if (!userId && Service::get()->getBool("demo", false)) t->setCondition("if-demo", true); Wt::WPushButton* saveBtn = t->bindNew("save-btn", Wt::WString::tr(userId ? "Lms.save" : "Lms.create")); diff --git a/src/lms/ui/common/AuthModeModel.cpp b/src/lms/ui/common/AuthModeModel.cpp index 1a74b440..e96b6daa 100644 --- a/src/lms/ui/common/AuthModeModel.cpp +++ b/src/lms/ui/common/AuthModeModel.cpp @@ -30,9 +30,9 @@ createAuthModeModel() { auto model {std::make_unique()}; - if (ServiceProvider<::Auth::IPasswordService>::get()->isAuthModeSupported(Database::User::AuthMode::Internal)) + if (Service<::Auth::IPasswordService>::get()->isAuthModeSupported(Database::User::AuthMode::Internal)) model->add(Wt::WString::tr("Lms.Admin.User.auth-mode.internal"), Database::User::AuthMode::Internal); - if (ServiceProvider<::Auth::IPasswordService>::get()->isAuthModeSupported(Database::User::AuthMode::PAM)) + if (Service<::Auth::IPasswordService>::get()->isAuthModeSupported(Database::User::AuthMode::PAM)) model->add(Wt::WString::tr("Lms.Admin.User.auth-mode.pam"), Database::User::AuthMode::PAM); return model; diff --git a/src/lms/ui/explore/ArtistView.cpp b/src/lms/ui/explore/ArtistView.cpp index 082f0634..4f86b4a0 100644 --- a/src/lms/ui/explore/ArtistView.cpp +++ b/src/lms/ui/explore/ArtistView.cpp @@ -73,7 +73,7 @@ Artist::refreshView() if (!artistId) return; - const std::vector similarArtistIds {ServiceProvider::get()->getSimilarArtists(LmsApp->getDbSession(), *artistId, 5)}; + const std::vector similarArtistIds {Service::get()->getSimilarArtists(LmsApp->getDbSession(), *artistId, 5)}; auto transaction {LmsApp->getDbSession().createSharedTransaction()}; diff --git a/src/lms/ui/explore/ReleaseView.cpp b/src/lms/ui/explore/ReleaseView.cpp index aa2d4350..4c9d10b7 100644 --- a/src/lms/ui/explore/ReleaseView.cpp +++ b/src/lms/ui/explore/ReleaseView.cpp @@ -76,7 +76,7 @@ Release::refreshView() if (!releaseId) return; - const std::vector similarReleasesIds {ServiceProvider::get()->getSimilarReleases(LmsApp->getDbSession(), *releaseId, 6)}; + const std::vector similarReleasesIds {Service::get()->getSimilarReleases(LmsApp->getDbSession(), *releaseId, 6)}; auto transaction {LmsApp->getDbSession().createSharedTransaction()}; diff --git a/src/lms/ui/resource/ImageResource.cpp b/src/lms/ui/resource/ImageResource.cpp index 37adb781..a99e7317 100644 --- a/src/lms/ui/resource/ImageResource.cpp +++ b/src/lms/ui/resource/ImageResource.cpp @@ -90,7 +90,7 @@ ImageResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Respons // DbSession are not thread safe { Wt::WApplication::UpdateLock lock {LmsApp}; - cover = ServiceProvider::get()->getFromTrack(LmsApp->getDbSession(), *trackId, CoverArt::Format::JPEG, *size); + cover = Service::get()->getFromTrack(LmsApp->getDbSession(), *trackId, CoverArt::Format::JPEG, *size); } } else if (releaseIdStr) @@ -104,7 +104,7 @@ ImageResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Respons // DbSession are not thread safe { Wt::WApplication::UpdateLock lock {LmsApp}; - cover = ServiceProvider::get()->getFromRelease(LmsApp->getDbSession(), *releaseId, CoverArt::Format::JPEG, *size); + cover = Service::get()->getFromRelease(LmsApp->getDbSession(), *releaseId, CoverArt::Format::JPEG, *size); } } else diff --git a/src/test/database/DatabaseTest.cpp b/src/test/database/DatabaseTest.cpp index c96a625d..8da1ecf6 100644 --- a/src/test/database/DatabaseTest.cpp +++ b/src/test/database/DatabaseTest.cpp @@ -1882,7 +1882,7 @@ int main() try { // log to stdout - ServiceProvider::create(std::cout); + Service logger {std::make_unique(std::cout)}; const std::filesystem::path tmpFile {std::tmpnam(nullptr)}; ScopedFileDeleter tmpFileDeleter {tmpFile}; diff --git a/src/tools/metadata/LmsMetadata.cpp b/src/tools/metadata/LmsMetadata.cpp index 24071070..963d44b0 100644 --- a/src/tools/metadata/LmsMetadata.cpp +++ b/src/tools/metadata/LmsMetadata.cpp @@ -156,7 +156,7 @@ int main(int argc, char *argv[]) try { // log to stdout - ServiceProvider::create(std::cout); + Service logger {std::make_unique(std::cout)}; for (std::size_t i {}; i < static_cast(argc - 1); ++i) { diff --git a/src/tools/recommendation/LmsRecommendation.cpp b/src/tools/recommendation/LmsRecommendation.cpp index dd8a017c..ecb625f8 100644 --- a/src/tools/recommendation/LmsRecommendation.cpp +++ b/src/tools/recommendation/LmsRecommendation.cpp @@ -131,15 +131,15 @@ int main(int argc, char *argv[]) try { // log to stdout - ServiceProvider::create(std::cout); + Service logger {std::make_unique(std::cout)}; std::filesystem::path configFilePath {"/etc/lms.conf"}; if (argc >= 2) configFilePath = std::string(argv[1], 0, 256); - ServiceProvider::assign(createConfig(configFilePath)); + Service config {createConfig(configFilePath)}; - Database::Db db {ServiceProvider::get()->getPath("working-dir") / "lms.db"}; + Database::Db db {config->getPath("working-dir") / "lms.db"}; Database::Session session {db}; std::cout << "Creating recommendation engine..." << std::endl; From d30283eee592661d063e995c1d3a76aea33c9a94 Mon Sep 17 00:00:00 2001 From: emeric Date: Mon, 24 Aug 2020 11:06:31 +0200 Subject: [PATCH 02/24] Detect embedded M4A cover arts. fixes #89 --- src/libs/metadata/impl/TagLibParser.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/libs/metadata/impl/TagLibParser.cpp b/src/libs/metadata/impl/TagLibParser.cpp index 2a83651a..85497ab8 100644 --- a/src/libs/metadata/impl/TagLibParser.cpp +++ b/src/libs/metadata/impl/TagLibParser.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -386,6 +387,15 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug) getAPETags(mp3File->APETag()); } + //MP4 + else if (TagLib::MP4::File* mp4File {dynamic_cast(f.file())}) + { + auto& coverItem {mp4File->tag()->itemListMap()["covr"]}; + TagLib::MP4::CoverArtList coverArtList {coverItem.toCoverArtList()}; + if (!coverArtList.isEmpty()) + track.hasCover = true; + } + // MPC else if (TagLib::MPC::File* mpcFile {dynamic_cast(f.file())}) { getAPETags(mpcFile->APETag()); From 5e37ec865a8e05dc60ca1abbe8814f05204cf2d5 Mon Sep 17 00:00:00 2001 From: emeric Date: Mon, 24 Aug 2020 14:08:04 +0200 Subject: [PATCH 03/24] Force next scan to full rescan database due to better cover detection. ref #89 --- 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 48d3de2e..ad86ece3 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 25 +#define LMS_DATABASE_VERSION 26 using Version = std::size_t; @@ -272,6 +272,12 @@ CREATE TABLE "user_backup" ( // User's AuthMode _session.execute("ALTER TABLE user ADD auth_mode INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast(User::defaultAuthMode)) + ")"); } + else if (version == 25) + { + // 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 a785847b9c1705b27d37974f1ca822b29adace84 Mon Sep 17 00:00:00 2001 From: emeric Date: Thu, 27 Aug 2020 13:31:25 +0200 Subject: [PATCH 04/24] [WIP] Zipper to download artists/releases, ref #37 --- approot/messages.xml | 1 + approot/messages_fr.xml | 1 + src/libs/utils/CMakeLists.txt | 1 + src/libs/utils/impl/Path.cpp | 17 +- src/libs/utils/impl/Zipper.cpp | 499 +++++++++++++++++++++++ src/libs/utils/include/utils/Path.hpp | 2 +- src/libs/utils/include/utils/String.hpp | 1 + src/libs/utils/include/utils/Zipper.hpp | 94 +++++ src/lms/CMakeLists.txt | 1 + src/lms/ui/LmsApplication.cpp | 5 +- src/lms/ui/explore/ArtistView.cpp | 4 +- src/lms/ui/explore/ReleasePopup.cpp | 3 + src/lms/ui/resource/DownloadResource.cpp | 225 ++++++++++ src/lms/ui/resource/DownloadResource.hpp | 64 +++ src/lms/ui/resource/ImageResource.hpp | 3 +- src/tools/CMakeLists.txt | 1 + src/tools/zipper/CMakeLists.txt | 9 + src/tools/zipper/LmsZipper.cpp | 71 ++++ 18 files changed, 985 insertions(+), 17 deletions(-) create mode 100644 src/libs/utils/impl/Zipper.cpp create mode 100644 src/libs/utils/include/utils/Zipper.hpp create mode 100644 src/lms/ui/resource/DownloadResource.cpp create mode 100644 src/lms/ui/resource/DownloadResource.hpp create mode 100644 src/tools/zipper/CMakeLists.txt create mode 100644 src/tools/zipper/LmsZipper.cpp diff --git a/approot/messages.xml b/approot/messages.xml index 146afb5e..d60cd82c 100644 --- a/approot/messages.xml +++ b/approot/messages.xml @@ -115,6 +115,7 @@ Add filter All Artists +Download Filter added Filters Links diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml index 11dc324e..310fe585 100644 --- a/approot/messages_fr.xml +++ b/approot/messages_fr.xml @@ -115,6 +115,7 @@ Ajouter filtre Tous Artistes +Télécharger Filtre ajouté Filtres Liens diff --git a/src/libs/utils/CMakeLists.txt b/src/libs/utils/CMakeLists.txt index 89505632..37a21f76 100644 --- a/src/libs/utils/CMakeLists.txt +++ b/src/libs/utils/CMakeLists.txt @@ -10,6 +10,7 @@ add_library(lmsutils SHARED impl/String.cpp impl/UUID.cpp impl/WtLogger.cpp + impl/Zipper.cpp ) target_include_directories(lmsutils INTERFACE diff --git a/src/libs/utils/impl/Path.cpp b/src/libs/utils/impl/Path.cpp index 0bfc807b..886ad9aa 100644 --- a/src/libs/utils/impl/Path.cpp +++ b/src/libs/utils/impl/Path.cpp @@ -32,14 +32,13 @@ #include "utils/Exception.hpp" #include "utils/Logger.hpp" -void -computeCrc(const std::filesystem::path& p, std::vector& crc) +std::uint32_t +computeCrc32(const std::filesystem::path& p) { using crc_type = boost::crc_32_type; crc_type result; - std::ifstream ifs( p.string().c_str(), std::ios_base::binary ); - + std::ifstream ifs {p.string().c_str(), std::ios_base::binary}; if (ifs) { do @@ -49,7 +48,7 @@ computeCrc(const std::filesystem::path& p, std::vector& crc) ifs.read( buffer.data(), buffer.size() ); result.process_bytes( buffer.data(), ifs.gcount() ); } - while ( ifs ); + while (ifs); } else { @@ -58,13 +57,7 @@ computeCrc(const std::filesystem::path& p, std::vector& crc) } - // Copy the result into a vector of unsigned char - const crc_type::value_type checksum = result.checksum(); - for (std::size_t i = 0; (i+1)*8 <= crc_type::bit_count; i++) - { - const unsigned char* data = reinterpret_cast( &checksum ); - crc.push_back(data[i]); - } + return result.checksum(); } bool diff --git a/src/libs/utils/impl/Zipper.cpp b/src/libs/utils/impl/Zipper.cpp new file mode 100644 index 00000000..f81fa33d --- /dev/null +++ b/src/libs/utils/impl/Zipper.cpp @@ -0,0 +1,499 @@ +/* + * 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 "utils/Zipper.hpp" + +#include +#include + +#include "utils/Path.hpp" +#include "utils/Logger.hpp" + +// Done using specs from https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT + +namespace Zip +{ + + class ZipHeader + { + public: + ZipHeader(std::byte* buffer, std::size_t bufferSize) + : _buffer {buffer} + , _bufferSize {bufferSize} + {} + + enum GeneralPurposeFlag : std::uint16_t + { + UseDataDescriptor = 1 << 3, + }; + + enum CompressionMethod : std::uint16_t + { + NoCompression = 0, + }; + + + + protected: + void write8(std::size_t offset, std::uint8_t value); + void write16(std::size_t offset, std::uint16_t value); + void write32(std::size_t offset, std::uint32_t value); + + private: + std::byte* _buffer {}; + std::size_t _bufferSize {}; + }; + + void + ZipHeader::write8(std::size_t offset, std::uint8_t value) + { + _buffer[offset] = static_cast(value); + } + + void + ZipHeader::write16(std::size_t offset, std::uint16_t value) + { + _buffer[offset] = static_cast(value & 0xff); + _buffer[offset + 1] = static_cast(value >> 8); + } + + void + ZipHeader::write32(std::size_t offset, std::uint32_t value) + { + _buffer[offset] = static_cast(value & 0xff); + _buffer[offset + 1] = static_cast((value >> 8) & 0xff); + _buffer[offset + 2] = static_cast((value >> 16) & 0xff); + _buffer[offset + 3] = static_cast(value >> 24); + } + + class LocalFileHeader : public ZipHeader + { + public: + using ZipHeader::ZipHeader; + + // Setters + void setSignature(); + void setVersionNeededToExtract(unsigned major, unsigned minor); + void setGeneralPurposeFlags(std::uint16_t flags); + void setCompressionMethod(CompressionMethod compressionMethod); + void setLastModifiedDateTime(); + void setCrc32UncompressedData(std::uint32_t crc); + void setCompressedSize(std::size_t size); + void setUncompressedSize(std::size_t size); + void setFileNameLength(std::size_t size); + void setExtraFieldLength(std::size_t size); + static constexpr std::size_t getHeaderSize() { return 30; } + }; + + void + LocalFileHeader::setSignature() + { + write32(0, 0x04034b50); + } + + void + LocalFileHeader::setVersionNeededToExtract(unsigned major, unsigned minor) + { + assert(minor < 10); + write16(4, major*10 + minor); + } + + void + LocalFileHeader::setGeneralPurposeFlags(std::uint16_t flags) + { + write16(6, flags); + } + + void + LocalFileHeader::setCompressionMethod(CompressionMethod compressionMethod) + { + write16(8, compressionMethod); + } + + void + LocalFileHeader::setLastModifiedDateTime() + { + // TODO + write16(10, 0); // time + write16(12, 0); // date + } + + void + LocalFileHeader::setCrc32UncompressedData(std::uint32_t crc) + { + write32(14, crc); + } + + void + LocalFileHeader::setCompressedSize(std::size_t size) + { + write32(18, size); + } + + void + LocalFileHeader::setUncompressedSize(std::size_t size) + { + write32(22, size); + } + + void + LocalFileHeader::setFileNameLength(std::size_t size) + { + write16(26, size); + } + + void + LocalFileHeader::setExtraFieldLength(std::size_t size) + { + write16(28, size); + } + + + class CentralDirectoryHeader : public ZipHeader + { + public: + using ZipHeader::ZipHeader; + + void setSignature() { write32(0, 0x02014b50); } + void setVersionMadeBy(unsigned major, unsigned minor) { assert(minor < 10); write16(4, major * 10 + minor); } + void setVersionNeededToExtract(unsigned major, unsigned minor) { assert(minor < 10); write16(6, major*10 + minor); } + void setGeneralPurposeFlags(std::uint16_t flags) { write16(8, flags); } + void setCompressionMethod(CompressionMethod method) { write16(10, method); } + void setLastModifiedDateTime() + { + write16(12, 0); // time + write16(14, 0); // date + } + void setCrc32UncompressedData(std::uint32_t crc32) { write32(16, crc32); } + void setCompressedSize(std::size_t size) { write32(20, size); } + void setUncompressedSize(std::size_t size) { write32(24, size); } + void setFileNameLength(std::size_t size) { write16(28, size); } + void setExtraFieldLength(std::size_t size) { write16(30, size); } + void setFileCommentLength(std::size_t size) { write16(32, size); } + void setDiskNumber(std::size_t number) { write16(34, number); } + void setInternalFileAttributes(std::uint16_t attributes) { write16(36, attributes); } + void setExternalFileAttributes(std::uint16_t attributes) { write32(38, attributes); } + void setRelativeFileHeaderOffset(std::size_t offset) { write32(42, offset); } + static constexpr std::size_t getHeaderSize() { return 46; } + }; + + class EndOfCentralDirectoryRecord : public ZipHeader + { + public: + using ZipHeader::ZipHeader; + + void setSignature() { write32(0, 0x06054b50); } + void setDiskNumber(unsigned number) { write16(4, number); } + void setCentralDirectoryDiskNumber(unsigned number) { write16(6, number); } + void setNbDiskCentralDirectoryRecords(unsigned number) { write16(8, number); } + void setNbCentralDirectoryRecords(unsigned number) { write16(10, number); } + void setCentralDirectorySize(std::size_t size) { write32(12, size); } + void setCentralDirectoryOffset(std::size_t offset) { write32(16, offset); } + void setCommentLength(std::size_t length) { write16(20, length); } + static constexpr std::size_t getHeaderSize() { return 22; } + }; + + Zipper::Zipper(const std::map& files, CompressionMethod compMethod) + : _compMethod {compMethod} + { + for (const auto& [filename, filePath] : files) + { + FileContext fileContext; + fileContext.filePath = filePath; + + std::error_code ec; + fileContext.fileSize = std::filesystem::file_size(filePath, ec); + if (ec) + { + LMS_LOG(UTILS, INFO) << "Cannot get file size for '" << filePath.string() << "': " << ec.message(); + continue; + } + fileContext.fileCrc32 = computeCrc32(filePath); + + LMS_LOG(UTILS, DEBUG) << "Processing '" << filePath.string() << "': File size = " << fileContext.fileSize; + + _files[filename] = std::move(fileContext); + } + + _currentFile = std::begin(_files); + } + + std::size_t + Zipper::writeSome(std::byte* buffer, std::size_t bufferSize) + { + // make sure we have some room for the headers + assert(bufferSize >= minOutputBufferSize); + + std::size_t nbTotalWrittenBytes {}; + + while (!isComplete() && (bufferSize >= minOutputBufferSize)) + { + std::size_t nbWrittenBytes {}; + + LMS_LOG(UTILS, DEBUG) << "Global offset = " << _currentZipOffset; + LMS_LOG(UTILS, DEBUG) << "Buffer ptr = " << buffer << ", remaining size = " << bufferSize; + + switch (_writeState) + { + case WriteState::LocalFileHeader: + nbWrittenBytes = writeLocalFileHeader(buffer, bufferSize); + break; + + case WriteState::LocalFileHeaderFileName: + nbWrittenBytes = writeLocalFileHeaderFileName(buffer, bufferSize); + break; + + case WriteState::FileData: + nbWrittenBytes = writeFileData(buffer, bufferSize); + break; + + case WriteState::CentralDirectoryHeader: + nbWrittenBytes = writeCentralDirectoryHeader(buffer, bufferSize); + break; + + case WriteState::CentralDirectoryHeaderFileName: + nbWrittenBytes = writeCentralDirectoryHeaderFileName(buffer, bufferSize); + break; + + case WriteState::EndOfCentralDirectoryRecord: + nbWrittenBytes = writeEndOfCentralDirectoryRecord(buffer, bufferSize); + break; + + case WriteState::Complete: + break; + } + + LMS_LOG(UI, DEBUG) << "nbWrittenBytes = " << nbWrittenBytes; + + buffer += nbWrittenBytes; + bufferSize -= nbWrittenBytes; + _currentZipOffset += nbWrittenBytes; + nbTotalWrittenBytes += nbWrittenBytes ; + } + + return nbTotalWrittenBytes; + } + + bool + Zipper::isComplete() const + { + return _writeState == WriteState::Complete; + } + + std::size_t + Zipper::writeLocalFileHeader(std::byte* buffer, std::size_t bufferSize) + { + static_assert(LocalFileHeader::getHeaderSize() <= minOutputBufferSize); + + assert(bufferSize >= minOutputBufferSize); + + if (_currentFile == std::cend(_files)) + { + _currentFile = std::begin(_files); + _writeState = WriteState::CentralDirectoryHeader; + return 0; + } + + LMS_LOG(UTILS, INFO) << "writeLocalFileHeader. crc = " << _currentFile->second.fileCrc32; + LocalFileHeader header {buffer, bufferSize}; + + header.setSignature(); + header.setVersionNeededToExtract(1, 0); + header.setGeneralPurposeFlags(0); + header.setCrc32UncompressedData(_currentFile->second.fileCrc32); + switch (_compMethod) + { + case CompressionMethod::NoCompression: + header.setCompressionMethod(ZipHeader::CompressionMethod::NoCompression); + header.setCompressedSize(_currentFile->second.fileSize); + header.setUncompressedSize(_currentFile->second.fileSize); + break; + } + header.setLastModifiedDateTime(); // getLastWriteTime(*_currentFile)); + header.setFileNameLength(_currentFile->first.size()); + header.setExtraFieldLength(0); + + _writeState = WriteState::LocalFileHeaderFileName; + _currentFile->second.localFileHeaderOffset = _currentZipOffset; + + return header.getHeaderSize(); + } + + std::size_t + Zipper::writeLocalFileHeaderFileName(std::byte* buffer, std::size_t bufferSize) + { + const std::string& fileName {_currentFile->first}; + + assert(_currentOffset <= fileName.size()); + if (_currentOffset == fileName.size()) + { + _writeState = WriteState::FileData; + _currentOffset = 0; + return 0; + } + + LMS_LOG(UTILS, INFO) << "writeLocalFileHeaderFileName"; + + const std::size_t nbBytesToCopy {std::min(fileName.size() - _currentOffset, bufferSize)}; + LMS_LOG(UTILS, INFO) << "\tnbBytesToCopy = " << nbBytesToCopy; + + std::copy(std::next(std::begin(fileName), _currentOffset), std::next(std::begin(fileName), nbBytesToCopy), reinterpret_cast(buffer)); + + _currentOffset += nbBytesToCopy; + return nbBytesToCopy; + } + + std::size_t + Zipper::writeFileData(std::byte* buffer, std::size_t bufferSize) + { + if (_currentOffset == _currentFile->second.fileSize) + { + _currentOffset = 0; + ++_currentFile; + _writeState = WriteState::LocalFileHeader; + + return 0; + } + + const std::string filePath {_currentFile->second.filePath.string()}; + + std::ifstream ifs {filePath.c_str(), std::ios_base::binary}; + if (!ifs) + throw ZipperException {"File '" + filePath + "' does no longer exist!"}; + + ifs.seekg(0, std::ios::end); + const ::uint64_t fileSize {static_cast<::uint64_t>(ifs.tellg())}; + ifs.seekg(0, std::ios::beg); + + if (fileSize != _currentFile->second.fileSize) + throw ZipperException {"File '" + filePath + "': size mismatch!"}; + + const std::size_t nbBytesToRead {std::min(fileSize - _currentOffset, bufferSize)}; + + ifs.seekg(_currentOffset, std::ios::beg); + ifs.read(reinterpret_cast(buffer), nbBytesToRead ); + const ::uint64_t actualReadSize {static_cast<::uint64_t>(ifs.gcount())}; + + LMS_LOG(UTILS, INFO) << "writeFileData: to read = " << nbBytesToRead << ", actually read = " << actualReadSize; + + _currentOffset += actualReadSize; + + return actualReadSize; + } + + std::size_t + Zipper::writeCentralDirectoryHeader(std::byte* buffer, std::size_t bufferSize) + { + assert(bufferSize >= minOutputBufferSize); + static_assert(CentralDirectoryHeader::getHeaderSize() <= minOutputBufferSize); + + if (_currentFile == std::begin(_files)) + { + LMS_LOG(UI, INFO) << "First record! _currentZipOffset = " << _currentZipOffset; + _centralDirectoryOffset = _currentZipOffset; + } + + if (_currentFile == std::end(_files)) + { + _writeState = WriteState::EndOfCentralDirectoryRecord; + _currentFile = std::begin(_files); + return 0; + } + + LMS_LOG(UTILS, INFO) << "writeCentralDirectoryHeader. Relative offset = " << _currentFile->second.localFileHeaderOffset << ", crc = " << std::hex << _currentFile->second.fileCrc32; + + CentralDirectoryHeader header {buffer, bufferSize}; + header.setSignature(); + header.setVersionMadeBy(2, 0); + header.setVersionNeededToExtract(1, 0); + header.setGeneralPurposeFlags(0); + switch (_compMethod) + { + case CompressionMethod::NoCompression: + header.setCompressionMethod(ZipHeader::CompressionMethod::NoCompression); + header.setCompressedSize(_currentFile->second.fileSize); + header.setUncompressedSize(_currentFile->second.fileSize); + break; + } + header.setLastModifiedDateTime(); // getLastWriteTime(*_currentFile)); + header.setCrc32UncompressedData(_currentFile->second.fileCrc32); + header.setFileNameLength(_currentFile->first.size()); + header.setExtraFieldLength(0); + header.setFileCommentLength(0); + header.setDiskNumber(0); + header.setInternalFileAttributes(0); + header.setExternalFileAttributes(0); + header.setRelativeFileHeaderOffset(_currentFile->second.localFileHeaderOffset); + + _writeState = WriteState::CentralDirectoryHeaderFileName; + _centralDirectorySize += header.getHeaderSize(); + + return header.getHeaderSize(); + } + + std::size_t + Zipper::writeCentralDirectoryHeaderFileName(std::byte* buffer, std::size_t bufferSize) + { + const std::string& fileName {_currentFile->first}; + + assert(_currentOffset <= fileName.size()); + if (_currentOffset == fileName.size()) + { + _currentOffset = 0; + ++_currentFile; + _writeState = WriteState::CentralDirectoryHeader; + + return 0; + } + + LMS_LOG(UTILS, INFO) << "writeCentralDirectoryHeaderFileName"; + const std::size_t nbBytesToCopy {std::min(fileName.size() - _currentOffset, bufferSize)}; + + std::copy(std::next(std::begin(fileName), _currentOffset), std::next(std::begin(fileName), nbBytesToCopy), reinterpret_cast(buffer)); + + _currentOffset += nbBytesToCopy; + _centralDirectorySize += nbBytesToCopy; + return nbBytesToCopy; + } + + + std::size_t + Zipper::writeEndOfCentralDirectoryRecord(std::byte* buffer, std::size_t bufferSize) + { + assert(bufferSize >= minOutputBufferSize); + static_assert(EndOfCentralDirectoryRecord::getHeaderSize() <= minOutputBufferSize); + + EndOfCentralDirectoryRecord record {buffer, bufferSize}; + + LMS_LOG(UTILS, DEBUG) << "Writing EOR. nb records = " << _files.size() << ", offset = " << _centralDirectoryOffset << ", size = " << _centralDirectorySize; + + record.setSignature(); + record.setDiskNumber(0); + record.setCentralDirectoryDiskNumber(0); + record.setNbDiskCentralDirectoryRecords(_files.size()); + record.setNbCentralDirectoryRecords(_files.size()); + record.setCentralDirectorySize(_centralDirectorySize); + record.setCentralDirectoryOffset(_centralDirectoryOffset); + record.setCommentLength(0); + + _writeState = WriteState::Complete; + return record.getHeaderSize(); + } + +} // namespace Zip diff --git a/src/libs/utils/include/utils/Path.hpp b/src/libs/utils/include/utils/Path.hpp index 573375e5..659442f9 100644 --- a/src/libs/utils/include/utils/Path.hpp +++ b/src/libs/utils/include/utils/Path.hpp @@ -26,7 +26,7 @@ #include -void computeCrc(const std::filesystem::path& p, std::vector& checksum); +std::uint32_t computeCrc32(const std::filesystem::path& p); // Make sure the given path is a directory // Create it if needed diff --git a/src/libs/utils/include/utils/String.hpp b/src/libs/utils/include/utils/String.hpp index d4655931..82f05f29 100644 --- a/src/libs/utils/include/utils/String.hpp +++ b/src/libs/utils/include/utils/String.hpp @@ -71,6 +71,7 @@ template<> std::optional readAs(const std::string& str); +[[nodiscard]] std::string replaceInString(const std::string& str, const std::string& from, const std::string& to); diff --git a/src/libs/utils/include/utils/Zipper.hpp b/src/libs/utils/include/utils/Zipper.hpp new file mode 100644 index 00000000..4692f5c8 --- /dev/null +++ b/src/libs/utils/include/utils/Zipper.hpp @@ -0,0 +1,94 @@ +/* + * 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 +#include +#include + +#include "Exception.hpp" + +namespace Zip +{ + + class ZipperException : public LmsException + { + using LmsException::LmsException; + }; + + // Very simple on-the-fly zip creator + class Zipper + { + public: + + enum class CompressionMethod + { + NoCompression, + }; + + Zipper(const std::map& files, CompressionMethod comp = CompressionMethod::NoCompression); + + static constexpr std::size_t minOutputBufferSize = 64; + std::size_t writeSome(std::byte* buffer, std::size_t bufferSize); + bool isComplete() const; + + private: + void setComplete(); + + std::size_t writeLocalFileHeader(std::byte* buffer, std::size_t bufferSize); + std::size_t writeLocalFileHeaderFileName(std::byte* buffer, std::size_t bufferSize); + std::size_t writeFileData(std::byte* buffer, std::size_t bufferSize); + std::size_t writeCentralDirectoryHeader(std::byte* buffer, std::size_t bufferSize); + std::size_t writeCentralDirectoryHeaderFileName(std::byte* buffer, std::size_t bufferSize); + std::size_t writeEndOfCentralDirectoryRecord(std::byte* buffer, std::size_t bufferSize); + + struct FileContext + { + std::filesystem::path filePath; + std::size_t fileSize; + std::uint32_t fileCrc32; + std::size_t localFileHeaderOffset {}; + }; + + using FileContainer = std::map; + FileContainer _files; + + enum class WriteState + { + LocalFileHeader, + LocalFileHeaderFileName, + FileData, + CentralDirectoryHeader, + CentralDirectoryHeaderFileName, + EndOfCentralDirectoryRecord, + Complete, + }; + + CompressionMethod _compMethod {CompressionMethod::NoCompression}; + WriteState _writeState {WriteState::LocalFileHeader}; + FileContainer::iterator _currentFile; + std::size_t _currentOffset {}; + std::size_t _currentZipOffset {}; + std::size_t _centralDirectoryOffset {}; + std::size_t _centralDirectorySize {}; + }; + +} // namespace Zip + diff --git a/src/lms/CMakeLists.txt b/src/lms/CMakeLists.txt index 88905053..6bb7e998 100644 --- a/src/lms/CMakeLists.txt +++ b/src/lms/CMakeLists.txt @@ -31,6 +31,7 @@ add_executable(lms ui/explore/TracksView.cpp ui/resource/AudioFileResource.cpp ui/resource/AudioTranscodeResource.cpp + ui/resource/DownloadResource.cpp ui/resource/ImageResource.cpp ) diff --git a/src/lms/ui/LmsApplication.cpp b/src/lms/ui/LmsApplication.cpp index adeb8ce7..35dcc8c9 100644 --- a/src/lms/ui/LmsApplication.cpp +++ b/src/lms/ui/LmsApplication.cpp @@ -45,6 +45,7 @@ #include "admin/UsersView.hpp" #include "resource/AudioFileResource.hpp" #include "resource/AudioTranscodeResource.hpp" +#include "resource/DownloadResource.hpp" #include "resource/ImageResource.hpp" #include "Auth.hpp" #include "LmsApplicationException.hpp" @@ -429,9 +430,9 @@ LmsApplication::handleUserLoggedIn(Database::IdType userId, bool strongAuth) void LmsApplication::createHome() { - _imageResource = std::make_shared(); - _audioTranscodeResource = std::make_shared(); _audioFileResource = std::make_shared(); + _audioTranscodeResource = std::make_shared(); + _imageResource = std::make_shared(); declareJavaScriptFunction("onLoadCover", "function(id) { id.className += \" Lms-cover-loaded\"}"); doJavaScript("$('body').tooltip({ selector: '[data-toggle=\"tooltip\"]'})"); diff --git a/src/lms/ui/explore/ArtistView.cpp b/src/lms/ui/explore/ArtistView.cpp index 4f86b4a0..71f58b6a 100644 --- a/src/lms/ui/explore/ArtistView.cpp +++ b/src/lms/ui/explore/ArtistView.cpp @@ -31,7 +31,7 @@ #include "utils/Logger.hpp" #include "utils/String.hpp" -#include "resource/ImageResource.hpp" +#include "resource/DownloadResource.hpp" #include "ArtistListHelpers.hpp" #include "Filters.hpp" #include "LmsApplication.hpp" @@ -131,6 +131,8 @@ Artist::refreshView() { artistsAction.emit(PlayQueueAction::PlayLast, {*artistId}); }); + popup->addItem(Wt::WString::tr("Lms.Explore.download")) + ->setLink(Wt::WLink {std::make_unique(*artistId)}); popup->exec(moreBtn); }); diff --git a/src/lms/ui/explore/ReleasePopup.cpp b/src/lms/ui/explore/ReleasePopup.cpp index 7ce4d099..19755b33 100644 --- a/src/lms/ui/explore/ReleasePopup.cpp +++ b/src/lms/ui/explore/ReleasePopup.cpp @@ -19,6 +19,7 @@ #include "ReleasePopup.hpp" +#include "resource/DownloadResource.hpp" #include "LmsApplication.hpp" namespace UserInterface @@ -41,6 +42,8 @@ namespace UserInterface { releasesAction.emit(PlayQueueAction::PlayLast, {releaseId}); }); + popup->addItem(Wt::WString::tr("Lms.Explore.download")) + ->setLink(Wt::WLink {std::make_unique(releaseId)}); popup->popup(&target); } diff --git a/src/lms/ui/resource/DownloadResource.cpp b/src/lms/ui/resource/DownloadResource.cpp new file mode 100644 index 00000000..1466a45e --- /dev/null +++ b/src/lms/ui/resource/DownloadResource.cpp @@ -0,0 +1,225 @@ +/* + * Copyright (C) 2014 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 "DownloadResource.hpp" + +#include +#include + +#include "database/Artist.hpp" +#include "database/Release.hpp" +#include "database/Track.hpp" +#include "utils/Exception.hpp" +#include "utils/Logger.hpp" +#include "utils/Zipper.hpp" + +#include "LmsApplication.hpp" + +#define LOG(level) LMS_LOG(UI, level) << "Download resource: " + +namespace UserInterface { + +DownloadResource::~DownloadResource() +{ + beingDeleted(); +} + +void +DownloadResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) +{ + try + { + std::shared_ptr zipper; + + // First, see if this request is for a continuation + Wt::Http::ResponseContinuation *continuation = request.continuation(); + if (continuation) + zipper = Wt::cpp17::any_cast>(continuation->data()); + else + { + zipper = createZipper(); + response.setMimeType("application/zip"); + } + + if (!zipper) + return; + + std::array buffer; + std::size_t nbWrittenBytes {zipper->writeSome(buffer.data(), buffer.size())}; + + response.out().write(reinterpret_cast(buffer.data()), nbWrittenBytes); + + if (!zipper->isComplete()) + { + auto* continuation {response.createContinuation()}; + continuation->setData(zipper); + } + } + catch (Zip::ZipperException& exception) + { + LOG(ERROR) << "Zipper exception: " << exception.what(); + } +} + + +static +std::string +getArtistPathName(Database::Artist::pointer artist) +{ + return StringUtils::replaceInString(artist->getName(), "/", "_"); +} + +static +std::string +getReleaseArtistPathName(Database::Release::pointer release) +{ + std::string releaseArtistName; + + std::vector> artists; + + artists = release->getReleaseArtists(); + if (artists.empty()) + artists = release->getArtists(); + + if (artists.size() > 1) + releaseArtistName = Wt::WString::tr("Lms.Explore.various-artists").toUTF8(); + else if (artists.size() == 1) + releaseArtistName = artists.front()->getName(); + + releaseArtistName = StringUtils::replaceInString(releaseArtistName, "/", "_"); + + return releaseArtistName; +} + +static +std::string +getReleasePathName(Database::Release::pointer release) +{ + std::string releaseName; + + if (auto releaseYear {release->getReleaseYear()}) + releaseName += std::to_string(*releaseYear) + " - "; + releaseName += StringUtils::replaceInString(release->getName(), "/", "_"); + + return releaseName; +} + +static +std::string +getTrackPathName(Database::Track::pointer track) +{ + std::string fileName; + + auto trackNumber {track->getTrackNumber()}; + auto discNumber {track->getDiscNumber()}; + + if (discNumber) + fileName += std::to_string(*discNumber) + "."; + if (trackNumber) + fileName += std::to_string(*trackNumber) + " - "; + + fileName += track->getName() + track->getPath().filename().extension().string(); + fileName = StringUtils::replaceInString(fileName , "/", "_"); + + return fileName; +} + +static +std::unique_ptr +createZipper(const std::vector& tracks) +{ + std::map files; + + for (const Database::Track::pointer& track : tracks) + { + std::string releaseName; + std::string releaseArtistName; + if (auto release {track->getRelease()}) + { + releaseName = getReleasePathName(release); + releaseArtistName = getReleaseArtistPathName(release); + } + + std::string fileName; + if (!releaseArtistName.empty()) + fileName += releaseArtistName + "/"; + if (!releaseName.empty()) + fileName += releaseName + "/"; + fileName += getTrackPathName(track); + + files.emplace(fileName, track->getPath()); + } + + return std::make_unique(files, Zip::Zipper::CompressionMethod::NoCompression); +} + +DownloadReleaseResource::DownloadReleaseResource(Database::IdType releaseId) +: _releaseId {releaseId} +{ + auto transaction {LmsApp->getDbSession().createSharedTransaction()}; + + Database::Release::pointer release {Database::Release::getById(LmsApp->getDbSession(), releaseId)}; + if (release) + suggestFileName(getReleasePathName(release) + ".zip"); +} + + +std::unique_ptr +DownloadReleaseResource::createZipper() +{ + Wt::WApplication::UpdateLock lock {LmsApp}; // DbSession are not thread safe + auto transaction {LmsApp->getDbSession().createSharedTransaction()}; + + const Database::Release::pointer release {Database::Release::getById(LmsApp->getDbSession(), _releaseId)}; + if (!release) + { + LOG(DEBUG) << "Cannot find release"; + return {}; + } + + return UserInterface::createZipper(release->getTracks()); +} + +DownloadArtistResource::DownloadArtistResource(Database::IdType artistId) +: _artistId {artistId} +{ + auto transaction {LmsApp->getDbSession().createSharedTransaction()}; + + Database::Artist::pointer artist {Database::Artist::getById(LmsApp->getDbSession(), artistId)}; + if (artist) + suggestFileName(getArtistPathName(artist) + ".zip"); +} + +std::unique_ptr +DownloadArtistResource::createZipper() +{ + Wt::WApplication::UpdateLock lock {LmsApp}; // DbSession are not thread safe + auto transaction {LmsApp->getDbSession().createSharedTransaction()}; + + const Database::Artist::pointer artist {Database::Artist::getById(LmsApp->getDbSession(), _artistId)}; + if (!artist) + { + LOG(DEBUG) << "Cannot find artist"; + return {}; + } + + return UserInterface::createZipper(artist->getTracks()); +} + +} // namespace UserInterface diff --git a/src/lms/ui/resource/DownloadResource.hpp b/src/lms/ui/resource/DownloadResource.hpp new file mode 100644 index 00000000..8b29ff8a --- /dev/null +++ b/src/lms/ui/resource/DownloadResource.hpp @@ -0,0 +1,64 @@ +/* + * 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 +#include + +#include "database/Types.hpp" +#include "utils/Zipper.hpp" + +namespace UserInterface { + +class DownloadResource : public Wt::WResource +{ + public: + static constexpr std::size_t bufferSize {32768}; + + ~DownloadResource(); + + private: + + void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override; + virtual std::unique_ptr createZipper() = 0; +}; + +class DownloadReleaseResource : public DownloadResource +{ + public: + DownloadReleaseResource(Database::IdType releaseId); + + private: + std::unique_ptr createZipper() override; + Database::IdType _releaseId; +}; + +class DownloadArtistResource : public DownloadResource +{ + public: + DownloadArtistResource(Database::IdType artistId); + + private: + std::unique_ptr createZipper() override; + Database::IdType _artistId; +}; + +} // namespace UserInterface + diff --git a/src/lms/ui/resource/ImageResource.hpp b/src/lms/ui/resource/ImageResource.hpp index 00865805..dc8756d2 100644 --- a/src/lms/ui/resource/ImageResource.hpp +++ b/src/lms/ui/resource/ImageResource.hpp @@ -47,7 +47,8 @@ class ImageResource : public Wt::WResource static std::string getMimeType(); - void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response); + private: + void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override; }; diff --git a/src/tools/CMakeLists.txt b/src/tools/CMakeLists.txt index e5877a5b..5c970f2a 100644 --- a/src/tools/CMakeLists.txt +++ b/src/tools/CMakeLists.txt @@ -1,5 +1,6 @@ add_subdirectory(metadata) add_subdirectory(recommendation) +add_subdirectory(zipper) diff --git a/src/tools/zipper/CMakeLists.txt b/src/tools/zipper/CMakeLists.txt new file mode 100644 index 00000000..8a588796 --- /dev/null +++ b/src/tools/zipper/CMakeLists.txt @@ -0,0 +1,9 @@ + +add_executable(lms-zipper + LmsZipper.cpp + ) + +target_link_libraries(lms-zipper PRIVATE + lmsutils + ) + diff --git a/src/tools/zipper/LmsZipper.cpp b/src/tools/zipper/LmsZipper.cpp new file mode 100644 index 00000000..8cdc92a4 --- /dev/null +++ b/src/tools/zipper/LmsZipper.cpp @@ -0,0 +1,71 @@ +/* + * 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 +#include +#include + +#include "utils/Service.hpp" +#include "utils/StreamLogger.hpp" +#include "utils/Zipper.hpp" + +int main(int argc, char* argv[]) +{ + // log to stdout + Service logger {std::make_unique(std::cout)}; + + if (argc < 2) + { + std::cerr << "Usage: [...]" << std::endl; + return EXIT_FAILURE; + } + + std::filesystem::path zipPath {argv[1]}; + + std::map files; + for (int i {2}; i < argc; ++i) + files.emplace(argv[i], argv[i]); + + + std::cout << "Compressing " << files.size() << " files..." << std::endl; + + using namespace Zip; + + std::ofstream ofs {zipPath.string().c_str(), std::ios_base::binary}; + if (!ofs) + { + std::cerr << "Cannot open file '" << zipPath.string() << "' for writing"; + return EXIT_FAILURE; + } + + Zipper zipper {files, Zipper::CompressionMethod::NoCompression}; + + while (!zipper.isComplete()) + { + std::array buffer; + + std::cout << "Call" << std::endl; + std::size_t nbWrittenBytes {zipper.writeSome(buffer.data(), buffer.size())}; + std::cout << "nbWrittenBytes = " << nbWrittenBytes << std::endl; + + ofs.write(reinterpret_cast(buffer.data()), nbWrittenBytes); + } + + return EXIT_SUCCESS; +} From d652a6791f237ad6a79ceae82c2e2f0ad7e90c79 Mon Sep 17 00:00:00 2001 From: emeric Date: Thu, 27 Aug 2020 13:44:16 +0200 Subject: [PATCH 05/24] Zipper: fixed UTF-8 encoded names --- src/libs/utils/impl/Zipper.cpp | 7 ++++--- src/tools/zipper/LmsZipper.cpp | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/libs/utils/impl/Zipper.cpp b/src/libs/utils/impl/Zipper.cpp index f81fa33d..7ea8b9c9 100644 --- a/src/libs/utils/impl/Zipper.cpp +++ b/src/libs/utils/impl/Zipper.cpp @@ -40,7 +40,8 @@ namespace Zip enum GeneralPurposeFlag : std::uint16_t { - UseDataDescriptor = 1 << 3, + UseDataDescriptor = 1 << 3, + LanguageEncoding = 1 << 11, }; enum CompressionMethod : std::uint16_t @@ -315,7 +316,7 @@ namespace Zip header.setSignature(); header.setVersionNeededToExtract(1, 0); - header.setGeneralPurposeFlags(0); + header.setGeneralPurposeFlags(ZipHeader::GeneralPurposeFlag::LanguageEncoding); header.setCrc32UncompressedData(_currentFile->second.fileCrc32); switch (_compMethod) { @@ -422,7 +423,7 @@ namespace Zip header.setSignature(); header.setVersionMadeBy(2, 0); header.setVersionNeededToExtract(1, 0); - header.setGeneralPurposeFlags(0); + header.setGeneralPurposeFlags(ZipHeader::GeneralPurposeFlag::LanguageEncoding); switch (_compMethod) { case CompressionMethod::NoCompression: diff --git a/src/tools/zipper/LmsZipper.cpp b/src/tools/zipper/LmsZipper.cpp index 8cdc92a4..5354fdb9 100644 --- a/src/tools/zipper/LmsZipper.cpp +++ b/src/tools/zipper/LmsZipper.cpp @@ -58,7 +58,7 @@ int main(int argc, char* argv[]) while (!zipper.isComplete()) { - std::array buffer; + std::array buffer; std::cout << "Call" << std::endl; std::size_t nbWrittenBytes {zipper.writeSome(buffer.data(), buffer.size())}; From e96727a94d6f8e99a9fc17fff3518094403b4261 Mon Sep 17 00:00:00 2001 From: emeric Date: Fri, 28 Aug 2020 00:00:08 +0200 Subject: [PATCH 06/24] Removed not that beautiful progressbar animations. fixes #90 --- approot/mediaplayer.xml | 2 +- docroot/css/lms.css | 4 ++++ docroot/js/mediaplayer.js | 2 -- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/approot/mediaplayer.xml b/approot/mediaplayer.xml index 43f0ff45..452dfd32 100644 --- a/approot/mediaplayer.xml +++ b/approot/mediaplayer.xml @@ -9,7 +9,7 @@
-
+
diff --git a/docroot/css/lms.css b/docroot/css/lms.css index b47302bb..17af16e4 100644 --- a/docroot/css/lms.css +++ b/docroot/css/lms.css @@ -362,6 +362,10 @@ a.Lms-artistname:hover, a.Lms-artistname:focus { background-color: var(--slider-background-color); } +.Lms-player-progress .progress-bar { + transition: none; +} + .Lms-player-seek { cursor: pointer; z-index: 1; diff --git a/docroot/js/mediaplayer.js b/docroot/js/mediaplayer.js index 776b5b0a..b50c4ff4 100644 --- a/docroot/js/mediaplayer.js +++ b/docroot/js/mediaplayer.js @@ -31,12 +31,10 @@ LMS.mediaplayer = function () { if (_elems.audio.paused) { _elems.playpause.classList.remove("fa-pause"); _elems.playpause.classList.add("fa-play"); - _elems.progress.classList.remove("active"); } else { _elems.playpause.classList.remove("fa-play"); _elems.playpause.classList.add("fa-pause"); - _elems.progress.classList.add("active"); } } From c9aed37fc297a429eae53dd4396f05353a295651 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 30 Aug 2020 15:13:46 +0200 Subject: [PATCH 07/24] Zipper: now computeing crc32 on the fly during zip creation --- src/libs/utils/impl/Path.cpp | 10 +- src/libs/utils/impl/Zipper.cpp | 190 ++++++------------ .../utils/include/utils/Crc32Calculator.hpp | 47 +++++ src/libs/utils/include/utils/Zipper.hpp | 15 +- src/lms/ui/resource/DownloadResource.cpp | 23 ++- src/tools/zipper/LmsZipper.cpp | 2 +- 6 files changed, 137 insertions(+), 150 deletions(-) create mode 100644 src/libs/utils/include/utils/Crc32Calculator.hpp diff --git a/src/libs/utils/impl/Path.cpp b/src/libs/utils/impl/Path.cpp index 886ad9aa..1d0b58ae 100644 --- a/src/libs/utils/impl/Path.cpp +++ b/src/libs/utils/impl/Path.cpp @@ -26,17 +26,16 @@ #include #include -#include // for boost::crc_32_type #include +#include "utils/Crc32Calculator.hpp" #include "utils/Exception.hpp" #include "utils/Logger.hpp" std::uint32_t computeCrc32(const std::filesystem::path& p) { - using crc_type = boost::crc_32_type; - crc_type result; + Utils::Crc32Calculator crc32; std::ifstream ifs {p.string().c_str(), std::ios_base::binary}; if (ifs) @@ -46,7 +45,7 @@ computeCrc32(const std::filesystem::path& p) std::array buffer; ifs.read( buffer.data(), buffer.size() ); - result.process_bytes( buffer.data(), ifs.gcount() ); + crc32.processBytes( reinterpret_cast(buffer.data()), ifs.gcount() ); } while (ifs); } @@ -56,8 +55,7 @@ computeCrc32(const std::filesystem::path& p) throw LmsException("Failed to open file '" + p.string() + "'" ); } - - return result.checksum(); + return crc32.getResult(); } bool diff --git a/src/libs/utils/impl/Zipper.cpp b/src/libs/utils/impl/Zipper.cpp index 7ea8b9c9..95c64432 100644 --- a/src/libs/utils/impl/Zipper.cpp +++ b/src/libs/utils/impl/Zipper.cpp @@ -23,7 +23,6 @@ #include #include "utils/Path.hpp" -#include "utils/Logger.hpp" // Done using specs from https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT @@ -49,6 +48,8 @@ namespace Zip NoCompression = 0, }; + static constexpr std::size_t UnknownCrc32 {0}; + static constexpr std::size_t UnknownFileSize {0}; protected: @@ -89,82 +90,34 @@ namespace Zip using ZipHeader::ZipHeader; // Setters - void setSignature(); - void setVersionNeededToExtract(unsigned major, unsigned minor); - void setGeneralPurposeFlags(std::uint16_t flags); - void setCompressionMethod(CompressionMethod compressionMethod); - void setLastModifiedDateTime(); - void setCrc32UncompressedData(std::uint32_t crc); - void setCompressedSize(std::size_t size); - void setUncompressedSize(std::size_t size); - void setFileNameLength(std::size_t size); - void setExtraFieldLength(std::size_t size); + void setSignature() { write32(0, 0x04034b50); } + void setVersionNeededToExtract(unsigned major, unsigned minor) { assert(minor < 10); write16(4, major*10 + minor); } + void setGeneralPurposeFlags(std::uint16_t flags) { write16(6, flags); } + void setCompressionMethod(CompressionMethod compressionMethod) { write16(8, compressionMethod); } + void setLastModifiedDateTime() + { // TODO + write16(10, 0); // time + write16(12, 0); // date + } + void setCrc32UncompressedData(std::uint32_t crc) { write32(14, crc); } + void setCompressedSize(std::size_t size) { write32(18, size); } + void setUncompressedSize(std::size_t size) { write32(22, size); } + void setFileNameLength(std::size_t size) { write16(26, size); } + void setExtraFieldLength(std::size_t size) { write16(28, size); } static constexpr std::size_t getHeaderSize() { return 30; } }; - void - LocalFileHeader::setSignature() + class DataDescriptor : public ZipHeader { - write32(0, 0x04034b50); - } - - void - LocalFileHeader::setVersionNeededToExtract(unsigned major, unsigned minor) - { - assert(minor < 10); - write16(4, major*10 + minor); - } - - void - LocalFileHeader::setGeneralPurposeFlags(std::uint16_t flags) - { - write16(6, flags); - } - - void - LocalFileHeader::setCompressionMethod(CompressionMethod compressionMethod) - { - write16(8, compressionMethod); - } - - void - LocalFileHeader::setLastModifiedDateTime() - { - // TODO - write16(10, 0); // time - write16(12, 0); // date - } - - void - LocalFileHeader::setCrc32UncompressedData(std::uint32_t crc) - { - write32(14, crc); - } - - void - LocalFileHeader::setCompressedSize(std::size_t size) - { - write32(18, size); - } - - void - LocalFileHeader::setUncompressedSize(std::size_t size) - { - write32(22, size); - } - - void - LocalFileHeader::setFileNameLength(std::size_t size) - { - write16(26, size); - } - - void - LocalFileHeader::setExtraFieldLength(std::size_t size) - { - write16(28, size); - } + public: + using ZipHeader::ZipHeader; + void setSignature() { write32(0, 0x08074b50 ); } + void setCrc32UncompressedData(std::uint32_t crc32) { write32(4, crc32); } + void setCompressedSize(std::size_t size) { write32(8, size); } + void setUncompressedSize(std::size_t size) { write32(12, size); } + static constexpr std::size_t getHeaderSize() { return 16; } + }; class CentralDirectoryHeader : public ZipHeader { @@ -210,8 +163,7 @@ namespace Zip static constexpr std::size_t getHeaderSize() { return 22; } }; - Zipper::Zipper(const std::map& files, CompressionMethod compMethod) - : _compMethod {compMethod} + Zipper::Zipper(const std::map& files) { for (const auto& [filename, filePath] : files) { @@ -221,13 +173,7 @@ namespace Zip std::error_code ec; fileContext.fileSize = std::filesystem::file_size(filePath, ec); if (ec) - { - LMS_LOG(UTILS, INFO) << "Cannot get file size for '" << filePath.string() << "': " << ec.message(); - continue; - } - fileContext.fileCrc32 = computeCrc32(filePath); - - LMS_LOG(UTILS, DEBUG) << "Processing '" << filePath.string() << "': File size = " << fileContext.fileSize; + throw ZipperException {"Cannot get file size for '" + filePath.string() + "': " + ec.message()}; _files[filename] = std::move(fileContext); } @@ -247,9 +193,6 @@ namespace Zip { std::size_t nbWrittenBytes {}; - LMS_LOG(UTILS, DEBUG) << "Global offset = " << _currentZipOffset; - LMS_LOG(UTILS, DEBUG) << "Buffer ptr = " << buffer << ", remaining size = " << bufferSize; - switch (_writeState) { case WriteState::LocalFileHeader: @@ -264,6 +207,10 @@ namespace Zip nbWrittenBytes = writeFileData(buffer, bufferSize); break; + case WriteState::DataDescriptor: + nbWrittenBytes = writeDataDescriptor(buffer, bufferSize); + break; + case WriteState::CentralDirectoryHeader: nbWrittenBytes = writeCentralDirectoryHeader(buffer, bufferSize); break; @@ -280,8 +227,6 @@ namespace Zip break; } - LMS_LOG(UI, DEBUG) << "nbWrittenBytes = " << nbWrittenBytes; - buffer += nbWrittenBytes; bufferSize -= nbWrittenBytes; _currentZipOffset += nbWrittenBytes; @@ -311,21 +256,15 @@ namespace Zip return 0; } - LMS_LOG(UTILS, INFO) << "writeLocalFileHeader. crc = " << _currentFile->second.fileCrc32; LocalFileHeader header {buffer, bufferSize}; header.setSignature(); header.setVersionNeededToExtract(1, 0); - header.setGeneralPurposeFlags(ZipHeader::GeneralPurposeFlag::LanguageEncoding); - header.setCrc32UncompressedData(_currentFile->second.fileCrc32); - switch (_compMethod) - { - case CompressionMethod::NoCompression: - header.setCompressionMethod(ZipHeader::CompressionMethod::NoCompression); - header.setCompressedSize(_currentFile->second.fileSize); - header.setUncompressedSize(_currentFile->second.fileSize); - break; - } + header.setGeneralPurposeFlags(ZipHeader::GeneralPurposeFlag::LanguageEncoding | ZipHeader::GeneralPurposeFlag::UseDataDescriptor); + header.setCompressionMethod(ZipHeader::CompressionMethod::NoCompression); + header.setCrc32UncompressedData(ZipHeader::UnknownCrc32); + header.setCompressedSize(ZipHeader::UnknownFileSize); + header.setUncompressedSize(ZipHeader::UnknownFileSize); header.setLastModifiedDateTime(); // getLastWriteTime(*_currentFile)); header.setFileNameLength(_currentFile->first.size()); header.setExtraFieldLength(0); @@ -339,6 +278,8 @@ namespace Zip std::size_t Zipper::writeLocalFileHeaderFileName(std::byte* buffer, std::size_t bufferSize) { + assert(_currentFile != std::end(_files)); + const std::string& fileName {_currentFile->first}; assert(_currentOffset <= fileName.size()); @@ -349,10 +290,7 @@ namespace Zip return 0; } - LMS_LOG(UTILS, INFO) << "writeLocalFileHeaderFileName"; - const std::size_t nbBytesToCopy {std::min(fileName.size() - _currentOffset, bufferSize)}; - LMS_LOG(UTILS, INFO) << "\tnbBytesToCopy = " << nbBytesToCopy; std::copy(std::next(std::begin(fileName), _currentOffset), std::next(std::begin(fileName), nbBytesToCopy), reinterpret_cast(buffer)); @@ -363,12 +301,12 @@ namespace Zip std::size_t Zipper::writeFileData(std::byte* buffer, std::size_t bufferSize) { + assert(_currentFile != std::end(_files)); + if (_currentOffset == _currentFile->second.fileSize) { _currentOffset = 0; - ++_currentFile; - _writeState = WriteState::LocalFileHeader; - + _writeState = WriteState::DataDescriptor; return 0; } @@ -391,13 +329,32 @@ namespace Zip ifs.read(reinterpret_cast(buffer), nbBytesToRead ); const ::uint64_t actualReadSize {static_cast<::uint64_t>(ifs.gcount())}; - LMS_LOG(UTILS, INFO) << "writeFileData: to read = " << nbBytesToRead << ", actually read = " << actualReadSize; - + _currentFile->second.fileCrc32.processBytes(buffer, actualReadSize); _currentOffset += actualReadSize; return actualReadSize; } + std::size_t + Zipper::writeDataDescriptor(std::byte* buffer, std::size_t bufferSize) + { + assert(bufferSize >= minOutputBufferSize); + static_assert(DataDescriptor::getHeaderSize() <= minOutputBufferSize); + + assert(_currentFile != std::end(_files)); + + DataDescriptor desc {buffer, bufferSize}; + desc.setSignature(); + desc.setCrc32UncompressedData(_currentFile->second.fileCrc32.getResult()); + desc.setCompressedSize(_currentFile->second.fileSize); + desc.setUncompressedSize(_currentFile->second.fileSize); + + ++_currentFile; + _writeState = WriteState::LocalFileHeader; + + return desc.getHeaderSize(); + } + std::size_t Zipper::writeCentralDirectoryHeader(std::byte* buffer, std::size_t bufferSize) { @@ -405,10 +362,7 @@ namespace Zip static_assert(CentralDirectoryHeader::getHeaderSize() <= minOutputBufferSize); if (_currentFile == std::begin(_files)) - { - LMS_LOG(UI, INFO) << "First record! _currentZipOffset = " << _currentZipOffset; _centralDirectoryOffset = _currentZipOffset; - } if (_currentFile == std::end(_files)) { @@ -417,23 +371,16 @@ namespace Zip return 0; } - LMS_LOG(UTILS, INFO) << "writeCentralDirectoryHeader. Relative offset = " << _currentFile->second.localFileHeaderOffset << ", crc = " << std::hex << _currentFile->second.fileCrc32; - CentralDirectoryHeader header {buffer, bufferSize}; header.setSignature(); header.setVersionMadeBy(2, 0); header.setVersionNeededToExtract(1, 0); - header.setGeneralPurposeFlags(ZipHeader::GeneralPurposeFlag::LanguageEncoding); - switch (_compMethod) - { - case CompressionMethod::NoCompression: - header.setCompressionMethod(ZipHeader::CompressionMethod::NoCompression); - header.setCompressedSize(_currentFile->second.fileSize); - header.setUncompressedSize(_currentFile->second.fileSize); - break; - } + header.setGeneralPurposeFlags(ZipHeader::GeneralPurposeFlag::LanguageEncoding | ZipHeader::GeneralPurposeFlag::UseDataDescriptor); + header.setCompressionMethod(ZipHeader::CompressionMethod::NoCompression); + header.setCompressedSize(_currentFile->second.fileSize); + header.setUncompressedSize(_currentFile->second.fileSize); header.setLastModifiedDateTime(); // getLastWriteTime(*_currentFile)); - header.setCrc32UncompressedData(_currentFile->second.fileCrc32); + header.setCrc32UncompressedData(_currentFile->second.fileCrc32.getResult()); header.setFileNameLength(_currentFile->first.size()); header.setExtraFieldLength(0); header.setFileCommentLength(0); @@ -463,7 +410,6 @@ namespace Zip return 0; } - LMS_LOG(UTILS, INFO) << "writeCentralDirectoryHeaderFileName"; const std::size_t nbBytesToCopy {std::min(fileName.size() - _currentOffset, bufferSize)}; std::copy(std::next(std::begin(fileName), _currentOffset), std::next(std::begin(fileName), nbBytesToCopy), reinterpret_cast(buffer)); @@ -482,8 +428,6 @@ namespace Zip EndOfCentralDirectoryRecord record {buffer, bufferSize}; - LMS_LOG(UTILS, DEBUG) << "Writing EOR. nb records = " << _files.size() << ", offset = " << _centralDirectoryOffset << ", size = " << _centralDirectorySize; - record.setSignature(); record.setDiskNumber(0); record.setCentralDirectoryDiskNumber(0); diff --git a/src/libs/utils/include/utils/Crc32Calculator.hpp b/src/libs/utils/include/utils/Crc32Calculator.hpp new file mode 100644 index 00000000..1e258d65 --- /dev/null +++ b/src/libs/utils/include/utils/Crc32Calculator.hpp @@ -0,0 +1,47 @@ +/* + * 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 // for boost::crc_32_type + +namespace Utils +{ + + class Crc32Calculator + { + public: + + void processBytes(const std::byte* _data, std::size_t dataSize) + { + _result.process_bytes(_data, dataSize); + } + + std::uint32_t getResult() const + { + return _result.checksum(); + } + + private: + using Crc32Type = boost::crc_32_type; + Crc32Type _result; + }; + +} + diff --git a/src/libs/utils/include/utils/Zipper.hpp b/src/libs/utils/include/utils/Zipper.hpp index 4692f5c8..57281078 100644 --- a/src/libs/utils/include/utils/Zipper.hpp +++ b/src/libs/utils/include/utils/Zipper.hpp @@ -24,6 +24,7 @@ #include #include "Exception.hpp" +#include "utils/Crc32Calculator.hpp" namespace Zip { @@ -33,17 +34,12 @@ namespace Zip using LmsException::LmsException; }; - // Very simple on-the-fly zip creator + // Very simple on-the-fly zip creator, "store" method only class Zipper { public: - enum class CompressionMethod - { - NoCompression, - }; - - Zipper(const std::map& files, CompressionMethod comp = CompressionMethod::NoCompression); + Zipper(const std::map& files); static constexpr std::size_t minOutputBufferSize = 64; std::size_t writeSome(std::byte* buffer, std::size_t bufferSize); @@ -55,6 +51,7 @@ namespace Zip std::size_t writeLocalFileHeader(std::byte* buffer, std::size_t bufferSize); std::size_t writeLocalFileHeaderFileName(std::byte* buffer, std::size_t bufferSize); std::size_t writeFileData(std::byte* buffer, std::size_t bufferSize); + std::size_t writeDataDescriptor(std::byte* buffer, std::size_t bufferSize); std::size_t writeCentralDirectoryHeader(std::byte* buffer, std::size_t bufferSize); std::size_t writeCentralDirectoryHeaderFileName(std::byte* buffer, std::size_t bufferSize); std::size_t writeEndOfCentralDirectoryRecord(std::byte* buffer, std::size_t bufferSize); @@ -63,7 +60,7 @@ namespace Zip { std::filesystem::path filePath; std::size_t fileSize; - std::uint32_t fileCrc32; + Utils::Crc32Calculator fileCrc32; std::size_t localFileHeaderOffset {}; }; @@ -75,13 +72,13 @@ namespace Zip LocalFileHeader, LocalFileHeaderFileName, FileData, + DataDescriptor, CentralDirectoryHeader, CentralDirectoryHeaderFileName, EndOfCentralDirectoryRecord, Complete, }; - CompressionMethod _compMethod {CompressionMethod::NoCompression}; WriteState _writeState {WriteState::LocalFileHeader}; FileContainer::iterator _currentFile; std::size_t _currentOffset {}; diff --git a/src/lms/ui/resource/DownloadResource.cpp b/src/lms/ui/resource/DownloadResource.cpp index 1466a45e..67945004 100644 --- a/src/lms/ui/resource/DownloadResource.cpp +++ b/src/lms/ui/resource/DownloadResource.cpp @@ -19,6 +19,9 @@ #include "DownloadResource.hpp" +#include +#include + #include #include @@ -124,20 +127,18 @@ static std::string getTrackPathName(Database::Track::pointer track) { - std::string fileName; + std::ostringstream fileName; - auto trackNumber {track->getTrackNumber()}; - auto discNumber {track->getDiscNumber()}; + if (auto discNumber {track->getDiscNumber()}) + fileName << *discNumber << "."; + if (auto trackNumber {track->getTrackNumber()}) + fileName << std::setw(2) << std::setfill('0') << *trackNumber << " - "; - if (discNumber) - fileName += std::to_string(*discNumber) + "."; - if (trackNumber) - fileName += std::to_string(*trackNumber) + " - "; + fileName << StringUtils::replaceInString(track->getName(), "/", "_") << track->getPath().filename().extension().string(); - fileName += track->getName() + track->getPath().filename().extension().string(); - fileName = StringUtils::replaceInString(fileName , "/", "_"); + LOG(DEBUG) << "Forged filename = '" << fileName.str() << "'"; - return fileName; + return fileName.str(); } static @@ -166,7 +167,7 @@ createZipper(const std::vector& tracks) files.emplace(fileName, track->getPath()); } - return std::make_unique(files, Zip::Zipper::CompressionMethod::NoCompression); + return std::make_unique(files); } DownloadReleaseResource::DownloadReleaseResource(Database::IdType releaseId) diff --git a/src/tools/zipper/LmsZipper.cpp b/src/tools/zipper/LmsZipper.cpp index 5354fdb9..d70b2bbb 100644 --- a/src/tools/zipper/LmsZipper.cpp +++ b/src/tools/zipper/LmsZipper.cpp @@ -54,7 +54,7 @@ int main(int argc, char* argv[]) return EXIT_FAILURE; } - Zipper zipper {files, Zipper::CompressionMethod::NoCompression}; + Zipper zipper {files}; while (!zipper.isComplete()) { From 093c36f257a3beadd9c331f3d77050e5434a9c73 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 30 Aug 2020 15:29:03 +0200 Subject: [PATCH 08/24] Zipper: fixed bug that corrupted filenames --- src/libs/utils/impl/Zipper.cpp | 4 ++-- src/tools/zipper/LmsZipper.cpp | 11 ++++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/libs/utils/impl/Zipper.cpp b/src/libs/utils/impl/Zipper.cpp index 95c64432..8c0f901e 100644 --- a/src/libs/utils/impl/Zipper.cpp +++ b/src/libs/utils/impl/Zipper.cpp @@ -292,7 +292,7 @@ namespace Zip const std::size_t nbBytesToCopy {std::min(fileName.size() - _currentOffset, bufferSize)}; - std::copy(std::next(std::begin(fileName), _currentOffset), std::next(std::begin(fileName), nbBytesToCopy), reinterpret_cast(buffer)); + std::copy(std::next(std::begin(fileName), _currentOffset), std::next(std::begin(fileName), _currentOffset + nbBytesToCopy), reinterpret_cast(buffer)); _currentOffset += nbBytesToCopy; return nbBytesToCopy; @@ -412,7 +412,7 @@ namespace Zip const std::size_t nbBytesToCopy {std::min(fileName.size() - _currentOffset, bufferSize)}; - std::copy(std::next(std::begin(fileName), _currentOffset), std::next(std::begin(fileName), nbBytesToCopy), reinterpret_cast(buffer)); + std::copy(std::next(std::begin(fileName), _currentOffset), std::next(std::begin(fileName), _currentOffset + nbBytesToCopy), reinterpret_cast(buffer)); _currentOffset += nbBytesToCopy; _centralDirectorySize += nbBytesToCopy; diff --git a/src/tools/zipper/LmsZipper.cpp b/src/tools/zipper/LmsZipper.cpp index d70b2bbb..bbeb8403 100644 --- a/src/tools/zipper/LmsZipper.cpp +++ b/src/tools/zipper/LmsZipper.cpp @@ -40,7 +40,10 @@ int main(int argc, char* argv[]) std::map files; for (int i {2}; i < argc; ++i) - files.emplace(argv[i], argv[i]); + { + std::filesystem::path path {argv[i]}; + files.emplace(path.relative_path(), path); + } std::cout << "Compressing " << files.size() << " files..." << std::endl; @@ -58,12 +61,10 @@ int main(int argc, char* argv[]) while (!zipper.isComplete()) { - std::array buffer; + //std::array buffer; + std::array buffer; - std::cout << "Call" << std::endl; std::size_t nbWrittenBytes {zipper.writeSome(buffer.data(), buffer.size())}; - std::cout << "nbWrittenBytes = " << nbWrittenBytes << std::endl; - ofs.write(reinterpret_cast(buffer.data()), nbWrittenBytes); } From f44f7d7ca5465d2bf98cfe7c2fd234660aaa60fd Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 30 Aug 2020 15:58:06 +0200 Subject: [PATCH 09/24] Zipper: added an overflow check to make sure to produce a valid zip (next step is to implement zip64) --- src/libs/utils/impl/Zipper.cpp | 14 ++++++++++++++ src/libs/utils/include/utils/Zipper.hpp | 5 +++++ src/tools/zipper/LmsZipper.cpp | 24 +++++++++++++++++------- 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/src/libs/utils/impl/Zipper.cpp b/src/libs/utils/impl/Zipper.cpp index 8c0f901e..b189fd28 100644 --- a/src/libs/utils/impl/Zipper.cpp +++ b/src/libs/utils/impl/Zipper.cpp @@ -176,8 +176,22 @@ namespace Zip throw ZipperException {"Cannot get file size for '" + filePath.string() + "': " + ec.message()}; _files[filename] = std::move(fileContext); + + _totalZipSize += LocalFileHeader::getHeaderSize(); + _totalZipSize += filename.size(); + if (fileContext.fileSize > 0) + { + _totalZipSize += fileContext.fileSize; + _totalZipSize += DataDescriptor::getHeaderSize(); + _totalZipSize += CentralDirectoryHeader::getHeaderSize(); + _totalZipSize += filename.size(); + } } + _totalZipSize += EndOfCentralDirectoryRecord::getHeaderSize(); + if (_totalZipSize > UINT32_MAX) + throw ZipperException {"Cannot create a zip file which is larger than " + std::to_string(UINT32_MAX) + " bytes!"}; + _currentFile = std::begin(_files); } diff --git a/src/libs/utils/include/utils/Zipper.hpp b/src/libs/utils/include/utils/Zipper.hpp index 57281078..d464abe2 100644 --- a/src/libs/utils/include/utils/Zipper.hpp +++ b/src/libs/utils/include/utils/Zipper.hpp @@ -39,12 +39,16 @@ namespace Zip { public: + using SizeZype = std::uint64_t; + Zipper(const std::map& files); static constexpr std::size_t minOutputBufferSize = 64; std::size_t writeSome(std::byte* buffer, std::size_t bufferSize); bool isComplete() const; + SizeZype getTotalZipFile() const { return _totalZipSize; } + private: void setComplete(); @@ -79,6 +83,7 @@ namespace Zip Complete, }; + SizeZype _totalZipSize {}; WriteState _writeState {WriteState::LocalFileHeader}; FileContainer::iterator _currentFile; std::size_t _currentOffset {}; diff --git a/src/tools/zipper/LmsZipper.cpp b/src/tools/zipper/LmsZipper.cpp index bbeb8403..7ec889e1 100644 --- a/src/tools/zipper/LmsZipper.cpp +++ b/src/tools/zipper/LmsZipper.cpp @@ -57,15 +57,25 @@ int main(int argc, char* argv[]) return EXIT_FAILURE; } - Zipper zipper {files}; - - while (!zipper.isComplete()) + try { - //std::array buffer; - std::array buffer; + Zipper zipper {files}; - std::size_t nbWrittenBytes {zipper.writeSome(buffer.data(), buffer.size())}; - ofs.write(reinterpret_cast(buffer.data()), nbWrittenBytes); + while (!zipper.isComplete()) + { + //std::array buffer; + std::array buffer; + + std::size_t nbWrittenBytes {zipper.writeSome(buffer.data(), buffer.size())}; + ofs.write(reinterpret_cast(buffer.data()), nbWrittenBytes); + } + + std::cout << "Total zip size = " << zipper.getTotalZipFile() << std::endl; + } + catch (const ZipperException& e) + { + std::cerr << "Caught Zipper exception: " << e.what() << std::endl; + return EXIT_FAILURE; } return EXIT_SUCCESS; From a55e69be29920d9989bbcc81d7a7195a7f8fb79a Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 30 Aug 2020 18:40:10 +0200 Subject: [PATCH 10/24] Removed useless log --- src/lms/ui/resource/DownloadResource.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/lms/ui/resource/DownloadResource.cpp b/src/lms/ui/resource/DownloadResource.cpp index 67945004..3f01d9d3 100644 --- a/src/lms/ui/resource/DownloadResource.cpp +++ b/src/lms/ui/resource/DownloadResource.cpp @@ -136,8 +136,6 @@ getTrackPathName(Database::Track::pointer track) fileName << StringUtils::replaceInString(track->getName(), "/", "_") << track->getPath().filename().extension().string(); - LOG(DEBUG) << "Forged filename = '" << fileName.str() << "'"; - return fileName.str(); } From ea317b62edfc8bdb3f12949faa5d5e60511528c5 Mon Sep 17 00:00:00 2001 From: emeric Date: Sun, 30 Aug 2020 18:40:52 +0200 Subject: [PATCH 11/24] Upgraded travis CI os --- .travis.yml | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/.travis.yml b/.travis.yml index ebaa7985..0635f43f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,25 +1,12 @@ language: cpp sudo: required -dist: bionic -matrix: - include: - - os: linux - addons: - apt: - sources: - - ubuntu-toolchain-r-test - packages: - - g++-9 - env: - - MATRIX_EVAL="CC=gcc-9 && CXX=g++-9" - compiler: - - gcc +dist: focal before_install: - eval "${MATRIX_EVAL}" - sudo apt-get install build-essential cmake libboost-all-dev libconfig++-dev libavcodec-dev libavutil-dev libavformat-dev ffmpeg libgraphicsmagick++1-dev libpstreams-dev libconfig++-dev libpstreams-dev libtag1-dev libpam0g-dev - git clone https://github.com/emweb/wt.git wt - pushd wt; -- git checkout 4.2.1 +- git checkout 4.4.0 - cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr && sudo make install - popd script: cmake -DCMAKE_BUILD_TYPE=Release . && make && make test From 8a84a078be409793286f6e1609a71ec31b9a3554 Mon Sep 17 00:00:00 2001 From: emeric Date: Mon, 31 Aug 2020 14:15:10 +0200 Subject: [PATCH 12/24] Fixed regression on recommendation engine --- CMakeLists.txt | 2 +- src/libs/database/impl/Db.cpp | 7 ++++ src/libs/database/include/database/Db.hpp | 6 +++ src/libs/recommendation/impl/Engine.cpp | 17 ++++++-- src/libs/recommendation/impl/Engine.hpp | 11 +++++- .../include/recommendation/IEngine.hpp | 3 +- src/libs/scanner/impl/MediaScanner.cpp | 2 + src/libs/scanner/impl/MediaScanner.hpp | 5 ++- .../scanner/include/scanner/IMediaScanner.hpp | 4 -- src/lms/main.cpp | 1 + src/tools/recommendation/CMakeLists.txt | 1 + .../recommendation/LmsRecommendation.cpp | 39 ++++++++++++++----- 12 files changed, 74 insertions(+), 24 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8d84e7d3..6099f285 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,7 +12,7 @@ find_package(PkgConfig REQUIRED) find_package(Filesystem REQUIRED) find_package(FFMPEGAV REQUIRED) find_package(Taglib REQUIRED) -find_package(Boost REQUIRED COMPONENTS system) +find_package(Boost REQUIRED COMPONENTS system program_options) find_package(PStreams REQUIRED) find_package(PAM) pkg_check_modules(GRAPHICSMAGICKXX REQUIRED GraphicsMagick++) diff --git a/src/libs/database/impl/Db.cpp b/src/libs/database/impl/Db.cpp index 4d745877..d06eb38b 100644 --- a/src/libs/database/impl/Db.cpp +++ b/src/libs/database/impl/Db.cpp @@ -43,6 +43,13 @@ Db::Db(const std::filesystem::path& dbPath) _connectionPool = std::move(connectionPool); } +Db::~Db() +{ + LMS_LOG(DB, DEBUG) << "Optimizing db..."; + executeSql("pragma optimize"); + LMS_LOG(DB, DEBUG) << "Optimizing db DONE"; +} + void Db::executeSql(const std::string& sql) { diff --git a/src/libs/database/include/database/Db.hpp b/src/libs/database/include/database/Db.hpp index 58345405..565ffcd6 100644 --- a/src/libs/database/include/database/Db.hpp +++ b/src/libs/database/include/database/Db.hpp @@ -32,6 +32,12 @@ class Db public: Db(const std::filesystem::path& dbPath); + ~Db(); + + Db(const Db&) = delete; + Db(Db&&) = delete; + Db& operator=(const Db&) = delete; + Db& operator=(Db&&) = delete; private: friend class Session; diff --git a/src/libs/recommendation/impl/Engine.cpp b/src/libs/recommendation/impl/Engine.cpp index 072c3d48..26caf31b 100644 --- a/src/libs/recommendation/impl/Engine.cpp +++ b/src/libs/recommendation/impl/Engine.cpp @@ -38,6 +38,12 @@ createEngine(Database::Db& db) Engine::Engine(Database::Db& db) : _dbSession {db} { + start(); +} + +Engine::~Engine() +{ + stop(); } void @@ -45,9 +51,6 @@ Engine::start() { assert(!_running); _running = true; - - requestReloadInternal(false); - _ioService.start(); } @@ -62,6 +65,12 @@ Engine::stop() _ioService.stop(); } +void +Engine::requestLoad() +{ + requestReloadInternal(false); +} + void Engine::requestReload() { @@ -254,7 +263,7 @@ void Engine::cancelPendingClassifiers() { std::unique_lock lock {_classifiersMutex}; - + for (IClassifier* classifier : _pendingClassifiers) classifier->requestCancelInit(); } diff --git a/src/libs/recommendation/impl/Engine.hpp b/src/libs/recommendation/impl/Engine.hpp index 4fab63f1..fd2f91fb 100644 --- a/src/libs/recommendation/impl/Engine.hpp +++ b/src/libs/recommendation/impl/Engine.hpp @@ -35,12 +35,19 @@ namespace Recommendation { public: Engine(Database::Db& db); + ~Engine(); + + Engine(const Engine&) = delete; + Engine(Engine&&) = delete; + Engine& operator=(const Engine&) = delete; + Engine& operator=(Engine&&) = delete; private: - void start() override; - void stop() override; + void start(); + void stop(); + void requestLoad() override; void requestReload() override; Wt::Signal<>& reloaded() override { return _sigReloaded; } diff --git a/src/libs/recommendation/include/recommendation/IEngine.hpp b/src/libs/recommendation/include/recommendation/IEngine.hpp index 37f7cb8c..7cf196e8 100644 --- a/src/libs/recommendation/include/recommendation/IEngine.hpp +++ b/src/libs/recommendation/include/recommendation/IEngine.hpp @@ -40,8 +40,7 @@ namespace Recommendation public: virtual ~IEngine() = default; - virtual void start() = 0; - virtual void stop() = 0; + virtual void requestLoad() = 0; virtual void requestReload() = 0; virtual Wt::Signal<>& reloaded() = 0; diff --git a/src/libs/scanner/impl/MediaScanner.cpp b/src/libs/scanner/impl/MediaScanner.cpp index e463f518..7b7d60cb 100644 --- a/src/libs/scanner/impl/MediaScanner.cpp +++ b/src/libs/scanner/impl/MediaScanner.cpp @@ -254,6 +254,8 @@ MediaScanner::MediaScanner(Database::Db& db) _ioService.setThreadCount(1); refreshScanSettings(); + + start(); } MediaScanner::~MediaScanner() diff --git a/src/libs/scanner/impl/MediaScanner.hpp b/src/libs/scanner/impl/MediaScanner.hpp index 0ff66730..88cbc27c 100644 --- a/src/libs/scanner/impl/MediaScanner.hpp +++ b/src/libs/scanner/impl/MediaScanner.hpp @@ -50,8 +50,6 @@ class MediaScanner : public IMediaScanner MediaScanner& operator=(const MediaScanner&) = delete; MediaScanner& operator=(MediaScanner&&) = delete; - void start() override; - void stop() override; void requestReload() override; void requestImmediateScan(bool force) override; @@ -64,6 +62,9 @@ class MediaScanner : public IMediaScanner private: + void start(); + void stop(); + // Job handling void scheduleNextScan(); void scheduleScan(bool force, const Wt::WDateTime& dateTime = {}); diff --git a/src/libs/scanner/include/scanner/IMediaScanner.hpp b/src/libs/scanner/include/scanner/IMediaScanner.hpp index 83e675fb..5a0afbdc 100644 --- a/src/libs/scanner/include/scanner/IMediaScanner.hpp +++ b/src/libs/scanner/include/scanner/IMediaScanner.hpp @@ -38,14 +38,10 @@ class IMediaScanner public: virtual ~IMediaScanner() = default; - virtual void start() = 0; - virtual void stop() = 0; - // Async requests virtual void requestReload() = 0; virtual void requestImmediateScan(bool force) = 0; - enum class State { NotScheduled, diff --git a/src/lms/main.cpp b/src/lms/main.cpp index c70f5ca3..657cd868 100644 --- a/src/lms/main.cpp +++ b/src/lms/main.cpp @@ -171,6 +171,7 @@ int main(int argc, char* argv[]) Service coverArtService {CoverArt::createGrabber(argv[0])}; coverArtService->setDefaultCover(server.appRoot() + "/images/unknown-cover.jpg"); Service recommendationEngineService {Recommendation::createEngine(database)}; + recommendationEngineService->requestLoad(); Service mediaScannerService {Scanner::createMediaScanner(database)}; mediaScannerService->scanComplete().connect([&]() diff --git a/src/tools/recommendation/CMakeLists.txt b/src/tools/recommendation/CMakeLists.txt index 75b3e5d0..12f20ef0 100644 --- a/src/tools/recommendation/CMakeLists.txt +++ b/src/tools/recommendation/CMakeLists.txt @@ -6,6 +6,7 @@ add_executable(lms-recommendation target_link_libraries(lms-recommendation PRIVATE lmsdatabase lmsrecommendation + Boost::program_options ) install(TARGETS lms-recommendation DESTINATION bin) diff --git a/src/tools/recommendation/LmsRecommendation.cpp b/src/tools/recommendation/LmsRecommendation.cpp index ecb625f8..9991a596 100644 --- a/src/tools/recommendation/LmsRecommendation.cpp +++ b/src/tools/recommendation/LmsRecommendation.cpp @@ -22,6 +22,8 @@ #include #include +#include + #include "database/Artist.hpp" #include "database/Cluster.hpp" #include "database/Db.hpp" @@ -130,14 +132,30 @@ int main(int argc, char *argv[]) { try { + namespace po = boost::program_options; + // log to stdout Service logger {std::make_unique(std::cout)}; - std::filesystem::path configFilePath {"/etc/lms.conf"}; - if (argc >= 2) - configFilePath = std::string(argv[1], 0, 256); + po::options_description desc{"Allowed options"}; + desc.add_options() + ("help,h", "print usage message") + ("conf,c", po::value()->default_value("/etc/lms.conf"), "LMS config file") + ("artists,a", "Display recommendation for artists") + ("releases,r", "Display recommendation for releases") + ("tracks,t", "Display recommendation for tracks") + ; - Service config {createConfig(configFilePath)}; + po::variables_map vm; + po::store(po::parse_command_line(argc, argv, desc), vm); + + if (vm.count("help")) + { + std::cout << desc << std::endl; + return EXIT_SUCCESS; + } + + Service config {createConfig(vm["conf"].as())}; Database::Db db {config->getPath("working-dir") / "lms.db"}; Database::Session session {db}; @@ -153,17 +171,20 @@ int main(int argc, char *argv[]) sem.notify(); }); - engine->start(); + engine->requestLoad(); std::cout << "Waiting for the recommendation engine to be loaded..." << std::endl; sem.wait(); std::cout << "Recommendation engine loaded!" << std::endl; - dumpTracksRecommendation(db, *engine); - dumpReleasesRecommendation(db, *engine); - dumpArtistsRecommendation(db, *engine); + if (vm.count("tracks")) + dumpTracksRecommendation(db, *engine); - engine->stop(); + if (vm.count("releases")) + dumpReleasesRecommendation(db, *engine); + + if (vm.count("artists")) + dumpArtistsRecommendation(db, *engine); } catch( std::exception& e) { From b3f14153d2f7288519ab6b3fb4ab16bce673e387 Mon Sep 17 00:00:00 2001 From: emeric Date: Tue, 1 Sep 2020 13:42:17 +0200 Subject: [PATCH 13/24] Added some color transitions, ref #82 --- approot/artist.xml | 4 ++-- approot/artists.xml | 4 ++-- approot/explore.xml | 4 ++-- approot/mediaplayer.xml | 8 ++++---- approot/playqueue.xml | 16 ++++++++-------- approot/release.xml | 4 ++-- approot/releases.xml | 4 ++-- approot/templates.xml | 14 +++++++------- approot/tracks.xml | 4 ++-- docroot/css/lms-darkly.css | 4 ---- docroot/css/lms.css | 16 +++++++++++----- 11 files changed, 42 insertions(+), 40 deletions(-) diff --git a/approot/artist.xml b/approot/artist.xml index 3e7bca0a..301682b6 100644 --- a/approot/artist.xml +++ b/approot/artist.xml @@ -24,7 +24,7 @@

${tr:Lms.Explore.links}

- ${mbid-link class="text-muted"} + ${mbid-link class="Lms-btn text-muted"}
${} @@ -41,7 +41,7 @@
${name class="Lms-releasename text-primary"}
- ${}
${artist class="Lms-artistname text-muted"}
${
} + ${}
${artist class="Lms-btn Lms-artistname text-muted"}
${
}
${}${year}${}${} (${orig-year})${}
diff --git a/approot/artists.xml b/approot/artists.xml index 83d44747..70516b79 100644 --- a/approot/artists.xml +++ b/approot/artists.xml @@ -18,13 +18,13 @@
-

${name class="Lms-artistname text-primary"}

+

${name class="Lms-btn Lms-artistname text-primary"}

- ${name class="Lms-artistname text-primary"} + ${name class="Lms-btn Lms-artistname text-primary"}
diff --git a/approot/explore.xml b/approot/explore.xml index 80881197..a1a84291 100644 --- a/approot/explore.xml +++ b/approot/explore.xml @@ -5,8 +5,8 @@ ${contents class="Lms-explore-contents"} - - + + ${add-filter class="btn-sm btn-info"} ${clusters class="Lms-explore-clusters"} diff --git a/approot/mediaplayer.xml b/approot/mediaplayer.xml index 452dfd32..1d0c8990 100644 --- a/approot/mediaplayer.xml +++ b/approot/mediaplayer.xml @@ -16,11 +16,11 @@
- +
diff --git a/approot/playqueue.xml b/approot/playqueue.xml index 3fb28e46..2eeb3268 100644 --- a/approot/playqueue.xml +++ b/approot/playqueue.xml @@ -2,10 +2,10 @@ - - - - + + + +
@@ -21,8 +21,8 @@
- - + +
@@ -34,7 +34,7 @@
${name}
${} - ${}${release class="Lms-releasename text-primary"}${} + ${}${release class="Lms-btn Lms-releasename text-primary"}${} ${}${artists}${} ${}
@@ -50,7 +50,7 @@ - ${artist class="Lms-artistname text-muted"} + ${artist class="Lms-btn Lms-artistname text-muted"} diff --git a/approot/release.xml b/approot/release.xml index cb49ffd7..78f93245 100644 --- a/approot/release.xml +++ b/approot/release.xml @@ -14,7 +14,7 @@

${name}

- ${}

${artist class="Lms-artistname text-muted"}

${
} + ${}

${artist class="Lms-btn Lms-artistname text-muted"}

${
}
${}${year}${}${} (${orig-year})${}
${clusters} ${play-btn}${more-btn} @@ -45,7 +45,7 @@

${tr:Lms.Explore.links}

- ${mbid-link class="text-muted"} + ${mbid-link class="Lms-btn text-muted"}
${} diff --git a/approot/releases.xml b/approot/releases.xml index 430e48dd..3d9ec83c 100644 --- a/approot/releases.xml +++ b/approot/releases.xml @@ -21,8 +21,8 @@ ${cover}
-
${release-name class="Lms-releasename text-primary"}
-
${}${year}${}${}${artist-name class="Lms-artistname text-muted"}${}
+
${release-name class="Lms-btn Lms-releasename text-primary"}
+
${}${year}${}${}${artist-name class="Lms-btn Lms-artistname text-muted"}${}
diff --git a/approot/templates.xml b/approot/templates.xml index 2145b7f7..5be1e07d 100644 --- a/approot/templates.xml +++ b/approot/templates.xml @@ -11,17 +11,17 @@ - ${title class="navbar-brand"} + ${title class="navbar-brand Lms-btn"}