diff --git a/README.md b/README.md index 447c0a10..0d6cd96c 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,10 @@ Conventional features include: - Audio transcode for maximum interoperability and low bandwith requirements - Custom tag support -The recommendation engine makes use of [Self-Organizing Maps](https://en.wikipedia.org/wiki/Self-organizing_map). Please note it may require some significant computation time on large datasets. +The recommendation engine makes use of [Self-Organizing Maps](https://en.wikipedia.org/wiki/Self-organizing_map)
+Please note this engine: +- may require some significant computation time on large datasets. +- makes use of computed data available on [AcousticBrainz](https://acousticbrainz.org/). Therefore your music must contain the [MusicBrainz Identifier](https://musicbrainz.org/doc/MusicBrainz_Identifier) for the recommendation engine to work. ## Installation Here are the required packages to build on Debian Stretch: diff --git a/approot/artist.xml b/approot/artist.xml index 28ab28a1..ef4d8032 100644 --- a/approot/artist.xml +++ b/approot/artist.xml @@ -20,7 +20,7 @@

- ${name} + ${name class="Lms-explore-releasename"}

${}

${tr:Lms.Explore.various-artists}

${
}

${}${year}${}${} (${orig-year})${}

diff --git a/approot/artistlink.xml b/approot/artistlink.xml index 1b1959b9..cb02a39c 100644 --- a/approot/artistlink.xml +++ b/approot/artistlink.xml @@ -5,7 +5,7 @@ diff --git a/approot/artists.xml b/approot/artists.xml index bff7fa7f..50053f22 100644 --- a/approot/artists.xml +++ b/approot/artists.xml @@ -23,7 +23,7 @@
-

${name}

+

${name class="Lms-explore-artistname Lms-explore-artists-entry-name"}

diff --git a/approot/playhistory.xml b/approot/playhistory.xml index 11bd1bca..25394beb 100644 --- a/approot/playhistory.xml +++ b/approot/playhistory.xml @@ -20,10 +20,10 @@

${name}

diff --git a/approot/playqueue.xml b/approot/playqueue.xml index b2ba1131..c347bc8d 100644 --- a/approot/playqueue.xml +++ b/approot/playqueue.xml @@ -46,10 +46,10 @@

${name}

${play-btn}${del-btn} diff --git a/approot/release.xml b/approot/release.xml index dfbce12c..4ac914ed 100644 --- a/approot/release.xml +++ b/approot/release.xml @@ -14,7 +14,7 @@
diff --git a/approot/releases.xml b/approot/releases.xml index b28540ed..96d94c20 100644 --- a/approot/releases.xml +++ b/approot/releases.xml @@ -29,9 +29,9 @@

- ${release-name} + ${release-name class="Lms-explore-releasename"}

- ${}

${artist-name}

${
} + ${}

${artist-name class="Lms-explore-artistname"}

${
} ${play-btn}${add-btn}
diff --git a/docroot/css/lms.css b/docroot/css/lms.css index 95b0799e..0e2cafca 100644 --- a/docroot/css/lms.css +++ b/docroot/css/lms.css @@ -1,5 +1,4 @@ a, a:link, a:active, a:visited { - color: black; text-decoration: none; cursor: pointer; } @@ -8,6 +7,20 @@ a:hover { color: #337ab7; } +/* Base style for artist names */ +.Lms-explore-artistname +{ + color: darkgrey; + font-style: italic; +} + +/* Base style for release names */ +.Lms-explore-releasename +{ + color: black; + font-style: normal; +} + .Lms-vertical-align { display: flex; align-items: center; @@ -141,12 +154,24 @@ a:hover { box-shadow: 0px 2px 3px rgba(0, 0, 0, 0.4); } +.Lms-explore-release-entry-tracknumber { + color: darkgrey; + text-align: center; + white-space: nowrap; +} + .Lms-explore-release-entry-name { margin-top: 4px; margin-bottom: 4px; font-weight: inherit; } +.Lms-explore-release-entry { + border-bottom-style: solid; + border-bottom-color: lightgrey; + border-bottom-width: thin; +} + .Lms-explore-release-entry:hover { background-color: lightgrey; } @@ -159,6 +184,10 @@ a:hover { font-weight: bold; } +.Lms-explore-release-entry-artist { + display: inline-block; +} + .Lms-explore-releaselink { margin: 0px; padding-top: 4px; @@ -169,6 +198,9 @@ a:hover { background-color: lightgrey; } +.Lms-explore-releaselink-artist { + color: dimgrey; +} .Lms-explore-artists-entry { min-height: 48px; @@ -178,6 +210,11 @@ a:hover { background-color: lightgrey; } +.Lms-explore-artists-entry-name { + color: black; + font-style: normal; +} + .Lms-explore-artist-entry { padding-top: 8px; padding-bottom: 8px; @@ -196,6 +233,10 @@ a:hover { background-color: lightgrey; } +.Lms-explore-artistlink-name { + color: black; + font-style: normal; +} .Lms-explore-tracks-entry { min-height: 32px; diff --git a/src/av/AvInfo.cpp b/src/av/AvInfo.cpp index 1b5f7c6d..62745243 100644 --- a/src/av/AvInfo.cpp +++ b/src/av/AvInfo.cpp @@ -19,6 +19,8 @@ #include "AvInfo.hpp" +#include + #include #include "utils/Logger.hpp" @@ -93,7 +95,7 @@ getMetaDataFromDictionnary(AVDictionary* dictionnary, std::mapkey, tag->value)); + res[boost::to_upper_copy(tag->key)] = tag->value; } } diff --git a/src/database/Artist.cpp b/src/database/Artist.cpp index ae479d12..2d98966d 100644 --- a/src/database/Artist.cpp +++ b/src/database/Artist.cpp @@ -31,8 +31,8 @@ namespace Database { Artist::Artist(const std::string& name, const std::string& MBID) -: _name(std::string(name, 0 , _maxNameLength)), -_MBID(MBID) +: _name{std::string(name, 0 , _maxNameLength)}, +_MBID{MBID} { } @@ -40,7 +40,7 @@ _MBID(MBID) std::vector Artist::getByName(Wt::Dbo::Session& session, const std::string& name) { - Wt::Dbo::collection res = session.find().where("name = ?").bind( std::string(name, 0, _maxNameLength) ); + Wt::Dbo::collection res = session.find().where("name = ?").bind( std::string{name, 0, _maxNameLength} ); return std::vector(res.begin(), res.end()); } @@ -72,7 +72,7 @@ Artist::getAll(Wt::Dbo::Session& session, int offset, int size) std::vector Artist::getAllOrphans(Wt::Dbo::Session& session) { - Wt::Dbo::collection res = session.query< Wt::Dbo::ptr >("SELECT DISTINCT a FROM artist a LEFT OUTER JOIN Track t ON a.id = t.artist_id WHERE t.id IS NULL"); + Wt::Dbo::collection res = session.query< Wt::Dbo::ptr >("SELECT DISTINCT a FROM artist a WHERE NOT EXISTS(SELECT 1 FROM track t INNER JOIN track_artist t_a ON t_a.artist_id = a.id WHERE t.id = t_a.track_id)"); return std::vector(res.begin(), res.end()); } @@ -93,7 +93,7 @@ getQuery(Wt::Dbo::Session& session, if (!clusterIds.empty()) { - oss << " INNER JOIN track t ON t.artist_id = a.id INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id"; + oss << " INNER JOIN track t ON t_a.track_id = t.id INNER JOIN track_artist t_a ON t_a.artist_id = a.id 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; @@ -145,7 +145,7 @@ Artist::getByFilter(Wt::Dbo::Session& session, std::vector Artist::getLastAdded(Wt::Dbo::Session& session, Wt::WDateTime after, int limit) { - Wt::Dbo::collection res = session.query("SELECT a from artist a INNER JOIN track t ON a.id = t.artist_id") + Wt::Dbo::collection res = session.query("SELECT a from artist a INNER JOIN track_artist t_a ON t_a.artist_id = a.id INNER JOIN track t ON t.id = t_a.track_id") .where("t.file_added > ?").bind(after) .groupBy("a.id") .orderBy("t.file_added DESC") @@ -164,7 +164,7 @@ Artist::getReleases(const std::set& clusterIds) const WhereClause where; std::ostringstream oss; - oss << "SELECT DISTINCT r FROM release r INNER JOIN artist a ON t.artist_id = a.id INNER JOIN track t ON t.release_id = r.id"; + oss << "SELECT DISTINCT r FROM release r INNER JOIN artist a ON t_a.artist_id = a.id INNER JOIN track_artist t_a ON t_a.track_id = t.id INNER JOIN track t ON t.release_id = r.id"; if (!clusterIds.empty()) { @@ -215,7 +215,7 @@ Artist::getClusterGroups(std::vector clusterTypes, std::si WhereClause where; std::ostringstream oss; - oss << "SELECT c from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN cluster_type c_type ON c.cluster_type_id = c_type.id INNER JOIN artist a ON t.artist_id = a.id"; + oss << "SELECT c from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN cluster_type c_type ON c.cluster_type_id = c_type.id INNER JOIN artist a ON t_a.artist_id = a.id INNER JOIN track_artist t_a ON t_a.track_id = t.id"; where.And(WhereClause("a.id = ?")).bind(std::to_string(self()->id())); { diff --git a/src/database/Artist.hpp b/src/database/Artist.hpp index ddc4410a..9f73635e 100644 --- a/src/database/Artist.hpp +++ b/src/database/Artist.hpp @@ -60,8 +60,8 @@ class Artist : public Wt::Dbo::Dbo static std::vector getLastAdded(Wt::Dbo::Session& session, Wt::WDateTime after, int size = 1); // Accessors - std::string getName(void) const { return _name; } - std::string getMBID(void) const { return _MBID; } + const std::string& getName(void) const { return _name; } + const std::string& getMBID(void) const { return _MBID; } // Get the releases that have at least one track for this artist that belongs to optional cluster filters std::vector> getReleases(const std::set& clusterIds = std::set()) const; @@ -84,7 +84,7 @@ class Artist : public Wt::Dbo::Dbo Wt::Dbo::field(a, _name, "name"); Wt::Dbo::field(a, _MBID, "mbid"); - Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToOne, "artist"); + Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToMany, "track_artist", "", Wt::Dbo::OnDeleteCascade); } private: diff --git a/src/database/DatabaseHandler.cpp b/src/database/DatabaseHandler.cpp index c9165e4b..5ba449f4 100644 --- a/src/database/DatabaseHandler.cpp +++ b/src/database/DatabaseHandler.cpp @@ -137,7 +137,6 @@ Handler::Handler(Wt::Dbo::SqlConnectionPool& connectionPool) _session.execute("CREATE INDEX IF NOT EXISTS track_name_idx ON track(name)"); _session.execute("CREATE INDEX IF NOT EXISTS artist_name_idx ON artist(name)"); _session.execute("CREATE INDEX IF NOT EXISTS release_name_idx ON release(name)"); - _session.execute("CREATE INDEX IF NOT EXISTS track_artist_idx ON track(artist_id)"); _session.execute("CREATE INDEX IF NOT EXISTS track_release_idx ON track(release_id)"); _session.execute("CREATE INDEX IF NOT EXISTS cluster_name_idx ON cluster(name)"); _session.execute("CREATE INDEX IF NOT EXISTS cluster_type_name_idx ON cluster_type(name)"); diff --git a/src/database/Release.cpp b/src/database/Release.cpp index 6f97e3a3..66063557 100644 --- a/src/database/Release.cpp +++ b/src/database/Release.cpp @@ -225,7 +225,7 @@ Release::getArtists() const assert(session()); Wt::Dbo::collection> res = session()->query>( - "SELECT DISTINCT a FROM artist a INNER JOIN release r ON t.artist_id = a.id INNER JOIN track t ON t.release_id = r.id") + "SELECT DISTINCT a FROM artist a INNER JOIN release r ON t.id = t_a.track_id INNER JOIN track_artist t_a ON t_a.artist_id = a.id INNER JOIN track t ON t.release_id = r.id") .where("r.id = ?") .bind(id()); diff --git a/src/database/Track.cpp b/src/database/Track.cpp index 1c8c2c78..8548798d 100644 --- a/src/database/Track.cpp +++ b/src/database/Track.cpp @@ -218,6 +218,28 @@ Track::getByFilter(Wt::Dbo::Session& session, return getByFilter(session, clusters, std::vector(), -1, -1, moreResults); } +void +Track::setArtists(const std::vector>& artists) +{ + _artists.clear(); + for (const Wt::Dbo::ptr& artist : artists) + _artists.insert(artist); +} + +void +Track::setClusters(const std::vector>& clusters) +{ + _clusters.clear(); + for (const Wt::Dbo::ptr& cluster : clusters) + _clusters.insert(cluster); +} + +void +Track::setFeatures(const Wt::Dbo::ptr& features) +{ + _trackFeatures = features; +} + boost::optional Track::getTrackNumber(void) const { diff --git a/src/database/Track.hpp b/src/database/Track.hpp index 2a1e831b..1f42f27e 100644 --- a/src/database/Track.hpp +++ b/src/database/Track.hpp @@ -75,9 +75,6 @@ class Track : public Wt::Dbo::Dbo // Create utility static pointer create(Wt::Dbo::Session& session, const boost::filesystem::path& p); - // Remove utility - static void removeClusters(std::string type); - // Accessors void setScanVersion(std::size_t version) { _scanVersion = version; } void setTrackNumber(int num) { _trackNumber = num; } @@ -91,15 +88,14 @@ class Track : public Wt::Dbo::Dbo void setChecksum(const std::vector& checksum) { _fileChecksum = checksum; } void setYear(int year) { _year = year; } void setOriginalYear(int year) { _originalYear = year; } - void setGenres(const std::string& genreList) { _genreList = genreList; } void setHasCover(bool hasCover) { _hasCover = hasCover; } void setMBID(const std::string& MBID) { _MBID = MBID; } void setCopyright(const std::string& copyright) { _copyright = std::string(copyright, 0, _maxCopyrightLength); } void setCopyrightURL(const std::string& copyrightURL) { _copyrightURL = std::string(copyrightURL, 0, _maxCopyrightURLLength); } - void setArtists(std::vector>& artists); + void setArtists(const std::vector>& artists); void setRelease(Wt::Dbo::ptr release) { _release = release; } - void eraseClusters() { _clusters.clear(); } - void eraseFeatures() { /*_trackFeatures.reset();*/ } + void setClusters(const std::vector>& clusters ); + void setFeatures(const Wt::Dbo::ptr& features); std::size_t getScanVersion() const { return _scanVersion; } boost::optional getTrackNumber() const; @@ -138,7 +134,6 @@ class Track : public Wt::Dbo::Dbo Wt::Dbo::field(a, _duration, "duration"); Wt::Dbo::field(a, _year, "year"); Wt::Dbo::field(a, _originalYear, "original_year"); - Wt::Dbo::field(a, _genreList, "genre_list"); Wt::Dbo::field(a, _filePath, "file_path"); Wt::Dbo::field(a, _fileLastWrite, "file_last_write"); Wt::Dbo::field(a, _fileAdded, "file_added"); @@ -171,7 +166,6 @@ class Track : public Wt::Dbo::Dbo std::chrono::duration _duration; int _year = 0; int _originalYear = 0; - std::string _genreList; std::string _filePath; std::vector _fileChecksum; Wt::WDateTime _fileLastWrite; diff --git a/src/database/TrackList.cpp b/src/database/TrackList.cpp index 55f7c86d..057a8b72 100644 --- a/src/database/TrackList.cpp +++ b/src/database/TrackList.cpp @@ -198,7 +198,7 @@ TrackList::getTopArtists(int limit) const assert(session()); assert(IdIsValid(self()->id())); - Wt::Dbo::collection res = session()->query("SELECT a from artist a INNER JOIN track t ON t.artist_id = a.id INNER JOIN tracklist_entry p_e ON p_e.track_id = t.id INNER JOIN tracklist p ON p.id = p_e.tracklist_id") + Wt::Dbo::collection res = session()->query("SELECT a from artist a INNER JOIN track t ON t.id = t_a.track_id INNER JOIN track_artist t_a ON t_a.artist_id = a.id INNER JOIN tracklist_entry p_e ON p_e.track_id = t.id INNER JOIN tracklist p ON p.id = p_e.tracklist_id") .where("p.id = ?").bind(self()->id()) .groupBy("a.id") .orderBy("COUNT(a.id) DESC") diff --git a/src/metadata/AvFormat.cpp b/src/metadata/AvFormat.cpp index a0f2f044..8b445e92 100644 --- a/src/metadata/AvFormat.cpp +++ b/src/metadata/AvFormat.cpp @@ -19,8 +19,6 @@ #include "AvFormat.hpp" -#include - #include "av/AvInfo.hpp" #include "utils/Logger.hpp" @@ -29,167 +27,199 @@ namespace MetaData { -boost::optional +using MetadataMap = std::map; + +boost::optional +findFirstValueOf(const MetadataMap& metadataMap, std::initializer_list tags) +{ + auto it = std::find_first_of(std::cbegin(metadataMap), std::cend(metadataMap), std::cbegin(tags), std::cend(tags), [](const auto& it, const auto& str) { return it.first == str; }); + if (it == std::cend(metadataMap)) + return boost::none; + + return stringTrim(it->second); +} + +static +boost::optional +getAlbum(const MetadataMap& metadataMap) +{ + boost::optional res; + + auto album {findFirstValueOf(metadataMap, {"ALBUM"})}; + if (!album) + return res; + + res = Album{*album, ""}; + + auto albumMBID {findFirstValueOf(metadataMap, {"MUSICBRAINZ ALBUM ID", "MUSICBRAINZ_ALBUMID"})}; + if (!albumMBID) + return res; + + res->musicBrainzAlbumID = *albumMBID; + + return res; +} + +static +boost::optional +getAlbumArtist(const MetadataMap& metadataMap) +{ + boost::optional res; + + auto artist {findFirstValueOf(metadataMap, {"ALBUM_ARTIST"})}; + if (!artist) + return res; + + res = Artist{*artist, ""}; + + auto artistMBID {findFirstValueOf(metadataMap, {"MUSICBRAINZ ALBUM ARTIST ID"})}; + if (!artistMBID) + return res; + + res->musicBrainzArtistID = *artistMBID; + + return res; +} + +static +std::vector +getArtists(const MetadataMap& metadataMap) +{ + std::vector artists; + + std::vector artistNames; + if (metadataMap.find("ARTISTS") != metadataMap.end()) + { + artistNames = splitString(metadataMap.find("ARTISTS")->second, "/;"); + } + else if (metadataMap.find("ARTIST") != metadataMap.end()) + { + artistNames = {metadataMap.find("ARTISTS")->second}; + } + + std::vector artistMBIDs; + { + auto mbids {findFirstValueOf(metadataMap, {"MUSICBRAINZ ARTIST ID", "MUSICBRAINZ_ARTISTID"})}; + if (mbids) + artistMBIDs = splitString(*mbids, "/"); + } + + for (std::size_t i {}; i < artistNames.size(); ++i) + { + Artist artist{std::move(artistNames[i]), ""}; + + if (artistNames.size() == artistMBIDs.size()) + artist.musicBrainzArtistID = std::move(artistMBIDs[i]); + + artists.emplace_back(std::move(artist)); + } + + return artists; +} + +boost::optional AvFormat::parse(const boost::filesystem::path& p, bool debug) { - Items items; + Track track; try { - Av::MediaFile mediaFile(p); + Av::MediaFile mediaFile {p}; // Stream info { std::vector audioStreams; - auto streams = mediaFile.getStreamInfo(); - - for (auto stream : streams) - audioStreams.push_back( {.bitRate = stream.bitrate } ); - - if (!audioStreams.empty()) - items.insert( std::make_pair(MetaData::Type::AudioStreams, audioStreams)); + for (auto stream : mediaFile.getStreamInfo()) + { + MetaData::AudioStream audioStream {.bitRate = static_cast(stream.bitrate)}; + track.audioStreams.emplace_back(audioStream); + } } - // Duration - items.insert( std::make_pair(MetaData::Type::Duration, mediaFile.getDuration() )); - - // Cover - items.insert( std::make_pair(MetaData::Type::HasCover, mediaFile.hasAttachedPictures())); - - // Embedded MetaData - // Make sure to convert strings into UTF-8 + track.duration = mediaFile.getDuration(); + track.hasCover = mediaFile.hasAttachedPictures(); MetaData::Clusters clusters; - std::map metadataMap = mediaFile.getMetaData(); - for (auto metadata : metadataMap) + const std::map metadataMap {mediaFile.getMetaData()}; + track.artists = getArtists(metadataMap); + track.album = getAlbum(metadataMap); + track.albumArtist = getAlbumArtist(metadataMap); + + for (const auto& metadata : metadataMap) { - const std::string tag = boost::to_upper_copy(metadata.first); - const std::string value = metadata.second; + const std::string& tag {metadata.first}; + const std::string& value {metadata.second}; if (debug) std::cout << "TAG = " << tag << ", VAL = " << value << std::endl; - if (tag == "ARTIST") - { - if (items.find(MetaData::Type::Artists) == items.end()) - items[MetaData::Type::Artists] = std::set{ stringTrim( value) }; - } - else if (tag == "ARTISTS") - { - std::vector strings {splitString(value, "/;")}; // Picard separator is '/' - - std::set artists; - for (const std::string& string : strings) - artists.insert(stringTrim(string)); - - items[MetaData::Type::Artists] = std::move(artists); - } - else if (tag == "ALBUM") - items.insert( std::make_pair(MetaData::Type::Album, stringTrim( value) )); - else if (tag == "TITLE") - items.insert( std::make_pair(MetaData::Type::Title, stringTrim( value) )); + if (tag == "TITLE") + track.title = value; else if (tag == "TRACK") { // Expecting 'Number/Total' - auto strings = splitString(value, "/"); + std::vector strings {splitString(value, "/") }; if (strings.size() > 0) { - auto number = readAs(strings[0]); - if (number) - items.insert( std::make_pair(MetaData::Type::TrackNumber, *number )); + track.trackNumber = readAs(strings[0]); if (strings.size() > 1) - { - auto totalNumber = readAs(strings[1]); - if (totalNumber) - items.insert( std::make_pair(MetaData::Type::TotalTrack, *totalNumber )); - } + track.totalTrack = readAs(strings[1]); } } else if (tag == "DISC") { // Expecting 'Number/Total' - auto strings = splitString(value, "/"); + std::vector strings {splitString(value, "/")}; if (strings.size() > 0) { - auto number = readAs(strings[0]); - if (number) - items.insert( std::make_pair(MetaData::Type::DiscNumber, *number )); + track.discNumber = readAs(strings[0]); if (strings.size() > 1) - { - auto totalNumber = readAs(strings[1]); - if (totalNumber) - items.insert( std::make_pair(MetaData::Type::TotalDisc, *totalNumber )); - } + track.totalDisc = readAs(strings[1]); } } else if (tag == "DATE" || tag == "YEAR" || tag == "WM/Year") { - auto date = readAs(value); - if (date) - items.insert(std::make_pair(MetaData::Type::Year, *date)); + track.year = readAs(value); } else if (tag == "TDOR" // Original release time (ID3v2 2.4) || tag == "TORY") // Original release year { - auto date = readAs(value); - if (date) - items.insert(std::make_pair(MetaData::Type::OriginalYear, *date)); + track.originalYear = readAs(value); } - else if (tag == "MUSICBRAINZ ARTIST ID" - || tag == "MUSICBRAINZ_ARTISTID") + else if (tag == "ACOUSTID ID") { - std::vector strings {splitString(value, "/")}; - - std::set mbids; - for (const std::string& string : strings) - mbids.insert(stringTrim(string)); - - items[MetaData::Type::MusicBrainzArtistID] = std::move(mbids); - } - else if (tag == "MUSICBRAINZ ALBUM ID" - || tag == "MUSICBRAINZ_ALBUMID") - { - items.insert( std::make_pair(MetaData::Type::MusicBrainzAlbumID, stringTrim(value)) ); + track.acoustID = value; } else if (tag == "MUSICBRAINZ RELEASE TRACK ID" || tag == "MUSICBRAINZ_RELEASETRACKID" || tag == "MUSICBRAINZ_TRACKID") { - items.insert( std::make_pair(MetaData::Type::MusicBrainzTrackID, stringTrim(value)) ); - } - else if (tag == "ACOUSTID ID") - { - items.insert( std::make_pair(MetaData::Type::AcoustID, stringTrim(value)) ); + track.musicBrainzTrackID = value; } else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end()) { - std::vector clusterNames = splitString(value, "/,;"); + std::vector clusterNames {splitString(value, "/,;")}; if (!clusterNames.empty()) - { - clusters[tag] = std::set(clusterNames.begin(), clusterNames.end()); - } + track.clusters[tag] = std::set{clusterNames.begin(), clusterNames.end()}; } } - - if (!clusters.empty()) - items.insert( std::make_pair(MetaData::Type::Clusters, clusters) ); } catch(Av::MediaFileException& e) { return boost::none; } - return items; + return track; } } // namespace MetaData diff --git a/src/metadata/AvFormat.hpp b/src/metadata/AvFormat.hpp index 9c29e3f6..164dd941 100644 --- a/src/metadata/AvFormat.hpp +++ b/src/metadata/AvFormat.hpp @@ -31,7 +31,7 @@ namespace MetaData class AvFormat : public Parser { public: - boost::optional parse(const boost::filesystem::path& p, bool debug = false) override; + boost::optional parse(const boost::filesystem::path& p, bool debug = false) override; }; } // namespace MetaData diff --git a/src/metadata/MetaData.hpp b/src/metadata/MetaData.hpp index c855d09f..39633924 100644 --- a/src/metadata/MetaData.hpp +++ b/src/metadata/MetaData.hpp @@ -19,6 +19,7 @@ #pragma once +#include #include #include @@ -39,7 +40,7 @@ namespace MetaData { std::string name; std::string musicBrainzAlbumID; - } + }; struct AudioStream { @@ -64,7 +65,7 @@ namespace MetaData boost::optional originalYear; bool hasCover {false}; std::vector audioStreams; - std::string acoustId; + std::string acoustID; std::string copyright; std::string copyrightURL; }; diff --git a/src/metadata/TagLibParser.cpp b/src/metadata/TagLibParser.cpp index 39ef0b2a..65364b98 100644 --- a/src/metadata/TagLibParser.cpp +++ b/src/metadata/TagLibParser.cpp @@ -31,7 +31,95 @@ namespace MetaData { -boost::optional +static +std::vector +splitAndTrimString(const std::string& str, const std::string& delimiters) +{ + std::vector res; + + std::vector strings {splitString(str, delimiters)}; + for (const std::string& s : strings) + res.emplace_back(stringTrim(s)); + + return res; +} + +static +std::vector +getMusicBrainzArtistID(const TagLib::PropertyMap& properties) +{ + if (!properties.contains("MUSICBRAINZ_ARTISTID")) + return {}; + + const auto& values {properties["MUSICBRAINZ_ARTISTID"]}; + + return splitAndTrimString(values.front().to8Bit(true), "/"); // Picard separator is '/' +} + +static +std::vector +getArtists(const TagLib::PropertyMap& properties) +{ + std::vector res; + + if (properties.contains("ARTISTS")) + { + const TagLib::StringList& values {properties["ARTISTS"]}; + + std::vector artists {splitAndTrimString(values.front().to8Bit(true), "/;")}; // Picard separator is '/' + std::vector artistsMBID {getMusicBrainzArtistID(properties)}; + + for (std::size_t i {}; i < artists.size(); ++i) + res.emplace_back(Artist{artists[i], artistsMBID.size() == artists.size() ? artistsMBID[i] : ""}); + + return res; + } + else if (properties.contains("ARTIST")) + { + const auto& value {properties["ARTIST"]}; + + res.emplace_back(Artist{value.front().to8Bit(true), ""}); + } + + return res; +} + +static +boost::optional +getAlbumArtist(const TagLib::PropertyMap& properties) +{ + boost::optional res; + + if (!properties.contains("ALBUMARTIST")) + return res; + + res = Artist{stringTrim(properties["ALBUMARTIST"].front().to8Bit(true)), ""}; + + if (properties.contains("MUSICBRAINZ_ALBUMARTISTID")) + res->musicBrainzArtistID = stringTrim(properties["MUSICBRAINZ_ALBUMARTISTID"].front().to8Bit(true)); + + return res; +} + + +static +boost::optional +getAlbum(const TagLib::PropertyMap& properties) +{ + boost::optional res; + + if (!properties.contains("ALBUM")) + return res; + + res = Album{stringTrim(properties["ALBUM"].front().to8Bit(true)), ""}; + + if (properties.contains("MUSICBRAINZ_ALBUMID")) + res->musicBrainzAlbumID = properties["MUSICBRAINZ_ALBUMID"].front().to8Bit(true); + + return res; +} + +boost::optional TagLibParser::parse(const boost::filesystem::path& p, bool debug) { TagLib::FileRef f {p.string().c_str(), @@ -44,26 +132,26 @@ TagLibParser::parse(const boost::filesystem::path& p, bool debug) if (!f.audioProperties()) return boost::none; - Items items; + Track track; { const TagLib::AudioProperties *properties {f.audioProperties() }; - items[MetaData::Type::Duration] = std::chrono::milliseconds {properties->length() * 1000}; + track.duration = std::chrono::milliseconds {properties->length() * 1000}; - MetaData::AudioStream audioStream {.bitRate = static_cast(properties->bitrate() * 1000)}; - items[MetaData::Type::AudioStreams] = std::vector {audioStream}; + MetaData::AudioStream audioStream {.bitRate = static_cast(properties->bitrate() * 1000)}; + track.audioStreams = {std::move(audioStream)}; } // Not that good embedded pictures handling // MP3 - if (TagLib::MPEG::File *mp3File = dynamic_cast(f.file())) + if (TagLib::MPEG::File *mp3File {dynamic_cast(f.file())}) { if (mp3File->ID3v2Tag()) { if (!mp3File->ID3v2Tag()->frameListMap()["APIC"].isEmpty()) - items.insert( std::make_pair(MetaData::Type::HasCover, true)); + track.hasCover = true; } } @@ -72,6 +160,10 @@ TagLibParser::parse(const boost::filesystem::path& p, bool debug) MetaData::Clusters clusters; const TagLib::PropertyMap& properties {f.file()->properties()}; + track.artists = getArtists(properties); + track.albumArtist = getAlbumArtist(properties); + track.album = getAlbum(properties); + for(auto property : properties) { const std::string tag = property.first.upper().to8Bit(true); @@ -80,172 +172,108 @@ TagLibParser::parse(const boost::filesystem::path& p, bool debug) if (tag.empty() || values.isEmpty() || values.front().isEmpty()) continue; + std::string value {stringTrim(values.front().to8Bit(true))}; + // TODO validate MBID format if (debug) { std::vector strs; - for (auto value : values) - strs.push_back(values.front().to8Bit(true)); + std::transform(values.begin(), values.end(), std::back_inserter(strs), [](const auto& value) { return value.to8Bit(true); }); std::cout << "[" << tag << "] = " << joinStrings(strs, ",") << std::endl; } - if (tag == "ARTIST") - { - // Lower priority than ARTISTS - if (items.find(MetaData::Type::Artists) == items.end()) - items[MetaData::Type::Artists] = std::vector{ stringTrim( values.front().to8Bit(true)) }; - } - else if (tag == "ARTISTS") - { - // Higher priority than ARTISTS - std::vector strings {splitString(values.front().to8Bit(), "/;")}; // Picard separator is '/' - - std::vector artists; - for (const std::string& string : strings) - artists.emplace_back(stringTrim(string)); - - items[MetaData::Type::Artists] = std::move(artists); - } - - else if (tag == "ALBUM") - items.insert( std::make_pair(MetaData::Type::Album, stringTrim( values.front().to8Bit(true)))); - else if (tag == "TITLE") - items.insert( std::make_pair(MetaData::Type::Title, stringTrim( values.front().to8Bit(true)))); + if (tag == "TITLE") + track.title = value; else if (tag == "MUSICBRAINZ_RELEASETRACKID" || tag == "MUSICBRAINZ RELEASE TRACK ID") { - items.insert( std::make_pair(MetaData::Type::MusicBrainzTrackID, stringTrim( values.front().to8Bit(true)))); + track.musicBrainzTrackID = value; } - else if (tag == "MUSICBRAINZ_ARTISTID") - { - std::vector strings {splitString(values.front().to8Bit(), "/")}; // Picard separator is '/' - - std::vector mbids; - for (const std::string& string : strings) - mbids.emplace_back(stringTrim(string)); - - items[MetaData::Type::MusicBrainzArtistID] = std::move(mbids); - } - else if (tag == "MUSICBRAINZ_ALBUMID") - items.insert( std::make_pair(MetaData::Type::MusicBrainzAlbumID, stringTrim( values.front().to8Bit(true)))); else if (tag == "MUSICBRAINZ_TRACKID") - items.insert( std::make_pair(MetaData::Type::MusicBrainzRecordingID, stringTrim( values.front().to8Bit(true)))); + track.musicBrainzRecordID = value; else if (tag == "ACOUSTID_ID") - items.insert( std::make_pair(MetaData::Type::AcoustID, stringTrim( values.front().to8Bit(true)))); + track.acoustID = value; else if (tag == "TRACKTOTAL") { - auto totalTrack {readAs(values.front().to8Bit(true)) }; + auto totalTrack = readAs(value); if (totalTrack) - items[MetaData::Type::TotalTrack] = *totalTrack; + track.totalTrack = totalTrack; } else if (tag == "TRACKNUMBER") { // Expecting 'Number/Total' - std::vector strings {splitString(values.front().to8Bit(), "/")}; + std::vector strings {splitAndTrimString(value, "/")}; if (!strings.empty()) { - auto number {readAs(strings[0])}; - if (number) - items.insert( std::make_pair(MetaData::Type::TrackNumber, *number )); + track.trackNumber = readAs(strings[0]); // Lower priority than TRACKTOTAL - if (strings.size() > 1 && items.find(MetaData::Type::TotalTrack) == items.end()) - { - auto totalTrack {readAs(strings[1])}; - if (totalTrack) - items[MetaData::Type::TotalTrack] = *totalTrack; - } + if (strings.size() > 1 && !track.totalTrack) + track.totalTrack = readAs(strings[1]); } } else if (tag == "DISCTOTAL") { - auto totalDisc = readAs(values.front().to8Bit(true)); + auto totalDisc = readAs(value); if (totalDisc) - items[MetaData::Type::TotalDisc] = *totalDisc; + track.totalDisc = totalDisc; } else if (tag == "DISCNUMBER") { // Expecting 'Number/Total' - auto strings = splitString(values.front().to8Bit(), "/"); + std::vector strings {splitString(value, "/")}; if (!strings.empty()) { - auto number = readAs(strings[0]); - if (number) - items.insert( std::make_pair(MetaData::Type::DiscNumber, *number)); + track.discNumber = readAs(strings[0]); // Lower priority than DISCTOTAL - if (strings.size() > 1 && items.find(MetaData::Type::TotalDisc) == items.end()) - { - auto totalDisc = readAs(strings[1]); - if (totalDisc) - items[MetaData::Type::TotalDisc] = *totalDisc; - } + if (strings.size() > 1 && !track.totalDisc) + track.totalDisc = readAs(strings[1]); } } else if (tag == "DATE") + track.year = readAs(value); + else if (tag == "ORIGINALDATE" && !track.originalYear) { - auto timePoint = readAs(values.front().to8Bit()); - if (timePoint) - items.insert( std::make_pair(MetaData::Type::Year, *timePoint)); - } - else if (tag == "ORIGINALDATE") - { - // Lower priority than original year - if (items.find(MetaData::Type::OriginalYear) == items.end()) - { - auto timePoint = readAs(values.front().to8Bit()); - if (timePoint) - items[MetaData::Type::OriginalYear] = *timePoint; - } + // Lower priority than ORIGINALYEAR + track.originalYear = readAs(value); } else if (tag == "ORIGINALYEAR") { - auto timePoint = readAs(values.front().to8Bit()); - if (timePoint) - { - // Take priority on original year - items[MetaData::Type::OriginalYear] = *timePoint; - } + // Higher priority than ORIGINALDATE + auto originalYear = readAs(value); + if (originalYear) + track.originalYear = originalYear; } else if (tag == "METADATA_BLOCK_PICTURE") - { - // Only add once - if (items.find(MetaData::Type::HasCover) == items.end()) - items.insert( std::make_pair(MetaData::Type::HasCover, true)); - } + track.hasCover = true; else if (tag == "COPYRIGHT") - { - items.insert(std::make_pair(MetaData::Type::Copyright, values.front().to8Bit())); - } + track.copyright = value; else if (tag == "COPYRIGHTURL") - { - items.insert(std::make_pair(MetaData::Type::CopyrightURL, values.front().to8Bit())); - } + track.copyrightURL = value; else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end()) { std::set clusterNames; for (const auto& valueList : values) { - auto values = splitString(valueList.to8Bit(), "/,;"); + auto values = splitAndTrimString(valueList.to8Bit(true), "/,;"); - for (auto value : values) + for (const auto& value : values) clusterNames.insert(value); } if (!clusterNames.empty()) - clusters[tag] = clusterNames; + track.clusters[tag] = clusterNames; } } - if (!clusters.empty()) - items.insert( std::make_pair(MetaData::Type::Clusters, clusters) ); } - return items; + return track; } } // namespace MetaData diff --git a/src/metadata/TagLibParser.hpp b/src/metadata/TagLibParser.hpp index 0ecadbe5..2ff03251 100644 --- a/src/metadata/TagLibParser.hpp +++ b/src/metadata/TagLibParser.hpp @@ -31,7 +31,7 @@ namespace MetaData class TagLibParser : public Parser { public: - boost::optional parse(const boost::filesystem::path& p, bool debug = false) override; + boost::optional parse(const boost::filesystem::path& p, bool debug = false) override; }; } // namespace MetaData diff --git a/src/scanner/MediaScanner.cpp b/src/scanner/MediaScanner.cpp index be9bdc79..085885d5 100644 --- a/src/scanner/MediaScanner.cpp +++ b/src/scanner/MediaScanner.cpp @@ -84,62 +84,69 @@ isPathInParentPath(const boost::filesystem::path& path, const boost::filesystem: return false; } -Artist::pointer -getArtist(Wt::Dbo::Session& session, const std::string& name, const std::string& mbid) +std::vector +getArtists(Wt::Dbo::Session& session, const std::vector& artistsInfo) { - Artist::pointer artist; + std::vector artists; - // First try to get by MBID - if (!mbid.empty()) + for (const MetaData::Artist& artistInfo : artistsInfo) { - artist = Artist::getByMBID(session, mbid); - if (!artist) - artist = Artist::create(session, name, mbid); + Artist::pointer artist; - return artist; - } - - // Fall back on artist name (collisions may occur) - if (!name.empty()) - { - for (Artist::pointer sameNamedArtist : Artist::getByName(session, name)) + // First try to get by MBID + if (!artistInfo.musicBrainzArtistID.empty()) { - if (sameNamedArtist->getMBID().empty()) - { - artist = sameNamedArtist; - break; - } + artist = Artist::getByMBID(session, artistInfo.musicBrainzArtistID); + if (!artist) + artist = Artist::create(session, artistInfo.name, artistInfo.musicBrainzArtistID); + + artists.emplace_back(std::move(artist)); + continue; } - // No Artist found with the same name and without MBID -> creating - if (!artist) - artist = Artist::create(session, name); + // Fall back on artist name (collisions may occur) + if (!artistInfo.name.empty()) + { + for (const Artist::pointer& sameNamedArtist : Artist::getByName(session, artistInfo.name)) + { + if (sameNamedArtist->getMBID().empty()) + { + artist = sameNamedArtist; + break; + } + } - return artist; + // No Artist found with the same name and without MBID -> creating + if (!artist) + artist = Artist::create(session, artistInfo.name); + + artists.emplace_back(std::move(artist)); + continue; + } } - return Artist::pointer(); + return artists; } Release::pointer -getRelease(Wt::Dbo::Session& session, const std::string& name, const std::string& mbid) +getRelease(Wt::Dbo::Session& session, const MetaData::Album& album) { Release::pointer release; // First try to get by MBID - if (!mbid.empty()) + if (!album.musicBrainzAlbumID.empty()) { - release = Release::getByMBID(session, mbid ); + release = Release::getByMBID(session, album.musicBrainzAlbumID); if (!release) - release = Release::create(session, name, mbid); + release = Release::create(session, album.name, album.musicBrainzAlbumID); return release; } // Fall back on release name (collisions may occur) - if (!name.empty()) + if (!album.name.empty()) { - for (Release::pointer sameNamedRelease : Release::getByName(session, name)) + for (const Release::pointer& sameNamedRelease : Release::getByName(session, album.name)) { if (sameNamedRelease->getMBID().empty()) { @@ -150,12 +157,12 @@ getRelease(Wt::Dbo::Session& session, const std::string& name, const std::string // No release found with the same name and without MBID -> creating if (!release) - release = Release::create(session, name); + release = Release::create(session, album.name); return release; } - return Release::pointer(); + return Release::pointer{}; } std::vector @@ -400,8 +407,8 @@ MediaScanner::scanAudioFile(const boost::filesystem::path& file, bool forceScan, } } - boost::optional items = _metadataParser.parse(file); - if (!items) + boost::optional trackInfo {_metadataParser.parse(file)}; + if (!trackInfo) { stats.scanErrors++; return; @@ -412,15 +419,14 @@ MediaScanner::scanAudioFile(const boost::filesystem::path& file, bool forceScan, std::vector checksum ; computeCrc(file, checksum); - Wt::Dbo::Transaction transaction(_db.getSession()); + Wt::Dbo::Transaction transaction {_db.getSession()}; - Wt::Dbo::ptr track = Track::getByPath(_db.getSession(), file); + Wt::Dbo::ptr track {Track::getByPath(_db.getSession(), file) }; // We estimate this is a audio file if: // - we found a least one audio stream // - the duration is not null - if ((*items).find(MetaData::Type::AudioStreams) == (*items).end() - || boost::any_cast> ((*items)[MetaData::Type::AudioStreams]).empty()) + if (trackInfo->audioStreams.empty()) { LMS_LOG(DBUPDATER, INFO) << "Skipped '" << file.string() << "' (no audio stream found)"; @@ -433,10 +439,9 @@ MediaScanner::scanAudioFile(const boost::filesystem::path& file, bool forceScan, stats.incompleteScans++; return; } - if ((*items).find(MetaData::Type::Duration) == (*items).end() - || boost::any_cast((*items)[MetaData::Type::Duration]).count() <= 0) + if (trackInfo->duration == std::chrono::milliseconds::zero()) { - LMS_LOG(DBUPDATER, INFO) << "Skipped '" << file.string() << "' (no duration or duration <= 0)"; + LMS_LOG(DBUPDATER, INFO) << "Skipped '" << file.string() << "' (duration is 0)"; // If Track exists here, delete it! if (track) @@ -450,10 +455,8 @@ MediaScanner::scanAudioFile(const boost::filesystem::path& file, bool forceScan, // ***** Title std::string title; - if ((*items).find(MetaData::Type::Title) != (*items).end()) - { - title = boost::any_cast((*items)[MetaData::Type::Title]); - } + if (!trackInfo->title.empty()) + title = trackInfo->title; else { // TODO parse file name guess track etc. @@ -462,51 +465,19 @@ MediaScanner::scanAudioFile(const boost::filesystem::path& file, bool forceScan, } // ***** Clusters - std::vector< Cluster::pointer > genres; - { - MetaData::Clusters clusterNames; + std::vector clusters {getClusters(_db.getSession(), trackInfo->clusters)}; - if ((*items).find(MetaData::Type::Clusters) != (*items).end()) - { - clusterNames = boost::any_cast ((*items)[MetaData::Type::Clusters]); - } - - genres = getClusters(_db.getSession(), clusterNames); - } - - // ***** Artist - std::set artists; - { - std::string artistName; - std::string artistMusicBrainzID; - - if ((*items).find(MetaData::Type::MusicBrainzArtistID) != (*items).end()) - artistMusicBrainzID = boost::any_cast((*items)[MetaData::Type::MusicBrainzArtistID] ); - - if ((*items).find(MetaData::Type::Artist) != (*items).end()) - artistName = boost::any_cast((*items)[MetaData::Type::Artist]); - - artist = getArtist(_db.getSession(), artistName, artistMusicBrainzID); - } + // ***** Artists + std::vector artists {getArtists(_db.getSession(), trackInfo->artists)}; // ***** Release Release::pointer release; - { - std::string releaseName; - std::string releaseMusicBrainzID; - - if ((*items).find(MetaData::Type::MusicBrainzAlbumID) != (*items).end()) - releaseMusicBrainzID = boost::any_cast((*items)[MetaData::Type::MusicBrainzAlbumID] ); - - if ((*items).find(MetaData::Type::Album) != (*items).end()) - releaseName = boost::any_cast((*items)[MetaData::Type::Album]); - - release = getRelease(_db.getSession(), releaseName, releaseMusicBrainzID); - } + if (trackInfo->album) + release = getRelease(_db.getSession(), *trackInfo->album); // If file already exist, update data // Otherwise, create it - bool trackAdded = false; + bool trackAdded {false}; if (!track) { // Create a new song @@ -519,9 +490,6 @@ MediaScanner::scanAudioFile(const boost::filesystem::path& file, bool forceScan, { LMS_LOG(DBUPDATER, INFO) << "Updating '" << file.string() << "'"; - // Remove the songs from its clusters - track.modify()->eraseClusters(); - stats.updates++; } @@ -529,74 +497,28 @@ MediaScanner::scanAudioFile(const boost::filesystem::path& file, bool forceScan, track.modify()->setScanVersion(_scanVersion); track.modify()->setChecksum(checksum); - track.modify()->setArtist(artist); + track.modify()->setArtists(artists); track.modify()->setRelease(release); + track.modify()->setClusters(clusters); track.modify()->setLastWriteTime(lastWriteTime); track.modify()->setName(title); - track.modify()->setDuration( boost::any_cast((*items)[MetaData::Type::Duration]) ); - track.modify()->setAddedTime( Wt::WLocalDateTime::currentServerDateTime().toUTC() ); + track.modify()->setDuration(trackInfo->duration); + track.modify()->setAddedTime(Wt::WLocalDateTime::currentServerDateTime().toUTC()); + track.modify()->setTrackNumber(trackInfo->trackNumber ? *trackInfo->trackNumber : 0); + track.modify()->setTotalTrackNumber(trackInfo->totalTrack ? *trackInfo->totalTrack : 0); + track.modify()->setDiscNumber(trackInfo->discNumber ? *trackInfo->discNumber : 0); + track.modify()->setTotalDiscNumber(trackInfo->totalDisc ? *trackInfo->totalDisc : 0); + track.modify()->setYear(trackInfo->year ? *trackInfo->year : 0); + track.modify()->setOriginalYear(trackInfo->originalYear ? *trackInfo->originalYear : 0); - { - std::string trackClusterList; - // Product genre list - for (Cluster::pointer genre : genres) - { - if (!trackClusterList.empty()) - trackClusterList += ", "; - trackClusterList += genre->getName(); + // If a file has an OriginalYear but no Year, set it to ease filtering + if (!trackInfo->year && trackInfo->originalYear) + track.modify()->setYear(*trackInfo->originalYear); - genre.modify()->addTrack(track); - } - - track.modify()->setGenres( trackClusterList ); - } - - if ((*items).find(MetaData::Type::TrackNumber) != (*items).end()) - track.modify()->setTrackNumber( boost::any_cast((*items)[MetaData::Type::TrackNumber]) ); - - if ((*items).find(MetaData::Type::TotalTrack) != (*items).end()) - track.modify()->setTotalTrackNumber( boost::any_cast((*items)[MetaData::Type::TotalTrack]) ); - - if ((*items).find(MetaData::Type::DiscNumber) != (*items).end()) - track.modify()->setDiscNumber( boost::any_cast((*items)[MetaData::Type::DiscNumber]) ); - - if ((*items).find(MetaData::Type::TotalDisc) != (*items).end()) - track.modify()->setTotalDiscNumber( boost::any_cast((*items)[MetaData::Type::TotalDisc]) ); - - if ((*items).find(MetaData::Type::Year) != (*items).end()) - track.modify()->setYear( boost::any_cast((*items)[MetaData::Type::Year]) ); - - if ((*items).find(MetaData::Type::OriginalYear) != (*items).end()) - { - track.modify()->setOriginalYear( boost::any_cast((*items)[MetaData::Type::OriginalYear]) ); - - // If a file has an OriginalYear but no Year, set it o ease filtering - if ((*items).find(MetaData::Type::Year) == (*items).end()) - track.modify()->setYear( boost::any_cast((*items)[MetaData::Type::OriginalYear]) ); - } - - if ((*items).find(MetaData::Type::MusicBrainzRecordingID) != (*items).end()) - { - track.modify()->setMBID( boost::any_cast((*items)[MetaData::Type::MusicBrainzRecordingID]) ); - } - - if ((*items).find(MetaData::Type::HasCover) != (*items).end()) - { - bool hasCover = boost::any_cast((*items)[MetaData::Type::HasCover]); - - track.modify()->setHasCover(hasCover); - } - - if ((*items).find(MetaData::Type::Copyright) != (*items).end()) - { - - track.modify()->setCopyright( boost::any_cast((*items)[MetaData::Type::Copyright]) ); - } - - if ((*items).find(MetaData::Type::CopyrightURL) != (*items).end()) - { - track.modify()->setCopyrightURL( boost::any_cast((*items)[MetaData::Type::CopyrightURL]) ); - } + track.modify()->setMBID(trackInfo->musicBrainzRecordID); + track.modify()->setHasCover(trackInfo->hasCover); + track.modify()->setCopyright(trackInfo->copyright); + track.modify()->setCopyrightURL(trackInfo->copyrightURL); transaction.commit(); diff --git a/src/similarity/features/SimilarityFeaturesScannerAddon.cpp b/src/similarity/features/SimilarityFeaturesScannerAddon.cpp index cda88d8e..9d617c5e 100644 --- a/src/similarity/features/SimilarityFeaturesScannerAddon.cpp +++ b/src/similarity/features/SimilarityFeaturesScannerAddon.cpp @@ -79,13 +79,13 @@ FeaturesScannerAddon::requestStop() void FeaturesScannerAddon::trackUpdated(Database::IdType trackId) { - Wt::Dbo::Transaction transaction(_db.getSession()); + Wt::Dbo::Transaction transaction {_db.getSession()}; - auto track = Database::Track::getById(_db.getSession(), trackId); + auto track {Database::Track::getById(_db.getSession(), trackId)}; if (!track) return; - track.modify()->eraseFeatures(); + track.modify()->setFeatures({}); } void diff --git a/src/similarity/features/SimilarityFeaturesSearcher.cpp b/src/similarity/features/SimilarityFeaturesSearcher.cpp index a9c86a1a..3f086e06 100644 --- a/src/similarity/features/SimilarityFeaturesSearcher.cpp +++ b/src/similarity/features/SimilarityFeaturesSearcher.cpp @@ -279,8 +279,8 @@ FeaturesSearcher::dump(Wt::Dbo::Session& session, std::ostream& os) const continue; os << "\t"; - if (track->getArtist()) - os << track->getArtist()->getName() << " - "; + for (auto artist : track->getArtists()) + os << artist->getName() << " - "; if (track->getRelease()) os << track->getRelease()->getName() << " - "; os << track->getName() << std::endl; @@ -334,10 +334,10 @@ FeaturesSearcher::init(Wt::Dbo::Session& session, _releasePositions[track->getRelease().id()].insert(position); _releasesMap[position].insert(track->getRelease().id()); } - if (track->getArtist()) + for (const auto& artist : track->getArtists()) { - _artistPositions[track->getArtist().id()].insert(position); - _artistsMap[position].insert(track->getArtist().id()); + _artistPositions[artist.id()].insert(position); + _artistsMap[position].insert(artist.id()); } } } diff --git a/src/ui/MediaPlayer.cpp b/src/ui/MediaPlayer.cpp index 832ee77f..da896817 100644 --- a/src/ui/MediaPlayer.cpp +++ b/src/ui/MediaPlayer.cpp @@ -82,10 +82,11 @@ MediaPlayer::loadTrack(Database::IdType trackId, bool play) _title->setText(Wt::WString::fromUTF8(track->getName())); - if (track->getArtist()) + auto artists = track->getArtists(); + if (!artists.empty()) { - _artist->setText(Wt::WString::fromUTF8(track->getArtist()->getName())); - _artist->setLink(LmsApp->createArtistLink(track->getArtist())); + _artist->setText(Wt::WString::fromUTF8(artists.front()->getName())); + _artist->setLink(LmsApp->createArtistLink(artists.front())); } else { diff --git a/src/ui/PlayHistoryView.cpp b/src/ui/PlayHistoryView.cpp index ac44f95a..7e75cd10 100644 --- a/src/ui/PlayHistoryView.cpp +++ b/src/ui/PlayHistoryView.cpp @@ -39,11 +39,11 @@ std::unique_ptr createEntry(Database::Track::pointer track) entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain); - auto artist = track->getArtist(); - if (artist) + auto artists {track->getArtists()}; + if (!artists.empty()) { entry->setCondition("if-has-artist", true); - entry->bindWidget("artist-name", LmsApplication::createArtistAnchor(track->getArtist())); + entry->bindWidget("artist-name", LmsApplication::createArtistAnchor(artists.front())); } auto release = track->getRelease(); if (release) diff --git a/src/ui/PlayQueueView.cpp b/src/ui/PlayQueueView.cpp index 4605813e..004b1b5c 100644 --- a/src/ui/PlayQueueView.cpp +++ b/src/ui/PlayQueueView.cpp @@ -330,11 +330,11 @@ PlayQueue::addSome() entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain); - auto artist = track->getArtist(); - if (artist) + auto artists {track->getArtists()}; + if (!artists.empty()) { entry->setCondition("if-has-artist", true); - entry->bindWidget("artist-name", LmsApplication::createArtistAnchor(track->getArtist())); + entry->bindWidget("artist-name", LmsApplication::createArtistAnchor(artists.front())); } auto release = track->getRelease(); if (release) diff --git a/src/ui/explore/Explore.cpp b/src/ui/explore/Explore.cpp index b2e45ea0..ee37f418 100644 --- a/src/ui/explore/Explore.cpp +++ b/src/ui/explore/Explore.cpp @@ -191,16 +191,7 @@ static std::vector getArtistTracks(Wt::Dbo::Session& s if (!artist) return res; - auto releases = artist->getReleases(clusters); - for (auto release : releases) - { - auto tracks = release->getTracks(clusters); - for (auto track : tracks) - { - if (track->getArtist() && track->getArtist().id() == artistId) - res.push_back(track); - } - } + res = artist->getTracks(); return res; } @@ -213,7 +204,9 @@ static std::vector getReleaseTracks(Wt::Dbo::Session& if (!release) return res; - return release->getTracks(clusters); + res = release->getTracks(clusters); + + return res; } static std::vector getTrack(Wt::Dbo::Session& session, Database::IdType trackId) diff --git a/src/ui/explore/ReleaseLink.cpp b/src/ui/explore/ReleaseLink.cpp index b036152b..16e44925 100644 --- a/src/ui/explore/ReleaseLink.cpp +++ b/src/ui/explore/ReleaseLink.cpp @@ -21,6 +21,7 @@ #include #include +#include #include "database/Release.hpp" #include "resource/ImageResource.hpp" @@ -47,7 +48,7 @@ ReleaseLink::ReleaseLink(Database::Release::pointer release) if (artists.size() > 1) { setCondition("if-has-artist", true); - bindString("artist-name", Wt::WString::tr("Lms.Explore.various-artists")); + bindNew("artist-name", Wt::WString::tr("Lms.Explore.various-artists")); } else if (artists.size() == 1) { diff --git a/src/ui/explore/ReleaseView.cpp b/src/ui/explore/ReleaseView.cpp index 737ac409..9fe68ce5 100644 --- a/src/ui/explore/ReleaseView.cpp +++ b/src/ui/explore/ReleaseView.cpp @@ -50,7 +50,8 @@ Release::Release(Filters* filters) refresh(); - filters->updated().connect([=] { + filters->updated().connect([=] + { refresh(); }); } @@ -62,31 +63,31 @@ Release::refresh() return; clear(); - auto releaseId = readAs(wApp->internalPathNextPart("/release/")); + auto releaseId {readAs(wApp->internalPathNextPart("/release/"))}; if (!releaseId) return; - Wt::Dbo::Transaction transaction(LmsApp->getDboSession()); + Wt::Dbo::Transaction transaction {LmsApp->getDboSession()}; - auto release = Database::Release::getById(LmsApp->getDboSession(), *releaseId); + auto release {Database::Release::getById(LmsApp->getDboSession(), *releaseId)}; if (!release) { LmsApp->goHome(); return; } - Wt::WTemplate* t = addNew(Wt::WString::tr("Lms.Explore.Release.template")); + Wt::WTemplate* t {addNew(Wt::WString::tr("Lms.Explore.Release.template"))}; t->addFunction("tr", &Wt::WTemplate::Functions::tr); t->bindString("name", Wt::WString::fromUTF8(release->getName()), Wt::TextFormat::Plain); - boost::optional year = release->getReleaseYear(); + boost::optional year {release->getReleaseYear()}; if (year) { t->setCondition("if-has-year", true); t->bindInt("year", *year); - boost::optional originalYear = release->getReleaseYear(true); + boost::optional originalYear {release->getReleaseYear(true)}; if (originalYear && *originalYear != *year) { t->setCondition("if-has-orig-year", true); @@ -95,11 +96,11 @@ Release::refresh() } { - auto artists = release->getArtists(); + auto artists {release->getArtists()}; if (artists.size() > 1) { t->setCondition("if-has-artist", true); - t->bindString("artist-name", Wt::WString::tr("Lms.Explore.various-artists")); + t->bindNew("artist-name", Wt::WString::tr("Lms.Explore.various-artists")); } else if (artists.size() == 1) { @@ -110,17 +111,17 @@ Release::refresh() t->bindNew("cover", Wt::WLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 512))); - Wt::WContainerWidget* clusterContainers = t->bindNew("clusters"); + Wt::WContainerWidget* clusterContainers {t->bindNew("clusters")}; { - auto clusterTypes = ScanSettings::get(LmsApp->getDboSession())->getClusterTypes(); - auto clusterGroups = release->getClusterGroups(clusterTypes, 3); + auto clusterTypes {ScanSettings::get(LmsApp->getDboSession())->getClusterTypes()}; + auto clusterGroups {release->getClusterGroups(clusterTypes, 3)}; for (auto clusters : clusterGroups) { for (auto cluster : clusters) { - auto clusterId = cluster.id(); - auto entry = clusterContainers->addWidget(LmsApp->createCluster(cluster)); + auto clusterId {cluster.id()}; + auto entry {clusterContainers->addWidget(LmsApp->createCluster(cluster))}; entry->clicked().connect([=] { _filters->add(clusterId); @@ -130,7 +131,7 @@ Release::refresh() } { - Wt::WText* playBtn = t->bindNew("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML); + Wt::WText* playBtn {t->bindNew("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML)}; playBtn->clicked().connect([=] { releasePlay.emit(*releaseId); @@ -138,55 +139,62 @@ Release::refresh() } { - Wt::WText* addBtn = t->bindNew("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML); + Wt::WText* addBtn {t->bindNew("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML)}; addBtn->clicked().connect([=] { releaseAdd.emit(*releaseId); }); } - Wt::WContainerWidget* tracksContainer = t->bindNew("tracks"); + Wt::WContainerWidget* tracksContainer {t->bindNew("tracks")}; - auto clusterIds = _filters->getClusterIds(); - auto tracks = release->getTracks(clusterIds); + auto clusterIds {_filters->getClusterIds()}; + auto tracks {release->getTracks(clusterIds)}; - bool variousArtists = release->hasVariousArtists(); + bool variousArtists {release->hasVariousArtists()}; - for (auto track : tracks) + for (const auto& track : tracks) { - auto trackId = track.id(); + auto trackId {track.id()}; - Wt::WTemplate* entry = tracksContainer->addNew(Wt::WString::tr("Lms.Explore.Release.template.entry")); + Wt::WTemplate* entry {tracksContainer->addNew(Wt::WString::tr("Lms.Explore.Release.template.entry"))}; entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain); - if (variousArtists && track->getArtist()) + auto artists {track->getArtists()}; + if (variousArtists && !artists.empty()) { - entry->setCondition("if-has-artist", true); - entry->bindWidget("artist-name", LmsApplication::createArtistAnchor(track->getArtist())); + entry->setCondition("if-has-artists", true); + + Wt::WContainerWidget* artistsContainer {entry->bindNew("artists")}; + for (const auto& artist : artists) + { + Wt::WTemplate *t {artistsContainer->addNew(Wt::WString::tr("Lms.Explore.Release.template.entry-artist"))}; + t->bindWidget("artist", LmsApplication::createArtistAnchor(artist)); + } } - auto trackNumber = track->getTrackNumber(); + auto trackNumber {track->getTrackNumber()}; if (trackNumber) { entry->setCondition("if-has-track-number", true); entry->bindInt("track-number", *trackNumber); } - auto discNumber = track->getDiscNumber(); - auto totalDiscNumber = track->getTotalDiscNumber(); + auto discNumber {track->getDiscNumber()}; + auto totalDiscNumber {track->getTotalDiscNumber()}; if (discNumber && totalDiscNumber && *totalDiscNumber > 1) { entry->setCondition("if-has-disc-number", true); entry->bindInt("disc-number", *discNumber); } - Wt::WText* playBtn = entry->bindNew("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML); + Wt::WText* playBtn {entry->bindNew("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML)}; playBtn->clicked().connect(std::bind([=] { trackPlay.emit(trackId); })); - Wt::WText* addBtn = entry->bindNew("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML); + Wt::WText* addBtn {entry->bindNew("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML)}; addBtn->clicked().connect(std::bind([=] { trackAdd.emit(trackId); diff --git a/src/ui/explore/ReleasesView.cpp b/src/ui/explore/ReleasesView.cpp index 657f072e..e23eec66 100644 --- a/src/ui/explore/ReleasesView.cpp +++ b/src/ui/explore/ReleasesView.cpp @@ -100,7 +100,7 @@ Releases::addSome() if (artists.size() > 1) { entry->setCondition("if-has-artist", true); - entry->bindString("artist-name", Wt::WString::tr("Lms.Explore.various-artists")); + entry->bindNew("artist-name", Wt::WString::tr("Lms.Explore.various-artists")); } else if (artists.size() == 1) { diff --git a/src/ui/explore/TracksView.cpp b/src/ui/explore/TracksView.cpp index 3299e832..25074604 100644 --- a/src/ui/explore/TracksView.cpp +++ b/src/ui/explore/TracksView.cpp @@ -116,10 +116,11 @@ Tracks::addSome() entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain); - if (track->getArtist()) + auto artists {track->getArtists()}; + if (!artists.empty()) { entry->setCondition("if-has-artist", true); - entry->bindWidget("artist-name", LmsApplication::createArtistAnchor(track->getArtist())); + entry->bindWidget("artist-name", LmsApplication::createArtistAnchor(artists.front())); } if (track->getRelease()) diff --git a/tools/metadata/LmsMetadata.cpp b/tools/metadata/LmsMetadata.cpp index e2b1e56b..e6b3ff11 100644 --- a/tools/metadata/LmsMetadata.cpp +++ b/tools/metadata/LmsMetadata.cpp @@ -9,118 +9,103 @@ #include "metadata/AvFormat.hpp" #include "metadata/TagLibParser.hpp" +std::ostream& operator<<(std::ostream& os, const MetaData::Artist& artist) +{ + os << artist.name; + + if (!artist.musicBrainzArtistID.empty()) + os << " (" << artist.musicBrainzArtistID << ")"; + + return os; +} + +std::ostream& operator<<(std::ostream& os, const MetaData::Album& album) +{ + os << album.name; + + if (!album.musicBrainzAlbumID.empty()) + os << " (" << album.musicBrainzAlbumID << ")"; + + return os; +} + + void parse(MetaData::Parser& parser, const boost::filesystem::path& file) { + using namespace MetaData; + parser.setClusterTypeNames( {"MOOD", "GENRE"} ); - boost::optional items {parser.parse(file, true)}; - if (!items) + boost::optional track {parser.parse(file, true)}; + if (!track) { std::cerr << "Parsing failed" << std::endl; return; } - std::cout << "Items:" << std::endl; - for (auto item : (*items)) + std::cout << "Track metadata:" << std::endl; + + for (const Artist& artist : track->artists) + std::cout << "Artist: " << artist << std::endl; + + if (track->albumArtist) + std::cout << "Album artist: " << *track->albumArtist << std::endl; + + if (track->album) + std::cout << "Album: " << *track->album << std::endl; + + std::cout << "Title: " << track->title << std::endl; + + if (!track->musicBrainzTrackID.empty()) + std::cout << "MB TrackID = " << track->musicBrainzTrackID << std::endl; + + if (!track->musicBrainzRecordID.empty()) + std::cout << "MB RecordID = " << track->musicBrainzRecordID << std::endl; + + for (const auto& cluster : track->clusters) { - switch (item.first) + std::cout << "Cluster: " << cluster.first << std::endl; + for (const auto& name : cluster.second) { - case MetaData::Type::Title: - std::cout << "Title: " << boost::any_cast(item.second) << std::endl; - break; - - case MetaData::Type::Artists: - for (const std::string& artist : boost::any_cast>(item.second)) - std::cout << "Artist: " << artist << std::endl; - break; - - case MetaData::Type::Album: - std::cout << "Album: " << boost::any_cast(item.second) << std::endl; - break; - - case MetaData::Type::Clusters: - for (const auto& cluster : boost::any_cast(item.second)) - { - std::cout << "Cluster: " << cluster.first << std::endl; - for (const auto name : cluster.second) - { - std::cout << "\t" << name << std::endl; - } - } - break; - - case MetaData::Type::Duration: - std::cout << "Duration: " << boost::any_cast(item.second).count() / 1000 << "s" << std::endl; - break; - - case MetaData::Type::TrackNumber: - std::cout << "Track: " << boost::any_cast(item.second) << std::endl; - break; - - case MetaData::Type::TotalTrack: - std::cout << "TotalTrack: " << boost::any_cast(item.second) << std::endl; - break; - - case MetaData::Type::DiscNumber: - std::cout << "Disc: " << boost::any_cast(item.second) << std::endl; - break; - - case MetaData::Type::TotalDisc: - std::cout << "TotalDisc: " << boost::any_cast(item.second) << std::endl; - break; - - case MetaData::Type::Year: - std::cout << "Year: " << std::to_string(boost::any_cast(item.second)) << std::endl; - break; - - case MetaData::Type::OriginalYear: - std::cout << "Original year: " << std::to_string(boost::any_cast(item.second)) << std::endl; - break; - - case MetaData::Type::HasCover: - std::cout << "HasCover = " << std::boolalpha << boost::any_cast(item.second) << std::endl; - break; - - case MetaData::Type::AudioStreams: - for (const auto& audioStream : boost::any_cast >(item.second)) - std::cout << "Audio stream: " << audioStream.bitRate << " bps" << std::endl; - break; - - case MetaData::Type::MusicBrainzArtistID: - for (const std::string& mbid : boost::any_cast>(item.second)) - std::cout << "MusicBrainzArtistID: " << mbid << std::endl; - break; - - case MetaData::Type::MusicBrainzAlbumID: - std::cout << "MusicBrainzAlbumID: " << boost::any_cast(item.second) << std::endl; - break; - - case MetaData::Type::MusicBrainzTrackID: - std::cout << "MusicBrainzTrackID: " << boost::any_cast(item.second) << std::endl; - break; - - case MetaData::Type::MusicBrainzRecordingID: - std::cout << "MusicBrainzRecordingID: " << boost::any_cast(item.second) << std::endl; - break; - - case MetaData::Type::AcoustID: - std::cout << "AcoustID: " << boost::any_cast(item.second) << std::endl; - break; - - case MetaData::Type::Copyright: - std::cout << "Copyright: " << boost::any_cast(item.second) << std::endl; - break; - - case MetaData::Type::CopyrightURL: - std::cout << "CopyrightURL: " << boost::any_cast(item.second) << std::endl; - break; - - default: - break; + std::cout << "\t" << name << std::endl; } } + std::cout << "Duration: " << track->duration.count() / 1000 << "s" << std::endl; + + if (track->trackNumber) + std::cout << "Track: " << *track->trackNumber << std::endl; + + if (track->totalTrack) + std::cout << "TotalTrack: " << *track->totalTrack << std::endl; + + if (track->discNumber) + std::cout << "Disc: " << *track->discNumber << std::endl; + + if (track->totalDisc) + std::cout << "TotalDisc: " << *track->totalDisc << std::endl; + + if (track->year) + std::cout << "Year: " << *track->year << std::endl; + + if (track->originalYear) + std::cout << "Original year: " << *track->originalYear << std::endl; + + std::cout << "HasCover = " << std::boolalpha << track->hasCover << std::endl; + + for (const auto& audioStream : track->audioStreams) + std::cout << "Audio stream: " << audioStream.bitRate << " bps" << std::endl; + + if (!track->acoustID.empty()) + std::cout << "AcoustID: " << track->acoustID << std::endl; + + if (!track->copyright.empty()) + std::cout << "Copyright: " << track->copyright << std::endl; + + if (!track->copyrightURL.empty()) + std::cout << "CopyrightURL: " << track->copyrightURL << std::endl; + std::cout << std::endl; }