Merge branch 'develop' for release v3.34.0

This commit is contained in:
emeric
2022-12-14 22:57:39 +01:00
42 changed files with 540 additions and 126 deletions
+56
View File
@@ -0,0 +1,56 @@
name: "CodeQL"
on:
push:
branches: [ "master", "develop" ]
pull_request:
branches: [ "master", "develop" ]
schedule:
- cron: "48 10 * * 4"
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ javascript, cpp ]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install Packages (cpp)
if: ${{ matrix.language == 'cpp' }}
run: |
sudo apt-get update
sudo apt-get install --yes build-essential cmake libboost-all-dev libconfig++-dev libavcodec-dev libavutil-dev libavformat-dev libstb-dev libtag1-dev libpam0g-dev libgtest-dev
export WT_VERSION=4.7.2
export WT_INSTALL_PREFIX=/usr
git clone https://github.com/emweb/wt.git wt
pushd wt
git checkout ${WT_VERSION}
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${WT_INSTALL_PREFIX} -DBUILD_EXAMPLES=OFF -DENABLE_LIBWTTEST=OFF -DCONNECTOR_FCGI=OFF
make
sudo make install
popd
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
queries: +security-and-quality
- name: Autobuild
uses: github/codeql-action/autobuild@v2
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
category: "/language:${{ matrix.language }}"
+2 -3
View File
@@ -1,16 +1,15 @@
# LMS - Lightweight Music Server
[![Last Release](https://img.shields.io/github/v/release/epoupon/lms?logo=github&label=latest)](https://github.com/epoupon/lms/releases) [![Build](https://img.shields.io/github/workflow/status/epoupon/lms/Build?logo=github)](https://github.com/epoupon/lms/actions) [![Language grade: C/C++](https://img.shields.io/lgtm/grade/cpp/g/epoupon/lms.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/epoupon/lms/context:cpp)
[![Last Release](https://img.shields.io/github/v/release/epoupon/lms?logo=github&label=latest)](https://github.com/epoupon/lms/releases) [![Build](https://img.shields.io/github/workflow/status/epoupon/lms/Build?logo=github)](https://github.com/epoupon/lms/actions)
_LMS_ is a self-hosted music streaming software: access your music collection from anywhere using a web interface!
A [demo instance](http://lms-demo.poupon.dev) is available. Note the administration panel is not available.
## Main features
* Low memory requirements: the demo instance runs on a _Raspberry Pi Zero W_
* Recommendation engine
* Audio transcode for maximum interoperability and low bandwith requirements
* Multi-value tags: artists, genres, composers, lyricists, moods, ...
* Multi-value tags: `artist`, `genre`, `composer`, `lyricist`, `mood`, `performer`, ...
* [MusicBrainz Identifier](https://musicbrainz.org/doc/MusicBrainz_Identifier) support to handle duplicated artist and release names
* [ListenBrainz](https://listenbrainz.org) support for:
* Scrobbling and synchronizing listens
+3 -3
View File
@@ -21,15 +21,15 @@
</div>
</div>
${<if-has-releases>}
${<if-section-titles>}<h3>${tr:Lms.Explore.releases}</h3>${</if-section-titles>}
${<if-release-title>}<h3>${tr:Lms.Explore.releases}</h3>${</if-release-title>}
${releases class="mb-3"}
${</if-has-releases>}
${<if-has-appears-on-releases>}
${<if-section-titles>}<h3>${tr:Lms.Explore.Artist.appears-on}</h3>${</if-section-titles>}
<h3>${tr:Lms.Explore.Artist.appears-on}</h3>
${appears-on-releases class="mb-3"}
${</if-has-appears-on-releases>}
${<if-has-non-release-tracks>}
${<if-section-titles>}<h3>${tr:Lms.Explore.tracks}</h3>${</if-section-titles>}
<h3>${tr:Lms.Explore.tracks}</h3>
${tracks class="mb-3"}
${</if-has-non-release-tracks>}
${<if-has-similar-artists>}
+1 -1
View File
@@ -43,7 +43,7 @@
<message id="Lms.Explore.template.info.artists">
<div class="row align-items-center">
<div class="col-4 col-sm-3">
<div class="col-4">
${type}
</div>
<div class="col">
+4
View File
@@ -175,6 +175,10 @@
<plural case="0">Mixer</plural>
<plural case="1">Mixers</plural>
</message>
<message id="Lms.Explore.Artists.linktype-performer">
<plural case="0">Performer</plural>
<plural case="1">Performers</plural>
</message>
<message id="Lms.Explore.Artists.linktype-producer">
<plural case="0">Producer</plural>
<plural case="1">Producers</plural>
+4
View File
@@ -175,6 +175,10 @@
<plural case="0">Mixeur</plural>
<plural case="1">Mixeurs</plural>
</message>
<message id="Lms.Explore.Artists.linktype-performer">
<plural case="0">Interprète</plural>
<plural case="1">Interprètes</plural>
</message>
<message id="Lms.Explore.Artists.linktype-producer">
<plural case="0">Producteur</plural>
<plural case="1">Producteurs</plural>
+4
View File
@@ -183,6 +183,10 @@
+4
View File
@@ -183,6 +183,10 @@
+3 -3
View File
@@ -87,7 +87,7 @@
<message id="Lms.Explore.Release.template.release-info">
<div class="modal fade" tabindex="-1">
<div class="modal-dialog">
<div class="modal-dialog modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">${tr:Lms.Explore.release-info}</h5>
@@ -98,7 +98,7 @@
${artist-table class="d-grid gap-2"}
${<if-has-codec>}
<div class="row align-items-center">
<div class="col-4 col-sm-3">
<div class="col-4">
${tr:Lms.Explore.codec}
</div>
<div class="col">
@@ -108,7 +108,7 @@
${</if-has-codec>}
${<if-has-bitrate>}
<div class="row align-items-center">
<div class="col-4 col-sm-3">
<div class="col-4">
${tr:Lms.Explore.bitrate}
</div>
<div class="col">
+5 -5
View File
@@ -70,7 +70,7 @@
<message id="Lms.Explore.Tracks.template.track-info">
<div class="modal fade" tabindex="-1">
<div class="modal-dialog">
<div class="modal-dialog modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">${tr:Lms.Explore.track-info}</h5>
@@ -80,7 +80,7 @@
<div class="container-fluid d-grid gap-2">
${<if-has-clusters>}
<div class="row align-items-center">
<div class="col-4 col-sm-3">
<div class="col-4">
${tr:Lms.clusters}
</div>
<div class="col">
@@ -90,7 +90,7 @@
${</if-has-clusters>}
${artist-table class="d-grid gap-2"}
<div class="row align-items-center">
<div class="col-4 col-sm-3">
<div class="col-4">
${tr:Lms.Explore.duration}
</div>
<div class="col">
@@ -99,7 +99,7 @@
</div>
${<if-has-codec>}
<div class="row align-items-center">
<div class="col-4 col-sm-3">
<div class="col-4">
${tr:Lms.Explore.codec}
</div>
<div class="col">
@@ -109,7 +109,7 @@
${</if-has-codec>}
${<if-has-bitrate>}
<div class="row align-items-center">
<div class="col-4 col-sm-3">
<div class="col-4">
${tr:Lms.Explore.bitrate}
</div>
<div class="col">
+52 -12
View File
@@ -132,6 +132,48 @@ getArtists(const TagLib::PropertyMap& properties,
return artists;
}
static
PerformerContainer
getPerformerArtists(const TagLib::PropertyMap& properties,
const std::vector<std::string_view>& artistTagNames)
{
PerformerContainer performers;
// picard stores like this: (see https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html#performer)
// PERFORMER: artist (role)
if (const std::vector<std::string> artistNames {getPropertyValuesFirstMatchAs<std::string>(properties, artistTagNames)}; !artistNames.empty())
{
for (std::string_view entry : artistNames)
{
Utils::PerformerArtist performer {Utils::extractPerformerAndRole(entry)};
StringUtils::capitalize(performer.role);
performers[performer.role].push_back(std::move(performer.artist));
}
}
// PERFORMER:role (MP3)
else
{
for (const auto& [key, values] : properties)
{
if (key.startsWith("PERFORMER"))
{
std::string performerStr {key.to8Bit(true)};
std::string role;
if (const std::size_t rolePos {performerStr.find(':')}; rolePos != std::string::npos)
{
role = StringUtils::stringToLower(performerStr.substr(rolePos + 1, performerStr.size() - rolePos + 1));
StringUtils::capitalize(role);
}
for (const auto& value : values)
performers[role].push_back(Artist {value.to8Bit(true)});
}
}
}
return performers;
}
static
std::optional<Album>
getAlbum(const TagLib::PropertyMap& properties)
@@ -140,7 +182,7 @@ getAlbum(const TagLib::PropertyMap& properties)
if (albumName.empty())
return std::nullopt;
const std::vector<UUID> albumMBID {getPropertyValuesFirstMatchAs<UUID>(properties, {"MUSICBRAINZ_ALBUMID", "MUSICBRAINZ ALBUM ID"})};
const std::vector<UUID> albumMBID {getPropertyValuesFirstMatchAs<UUID>(properties, {"MUSICBRAINZ_ALBUMID", "MUSICBRAINZ ALBUM ID", "MUSICBRAINZ/ALBUM ID"})};
if (albumMBID.empty())
return Album {std::move(albumName.front()), {}};
@@ -187,12 +229,14 @@ TagLibParser::processTag(Track& track, const std::string& tag, const TagLib::Str
if (tag == "TITLE")
track.title = value;
else if (tag == "MUSICBRAINZ_RELEASETRACKID"
|| tag == "MUSICBRAINZ RELEASE TRACK ID")
|| tag == "MUSICBRAINZ RELEASE TRACK ID"
|| tag == "MUSICBRAINZ/RELEASE TRACK ID")
{
track.trackMBID = UUID::fromString(value);
}
else if (tag == "MUSICBRAINZ_TRACKID"
|| tag == "MUSICBRAINZ TRACK ID")
|| tag == "MUSICBRAINZ TRACK ID"
|| tag == "MUSICBRAINZ/TRACK ID")
track.recordingMBID = UUID::fromString(value);
else if (tag == "ACOUSTID_ID")
track.acoustID = UUID::fromString(value);
@@ -416,23 +460,19 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
track.hasCover = true;
}
for (const auto& property : properties)
{
const std::string tag {property.first.upper().to8Bit(true)};
const TagLib::StringList& values {property.second};
processTag(track, tag, values, debug);
}
for (const auto& [tag, values] : properties)
processTag(track, tag.upper().to8Bit(true), values, debug);
track.album = getAlbum(properties);
track.artists = getArtists(properties, {"ARTISTS", "ARTIST"}, {"ARTISTSORT"}, {"MUSICBRAINZ_ARTISTID", "MUSICBRAINZ ARTIST ID"});
track.albumArtists = getArtists(properties, {"ALBUMARTISTS", "ALBUMARTIST"}, {"ALBUMARTISTSSORT", "ALBUMARTISTSORT"}, {"MUSICBRAINZ_ALBUMARTISTID", "MUSICBRAINZ ALBUM ARTIST ID"});
track.artists = getArtists(properties, {"ARTISTS", "ARTIST"}, {"ARTISTSORT"}, {"MUSICBRAINZ_ARTISTID", "MUSICBRAINZ ARTIST ID", "MUSICBRAINZ/ARTIST ID"});
track.albumArtists = getArtists(properties, {"ALBUMARTISTS", "ALBUMARTIST"}, {"ALBUMARTISTSSORT", "ALBUMARTISTSORT"}, {"MUSICBRAINZ_ALBUMARTISTID", "MUSICBRAINZ ALBUM ARTIST ID", "MUSICBRAINZ/ALBUM ARTIST ID"});
track.conductorArtists = getArtists(properties, {"CONDUCTORS", "CONDUCTOR"}, {"CONDUCTORSSORT", "CONDUCTORSORT"}, {});
track.composerArtists = getArtists(properties, {"COMPOSERS", "COMPOSER"}, {"COMPOSERSSORT", "COMPOSERSORT"}, {});
track.lyricistArtists = getArtists(properties, {"LYRICISTS", "LYRICIST"}, {"LYRICISTSSORT", "LYRICISTSORT"}, {});
track.mixerArtists = getArtists(properties, {"MIXERS", "MIXER"}, {"MIXERSSORT", "MIXERSORT"}, {});
track.producerArtists = getArtists(properties, {"PRODUCERS", "PRODUCER"}, {"PRODUCERSSORT", "PRODUCERSORT"}, {});
track.remixerArtists = getArtists(properties, {"REMIXERS", "REMIXER", "ModifiedBy"}, {"REMIXERSSORT", "REMIXERSORT"}, {});
track.performerArtists = getPerformerArtists(properties, {"PERFORMERS", "PERFORMER"});
return track;
}
+46
View File
@@ -71,5 +71,51 @@ namespace MetaData::Utils
throw LmsException {"Unknown read style"};
}
PerformerArtist
extractPerformerAndRole(std::string_view entry)
{
std::string_view artistName;
std::string_view role;
std::size_t roleBegin {};
std::size_t roleEnd {};
std::size_t count {};
for (std::size_t i {}; i < entry.size(); ++i)
{
std::size_t currentIndex {entry.size() - i - 1};
const char c {entry[currentIndex]};
if (std::isspace(c))
continue;
if (c == ')')
{
if (count++ == 0)
roleEnd = currentIndex;
}
else if (c == '(')
{
if (count == 0)
break;
if (--count == 0)
{
roleBegin = currentIndex + 1;
role = StringUtils::stringTrim(entry.substr(roleBegin, roleEnd - roleBegin));
artistName = StringUtils::stringTrim(entry.substr(0, currentIndex));
break;
}
}
else if (count == 0)
break;
}
if (!roleEnd || !roleBegin)
artistName = StringUtils::stringTrim(entry);
return PerformerArtist {Artist {artistName}, std::string {role}};
}
}
+12
View File
@@ -18,6 +18,9 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <string_view>
#include <Wt/WDate.h>
#include "metadata/IParser.hpp"
@@ -26,5 +29,14 @@ namespace MetaData::Utils
{
Wt::WDate parseDate(const std::string& dateStr);
std::string_view readStyleToString(ParserReadStyle readStyle);
struct PerformerArtist
{
Artist artist;
std::string role;
};
// format is "artist name (role)"
PerformerArtist extractPerformerAndRole(std::string_view entry);
}
@@ -24,6 +24,7 @@
#include <map>
#include <optional>
#include <set>
#include <string>
#include <string_view>
#include <vector>
@@ -44,6 +45,8 @@ namespace MetaData
Artist(std::string_view _name, std::optional<std::string> _sortName, std::optional<UUID> _musicBrainzArtistID) : name {_name}, sortName {_sortName}, musicBrainzArtistID {_musicBrainzArtistID} {}
};
using PerformerContainer = std::map<std::string /*role*/, std::vector<Artist>>;
struct Album
{
std::string name;
@@ -83,6 +86,7 @@ namespace MetaData
std::vector<Artist> composerArtists;
std::vector<Artist> lyricistArtists;
std::vector<Artist> mixerArtists;
PerformerContainer performerArtists;
std::vector<Artist> producerArtists;
std::vector<Artist> remixerArtists;
};
+44 -1
View File
@@ -62,7 +62,6 @@ TEST(MetaData, parseDate)
{ "1995-05/09", Wt::WDate {} }, // invalid mixup separators
};
for (const TestCase& testCase : testCases)
{
const Wt::WDate parsed {parseDate(testCase.str)};
@@ -72,3 +71,47 @@ TEST(MetaData, parseDate)
EXPECT_EQ(parsed.day(), testCase.result.day()) << " str was '" << testCase.str << "'";
}
}
TEST(MetaData, extractPerformerAndRole)
{
using namespace MetaData::Utils;
struct TestCase
{
std::string str;
std::string expectedArtistName;
std::string expectedRole;
} testCases []
{
{ "", "", "" },
{ "(myrole)", "", "myrole" },
{ "(my role)", "", "my role" },
{ " ( my role ) ", "", "my role" },
{ " (()) ", "", "()" },
{ ")", ")", "" },
{ "(", "(", "" },
{ "artist name (my role)", "artist name", "my role" },
{ "artist name ()", "artist name", "" },
{ "artist name ( )", "artist name", "" },
{ "artist (subname) name", "artist (subname) name", "" },
{ " artist name ( my role )", "artist name", "my role" },
{ "artist name (artist subname) (my role)", "artist name (artist subname)", "my role" },
{ "artist name", "artist name", "" },
{ " artist name ", "artist name", "" },
{ "artist name (", "artist name (", "" },
{ "artist name )", "artist name )", "" },
{ "artist name (()", "artist name (", "" },
{ "artist name (())", "artist name", "()" },
{ "artist name ( () )", "artist name", "()" },
{ "artist name (drums (drum set))", "artist name", "drums (drum set)" },
{ "artist name ( drums (drum set) )", "artist name", "drums (drum set)" },
};
for (const TestCase& testCase : testCases)
{
PerformerArtist performer {extractPerformerAndRole(testCase.str)};
EXPECT_EQ(performer.artist.name, testCase.expectedArtistName) << " str was '" << testCase.str << "'";
EXPECT_EQ(performer.role, testCase.expectedRole) << " str was '" << testCase.str << "'";
}
}
+13 -1
View File
@@ -467,7 +467,7 @@ CREATE TABLE "starred_track" (
return itScrobbler->second;
auto query {session.getDboSession().query<Scrobbler>("SELECT scrobbler FROM user WHERE id = ?").bind(userId)};
auto [itInserted, inserted] {userScrobblers.emplace(userId, query.resultValue())};
[[maybe_unused]] auto [itInserted, inserted] {userScrobblers.emplace(userId, query.resultValue())};
assert(inserted);
return itInserted->second;
}};
@@ -600,6 +600,17 @@ CREATE TABLE IF NOT EXISTS "track_artist_link_backup" (
ScanSettings::get(session).modify()->incScanVersion();
}
static
void
migrateFromV37(Session& session)
{
// Support Performer tags (via subtypes)
session.getDboSession().execute("ALTER TABLE track_artist_link ADD subtype TEXT");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(session).modify()->incScanVersion();
}
void
doDbMigration(Session& session)
{
@@ -643,6 +654,7 @@ CREATE TABLE IF NOT EXISTS "track_artist_link_backup" (
{34, migrateFromV34},
{35, migrateFromV35},
{36, migrateFromV36},
{37, migrateFromV37},
};
while (1)
@@ -26,7 +26,7 @@ namespace Database
class Session;
using Version = std::size_t;
static constexpr Version LMS_DATABASE_VERSION {37};
static constexpr Version LMS_DATABASE_VERSION {38};
class VersionInfo
{
public:
@@ -24,36 +24,78 @@
#include "services/database/Track.hpp"
#include "IdTypeTraits.hpp"
#include "Utils.hpp"
namespace Database {
TrackArtistLink::TrackArtistLink(ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type)
: _type {type},
_track {getDboPtr(track)},
_artist {getDboPtr(artist)}
namespace Database
{
}
TrackArtistLink::pointer
TrackArtistLink::create(Session& session, ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type)
{
session.checkUniqueLocked();
TrackArtistLink::pointer res {session.getDboSession().add(std::make_unique<TrackArtistLink>(track, artist, type))};
session.getDboSession().flush();
return res;
}
EnumSet<TrackArtistLinkType>
TrackArtistLink::findUsedTypes(Session& session)
{
session.checkSharedLocked();
auto res {session.getDboSession().query<TrackArtistLinkType>("SELECT DISTINCT type from track_artist_link").resultList()};
return EnumSet<TrackArtistLinkType>(std::begin(res), std::end(res));
}
static
Wt::Dbo::Query<TrackArtistLinkId>
createQuery(Session& session, const TrackArtistLink::FindParameters& params)
{
session.checkSharedLocked();
auto query {session.getDboSession().query<TrackArtistLinkId>("SELECT DISTINCT t_a_l.id FROM track_artist_link t_a_l")};
if (params.linkType)
query.where("t_a_l.type = ?").bind(*params.linkType);
if (params.track.isValid() || params.release.isValid())
query.join("track t ON t.id = t_a_l.track_id");
if (params.track.isValid())
query.where("t.id = ?").bind(params.track);
if (params.release.isValid())
query.where("t.release_id = ?").bind(params.release);
return query;
}
TrackArtistLink::TrackArtistLink(ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType)
: _type {type}
, _subType {subType}
, _track {getDboPtr(track)}
, _artist {getDboPtr(artist)}
{
}
TrackArtistLink::pointer
TrackArtistLink::create(Session& session, ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType)
{
session.checkUniqueLocked();
TrackArtistLink::pointer res {session.getDboSession().add(std::make_unique<TrackArtistLink>(track, artist, type, subType))};
session.getDboSession().flush();
return res;
}
TrackArtistLink::pointer
TrackArtistLink::find(Session& session, TrackArtistLinkId id)
{
session.checkSharedLocked();
return session.getDboSession().find<TrackArtistLink>().where("id = ?").bind(id).resultValue();
}
RangeResults<TrackArtistLinkId>
TrackArtistLink::find(Session& session, const FindParameters& params)
{
session.checkSharedLocked();
auto query {createQuery(session, params)};
return Utils::execQuery(query, params.range);
}
EnumSet<TrackArtistLinkType>
TrackArtistLink::findUsedTypes(Session& session)
{
session.checkSharedLocked();
auto res {session.getDboSession().query<TrackArtistLinkType>("SELECT DISTINCT type from track_artist_link").resultList()};
return EnumSet<TrackArtistLinkType>(std::begin(res), std::end(res));
}
}
@@ -87,7 +87,6 @@ class Artist : public Object<Artist, ArtistId>
static RangeResults<ArtistId> findAllOrphans(Session& session, Range range); // No track related
static bool exists(Session& session, ArtistId id);
// Accessors
const std::string& getName() const { return _name; }
const std::string& getSortName() const { return _sortName; }
@@ -20,11 +20,14 @@
#pragma once
#include <string>
#include <string_view>
#include <Wt/Dbo/Dbo.h>
#include "services/database/IdType.hpp"
#include "services/database/Object.hpp"
#include "services/database/ReleaseId.hpp"
#include "services/database/TrackId.hpp"
#include "services/database/Types.hpp"
#include "utils/EnumSet.hpp"
@@ -40,31 +43,48 @@ namespace Database
class TrackArtistLink : public Object<TrackArtistLink, TrackArtistLinkId>
{
public:
TrackArtistLink() = default;
TrackArtistLink(ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type);
struct FindParameters
{
Range range;
std::optional<TrackArtistLinkType> linkType; // if set, only artists that have produced at least one track with this link type
TrackId track; // artists involved in this track
ReleaseId release; // artists involved in this release
static pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type);
FindParameters& setRange(Range _range) {range = _range; return *this; }
FindParameters& setLinkType(std::optional<TrackArtistLinkType> _linkType) { linkType = _linkType; return *this; }
FindParameters& setTrack(TrackId _track) { track = _track; return *this; }
FindParameters& setRelease(ReleaseId _release) { release = _release; return *this; }
};
TrackArtistLink() = default;
TrackArtistLink(ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType);
static RangeResults<TrackArtistLinkId> find(Session& session, const FindParameters& parameters);
static pointer find(Session& session, TrackArtistLinkId linkId);
static pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType = {});
static EnumSet<TrackArtistLinkType> findUsedTypes(Session& session);
ObjectPtr<Track> getTrack() const { return _track; }
ObjectPtr<Artist> getArtist() const { return _artist; }
TrackArtistLinkType getType() const { return _type; }
std::string_view getSubType() const { return _subType; }
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _type, "type");
void persist(Action& a)
{
Wt::Dbo::field(a, _type, "type");
Wt::Dbo::field(a, _subType, "subtype");
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade);
}
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade);
}
private:
TrackArtistLinkType _type;
std::string _subType;
Wt::Dbo::ptr<Track> _track;
Wt::Dbo::ptr<Artist> _artist;
};
}
@@ -53,8 +53,8 @@ TEST_F(DatabaseFixture, MultiTracksSingleArtistSingleRelease)
ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId());
const auto tracks {Track::find(session, Track::FindParameters {}.setRelease(release.getId()))};
EXPECT_EQ(tracks.results.size(), nbTracks);
const auto releaseTracks {Track::find(session, Track::FindParameters {}.setRelease(release.getId()))};
EXPECT_EQ(releaseTracks.results.size(), nbTracks);
}
}
+5 -3
View File
@@ -39,9 +39,11 @@ TEST_F(DatabaseFixture, Release)
EXPECT_EQ(Release::getCount(session), 1);
EXPECT_TRUE(Release::exists(session, release.getId()));
auto releases {Release::findOrphans(session, Range {})};
ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId());
{
auto releases {Release::findOrphans(session, Range {})};
ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId());
}
{
auto releases {Release::find(session, Release::FindParameters {})};
@@ -410,7 +410,7 @@ FeaturesEngine::load(const SOM::Network& network, const TrackPositions& trackPos
auto itArtists {_artistMatrix.find(artistLink->getType())};
if (itArtists == std::cend(_artistMatrix))
{
auto [it, inserted] = _artistMatrix.try_emplace(artistLink->getType(), ArtistMatrix {width, height});
[[maybe_unused]] auto [it, inserted] = _artistMatrix.try_emplace(artistLink->getType(), ArtistMatrix {width, height});
assert(inserted);
itArtists = it;
}
@@ -837,6 +837,12 @@ ScannerService::scanAudioFile(const std::filesystem::path& file, bool forceScan,
for (const Artist::pointer& mixer : getOrCreateArtists(_dbSession, trackInfo->mixerArtists, true))
track.modify()->addArtistLink(TrackArtistLink::create(_dbSession, track, mixer, TrackArtistLinkType::Mixer));
for (const auto& [role, performers] : trackInfo->performerArtists)
{
for (const Artist::pointer& performer : getOrCreateArtists(_dbSession, performers, true))
track.modify()->addArtistLink(TrackArtistLink::create(_dbSession, track, performer, TrackArtistLinkType::Performer, role));
}
for (const Artist::pointer& producer : getOrCreateArtists(_dbSession, trackInfo->producerArtists, true))
track.modify()->addArtistLink(TrackArtistLink::create(_dbSession, track, producer, TrackArtistLinkType::Producer));
@@ -17,6 +17,8 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "services/scrobbling/Exception.hpp"
namespace Scrobbling::ListenBrainz
@@ -23,7 +23,6 @@
#include <Wt/Json/Object.h>
#include <Wt/Json/Value.h>
#include <Wt/Json/Parser.h>
//#include <Wt/Json/Serializer.h>
#include "services/scrobbling/Exception.hpp"
#include "Exception.hpp"
@@ -17,6 +17,8 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <string_view>
#include "FeedbackTypes.hpp"
@@ -234,7 +234,7 @@ namespace Scrobbling::ListenBrainz
auto itContext {_userContexts.find(userId)};
if (itContext == std::cend(_userContexts))
{
auto [itNewContext, inserted] {_userContexts.emplace(userId, userId)};
[[maybe_unused]] auto [itNewContext, inserted] {_userContexts.emplace(userId, userId)};
itContext = itNewContext;
}
@@ -246,7 +246,7 @@ namespace Scrobbling::ListenBrainz
{
return std::any_of(std::cbegin(_userContexts), std::cend(_userContexts), [](const auto& contextEntry)
{
const auto& [userId, context] {contextEntry};
[[maybe_unused]] const auto& [userId, context] {contextEntry};
return context.syncing;
});
}
@@ -354,7 +354,7 @@ namespace Scrobbling::ListenBrainz
auto itContext {_userContexts.find(userId)};
if (itContext == std::cend(_userContexts))
{
auto [itNewContext, inserted] {_userContexts.emplace(userId, userId)};
[[maybe_unused]] auto [itNewContext, inserted] {_userContexts.emplace(userId, userId)};
itContext = itNewContext;
}
@@ -366,7 +366,7 @@ namespace Scrobbling::ListenBrainz
{
return std::any_of(std::cbegin(_userContexts), std::cend(_userContexts), [](const auto& contextEntry)
{
const auto& [userId, context] {contextEntry};
[[maybe_unused]] const auto& [userId, context] {contextEntry};
return context.syncing;
});
}
+7 -8
View File
@@ -993,10 +993,9 @@ handleGetMusicDirectoryRequest(RequestContext& context)
// Mandatory params
const auto artistId {getParameterAs<ArtistId>(context.parameters, "id")};
const auto releaseId {getParameterAs<ReleaseId>(context.parameters, "id")};
const auto trackId {getParameterAs<TrackId>(context.parameters, "id")};
const auto root {getParameterAs<RootId>(context.parameters, "id")};
if (!root && !artistId && !releaseId && !trackId)
if (!root && !artistId && !releaseId)
throw BadParameterGenericError {"id"};
Response response {Response::createOkResponse(context.serverProtocolVersion)};
@@ -1013,10 +1012,10 @@ handleGetMusicDirectoryRequest(RequestContext& context)
directoryNode.setAttribute("id", idToString(RootId {}));
directoryNode.setAttribute("name", "Music");
auto artistIds {Artist::find(context.dbSession, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::BySortName))};
for (const ArtistId artistId : artistIds.results)
auto rootArtistIds {Artist::find(context.dbSession, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::BySortName))};
for (const ArtistId rootArtistId : rootArtistIds.results)
{
const Artist::pointer artist {Artist::find(context.dbSession, artistId)};
const Artist::pointer artist {Artist::find(context.dbSession, rootArtistId)};
directoryNode.addArrayChild("child", artistToResponseNode(artist, context.dbSession, user, false /* no id3 */));
}
}
@@ -1030,10 +1029,10 @@ handleGetMusicDirectoryRequest(RequestContext& context)
directoryNode.setAttribute("name", makeNameFilesystemCompatible(artist->getName()));
const auto releases {Release::find(context.dbSession, Release::FindParameters {}.setArtist(*artistId))};
for (const ReleaseId releaseId : releases.results)
const auto artistReleases {Release::find(context.dbSession, Release::FindParameters {}.setArtist(*artistId))};
for (const ReleaseId artistReleaseId : artistReleases.results)
{
const Release::pointer release {Release::find(context.dbSession, releaseId)};
const Release::pointer release {Release::find(context.dbSession, artistReleaseId)};
directoryNode.addArrayChild("child", releaseToResponseNode(release, context.dbSession, user, false /* no id3 */));
}
}
+1 -1
View File
@@ -232,7 +232,7 @@ Response::writeJSON(std::ostream& os)
{
Json::Object res;
auto valueToJsonValue {[](const Node::Value& value) -> Json::Value
auto valueToJsonValue {[](const Node::ValueType& value) -> Json::Value
{
if (std::holds_alternative<std::string>(value))
return Json::Value {std::get<std::string>(value)};
+3 -3
View File
@@ -206,9 +206,9 @@ class Response
void setVersionAttribute(ProtocolVersion version);
friend class Response;
using Value = std::variant<std::string, bool, long long>;
std::map<std::string, Value> _attributes;
std::optional<Value> _value;
using ValueType = std::variant<std::string, bool, long long>;
std::map<std::string, ValueType> _attributes;
std::optional<ValueType> _value;
std::map<std::string, std::vector<Node>> _children;
std::map<std::string, std::vector<Node>> _childrenArrays;
};
+25 -6
View File
@@ -115,17 +115,21 @@ joinStrings(const std::vector<std::string>& strings, const std::string& delimite
return boost::algorithm::join(strings, delimiter);
}
std::string
std::string_view
stringTrim(std::string_view str, std::string_view whitespaces)
{
std::string_view res;
const auto strBegin = str.find_first_not_of(whitespaces);
if (strBegin == std::string_view::npos)
return ""; // no content
if (strBegin != std::string_view::npos)
{
const auto strEnd {str.find_last_not_of(whitespaces)};
const auto strRange {strEnd - strBegin + 1};
const auto strEnd = str.find_last_not_of(whitespaces);
const auto strRange = strEnd - strBegin + 1;
res = str.substr(strBegin, strRange);
}
return std::string {str.substr(strBegin, strRange)};
return res;
}
std::string
@@ -175,6 +179,21 @@ bufferToString(const std::vector<unsigned char>& data)
return oss.str();
}
void
capitalize(std::string& str)
{
for (auto it {std::begin(str)}; it != std::end(str); ++it)
{
if (std::isspace(*it))
continue;
if (std::isalpha(*it))
*it = std::toupper(*it);
break;
}
}
std::string
replaceInString(std::string_view str, const std::string& from, const std::string& to)
{
+4 -1
View File
@@ -44,7 +44,7 @@ std::string
joinStrings(const std::vector<std::string>& strings, const std::string& delimiter);
[[nodiscard]]
std::string
std::string_view
stringTrim(std::string_view str, std::string_view whitespaces = " \t");
[[nodiscard]]
@@ -66,6 +66,9 @@ stringToUpper(const std::string& str);
std::string
bufferToString(const std::vector<unsigned char>& data);
void
capitalize(std::string& str);
template<typename T>
[[nodiscard]]
std::optional<T> readAs(std::string_view str)
+29
View File
@@ -118,3 +118,32 @@ TEST(StringUtils, readAs)
EXPECT_EQ(StringUtils::readAs<bool>("foo"), std::nullopt);
EXPECT_EQ(StringUtils::readAs<bool>(""), std::nullopt);
}
TEST(StrinUtils, capitalize)
{
struct TestCase
{
std::string input;
std::string expectedOutput;
};
TestCase tests[]
{
{"", ""},
{"C", "C"},
{"c", "C"},
{" c", " C"},
{" cc", " Cc"},
{"(c", "(c"},
{"1c", "1c"},
{"&c", "&c"},
{"c c", "C c"}
};
for (const TestCase& test : tests)
{
std::string str {test.input};
StringUtils::capitalize(str);
EXPECT_EQ(str, test.expectedOutput) << " str was '" << test.input << "'";
}
}
+5 -10
View File
@@ -114,19 +114,14 @@ Artist::refreshView()
LmsApp->setTitle(artist->getName());
_artistId = *artistId;
std::size_t sectionCount{};
if (refreshReleases())
sectionCount++;
if (refreshAppearsOnReleases())
sectionCount++;
if (refreshNonReleaseTracks())
sectionCount++;
bool setReleaseTitle {};
refreshReleases();
setReleaseTitle |= refreshAppearsOnReleases();
setReleaseTitle |= refreshNonReleaseTracks();
refreshLinks(artist);
refreshSimilarArtists(similarArtistIds);
if (sectionCount > 1)
setCondition("if-section-titles", true);
setCondition("if-release-title", setReleaseTitle);
Wt::WContainerWidget* clusterContainers {bindNew<Wt::WContainerWidget>("clusters")};
+2
View File
@@ -145,12 +145,14 @@ Artists::refreshArtistLinkTypes()
// add default one first (none)
linkTypeModel->add(Wt::WString::tr("Lms.Explore.Artists.linktype-all"), std::nullopt);
// TODO: sort by translated strings
addTypeIfUsed(TrackArtistLinkType::Artist, "Lms.Explore.Artists.linktype-artist");
addTypeIfUsed(TrackArtistLinkType::ReleaseArtist, "Lms.Explore.Artists.linktype-releaseartist");
addTypeIfUsed(TrackArtistLinkType::Composer, "Lms.Explore.Artists.linktype-composer");
addTypeIfUsed(TrackArtistLinkType::Conductor, "Lms.Explore.Artists.linktype-conductor");
addTypeIfUsed(TrackArtistLinkType::Lyricist, "Lms.Explore.Artists.linktype-lyricist");
addTypeIfUsed(TrackArtistLinkType::Mixer, "Lms.Explore.Artists.linktype-mixer");
addTypeIfUsed(TrackArtistLinkType::Performer, "Lms.Explore.Artists.linktype-performer");
addTypeIfUsed(TrackArtistLinkType::Producer, "Lms.Explore.Artists.linktype-producer");
addTypeIfUsed(TrackArtistLinkType::Remixer, "Lms.Explore.Artists.linktype-remixer");
}
+36 -5
View File
@@ -19,6 +19,7 @@
#include "ReleaseView.hpp"
#include <map>
#include <Wt/WAnchor.h>
#include <Wt/WImage.h>
#include <Wt/WPushButton.h>
@@ -30,6 +31,7 @@
#include "services/database/ScanSettings.hpp"
#include "services/database/Session.hpp"
#include "services/database/Track.hpp"
#include "services/database/TrackArtistLink.hpp"
#include "services/recommendation/IRecommendationService.hpp"
#include "services/scrobbling/IScrobblingService.hpp"
#include "utils/Logger.hpp"
@@ -64,6 +66,7 @@ showReleaseInfoModal(Database::ReleaseId releaseId)
releaseInfo->addFunction("tr", &Wt::WTemplate::Functions::tr);
Wt::WContainerWidget* artistTable {releaseInfo->bindNew<Wt::WContainerWidget>("artist-table")};
std::map<Wt::WString, std::set<ArtistId>> artistMap;
auto addArtists = [&](TrackArtistLinkType linkType, const char* type)
{
@@ -74,11 +77,28 @@ showReleaseInfoModal(Database::ReleaseId releaseId)
if (artistIds.results.empty())
return;
std::unique_ptr<Wt::WContainerWidget> artistContainer {Utils::createArtistContainer(artistIds.results)};
auto artistsEntry {std::make_unique<Template>(Wt::WString::tr("Lms.Explore.template.info.artists"))};
artistsEntry->bindString("type", Wt::WString::trn(type, artistContainer->count()));
artistsEntry->bindWidget("artist-container", std::move(artistContainer));
artistTable->addWidget(std::move(artistsEntry));
Wt::WString typeStr {Wt::WString::trn(type, artistIds.results.size())};;
for (ArtistId artistId : artistIds.results)
artistMap[typeStr].insert(artistId);
};
auto addPerformerArtists = [&]
{
TrackArtistLink::FindParameters params;
params.setRelease(releaseId);
params.setLinkType(TrackArtistLinkType::Performer);
const auto links {TrackArtistLink::find(LmsApp->getDbSession(), params)};
if (links.results.empty())
return;
for (const TrackArtistLinkId linkId : links.results)
{
const TrackArtistLink::pointer link {TrackArtistLink::find(LmsApp->getDbSession(), linkId)};
if (!link)
continue;
artistMap[std::string {link->getSubType()}].insert(link->getArtist()->getId());
}
};
addArtists(TrackArtistLinkType::Composer, "Lms.Explore.Artists.linktype-composer");
@@ -87,6 +107,17 @@ showReleaseInfoModal(Database::ReleaseId releaseId)
addArtists(TrackArtistLinkType::Mixer, "Lms.Explore.Artists.linktype-mixer");
addArtists(TrackArtistLinkType::Remixer, "Lms.Explore.Artists.linktype-remixer");
addArtists(TrackArtistLinkType::Producer, "Lms.Explore.Artists.linktype-producer");
addPerformerArtists();
for (const auto& [role, artistIds] : artistMap)
{
std::unique_ptr<Wt::WContainerWidget> artistContainer {Utils::createArtistContainer(std::vector (std::cbegin(artistIds), std::cend(artistIds)))};
auto artistsEntry {std::make_unique<Template>(Wt::WString::tr("Lms.Explore.template.info.artists"))};
artistsEntry->bindString("type", role);
artistsEntry->bindWidget("artist-container", std::move(artistContainer));
artistTable->addWidget(std::move(artistsEntry));
}
// TODO: save in DB and mean all this
for (TrackId trackId : Track::find(LmsApp->getDbSession(), Track::FindParameters {}.setRelease(releaseId).setRange(Range {0, 1})).results)
+36 -6
View File
@@ -19,6 +19,7 @@
#include "TrackListHelpers.hpp"
#include <map>
#include <Wt/WAnchor.h>
#include <Wt/WImage.h>
#include <Wt/WPushButton.h>
@@ -26,9 +27,10 @@
#include "av/IAudioFile.hpp"
#include "services/database/Artist.hpp"
#include "services/database/Release.hpp"
#include "services/scrobbling/IScrobblingService.hpp"
#include "services/database/Session.hpp"
#include "services/database/Track.hpp"
#include "services/database/TrackArtistLink.hpp"
#include "services/scrobbling/IScrobblingService.hpp"
#include "utils/Service.hpp"
#include "common/Template.hpp"
@@ -58,6 +60,7 @@ namespace UserInterface::TrackListHelpers
trackInfo->addFunction("tr", &Wt::WTemplate::Functions::tr);
Wt::WContainerWidget* artistTable {trackInfo->bindNew<Wt::WContainerWidget>("artist-table")};
std::map<Wt::WString, std::set<ArtistId>> artistMap;
auto addArtists = [&](TrackArtistLinkType linkType, const char* type)
{
@@ -68,11 +71,28 @@ namespace UserInterface::TrackListHelpers
if (artistIds.results.empty())
return;
std::unique_ptr<Wt::WContainerWidget> artistContainer {Utils::createArtistContainer(artistIds.results)};
auto artistsEntry {std::make_unique<Template>(Wt::WString::tr("Lms.Explore.template.info.artists"))};
artistsEntry->bindString("type", Wt::WString::trn(type, artistContainer->count()));
artistsEntry->bindWidget("artist-container", std::move(artistContainer));
artistTable->addWidget(std::move(artistsEntry));
Wt::WString typeStr {Wt::WString::trn(type, artistIds.results.size())};;
for (ArtistId artistId : artistIds.results)
artistMap[typeStr].insert(artistId);
};
auto addPerformerArtists = [&]
{
TrackArtistLink::FindParameters params;
params.setTrack(trackId);
params.setLinkType(TrackArtistLinkType::Performer);
const auto links {TrackArtistLink::find(LmsApp->getDbSession(), params)};
if (links.results.empty())
return;
for (const TrackArtistLinkId linkId : links.results)
{
const TrackArtistLink::pointer link {TrackArtistLink::find(LmsApp->getDbSession(), linkId)};
if (!link)
continue;
artistMap[std::string {link->getSubType()}].insert(link->getArtist()->getId());
}
};
addArtists(TrackArtistLinkType::Composer, "Lms.Explore.Artists.linktype-composer");
@@ -81,6 +101,16 @@ namespace UserInterface::TrackListHelpers
addArtists(TrackArtistLinkType::Mixer, "Lms.Explore.Artists.linktype-mixer");
addArtists(TrackArtistLinkType::Remixer, "Lms.Explore.Artists.linktype-remixer");
addArtists(TrackArtistLinkType::Producer, "Lms.Explore.Artists.linktype-producer");
addPerformerArtists();
for (const auto& [role, artistIds] : artistMap)
{
std::unique_ptr<Wt::WContainerWidget> artistContainer {Utils::createArtistContainer(std::vector (std::cbegin(artistIds), std::cend(artistIds)))};
auto artistsEntry {std::make_unique<Template>(Wt::WString::tr("Lms.Explore.template.info.artists"))};
artistsEntry->bindString("type", role);
artistsEntry->bindWidget("artist-container", std::move(artistContainer));
artistTable->addWidget(std::move(artistsEntry));
}
if (const auto audioFile {Av::parseAudioFile(track->getPath())})
{
+1 -2
View File
@@ -54,8 +54,7 @@ DownloadResource::handleRequest(const Wt::Http::Request& request, Wt::Http::Resp
std::shared_ptr<Zip::Zipper> zipper;
// First, see if this request is for a continuation
Wt::Http::ResponseContinuation *continuation = request.continuation();
if (continuation)
if (Wt::Http::ResponseContinuation *continuation {request.continuation()})
zipper = Wt::cpp17::any_cast<std::shared_ptr<Zip::Zipper>>(continuation->data());
else
{
+10 -2
View File
@@ -52,8 +52,6 @@ std::ostream& operator<<(std::ostream& os, const MetaData::Album& album)
return os;
}
void parse(MetaData::IParser& parser, const std::filesystem::path& file)
{
using namespace MetaData;
@@ -91,6 +89,16 @@ void parse(MetaData::IParser& parser, const std::filesystem::path& file)
for (const Artist& artist : track->mixerArtists)
std::cout << "Mixer: " << artist << std::endl;
for (const auto& [role, artists] : track->performerArtists)
{
std::cout << "Performer";
if (!role.empty())
std::cout << " (" << role << ")";
std::cout << ":" << std::endl;
for (const Artist& artist : artists)
std::cout << "\t" << artist << std::endl;
}
for (const Artist& artist : track->producerArtists)
std::cout << "Producer: " << artist << std::endl;
-1
View File
@@ -66,7 +66,6 @@ int main(int argc, char* argv[])
Zip::SizeType nbTotalWrittenBytes {};
while (!zipper.isComplete())
{
//std::array<std::byte, Zipper::minOutputBufferSize> buffer;
std::array<std::byte, 65536> buffer;
const Zip::SizeType nbWrittenBytes {zipper.writeSome(buffer.data(), buffer.size())};