Added multi artists support
This commit is contained in:
@@ -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)</br>
|
||||
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:
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@
|
||||
</div>
|
||||
<div class="media-body media-middle">
|
||||
<h4 class="media-heading">
|
||||
${name}
|
||||
${name class="Lms-explore-releasename"}
|
||||
</h4>
|
||||
${<if-has-various-artists>}<h4><small><i>${tr:Lms.Explore.various-artists}</i></small></h4>${</if-has-various-artists>}
|
||||
<h4><small>${<if-has-year>}${year}${</if-has-year>}${<if-has-orig-year>} (${orig-year})${</if-has-orig-year>}</small></h4>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<message id="Lms.Explore.ArtistLink.template">
|
||||
<div class="row Lms-explore-artistlink Lms-vertical-align">
|
||||
<div class="col-xs-12">
|
||||
${name}
|
||||
${name class="Lms-explore-artistname Lms-explore-artistlink-name"}
|
||||
</div>
|
||||
</div>
|
||||
</message>
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@
|
||||
<message id="Lms.Explore.Artists.template.entry">
|
||||
<div class="row">
|
||||
<div class="col-xs-12 Lms-explore-artists-entry Lms-vertical-align">
|
||||
<h4>${name}</h4>
|
||||
<h4>${name class="Lms-explore-artistname Lms-explore-artists-entry-name"}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</message>
|
||||
|
||||
@@ -20,10 +20,10 @@
|
||||
<h4>${name}</h4>
|
||||
</div>
|
||||
<div class="col-sm-3 hidden-xs">
|
||||
${<if-has-release>}<small>${release-name}</small>${</if-has-release>}
|
||||
${<if-has-release>}<small>${release-name class="Lms-explore-releasename"}</small>${</if-has-release>}
|
||||
</div>
|
||||
<div class="col-sm-3 hidden-xs">
|
||||
${<if-has-artist>}<small><i>${artist-name}</i></small>${</if-has-artist>}
|
||||
${<if-has-artist>}<small>${artist-name class="Lms-explore-artistname"}</small>${</if-has-artist>}
|
||||
</div>
|
||||
</div>
|
||||
</message>
|
||||
|
||||
@@ -46,10 +46,10 @@
|
||||
<h4>${name}</h4>
|
||||
</div>
|
||||
<div class="col-sm-3 hidden-xs">
|
||||
${<if-has-release>}<small>${release-name}</small>${</if-has-release>}
|
||||
${<if-has-release>}<small>${release-name class="Lms-explore-releasename"}</small>${</if-has-release>}
|
||||
</div>
|
||||
<div class="col-sm-3 hidden-xs">
|
||||
${<if-has-artist>}<small><i>${artist-name}</i></small>${</if-has-artist>}
|
||||
${<if-has-artist>}<small>${artist-name class="Lms-explore-artistname"}</small>${</if-has-artist>}
|
||||
</div>
|
||||
<div class="col-xs-3 Lms-playqueue-entry-controls">
|
||||
${play-btn}${del-btn}
|
||||
|
||||
+11
-4
@@ -14,7 +14,7 @@
|
||||
</div>
|
||||
<div class="page-header">
|
||||
<h3>${name}</h3>
|
||||
${<if-has-artist>}<h4><i>${artist-name}</i></h4>${</if-has-artist>}
|
||||
${<if-has-artist>}<h4><i>${artist-name class="Lms-explore-artistname"}</i></h4>${</if-has-artist>}
|
||||
<h4><small>${<if-has-year>}${year}${</if-has-year>}${<if-has-orig-year>} (${orig-year})${</if-has-orig-year>}</small></h4>
|
||||
${clusters}
|
||||
${play-btn}${add-btn}
|
||||
@@ -24,17 +24,24 @@
|
||||
|
||||
<message id="Lms.Explore.Release.template.entry">
|
||||
<div class="row Lms-explore-release-entry Lms-vertical-align">
|
||||
<div class="col-xs-9 Lms-explore-release-entry-name">
|
||||
${<if-has-disc-number>}${disc-number}-${</if-has-disc-number>}${<if-has-track-number>}${track-number}. ${</if-has-track-number>}
|
||||
${<if-has-artist>}<i>${artist-name}</i> - ${</if-has-artist>}
|
||||
<div class="col-xs-1 Lms-explore-release-entry-tracknumber">
|
||||
${<if-has-disc-number>}${disc-number}-${</if-has-disc-number>}${<if-has-track-number>}${track-number}${</if-has-track-number>}
|
||||
</div>
|
||||
<div class="col-xs-8 Lms-explore-release-entry-name">
|
||||
${name}
|
||||
${<if-has-artists>}<small>${artists}</small>${</if-has-artists>}
|
||||
</div>
|
||||
<div class="col-xs-3 Lms-explore-release-entry-controls">
|
||||
${play-btn}${add-btn}
|
||||
</div>
|
||||
</div>
|
||||
</message>
|
||||
|
||||
<message id="Lms.Explore.Release.template.entry-artist">
|
||||
${artist class="Lms-explore-artistname Lms-explore-release-entry-artist"}
|
||||
</message>
|
||||
|
||||
|
||||
|
||||
|
||||
</messages>
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
</div>
|
||||
<div class="media-body media-middle">
|
||||
<div class="media-heading">
|
||||
${release-name}
|
||||
${release-name class="Lms-explore-releasename"}
|
||||
</div>
|
||||
${<if-has-artist>}<small><i>${artist-name}</i></small>${</if-has-artist>}
|
||||
${<if-has-artist>}<small>${artist-name class="Lms-explore-artistname Lms-explore-releaselink-artist"}</small>${</if-has-artist>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -29,9 +29,9 @@
|
||||
</div>
|
||||
<div class="media-body media-middle">
|
||||
<h4 class="media-heading">
|
||||
${release-name}
|
||||
${release-name class="Lms-explore-releasename"}
|
||||
</h4>
|
||||
${<if-has-artist>}<h4><small><i>${artist-name}</i></small></h4>${</if-has-artist>}
|
||||
${<if-has-artist>}<h4><small>${artist-name class="Lms-explore-artistname"}</small></h4>${</if-has-artist>}
|
||||
${play-btn}${add-btn}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+42
-1
@@ -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;
|
||||
|
||||
+3
-1
@@ -19,6 +19,8 @@
|
||||
|
||||
#include "AvInfo.hpp"
|
||||
|
||||
#include <boost/algorithm/string.hpp>
|
||||
|
||||
#include <array>
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
@@ -93,7 +95,7 @@ getMetaDataFromDictionnary(AVDictionary* dictionnary, std::map<std::string, std:
|
||||
AVDictionaryEntry *tag = NULL;
|
||||
while ((tag = av_dict_get(dictionnary, "", tag, AV_DICT_IGNORE_SUFFIX)))
|
||||
{
|
||||
res.insert( std::make_pair(tag->key, tag->value));
|
||||
res[boost::to_upper_copy<std::string>(tag->key)] = tag->value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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::pointer>
|
||||
Artist::getByName(Wt::Dbo::Session& session, const std::string& name)
|
||||
{
|
||||
Wt::Dbo::collection<Artist::pointer> res = session.find<Artist>().where("name = ?").bind( std::string(name, 0, _maxNameLength) );
|
||||
Wt::Dbo::collection<Artist::pointer> res = session.find<Artist>().where("name = ?").bind( std::string{name, 0, _maxNameLength} );
|
||||
return std::vector<Artist::pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ Artist::getAll(Wt::Dbo::Session& session, int offset, int size)
|
||||
std::vector<Artist::pointer>
|
||||
Artist::getAllOrphans(Wt::Dbo::Session& session)
|
||||
{
|
||||
Wt::Dbo::collection<Artist::pointer> res = session.query< Wt::Dbo::ptr<Artist> >("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<Artist::pointer> res = session.query< Wt::Dbo::ptr<Artist> >("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<pointer>(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::pointer>
|
||||
Artist::getLastAdded(Wt::Dbo::Session& session, Wt::WDateTime after, int limit)
|
||||
{
|
||||
Wt::Dbo::collection<Artist::pointer> res = session.query<Artist::pointer>("SELECT a from artist a INNER JOIN track t ON a.id = t.artist_id")
|
||||
Wt::Dbo::collection<Artist::pointer> res = session.query<Artist::pointer>("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<IdType>& 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<ClusterType::pointer> 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()));
|
||||
{
|
||||
|
||||
@@ -60,8 +60,8 @@ class Artist : public Wt::Dbo::Dbo<Artist>
|
||||
static std::vector<pointer> 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<Wt::Dbo::ptr<Release>> getReleases(const std::set<IdType>& clusterIds = std::set<IdType>()) const;
|
||||
@@ -84,7 +84,7 @@ class Artist : public Wt::Dbo::Dbo<Artist>
|
||||
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:
|
||||
|
||||
@@ -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)");
|
||||
|
||||
@@ -225,7 +225,7 @@ Release::getArtists() const
|
||||
assert(session());
|
||||
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> res = session()->query<Wt::Dbo::ptr<Artist>>(
|
||||
"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());
|
||||
|
||||
|
||||
@@ -218,6 +218,28 @@ Track::getByFilter(Wt::Dbo::Session& session,
|
||||
return getByFilter(session, clusters, std::vector<std::string>(), -1, -1, moreResults);
|
||||
}
|
||||
|
||||
void
|
||||
Track::setArtists(const std::vector<Wt::Dbo::ptr<Artist>>& artists)
|
||||
{
|
||||
_artists.clear();
|
||||
for (const Wt::Dbo::ptr<Artist>& artist : artists)
|
||||
_artists.insert(artist);
|
||||
}
|
||||
|
||||
void
|
||||
Track::setClusters(const std::vector<Wt::Dbo::ptr<Cluster>>& clusters)
|
||||
{
|
||||
_clusters.clear();
|
||||
for (const Wt::Dbo::ptr<Cluster>& cluster : clusters)
|
||||
_clusters.insert(cluster);
|
||||
}
|
||||
|
||||
void
|
||||
Track::setFeatures(const Wt::Dbo::ptr<TrackFeatures>& features)
|
||||
{
|
||||
_trackFeatures = features;
|
||||
}
|
||||
|
||||
boost::optional<std::size_t>
|
||||
Track::getTrackNumber(void) const
|
||||
{
|
||||
|
||||
@@ -75,9 +75,6 @@ class Track : public Wt::Dbo::Dbo<Track>
|
||||
// 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<Track>
|
||||
void setChecksum(const std::vector<unsigned char>& 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<Wt::Dbo::ptr<Artist>>& artists);
|
||||
void setArtists(const std::vector<Wt::Dbo::ptr<Artist>>& artists);
|
||||
void setRelease(Wt::Dbo::ptr<Release> release) { _release = release; }
|
||||
void eraseClusters() { _clusters.clear(); }
|
||||
void eraseFeatures() { /*_trackFeatures.reset();*/ }
|
||||
void setClusters(const std::vector<Wt::Dbo::ptr<Cluster>>& clusters );
|
||||
void setFeatures(const Wt::Dbo::ptr<TrackFeatures>& features);
|
||||
|
||||
std::size_t getScanVersion() const { return _scanVersion; }
|
||||
boost::optional<std::size_t> getTrackNumber() const;
|
||||
@@ -138,7 +134,6 @@ class Track : public Wt::Dbo::Dbo<Track>
|
||||
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<Track>
|
||||
std::chrono::duration<int, std::milli> _duration;
|
||||
int _year = 0;
|
||||
int _originalYear = 0;
|
||||
std::string _genreList;
|
||||
std::string _filePath;
|
||||
std::vector<unsigned char> _fileChecksum;
|
||||
Wt::WDateTime _fileLastWrite;
|
||||
|
||||
@@ -198,7 +198,7 @@ TrackList::getTopArtists(int limit) const
|
||||
assert(session());
|
||||
assert(IdIsValid(self()->id()));
|
||||
|
||||
Wt::Dbo::collection<Artist::pointer> res = session()->query<Artist::pointer>("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<Artist::pointer> res = session()->query<Artist::pointer>("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")
|
||||
|
||||
+124
-94
@@ -19,8 +19,6 @@
|
||||
|
||||
#include "AvFormat.hpp"
|
||||
|
||||
#include <boost/algorithm/string.hpp>
|
||||
|
||||
#include "av/AvInfo.hpp"
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
@@ -29,167 +27,199 @@
|
||||
namespace MetaData
|
||||
{
|
||||
|
||||
boost::optional<Items>
|
||||
using MetadataMap = std::map<std::string, std::string>;
|
||||
|
||||
boost::optional<std::string>
|
||||
findFirstValueOf(const MetadataMap& metadataMap, std::initializer_list<std::string> 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<Album>
|
||||
getAlbum(const MetadataMap& metadataMap)
|
||||
{
|
||||
boost::optional<Album> 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<Artist>
|
||||
getAlbumArtist(const MetadataMap& metadataMap)
|
||||
{
|
||||
boost::optional<Artist> 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<Artist>
|
||||
getArtists(const MetadataMap& metadataMap)
|
||||
{
|
||||
std::vector<Artist> artists;
|
||||
|
||||
std::vector<std::string> 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<std::string> 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<Track>
|
||||
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<AudioStream> 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<unsigned>(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<std::string, std::string> metadataMap = mediaFile.getMetaData();
|
||||
for (auto metadata : metadataMap)
|
||||
const std::map<std::string, std::string> 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<std::string>(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<std::string>{ stringTrim( value) };
|
||||
}
|
||||
else if (tag == "ARTISTS")
|
||||
{
|
||||
std::vector<std::string> strings {splitString(value, "/;")}; // Picard separator is '/'
|
||||
|
||||
std::set<std::string> 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<std::string> strings {splitString(value, "/") };
|
||||
|
||||
if (strings.size() > 0)
|
||||
{
|
||||
auto number = readAs<std::size_t>(strings[0]);
|
||||
if (number)
|
||||
items.insert( std::make_pair(MetaData::Type::TrackNumber, *number ));
|
||||
track.trackNumber = readAs<std::size_t>(strings[0]);
|
||||
|
||||
if (strings.size() > 1)
|
||||
{
|
||||
auto totalNumber = readAs<std::size_t>(strings[1]);
|
||||
if (totalNumber)
|
||||
items.insert( std::make_pair(MetaData::Type::TotalTrack, *totalNumber ));
|
||||
}
|
||||
track.totalTrack = readAs<std::size_t>(strings[1]);
|
||||
}
|
||||
}
|
||||
else if (tag == "DISC")
|
||||
{
|
||||
// Expecting 'Number/Total'
|
||||
auto strings = splitString(value, "/");
|
||||
std::vector<std::string> strings {splitString(value, "/")};
|
||||
|
||||
if (strings.size() > 0)
|
||||
{
|
||||
auto number = readAs<std::size_t>(strings[0]);
|
||||
if (number)
|
||||
items.insert( std::make_pair(MetaData::Type::DiscNumber, *number ));
|
||||
track.discNumber = readAs<std::size_t>(strings[0]);
|
||||
|
||||
if (strings.size() > 1)
|
||||
{
|
||||
auto totalNumber = readAs<std::size_t>(strings[1]);
|
||||
if (totalNumber)
|
||||
items.insert( std::make_pair(MetaData::Type::TotalDisc, *totalNumber ));
|
||||
}
|
||||
track.totalDisc = readAs<std::size_t>(strings[1]);
|
||||
}
|
||||
}
|
||||
else if (tag == "DATE"
|
||||
|| tag == "YEAR"
|
||||
|| tag == "WM/Year")
|
||||
{
|
||||
auto date = readAs<int>(value);
|
||||
if (date)
|
||||
items.insert(std::make_pair(MetaData::Type::Year, *date));
|
||||
track.year = readAs<int>(value);
|
||||
}
|
||||
else if (tag == "TDOR" // Original release time (ID3v2 2.4)
|
||||
|| tag == "TORY") // Original release year
|
||||
{
|
||||
auto date = readAs<int>(value);
|
||||
if (date)
|
||||
items.insert(std::make_pair(MetaData::Type::OriginalYear, *date));
|
||||
track.originalYear = readAs<int>(value);
|
||||
}
|
||||
else if (tag == "MUSICBRAINZ ARTIST ID"
|
||||
|| tag == "MUSICBRAINZ_ARTISTID")
|
||||
else if (tag == "ACOUSTID ID")
|
||||
{
|
||||
std::vector<std::string> strings {splitString(value, "/")};
|
||||
|
||||
std::set<std::string> 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<std::string> clusterNames = splitString(value, "/,;");
|
||||
std::vector<std::string> clusterNames {splitString(value, "/,;")};
|
||||
|
||||
if (!clusterNames.empty())
|
||||
{
|
||||
clusters[tag] = std::set<std::string>(clusterNames.begin(), clusterNames.end());
|
||||
}
|
||||
track.clusters[tag] = std::set<std::string>{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
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace MetaData
|
||||
class AvFormat : public Parser
|
||||
{
|
||||
public:
|
||||
boost::optional<Items> parse(const boost::filesystem::path& p, bool debug = false) override;
|
||||
boost::optional<Track> parse(const boost::filesystem::path& p, bool debug = false) override;
|
||||
};
|
||||
|
||||
} // namespace MetaData
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <map>
|
||||
#include <set>
|
||||
|
||||
@@ -39,7 +40,7 @@ namespace MetaData
|
||||
{
|
||||
std::string name;
|
||||
std::string musicBrainzAlbumID;
|
||||
}
|
||||
};
|
||||
|
||||
struct AudioStream
|
||||
{
|
||||
@@ -64,7 +65,7 @@ namespace MetaData
|
||||
boost::optional<int> originalYear;
|
||||
bool hasCover {false};
|
||||
std::vector<AudioStream> audioStreams;
|
||||
std::string acoustId;
|
||||
std::string acoustID;
|
||||
std::string copyright;
|
||||
std::string copyrightURL;
|
||||
};
|
||||
|
||||
+134
-106
@@ -31,7 +31,95 @@
|
||||
namespace MetaData
|
||||
{
|
||||
|
||||
boost::optional<Items>
|
||||
static
|
||||
std::vector<std::string>
|
||||
splitAndTrimString(const std::string& str, const std::string& delimiters)
|
||||
{
|
||||
std::vector<std::string> res;
|
||||
|
||||
std::vector<std::string> strings {splitString(str, delimiters)};
|
||||
for (const std::string& s : strings)
|
||||
res.emplace_back(stringTrim(s));
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
static
|
||||
std::vector<std::string>
|
||||
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<Artist>
|
||||
getArtists(const TagLib::PropertyMap& properties)
|
||||
{
|
||||
std::vector<Artist> res;
|
||||
|
||||
if (properties.contains("ARTISTS"))
|
||||
{
|
||||
const TagLib::StringList& values {properties["ARTISTS"]};
|
||||
|
||||
std::vector<std::string> artists {splitAndTrimString(values.front().to8Bit(true), "/;")}; // Picard separator is '/'
|
||||
std::vector<std::string> 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<Artist>
|
||||
getAlbumArtist(const TagLib::PropertyMap& properties)
|
||||
{
|
||||
boost::optional<Artist> 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<Album>
|
||||
getAlbum(const TagLib::PropertyMap& properties)
|
||||
{
|
||||
boost::optional<Album> 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<Track>
|
||||
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<std::size_t>(properties->bitrate() * 1000)};
|
||||
items[MetaData::Type::AudioStreams] = std::vector<MetaData::AudioStream> {audioStream};
|
||||
MetaData::AudioStream audioStream {.bitRate = static_cast<unsigned>(properties->bitrate() * 1000)};
|
||||
track.audioStreams = {std::move(audioStream)};
|
||||
}
|
||||
|
||||
// Not that good embedded pictures handling
|
||||
|
||||
// MP3
|
||||
if (TagLib::MPEG::File *mp3File = dynamic_cast<TagLib::MPEG::File*>(f.file()))
|
||||
if (TagLib::MPEG::File *mp3File {dynamic_cast<TagLib::MPEG::File*>(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<std::string> 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<std::string>{ stringTrim( values.front().to8Bit(true)) };
|
||||
}
|
||||
else if (tag == "ARTISTS")
|
||||
{
|
||||
// Higher priority than ARTISTS
|
||||
std::vector<std::string> strings {splitString(values.front().to8Bit(), "/;")}; // Picard separator is '/'
|
||||
|
||||
std::vector<std::string> 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<std::string> strings {splitString(values.front().to8Bit(), "/")}; // Picard separator is '/'
|
||||
|
||||
std::vector<std::string> 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<std::size_t>(values.front().to8Bit(true)) };
|
||||
auto totalTrack = readAs<std::size_t>(value);
|
||||
if (totalTrack)
|
||||
items[MetaData::Type::TotalTrack] = *totalTrack;
|
||||
track.totalTrack = totalTrack;
|
||||
}
|
||||
else if (tag == "TRACKNUMBER")
|
||||
{
|
||||
// Expecting 'Number/Total'
|
||||
std::vector<std::string> strings {splitString(values.front().to8Bit(), "/")};
|
||||
std::vector<std::string> strings {splitAndTrimString(value, "/")};
|
||||
|
||||
if (!strings.empty())
|
||||
{
|
||||
auto number {readAs<std::size_t>(strings[0])};
|
||||
if (number)
|
||||
items.insert( std::make_pair(MetaData::Type::TrackNumber, *number ));
|
||||
track.trackNumber = readAs<std::size_t>(strings[0]);
|
||||
|
||||
// Lower priority than TRACKTOTAL
|
||||
if (strings.size() > 1 && items.find(MetaData::Type::TotalTrack) == items.end())
|
||||
{
|
||||
auto totalTrack {readAs<std::size_t>(strings[1])};
|
||||
if (totalTrack)
|
||||
items[MetaData::Type::TotalTrack] = *totalTrack;
|
||||
}
|
||||
if (strings.size() > 1 && !track.totalTrack)
|
||||
track.totalTrack = readAs<std::size_t>(strings[1]);
|
||||
}
|
||||
}
|
||||
else if (tag == "DISCTOTAL")
|
||||
{
|
||||
auto totalDisc = readAs<std::size_t>(values.front().to8Bit(true));
|
||||
auto totalDisc = readAs<std::size_t>(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<std::string> strings {splitString(value, "/")};
|
||||
|
||||
if (!strings.empty())
|
||||
{
|
||||
auto number = readAs<std::size_t>(strings[0]);
|
||||
if (number)
|
||||
items.insert( std::make_pair(MetaData::Type::DiscNumber, *number));
|
||||
track.discNumber = readAs<std::size_t>(strings[0]);
|
||||
|
||||
// Lower priority than DISCTOTAL
|
||||
if (strings.size() > 1 && items.find(MetaData::Type::TotalDisc) == items.end())
|
||||
{
|
||||
auto totalDisc = readAs<std::size_t>(strings[1]);
|
||||
if (totalDisc)
|
||||
items[MetaData::Type::TotalDisc] = *totalDisc;
|
||||
}
|
||||
if (strings.size() > 1 && !track.totalDisc)
|
||||
track.totalDisc = readAs<std::size_t>(strings[1]);
|
||||
}
|
||||
}
|
||||
else if (tag == "DATE")
|
||||
track.year = readAs<int>(value);
|
||||
else if (tag == "ORIGINALDATE" && !track.originalYear)
|
||||
{
|
||||
auto timePoint = readAs<int>(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<int>(values.front().to8Bit());
|
||||
if (timePoint)
|
||||
items[MetaData::Type::OriginalYear] = *timePoint;
|
||||
}
|
||||
// Lower priority than ORIGINALYEAR
|
||||
track.originalYear = readAs<int>(value);
|
||||
}
|
||||
else if (tag == "ORIGINALYEAR")
|
||||
{
|
||||
auto timePoint = readAs<int>(values.front().to8Bit());
|
||||
if (timePoint)
|
||||
{
|
||||
// Take priority on original year
|
||||
items[MetaData::Type::OriginalYear] = *timePoint;
|
||||
}
|
||||
// Higher priority than ORIGINALDATE
|
||||
auto originalYear = readAs<int>(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<std::string> 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
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace MetaData
|
||||
class TagLibParser : public Parser
|
||||
{
|
||||
public:
|
||||
boost::optional<Items> parse(const boost::filesystem::path& p, bool debug = false) override;
|
||||
boost::optional<Track> parse(const boost::filesystem::path& p, bool debug = false) override;
|
||||
};
|
||||
|
||||
} // namespace MetaData
|
||||
|
||||
+72
-150
@@ -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<Artist::pointer>
|
||||
getArtists(Wt::Dbo::Session& session, const std::vector<MetaData::Artist>& artistsInfo)
|
||||
{
|
||||
Artist::pointer artist;
|
||||
std::vector<Artist::pointer> 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<Cluster::pointer>
|
||||
@@ -400,8 +407,8 @@ MediaScanner::scanAudioFile(const boost::filesystem::path& file, bool forceScan,
|
||||
}
|
||||
}
|
||||
|
||||
boost::optional<MetaData::Items> items = _metadataParser.parse(file);
|
||||
if (!items)
|
||||
boost::optional<MetaData::Track> trackInfo {_metadataParser.parse(file)};
|
||||
if (!trackInfo)
|
||||
{
|
||||
stats.scanErrors++;
|
||||
return;
|
||||
@@ -412,15 +419,14 @@ MediaScanner::scanAudioFile(const boost::filesystem::path& file, bool forceScan,
|
||||
std::vector<unsigned char> checksum ;
|
||||
computeCrc(file, checksum);
|
||||
|
||||
Wt::Dbo::Transaction transaction(_db.getSession());
|
||||
Wt::Dbo::Transaction transaction {_db.getSession()};
|
||||
|
||||
Wt::Dbo::ptr<Track> track = Track::getByPath(_db.getSession(), file);
|
||||
Wt::Dbo::ptr<Track> 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<std::vector<MetaData::AudioStream>> ((*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<std::chrono::milliseconds>((*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<std::string>((*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<Cluster::pointer> clusters {getClusters(_db.getSession(), trackInfo->clusters)};
|
||||
|
||||
if ((*items).find(MetaData::Type::Clusters) != (*items).end())
|
||||
{
|
||||
clusterNames = boost::any_cast<MetaData::Clusters> ((*items)[MetaData::Type::Clusters]);
|
||||
}
|
||||
|
||||
genres = getClusters(_db.getSession(), clusterNames);
|
||||
}
|
||||
|
||||
// ***** Artist
|
||||
std::set<Artist::pointer> artists;
|
||||
{
|
||||
std::string artistName;
|
||||
std::string artistMusicBrainzID;
|
||||
|
||||
if ((*items).find(MetaData::Type::MusicBrainzArtistID) != (*items).end())
|
||||
artistMusicBrainzID = boost::any_cast<std::string>((*items)[MetaData::Type::MusicBrainzArtistID] );
|
||||
|
||||
if ((*items).find(MetaData::Type::Artist) != (*items).end())
|
||||
artistName = boost::any_cast<std::string>((*items)[MetaData::Type::Artist]);
|
||||
|
||||
artist = getArtist(_db.getSession(), artistName, artistMusicBrainzID);
|
||||
}
|
||||
// ***** Artists
|
||||
std::vector<Artist::pointer> 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<std::string>((*items)[MetaData::Type::MusicBrainzAlbumID] );
|
||||
|
||||
if ((*items).find(MetaData::Type::Album) != (*items).end())
|
||||
releaseName = boost::any_cast<std::string>((*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<std::chrono::milliseconds>((*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<std::size_t>((*items)[MetaData::Type::TrackNumber]) );
|
||||
|
||||
if ((*items).find(MetaData::Type::TotalTrack) != (*items).end())
|
||||
track.modify()->setTotalTrackNumber( boost::any_cast<std::size_t>((*items)[MetaData::Type::TotalTrack]) );
|
||||
|
||||
if ((*items).find(MetaData::Type::DiscNumber) != (*items).end())
|
||||
track.modify()->setDiscNumber( boost::any_cast<std::size_t>((*items)[MetaData::Type::DiscNumber]) );
|
||||
|
||||
if ((*items).find(MetaData::Type::TotalDisc) != (*items).end())
|
||||
track.modify()->setTotalDiscNumber( boost::any_cast<std::size_t>((*items)[MetaData::Type::TotalDisc]) );
|
||||
|
||||
if ((*items).find(MetaData::Type::Year) != (*items).end())
|
||||
track.modify()->setYear( boost::any_cast<int>((*items)[MetaData::Type::Year]) );
|
||||
|
||||
if ((*items).find(MetaData::Type::OriginalYear) != (*items).end())
|
||||
{
|
||||
track.modify()->setOriginalYear( boost::any_cast<int>((*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<int>((*items)[MetaData::Type::OriginalYear]) );
|
||||
}
|
||||
|
||||
if ((*items).find(MetaData::Type::MusicBrainzRecordingID) != (*items).end())
|
||||
{
|
||||
track.modify()->setMBID( boost::any_cast<std::string>((*items)[MetaData::Type::MusicBrainzRecordingID]) );
|
||||
}
|
||||
|
||||
if ((*items).find(MetaData::Type::HasCover) != (*items).end())
|
||||
{
|
||||
bool hasCover = boost::any_cast<bool>((*items)[MetaData::Type::HasCover]);
|
||||
|
||||
track.modify()->setHasCover(hasCover);
|
||||
}
|
||||
|
||||
if ((*items).find(MetaData::Type::Copyright) != (*items).end())
|
||||
{
|
||||
|
||||
track.modify()->setCopyright( boost::any_cast<std::string>((*items)[MetaData::Type::Copyright]) );
|
||||
}
|
||||
|
||||
if ((*items).find(MetaData::Type::CopyrightURL) != (*items).end())
|
||||
{
|
||||
track.modify()->setCopyrightURL( boost::any_cast<std::string>((*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();
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -39,11 +39,11 @@ std::unique_ptr<Wt::WTemplate> 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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -191,16 +191,7 @@ static std::vector<Database::Track::pointer> 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<Database::Track::pointer> getReleaseTracks(Wt::Dbo::Session&
|
||||
if (!release)
|
||||
return res;
|
||||
|
||||
return release->getTracks(clusters);
|
||||
res = release->getTracks(clusters);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
static std::vector<Database::Track::pointer> getTrack(Wt::Dbo::Session& session, Database::IdType trackId)
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
#include <Wt/WAnchor.h>
|
||||
#include <Wt/WImage.h>
|
||||
#include <Wt/WText.h>
|
||||
|
||||
#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<Wt::WText>("artist-name", Wt::WString::tr("Lms.Explore.various-artists"));
|
||||
}
|
||||
else if (artists.size() == 1)
|
||||
{
|
||||
|
||||
@@ -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<Database::IdType>(wApp->internalPathNextPart("/release/"));
|
||||
auto releaseId {readAs<Database::IdType>(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::WTemplate>(Wt::WString::tr("Lms.Explore.Release.template"));
|
||||
Wt::WTemplate* t {addNew<Wt::WTemplate>(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<int> year = release->getReleaseYear();
|
||||
boost::optional<int> year {release->getReleaseYear()};
|
||||
if (year)
|
||||
{
|
||||
t->setCondition("if-has-year", true);
|
||||
t->bindInt("year", *year);
|
||||
|
||||
boost::optional<int> originalYear = release->getReleaseYear(true);
|
||||
boost::optional<int> 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<Wt::WText>("artist-name", Wt::WString::tr("Lms.Explore.various-artists"));
|
||||
}
|
||||
else if (artists.size() == 1)
|
||||
{
|
||||
@@ -110,17 +111,17 @@ Release::refresh()
|
||||
|
||||
t->bindNew<Wt::WImage>("cover", Wt::WLink(LmsApp->getImageResource()->getReleaseUrl(release.id(), 512)));
|
||||
|
||||
Wt::WContainerWidget* clusterContainers = t->bindNew<Wt::WContainerWidget>("clusters");
|
||||
Wt::WContainerWidget* clusterContainers {t->bindNew<Wt::WContainerWidget>("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<Wt::WText>("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML);
|
||||
Wt::WText* playBtn {t->bindNew<Wt::WText>("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<Wt::WText>("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML);
|
||||
Wt::WText* addBtn {t->bindNew<Wt::WText>("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML)};
|
||||
addBtn->clicked().connect([=]
|
||||
{
|
||||
releaseAdd.emit(*releaseId);
|
||||
});
|
||||
}
|
||||
|
||||
Wt::WContainerWidget* tracksContainer = t->bindNew<Wt::WContainerWidget>("tracks");
|
||||
Wt::WContainerWidget* tracksContainer {t->bindNew<Wt::WContainerWidget>("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::WTemplate>(Wt::WString::tr("Lms.Explore.Release.template.entry"));
|
||||
Wt::WTemplate* entry {tracksContainer->addNew<Wt::WTemplate>(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<Wt::WContainerWidget>("artists")};
|
||||
for (const auto& artist : artists)
|
||||
{
|
||||
Wt::WTemplate *t {artistsContainer->addNew<Wt::WTemplate>(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<Wt::WText>("play-btn", Wt::WString::tr("Lms.Explore.template.play-btn"), Wt::TextFormat::XHTML);
|
||||
Wt::WText* playBtn {entry->bindNew<Wt::WText>("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<Wt::WText>("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML);
|
||||
Wt::WText* addBtn {entry->bindNew<Wt::WText>("add-btn", Wt::WString::tr("Lms.Explore.template.add-btn"), Wt::TextFormat::XHTML)};
|
||||
addBtn->clicked().connect(std::bind([=]
|
||||
{
|
||||
trackAdd.emit(trackId);
|
||||
|
||||
@@ -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<Wt::WText>("artist-name", Wt::WString::tr("Lms.Explore.various-artists"));
|
||||
}
|
||||
else if (artists.size() == 1)
|
||||
{
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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<MetaData::Items> items {parser.parse(file, true)};
|
||||
if (!items)
|
||||
boost::optional<Track> 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<std::string>(item.second) << std::endl;
|
||||
break;
|
||||
|
||||
case MetaData::Type::Artists:
|
||||
for (const std::string& artist : boost::any_cast<std::set<std::string>>(item.second))
|
||||
std::cout << "Artist: " << artist << std::endl;
|
||||
break;
|
||||
|
||||
case MetaData::Type::Album:
|
||||
std::cout << "Album: " << boost::any_cast<std::string>(item.second) << std::endl;
|
||||
break;
|
||||
|
||||
case MetaData::Type::Clusters:
|
||||
for (const auto& cluster : boost::any_cast<MetaData::Clusters>(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<std::chrono::milliseconds>(item.second).count() / 1000 << "s" << std::endl;
|
||||
break;
|
||||
|
||||
case MetaData::Type::TrackNumber:
|
||||
std::cout << "Track: " << boost::any_cast<std::size_t>(item.second) << std::endl;
|
||||
break;
|
||||
|
||||
case MetaData::Type::TotalTrack:
|
||||
std::cout << "TotalTrack: " << boost::any_cast<std::size_t>(item.second) << std::endl;
|
||||
break;
|
||||
|
||||
case MetaData::Type::DiscNumber:
|
||||
std::cout << "Disc: " << boost::any_cast<std::size_t>(item.second) << std::endl;
|
||||
break;
|
||||
|
||||
case MetaData::Type::TotalDisc:
|
||||
std::cout << "TotalDisc: " << boost::any_cast<std::size_t>(item.second) << std::endl;
|
||||
break;
|
||||
|
||||
case MetaData::Type::Year:
|
||||
std::cout << "Year: " << std::to_string(boost::any_cast<int>(item.second)) << std::endl;
|
||||
break;
|
||||
|
||||
case MetaData::Type::OriginalYear:
|
||||
std::cout << "Original year: " << std::to_string(boost::any_cast<int>(item.second)) << std::endl;
|
||||
break;
|
||||
|
||||
case MetaData::Type::HasCover:
|
||||
std::cout << "HasCover = " << std::boolalpha << boost::any_cast<bool>(item.second) << std::endl;
|
||||
break;
|
||||
|
||||
case MetaData::Type::AudioStreams:
|
||||
for (const auto& audioStream : boost::any_cast<std::vector<MetaData::AudioStream> >(item.second))
|
||||
std::cout << "Audio stream: " << audioStream.bitRate << " bps" << std::endl;
|
||||
break;
|
||||
|
||||
case MetaData::Type::MusicBrainzArtistID:
|
||||
for (const std::string& mbid : boost::any_cast<std::set<std::string>>(item.second))
|
||||
std::cout << "MusicBrainzArtistID: " << mbid << std::endl;
|
||||
break;
|
||||
|
||||
case MetaData::Type::MusicBrainzAlbumID:
|
||||
std::cout << "MusicBrainzAlbumID: " << boost::any_cast<std::string>(item.second) << std::endl;
|
||||
break;
|
||||
|
||||
case MetaData::Type::MusicBrainzTrackID:
|
||||
std::cout << "MusicBrainzTrackID: " << boost::any_cast<std::string>(item.second) << std::endl;
|
||||
break;
|
||||
|
||||
case MetaData::Type::MusicBrainzRecordingID:
|
||||
std::cout << "MusicBrainzRecordingID: " << boost::any_cast<std::string>(item.second) << std::endl;
|
||||
break;
|
||||
|
||||
case MetaData::Type::AcoustID:
|
||||
std::cout << "AcoustID: " << boost::any_cast<std::string>(item.second) << std::endl;
|
||||
break;
|
||||
|
||||
case MetaData::Type::Copyright:
|
||||
std::cout << "Copyright: " << boost::any_cast<std::string>(item.second) << std::endl;
|
||||
break;
|
||||
|
||||
case MetaData::Type::CopyrightURL:
|
||||
std::cout << "CopyrightURL: " << boost::any_cast<std::string>(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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user