diff --git a/README.md b/README.md
index 6c12f894..a9bd385c 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@ A [demo instance](http://lms-demo.poupon.dev) is available. Note the administrat
## Main features
* Recommendation engine
* Audio transcoding for maximum interoperability and reduced bandwith requirements
-* Multi-value tags: `genre`, `albumgenre`, `mood`, `albummood`, `albumgrouping`, ...
+* Multi-valued tags: `genre`, `mood`, `artists`, ...
* Artist relationships: `composer`, `conductor`, `lyricist`, `mixer`, `performer`, `producer`, `remixer`
* [Release types](https://musicbrainz.org/doc/Release_Group/Type): `album`, `single`, `EP`, `compilation`, `live`, ...
* [MusicBrainz Identifier](https://musicbrainz.org/doc/MusicBrainz_Identifier) support to handle duplicated artist and release names
@@ -45,7 +45,7 @@ It is possible to apply global filters on your collection using `genre`, `mood`,
__Note__: you can use the `lms-metadata` tool to have an idea of the tags parsed by _LMS_ using [TagLib](https://github.com/taglib/taglib).
### Multiple album artists
-_LMS_ requires the `albumartists` and `albumartistssort` tags to properly handle multiple album artists on the same album. As they are a custom tags, you may need to setup your favorite tagger to add them.
+_LMS_ requires the `albumartists` and `albumartistssort` tags to properly handle multiple album artists on the same album. As they are custom tags, you may need to set up your favorite tagger to add them.
__Note__: if you use [Picard](https://picard.musicbrainz.org/), add the following script to include these tags:
```
diff --git a/approot/admin-scansettings.xml b/approot/admin-scansettings.xml
index 15f84aa7..0e760a7a 100644
--- a/approot/admin-scansettings.xml
+++ b/approot/admin-scansettings.xml
@@ -42,6 +42,24 @@
${extra-tags-to-scan-info}
+
+
+ ${tr:Lms.Admin.Database.artist-tag-delimiter}
+
+ ${artist-tag-delimiter class="form-control"}
+
+ ${artist-tag-delimiter-info}
+
+
+
+
+ ${tr:Lms.Admin.Database.default-tag-delimiter}
+
+ ${default-tag-delimiter class="form-control"}
+
+ ${default-tag-delimiter-info}
+
+
${save-btn class="btn btn-primary me-1"}${discard-btn class="btn btn-secondary"}
diff --git a/approot/messages.xml b/approot/messages.xml
index 485ab554..d774dec0 100644
--- a/approot/messages.xml
+++ b/approot/messages.xml
@@ -64,7 +64,9 @@
Root directory
+Delimiter to be used for splitting artist tags (only if the file does not contain any multi-valued tag)
Daily
+Delimiter to be used for splitting other tags (only if the file does not contain any multi-valued tag)
Hourly
Scan now!
@@ -77,6 +79,7 @@
Similarity engine
Tag-based
None
+The tag delimiter must not consist solely of spaces
Update period
Update start time
Weekly
diff --git a/approot/messages_fr.xml b/approot/messages_fr.xml
index 0333e61d..c4af2527 100644
--- a/approot/messages_fr.xml
+++ b/approot/messages_fr.xml
@@ -64,7 +64,9 @@
Répertoire racine
+Délimiteur à utiliser pour séparer les tags d'artistes (uniquement si le fichier ne contient pas de tag à valeurs multiples)
Tous les jours
+Délimiteur à utiliser pour séparer les autres tags (uniquement si le fichier ne contient pas de tag à valeurs multiples)
Toutes les heures
Scanner maintenant !
@@ -77,6 +79,7 @@
Moteur de similarité
Basé sur les tags
Aucun
+Le délimiteur de tag ne doit pas comporter uniquement des espaces
Périodicité des mises à jour
Heure de départ de la mise à jour
Toutes les semaines
diff --git a/approot/messages_it.xml b/approot/messages_it.xml
index 0db644b8..f7136b27 100644
--- a/approot/messages_it.xml
+++ b/approot/messages_it.xml
@@ -64,7 +64,9 @@
Cartella principale
+Delimitatore da utilizzare per separare i tag degli artisti (solo se il file non contiene alcun tag con valori multipli)
Giornaliera
+Delimitatore da utilizzare per separare gli altri tag (solo se il file non contiene alcun tag con valori multipli)
Ogni ora
Scansiona ora!
@@ -77,6 +79,7 @@
Motore di similarità
Basato su tag
Nessuno
+Il delimitatore del tag non deve consistere esclusivamente di spazi
Frequenza di aggiornamento
Orario di aggiornamento
Settimanale
diff --git a/approot/messages_zh.xml b/approot/messages_zh.xml
index 466c745e..2a06a2a1 100644
--- a/approot/messages_zh.xml
+++ b/approot/messages_zh.xml
@@ -64,8 +64,10 @@
+
每日
+
每小时
立即扫描!
每月
diff --git a/approot/release.xml b/approot/release.xml
index c2f9dcfc..4428ed2a 100644
--- a/approot/release.xml
+++ b/approot/release.xml
@@ -50,7 +50,21 @@
- ${disc-title}
+
+
${disc-title}
+
+ ${play-btn class="d-none d-sm-block btn btn-sm btn-outline-secondary border-0"}
+
+ ${more-btn data-bs-toggle="dropdown" aria-expanded="false" class="btn btn-sm btn-outline-secondary border-0"}
+
+
+
+
${tracks class="d-grid gap-1 Lms-row-container"}
diff --git a/conf/lms.conf b/conf/lms.conf
index ef239776..e65b63bd 100644
--- a/conf/lms.conf
+++ b/conf/lms.conf
@@ -33,7 +33,7 @@ approot = "/usr/share/lms/approot";
# Location for deployment (See README if you want to deploy on a non root path)
deploy-path = "/";
-# Number of threads to be used to dispatch http requests (0 means auto detect)
+# Number of threads to be used to dispatch http requests (0 means number of logical CPUs)
http-server-thread-count = 0;
# ListenBrainz root API
@@ -98,3 +98,6 @@ scanner-skip-duplicate-mbid = false;
# Scanner read style for metadata, maybe be 'fast', 'average' or 'accurate'
scanner-parser-read-style = "average";
+
+# Number of threads to use for scanning file metadata (0 means number of logical CPUs / 2)
+scanner-metadata-thread-count = 0;
\ No newline at end of file
diff --git a/docroot/js/mediaplayer.js b/docroot/js/mediaplayer.js
index b5dfdeb1..f6f61110 100644
--- a/docroot/js/mediaplayer.js
+++ b/docroot/js/mediaplayer.js
@@ -66,6 +66,9 @@ LMS.mediaplayer = function () {
navigator.mediaSession.setActionHandler("nexttrack", function() {
_playNext();
});
+ navigator.mediaSession.setActionHandler("seekto", function(e) {
+ _seekTo(e.seekTime);
+ });
}
if (_pendingTrackParameters != null) {
@@ -81,16 +84,10 @@ LMS.mediaplayer = function () {
if (_elems.audio.paused) {
_elems.playpause.firstElementChild.classList.remove(pauseClass);
_elems.playpause.firstElementChild.classList.add(playClass);
- if ("mediaSession" in navigator) {
- navigator.mediaSession.playbackState = "paused";
- }
}
else {
_elems.playpause.firstElementChild.classList.remove(playClass);
_elems.playpause.firstElementChild.classList.add(pauseClass);
- if ("mediaSession" in navigator) {
- navigator.mediaSession.playbackState = "playing";
- }
}
}
@@ -202,6 +199,46 @@ LMS.mediaplayer = function () {
_gainNode.gain.value = Math.pow(10, (_settings.replayGain.preAmpGain + replayGain) / 20);
}
+ let _seekTo = function(seekTime) {
+ _initAudioCtx();
+ let mode = _getAudioMode();
+ if (!mode)
+ return;
+
+ switch (mode) {
+ case Mode.Transcoding:
+ _offset = seekTime;
+ _removeAudioSources();
+ _addAudioSource(_audioTranscodingSrc + "&offset=" + _offset);
+ _elems.audio.load();
+ _elems.audio.currentTime = 0;
+ _playTrack();
+ break;
+
+ case Mode.File:
+ _elems.audio.currentTime = seekTime;
+ _playTrack();
+ break;
+ }
+
+ _updateMediaSessionState();
+ }
+
+ let _updateMediaSessionState = function() {
+ if ("mediaSession" in navigator) {
+ navigator.mediaSession.setPositionState({
+ duration: _duration,
+ playbackRate: 1,
+ position: _offset + _elems.audio.currentTime,
+ });
+
+ if (_elems.audio.paused)
+ navigator.mediaSession.playbackState = "paused";
+ else
+ navigator.mediaSession.playbackState = "playing";
+ }
+ }
+
let init = function(root, defaultSettings) {
_root = root;
@@ -228,34 +265,17 @@ LMS.mediaplayer = function () {
_playNext();
});
_elems.seek.addEventListener("change", function() {
- _initAudioCtx();
- let mode = _getAudioMode();
- if (!mode)
- return;
-
- let selectedOffset = parseInt(_elems.seek.value, 10);
-
- switch (mode) {
- case Mode.Transcoding:
- _offset = selectedOffset;
- _removeAudioSources();
- _addAudioSource(_audioTranscodingSrc + "&offset=" + _offset);
- _elems.audio.load();
- _elems.audio.currentTime = 0;
- _playTrack();
- break;
-
- case Mode.File:
- _elems.audio.currentTime = selectedOffset;
- _playTrack();
- break;
- }
+ _seekTo(parseInt(_elems.seek.value, 10));
});
_elems.audio.addEventListener("play", _updateControls);
_elems.audio.addEventListener("playing", _updateControls);
_elems.audio.addEventListener("pause", _updateControls);
+ _elems.audio.addEventListener("play", _updateMediaSessionState);
+ _elems.audio.addEventListener("playing", _updateMediaSessionState);
+ _elems.audio.addEventListener("pause", _updateMediaSessionState);
+
_elems.audio.addEventListener("pause", _stopTimer);
_elems.audio.addEventListener("playing", _startTimer);
_elems.audio.addEventListener("waiting", _stopTimer);
diff --git a/src/libs/av/include/av/IAudioFile.hpp b/src/libs/av/include/av/IAudioFile.hpp
index 1ce66e57..c4368b78 100644
--- a/src/libs/av/include/av/IAudioFile.hpp
+++ b/src/libs/av/include/av/IAudioFile.hpp
@@ -84,6 +84,7 @@ namespace Av
public:
virtual ~IAudioFile() = default;
+ // Keys are forced to be in upper case
using MetadataMap = std::unordered_map;
virtual const std::filesystem::path& getPath() const = 0;
diff --git a/src/libs/database/impl/Migration.cpp b/src/libs/database/impl/Migration.cpp
index b3adbb49..746d222b 100644
--- a/src/libs/database/impl/Migration.cpp
+++ b/src/libs/database/impl/Migration.cpp
@@ -30,6 +30,15 @@
namespace Database
{
+ namespace
+ {
+ static constexpr Version LMS_DATABASE_VERSION{ 54 };
+ }
+
+ VersionInfo::VersionInfo()
+ : _version{ LMS_DATABASE_VERSION }
+ {}
+
VersionInfo::pointer VersionInfo::getOrCreate(Session& session)
{
session.checkWriteTransaction();
@@ -72,11 +81,6 @@ namespace Database::Migration
Db& _db;
};
- static void migrateFromV32(Session& session)
- {
- ScanSettings::get(session).modify()->addAudioFileExtension(".wv");
- }
-
static void migrateFromV33(Session& session)
{
// remove name from track_artist_link
@@ -220,7 +224,7 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
session.getDboSession().execute("ALTER TABLE starred_release RENAME COLUMN scrobbling_state TO sync_state");
session.getDboSession().execute("ALTER TABLE starred_track RENAME COLUMN scrobbler TO backend");
session.getDboSession().execute("ALTER TABLE starred_track RENAME COLUMN scrobbling_state TO sync_state");
-
+
session.getDboSession().execute("UPDATE user SET feedback_backend = scrobbling_backend");
}
@@ -364,7 +368,7 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
constraint "fk_track_media_library" foreign key ("media_library_id") references "media_library" ("id") on delete set null deferrable initially deferred
))");
-// Migrate data, with the new media_library_id field set to 1
+ // Migrate data, with the new media_library_id field set to 1
session.getDboSession().execute(R"(INSERT INTO track_backup
SELECT
id,
@@ -399,6 +403,31 @@ SELECT
session.getDboSession().execute("ALTER TABLE track_backup RENAME TO track");
}
+ void migrateFromV51(Session& session)
+ {
+ // Add custom artist tag delimiters, no need to rescan since it has no effect when empty
+ session.getDboSession().execute("ALTER TABLE scan_settings ADD artist_tag_delimiters TEXT NOT NULL DEFAULT ''");
+ session.getDboSession().execute("ALTER TABLE scan_settings ADD default_tag_delimiters TEXT NOT NULL DEFAULT ''");
+ }
+
+ void migrateFromV52(Session& session)
+ {
+ // Add sort name for releases
+ session.getDboSession().execute("ALTER TABLE release ADD sort_name TEXT NOT NULL DEFAULT ''");
+
+ // Just increment the scan version of the settings to make the next scheduled scan rescan everything
+ session.getDboSession().execute("UPDATE scan_settings SET scan_version = scan_version + 1");
+ }
+
+ void migrateFromV53(Session& session)
+ {
+ // Add release group mbid
+ session.getDboSession().execute("ALTER TABLE release ADD group_mbid TEXT NOT NULL DEFAULT ''");
+
+ // Just increment the scan version of the settings to make the next scheduled scan rescan everything
+ session.getDboSession().execute("UPDATE scan_settings SET scan_version = scan_version + 1");
+ }
+
void doDbMigration(Session& session)
{
static const std::string outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
@@ -409,7 +438,6 @@ SELECT
const std::map migrationFunctions
{
- {32, migrateFromV32},
{33, migrateFromV33},
{34, migrateFromV34},
{35, migrateFromV35},
@@ -428,6 +456,9 @@ SELECT
{48, migrateFromV48},
{49, migrateFromV49},
{50, migrateFromV50},
+ {51, migrateFromV51},
+ {52, migrateFromV52},
+ {53, migrateFromV53},
};
{
diff --git a/src/libs/database/impl/Migration.hpp b/src/libs/database/impl/Migration.hpp
index 61452001..559c7f29 100644
--- a/src/libs/database/impl/Migration.hpp
+++ b/src/libs/database/impl/Migration.hpp
@@ -26,12 +26,14 @@ namespace Database
class Session;
using Version = std::size_t;
- static constexpr Version LMS_DATABASE_VERSION{ 51 };
+
class VersionInfo
{
public:
using pointer = Wt::Dbo::ptr;
+ VersionInfo();
+
static VersionInfo::pointer getOrCreate(Session& session);
static VersionInfo::pointer get(Session& session);
@@ -45,7 +47,7 @@ namespace Database
}
private:
- int _version{ LMS_DATABASE_VERSION };
+ int _version;
};
namespace Migration
diff --git a/src/libs/database/impl/ScanSettings.cpp b/src/libs/database/impl/ScanSettings.cpp
index d75c0995..5c8d35b6 100644
--- a/src/libs/database/impl/ScanSettings.cpp
+++ b/src/libs/database/impl/ScanSettings.cpp
@@ -46,7 +46,7 @@ namespace Database
std::vector ScanSettings::getAudioFileExtensions() const
{
- const auto extensions{ StringUtils::splitString(_audioFileExtensions, " ") };
+ const auto extensions{ StringUtils::splitString(_audioFileExtensions, ' ') };
std::vector res(std::cbegin(extensions), std::cend(extensions));
std::sort(std::begin(res), std::end(res));
@@ -55,14 +55,19 @@ namespace Database
return res;
}
- void ScanSettings::addAudioFileExtension(const std::filesystem::path& ext)
- {
- _audioFileExtensions += " " + ext.string();
- }
-
std::vector ScanSettings::getExtraTagsToScan() const
{
- return StringUtils::splitString(_extraTagsToScan, ";");
+ return StringUtils::splitString(_extraTagsToScan, ';');
+ }
+
+ std::vector ScanSettings::getArtistTagDelimiters() const
+ {
+ return StringUtils::splitEscapedStrings(_artistTagDelimiters, ';', '\\');
+ }
+
+ std::vector ScanSettings::getDefaultTagDelimiters() const
+ {
+ return StringUtils::splitEscapedStrings(_defaultTagDelimiters, ';', '\\');
}
void ScanSettings::setExtraTagsToScan(const std::vector& extraTags)
@@ -74,6 +79,26 @@ namespace Database
_extraTagsToScan = std::move(newTagsToScan);
}
+ void ScanSettings::setArtistTagDelimiters(std::span delimiters)
+ {
+ std::string tagDelimiters{ StringUtils::escapeAndJoinStrings(delimiters, ';', '\\') };
+ if (tagDelimiters != _artistTagDelimiters)
+ {
+ _artistTagDelimiters.swap(tagDelimiters);
+ incScanVersion();
+ }
+ }
+
+ void ScanSettings::setDefaultTagDelimiters(std::span delimiters)
+ {
+ std::string tagDelimiters{ StringUtils::escapeAndJoinStrings(delimiters, ';', '\\') };
+ if (tagDelimiters != _defaultTagDelimiters)
+ {
+ _defaultTagDelimiters.swap(tagDelimiters);
+ incScanVersion();
+ }
+ }
+
void ScanSettings::incScanVersion()
{
_scanVersion += 1;
diff --git a/src/libs/database/impl/Track.cpp b/src/libs/database/impl/Track.cpp
index 4a53b593..53fabd4e 100644
--- a/src/libs/database/impl/Track.cpp
+++ b/src/libs/database/impl/Track.cpp
@@ -142,6 +142,9 @@ namespace Database
if (params.trackNumber)
query.where("t.track_number = ?").bind(*params.trackNumber);
+ if (params.discNumber)
+ query.where("t.disc_number = ?").bind(*params.discNumber);
+
if (params.mediaLibrary.isValid())
query.where("t.media_library_id = ?").bind(params.mediaLibrary);
diff --git a/src/libs/database/include/database/Release.hpp b/src/libs/database/include/database/Release.hpp
index 40c9d80a..52f5100d 100644
--- a/src/libs/database/include/database/Release.hpp
+++ b/src/libs/database/include/database/Release.hpp
@@ -143,8 +143,10 @@ namespace Database
std::size_t getMeanBitrate() const;
// Accessors
- const std::string& getName() const { return _name; }
+ std::string_view getName() const { return _name; }
+ std::string_view getSortName() const { return _sortName; }
std::optional getMBID() const { return UUID::fromString(_MBID); }
+ std::optional getGroupMBID() const { return UUID::fromString(_groupMBID); }
std::optional getTotalDisc() const { return _totalDisc; }
std::size_t getDiscCount() const; // may not be total disc (if incomplete for example)
std::vector getDiscs() const;
@@ -157,7 +159,9 @@ namespace Database
// Setters
void setName(std::string_view name) { _name = name; }
+ void setSortName(std::string_view sortName) { _sortName = sortName; }
void setMBID(const std::optional& mbid) { _MBID = mbid ? mbid->getAsString() : ""; }
+ void setGroupMBID(const std::optional& mbid) { _groupMBID = mbid ? mbid->getAsString() : ""; }
void setTotalDisc(std::optional totalDisc) { _totalDisc = totalDisc; }
void setArtistDisplayName(std::string_view name) { _artistDisplayName = name; }
void clearReleaseTypes();
@@ -173,7 +177,9 @@ namespace Database
void persist(Action& a)
{
Wt::Dbo::field(a, _name, "name");
+ Wt::Dbo::field(a, _sortName, "sort_name");
Wt::Dbo::field(a, _MBID, "mbid");
+ Wt::Dbo::field(a, _groupMBID, "group_mbid");
Wt::Dbo::field(a, _totalDisc, "total_disc");
Wt::Dbo::field(a, _artistDisplayName, "artist_display_name");
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToOne, "release");
@@ -191,7 +197,9 @@ namespace Database
static constexpr std::size_t _maxNameLength{ 256 };
std::string _name;
+ std::string _sortName;
std::string _MBID;
+ std::string _groupMBID;
std::optional _totalDisc{};
std::string _artistDisplayName;
diff --git a/src/libs/database/include/database/ScanSettings.hpp b/src/libs/database/include/database/ScanSettings.hpp
index 0026e489..8337a224 100644
--- a/src/libs/database/include/database/ScanSettings.hpp
+++ b/src/libs/database/include/database/ScanSettings.hpp
@@ -20,6 +20,7 @@
#pragma once
#include
+#include
#include
#include
#include
@@ -62,19 +63,22 @@ namespace Database
static pointer get(Session& session);
// Getters
- std::size_t getScanVersion() const { return _scanVersion; }
- Wt::WTime getUpdateStartTime() const { return _startTime; }
- UpdatePeriod getUpdatePeriod() const { return _updatePeriod; }
- std::vector getExtraTagsToScan() const;
- std::vector getAudioFileExtensions() const;
- SimilarityEngineType getSimilarityEngineType() const { return _similarityEngineType; }
+ std::size_t getScanVersion() const { return _scanVersion; }
+ Wt::WTime getUpdateStartTime() const { return _startTime; }
+ UpdatePeriod getUpdatePeriod() const { return _updatePeriod; }
+ std::vector getExtraTagsToScan() const;
+ std::vector getAudioFileExtensions() const;
+ SimilarityEngineType getSimilarityEngineType() const { return _similarityEngineType; }
+ std::vector getArtistTagDelimiters() const;
+ std::vector getDefaultTagDelimiters() const;
// Setters
- void addAudioFileExtension(const std::filesystem::path& ext);
void setUpdateStartTime(Wt::WTime t) { _startTime = t; }
void setUpdatePeriod(UpdatePeriod p) { _updatePeriod = p; }
void setExtraTagsToScan(const std::vector& extraTags);
void setSimilarityEngineType(SimilarityEngineType type) { _similarityEngineType = type; }
+ void setArtistTagDelimiters(std::span delimiters);
+ void setDefaultTagDelimiters(std::span delimiters);
void incScanVersion();
template
@@ -86,6 +90,8 @@ namespace Database
Wt::Dbo::field(a, _audioFileExtensions, "audio_file_extensions");
Wt::Dbo::field(a, _similarityEngineType, "similarity_engine_type");
Wt::Dbo::field(a, _extraTagsToScan, "extra_tags_to_scan");
+ Wt::Dbo::field(a, _artistTagDelimiters, "artist_tag_delimiters");
+ Wt::Dbo::field(a, _defaultTagDelimiters, "default_tag_delimiters");
}
private:
@@ -95,6 +101,7 @@ namespace Database
SimilarityEngineType _similarityEngineType{ SimilarityEngineType::Clusters };
std::string _audioFileExtensions{ ".alac .mp3 .ogg .oga .aac .m4a .m4b .flac .wav .wma .aif .aiff .ape .mpc .shn .opus .wv" };
std::string _extraTagsToScan;
+ std::string _artistTagDelimiters;
+ std::string _defaultTagDelimiters;
};
} // namespace Database
-
diff --git a/src/libs/database/include/database/Track.hpp b/src/libs/database/include/database/Track.hpp
index 4a815323..a1cd4fd8 100644
--- a/src/libs/database/include/database/Track.hpp
+++ b/src/libs/database/include/database/Track.hpp
@@ -78,6 +78,7 @@ namespace Database {
std::string releaseName; // matching this release name
TrackListId trackList; // matching this trackList
std::optional trackNumber; // matching this track number
+ std::optional discNumber; // matching this disc number
MediaLibraryId mediaLibrary; // If set, tracks in this library
FindParameters& setClusters(const std::vector& _clusters) { clusters = _clusters; return *this; }
@@ -94,6 +95,7 @@ namespace Database {
FindParameters& setReleaseName(std::string_view _releaseName) { releaseName = _releaseName; return *this; }
FindParameters& setTrackList(TrackListId _trackList) { trackList = _trackList; return *this; }
FindParameters& setTrackNumber(int _trackNumber) { trackNumber = _trackNumber; return *this; }
+ FindParameters& setDiscNumber(int _discNumber) { discNumber = _discNumber; return *this; }
FindParameters& setMediaLibrary(MediaLibraryId _mediaLibrary) { mediaLibrary = _mediaLibrary; return *this; }
};
diff --git a/src/libs/metadata/CMakeLists.txt b/src/libs/metadata/CMakeLists.txt
index b2502aa3..8a11a7cf 100644
--- a/src/libs/metadata/CMakeLists.txt
+++ b/src/libs/metadata/CMakeLists.txt
@@ -4,9 +4,9 @@ if(BUILD_TESTING)
endif()
add_library(lmsmetadata SHARED
- impl/AvFormatParser.cpp
- impl/Factory.cpp
- impl/TagLibParser.cpp
+ impl/AvFormatTagReader.cpp
+ impl/Parser.cpp
+ impl/TagLibTagReader.cpp
impl/Utils.cpp
)
diff --git a/src/libs/metadata/impl/AvFormatParser.cpp b/src/libs/metadata/impl/AvFormatParser.cpp
deleted file mode 100644
index 6784843d..00000000
--- a/src/libs/metadata/impl/AvFormatParser.cpp
+++ /dev/null
@@ -1,250 +0,0 @@
-/*
- * Copyright (C) 2013 Emeric Poupon
- *
- * This file is part of LMS.
- *
- * LMS is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LMS is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with LMS. If not, see .
- */
-
-#include "AvFormatParser.hpp"
-
-#include
-#include
-
-#include "av/IAudioFile.hpp"
-#include "utils/ILogger.hpp"
-#include "utils/String.hpp"
-#include "Utils.hpp"
-
-namespace MetaData
-{
- namespace
- {
- template
- std::optional findFirstValueOfAs(const Av::IAudioFile::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 std::nullopt;
-
- return StringUtils::readAs(StringUtils::stringTrim(it->second));
- }
-
- template <>
- std::optional> findFirstValueOfAs(const Av::IAudioFile::MetadataMap& metadataMap, std::initializer_list tags)
- {
- std::optional str{ findFirstValueOfAs(metadataMap, tags) };
- if (!str)
- return std::nullopt;
-
- const std::vector strUuids{ StringUtils::splitString(*str, "/") };
- std::vector res;
-
- for (std::string_view strUuid : strUuids)
- {
- std::optional uuid{ UUID::fromString(strUuid) };
- if (!uuid)
- return std::nullopt;
-
- res.push_back(std::move(*uuid));
- }
-
- return res;
- }
-
- std::vector getReleaseArtists(const Av::IAudioFile::MetadataMap& metadataMap)
- {
- std::vector res;
-
- auto name{ findFirstValueOfAs(metadataMap, {"ALBUM_ARTIST"}) };
- if (!name)
- return res;
-
- auto mbid{ findFirstValueOfAs(metadataMap, {"MUSICBRAINZ ALBUM ARTIST ID", "MUSICBRAINZ/ALBUM ARTIST ID"}) };
-
- return { Artist {mbid, *name, std::nullopt} };
- }
-
- std::vector getArtists(const Av::IAudioFile::MetadataMap& metadataMap)
- {
- std::vector artists;
-
- std::vector artistNames;
- if (metadataMap.find("ARTISTS") != metadataMap.end())
- {
- artistNames = StringUtils::splitString(metadataMap.find("ARTISTS")->second, "/;");
- }
- else if (metadataMap.find("ARTIST") != metadataMap.end())
- {
- artistNames = { metadataMap.find("ARTIST")->second };
- }
-
- auto artistMBIDs{ findFirstValueOfAs>(metadataMap, {"MUSICBRAINZ ARTIST ID", "MUSICBRAINZ_ARTISTID", "MUSICBRAINZ/ARTIST ID"}) };
-
- for (std::size_t i{}; i < artistNames.size(); ++i)
- {
- if (artistMBIDs && artistNames.size() == artistMBIDs->size())
- artists.emplace_back(Artist{ (*artistMBIDs)[i], artistNames[i], std::nullopt });
- else
- artists.emplace_back(Artist{ std::nullopt, artistNames[i], std::nullopt });
- }
-
- return artists;
- }
-
- std::optional getRelease(const Av::IAudioFile::MetadataMap& metadataMap)
- {
- std::optional res;
-
- std::optional releaseName{ findFirstValueOfAs(metadataMap, {"ALBUM", "TALB", "WM/ALBUMTITLE"}) };
- if (!releaseName)
- return res;
-
- res.emplace();
- res->name = std::move(*releaseName);
- res->mbid = findFirstValueOfAs(metadataMap, { "MUSICBRAINZ ALBUM ID", "MUSICBRAINZ_ALBUMID", "MUSICBRAINZ/ALBUM ID" });
- res->artists = getReleaseArtists(metadataMap);
- res->mediumCount = findFirstValueOfAs(metadataMap, { "TOTALDISCS", "DISCTOTAL" });
- if (!res->mediumCount)
- {
- // mediumCount may be encoded as position/count
- if (const auto value{ findFirstValueOfAs(metadataMap, {"TPOS", "DISC", "DISK", "DISCNUMBER", "WM/PARTOFSET"}) })
- {
- // Expecting 'Number/Total'
- const std::vector strings{ StringUtils::splitString(*value, "/") };
- if (strings.size() == 2)
- res->mediumCount = StringUtils::readAs(strings[1]);
- }
- }
-
- return res;
- }
-
- std::optional getMedium(const Av::IAudioFile::MetadataMap& metadataMap)
- {
- std::optional res;
- res.emplace();
-
- res->type = findFirstValueOfAs(metadataMap, { "TMED", "MEDIA", "WM/MEDIA" }).value_or("");
- res->name = findFirstValueOfAs(metadataMap, { "TSST", "DISCSUBTITLE", "SETSUBTITLE" }).value_or("");
- res->trackCount = findFirstValueOfAs(metadataMap, { "TOTALTRACKS", "TRACKTOTAL" });
- if (!res->trackCount)
- {
- // totalTracks may be encoded as "position/count"
- if (const auto value{ findFirstValueOfAs(metadataMap, {"TRCK", "TRACK", "TRACKNUMBER", "TRKN", "WM/TRACKNUMBER"}) })
- {
- // Expecting 'Number/Total'
- const std::vector strings{ StringUtils::splitString(*value, "/") };
- if (strings.size() == 2)
- res->trackCount = StringUtils::readAs(strings[1]);
- }
- }
-
- // position may be encoded in TPOS/DISC/DISK as "position/count". Expecting 'Number[/Total]'
- res->position = findFirstValueOfAs(metadataMap, { "TPOS", "DISC", "DISK", "DISCNUMBER", "WM/PARTOFSET" });
- res->release = getRelease(metadataMap);
-
- if (res->type.empty()
- && res->name.empty()
- && !res->trackCount
- && !res->position
- && !res->release
- && !res->replayGain)
- {
- res.reset();
- }
-
- return res;
- }
- }
-
- std::optional AvFormatParser::parse(const std::filesystem::path& p, bool debug)
- {
- Track track;
-
- try
- {
- const auto mediaFile{ Av::parseAudioFile(p) };
-
- Av::ContainerInfo info{ mediaFile->getContainerInfo() };
- track.duration = info.duration;
- track.bitrate = info.bitrate;
- track.hasCover = mediaFile->hasAttachedPictures();
-
- MetaData::Tags tags;
-
- const Av::IAudioFile::MetadataMap metadataMap{ mediaFile->getMetaData() };
-
- track.artists = getArtists(metadataMap);
- track.medium = getMedium(metadataMap);
-
- for (const auto& [tag, value] : metadataMap)
- {
- if (debug)
- std::cout << "TAG = " << tag << ", VAL = " << value << std::endl;
-
- if (tag == "TITLE")
- track.title = value;
- else if (tag == "TRACK")
- {
- // Expecting 'Number/Total'
- track.position = StringUtils::readAs(value);
- }
- else if (tag == "DATE"
- || tag == "YEAR"
- || tag == "WM/YEAR")
- {
- track.date = Utils::parseDate(value);
- }
- else if (tag == "TDOR" // Original release time (ID3v2 2.4)
- || tag == "TORY") // Original release year
- {
- track.originalDate = Utils::parseDate(value);
- }
- else if (tag == "ACOUSTID ID")
- {
- track.acoustID = UUID::fromString(value);
- }
- else if (tag == "MUSICBRAINZ RELEASE TRACK ID"
- || tag == "MUSICBRAINZ_RELEASETRACKID")
- {
- track.mbid = UUID::fromString(value);
- }
- else if (tag == "MUSICBRAINZ_TRACKID"
- || tag == "MUSICBRAINZ/TRACK ID")
- {
- track.recordingMBID = UUID::fromString(value);
- }
- else if (std::find(std::cbegin(_userExtraTags), std::cend(_userExtraTags), tag) != std::cend(_userExtraTags))
- {
- const std::vector tagValues{ StringUtils::splitString(value, "/,;") };
-
- if (!tagValues.empty())
- {
- std::vector values;
- std::transform(std::cbegin(tagValues), std::cend(tagValues), std::inserter(values, std::begin(values)), [](std::string_view v) { return std::string{ v }; });
- track.userExtraTags[tag] = std::move(values);
- }
- }
- }
- }
- catch (Av::Exception& e)
- {
- return std::nullopt;
- }
-
- return track;
- }
-
-} // namespace MetaData
diff --git a/src/libs/metadata/impl/AvFormatTagReader.cpp b/src/libs/metadata/impl/AvFormatTagReader.cpp
new file mode 100644
index 00000000..0a81fec3
--- /dev/null
+++ b/src/libs/metadata/impl/AvFormatTagReader.cpp
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2013 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#include "AvFormatTagReader.hpp"
+
+#include
+#include
+
+#include "av/IAudioFile.hpp"
+#include "metadata/Exception.hpp"
+#include "utils/ILogger.hpp"
+#include "utils/String.hpp"
+#include "Utils.hpp"
+
+namespace MetaData
+{
+ namespace
+ {
+
+ // Mapping to internal avformat names and/or common alternative custom names
+ static const std::unordered_map> tagMapping
+ {
+ { TagType::AcoustID, { "ACOUSTID_ID", "ACOUSTID ID" } },
+ { TagType::Album, { "ALBUM", "TALB", "WM/ALBUMTITLE" } },
+ { TagType::AlbumArtist, { "ALBUMARTIST", "ALBUM_ARTIST" } },
+ { TagType::AlbumArtistSortOrder, { "ALBUMARTISTSORT", "TSO2" } },
+ { TagType::AlbumArtists, { "ALBUMARTISTS" } },
+ { TagType::AlbumArtistsSortOrder, { "ALBUMARTISTSSORT" } },
+ { TagType::AlbumSortOrder, { "ALBUMSORT", "ALBUM-SORT" } },
+ { TagType::Arranger, { "ARRANGER" } },
+ { TagType::Artist, { "ARTIST" } },
+ { TagType::ArtistSortOrder, { "ARTISTSORT", "ARTIST-SORT", "WM/ARTISTSORTORDER" } },
+ { TagType::Artists, { "ARTISTS", "WM/ARTISTS" } },
+ { TagType::ASIN, { "ASIN" } },
+ { TagType::Barcode, { "BARCODE", "WM/BARCODE" } },
+ { TagType::BPM, { "BPM" } },
+ { TagType::CatalogNumber, { "CATALOGNUMBER", "WM/CATALOGNO" } },
+ { TagType::Comment, { "COMMENT" } },
+ { TagType::Compilation, { "COMPILATION", "TCMP" } },
+ { TagType::Composer, { "COMPOSER" } },
+ { TagType::Composers, { "COMPOSERS" } },
+ { TagType::ComposerSortOrder, { "COMPOSERSORT", "TSOC" } },
+ { TagType::ComposersSortOrder, { "COMPOSERSSORT" } },
+ { TagType::Conductor, { "CONDUCTOR" } },
+ { TagType::ConductorSortOrder, { "CONDUCTORSORT" } },
+ { TagType::Conductors, { "CONDUCTORS" } },
+ { TagType::ConductorsSortOrder, { "CONDUCTORSSORT" } },
+ { TagType::Copyright, { "COPYRIGHT" } },
+ { TagType::CopyrightURL, { "COPYRIGHTURL" } },
+ { TagType::Date, { "DATE", "YEAR", "WM/YEAR" } },
+ { TagType::Director, { "DIRECTOR" } },
+ { TagType::DiscNumber, { "TPOS", "DISC", "DISK", "DISCNUMBER", "WM/PARTOFSET" } },
+ { TagType::DiscSubtitle, { "TSST", "DISCSUBTITLE", "SETSUBTITLE" } },
+ { TagType::EncodedBy, { "ENCODEDBY" } },
+ { TagType::Engineer, { "ENGINEER" } },
+ { TagType::GaplessPlayback, { "GAPLESSPLAYBACK" } },
+ { TagType::Genre, { "GENRE" } },
+ { TagType::Grouping, { "GROUPING", "WM/CONTENTGROUPDESCRIPTION", "ALBUMGROUPING" } },
+ { TagType::InitialKey, { "INITIALKEY" } },
+ { TagType::ISRC, { "ISRC", "WM/ISRC", "TSRC" } },
+ { TagType::Language, { "LANGUAGE" } },
+ { TagType::License, { "LICENSE" } },
+ { TagType::Lyricist, { "LYRICIST" } },
+ { TagType::LyricistSortOrder, { "LYRICISTSORT" } },
+ { TagType::Lyricists, { "LYRICISTS" } },
+ { TagType::LyricistsSortOrder, { "LYRICISTSSORT" } },
+ { TagType::Lyrics, { "LYRICS" } },
+ { TagType::Media, { "TMED", "MEDIA", "WM/MEDIA" } },
+ { TagType::MixDJ, { "DJMIXER" } },
+ { TagType::Mixer, { "MIXER" } },
+ { TagType::MixerSortOrder, { "MIXERSORT" } },
+ { TagType::Mixers, { "MIXERS" } },
+ { TagType::MixersSortOrder, { "MIXERSSORT" } },
+ { TagType::Mood, { "MOOD" } },
+ { TagType::Movement, { "MOVEMENT", "MOVEMENTNAME" } },
+ { TagType::MovementCount, { "MOVEMENTCOUNT" } },
+ { TagType::MovementNumber, { "MOVEMENTNUMBER" } },
+ { TagType::MusicBrainzArtistID, { "MUSICBRAINZ_ARTISTID", "MUSICBRAINZ ARTIST ID", "MUSICBRAINZ/ARTIST ID" } },
+ { TagType::MusicBrainzDiscID, { "MUSICBRAINZ_DISCID", "MUSICBRAINZ DISC ID", "MUSICBRAINZ/DISC ID" } },
+ { TagType::MusicBrainzOriginalArtistID, { "MUSICBRAINZ_ORIGINALARTISTID", "MUSICBRAINZ ORIGINAL ARTIST ID", "MUSICBRAINZ/ORIGINAL ARTIST ID" } },
+ { TagType::MusicBrainzOriginalReleaseID, { "MUSICBRAINZ_ORIGINALRELEASEID", "MUSICBRAINZ ORIGINAL RELEASE ID", "MUSICBRAINZ/ORIGINAL RELEASE ID" } },
+ { TagType::MusicBrainzRecordingID, { "MUSICBRAINZ_TRACKID", "MUSICBRAINZ TRACK ID", "MUSICBRAINZ/TRACK ID" } },
+ { TagType::MusicBrainzReleaseArtistID, { "MUSICBRAINZ_ALBUMARTISTID", "MUSICBRAINZ ALBUM ARTIST ID", "MUSICBRAINZ/ALBUM ARTIST ID" } },
+ { TagType::MusicBrainzReleaseGroupID, { "MUSICBRAINZ_RELEASEGROUPID", "MUSICBRAINZ RELEASE GROUP ID", "MUSICBRAINZ/RELEASE GROUP ID" } },
+ { TagType::MusicBrainzReleaseID, { "MUSICBRAINZ_ALBUMID", "MUSICBRAINZ ALBUM ID", "MUSICBRAINZ/ALBUM ID" } },
+ { TagType::MusicBrainzTrackID, { "MUSICBRAINZ_RELEASETRACKID", "MUSICBRAINZ RELEASE TRACK ID", "MUSICBRAINZ/RELEASE TRACK ID" } },
+ { TagType::MusicBrainzWorkID, { "MUSICBRAINZ_WORKID", "MUSICBRAINZ WORK ID", "MUSICBRAINZ/WORK ID" } },
+ { TagType::OriginalArtist, { "ORIGINALARTIST" } },
+ { TagType::OriginalFilename, { "ORIGINALFILENAME" } },
+ { TagType::OriginalReleaseDate, { "ORIGINALDATE", "TDOR", "WM/ORIGINALRELEASETIME" } },
+ { TagType::OriginalReleaseYear, { "ORIGINALYEAR", "TORY", "WM/ORIGINALRELEASEYEAR" } },
+ { TagType::Podcast, { "PODCAST" } },
+ { TagType::PodcastURL, { "PODCASTURL" } },
+ { TagType::Producer, { "PRODUCER" } },
+ { TagType::ProducerSortOrder, { "PRODUCERSORTORDER" } },
+ { TagType::Producers, { "PRODUCERS" } },
+ { TagType::ProducersSortOrder, { "PRODUCERSSORTORDER" } },
+ { TagType::RecordLabel, { "LABEL", "PUBLISHER" } },
+ { TagType::ReleaseCountry, { "RELEASECOUNTRY" } },
+ { TagType::ReleaseDate, { "RELEASEDATE" } },
+ { TagType::ReleaseStatus, { "RELEASESTATUS" } },
+ { TagType::ReleaseType, { "RELEASETYPE", "MUSICBRAINZ_ALBUMTYPE", "MUSICBRAINZ ALBUM TYPE", "MUSICBRAINZ/ALBUM TYPE" } },
+ { TagType::Remixer, { "REMIXER", "MODIFIEDBY", "MIXARTIST" } },
+ { TagType::RemixerSortOrder, { "REMIXERSORTORDER", "MIXARTISTSORTORDER" } },
+ { TagType::Remixers, { "REMIXERS" } },
+ { TagType::RemixersSortOrder, { "REMIXERSSORTORDER", "MIXARTISTSSORTORDER" } },
+ { TagType::ReplayGainAlbumGain, { "REPLAYGAIN_ALBUM_GAIN" } },
+ { TagType::ReplayGainAlbumPeak, { "REPLAYGAIN_ALBUM_PEAK" } },
+ { TagType::ReplayGainAlbumRange, { "REPLAYGAIN_ALBUM_RANGE" } },
+ { TagType::ReplayGainReferenceLoudness, { "REPLAYGAIN_REFERENCE_LOUDNESS" } },
+ { TagType::ReplayGainTrackGain, { "REPLAYGAIN_TRACK_GAIN" } },
+ { TagType::ReplayGainTrackPeak, { "REPLAYGAIN_TRACK_PEAK" } },
+ { TagType::ReplayGainTrackRange, { "REPLAYGAIN_TRACK_RANGE" } },
+ { TagType::Script, { "SCRIPT", "WM/SCRIPT" } },
+ { TagType::ShowWorkAndMovement, { "SHOWWORKMOVEMENT", "SHOWMOVEMENT" } },
+ { TagType::Subtitle, { "SUBTITLE" } },
+ { TagType::TotalDiscs, { "DISCTOTAL", "TOTALDISCS"} },
+ { TagType::TotalTracks, { "TRACKTOTAL", "TOTALTRACKS" } },
+ { TagType::TrackNumber, { "TRCK", "TRACK", "TRACKNUMBER", "TRKN", "WM/TRACKNUMBER" } },
+ { TagType::TrackTitle, { "TITLE" } },
+ { TagType::TrackTitleSortOrder, { "TITLESORT" } },
+ { TagType::WorkTitle, { "WORK" } },
+ { TagType::Writer, { "WRITER" } },
+ };
+ }
+
+ AvFormatTagReader::AvFormatTagReader(const std::filesystem::path& p, bool debug)
+ {
+ try
+ {
+ const auto audioFile{ Av::parseAudioFile(p) };
+
+ _containerInfo = audioFile->getContainerInfo();
+ _metaDataMap = audioFile->getMetaData();
+ _hasEmbeddedCover = audioFile->hasAttachedPictures();
+
+ if (debug && Service::get()->isSeverityActive(Severity::DEBUG))
+ {
+ for (const auto& [key, value] : _metaDataMap)
+ LMS_LOG(METADATA, DEBUG, "Key = '" << key << "', value = '" << value << "'");
+ }
+ }
+ catch (Av::Exception& e)
+ {
+ throw ParseException{};
+ }
+ }
+
+ void AvFormatTagReader::visitTagValues(TagType tag, TagValueVisitor visitor) const
+ {
+ auto itTagNames{ tagMapping.find(tag) };
+ if (itTagNames == std::cend(tagMapping))
+ return;
+
+ for (const std::string& tagName : itTagNames->second)
+ {
+ bool visited{};
+
+ visitTagValues(tagName, [&](std::string_view value)
+ {
+ visited = true;
+ visitor(value);
+ });
+
+ if (visited)
+ break;
+ }
+ }
+
+ void AvFormatTagReader::visitTagValues(std::string_view key, TagValueVisitor visitor) const
+ {
+ auto itValues{ _metaDataMap.find(std::string{ key }) };
+ if (itValues == std::cend(_metaDataMap))
+ return;
+
+ visitor(itValues->second);
+ }
+
+ void AvFormatTagReader::visitPerformerTags(PerformerVisitor visitor) const
+ {
+ visitTagValues("PERFORMER", [&](std::string_view value)
+ {
+ visitor("", value);
+ });
+ }
+} // namespace MetaData
diff --git a/src/libs/metadata/impl/AvFormatTagReader.hpp b/src/libs/metadata/impl/AvFormatTagReader.hpp
new file mode 100644
index 00000000..d2039eb4
--- /dev/null
+++ b/src/libs/metadata/impl/AvFormatTagReader.hpp
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2018 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#pragma once
+
+#include
+
+#include "av/IAudioFile.hpp"
+#include "metadata/IParser.hpp"
+#include "ITagReader.hpp"
+
+namespace MetaData
+{
+ class AvFormatTagReader : public ITagReader
+ {
+ public:
+ AvFormatTagReader(const std::filesystem::path& path, bool debug);
+
+ private:
+ AvFormatTagReader(const AvFormatTagReader&) = delete;
+ AvFormatTagReader& operator=(const AvFormatTagReader&) = delete;
+
+ bool hasMultiValuedTags() const override { return false; /* not supported */}
+ void visitTagValues(TagType tag, TagValueVisitor visitor) const override;
+ void visitTagValues(std::string_view tag, TagValueVisitor visitor) const override;
+ void visitPerformerTags(PerformerVisitor visitor) const override;
+ bool hasEmbeddedCover() const override { return _hasEmbeddedCover; }
+
+ std::chrono::milliseconds getDuration() const override { return _containerInfo.duration; }
+ std::size_t getBitrate() const override { return _containerInfo.bitrate; }
+ std::size_t getBitsPerSample() const override { return 0; }
+ std::size_t getSampleRate() const override { return 0; }
+
+ Av::IAudioFile::MetadataMap _metaDataMap;
+ Av::ContainerInfo _containerInfo;
+ bool _hasEmbeddedCover{};
+ };
+} // namespace MetaData
+
diff --git a/src/libs/metadata/impl/Factory.cpp b/src/libs/metadata/impl/Factory.cpp
deleted file mode 100644
index d6d1d725..00000000
--- a/src/libs/metadata/impl/Factory.cpp
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * Copyright (C) 2022 Emeric Poupon
- *
- * This file is part of LMS.
- *
- * LMS is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LMS is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with LMS. If not, see .
- */
-
-#include "metadata/IParser.hpp"
-
-#include "utils/Exception.hpp"
-#include "utils/ILogger.hpp"
-
-#include "AvFormatParser.hpp"
-#include "TagLibParser.hpp"
-#include "Utils.hpp"
-
-namespace MetaData
-{
- std::unique_ptr
- createParser(ParserType parserType, ParserReadStyle parserReadStyle)
- {
- switch (parserType)
- {
- case ParserType::TagLib:
- LMS_LOG(METADATA, INFO, "Creating TagLib parser with read style = " << Utils::readStyleToString(parserReadStyle));
- return std::make_unique(parserReadStyle);
- case ParserType::AvFormat:
- LMS_LOG(METADATA, INFO, "Creating AvFormat parser");
- return std::make_unique();
- }
-
- throw LmsException {"Unhandled parser type"};
- }
-}
-
diff --git a/src/libs/metadata/impl/ITagReader.hpp b/src/libs/metadata/impl/ITagReader.hpp
new file mode 100644
index 00000000..ef0dbf14
--- /dev/null
+++ b/src/libs/metadata/impl/ITagReader.hpp
@@ -0,0 +1,164 @@
+/*
+ * Copyright (C) 2018 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#pragma once
+
+#include
+#include
+
+namespace MetaData
+{
+ // using picard internal names
+ // see https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html
+ enum class TagType
+ {
+ AcoustID,
+ AcoustIDFingerprint,
+ Album,
+ AlbumArtist,
+ AlbumArtists, // non standard
+ AlbumArtistSortOrder,
+ AlbumArtistsSortOrder, // non standard
+ AlbumSortOrder,
+ Arranger,
+ Artist,
+ ArtistSortOrder,
+ Artists,
+ ASIN,
+ Barcode,
+ BPM,
+ CatalogNumber,
+ Comment,
+ Compilation,
+ Composer,
+ ComposerSortOrder,
+ Composers, // non standard
+ ComposersSortOrder, // non standard
+ Conductor,
+ ConductorSortOrder, // non standard
+ Conductors, // non standard
+ ConductorsSortOrder, // non standard
+ Copyright,
+ CopyrightURL, // non standard
+ Date,
+ Director,
+ DiscNumber,
+ DiscSubtitle,
+ EncodedBy,
+ EncoderSettings,
+ Engineer,
+ GaplessPlayback,
+ Genre,
+ Grouping,
+ InitialKey,
+ ISRC,
+ Language,
+ License,
+ Lyricist,
+ LyricistSortOrder, // non standard
+ Lyricists, // non standard
+ LyricistsSortOrder, // non standard
+ Lyrics,
+ Media,
+ MixDJ,
+ Mixer,
+ MixerSortOrder, // non standard
+ Mixers, // non standard
+ MixersSortOrder, // non standard
+ Mood,
+ Movement,
+ MovementCount,
+ MovementNumber,
+ MusicBrainzArtistID,
+ MusicBrainzDiscID,
+ MusicBrainzOriginalArtistID,
+ MusicBrainzOriginalReleaseID,
+ MusicBrainzRecordingID,
+ MusicBrainzReleaseArtistID,
+ MusicBrainzReleaseGroupID,
+ MusicBrainzReleaseID,
+ MusicBrainzTrackID,
+ MusicBrainzWorkID,
+ MusicIPFingerprint,
+ MusicIPPUID,
+ OriginalAlbum,
+ OriginalArtist,
+ OriginalFilename,
+ OriginalReleaseDate,
+ OriginalReleaseYear,
+ Podcast,
+ PodcastURL,
+ Producer,
+ ProducerSortOrder, // non standard
+ Producers, // non standard
+ ProducersSortOrder, // non standard
+ Rating,
+ RecordLabel,
+ ReleaseCountry,
+ ReleaseDate,
+ ReleaseStatus,
+ ReleaseType,
+ Remixer,
+ RemixerSortOrder,
+ Remixers,
+ RemixersSortOrder,
+ ReplayGainAlbumGain,
+ ReplayGainAlbumPeak,
+ ReplayGainAlbumRange,
+ ReplayGainReferenceLoudness,
+ ReplayGainTrackGain,
+ ReplayGainTrackPeak,
+ ReplayGainTrackRange,
+ Script,
+ ShowName,
+ ShowNameSortOrder,
+ ShowWorkAndMovement,
+ Subtitle,
+ TotalDiscs,
+ TotalTracks,
+ TrackNumber,
+ TrackTitle,
+ TrackTitleSortOrder,
+ Website,
+ WorkTitle,
+ Writer,
+ };
+
+ class ITagReader
+ {
+ public:
+ virtual ~ITagReader() = default;
+
+ virtual bool hasMultiValuedTags() const = 0;
+
+ using TagValueVisitor = std::function;
+ virtual void visitTagValues(TagType tag, TagValueVisitor visitor) const = 0;
+ virtual void visitTagValues(std::string_view tag, TagValueVisitor visitor) const = 0;
+
+ using PerformerVisitor = std::function;
+ virtual void visitPerformerTags(PerformerVisitor visitor) const = 0;
+
+ virtual bool hasEmbeddedCover() const = 0;
+
+ virtual std::chrono::milliseconds getDuration() const = 0;
+ virtual std::size_t getBitrate() const = 0;
+ virtual std::size_t getBitsPerSample() const = 0;
+ virtual std::size_t getSampleRate() const = 0;
+ };
+} // namespace MetaData
diff --git a/src/libs/metadata/impl/Parser.cpp b/src/libs/metadata/impl/Parser.cpp
new file mode 100644
index 00000000..80a10e66
--- /dev/null
+++ b/src/libs/metadata/impl/Parser.cpp
@@ -0,0 +1,397 @@
+/*
+ * Copyright (C) 2016 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#include "Parser.hpp"
+
+#include
+
+#include "metadata/Exception.hpp"
+#include "utils/ILogger.hpp"
+#include "utils/String.hpp"
+
+#include "AvFormatTagReader.hpp"
+#include "TagLibTagReader.hpp"
+#include "Utils.hpp"
+
+namespace MetaData
+{
+ namespace
+ {
+ void visitTagValues(const ITagReader& tagReader, std::string_view tagType, std::span tagDelimiters, ITagReader::TagValueVisitor visitor)
+ {
+ tagReader.visitTagValues(tagType, [&](std::string_view value)
+ {
+ auto visitTagIfNonEmpty{ [&](std::string_view tag)
+ {
+ tag = StringUtils::stringTrim(tag);
+ if (!tag.empty())
+ visitor(tag);
+ } };
+
+ if (!tagReader.hasMultiValuedTags())
+ {
+ for (std::string_view tagDelimiter : tagDelimiters)
+ {
+ if (value.find(tagDelimiter) != std::string_view::npos)
+ {
+ for (std::string_view splitTag : StringUtils::splitString(value, tagDelimiter))
+ visitTagIfNonEmpty(splitTag);
+ return;
+ }
+ }
+ }
+
+ visitTagIfNonEmpty(value);
+ });
+ }
+
+ template
+ std::vector getTagValuesFirstMatchAs(const ITagReader& tagReader, std::initializer_list tagTypes, std::span tagDelimiters)
+ {
+ std::vector res;
+
+ for (const TagType tagType : tagTypes)
+ {
+ auto addTagIfNonEmpty{ [&res](std::string_view tag)
+ {
+ tag = StringUtils::stringTrim(tag);
+ if (!tag.empty())
+ {
+ std::optional val{ StringUtils::readAs(tag) };
+ if (val)
+ res.emplace_back(std::move(*val));
+ }
+ } };
+
+ tagReader.visitTagValues(tagType, [&](std::string_view value)
+ {
+ if (!tagReader.hasMultiValuedTags())
+ {
+ for (std::string_view tagDelimiter : tagDelimiters)
+ {
+ if (value.find(tagDelimiter) != std::string_view::npos)
+ {
+ for (std::string_view splitTag : StringUtils::splitString(value, tagDelimiter))
+ addTagIfNonEmpty(splitTag);
+
+ return;
+ }
+ }
+ }
+
+ // no delimiter found, or no delimiter to be used
+ addTagIfNonEmpty(value);
+ });
+
+ if (!res.empty())
+ break;
+ }
+
+ return res;
+ }
+
+ template
+ std::optional getTagValueFirstMatchAs(const ITagReader& tagReader, std::initializer_list tagTypes)
+ {
+ std::optional res;
+ std::vector values{ getTagValuesFirstMatchAs(tagReader, tagTypes, {} /* don't expect multiple values here */) };
+ if (!values.empty())
+ res = std::move(values.front());
+
+ return res;
+ }
+
+ template
+ std::vector getTagValuesAs(const ITagReader& tagReader, TagType tagType, std::span tagDelimiters)
+ {
+ return getTagValuesFirstMatchAs(tagReader, { tagType }, tagDelimiters);
+ }
+
+ template
+ std::optional getTagValueAs(const ITagReader& tagReader, TagType tagType)
+ {
+ return getTagValueFirstMatchAs(tagReader, { tagType });
+ }
+
+ std::vector getArtists(const ITagReader& tagReader,
+ std::initializer_list artistTagNames,
+ std::initializer_list artistSortTagNames,
+ std::initializer_list artistMBIDTagNames,
+ std::span artistTagDelimiters
+ )
+ {
+ std::vector artistNames{ getTagValuesFirstMatchAs(tagReader, artistTagNames, artistTagDelimiters) };
+ if (artistNames.empty())
+ return {};
+
+ std::vector artistSortNames{ getTagValuesFirstMatchAs(tagReader, artistSortTagNames, artistTagDelimiters) };
+ std::vector artistMBIDs{ getTagValuesFirstMatchAs(tagReader, artistMBIDTagNames, artistTagDelimiters) };
+
+ std::vector artists;
+ artists.reserve(artistNames.size());
+
+ for (std::size_t i{}; i < artistNames.size(); ++i)
+ {
+ Artist& artist{ artists.emplace_back(std::move(artistNames[i])) };
+
+ if (artistNames.size() == artistSortNames.size())
+ artist.sortName = std::move(artistSortNames[i]);
+ if (artistNames.size() == artistMBIDs.size())
+ artist.mbid = std::move(artistMBIDs[i]);
+ }
+
+ return artists;
+ }
+
+ PerformerContainer getPerformerArtists(const ITagReader& tagReader)
+ {
+ PerformerContainer performers;
+
+ tagReader.visitPerformerTags([&](std::string_view role, std::string_view name)
+ {
+ // picard stores like this: (see https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html#performer)
+ // We consider we may hit both styles for the same track
+ if (role.empty())
+ {
+ // "PERFORMER" "artist (role)"
+ Utils::PerformerArtist performer{ Utils::extractPerformerAndRole(name) };
+ StringUtils::capitalize(performer.role);
+ performers[performer.role].push_back(std::move(performer.artist));
+ }
+ else
+ {
+ // "PERFORMER:role", "artist" (MP3)
+ std::string roleCapitalized{ StringUtils::stringToLower(role) };
+ StringUtils::capitalize(roleCapitalized);
+ performers[roleCapitalized].push_back(Artist{ name });
+ }
+ });
+
+ return performers;
+ }
+ }
+
+ std::unique_ptr createParser(ParserBackend parserBackend, ParserReadStyle parserReadStyle)
+ {
+ return std::make_unique(parserBackend, parserReadStyle);
+ }
+
+ Parser::Parser(ParserBackend parserBackend, ParserReadStyle readStyle)
+ : _parserBackend{ parserBackend }
+ , _readStyle{ readStyle }
+ {
+ switch (_parserBackend)
+ {
+ case ParserBackend::TagLib:
+ LMS_LOG(METADATA, INFO, "Using TagLib parser with read style = " << Utils::readStyleToString(readStyle));
+ break;
+
+ case ParserBackend::AvFormat:
+ LMS_LOG(METADATA, INFO, "Using AvFormat parser");
+ break;
+ }
+ }
+
+ std::unique_ptr Parser::parse(const std::filesystem::path& p, bool debug)
+ {
+ try
+ {
+ std::unique_ptr tagReader;
+ switch (_parserBackend)
+ {
+ case ParserBackend::TagLib:
+ tagReader = std::make_unique(p, _readStyle, debug);
+ break;
+
+ case ParserBackend::AvFormat:
+ tagReader = std::make_unique(p, debug);
+ break;
+ }
+ if (!tagReader)
+ throw ParseException{ "Unhandled parser backend" };
+
+ return parse(*tagReader);
+ }
+ catch (const Exception& e)
+ {
+ LMS_LOG(METADATA, ERROR, "File '" << p.string() << "': parsing failed");
+ throw ParseException{};
+ }
+ }
+
+ std::unique_ptr Parser::parse(const ITagReader& tagReader)
+ {
+ auto track{ std::make_unique() };
+
+ processAudioProperties(tagReader, *track);
+ processTags(tagReader, *track);
+
+ return track;
+ }
+
+ void Parser::processAudioProperties(const ITagReader& tagReader, Track& track)
+ {
+ track.duration = tagReader.getDuration();
+ track.bitrate = tagReader.getBitrate();
+ }
+
+ void Parser::processTags(const ITagReader& tagReader, Track& track)
+ {
+ track.hasCover = tagReader.hasEmbeddedCover();
+
+ track.title = getTagValueAs(tagReader, TagType::TrackTitle).value_or("");
+ track.mbid = getTagValueAs(tagReader, TagType::MusicBrainzTrackID);
+ track.recordingMBID = getTagValueAs(tagReader, TagType::MusicBrainzRecordingID);
+ track.acoustID = getTagValueAs(tagReader, TagType::AcoustID);
+ track.position = getTagValueAs(tagReader, TagType::TrackNumber); // May parse 'Number/Total', that's fine
+ if (auto dateStr = getTagValueAs(tagReader, TagType::Date))
+ {
+ if (const Wt::WDate date{ Utils::parseDate(*dateStr) }; date.isValid())
+ {
+ track.date = date;
+ track.year = date.year();
+ }
+ else
+ {
+ track.year = Utils::parseYear(*dateStr);
+ }
+ }
+ if (auto dateStr = getTagValueAs(tagReader, TagType::OriginalReleaseDate))
+ {
+ if (const Wt::WDate date{ Utils::parseDate(*dateStr) }; date.isValid())
+ {
+ track.originalDate = date;
+ track.originalYear = date.year();
+ }
+ else
+ {
+ track.originalYear = Utils::parseYear(*dateStr);
+ }
+ }
+ if (auto dateStr = getTagValueAs(tagReader, TagType::OriginalReleaseYear))
+ {
+ track.originalYear = Utils::parseYear(*dateStr);
+ }
+
+ track.copyright = getTagValueAs(tagReader, TagType::Copyright).value_or("");
+ track.copyrightURL = getTagValueAs(tagReader, TagType::CopyrightURL).value_or("");
+ track.replayGain = getTagValueAs(tagReader, TagType::ReplayGainTrackGain);
+ track.artistDisplayName = getTagValueAs(tagReader, TagType::Artist).value_or(""); // TODO join on artists if present
+
+ for (const std::string& userExtraTag : _userExtraTags)
+ {
+ visitTagValues(tagReader, userExtraTag, _defaultTagDelimiters, [&](std::string_view value)
+ {
+ value = StringUtils::stringTrim(value);
+ if (!value.empty())
+ track.userExtraTags[userExtraTag].push_back(std::string{ value });
+ });
+ }
+
+ track.genres = getTagValuesAs(tagReader, TagType::Genre, _defaultTagDelimiters);
+ track.moods = getTagValuesAs(tagReader, TagType::Mood, _defaultTagDelimiters);
+ track.groupings = getTagValuesAs(tagReader, TagType::Grouping, _defaultTagDelimiters);
+ track.labels = getTagValuesAs(tagReader, TagType::RecordLabel, _defaultTagDelimiters);
+ track.languages = getTagValuesAs(tagReader, TagType::Language, _defaultTagDelimiters);
+
+ std::vector artistDelimiters{};
+
+ track.medium = getMedium(tagReader);
+ track.artists = getArtists(tagReader, { TagType::Artists, TagType::Artist }, { TagType::ArtistSortOrder }, { TagType::MusicBrainzArtistID }, _artistTagDelimiters);
+ track.conductorArtists = getArtists(tagReader, { TagType::Conductors, TagType::Conductor }, { TagType::ConductorsSortOrder, TagType::ConductorSortOrder }, {}, _artistTagDelimiters);
+ track.composerArtists = getArtists(tagReader, { TagType::Composers, TagType::Composer }, { TagType::ComposersSortOrder, TagType::ComposerSortOrder }, {}, _artistTagDelimiters);
+ track.lyricistArtists = getArtists(tagReader, { TagType::Lyricists, TagType::Lyricist }, { TagType::LyricistsSortOrder, TagType::LyricistSortOrder }, {}, _artistTagDelimiters);
+ track.mixerArtists = getArtists(tagReader, { TagType::Mixers, TagType::Mixer }, { TagType::MixersSortOrder, TagType::MixerSortOrder }, {}, _artistTagDelimiters);
+ track.producerArtists = getArtists(tagReader, { TagType::Producers, TagType::Producer }, { TagType::ProducersSortOrder, TagType::ProducerSortOrder }, {}, _artistTagDelimiters);
+ track.remixerArtists = getArtists(tagReader, { TagType::Remixers, TagType::Remixer }, { TagType::RemixersSortOrder, TagType::RemixerSortOrder }, {}, _artistTagDelimiters);
+ track.performerArtists = getPerformerArtists(tagReader); // artistDelimiters not supported
+
+ // If a file has date but no year, set it
+ if (!track.year && track.date.isValid())
+ track.year = track.date.year();
+
+ // If a file has originalDate but no originalYear, set it
+ if (!track.originalYear && track.originalDate.isValid())
+ track.originalYear = track.originalDate.year();
+ }
+
+
+ std::optional Parser::getMedium(const ITagReader& tagReader)
+ {
+ std::optional medium;
+ medium.emplace();
+
+ medium->media = getTagValueAs(tagReader, TagType::Media).value_or("");
+ medium->name = getTagValueAs(tagReader, TagType::DiscSubtitle).value_or("");
+ medium->trackCount = getTagValueAs(tagReader, TagType::TotalTracks);
+ if (!medium->trackCount)
+ {
+ // totalTracks may be encoded as "position/count"
+ if (const auto value{ getTagValueAs(tagReader, TagType::TrackNumber) })
+ {
+ // Expecting 'Number/Total'
+ const std::vector strings{ StringUtils::splitString(*value, '/') };
+ if (strings.size() == 2)
+ medium->trackCount = StringUtils::readAs(strings[1]);
+ }
+ }
+ // Expecting 'Number[/Total]'
+ medium->position = getTagValueAs(tagReader, TagType::DiscNumber);
+ medium->release = getRelease(tagReader);
+ medium->replayGain = getTagValueAs(tagReader, TagType::ReplayGainAlbumGain);
+
+ if (medium->isDefault())
+ medium.reset();
+
+ return medium;
+ }
+
+ std::optional Parser::getRelease(const ITagReader& tagReader)
+ {
+ std::optional release;
+
+ auto releaseName{ getTagValueAs(tagReader, TagType::Album) };
+ if (!releaseName)
+ return release;
+
+ release.emplace();
+ release->name = std::move(*releaseName);
+ release->sortName = getTagValueAs(tagReader, TagType::AlbumSortOrder).value_or("");
+ release->artistDisplayName = getTagValueAs(tagReader, TagType::AlbumArtist).value_or(""); // TODO try to join albumartists if present
+ release->mbid = getTagValueAs(tagReader, TagType::MusicBrainzReleaseID);
+ release->groupMBID = getTagValueAs(tagReader, TagType::MusicBrainzReleaseGroupID);
+ release->artists = getArtists(tagReader, { TagType::AlbumArtists, TagType::AlbumArtist }, { TagType::AlbumArtistsSortOrder, TagType::AlbumArtistSortOrder }, { TagType::MusicBrainzReleaseArtistID }, _artistTagDelimiters);
+ release->mediumCount = getTagValueAs(tagReader, TagType::TotalDiscs);
+ if (!release->mediumCount)
+ {
+ // mediumCount may be encoded as "position/count"
+ if (const auto value{ getTagValueAs(tagReader, TagType::DiscNumber) })
+ {
+ // Expecting 'Number/Total'
+ const std::vector strings{ StringUtils::splitString(*value, '/') };
+ if (strings.size() == 2)
+ release->mediumCount = StringUtils::readAs(strings[1]);
+ }
+ }
+
+ release->releaseTypes = getTagValuesAs(tagReader, TagType::ReleaseType, _defaultTagDelimiters);
+
+ return release;
+ }
+} // namespace MetaData
\ No newline at end of file
diff --git a/src/libs/metadata/impl/Parser.hpp b/src/libs/metadata/impl/Parser.hpp
new file mode 100644
index 00000000..6979beba
--- /dev/null
+++ b/src/libs/metadata/impl/Parser.hpp
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2018 Emeric Poupon
+ *
+ * This file is part of LMS.
+ *
+ * LMS is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * LMS is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with LMS. If not, see .
+ */
+
+#pragma once
+
+#include "metadata/IParser.hpp"
+#include "ITagReader.hpp"
+
+namespace MetaData
+{
+ class Parser : public IParser
+ {
+ public:
+ Parser(ParserBackend parserBackend = ParserBackend::TagLib, ParserReadStyle readStyle = ParserReadStyle::Average);
+
+ std::unique_ptr parse(const std::filesystem::path& p, bool debug = false) override;
+ std::unique_ptr parse(const ITagReader& reader);
+
+ private:
+ void setUserExtraTags(std::span extraTags) override { _userExtraTags.assign(std::cbegin(extraTags), std::cend(extraTags)); }
+ void setArtistTagDelimiters(std::span delimiters) override { _artistTagDelimiters.assign(std::cbegin(delimiters), std::cend(delimiters)); }
+ void setDefaultTagDelimiters(std::span delimiters) override { _defaultTagDelimiters.assign(std::cbegin(delimiters), std::cend(delimiters)); }
+
+ void processAudioProperties(const ITagReader& reader, Track& track);
+ void processTags(const ITagReader& reader, Track& track);
+
+ std::optional getMedium(const ITagReader& tagReader);
+ std::optional getRelease(const ITagReader& tagReader);
+
+ const ParserBackend _parserBackend;
+ const ParserReadStyle _readStyle;
+
+ std::vector _userExtraTags;
+ std::vector _artistTagDelimiters;
+ std::vector _defaultTagDelimiters;
+ };
+} // namespace MetaData
+
diff --git a/src/libs/metadata/impl/TagLibParser.cpp b/src/libs/metadata/impl/TagLibParser.cpp
deleted file mode 100644
index 2bfe2f1d..00000000
--- a/src/libs/metadata/impl/TagLibParser.cpp
+++ /dev/null
@@ -1,521 +0,0 @@
-/*
- * Copyright (C) 2016 Emeric Poupon
- *
- * This file is part of LMS.
- *
- * LMS is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * LMS is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with LMS. If not, see .
- */
-
-#include "TagLibParser.hpp"
-
-#include
-
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-#include "utils/IConfig.hpp"
-#include "utils/Exception.hpp"
-#include "utils/ILogger.hpp"
-#include "utils/Service.hpp"
-#include "utils/String.hpp"
-#include "Utils.hpp"
-
-namespace MetaData
-{
- namespace
- {
- // TODO use string_views here for values
- using TagMap = std::map>;
-
- template
- std::vector getPropertyValuesFirstMatchAs(const TagMap& tags, std::initializer_list keys)
- {
- std::vector res;
-
- for (std::string_view key : keys)
- {
- const auto itValues{ tags.find(std::string {key}) };
- if (itValues == std::cend(tags))
- continue;
-
- const std::vector& values{ itValues->second };
- if (values.empty())
- continue;
-
- res.reserve(values.size());
-
- for (const auto& value : values)
- {
- std::optional val{ StringUtils::readAs(value) };
- if (!val)
- continue;
-
- res.emplace_back(std::move(*val));
- }
-
- break;
- }
-
- return res;
- }
-
- template
- std::optional getPropertyValueFirstMatchAs(const TagMap& tags, std::initializer_list keys)
- {
- std::optional res;
- std::vector values{ getPropertyValuesFirstMatchAs(tags, keys) };
- if (!values.empty())
- res = std::move(values.front());
-
- return res;
- }
-
- template
- std::vector getPropertyValuesAs(const TagMap& tags, std::string_view key)
- {
- return getPropertyValuesFirstMatchAs(tags, { key });
- }
-
- template
- std::optional getPropertyValueAs(const TagMap& tags, std::string_view key)
- {
- return getPropertyValueFirstMatchAs(tags, { key });
- }
-
- std::vector splitAndTrimString(std::string_view str, std::string_view delimiters)
- {
- std::vector strings{ StringUtils::splitString(str, delimiters) };
- for (std::string_view& s : strings)
- s = StringUtils::stringTrim(s);
-
- return strings;
- }
-
- std::vector getArtists(const TagMap& tags,
- std::initializer_list artistTagNames,
- std::initializer_list artistSortTagNames,
- std::initializer_list artistMBIDTagNames
- )
- {
- const std::vector artistNames{ getPropertyValuesFirstMatchAs(tags, artistTagNames) };
- if (artistNames.empty())
- return {};
-
- std::vector artists;
- artists.reserve(artistNames.size());
- std::transform(std::cbegin(artistNames), std::cend(artistNames), std::back_inserter(artists),
- [&](std::string_view name) { return Artist{ name }; });
-
- {
- const std::vector artistSortNames{ getPropertyValuesFirstMatchAs(tags, artistSortTagNames) };
- if (artistSortNames.size() == artists.size())
- {
- for (std::size_t i{}; i < artistSortNames.size(); ++i)
- artists[i].sortName = artistSortNames[i];
- }
- }
-
- {
- const std::vector artistsMBID{ getPropertyValuesFirstMatchAs(tags, artistMBIDTagNames) };
-
- if (artistNames.size() == artistsMBID.size())
- {
- for (std::size_t i{}; i < artistsMBID.size(); ++i)
- artists[i].mbid = artistsMBID[i];
- }
- }
-
-
- return artists;
- }
-
- PerformerContainer getPerformerArtists(const TagMap& tags, std::initializer_list artistTagNames)
- {
- PerformerContainer performers;
-
- // picard stores like this: (see https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html#performer)
- // We may hit both styles for the same track
- // PERFORMER: artist (role)
- if (const std::vector artistNames{ getPropertyValuesFirstMatchAs(tags, artistTagNames) }; !artistNames.empty())
- {
- for (std::string_view entry : artistNames)
- {
- Utils::PerformerArtist performer{ Utils::extractPerformerAndRole(entry) };
- StringUtils::capitalize(performer.role);
- performers[performer.role].push_back(std::move(performer.artist));
- }
- }
- // PERFORMER:role (MP3)
- for (const auto& [key, values] : tags)
- {
- if (key.find("PERFORMER:") == 0)
- {
- std::string performerStr{ key };
- std::string role;
- if (const std::size_t rolePos{ performerStr.find(':') }; rolePos != std::string::npos)
- {
- role = StringUtils::stringToLower(performerStr.substr(rolePos + 1, performerStr.size() - rolePos + 1));
- StringUtils::capitalize(role);
- }
-
- for (const auto& value : values)
- performers[role].push_back(Artist{ value });
- }
- }
-
- return performers;
- }
-
- std::optional getRelease(const TagMap& tags)
- {
- std::optional release;
-
- auto releaseName{ getPropertyValueAs(tags, "ALBUM") };
- if (!releaseName)
- return release;
-
- release.emplace();
- release->name = std::move(*releaseName);
- release->artistDisplayName = getPropertyValueAs(tags, "ALBUMARTIST").value_or("");
- release->mbid = getPropertyValueFirstMatchAs(tags, { "MUSICBRAINZ_ALBUMID", "MUSICBRAINZ ALBUM ID", "MUSICBRAINZ/ALBUM ID" });
- release->artists = getArtists(tags, { "ALBUMARTISTS", "ALBUMARTIST" }, { "ALBUMARTISTSSORT", "ALBUMARTISTSORT" }, { "MUSICBRAINZ_ALBUMARTISTID", "MUSICBRAINZ ALBUM ARTIST ID", "MUSICBRAINZ/ALBUM ARTIST ID" });
- release->mediumCount = getPropertyValueAs(tags, "DISCTOTAL");
- if (!release->mediumCount)
- {
- // mediumCount may be encoded as "position/count"
- if (const auto value{ getPropertyValueAs(tags, "DISCNUMBER") })
- {
- // Expecting 'Number/Total'
- const std::vector strings{ StringUtils::splitString(*value, "/") };
- if (strings.size() == 2)
- release->mediumCount = StringUtils::readAs(strings[1]);
- }
- }
-
- release->releaseTypes = getPropertyValuesFirstMatchAs(tags, { "MUSICBRAINZ_ALBUMTYPE", "RELEASETYPE", "MUSICBRAINZ ALBUM TYPE", "MUSICBRAINZ/ALBUM TYPE" });
-
- return release;
- }
-
- std::optional getMedium(const TagMap& tags)
- {
- std::optional medium;
- medium.emplace();
-
- medium->type = getPropertyValueAs(tags, "MEDIA").value_or("");
- medium->name = getPropertyValueFirstMatchAs(tags, { "DISCSUBTITLE", "SETSUBTITLE" }).value_or("");
- medium->trackCount = getPropertyValueAs(tags, "TRACKTOTAL");
- if (!medium->trackCount)
- {
- // totalTracks may be encoded as "position/count"
- if (const auto value{ getPropertyValueAs(tags, "TRACKNUMBER") })
- {
- // Expecting 'Number/Total'
- const std::vector strings{ StringUtils::splitString(*value, "/") };
- if (strings.size() == 2)
- medium->trackCount = StringUtils::readAs(strings[1]);
- }
- }
- // Expecting 'Number[/Total]'
- medium->position = getPropertyValueAs(tags, "DISCNUMBER");
- medium->release = getRelease(tags);
- medium->replayGain = getPropertyValueAs