Merge branch 'develop' for release v3.65.0

This commit is contained in:
emeric
2025-03-24 20:47:07 +01:00
145 changed files with 4007 additions and 828 deletions
+3 -3
View File
@@ -6,7 +6,7 @@ on:
pull_request:
branches: [ "master", "develop" ]
schedule:
- cron: "48 10 * * 4"
- cron: "48 10 1 * *"
jobs:
analyze:
@@ -30,8 +30,8 @@ jobs:
name: Install dependencies (cpp)
run: |
sudo apt-get update
sudo apt-get install --yes build-essential cmake libboost-all-dev libconfig++-dev libavcodec-dev libavutil-dev libavformat-dev libstb-dev libtag1-dev libpam0g-dev libgtest-dev libarchive-dev
export WT_VERSION=4.9.0
sudo apt-get install --yes build-essential cmake libboost-all-dev libconfig++-dev libavcodec-dev libavutil-dev libavformat-dev libstb-dev libtag1-dev libpam0g-dev libgtest-dev libarchive-dev libxxhash-dev
export WT_VERSION=4.11.3
export WT_INSTALL_PREFIX=/usr
git clone https://github.com/emweb/wt.git /tmp/wt
pushd /tmp/wt
+2 -1
View File
@@ -23,7 +23,8 @@ ARG LMS_BUILD_PACKAGES=" \
taglib-dev \
stb \
wt-dev \
gtest-dev"
gtest-dev \
xxhash-dev"
COPY --from=xx / /
+2 -1
View File
@@ -13,7 +13,8 @@ ARG BUILD_PACKAGES="\
make \
pkgconfig \
taglib \
wt"
wt \
xxhash"
RUN pacman -Syu --noconfirm
RUN pacman -S --noconfirm ${BUILD_PACKAGES}
+2 -1
View File
@@ -32,7 +32,8 @@ ARG BUILD_PACKAGES=" \
libconfig-dev \
utfcpp \
sqlite-dev \
gtest-dev"
gtest-dev \
xxhash-dev"
RUN apk add --no-cache --update ${BUILD_PACKAGES}
+1 -1
View File
@@ -37,7 +37,7 @@ __Notes__:
* a C++20 compiler is needed
* ffmpeg version 4 minimum is required
```sh
apt-get install g++ cmake libboost-program-options-dev libboost-system-dev libavutil-dev libavformat-dev libstb-dev libconfig++-dev ffmpeg libtag1-dev libpam0g-dev libgtest-dev libarchive-dev
apt-get install g++ cmake libboost-program-options-dev libboost-system-dev libavutil-dev libavformat-dev libstb-dev libconfig++-dev ffmpeg libtag1-dev libpam0g-dev libgtest-dev libarchive-dev libxxhash-dev
```
__Notes__:
* libpam0g-dev is optional (only for using PAM authentication)
+9 -2
View File
@@ -43,6 +43,13 @@ __Note__: depending on your database size and/or your hardware, the tag-based re
## About tags
_LMS_ primarily relies on tags to organize your music collection but also supports browsing by directory using the [Subsonic/OpenSubsonic API](SUBSONIC.md).
## Artist information folder
_LMS_ supports an Artist information folder to manage metadata and images for artists. This folder can be placed anywhere within the scanned libraries but is best located in a dedicated `ArtistInfo` directory in the root of a media library for maximum compatibility with other softwares.
The folder must follow a structure defined by Kodi, as detailed [here](https://kodi.wiki/view/Artist_information_folder). `artist.nfo` files are used to define additional artist information such as biography, sort name, and MusicBrainz ArtistID. See the format [here](https://kodi.wiki/view/NFO_files/Artists).
__Note__: `artist.nfo` files must include both `name` and `musicBrainzArtistID` fields to be recognized.
### Filtering
It is possible to apply global filters on your collection using `genre`, `mood`, `grouping`, `language`, and by music library. More tags, including custom ones, can be added in the database administration settings.
@@ -77,8 +84,8 @@ For an album to be considered a match, the following conditions must be met:
## Artist image lookup
The recommended method is to name the artist image file using the artist's MusicBrainz ArtistID. This file can be placed anywhere within one of the scanned libraries.
If no file with the MusicBrainz ArtistID is found, _LMS_ will look for a file named `artist` (or another name configured in `lms.conf`) in the artist's directories:
If no file with the MusicBrainz ArtistID is found, _LMS_ will first look for files named `folder` and then `thumb` in the artist information directory, where the corresponding `artist.nfo` file is located.
If neither exists, it will then search for a file named `artist` (or another name configured in `lms.conf`) in the artist's directories, using this logic:
1. Identify the artist's directory: _LMS_ selects all albums by the artist using the "AlbumArtist" link and determines the longest common path among them.
2. Scan for the image: the directory is scanned starting from this common path, moving upwards if needed, until the artist image file is found.
3. Fallback search: if no image is found, _LMS_ will then search within each individual album folder.
+1
View File
@@ -36,6 +36,7 @@ The following extra fields are implemented:
* `recordLabels`
* `releaseTypes`
* `userRating`
* `version`
* `Child` response:
* `albumArtists`
* `artists`
+24 -16
View File
@@ -3,22 +3,30 @@
<!--FORMS message blocks-->
<message id="Lms.Explore.Artist.template">
<div class="mb-3">
<h2>${name}</h2>
${clusters class="mb-2"}
<div class="btn-group">
${play-btn class="btn btn-primary"}
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false"/>
<ul class="dropdown-menu">
<li>${play-next class="dropdown-item"}</li>
<li>${play-last class="dropdown-item"}</li>
<li>${play-shuffled class="dropdown-item"}</li>
<li>${star class="dropdown-item"}</li>
${<if-has-mbid>}
<li><a href="${mbid-link}" target="_blank" class="dropdown-item">${tr:Lms.Explore.musicbrainz-artist}</a></li>
${</if-has-mbid>}
<li>${download class="dropdown-item"}</li>
</ul>
<div class="row mb-3 gy-3">
<div class="col-lg-3 col-md-4">
${artwork class="Lms-cursor-pointer rounded d-block mx-auto"}
</div>
<div class="col">
<h2>${name}</h2>
${clusters class="mb-2"}
${<if-has-biography>}
${biography class="mb-2 Lms-multiline-clamp Lms-cursor-pointer"}
${</if-has-biography>}
<div class="btn-group">
${play-btn class="btn btn-primary"}
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false"/>
<ul class="dropdown-menu">
<li>${play-next class="dropdown-item"}</li>
<li>${play-last class="dropdown-item"}</li>
<li>${play-shuffled class="dropdown-item"}</li>
<li>${star class="dropdown-item"}</li>
${<if-has-mbid>}
<li><a href="${mbid-link}" target="_blank" class="dropdown-item">${tr:Lms.Explore.musicbrainz-artist}</a></li>
${</if-has-mbid>}
<li>${download class="dropdown-item"}</li>
</ul>
</div>
</div>
</div>
${release-containers}
+12
View File
@@ -69,4 +69,16 @@
</div>
</message>
<message id="Lms.Explore.template.full-modal-artwork">
<div class="modal fade" tabindex="-1">
<div class="modal-dialog modal-dialog-centered Lms-artwork-full-modal-dialog">
<div class="modal-content bg-transparent border-0">
<div class="modal-body text-center">
${artwork class="Lms-cover-full Lms-cursor-pointer"}
</div>
</div>
</div>
</div>
</message>
</messages>
+1 -1
View File
@@ -1,4 +1,4 @@
<svg width="256px" height="256px" viewBox="0 0 16.00 16.00" xmlns="http://www.w3.org/2000/svg" fill="#c9c9c9" stroke="#c9c9c9" stroke-width="0.00016" transform="rotate(0)matrix(1, 0, 0, 1, 0, 0)">
<svg width="512px" height="512px" viewBox="0 0 16.00 16.00" xmlns="http://www.w3.org/2000/svg" fill="#c9c9c9" stroke="#c9c9c9" stroke-width="0.00016" transform="rotate(0)matrix(1, 0, 0, 1, 0, 0)">
<g id="SVGRepo_bgCarrier" stroke-width="0" transform="translate(0,0), scale(1)"></g>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round" stroke="#CCCCCC" stroke-width="0.6719999999999999"></g>
<g id="SVGRepo_iconCarrier">

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

+4
View File
@@ -97,6 +97,7 @@
<!--Scanner Controller-->
<message id="Lms.Admin.ScannerController.bad-duration">Cannot get track duration</message>
<message id="Lms.Admin.ScannerController.cannot-read-artist-info-file">Cannot parse artist info file</message>
<message id="Lms.Admin.ScannerController.cannot-read-audio-file">Cannot parse audio file</message>
<message id="Lms.Admin.ScannerController.cannot-read-file">Cannot read file</message>
<message id="Lms.Admin.ScannerController.cannot-read-image-file">Cannot parse image file</message>
@@ -208,6 +209,7 @@
<!--Explore:Artist-->
<message id="Lms.Explore.Artist.appears-on">Appears on</message>
<message id="Lms.Explore.Artist.biography">Biography</message>
<message id="Lms.Explore.Artist.similar-artists">Similar artists</message>
<!--Explore:Artists-->
@@ -309,8 +311,10 @@
<message id="Lms.Settings.default-transcoding-output-bitrate">Default transcoding output bitrate</message>
<message id="Lms.Settings.default-transcoding-output-format">Default transcoding output format</message>
<message id="Lms.Settings.demo-cannot-save">Cannot save using a demo account!</message>
<message id="Lms.Settings.enable-inline-artist-relationships">Display artist relationships inline within albums</message>
<message id="Lms.Settings.enable-transcoding-by-default">Enable transcoding by default</message>
<message id="Lms.Settings.feedback">Feedback</message>
<message id="Lms.Settings.inline-artist-relationships">Types of artist relationships to display</message>
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Settings</message>
<message id="Lms.Settings.name">Album Name</message>
<message id="Lms.Settings.original-date-asc">Original Release Date (Ascending)</message>
+4
View File
@@ -97,6 +97,7 @@
<!--Scanner Controller-->
<message id="Lms.Admin.ScannerController.bad-duration">Impossible de récupérer la durée de la piste</message>
<message id="Lms.Admin.ScannerController.cannot-read-artist-info-file">Impossible d'analyser le fichier d'informations sur l'artiste</message>
<message id="Lms.Admin.ScannerController.cannot-read-audio-file">Impossible d'analyser le fichier audio</message>
<message id="Lms.Admin.ScannerController.cannot-read-file">Impossible de lire le fichier</message>
<message id="Lms.Admin.ScannerController.cannot-read-image-file">Impossible d'analyser le fichier image</message>
@@ -208,6 +209,7 @@
<!--Explore:Artist-->
<message id="Lms.Explore.Artist.appears-on">Apparaît dans</message>
<message id="Lms.Explore.Artist.biography">Biographie</message>
<message id="Lms.Explore.Artist.similar-artists">Artistes similaires</message>
<!--Explore:Artists-->
@@ -309,8 +311,10 @@
<message id="Lms.Settings.default-transcoding-output-bitrate">Bitrate par défaut du transcodage</message>
<message id="Lms.Settings.default-transcoding-output-format">Format par défaut du transcodage</message>
<message id="Lms.Settings.demo-cannot-save">Impossible de sauvegarder en utilisant un compte de démo !</message>
<message id="Lms.Settings.enable-inline-artist-relationships">Afficher les relations d'artiste en ligne dans les albums</message>
<message id="Lms.Settings.enable-transcoding-by-default">Activer par défaut le transcodage</message>
<message id="Lms.Settings.feedback">Feedback</message>
<message id="Lms.Settings.inline-artist-relationships">Types de relations d'artiste à afficher</message>
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Paramètres</message>
<message id="Lms.Settings.name">Nom de l'album</message>
<message id="Lms.Settings.original-date-asc">Date de sortie originale (Croissant)</message>
+4
View File
@@ -97,6 +97,7 @@
<!--Scanner Controller-->
<message id="Lms.Admin.ScannerController.bad-duration">Non sono stato in grado di determinare la durata della traccia</message>
<message id="Lms.Admin.ScannerController.cannot-read-artist-info-file">Impossibile analizzare il file delle informazioni sull'artista</message>
<message id="Lms.Admin.ScannerController.cannot-read-audio-file">Impossibile analizzare il file audio</message>
<message id="Lms.Admin.ScannerController.cannot-read-file">Non in grado di leggere il file</message>
<message id="Lms.Admin.ScannerController.cannot-read-image-file">Impossibile analizzare il file immagine</message>
@@ -208,6 +209,7 @@
<!--Explore:Artist-->
<message id="Lms.Explore.Artist.appears-on">Appare su</message>
<message id="Lms.Explore.Artist.biography">Biografia</message>
<message id="Lms.Explore.Artist.similar-artists">Artisti simili</message>
<!--Explore:Artists-->
@@ -309,8 +311,10 @@
<message id="Lms.Settings.default-transcoding-output-bitrate">Bitrate di default per la transcodifica</message>
<message id="Lms.Settings.default-transcoding-output-format">Formato di default per la transcodifica</message>
<message id="Lms.Settings.demo-cannot-save">Non posso salvare le impostazioni utilizzando l'account demo!</message>
<message id="Lms.Settings.enable-inline-artist-relationships">Mostra le relazioni degli artisti in linea negli album</message>
<message id="Lms.Settings.enable-transcoding-by-default">Abilita transcodifica di default</message>
<message id="Lms.Settings.feedback">Feedback</message>
<message id="Lms.Settings.inline-artist-relationships">Tipi di relazioni artistiche da visualizzare</message>
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Impostazioni</message>
<message id="Lms.Settings.name">Nome dell'album</message>
<message id="Lms.Settings.original-date-asc">Data di uscita originale (Crescente)</message>
+4
View File
@@ -98,6 +98,7 @@
<!--Scanner Controller-->
<message id="Lms.Admin.ScannerController.bad-duration">Nie udało się ustalić długości ścieżki</message>
<message id="Lms.Admin.ScannerController.cannot-read-artist-info-file">Nie można przetworzyć pliku z informacjami o artyście"</message>
<message id="Lms.Admin.ScannerController.cannot-read-audio-file">Nie można przeanalizować pliku audio</message>
<message id="Lms.Admin.ScannerController.cannot-read-file">Nie udało się odczytać pliku</message>
<message id="Lms.Admin.ScannerController.cannot-read-image-file">Nie można przeanalizować pliku obrazu</message>
@@ -225,6 +226,7 @@
<!--Explore:Artist-->
<message id="Lms.Explore.Artist.appears-on">Pojawia się na</message>
<message id="Lms.Explore.Artist.biography">Biografia</message>
<message id="Lms.Explore.Artist.similar-artists">Podobni artyści</message>
<!--Explore:Artists-->
@@ -335,8 +337,10 @@
<message id="Lms.Settings.default-transcoding-output-bitrate">Domyślna przepływność transkodowania</message>
<message id="Lms.Settings.default-transcoding-output-format">Domyślny format wyjściowy transkodowania</message>
<message id="Lms.Settings.demo-cannot-save">Nie można zapisywać na koncie demonstracyjnym!</message>
<message id="Lms.Settings.enable-inline-artist-relationships">Wyświetlaj powiązania artystów w albumach</message>
<message id="Lms.Settings.enable-transcoding-by-default">Aktywuj domyślnie transkodowanie</message>
<message id="Lms.Settings.feedback">Feedback</message>
<message id="Lms.Settings.inline-artist-relationships">Typy powiązań artystów do wyświetlenia</message>
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Ustawienia</message>
<message id="Lms.Settings.name">Nazwa albumu</message>
<message id="Lms.Settings.original-date-asc">Oryginalna data wydania (Rosnąco)</message>
+4
View File
@@ -97,6 +97,7 @@
<!--Scanner Controller-->
<message id="Lms.Admin.ScannerController.bad-duration">无法获得音轨时间</message>
<message id="Lms.Admin.ScannerController.cannot-read-audio-file">无法解析文件</message>
<message id="Lms.Admin.ScannerController.cannot-read-file">无法读取文件</message>
@@ -208,6 +209,7 @@
<!--Explore:Artist-->
<message id="Lms.Explore.Artist.appears-on">出现于</message>
<message id="Lms.Explore.Artist.similar-artists">相似歌手</message>
<!--Explore:Artists-->
@@ -311,6 +313,8 @@
<message id="Lms.Settings.demo-cannot-save">使用演示账号时不能保存!</message>
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> 设置</message>
+10 -15
View File
@@ -9,7 +9,7 @@
<message id="Lms.Explore.Release.template">
<div class="row mb-3 gy-3">
<div class="col-lg-3 col-md-4">
${cover class="Lms-cursor-pointer rounded"}
${cover class="Lms-cursor-pointer rounded d-block mx-auto"}
</div>
<div class="col">
<h2>${name}${<if-has-release-comment>}<span class="ms-1 text-muted">[${comment}]</span>${</if-has-release-comment>}</h2>
@@ -49,18 +49,6 @@
${</if-has-similar-releases>}
</message>
<message id="Lms.Explore.Release.template.full-cover">
<div class="modal fade" tabindex="-1">
<div class="modal-dialog modal-dialog-centered Lms-cover-full-modal-dialog">
<div class="modal-content bg-transparent border-0">
<div class="modal-body text-center">
${cover-full class="Lms-cover-full"}
</div>
</div>
</div>
</div>
</message>
<message id="Lms.Explore.Release.template.entry-release-artist">
${artist class="text-decoration-none link-success"}
</message>
@@ -94,13 +82,14 @@
${<if-has-track-number>}${track-number}${</if-has-track-number>}
</div>
<div class="row align-items-center flex-fill overflow-hidden">
<div class="col-12 col-md-6">
<div class="col-12 col-md-8">
<div class="p-2">
${name}
${<if-has-artists>}${artists class="d-block d-md-none"}${</if-has-artists>}
${<if-has-artist-links>}${artist-links}${</if-has-artist-links>}
</div>
</div>
<div class="col-md-6">
<div class="col-md-4">
${<if-has-artists>}${artists-md class="d-none d-md-block"}${</if-has-artists>}
</div>
</div>
@@ -125,6 +114,12 @@
</div>
</message>
<message id="Lms.Explore.Release.template.artist-links-entry">
<div class=" text-muted small ms-2 d-flex">
<span class="text-nowrap text-muted">${role} — </span>${anchors class="ms-1"}
</div>
</message>
<message id="Lms.Explore.Release.template.release-info">
<div class="modal fade" tabindex="-1">
<div class="modal-dialog modal-dialog-scrollable">
+20
View File
@@ -15,6 +15,26 @@
${artist-release-sort-method-info}
</div>
</div>
<div class="col-12">
<div class="form-check">
${enable-inline-artist-relationships class="form-check-input"}
<label class="form-check-label" for="${id:enable-inline-artist-relationships}">
${tr:Lms.Settings.enable-inline-artist-relationships}
</label>
<div class="invalid-feedback">
${enable-inline-artist-relationships}
</div>
</div>
</div>
<div class="col-12">
<label class="form-label" for="${id:inline-artist-relationships}">
${tr:Lms.Settings.inline-artist-relationships}
</label>
${inline-artist-relationships class="form-control"}
<div class="invalid-feedback">
${inline-artist-relationships-info}
</div>
</div>
<legend>${tr:Lms.Settings.audio}
<button type="button" class="btn btn-sm p-0" data-bs-toggle="tooltip" data-bs-placement="right" title="${tr:Lms.Settings.audio-settings-are-local}">
<i class="fa fa-fw fa-info-circle" aria-hidden="true"></i>
+2 -3
View File
@@ -87,7 +87,7 @@ body {
min-width: 64px;
}
.Lms-cover-full-modal-dialog {
.Lms-artwork-full-modal-dialog {
width: auto;
max-width: none;
max-height: 90vh;
@@ -103,12 +103,11 @@ body {
height: auto;
display: block;
margin: auto;
cursor: pointer;
object-fit: contain;
}
.Lms-cursor-pointer {
cursor: pointer;
cursor: pointer;
}
#lms-mp-progress {
+3
View File
@@ -1,5 +1,6 @@
pkg_check_modules(Config++ REQUIRED IMPORTED_TARGET libconfig++)
pkg_check_modules(Archive REQUIRED IMPORTED_TARGET libarchive)
pkg_check_modules(XXHASH REQUIRED IMPORTED_TARGET libxxhash)
add_library(lmscore STATIC
impl/http/Client.cpp
@@ -21,6 +22,7 @@ add_library(lmscore STATIC
impl/TraceLogger.cpp
impl/UUID.cpp
impl/WtLogger.cpp
impl/XxHash3.cpp
)
target_include_directories(lmscore INTERFACE
@@ -34,6 +36,7 @@ target_include_directories(lmscore PRIVATE
target_link_libraries(lmscore PRIVATE
PkgConfig::Config++
PkgConfig::Archive
PkgConfig::XXHASH
)
target_link_libraries(lmscore PUBLIC
+31
View File
@@ -0,0 +1,31 @@
/*
* Copyright (C) 2025 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 <http://www.gnu.org/licenses/>.
*/
#include "core/XxHash3.hpp"
#define XXH_INLINE_ALL
#include <xxhash.h>
namespace lms::core
{
std::uint64_t xxHash3_64(std::span<const std::byte> buf)
{
return XXH3_64bits(buf.data(), buf.size());
}
} // namespace lms::core
+41
View File
@@ -0,0 +1,41 @@
/*
* Copyright (C) 2025 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
namespace lms::core
{
template<typename Tag, typename T>
class TaggedType
{
public:
using underlying_type = T;
explicit constexpr TaggedType() = default;
explicit constexpr TaggedType(T value)
: _value{ value } {}
constexpr T value() const { return _value; }
auto operator<=>(const TaggedType&) const = default;
private:
T _value{};
};
} // namespace lms::core
+29
View File
@@ -0,0 +1,29 @@
/*
* Copyright (C) 2025 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <cstddef>
#include <cstdint>
#include <span>
namespace lms::core
{
std::uint64_t xxHash3_64(std::span<const std::byte> buf);
} // namespace lms::core
+1
View File
@@ -11,6 +11,7 @@ add_executable(test-core
TraceLogger.cpp
Utils.cpp
UUID.cpp
XxHash3.cpp
)
target_link_libraries(test-core PRIVATE
+37
View File
@@ -0,0 +1,37 @@
/*
* Copyright (C) 2025 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 <http://www.gnu.org/licenses/>.
*/
#include <gtest/gtest.h>
#include "core/XxHash3.hpp"
namespace lms::core
{
TEST(Xxhash3_64, basic)
{
std::vector<std::byte> buffer;
buffer.resize(1024);
for (std::size_t i{}; i < buffer.size(); ++i)
buffer[i] = static_cast<std::byte>(i);
const std::uint64_t hash{ xxHash3_64(buffer) };
EXPECT_EQ(hash, 12137474952470826274ULL);
}
} // namespace lms::core
+3
View File
@@ -1,5 +1,6 @@
add_library(lmsdatabase STATIC
impl/Artist.cpp
impl/ArtistInfo.cpp
impl/AuthToken.cpp
impl/Cluster.cpp
impl/Db.cpp
@@ -25,6 +26,8 @@ add_library(lmsdatabase STATIC
impl/SqlQuery.cpp
impl/Track.cpp
impl/TrackBookmark.cpp
impl/TrackEmbeddedImage.cpp
impl/TrackEmbeddedImageLink.cpp
impl/TrackLyrics.cpp
impl/Types.cpp
impl/UIState.cpp
+15 -4
View File
@@ -29,9 +29,9 @@
#include "database/Track.hpp"
#include "database/User.hpp"
#include "IdTypeTraits.hpp"
#include "SqlQuery.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
namespace lms::db
{
@@ -169,10 +169,10 @@ namespace lms::db
query.orderBy("RANDOM()");
break;
case ArtistSortMethod::LastWrittenDesc:
query.orderBy("MAX(t.file_last_write) DESC");
query.orderBy("MAX(t.file_last_write) DESC, a.sort_name");
break;
case ArtistSortMethod::AddedDesc:
query.orderBy("MAX(t.file_added) DESC");
query.orderBy("MIN(t.file_added) DESC, a.sort_name");
break;
case ArtistSortMethod::StarredDateDesc:
assert(params.starringUser.isValid());
@@ -265,7 +265,18 @@ namespace lms::db
RangeResults<ArtistId> Artist::findOrphanIds(Session& session, std::optional<Range> range)
{
session.checkReadTransaction();
auto query{ session.getDboSession()->query<ArtistId>("SELECT DISTINCT a.id FROM artist a WHERE NOT EXISTS(SELECT 1 FROM track t INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id WHERE t.id = t_a_l.track_id)") };
auto query{ session.getDboSession()->query<ArtistId>(R"(SELECT DISTINCT a.id FROM artist a
WHERE NOT EXISTS (
SELECT 1
FROM track t
INNER JOIN track_artist_link t_a_l
ON t_a_l.artist_id = a.id
WHERE t.id = t_a_l.track_id
)
AND NOT EXISTS (
SELECT 1
FROM artist_info ai
WHERE ai.artist_id = a.id))") };
return utils::execRangeQuery<ArtistId>(query, range);
}
+114
View File
@@ -0,0 +1,114 @@
/*
* Copyright (C) 2025 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 <http://www.gnu.org/licenses/>.
*/
#include "database/ArtistInfo.hpp"
#include <Wt/Dbo/WtSqlTraits.h>
#include "database/Artist.hpp"
#include "database/ArtistInfoId.hpp"
#include "database/Directory.hpp"
#include "database/Session.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
#include "traits/PathTraits.hpp"
namespace lms::db
{
ArtistInfo::pointer ArtistInfo::create(Session& session)
{
return session.getDboSession()->add(std::unique_ptr<ArtistInfo>{ new ArtistInfo{} });
}
std::size_t ArtistInfo::getCount(Session& session)
{
session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM artist_info"));
}
ArtistInfo::pointer ArtistInfo::find(Session& session, const std::filesystem::path& p)
{
session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<ArtistInfo>>("SELECT a_i from artist_info a_i").where("a_i.absolute_file_path = ?").bind(p));
}
ArtistInfo::pointer ArtistInfo::find(Session& session, ArtistInfoId id)
{
session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<ArtistInfo>>("SELECT a_i from artist_info a_i").where("a_i.id = ?").bind(id));
}
void ArtistInfo::find(Session& session, ArtistId id, std::optional<Range> range, const std::function<void(const pointer&)>& func)
{
session.checkReadTransaction();
auto query{ session.getDboSession()->query<Wt::Dbo::ptr<ArtistInfo>>("SELECT a_i from artist_info a_i").where("a_i.artist_id = ?").bind(id) };
utils::forEachQueryRangeResult(query, range, [&](const ArtistInfo::pointer& entry) {
func(entry);
});
}
void ArtistInfo::find(Session& session, ArtistId id, const std::function<void(const pointer&)>& func)
{
find(session, id, std::nullopt, std::move(func));
}
void ArtistInfo::find(Session& session, ArtistInfoId& lastRetrievedId, std::size_t count, const std::function<void(const pointer&)>& func)
{
session.checkReadTransaction();
auto query{ session.getDboSession()->query<Wt::Dbo::ptr<ArtistInfo>>("SELECT a_i from artist_info a_i").orderBy("a_i.id").where("a_i.id > ?").bind(lastRetrievedId).limit(static_cast<int>(count)) };
utils::forEachQueryResult(query, [&](const ArtistInfo::pointer& entry) {
func(entry);
lastRetrievedId = entry->getId();
});
}
Artist::pointer ArtistInfo::getArtist() const
{
return _artist;
}
Directory::pointer ArtistInfo::getDirectory() const
{
return _directory;
}
void ArtistInfo::setAbsoluteFilePath(const std::filesystem::path& filePath)
{
assert(filePath.is_absolute());
_absoluteFilePath = filePath;
_fileStem = filePath.stem();
}
void ArtistInfo::setDirectory(ObjectPtr<Directory> directory)
{
_directory = getDboPtr(directory);
}
void ArtistInfo::setArtist(ObjectPtr<Artist> artist)
{
_artist = getDboPtr(artist);
}
} // namespace lms::db
+2 -2
View File
@@ -24,9 +24,9 @@
#include "database/Session.hpp"
#include "database/User.hpp"
#include "IdTypeTraits.hpp"
#include "StringViewTraits.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
#include "traits/StringViewTraits.hpp"
namespace lms::db
{
+2 -2
View File
@@ -27,10 +27,10 @@
#include "database/Session.hpp"
#include "database/Track.hpp"
#include "IdTypeTraits.hpp"
#include "SqlQuery.hpp"
#include "StringViewTraits.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
#include "traits/StringViewTraits.hpp"
namespace lms::db
{
+4 -2
View File
@@ -23,9 +23,9 @@
#include "database/Session.hpp"
#include "database/Types.hpp"
#include "IdTypeTraits.hpp"
#include "PathTraits.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
#include "traits/PathTraits.hpp"
namespace lms::db
{
@@ -176,11 +176,13 @@ namespace lms::db
query.leftJoin("image i ON d.id = i.directory_id");
query.leftJoin("track_lyrics l_lrc ON d.id = l_lrc.directory_id");
query.leftJoin("playlist_file pl_f ON d.id = pl_f.directory_id");
query.leftJoin("artist_info a_i ON d.id = a_i.directory_id");
query.where("d_child.id IS NULL");
query.where("t.directory_id IS NULL");
query.where("i.directory_id IS NULL");
query.where("l_lrc.directory_id IS NULL");
query.where("pl_f.directory_id IS NULL");
query.where("a_i.directory_id IS NULL");
return utils::execRangeQuery<DirectoryId>(query, range);
}
+2 -2
View File
@@ -26,9 +26,9 @@
#include "database/Release.hpp"
#include "database/Session.hpp"
#include "IdTypeTraits.hpp"
#include "PathTraits.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
#include "traits/PathTraits.hpp"
namespace lms::db
{
+1 -1
View File
@@ -22,9 +22,9 @@
#include "database/Track.hpp"
#include "database/User.hpp"
#include "IdTypeTraits.hpp"
#include "SqlQuery.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
namespace lms::db
{
+3 -3
View File
@@ -23,10 +23,10 @@
#include "database/Session.hpp"
#include "database/Track.hpp"
#include "IdTypeTraits.hpp"
#include "PathTraits.hpp"
#include "StringViewTraits.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
#include "traits/PathTraits.hpp"
#include "traits/StringViewTraits.hpp"
namespace lms::db
{
+64 -1
View File
@@ -35,7 +35,7 @@ namespace lms::db
{
namespace
{
static constexpr Version LMS_DATABASE_VERSION{ 82 };
static constexpr Version LMS_DATABASE_VERSION{ 85 };
}
VersionInfo::VersionInfo()
@@ -1099,6 +1099,66 @@ FROM tracklist)");
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
}
void migrateFromV82(Session& session)
{
// New setting to display inline artist relationships in the release view
utils::executeCommand(*session.getDboSession(), "ALTER TABLE user ADD COLUMN ui_enable_inline_artist_relationships BOOLEAN NOT NULL DEFAULT(false)");
utils::executeCommand(*session.getDboSession(), "ALTER TABLE user ADD COLUMN ui_inline_artist_relationships BIGINT NOT NULL DEFAULT(68)"); // Composer + Performer
}
void migrateFromV83(Session& session)
{
// New embedded track image handling
utils::executeCommand(*session.getDboSession(), "ALTER TABLE track DROP COLUMN has_cover");
utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "track_embedded_image" (
"id" integer primary key autoincrement,
"version" integer not null,
"hash" text not null,
"size" integer not null,
"width" integer not null,
"height" integer not null,
"mime_type" text not null
))");
utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "track_embedded_image_link" (
"id" integer primary key autoincrement,
"version" integer not null,
"index" integer not null,
"is_preferred" boolean not null,
"type" integer not null,
"description" text not null,
"track_id" bigint,
"track_embedded_image_id" bigint,
constraint "fk_track_embedded_image_link_track" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred,
constraint "fk_track_embedded_image_link_track_embedded_image" foreign key ("track_embedded_image_id") references "track_embedded_image" ("id") on delete cascade deferrable initially deferred
))");
// Just increment the scan version of the settings to make the next scan rescan everything
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
}
void migrateFromV84(Session& session)
{
// New artist info feature
utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "artist_info" (
"id" integer primary key autoincrement,
"version" integer not null,
"absolute_file_path" text not null,
"file_last_write" text,
"type" text not null,
"gender" text not null,
"disambiguation" text not null,
"biography" text not null,
"directory_id" bigint,
"artist_id" bigint,
constraint "fk_artist_info_directory" foreign key ("directory_id") references "directory" ("id") on delete cascade deferrable initially deferred,
constraint "fk_artist_info_artist" foreign key ("artist_id") references "artist" ("id") on delete cascade deferrable initially deferred
))");
// Just increment the scan version of the settings to make the next scan rescan everything
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
}
bool doDbMigration(Session& session)
{
constexpr std::string_view outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
@@ -1157,6 +1217,9 @@ FROM tracklist)");
{ 79, migrateFromV79 },
{ 80, migrateFromV80 },
{ 81, migrateFromV81 },
{ 82, migrateFromV82 },
{ 83, migrateFromV83 },
{ 84, migrateFromV84 },
};
bool migrationPerformed{};
+3 -3
View File
@@ -28,10 +28,10 @@
#include "database/TrackList.hpp"
#include "database/User.hpp"
#include "IdTypeTraits.hpp"
#include "PathTraits.hpp"
#include "StringViewTraits.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
#include "traits/PathTraits.hpp"
#include "traits/StringViewTraits.hpp"
namespace lms::db
{
+2 -2
View File
@@ -28,9 +28,9 @@
#include "database/Track.hpp"
#include "database/User.hpp"
#include "IdTypeTraits.hpp"
#include "StringViewTraits.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
#include "traits/StringViewTraits.hpp"
namespace lms::db
{
+1 -1
View File
@@ -25,8 +25,8 @@
#include "database/Session.hpp"
#include "database/User.hpp"
#include "IdTypeTraits.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
namespace lms::db
{
+1 -1
View File
@@ -25,8 +25,8 @@
#include "database/Session.hpp"
#include "database/User.hpp"
#include "IdTypeTraits.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
namespace lms::db
{
+1 -1
View File
@@ -25,8 +25,8 @@
#include "database/Track.hpp"
#include "database/User.hpp"
#include "IdTypeTraits.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
namespace lms::db
{
+4 -4
View File
@@ -31,12 +31,12 @@
#include "database/Types.hpp"
#include "database/User.hpp"
#include "EnumSetTraits.hpp"
#include "IdTypeTraits.hpp"
#include "PartialDateTimeTraits.hpp"
#include "SqlQuery.hpp"
#include "StringViewTraits.hpp"
#include "Utils.hpp"
#include "traits/EnumSetTraits.hpp"
#include "traits/IdTypeTraits.hpp"
#include "traits/PartialDateTimeTraits.hpp"
#include "traits/StringViewTraits.hpp"
namespace lms::db
{
+22 -5
View File
@@ -19,10 +19,10 @@
#include "database/Session.hpp"
#include "core/Exception.hpp"
#include "core/ILogger.hpp"
#include "core/ITraceLogger.hpp"
#include "database/Artist.hpp"
#include "database/ArtistInfo.hpp"
#include "database/AuthToken.hpp"
#include "database/Cluster.hpp"
#include "database/Db.hpp"
@@ -43,6 +43,8 @@
#include "database/Track.hpp"
#include "database/TrackArtistLink.hpp"
#include "database/TrackBookmark.hpp"
#include "database/TrackEmbeddedImage.hpp"
#include "database/TrackEmbeddedImageLink.hpp"
#include "database/TrackFeatures.hpp"
#include "database/TrackList.hpp"
#include "database/TrackLyrics.hpp"
@@ -50,11 +52,12 @@
#include "database/UIState.hpp"
#include "database/User.hpp"
#include "EnumSetTraits.hpp"
#include "Migration.hpp"
#include "PartialDateTimeTraits.hpp"
#include "PathTraits.hpp"
#include "Utils.hpp"
#include "traits/EnumSetTraits.hpp"
#include "traits/ImageHashTypeTraits.hpp"
#include "traits/PartialDateTimeTraits.hpp"
#include "traits/PathTraits.hpp"
namespace lms::db
{
@@ -97,8 +100,8 @@ namespace lms::db
{
_session.setConnectionPool(_db.getConnectionPool());
_session.mapClass<VersionInfo>("version_info");
_session.mapClass<Artist>("artist");
_session.mapClass<ArtistInfo>("artist_info");
_session.mapClass<AuthToken>("auth_token");
_session.mapClass<Cluster>("cluster");
_session.mapClass<ClusterType>("cluster_type");
@@ -122,12 +125,15 @@ namespace lms::db
_session.mapClass<Track>("track");
_session.mapClass<TrackBookmark>("track_bookmark");
_session.mapClass<TrackArtistLink>("track_artist_link");
_session.mapClass<TrackEmbeddedImage>("track_embedded_image");
_session.mapClass<TrackEmbeddedImageLink>("track_embedded_image_link");
_session.mapClass<TrackFeatures>("track_features");
_session.mapClass<TrackList>("tracklist");
_session.mapClass<TrackListEntry>("tracklist_entry");
_session.mapClass<TrackLyrics>("track_lyrics");
_session.mapClass<UIState>("ui_state");
_session.mapClass<User>("user");
_session.mapClass<VersionInfo>("version_info");
}
WriteTransaction Session::createWriteTransaction()
@@ -193,6 +199,10 @@ namespace lms::db
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_sort_name_nocase_idx ON artist(sort_name COLLATE NOCASE)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_mbid_idx ON artist(mbid)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_info_path_idx ON artist_info(absolute_file_path)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_info_directory_id_idx ON artist_info(directory_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_info_artist_id_idx ON artist_info(artist_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_user_domain_idx ON auth_token(user_id, domain)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_domain_expiry_idx ON auth_token(domain, expiry)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_domain_value_idx ON auth_token(domain, value)");
@@ -209,6 +219,13 @@ namespace lms::db
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS directory_media_library_idx ON directory(media_library_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS directory_name_idx ON directory(name COLLATE NOCASE)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_id_idx ON track_embedded_image(id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_hash_idx ON track_embedded_image(hash)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_link_track_id_idx ON track_embedded_image_link(track_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_link_track_embedded_image_id_track_id_idx ON track_embedded_image_link(track_embedded_image_id, track_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_embedded_image_link_is_preferred_track_id_track_embedded_image_id_idx ON track_embedded_image_link(is_preferred, track_id, track_embedded_image_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS image_directory_stem_idx ON image(directory_id, stem COLLATE NOCASE)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS image_id_idx ON image(id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS image_path_idx ON image(absolute_file_path)");
+1 -1
View File
@@ -25,8 +25,8 @@
#include "database/Session.hpp"
#include "database/User.hpp"
#include "IdTypeTraits.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
namespace lms::db
{
+1 -1
View File
@@ -25,8 +25,8 @@
#include "database/Session.hpp"
#include "database/User.hpp"
#include "IdTypeTraits.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
namespace lms::db
{
+1 -1
View File
@@ -25,8 +25,8 @@
#include "database/Track.hpp"
#include "database/User.hpp"
#include "IdTypeTraits.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
namespace lms::db
{
+22 -7
View File
@@ -29,16 +29,18 @@
#include "database/Release.hpp"
#include "database/Session.hpp"
#include "database/TrackArtistLink.hpp"
#include "database/TrackEmbeddedImage.hpp"
#include "database/TrackEmbeddedImageLink.hpp"
#include "database/TrackFeatures.hpp"
#include "database/TrackLyrics.hpp"
#include "database/User.hpp"
#include "IdTypeTraits.hpp"
#include "PartialDateTimeTraits.hpp"
#include "PathTraits.hpp"
#include "SqlQuery.hpp"
#include "StringViewTraits.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
#include "traits/PartialDateTimeTraits.hpp"
#include "traits/PathTraits.hpp"
#include "traits/StringViewTraits.hpp"
namespace lms::db
{
@@ -177,12 +179,15 @@ namespace lms::db
if (params.directory.isValid())
query.where("t.directory_id = ?").bind(params.directory);
if (params.hasEmbeddedImage.has_value())
query.where("t.has_cover = ?").bind(params.hasEmbeddedImage.value());
if (params.fileSize.has_value())
query.where("t.file_size = ?").bind(static_cast<long long>(params.fileSize.value()));
if (params.embeddedImageId.isValid())
{
query.join("track_embedded_image_link t_e_i_l ON t_e_i_l.track_id = t.id");
query.where("t_e_i_l.track_embedded_image_id = ?").bind(params.embeddedImageId);
}
switch (params.sortMethod)
{
case TrackSortMethod::None:
@@ -469,6 +474,16 @@ namespace lms::db
_trackLyrics.insert(getDboPtr(lyrics));
}
void Track::clearEmbeddedImageLinks()
{
_embeddedImageLinks.clear();
}
void Track::addEmbeddedImageLink(const ObjectPtr<TrackEmbeddedImageLink>& image)
{
_embeddedImageLinks.insert(getDboPtr(image));
}
std::optional<int> Track::getYear() const
{
return _date.getYear();
+1 -1
View File
@@ -23,8 +23,8 @@
#include "database/Session.hpp"
#include "database/Track.hpp"
#include "IdTypeTraits.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
namespace lms::db
{
+1 -1
View File
@@ -23,8 +23,8 @@
#include "database/Track.hpp"
#include "database/User.hpp"
#include "IdTypeTraits.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
namespace lms::db
{
@@ -0,0 +1,124 @@
/*
* Copyright (C) 2025 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 <http://www.gnu.org/licenses/>.
*/
#include "database/Directory.hpp"
#include "database/Session.hpp"
#include "database/Track.hpp"
#include "database/TrackEmbeddedImage.hpp"
#include "database/Types.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
#include "traits/ImageHashTypeTraits.hpp"
namespace lms::db
{
namespace
{
Wt::Dbo::Query<Wt::Dbo::ptr<TrackEmbeddedImage>> createQuery(Session& session, const TrackEmbeddedImage::FindParameters& params)
{
session.checkReadTransaction();
auto query{ session.getDboSession()->query<Wt::Dbo::ptr<TrackEmbeddedImage>>("SELECT t_e_i FROM track_embedded_image t_e_i") };
if (params.isPreferred
|| params.track.isValid()
|| params.release.isValid()
|| params.trackList.isValid()
|| params.sortMethod == TrackEmbeddedImageSortMethod::FrontCoverAndSize)
{
query.join("track_embedded_image_link t_e_i_l ON t_e_i_l.track_embedded_image_id = t_e_i.id");
if (params.isPreferred)
query.where("t_e_i_l.is_preferred = ?").bind(params.isPreferred.value());
if (params.track.isValid())
query.where("t_e_i_l.track_id = ?").bind(params.track);
if (params.release.isValid())
{
query.join("track t ON t_e_i_l.track_id = t.id");
query.where("t.release_id = ?").bind(params.release);
}
if (params.trackList.isValid())
{
query.join("tracklist_entry t_l_e ON t_l_e.track_id = t_e_i_l.track_id");
query.where("t_l_e.tracklist_id = ?").bind(params.trackList);
}
}
switch (params.sortMethod)
{
case TrackEmbeddedImageSortMethod::None:
break;
case TrackEmbeddedImageSortMethod::FrontCoverAndSize:
query.orderBy("CASE WHEN t_e_i_l.type = ? THEN 0 ELSE 1 END, t_e_i.size").bind(ImageType::FrontCover);
break;
}
return query;
}
} // namespace
TrackEmbeddedImage::pointer TrackEmbeddedImage::create(Session& session)
{
return session.getDboSession()->add(std::unique_ptr<TrackEmbeddedImage>{ new TrackEmbeddedImage{} });
}
std::size_t TrackEmbeddedImage::getCount(Session& session)
{
session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM track_embedded_image"));
}
TrackEmbeddedImage::pointer TrackEmbeddedImage::find(Session& session, TrackEmbeddedImageId id)
{
session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->find<TrackEmbeddedImage>().where("id = ?").bind(id));
}
void TrackEmbeddedImage::find(Session& session, const FindParameters& params, const std::function<void(const pointer&)>& func)
{
session.checkReadTransaction();
auto query{ createQuery(session, params) };
utils::forEachQueryRangeResult(query, params.range, func);
}
TrackEmbeddedImage::pointer TrackEmbeddedImage::find(Session& session, std::size_t size, ImageHashType hash)
{
session.checkReadTransaction();
auto query{ session.getDboSession()->find<TrackEmbeddedImage>().where("size = ?").bind(static_cast<int>(size)).where("hash = ?").bind(hash) };
return utils::fetchQuerySingleResult(query);
}
RangeResults<TrackEmbeddedImageId> TrackEmbeddedImage::findOrphanIds(Session& session, std::optional<Range> range)
{
session.checkReadTransaction();
auto query{ session.getDboSession()->query<TrackEmbeddedImageId>("SELECT t_e_i.id FROM track_embedded_image t_e_i LEFT JOIN track_embedded_image_link t_e_i_l ON t_e_i.id = t_e_i_l.track_embedded_image_id WHERE t_e_i_l.track_embedded_image_id IS NULL") };
return utils::execRangeQuery<TrackEmbeddedImageId>(query, range);
}
} // namespace lms::db
@@ -0,0 +1,66 @@
/*
* Copyright (C) 2025 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 <http://www.gnu.org/licenses/>.
*/
#include "database/TrackEmbeddedImageLink.hpp"
#include "database/Session.hpp"
#include "database/Track.hpp"
#include "database/TrackEmbeddedImage.hpp"
#include "database/Types.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
namespace lms::db
{
TrackEmbeddedImageLink::TrackEmbeddedImageLink(ObjectPtr<Track> track, ObjectPtr<TrackEmbeddedImage> image)
: _track{ getDboPtr(track) }
, _image{ getDboPtr(image) }
{
}
TrackEmbeddedImageLink::pointer TrackEmbeddedImageLink::create(Session& session, ObjectPtr<Track> track, ObjectPtr<TrackEmbeddedImage> image)
{
return session.getDboSession()->add(std::unique_ptr<TrackEmbeddedImageLink>{ new TrackEmbeddedImageLink{ track, image } });
}
std::size_t TrackEmbeddedImageLink::getCount(Session& session)
{
session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM track_embedded_image_link"));
}
TrackEmbeddedImageLink::pointer TrackEmbeddedImageLink::find(Session& session, TrackEmbeddedImageLinkId id)
{
session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->find<TrackEmbeddedImageLink>().where("id = ?").bind(id));
}
ObjectPtr<Track> TrackEmbeddedImageLink::getTrack() const
{
return _track;
}
ObjectPtr<TrackEmbeddedImage> TrackEmbeddedImageLink::getImage() const
{
return _image;
}
} // namespace lms::db
+1 -1
View File
@@ -27,8 +27,8 @@
#include "database/Session.hpp"
#include "database/Track.hpp"
#include "IdTypeTraits.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
namespace lms::db
{
+2 -2
View File
@@ -26,10 +26,10 @@
#include "database/Track.hpp"
#include "database/User.hpp"
#include "IdTypeTraits.hpp"
#include "SqlQuery.hpp"
#include "StringViewTraits.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
#include "traits/StringViewTraits.hpp"
namespace lms::db
{
+2 -2
View File
@@ -28,9 +28,9 @@
#include "database/Session.hpp"
#include "database/Track.hpp"
#include "IdTypeTraits.hpp"
#include "PathTraits.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
#include "traits/PathTraits.hpp"
namespace lms::db
{
+2 -3
View File
@@ -19,13 +19,12 @@
#include "database/UIState.hpp"
#include "core/ILogger.hpp"
#include "database/Session.hpp"
#include "database/User.hpp"
#include "IdTypeTraits.hpp"
#include "StringViewTraits.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
#include "traits/StringViewTraits.hpp"
namespace lms::db
{
+5 -3
View File
@@ -19,16 +19,18 @@
#include "database/User.hpp"
#include "core/ILogger.hpp"
#include <cassert>
#include "database/Artist.hpp"
#include "database/Release.hpp"
#include "database/Session.hpp"
#include "database/Track.hpp"
#include "database/UIState.hpp"
#include "IdTypeTraits.hpp"
#include "StringViewTraits.hpp"
#include "Utils.hpp"
#include "traits/EnumSetTraits.hpp"
#include "traits/IdTypeTraits.hpp"
#include "traits/StringViewTraits.hpp"
namespace lms::db
{
@@ -0,0 +1,62 @@
/*
* Copyright (C) 2025 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <Wt/Dbo/StdSqlTraits.h>
#include "core/String.hpp"
#include "database/Types.hpp"
namespace Wt::Dbo
{
template<>
struct sql_value_traits<lms::db::ImageHashType>
{
static const bool specialized = true;
// Uses an underlying string to encode this big value
static std::string type(SqlConnection* conn, int size)
{
return sql_value_traits<std::string, void>::type(conn, size);
}
static void bind(const lms::db::ImageHashType& v, SqlStatement* statement, int column, int size)
{
std::string valueAsStr{ std::to_string(v.value()) };
sql_value_traits<std::string>::bind(valueAsStr, statement, column, size);
}
static bool read(lms::db::ImageHashType& v, SqlStatement* statement, int column, int size)
{
std::string valueAsStr;
if (sql_value_traits<std::string>::read(valueAsStr, statement, column, size))
{
if (const auto parsedValue{ lms::core::stringUtils::readAs<lms::db::ImageHashType::underlying_type>(valueAsStr) })
{
v = lms::db::ImageHashType{ *parsedValue };
return true;
}
}
v = lms::db::ImageHashType{};
return false;
}
};
} // namespace Wt::Dbo
@@ -0,0 +1,108 @@
/*
* Copyright (C) 2025 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <filesystem>
#include <optional>
#include <string>
#include <string_view>
#include <Wt/Dbo/Dbo.h>
#include <Wt/WDateTime.h>
#include "database/ArtistId.hpp"
#include "database/ArtistInfoId.hpp"
#include "database/DirectoryId.hpp"
#include "database/Object.hpp"
#include "database/Types.hpp"
namespace lms::db
{
class Artist;
class Directory;
class Session;
class ArtistInfo final : public Object<ArtistInfo, ArtistInfoId>
{
public:
ArtistInfo() = default;
// find
static std::size_t getCount(Session& session);
static pointer find(Session& session, ArtistInfoId id);
static void find(Session& session, ArtistId id, std::optional<Range> range, const std::function<void(const pointer&)>& func);
static void find(Session& session, ArtistId id, const std::function<void(const pointer&)>& func);
static pointer find(Session& session, const std::filesystem::path& path);
static void find(Session& session, ArtistInfoId& lastRetrievedId, std::size_t count, const std::function<void(const pointer&)>& func);
// getters
const std::filesystem::path& getAbsoluteFilePath() const { return _absoluteFilePath; }
const Wt::WDateTime& getLastWriteTime() const { return _fileLastWrite; }
ObjectPtr<Directory> getDirectory() const;
ObjectPtr<Artist> getArtist() const;
DirectoryId getDirectoryId() const { return _directory.id(); }
std::string_view getType() const { return _type; }
std::string_view getGender() const { return _gender; }
std::string_view getDisambiguation() const { return _disambiguation; }
std::string_view getBiography() const { return _biography; }
// setters
void setAbsoluteFilePath(const std::filesystem::path& filePath);
void setLastWriteTime(Wt::WDateTime time) { _fileLastWrite = time; }
void setDirectory(ObjectPtr<Directory> directory);
void setArtist(ObjectPtr<Artist> artist);
void setType(std::string_view type) { _type = type; }
void setGender(std::string_view gender) { _gender = gender; }
void setDisambiguation(std::string_view disambiguation) { _disambiguation = disambiguation; }
void setBiography(std::string_view biography) { _biography = biography; };
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _absoluteFilePath, "absolute_file_path");
Wt::Dbo::field(a, _fileLastWrite, "file_last_write");
Wt::Dbo::field(a, _type, "type");
Wt::Dbo::field(a, _gender, "gender");
Wt::Dbo::field(a, _disambiguation, "disambiguation");
Wt::Dbo::field(a, _biography, "biography");
Wt::Dbo::belongsTo(a, _directory, "directory", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade);
}
private:
friend class Session;
static pointer create(Session& session);
// Set when coming from artist info file
std::filesystem::path _absoluteFilePath;
std::string _fileStem;
Wt::WDateTime _fileLastWrite;
std::string _type;
std::string _gender;
std::string _disambiguation;
std::string _biography;
Wt::Dbo::ptr<Directory> _directory;
Wt::Dbo::ptr<Artist> _artist;
};
} // namespace lms::db
@@ -0,0 +1,24 @@
/*
* Copyright (C) 2021 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "database/IdType.hpp"
LMS_DECLARE_IDTYPE(ArtistInfoId)
+12 -10
View File
@@ -41,6 +41,7 @@
#include "database/MediaLibraryId.hpp"
#include "database/Object.hpp"
#include "database/ReleaseId.hpp"
#include "database/TrackEmbeddedImageId.hpp"
#include "database/TrackId.hpp"
#include "database/TrackListId.hpp"
#include "database/Types.hpp"
@@ -52,6 +53,7 @@ namespace lms::db
class Cluster;
class ClusterType;
class Directory;
class TrackEmbeddedImageLink;
class MediaLibrary;
class Release;
class Session;
@@ -85,8 +87,8 @@ namespace lms::db
std::optional<int> trackNumber; // matching this track number
std::optional<int> discNumber; // matching this disc number
DirectoryId directory; // if set, tracks in this directory
std::optional<bool> hasEmbeddedImage; // if set, tracks that have or not embedded images
std::optional<std::size_t> fileSize; // if set, tracks that match this file size
TrackEmbeddedImageId embeddedImageId; // if set, tracks that have this embedded image
FindParameters& setFilters(const Filters& _filters)
{
@@ -182,16 +184,16 @@ namespace lms::db
directory = _directory;
return *this;
}
FindParameters& setHasEmbeddedImage(std::optional<bool> _hasEmbeddedImage)
{
hasEmbeddedImage = _hasEmbeddedImage;
return *this;
}
FindParameters& setFileSize(std::optional<std::size_t> _fileSize)
{
fileSize = _fileSize;
return *this;
}
FindParameters& setEmbeddedImage(TrackEmbeddedImageId _embeddedImageId)
{
embeddedImageId = _embeddedImageId;
return *this;
}
};
Track() = default;
@@ -232,7 +234,6 @@ namespace lms::db
void setSampleRate(std::size_t channelCount) { _sampleRate = channelCount; }
void setDate(const core::PartialDateTime& date) { _date = date; }
void setOriginalDate(const core::PartialDateTime& date) { _originalDate = date; }
void setHasCover(bool hasCover) { _hasCover = hasCover; }
void setTrackMBID(const std::optional<core::UUID>& MBID) { _trackMBID = MBID ? MBID->getAsString() : ""; }
void setRecordingMBID(const std::optional<core::UUID>& MBID) { _recordingMBID = MBID ? MBID->getAsString() : ""; }
void setCopyright(std::string_view copyright);
@@ -249,6 +250,8 @@ namespace lms::db
void clearLyrics();
void clearEmbeddedLyrics();
void addLyrics(const ObjectPtr<TrackLyrics>& lyrics);
void clearEmbeddedImageLinks();
void addEmbeddedImageLink(const ObjectPtr<TrackEmbeddedImageLink>& link);
void setMediaLibrary(ObjectPtr<MediaLibrary> mediaLibrary) { _mediaLibrary = getDboPtr(mediaLibrary); }
void setDirectory(ObjectPtr<Directory> directory) { _directory = getDboPtr(directory); }
@@ -273,7 +276,6 @@ namespace lms::db
std::optional<int> getOriginalYear() const;
const Wt::WDateTime& getLastWriteTime() const { return _fileLastWrite; }
const Wt::WDateTime& getAddedTime() const { return _fileAdded; }
bool hasCover() const { return _hasCover; }
bool hasLyrics() const;
std::optional<core::UUID> getTrackMBID() const { return core::UUID::fromString(_trackMBID); }
std::optional<core::UUID> getRecordingMBID() const { return core::UUID::fromString(_recordingMBID); }
@@ -321,7 +323,6 @@ namespace lms::db
Wt::Dbo::field(a, _fileSize, "file_size");
Wt::Dbo::field(a, _fileLastWrite, "file_last_write");
Wt::Dbo::field(a, _fileAdded, "file_added");
Wt::Dbo::field(a, _hasCover, "has_cover");
Wt::Dbo::field(a, _trackMBID, "mbid");
Wt::Dbo::field(a, _recordingMBID, "recording_mbid");
Wt::Dbo::field(a, _copyright, "copyright");
@@ -338,6 +339,7 @@ namespace lms::db
Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "track");
Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::hasMany(a, _trackLyrics, Wt::Dbo::ManyToOne, "track");
Wt::Dbo::hasMany(a, _embeddedImageLinks, Wt::Dbo::ManyToOne, "track");
}
private:
@@ -368,7 +370,6 @@ namespace lms::db
long long _fileSize{};
Wt::WDateTime _fileLastWrite;
Wt::WDateTime _fileAdded;
bool _hasCover{};
std::string _trackMBID;
std::string _recordingMBID;
std::string _copyright;
@@ -384,6 +385,7 @@ namespace lms::db
Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks;
Wt::Dbo::collection<Wt::Dbo::ptr<Cluster>> _clusters;
Wt::Dbo::collection<Wt::Dbo::ptr<TrackLyrics>> _trackLyrics;
Wt::Dbo::collection<Wt::Dbo::ptr<TrackEmbeddedImageLink>> _embeddedImageLinks;
};
namespace Debug
@@ -0,0 +1,133 @@
/*
* Copyright (C) 2024 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <optional>
#include <string>
#include <string_view>
#include <Wt/Dbo/Dbo.h>
#include "database/Object.hpp"
#include "database/ReleaseId.hpp"
#include "database/TrackEmbeddedImageId.hpp"
#include "database/TrackId.hpp"
#include "database/TrackListId.hpp"
#include "database/Types.hpp"
namespace lms::db
{
class TrackEmbeddedImageLink;
class Session;
class Track;
class TrackEmbeddedImage final : public Object<TrackEmbeddedImage, TrackEmbeddedImageId>
{
public:
TrackEmbeddedImage() = default;
struct FindParameters
{
std::optional<Range> range;
TrackId track;
ReleaseId release;
TrackListId trackList;
std::optional<bool> isPreferred;
TrackEmbeddedImageSortMethod sortMethod{ TrackEmbeddedImageSortMethod::None };
FindParameters& setRange(std::optional<Range> _range)
{
range = _range;
return *this;
}
FindParameters& setTrack(TrackId _track)
{
track = _track;
return *this;
}
FindParameters& setRelease(ReleaseId _release)
{
release = _release;
return *this;
}
FindParameters& setTrackList(TrackListId _trackList)
{
trackList = _trackList;
return *this;
}
FindParameters& setIsPreferred(std::optional<bool> _isPreferred)
{
isPreferred = _isPreferred;
return *this;
}
FindParameters& setSortMethod(TrackEmbeddedImageSortMethod _sortMethod)
{
sortMethod = _sortMethod;
return *this;
}
};
// find
static std::size_t getCount(Session& session);
static pointer find(Session& session, TrackEmbeddedImageId id);
static void find(Session& session, const FindParameters& params, const std::function<void(const pointer&)>& func);
static pointer find(Session& session, std::size_t size, ImageHashType hash);
static RangeResults<TrackEmbeddedImageId> findOrphanIds(Session& session, std::optional<Range> range);
// getters
ImageHashType getHash() const { return _hash; }
std::size_t getSize() const { return _size; }
std::size_t getWidth() const { return _width; }
std::size_t getHeight() const { return _height; }
std::string_view getMimeType() const { return _mimeType; }
// setters
void setHash(ImageHashType hash) { _hash = hash; }
void setSize(std::size_t size) { _size = static_cast<int>(size); }
void setWidth(std::size_t width) { _width = static_cast<int>(width); }
void setHeight(std::size_t height) { _height = static_cast<int>(height); }
void setMimeType(std::string_view mimeType) { _mimeType = mimeType; }
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _hash, "hash");
Wt::Dbo::field(a, _size, "size");
Wt::Dbo::field(a, _width, "width");
Wt::Dbo::field(a, _height, "height");
Wt::Dbo::field(a, _mimeType, "mime_type");
Wt::Dbo::hasMany(a, _links, Wt::Dbo::ManyToOne, "track_embedded_image");
}
private:
friend class Session;
static pointer create(Session& session);
ImageHashType _hash;
int _size{};
int _width{};
int _height{};
std::string _mimeType;
Wt::Dbo::collection<Wt::Dbo::ptr<TrackEmbeddedImageLink>> _links;
};
} // namespace lms::db
@@ -0,0 +1,24 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "database/IdType.hpp"
LMS_DECLARE_IDTYPE(TrackEmbeddedImageId)
@@ -0,0 +1,88 @@
/*
* Copyright (C) 2024 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <string>
#include <string_view>
#include <Wt/Dbo/Dbo.h>
#include "database/Object.hpp"
#include "database/TrackEmbeddedImageLinkId.hpp"
#include "database/Types.hpp"
namespace lms::db
{
class TrackEmbeddedImage;
class Session;
class Track;
class TrackEmbeddedImageLink final : public Object<TrackEmbeddedImageLink, TrackEmbeddedImageLinkId>
{
public:
TrackEmbeddedImageLink() = default;
using HashType = std::uint64_t;
// find
static std::size_t getCount(Session& session);
static pointer find(Session& session, TrackEmbeddedImageLinkId id);
// getters
ObjectPtr<Track> getTrack() const;
ObjectPtr<TrackEmbeddedImage> getImage() const;
std::size_t getIndex() const { return _index; }
bool isPreferred() const { return _isPreferred; }
ImageType getType() const { return _type; }
std::string_view getDescription() const { return _description; }
// setters
void setIndex(std::size_t index) { _index = static_cast<int>(index); }
void setIsPreferred(bool isPreferred) { _isPreferred = isPreferred; }
void setType(ImageType type) { _type = type; }
void setDescription(std::string_view description) { _description = description; }
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _index, "index");
Wt::Dbo::field(a, _isPreferred, "is_preferred");
Wt::Dbo::field(a, _type, "type");
Wt::Dbo::field(a, _description, "description");
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::belongsTo(a, _image, "track_embedded_image", Wt::Dbo::OnDeleteCascade);
}
private:
TrackEmbeddedImageLink(ObjectPtr<Track> track, ObjectPtr<TrackEmbeddedImage> image);
friend class Session;
static pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<TrackEmbeddedImage> image);
int _index{}; // index within the track
bool _isPreferred{};
ImageType _type{ ImageType::Unknown };
std::string _description;
Wt::Dbo::ptr<Track> _track;
Wt::Dbo::ptr<TrackEmbeddedImage> _image;
};
} // namespace lms::db
@@ -0,0 +1,24 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "database/IdType.hpp"
LMS_DECLARE_IDTYPE(TrackEmbeddedImageLinkId)
@@ -26,6 +26,7 @@
#include <Wt/WDate.h>
#include "core/Exception.hpp"
#include "core/TaggedType.hpp"
namespace lms::db
{
@@ -136,6 +137,8 @@ namespace lms::db
Name,
};
using ImageHashType = core::TaggedType<class ImageHash, std::uint64_t>;
enum class LabelSortMethod
{
None,
@@ -165,6 +168,12 @@ namespace lms::db
Name,
};
enum class TrackEmbeddedImageSortMethod
{
None,
FrontCoverAndSize,
};
enum class TrackListSortMethod
{
None,
@@ -194,6 +203,32 @@ namespace lms::db
EmbeddedFirst,
};
enum class ImageType
{
Unknown = 0,
Other = 1,
FileIcon = 2,
OtherFileIcon = 3,
FrontCover = 4,
BackCover = 5,
LeafletPage = 6,
Media = 7,
LeadArtist = 8,
Artist = 9,
Conductor = 10,
Band = 11,
Composer = 12,
Lyricist = 13,
RecordingLocation = 14,
DuringRecording = 15,
DuringPerformance = 16,
MovieScreenCapture = 17,
ColouredFish = 18,
Illustration = 19,
BandLogo = 20,
PublisherLogo = 21
};
enum class TrackArtistLinkType
{
Artist = 0, // regular track artist
+9 -1
View File
@@ -21,11 +21,11 @@
#include <optional>
#include <string_view>
#include <vector>
#include <Wt/Dbo/Dbo.h>
#include <Wt/WDateTime.h>
#include "core/EnumSet.hpp"
#include "core/UUID.hpp"
#include "database/Object.hpp"
#include "database/Types.hpp"
@@ -110,6 +110,8 @@ namespace lms::db
void setSubsonicDefaultTranscodingOutputBitrate(Bitrate bitrate);
void setUITheme(UITheme uiTheme) { _uiTheme = uiTheme; }
void setUIArtistReleaseSortMethod(ReleaseSortMethod method) { _uiArtistReleaseSortMethod = method; }
void setUIEnableInlineArtistRelationships(bool enable) { _uiEnableInlineArtistRelationships = enable; }
void setUIInlineArtistRelationships(core::EnumSet<TrackArtistLinkType> types) { _uiInlineArtistRelationships = types; }
void setSubsonicArtistListMode(SubsonicArtistListMode mode) { _subsonicArtistListMode = mode; }
void setFeedbackBackend(FeedbackBackend feedbackBackend) { _feedbackBackend = feedbackBackend; }
void setScrobblingBackend(ScrobblingBackend scrobblingBackend) { _scrobblingBackend = scrobblingBackend; }
@@ -124,6 +126,8 @@ namespace lms::db
Bitrate getSubsonicDefaultTranscodingOutputBitrate() const { return _subsonicDefaultTranscodingOutputBitrate; }
UITheme getUITheme() const { return _uiTheme; }
ReleaseSortMethod getUIArtistReleaseSortMethod() const { return _uiArtistReleaseSortMethod; }
bool getUIEnableInlineArtistRelationships() const { return _uiEnableInlineArtistRelationships; }
core::EnumSet<TrackArtistLinkType> getUIInlineArtistRelationships() const { return _uiInlineArtistRelationships; }
SubsonicArtistListMode getSubsonicArtistListMode() const { return _subsonicArtistListMode; }
FeedbackBackend getFeedbackBackend() const { return _feedbackBackend; }
ScrobblingBackend getScrobblingBackend() const { return _scrobblingBackend; }
@@ -144,6 +148,8 @@ namespace lms::db
Wt::Dbo::field(a, _subsonicArtistListMode, "subsonic_artist_list_mode");
Wt::Dbo::field(a, _uiTheme, "ui_theme");
Wt::Dbo::field(a, _uiArtistReleaseSortMethod, "ui_artist_release_sort_method");
Wt::Dbo::field(a, _uiEnableInlineArtistRelationships, "ui_enable_inline_artist_relationships");
Wt::Dbo::field(a, _uiInlineArtistRelationships, "ui_inline_artist_relationships");
Wt::Dbo::field(a, _feedbackBackend, "feedback_backend");
Wt::Dbo::field(a, _scrobblingBackend, "scrobbling_backend");
Wt::Dbo::field(a, _listenbrainzToken, "listenbrainz_token");
@@ -164,6 +170,8 @@ namespace lms::db
Wt::WDateTime _lastLogin;
UITheme _uiTheme{ defaultUITheme };
ReleaseSortMethod _uiArtistReleaseSortMethod{ _defaultUIArtistReleaseSortMethod };
bool _uiEnableInlineArtistRelationships{};
core::EnumSet<TrackArtistLinkType> _uiInlineArtistRelationships{ TrackArtistLinkType::Composer, TrackArtistLinkType::Performer };
FeedbackBackend _feedbackBackend{ defaultFeedbackBackend };
ScrobblingBackend _scrobblingBackend{ defaultScrobblingBackend };
std::string _listenbrainzToken; // Musicbrainz Identifier
+2 -2
View File
@@ -768,8 +768,8 @@ namespace lms::db::tests
auto transaction{ session.createReadTransaction() };
const auto artists{ Artist::findIds(session, Artist::FindParameters{}.setSortMethod(ArtistSortMethod::AddedDesc)) };
ASSERT_EQ(artists.results.size(), 4);
EXPECT_EQ(artists.results[0], artistA.getId());
EXPECT_EQ(artists.results[1], artistD.getId());
EXPECT_EQ(artists.results[0], artistD.getId());
EXPECT_EQ(artists.results[1], artistA.getId());
EXPECT_EQ(artists.results[2], artistB.getId());
EXPECT_EQ(artists.results[3], artistC.getId());
}
+112
View File
@@ -0,0 +1,112 @@
/*
* Copyright (C) 2024 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 <http://www.gnu.org/licenses/>.
*/
#include "Common.hpp"
#include "database/Artist.hpp"
#include "database/ArtistInfo.hpp"
#include "database/Directory.hpp"
namespace lms::db::tests
{
using ScopedArtistInfo = ScopedEntity<db::ArtistInfo>;
using ScopedDirectory = ScopedEntity<db::Directory>;
TEST_F(DatabaseFixture, ArtistInfo)
{
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(ArtistInfo::getCount(session), 0);
}
ScopedArtistInfo artistInfo{ session };
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(ArtistInfo::getCount(session), 1);
}
{
auto transaction{ session.createReadTransaction() };
const ArtistInfo::pointer dbArtistInfo{ ArtistInfo::find(session, artistInfo.getId()) };
EXPECT_EQ(dbArtistInfo->getAbsoluteFilePath(), "");
EXPECT_EQ(dbArtistInfo->getLastWriteTime(), Wt::WDateTime{});
EXPECT_EQ(dbArtistInfo->getArtist(), Artist::pointer{});
EXPECT_EQ(dbArtistInfo->getDirectory(), Directory::pointer{});
EXPECT_EQ(dbArtistInfo->getType(), "");
EXPECT_EQ(dbArtistInfo->getGender(), "");
EXPECT_EQ(dbArtistInfo->getDisambiguation(), "");
EXPECT_EQ(dbArtistInfo->getBiography(), "");
}
ScopedArtist artist{ session, "MyArtist" };
ScopedDirectory directory{ session, "/tmp" };
const Wt::WDateTime dateTime{ Wt::WDate{ 2024, 30, 1 }, Wt::WTime{ 12, 58, 29 } };
// Now change some values
{
auto transaction{ session.createWriteTransaction() };
ArtistInfo::pointer dbArtistInfo{ ArtistInfo::find(session, artistInfo.getId()) };
dbArtistInfo.modify()->setAbsoluteFilePath("/tmp/artist.nfo");
dbArtistInfo.modify()->setLastWriteTime(dateTime);
dbArtistInfo.modify()->setArtist(artist.get());
dbArtistInfo.modify()->setDirectory(directory.get());
dbArtistInfo.modify()->setType("MyType");
dbArtistInfo.modify()->setGender("MyGender");
dbArtistInfo.modify()->setDisambiguation("MyDisambiguation");
dbArtistInfo.modify()->setBiography("MyBiography");
}
// Check values are reflected
{
auto transaction{ session.createReadTransaction() };
const ArtistInfo::pointer dbArtistInfo{ ArtistInfo::find(session, artistInfo.getId()) };
EXPECT_EQ(dbArtistInfo->getAbsoluteFilePath(), "/tmp/artist.nfo");
EXPECT_EQ(dbArtistInfo->getLastWriteTime(), dateTime);
EXPECT_EQ(dbArtistInfo->getArtist(), artist.get());
EXPECT_EQ(dbArtistInfo->getDirectory(), directory.get());
EXPECT_EQ(dbArtistInfo->getDirectoryId(), directory.getId());
EXPECT_EQ(dbArtistInfo->getType(), "MyType");
EXPECT_EQ(dbArtistInfo->getGender(), "MyGender");
EXPECT_EQ(dbArtistInfo->getDisambiguation(), "MyDisambiguation");
EXPECT_EQ(dbArtistInfo->getBiography(), "MyBiography");
}
{
auto transaction{ session.createReadTransaction() };
bool visited{};
ArtistInfo::find(session, artist.getId(), [&](const ArtistInfo::pointer& dbArtistInfo) {
ASSERT_NE(dbArtistInfo, ArtistInfo::pointer{});
EXPECT_EQ(dbArtistInfo->getId(), artistInfo.getId());
visited = true;
});
EXPECT_TRUE(visited);
}
}
} // namespace lms::db::tests
+3 -1
View File
@@ -1,7 +1,8 @@
add_executable(test-database
AuthToken.cpp
Artist.cpp
ArtistInfo.cpp
AuthToken.cpp
Cluster.cpp
Common.cpp
DatabaseTest.cpp
@@ -19,6 +20,7 @@ add_executable(test-database
StarredTrack.cpp
Track.cpp
TrackBookmark.cpp
TrackEmbeddedImage.cpp
TrackFeatures.cpp
TrackList.cpp
TrackLyrics.cpp
+7
View File
@@ -20,6 +20,8 @@
#include "Common.hpp"
#include "core/String.hpp"
#include "database/Artist.hpp"
#include "database/ArtistInfo.hpp"
#include "database/AuthToken.hpp"
#include "database/Db.hpp"
#include "database/Directory.hpp"
@@ -32,6 +34,8 @@
#include "database/StarredArtist.hpp"
#include "database/StarredRelease.hpp"
#include "database/StarredTrack.hpp"
#include "database/TrackEmbeddedImage.hpp"
#include "database/TrackEmbeddedImageLink.hpp"
#include "database/TrackLyrics.hpp"
#include "database/UIState.hpp"
#include "database/User.hpp"
@@ -339,11 +343,14 @@ VALUES
auto transaction{ session.createReadTransaction() };
EXPECT_FALSE(Artist::find(session, ArtistId{}));
EXPECT_FALSE(ArtistInfo::find(session, ArtistInfoId{}));
EXPECT_FALSE(AuthToken::find(session, AuthTokenId{}));
EXPECT_FALSE(Country::find(session, CountryId{}));
EXPECT_FALSE(Cluster::find(session, ClusterId{}));
EXPECT_FALSE(ClusterType::find(session, ClusterTypeId{}));
EXPECT_FALSE(Directory::find(session, DirectoryId{}));
EXPECT_FALSE(TrackEmbeddedImage::find(session, TrackEmbeddedImageId{}));
EXPECT_FALSE(TrackEmbeddedImageLink::find(session, TrackEmbeddedImageLinkId{}));
EXPECT_FALSE(Image::find(session, ImageId{}));
EXPECT_FALSE(Label::find(session, LabelId{}));
EXPECT_FALSE(Listen::find(session, ListenId{}));
@@ -0,0 +1,283 @@
/*
* Copyright (C) 2024 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 <http://www.gnu.org/licenses/>.
*/
#include <limits>
#include "database/TrackEmbeddedImage.hpp"
#include "database/TrackEmbeddedImageLink.hpp"
#include "database/Types.hpp"
#include "Common.hpp"
namespace lms::db::tests
{
using ScopedTrackEmbeddedImage = ScopedEntity<db::TrackEmbeddedImage>;
using ScopedTrackEmbeddedImageLink = ScopedEntity<db::TrackEmbeddedImageLink>;
TEST_F(DatabaseFixture, TrackEmbeddedImage)
{
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(TrackEmbeddedImage::getCount(session), 0);
}
ScopedTrackEmbeddedImage image{ session };
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(TrackEmbeddedImage::getCount(session), 1);
const TrackEmbeddedImage::pointer img{ TrackEmbeddedImage::find(session, image.getId()) };
ASSERT_NE(img, TrackEmbeddedImage::pointer{});
EXPECT_EQ(img->getHash(), db::ImageHashType{});
EXPECT_EQ(img->getSize(), 0);
EXPECT_EQ(img->getWidth(), 0);
EXPECT_EQ(img->getHeight(), 0);
EXPECT_EQ(img->getMimeType(), "");
}
{
auto transaction{ session.createWriteTransaction() };
TrackEmbeddedImage::pointer img{ TrackEmbeddedImage::find(session, image.getId()) };
ASSERT_NE(img, TrackEmbeddedImage::pointer{});
img.modify()->setHash(db::ImageHashType{ std::numeric_limits<std::uint64_t>::max() });
img.modify()->setSize(1024 * 1024);
img.modify()->setWidth(640);
img.modify()->setHeight(480);
img.modify()->setMimeType("image/jpeg");
}
{
auto transaction{ session.createReadTransaction() };
const TrackEmbeddedImage::pointer img{ TrackEmbeddedImage::find(session, image.getId()) };
ASSERT_NE(img, TrackEmbeddedImage::pointer{});
EXPECT_EQ(img->getHash(), db::ImageHashType{ std::numeric_limits<std::uint64_t>::max() });
EXPECT_EQ(img->getSize(), 1024 * 1024);
EXPECT_EQ(img->getWidth(), 640);
EXPECT_EQ(img->getHeight(), 480);
EXPECT_EQ(img->getMimeType(), "image/jpeg");
}
}
TEST_F(DatabaseFixture, TrackEmbeddedImage_findByHash)
{
ScopedTrackEmbeddedImage image{ session };
constexpr std::size_t size{ 1024 };
constexpr db::ImageHashType hash{ 42 };
{
auto transaction{ session.createWriteTransaction() };
TrackEmbeddedImage::pointer img{ TrackEmbeddedImage::find(session, image.getId()) };
ASSERT_NE(img, TrackEmbeddedImage::pointer{});
img.modify()->setHash(hash);
img.modify()->setSize(size);
}
{
auto transaction{ session.createReadTransaction() };
const TrackEmbeddedImage::pointer img{ TrackEmbeddedImage::find(session, size, hash) };
ASSERT_NE(img, TrackEmbeddedImage::pointer{});
EXPECT_EQ(image.getId(), img->getId());
}
{
auto transaction{ session.createReadTransaction() };
const TrackEmbeddedImage::pointer img{ TrackEmbeddedImage::find(session, size + 1, hash) };
EXPECT_EQ(img, TrackEmbeddedImage::pointer{});
}
}
TEST_F(DatabaseFixture, TrackEmbeddedImage_findByParams)
{
ScopedTrackEmbeddedImage image{ session };
ScopedTrack track{ session };
ScopedRelease release{ session, "MyRelease" };
ScopedTrackEmbeddedImageLink link{ session, track.lockAndGet(), image.lockAndGet() };
{
auto transaction{ session.createReadTransaction() };
TrackEmbeddedImage::FindParameters params;
params.setIsPreferred(true);
bool visited{};
TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; });
EXPECT_FALSE(visited);
}
{
auto transaction{ session.createWriteTransaction() };
link.get().modify()->setIsPreferred(true);
}
{
auto transaction{ session.createReadTransaction() };
TrackEmbeddedImage::FindParameters params;
params.setIsPreferred(true);
bool visited{};
TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; });
EXPECT_TRUE(visited);
}
{
auto transaction{ session.createReadTransaction() };
TrackEmbeddedImage::FindParameters params;
params.setIsPreferred(true);
params.setRelease(release.getId());
bool visited{};
TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; });
EXPECT_FALSE(visited);
}
{
auto transaction{ session.createWriteTransaction() };
track.get().modify()->setRelease(release.get());
}
{
auto transaction{ session.createReadTransaction() };
TrackEmbeddedImage::FindParameters params;
params.setIsPreferred(true);
params.setRelease(release.getId());
params.setSortMethod(TrackEmbeddedImageSortMethod::FrontCoverAndSize);
bool visited{};
TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; });
EXPECT_TRUE(visited);
}
{
auto transaction{ session.createReadTransaction() };
TrackEmbeddedImage::FindParameters params;
params.setIsPreferred(true);
params.setTrack(track.getId());
params.setSortMethod(TrackEmbeddedImageSortMethod::FrontCoverAndSize);
bool visited{};
TrackEmbeddedImage::find(session, params, [&](const auto&) { visited = true; });
EXPECT_TRUE(visited);
}
}
TEST_F(DatabaseFixture, Track_findByEmbeddedImage)
{
ScopedTrackEmbeddedImage image{ session };
ScopedTrack track{ session };
ScopedTrackEmbeddedImageLink link{ session, track.lockAndGet(), image.lockAndGet() };
{
auto transaction{ session.createReadTransaction() };
Track::FindParameters params;
params.setEmbeddedImage(image->getId());
bool visited{};
Track::find(session, params, [&](const auto&) { visited = true; });
EXPECT_TRUE(visited);
}
}
TEST_F(DatabaseFixture, TrackEmbeddedImage_findOrphans)
{
ScopedTrackEmbeddedImage image{ session };
{
auto transaction{ session.createReadTransaction() };
auto orphans{ TrackEmbeddedImage::findOrphanIds(session, std::nullopt) };
ASSERT_EQ(orphans.results.size(), 1);
EXPECT_EQ(orphans.results[0], image.getId());
}
{
ScopedTrack track{ session };
ScopedTrackEmbeddedImageLink link{ session, track.lockAndGet(), image.lockAndGet() };
{
auto transaction{ session.createReadTransaction() };
auto orphans{ TrackEmbeddedImage::findOrphanIds(session, std::nullopt) };
ASSERT_EQ(orphans.results.size(), 0);
}
}
{
auto transaction{ session.createReadTransaction() };
auto orphans{ TrackEmbeddedImage::findOrphanIds(session, std::nullopt) };
ASSERT_EQ(orphans.results.size(), 1);
EXPECT_EQ(orphans.results[0], image.getId());
}
}
TEST_F(DatabaseFixture, TrackEmbeddedImageLink)
{
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(TrackEmbeddedImage::getCount(session), 0);
}
ScopedTrack track{ session };
ScopedTrackEmbeddedImage image{ session };
ScopedTrackEmbeddedImageLink imageLink{ session, track.lockAndGet(), image.lockAndGet() };
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(TrackEmbeddedImageLink::getCount(session), 1);
const TrackEmbeddedImageLink::pointer link{ TrackEmbeddedImageLink::find(session, imageLink.getId()) };
ASSERT_NE(link, TrackEmbeddedImageLink::pointer{});
EXPECT_EQ(link->getIndex(), 0);
EXPECT_EQ(link->getType(), ImageType::Unknown);
EXPECT_EQ(link->getDescription(), "");
EXPECT_EQ(link->getTrack(), track.get());
EXPECT_EQ(link->getImage(), image.get());
}
{
auto transaction{ session.createWriteTransaction() };
TrackEmbeddedImageLink::pointer link{ TrackEmbeddedImageLink::find(session, imageLink.getId()) };
ASSERT_NE(link, TrackEmbeddedImage::pointer{});
link.modify()->setIndex(2);
link.modify()->setType(ImageType::FrontCover);
link.modify()->setDescription("MyDesc");
}
{
auto transaction{ session.createReadTransaction() };
const TrackEmbeddedImageLink::pointer img{ TrackEmbeddedImageLink::find(session, imageLink.getId()) };
ASSERT_NE(img, TrackEmbeddedImage::pointer{});
EXPECT_EQ(img->getIndex(), 2);
EXPECT_EQ(img->getType(), ImageType::FrontCover);
EXPECT_EQ(img->getDescription(), "MyDesc");
}
}
} // namespace lms::db::tests
@@ -77,6 +77,29 @@ namespace lms::image
}
}
ImageProperties probeImage(std::span<const std::byte> encodedData)
{
LMS_SCOPED_TRACE_DETAILED("Image", "ProbeBuffer");
try
{
Magick::Image image;
Magick::Blob blob{ encodedData.data(), encodedData.size() };
image.ping(blob);
ImageProperties properties;
properties.width = image.size().width();
properties.height = image.size().height();
return properties;
}
catch (Magick::Exception& e)
{
LMS_LOG(COVER, ERROR, "Caught Magick exception: " << e.what());
throw Exception{ std::string{ "Magick probe error: " } + e.what() };
}
}
std::unique_ptr<IRawImage> decodeImage(std::span<const std::byte> encodedData)
{
LMS_SCOPED_TRACE_DETAILED("Image", "DecodeBuffer");
+18
View File
@@ -60,6 +60,24 @@ namespace lms::image
return properties;
}
ImageProperties probeImage(std::span<const std::byte> encodedData)
{
LMS_SCOPED_TRACE_DETAILED("Image", "ProbeBuffer");
int x{};
int y{};
int comp{};
if (::stbi_info_from_memory(reinterpret_cast<const stbi_uc*>(encodedData.data()), static_cast<int>(encodedData.size()), &x, &y, &comp) == 0)
throw StbiException{ "Probe failed" };
ImageProperties properties;
properties.width = x;
properties.height = y;
return properties;
}
std::unique_ptr<IRawImage> decodeImage(std::span<const std::byte> encodedData)
{
LMS_SCOPED_TRACE_DETAILED("Image", "DecodeBuffer");
+1
View File
@@ -33,6 +33,7 @@ namespace lms::image
// All these methods may throw Exception
ImageProperties probeImage(const std::filesystem::path& path);
ImageProperties probeImage(std::span<const std::byte> encodedData);
std::unique_ptr<IRawImage> decodeImage(std::span<const std::byte> encodedData);
std::unique_ptr<IRawImage> decodeImage(const std::filesystem::path& path);
+4 -1
View File
@@ -9,10 +9,13 @@ if (BUILD_BENCHMARKS)
endif()
add_library(lmsmetadata STATIC
impl/ArtistInfo.cpp
impl/AudioFileParser.cpp
impl/AvFormatImageReader.cpp
impl/AvFormatTagReader.cpp
impl/Lyrics.cpp
impl/Parser.cpp
impl/PlayList.cpp
impl/TagLibImageReader.cpp
impl/TagLibTagReader.cpp
impl/Utils.cpp
)
+62
View File
@@ -0,0 +1,62 @@
/*
* Copyright (C) 2025 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 <http://www.gnu.org/licenses/>.
*/
#include "metadata/ArtistInfo.hpp"
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/xml_parser.hpp>
#include "core/ILogger.hpp"
namespace lms::metadata
{
std::span<const std::filesystem::path> getSupportedInfoFileExtensions()
{
static const std::array<std::filesystem::path, 1> fileExtensions{ ".nfo" };
return fileExtensions;
}
ArtistInfo parseArtistInfo(std::istream& is)
{
try
{
ArtistInfo artistInfo;
boost::property_tree::ptree root;
boost::property_tree::read_xml(is, root);
const auto& artistNode{ root.get_child("artist") };
artistInfo.mbid = core::UUID::fromString(artistNode.get_optional<std::string>("musicBrainzArtistID").value_or(""));
artistInfo.name = artistNode.get_optional<std::string>("name").value_or("");
artistInfo.sortName = artistNode.get_optional<std::string>("sortname").value_or("");
artistInfo.type = artistNode.get_optional<std::string>("type").value_or("");
artistInfo.gender = artistNode.get_optional<std::string>("gender").value_or("");
artistInfo.disambiguation = artistNode.get_optional<std::string>("disambiguation").value_or("");
artistInfo.biography = artistNode.get_optional<std::string>("biography").value_or("");
return artistInfo;
}
catch (boost::property_tree::ptree_error& error)
{
LMS_LOG(RECOMMENDATION, ERROR, "Cannot read artist xml info: " << error.what());
throw ArtistInfoParseException{ error.what() };
}
}
} // namespace lms::metadata
@@ -17,7 +17,7 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Parser.hpp"
#include "AudioFileParser.hpp"
#include <span>
@@ -26,7 +26,9 @@
#include "core/String.hpp"
#include "metadata/Exception.hpp"
#include "AvFormatImageReader.hpp"
#include "AvFormatTagReader.hpp"
#include "TagLibImageReader.hpp"
#include "TagLibTagReader.hpp"
#include "Utils.hpp"
@@ -149,15 +151,14 @@ namespace lms::metadata
std::initializer_list<TagType> artistTagNames,
std::initializer_list<TagType> artistSortTagNames,
std::initializer_list<TagType> artistMBIDTagNames,
std::span<const std::string> artistTagDelimiters,
std::span<const std::string> defaultTagDelimiters)
const AudioFileParserParameters& params)
{
std::vector<std::string> artistNames{ getTagValuesFirstMatchAs<std::string>(tagReader, artistTagNames, artistTagDelimiters) };
std::vector<std::string> artistNames{ getTagValuesFirstMatchAs<std::string>(tagReader, artistTagNames, params.artistTagDelimiters) };
if (artistNames.empty())
return {};
std::vector<std::string> artistSortNames{ getTagValuesFirstMatchAs<std::string>(tagReader, artistSortTagNames, artistTagDelimiters) };
std::vector<core::UUID> artistMBIDs{ getTagValuesFirstMatchAs<core::UUID>(tagReader, artistMBIDTagNames, defaultTagDelimiters) };
std::vector<std::string> artistSortNames{ getTagValuesFirstMatchAs<std::string>(tagReader, artistSortTagNames, params.artistTagDelimiters) };
std::vector<core::UUID> artistMBIDs{ getTagValuesFirstMatchAs<core::UUID>(tagReader, artistMBIDTagNames, params.defaultTagDelimiters) };
std::vector<Artist> artists;
artists.reserve(artistNames.size());
@@ -222,7 +223,7 @@ namespace lms::metadata
return std::any_of(std::cbegin(subStrs), std::cend(subStrs), [&str](const std::string& subStr) { return str.find(subStr) != std::string_view::npos; });
}
std::string computeArtistDisplayName(std::span<const Artist> artists, const std::optional<std::string> artistTag, std::span<const std::string> artistTagDelimiters)
std::string computeArtistDisplayName(std::span<const Artist> artists, const std::optional<std::string>& artistTag, std::span<const std::string> artistTagDelimiters)
{
std::string artistDisplayName;
@@ -316,19 +317,18 @@ namespace lms::metadata
}
} // namespace
std::unique_ptr<IParser> createParser(ParserBackend parserBackend, ParserReadStyle parserReadStyle)
std::unique_ptr<IAudioFileParser> createAudioFileParser(const AudioFileParserParameters& params)
{
return std::make_unique<Parser>(parserBackend, parserReadStyle);
return std::make_unique<AudioFileParser>(params);
}
Parser::Parser(ParserBackend parserBackend, ParserReadStyle readStyle)
: _parserBackend{ parserBackend }
, _readStyle{ readStyle }
AudioFileParser::AudioFileParser(const AudioFileParserParameters& params)
: _params{ params }
{
switch (_parserBackend)
switch (_params.backend)
{
case ParserBackend::TagLib:
LMS_LOG(METADATA, INFO, "Using TagLib parser with read style = " << utils::readStyleToString(readStyle));
LMS_LOG(METADATA, INFO, "Using TagLib parser with read style = " << utils::readStyleToString(_params.readStyle));
break;
case ParserBackend::AvFormat:
@@ -337,7 +337,7 @@ namespace lms::metadata
}
}
std::span<const std::filesystem::path> Parser::getSupportedExtensions() const
std::span<const std::filesystem::path> AudioFileParser::getSupportedExtensions() const
{
// TODO: use backend capability to retrieve supported formats
static const std::array<std::filesystem::path, 18> fileExtensions{
@@ -363,34 +363,61 @@ namespace lms::metadata
return fileExtensions;
}
std::unique_ptr<Track> Parser::parse(const std::filesystem::path& p, bool debug)
std::unique_ptr<Track> AudioFileParser::parseMetaData(const std::filesystem::path& p)
{
try
{
std::unique_ptr<ITagReader> tagReader;
switch (_parserBackend)
switch (_params.backend)
{
case ParserBackend::TagLib:
tagReader = std::make_unique<TagLibTagReader>(p, _readStyle, debug);
tagReader = std::make_unique<TagLibTagReader>(p, _params.readStyle, _params.debug);
break;
case ParserBackend::AvFormat:
tagReader = std::make_unique<AvFormatTagReader>(p, debug);
tagReader = std::make_unique<AvFormatTagReader>(p, _params.debug);
break;
}
if (!tagReader)
throw ParseException{ "Unhandled parser backend" };
return parse(*tagReader);
return parseMetaData(*tagReader);
}
catch (const Exception& e)
{
LMS_LOG(METADATA, ERROR, "File " << p << ": parsing failed");
LMS_LOG(METADATA, ERROR, "File " << p << ": metadata parsing failed");
throw ParseException{};
}
}
std::unique_ptr<Track> Parser::parse(const ITagReader& tagReader)
void AudioFileParser::parseImages(const std::filesystem::path& p, ImageVisitor visitor)
{
try
{
std::unique_ptr<IImageReader> imageReader;
switch (_params.backend)
{
case ParserBackend::TagLib:
imageReader = std::make_unique<TagLibImageReader>(p);
break;
case ParserBackend::AvFormat:
imageReader = std::make_unique<AvFormatImageReader>(p);
break;
}
if (!imageReader)
throw ParseException{ "Unhandled parser backend" };
parseImages(*imageReader, std::move(visitor));
}
catch (const Exception& e)
{
LMS_LOG(METADATA, ERROR, "File " << p << ": image parsing failed");
throw ParseException{};
}
}
std::unique_ptr<Track> AudioFileParser::parseMetaData(const ITagReader& tagReader)
{
auto track{ std::make_unique<Track>() };
@@ -400,10 +427,8 @@ namespace lms::metadata
return track;
}
void Parser::processTags(const ITagReader& tagReader, Track& track)
void AudioFileParser::processTags(const ITagReader& tagReader, Track& track)
{
track.hasCover = tagReader.hasEmbeddedCover();
track.title = getTagValueAs<std::string>(tagReader, TagType::TrackTitle).value_or("");
track.mbid = getTagValueAs<core::UUID>(tagReader, TagType::MusicBrainzTrackID);
track.recordingMBID = getTagValueAs<core::UUID>(tagReader, TagType::MusicBrainzRecordingID);
@@ -436,32 +461,32 @@ namespace lms::metadata
track.copyrightURL = getTagValueAs<std::string>(tagReader, TagType::CopyrightURL).value_or("");
track.replayGain = getTagValueAs<float>(tagReader, TagType::ReplayGainTrackGain);
for (const std::string& userExtraTag : _userExtraTags)
for (const std::string& userExtraTag : _params.userExtraTags)
{
visitTagValues(tagReader, userExtraTag, _defaultTagDelimiters, [&](std::string_view value) {
visitTagValues(tagReader, userExtraTag, _params.defaultTagDelimiters, [&](std::string_view value) {
value = core::stringUtils::stringTrim(value);
if (!value.empty())
track.userExtraTags[userExtraTag].push_back(std::string{ value });
});
}
track.genres = getTagValuesAs<std::string>(tagReader, TagType::Genre, _defaultTagDelimiters);
track.moods = getTagValuesAs<std::string>(tagReader, TagType::Mood, _defaultTagDelimiters);
track.groupings = getTagValuesAs<std::string>(tagReader, TagType::Grouping, _defaultTagDelimiters);
track.languages = getTagValuesAs<std::string>(tagReader, TagType::Language, _defaultTagDelimiters);
track.genres = getTagValuesAs<std::string>(tagReader, TagType::Genre, _params.defaultTagDelimiters);
track.moods = getTagValuesAs<std::string>(tagReader, TagType::Mood, _params.defaultTagDelimiters);
track.groupings = getTagValuesAs<std::string>(tagReader, TagType::Grouping, _params.defaultTagDelimiters);
track.languages = getTagValuesAs<std::string>(tagReader, TagType::Language, _params.defaultTagDelimiters);
std::vector<std::string_view> artistDelimiters{};
track.medium = getMedium(tagReader);
track.artists = getArtists(tagReader, { TagType::Artists, TagType::Artist }, { TagType::ArtistSortOrder }, { TagType::MusicBrainzArtistID }, _artistTagDelimiters, _defaultTagDelimiters);
track.artistDisplayName = computeArtistDisplayName(track.artists, getTagValueAs<std::string>(tagReader, TagType::Artist), _artistTagDelimiters);
track.artists = getArtists(tagReader, { TagType::Artists, TagType::Artist }, { TagType::ArtistSortOrder }, { TagType::MusicBrainzArtistID }, _params);
track.artistDisplayName = computeArtistDisplayName(track.artists, getTagValueAs<std::string>(tagReader, TagType::Artist), _params.artistTagDelimiters);
track.conductorArtists = getArtists(tagReader, { TagType::Conductors, TagType::Conductor }, { TagType::ConductorsSortOrder, TagType::ConductorSortOrder }, {}, _artistTagDelimiters, _defaultTagDelimiters);
track.composerArtists = getArtists(tagReader, { TagType::Composers, TagType::Composer }, { TagType::ComposersSortOrder, TagType::ComposerSortOrder }, {}, _artistTagDelimiters, _defaultTagDelimiters);
track.lyricistArtists = getArtists(tagReader, { TagType::Lyricists, TagType::Lyricist }, { TagType::LyricistsSortOrder, TagType::LyricistSortOrder }, {}, _artistTagDelimiters, _defaultTagDelimiters);
track.mixerArtists = getArtists(tagReader, { TagType::Mixers, TagType::Mixer }, { TagType::MixersSortOrder, TagType::MixerSortOrder }, {}, _artistTagDelimiters, _defaultTagDelimiters);
track.producerArtists = getArtists(tagReader, { TagType::Producers, TagType::Producer }, { TagType::ProducersSortOrder, TagType::ProducerSortOrder }, {}, _artistTagDelimiters, _defaultTagDelimiters);
track.remixerArtists = getArtists(tagReader, { TagType::Remixers, TagType::Remixer }, { TagType::RemixersSortOrder, TagType::RemixerSortOrder }, {}, _artistTagDelimiters, _defaultTagDelimiters);
track.conductorArtists = getArtists(tagReader, { TagType::Conductors, TagType::Conductor }, { TagType::ConductorsSortOrder, TagType::ConductorSortOrder }, {}, _params);
track.composerArtists = getArtists(tagReader, { TagType::Composers, TagType::Composer }, { TagType::ComposersSortOrder, TagType::ComposerSortOrder }, {}, _params);
track.lyricistArtists = getArtists(tagReader, { TagType::Lyricists, TagType::Lyricist }, { TagType::LyricistsSortOrder, TagType::LyricistSortOrder }, {}, _params);
track.mixerArtists = getArtists(tagReader, { TagType::Mixers, TagType::Mixer }, { TagType::MixersSortOrder, TagType::MixerSortOrder }, {}, _params);
track.producerArtists = getArtists(tagReader, { TagType::Producers, TagType::Producer }, { TagType::ProducersSortOrder, TagType::ProducerSortOrder }, {}, _params);
track.remixerArtists = getArtists(tagReader, { TagType::Remixers, TagType::Remixer }, { TagType::RemixersSortOrder, TagType::RemixerSortOrder }, {}, _params);
track.performerArtists = getPerformerArtists(tagReader); // artistDelimiters not supported
fillMissingMbids(track);
@@ -471,7 +496,7 @@ namespace lms::metadata
track.originalYear = track.originalDate.getYear();
}
std::optional<Medium> Parser::getMedium(const ITagReader& tagReader)
std::optional<Medium> AudioFileParser::getMedium(const ITagReader& tagReader)
{
std::optional<Medium> medium;
medium.emplace();
@@ -501,7 +526,7 @@ namespace lms::metadata
return medium;
}
std::optional<Release> Parser::getRelease(const ITagReader& tagReader)
std::optional<Release> AudioFileParser::getRelease(const ITagReader& tagReader)
{
std::optional<Release> release;
@@ -512,16 +537,16 @@ namespace lms::metadata
release.emplace();
release->name = std::move(*releaseName);
release->sortName = getTagValueAs<std::string>(tagReader, TagType::AlbumSortOrder).value_or(release->name);
release->artists = getArtists(tagReader, { TagType::AlbumArtists, TagType::AlbumArtist }, { TagType::AlbumArtistsSortOrder, TagType::AlbumArtistSortOrder }, { TagType::MusicBrainzReleaseArtistID }, _artistTagDelimiters, _defaultTagDelimiters);
release->artistDisplayName = computeArtistDisplayName(release->artists, getTagValueAs<std::string>(tagReader, TagType::AlbumArtist), _artistTagDelimiters);
release->artists = getArtists(tagReader, { TagType::AlbumArtists, TagType::AlbumArtist }, { TagType::AlbumArtistsSortOrder, TagType::AlbumArtistSortOrder }, { TagType::MusicBrainzReleaseArtistID }, _params);
release->artistDisplayName = computeArtistDisplayName(release->artists, getTagValueAs<std::string>(tagReader, TagType::AlbumArtist), _params.artistTagDelimiters);
release->mbid = getTagValueAs<core::UUID>(tagReader, TagType::MusicBrainzReleaseID);
release->groupMBID = getTagValueAs<core::UUID>(tagReader, TagType::MusicBrainzReleaseGroupID);
release->mediumCount = getTagValueAs<std::size_t>(tagReader, TagType::TotalDiscs);
release->isCompilation = getTagValueAs<bool>(tagReader, TagType::Compilation).value_or(false);
release->barcode = getTagValueAs<std::string>(tagReader, TagType::Barcode).value_or("");
release->labels = getTagValuesAs<std::string>(tagReader, TagType::RecordLabel, _defaultTagDelimiters);
release->labels = getTagValuesAs<std::string>(tagReader, TagType::RecordLabel, _params.defaultTagDelimiters);
release->comment = getTagValueAs<std::string>(tagReader, TagType::AlbumComment).value_or("");
release->countries = getTagValuesAs<std::string>(tagReader, TagType::ReleaseCountry, _defaultTagDelimiters);
release->countries = getTagValuesAs<std::string>(tagReader, TagType::ReleaseCountry, _params.defaultTagDelimiters);
if (!release->mediumCount)
{
// mediumCount may be encoded as "position/count"
@@ -534,8 +559,15 @@ namespace lms::metadata
}
}
release->releaseTypes = getTagValuesAs<std::string>(tagReader, TagType::ReleaseType, _defaultTagDelimiters);
release->releaseTypes = getTagValuesAs<std::string>(tagReader, TagType::ReleaseType, _params.defaultTagDelimiters);
return release;
}
void AudioFileParser::parseImages(const IImageReader& reader, ImageVisitor visitor)
{
reader.visitImages([&](const Image& image) {
visitor(image);
});
}
} // namespace lms::metadata
@@ -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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <optional>
#include "metadata/IAudioFileParser.hpp"
namespace lms::metadata
{
class IImageReader;
class ITagReader;
class AudioFileParser : public IAudioFileParser
{
public:
AudioFileParser(const AudioFileParserParameters& params = {});
~AudioFileParser() override = default;
AudioFileParser(const AudioFileParser&) = delete;
AudioFileParser& operator=(const AudioFileParser&) = delete;
protected:
std::unique_ptr<Track> parseMetaData(const std::filesystem::path& p) override;
std::unique_ptr<Track> parseMetaData(const ITagReader& reader);
static void parseImages(const IImageReader& reader, ImageVisitor visitor);
private:
void parseImages(const std::filesystem::path& p, ImageVisitor visitor) override;
std::span<const std::filesystem::path> getSupportedExtensions() const override;
void processTags(const ITagReader& reader, Track& track);
std::optional<Medium> getMedium(const ITagReader& tagReader);
std::optional<Release> getRelease(const ITagReader& tagReader);
const AudioFileParserParameters _params;
};
} // namespace lms::metadata
@@ -0,0 +1,59 @@
/*
* Copyright (C) 2025 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 <http://www.gnu.org/licenses/>.
*/
#include "AvFormatImageReader.hpp"
#include "av/IAudioFile.hpp"
#include "av/Types.hpp"
namespace lms::metadata
{
AvFormatImageReader::AvFormatImageReader(const std::filesystem::path& p)
{
try
{
_audioFile = av::parseAudioFile(p);
}
catch (av::Exception& e)
{
throw ParseException{};
}
}
AvFormatImageReader::~AvFormatImageReader() = default;
void AvFormatImageReader::visitImages(ImageVisitor visitor) const
{
auto metaDataHasKeyword{ [](const av::IAudioFile::MetadataMap& metadata, std::string_view keyword) {
return std::any_of(std::cbegin(metadata), std::cend(metadata), [&](const auto& keyValue) { return core::stringUtils::stringCaseInsensitiveContains(keyValue.second, keyword); });
} };
_audioFile->visitAttachedPictures([&](const av::Picture& picture, const av::IAudioFile::MetadataMap& metaData) {
Image image;
image.data = picture.data;
image.mimeType = picture.mimeType;
if (metaDataHasKeyword(metaData, "front"))
image.type = Image::Type::FrontCover;
else if (metaDataHasKeyword(metaData, "back"))
image.type = Image::Type::BackCover;
visitor(image);
});
}
} // namespace lms::metadata
@@ -0,0 +1,45 @@
/*
* Copyright (C) 2025 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "IImageReader.hpp"
namespace lms::av
{
class IAudioFile;
}
namespace lms::metadata
{
class AvFormatImageReader : public IImageReader
{
public:
AvFormatImageReader(const std::filesystem::path& p);
~AvFormatImageReader() override;
AvFormatImageReader(const AvFormatImageReader&) = delete;
AvFormatImageReader& operator=(const AvFormatImageReader&) = delete;
private:
void visitImages(ImageVisitor visitor) const override;
std::unique_ptr<av::IAudioFile> _audioFile;
};
} // namespace lms::metadata
@@ -156,7 +156,6 @@ namespace lms::metadata
_containerInfo = audioFile->getContainerInfo();
_metaDataMap = audioFile->getMetaData();
_hasEmbeddedCover = audioFile->hasAttachedPictures();
if (debug && core::Service<core::logging::ILogger>::get()->isSeverityActive(core::logging::Severity::DEBUG))
{
@@ -40,12 +40,10 @@ namespace lms::metadata
void visitTagValues(std::string_view tag, TagValueVisitor visitor) const override;
void visitPerformerTags(PerformerVisitor visitor) const override;
void visitLyricsTags(LyricsVisitor visitor) const override;
bool hasEmbeddedCover() const override { return _hasEmbeddedCover; }
const AudioProperties& getAudioProperties() const override { return _audioProperties; }
AudioProperties _audioProperties;
av::IAudioFile::MetadataMap _metaDataMap;
av::ContainerInfo _containerInfo;
bool _hasEmbeddedCover{};
};
} // namespace lms::metadata
+36
View File
@@ -0,0 +1,36 @@
/*
* Copyright (C) 2025 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <functional>
#include "metadata/Types.hpp"
namespace lms::metadata
{
class IImageReader
{
public:
virtual ~IImageReader() = default;
using ImageVisitor = std::function<void(const Image& image)>;
virtual void visitImages(ImageVisitor visitor) const = 0;
};
} // namespace lms::metadata
-1
View File
@@ -161,7 +161,6 @@ namespace lms::metadata
using LyricsVisitor = std::function<void(std::string_view language, std::string_view lyrics)>;
virtual void visitLyricsTags(LyricsVisitor visitor) const = 0;
virtual bool hasEmbeddedCover() const = 0;
virtual const AudioProperties& getAudioProperties() const = 0;
};
} // namespace lms::metadata
-54
View File
@@ -1,54 +0,0 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "metadata/IParser.hpp"
#include "ITagReader.hpp"
namespace lms::metadata
{
class Parser : public IParser
{
public:
Parser(ParserBackend parserBackend = ParserBackend::TagLib, ParserReadStyle readStyle = ParserReadStyle::Average);
std::unique_ptr<Track> parse(const std::filesystem::path& p, bool debug = false) override;
std::unique_ptr<Track> parse(const ITagReader& reader);
private:
std::span<const std::filesystem::path> getSupportedExtensions() const override;
void setUserExtraTags(std::span<const std::string> extraTags) override { _userExtraTags.assign(std::cbegin(extraTags), std::cend(extraTags)); }
void setArtistTagDelimiters(std::span<const std::string> delimiters) override { _artistTagDelimiters.assign(std::cbegin(delimiters), std::cend(delimiters)); }
void setDefaultTagDelimiters(std::span<const std::string> delimiters) override { _defaultTagDelimiters.assign(std::cbegin(delimiters), std::cend(delimiters)); }
void processTags(const ITagReader& reader, Track& track);
std::optional<Medium> getMedium(const ITagReader& tagReader);
std::optional<Release> getRelease(const ITagReader& tagReader);
const ParserBackend _parserBackend;
const ParserReadStyle _readStyle;
std::vector<std::string> _userExtraTags;
std::vector<std::string> _artistTagDelimiters;
std::vector<std::string> _defaultTagDelimiters;
};
} // namespace lms::metadata
+32
View File
@@ -0,0 +1,32 @@
/*
* Copyright (C) 2025 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <taglib/taglib.h>
// TAGLIB_HAS_MP4_ITEM_TYPE if version >= 2.0.1
#if ((TAGLIB_MAJOR_VERSION > 2) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_MINOR_VERSION > 0) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_PATCH_VERSION >= 1))
#define TAGLIB_HAS_MP4_ITEM_TYPE 1
#endif
// TAGLIB_HAS_APE_COMPLEX_PROPERTIES if version >= 2.0.2
#if ((TAGLIB_MAJOR_VERSION > 2) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_MINOR_VERSION > 0) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_PATCH_VERSION >= 2))
#define TAGLIB_HAS_APE_COMPLEX_PROPERTIES 1
#endif
@@ -0,0 +1,431 @@
/*
* Copyright (C) 2025 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 <http://www.gnu.org/licenses/>.
*/
#include "TagLibImageReader.hpp"
#include <taglib/aifffile.h>
#include <taglib/apetag.h>
#include <taglib/asffile.h>
#include <taglib/attachedpictureframe.h>
#include <taglib/flacfile.h>
#include <taglib/flacpicture.h>
#include <taglib/id3v2tag.h>
#include <taglib/mp4coverart.h>
#include <taglib/mp4file.h>
#include <taglib/mpcfile.h>
#include <taglib/mpegfile.h>
#include <taglib/opusfile.h>
#include <taglib/vorbisfile.h>
#include <taglib/wavfile.h>
#include <taglib/wavpackfile.h>
#include <tbytevector.h>
#include "core/ILogger.hpp"
#include "core/ITraceLogger.hpp"
#include "core/String.hpp"
#include "metadata/Exception.hpp"
#include "TagLibDefs.hpp"
namespace lms::metadata
{
namespace
{
class ParsingFailedException : public Exception
{
};
Image::Type imageTypeFromfromIDv2(TagLib::ID3v2::AttachedPictureFrame::Type type)
{
switch (type)
{
case TagLib::ID3v2::AttachedPictureFrame::Type::Other:
return Image::Type::Other;
case TagLib::ID3v2::AttachedPictureFrame::Type::FileIcon:
return Image::Type::FileIcon;
case TagLib::ID3v2::AttachedPictureFrame::Type::OtherFileIcon:
return Image::Type::OtherFileIcon;
case TagLib::ID3v2::AttachedPictureFrame::Type::FrontCover:
return Image::Type::FrontCover;
case TagLib::ID3v2::AttachedPictureFrame::Type::BackCover:
return Image::Type::BackCover;
case TagLib::ID3v2::AttachedPictureFrame::Type::LeafletPage:
return Image::Type::LeafletPage;
case TagLib::ID3v2::AttachedPictureFrame::Type::Media:
return Image::Type::Media;
case TagLib::ID3v2::AttachedPictureFrame::Type::LeadArtist:
return Image::Type::LeadArtist;
case TagLib::ID3v2::AttachedPictureFrame::Type::Artist:
return Image::Type::Artist;
case TagLib::ID3v2::AttachedPictureFrame::Type::Conductor:
return Image::Type::Conductor;
case TagLib::ID3v2::AttachedPictureFrame::Type::Band:
return Image::Type::Band;
case TagLib::ID3v2::AttachedPictureFrame::Type::Composer:
return Image::Type::Composer;
case TagLib::ID3v2::AttachedPictureFrame::Type::Lyricist:
return Image::Type::Lyricist;
case TagLib::ID3v2::AttachedPictureFrame::Type::RecordingLocation:
return Image::Type::RecordingLocation;
case TagLib::ID3v2::AttachedPictureFrame::Type::DuringRecording:
return Image::Type::DuringRecording;
case TagLib::ID3v2::AttachedPictureFrame::Type::DuringPerformance:
return Image::Type::DuringPerformance;
case TagLib::ID3v2::AttachedPictureFrame::Type::MovieScreenCapture:
return Image::Type::MovieScreenCapture;
case TagLib::ID3v2::AttachedPictureFrame::Type::ColouredFish:
return Image::Type::ColouredFish;
case TagLib::ID3v2::AttachedPictureFrame::Type::Illustration:
return Image::Type::Illustration;
case TagLib::ID3v2::AttachedPictureFrame::Type::BandLogo:
return Image::Type::BandLogo;
case TagLib::ID3v2::AttachedPictureFrame::Type::PublisherLogo:
return Image::Type::PublisherLogo;
}
return Image::Type::Unknown;
}
Image::Type imageTypeFromfromASF(TagLib::ASF::Picture::Type type)
{
switch (type)
{
case TagLib::ASF::Picture::Type::Other:
return Image::Type::Other;
case TagLib::ASF::Picture::Type::FileIcon:
return Image::Type::FileIcon;
case TagLib::ASF::Picture::Type::OtherFileIcon:
return Image::Type::OtherFileIcon;
case TagLib::ASF::Picture::Type::FrontCover:
return Image::Type::FrontCover;
case TagLib::ASF::Picture::Type::BackCover:
return Image::Type::BackCover;
case TagLib::ASF::Picture::Type::LeafletPage:
return Image::Type::LeafletPage;
case TagLib::ASF::Picture::Type::Media:
return Image::Type::Media;
case TagLib::ASF::Picture::Type::LeadArtist:
return Image::Type::LeadArtist;
case TagLib::ASF::Picture::Type::Artist:
return Image::Type::Artist;
case TagLib::ASF::Picture::Type::Conductor:
return Image::Type::Conductor;
case TagLib::ASF::Picture::Type::Band:
return Image::Type::Band;
case TagLib::ASF::Picture::Type::Composer:
return Image::Type::Composer;
case TagLib::ASF::Picture::Type::Lyricist:
return Image::Type::Lyricist;
case TagLib::ASF::Picture::Type::RecordingLocation:
return Image::Type::RecordingLocation;
case TagLib::ASF::Picture::Type::DuringRecording:
return Image::Type::DuringRecording;
case TagLib::ASF::Picture::Type::DuringPerformance:
return Image::Type::DuringPerformance;
case TagLib::ASF::Picture::Type::MovieScreenCapture:
return Image::Type::MovieScreenCapture;
case TagLib::ASF::Picture::Type::ColouredFish:
return Image::Type::ColouredFish;
case TagLib::ASF::Picture::Type::Illustration:
return Image::Type::Illustration;
case TagLib::ASF::Picture::Type::BandLogo:
return Image::Type::BandLogo;
case TagLib::ASF::Picture::Type::PublisherLogo:
return Image::Type::PublisherLogo;
}
return Image::Type::Unknown;
}
Image::Type imageTypeFromfromFLAC(TagLib::FLAC::Picture::Type type)
{
switch (type)
{
case TagLib::FLAC::Picture::Type::Other:
return Image::Type::Other;
case TagLib::FLAC::Picture::Type::FileIcon:
return Image::Type::FileIcon;
case TagLib::FLAC::Picture::Type::OtherFileIcon:
return Image::Type::OtherFileIcon;
case TagLib::FLAC::Picture::Type::FrontCover:
return Image::Type::FrontCover;
case TagLib::FLAC::Picture::Type::BackCover:
return Image::Type::BackCover;
case TagLib::FLAC::Picture::Type::LeafletPage:
return Image::Type::LeafletPage;
case TagLib::FLAC::Picture::Type::Media:
return Image::Type::Media;
case TagLib::FLAC::Picture::Type::LeadArtist:
return Image::Type::LeadArtist;
case TagLib::FLAC::Picture::Type::Artist:
return Image::Type::Artist;
case TagLib::FLAC::Picture::Type::Conductor:
return Image::Type::Conductor;
case TagLib::FLAC::Picture::Type::Band:
return Image::Type::Band;
case TagLib::FLAC::Picture::Type::Composer:
return Image::Type::Composer;
case TagLib::FLAC::Picture::Type::Lyricist:
return Image::Type::Lyricist;
case TagLib::FLAC::Picture::Type::RecordingLocation:
return Image::Type::RecordingLocation;
case TagLib::FLAC::Picture::Type::DuringRecording:
return Image::Type::DuringRecording;
case TagLib::FLAC::Picture::Type::DuringPerformance:
return Image::Type::DuringPerformance;
case TagLib::FLAC::Picture::Type::MovieScreenCapture:
return Image::Type::MovieScreenCapture;
case TagLib::FLAC::Picture::Type::ColouredFish:
return Image::Type::ColouredFish;
case TagLib::FLAC::Picture::Type::Illustration:
return Image::Type::Illustration;
case TagLib::FLAC::Picture::Type::BandLogo:
return Image::Type::BandLogo;
case TagLib::FLAC::Picture::Type::PublisherLogo:
return Image::Type::PublisherLogo;
}
return Image::Type::Unknown;
}
const char* mp4ImageFormatToMimeType(TagLib::MP4::CoverArt::Format format)
{
switch (format)
{
case TagLib::MP4::CoverArt::Format::BMP:
return "image/bmp";
case TagLib::MP4::CoverArt::Format::GIF:
return "image/gif";
case TagLib::MP4::CoverArt::Format::JPEG:
return "image/jpeg";
case TagLib::MP4::CoverArt::Format::PNG:
return "image/png";
case TagLib::MP4::CoverArt::Format::Unknown:
return "application/octet-stream";
}
return "application/octet-stream";
}
#if TAGLIB_HAS_APE_COMPLEX_PROPERTIES
Image::Type imageTypeFromAPEPictureType(std::string_view pictureType)
{
if (core::stringUtils::stringCaseInsensitiveContains(pictureType, "front"))
return Image::Type::FrontCover;
else if (core::stringUtils::stringCaseInsensitiveContains(pictureType, "back"))
return Image::Type::BackCover;
return Image::Type::Unknown;
}
#endif // TAGLIB_HAS_APE_COMPLEX_PROPERTIES
void visitID3V2Images(const TagLib::ID3v2::Tag& id3v2Tags, TagLibImageReader::ImageVisitor visitor)
{
const auto& frameListMap{ id3v2Tags.frameListMap() };
for (const TagLib::ID3v2::Frame* frame : frameListMap["APIC"])
{
const auto* attachedPictureFrame{ dynamic_cast<const TagLib::ID3v2::AttachedPictureFrame*>(frame) };
if (!attachedPictureFrame)
continue;
TagLib::ByteVector picture{ attachedPictureFrame->picture() };
std::span<const std::byte> pictureData{ reinterpret_cast<const std::byte*>(picture.data()), picture.size() };
Image image;
image.type = imageTypeFromfromIDv2(attachedPictureFrame->type());
image.description = attachedPictureFrame->description().to8Bit(true);
image.mimeType = attachedPictureFrame->mimeType().to8Bit(true);
image.data = pictureData;
visitor(image);
}
}
void visitASFImages(const TagLib::ASF::Tag& asfTags, TagLibImageReader::ImageVisitor visitor)
{
for (const TagLib::ASF::Attribute& attribute : asfTags.attribute("WM/Picture"))
{
TagLib::ASF::Picture asfPicture{ attribute.toPicture() };
if (!asfPicture.isValid())
continue;
TagLib::ByteVector picture{ asfPicture.picture() };
std::span<const std::byte> pictureData{ reinterpret_cast<const std::byte*>(picture.data()), picture.size() };
Image image;
image.type = imageTypeFromfromASF(asfPicture.type());
image.description = asfPicture.description().to8Bit(true);
image.mimeType = asfPicture.mimeType().to8Bit(true);
image.data = pictureData;
visitor(image);
}
}
void visitMP4Images(const TagLib::MP4::File& mp4File, TagLibImageReader::ImageVisitor visitor)
{
const TagLib::MP4::Item coverItem{ mp4File.tag()->item("covr") };
if (!coverItem.isValid())
return;
#if TAGLIB_HAS_MP4_ITEM_TYPE
if (coverItem.type() != TagLib::MP4::Item::Type::CoverArtList)
return;
#endif
TagLib::MP4::CoverArtList coverArtList{ coverItem.toCoverArtList() };
for (TagLib::MP4::CoverArt& coverArt : coverArtList)
{
TagLib::ByteVector picture{ coverArt.data() };
std::span<const std::byte> pictureData{ reinterpret_cast<const std::byte*>(picture.data()), picture.size() };
Image image;
image.mimeType = mp4ImageFormatToMimeType(coverArt.format());
image.data = pictureData;
visitor(image);
}
}
void visitFLACImages(const TagLib::List<TagLib::FLAC::Picture*> pictureList, TagLibImageReader::ImageVisitor visitor)
{
for (TagLib::FLAC::Picture* flacPicture : pictureList)
{
TagLib::ByteVector picture{ flacPicture->data() };
std::span<const std::byte> pictureData{ reinterpret_cast<const std::byte*>(picture.data()), picture.size() };
Image image;
image.type = imageTypeFromfromFLAC(flacPicture->type());
image.description = flacPicture->description().to8Bit(true);
image.mimeType = flacPicture->mimeType().to8Bit(true);
image.data = pictureData;
visitor(image);
}
}
void visitAPEImages([[maybe_unused]] const TagLib::APE::Tag& apeTags, [[maybe_unused]] TagLibImageReader::ImageVisitor visitor)
{
#if TAGLIB_HAS_APE_COMPLEX_PROPERTIES
const TagLib::List<TagLib::VariantMap> pictureProperties{ apeTags.complexProperties("PICTURE") };
for (const TagLib::VariantMap& pictureProperty : pictureProperties)
{
Image image;
TagLib::ByteVector picture;
if (auto it{ pictureProperty.find("pictureType") }; it != pictureProperty.cend())
image.type = imageTypeFromAPEPictureType(it->second.toString().to8Bit(true));
if (auto it{ pictureProperty.find("mimeType") }; it != pictureProperty.cend())
image.mimeType = it->second.toString().to8Bit(true);
if (auto it{ pictureProperty.find("description") }; it != pictureProperty.cend())
image.description = it->second.toString().to8Bit(true);
if (auto it{ pictureProperty.find("data") }; it != pictureProperty.cend())
{
picture = it->second.toByteVector();
image.data = { reinterpret_cast<const std::byte*>(picture.data()), picture.size() };
}
if (!image.data.empty())
visitor(image);
}
#endif // TAGLIB_HAS_APE_COMPLEX_PROPERTIES
}
} // namespace
TagLib::FileRef parseFile(const std::filesystem::path& p)
{
LMS_SCOPED_TRACE_DETAILED("MetaData", "TagLibParseFileForImages");
return TagLib::FileRef{ p.c_str(), false };
}
TagLibImageReader::TagLibImageReader(const std::filesystem::path& p)
: _file{ parseFile(p) }
{
if (_file.isNull())
{
LMS_LOG(METADATA, ERROR, "File " << p << ": parsing failed");
throw ParsingFailedException{};
}
}
void TagLibImageReader::visitImages(ImageVisitor visitor) const
{
// MP3
if (TagLib::MPEG::File * mp3File{ dynamic_cast<TagLib::MPEG::File*>(_file.file()) })
{
if (mp3File->hasID3v2Tag())
visitID3V2Images(*mp3File->ID3v2Tag(), std::move(visitor));
}
// MP4
else if (TagLib::MP4::File * mp4File{ dynamic_cast<TagLib::MP4::File*>(_file.file()) })
{
visitMP4Images(*mp4File, std::move(visitor));
}
// WMA
else if (TagLib::ASF::File * asfFile{ dynamic_cast<TagLib::ASF::File*>(_file.file()) })
{
if (const TagLib::ASF::Tag * tag{ asfFile->tag() })
visitASFImages(*tag, std::move(visitor));
}
// FLAC
else if (TagLib::FLAC::File * flacFile{ dynamic_cast<TagLib::FLAC::File*>(_file.file()) })
{
if (flacFile->hasID3v2Tag()) // usage discouraged
visitID3V2Images(*flacFile->ID3v2Tag(), std::move(visitor));
else
visitFLACImages(flacFile->pictureList(), std::move(visitor));
}
// Ogg vorbis
else if (TagLib::Ogg::Vorbis::File * vorbisFile{ dynamic_cast<TagLib::Ogg::Vorbis::File*>(_file.file()) })
{
visitFLACImages(vorbisFile->tag()->pictureList(), std::move(visitor));
}
// Ogg Opus
else if (TagLib::Ogg::Opus::File * opusFile{ dynamic_cast<TagLib::Ogg::Opus::File*>(_file.file()) })
{
visitFLACImages(opusFile->tag()->pictureList(), std::move(visitor));
}
// Aiff
else if (TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast<TagLib::RIFF::AIFF::File*>(_file.file()) })
{
if (aiffFile->hasID3v2Tag())
visitID3V2Images(*aiffFile->tag(), std::move(visitor));
}
// Wav
else if (TagLib::RIFF::WAV::File * wavFile{ dynamic_cast<TagLib::RIFF::WAV::File*>(_file.file()) })
{
if (wavFile->hasID3v2Tag())
visitID3V2Images(*wavFile->ID3v2Tag(), std::move(visitor));
}
// MPC
else if (TagLib::MPC::File * mpcFile{ dynamic_cast<TagLib::MPC::File*>(_file.file()) })
{
if (mpcFile->hasAPETag())
visitAPEImages(*mpcFile->APETag(), std::move(visitor));
}
// WavPack
else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast<TagLib::WavPack::File*>(_file.file()) })
{
if (wavPackFile->hasAPETag())
visitAPEImages(*wavPackFile->APETag(), std::move(visitor));
}
}
} // namespace lms::metadata
@@ -0,0 +1,37 @@
/*
* Copyright (C) 2025 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "IImageReader.hpp"
#include <taglib/fileref.h>
namespace lms::metadata
{
class TagLibImageReader : public IImageReader
{
public:
TagLibImageReader(const std::filesystem::path& p);
void visitImages(ImageVisitor visitor) const override;
const TagLib::FileRef _file;
};
} // namespace lms::metadata
+25 -38
View File
@@ -49,9 +49,7 @@
#include "core/String.hpp"
#include "metadata/Exception.hpp"
#if (TAGLIB_MAJOR_VERSION > 2) || (TAGLIB_MAJOR_VERSION == 2 && TAGLIB_MINOR_VERSION > 0)
#define TAGLIB_HAS_MP4_ITEM_TYPE
#endif
#include "TagLibDefs.hpp"
namespace lms::metadata
{
@@ -216,7 +214,7 @@ namespace lms::metadata
TagLib::FileRef parseFile(const std::filesystem::path& p, ParserReadStyle parserReadStyle)
{
LMS_SCOPED_TRACE_DETAILED("MetaData", "TagLibParseFile");
LMS_SCOPED_TRACE_DETAILED("MetaData", "TagLibParseFileForMetaData");
return TagLib::FileRef{ p.c_str(), true // read audio properties
,
@@ -269,10 +267,6 @@ namespace lms::metadata
const auto& frameListMap{ id3v2Tags.frameListMap() };
// Not that good embedded pictures handling
if (!frameListMap["APIC"].isEmpty())
_hasEmbeddedCover = true;
// Get some extra tags that may not be known by taglib
if (!frameListMap["TSST"].isEmpty() && !_propertyMap.contains("DISCSUBTITLE"))
_propertyMap["DISCSUBTITLE"] = { frameListMap["TSST"].front()->toString() };
@@ -329,33 +323,44 @@ namespace lms::metadata
{
if (const TagLib::ASF::Tag * tag{ asfFile->tag() })
{
if (tag->attributeListMap().contains("WM/Picture"))
_hasEmbeddedCover = true;
for (const auto& [name, attributeList] : tag->attributeListMap())
{
if (attributeList.isEmpty())
continue;
std::string strName{ core::stringUtils::stringToUpper(name.to8Bit(true)) };
if (strName.find("WM/") == 0 || _propertyMap.find(strName) != std::cend(_propertyMap))
const std::string strName{ core::stringUtils::stringToUpper(name.to8Bit(true)) };
if (debug)
{
for (const auto& attribute : attributeList)
LMS_LOG(METADATA, DEBUG, "ASF Attribute, Key = '" << strName << "', value = '" << (attribute.type() == TagLib::ASF::Attribute::AttributeTypes::UnicodeType ? attribute.toString() : TagLib::String{ "<Non unicode>" }) << "'");
}
if (strName.find("WM/") == 0 || _propertyMap.contains(strName))
continue;
TagLib::StringList attributes;
TagLib::StringList strAttributes;
for (const TagLib::ASF::Attribute& attribute : attributeList)
{
if (attribute.type() == TagLib::ASF::Attribute::AttributeTypes::UnicodeType)
attributes.append(attribute.toString());
strAttributes.append(attribute.toString());
}
if (!attributes.isEmpty())
_propertyMap[strName] = std::move(attributes);
if (!strAttributes.isEmpty())
_propertyMap[strName] = strAttributes;
}
// Merge artists that may have been saved only in Author (see #597)
if (auto itAuthor{ _propertyMap.find("AUTHOR") }; itAuthor != _propertyMap.end() && _propertyMap.unsupportedData().contains("Author"))
{
if (!_propertyMap.contains("ARTISTS"))
_propertyMap["ARTIST"].append(itAuthor->second);
{
auto& artistEntries{ _propertyMap["ARTIST"] };
for (const auto& author : itAuthor->second)
{
if (!artistEntries.contains(author))
artistEntries.append(author);
}
}
}
}
}
@@ -370,14 +375,6 @@ namespace lms::metadata
// MP4
else if (TagLib::MP4::File * mp4File{ dynamic_cast<TagLib::MP4::File*>(_file.file()) })
{
if (const TagLib::MP4::Item coverItem{ mp4File->tag()->item("covr") }; coverItem.isValid())
{
#if TAGLIB_HAS_MP4_ITEM_TYPE
if (coverItem.type() == TagLib::MP4::Item::Type::CoverArtList)
#endif
_hasEmbeddedCover = true;
}
// Taglib does not expose rtng in properties
if (const TagLib::MP4::Item rtngItem{ mp4File->tag()->item("rtng") }; rtngItem.isValid())
{
@@ -421,18 +418,8 @@ namespace lms::metadata
// FLAC
else if (TagLib::FLAC::File * flacFile{ dynamic_cast<TagLib::FLAC::File*>(_file.file()) })
{
if (!flacFile->pictureList().isEmpty())
_hasEmbeddedCover = true;
}
else if (TagLib::Ogg::Vorbis::File * vorbisFile{ dynamic_cast<TagLib::Ogg::Vorbis::File*>(_file.file()) })
{
if (!vorbisFile->tag()->pictureList().isEmpty())
_hasEmbeddedCover = true;
}
else if (TagLib::Ogg::Opus::File * opusFile{ dynamic_cast<TagLib::Ogg::Opus::File*>(_file.file()) })
{
if (!opusFile->tag()->pictureList().isEmpty())
_hasEmbeddedCover = true;
if (flacFile->hasID3v2Tag()) // discouraged usage
processID3v2Tags(*flacFile->ID3v2Tag());
}
else if (TagLib::RIFF::AIFF::File * aiffFile{ dynamic_cast<TagLib::RIFF::AIFF::File*>(_file.file()) })
{
+2 -6
View File
@@ -26,8 +26,6 @@
#include <taglib/fileref.h>
#include <taglib/tpropertymap.h>
#include "metadata/IParser.hpp"
#include "ITagReader.hpp"
namespace lms::metadata
@@ -36,24 +34,22 @@ namespace lms::metadata
{
public:
TagLibTagReader(const std::filesystem::path& path, ParserReadStyle parserReadStyle, bool debug);
private:
~TagLibTagReader() = default;
TagLibTagReader(const TagLibTagReader&) = delete;
TagLibTagReader& operator=(const TagLibTagReader&) = delete;
private:
void computeAudioProperties();
void visitTagValues(TagType tag, TagValueVisitor visitor) const override;
void visitTagValues(std::string_view tag, TagValueVisitor visitor) const override;
void visitPerformerTags(PerformerVisitor visitor) const override;
void visitLyricsTags(LyricsVisitor visitor) const override;
bool hasEmbeddedCover() const override { return _hasEmbeddedCover; }
const AudioProperties& getAudioProperties() const override { return _audioProperties; }
const TagLib::FileRef _file;
AudioProperties _audioProperties;
TagLib::PropertyMap _propertyMap; // case-insensitive keys
bool _hasEmbeddedCover{};
std::multimap<std::string /* language*/, std::string /* lyrics */> _id3v2Lyrics;
};
} // namespace lms::metadata
+1 -1
View File
@@ -25,7 +25,7 @@
#include <Wt/WDate.h>
#include "metadata/IParser.hpp"
#include "metadata/Types.hpp"
namespace lms::metadata::utils
{
@@ -0,0 +1,56 @@
/*
* Copyright (C) 2025 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <filesystem>
#include <iosfwd>
#include <optional>
#include <span>
#include <string>
#include "core/UUID.hpp"
#include "metadata/Exception.hpp"
namespace lms::metadata
{
// See:
// - for the content for the info file: https://kodi.wiki/view/NFO_files/Artists
// - for the definition of some mb fields: https://musicbrainz.org/doc/Artist
struct ArtistInfo
{
std::string name;
std::optional<core::UUID> mbid;
std::string sortName; // mb
std::string type; // mb
std::string gender; // mb
std::string disambiguation; // mb
std::string biography;
};
class ArtistInfoParseException : public Exception
{
public:
using Exception::Exception;
};
std::span<const std::filesystem::path> getSupportedInfoFileExtensions();
ArtistInfo parseArtistInfo(std::istream& is);
} // namespace lms::metadata
@@ -20,6 +20,7 @@
#pragma once
#include <filesystem>
#include <functional>
#include <memory>
#include <span>
@@ -27,30 +28,18 @@
namespace lms::metadata
{
class IParser
class IAudioFileParser
{
public:
virtual ~IParser() = default;
virtual ~IAudioFileParser() = default;
virtual std::unique_ptr<Track> parse(const std::filesystem::path& p, bool debug = false) = 0;
virtual std::unique_ptr<Track> parseMetaData(const std::filesystem::path& p) = 0;
using ImageVisitor = std::function<void(const Image&)>;
virtual void parseImages(const std::filesystem::path& p, ImageVisitor visitor) = 0;
virtual std::span<const std::filesystem::path> getSupportedExtensions() const = 0;
virtual void setUserExtraTags(std::span<const std::string> extraTags) = 0;
virtual void setArtistTagDelimiters(std::span<const std::string> delimiters) = 0;
virtual void setDefaultTagDelimiters(std::span<const std::string> delimiters) = 0;
};
enum class ParserBackend
{
TagLib,
AvFormat,
};
enum class ParserReadStyle
{
Fast,
Average,
Accurate,
};
std::unique_ptr<IParser> createParser(ParserBackend parserBackend, ParserReadStyle parserReadStyle);
std::unique_ptr<IAudioFileParser> createAudioFileParser(const AudioFileParserParameters& params);
} // namespace lms::metadata
+81 -1
View File
@@ -22,6 +22,7 @@
#include <chrono>
#include <map>
#include <optional>
#include <span>
#include <string>
#include <string_view>
#include <vector>
@@ -125,7 +126,6 @@ namespace lms::metadata
core::PartialDateTime originalDate;
std::optional<Advisory> advisory;
core::PartialDateTime encodingTime;
bool hasCover{};
std::optional<core::UUID> acoustID;
std::string copyright;
std::string copyrightURL;
@@ -142,4 +142,84 @@ namespace lms::metadata
std::vector<Artist> producerArtists;
std::vector<Artist> remixerArtists;
};
struct Image
{
// See TagLib types (based on ID3v2 APIC types)
enum class Type
{
// No information
Unknown,
// A type not enumerated below
Other,
// 32x32 PNG image that should be used as the file icon
FileIcon,
// File icon of a different size or format
OtherFileIcon,
// Front cover image of the album
FrontCover,
// Back cover image of the album
BackCover,
// Inside leaflet page of the album
LeafletPage,
// Image from the album itself
Media,
// Picture of the lead artist or soloist
LeadArtist,
// Picture of the artist or performer
Artist,
// Picture of the conductor
Conductor,
// Picture of the band or orchestra
Band,
// Picture of the composer
Composer,
// Picture of the lyricist or text writer
Lyricist,
// Picture of the recording location or studio
RecordingLocation,
// Picture of the artists during recording
DuringRecording,
// Picture of the artists during performance
DuringPerformance,
// Picture from a movie or video related to the track
MovieScreenCapture,
// Picture of a large, coloured fish
ColouredFish,
// Illustration related to the track
Illustration,
// Logo of the band or performer
BandLogo,
// Logo of the publisher (record company)
PublisherLogo
};
Type type{ Type::Unknown };
std::string mimeType{ "application/octet-stream" };
std::string description;
std::span<const std::byte> data;
};
enum class ParserBackend
{
TagLib,
AvFormat,
};
enum class ParserReadStyle
{
Fast,
Average,
Accurate,
};
struct AudioFileParserParameters
{
ParserBackend backend{ ParserBackend::TagLib };
ParserReadStyle readStyle{ ParserReadStyle::Average };
std::vector<std::string> artistTagDelimiters;
std::vector<std::string> defaultTagDelimiters;
std::vector<std::string> userExtraTags;
bool debug{};
};
} // namespace lms::metadata
+79
View File
@@ -0,0 +1,79 @@
/*
* Copyright (C) 2024 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 <http://www.gnu.org/licenses/>.
*/
#include <gtest/gtest.h>
#include "metadata/ArtistInfo.hpp"
namespace lms::metadata::tests
{
TEST(ArtistInfo, basic)
{
std::istringstream is{ R"(<?xml version='1.0' encoding='UTF-8' standalone='yes'?>
<artist>
<name>Tim Taylor</name>
<musicBrainzArtistID>38811c52-85e3-4e2e-3319-ab7d9f2cfa5b</musicBrainzArtistID>
<sortname>Taylor, Tim</sortname>
<disambiguation>Timothy Taylor</disambiguation>
<biography>DJ and producer based in London, UK. Founder of Missile Records and Planet Of Drums.&#13;
&#13;
He moved from the UK to Montreal in 1984 to become resident DJ at a number of clubs. In 1987, he began working as an A&amp;R for JSE Agency &amp; Management in New York, managing the likes of Tommy Musto, Frankie Bones, and The KLF. He also arranged and was tour manager for artists such as Womack &amp; Womack, Jungle Brothers, Ice-T, and Guru Josh.</biography>
<thumb spoof="" cache="" aspect="thumb" preview="https://i.discogs.com/mG5pml7VOsld5ix_X_GNlY-wiN6axOjpFD4eZBkszL0/rs:fit/g:sm/q:40/h:150/w:150/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTQwMjQwMTYxNi01/MTAwLmpwZWc.jpeg">https://i.discogs.com/zY8kWeJfDfWgDDJZ44uYARjNEzDLLqRiXk23LUlik-c/rs:fit/g:sm/q:90/h:800/w:600/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTQwMjQwMTYxNi01/MTAwLmpwZWc.jpeg</thumb>
<thumb spoof="" cache="" aspect="thumb" preview="https://i.discogs.com/AOk30C5KRPIub0DW8Q_3NP-PhtE0l3caXV1_r0lP3ao/rs:fit/g:sm/q:40/h:150/w:150/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTEyMTg2NTk2MC5q/cGc.jpeg">https://i.discogs.com/7Do2Xbok8HnWJEjcW6b0u9hyYMpNleGY3HRIEhNlxlM/rs:fit/g:sm/q:90/h:387/w:281/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTEyMTg2NTk2MC5q/cGc.jpeg</thumb>
<thumb spoof="" cache="" aspect="thumb" preview="https://i.discogs.com/6YyKIXiD5wNTDVzS7JxpnipMUQ5UoJmtQgzhgV0-RB0/rs:fit/g:sm/q:40/h:150/w:150/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTQwMjQwMTYxNi05/NDc3LmpwZWc.jpeg">https://i.discogs.com/fOeq1muY2Cu-gAJZGo5yK0AHIS1PJ1rcWqu8p_e3opY/rs:fit/g:sm/q:90/h:399/w:600/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTQwMjQwMTYxNi05/NDc3LmpwZWc.jpeg</thumb>
<thumb spoof="" cache="" aspect="thumb" preview="https://i.discogs.com/Ubw6Imsd8FUqoQnAb3VVbIqnh1b8VJDKAclTe2X7cXw/rs:fit/g:sm/q:40/h:150/w:150/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTQwMjQwMTYxNS05/NzQ3LmpwZWc.jpeg">https://i.discogs.com/vhMFP7ICq7VyJcZaGim2X0x4nfKNGXkk7U5u153owfs/rs:fit/g:sm/q:90/h:540/w:364/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTQwMjQwMTYxNS05/NzQ3LmpwZWc.jpeg</thumb>
<thumb spoof="" cache="" aspect="thumb" preview="https://i.discogs.com/gXRlEh7W0awsBO3Cndww_n46JNLycyI6EOWajsBOU0A/rs:fit/g:sm/q:40/h:150/w:150/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTQwMjQwMTYxNS03/NDYwLmpwZWc.jpeg">https://i.discogs.com/wsJi9gfDDaoamUjsxFm0R02VAllhW4iaFsCnwVfouO4/rs:fit/g:sm/q:90/h:399/w:600/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTQwMjQwMTYxNS03/NDYwLmpwZWc.jpeg</thumb>
<thumb spoof="" cache="" aspect="thumb" preview="https://i.discogs.com/QbSO414VlPwlRLcBAvhe6NFxCcsdy1rQkAmCzQ8Xe_o/rs:fit/g:sm/q:40/h:150/w:150/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTQwMjQwMTYxNy02/MTg5LmpwZWc.jpeg">https://i.discogs.com/fjy0PGAGHsHIXex5HqMDitJI0Yh3MesiPL6ZOyko4bk/rs:fit/g:sm/q:90/h:902/w:600/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTQwMjQwMTYxNy02/MTg5LmpwZWc.jpeg</thumb>
<thumb spoof="" cache="" aspect="thumb" preview="https://i.discogs.com/8rHEak0VmPSeBRQ8kTg7xwARlg0-yqJtlLCjUiWd75c/rs:fit/g:sm/q:40/h:150/w:150/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTQwMjQwMTYxNi0x/NDgzLmpwZWc.jpeg">https://i.discogs.com/k79VVA9du3LW57naLLjXLlWesxbtygfstnwrHp6Ku84/rs:fit/g:sm/q:90/h:450/w:600/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTQwMjQwMTYxNi0x/NDgzLmpwZWc.jpeg</thumb>
<thumb spoof="" cache="" aspect="thumb" preview="https://i.discogs.com/KtxVQl-q2BzNmIP0hk-Ip8AKwCYZZP0-excxgmMMi68/rs:fit/g:sm/q:40/h:150/w:150/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTQwMjQwMTYxNi01/MTY1LmpwZWc.jpeg">https://i.discogs.com/TLjVejJmVWFkQuAhXndIV0Ovt-1GJ5mHE5NWr3MNXGk/rs:fit/g:sm/q:90/h:399/w:600/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTQwMjQwMTYxNi01/MTY1LmpwZWc.jpeg</thumb>
<thumb spoof="" cache="" aspect="thumb" preview="https://i.discogs.com/Qf4r5w-aRA9ysSo53rnI0E-xDM8XaB7R4CGiHwla_a8/rs:fit/g:sm/q:40/h:150/w:150/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTQwMjQwMTYxNy0y/Mjc5LmpwZWc.jpeg">https://i.discogs.com/Y8V1WqvgdSmcIsgC2CAq_VNhfaWTX9gWJi9bQ6c0Vno/rs:fit/g:sm/q:90/h:398/w:600/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTQwMjQwMTYxNy0y/Mjc5LmpwZWc.jpeg</thumb>
<thumb spoof="" cache="" aspect="thumb" preview="https://i.discogs.com/8a-6X1gRPL0h4PqUBwCffauybMNwz8JYJ81E6JyjnpY/rs:fit/g:sm/q:40/h:150/w:150/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTQwMjQwMTYxNy00/MzcyLmpwZWc.jpeg">https://i.discogs.com/OD2sPGIfSZGnrT6JyfKmlO7kuX4ZadJhP-iNaEzbbuE/rs:fit/g:sm/q:90/h:902/w:600/czM6Ly9kaXNjb2dz/LWRhdGFiYXNlLWlt/YWdlcy9BLTMyNDMt/MTQwMjQwMTYxNy00/MzcyLmpwZWc.jpeg</thumb>
<genre clear="true">Acid House / Hardcore / Techno / Acid / Breakbeat / Minimal / Tech House / Tribal</genre>
<album>
<title>The Penguin / Scissorhands</title>
<year>1996</year>
</album>
<album>
<title>The Minneapolis Sessions (2016 Reissue)</title>
<year>1997</year>
</album>
<album>
<title>Over The Hill</title>
<year>2001</year>
</album>
<album>
<title>Over The Hill Remixes</title>
<year>2001</year>
</album>
<album>
<title>Pleasure Unit</title>
<year>2016</year>
</album>
</artist>)" };
const ArtistInfo artistInfo{ parseArtistInfo(is) };
EXPECT_EQ(artistInfo.mbid, core::UUID::fromString("38811c52-85e3-4e2e-3319-ab7d9f2cfa5b"));
EXPECT_EQ(artistInfo.name, "Tim Taylor");
ASSERT_EQ(artistInfo.sortName, "Taylor, Tim");
ASSERT_EQ(artistInfo.disambiguation, "Timothy Taylor");
ASSERT_EQ(artistInfo.biography, "DJ and producer based in London, UK. Founder of Missile Records and Planet Of Drums.\r\n\r\nHe moved from the UK to Montreal in 1984 to become resident DJ at a number of clubs. In 1987, he began working as an A&R for JSE Agency & Management in New York, managing the likes of Tommy Musto, Frankie Bones, and The KLF. He also arranged and was tour manager for artists such as Womack & Womack, Jungle Brothers, Ice-T, and Guru Josh.");
}
} // namespace lms::metadata::tests
@@ -23,14 +23,25 @@
#include <Wt/WTime.h>
#include "Parser.hpp"
#include "AudioFileParser.hpp"
#include "TestTagReader.hpp"
#include "metadata/Types.hpp"
namespace lms::metadata
{
TEST(Parser, generalTest)
class TestAudioFileParser : public AudioFileParser
{
Parser parser;
public:
using AudioFileParser::AudioFileParser;
using AudioFileParser::parseMetaData;
};
TEST(AudioFileParser, generalTest)
{
AudioFileParserParameters params;
params.userExtraTags = { "MY_AWESOME_TAG_A", "MY_AWESOME_TAG_B", "MY_AWESOME_MISSING_TAG" };
TestAudioFileParser parser{ params };
TestTagReader testTags{
{
{ TagType::AcoustID, { "e987a441-e134-4960-8019-274eddacc418" } },
@@ -88,9 +99,7 @@ namespace lms::metadata
{ "RoleB", { "MyPerformer1ForRoleB", "MyPerformer2ForRoleB" } } });
testTags.setLyricsTags({ { "eng", "[00:00.00]First line\n[00:01.00]Second line" } });
static_cast<IParser&>(parser).setUserExtraTags(std::vector<std::string>{ "MY_AWESOME_TAG_A", "MY_AWESOME_TAG_B", "MY_AWESOME_MISSING_TAG" });
const std::unique_ptr<Track> track{ parser.parse(testTags) };
const std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
// Audio properties
{
@@ -130,7 +139,6 @@ namespace lms::metadata
EXPECT_EQ(track->date.getYear(), 2020);
EXPECT_EQ(track->date.getMonth(), 3);
EXPECT_EQ(track->date.getDay(), 4);
EXPECT_FALSE(track->hasCover);
ASSERT_EQ(track->genres.size(), 2);
EXPECT_EQ(track->genres[0], "Genre1");
EXPECT_EQ(track->genres[1], "Genre2");
@@ -234,7 +242,7 @@ namespace lms::metadata
}
}
TEST(Parser, trim)
TEST(AudioFileParser, trim)
{
const TestTagReader testTags{
{
@@ -242,7 +250,7 @@ namespace lms::metadata
}
};
std::unique_ptr<Track> track{ Parser{}.parse(testTags) };
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
ASSERT_EQ(track->genres.size(), 3);
EXPECT_EQ(track->genres[0], "Genre1");
@@ -250,7 +258,7 @@ namespace lms::metadata
EXPECT_EQ(track->genres[2], "Genre3");
}
TEST(Parser, customDelimiters)
TEST(AudioFileParser, customDelimiters)
{
const TestTagReader testTags{
{
@@ -259,14 +267,14 @@ namespace lms::metadata
{ TagType::Artist, { " Artist1 / Artist2 feat. Artist3 " } },
{ TagType::Genre, { "Genre1 ; Genre2" } },
{ TagType::Language, { " Lang1/Lang2 / Lang3" } },
}
};
Parser parser;
static_cast<IParser&>(parser).setDefaultTagDelimiters(std::vector<std::string>{ " ; ", "/" });
static_cast<IParser&>(parser).setArtistTagDelimiters(std::vector<std::string>{ " / ", " feat. " });
std::unique_ptr<Track> track{ parser.parse(testTags) };
AudioFileParserParameters params;
params.defaultTagDelimiters = { " ; ", "/" };
params.artistTagDelimiters = { " / ", " feat. " };
TestAudioFileParser parser{ params };
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
ASSERT_EQ(track->artists.size(), 3);
EXPECT_EQ(track->artists[0].name, "Artist1");
@@ -292,7 +300,7 @@ namespace lms::metadata
EXPECT_EQ(track->medium->release->artistDisplayName, "AlbumArtist1, AlbumArtist2");
}
TEST(Parser, customDelimiters_foundInArtist)
TEST(AudioFileParser, customDelimiters_foundInArtist)
{
const TestTagReader testTags{
{
@@ -301,10 +309,11 @@ namespace lms::metadata
}
};
Parser parser;
static_cast<IParser&>(parser).setArtistTagDelimiters(std::vector<std::string>{ "; " });
AudioFileParserParameters params;
params.artistTagDelimiters = { "; " };
TestAudioFileParser parser{ params };
std::unique_ptr<Track> track{ parser.parse(testTags) };
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
ASSERT_EQ(track->artists.size(), 2);
EXPECT_EQ(track->artists[0].name, "Artist1");
@@ -312,7 +321,7 @@ namespace lms::metadata
EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2"); // reconstruct the display name since we hit a custom delimiter in Artist
}
TEST(Parser, customDelimiters_foundInArtists)
TEST(AudioFileParser, customDelimiters_foundInArtists)
{
const TestTagReader testTags{
{
@@ -321,10 +330,11 @@ namespace lms::metadata
}
};
Parser parser;
static_cast<IParser&>(parser).setArtistTagDelimiters(std::vector<std::string>{ "; " });
AudioFileParserParameters params;
params.artistTagDelimiters = { "; " };
TestAudioFileParser parser{ params };
std::unique_ptr<Track> track{ parser.parse(testTags) };
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
ASSERT_EQ(track->artists.size(), 2);
EXPECT_EQ(track->artists[0].name, "Artist1");
@@ -332,7 +342,7 @@ namespace lms::metadata
EXPECT_EQ(track->artistDisplayName, "Artist1 feat. Artist2");
}
TEST(Parser, customDelimiters_notUsed)
TEST(AudioFileParser, customDelimiters_notUsed)
{
const TestTagReader testTags{
{
@@ -341,10 +351,11 @@ namespace lms::metadata
}
};
Parser parser;
static_cast<IParser&>(parser).setArtistTagDelimiters(std::vector<std::string>{ "; " });
AudioFileParserParameters params;
params.artistTagDelimiters = { "; " };
TestAudioFileParser parser{ params };
std::unique_ptr<Track> track{ parser.parse(testTags) };
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
ASSERT_EQ(track->artists.size(), 2);
EXPECT_EQ(track->artists[0].name, "Artist1");
@@ -352,7 +363,7 @@ namespace lms::metadata
EXPECT_EQ(track->artistDisplayName, "Artist1 & Artist2");
}
TEST(Parser, customDelimiters_onlyInArtist)
TEST(AudioFileParser, customDelimiters_onlyInArtist)
{
const TestTagReader testTags{
{
@@ -360,10 +371,11 @@ namespace lms::metadata
}
};
Parser parser;
static_cast<IParser&>(parser).setArtistTagDelimiters(std::vector<std::string>{ " & " });
AudioFileParserParameters params;
params.artistTagDelimiters = { " & " };
TestAudioFileParser parser{ params };
std::unique_ptr<Track> track{ parser.parse(testTags) };
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
ASSERT_EQ(track->artists.size(), 2);
EXPECT_EQ(track->artists[0].name, "Artist1");
@@ -371,7 +383,7 @@ namespace lms::metadata
EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2"); // reconstructed since a custom delimiter was hit for parsing
}
TEST(Parser, customDelimitersUsedForArtists)
TEST(AudioFileParser, customDelimitersUsedForArtists)
{
const TestTagReader testTags{
{
@@ -379,10 +391,11 @@ namespace lms::metadata
}
};
Parser parser;
static_cast<IParser&>(parser).setArtistTagDelimiters(std::vector<std::string>{ " & " });
AudioFileParserParameters params;
params.artistTagDelimiters = { " & " };
TestAudioFileParser parser{ params };
std::unique_ptr<Track> track{ parser.parse(testTags) };
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
ASSERT_EQ(track->artists.size(), 2);
EXPECT_EQ(track->artists[0].name, "Artist1");
@@ -390,7 +403,7 @@ namespace lms::metadata
EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2"); // reconstructed since a custom delimiter was hit for parsing
}
TEST(Parser, noArtistInArtist)
TEST(AudioFileParser, noArtistInArtist)
{
const TestTagReader testTags{
{
@@ -398,13 +411,13 @@ namespace lms::metadata
}
};
std::unique_ptr<Track> track{ Parser{}.parse(testTags) };
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
ASSERT_EQ(track->artists.size(), 0);
EXPECT_EQ(track->artistDisplayName, "");
}
TEST(Parser, singleArtistInArtists)
TEST(AudioFileParser, singleArtistInArtists)
{
const TestTagReader testTags{
{
@@ -413,14 +426,14 @@ namespace lms::metadata
}
};
std::unique_ptr<Track> track{ Parser{}.parse(testTags) };
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
ASSERT_EQ(track->artists.size(), 1);
EXPECT_EQ(track->artists[0].name, "Artist1");
EXPECT_EQ(track->artistDisplayName, "Artist1");
}
TEST(Parser, multipleArtistsInArtist)
TEST(AudioFileParser, multipleArtistsInArtist)
{
const TestTagReader testTags{
{
@@ -429,7 +442,7 @@ namespace lms::metadata
}
};
std::unique_ptr<Track> track{ Parser{}.parse(testTags) };
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
ASSERT_EQ(track->artists.size(), 2);
EXPECT_EQ(track->artists[0].name, "Artist1");
@@ -437,7 +450,7 @@ namespace lms::metadata
EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2"); // reconstruct artist display name since multiple entries are found
}
TEST(Parser, multipleArtistsInArtists)
TEST(AudioFileParser, multipleArtistsInArtists)
{
const TestTagReader testTags{
{
@@ -446,7 +459,7 @@ namespace lms::metadata
}
};
std::unique_ptr<Track> track{ Parser{}.parse(testTags) };
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
ASSERT_EQ(track->artists.size(), 2);
EXPECT_EQ(track->artists[0].name, "Artist1");
@@ -454,7 +467,7 @@ namespace lms::metadata
EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2"); // reconstruct artist display name since multiple entries are found and nothing is set in artist
}
TEST(Parser, multipleArtistsInArtistsWithEndDelimiter)
TEST(AudioFileParser, multipleArtistsInArtistsWithEndDelimiter)
{
const TestTagReader testTags{
{
@@ -463,7 +476,7 @@ namespace lms::metadata
}
};
std::unique_ptr<Track> track{ Parser{}.parse(testTags) };
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
ASSERT_EQ(track->artists.size(), 2);
EXPECT_EQ(track->artists[0].name, "Artist1");
@@ -471,7 +484,7 @@ namespace lms::metadata
EXPECT_EQ(track->artistDisplayName, "Artist1 & (CV. Artist2)");
}
TEST(Parser, singleArtistInAlbumArtists)
TEST(AudioFileParser, singleArtistInAlbumArtists)
{
const TestTagReader testTags{
{
@@ -481,7 +494,7 @@ namespace lms::metadata
}
};
std::unique_ptr<Track> track{ Parser{}.parse(testTags) };
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
ASSERT_TRUE(track->medium);
ASSERT_TRUE(track->medium->release);
@@ -490,7 +503,7 @@ namespace lms::metadata
EXPECT_EQ(track->medium->release->artistDisplayName, "Artist1");
}
TEST(Parser, multipleArtistsInAlbumArtist)
TEST(AudioFileParser, multipleArtistsInAlbumArtist)
{
const TestTagReader testTags{
{
@@ -500,7 +513,7 @@ namespace lms::metadata
}
};
std::unique_ptr<Track> track{ Parser{}.parse(testTags) };
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
ASSERT_TRUE(track->medium);
ASSERT_TRUE(track->medium->release);
@@ -510,7 +523,7 @@ namespace lms::metadata
EXPECT_EQ(track->medium->release->artistDisplayName, "Artist1, Artist2"); // reconstruct artist display name since multiple entries are found
}
TEST(Parser, multipleArtistsInAlbumArtists_displayName)
TEST(AudioFileParser, multipleArtistsInAlbumArtists_displayName)
{
const TestTagReader testTags{
{
@@ -520,7 +533,7 @@ namespace lms::metadata
}
};
std::unique_ptr<Track> track{ Parser{}.parse(testTags) };
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
ASSERT_TRUE(track->medium);
ASSERT_TRUE(track->medium->release);
@@ -530,7 +543,7 @@ namespace lms::metadata
EXPECT_EQ(track->medium->release->artistDisplayName, "Artist1 & Artist2");
}
TEST(Parser, multipleArtistsInAlbumArtists)
TEST(AudioFileParser, multipleArtistsInAlbumArtists)
{
const TestTagReader testTags{
{
@@ -540,7 +553,7 @@ namespace lms::metadata
}
};
std::unique_ptr<Track> track{ Parser{}.parse(testTags) };
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
ASSERT_TRUE(track->medium);
ASSERT_TRUE(track->medium->release);
@@ -550,7 +563,7 @@ namespace lms::metadata
EXPECT_EQ(track->medium->release->artistDisplayName, "Artist1, Artist2"); // reconstruct artist display name since multiple entries are found and nothing is set in artist
}
TEST(Parser, multipleArtistsInArtistsButNotAllMBIDs)
TEST(AudioFileParser, multipleArtistsInArtistsButNotAllMBIDs)
{
const TestTagReader testTags{
{
@@ -560,7 +573,7 @@ namespace lms::metadata
}
};
std::unique_ptr<Track> track{ Parser{}.parse(testTags) };
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
ASSERT_EQ(track->artists.size(), 2);
EXPECT_EQ(track->artists[0].name, "Artist1");
@@ -570,7 +583,7 @@ namespace lms::metadata
EXPECT_EQ(track->artistDisplayName, "Artist1 & Artist2");
}
TEST(Parser, multipleArtistsInArtistsButNotAllMBIDs_customDelimiters)
TEST(AudioFileParser, multipleArtistsInArtistsButNotAllMBIDs_customDelimiters)
{
const TestTagReader testTags{
{
@@ -579,9 +592,10 @@ namespace lms::metadata
}
};
Parser parser;
static_cast<IParser&>(parser).setArtistTagDelimiters(std::vector<std::string>{ " / " });
std::unique_ptr<Track> track{ parser.parse(testTags) };
AudioFileParserParameters params;
params.artistTagDelimiters = { " / " };
TestAudioFileParser parser{ params };
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
ASSERT_EQ(track->artists.size(), 2);
EXPECT_EQ(track->artists[0].name, "Artist1");
@@ -591,7 +605,7 @@ namespace lms::metadata
EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2"); // reconstruct the artist display name
}
TEST(Parser, MBIDs_fallback)
TEST(AudioFileParser, MBIDs_fallback)
{
TestTagReader testTags{
{
@@ -611,7 +625,7 @@ namespace lms::metadata
testTags.setPerformersTags({ { "RoleA", { "Artist1", "Artist3" } },
{ "RoleB", { "Artist2", "Artist4" } } });
std::unique_ptr<Track> track{ Parser{}.parse(testTags) };
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
ASSERT_EQ(track->artists.size(), 2);
EXPECT_EQ(track->artists[0].name, "Artist1");
@@ -696,7 +710,7 @@ namespace lms::metadata
EXPECT_EQ(track->performerArtists["Roleb"][1].mbid.value(), core::UUID::fromString("6fc64a4b-26f5-441f-993c-fd511290233b"));
}
TEST(Parser, MBIDs_fallback_priority)
TEST(AudioFileParser, MBIDs_fallback_priority)
{
const TestTagReader testTags{
{
@@ -708,7 +722,7 @@ namespace lms::metadata
{ TagType::Composer, { "Artist1" } },
}
};
std::unique_ptr<Track> track{ Parser{}.parse(testTags) };
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
ASSERT_EQ(track->composerArtists.size(), 1);
EXPECT_EQ(track->composerArtists[0].name, "Artist1");
@@ -716,7 +730,7 @@ namespace lms::metadata
EXPECT_EQ(track->composerArtists[0].mbid.value(), core::UUID::fromString("6643f584-5edc-45ce-927d-0a4ab25c2673"));
}
TEST(Parser, release_sortNameFallback)
TEST(AudioFileParser, release_sortNameFallback)
{
const TestTagReader testTags{
{
@@ -724,14 +738,14 @@ namespace lms::metadata
// No AlbumSortOrder
}
};
std::unique_ptr<Track> track{ Parser{}.parse(testTags) };
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
ASSERT_TRUE(track->medium.has_value());
ASSERT_TRUE(track->medium->release.has_value());
EXPECT_EQ(track->medium->release->sortName, "MyAlbum");
}
TEST(Parser, advisory)
TEST(AudioFileParser, advisory)
{
auto doTest = [](std::string_view value, std::optional<Track::Advisory> expectedValue) {
const TestTagReader testTags{
@@ -740,8 +754,8 @@ namespace lms::metadata
}
};
Parser parser;
std::unique_ptr<Track> track{ Parser{}.parse(testTags) };
AudioFileParser parser;
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
ASSERT_EQ(track->advisory.has_value(), expectedValue.has_value()) << "Value = '" << value << "'";
if (track->advisory.has_value())
@@ -758,7 +772,7 @@ namespace lms::metadata
doTest("3", std::nullopt);
}
TEST(Parser, encodingTime)
TEST(AudioFileParser, encodingTime)
{
auto doTest = [](std::string_view value, core::PartialDateTime expectedValue) {
const TestTagReader testTags{
@@ -767,8 +781,7 @@ namespace lms::metadata
}
};
Parser parser;
std::unique_ptr<Track> track{ Parser{}.parse(testTags) };
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
ASSERT_EQ(track->encodingTime, expectedValue) << "Value = '" << value << "'";
};
@@ -780,7 +793,7 @@ namespace lms::metadata
doTest("2020/01/03", core::PartialDateTime{ 2020, 01, 03 });
}
TEST(Parser, date)
TEST(AudioFileParser, date)
{
auto doTest = [](std::string_view value, core::PartialDateTime expectedValue) {
const TestTagReader testTags{
@@ -789,8 +802,7 @@ namespace lms::metadata
}
};
Parser parser;
std::unique_ptr<Track> track{ Parser{}.parse(testTags) };
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
ASSERT_EQ(track->date, expectedValue) << "Value = '" << value << "'";
};
+2 -1
View File
@@ -1,9 +1,10 @@
include(GoogleTest)
add_executable(test-metadata
ArtistInfo.cpp
Lyrics.cpp
Metadata.cpp
Parser.cpp
AudioFileParser.cpp
PlayList.cpp
Utils.cpp
)

Some files were not shown because too many files have changed in this diff Show More