Merge branch 'develop' for release v3.56.0
This commit is contained in:
+5
-2
@@ -1,9 +1,12 @@
|
|||||||
# Subsonic API
|
# Subsonic API
|
||||||
The API version implemented is 1.16.0 and has been tested on _Android_ using _Subsonic Player_, _Ultrasonic_, _Symfonium_, and _DSub_.
|
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.
|
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 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/)
|
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/)
|
||||||
|
|||||||
@@ -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-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-scanning-files">Scanning files: {1}/{2} ({3}%)...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-status">Step status</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-->
|
<!--Tracing-->
|
||||||
<message id="Lms.Admin.Tracing.export-current-buffer">Export traces</message>
|
<message id="Lms.Admin.Tracing.export-current-buffer">Export traces</message>
|
||||||
|
|||||||
@@ -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-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-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-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-->
|
<!--Tracing-->
|
||||||
<message id="Lms.Admin.Tracing.export-current-buffer">Exporter les traces</message>
|
<message id="Lms.Admin.Tracing.export-current-buffer">Exporter les traces</message>
|
||||||
|
|||||||
@@ -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-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-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-status">Stato passo</message>
|
||||||
|
<message id="Lms.Admin.ScannerController.step-updating-library-fields">Aggiornamento dei campi della libreria: {1} voci</message>
|
||||||
|
|
||||||
<!--Tracing-->
|
<!--Tracing-->
|
||||||
<message id="Lms.Admin.Tracing.export-current-buffer">Esporta tracce</message>
|
<message id="Lms.Admin.Tracing.export-current-buffer">Esporta tracce</message>
|
||||||
|
|||||||
@@ -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-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-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-status">Obecny krok</message>
|
||||||
|
<message id="Lms.Admin.ScannerController.step-updating-library-fields">Aktualizowanie pól biblioteki: {1} wpisów</message>
|
||||||
|
|
||||||
<!--Tracing-->
|
<!--Tracing-->
|
||||||
<message id="Lms.Admin.Tracing.export-current-buffer">Eksportuj ślady</message>
|
<message id="Lms.Admin.Tracing.export-current-buffer">Eksportuj ślady</message>
|
||||||
|
|||||||
@@ -122,6 +122,7 @@
|
|||||||
<message id="Lms.Admin.ScannerController.step-scanning-files">扫描文件中: {1}/{2} 个文件 ({3}%)...</message>
|
<message id="Lms.Admin.ScannerController.step-scanning-files">扫描文件中: {1}/{2} 个文件 ({3}%)...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-status">当前步骤状态</message>
|
<message id="Lms.Admin.ScannerController.step-status">当前步骤状态</message>
|
||||||
|
|
||||||
|
|
||||||
<!--Tracing-->
|
<!--Tracing-->
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -183,10 +183,10 @@ namespace lms::db
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Artist::Artist(const std::string& name, const std::optional<core::UUID>& MBID)
|
Artist::Artist(const std::string& name, const std::optional<core::UUID>& MBID)
|
||||||
: _name{ std::string(name, 0, _maxNameLength) }
|
: _MBID{ MBID ? MBID->getAsString() : "" }
|
||||||
, _sortName{ _name }
|
|
||||||
, _MBID{ MBID ? MBID->getAsString() : "" }
|
|
||||||
{
|
{
|
||||||
|
setName(name);
|
||||||
|
_sortName = _name;
|
||||||
}
|
}
|
||||||
|
|
||||||
Artist::pointer Artist::create(Session& session, const std::string& name, const std::optional<core::UUID>& MBID)
|
Artist::pointer Artist::create(Session& session, const std::string& name, const std::optional<core::UUID>& MBID)
|
||||||
@@ -360,9 +360,19 @@ namespace lms::db
|
|||||||
return res;
|
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)
|
void Artist::setImage(ObjectPtr<Image> image)
|
||||||
|
|||||||
@@ -96,9 +96,12 @@ namespace lms::db
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Cluster::Cluster(ObjectPtr<ClusterType> type, std::string_view name)
|
Cluster::Cluster(ObjectPtr<ClusterType> type, std::string_view name)
|
||||||
: _name{ std::string{ name, 0, _maxNameLength } }
|
: _name{ name }
|
||||||
, _clusterType{ getDboPtr(type) }
|
, _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)
|
Cluster::pointer Cluster::create(Session& session, ObjectPtr<ClusterType> type, std::string_view name)
|
||||||
@@ -183,6 +186,9 @@ namespace lms::db
|
|||||||
ClusterType::ClusterType(std::string_view name)
|
ClusterType::ClusterType(std::string_view name)
|
||||||
: _name{ 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)
|
ClusterType::pointer ClusterType::create(Session& session, std::string_view name)
|
||||||
@@ -230,6 +236,9 @@ namespace lms::db
|
|||||||
{
|
{
|
||||||
session.checkReadTransaction();
|
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));
|
return utils::fetchQuerySingleResult(session.getDboSession()->find<ClusterType>().where("name = ?").bind(name));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,6 +263,9 @@ namespace lms::db
|
|||||||
assert(self());
|
assert(self());
|
||||||
assert(session());
|
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()));
|
return utils::fetchQuerySingleResult(session()->find<Cluster>().where("name = ?").bind(name).where("cluster_type_id = ?").bind(getId()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
#include "database/Directory.hpp"
|
#include "database/Directory.hpp"
|
||||||
|
|
||||||
|
#include "database/MediaLibrary.hpp"
|
||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
|
|
||||||
#include "IdTypeTraits.hpp"
|
#include "IdTypeTraits.hpp"
|
||||||
@@ -33,10 +34,28 @@ namespace lms::db
|
|||||||
{
|
{
|
||||||
auto query{ session.getDboSession()->query<Wt::Dbo::ptr<Directory>>("SELECT d FROM directory d") };
|
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())
|
if (params.artist.isValid())
|
||||||
{
|
{
|
||||||
query.join("track t ON t.directory_id = d.id")
|
query.join("artist a ON a.id = t_a_l.artist_id")
|
||||||
.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")
|
.join("track_artist_link t_a_l ON t_a_l.track_id = t.id")
|
||||||
.where("a.id = ?")
|
.where("a.id = ?")
|
||||||
.bind(params.artist);
|
.bind(params.artist);
|
||||||
@@ -57,12 +76,32 @@ namespace lms::db
|
|||||||
}
|
}
|
||||||
query.where(oss.str());
|
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;
|
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
|
} // namespace
|
||||||
|
|
||||||
Directory::Directory(const std::filesystem::path& p)
|
Directory::Directory(const std::filesystem::path& p)
|
||||||
@@ -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)
|
void Directory::find(Session& session, const FindParameters& params, const std::function<void(const Directory::pointer&)>& func)
|
||||||
{
|
{
|
||||||
auto query{ createQuery(session, params) };
|
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);
|
func(dir);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -131,6 +176,23 @@ namespace lms::db
|
|||||||
return utils::execRangeQuery<DirectoryId>(query, range);
|
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)
|
void Directory::setAbsolutePath(const std::filesystem::path& p)
|
||||||
{
|
{
|
||||||
assert(p.is_absolute());
|
assert(p.is_absolute());
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ namespace lms::db
|
|||||||
{
|
{
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
static constexpr Version LMS_DATABASE_VERSION{ 61 };
|
static constexpr Version LMS_DATABASE_VERSION{ 62 };
|
||||||
}
|
}
|
||||||
|
|
||||||
VersionInfo::VersionInfo()
|
VersionInfo::VersionInfo()
|
||||||
@@ -631,6 +631,39 @@ SELECT
|
|||||||
|
|
||||||
} // namespace
|
} // 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)
|
bool doDbMigration(Session& session)
|
||||||
{
|
{
|
||||||
static const std::string outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
|
static const std::string outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
|
||||||
@@ -668,6 +701,7 @@ SELECT
|
|||||||
{ 58, migrateFromV58 },
|
{ 58, migrateFromV58 },
|
||||||
{ 59, migrateFromV59 },
|
{ 59, migrateFromV59 },
|
||||||
{ 60, migrateFromV60 },
|
{ 60, migrateFromV60 },
|
||||||
|
{ 61, migrateFromV61 },
|
||||||
};
|
};
|
||||||
|
|
||||||
bool migrationPerformed{};
|
bool migrationPerformed{};
|
||||||
|
|||||||
@@ -53,7 +53,8 @@ namespace lms::db
|
|||||||
|| params.dateRange
|
|| params.dateRange
|
||||||
|| params.artist.isValid()
|
|| params.artist.isValid()
|
||||||
|| params.clusters.size() == 1
|
|| params.clusters.size() == 1
|
||||||
|| params.mediaLibrary.isValid())
|
|| params.mediaLibrary.isValid()
|
||||||
|
|| params.directory.isValid())
|
||||||
{
|
{
|
||||||
query.join("track t ON t.release_id = r.id");
|
query.join("track t ON t.release_id = r.id");
|
||||||
}
|
}
|
||||||
@@ -61,6 +62,9 @@ namespace lms::db
|
|||||||
if (params.mediaLibrary.isValid())
|
if (params.mediaLibrary.isValid())
|
||||||
query.where("t.media_library_id = ?").bind(params.mediaLibrary);
|
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())
|
if (!params.releaseType.empty())
|
||||||
{
|
{
|
||||||
query.join("release_release_type r_r_t ON r_r_t.release_id = r.id");
|
query.join("release_release_type r_r_t ON r_r_t.release_id = r.id");
|
||||||
@@ -213,8 +217,11 @@ namespace lms::db
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
ReleaseType::ReleaseType(std::string_view name)
|
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)
|
ReleaseType::pointer ReleaseType::create(Session& session, std::string_view name)
|
||||||
@@ -233,6 +240,9 @@ namespace lms::db
|
|||||||
{
|
{
|
||||||
session.checkReadTransaction();
|
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));
|
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<ReleaseType>>("SELECT r_t from release_type r_t").where("r_t.name = ?").bind(name));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -510,6 +520,11 @@ 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());
|
||||||
|
|||||||
@@ -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_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_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_artist_idx ON image(artist_id)");
|
||||||
_session.execute("CREATE INDEX IF NOT EXISTS image_directory_idx ON image(directory_id)");
|
_session.execute("CREATE INDEX IF NOT EXISTS image_directory_idx ON image(directory_id)");
|
||||||
@@ -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_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_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_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_name_nocase_idx ON track(name COLLATE NOCASE)");
|
||||||
_session.execute("CREATE INDEX IF NOT EXISTS track_mbid_idx ON track(mbid)");
|
_session.execute("CREATE INDEX IF NOT EXISTS track_mbid_idx ON track(mbid)");
|
||||||
|
|||||||
@@ -154,6 +154,9 @@ namespace lms::db
|
|||||||
if (params.mediaLibrary.isValid())
|
if (params.mediaLibrary.isValid())
|
||||||
query.where("t.media_library_id = ?").bind(params.mediaLibrary);
|
query.where("t.media_library_id = ?").bind(params.mediaLibrary);
|
||||||
|
|
||||||
|
if (params.directory.isValid())
|
||||||
|
query.where("t.directory_id = ?").bind(params.directory);
|
||||||
|
|
||||||
switch (params.sortMethod)
|
switch (params.sortMethod)
|
||||||
{
|
{
|
||||||
case TrackSortMethod::None:
|
case TrackSortMethod::None:
|
||||||
@@ -374,6 +377,27 @@ namespace lms::db
|
|||||||
_relativeFilePath = filePath;
|
_relativeFilePath = filePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Track::setName(std::string_view name)
|
||||||
|
{
|
||||||
|
_name = std::string{ name, 0, _maxNameLength };
|
||||||
|
if (name.size() > _maxNameLength)
|
||||||
|
LMS_LOG(DB, WARNING, "Track name too long, truncated to '" << _name << "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
void Track::setCopyright(std::string_view copyright)
|
||||||
|
{
|
||||||
|
_copyright = std::string{ copyright, 0, _maxCopyrightLength };
|
||||||
|
if (copyright.size() > _maxCopyrightLength)
|
||||||
|
LMS_LOG(DB, WARNING, "Track copyright too long, truncated to '" << _copyright << "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
void Track::setCopyrightURL(std::string_view copyrightURL)
|
||||||
|
{
|
||||||
|
_copyrightURL = std::string{ copyrightURL, 0, _maxCopyrightURLLength };
|
||||||
|
if (copyrightURL.size() > _maxCopyrightURLLength)
|
||||||
|
LMS_LOG(DB, WARNING, "Track copyright URL too long, truncated to '" << _copyrightURL << "'");
|
||||||
|
}
|
||||||
|
|
||||||
void Track::clearArtistLinks()
|
void Track::clearArtistLinks()
|
||||||
{
|
{
|
||||||
_trackArtistLinks.clear();
|
_trackArtistLinks.clear();
|
||||||
|
|||||||
@@ -150,9 +150,9 @@ namespace lms::db
|
|||||||
// size is the max number of cluster per cluster type
|
// size is the max number of cluster per cluster type
|
||||||
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(std::vector<ClusterTypeId> clusterTypeIds, std::size_t size) const;
|
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(std::vector<ClusterTypeId> clusterTypeIds, std::size_t size) const;
|
||||||
|
|
||||||
void setName(std::string_view name) { _name = name; }
|
void setName(std::string_view name);
|
||||||
void setMBID(const std::optional<core::UUID>& mbid) { _MBID = mbid ? mbid->getAsString() : ""; }
|
void setMBID(const std::optional<core::UUID>& mbid) { _MBID = mbid ? mbid->getAsString() : ""; }
|
||||||
void setSortName(const std::string& sortName);
|
void setSortName(std::string_view sortName);
|
||||||
void setImage(ObjectPtr<Image> image);
|
void setImage(ObjectPtr<Image> image);
|
||||||
|
|
||||||
template<class Action>
|
template<class Action>
|
||||||
@@ -168,7 +168,7 @@ namespace lms::db
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static constexpr std::size_t _maxNameLength{ 256 };
|
static constexpr std::size_t _maxNameLength{ 512 };
|
||||||
|
|
||||||
friend class Session;
|
friend class Session;
|
||||||
// Create
|
// Create
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ namespace lms::db
|
|||||||
class Cluster final : public Object<Cluster, ClusterId>
|
class Cluster final : public Object<Cluster, ClusterId>
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
static constexpr std::size_t maxNameLength{ 512 };
|
||||||
|
|
||||||
struct FindParameters
|
struct FindParameters
|
||||||
{
|
{
|
||||||
std::optional<Range> range;
|
std::optional<Range> range;
|
||||||
@@ -126,8 +128,6 @@ namespace lms::db
|
|||||||
Cluster(ObjectPtr<ClusterType> type, std::string_view name);
|
Cluster(ObjectPtr<ClusterType> type, std::string_view name);
|
||||||
static pointer create(Session& session, ObjectPtr<ClusterType> type, std::string_view name);
|
static pointer create(Session& session, ObjectPtr<ClusterType> type, std::string_view name);
|
||||||
|
|
||||||
static const std::size_t _maxNameLength = 128;
|
|
||||||
|
|
||||||
std::string _name;
|
std::string _name;
|
||||||
int _trackCount{};
|
int _trackCount{};
|
||||||
int _releaseCount{};
|
int _releaseCount{};
|
||||||
@@ -141,6 +141,8 @@ namespace lms::db
|
|||||||
public:
|
public:
|
||||||
ClusterType() = default;
|
ClusterType() = default;
|
||||||
|
|
||||||
|
static constexpr std::size_t maxNameLength{ 512 };
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
static std::size_t getCount(Session& session);
|
static std::size_t getCount(Session& session);
|
||||||
static RangeResults<ClusterTypeId> findIds(Session& session, std::optional<Range> range = std::nullopt);
|
static RangeResults<ClusterTypeId> findIds(Session& session, std::optional<Range> range = std::nullopt);
|
||||||
@@ -169,8 +171,6 @@ namespace lms::db
|
|||||||
ClusterType(std::string_view name);
|
ClusterType(std::string_view name);
|
||||||
static pointer create(Session& session, std::string_view name);
|
static pointer create(Session& session, std::string_view name);
|
||||||
|
|
||||||
static const std::size_t _maxNameLength = 128;
|
|
||||||
|
|
||||||
std::string _name;
|
std::string _name;
|
||||||
Wt::Dbo::collection<Wt::Dbo::ptr<Cluster>> _clusters;
|
Wt::Dbo::collection<Wt::Dbo::ptr<Cluster>> _clusters;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,18 +21,24 @@
|
|||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include <Wt/Dbo/Dbo.h>
|
#include <Wt/Dbo/Dbo.h>
|
||||||
|
|
||||||
#include "core/EnumSet.hpp"
|
#include "core/EnumSet.hpp"
|
||||||
#include "database/ArtistId.hpp"
|
#include "database/ArtistId.hpp"
|
||||||
#include "database/DirectoryId.hpp"
|
#include "database/DirectoryId.hpp"
|
||||||
|
#include "database/MediaLibraryId.hpp"
|
||||||
#include "database/Object.hpp"
|
#include "database/Object.hpp"
|
||||||
|
#include "database/ReleaseId.hpp"
|
||||||
#include "database/Types.hpp"
|
#include "database/Types.hpp"
|
||||||
|
|
||||||
namespace lms::db
|
namespace lms::db
|
||||||
{
|
{
|
||||||
class Session;
|
class Session;
|
||||||
|
class MediaLibrary;
|
||||||
|
|
||||||
class Directory final : public Object<Directory, DirectoryId>
|
class Directory final : public Object<Directory, DirectoryId>
|
||||||
{
|
{
|
||||||
@@ -42,20 +48,50 @@ namespace lms::db
|
|||||||
struct FindParameters
|
struct FindParameters
|
||||||
{
|
{
|
||||||
std::optional<Range> range;
|
std::optional<Range> range;
|
||||||
ArtistId artist; // only tracks that involve this artist
|
std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords
|
||||||
|
ArtistId artist; // only directory that involve this artist
|
||||||
|
ReleaseId release; // only releases that involve this artist
|
||||||
core::EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types
|
core::EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types
|
||||||
|
DirectoryId parentDirectory; // If set, directories that have this parent
|
||||||
|
bool withNoTrack{}; // If set, directories that do not contain any track
|
||||||
|
MediaLibraryId mediaLibrary; // If set, directories in this library
|
||||||
|
|
||||||
FindParameters& setRange(std::optional<Range> _range)
|
FindParameters& setRange(std::optional<Range> _range)
|
||||||
{
|
{
|
||||||
range = _range;
|
range = _range;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
FindParameters& setKeywords(const std::vector<std::string_view>& _keywords)
|
||||||
|
{
|
||||||
|
keywords = _keywords;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
FindParameters& setArtist(ArtistId _artist, core::EnumSet<TrackArtistLinkType> _trackArtistLinkTypes = {})
|
FindParameters& setArtist(ArtistId _artist, core::EnumSet<TrackArtistLinkType> _trackArtistLinkTypes = {})
|
||||||
{
|
{
|
||||||
artist = _artist;
|
artist = _artist;
|
||||||
trackArtistLinkTypes = _trackArtistLinkTypes;
|
trackArtistLinkTypes = _trackArtistLinkTypes;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
FindParameters& setRelease(ReleaseId _release)
|
||||||
|
{
|
||||||
|
release = _release;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
FindParameters& setParentDirectory(DirectoryId _parentDirectory)
|
||||||
|
{
|
||||||
|
parentDirectory = _parentDirectory;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
FindParameters& setWithNoTrack(bool _withNoTrack)
|
||||||
|
{
|
||||||
|
withNoTrack = _withNoTrack;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
FindParameters& setMediaLibrary(MediaLibraryId _mediaLibrary)
|
||||||
|
{
|
||||||
|
mediaLibrary = _mediaLibrary;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// find
|
// find
|
||||||
@@ -63,17 +99,22 @@ namespace lms::db
|
|||||||
static pointer find(Session& session, DirectoryId id);
|
static pointer find(Session& session, DirectoryId id);
|
||||||
static pointer find(Session& session, const std::filesystem::path& path);
|
static pointer find(Session& session, const std::filesystem::path& path);
|
||||||
static void find(Session& session, DirectoryId& lastRetrievedDirectory, std::size_t count, const std::function<void(const Directory::pointer&)>& func);
|
static void find(Session& session, DirectoryId& lastRetrievedDirectory, std::size_t count, const std::function<void(const Directory::pointer&)>& func);
|
||||||
|
static RangeResults<Directory::pointer> find(Session& session, const FindParameters& params);
|
||||||
static void find(Session& session, const FindParameters& parameters, const std::function<void(const Directory::pointer&)>& func);
|
static void find(Session& session, const FindParameters& parameters, const std::function<void(const Directory::pointer&)>& func);
|
||||||
static RangeResults<DirectoryId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt);
|
static RangeResults<DirectoryId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt);
|
||||||
|
static RangeResults<DirectoryId> findMismatchedLibrary(Session& session, std::optional<Range> range, const std::filesystem::path& rootPath, MediaLibraryId expectedLibraryId);
|
||||||
|
static RangeResults<pointer> findRootDirectories(Session& session, std::optional<Range> range = std::nullopt);
|
||||||
|
|
||||||
// getters
|
// getters
|
||||||
const std::filesystem::path& getAbsolutePath() const { return _absolutePath; }
|
const std::filesystem::path& getAbsolutePath() const { return _absolutePath; }
|
||||||
std::string_view getName() const { return _name; }
|
std::string_view getName() const { return _name; }
|
||||||
ObjectPtr<Directory> getParent() const { return _parent; }
|
ObjectPtr<Directory> getParentDirectory() const { return _parent; }
|
||||||
|
ObjectPtr<MediaLibrary> getMediaLibrary() const { return _mediaLibrary; }
|
||||||
|
|
||||||
// setters
|
// setters
|
||||||
void setAbsolutePath(const std::filesystem::path& p);
|
void setAbsolutePath(const std::filesystem::path& p);
|
||||||
void setParent(ObjectPtr<Directory> parent);
|
void setParent(ObjectPtr<Directory> parent);
|
||||||
|
void setMediaLibrary(ObjectPtr<MediaLibrary> mediaLibrary) { _mediaLibrary = getDboPtr(mediaLibrary); }
|
||||||
|
|
||||||
template<class Action>
|
template<class Action>
|
||||||
void persist(Action& a)
|
void persist(Action& a)
|
||||||
@@ -82,6 +123,7 @@ namespace lms::db
|
|||||||
Wt::Dbo::field(a, _name, "name");
|
Wt::Dbo::field(a, _name, "name");
|
||||||
|
|
||||||
Wt::Dbo::belongsTo(a, _parent, "parent_directory", Wt::Dbo::OnDeleteCascade);
|
Wt::Dbo::belongsTo(a, _parent, "parent_directory", Wt::Dbo::OnDeleteCascade);
|
||||||
|
Wt::Dbo::belongsTo(a, _mediaLibrary, "media_library", Wt::Dbo::OnDeleteSetNull); // don't delete directories on media library removal, we want to wait for the next scan to have a chance to migrate files
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -93,5 +135,6 @@ namespace lms::db
|
|||||||
std::string _name;
|
std::string _name;
|
||||||
|
|
||||||
Wt::Dbo::ptr<Directory> _parent;
|
Wt::Dbo::ptr<Directory> _parent;
|
||||||
|
Wt::Dbo::ptr<MediaLibrary> _mediaLibrary;
|
||||||
};
|
};
|
||||||
} // namespace lms::db
|
} // namespace lms::db
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
#include "core/UUID.hpp"
|
#include "core/UUID.hpp"
|
||||||
#include "database/ArtistId.hpp"
|
#include "database/ArtistId.hpp"
|
||||||
#include "database/ClusterId.hpp"
|
#include "database/ClusterId.hpp"
|
||||||
|
#include "database/DirectoryId.hpp"
|
||||||
#include "database/MediaLibraryId.hpp"
|
#include "database/MediaLibraryId.hpp"
|
||||||
#include "database/Object.hpp"
|
#include "database/Object.hpp"
|
||||||
#include "database/ReleaseId.hpp"
|
#include "database/ReleaseId.hpp"
|
||||||
@@ -68,7 +69,7 @@ namespace lms::db
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static constexpr std::size_t _maxNameLength{ 128 };
|
static constexpr std::size_t _maxNameLength{ 512 };
|
||||||
|
|
||||||
friend class Session;
|
friend class Session;
|
||||||
ReleaseType(std::string_view name);
|
ReleaseType(std::string_view name);
|
||||||
@@ -96,6 +97,7 @@ namespace lms::db
|
|||||||
core::EnumSet<TrackArtistLinkType> excludedTrackArtistLinkTypes; // but not for these link types
|
core::EnumSet<TrackArtistLinkType> excludedTrackArtistLinkTypes; // but not for these link types
|
||||||
std::string releaseType; // If set, albums that has this release type
|
std::string releaseType; // If set, albums that has this release type
|
||||||
MediaLibraryId mediaLibrary; // If set, releases that has at least a track in this library
|
MediaLibraryId mediaLibrary; // If set, releases that has at least a track in this library
|
||||||
|
DirectoryId directory; // if set, tracks in this directory
|
||||||
|
|
||||||
FindParameters& setClusters(std::span<const ClusterId> _clusters)
|
FindParameters& setClusters(std::span<const ClusterId> _clusters)
|
||||||
{
|
{
|
||||||
@@ -150,6 +152,11 @@ namespace lms::db
|
|||||||
mediaLibrary = _mediaLibrary;
|
mediaLibrary = _mediaLibrary;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
FindParameters& setDirectory(DirectoryId _directory)
|
||||||
|
{
|
||||||
|
directory = _directory;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Release() = default;
|
Release() = default;
|
||||||
@@ -211,6 +218,7 @@ 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)
|
||||||
@@ -233,7 +241,7 @@ namespace lms::db
|
|||||||
Wt::WDate getDate(bool original) const;
|
Wt::WDate getDate(bool original) const;
|
||||||
std::optional<int> getYear(bool original) const;
|
std::optional<int> getYear(bool original) const;
|
||||||
|
|
||||||
static constexpr std::size_t _maxNameLength{ 256 };
|
static constexpr std::size_t _maxNameLength{ 512 };
|
||||||
|
|
||||||
std::string _name;
|
std::string _name;
|
||||||
std::string _sortName;
|
std::string _sortName;
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
#include "core/UUID.hpp"
|
#include "core/UUID.hpp"
|
||||||
#include "database/ArtistId.hpp"
|
#include "database/ArtistId.hpp"
|
||||||
#include "database/ClusterId.hpp"
|
#include "database/ClusterId.hpp"
|
||||||
|
#include "database/DirectoryId.hpp"
|
||||||
#include "database/MediaLibraryId.hpp"
|
#include "database/MediaLibraryId.hpp"
|
||||||
#include "database/Object.hpp"
|
#include "database/Object.hpp"
|
||||||
#include "database/ReleaseId.hpp"
|
#include "database/ReleaseId.hpp"
|
||||||
@@ -81,6 +82,7 @@ namespace lms::db
|
|||||||
std::optional<int> trackNumber; // matching this track number
|
std::optional<int> trackNumber; // matching this track number
|
||||||
std::optional<int> discNumber; // matching this disc number
|
std::optional<int> discNumber; // matching this disc number
|
||||||
MediaLibraryId mediaLibrary; // If set, tracks in this library
|
MediaLibraryId mediaLibrary; // If set, tracks in this library
|
||||||
|
DirectoryId directory; // if set, tracks in this directory
|
||||||
|
|
||||||
FindParameters& setClusters(std::span<const ClusterId> _clusters)
|
FindParameters& setClusters(std::span<const ClusterId> _clusters)
|
||||||
{
|
{
|
||||||
@@ -165,6 +167,11 @@ namespace lms::db
|
|||||||
mediaLibrary = _mediaLibrary;
|
mediaLibrary = _mediaLibrary;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
FindParameters& setDirectory(DirectoryId _directory)
|
||||||
|
{
|
||||||
|
directory = _directory;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
struct PathResult
|
struct PathResult
|
||||||
@@ -197,8 +204,8 @@ namespace lms::db
|
|||||||
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 setDiscNumber(std::optional<int> num) { _discNumber = num; }
|
||||||
void setTotalTrack(std::optional<int> totalTrack) { _totalTrack = totalTrack; }
|
void setTotalTrack(std::optional<int> totalTrack) { _totalTrack = totalTrack; }
|
||||||
void setDiscSubtitle(const std::string& name) { _discSubtitle = name; }
|
void setDiscSubtitle(std::string_view name) { _discSubtitle = name; }
|
||||||
void setName(const std::string& name) { _name = std::string(name, 0, _maxNameLength); }
|
void setName(std::string_view name);
|
||||||
void setAbsoluteFilePath(const std::filesystem::path& filePath);
|
void setAbsoluteFilePath(const std::filesystem::path& filePath);
|
||||||
void setRelativeFilePath(const std::filesystem::path& filePath);
|
void setRelativeFilePath(const std::filesystem::path& filePath);
|
||||||
void setFileSize(std::size_t fileSize) { _fileSize = fileSize; }
|
void setFileSize(std::size_t fileSize) { _fileSize = fileSize; }
|
||||||
@@ -216,8 +223,8 @@ namespace lms::db
|
|||||||
void setHasCover(bool hasCover) { _hasCover = hasCover; }
|
void setHasCover(bool hasCover) { _hasCover = hasCover; }
|
||||||
void setTrackMBID(const std::optional<core::UUID>& MBID) { _trackMBID = MBID ? MBID->getAsString() : ""; }
|
void setTrackMBID(const std::optional<core::UUID>& MBID) { _trackMBID = MBID ? MBID->getAsString() : ""; }
|
||||||
void setRecordingMBID(const std::optional<core::UUID>& MBID) { _recordingMBID = MBID ? MBID->getAsString() : ""; }
|
void setRecordingMBID(const std::optional<core::UUID>& MBID) { _recordingMBID = MBID ? MBID->getAsString() : ""; }
|
||||||
void setCopyright(const std::string& copyright) { _copyright = std::string(copyright, 0, _maxCopyrightLength); }
|
void setCopyright(std::string_view copyright);
|
||||||
void setCopyrightURL(const std::string& copyrightURL) { _copyrightURL = std::string(copyrightURL, 0, _maxCopyrightURLLength); }
|
void setCopyrightURL(std::string_view copyrightURL);
|
||||||
void setTrackReplayGain(std::optional<float> replayGain) { _trackReplayGain = replayGain; }
|
void setTrackReplayGain(std::optional<float> replayGain) { _trackReplayGain = replayGain; }
|
||||||
void setReleaseReplayGain(std::optional<float> replayGain) { _releaseReplayGain = replayGain; } // may be by disc!
|
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; }
|
||||||
@@ -311,9 +318,9 @@ namespace lms::db
|
|||||||
friend class Session;
|
friend class Session;
|
||||||
static pointer create(Session& session);
|
static pointer create(Session& session);
|
||||||
|
|
||||||
static constexpr std::size_t _maxNameLength{ 256 };
|
static constexpr std::size_t _maxNameLength{ 512 };
|
||||||
static constexpr std::size_t _maxCopyrightLength{ 256 };
|
static constexpr std::size_t _maxCopyrightLength{ 512 };
|
||||||
static constexpr std::size_t _maxCopyrightURLLength{ 256 };
|
static constexpr std::size_t _maxCopyrightURLLength{ 512 };
|
||||||
|
|
||||||
int _scanVersion{};
|
int _scanVersion{};
|
||||||
std::optional<int> _trackNumber{};
|
std::optional<int> _trackNumber{};
|
||||||
|
|||||||
@@ -25,8 +25,16 @@
|
|||||||
|
|
||||||
#include <Wt/WDate.h>
|
#include <Wt/WDate.h>
|
||||||
|
|
||||||
|
#include "core/Exception.hpp"
|
||||||
|
|
||||||
namespace lms::db
|
namespace lms::db
|
||||||
{
|
{
|
||||||
|
class Exception : public core::LmsException
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using LmsException::LmsException;
|
||||||
|
};
|
||||||
|
|
||||||
// Caution: do not change enum values if they are set!
|
// Caution: do not change enum values if they are set!
|
||||||
|
|
||||||
// Request:
|
// Request:
|
||||||
|
|||||||
@@ -68,8 +68,8 @@ namespace lms::db
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
static inline constexpr std::size_t MinNameLength{ 3 };
|
static inline constexpr std::size_t minNameLength{ 3 };
|
||||||
static inline constexpr std::size_t MaxNameLength{ 15 };
|
static inline constexpr std::size_t maxNameLength{ 32 };
|
||||||
static inline constexpr bool defaultSubsonicEnableTranscodingByDefault{ false };
|
static inline constexpr bool defaultSubsonicEnableTranscodingByDefault{ false };
|
||||||
static inline constexpr TranscodingOutputFormat defaultSubsonicTranscodingOutputFormat{ TranscodingOutputFormat::OGG_OPUS };
|
static inline constexpr TranscodingOutputFormat defaultSubsonicTranscodingOutputFormat{ TranscodingOutputFormat::OGG_OPUS };
|
||||||
static inline constexpr Bitrate defaultSubsonicTranscodingOutputBitrate{ 128000 };
|
static inline constexpr Bitrate defaultSubsonicTranscodingOutputBitrate{ 128000 };
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ namespace lms::db::tests
|
|||||||
ScopedClusterType clusterType{ session, "MyType" };
|
ScopedClusterType clusterType{ session, "MyType" };
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction{ session.createWriteTransaction() };
|
auto transaction{ session.createReadTransaction() };
|
||||||
EXPECT_EQ(ClusterType::getCount(session), 1);
|
EXPECT_EQ(ClusterType::getCount(session), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ namespace lms::db::tests
|
|||||||
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
|
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction{ session.createWriteTransaction() };
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
EXPECT_EQ(Cluster::getCount(session), 1);
|
EXPECT_EQ(Cluster::getCount(session), 1);
|
||||||
EXPECT_EQ(cluster->getType()->getId(), clusterType.getId());
|
EXPECT_EQ(cluster->getType()->getId(), clusterType.getId());
|
||||||
@@ -74,7 +74,7 @@ namespace lms::db::tests
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction{ session.createWriteTransaction() };
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
auto clusterTypes{ ClusterType::findOrphanIds(session) };
|
auto clusterTypes{ ClusterType::findOrphanIds(session) };
|
||||||
ASSERT_EQ(clusterTypes.results.size(), 1);
|
ASSERT_EQ(clusterTypes.results.size(), 1);
|
||||||
@@ -84,6 +84,70 @@ namespace lms::db::tests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Cluster_find)
|
||||||
|
{
|
||||||
|
ScopedClusterType clusterType{ session, "MyType" };
|
||||||
|
ScopedCluster cluster1{ session, clusterType.lockAndGet(), "MyCluster" };
|
||||||
|
ScopedCluster cluster2{ session, clusterType.lockAndGet(), "Mycluster" };
|
||||||
|
ScopedCluster cluster3{ session, clusterType.lockAndGet(), "MyOtherCluster" };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
|
EXPECT_EQ(clusterType->getCluster("MyCluster"), cluster1.get());
|
||||||
|
EXPECT_EQ(clusterType->getCluster("Mycluster"), cluster2.get());
|
||||||
|
|
||||||
|
EXPECT_EQ(clusterType->getCluster(" Mycluster"), Cluster::pointer{});
|
||||||
|
EXPECT_EQ(clusterType->getCluster("Mycluster "), Cluster::pointer{});
|
||||||
|
EXPECT_EQ(clusterType->getCluster("mycluster"), Cluster::pointer{});
|
||||||
|
EXPECT_EQ(clusterType->getCluster("My"), Cluster::pointer{});
|
||||||
|
EXPECT_EQ(clusterType->getCluster("Cluster"), Cluster::pointer{});
|
||||||
|
EXPECT_EQ(clusterType->getCluster("MyCluster1"), Cluster::pointer{});
|
||||||
|
EXPECT_EQ(clusterType->getCluster("MyCluster2"), Cluster::pointer{});
|
||||||
|
EXPECT_EQ(clusterType->getCluster(""), Cluster::pointer{});
|
||||||
|
EXPECT_EQ(clusterType->getCluster(" "), Cluster::pointer{});
|
||||||
|
EXPECT_EQ(clusterType->getCluster("*"), Cluster::pointer{});
|
||||||
|
EXPECT_EQ(clusterType->getCluster(R"(%)"), Cluster::pointer{});
|
||||||
|
EXPECT_EQ(clusterType->getCluster(R"(%%)"), Cluster::pointer{});
|
||||||
|
EXPECT_EQ(clusterType->getCluster(R"(")"), Cluster::pointer{});
|
||||||
|
EXPECT_EQ(clusterType->getCluster(R"("")"), Cluster::pointer{});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Cluster_create)
|
||||||
|
{
|
||||||
|
ScopedClusterType clusterType{ session, "MyType" };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
|
||||||
|
auto createdCluster{ session.create<Cluster>(clusterType.get(), "Foo") };
|
||||||
|
auto foundCluster{ clusterType->getCluster("Foo") };
|
||||||
|
EXPECT_EQ(createdCluster, foundCluster);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
|
||||||
|
auto createdCluster{ session.create<Cluster>(clusterType.get(), "") };
|
||||||
|
auto foundCluster{ clusterType->getCluster("") };
|
||||||
|
EXPECT_EQ(createdCluster, foundCluster);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Cluster_create_long)
|
||||||
|
{
|
||||||
|
ScopedClusterType clusterType{ session, "MyType" };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
|
||||||
|
auto createdCluster{ session.create<Cluster>(clusterType.get(), "Alternative Rock; Art Pop; Art Rock; Britpop; Chamber Pop; Electronic Rock; Electronica; Experimental Rock; Neo-Progressive Rock; Foo") };
|
||||||
|
auto foundCluster{ clusterType->getCluster("Alternative Rock; Art Pop; Art Rock; Britpop; Chamber Pop; Electronic Rock; Electronica; Experimental Rock; Neo-Progressive Rock; Foo") };
|
||||||
|
EXPECT_EQ(createdCluster, foundCluster);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, Cluster_singleTrack)
|
TEST_F(DatabaseFixture, Cluster_singleTrack)
|
||||||
{
|
{
|
||||||
ScopedTrack track{ session };
|
ScopedTrack track{ session };
|
||||||
|
|||||||
@@ -107,23 +107,35 @@ namespace lms::db::tests
|
|||||||
{
|
{
|
||||||
auto transaction{ session.createReadTransaction() };
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
auto dir{ child->getParent() };
|
auto dir{ child->getParentDirectory() };
|
||||||
EXPECT_EQ(dir, Directory::pointer{});
|
EXPECT_EQ(dir, Directory::pointer{});
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction{ session.createWriteTransaction() };
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
|
||||||
child.get().modify()->setParent(parent.lockAndGet());
|
child.get().modify()->setParent(parent.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
auto transaction{ session.createReadTransaction() };
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
auto dir{ child->getParent() };
|
auto dir{ child->getParentDirectory() };
|
||||||
ASSERT_NE(dir, Directory::pointer{});
|
ASSERT_NE(dir, Directory::pointer{});
|
||||||
EXPECT_EQ(dir->getId(), parent.getId());
|
EXPECT_EQ(dir->getId(), parent.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
|
Directory::pointer foundDir;
|
||||||
|
Directory::find(session, Directory::FindParameters{}.setParentDirectory(parent->getId()), [&](const Directory::pointer& dir) {
|
||||||
|
ASSERT_EQ(foundDir, Directory::pointer{});
|
||||||
|
foundDir = dir;
|
||||||
|
});
|
||||||
|
ASSERT_NE(foundDir, Directory::pointer{});
|
||||||
|
EXPECT_EQ(foundDir->getId(), child.getId());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, Directory_orphaned)
|
TEST_F(DatabaseFixture, Directory_orphaned)
|
||||||
@@ -141,7 +153,7 @@ namespace lms::db::tests
|
|||||||
{
|
{
|
||||||
auto transaction{ session.createWriteTransaction() };
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
|
||||||
child.get().modify()->setParent(parent.lockAndGet());
|
child.get().modify()->setParent(parent.get());
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -152,4 +164,125 @@ namespace lms::db::tests
|
|||||||
EXPECT_EQ(directories.front(), child.getId());
|
EXPECT_EQ(directories.front(), child.getId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Directory_findRootDirectories)
|
||||||
|
{
|
||||||
|
ScopedDirectory parent1{ session, "/root1" };
|
||||||
|
ScopedDirectory child{ session, "/root1/child" };
|
||||||
|
ScopedDirectory parent2{ session, "/root2" };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
|
||||||
|
child.get().modify()->setParent(parent1.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
|
const auto directories{ Directory::findRootDirectories(session).results };
|
||||||
|
ASSERT_EQ(directories.size(), 2);
|
||||||
|
EXPECT_EQ(directories[0]->getId(), parent1.getId());
|
||||||
|
EXPECT_EQ(directories[1]->getId(), parent2.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Directory_findNonTrackDirectories)
|
||||||
|
{
|
||||||
|
ScopedDirectory parent{ session, "/root" };
|
||||||
|
ScopedDirectory child1{ session, "/root/child1" };
|
||||||
|
ScopedDirectory child2{ session, "/root/child2" };
|
||||||
|
ScopedTrack track{ session };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
|
||||||
|
child1.get().modify()->setParent(parent.get());
|
||||||
|
child2.get().modify()->setParent(parent.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
|
Directory::FindParameters params;
|
||||||
|
params.setWithNoTrack(true);
|
||||||
|
|
||||||
|
auto res{ Directory::find(session, params).results };
|
||||||
|
|
||||||
|
ASSERT_EQ(res.size(), 3);
|
||||||
|
EXPECT_EQ(res[0]->getId(), parent.getId());
|
||||||
|
EXPECT_EQ(res[1]->getId(), child1.getId());
|
||||||
|
EXPECT_EQ(res[2]->getId(), child2.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
track.get().modify()->setDirectory(child2.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
|
Directory::FindParameters params;
|
||||||
|
params.setWithNoTrack(true);
|
||||||
|
|
||||||
|
auto res{ Directory::find(session, params).results };
|
||||||
|
ASSERT_EQ(res.size(), 2);
|
||||||
|
EXPECT_EQ(res[0]->getId(), parent.getId());
|
||||||
|
EXPECT_EQ(res[1]->getId(), child1.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Directory_findWithKeywords)
|
||||||
|
{
|
||||||
|
ScopedDirectory parent{ session, "/root" };
|
||||||
|
ScopedDirectory child1{ session, "/root/foo" };
|
||||||
|
ScopedDirectory child2{ session, "/root/bar/foo" };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
|
Directory::FindParameters params;
|
||||||
|
params.setKeywords({ "foo" });
|
||||||
|
|
||||||
|
auto res{ Directory::find(session, params).results };
|
||||||
|
|
||||||
|
ASSERT_EQ(res.size(), 2);
|
||||||
|
EXPECT_EQ(res[0]->getId(), child1.getId());
|
||||||
|
EXPECT_EQ(res[1]->getId(), child2.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DatabaseFixture, Directory_findMismatchedLibrary)
|
||||||
|
{
|
||||||
|
ScopedDirectory parent1{ session, "/root" };
|
||||||
|
ScopedDirectory child1{ session, "/root/foo" };
|
||||||
|
ScopedDirectory parent2{ session, "/root_1" };
|
||||||
|
ScopedDirectory child2{ session, "/root_1/foo" };
|
||||||
|
|
||||||
|
ScopedMediaLibrary library{ session, "/root" };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
|
const auto res{ Directory::findMismatchedLibrary(session, std::nullopt, library->getPath(), library->getId()).results };
|
||||||
|
ASSERT_EQ(res.size(), 2);
|
||||||
|
EXPECT_EQ(res[0], parent1.getId());
|
||||||
|
EXPECT_EQ(res[1], child1.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
|
||||||
|
parent1.get().modify()->setMediaLibrary(library.get());
|
||||||
|
child1.get().modify()->setMediaLibrary(library.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
|
const auto res{ Directory::findMismatchedLibrary(session, std::nullopt, library->getPath(), library->getId()).results };
|
||||||
|
EXPECT_EQ(res.size(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
} // namespace lms::db::tests
|
} // namespace lms::db::tests
|
||||||
@@ -12,6 +12,7 @@ add_library(lmsscanner SHARED
|
|||||||
impl/ScanStepOptimize.cpp
|
impl/ScanStepOptimize.cpp
|
||||||
impl/ScanStepRemoveOrphanedDbEntries.cpp
|
impl/ScanStepRemoveOrphanedDbEntries.cpp
|
||||||
impl/ScanStepScanFiles.cpp
|
impl/ScanStepScanFiles.cpp
|
||||||
|
impl/ScanStepUpdateLibraryFields.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(lmsscanner INTERFACE
|
target_include_directories(lmsscanner INTERFACE
|
||||||
|
|||||||
@@ -205,6 +205,9 @@ namespace lms::scanner
|
|||||||
|
|
||||||
void ScanStepAssociateArtistImages::process(ScanContext& context)
|
void ScanStepAssociateArtistImages::process(ScanContext& context)
|
||||||
{
|
{
|
||||||
|
if (_abortScan)
|
||||||
|
return;
|
||||||
|
|
||||||
if (context.stats.nbChanges() == 0)
|
if (context.stats.nbChanges() == 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -224,6 +227,9 @@ namespace lms::scanner
|
|||||||
ArtistImageAssociationContainer artistImageAssociations;
|
ArtistImageAssociationContainer artistImageAssociations;
|
||||||
while (fetchNextArtistImagesToUpdate(searchContext, artistImageAssociations))
|
while (fetchNextArtistImagesToUpdate(searchContext, artistImageAssociations))
|
||||||
{
|
{
|
||||||
|
if (_abortScan)
|
||||||
|
return;
|
||||||
|
|
||||||
updateArtistImages(session, artistImageAssociations);
|
updateArtistImages(session, artistImageAssociations);
|
||||||
context.currentStepStats.processedElems += readBatchSize;
|
context.currentStepStats.processedElems += readBatchSize;
|
||||||
_progressCallback(context.currentStepStats);
|
_progressCallback(context.currentStepStats);
|
||||||
|
|||||||
@@ -104,18 +104,20 @@ namespace lms::scanner
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
Directory::pointer getOrCreateDirectory(Session& session, const std::filesystem::path& path, const std::filesystem::path& rootPath)
|
Directory::pointer getOrCreateDirectory(Session& session, const std::filesystem::path& path, const MediaLibrary::pointer& mediaLibrary)
|
||||||
{
|
{
|
||||||
Directory::pointer directory{ Directory::find(session, path) };
|
Directory::pointer directory{ Directory::find(session, path) };
|
||||||
if (!directory)
|
if (!directory)
|
||||||
{
|
{
|
||||||
Directory::pointer parentDirectory;
|
Directory::pointer parentDirectory;
|
||||||
if (path != rootPath)
|
if (path != mediaLibrary->getPath())
|
||||||
parentDirectory = getOrCreateDirectory(session, path.parent_path(), rootPath);
|
parentDirectory = getOrCreateDirectory(session, path.parent_path(), mediaLibrary);
|
||||||
|
|
||||||
directory = session.create<Directory>(path);
|
directory = session.create<Directory>(path);
|
||||||
directory.modify()->setParent(parentDirectory);
|
directory.modify()->setParent(parentDirectory);
|
||||||
|
directory.modify()->setMediaLibrary(mediaLibrary);
|
||||||
}
|
}
|
||||||
|
// Don't update library if it does not match, will be updated elsewhere
|
||||||
|
|
||||||
return directory;
|
return directory;
|
||||||
}
|
}
|
||||||
@@ -350,12 +352,13 @@ namespace lms::scanner
|
|||||||
|
|
||||||
std::vector<FileScanResult> scanResults;
|
std::vector<FileScanResult> scanResults;
|
||||||
|
|
||||||
|
std::filesystem::path currentDirectory;
|
||||||
core::pathUtils::exploreFilesRecursive(
|
core::pathUtils::exploreFilesRecursive(
|
||||||
mediaLibrary.rootDirectory, [&](std::error_code ec, const std::filesystem::path& path) {
|
mediaLibrary.rootDirectory, [&](std::error_code ec, const std::filesystem::path& path) {
|
||||||
LMS_SCOPED_TRACE_DETAILED("Scanner", "OnExploreFile");
|
LMS_SCOPED_TRACE_DETAILED("Scanner", "OnExploreFile");
|
||||||
|
|
||||||
if (_abortScan)
|
if (_abortScan)
|
||||||
return false;
|
return false; // stop iterating
|
||||||
|
|
||||||
if (ec)
|
if (ec)
|
||||||
{
|
{
|
||||||
@@ -364,21 +367,21 @@ namespace lms::scanner
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
bool fileToProcess{};
|
bool fileMatched{};
|
||||||
if (core::pathUtils::hasFileAnyExtension(path, _settings.supportedAudioFileExtensions))
|
if (core::pathUtils::hasFileAnyExtension(path, _settings.supportedAudioFileExtensions))
|
||||||
{
|
{
|
||||||
fileToProcess = true;
|
fileMatched = true;
|
||||||
if (checkAudioFileNeedScan(context, path, mediaLibrary))
|
if (checkAudioFileNeedScan(context, path, mediaLibrary))
|
||||||
_fileScanQueue.pushScanRequest(path, FileScanQueue::ScanRequestType::AudioFile);
|
_fileScanQueue.pushScanRequest(path, FileScanQueue::ScanRequestType::AudioFile);
|
||||||
}
|
}
|
||||||
else if (core::pathUtils::hasFileAnyExtension(path, _settings.supportedImageFileExtensions))
|
else if (core::pathUtils::hasFileAnyExtension(path, _settings.supportedImageFileExtensions))
|
||||||
{
|
{
|
||||||
fileToProcess = true;
|
fileMatched = true;
|
||||||
if (checkImageFileNeedScan(context, path))
|
if (checkImageFileNeedScan(context, path))
|
||||||
_fileScanQueue.pushScanRequest(path, FileScanQueue::ScanRequestType::ImageFile);
|
_fileScanQueue.pushScanRequest(path, FileScanQueue::ScanRequestType::ImageFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fileToProcess)
|
if (fileMatched)
|
||||||
{
|
{
|
||||||
context.currentStepStats.processedElems++;
|
context.currentStepStats.processedElems++;
|
||||||
_progressCallback(context.currentStepStats);
|
_progressCallback(context.currentStepStats);
|
||||||
@@ -415,17 +418,19 @@ namespace lms::scanner
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (context.scanOptions.fullScan)
|
||||||
|
return true;
|
||||||
|
|
||||||
bool needUpdateLibrary{};
|
bool needUpdateLibrary{};
|
||||||
if (!context.scanOptions.fullScan)
|
db::Session& dbSession{ _db.getTLSSession() };
|
||||||
|
|
||||||
{
|
{
|
||||||
|
auto transaction{ dbSession.createReadTransaction() };
|
||||||
|
|
||||||
// Skip file if last write is the same
|
// Skip file if last write is the same
|
||||||
db::Session& dbSession{ _db.getTLSSession() };
|
|
||||||
auto transaction{ _db.getTLSSession().createReadTransaction() };
|
|
||||||
|
|
||||||
const Track::pointer track{ Track::findByPath(dbSession, file) };
|
const Track::pointer track{ Track::findByPath(dbSession, file) };
|
||||||
|
|
||||||
if (track
|
if (track
|
||||||
&& track->getLastWriteTime().toTime_t() == lastWriteTime.toTime_t()
|
&& track->getLastWriteTime() == lastWriteTime
|
||||||
&& track->getScanVersion() == _settings.scanVersion)
|
&& track->getScanVersion() == _settings.scanVersion)
|
||||||
{
|
{
|
||||||
// this file may have been moved from one library to another, then we just need to update the media library id instead of a full rescan
|
// this file may have been moved from one library to another, then we just need to update the media library id instead of a full rescan
|
||||||
@@ -442,8 +447,7 @@ namespace lms::scanner
|
|||||||
|
|
||||||
if (needUpdateLibrary)
|
if (needUpdateLibrary)
|
||||||
{
|
{
|
||||||
db::Session& dbSession{ _db.getTLSSession() };
|
auto transaction{ dbSession.createWriteTransaction() };
|
||||||
auto transaction{ _db.getTLSSession().createWriteTransaction() };
|
|
||||||
|
|
||||||
Track::pointer track{ Track::findByPath(dbSession, file) };
|
Track::pointer track{ Track::findByPath(dbSession, file) };
|
||||||
assert(track);
|
assert(track);
|
||||||
@@ -497,32 +501,18 @@ namespace lms::scanner
|
|||||||
|
|
||||||
if (const AudioFileScanData * scanData{ std::get_if<AudioFileScanData>(&scanResult.scanData) })
|
if (const AudioFileScanData * scanData{ std::get_if<AudioFileScanData>(&scanResult.scanData) })
|
||||||
{
|
{
|
||||||
if (metadata::Track * track{ scanData->get() })
|
context.stats.scans++;
|
||||||
{
|
processAudioFileScanData(context, scanResult.path, scanData->get(), libraryInfo);
|
||||||
context.stats.scans++;
|
|
||||||
processAudioFileScanData(context, scanResult.path, *track, libraryInfo);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
context.stats.errors.emplace_back(scanResult.path, ScanErrorType::CannotReadAudioFile);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if (const ImageFileScanData * scanData{ std::get_if<ImageFileScanData>(&scanResult.scanData) })
|
else if (const ImageFileScanData * scanData{ std::get_if<ImageFileScanData>(&scanResult.scanData) })
|
||||||
{
|
{
|
||||||
if (scanData->has_value())
|
context.stats.scans++;
|
||||||
{
|
processImageFileScanData(context, scanResult.path, scanData->has_value() ? &scanData->value() : nullptr, libraryInfo);
|
||||||
context.stats.scans++;
|
|
||||||
processImageFileScanData(context, scanResult.path, scanData->value(), libraryInfo);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
context.stats.errors.emplace_back(scanResult.path, ScanErrorType::CannotReadImageFile);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ScanStepScanFiles::processAudioFileScanData(ScanContext& context, const std::filesystem::path& file, const metadata::Track& trackMetadata, const ScannerSettings::MediaLibraryInfo& libraryInfo)
|
void ScanStepScanFiles::processAudioFileScanData(ScanContext& context, const std::filesystem::path& file, const metadata::Track* trackMetadata, const ScannerSettings::MediaLibraryInfo& libraryInfo)
|
||||||
{
|
{
|
||||||
LMS_SCOPED_TRACE_DETAILED("Scanner", "ProcessAudioScanData");
|
LMS_SCOPED_TRACE_DETAILED("Scanner", "ProcessAudioScanData");
|
||||||
|
|
||||||
@@ -538,9 +528,20 @@ namespace lms::scanner
|
|||||||
db::Session& dbSession{ _db.getTLSSession() };
|
db::Session& dbSession{ _db.getTLSSession() };
|
||||||
Track::pointer track{ Track::findByPath(dbSession, file) };
|
Track::pointer track{ Track::findByPath(dbSession, file) };
|
||||||
|
|
||||||
if (trackMetadata.mbid && (!track || _settings.skipDuplicateMBID))
|
if (!trackMetadata)
|
||||||
{
|
{
|
||||||
std::vector<Track::pointer> duplicateTracks{ Track::findByMBID(dbSession, *trackMetadata.mbid) };
|
if (track)
|
||||||
|
{
|
||||||
|
track.remove();
|
||||||
|
stats.deletions++;
|
||||||
|
}
|
||||||
|
context.stats.errors.emplace_back(file, ScanErrorType::CannotReadAudioFile);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trackMetadata->mbid && (!track || _settings.skipDuplicateMBID))
|
||||||
|
{
|
||||||
|
std::vector<Track::pointer> duplicateTracks{ Track::findByMBID(dbSession, *trackMetadata->mbid) };
|
||||||
|
|
||||||
// find for an existing track MBID as the file may have just been moved
|
// find for an existing track MBID as the file may have just been moved
|
||||||
if (!track && duplicateTracks.size() == 1)
|
if (!track && duplicateTracks.size() == 1)
|
||||||
@@ -586,7 +587,7 @@ namespace lms::scanner
|
|||||||
}
|
}
|
||||||
|
|
||||||
// We estimate this is an audio file if the duration is not null
|
// We estimate this is an audio file if the duration is not null
|
||||||
if (trackMetadata.audioProperties.duration == std::chrono::milliseconds::zero())
|
if (trackMetadata->audioProperties.duration == std::chrono::milliseconds::zero())
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Skipped '" << file.string() << "' (duration is 0)");
|
LMS_LOG(DBUPDATER, DEBUG, "Skipped '" << file.string() << "' (duration is 0)");
|
||||||
|
|
||||||
@@ -596,14 +597,14 @@ namespace lms::scanner
|
|||||||
track.remove();
|
track.remove();
|
||||||
stats.deletions++;
|
stats.deletions++;
|
||||||
}
|
}
|
||||||
stats.errors.emplace_back(ScanError{ file, ScanErrorType::BadDuration });
|
stats.errors.emplace_back(file, ScanErrorType::BadDuration);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ***** Title
|
// ***** Title
|
||||||
std::string title;
|
std::string title;
|
||||||
if (!trackMetadata.title.empty())
|
if (!trackMetadata->title.empty())
|
||||||
title = trackMetadata.title;
|
title = trackMetadata->title;
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// TODO parse file name guess track etc.
|
// TODO parse file name guess track etc.
|
||||||
@@ -625,91 +626,92 @@ namespace lms::scanner
|
|||||||
assert(track);
|
assert(track);
|
||||||
|
|
||||||
// Audio properties
|
// Audio properties
|
||||||
track.modify()->setBitrate(trackMetadata.audioProperties.bitrate);
|
track.modify()->setBitrate(trackMetadata->audioProperties.bitrate);
|
||||||
track.modify()->setBitsPerSample(trackMetadata.audioProperties.bitsPerSample);
|
track.modify()->setBitsPerSample(trackMetadata->audioProperties.bitsPerSample);
|
||||||
track.modify()->setChannelCount(trackMetadata.audioProperties.channelCount);
|
track.modify()->setChannelCount(trackMetadata->audioProperties.channelCount);
|
||||||
track.modify()->setDuration(trackMetadata.audioProperties.duration);
|
track.modify()->setDuration(trackMetadata->audioProperties.duration);
|
||||||
track.modify()->setSampleRate(trackMetadata.audioProperties.sampleRate);
|
track.modify()->setSampleRate(trackMetadata->audioProperties.sampleRate);
|
||||||
|
|
||||||
track.modify()->setRelativeFilePath(fileInfo->relativePath);
|
track.modify()->setRelativeFilePath(fileInfo->relativePath);
|
||||||
track.modify()->setFileSize(fileInfo->fileSize);
|
track.modify()->setFileSize(fileInfo->fileSize);
|
||||||
track.modify()->setLastWriteTime(fileInfo->lastWriteTime);
|
track.modify()->setLastWriteTime(fileInfo->lastWriteTime);
|
||||||
|
|
||||||
track.modify()->setMediaLibrary(MediaLibrary::find(dbSession, libraryInfo.id)); // may be null if settings are updated in // => next scan will correct this
|
MediaLibrary::pointer mediaLibrary{ MediaLibrary::find(dbSession, libraryInfo.id) }; // may be null if settings are updated in // => next scan will correct this
|
||||||
track.modify()->setDirectory(getOrCreateDirectory(dbSession, file.parent_path(), libraryInfo.rootDirectory));
|
track.modify()->setMediaLibrary(mediaLibrary);
|
||||||
|
track.modify()->setDirectory(getOrCreateDirectory(dbSession, file.parent_path(), mediaLibrary));
|
||||||
|
|
||||||
track.modify()->clearArtistLinks();
|
track.modify()->clearArtistLinks();
|
||||||
// Do not fallback on artists with the same name but having a MBID for artist and releaseArtists, as it may be corrected by properly tagging files
|
// Do not fallback on artists with the same name but having a MBID for artist and releaseArtists, as it may be corrected by properly tagging files
|
||||||
for (const Artist::pointer& artist : getOrCreateArtists(dbSession, trackMetadata.artists, false))
|
for (const Artist::pointer& artist : getOrCreateArtists(dbSession, trackMetadata->artists, false))
|
||||||
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, artist, TrackArtistLinkType::Artist));
|
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, artist, TrackArtistLinkType::Artist));
|
||||||
|
|
||||||
if (trackMetadata.medium && trackMetadata.medium->release)
|
if (trackMetadata->medium && trackMetadata->medium->release)
|
||||||
{
|
{
|
||||||
for (const Artist::pointer& releaseArtist : getOrCreateArtists(dbSession, trackMetadata.medium->release->artists, false))
|
for (const Artist::pointer& releaseArtist : getOrCreateArtists(dbSession, trackMetadata->medium->release->artists, false))
|
||||||
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, releaseArtist, TrackArtistLinkType::ReleaseArtist));
|
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, releaseArtist, TrackArtistLinkType::ReleaseArtist));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow fallbacks on artists with the same name even if they have MBID, since there is no tag to indicate the MBID of these artists
|
// Allow fallbacks on artists with the same name even if they have MBID, since there is no tag to indicate the MBID of these artists
|
||||||
// We could ask MusicBrainz to get all the information, but that would heavily slow down the import process
|
// We could ask MusicBrainz to get all the information, but that would heavily slow down the import process
|
||||||
for (const Artist::pointer& conductor : getOrCreateArtists(dbSession, trackMetadata.conductorArtists, true))
|
for (const Artist::pointer& conductor : getOrCreateArtists(dbSession, trackMetadata->conductorArtists, true))
|
||||||
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, conductor, TrackArtistLinkType::Conductor));
|
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, conductor, TrackArtistLinkType::Conductor));
|
||||||
|
|
||||||
for (const Artist::pointer& composer : getOrCreateArtists(dbSession, trackMetadata.composerArtists, true))
|
for (const Artist::pointer& composer : getOrCreateArtists(dbSession, trackMetadata->composerArtists, true))
|
||||||
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, composer, TrackArtistLinkType::Composer));
|
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, composer, TrackArtistLinkType::Composer));
|
||||||
|
|
||||||
for (const Artist::pointer& lyricist : getOrCreateArtists(dbSession, trackMetadata.lyricistArtists, true))
|
for (const Artist::pointer& lyricist : getOrCreateArtists(dbSession, trackMetadata->lyricistArtists, true))
|
||||||
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, lyricist, TrackArtistLinkType::Lyricist));
|
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, lyricist, TrackArtistLinkType::Lyricist));
|
||||||
|
|
||||||
for (const Artist::pointer& mixer : getOrCreateArtists(dbSession, trackMetadata.mixerArtists, true))
|
for (const Artist::pointer& mixer : getOrCreateArtists(dbSession, trackMetadata->mixerArtists, true))
|
||||||
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, mixer, TrackArtistLinkType::Mixer));
|
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, mixer, TrackArtistLinkType::Mixer));
|
||||||
|
|
||||||
for (const auto& [role, performers] : trackMetadata.performerArtists)
|
for (const auto& [role, performers] : trackMetadata->performerArtists)
|
||||||
{
|
{
|
||||||
for (const Artist::pointer& performer : getOrCreateArtists(dbSession, performers, true))
|
for (const Artist::pointer& performer : getOrCreateArtists(dbSession, performers, true))
|
||||||
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, performer, TrackArtistLinkType::Performer, role));
|
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, performer, TrackArtistLinkType::Performer, role));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const Artist::pointer& producer : getOrCreateArtists(dbSession, trackMetadata.producerArtists, true))
|
for (const Artist::pointer& producer : getOrCreateArtists(dbSession, trackMetadata->producerArtists, true))
|
||||||
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, producer, TrackArtistLinkType::Producer));
|
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, producer, TrackArtistLinkType::Producer));
|
||||||
|
|
||||||
for (const Artist::pointer& remixer : getOrCreateArtists(dbSession, trackMetadata.remixerArtists, true))
|
for (const Artist::pointer& remixer : getOrCreateArtists(dbSession, trackMetadata->remixerArtists, true))
|
||||||
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, remixer, TrackArtistLinkType::Remixer));
|
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, remixer, TrackArtistLinkType::Remixer));
|
||||||
|
|
||||||
track.modify()->setScanVersion(_settings.scanVersion);
|
track.modify()->setScanVersion(_settings.scanVersion);
|
||||||
if (trackMetadata.medium && trackMetadata.medium->release)
|
if (trackMetadata->medium && trackMetadata->medium->release)
|
||||||
track.modify()->setRelease(getOrCreateRelease(dbSession, *trackMetadata.medium->release, file.parent_path()));
|
track.modify()->setRelease(getOrCreateRelease(dbSession, *trackMetadata->medium->release, file.parent_path()));
|
||||||
else
|
else
|
||||||
track.modify()->setRelease({});
|
track.modify()->setRelease({});
|
||||||
track.modify()->setTotalTrack(trackMetadata.medium ? trackMetadata.medium->trackCount : std::nullopt);
|
track.modify()->setTotalTrack(trackMetadata->medium ? trackMetadata->medium->trackCount : std::nullopt);
|
||||||
track.modify()->setReleaseReplayGain(trackMetadata.medium ? trackMetadata.medium->replayGain : std::nullopt);
|
track.modify()->setReleaseReplayGain(trackMetadata->medium ? trackMetadata->medium->replayGain : std::nullopt);
|
||||||
track.modify()->setDiscSubtitle(trackMetadata.medium ? trackMetadata.medium->name : "");
|
track.modify()->setDiscSubtitle(trackMetadata->medium ? trackMetadata->medium->name : "");
|
||||||
track.modify()->setClusters(getOrCreateClusters(dbSession, trackMetadata));
|
track.modify()->setClusters(getOrCreateClusters(dbSession, *trackMetadata));
|
||||||
track.modify()->setName(title);
|
track.modify()->setName(title);
|
||||||
track.modify()->setAddedTime(Wt::WDateTime::currentDateTime());
|
track.modify()->setAddedTime(Wt::WDateTime::currentDateTime());
|
||||||
track.modify()->setTrackNumber(trackMetadata.position);
|
track.modify()->setTrackNumber(trackMetadata->position);
|
||||||
track.modify()->setDiscNumber(trackMetadata.medium ? trackMetadata.medium->position : std::nullopt);
|
track.modify()->setDiscNumber(trackMetadata->medium ? trackMetadata->medium->position : std::nullopt);
|
||||||
track.modify()->setDate(trackMetadata.date);
|
track.modify()->setDate(trackMetadata->date);
|
||||||
track.modify()->setYear(trackMetadata.year);
|
track.modify()->setYear(trackMetadata->year);
|
||||||
track.modify()->setOriginalDate(trackMetadata.originalDate);
|
track.modify()->setOriginalDate(trackMetadata->originalDate);
|
||||||
track.modify()->setOriginalYear(trackMetadata.originalYear);
|
track.modify()->setOriginalYear(trackMetadata->originalYear);
|
||||||
|
|
||||||
// If a file has an OriginalDate but no date, set it to ease filtering
|
// If a file has an OriginalDate but no date, set it to ease filtering
|
||||||
if (!trackMetadata.date.isValid() && trackMetadata.originalDate.isValid())
|
if (!trackMetadata->date.isValid() && trackMetadata->originalDate.isValid())
|
||||||
track.modify()->setDate(trackMetadata.originalDate);
|
track.modify()->setDate(trackMetadata->originalDate);
|
||||||
|
|
||||||
// If a file has an OriginalYear but no Year, set it to ease filtering
|
// If a file has an OriginalYear but no Year, set it to ease filtering
|
||||||
if (!trackMetadata.year && trackMetadata.originalYear)
|
if (!trackMetadata->year && trackMetadata->originalYear)
|
||||||
track.modify()->setYear(trackMetadata.originalYear);
|
track.modify()->setYear(trackMetadata->originalYear);
|
||||||
|
|
||||||
track.modify()->setRecordingMBID(trackMetadata.recordingMBID);
|
track.modify()->setRecordingMBID(trackMetadata->recordingMBID);
|
||||||
track.modify()->setTrackMBID(trackMetadata.mbid);
|
track.modify()->setTrackMBID(trackMetadata->mbid);
|
||||||
if (auto trackFeatures{ TrackFeatures::find(dbSession, track->getId()) })
|
if (auto trackFeatures{ TrackFeatures::find(dbSession, track->getId()) })
|
||||||
trackFeatures.remove(); // TODO: only if MBID changed?
|
trackFeatures.remove(); // TODO: only if MBID changed?
|
||||||
track.modify()->setHasCover(trackMetadata.hasCover);
|
track.modify()->setHasCover(trackMetadata->hasCover);
|
||||||
track.modify()->setCopyright(trackMetadata.copyright);
|
track.modify()->setCopyright(trackMetadata->copyright);
|
||||||
track.modify()->setCopyrightURL(trackMetadata.copyrightURL);
|
track.modify()->setCopyrightURL(trackMetadata->copyrightURL);
|
||||||
track.modify()->setTrackReplayGain(trackMetadata.replayGain);
|
track.modify()->setTrackReplayGain(trackMetadata->replayGain);
|
||||||
track.modify()->setArtistDisplayName(trackMetadata.artistDisplayName);
|
track.modify()->setArtistDisplayName(trackMetadata->artistDisplayName);
|
||||||
|
|
||||||
if (added)
|
if (added)
|
||||||
{
|
{
|
||||||
@@ -723,7 +725,7 @@ namespace lms::scanner
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ScanStepScanFiles::processImageFileScanData(ScanContext& context, const std::filesystem::path& file, const ImageInfo& imageInfo, const ScannerSettings::MediaLibraryInfo& libraryInfo)
|
void ScanStepScanFiles::processImageFileScanData(ScanContext& context, const std::filesystem::path& file, const ImageInfo* imageInfo, const ScannerSettings::MediaLibraryInfo& libraryInfo)
|
||||||
{
|
{
|
||||||
LMS_SCOPED_TRACE_DETAILED("Scanner", "ProcessImageScanData");
|
LMS_SCOPED_TRACE_DETAILED("Scanner", "ProcessImageScanData");
|
||||||
|
|
||||||
@@ -739,6 +741,17 @@ namespace lms::scanner
|
|||||||
db::Session& dbSession{ _db.getTLSSession() };
|
db::Session& dbSession{ _db.getTLSSession() };
|
||||||
db::Image::pointer image{ db::Image::find(dbSession, file) };
|
db::Image::pointer image{ db::Image::find(dbSession, file) };
|
||||||
|
|
||||||
|
if (!imageInfo)
|
||||||
|
{
|
||||||
|
if (image)
|
||||||
|
{
|
||||||
|
image.remove();
|
||||||
|
stats.deletions++;
|
||||||
|
}
|
||||||
|
context.stats.errors.emplace_back(file, ScanErrorType::CannotReadImageFile);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
bool added;
|
bool added;
|
||||||
if (!image)
|
if (!image)
|
||||||
{
|
{
|
||||||
@@ -752,9 +765,10 @@ namespace lms::scanner
|
|||||||
|
|
||||||
image.modify()->setLastWriteTime(fileInfo->lastWriteTime);
|
image.modify()->setLastWriteTime(fileInfo->lastWriteTime);
|
||||||
image.modify()->setFileSize(fileInfo->fileSize);
|
image.modify()->setFileSize(fileInfo->fileSize);
|
||||||
image.modify()->setHeight(imageInfo.height);
|
image.modify()->setHeight(imageInfo->height);
|
||||||
image.modify()->setWidth(imageInfo.width);
|
image.modify()->setWidth(imageInfo->width);
|
||||||
image.modify()->setDirectory(getOrCreateDirectory(dbSession, file.parent_path(), libraryInfo.rootDirectory));
|
MediaLibrary::pointer mediaLibrary{ MediaLibrary::find(dbSession, libraryInfo.id) }; // may be null if settings are updated in // => next scan will correct this
|
||||||
|
image.modify()->setDirectory(getOrCreateDirectory(dbSession, file.parent_path(), mediaLibrary));
|
||||||
|
|
||||||
if (added)
|
if (added)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -46,8 +46,8 @@ namespace lms::scanner
|
|||||||
bool checkImageFileNeedScan(ScanContext& context, const std::filesystem::path& file);
|
bool checkImageFileNeedScan(ScanContext& context, const std::filesystem::path& file);
|
||||||
|
|
||||||
void processFileScanResults(ScanContext& context, std::span<const FileScanResult> scanResults, const ScannerSettings::MediaLibraryInfo& libraryInfo);
|
void processFileScanResults(ScanContext& context, std::span<const FileScanResult> scanResults, const ScannerSettings::MediaLibraryInfo& libraryInfo);
|
||||||
void processAudioFileScanData(ScanContext& context, const std::filesystem::path& path, const metadata::Track& trackMetadata, const ScannerSettings::MediaLibraryInfo& libraryInfo);
|
void processAudioFileScanData(ScanContext& context, const std::filesystem::path& path, const metadata::Track* trackMetadata, const ScannerSettings::MediaLibraryInfo& libraryInfo);
|
||||||
void processImageFileScanData(ScanContext& context, const std::filesystem::path& path, const ImageInfo& imageInfo, const ScannerSettings::MediaLibraryInfo& libraryInfo);
|
void processImageFileScanData(ScanContext& context, const std::filesystem::path& path, const ImageInfo* imageInfo, const ScannerSettings::MediaLibraryInfo& libraryInfo);
|
||||||
|
|
||||||
std::unique_ptr<metadata::IParser> _metadataParser;
|
std::unique_ptr<metadata::IParser> _metadataParser;
|
||||||
const std::vector<std::string> _extraTagsToParse;
|
const std::vector<std::string> _extraTagsToParse;
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 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 "ScanStepUpdateLibraryFields.hpp"
|
||||||
|
|
||||||
|
#include "core/ILogger.hpp"
|
||||||
|
#include "core/Path.hpp"
|
||||||
|
#include "database/Db.hpp"
|
||||||
|
#include "database/Directory.hpp"
|
||||||
|
#include "database/MediaLibrary.hpp"
|
||||||
|
#include "database/Session.hpp"
|
||||||
|
|
||||||
|
namespace lms::scanner
|
||||||
|
{
|
||||||
|
|
||||||
|
void ScanStepUpdateLibraryFields::process(ScanContext& context)
|
||||||
|
{
|
||||||
|
processDirectories(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScanStepUpdateLibraryFields::processDirectories(ScanContext& context)
|
||||||
|
{
|
||||||
|
for (const ScannerSettings::MediaLibraryInfo& mediaLibrary : _settings.mediaLibraries)
|
||||||
|
{
|
||||||
|
if (_abortScan)
|
||||||
|
break;
|
||||||
|
|
||||||
|
processDirectory(context, mediaLibrary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScanStepUpdateLibraryFields::processDirectory(ScanContext& context, const ScannerSettings::MediaLibraryInfo& mediaLibrary)
|
||||||
|
{
|
||||||
|
db::Session& session{ _db.getTLSSession() };
|
||||||
|
|
||||||
|
constexpr std::size_t batchSize = 100;
|
||||||
|
|
||||||
|
db::RangeResults<db::DirectoryId> entries;
|
||||||
|
while (!_abortScan)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
|
entries = db::Directory::findMismatchedLibrary(session, db::Range{ 0, batchSize }, mediaLibrary.rootDirectory, mediaLibrary.id);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (entries.results.empty())
|
||||||
|
break;
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ session.createWriteTransaction() };
|
||||||
|
|
||||||
|
db::MediaLibrary::pointer library{ db::MediaLibrary::find(session, mediaLibrary.id) };
|
||||||
|
if (!library) // may be legit
|
||||||
|
break;
|
||||||
|
|
||||||
|
for (const db::DirectoryId directoryId : entries.results)
|
||||||
|
{
|
||||||
|
if (_abortScan)
|
||||||
|
break;
|
||||||
|
|
||||||
|
db::Directory::pointer directory{ db::Directory::find(session, directoryId) };
|
||||||
|
directory.modify()->setMediaLibrary(library);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context.currentStepStats.processedElems += entries.results.size();
|
||||||
|
_progressCallback(context.currentStepStats);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace lms::scanner
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 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/DirectoryId.hpp"
|
||||||
|
|
||||||
|
#include "ScanStepBase.hpp"
|
||||||
|
|
||||||
|
namespace lms::scanner
|
||||||
|
{
|
||||||
|
class ScanStepUpdateLibraryFields : public ScanStepBase
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using ScanStepBase::ScanStepBase;
|
||||||
|
|
||||||
|
private:
|
||||||
|
core::LiteralString getStepName() const override { return "Update Library fields"; }
|
||||||
|
ScanStep getStep() const override { return ScanStep::UpdateLibraryFields; }
|
||||||
|
void process(ScanContext& context) override;
|
||||||
|
|
||||||
|
void processDirectories(ScanContext& context);
|
||||||
|
void processDirectory(ScanContext& context, const ScannerSettings::MediaLibraryInfo& mediaLibrary);
|
||||||
|
};
|
||||||
|
} // namespace lms::scanner
|
||||||
@@ -40,6 +40,7 @@
|
|||||||
#include "ScanStepOptimize.hpp"
|
#include "ScanStepOptimize.hpp"
|
||||||
#include "ScanStepRemoveOrphanedDbEntries.hpp"
|
#include "ScanStepRemoveOrphanedDbEntries.hpp"
|
||||||
#include "ScanStepScanFiles.hpp"
|
#include "ScanStepScanFiles.hpp"
|
||||||
|
#include "ScanStepUpdateLibraryFields.hpp"
|
||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
{
|
{
|
||||||
@@ -279,7 +280,7 @@ namespace lms::scanner
|
|||||||
LMS_SCOPED_TRACE_OVERVIEW("Scanner", scanStep->getStepName());
|
LMS_SCOPED_TRACE_OVERVIEW("Scanner", scanStep->getStepName());
|
||||||
|
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Starting scan step '" << scanStep->getStepName() << "'");
|
LMS_LOG(DBUPDATER, DEBUG, "Starting scan step '" << scanStep->getStepName() << "'");
|
||||||
scanContext.currentStepStats = ScanStepStats{ .startTime = Wt::WDateTime::currentDateTime(), .stepIndex = stepIndex++, .currentStep = scanStep->getStep() };
|
scanContext.currentStepStats = ScanStepStats{ .startTime = Wt::WDateTime::currentDateTime(), .stepCount = _scanSteps.size(), .stepIndex = stepIndex++, .currentStep = scanStep->getStep() };
|
||||||
|
|
||||||
notifyInProgress(scanContext.currentStepStats);
|
notifyInProgress(scanContext.currentStepStats);
|
||||||
scanStep->process(scanContext);
|
scanStep->process(scanContext);
|
||||||
@@ -339,11 +340,12 @@ namespace lms::scanner
|
|||||||
_db
|
_db
|
||||||
};
|
};
|
||||||
|
|
||||||
// Order is important
|
// Order is important, steps are sequential
|
||||||
_scanSteps.clear();
|
_scanSteps.clear();
|
||||||
_scanSteps.push_back(std::make_unique<ScanStepDiscoverFiles>(params));
|
_scanSteps.push_back(std::make_unique<ScanStepDiscoverFiles>(params));
|
||||||
_scanSteps.push_back(std::make_unique<ScanStepScanFiles>(params));
|
_scanSteps.push_back(std::make_unique<ScanStepScanFiles>(params));
|
||||||
_scanSteps.push_back(std::make_unique<ScanStepCheckForRemovedFiles>(params));
|
_scanSteps.push_back(std::make_unique<ScanStepCheckForRemovedFiles>(params));
|
||||||
|
_scanSteps.push_back(std::make_unique<ScanStepUpdateLibraryFields>(params));
|
||||||
_scanSteps.push_back(std::make_unique<ScanStepAssociateArtistImages>(params));
|
_scanSteps.push_back(std::make_unique<ScanStepAssociateArtistImages>(params));
|
||||||
_scanSteps.push_back(std::make_unique<ScanStepRemoveOrphanedDbEntries>(params));
|
_scanSteps.push_back(std::make_unique<ScanStepRemoveOrphanedDbEntries>(params));
|
||||||
_scanSteps.push_back(std::make_unique<ScanStepCompact>(params));
|
_scanSteps.push_back(std::make_unique<ScanStepCompact>(params));
|
||||||
|
|||||||
@@ -72,14 +72,15 @@ namespace lms::scanner
|
|||||||
ReloadSimilarityEngine,
|
ReloadSimilarityEngine,
|
||||||
RemoveOrphanedDbEntries,
|
RemoveOrphanedDbEntries,
|
||||||
ScanFiles,
|
ScanFiles,
|
||||||
|
UpdateLibraryFields,
|
||||||
};
|
};
|
||||||
static inline constexpr unsigned ScanProgressStepCount{ 11 };
|
|
||||||
|
|
||||||
// reduced scan stats
|
// reduced scan stats
|
||||||
struct ScanStepStats
|
struct ScanStepStats
|
||||||
{
|
{
|
||||||
Wt::WDateTime startTime;
|
Wt::WDateTime startTime;
|
||||||
|
|
||||||
|
std::size_t stepCount{};
|
||||||
std::size_t stepIndex{};
|
std::size_t stepIndex{};
|
||||||
ScanStep currentStep;
|
ScanStep currentStep;
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
|
|
||||||
#include "ClientInfo.hpp"
|
#include "ClientInfo.hpp"
|
||||||
#include "ProtocolVersion.hpp"
|
#include "ProtocolVersion.hpp"
|
||||||
|
#include "SubsonicResponse.hpp"
|
||||||
|
|
||||||
namespace lms::db
|
namespace lms::db
|
||||||
{
|
{
|
||||||
@@ -43,6 +44,7 @@ namespace lms::api::subsonic
|
|||||||
const db::ObjectPtr<db::User> user;
|
const db::ObjectPtr<db::User> user;
|
||||||
ClientInfo clientInfo;
|
ClientInfo clientInfo;
|
||||||
ProtocolVersion serverProtocolVersion;
|
ProtocolVersion serverProtocolVersion;
|
||||||
|
ResponseFormat responseFormat;
|
||||||
bool enableOpenSubsonic{ true };
|
bool enableOpenSubsonic{ true };
|
||||||
bool enableDefaultCover{};
|
bool enableDefaultCover{};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -31,6 +31,11 @@ namespace lms::api::subsonic
|
|||||||
return "ar-" + id.toString();
|
return "ar-" + id.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string idToString(db::DirectoryId id)
|
||||||
|
{
|
||||||
|
return "dir-" + id.toString();
|
||||||
|
}
|
||||||
|
|
||||||
std::string idToString(db::MediaLibraryId id)
|
std::string idToString(db::MediaLibraryId id)
|
||||||
{
|
{
|
||||||
// No need to prefix as this is only used at well known places
|
// No need to prefix as this is only used at well known places
|
||||||
@@ -76,6 +81,22 @@ namespace lms::core::stringUtils
|
|||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template<>
|
||||||
|
std::optional<db::DirectoryId> readAs(std::string_view str)
|
||||||
|
{
|
||||||
|
std::vector<std::string_view> values{ core::stringUtils::splitString(str, '-') };
|
||||||
|
if (values.size() != 2)
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
if (values[0] != "dir")
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
if (const auto value{ core::stringUtils::readAs<db::DirectoryId::ValueType>(values[1]) })
|
||||||
|
return db::DirectoryId{ *value };
|
||||||
|
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
template<>
|
template<>
|
||||||
std::optional<db::MediaLibraryId> readAs(std::string_view str)
|
std::optional<db::MediaLibraryId> readAs(std::string_view str)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
|
|
||||||
#include "core/String.hpp"
|
#include "core/String.hpp"
|
||||||
#include "database/ArtistId.hpp"
|
#include "database/ArtistId.hpp"
|
||||||
|
#include "database/DirectoryId.hpp"
|
||||||
#include "database/MediaLibraryId.hpp"
|
#include "database/MediaLibraryId.hpp"
|
||||||
#include "database/ReleaseId.hpp"
|
#include "database/ReleaseId.hpp"
|
||||||
#include "database/TrackId.hpp"
|
#include "database/TrackId.hpp"
|
||||||
@@ -33,6 +34,7 @@ namespace lms::api::subsonic
|
|||||||
};
|
};
|
||||||
|
|
||||||
std::string idToString(db::ArtistId id);
|
std::string idToString(db::ArtistId id);
|
||||||
|
std::string idToString(db::DirectoryId id);
|
||||||
std::string idToString(db::MediaLibraryId id);
|
std::string idToString(db::MediaLibraryId id);
|
||||||
std::string idToString(db::ReleaseId id);
|
std::string idToString(db::ReleaseId id);
|
||||||
std::string idToString(db::TrackId id);
|
std::string idToString(db::TrackId id);
|
||||||
@@ -49,6 +51,9 @@ namespace lms::core::stringUtils
|
|||||||
template<>
|
template<>
|
||||||
std::optional<db::ArtistId> readAs(std::string_view str);
|
std::optional<db::ArtistId> readAs(std::string_view str);
|
||||||
|
|
||||||
|
template<>
|
||||||
|
std::optional<db::DirectoryId> readAs(std::string_view str);
|
||||||
|
|
||||||
template<>
|
template<>
|
||||||
std::optional<db::MediaLibraryId> readAs(std::string_view str);
|
std::optional<db::MediaLibraryId> readAs(std::string_view str);
|
||||||
|
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ namespace lms::api::subsonic
|
|||||||
{ "/getAlbum", { handleGetAlbumRequest } },
|
{ "/getAlbum", { handleGetAlbumRequest } },
|
||||||
{ "/getSong", { handleGetSongRequest } },
|
{ "/getSong", { handleGetSongRequest } },
|
||||||
{ "/getVideos", { handleNotImplemented } },
|
{ "/getVideos", { handleNotImplemented } },
|
||||||
{ "/getArtistInfo", { handleGetArtistInfoRequest } },
|
{ "/getArtistInfo", { handleNotImplemented } },
|
||||||
{ "/getArtistInfo2", { handleGetArtistInfo2Request } },
|
{ "/getArtistInfo2", { handleGetArtistInfo2Request } },
|
||||||
{ "/getAlbumInfo", { handleNotImplemented } },
|
{ "/getAlbumInfo", { handleNotImplemented } },
|
||||||
{ "/getAlbumInfo2", { handleNotImplemented } },
|
{ "/getAlbumInfo2", { handleNotImplemented } },
|
||||||
@@ -411,6 +411,7 @@ namespace lms::api::subsonic
|
|||||||
const db::UserId userId{ authenticateUser(request, clientInfo) };
|
const db::UserId userId{ authenticateUser(request, clientInfo) };
|
||||||
bool enableOpenSubsonic{ _openSubsonicDisabledClients.find(clientInfo.name) == std::cend(_openSubsonicDisabledClients) };
|
bool enableOpenSubsonic{ _openSubsonicDisabledClients.find(clientInfo.name) == std::cend(_openSubsonicDisabledClients) };
|
||||||
bool enableDefaultCover{ _defaultCoverClients.find(clientInfo.name) != std::cend(_openSubsonicDisabledClients) };
|
bool enableDefaultCover{ _defaultCoverClients.find(clientInfo.name) != std::cend(_openSubsonicDisabledClients) };
|
||||||
|
const ResponseFormat format{ getParameterAs<std::string>(request.getParameterMap(), "f").value_or("xml") == "json" ? ResponseFormat::json : ResponseFormat::xml };
|
||||||
|
|
||||||
db::User::pointer user;
|
db::User::pointer user;
|
||||||
{
|
{
|
||||||
@@ -422,7 +423,16 @@ namespace lms::api::subsonic
|
|||||||
throw UserNotAuthorizedError{};
|
throw UserNotAuthorizedError{};
|
||||||
}
|
}
|
||||||
|
|
||||||
return { parameters, _db.getTLSSession(), user, clientInfo, getServerProtocolVersion(clientInfo.name), enableOpenSubsonic, enableDefaultCover };
|
return RequestContext{
|
||||||
|
.parameters = parameters,
|
||||||
|
.dbSession = _db.getTLSSession(),
|
||||||
|
.user = user,
|
||||||
|
.clientInfo = clientInfo,
|
||||||
|
.serverProtocolVersion = getServerProtocolVersion(clientInfo.name),
|
||||||
|
.responseFormat = format,
|
||||||
|
.enableOpenSubsonic = enableOpenSubsonic,
|
||||||
|
.enableDefaultCover = enableDefaultCover
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
db::UserId SubsonicResource::authenticateUser(const Wt::Http::Request& request, const ClientInfo& clientInfo)
|
db::UserId SubsonicResource::authenticateUser(const Wt::Http::Request& request, const ClientInfo& clientInfo)
|
||||||
|
|||||||
@@ -193,18 +193,6 @@ namespace lms::api::subsonic
|
|||||||
std::function<boost::property_tree::ptree(const Node&)> nodeToPropertyTree = [&](const Node& node) {
|
std::function<boost::property_tree::ptree(const Node&)> nodeToPropertyTree = [&](const Node& node) {
|
||||||
boost::property_tree::ptree res;
|
boost::property_tree::ptree res;
|
||||||
|
|
||||||
for (const auto& [key, value] : node._attributes)
|
|
||||||
{
|
|
||||||
if (std::holds_alternative<Node::string>(value))
|
|
||||||
res.put("<xmlattr>." + std::string{ key.str() }, std::get<Node::string>(value));
|
|
||||||
else if (std::holds_alternative<bool>(value))
|
|
||||||
res.put("<xmlattr>." + std::string{ key.str() }, std::get<bool>(value));
|
|
||||||
else if (std::holds_alternative<float>(value))
|
|
||||||
res.put("<xmlattr>." + std::string{ key.str() }, std::get<float>(value));
|
|
||||||
else if (std::holds_alternative<long long>(value))
|
|
||||||
res.put("<xmlattr>." + std::string{ key.str() }, std::get<long long>(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
auto valueToPropertyTree = [](const Node::ValueType& value) {
|
auto valueToPropertyTree = [](const Node::ValueType& value) {
|
||||||
boost::property_tree::ptree res;
|
boost::property_tree::ptree res;
|
||||||
std::visit([&](const auto& rawValue) {
|
std::visit([&](const auto& rawValue) {
|
||||||
@@ -239,6 +227,20 @@ namespace lms::api::subsonic
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const auto& [key, value] : node._attributes)
|
||||||
|
{
|
||||||
|
if (std::holds_alternative<Node::string>(value))
|
||||||
|
res.put("<xmlattr>." + std::string{ key.str() }, std::get<Node::string>(value));
|
||||||
|
else if (std::holds_alternative<bool>(value))
|
||||||
|
res.put("<xmlattr>." + std::string{ key.str() }, std::get<bool>(value));
|
||||||
|
else if (std::holds_alternative<float>(value))
|
||||||
|
res.put("<xmlattr>." + std::string{ key.str() }, std::get<float>(value));
|
||||||
|
else if (std::holds_alternative<long long>(value))
|
||||||
|
res.put("<xmlattr>." + std::string{ key.str() }, std::get<long long>(value));
|
||||||
|
else
|
||||||
|
assert(false);
|
||||||
|
}
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -264,74 +266,64 @@ namespace lms::api::subsonic
|
|||||||
first = false;
|
first = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node._value)
|
// Values are handled manually (using attributes) in json format
|
||||||
|
assert(!node._value);
|
||||||
|
|
||||||
|
for (const auto& [key, childNode] : node._children)
|
||||||
{
|
{
|
||||||
if (!first)
|
if (!first)
|
||||||
os << ',';
|
os << ',';
|
||||||
|
|
||||||
os << "\"value\":";
|
serializeEscapedString(os, key.str());
|
||||||
serializeValue(os, *node._value);
|
os << ':';
|
||||||
|
serializeNode(os, childNode);
|
||||||
|
|
||||||
first = false;
|
first = false;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
for (const auto& [key, childArrayNodes] : node._childrenArrays)
|
||||||
{
|
{
|
||||||
for (const auto& [key, childNode] : node._children)
|
if (!first)
|
||||||
{
|
os << ',';
|
||||||
if (!first)
|
|
||||||
os << ',';
|
serializeEscapedString(os, key.str());
|
||||||
|
os << ":[";
|
||||||
|
|
||||||
|
bool firstChild{ true };
|
||||||
|
for (const Response::Node& childNode : childArrayNodes)
|
||||||
|
{
|
||||||
|
if (!firstChild)
|
||||||
|
os << ",";
|
||||||
|
|
||||||
serializeEscapedString(os, key.str());
|
|
||||||
os << ':';
|
|
||||||
serializeNode(os, childNode);
|
serializeNode(os, childNode);
|
||||||
|
firstChild = false;
|
||||||
first = false;
|
|
||||||
}
|
}
|
||||||
|
os << ']';
|
||||||
|
|
||||||
for (const auto& [key, childArrayNodes] : node._childrenArrays)
|
first = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& [key, childValues] : node._childrenValues)
|
||||||
|
{
|
||||||
|
if (!first)
|
||||||
|
os << ',';
|
||||||
|
|
||||||
|
serializeEscapedString(os, key.str());
|
||||||
|
os << ":[";
|
||||||
|
|
||||||
|
bool firstChild{ true };
|
||||||
|
for (const Node::ValueType& childValue : childValues)
|
||||||
{
|
{
|
||||||
if (!first)
|
if (!firstChild)
|
||||||
os << ',';
|
os << ",";
|
||||||
|
|
||||||
serializeEscapedString(os, key.str());
|
serializeValue(os, childValue);
|
||||||
os << ":[";
|
|
||||||
|
|
||||||
bool firstChild{ true };
|
firstChild = false;
|
||||||
for (const Response::Node& childNode : childArrayNodes)
|
|
||||||
{
|
|
||||||
if (!firstChild)
|
|
||||||
os << ",";
|
|
||||||
|
|
||||||
serializeNode(os, childNode);
|
|
||||||
firstChild = false;
|
|
||||||
}
|
|
||||||
os << ']';
|
|
||||||
|
|
||||||
first = false;
|
|
||||||
}
|
}
|
||||||
|
os << ']';
|
||||||
|
|
||||||
for (const auto& [key, childValues] : node._childrenValues)
|
first = false;
|
||||||
{
|
|
||||||
if (!first)
|
|
||||||
os << ',';
|
|
||||||
|
|
||||||
serializeEscapedString(os, key.str());
|
|
||||||
os << ":[";
|
|
||||||
|
|
||||||
bool firstChild{ true };
|
|
||||||
for (const Node::ValueType& childValue : childValues)
|
|
||||||
{
|
|
||||||
if (!firstChild)
|
|
||||||
os << ",";
|
|
||||||
|
|
||||||
serializeValue(os, childValue);
|
|
||||||
|
|
||||||
firstChild = false;
|
|
||||||
}
|
|
||||||
os << ']';
|
|
||||||
|
|
||||||
first = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
os << '}';
|
os << '}';
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
|
|
||||||
#include "core/LiteralString.hpp"
|
#include "core/LiteralString.hpp"
|
||||||
|
|
||||||
#include "RequestContext.hpp"
|
#include "ProtocolVersion.hpp"
|
||||||
#include "SubsonicResponseAllocator.hpp"
|
#include "SubsonicResponseAllocator.hpp"
|
||||||
|
|
||||||
namespace lms::api::subsonic
|
namespace lms::api::subsonic
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ namespace lms::api::subsonic
|
|||||||
for (const ReleaseId releaseId : releases.results)
|
for (const ReleaseId releaseId : releases.results)
|
||||||
{
|
{
|
||||||
const Release::pointer release{ Release::find(context.dbSession, releaseId) };
|
const Release::pointer release{ Release::find(context.dbSession, releaseId) };
|
||||||
albumListNode.addArrayChild("album", createAlbumNode(context, release, context.user, id3));
|
albumListNode.addArrayChild("album", createAlbumNode(context, release, id3));
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
@@ -187,6 +187,8 @@ namespace lms::api::subsonic
|
|||||||
|
|
||||||
feedback::IFeedbackService& feedbackService{ *core::Service<feedback::IFeedbackService>::get() };
|
feedback::IFeedbackService& feedbackService{ *core::Service<feedback::IFeedbackService>::get() };
|
||||||
|
|
||||||
|
// We don't support starring directories
|
||||||
|
if (id3)
|
||||||
{
|
{
|
||||||
feedback::IFeedbackService::ArtistFindParameters artistFindParams;
|
feedback::IFeedbackService::ArtistFindParameters artistFindParams;
|
||||||
artistFindParams.setUser(context.user->getId());
|
artistFindParams.setUser(context.user->getId());
|
||||||
@@ -194,7 +196,7 @@ namespace lms::api::subsonic
|
|||||||
for (const ArtistId artistId : feedbackService.findStarredArtists(artistFindParams).results)
|
for (const ArtistId artistId : feedbackService.findStarredArtists(artistFindParams).results)
|
||||||
{
|
{
|
||||||
if (auto artist{ Artist::find(context.dbSession, artistId) })
|
if (auto artist{ Artist::find(context.dbSession, artistId) })
|
||||||
starredNode.addArrayChild("artist", createArtistNode(context, artist, context.user, id3));
|
starredNode.addArrayChild("artist", createArtistNode(context, artist));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,7 +207,7 @@ namespace lms::api::subsonic
|
|||||||
for (const ReleaseId releaseId : feedbackService.findStarredReleases(findParameters).results)
|
for (const ReleaseId releaseId : feedbackService.findStarredReleases(findParameters).results)
|
||||||
{
|
{
|
||||||
if (auto release{ Release::find(context.dbSession, releaseId) })
|
if (auto release{ Release::find(context.dbSession, releaseId) })
|
||||||
starredNode.addArrayChild("album", createAlbumNode(context, release, context.user, id3));
|
starredNode.addArrayChild("album", createAlbumNode(context, release, id3));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const TrackId trackId : feedbackService.findStarredTracks(findParameters).results)
|
for (const TrackId trackId : feedbackService.findStarredTracks(findParameters).results)
|
||||||
|
|||||||
@@ -24,11 +24,13 @@
|
|||||||
#include "core/Service.hpp"
|
#include "core/Service.hpp"
|
||||||
#include "database/Artist.hpp"
|
#include "database/Artist.hpp"
|
||||||
#include "database/Cluster.hpp"
|
#include "database/Cluster.hpp"
|
||||||
|
#include "database/Directory.hpp"
|
||||||
#include "database/MediaLibrary.hpp"
|
#include "database/MediaLibrary.hpp"
|
||||||
#include "database/Release.hpp"
|
#include "database/Release.hpp"
|
||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
#include "database/Track.hpp"
|
#include "database/Track.hpp"
|
||||||
#include "database/User.hpp"
|
#include "database/User.hpp"
|
||||||
|
#include "services/feedback/IFeedbackService.hpp"
|
||||||
#include "services/recommendation/IRecommendationService.hpp"
|
#include "services/recommendation/IRecommendationService.hpp"
|
||||||
#include "services/scrobbling/IScrobblingService.hpp"
|
#include "services/scrobbling/IScrobblingService.hpp"
|
||||||
|
|
||||||
@@ -48,123 +50,58 @@ namespace lms::api::subsonic
|
|||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
Response handleGetArtistInfoRequestCommon(RequestContext& context, bool id3)
|
std::vector<Directory::pointer> getRootDirectories(Session& session, MediaLibraryId libraryId)
|
||||||
{
|
{
|
||||||
// Mandatory params
|
std::vector<Directory::pointer> res;
|
||||||
ArtistId id{ getMandatoryParameterAs<ArtistId>(context.parameters, "id") };
|
|
||||||
|
|
||||||
// Optional params
|
|
||||||
std::size_t count{ getParameterAs<std::size_t>(context.parameters, "count").value_or(20) };
|
|
||||||
|
|
||||||
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
|
||||||
Response::Node& artistInfoNode{ response.createNode(id3 ? Response::Node::Key{ "artistInfo2" } : Response::Node::Key{ "artistInfo" }) };
|
|
||||||
|
|
||||||
|
if (libraryId.isValid())
|
||||||
{
|
{
|
||||||
auto transaction{ context.dbSession.createReadTransaction() };
|
const MediaLibrary::pointer library{ MediaLibrary::find(session, libraryId) };
|
||||||
|
if (!library)
|
||||||
|
throw BadParameterGenericError{ "id" };
|
||||||
|
|
||||||
const Artist::pointer artist{ Artist::find(context.dbSession, id) };
|
if (Directory::pointer rootDirectory{ Directory::find(session, library->getPath()) })
|
||||||
if (!artist)
|
res.push_back(rootDirectory);
|
||||||
throw RequestedDataNotFoundError{};
|
}
|
||||||
|
else
|
||||||
std::optional<core::UUID> artistMBID{ artist->getMBID() };
|
{
|
||||||
if (artistMBID)
|
res = Directory::findRootDirectories(session).results;
|
||||||
artistInfoNode.createChild("musicBrainzId").setValue(artistMBID->getAsString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auto similarArtistsId{ core::Service<recommendation::IRecommendationService>::get()->getSimilarArtists(id, { TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist }, count) };
|
return res;
|
||||||
|
|
||||||
{
|
|
||||||
auto transaction{ context.dbSession.createReadTransaction() };
|
|
||||||
|
|
||||||
for (const ArtistId similarArtistId : similarArtistsId)
|
|
||||||
{
|
|
||||||
const Artist::pointer similarArtist{ Artist::find(context.dbSession, similarArtistId) };
|
|
||||||
if (similarArtist)
|
|
||||||
artistInfoNode.addArrayChild("similarArtist", createArtistNode(context, similarArtist, context.user, id3));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Response handleGetArtistsRequestCommon(RequestContext& context, bool id3)
|
struct IndexComparator
|
||||||
{
|
{
|
||||||
// Optional params
|
constexpr bool operator()(char lhs, char rhs) const
|
||||||
const MediaLibraryId mediaLibrary{ getParameterAs<MediaLibraryId>(context.parameters, "musicFolderId").value_or(MediaLibraryId{}) };
|
|
||||||
|
|
||||||
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
|
||||||
|
|
||||||
Response::Node& artistsNode{ response.createNode(id3 ? "artists" : "indexes") };
|
|
||||||
artistsNode.setAttribute("ignoredArticles", "");
|
|
||||||
artistsNode.setAttribute("lastModified", reportedDummyDateULong); // TODO report last file write?
|
|
||||||
|
|
||||||
Artist::FindParameters parameters;
|
|
||||||
{
|
{
|
||||||
auto transaction{ context.dbSession.createReadTransaction() };
|
if (lhs == '#' && std::isalpha(rhs))
|
||||||
|
return false;
|
||||||
|
if (rhs == '#' && std::isalpha(lhs))
|
||||||
|
return true;
|
||||||
|
|
||||||
parameters.setSortMethod(ArtistSortMethod::SortName);
|
return lhs < rhs;
|
||||||
switch (context.user->getSubsonicArtistListMode())
|
|
||||||
{
|
|
||||||
case SubsonicArtistListMode::AllArtists:
|
|
||||||
break;
|
|
||||||
case SubsonicArtistListMode::ReleaseArtists:
|
|
||||||
parameters.setLinkType(TrackArtistLinkType::ReleaseArtist);
|
|
||||||
break;
|
|
||||||
case SubsonicArtistListMode::TrackArtists:
|
|
||||||
parameters.setLinkType(TrackArtistLinkType::Artist);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
parameters.setMediaLibrary(mediaLibrary);
|
};
|
||||||
|
|
||||||
// This endpoint does not scale: make sort lived transactions in order not to block the whole application
|
using IndexMap = std::map<char, std::vector<Directory::pointer>, IndexComparator>;
|
||||||
|
void getIndexedChildDirectories(RequestContext& context, const Directory::pointer& parentDirectory, IndexMap& res)
|
||||||
|
{
|
||||||
|
Directory::FindParameters params;
|
||||||
|
params.setParentDirectory(parentDirectory->getId());
|
||||||
|
|
||||||
// first pass: dispatch the artists by first letter
|
Directory::find(context.dbSession, params, [&](const Directory::pointer& directory) {
|
||||||
LMS_LOG(API_SUBSONIC, DEBUG, "GetArtists: fetching all artists...");
|
const std::string_view name{ directory->getName() };
|
||||||
std::map<char, std::vector<ArtistId>> artistsSortedByFirstChar;
|
assert(!name.empty());
|
||||||
std::size_t currentArtistOffset{ 0 };
|
|
||||||
constexpr std::size_t batchSize{ 100 };
|
|
||||||
bool hasMoreArtists{ true };
|
|
||||||
while (hasMoreArtists)
|
|
||||||
{
|
|
||||||
auto transaction{ context.dbSession.createReadTransaction() };
|
|
||||||
|
|
||||||
parameters.setRange(Range{ currentArtistOffset, batchSize });
|
char sortChar;
|
||||||
const auto artists{ Artist::find(context.dbSession, parameters) };
|
if (name.empty() || !std::isalpha(name[0]))
|
||||||
for (const Artist::pointer& artist : artists.results)
|
sortChar = '#';
|
||||||
{
|
else
|
||||||
std::string_view sortName{ artist->getSortName() };
|
sortChar = std::toupper(name[0]);
|
||||||
|
|
||||||
char sortChar;
|
res[sortChar].push_back(directory);
|
||||||
if (sortName.empty() || !std::isalpha(sortName[0]))
|
});
|
||||||
sortChar = '?';
|
|
||||||
else
|
|
||||||
sortChar = std::toupper(sortName[0]);
|
|
||||||
|
|
||||||
artistsSortedByFirstChar[sortChar].push_back(artist->getId());
|
|
||||||
}
|
|
||||||
|
|
||||||
hasMoreArtists = artists.moreResults;
|
|
||||||
currentArtistOffset += artists.results.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
// second pass: add each artist
|
|
||||||
LMS_LOG(API_SUBSONIC, DEBUG, "GetArtists: constructing response...");
|
|
||||||
for (const auto& [sortChar, artistIds] : artistsSortedByFirstChar)
|
|
||||||
{
|
|
||||||
Response::Node& indexNode{ artistsNode.createArrayChild("index") };
|
|
||||||
indexNode.setAttribute("name", std::string{ sortChar });
|
|
||||||
|
|
||||||
for (const ArtistId artistId : artistIds)
|
|
||||||
{
|
|
||||||
auto transaction{ context.dbSession.createReadTransaction() };
|
|
||||||
|
|
||||||
if (const Artist::pointer artist{ Artist::find(context.dbSession, artistId) })
|
|
||||||
indexNode.addArrayChild("artist", createArtistNode(context, artist, context.user, id3));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<TrackId> findSimilarSongs(RequestContext& context, ArtistId artistId, std::size_t count)
|
std::vector<TrackId> findSimilarSongs(RequestContext& context, ArtistId artistId, std::size_t count)
|
||||||
@@ -266,6 +203,21 @@ namespace lms::api::subsonic
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Release::pointer getReleaseFromDirectory(Session& session, DirectoryId directoryId)
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
|
Release::FindParameters params;
|
||||||
|
params.setDirectory(directoryId);
|
||||||
|
params.setRange(Range{ 0, 1 }); // only support 1 directory <-> 1 release
|
||||||
|
|
||||||
|
Release::pointer res;
|
||||||
|
Release::find(session, params, [&](const Release::pointer& release) {
|
||||||
|
res = release;
|
||||||
|
});
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Response handleGetMusicFoldersRequest(RequestContext& context)
|
Response handleGetMusicFoldersRequest(RequestContext& context)
|
||||||
@@ -286,64 +238,112 @@ namespace lms::api::subsonic
|
|||||||
|
|
||||||
Response handleGetIndexesRequest(RequestContext& context)
|
Response handleGetIndexesRequest(RequestContext& context)
|
||||||
{
|
{
|
||||||
return handleGetArtistsRequestCommon(context, false /* no id3 */);
|
// Optional params
|
||||||
|
const MediaLibraryId mediaLibrary{ getParameterAs<MediaLibraryId>(context.parameters, "musicFolderId").value_or(MediaLibraryId{}) };
|
||||||
|
|
||||||
|
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||||
|
Response::Node& indexesNode{ response.createNode("indexes") };
|
||||||
|
indexesNode.setAttribute("ignoredArticles", "");
|
||||||
|
indexesNode.setAttribute("lastModified", reportedDummyDateULong); // TODO report last file write?
|
||||||
|
|
||||||
|
auto transaction{ context.dbSession.createReadTransaction() };
|
||||||
|
|
||||||
|
const std::vector<Directory::pointer> rootDirectories{ getRootDirectories(context.dbSession, mediaLibrary) };
|
||||||
|
|
||||||
|
IndexMap indexedDirectories;
|
||||||
|
for (const Directory::pointer& rootdirectory : rootDirectories)
|
||||||
|
{
|
||||||
|
Track::FindParameters params;
|
||||||
|
params.setDirectory(rootdirectory->getId());
|
||||||
|
|
||||||
|
Track::find(context.dbSession, params, [&](const Track::pointer& track) {
|
||||||
|
indexesNode.addArrayChild("child", createSongNode(context, track, context.user));
|
||||||
|
});
|
||||||
|
|
||||||
|
getIndexedChildDirectories(context, rootdirectory, indexedDirectories);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& [index, directories] : indexedDirectories)
|
||||||
|
{
|
||||||
|
Response::Node& indexNode{ indexesNode.createArrayChild("index") };
|
||||||
|
indexNode.setAttribute("name", std::string{ index });
|
||||||
|
|
||||||
|
for (const Directory::pointer& directory : directories)
|
||||||
|
{
|
||||||
|
// Legacy behavior: all sub directories are considered as artists (even if this is just containing an album, or just an intermediary directory)
|
||||||
|
|
||||||
|
Response::Node childNode;
|
||||||
|
childNode.setAttribute("id", idToString(directory->getId()));
|
||||||
|
childNode.setAttribute("name", directory->getName());
|
||||||
|
|
||||||
|
indexNode.addArrayChild("artist", std::move(childNode));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
Response handleGetMusicDirectoryRequest(RequestContext& context)
|
Response handleGetMusicDirectoryRequest(RequestContext& context)
|
||||||
{
|
{
|
||||||
// Mandatory params
|
// Mandatory params
|
||||||
const auto artistId{ getParameterAs<ArtistId>(context.parameters, "id") };
|
const auto directoryId{ getMandatoryParameterAs<DirectoryId>(context.parameters, "id") };
|
||||||
const auto releaseId{ getParameterAs<ReleaseId>(context.parameters, "id") };
|
|
||||||
const auto root{ getParameterAs<RootId>(context.parameters, "id") };
|
|
||||||
|
|
||||||
if (!root && !artistId && !releaseId)
|
|
||||||
throw BadParameterGenericError{ "id" };
|
|
||||||
|
|
||||||
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||||
Response::Node& directoryNode{ response.createNode("directory") };
|
Response::Node& directoryNode{ response.createNode("directory") };
|
||||||
|
|
||||||
auto transaction{ context.dbSession.createReadTransaction() };
|
auto transaction{ context.dbSession.createReadTransaction() };
|
||||||
|
|
||||||
if (root)
|
const Directory::pointer directory{ Directory::find(context.dbSession, directoryId) };
|
||||||
{
|
if (!directory)
|
||||||
directoryNode.setAttribute("id", idToString(RootId{}));
|
throw RequestedDataNotFoundError{};
|
||||||
directoryNode.setAttribute("name", "Music");
|
|
||||||
|
|
||||||
// TODO: this does not scale when a lot of artists are present
|
if (const Release::pointer release{ getReleaseFromDirectory(context.dbSession, directoryId) })
|
||||||
Artist::find(context.dbSession, Artist::FindParameters{}.setSortMethod(ArtistSortMethod::SortName), [&](const Artist::pointer& artist) {
|
{
|
||||||
directoryNode.addArrayChild("child", createArtistNode(context, artist, context.user, false /* no id3 */));
|
directoryNode.setAttribute("playCount", core::Service<scrobbling::IScrobblingService>::get()->getCount(context.user->getId(), release->getId()));
|
||||||
|
if (const Wt::WDateTime dateTime{ core::Service<feedback::IFeedbackService>::get()->getStarredDateTime(context.user->getId(), release->getId()) }; dateTime.isValid())
|
||||||
|
directoryNode.setAttribute("starred", core::stringUtils::toISO8601String(dateTime));
|
||||||
|
}
|
||||||
|
|
||||||
|
directoryNode.setAttribute("id", idToString(directory->getId()));
|
||||||
|
directoryNode.setAttribute("name", directory->getName());
|
||||||
|
// Original Subsonic does not report parent if this the parent directory is the root directory
|
||||||
|
if (const Directory::pointer parentDirectory{ directory->getParentDirectory() })
|
||||||
|
directoryNode.setAttribute("parent", idToString(parentDirectory->getId()));
|
||||||
|
|
||||||
|
// list all sub directories
|
||||||
|
{
|
||||||
|
Directory::FindParameters params;
|
||||||
|
params.setParentDirectory(directory->getId());
|
||||||
|
|
||||||
|
Directory::find(context.dbSession, params, [&](const Directory::pointer& subDirectory) {
|
||||||
|
const Release::pointer release{ getReleaseFromDirectory(context.dbSession, subDirectory->getId()) };
|
||||||
|
|
||||||
|
if (release)
|
||||||
|
{
|
||||||
|
directoryNode.addArrayChild("child", createAlbumNode(context, release, false, subDirectory));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Response::Node childNode;
|
||||||
|
childNode.setAttribute("id", idToString(subDirectory->getId()));
|
||||||
|
childNode.setAttribute("title", subDirectory->getName());
|
||||||
|
childNode.setAttribute("isDir", true);
|
||||||
|
childNode.setAttribute("parent", idToString(directory->getId()));
|
||||||
|
|
||||||
|
directoryNode.addArrayChild("child", std::move(childNode));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else if (artistId)
|
|
||||||
|
// list all tracks
|
||||||
{
|
{
|
||||||
directoryNode.setAttribute("id", idToString(*artistId));
|
Track::FindParameters params;
|
||||||
|
params.setDirectory(directory->getId());
|
||||||
|
|
||||||
auto artist{ Artist::find(context.dbSession, *artistId) };
|
Track::find(context.dbSession, params, [&](const Track::pointer& track) {
|
||||||
if (!artist)
|
|
||||||
throw RequestedDataNotFoundError{};
|
|
||||||
|
|
||||||
directoryNode.setAttribute("name", utils::makeNameFilesystemCompatible(artist->getName()));
|
|
||||||
|
|
||||||
Release::find(context.dbSession, Release::FindParameters{}.setArtist(*artistId), [&](const Release::pointer& release) {
|
|
||||||
directoryNode.addArrayChild("child", createAlbumNode(context, release, context.user, false /* no id3 */));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else if (releaseId)
|
|
||||||
{
|
|
||||||
directoryNode.setAttribute("id", idToString(*releaseId));
|
|
||||||
|
|
||||||
auto release{ Release::find(context.dbSession, *releaseId) };
|
|
||||||
if (!release)
|
|
||||||
throw RequestedDataNotFoundError{};
|
|
||||||
|
|
||||||
directoryNode.setAttribute("name", utils::makeNameFilesystemCompatible(release->getName()));
|
|
||||||
|
|
||||||
Track::find(context.dbSession, Track::FindParameters{}.setRelease(*releaseId).setSortMethod(TrackSortMethod::Release), [&](const Track::pointer& track) {
|
|
||||||
directoryNode.addArrayChild("child", createSongNode(context, track, context.user));
|
directoryNode.addArrayChild("child", createSongNode(context, track, context.user));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else
|
|
||||||
throw BadParameterGenericError{ "id" };
|
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
@@ -362,7 +362,7 @@ namespace lms::api::subsonic
|
|||||||
const auto clusters{ clusterType->getClusters() };
|
const auto clusters{ clusterType->getClusters() };
|
||||||
|
|
||||||
for (const Cluster::pointer& cluster : clusters)
|
for (const Cluster::pointer& cluster : clusters)
|
||||||
genresNode.addArrayChild("genre", createGenreNode(cluster));
|
genresNode.addArrayChild("genre", createGenreNode(context, cluster));
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
@@ -370,7 +370,82 @@ namespace lms::api::subsonic
|
|||||||
|
|
||||||
Response handleGetArtistsRequest(RequestContext& context)
|
Response handleGetArtistsRequest(RequestContext& context)
|
||||||
{
|
{
|
||||||
return handleGetArtistsRequestCommon(context, true /* id3 */);
|
// Optional params
|
||||||
|
const MediaLibraryId mediaLibrary{ getParameterAs<MediaLibraryId>(context.parameters, "musicFolderId").value_or(MediaLibraryId{}) };
|
||||||
|
|
||||||
|
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||||
|
|
||||||
|
Response::Node& artistsNode{ response.createNode("artists") };
|
||||||
|
artistsNode.setAttribute("ignoredArticles", "");
|
||||||
|
artistsNode.setAttribute("lastModified", reportedDummyDateULong); // TODO report last file write?
|
||||||
|
|
||||||
|
Artist::FindParameters parameters;
|
||||||
|
{
|
||||||
|
auto transaction{ context.dbSession.createReadTransaction() };
|
||||||
|
|
||||||
|
parameters.setSortMethod(ArtistSortMethod::SortName);
|
||||||
|
switch (context.user->getSubsonicArtistListMode())
|
||||||
|
{
|
||||||
|
case SubsonicArtistListMode::AllArtists:
|
||||||
|
break;
|
||||||
|
case SubsonicArtistListMode::ReleaseArtists:
|
||||||
|
parameters.setLinkType(TrackArtistLinkType::ReleaseArtist);
|
||||||
|
break;
|
||||||
|
case SubsonicArtistListMode::TrackArtists:
|
||||||
|
parameters.setLinkType(TrackArtistLinkType::Artist);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parameters.setMediaLibrary(mediaLibrary);
|
||||||
|
|
||||||
|
// This endpoint does not scale: make sort lived transactions in order not to block the whole application
|
||||||
|
|
||||||
|
// first pass: dispatch the artists by first letter
|
||||||
|
LMS_LOG(API_SUBSONIC, DEBUG, "GetArtists: fetching all artists...");
|
||||||
|
std::map<char, std::vector<ArtistId>> artistsSortedByFirstChar;
|
||||||
|
std::size_t currentArtistOffset{ 0 };
|
||||||
|
constexpr std::size_t batchSize{ 100 };
|
||||||
|
bool hasMoreArtists{ true };
|
||||||
|
while (hasMoreArtists)
|
||||||
|
{
|
||||||
|
auto transaction{ context.dbSession.createReadTransaction() };
|
||||||
|
|
||||||
|
parameters.setRange(Range{ currentArtistOffset, batchSize });
|
||||||
|
const auto artists{ Artist::find(context.dbSession, parameters) };
|
||||||
|
for (const Artist::pointer& artist : artists.results)
|
||||||
|
{
|
||||||
|
std::string_view sortName{ artist->getSortName() };
|
||||||
|
|
||||||
|
char sortChar;
|
||||||
|
if (sortName.empty() || !std::isalpha(sortName[0]))
|
||||||
|
sortChar = '#';
|
||||||
|
else
|
||||||
|
sortChar = std::toupper(sortName[0]);
|
||||||
|
|
||||||
|
artistsSortedByFirstChar[sortChar].push_back(artist->getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
hasMoreArtists = artists.moreResults;
|
||||||
|
currentArtistOffset += artists.results.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
// second pass: add each artist
|
||||||
|
LMS_LOG(API_SUBSONIC, DEBUG, "GetArtists: constructing response...");
|
||||||
|
for (const auto& [sortChar, artistIds] : artistsSortedByFirstChar)
|
||||||
|
{
|
||||||
|
Response::Node& indexNode{ artistsNode.createArrayChild("index") };
|
||||||
|
indexNode.setAttribute("name", std::string{ sortChar });
|
||||||
|
|
||||||
|
for (const ArtistId artistId : artistIds)
|
||||||
|
{
|
||||||
|
auto transaction{ context.dbSession.createReadTransaction() };
|
||||||
|
|
||||||
|
if (const Artist::pointer artist{ Artist::find(context.dbSession, artistId) })
|
||||||
|
indexNode.addArrayChild("artist", createArtistNode(context, artist));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
Response handleGetArtistRequest(RequestContext& context)
|
Response handleGetArtistRequest(RequestContext& context)
|
||||||
@@ -385,11 +460,11 @@ namespace lms::api::subsonic
|
|||||||
throw RequestedDataNotFoundError{};
|
throw RequestedDataNotFoundError{};
|
||||||
|
|
||||||
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||||
Response::Node artistNode{ createArtistNode(context, artist, context.user, true /* id3 */) };
|
Response::Node artistNode{ createArtistNode(context, artist) };
|
||||||
|
|
||||||
const auto releases{ Release::find(context.dbSession, Release::FindParameters{}.setArtist(artist->getId())) };
|
const auto releases{ Release::find(context.dbSession, Release::FindParameters{}.setArtist(artist->getId())) };
|
||||||
for (const Release::pointer& release : releases.results)
|
for (const Release::pointer& release : releases.results)
|
||||||
artistNode.addArrayChild("album", createAlbumNode(context, release, context.user, true /* id3 */));
|
artistNode.addArrayChild("album", createAlbumNode(context, release, true /* id3 */));
|
||||||
|
|
||||||
response.addNode("artist", std::move(artistNode));
|
response.addNode("artist", std::move(artistNode));
|
||||||
|
|
||||||
@@ -408,11 +483,11 @@ namespace lms::api::subsonic
|
|||||||
throw RequestedDataNotFoundError{};
|
throw RequestedDataNotFoundError{};
|
||||||
|
|
||||||
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||||
Response::Node albumNode{ createAlbumNode(context, release, context.user, true /* id3 */) };
|
Response::Node albumNode{ createAlbumNode(context, release, true /* id3 */) };
|
||||||
|
|
||||||
const auto tracks{ Track::find(context.dbSession, Track::FindParameters{}.setRelease(id).setSortMethod(TrackSortMethod::Release)) };
|
const auto tracks{ Track::find(context.dbSession, Track::FindParameters{}.setRelease(id).setSortMethod(TrackSortMethod::Release)) };
|
||||||
for (const Track::pointer& track : tracks.results)
|
for (const Track::pointer& track : tracks.results)
|
||||||
albumNode.addArrayChild("song", createSongNode(context, track, context.user));
|
albumNode.addArrayChild("song", createSongNode(context, track, true /* id3 */));
|
||||||
|
|
||||||
response.addNode("album", std::move(albumNode));
|
response.addNode("album", std::move(albumNode));
|
||||||
|
|
||||||
@@ -436,14 +511,53 @@ namespace lms::api::subsonic
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
Response handleGetArtistInfoRequest(RequestContext& context)
|
|
||||||
{
|
|
||||||
return handleGetArtistInfoRequestCommon(context, false /* no id3 */);
|
|
||||||
}
|
|
||||||
|
|
||||||
Response handleGetArtistInfo2Request(RequestContext& context)
|
Response handleGetArtistInfo2Request(RequestContext& context)
|
||||||
{
|
{
|
||||||
return handleGetArtistInfoRequestCommon(context, true /* id3 */);
|
// Mandatory params
|
||||||
|
ArtistId id{ getMandatoryParameterAs<ArtistId>(context.parameters, "id") };
|
||||||
|
|
||||||
|
// Optional params
|
||||||
|
std::size_t count{ getParameterAs<std::size_t>(context.parameters, "count").value_or(20) };
|
||||||
|
|
||||||
|
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||||
|
Response::Node& artistInfoNode{ response.createNode(Response::Node::Key{ "artistInfo2" }) };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ context.dbSession.createReadTransaction() };
|
||||||
|
|
||||||
|
const Artist::pointer artist{ Artist::find(context.dbSession, id) };
|
||||||
|
if (!artist)
|
||||||
|
throw RequestedDataNotFoundError{};
|
||||||
|
|
||||||
|
std::optional<core::UUID> artistMBID{ artist->getMBID() };
|
||||||
|
if (artistMBID)
|
||||||
|
{
|
||||||
|
switch (context.responseFormat)
|
||||||
|
{
|
||||||
|
case ResponseFormat::json:
|
||||||
|
artistInfoNode.setAttribute("musicBrainzId", artistMBID->getAsString());
|
||||||
|
break;
|
||||||
|
case ResponseFormat::xml:
|
||||||
|
artistInfoNode.createChild("musicBrainzId").setValue(artistMBID->getAsString());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto similarArtistsId{ core::Service<recommendation::IRecommendationService>::get()->getSimilarArtists(id, { TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist }, count) };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ context.dbSession.createReadTransaction() };
|
||||||
|
|
||||||
|
for (const ArtistId similarArtistId : similarArtistsId)
|
||||||
|
{
|
||||||
|
const Artist::pointer similarArtist{ Artist::find(context.dbSession, similarArtistId) };
|
||||||
|
if (similarArtist)
|
||||||
|
artistInfoNode.addArrayChild("similarArtist", createArtistNode(context, similarArtist));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
Response handleGetSimilarSongsRequest(RequestContext& context)
|
Response handleGetSimilarSongsRequest(RequestContext& context)
|
||||||
|
|||||||
@@ -23,7 +23,9 @@
|
|||||||
|
|
||||||
#include "core/Service.hpp"
|
#include "core/Service.hpp"
|
||||||
#include "database/ArtistId.hpp"
|
#include "database/ArtistId.hpp"
|
||||||
|
#include "database/Release.hpp"
|
||||||
#include "database/ReleaseId.hpp"
|
#include "database/ReleaseId.hpp"
|
||||||
|
#include "database/Session.hpp"
|
||||||
#include "database/TrackId.hpp"
|
#include "database/TrackId.hpp"
|
||||||
#include "database/User.hpp"
|
#include "database/User.hpp"
|
||||||
#include "services/feedback/IFeedbackService.hpp"
|
#include "services/feedback/IFeedbackService.hpp"
|
||||||
@@ -43,25 +45,50 @@ namespace lms::api::subsonic
|
|||||||
std::vector<ArtistId> artistIds;
|
std::vector<ArtistId> artistIds;
|
||||||
std::vector<ReleaseId> releaseIds;
|
std::vector<ReleaseId> releaseIds;
|
||||||
std::vector<TrackId> trackIds;
|
std::vector<TrackId> trackIds;
|
||||||
|
std::vector<DirectoryId> directoryIds;
|
||||||
};
|
};
|
||||||
|
|
||||||
StarParameters getStarParameters(const Wt::Http::ParameterMap& parameters)
|
StarParameters getStarParameters(const Wt::Http::ParameterMap& parameters)
|
||||||
{
|
{
|
||||||
StarParameters res;
|
StarParameters res;
|
||||||
|
|
||||||
// TODO handle parameters for legacy file browsing
|
// id could be either a trackId or a directory id
|
||||||
|
res.directoryIds = getMultiParametersAs<DirectoryId>(parameters, "id");
|
||||||
res.trackIds = getMultiParametersAs<TrackId>(parameters, "id");
|
res.trackIds = getMultiParametersAs<TrackId>(parameters, "id");
|
||||||
res.artistIds = getMultiParametersAs<ArtistId>(parameters, "artistId");
|
res.artistIds = getMultiParametersAs<ArtistId>(parameters, "artistId");
|
||||||
res.releaseIds = getMultiParametersAs<ReleaseId>(parameters, "albumId");
|
res.releaseIds = getMultiParametersAs<ReleaseId>(parameters, "albumId");
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ReleaseId getReleaseFromDirectory(Session& session, DirectoryId directory)
|
||||||
|
{
|
||||||
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
|
Release::FindParameters params;
|
||||||
|
params.setDirectory(directory);
|
||||||
|
params.setRange(Range{ 0, 1 }); // consider one directory <-> one release
|
||||||
|
|
||||||
|
Release::pointer res;
|
||||||
|
Release::find(session, params, [&](const Release::pointer& release) {
|
||||||
|
res = release;
|
||||||
|
});
|
||||||
|
|
||||||
|
return res ? res->getId() : ReleaseId{};
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Response handleStarRequest(RequestContext& context)
|
Response handleStarRequest(RequestContext& context)
|
||||||
{
|
{
|
||||||
StarParameters params{ getStarParameters(context.parameters) };
|
StarParameters params{ getStarParameters(context.parameters) };
|
||||||
|
|
||||||
|
for (const DirectoryId id : params.directoryIds)
|
||||||
|
{
|
||||||
|
if (const ReleaseId releaseId{ getReleaseFromDirectory(context.dbSession, id) }; releaseId.isValid())
|
||||||
|
core::Service<feedback::IFeedbackService>::get()->star(context.user->getId(), releaseId);
|
||||||
|
}
|
||||||
|
|
||||||
for (const ArtistId id : params.artistIds)
|
for (const ArtistId id : params.artistIds)
|
||||||
core::Service<feedback::IFeedbackService>::get()->star(context.user->getId(), id);
|
core::Service<feedback::IFeedbackService>::get()->star(context.user->getId(), id);
|
||||||
|
|
||||||
@@ -78,6 +105,12 @@ namespace lms::api::subsonic
|
|||||||
{
|
{
|
||||||
StarParameters params{ getStarParameters(context.parameters) };
|
StarParameters params{ getStarParameters(context.parameters) };
|
||||||
|
|
||||||
|
for (const DirectoryId id : params.directoryIds)
|
||||||
|
{
|
||||||
|
if (const ReleaseId releaseId{ getReleaseFromDirectory(context.dbSession, id) }; releaseId.isValid())
|
||||||
|
core::Service<feedback::IFeedbackService>::get()->unstar(context.user->getId(), releaseId);
|
||||||
|
}
|
||||||
|
|
||||||
for (const ArtistId id : params.artistIds)
|
for (const ArtistId id : params.artistIds)
|
||||||
core::Service<feedback::IFeedbackService>::get()->unstar(context.user->getId(), id);
|
core::Service<feedback::IFeedbackService>::get()->unstar(context.user->getId(), id);
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
|
|
||||||
#include "core/Random.hpp"
|
#include "core/Random.hpp"
|
||||||
#include "database/Artist.hpp"
|
#include "database/Artist.hpp"
|
||||||
|
#include "database/Directory.hpp"
|
||||||
#include "database/Release.hpp"
|
#include "database/Release.hpp"
|
||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
#include "database/Track.hpp"
|
#include "database/Track.hpp"
|
||||||
@@ -113,7 +114,36 @@ namespace lms::api::subsonic
|
|||||||
_ongoingScans[scanInfo] = { now, lastRetrievedId };
|
_ongoingScans[scanInfo] = { now, lastRetrievedId };
|
||||||
}
|
}
|
||||||
|
|
||||||
void findRequestedArtists(RequestContext& context, bool id3, const std::vector<std::string_view>& keywords, MediaLibraryId mediaLibrary, const User::pointer& user, Response::Node& searchResultNode)
|
void findRequestedArtistDirectories(RequestContext& context, const std::vector<std::string_view>& keywords, MediaLibraryId mediaLibrary, Response::Node& searchResultNode)
|
||||||
|
{
|
||||||
|
// For now, no need to optimize all this
|
||||||
|
// Find all the directories that match the name and that do not contain any track (considered by the legacy API as artists)
|
||||||
|
const std::size_t artistCount{ getParameterAs<std::size_t>(context.parameters, "artistCount").value_or(20) };
|
||||||
|
if (artistCount == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (artistCount > defaultMaxCountSize)
|
||||||
|
throw ParameterValueTooHighGenericError{ "artistCount", defaultMaxCountSize };
|
||||||
|
|
||||||
|
const std::size_t artistOffset{ getParameterAs<std::size_t>(context.parameters, "artistOffset").value_or(0) };
|
||||||
|
|
||||||
|
Directory::FindParameters params;
|
||||||
|
params.setKeywords(keywords);
|
||||||
|
params.setRange(Range{ artistOffset, artistCount });
|
||||||
|
params.setWithNoTrack(true);
|
||||||
|
params.setMediaLibrary(mediaLibrary);
|
||||||
|
|
||||||
|
Directory::find(context.dbSession, params, [&](const Directory::pointer& directory) {
|
||||||
|
Response::Node childNode;
|
||||||
|
childNode.setAttribute("id", idToString(directory->getId()));
|
||||||
|
childNode.setAttribute("name", directory->getName());
|
||||||
|
childNode.setAttribute("isDir", true);
|
||||||
|
|
||||||
|
searchResultNode.addArrayChild("artist", std::move(childNode));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void findRequestedArtists(RequestContext& context, const std::vector<std::string_view>& keywords, MediaLibraryId mediaLibrary, Response::Node& searchResultNode)
|
||||||
{
|
{
|
||||||
static ScanTracker<ArtistId> currentScansInProgress;
|
static ScanTracker<ArtistId> currentScansInProgress;
|
||||||
|
|
||||||
@@ -135,7 +165,7 @@ namespace lms::api::subsonic
|
|||||||
params.setSortMethod(ArtistSortMethod::Id); // must be consistent with both methods
|
params.setSortMethod(ArtistSortMethod::Id); // must be consistent with both methods
|
||||||
|
|
||||||
Artist::find(context.dbSession, params, [&](const Artist::pointer& artist) {
|
Artist::find(context.dbSession, params, [&](const Artist::pointer& artist) {
|
||||||
searchResultNode.addArrayChild("artist", createArtistNode(context, artist, user, id3));
|
searchResultNode.addArrayChild("artist", createArtistNode(context, artist));
|
||||||
lastRetrievedId = artist->getId();
|
lastRetrievedId = artist->getId();
|
||||||
});
|
});
|
||||||
} };
|
} };
|
||||||
@@ -158,7 +188,7 @@ namespace lms::api::subsonic
|
|||||||
{
|
{
|
||||||
Artist::find(
|
Artist::find(
|
||||||
context.dbSession, cachedLastRetrievedId, artistCount, [&](const Artist::pointer& artist) {
|
context.dbSession, cachedLastRetrievedId, artistCount, [&](const Artist::pointer& artist) {
|
||||||
searchResultNode.addArrayChild("artist", createArtistNode(context, artist, user, id3));
|
searchResultNode.addArrayChild("artist", createArtistNode(context, artist));
|
||||||
},
|
},
|
||||||
mediaLibrary);
|
mediaLibrary);
|
||||||
lastRetrievedId = cachedLastRetrievedId;
|
lastRetrievedId = cachedLastRetrievedId;
|
||||||
@@ -176,7 +206,7 @@ namespace lms::api::subsonic
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void findRequestedAlbums(RequestContext& context, bool id3, const std::vector<std::string_view>& keywords, MediaLibraryId mediaLibrary, const User::pointer& user, Response::Node& searchResultNode)
|
void findRequestedAlbums(RequestContext& context, bool id3, const std::vector<std::string_view>& keywords, MediaLibraryId mediaLibrary, Response::Node& searchResultNode)
|
||||||
{
|
{
|
||||||
static ScanTracker<ReleaseId> currentScansInProgress;
|
static ScanTracker<ReleaseId> currentScansInProgress;
|
||||||
|
|
||||||
@@ -199,7 +229,7 @@ namespace lms::api::subsonic
|
|||||||
params.setSortMethod(ReleaseSortMethod::Id); // must be consistent with both methods
|
params.setSortMethod(ReleaseSortMethod::Id); // must be consistent with both methods
|
||||||
|
|
||||||
Release::find(context.dbSession, params, [&](const Release::pointer& release) {
|
Release::find(context.dbSession, params, [&](const Release::pointer& release) {
|
||||||
searchResultNode.addArrayChild("album", createAlbumNode(context, release, user, id3));
|
searchResultNode.addArrayChild("album", createAlbumNode(context, release, id3));
|
||||||
lastRetrievedId = release->getId();
|
lastRetrievedId = release->getId();
|
||||||
});
|
});
|
||||||
} };
|
} };
|
||||||
@@ -222,7 +252,7 @@ namespace lms::api::subsonic
|
|||||||
{
|
{
|
||||||
Release::find(
|
Release::find(
|
||||||
context.dbSession, cachedLastRetrievedId, albumCount, [&](const Release::pointer& release) {
|
context.dbSession, cachedLastRetrievedId, albumCount, [&](const Release::pointer& release) {
|
||||||
searchResultNode.addArrayChild("album", createAlbumNode(context, release, user, id3));
|
searchResultNode.addArrayChild("album", createAlbumNode(context, release, id3));
|
||||||
},
|
},
|
||||||
mediaLibrary);
|
mediaLibrary);
|
||||||
lastRetrievedId = cachedLastRetrievedId;
|
lastRetrievedId = cachedLastRetrievedId;
|
||||||
@@ -240,7 +270,7 @@ namespace lms::api::subsonic
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void findRequestedTracks(RequestContext& context, const std::vector<std::string_view>& keywords, MediaLibraryId mediaLibrary, const User::pointer& user, Response::Node& searchResultNode)
|
void findRequestedTracks(RequestContext& context, bool id3, const std::vector<std::string_view>& keywords, MediaLibraryId mediaLibrary, Response::Node& searchResultNode)
|
||||||
{
|
{
|
||||||
static ScanTracker<TrackId> currentScansInProgress;
|
static ScanTracker<TrackId> currentScansInProgress;
|
||||||
|
|
||||||
@@ -263,7 +293,7 @@ namespace lms::api::subsonic
|
|||||||
params.setSortMethod(TrackSortMethod::Id); // must be consistent with both methods
|
params.setSortMethod(TrackSortMethod::Id); // must be consistent with both methods
|
||||||
|
|
||||||
Track::find(context.dbSession, params, [&](const Track::pointer& track) {
|
Track::find(context.dbSession, params, [&](const Track::pointer& track) {
|
||||||
searchResultNode.addArrayChild("song", createSongNode(context, track, user));
|
searchResultNode.addArrayChild("song", createSongNode(context, track, id3));
|
||||||
lastRetrievedId = track->getId();
|
lastRetrievedId = track->getId();
|
||||||
});
|
});
|
||||||
} };
|
} };
|
||||||
@@ -286,7 +316,7 @@ namespace lms::api::subsonic
|
|||||||
{
|
{
|
||||||
Track::find(
|
Track::find(
|
||||||
context.dbSession, cachedLastRetrievedId, songCount, [&](const Track::pointer& track) {
|
context.dbSession, cachedLastRetrievedId, songCount, [&](const Track::pointer& track) {
|
||||||
searchResultNode.addArrayChild("song", createSongNode(context, track, user));
|
searchResultNode.addArrayChild("song", createSongNode(context, track, id3));
|
||||||
},
|
},
|
||||||
mediaLibrary);
|
mediaLibrary);
|
||||||
lastRetrievedId = cachedLastRetrievedId;
|
lastRetrievedId = cachedLastRetrievedId;
|
||||||
@@ -303,10 +333,7 @@ namespace lms::api::subsonic
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} // namespace
|
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
Response handleSearchRequestCommon(RequestContext& context, bool id3)
|
Response handleSearchRequestCommon(RequestContext& context, bool id3)
|
||||||
{
|
{
|
||||||
// Mandatory params
|
// Mandatory params
|
||||||
@@ -329,9 +356,12 @@ namespace lms::api::subsonic
|
|||||||
|
|
||||||
auto transaction{ context.dbSession.createReadTransaction() };
|
auto transaction{ context.dbSession.createReadTransaction() };
|
||||||
|
|
||||||
findRequestedArtists(context, id3, keywords, mediaLibrary, context.user, searchResultNode);
|
if (id3)
|
||||||
findRequestedAlbums(context, id3, keywords, mediaLibrary, context.user, searchResultNode);
|
findRequestedArtists(context, keywords, mediaLibrary, searchResultNode);
|
||||||
findRequestedTracks(context, keywords, mediaLibrary, context.user, searchResultNode);
|
else
|
||||||
|
findRequestedArtistDirectories(context, keywords, mediaLibrary, searchResultNode);
|
||||||
|
findRequestedAlbums(context, id3, keywords, mediaLibrary, searchResultNode);
|
||||||
|
findRequestedTracks(context, id3, keywords, mediaLibrary, searchResultNode);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ namespace lms::api::subsonic
|
|||||||
throw RequestedDataNotFoundError{};
|
throw RequestedDataNotFoundError{};
|
||||||
|
|
||||||
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||||
response.addNode("user", createUserNode(user));
|
response.addNode("user", createUserNode(context, user));
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
@@ -47,7 +47,7 @@ namespace lms::api::subsonic
|
|||||||
|
|
||||||
auto transaction{ context.dbSession.createReadTransaction() };
|
auto transaction{ context.dbSession.createReadTransaction() };
|
||||||
User::find(context.dbSession, User::FindParameters{}, [&](const User::pointer& user) {
|
User::find(context.dbSession, User::FindParameters{}, [&](const User::pointer& user) {
|
||||||
usersNode.addArrayChild("user", createUserNode(user));
|
usersNode.addArrayChild("user", createUserNode(context, user));
|
||||||
});
|
});
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
|
|||||||
@@ -24,11 +24,13 @@
|
|||||||
#include "core/String.hpp"
|
#include "core/String.hpp"
|
||||||
#include "database/Artist.hpp"
|
#include "database/Artist.hpp"
|
||||||
#include "database/Cluster.hpp"
|
#include "database/Cluster.hpp"
|
||||||
|
#include "database/Directory.hpp"
|
||||||
#include "database/Release.hpp"
|
#include "database/Release.hpp"
|
||||||
#include "database/User.hpp"
|
#include "database/User.hpp"
|
||||||
#include "services/feedback/IFeedbackService.hpp"
|
#include "services/feedback/IFeedbackService.hpp"
|
||||||
#include "services/scrobbling/IScrobblingService.hpp"
|
#include "services/scrobbling/IScrobblingService.hpp"
|
||||||
|
|
||||||
|
#include "RequestContext.hpp"
|
||||||
#include "SubsonicId.hpp"
|
#include "SubsonicId.hpp"
|
||||||
#include "responses/Artist.hpp"
|
#include "responses/Artist.hpp"
|
||||||
#include "responses/DiscTitle.hpp"
|
#include "responses/DiscTitle.hpp"
|
||||||
@@ -39,7 +41,7 @@ namespace lms::api::subsonic
|
|||||||
{
|
{
|
||||||
using namespace db;
|
using namespace db;
|
||||||
|
|
||||||
Response::Node createAlbumNode(RequestContext& context, const Release::pointer& release, const User::pointer& user, bool id3)
|
Response::Node createAlbumNode(RequestContext& context, const Release::pointer& release, bool id3, const Directory::pointer& directory)
|
||||||
{
|
{
|
||||||
LMS_SCOPED_TRACE_DETAILED("Subsonic", "CreateAlbum");
|
LMS_SCOPED_TRACE_DETAILED("Subsonic", "CreateAlbum");
|
||||||
|
|
||||||
@@ -47,21 +49,38 @@ namespace lms::api::subsonic
|
|||||||
|
|
||||||
if (id3)
|
if (id3)
|
||||||
{
|
{
|
||||||
|
albumNode.setAttribute("id", idToString(release->getId()));
|
||||||
albumNode.setAttribute("name", release->getName());
|
albumNode.setAttribute("name", release->getName());
|
||||||
albumNode.setAttribute("songCount", release->getTrackCount());
|
albumNode.setAttribute("songCount", release->getTrackCount());
|
||||||
albumNode.setAttribute(
|
albumNode.setAttribute("duration", std::chrono::duration_cast<std::chrono::seconds>(release->getDuration()).count());
|
||||||
"duration", std::chrono::duration_cast<std::chrono::seconds>(
|
|
||||||
release->getDuration())
|
|
||||||
.count());
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
albumNode.setAttribute("title", release->getName());
|
Directory::pointer directoryToReport{ directory };
|
||||||
|
|
||||||
|
if (!directoryToReport)
|
||||||
|
{
|
||||||
|
Directory::FindParameters params;
|
||||||
|
params.setRelease(release->getId());
|
||||||
|
params.setRange(Range{ 0, 1 }); // only support 1 directory <-> 1 release
|
||||||
|
Directory::find(context.dbSession, params, [&](const Directory::pointer& foundDirectory) {
|
||||||
|
directoryToReport = foundDirectory;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (directoryToReport)
|
||||||
|
{
|
||||||
|
albumNode.setAttribute("title", directoryToReport->getName());
|
||||||
|
albumNode.setAttribute("id", idToString(directoryToReport->getId()));
|
||||||
|
if (const Directory::pointer & parentDirectory{ directoryToReport->getParentDirectory() })
|
||||||
|
albumNode.setAttribute("parent", idToString(parentDirectory->getId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
albumNode.setAttribute("album", release->getName());
|
||||||
albumNode.setAttribute("isDir", true);
|
albumNode.setAttribute("isDir", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
albumNode.setAttribute("created", core::stringUtils::toISO8601String(release->getLastWritten()));
|
albumNode.setAttribute("created", core::stringUtils::toISO8601String(release->getLastWritten()));
|
||||||
albumNode.setAttribute("id", idToString(release->getId()));
|
|
||||||
albumNode.setAttribute("coverArt", idToString(release->getId()));
|
albumNode.setAttribute("coverArt", idToString(release->getId()));
|
||||||
if (const auto year{ release->getYear() })
|
if (const auto year{ release->getYear() })
|
||||||
albumNode.setAttribute("year", *year);
|
albumNode.setAttribute("year", *year);
|
||||||
@@ -70,11 +89,7 @@ namespace lms::api::subsonic
|
|||||||
if (artists.empty())
|
if (artists.empty())
|
||||||
artists = release->getArtists();
|
artists = release->getArtists();
|
||||||
|
|
||||||
if (artists.empty() && !id3)
|
if (!artists.empty())
|
||||||
{
|
|
||||||
albumNode.setAttribute("parent", idToString(RootId{}));
|
|
||||||
}
|
|
||||||
else if (!artists.empty())
|
|
||||||
{
|
{
|
||||||
if (!release->getArtistDisplayName().empty())
|
if (!release->getArtistDisplayName().empty())
|
||||||
albumNode.setAttribute("artist", release->getArtistDisplayName());
|
albumNode.setAttribute("artist", release->getArtistDisplayName());
|
||||||
@@ -83,16 +98,11 @@ namespace lms::api::subsonic
|
|||||||
|
|
||||||
if (artists.size() == 1)
|
if (artists.size() == 1)
|
||||||
{
|
{
|
||||||
albumNode.setAttribute(id3 ? Response::Node::Key{ "artistId" } : Response::Node::Key{ "parent" }, idToString(artists.front()->getId()));
|
albumNode.setAttribute("artistId", idToString(artists.front()->getId()));
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (!id3)
|
|
||||||
albumNode.setAttribute("parent", idToString(RootId{}));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
albumNode.setAttribute("playCount", core::Service<scrobbling::IScrobblingService>::get()->getCount(user->getId(), release->getId()));
|
albumNode.setAttribute("playCount", core::Service<scrobbling::IScrobblingService>::get()->getCount(context.user->getId(), release->getId()));
|
||||||
|
|
||||||
// Report the first GENRE for this track
|
// Report the first GENRE for this track
|
||||||
const ClusterType::pointer genreClusterType{ ClusterType::find(context.dbSession, "GENRE") };
|
const ClusterType::pointer genreClusterType{ ClusterType::find(context.dbSession, "GENRE") };
|
||||||
@@ -103,7 +113,7 @@ namespace lms::api::subsonic
|
|||||||
albumNode.setAttribute("genre", clusters.front().front()->getName());
|
albumNode.setAttribute("genre", clusters.front().front()->getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (const Wt::WDateTime dateTime{ core::Service<feedback::IFeedbackService>::get()->getStarredDateTime(user->getId(), release->getId()) }; dateTime.isValid())
|
if (const Wt::WDateTime dateTime{ core::Service<feedback::IFeedbackService>::get()->getStarredDateTime(context.user->getId(), release->getId()) }; dateTime.isValid())
|
||||||
albumNode.setAttribute("starred", core::stringUtils::toISO8601String(dateTime));
|
albumNode.setAttribute("starred", core::stringUtils::toISO8601String(dateTime));
|
||||||
|
|
||||||
if (!context.enableOpenSubsonic)
|
if (!context.enableOpenSubsonic)
|
||||||
@@ -111,12 +121,10 @@ namespace lms::api::subsonic
|
|||||||
|
|
||||||
// OpenSubsonic specific fields (must always be set)
|
// OpenSubsonic specific fields (must always be set)
|
||||||
albumNode.setAttribute("sortName", release->getSortName());
|
albumNode.setAttribute("sortName", release->getSortName());
|
||||||
|
albumNode.setAttribute("mediaType", "album");
|
||||||
if (!id3)
|
|
||||||
albumNode.setAttribute("mediaType", "album");
|
|
||||||
|
|
||||||
{
|
{
|
||||||
const Wt::WDateTime dateTime{ core::Service<scrobbling::IScrobblingService>::get()->getLastListenDateTime(user->getId(), release->getId()) };
|
const Wt::WDateTime dateTime{ core::Service<scrobbling::IScrobblingService>::get()->getLastListenDateTime(context.user->getId(), release->getId()) };
|
||||||
albumNode.setAttribute("played", dateTime.isValid() ? core::stringUtils::toISO8601String(dateTime) : std::string{ "" });
|
albumNode.setAttribute("played", dateTime.isValid() ? core::stringUtils::toISO8601String(dateTime) : std::string{ "" });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
|
|
||||||
namespace lms::db
|
namespace lms::db
|
||||||
{
|
{
|
||||||
|
class Directory;
|
||||||
class Release;
|
class Release;
|
||||||
class User;
|
class User;
|
||||||
class Session;
|
class Session;
|
||||||
@@ -32,5 +33,7 @@ namespace lms::db
|
|||||||
|
|
||||||
namespace lms::api::subsonic
|
namespace lms::api::subsonic
|
||||||
{
|
{
|
||||||
Response::Node createAlbumNode(RequestContext& context, const db::ObjectPtr<db::Release>& release, const db::ObjectPtr<db::User>& user, bool id3);
|
class RequestContext;
|
||||||
}
|
|
||||||
|
Response::Node createAlbumNode(RequestContext& context, const db::ObjectPtr<db::Release>& release, bool id3, const db::ObjectPtr<db::Directory>& directory = {});
|
||||||
|
} // namespace lms::api::subsonic
|
||||||
@@ -29,6 +29,7 @@
|
|||||||
#include "database/User.hpp"
|
#include "database/User.hpp"
|
||||||
#include "services/feedback/IFeedbackService.hpp"
|
#include "services/feedback/IFeedbackService.hpp"
|
||||||
|
|
||||||
|
#include "RequestContext.hpp"
|
||||||
#include "SubsonicId.hpp"
|
#include "SubsonicId.hpp"
|
||||||
|
|
||||||
namespace lms::api::subsonic
|
namespace lms::api::subsonic
|
||||||
@@ -85,7 +86,7 @@ namespace lms::api::subsonic
|
|||||||
}
|
}
|
||||||
} // namespace utils
|
} // namespace utils
|
||||||
|
|
||||||
Response::Node createArtistNode(RequestContext& context, const Artist::pointer& artist, const User::pointer& user, bool id3)
|
Response::Node createArtistNode(RequestContext& context, const Artist::pointer& artist)
|
||||||
{
|
{
|
||||||
LMS_SCOPED_TRACE_DETAILED("Subsonic", "CreateArtist");
|
LMS_SCOPED_TRACE_DETAILED("Subsonic", "CreateArtist");
|
||||||
|
|
||||||
@@ -96,20 +97,16 @@ namespace lms::api::subsonic
|
|||||||
if (const db::Image::pointer artistImage{ artist->getImage() })
|
if (const db::Image::pointer artistImage{ artist->getImage() })
|
||||||
artistNode.setAttribute("coverArt", idToString(artist->getId()));
|
artistNode.setAttribute("coverArt", idToString(artist->getId()));
|
||||||
|
|
||||||
if (id3)
|
const std::size_t count{ Release::getCount(context.dbSession, Release::FindParameters{}.setArtist(artist->getId())) };
|
||||||
{
|
artistNode.setAttribute("albumCount", count);
|
||||||
const std::size_t count{ Release::getCount(context.dbSession, Release::FindParameters{}.setArtist(artist->getId())) };
|
|
||||||
artistNode.setAttribute("albumCount", count);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (const Wt::WDateTime dateTime{ core::Service<feedback::IFeedbackService>::get()->getStarredDateTime(user->getId(), artist->getId()) }; dateTime.isValid())
|
if (const Wt::WDateTime dateTime{ core::Service<feedback::IFeedbackService>::get()->getStarredDateTime(context.user->getId(), artist->getId()) }; dateTime.isValid())
|
||||||
artistNode.setAttribute("starred", core::stringUtils::toISO8601String(dateTime));
|
artistNode.setAttribute("starred", core::stringUtils::toISO8601String(dateTime));
|
||||||
|
|
||||||
// OpenSubsonic specific fields (must always be set)
|
// OpenSubsonic specific fields (must always be set)
|
||||||
if (context.enableOpenSubsonic)
|
if (context.enableOpenSubsonic)
|
||||||
{
|
{
|
||||||
if (!id3)
|
artistNode.setAttribute("mediaType", "artist");
|
||||||
artistNode.setAttribute("mediaType", "artist");
|
|
||||||
|
|
||||||
{
|
{
|
||||||
std::optional<core::UUID> mbid{ artist->getMBID() };
|
std::optional<core::UUID> mbid{ artist->getMBID() };
|
||||||
|
|||||||
@@ -36,11 +36,14 @@ namespace lms::db
|
|||||||
|
|
||||||
namespace lms::api::subsonic
|
namespace lms::api::subsonic
|
||||||
{
|
{
|
||||||
|
class RequestContext;
|
||||||
|
|
||||||
namespace utils
|
namespace utils
|
||||||
{
|
{
|
||||||
std::string joinArtistNames(const std::vector<db::ObjectPtr<db::Artist>>& artists);
|
std::string joinArtistNames(const std::vector<db::ObjectPtr<db::Artist>>& artists);
|
||||||
std::string_view toString(db::TrackArtistLinkType type);
|
std::string_view toString(db::TrackArtistLinkType type);
|
||||||
} // namespace utils
|
} // namespace utils
|
||||||
Response::Node createArtistNode(RequestContext& context, const db::ObjectPtr<db::Artist>& artist, const db::ObjectPtr<db::User>& user, bool id3);
|
|
||||||
|
Response::Node createArtistNode(RequestContext& context, const db::ObjectPtr<db::Artist>& artist);
|
||||||
Response::Node createArtistNode(const db::ObjectPtr<db::Artist>& artist); // only minimal info
|
Response::Node createArtistNode(const db::ObjectPtr<db::Artist>& artist); // only minimal info
|
||||||
} // namespace lms::api::subsonic
|
} // namespace lms::api::subsonic
|
||||||
@@ -21,13 +21,23 @@
|
|||||||
|
|
||||||
#include "database/Cluster.hpp"
|
#include "database/Cluster.hpp"
|
||||||
|
|
||||||
|
#include "RequestContext.hpp"
|
||||||
|
|
||||||
namespace lms::api::subsonic
|
namespace lms::api::subsonic
|
||||||
{
|
{
|
||||||
Response::Node createGenreNode(const db::Cluster::pointer& cluster)
|
Response::Node createGenreNode(RequestContext& context, const db::Cluster::pointer& cluster)
|
||||||
{
|
{
|
||||||
Response::Node clusterNode;
|
Response::Node clusterNode;
|
||||||
|
|
||||||
clusterNode.setValue(cluster->getName());
|
switch (context.responseFormat)
|
||||||
|
{
|
||||||
|
case ResponseFormat::json:
|
||||||
|
clusterNode.setAttribute("value", cluster->getName());
|
||||||
|
break;
|
||||||
|
case ResponseFormat::xml:
|
||||||
|
clusterNode.setValue(cluster->getName());
|
||||||
|
break;
|
||||||
|
}
|
||||||
clusterNode.setAttribute("songCount", cluster->getTrackCount());
|
clusterNode.setAttribute("songCount", cluster->getTrackCount());
|
||||||
clusterNode.setAttribute("albumCount", cluster->getReleasesCount());
|
clusterNode.setAttribute("albumCount", cluster->getReleasesCount());
|
||||||
|
|
||||||
|
|||||||
@@ -30,5 +30,7 @@ namespace lms::db
|
|||||||
|
|
||||||
namespace lms::api::subsonic
|
namespace lms::api::subsonic
|
||||||
{
|
{
|
||||||
Response::Node createGenreNode(const db::ObjectPtr<db::Cluster>& cluster);
|
class RequestContext;
|
||||||
}
|
|
||||||
|
Response::Node createGenreNode(RequestContext& context, const db::ObjectPtr<db::Cluster>& cluster);
|
||||||
|
} // namespace lms::api::subsonic
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
#include "core/String.hpp"
|
#include "core/String.hpp"
|
||||||
#include "database/Artist.hpp"
|
#include "database/Artist.hpp"
|
||||||
#include "database/Cluster.hpp"
|
#include "database/Cluster.hpp"
|
||||||
|
#include "database/Directory.hpp"
|
||||||
#include "database/Release.hpp"
|
#include "database/Release.hpp"
|
||||||
#include "database/Track.hpp"
|
#include "database/Track.hpp"
|
||||||
#include "database/TrackArtistLink.hpp"
|
#include "database/TrackArtistLink.hpp"
|
||||||
@@ -34,6 +35,7 @@
|
|||||||
#include "services/feedback/IFeedbackService.hpp"
|
#include "services/feedback/IFeedbackService.hpp"
|
||||||
#include "services/scrobbling/IScrobblingService.hpp"
|
#include "services/scrobbling/IScrobblingService.hpp"
|
||||||
|
|
||||||
|
#include "RequestContext.hpp"
|
||||||
#include "SubsonicId.hpp"
|
#include "SubsonicId.hpp"
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
#include "responses/Artist.hpp"
|
#include "responses/Artist.hpp"
|
||||||
@@ -67,14 +69,20 @@ namespace lms::api::subsonic
|
|||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Response::Node createSongNode(RequestContext& context, const Track::pointer& track, const User::pointer& user)
|
Response::Node createSongNode(RequestContext& context, const Track::pointer& track, bool id3)
|
||||||
{
|
{
|
||||||
LMS_SCOPED_TRACE_DETAILED("Subsonic", "CreateSong");
|
LMS_SCOPED_TRACE_DETAILED("Subsonic", "CreateSong");
|
||||||
|
|
||||||
Response::Node trackResponse;
|
Response::Node trackResponse;
|
||||||
|
|
||||||
|
if (!id3)
|
||||||
|
{
|
||||||
|
if (const auto directory{ track->getDirectory() })
|
||||||
|
trackResponse.setAttribute("parent", idToString(directory->getId()));
|
||||||
|
trackResponse.setAttribute("isDir", false);
|
||||||
|
}
|
||||||
|
|
||||||
trackResponse.setAttribute("id", idToString(track->getId()));
|
trackResponse.setAttribute("id", idToString(track->getId()));
|
||||||
trackResponse.setAttribute("isDir", false);
|
|
||||||
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());
|
||||||
@@ -82,7 +90,7 @@ namespace lms::api::subsonic
|
|||||||
trackResponse.setAttribute("discNumber", *track->getDiscNumber());
|
trackResponse.setAttribute("discNumber", *track->getDiscNumber());
|
||||||
if (track->getYear())
|
if (track->getYear())
|
||||||
trackResponse.setAttribute("year", *track->getYear());
|
trackResponse.setAttribute("year", *track->getYear());
|
||||||
trackResponse.setAttribute("playCount", core::Service<scrobbling::IScrobblingService>::get()->getCount(user->getId(), track->getId()));
|
trackResponse.setAttribute("playCount", core::Service<scrobbling::IScrobblingService>::get()->getCount(context.user->getId(), track->getId()));
|
||||||
trackResponse.setAttribute("path", track->getRelativeFilePath().string());
|
trackResponse.setAttribute("path", track->getRelativeFilePath().string());
|
||||||
trackResponse.setAttribute("size", track->getFileSize());
|
trackResponse.setAttribute("size", track->getFileSize());
|
||||||
|
|
||||||
@@ -93,12 +101,13 @@ namespace lms::api::subsonic
|
|||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const std::string fileSuffix{ formatToSuffix(user->getSubsonicDefaultTranscodingOutputFormat()) };
|
const std::string fileSuffix{ formatToSuffix(context.user->getSubsonicDefaultTranscodingOutputFormat()) };
|
||||||
trackResponse.setAttribute("transcodedSuffix", fileSuffix);
|
trackResponse.setAttribute("transcodedSuffix", fileSuffix);
|
||||||
trackResponse.setAttribute("transcodedContentType", av::getMimeType(std::filesystem::path{ "." + fileSuffix }));
|
trackResponse.setAttribute("transcodedContentType", av::getMimeType(std::filesystem::path{ "." + fileSuffix }));
|
||||||
}
|
}
|
||||||
|
|
||||||
trackResponse.setAttribute("coverArt", idToString(track->getId()));
|
if (track->hasCover())
|
||||||
|
trackResponse.setAttribute("coverArt", idToString(track->getId()));
|
||||||
|
|
||||||
const std::vector<Artist::pointer>& artists{ track->getArtists({ TrackArtistLinkType::Artist }) };
|
const std::vector<Artist::pointer>& artists{ track->getArtists({ TrackArtistLinkType::Artist }) };
|
||||||
if (!artists.empty())
|
if (!artists.empty())
|
||||||
@@ -117,7 +126,6 @@ namespace lms::api::subsonic
|
|||||||
{
|
{
|
||||||
trackResponse.setAttribute("album", release->getName());
|
trackResponse.setAttribute("album", release->getName());
|
||||||
trackResponse.setAttribute("albumId", idToString(release->getId()));
|
trackResponse.setAttribute("albumId", idToString(release->getId()));
|
||||||
trackResponse.setAttribute("parent", idToString(release->getId()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
trackResponse.setAttribute("duration", std::chrono::duration_cast<std::chrono::seconds>(track->getDuration()).count());
|
trackResponse.setAttribute("duration", std::chrono::duration_cast<std::chrono::seconds>(track->getDuration()).count());
|
||||||
@@ -126,7 +134,7 @@ namespace lms::api::subsonic
|
|||||||
trackResponse.setAttribute("created", core::stringUtils::toISO8601String(track->getLastWritten()));
|
trackResponse.setAttribute("created", core::stringUtils::toISO8601String(track->getLastWritten()));
|
||||||
trackResponse.setAttribute("contentType", av::getMimeType(track->getAbsoluteFilePath().extension()));
|
trackResponse.setAttribute("contentType", av::getMimeType(track->getAbsoluteFilePath().extension()));
|
||||||
|
|
||||||
if (const Wt::WDateTime dateTime{ core::Service<feedback::IFeedbackService>::get()->getStarredDateTime(user->getId(), track->getId()) }; dateTime.isValid())
|
if (const Wt::WDateTime dateTime{ core::Service<feedback::IFeedbackService>::get()->getStarredDateTime(context.user->getId(), track->getId()) }; dateTime.isValid())
|
||||||
trackResponse.setAttribute("starred", core::stringUtils::toISO8601String(dateTime));
|
trackResponse.setAttribute("starred", core::stringUtils::toISO8601String(dateTime));
|
||||||
|
|
||||||
// Report the first GENRE for this track
|
// Report the first GENRE for this track
|
||||||
@@ -152,7 +160,7 @@ namespace lms::api::subsonic
|
|||||||
trackResponse.setAttribute("mediaType", "song");
|
trackResponse.setAttribute("mediaType", "song");
|
||||||
|
|
||||||
{
|
{
|
||||||
const Wt::WDateTime dateTime{ core::Service<scrobbling::IScrobblingService>::get()->getLastListenDateTime(user->getId(), track->getId()) };
|
const Wt::WDateTime dateTime{ core::Service<scrobbling::IScrobblingService>::get()->getLastListenDateTime(context.user->getId(), track->getId()) };
|
||||||
trackResponse.setAttribute("played", dateTime.isValid() ? core::stringUtils::toISO8601String(dateTime) : "");
|
trackResponse.setAttribute("played", dateTime.isValid() ? core::stringUtils::toISO8601String(dateTime) : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,5 +32,7 @@ namespace lms::db
|
|||||||
|
|
||||||
namespace lms::api::subsonic
|
namespace lms::api::subsonic
|
||||||
{
|
{
|
||||||
Response::Node createSongNode(RequestContext& context, const db::ObjectPtr<db::Track>& track, const db::ObjectPtr<db::User>& user);
|
class RequestContext;
|
||||||
}
|
|
||||||
|
Response::Node createSongNode(RequestContext& context, const db::ObjectPtr<db::Track>& track, bool id3);
|
||||||
|
} // namespace lms::api::subsonic
|
||||||
@@ -19,13 +19,15 @@
|
|||||||
|
|
||||||
#include "responses/User.hpp"
|
#include "responses/User.hpp"
|
||||||
|
|
||||||
|
#include "database/MediaLibrary.hpp"
|
||||||
#include "database/User.hpp"
|
#include "database/User.hpp"
|
||||||
|
|
||||||
|
#include "RequestContext.hpp"
|
||||||
|
#include "SubsonicId.hpp"
|
||||||
|
|
||||||
namespace lms::api::subsonic
|
namespace lms::api::subsonic
|
||||||
{
|
{
|
||||||
using namespace db;
|
Response::Node createUserNode(RequestContext& context, const db::User::pointer& user)
|
||||||
|
|
||||||
Response::Node createUserNode(const User::pointer& user)
|
|
||||||
{
|
{
|
||||||
Response::Node userNode;
|
Response::Node userNode;
|
||||||
|
|
||||||
@@ -38,14 +40,15 @@ namespace lms::api::subsonic
|
|||||||
userNode.setAttribute("playlistRole", true);
|
userNode.setAttribute("playlistRole", true);
|
||||||
userNode.setAttribute("coverArtRole", false);
|
userNode.setAttribute("coverArtRole", false);
|
||||||
userNode.setAttribute("commentRole", false);
|
userNode.setAttribute("commentRole", false);
|
||||||
userNode.setAttribute("podcastRole", false);
|
userNode.setAttribute("podcastRole", false); // not supported
|
||||||
userNode.setAttribute("streamRole", true);
|
userNode.setAttribute("streamRole", true);
|
||||||
userNode.setAttribute("jukeboxRole", false);
|
userNode.setAttribute("jukeboxRole", false); // not supported
|
||||||
userNode.setAttribute("shareRole", false);
|
userNode.setAttribute("shareRole", false); // not supported
|
||||||
|
|
||||||
Response::Node folder;
|
// users can access all libraries
|
||||||
folder.setValue("0");
|
db::MediaLibrary::find(context.dbSession, [&](const db::MediaLibrary::pointer& library) {
|
||||||
userNode.addArrayChild("folder", std::move(folder));
|
userNode.addArrayValue("folder", idToString(library->getId()));
|
||||||
|
});
|
||||||
|
|
||||||
return userNode;
|
return userNode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,5 +30,7 @@ namespace lms::db
|
|||||||
|
|
||||||
namespace lms::api::subsonic
|
namespace lms::api::subsonic
|
||||||
{
|
{
|
||||||
Response::Node createUserNode(const db::ObjectPtr<db::User>& user);
|
class RequestContext;
|
||||||
}
|
|
||||||
|
Response::Node createUserNode(RequestContext& context, const db::ObjectPtr<db::User>& user);
|
||||||
|
} // namespace lms::api::subsonic
|
||||||
@@ -256,7 +256,7 @@ namespace lms::ui
|
|||||||
|
|
||||||
_status->setText(Wt::WString::tr("Lms.Admin.ScannerController.status-in-progress")
|
_status->setText(Wt::WString::tr("Lms.Admin.ScannerController.status-in-progress")
|
||||||
.arg(status.currentScanStepStats->stepIndex + 1)
|
.arg(status.currentScanStepStats->stepIndex + 1)
|
||||||
.arg(scanner::ScanProgressStepCount));
|
.arg(status.currentScanStepStats->stepCount));
|
||||||
|
|
||||||
refreshCurrentStep(*status.currentScanStepStats);
|
refreshCurrentStep(*status.currentScanStepStats);
|
||||||
break;
|
break;
|
||||||
@@ -326,6 +326,11 @@ namespace lms::ui
|
|||||||
.arg(stepStats.totalElems)
|
.arg(stepStats.totalElems)
|
||||||
.arg(stepStats.progress()));
|
.arg(stepStats.progress()));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case ScanStep::UpdateLibraryFields:
|
||||||
|
_stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-updating-library-fields")
|
||||||
|
.arg(stepStats.processedElems));
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} // namespace lms::ui
|
} // namespace lms::ui
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ namespace lms::ui
|
|||||||
{
|
{
|
||||||
auto v = std::make_unique<LengthValidator>();
|
auto v = std::make_unique<LengthValidator>();
|
||||||
v->setMandatory(true);
|
v->setMandatory(true);
|
||||||
v->setMinimumLength(db::User::MinNameLength);
|
v->setMinimumLength(db::User::minNameLength);
|
||||||
v->setMaximumLength(db::User::MaxNameLength);
|
v->setMaximumLength(db::User::maxNameLength);
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
} // namespace lms::ui
|
} // namespace lms::ui
|
||||||
|
|||||||
@@ -321,7 +321,8 @@ namespace lms::ui
|
|||||||
const bool variousArtists{ release->hasVariousArtists() };
|
const bool variousArtists{ release->hasVariousArtists() };
|
||||||
const auto totalDisc{ release->getTotalDisc() };
|
const auto totalDisc{ release->getTotalDisc() };
|
||||||
const std::size_t discCount{ release->getDiscCount() };
|
const std::size_t discCount{ release->getDiscCount() };
|
||||||
const bool isReleaseMultiDisc{ (discCount > 1) || (totalDisc && *totalDisc > 1) };
|
const bool hasDiscSubtitle{ release->hasDiscSubtitle() };
|
||||||
|
const bool useSubtitleContainers{ (discCount > 1) || (totalDisc && *totalDisc > 1) || hasDiscSubtitle };
|
||||||
|
|
||||||
// Expect to be called in asc order
|
// Expect to be called in asc order
|
||||||
std::map<std::size_t, Wt::WContainerWidget*> trackContainers;
|
std::map<std::size_t, Wt::WContainerWidget*> trackContainers;
|
||||||
@@ -392,8 +393,10 @@ namespace lms::ui
|
|||||||
const auto discNumber{ track->getDiscNumber() };
|
const auto discNumber{ track->getDiscNumber() };
|
||||||
|
|
||||||
Wt::WContainerWidget* container;
|
Wt::WContainerWidget* container;
|
||||||
if (isReleaseMultiDisc && discNumber)
|
if (useSubtitleContainers && discNumber)
|
||||||
container = getOrAddDiscContainer(*discNumber, track->getDiscSubtitle());
|
container = getOrAddDiscContainer(*discNumber, track->getDiscSubtitle());
|
||||||
|
else if (hasDiscSubtitle && !discNumber)
|
||||||
|
container = getOrAddDiscContainer(0, track->getDiscSubtitle());
|
||||||
else
|
else
|
||||||
container = getOrAddNoDiscContainer();
|
container = getOrAddNoDiscContainer();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user