Merge branch 'develop' for release v3.66.0
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
name: Build (alpine)
|
||||
on: [push, pull_request]
|
||||
name: Multi-platform Builds (Alpine)
|
||||
on: [pull_request]
|
||||
jobs:
|
||||
Build:
|
||||
strategy:
|
||||
@@ -20,9 +20,9 @@ jobs:
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build (alpine)
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: ./
|
||||
@@ -32,4 +32,4 @@ jobs:
|
||||
push: false
|
||||
cache-from: type=local,src=/tmp/.buildx-cache
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache
|
||||
platforms: linux/amd64,linux/arm/v6
|
||||
platforms: linux/amd64,linux/arm64,linux/arm/v6
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
name: All-option Builds (Arch)
|
||||
on: [pull_request]
|
||||
jobs:
|
||||
Build:
|
||||
strategy:
|
||||
matrix:
|
||||
LMS_BUILD_TYPE: [Release, Debug]
|
||||
LMS_UNITY_BUILD: [ON, OFF]
|
||||
LMS_IMAGE_BACKEND: [stb, graphicsmagick]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check Out Repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-buildx-
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: ./
|
||||
file: ./Dockerfile-build-arch
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
build-args: |
|
||||
LMS_BUILD_TYPE=${{ matrix.LMS_BUILD_TYPE }}
|
||||
LMS_UNITY_BUILD=${{ matrix.LMS_UNITY_BUILD }}
|
||||
LMS_IMAGE_BACKEND=${{ matrix.LMS_IMAGE_BACKEND }}
|
||||
push: false
|
||||
cache-from: type=local,src=/tmp/.buildx-cache
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache
|
||||
@@ -1,10 +1,7 @@
|
||||
name: Build (arch)
|
||||
on: [push, pull_request]
|
||||
name: Basic Build (Arch)
|
||||
on: [push]
|
||||
jobs:
|
||||
Build:
|
||||
strategy:
|
||||
matrix:
|
||||
BUILD_TYPE: [Release, Debug]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check Out Repo
|
||||
@@ -20,7 +17,7 @@ jobs:
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v3
|
||||
@@ -28,7 +25,10 @@ jobs:
|
||||
context: ./
|
||||
file: ./Dockerfile-build-arch
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
build-args: LMS_BUILD_TYPE=${{ matrix.BUILD_TYPE }}
|
||||
build-args: |
|
||||
LMS_BUILD_TYPE=Release
|
||||
LMS_UNITY_BUILD=ON
|
||||
LMS_IMAGE_BACKEND=stb
|
||||
push: false
|
||||
cache-from: type=local,src=/tmp/.buildx-cache
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache
|
||||
@@ -1,8 +1,6 @@
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master", "develop" ]
|
||||
pull_request:
|
||||
branches: [ "master", "develop" ]
|
||||
schedule:
|
||||
@@ -42,24 +40,24 @@ jobs:
|
||||
popd
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: +security-and-quality
|
||||
|
||||
- if: matrix.language == 'javascript'
|
||||
name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
|
||||
- if: matrix.language == 'cpp'
|
||||
name: Build
|
||||
run: |
|
||||
mkdir -p build
|
||||
cd build
|
||||
cmake -DCMAKE_VERBOSE_MAKEFILE=ON -DCMAKE_BUILD_TYPE=Release ..
|
||||
cmake -DCMAKE_VERBOSE_MAKEFILE=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_UNITY_BUILD=ON ..
|
||||
make -j$(nproc)
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: "/language:${{ matrix.language }}"
|
||||
|
||||
@@ -37,7 +37,7 @@ ARG LMS_BUILD_TYPE="Release"
|
||||
RUN \
|
||||
DIR=/tmp/lms/build && mkdir -p ${DIR} && cd ${DIR} && \
|
||||
xx-info is-cross && export BUILD_TESTS=OFF || export BUILD_TESTS=ON && \
|
||||
PKG_CONFIG_PATH=/$(xx-info)/usr/lib/pkgconfig cmake /tmp/lms/ -DCMAKE_INCLUDE_PATH=/$(xx-info)/usr/include -DCMAKE_BUILD_TYPE=${LMS_BUILD_TYPE} $(xx-clang --print-cmake-defines) -DCMAKE_PREFIX_PATH=/$(xx-info)/usr/lib/cmake -DBUILD_TESTING=${BUILD_TESTS} -DBUILD_BENCHMARKS=ON && \
|
||||
PKG_CONFIG_PATH=/$(xx-info)/usr/lib/pkgconfig cmake /tmp/lms/ -DCMAKE_INCLUDE_PATH=/$(xx-info)/usr/include -DCMAKE_UNITY_BUILD=ON -DCMAKE_BUILD_TYPE=${LMS_BUILD_TYPE} $(xx-clang --print-cmake-defines) -DCMAKE_PREFIX_PATH=/$(xx-info)/usr/lib/cmake -DBUILD_TESTING=${BUILD_TESTS} -DBUILD_BENCHMARKS=ON && \
|
||||
VERBOSE=1 make -j$(nproc) && \
|
||||
xx-verify src/lms/lms && \
|
||||
(xx-info is-cross || make test)
|
||||
|
||||
+13
-8
@@ -12,18 +12,23 @@ ARG BUILD_PACKAGES="\
|
||||
libconfig \
|
||||
make \
|
||||
pkgconfig \
|
||||
stb \
|
||||
taglib \
|
||||
wt \
|
||||
xxhash"
|
||||
|
||||
RUN pacman -Syu --noconfirm
|
||||
RUN pacman -S --noconfirm ${BUILD_PACKAGES}
|
||||
RUN pacman -Syu --noconfirm ${BUILD_PACKAGES} && \
|
||||
pacman -Scc --noconfirm && \
|
||||
rm -rf /var/cache/pacman/pkg/*
|
||||
|
||||
# LMS
|
||||
COPY . /tmp/lms/
|
||||
ARG LMS_BUILD_TYPE="Release"
|
||||
RUN \
|
||||
DIR=/tmp/lms/build && mkdir -p ${DIR} && cd ${DIR} && \
|
||||
cmake /tmp/lms/ -DCMAKE_BUILD_TYPE=${LMS_BUILD_TYPE} -DCMAKE_INSTALL_PREFIX=/usr -DLMS_IMAGE_BACKEND=graphicsmagick -DBUILD_BENCHMARKS=ON && \
|
||||
VERBOSE=1 make -j$(nproc) && \
|
||||
make test
|
||||
RUN mkdir -p /tmp/lms/build
|
||||
WORKDIR /tmp/lms/build
|
||||
|
||||
ARG LMS_BUILD_TYPE=Release
|
||||
ARG LMS_UNITY_BUILD=ON
|
||||
ARG LMS_IMAGE_BACKEND=stb
|
||||
RUN cmake .. -DCMAKE_BUILD_TYPE=${LMS_BUILD_TYPE} -DCMAKE_UNITY_BUILD=${LMS_UNITY_BUILD} -DLMS_IMAGE_BACKEND=${LMS_IMAGE_BACKEND} -DBUILD_BENCHMARKS=ON
|
||||
RUN VERBOSE=1 make -j$(nproc)
|
||||
RUN make test
|
||||
|
||||
+1
-1
@@ -116,7 +116,7 @@ RUN \
|
||||
COPY . /tmp/lms/
|
||||
RUN \
|
||||
DIR=/tmp/lms/build && mkdir -p ${DIR} && cd ${DIR} && \
|
||||
PKG_CONFIG_PATH=/tmp/install/lib/pkgconfig CXXFLAGS="-I${PREFIX}/include" LDFLAGS="-L${PREFIX}/lib -Wl,--rpath-link=${PREFIX}/lib" cmake /tmp/lms/ -DCMAKE_BUILD_TYPE=Release -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=TRUE -DCMAKE_INSTALL_PREFIX=${PREFIX} -DCMAKE_PREFIX_PATH=${PREFIX} && \
|
||||
PKG_CONFIG_PATH=/tmp/install/lib/pkgconfig CXXFLAGS="-I${PREFIX}/include" LDFLAGS="-L${PREFIX}/lib -Wl,--rpath-link=${PREFIX}/lib" cmake /tmp/lms/ -DCMAKE_BUILD_TYPE=Release -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=TRUE -DCMAKE_UNITY_BUILD=ON -DCMAKE_INSTALL_PREFIX=${PREFIX} -DCMAKE_PREFIX_PATH=${PREFIX} && \
|
||||
LD_LIBRARY_PATH=${PREFIX}/lib make -j$(nproc) install && \
|
||||
LD_LIBRARY_PATH=${PREFIX}/lib make test && \
|
||||
mkdir -p ${PREFIX}/etc/ && \
|
||||
|
||||
+2
-3
@@ -51,15 +51,14 @@ git clone https://github.com/epoupon/lms.git lms
|
||||
cd lms
|
||||
mkdir build
|
||||
cd build
|
||||
cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr
|
||||
cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_UNITY_BUILD=ON -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=TRUE -DCMAKE_INSTALL_PREFIX=/usr
|
||||
```
|
||||
__Notes__:
|
||||
* you can customize the installation directory using `-DCMAKE_INSTALL_PREFIX=path` (defaults to `/usr/local`).
|
||||
* you can customize the image library using `-DLMS_IMAGE_BACKEND=<stb|graphicsmagick>` (defaults to `stb`)
|
||||
```sh
|
||||
make
|
||||
make -j$(nproc)
|
||||
```
|
||||
__Note__: you can use `make -jN` to speed up compilation time (N is the number of compilation workers to spawn).
|
||||
### Installation
|
||||
__Note__: the commands of this section require root privileges.
|
||||
```sh
|
||||
|
||||
@@ -39,6 +39,15 @@
|
||||
</label>
|
||||
${artist-tag-delimiter-container class="row gy-3"}
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="${id:artists-to-not-split}">
|
||||
${tr:Lms.Admin.Database.artists-to-not-split}
|
||||
</label>
|
||||
${artists-to-not-split class="form-control"}
|
||||
<div class="invalid-feedback">
|
||||
${artists-to-not-split-info}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">
|
||||
${tr:Lms.Admin.Database.default-tag-delimiters}
|
||||
@@ -58,6 +67,17 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="form-check">
|
||||
${allow-mbid-artist-merge class="form-check-input"}
|
||||
<label class="form-check-label" for="${id:allow-mbid-artist-merge}">
|
||||
${tr:Lms.Admin.Database.allow-mbid-artist-merge}
|
||||
</label>
|
||||
<div class="invalid-feedback">
|
||||
${allow-mbid-artist-merge-info}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="${id:similarity-engine-type}">
|
||||
${tr:Lms.Admin.Database.similarity-engine-type}
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
<plural case="1">{1} tracks</plural>
|
||||
</message>
|
||||
<message id="Lms.user">User</message>
|
||||
<message id="Lms.uuid-invalid">Invalid UUID</message>
|
||||
|
||||
<!--Locale-->
|
||||
<message id="Lms.locale.decimal-point">.</message>
|
||||
@@ -72,7 +73,9 @@
|
||||
<message id="Lms.Admin.MediaLibrary.root-path">Root directory</message>
|
||||
|
||||
<!--Scan settings-->
|
||||
<message id="Lms.Admin.Database.allow-mbid-artist-merge">Allow merging artists without an MBID to those with one</message>
|
||||
<message id="Lms.Admin.Database.artist-tag-delimiters">Delimiters to be used for splitting artist tags</message>
|
||||
<message id="Lms.Admin.Database.artists-to-not-split">Artists to not split using the delimiters (one artist per line)</message>
|
||||
<message id="Lms.Admin.Database.daily">Daily</message>
|
||||
<message id="Lms.Admin.Database.default-tag-delimiters">Delimiters to be used for splitting other tags</message>
|
||||
<message id="Lms.Admin.Database.extra-tags-to-scan">Extra tags to scan</message>
|
||||
@@ -107,7 +110,7 @@
|
||||
<message id="Lms.Admin.ScannerController.duplicates-header">{1} duplicate files:</message>
|
||||
<message id="Lms.Admin.ScannerController.errors-header">{1} errors:</message>
|
||||
<message id="Lms.Admin.ScannerController.force-optimize">Force database optimization</message>
|
||||
<message id="Lms.Admin.ScannerController.full-scan">Rescan all files</message>
|
||||
<message id="Lms.Admin.ScannerController.full-scan">Force scan all files</message>
|
||||
<message id="Lms.Admin.ScannerController.get-report">Get report</message>
|
||||
<message id="Lms.Admin.ScannerController.last-scan">Last scan</message>
|
||||
<message id="Lms.Admin.ScannerController.last-scan-not-available">Not available</message>
|
||||
@@ -133,6 +136,7 @@
|
||||
<message id="Lms.Admin.ScannerController.step-discovering-files">Discovering files: {1} files</message>
|
||||
<message id="Lms.Admin.ScannerController.step-fetching-track-features">Fetching track features from AcousticBrainz: {1}/{2} tracks ({3}%)...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-optimize">Optimizing database... {1}%...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-reconciliate-artists">Reconciliating artists: {1} entries...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">Reloading similarity engine: {1}%...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-removing-orphaned-entries">Removing orphaned entries: {1} entries...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-scanning-files">Scanning files: {1}/{2} ({3}%)...</message>
|
||||
|
||||
@@ -0,0 +1,396 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<messages xmlns:if="Wt.WTemplate.conditions"
|
||||
nplurals="2" plural="n > 1 ? 1 : 0">
|
||||
|
||||
<!--Common-->
|
||||
<message id="Lms.add">Añadir</message>
|
||||
<message id="Lms.administration">Administración</message>
|
||||
<message id="Lms.cancel">Cancelar</message>
|
||||
<message id="Lms.clusters">Etiquetas</message>
|
||||
<message id="Lms.create">Crear</message>
|
||||
<message id="Lms.delete">Eliminar</message>
|
||||
<message id="Lms.discard">Descartar</message>
|
||||
<message id="Lms.edit">Editar</message>
|
||||
<message id="Lms.field-must-be-in-upper-case">Este campo debe estar en mayúsculas</message>
|
||||
<message id="Lms.loading">Cargando...</message>
|
||||
<message id="Lms.login">Usuario</message>
|
||||
<message id="Lms.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> Salir</message>
|
||||
<message id="Lms.ok">Ok</message>
|
||||
<message id="Lms.password">Contraseña</message>
|
||||
<message id="Lms.password-bad-login-combination">Combinación incorrecta de usuario / contraseña</message>
|
||||
<message id="Lms.password-client-throttled">Demasiados intentos de conexión, inténtelo de nuevo mas tarde</message>
|
||||
<message id="Lms.password-confirm">Confirma la contraseña</message>
|
||||
<message id="Lms.password-must-match-login">¡La contraseña debe coincidir con el usuario!</message>
|
||||
<message id="Lms.password-new">Contraseña nueva</message>
|
||||
<message id="Lms.password-old">Contraseña antigua</message>
|
||||
<message id="Lms.password-too-weak">La contraseña es demasiado débil</message>
|
||||
<message id="Lms.passwords-dont-match">Las contraseñas no coinciden</message>
|
||||
<message id="Lms.quit-other-session">Se ha abierto otra sesión. ¿Reabrir la actúal?</message>
|
||||
<message id="Lms.save">Guardar</message>
|
||||
<message id="Lms.settings-saved">¡Ajustes guardados!</message>
|
||||
<message id="Lms.track-count">
|
||||
<plural case="0">{1} pista</plural>
|
||||
<plural case="1">{1} pistas</plural>
|
||||
</message>
|
||||
<message id="Lms.user">Usuario</message>
|
||||
<message id="Lms.uuid-invalid">UUID inválido</message>
|
||||
|
||||
<!--Locale-->
|
||||
<message id="Lms.locale.decimal-point">.</message>
|
||||
<message id="Lms.locale.group-separator"></message>
|
||||
<message id="Lms.locale.date-format">dd/MM/yyyy</message>
|
||||
<message id="Lms.locale.time-format">HH:mm:ss</message>
|
||||
<message id="Lms.locale.date-time-format">dd/MM/yyyy HH:mm:ss</message>
|
||||
|
||||
<!--Errors-->
|
||||
<message id="Lms.Error.artist-not-found">No se ha entrado el artista</message>
|
||||
<message id="Lms.Error.error-occurred">¡Ha ocurrido un error!</message>
|
||||
<message id="Lms.Error.go-home">Volver a la pantalla de bienvenida</message>
|
||||
<message id="Lms.Error.release-not-found">Este álbum no existe</message>
|
||||
<message id="Lms.Error.tracklist-not-found">Lista de reproducción no encontrada</message>
|
||||
<message id="Lms.Error.user-not-allowed">No tiene los permisos para efectuar esta operación</message>
|
||||
<message id="Lms.Error.user-not-found">No existe el usuario</message>
|
||||
|
||||
<!--Administration-->
|
||||
<message id="Lms.Admin.menu-media-libraries"><i class="fa fa-fw fa-database" aria-hidden="true"></i> Bibliotecas</message>
|
||||
<message id="Lms.Admin.menu-scan-settings"><i class="fa fa-fw fa-cogs" aria-hidden="true"></i> Opciones de escaneo</message>
|
||||
<message id="Lms.Admin.menu-scanner"><i class="fa fa-fw fa-wrench" aria-hidden="true"></i> Escanear</message>
|
||||
<message id="Lms.Admin.menu-tracing"><i class="fa fa-fw fa-bar-chart" aria-hidden="true"></i> Traces</message>
|
||||
<message id="Lms.Admin.menu-users"><i class="fa fa-fw fa-users" aria-hidden="true"></i> Usuarios</message>
|
||||
|
||||
<!--MediaLibraries-->
|
||||
<message id="Lms.Admin.MediaLibraries.media-libraries">Bibliotecas musicales</message>
|
||||
<message id="Lms.Admin.MediaLibrary.create-library">Crear una biblioteca</message>
|
||||
<message id="Lms.Admin.MediaLibrary.del-library-confirm">¿Eliminar la biblioteca?</message>
|
||||
<message id="Lms.Admin.MediaLibrary.edit-library">Edición de la biblioteca</message>
|
||||
<message id="Lms.Admin.MediaLibrary.library-created">¡Biblioteca creada!</message>
|
||||
<message id="Lms.Admin.MediaLibrary.library-deleted">¡Biblioteca eliminada!</message>
|
||||
<message id="Lms.Admin.MediaLibrary.name">Nombre de la biblioteca</message>
|
||||
<message id="Lms.Admin.MediaLibrary.name-already-exists">Ya hay una biblioteca con ese nombre</message>
|
||||
<message id="Lms.Admin.MediaLibrary.path-must-be-absolute">La ruta del fichero debe ser absoluta</message>
|
||||
<message id="Lms.Admin.MediaLibrary.path-must-not-overlap">La ruta no debe solapar la de otra biblioteca</message>
|
||||
<message id="Lms.Admin.MediaLibrary.path-must-be-existing-directory">El directorio no existe</message>
|
||||
<message id="Lms.Admin.MediaLibrary.root-path">Ruta raíz</message>
|
||||
|
||||
<!--Scan settings-->
|
||||
<message id="Lms.Admin.Database.allow-mbid-artist-merge">Permitir la fusión de artistas sin MBID con aquellos que tienen uno</message>
|
||||
<message id="Lms.Admin.Database.artist-tag-delimiters">Delimitadores usados para separar las etiquetas de los artistas</message>
|
||||
<message id="Lms.Admin.Database.artists-to-not-split">Artistas que no se deben dividir usando los delimitadores (un artista por línea)</message>
|
||||
<message id="Lms.Admin.Database.daily">Diariamente</message>
|
||||
<message id="Lms.Admin.Database.default-tag-delimiters">Delimitadores usados para separar otras etiquetas</message>
|
||||
<message id="Lms.Admin.Database.extra-tags-to-scan">Etiquetas adicionales a escanear</message>
|
||||
<message id="Lms.Admin.Database.hourly">Cada hora</message>
|
||||
<message id="Lms.Admin.Database.immediate-scan">¡Escanear ahora!</message>
|
||||
<message id="Lms.Admin.Database.misc">Varios</message>
|
||||
<message id="Lms.Admin.Database.monthly">Mensualmente</message>
|
||||
<message id="Lms.Admin.Database.never">Nunca</message>
|
||||
<message id="Lms.Admin.Database.scan-aborted">¡Escaneo interrumpido!</message>
|
||||
<message id="Lms.Admin.Database.scan-complete">Escaneo terminado : {1} ficheros, {2} añadidos, {3} actualizados, {4} eliminados, {5} duplicados, {6} errores</message>
|
||||
<message id="Lms.Admin.Database.scan-launched">¡Escaneo comenzado!</message>
|
||||
<message id="Lms.Admin.Database.scan-settings">Opciones de escaneo</message>
|
||||
<message id="Lms.Admin.Database.similarity-engine-type">Motor de semejanza</message>
|
||||
<message id="Lms.Admin.Database.similarity-engine-type.clusters">Basado en las etiquetas</message>
|
||||
<message id="Lms.Admin.Database.similarity-engine-type.none">Ninguno</message>
|
||||
<message id="Lms.Admin.Database.skip-single-release-playlists">Ignorar las lista de reproducción que contienen pistas del mismo álbum</message>
|
||||
<message id="Lms.Admin.Database.tag-delimiter-must-not-contain-only-spaces">Los delimitadores de las etiquetas no deben consistir solo en espacios</message>
|
||||
<message id="Lms.Admin.Database.tag-parsing">Análisis de las etiquetas</message>
|
||||
<message id="Lms.Admin.Database.update-period">Frecuencia de la actualización</message>
|
||||
<message id="Lms.Admin.Database.update-start-time">Hora de la actualización</message>
|
||||
<message id="Lms.Admin.Database.weekly">Semanalmente</message>
|
||||
|
||||
<!--Scanner Controller-->
|
||||
<message id="Lms.Admin.ScannerController.bad-duration">No ha sido posible obtener la duración de la pista</message>
|
||||
<message id="Lms.Admin.ScannerController.cannot-read-artist-info-file">No ha sido posible analizar el fichero de información sobre el artista</message>
|
||||
<message id="Lms.Admin.ScannerController.cannot-read-audio-file">No ha sido posible analizar el fichero de audio</message>
|
||||
<message id="Lms.Admin.ScannerController.cannot-read-file">No ha sido posible leer el fichero</message>
|
||||
<message id="Lms.Admin.ScannerController.cannot-read-image-file">No ha sido posible analizar del fichero de imagen</message>
|
||||
<message id="Lms.Admin.ScannerController.cannot-read-lyrics-file">No ha sido posible analizar el fichero de letras</message>
|
||||
<message id="Lms.Admin.ScannerController.cannot-read-playlist-file">No ha sido posible analizar el fichero de la lista de distribución</message>
|
||||
<message id="Lms.Admin.ScannerController.compact">Compactar la base de datos. <strong>¡Atención!:</strong> esta operación puede tardar bastante tiempo y la aplicación se bloqueará mientras dura la operación de compactación</message>
|
||||
<message id="Lms.Admin.ScannerController.duplicates-header">{1} ficheros duplicados:</message>
|
||||
<message id="Lms.Admin.ScannerController.errors-header">{1} errores :</message>
|
||||
<message id="Lms.Admin.ScannerController.force-optimize">Forzar la optimización de la base de datos</message>
|
||||
<message id="Lms.Admin.ScannerController.full-scan">Reescanear todos los ficheros</message>
|
||||
<message id="Lms.Admin.ScannerController.get-report">Informe</message>
|
||||
<message id="Lms.Admin.ScannerController.last-scan">Último escaneo</message>
|
||||
<message id="Lms.Admin.ScannerController.last-scan-not-available">No disponible</message>
|
||||
<message id="Lms.Admin.ScannerController.last-scan-status">{1} ficheros analizados en {2} el {3} a las {4} (UTC) - {5} errores, {6} duplicados</message>
|
||||
<message id="Lms.Admin.ScannerController.no-audio-track">No es una pista de audio</message>
|
||||
<message id="Lms.Admin.ScannerController.same-hash">Hash del fichero duplicado</message>
|
||||
<message id="Lms.Admin.ScannerController.same-mbid">MBID de la pista duplicado</message>
|
||||
<message id="Lms.Admin.ScannerController.scan-now">Escanear ahora</message>
|
||||
<message id="Lms.Admin.ScannerController.scan-options">Opciones de escaneo</message>
|
||||
<message id="Lms.Admin.ScannerController.scanner">Escanear</message>
|
||||
<message id="Lms.Admin.ScannerController.status">Estado</message>
|
||||
<message id="Lms.Admin.ScannerController.status-not-scheduled">No progamado</message>
|
||||
<message id="Lms.Admin.ScannerController.status-scheduled">Programado el {1} a las {2} (UTC)</message>
|
||||
<message id="Lms.Admin.ScannerController.status-in-progress">Escaneando: etapa {1}/{2}</message>
|
||||
<message id="Lms.Admin.ScannerController.step-associating-artist-images">Association des images des artistes: {1}%...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-associating-external-lyrics">Association des paroles externes: {1}%...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-associating-playlist-tracks">Association des pistes des listes de lectures: {1}%...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-associating-release-images">Asignando imágenes de los álbumes: {1}%...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">Comprobando ficheros duplicados ... {1} ficheros</message>
|
||||
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Comprobando ficheros eliminados... {1}%</message>
|
||||
<message id="Lms.Admin.ScannerController.step-compact">Compactando la base de datos...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Calculando estadísticas... {1}%</message>
|
||||
<message id="Lms.Admin.ScannerController.step-discovering-files">Descubriendo ficheros: {1} ficheros</message>
|
||||
<message id="Lms.Admin.ScannerController.step-fetching-track-features">Accediendo a las características de la pista desde AcousticBrainz : {1}/{2} ficheros ({3}%)...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-optimize">Optimizando la base de datos... {1}%...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-reconciliate-artists">Reconciliando artistas: {1} entradas...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">Recargando el motor de similitud: {1}%...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-removing-orphaned-entries">Borrando entradas huérfanas: {1} entradas...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-scanning-files">Escaneando ficheros: {1} / {2} ({3}%)...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-status">Estado de las etapas</message>
|
||||
<message id="Lms.Admin.ScannerController.step-updating-library-fields">Actualizando campos de la biblioteca: {1} entradas</message>
|
||||
|
||||
<!--Tracing-->
|
||||
<message id="Lms.Admin.Tracing.export-current-buffer">Exportar las trazas</message>
|
||||
<message id="Lms.Admin.Tracing.tracing">Trazas</message>
|
||||
|
||||
<!--Users-->
|
||||
<message id="Lms.Admin.Users.add">Añadir</message>
|
||||
<message id="Lms.Admin.Users.admin">Administrador</message>
|
||||
<message id="Lms.Admin.Users.del-user-confirm">¿Eliminar el usuario?</message>
|
||||
<message id="Lms.Admin.Users.demo">Demo</message>
|
||||
<message id="Lms.Admin.Users.users">Usuarios</message>
|
||||
|
||||
<!--User-->
|
||||
<message id="Lms.Admin.User.demo-account">Cuenta de demostración</message>
|
||||
<message id="Lms.Admin.User.demo-account-already-exists">¡Ya existe una cuenta de demostración!</message>
|
||||
<message id="Lms.Admin.User.last-login">Última fecha de inicio de sesión</message>
|
||||
<message id="Lms.Admin.User.user-already-exists">¡Ya existe ese usuario!</message>
|
||||
<message id="Lms.Admin.User.user-create">Nuevo usuario</message>
|
||||
<message id="Lms.Admin.User.user-created">¡Nuevo usuario creado!</message>
|
||||
<message id="Lms.Admin.User.user-edit">Usuario '{1}'</message>
|
||||
<message id="Lms.Admin.User.user-updated">¡Se ha actualizado el usuario!</message>
|
||||
|
||||
<!--Wizard-->
|
||||
<message id="Lms.Admin.InitWizard.done">Se ha creado la cuenta de administrador. ¡Recarga la página para continuar!</message>
|
||||
<message id="Lms.Admin.InitWizard.header">Crear cuenta de administrador</message>
|
||||
|
||||
<!--Auth-->
|
||||
<message id="Lms.Auth.remember-me">Recordarme</message>
|
||||
<message id="Lms.Auth.welcome">¡Bienvenido!</message>
|
||||
|
||||
<!--Explore-->
|
||||
<message id="Lms.Explore.add-filter">Añadir filtro</message>
|
||||
<message id="Lms.Explore.all">Todos</message>
|
||||
<message id="Lms.Explore.artists">Artistas</message>
|
||||
<message id="Lms.Explore.bitrate">Bitrate</message>
|
||||
<message id="Lms.Explore.codec">Codec</message>
|
||||
<message id="Lms.Explore.download">Descargar</message>
|
||||
<message id="Lms.Explore.duration">Duración</message>
|
||||
<message id="Lms.Explore.filter-added">Filtro añadido</message>
|
||||
<message id="Lms.Explore.filters">Filtros</message>
|
||||
<message id="Lms.Explore.label">Sello discográfico</message>
|
||||
<message id="Lms.Explore.media-library">Biblioteca musical</message>
|
||||
<message id="Lms.Explore.most-played">Reproducidos frecuentemente</message>
|
||||
<message id="Lms.Explore.musicbrainz-artist">Artista de MusicBrainz</message>
|
||||
<message id="Lms.Explore.musicbrainz-release">Álbum de MusicBrainz</message>
|
||||
<message id="Lms.Explore.play">Reproducir</message>
|
||||
<message id="Lms.Explore.play-last">Reproducir al final</message>
|
||||
<message id="Lms.Explore.play-next">Reproducir a continuación</message>
|
||||
<message id="Lms.Explore.play-shuffled">Reproducción aleatoria</message>
|
||||
<message id="Lms.Explore.playcount">Número de reproducciones</message>
|
||||
<message id="Lms.Explore.random">Aleatorio</message>
|
||||
<message id="Lms.Explore.recently-added">Añadidos recientemente</message>
|
||||
<message id="Lms.Explore.recently-modified">Modificados recientemente</message>
|
||||
<message id="Lms.Explore.recently-played">Reproducidos recientemente</message>
|
||||
<message id="Lms.Explore.release-info">Información sobre el álbum</message>
|
||||
<message id="Lms.Explore.release-type">Tipo de publicación</message>
|
||||
<message id="Lms.Explore.releases">Álbumes</message>
|
||||
<message id="Lms.Explore.search">Buscar</message>
|
||||
<message id="Lms.Explore.star">Añadir a favoritos</message>
|
||||
<message id="Lms.Explore.starred">Favoritos</message>
|
||||
<message id="Lms.Explore.track-info">Información sobre la pista</message>
|
||||
<message id="Lms.Explore.track-lyrics">Letras</message>
|
||||
<message id="Lms.Explore.tracklists">Listas de reproducción</message>
|
||||
<message id="Lms.Explore.tracks">Pistas</message>
|
||||
<message id="Lms.Explore.type">Type</message>
|
||||
<message id="Lms.Explore.unstar">Eliminar de favoritos</message>
|
||||
<message id="Lms.Explore.value">Valor</message>
|
||||
<message id="Lms.Explore.various-artists">Varios artistas</message>
|
||||
|
||||
<!--Explore:Artist-->
|
||||
<message id="Lms.Explore.Artist.appears-on">Aparece en</message>
|
||||
<message id="Lms.Explore.Artist.biography">Biografía</message>
|
||||
<message id="Lms.Explore.Artist.similar-artists">Artistas similares</message>
|
||||
|
||||
<!--Explore:Artists-->
|
||||
<message id="Lms.Explore.Artists.linktype-all">Todos los artistas</message>
|
||||
<message id="Lms.Explore.Artists.linktype-artist">
|
||||
<plural case="0">Artista de la pista</plural>
|
||||
<plural case="1">Artistas de la pista</plural>
|
||||
</message>
|
||||
<message id="Lms.Explore.Artists.linktype-composer">
|
||||
<plural case="0">Compositor</plural>
|
||||
<plural case="1">Compositores</plural>
|
||||
</message>
|
||||
<message id="Lms.Explore.Artists.linktype-conductor">
|
||||
<plural case="0">Director de orquesta</plural>
|
||||
<plural case="1">Directores de orquesta</plural>
|
||||
</message>
|
||||
<message id="Lms.Explore.Artists.linktype-lyricist">
|
||||
<plural case="0">Letrista</plural>
|
||||
<plural case="1">Letristas</plural>
|
||||
</message>
|
||||
<message id="Lms.Explore.Artists.linktype-mixer">
|
||||
<plural case="0">Mezclador</plural>
|
||||
<plural case="1">Mezcladores</plural>
|
||||
</message>
|
||||
<message id="Lms.Explore.Artists.linktype-performer">
|
||||
<plural case="0">Intérprete</plural>
|
||||
<plural case="1">Intérpretes</plural>
|
||||
</message>
|
||||
<message id="Lms.Explore.Artists.linktype-producer">
|
||||
<plural case="0">Productor</plural>
|
||||
<plural case="1">Productores</plural>
|
||||
</message>
|
||||
<message id="Lms.Explore.Artists.linktype-releaseartist">
|
||||
<plural case="0">Artista del álbum</plural>
|
||||
<plural case="1">Artistas del álbum</plural>
|
||||
</message>
|
||||
<message id="Lms.Explore.Artists.linktype-remixer">
|
||||
<plural case="0">Remezclador</plural>
|
||||
<plural case="1">Remezcladores</plural>
|
||||
</message>
|
||||
|
||||
<!--Explore:Release-->
|
||||
<message id="Lms.Explore.Release.copyright">Copyright</message>
|
||||
<message id="Lms.Explore.Release.disc">Disco {1}</message>
|
||||
<message id="Lms.Explore.Release.other-versions">Otras versiones</message>
|
||||
<message id="Lms.Explore.Release.similar-releases">Álbumes similares</message>
|
||||
<message id="Lms.Explore.Release.type">Tipo</message>
|
||||
<message id="Lms.Explore.Release.type-primary-album">Álbum</message>
|
||||
<message id="Lms.Explore.Release.type-primary-broadcast">Difusión</message>
|
||||
<message id="Lms.Explore.Release.type-primary-ep">EP</message>
|
||||
<message id="Lms.Explore.Release.type-primary-other">Otro</message>
|
||||
<message id="Lms.Explore.Release.type-primary-single">Single</message>
|
||||
<message id="Lms.Explore.Release.type-secondary-audiobook">Audiolibro</message>
|
||||
<message id="Lms.Explore.Release.type-secondary-audiodrama">Drama</message>
|
||||
<message id="Lms.Explore.Release.type-secondary-compilation">Recopilatorio</message>
|
||||
<message id="Lms.Explore.Release.type-secondary-demo">Demo</message>
|
||||
<message id="Lms.Explore.Release.type-secondary-djmix">DJ-mix</message>
|
||||
<message id="Lms.Explore.Release.type-secondary-field-recording">Grabación de campo</message>
|
||||
<message id="Lms.Explore.Release.type-secondary-interview">Entrevista</message>
|
||||
<message id="Lms.Explore.Release.type-secondary-live">En vivo</message>
|
||||
<message id="Lms.Explore.Release.type-secondary-mixtape-street">Mixtape/Street</message>
|
||||
<message id="Lms.Explore.Release.type-secondary-remix">Remix</message>
|
||||
<message id="Lms.Explore.Release.type-secondary-soundtrack">Banda sonora</message>
|
||||
<message id="Lms.Explore.Release.type-secondary-spokenword">Creación locutada</message>
|
||||
|
||||
<!--Explore:TrackLists-->
|
||||
<message id="Lms.Explore.TrackLists.del-tracklist-confirm">¿Eliminar la lista de reproducción?</message>
|
||||
<message id="Lms.Explore.TrackLists.type-owned">Mis listas</message>
|
||||
<message id="Lms.Explore.TrackLists.type-shared">Listas compartidas</message>
|
||||
|
||||
<!--Explore:Search-->
|
||||
<message id="Lms.Explore.Search.search-placeholder">Buscar...</message>
|
||||
|
||||
<!--Player-->
|
||||
<message id="Lms.Player.transcoding-active">Transcodificación activa</message>
|
||||
|
||||
<!--Playqueue-->
|
||||
<message id="Lms.PlayQueue.clear">Vaciar la lista</message>
|
||||
<message id="Lms.PlayQueue.create-tracklist">Crear una nueva lista de reproducción</message>
|
||||
<message id="Lms.PlayQueue.playqueue">Lista de reproducción</message>
|
||||
<message id="Lms.PlayQueue.radio-mode">Modo radio</message>
|
||||
<message id="Lms.PlayQueue.repeat">Repetir</message>
|
||||
<message id="Lms.PlayQueue.replace-tracklist">Reemplazar una lista de distribución existente</message>
|
||||
<message id="Lms.PlayQueue.shuffle">Modo aleatorio</message>
|
||||
|
||||
<!--PlayHistory-->
|
||||
<message id="Lms.PlayHistory.playhistory">Histórico de reproducciones</message>
|
||||
|
||||
<!--Settings-->
|
||||
<message id="Lms.Settings.artist-release-sort-method">Método para ordenar los álbumes del artista</message>
|
||||
<message id="Lms.Settings.audio">Audio</message>
|
||||
<message id="Lms.Settings.audio-settings-are-local">Estas opciones de audio son locales a tu navegador</message>
|
||||
<message id="Lms.Settings.backend.internal">Interno</message>
|
||||
<message id="Lms.Settings.backend.listenbrainz">ListenBrainz</message>
|
||||
<message id="Lms.Settings.backend.listenbrainz-token">Token de la API de ListenBrainz</message>
|
||||
<message id="Lms.Settings.change-password">Cambiar contraseña</message>
|
||||
<message id="Lms.Settings.date-asc">Fecha de publicación (Ascendiente)</message>
|
||||
<message id="Lms.Settings.date-desc">Fecha de publicación (Descendiente)</message>
|
||||
<message id="Lms.Settings.default-transcoding-output-bitrate">Bitrate por omisión de transcodificación</message>
|
||||
<message id="Lms.Settings.default-transcoding-output-format">Formato por omisión de transcodificación</message>
|
||||
<message id="Lms.Settings.demo-cannot-save">¡No se puede guardar usando una cuenta de demostración!</message>
|
||||
<message id="Lms.Settings.enable-inline-artist-relationships">Mostrar las relaciones entre artistas dentro de los álbumes</message>
|
||||
<message id="Lms.Settings.enable-transcoding-by-default">Activar por defecto la transcodificación</message>
|
||||
<message id="Lms.Settings.feedback">Feedback</message>
|
||||
<message id="Lms.Settings.inline-artist-relationships">Tipos de relaciones entre artistas a mostrar</message>
|
||||
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Ajustes</message>
|
||||
<message id="Lms.Settings.name">Nombre del álbum</message>
|
||||
<message id="Lms.Settings.original-date-asc">Fecha de publicación original (Ascendiente)</message>
|
||||
<message id="Lms.Settings.original-date-desc">Fecha de publicación original (Descendiente)</message>
|
||||
<message id="Lms.Settings.password-bad">Contraseña incorrecta</message>
|
||||
<message id="Lms.Settings.password-must-fill-old-password">La contraseña antigua debe ser introducida</message>
|
||||
<message id="Lms.Settings.regen-token">Regenerar</message>
|
||||
<message id="Lms.Settings.replaygain-mode">Modo ReplayGain</message>
|
||||
<message id="Lms.Settings.replaygain-mode.none">Sin ReplayGain</message>
|
||||
<message id="Lms.Settings.replaygain-mode.auto">Automático</message>
|
||||
<message id="Lms.Settings.replaygain-mode.track">Pista</message>
|
||||
<message id="Lms.Settings.replaygain-mode.release">Álbum</message>
|
||||
<message id="Lms.Settings.replaygain-preamp">Pre-amplificación ReplayGain</message>
|
||||
<message id="Lms.Settings.replaygain-preamp-no-rg-info">Pre-amplification ReplayGain (si no hay información)</message>
|
||||
<message id="Lms.Settings.scrobbling">Scrobbling</message>
|
||||
<message id="Lms.Settings.services">Servicios</message>
|
||||
<message id="Lms.Settings.settings">Ajustes</message>
|
||||
<message id="Lms.Settings.settings-saved">¡Ajustes guardados!</message>
|
||||
<message id="Lms.Settings.subsonic-artist-list-mode">Modo de listado de artistas</message>
|
||||
<message id="Lms.Settings.subsonic-artist-list-mode.all-artists">Todos los artistas</message>
|
||||
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">Todos los artistas de álbum</message>
|
||||
<message id="Lms.Settings.subsonic-artist-list-mode.track-artists">Todos los artistas de pistas</message>
|
||||
<message id="Lms.Settings.subsonic-api">API Subsonic</message>
|
||||
<message id="Lms.Settings.subsonic-token">Clave para la API de OpenSubsonic</message>
|
||||
<message id="Lms.Settings.subsonic-token-usage">Use esta clave como la contraseña en aquellos clientes que no soporten la extensión 'API Key Authentication'</message>
|
||||
<message id="Lms.Settings.transcoding">Transcodificación</message>
|
||||
<message id="Lms.Settings.transcoding-mode">Aplicar la transcodificación</message>
|
||||
<message id="Lms.Settings.transcoding-mode.always">Siempre</message>
|
||||
<message id="Lms.Settings.transcoding-mode.if-format-not-supported">Únicamente cuando el formato no esté soportado por el navegador</message>
|
||||
<message id="Lms.Settings.transcoding-mode.never">Nunca</message>
|
||||
<message id="Lms.Settings.transcoding-output-bitrate">Bitrate de transcodificación</message>
|
||||
<message id="Lms.Settings.transcoding-output-format">Formato de transcodificación</message>
|
||||
<message id="Lms.Settings.transcoding-output-format.matroska_opus">Matroska/Opus</message>
|
||||
<message id="Lms.Settings.transcoding-output-format.mp3">MP3</message>
|
||||
<message id="Lms.Settings.transcoding-output-format.ogg_opus">Ogg/Opus</message>
|
||||
<message id="Lms.Settings.transcoding-output-format.ogg_vorbis">Ogg/Vorbis</message>
|
||||
<message id="Lms.Settings.transcoding-output-format.webm_vorbis">WebM/Vorbis</message>
|
||||
<message id="Lms.Settings.user-interface">Interfaz de usuario</message>
|
||||
|
||||
<!--Wt-->
|
||||
<message id="Wt.WDateTime.LessThanASecond">menos de uns segundo</message>
|
||||
<message id="Wt.WDateTime.seconds">
|
||||
<plural case="0">un segundo</plural>
|
||||
<plural case="1">{1} segundos</plural>
|
||||
</message>
|
||||
<message id="Wt.WDateTime.minutes">
|
||||
<plural case="0">un minuto</plural>
|
||||
<plural case="1">{1} minutos</plural>
|
||||
</message>
|
||||
<message id="Wt.WDateTime.hours">
|
||||
<plural case="0">una hora</plural>
|
||||
<plural case="1">{1} horas</plural>
|
||||
</message>
|
||||
<message id="Wt.WDateTime.days">
|
||||
<plural case="0">un día</plural>
|
||||
<plural case="1">{1} días</plural>
|
||||
</message>
|
||||
<message id="Wt.WDateTime.weeks">
|
||||
<plural case="0">una semana</plural>
|
||||
<plural case="1">{1} semanas</plural>
|
||||
</message>
|
||||
<message id="Wt.WDateTime.months">
|
||||
<plural case="0">un mes</plural>
|
||||
<plural case="1">{1} meses</plural>
|
||||
</message>
|
||||
<message id="Wt.WDateTime.years">
|
||||
<plural case="0">un año</plural>
|
||||
<plural case="1">{1} años</plural>
|
||||
</message>
|
||||
<message id="Wt.WDateTime.null"></message>
|
||||
<message id="Wt.WMessageBox.Yes">Si</message>
|
||||
<message id="Wt.WMessageBox.No">No</message>
|
||||
<message id="Wt.WValidator.Invalid">Este campo no puede estar vacío</message>
|
||||
<message id="Wt.WDoubleValidator.BadRange">La cifra debe estar comprendida entre {1} y {2}</message>
|
||||
</messages>
|
||||
@@ -33,6 +33,7 @@
|
||||
<plural case="1">{1} pistes</plural>
|
||||
</message>
|
||||
<message id="Lms.user">Utilisateur</message>
|
||||
<message id="Lms.uuid-invalid">UUID invalide</message>
|
||||
|
||||
<!--Locale-->
|
||||
<message id="Lms.locale.decimal-point">,</message>
|
||||
@@ -72,7 +73,9 @@
|
||||
<message id="Lms.Admin.MediaLibrary.root-path">Répertoire racine</message>
|
||||
|
||||
<!--Scan settings-->
|
||||
<message id="Lms.Admin.Database.allow-mbid-artist-merge">Permettre la fusion des artistes sans MBID avec ceux qui en ont un</message>
|
||||
<message id="Lms.Admin.Database.artist-tag-delimiters">Délimiteurs à utiliser pour séparer les tags d'artistes</message>
|
||||
<message id="Lms.Admin.Database.artists-to-not-split">Artistes à ne pas séparer en utilisant les délimiteurs (un artiste par ligne)</message>
|
||||
<message id="Lms.Admin.Database.daily">Tous les jours</message>
|
||||
<message id="Lms.Admin.Database.default-tag-delimiters">Délimiteurs à utiliser pour séparer les autres tags</message>
|
||||
<message id="Lms.Admin.Database.extra-tags-to-scan">Tags supplémentaires à scanner</message>
|
||||
@@ -107,7 +110,7 @@
|
||||
<message id="Lms.Admin.ScannerController.duplicates-header">{1} fichiers dupliqués :</message>
|
||||
<message id="Lms.Admin.ScannerController.errors-header">{1} erreurs :</message>
|
||||
<message id="Lms.Admin.ScannerController.force-optimize">Forcer l'optimisation de la base de données</message>
|
||||
<message id="Lms.Admin.ScannerController.full-scan">Rescanner tous les fichiers</message>
|
||||
<message id="Lms.Admin.ScannerController.full-scan">Forcer le scan de tous les fichiers</message>
|
||||
<message id="Lms.Admin.ScannerController.get-report">Rapport</message>
|
||||
<message id="Lms.Admin.ScannerController.last-scan">Dernier scan</message>
|
||||
<message id="Lms.Admin.ScannerController.last-scan-not-available">Non disponible</message>
|
||||
@@ -133,6 +136,7 @@
|
||||
<message id="Lms.Admin.ScannerController.step-discovering-files">Découverte des fichiers : {1} fichiers</message>
|
||||
<message id="Lms.Admin.ScannerController.step-fetching-track-features">Récupération des métadonnées AcousticBrainz : {1}/{2} fichiers ({3}%)...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-optimize">Optimisation de la base de données... {1}%...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-reconciliate-artists">Reconciliation des artistes: {1} entrées...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">Rechargement du moteur de recommandation : {1}%...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-removing-orphaned-entries">Retrait des entrées orphelines: {1} entrées...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-scanning-files">Scan des fichiers : {1}/{2} ({3}%)...</message>
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
<plural case="1">{1} tracce</plural>
|
||||
</message>
|
||||
<message id="Lms.user">Utente</message>
|
||||
<message id="Lms.uuid-invalid">UUID non valido</message>
|
||||
|
||||
<!--Locale-->
|
||||
<message id="Lms.locale.decimal-point">,</message>
|
||||
@@ -72,7 +73,9 @@
|
||||
<message id="Lms.Admin.MediaLibrary.root-path">Cartella principale</message>
|
||||
|
||||
<!--Scan settings-->
|
||||
<message id="Lms.Admin.Database.allow-mbid-artist-merge">Consentire la fusione degli artisti senza MBID con quelli che ne hanno uno</message>
|
||||
<message id="Lms.Admin.Database.artist-tag-delimiters">Delimitatori da utilizzare per separare i tag degli artisti</message>
|
||||
<message id="Lms.Admin.Database.artists-to-not-split">Artisti da non suddividere utilizzando i delimitatori (un artista per riga)</message>
|
||||
<message id="Lms.Admin.Database.daily">Giornaliera</message>
|
||||
<message id="Lms.Admin.Database.default-tag-delimiters">Delimitatori da utilizzare per separare gli altri tag</message>
|
||||
<message id="Lms.Admin.Database.extra-tags-to-scan">Tag aggiuntivi da scansionare</message>
|
||||
@@ -133,6 +136,7 @@
|
||||
<message id="Lms.Admin.ScannerController.step-discovering-files">File trovati: {1} files</message>
|
||||
<message id="Lms.Admin.ScannerController.step-fetching-track-features">Recupero metadati da AcousticBrainz: {1}/{2} tracce ({3}%)...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-optimize">Ottimizzazione del database... {1}%...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-reconciliate-artists">Riconciliazione artisti: {1} voci...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">Ricarica motore di tracce simili: {1}%...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-removing-orphaned-entries">Rimozione voci orfane: {1} voci...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-scanning-files">Scansione dei file: {1}/{2} ({3}%)...</message>
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
<plural case="2">{1} ścieżek</plural>
|
||||
</message>
|
||||
<message id="Lms.user">Użytkownik</message>
|
||||
<message id="Lms.uuid-invalid">Nieprawidłowy UUID</message>
|
||||
|
||||
<!--Locale-->
|
||||
<message id="Lms.locale.decimal-point">,</message>
|
||||
@@ -73,7 +74,9 @@
|
||||
<message id="Lms.Admin.MediaLibrary.root-path">Katalog główny</message>
|
||||
|
||||
<!--Scan settings-->
|
||||
<message id="Lms.Admin.Database.allow-mbid-artist-merge">Pozwól na łączenie artystów bez MBID z tymi, którzy go mają</message>
|
||||
<message id="Lms.Admin.Database.artist-tag-delimiters">Znaki rozdzielające artystów</message>
|
||||
<message id="Lms.Admin.Database.artists-to-not-split">Artyści, których nie należy dzielić przy użyciu separatorów (jeden artysta na linię)</message>
|
||||
<message id="Lms.Admin.Database.daily">Codziennie</message>
|
||||
<message id="Lms.Admin.Database.default-tag-delimiters">Znaki rozdzielające inne oznaczenia</message>
|
||||
<message id="Lms.Admin.Database.extra-tags-to-scan">Szukaj dodatkowych znaczników</message>
|
||||
@@ -116,7 +119,7 @@
|
||||
<plural case="2">{1} błędów:</plural>
|
||||
</message>
|
||||
<message id="Lms.Admin.ScannerController.force-optimize">Wymuś optymalizację bazy danych</message>
|
||||
<message id="Lms.Admin.ScannerController.full-scan">Ponownie przeskanuj wszystkie pliki</message>
|
||||
<message id="Lms.Admin.ScannerController.full-scan">Wymuś skanowanie wszystkich plików</message>
|
||||
<message id="Lms.Admin.ScannerController.get-report">Pokaż raport</message>
|
||||
<message id="Lms.Admin.ScannerController.last-scan">Ostatnie skanowanie</message>
|
||||
<message id="Lms.Admin.ScannerController.last-scan-not-available">Niedostępne</message>
|
||||
@@ -150,6 +153,7 @@
|
||||
</message>
|
||||
<message id="Lms.Admin.ScannerController.step-fetching-track-features">Pobieranie danych o ścieżce z AcousticBrainz: {1}/{2} ścieżek ({3}%)...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-optimize">Optymalizowanie bazy danych... {1}%...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-reconciliate-artists">Uzgodnianie artystów: {1} wpisów...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">Przeładowywanie silnika podobieństw: {1}%...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-removing-orphaned-entries">Usuwanie osieroconych wpisów: {1} wpisów...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-scanning-files">Skanowanie plików: {1}/{2} ({3}%)...</message>
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
|
||||
<message id="Lms.user">用户</message>
|
||||
|
||||
|
||||
<!--Locale-->
|
||||
<message id="Lms.locale.decimal-point">.</message>
|
||||
<message id="Lms.locale.group-separator">,</message>
|
||||
@@ -73,6 +74,7 @@
|
||||
|
||||
<!--Scan settings-->
|
||||
|
||||
|
||||
<message id="Lms.Admin.Database.daily">每日</message>
|
||||
|
||||
|
||||
@@ -133,6 +135,7 @@
|
||||
<message id="Lms.Admin.ScannerController.step-discovering-files">检索文件中: {1} 文件</message>
|
||||
<message id="Lms.Admin.ScannerController.step-fetching-track-features">从 AcousticBrainz 获取音轨特征: {1}/{2} 音轨 ({3}%)...</message>
|
||||
|
||||
|
||||
<message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">重载相似引擎中 {1}%...</message>
|
||||
|
||||
<message id="Lms.Admin.ScannerController.step-scanning-files">扫描文件中: {1}/{2} 个文件 ({3}%)...</message>
|
||||
@@ -353,6 +356,7 @@
|
||||
<message id="Lms.Settings.transcoding-output-format.ogg_vorbis">Ogg/Vorbis</message>
|
||||
<message id="Lms.Settings.transcoding-output-format.webm_vorbis">WebM/Vorbis</message>
|
||||
|
||||
|
||||
<!--Wt-->
|
||||
<message id="Wt.WMessageBox.Yes">是</message>
|
||||
<message id="Wt.WMessageBox.No">否</message>
|
||||
|
||||
@@ -100,7 +100,7 @@ namespace lms::core
|
||||
};
|
||||
}
|
||||
|
||||
std::string PartialDateTime::toISO8601String() const
|
||||
std::string PartialDateTime::toString() const
|
||||
{
|
||||
if (_precision == Precision::Invalid)
|
||||
return "";
|
||||
|
||||
@@ -471,7 +471,7 @@ namespace lms::core::stringUtils
|
||||
if (dateTime.isValid())
|
||||
{
|
||||
// assume UTC
|
||||
return dateTime.toString("yyyy-MM-ddThh:mm:ss.zzz", false).toUTF8();
|
||||
return dateTime.toString("yyyy-MM-ddThh:mm:ss.zzz", false).toUTF8() + 'Z';
|
||||
}
|
||||
|
||||
return "";
|
||||
@@ -491,6 +491,9 @@ namespace lms::core::stringUtils
|
||||
Wt::WDateTime fromISO8601String(std::string_view dateTime)
|
||||
{
|
||||
// assume UTC
|
||||
if (!dateTime.empty() && dateTime.back() == 'Z')
|
||||
dateTime.remove_suffix(1);
|
||||
|
||||
return Wt::WDateTime::fromString(Wt::WString{ std::string{ dateTime } }, "yyyy-MM-ddThh:mm:ss.zzz");
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ namespace lms::core
|
||||
|
||||
static PartialDateTime fromString(std::string_view str);
|
||||
static PartialDateTime fromWtDateTime(const Wt::WDateTime& dateTime);
|
||||
std::string toISO8601String() const;
|
||||
std::string toString() const;
|
||||
|
||||
constexpr bool isValid() const { return _precision != Precision::Invalid; }
|
||||
|
||||
|
||||
@@ -38,4 +38,7 @@ namespace lms::core
|
||||
private:
|
||||
T _value{};
|
||||
};
|
||||
|
||||
template<typename Tag>
|
||||
using TaggedBool = TaggedType<Tag, bool>;
|
||||
} // namespace lms::core
|
||||
@@ -61,21 +61,21 @@ namespace lms::core::stringUtils::tests
|
||||
|
||||
TEST(PartialDateTime, stringComparison)
|
||||
{
|
||||
EXPECT_EQ((PartialDateTime{ 1992, 3, 27 }.toISO8601String()), (PartialDateTime{ 1992, 3, 27 }.toISO8601String()));
|
||||
EXPECT_EQ((PartialDateTime{ 1992, 3 }.toISO8601String()), (PartialDateTime{ 1992, 3 }.toISO8601String()));
|
||||
EXPECT_EQ(PartialDateTime{ 1992 }.toISO8601String(), PartialDateTime{ 1992 }.toISO8601String());
|
||||
EXPECT_NE((PartialDateTime{ 1992, 3, 27 }.toISO8601String()), (PartialDateTime{ 1992, 3 }.toISO8601String()));
|
||||
EXPECT_NE((PartialDateTime{ 1992, 3, 27 }.toISO8601String()), (PartialDateTime{ 1992 }.toISO8601String()));
|
||||
EXPECT_NE((PartialDateTime{ 1992, 3 }.toISO8601String()), (PartialDateTime{ 1992 }.toISO8601String()));
|
||||
EXPECT_NE((PartialDateTime{ 1992, 3, 27 }.toISO8601String()), (PartialDateTime{ 1992, 3, 28 }.toISO8601String()));
|
||||
EXPECT_NE((PartialDateTime{ 1992, 3, 27 }.toISO8601String()), (PartialDateTime{ 1992, 4, 27 }.toISO8601String()));
|
||||
EXPECT_NE((PartialDateTime{ 1992, 3, 27 }.toISO8601String()), (PartialDateTime{ 1993, 3, 27 }.toISO8601String()));
|
||||
EXPECT_GT((PartialDateTime{ 1993, 3, 28 }.toISO8601String()), (PartialDateTime{ 1993, 3, 27 }.toISO8601String()));
|
||||
EXPECT_GT((PartialDateTime{ 1993, 4 }.toISO8601String()), (PartialDateTime{ 1993, 3, 27 }.toISO8601String()));
|
||||
EXPECT_GT((PartialDateTime{ 1994 }.toISO8601String()), (PartialDateTime{ 1993, 3, 27 }.toISO8601String()));
|
||||
EXPECT_LT((PartialDateTime{ 1993, 3, 27 }.toISO8601String()), (PartialDateTime{ 1993, 3, 28 }.toISO8601String()));
|
||||
EXPECT_LT((PartialDateTime{ 1993, 3, 27 }.toISO8601String()), (PartialDateTime{ 1993, 4 }.toISO8601String()));
|
||||
EXPECT_LT((PartialDateTime{ 1993, 3, 27 }.toISO8601String()), (PartialDateTime{ 1994 }.toISO8601String()));
|
||||
EXPECT_EQ((PartialDateTime{ 1992, 3, 27 }.toString()), (PartialDateTime{ 1992, 3, 27 }.toString()));
|
||||
EXPECT_EQ((PartialDateTime{ 1992, 3 }.toString()), (PartialDateTime{ 1992, 3 }.toString()));
|
||||
EXPECT_EQ(PartialDateTime{ 1992 }.toString(), PartialDateTime{ 1992 }.toString());
|
||||
EXPECT_NE((PartialDateTime{ 1992, 3, 27 }.toString()), (PartialDateTime{ 1992, 3 }.toString()));
|
||||
EXPECT_NE((PartialDateTime{ 1992, 3, 27 }.toString()), (PartialDateTime{ 1992 }.toString()));
|
||||
EXPECT_NE((PartialDateTime{ 1992, 3 }.toString()), (PartialDateTime{ 1992 }.toString()));
|
||||
EXPECT_NE((PartialDateTime{ 1992, 3, 27 }.toString()), (PartialDateTime{ 1992, 3, 28 }.toString()));
|
||||
EXPECT_NE((PartialDateTime{ 1992, 3, 27 }.toString()), (PartialDateTime{ 1992, 4, 27 }.toString()));
|
||||
EXPECT_NE((PartialDateTime{ 1992, 3, 27 }.toString()), (PartialDateTime{ 1993, 3, 27 }.toString()));
|
||||
EXPECT_GT((PartialDateTime{ 1993, 3, 28 }.toString()), (PartialDateTime{ 1993, 3, 27 }.toString()));
|
||||
EXPECT_GT((PartialDateTime{ 1993, 4 }.toString()), (PartialDateTime{ 1993, 3, 27 }.toString()));
|
||||
EXPECT_GT((PartialDateTime{ 1994 }.toString()), (PartialDateTime{ 1993, 3, 27 }.toString()));
|
||||
EXPECT_LT((PartialDateTime{ 1993, 3, 27 }.toString()), (PartialDateTime{ 1993, 3, 28 }.toString()));
|
||||
EXPECT_LT((PartialDateTime{ 1993, 3, 27 }.toString()), (PartialDateTime{ 1993, 4 }.toString()));
|
||||
EXPECT_LT((PartialDateTime{ 1993, 3, 27 }.toString()), (PartialDateTime{ 1994 }.toString()));
|
||||
}
|
||||
|
||||
TEST(PartialDateTime, stringConversions)
|
||||
@@ -120,7 +120,7 @@ namespace lms::core::stringUtils::tests
|
||||
for (const TestCase& test : tests)
|
||||
{
|
||||
const PartialDateTime dateTime{ PartialDateTime::fromString(test.input) };
|
||||
EXPECT_EQ(dateTime.toISO8601String(), test.expectedOutput) << "Input = '" << test.input;
|
||||
EXPECT_EQ(dateTime.toString(), test.expectedOutput) << "Input = '" << test.input;
|
||||
}
|
||||
}
|
||||
} // namespace lms::core::stringUtils::tests
|
||||
|
||||
@@ -316,12 +316,12 @@ namespace lms::core::stringUtils::tests
|
||||
{
|
||||
{
|
||||
const Wt::WDateTime dateTime{ Wt::WDate{ 2020, 01, 03 }, Wt::WTime{ 9, 8, 11, 75 } };
|
||||
EXPECT_EQ(toISO8601String(dateTime), "2020-01-03T09:08:11.075");
|
||||
EXPECT_EQ(toISO8601String(dateTime), "2020-01-03T09:08:11.075Z");
|
||||
}
|
||||
|
||||
{
|
||||
const Wt::WDateTime dateTime{ Wt::WDate{ 2020, 01, 03 } };
|
||||
EXPECT_EQ(toISO8601String(dateTime), "2020-01-03T00:00:00.000");
|
||||
EXPECT_EQ(toISO8601String(dateTime), "2020-01-03T00:00:00.000Z");
|
||||
}
|
||||
|
||||
{
|
||||
@@ -332,6 +332,7 @@ namespace lms::core::stringUtils::tests
|
||||
|
||||
TEST(Stringutils, DateTimeFromString)
|
||||
{
|
||||
EXPECT_EQ(fromISO8601String("2020-01-03T09:08:11.075Z"), (Wt::WDateTime{ Wt::WDate{ 2020, 01, 03 }, Wt::WTime{ 9, 8, 11, 75 } }));
|
||||
EXPECT_EQ(fromISO8601String("2020-01-03T09:08:11.075"), (Wt::WDateTime{ Wt::WDate{ 2020, 01, 03 }, Wt::WTime{ 9, 8, 11, 75 } }));
|
||||
EXPECT_EQ(fromISO8601String("2020-01-03"), Wt::WDateTime{});
|
||||
EXPECT_EQ(fromISO8601String(""), Wt::WDateTime{});
|
||||
|
||||
@@ -5,6 +5,7 @@ add_library(lmsdatabase STATIC
|
||||
impl/Cluster.cpp
|
||||
impl/Db.cpp
|
||||
impl/Directory.cpp
|
||||
impl/IdType.cpp
|
||||
impl/Image.cpp
|
||||
impl/Listen.cpp
|
||||
impl/MediaLibrary.cpp
|
||||
|
||||
@@ -256,30 +256,6 @@ namespace lms::db
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Artist>>("SELECT a FROM artist a").where("a.id = ?").bind(id));
|
||||
}
|
||||
|
||||
bool Artist::exists(Session& session, ArtistId id)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT 1 FROM artist").where("id = ?").bind(id)) == 1;
|
||||
}
|
||||
|
||||
RangeResults<ArtistId> Artist::findOrphanIds(Session& session, std::optional<Range> range)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
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);
|
||||
}
|
||||
|
||||
RangeResults<ArtistId> Artist::findIds(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
@@ -304,6 +280,41 @@ AND NOT EXISTS (
|
||||
utils::forEachQueryRangeResult(query, params.range, func);
|
||||
}
|
||||
|
||||
RangeResults<ArtistId> Artist::findOrphanIds(Session& session, std::optional<Range> range)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
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);
|
||||
}
|
||||
|
||||
bool Artist::exists(Session& session, ArtistId id)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT 1 FROM artist").where("id = ?").bind(id)) == 1;
|
||||
}
|
||||
|
||||
std::optional<core::UUID> Artist::getMBID() const
|
||||
{
|
||||
return core::UUID::fromString(_mbid);
|
||||
}
|
||||
|
||||
bool Artist::hasMBID() const
|
||||
{
|
||||
// TODO optim this
|
||||
return getMBID().has_value();
|
||||
}
|
||||
|
||||
ObjectPtr<Image> Artist::getImage() const
|
||||
{
|
||||
return ObjectPtr<Image>{ _image };
|
||||
|
||||
@@ -70,7 +70,7 @@ namespace lms::db
|
||||
|
||||
void ArtistInfo::find(Session& session, ArtistId id, const std::function<void(const pointer&)>& func)
|
||||
{
|
||||
find(session, id, std::nullopt, std::move(func));
|
||||
find(session, id, std::nullopt, func);
|
||||
}
|
||||
|
||||
void ArtistInfo::find(Session& session, ArtistInfoId& lastRetrievedId, std::size_t count, const std::function<void(const pointer&)>& func)
|
||||
@@ -85,6 +85,45 @@ namespace lms::db
|
||||
});
|
||||
}
|
||||
|
||||
void ArtistInfo::findArtistNameNoLongerMatch(Session& session, 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") };
|
||||
query.join("artist a ON a_i.artist_id = a.id");
|
||||
query.where("a_i.mbid_matched = FALSE");
|
||||
query.where("a_i.name <> a.name");
|
||||
|
||||
utils::applyRange(query, range);
|
||||
utils::forEachQueryResult(query, [&](const pointer& info) {
|
||||
func(info);
|
||||
});
|
||||
}
|
||||
|
||||
void ArtistInfo::findWithArtistNameAmbiguity(Session& session, std::optional<Range> range, bool allowArtistMBIDFallback, 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") };
|
||||
query.join("artist a ON a_i.artist_id = a.id");
|
||||
query.where("a_i.mbid_matched = FALSE");
|
||||
if (!allowArtistMBIDFallback)
|
||||
{
|
||||
query.where("a.mbid <> ''");
|
||||
}
|
||||
else
|
||||
{
|
||||
query.where(R"(
|
||||
(a.mbid <> '' AND EXISTS (SELECT 1 FROM artist a2 WHERE a2.name = a.name AND a2.mbid <> '' AND a2.mbid <> a.mbid))
|
||||
OR (a.mbid = '' AND (SELECT COUNT(*) FROM artist a2 WHERE a2.name = a.name AND a2.mbid <> '') = 1))");
|
||||
}
|
||||
|
||||
utils::applyRange(query, range);
|
||||
utils::forEachQueryResult(query, [&](const pointer& info) {
|
||||
func(info);
|
||||
});
|
||||
}
|
||||
|
||||
Artist::pointer ArtistInfo::getArtist() const
|
||||
{
|
||||
return _artist;
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
#include "database/IdType.hpp"
|
||||
|
||||
#include <cassert>
|
||||
#include <type_traits>
|
||||
|
||||
#include <Wt/Dbo/ptr.h>
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
static_assert(std::is_same_v<IdType::ValueType, Wt::Dbo::dbo_default_traits::IdType>);
|
||||
|
||||
IdType::IdType()
|
||||
: _id{ Wt::Dbo::dbo_default_traits::invalidId() }
|
||||
{
|
||||
}
|
||||
|
||||
IdType::IdType(ValueType id)
|
||||
: _id{ id }
|
||||
{
|
||||
}
|
||||
|
||||
bool IdType::isValid() const
|
||||
{
|
||||
return _id != Wt::Dbo::dbo_default_traits::invalidId();
|
||||
}
|
||||
|
||||
std::string IdType::toString() const
|
||||
{
|
||||
assert(isValid());
|
||||
return std::to_string(_id);
|
||||
}
|
||||
|
||||
} // namespace lms::db
|
||||
@@ -35,7 +35,7 @@ namespace lms::db
|
||||
{
|
||||
namespace
|
||||
{
|
||||
static constexpr Version LMS_DATABASE_VERSION{ 85 };
|
||||
static constexpr Version LMS_DATABASE_VERSION{ 88 };
|
||||
}
|
||||
|
||||
VersionInfo::VersionInfo()
|
||||
@@ -1159,6 +1159,36 @@ FROM tracklist)");
|
||||
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
||||
}
|
||||
|
||||
void migrateFromV85(Session& session)
|
||||
{
|
||||
dropIndexes(session);
|
||||
|
||||
// Artist merging feature
|
||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE scan_settings ADD COLUMN allow_mbid_artist_merge BOLLEAN DEFAULT(false)");
|
||||
|
||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE track_artist_link ADD COLUMN artist_name TEXT NULL DEFAULT('')");
|
||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE track_artist_link ADD COLUMN artist_sort_name TEXT NULL DEFAULT('')");
|
||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE track_artist_link ADD COLUMN artist_mbid_matched BOOLEAN NOT NULL DEFAULT(false)");
|
||||
|
||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE artist_info ADD COLUMN name TEXT NULL DEFAULT('')");
|
||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE artist_info ADD COLUMN sort_name TEXT NULL DEFAULT('')");
|
||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE artist_info ADD COLUMN mbid_matched BOOLEAN NOT NULL DEFAULT(false)");
|
||||
|
||||
// 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 migrateFromV86(Session& session)
|
||||
{
|
||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE scan_settings ADD COLUMN name TEXT NON NULL DEFAULT('')");
|
||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE scan_settings RENAME COLUMN scan_version TO audio_scan_version");
|
||||
}
|
||||
|
||||
void migrateFromV87(Session& session)
|
||||
{
|
||||
utils::executeCommand(*session.getDboSession(), "ALTER TABLE scan_settings ADD COLUMN artists_to_not_split TEXT NON NULL DEFAULT('')");
|
||||
}
|
||||
|
||||
bool doDbMigration(Session& session)
|
||||
{
|
||||
constexpr std::string_view outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
|
||||
@@ -1220,6 +1250,9 @@ FROM tracklist)");
|
||||
{ 82, migrateFromV82 },
|
||||
{ 83, migrateFromV83 },
|
||||
{ 84, migrateFromV84 },
|
||||
{ 85, migrateFromV85 },
|
||||
{ 86, migrateFromV86 },
|
||||
{ 87, migrateFromV87 },
|
||||
};
|
||||
|
||||
bool migrationPerformed{};
|
||||
|
||||
@@ -26,24 +26,33 @@
|
||||
#include "database/Session.hpp"
|
||||
|
||||
#include "Utils.hpp"
|
||||
#include "traits/IdTypeTraits.hpp"
|
||||
#include "traits/StringViewTraits.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
void ScanSettings::init(Session& session)
|
||||
ScanSettings::ScanSettings(std::string_view name)
|
||||
: _name{ name }
|
||||
{
|
||||
session.checkWriteTransaction();
|
||||
|
||||
if (pointer settings{ get(session) })
|
||||
return;
|
||||
|
||||
session.getDboSession()->add(std::make_unique<ScanSettings>());
|
||||
}
|
||||
|
||||
ScanSettings::pointer ScanSettings::get(Session& session)
|
||||
ScanSettings::pointer ScanSettings::create(Session& session, std::string_view name)
|
||||
{
|
||||
return session.getDboSession()->add(std::unique_ptr<ScanSettings>(new ScanSettings{ name }));
|
||||
}
|
||||
|
||||
ScanSettings::pointer ScanSettings::find(Session& session, ScanSettingsId id)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->find<ScanSettings>());
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<ScanSettings>>("SELECT s_s from scan_settings s_s").where("s_s.id = ?").bind(id));
|
||||
}
|
||||
|
||||
ScanSettings::pointer ScanSettings::find(Session& session, std::string_view name)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->find<ScanSettings>().where("name = ?").bind(name));
|
||||
}
|
||||
|
||||
std::vector<std::string_view> ScanSettings::getExtraTagsToScan() const
|
||||
@@ -65,13 +74,19 @@ namespace lms::db
|
||||
return core::stringUtils::splitEscapedStrings(_defaultTagDelimiters, ';', '\\');
|
||||
}
|
||||
|
||||
std::vector<std::string> ScanSettings::getArtistsToNotSplit() const
|
||||
{
|
||||
return core::stringUtils::splitEscapedStrings(_artistsToNotSplit, ';', '\\');
|
||||
}
|
||||
|
||||
void ScanSettings::setExtraTagsToScan(std::span<const std::string_view> extraTags)
|
||||
{
|
||||
std::string newTagsToScan{ core::stringUtils::joinStrings(extraTags, ";") };
|
||||
if (newTagsToScan != _extraTagsToScan)
|
||||
incScanVersion();
|
||||
|
||||
_extraTagsToScan = std::move(newTagsToScan);
|
||||
{
|
||||
_extraTagsToScan.swap(newTagsToScan);
|
||||
incAudioScanVersion();
|
||||
}
|
||||
}
|
||||
|
||||
void ScanSettings::setArtistTagDelimiters(std::span<const std::string_view> delimiters)
|
||||
@@ -80,7 +95,17 @@ namespace lms::db
|
||||
if (tagDelimiters != _artistTagDelimiters)
|
||||
{
|
||||
_artistTagDelimiters.swap(tagDelimiters);
|
||||
incScanVersion();
|
||||
incAudioScanVersion();
|
||||
}
|
||||
}
|
||||
|
||||
void ScanSettings::setArtistsToNotSplit(std::span<const std::string_view> artists)
|
||||
{
|
||||
std::string artistsToNotSplit{ core::stringUtils::escapeAndJoinStrings(artists, ';', '\\') };
|
||||
if (artistsToNotSplit != _artistsToNotSplit)
|
||||
{
|
||||
_artistsToNotSplit.swap(artistsToNotSplit);
|
||||
incAudioScanVersion();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,21 +115,24 @@ namespace lms::db
|
||||
if (tagDelimiters != _defaultTagDelimiters)
|
||||
{
|
||||
_defaultTagDelimiters.swap(tagDelimiters);
|
||||
incScanVersion();
|
||||
incAudioScanVersion();
|
||||
}
|
||||
}
|
||||
|
||||
void ScanSettings::setSkipSingleReleasePlayLists(bool value)
|
||||
{
|
||||
if (_skipSingleReleasePlayLists != value)
|
||||
{
|
||||
_skipSingleReleasePlayLists = value;
|
||||
incScanVersion();
|
||||
}
|
||||
}
|
||||
|
||||
void ScanSettings::incScanVersion()
|
||||
void ScanSettings::setAllowMBIDArtistMerge(bool value)
|
||||
{
|
||||
_scanVersion += 1;
|
||||
if (_allowMBIDArtistMerge != value)
|
||||
_allowMBIDArtistMerge = value;
|
||||
}
|
||||
|
||||
void ScanSettings::incAudioScanVersion()
|
||||
{
|
||||
_audioScanVersion += 1;
|
||||
}
|
||||
} // namespace lms::db
|
||||
|
||||
@@ -180,7 +180,9 @@ namespace lms::db
|
||||
// TODO: move this elsewhere
|
||||
{
|
||||
auto uniqueTransaction{ createWriteTransaction() };
|
||||
ScanSettings::init(*this);
|
||||
|
||||
if (!ScanSettings::find(*this))
|
||||
create<ScanSettings>();
|
||||
}
|
||||
|
||||
return migrationPerformed;
|
||||
@@ -195,13 +197,14 @@ namespace lms::db
|
||||
auto transaction{ createWriteTransaction() };
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_id_idx ON artist(id)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_image_idx ON artist(image_id)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_name_idx ON artist(name)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS artist_name_mbid_idx ON artist(name, mbid)");
|
||||
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 artist_info_mbid_matched_artist_idx ON artist_info(mbid_matched, 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)");
|
||||
@@ -296,8 +299,9 @@ namespace lms::db
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS tracklist_entry_tracklist_track_idx ON tracklist_entry(tracklist_id, track_id)");
|
||||
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_artist_link_artist_idx ON track_artist_link(artist_id)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_artist_link_artist_mbid_matched_artist_idx ON track_artist_link(artist_mbid_matched, artist_id)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_artist_link_artist_track_idx ON track_artist_link(artist_id, track_id)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_artist_link_artist_type_idx ON track_artist_link(artist_id,type)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_artist_link_artist_type_idx ON track_artist_link(artist_id, type)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_artist_link_track_artist_idx ON track_artist_link(track_id, artist_id)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_artist_link_track_type_idx ON track_artist_link(track_id,type)");
|
||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS track_artist_link_type_track_artist_idx ON track_artist_link(type, track_id, artist_id)");
|
||||
|
||||
@@ -55,28 +55,41 @@ namespace lms::db
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TrackArtistLink::TrackArtistLink(ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType)
|
||||
TrackArtistLink::TrackArtistLink(const ObjectPtr<Track>& track, const ObjectPtr<Artist>& artist, TrackArtistLinkType type, std::string_view subType, bool artistMBIDMatched)
|
||||
: _type{ type }
|
||||
, _subType{ subType }
|
||||
, _artistMBIDMatched{ artistMBIDMatched }
|
||||
, _track{ getDboPtr(track) }
|
||||
, _artist{ getDboPtr(artist) }
|
||||
{
|
||||
}
|
||||
|
||||
TrackArtistLink::pointer TrackArtistLink::create(Session& session, ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType)
|
||||
TrackArtistLink::pointer TrackArtistLink::create(Session& session, const ObjectPtr<Track>& track, const ObjectPtr<Artist>& artist, TrackArtistLinkType type, std::string_view subType, bool artistMBIDMatched)
|
||||
{
|
||||
session.checkWriteTransaction();
|
||||
|
||||
TrackArtistLink::pointer res{ session.getDboSession()->add(std::make_unique<TrackArtistLink>(track, artist, type, subType)) };
|
||||
TrackArtistLink::pointer res{ session.getDboSession()->add(std::make_unique<TrackArtistLink>(track, artist, type, subType, artistMBIDMatched)) };
|
||||
session.getDboSession()->flush();
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::size_t TrackArtistLink::getCount(Session& session)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM track_artist_link"));
|
||||
}
|
||||
|
||||
TrackArtistLink::pointer TrackArtistLink::create(Session& session, const ObjectPtr<Track>& track, const ObjectPtr<Artist>& artist, TrackArtistLinkType type, bool artistMBIDMatched)
|
||||
{
|
||||
return create(session, track, artist, type, std::string_view{}, artistMBIDMatched);
|
||||
}
|
||||
|
||||
TrackArtistLink::pointer TrackArtistLink::find(Session& session, TrackArtistLinkId id)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->find<TrackArtistLink>().where("id = ?").bind(id));
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<TrackArtistLink>>("SELECT t_a_l from track_artist_link t_a_l").where("t_a_l.id = ?").bind(id));
|
||||
}
|
||||
|
||||
void TrackArtistLink::find(Session& session, TrackId trackId, const std::function<void(const TrackArtistLink::pointer& link, const ObjectPtr<Artist>& artist)>& func)
|
||||
@@ -113,4 +126,58 @@ namespace lms::db
|
||||
});
|
||||
return res;
|
||||
}
|
||||
|
||||
void TrackArtistLink::findArtistNameNoLongerMatch(Session& session, std::optional<Range> range, const std::function<void(const TrackArtistLink::pointer&)>& func)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession()->query<Wt::Dbo::ptr<TrackArtistLink>>("SELECT t_a_l FROM track_artist_link t_a_l") };
|
||||
query.join("artist a ON t_a_l.artist_id = a.id");
|
||||
query.where("t_a_l.artist_mbid_matched = FALSE");
|
||||
query.where("t_a_l.artist_name <> a.name");
|
||||
|
||||
utils::applyRange(query, range);
|
||||
utils::forEachQueryResult(query, [&](const TrackArtistLink::pointer& link) {
|
||||
func(link);
|
||||
});
|
||||
}
|
||||
|
||||
void TrackArtistLink::findWithArtistNameAmbiguity(Session& session, std::optional<Range> range, bool allowArtistMBIDFallback, const std::function<void(const TrackArtistLink::pointer&)>& func)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession()->query<Wt::Dbo::ptr<TrackArtistLink>>("SELECT t_a_l FROM track_artist_link t_a_l") };
|
||||
query.join("artist a ON t_a_l.artist_id = a.id");
|
||||
query.where("t_a_l.artist_mbid_matched = FALSE");
|
||||
if (!allowArtistMBIDFallback)
|
||||
{
|
||||
query.where("a.mbid <> ''");
|
||||
}
|
||||
else
|
||||
{
|
||||
query.where(R"(
|
||||
(a.mbid <> '' AND EXISTS (SELECT 1 FROM artist a2 WHERE a2.name = a.name AND a2.mbid <> '' AND a2.mbid <> a.mbid))
|
||||
OR (a.mbid = '' AND (SELECT COUNT(*) FROM artist a2 WHERE a2.name = a.name AND a2.mbid <> '') = 1))");
|
||||
}
|
||||
|
||||
utils::applyRange(query, range);
|
||||
utils::forEachQueryResult(query, [&](const TrackArtistLink::pointer& link) {
|
||||
func(link);
|
||||
});
|
||||
}
|
||||
|
||||
void TrackArtistLink::setArtist(ObjectPtr<Artist> artist)
|
||||
{
|
||||
_artist = getDboPtr(artist);
|
||||
}
|
||||
|
||||
void TrackArtistLink::setArtistName(std::string_view artistName)
|
||||
{
|
||||
_artistName = artistName;
|
||||
}
|
||||
|
||||
void TrackArtistLink::setArtistSortName(std::string_view artistSortName)
|
||||
{
|
||||
_artistSortName = artistSortName;
|
||||
}
|
||||
} // namespace lms::db
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace Wt::Dbo
|
||||
if (!dateTime.isValid())
|
||||
statement->bindNull(column);
|
||||
else
|
||||
statement->bind(column, dateTime.toISO8601String());
|
||||
statement->bind(column, dateTime.toString());
|
||||
}
|
||||
|
||||
static bool read(lms::core::PartialDateTime& dateTime, SqlStatement* statement, int column, int size)
|
||||
|
||||
@@ -134,8 +134,10 @@ namespace lms::db
|
||||
// Accessors
|
||||
const std::string& getName() const { return _name; }
|
||||
const std::string& getSortName() const { return _sortName; }
|
||||
std::optional<core::UUID> getMBID() const { return core::UUID::fromString(_mbid); }
|
||||
std::optional<core::UUID> getMBID() const;
|
||||
bool hasMBID() const;
|
||||
ObjectPtr<Image> getImage() const;
|
||||
void visitLinks(std::function<void(const ObjectPtr<TrackArtistLink>& link)> visitor) const;
|
||||
|
||||
// No artistLinkTypes means get them all
|
||||
RangeResults<ArtistId> findSimilarArtistIds(core::EnumSet<TrackArtistLinkType> artistLinkTypes = {}, std::optional<Range> range = std::nullopt) const;
|
||||
|
||||
@@ -51,6 +51,8 @@ namespace lms::db
|
||||
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);
|
||||
static void findArtistNameNoLongerMatch(Session& session, std::optional<Range> range, const std::function<void(const pointer&)>& func);
|
||||
static void findWithArtistNameAmbiguity(Session& session, std::optional<Range> range, bool allowArtistMBIDFallback, const std::function<void(const pointer&)>& func);
|
||||
|
||||
// getters
|
||||
const std::filesystem::path& getAbsoluteFilePath() const { return _absoluteFilePath; }
|
||||
@@ -58,20 +60,26 @@ namespace lms::db
|
||||
ObjectPtr<Directory> getDirectory() const;
|
||||
ObjectPtr<Artist> getArtist() const;
|
||||
DirectoryId getDirectoryId() const { return _directory.id(); }
|
||||
std::string_view getName() const { return _name; }
|
||||
std::string_view getSortName() const { return _name; }
|
||||
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; }
|
||||
bool isMBIDMatched() const { return _MBIDMatched; }
|
||||
|
||||
// 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 setName(std::string_view name) { _name = name; }
|
||||
void setSortName(std::string_view sortName) { _sortName = sortName; }
|
||||
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; };
|
||||
void setMBIDMatched(bool matched) { _MBIDMatched = matched; }
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
@@ -79,11 +87,15 @@ namespace lms::db
|
||||
Wt::Dbo::field(a, _absoluteFilePath, "absolute_file_path");
|
||||
Wt::Dbo::field(a, _fileLastWrite, "file_last_write");
|
||||
|
||||
Wt::Dbo::field(a, _name, "name");
|
||||
Wt::Dbo::field(a, _sortName, "sort_name");
|
||||
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::field(a, _MBIDMatched, "mbid_matched");
|
||||
|
||||
Wt::Dbo::belongsTo(a, _directory, "directory", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
@@ -97,11 +109,18 @@ namespace lms::db
|
||||
std::string _fileStem;
|
||||
Wt::WDateTime _fileLastWrite;
|
||||
|
||||
// this info may be redondant with what found in the linked artist
|
||||
// but we actually need them in case of artist merge/split
|
||||
std::string _name;
|
||||
std::string _sortName;
|
||||
|
||||
std::string _type;
|
||||
std::string _gender;
|
||||
std::string _disambiguation;
|
||||
std::string _biography;
|
||||
|
||||
bool _MBIDMatched{};
|
||||
|
||||
Wt::Dbo::ptr<Directory> _directory;
|
||||
Wt::Dbo::ptr<Artist> _artist;
|
||||
};
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
#include "database/ClusterId.hpp"
|
||||
#include "database/LabelId.hpp"
|
||||
|
||||
@@ -19,9 +19,6 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Wt/Dbo/ptr.h>
|
||||
#include <cassert>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
namespace lms::db
|
||||
@@ -29,24 +26,19 @@ namespace lms::db
|
||||
class IdType
|
||||
{
|
||||
public:
|
||||
using ValueType = Wt::Dbo::dbo_default_traits::IdType;
|
||||
using ValueType = long long;
|
||||
|
||||
IdType() = default;
|
||||
IdType(ValueType id)
|
||||
: _id{ id } {}
|
||||
IdType();
|
||||
IdType(ValueType id);
|
||||
|
||||
bool isValid() const { return _id != Wt::Dbo::dbo_default_traits::invalidId(); }
|
||||
std::string toString() const
|
||||
{
|
||||
assert(isValid());
|
||||
return std::to_string(_id);
|
||||
}
|
||||
bool isValid() const;
|
||||
std::string toString() const;
|
||||
|
||||
ValueType getValue() const { return _id; }
|
||||
auto operator<=>(const IdType& other) const = default;
|
||||
|
||||
private:
|
||||
Wt::Dbo::dbo_default_traits::IdType _id{ Wt::Dbo::dbo_default_traits::invalidId() };
|
||||
ValueType _id;
|
||||
};
|
||||
|
||||
#define LMS_DECLARE_IDTYPE(name) \
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
@@ -58,19 +57,22 @@ namespace lms::db
|
||||
None,
|
||||
};
|
||||
|
||||
static void init(Session& session);
|
||||
ScanSettings() = default;
|
||||
|
||||
static pointer get(Session& session);
|
||||
static pointer find(Session& session, std::string_view name = "");
|
||||
static pointer find(Session& session, ScanSettingsId id);
|
||||
|
||||
// Getters
|
||||
std::size_t getScanVersion() const { return _scanVersion; }
|
||||
std::size_t getAudioScanVersion() const { return _audioScanVersion; }
|
||||
Wt::WTime getUpdateStartTime() const { return _startTime; }
|
||||
UpdatePeriod getUpdatePeriod() const { return _updatePeriod; }
|
||||
std::vector<std::string_view> getExtraTagsToScan() const;
|
||||
SimilarityEngineType getSimilarityEngineType() const { return _similarityEngineType; }
|
||||
std::vector<std::string> getArtistTagDelimiters() const;
|
||||
std::vector<std::string> getDefaultTagDelimiters() const;
|
||||
std::vector<std::string> getArtistsToNotSplit() const;
|
||||
bool getSkipSingleReleasePlayLists() const { return _skipSingleReleasePlayLists; }
|
||||
bool getAllowMBIDArtistMerge() const { return _allowMBIDArtistMerge; }
|
||||
|
||||
// Setters
|
||||
void setUpdateStartTime(Wt::WTime t) { _startTime = t; }
|
||||
@@ -78,31 +80,45 @@ namespace lms::db
|
||||
void setExtraTagsToScan(std::span<const std::string_view> extraTags);
|
||||
void setSimilarityEngineType(SimilarityEngineType type) { _similarityEngineType = type; }
|
||||
void setArtistTagDelimiters(std::span<const std::string_view> delimiters);
|
||||
void setArtistsToNotSplit(std::span<const std::string_view> artists);
|
||||
void setDefaultTagDelimiters(std::span<const std::string_view> delimiters);
|
||||
void setSkipSingleReleasePlayLists(bool value);
|
||||
void incScanVersion();
|
||||
void setAllowMBIDArtistMerge(bool value);
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _scanVersion, "scan_version");
|
||||
Wt::Dbo::field(a, _name, "name");
|
||||
Wt::Dbo::field(a, _audioScanVersion, "audio_scan_version");
|
||||
Wt::Dbo::field(a, _startTime, "start_time");
|
||||
Wt::Dbo::field(a, _updatePeriod, "update_period");
|
||||
Wt::Dbo::field(a, _similarityEngineType, "similarity_engine_type");
|
||||
Wt::Dbo::field(a, _extraTagsToScan, "extra_tags_to_scan");
|
||||
Wt::Dbo::field(a, _artistTagDelimiters, "artist_tag_delimiters");
|
||||
Wt::Dbo::field(a, _artistsToNotSplit, "artists_to_not_split");
|
||||
Wt::Dbo::field(a, _defaultTagDelimiters, "default_tag_delimiters");
|
||||
Wt::Dbo::field(a, _skipSingleReleasePlayLists, "skip_single_release_playlists");
|
||||
Wt::Dbo::field(a, _allowMBIDArtistMerge, "allow_mbid_artist_merge");
|
||||
}
|
||||
|
||||
private:
|
||||
int _scanVersion{};
|
||||
friend class Session;
|
||||
|
||||
ScanSettings(std::string_view name);
|
||||
static pointer create(Session& session, std::string_view name = "");
|
||||
|
||||
void incAudioScanVersion();
|
||||
|
||||
std::string _name;
|
||||
int _audioScanVersion{};
|
||||
Wt::WTime _startTime = Wt::WTime{ 0, 0, 0 };
|
||||
UpdatePeriod _updatePeriod{ UpdatePeriod::Never };
|
||||
SimilarityEngineType _similarityEngineType{ SimilarityEngineType::Clusters };
|
||||
std::string _extraTagsToScan;
|
||||
std::string _artistTagDelimiters;
|
||||
std::string _artistsToNotSplit;
|
||||
std::string _defaultTagDelimiters;
|
||||
bool _skipSingleReleasePlayLists{ false };
|
||||
bool _skipSingleReleasePlayLists{};
|
||||
bool _allowMBIDArtistMerge{};
|
||||
};
|
||||
} // namespace lms::db
|
||||
|
||||
@@ -80,32 +80,51 @@ namespace lms::db
|
||||
};
|
||||
|
||||
TrackArtistLink() = default;
|
||||
TrackArtistLink(ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType);
|
||||
TrackArtistLink(const ObjectPtr<Track>& track, const ObjectPtr<Artist>& artist, TrackArtistLinkType type, std::string_view subType, bool artistMBIDMatched);
|
||||
|
||||
static void find(Session& session, TrackId trackId, const std::function<void(const TrackArtistLink::pointer&, const ObjectPtr<Artist>&)>&);
|
||||
static void find(Session& session, const FindParameters& parameters, const std::function<void(const TrackArtistLink::pointer&)>&);
|
||||
static void find(Session& session, TrackId trackId, const std::function<void(const pointer&, const ObjectPtr<Artist>&)>& func);
|
||||
static void find(Session& session, const FindParameters& parameters, const std::function<void(const pointer&)>& func);
|
||||
static pointer find(Session& session, TrackArtistLinkId linkId);
|
||||
static pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType = {});
|
||||
static std::size_t getCount(Session& session);
|
||||
static pointer create(Session& session, const ObjectPtr<Track>& track, const ObjectPtr<Artist>& artist, TrackArtistLinkType type, std::string_view subType, bool artistMBIDMatched = false);
|
||||
static pointer create(Session& session, const ObjectPtr<Track>& track, const ObjectPtr<Artist>& artist, TrackArtistLinkType type, bool artistMBIDMatched = false);
|
||||
static core::EnumSet<TrackArtistLinkType> findUsedTypes(Session& session, ArtistId _artist);
|
||||
static void findArtistNameNoLongerMatch(Session& session, std::optional<Range> range, const std::function<void(const pointer&)>& func);
|
||||
static void findWithArtistNameAmbiguity(Session& session, std::optional<Range> range, bool allowArtistMBIDFallback, const std::function<void(const pointer&)>& func);
|
||||
|
||||
// accessors
|
||||
ObjectPtr<Track> getTrack() const { return _track; }
|
||||
ObjectPtr<Artist> getArtist() const { return _artist; }
|
||||
TrackArtistLinkType getType() const { return _type; }
|
||||
std::string_view getSubType() const { return _subType; }
|
||||
std::string_view getArtistName() const { return _artistName; }
|
||||
std::string_view getArtistSortName() const { return _artistSortName; }
|
||||
bool isArtistMBIDMatched() const { return _artistMBIDMatched; }
|
||||
|
||||
// setters
|
||||
void setArtist(ObjectPtr<Artist> artist);
|
||||
void setArtistName(std::string_view artistName);
|
||||
void setArtistSortName(std::string_view artistSortName);
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _type, "type");
|
||||
Wt::Dbo::field(a, _subType, "subtype");
|
||||
Wt::Dbo::field(a, _artistName, "artist_name");
|
||||
Wt::Dbo::field(a, _artistSortName, "artist_sort_name");
|
||||
Wt::Dbo::field(a, _artistMBIDMatched, "artist_mbid_matched");
|
||||
|
||||
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
|
||||
private:
|
||||
TrackArtistLinkType _type;
|
||||
TrackArtistLinkType _type{ TrackArtistLinkType::Artist };
|
||||
std::string _subType;
|
||||
std::string _artistName; // as it was in the tags
|
||||
std::string _artistSortName; // as it was in the tags
|
||||
bool _artistMBIDMatched{};
|
||||
|
||||
Wt::Dbo::ptr<Track> _track;
|
||||
Wt::Dbo::ptr<Artist> _artist;
|
||||
|
||||
@@ -109,4 +109,103 @@ namespace lms::db::tests
|
||||
EXPECT_TRUE(visited);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, ArtistInfo_findArtistNameNoLongerMatch)
|
||||
{
|
||||
ScopedArtistInfo artistInfo{ session };
|
||||
ScopedArtist artist{ session, "MyArtist" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
artistInfo.get().modify()->setArtist(artist.get());
|
||||
artistInfo.get().modify()->setName("MyArtist");
|
||||
artistInfo.get().modify()->setMBIDMatched(false);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
bool visited{};
|
||||
ArtistInfo::findArtistNameNoLongerMatch(session, std::nullopt, [&](const ArtistInfo::pointer&) {
|
||||
visited = true;
|
||||
});
|
||||
ASSERT_FALSE(visited);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
artist.get().modify()->setName("MyArtist2");
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
bool visited{};
|
||||
ArtistInfo::findArtistNameNoLongerMatch(session, std::nullopt, [&](const ArtistInfo::pointer&) {
|
||||
visited = true;
|
||||
});
|
||||
ASSERT_TRUE(visited);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, ArtistInfo_findWithArtistNameAmbiguity_split)
|
||||
{
|
||||
ScopedArtistInfo artistInfo1{ session };
|
||||
ScopedArtist artist1{ session, "MyArtist", core::UUID::fromString("b227426f-98b8-4b39-b3a7-ff25e7711e9b") };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
artistInfo1.get().modify()->setArtist(artist1.get());
|
||||
artistInfo1.get().modify()->setName("MyArtist");
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
bool visited{};
|
||||
ArtistInfo::findWithArtistNameAmbiguity(session, std::nullopt, true /*allow fallback*/, [&](const ArtistInfo::pointer&) {
|
||||
visited = true;
|
||||
});
|
||||
ASSERT_FALSE(visited);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
bool visited{};
|
||||
ArtistInfo::findWithArtistNameAmbiguity(session, std::nullopt, false /*allow fallback*/, [&](const ArtistInfo::pointer&) {
|
||||
visited = true;
|
||||
});
|
||||
ASSERT_TRUE(visited);
|
||||
}
|
||||
|
||||
ScopedArtist artist2{ session, "MyArtist", core::UUID::fromString("97d1fb6f-db09-4760-b0b3-816559bcb632") };
|
||||
ScopedArtistInfo artistInfo2{ session };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
artistInfo2.get().modify()->setArtist(artist2.get());
|
||||
artistInfo2.get().modify()->setName("MyArtist");
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
bool visited{};
|
||||
ArtistInfo::findWithArtistNameAmbiguity(session, std::nullopt, true /*allow fallback*/, [&](const ArtistInfo::pointer&) {
|
||||
visited = true;
|
||||
});
|
||||
ASSERT_TRUE(visited);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
bool visited{};
|
||||
ArtistInfo::findWithArtistNameAmbiguity(session, std::nullopt, false /*allow fallback*/, [&](const ArtistInfo::pointer&) {
|
||||
visited = true;
|
||||
});
|
||||
ASSERT_TRUE(visited);
|
||||
}
|
||||
}
|
||||
} // namespace lms::db::tests
|
||||
@@ -15,10 +15,12 @@ add_executable(test-database
|
||||
RatedRelease.cpp
|
||||
RatedTrack.cpp
|
||||
Release.cpp
|
||||
ScanSettings.cpp
|
||||
StarredArtist.cpp
|
||||
StarredRelease.cpp
|
||||
StarredTrack.cpp
|
||||
Track.cpp
|
||||
TrackArtistLink.cpp
|
||||
TrackBookmark.cpp
|
||||
TrackEmbeddedImage.cpp
|
||||
TrackFeatures.cpp
|
||||
|
||||
@@ -31,9 +31,11 @@
|
||||
#include "database/RatedArtist.hpp"
|
||||
#include "database/RatedRelease.hpp"
|
||||
#include "database/RatedTrack.hpp"
|
||||
#include "database/ScanSettings.hpp"
|
||||
#include "database/StarredArtist.hpp"
|
||||
#include "database/StarredRelease.hpp"
|
||||
#include "database/StarredTrack.hpp"
|
||||
#include "database/TrackArtistLink.hpp"
|
||||
#include "database/TrackEmbeddedImage.hpp"
|
||||
#include "database/TrackEmbeddedImageLink.hpp"
|
||||
#include "database/TrackLyrics.hpp"
|
||||
@@ -361,10 +363,12 @@ VALUES
|
||||
EXPECT_FALSE(RatedTrack::find(session, RatedTrackId{}));
|
||||
EXPECT_FALSE(Release::find(session, ReleaseId{}));
|
||||
EXPECT_FALSE(ReleaseType::find(session, ReleaseTypeId{}));
|
||||
EXPECT_FALSE(ScanSettings::find(session, ScanSettingsId{}));
|
||||
EXPECT_FALSE(StarredArtist::find(session, StarredArtistId{}));
|
||||
EXPECT_FALSE(StarredRelease::find(session, StarredReleaseId{}));
|
||||
EXPECT_FALSE(StarredTrack::find(session, StarredTrackId{}));
|
||||
EXPECT_FALSE(Track::find(session, TrackId{}));
|
||||
EXPECT_FALSE(TrackArtistLink::find(session, TrackArtistLinkId{}));
|
||||
EXPECT_FALSE(TrackList::find(session, TrackListId{}));
|
||||
EXPECT_FALSE(TrackLyrics::find(session, TrackLyricsId{}));
|
||||
EXPECT_FALSE(UIState::find(session, UIStateId{}));
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
#include "database/ScanSettings.hpp"
|
||||
|
||||
#include <initializer_list>
|
||||
|
||||
#include "Common.hpp"
|
||||
|
||||
namespace lms::db::tests
|
||||
{
|
||||
using ScopedScanSettings = ScopedEntity<db::ScanSettings>;
|
||||
|
||||
TEST_F(DatabaseFixture, ScanSettings)
|
||||
{
|
||||
ScopedScanSettings settings{ session, "test" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
const auto artists{ settings.get()->getArtistsToNotSplit() };
|
||||
ASSERT_EQ(artists.size(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
settings.get().modify()->setArtistsToNotSplit(std::initializer_list<std::string_view>{ "AC/DC", "My/Artist" });
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto artists{ settings.get()->getArtistsToNotSplit() };
|
||||
ASSERT_EQ(artists.size(), 2);
|
||||
}
|
||||
}
|
||||
} // namespace lms::db::tests
|
||||
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* 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 "Common.hpp"
|
||||
#include "database/Types.hpp"
|
||||
|
||||
#include "database/TrackArtistLink.hpp"
|
||||
|
||||
namespace lms::db::tests
|
||||
{
|
||||
TEST_F(DatabaseFixture, TrackArtistLink_findArtistNameNoLongerMatch)
|
||||
{
|
||||
ScopedTrack track{ session };
|
||||
ScopedArtist artist{ session, "MyArtist" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
auto link{ session.create<TrackArtistLink>(track.get(), artist.get(), TrackArtistLinkType::Artist, false) };
|
||||
link.modify()->setArtistName("MyArtist");
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
bool visited{};
|
||||
TrackArtistLink::findArtistNameNoLongerMatch(session, std::nullopt, [&](const TrackArtistLink::pointer&) {
|
||||
visited = true;
|
||||
});
|
||||
ASSERT_FALSE(visited);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
artist.get().modify()->setName("MyArtist2");
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
bool visited{};
|
||||
TrackArtistLink::findArtistNameNoLongerMatch(session, std::nullopt, [&](const TrackArtistLink::pointer&) {
|
||||
visited = true;
|
||||
});
|
||||
ASSERT_TRUE(visited);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, TrackArtistLink_findWithArtistNameAmbiguity_split)
|
||||
{
|
||||
ScopedTrack track{ session };
|
||||
ScopedArtist artist1{ session, "MyArtist", core::UUID::fromString("b227426f-98b8-4b39-b3a7-ff25e7711e9b") };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
auto link{ session.create<TrackArtistLink>(track.get(), artist1.get(), TrackArtistLinkType::Artist, false) };
|
||||
link.modify()->setArtistName("MyArtist");
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
bool visited{};
|
||||
TrackArtistLink::findWithArtistNameAmbiguity(session, std::nullopt, true /*allow fallback*/, [&](const TrackArtistLink::pointer&) {
|
||||
visited = true;
|
||||
});
|
||||
ASSERT_FALSE(visited);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
bool visited{};
|
||||
TrackArtistLink::findWithArtistNameAmbiguity(session, std::nullopt, false /*allow fallback*/, [&](const TrackArtistLink::pointer&) {
|
||||
visited = true;
|
||||
});
|
||||
ASSERT_TRUE(visited);
|
||||
}
|
||||
|
||||
ScopedArtist artist2{ session, "MyArtist", core::UUID::fromString("97d1fb6f-db09-4760-b0b3-816559bcb632") };
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
bool visited{};
|
||||
TrackArtistLink::findWithArtistNameAmbiguity(session, std::nullopt, true /*allow fallback*/, [&](const TrackArtistLink::pointer&) {
|
||||
visited = true;
|
||||
});
|
||||
ASSERT_TRUE(visited);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
bool visited{};
|
||||
TrackArtistLink::findWithArtistNameAmbiguity(session, std::nullopt, false /*allow fallback*/, [&](const TrackArtistLink::pointer&) {
|
||||
visited = true;
|
||||
});
|
||||
ASSERT_TRUE(visited);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, TrackArtistLink_findWithArtistNameAmbiguity_merge)
|
||||
{
|
||||
ScopedTrack track{ session };
|
||||
ScopedArtist artist1{ session, "MyArtist" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
auto link{ session.create<TrackArtistLink>(track.get(), artist1.get(), TrackArtistLinkType::Artist, false) };
|
||||
link.modify()->setArtistName("MyArtist");
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
{
|
||||
bool visited{};
|
||||
TrackArtistLink::findWithArtistNameAmbiguity(session, std::nullopt, true /*allow fallback*/, [&](const TrackArtistLink::pointer&) {
|
||||
visited = false;
|
||||
});
|
||||
ASSERT_FALSE(visited);
|
||||
}
|
||||
{
|
||||
bool visited{};
|
||||
TrackArtistLink::findWithArtistNameAmbiguity(session, std::nullopt, false /*allow fallback*/, [&](const TrackArtistLink::pointer&) {
|
||||
visited = true;
|
||||
});
|
||||
ASSERT_FALSE(visited);
|
||||
}
|
||||
}
|
||||
|
||||
ScopedArtist artist2{ session, "MyArtist", core::UUID::fromString("97d1fb6f-db09-4760-b0b3-816559bcb632") };
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
{
|
||||
bool visited{};
|
||||
TrackArtistLink::findWithArtistNameAmbiguity(session, std::nullopt, true /*allow fallback*/, [&](const TrackArtistLink::pointer&) {
|
||||
visited = true;
|
||||
});
|
||||
ASSERT_TRUE(visited);
|
||||
}
|
||||
{
|
||||
bool visited{};
|
||||
TrackArtistLink::findWithArtistNameAmbiguity(session, std::nullopt, false /*allow fallback*/, [&](const TrackArtistLink::pointer&) {
|
||||
visited = true;
|
||||
});
|
||||
ASSERT_FALSE(visited);
|
||||
}
|
||||
}
|
||||
|
||||
ScopedArtist artist3{ session, "MyArtist", core::UUID::fromString("3d46c4fb-110d-4d4f-a2d5-5ca57ef1d582") };
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
{
|
||||
bool visited{};
|
||||
TrackArtistLink::findWithArtistNameAmbiguity(session, std::nullopt, true /*allow fallback*/, [&](const TrackArtistLink::pointer&) {
|
||||
visited = true;
|
||||
});
|
||||
ASSERT_FALSE(visited);
|
||||
}
|
||||
{
|
||||
bool visited{};
|
||||
TrackArtistLink::findWithArtistNameAmbiguity(session, std::nullopt, false /*allow fallback*/, [&](const TrackArtistLink::pointer&) {
|
||||
visited = true;
|
||||
});
|
||||
ASSERT_FALSE(visited);
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace lms::db::tests
|
||||
@@ -26,11 +26,17 @@ if (${LMS_IMAGE_BACKEND} STREQUAL "stb")
|
||||
|
||||
target_sources(lmsimage PRIVATE
|
||||
impl/stb/Image.cpp
|
||||
impl/stb/Exception.cpp
|
||||
impl/stb/RawImage.cpp
|
||||
impl/stb/StbImage.cpp
|
||||
impl/stb/StbImageResize.cpp
|
||||
impl/stb/StbImageWrite.cpp
|
||||
)
|
||||
|
||||
set_property(SOURCE impl/stb/StbImage.cpp PROPERTY SKIP_UNITY_BUILD_INCLUSION ON)
|
||||
set_property(SOURCE impl/stb/StbImageResize.cpp PROPERTY SKIP_UNITY_BUILD_INCLUSION ON)
|
||||
set_property(SOURCE impl/stb/StbImageWrite.cpp PROPERTY SKIP_UNITY_BUILD_INCLUSION ON)
|
||||
|
||||
target_compile_options(lmsimage PRIVATE "-DSTB_IMAGE_RESIZE_VERSION=${STB_IMAGE_RESIZE_VERSION}")
|
||||
target_include_directories(lmsimage PRIVATE ${STB_IMAGE_INCLUDE_DIR})
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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 "Exception.hpp"
|
||||
|
||||
#include "StbImage.hpp"
|
||||
|
||||
namespace lms::image
|
||||
{
|
||||
StbiException::StbiException(std::string_view desc)
|
||||
: Exception{ std::string{ desc } + ": " + getLastFailureReason() }
|
||||
{
|
||||
}
|
||||
|
||||
std::string StbiException::getLastFailureReason()
|
||||
{
|
||||
const char* failureReason{ ::stbi_failure_reason() };
|
||||
return failureReason ? failureReason : "unknown reason";
|
||||
}
|
||||
} // namespace lms::image
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright (C) 2020 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_view>
|
||||
|
||||
#include "image/Exception.hpp"
|
||||
|
||||
namespace lms::image
|
||||
{
|
||||
class StbiException : public Exception
|
||||
{
|
||||
public:
|
||||
StbiException(std::string_view desc);
|
||||
|
||||
private:
|
||||
static std::string getLastFailureReason();
|
||||
};
|
||||
} // namespace lms::image
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
#include <array>
|
||||
|
||||
#include "Exception.hpp"
|
||||
#include "StbImage.hpp"
|
||||
#include "StbImageWrite.hpp"
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
#include "RawImage.hpp"
|
||||
|
||||
#include "Exception.hpp"
|
||||
#include "StbImage.hpp"
|
||||
#include "StbImageResize.hpp"
|
||||
|
||||
|
||||
@@ -19,17 +19,3 @@
|
||||
|
||||
#define STB_IMAGE_IMPLEMENTATION
|
||||
#include "StbImage.hpp"
|
||||
|
||||
namespace lms::image
|
||||
{
|
||||
StbiException::StbiException(std::string_view desc)
|
||||
: Exception{ std::string{ desc } + ": " + getLastFailureReason() }
|
||||
{
|
||||
}
|
||||
|
||||
std::string StbiException::getLastFailureReason()
|
||||
{
|
||||
const char* failureReason{ ::stbi_failure_reason() };
|
||||
return failureReason ? failureReason : "unknown reason";
|
||||
}
|
||||
} // namespace lms::image
|
||||
@@ -24,19 +24,3 @@
|
||||
#define STBI_FAILURE_USERMSG
|
||||
|
||||
#include <stb_image.h>
|
||||
|
||||
#include <string_view>
|
||||
|
||||
#include "image/Exception.hpp"
|
||||
|
||||
namespace lms::image
|
||||
{
|
||||
class StbiException : public Exception
|
||||
{
|
||||
public:
|
||||
StbiException(std::string_view desc);
|
||||
|
||||
private:
|
||||
static std::string getLastFailureReason();
|
||||
};
|
||||
} // namespace lms::image
|
||||
@@ -17,6 +17,8 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace lms::image
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include <boost/property_tree/xml_parser.hpp>
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/String.hpp"
|
||||
|
||||
namespace lms::metadata
|
||||
{
|
||||
@@ -43,12 +44,12 @@ namespace lms::metadata
|
||||
|
||||
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.mbid = core::UUID::fromString(core::stringUtils::stringTrim(artistNode.get_optional<std::string>("musicBrainzArtistID").value_or("")));
|
||||
artistInfo.name = core::stringUtils::stringTrim(artistNode.get_optional<std::string>("name").value_or(""));
|
||||
artistInfo.sortName = core::stringUtils::stringTrim(artistNode.get_optional<std::string>("sortname").value_or(""));
|
||||
artistInfo.type = core::stringUtils::stringTrim(artistNode.get_optional<std::string>("type").value_or(""));
|
||||
artistInfo.gender = core::stringUtils::stringTrim(artistNode.get_optional<std::string>("gender").value_or(""));
|
||||
artistInfo.disambiguation = core::stringUtils::stringTrim(artistNode.get_optional<std::string>("disambiguation").value_or(""));
|
||||
artistInfo.biography = artistNode.get_optional<std::string>("biography").value_or("");
|
||||
|
||||
return artistInfo;
|
||||
|
||||
@@ -62,36 +62,41 @@ namespace lms::metadata
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
std::vector<T> getTagValuesFirstMatchAs(const ITagReader& tagReader, std::initializer_list<TagType> tagTypes, std::span<const std::string> tagDelimiters)
|
||||
void addTagIfNonEmpty(std::vector<T>& res, std::string_view tag)
|
||||
{
|
||||
tag = core::stringUtils::stringTrim(tag);
|
||||
if (tag.empty())
|
||||
return;
|
||||
|
||||
if (std::optional<T> val{ core::stringUtils::readAs<T>(tag) })
|
||||
res.emplace_back(std::move(*val));
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
std::vector<T> getTagValuesFirstMatchAs(const ITagReader& tagReader, std::initializer_list<TagType> tagTypes, std::span<const std::string> tagDelimiters, const WhiteList* whitelist = nullptr)
|
||||
{
|
||||
std::vector<T> res;
|
||||
|
||||
for (const TagType tagType : tagTypes)
|
||||
{
|
||||
auto addTagIfNonEmpty{ [&res](std::string_view tag) {
|
||||
tag = core::stringUtils::stringTrim(tag);
|
||||
if (!tag.empty())
|
||||
{
|
||||
std::optional<T> val{ core::stringUtils::readAs<T>(tag) };
|
||||
if (val)
|
||||
res.emplace_back(std::move(*val));
|
||||
}
|
||||
} };
|
||||
|
||||
tagReader.visitTagValues(tagType, [&](std::string_view value) {
|
||||
for (std::string_view tagDelimiter : tagDelimiters)
|
||||
value = core::stringUtils::stringTrim(value);
|
||||
if (!whitelist || !whitelist->contains(value))
|
||||
{
|
||||
if (value.find(tagDelimiter) != std::string_view::npos)
|
||||
for (std::string_view tagDelimiter : tagDelimiters)
|
||||
{
|
||||
for (std::string_view splitTag : core::stringUtils::splitString(value, tagDelimiters))
|
||||
addTagIfNonEmpty(splitTag);
|
||||
if (value.find(tagDelimiter) != std::string_view::npos)
|
||||
{
|
||||
for (std::string_view splitTag : core::stringUtils::splitString(value, tagDelimiters))
|
||||
addTagIfNonEmpty(res, splitTag);
|
||||
|
||||
return;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// no delimiter found, or no delimiter to be used
|
||||
addTagIfNonEmpty(value);
|
||||
addTagIfNonEmpty(res, value);
|
||||
});
|
||||
|
||||
if (!res.empty())
|
||||
@@ -153,11 +158,11 @@ namespace lms::metadata
|
||||
std::initializer_list<TagType> artistMBIDTagNames,
|
||||
const AudioFileParserParameters& params)
|
||||
{
|
||||
std::vector<std::string> artistNames{ getTagValuesFirstMatchAs<std::string>(tagReader, artistTagNames, params.artistTagDelimiters) };
|
||||
std::vector<std::string> artistNames{ getTagValuesFirstMatchAs<std::string>(tagReader, artistTagNames, params.artistTagDelimiters, ¶ms.artistsToNotSplit) };
|
||||
if (artistNames.empty())
|
||||
return {};
|
||||
|
||||
std::vector<std::string> artistSortNames{ getTagValuesFirstMatchAs<std::string>(tagReader, artistSortTagNames, params.artistTagDelimiters) };
|
||||
std::vector<std::string> artistSortNames{ getTagValuesFirstMatchAs<std::string>(tagReader, artistSortTagNames, params.artistTagDelimiters, ¶ms.artistsToNotSplit) };
|
||||
std::vector<core::UUID> artistMBIDs{ getTagValuesFirstMatchAs<core::UUID>(tagReader, artistMBIDTagNames, params.defaultTagDelimiters) };
|
||||
|
||||
std::vector<Artist> artists;
|
||||
@@ -238,6 +243,7 @@ namespace lms::metadata
|
||||
// Otherwise, we reconstruct the string using a standard, hardcoded, join
|
||||
if (artistTag && strIsMatchingArtistNames(*artistTag, artistNames))
|
||||
{
|
||||
// Limitation: this test does not take the whitelist into account
|
||||
if (!strIsContainingAny(*artistTag, artistTagDelimiters))
|
||||
artistDisplayName = *artistTag;
|
||||
}
|
||||
@@ -267,54 +273,6 @@ namespace lms::metadata
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void fillInArtistsWithMbid(std::span<const Artist> artists, std::unordered_map<std::string_view, core::UUID>& artistsWithMbid)
|
||||
{
|
||||
for (const Artist& artist : artists)
|
||||
{
|
||||
if (artist.mbid.has_value())
|
||||
{
|
||||
// there may collisions, we don't want to replace
|
||||
artistsWithMbid.emplace(artist.name, *artist.mbid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void fillInMbids(std::span<Artist> artists, const std::unordered_map<std::string_view, core::UUID>& artistsWithMbid)
|
||||
{
|
||||
for (Artist& artist : artists)
|
||||
{
|
||||
if (!artist.mbid)
|
||||
{
|
||||
const auto it{ artistsWithMbid.find(artist.name) };
|
||||
if (it != std::cend(artistsWithMbid))
|
||||
artist.mbid = it->second;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void fillMissingMbids(Track& track)
|
||||
{
|
||||
// first pass: collect all artists that have mbids
|
||||
std::unordered_map<std::string_view, core::UUID> artistsWithMbid;
|
||||
|
||||
// For now, mbids can only set in artist and album artist tags
|
||||
// filling order is important: we estimate track-level artists are more likely
|
||||
// to be set in other fields than album artists
|
||||
fillInArtistsWithMbid(track.artists, artistsWithMbid);
|
||||
if (track.medium && track.medium->release)
|
||||
fillInArtistsWithMbid(track.medium->release->artists, artistsWithMbid);
|
||||
|
||||
// second pass: fill in all artists that have no mbid set with the same name
|
||||
fillInMbids(track.conductorArtists, artistsWithMbid);
|
||||
fillInMbids(track.composerArtists, artistsWithMbid);
|
||||
fillInMbids(track.lyricistArtists, artistsWithMbid);
|
||||
fillInMbids(track.mixerArtists, artistsWithMbid);
|
||||
fillInMbids(track.producerArtists, artistsWithMbid);
|
||||
fillInMbids(track.remixerArtists, artistsWithMbid);
|
||||
for (auto& [role, artists] : track.performerArtists)
|
||||
fillInMbids(artists, artistsWithMbid);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<IAudioFileParser> createAudioFileParser(const AudioFileParserParameters& params)
|
||||
@@ -489,8 +447,6 @@ namespace lms::metadata
|
||||
track.remixerArtists = getArtists(tagReader, { TagType::Remixers, TagType::Remixer }, { TagType::RemixersSortOrder, TagType::RemixerSortOrder }, {}, _params);
|
||||
track.performerArtists = getPerformerArtists(tagReader); // artistDelimiters not supported
|
||||
|
||||
fillMissingMbids(track);
|
||||
|
||||
// If a file has originalDate but no originalYear, set it
|
||||
if (!track.originalYear)
|
||||
track.originalYear = track.originalDate.getYear();
|
||||
|
||||
@@ -30,7 +30,7 @@ namespace lms::metadata
|
||||
namespace
|
||||
{
|
||||
// Mapping to internal avformat names and/or common alternative custom names
|
||||
static const std::unordered_map<TagType, std::vector<std::string>> tagMapping{
|
||||
static const std::unordered_map<TagType, std::vector<std::string>> avFormatTagMapping{
|
||||
{ TagType::AcoustID, { "ACOUSTID_ID", "ACOUSTID ID" } },
|
||||
{ TagType::Advisory, { "ITUNESADVISORY" } },
|
||||
{ TagType::Album, { "ALBUM", "TALB", "WM/ALBUMTITLE" } },
|
||||
@@ -171,8 +171,8 @@ namespace lms::metadata
|
||||
|
||||
void AvFormatTagReader::visitTagValues(TagType tag, TagValueVisitor visitor) const
|
||||
{
|
||||
auto itTagNames{ tagMapping.find(tag) };
|
||||
if (itTagNames == std::cend(tagMapping))
|
||||
auto itTagNames{ avFormatTagMapping.find(tag) };
|
||||
if (itTagNames == std::cend(avFormatTagMapping))
|
||||
return;
|
||||
|
||||
for (const std::string& tagName : itTagNames->second)
|
||||
|
||||
@@ -48,7 +48,7 @@ namespace lms::metadata
|
||||
{
|
||||
namespace
|
||||
{
|
||||
class ParsingFailedException : public Exception
|
||||
class ImageParsingFailedException : public Exception
|
||||
{
|
||||
};
|
||||
|
||||
@@ -362,7 +362,7 @@ namespace lms::metadata
|
||||
if (_file.isNull())
|
||||
{
|
||||
LMS_LOG(METADATA, ERROR, "File " << p << ": parsing failed");
|
||||
throw ParsingFailedException{};
|
||||
throw ImageParsingFailedException{};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -55,12 +55,12 @@ namespace lms::metadata
|
||||
{
|
||||
namespace
|
||||
{
|
||||
class ParsingFailedException : public Exception
|
||||
class TagParsingFailedException : public Exception
|
||||
{
|
||||
};
|
||||
|
||||
// Mapping to internal taglib names and/or common alternative custom names
|
||||
const std::unordered_map<TagType, std::vector<std::string>> tagMapping{
|
||||
const std::unordered_map<TagType, std::vector<std::string>> tagLibTagMapping{
|
||||
{ TagType::AcoustID, { "ACOUSTID_ID", "ACOUSTID ID" } },
|
||||
{ TagType::Advisory, { "ITUNESADVISORY" } },
|
||||
{ TagType::Album, { "ALBUM" } },
|
||||
@@ -228,13 +228,13 @@ namespace lms::metadata
|
||||
if (_file.isNull())
|
||||
{
|
||||
LMS_LOG(METADATA, ERROR, "File " << p << ": parsing failed");
|
||||
throw ParsingFailedException{};
|
||||
throw TagParsingFailedException{};
|
||||
}
|
||||
|
||||
if (!_file.audioProperties())
|
||||
{
|
||||
LMS_LOG(METADATA, ERROR, "File " << p << ": no audio properties");
|
||||
throw ParsingFailedException{};
|
||||
throw TagParsingFailedException{};
|
||||
}
|
||||
|
||||
computeAudioProperties();
|
||||
@@ -465,8 +465,8 @@ namespace lms::metadata
|
||||
|
||||
void TagLibTagReader::visitTagValues(TagType tag, TagValueVisitor visitor) const
|
||||
{
|
||||
auto itTagNames{ tagMapping.find(tag) };
|
||||
if (itTagNames == std::cend(tagMapping))
|
||||
auto itTagNames{ tagLibTagMapping.find(tag) };
|
||||
if (itTagNames == std::cend(tagLibTagMapping))
|
||||
return;
|
||||
|
||||
for (const std::string& tagName : itTagNames->second)
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "core/PartialDateTime.hpp"
|
||||
@@ -213,11 +214,28 @@ namespace lms::metadata
|
||||
Accurate,
|
||||
};
|
||||
|
||||
struct WhiteListHash : std::hash<std::string>, std::hash<std::string_view>
|
||||
{
|
||||
using is_transparent = void;
|
||||
|
||||
[[nodiscard]] size_t operator()(std::string_view str) const
|
||||
{
|
||||
return std::hash<std::string_view>{}(str);
|
||||
}
|
||||
|
||||
[[nodiscard]] size_t operator()(const std::string& str) const
|
||||
{
|
||||
return std::hash<std::string>{}(str);
|
||||
}
|
||||
};
|
||||
|
||||
using WhiteList = std::unordered_set<std::string, WhiteListHash, std::equal_to<>>;
|
||||
struct AudioFileParserParameters
|
||||
{
|
||||
ParserBackend backend{ ParserBackend::TagLib };
|
||||
ParserReadStyle readStyle{ ParserReadStyle::Average };
|
||||
std::vector<std::string> artistTagDelimiters;
|
||||
WhiteList artistsToNotSplit;
|
||||
std::vector<std::string> defaultTagDelimiters;
|
||||
std::vector<std::string> userExtraTags;
|
||||
bool debug{};
|
||||
|
||||
@@ -76,4 +76,22 @@ He moved from the UK to Montreal in 1984 to become resident DJ at a number of cl
|
||||
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.");
|
||||
}
|
||||
|
||||
TEST(ArtistInfo, trim)
|
||||
{
|
||||
std::istringstream is{ R"(<?xml version='1.0' encoding='UTF-8' standalone='yes'?>
|
||||
<artist>
|
||||
<name> My Artist </name>
|
||||
<musicBrainzArtistID> 38811c52-85e3-4e2e-3319-ab7d9f2cfa5b </musicBrainzArtistID>
|
||||
<sortname> Artist, My </sortname>
|
||||
<disambiguation> My Artist </disambiguation>
|
||||
</artist>)" };
|
||||
|
||||
const ArtistInfo artistInfo{ parseArtistInfo(is) };
|
||||
|
||||
EXPECT_EQ(artistInfo.mbid, core::UUID::fromString("38811c52-85e3-4e2e-3319-ab7d9f2cfa5b"));
|
||||
EXPECT_EQ(artistInfo.name, "My Artist");
|
||||
ASSERT_EQ(artistInfo.sortName, "Artist, My");
|
||||
ASSERT_EQ(artistInfo.disambiguation, "My Artist");
|
||||
}
|
||||
} // namespace lms::metadata::tests
|
||||
@@ -263,9 +263,9 @@ namespace lms::metadata
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ TagType::Album, { "MyAlbum" } },
|
||||
{ TagType::AlbumArtist, { "AlbumArtist1 / AlbumArtist2" } },
|
||||
{ TagType::AlbumArtist, { "AlbumArtist1 / AlbumArtist2" } },
|
||||
{ TagType::Artist, { " Artist1 / Artist2 feat. Artist3 " } },
|
||||
{ TagType::Genre, { "Genre1 ; Genre2" } },
|
||||
{ TagType::Genre, { "Genre1 ; Genre2" } },
|
||||
{ TagType::Language, { " Lang1/Lang2 / Lang3" } },
|
||||
}
|
||||
};
|
||||
@@ -295,11 +295,60 @@ namespace lms::metadata
|
||||
// Release
|
||||
ASSERT_TRUE(track->medium->release.has_value());
|
||||
EXPECT_EQ(track->medium->release->name, "MyAlbum");
|
||||
ASSERT_EQ(track->medium->release->artists.size(), 2);
|
||||
EXPECT_EQ(track->medium->release->artists[0].name, "AlbumArtist1");
|
||||
EXPECT_EQ(track->medium->release->artists[1].name, "AlbumArtist2");
|
||||
EXPECT_EQ(track->medium->release->artistDisplayName, "AlbumArtist1, AlbumArtist2");
|
||||
}
|
||||
|
||||
TEST(AudioFileParser, customArtistDelimiters_whitelist)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ TagType::Album, { "MyAlbum" } },
|
||||
{ TagType::AlbumArtist, { " AC/DC " } },
|
||||
{ TagType::Artist, { "AC/DC " } },
|
||||
}
|
||||
};
|
||||
|
||||
AudioFileParserParameters params;
|
||||
params.artistTagDelimiters = { "/" };
|
||||
params.artistsToNotSplit = { "AC/DC" };
|
||||
TestAudioFileParser parser{ params };
|
||||
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track->artists.size(), 1);
|
||||
EXPECT_EQ(track->artists[0].name, "AC/DC");
|
||||
EXPECT_EQ(track->artistDisplayName, "AC/DC");
|
||||
ASSERT_TRUE(track->medium.has_value());
|
||||
ASSERT_TRUE(track->medium->release.has_value());
|
||||
EXPECT_EQ(track->medium->release->name, "MyAlbum");
|
||||
ASSERT_EQ(track->medium->release->artists.size(), 1);
|
||||
EXPECT_EQ(track->medium->release->artists[0].name, "AC/DC");
|
||||
EXPECT_EQ(track->medium->release->artistDisplayName, "AC/DC");
|
||||
}
|
||||
|
||||
TEST(AudioFileParser, customArtistDelimiters_whitelist_multi)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ TagType::Artist, { "AC/DC and MyArtist" } },
|
||||
{ TagType::Artists, { "AC/DC", "MyArtist" } },
|
||||
}
|
||||
};
|
||||
|
||||
AudioFileParserParameters params;
|
||||
params.artistTagDelimiters = { "/" };
|
||||
params.artistsToNotSplit = { "AC/DC" };
|
||||
TestAudioFileParser parser{ params };
|
||||
std::unique_ptr<Track> track{ parser.parseMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track->artists.size(), 2);
|
||||
EXPECT_EQ(track->artists[0].name, "AC/DC");
|
||||
EXPECT_EQ(track->artists[1].name, "MyArtist");
|
||||
EXPECT_EQ(track->artistDisplayName, "AC/DC, MyArtist"); // Reconstructed since this use case is not handled
|
||||
}
|
||||
|
||||
TEST(AudioFileParser, customDelimiters_foundInArtist)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
@@ -605,131 +654,6 @@ namespace lms::metadata
|
||||
EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2"); // reconstruct the artist display name
|
||||
}
|
||||
|
||||
TEST(AudioFileParser, MBIDs_fallback)
|
||||
{
|
||||
TestTagReader testTags{
|
||||
{
|
||||
{ TagType::Artist, { "Artist1", "Artist2" } },
|
||||
{ TagType::Album, { "MyAlbum" } },
|
||||
{ TagType::AlbumArtists, { "Artist3", "Artist4" } },
|
||||
{ TagType::MusicBrainzArtistID, { "6643f584-5edc-45ce-927d-0a4ab25c2673", "481c5912-bf1a-47f7-b03c-d34e49711706" } },
|
||||
{ TagType::MusicBrainzReleaseArtistID, { "ed42bcaf-e147-4f34-8f26-d74acc97670a", "6fc64a4b-26f5-441f-993c-fd511290233b" } },
|
||||
{ TagType::Composer, { "Artist1", "Artist3" } },
|
||||
{ TagType::Conductor, { "Artist1", "Artist3" } },
|
||||
{ TagType::Lyricist, { "Artist1", "Artist3" } },
|
||||
{ TagType::Mixer, { "Artist1", "Artist3" } },
|
||||
{ TagType::Producer, { "Artist1", "Artist3" } },
|
||||
{ TagType::Remixers, { "Artist1", "Artist3" } },
|
||||
}
|
||||
};
|
||||
|
||||
testTags.setPerformersTags({ { "RoleA", { "Artist1", "Artist3" } },
|
||||
{ "RoleB", { "Artist2", "Artist4" } } });
|
||||
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track->artists.size(), 2);
|
||||
EXPECT_EQ(track->artists[0].name, "Artist1");
|
||||
ASSERT_TRUE(track->artists[0].mbid.has_value());
|
||||
EXPECT_EQ(track->artists[0].mbid.value(), core::UUID::fromString("6643f584-5edc-45ce-927d-0a4ab25c2673"));
|
||||
EXPECT_EQ(track->artists[1].name, "Artist2");
|
||||
ASSERT_TRUE(track->artists[1].mbid.has_value());
|
||||
EXPECT_EQ(track->artists[1].mbid.value(), core::UUID::fromString("481c5912-bf1a-47f7-b03c-d34e49711706"));
|
||||
|
||||
ASSERT_TRUE(track->medium.has_value());
|
||||
ASSERT_TRUE(track->medium->release.has_value());
|
||||
ASSERT_EQ(track->medium->release->artists.size(), 2);
|
||||
EXPECT_EQ(track->medium->release->artists[0].name, "Artist3");
|
||||
ASSERT_TRUE(track->medium->release->artists[0].mbid.has_value());
|
||||
EXPECT_EQ(track->medium->release->artists[0].mbid.value(), core::UUID::fromString("ed42bcaf-e147-4f34-8f26-d74acc97670a"));
|
||||
EXPECT_EQ(track->medium->release->artists[1].name, "Artist4");
|
||||
ASSERT_TRUE(track->medium->release->artists[1].mbid.has_value());
|
||||
EXPECT_EQ(track->medium->release->artists[1].mbid.value(), core::UUID::fromString("6fc64a4b-26f5-441f-993c-fd511290233b"));
|
||||
|
||||
ASSERT_EQ(track->composerArtists.size(), 2);
|
||||
EXPECT_EQ(track->composerArtists[0].name, "Artist1");
|
||||
ASSERT_TRUE(track->composerArtists[0].mbid.has_value());
|
||||
EXPECT_EQ(track->composerArtists[0].mbid.value(), core::UUID::fromString("6643f584-5edc-45ce-927d-0a4ab25c2673"));
|
||||
EXPECT_EQ(track->composerArtists[1].name, "Artist3");
|
||||
ASSERT_TRUE(track->composerArtists[1].mbid.has_value());
|
||||
EXPECT_EQ(track->composerArtists[1].mbid.value(), core::UUID::fromString("ed42bcaf-e147-4f34-8f26-d74acc97670a"));
|
||||
|
||||
ASSERT_EQ(track->conductorArtists.size(), 2);
|
||||
EXPECT_EQ(track->conductorArtists[0].name, "Artist1");
|
||||
ASSERT_TRUE(track->conductorArtists[0].mbid.has_value());
|
||||
EXPECT_EQ(track->conductorArtists[0].mbid.value(), core::UUID::fromString("6643f584-5edc-45ce-927d-0a4ab25c2673"));
|
||||
EXPECT_EQ(track->conductorArtists[1].name, "Artist3");
|
||||
ASSERT_TRUE(track->conductorArtists[1].mbid.has_value());
|
||||
EXPECT_EQ(track->conductorArtists[1].mbid.value(), core::UUID::fromString("ed42bcaf-e147-4f34-8f26-d74acc97670a"));
|
||||
|
||||
ASSERT_EQ(track->lyricistArtists.size(), 2);
|
||||
EXPECT_EQ(track->lyricistArtists[0].name, "Artist1");
|
||||
ASSERT_TRUE(track->lyricistArtists[0].mbid.has_value());
|
||||
EXPECT_EQ(track->lyricistArtists[0].mbid.value(), core::UUID::fromString("6643f584-5edc-45ce-927d-0a4ab25c2673"));
|
||||
EXPECT_EQ(track->lyricistArtists[1].name, "Artist3");
|
||||
ASSERT_TRUE(track->lyricistArtists[1].mbid.has_value());
|
||||
EXPECT_EQ(track->lyricistArtists[1].mbid.value(), core::UUID::fromString("ed42bcaf-e147-4f34-8f26-d74acc97670a"));
|
||||
|
||||
ASSERT_EQ(track->mixerArtists.size(), 2);
|
||||
EXPECT_EQ(track->mixerArtists[0].name, "Artist1");
|
||||
ASSERT_TRUE(track->mixerArtists[0].mbid.has_value());
|
||||
EXPECT_EQ(track->mixerArtists[0].mbid.value(), core::UUID::fromString("6643f584-5edc-45ce-927d-0a4ab25c2673"));
|
||||
EXPECT_EQ(track->mixerArtists[1].name, "Artist3");
|
||||
ASSERT_TRUE(track->mixerArtists[1].mbid.has_value());
|
||||
EXPECT_EQ(track->mixerArtists[1].mbid.value(), core::UUID::fromString("ed42bcaf-e147-4f34-8f26-d74acc97670a"));
|
||||
|
||||
ASSERT_EQ(track->producerArtists.size(), 2);
|
||||
EXPECT_EQ(track->producerArtists[0].name, "Artist1");
|
||||
ASSERT_TRUE(track->producerArtists[0].mbid.has_value());
|
||||
EXPECT_EQ(track->producerArtists[0].mbid.value(), core::UUID::fromString("6643f584-5edc-45ce-927d-0a4ab25c2673"));
|
||||
EXPECT_EQ(track->producerArtists[1].name, "Artist3");
|
||||
ASSERT_TRUE(track->producerArtists[1].mbid.has_value());
|
||||
EXPECT_EQ(track->producerArtists[1].mbid.value(), core::UUID::fromString("ed42bcaf-e147-4f34-8f26-d74acc97670a"));
|
||||
|
||||
ASSERT_EQ(track->remixerArtists.size(), 2);
|
||||
EXPECT_EQ(track->remixerArtists[0].name, "Artist1");
|
||||
ASSERT_TRUE(track->remixerArtists[0].mbid.has_value());
|
||||
EXPECT_EQ(track->remixerArtists[0].mbid.value(), core::UUID::fromString("6643f584-5edc-45ce-927d-0a4ab25c2673"));
|
||||
EXPECT_EQ(track->remixerArtists[1].name, "Artist3");
|
||||
ASSERT_TRUE(track->remixerArtists[1].mbid.has_value());
|
||||
EXPECT_EQ(track->remixerArtists[1].mbid.value(), core::UUID::fromString("ed42bcaf-e147-4f34-8f26-d74acc97670a"));
|
||||
|
||||
ASSERT_TRUE(track->performerArtists.contains("Rolea"));
|
||||
ASSERT_EQ(track->performerArtists["Rolea"].size(), 2);
|
||||
EXPECT_EQ(track->performerArtists["Rolea"][0].name, "Artist1");
|
||||
ASSERT_TRUE(track->performerArtists["Rolea"][0].mbid.has_value());
|
||||
EXPECT_EQ(track->performerArtists["Rolea"][0].mbid.value(), core::UUID::fromString("6643f584-5edc-45ce-927d-0a4ab25c2673"));
|
||||
EXPECT_EQ(track->performerArtists["Rolea"][1].name, "Artist3");
|
||||
ASSERT_TRUE(track->performerArtists["Rolea"][1].mbid.has_value());
|
||||
EXPECT_EQ(track->performerArtists["Rolea"][1].mbid.value(), core::UUID::fromString("ed42bcaf-e147-4f34-8f26-d74acc97670a"));
|
||||
ASSERT_EQ(track->performerArtists["Roleb"].size(), 2);
|
||||
EXPECT_EQ(track->performerArtists["Roleb"][0].name, "Artist2");
|
||||
ASSERT_TRUE(track->performerArtists["Roleb"][0].mbid.has_value());
|
||||
EXPECT_EQ(track->performerArtists["Roleb"][0].mbid.value(), core::UUID::fromString("481c5912-bf1a-47f7-b03c-d34e49711706"));
|
||||
EXPECT_EQ(track->performerArtists["Roleb"][1].name, "Artist4");
|
||||
ASSERT_TRUE(track->performerArtists["Roleb"][1].mbid.has_value());
|
||||
EXPECT_EQ(track->performerArtists["Roleb"][1].mbid.value(), core::UUID::fromString("6fc64a4b-26f5-441f-993c-fd511290233b"));
|
||||
}
|
||||
|
||||
TEST(AudioFileParser, MBIDs_fallback_priority)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
{
|
||||
{ TagType::Artist, { "Artist1" } },
|
||||
{ TagType::Album, { "MyAlbum" } },
|
||||
{ TagType::AlbumArtists, { "Artist1" } },
|
||||
{ TagType::MusicBrainzArtistID, { "6643f584-5edc-45ce-927d-0a4ab25c2673" } },
|
||||
{ TagType::MusicBrainzReleaseArtistID, { "ed42bcaf-e147-4f34-8f26-d74acc97670a" } },
|
||||
{ TagType::Composer, { "Artist1" } },
|
||||
}
|
||||
};
|
||||
std::unique_ptr<Track> track{ TestAudioFileParser{}.parseMetaData(testTags) };
|
||||
|
||||
ASSERT_EQ(track->composerArtists.size(), 1);
|
||||
EXPECT_EQ(track->composerArtists[0].name, "Artist1");
|
||||
ASSERT_TRUE(track->composerArtists[0].mbid.has_value());
|
||||
EXPECT_EQ(track->composerArtists[0].mbid.value(), core::UUID::fromString("6643f584-5edc-45ce-927d-0a4ab25c2673"));
|
||||
}
|
||||
|
||||
TEST(AudioFileParser, release_sortNameFallback)
|
||||
{
|
||||
const TestTagReader testTags{
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <cassert>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <shared_mutex>
|
||||
#include <unordered_map>
|
||||
@@ -27,7 +29,6 @@
|
||||
|
||||
#include "database/ImageId.hpp"
|
||||
#include "database/TrackEmbeddedImageId.hpp"
|
||||
#include "database/TrackId.hpp"
|
||||
#include "image/IEncodedImage.hpp"
|
||||
|
||||
namespace lms::cover
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
|
||||
#include "database/UserId.hpp"
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "database/StarredArtistId.hpp"
|
||||
#include "database/StarredReleaseId.hpp"
|
||||
#include "database/StarredTrackId.hpp"
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace lms::recommendation
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
return db::ScanSettings::get(session)->getSimilarityEngineType();
|
||||
return db::ScanSettings::find(session)->getSimilarityEngineType();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
add_library(lmsscanner STATIC
|
||||
impl/helpers/ArtistHelpers.cpp
|
||||
impl/scanners/ArtistInfoFileScanner.cpp
|
||||
impl/scanners/AudioFileScanOperation.cpp
|
||||
impl/scanners/AudioFileScanner.cpp
|
||||
impl/scanners/ImageFileScanner.cpp
|
||||
impl/scanners/LyricsFileScanner.cpp
|
||||
impl/scanners/PlayListFileScanner.cpp
|
||||
impl/scanners/Utils.cpp
|
||||
impl/steps/FileScanQueue.cpp
|
||||
impl/steps/ScanStepArtistReconciliation.cpp
|
||||
impl/steps/ScanStepAssociateArtistImages.cpp
|
||||
impl/steps/ScanStepAssociateExternalLyrics.cpp
|
||||
impl/steps/ScanStepAssociatePlayListTracks.cpp
|
||||
impl/steps/ScanStepAssociateReleaseImages.cpp
|
||||
impl/steps/ScanStepBase.cpp
|
||||
impl/steps/ScanStepCheckForDuplicatedFiles.cpp
|
||||
impl/steps/ScanStepCheckForRemovedFiles.cpp
|
||||
impl/steps/ScanStepCompact.cpp
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "core/ITraceLogger.hpp"
|
||||
#include "database/MediaLibrary.hpp"
|
||||
#include "database/ScanSettings.hpp"
|
||||
#include "database/Session.hpp"
|
||||
|
||||
#include "scanners/ArtistInfoFileScanner.hpp"
|
||||
#include "scanners/AudioFileScanner.hpp"
|
||||
@@ -35,6 +36,7 @@
|
||||
#include "scanners/LyricsFileScanner.hpp"
|
||||
#include "scanners/PlayListFileScanner.hpp"
|
||||
|
||||
#include "steps/ScanStepArtistReconciliation.hpp"
|
||||
#include "steps/ScanStepAssociateArtistImages.hpp"
|
||||
#include "steps/ScanStepAssociateExternalLyrics.hpp"
|
||||
#include "steps/ScanStepAssociatePlayListTracks.hpp"
|
||||
@@ -55,6 +57,9 @@ namespace lms::scanner
|
||||
|
||||
namespace
|
||||
{
|
||||
static constexpr std::string_view currentSettingsName{ "" };
|
||||
static constexpr std::string_view lastScanSettingsName{ "last_scan" };
|
||||
|
||||
Wt::WDate getNextMonday(Wt::WDate current)
|
||||
{
|
||||
do
|
||||
@@ -74,6 +79,61 @@ namespace lms::scanner
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
std::optional<ScannerSettings> readScannerSettings(db::Session& session, std::string_view name)
|
||||
{
|
||||
std::optional<ScannerSettings> settings;
|
||||
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const ScanSettings::pointer scanSettings{ ScanSettings::find(session, name) };
|
||||
if (!scanSettings)
|
||||
return settings;
|
||||
|
||||
settings.emplace();
|
||||
settings->audioScanVersion = scanSettings->getAudioScanVersion();
|
||||
settings->startTime = scanSettings->getUpdateStartTime();
|
||||
settings->updatePeriod = scanSettings->getUpdatePeriod();
|
||||
|
||||
MediaLibrary::find(session, [&](const MediaLibrary::pointer& mediaLibrary) {
|
||||
MediaLibraryInfo info;
|
||||
info.firstScan = mediaLibrary->isEmpty();
|
||||
info.id = mediaLibrary->getId();
|
||||
info.rootDirectory = mediaLibrary->getPath().lexically_normal();
|
||||
|
||||
settings->mediaLibraries.push_back(info);
|
||||
});
|
||||
|
||||
{
|
||||
const auto& tags{ scanSettings->getExtraTagsToScan() };
|
||||
std::transform(std::cbegin(tags), std::cend(tags), std::back_inserter(settings->extraTags), [](std::string_view tag) { return std::string{ tag }; });
|
||||
}
|
||||
|
||||
settings->artistTagDelimiters = scanSettings->getArtistTagDelimiters();
|
||||
settings->defaultTagDelimiters = scanSettings->getDefaultTagDelimiters();
|
||||
settings->artistsToNotSplit = scanSettings->getArtistsToNotSplit();
|
||||
|
||||
settings->skipSingleReleasePlayLists = scanSettings->getSkipSingleReleasePlayLists();
|
||||
settings->allowArtistMBIDFallback = scanSettings->getAllowMBIDArtistMerge();
|
||||
|
||||
// TODO, store this in DB + expose in UI
|
||||
settings->skipDuplicateTrackMBID = core::Service<core::IConfig>::get()->getBool("scanner-skip-duplicate-mbid", false);
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
void writeScannerSettings(db::Session& session, std::string_view name, const ScannerSettings& settings)
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
ScanSettings::pointer scanSettings{ ScanSettings::find(session, name) };
|
||||
if (!scanSettings)
|
||||
scanSettings = session.create<ScanSettings>(name);
|
||||
|
||||
scanSettings.modify()->setAllowMBIDArtistMerge(settings.allowArtistMBIDFallback);
|
||||
scanSettings.modify()->setSkipSingleReleasePlayLists(settings.skipSingleReleasePlayLists);
|
||||
// TODO add more fields
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<IScannerService> createScannerService(Db& db)
|
||||
@@ -282,26 +342,7 @@ namespace lms::scanner
|
||||
ScanStats& stats{ scanContext.stats };
|
||||
stats.startTime = Wt::WDateTime::currentDateTime();
|
||||
|
||||
std::size_t stepIndex{};
|
||||
for (auto& scanStep : _scanSteps)
|
||||
{
|
||||
LMS_SCOPED_TRACE_OVERVIEW("Scanner", scanStep->getStepName());
|
||||
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Starting scan step '" << scanStep->getStepName() << "'");
|
||||
scanContext.currentStepStats = ScanStepStats{
|
||||
.startTime = Wt::WDateTime::currentDateTime(),
|
||||
.stepCount = _scanSteps.size(),
|
||||
.stepIndex = stepIndex++,
|
||||
.currentStep = scanStep->getStep(),
|
||||
.totalElems = 0,
|
||||
.processedElems = 0
|
||||
};
|
||||
|
||||
notifyInProgress(scanContext.currentStepStats);
|
||||
scanStep->process(scanContext);
|
||||
notifyInProgress(scanContext.currentStepStats);
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Completed scan step '" << scanStep->getStepName() << "'");
|
||||
}
|
||||
processScanSteps(scanContext);
|
||||
|
||||
{
|
||||
std::unique_lock lock{ _statusMutex };
|
||||
@@ -321,6 +362,10 @@ namespace lms::scanner
|
||||
_lastCompleteScanStats = stats;
|
||||
}
|
||||
|
||||
// save current settings as last scan settings to compare during next scans if something changed
|
||||
writeScannerSettings(_db.getTLSSession(), lastScanSettingsName, _settings);
|
||||
_lastScanSettings = _settings;
|
||||
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Scan not aborted, scheduling next scan!");
|
||||
scheduleNextScan();
|
||||
|
||||
@@ -332,23 +377,61 @@ namespace lms::scanner
|
||||
}
|
||||
}
|
||||
|
||||
void ScannerService::processScanSteps(ScanContext& context)
|
||||
{
|
||||
std::size_t stepIndex{};
|
||||
for (auto& scanStep : _scanSteps)
|
||||
{
|
||||
context.currentStepStats = ScanStepStats{
|
||||
.startTime = Wt::WDateTime::currentDateTime(),
|
||||
.stepCount = _scanSteps.size(),
|
||||
.stepIndex = stepIndex++,
|
||||
.currentStep = scanStep->getStep(),
|
||||
.totalElems = 0,
|
||||
.processedElems = 0
|
||||
};
|
||||
|
||||
if (_abortScan)
|
||||
break;
|
||||
|
||||
if (!scanStep->needProcess(context))
|
||||
{
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Skipping scan step '" << scanStep->getStepName() << "'");
|
||||
continue;
|
||||
}
|
||||
|
||||
{
|
||||
LMS_SCOPED_TRACE_OVERVIEW("Scanner", scanStep->getStepName());
|
||||
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Starting scan step '" << scanStep->getStepName() << "'");
|
||||
notifyInProgress(context.currentStepStats);
|
||||
scanStep->process(context);
|
||||
notifyInProgress(context.currentStepStats);
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Completed scan step '" << scanStep->getStepName() << "'");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ScannerService::refreshScanSettings()
|
||||
{
|
||||
ScannerSettings newSettings{ readSettings() };
|
||||
if (_settings == newSettings)
|
||||
std::optional<ScannerSettings> newSettings{ readScannerSettings(_db.getTLSSession(), currentSettingsName) };
|
||||
assert(newSettings.has_value());
|
||||
if (_settings == *newSettings)
|
||||
return;
|
||||
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Scanner settings updated");
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Using scan settings version " << newSettings.scanVersion);
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Using audio scan settings version " << newSettings->audioScanVersion);
|
||||
|
||||
_settings = std::move(newSettings);
|
||||
_settings = std::move(*newSettings);
|
||||
if (!_lastScanSettings)
|
||||
_lastScanSettings = readScannerSettings(_db.getTLSSession(), lastScanSettingsName);
|
||||
|
||||
auto cbFunc{ [this](const ScanStepStats& stats) {
|
||||
notifyInProgressIfNeeded(stats);
|
||||
} };
|
||||
|
||||
_fileScanners.clear();
|
||||
_fileScanners.emplace_back(std::make_unique<ArtistInfoFileScanner>(_db));
|
||||
_fileScanners.emplace_back(std::make_unique<ArtistInfoFileScanner>(_settings, _db));
|
||||
_fileScanners.emplace_back(std::make_unique<AudioFileScanner>(_db, _settings));
|
||||
_fileScanners.emplace_back(std::make_unique<ImageFileScanner>(_db));
|
||||
_fileScanners.emplace_back(std::make_unique<LyricsFileScanner>(_db));
|
||||
@@ -359,6 +442,7 @@ namespace lms::scanner
|
||||
|
||||
ScanStepBase::InitParams params{
|
||||
.settings = _settings,
|
||||
.lastScanSettings = _lastScanSettings.has_value() ? &(_lastScanSettings.value()) : nullptr,
|
||||
.progressCallback = cbFunc,
|
||||
.abortScan = _abortScan,
|
||||
.db = _db,
|
||||
@@ -370,6 +454,7 @@ namespace lms::scanner
|
||||
_scanSteps.emplace_back(std::make_unique<ScanStepDiscoverFiles>(params));
|
||||
_scanSteps.emplace_back(std::make_unique<ScanStepScanFiles>(params));
|
||||
_scanSteps.emplace_back(std::make_unique<ScanStepCheckForRemovedFiles>(params));
|
||||
_scanSteps.emplace_back(std::make_unique<ScanStepArtistReconciliation>(params));
|
||||
_scanSteps.emplace_back(std::make_unique<ScanStepAssociatePlayListTracks>(params));
|
||||
_scanSteps.emplace_back(std::make_unique<ScanStepUpdateLibraryFields>(params));
|
||||
_scanSteps.emplace_back(std::make_unique<ScanStepAssociateArtistImages>(params));
|
||||
@@ -382,43 +467,6 @@ namespace lms::scanner
|
||||
_scanSteps.emplace_back(std::make_unique<ScanStepCheckForDuplicatedFiles>(params));
|
||||
}
|
||||
|
||||
ScannerSettings ScannerService::readSettings()
|
||||
{
|
||||
ScannerSettings newSettings;
|
||||
|
||||
newSettings.skipDuplicateMBID = core::Service<core::IConfig>::get()->getBool("scanner-skip-duplicate-mbid", false);
|
||||
{
|
||||
auto transaction{ _db.getTLSSession().createReadTransaction() };
|
||||
|
||||
const ScanSettings::pointer scanSettings{ ScanSettings::get(_db.getTLSSession()) };
|
||||
|
||||
newSettings.scanVersion = scanSettings->getScanVersion();
|
||||
newSettings.startTime = scanSettings->getUpdateStartTime();
|
||||
newSettings.updatePeriod = scanSettings->getUpdatePeriod();
|
||||
|
||||
MediaLibrary::find(_db.getTLSSession(), [&](const MediaLibrary::pointer& mediaLibrary) {
|
||||
MediaLibraryInfo info;
|
||||
info.firstScan = mediaLibrary->isEmpty();
|
||||
info.id = mediaLibrary->getId();
|
||||
info.rootDirectory = mediaLibrary->getPath().lexically_normal();
|
||||
|
||||
newSettings.mediaLibraries.push_back(info);
|
||||
});
|
||||
|
||||
{
|
||||
const auto& tags{ scanSettings->getExtraTagsToScan() };
|
||||
std::transform(std::cbegin(tags), std::cend(tags), std::back_inserter(newSettings.extraTags), [](std::string_view tag) { return std::string{ tag }; });
|
||||
}
|
||||
|
||||
newSettings.artistTagDelimiters = scanSettings->getArtistTagDelimiters();
|
||||
newSettings.defaultTagDelimiters = scanSettings->getDefaultTagDelimiters();
|
||||
|
||||
newSettings.skipSingleReleasePlayLists = scanSettings->getSkipSingleReleasePlayLists();
|
||||
}
|
||||
|
||||
return newSettings;
|
||||
}
|
||||
|
||||
void ScannerService::notifyInProgress(const ScanStepStats& stepStats)
|
||||
{
|
||||
{
|
||||
|
||||
@@ -39,6 +39,9 @@ namespace lms::scanner
|
||||
{
|
||||
class IFileScanner;
|
||||
|
||||
// Main goals to keepthe scanner fast:
|
||||
// - single pass on files: only 1 filesystem exploration must be done (no further reads triggered by parsed values)
|
||||
// - stable: 1 single scan (full or not) is enough: successive scans must have no effect if there is no change in the files
|
||||
class ScannerService : public IScannerService
|
||||
{
|
||||
public:
|
||||
@@ -65,12 +68,12 @@ namespace lms::scanner
|
||||
|
||||
// Update database (scheduled callback)
|
||||
void scan(const ScanOptions& scanOptions);
|
||||
void processScanSteps(ScanContext& context);
|
||||
|
||||
void scanMediaDirectory(const std::filesystem::path& mediaDirectory, bool forceScan, ScanStats& stats);
|
||||
|
||||
// Helpers
|
||||
void refreshScanSettings();
|
||||
ScannerSettings readSettings();
|
||||
|
||||
void notifyInProgressIfNeeded(const ScanStepStats& stats);
|
||||
void notifyInProgress(const ScanStepStats& stats);
|
||||
@@ -93,5 +96,6 @@ namespace lms::scanner
|
||||
Wt::WDateTime _nextScheduledScan;
|
||||
|
||||
ScannerSettings _settings;
|
||||
std::optional<ScannerSettings> _lastScanSettings;
|
||||
};
|
||||
} // namespace lms::scanner
|
||||
|
||||
@@ -35,14 +35,16 @@ namespace lms::scanner
|
||||
|
||||
struct ScannerSettings
|
||||
{
|
||||
std::size_t scanVersion{};
|
||||
std::size_t audioScanVersion{};
|
||||
Wt::WTime startTime;
|
||||
db::ScanSettings::UpdatePeriod updatePeriod{ db::ScanSettings::UpdatePeriod::Never };
|
||||
bool skipDuplicateMBID{};
|
||||
bool skipDuplicateTrackMBID{};
|
||||
std::vector<std::string> extraTags;
|
||||
std::vector<std::string> artistTagDelimiters;
|
||||
std::vector<std::string> artistsToNotSplit;
|
||||
std::vector<std::string> defaultTagDelimiters;
|
||||
bool skipSingleReleasePlayLists{};
|
||||
bool allowArtistMBIDFallback{ true }; // TODO false?
|
||||
|
||||
std::vector<MediaLibraryInfo> mediaLibraries;
|
||||
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* 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 "ArtistHelpers.hpp"
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "metadata/Types.hpp"
|
||||
|
||||
namespace lms::scanner::helpers
|
||||
{
|
||||
namespace
|
||||
{
|
||||
db::Artist::pointer createArtist(db::Session& session, const metadata::Artist& artistInfo)
|
||||
{
|
||||
db::Artist::pointer artist{ session.create<db::Artist>(artistInfo.name) };
|
||||
|
||||
if (artistInfo.mbid)
|
||||
artist.modify()->setMBID(artistInfo.mbid);
|
||||
|
||||
artist.modify()->setSortName(artistInfo.sortName ? *artistInfo.sortName : artistInfo.name);
|
||||
|
||||
return artist;
|
||||
}
|
||||
|
||||
std::string optionalMBIDAsString(const std::optional<core::UUID>& uuid)
|
||||
{
|
||||
return uuid ? std::string{ uuid->getAsString() } : "<no MBID>";
|
||||
}
|
||||
|
||||
void updateArtistIfNeeded(db::Artist::pointer artist, const metadata::Artist& artistInfo)
|
||||
{
|
||||
// MBID may be set
|
||||
if (artist->getMBID() != artistInfo.mbid)
|
||||
artist.modify()->setMBID(artistInfo.mbid);
|
||||
|
||||
// Name may have been updated
|
||||
if (artist->getName() != artistInfo.name)
|
||||
{
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Artist [" << optionalMBIDAsString(artist->getMBID()) << "], updated name from '" << artist->getName() << "' to '" << artistInfo.name << "'");
|
||||
artist.modify()->setName(artistInfo.name);
|
||||
}
|
||||
|
||||
// Sortname may have been updated
|
||||
// As the sort name is quite often not filled in, we update it only if already set (for now?)
|
||||
if (artistInfo.sortName && *artistInfo.sortName != artist->getSortName())
|
||||
{
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Artist [" << optionalMBIDAsString(artist->getMBID()) << "], updated sort name from '" << artist->getSortName() << "' to '" << *artistInfo.sortName << "'");
|
||||
artist.modify()->setSortName(*artistInfo.sortName);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
db::Artist::pointer getOrCreateArtistByMBID(db::Session& session, const metadata::Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries)
|
||||
{
|
||||
assert(artistInfo.mbid.has_value());
|
||||
db::Artist::pointer artist{ db::Artist::find(session, *artistInfo.mbid) };
|
||||
if (artist)
|
||||
{
|
||||
updateArtistIfNeeded(artist, artistInfo);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (allowFallbackOnMBIDEntries.value())
|
||||
{
|
||||
// an artist with the same name may already exist, let's recycle it
|
||||
for (const db::Artist::pointer& artistWithSameName : db::Artist::find(session, artistInfo.name))
|
||||
{
|
||||
if (!artistWithSameName->hasMBID())
|
||||
{
|
||||
artist = artistWithSameName;
|
||||
updateArtistIfNeeded(artist, artistInfo);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!artist)
|
||||
artist = createArtist(session, artistInfo);
|
||||
}
|
||||
|
||||
return artist;
|
||||
}
|
||||
|
||||
db::Artist::pointer getOrCreateArtistByName(db::Session& session, const metadata::Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries)
|
||||
{
|
||||
db::Artist::pointer artist;
|
||||
|
||||
// Here we can have only one artist with no MBID, others all have mbids
|
||||
const std::vector<db::Artist::pointer> artistsWithSameName{ db::Artist::find(session, artistInfo.name) };
|
||||
const auto itArtistWithoutMBID{ std::find_if(std::begin(artistsWithSameName), std::end(artistsWithSameName), [](const db::Artist::pointer& artist) { return !artist->hasMBID(); }) };
|
||||
const std::size_t artistCountWithMBID{ artistsWithSameName.size() - (itArtistWithoutMBID != std::end(artistsWithSameName) ? 1 : 0) };
|
||||
|
||||
if (!allowFallbackOnMBIDEntries.value() || artistCountWithMBID > 1)
|
||||
{
|
||||
if (itArtistWithoutMBID != std::end(artistsWithSameName))
|
||||
{
|
||||
artist = *itArtistWithoutMBID;
|
||||
updateArtistIfNeeded(artist, artistInfo);
|
||||
}
|
||||
else
|
||||
artist = createArtist(session, artistInfo);
|
||||
}
|
||||
else
|
||||
{
|
||||
const auto itArtistWithMBID{ std::find_if(std::begin(artistsWithSameName), std::end(artistsWithSameName), [](const db::Artist::pointer& artist) { return artist->hasMBID(); }) };
|
||||
|
||||
if (itArtistWithMBID != std::end(artistsWithSameName))
|
||||
{
|
||||
artist = *itArtistWithMBID;
|
||||
// not updating artist here: consider metadata quality is less good
|
||||
}
|
||||
else if (itArtistWithoutMBID != std::end(artistsWithSameName))
|
||||
{
|
||||
artist = *itArtistWithoutMBID;
|
||||
updateArtistIfNeeded(artist, artistInfo);
|
||||
}
|
||||
else
|
||||
artist = createArtist(session, artistInfo);
|
||||
}
|
||||
|
||||
return artist;
|
||||
}
|
||||
|
||||
db::Artist::pointer getOrCreateArtist(db::Session& session, const metadata::Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries)
|
||||
{
|
||||
// First try to get by MBID
|
||||
if (artistInfo.mbid)
|
||||
return getOrCreateArtistByMBID(session, artistInfo, allowFallbackOnMBIDEntries);
|
||||
|
||||
// Fall back on artist name (collisions may occur)
|
||||
return getOrCreateArtistByName(session, artistInfo, allowFallbackOnMBIDEntries);
|
||||
}
|
||||
} // namespace lms::scanner::helpers
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* 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 "core/TaggedType.hpp"
|
||||
#include "database/Artist.hpp"
|
||||
|
||||
namespace lms::metadata
|
||||
{
|
||||
struct Artist;
|
||||
}
|
||||
|
||||
namespace lms::scanner::helpers
|
||||
{
|
||||
using AllowFallbackOnMBIDEntry = core::TaggedBool<struct AllowFallbackOnMBIDEntryTag>;
|
||||
|
||||
db::Artist::pointer getOrCreateArtistByMBID(db::Session& session, const metadata::Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries);
|
||||
db::Artist::pointer getOrCreateArtistByName(db::Session& session, const metadata::Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries);
|
||||
db::Artist::pointer getOrCreateArtist(db::Session& session, const metadata::Artist& artistInfo, AllowFallbackOnMBIDEntry allowFallbackOnMBIDEntries);
|
||||
|
||||
} // namespace lms::scanner::helpers
|
||||
@@ -32,7 +32,10 @@
|
||||
|
||||
#include "IFileScanOperation.hpp"
|
||||
#include "ScanContext.hpp"
|
||||
#include "ScannerSettings.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "helpers/ArtistHelpers.hpp"
|
||||
#include "metadata/Types.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
@@ -41,10 +44,13 @@ namespace lms::scanner
|
||||
class ArtistInfoFileScanOperation : public IFileScanOperation
|
||||
{
|
||||
public:
|
||||
ArtistInfoFileScanOperation(const FileToScan& file, db::Db& db)
|
||||
ArtistInfoFileScanOperation(const FileToScan& file, const ScannerSettings& settings, db::Db& db)
|
||||
: _file{ file.file }
|
||||
, _mediaLibrary{ file.mediaLibrary }
|
||||
, _db{ db } {}
|
||||
, _settings{ settings }
|
||||
, _db{ db }
|
||||
{
|
||||
}
|
||||
~ArtistInfoFileScanOperation() override = default;
|
||||
ArtistInfoFileScanOperation(const ArtistInfoFileScanOperation&) = delete;
|
||||
ArtistInfoFileScanOperation& operator=(const ArtistInfoFileScanOperation&) = delete;
|
||||
@@ -59,6 +65,7 @@ namespace lms::scanner
|
||||
|
||||
const std::filesystem::path _file;
|
||||
const MediaLibraryInfo _mediaLibrary;
|
||||
const ScannerSettings& _settings;
|
||||
db::Db& _db;
|
||||
|
||||
std::optional<metadata::ArtistInfo> _parsedArtistInfo;
|
||||
@@ -76,12 +83,7 @@ namespace lms::scanner
|
||||
}
|
||||
|
||||
_parsedArtistInfo = metadata::parseArtistInfo(ifs);
|
||||
if (!_parsedArtistInfo->mbid.has_value())
|
||||
{
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Discarding artist info in file " << _file << ": no mbid set");
|
||||
_parsedArtistInfo.reset();
|
||||
}
|
||||
else if (_parsedArtistInfo->name.empty())
|
||||
if (_parsedArtistInfo->name.empty())
|
||||
{
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Discarding artist info in file " << _file << ": no name set");
|
||||
_parsedArtistInfo.reset();
|
||||
@@ -125,6 +127,8 @@ namespace lms::scanner
|
||||
artistInfo.modify()->setAbsoluteFilePath(_file);
|
||||
}
|
||||
|
||||
artistInfo.modify()->setName(_parsedArtistInfo->name);
|
||||
artistInfo.modify()->setSortName(_parsedArtistInfo->sortName);
|
||||
artistInfo.modify()->setLastWriteTime(fileInfo->lastWriteTime);
|
||||
artistInfo.modify()->setType(_parsedArtistInfo->type);
|
||||
artistInfo.modify()->setGender(_parsedArtistInfo->gender);
|
||||
@@ -134,12 +138,8 @@ namespace lms::scanner
|
||||
db::MediaLibrary::pointer mediaLibrary{ db::MediaLibrary::find(dbSession, _mediaLibrary.id) }; // may be null if settings are updated in // => next scan will correct this
|
||||
artistInfo.modify()->setDirectory(utils::getOrCreateDirectory(dbSession, _file.parent_path(), mediaLibrary));
|
||||
|
||||
db::Artist::pointer artist{ db::Artist::find(dbSession, *_parsedArtistInfo->mbid) };
|
||||
if (!artist)
|
||||
artist = dbSession.create<db::Artist>(_parsedArtistInfo->name, _parsedArtistInfo->mbid);
|
||||
|
||||
artist.modify()->setName(_parsedArtistInfo->name);
|
||||
artist.modify()->setSortName(_parsedArtistInfo->sortName);
|
||||
const metadata::Artist artistMetadata{ _parsedArtistInfo->mbid, _parsedArtistInfo->name, _parsedArtistInfo->sortName.empty() ? std::nullopt : std::make_optional<std::string>(_parsedArtistInfo->sortName) };
|
||||
db::Artist::pointer artist{ helpers::getOrCreateArtist(dbSession, artistMetadata, helpers::AllowFallbackOnMBIDEntry{ _settings.allowArtistMBIDFallback }) };
|
||||
artistInfo.modify()->setArtist(artist);
|
||||
|
||||
if (added)
|
||||
@@ -155,8 +155,9 @@ namespace lms::scanner
|
||||
}
|
||||
} // namespace
|
||||
|
||||
ArtistInfoFileScanner::ArtistInfoFileScanner(db::Db& db)
|
||||
: _db{ db }
|
||||
ArtistInfoFileScanner::ArtistInfoFileScanner(const ScannerSettings& settings, db::Db& db)
|
||||
: _settings{ settings }
|
||||
, _db{ db }
|
||||
{
|
||||
}
|
||||
|
||||
@@ -202,6 +203,6 @@ namespace lms::scanner
|
||||
|
||||
std::unique_ptr<IFileScanOperation> ArtistInfoFileScanner::createScanOperation(const FileToScan& fileToScan) const
|
||||
{
|
||||
return std::make_unique<ArtistInfoFileScanOperation>(fileToScan, _db);
|
||||
return std::make_unique<ArtistInfoFileScanOperation>(fileToScan, _settings, _db);
|
||||
}
|
||||
} // namespace lms::scanner
|
||||
@@ -31,10 +31,12 @@ namespace lms
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
struct ScannerSettings;
|
||||
|
||||
class ArtistInfoFileScanner : public IFileScanner
|
||||
{
|
||||
public:
|
||||
ArtistInfoFileScanner(db::Db& db);
|
||||
ArtistInfoFileScanner(const ScannerSettings& _settings, db::Db& db);
|
||||
~ArtistInfoFileScanner() override = default;
|
||||
ArtistInfoFileScanner(const ArtistInfoFileScanner&) = delete;
|
||||
ArtistInfoFileScanner& operator=(const ArtistInfoFileScanner&) = delete;
|
||||
@@ -45,6 +47,7 @@ namespace lms::scanner
|
||||
bool needsScan(ScanContext& context, const FileToScan& file) const override;
|
||||
std::unique_ptr<IFileScanOperation> createScanOperation(const FileToScan& fileToScan) const override;
|
||||
|
||||
const ScannerSettings& _settings;
|
||||
db::Db& _db;
|
||||
};
|
||||
} // namespace lms::scanner
|
||||
@@ -0,0 +1,744 @@
|
||||
/*
|
||||
* 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 "AudioFileScanOperation.hpp"
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/ITraceLogger.hpp"
|
||||
#include "core/PartialDateTime.hpp"
|
||||
#include "core/Path.hpp"
|
||||
#include "core/XxHash3.hpp"
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Cluster.hpp"
|
||||
#include "database/Db.hpp"
|
||||
#include "database/Directory.hpp"
|
||||
#include "database/MediaLibrary.hpp"
|
||||
#include "database/Release.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "database/TrackArtistLink.hpp"
|
||||
#include "database/TrackEmbeddedImage.hpp"
|
||||
#include "database/TrackEmbeddedImageLink.hpp"
|
||||
#include "database/TrackFeatures.hpp"
|
||||
#include "database/TrackLyrics.hpp"
|
||||
#include "database/Types.hpp"
|
||||
#include "image/Exception.hpp"
|
||||
#include "image/Image.hpp"
|
||||
#include "metadata/Exception.hpp"
|
||||
|
||||
#include "IFileScanOperation.hpp"
|
||||
#include "ScanContext.hpp"
|
||||
#include "ScannerSettings.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "helpers/ArtistHelpers.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
namespace
|
||||
{
|
||||
void createTrackArtistLinks(db::Session& session, const db::Track::pointer& track, db::TrackArtistLinkType linkType, std::string_view role, std::span<const metadata::Artist> artists, helpers::AllowFallbackOnMBIDEntry allowArtistMBIDFallback)
|
||||
{
|
||||
for (const metadata::Artist& artistInfo : artists)
|
||||
{
|
||||
db::Artist::pointer artist{ helpers::getOrCreateArtist(session, artistInfo, allowArtistMBIDFallback) };
|
||||
|
||||
const bool matchedUsingMbid{ artist->getMBID() == artistInfo.mbid };
|
||||
db::TrackArtistLink::pointer link{ session.create<db::TrackArtistLink>(track, artist, linkType, role, matchedUsingMbid) };
|
||||
link.modify()->setArtistName(artistInfo.name);
|
||||
if (artistInfo.sortName)
|
||||
link.modify()->setArtistSortName(*artistInfo.sortName);
|
||||
}
|
||||
}
|
||||
|
||||
void createTrackArtistLinks(db::Session& session, const db::Track::pointer& track, db::TrackArtistLinkType linkType, std::span<const metadata::Artist> artists, helpers::AllowFallbackOnMBIDEntry allowArtistMBIDFallback)
|
||||
{
|
||||
constexpr std::string_view noRole{};
|
||||
createTrackArtistLinks(session, track, linkType, noRole, artists, allowArtistMBIDFallback);
|
||||
}
|
||||
|
||||
db::ReleaseType::pointer getOrCreateReleaseType(db::Session& session, std::string_view name)
|
||||
{
|
||||
db::ReleaseType::pointer releaseType{ db::ReleaseType::find(session, name) };
|
||||
if (!releaseType)
|
||||
releaseType = session.create<db::ReleaseType>(name);
|
||||
|
||||
return releaseType;
|
||||
}
|
||||
|
||||
db::Country::pointer getOrCreateCountry(db::Session& session, std::string_view name)
|
||||
{
|
||||
db::Country::pointer country{ db::Country::find(session, name) };
|
||||
if (!country)
|
||||
country = session.create<db::Country>(name);
|
||||
|
||||
return country;
|
||||
}
|
||||
|
||||
db::Label::pointer getOrCreateLabel(db::Session& session, std::string_view name)
|
||||
{
|
||||
db::Label::pointer label{ db::Label::find(session, name) };
|
||||
if (!label)
|
||||
label = session.create<db::Label>(name);
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
void updateReleaseIfNeeded(db::Session& session, db::Release::pointer release, const metadata::Release& releaseInfo)
|
||||
{
|
||||
if (release->getName() != releaseInfo.name)
|
||||
release.modify()->setName(releaseInfo.name);
|
||||
if (release->getSortName() != releaseInfo.sortName)
|
||||
release.modify()->setSortName(releaseInfo.sortName);
|
||||
if (release->getGroupMBID() != releaseInfo.groupMBID)
|
||||
release.modify()->setGroupMBID(releaseInfo.groupMBID);
|
||||
if (release->getTotalDisc() != releaseInfo.mediumCount)
|
||||
release.modify()->setTotalDisc(releaseInfo.mediumCount);
|
||||
if (release->getArtistDisplayName() != releaseInfo.artistDisplayName)
|
||||
release.modify()->setArtistDisplayName(releaseInfo.artistDisplayName);
|
||||
if (release->isCompilation() != releaseInfo.isCompilation)
|
||||
release.modify()->setCompilation(releaseInfo.isCompilation);
|
||||
if (release->getBarcode() != releaseInfo.barcode)
|
||||
release.modify()->setBarcode(releaseInfo.barcode);
|
||||
if (release->getComment() != releaseInfo.comment)
|
||||
release.modify()->setComment(releaseInfo.comment);
|
||||
if (release->getReleaseTypeNames() != releaseInfo.releaseTypes)
|
||||
{
|
||||
release.modify()->clearReleaseTypes();
|
||||
for (std::string_view releaseType : releaseInfo.releaseTypes)
|
||||
release.modify()->addReleaseType(getOrCreateReleaseType(session, releaseType));
|
||||
}
|
||||
if (release->getCountryNames() != releaseInfo.countries)
|
||||
{
|
||||
release.modify()->clearCountries();
|
||||
for (std::string_view country : releaseInfo.countries)
|
||||
release.modify()->addCountry(getOrCreateCountry(session, country));
|
||||
}
|
||||
if (release->getLabelNames() != releaseInfo.labels)
|
||||
{
|
||||
release.modify()->clearLabels();
|
||||
for (std::string_view label : releaseInfo.labels)
|
||||
release.modify()->addLabel(getOrCreateLabel(session, label));
|
||||
}
|
||||
}
|
||||
|
||||
// Compare release level info
|
||||
bool isReleaseMatching(const db::Release::pointer& candidateRelease, const metadata::Release& releaseInfo)
|
||||
{
|
||||
// TODO: add more criterias?
|
||||
return candidateRelease->getName() == releaseInfo.name
|
||||
&& candidateRelease->getSortName() == releaseInfo.sortName
|
||||
&& candidateRelease->getTotalDisc() == releaseInfo.mediumCount
|
||||
&& candidateRelease->isCompilation() == releaseInfo.isCompilation
|
||||
&& candidateRelease->getLabelNames() == releaseInfo.labels
|
||||
&& candidateRelease->getBarcode() == releaseInfo.barcode;
|
||||
}
|
||||
|
||||
db::Release::pointer getOrCreateRelease(db::Session& session, const metadata::Release& releaseInfo, const db::Directory::pointer& currentDirectory)
|
||||
{
|
||||
db::Release::pointer release;
|
||||
|
||||
// First try to get by MBID: fastest, safest
|
||||
if (releaseInfo.mbid)
|
||||
{
|
||||
release = db::Release::find(session, *releaseInfo.mbid);
|
||||
if (!release)
|
||||
release = session.create<db::Release>(releaseInfo.name, releaseInfo.mbid);
|
||||
}
|
||||
else if (releaseInfo.name.empty())
|
||||
{
|
||||
// No release name (only mbid) -> nothing to do
|
||||
return release;
|
||||
}
|
||||
|
||||
// Fall back on release name (collisions may occur)
|
||||
// First try using all sibling directories (case for Album/DiscX), only if the disc number is set
|
||||
const db::DirectoryId parentDirectoryId{ currentDirectory->getParentDirectoryId() };
|
||||
if (!release && releaseInfo.mediumCount && *releaseInfo.mediumCount > 1 && parentDirectoryId.isValid())
|
||||
{
|
||||
db::Release::FindParameters params;
|
||||
params.setParentDirectory(parentDirectoryId);
|
||||
params.setName(releaseInfo.name);
|
||||
db::Release::find(session, params, [&](const db::Release::pointer& candidateRelease) {
|
||||
// Already found a candidate
|
||||
if (release)
|
||||
return;
|
||||
|
||||
// Do not fallback on properly tagged releases
|
||||
if (candidateRelease->getMBID().has_value())
|
||||
return;
|
||||
|
||||
if (!isReleaseMatching(candidateRelease, releaseInfo))
|
||||
return;
|
||||
|
||||
release = candidateRelease;
|
||||
});
|
||||
}
|
||||
|
||||
// Lastly try in the current directory: we do this at last to have
|
||||
// opportunities to merge releases in case of migration / rescan
|
||||
if (!release)
|
||||
{
|
||||
db::Release::FindParameters params;
|
||||
params.setDirectory(currentDirectory->getId());
|
||||
params.setName(releaseInfo.name);
|
||||
db::Release::find(session, params, [&](const db::Release::pointer& candidateRelease) {
|
||||
// Already found a candidate
|
||||
if (release)
|
||||
return;
|
||||
|
||||
// Do not fallback on properly tagged releases
|
||||
if (candidateRelease->getMBID().has_value())
|
||||
return;
|
||||
|
||||
if (!isReleaseMatching(candidateRelease, releaseInfo))
|
||||
return;
|
||||
|
||||
release = candidateRelease;
|
||||
});
|
||||
}
|
||||
|
||||
if (!release)
|
||||
release = session.create<db::Release>(releaseInfo.name);
|
||||
|
||||
updateReleaseIfNeeded(session, release, releaseInfo);
|
||||
return release;
|
||||
}
|
||||
|
||||
std::vector<db::Cluster::pointer> getOrCreateClusters(db::Session& session, const metadata::Track& track)
|
||||
{
|
||||
std::vector<db::Cluster::pointer> clusters;
|
||||
|
||||
auto getOrCreateClusters{ [&](std::string_view tag, std::span<const std::string> values) {
|
||||
auto clusterType = db::ClusterType::find(session, tag);
|
||||
if (!clusterType)
|
||||
clusterType = session.create<db::ClusterType>(tag);
|
||||
|
||||
for (const auto& value : values)
|
||||
{
|
||||
auto cluster{ clusterType->getCluster(value) };
|
||||
if (!cluster)
|
||||
cluster = session.create<db::Cluster>(clusterType, value);
|
||||
|
||||
clusters.push_back(cluster);
|
||||
}
|
||||
} };
|
||||
|
||||
// TODO: migrate these fields in dedicated tables in DB
|
||||
getOrCreateClusters("GENRE", track.genres);
|
||||
getOrCreateClusters("MOOD", track.moods);
|
||||
getOrCreateClusters("LANGUAGE", track.languages);
|
||||
getOrCreateClusters("GROUPING", track.groupings);
|
||||
|
||||
for (const auto& [tag, values] : track.userExtraTags)
|
||||
getOrCreateClusters(tag, values);
|
||||
|
||||
return clusters;
|
||||
}
|
||||
|
||||
db::TrackLyrics::pointer createLyrics(db::Session& session, const metadata::Lyrics& lyricsInfo)
|
||||
{
|
||||
db::TrackLyrics::pointer lyrics{ session.create<db::TrackLyrics>() };
|
||||
|
||||
lyrics.modify()->setLanguage(!lyricsInfo.language.empty() ? lyricsInfo.language : "xxx");
|
||||
lyrics.modify()->setOffset(lyricsInfo.offset);
|
||||
lyrics.modify()->setDisplayArtist(lyricsInfo.displayArtist);
|
||||
lyrics.modify()->setDisplayTitle(lyricsInfo.displayTitle);
|
||||
if (!lyricsInfo.synchronizedLines.empty())
|
||||
lyrics.modify()->setSynchronizedLines(lyricsInfo.synchronizedLines);
|
||||
else
|
||||
lyrics.modify()->setUnsynchronizedLines(lyricsInfo.unsynchronizedLines);
|
||||
|
||||
return lyrics;
|
||||
}
|
||||
|
||||
db::ImageType convertImageType(metadata::Image::Type type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case metadata::Image::Type::Unknown:
|
||||
return db::ImageType::Unknown;
|
||||
case metadata::Image::Type::Other:
|
||||
return db::ImageType::Other;
|
||||
case metadata::Image::Type::FileIcon:
|
||||
return db::ImageType::FileIcon;
|
||||
case metadata::Image::Type::OtherFileIcon:
|
||||
return db::ImageType::OtherFileIcon;
|
||||
case metadata::Image::Type::FrontCover:
|
||||
return db::ImageType::FrontCover;
|
||||
case metadata::Image::Type::BackCover:
|
||||
return db::ImageType::BackCover;
|
||||
case metadata::Image::Type::LeafletPage:
|
||||
return db::ImageType::LeafletPage;
|
||||
case metadata::Image::Type::Media:
|
||||
return db::ImageType::Media;
|
||||
case metadata::Image::Type::LeadArtist:
|
||||
return db::ImageType::LeadArtist;
|
||||
case metadata::Image::Type::Artist:
|
||||
return db::ImageType::Artist;
|
||||
case metadata::Image::Type::Conductor:
|
||||
return db::ImageType::Conductor;
|
||||
case metadata::Image::Type::Band:
|
||||
return db::ImageType::Band;
|
||||
case metadata::Image::Type::Composer:
|
||||
return db::ImageType::Composer;
|
||||
case metadata::Image::Type::Lyricist:
|
||||
return db::ImageType::Lyricist;
|
||||
case metadata::Image::Type::RecordingLocation:
|
||||
return db::ImageType::RecordingLocation;
|
||||
case metadata::Image::Type::DuringRecording:
|
||||
return db::ImageType::DuringRecording;
|
||||
case metadata::Image::Type::DuringPerformance:
|
||||
return db::ImageType::DuringPerformance;
|
||||
case metadata::Image::Type::MovieScreenCapture:
|
||||
return db::ImageType::MovieScreenCapture;
|
||||
case metadata::Image::Type::ColouredFish:
|
||||
return db::ImageType::ColouredFish;
|
||||
case metadata::Image::Type::Illustration:
|
||||
return db::ImageType::Illustration;
|
||||
case metadata::Image::Type::BandLogo:
|
||||
return db::ImageType::BandLogo;
|
||||
case metadata::Image::Type::PublisherLogo:
|
||||
return db::ImageType::PublisherLogo;
|
||||
}
|
||||
|
||||
return db::ImageType::Unknown;
|
||||
}
|
||||
|
||||
db::TrackEmbeddedImage::pointer getOrCreateTrackEmbeddedImage(db::Session& session, const ImageInfo& imageInfo)
|
||||
{
|
||||
db::TrackEmbeddedImage::pointer image{ db::TrackEmbeddedImage::find(session, imageInfo.size, db::ImageHashType{ imageInfo.hash }) };
|
||||
if (!image)
|
||||
{
|
||||
image = session.create<db::TrackEmbeddedImage>();
|
||||
image.modify()->setSize(imageInfo.size);
|
||||
image.modify()->setHash(db::ImageHashType{ imageInfo.hash });
|
||||
image.modify()->setWidth(imageInfo.properties.width);
|
||||
image.modify()->setHeight(imageInfo.properties.height);
|
||||
image.modify()->setMimeType(imageInfo.mimeType);
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
db::TrackEmbeddedImageLink::pointer createTrackEmbeddedImageLink(db::Session& session, const db::Track::pointer& track, const ImageInfo& imageInfo)
|
||||
{
|
||||
const db::TrackEmbeddedImage::pointer image{ getOrCreateTrackEmbeddedImage(session, imageInfo) };
|
||||
db::TrackEmbeddedImageLink::pointer imageLink{ session.create<db::TrackEmbeddedImageLink>(track, image) };
|
||||
imageLink.modify()->setIndex(imageInfo.index);
|
||||
imageLink.modify()->setType(convertImageType(imageInfo.type));
|
||||
imageLink.modify()->setDescription(imageInfo.description);
|
||||
|
||||
return imageLink;
|
||||
}
|
||||
|
||||
void updateEmbeddedImages(db::Session& session, db::Track::pointer& track, std::span<const ImageInfo> images)
|
||||
{
|
||||
db::TrackEmbeddedImageLink::pointer preferredImageLink;
|
||||
|
||||
track.modify()->clearEmbeddedImageLinks();
|
||||
for (const ImageInfo& imageInfo : images)
|
||||
{
|
||||
db::TrackEmbeddedImageLink::pointer link{ createTrackEmbeddedImageLink(session, track, imageInfo) };
|
||||
track.modify()->addEmbeddedImageLink(link);
|
||||
|
||||
if (!preferredImageLink
|
||||
|| (preferredImageLink->getType() != db::ImageType::FrontCover && link->getType() == db::ImageType::FrontCover)
|
||||
|| (preferredImageLink->getImage()->getSize() < link->getImage()->getSize()))
|
||||
{
|
||||
preferredImageLink = link;
|
||||
}
|
||||
}
|
||||
|
||||
if (preferredImageLink)
|
||||
preferredImageLink.modify()->setIsPreferred(true);
|
||||
}
|
||||
|
||||
db::Advisory getAdvisory(std::optional<metadata::Track::Advisory> advisory)
|
||||
{
|
||||
if (!advisory)
|
||||
return db::Advisory::UnSet;
|
||||
|
||||
switch (advisory.value())
|
||||
{
|
||||
case metadata::Track::Advisory::Clean:
|
||||
return db::Advisory::Clean;
|
||||
case metadata::Track::Advisory::Explicit:
|
||||
return db::Advisory::Explicit;
|
||||
case metadata::Track::Advisory::Unknown:
|
||||
return db::Advisory::Unknown;
|
||||
}
|
||||
|
||||
return db::Advisory::UnSet;
|
||||
}
|
||||
|
||||
db::Track::pointer findMovedTrackBySizeAndMetaData(db::Session& session, const metadata::Track& parsedTrack, const FileInfo& fileInfo)
|
||||
{
|
||||
db::Track::FindParameters params;
|
||||
// Add as many fields as possible to limit errors
|
||||
params.setName(parsedTrack.title);
|
||||
if (parsedTrack.medium)
|
||||
{
|
||||
if (parsedTrack.medium->position)
|
||||
params.setDiscNumber(*parsedTrack.medium->position);
|
||||
if (parsedTrack.medium->release)
|
||||
params.setReleaseName(parsedTrack.medium->release->name);
|
||||
}
|
||||
if (parsedTrack.position)
|
||||
params.setTrackNumber(*parsedTrack.position);
|
||||
params.setFileSize(fileInfo.fileSize);
|
||||
|
||||
bool error{};
|
||||
db::Track::pointer res;
|
||||
db::Track::find(session, params, [&](const db::Track::pointer& track) {
|
||||
// Check that the track is truly no longer where it was during the last scan
|
||||
std::error_code ec;
|
||||
if (std::filesystem::exists(track->getAbsoluteFilePath(), ec))
|
||||
return;
|
||||
|
||||
if (res)
|
||||
{
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Found too many candidates for file move. New file = " << fileInfo.relativePath << ", candidate = " << track->getAbsoluteFilePath() << ", previous candidate = " << res->getAbsoluteFilePath());
|
||||
error = true;
|
||||
}
|
||||
res = track;
|
||||
});
|
||||
|
||||
if (error)
|
||||
res = db::Track::pointer{};
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
void fillInArtistsWithMbid(std::span<const metadata::Artist> artists, std::unordered_map<std::string_view, core::UUID>& artistsWithMbid)
|
||||
{
|
||||
for (const metadata::Artist& artist : artists)
|
||||
{
|
||||
if (artist.mbid.has_value())
|
||||
{
|
||||
// there may collisions, we don't want to replace
|
||||
artistsWithMbid.emplace(artist.name, *artist.mbid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void fillInMbids(std::span<metadata::Artist> artists, const std::unordered_map<std::string_view, core::UUID>& artistsWithMbid)
|
||||
{
|
||||
for (metadata::Artist& artist : artists)
|
||||
{
|
||||
if (!artist.mbid)
|
||||
{
|
||||
const auto it{ artistsWithMbid.find(artist.name) };
|
||||
if (it != std::cend(artistsWithMbid))
|
||||
artist.mbid = it->second;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void fillMissingMbids(metadata::Track& track)
|
||||
{
|
||||
// first pass: collect all artists that have mbids
|
||||
std::unordered_map<std::string_view, core::UUID> artistsWithMbid;
|
||||
|
||||
// For now, mbids can only set in artist and album artist tags
|
||||
// filling order is important: we estimate track-level artists are more likely
|
||||
// to be set in other fields than album artists
|
||||
fillInArtistsWithMbid(track.artists, artistsWithMbid);
|
||||
if (track.medium && track.medium->release)
|
||||
fillInArtistsWithMbid(track.medium->release->artists, artistsWithMbid);
|
||||
|
||||
// second pass: fill in all artists that have no mbid set with the same name
|
||||
fillInMbids(track.conductorArtists, artistsWithMbid);
|
||||
fillInMbids(track.composerArtists, artistsWithMbid);
|
||||
fillInMbids(track.lyricistArtists, artistsWithMbid);
|
||||
fillInMbids(track.mixerArtists, artistsWithMbid);
|
||||
fillInMbids(track.producerArtists, artistsWithMbid);
|
||||
fillInMbids(track.remixerArtists, artistsWithMbid);
|
||||
for (auto& [role, artists] : track.performerArtists)
|
||||
fillInMbids(artists, artistsWithMbid);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void AudioFileScanOperation::scan()
|
||||
{
|
||||
LMS_SCOPED_TRACE_OVERVIEW("Scanner", "ScanAudioFile");
|
||||
std::unique_ptr<metadata::Track> track;
|
||||
|
||||
try
|
||||
{
|
||||
_parsedTrack = _parser.parseMetaData(_file);
|
||||
|
||||
// We fill missing artist mbids with mbids found on other artist roles
|
||||
fillMissingMbids(*_parsedTrack);
|
||||
|
||||
std::size_t index{};
|
||||
_parser.parseImages(_file, [&](const metadata::Image& image) {
|
||||
try
|
||||
{
|
||||
image::ImageProperties properties{ image::probeImage(image.data) };
|
||||
|
||||
ImageInfo info;
|
||||
info.index = index;
|
||||
info.type = image.type;
|
||||
{
|
||||
LMS_SCOPED_TRACE_DETAILED("Scanner", "ImageHash");
|
||||
info.hash = core::xxHash3_64(image.data);
|
||||
}
|
||||
info.size = image.data.size();
|
||||
info.mimeType = image.mimeType;
|
||||
info.description = image.description;
|
||||
info.properties = properties;
|
||||
|
||||
_parsedImages.push_back(std::move(info));
|
||||
}
|
||||
catch (const image::Exception& e)
|
||||
{
|
||||
LMS_LOG(DBUPDATER, ERROR, "Failed to parse image in track file " << _file);
|
||||
}
|
||||
|
||||
index++;
|
||||
});
|
||||
}
|
||||
catch (const metadata::Exception& e)
|
||||
{
|
||||
LMS_LOG(DBUPDATER, ERROR, "Failed to parse audio file " << _file);
|
||||
}
|
||||
}
|
||||
|
||||
void AudioFileScanOperation::processResult(ScanContext& context)
|
||||
{
|
||||
LMS_SCOPED_TRACE_DETAILED("Scanner", "ProcessAudioScanData");
|
||||
|
||||
ScanStats& stats{ context.stats };
|
||||
|
||||
const std::optional<FileInfo> fileInfo{ utils::retrieveFileInfo(_file, _mediaLibrary.rootDirectory) };
|
||||
if (!fileInfo)
|
||||
{
|
||||
stats.skips++;
|
||||
return;
|
||||
}
|
||||
|
||||
db::Session& dbSession{ _db.getTLSSession() };
|
||||
db::Track::pointer track{ db::Track::findByPath(dbSession, _file) };
|
||||
|
||||
if (!_parsedTrack)
|
||||
{
|
||||
if (track)
|
||||
{
|
||||
track.remove();
|
||||
stats.deletions++;
|
||||
}
|
||||
context.stats.errors.emplace_back(_file, ScanErrorType::CannotReadAudioFile);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_parsedTrack->mbid && (!track || _settings.skipDuplicateTrackMBID))
|
||||
{
|
||||
std::vector<db::Track::pointer> duplicateTracks{ db::Track::findByMBID(dbSession, *_parsedTrack->mbid) };
|
||||
|
||||
// find for an existing track MBID as the file may have just been moved
|
||||
if (!track && duplicateTracks.size() == 1)
|
||||
{
|
||||
db::Track::pointer otherTrack{ duplicateTracks.front() };
|
||||
std::error_code ec;
|
||||
if (!std::filesystem::exists(otherTrack->getAbsoluteFilePath(), ec))
|
||||
{
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Considering track " << _file << " moved from " << otherTrack->getAbsoluteFilePath());
|
||||
track = otherTrack;
|
||||
track.modify()->setAbsoluteFilePath(_file);
|
||||
}
|
||||
}
|
||||
|
||||
// Skip duplicate track MBID
|
||||
if (_settings.skipDuplicateTrackMBID)
|
||||
{
|
||||
for (db::Track::pointer& otherTrack : duplicateTracks)
|
||||
{
|
||||
// Skip ourselves
|
||||
if (track && track->getId() == otherTrack->getId())
|
||||
continue;
|
||||
|
||||
// Skip if duplicate files no longer in media root: as it will be removed later, we will end up with no file
|
||||
if (std::none_of(std::cbegin(_settings.mediaLibraries), std::cend(_settings.mediaLibraries),
|
||||
[&](const MediaLibraryInfo& libraryInfo) {
|
||||
return core::pathUtils::isPathInRootPath(_file, libraryInfo.rootDirectory, &excludeDirFileName);
|
||||
}))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Skipped " << _file << " (similar MBID in " << otherTrack->getAbsoluteFilePath() << ")");
|
||||
// As this MBID already exists, just remove what we just scanned
|
||||
if (track)
|
||||
{
|
||||
track.remove();
|
||||
stats.deletions++;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!track)
|
||||
{
|
||||
// maybe the file just moved?
|
||||
track = findMovedTrackBySizeAndMetaData(dbSession, *_parsedTrack, *fileInfo);
|
||||
if (track)
|
||||
{
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Considering track " << _file << " moved from " << track->getAbsoluteFilePath());
|
||||
track.modify()->setAbsoluteFilePath(_file);
|
||||
}
|
||||
}
|
||||
|
||||
// We estimate this is an audio file if the duration is not null
|
||||
if (_parsedTrack->audioProperties.duration == std::chrono::milliseconds::zero())
|
||||
{
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Skipped " << _file << " (duration is 0)");
|
||||
|
||||
// If Track exists here, delete it!
|
||||
if (track)
|
||||
{
|
||||
track.remove();
|
||||
stats.deletions++;
|
||||
}
|
||||
stats.errors.emplace_back(_file, ScanErrorType::BadDuration);
|
||||
return;
|
||||
}
|
||||
|
||||
// ***** Title
|
||||
std::string title;
|
||||
if (!_parsedTrack->title.empty())
|
||||
title = _parsedTrack->title;
|
||||
else
|
||||
{
|
||||
// TODO parse file name guess track etc.
|
||||
// For now juste use file name as title
|
||||
title = _file.filename().string();
|
||||
}
|
||||
|
||||
// If file already exists, update its data
|
||||
// Otherwise, create it
|
||||
bool added{};
|
||||
if (!track)
|
||||
{
|
||||
track = dbSession.create<db::Track>();
|
||||
added = true;
|
||||
|
||||
track.modify()->setAbsoluteFilePath(_file);
|
||||
track.modify()->setAddedTime(_mediaLibrary.firstScan ? fileInfo->lastWriteTime : Wt::WDateTime::currentDateTime()); // may be erased by encodingTime
|
||||
}
|
||||
|
||||
// Track related data
|
||||
assert(track);
|
||||
track.modify()->setScanVersion(_settings.audioScanVersion);
|
||||
|
||||
// Audio properties
|
||||
track.modify()->setBitrate(_parsedTrack->audioProperties.bitrate);
|
||||
track.modify()->setBitsPerSample(_parsedTrack->audioProperties.bitsPerSample);
|
||||
track.modify()->setChannelCount(_parsedTrack->audioProperties.channelCount);
|
||||
track.modify()->setDuration(_parsedTrack->audioProperties.duration);
|
||||
track.modify()->setSampleRate(_parsedTrack->audioProperties.sampleRate);
|
||||
|
||||
track.modify()->setRelativeFilePath(fileInfo->relativePath);
|
||||
track.modify()->setFileSize(fileInfo->fileSize);
|
||||
track.modify()->setLastWriteTime(fileInfo->lastWriteTime);
|
||||
|
||||
if (_parsedTrack->encodingTime.isValid())
|
||||
{
|
||||
const core::PartialDateTime& encodingTime{ _parsedTrack->encodingTime };
|
||||
Wt::WDate date;
|
||||
Wt::WTime time;
|
||||
if (encodingTime.getPrecision() >= core::PartialDateTime::Precision::Day)
|
||||
date = Wt::WDate{ *encodingTime.getYear(), *encodingTime.getMonth(), *encodingTime.getDay() };
|
||||
if (encodingTime.getPrecision() >= core::PartialDateTime::Precision::Sec)
|
||||
time = Wt::WTime{ *encodingTime.getHour(), *encodingTime.getMin(), *encodingTime.getSec() };
|
||||
|
||||
if (date.isValid())
|
||||
track.modify()->setAddedTime(time.isValid() ? Wt::WDateTime{ date, time } : Wt::WDateTime{ date });
|
||||
}
|
||||
|
||||
db::MediaLibrary::pointer mediaLibrary{ db::MediaLibrary::find(dbSession, _mediaLibrary.id) }; // may be null if settings are updated in // => next scan will correct this
|
||||
track.modify()->setMediaLibrary(mediaLibrary);
|
||||
db::Directory::pointer directory{ utils::getOrCreateDirectory(dbSession, _file.parent_path(), mediaLibrary) };
|
||||
track.modify()->setDirectory(directory);
|
||||
|
||||
track.modify()->clearArtistLinks();
|
||||
|
||||
const helpers::AllowFallbackOnMBIDEntry allowFallback{ _settings.allowArtistMBIDFallback };
|
||||
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Artist, _parsedTrack->artists, allowFallback);
|
||||
if (_parsedTrack->medium && _parsedTrack->medium->release)
|
||||
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::ReleaseArtist, _parsedTrack->medium->release->artists, allowFallback);
|
||||
|
||||
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Conductor, _parsedTrack->conductorArtists, allowFallback);
|
||||
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Composer, _parsedTrack->composerArtists, allowFallback);
|
||||
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Lyricist, _parsedTrack->lyricistArtists, allowFallback);
|
||||
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Mixer, _parsedTrack->mixerArtists, allowFallback);
|
||||
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Remixer, _parsedTrack->remixerArtists, allowFallback);
|
||||
|
||||
for (const auto& [role, performers] : _parsedTrack->performerArtists)
|
||||
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Performer, role, performers, allowFallback);
|
||||
|
||||
if (_parsedTrack->medium && _parsedTrack->medium->release)
|
||||
track.modify()->setRelease(getOrCreateRelease(dbSession, *_parsedTrack->medium->release, directory));
|
||||
else
|
||||
track.modify()->setRelease({});
|
||||
track.modify()->setTotalTrack(_parsedTrack->medium ? _parsedTrack->medium->trackCount : std::nullopt);
|
||||
track.modify()->setReleaseReplayGain(_parsedTrack->medium ? _parsedTrack->medium->replayGain : std::nullopt);
|
||||
track.modify()->setDiscSubtitle(_parsedTrack->medium ? _parsedTrack->medium->name : "");
|
||||
track.modify()->setClusters(getOrCreateClusters(dbSession, *_parsedTrack));
|
||||
track.modify()->setName(title);
|
||||
track.modify()->setTrackNumber(_parsedTrack->position);
|
||||
track.modify()->setDiscNumber(_parsedTrack->medium ? _parsedTrack->medium->position : std::nullopt);
|
||||
track.modify()->setDate(_parsedTrack->date);
|
||||
track.modify()->setOriginalDate(_parsedTrack->originalDate);
|
||||
if (!track->getOriginalDate().isValid() && _parsedTrack->originalYear)
|
||||
track.modify()->setOriginalDate(core::PartialDateTime{ *_parsedTrack->originalYear });
|
||||
|
||||
// If a file has an OriginalDate but no date, set it to ease filtering
|
||||
if (!_parsedTrack->date.isValid() && _parsedTrack->originalDate.isValid())
|
||||
track.modify()->setDate(_parsedTrack->originalDate);
|
||||
|
||||
track.modify()->setRecordingMBID(_parsedTrack->recordingMBID);
|
||||
track.modify()->setTrackMBID(_parsedTrack->mbid);
|
||||
if (auto trackFeatures{ db::TrackFeatures::find(dbSession, track->getId()) })
|
||||
trackFeatures.remove(); // TODO: only if MBID changed?
|
||||
track.modify()->setCopyright(_parsedTrack->copyright);
|
||||
track.modify()->setCopyrightURL(_parsedTrack->copyrightURL);
|
||||
track.modify()->setAdvisory(getAdvisory(_parsedTrack->advisory));
|
||||
track.modify()->setComment(!_parsedTrack->comments.empty() ? _parsedTrack->comments.front() : ""); // only take the first one for now
|
||||
track.modify()->setTrackReplayGain(_parsedTrack->replayGain);
|
||||
track.modify()->setArtistDisplayName(_parsedTrack->artistDisplayName);
|
||||
|
||||
track.modify()->clearEmbeddedLyrics();
|
||||
for (const metadata::Lyrics& lyricsInfo : _parsedTrack->lyrics)
|
||||
track.modify()->addLyrics(createLyrics(dbSession, lyricsInfo));
|
||||
|
||||
updateEmbeddedImages(dbSession, track, _parsedImages);
|
||||
|
||||
if (added)
|
||||
{
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Added audio file " << _file);
|
||||
stats.additions++;
|
||||
}
|
||||
else
|
||||
{
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Updated audio file " << _file);
|
||||
stats.updates++;
|
||||
}
|
||||
}
|
||||
} // namespace lms::scanner
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* 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 "IFileScanOperation.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include "image/Types.hpp"
|
||||
#include "metadata/IAudioFileParser.hpp"
|
||||
|
||||
#include "FileToScan.hpp"
|
||||
#include "IFileScanner.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
class Db;
|
||||
} // namespace lms::db
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
struct ImageInfo
|
||||
{
|
||||
std::size_t index;
|
||||
metadata::Image::Type type{ metadata::Image::Type::Unknown };
|
||||
std::uint64_t hash{};
|
||||
std::size_t size{};
|
||||
image::ImageProperties properties;
|
||||
std::string mimeType;
|
||||
std::string description;
|
||||
};
|
||||
|
||||
class AudioFileScanOperation : public IFileScanOperation
|
||||
{
|
||||
public:
|
||||
AudioFileScanOperation(const FileToScan& fileToScan, db::Db& db, metadata::IAudioFileParser& parser, const ScannerSettings& settings)
|
||||
: _file{ fileToScan.file }
|
||||
, _mediaLibrary{ fileToScan.mediaLibrary }
|
||||
, _db{ db }
|
||||
, _parser{ parser }
|
||||
, _settings{ settings }
|
||||
{
|
||||
}
|
||||
~AudioFileScanOperation() override = default;
|
||||
AudioFileScanOperation(const AudioFileScanOperation&) = delete;
|
||||
AudioFileScanOperation& operator=(const AudioFileScanOperation&) = delete;
|
||||
|
||||
private:
|
||||
const std::filesystem::path& getFile() const override { return _file; };
|
||||
core::LiteralString getName() const override { return "ScanAudioFile"; }
|
||||
void scan() override;
|
||||
void processResult(ScanContext& context) override;
|
||||
|
||||
const std::filesystem::path _file;
|
||||
const MediaLibraryInfo _mediaLibrary;
|
||||
db::Db& _db;
|
||||
metadata::IAudioFileParser& _parser;
|
||||
const ScannerSettings& _settings;
|
||||
std::unique_ptr<metadata::Track> _parsedTrack;
|
||||
std::vector<ImageInfo> _parsedImages;
|
||||
};
|
||||
|
||||
} // namespace lms::scanner
|
||||
@@ -20,804 +20,21 @@
|
||||
#include "AudioFileScanner.hpp"
|
||||
|
||||
#include "core/IConfig.hpp"
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/ITraceLogger.hpp"
|
||||
#include "core/PartialDateTime.hpp"
|
||||
#include "core/Path.hpp"
|
||||
#include "core/Service.hpp"
|
||||
#include "core/XxHash3.hpp"
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Cluster.hpp"
|
||||
#include "database/Db.hpp"
|
||||
#include "database/Directory.hpp"
|
||||
#include "database/MediaLibrary.hpp"
|
||||
#include "database/Release.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "database/TrackArtistLink.hpp"
|
||||
#include "database/TrackEmbeddedImage.hpp"
|
||||
#include "database/TrackEmbeddedImageLink.hpp"
|
||||
#include "database/TrackFeatures.hpp"
|
||||
#include "database/TrackLyrics.hpp"
|
||||
#include "database/Types.hpp"
|
||||
#include "image/Exception.hpp"
|
||||
#include "image/Image.hpp"
|
||||
#include "metadata/Exception.hpp"
|
||||
#include "metadata/IAudioFileParser.hpp"
|
||||
|
||||
#include "IFileScanOperation.hpp"
|
||||
#include "AudioFileScanOperation.hpp"
|
||||
#include "ScanContext.hpp"
|
||||
#include "ScannerSettings.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "metadata/Types.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
namespace
|
||||
{
|
||||
db::Artist::pointer createArtist(db::Session& session, const metadata::Artist& artistInfo)
|
||||
{
|
||||
db::Artist::pointer artist{ session.create<db::Artist>(artistInfo.name) };
|
||||
|
||||
if (artistInfo.mbid)
|
||||
artist.modify()->setMBID(artistInfo.mbid);
|
||||
|
||||
artist.modify()->setSortName(artistInfo.sortName ? *artistInfo.sortName : artistInfo.name);
|
||||
|
||||
return artist;
|
||||
}
|
||||
|
||||
std::string optionalMBIDAsString(const std::optional<core::UUID>& uuid)
|
||||
{
|
||||
return uuid ? std::string{ uuid->getAsString() } : "<no MBID>";
|
||||
}
|
||||
|
||||
void updateArtistIfNeeded(db::Artist::pointer artist, const metadata::Artist& artistInfo)
|
||||
{
|
||||
// Name may have been updated
|
||||
if (artist->getName() != artistInfo.name)
|
||||
{
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Artist [" << optionalMBIDAsString(artist->getMBID()) << "], updated name from '" << artist->getName() << "' to '" << artistInfo.name << "'");
|
||||
artist.modify()->setName(artistInfo.name);
|
||||
}
|
||||
|
||||
// Sortname may have been updated
|
||||
// As the sort name is quite often not filled in, we update it only if already set (for now?)
|
||||
if (artistInfo.sortName && *artistInfo.sortName != artist->getSortName())
|
||||
{
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Artist [" << optionalMBIDAsString(artist->getMBID()) << "], updated sort name from '" << artist->getSortName() << "' to '" << *artistInfo.sortName << "'");
|
||||
artist.modify()->setSortName(*artistInfo.sortName);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<db::Artist::pointer> getOrCreateArtists(db::Session& session, const std::vector<metadata::Artist>& artistsInfo, bool allowFallbackOnMBIDEntries)
|
||||
{
|
||||
std::vector<db::Artist::pointer> artists;
|
||||
|
||||
for (const metadata::Artist& artistInfo : artistsInfo)
|
||||
{
|
||||
db::Artist::pointer artist;
|
||||
|
||||
// First try to get by MBID
|
||||
if (artistInfo.mbid)
|
||||
{
|
||||
artist = db::Artist::find(session, *artistInfo.mbid);
|
||||
if (!artist)
|
||||
artist = createArtist(session, artistInfo);
|
||||
else
|
||||
updateArtistIfNeeded(artist, artistInfo);
|
||||
|
||||
artists.emplace_back(std::move(artist));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fall back on artist name (collisions may occur)
|
||||
if (!artistInfo.name.empty())
|
||||
{
|
||||
for (const db::Artist::pointer& sameNamedArtist : db::Artist::find(session, artistInfo.name))
|
||||
{
|
||||
// Do not fallback on artist that is correctly tagged
|
||||
if (!allowFallbackOnMBIDEntries && sameNamedArtist->getMBID())
|
||||
continue;
|
||||
|
||||
artist = sameNamedArtist;
|
||||
break;
|
||||
}
|
||||
|
||||
// No Artist found with the same name and without MBID -> creating
|
||||
if (!artist)
|
||||
artist = createArtist(session, artistInfo);
|
||||
else
|
||||
updateArtistIfNeeded(artist, artistInfo);
|
||||
|
||||
artists.emplace_back(std::move(artist));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return artists;
|
||||
}
|
||||
|
||||
db::ReleaseType::pointer getOrCreateReleaseType(db::Session& session, std::string_view name)
|
||||
{
|
||||
db::ReleaseType::pointer releaseType{ db::ReleaseType::find(session, name) };
|
||||
if (!releaseType)
|
||||
releaseType = session.create<db::ReleaseType>(name);
|
||||
|
||||
return releaseType;
|
||||
}
|
||||
|
||||
db::Country::pointer getOrCreateCountry(db::Session& session, std::string_view name)
|
||||
{
|
||||
db::Country::pointer country{ db::Country::find(session, name) };
|
||||
if (!country)
|
||||
country = session.create<db::Country>(name);
|
||||
|
||||
return country;
|
||||
}
|
||||
|
||||
db::Label::pointer getOrCreateLabel(db::Session& session, std::string_view name)
|
||||
{
|
||||
db::Label::pointer label{ db::Label::find(session, name) };
|
||||
if (!label)
|
||||
label = session.create<db::Label>(name);
|
||||
|
||||
return label;
|
||||
}
|
||||
|
||||
void updateReleaseIfNeeded(db::Session& session, db::Release::pointer release, const metadata::Release& releaseInfo)
|
||||
{
|
||||
if (release->getName() != releaseInfo.name)
|
||||
release.modify()->setName(releaseInfo.name);
|
||||
if (release->getSortName() != releaseInfo.sortName)
|
||||
release.modify()->setSortName(releaseInfo.sortName);
|
||||
if (release->getGroupMBID() != releaseInfo.groupMBID)
|
||||
release.modify()->setGroupMBID(releaseInfo.groupMBID);
|
||||
if (release->getTotalDisc() != releaseInfo.mediumCount)
|
||||
release.modify()->setTotalDisc(releaseInfo.mediumCount);
|
||||
if (release->getArtistDisplayName() != releaseInfo.artistDisplayName)
|
||||
release.modify()->setArtistDisplayName(releaseInfo.artistDisplayName);
|
||||
if (release->isCompilation() != releaseInfo.isCompilation)
|
||||
release.modify()->setCompilation(releaseInfo.isCompilation);
|
||||
if (release->getBarcode() != releaseInfo.barcode)
|
||||
release.modify()->setBarcode(releaseInfo.barcode);
|
||||
if (release->getComment() != releaseInfo.comment)
|
||||
release.modify()->setComment(releaseInfo.comment);
|
||||
if (release->getReleaseTypeNames() != releaseInfo.releaseTypes)
|
||||
{
|
||||
release.modify()->clearReleaseTypes();
|
||||
for (std::string_view releaseType : releaseInfo.releaseTypes)
|
||||
release.modify()->addReleaseType(getOrCreateReleaseType(session, releaseType));
|
||||
}
|
||||
if (release->getCountryNames() != releaseInfo.countries)
|
||||
{
|
||||
release.modify()->clearCountries();
|
||||
for (std::string_view country : releaseInfo.countries)
|
||||
release.modify()->addCountry(getOrCreateCountry(session, country));
|
||||
}
|
||||
if (release->getLabelNames() != releaseInfo.labels)
|
||||
{
|
||||
release.modify()->clearLabels();
|
||||
for (std::string_view label : releaseInfo.labels)
|
||||
release.modify()->addLabel(getOrCreateLabel(session, label));
|
||||
}
|
||||
}
|
||||
|
||||
// Compare release level info
|
||||
bool isReleaseMatching(const db::Release::pointer& candidateRelease, const metadata::Release& releaseInfo)
|
||||
{
|
||||
// TODO: add more criterias?
|
||||
return candidateRelease->getName() == releaseInfo.name
|
||||
&& candidateRelease->getSortName() == releaseInfo.sortName
|
||||
&& candidateRelease->getTotalDisc() == releaseInfo.mediumCount
|
||||
&& candidateRelease->isCompilation() == releaseInfo.isCompilation
|
||||
&& candidateRelease->getLabelNames() == releaseInfo.labels
|
||||
&& candidateRelease->getBarcode() == releaseInfo.barcode;
|
||||
}
|
||||
|
||||
db::Release::pointer getOrCreateRelease(db::Session& session, const metadata::Release& releaseInfo, const db::Directory::pointer& currentDirectory)
|
||||
{
|
||||
db::Release::pointer release;
|
||||
|
||||
// First try to get by MBID: fastest, safest
|
||||
if (releaseInfo.mbid)
|
||||
{
|
||||
release = db::Release::find(session, *releaseInfo.mbid);
|
||||
if (!release)
|
||||
release = session.create<db::Release>(releaseInfo.name, releaseInfo.mbid);
|
||||
}
|
||||
else if (releaseInfo.name.empty())
|
||||
{
|
||||
// No release name (only mbid) -> nothing to do
|
||||
return release;
|
||||
}
|
||||
|
||||
// Fall back on release name (collisions may occur)
|
||||
// First try using all sibling directories (case for Album/DiscX), only if the disc number is set
|
||||
const db::DirectoryId parentDirectoryId{ currentDirectory->getParentDirectoryId() };
|
||||
if (!release && releaseInfo.mediumCount && *releaseInfo.mediumCount > 1 && parentDirectoryId.isValid())
|
||||
{
|
||||
db::Release::FindParameters params;
|
||||
params.setParentDirectory(parentDirectoryId);
|
||||
params.setName(releaseInfo.name);
|
||||
db::Release::find(session, params, [&](const db::Release::pointer& candidateRelease) {
|
||||
// Already found a candidate
|
||||
if (release)
|
||||
return;
|
||||
|
||||
// Do not fallback on properly tagged releases
|
||||
if (candidateRelease->getMBID().has_value())
|
||||
return;
|
||||
|
||||
if (!isReleaseMatching(candidateRelease, releaseInfo))
|
||||
return;
|
||||
|
||||
release = candidateRelease;
|
||||
});
|
||||
}
|
||||
|
||||
// Lastly try in the current directory: we do this at last to have
|
||||
// opportunities to merge releases in case of migration / rescan
|
||||
if (!release)
|
||||
{
|
||||
db::Release::FindParameters params;
|
||||
params.setDirectory(currentDirectory->getId());
|
||||
params.setName(releaseInfo.name);
|
||||
db::Release::find(session, params, [&](const db::Release::pointer& candidateRelease) {
|
||||
// Already found a candidate
|
||||
if (release)
|
||||
return;
|
||||
|
||||
// Do not fallback on properly tagged releases
|
||||
if (candidateRelease->getMBID().has_value())
|
||||
return;
|
||||
|
||||
if (!isReleaseMatching(candidateRelease, releaseInfo))
|
||||
return;
|
||||
|
||||
release = candidateRelease;
|
||||
});
|
||||
}
|
||||
|
||||
if (!release)
|
||||
release = session.create<db::Release>(releaseInfo.name);
|
||||
|
||||
updateReleaseIfNeeded(session, release, releaseInfo);
|
||||
return release;
|
||||
}
|
||||
|
||||
std::vector<db::Cluster::pointer> getOrCreateClusters(db::Session& session, const metadata::Track& track)
|
||||
{
|
||||
std::vector<db::Cluster::pointer> clusters;
|
||||
|
||||
auto getOrCreateClusters{ [&](std::string_view tag, std::span<const std::string> values) {
|
||||
auto clusterType = db::ClusterType::find(session, tag);
|
||||
if (!clusterType)
|
||||
clusterType = session.create<db::ClusterType>(tag);
|
||||
|
||||
for (const auto& value : values)
|
||||
{
|
||||
auto cluster{ clusterType->getCluster(value) };
|
||||
if (!cluster)
|
||||
cluster = session.create<db::Cluster>(clusterType, value);
|
||||
|
||||
clusters.push_back(cluster);
|
||||
}
|
||||
} };
|
||||
|
||||
// TODO: migrate these fields in dedicated tables in DB
|
||||
getOrCreateClusters("GENRE", track.genres);
|
||||
getOrCreateClusters("MOOD", track.moods);
|
||||
getOrCreateClusters("LANGUAGE", track.languages);
|
||||
getOrCreateClusters("GROUPING", track.groupings);
|
||||
|
||||
for (const auto& [tag, values] : track.userExtraTags)
|
||||
getOrCreateClusters(tag, values);
|
||||
|
||||
return clusters;
|
||||
}
|
||||
|
||||
db::TrackLyrics::pointer createLyrics(db::Session& session, const metadata::Lyrics& lyricsInfo)
|
||||
{
|
||||
db::TrackLyrics::pointer lyrics{ session.create<db::TrackLyrics>() };
|
||||
|
||||
lyrics.modify()->setLanguage(!lyricsInfo.language.empty() ? lyricsInfo.language : "xxx");
|
||||
lyrics.modify()->setOffset(lyricsInfo.offset);
|
||||
lyrics.modify()->setDisplayArtist(lyricsInfo.displayArtist);
|
||||
lyrics.modify()->setDisplayTitle(lyricsInfo.displayTitle);
|
||||
if (!lyricsInfo.synchronizedLines.empty())
|
||||
lyrics.modify()->setSynchronizedLines(lyricsInfo.synchronizedLines);
|
||||
else
|
||||
lyrics.modify()->setUnsynchronizedLines(lyricsInfo.unsynchronizedLines);
|
||||
|
||||
return lyrics;
|
||||
}
|
||||
|
||||
struct ImageInfo
|
||||
{
|
||||
std::size_t index;
|
||||
metadata::Image::Type type{ metadata::Image::Type::Unknown };
|
||||
std::uint64_t hash{};
|
||||
std::size_t size{};
|
||||
image::ImageProperties properties;
|
||||
std::string mimeType;
|
||||
std::string description;
|
||||
};
|
||||
|
||||
db::ImageType convertImageType(metadata::Image::Type type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case metadata::Image::Type::Unknown:
|
||||
return db::ImageType::Unknown;
|
||||
case metadata::Image::Type::Other:
|
||||
return db::ImageType::Other;
|
||||
case metadata::Image::Type::FileIcon:
|
||||
return db::ImageType::FileIcon;
|
||||
case metadata::Image::Type::OtherFileIcon:
|
||||
return db::ImageType::OtherFileIcon;
|
||||
case metadata::Image::Type::FrontCover:
|
||||
return db::ImageType::FrontCover;
|
||||
case metadata::Image::Type::BackCover:
|
||||
return db::ImageType::BackCover;
|
||||
case metadata::Image::Type::LeafletPage:
|
||||
return db::ImageType::LeafletPage;
|
||||
case metadata::Image::Type::Media:
|
||||
return db::ImageType::Media;
|
||||
case metadata::Image::Type::LeadArtist:
|
||||
return db::ImageType::LeadArtist;
|
||||
case metadata::Image::Type::Artist:
|
||||
return db::ImageType::Artist;
|
||||
case metadata::Image::Type::Conductor:
|
||||
return db::ImageType::Conductor;
|
||||
case metadata::Image::Type::Band:
|
||||
return db::ImageType::Band;
|
||||
case metadata::Image::Type::Composer:
|
||||
return db::ImageType::Composer;
|
||||
case metadata::Image::Type::Lyricist:
|
||||
return db::ImageType::Lyricist;
|
||||
case metadata::Image::Type::RecordingLocation:
|
||||
return db::ImageType::RecordingLocation;
|
||||
case metadata::Image::Type::DuringRecording:
|
||||
return db::ImageType::DuringRecording;
|
||||
case metadata::Image::Type::DuringPerformance:
|
||||
return db::ImageType::DuringPerformance;
|
||||
case metadata::Image::Type::MovieScreenCapture:
|
||||
return db::ImageType::MovieScreenCapture;
|
||||
case metadata::Image::Type::ColouredFish:
|
||||
return db::ImageType::ColouredFish;
|
||||
case metadata::Image::Type::Illustration:
|
||||
return db::ImageType::Illustration;
|
||||
case metadata::Image::Type::BandLogo:
|
||||
return db::ImageType::BandLogo;
|
||||
case metadata::Image::Type::PublisherLogo:
|
||||
return db::ImageType::PublisherLogo;
|
||||
}
|
||||
|
||||
return db::ImageType::Unknown;
|
||||
}
|
||||
|
||||
db::TrackEmbeddedImage::pointer getOrCreateTrackEmbeddedImage(db::Session& session, const ImageInfo& imageInfo)
|
||||
{
|
||||
db::TrackEmbeddedImage::pointer image{ db::TrackEmbeddedImage::find(session, imageInfo.size, db::ImageHashType{ imageInfo.hash }) };
|
||||
if (!image)
|
||||
{
|
||||
image = session.create<db::TrackEmbeddedImage>();
|
||||
image.modify()->setSize(imageInfo.size);
|
||||
image.modify()->setHash(db::ImageHashType{ imageInfo.hash });
|
||||
image.modify()->setWidth(imageInfo.properties.width);
|
||||
image.modify()->setHeight(imageInfo.properties.height);
|
||||
image.modify()->setMimeType(imageInfo.mimeType);
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
db::TrackEmbeddedImageLink::pointer createTrackEmbeddedImageLink(db::Session& session, const db::Track::pointer& track, const ImageInfo& imageInfo)
|
||||
{
|
||||
const db::TrackEmbeddedImage::pointer image{ getOrCreateTrackEmbeddedImage(session, imageInfo) };
|
||||
db::TrackEmbeddedImageLink::pointer imageLink{ session.create<db::TrackEmbeddedImageLink>(track, image) };
|
||||
imageLink.modify()->setIndex(imageInfo.index);
|
||||
imageLink.modify()->setType(convertImageType(imageInfo.type));
|
||||
imageLink.modify()->setDescription(imageInfo.description);
|
||||
|
||||
return imageLink;
|
||||
}
|
||||
|
||||
void updateEmbeddedImages(db::Session& session, db::Track::pointer& track, std::span<const ImageInfo> images)
|
||||
{
|
||||
db::TrackEmbeddedImageLink::pointer preferredImageLink;
|
||||
|
||||
track.modify()->clearEmbeddedImageLinks();
|
||||
for (const ImageInfo& imageInfo : images)
|
||||
{
|
||||
db::TrackEmbeddedImageLink::pointer link{ createTrackEmbeddedImageLink(session, track, imageInfo) };
|
||||
track.modify()->addEmbeddedImageLink(link);
|
||||
|
||||
if (!preferredImageLink
|
||||
|| (preferredImageLink->getType() != db::ImageType::FrontCover && link->getType() == db::ImageType::FrontCover)
|
||||
|| (preferredImageLink->getImage()->getSize() < link->getImage()->getSize()))
|
||||
{
|
||||
preferredImageLink = link;
|
||||
}
|
||||
}
|
||||
|
||||
if (preferredImageLink)
|
||||
preferredImageLink.modify()->setIsPreferred(true);
|
||||
}
|
||||
|
||||
db::Advisory getAdvisory(std::optional<metadata::Track::Advisory> advisory)
|
||||
{
|
||||
if (!advisory)
|
||||
return db::Advisory::UnSet;
|
||||
|
||||
switch (advisory.value())
|
||||
{
|
||||
case metadata::Track::Advisory::Clean:
|
||||
return db::Advisory::Clean;
|
||||
case metadata::Track::Advisory::Explicit:
|
||||
return db::Advisory::Explicit;
|
||||
case metadata::Track::Advisory::Unknown:
|
||||
return db::Advisory::Unknown;
|
||||
}
|
||||
|
||||
return db::Advisory::UnSet;
|
||||
}
|
||||
|
||||
db::Track::pointer findMovedTrackBySizeAndMetaData(db::Session& session, const metadata::Track& parsedTrack, const FileInfo& fileInfo)
|
||||
{
|
||||
db::Track::FindParameters params;
|
||||
// Add as many fields as possible to limit errors
|
||||
params.setName(parsedTrack.title);
|
||||
if (parsedTrack.medium)
|
||||
{
|
||||
if (parsedTrack.medium->position)
|
||||
params.setDiscNumber(*parsedTrack.medium->position);
|
||||
if (parsedTrack.medium->release)
|
||||
params.setReleaseName(parsedTrack.medium->release->name);
|
||||
}
|
||||
if (parsedTrack.position)
|
||||
params.setTrackNumber(*parsedTrack.position);
|
||||
params.setFileSize(fileInfo.fileSize);
|
||||
|
||||
bool error{};
|
||||
db::Track::pointer res;
|
||||
db::Track::find(session, params, [&](const db::Track::pointer& track) {
|
||||
// Check that the track is truly no longer where it was during the last scan
|
||||
std::error_code ec;
|
||||
if (std::filesystem::exists(track->getAbsoluteFilePath(), ec))
|
||||
return;
|
||||
|
||||
if (res)
|
||||
{
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Found too many candidates for file move. New file = " << fileInfo.relativePath << ", candidate = " << track->getAbsoluteFilePath() << ", previous candidate = " << res->getAbsoluteFilePath());
|
||||
error = true;
|
||||
}
|
||||
res = track;
|
||||
});
|
||||
|
||||
if (error)
|
||||
res = db::Track::pointer{};
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
class AudioFileScanOperation : public IFileScanOperation
|
||||
{
|
||||
public:
|
||||
AudioFileScanOperation(const FileToScan& fileToScan, db::Db& db, metadata::IAudioFileParser& parser, const ScannerSettings& settings)
|
||||
: _file{ fileToScan.file }
|
||||
, _mediaLibrary{ fileToScan.mediaLibrary }
|
||||
, _db{ db }
|
||||
, _parser{ parser }
|
||||
, _settings{ settings }
|
||||
{
|
||||
}
|
||||
~AudioFileScanOperation() override = default;
|
||||
AudioFileScanOperation(const AudioFileScanOperation&) = delete;
|
||||
AudioFileScanOperation& operator=(const AudioFileScanOperation&) = delete;
|
||||
|
||||
private:
|
||||
const std::filesystem::path& getFile() const override { return _file; };
|
||||
core::LiteralString getName() const override { return "ScanAudioFile"; }
|
||||
void scan() override;
|
||||
void processResult(ScanContext& context) override;
|
||||
|
||||
const std::filesystem::path _file;
|
||||
const MediaLibraryInfo _mediaLibrary;
|
||||
db::Db& _db;
|
||||
metadata::IAudioFileParser& _parser;
|
||||
const ScannerSettings& _settings;
|
||||
std::unique_ptr<metadata::Track> _parsedTrack;
|
||||
std::vector<ImageInfo> _parsedImages;
|
||||
};
|
||||
|
||||
void AudioFileScanOperation::scan()
|
||||
{
|
||||
LMS_SCOPED_TRACE_OVERVIEW("Scanner", "ScanAudioFile");
|
||||
std::unique_ptr<metadata::Track> track;
|
||||
|
||||
try
|
||||
{
|
||||
_parsedTrack = _parser.parseMetaData(_file);
|
||||
|
||||
std::size_t index{};
|
||||
_parser.parseImages(_file, [&](const metadata::Image& image) {
|
||||
try
|
||||
{
|
||||
image::ImageProperties properties{ image::probeImage(image.data) };
|
||||
|
||||
ImageInfo info;
|
||||
info.index = index;
|
||||
info.type = image.type;
|
||||
{
|
||||
LMS_SCOPED_TRACE_DETAILED("Scanner", "ImageHash");
|
||||
info.hash = core::xxHash3_64(image.data);
|
||||
}
|
||||
info.size = image.data.size();
|
||||
info.mimeType = image.mimeType;
|
||||
info.description = image.description;
|
||||
info.properties = properties;
|
||||
|
||||
_parsedImages.push_back(std::move(info));
|
||||
}
|
||||
catch (const image::Exception& e)
|
||||
{
|
||||
LMS_LOG(DBUPDATER, ERROR, "Failed to parse image in track file " << _file);
|
||||
}
|
||||
|
||||
index++;
|
||||
});
|
||||
}
|
||||
catch (const metadata::Exception& e)
|
||||
{
|
||||
LMS_LOG(DBUPDATER, ERROR, "Failed to parse audio file " << _file);
|
||||
}
|
||||
}
|
||||
|
||||
void AudioFileScanOperation::processResult(ScanContext& context)
|
||||
{
|
||||
LMS_SCOPED_TRACE_DETAILED("Scanner", "ProcessAudioScanData");
|
||||
|
||||
ScanStats& stats{ context.stats };
|
||||
|
||||
const std::optional<FileInfo> fileInfo{ utils::retrieveFileInfo(_file, _mediaLibrary.rootDirectory) };
|
||||
if (!fileInfo)
|
||||
{
|
||||
stats.skips++;
|
||||
return;
|
||||
}
|
||||
|
||||
db::Session& dbSession{ _db.getTLSSession() };
|
||||
db::Track::pointer track{ db::Track::findByPath(dbSession, _file) };
|
||||
|
||||
if (!_parsedTrack)
|
||||
{
|
||||
if (track)
|
||||
{
|
||||
track.remove();
|
||||
stats.deletions++;
|
||||
}
|
||||
context.stats.errors.emplace_back(_file, ScanErrorType::CannotReadAudioFile);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_parsedTrack->mbid && (!track || _settings.skipDuplicateMBID))
|
||||
{
|
||||
std::vector<db::Track::pointer> duplicateTracks{ db::Track::findByMBID(dbSession, *_parsedTrack->mbid) };
|
||||
|
||||
// find for an existing track MBID as the file may have just been moved
|
||||
if (!track && duplicateTracks.size() == 1)
|
||||
{
|
||||
db::Track::pointer otherTrack{ duplicateTracks.front() };
|
||||
std::error_code ec;
|
||||
if (!std::filesystem::exists(otherTrack->getAbsoluteFilePath(), ec))
|
||||
{
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Considering track " << _file << " moved from " << otherTrack->getAbsoluteFilePath());
|
||||
track = otherTrack;
|
||||
track.modify()->setAbsoluteFilePath(_file);
|
||||
}
|
||||
}
|
||||
|
||||
// Skip duplicate track MBID
|
||||
if (_settings.skipDuplicateMBID)
|
||||
{
|
||||
for (db::Track::pointer& otherTrack : duplicateTracks)
|
||||
{
|
||||
// Skip ourselves
|
||||
if (track && track->getId() == otherTrack->getId())
|
||||
continue;
|
||||
|
||||
// Skip if duplicate files no longer in media root: as it will be removed later, we will end up with no file
|
||||
if (std::none_of(std::cbegin(_settings.mediaLibraries), std::cend(_settings.mediaLibraries),
|
||||
[&](const MediaLibraryInfo& libraryInfo) {
|
||||
return core::pathUtils::isPathInRootPath(_file, libraryInfo.rootDirectory, &excludeDirFileName);
|
||||
}))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Skipped " << _file << " (similar MBID in " << otherTrack->getAbsoluteFilePath() << ")");
|
||||
// As this MBID already exists, just remove what we just scanned
|
||||
if (track)
|
||||
{
|
||||
track.remove();
|
||||
stats.deletions++;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!track)
|
||||
{
|
||||
// maybe the file just moved?
|
||||
track = findMovedTrackBySizeAndMetaData(dbSession, *_parsedTrack, *fileInfo);
|
||||
if (track)
|
||||
{
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Considering track " << _file << " moved from " << track->getAbsoluteFilePath());
|
||||
track.modify()->setAbsoluteFilePath(_file);
|
||||
}
|
||||
}
|
||||
|
||||
// We estimate this is an audio file if the duration is not null
|
||||
if (_parsedTrack->audioProperties.duration == std::chrono::milliseconds::zero())
|
||||
{
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Skipped " << _file << " (duration is 0)");
|
||||
|
||||
// If Track exists here, delete it!
|
||||
if (track)
|
||||
{
|
||||
track.remove();
|
||||
stats.deletions++;
|
||||
}
|
||||
stats.errors.emplace_back(_file, ScanErrorType::BadDuration);
|
||||
return;
|
||||
}
|
||||
|
||||
// ***** Title
|
||||
std::string title;
|
||||
if (!_parsedTrack->title.empty())
|
||||
title = _parsedTrack->title;
|
||||
else
|
||||
{
|
||||
// TODO parse file name guess track etc.
|
||||
// For now juste use file name as title
|
||||
title = _file.filename().string();
|
||||
}
|
||||
|
||||
// If file already exists, update its data
|
||||
// Otherwise, create it
|
||||
bool added{};
|
||||
if (!track)
|
||||
{
|
||||
track = dbSession.create<db::Track>();
|
||||
added = true;
|
||||
|
||||
track.modify()->setAbsoluteFilePath(_file);
|
||||
track.modify()->setAddedTime(_mediaLibrary.firstScan ? fileInfo->lastWriteTime : Wt::WDateTime::currentDateTime()); // may be erased by encodingTime
|
||||
}
|
||||
|
||||
// Track related data
|
||||
assert(track);
|
||||
|
||||
// Audio properties
|
||||
track.modify()->setBitrate(_parsedTrack->audioProperties.bitrate);
|
||||
track.modify()->setBitsPerSample(_parsedTrack->audioProperties.bitsPerSample);
|
||||
track.modify()->setChannelCount(_parsedTrack->audioProperties.channelCount);
|
||||
track.modify()->setDuration(_parsedTrack->audioProperties.duration);
|
||||
track.modify()->setSampleRate(_parsedTrack->audioProperties.sampleRate);
|
||||
|
||||
track.modify()->setRelativeFilePath(fileInfo->relativePath);
|
||||
track.modify()->setFileSize(fileInfo->fileSize);
|
||||
track.modify()->setLastWriteTime(fileInfo->lastWriteTime);
|
||||
|
||||
if (_parsedTrack->encodingTime.isValid())
|
||||
{
|
||||
const core::PartialDateTime& encodingTime{ _parsedTrack->encodingTime };
|
||||
Wt::WDate date;
|
||||
Wt::WTime time;
|
||||
if (encodingTime.getPrecision() >= core::PartialDateTime::Precision::Day)
|
||||
date = Wt::WDate{ *encodingTime.getYear(), *encodingTime.getMonth(), *encodingTime.getDay() };
|
||||
if (encodingTime.getPrecision() >= core::PartialDateTime::Precision::Sec)
|
||||
time = Wt::WTime{ *encodingTime.getHour(), *encodingTime.getMin(), *encodingTime.getSec() };
|
||||
|
||||
if (date.isValid())
|
||||
track.modify()->setAddedTime(time.isValid() ? Wt::WDateTime{ date, time } : Wt::WDateTime{ date });
|
||||
}
|
||||
|
||||
db::MediaLibrary::pointer mediaLibrary{ db::MediaLibrary::find(dbSession, _mediaLibrary.id) }; // may be null if settings are updated in // => next scan will correct this
|
||||
track.modify()->setMediaLibrary(mediaLibrary);
|
||||
db::Directory::pointer directory{ utils::getOrCreateDirectory(dbSession, _file.parent_path(), mediaLibrary) };
|
||||
track.modify()->setDirectory(directory);
|
||||
|
||||
track.modify()->clearArtistLinks();
|
||||
// Do not fallback on artists with the same name but having a MBID for artist and releaseArtists, as it may be corrected by properly tagging files
|
||||
for (const db::Artist::pointer& artist : getOrCreateArtists(dbSession, _parsedTrack->artists, false))
|
||||
track.modify()->addArtistLink(db::TrackArtistLink::create(dbSession, track, artist, db::TrackArtistLinkType::Artist));
|
||||
|
||||
if (_parsedTrack->medium && _parsedTrack->medium->release)
|
||||
{
|
||||
for (const db::Artist::pointer& releaseArtist : getOrCreateArtists(dbSession, _parsedTrack->medium->release->artists, false))
|
||||
track.modify()->addArtistLink(db::TrackArtistLink::create(dbSession, track, releaseArtist, db::TrackArtistLinkType::ReleaseArtist));
|
||||
}
|
||||
|
||||
// Allow fallbacks on artists with the same name even if they have MBID, since there is no tag to indicate the MBID of these artists
|
||||
// We could ask MusicBrainz to get all the information, but that would heavily slow down the import process
|
||||
for (const db::Artist::pointer& conductor : getOrCreateArtists(dbSession, _parsedTrack->conductorArtists, true))
|
||||
track.modify()->addArtistLink(db::TrackArtistLink::create(dbSession, track, conductor, db::TrackArtistLinkType::Conductor));
|
||||
|
||||
for (const db::Artist::pointer& composer : getOrCreateArtists(dbSession, _parsedTrack->composerArtists, true))
|
||||
track.modify()->addArtistLink(db::TrackArtistLink::create(dbSession, track, composer, db::TrackArtistLinkType::Composer));
|
||||
|
||||
for (const db::Artist::pointer& lyricist : getOrCreateArtists(dbSession, _parsedTrack->lyricistArtists, true))
|
||||
track.modify()->addArtistLink(db::TrackArtistLink::create(dbSession, track, lyricist, db::TrackArtistLinkType::Lyricist));
|
||||
|
||||
for (const db::Artist::pointer& mixer : getOrCreateArtists(dbSession, _parsedTrack->mixerArtists, true))
|
||||
track.modify()->addArtistLink(db::TrackArtistLink::create(dbSession, track, mixer, db::TrackArtistLinkType::Mixer));
|
||||
|
||||
for (const auto& [role, performers] : _parsedTrack->performerArtists)
|
||||
{
|
||||
for (const db::Artist::pointer& performer : getOrCreateArtists(dbSession, performers, true))
|
||||
track.modify()->addArtistLink(db::TrackArtistLink::create(dbSession, track, performer, db::TrackArtistLinkType::Performer, role));
|
||||
}
|
||||
|
||||
for (const db::Artist::pointer& producer : getOrCreateArtists(dbSession, _parsedTrack->producerArtists, true))
|
||||
track.modify()->addArtistLink(db::TrackArtistLink::create(dbSession, track, producer, db::TrackArtistLinkType::Producer));
|
||||
|
||||
for (const db::Artist::pointer& remixer : getOrCreateArtists(dbSession, _parsedTrack->remixerArtists, true))
|
||||
track.modify()->addArtistLink(db::TrackArtistLink::create(dbSession, track, remixer, db::TrackArtistLinkType::Remixer));
|
||||
|
||||
track.modify()->setScanVersion(_settings.scanVersion);
|
||||
if (_parsedTrack->medium && _parsedTrack->medium->release)
|
||||
track.modify()->setRelease(getOrCreateRelease(dbSession, *_parsedTrack->medium->release, directory));
|
||||
else
|
||||
track.modify()->setRelease({});
|
||||
track.modify()->setTotalTrack(_parsedTrack->medium ? _parsedTrack->medium->trackCount : std::nullopt);
|
||||
track.modify()->setReleaseReplayGain(_parsedTrack->medium ? _parsedTrack->medium->replayGain : std::nullopt);
|
||||
track.modify()->setDiscSubtitle(_parsedTrack->medium ? _parsedTrack->medium->name : "");
|
||||
track.modify()->setClusters(getOrCreateClusters(dbSession, *_parsedTrack));
|
||||
track.modify()->setName(title);
|
||||
track.modify()->setTrackNumber(_parsedTrack->position);
|
||||
track.modify()->setDiscNumber(_parsedTrack->medium ? _parsedTrack->medium->position : std::nullopt);
|
||||
track.modify()->setDate(_parsedTrack->date);
|
||||
track.modify()->setOriginalDate(_parsedTrack->originalDate);
|
||||
if (!track->getOriginalDate().isValid() && _parsedTrack->originalYear)
|
||||
track.modify()->setOriginalDate(core::PartialDateTime{ *_parsedTrack->originalYear });
|
||||
|
||||
// If a file has an OriginalDate but no date, set it to ease filtering
|
||||
if (!_parsedTrack->date.isValid() && _parsedTrack->originalDate.isValid())
|
||||
track.modify()->setDate(_parsedTrack->originalDate);
|
||||
|
||||
track.modify()->setRecordingMBID(_parsedTrack->recordingMBID);
|
||||
track.modify()->setTrackMBID(_parsedTrack->mbid);
|
||||
if (auto trackFeatures{ db::TrackFeatures::find(dbSession, track->getId()) })
|
||||
trackFeatures.remove(); // TODO: only if MBID changed?
|
||||
track.modify()->setCopyright(_parsedTrack->copyright);
|
||||
track.modify()->setCopyrightURL(_parsedTrack->copyrightURL);
|
||||
track.modify()->setAdvisory(getAdvisory(_parsedTrack->advisory));
|
||||
track.modify()->setComment(!_parsedTrack->comments.empty() ? _parsedTrack->comments.front() : ""); // only take the first one for now
|
||||
track.modify()->setTrackReplayGain(_parsedTrack->replayGain);
|
||||
track.modify()->setArtistDisplayName(_parsedTrack->artistDisplayName);
|
||||
|
||||
track.modify()->clearEmbeddedLyrics();
|
||||
for (const metadata::Lyrics& lyricsInfo : _parsedTrack->lyrics)
|
||||
track.modify()->addLyrics(createLyrics(dbSession, lyricsInfo));
|
||||
|
||||
updateEmbeddedImages(dbSession, track, _parsedImages);
|
||||
|
||||
if (added)
|
||||
{
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Added audio file " << _file);
|
||||
stats.additions++;
|
||||
}
|
||||
else
|
||||
{
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Updated audio file " << _file);
|
||||
stats.updates++;
|
||||
}
|
||||
}
|
||||
|
||||
metadata::ParserReadStyle getParserReadStyle()
|
||||
{
|
||||
std::string_view readStyle{ core::Service<core::IConfig>::get()->getString("scanner-parser-read-style", "average") };
|
||||
@@ -838,6 +55,7 @@ namespace lms::scanner
|
||||
params.userExtraTags = settings.extraTags;
|
||||
params.artistTagDelimiters = settings.artistTagDelimiters;
|
||||
params.defaultTagDelimiters = settings.defaultTagDelimiters;
|
||||
params.artistsToNotSplit.insert(settings.artistsToNotSplit.cbegin(), settings.artistsToNotSplit.end());
|
||||
params.backend = metadata::ParserBackend::TagLib;
|
||||
params.readStyle = getParserReadStyle();
|
||||
|
||||
@@ -845,7 +63,6 @@ namespace lms::scanner
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
AudioFileScanner::AudioFileScanner(db::Db& db, const ScannerSettings& settings)
|
||||
: _db{ db }
|
||||
, _settings{ settings }
|
||||
@@ -890,7 +107,7 @@ namespace lms::scanner
|
||||
const db::Track::pointer track{ db::Track::findByPath(dbSession, file.file) };
|
||||
if (track
|
||||
&& track->getLastWriteTime() == lastWriteTime
|
||||
&& track->getScanVersion() == _settings.scanVersion)
|
||||
&& track->getScanVersion() == _settings.audioScanVersion)
|
||||
{
|
||||
// this file may have been moved from one library to another, then we just need to update the media library id instead of a full rescan
|
||||
const auto trackMediaLibrary{ track->getMediaLibrary() };
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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 <filesystem>
|
||||
|
||||
#include "MediaLibraryInfo.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
struct FileToScan
|
||||
{
|
||||
std::filesystem::path file;
|
||||
MediaLibraryInfo mediaLibrary;
|
||||
};
|
||||
} // namespace lms::scanner
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
#include "core/LiteralString.hpp"
|
||||
|
||||
#include "MediaLibraryInfo.hpp"
|
||||
#include "FileToScan.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
@@ -32,12 +32,6 @@ namespace lms::scanner
|
||||
struct ScanContext;
|
||||
struct ScannerSettings;
|
||||
|
||||
struct FileToScan
|
||||
{
|
||||
std::filesystem::path file;
|
||||
MediaLibraryInfo mediaLibrary;
|
||||
};
|
||||
|
||||
class IFileScanner
|
||||
{
|
||||
public:
|
||||
|
||||
@@ -32,6 +32,7 @@ namespace lms::scanner
|
||||
|
||||
virtual ScanStep getStep() const = 0;
|
||||
virtual core::LiteralString getStepName() const = 0;
|
||||
virtual bool needProcess(const ScanContext& context) const = 0;
|
||||
virtual void process(ScanContext& context) = 0;
|
||||
};
|
||||
} // namespace lms::scanner
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
/*
|
||||
* 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 "ScanStepArtistReconciliation.hpp"
|
||||
|
||||
#include <cassert>
|
||||
#include <ostream>
|
||||
|
||||
#include "core/ILogger.hpp"
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/ArtistInfo.hpp"
|
||||
#include "database/Db.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "database/TrackArtistLink.hpp"
|
||||
#include "database/TrackList.hpp"
|
||||
#include "metadata/Types.hpp"
|
||||
|
||||
#include "ScannerSettings.hpp"
|
||||
#include "helpers/ArtistHelpers.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
namespace
|
||||
{
|
||||
std::ostream& operator<<(std::ostream& os, const db::Artist::pointer& artist)
|
||||
{
|
||||
os << artist->getName();
|
||||
if (const auto mbid{ artist->getMBID() })
|
||||
os << " [" << mbid->getAsString() << "]";
|
||||
|
||||
return os;
|
||||
}
|
||||
|
||||
void recomputeArtist(db::Session& session, db::TrackArtistLink::pointer link, bool allowArtistMBIDFallback)
|
||||
{
|
||||
assert(!link->isArtistMBIDMatched());
|
||||
|
||||
metadata::Artist artistInfo{ std::nullopt, link->getArtistName(), link->getArtistSortName().empty() ? std::nullopt : std::make_optional<std::string>(link->getArtistSortName()) };
|
||||
|
||||
db::Artist::pointer newArtist{ helpers::getOrCreateArtistByName(session, artistInfo, helpers::AllowFallbackOnMBIDEntry{ allowArtistMBIDFallback }) };
|
||||
LMS_LOG(DB, INFO, "Reconcile artist link for track " << link->getTrack()->getAbsoluteFilePath() << ", type " << static_cast<int>(link->getType()) << " from " << link->getArtist() << " to " << newArtist);
|
||||
|
||||
assert(newArtist != link->getArtist());
|
||||
link.modify()->setArtist(newArtist);
|
||||
}
|
||||
|
||||
void recomputeArtist(db::Session& session, db::ArtistInfo::pointer artistInfo, bool allowArtistMBIDFallback)
|
||||
{
|
||||
assert(!artistInfo->isMBIDMatched());
|
||||
|
||||
const metadata::Artist artistMetadata{ std::nullopt, artistInfo->getName(), artistInfo->getSortName().empty() ? std::nullopt : std::make_optional<std::string>(artistInfo->getSortName()) };
|
||||
db::Artist::pointer newArtist{ helpers::getOrCreateArtistByName(session, artistMetadata, helpers::AllowFallbackOnMBIDEntry{ allowArtistMBIDFallback }) };
|
||||
LMS_LOG(DB, INFO, "Reconcile artist link for artist info " << artistInfo->getAbsoluteFilePath() << " from " << artistInfo->getArtist() << " to " << newArtist);
|
||||
|
||||
assert(newArtist != artistInfo->getArtist());
|
||||
artistInfo.modify()->setArtist(newArtist);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool ScanStepArtistReconciliation::needProcess([[maybe_unused]] const ScanContext& context) const
|
||||
{
|
||||
// Since this step is very fast in case there is nothing to do, no need to skip if nothing has changed
|
||||
return true;
|
||||
}
|
||||
|
||||
void ScanStepArtistReconciliation::process(ScanContext& context)
|
||||
{
|
||||
// Reconcile artist links
|
||||
{
|
||||
// Order is important
|
||||
updateLinksForArtistNameNoLongerMatch(context);
|
||||
updateLinksWithArtistNameAmbiguity(context);
|
||||
}
|
||||
|
||||
// Reconcile artist info
|
||||
{
|
||||
// Order is important
|
||||
updateArtistInfoForArtistNameNoLongerMatch(context);
|
||||
updateArtistInfoWithArtistNameAmbiguity(context);
|
||||
}
|
||||
}
|
||||
|
||||
void ScanStepArtistReconciliation::updateArtistInfoForArtistNameNoLongerMatch(ScanContext& context)
|
||||
{
|
||||
static constexpr std::size_t batchSize{ 50 };
|
||||
|
||||
const bool allowArtistMBIDFallback{ _settings.allowArtistMBIDFallback };
|
||||
db::Session& session{ _db.getTLSSession() };
|
||||
|
||||
std::vector<db::ArtistInfo::pointer> artistInfo;
|
||||
while (!_abortScan)
|
||||
{
|
||||
artistInfo.clear();
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
db::ArtistInfo::findArtistNameNoLongerMatch(session, db::Range{ .offset = 0, .size = batchSize }, [&](const db::ArtistInfo::pointer& link) {
|
||||
artistInfo.push_back(link);
|
||||
});
|
||||
}
|
||||
|
||||
if (artistInfo.empty())
|
||||
break;
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
for (db::ArtistInfo::pointer& info : artistInfo)
|
||||
{
|
||||
recomputeArtist(session, info, allowArtistMBIDFallback);
|
||||
context.currentStepStats.processedElems++;
|
||||
}
|
||||
|
||||
_progressCallback(context.currentStepStats);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ScanStepArtistReconciliation::updateArtistInfoWithArtistNameAmbiguity(ScanContext& context)
|
||||
{
|
||||
static constexpr std::size_t batchSize{ 50 };
|
||||
|
||||
const bool allowArtistMBIDFallback{ _settings.allowArtistMBIDFallback };
|
||||
db::Session& session{ _db.getTLSSession() };
|
||||
|
||||
std::vector<db::ArtistInfo::pointer> artistInfo;
|
||||
while (!_abortScan)
|
||||
{
|
||||
artistInfo.clear();
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
db::ArtistInfo::findWithArtistNameAmbiguity(session, db::Range{ .offset = 0, .size = batchSize }, allowArtistMBIDFallback, [&](const db::ArtistInfo::pointer& info) {
|
||||
artistInfo.push_back(info);
|
||||
});
|
||||
}
|
||||
|
||||
if (artistInfo.empty())
|
||||
break;
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
for (db::ArtistInfo::pointer& info : artistInfo)
|
||||
{
|
||||
recomputeArtist(session, info, allowArtistMBIDFallback);
|
||||
context.currentStepStats.processedElems++;
|
||||
}
|
||||
|
||||
_progressCallback(context.currentStepStats);
|
||||
}
|
||||
}
|
||||
}
|
||||
void ScanStepArtistReconciliation::updateLinksForArtistNameNoLongerMatch(ScanContext& context)
|
||||
{
|
||||
static constexpr std::size_t batchSize{ 50 };
|
||||
|
||||
const bool allowArtistMBIDFallback{ _settings.allowArtistMBIDFallback };
|
||||
db::Session& session{ _db.getTLSSession() };
|
||||
|
||||
std::vector<db::TrackArtistLink::pointer> links;
|
||||
while (!_abortScan)
|
||||
{
|
||||
links.clear();
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
db::TrackArtistLink::findArtistNameNoLongerMatch(session, db::Range{ .offset = 0, .size = batchSize }, [&](const db::TrackArtistLink::pointer& link) {
|
||||
links.push_back(link);
|
||||
});
|
||||
}
|
||||
|
||||
if (links.empty())
|
||||
break;
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
for (db::TrackArtistLink::pointer& link : links)
|
||||
{
|
||||
recomputeArtist(session, link, allowArtistMBIDFallback);
|
||||
context.currentStepStats.processedElems++;
|
||||
}
|
||||
|
||||
_progressCallback(context.currentStepStats);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ScanStepArtistReconciliation::updateLinksWithArtistNameAmbiguity(ScanContext& context)
|
||||
{
|
||||
static constexpr std::size_t batchSize{ 50 };
|
||||
|
||||
const bool allowArtistMBIDFallback{ _settings.allowArtistMBIDFallback };
|
||||
db::Session& session{ _db.getTLSSession() };
|
||||
|
||||
std::vector<db::TrackArtistLink::pointer> links;
|
||||
while (!_abortScan)
|
||||
{
|
||||
links.clear();
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
db::TrackArtistLink::findWithArtistNameAmbiguity(session, db::Range{ .offset = 0, .size = batchSize }, allowArtistMBIDFallback, [&](const db::TrackArtistLink::pointer& link) {
|
||||
links.push_back(link);
|
||||
});
|
||||
}
|
||||
|
||||
if (links.empty())
|
||||
break;
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
for (db::TrackArtistLink::pointer& link : links)
|
||||
{
|
||||
recomputeArtist(session, link, allowArtistMBIDFallback);
|
||||
context.currentStepStats.processedElems++;
|
||||
}
|
||||
|
||||
_progressCallback(context.currentStepStats);
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace lms::scanner
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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 "ScanStepBase.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
class ScanStepArtistReconciliation : public ScanStepBase
|
||||
{
|
||||
public:
|
||||
using ScanStepBase::ScanStepBase;
|
||||
|
||||
private:
|
||||
ScanStep getStep() const override { return ScanStep::ReconciliateArtists; }
|
||||
core::LiteralString getStepName() const override { return "Artist reconciliation"; }
|
||||
bool needProcess(const ScanContext& context) const override;
|
||||
void process(ScanContext& context) override;
|
||||
|
||||
void updateLinksForArtistNameNoLongerMatch(ScanContext& context);
|
||||
void updateLinksWithArtistNameAmbiguity(ScanContext& context);
|
||||
void updateArtistInfoForArtistNameNoLongerMatch(ScanContext& context);
|
||||
void updateArtistInfoWithArtistNameAmbiguity(ScanContext& context);
|
||||
};
|
||||
} // namespace lms::scanner
|
||||
@@ -51,7 +51,7 @@ namespace lms::scanner
|
||||
};
|
||||
using ArtistImageAssociationContainer = std::deque<ArtistImageAssociation>;
|
||||
|
||||
struct SearchImageContext
|
||||
struct SearchArtistImageContext
|
||||
{
|
||||
db::Session& session;
|
||||
db::ArtistId lastRetrievedArtistId;
|
||||
@@ -59,7 +59,7 @@ namespace lms::scanner
|
||||
std::span<const std::string> artistFileNames;
|
||||
};
|
||||
|
||||
db::Image::pointer findImageInDirectory(SearchImageContext& searchContext, const std::filesystem::path& directoryPath, std::span<const std::string> fileStemsToSearch)
|
||||
db::Image::pointer findImageInDirectory(SearchArtistImageContext& searchContext, const std::filesystem::path& directoryPath, std::span<const std::string> fileStemsToSearch)
|
||||
{
|
||||
db::Image::pointer image;
|
||||
|
||||
@@ -85,7 +85,7 @@ namespace lms::scanner
|
||||
return image;
|
||||
}
|
||||
|
||||
db::Image::pointer getImageFromMbid(SearchImageContext& searchContext, const core::UUID& mbid)
|
||||
db::Image::pointer getImageFromMbid(SearchArtistImageContext& searchContext, const core::UUID& mbid)
|
||||
{
|
||||
db::Image::pointer image;
|
||||
|
||||
@@ -98,7 +98,7 @@ namespace lms::scanner
|
||||
return image;
|
||||
}
|
||||
|
||||
db::Image::pointer searchImageInArtistInfoDirectory(SearchImageContext& searchContext, db::ArtistId artistId)
|
||||
db::Image::pointer searchImageInArtistInfoDirectory(SearchArtistImageContext& searchContext, db::ArtistId artistId)
|
||||
{
|
||||
db::Image::pointer image;
|
||||
|
||||
@@ -116,7 +116,7 @@ namespace lms::scanner
|
||||
return image;
|
||||
}
|
||||
|
||||
db::Image::pointer searchImageInDirectories(SearchImageContext& searchContext, db::ArtistId artistId)
|
||||
db::Image::pointer searchImageInDirectories(SearchArtistImageContext& searchContext, db::ArtistId artistId)
|
||||
{
|
||||
db::Image::pointer image;
|
||||
|
||||
@@ -169,7 +169,7 @@ namespace lms::scanner
|
||||
return image;
|
||||
}
|
||||
|
||||
db::Image::pointer computeBestArtistImage(SearchImageContext& searchContext, const db::Artist::pointer& artist)
|
||||
db::Image::pointer computeBestArtistImage(SearchArtistImageContext& searchContext, const db::Artist::pointer& artist)
|
||||
{
|
||||
db::Image::pointer image;
|
||||
|
||||
@@ -185,7 +185,7 @@ namespace lms::scanner
|
||||
return image;
|
||||
}
|
||||
|
||||
bool fetchNextArtistImagesToUpdate(SearchImageContext& searchContext, ArtistImageAssociationContainer& artistImageAssociations)
|
||||
bool fetchNextArtistImagesToUpdate(SearchArtistImageContext& searchContext, ArtistImageAssociationContainer& artistImageAssociations)
|
||||
{
|
||||
const db::ArtistId artistId{ searchContext.lastRetrievedArtistId };
|
||||
|
||||
@@ -254,14 +254,16 @@ namespace lms::scanner
|
||||
{
|
||||
}
|
||||
|
||||
bool ScanStepAssociateArtistImages::needProcess(const ScanContext& context) const
|
||||
{
|
||||
if (context.stats.nbChanges() > 0)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void ScanStepAssociateArtistImages::process(ScanContext& context)
|
||||
{
|
||||
if (_abortScan)
|
||||
return;
|
||||
|
||||
if (context.stats.nbChanges() == 0)
|
||||
return;
|
||||
|
||||
auto& session{ _db.getTLSSession() };
|
||||
|
||||
{
|
||||
@@ -269,7 +271,7 @@ namespace lms::scanner
|
||||
context.currentStepStats.totalElems = db::Artist::getCount(session);
|
||||
}
|
||||
|
||||
SearchImageContext searchContext{
|
||||
SearchArtistImageContext searchContext{
|
||||
.session = session,
|
||||
.lastRetrievedArtistId = {},
|
||||
.artistFileNames = _artistFileNames,
|
||||
|
||||
@@ -37,6 +37,7 @@ namespace lms::scanner
|
||||
private:
|
||||
ScanStep getStep() const override { return ScanStep::AssociateArtistImages; }
|
||||
core::LiteralString getStepName() const override { return "Associate artist images"; }
|
||||
bool needProcess(const ScanContext& context) const override;
|
||||
void process(ScanContext& context) override;
|
||||
|
||||
const std::vector<std::string> _artistFileNames;
|
||||
|
||||
@@ -32,9 +32,6 @@ namespace lms::scanner
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr std::size_t readBatchSize{ 100 };
|
||||
constexpr std::size_t writeBatchSize{ 20 };
|
||||
|
||||
struct TrackLyricsAssociation
|
||||
{
|
||||
db::TrackLyricsId trackLyricsId;
|
||||
@@ -83,6 +80,8 @@ namespace lms::scanner
|
||||
|
||||
bool fetchNextTrackLyricsToUpdate(SearchTrackLyricsContext& searchContext, TrackLyricsAssociationContainer& trackLyricsAssociations)
|
||||
{
|
||||
constexpr std::size_t readBatchSize{ 100 };
|
||||
|
||||
const db::TrackLyricsId trackLyricsId{ searchContext.lastRetrievedTrackLyricsId };
|
||||
|
||||
{
|
||||
@@ -125,6 +124,8 @@ namespace lms::scanner
|
||||
|
||||
void updateTrackLyrics(db::Session& session, TrackLyricsAssociationContainer& lyricsAssociations)
|
||||
{
|
||||
constexpr std::size_t writeBatchSize{ 20 };
|
||||
|
||||
while (!lyricsAssociations.empty())
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
@@ -138,14 +139,16 @@ namespace lms::scanner
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool ScanStepAssociateExternalLyrics::needProcess(const ScanContext& context) const
|
||||
{
|
||||
if (context.stats.nbChanges() > 0)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void ScanStepAssociateExternalLyrics::process(ScanContext& context)
|
||||
{
|
||||
if (_abortScan)
|
||||
return;
|
||||
|
||||
if (context.stats.nbChanges() == 0)
|
||||
return;
|
||||
|
||||
auto& session{ _db.getTLSSession() };
|
||||
|
||||
{
|
||||
|
||||
@@ -31,6 +31,7 @@ namespace lms::scanner
|
||||
private:
|
||||
ScanStep getStep() const override { return ScanStep::AssociateExternalLyrics; }
|
||||
core::LiteralString getStepName() const override { return "Associate external lyrics"; }
|
||||
bool needProcess(const ScanContext& context) const override;
|
||||
void process(ScanContext& context) override;
|
||||
};
|
||||
} // namespace lms::scanner
|
||||
|
||||
@@ -37,9 +37,6 @@ namespace lms::scanner
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr std::size_t readBatchSize{ 20 };
|
||||
constexpr std::size_t writeBatchSize{ 5 };
|
||||
|
||||
struct TrackInfo
|
||||
{
|
||||
db::TrackId trackId;
|
||||
@@ -118,6 +115,8 @@ namespace lms::scanner
|
||||
const db::PlayListFileId playListFileIdId{ searchContext.lastRetrievedPlayListFileId };
|
||||
|
||||
{
|
||||
constexpr std::size_t readBatchSize{ 20 };
|
||||
|
||||
auto transaction{ searchContext.session.createReadTransaction() };
|
||||
|
||||
db::PlayListFile::find(searchContext.session, searchContext.lastRetrievedPlayListFileId, readBatchSize, [&](const db::PlayListFile::pointer& playListFile) {
|
||||
@@ -199,6 +198,8 @@ namespace lms::scanner
|
||||
|
||||
void updatePlayListFiles(db::Session& session, PlayListFileAssociationContainer& playListFileAssociations)
|
||||
{
|
||||
constexpr std::size_t writeBatchSize{ 5 };
|
||||
|
||||
while (!playListFileAssociations.empty())
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
@@ -212,14 +213,19 @@ namespace lms::scanner
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool ScanStepAssociatePlayListTracks::needProcess(const ScanContext& context) const
|
||||
{
|
||||
if (context.stats.nbChanges() > 0)
|
||||
return true;
|
||||
|
||||
if (getLastScanSettings() && getLastScanSettings()->skipSingleReleasePlayLists != _settings.skipSingleReleasePlayLists)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void ScanStepAssociatePlayListTracks::process(ScanContext& context)
|
||||
{
|
||||
if (_abortScan)
|
||||
return;
|
||||
|
||||
if (context.stats.nbChanges() == 0)
|
||||
return;
|
||||
|
||||
auto& session{ _db.getTLSSession() };
|
||||
|
||||
{
|
||||
|
||||
@@ -31,6 +31,7 @@ namespace lms::scanner
|
||||
private:
|
||||
ScanStep getStep() const override { return ScanStep::AssociatePlayListTracks; }
|
||||
core::LiteralString getStepName() const override { return "Associate playlist tracks"; }
|
||||
bool needProcess(const ScanContext& context) const override;
|
||||
void process(ScanContext& context) override;
|
||||
};
|
||||
} // namespace lms::scanner
|
||||
|
||||
@@ -38,9 +38,6 @@ namespace lms::scanner
|
||||
{
|
||||
namespace
|
||||
{
|
||||
constexpr std::size_t readBatchSize{ 100 };
|
||||
constexpr std::size_t writeBatchSize{ 20 };
|
||||
|
||||
struct ReleaseImageAssociation
|
||||
{
|
||||
db::ReleaseId releaseId;
|
||||
@@ -48,7 +45,7 @@ namespace lms::scanner
|
||||
};
|
||||
using ReleaseImageAssociationContainer = std::deque<ReleaseImageAssociation>;
|
||||
|
||||
struct SearchImageContext
|
||||
struct SearchReleaseImageContext
|
||||
{
|
||||
db::Session& session;
|
||||
db::ReleaseId lastRetrievedReleaseId;
|
||||
@@ -56,7 +53,7 @@ namespace lms::scanner
|
||||
const std::vector<std::string>& releaseFileNames;
|
||||
};
|
||||
|
||||
db::Image::pointer findImageInDirectory(SearchImageContext& searchContext, const std::filesystem::path& directoryPath)
|
||||
db::Image::pointer findImageInDirectory(SearchReleaseImageContext& searchContext, const std::filesystem::path& directoryPath)
|
||||
{
|
||||
db::Image::pointer image;
|
||||
|
||||
@@ -82,7 +79,7 @@ namespace lms::scanner
|
||||
return image;
|
||||
}
|
||||
|
||||
db::Image::pointer computeBestReleaseImage(SearchImageContext& searchContext, const db::Release::pointer& release)
|
||||
db::Image::pointer computeBestReleaseImage(SearchReleaseImageContext& searchContext, const db::Release::pointer& release)
|
||||
{
|
||||
db::Image::pointer image;
|
||||
|
||||
@@ -130,11 +127,13 @@ namespace lms::scanner
|
||||
return image;
|
||||
}
|
||||
|
||||
bool fetchNextReleaseImagesToUpdate(SearchImageContext& searchContext, ReleaseImageAssociationContainer& releaseImageAssociations)
|
||||
bool fetchNextReleaseImagesToUpdate(SearchReleaseImageContext& searchContext, ReleaseImageAssociationContainer& releaseImageAssociations)
|
||||
{
|
||||
const db::ReleaseId releaseId{ searchContext.lastRetrievedReleaseId };
|
||||
|
||||
{
|
||||
constexpr std::size_t readBatchSize{ 100 };
|
||||
|
||||
auto transaction{ searchContext.session.createReadTransaction() };
|
||||
|
||||
db::Release::find(searchContext.session, searchContext.lastRetrievedReleaseId, readBatchSize, [&](const db::Release::pointer& release) {
|
||||
@@ -166,6 +165,8 @@ namespace lms::scanner
|
||||
|
||||
void updateReleaseImages(db::Session& session, ReleaseImageAssociationContainer& imageAssociations)
|
||||
{
|
||||
constexpr std::size_t writeBatchSize{ 20 };
|
||||
|
||||
while (!imageAssociations.empty())
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
@@ -199,14 +200,16 @@ namespace lms::scanner
|
||||
{
|
||||
}
|
||||
|
||||
bool ScanStepAssociateReleaseImages::needProcess(const ScanContext& context) const
|
||||
{
|
||||
if (context.stats.nbChanges() > 0)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void ScanStepAssociateReleaseImages::process(ScanContext& context)
|
||||
{
|
||||
if (_abortScan)
|
||||
return;
|
||||
|
||||
if (context.stats.nbChanges() == 0)
|
||||
return;
|
||||
|
||||
auto& session{ _db.getTLSSession() };
|
||||
|
||||
{
|
||||
@@ -214,7 +217,7 @@ namespace lms::scanner
|
||||
context.currentStepStats.totalElems = db::Release::getCount(session);
|
||||
}
|
||||
|
||||
SearchImageContext searchContext{
|
||||
SearchReleaseImageContext searchContext{
|
||||
.session = session,
|
||||
.lastRetrievedReleaseId = {},
|
||||
.releaseFileNames = _releaseFileNames,
|
||||
|
||||
@@ -37,6 +37,7 @@ namespace lms::scanner
|
||||
private:
|
||||
ScanStep getStep() const override { return ScanStep::AssociateReleaseImages; }
|
||||
core::LiteralString getStepName() const override { return "Associate release images"; }
|
||||
bool needProcess(const ScanContext& context) const override;
|
||||
void process(ScanContext& context) override;
|
||||
|
||||
const std::vector<std::string> _releaseFileNames;
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (C) 2023 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 "ScanStepBase.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
ScanStepBase::ScanStepBase(InitParams& initParams)
|
||||
: _settings{ initParams.settings }
|
||||
, _progressCallback{ initParams.progressCallback }
|
||||
, _abortScan{ initParams.abortScan }
|
||||
, _db{ initParams.db }
|
||||
, _fileScanners(std::cbegin(initParams.fileScanners), std::cend(initParams.fileScanners))
|
||||
, _lastScanSettings{ initParams.lastScanSettings }
|
||||
{
|
||||
}
|
||||
|
||||
ScanStepBase::~ScanStepBase() = default;
|
||||
} // namespace lms::scanner
|
||||
@@ -44,29 +44,27 @@ namespace lms::scanner
|
||||
struct InitParams
|
||||
{
|
||||
const ScannerSettings& settings;
|
||||
const ScannerSettings* lastScanSettings{};
|
||||
ProgressCallback progressCallback;
|
||||
bool& abortScan;
|
||||
db::Db& db;
|
||||
std::span<IFileScanner*> fileScanners;
|
||||
};
|
||||
ScanStepBase(InitParams& initParams)
|
||||
: _settings{ initParams.settings }
|
||||
, _progressCallback{ initParams.progressCallback }
|
||||
, _abortScan{ initParams.abortScan }
|
||||
, _db{ initParams.db }
|
||||
, _fileScanners(std::cbegin(initParams.fileScanners), std::cend(initParams.fileScanners))
|
||||
{
|
||||
}
|
||||
|
||||
protected:
|
||||
~ScanStepBase() override = default;
|
||||
ScanStepBase(InitParams& initParams);
|
||||
~ScanStepBase() override;
|
||||
ScanStepBase(const ScanStepBase&) = delete;
|
||||
ScanStepBase& operator=(const ScanStepBase&) = delete;
|
||||
|
||||
protected:
|
||||
const ScannerSettings* getLastScanSettings() const { return _lastScanSettings; }
|
||||
|
||||
const ScannerSettings& _settings;
|
||||
ProgressCallback _progressCallback;
|
||||
bool& _abortScan;
|
||||
db::Db& _db;
|
||||
std::vector<IFileScanner*> _fileScanners;
|
||||
|
||||
private:
|
||||
const ScannerSettings* _lastScanSettings{};
|
||||
};
|
||||
} // namespace lms::scanner
|
||||
|
||||
@@ -26,13 +26,16 @@
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
bool ScanStepCheckForDuplicatedFiles::needProcess([[maybe_unused]] const ScanContext& context) const
|
||||
{
|
||||
// Always check for everything
|
||||
return true;
|
||||
}
|
||||
|
||||
void ScanStepCheckForDuplicatedFiles::process(ScanContext& context)
|
||||
{
|
||||
using namespace db;
|
||||
|
||||
if (_abortScan)
|
||||
return;
|
||||
|
||||
Session& session{ _db.getTLSSession() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ namespace lms::scanner
|
||||
private:
|
||||
core::LiteralString getStepName() const override { return "Check for duplicated files"; }
|
||||
ScanStep getStep() const override { return ScanStep::CheckForDuplicatedFiles; }
|
||||
bool needProcess(const ScanContext& context) const override;
|
||||
void process(ScanContext& context) override;
|
||||
};
|
||||
} // namespace lms::scanner
|
||||
|
||||
@@ -41,11 +41,14 @@ namespace lms::scanner
|
||||
constexpr std::size_t batchSize = 100;
|
||||
}
|
||||
|
||||
bool ScanStepCheckForRemovedFiles::needProcess([[maybe_unused]] const ScanContext& context) const
|
||||
{
|
||||
// always check for removed files
|
||||
return true;
|
||||
}
|
||||
|
||||
void ScanStepCheckForRemovedFiles::process(ScanContext& context)
|
||||
{
|
||||
if (_abortScan)
|
||||
return;
|
||||
|
||||
db::Session& session{ _db.getTLSSession() };
|
||||
|
||||
{
|
||||
|
||||
@@ -34,6 +34,7 @@ namespace lms::scanner
|
||||
private:
|
||||
core::LiteralString getStepName() const override { return "Check for removed files"; }
|
||||
ScanStep getStep() const override { return ScanStep::CheckForRemovedFiles; }
|
||||
bool needProcess(const ScanContext& context) const override;
|
||||
void process(ScanContext& context) override;
|
||||
|
||||
template<typename Object>
|
||||
|
||||
@@ -24,10 +24,14 @@
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
void ScanStepCompact::process(ScanContext& context)
|
||||
bool ScanStepCompact::needProcess(const ScanContext& context) const
|
||||
{
|
||||
// Don't auto compact as it may be too annoying to block the whole application for very large databases
|
||||
if (context.scanOptions.compact)
|
||||
_db.getTLSSession().vacuum();
|
||||
return context.scanOptions.compact;
|
||||
}
|
||||
|
||||
void ScanStepCompact::process([[maybe_unused]] ScanContext& context)
|
||||
{
|
||||
_db.getTLSSession().vacuum();
|
||||
}
|
||||
} // namespace lms::scanner
|
||||
|
||||
@@ -31,6 +31,7 @@ namespace lms::scanner
|
||||
private:
|
||||
ScanStep getStep() const override { return ScanStep::Compact; }
|
||||
core::LiteralString getStepName() const override { return "Compact"; }
|
||||
bool needProcess(const ScanContext& context) const override;
|
||||
void process(ScanContext& context) override;
|
||||
};
|
||||
} // namespace lms::scanner
|
||||
|
||||
@@ -25,13 +25,18 @@
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
bool ScanStepComputeClusterStats::needProcess(const ScanContext& context) const
|
||||
{
|
||||
if (context.stats.nbChanges() > 0)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void ScanStepComputeClusterStats::process(ScanContext& context)
|
||||
{
|
||||
using namespace db;
|
||||
|
||||
if (context.stats.nbChanges() == 0)
|
||||
return;
|
||||
|
||||
Session& dbSession{ _db.getTLSSession() };
|
||||
|
||||
const std::size_t clusterCount{ [&] {
|
||||
|
||||
@@ -31,6 +31,7 @@ namespace lms::scanner
|
||||
private:
|
||||
ScanStep getStep() const override { return ScanStep::ComputeClusterStats; }
|
||||
core::LiteralString getStepName() const override { return "Compute cluster stats"; }
|
||||
bool needProcess(const ScanContext& context) const override;
|
||||
void process(ScanContext& context) override;
|
||||
};
|
||||
} // namespace lms::scanner
|
||||
|
||||
@@ -28,6 +28,12 @@
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
bool ScanStepDiscoverFiles::needProcess([[maybe_unused]] const ScanContext& context) const
|
||||
{
|
||||
// always discover files
|
||||
return true;
|
||||
}
|
||||
|
||||
void ScanStepDiscoverFiles::process(ScanContext& context)
|
||||
{
|
||||
context.stats.totalFileCount = 0;
|
||||
|
||||
@@ -31,6 +31,7 @@ namespace lms::scanner
|
||||
private:
|
||||
ScanStep getStep() const override { return ScanStep::DiscoverFiles; }
|
||||
core::LiteralString getStepName() const override { return "Discover files"; }
|
||||
bool needProcess(const ScanContext& context) const override;
|
||||
void process(ScanContext& context) override;
|
||||
};
|
||||
} // namespace lms::scanner
|
||||
|
||||
@@ -25,32 +25,38 @@
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
bool ScanStepOptimize::needProcess(const ScanContext& context) const
|
||||
{
|
||||
if (context.scanOptions.forceOptimize)
|
||||
return true;
|
||||
|
||||
if (context.stats.nbChanges() > (context.stats.nbFiles() / 10))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void ScanStepOptimize::process(ScanContext& context)
|
||||
{
|
||||
ScanStats& stats{ context.stats };
|
||||
LMS_LOG(DBUPDATER, INFO, "Database analyze started");
|
||||
|
||||
if (context.scanOptions.forceOptimize || (stats.nbChanges() > (stats.nbFiles() / 10)))
|
||||
auto& session{ _db.getTLSSession() };
|
||||
|
||||
std::vector<std::string> entries;
|
||||
session.retrieveEntriesToAnalyze(entries);
|
||||
context.currentStepStats.totalElems = entries.size();
|
||||
_progressCallback(context.currentStepStats);
|
||||
|
||||
for (const std::string& entry : entries)
|
||||
{
|
||||
LMS_LOG(DBUPDATER, INFO, "Database analyze started");
|
||||
if (_abortScan)
|
||||
break;
|
||||
|
||||
auto& session{ _db.getTLSSession() };
|
||||
|
||||
std::vector<std::string> entries;
|
||||
session.retrieveEntriesToAnalyze(entries);
|
||||
context.currentStepStats.totalElems = entries.size();
|
||||
_db.getTLSSession().analyzeEntry(entry);
|
||||
context.currentStepStats.processedElems++;
|
||||
_progressCallback(context.currentStepStats);
|
||||
|
||||
for (const std::string& entry : entries)
|
||||
{
|
||||
if (_abortScan)
|
||||
break;
|
||||
|
||||
_db.getTLSSession().analyzeEntry(entry);
|
||||
context.currentStepStats.processedElems++;
|
||||
_progressCallback(context.currentStepStats);
|
||||
}
|
||||
|
||||
LMS_LOG(DBUPDATER, INFO, "Database analyze complete");
|
||||
}
|
||||
|
||||
LMS_LOG(DBUPDATER, INFO, "Database analyze complete");
|
||||
}
|
||||
} // namespace lms::scanner
|
||||
|
||||
@@ -31,6 +31,7 @@ namespace lms::scanner
|
||||
private:
|
||||
ScanStep getStep() const override { return ScanStep::Optimize; }
|
||||
core::LiteralString getStepName() const override { return "Optimize"; }
|
||||
bool needProcess(const ScanContext& context) const override;
|
||||
void process(ScanContext& context) override;
|
||||
};
|
||||
} // namespace lms::scanner
|
||||
|
||||
@@ -31,6 +31,12 @@
|
||||
|
||||
namespace lms::scanner
|
||||
{
|
||||
bool ScanStepRemoveOrphanedDbEntries::needProcess([[maybe_unused]] const ScanContext& context) const
|
||||
{
|
||||
// fast enough when there is nothing to do
|
||||
return true;
|
||||
}
|
||||
|
||||
void ScanStepRemoveOrphanedDbEntries::process(ScanContext& context)
|
||||
{
|
||||
removeOrphanedClusters(context);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user