Added support for record labels, fixes #502

This commit is contained in:
emeric
2024-09-02 08:06:17 +02:00
parent 24e867ea8c
commit cb4014b0e5
17 changed files with 267 additions and 12 deletions
+24 -1
View File
@@ -35,7 +35,7 @@ namespace lms::db
{
namespace
{
static constexpr Version LMS_DATABASE_VERSION{ 64 };
static constexpr Version LMS_DATABASE_VERSION{ 65 };
}
VersionInfo::VersionInfo()
@@ -714,6 +714,28 @@ SELECT
session.getDboSession()->execute("DROP INDEX IF EXISTS listen_user_backend_date_time");
}
void migrateFromV64(Session& session)
{
session.getDboSession()->execute(R"(CREATE TABLE IF NOT EXISTS "label" (
"id" integer primary key autoincrement,
"version" integer not null,
"name" text not null
))");
session.getDboSession()->execute(R"(CREATE TABLE IF NOT EXISTS "release_label" (
"label_id" bigint,
"release_id" bigint,
primary key ("label_id", "release_id"),
constraint "fk_release_label_key1" foreign key ("label_id") references "label" ("id") on delete cascade deferrable initially deferred,
constraint "fk_release_label_key2" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred
))");
session.getDboSession()->execute(R"(CREATE INDEX "release_label_label" on "release_label" ("label_id"))");
session.getDboSession()->execute(R"(CREATE INDEX "release_label_release" on "release_label" ("release_id"))");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
}
bool doDbMigration(Session& session)
{
static const std::string outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
@@ -754,6 +776,7 @@ SELECT
{ 61, migrateFromV61 },
{ 62, migrateFromV62 },
{ 63, migrateFromV63 },
{ 64, migrateFromV64 },
};
bool migrationPerformed{};
+57
View File
@@ -216,6 +216,36 @@ namespace lms::db
}
} // namespace
Label::Label(std::string_view name)
: _name{ name }
{
// As we use the name to uniquely identoify release type, we must throw (and not truncate)
if (name.size() > _maxNameLength)
throw Exception{ "Label name is too long: " + std::string{ name } + "'" };
}
Label::pointer Label::create(Session& session, std::string_view name)
{
return session.getDboSession()->add(std::unique_ptr<Label>{ new Label{ name } });
}
Label::pointer Label::find(Session& session, LabelId id)
{
session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Label>>("SELECT l from label l").where("l.id = ?").bind(id));
}
Label::pointer Label::find(Session& session, std::string_view name)
{
session.checkReadTransaction();
if (name.size() > _maxNameLength)
throw Exception{ "Requeted Label name is too long: " + std::string{ name } + "'" };
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Label>>("SELECT l from label l").where("l.name = ?").bind(name));
}
ReleaseType::ReleaseType(std::string_view name)
: _name{ name }
{
@@ -504,11 +534,21 @@ namespace lms::db
return utils::fetchQueryResults<Release::pointer>(query);
}
void Release::clearLabels()
{
_labels.clear();
}
void Release::clearReleaseTypes()
{
_releaseTypes.clear();
}
void Release::addLabel(ObjectPtr<Label> label)
{
_labels.insert(getDboPtr(label));
}
void Release::addReleaseType(ObjectPtr<ReleaseType> releaseType)
{
_releaseTypes.insert(getDboPtr(releaseType));
@@ -537,6 +577,16 @@ namespace lms::db
return utils::fetchQueryResults<ReleaseType::pointer>(_releaseTypes.find());
}
std::vector<std::string> Release::getLabelNames() const
{
std::vector<std::string> res;
for (const auto& label : _labels)
res.push_back(std::string{ label->getName() });
return res;
}
std::vector<std::string> Release::getReleaseTypeNames() const
{
std::vector<std::string> res;
@@ -547,6 +597,13 @@ namespace lms::db
return res;
}
void Release::visitLabels(const std::function<void(const Label::pointer& label)>& _func) const
{
assert(session());
auto query{ _labels.find() };
utils::forEachQueryResult(query, _func);
}
std::chrono::milliseconds Release::getDuration() const
{
assert(session());
+3
View File
@@ -99,6 +99,7 @@ namespace lms::db
_session.mapClass<ClusterType>("cluster_type");
_session.mapClass<Directory>("directory");
_session.mapClass<Image>("image");
_session.mapClass<Label>("label");
_session.mapClass<Listen>("listen");
_session.mapClass<MediaLibrary>("media_library");
_session.mapClass<RatedArtist>("rated_artist");
@@ -197,6 +198,8 @@ namespace lms::db
_session.execute("CREATE INDEX IF NOT EXISTS image_path_idx ON image(absolute_file_path)");
_session.execute("CREATE INDEX IF NOT EXISTS image_stem_idx ON image(stem)");
_session.execute("CREATE INDEX IF NOT EXISTS label_name_idx ON label(name)");
_session.execute("CREATE INDEX IF NOT EXISTS listen_backend_idx ON listen(backend)");
_session.execute("CREATE INDEX IF NOT EXISTS listen_id_idx ON listen(id)");
_session.execute("CREATE INDEX IF NOT EXISTS listen_user_backend_idx ON listen(user_id,backend)");
@@ -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(LabelId)
+38 -2
View File
@@ -34,6 +34,7 @@
#include "database/ArtistId.hpp"
#include "database/ClusterId.hpp"
#include "database/DirectoryId.hpp"
#include "database/LabelId.hpp"
#include "database/MediaLibraryId.hpp"
#include "database/Object.hpp"
#include "database/ReleaseId.hpp"
@@ -51,6 +52,34 @@ namespace lms::db
class Track;
class User;
class Label final : public Object<Label, LabelId>
{
public:
Label() = default;
static pointer find(Session& session, LabelId id);
static pointer find(Session& session, std::string_view name);
// Accessors
std::string_view getName() const { return _name; }
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _name, "name");
Wt::Dbo::hasMany(a, _releases, Wt::Dbo::ManyToMany, "release_label", "", Wt::Dbo::OnDeleteCascade);
}
private:
static constexpr std::size_t _maxNameLength{ 512 };
friend class Session;
Label(std::string_view name);
static pointer create(Session& session, std::string_view name);
std::string _name;
Wt::Dbo::collection<Wt::Dbo::ptr<Release>> _releases; // releases that match this label
};
class ReleaseType final : public Object<ReleaseType, ReleaseTypeId>
{
public:
@@ -201,7 +230,9 @@ namespace lms::db
std::string_view getArtistDisplayName() const { return _artistDisplayName; }
std::size_t getTrackCount() const;
std::vector<ObjectPtr<ReleaseType>> getReleaseTypes() const;
std::vector<std::string> getLabelNames() const;
std::vector<std::string> getReleaseTypeNames() const;
void visitLabels(const std::function<void(const Label::pointer& label)>& _func) const;
// Setters
void setName(std::string_view name) { _name = name; }
@@ -210,7 +241,9 @@ namespace lms::db
void setGroupMBID(const std::optional<core::UUID>& mbid) { _groupMBID = mbid ? mbid->getAsString() : ""; }
void setTotalDisc(std::optional<int> totalDisc) { _totalDisc = totalDisc; }
void setArtistDisplayName(std::string_view name) { _artistDisplayName = name; }
void clearLabels();
void clearReleaseTypes();
void addLabel(ObjectPtr<Label> releaseType);
void addReleaseType(ObjectPtr<ReleaseType> releaseType);
// Get the artists of this release
@@ -230,6 +263,8 @@ namespace lms::db
Wt::Dbo::field(a, _totalDisc, "total_disc");
Wt::Dbo::field(a, _artistDisplayName, "artist_display_name");
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToOne, "release");
Wt::Dbo::hasMany(a, _labels, Wt::Dbo::ManyToMany, "release_label", "", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::hasMany(a, _releaseTypes, Wt::Dbo::ManyToMany, "release_release_type", "", Wt::Dbo::OnDeleteCascade);
}
@@ -250,8 +285,9 @@ namespace lms::db
std::optional<int> _totalDisc{};
std::string _artistDisplayName;
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> _tracks; // Tracks in the release
Wt::Dbo::collection<Wt::Dbo::ptr<ReleaseType>> _releaseTypes; // Release types
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> _tracks;
Wt::Dbo::collection<Wt::Dbo::ptr<Label>> _labels;
Wt::Dbo::collection<Wt::Dbo::ptr<ReleaseType>> _releaseTypes;
};
} // namespace lms::db
+2
View File
@@ -337,11 +337,13 @@ VALUES
EXPECT_FALSE(ClusterType::find(session, ClusterTypeId{}));
EXPECT_FALSE(Directory::find(session, DirectoryId{}));
EXPECT_FALSE(Image::find(session, ImageId{}));
EXPECT_FALSE(Label::find(session, LabelId{}));
EXPECT_FALSE(Listen::find(session, ListenId{}));
EXPECT_FALSE(RatedArtist::find(session, RatedArtistId{}));
EXPECT_FALSE(RatedRelease::find(session, RatedReleaseId{}));
EXPECT_FALSE(RatedTrack::find(session, RatedTrackId{}));
EXPECT_FALSE(Release::find(session, ReleaseId{}));
EXPECT_FALSE(ReleaseType::find(session, ReleaseTypeId{}));
EXPECT_FALSE(StarredArtist::find(session, StarredArtistId{}));
EXPECT_FALSE(StarredRelease::find(session, StarredReleaseId{}));
EXPECT_FALSE(StarredTrack::find(session, StarredTrackId{}));
+18
View File
@@ -21,6 +21,7 @@
namespace lms::db::tests
{
using ScopedLabel = ScopedEntity<db::Label>;
using ScopedReleaseType = ScopedEntity<db::ReleaseType>;
TEST_F(DatabaseFixture, Release)
@@ -744,6 +745,23 @@ namespace lms::db::tests
}
}
TEST_F(DatabaseFixture, Label)
{
{
auto transaction{ session.createReadTransaction() };
Label::pointer res{ Label::find(session, "label") };
EXPECT_EQ(res, Label::pointer{});
}
ScopedLabel label{ session, "MyLabel" };
{
auto transaction{ session.createReadTransaction() };
Label::pointer res{ Label::find(session, "MyLabel") };
EXPECT_EQ(res, label.get());
}
}
TEST_F(DatabaseFixture, ReleaseType)
{
{