Introducing new table medium, ref #699

This commit is contained in:
emeric
2025-07-28 22:36:27 +02:00
parent 3c3e551676
commit 0ceaf4cfaa
36 changed files with 964 additions and 422 deletions
+1 -1
View File
@@ -33,7 +33,7 @@
</div> </div>
</div> </div>
<div class="col-12"> <div class="col-12">
${container class="d-grid gap-3"} ${disc-container class="d-grid gap-3"}
${<if-has-copyright>} ${<if-has-copyright>}
<div class="pt-1 mt-1 border-top border-dark small text-muted fw-lighter fst-italic">${copyright class="text-reset"}</div> <div class="pt-1 mt-1 border-top border-dark small text-muted fw-lighter fst-italic">${copyright class="text-reset"}</div>
${</if-has-copyright>} ${</if-has-copyright>}
+1
View File
@@ -8,6 +8,7 @@ add_library(lmsdatabase STATIC
impl/objects/Image.cpp impl/objects/Image.cpp
impl/objects/Listen.cpp impl/objects/Listen.cpp
impl/objects/MediaLibrary.cpp impl/objects/MediaLibrary.cpp
impl/objects/Medium.cpp
impl/objects/PlayListFile.cpp impl/objects/PlayListFile.cpp
impl/objects/PlayQueue.cpp impl/objects/PlayQueue.cpp
impl/objects/TrackArtistLink.cpp impl/objects/TrackArtistLink.cpp
+107 -1
View File
@@ -34,7 +34,7 @@ namespace lms::db
{ {
namespace namespace
{ {
static constexpr Version LMS_DATABASE_VERSION{ 98 }; static constexpr Version LMS_DATABASE_VERSION{ 99 };
} }
VersionInfo::VersionInfo() VersionInfo::VersionInfo()
@@ -1423,6 +1423,111 @@ WHERE art.image_id IS NULL)");
dropIndexes(session); dropIndexes(session);
} }
void migrateFromV98(Session& session)
{
// Medium support
dropIndexes(session);
// New table for mediums
utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "medium" (
"id" integer primary key autoincrement,
"version" integer not null,
"name" text not null,
"position" integer,
"track_count" integer,
"media" text not null,
"replay_gain" real,
"release_id" bigint,
"preferred_artwork_id" bigint,
constraint "fk_medium_release" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred,
constraint "fk_medium_preferred_artwork" foreign key ("preferred_artwork_id") references "artwork" ("id") on delete set null deferrable initially deferred
))");
// New primary key in track -> need to recreate the table
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,
"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,
"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,
"replay_gain" real,
"artist_display_name" text not null,
"comment" text not null,
"medium_id" bigint,
"release_id" bigint,
"media_library_id" bigint,
"directory_id" bigint,
"preferred_artwork_id" bigint,
"preferred_media_artwork_id" bigint,
constraint "fk_track_medium" foreign key ("medium_id") references "medium" ("id") on delete cascade deferrable initially deferred,
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:
// - removed disc_number, total_track, disc_subtitle, release_replay_gain
// - renamed track_replay_gain to replay_gain
// - with the new medium_id field set to null
utils::executeCommand(*session.getDboSession(), R"(INSERT INTO track_backup
SELECT
id,
version,
scan_version,
track_number,
name,
duration,
bitrate,
bits_per_sample,
channel_count,
sample_rate,
date,
original_date,
absolute_file_path,
file_size,
file_last_write,
file_added,
mbid,
recording_mbid,
copyright,
copyright_url,
advisory,
track_replay_gain AS replay_gain,
artist_display_name,
comment,
NULL as medium_id,
release_id,
media_library_id,
directory_id,
preferred_artwork_id,
preferred_media_artwork_id
FROM track)");
utils::executeCommand(*session.getDboSession(), "DROP TABLE track");
utils::executeCommand(*session.getDboSession(), "ALTER TABLE track_backup RENAME TO track");
// Just increment the scan version of the settings to make the next scan rescan all audio files
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET artist_info_scan_version = artist_info_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)" };
@@ -1497,6 +1602,7 @@ WHERE art.image_id IS NULL)");
{ 95, migrateFromV95 }, { 95, migrateFromV95 },
{ 96, migrateFromV96 }, { 96, migrateFromV96 },
{ 97, migrateFromV97 }, { 97, migrateFromV97 },
{ 98, migrateFromV98 },
}; };
bool migrationPerformed{}; bool migrationPerformed{};
+5 -1
View File
@@ -33,6 +33,7 @@
#include "database/objects/Image.hpp" #include "database/objects/Image.hpp"
#include "database/objects/Listen.hpp" #include "database/objects/Listen.hpp"
#include "database/objects/MediaLibrary.hpp" #include "database/objects/MediaLibrary.hpp"
#include "database/objects/Medium.hpp"
#include "database/objects/PlayListFile.hpp" #include "database/objects/PlayListFile.hpp"
#include "database/objects/PlayQueue.hpp" #include "database/objects/PlayQueue.hpp"
#include "database/objects/RatedArtist.hpp" #include "database/objects/RatedArtist.hpp"
@@ -82,6 +83,7 @@ namespace lms::db
_session.mapClass<Label>("label"); _session.mapClass<Label>("label");
_session.mapClass<Listen>("listen"); _session.mapClass<Listen>("listen");
_session.mapClass<MediaLibrary>("media_library"); _session.mapClass<MediaLibrary>("media_library");
_session.mapClass<Medium>("medium");
_session.mapClass<PlayListFile>("playlist_file"); _session.mapClass<PlayListFile>("playlist_file");
_session.mapClass<PlayQueue>("playqueue"); _session.mapClass<PlayQueue>("playqueue");
_session.mapClass<RatedArtist>("rated_artist"); _session.mapClass<RatedArtist>("rated_artist");
@@ -235,6 +237,8 @@ namespace lms::db
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS media_library_id_idx ON media_library(id)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS media_library_id_idx ON media_library(id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS medium_release_position_idx ON medium(release_id, position)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS playlist_file_id_idx ON playlist_file(id)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS playlist_file_id_idx ON playlist_file(id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS playlist_file_directory_idx ON playlist_file(directory_id);"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS playlist_file_directory_idx ON playlist_file(directory_id);");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS playlist_file_absolute_file_path_idx ON playlist_file(absolute_file_path)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS playlist_file_absolute_file_path_idx ON playlist_file(absolute_file_path)");
@@ -262,12 +266,12 @@ namespace lms::db
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_file_last_write_idx ON track(file_last_write)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_file_last_write_idx ON track(file_last_write)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_media_library_idx ON track(media_library_id)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_media_library_idx ON track(media_library_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_media_library_release_idx ON track(media_library_id, release_id)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_media_library_release_idx ON track(media_library_id, release_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_medium_idx ON track(medium_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_mbid_idx ON track(mbid)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_mbid_idx ON track(mbid)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_name_file_size_idx ON track(name, file_size)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_name_file_size_idx ON track(name, file_size)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_name_nocase_idx ON track(name COLLATE NOCASE)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_name_nocase_idx ON track(name COLLATE NOCASE)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_original_date_idx ON track(original_date)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_original_date_idx ON track(original_date)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_recording_mbid_idx ON track(recording_mbid)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_recording_mbid_idx ON track(recording_mbid)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_release_disc_idx ON track(release_id, disc_number)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_release_date_idx ON track(release_id, date)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_release_date_idx ON track(release_id, date)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_release_file_last_write_idx ON track(release_id, file_last_write)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_release_file_last_write_idx ON track(release_id, file_last_write)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_release_file_added_idx ON track(release_id, file_added)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_release_file_added_idx ON track(release_id, file_added)");
@@ -26,6 +26,7 @@
#include "database/objects/Artwork.hpp" #include "database/objects/Artwork.hpp"
#include "database/objects/Directory.hpp" #include "database/objects/Directory.hpp"
#include "database/objects/MediaLibrary.hpp" #include "database/objects/MediaLibrary.hpp"
#include "database/objects/Medium.hpp"
#include "database/objects/Release.hpp" #include "database/objects/Release.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "database/objects/TrackArtistLink.hpp" #include "database/objects/TrackArtistLink.hpp"
+86
View File
@@ -0,0 +1,86 @@
/*
* 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/objects/Medium.hpp"
#include <Wt/Dbo/Impl.h>
#include "database/Session.hpp"
#include "database/objects/Artist.hpp"
#include "database/objects/Artwork.hpp"
#include "database/objects/Cluster.hpp"
#include "database/objects/Directory.hpp"
#include "database/objects/MediaLibrary.hpp"
#include "database/objects/Release.hpp"
#include "database/objects/Track.hpp"
#include "database/objects/TrackArtistLink.hpp"
#include "database/objects/TrackEmbeddedImage.hpp"
#include "database/objects/TrackEmbeddedImageLink.hpp"
#include "database/objects/TrackLyrics.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
DBO_INSTANTIATE_TEMPLATES(lms::db::Medium);
namespace lms::db
{
Medium::Medium(ObjectPtr<Release> release)
: _release(getDboPtr(release))
{
}
Medium::pointer Medium::create(Session& session, ObjectPtr<Release> release)
{
return session.getDboSession()->add(std::unique_ptr<Medium>{ new Medium{ release } });
}
std::size_t Medium::getCount(Session& session)
{
session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM medium"));
}
Medium::pointer Medium::find(Session& session, MediumId id)
{
session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Medium>>("SELECT m from medium m").where("m.id = ?").bind(id));
}
Medium::pointer Medium::find(Session& session, ReleaseId releaseId, std::optional<std::size_t> position)
{
session.checkReadTransaction();
std::optional<int> dbPosition;
if (position)
dbPosition = static_cast<int>(*position);
auto query{ session.getDboSession()->query<Wt::Dbo::ptr<Medium>>("SELECT m from medium m") };
query.where("m.release_id = ?").bind(releaseId);
if (position)
query.where("m.position = ?").bind(*dbPosition);
else
query.where("m.position IS NULL");
return utils::fetchQuerySingleResult(query);
}
} // namespace lms::db
@@ -28,6 +28,7 @@
#include "database/objects/Cluster.hpp" #include "database/objects/Cluster.hpp"
#include "database/objects/Directory.hpp" #include "database/objects/Directory.hpp"
#include "database/objects/MediaLibrary.hpp" #include "database/objects/MediaLibrary.hpp"
#include "database/objects/Medium.hpp"
#include "database/objects/Release.hpp" #include "database/objects/Release.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "database/objects/TrackArtistLink.hpp" #include "database/objects/TrackArtistLink.hpp"
+13 -27
View File
@@ -30,6 +30,7 @@
#include "database/objects/Cluster.hpp" #include "database/objects/Cluster.hpp"
#include "database/objects/Directory.hpp" #include "database/objects/Directory.hpp"
#include "database/objects/MediaLibrary.hpp" #include "database/objects/MediaLibrary.hpp"
#include "database/objects/Medium.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "database/objects/TrackArtistLink.hpp" #include "database/objects/TrackArtistLink.hpp"
#include "database/objects/TrackEmbeddedImage.hpp" #include "database/objects/TrackEmbeddedImage.hpp"
@@ -577,28 +578,6 @@ namespace lms::db
return utils::fetchQuerySingleResult(createQuery<int>(session, "COUNT(DISTINCT r.id)", params)); return utils::fetchQuerySingleResult(createQuery<int>(session, "COUNT(DISTINCT r.id)", params));
} }
std::size_t Release::getDiscCount() const
{
assert(session());
int res{ utils::fetchQuerySingleResult(session()->query<int>("SELECT COUNT(DISTINCT disc_number) FROM track t").where("t.release_id = ?").bind(getId())) };
return res;
}
std::vector<DiscInfo> Release::getDiscs() const
{
assert(session());
using ResultType = std::tuple<int, std::string>;
const auto query{ session()->query<ResultType>("SELECT DISTINCT disc_number, disc_subtitle FROM track t").where("t.release_id = ?").bind(getId()).orderBy("disc_number") };
std::vector<DiscInfo> discs;
utils::forEachQueryResult(query, [&](ResultType&& res) {
discs.emplace_back(DiscInfo{ static_cast<std::size_t>(std::get<int>(res)), std::move(std::get<std::string>(res)) });
});
return discs;
}
core::PartialDateTime Release::getDate() const core::PartialDateTime Release::getDate() const
{ {
return getDate(false); return getDate(false);
@@ -753,6 +732,18 @@ namespace lms::db
return _preferredArtwork.id(); return _preferredArtwork.id();
} }
std::vector<ObjectPtr<Medium>> Release::getMediums() const
{
assert(session());
// Select the similar releases using the 5 most used clusters of the release
auto query{ session()->query<Wt::Dbo::ptr<Medium>>("SELECT m from medium m") };
query.where("m.release_id = ?").bind(getId());
query.orderBy("m.position");
return utils::fetchQueryResults<Medium::pointer>(query);
}
void Release::clearLabels() void Release::clearLabels()
{ {
_labels.clear(); _labels.clear();
@@ -794,11 +785,6 @@ namespace lms::db
return getArtists().size() > 1; return getArtists().size() > 1;
} }
bool Release::hasDiscSubtitle() const
{
return utils::fetchQuerySingleResult(session()->query<int>("SELECT EXISTS (SELECT 1 FROM track WHERE disc_subtitle IS NOT NULL AND disc_subtitle <> '' AND release_id = ?)").bind(getId()));
}
std::size_t Release::getTrackCount() const std::size_t Release::getTrackCount() const
{ {
assert(session()); assert(session());
+17 -6
View File
@@ -24,11 +24,13 @@
#include "core/ILogger.hpp" #include "core/ILogger.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/Types.hpp"
#include "database/objects/Artist.hpp" #include "database/objects/Artist.hpp"
#include "database/objects/Artwork.hpp" #include "database/objects/Artwork.hpp"
#include "database/objects/Cluster.hpp" #include "database/objects/Cluster.hpp"
#include "database/objects/Directory.hpp" #include "database/objects/Directory.hpp"
#include "database/objects/MediaLibrary.hpp" #include "database/objects/MediaLibrary.hpp"
#include "database/objects/Medium.hpp"
#include "database/objects/Release.hpp" #include "database/objects/Release.hpp"
#include "database/objects/TrackArtistLink.hpp" #include "database/objects/TrackArtistLink.hpp"
#include "database/objects/TrackEmbeddedImage.hpp" #include "database/objects/TrackEmbeddedImage.hpp"
@@ -146,6 +148,9 @@ namespace lms::db
query.where("r.name = ?").bind(params.releaseName); query.where("r.name = ?").bind(params.releaseName);
} }
if (params.medium.isValid())
query.where("t.medium_id = ?").bind(params.medium);
if (params.trackList.isValid() || params.sortMethod == TrackSortMethod::TrackList) if (params.trackList.isValid() || params.sortMethod == TrackSortMethod::TrackList)
{ {
query.join("tracklist t_l ON t_l_e.tracklist_id = t_l.id"); query.join("tracklist t_l ON t_l_e.tracklist_id = t_l.id");
@@ -156,8 +161,11 @@ namespace lms::db
if (params.trackNumber) if (params.trackNumber)
query.where("t.track_number = ?").bind(*params.trackNumber); query.where("t.track_number = ?").bind(*params.trackNumber);
if (params.discNumber) if (params.sortMethod == TrackSortMethod::DateDescAndRelease
query.where("t.disc_number = ?").bind(*params.discNumber); || params.sortMethod == TrackSortMethod::Release)
{
query.join("medium m ON t.medium_id = m.id");
}
if (params.filters.mediaLibrary.isValid()) if (params.filters.mediaLibrary.isValid())
query.where("t.media_library_id = ?").bind(params.filters.mediaLibrary); query.where("t.media_library_id = ?").bind(params.filters.mediaLibrary);
@@ -213,16 +221,19 @@ namespace lms::db
query.orderBy("t.absolute_file_path COLLATE NOCASE"); query.orderBy("t.absolute_file_path COLLATE NOCASE");
break; break;
case TrackSortMethod::DateDescAndRelease: case TrackSortMethod::DateDescAndRelease:
query.orderBy("t.date DESC,t.release_id,t.disc_number,t.track_number"); query.orderBy("t.date DESC,t.release_id,m.position,t.track_number");
break; break;
case TrackSortMethod::Release: case TrackSortMethod::Release:
query.orderBy("t.disc_number,t.track_number"); query.orderBy("m.position,t.track_number");
break; break;
case TrackSortMethod::TrackList: case TrackSortMethod::TrackList:
assert(params.trackList.isValid()); assert(params.trackList.isValid());
query.orderBy("t_l_e.id"); query.orderBy("t_l_e.id");
break;
case TrackSortMethod::TrackNumber:
query.orderBy("t.track_number");
break;
} }
return query; return query;
} }
@@ -360,7 +371,7 @@ namespace lms::db
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ session.getDboSession()->query<TrackId>("SELECT track.id FROM track WHERE mbid in (SELECT mbid FROM track WHERE mbid <> '' GROUP BY mbid HAVING COUNT (*) > 1)").orderBy("track.release_id,track.disc_number,track.track_number,track.mbid") }; auto query{ session.getDboSession()->query<TrackId>("SELECT track.id FROM track WHERE mbid in (SELECT mbid FROM track WHERE mbid <> '' GROUP BY mbid HAVING COUNT (*) > 1)").orderBy("track.release_id,track.mbid") };
return utils::execRangeQuery<TrackId>(query, range); return utils::execRangeQuery<TrackId>(query, range);
} }
@@ -43,8 +43,7 @@ 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.artist.isValid() if (params.medium.isValid()
|| params.discNumber.has_value()
|| params.track.isValid() || params.track.isValid()
|| params.release.isValid() || params.release.isValid()
|| params.trackList.isValid() || params.trackList.isValid()
@@ -54,39 +53,27 @@ namespace lms::db
{ {
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.artist.isValid())
{
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.medium.isValid()
|| params.sortMethod == TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc || params.sortMethod == TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc
|| params.sortMethod == TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc) || params.sortMethod == TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc)
{ {
assert(!(params.release.isValid() && params.medium.isValid()));
assert((params.sortMethod != TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc || params.release.isValid()));
assert((params.sortMethod != TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc || params.medium.isValid()));
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");
if (params.release.isValid()) if (params.release.isValid())
query.where("t.release_id = ?").bind(params.release); query.where("t.release_id = ?").bind(params.release);
if (params.discNumber.has_value()) if (params.medium.isValid())
query.where("t.disc_number = ?").bind(params.discNumber.value()); query.where("t.medium_id = ?").bind(params.medium);
if (params.sortMethod == TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc)
query.join("medium m ON m.id = t.medium_id");
} }
if (params.trackList.isValid()) if (params.trackList.isValid())
@@ -107,7 +94,7 @@ namespace lms::db
query.orderBy("t_e_i.size DESC"); query.orderBy("t_e_i.size DESC");
break; break;
case TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc: case TrackEmbeddedImageSortMethod::DiscNumberThenTrackNumberThenSizeDesc:
query.orderBy("t.disc_number, t.track_number, t_e_i.size DESC"); query.orderBy("m.position, t.track_number, t_e_i.size DESC");
break; break;
case TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc: case TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc:
query.orderBy("t.track_number, t_e_i.size DESC"); query.orderBy("t.track_number, t_e_i.size DESC");
+7 -6
View File
@@ -119,12 +119,6 @@ namespace lms::db
int end{}; int end{};
}; };
struct DiscInfo
{
std::size_t position;
std::string name;
};
struct FileInfo struct FileInfo
{ {
Wt::WDateTime lastWrittenTime; Wt::WDateTime lastWrittenTime;
@@ -163,6 +157,12 @@ namespace lms::db
Name, Name,
}; };
enum class MediumSortMethod
{
None,
PositionAsc,
};
enum class ReleaseSortMethod enum class ReleaseSortMethod
{ {
None, None,
@@ -215,6 +215,7 @@ namespace lms::db
DateDescAndRelease, DateDescAndRelease,
Release, // order by disc/track number Release, // order by disc/track number
TrackList, // order by asc order in tracklist TrackList, // order by asc order in tracklist
TrackNumber,
}; };
enum class TrackLyricsSortMethod enum class TrackLyricsSortMethod
@@ -0,0 +1,126 @@
/*
* 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 <functional>
#include <optional>
#include <string>
#include <string_view>
#include <Wt/Dbo/Field.h>
#include <Wt/Dbo/collection.h>
#include "database/Object.hpp"
#include "database/Types.hpp"
#include "database/objects/ArtworkId.hpp"
#include "database/objects/MediumId.hpp"
#include "database/objects/ReleaseId.hpp"
namespace lms::db
{
class Artwork;
class Release;
class Session;
class Track;
class Medium final : public Object<Medium, MediumId>
{
public:
static const std::size_t maxMediaLength{ 64 };
Medium() = default;
// find
struct FindParameters
{
ReleaseId release;
MediumSortMethod sortMethod{ MediumSortMethod::None };
std::optional<Range> range;
FindParameters& setRelease(ReleaseId _release)
{
release = _release;
return *this;
}
FindParameters& setSortMethod(MediumSortMethod _sortMethod)
{
sortMethod = _sortMethod;
return *this;
}
FindParameters& setRange(std::optional<Range> _range)
{
range = _range;
return *this;
}
};
static std::size_t getCount(Session& session);
static pointer find(Session& session, MediumId id);
static pointer find(Session& session, ReleaseId id, std::optional<std::size_t> position);
static void find(Session& session, const FindParameters& params, std::function<void(const pointer&)> func);
// getters
std::string_view getName() const { return _name; }
std::optional<std::size_t> getPosition() const { return _position; }
std::optional<std::size_t> getTrackCount() const { return _trackCount; } // not necessarily the number of tracks in the medium, but the number of tracks that should be in the medium
std::string_view getMedia() const { return _media; }
std::optional<float> getReplayGain() const { return _replayGain; }
ReleaseId getReleaseId() const { return _release.id(); }
ObjectPtr<Release> getRelease() const { return _release; }
ObjectPtr<Artwork> getPreferredArtwork() const { return _preferredArtwork; }
ArtworkId getPreferredArtworkId() const { return _preferredArtwork.id(); }
// setters
void setName(std::string_view name) { _name = name; }
void setPosition(std::optional<std::size_t> position) { _position = position; }
void setTrackCount(std::optional<std::size_t> trackCount) { _trackCount = trackCount; }
void setMedia(std::string_view media) { _media = media; }
void setReplayGain(std::optional<float> replayGain) { _replayGain = replayGain; }
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _name, "name");
Wt::Dbo::field(a, _position, "position");
Wt::Dbo::field(a, _trackCount, "track_count");
Wt::Dbo::field(a, _media, "media");
Wt::Dbo::field(a, _replayGain, "replay_gain");
Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::belongsTo(a, _preferredArtwork, "preferred_artwork", Wt::Dbo::OnDeleteSetNull);
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToOne, "medium");
}
private:
friend class Session;
Medium(ObjectPtr<Release> release);
static pointer create(Session& session, ObjectPtr<Release> release);
std::string _name;
std::optional<int> _position; // position in the release
std::optional<int> _trackCount;
std::string _media; // CD, etc.
std::optional<float> _replayGain;
Wt::Dbo::ptr<Release> _release;
Wt::Dbo::ptr<Artwork> _preferredArtwork;
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> _tracks; // tracks that match this medium
};
} // namespace lms::db
@@ -0,0 +1,24 @@
/*
* 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 "database/IdType.hpp"
LMS_DECLARE_IDTYPE(MediumId)
@@ -51,6 +51,7 @@ namespace lms::db
class Artwork; class Artwork;
class Cluster; class Cluster;
class ClusterType; class ClusterType;
class Medium;
class Release; class Release;
class Session; class Session;
class Track; class Track;
@@ -282,9 +283,7 @@ namespace lms::db
std::string_view getSortName() const { return _sortName; } std::string_view getSortName() const { return _sortName; }
std::optional<core::UUID> getMBID() const { return core::UUID::fromString(_MBID); } std::optional<core::UUID> getMBID() const { return core::UUID::fromString(_MBID); }
std::optional<core::UUID> getGroupMBID() const { return core::UUID::fromString(_groupMBID); } std::optional<core::UUID> getGroupMBID() const { return core::UUID::fromString(_groupMBID); }
std::optional<std::size_t> getTotalDisc() const { return _totalDisc; } std::optional<std::size_t> getTotalDisc() const { return _totalDisc; } // the number of discs this release should have if complete
std::size_t getDiscCount() const; // may not be total disc (if incomplete for example)
std::vector<DiscInfo> getDiscs() const;
std::chrono::milliseconds getDuration() const; std::chrono::milliseconds getDuration() const;
Wt::WDateTime getAddedTime() const; Wt::WDateTime getAddedTime() const;
Wt::WDateTime getLastWrittenTime() const; Wt::WDateTime getLastWrittenTime() const;
@@ -301,6 +300,7 @@ namespace lms::db
std::string_view getComment() const { return _comment; } std::string_view getComment() const { return _comment; }
ObjectPtr<Artwork> getPreferredArtwork() const; ObjectPtr<Artwork> getPreferredArtwork() const;
ArtworkId getPreferredArtworkId() const; ArtworkId getPreferredArtworkId() const;
std::vector<ObjectPtr<Medium>> getMediums() const;
// Setters // Setters
void setName(std::string_view name) { _name = name; } void setName(std::string_view name) { _name = name; }
@@ -326,7 +326,6 @@ namespace lms::db
std::vector<ObjectPtr<Artist>> getReleaseArtists() const { return getArtists(TrackArtistLinkType::ReleaseArtist); } std::vector<ObjectPtr<Artist>> getReleaseArtists() const { return getArtists(TrackArtistLinkType::ReleaseArtist); }
bool hasVariousArtists() const; bool hasVariousArtists() const;
std::vector<pointer> getSimilarReleases(std::optional<std::size_t> offset = {}, std::optional<std::size_t> count = {}) const; std::vector<pointer> getSimilarReleases(std::optional<std::size_t> offset = {}, std::optional<std::size_t> count = {}) const;
bool hasDiscSubtitle() const;
template<class Action> template<class Action>
void persist(Action& a) void persist(Action& a)
@@ -43,6 +43,7 @@
#include "database/objects/DirectoryId.hpp" #include "database/objects/DirectoryId.hpp"
#include "database/objects/Filters.hpp" #include "database/objects/Filters.hpp"
#include "database/objects/MediaLibraryId.hpp" #include "database/objects/MediaLibraryId.hpp"
#include "database/objects/MediumId.hpp"
#include "database/objects/ReleaseId.hpp" #include "database/objects/ReleaseId.hpp"
#include "database/objects/TrackEmbeddedImageId.hpp" #include "database/objects/TrackEmbeddedImageId.hpp"
#include "database/objects/TrackId.hpp" #include "database/objects/TrackId.hpp"
@@ -58,6 +59,7 @@ namespace lms::db
class Directory; class Directory;
class TrackEmbeddedImageLink; class TrackEmbeddedImageLink;
class MediaLibrary; class MediaLibrary;
class Medium;
class Release; class Release;
class Session; class Session;
class TrackArtistLink; class TrackArtistLink;
@@ -82,11 +84,11 @@ namespace lms::db
std::string artistName; // only tracks that involve this artist name std::string artistName; // only tracks that involve this artist name
core::EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types core::EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types
bool nonRelease{}; // only tracks that do not belong to a release bool nonRelease{}; // only tracks that do not belong to a release
MediumId medium; // matching this medium
ReleaseId release; // matching this release ReleaseId release; // matching this release
std::string releaseName; // matching this release name std::string releaseName; // matching this release name
TrackListId trackList; // matching this trackList TrackListId trackList; // matching this trackList
std::optional<int> trackNumber; // matching this track number std::optional<int> trackNumber; // matching this track number
std::optional<int> discNumber; // matching this disc number
DirectoryId directory; // if set, tracks in this directory DirectoryId directory; // if set, tracks in this directory
std::optional<std::size_t> fileSize; // if set, tracks that match this file size std::optional<std::size_t> fileSize; // if set, tracks that match this file size
TrackEmbeddedImageId embeddedImageId; // if set, tracks that have this embedded image TrackEmbeddedImageId embeddedImageId; // if set, tracks that have this embedded image
@@ -144,6 +146,11 @@ namespace lms::db
nonRelease = _nonRelease; nonRelease = _nonRelease;
return *this; return *this;
} }
FindParameters& setMedium(MediumId _medium)
{
medium = _medium;
return *this;
}
FindParameters& setRelease(ReleaseId _release) FindParameters& setRelease(ReleaseId _release)
{ {
release = _release; release = _release;
@@ -164,11 +171,6 @@ namespace lms::db
trackNumber = _trackNumber; trackNumber = _trackNumber;
return *this; return *this;
} }
FindParameters& setDiscNumber(int _discNumber)
{
discNumber = _discNumber;
return *this;
}
FindParameters& setDirectory(DirectoryId _directory) FindParameters& setDirectory(DirectoryId _directory)
{ {
directory = _directory; directory = _directory;
@@ -217,9 +219,6 @@ namespace lms::db
// 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; }
void setDiscNumber(std::optional<int> num) { _discNumber = num; }
void setTotalTrack(std::optional<int> totalTrack) { _totalTrack = totalTrack; }
void setDiscSubtitle(std::string_view name) { _discSubtitle = name; }
void setName(std::string_view name); void setName(std::string_view name);
void setAbsoluteFilePath(const std::filesystem::path& filePath); void setAbsoluteFilePath(const std::filesystem::path& filePath);
void setFileSize(std::size_t fileSize) { _fileSize = fileSize; } void setFileSize(std::size_t fileSize) { _fileSize = fileSize; }
@@ -237,13 +236,13 @@ namespace lms::db
void setCopyright(std::string_view copyright); void setCopyright(std::string_view copyright);
void setCopyrightURL(std::string_view copyrightURL); void setCopyrightURL(std::string_view copyrightURL);
void setAdvisory(Advisory advisory) { _advisory = advisory; } void setAdvisory(Advisory advisory) { _advisory = advisory; }
void setTrackReplayGain(std::optional<float> replayGain) { _trackReplayGain = replayGain; } void setReplayGain(std::optional<float> replayGain) { _replayGain = replayGain; }
void setReleaseReplayGain(std::optional<float> replayGain) { _releaseReplayGain = replayGain; } // may be by disc!
void setArtistDisplayName(std::string_view name) { _artistDisplayName = name; } void setArtistDisplayName(std::string_view name) { _artistDisplayName = name; }
void setComment(std::string_view comment) { _comment = comment; } void setComment(std::string_view comment) { _comment = comment; }
void clearArtistLinks(); void clearArtistLinks();
void addArtistLink(const ObjectPtr<TrackArtistLink>& artistLink); void addArtistLink(const ObjectPtr<TrackArtistLink>& artistLink);
void setRelease(ObjectPtr<Release> release) { _release = getDboPtr(release); } void setRelease(ObjectPtr<Release> release) { _release = getDboPtr(release); }
void setMedium(ObjectPtr<Medium> medium) { _medium = getDboPtr(medium); }
void setClusters(const std::vector<ObjectPtr<Cluster>>& clusters); void setClusters(const std::vector<ObjectPtr<Cluster>>& clusters);
void clearLyrics(); void clearLyrics();
void clearEmbeddedLyrics(); void clearEmbeddedLyrics();
@@ -257,9 +256,6 @@ namespace lms::db
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; }
std::optional<std::size_t> getTotalTrack() const { return _totalTrack; }
std::optional<std::size_t> getDiscNumber() const { return _discNumber; }
const std::string& getDiscSubtitle() const { return _discSubtitle; }
std::string getName() const { return _name; } std::string getName() const { return _name; }
const std::filesystem::path& getAbsoluteFilePath() const { return _absoluteFilePath; } const std::filesystem::path& getAbsoluteFilePath() const { return _absoluteFilePath; }
long long getFileSize() const { return _fileSize; } long long getFileSize() const { return _fileSize; }
@@ -281,8 +277,7 @@ namespace lms::db
std::optional<std::string> getCopyright() const; std::optional<std::string> getCopyright() const;
std::optional<std::string> getCopyrightURL() const; std::optional<std::string> getCopyrightURL() const;
Advisory getAdvisory() const { return _advisory; } Advisory getAdvisory() const { return _advisory; }
std::optional<float> getTrackReplayGain() const { return _trackReplayGain; } std::optional<float> getReplayGain() const { return _replayGain; }
std::optional<float> getReleaseReplayGain() const { return _releaseReplayGain; }
std::string_view getArtistDisplayName() const { return _artistDisplayName; } std::string_view getArtistDisplayName() const { return _artistDisplayName; }
std::string_view getComment() const { return _comment; } std::string_view getComment() const { return _comment; }
@@ -292,6 +287,8 @@ namespace lms::db
std::vector<ObjectPtr<TrackArtistLink>> getArtistLinks() const; std::vector<ObjectPtr<TrackArtistLink>> getArtistLinks() const;
ReleaseId getReleaseId() const { return _release.id(); } ReleaseId getReleaseId() const { return _release.id(); }
ObjectPtr<Release> getRelease() const { return _release; } ObjectPtr<Release> getRelease() const { return _release; }
MediumId getMediumId() const { return _medium.id(); }
ObjectPtr<Medium> getMedium() const { return _medium; }
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; ObjectPtr<MediaLibrary> getMediaLibrary() const;
@@ -308,9 +305,6 @@ namespace lms::db
{ {
Wt::Dbo::field(a, _scanVersion, "scan_version"); Wt::Dbo::field(a, _scanVersion, "scan_version");
Wt::Dbo::field(a, _trackNumber, "track_number"); Wt::Dbo::field(a, _trackNumber, "track_number");
Wt::Dbo::field(a, _discNumber, "disc_number");
Wt::Dbo::field(a, _totalTrack, "total_track"); // here in Track since Release does not have concept of "disc" (yet?)
Wt::Dbo::field(a, _discSubtitle, "disc_subtitle"); // here in Track since Release does not have concept of "disc" (yet?)
Wt::Dbo::field(a, _name, "name"); Wt::Dbo::field(a, _name, "name");
Wt::Dbo::field(a, _duration, "duration"); Wt::Dbo::field(a, _duration, "duration");
Wt::Dbo::field(a, _bitrate, "bitrate"); Wt::Dbo::field(a, _bitrate, "bitrate");
@@ -328,11 +322,11 @@ namespace lms::db
Wt::Dbo::field(a, _copyright, "copyright"); Wt::Dbo::field(a, _copyright, "copyright");
Wt::Dbo::field(a, _copyrightURL, "copyright_url"); Wt::Dbo::field(a, _copyrightURL, "copyright_url");
Wt::Dbo::field(a, _advisory, "advisory"); Wt::Dbo::field(a, _advisory, "advisory");
Wt::Dbo::field(a, _trackReplayGain, "track_replay_gain"); Wt::Dbo::field(a, _replayGain, "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, _artistDisplayName, "artist_display_name"); Wt::Dbo::field(a, _artistDisplayName, "artist_display_name");
Wt::Dbo::field(a, _comment, "comment"); // TODO: move in a dedicated table Wt::Dbo::field(a, _comment, "comment"); // TODO: move in a dedicated table
Wt::Dbo::belongsTo(a, _medium, "medium", Wt::Dbo::OnDeleteCascade);
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);
@@ -353,10 +347,7 @@ namespace lms::db
static constexpr std::size_t _maxCopyrightURLLength{ 512 }; static constexpr std::size_t _maxCopyrightURLLength{ 512 };
int _scanVersion{}; int _scanVersion{};
std::optional<int> _trackNumber{}; std::optional<int> _trackNumber;
std::optional<int> _discNumber{};
std::optional<int> _totalTrack{};
std::string _discSubtitle;
std::string _name; std::string _name;
int _bitrate{}; // in bps int _bitrate{}; // in bps
int _bitsPerSample{}; int _bitsPerSample{};
@@ -374,10 +365,10 @@ namespace lms::db
std::string _copyright; std::string _copyright;
std::string _copyrightURL; std::string _copyrightURL;
Advisory _advisory{ Advisory::UnSet }; Advisory _advisory{ Advisory::UnSet };
std::optional<float> _trackReplayGain; std::optional<float> _replayGain;
std::optional<float> _releaseReplayGain;
std::string _artistDisplayName; std::string _artistDisplayName;
std::string _comment; std::string _comment;
Wt::Dbo::ptr<Medium> _medium;
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;
@@ -26,10 +26,9 @@
#include <Wt/Dbo/Field.h> #include <Wt/Dbo/Field.h>
#include <Wt/Dbo/collection.h> #include <Wt/Dbo/collection.h>
#include "core/EnumSet.hpp"
#include "database/Object.hpp" #include "database/Object.hpp"
#include "database/Types.hpp" #include "database/Types.hpp"
#include "database/objects/ArtistId.hpp" #include "database/objects/MediumId.hpp"
#include "database/objects/ReleaseId.hpp" #include "database/objects/ReleaseId.hpp"
#include "database/objects/TrackEmbeddedImageId.hpp" #include "database/objects/TrackEmbeddedImageId.hpp"
#include "database/objects/TrackId.hpp" #include "database/objects/TrackId.hpp"
@@ -49,11 +48,9 @@ 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; MediumId medium;
TrackListId trackList; TrackListId trackList;
std::optional<ImageType> imageType; std::optional<ImageType> imageType;
TrackEmbeddedImageSortMethod sortMethod{ TrackEmbeddedImageSortMethod::None }; TrackEmbeddedImageSortMethod sortMethod{ TrackEmbeddedImageSortMethod::None };
@@ -63,13 +60,6 @@ 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;
@@ -80,9 +70,9 @@ namespace lms::db
release = _release; release = _release;
return *this; return *this;
} }
FindParameters& setDiscNumber(std::optional<int> _discNumber) FindParameters& setMedium(MediumId _medium)
{ {
discNumber = _discNumber; medium = _medium;
return *this; return *this;
} }
FindParameters& setTrackList(TrackListId _trackList) FindParameters& setTrackList(TrackListId _trackList)
+1
View File
@@ -10,6 +10,7 @@ add_executable(test-database
Directory.cpp Directory.cpp
Image.cpp Image.cpp
Listen.cpp Listen.cpp
Medium.cpp
Migration.cpp Migration.cpp
PlayListFile.cpp PlayListFile.cpp
RatedArtist.cpp RatedArtist.cpp
+195
View File
@@ -0,0 +1,195 @@
/*
* 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 "Common.hpp"
#include "database/Types.hpp"
#include "database/objects/Medium.hpp"
#include "database/objects/Track.hpp"
namespace lms::db::tests
{
using ScopedMedium = ScopedEntity<db::Medium>;
TEST_F(DatabaseFixture, Medium)
{
ScopedRelease release{ session, "MyRelease" };
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(Medium::getCount(session), 0);
Medium::pointer medium{ Medium::find(session, MediumId{}) };
ASSERT_EQ(medium, Medium::pointer{});
}
ScopedMedium medium{ session, release.lockAndGet() };
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(Medium::getCount(session), 1);
Medium::pointer foundMedium{ Medium::find(session, medium.getId()) };
ASSERT_NE(foundMedium, Medium::pointer{});
EXPECT_EQ(foundMedium->getReleaseId(), release.getId());
EXPECT_EQ(foundMedium->getPosition(), std::nullopt);
EXPECT_EQ(foundMedium->getMedia(), "");
EXPECT_EQ(foundMedium->getName(), "");
}
{
auto transaction{ session.createWriteTransaction() };
medium.get().modify()->setName("MySubtitle");
medium.get().modify()->setPosition(1);
medium.get().modify()->setMedia("MyMedia");
}
{
auto transaction{ session.createReadTransaction() };
Medium::pointer foundMedium{ Medium::find(session, medium.getId()) };
ASSERT_NE(foundMedium, Medium::pointer{});
EXPECT_EQ(foundMedium->getName(), "MySubtitle");
EXPECT_EQ(foundMedium->getPosition(), 1);
EXPECT_EQ(foundMedium->getMedia(), "MyMedia");
}
}
TEST_F(DatabaseFixture, MediumFindByRelease_noPosition)
{
ScopedRelease release{ session, "MyRelease" };
{
auto transaction{ session.createReadTransaction() };
Medium::pointer foundMedium{ Medium::find(session, release.getId(), std::nullopt) };
ASSERT_EQ(foundMedium, Medium::pointer{});
}
ScopedMedium medium{ session, release.lockAndGet() };
{
auto transaction{ session.createReadTransaction() };
Medium::pointer foundMedium{ Medium::find(session, release.getId(), std::nullopt) };
ASSERT_NE(foundMedium, Medium::pointer{});
EXPECT_EQ(foundMedium->getId(), medium.getId());
}
}
TEST_F(DatabaseFixture, MediumFindByRelease_withPosition)
{
ScopedRelease release{ session, "MyRelease" };
ScopedMedium medium{ session, release.lockAndGet() };
{
auto transaction{ session.createReadTransaction() };
Medium::pointer foundMedium{ Medium::find(session, release.getId(), 1) };
ASSERT_EQ(foundMedium, Medium::pointer{});
}
{
auto transaction{ session.createWriteTransaction() };
medium.get().modify()->setPosition(1);
}
{
auto transaction{ session.createReadTransaction() };
Medium::pointer foundMedium{ Medium::find(session, release.getId(), 1) };
ASSERT_NE(foundMedium, Medium::pointer{});
EXPECT_EQ(foundMedium->getId(), medium.getId());
}
}
TEST_F(DatabaseFixture, Medium_findTracksByMedium)
{
ScopedRelease release{ session, "MyRelease" };
ScopedMedium medium{ session, release.lockAndGet() };
{
auto transaction{ session.createReadTransaction() };
db::Track::FindParameters params;
params.setMedium(medium.getId());
params.setSortMethod(db::TrackSortMethod::TrackNumber);
bool visited{};
db::Track::find(session, params, [&](const db::Track::pointer&) {
visited = true;
});
EXPECT_FALSE(visited);
}
ScopedTrack track1{ session };
ScopedTrack track2{ session };
{
auto transaction{ session.createWriteTransaction() };
track1.get().modify()->setRelease(release.get());
track2.get().modify()->setRelease(release.get());
}
{
auto transaction{ session.createReadTransaction() };
db::Track::FindParameters params;
params.setMedium(medium.getId());
params.setSortMethod(db::TrackSortMethod::TrackNumber);
bool visited{};
db::Track::find(session, params, [&](const db::Track::pointer&) {
visited = true;
});
EXPECT_FALSE(visited);
}
{
auto transaction{ session.createWriteTransaction() };
track1.get().modify()->setMedium(medium.get());
track2.get().modify()->setMedium(medium.get());
track1.get().modify()->setTrackNumber(3);
track2.get().modify()->setTrackNumber(1);
}
{
auto transaction{ session.createReadTransaction() };
db::Track::FindParameters params;
params.setMedium(medium.getId());
params.setSortMethod(db::TrackSortMethod::TrackNumber);
std::vector<db::TrackId> visitedTrackIds;
db::Track::find(session, params, [&](const db::Track::pointer& track) {
visitedTrackIds.push_back(track->getId());
});
ASSERT_EQ(visitedTrackIds.size(), 2);
EXPECT_EQ(visitedTrackIds[0], track2.getId());
EXPECT_EQ(visitedTrackIds[1], track1.getId());
}
}
} // namespace lms::db::tests
+2
View File
@@ -25,6 +25,7 @@
#include "database/objects/AuthToken.hpp" #include "database/objects/AuthToken.hpp"
#include "database/objects/Directory.hpp" #include "database/objects/Directory.hpp"
#include "database/objects/Image.hpp" #include "database/objects/Image.hpp"
#include "database/objects/Medium.hpp"
#include "database/objects/PlayListFile.hpp" #include "database/objects/PlayListFile.hpp"
#include "database/objects/PlayQueue.hpp" #include "database/objects/PlayQueue.hpp"
#include "database/objects/RatedArtist.hpp" #include "database/objects/RatedArtist.hpp"
@@ -353,6 +354,7 @@ VALUES
EXPECT_FALSE(TrackEmbeddedImage::find(session, TrackEmbeddedImageId{})); EXPECT_FALSE(TrackEmbeddedImage::find(session, TrackEmbeddedImageId{}));
EXPECT_FALSE(TrackEmbeddedImageLink::find(session, TrackEmbeddedImageLinkId{})); EXPECT_FALSE(TrackEmbeddedImageLink::find(session, TrackEmbeddedImageLinkId{}));
EXPECT_FALSE(Image::find(session, ImageId{})); EXPECT_FALSE(Image::find(session, ImageId{}));
EXPECT_FALSE(Medium::find(session, MediumId{}));
EXPECT_FALSE(Label::find(session, LabelId{})); EXPECT_FALSE(Label::find(session, LabelId{}));
EXPECT_FALSE(Listen::find(session, ListenId{})); EXPECT_FALSE(Listen::find(session, ListenId{}));
EXPECT_FALSE(PlayListFile::find(session, PlayListFileId{})); EXPECT_FALSE(PlayListFile::find(session, PlayListFileId{}));
+58 -64
View File
@@ -22,13 +22,15 @@
#include "core/PartialDateTime.hpp" #include "core/PartialDateTime.hpp"
#include "database/objects/Artwork.hpp" #include "database/objects/Artwork.hpp"
#include "database/objects/Image.hpp" #include "database/objects/Image.hpp"
#include "database/objects/Medium.hpp"
namespace lms::db::tests namespace lms::db::tests
{ {
using ScopedArtwork = ScopedEntity<db::Artwork>; using ScopedArtwork = ScopedEntity<db::Artwork>;
using ScopedCountry = ScopedEntity<db::Country>;
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 ScopedMedium = ScopedEntity<db::Medium>;
using ScopedReleaseType = ScopedEntity<db::ReleaseType>; using ScopedReleaseType = ScopedEntity<db::ReleaseType>;
TEST_F(DatabaseFixture, Release) TEST_F(DatabaseFixture, Release)
@@ -438,15 +440,12 @@ namespace lms::db::tests
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
track1.get().modify()->setTotalTrack(36);
release1.get().modify()->setTotalDisc(6); release1.get().modify()->setTotalDisc(6);
} }
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
ASSERT_TRUE(track1->getTotalTrack());
EXPECT_EQ(*track1->getTotalTrack(), 36);
ASSERT_TRUE(release1->getTotalDisc()); ASSERT_TRUE(release1->getTotalDisc());
EXPECT_EQ(*release1->getTotalDisc(), 6); EXPECT_EQ(*release1->getTotalDisc(), 6);
} }
@@ -456,15 +455,12 @@ namespace lms::db::tests
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
track2.get().modify()->setRelease(release1.get()); track2.get().modify()->setRelease(release1.get());
track2.get().modify()->setTotalTrack(37);
release1.get().modify()->setTotalDisc(67); release1.get().modify()->setTotalDisc(67);
} }
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
ASSERT_TRUE(track1->getTotalTrack());
EXPECT_EQ(*track1->getTotalTrack(), 36);
ASSERT_TRUE(release1->getTotalDisc()); ASSERT_TRUE(release1->getTotalDisc());
EXPECT_EQ(*release1->getTotalDisc(), 67); EXPECT_EQ(*release1->getTotalDisc(), 67);
} }
@@ -481,18 +477,13 @@ namespace lms::db::tests
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
track3.get().modify()->setRelease(release2.get()); track3.get().modify()->setRelease(release2.get());
track3.get().modify()->setTotalTrack(7);
release2.get().modify()->setTotalDisc(5); release2.get().modify()->setTotalDisc(5);
} }
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
ASSERT_TRUE(track1->getTotalTrack());
EXPECT_EQ(*track1->getTotalTrack(), 36);
ASSERT_TRUE(release1->getTotalDisc()); ASSERT_TRUE(release1->getTotalDisc());
EXPECT_EQ(*release2->getTotalDisc(), 5); EXPECT_EQ(*release2->getTotalDisc(), 5);
ASSERT_TRUE(track3->getTotalTrack());
EXPECT_EQ(*track3->getTotalTrack(), 7);
ASSERT_TRUE(release2->getTotalDisc()); ASSERT_TRUE(release2->getTotalDisc());
EXPECT_EQ(*release2->getTotalDisc(), 5); EXPECT_EQ(*release2->getTotalDisc(), 5);
} }
@@ -501,7 +492,11 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, MultiTracksSingleReleaseFirstTrack) TEST_F(DatabaseFixture, MultiTracksSingleReleaseFirstTrack)
{ {
ScopedRelease release1{ session, "MyRelease1" }; ScopedRelease release1{ session, "MyRelease1" };
ScopedMedium medium1A{ session, release1.lockAndGet() };
ScopedMedium medium1B{ session, release1.lockAndGet() };
ScopedRelease release2{ session, "MyRelease2" }; ScopedRelease release2{ session, "MyRelease2" };
ScopedMedium medium2A{ session, release2.lockAndGet() };
ScopedMedium medium2B{ session, release2.lockAndGet() };
ScopedTrack track1A{ session }; ScopedTrack track1A{ session };
ScopedTrack track1B{ session }; ScopedTrack track1B{ session };
@@ -519,17 +514,21 @@ namespace lms::db::tests
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
track1A.get().modify()->setRelease(release1.get()); track1A.get().modify()->setRelease(release1.get());
track1A.get().modify()->setMedium(medium1A.get());
track1B.get().modify()->setRelease(release1.get()); track1B.get().modify()->setRelease(release1.get());
track1B.get().modify()->setMedium(medium1B.get());
track2A.get().modify()->setRelease(release2.get()); track2A.get().modify()->setRelease(release2.get());
track2A.get().modify()->setMedium(medium2A.get());
track2B.get().modify()->setRelease(release2.get()); track2B.get().modify()->setRelease(release2.get());
track2B.get().modify()->setMedium(medium2B.get());
track1A.get().modify()->setTrackNumber(1); track1A.get().modify()->setTrackNumber(1);
track1B.get().modify()->setTrackNumber(2); track1B.get().modify()->setTrackNumber(2);
track2A.get().modify()->setDiscNumber(2);
track2A.get().modify()->setTrackNumber(1); track2A.get().modify()->setTrackNumber(1);
track2B.get().modify()->setTrackNumber(2); track2B.get().modify()->setTrackNumber(2);
track2B.get().modify()->setDiscNumber(1);
} }
{ {
@@ -537,14 +536,16 @@ namespace lms::db::tests
{ {
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setRelease(release1.getId()).setSortMethod(TrackSortMethod::Release)) }; const auto tracks{ Track::findIds(session, Track::FindParameters{}.setRelease(release1.getId()).setSortMethod(TrackSortMethod::Release)) };
EXPECT_EQ(tracks.results.size(), 2); ASSERT_EQ(tracks.results.size(), 2);
EXPECT_EQ(tracks.results.front(), track1A.getId()); EXPECT_EQ(tracks.results[0], track1A.getId());
EXPECT_EQ(tracks.results[1], track1B.getId());
} }
{ {
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setRelease(release2.getId()).setSortMethod(TrackSortMethod::Release)) }; const auto tracks{ Track::findIds(session, Track::FindParameters{}.setRelease(release2.getId()).setSortMethod(TrackSortMethod::Release)) };
EXPECT_EQ(tracks.results.size(), 2); ASSERT_EQ(tracks.results.size(), 2);
EXPECT_EQ(tracks.results.front(), track2B.getId()); EXPECT_EQ(tracks.results[0], track2A.getId());
EXPECT_EQ(tracks.results[1], track2B.getId());
} }
} }
} }
@@ -815,52 +816,6 @@ namespace lms::db::tests
EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setArtist(artist.getId())), 1); EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setArtist(artist.getId())), 1);
} }
} }
TEST_F(DatabaseFixture, Release_getDiscCount)
{
ScopedRelease release{ session, "MyRelease" };
ScopedTrack track{ session };
ScopedTrack track2{ session };
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(release.get()->getDiscCount(), 0);
}
{
auto transaction{ session.createWriteTransaction() };
track.get().modify()->setRelease(release.get());
}
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(release.get()->getDiscCount(), 0);
}
{
auto transaction{ session.createWriteTransaction() };
track.get().modify()->setDiscNumber(5);
}
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(release.get()->getDiscCount(), 1);
}
{
auto transaction{ session.createWriteTransaction() };
track2.get().modify()->setRelease(release.get());
track2.get().modify()->setDiscNumber(5);
}
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(release.get()->getDiscCount(), 1);
}
{
auto transaction{ session.createWriteTransaction() };
track2.get().modify()->setDiscNumber(6);
}
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(release.get()->getDiscCount(), 2);
}
}
TEST_F(DatabaseFixture, Release_isCompilation) TEST_F(DatabaseFixture, Release_isCompilation)
{ {
ScopedRelease release{ session, "MyRelease" }; ScopedRelease release{ session, "MyRelease" };
@@ -1549,4 +1504,43 @@ namespace lms::db::tests
} }
} }
TEST_F(DatabaseFixture, Release_mediums)
{
ScopedRelease release{ session, "MyRelease" };
{
auto transaction{ session.createReadTransaction() };
const auto mediums{ release->getMediums() };
EXPECT_EQ(mediums.size(), 0);
}
ScopedMedium medium2{ session, release.lockAndGet() };
{
auto transaction{ session.createReadTransaction() };
const auto mediums{ release->getMediums() };
ASSERT_EQ(mediums.size(), 1);
EXPECT_EQ(mediums[0]->getId(), medium2.getId());
}
ScopedMedium medium1{ session, release.lockAndGet() };
{
auto transaction{ session.createWriteTransaction() };
medium1.get().modify()->setPosition(1);
medium2.get().modify()->setPosition(2);
}
{
auto transaction{ session.createReadTransaction() };
const auto mediums{ release->getMediums() };
ASSERT_EQ(mediums.size(), 2);
EXPECT_EQ(mediums[0]->getId(), medium1.getId());
EXPECT_EQ(mediums[1]->getId(), medium2.getId());
}
}
} // namespace lms::db::tests } // namespace lms::db::tests
+36 -48
View File
@@ -20,6 +20,7 @@
#include <limits> #include <limits>
#include "database/Types.hpp" #include "database/Types.hpp"
#include "database/objects/Medium.hpp"
#include "database/objects/TrackEmbeddedImage.hpp" #include "database/objects/TrackEmbeddedImage.hpp"
#include "database/objects/TrackEmbeddedImageLink.hpp" #include "database/objects/TrackEmbeddedImageLink.hpp"
#include "database/objects/TrackList.hpp" #include "database/objects/TrackList.hpp"
@@ -28,6 +29,7 @@
namespace lms::db::tests namespace lms::db::tests
{ {
using ScopedMedium = ScopedEntity<db::Medium>;
using ScopedTrackEmbeddedImage = ScopedEntity<db::TrackEmbeddedImage>; using ScopedTrackEmbeddedImage = ScopedEntity<db::TrackEmbeddedImage>;
using ScopedTrackEmbeddedImageLink = ScopedEntity<db::TrackEmbeddedImageLink>; using ScopedTrackEmbeddedImageLink = ScopedEntity<db::TrackEmbeddedImageLink>;
@@ -113,6 +115,7 @@ namespace lms::db::tests
ScopedTrackEmbeddedImage image{ session }; ScopedTrackEmbeddedImage image{ session };
ScopedTrack track{ session }; ScopedTrack track{ session };
ScopedRelease release{ session, "MyRelease" }; ScopedRelease release{ session, "MyRelease" };
ScopedMedium medium{ session, release.lockAndGet() };
ScopedTrackEmbeddedImageLink link{ session, track.lockAndGet(), image.lockAndGet() }; ScopedTrackEmbeddedImageLink link{ session, track.lockAndGet(), image.lockAndGet() };
{ {
@@ -133,6 +136,7 @@ namespace lms::db::tests
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
track.get().modify()->setRelease(release.get()); track.get().modify()->setRelease(release.get());
track.get().modify()->setMedium(medium.get());
} }
{ {
@@ -151,13 +155,24 @@ namespace lms::db::tests
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
TrackEmbeddedImage::FindParameters params; TrackEmbeddedImage::FindParameters params;
params.setTrack(track.getId()); params.setMedium(medium.getId());
params.setSortMethod(TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc); params.setSortMethod(TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc);
bool visited{}; bool visited{};
TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; }); TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; });
EXPECT_TRUE(visited); EXPECT_TRUE(visited);
} }
{
auto transaction{ session.createReadTransaction() };
TrackEmbeddedImage::FindParameters params;
params.setTrack(track.getId());
bool visited{};
TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; });
EXPECT_TRUE(visited);
}
} }
TEST_F(DatabaseFixture, TrackEmbeddedImage_findByParams_sorts) TEST_F(DatabaseFixture, TrackEmbeddedImage_findByParams_sorts)
@@ -169,6 +184,8 @@ namespace lms::db::tests
ScopedTrack track1{ session }; ScopedTrack track1{ session };
ScopedTrack track2{ session }; ScopedTrack track2{ session };
ScopedRelease release{ session, "MyRelease" }; ScopedRelease release{ session, "MyRelease" };
ScopedMedium medium1{ session, release.lockAndGet() };
ScopedMedium medium2{ session, release.lockAndGet() };
ScopedTrackEmbeddedImageLink link1{ session, track1.lockAndGet(), image1.lockAndGet() }; ScopedTrackEmbeddedImageLink link1{ session, track1.lockAndGet(), image1.lockAndGet() };
ScopedTrackEmbeddedImageLink link2{ session, track1.lockAndGet(), image2.lockAndGet() }; ScopedTrackEmbeddedImageLink link2{ session, track1.lockAndGet(), image2.lockAndGet() };
ScopedTrackEmbeddedImageLink link3{ session, track1.lockAndGet(), image3.lockAndGet() }; ScopedTrackEmbeddedImageLink link3{ session, track1.lockAndGet(), image3.lockAndGet() };
@@ -176,7 +193,11 @@ namespace lms::db::tests
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
medium1.get().modify()->setPosition(1);
medium2.get().modify()->setPosition(2);
track1.get().modify()->setRelease(release.get()); track1.get().modify()->setRelease(release.get());
track1.get().modify()->setMedium(medium1.get());
track1.get().modify()->setTrackNumber(2); track1.get().modify()->setTrackNumber(2);
link1.get().modify()->setType(ImageType::FrontCover); link1.get().modify()->setType(ImageType::FrontCover);
@@ -187,6 +208,7 @@ namespace lms::db::tests
image3.get().modify()->setSize(2000); image3.get().modify()->setSize(2000);
track2.get().modify()->setRelease(release.get()); track2.get().modify()->setRelease(release.get());
track2.get().modify()->setMedium(medium2.get());
track2.get().modify()->setTrackNumber(1); track2.get().modify()->setTrackNumber(1);
link4.get().modify()->setType(ImageType::Media); link4.get().modify()->setType(ImageType::Media);
@@ -213,16 +235,15 @@ namespace lms::db::tests
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
TrackEmbeddedImage::FindParameters params; TrackEmbeddedImage::FindParameters params;
params.setRelease(release.getId()); params.setMedium(medium1.getId());
params.setImageType(ImageType::Media); params.setImageType(ImageType::Media);
params.setSortMethod(TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc); params.setSortMethod(TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc);
std::vector<TrackEmbeddedImageId> visitedIds; std::vector<TrackEmbeddedImageId> visitedIds;
TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer& image) { visitedIds.push_back(image->getId()); }); TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer& image) { visitedIds.push_back(image->getId()); });
ASSERT_EQ(visitedIds.size(), 3); ASSERT_EQ(visitedIds.size(), 2);
EXPECT_EQ(visitedIds[0], image4.getId()); EXPECT_EQ(visitedIds[0], image3.getId());
EXPECT_EQ(visitedIds[1], image3.getId()); EXPECT_EQ(visitedIds[1], image2.getId());
EXPECT_EQ(visitedIds[2], image2.getId());
} }
{ {
@@ -236,9 +257,9 @@ namespace lms::db::tests
std::vector<TrackEmbeddedImageId> visitedIds; std::vector<TrackEmbeddedImageId> visitedIds;
TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer& image) { visitedIds.push_back(image->getId()); }); TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer& image) { visitedIds.push_back(image->getId()); });
ASSERT_EQ(visitedIds.size(), 3); ASSERT_EQ(visitedIds.size(), 3);
EXPECT_EQ(visitedIds[0], image4.getId()); EXPECT_EQ(visitedIds[0], image3.getId());
EXPECT_EQ(visitedIds[1], image3.getId()); EXPECT_EQ(visitedIds[1], image2.getId());
EXPECT_EQ(visitedIds[2], image2.getId()); EXPECT_EQ(visitedIds[2], image4.getId());
} }
{ {
@@ -255,59 +276,26 @@ namespace lms::db::tests
} }
} }
TEST_F(DatabaseFixture, TrackEmbeddedImage_findByParams_artist) TEST_F(DatabaseFixture, TrackEmbeddedImage_findByParams_medium)
{
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 }; ScopedTrackEmbeddedImage image{ session };
ScopedTrack track{ session }; ScopedTrack track{ session };
ScopedRelease release{ session, "MyRelease" }; ScopedRelease release{ session, "MyRelease" };
ScopedMedium medium{ session, release.lockAndGet() };
ScopedMedium otherMedium{ session, release.lockAndGet() };
ScopedTrackEmbeddedImageLink link{ session, track.lockAndGet(), image.lockAndGet() }; ScopedTrackEmbeddedImageLink link{ session, track.lockAndGet(), image.lockAndGet() };
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
track.get().modify()->setRelease(release.get()); track.get().modify()->setRelease(release.get());
track.get().modify()->setDiscNumber(1); track.get().modify()->setMedium(medium.get());
} }
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
TrackEmbeddedImage::FindParameters params; TrackEmbeddedImage::FindParameters params;
params.setDiscNumber(1); params.setMedium(medium.getId());
bool visited{}; bool visited{};
TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer&) { visited = true; }); TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer&) { visited = true; });
@@ -318,7 +306,7 @@ namespace lms::db::tests
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
TrackEmbeddedImage::FindParameters params; TrackEmbeddedImage::FindParameters params;
params.setDiscNumber(2); params.setMedium(2);
bool visited{}; bool visited{};
TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer&) { visited = true; }); TrackEmbeddedImage::find(session, params, [&](const TrackEmbeddedImage::pointer&) { visited = true; });
@@ -32,6 +32,7 @@
#include "database/objects/Cluster.hpp" #include "database/objects/Cluster.hpp"
#include "database/objects/Directory.hpp" #include "database/objects/Directory.hpp"
#include "database/objects/MediaLibrary.hpp" #include "database/objects/MediaLibrary.hpp"
#include "database/objects/Medium.hpp"
#include "database/objects/Release.hpp" #include "database/objects/Release.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "database/objects/TrackArtistLink.hpp" #include "database/objects/TrackArtistLink.hpp"
@@ -222,6 +223,26 @@ namespace lms::scanner
return release; return release;
} }
db::Medium::pointer getOrCreateMedium(db::Session& session, const metadata::Medium& medium, const db::Release::pointer& release)
{
db::Medium::pointer dbMedium{ db::Medium::find(session, release->getId(), medium.position) };
if (!dbMedium)
dbMedium = session.create<db::Medium>(release);
if (dbMedium->getPosition() != medium.position)
dbMedium.modify()->setPosition(medium.position);
if (dbMedium->getMedia() != medium.media)
dbMedium.modify()->setMedia(medium.media);
if (dbMedium->getName() != medium.name)
dbMedium.modify()->setName(medium.name);
if (dbMedium->getTrackCount() != medium.trackCount)
dbMedium.modify()->setTrackCount(medium.trackCount);
if (dbMedium->getReplayGain() != medium.replayGain)
dbMedium.modify()->setReplayGain(medium.replayGain);
return dbMedium;
}
std::vector<db::Cluster::pointer> getOrCreateClusters(db::Session& session, const metadata::Track& track) std::vector<db::Cluster::pointer> getOrCreateClusters(db::Session& session, const metadata::Track& track)
{ {
std::vector<db::Cluster::pointer> clusters; std::vector<db::Cluster::pointer> clusters;
@@ -387,8 +408,6 @@ namespace lms::scanner
params.setFileSize(fileSize); params.setFileSize(fileSize);
if (parsedTrack.medium) if (parsedTrack.medium)
{ {
if (parsedTrack.medium->position)
params.setDiscNumber(*parsedTrack.medium->position);
if (parsedTrack.medium->release) if (parsedTrack.medium->release)
params.setReleaseName(parsedTrack.medium->release->name); params.setReleaseName(parsedTrack.medium->release->name);
} }
@@ -691,17 +710,22 @@ namespace lms::scanner
for (const auto& [role, performers] : _parsedTrack->performerArtists) for (const auto& [role, performers] : _parsedTrack->performerArtists)
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Performer, role, performers, allowFallback); createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Performer, role, performers, allowFallback);
// For now, alway tie a medium to a release, and a release mst have at least one medium, even if no disc number is set
if (_parsedTrack->medium && _parsedTrack->medium->release) if (_parsedTrack->medium && _parsedTrack->medium->release)
track.modify()->setRelease(getOrCreateRelease(dbSession, *_parsedTrack->medium->release, directory)); {
db::Release::pointer release{ getOrCreateRelease(dbSession, *_parsedTrack->medium->release, directory) };
assert(release);
track.modify()->setRelease(release);
track.modify()->setMedium(getOrCreateMedium(dbSession, *_parsedTrack->medium, release));
}
else else
{
track.modify()->setRelease({}); track.modify()->setRelease({});
track.modify()->setTotalTrack(_parsedTrack->medium ? _parsedTrack->medium->trackCount : std::nullopt); track.modify()->setMedium({});
track.modify()->setReleaseReplayGain(_parsedTrack->medium ? _parsedTrack->medium->replayGain : std::nullopt); }
track.modify()->setDiscSubtitle(_parsedTrack->medium ? _parsedTrack->medium->name : "");
track.modify()->setClusters(getOrCreateClusters(dbSession, *_parsedTrack)); track.modify()->setClusters(getOrCreateClusters(dbSession, *_parsedTrack));
track.modify()->setName(title); track.modify()->setName(title);
track.modify()->setTrackNumber(_parsedTrack->position); track.modify()->setTrackNumber(_parsedTrack->position);
track.modify()->setDiscNumber(_parsedTrack->medium ? _parsedTrack->medium->position : std::nullopt);
track.modify()->setDate(_parsedTrack->date); track.modify()->setDate(_parsedTrack->date);
track.modify()->setOriginalDate(_parsedTrack->originalDate); track.modify()->setOriginalDate(_parsedTrack->originalDate);
if (!track->getOriginalDate().isValid() && _parsedTrack->originalYear) if (!track->getOriginalDate().isValid() && _parsedTrack->originalYear)
@@ -719,7 +743,7 @@ namespace lms::scanner
track.modify()->setCopyrightURL(_parsedTrack->copyrightURL); track.modify()->setCopyrightURL(_parsedTrack->copyrightURL);
track.modify()->setAdvisory(getAdvisory(_parsedTrack->advisory)); track.modify()->setAdvisory(getAdvisory(_parsedTrack->advisory));
track.modify()->setComment(!_parsedTrack->comments.empty() ? _parsedTrack->comments.front() : ""); // only take the first one for now track.modify()->setComment(!_parsedTrack->comments.empty() ? _parsedTrack->comments.front() : ""); // only take the first one for now
track.modify()->setTrackReplayGain(_parsedTrack->replayGain); track.modify()->setReplayGain(_parsedTrack->replayGain);
track.modify()->setArtistDisplayName(_parsedTrack->artistDisplayName); track.modify()->setArtistDisplayName(_parsedTrack->artistDisplayName);
track.modify()->clearEmbeddedLyrics(); track.modify()->clearEmbeddedLyrics();
@@ -106,15 +106,13 @@ namespace lms::scanner
return res; return res;
// fallback on another track of the same disc // fallback on another track of the same disc
const db::ReleaseId releaseId{ track->getReleaseId() }; const db::MediumId mediumId{ track->getMediumId() };
if (!releaseId.isValid()) if (!mediumId.isValid())
return res; return res;
if (const auto discNumber{ track->getDiscNumber() })
{ {
db::TrackEmbeddedImage::FindParameters params; db::TrackEmbeddedImage::FindParameters params;
params.setRelease(releaseId); params.setMedium(track->getMediumId());
params.setDiscNumber(discNumber);
params.setImageType(db::ImageType::Media); params.setImageType(db::ImageType::Media);
params.setSortMethod(db::TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc); params.setSortMethod(db::TrackEmbeddedImageSortMethod::TrackNumberThenSizeDesc);
+6 -3
View File
@@ -27,6 +27,7 @@
#include "database/objects/Artwork.hpp" #include "database/objects/Artwork.hpp"
#include "database/objects/Cluster.hpp" #include "database/objects/Cluster.hpp"
#include "database/objects/Directory.hpp" #include "database/objects/Directory.hpp"
#include "database/objects/Medium.hpp"
#include "database/objects/Release.hpp" #include "database/objects/Release.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "database/objects/User.hpp" #include "database/objects/User.hpp"
@@ -207,10 +208,12 @@ namespace lms::api::subsonic
albumNode.addArrayValue("releaseTypes", releaseType); albumNode.addArrayValue("releaseTypes", releaseType);
albumNode.createEmptyArrayChild("discTitles"); albumNode.createEmptyArrayChild("discTitles");
for (const DiscInfo& discInfo : release->getDiscs()) for (const auto& medium : release->getMediums())
{ {
if (!discInfo.name.empty()) if (medium->getName().empty())
albumNode.addArrayChild("discTitles", createDiscTitle(discInfo)); continue;
albumNode.addArrayChild("discTitles", createDiscTitle(medium));
} }
albumNode.createEmptyArrayChild("recordLabels"); albumNode.createEmptyArrayChild("recordLabels");
@@ -19,14 +19,16 @@
#include "responses/DiscTitle.hpp" #include "responses/DiscTitle.hpp"
#include "database/objects/Medium.hpp"
namespace lms::api::subsonic namespace lms::api::subsonic
{ {
Response::Node createDiscTitle(const db::DiscInfo& discInfo) Response::Node createDiscTitle(const db::ObjectPtr<db::Medium>& medium)
{ {
Response::Node discTitleNode; Response::Node discTitleNode;
discTitleNode.setAttribute("disc", discInfo.position); discTitleNode.setAttribute("disc", medium->getPosition() ? *medium->getPosition() : 0);
discTitleNode.setAttribute("title", discInfo.name); discTitleNode.setAttribute("title", medium->getName());
return discTitleNode; return discTitleNode;
} }
@@ -19,11 +19,16 @@
#pragma once #pragma once
#include "database/Types.hpp" #include "database/Object.hpp"
#include "SubsonicResponse.hpp" #include "SubsonicResponse.hpp"
namespace lms::db
{
class Medium;
}
namespace lms::api::subsonic namespace lms::api::subsonic
{ {
Response::Node createDiscTitle(const db::DiscInfo& discInfo); Response::Node createDiscTitle(const db::ObjectPtr<db::Medium>& medium);
} }
@@ -19,19 +19,23 @@
#include "responses/ReplayGain.hpp" #include "responses/ReplayGain.hpp"
#include "database/objects/Medium.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
namespace lms::api::subsonic namespace lms::api::subsonic
{ {
Response::Node createReplayGainNode(const db::ObjectPtr<db::Track>& track) Response::Node createReplayGainNode(const db::Track::pointer& track, const db::Medium::pointer& medium)
{ {
Response::Node replayGainNode; Response::Node replayGainNode;
if (const auto trackReplayGain{ track->getTrackReplayGain() }) if (const auto trackReplayGain{ track->getReplayGain() })
replayGainNode.setAttribute("trackGain", *trackReplayGain); replayGainNode.setAttribute("trackGain", *trackReplayGain);
if (const auto releaseReplayGain{ track->getReleaseReplayGain() }) if (medium)
replayGainNode.setAttribute("albumGain", *releaseReplayGain); {
if (const auto releaseReplayGain{ medium->getReplayGain() })
replayGainNode.setAttribute("albumGain", *releaseReplayGain);
}
return replayGainNode; return replayGainNode;
} }
@@ -25,10 +25,11 @@
namespace lms::db namespace lms::db
{ {
class Medium;
class Track; class Track;
} } // namespace lms::db
namespace lms::api::subsonic namespace lms::api::subsonic
{ {
Response::Node createReplayGainNode(const db::ObjectPtr<db::Track>& track); Response::Node createReplayGainNode(const db::ObjectPtr<db::Track>& track, const db::ObjectPtr<db::Medium>& medium);
} }
+6 -3
View File
@@ -33,6 +33,7 @@
#include "database/objects/Cluster.hpp" #include "database/objects/Cluster.hpp"
#include "database/objects/Directory.hpp" #include "database/objects/Directory.hpp"
#include "database/objects/MediaLibrary.hpp" #include "database/objects/MediaLibrary.hpp"
#include "database/objects/Medium.hpp"
#include "database/objects/Release.hpp" #include "database/objects/Release.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "database/objects/TrackArtistLink.hpp" #include "database/objects/TrackArtistLink.hpp"
@@ -78,6 +79,8 @@ namespace lms::api::subsonic
{ {
LMS_SCOPED_TRACE_DETAILED("Subsonic", "CreateSong"); LMS_SCOPED_TRACE_DETAILED("Subsonic", "CreateSong");
const auto medium{ track->getMedium() };
Response::Node trackResponse; Response::Node trackResponse;
if (!id3) if (!id3)
@@ -91,8 +94,8 @@ namespace lms::api::subsonic
trackResponse.setAttribute("title", track->getName()); trackResponse.setAttribute("title", track->getName());
if (track->getTrackNumber()) if (track->getTrackNumber())
trackResponse.setAttribute("track", *track->getTrackNumber()); trackResponse.setAttribute("track", *track->getTrackNumber());
if (track->getDiscNumber()) if (medium && medium->getPosition())
trackResponse.setAttribute("discNumber", *track->getDiscNumber()); trackResponse.setAttribute("discNumber", *medium->getPosition());
if (const auto originalYear{ track->getOriginalYear() }) if (const auto originalYear{ track->getOriginalYear() })
trackResponse.setAttribute("year", *originalYear); trackResponse.setAttribute("year", *originalYear);
else if (const auto year{ track->getYear() }) else if (const auto year{ track->getYear() })
@@ -254,7 +257,7 @@ namespace lms::api::subsonic
}; };
trackResponse.setAttribute("explicitStatus", advisoryToExplicitStatus(track->getAdvisory())); trackResponse.setAttribute("explicitStatus", advisoryToExplicitStatus(track->getAdvisory()));
trackResponse.addChild("replayGain", createReplayGainNode(track)); trackResponse.addChild("replayGain", createReplayGainNode(track, medium));
return trackResponse; return trackResponse;
} }
+17 -8
View File
@@ -34,6 +34,7 @@
#include "core/Service.hpp" #include "core/Service.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/objects/Artist.hpp" #include "database/objects/Artist.hpp"
#include "database/objects/Medium.hpp"
#include "database/objects/Release.hpp" #include "database/objects/Release.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "database/objects/TrackList.hpp" #include "database/objects/TrackList.hpp"
@@ -621,14 +622,20 @@ namespace lms::ui
return std::nullopt; return std::nullopt;
case MediaPlayer::Settings::ReplayGain::Mode::Track: case MediaPlayer::Settings::ReplayGain::Mode::Track:
gain = track->getTrackReplayGain(); gain = track->getReplayGain();
break; break;
case MediaPlayer::Settings::ReplayGain::Mode::Release: case MediaPlayer::Settings::ReplayGain::Mode::Release:
gain = track->getReleaseReplayGain(); {
if (!gain) const auto medium{ track->getMedium() };
gain = track->getTrackReplayGain(); if (medium && medium->getReplayGain())
break; gain = medium->getReplayGain();
if (!gain)
gain = track->getReplayGain();
break;
}
case MediaPlayer::Settings::ReplayGain::Mode::Auto: case MediaPlayer::Settings::ReplayGain::Mode::Auto:
{ {
@@ -641,13 +648,15 @@ namespace lms::ui
if ((prevTrack && prevTrack->getRelease() && prevTrack->getRelease() == track->getRelease()) if ((prevTrack && prevTrack->getRelease() && prevTrack->getRelease() == track->getRelease())
|| (nextTrack && nextTrack->getRelease() && nextTrack->getRelease() == track->getRelease())) || (nextTrack && nextTrack->getRelease() && nextTrack->getRelease() == track->getRelease()))
{ {
gain = track->getReleaseReplayGain(); const auto medium{ track->getMedium() };
if (medium && medium->getReplayGain())
gain = medium->getReplayGain();
if (!gain) if (!gain)
gain = track->getTrackReplayGain(); gain = track->getReplayGain();
} }
else else
{ {
gain = track->getTrackReplayGain(); gain = track->getReplayGain();
} }
break; break;
} }
+13 -25
View File
@@ -61,14 +61,13 @@ namespace lms::ui
std::vector<db::TrackId> getReleasesTracks(db::Session& session, const std::vector<db::ReleaseId>& releasesId, const Filters& filters, std::size_t maxTrackCount) std::vector<db::TrackId> getReleasesTracks(db::Session& session, const std::vector<db::ReleaseId>& releasesId, const Filters& filters, std::size_t maxTrackCount)
{ {
using namespace db;
assert(maxTrackCount); assert(maxTrackCount);
std::vector<TrackId> res; std::vector<db::TrackId> res;
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
for (const ReleaseId releaseId : releasesId) for (const db::ReleaseId releaseId : releasesId)
{ {
db::Track::FindParameters params; db::Track::FindParameters params;
params.setRelease(releaseId); params.setRelease(releaseId);
@@ -88,34 +87,23 @@ namespace lms::ui
return res; return res;
} }
std::vector<db::TrackId> getDiscTracks(db::Session& session, const std::vector<PlayQueueController::Disc>& discs, const Filters& filters, std::size_t maxTrackCount) std::vector<db::TrackId> getMediumTracks(db::Session& session, db::MediumId medium, const Filters& filters, std::size_t maxTrackCount)
{ {
using namespace db;
assert(maxTrackCount); assert(maxTrackCount);
std::vector<TrackId> res; std::vector<db::TrackId> res;
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
for (const PlayQueueController::Disc& disc : discs) db::Track::FindParameters params;
{ params.setMedium(medium);
db::Track::FindParameters params; params.setSortMethod(db::TrackSortMethod::TrackNumber);
params.setRelease(disc.releaseId); params.setFilters(filters.getDbFilters());
params.setSortMethod(db::TrackSortMethod::Release); params.setRange(db::Range{ .offset = 0, .size = maxTrackCount });
params.setDiscNumber(disc.discNumber);
params.setFilters(filters.getDbFilters());
params.setRange(db::Range{ 0, maxTrackCount - res.size() });
const auto tracks{ db::Track::findIds(session, params) }; const auto tracks{ db::Track::findIds(session, params) };
res.reserve(res.size() + tracks.results.size()); return tracks.results;
res.insert(std::end(res), std::cbegin(tracks.results), std::cend(tracks.results));
if (res.size() == maxTrackCount)
break;
}
return res;
} }
std::vector<db::TrackId> getTrackListTracks(db::Session& session, db::TrackListId trackListId, const Filters& filters, std::size_t maxTrackCount) std::vector<db::TrackId> getTrackListTracks(db::Session& session, db::TrackListId trackListId, const Filters& filters, std::size_t maxTrackCount)
@@ -179,9 +167,9 @@ namespace lms::ui
processCommand(command, tracks); processCommand(command, tracks);
} }
void PlayQueueController::processCommand(Command command, const std::vector<Disc>& discs) void PlayQueueController::processCommand(Command command, db::MediumId medium)
{ {
const std::vector<db::TrackId> tracks{ getDiscTracks(LmsApp->getDbSession(), discs, _filters, _maxTrackCountToEnqueue) }; const std::vector<db::TrackId> tracks{ getMediumTracks(LmsApp->getDbSession(), medium, _filters, _maxTrackCountToEnqueue) };
processCommand(command, tracks); processCommand(command, tracks);
} }
+2 -6
View File
@@ -22,6 +22,7 @@
#include <vector> #include <vector>
#include "database/objects/ArtistId.hpp" #include "database/objects/ArtistId.hpp"
#include "database/objects/MediumId.hpp"
#include "database/objects/ReleaseId.hpp" #include "database/objects/ReleaseId.hpp"
#include "database/objects/TrackId.hpp" #include "database/objects/TrackId.hpp"
#include "database/objects/TrackListId.hpp" #include "database/objects/TrackListId.hpp"
@@ -49,12 +50,7 @@ namespace lms::ui
void processCommand(Command command, const std::vector<db::ReleaseId>& releases); void processCommand(Command command, const std::vector<db::ReleaseId>& releases);
void processCommand(Command command, const std::vector<db::TrackId>& tracks); void processCommand(Command command, const std::vector<db::TrackId>& tracks);
struct Disc void processCommand(Command command, db::MediumId medium);
{
db::ReleaseId releaseId;
size_t discNumber;
};
void processCommand(Command command, const std::vector<Disc>& discs);
void processCommand(Command command, db::TrackListId trackList); void processCommand(Command command, db::TrackListId trackList);
void playTrackInRelease(db::TrackId track); void playTrackInRelease(db::TrackId track);
+128 -122
View File
@@ -19,6 +19,7 @@
#include "ReleaseView.hpp" #include "ReleaseView.hpp"
#include <Wt/WContainerWidget.h>
#include <map> #include <map>
#include <Wt/WAnchor.h> #include <Wt/WAnchor.h>
@@ -32,6 +33,7 @@
#include "database/Types.hpp" #include "database/Types.hpp"
#include "database/objects/Artist.hpp" #include "database/objects/Artist.hpp"
#include "database/objects/Cluster.hpp" #include "database/objects/Cluster.hpp"
#include "database/objects/Medium.hpp"
#include "database/objects/Release.hpp" #include "database/objects/Release.hpp"
#include "database/objects/ScanSettings.hpp" #include "database/objects/ScanSettings.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
@@ -233,6 +235,11 @@ namespace lms::ui
return res; return res;
} }
std::unique_ptr<Wt::WTemplate> createNoDiscContainer()
{
return std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Release.template.entry-nodisc"));
}
} // namespace } // namespace
Release::Release(Filters& filters, PlayQueueController& playQueueController) Release::Release(Filters& filters, PlayQueueController& playQueueController)
@@ -275,9 +282,10 @@ namespace lms::ui
auto similarReleasesIds{ core::Service<recommendation::IRecommendationService>::get()->getSimilarReleases(*releaseId, 5) }; auto similarReleasesIds{ core::Service<recommendation::IRecommendationService>::get()->getSimilarReleases(*releaseId, 5) };
auto transaction{ LmsApp->getDbSession().createReadTransaction() }; auto& session{ LmsApp->getDbSession() };
auto transaction{ session.createReadTransaction() };
const db::Release::pointer release{ db::Release::find(LmsApp->getDbSession(), *releaseId) }; const db::Release::pointer release{ db::Release::find(session, *releaseId) };
if (!release) if (!release)
throw ReleaseNotFoundException{}; throw ReleaseNotFoundException{};
@@ -310,7 +318,7 @@ namespace lms::ui
Wt::WContainerWidget* clusterContainers{ bindNew<Wt::WContainerWidget>("clusters") }; Wt::WContainerWidget* clusterContainers{ bindNew<Wt::WContainerWidget>("clusters") };
{ {
const auto clusterTypeIds{ ClusterType::findIds(LmsApp->getDbSession()).results }; const auto clusterTypeIds{ ClusterType::findIds(session).results };
const auto clusterGroups{ release->getClusterGroups(clusterTypeIds, 3) }; const auto clusterGroups{ release->getClusterGroups(clusterTypeIds, 3) };
for (const auto& clusters : clusterGroups) for (const auto& clusters : clusterGroups)
@@ -377,113 +385,86 @@ namespace lms::ui
}); });
} }
Wt::WContainerWidget* rootContainer{ bindNew<Wt::WContainerWidget>("container") }; refreshDiscs(release);
}
bool displayTrackArtists{ shouldDisplayTrackArtists(*releaseId) }; void Release::refreshArtwork(db::ArtworkId artworkId)
const auto totalDisc{ release->getTotalDisc() }; {
const std::size_t discCount{ release->getDiscCount() }; std::unique_ptr<Wt::WImage> artworkImage;
const bool hasDiscSubtitle{ release->hasDiscSubtitle() }; if (artworkId.isValid())
const bool useSubtitleContainers{ (discCount > 1) || (totalDisc && *totalDisc > 1) || hasDiscSubtitle }; {
artworkImage = utils::createArtworkImage(artworkId, ArtworkResource::DefaultArtworkType::Release, ArtworkResource::Size::Large);
artworkImage->addStyleClass("Lms-cursor-pointer"); // HACK
}
else
artworkImage = utils::createDefaultArtworkImage(ArtworkResource::DefaultArtworkType::Release);
// Expect to be called in asc order auto* image{ bindWidget<Wt::WImage>("artwork", std::move(artworkImage)) };
std::map<std::size_t, Wt::WContainerWidget*> trackContainers; if (artworkId.isValid())
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)) image->clicked().connect([artworkId] {
return it->second; utils::showArtworkModal(Wt::WLink{ LmsApp->getArtworkResource()->getArtworkUrl(artworkId, ArtworkResource::DefaultArtworkType::Release) });
Template* disc{ rootContainer->addNew<Template>(Wt::WString::tr("Lms.Explore.Release.template.entry-disc")) };
disc->addFunction("id", &Wt::WTemplate::Functions::id);
if (mediaArtworkId.isValid())
{
auto image{ utils::createArtworkImage(mediaArtworkId, ArtworkResource::DefaultArtworkType::Release, 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, ArtworkResource::DefaultArtworkType::Release) });
});
disc->bindWidget<Wt::WImage>("artwork", std::move(image));
}
if (discSubtitle.empty())
disc->bindNew<Wt::WText>("disc-title", Wt::WString::tr("Lms.Explore.Release.disc").arg(discNumber));
else
disc->bindString("disc-title", Wt::WString::fromUTF8(discSubtitle), Wt::TextFormat::Plain);
Wt::WPushButton* playBtn{ disc->bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.template.play-btn"), Wt::TextFormat::XHTML) };
playBtn->clicked().connect([this, releaseId, discNumber] {
_playQueueController.processCommand(PlayQueueController::Command::Play, { PlayQueueController::Disc{ releaseId, discNumber } });
}); });
disc->bindNew<Wt::WPushButton>("more-btn", Wt::WString::tr("Lms.template.more-btn"), Wt::TextFormat::XHTML); }
disc->bindNew<Wt::WPushButton>("play", Wt::WString::tr("Lms.Explore.play")) }
->clicked()
.connect([this, releaseId, discNumber] {
_playQueueController.processCommand(PlayQueueController::Command::Play, { PlayQueueController::Disc{ releaseId, discNumber } });
});
disc->bindNew<Wt::WPushButton>("play-next", Wt::WString::tr("Lms.Explore.play-next"))
->clicked()
.connect([this, releaseId, discNumber] {
_playQueueController.processCommand(PlayQueueController::Command::PlayNext, { PlayQueueController::Disc{ releaseId, discNumber } });
});
disc->bindNew<Wt::WPushButton>("play-shuffled", Wt::WString::tr("Lms.Explore.play-shuffled"), Wt::TextFormat::Plain) void Release::refreshReleaseArtists(const db::Release::pointer& release)
->clicked() {
.connect([this, releaseId, discNumber] { auto container{ utils::createArtistsAnchorsForRelease(release) };
_playQueueController.processCommand(PlayQueueController::Command::PlayShuffled, { PlayQueueController::Disc{ releaseId, discNumber } }); if (container)
}); {
disc->bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last")) setCondition("if-has-release-artists", true);
->clicked() bindWidget("artists", std::move(container));
.connect([this, releaseId, discNumber] { }
_playQueueController.processCommand(PlayQueueController::Command::PlayOrAddLast, { PlayQueueController::Disc{ releaseId, discNumber } }); }
});
void Release::refreshDiscs(const db::Release::pointer& release)
{
Wt::WContainerWidget* discsContainer{ bindNew<Wt::WContainerWidget>("disc-container") };
const std::vector<db::Medium::pointer> mediums{ release->getMediums() };
if (mediums.empty())
return;
const bool displayTrackArtists{ shouldDisplayTrackArtists(_releaseId) };
const auto totalDisc{ release->getTotalDisc() };
const bool createDiscs{ mediums.size() > 1
|| !mediums[0]->getName().empty()
|| totalDisc > 1 };
for (const db::Medium::pointer& medium : mediums)
{
Wt::WTemplate* disc{ discsContainer->addWidget<Wt::WTemplate>(createDiscs ? createDisc(medium) : createNoDiscContainer()) };
Wt::WContainerWidget* tracksContainer{ disc->bindNew<Wt::WContainerWidget>("tracks") }; Wt::WContainerWidget* tracksContainer{ disc->bindNew<Wt::WContainerWidget>("tracks") };
trackContainers[discNumber] = tracksContainer; createTracks(tracksContainer, medium, displayTrackArtists);
}
return tracksContainer; }
};
Wt::WContainerWidget* noDiscTracksContainer{};
auto getOrAddNoDiscContainer = [&] {
if (noDiscTracksContainer)
return noDiscTracksContainer;
Wt::WTemplate* disc{ rootContainer->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Release.template.entry-nodisc")) };
noDiscTracksContainer = disc->bindNew<Wt::WContainerWidget>("tracks");
return noDiscTracksContainer;
};
void Release::createTracks(Wt::WContainerWidget* tracksContainer, const db::Medium::pointer& medium, bool displayTrackArtists)
{
db::Track::FindParameters params; db::Track::FindParameters params;
params.setRelease(_releaseId); params.setMedium(medium->getId());
params.setSortMethod(db::TrackSortMethod::Release); params.setSortMethod(db::TrackSortMethod::TrackNumber);
params.setFilters(_filters.getDbFilters()); // TODO: do we really want to hide all tracks when a release does not match the current label filter? params.setFilters(_filters.getDbFilters()); // TODO: do we really want to hide all tracks when a release does not match the current label filter?
db::Track::find(LmsApp->getDbSession(), params, [&](const db::Track::pointer& track) { db::Track::find(LmsApp->getDbSession(), params, [&](const db::Track::pointer& track) {
const db::TrackId trackId{ track->getId() }; const db::TrackId trackId{ track->getId() };
const auto discNumber{ track->getDiscNumber() };
Wt::WContainerWidget* container{}; Template* entry{ tracksContainer->addNew<Template>(Wt::WString::tr("Lms.Explore.Release.template.entry")) };
if (useSubtitleContainers && discNumber)
container = getOrAddDiscContainer(*discNumber, track->getDiscSubtitle(), track->getPreferredMediaArtworkId());
else if (hasDiscSubtitle && !discNumber)
container = getOrAddDiscContainer(0, track->getDiscSubtitle(), track->getPreferredMediaArtworkId());
else
container = getOrAddNoDiscContainer();
Template* entry{ container->addNew<Template>(Wt::WString::tr("Lms.Explore.Release.template.entry")) };
entry->addFunction("id", &Wt::WTemplate::Functions::id); entry->addFunction("id", &Wt::WTemplate::Functions::id);
entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain); entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain);
const auto artists{ track->getArtistIds({ TrackArtistLinkType::Artist }) }; if (displayTrackArtists)
if (displayTrackArtists && !artists.empty())
{ {
entry->setCondition("if-has-artists", true); const auto artists{ track->getArtistIds({ TrackArtistLinkType::Artist }) };
entry->bindWidget("artists", utils::createArtistDisplayNameWithAnchors(track->getArtistDisplayName(), artists)); if (!artists.empty())
entry->bindWidget("artists-md", utils::createArtistDisplayNameWithAnchors(track->getArtistDisplayName(), artists)); {
entry->setCondition("if-has-artists", true);
entry->bindWidget("artists", utils::createArtistDisplayNameWithAnchors(track->getArtistDisplayName(), artists));
entry->bindWidget("artists-md", utils::createArtistDisplayNameWithAnchors(track->getArtistDisplayName(), artists));
}
} }
fillTrackArtistLinks(entry, track->getId()); fillTrackArtistLinks(entry, track->getId());
@@ -565,36 +546,6 @@ namespace lms::ui
}); });
} }
void Release::refreshArtwork(db::ArtworkId artworkId)
{
std::unique_ptr<Wt::WImage> artworkImage;
if (artworkId.isValid())
{
artworkImage = utils::createArtworkImage(artworkId, ArtworkResource::DefaultArtworkType::Release, ArtworkResource::Size::Large);
artworkImage->addStyleClass("Lms-cursor-pointer"); // HACK
}
else
artworkImage = utils::createDefaultArtworkImage(ArtworkResource::DefaultArtworkType::Release);
auto* image{ bindWidget<Wt::WImage>("artwork", std::move(artworkImage)) };
if (artworkId.isValid())
{
image->clicked().connect([artworkId] {
utils::showArtworkModal(Wt::WLink{ LmsApp->getArtworkResource()->getArtworkUrl(artworkId, ArtworkResource::DefaultArtworkType::Release) });
});
}
}
void Release::refreshReleaseArtists(const db::Release::pointer& release)
{
auto container{ utils::createArtistsAnchorsForRelease(release) };
if (container)
{
setCondition("if-has-release-artists", true);
bindWidget("artists", std::move(container));
}
}
void Release::refreshCopyright(const db::Release::pointer& release) void Release::refreshCopyright(const db::Release::pointer& release)
{ {
std::optional<std::string> copyright{ release->getCopyright() }; std::optional<std::string> copyright{ release->getCopyright() };
@@ -680,4 +631,59 @@ namespace lms::ui
} }
} }
std::unique_ptr<Wt::WTemplate> Release::createDisc(const db::Medium::pointer& medium)
{
const db::MediumId mediumId{ medium->getId() };
std::unique_ptr<Wt::WTemplate> disc{ std::make_unique<Template>(Wt::WString::tr("Lms.Explore.Release.template.entry-disc")) };
disc->addFunction("id", &Wt::WTemplate::Functions::id);
if (db::ArtworkId artworkId{ medium->getPreferredArtworkId() }; artworkId.isValid())
{
auto image{ utils::createArtworkImage(artworkId, ArtworkResource::DefaultArtworkType::Release, ArtworkResource::Size::Small) };
disc->setCondition("if-has-artwork", true);
image->addStyleClass("Lms-cover-track rounded"); // HACK
image->clicked().connect([artworkId] {
utils::showArtworkModal(Wt::WLink{ LmsApp->getArtworkResource()->getArtworkUrl(artworkId, ArtworkResource::DefaultArtworkType::Release) });
});
disc->bindWidget<Wt::WImage>("artwork", std::move(image));
}
if (medium->getName().empty())
disc->bindNew<Wt::WText>("disc-title", Wt::WString::tr("Lms.Explore.Release.disc").arg(medium->getPosition() ? *medium->getPosition() : 1 /* TODO */));
else
disc->bindString("disc-title", Wt::WString::fromUTF8(std::string{ medium->getName() }), Wt::TextFormat::Plain);
Wt::WPushButton* playBtn{ disc->bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.template.play-btn"), Wt::TextFormat::XHTML) };
playBtn->clicked().connect([this, mediumId] {
_playQueueController.processCommand(PlayQueueController::Command::Play, mediumId);
});
disc->bindNew<Wt::WPushButton>("more-btn", Wt::WString::tr("Lms.template.more-btn"), Wt::TextFormat::XHTML);
disc->bindNew<Wt::WPushButton>("play", Wt::WString::tr("Lms.Explore.play"))
->clicked()
.connect([this, mediumId] {
_playQueueController.processCommand(PlayQueueController::Command::Play, mediumId);
});
disc->bindNew<Wt::WPushButton>("play-next", Wt::WString::tr("Lms.Explore.play-next"))
->clicked()
.connect([this, mediumId] {
_playQueueController.processCommand(PlayQueueController::Command::PlayNext, mediumId);
});
disc->bindNew<Wt::WPushButton>("play-shuffled", Wt::WString::tr("Lms.Explore.play-shuffled"), Wt::TextFormat::Plain)
->clicked()
.connect([this, mediumId] {
_playQueueController.processCommand(PlayQueueController::Command::PlayShuffled, mediumId);
});
disc->bindNew<Wt::WPushButton>("play-last", Wt::WString::tr("Lms.Explore.play-last"))
->clicked()
.connect([this, mediumId] {
_playQueueController.processCommand(PlayQueueController::Command::PlayOrAddLast, mediumId);
});
return disc;
}
} // namespace lms::ui } // namespace lms::ui
+6 -1
View File
@@ -27,8 +27,9 @@
namespace lms::db namespace lms::db
{ {
class Medium;
class Release; class Release;
} } // namespace lms::db
namespace lms::ui namespace lms::ui
{ {
@@ -44,11 +45,15 @@ namespace lms::ui
void refreshView(); void refreshView();
void refreshArtwork(db::ArtworkId artworkId); void refreshArtwork(db::ArtworkId artworkId);
void refreshReleaseArtists(const db::ObjectPtr<db::Release>& release); void refreshReleaseArtists(const db::ObjectPtr<db::Release>& release);
void refreshDiscs(const db::ObjectPtr<db::Release>& release);
void createTracks(Wt::WContainerWidget* tracksContainer, const db::ObjectPtr<db::Medium>& medium, bool displayTrackArtists);
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);
void refreshOtherVersions(const db::ObjectPtr<db::Release>& release); void refreshOtherVersions(const db::ObjectPtr<db::Release>& release);
void refreshSimilarReleases(const std::vector<db::ReleaseId>& similarReleaseIds); void refreshSimilarReleases(const std::vector<db::ReleaseId>& similarReleaseIds);
std::unique_ptr<Wt::WTemplate> createDisc(const db::ObjectPtr<db::Medium>& medium);
Filters& _filters; Filters& _filters;
PlayQueueController& _playQueueController; PlayQueueController& _playQueueController;
db::ReleaseId _releaseId; db::ReleaseId _releaseId;
+5 -3
View File
@@ -25,6 +25,7 @@
#include "core/ILogger.hpp" #include "core/ILogger.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/objects/Artist.hpp" #include "database/objects/Artist.hpp"
#include "database/objects/Medium.hpp"
#include "database/objects/Release.hpp" #include "database/objects/Release.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "database/objects/TrackList.hpp" #include "database/objects/TrackList.hpp"
@@ -116,12 +117,13 @@ namespace lms::ui
namespace details namespace details
{ {
std::string getTrackPathName(db::Track::pointer track) std::string getTrackPathName(const db::Track::pointer& track)
{ {
std::ostringstream fileName; std::ostringstream fileName;
if (auto discNumber{ track->getDiscNumber() }) const db::Medium::pointer medium{ track->getMedium() };
fileName << *discNumber << "."; if (medium && medium->getPosition())
fileName << *medium->getPosition() << ".";
if (auto trackNumber{ track->getTrackNumber() }) if (auto trackNumber{ track->getTrackNumber() })
fileName << std::setw(2) << std::setfill('0') << *trackNumber << " - "; fileName << std::setw(2) << std::setfill('0') << *trackNumber << " - ";
+4 -2
View File
@@ -37,6 +37,7 @@
#include "database/objects/Artist.hpp" #include "database/objects/Artist.hpp"
#include "database/objects/Cluster.hpp" #include "database/objects/Cluster.hpp"
#include "database/objects/MediaLibrary.hpp" #include "database/objects/MediaLibrary.hpp"
#include "database/objects/Medium.hpp"
#include "database/objects/Release.hpp" #include "database/objects/Release.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "database/objects/TrackArtistLink.hpp" #include "database/objects/TrackArtistLink.hpp"
@@ -90,6 +91,8 @@ namespace lms
const core::UUID releaseMBID{ core::UUID::generate() }; const core::UUID releaseMBID{ core::UUID::generate() };
const std::string releaseName{ "Release-" + std::string{ core::UUID::generate().getAsString() } }; const std::string releaseName{ "Release-" + std::string{ core::UUID::generate().getAsString() } };
Release::pointer release{ context.session.create<Release>(releaseName, releaseMBID) }; Release::pointer release{ context.session.create<Release>(releaseName, releaseMBID) };
Medium::pointer medium{ context.session.create<Medium>(release) };
medium.modify()->setTrackCount(params.trackCountPerRelease);
Artist::pointer artist{ generateArtist(context.session) }; Artist::pointer artist{ generateArtist(context.session) };
@@ -106,13 +109,12 @@ namespace lms
Track::pointer track{ context.session.create<Track>() }; Track::pointer track{ context.session.create<Track>() };
track.modify()->setName("Track-" + std::string{ core::UUID::generate().getAsString() }); track.modify()->setName("Track-" + std::string{ core::UUID::generate().getAsString() });
track.modify()->setDiscNumber(1); track.modify()->setMedium(medium);
track.modify()->setTrackNumber(i); track.modify()->setTrackNumber(i);
track.modify()->setDuration(std::chrono::seconds{ core::random::getRandom(30, 300) }); track.modify()->setDuration(std::chrono::seconds{ core::random::getRandom(30, 300) });
track.modify()->setRelease(release); track.modify()->setRelease(release);
track.modify()->setTrackMBID(core::UUID::generate()); track.modify()->setTrackMBID(core::UUID::generate());
track.modify()->setRecordingMBID(core::UUID::generate()); track.modify()->setRecordingMBID(core::UUID::generate());
track.modify()->setTotalTrack(params.trackCountPerRelease);
if (mediaLibrary) if (mediaLibrary)
track.modify()->setMediaLibrary(mediaLibrary); track.modify()->setMediaLibrary(mediaLibrary);