Skip to content

Commit

Permalink
Merge branch 'develop' for release v3.56.0
Browse files Browse the repository at this point in the history
  • Loading branch information
epoupon committed Jul 28, 2024
2 parents 4cb6689 + cb7ee34 commit 0f5c665
Show file tree
Hide file tree
Showing 54 changed files with 1,265 additions and 428 deletions.
7 changes: 5 additions & 2 deletions SUBSONIC.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
# Subsonic API
The API version implemented is 1.16.0 and has been tested on _Android_ using _Subsonic Player_, _Ultrasonic_, _Symfonium_, and _DSub_.
Since _LMS_ uses metadata tags to organize music, a compatibility mode is used to browse the collection when using the directory browsing commands.

Folder navigation commands are supported. However, since _LMS_ does not store information for each folder, it is not possible to star/unstar folders considered as artists.
Given the API limitations of folder navigation commands, it is recommended to place all tracks of an album in the same folder and not to mix multiple albums in the same folder.

The Subsonic API is enabled by default.

__Note__: since _LMS_ may store hashed and salted passwords or may forward authentication requests to external services, it cannot handle the __token authentication__ method. You may need to check your client to make sure to use the __password__ authentication method.
__Note__: since _LMS_ may store hashed and salted passwords or may forward authentication requests to external services, it cannot handle the __token authentication__ method. You may need to check your client to make sure to use the __password__ authentication method. Since logins/passwords are passed in plain text through URLs, it is highly recommended to use a unique password when using the Subsonic API. Note that this may affect the use of authentication via PAM. In any case, ensure that read access to the web server logs (and to the proxy, if applicable) is well protected.

# OpenSubsonic API
OpenSubsonic is an initiative to patch and extend the legacy Subsonic API. You'll find more details in the [official documentation](https://opensubsonic.netlify.app/)
Expand Down
1 change: 1 addition & 0 deletions approot/messages.xml
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@
<message id="Lms.Admin.ScannerController.step-removing-orphaned-entries">Removing orphaned entries: {1} entries...</message>
<message id="Lms.Admin.ScannerController.step-scanning-files">Scanning files: {1}/{2} ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-status">Step status</message>
<message id="Lms.Admin.ScannerController.step-updating-library-fields">Updating library fields: {1} entries</message>

<!--Tracing-->
<message id="Lms.Admin.Tracing.export-current-buffer">Export traces</message>
Expand Down
1 change: 1 addition & 0 deletions approot/messages_fr.xml
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@
<message id="Lms.Admin.ScannerController.step-removing-orphaned-entries">Retrait des entrées orphelines: {1} entrées...</message>
<message id="Lms.Admin.ScannerController.step-scanning-files">Scan des fichiers : {1}/{2} ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-status">Statut de l'étape</message>
<message id="Lms.Admin.ScannerController.step-updating-library-fields">Mise à jour des champs des bibliothèques: {1} entrées</message>

<!--Tracing-->
<message id="Lms.Admin.Tracing.export-current-buffer">Exporter les traces</message>
Expand Down
1 change: 1 addition & 0 deletions approot/messages_it.xml
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@
<message id="Lms.Admin.ScannerController.step-removing-orphaned-entries">Rimozione voci orfane: {1} voci...</message>
<message id="Lms.Admin.ScannerController.step-scanning-files">Scansione dei file: {1}/{2} ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-status">Stato passo</message>
<message id="Lms.Admin.ScannerController.step-updating-library-fields">Aggiornamento dei campi della libreria: {1} voci</message>

<!--Tracing-->
<message id="Lms.Admin.Tracing.export-current-buffer">Esporta tracce</message>
Expand Down
1 change: 1 addition & 0 deletions approot/messages_pl.xml
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@
<message id="Lms.Admin.ScannerController.step-removing-orphaned-entries">Usuwanie osieroconych wpisów: {1} wpisów...</message>
<message id="Lms.Admin.ScannerController.step-scanning-files">Skanowanie plików: {1}/{2} ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-status">Obecny krok</message>
<message id="Lms.Admin.ScannerController.step-updating-library-fields">Aktualizowanie pól biblioteki: {1} wpisów</message>

<!--Tracing-->
<message id="Lms.Admin.Tracing.export-current-buffer">Eksportuj ślady</message>
Expand Down
1 change: 1 addition & 0 deletions approot/messages_zh.xml
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@
<message id="Lms.Admin.ScannerController.step-scanning-files">扫描文件中: {1}/{2} 个文件 ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-status">当前步骤状态</message>


<!--Tracing-->


Expand Down
20 changes: 15 additions & 5 deletions src/libs/database/impl/Artist.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -183,10 +183,10 @@ namespace lms::db
} // namespace

Artist::Artist(const std::string& name, const std::optional<core::UUID>& MBID)
: _name{ std::string(name, 0, _maxNameLength) }
, _sortName{ _name }
, _MBID{ MBID ? MBID->getAsString() : "" }
: _MBID{ MBID ? MBID->getAsString() : "" }
{
setName(name);
_sortName = _name;
}

Artist::pointer Artist::create(Session& session, const std::string& name, const std::optional<core::UUID>& MBID)
Expand Down Expand Up @@ -360,9 +360,19 @@ namespace lms::db
return res;
}

void Artist::setSortName(const std::string& sortName)
void Artist::setName(std::string_view name)
{
_sortName = std::string(sortName, 0, _maxNameLength);
_name.assign(name, 0, _maxNameLength);
if (name.size() > _maxNameLength)
LMS_LOG(DB, WARNING, "Artist name too long, truncated to '" << _name << "'");
}

void Artist::setSortName(std::string_view sortName)
{
_sortName.assign(sortName, 0, _maxNameLength);

if (sortName.size() > _maxNameLength)
LMS_LOG(DB, WARNING, "Artist sort name too long, truncated to '" << _sortName << "'");
}

void Artist::setImage(ObjectPtr<Image> image)
Expand Down
14 changes: 13 additions & 1 deletion src/libs/database/impl/Cluster.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,12 @@ namespace lms::db
} // namespace

Cluster::Cluster(ObjectPtr<ClusterType> type, std::string_view name)
: _name{ std::string{ name, 0, _maxNameLength } }
: _name{ name }
, _clusterType{ getDboPtr(type) }
{
// As we use the name to uniquely identify clusters and cluster types, we must throw (and not truncate)
if (name.size() > maxNameLength)
throw Exception{ "Cluster name is too long: " + std::string{ name } + "'" };
}

Cluster::pointer Cluster::create(Session& session, ObjectPtr<ClusterType> type, std::string_view name)
Expand Down Expand Up @@ -183,6 +186,9 @@ namespace lms::db
ClusterType::ClusterType(std::string_view name)
: _name{ name }
{
// As we use the name to uniquely identify clusters and cluster types, we must throw
if (name.size() > maxNameLength)
throw Exception{ "ClusterType name is too long: " + std::string{ name } + "'" };
}

ClusterType::pointer ClusterType::create(Session& session, std::string_view name)
Expand Down Expand Up @@ -230,6 +236,9 @@ namespace lms::db
{
session.checkReadTransaction();

if (name.size() > maxNameLength)
throw Exception{ "Requested ClusterType name is too long: " + std::string{ name } + "'" };

return utils::fetchQuerySingleResult(session.getDboSession()->find<ClusterType>().where("name = ?").bind(name));
}

Expand All @@ -254,6 +263,9 @@ namespace lms::db
assert(self());
assert(session());

if (name.size() > Cluster::maxNameLength)
throw Exception{ "Requested Cluster name is too long: " + std::string{ name } + "'" };

return utils::fetchQuerySingleResult(session()->find<Cluster>().where("name = ?").bind(name).where("cluster_type_id = ?").bind(getId()));
}

Expand Down
72 changes: 67 additions & 5 deletions src/libs/database/impl/Directory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

#include "database/Directory.hpp"

#include "database/MediaLibrary.hpp"
#include "database/Session.hpp"

#include "IdTypeTraits.hpp"
Expand All @@ -33,10 +34,28 @@ namespace lms::db
{
auto query{ session.getDboSession()->query<Wt::Dbo::ptr<Directory>>("SELECT d FROM directory d") };

for (std::string_view keyword : params.keywords)
query.where("d.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + utils::escapeLikeKeyword(keyword) + "%");

if (params.artist.isValid()
|| params.release.isValid())
{
query.join("track t ON t.directory_id = d.id");
query.groupBy("d.id");
}

if (params.mediaLibrary.isValid())
query.where("d.media_library_id = ?").bind(params.mediaLibrary);

if (params.parentDirectory.isValid())
query.where("d.parent_directory_id = ?").bind(params.parentDirectory);

if (params.release.isValid())
query.where("t.release_id = ?").bind(params.release);

if (params.artist.isValid())
{
query.join("track t ON t.directory_id = d.id")
.join("artist a ON a.id = t_a_l.artist_id")
query.join("artist a ON a.id = t_a_l.artist_id")
.join("track_artist_link t_a_l ON t_a_l.track_id = t.id")
.where("a.id = ?")
.bind(params.artist);
Expand All @@ -57,12 +76,32 @@ namespace lms::db
}
query.where(oss.str());
}

query.groupBy("d.id");
}

if (params.withNoTrack)
query.where("NOT EXISTS (SELECT 1 FROM track t WHERE t.directory_id = d.id)");

return query;
}

std::filesystem::path getPathWithTrailingSeparator(const std::filesystem::path& path)
{
if (path.empty())
return path;

// Convert the path to string
std::string pathStr{ path.string() };

// Check if the last character is a directory separator
if (pathStr.back() != std::filesystem::path::preferred_separator)
{
// If not, add the preferred separator
pathStr += std::filesystem::path::preferred_separator;
}

// Return the new path
return std::filesystem::path{ pathStr };
}
} // namespace

Directory::Directory(const std::filesystem::path& p)
Expand Down Expand Up @@ -108,10 +147,16 @@ namespace lms::db
});
}

RangeResults<Directory::pointer> Directory::find(Session& session, const FindParameters& params)
{
auto query{ createQuery(session, params) };
return utils::execRangeQuery<Directory::pointer>(query, params.range);
}

void Directory::find(Session& session, const FindParameters& params, const std::function<void(const Directory::pointer&)>& func)
{
auto query{ createQuery(session, params) };
utils::forEachQueryResult(query, [&func](const Directory::pointer& dir) {
utils::forEachQueryRangeResult(query, params.range, [&func](const Directory::pointer& dir) {
func(dir);
});
}
Expand All @@ -131,6 +176,23 @@ namespace lms::db
return utils::execRangeQuery<DirectoryId>(query, range);
}

RangeResults<DirectoryId> Directory::findMismatchedLibrary(Session& session, std::optional<Range> range, const std::filesystem::path& rootPath, MediaLibraryId expectedLibraryId)
{
session.checkReadTransaction();

auto query{ session.getDboSession()->query<DirectoryId>("SELECT d.id FROM directory d") };
query.where("d.absolute_path = ? OR d.absolute_path LIKE ?").bind(rootPath).bind(getPathWithTrailingSeparator(rootPath).string() + "%");
query.where("d.media_library_id <> ? OR d.media_library_id IS NULL").bind(expectedLibraryId);

return utils::execRangeQuery<DirectoryId>(query, range);
}

RangeResults<Directory::pointer> Directory::findRootDirectories(Session& session, std::optional<Range> range)
{
auto query{ session.getDboSession()->query<Wt::Dbo::ptr<Directory>>("SELECT d from directory d").where("d.parent_directory_id IS NULL") };
return utils::execRangeQuery<Directory::pointer>(query, range);
}

void Directory::setAbsolutePath(const std::filesystem::path& p)
{
assert(p.is_absolute());
Expand Down
36 changes: 35 additions & 1 deletion src/libs/database/impl/Migration.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ namespace lms::db
{
namespace
{
static constexpr Version LMS_DATABASE_VERSION{ 61 };
static constexpr Version LMS_DATABASE_VERSION{ 62 };
}

VersionInfo::VersionInfo()
Expand Down Expand Up @@ -631,6 +631,39 @@ SELECT

} // namespace

void migrateFromV61(Session& session)
{
// Added a media_library_id in Directory
session.getDboSession()->execute(R"(
CREATE TABLE IF NOT EXISTS "directory_backup" (
"id" integer primary key autoincrement,
"version" integer not null,
"absolute_path" text not null,
"name" text not null,
"parent_directory_id" bigint,
"media_library_id" bigint,
constraint "fk_directory_parent_directory" foreign key ("parent_directory_id") references "directory" ("id") on delete cascade deferrable initially deferred,
constraint "fk_directory_media_library" foreign key ("media_library_id") references "media_library" ("id") on delete set null deferrable initially deferred
))");

// Migrate data, with the new directory_id field set to null
session.getDboSession()->execute(R"(INSERT INTO directory_backup
SELECT
id,
version,
absolute_path,
name,
parent_directory_id,
NULL
FROM directory)");

session.getDboSession()->execute("DROP TABLE directory");
session.getDboSession()->execute("ALTER TABLE directory_backup RENAME TO directory");

// 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)" };
Expand Down Expand Up @@ -668,6 +701,7 @@ SELECT
{ 58, migrateFromV58 },
{ 59, migrateFromV59 },
{ 60, migrateFromV60 },
{ 61, migrateFromV61 },
};

bool migrationPerformed{};
Expand Down
19 changes: 17 additions & 2 deletions src/libs/database/impl/Release.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,18 @@ namespace lms::db
|| params.dateRange
|| params.artist.isValid()
|| params.clusters.size() == 1
|| params.mediaLibrary.isValid())
|| params.mediaLibrary.isValid()
|| params.directory.isValid())
{
query.join("track t ON t.release_id = r.id");
}

if (params.mediaLibrary.isValid())
query.where("t.media_library_id = ?").bind(params.mediaLibrary);

if (params.directory.isValid())
query.where("t.directory_id = ?").bind(params.directory);

if (!params.releaseType.empty())
{
query.join("release_release_type r_r_t ON r_r_t.release_id = r.id");
Expand Down Expand Up @@ -213,8 +217,11 @@ namespace lms::db
} // namespace

ReleaseType::ReleaseType(std::string_view name)
: _name{ std::string(name, 0, _maxNameLength) }
: _name{ name }
{
// As we use the name to uniquely identoify release type, we must throw (and not truncate)
if (name.size() > _maxNameLength)
throw Exception{ "ReleaseType name is too long: " + std::string{ name } + "'" };
}

ReleaseType::pointer ReleaseType::create(Session& session, std::string_view name)
Expand All @@ -233,6 +240,9 @@ namespace lms::db
{
session.checkReadTransaction();

if (name.size() > _maxNameLength)
throw Exception{ "Requeted ReleaseType name is too long: " + std::string{ name } + "'" };

return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<ReleaseType>>("SELECT r_t from release_type r_t").where("r_t.name = ?").bind(name));
}

Expand Down Expand Up @@ -510,6 +520,11 @@ namespace lms::db
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
{
assert(session());
Expand Down
2 changes: 2 additions & 0 deletions src/libs/database/impl/Session.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ namespace lms::db

_session.execute("CREATE INDEX IF NOT EXISTS directory_id_idx ON directory(id)");
_session.execute("CREATE INDEX IF NOT EXISTS directory_path_idx ON directory(absolute_path)");
_session.execute("CREATE INDEX IF NOT EXISTS directory_media_library_idx ON directory(media_library_id)");

_session.execute("CREATE INDEX IF NOT EXISTS image_artist_idx ON image(artist_id)");
_session.execute("CREATE INDEX IF NOT EXISTS image_directory_idx ON image(directory_id)");
Expand All @@ -205,6 +206,7 @@ namespace lms::db

_session.execute("CREATE INDEX IF NOT EXISTS track_id_idx ON track(id)");
_session.execute("CREATE INDEX IF NOT EXISTS track_absolute_path_idx ON track(absolute_file_path)");
_session.execute("CREATE INDEX IF NOT EXISTS track_directory_release_idx ON track(directory_id, release_id);");
_session.execute("CREATE INDEX IF NOT EXISTS track_name_idx ON track(name)");
_session.execute("CREATE INDEX IF NOT EXISTS track_name_nocase_idx ON track(name COLLATE NOCASE)");
_session.execute("CREATE INDEX IF NOT EXISTS track_mbid_idx ON track(mbid)");
Expand Down
Loading

0 comments on commit 0f5c665

Please sign in to comment.