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}
+
+
+ ${artist-tag-delimiter class="form-control"}
+
+ ${artist-tag-delimiter-info}
+
+
+
+
+ ${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..420c6f03 100644
--- a/approot/messages.xml
+++ b/approot/messages.xml
@@ -64,7 +64,9 @@
Root directory
+Delimiter to be used for splitting artist names (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..68522632 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 noms 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..6f2d731b 100644
--- a/approot/messages_it.xml
+++ b/approot/messages_it.xml
@@ -64,7 +64,9 @@
Cartella principale
+Delimitatore da utilizzare per separare i nomi 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/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 1956022a..7ac08ba5 100644
--- a/src/libs/database/impl/Migration.cpp
+++ b/src/libs/database/impl/Migration.cpp
@@ -215,7 +215,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");
}
@@ -359,7 +359,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,
@@ -394,6 +394,13 @@ 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 doDbMigration(Session& session)
{
static const std::string outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
@@ -422,6 +429,7 @@ SELECT
{48, migrateFromV48},
{49, migrateFromV49},
{50, migrateFromV50},
+ {51, migrateFromV51},
};
{
diff --git a/src/libs/database/impl/Migration.hpp b/src/libs/database/impl/Migration.hpp
index 61452001..0d2d0d80 100644
--- a/src/libs/database/impl/Migration.hpp
+++ b/src/libs/database/impl/Migration.hpp
@@ -26,7 +26,7 @@ namespace Database
class Session;
using Version = std::size_t;
- static constexpr Version LMS_DATABASE_VERSION{ 51 };
+ static constexpr Version LMS_DATABASE_VERSION{ 52 };
class VersionInfo
{
public:
diff --git a/src/libs/database/impl/ScanSettings.cpp b/src/libs/database/impl/ScanSettings.cpp
index b11b9db9..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));
@@ -57,7 +57,17 @@ namespace Database
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)
@@ -69,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/include/database/ScanSettings.hpp b/src/libs/database/include/database/ScanSettings.hpp
index 715e127c..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,18 +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 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
@@ -85,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:
@@ -94,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/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