diff --git a/src/libs/database/impl/objects/Artwork.cpp b/src/libs/database/impl/objects/Artwork.cpp index 2dda27ee..34e4f61e 100644 --- a/src/libs/database/impl/objects/Artwork.cpp +++ b/src/libs/database/impl/objects/Artwork.cpp @@ -20,7 +20,6 @@ #include "database/objects/Artwork.hpp" #include -#include #include "database/Session.hpp" #include "database/objects/Image.hpp" @@ -92,18 +91,6 @@ namespace lms::db return res; } - Wt::WDateTime Artwork::getLastWrittenTime() const - { - auto query{ session()->query("SELECT MAX(COALESCE(image.file_last_write, track.file_last_write)) AS last_written_datetime FROM artwork") }; - query.leftJoin("image ON artwork.image_id = image.id"); - query.leftJoin("track_embedded_image ON artwork.track_embedded_image_id = track_embedded_image.id"); - query.leftJoin("track_embedded_image_link ON track_embedded_image.id = track_embedded_image_link.track_embedded_image_id"); - query.leftJoin("track ON track.id = track_embedded_image_link.track_id"); - query.where("artwork.id = ?").bind(getId()); - - return utils::fetchQuerySingleResult(query); - } - std::filesystem::path Artwork::getAbsoluteFilePath() const { auto query{ session()->query("SELECT COALESCE(image.absolute_file_path, track.absolute_file_path) AS absolute_file_path FROM artwork") }; diff --git a/src/libs/database/include/database/objects/Artwork.hpp b/src/libs/database/include/database/objects/Artwork.hpp index 284594f2..d05a1292 100644 --- a/src/libs/database/include/database/objects/Artwork.hpp +++ b/src/libs/database/include/database/objects/Artwork.hpp @@ -23,7 +23,6 @@ #include #include -#include #include "database/Object.hpp" #include "database/objects/ArtworkId.hpp" @@ -50,7 +49,6 @@ namespace lms::db // getters using UnderlyingId = std::variant; UnderlyingId getUnderlyingId() const; - Wt::WDateTime getLastWrittenTime() const; std::filesystem::path getAbsoluteFilePath() const; ObjectPtr getImage() const; ImageId getImageId() const; @@ -68,7 +66,6 @@ namespace lms::db Artwork(ObjectPtr image); static pointer create(Session& session, ObjectPtr trackEmbeddedImage); static pointer create(Session& session, ObjectPtr image); - Wt::Dbo::ptr _trackEmbeddedImage; Wt::Dbo::ptr _image; }; diff --git a/src/libs/database/test/Artwork.cpp b/src/libs/database/test/Artwork.cpp index 1a9306c5..25073524 100644 --- a/src/libs/database/test/Artwork.cpp +++ b/src/libs/database/test/Artwork.cpp @@ -36,24 +36,15 @@ namespace lms::db::tests } ScopedImage image{ session, "/MyImage" }; + { + auto transaction{ session.createWriteTransaction() }; + image.get().modify()->setAbsoluteFilePath("/tmp/foo"); + } ScopedArtwork artwork{ session, image.lockAndGet() }; - const Wt::WDateTime dateTime{ Wt::WDate{ 2025, 1, 1 } }; - { auto transaction{ session.createReadTransaction() }; EXPECT_EQ(Artwork::getCount(session), 1); - } - - { - auto transaction{ session.createWriteTransaction() }; - image.get().modify()->setLastWriteTime(dateTime); - image.get().modify()->setAbsoluteFilePath("/tmp/foo"); - } - - { - auto transaction{ session.createReadTransaction() }; - EXPECT_EQ(artwork.get()->getLastWrittenTime(), dateTime); EXPECT_EQ(artwork.get()->getAbsoluteFilePath(), "/tmp/foo"); } } @@ -67,20 +58,15 @@ namespace lms::db::tests ScopedTrackEmbeddedImage image{ session }; ScopedTrack track{ session }; - ScopedArtwork artwork{ session, image.lockAndGet() }; - - const Wt::WDateTime dateTime{ Wt::WDate{ 2025, 1, 1 } }; - { auto transaction{ session.createWriteTransaction() }; session.create(track.get(), image.get()); - track.get().modify()->setLastWriteTime(dateTime); track.get().modify()->setAbsoluteFilePath("/tmp/foo"); } + ScopedArtwork artwork{ session, image.lockAndGet() }; { auto transaction{ session.createReadTransaction() }; - EXPECT_EQ(artwork.get()->getLastWrittenTime(), dateTime); EXPECT_EQ(artwork.get()->getAbsoluteFilePath(), "/tmp/foo"); } } diff --git a/src/libs/services/scanner/impl/scanners/ImageFileScanner.cpp b/src/libs/services/scanner/impl/scanners/ImageFileScanner.cpp index 166a43fd..cc5f3061 100644 --- a/src/libs/services/scanner/impl/scanners/ImageFileScanner.cpp +++ b/src/libs/services/scanner/impl/scanners/ImageFileScanner.cpp @@ -85,11 +85,20 @@ namespace lms::scanner } const bool added{ !image }; - if (!image) + if (added) { image = dbSession.create(getFilePath()); dbSession.create(image); } + else if (image->getLastWriteTime() != getLastWriteTime() + || image->getFileSize() != getFileSize() + || image->getHeight() != _parsedImageProperties->height + || image->getWidth() != _parsedImageProperties->width) + { + if (auto artwork{ db::Artwork::find(dbSession, image->getId()) }) + artwork.remove(); + dbSession.create(image); + } image.modify()->setLastWriteTime(getLastWriteTime()); image.modify()->setFileSize(getFileSize()); diff --git a/src/libs/subsonic/CMakeLists.txt b/src/libs/subsonic/CMakeLists.txt index 2fd1576d..c3c27e6e 100644 --- a/src/libs/subsonic/CMakeLists.txt +++ b/src/libs/subsonic/CMakeLists.txt @@ -34,7 +34,6 @@ add_library(lmssubsonic STATIC impl/responses/ReplayGain.cpp impl/responses/Song.cpp impl/responses/User.cpp - impl/CoverArtId.cpp impl/RequestContext.cpp impl/ResponseFormat.cpp impl/ProtocolVersion.cpp diff --git a/src/libs/subsonic/impl/CoverArtId.cpp b/src/libs/subsonic/impl/CoverArtId.cpp deleted file mode 100644 index 9f184a21..00000000 --- a/src/libs/subsonic/impl/CoverArtId.cpp +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (C) 2024 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 "CoverArtId.hpp" - -#include - -#include "core/String.hpp" -#include "database/objects/ArtworkId.hpp" - -namespace lms::api::subsonic -{ - namespace - { - constexpr char timestampSeparatorChar{ '-' }; - - std::string idToString(db::ArtworkId id) - { - return "art-" + id.toString(); - } - } // namespace - - std::string idToString(CoverArtId coverId) - { - std::string res; - - // produce "art-id-timestamp" - res = idToString(coverId.id); - res += timestampSeparatorChar; - res += std::to_string(coverId.timestamp); - - assert(!res.empty()); - return res; - } -} // namespace lms::api::subsonic - -// Used to parse parameters -namespace lms::core::stringUtils -{ - template<> - std::optional readAs(std::string_view str) - { - std::optional res; - - std::vector values{ core::stringUtils::splitString(str, '-') }; - - // expect "art-id-timestamp" - if (values.size() != 3) - return res; - - if (values[0] != "art") - return res; - - const auto value{ core::stringUtils::readAs(values[1]) }; - const auto timestamp{ core::stringUtils::readAs(values[2]) }; - if (!value || !timestamp) - return std::nullopt; - - res.emplace(api::subsonic::CoverArtId{ *value, *timestamp }); - - return res; - } -} // namespace lms::core::stringUtils diff --git a/src/libs/subsonic/impl/CoverArtId.hpp b/src/libs/subsonic/impl/CoverArtId.hpp deleted file mode 100644 index ec0c3ccb..00000000 --- a/src/libs/subsonic/impl/CoverArtId.hpp +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (C) 2024 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 "core/String.hpp" -#include "database/objects/ArtworkId.hpp" - -namespace lms::api::subsonic -{ - struct CoverArtId - { - db::ArtworkId id; - std::time_t timestamp; - }; - - std::string idToString(CoverArtId coverId); -} // namespace lms::api::subsonic - -// Used to parse parameters -namespace lms::core::stringUtils -{ - template<> - std::optional readAs(std::string_view str); -} // namespace lms::core::stringUtils diff --git a/src/libs/subsonic/impl/SubsonicId.cpp b/src/libs/subsonic/impl/SubsonicId.cpp index 11afc904..7226ccdf 100644 --- a/src/libs/subsonic/impl/SubsonicId.cpp +++ b/src/libs/subsonic/impl/SubsonicId.cpp @@ -26,6 +26,11 @@ namespace lms::api::subsonic return "ar-" + id.toString(); } + std::string idToString(db::ArtworkId id) + { + return "art-" + id.toString(); + } + std::string idToString(db::DirectoryId id) { return "dir-" + id.toString(); @@ -59,6 +64,19 @@ namespace lms::api::subsonic namespace lms::core::stringUtils { + template<> + std::optional readAs(std::string_view str) + { + std::vector values{ core::stringUtils::splitString(str, '-') }; + if (values.size() != 2 || values[0] != "art") + return std::nullopt; + + if (const auto value{ core::stringUtils::readAs(values[1]) }) + return db::ArtworkId{ *value }; + + return std::nullopt; + } + template<> std::optional readAs(std::string_view str) { diff --git a/src/libs/subsonic/impl/SubsonicId.hpp b/src/libs/subsonic/impl/SubsonicId.hpp index 4f1983e4..483c8eeb 100644 --- a/src/libs/subsonic/impl/SubsonicId.hpp +++ b/src/libs/subsonic/impl/SubsonicId.hpp @@ -20,7 +20,9 @@ #pragma once #include "core/String.hpp" + #include "database/objects/ArtistId.hpp" +#include "database/objects/ArtworkId.hpp" #include "database/objects/DirectoryId.hpp" #include "database/objects/MediaLibraryId.hpp" #include "database/objects/PodcastEpisodeId.hpp" @@ -32,6 +34,7 @@ namespace lms::api::subsonic { std::string idToString(db::ArtistId id); + std::string idToString(db::ArtworkId id); std::string idToString(db::DirectoryId id); std::string idToString(db::PodcastEpisodeId id); std::string idToString(db::PodcastId id); @@ -46,6 +49,9 @@ namespace lms::core::stringUtils template<> std::optional readAs(std::string_view str); + template<> + std::optional readAs(std::string_view str); + template<> std::optional readAs(std::string_view str); diff --git a/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp b/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp index 954d2523..47446d26 100644 --- a/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp +++ b/src/libs/subsonic/impl/endpoints/MediaRetrieval.cpp @@ -39,7 +39,6 @@ #include "services/artwork/IArtworkService.hpp" #include "services/transcoding/ITranscodeService.hpp" -#include "CoverArtId.hpp" #include "ParameterParsing.hpp" #include "RequestContext.hpp" #include "SubsonicId.hpp" @@ -359,13 +358,13 @@ namespace lms::api::subsonic void handleGetCoverArt(RequestContext& context, const Wt::Http::Request& /*request*/, Wt::Http::Response& response) { // Mandatory params - const CoverArtId coverArtId{ getMandatoryParameterAs(context.getParameters(), "id") }; + const db::ArtworkId artworkId{ getMandatoryParameterAs(context.getParameters(), "id") }; std::optional size{ getParameterAs(context.getParameters(), "size") }; if (size) *size = std::clamp(*size, std::size_t{ 32 }, std::size_t{ 2048 }); - std::shared_ptr image{ core::Service::get()->getImage(coverArtId.id, size) }; + std::shared_ptr image{ core::Service::get()->getImage(artworkId, size) }; if (!image) { response.setStatus(404); diff --git a/src/libs/subsonic/impl/responses/Album.cpp b/src/libs/subsonic/impl/responses/Album.cpp index e166266c..c76b8f7b 100644 --- a/src/libs/subsonic/impl/responses/Album.cpp +++ b/src/libs/subsonic/impl/responses/Album.cpp @@ -24,7 +24,6 @@ #include "core/String.hpp" #include "database/Types.hpp" #include "database/objects/Artist.hpp" -#include "database/objects/Artwork.hpp" #include "database/objects/Directory.hpp" #include "database/objects/Genre.hpp" #include "database/objects/Grouping.hpp" @@ -37,7 +36,6 @@ #include "services/feedback/IFeedbackService.hpp" #include "services/scrobbling/IScrobblingService.hpp" -#include "CoverArtId.hpp" #include "RequestContext.hpp" #include "SubsonicId.hpp" #include "responses/Artist.hpp" @@ -91,11 +89,8 @@ namespace lms::api::subsonic albumNode.setAttribute("created", core::stringUtils::toISO8601String(release->getAddedTime())); - if (const auto artwork{ release->getPreferredArtwork() }) - { - CoverArtId coverArtId{ artwork->getId(), artwork->getLastWrittenTime().toTime_t() }; - albumNode.setAttribute("coverArt", idToString(coverArtId)); - } + if (const db::ArtworkId artworkId{ release->getPreferredArtworkId() }; artworkId.isValid()) + albumNode.setAttribute("coverArt", idToString(artworkId)); if (const auto originalYear{ release->getOriginalYear() }) albumNode.setAttribute("year", *originalYear); diff --git a/src/libs/subsonic/impl/responses/Artist.cpp b/src/libs/subsonic/impl/responses/Artist.cpp index 43283780..5dbd597b 100644 --- a/src/libs/subsonic/impl/responses/Artist.cpp +++ b/src/libs/subsonic/impl/responses/Artist.cpp @@ -24,14 +24,12 @@ #include "core/String.hpp" #include "database/objects/Artist.hpp" -#include "database/objects/Artwork.hpp" #include "database/objects/Release.hpp" #include "database/objects/ReleaseArtistLink.hpp" #include "database/objects/TrackArtistLink.hpp" #include "database/objects/User.hpp" #include "services/feedback/IFeedbackService.hpp" -#include "CoverArtId.hpp" #include "RequestContext.hpp" #include "SubsonicId.hpp" @@ -69,11 +67,8 @@ namespace lms::api::subsonic Response::Node artistNode{ createMinimalArtistNode(artist) }; - if (const auto artwork{ artist->getPreferredArtwork() }) - { - CoverArtId coverArtId{ artwork->getId(), artwork->getLastWrittenTime().toTime_t() }; - artistNode.setAttribute("coverArt", idToString(coverArtId)); - } + if (const db::ArtworkId artworkId{ artist->getPreferredArtworkId() }; artworkId.isValid()) + artistNode.setAttribute("coverArt", idToString(artworkId)); bool hasAlbums{}; { diff --git a/src/libs/subsonic/impl/responses/DiscTitle.cpp b/src/libs/subsonic/impl/responses/DiscTitle.cpp index 355409c5..056155ae 100644 --- a/src/libs/subsonic/impl/responses/DiscTitle.cpp +++ b/src/libs/subsonic/impl/responses/DiscTitle.cpp @@ -19,10 +19,9 @@ #include "responses/DiscTitle.hpp" -#include "database/objects/Artwork.hpp" #include "database/objects/Medium.hpp" -#include "CoverArtId.hpp" +#include "SubsonicId.hpp" namespace lms::api::subsonic { @@ -32,11 +31,8 @@ namespace lms::api::subsonic discTitleNode.setAttribute("disc", medium->getPosition() ? *medium->getPosition() : 0); discTitleNode.setAttribute("title", medium->getName()); - if (const auto artwork{ medium->getPreferredArtwork() }) - { - CoverArtId coverArtId{ artwork->getId(), artwork->getLastWrittenTime().toTime_t() }; - discTitleNode.setAttribute("coverArt", idToString(coverArtId)); - } + if (const db::ArtworkId artworkId{ medium->getPreferredArtworkId() }; artworkId.isValid()) + discTitleNode.setAttribute("coverArt", idToString(artworkId)); return discTitleNode; } diff --git a/src/libs/subsonic/impl/responses/Playlist.cpp b/src/libs/subsonic/impl/responses/Playlist.cpp index 9e4c6871..b56dd31d 100644 --- a/src/libs/subsonic/impl/responses/Playlist.cpp +++ b/src/libs/subsonic/impl/responses/Playlist.cpp @@ -21,12 +21,10 @@ #include "core/Service.hpp" #include "core/String.hpp" -#include "database/objects/Artwork.hpp" #include "database/objects/TrackList.hpp" #include "database/objects/User.hpp" #include "services/artwork/IArtworkService.hpp" -#include "CoverArtId.hpp" #include "RequestContext.hpp" #include "SubsonicId.hpp" @@ -49,13 +47,7 @@ namespace lms::api::subsonic playlistNode.setAttribute("owner", trackListUser->getLoginName()); if (const db::ArtworkId artworkId{ core::Service::get()->findTrackListImage(tracklist->getId()) }; artworkId.isValid()) - { - if (const auto artwork{ db::Artwork::find(context.getDbSession(), artworkId) }) - { - CoverArtId coverArtId{ artwork->getId(), artwork->getLastWrittenTime().toTime_t() }; - playlistNode.setAttribute("coverArt", idToString(coverArtId)); - } - } + playlistNode.setAttribute("coverArt", idToString(artworkId)); if (context.isOpenSubsonicEnabled()) { diff --git a/src/libs/subsonic/impl/responses/Podcast.cpp b/src/libs/subsonic/impl/responses/Podcast.cpp index 4f89d1f0..5862edab 100644 --- a/src/libs/subsonic/impl/responses/Podcast.cpp +++ b/src/libs/subsonic/impl/responses/Podcast.cpp @@ -24,11 +24,9 @@ #include #include "core/String.hpp" -#include "database/objects/Artwork.hpp" #include "database/objects/Podcast.hpp" #include "database/objects/PodcastEpisode.hpp" -#include "CoverArtId.hpp" #include "RequestContext.hpp" #include "SubsonicId.hpp" @@ -69,11 +67,8 @@ namespace lms::api::subsonic // estimated bitrate if (episode->getEnclosureLength() > 0 && episode->getDuration() > std::chrono::milliseconds::zero()) episodeNode.setAttribute("bitrate", episode->getEnclosureLength() * 8 / std::chrono::duration_cast(episode->getDuration()).count()); - if (const auto artwork{ episode->getArtwork() }) - { - CoverArtId coverArtId{ artwork->getId(), artwork->getLastWrittenTime().toTime_t() }; - episodeNode.setAttribute("coverArt", idToString(coverArtId)); - } + if (const db::ArtworkId artworkId{ episode->getArtworkId() }; artworkId.isValid()) + episodeNode.setAttribute("coverArt", idToString(artworkId)); // Podcast specific attributes // Expose the streamId only if the episode is actually downloaded @@ -111,11 +106,8 @@ namespace lms::api::subsonic podcastNode.setAttribute("status", getStatus(podcast)); - if (const auto artwork{ podcast->getArtwork() }) - { - CoverArtId coverArtId{ artwork->getId(), artwork->getLastWrittenTime().toTime_t() }; - podcastNode.setAttribute("coverArt", idToString(coverArtId)); - } + if (const db::ArtworkId artworkId{ podcast->getArtworkId() }; artworkId.isValid()) + podcastNode.setAttribute("coverArt", idToString(artworkId)); if (includeEpisodes) { diff --git a/src/libs/subsonic/impl/responses/Song.cpp b/src/libs/subsonic/impl/responses/Song.cpp index 7c36bc15..5289a7fc 100644 --- a/src/libs/subsonic/impl/responses/Song.cpp +++ b/src/libs/subsonic/impl/responses/Song.cpp @@ -30,7 +30,6 @@ #include "database/Types.hpp" #include "database/objects/Artist.hpp" -#include "database/objects/Artwork.hpp" #include "database/objects/Directory.hpp" #include "database/objects/Genre.hpp" #include "database/objects/Grouping.hpp" @@ -45,7 +44,6 @@ #include "services/feedback/IFeedbackService.hpp" #include "services/scrobbling/IScrobblingService.hpp" -#include "CoverArtId.hpp" #include "RequestContext.hpp" #include "SubsonicId.hpp" #include "responses/Artist.hpp" @@ -124,15 +122,9 @@ namespace lms::api::subsonic trackResponse.setAttribute("transcodedContentType", core::getMimeType(std::filesystem::path{ "." + fileSuffix })); } - auto artwork{ track->getPreferredMediaArtwork() }; - if (!artwork) - artwork = track->getPreferredArtwork(); - - if (artwork) - { - CoverArtId coverArtId{ artwork->getId(), artwork->getLastWrittenTime().toTime_t() }; - trackResponse.setAttribute("coverArt", idToString(coverArtId)); - } + const db::ArtworkId artworkId{ track->getPreferredMediaArtworkId().isValid() ? track->getPreferredMediaArtworkId() : track->getPreferredArtworkId() }; + if (artworkId.isValid()) + trackResponse.setAttribute("coverArt", idToString(artworkId)); std::vector artistLinks; std::vector trackArtistLinks;