Merge branch 'develop' for release v3.65.0
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -23,7 +23,8 @@ ARG LMS_BUILD_PACKAGES=" \
|
||||
taglib-dev \
|
||||
stb \
|
||||
wt-dev \
|
||||
gtest-dev"
|
||||
gtest-dev \
|
||||
xxhash-dev"
|
||||
|
||||
COPY --from=xx / /
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -36,6 +36,7 @@ The following extra fields are implemented:
|
||||
* `recordLabels`
|
||||
* `releaseTypes`
|
||||
* `userRating`
|
||||
* `version`
|
||||
* `Child` response:
|
||||
* `albumArtists`
|
||||
* `artists`
|
||||
|
||||
+24
-16
@@ -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}
|
||||
|
||||
@@ -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,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 |
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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">
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -11,6 +11,7 @@ add_executable(test-core
|
||||
TraceLogger.cpp
|
||||
Utils.cpp
|
||||
UUID.cpp
|
||||
XxHash3.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(test-core PRIVATE
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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{};
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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)");
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()) })
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
+8
-19
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
|
||||
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&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.</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 << "'";
|
||||
};
|
||||
@@ -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
Reference in New Issue
Block a user