Skip to content

Commit

Permalink
Subsonic API: added timestamps into coverart ids, Made use of image i…
Browse files Browse the repository at this point in the history
…ds to save a lookup, ref #558
  • Loading branch information
epoupon committed Dec 8, 2024
1 parent 38efdfa commit 4a8754a
Show file tree
Hide file tree
Showing 18 changed files with 248 additions and 174 deletions.
3 changes: 0 additions & 3 deletions conf/lms.conf
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,6 @@ api-subsonic-support-user-password-auth = true;
# Main usage is to make auto detections for the 'p' (password) parameter work
api-subsonic-old-server-protocol-clients = ("DSub");

# List of clients for whom a default cover is served (as they do not have their own)
api-subsonic-default-cover-clients = ("DSub", "substreamer");

# List of clients for whom open subsonic extensions and extra fields are disabled
api-open-subsonic-disabled-clients = ("DSub");

Expand Down
71 changes: 20 additions & 51 deletions src/libs/services/artwork/impl/ArtworkService.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -192,85 +192,54 @@ namespace lms::cover
return image;
}

std::shared_ptr<image::IEncodedImage> ArtworkService::getTrackImage(db::TrackId trackId, std::optional<image::ImageSize> width)
std::shared_ptr<image::IEncodedImage> ArtworkService::getImage(db::ImageId imageId, std::optional<image::ImageSize> width)
{
const ImageCache::EntryDesc cacheEntryDesc{ trackId, width };
const ImageCache::EntryDesc cacheEntryDesc{ imageId, width };

std::shared_ptr<image::IEncodedImage> cover{ _cache.getImage(cacheEntryDesc) };
if (cover)
return cover;

std::filesystem::path trackFile;
std::filesystem::path imageFile;
{
db::Session& session{ _db.getTLSSession() };
auto transaction{ session.createReadTransaction() };

const db::Track::pointer track{ db::Track::find(session, trackId) };
if (track && track->hasCover())
trackFile = track->getAbsoluteFilePath();
const db::Image::pointer image{ db::Image::find(session, imageId) };
if (image)
imageFile = image->getAbsoluteFilePath();
}

cover = getTrackImage(trackFile, width);
cover = getFromImageFile(imageFile, width);
if (cover)
_cache.addImage(cacheEntryDesc, cover);

return cover;
}

std::shared_ptr<image::IEncodedImage> ArtworkService::getReleaseCover(db::ReleaseId releaseId, std::optional<image::ImageSize> width)
{
const ImageCache::EntryDesc cacheEntryDesc{ releaseId, width };

std::shared_ptr<image::IEncodedImage> image{ _cache.getImage(cacheEntryDesc) };
if (image)
return image;

std::filesystem::path imagePath;
{
db::Session& session{ _db.getTLSSession() };
auto transaction{ session.createReadTransaction() };

const db::Release::pointer release{ db::Release::find(session, releaseId) };
if (release)
{
if (const db::Image::pointer dbImage{ release->getImage() })
imagePath = dbImage->getAbsoluteFilePath();
}
}

image = getFromImageFile(imagePath, width);
if (image)
_cache.addImage(cacheEntryDesc, image);

return image;
}

std::shared_ptr<image::IEncodedImage> ArtworkService::getArtistImage(db::ArtistId artistId, std::optional<image::ImageSize> width)
std::shared_ptr<image::IEncodedImage> ArtworkService::getTrackImage(db::TrackId trackId, std::optional<image::ImageSize> width)
{
const ImageCache::EntryDesc cacheEntryDesc{ artistId, width };
const ImageCache::EntryDesc cacheEntryDesc{ trackId, width };

std::shared_ptr<image::IEncodedImage> artistImage{ _cache.getImage(cacheEntryDesc) };
if (artistImage)
return artistImage;
std::shared_ptr<image::IEncodedImage> cover{ _cache.getImage(cacheEntryDesc) };
if (cover)
return cover;

std::filesystem::path imagePath;
std::filesystem::path trackFile;
{
db::Session& session{ _db.getTLSSession() };

auto transaction{ session.createReadTransaction() };

if (const db::Artist::pointer artist{ db::Artist::find(session, artistId) })
{
if (const db::Image::pointer image{ artist->getImage() })
imagePath = image->getAbsoluteFilePath();
}
const db::Track::pointer track{ db::Track::find(session, trackId) };
if (track && track->hasCover())
trackFile = track->getAbsoluteFilePath();
}

artistImage = getFromImageFile(imagePath, width);
if (artistImage)
_cache.addImage(cacheEntryDesc, artistImage);
cover = getTrackImage(trackFile, width);
if (cover)
_cache.addImage(cacheEntryDesc, cover);

return artistImage;
return cover;
}

void ArtworkService::flushCache()
Expand Down
3 changes: 1 addition & 2 deletions src/libs/services/artwork/impl/ArtworkService.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,8 @@ namespace lms::cover
ArtworkService& operator=(const ArtworkService&) = delete;

private:
std::shared_ptr<image::IEncodedImage> getImage(db::ImageId imageId, std::optional<image::ImageSize> width) override;
std::shared_ptr<image::IEncodedImage> getTrackImage(db::TrackId trackId, std::optional<image::ImageSize> width) override;
std::shared_ptr<image::IEncodedImage> getReleaseCover(db::ReleaseId releaseId, std::optional<image::ImageSize> width) override;
std::shared_ptr<image::IEncodedImage> getArtistImage(db::ArtistId artistId, std::optional<image::ImageSize> width) override;
std::shared_ptr<image::IEncodedImage> getDefaultReleaseCover() override;
std::shared_ptr<image::IEncodedImage> getDefaultArtistImage() override;

Expand Down
5 changes: 2 additions & 3 deletions src/libs/services/artwork/impl/ImageCache.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@
#include <unordered_map>
#include <variant>

#include "database/ArtistId.hpp"
#include "database/ReleaseId.hpp"
#include "database/ImageId.hpp"
#include "database/TrackId.hpp"
#include "image/IEncodedImage.hpp"

Expand All @@ -39,7 +38,7 @@ namespace lms::cover

struct EntryDesc
{
using VariantType = std::variant<db::ArtistId, db::ReleaseId, db::TrackId>;
using VariantType = std::variant<db::TrackId, db::ImageId>;
VariantType id;
std::optional<std::size_t> size;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@
#include <memory>
#include <optional>

#include "database/ArtistId.hpp"
#include "database/ReleaseId.hpp"
#include "database/ImageId.hpp"
#include "database/TrackId.hpp"
#include "image/IEncodedImage.hpp"

Expand All @@ -40,14 +39,11 @@ namespace lms::cover
public:
virtual ~IArtworkService() = default;

virtual std::shared_ptr<image::IEncodedImage> getArtistImage(db::ArtistId artistId, std::optional<image::ImageSize> width) = 0;
virtual std::shared_ptr<image::IEncodedImage> getImage(db::ImageId imageId, std::optional<image::ImageSize> width) = 0;

// no logic to fallback to release here
virtual std::shared_ptr<image::IEncodedImage> getTrackImage(db::TrackId trackId, std::optional<image::ImageSize> width) = 0;

// no logic to fallback to track here
virtual std::shared_ptr<image::IEncodedImage> getReleaseCover(db::ReleaseId releaseId, std::optional<image::ImageSize> width) = 0;

// Svg images dont have image "size"
virtual std::shared_ptr<image::IEncodedImage> getDefaultReleaseCover() = 0;
virtual std::shared_ptr<image::IEncodedImage> getDefaultArtistImage() = 0;
Expand Down
1 change: 1 addition & 0 deletions src/libs/subsonic/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ add_library(lmssubsonic SHARED
impl/responses/ReplayGain.cpp
impl/responses/Song.cpp
impl/responses/User.cpp
impl/CoverArtId.cpp
impl/ResponseFormat.cpp
impl/ProtocolVersion.cpp
impl/ParameterParsing.cpp
Expand Down
97 changes: 97 additions & 0 deletions src/libs/subsonic/impl/CoverArtId.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/

#include "CoverArtId.hpp"

#include "SubsonicId.hpp"
#include "core/String.hpp"

namespace lms::api::subsonic
{
namespace
{
constexpr char timestampSeparatorChar{ ':' };
}

std::string idToString(db::ImageId id)
{
return "im-" + id.toString();
}

std::string idToString(CoverArtId coverId)
{
// produce "id:timestamp"
std::string res{ std::visit([](auto&& id) {
return idToString(id);
},
coverId.id) };

res += timestampSeparatorChar;
res += std::to_string(coverId.timestamp);

return res;
}
} // namespace lms::api::subsonic

// Used to parse parameters
namespace lms::core::stringUtils
{
template<>
std::optional<db::ImageId> readAs(std::string_view str)
{
std::vector<std::string_view> values{ core::stringUtils::splitString(str, '-') };
if (values.size() != 2)
return std::nullopt;

if (values[0] != "im")
return std::nullopt;

if (const auto value{ core::stringUtils::readAs<db::ReleaseId::ValueType>(values[1]) })
return db::ImageId{ *value };

return std::nullopt;
}

template<>
std::optional<api::subsonic::CoverArtId> readAs(std::string_view str)
{
// expect "id:timestamp"
auto timeStampSeparator{ str.find_last_of(api::subsonic::timestampSeparatorChar) };
if (timeStampSeparator == std::string_view::npos)
return std::nullopt;

std::string_view strId{ str.substr(0, timeStampSeparator) };
std::string_view strTimestamp{ str.substr(timeStampSeparator + 1) };

api::subsonic::CoverArtId cover;
if (const auto imagetId{ readAs<db::ImageId>(strId) })
cover.id = *imagetId;
else if (const auto trackId{ readAs<db::TrackId>(strId) })
cover.id = *trackId;
else
return std::nullopt;

if (const auto timestamp{ readAs<std::time_t>(strTimestamp) })
cover.timestamp = *timestamp;
else
return std::nullopt;

return cover;
}
} // namespace lms::core::stringUtils
49 changes: 49 additions & 0 deletions src/libs/subsonic/impl/CoverArtId.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/

#pragma once

#include <ctime>
#include <variant>

#include "core/String.hpp"
#include "database/ImageId.hpp"
#include "database/TrackId.hpp"

namespace lms::api::subsonic
{
struct CoverArtId
{
std::variant<db::ImageId, db::TrackId> id;
std::time_t timestamp;
};

std::string idToString(CoverArtId coverId);
std::string idToString(db::ImageId imageId);
} // namespace lms::api::subsonic

// Used to parse parameters
namespace lms::core::stringUtils
{
template<>
std::optional<db::ImageId> readAs(std::string_view str);

template<>
std::optional<api::subsonic::CoverArtId> readAs(std::string_view str);
} // namespace lms::core::stringUtils
1 change: 0 additions & 1 deletion src/libs/subsonic/impl/RequestContext.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,5 @@ namespace lms::api::subsonic
ProtocolVersion serverProtocolVersion;
ResponseFormat responseFormat;
bool enableOpenSubsonic{ true };
bool enableDefaultCover{};
};
} // namespace lms::api::subsonic
16 changes: 0 additions & 16 deletions src/libs/subsonic/impl/SubsonicResource.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -85,19 +85,6 @@ namespace lms::api::subsonic
return res;
}

std::unordered_set<std::string> readDefaultCoverClients()
{
std::unordered_set<std::string> res;

core::Service<core::IConfig>::get()->visitStrings("api-subsonic-default-cover-clients",
[&](std::string_view client) {
res.emplace(std::string{ client });
},
{ "DSub", "substreamer" });

return res;
}

std::string parameterMapToDebugString(const Wt::Http::ParameterMap& parameterMap)
{
auto censorValue = [](const std::string& type, const std::string& value) -> std::string {
Expand Down Expand Up @@ -307,7 +294,6 @@ namespace lms::api::subsonic
SubsonicResource::SubsonicResource(db::Db& db)
: _serverProtocolVersionsByClient{ readConfigProtocolVersions() }
, _openSubsonicDisabledClients{ readOpenSubsonicDisabledClients() }
, _defaultReleaseCoverClients{ readDefaultCoverClients() }
, _supportUserPasswordAuthentication{ core::Service<core::IConfig>::get()->getBool("api-subsonic-support-user-password-auth", true) }
, _db{ db }
{
Expand Down Expand Up @@ -425,7 +411,6 @@ namespace lms::api::subsonic
const Wt::Http::ParameterMap& parameters{ request.getParameterMap() };
const ClientInfo clientInfo{ getClientInfo(request) };
bool enableOpenSubsonic{ !_openSubsonicDisabledClients.contains(clientInfo.name) };
bool enableDefaultCover{ _defaultReleaseCoverClients.contains(clientInfo.name) };
const ResponseFormat format{ getParameterAs<std::string>(request.getParameterMap(), "f").value_or("xml") == "json" ? ResponseFormat::json : ResponseFormat::xml };

return RequestContext{
Expand All @@ -437,7 +422,6 @@ namespace lms::api::subsonic
.serverProtocolVersion = getServerProtocolVersion(clientInfo.name),
.responseFormat = format,
.enableOpenSubsonic = enableOpenSubsonic,
.enableDefaultCover = enableDefaultCover
};
}

Expand Down
1 change: 0 additions & 1 deletion src/libs/subsonic/impl/SubsonicResource.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ namespace lms::api::subsonic

const std::unordered_map<std::string, ProtocolVersion> _serverProtocolVersionsByClient;
const std::unordered_set<std::string> _openSubsonicDisabledClients;
const std::unordered_set<std::string> _defaultReleaseCoverClients;
const bool _supportUserPasswordAuthentication;

db::Db& _db;
Expand Down
Loading

0 comments on commit 4a8754a

Please sign in to comment.