Merge branch 'artwork-disc' into develop
This commit is contained in:
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
<message id="Lms.Explore.Artist.template">
|
<message id="Lms.Explore.Artist.template">
|
||||||
<div class="row mb-3 gy-3">
|
<div class="row mb-3 gy-3">
|
||||||
<div class="col-lg-3 col-md-4">
|
<div class="col-lg-3 col-md-4">
|
||||||
${artwork class="Lms-cursor-pointer rounded d-block mx-auto"}
|
${artwork class="rounded d-block mx-auto"}
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h2>${name}</h2>
|
<h2>${name}</h2>
|
||||||
|
|||||||
@@ -133,6 +133,7 @@
|
|||||||
<message id="Lms.Admin.ScannerController.step-associating-external-lyrics">Associating external lyrics: {1}%...</message>
|
<message id="Lms.Admin.ScannerController.step-associating-external-lyrics">Associating external lyrics: {1}%...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-associating-playlist-tracks">Associating playlist tracks: {1}%...</message>
|
<message id="Lms.Admin.ScannerController.step-associating-playlist-tracks">Associating playlist tracks: {1}%...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-associating-release-images">Associating release images: {1}%...</message>
|
<message id="Lms.Admin.ScannerController.step-associating-release-images">Associating release images: {1}%...</message>
|
||||||
|
<message id="Lms.Admin.ScannerController.step-associating-track-images">Associating track images: {1}%...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">Checking for duplicate files... {1} files</message>
|
<message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">Checking for duplicate files... {1} files</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Checking for removed files... {1}%</message>
|
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Checking for removed files... {1}%</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-compact">Compacting database...</message>
|
<message id="Lms.Admin.ScannerController.step-compact">Compacting database...</message>
|
||||||
|
|||||||
@@ -133,6 +133,7 @@
|
|||||||
<message id="Lms.Admin.ScannerController.step-associating-external-lyrics">Association des paroles externes: {1}%...</message>
|
<message id="Lms.Admin.ScannerController.step-associating-external-lyrics">Association des paroles externes: {1}%...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-associating-playlist-tracks">Association des pistes des listes de lectures: {1}%...</message>
|
<message id="Lms.Admin.ScannerController.step-associating-playlist-tracks">Association des pistes des listes de lectures: {1}%...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-associating-release-images">Asignando imágenes de los álbumes: {1}%...</message>
|
<message id="Lms.Admin.ScannerController.step-associating-release-images">Asignando imágenes de los álbumes: {1}%...</message>
|
||||||
|
<message id="Lms.Admin.ScannerController.step-associating-track-images">Asignando imágenes de las pistas: {1}%...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">Comprobando ficheros duplicados ... {1} ficheros</message>
|
<message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">Comprobando ficheros duplicados ... {1} ficheros</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Comprobando ficheros eliminados... {1}%</message>
|
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Comprobando ficheros eliminados... {1}%</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-compact">Compactando la base de datos...</message>
|
<message id="Lms.Admin.ScannerController.step-compact">Compactando la base de datos...</message>
|
||||||
|
|||||||
@@ -133,6 +133,7 @@
|
|||||||
<message id="Lms.Admin.ScannerController.step-associating-external-lyrics">Association des paroles externes: {1}%...</message>
|
<message id="Lms.Admin.ScannerController.step-associating-external-lyrics">Association des paroles externes: {1}%...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-associating-playlist-tracks">Association des pistes des listes de lectures: {1}%...</message>
|
<message id="Lms.Admin.ScannerController.step-associating-playlist-tracks">Association des pistes des listes de lectures: {1}%...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-associating-release-images">Association des images des albums: {1}%...</message>
|
<message id="Lms.Admin.ScannerController.step-associating-release-images">Association des images des albums: {1}%...</message>
|
||||||
|
<message id="Lms.Admin.ScannerController.step-associating-track-images">Association des images des pistes: {1}%...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">Vérification des fichiers dupliqués... {1} fichiers</message>
|
<message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">Vérification des fichiers dupliqués... {1} fichiers</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Vérification des fichiers supprimés... {1}%</message>
|
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Vérification des fichiers supprimés... {1}%</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-compact">Compactage de la base de données...</message>
|
<message id="Lms.Admin.ScannerController.step-compact">Compactage de la base de données...</message>
|
||||||
|
|||||||
@@ -133,6 +133,7 @@
|
|||||||
<message id="Lms.Admin.ScannerController.step-associating-external-lyrics">Associazione dei testi esterni: {1}%...</message>
|
<message id="Lms.Admin.ScannerController.step-associating-external-lyrics">Associazione dei testi esterni: {1}%...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-associating-playlist-tracks">Associando brani della playlist: {1}%...</message>
|
<message id="Lms.Admin.ScannerController.step-associating-playlist-tracks">Associando brani della playlist: {1}%...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-associating-release-images">Associando immagini degli album: {1}%...</message>
|
<message id="Lms.Admin.ScannerController.step-associating-release-images">Associando immagini degli album: {1}%...</message>
|
||||||
|
<message id="Lms.Admin.ScannerController.step-associating-track-images">Associando immagini delle tracce: {1}%...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">Controllo duplicati... {1} files</message>
|
<message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">Controllo duplicati... {1} files</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Controllo file... {1}%</message>
|
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Controllo file... {1}%</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-compact">Compattazione del database...</message>
|
<message id="Lms.Admin.ScannerController.step-compact">Compattazione del database...</message>
|
||||||
|
|||||||
@@ -142,6 +142,7 @@
|
|||||||
<message id="Lms.Admin.ScannerController.step-associating-external-lyrics">Kojarzenie zewnętrznych tekstów: {1}%...</message>
|
<message id="Lms.Admin.ScannerController.step-associating-external-lyrics">Kojarzenie zewnętrznych tekstów: {1}%...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-associating-playlist-tracks">Kojarzenie utworów z playlisty: {1}%...</message>
|
<message id="Lms.Admin.ScannerController.step-associating-playlist-tracks">Kojarzenie utworów z playlisty: {1}%...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-associating-release-images">Kojarzenie obrazów albumów: {1}%...</message>
|
<message id="Lms.Admin.ScannerController.step-associating-release-images">Kojarzenie obrazów albumów: {1}%...</message>
|
||||||
|
<message id="Lms.Admin.ScannerController.step-associating-track-images">Kojarzenie obrazów ścieżek: {1}%...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">
|
<message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">
|
||||||
<plural case="0">Sprawdzanie duplikatów... {1} plik</plural>
|
<plural case="0">Sprawdzanie duplikatów... {1} plik</plural>
|
||||||
<plural case="1">Sprawdzanie duplikatów... {1} pliki</plural>
|
<plural case="1">Sprawdzanie duplikatów... {1} pliki</plural>
|
||||||
|
|||||||
+7
-4
@@ -9,7 +9,7 @@
|
|||||||
<message id="Lms.Explore.Release.template">
|
<message id="Lms.Explore.Release.template">
|
||||||
<div class="row mb-3 gy-3">
|
<div class="row mb-3 gy-3">
|
||||||
<div class="col-lg-3 col-md-4">
|
<div class="col-lg-3 col-md-4">
|
||||||
${cover class="Lms-cursor-pointer rounded d-block mx-auto"}
|
${artwork class="rounded d-block mx-auto"}
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<h2>${name}${<if-has-release-comment>}<span class="ms-1 text-muted">[${comment}]</span>${</if-has-release-comment>}</h2>
|
<h2>${name}${<if-has-release-comment>}<span class="ms-1 text-muted">[${comment}]</span>${</if-has-release-comment>}</h2>
|
||||||
@@ -54,8 +54,11 @@
|
|||||||
</message>
|
</message>
|
||||||
|
|
||||||
<message id="Lms.Explore.Release.template.entry-disc">
|
<message id="Lms.Explore.Release.template.entry-disc">
|
||||||
<div class="d-flex align-items-center">
|
<h4 class="d-flex align-items-center">
|
||||||
<h4 class="flex-fill">${disc-title}</h4>
|
${<if-has-artwork>}
|
||||||
|
<div class="p-1">${artwork class="Lms-cursor-pointer"}</div>
|
||||||
|
${</if-has-artwork>}
|
||||||
|
<div class="p-2 flex-fill">${disc-title}</div>
|
||||||
<div class="p-2 d-flex">
|
<div class="p-2 d-flex">
|
||||||
${play-btn class="d-none d-sm-block btn btn-sm btn-outline-secondary border-0"}
|
${play-btn class="d-none d-sm-block btn btn-sm btn-outline-secondary border-0"}
|
||||||
<div class="dropdown d-inline-block">
|
<div class="dropdown d-inline-block">
|
||||||
@@ -68,7 +71,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</h4>
|
||||||
${tracks class="d-grid gap-1 Lms-row-container"}
|
${tracks class="d-grid gap-1 Lms-row-container"}
|
||||||
</message>
|
</message>
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
|
|
||||||
#include "FileResourceHandler.hpp"
|
#include "FileResourceHandler.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
#include "core/ILogger.hpp"
|
#include "core/ILogger.hpp"
|
||||||
#include "core/MimeTypes.hpp"
|
#include "core/MimeTypes.hpp"
|
||||||
|
|
||||||
@@ -99,7 +101,7 @@ namespace lms::core
|
|||||||
} // end initial response setup
|
} // end initial response setup
|
||||||
|
|
||||||
::uint64_t restSize = _beyondLastByte - _offset;
|
::uint64_t restSize = _beyondLastByte - _offset;
|
||||||
::uint64_t pieceSize = std::min(restSize, _chunkSize);
|
::uint64_t pieceSize = std::min(restSize, static_cast<::uint64_t>(_chunkSize));
|
||||||
|
|
||||||
std::vector<char> buf(pieceSize);
|
std::vector<char> buf(pieceSize);
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
add_library(lmsdatabase STATIC
|
add_library(lmsdatabase STATIC
|
||||||
impl/Artist.cpp
|
impl/Artist.cpp
|
||||||
impl/ArtistInfo.cpp
|
impl/ArtistInfo.cpp
|
||||||
|
impl/Artwork.cpp
|
||||||
impl/AuthToken.cpp
|
impl/AuthToken.cpp
|
||||||
impl/Cluster.cpp
|
impl/Cluster.cpp
|
||||||
impl/Db.cpp
|
impl/Db.cpp
|
||||||
|
|||||||
@@ -21,9 +21,9 @@
|
|||||||
#include <Wt/Dbo/WtSqlTraits.h>
|
#include <Wt/Dbo/WtSqlTraits.h>
|
||||||
|
|
||||||
#include "core/ILogger.hpp"
|
#include "core/ILogger.hpp"
|
||||||
|
#include "database/Artwork.hpp"
|
||||||
#include "database/Cluster.hpp"
|
#include "database/Cluster.hpp"
|
||||||
#include "database/Directory.hpp"
|
#include "database/Directory.hpp"
|
||||||
#include "database/Image.hpp"
|
|
||||||
#include "database/Release.hpp"
|
#include "database/Release.hpp"
|
||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
#include "database/Track.hpp"
|
#include "database/Track.hpp"
|
||||||
@@ -315,9 +315,14 @@ AND NOT EXISTS (
|
|||||||
return getMBID().has_value();
|
return getMBID().has_value();
|
||||||
}
|
}
|
||||||
|
|
||||||
ObjectPtr<Image> Artist::getImage() const
|
ObjectPtr<Artwork> Artist::getPreferredArtwork() const
|
||||||
{
|
{
|
||||||
return ObjectPtr<Image>{ _image };
|
return ObjectPtr<Artwork>{ _preferredArtwork };
|
||||||
|
}
|
||||||
|
|
||||||
|
ArtworkId Artist::getPreferredArtworkId() const
|
||||||
|
{
|
||||||
|
return _preferredArtwork.id();
|
||||||
}
|
}
|
||||||
|
|
||||||
RangeResults<ArtistId> Artist::findSimilarArtistIds(core::EnumSet<TrackArtistLinkType> artistLinkTypes, std::optional<Range> range) const
|
RangeResults<ArtistId> Artist::findSimilarArtistIds(core::EnumSet<TrackArtistLinkType> artistLinkTypes, std::optional<Range> range) const
|
||||||
@@ -416,8 +421,8 @@ AND NOT EXISTS (
|
|||||||
LMS_LOG(DB, WARNING, "Artist sort name too long, truncated to '" << _sortName << "'");
|
LMS_LOG(DB, WARNING, "Artist sort name too long, truncated to '" << _sortName << "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
void Artist::setImage(ObjectPtr<Image> image)
|
void Artist::setPreferredArtwork(ObjectPtr<Artwork> artwork)
|
||||||
{
|
{
|
||||||
_image = getDboPtr(image);
|
_preferredArtwork = getDboPtr(artwork);
|
||||||
}
|
}
|
||||||
} // namespace lms::db
|
} // namespace lms::db
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 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 "database/Artwork.hpp"
|
||||||
|
|
||||||
|
#include <Wt/Dbo/WtSqlTraits.h>
|
||||||
|
|
||||||
|
#include "database/Image.hpp"
|
||||||
|
#include "database/Session.hpp"
|
||||||
|
#include "database/TrackEmbeddedImage.hpp"
|
||||||
|
|
||||||
|
#include "Utils.hpp"
|
||||||
|
#include "traits/IdTypeTraits.hpp"
|
||||||
|
|
||||||
|
namespace lms::db
|
||||||
|
{
|
||||||
|
Artwork::Artwork(ObjectPtr<TrackEmbeddedImage> trackEmbeddedImage)
|
||||||
|
: _trackEmbeddedImage{ getDboPtr(trackEmbeddedImage) }
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
Artwork::Artwork(ObjectPtr<Image> image)
|
||||||
|
: _image{ getDboPtr(image) }
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
Artwork::pointer Artwork::create(Session& session, ObjectPtr<TrackEmbeddedImage> trackEmbeddedImage)
|
||||||
|
{
|
||||||
|
session.checkWriteTransaction();
|
||||||
|
return session.getDboSession()->add(std::unique_ptr<Artwork>{ new Artwork{ trackEmbeddedImage } });
|
||||||
|
}
|
||||||
|
|
||||||
|
Artwork::pointer Artwork::create(Session& session, ObjectPtr<Image> image)
|
||||||
|
{
|
||||||
|
session.checkWriteTransaction();
|
||||||
|
return session.getDboSession()->add(std::unique_ptr<Artwork>{ new Artwork{ image } });
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t Artwork::getCount(Session& session)
|
||||||
|
{
|
||||||
|
session.checkReadTransaction();
|
||||||
|
return utils::fetchQuerySingleResult(session.getDboSession()->query<long>("SELECT COUNT(*) FROM artwork"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Artwork::pointer Artwork::find(Session& session, ArtworkId id)
|
||||||
|
{
|
||||||
|
session.checkReadTransaction();
|
||||||
|
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Artwork>>("SELECT a FROM artwork a").where("a.id = ?").bind(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
Artwork::pointer Artwork::find(Session& session, TrackEmbeddedImageId id)
|
||||||
|
{
|
||||||
|
session.checkReadTransaction();
|
||||||
|
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Artwork>>("SELECT a FROM artwork a").where("a.track_embedded_image_id = ?").bind(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
Artwork::pointer Artwork::find(Session& session, ImageId id)
|
||||||
|
{
|
||||||
|
session.checkReadTransaction();
|
||||||
|
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Artwork>>("SELECT a FROM artwork a").where("a.image_id = ?").bind(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
Wt::WDateTime Artwork::getLastWrittenTime() const
|
||||||
|
{
|
||||||
|
auto query{ session()->query<Wt::WDateTime>("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);
|
||||||
|
}
|
||||||
|
} // namespace lms::db
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
#include "database/Cluster.hpp"
|
#include "database/Cluster.hpp"
|
||||||
|
|
||||||
#include "database/Artist.hpp"
|
#include "database/Artist.hpp"
|
||||||
|
#include "database/Artwork.hpp"
|
||||||
#include "database/Directory.hpp"
|
#include "database/Directory.hpp"
|
||||||
#include "database/MediaLibrary.hpp"
|
#include "database/MediaLibrary.hpp"
|
||||||
#include "database/Release.hpp"
|
#include "database/Release.hpp"
|
||||||
|
|||||||
@@ -23,7 +23,6 @@
|
|||||||
|
|
||||||
#include "database/Artist.hpp"
|
#include "database/Artist.hpp"
|
||||||
#include "database/Directory.hpp"
|
#include "database/Directory.hpp"
|
||||||
#include "database/Release.hpp"
|
|
||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
|
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ namespace lms::db
|
|||||||
{
|
{
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
static constexpr Version LMS_DATABASE_VERSION{ 91 };
|
static constexpr Version LMS_DATABASE_VERSION{ 93 };
|
||||||
}
|
}
|
||||||
|
|
||||||
VersionInfo::VersionInfo()
|
VersionInfo::VersionInfo()
|
||||||
@@ -411,7 +411,7 @@ SELECT
|
|||||||
copyright_url,
|
copyright_url,
|
||||||
track_replay_gain,
|
track_replay_gain,
|
||||||
release_replay_gain,
|
release_replay_gain,
|
||||||
COALESCE(artist_display_name, ""),
|
artist_display_name,
|
||||||
release_id,
|
release_id,
|
||||||
1
|
1
|
||||||
FROM track)");
|
FROM track)");
|
||||||
@@ -1213,6 +1213,173 @@ FROM tracklist)");
|
|||||||
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET artist_info_scan_version = artist_info_scan_version + 1");
|
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET artist_info_scan_version = artist_info_scan_version + 1");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void migrateFromV91(Session& session)
|
||||||
|
{
|
||||||
|
dropIndexes(session);
|
||||||
|
utils::executeCommand(*session.getDboSession(), "ALTER TABLE track_embedded_image_link DROP COLUMN is_preferred");
|
||||||
|
}
|
||||||
|
|
||||||
|
void migrateFromV92(Session& session)
|
||||||
|
{
|
||||||
|
// Create the new artwork table
|
||||||
|
utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "artwork" (
|
||||||
|
"id" integer primary key autoincrement,
|
||||||
|
"version" integer not null,
|
||||||
|
"track_embedded_image_id" bigint,
|
||||||
|
"image_id" bigint,
|
||||||
|
constraint "fk_artwork_track_embedded_image" foreign key ("track_embedded_image_id") references "track_embedded_image" ("id") on delete cascade deferrable initially deferred,
|
||||||
|
constraint "fk_artwork_image" foreign key ("image_id") references "image" ("id") on delete cascade deferrable initially deferred))");
|
||||||
|
|
||||||
|
// Replaced image by artwork for release
|
||||||
|
utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "release_backup" (
|
||||||
|
"id" integer primary key autoincrement,
|
||||||
|
"version" integer not null,
|
||||||
|
"name" text not null,
|
||||||
|
"sort_name" text not null,
|
||||||
|
"mbid" text not null,
|
||||||
|
"group_mbid" text not null,
|
||||||
|
"total_disc" integer,
|
||||||
|
"artist_display_name" text not null,
|
||||||
|
"is_compilation" boolean not null,
|
||||||
|
"barcode" text not null,
|
||||||
|
"comment" text not null,
|
||||||
|
"preferred_artwork_id" bigint,
|
||||||
|
constraint "fk_release_preferred_artwork" foreign key ("preferred_artwork_id") references "artwork" ("id") on delete set null deferrable initially deferred))");
|
||||||
|
// Migrate data, with the new preferred_artwork_id field set to null
|
||||||
|
utils::executeCommand(*session.getDboSession(), R"(INSERT INTO release_backup
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
version,
|
||||||
|
name,
|
||||||
|
sort_name,
|
||||||
|
mbid,
|
||||||
|
group_mbid,
|
||||||
|
total_disc,
|
||||||
|
COALESCE(artist_display_name, ''),
|
||||||
|
is_compilation,
|
||||||
|
barcode,
|
||||||
|
comment,
|
||||||
|
NULL as preferred_artwork_id
|
||||||
|
FROM release)");
|
||||||
|
|
||||||
|
utils::executeCommand(*session.getDboSession(), "DROP TABLE release");
|
||||||
|
utils::executeCommand(*session.getDboSession(), "ALTER TABLE release_backup RENAME TO release");
|
||||||
|
|
||||||
|
// Replaced image by artwork for track
|
||||||
|
utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "track_backup" (
|
||||||
|
"id" integer primary key autoincrement,
|
||||||
|
"version" integer not null,
|
||||||
|
"scan_version" integer not null,
|
||||||
|
"track_number" integer,
|
||||||
|
"disc_number" integer,
|
||||||
|
"total_track" integer,
|
||||||
|
"disc_subtitle" text not null,
|
||||||
|
"name" text not null,
|
||||||
|
"duration" integer,
|
||||||
|
"bitrate" integer not null,
|
||||||
|
"bits_per_sample" integer not null,
|
||||||
|
"channel_count" integer not null,
|
||||||
|
"sample_rate" integer not null,
|
||||||
|
"date" text,
|
||||||
|
"original_date" text,
|
||||||
|
"absolute_file_path" text not null,
|
||||||
|
"relative_file_path" text not null,
|
||||||
|
"file_stem" text not null,
|
||||||
|
"file_name" text not null,
|
||||||
|
"file_size" bigint not null,
|
||||||
|
"file_last_write" text,
|
||||||
|
"file_added" text,
|
||||||
|
"mbid" text not null,
|
||||||
|
"recording_mbid" text not null,
|
||||||
|
"copyright" text not null,
|
||||||
|
"copyright_url" text not null,
|
||||||
|
"advisory" integer not null,
|
||||||
|
"track_replay_gain" real,
|
||||||
|
"release_replay_gain" real,
|
||||||
|
"artist_display_name" text not null,
|
||||||
|
"comment" text not null,
|
||||||
|
"release_id" bigint,
|
||||||
|
"media_library_id" bigint,
|
||||||
|
"directory_id" bigint,
|
||||||
|
"preferred_artwork_id" bigint,
|
||||||
|
"preferred_media_artwork_id" bigint,
|
||||||
|
constraint "fk_track_release" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred,
|
||||||
|
constraint "fk_track_media_library" foreign key ("media_library_id") references "media_library" ("id") on delete set null deferrable initially deferred,
|
||||||
|
constraint "fk_track_directory" foreign key ("directory_id") references "directory" ("id") on delete cascade deferrable initially deferred,
|
||||||
|
constraint "fk_track_preferred_artwork" foreign key ("preferred_artwork_id") references "artwork" ("id") on delete set null deferrable initially deferred,
|
||||||
|
constraint "fk_track_preferred_media_artwork" foreign key ("preferred_media_artwork_id") references "artwork" ("id") on delete set null deferrable initially deferred
|
||||||
|
))");
|
||||||
|
// Migrate data, with the new preferred_artwork_id and preferred_media_artwork_id fields set to null
|
||||||
|
utils::executeCommand(*session.getDboSession(), R"(INSERT INTO track_backup
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
version,
|
||||||
|
scan_version,
|
||||||
|
track_number,
|
||||||
|
disc_number,
|
||||||
|
total_track,
|
||||||
|
disc_subtitle,
|
||||||
|
name,
|
||||||
|
duration,
|
||||||
|
bitrate,
|
||||||
|
bits_per_sample,
|
||||||
|
channel_count,
|
||||||
|
sample_rate,
|
||||||
|
date,
|
||||||
|
original_date,
|
||||||
|
absolute_file_path,
|
||||||
|
relative_file_path,
|
||||||
|
file_stem,
|
||||||
|
file_name,
|
||||||
|
file_size,
|
||||||
|
file_last_write,
|
||||||
|
file_added,
|
||||||
|
mbid,
|
||||||
|
recording_mbid,
|
||||||
|
copyright,
|
||||||
|
copyright_url,
|
||||||
|
advisory,
|
||||||
|
track_replay_gain,
|
||||||
|
release_replay_gain,
|
||||||
|
artist_display_name,
|
||||||
|
comment,
|
||||||
|
release_id,
|
||||||
|
media_library_id,
|
||||||
|
directory_id,
|
||||||
|
NULL as preferred_artwork_id,
|
||||||
|
NULL as preferred_media_artwork_id
|
||||||
|
FROM track)");
|
||||||
|
utils::executeCommand(*session.getDboSession(), "DROP TABLE track");
|
||||||
|
utils::executeCommand(*session.getDboSession(), "ALTER TABLE track_backup RENAME TO track");
|
||||||
|
|
||||||
|
// Replaced image by artwork for artist
|
||||||
|
utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "artist_backup" (
|
||||||
|
"id" integer primary key autoincrement,
|
||||||
|
"version" integer not null,
|
||||||
|
"name" text not null,
|
||||||
|
"sort_name" text not null,
|
||||||
|
"mbid" text not null,
|
||||||
|
"preferred_artwork_id" bigint,
|
||||||
|
constraint "fk_artist_preferred_artwork" foreign key ("preferred_artwork_id") references "artwork" ("id") on delete set null deferrable initially deferred
|
||||||
|
))");
|
||||||
|
// Migrate data, with the new preferred_artwork_id field set to null
|
||||||
|
utils::executeCommand(*session.getDboSession(), R"(INSERT INTO artist_backup
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
version,
|
||||||
|
name,
|
||||||
|
sort_name,
|
||||||
|
mbid,
|
||||||
|
NULL as preferred_artwork_id
|
||||||
|
FROM artist)");
|
||||||
|
|
||||||
|
utils::executeCommand(*session.getDboSession(), "DROP TABLE artist");
|
||||||
|
utils::executeCommand(*session.getDboSession(), "ALTER TABLE artist_backup RENAME TO artist");
|
||||||
|
|
||||||
|
// Just increment the scan version of the settings to make the next scan rescan everything
|
||||||
|
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET audio_scan_version = audio_scan_version + 1");
|
||||||
|
}
|
||||||
|
|
||||||
bool doDbMigration(Session& session)
|
bool doDbMigration(Session& session)
|
||||||
{
|
{
|
||||||
constexpr std::string_view outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
|
constexpr std::string_view outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
|
||||||
@@ -1280,6 +1447,8 @@ FROM tracklist)");
|
|||||||
{ 88, migrateFromV88 },
|
{ 88, migrateFromV88 },
|
||||||
{ 89, migrateFromV89 },
|
{ 89, migrateFromV89 },
|
||||||
{ 90, migrateFromV90 },
|
{ 90, migrateFromV90 },
|
||||||
|
{ 91, migrateFromV91 },
|
||||||
|
{ 92, migrateFromV92 },
|
||||||
};
|
};
|
||||||
|
|
||||||
bool migrationPerformed{};
|
bool migrationPerformed{};
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
|
|
||||||
#include <Wt/Dbo/SqlTraits.h>
|
#include <Wt/Dbo/SqlTraits.h>
|
||||||
|
|
||||||
|
#include "database/Artwork.hpp"
|
||||||
#include "database/Directory.hpp"
|
#include "database/Directory.hpp"
|
||||||
#include "database/MediaLibrary.hpp"
|
#include "database/MediaLibrary.hpp"
|
||||||
#include "database/Release.hpp"
|
#include "database/Release.hpp"
|
||||||
|
|||||||
@@ -23,9 +23,9 @@
|
|||||||
|
|
||||||
#include "core/PartialDateTime.hpp"
|
#include "core/PartialDateTime.hpp"
|
||||||
#include "database/Artist.hpp"
|
#include "database/Artist.hpp"
|
||||||
|
#include "database/Artwork.hpp"
|
||||||
#include "database/Cluster.hpp"
|
#include "database/Cluster.hpp"
|
||||||
#include "database/Directory.hpp"
|
#include "database/Directory.hpp"
|
||||||
#include "database/Image.hpp"
|
|
||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
#include "database/Track.hpp"
|
#include "database/Track.hpp"
|
||||||
#include "database/Types.hpp"
|
#include "database/Types.hpp"
|
||||||
@@ -701,9 +701,14 @@ namespace lms::db
|
|||||||
return utils::fetchQueryResults<Release::pointer>(query);
|
return utils::fetchQueryResults<Release::pointer>(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
ObjectPtr<Image> Release::getImage() const
|
ObjectPtr<Artwork> Release::getPreferredArtwork() const
|
||||||
{
|
{
|
||||||
return ObjectPtr<Image>{ _image };
|
return ObjectPtr<Artwork>{ _preferredArtwork };
|
||||||
|
}
|
||||||
|
|
||||||
|
ArtworkId Release::getPreferredArtworkId() const
|
||||||
|
{
|
||||||
|
return _preferredArtwork.id();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Release::clearLabels()
|
void Release::clearLabels()
|
||||||
@@ -736,9 +741,9 @@ namespace lms::db
|
|||||||
_releaseTypes.insert(getDboPtr(releaseType));
|
_releaseTypes.insert(getDboPtr(releaseType));
|
||||||
}
|
}
|
||||||
|
|
||||||
void Release::setImage(ObjectPtr<Image> image)
|
void Release::setPreferredArtwork(ObjectPtr<Artwork> artwork)
|
||||||
{
|
{
|
||||||
_image = getDboPtr(image);
|
_preferredArtwork = getDboPtr(artwork);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Release::hasVariousArtists() const
|
bool Release::hasVariousArtists() const
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
#include "core/ITraceLogger.hpp"
|
#include "core/ITraceLogger.hpp"
|
||||||
#include "database/Artist.hpp"
|
#include "database/Artist.hpp"
|
||||||
#include "database/ArtistInfo.hpp"
|
#include "database/ArtistInfo.hpp"
|
||||||
|
#include "database/Artwork.hpp"
|
||||||
#include "database/AuthToken.hpp"
|
#include "database/AuthToken.hpp"
|
||||||
#include "database/Cluster.hpp"
|
#include "database/Cluster.hpp"
|
||||||
#include "database/Db.hpp"
|
#include "database/Db.hpp"
|
||||||
@@ -102,6 +103,7 @@ namespace lms::db
|
|||||||
|
|
||||||
_session.mapClass<Artist>("artist");
|
_session.mapClass<Artist>("artist");
|
||||||
_session.mapClass<ArtistInfo>("artist_info");
|
_session.mapClass<ArtistInfo>("artist_info");
|
||||||
|
_session.mapClass<Artwork>("artwork");
|
||||||
_session.mapClass<AuthToken>("auth_token");
|
_session.mapClass<AuthToken>("auth_token");
|
||||||
_session.mapClass<Cluster>("cluster");
|
_session.mapClass<Cluster>("cluster");
|
||||||
_session.mapClass<ClusterType>("cluster_type");
|
_session.mapClass<ClusterType>("cluster_type");
|
||||||
@@ -196,7 +198,6 @@ namespace lms::db
|
|||||||
{
|
{
|
||||||
auto transaction{ createWriteTransaction() };
|
auto transaction{ createWriteTransaction() };
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_id_idx ON artist(id)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_id_idx ON artist(id)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_image_idx ON artist(image_id)");
|
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_name_mbid_idx ON artist(name, mbid)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_name_mbid_idx ON artist(name, mbid)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_sort_name_nocase_idx ON artist(sort_name COLLATE NOCASE)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_sort_name_nocase_idx ON artist(sort_name COLLATE NOCASE)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_mbid_idx ON artist(mbid)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_mbid_idx ON artist(mbid)");
|
||||||
@@ -206,6 +207,10 @@ namespace lms::db
|
|||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_info_artist_id_idx ON artist_info(artist_id)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_info_artist_id_idx ON artist_info(artist_id)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_info_mbid_matched_artist_idx ON artist_info(mbid_matched, artist_id)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_info_mbid_matched_artist_idx ON artist_info(mbid_matched, artist_id)");
|
||||||
|
|
||||||
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artwork_id_idx ON artwork(id)");
|
||||||
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artwork_image_idx ON artwork(image_id)");
|
||||||
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artwork_track_embedded_image_idx ON artwork(track_embedded_image_id)");
|
||||||
|
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_user_domain_idx ON auth_token(user_id, domain)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_user_domain_idx ON auth_token(user_id, domain)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_domain_expiry_idx ON auth_token(domain, expiry)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_domain_expiry_idx ON auth_token(domain, expiry)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_domain_value_idx ON auth_token(domain, value)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_domain_value_idx ON auth_token(domain, value)");
|
||||||
@@ -229,7 +234,7 @@ namespace lms::db
|
|||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_link_id_idx ON track_embedded_image_link(id)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_link_id_idx ON track_embedded_image_link(id)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_link_track_id_idx ON track_embedded_image_link(track_id)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_link_track_id_idx ON track_embedded_image_link(track_id)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_link_track_embedded_image_id_track_id_idx ON track_embedded_image_link(track_embedded_image_id, track_id)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_link_track_embedded_image_id_track_id_idx ON track_embedded_image_link(track_embedded_image_id, track_id)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_link_is_preferred_track_id_track_embedded_image_id_idx ON track_embedded_image_link(is_preferred, track_id, track_embedded_image_id)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_link_type_track_embedded_image_id_track_id_idx ON track_embedded_image_link(type,track_embedded_image_id,track_id)");
|
||||||
|
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS image_directory_stem_idx ON image(directory_id, stem COLLATE NOCASE)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS image_directory_stem_idx ON image(directory_id, stem COLLATE NOCASE)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS image_id_idx ON image(id)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS image_id_idx ON image(id)");
|
||||||
@@ -256,7 +261,6 @@ namespace lms::db
|
|||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS rated_track_user_track_idx ON rated_track(user_id,track_id)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS rated_track_user_track_idx ON rated_track(user_id,track_id)");
|
||||||
|
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_id_idx ON release(id)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_id_idx ON release(id)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_image_idx ON release(image_id)");
|
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_group_mbid_idx ON release(group_mbid)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_group_mbid_idx ON release(group_mbid)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_mbid_idx ON release(mbid)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_mbid_idx ON release(mbid)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_name_idx ON release(name)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_name_idx ON release(name)");
|
||||||
@@ -397,6 +401,16 @@ namespace lms::db
|
|||||||
LMS_LOG(DB, INFO, "Analyze complete!");
|
LMS_LOG(DB, INFO, "Analyze complete!");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Session::areAllTablesEmpty()
|
||||||
|
{
|
||||||
|
const std::vector<std::string> entryList{ utils::fetchQueryResults(_session.query<std::string>("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")) };
|
||||||
|
|
||||||
|
return std::all_of(entryList.cbegin(), entryList.cend(), [this](const std::string& entry) {
|
||||||
|
const auto count{ utils::fetchQuerySingleResult(_session.query<long>("SELECT COUNT(*) FROM " + entry)) };
|
||||||
|
return count == 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void Session::retrieveEntriesToAnalyze(std::vector<std::string>& entryList)
|
void Session::retrieveEntriesToAnalyze(std::vector<std::string>& entryList)
|
||||||
{
|
{
|
||||||
auto transaction{ createReadTransaction() };
|
auto transaction{ createReadTransaction() };
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
|
|
||||||
#include "core/ILogger.hpp"
|
#include "core/ILogger.hpp"
|
||||||
#include "database/Artist.hpp"
|
#include "database/Artist.hpp"
|
||||||
|
#include "database/Artwork.hpp"
|
||||||
#include "database/Cluster.hpp"
|
#include "database/Cluster.hpp"
|
||||||
#include "database/Directory.hpp"
|
#include "database/Directory.hpp"
|
||||||
#include "database/MediaLibrary.hpp"
|
#include "database/MediaLibrary.hpp"
|
||||||
@@ -324,6 +325,26 @@ namespace lms::db
|
|||||||
return utils::execRangeQuery<TrackId>(query, range);
|
return utils::execRangeQuery<TrackId>(query, range);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Track::updatePreferredArtwork(Session& session, TrackId trackId, ArtworkId artworkId)
|
||||||
|
{
|
||||||
|
session.checkWriteTransaction();
|
||||||
|
|
||||||
|
if (artworkId.isValid())
|
||||||
|
utils::executeCommand(*session.getDboSession(), "UPDATE track SET preferred_artwork_id = ? WHERE id = ?", artworkId, trackId);
|
||||||
|
else
|
||||||
|
utils::executeCommand(*session.getDboSession(), "UPDATE track SET preferred_artwork_id = NULL WHERE id = ?", trackId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Track::updatePreferredMediaArtwork(Session& session, TrackId trackId, ArtworkId artworkId)
|
||||||
|
{
|
||||||
|
session.checkWriteTransaction();
|
||||||
|
|
||||||
|
if (artworkId.isValid())
|
||||||
|
utils::executeCommand(*session.getDboSession(), "UPDATE track SET preferred_media_artwork_id = ? WHERE id = ?", artworkId, trackId);
|
||||||
|
else
|
||||||
|
utils::executeCommand(*session.getDboSession(), "UPDATE track SET preferred_media_artwork_id = NULL WHERE id = ?", trackId);
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<Cluster::pointer> Track::getClusters() const
|
std::vector<Cluster::pointer> Track::getClusters() const
|
||||||
{
|
{
|
||||||
return utils::fetchQueryResults<Cluster::pointer>(_clusters.find());
|
return utils::fetchQueryResults<Cluster::pointer>(_clusters.find());
|
||||||
@@ -338,6 +359,36 @@ namespace lms::db
|
|||||||
return utils::fetchQueryResults(query);
|
return utils::fetchQueryResults(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ObjectPtr<MediaLibrary> Track::getMediaLibrary() const
|
||||||
|
{
|
||||||
|
return _mediaLibrary;
|
||||||
|
}
|
||||||
|
|
||||||
|
ObjectPtr<Directory> Track::getDirectory() const
|
||||||
|
{
|
||||||
|
return _directory;
|
||||||
|
}
|
||||||
|
|
||||||
|
ObjectPtr<Artwork> Track::getPreferredArtwork() const
|
||||||
|
{
|
||||||
|
return _preferredArtwork;
|
||||||
|
}
|
||||||
|
|
||||||
|
ArtworkId Track::getPreferredArtworkId() const
|
||||||
|
{
|
||||||
|
return _preferredArtwork.id();
|
||||||
|
}
|
||||||
|
|
||||||
|
ObjectPtr<Artwork> Track::getPreferredMediaArtwork() const
|
||||||
|
{
|
||||||
|
return _preferredMediaArtwork;
|
||||||
|
}
|
||||||
|
|
||||||
|
ArtworkId Track::getPreferredMediaArtworkId() const
|
||||||
|
{
|
||||||
|
return _preferredMediaArtwork.id();
|
||||||
|
}
|
||||||
|
|
||||||
RangeResults<TrackId> Track::findIds(Session& session, const FindParameters& parameters)
|
RangeResults<TrackId> Track::findIds(Session& session, const FindParameters& parameters)
|
||||||
{
|
{
|
||||||
session.checkReadTransaction();
|
session.checkReadTransaction();
|
||||||
@@ -484,6 +535,26 @@ namespace lms::db
|
|||||||
_embeddedImageLinks.insert(getDboPtr(image));
|
_embeddedImageLinks.insert(getDboPtr(image));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Track::setMediaLibrary(ObjectPtr<MediaLibrary> mediaLibrary)
|
||||||
|
{
|
||||||
|
_mediaLibrary = getDboPtr(mediaLibrary);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Track::setDirectory(ObjectPtr<Directory> directory)
|
||||||
|
{
|
||||||
|
_directory = getDboPtr(directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Track::setPreferredArtwork(ObjectPtr<Artwork> artwork)
|
||||||
|
{
|
||||||
|
_preferredArtwork = getDboPtr(artwork);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Track::setPreferredMediaArtwork(ObjectPtr<Artwork> artwork)
|
||||||
|
{
|
||||||
|
_preferredMediaArtwork = getDboPtr(artwork);
|
||||||
|
}
|
||||||
|
|
||||||
std::optional<int> Track::getYear() const
|
std::optional<int> Track::getYear() const
|
||||||
{
|
{
|
||||||
return _date.getYear();
|
return _date.getYear();
|
||||||
|
|||||||
@@ -38,24 +38,50 @@ namespace lms::db
|
|||||||
|
|
||||||
auto query{ session.getDboSession()->query<Wt::Dbo::ptr<TrackEmbeddedImage>>("SELECT t_e_i FROM track_embedded_image t_e_i") };
|
auto query{ session.getDboSession()->query<Wt::Dbo::ptr<TrackEmbeddedImage>>("SELECT t_e_i FROM track_embedded_image t_e_i") };
|
||||||
|
|
||||||
if (params.isPreferred
|
if (params.artist.isValid()
|
||||||
|
|| params.discNumber.has_value()
|
||||||
|| params.track.isValid()
|
|| params.track.isValid()
|
||||||
|| params.release.isValid()
|
|| params.release.isValid()
|
||||||
|| params.trackList.isValid()
|
|| params.trackList.isValid()
|
||||||
|| params.sortMethod == TrackEmbeddedImageSortMethod::FrontCoverAndSize)
|
|| !params.imageTypes.empty()
|
||||||
|
|| params.sortMethod == TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc
|
||||||
|
|| params.sortMethod == TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc)
|
||||||
{
|
{
|
||||||
query.join("track_embedded_image_link t_e_i_l ON t_e_i_l.track_embedded_image_id = t_e_i.id");
|
query.join("track_embedded_image_link t_e_i_l ON t_e_i_l.track_embedded_image_id = t_e_i.id");
|
||||||
|
|
||||||
if (params.isPreferred)
|
if (params.artist.isValid())
|
||||||
query.where("t_e_i_l.is_preferred = ?").bind(params.isPreferred.value());
|
{
|
||||||
|
query.join("track_artist_link t_a_l ON t_a_l.track_id = t_e_i_l.track_id");
|
||||||
|
query.where("t_a_l.artist_id = ?").bind(params.artist);
|
||||||
|
|
||||||
|
if (!params.trackArtistLinkTypes.empty())
|
||||||
|
{
|
||||||
|
std::string clause{ "t_a_l.type IN (" };
|
||||||
|
for (const auto& type : params.trackArtistLinkTypes)
|
||||||
|
{
|
||||||
|
if (clause.back() != '(')
|
||||||
|
clause += ",";
|
||||||
|
clause += "?";
|
||||||
|
query.bind(type);
|
||||||
|
}
|
||||||
|
clause += ")";
|
||||||
|
query.where(clause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (params.track.isValid())
|
if (params.track.isValid())
|
||||||
query.where("t_e_i_l.track_id = ?").bind(params.track);
|
query.where("t_e_i_l.track_id = ?").bind(params.track);
|
||||||
|
|
||||||
if (params.release.isValid())
|
if (params.release.isValid()
|
||||||
|
|| params.discNumber.has_value()
|
||||||
|
|| params.sortMethod == TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc
|
||||||
|
|| params.sortMethod == TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc)
|
||||||
{
|
{
|
||||||
query.join("track t ON t_e_i_l.track_id = t.id");
|
query.join("track t ON t_e_i_l.track_id = t.id");
|
||||||
query.where("t.release_id = ?").bind(params.release);
|
if (params.release.isValid())
|
||||||
|
query.where("t.release_id = ?").bind(params.release);
|
||||||
|
if (params.discNumber.has_value())
|
||||||
|
query.where("t.disc_number = ?").bind(params.discNumber.value());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.trackList.isValid())
|
if (params.trackList.isValid())
|
||||||
@@ -63,14 +89,38 @@ namespace lms::db
|
|||||||
query.join("tracklist_entry t_l_e ON t_l_e.track_id = t_e_i_l.track_id");
|
query.join("tracklist_entry t_l_e ON t_l_e.track_id = t_e_i_l.track_id");
|
||||||
query.where("t_l_e.tracklist_id = ?").bind(params.trackList);
|
query.where("t_l_e.tracklist_id = ?").bind(params.trackList);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!params.imageTypes.empty())
|
||||||
|
{
|
||||||
|
std::string clause{ "t_e_i_l.type IN (" };
|
||||||
|
for (const auto& type : params.imageTypes)
|
||||||
|
{
|
||||||
|
if (clause.back() != '(')
|
||||||
|
clause += ",";
|
||||||
|
clause += "?";
|
||||||
|
query.bind(type);
|
||||||
|
}
|
||||||
|
clause += ")";
|
||||||
|
query.where(clause);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (params.sortMethod)
|
switch (params.sortMethod)
|
||||||
{
|
{
|
||||||
case TrackEmbeddedImageSortMethod::None:
|
case TrackEmbeddedImageSortMethod::None:
|
||||||
break;
|
break;
|
||||||
case TrackEmbeddedImageSortMethod::FrontCoverAndSize:
|
case TrackEmbeddedImageSortMethod::SizeDesc:
|
||||||
query.orderBy("CASE WHEN t_e_i_l.type = ? THEN 0 ELSE 1 END, t_e_i.size").bind(ImageType::FrontCover);
|
query.orderBy("t_e_i.size DESC");
|
||||||
|
break;
|
||||||
|
case TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc:
|
||||||
|
query.orderBy("t.disc_number, t.track_number, t_e_i.size DESC");
|
||||||
|
break;
|
||||||
|
case TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc:
|
||||||
|
query.orderBy("t.track_number, t_e_i.size DESC");
|
||||||
|
break;
|
||||||
|
case TrackEmbeddedImageSortMethod::TrackListIndexAscThenSizeDesc:
|
||||||
|
assert(params.trackList.isValid());
|
||||||
|
query.orderBy("t_l_e.id, t_e_i.size DESC");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,14 @@ namespace lms::db
|
|||||||
return utils::fetchQuerySingleResult(session.getDboSession()->find<TrackEmbeddedImageLink>().where("id = ?").bind(id));
|
return utils::fetchQuerySingleResult(session.getDboSession()->find<TrackEmbeddedImageLink>().where("id = ?").bind(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TrackEmbeddedImageLink::find(Session& session, TrackEmbeddedImageId trackEmbeddedImageId, std::function<void(const pointer&)> visitor)
|
||||||
|
{
|
||||||
|
auto query{ session.getDboSession()->find<TrackEmbeddedImageLink>() };
|
||||||
|
query.where("track_embedded_image_id = ?").bind(trackEmbeddedImageId);
|
||||||
|
|
||||||
|
return utils::forEachQueryResult(query, visitor);
|
||||||
|
}
|
||||||
|
|
||||||
ObjectPtr<Track> TrackEmbeddedImageLink::getTrack() const
|
ObjectPtr<Track> TrackEmbeddedImageLink::getTrack() const
|
||||||
{
|
{
|
||||||
return _track;
|
return _track;
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
#include "core/EnumSet.hpp"
|
#include "core/EnumSet.hpp"
|
||||||
#include "core/UUID.hpp"
|
#include "core/UUID.hpp"
|
||||||
#include "database/ArtistId.hpp"
|
#include "database/ArtistId.hpp"
|
||||||
|
#include "database/ArtworkId.hpp"
|
||||||
#include "database/ClusterId.hpp"
|
#include "database/ClusterId.hpp"
|
||||||
#include "database/Filters.hpp"
|
#include "database/Filters.hpp"
|
||||||
#include "database/MediaLibraryId.hpp"
|
#include "database/MediaLibraryId.hpp"
|
||||||
@@ -42,10 +43,9 @@
|
|||||||
|
|
||||||
namespace lms::db
|
namespace lms::db
|
||||||
{
|
{
|
||||||
|
class Artwork;
|
||||||
class Cluster;
|
class Cluster;
|
||||||
class ClusterType;
|
class ClusterType;
|
||||||
class Image;
|
|
||||||
class Release;
|
class Release;
|
||||||
class Session;
|
class Session;
|
||||||
class StarredArtist;
|
class StarredArtist;
|
||||||
@@ -136,7 +136,8 @@ namespace lms::db
|
|||||||
const std::string& getSortName() const { return _sortName; }
|
const std::string& getSortName() const { return _sortName; }
|
||||||
std::optional<core::UUID> getMBID() const;
|
std::optional<core::UUID> getMBID() const;
|
||||||
bool hasMBID() const;
|
bool hasMBID() const;
|
||||||
ObjectPtr<Image> getImage() const;
|
ObjectPtr<Artwork> getPreferredArtwork() const;
|
||||||
|
ArtworkId getPreferredArtworkId() const;
|
||||||
void visitLinks(std::function<void(const ObjectPtr<TrackArtistLink>& link)> visitor) const;
|
void visitLinks(std::function<void(const ObjectPtr<TrackArtistLink>& link)> visitor) const;
|
||||||
|
|
||||||
// No artistLinkTypes means get them all
|
// No artistLinkTypes means get them all
|
||||||
@@ -150,7 +151,7 @@ namespace lms::db
|
|||||||
void setName(std::string_view name);
|
void setName(std::string_view name);
|
||||||
void setMBID(const std::optional<core::UUID>& mbid) { _mbid = mbid ? mbid->getAsString() : ""; }
|
void setMBID(const std::optional<core::UUID>& mbid) { _mbid = mbid ? mbid->getAsString() : ""; }
|
||||||
void setSortName(std::string_view sortName);
|
void setSortName(std::string_view sortName);
|
||||||
void setImage(ObjectPtr<Image> image);
|
void setPreferredArtwork(ObjectPtr<Artwork> artwork);
|
||||||
|
|
||||||
template<class Action>
|
template<class Action>
|
||||||
void persist(Action& a)
|
void persist(Action& a)
|
||||||
@@ -159,7 +160,7 @@ namespace lms::db
|
|||||||
Wt::Dbo::field(a, _sortName, "sort_name");
|
Wt::Dbo::field(a, _sortName, "sort_name");
|
||||||
Wt::Dbo::field(a, _mbid, "mbid");
|
Wt::Dbo::field(a, _mbid, "mbid");
|
||||||
|
|
||||||
Wt::Dbo::belongsTo(a, _image, "image", Wt::Dbo::OnDeleteSetNull);
|
Wt::Dbo::belongsTo(a, _preferredArtwork, "preferred_artwork", Wt::Dbo::OnDeleteSetNull);
|
||||||
Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "artist");
|
Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "artist");
|
||||||
Wt::Dbo::hasMany(a, _starredArtists, Wt::Dbo::ManyToMany, "user_starred_artists", "", Wt::Dbo::OnDeleteCascade);
|
Wt::Dbo::hasMany(a, _starredArtists, Wt::Dbo::ManyToMany, "user_starred_artists", "", Wt::Dbo::OnDeleteCascade);
|
||||||
}
|
}
|
||||||
@@ -176,7 +177,7 @@ namespace lms::db
|
|||||||
std::string _sortName;
|
std::string _sortName;
|
||||||
std::string _mbid; // Musicbrainz Identifier
|
std::string _mbid; // Musicbrainz Identifier
|
||||||
|
|
||||||
Wt::Dbo::ptr<Image> _image;
|
Wt::Dbo::ptr<Artwork> _preferredArtwork;
|
||||||
Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks; // Tracks involving this artist
|
Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks; // Tracks involving this artist
|
||||||
Wt::Dbo::collection<Wt::Dbo::ptr<StarredArtist>> _starredArtists; // starred entries for this artist
|
Wt::Dbo::collection<Wt::Dbo::ptr<StarredArtist>> _starredArtists; // starred entries for this artist
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2021 Emeric Poupon
|
* Copyright (C) 2025 Emeric Poupon
|
||||||
*
|
*
|
||||||
* This file is part of LMS.
|
* This file is part of LMS.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 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 <Wt/Dbo/Dbo.h>
|
||||||
|
#include <Wt/WDateTime.h>
|
||||||
|
|
||||||
|
#include "database/ArtworkId.hpp"
|
||||||
|
#include "database/ImageId.hpp"
|
||||||
|
#include "database/Object.hpp"
|
||||||
|
#include "database/TrackEmbeddedImageId.hpp"
|
||||||
|
|
||||||
|
namespace lms::db
|
||||||
|
{
|
||||||
|
class Image;
|
||||||
|
class Session;
|
||||||
|
class TrackEmbeddedImage;
|
||||||
|
|
||||||
|
class Artwork final : public Object<Artwork, ArtworkId>
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
Artwork() = default;
|
||||||
|
|
||||||
|
// find
|
||||||
|
static std::size_t getCount(Session& session);
|
||||||
|
static pointer find(Session& session, ArtworkId id);
|
||||||
|
static pointer find(Session& session, TrackEmbeddedImageId id);
|
||||||
|
static pointer find(Session& session, ImageId id);
|
||||||
|
|
||||||
|
// getters
|
||||||
|
TrackEmbeddedImageId getTrackEmbeddedImageId() const { return _trackEmbeddedImage.id(); }
|
||||||
|
ImageId getImageId() const { return _image.id(); }
|
||||||
|
Wt::WDateTime getLastWrittenTime() const;
|
||||||
|
|
||||||
|
template<class Action>
|
||||||
|
void persist(Action& a)
|
||||||
|
{
|
||||||
|
Wt::Dbo::belongsTo(a, _trackEmbeddedImage, "track_embedded_image", Wt::Dbo::OnDeleteCascade);
|
||||||
|
Wt::Dbo::belongsTo(a, _image, "image", Wt::Dbo::OnDeleteCascade);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
friend class Session;
|
||||||
|
Artwork(ObjectPtr<TrackEmbeddedImage> trackEmbeddedImage);
|
||||||
|
Artwork(ObjectPtr<Image> image);
|
||||||
|
static pointer create(Session& session, ObjectPtr<TrackEmbeddedImage> trackEmbeddedImage);
|
||||||
|
static pointer create(Session& session, ObjectPtr<Image> image);
|
||||||
|
|
||||||
|
Wt::Dbo::ptr<TrackEmbeddedImage> _trackEmbeddedImage;
|
||||||
|
Wt::Dbo::ptr<Image> _image;
|
||||||
|
};
|
||||||
|
} // namespace lms::db
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 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 "database/IdType.hpp"
|
||||||
|
|
||||||
|
LMS_DECLARE_IDTYPE(ArtworkId)
|
||||||
@@ -32,9 +32,7 @@
|
|||||||
|
|
||||||
namespace lms::db
|
namespace lms::db
|
||||||
{
|
{
|
||||||
class Artist;
|
|
||||||
class Directory;
|
class Directory;
|
||||||
class Release;
|
|
||||||
class Session;
|
class Session;
|
||||||
|
|
||||||
class Image final : public Object<Image, ImageId>
|
class Image final : public Object<Image, ImageId>
|
||||||
@@ -100,8 +98,6 @@ namespace lms::db
|
|||||||
Wt::Dbo::field(a, _width, "width");
|
Wt::Dbo::field(a, _width, "width");
|
||||||
Wt::Dbo::field(a, _height, "height");
|
Wt::Dbo::field(a, _height, "height");
|
||||||
|
|
||||||
Wt::Dbo::hasMany(a, _artists, Wt::Dbo::ManyToOne, "image");
|
|
||||||
Wt::Dbo::hasMany(a, _releases, Wt::Dbo::ManyToOne, "image");
|
|
||||||
Wt::Dbo::belongsTo(a, _directory, "directory", Wt::Dbo::OnDeleteCascade);
|
Wt::Dbo::belongsTo(a, _directory, "directory", Wt::Dbo::OnDeleteCascade);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,8 +113,6 @@ namespace lms::db
|
|||||||
int _width{};
|
int _width{};
|
||||||
int _height{};
|
int _height{};
|
||||||
|
|
||||||
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> _artists;
|
|
||||||
Wt::Dbo::collection<Wt::Dbo::ptr<Release>> _releases;
|
|
||||||
Wt::Dbo::ptr<Directory> _directory;
|
Wt::Dbo::ptr<Directory> _directory;
|
||||||
};
|
};
|
||||||
} // namespace lms::db
|
} // namespace lms::db
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
#include "core/PartialDateTime.hpp"
|
#include "core/PartialDateTime.hpp"
|
||||||
#include "core/UUID.hpp"
|
#include "core/UUID.hpp"
|
||||||
#include "database/ArtistId.hpp"
|
#include "database/ArtistId.hpp"
|
||||||
|
#include "database/ArtworkId.hpp"
|
||||||
#include "database/CountryId.hpp"
|
#include "database/CountryId.hpp"
|
||||||
#include "database/DirectoryId.hpp"
|
#include "database/DirectoryId.hpp"
|
||||||
#include "database/Filters.hpp"
|
#include "database/Filters.hpp"
|
||||||
@@ -45,9 +46,9 @@
|
|||||||
namespace lms::db
|
namespace lms::db
|
||||||
{
|
{
|
||||||
class Artist;
|
class Artist;
|
||||||
|
class Artwork;
|
||||||
class Cluster;
|
class Cluster;
|
||||||
class ClusterType;
|
class ClusterType;
|
||||||
class Image;
|
|
||||||
class Release;
|
class Release;
|
||||||
class Session;
|
class Session;
|
||||||
class Track;
|
class Track;
|
||||||
@@ -291,7 +292,8 @@ namespace lms::db
|
|||||||
core::EnumSet<Advisory> getAdvisories() const;
|
core::EnumSet<Advisory> getAdvisories() const;
|
||||||
std::string_view getBarcode() const { return _barcode; }
|
std::string_view getBarcode() const { return _barcode; }
|
||||||
std::string_view getComment() const { return _comment; }
|
std::string_view getComment() const { return _comment; }
|
||||||
ObjectPtr<Image> getImage() const;
|
ObjectPtr<Artwork> getPreferredArtwork() const;
|
||||||
|
ArtworkId getPreferredArtworkId() const;
|
||||||
|
|
||||||
// Setters
|
// Setters
|
||||||
void setName(std::string_view name) { _name = name; }
|
void setName(std::string_view name) { _name = name; }
|
||||||
@@ -309,7 +311,7 @@ namespace lms::db
|
|||||||
void addReleaseType(ObjectPtr<ReleaseType> releaseType);
|
void addReleaseType(ObjectPtr<ReleaseType> releaseType);
|
||||||
void setBarcode(std::string_view barcode) { _barcode = barcode; }
|
void setBarcode(std::string_view barcode) { _barcode = barcode; }
|
||||||
void setComment(std::string_view comment) { _comment = comment; }
|
void setComment(std::string_view comment) { _comment = comment; }
|
||||||
void setImage(ObjectPtr<Image> image);
|
void setPreferredArtwork(ObjectPtr<Artwork> artwork);
|
||||||
|
|
||||||
// Get the artists of this release
|
// Get the artists of this release
|
||||||
std::vector<ObjectPtr<Artist>> getArtists(TrackArtistLinkType type = TrackArtistLinkType::Artist) const;
|
std::vector<ObjectPtr<Artist>> getArtists(TrackArtistLinkType type = TrackArtistLinkType::Artist) const;
|
||||||
@@ -331,9 +333,9 @@ namespace lms::db
|
|||||||
Wt::Dbo::field(a, _isCompilation, "is_compilation");
|
Wt::Dbo::field(a, _isCompilation, "is_compilation");
|
||||||
Wt::Dbo::field(a, _barcode, "barcode");
|
Wt::Dbo::field(a, _barcode, "barcode");
|
||||||
Wt::Dbo::field(a, _comment, "comment");
|
Wt::Dbo::field(a, _comment, "comment");
|
||||||
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToOne, "release");
|
|
||||||
|
|
||||||
Wt::Dbo::belongsTo(a, _image, "image", Wt::Dbo::OnDeleteSetNull);
|
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToOne, "release");
|
||||||
|
Wt::Dbo::belongsTo(a, _preferredArtwork, "preferred_artwork", Wt::Dbo::OnDeleteSetNull);
|
||||||
Wt::Dbo::hasMany(a, _labels, Wt::Dbo::ManyToMany, "release_label", "", Wt::Dbo::OnDeleteCascade);
|
Wt::Dbo::hasMany(a, _labels, Wt::Dbo::ManyToMany, "release_label", "", Wt::Dbo::OnDeleteCascade);
|
||||||
Wt::Dbo::hasMany(a, _releaseTypes, Wt::Dbo::ManyToMany, "release_release_type", "", Wt::Dbo::OnDeleteCascade);
|
Wt::Dbo::hasMany(a, _releaseTypes, Wt::Dbo::ManyToMany, "release_release_type", "", Wt::Dbo::OnDeleteCascade);
|
||||||
Wt::Dbo::hasMany(a, _countries, Wt::Dbo::ManyToMany, "release_country", "", Wt::Dbo::OnDeleteCascade);
|
Wt::Dbo::hasMany(a, _countries, Wt::Dbo::ManyToMany, "release_country", "", Wt::Dbo::OnDeleteCascade);
|
||||||
@@ -359,8 +361,8 @@ namespace lms::db
|
|||||||
std::string _barcode;
|
std::string _barcode;
|
||||||
std::string _comment;
|
std::string _comment;
|
||||||
|
|
||||||
Wt::Dbo::ptr<Image> _image;
|
|
||||||
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> _tracks;
|
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> _tracks;
|
||||||
|
Wt::Dbo::ptr<Artwork> _preferredArtwork;
|
||||||
Wt::Dbo::collection<Wt::Dbo::ptr<Label>> _labels;
|
Wt::Dbo::collection<Wt::Dbo::ptr<Label>> _labels;
|
||||||
Wt::Dbo::collection<Wt::Dbo::ptr<ReleaseType>> _releaseTypes;
|
Wt::Dbo::collection<Wt::Dbo::ptr<ReleaseType>> _releaseTypes;
|
||||||
Wt::Dbo::collection<Wt::Dbo::ptr<Country>> _countries;
|
Wt::Dbo::collection<Wt::Dbo::ptr<Country>> _countries;
|
||||||
|
|||||||
@@ -96,6 +96,8 @@ namespace lms::db
|
|||||||
void retrieveEntriesToAnalyze(std::vector<std::string>& entryList);
|
void retrieveEntriesToAnalyze(std::vector<std::string>& entryList);
|
||||||
void analyzeEntry(const std::string& entry);
|
void analyzeEntry(const std::string& entry);
|
||||||
|
|
||||||
|
bool areAllTablesEmpty(); // need to acquire a read transaction
|
||||||
|
|
||||||
void prepareTablesIfNeeded(); // need to run only once at startup
|
void prepareTablesIfNeeded(); // need to run only once at startup
|
||||||
bool migrateSchemaIfNeeded(); // returns true if migration was performed
|
bool migrateSchemaIfNeeded(); // returns true if migration was performed
|
||||||
void createIndexesIfNeeded();
|
void createIndexesIfNeeded();
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
#include "core/PartialDateTime.hpp"
|
#include "core/PartialDateTime.hpp"
|
||||||
#include "core/UUID.hpp"
|
#include "core/UUID.hpp"
|
||||||
#include "database/ArtistId.hpp"
|
#include "database/ArtistId.hpp"
|
||||||
|
#include "database/ArtworkId.hpp"
|
||||||
#include "database/ClusterId.hpp"
|
#include "database/ClusterId.hpp"
|
||||||
#include "database/DirectoryId.hpp"
|
#include "database/DirectoryId.hpp"
|
||||||
#include "database/Filters.hpp"
|
#include "database/Filters.hpp"
|
||||||
@@ -50,6 +51,7 @@
|
|||||||
namespace lms::db
|
namespace lms::db
|
||||||
{
|
{
|
||||||
class Artist;
|
class Artist;
|
||||||
|
class Artwork;
|
||||||
class Cluster;
|
class Cluster;
|
||||||
class ClusterType;
|
class ClusterType;
|
||||||
class Directory;
|
class Directory;
|
||||||
@@ -215,6 +217,10 @@ namespace lms::db
|
|||||||
static RangeResults<TrackId> findIdsTrackMBIDDuplicates(Session& session, std::optional<Range> range = std::nullopt);
|
static RangeResults<TrackId> findIdsTrackMBIDDuplicates(Session& session, std::optional<Range> range = std::nullopt);
|
||||||
static RangeResults<TrackId> findIdsWithRecordingMBIDAndMissingFeatures(Session& session, std::optional<Range> range = std::nullopt);
|
static RangeResults<TrackId> findIdsWithRecordingMBIDAndMissingFeatures(Session& session, std::optional<Range> range = std::nullopt);
|
||||||
|
|
||||||
|
// Update utility functions
|
||||||
|
static void updatePreferredArtwork(Session& session, TrackId trackId, ArtworkId artworkId);
|
||||||
|
static void updatePreferredMediaArtwork(Session& session, TrackId trackId, ArtworkId artworkId);
|
||||||
|
|
||||||
// Accessors
|
// Accessors
|
||||||
void setScanVersion(std::size_t version) { _scanVersion = version; }
|
void setScanVersion(std::size_t version) { _scanVersion = version; }
|
||||||
void setTrackNumber(std::optional<int> num) { _trackNumber = num; }
|
void setTrackNumber(std::optional<int> num) { _trackNumber = num; }
|
||||||
@@ -252,8 +258,10 @@ namespace lms::db
|
|||||||
void addLyrics(const ObjectPtr<TrackLyrics>& lyrics);
|
void addLyrics(const ObjectPtr<TrackLyrics>& lyrics);
|
||||||
void clearEmbeddedImageLinks();
|
void clearEmbeddedImageLinks();
|
||||||
void addEmbeddedImageLink(const ObjectPtr<TrackEmbeddedImageLink>& link);
|
void addEmbeddedImageLink(const ObjectPtr<TrackEmbeddedImageLink>& link);
|
||||||
void setMediaLibrary(ObjectPtr<MediaLibrary> mediaLibrary) { _mediaLibrary = getDboPtr(mediaLibrary); }
|
void setMediaLibrary(ObjectPtr<MediaLibrary> mediaLibrary);
|
||||||
void setDirectory(ObjectPtr<Directory> directory) { _directory = getDboPtr(directory); }
|
void setDirectory(ObjectPtr<Directory> directory);
|
||||||
|
void setPreferredArtwork(ObjectPtr<Artwork> artwork);
|
||||||
|
void setPreferredMediaArtwork(ObjectPtr<Artwork> artwork);
|
||||||
|
|
||||||
std::size_t getScanVersion() const { return _scanVersion; }
|
std::size_t getScanVersion() const { return _scanVersion; }
|
||||||
std::optional<std::size_t> getTrackNumber() const { return _trackNumber; }
|
std::optional<std::size_t> getTrackNumber() const { return _trackNumber; }
|
||||||
@@ -295,8 +303,12 @@ namespace lms::db
|
|||||||
ObjectPtr<Release> getRelease() const { return _release; }
|
ObjectPtr<Release> getRelease() const { return _release; }
|
||||||
std::vector<ObjectPtr<Cluster>> getClusters() const;
|
std::vector<ObjectPtr<Cluster>> getClusters() const;
|
||||||
std::vector<ClusterId> getClusterIds() const;
|
std::vector<ClusterId> getClusterIds() const;
|
||||||
ObjectPtr<MediaLibrary> getMediaLibrary() const { return _mediaLibrary; }
|
ObjectPtr<MediaLibrary> getMediaLibrary() const;
|
||||||
ObjectPtr<Directory> getDirectory() const { return _directory; }
|
ObjectPtr<Directory> getDirectory() const;
|
||||||
|
ObjectPtr<Artwork> getPreferredArtwork() const;
|
||||||
|
ArtworkId getPreferredArtworkId() const;
|
||||||
|
ObjectPtr<Artwork> getPreferredMediaArtwork() const;
|
||||||
|
ArtworkId getPreferredMediaArtworkId() const;
|
||||||
|
|
||||||
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(const std::vector<ClusterTypeId>& clusterTypes, std::size_t size) const;
|
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(const std::vector<ClusterTypeId>& clusterTypes, std::size_t size) const;
|
||||||
|
|
||||||
@@ -331,11 +343,13 @@ namespace lms::db
|
|||||||
Wt::Dbo::field(a, _trackReplayGain, "track_replay_gain");
|
Wt::Dbo::field(a, _trackReplayGain, "track_replay_gain");
|
||||||
Wt::Dbo::field(a, _releaseReplayGain, "release_replay_gain"); // here in Track since Release does not have concept of "disc" (yet?)
|
Wt::Dbo::field(a, _releaseReplayGain, "release_replay_gain"); // here in Track since Release does not have concept of "disc" (yet?)
|
||||||
Wt::Dbo::field(a, _artistDisplayName, "artist_display_name");
|
Wt::Dbo::field(a, _artistDisplayName, "artist_display_name");
|
||||||
Wt::Dbo::field(a, _comment, "comment");
|
Wt::Dbo::field(a, _comment, "comment"); // TODO: move in a dedicated table
|
||||||
|
|
||||||
Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade);
|
Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade);
|
||||||
Wt::Dbo::belongsTo(a, _mediaLibrary, "media_library", Wt::Dbo::OnDeleteSetNull); // don't delete track on media library removal, we want to wait for the next scan to have a chance to migrate files
|
Wt::Dbo::belongsTo(a, _mediaLibrary, "media_library", Wt::Dbo::OnDeleteSetNull); // don't delete track on media library removal, we want to wait for the next scan to have a chance to migrate files
|
||||||
Wt::Dbo::belongsTo(a, _directory, "directory", Wt::Dbo::OnDeleteCascade);
|
Wt::Dbo::belongsTo(a, _directory, "directory", Wt::Dbo::OnDeleteCascade);
|
||||||
|
Wt::Dbo::belongsTo(a, _preferredArtwork, "preferred_artwork", Wt::Dbo::OnDeleteSetNull);
|
||||||
|
Wt::Dbo::belongsTo(a, _preferredMediaArtwork, "preferred_media_artwork", Wt::Dbo::OnDeleteSetNull);
|
||||||
Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "track");
|
Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "track");
|
||||||
Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade);
|
Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade);
|
||||||
Wt::Dbo::hasMany(a, _trackLyrics, Wt::Dbo::ManyToOne, "track");
|
Wt::Dbo::hasMany(a, _trackLyrics, Wt::Dbo::ManyToOne, "track");
|
||||||
@@ -382,6 +396,8 @@ namespace lms::db
|
|||||||
Wt::Dbo::ptr<Release> _release;
|
Wt::Dbo::ptr<Release> _release;
|
||||||
Wt::Dbo::ptr<MediaLibrary> _mediaLibrary;
|
Wt::Dbo::ptr<MediaLibrary> _mediaLibrary;
|
||||||
Wt::Dbo::ptr<Directory> _directory;
|
Wt::Dbo::ptr<Directory> _directory;
|
||||||
|
Wt::Dbo::ptr<Artwork> _preferredArtwork;
|
||||||
|
Wt::Dbo::ptr<Artwork> _preferredMediaArtwork;
|
||||||
Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks;
|
Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks;
|
||||||
Wt::Dbo::collection<Wt::Dbo::ptr<Cluster>> _clusters;
|
Wt::Dbo::collection<Wt::Dbo::ptr<Cluster>> _clusters;
|
||||||
Wt::Dbo::collection<Wt::Dbo::ptr<TrackLyrics>> _trackLyrics;
|
Wt::Dbo::collection<Wt::Dbo::ptr<TrackLyrics>> _trackLyrics;
|
||||||
|
|||||||
@@ -25,6 +25,8 @@
|
|||||||
|
|
||||||
#include <Wt/Dbo/Dbo.h>
|
#include <Wt/Dbo/Dbo.h>
|
||||||
|
|
||||||
|
#include "core/EnumSet.hpp"
|
||||||
|
#include "database/ArtistId.hpp"
|
||||||
#include "database/Object.hpp"
|
#include "database/Object.hpp"
|
||||||
#include "database/ReleaseId.hpp"
|
#include "database/ReleaseId.hpp"
|
||||||
#include "database/TrackEmbeddedImageId.hpp"
|
#include "database/TrackEmbeddedImageId.hpp"
|
||||||
@@ -46,10 +48,13 @@ namespace lms::db
|
|||||||
struct FindParameters
|
struct FindParameters
|
||||||
{
|
{
|
||||||
std::optional<Range> range;
|
std::optional<Range> range;
|
||||||
|
ArtistId artist;
|
||||||
|
core::EnumSet<TrackArtistLinkType> trackArtistLinkTypes;
|
||||||
TrackId track;
|
TrackId track;
|
||||||
ReleaseId release;
|
ReleaseId release;
|
||||||
|
std::optional<int> discNumber;
|
||||||
TrackListId trackList;
|
TrackListId trackList;
|
||||||
std::optional<bool> isPreferred;
|
core::EnumSet<ImageType> imageTypes;
|
||||||
TrackEmbeddedImageSortMethod sortMethod{ TrackEmbeddedImageSortMethod::None };
|
TrackEmbeddedImageSortMethod sortMethod{ TrackEmbeddedImageSortMethod::None };
|
||||||
|
|
||||||
FindParameters& setRange(std::optional<Range> _range)
|
FindParameters& setRange(std::optional<Range> _range)
|
||||||
@@ -57,6 +62,13 @@ namespace lms::db
|
|||||||
range = _range;
|
range = _range;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
FindParameters& setArtist(ArtistId _artist, core::EnumSet<TrackArtistLinkType> _trackArtistLinkTypes = {})
|
||||||
|
{
|
||||||
|
artist = _artist;
|
||||||
|
trackArtistLinkTypes = _trackArtistLinkTypes;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
FindParameters& setTrack(TrackId _track)
|
FindParameters& setTrack(TrackId _track)
|
||||||
{
|
{
|
||||||
track = _track;
|
track = _track;
|
||||||
@@ -67,14 +79,19 @@ namespace lms::db
|
|||||||
release = _release;
|
release = _release;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
FindParameters& setDiscNumber(std::optional<int> _discNumber)
|
||||||
|
{
|
||||||
|
discNumber = _discNumber;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
FindParameters& setTrackList(TrackListId _trackList)
|
FindParameters& setTrackList(TrackListId _trackList)
|
||||||
{
|
{
|
||||||
trackList = _trackList;
|
trackList = _trackList;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
FindParameters& setIsPreferred(std::optional<bool> _isPreferred)
|
FindParameters& setImageTypes(core::EnumSet<ImageType> _imageTypes)
|
||||||
{
|
{
|
||||||
isPreferred = _isPreferred;
|
imageTypes = _imageTypes;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
FindParameters& setSortMethod(TrackEmbeddedImageSortMethod _sortMethod)
|
FindParameters& setSortMethod(TrackEmbeddedImageSortMethod _sortMethod)
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
#include <Wt/Dbo/Dbo.h>
|
#include <Wt/Dbo/Dbo.h>
|
||||||
|
|
||||||
#include "database/Object.hpp"
|
#include "database/Object.hpp"
|
||||||
|
#include "database/TrackEmbeddedImageId.hpp"
|
||||||
#include "database/TrackEmbeddedImageLinkId.hpp"
|
#include "database/TrackEmbeddedImageLinkId.hpp"
|
||||||
#include "database/Types.hpp"
|
#include "database/Types.hpp"
|
||||||
|
|
||||||
@@ -44,18 +45,17 @@ namespace lms::db
|
|||||||
// find
|
// find
|
||||||
static std::size_t getCount(Session& session);
|
static std::size_t getCount(Session& session);
|
||||||
static pointer find(Session& session, TrackEmbeddedImageLinkId id);
|
static pointer find(Session& session, TrackEmbeddedImageLinkId id);
|
||||||
|
static void find(Session& session, TrackEmbeddedImageId trackEmbeddedImageId, std::function<void(const pointer&)> visitor);
|
||||||
|
|
||||||
// getters
|
// getters
|
||||||
ObjectPtr<Track> getTrack() const;
|
ObjectPtr<Track> getTrack() const;
|
||||||
ObjectPtr<TrackEmbeddedImage> getImage() const;
|
ObjectPtr<TrackEmbeddedImage> getImage() const;
|
||||||
std::size_t getIndex() const { return _index; }
|
std::size_t getIndex() const { return _index; }
|
||||||
bool isPreferred() const { return _isPreferred; }
|
|
||||||
ImageType getType() const { return _type; }
|
ImageType getType() const { return _type; }
|
||||||
std::string_view getDescription() const { return _description; }
|
std::string_view getDescription() const { return _description; }
|
||||||
|
|
||||||
// setters
|
// setters
|
||||||
void setIndex(std::size_t index) { _index = static_cast<int>(index); }
|
void setIndex(std::size_t index) { _index = static_cast<int>(index); }
|
||||||
void setIsPreferred(bool isPreferred) { _isPreferred = isPreferred; }
|
|
||||||
void setType(ImageType type) { _type = type; }
|
void setType(ImageType type) { _type = type; }
|
||||||
void setDescription(std::string_view description) { _description = description; }
|
void setDescription(std::string_view description) { _description = description; }
|
||||||
|
|
||||||
@@ -63,7 +63,6 @@ namespace lms::db
|
|||||||
void persist(Action& a)
|
void persist(Action& a)
|
||||||
{
|
{
|
||||||
Wt::Dbo::field(a, _index, "index");
|
Wt::Dbo::field(a, _index, "index");
|
||||||
Wt::Dbo::field(a, _isPreferred, "is_preferred");
|
|
||||||
Wt::Dbo::field(a, _type, "type");
|
Wt::Dbo::field(a, _type, "type");
|
||||||
Wt::Dbo::field(a, _description, "description");
|
Wt::Dbo::field(a, _description, "description");
|
||||||
|
|
||||||
@@ -78,7 +77,6 @@ namespace lms::db
|
|||||||
static pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<TrackEmbeddedImage> image);
|
static pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<TrackEmbeddedImage> image);
|
||||||
|
|
||||||
int _index{}; // index within the track
|
int _index{}; // index within the track
|
||||||
bool _isPreferred{};
|
|
||||||
ImageType _type{ ImageType::Unknown };
|
ImageType _type{ ImageType::Unknown };
|
||||||
std::string _description;
|
std::string _description;
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
@@ -171,7 +172,10 @@ namespace lms::db
|
|||||||
enum class TrackEmbeddedImageSortMethod
|
enum class TrackEmbeddedImageSortMethod
|
||||||
{
|
{
|
||||||
None,
|
None,
|
||||||
FrontCoverAndSize,
|
SizeDesc,
|
||||||
|
TrackNumberThenSizeDesc,
|
||||||
|
DiscNumberThenTrackNumberThenSizeDesc,
|
||||||
|
TrackListIndexAscThenSizeDesc,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class TrackListSortMethod
|
enum class TrackListSortMethod
|
||||||
|
|||||||
@@ -19,10 +19,12 @@
|
|||||||
|
|
||||||
#include "Common.hpp"
|
#include "Common.hpp"
|
||||||
|
|
||||||
|
#include "database/Artwork.hpp"
|
||||||
#include "database/Image.hpp"
|
#include "database/Image.hpp"
|
||||||
|
|
||||||
namespace lms::db::tests
|
namespace lms::db::tests
|
||||||
{
|
{
|
||||||
|
using ScopedArtwork = ScopedEntity<db::Artwork>;
|
||||||
using ScopedImage = ScopedEntity<db::Image>;
|
using ScopedImage = ScopedEntity<db::Image>;
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, Artist)
|
TEST_F(DatabaseFixture, Artist)
|
||||||
@@ -711,27 +713,28 @@ namespace lms::db::tests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, Artist_image)
|
TEST_F(DatabaseFixture, Artist_artwork)
|
||||||
{
|
{
|
||||||
ScopedArtist release{ session, "MyArtist" };
|
ScopedImage image{ session, "/image1.jpg" };
|
||||||
|
ScopedArtwork artwork{ session, image.lockAndGet() };
|
||||||
|
|
||||||
|
ScopedArtist artist{ session, "MyArtist" };
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction{ session.createReadTransaction() };
|
auto transaction{ session.createReadTransaction() };
|
||||||
EXPECT_FALSE(release.get()->getImage());
|
EXPECT_FALSE(artist.get()->getPreferredArtwork());
|
||||||
}
|
}
|
||||||
|
|
||||||
ScopedImage image{ session, "/myImage" };
|
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction{ session.createWriteTransaction() };
|
auto transaction{ session.createWriteTransaction() };
|
||||||
release.get().modify()->setImage(image.get());
|
artist.get().modify()->setPreferredArtwork(artwork.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction{ session.createReadTransaction() };
|
auto transaction{ session.createReadTransaction() };
|
||||||
auto artistImage(release.get()->getImage());
|
auto artistArtwork(artist.get()->getPreferredArtwork());
|
||||||
ASSERT_TRUE(artistImage);
|
ASSERT_TRUE(artistArtwork);
|
||||||
EXPECT_EQ(artistImage->getId(), image.getId());
|
EXPECT_EQ(artistArtwork->getId(), artwork.getId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025of 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 "Common.hpp"
|
||||||
|
|
||||||
|
#include "database/Artwork.hpp"
|
||||||
|
#include "database/Image.hpp"
|
||||||
|
#include "database/TrackEmbeddedImage.hpp"
|
||||||
|
#include "database/TrackEmbeddedImageLink.hpp"
|
||||||
|
|
||||||
|
namespace lms::db::tests
|
||||||
|
{
|
||||||
|
using ScopedArtwork = ScopedEntity<db::Artwork>;
|
||||||
|
using ScopedImage = ScopedEntity<db::Image>;
|
||||||
|
using ScopedTrackEmbeddedImage = ScopedEntity<db::TrackEmbeddedImage>;
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Artwork_image)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
EXPECT_EQ(Artwork::getCount(session), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
ScopedImage image{ session, "/MyImage" };
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
EXPECT_EQ(artwork.get()->getLastWrittenTime(), dateTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Artwork_trackEmbeddedImage)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
EXPECT_EQ(Artwork::getCount(session), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
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<db::TrackEmbeddedImageLink>(track.get(), image.get());
|
||||||
|
track.get().modify()->setLastWriteTime(dateTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
EXPECT_EQ(artwork.get()->getLastWrittenTime(), dateTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace lms::db::tests
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
add_executable(test-database
|
add_executable(test-database
|
||||||
Artist.cpp
|
Artist.cpp
|
||||||
ArtistInfo.cpp
|
ArtistInfo.cpp
|
||||||
|
Artwork.cpp
|
||||||
AuthToken.cpp
|
AuthToken.cpp
|
||||||
Cluster.cpp
|
Cluster.cpp
|
||||||
Common.cpp
|
Common.cpp
|
||||||
|
|||||||
@@ -19,23 +19,10 @@
|
|||||||
|
|
||||||
#include "Common.hpp"
|
#include "Common.hpp"
|
||||||
|
|
||||||
#include "database/Artist.hpp"
|
|
||||||
#include "database/Cluster.hpp"
|
|
||||||
#include "database/Db.hpp"
|
#include "database/Db.hpp"
|
||||||
#include "database/Image.hpp"
|
|
||||||
#include "database/Listen.hpp"
|
|
||||||
#include "database/MediaLibrary.hpp"
|
|
||||||
#include "database/Release.hpp"
|
#include "database/Release.hpp"
|
||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
#include "database/StarredArtist.hpp"
|
|
||||||
#include "database/StarredRelease.hpp"
|
|
||||||
#include "database/StarredTrack.hpp"
|
|
||||||
#include "database/Track.hpp"
|
|
||||||
#include "database/TrackArtistLink.hpp"
|
|
||||||
#include "database/TrackBookmark.hpp"
|
|
||||||
#include "database/TrackList.hpp"
|
|
||||||
#include "database/Types.hpp"
|
#include "database/Types.hpp"
|
||||||
#include "database/User.hpp"
|
|
||||||
|
|
||||||
namespace lms::db::tests
|
namespace lms::db::tests
|
||||||
{
|
{
|
||||||
@@ -75,24 +62,9 @@ namespace lms::db::tests
|
|||||||
{
|
{
|
||||||
using namespace db;
|
using namespace db;
|
||||||
|
|
||||||
auto transaction{ session.createWriteTransaction() };
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
EXPECT_EQ(Artist::getCount(session), 0);
|
EXPECT_TRUE(session.areAllTablesEmpty());
|
||||||
EXPECT_EQ(Cluster::getCount(session), 0);
|
|
||||||
EXPECT_EQ(ClusterType::getCount(session), 0);
|
|
||||||
EXPECT_EQ(Label::getCount(session), 0);
|
|
||||||
EXPECT_EQ(Listen::getCount(session), 0);
|
|
||||||
EXPECT_EQ(Image::getCount(session), 0);
|
|
||||||
EXPECT_EQ(MediaLibrary::getCount(session), 0);
|
|
||||||
EXPECT_EQ(Release::getCount(session), 0);
|
|
||||||
EXPECT_EQ(ReleaseType::getCount(session), 0);
|
|
||||||
EXPECT_EQ(StarredArtist::getCount(session), 0);
|
|
||||||
EXPECT_EQ(StarredRelease::getCount(session), 0);
|
|
||||||
EXPECT_EQ(StarredTrack::getCount(session), 0);
|
|
||||||
EXPECT_EQ(Track::getCount(session), 0);
|
|
||||||
EXPECT_EQ(TrackBookmark::getCount(session), 0);
|
|
||||||
EXPECT_EQ(TrackList::getCount(session), 0);
|
|
||||||
EXPECT_EQ(User::getCount(session), 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, vacuum)
|
TEST_F(DatabaseFixture, vacuum)
|
||||||
|
|||||||
@@ -20,10 +20,12 @@
|
|||||||
#include "Common.hpp"
|
#include "Common.hpp"
|
||||||
|
|
||||||
#include "core/PartialDateTime.hpp"
|
#include "core/PartialDateTime.hpp"
|
||||||
|
#include "database/Artwork.hpp"
|
||||||
#include "database/Image.hpp"
|
#include "database/Image.hpp"
|
||||||
|
|
||||||
namespace lms::db::tests
|
namespace lms::db::tests
|
||||||
{
|
{
|
||||||
|
using ScopedArtwork = ScopedEntity<db::Artwork>;
|
||||||
using ScopedImage = ScopedEntity<db::Image>;
|
using ScopedImage = ScopedEntity<db::Image>;
|
||||||
using ScopedLabel = ScopedEntity<db::Label>;
|
using ScopedLabel = ScopedEntity<db::Label>;
|
||||||
using ScopedCountry = ScopedEntity<db::Country>;
|
using ScopedCountry = ScopedEntity<db::Country>;
|
||||||
@@ -1195,27 +1197,40 @@ namespace lms::db::tests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, Release_image)
|
TEST_F(DatabaseFixture, Release_artwork)
|
||||||
{
|
{
|
||||||
ScopedRelease release{ session, "MyRelease" };
|
ScopedRelease release{ session, "MyRelease" };
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction{ session.createReadTransaction() };
|
auto transaction{ session.createReadTransaction() };
|
||||||
EXPECT_FALSE(release.get()->getImage());
|
EXPECT_FALSE(release.get()->getPreferredArtwork());
|
||||||
}
|
}
|
||||||
|
|
||||||
ScopedImage image{ session, "/myImage" };
|
ScopedImage image{ session, "/image.jpg" };
|
||||||
|
ScopedArtwork artwork{ session, image.lockAndGet() };
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction{ session.createWriteTransaction() };
|
auto transaction{ session.createWriteTransaction() };
|
||||||
release.get().modify()->setImage(image.get());
|
release.get().modify()->setPreferredArtwork(artwork.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction{ session.createReadTransaction() };
|
auto transaction{ session.createReadTransaction() };
|
||||||
auto releaseImage(release.get()->getImage());
|
auto releaseArtwork(release.get()->getPreferredArtwork());
|
||||||
ASSERT_TRUE(releaseImage);
|
ASSERT_TRUE(releaseArtwork);
|
||||||
EXPECT_EQ(releaseImage->getId(), image.getId());
|
EXPECT_EQ(releaseArtwork->getId(), artwork.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check cascade delete
|
||||||
|
{
|
||||||
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
image.lockAndGet().remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
auto releaseArtwork(release.get()->getPreferredArtwork());
|
||||||
|
ASSERT_FALSE(releaseArtwork);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,8 +21,14 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include "database/Artwork.hpp"
|
||||||
|
#include "database/Image.hpp"
|
||||||
|
|
||||||
namespace lms::db::tests
|
namespace lms::db::tests
|
||||||
{
|
{
|
||||||
|
using ScopedArtwork = ScopedEntity<db::Artwork>;
|
||||||
|
using ScopedImage = ScopedEntity<db::Image>;
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, Track)
|
TEST_F(DatabaseFixture, Track)
|
||||||
{
|
{
|
||||||
{
|
{
|
||||||
@@ -439,4 +445,41 @@ namespace lms::db::tests
|
|||||||
EXPECT_EQ(tracks.results[3], track3.getId());
|
EXPECT_EQ(tracks.results[3], track3.getId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Track_updateArtworks)
|
||||||
|
{
|
||||||
|
ScopedTrack track{ session };
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
EXPECT_EQ(track->getPreferredArtwork(), Artwork::pointer{});
|
||||||
|
EXPECT_EQ(track->getPreferredMediaArtwork(), Artwork::pointer{});
|
||||||
|
}
|
||||||
|
|
||||||
|
ScopedImage image1{ session, "/image1.jpg" };
|
||||||
|
ScopedArtwork artwork1{ session, image1.lockAndGet() };
|
||||||
|
ScopedImage image2{ session, "/image2.jpg" };
|
||||||
|
ScopedArtwork artwork2{ session, image2.lockAndGet() };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
Track::updatePreferredArtwork(session, track->getId(), artwork1->getId());
|
||||||
|
Track::updatePreferredMediaArtwork(session, track->getId(), artwork2->getId());
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
EXPECT_EQ(track->getPreferredArtwork()->getId(), artwork1->getId());
|
||||||
|
EXPECT_EQ(track->getPreferredMediaArtwork()->getId(), artwork2->getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
Track::updatePreferredArtwork(session, track->getId(), ArtworkId{});
|
||||||
|
Track::updatePreferredMediaArtwork(session, track->getId(), ArtworkId{});
|
||||||
|
}
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
EXPECT_EQ(track->getPreferredArtwork(), Artwork::pointer{});
|
||||||
|
EXPECT_EQ(track->getPreferredMediaArtwork(), Artwork::pointer{});
|
||||||
|
}
|
||||||
|
}
|
||||||
} // namespace lms::db::tests
|
} // namespace lms::db::tests
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
|
|
||||||
#include "database/TrackEmbeddedImage.hpp"
|
#include "database/TrackEmbeddedImage.hpp"
|
||||||
#include "database/TrackEmbeddedImageLink.hpp"
|
#include "database/TrackEmbeddedImageLink.hpp"
|
||||||
|
#include "database/TrackList.hpp"
|
||||||
#include "database/Types.hpp"
|
#include "database/Types.hpp"
|
||||||
|
|
||||||
#include "Common.hpp"
|
#include "Common.hpp"
|
||||||
@@ -118,23 +119,6 @@ namespace lms::db::tests
|
|||||||
auto transaction{ session.createReadTransaction() };
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
TrackEmbeddedImage::FindParameters params;
|
TrackEmbeddedImage::FindParameters params;
|
||||||
params.setIsPreferred(true);
|
|
||||||
|
|
||||||
bool visited{};
|
|
||||||
TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; });
|
|
||||||
EXPECT_FALSE(visited);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
auto transaction{ session.createWriteTransaction() };
|
|
||||||
link.get().modify()->setIsPreferred(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
auto transaction{ session.createReadTransaction() };
|
|
||||||
|
|
||||||
TrackEmbeddedImage::FindParameters params;
|
|
||||||
params.setIsPreferred(true);
|
|
||||||
|
|
||||||
bool visited{};
|
bool visited{};
|
||||||
TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; });
|
TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; });
|
||||||
@@ -142,15 +126,8 @@ namespace lms::db::tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction{ session.createReadTransaction() };
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
link.get().modify()->setType(ImageType::FrontCover);
|
||||||
TrackEmbeddedImage::FindParameters params;
|
|
||||||
params.setIsPreferred(true);
|
|
||||||
params.setRelease(release.getId());
|
|
||||||
|
|
||||||
bool visited{};
|
|
||||||
TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; });
|
|
||||||
EXPECT_FALSE(visited);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -162,9 +139,8 @@ namespace lms::db::tests
|
|||||||
auto transaction{ session.createReadTransaction() };
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
TrackEmbeddedImage::FindParameters params;
|
TrackEmbeddedImage::FindParameters params;
|
||||||
params.setIsPreferred(true);
|
|
||||||
params.setRelease(release.getId());
|
params.setRelease(release.getId());
|
||||||
params.setSortMethod(TrackEmbeddedImageSortMethod::FrontCoverAndSize);
|
params.setSortMethod(TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc);
|
||||||
|
|
||||||
bool visited{};
|
bool visited{};
|
||||||
TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; });
|
TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; });
|
||||||
@@ -175,9 +151,8 @@ namespace lms::db::tests
|
|||||||
auto transaction{ session.createReadTransaction() };
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
TrackEmbeddedImage::FindParameters params;
|
TrackEmbeddedImage::FindParameters params;
|
||||||
params.setIsPreferred(true);
|
|
||||||
params.setTrack(track.getId());
|
params.setTrack(track.getId());
|
||||||
params.setSortMethod(TrackEmbeddedImageSortMethod::FrontCoverAndSize);
|
params.setSortMethod(TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc);
|
||||||
|
|
||||||
bool visited{};
|
bool visited{};
|
||||||
TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; });
|
TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; });
|
||||||
@@ -185,6 +160,159 @@ namespace lms::db::tests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, TrackEmbeddedImage_findByParams_sorts)
|
||||||
|
{
|
||||||
|
ScopedTrackEmbeddedImage image1{ session };
|
||||||
|
ScopedTrackEmbeddedImage image2{ session };
|
||||||
|
ScopedTrackEmbeddedImage image3{ session };
|
||||||
|
ScopedTrackEmbeddedImage image4{ session };
|
||||||
|
ScopedTrack track1{ session };
|
||||||
|
ScopedTrack track2{ session };
|
||||||
|
ScopedRelease release{ session, "MyRelease" };
|
||||||
|
ScopedTrackEmbeddedImageLink link1{ session, track1.lockAndGet(), image1.lockAndGet() };
|
||||||
|
ScopedTrackEmbeddedImageLink link2{ session, track1.lockAndGet(), image2.lockAndGet() };
|
||||||
|
ScopedTrackEmbeddedImageLink link3{ session, track1.lockAndGet(), image3.lockAndGet() };
|
||||||
|
ScopedTrackEmbeddedImageLink link4{ session, track2.lockAndGet(), image4.lockAndGet() };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
track1.get().modify()->setRelease(release.get());
|
||||||
|
track1.get().modify()->setTrackNumber(2);
|
||||||
|
|
||||||
|
link1.get().modify()->setType(ImageType::FrontCover);
|
||||||
|
image1.get().modify()->setSize(750);
|
||||||
|
link2.get().modify()->setType(ImageType::Media);
|
||||||
|
image2.get().modify()->setSize(1000);
|
||||||
|
link3.get().modify()->setType(ImageType::Media);
|
||||||
|
image3.get().modify()->setSize(2000);
|
||||||
|
|
||||||
|
track2.get().modify()->setRelease(release.get());
|
||||||
|
track2.get().modify()->setTrackNumber(1);
|
||||||
|
|
||||||
|
link4.get().modify()->setType(ImageType::Media);
|
||||||
|
image4.get().modify()->setSize(1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
|
TrackEmbeddedImage::FindParameters params;
|
||||||
|
params.setRelease(release.getId());
|
||||||
|
params.setImageTypes({ ImageType::Media });
|
||||||
|
params.setSortMethod(TrackEmbeddedImageSortMethod::SizeDesc);
|
||||||
|
|
||||||
|
std::vector<TrackEmbeddedImageId> visitedIds;
|
||||||
|
TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer& image) { visitedIds.push_back(image->getId()); });
|
||||||
|
ASSERT_EQ(visitedIds.size(), 3);
|
||||||
|
EXPECT_EQ(visitedIds[0], image3.getId());
|
||||||
|
EXPECT_EQ(visitedIds[1], image4.getId());
|
||||||
|
EXPECT_EQ(visitedIds[2], image2.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
|
TrackEmbeddedImage::FindParameters params;
|
||||||
|
params.setRelease(release.getId());
|
||||||
|
params.setImageTypes({ ImageType::Media });
|
||||||
|
params.setSortMethod(TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc);
|
||||||
|
|
||||||
|
std::vector<TrackEmbeddedImageId> visitedIds;
|
||||||
|
TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer& image) { visitedIds.push_back(image->getId()); });
|
||||||
|
ASSERT_EQ(visitedIds.size(), 3);
|
||||||
|
EXPECT_EQ(visitedIds[0], image4.getId());
|
||||||
|
EXPECT_EQ(visitedIds[1], image3.getId());
|
||||||
|
EXPECT_EQ(visitedIds[2], image2.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
|
TrackEmbeddedImage::FindParameters params;
|
||||||
|
params.setRelease(release.getId());
|
||||||
|
params.setImageTypes({ ImageType::Media });
|
||||||
|
params.setSortMethod(TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc);
|
||||||
|
|
||||||
|
std::vector<TrackEmbeddedImageId> visitedIds;
|
||||||
|
TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer& image) { visitedIds.push_back(image->getId()); });
|
||||||
|
ASSERT_EQ(visitedIds.size(), 3);
|
||||||
|
EXPECT_EQ(visitedIds[0], image4.getId());
|
||||||
|
EXPECT_EQ(visitedIds[1], image3.getId());
|
||||||
|
EXPECT_EQ(visitedIds[2], image2.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, TrackEmbeddedImage_findByParams_artist)
|
||||||
|
{
|
||||||
|
ScopedTrackEmbeddedImage image{ session };
|
||||||
|
ScopedTrack track{ session };
|
||||||
|
ScopedArtist artist{ session, "MyArtist" };
|
||||||
|
ScopedTrackEmbeddedImageLink link{ session, track.lockAndGet(), image.lockAndGet() };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
|
TrackEmbeddedImage::FindParameters params;
|
||||||
|
params.setArtist(artist.getId());
|
||||||
|
|
||||||
|
bool visited{};
|
||||||
|
TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer&) { visited = true; });
|
||||||
|
ASSERT_TRUE(visited);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
|
TrackEmbeddedImage::FindParameters params;
|
||||||
|
params.setArtist(artist.getId(), { TrackArtistLinkType::ReleaseArtist });
|
||||||
|
|
||||||
|
bool visited{};
|
||||||
|
TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer&) { visited = true; });
|
||||||
|
ASSERT_FALSE(visited);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, TrackEmbeddedImage_findByParams_discNumber)
|
||||||
|
{
|
||||||
|
ScopedTrackEmbeddedImage image{ session };
|
||||||
|
ScopedTrack track{ session };
|
||||||
|
ScopedRelease release{ session, "MyRelease" };
|
||||||
|
ScopedTrackEmbeddedImageLink link{ session, track.lockAndGet(), image.lockAndGet() };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
track.get().modify()->setRelease(release.get());
|
||||||
|
track.get().modify()->setDiscNumber(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
|
TrackEmbeddedImage::FindParameters params;
|
||||||
|
params.setDiscNumber(1);
|
||||||
|
|
||||||
|
bool visited{};
|
||||||
|
TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer&) { visited = true; });
|
||||||
|
ASSERT_TRUE(visited);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
|
TrackEmbeddedImage::FindParameters params;
|
||||||
|
params.setDiscNumber(2);
|
||||||
|
|
||||||
|
bool visited{};
|
||||||
|
TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer&) { visited = true; });
|
||||||
|
ASSERT_FALSE(visited);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, Track_findByEmbeddedImage)
|
TEST_F(DatabaseFixture, Track_findByEmbeddedImage)
|
||||||
{
|
{
|
||||||
ScopedTrackEmbeddedImage image{ session };
|
ScopedTrackEmbeddedImage image{ session };
|
||||||
@@ -279,5 +407,54 @@ namespace lms::db::tests
|
|||||||
EXPECT_EQ(img->getType(), ImageType::FrontCover);
|
EXPECT_EQ(img->getType(), ImageType::FrontCover);
|
||||||
EXPECT_EQ(img->getDescription(), "MyDesc");
|
EXPECT_EQ(img->getDescription(), "MyDesc");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
|
bool visited{};
|
||||||
|
TrackEmbeddedImageLink::find(session, image->getId(), [&](const TrackEmbeddedImageLink::pointer& link) {
|
||||||
|
EXPECT_EQ(link->getIndex(), 2);
|
||||||
|
EXPECT_EQ(link->getType(), ImageType::FrontCover);
|
||||||
|
EXPECT_EQ(link->getDescription(), "MyDesc");
|
||||||
|
EXPECT_EQ(link->getTrack(), track.get());
|
||||||
|
|
||||||
|
visited = true;
|
||||||
|
});
|
||||||
|
EXPECT_TRUE(visited);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, TrackEmbeddedImage_TrackList)
|
||||||
|
{
|
||||||
|
ScopedTrackList trackList{ session, "MytrackList", TrackListType::PlayList };
|
||||||
|
|
||||||
|
ScopedTrack track1{ session };
|
||||||
|
ScopedTrack track2{ session };
|
||||||
|
ScopedTrackEmbeddedImage image1{ session };
|
||||||
|
ScopedTrackEmbeddedImageLink imageLink1{ session, track1.lockAndGet(), image1.lockAndGet() };
|
||||||
|
ScopedTrackEmbeddedImage image2{ session };
|
||||||
|
ScopedTrackEmbeddedImageLink imageLink2{ session, track2.lockAndGet(), image2.lockAndGet() };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
session.create<TrackListEntry>(track2.get(), trackList.get());
|
||||||
|
session.create<TrackListEntry>(track1.get(), trackList.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
|
TrackEmbeddedImage::FindParameters params;
|
||||||
|
params.setTrackList(trackList.getId());
|
||||||
|
params.setSortMethod(TrackEmbeddedImageSortMethod::TrackListIndexAscThenSizeDesc);
|
||||||
|
|
||||||
|
std::vector<TrackEmbeddedImageId> visitedIds;
|
||||||
|
TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer& image) {
|
||||||
|
visitedIds.push_back(image->getId());
|
||||||
|
});
|
||||||
|
ASSERT_EQ(visitedIds.size(), 2);
|
||||||
|
EXPECT_EQ(visitedIds[0], image2.getId());
|
||||||
|
EXPECT_EQ(visitedIds[1], image1.getId());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} // namespace lms::db::tests
|
} // namespace lms::db::tests
|
||||||
@@ -19,31 +19,27 @@
|
|||||||
|
|
||||||
#include "ArtworkService.hpp"
|
#include "ArtworkService.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <functional>
|
|
||||||
|
|
||||||
#include "core/IConfig.hpp"
|
#include "core/IConfig.hpp"
|
||||||
#include "core/ILogger.hpp"
|
#include "core/ILogger.hpp"
|
||||||
#include "core/Utils.hpp"
|
#include "core/Utils.hpp"
|
||||||
|
#include "database/Artist.hpp"
|
||||||
|
#include "database/Artwork.hpp"
|
||||||
#include "database/Db.hpp"
|
#include "database/Db.hpp"
|
||||||
#include "database/Image.hpp"
|
#include "database/Image.hpp"
|
||||||
|
#include "database/ImageId.hpp"
|
||||||
|
#include "database/Release.hpp"
|
||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
#include "database/Track.hpp"
|
#include "database/Track.hpp"
|
||||||
|
#include "database/TrackEmbeddedImage.hpp"
|
||||||
|
#include "database/TrackEmbeddedImageLink.hpp"
|
||||||
|
#include "database/TrackList.hpp"
|
||||||
#include "image/Exception.hpp"
|
#include "image/Exception.hpp"
|
||||||
#include "image/IEncodedImage.hpp"
|
#include "image/IEncodedImage.hpp"
|
||||||
#include "image/Image.hpp"
|
#include "image/Image.hpp"
|
||||||
#include "metadata/IAudioFileParser.hpp"
|
#include "metadata/IAudioFileParser.hpp"
|
||||||
|
|
||||||
namespace lms::cover
|
namespace lms::artwork
|
||||||
{
|
{
|
||||||
namespace
|
|
||||||
{
|
|
||||||
bool isFileSupported(const std::filesystem::path& file, const std::vector<std::filesystem::path>& extensions)
|
|
||||||
{
|
|
||||||
return (std::find(std::cbegin(extensions), std::cend(extensions), file.extension()) != std::cend(extensions));
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
std::unique_ptr<IArtworkService> createArtworkService(db::Db& db, const std::filesystem::path& defaultReleaseCoverSvgPath, const std::filesystem::path& defaultArtistImageSvgPath)
|
std::unique_ptr<IArtworkService> createArtworkService(db::Db& db, const std::filesystem::path& defaultReleaseCoverSvgPath, const std::filesystem::path& defaultArtistImageSvgPath)
|
||||||
{
|
{
|
||||||
return std::make_unique<ArtworkService>(db, defaultReleaseCoverSvgPath, defaultArtistImageSvgPath);
|
return std::make_unique<ArtworkService>(db, defaultReleaseCoverSvgPath, defaultArtistImageSvgPath);
|
||||||
@@ -92,59 +88,28 @@ namespace lms::cover
|
|||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::shared_ptr<image::IEncodedImage> ArtworkService::getDefaultReleaseCover()
|
std::shared_ptr<image::IEncodedImage> ArtworkService::getDefaultReleaseArtwork()
|
||||||
{
|
{
|
||||||
return _defaultReleaseCover;
|
return _defaultReleaseCover;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::shared_ptr<image::IEncodedImage> ArtworkService::getDefaultArtistImage()
|
std::shared_ptr<image::IEncodedImage> ArtworkService::getDefaultArtistArtwork()
|
||||||
{
|
{
|
||||||
return _defaultArtistImage;
|
return _defaultArtistImage;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ArtworkService::checkImageFile(const std::filesystem::path& filePath)
|
std::unique_ptr<image::IEncodedImage> ArtworkService::getTrackImage(const std::filesystem::path& p, std::size_t index, std::optional<image::ImageSize> width) const
|
||||||
{
|
{
|
||||||
std::error_code ec;
|
std::unique_ptr<image::IEncodedImage> image;
|
||||||
|
|
||||||
if (!isFileSupported(filePath, _fileExtensions))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (!std::filesystem::exists(filePath, ec))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (!std::filesystem::is_regular_file(filePath, ec))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::unique_ptr<image::IEncodedImage> ArtworkService::getTrackImage(const std::filesystem::path& p, std::optional<image::ImageSize> width) const
|
|
||||||
{
|
|
||||||
struct CandidateImage
|
|
||||||
{
|
|
||||||
std::unique_ptr<image::IEncodedImage> image;
|
|
||||||
bool isFront{};
|
|
||||||
std::size_t index;
|
|
||||||
|
|
||||||
// > means is better candidate
|
|
||||||
bool operator>(const CandidateImage& other) const
|
|
||||||
{
|
|
||||||
if (!isFront && other.isFront)
|
|
||||||
return false;
|
|
||||||
if (isFront && !other.isFront)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
return index < other.index;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
std::vector<CandidateImage> candidateImages;
|
|
||||||
std::size_t pictureIndex{};
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
std::size_t currentIndex{};
|
||||||
|
|
||||||
_audioFileParser->parseImages(p, [&](const metadata::Image& parsedImage) {
|
_audioFileParser->parseImages(p, [&](const metadata::Image& parsedImage) {
|
||||||
std::unique_ptr<image::IEncodedImage> image;
|
if (currentIndex++ != index)
|
||||||
|
return;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!width)
|
if (!width)
|
||||||
@@ -162,8 +127,6 @@ namespace lms::cover
|
|||||||
{
|
{
|
||||||
LMS_LOG(COVER, ERROR, "Cannot decode image from track " << p << ": " << e.what());
|
LMS_LOG(COVER, ERROR, "Cannot decode image from track " << p << ": " << e.what());
|
||||||
}
|
}
|
||||||
|
|
||||||
candidateImages.emplace_back(CandidateImage{ .image = std::move(image), .isFront = parsedImage.type == metadata::Image::Type::FrontCover, .index = pictureIndex++ });
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (const metadata::Exception& e)
|
catch (const metadata::Exception& e)
|
||||||
@@ -171,63 +134,108 @@ namespace lms::cover
|
|||||||
LMS_LOG(COVER, ERROR, "Cannot parse images from track " << p << ": " << e.what());
|
LMS_LOG(COVER, ERROR, "Cannot parse images from track " << p << ": " << e.what());
|
||||||
}
|
}
|
||||||
|
|
||||||
std::stable_sort(std::begin(candidateImages), std::end(candidateImages), std::greater<>());
|
return image;
|
||||||
if (!candidateImages.empty())
|
}
|
||||||
return std::move(candidateImages.front().image);
|
|
||||||
|
|
||||||
return {};
|
db::ArtworkId ArtworkService::findTrackListImage(db::TrackListId trackListId)
|
||||||
|
{
|
||||||
|
db::ArtworkId artworkId;
|
||||||
|
|
||||||
|
// Iterate over all tracks and stop when we find an artwork
|
||||||
|
db::Session& session{ _db.getTLSSession() };
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
|
db::TrackList::pointer trackList{ db::TrackList::find(session, trackListId) };
|
||||||
|
if (!trackList)
|
||||||
|
return artworkId;
|
||||||
|
|
||||||
|
const auto entries{ trackList->getEntries(db::Range{ 0, 10 }) };
|
||||||
|
for (const auto& entry : entries.results)
|
||||||
|
{
|
||||||
|
const auto track{ entry->getTrack() };
|
||||||
|
if (track->getPreferredMediaArtworkId().isValid())
|
||||||
|
{
|
||||||
|
artworkId = track->getPreferredMediaArtworkId();
|
||||||
|
break; // stop iteration
|
||||||
|
}
|
||||||
|
|
||||||
|
if (track->getPreferredArtworkId().isValid())
|
||||||
|
{
|
||||||
|
artworkId = track->getPreferredArtworkId();
|
||||||
|
break; // stop iteration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return artworkId;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::shared_ptr<image::IEncodedImage> ArtworkService::getImage(db::ArtworkId artworkId, std::optional<image::ImageSize> width)
|
||||||
|
{
|
||||||
|
const ImageCache::EntryDesc cacheEntryDesc{ artworkId, width };
|
||||||
|
|
||||||
|
std::shared_ptr<image::IEncodedImage> image{ _cache.getImage(cacheEntryDesc) };
|
||||||
|
if (image)
|
||||||
|
return image;
|
||||||
|
|
||||||
|
db::TrackEmbeddedImageId trackEmbeddedImageId;
|
||||||
|
db::ImageId imageId;
|
||||||
|
|
||||||
|
{
|
||||||
|
db::Session& session{ _db.getTLSSession() };
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
|
db::Artwork::pointer artwork{ db::Artwork::find(session, artworkId) };
|
||||||
|
if (artwork)
|
||||||
|
{
|
||||||
|
trackEmbeddedImageId = artwork->getTrackEmbeddedImageId();
|
||||||
|
imageId = artwork->getImageId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trackEmbeddedImageId.isValid())
|
||||||
|
image = getTrackEmbeddedImage(trackEmbeddedImageId, width);
|
||||||
|
else if (imageId.isValid())
|
||||||
|
image = getImage(imageId, width);
|
||||||
|
|
||||||
|
if (image)
|
||||||
|
_cache.addImage(cacheEntryDesc, image);
|
||||||
|
|
||||||
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::shared_ptr<image::IEncodedImage> ArtworkService::getImage(db::ImageId imageId, std::optional<image::ImageSize> width)
|
std::shared_ptr<image::IEncodedImage> ArtworkService::getImage(db::ImageId imageId, std::optional<image::ImageSize> width)
|
||||||
{
|
{
|
||||||
const ImageCache::EntryDesc cacheEntryDesc{ imageId, width };
|
|
||||||
|
|
||||||
std::shared_ptr<image::IEncodedImage> cover{ _cache.getImage(cacheEntryDesc) };
|
|
||||||
if (cover)
|
|
||||||
return cover;
|
|
||||||
|
|
||||||
std::filesystem::path imageFile;
|
std::filesystem::path imageFile;
|
||||||
{
|
{
|
||||||
db::Session& session{ _db.getTLSSession() };
|
db::Session& session{ _db.getTLSSession() };
|
||||||
auto transaction{ session.createReadTransaction() };
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
const db::Image::pointer image{ db::Image::find(session, imageId) };
|
const db::Image::pointer image{ db::Image::find(session, imageId) };
|
||||||
if (image)
|
if (!image)
|
||||||
imageFile = image->getAbsoluteFilePath();
|
return nullptr;
|
||||||
|
|
||||||
|
imageFile = image->getAbsoluteFilePath();
|
||||||
}
|
}
|
||||||
|
|
||||||
cover = getFromImageFile(imageFile, width);
|
return getFromImageFile(imageFile, width);
|
||||||
if (cover)
|
|
||||||
_cache.addImage(cacheEntryDesc, cover);
|
|
||||||
|
|
||||||
return cover;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::shared_ptr<image::IEncodedImage> ArtworkService::getTrackEmbeddedImage(db::TrackEmbeddedImageId trackEmbeddedImageId, std::optional<image::ImageSize> width)
|
std::shared_ptr<image::IEncodedImage> ArtworkService::getTrackEmbeddedImage(db::TrackEmbeddedImageId trackEmbeddedImageId, std::optional<image::ImageSize> width)
|
||||||
{
|
{
|
||||||
const ImageCache::EntryDesc cacheEntryDesc{ trackEmbeddedImageId, width };
|
std::shared_ptr<image::IEncodedImage> image;
|
||||||
|
|
||||||
std::shared_ptr<image::IEncodedImage> cover{ _cache.getImage(cacheEntryDesc) };
|
|
||||||
if (cover)
|
|
||||||
return cover;
|
|
||||||
|
|
||||||
std::filesystem::path trackFile;
|
|
||||||
{
|
{
|
||||||
db::Session& session{ _db.getTLSSession() };
|
db::Session& session{ _db.getTLSSession() };
|
||||||
auto transaction{ session.createReadTransaction() };
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
db::Track::FindParameters params;
|
// TODO: could be put outside transaction
|
||||||
params.setEmbeddedImage(trackEmbeddedImageId);
|
db::TrackEmbeddedImageLink::find(session, trackEmbeddedImageId, [&](const db::TrackEmbeddedImageLink::pointer& link) {
|
||||||
db::Track::find(session, params, [&](const db::Track::pointer& track) {
|
if (!image)
|
||||||
if (!cover)
|
image = getTrackImage(link->getTrack()->getAbsoluteFilePath(), link->getIndex(), width);
|
||||||
cover = getTrackImage(track->getAbsoluteFilePath(), width);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cover)
|
return image;
|
||||||
_cache.addImage(cacheEntryDesc, cover);
|
|
||||||
|
|
||||||
return cover;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ArtworkService::flushCache()
|
void ArtworkService::flushCache()
|
||||||
@@ -241,5 +249,4 @@ namespace lms::cover
|
|||||||
|
|
||||||
LMS_LOG(COVER, INFO, "JPEG export quality = " << _jpegQuality);
|
LMS_LOG(COVER, INFO, "JPEG export quality = " << _jpegQuality);
|
||||||
}
|
}
|
||||||
|
} // namespace lms::artwork
|
||||||
} // namespace lms::cover
|
|
||||||
|
|||||||
@@ -22,6 +22,8 @@
|
|||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "database/ImageId.hpp"
|
||||||
|
#include "database/TrackEmbeddedImageId.hpp"
|
||||||
#include "services/artwork/IArtworkService.hpp"
|
#include "services/artwork/IArtworkService.hpp"
|
||||||
|
|
||||||
#include "ImageCache.hpp"
|
#include "ImageCache.hpp"
|
||||||
@@ -36,7 +38,7 @@ namespace lms::metadata
|
|||||||
class IAudioFileParser;
|
class IAudioFileParser;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace lms::cover
|
namespace lms::artwork
|
||||||
{
|
{
|
||||||
class ArtworkService : public IArtworkService
|
class ArtworkService : public IArtworkService
|
||||||
{
|
{
|
||||||
@@ -47,19 +49,21 @@ namespace lms::cover
|
|||||||
ArtworkService& operator=(const ArtworkService&) = delete;
|
ArtworkService& operator=(const ArtworkService&) = delete;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::shared_ptr<image::IEncodedImage> getImage(db::ImageId imageId, std::optional<image::ImageSize> width) override;
|
db::ArtworkId findTrackListImage(db::TrackListId trackListId) override;
|
||||||
std::shared_ptr<image::IEncodedImage> getTrackEmbeddedImage(db::TrackEmbeddedImageId trackEmbeddedImageId, std::optional<image::ImageSize> width) override;
|
|
||||||
|
|
||||||
std::shared_ptr<image::IEncodedImage> getDefaultReleaseCover() override;
|
std::shared_ptr<image::IEncodedImage> getImage(db::ArtworkId artworkId, std::optional<image::ImageSize> width) override;
|
||||||
std::shared_ptr<image::IEncodedImage> getDefaultArtistImage() override;
|
|
||||||
|
std::shared_ptr<image::IEncodedImage> getDefaultReleaseArtwork() override;
|
||||||
|
std::shared_ptr<image::IEncodedImage> getDefaultArtistArtwork() override;
|
||||||
|
|
||||||
void flushCache() override;
|
void flushCache() override;
|
||||||
void setJpegQuality(unsigned quality) override;
|
void setJpegQuality(unsigned quality) override;
|
||||||
|
|
||||||
std::unique_ptr<image::IEncodedImage> getFromImageFile(const std::filesystem::path& p, std::optional<image::ImageSize> width) const;
|
std::shared_ptr<image::IEncodedImage> getImage(db::ImageId imageId, std::optional<image::ImageSize> width);
|
||||||
std::unique_ptr<image::IEncodedImage> getTrackImage(const std::filesystem::path& path, std::optional<image::ImageSize> width) const;
|
std::shared_ptr<image::IEncodedImage> getTrackEmbeddedImage(db::TrackEmbeddedImageId trackEmbeddedImageId, std::optional<image::ImageSize> width);
|
||||||
|
|
||||||
static bool checkImageFile(const std::filesystem::path& filePath);
|
std::unique_ptr<image::IEncodedImage> getFromImageFile(const std::filesystem::path& p, std::optional<image::ImageSize> width) const;
|
||||||
|
std::unique_ptr<image::IEncodedImage> getTrackImage(const std::filesystem::path& path, std::size_t index, std::optional<image::ImageSize> width) const;
|
||||||
|
|
||||||
db::Db& _db;
|
db::Db& _db;
|
||||||
|
|
||||||
@@ -72,4 +76,4 @@ namespace lms::cover
|
|||||||
unsigned _jpegQuality;
|
unsigned _jpegQuality;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace lms::cover
|
} // namespace lms::artwork
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
#include "core/ILogger.hpp"
|
#include "core/ILogger.hpp"
|
||||||
#include "core/Random.hpp"
|
#include "core/Random.hpp"
|
||||||
|
|
||||||
namespace lms::cover
|
namespace lms::artwork
|
||||||
{
|
{
|
||||||
ImageCache::ImageCache(std::size_t maxCacheSize)
|
ImageCache::ImageCache(std::size_t maxCacheSize)
|
||||||
: _maxCacheSize{ maxCacheSize }
|
: _maxCacheSize{ maxCacheSize }
|
||||||
@@ -79,4 +79,4 @@ namespace lms::cover
|
|||||||
_cacheSize = 0;
|
_cacheSize = 0;
|
||||||
_cache.clear();
|
_cache.clear();
|
||||||
}
|
}
|
||||||
} // namespace lms::cover
|
} // namespace lms::artwork
|
||||||
@@ -25,13 +25,11 @@
|
|||||||
#include <optional>
|
#include <optional>
|
||||||
#include <shared_mutex>
|
#include <shared_mutex>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <variant>
|
|
||||||
|
|
||||||
#include "database/ImageId.hpp"
|
#include "database/ArtworkId.hpp"
|
||||||
#include "database/TrackEmbeddedImageId.hpp"
|
|
||||||
#include "image/IEncodedImage.hpp"
|
#include "image/IEncodedImage.hpp"
|
||||||
|
|
||||||
namespace lms::cover
|
namespace lms::artwork
|
||||||
{
|
{
|
||||||
class ImageCache
|
class ImageCache
|
||||||
{
|
{
|
||||||
@@ -40,8 +38,7 @@ namespace lms::cover
|
|||||||
|
|
||||||
struct EntryDesc
|
struct EntryDesc
|
||||||
{
|
{
|
||||||
using VariantType = std::variant<db::TrackEmbeddedImageId, db::ImageId>;
|
db::ArtworkId id;
|
||||||
VariantType id;
|
|
||||||
std::optional<std::size_t> size;
|
std::optional<std::size_t> size;
|
||||||
|
|
||||||
bool operator==(const EntryDesc& other) const = default;
|
bool operator==(const EntryDesc& other) const = default;
|
||||||
@@ -63,13 +60,13 @@ namespace lms::cover
|
|||||||
std::size_t operator()(const EntryDesc& entry) const
|
std::size_t operator()(const EntryDesc& entry) const
|
||||||
{
|
{
|
||||||
assert(entry.size); // should not cache unresized images
|
assert(entry.size); // should not cache unresized images
|
||||||
return std::hash<EntryDesc::VariantType>{}(entry.id) ^ std::hash<std::size_t>{}(*entry.size);
|
return std::hash<db::ArtworkId>{}(entry.id) ^ std::hash<std::size_t>{}(*entry.size);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
std::unordered_map<EntryDesc, std::shared_ptr<image::IEncodedImage>, EntryHasher> _cache;
|
std::unordered_map<EntryDesc, std::shared_ptr<image::IEncodedImage>, EntryHasher> _cache;
|
||||||
std::size_t _cacheSize{};
|
std::size_t _cacheSize{};
|
||||||
mutable std::atomic<std::size_t> _cacheMisses{};
|
mutable std::atomic<std::size_t> _cacheMisses;
|
||||||
mutable std::atomic<std::size_t> _cacheHits{};
|
mutable std::atomic<std::size_t> _cacheHits;
|
||||||
};
|
};
|
||||||
} // namespace lms::cover
|
} // namespace lms::artwork
|
||||||
@@ -23,8 +23,8 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
|
||||||
#include "database/ImageId.hpp"
|
#include "database/ArtworkId.hpp"
|
||||||
#include "database/TrackEmbeddedImageId.hpp"
|
#include "database/TrackListId.hpp"
|
||||||
#include "image/IEncodedImage.hpp"
|
#include "image/IEncodedImage.hpp"
|
||||||
|
|
||||||
namespace lms::db
|
namespace lms::db
|
||||||
@@ -32,19 +32,22 @@ namespace lms::db
|
|||||||
class Db;
|
class Db;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace lms::cover
|
namespace lms::artwork
|
||||||
{
|
{
|
||||||
class IArtworkService
|
class IArtworkService
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
virtual ~IArtworkService() = default;
|
virtual ~IArtworkService() = default;
|
||||||
|
|
||||||
virtual std::shared_ptr<image::IEncodedImage> getImage(db::ImageId imageId, std::optional<image::ImageSize> width) = 0;
|
// Helpers to get preferred artworks
|
||||||
virtual std::shared_ptr<image::IEncodedImage> getTrackEmbeddedImage(db::TrackEmbeddedImageId trackEmbeddedImageId, std::optional<image::ImageSize> width) = 0;
|
virtual db::ArtworkId findTrackListImage(db::TrackListId trackListId) = 0;
|
||||||
|
|
||||||
// Svg images dont have image "size"
|
// Image retrieval, no width means original size
|
||||||
virtual std::shared_ptr<image::IEncodedImage> getDefaultReleaseCover() = 0;
|
virtual std::shared_ptr<image::IEncodedImage> getImage(db::ArtworkId artworkId, std::optional<image::ImageSize> width) = 0;
|
||||||
virtual std::shared_ptr<image::IEncodedImage> getDefaultArtistImage() = 0;
|
|
||||||
|
// Svg images don't have image "size"
|
||||||
|
virtual std::shared_ptr<image::IEncodedImage> getDefaultReleaseArtwork() = 0;
|
||||||
|
virtual std::shared_ptr<image::IEncodedImage> getDefaultArtistArtwork() = 0;
|
||||||
|
|
||||||
virtual void flushCache() = 0;
|
virtual void flushCache() = 0;
|
||||||
|
|
||||||
@@ -53,4 +56,4 @@ namespace lms::cover
|
|||||||
|
|
||||||
std::unique_ptr<IArtworkService> createArtworkService(db::Db& db, const std::filesystem::path& defaultReleaseCoverSvgPath, const std::filesystem::path& defaultArtistImageSvgPath);
|
std::unique_ptr<IArtworkService> createArtworkService(db::Db& db, const std::filesystem::path& defaultReleaseCoverSvgPath, const std::filesystem::path& defaultArtistImageSvgPath);
|
||||||
|
|
||||||
} // namespace lms::cover
|
} // namespace lms::artwork
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
|
|
||||||
#include "PlaylistGeneratorService.hpp"
|
#include "PlaylistGeneratorService.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
#include "core/ILogger.hpp"
|
#include "core/ILogger.hpp"
|
||||||
#include "database/Db.hpp"
|
#include "database/Db.hpp"
|
||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ add_library(lmsscanner STATIC
|
|||||||
impl/scanners/LyricsFileScanner.cpp
|
impl/scanners/LyricsFileScanner.cpp
|
||||||
impl/scanners/PlayListFileScanner.cpp
|
impl/scanners/PlayListFileScanner.cpp
|
||||||
impl/scanners/Utils.cpp
|
impl/scanners/Utils.cpp
|
||||||
|
impl/steps/ArtworkUtils.cpp
|
||||||
impl/steps/FileScanQueue.cpp
|
impl/steps/FileScanQueue.cpp
|
||||||
impl/steps/ScanErrorLogger.cpp
|
impl/steps/ScanErrorLogger.cpp
|
||||||
impl/steps/ScanStepArtistReconciliation.cpp
|
impl/steps/ScanStepArtistReconciliation.cpp
|
||||||
@@ -15,6 +16,7 @@ add_library(lmsscanner STATIC
|
|||||||
impl/steps/ScanStepAssociateExternalLyrics.cpp
|
impl/steps/ScanStepAssociateExternalLyrics.cpp
|
||||||
impl/steps/ScanStepAssociatePlayListTracks.cpp
|
impl/steps/ScanStepAssociatePlayListTracks.cpp
|
||||||
impl/steps/ScanStepAssociateReleaseImages.cpp
|
impl/steps/ScanStepAssociateReleaseImages.cpp
|
||||||
|
impl/steps/ScanStepAssociateTrackImages.cpp
|
||||||
impl/steps/ScanStepBase.cpp
|
impl/steps/ScanStepBase.cpp
|
||||||
impl/steps/ScanStepCheckForDuplicatedFiles.cpp
|
impl/steps/ScanStepCheckForDuplicatedFiles.cpp
|
||||||
impl/steps/ScanStepCheckForRemovedFiles.cpp
|
impl/steps/ScanStepCheckForRemovedFiles.cpp
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
#include "steps/ScanStepAssociateExternalLyrics.hpp"
|
#include "steps/ScanStepAssociateExternalLyrics.hpp"
|
||||||
#include "steps/ScanStepAssociatePlayListTracks.hpp"
|
#include "steps/ScanStepAssociatePlayListTracks.hpp"
|
||||||
#include "steps/ScanStepAssociateReleaseImages.hpp"
|
#include "steps/ScanStepAssociateReleaseImages.hpp"
|
||||||
|
#include "steps/ScanStepAssociateTrackImages.hpp"
|
||||||
#include "steps/ScanStepCheckForDuplicatedFiles.hpp"
|
#include "steps/ScanStepCheckForDuplicatedFiles.hpp"
|
||||||
#include "steps/ScanStepCheckForRemovedFiles.hpp"
|
#include "steps/ScanStepCheckForRemovedFiles.hpp"
|
||||||
#include "steps/ScanStepCompact.hpp"
|
#include "steps/ScanStepCompact.hpp"
|
||||||
@@ -461,8 +462,9 @@ namespace lms::scanner
|
|||||||
_scanSteps.emplace_back(std::make_unique<ScanStepArtistReconciliation>(params));
|
_scanSteps.emplace_back(std::make_unique<ScanStepArtistReconciliation>(params));
|
||||||
_scanSteps.emplace_back(std::make_unique<ScanStepAssociatePlayListTracks>(params));
|
_scanSteps.emplace_back(std::make_unique<ScanStepAssociatePlayListTracks>(params));
|
||||||
_scanSteps.emplace_back(std::make_unique<ScanStepUpdateLibraryFields>(params));
|
_scanSteps.emplace_back(std::make_unique<ScanStepUpdateLibraryFields>(params));
|
||||||
_scanSteps.emplace_back(std::make_unique<ScanStepAssociateArtistImages>(params));
|
|
||||||
_scanSteps.emplace_back(std::make_unique<ScanStepAssociateReleaseImages>(params));
|
_scanSteps.emplace_back(std::make_unique<ScanStepAssociateReleaseImages>(params));
|
||||||
|
_scanSteps.emplace_back(std::make_unique<ScanStepAssociateArtistImages>(params)); // must come after ScanStepAssociateReleaseImages
|
||||||
|
_scanSteps.emplace_back(std::make_unique<ScanStepAssociateTrackImages>(params)); // must come after ScanStepAssociateReleaseImages
|
||||||
_scanSteps.emplace_back(std::make_unique<ScanStepAssociateExternalLyrics>(params));
|
_scanSteps.emplace_back(std::make_unique<ScanStepAssociateExternalLyrics>(params));
|
||||||
_scanSteps.emplace_back(std::make_unique<ScanStepRemoveOrphanedDbEntries>(params));
|
_scanSteps.emplace_back(std::make_unique<ScanStepRemoveOrphanedDbEntries>(params));
|
||||||
_scanSteps.emplace_back(std::make_unique<ScanStepCompact>(params));
|
_scanSteps.emplace_back(std::make_unique<ScanStepCompact>(params));
|
||||||
|
|||||||
@@ -350,24 +350,12 @@ namespace lms::scanner
|
|||||||
|
|
||||||
void updateEmbeddedImages(db::Session& session, db::Track::pointer& track, std::span<const ImageInfo> images)
|
void updateEmbeddedImages(db::Session& session, db::Track::pointer& track, std::span<const ImageInfo> images)
|
||||||
{
|
{
|
||||||
db::TrackEmbeddedImageLink::pointer preferredImageLink;
|
|
||||||
|
|
||||||
track.modify()->clearEmbeddedImageLinks();
|
track.modify()->clearEmbeddedImageLinks();
|
||||||
for (const ImageInfo& imageInfo : images)
|
for (const ImageInfo& imageInfo : images)
|
||||||
{
|
{
|
||||||
db::TrackEmbeddedImageLink::pointer link{ createTrackEmbeddedImageLink(session, track, imageInfo) };
|
db::TrackEmbeddedImageLink::pointer link{ createTrackEmbeddedImageLink(session, track, imageInfo) };
|
||||||
track.modify()->addEmbeddedImageLink(link);
|
track.modify()->addEmbeddedImageLink(link);
|
||||||
|
|
||||||
if (!preferredImageLink
|
|
||||||
|| (preferredImageLink->getType() != db::ImageType::FrontCover && link->getType() == db::ImageType::FrontCover)
|
|
||||||
|| (preferredImageLink->getImage()->getSize() < link->getImage()->getSize()))
|
|
||||||
{
|
|
||||||
preferredImageLink = link;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (preferredImageLink)
|
|
||||||
preferredImageLink.modify()->setIsPreferred(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
db::Advisory getAdvisory(std::optional<metadata::Track::Advisory> advisory)
|
db::Advisory getAdvisory(std::optional<metadata::Track::Advisory> advisory)
|
||||||
|
|||||||
@@ -30,10 +30,7 @@ namespace lms::db
|
|||||||
class Session;
|
class Session;
|
||||||
} // namespace lms::db
|
} // namespace lms::db
|
||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner::utils
|
||||||
{
|
{
|
||||||
namespace utils
|
db::ObjectPtr<db::Directory> getOrCreateDirectory(db::Session& session, const std::filesystem::path& path, const db::ObjectPtr<db::MediaLibrary>& mediaLibrary);
|
||||||
{
|
} // namespace lms::scanner::utils
|
||||||
db::ObjectPtr<db::Directory> getOrCreateDirectory(db::Session& session, const std::filesystem::path& path, const db::ObjectPtr<db::MediaLibrary>& mediaLibrary);
|
|
||||||
} // namespace utils
|
|
||||||
} // namespace lms::scanner
|
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
/*
|
||||||
|
* 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 "ArtworkUtils.hpp"
|
||||||
|
|
||||||
|
#include "database/Artwork.hpp"
|
||||||
|
#include "database/Image.hpp"
|
||||||
|
#include "database/Session.hpp"
|
||||||
|
#include "database/Track.hpp"
|
||||||
|
#include "database/TrackEmbeddedImage.hpp"
|
||||||
|
|
||||||
|
namespace lms::scanner::utils
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
std::filesystem::path toPath(db::Session& session, const db::TrackEmbeddedImageId trackEmbeddedImageId)
|
||||||
|
{
|
||||||
|
session.checkReadTransaction();
|
||||||
|
|
||||||
|
std::filesystem::path res;
|
||||||
|
|
||||||
|
db::Track::FindParameters params;
|
||||||
|
params.setEmbeddedImage(trackEmbeddedImageId);
|
||||||
|
params.setRange(db::Range{ .offset = 0, .size = 1 });
|
||||||
|
db::Track::find(session, params, [&](const db::Track::pointer& track) {
|
||||||
|
res = track->getAbsoluteFilePath();
|
||||||
|
});
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path toPath(db::Session& session, db::ImageId imageId)
|
||||||
|
{
|
||||||
|
session.checkReadTransaction();
|
||||||
|
|
||||||
|
db::Image::pointer image{ db::Image::find(session, imageId) };
|
||||||
|
return image ? image->getAbsoluteFilePath() : std::filesystem::path{};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
db::ObjectPtr<db::Artwork> getOrCreateArtworkFromTrackEmbeddedImage(db::Session& session, db::TrackEmbeddedImageId trackEmbeddedImageId)
|
||||||
|
{
|
||||||
|
assert(trackEmbeddedImageId.isValid());
|
||||||
|
session.checkWriteTransaction();
|
||||||
|
|
||||||
|
db::ObjectPtr<db::Artwork> artwork{ db::Artwork::find(session, trackEmbeddedImageId) };
|
||||||
|
if (!artwork)
|
||||||
|
{
|
||||||
|
db::TrackEmbeddedImage::pointer trackEmbeddedImage{ db::TrackEmbeddedImage::find(session, trackEmbeddedImageId) };
|
||||||
|
assert(trackEmbeddedImage);
|
||||||
|
artwork = session.create<db::Artwork>(trackEmbeddedImage);
|
||||||
|
}
|
||||||
|
return artwork;
|
||||||
|
}
|
||||||
|
|
||||||
|
db::ObjectPtr<db::Artwork> getOrCreateArtworkFromImage(db::Session& session, db::ImageId imageId)
|
||||||
|
{
|
||||||
|
assert(imageId.isValid());
|
||||||
|
session.checkWriteTransaction();
|
||||||
|
|
||||||
|
db::ObjectPtr<db::Artwork> artwork{ db::Artwork::find(session, imageId) };
|
||||||
|
if (!artwork)
|
||||||
|
{
|
||||||
|
db::Image::pointer image{ db::Image::find(session, imageId) };
|
||||||
|
assert(image);
|
||||||
|
artwork = session.create<db::Artwork>(image);
|
||||||
|
}
|
||||||
|
return artwork;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path toPath(db::Session& session, db::ArtworkId artworkId)
|
||||||
|
{
|
||||||
|
session.checkReadTransaction();
|
||||||
|
|
||||||
|
db::Artwork::pointer artwork{ db::Artwork::find(session, artworkId) };
|
||||||
|
if (!artwork)
|
||||||
|
return std::filesystem::path{};
|
||||||
|
|
||||||
|
if (artwork->getTrackEmbeddedImageId().isValid())
|
||||||
|
return toPath(session, artwork->getTrackEmbeddedImageId());
|
||||||
|
if (artwork->getImageId().isValid())
|
||||||
|
return toPath(session, artwork->getImageId());
|
||||||
|
|
||||||
|
return std::filesystem::path{};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace lms::scanner::utils
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* 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 <filesystem>
|
||||||
|
|
||||||
|
#include "database/ArtworkId.hpp"
|
||||||
|
#include "database/ImageId.hpp"
|
||||||
|
#include "database/Object.hpp"
|
||||||
|
#include "database/TrackEmbeddedImageId.hpp"
|
||||||
|
|
||||||
|
namespace lms::db
|
||||||
|
{
|
||||||
|
class Artwork;
|
||||||
|
class Session;
|
||||||
|
} // namespace lms::db
|
||||||
|
|
||||||
|
namespace lms::scanner::utils
|
||||||
|
{
|
||||||
|
db::ObjectPtr<db::Artwork> getOrCreateArtworkFromTrackEmbeddedImage(db::Session& session, db::TrackEmbeddedImageId trackEmbeddedImageId);
|
||||||
|
db::ObjectPtr<db::Artwork> getOrCreateArtworkFromImage(db::Session& session, db::ImageId imageId);
|
||||||
|
|
||||||
|
std::filesystem::path toPath(db::Session& session, db::ArtworkId artworkId);
|
||||||
|
} // namespace lms::scanner::utils
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
#include <deque>
|
#include <deque>
|
||||||
#include <set>
|
#include <set>
|
||||||
#include <span>
|
#include <span>
|
||||||
|
#include <variant>
|
||||||
|
|
||||||
#include "core/IConfig.hpp"
|
#include "core/IConfig.hpp"
|
||||||
#include "core/ILogger.hpp"
|
#include "core/ILogger.hpp"
|
||||||
@@ -31,27 +32,38 @@
|
|||||||
#include "core/String.hpp"
|
#include "core/String.hpp"
|
||||||
#include "database/Artist.hpp"
|
#include "database/Artist.hpp"
|
||||||
#include "database/ArtistInfo.hpp"
|
#include "database/ArtistInfo.hpp"
|
||||||
|
#include "database/Artwork.hpp"
|
||||||
#include "database/Db.hpp"
|
#include "database/Db.hpp"
|
||||||
#include "database/Directory.hpp"
|
#include "database/Directory.hpp"
|
||||||
#include "database/Image.hpp"
|
#include "database/Image.hpp"
|
||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
#include "database/Track.hpp"
|
#include "database/Track.hpp"
|
||||||
|
|
||||||
|
#include "ArtworkUtils.hpp"
|
||||||
#include "ScanContext.hpp"
|
#include "ScanContext.hpp"
|
||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
{
|
{
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
constexpr std::size_t readBatchSize{ 100 };
|
using ArtistArtwork = std::variant<std::monostate, db::ImageId>; // TODO handle embedded images in tracks?
|
||||||
constexpr std::size_t writeBatchSize{ 20 };
|
bool isSameArtwork(ArtistArtwork preferredArtwork, const db::ObjectPtr<db::Artwork>& artwork)
|
||||||
|
|
||||||
struct ArtistImageAssociation
|
|
||||||
{
|
{
|
||||||
db::ArtistId artistId;
|
if (std::holds_alternative<std::monostate>(preferredArtwork))
|
||||||
db::ImageId imageId;
|
return !artwork;
|
||||||
|
|
||||||
|
if (const db::ImageId* imageId = std::get_if<db::ImageId>(&preferredArtwork))
|
||||||
|
return artwork && *imageId == artwork->getImageId();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ArtistArtworkAssociation
|
||||||
|
{
|
||||||
|
db::Artist::pointer artist;
|
||||||
|
ArtistArtwork preferredArtwork;
|
||||||
};
|
};
|
||||||
using ArtistImageAssociationContainer = std::deque<ArtistImageAssociation>;
|
using ArtistArtworkAssociationContainer = std::deque<ArtistArtworkAssociation>;
|
||||||
|
|
||||||
struct SearchArtistImageContext
|
struct SearchArtistImageContext
|
||||||
{
|
{
|
||||||
@@ -87,7 +99,7 @@ namespace lms::scanner
|
|||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
db::Image::pointer getImageFromMbid(SearchArtistImageContext& searchContext, const core::UUID& mbid)
|
db::ImageId getImageFromMbid(SearchArtistImageContext& searchContext, const core::UUID& mbid)
|
||||||
{
|
{
|
||||||
db::Image::pointer image;
|
db::Image::pointer image;
|
||||||
|
|
||||||
@@ -97,10 +109,10 @@ namespace lms::scanner
|
|||||||
image = foundImg;
|
image = foundImg;
|
||||||
});
|
});
|
||||||
|
|
||||||
return image;
|
return image ? image->getId() : db::ImageId{};
|
||||||
}
|
}
|
||||||
|
|
||||||
db::Image::pointer searchImageInArtistInfoDirectory(SearchArtistImageContext& searchContext, db::ArtistId artistId)
|
db::ImageId searchImageInArtistInfoDirectory(SearchArtistImageContext& searchContext, db::ArtistId artistId)
|
||||||
{
|
{
|
||||||
db::Image::pointer image;
|
db::Image::pointer image;
|
||||||
|
|
||||||
@@ -115,10 +127,10 @@ namespace lms::scanner
|
|||||||
if (fileInfoPaths.size() > 1)
|
if (fileInfoPaths.size() > 1)
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Found " << fileInfoPaths.size() << " artist info files for same artist: " << core::stringUtils::joinStrings(fileInfoPaths, ", "));
|
LMS_LOG(DBUPDATER, DEBUG, "Found " << fileInfoPaths.size() << " artist info files for same artist: " << core::stringUtils::joinStrings(fileInfoPaths, ", "));
|
||||||
|
|
||||||
return image;
|
return image ? image->getId() : db::ImageId{};
|
||||||
}
|
}
|
||||||
|
|
||||||
db::Image::pointer searchImageInDirectories(SearchArtistImageContext& searchContext, db::ArtistId artistId)
|
db::ImageId searchImageInDirectories(SearchArtistImageContext& searchContext, db::ArtistId artistId)
|
||||||
{
|
{
|
||||||
db::Image::pointer image;
|
db::Image::pointer image;
|
||||||
|
|
||||||
@@ -147,7 +159,7 @@ namespace lms::scanner
|
|||||||
{
|
{
|
||||||
image = findImageInDirectory(searchContext, directoryToInspect, searchContext.artistFileNames);
|
image = findImageInDirectory(searchContext, directoryToInspect, searchContext.artistFileNames);
|
||||||
if (image)
|
if (image)
|
||||||
return image;
|
return image->getId();
|
||||||
|
|
||||||
std::filesystem::path parentPath{ directoryToInspect.parent_path() };
|
std::filesystem::path parentPath{ directoryToInspect.parent_path() };
|
||||||
if (parentPath == directoryToInspect)
|
if (parentPath == directoryToInspect)
|
||||||
@@ -164,44 +176,44 @@ namespace lms::scanner
|
|||||||
{
|
{
|
||||||
image = findImageInDirectory(searchContext, releasePath, searchContext.artistFileNames);
|
image = findImageInDirectory(searchContext, releasePath, searchContext.artistFileNames);
|
||||||
if (image)
|
if (image)
|
||||||
return image;
|
return image->getId();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return image;
|
return image ? image->getId() : db::ImageId{};
|
||||||
}
|
}
|
||||||
|
|
||||||
db::Image::pointer computeBestArtistImage(SearchArtistImageContext& searchContext, const db::Artist::pointer& artist)
|
ArtistArtwork computePreferredArtwork(SearchArtistImageContext& searchContext, const db::Artist::pointer& artist)
|
||||||
{
|
{
|
||||||
db::Image::pointer image;
|
db::ImageId imageId;
|
||||||
|
|
||||||
if (const auto mbid{ artist->getMBID() })
|
if (const auto mbid{ artist->getMBID() })
|
||||||
image = getImageFromMbid(searchContext, *mbid);
|
imageId = getImageFromMbid(searchContext, *mbid);
|
||||||
|
|
||||||
if (!image)
|
if (!imageId.isValid())
|
||||||
image = searchImageInArtistInfoDirectory(searchContext, artist->getId());
|
imageId = searchImageInArtistInfoDirectory(searchContext, artist->getId());
|
||||||
|
|
||||||
if (!image)
|
if (!imageId.isValid())
|
||||||
image = searchImageInDirectories(searchContext, artist->getId());
|
imageId = searchImageInDirectories(searchContext, artist->getId());
|
||||||
|
|
||||||
return image;
|
return imageId.isValid() ? ArtistArtwork{ imageId } : ArtistArtwork{};
|
||||||
}
|
}
|
||||||
|
|
||||||
bool fetchNextArtistImagesToUpdate(SearchArtistImageContext& searchContext, ArtistImageAssociationContainer& artistImageAssociations)
|
bool fetchNextArtistArtworksToUpdate(SearchArtistImageContext& searchContext, ArtistArtworkAssociationContainer& ArtistArtworkAssociations)
|
||||||
{
|
{
|
||||||
const db::ArtistId artistId{ searchContext.lastRetrievedArtistId };
|
const db::ArtistId artistId{ searchContext.lastRetrievedArtistId };
|
||||||
|
|
||||||
{
|
{
|
||||||
|
constexpr std::size_t readBatchSize{ 100 };
|
||||||
|
|
||||||
auto transaction{ searchContext.session.createReadTransaction() };
|
auto transaction{ searchContext.session.createReadTransaction() };
|
||||||
|
|
||||||
db::Artist::find(searchContext.session, searchContext.lastRetrievedArtistId, readBatchSize, [&](const db::Artist::pointer& artist) {
|
db::Artist::find(searchContext.session, searchContext.lastRetrievedArtistId, readBatchSize, [&](const db::Artist::pointer& artist) {
|
||||||
db::Image::pointer image{ computeBestArtistImage(searchContext, artist) };
|
ArtistArtwork preferredArtwork{ computePreferredArtwork(searchContext, artist) };
|
||||||
|
|
||||||
|
if (!isSameArtwork(preferredArtwork, artist->getPreferredArtwork()))
|
||||||
|
ArtistArtworkAssociations.push_back(ArtistArtworkAssociation{ artist, preferredArtwork });
|
||||||
|
|
||||||
if (image != artist->getImage())
|
|
||||||
{
|
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Updating artist image for artist '" << artist->getName() << "', using '" << (image ? image->getAbsoluteFilePath().c_str() : "<none>") << "'");
|
|
||||||
artistImageAssociations.push_back(ArtistImageAssociation{ artist->getId(), image ? image->getId() : db::ImageId{} });
|
|
||||||
}
|
|
||||||
searchContext.processedArtistCount++;
|
searchContext.processedArtistCount++;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -209,27 +221,32 @@ namespace lms::scanner
|
|||||||
return artistId != searchContext.lastRetrievedArtistId;
|
return artistId != searchContext.lastRetrievedArtistId;
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateArtistImage(db::Session& session, const ArtistImageAssociation& artistImageAssociation)
|
void updateArtistPreferredArtwork(db::Session& session, const ArtistArtworkAssociation& ArtistArtworkAssociation)
|
||||||
{
|
{
|
||||||
db::Artist::pointer artist{ db::Artist::find(session, artistImageAssociation.artistId) };
|
db::Artist::pointer artist{ ArtistArtworkAssociation.artist };
|
||||||
assert(artist);
|
|
||||||
|
|
||||||
db::Image::pointer image;
|
db::Artwork::pointer artwork;
|
||||||
if (artistImageAssociation.imageId.isValid())
|
if (const db::ImageId * imageId{ std::get_if<db::ImageId>(&ArtistArtworkAssociation.preferredArtwork) })
|
||||||
image = db::Image::find(session, artistImageAssociation.imageId);
|
artwork = utils::getOrCreateArtworkFromImage(session, *imageId);
|
||||||
|
|
||||||
artist.modify()->setImage(image);
|
artist.modify()->setPreferredArtwork(artwork);
|
||||||
|
if (artwork)
|
||||||
|
LMS_LOG(DBUPDATER, DEBUG, "Updated preferred artwork for artist '" << artist->getName() << "' with image in " << utils::toPath(session, artwork->getId()));
|
||||||
|
else
|
||||||
|
LMS_LOG(DBUPDATER, DEBUG, "Removed preferred artwork from artist '" << artist->getName() << "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateArtistImages(db::Session& session, ArtistImageAssociationContainer& imageAssociations)
|
void updateArtistArtworks(db::Session& session, ArtistArtworkAssociationContainer& imageAssociations)
|
||||||
{
|
{
|
||||||
|
constexpr std::size_t writeBatchSize{ 50 };
|
||||||
|
|
||||||
while (!imageAssociations.empty())
|
while (!imageAssociations.empty())
|
||||||
{
|
{
|
||||||
auto transaction{ session.createWriteTransaction() };
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
|
||||||
for (std::size_t i{}; !imageAssociations.empty() && i < writeBatchSize; ++i)
|
for (std::size_t i{}; !imageAssociations.empty() && i < writeBatchSize; ++i)
|
||||||
{
|
{
|
||||||
updateArtistImage(session, imageAssociations.front());
|
updateArtistPreferredArtwork(session, imageAssociations.front());
|
||||||
imageAssociations.pop_front();
|
imageAssociations.pop_front();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -258,10 +275,7 @@ namespace lms::scanner
|
|||||||
|
|
||||||
bool ScanStepAssociateArtistImages::needProcess(const ScanContext& context) const
|
bool ScanStepAssociateArtistImages::needProcess(const ScanContext& context) const
|
||||||
{
|
{
|
||||||
if (context.stats.nbChanges() > 0)
|
return context.stats.nbChanges() > 0;
|
||||||
return true;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ScanStepAssociateArtistImages::process(ScanContext& context)
|
void ScanStepAssociateArtistImages::process(ScanContext& context)
|
||||||
@@ -279,13 +293,13 @@ namespace lms::scanner
|
|||||||
.artistFileNames = _artistFileNames,
|
.artistFileNames = _artistFileNames,
|
||||||
};
|
};
|
||||||
|
|
||||||
ArtistImageAssociationContainer artistImageAssociations;
|
ArtistArtworkAssociationContainer ArtistArtworkAssociations;
|
||||||
while (fetchNextArtistImagesToUpdate(searchContext, artistImageAssociations))
|
while (fetchNextArtistArtworksToUpdate(searchContext, ArtistArtworkAssociations))
|
||||||
{
|
{
|
||||||
if (_abortScan)
|
if (_abortScan)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
updateArtistImages(session, artistImageAssociations);
|
updateArtistArtworks(session, ArtistArtworkAssociations);
|
||||||
context.currentStepStats.processedElems = searchContext.processedArtistCount;
|
context.currentStepStats.processedElems = searchContext.processedArtistCount;
|
||||||
_progressCallback(context.currentStepStats);
|
_progressCallback(context.currentStepStats);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,46 +23,66 @@
|
|||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <deque>
|
#include <deque>
|
||||||
#include <set>
|
#include <set>
|
||||||
|
#include <variant>
|
||||||
|
|
||||||
#include "core/IConfig.hpp"
|
#include "core/IConfig.hpp"
|
||||||
#include "core/ILogger.hpp"
|
#include "core/ILogger.hpp"
|
||||||
#include "core/Path.hpp"
|
#include "core/Path.hpp"
|
||||||
|
#include "database/Artwork.hpp"
|
||||||
#include "database/Db.hpp"
|
#include "database/Db.hpp"
|
||||||
#include "database/Directory.hpp"
|
#include "database/Directory.hpp"
|
||||||
#include "database/Image.hpp"
|
#include "database/Image.hpp"
|
||||||
#include "database/Release.hpp"
|
#include "database/Release.hpp"
|
||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
#include "database/Track.hpp"
|
#include "database/Track.hpp"
|
||||||
|
#include "database/TrackEmbeddedImage.hpp"
|
||||||
|
#include "database/TrackEmbeddedImageId.hpp"
|
||||||
|
|
||||||
|
#include "ArtworkUtils.hpp"
|
||||||
#include "ScanContext.hpp"
|
#include "ScanContext.hpp"
|
||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
{
|
{
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
struct ReleaseImageAssociation
|
using ReleaseArtwork = std::variant<std::monostate, db::TrackEmbeddedImageId, db::ImageId>;
|
||||||
|
bool isSameArtwork(ReleaseArtwork preferredArtwork, const db::ObjectPtr<db::Artwork>& artwork)
|
||||||
{
|
{
|
||||||
db::ReleaseId releaseId;
|
if (std::holds_alternative<std::monostate>(preferredArtwork))
|
||||||
db::ImageId imageId;
|
return !artwork;
|
||||||
};
|
|
||||||
using ReleaseImageAssociationContainer = std::deque<ReleaseImageAssociation>;
|
|
||||||
|
|
||||||
struct SearchReleaseImageContext
|
if (const db::TrackEmbeddedImageId* trackEmbeddedImageId = std::get_if<db::TrackEmbeddedImageId>(&preferredArtwork))
|
||||||
|
return artwork && *trackEmbeddedImageId == artwork->getTrackEmbeddedImageId();
|
||||||
|
|
||||||
|
if (const db::ImageId* imageId = std::get_if<db::ImageId>(&preferredArtwork))
|
||||||
|
return artwork && *imageId == artwork->getImageId();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ReleaseArtworkAssociation
|
||||||
|
{
|
||||||
|
db::Release::pointer release;
|
||||||
|
ReleaseArtwork preferredArtwork;
|
||||||
|
};
|
||||||
|
using ReleaseArtworkAssociationContainer = std::deque<ReleaseArtworkAssociation>;
|
||||||
|
|
||||||
|
struct SearchReleaseArtworkContext
|
||||||
{
|
{
|
||||||
db::Session& session;
|
db::Session& session;
|
||||||
db::ReleaseId lastRetrievedReleaseId;
|
db::ReleaseId lastRetrievedReleaseId;
|
||||||
std::size_t processedReleaseCount{};
|
std::size_t processedReleaseCount{};
|
||||||
const std::vector<std::string>& releaseFileNames;
|
const std::vector<std::string>& releaseImageFileNames;
|
||||||
};
|
};
|
||||||
|
|
||||||
db::Image::pointer findImageInDirectory(SearchReleaseImageContext& searchContext, const std::filesystem::path& directoryPath)
|
db::Image::pointer findImageInDirectory(SearchReleaseArtworkContext& searchContext, const std::filesystem::path& directoryPath)
|
||||||
{
|
{
|
||||||
db::Image::pointer image;
|
db::Image::pointer image;
|
||||||
|
|
||||||
const db::Directory::pointer directory{ db::Directory::find(searchContext.session, directoryPath) };
|
const db::Directory::pointer directory{ db::Directory::find(searchContext.session, directoryPath) };
|
||||||
if (directory) // may not exist for releases that are split on different media libraries
|
if (directory) // may not exist for releases that are split on different media libraries
|
||||||
{
|
{
|
||||||
for (std::string_view fileStem : searchContext.releaseFileNames)
|
for (std::string_view fileStem : searchContext.releaseImageFileNames)
|
||||||
{
|
{
|
||||||
db::Image::FindParameters params;
|
db::Image::FindParameters params;
|
||||||
params.setDirectory(directory->getId());
|
params.setDirectory(directory->getId());
|
||||||
@@ -81,7 +101,7 @@ namespace lms::scanner
|
|||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
db::Image::pointer computeBestReleaseImage(SearchReleaseImageContext& searchContext, const db::Release::pointer& release)
|
db::Image::pointer computePreferredReleaseImage(SearchReleaseArtworkContext& searchContext, const db::Release::pointer& release)
|
||||||
{
|
{
|
||||||
db::Image::pointer image;
|
db::Image::pointer image;
|
||||||
|
|
||||||
@@ -129,7 +149,45 @@ namespace lms::scanner
|
|||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool fetchNextReleaseImagesToUpdate(SearchReleaseImageContext& searchContext, ReleaseImageAssociationContainer& releaseImageAssociations)
|
ReleaseArtwork computePreferredReleaseArtwork(SearchReleaseArtworkContext& searchContext, const db::Release::pointer& release)
|
||||||
|
{
|
||||||
|
const db::Image::pointer image{ computePreferredReleaseImage(searchContext, release) };
|
||||||
|
if (image)
|
||||||
|
return ReleaseArtwork{ image->getId() };
|
||||||
|
|
||||||
|
// Fallback on embedded Front image
|
||||||
|
db::TrackEmbeddedImageId trackEmbeddedImageId;
|
||||||
|
{
|
||||||
|
db::TrackEmbeddedImage::FindParameters params;
|
||||||
|
params.setRelease(release->getId());
|
||||||
|
params.setImageTypes({ db::ImageType::FrontCover });
|
||||||
|
params.setSortMethod(db::TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc);
|
||||||
|
params.setRange(db::Range{ .offset = 0, .size = 1 });
|
||||||
|
|
||||||
|
db::TrackEmbeddedImage::find(searchContext.session, params, [&](const db::TrackEmbeddedImage::pointer& image) { trackEmbeddedImageId = image->getId(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trackEmbeddedImageId.isValid())
|
||||||
|
return ReleaseArtwork{ trackEmbeddedImageId };
|
||||||
|
|
||||||
|
// Fallback on embedded media image
|
||||||
|
{
|
||||||
|
db::TrackEmbeddedImage::FindParameters params;
|
||||||
|
params.setRelease(release->getId());
|
||||||
|
params.setImageTypes({ db::ImageType::Media });
|
||||||
|
params.setSortMethod(db::TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc);
|
||||||
|
params.setRange(db::Range{ .offset = 0, .size = 1 });
|
||||||
|
|
||||||
|
db::TrackEmbeddedImage::find(searchContext.session, params, [&](const db::TrackEmbeddedImage::pointer& image) { trackEmbeddedImageId = image->getId(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trackEmbeddedImageId.isValid())
|
||||||
|
return ReleaseArtwork{ trackEmbeddedImageId };
|
||||||
|
|
||||||
|
return ReleaseArtwork{};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool fetchNextReleaseArtworksToUpdate(SearchReleaseArtworkContext& searchContext, ReleaseArtworkAssociationContainer& ReleaseArtworkAssociations)
|
||||||
{
|
{
|
||||||
const db::ReleaseId releaseId{ searchContext.lastRetrievedReleaseId };
|
const db::ReleaseId releaseId{ searchContext.lastRetrievedReleaseId };
|
||||||
|
|
||||||
@@ -139,13 +197,11 @@ namespace lms::scanner
|
|||||||
auto transaction{ searchContext.session.createReadTransaction() };
|
auto transaction{ searchContext.session.createReadTransaction() };
|
||||||
|
|
||||||
db::Release::find(searchContext.session, searchContext.lastRetrievedReleaseId, readBatchSize, [&](const db::Release::pointer& release) {
|
db::Release::find(searchContext.session, searchContext.lastRetrievedReleaseId, readBatchSize, [&](const db::Release::pointer& release) {
|
||||||
db::Image::pointer image{ computeBestReleaseImage(searchContext, release) };
|
const ReleaseArtwork preferredArtwork{ computePreferredReleaseArtwork(searchContext, release) };
|
||||||
|
const db::Artwork::pointer currentPreferredArtwork{ release->getPreferredArtwork() };
|
||||||
|
|
||||||
if (image != release->getImage())
|
if (!isSameArtwork(preferredArtwork, currentPreferredArtwork))
|
||||||
{
|
ReleaseArtworkAssociations.push_back(ReleaseArtworkAssociation{ release, preferredArtwork });
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Updating release image for release '" << release->getName() << "', using '" << (image ? image->getAbsoluteFilePath().c_str() : "<none>") << "'");
|
|
||||||
releaseImageAssociations.push_back(ReleaseImageAssociation{ release->getId(), image ? image->getId() : db::ImageId{} });
|
|
||||||
}
|
|
||||||
searchContext.processedReleaseCount++;
|
searchContext.processedReleaseCount++;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -153,21 +209,27 @@ namespace lms::scanner
|
|||||||
return releaseId != searchContext.lastRetrievedReleaseId;
|
return releaseId != searchContext.lastRetrievedReleaseId;
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateReleaseImage(db::Session& session, const ReleaseImageAssociation& releaseImageAssociation)
|
void updateReleaseArtwork(db::Session& session, const ReleaseArtworkAssociation& ReleaseArtworkAssociation)
|
||||||
{
|
{
|
||||||
db::Release::pointer release{ db::Release::find(session, releaseImageAssociation.releaseId) };
|
db::Release::pointer release{ ReleaseArtworkAssociation.release };
|
||||||
assert(release);
|
|
||||||
|
|
||||||
db::Image::pointer image;
|
db::Artwork::pointer artwork;
|
||||||
if (releaseImageAssociation.imageId.isValid())
|
if (const db::TrackEmbeddedImageId * trackEmbeddedImageId{ std::get_if<db::TrackEmbeddedImageId>(&ReleaseArtworkAssociation.preferredArtwork) })
|
||||||
image = db::Image::find(session, releaseImageAssociation.imageId);
|
artwork = utils::getOrCreateArtworkFromTrackEmbeddedImage(session, *trackEmbeddedImageId);
|
||||||
|
else if (const db::ImageId * imageId{ std::get_if<db::ImageId>(&ReleaseArtworkAssociation.preferredArtwork) })
|
||||||
|
artwork = utils::getOrCreateArtworkFromImage(session, *imageId);
|
||||||
|
|
||||||
release.modify()->setImage(image);
|
release.modify()->setPreferredArtwork(artwork);
|
||||||
|
|
||||||
|
if (artwork)
|
||||||
|
LMS_LOG(DBUPDATER, DEBUG, "Updated preferred artwork in release '" << release->getName() << "' with image in " << utils::toPath(session, artwork->getId()));
|
||||||
|
else
|
||||||
|
LMS_LOG(DBUPDATER, DEBUG, "Removed preferred artwork from release '" << release->getName() << "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateReleaseImages(db::Session& session, ReleaseImageAssociationContainer& imageAssociations)
|
void updateReleaseArtworks(db::Session& session, ReleaseArtworkAssociationContainer& imageAssociations)
|
||||||
{
|
{
|
||||||
constexpr std::size_t writeBatchSize{ 20 };
|
constexpr std::size_t writeBatchSize{ 50 };
|
||||||
|
|
||||||
while (!imageAssociations.empty())
|
while (!imageAssociations.empty())
|
||||||
{
|
{
|
||||||
@@ -175,13 +237,13 @@ namespace lms::scanner
|
|||||||
|
|
||||||
for (std::size_t i{}; !imageAssociations.empty() && i < writeBatchSize; ++i)
|
for (std::size_t i{}; !imageAssociations.empty() && i < writeBatchSize; ++i)
|
||||||
{
|
{
|
||||||
updateReleaseImage(session, imageAssociations.front());
|
updateReleaseArtwork(session, imageAssociations.front());
|
||||||
imageAssociations.pop_front();
|
imageAssociations.pop_front();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::string> constructReleaseFileNames()
|
std::vector<std::string> constructReleaseImageFileNames()
|
||||||
{
|
{
|
||||||
std::vector<std::string> res;
|
std::vector<std::string> res;
|
||||||
|
|
||||||
@@ -198,16 +260,13 @@ namespace lms::scanner
|
|||||||
|
|
||||||
ScanStepAssociateReleaseImages::ScanStepAssociateReleaseImages(InitParams& initParams)
|
ScanStepAssociateReleaseImages::ScanStepAssociateReleaseImages(InitParams& initParams)
|
||||||
: ScanStepBase{ initParams }
|
: ScanStepBase{ initParams }
|
||||||
, _releaseFileNames{ constructReleaseFileNames() }
|
, _releaseImageFileNames{ constructReleaseImageFileNames() }
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ScanStepAssociateReleaseImages::needProcess(const ScanContext& context) const
|
bool ScanStepAssociateReleaseImages::needProcess(const ScanContext& context) const
|
||||||
{
|
{
|
||||||
if (context.stats.nbChanges() > 0)
|
return context.stats.nbChanges() > 0;
|
||||||
return true;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ScanStepAssociateReleaseImages::process(ScanContext& context)
|
void ScanStepAssociateReleaseImages::process(ScanContext& context)
|
||||||
@@ -219,19 +278,19 @@ namespace lms::scanner
|
|||||||
context.currentStepStats.totalElems = db::Release::getCount(session);
|
context.currentStepStats.totalElems = db::Release::getCount(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
SearchReleaseImageContext searchContext{
|
SearchReleaseArtworkContext searchContext{
|
||||||
.session = session,
|
.session = session,
|
||||||
.lastRetrievedReleaseId = {},
|
.lastRetrievedReleaseId = {},
|
||||||
.releaseFileNames = _releaseFileNames,
|
.releaseImageFileNames = _releaseImageFileNames,
|
||||||
};
|
};
|
||||||
|
|
||||||
ReleaseImageAssociationContainer releaseImageAssociations;
|
ReleaseArtworkAssociationContainer ReleaseArtworkAssociations;
|
||||||
while (fetchNextReleaseImagesToUpdate(searchContext, releaseImageAssociations))
|
while (fetchNextReleaseArtworksToUpdate(searchContext, ReleaseArtworkAssociations))
|
||||||
{
|
{
|
||||||
if (_abortScan)
|
if (_abortScan)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
updateReleaseImages(session, releaseImageAssociations);
|
updateReleaseArtworks(session, ReleaseArtworkAssociations);
|
||||||
context.currentStepStats.processedElems = searchContext.processedReleaseCount;
|
context.currentStepStats.processedElems = searchContext.processedReleaseCount;
|
||||||
_progressCallback(context.currentStepStats);
|
_progressCallback(context.currentStepStats);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,6 @@ namespace lms::scanner
|
|||||||
bool needProcess(const ScanContext& context) const override;
|
bool needProcess(const ScanContext& context) const override;
|
||||||
void process(ScanContext& context) override;
|
void process(ScanContext& context) override;
|
||||||
|
|
||||||
const std::vector<std::string> _releaseFileNames;
|
const std::vector<std::string> _releaseImageFileNames;
|
||||||
};
|
};
|
||||||
} // namespace lms::scanner
|
} // namespace lms::scanner
|
||||||
|
|||||||
@@ -0,0 +1,298 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 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 "ScanStepAssociateTrackImages.hpp"
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
|
#include <deque>
|
||||||
|
#include <variant>
|
||||||
|
|
||||||
|
#include "core/ILogger.hpp"
|
||||||
|
#include "database/Artwork.hpp"
|
||||||
|
#include "database/Db.hpp"
|
||||||
|
#include "database/Directory.hpp"
|
||||||
|
#include "database/Image.hpp"
|
||||||
|
#include "database/Release.hpp"
|
||||||
|
#include "database/Session.hpp"
|
||||||
|
#include "database/Track.hpp"
|
||||||
|
#include "database/TrackEmbeddedImage.hpp"
|
||||||
|
#include "database/TrackEmbeddedImageId.hpp"
|
||||||
|
|
||||||
|
#include "ArtworkUtils.hpp"
|
||||||
|
#include "ScanContext.hpp"
|
||||||
|
|
||||||
|
namespace lms::scanner
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
// May come from an embedded image in a track, or from what has been previously resolved for the release
|
||||||
|
using TrackArtwork = std::variant<std::monostate, db::TrackEmbeddedImageId, db::ArtworkId>;
|
||||||
|
bool isSameArtwork(TrackArtwork preferredArtwork, const db::ObjectPtr<db::Artwork>& artwork)
|
||||||
|
{
|
||||||
|
if (std::holds_alternative<std::monostate>(preferredArtwork))
|
||||||
|
return !artwork;
|
||||||
|
|
||||||
|
if (const db::TrackEmbeddedImageId* trackEmbeddedImageId = std::get_if<db::TrackEmbeddedImageId>(&preferredArtwork))
|
||||||
|
return artwork && *trackEmbeddedImageId == artwork->getTrackEmbeddedImageId();
|
||||||
|
|
||||||
|
if (const db::ArtworkId* artworkId = std::get_if<db::ArtworkId>(&preferredArtwork))
|
||||||
|
return artwork && *artworkId == artwork->getId();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isValid(const TrackArtwork& res)
|
||||||
|
{
|
||||||
|
return !std::holds_alternative<std::monostate>(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TrackArtworksAssociation
|
||||||
|
{
|
||||||
|
db::Track::pointer track;
|
||||||
|
TrackArtwork preferredArtwork;
|
||||||
|
TrackArtwork preferredMediaArtwork;
|
||||||
|
};
|
||||||
|
using TrackArtworksAssociationContainer = std::deque<TrackArtworksAssociation>;
|
||||||
|
|
||||||
|
struct SearchTrackArtworkContext
|
||||||
|
{
|
||||||
|
db::Session& session;
|
||||||
|
db::TrackId lastRetrievedTrackId;
|
||||||
|
std::size_t processedTrackCount{};
|
||||||
|
};
|
||||||
|
|
||||||
|
TrackArtwork computePreferredTrackArtwork(SearchTrackArtworkContext& searchContext, const db::Track::pointer& track)
|
||||||
|
{
|
||||||
|
// Try to get a media image
|
||||||
|
TrackArtwork res;
|
||||||
|
{
|
||||||
|
db::TrackEmbeddedImage::FindParameters params;
|
||||||
|
params.setTrack(track->getId());
|
||||||
|
params.setImageTypes({ db::ImageType::Media });
|
||||||
|
params.setSortMethod(db::TrackEmbeddedImageSortMethod::SizeDesc);
|
||||||
|
params.setRange(db::Range{ .offset = 0, .size = 1 });
|
||||||
|
db::TrackEmbeddedImage::find(searchContext.session, params, [&](const db::TrackEmbeddedImage::pointer& image) { res = image->getId(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isValid(res))
|
||||||
|
return res;
|
||||||
|
|
||||||
|
// Fallback on another track of the same disc
|
||||||
|
const db::ReleaseId releaseId{ track->getReleaseId() };
|
||||||
|
if (!releaseId.isValid())
|
||||||
|
return res;
|
||||||
|
|
||||||
|
{
|
||||||
|
db::TrackEmbeddedImage::FindParameters params;
|
||||||
|
params.setRelease(releaseId);
|
||||||
|
params.setDiscNumber(track->getDiscNumber());
|
||||||
|
params.setImageTypes({ db::ImageType::Media });
|
||||||
|
params.setSortMethod(db::TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc);
|
||||||
|
params.setRange(db::Range{ .offset = 0, .size = 1 });
|
||||||
|
db::TrackEmbeddedImage::find(searchContext.session, params, [&](const db::TrackEmbeddedImage::pointer& image) { res = image->getId(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isValid(res))
|
||||||
|
return res;
|
||||||
|
|
||||||
|
// Fallback on front cover for this track
|
||||||
|
{
|
||||||
|
db::TrackEmbeddedImage::FindParameters params;
|
||||||
|
params.setTrack(track->getId());
|
||||||
|
params.setImageTypes({ db::ImageType::FrontCover });
|
||||||
|
params.setSortMethod(db::TrackEmbeddedImageSortMethod::SizeDesc);
|
||||||
|
params.setRange(db::Range{ .offset = 0, .size = 1 });
|
||||||
|
db::TrackEmbeddedImage::find(searchContext.session, params, [&](const db::TrackEmbeddedImage::pointer& image) { res = image->getId(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isValid(res))
|
||||||
|
return res;
|
||||||
|
|
||||||
|
// Fallback on the artwork already resolved for the release
|
||||||
|
if (const db::Release::pointer release{ db::Release::find(searchContext.session, releaseId) })
|
||||||
|
{
|
||||||
|
db::ArtworkId artworkId{ release->getPreferredArtworkId() };
|
||||||
|
if (artworkId.isValid())
|
||||||
|
res = artworkId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
TrackArtwork computePreferredTrackMediaArtwork(SearchTrackArtworkContext& searchContext, const db::Track::pointer& track)
|
||||||
|
{
|
||||||
|
TrackArtwork res;
|
||||||
|
{
|
||||||
|
db::TrackEmbeddedImage::FindParameters params;
|
||||||
|
params.setTrack(track->getId());
|
||||||
|
params.setImageTypes({ db::ImageType::Media });
|
||||||
|
params.setSortMethod(db::TrackEmbeddedImageSortMethod::SizeDesc);
|
||||||
|
params.setRange(db::Range{ .offset = 0, .size = 1 });
|
||||||
|
db::TrackEmbeddedImage::find(searchContext.session, params, [&](const db::TrackEmbeddedImage::pointer& image) { res = image->getId(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isValid(res))
|
||||||
|
return res;
|
||||||
|
|
||||||
|
// fallback on another track of the same disc
|
||||||
|
if (const db::ReleaseId releaseId{ track->getReleaseId() }; releaseId.isValid())
|
||||||
|
{
|
||||||
|
db::TrackEmbeddedImage::FindParameters params;
|
||||||
|
params.setRelease(releaseId);
|
||||||
|
params.setDiscNumber(track->getDiscNumber());
|
||||||
|
params.setImageTypes({ db::ImageType::Media });
|
||||||
|
params.setSortMethod(db::TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc);
|
||||||
|
params.setRange(db::Range{ .offset = 0, .size = 1 });
|
||||||
|
db::TrackEmbeddedImage::find(searchContext.session, params, [&](const db::TrackEmbeddedImage::pointer& image) { res = image->getId(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool fetchNextTrackArtworksToUpdate(SearchTrackArtworkContext& searchContext, TrackArtworksAssociationContainer& TrackArtworksAssociations)
|
||||||
|
{
|
||||||
|
const db::TrackId trackId{ searchContext.lastRetrievedTrackId };
|
||||||
|
|
||||||
|
{
|
||||||
|
constexpr std::size_t readBatchSize{ 100 };
|
||||||
|
|
||||||
|
auto transaction{ searchContext.session.createReadTransaction() };
|
||||||
|
|
||||||
|
db::Track::find(searchContext.session, searchContext.lastRetrievedTrackId, readBatchSize, [&](const db::Track::pointer& track) {
|
||||||
|
const TrackArtwork preferredArtwork{ computePreferredTrackArtwork(searchContext, track) };
|
||||||
|
const TrackArtwork preferredMediaArtwork{ computePreferredTrackMediaArtwork(searchContext, track) };
|
||||||
|
|
||||||
|
const db::Artwork::pointer currentPreferredArtwork{ track->getPreferredArtwork() };
|
||||||
|
const db::Artwork::pointer currentPreferredMediaArtwork{ track->getPreferredMediaArtwork() };
|
||||||
|
|
||||||
|
if (!isSameArtwork(preferredArtwork, currentPreferredArtwork)
|
||||||
|
|| !isSameArtwork(preferredMediaArtwork, currentPreferredMediaArtwork))
|
||||||
|
{
|
||||||
|
TrackArtworksAssociations.push_back(TrackArtworksAssociation{ track, preferredArtwork, preferredMediaArtwork });
|
||||||
|
}
|
||||||
|
|
||||||
|
searchContext.processedTrackCount++;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return trackId != searchContext.lastRetrievedTrackId;
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateTrackPreferredArtwork(db::Session& session, db::Track::pointer& track, TrackArtwork preferredArtwork)
|
||||||
|
{
|
||||||
|
db::Artwork::pointer artwork;
|
||||||
|
if (const db::TrackEmbeddedImageId * trackEmbeddedImageId{ std::get_if<db::TrackEmbeddedImageId>(&preferredArtwork) })
|
||||||
|
artwork = utils::getOrCreateArtworkFromTrackEmbeddedImage(session, *trackEmbeddedImageId);
|
||||||
|
else if (const db::ArtworkId * artworkId{ std::get_if<db::ArtworkId>(&preferredArtwork) })
|
||||||
|
artwork = db::Artwork::find(session, *artworkId);
|
||||||
|
|
||||||
|
// Using track.modify() is quite CPU intensive as the track class has too many fields
|
||||||
|
db::Track::updatePreferredArtwork(session, track->getId(), artwork->getId());
|
||||||
|
if (artwork)
|
||||||
|
LMS_LOG(DBUPDATER, DEBUG, "Updated preferred artwork in track " << track->getAbsoluteFilePath() << " with image in " << utils::toPath(session, artwork->getId()));
|
||||||
|
else
|
||||||
|
LMS_LOG(DBUPDATER, DEBUG, "Removed preferred artwork from track " << track->getAbsoluteFilePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateTrackPreferredMediaArtwork(db::Session& session, db::Track::pointer& track, TrackArtwork preferredArtwork)
|
||||||
|
{
|
||||||
|
db::Artwork::pointer artwork;
|
||||||
|
if (const db::TrackEmbeddedImageId * trackEmbeddedImageId{ std::get_if<db::TrackEmbeddedImageId>(&preferredArtwork) })
|
||||||
|
artwork = utils::getOrCreateArtworkFromTrackEmbeddedImage(session, *trackEmbeddedImageId);
|
||||||
|
else if (const db::ArtworkId * artworkId{ std::get_if<db::ArtworkId>(&preferredArtwork) })
|
||||||
|
artwork = db::Artwork::find(session, *artworkId);
|
||||||
|
|
||||||
|
// Using track.modify() is quite CPU intensive as the track class has too many fields
|
||||||
|
db::Track::updatePreferredMediaArtwork(session, track->getId(), artwork->getId());
|
||||||
|
if (artwork)
|
||||||
|
LMS_LOG(DBUPDATER, DEBUG, "Updated preferred media artwork in track '" << track->getAbsoluteFilePath() << "' with image in " << utils::toPath(session, artwork->getId()));
|
||||||
|
else
|
||||||
|
LMS_LOG(DBUPDATER, DEBUG, "Removed preferred media artwork from track '" << track->getAbsoluteFilePath() << "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateTrackPreferredArtworks(db::Session& session, const TrackArtworksAssociation& TrackArtworksAssociation)
|
||||||
|
{
|
||||||
|
db::Track::pointer track{ TrackArtworksAssociation.track };
|
||||||
|
|
||||||
|
{
|
||||||
|
const db::Artwork::pointer currentPreferredArtwork{ track->getPreferredArtwork() };
|
||||||
|
if (!isSameArtwork(TrackArtworksAssociation.preferredArtwork, currentPreferredArtwork))
|
||||||
|
updateTrackPreferredArtwork(session, track, TrackArtworksAssociation.preferredArtwork);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const db::Artwork::pointer currentPreferredMediaArtwork{ track->getPreferredMediaArtwork() };
|
||||||
|
if (!isSameArtwork(TrackArtworksAssociation.preferredMediaArtwork, currentPreferredMediaArtwork))
|
||||||
|
updateTrackPreferredMediaArtwork(session, track, TrackArtworksAssociation.preferredMediaArtwork);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateTrackPreferredArtworks(db::Session& session, TrackArtworksAssociationContainer& imageAssociations)
|
||||||
|
{
|
||||||
|
constexpr std::size_t writeBatchSize{ 50 };
|
||||||
|
|
||||||
|
while (!imageAssociations.empty())
|
||||||
|
{
|
||||||
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
|
||||||
|
for (std::size_t i{}; !imageAssociations.empty() && i < writeBatchSize; ++i)
|
||||||
|
{
|
||||||
|
updateTrackPreferredArtworks(session, imageAssociations.front());
|
||||||
|
imageAssociations.pop_front();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ScanStepAssociateTrackImages::ScanStepAssociateTrackImages(InitParams& initParams)
|
||||||
|
: ScanStepBase{ initParams }
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ScanStepAssociateTrackImages::needProcess(const ScanContext& context) const
|
||||||
|
{
|
||||||
|
return context.stats.nbChanges() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScanStepAssociateTrackImages::process(ScanContext& context)
|
||||||
|
{
|
||||||
|
auto& session{ _db.getTLSSession() };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
context.currentStepStats.totalElems = db::Track::getCount(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchTrackArtworkContext searchContext{
|
||||||
|
.session = session,
|
||||||
|
.lastRetrievedTrackId = {},
|
||||||
|
};
|
||||||
|
|
||||||
|
TrackArtworksAssociationContainer TrackArtworksAssociations;
|
||||||
|
while (fetchNextTrackArtworksToUpdate(searchContext, TrackArtworksAssociations))
|
||||||
|
{
|
||||||
|
if (_abortScan)
|
||||||
|
return;
|
||||||
|
|
||||||
|
updateTrackPreferredArtworks(session, TrackArtworksAssociations);
|
||||||
|
context.currentStepStats.processedElems = searchContext.processedTrackCount;
|
||||||
|
_progressCallback(context.currentStepStats);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace lms::scanner
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* 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 "ScanStepBase.hpp"
|
||||||
|
|
||||||
|
namespace lms::scanner
|
||||||
|
{
|
||||||
|
class ScanStepAssociateTrackImages : public ScanStepBase
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ScanStepAssociateTrackImages(InitParams& initParams);
|
||||||
|
~ScanStepAssociateTrackImages() override = default;
|
||||||
|
ScanStepAssociateTrackImages(const ScanStepAssociateTrackImages&) = delete;
|
||||||
|
ScanStepAssociateTrackImages& operator=(const ScanStepAssociateTrackImages&) = delete;
|
||||||
|
|
||||||
|
private:
|
||||||
|
ScanStep getStep() const override { return ScanStep::AssociateTrackImages; }
|
||||||
|
core::LiteralString getStepName() const override { return "Associate track images"; }
|
||||||
|
bool needProcess(const ScanContext& context) const override;
|
||||||
|
void process(ScanContext& context) override;
|
||||||
|
};
|
||||||
|
} // namespace lms::scanner
|
||||||
@@ -49,6 +49,7 @@ namespace lms::scanner
|
|||||||
AssociateExternalLyrics,
|
AssociateExternalLyrics,
|
||||||
AssociatePlayListTracks,
|
AssociatePlayListTracks,
|
||||||
AssociateReleaseImages,
|
AssociateReleaseImages,
|
||||||
|
AssociateTrackImages,
|
||||||
CheckForDuplicatedFiles,
|
CheckForDuplicatedFiles,
|
||||||
CheckForRemovedFiles,
|
CheckForRemovedFiles,
|
||||||
ComputeClusterStats,
|
ComputeClusterStats,
|
||||||
|
|||||||
@@ -22,8 +22,7 @@
|
|||||||
#include <cassert>
|
#include <cassert>
|
||||||
|
|
||||||
#include "core/String.hpp"
|
#include "core/String.hpp"
|
||||||
#include "database/ImageId.hpp"
|
#include "database/ArtworkId.hpp"
|
||||||
#include "database/TrackEmbeddedImageId.hpp"
|
|
||||||
|
|
||||||
namespace lms::api::subsonic
|
namespace lms::api::subsonic
|
||||||
{
|
{
|
||||||
@@ -31,14 +30,9 @@ namespace lms::api::subsonic
|
|||||||
{
|
{
|
||||||
constexpr char timestampSeparatorChar{ '-' };
|
constexpr char timestampSeparatorChar{ '-' };
|
||||||
|
|
||||||
std::string idToString(db::ImageId id)
|
std::string idToString(db::ArtworkId id)
|
||||||
{
|
{
|
||||||
return "im-" + id.toString();
|
return "art-" + id.toString();
|
||||||
}
|
|
||||||
|
|
||||||
std::string idToString(db::TrackEmbeddedImageId id)
|
|
||||||
{
|
|
||||||
return "trim-" + id.toString();
|
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@@ -46,19 +40,10 @@ namespace lms::api::subsonic
|
|||||||
{
|
{
|
||||||
std::string res;
|
std::string res;
|
||||||
|
|
||||||
// produce "type-id[-timestamp]"
|
// produce "art-id-timestamp"
|
||||||
if (db::TrackEmbeddedImageId * imageId{ std::get_if<db::TrackEmbeddedImageId>(&coverId.id) })
|
res = idToString(coverId.id);
|
||||||
{
|
res += timestampSeparatorChar;
|
||||||
res = idToString(*imageId);
|
res += std::to_string(coverId.timestamp);
|
||||||
assert(!coverId.timestamp.has_value());
|
|
||||||
}
|
|
||||||
else if (db::ImageId * imageId{ std::get_if<db::ImageId>(&coverId.id) })
|
|
||||||
{
|
|
||||||
res = idToString(*imageId);
|
|
||||||
res += timestampSeparatorChar;
|
|
||||||
assert(coverId.timestamp.has_value());
|
|
||||||
res += std::to_string(*coverId.timestamp);
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(!res.empty());
|
assert(!res.empty());
|
||||||
return res;
|
return res;
|
||||||
@@ -74,30 +59,20 @@ namespace lms::core::stringUtils
|
|||||||
std::optional<api::subsonic::CoverArtId> res;
|
std::optional<api::subsonic::CoverArtId> res;
|
||||||
|
|
||||||
std::vector<std::string_view> values{ core::stringUtils::splitString(str, '-') };
|
std::vector<std::string_view> values{ core::stringUtils::splitString(str, '-') };
|
||||||
if (values.size() <= 1)
|
|
||||||
|
// expect "art-id-timestamp"
|
||||||
|
if (values.size() != 3)
|
||||||
|
return res;
|
||||||
|
|
||||||
|
if (values[0] != "art")
|
||||||
|
return res;
|
||||||
|
|
||||||
|
const auto value{ core::stringUtils::readAs<db::ArtworkId::ValueType>(values[1]) };
|
||||||
|
const auto timestamp{ core::stringUtils::readAs<std::time_t>(values[2]) };
|
||||||
|
if (!value || !timestamp)
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
|
|
||||||
if (values[0] == "trim")
|
res.emplace(api::subsonic::CoverArtId{ *value, *timestamp });
|
||||||
{
|
|
||||||
// expect "trim-id"
|
|
||||||
if (values.size() == 2)
|
|
||||||
{
|
|
||||||
if (const auto value{ core::stringUtils::readAs<db::TrackEmbeddedImageId::ValueType>(values[1]) })
|
|
||||||
res.emplace(db::TrackEmbeddedImageId{ *value });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (values[0] == "im")
|
|
||||||
{
|
|
||||||
// expect "im-id-timestamp"
|
|
||||||
if (values.size() == 3)
|
|
||||||
{
|
|
||||||
const auto imageId{ core::stringUtils::readAs<db::ImageId::ValueType>(values[1]) };
|
|
||||||
const auto timestamp{ core::stringUtils::readAs<std::time_t>(values[2]) };
|
|
||||||
|
|
||||||
if (imageId && timestamp)
|
|
||||||
res.emplace(db::ImageId{ *imageId }, *timestamp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,24 +20,16 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <ctime>
|
#include <ctime>
|
||||||
#include <variant>
|
|
||||||
|
|
||||||
#include "core/String.hpp"
|
#include "core/String.hpp"
|
||||||
#include "database/ImageId.hpp"
|
#include "database/ArtworkId.hpp"
|
||||||
#include "database/TrackEmbeddedImageId.hpp"
|
|
||||||
|
|
||||||
namespace lms::api::subsonic
|
namespace lms::api::subsonic
|
||||||
{
|
{
|
||||||
struct CoverArtId
|
struct CoverArtId
|
||||||
{
|
{
|
||||||
std::variant<db::ImageId, db::TrackEmbeddedImageId> id;
|
db::ArtworkId id;
|
||||||
std::optional<std::time_t> timestamp;
|
std::time_t timestamp;
|
||||||
|
|
||||||
CoverArtId(db::ImageId _id, std::time_t _timestamp)
|
|
||||||
: id{ _id }
|
|
||||||
, timestamp{ _timestamp } {}
|
|
||||||
CoverArtId(db::TrackEmbeddedImageId _id)
|
|
||||||
: id{ _id } {}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
std::string idToString(CoverArtId coverId);
|
std::string idToString(CoverArtId coverId);
|
||||||
@@ -46,9 +38,6 @@ namespace lms::api::subsonic
|
|||||||
// Used to parse parameters
|
// Used to parse parameters
|
||||||
namespace lms::core::stringUtils
|
namespace lms::core::stringUtils
|
||||||
{
|
{
|
||||||
template<>
|
|
||||||
std::optional<db::ImageId> readAs(std::string_view str);
|
|
||||||
|
|
||||||
template<>
|
template<>
|
||||||
std::optional<api::subsonic::CoverArtId> readAs(std::string_view str);
|
std::optional<api::subsonic::CoverArtId> readAs(std::string_view str);
|
||||||
} // namespace lms::core::stringUtils
|
} // namespace lms::core::stringUtils
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
|
|
||||||
#include "AlbumSongLists.hpp"
|
#include "AlbumSongLists.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
#include "core/Service.hpp"
|
#include "core/Service.hpp"
|
||||||
#include "database/Artist.hpp"
|
#include "database/Artist.hpp"
|
||||||
#include "database/Cluster.hpp"
|
#include "database/Cluster.hpp"
|
||||||
|
|||||||
@@ -336,12 +336,7 @@ namespace lms::api::subsonic
|
|||||||
if (size)
|
if (size)
|
||||||
*size = core::utils::clamp(*size, std::size_t{ 32 }, std::size_t{ 2048 });
|
*size = core::utils::clamp(*size, std::size_t{ 32 }, std::size_t{ 2048 });
|
||||||
|
|
||||||
std::shared_ptr<image::IEncodedImage> image;
|
std::shared_ptr<image::IEncodedImage> image{ core::Service<artwork::IArtworkService>::get()->getImage(coverArtId.id, size) };
|
||||||
if (const db::TrackEmbeddedImageId * trackEmbeddedImageId{ std::get_if<db::TrackEmbeddedImageId>(&coverArtId.id) })
|
|
||||||
image = core::Service<cover::IArtworkService>::get()->getTrackEmbeddedImage(*trackEmbeddedImageId, size);
|
|
||||||
else if (const db::ImageId * imageId{ std::get_if<db::ImageId>(&coverArtId.id) })
|
|
||||||
image = core::Service<cover::IArtworkService>::get()->getImage(*imageId, size);
|
|
||||||
|
|
||||||
if (!image)
|
if (!image)
|
||||||
{
|
{
|
||||||
response.setStatus(404);
|
response.setStatus(404);
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ namespace lms::api::subsonic
|
|||||||
Response::Node& playlistsNode{ response.createNode("playlists") };
|
Response::Node& playlistsNode{ response.createNode("playlists") };
|
||||||
|
|
||||||
auto addTrackList{ [&](const db::TrackList::pointer& trackList) {
|
auto addTrackList{ [&](const db::TrackList::pointer& trackList) {
|
||||||
playlistsNode.addArrayChild("playlist", createPlaylistNode(trackList, context.dbSession));
|
playlistsNode.addArrayChild("playlist", createPlaylistNode(context, trackList));
|
||||||
} };
|
} };
|
||||||
|
|
||||||
// First add user's playlists
|
// First add user's playlists
|
||||||
@@ -104,7 +104,7 @@ namespace lms::api::subsonic
|
|||||||
throw RequestedDataNotFoundError{};
|
throw RequestedDataNotFoundError{};
|
||||||
|
|
||||||
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||||
Response::Node playlistNode{ createPlaylistNode(trackList, context.dbSession) };
|
Response::Node playlistNode{ createPlaylistNode(context, trackList) };
|
||||||
|
|
||||||
auto entries{ trackList->getEntries() };
|
auto entries{ trackList->getEntries() };
|
||||||
for (const TrackListEntry::pointer& entry : entries.results)
|
for (const TrackListEntry::pointer& entry : entries.results)
|
||||||
@@ -157,7 +157,7 @@ namespace lms::api::subsonic
|
|||||||
}
|
}
|
||||||
|
|
||||||
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||||
Response::Node playlistNode{ createPlaylistNode(trackList, context.dbSession) };
|
Response::Node playlistNode{ createPlaylistNode(context, trackList) };
|
||||||
|
|
||||||
auto entries{ trackList->getEntries() };
|
auto entries{ trackList->getEntries() };
|
||||||
for (const TrackListEntry::pointer& entry : entries.results)
|
for (const TrackListEntry::pointer& entry : entries.results)
|
||||||
|
|||||||
@@ -23,12 +23,11 @@
|
|||||||
#include "core/Service.hpp"
|
#include "core/Service.hpp"
|
||||||
#include "core/String.hpp"
|
#include "core/String.hpp"
|
||||||
#include "database/Artist.hpp"
|
#include "database/Artist.hpp"
|
||||||
|
#include "database/Artwork.hpp"
|
||||||
#include "database/Cluster.hpp"
|
#include "database/Cluster.hpp"
|
||||||
#include "database/Directory.hpp"
|
#include "database/Directory.hpp"
|
||||||
#include "database/Image.hpp"
|
|
||||||
#include "database/Release.hpp"
|
#include "database/Release.hpp"
|
||||||
#include "database/Track.hpp"
|
#include "database/Track.hpp"
|
||||||
#include "database/TrackEmbeddedImage.hpp"
|
|
||||||
#include "database/Types.hpp"
|
#include "database/Types.hpp"
|
||||||
#include "database/User.hpp"
|
#include "database/User.hpp"
|
||||||
#include "services/feedback/IFeedbackService.hpp"
|
#include "services/feedback/IFeedbackService.hpp"
|
||||||
@@ -87,24 +86,13 @@ namespace lms::api::subsonic
|
|||||||
}
|
}
|
||||||
|
|
||||||
albumNode.setAttribute("created", core::stringUtils::toISO8601String(release->getAddedTime()));
|
albumNode.setAttribute("created", core::stringUtils::toISO8601String(release->getAddedTime()));
|
||||||
if (const auto image{ release->getImage() })
|
|
||||||
|
if (const auto artwork{ release->getPreferredArtwork() })
|
||||||
{
|
{
|
||||||
const CoverArtId coverArtId{ image->getId(), image->getLastWriteTime().toTime_t() };
|
CoverArtId coverArtId{ artwork->getId(), artwork->getLastWrittenTime().toTime_t() };
|
||||||
albumNode.setAttribute("coverArt", idToString(coverArtId));
|
albumNode.setAttribute("coverArt", idToString(coverArtId));
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
db::TrackEmbeddedImage::FindParameters params;
|
|
||||||
params.setRelease(release->getId());
|
|
||||||
params.setIsPreferred(true);
|
|
||||||
params.setSortMethod(db::TrackEmbeddedImageSortMethod::FrontCoverAndSize);
|
|
||||||
params.setRange(db::Range{ 0, 1 });
|
|
||||||
|
|
||||||
db::TrackEmbeddedImage::find(context.dbSession, params, [&](const db::TrackEmbeddedImage::pointer& image) {
|
|
||||||
const CoverArtId coverArtId{ image->getId() };
|
|
||||||
albumNode.setAttribute("coverArt", idToString(coverArtId));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (const auto originalYear{ release->getOriginalYear() })
|
if (const auto originalYear{ release->getOriginalYear() })
|
||||||
albumNode.setAttribute("year", *originalYear);
|
albumNode.setAttribute("year", *originalYear);
|
||||||
else if (const auto year{ release->getYear() })
|
else if (const auto year{ release->getYear() })
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
#include "core/Service.hpp"
|
#include "core/Service.hpp"
|
||||||
#include "core/String.hpp"
|
#include "core/String.hpp"
|
||||||
#include "database/Artist.hpp"
|
#include "database/Artist.hpp"
|
||||||
#include "database/Image.hpp"
|
#include "database/Artwork.hpp"
|
||||||
#include "database/Release.hpp"
|
#include "database/Release.hpp"
|
||||||
#include "database/TrackArtistLink.hpp"
|
#include "database/TrackArtistLink.hpp"
|
||||||
#include "database/User.hpp"
|
#include "database/User.hpp"
|
||||||
@@ -95,9 +95,9 @@ namespace lms::api::subsonic
|
|||||||
|
|
||||||
artistNode.setAttribute("id", idToString(artist->getId()));
|
artistNode.setAttribute("id", idToString(artist->getId()));
|
||||||
artistNode.setAttribute("name", artist->getName());
|
artistNode.setAttribute("name", artist->getName());
|
||||||
if (const auto image{ artist->getImage() })
|
if (const auto artwork{ artist->getPreferredArtwork() })
|
||||||
{
|
{
|
||||||
const CoverArtId coverArtId{ image->getId(), image->getLastWriteTime().toTime_t() };
|
CoverArtId coverArtId{ artwork->getId(), artwork->getLastWrittenTime().toTime_t() };
|
||||||
artistNode.setAttribute("coverArt", idToString(coverArtId));
|
artistNode.setAttribute("coverArt", idToString(coverArtId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,17 +19,20 @@
|
|||||||
|
|
||||||
#include "Playlist.hpp"
|
#include "Playlist.hpp"
|
||||||
|
|
||||||
|
#include "core/Service.hpp"
|
||||||
#include "core/String.hpp"
|
#include "core/String.hpp"
|
||||||
#include "database/TrackEmbeddedImage.hpp"
|
#include "database/Artwork.hpp"
|
||||||
#include "database/TrackList.hpp"
|
#include "database/TrackList.hpp"
|
||||||
#include "database/User.hpp"
|
#include "database/User.hpp"
|
||||||
|
#include "services/artwork/IArtworkService.hpp"
|
||||||
|
|
||||||
#include "CoverArtId.hpp"
|
#include "CoverArtId.hpp"
|
||||||
|
#include "RequestContext.hpp"
|
||||||
#include "SubsonicId.hpp"
|
#include "SubsonicId.hpp"
|
||||||
|
|
||||||
namespace lms::api::subsonic
|
namespace lms::api::subsonic
|
||||||
{
|
{
|
||||||
Response::Node createPlaylistNode(const db::TrackList::pointer& tracklist, db::Session& session)
|
Response::Node createPlaylistNode(RequestContext& context, const db::TrackList::pointer& tracklist)
|
||||||
{
|
{
|
||||||
Response::Node playlistNode;
|
Response::Node playlistNode;
|
||||||
|
|
||||||
@@ -43,16 +46,14 @@ namespace lms::api::subsonic
|
|||||||
if (const db::User::pointer user{ tracklist->getUser() })
|
if (const db::User::pointer user{ tracklist->getUser() })
|
||||||
playlistNode.setAttribute("owner", user->getLoginName());
|
playlistNode.setAttribute("owner", user->getLoginName());
|
||||||
|
|
||||||
db::TrackEmbeddedImage::FindParameters params;
|
if (const db::ArtworkId artworkId{ core::Service<artwork::IArtworkService>::get()->findTrackListImage(tracklist->getId()) }; artworkId.isValid())
|
||||||
params.setTrackList(tracklist->getId());
|
{
|
||||||
params.setIsPreferred(true);
|
if (const auto artwork{ db::Artwork::find(context.dbSession, artworkId) })
|
||||||
params.setSortMethod(db::TrackEmbeddedImageSortMethod::FrontCoverAndSize);
|
{
|
||||||
params.setRange(db::Range{ .offset = 0, .size = 1 });
|
CoverArtId coverArtId{ artwork->getId(), artwork->getLastWrittenTime().toTime_t() };
|
||||||
|
playlistNode.setAttribute("coverArt", idToString(coverArtId));
|
||||||
db::TrackEmbeddedImage::find(session, params, [&](const db::TrackEmbeddedImage::pointer& image) {
|
}
|
||||||
const CoverArtId coverArtId{ image->getId() };
|
}
|
||||||
playlistNode.setAttribute("coverArt", idToString(coverArtId));
|
|
||||||
});
|
|
||||||
|
|
||||||
return playlistNode;
|
return playlistNode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,10 +26,11 @@
|
|||||||
namespace lms::db
|
namespace lms::db
|
||||||
{
|
{
|
||||||
class TrackList;
|
class TrackList;
|
||||||
class Session;
|
|
||||||
} // namespace lms::db
|
} // namespace lms::db
|
||||||
|
|
||||||
namespace lms::api::subsonic
|
namespace lms::api::subsonic
|
||||||
{
|
{
|
||||||
Response::Node createPlaylistNode(const db::ObjectPtr<db::TrackList>& tracklist, db::Session& session);
|
struct RequestContext;
|
||||||
}
|
|
||||||
|
Response::Node createPlaylistNode(RequestContext& context, const db::ObjectPtr<db::TrackList>& tracklist);
|
||||||
|
} // namespace lms::api::subsonic
|
||||||
@@ -21,19 +21,17 @@
|
|||||||
|
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
|
||||||
#include "av/IAudioFile.hpp"
|
|
||||||
#include "core/ITraceLogger.hpp"
|
#include "core/ITraceLogger.hpp"
|
||||||
#include "core/MimeTypes.hpp"
|
#include "core/MimeTypes.hpp"
|
||||||
#include "core/Service.hpp"
|
#include "core/Service.hpp"
|
||||||
#include "core/String.hpp"
|
#include "core/String.hpp"
|
||||||
#include "database/Artist.hpp"
|
#include "database/Artist.hpp"
|
||||||
|
#include "database/Artwork.hpp"
|
||||||
#include "database/Cluster.hpp"
|
#include "database/Cluster.hpp"
|
||||||
#include "database/Directory.hpp"
|
#include "database/Directory.hpp"
|
||||||
#include "database/Image.hpp"
|
|
||||||
#include "database/Release.hpp"
|
#include "database/Release.hpp"
|
||||||
#include "database/Track.hpp"
|
#include "database/Track.hpp"
|
||||||
#include "database/TrackArtistLink.hpp"
|
#include "database/TrackArtistLink.hpp"
|
||||||
#include "database/TrackEmbeddedImage.hpp"
|
|
||||||
#include "database/Types.hpp"
|
#include "database/Types.hpp"
|
||||||
#include "database/User.hpp"
|
#include "database/User.hpp"
|
||||||
#include "services/feedback/IFeedbackService.hpp"
|
#include "services/feedback/IFeedbackService.hpp"
|
||||||
@@ -111,28 +109,14 @@ namespace lms::api::subsonic
|
|||||||
trackResponse.setAttribute("transcodedContentType", core::getMimeType(std::filesystem::path{ "." + fileSuffix }));
|
trackResponse.setAttribute("transcodedContentType", core::getMimeType(std::filesystem::path{ "." + fileSuffix }));
|
||||||
}
|
}
|
||||||
|
|
||||||
const Release::pointer release{ track->getRelease() };
|
auto artwork{ track->getPreferredMediaArtwork() };
|
||||||
|
if (!artwork)
|
||||||
|
artwork = track->getPreferredArtwork();
|
||||||
|
|
||||||
|
if (artwork)
|
||||||
{
|
{
|
||||||
TrackEmbeddedImage::FindParameters params;
|
CoverArtId coverArtId{ artwork->getId(), artwork->getLastWrittenTime().toTime_t() };
|
||||||
params.setTrack(track->getId());
|
trackResponse.setAttribute("coverArt", idToString(coverArtId));
|
||||||
params.setIsPreferred(true);
|
|
||||||
params.setRange(Range{ .offset = 0, .size = 1 });
|
|
||||||
|
|
||||||
bool hasEmbeddedImage{};
|
|
||||||
TrackEmbeddedImage::find(context.dbSession, params, [&](const TrackEmbeddedImage::pointer& image) {
|
|
||||||
const CoverArtId coverArtId{ image->getId() };
|
|
||||||
trackResponse.setAttribute("coverArt", idToString(coverArtId));
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!hasEmbeddedImage && release)
|
|
||||||
{
|
|
||||||
if (const db::Image::pointer image{ release->getImage() })
|
|
||||||
{
|
|
||||||
const CoverArtId coverArtId{ image->getId(), image->getLastWriteTime().toTime_t() };
|
|
||||||
trackResponse.setAttribute("coverArt", idToString(coverArtId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::vector<Artist::pointer>& artists{ track->getArtists({ TrackArtistLinkType::Artist }) };
|
const std::vector<Artist::pointer>& artists{ track->getArtists({ TrackArtistLinkType::Artist }) };
|
||||||
@@ -147,6 +131,7 @@ namespace lms::api::subsonic
|
|||||||
trackResponse.setAttribute("artistId", idToString(artists.front()->getId()));
|
trackResponse.setAttribute("artistId", idToString(artists.front()->getId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Release::pointer release{ track->getRelease() };
|
||||||
if (release)
|
if (release)
|
||||||
{
|
{
|
||||||
trackResponse.setAttribute("album", release->getName());
|
trackResponse.setAttribute("album", release->getName());
|
||||||
|
|||||||
+1
-1
@@ -370,7 +370,7 @@ namespace lms
|
|||||||
}
|
}
|
||||||
|
|
||||||
image::init(argv[0]);
|
image::init(argv[0]);
|
||||||
core::Service<cover::IArtworkService> artworkService{ cover::createArtworkService(database, server.appRoot() + "/images/unknown-cover.svg", server.appRoot() + "/images/unknown-artist.svg") };
|
core::Service<artwork::IArtworkService> artworkService{ artwork::createArtworkService(database, server.appRoot() + "/images/unknown-cover.svg", server.appRoot() + "/images/unknown-artist.svg") };
|
||||||
core::Service<recommendation::IRecommendationService> recommendationService{ recommendation::createRecommendationService(database) };
|
core::Service<recommendation::IRecommendationService> recommendationService{ recommendation::createRecommendationService(database) };
|
||||||
core::Service<recommendation::IPlaylistGeneratorService> playlistGeneratorService{ recommendation::createPlaylistGeneratorService(database, *recommendationService.get()) };
|
core::Service<recommendation::IPlaylistGeneratorService> playlistGeneratorService{ recommendation::createPlaylistGeneratorService(database, *recommendationService.get()) };
|
||||||
core::Service<scanner::IScannerService> scannerService{ scanner::createScannerService(database) };
|
core::Service<scanner::IScannerService> scannerService{ scanner::createScannerService(database) };
|
||||||
|
|||||||
@@ -251,12 +251,26 @@ namespace lms::ui
|
|||||||
<< " replayGain: " << replayGain << ","
|
<< " replayGain: " << replayGain << ","
|
||||||
<< " title: \"" << core::stringUtils::jsEscape(track->getName()) << "\","
|
<< " title: \"" << core::stringUtils::jsEscape(track->getName()) << "\","
|
||||||
<< " artist: \"" << (!artists.empty() ? core::stringUtils::jsEscape(track->getArtistDisplayName()) : "") << "\","
|
<< " artist: \"" << (!artists.empty() ? core::stringUtils::jsEscape(track->getArtistDisplayName()) : "") << "\","
|
||||||
<< " release: \"" << (track->getRelease() ? core::stringUtils::jsEscape(track->getRelease()->getName()) : "") << "\","
|
<< " release: \"" << (track->getRelease() ? core::stringUtils::jsEscape(track->getRelease()->getName()) : "") << "\",";
|
||||||
<< " artwork: ["
|
|
||||||
<< " { src: \"" << LmsApp->getArtworkResource()->getPreferredTrackImageUrl(trackId, ArtworkResource::Size::Small) << "\", sizes: \"128x128\", type: \"image/jpeg\" },"
|
db::ArtworkId artworkId{ track->getPreferredMediaArtworkId() };
|
||||||
<< " { src: \"" << LmsApp->getArtworkResource()->getPreferredTrackImageUrl(trackId, ArtworkResource::Size::Large) << "\", sizes: \"512x512\", type: \"image/jpeg\" },"
|
if (!artworkId.isValid())
|
||||||
<< " ]"
|
artworkId = track->getPreferredArtworkId();
|
||||||
<< "};";
|
if (artworkId.isValid())
|
||||||
|
{
|
||||||
|
oss << " artwork: ["
|
||||||
|
<< " { src: \"" << LmsApp->getArtworkResource()->getArtworkUrl(artworkId, ArtworkResource::Size::Small) << "\", sizes: \"128x128\", type: \"image/jpeg\" },"
|
||||||
|
<< " { src: \"" << LmsApp->getArtworkResource()->getArtworkUrl(artworkId, ArtworkResource::Size::Large) << "\", sizes: \"512x512\", type: \"image/jpeg\" },"
|
||||||
|
<< " ]";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
oss << " artwork: ["
|
||||||
|
<< " { src: \"" << LmsApp->getArtworkResource()->getDefaultTrackArtworkUrl() << "\", type: \"image/svg+xml\" },"
|
||||||
|
<< " ]";
|
||||||
|
}
|
||||||
|
oss << "};";
|
||||||
|
|
||||||
// Update 'sizes' above to match this:
|
// Update 'sizes' above to match this:
|
||||||
static_assert(static_cast<std::underlying_type_t<ArtworkResource::Size>>(ArtworkResource::Size::Small) == 128);
|
static_assert(static_cast<std::underlying_type_t<ArtworkResource::Size>>(ArtworkResource::Size::Small) == 128);
|
||||||
static_assert(static_cast<std::underlying_type_t<ArtworkResource::Size>>(ArtworkResource::Size::Large) == 512);
|
static_assert(static_cast<std::underlying_type_t<ArtworkResource::Size>>(ArtworkResource::Size::Large) == 512);
|
||||||
|
|||||||
@@ -490,8 +490,18 @@ namespace lms::ui
|
|||||||
entry->bindWidget("artists-md", utils::createArtistAnchorList(artists));
|
entry->bindWidget("artists-md", utils::createArtistAnchorList(artists));
|
||||||
}
|
}
|
||||||
|
|
||||||
auto image{ utils::createTrackImage(track->getId(), ArtworkResource::Size::Small) };
|
db::ArtworkId artworkId{ track->getPreferredMediaArtworkId() };
|
||||||
image->addStyleClass("Lms-cover-track rounded");
|
if (!artworkId.isValid())
|
||||||
|
artworkId = track->getPreferredArtworkId();
|
||||||
|
|
||||||
|
std::unique_ptr<Wt::WImage> image;
|
||||||
|
if (artworkId.isValid())
|
||||||
|
image = utils::createArtworkImage(artworkId, ArtworkResource::Size::Small);
|
||||||
|
else
|
||||||
|
image = utils::createDefaultTrackArtworkImage();
|
||||||
|
|
||||||
|
image->addStyleClass("Lms-cover-track rounded"); // HACK
|
||||||
|
|
||||||
if (const auto release{ track->getRelease() })
|
if (const auto release{ track->getRelease() })
|
||||||
{
|
{
|
||||||
entry->setCondition("if-has-release", true);
|
entry->setCondition("if-has-release", true);
|
||||||
|
|||||||
+29
-17
@@ -40,6 +40,18 @@
|
|||||||
|
|
||||||
namespace lms::ui::utils
|
namespace lms::ui::utils
|
||||||
{
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
std::unique_ptr<Wt::WImage> createArtworkImage()
|
||||||
|
{
|
||||||
|
auto image{ std::make_unique<Wt::WImage>() };
|
||||||
|
image->setStyleClass("Lms-cover img-fluid"); // HACK
|
||||||
|
image->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)"); // HACK
|
||||||
|
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
std::string durationToString(std::chrono::milliseconds msDuration)
|
std::string durationToString(std::chrono::milliseconds msDuration)
|
||||||
{
|
{
|
||||||
const std::chrono::seconds duration{ std::chrono::duration_cast<std::chrono::seconds>(msDuration) };
|
const std::chrono::seconds duration{ std::chrono::duration_cast<std::chrono::seconds>(msDuration) };
|
||||||
@@ -76,32 +88,32 @@ namespace lms::ui::utils
|
|||||||
LmsApp->getModalManager().show(std::move(rawImage));
|
LmsApp->getModalManager().show(std::move(rawImage));
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<Wt::WImage> createArtistImage(db::ArtistId artistId, ArtworkResource::Size size)
|
std::unique_ptr<Wt::WImage> createArtworkImage(db::ArtworkId artworkId, ArtworkResource::Size size)
|
||||||
{
|
{
|
||||||
auto image{ std::make_unique<Wt::WImage>() };
|
auto image{ createArtworkImage() };
|
||||||
image->setImageLink(LmsApp->getArtworkResource()->getArtistImageUrl(artistId, size));
|
image->setImageLink(LmsApp->getArtworkResource()->getArtworkUrl(artworkId, size));
|
||||||
image->setStyleClass("Lms-cover img-fluid"); // HACK
|
|
||||||
image->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)"); // HACK
|
|
||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<Wt::WImage> createReleaseCover(db::ReleaseId releaseId, ArtworkResource::Size size)
|
std::unique_ptr<Wt::WImage> createDefaultArtistArtworkImage()
|
||||||
{
|
{
|
||||||
auto cover{ std::make_unique<Wt::WImage>() };
|
auto image{ createArtworkImage() };
|
||||||
cover->setImageLink(LmsApp->getArtworkResource()->getReleaseCoverUrl(releaseId, size));
|
image->setImageLink(LmsApp->getArtworkResource()->getDefaultArtistArtworkUrl());
|
||||||
cover->setStyleClass("Lms-cover img-fluid"); // HACK
|
return image;
|
||||||
cover->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)"); // HACK
|
|
||||||
return cover;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<Wt::WImage> createTrackImage(db::TrackId trackId, ArtworkResource::Size size)
|
std::unique_ptr<Wt::WImage> createDefaultReleaseArtworkImage()
|
||||||
{
|
{
|
||||||
auto cover{ std::make_unique<Wt::WImage>() };
|
auto image{ createArtworkImage() };
|
||||||
cover->setImageLink(LmsApp->getArtworkResource()->getPreferredTrackImageUrl(trackId, size));
|
image->setImageLink(LmsApp->getArtworkResource()->getDefaultReleaseArtworkUrl());
|
||||||
cover->setStyleClass("Lms-cover img-fluid"); // HACK
|
return image;
|
||||||
cover->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)"); // HACK
|
}
|
||||||
|
|
||||||
return cover;
|
std::unique_ptr<Wt::WImage> createDefaultTrackArtworkImage()
|
||||||
|
{
|
||||||
|
auto image{ createArtworkImage() };
|
||||||
|
image->setImageLink(LmsApp->getArtworkResource()->getDefaultTrackArtworkUrl());
|
||||||
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<Wt::WInteractWidget> createFilter(const Wt::WString& name, const Wt::WString& tooltip, std::string_view colorStyleClass, bool canDelete)
|
std::unique_ptr<Wt::WInteractWidget> createFilter(const Wt::WString& name, const Wt::WString& tooltip, std::string_view colorStyleClass, bool canDelete)
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
#include "database/ReleaseId.hpp"
|
#include "database/ReleaseId.hpp"
|
||||||
#include "database/TrackId.hpp"
|
#include "database/TrackId.hpp"
|
||||||
|
|
||||||
|
#include "database/Types.hpp"
|
||||||
#include "resource/ArtworkResource.hpp"
|
#include "resource/ArtworkResource.hpp"
|
||||||
|
|
||||||
namespace lms::db
|
namespace lms::db
|
||||||
@@ -56,9 +57,10 @@ namespace lms::ui::utils
|
|||||||
|
|
||||||
void showArtworkModal(Wt::WLink image);
|
void showArtworkModal(Wt::WLink image);
|
||||||
|
|
||||||
std::unique_ptr<Wt::WImage> createReleaseCover(db::ReleaseId releaseId, ArtworkResource::Size size);
|
std::unique_ptr<Wt::WImage> createArtworkImage(db::ArtworkId artworkId, ArtworkResource::Size size);
|
||||||
std::unique_ptr<Wt::WImage> createTrackImage(db::TrackId trackId, ArtworkResource::Size size);
|
std::unique_ptr<Wt::WImage> createDefaultArtistArtworkImage();
|
||||||
std::unique_ptr<Wt::WImage> createArtistImage(db::ArtistId artistId, ArtworkResource::Size size);
|
std::unique_ptr<Wt::WImage> createDefaultReleaseArtworkImage();
|
||||||
|
std::unique_ptr<Wt::WImage> createDefaultTrackArtworkImage();
|
||||||
|
|
||||||
std::unique_ptr<Wt::WInteractWidget> createFilter(const Wt::WString& name, const Wt::WString& tooltip, std::string_view colorStyleClass, bool canDelete = false);
|
std::unique_ptr<Wt::WInteractWidget> createFilter(const Wt::WString& name, const Wt::WString& tooltip, std::string_view colorStyleClass, bool canDelete = false);
|
||||||
std::unique_ptr<Wt::WInteractWidget> createFilterCluster(db::ClusterId clusterId, bool canDelete = false);
|
std::unique_ptr<Wt::WInteractWidget> createFilterCluster(db::ClusterId clusterId, bool canDelete = false);
|
||||||
|
|||||||
@@ -188,6 +188,11 @@ namespace lms::ui
|
|||||||
.arg(stepStats.progress()));
|
.arg(stepStats.progress()));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case ScanStep::AssociateTrackImages:
|
||||||
|
_stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-associating-track-images")
|
||||||
|
.arg(stepStats.progress()));
|
||||||
|
break;
|
||||||
|
|
||||||
case ScanStep::CheckForDuplicatedFiles:
|
case ScanStep::CheckForDuplicatedFiles:
|
||||||
_stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-checking-for-duplicate-files")
|
_stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-checking-for-duplicate-files")
|
||||||
.arg(stepStats.processedElems));
|
.arg(stepStats.processedElems));
|
||||||
|
|||||||
@@ -22,18 +22,23 @@
|
|||||||
|
|
||||||
#include "database/Artist.hpp"
|
#include "database/Artist.hpp"
|
||||||
|
|
||||||
#include "LmsApplication.hpp"
|
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
|
|
||||||
namespace lms::ui::ArtistListHelpers
|
namespace lms::ui::ArtistListHelpers
|
||||||
{
|
{
|
||||||
std::unique_ptr<Wt::WTemplate> createEntry(const db::ObjectPtr<db::Artist>& artist)
|
std::unique_ptr<Wt::WTemplate> createEntry(const db::Artist::pointer& artist)
|
||||||
{
|
{
|
||||||
auto entry{ std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Artists.template.entry")) };
|
auto entry{ std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Artists.template.entry")) };
|
||||||
entry->bindWidget("name", utils::createArtistAnchor(artist));
|
entry->bindWidget("name", utils::createArtistAnchor(artist));
|
||||||
|
|
||||||
Wt::WAnchor* anchor{ entry->bindWidget("image", utils::createArtistAnchor(artist, false)) };
|
Wt::WAnchor* anchor{ entry->bindWidget("image", utils::createArtistAnchor(artist, false)) };
|
||||||
auto image{ utils::createArtistImage(artist->getId(), ArtworkResource::Size::Large) };
|
|
||||||
|
std::unique_ptr<Wt::WImage> image;
|
||||||
|
if (artist->getPreferredArtworkId().isValid())
|
||||||
|
image = utils::createArtworkImage(artist->getPreferredArtworkId(), ArtworkResource::Size::Large);
|
||||||
|
else
|
||||||
|
image = utils::createDefaultArtistArtworkImage();
|
||||||
|
|
||||||
image->addStyleClass("Lms-cover-release Lms-cover-anchor rounded"); // hack
|
image->addStyleClass("Lms-cover-release Lms-cover-anchor rounded"); // hack
|
||||||
anchor->setImage(std::move(image));
|
anchor->setImage(std::move(image));
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
#include "core/String.hpp"
|
#include "core/String.hpp"
|
||||||
#include "database/Artist.hpp"
|
#include "database/Artist.hpp"
|
||||||
#include "database/ArtistInfo.hpp"
|
#include "database/ArtistInfo.hpp"
|
||||||
|
#include "database/ArtworkId.hpp"
|
||||||
#include "database/Cluster.hpp"
|
#include "database/Cluster.hpp"
|
||||||
#include "database/Release.hpp"
|
#include "database/Release.hpp"
|
||||||
#include "database/ScanSettings.hpp"
|
#include "database/ScanSettings.hpp"
|
||||||
@@ -119,7 +120,7 @@ namespace lms::ui
|
|||||||
LmsApp->setTitle(artist->getName());
|
LmsApp->setTitle(artist->getName());
|
||||||
_artistId = *artistId;
|
_artistId = *artistId;
|
||||||
|
|
||||||
refreshArtwork();
|
refreshArtwork(artist->getPreferredArtworkId());
|
||||||
refreshArtistInfo();
|
refreshArtistInfo();
|
||||||
refreshReleases();
|
refreshReleases();
|
||||||
refreshAppearsOnReleases();
|
refreshAppearsOnReleases();
|
||||||
@@ -191,12 +192,24 @@ namespace lms::ui
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Artist::refreshArtwork()
|
void Artist::refreshArtwork(db::ArtworkId artworkId)
|
||||||
{
|
{
|
||||||
auto* image{ bindWidget<Wt::WImage>("artwork", utils::createArtistImage(_artistId, ArtworkResource::Size::Large)) };
|
std::unique_ptr<Wt::WImage> artworkImage;
|
||||||
image->clicked().connect([this] {
|
if (artworkId.isValid())
|
||||||
utils::showArtworkModal(Wt::WLink{ LmsApp->getArtworkResource()->getArtistImageUrl(_artistId) });
|
{
|
||||||
});
|
artworkImage = utils::createArtworkImage(artworkId, ArtworkResource::Size::Large);
|
||||||
|
artworkImage->addStyleClass("Lms-cursor-pointer"); // HACK
|
||||||
|
}
|
||||||
|
else
|
||||||
|
artworkImage = utils::createDefaultArtistArtworkImage();
|
||||||
|
|
||||||
|
auto* image{ bindWidget<Wt::WImage>("artwork", std::move(artworkImage)) };
|
||||||
|
if (artworkId.isValid())
|
||||||
|
{
|
||||||
|
image->clicked().connect([artworkId] {
|
||||||
|
utils::showArtworkModal(Wt::WLink{ LmsApp->getArtworkResource()->getArtworkUrl(artworkId) });
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Artist::refreshArtistInfo()
|
void Artist::refreshArtistInfo()
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
#include <map>
|
#include <map>
|
||||||
|
|
||||||
#include "database/ArtistId.hpp"
|
#include "database/ArtistId.hpp"
|
||||||
|
#include "database/ArtworkId.hpp"
|
||||||
#include "database/Object.hpp"
|
#include "database/Object.hpp"
|
||||||
#include "database/ReleaseId.hpp"
|
#include "database/ReleaseId.hpp"
|
||||||
|
|
||||||
@@ -47,7 +48,7 @@ namespace lms::ui
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
void refreshView();
|
void refreshView();
|
||||||
void refreshArtwork();
|
void refreshArtwork(db::ArtworkId artworkId);
|
||||||
void refreshArtistInfo();
|
void refreshArtistInfo();
|
||||||
void refreshReleases();
|
void refreshReleases();
|
||||||
void refreshAppearsOnReleases();
|
void refreshAppearsOnReleases();
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
|
|
||||||
#include "DatabaseCollectorBase.hpp"
|
#include "DatabaseCollectorBase.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
#include "core/String.hpp"
|
#include "core/String.hpp"
|
||||||
|
|
||||||
#include "explore/Filters.hpp"
|
#include "explore/Filters.hpp"
|
||||||
|
|||||||
@@ -51,7 +51,13 @@ namespace lms::ui::releaseListHelpers
|
|||||||
|
|
||||||
{
|
{
|
||||||
Wt::WAnchor* anchor{ entry->bindWidget("cover", utils::createReleaseAnchor(release, false)) };
|
Wt::WAnchor* anchor{ entry->bindWidget("cover", utils::createReleaseAnchor(release, false)) };
|
||||||
auto image{ utils::createReleaseCover(release->getId(), ArtworkResource::Size::Large) };
|
|
||||||
|
std::unique_ptr<Wt::WImage> image;
|
||||||
|
if (release->getPreferredArtworkId().isValid())
|
||||||
|
image = utils::createArtworkImage(release->getPreferredArtworkId(), ArtworkResource::Size::Large);
|
||||||
|
else
|
||||||
|
image = utils::createDefaultReleaseArtworkImage();
|
||||||
|
|
||||||
image->addStyleClass("Lms-cover-release Lms-cover-anchor rounded"); // hack
|
image->addStyleClass("Lms-cover-release Lms-cover-anchor rounded"); // hack
|
||||||
anchor->setImage(std::move(image));
|
anchor->setImage(std::move(image));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -306,11 +306,7 @@ namespace lms::ui
|
|||||||
bindString("duration", utils::durationToString(release->getDuration()), Wt::TextFormat::Plain);
|
bindString("duration", utils::durationToString(release->getDuration()), Wt::TextFormat::Plain);
|
||||||
|
|
||||||
refreshReleaseArtists(release);
|
refreshReleaseArtists(release);
|
||||||
|
refreshArtwork(release->getPreferredArtworkId());
|
||||||
auto* image{ bindWidget<Wt::WImage>("cover", utils::createReleaseCover(release->getId(), ArtworkResource::Size::Large)) };
|
|
||||||
image->clicked().connect([=] {
|
|
||||||
utils::showArtworkModal(Wt::WLink{ LmsApp->getArtworkResource()->getReleaseCoverUrl(*releaseId) });
|
|
||||||
});
|
|
||||||
|
|
||||||
Wt::WContainerWidget* clusterContainers{ bindNew<Wt::WContainerWidget>("clusters") };
|
Wt::WContainerWidget* clusterContainers{ bindNew<Wt::WContainerWidget>("clusters") };
|
||||||
{
|
{
|
||||||
@@ -391,13 +387,26 @@ namespace lms::ui
|
|||||||
|
|
||||||
// Expect to be called in asc order
|
// Expect to be called in asc order
|
||||||
std::map<std::size_t, Wt::WContainerWidget*> trackContainers;
|
std::map<std::size_t, Wt::WContainerWidget*> trackContainers;
|
||||||
auto getOrAddDiscContainer = [&, releaseId = _releaseId](std::size_t discNumber, const std::string& discSubtitle) -> Wt::WContainerWidget* {
|
auto getOrAddDiscContainer = [&, releaseId = _releaseId](std::size_t discNumber, const std::string& discSubtitle, db::ArtworkId mediaArtworkId) -> Wt::WContainerWidget* {
|
||||||
if (auto it{ trackContainers.find(discNumber) }; it != std::cend(trackContainers))
|
if (auto it{ trackContainers.find(discNumber) }; it != std::cend(trackContainers))
|
||||||
return it->second;
|
return it->second;
|
||||||
|
|
||||||
Template* disc{ rootContainer->addNew<Template>(Wt::WString::tr("Lms.Explore.Release.template.entry-disc")) };
|
Template* disc{ rootContainer->addNew<Template>(Wt::WString::tr("Lms.Explore.Release.template.entry-disc")) };
|
||||||
disc->addFunction("id", &Wt::WTemplate::Functions::id);
|
disc->addFunction("id", &Wt::WTemplate::Functions::id);
|
||||||
|
|
||||||
|
if (mediaArtworkId.isValid())
|
||||||
|
{
|
||||||
|
auto image{ utils::createArtworkImage(mediaArtworkId, ArtworkResource::Size::Small) };
|
||||||
|
|
||||||
|
disc->setCondition("if-has-artwork", true);
|
||||||
|
|
||||||
|
image->addStyleClass("Lms-cover-track rounded"); // HACK
|
||||||
|
image->clicked().connect([=] {
|
||||||
|
utils::showArtworkModal(Wt::WLink{ LmsApp->getArtworkResource()->getArtworkUrl(mediaArtworkId) });
|
||||||
|
});
|
||||||
|
disc->bindWidget<Wt::WImage>("artwork", std::move(image));
|
||||||
|
}
|
||||||
|
|
||||||
if (discSubtitle.empty())
|
if (discSubtitle.empty())
|
||||||
disc->bindNew<Wt::WText>("disc-title", Wt::WString::tr("Lms.Explore.Release.disc").arg(discNumber));
|
disc->bindNew<Wt::WText>("disc-title", Wt::WString::tr("Lms.Explore.Release.disc").arg(discNumber));
|
||||||
else
|
else
|
||||||
@@ -458,9 +467,9 @@ namespace lms::ui
|
|||||||
|
|
||||||
Wt::WContainerWidget* container{};
|
Wt::WContainerWidget* container{};
|
||||||
if (useSubtitleContainers && discNumber)
|
if (useSubtitleContainers && discNumber)
|
||||||
container = getOrAddDiscContainer(*discNumber, track->getDiscSubtitle());
|
container = getOrAddDiscContainer(*discNumber, track->getDiscSubtitle(), track->getPreferredMediaArtworkId());
|
||||||
else if (hasDiscSubtitle && !discNumber)
|
else if (hasDiscSubtitle && !discNumber)
|
||||||
container = getOrAddDiscContainer(0, track->getDiscSubtitle());
|
container = getOrAddDiscContainer(0, track->getDiscSubtitle(), track->getPreferredMediaArtworkId());
|
||||||
else
|
else
|
||||||
container = getOrAddNoDiscContainer();
|
container = getOrAddNoDiscContainer();
|
||||||
|
|
||||||
@@ -556,6 +565,26 @@ namespace lms::ui
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Release::refreshArtwork(db::ArtworkId artworkId)
|
||||||
|
{
|
||||||
|
std::unique_ptr<Wt::WImage> artworkImage;
|
||||||
|
if (artworkId.isValid())
|
||||||
|
{
|
||||||
|
artworkImage = utils::createArtworkImage(artworkId, ArtworkResource::Size::Large);
|
||||||
|
artworkImage->addStyleClass("Lms-cursor-pointer"); // HACK
|
||||||
|
}
|
||||||
|
else
|
||||||
|
artworkImage = utils::createDefaultReleaseArtworkImage();
|
||||||
|
|
||||||
|
auto* image{ bindWidget<Wt::WImage>("artwork", std::move(artworkImage)) };
|
||||||
|
if (artworkId.isValid())
|
||||||
|
{
|
||||||
|
image->clicked().connect([artworkId] {
|
||||||
|
utils::showArtworkModal(Wt::WLink{ LmsApp->getArtworkResource()->getArtworkUrl(artworkId) });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Release::refreshReleaseArtists(const db::Release::pointer& release)
|
void Release::refreshReleaseArtists(const db::Release::pointer& release)
|
||||||
{
|
{
|
||||||
auto container{ utils::createArtistsAnchorsForRelease(release) };
|
auto container{ utils::createArtistsAnchorsForRelease(release) };
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "database/ArtworkId.hpp"
|
||||||
#include "database/Object.hpp"
|
#include "database/Object.hpp"
|
||||||
#include "database/ReleaseId.hpp"
|
#include "database/ReleaseId.hpp"
|
||||||
|
|
||||||
@@ -41,6 +42,7 @@ namespace lms::ui
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
void refreshView();
|
void refreshView();
|
||||||
|
void refreshArtwork(db::ArtworkId artworkId);
|
||||||
void refreshReleaseArtists(const db::ObjectPtr<db::Release>& release);
|
void refreshReleaseArtists(const db::ObjectPtr<db::Release>& release);
|
||||||
void refreshCopyright(const db::ObjectPtr<db::Release>& release);
|
void refreshCopyright(const db::ObjectPtr<db::Release>& release);
|
||||||
void refreshLinks(const db::ObjectPtr<db::Release>& release);
|
void refreshLinks(const db::ObjectPtr<db::Release>& release);
|
||||||
|
|||||||
@@ -228,8 +228,15 @@ namespace lms::ui::TrackListHelpers
|
|||||||
entry->bindWidget("artists-md", utils::createArtistDisplayNameWithAnchors(track->getArtistDisplayName(), artists));
|
entry->bindWidget("artists-md", utils::createArtistDisplayNameWithAnchors(track->getArtistDisplayName(), artists));
|
||||||
}
|
}
|
||||||
|
|
||||||
auto image{ utils::createTrackImage(trackId, ArtworkResource::Size::Small) };
|
std::unique_ptr<Wt::WImage> image;
|
||||||
image->addStyleClass("Lms-cover-track rounded");
|
if (track->getPreferredMediaArtworkId().isValid())
|
||||||
|
image = utils::createArtworkImage(track->getPreferredMediaArtworkId(), ArtworkResource::Size::Small);
|
||||||
|
else if (track->getPreferredArtworkId().isValid())
|
||||||
|
image = utils::createArtworkImage(track->getPreferredArtworkId(), ArtworkResource::Size::Small);
|
||||||
|
else
|
||||||
|
image = utils::createDefaultTrackArtworkImage();
|
||||||
|
|
||||||
|
image->addStyleClass("Lms-cover-track rounded"); // hack
|
||||||
if (track->getRelease())
|
if (track->getRelease())
|
||||||
{
|
{
|
||||||
entry->setCondition("if-has-release", true);
|
entry->setCondition("if-has-release", true);
|
||||||
|
|||||||
@@ -26,13 +26,6 @@
|
|||||||
#include "core/ITraceLogger.hpp"
|
#include "core/ITraceLogger.hpp"
|
||||||
#include "core/Service.hpp"
|
#include "core/Service.hpp"
|
||||||
#include "core/String.hpp"
|
#include "core/String.hpp"
|
||||||
#include "database/Artist.hpp"
|
|
||||||
#include "database/Image.hpp"
|
|
||||||
#include "database/Release.hpp"
|
|
||||||
#include "database/Session.hpp"
|
|
||||||
#include "database/Track.hpp"
|
|
||||||
#include "database/TrackEmbeddedImage.hpp"
|
|
||||||
#include "database/Types.hpp"
|
|
||||||
#include "services/artwork/IArtworkService.hpp"
|
#include "services/artwork/IArtworkService.hpp"
|
||||||
|
|
||||||
#include "LmsApplication.hpp"
|
#include "LmsApplication.hpp"
|
||||||
@@ -54,123 +47,28 @@ namespace lms::ui
|
|||||||
beingDeleted();
|
beingDeleted();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string ArtworkResource::getArtistImageUrl(db::ArtistId artistId, std::optional<Size> size) const
|
std::string ArtworkResource::getArtworkUrl(db::ArtworkId artworkId, std::optional<Size> size) const
|
||||||
{
|
{
|
||||||
std::string url;
|
std::string res{ url() + "&artworkid=" + artworkId.toString() };
|
||||||
|
|
||||||
{
|
|
||||||
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
|
|
||||||
|
|
||||||
const db::Artist::pointer artist{ db::Artist::find(LmsApp->getDbSession(), artistId) };
|
|
||||||
if (artist)
|
|
||||||
{
|
|
||||||
if (const db::Image::pointer image{ artist->getImage() })
|
|
||||||
url = getImageUrl(image->getId(), size, "artist");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (url.empty())
|
|
||||||
url = getDefaultArtistImageUrl();
|
|
||||||
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string ArtworkResource::getReleaseCoverUrl(db::ReleaseId releaseId, std::optional<Size> size) const
|
|
||||||
{
|
|
||||||
std::string url;
|
|
||||||
|
|
||||||
{
|
|
||||||
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
|
|
||||||
|
|
||||||
const db::Release::pointer release{ db::Release::find(LmsApp->getDbSession(), releaseId) };
|
|
||||||
if (release)
|
|
||||||
{
|
|
||||||
if (const db::Image::pointer image{ release->getImage() })
|
|
||||||
{
|
|
||||||
url = getImageUrl(image->getId(), size, "release");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
db::TrackEmbeddedImage::FindParameters params;
|
|
||||||
params.setRelease(releaseId);
|
|
||||||
params.setIsPreferred(true);
|
|
||||||
params.setSortMethod(db::TrackEmbeddedImageSortMethod::FrontCoverAndSize);
|
|
||||||
params.setRange(db::Range{ 0, 1 });
|
|
||||||
|
|
||||||
db::TrackEmbeddedImage::find(LmsApp->getDbSession(), params, [&](const db::TrackEmbeddedImage::pointer& image) {
|
|
||||||
url = getImageUrl(image->getId(), size, "release");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (url.empty())
|
|
||||||
url = getDefaultReleaseCoverUrl();
|
|
||||||
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string ArtworkResource::getPreferredTrackImageUrl(db::TrackId trackId, std::optional<Size> size) const
|
|
||||||
{
|
|
||||||
std::string url;
|
|
||||||
|
|
||||||
{
|
|
||||||
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
|
|
||||||
|
|
||||||
db::TrackEmbeddedImage::FindParameters params;
|
|
||||||
params.setTrack(trackId);
|
|
||||||
params.setIsPreferred(true);
|
|
||||||
params.setRange(db::Range{ .offset = 0, .size = 1 });
|
|
||||||
|
|
||||||
db::TrackEmbeddedImage::find(LmsApp->getDbSession(), params, [&](const db::TrackEmbeddedImage::pointer& image) {
|
|
||||||
url = getImageUrl(image->getId(), size, "release");
|
|
||||||
});
|
|
||||||
|
|
||||||
if (url.empty())
|
|
||||||
{
|
|
||||||
db::Track::pointer track{ db::Track::find(LmsApp->getDbSession(), trackId) };
|
|
||||||
if (track)
|
|
||||||
{
|
|
||||||
if (const db::Release::pointer release{ track->getRelease() })
|
|
||||||
{
|
|
||||||
if (const db::Image::pointer image{ release->getImage() })
|
|
||||||
url = getImageUrl(image->getId(), size, "release");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (url.empty())
|
|
||||||
url = getDefaultReleaseCoverUrl();
|
|
||||||
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string ArtworkResource::getImageUrl(db::ImageId imageId, std::optional<Size> size, std::string_view type) const
|
|
||||||
{
|
|
||||||
std::string res{ url() + "&imageid=" + imageId.toString() + "&type=" + std::string{ type } };
|
|
||||||
if (size)
|
if (size)
|
||||||
res += "&size=" + std::to_string(static_cast<std::size_t>(*size));
|
res += "&size=" + std::to_string(static_cast<std::size_t>(*size));
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string ArtworkResource::getImageUrl(db::TrackEmbeddedImageId trackId, std::optional<Size> size, std::string_view type) const
|
std::string ArtworkResource::getDefaultArtistArtworkUrl() const
|
||||||
{
|
{
|
||||||
std::string res{ url() + "&trimageid=" + trackId.toString() + "&type=" + std::string{ type } };
|
return url() + "&type=defartist";
|
||||||
if (size)
|
|
||||||
res += "&size=" + std::to_string(static_cast<std::size_t>(*size));
|
|
||||||
return res;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string ArtworkResource::getDefaultArtistImageUrl() const
|
std::string ArtworkResource::getDefaultReleaseArtworkUrl() const
|
||||||
{
|
{
|
||||||
return url() + "&type=artist";
|
return url() + "&type=defrelease";
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string ArtworkResource::getDefaultReleaseCoverUrl() const
|
std::string ArtworkResource::getDefaultTrackArtworkUrl() const
|
||||||
{
|
{
|
||||||
return url() + "&type=release";
|
return url() + "&type=defrelease";
|
||||||
}
|
}
|
||||||
|
|
||||||
void ArtworkResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response)
|
void ArtworkResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response)
|
||||||
@@ -178,40 +76,64 @@ namespace lms::ui
|
|||||||
LMS_SCOPED_TRACE_OVERVIEW("UI", "HandleCoverRequest");
|
LMS_SCOPED_TRACE_OVERVIEW("UI", "HandleCoverRequest");
|
||||||
|
|
||||||
// Retrieve parameters
|
// Retrieve parameters
|
||||||
const std::string* imageIdStr = request.getParameter("imageid");
|
const std::string* artworkIdStr = request.getParameter("artworkid");
|
||||||
const std::string* trackEmbeddedImageIdStr = request.getParameter("trimageid");
|
|
||||||
const std::string* sizeStr = request.getParameter("size");
|
const std::string* sizeStr = request.getParameter("size");
|
||||||
const std::string* typeStr = request.getParameter("type");
|
const std::string* typeStr = request.getParameter("type");
|
||||||
|
|
||||||
std::shared_ptr<image::IEncodedImage> image;
|
std::shared_ptr<image::IEncodedImage> image;
|
||||||
|
|
||||||
if ((imageIdStr || trackEmbeddedImageIdStr))
|
if (artworkIdStr && typeStr)
|
||||||
|
{
|
||||||
|
ARTWORK_RESOURCE_LOG(DEBUG, "both artwork ID and type provided, only one is allowed");
|
||||||
|
response.setStatus(400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!artworkIdStr && !typeStr)
|
||||||
|
{
|
||||||
|
ARTWORK_RESOURCE_LOG(DEBUG, "no artwork ID or type provided");
|
||||||
|
response.setStatus(400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (artworkIdStr)
|
||||||
{
|
{
|
||||||
const auto size{ sizeStr ? core::stringUtils::readAs<std::size_t>(*sizeStr) : std::nullopt };
|
const auto size{ sizeStr ? core::stringUtils::readAs<std::size_t>(*sizeStr) : std::nullopt };
|
||||||
if (size && *size > maxSize)
|
if (size && *size > maxSize)
|
||||||
{
|
{
|
||||||
ARTWORK_RESOURCE_LOG(DEBUG, "invalid size provided!");
|
ARTWORK_RESOURCE_LOG(DEBUG, "invalid size provided: " << *sizeStr);
|
||||||
|
response.setStatus(400);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (imageIdStr)
|
const auto artworkId{ core::stringUtils::readAs<db::ArtworkId::ValueType>(*artworkIdStr) };
|
||||||
|
if (!artworkId)
|
||||||
{
|
{
|
||||||
if (const auto imageId{ core::stringUtils::readAs<db::ImageId::ValueType>(*imageIdStr) })
|
ARTWORK_RESOURCE_LOG(DEBUG, "invalid artwork ID provided: '" << *artworkIdStr << "'");
|
||||||
image = core::Service<cover::IArtworkService>::get()->getImage(*imageId, size);
|
response.setStatus(400);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
else if (trackEmbeddedImageIdStr)
|
|
||||||
{
|
|
||||||
if (const auto imageId{ core::stringUtils::readAs<db::TrackEmbeddedImageId::ValueType>(*trackEmbeddedImageIdStr) })
|
|
||||||
image = core::Service<cover::IArtworkService>::get()->getTrackEmbeddedImage(*imageId, size);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!image && typeStr)
|
image = core::Service<artwork::IArtworkService>::get()->getImage(*artworkId, size);
|
||||||
|
if (!image)
|
||||||
|
ARTWORK_RESOURCE_LOG(DEBUG, "no image found for artwork ID: '" << *artworkIdStr << "'");
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
if (*typeStr == "release")
|
assert(typeStr);
|
||||||
image = core::Service<cover::IArtworkService>::get()->getDefaultReleaseCover();
|
if (*typeStr == "defartist")
|
||||||
else if (*typeStr == "artist")
|
image = core::Service<artwork::IArtworkService>::get()->getDefaultArtistArtwork();
|
||||||
image = core::Service<cover::IArtworkService>::get()->getDefaultArtistImage();
|
else if (*typeStr == "defrelease")
|
||||||
|
image = core::Service<artwork::IArtworkService>::get()->getDefaultReleaseArtwork();
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ARTWORK_RESOURCE_LOG(DEBUG, "invalid type provided: '" << *typeStr << "'");
|
||||||
|
response.setStatus(400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!image)
|
||||||
|
ARTWORK_RESOURCE_LOG(DEBUG, "no default image found for type: '" << *typeStr << "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (image)
|
if (image)
|
||||||
@@ -220,6 +142,8 @@ namespace lms::ui
|
|||||||
response.out().write(reinterpret_cast<const char*>(image->getData().data()), image->getData().size());
|
response.out().write(reinterpret_cast<const char*>(image->getData().data()), image->getData().size());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
response.setStatus(404);
|
response.setStatus(404);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} // namespace lms::ui
|
} // namespace lms::ui
|
||||||
|
|||||||
@@ -23,11 +23,7 @@
|
|||||||
|
|
||||||
#include <Wt/WResource.h>
|
#include <Wt/WResource.h>
|
||||||
|
|
||||||
#include "database/ArtistId.hpp"
|
#include "database/ArtworkId.hpp"
|
||||||
#include "database/ImageId.hpp"
|
|
||||||
#include "database/ReleaseId.hpp"
|
|
||||||
#include "database/TrackEmbeddedImageId.hpp"
|
|
||||||
#include "database/TrackId.hpp"
|
|
||||||
|
|
||||||
namespace lms::ui
|
namespace lms::ui
|
||||||
{
|
{
|
||||||
@@ -37,7 +33,7 @@ namespace lms::ui
|
|||||||
static const std::size_t maxSize{ 512 };
|
static const std::size_t maxSize{ 512 };
|
||||||
|
|
||||||
ArtworkResource();
|
ArtworkResource();
|
||||||
~ArtworkResource();
|
~ArtworkResource() override;
|
||||||
|
|
||||||
enum class Size : std::size_t
|
enum class Size : std::size_t
|
||||||
{
|
{
|
||||||
@@ -45,17 +41,13 @@ namespace lms::ui
|
|||||||
Large = 512,
|
Large = 512,
|
||||||
};
|
};
|
||||||
|
|
||||||
std::string getArtistImageUrl(db::ArtistId artistId, std::optional<Size> size = std::nullopt) const;
|
std::string getArtworkUrl(db::ArtworkId artworkId, std::optional<Size> size = std::nullopt) const;
|
||||||
std::string getReleaseCoverUrl(db::ReleaseId releaseId, std::optional<Size> size = std::nullopt) const;
|
|
||||||
std::string getPreferredTrackImageUrl(db::TrackId trackId, std::optional<Size> size = std::nullopt) const;
|
std::string getDefaultArtistArtworkUrl() const;
|
||||||
|
std::string getDefaultReleaseArtworkUrl() const;
|
||||||
|
std::string getDefaultTrackArtworkUrl() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::string getImageUrl(db::ImageId imageId, std::optional<Size> size, std::string_view type) const;
|
|
||||||
std::string getImageUrl(db::TrackEmbeddedImageId trackEmbeddedImageId, std::optional<Size> size, std::string_view type) const;
|
|
||||||
|
|
||||||
std::string getDefaultArtistImageUrl() const;
|
|
||||||
std::string getDefaultReleaseCoverUrl() const;
|
|
||||||
|
|
||||||
void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override;
|
void handleRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override;
|
||||||
};
|
};
|
||||||
} // namespace lms::ui
|
} // namespace lms::ui
|
||||||
|
|||||||
Reference in New Issue
Block a user