Merge branch 'develop' for release 3.10.0
This commit is contained in:
+1
-1
@@ -1,4 +1,4 @@
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
cmake_minimum_required(VERSION 3.12)
|
||||
|
||||
project(lms)
|
||||
|
||||
|
||||
@@ -98,6 +98,7 @@ Get the latest stable release and build it:
|
||||
git clone https://github.com/epoupon/lms.git lms
|
||||
cd lms
|
||||
mkdir build
|
||||
cd build
|
||||
cmake .. -DCMAKE_BUILD_TYPE=Release
|
||||
```
|
||||
__Note__: in order to customize the installation directory, you can use the _-DCMAKE_INSTALL_PREFIX_ option (defaults to `/usr/local`).
|
||||
@@ -163,6 +164,19 @@ All other settings are set using the web interface (user management, scan settin
|
||||
|
||||
If a setting is not present in the configuration file, a hardcoded default value is used (the same as in the [default.conf](https://github.com/epoupon/lms/blob/master/conf/lms.conf) file)
|
||||
|
||||
### Deploy on non root path
|
||||
If you want to deploy on non root path (e.g. https://mydomain.com/newroot/), you have to set the `deploy-path` option accordingly in `lms.conf`.
|
||||
|
||||
As static resources are __not__ related to the `deploy-path` option, you have to perform the following steps if you want them to be on a non root path too:
|
||||
* Create a new intermediary `newroot` directory in `/usr/share/lms/docroot` and move everything in it.
|
||||
* Symlink `/usr/share/lms/docroot/newroot/resources` to `/usr/share/Wt/resources`.
|
||||
* Edit `lms.conf` and set:
|
||||
```
|
||||
wt-resources = "" # do not comment the whole line
|
||||
docroot = "/usr/share/lms/docroot/;/newroot/resources,/newroot/css,/newroot/images,/newroot/js,/newroot/favicon.ico";`
|
||||
deploy-path = "/newroot/"; # ending slash is important
|
||||
```
|
||||
|
||||
### Reverse proxy settings
|
||||
_LMS_ is shipped with an embedded web server, but it is recommended to deploy behind a reverse proxy. You have to set the _behind-reverse-proxy_ option to _true_ in the `lms.conf` configuration file.
|
||||
|
||||
|
||||
+3
-4
@@ -18,19 +18,18 @@ listen-port = 5082;
|
||||
listen-addr = "0.0.0.0";
|
||||
behind-reverse-proxy = false;
|
||||
|
||||
# Location for deployment
|
||||
deploy-path = "/";
|
||||
|
||||
# If enabled, these files have to exist and have correct permissions
|
||||
tls-enable = false;
|
||||
tls-cert = "/var/lms/cert.pem";
|
||||
tls-key = "/var/lms/privkey.pem";
|
||||
tls-dh = "/var/lms/dh2048.pem";
|
||||
|
||||
# Path to the resources used by the web interface
|
||||
# Path to the resources used by the web interface.
|
||||
wt-resources = "/usr/share/Wt/resources";
|
||||
docroot = "/usr/share/lms/docroot/;/resources,/css,/images,/js,/favicon.ico";
|
||||
approot = "/usr/share/lms/approot";
|
||||
# Location for deployment (See README if you want to deploy on a non root path)
|
||||
deploy-path = "/";
|
||||
|
||||
# Acoustic brainz's root API
|
||||
acousticbrainz-api-url = "https://acousticbrainz.org/api/v1/";
|
||||
|
||||
@@ -74,14 +74,87 @@ Artist::create(Session& session, const std::string& name, const std::optional<UU
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer>
|
||||
Artist::getAll(Session& session, std::optional<std::size_t> offset, std::optional<std::size_t> size)
|
||||
static
|
||||
Wt::Dbo::Query<Artist::pointer>
|
||||
getQuery(Session& session,
|
||||
const std::set<IdType>& clusterIds,
|
||||
const std::vector<std::string>& keywords,
|
||||
std::optional<TrackArtistLink::Type> linkType,
|
||||
Artist::SortMethod sortMethod)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
Wt::Dbo::collection<pointer> res = session.getDboSession().find<Artist>()
|
||||
.offset(offset ? static_cast<int>(*offset) : -1)
|
||||
.limit(size ? static_cast<int>(*size) : -1)
|
||||
.orderBy("sort_name COLLATE NOCASE");
|
||||
|
||||
WhereClause where;
|
||||
|
||||
std::ostringstream oss;
|
||||
oss << "SELECT DISTINCT a FROM artist a";
|
||||
|
||||
if (!keywords.empty())
|
||||
{
|
||||
WhereClause whereKeywordsName;
|
||||
WhereClause whereKeywordsSortName;
|
||||
|
||||
for (auto keyword : keywords)
|
||||
{
|
||||
whereKeywordsName.And(WhereClause("a.name LIKE ?")).bind("%%" + keyword + "%%");
|
||||
whereKeywordsSortName.And(WhereClause("a.sort_name LIKE ?")).bind("%%" + keyword + "%%");
|
||||
}
|
||||
|
||||
where.And(whereKeywordsName.Or(whereKeywordsSortName));
|
||||
}
|
||||
|
||||
if (!clusterIds.empty() || linkType)
|
||||
{
|
||||
oss << " INNER JOIN track t ON t.id = t_a_l.track_id INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id";
|
||||
|
||||
if (!clusterIds.empty())
|
||||
{
|
||||
oss << " INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
|
||||
WhereClause clusterClause;
|
||||
|
||||
for (auto id : clusterIds)
|
||||
clusterClause.Or(WhereClause("c.id = ?")).bind(std::to_string(id));
|
||||
|
||||
where.And(clusterClause);
|
||||
}
|
||||
if (linkType)
|
||||
where.And(WhereClause {"t_a_l.type = ?"}.bind(std::to_string(static_cast<int>(*linkType))));
|
||||
}
|
||||
oss << " " << where.get();
|
||||
|
||||
if (!clusterIds.empty())
|
||||
oss << " GROUP BY t.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size();
|
||||
|
||||
switch (sortMethod)
|
||||
{
|
||||
case Artist::SortMethod::None:
|
||||
break;
|
||||
case Artist::SortMethod::ByName:
|
||||
oss << " ORDER BY a.name COLLATE NOCASE";
|
||||
break;
|
||||
case Artist::SortMethod::BySortName:
|
||||
oss << " ORDER BY a.sort_name COLLATE NOCASE";
|
||||
break;
|
||||
}
|
||||
|
||||
Wt::Dbo::Query<Artist::pointer> query = session.getDboSession().query<Artist::pointer>( oss.str() );
|
||||
|
||||
for (const std::string& bindArg : where.getBindArgs())
|
||||
{
|
||||
query.bind(bindArg);
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer>
|
||||
Artist::getAll(Session& session, SortMethod sortMethod, std::optional<std::size_t> offset, std::optional<std::size_t> size)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<Artist::pointer> res = getQuery(session, {}, {}, std::nullopt, sortMethod)
|
||||
.limit(size ? static_cast<int>(*size) + 1 : -1)
|
||||
.offset(offset ? static_cast<int>(*offset) : -1);
|
||||
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
@@ -111,73 +184,21 @@ Artist::getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit
|
||||
|
||||
Wt::Dbo::collection<IdType> res = session.getDboSession().query<IdType>
|
||||
("SELECT DISTINCT a.id FROM artist a"
|
||||
" INNER JOIN track t ON t.id = t_a_l.track_id INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id"
|
||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id")
|
||||
" INNER JOIN track t ON t.id = t_a_l.track_id INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id"
|
||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id")
|
||||
.limit(limit ? static_cast<int>(*limit) : -1);
|
||||
|
||||
return std::vector<IdType>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
|
||||
static
|
||||
Wt::Dbo::Query<Artist::pointer>
|
||||
getQuery(Session& session,
|
||||
const std::set<IdType>& clusterIds,
|
||||
const std::vector<std::string>& keywords,
|
||||
std::optional<TrackArtistLink::Type> linkType)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
WhereClause where;
|
||||
|
||||
std::ostringstream oss;
|
||||
oss << "SELECT DISTINCT a FROM artist a";
|
||||
|
||||
for (auto keyword : keywords)
|
||||
where.And(WhereClause("a.name LIKE ?")).bind("%%" + keyword + "%%");
|
||||
|
||||
if (!clusterIds.empty() || linkType)
|
||||
{
|
||||
oss << " INNER JOIN track t ON t.id = t_a_l.track_id INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id";
|
||||
|
||||
if (!clusterIds.empty())
|
||||
{
|
||||
oss << " INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
|
||||
WhereClause clusterClause;
|
||||
|
||||
for (auto id : clusterIds)
|
||||
clusterClause.Or(WhereClause("c.id = ?")).bind(std::to_string(id));
|
||||
|
||||
where.And(clusterClause);
|
||||
}
|
||||
if (linkType)
|
||||
where.And(WhereClause {"t_a_l.type = ?"}.bind(std::to_string(static_cast<int>(*linkType))));
|
||||
}
|
||||
oss << " " << where.get();
|
||||
|
||||
if (!clusterIds.empty())
|
||||
oss << " GROUP BY t.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size();
|
||||
|
||||
oss << " ORDER BY a.sort_name COLLATE NOCASE";
|
||||
|
||||
Wt::Dbo::Query<Artist::pointer> query = session.getDboSession().query<Artist::pointer>( oss.str() );
|
||||
|
||||
for (const std::string& bindArg : where.getBindArgs())
|
||||
{
|
||||
query.bind(bindArg);
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer>
|
||||
Artist::getByClusters(Session& session, const std::set<IdType>& clusters)
|
||||
Artist::getByClusters(Session& session, const std::set<IdType>& clusters, SortMethod sortMethod)
|
||||
{
|
||||
assert(!clusters.empty());
|
||||
|
||||
session.checkSharedLocked();
|
||||
bool more;
|
||||
return getByFilter(session, clusters, {}, {}, {}, {}, more);
|
||||
return getByFilter(session, clusters, {}, {}, sortMethod, {}, {}, more);
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer>
|
||||
@@ -185,12 +206,13 @@ Artist::getByFilter(Session& session,
|
||||
const std::set<IdType>& clusters,
|
||||
const std::vector<std::string>& keywords,
|
||||
std::optional<TrackArtistLink::Type> linkType,
|
||||
SortMethod sortMethod,
|
||||
std::optional<std::size_t> offset,
|
||||
std::optional<std::size_t> size,
|
||||
bool& moreResults)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
Wt::Dbo::collection<Artist::pointer> collection = getQuery(session, clusters, keywords, linkType)
|
||||
Wt::Dbo::collection<Artist::pointer> collection = getQuery(session, clusters, keywords, linkType, sortMethod)
|
||||
.limit(size ? static_cast<int>(*size) + 1 : -1)
|
||||
.offset(offset ? static_cast<int>(*offset) : -1);
|
||||
|
||||
@@ -405,6 +427,7 @@ void
|
||||
Artist::setSortName(const std::string& sortName)
|
||||
{
|
||||
_sortName = std::string(sortName, 0 , _maxNameLength);
|
||||
LMS_LOG(DB, DEBUG) << "SORT NAME = '" << _sortName << "'";
|
||||
}
|
||||
|
||||
} // namespace Database
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
|
||||
namespace Database {
|
||||
|
||||
#define LMS_DATABASE_VERSION 14
|
||||
#define LMS_DATABASE_VERSION 15
|
||||
|
||||
using Version = std::size_t;
|
||||
|
||||
@@ -163,6 +163,12 @@ CREATE TABLE IF NOT EXISTS "track_bookmark" (
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(*this).modify()->incScanVersion();
|
||||
}
|
||||
else if (version == 14)
|
||||
{
|
||||
// SortName now set from metadata
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(*this).modify()->incScanVersion();
|
||||
}
|
||||
else
|
||||
{
|
||||
LMS_LOG(DB, ERROR) << "Database version " << version << " cannot be handled using migration";
|
||||
@@ -318,9 +324,12 @@ Session::prepareTables()
|
||||
void
|
||||
Session::optimize()
|
||||
{
|
||||
auto uniqueTransaction {createUniqueTransaction()};
|
||||
|
||||
_session.execute("ANALYZE");
|
||||
LMS_LOG(DB, DEBUG) << "Optimizing db...";
|
||||
{
|
||||
auto uniqueTransaction {createUniqueTransaction()};
|
||||
_session.execute("ANALYZE");
|
||||
}
|
||||
LMS_LOG(DB, DEBUG) << "Optimized db!";
|
||||
}
|
||||
|
||||
} // namespace Database
|
||||
|
||||
@@ -45,6 +45,13 @@ class Artist : public Wt::Dbo::Dbo<Artist>
|
||||
{
|
||||
public:
|
||||
|
||||
enum class SortMethod
|
||||
{
|
||||
None,
|
||||
ByName,
|
||||
BySortName,
|
||||
};
|
||||
|
||||
using pointer = Wt::Dbo::ptr<Artist>;
|
||||
|
||||
Artist() {}
|
||||
@@ -53,26 +60,30 @@ class Artist : public Wt::Dbo::Dbo<Artist>
|
||||
// Accessors
|
||||
static pointer getByMBID(Session& session, const UUID& MBID);
|
||||
static pointer getById(Session& session, IdType id);
|
||||
static std::vector<pointer> getByName(Session& session, const std::string& name);
|
||||
static std::vector<pointer> getByName(Session& session, const std::string& name); // exact match on name field
|
||||
static std::vector<pointer> getByClusters(Session& session,
|
||||
const std::set<IdType>& clusters); // at least one track that belongs to these clusters
|
||||
const std::set<IdType>& clusters, // at least one track that belongs to these clusters
|
||||
SortMethod sortMethod
|
||||
);
|
||||
static std::vector<pointer> getByFilter(Session& session,
|
||||
const std::set<IdType>& clusters, // if non empty, at least one artist that belongs to these clusters
|
||||
const std::vector<std::string>& keywords, // if non empty, name must match all of these keywords
|
||||
const std::vector<std::string>& keywords, // if non empty, name must match all of these keywords (name + sort name fields)
|
||||
std::optional<TrackArtistLink::Type> linkType, // if set, only artists that have produced at least one track with this link type
|
||||
SortMethod sortMethod,
|
||||
std::optional<std::size_t> offset,
|
||||
std::optional<std::size_t> size,
|
||||
bool& moreExpected);
|
||||
|
||||
static std::vector<pointer> getAll(Session& session, std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {});
|
||||
static std::vector<pointer> getAll(Session& session, SortMethod sortMethod, std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {});
|
||||
static std::vector<IdType> getAllIds(Session& session);
|
||||
static std::vector<pointer> getAllOrphans(Session& session); // No track related
|
||||
static std::vector<pointer> getLastAdded(Session& session, Wt::WDateTime after, std::optional<std::size_t> size = {});
|
||||
static std::vector<IdType> getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit = {});
|
||||
|
||||
// Accessors
|
||||
const std::string& getName(void) const { return _name; }
|
||||
std::optional<UUID> getMBID(void) const { return UUID::fromString(_MBID); }
|
||||
const std::string& getName() const { return _name; }
|
||||
const std::string& getSortName() const { return _sortName; }
|
||||
std::optional<UUID> getMBID() const { return UUID::fromString(_MBID); }
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Release>> getReleases(const std::set<IdType>& clusterIds = {}) const; // if non empty, get the releases that match all these clusters
|
||||
std::size_t getReleaseCount() const;
|
||||
@@ -97,7 +108,7 @@ class Artist : public Wt::Dbo::Dbo<Artist>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _name, "name");
|
||||
Wt::Dbo::field(a, _name, "sort_name");
|
||||
Wt::Dbo::field(a, _sortName, "sort_name");
|
||||
Wt::Dbo::field(a, _MBID, "mbid");
|
||||
|
||||
Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "artist");
|
||||
|
||||
@@ -94,7 +94,7 @@ getAlbumArtists(const MetadataMap& metadataMap)
|
||||
|
||||
auto mbid {findFirstValueOfAs<UUID>(metadataMap, {"MUSICBRAINZ ALBUM ARTIST ID", "MUSICBRAINZ/ALBUM ARTIST ID"})};
|
||||
|
||||
return {Artist {*name, mbid} };
|
||||
return {Artist {*name, std::nullopt, mbid} };
|
||||
}
|
||||
|
||||
static
|
||||
@@ -118,9 +118,9 @@ getArtists(const MetadataMap& metadataMap)
|
||||
for (std::size_t i {}; i < artistNames.size(); ++i)
|
||||
{
|
||||
if (artistMBIDs && artistNames.size() == artistMBIDs->size())
|
||||
artists.emplace_back(Artist {artistNames[i], (*artistMBIDs)[i]});
|
||||
artists.emplace_back(Artist {artistNames[i], std::nullopt, (*artistMBIDs)[i]});
|
||||
else
|
||||
artists.emplace_back(Artist {artistNames[i], {}});
|
||||
artists.emplace_back(Artist {artistNames[i], std::nullopt, {}});
|
||||
}
|
||||
|
||||
return artists;
|
||||
|
||||
@@ -87,55 +87,74 @@ static
|
||||
std::vector<Artist>
|
||||
getArtists(const TagLib::PropertyMap& properties)
|
||||
{
|
||||
std::vector<Artist> res;
|
||||
|
||||
std::vector<std::string> artistNames {getPropertyValuesAs<std::string>(properties, "ARTISTS")};
|
||||
if (artistNames.empty())
|
||||
artistNames = getPropertyValuesAs<std::string>(properties, "ARTIST");
|
||||
|
||||
if (artistNames.empty())
|
||||
return res;
|
||||
return {};
|
||||
|
||||
const std::vector<UUID> artistsMBID {getPropertyValuesFirstMatchAs<UUID>(properties, {"MUSICBRAINZ_ARTISTID", "MUSICBRAINZ ARTIST ID"})};
|
||||
std::vector<Artist> artists;
|
||||
artists.reserve(artistNames.size());
|
||||
std::transform(std::cbegin(artistNames), std::cend(artistNames), std::back_inserter(artists),
|
||||
[&](const std::string& name) { return Artist {name}; });
|
||||
|
||||
if (artistNames.size() == artistsMBID.size())
|
||||
{
|
||||
std::transform(std::cbegin(artistNames), std::cend(artistNames), std::cbegin(artistsMBID), std::back_inserter(res),
|
||||
[&](const std::string& name, const UUID& mbid) { return Artist {name, mbid}; });
|
||||
}
|
||||
else
|
||||
{
|
||||
std::transform(std::cbegin(artistNames), std::cend(artistNames), std::back_inserter(res),
|
||||
[&](const std::string& name) { return Artist{name, {}}; });
|
||||
const std::vector<std::string> artistSortNames {getPropertyValuesAs<std::string>(properties, "ARTISTSORT")};
|
||||
if (artistSortNames.size() == artists.size())
|
||||
{
|
||||
for (std::size_t i {}; i < artistSortNames.size(); ++i)
|
||||
artists[i].sortName = artistSortNames[i];
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
{
|
||||
const std::vector<UUID> artistsMBID {getPropertyValuesFirstMatchAs<UUID>(properties, {"MUSICBRAINZ_ARTISTID", "MUSICBRAINZ ARTIST ID"})};
|
||||
|
||||
if (artistNames.size() == artistsMBID.size())
|
||||
{
|
||||
for (std::size_t i {}; i < artistsMBID.size(); ++i)
|
||||
artists[i].musicBrainzArtistID = artistsMBID[i];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return artists;
|
||||
}
|
||||
|
||||
static
|
||||
std::vector<Artist>
|
||||
getAlbumArtists(const TagLib::PropertyMap& properties)
|
||||
{
|
||||
std::vector<Artist> res;
|
||||
|
||||
std::vector<std::string> artistNames {getPropertyValuesAs<std::string>(properties, "ALBUMARTIST")};
|
||||
if (artistNames.empty())
|
||||
return res;
|
||||
return {};
|
||||
|
||||
const std::vector<UUID> artistsMBID {getPropertyValuesFirstMatchAs<UUID>(properties, {"MUSICBRAINZ_ALBUMARTISTID", "MUSICBRAINZ ALBUM ARTIST ID"})};
|
||||
std::vector<Artist> artists;
|
||||
artists.reserve(artistNames.size());
|
||||
std::transform(std::cbegin(artistNames), std::cend(artistNames), std::back_inserter(artists),
|
||||
[&](const std::string& name) { return Artist {name}; });
|
||||
|
||||
if (artistNames.size() == artistsMBID.size())
|
||||
{
|
||||
std::transform(std::cbegin(artistNames), std::cend(artistNames), std::cbegin(artistsMBID), std::back_inserter(res),
|
||||
[&](const std::string& name, const UUID& mbid) { return Artist{name, mbid}; });
|
||||
}
|
||||
else
|
||||
{
|
||||
std::transform(std::cbegin(artistNames), std::cend(artistNames), std::back_inserter(res),
|
||||
[&](const std::string& name) { return Artist{name, {}}; });
|
||||
const std::vector<std::string> artistSortNames {getPropertyValuesAs<std::string>(properties, "ALBUMARTISTSORT")};
|
||||
if (artistSortNames.size() == artists.size())
|
||||
{
|
||||
for (std::size_t i {}; i < artistSortNames.size(); ++i)
|
||||
artists[i].sortName = artistSortNames[i];
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
{
|
||||
const std::vector<UUID> artistsMBID {getPropertyValuesFirstMatchAs<UUID>(properties, {"MUSICBRAINZ_ALBUMARTISTID", "MUSICBRAINZ ALBUM ARTIST ID"})};
|
||||
|
||||
if (artistsMBID.size() == artists.size())
|
||||
{
|
||||
for (std::size_t i {}; i < artistsMBID.size(); ++i)
|
||||
artists[i].musicBrainzArtistID = artistsMBID[i];
|
||||
}
|
||||
}
|
||||
|
||||
return artists;
|
||||
}
|
||||
|
||||
static
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "utils/UUID.hpp"
|
||||
@@ -35,7 +36,11 @@ namespace MetaData
|
||||
struct Artist
|
||||
{
|
||||
std::string name;
|
||||
std::optional<std::string> sortName;
|
||||
std::optional<UUID> musicBrainzArtistID;
|
||||
|
||||
Artist(std::string_view _name) : name {_name} {}
|
||||
Artist(std::string_view _name, std::optional<std::string> _sortName, std::optional<UUID> _musicBrainzArtistID) : name {_name}, sortName {_sortName}, musicBrainzArtistID {_musicBrainzArtistID} {}
|
||||
};
|
||||
|
||||
struct Album
|
||||
|
||||
@@ -84,6 +84,38 @@ isPathInParentPath(const std::filesystem::path& path, const std::filesystem::pat
|
||||
return false;
|
||||
}
|
||||
|
||||
static
|
||||
Artist::pointer
|
||||
createArtist(Session& session, const MetaData::Artist& artistInfo)
|
||||
{
|
||||
Artist::pointer artist {Artist::create(session, artistInfo.name)};
|
||||
|
||||
if (artistInfo.musicBrainzArtistID)
|
||||
artist.modify()->setMBID(*artistInfo.musicBrainzArtistID);
|
||||
if (artistInfo.sortName)
|
||||
artist.modify()->setSortName(*artistInfo.sortName);
|
||||
|
||||
return artist;
|
||||
}
|
||||
|
||||
static
|
||||
void
|
||||
updateArtistIfNeeded(const Artist::pointer& artist, const MetaData::Artist& artistInfo)
|
||||
{
|
||||
// Name may have been updated
|
||||
if (artist->getName() != artistInfo.name)
|
||||
{
|
||||
artist.modify()->setName(artistInfo.name);
|
||||
}
|
||||
|
||||
// Sortname may have been updated
|
||||
if (artistInfo.sortName && *artistInfo.sortName != artist->getSortName() )
|
||||
{
|
||||
LMS_LOG(DBUPDATER, INFO) << "Setting sort name = '" << *artistInfo.sortName << "'";
|
||||
artist.modify()->setSortName(*artistInfo.sortName);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer>
|
||||
getOrCreateArtists(Session& session, const std::vector<MetaData::Artist>& artistsInfo)
|
||||
{
|
||||
@@ -98,14 +130,9 @@ getOrCreateArtists(Session& session, const std::vector<MetaData::Artist>& artist
|
||||
{
|
||||
artist = Artist::getByMBID(session, *artistInfo.musicBrainzArtistID);
|
||||
if (!artist)
|
||||
{
|
||||
artist = Artist::create(session, artistInfo.name, artistInfo.musicBrainzArtistID);
|
||||
}
|
||||
else if (artist->getName() != artistInfo.name)
|
||||
{
|
||||
// Name may have been updated
|
||||
artist.modify()->setName(artistInfo.name);
|
||||
}
|
||||
artist = createArtist(session, artistInfo);
|
||||
else
|
||||
updateArtistIfNeeded(artist, artistInfo);
|
||||
|
||||
artists.emplace_back(std::move(artist));
|
||||
continue;
|
||||
@@ -126,7 +153,9 @@ getOrCreateArtists(Session& session, const std::vector<MetaData::Artist>& artist
|
||||
|
||||
// No Artist found with the same name and without MBID -> creating
|
||||
if (!artist)
|
||||
artist = Artist::create(session, artistInfo.name);
|
||||
artist = createArtist(session, artistInfo);
|
||||
else
|
||||
updateArtistIfNeeded(artist, artistInfo);
|
||||
|
||||
artists.emplace_back(std::move(artist));
|
||||
continue;
|
||||
@@ -430,9 +459,7 @@ MediaScanner::scan(boost::system::error_code err)
|
||||
|
||||
LMS_LOG(DBUPDATER, INFO) << "Scan " << (_running ? "complete" : "aborted") << ". Changes = " << stats.nbChanges() << " (added = " << stats.additions << ", removed = " << stats.deletions << ", updated = " << stats.updates << "), Not changed = " << stats.skips << ", Scanned = " << stats.scans << " (errors = " << stats.errors.size() << "), features fetched = " << stats.featuresFetched << "/" << stats.featuresToFetch <<", duplicates = " << stats.duplicates.size();
|
||||
|
||||
LMS_LOG(DBUPDATER, INFO) << "Optimizing db...";
|
||||
_dbSession.optimize();
|
||||
LMS_LOG(DBUPDATER, INFO) << "Optimize db done!";
|
||||
|
||||
if (_running)
|
||||
{
|
||||
|
||||
@@ -100,6 +100,7 @@ getStreamParameters(RequestContext& context)
|
||||
|
||||
transcodeParameters.bitrate = *maxBitRate * 1000;
|
||||
transcodeParameters.encoding = userTranscodeFormatToAvEncoding(user->getAudioTranscodeFormat());
|
||||
transcodeParameters.stripMetadata = false; // We want clients to use metadata (offline use, replay gain, etc.)
|
||||
|
||||
parameters.transcodeParameters = std::move(transcodeParameters);
|
||||
}
|
||||
|
||||
@@ -901,7 +901,7 @@ handleGetArtistsRequest(RequestContext& context)
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError {};
|
||||
|
||||
auto artists {Artist::getAll(context.dbSession)};
|
||||
auto artists {Artist::getAll(context.dbSession, Artist::SortMethod::BySortName)};
|
||||
for (const Artist::pointer& artist : artists)
|
||||
indexNode.addArrayChild("artist", artistToResponseNode(user, artist, true /* id3 */));
|
||||
|
||||
@@ -932,7 +932,7 @@ handleGetMusicDirectoryRequest(RequestContext& context)
|
||||
{
|
||||
directoryNode.setAttribute("name", "Music");
|
||||
|
||||
auto artists {Artist::getAll(context.dbSession)};
|
||||
auto artists {Artist::getAll(context.dbSession, Artist::SortMethod::BySortName)};
|
||||
for (const Artist::pointer& artist : artists)
|
||||
directoryNode.addArrayChild("child", artistToResponseNode(user, artist, false /* no id3 */));
|
||||
|
||||
@@ -1028,7 +1028,7 @@ handleGetIndexesRequest(RequestContext& context)
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError {};
|
||||
|
||||
auto artists {Artist::getAll(context.dbSession)};
|
||||
auto artists {Artist::getAll(context.dbSession, Artist::SortMethod::BySortName)};
|
||||
for (const Artist::pointer& artist : artists)
|
||||
indexNode.addArrayChild("artist", artistToResponseNode(user, artist, false /* no id3 */));
|
||||
|
||||
@@ -1317,7 +1317,7 @@ handleSearchRequestCommon(RequestContext& context, bool id3)
|
||||
|
||||
bool more;
|
||||
{
|
||||
auto artists {Artist::getByFilter(context.dbSession, {}, keywords, {}, artistOffset, artistCount, more)};
|
||||
auto artists {Artist::getByFilter(context.dbSession, {}, keywords, std::nullopt, Artist::SortMethod::BySortName, artistOffset, artistCount, more)};
|
||||
for (const Artist::pointer& artist : artists)
|
||||
searchResult2Node.addArrayChild("artist", artistToResponseNode(user, artist, id3));
|
||||
}
|
||||
|
||||
+4
-1
@@ -43,13 +43,15 @@ std::vector<std::string> generateWtConfig(std::string execPath)
|
||||
const std::filesystem::path wtConfigPath {ServiceProvider<IConfig>::get()->getPath("working-dir") / "wt_config.xml"};
|
||||
const std::filesystem::path wtLogFilePath {ServiceProvider<IConfig>::get()->getPath("log-file", "/var/log/lms.log")};
|
||||
const std::filesystem::path wtAccessLogFilePath {ServiceProvider<IConfig>::get()->getPath("access-log-file", "/var/log/lms.access.log")};
|
||||
const std::filesystem::path wtResourcesPath {ServiceProvider<IConfig>::get()->getPath("wt-resources", "/usr/share/Wt/resources")};
|
||||
|
||||
args.push_back(execPath);
|
||||
args.push_back("--config=" + wtConfigPath.string());
|
||||
args.push_back("--docroot=" + ServiceProvider<IConfig>::get()->getString("docroot"));
|
||||
args.push_back("--approot=" + ServiceProvider<IConfig>::get()->getString("approot"));
|
||||
args.push_back("--deploy-path=" + ServiceProvider<IConfig>::get()->getString("deploy-path", "/"));
|
||||
args.push_back("--resources-dir=" + ServiceProvider<IConfig>::get()->getString("wt-resources"));
|
||||
if (!wtResourcesPath.empty())
|
||||
args.push_back("--resources-dir=" + wtResourcesPath.string());
|
||||
|
||||
if (ServiceProvider<IConfig>::get()->getBool("tls-enable", false))
|
||||
{
|
||||
@@ -135,6 +137,7 @@ int main(int argc, char* argv[])
|
||||
{
|
||||
Database::Session session {database};
|
||||
session.prepareTables();
|
||||
session.optimize();
|
||||
}
|
||||
|
||||
UserInterface::LmsApplicationGroupContainer appGroups;
|
||||
|
||||
@@ -153,9 +153,9 @@ LmsApplication::LmsApplication(const Wt::WEnvironment& env,
|
||||
messageResourceBundle().use(appRoot() + "tracksinfo");
|
||||
|
||||
// Require js here to avoid async problems
|
||||
requireJQuery("/js/jquery-1.10.2.min.js");
|
||||
require("/js/mediaplayer.js");
|
||||
require("/js/bootstrap-notify.js");
|
||||
requireJQuery("js/jquery-1.10.2.min.js");
|
||||
require("js/mediaplayer.js");
|
||||
require("js/bootstrap-notify.js");
|
||||
|
||||
setTitle("LMS");
|
||||
|
||||
@@ -293,7 +293,7 @@ LmsApplication::handleException(LmsApplicationException& e)
|
||||
btn->clicked().connect([this]()
|
||||
{
|
||||
setConfirmCloseMessage("");
|
||||
redirect("/");
|
||||
redirect(".");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -302,7 +302,7 @@ LmsApplication::goHomeAndQuit()
|
||||
{
|
||||
setConfirmCloseMessage("");
|
||||
WApplication::quit("");
|
||||
redirect("/");
|
||||
redirect(".");
|
||||
}
|
||||
|
||||
enum IdxRoot
|
||||
|
||||
@@ -95,6 +95,7 @@ Artists::addSome()
|
||||
clusterIds,
|
||||
searchKeywords,
|
||||
linkModel->getValue(_linkType->currentIndex()),
|
||||
Artist::SortMethod::BySortName,
|
||||
_container->count(), 20, moreResults)};
|
||||
|
||||
for (const auto& artist : artists)
|
||||
|
||||
@@ -172,7 +172,7 @@ testSingleArtist(Session& session)
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
auto artists {Artist::getAll(session)};
|
||||
auto artists {Artist::getAll(session, Artist::SortMethod::ByName)};
|
||||
CHECK(artists.size() == 1);
|
||||
CHECK(artists.front().id() == artist.getId());
|
||||
|
||||
@@ -311,11 +311,11 @@ testSingleTrackSingleArtistMultiRoles(Session& session)
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
bool hasMore{};
|
||||
CHECK(Artist::getByFilter(session, {}, {}, {}, {}, {}, hasMore).size() == 1);
|
||||
CHECK(Artist::getByFilter(session, {}, {}, TrackArtistLink::Type::Artist, {}, {}, hasMore).size() == 1);
|
||||
CHECK(Artist::getByFilter(session, {}, {}, TrackArtistLink::Type::ReleaseArtist, {}, {}, hasMore).size() == 1);
|
||||
CHECK(Artist::getByFilter(session, {}, {}, TrackArtistLink::Type::Writer, {}, {}, hasMore).size() == 1);
|
||||
CHECK(Artist::getByFilter(session, {}, {}, TrackArtistLink::Type::Composer, {}, {}, hasMore).empty());
|
||||
CHECK(Artist::getByFilter(session, {}, {}, std::nullopt, Artist::SortMethod::ByName, {}, {}, hasMore).size() == 1);
|
||||
CHECK(Artist::getByFilter(session, {}, {}, TrackArtistLink::Type::Artist, Artist::SortMethod::ByName, {}, {}, hasMore).size() == 1);
|
||||
CHECK(Artist::getByFilter(session, {}, {}, TrackArtistLink::Type::ReleaseArtist, Artist::SortMethod::ByName, {}, {}, hasMore).size() == 1);
|
||||
CHECK(Artist::getByFilter(session, {}, {}, TrackArtistLink::Type::Writer, Artist::SortMethod::ByName, {}, {}, hasMore).size() == 1);
|
||||
CHECK(Artist::getByFilter(session, {}, {}, TrackArtistLink::Type::Composer, Artist::SortMethod::ByName, {}, {}, hasMore).empty());
|
||||
}
|
||||
|
||||
{
|
||||
@@ -369,7 +369,8 @@ testSingleTrackMultiArtists(Session& session)
|
||||
|
||||
CHECK(track->getArtists(TrackArtistLink::Type::Artist).size() == 2);
|
||||
CHECK(track->getArtists(TrackArtistLink::Type::ReleaseArtist).empty());
|
||||
CHECK(Artist::getAll(session).size() == 2);
|
||||
CHECK(Artist::getAll(session, Artist::SortMethod::ByName).size() == 2);
|
||||
CHECK(Artist::getAllIds(session).size() == 2);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -385,6 +386,65 @@ testSingleTrackMultiArtists(Session& session)
|
||||
}
|
||||
}
|
||||
|
||||
static
|
||||
void
|
||||
testSingleArtistSearchByName(Session& session)
|
||||
{
|
||||
ScopedArtist artist {session, "AAA"};
|
||||
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
artist.get().modify()->setSortName("ZZZ");
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
bool more {};
|
||||
CHECK(Artist::getByFilter(session, {}, {"N"}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, std::nullopt, more).empty());
|
||||
|
||||
const auto artistsByAAA {Artist::Artist::getByFilter(session, {}, {"A"}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, std::nullopt, more)};
|
||||
CHECK(artistsByAAA.size() == 1);
|
||||
CHECK(artistsByAAA.front().id() == artist.getId());
|
||||
|
||||
const auto artistsByZZZ {Artist::Artist::getByFilter(session, {}, {"Z"}, std::nullopt, Artist::SortMethod::ByName, std::nullopt, std::nullopt, more)};
|
||||
CHECK(artistsByZZZ.size() == 1);
|
||||
CHECK(artistsByZZZ.front().id() == artist.getId());
|
||||
|
||||
CHECK(Artist::getByName(session, "NNN").empty());
|
||||
}
|
||||
}
|
||||
|
||||
static
|
||||
void
|
||||
testMultiArtistsSortMethod(Session& session)
|
||||
{
|
||||
ScopedArtist artistA {session, "artistA"};
|
||||
ScopedArtist artistB {session, "artistB"};
|
||||
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
artistA.get().modify()->setSortName("sortNameB");
|
||||
artistB.get().modify()->setSortName("sortNameA");
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
auto allArtistsByName {Artist::getAll(session, Artist::SortMethod::ByName)};
|
||||
auto allArtistsBySortName {Artist::getAll(session, Artist::SortMethod::BySortName)};
|
||||
|
||||
CHECK(allArtistsByName.size() == 2);
|
||||
CHECK(allArtistsByName.front().id() == artistA.getId());
|
||||
CHECK(allArtistsByName.back().id() == artistB.getId());
|
||||
|
||||
CHECK(allArtistsBySortName.size() == 2);
|
||||
CHECK(allArtistsBySortName.front().id() == artistB.getId());
|
||||
CHECK(allArtistsBySortName.back().id() == artistA.getId());
|
||||
}
|
||||
}
|
||||
|
||||
static
|
||||
void
|
||||
testSingleTrackSingleRelease(Session& session)
|
||||
@@ -699,12 +759,12 @@ testSingleTrackSingleArtistMultiClusters(Session& session)
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
auto artists {Artist::getByClusters(session, {cluster1.getId()})};
|
||||
auto artists {Artist::getByClusters(session, {cluster1.getId()}, Artist::SortMethod::ByName)};
|
||||
CHECK(artists.size() == 1);
|
||||
CHECK(artists.front().id() == artist.getId());
|
||||
|
||||
CHECK(Artist::getByClusters(session, {cluster2.getId()}).empty());
|
||||
CHECK(Artist::getByClusters(session, {cluster3.getId()}).empty());
|
||||
CHECK(Artist::getByClusters(session, {cluster2.getId()}, Artist::SortMethod::ByName).empty());
|
||||
CHECK(Artist::getByClusters(session, {cluster3.getId()}, Artist::SortMethod::ByName).empty());
|
||||
|
||||
cluster2.get().modify()->addTrack(track.get());
|
||||
}
|
||||
@@ -712,19 +772,19 @@ testSingleTrackSingleArtistMultiClusters(Session& session)
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
auto artists {Artist::getByClusters(session, {cluster1.getId()})};
|
||||
auto artists {Artist::getByClusters(session, {cluster1.getId()}, Artist::SortMethod::ByName)};
|
||||
CHECK(artists.size() == 1);
|
||||
CHECK(artists.front().id() == artist.getId());
|
||||
|
||||
artists = Artist::getByClusters(session, {cluster2.getId()});
|
||||
artists = Artist::getByClusters(session, {cluster2.getId()}, Artist::SortMethod::ByName);
|
||||
CHECK(artists.size() == 1);
|
||||
CHECK(artists.front().id() == artist.getId());
|
||||
|
||||
artists = Artist::getByClusters(session, {cluster1.getId(), cluster2.getId()});
|
||||
artists = Artist::getByClusters(session, {cluster1.getId(), cluster2.getId()}, Artist::SortMethod::ByName);
|
||||
CHECK(artists.size() == 1);
|
||||
CHECK(artists.front().id() == artist.getId());
|
||||
|
||||
CHECK(Artist::getByClusters(session, {cluster3.getId()}).empty());
|
||||
CHECK(Artist::getByClusters(session, {cluster3.getId()}, Artist::SortMethod::ByName).empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -755,7 +815,7 @@ testSingleTrackSingleArtistMultiRolesMultiClusters(Session& session)
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
auto artists {Artist::getByClusters(session, {cluster.getId()})};
|
||||
auto artists {Artist::getByClusters(session, {cluster.getId()}, Artist::SortMethod::ByName)};
|
||||
CHECK(artists.size() == 1);
|
||||
CHECK(artists.front().id() == artist.getId());
|
||||
}
|
||||
@@ -799,7 +859,7 @@ testMultiTracksSingleArtistMultiClusters(Session& session)
|
||||
std::set<IdType> clusterIds;
|
||||
std::transform(std::cbegin(clusters), std::cend(clusters), std::inserter(clusterIds, std::begin(clusterIds)), [](const ScopedCluster& cluster) { return cluster.getId(); });
|
||||
|
||||
auto artists {Artist::getByClusters(session, clusterIds)};
|
||||
auto artists {Artist::getByClusters(session, clusterIds, Artist::SortMethod::ByName)};
|
||||
CHECK(artists.size() == 1);
|
||||
CHECK(artists.front().id() == artist.getId());
|
||||
}
|
||||
@@ -914,7 +974,7 @@ testSingleTrackSingleReleaseSingleArtistSingleCluster(Session& session)
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
auto artists {Artist::getByClusters(session, {cluster.getId()})};
|
||||
auto artists {Artist::getByClusters(session, {cluster.getId()}, Artist::SortMethod::ByName)};
|
||||
CHECK(artists.size() == 1);
|
||||
CHECK(artists.front().id() == artist.getId());
|
||||
|
||||
@@ -1346,7 +1406,7 @@ testDatabaseEmpty(Session& session)
|
||||
{
|
||||
auto uniqueTransaction {session.createUniqueTransaction()};
|
||||
|
||||
CHECK(Artist::getAll(session).empty());
|
||||
CHECK(Artist::getAll(session, Artist::SortMethod::ByName).empty());
|
||||
CHECK(Cluster::getAll(session).empty());
|
||||
CHECK(ClusterType::getAll(session).empty());
|
||||
CHECK(Release::getAll(session).empty());
|
||||
@@ -1397,6 +1457,9 @@ int main()
|
||||
RUN_TEST(testSingleTrackSingleArtistMultiRoles);
|
||||
RUN_TEST(testSingleTrackMultiArtists);
|
||||
|
||||
RUN_TEST(testSingleArtistSearchByName);
|
||||
RUN_TEST(testMultiArtistsSortMethod);
|
||||
|
||||
RUN_TEST(testSingleTrackSingleRelease);
|
||||
|
||||
RUN_TEST(testSingleTrackSingleCluster);
|
||||
|
||||
@@ -5,6 +5,7 @@ add_executable(lms-metadata
|
||||
|
||||
target_link_libraries(lms-metadata PRIVATE
|
||||
lmsmetadata
|
||||
lmsutils
|
||||
)
|
||||
|
||||
install(TARGETS lms-metadata DESTINATION bin)
|
||||
|
||||
@@ -36,6 +36,9 @@ std::ostream& operator<<(std::ostream& os, const MetaData::Artist& artist)
|
||||
if (artist.musicBrainzArtistID)
|
||||
os << " (" << artist.musicBrainzArtistID->getAsString() << ")";
|
||||
|
||||
if (artist.sortName)
|
||||
os << " '" << *artist.sortName << "'";
|
||||
|
||||
return os;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user