Merge branch 'develop' for release v3.66.0
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
name: Build (alpine)
|
name: Multi-platform Builds (Alpine)
|
||||||
on: [push, pull_request]
|
on: [pull_request]
|
||||||
jobs:
|
jobs:
|
||||||
Build:
|
Build:
|
||||||
strategy:
|
strategy:
|
||||||
@@ -20,9 +20,9 @@ jobs:
|
|||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: 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
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
context: ./
|
context: ./
|
||||||
@@ -32,4 +32,4 @@ jobs:
|
|||||||
push: false
|
push: false
|
||||||
cache-from: type=local,src=/tmp/.buildx-cache
|
cache-from: type=local,src=/tmp/.buildx-cache
|
||||||
cache-to: type=local,dest=/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)
|
name: Basic Build (Arch)
|
||||||
on: [push, pull_request]
|
on: [push]
|
||||||
jobs:
|
jobs:
|
||||||
Build:
|
Build:
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
BUILD_TYPE: [Release, Debug]
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check Out Repo
|
- name: Check Out Repo
|
||||||
@@ -20,7 +17,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v3
|
||||||
@@ -28,7 +25,10 @@ jobs:
|
|||||||
context: ./
|
context: ./
|
||||||
file: ./Dockerfile-build-arch
|
file: ./Dockerfile-build-arch
|
||||||
builder: ${{ steps.buildx.outputs.name }}
|
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
|
push: false
|
||||||
cache-from: type=local,src=/tmp/.buildx-cache
|
cache-from: type=local,src=/tmp/.buildx-cache
|
||||||
cache-to: type=local,dest=/tmp/.buildx-cache
|
cache-to: type=local,dest=/tmp/.buildx-cache
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
name: "CodeQL"
|
name: "CodeQL"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
|
||||||
branches: [ "master", "develop" ]
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ "master", "develop" ]
|
branches: [ "master", "develop" ]
|
||||||
schedule:
|
schedule:
|
||||||
@@ -42,24 +40,24 @@ jobs:
|
|||||||
popd
|
popd
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v2
|
uses: github/codeql-action/init@v3
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
queries: +security-and-quality
|
queries: +security-and-quality
|
||||||
|
|
||||||
- if: matrix.language == 'javascript'
|
- if: matrix.language == 'javascript'
|
||||||
name: Autobuild
|
name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v2
|
uses: github/codeql-action/autobuild@v3
|
||||||
|
|
||||||
- if: matrix.language == 'cpp'
|
- if: matrix.language == 'cpp'
|
||||||
name: Build
|
name: Build
|
||||||
run: |
|
run: |
|
||||||
mkdir -p build
|
mkdir -p build
|
||||||
cd 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)
|
make -j$(nproc)
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v2
|
uses: github/codeql-action/analyze@v3
|
||||||
with:
|
with:
|
||||||
category: "/language:${{ matrix.language }}"
|
category: "/language:${{ matrix.language }}"
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ ARG LMS_BUILD_TYPE="Release"
|
|||||||
RUN \
|
RUN \
|
||||||
DIR=/tmp/lms/build && mkdir -p ${DIR} && cd ${DIR} && \
|
DIR=/tmp/lms/build && mkdir -p ${DIR} && cd ${DIR} && \
|
||||||
xx-info is-cross && export BUILD_TESTS=OFF || export BUILD_TESTS=ON && \
|
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) && \
|
VERBOSE=1 make -j$(nproc) && \
|
||||||
xx-verify src/lms/lms && \
|
xx-verify src/lms/lms && \
|
||||||
(xx-info is-cross || make test)
|
(xx-info is-cross || make test)
|
||||||
|
|||||||
+13
-8
@@ -12,18 +12,23 @@ ARG BUILD_PACKAGES="\
|
|||||||
libconfig \
|
libconfig \
|
||||||
make \
|
make \
|
||||||
pkgconfig \
|
pkgconfig \
|
||||||
|
stb \
|
||||||
taglib \
|
taglib \
|
||||||
wt \
|
wt \
|
||||||
xxhash"
|
xxhash"
|
||||||
|
|
||||||
RUN pacman -Syu --noconfirm
|
RUN pacman -Syu --noconfirm ${BUILD_PACKAGES} && \
|
||||||
RUN pacman -S --noconfirm ${BUILD_PACKAGES}
|
pacman -Scc --noconfirm && \
|
||||||
|
rm -rf /var/cache/pacman/pkg/*
|
||||||
|
|
||||||
# LMS
|
# LMS
|
||||||
COPY . /tmp/lms/
|
COPY . /tmp/lms/
|
||||||
ARG LMS_BUILD_TYPE="Release"
|
RUN mkdir -p /tmp/lms/build
|
||||||
RUN \
|
WORKDIR /tmp/lms/build
|
||||||
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 && \
|
ARG LMS_BUILD_TYPE=Release
|
||||||
VERBOSE=1 make -j$(nproc) && \
|
ARG LMS_UNITY_BUILD=ON
|
||||||
make test
|
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/
|
COPY . /tmp/lms/
|
||||||
RUN \
|
RUN \
|
||||||
DIR=/tmp/lms/build && mkdir -p ${DIR} && cd ${DIR} && \
|
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 -j$(nproc) install && \
|
||||||
LD_LIBRARY_PATH=${PREFIX}/lib make test && \
|
LD_LIBRARY_PATH=${PREFIX}/lib make test && \
|
||||||
mkdir -p ${PREFIX}/etc/ && \
|
mkdir -p ${PREFIX}/etc/ && \
|
||||||
|
|||||||
+2
-3
@@ -51,15 +51,14 @@ git clone https://github.com/epoupon/lms.git lms
|
|||||||
cd lms
|
cd lms
|
||||||
mkdir build
|
mkdir build
|
||||||
cd 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__:
|
__Notes__:
|
||||||
* you can customize the installation directory using `-DCMAKE_INSTALL_PREFIX=path` (defaults to `/usr/local`).
|
* 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`)
|
* you can customize the image library using `-DLMS_IMAGE_BACKEND=<stb|graphicsmagick>` (defaults to `stb`)
|
||||||
```sh
|
```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
|
### Installation
|
||||||
__Note__: the commands of this section require root privileges.
|
__Note__: the commands of this section require root privileges.
|
||||||
```sh
|
```sh
|
||||||
|
|||||||
@@ -39,6 +39,15 @@
|
|||||||
</label>
|
</label>
|
||||||
${artist-tag-delimiter-container class="row gy-3"}
|
${artist-tag-delimiter-container class="row gy-3"}
|
||||||
</div>
|
</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">
|
<div class="col-12">
|
||||||
<label class="form-label">
|
<label class="form-label">
|
||||||
${tr:Lms.Admin.Database.default-tag-delimiters}
|
${tr:Lms.Admin.Database.default-tag-delimiters}
|
||||||
@@ -58,6 +67,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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">
|
<div class="col-12">
|
||||||
<label class="form-label" for="${id:similarity-engine-type}">
|
<label class="form-label" for="${id:similarity-engine-type}">
|
||||||
${tr:Lms.Admin.Database.similarity-engine-type}
|
${tr:Lms.Admin.Database.similarity-engine-type}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
<plural case="1">{1} tracks</plural>
|
<plural case="1">{1} tracks</plural>
|
||||||
</message>
|
</message>
|
||||||
<message id="Lms.user">User</message>
|
<message id="Lms.user">User</message>
|
||||||
|
<message id="Lms.uuid-invalid">Invalid UUID</message>
|
||||||
|
|
||||||
<!--Locale-->
|
<!--Locale-->
|
||||||
<message id="Lms.locale.decimal-point">.</message>
|
<message id="Lms.locale.decimal-point">.</message>
|
||||||
@@ -72,7 +73,9 @@
|
|||||||
<message id="Lms.Admin.MediaLibrary.root-path">Root directory</message>
|
<message id="Lms.Admin.MediaLibrary.root-path">Root directory</message>
|
||||||
|
|
||||||
<!--Scan settings-->
|
<!--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.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.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.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>
|
<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.duplicates-header">{1} duplicate files:</message>
|
||||||
<message id="Lms.Admin.ScannerController.errors-header">{1} errors:</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.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.get-report">Get report</message>
|
||||||
<message id="Lms.Admin.ScannerController.last-scan">Last scan</message>
|
<message id="Lms.Admin.ScannerController.last-scan">Last scan</message>
|
||||||
<message id="Lms.Admin.ScannerController.last-scan-not-available">Not available</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-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-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-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-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-removing-orphaned-entries">Removing orphaned entries: {1} entries...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-scanning-files">Scanning files: {1}/{2} ({3}%)...</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>
|
<plural case="1">{1} pistes</plural>
|
||||||
</message>
|
</message>
|
||||||
<message id="Lms.user">Utilisateur</message>
|
<message id="Lms.user">Utilisateur</message>
|
||||||
|
<message id="Lms.uuid-invalid">UUID invalide</message>
|
||||||
|
|
||||||
<!--Locale-->
|
<!--Locale-->
|
||||||
<message id="Lms.locale.decimal-point">,</message>
|
<message id="Lms.locale.decimal-point">,</message>
|
||||||
@@ -72,7 +73,9 @@
|
|||||||
<message id="Lms.Admin.MediaLibrary.root-path">Répertoire racine</message>
|
<message id="Lms.Admin.MediaLibrary.root-path">Répertoire racine</message>
|
||||||
|
|
||||||
<!--Scan settings-->
|
<!--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.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.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.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>
|
<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.duplicates-header">{1} fichiers dupliqués :</message>
|
||||||
<message id="Lms.Admin.ScannerController.errors-header">{1} erreurs :</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.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.get-report">Rapport</message>
|
||||||
<message id="Lms.Admin.ScannerController.last-scan">Dernier scan</message>
|
<message id="Lms.Admin.ScannerController.last-scan">Dernier scan</message>
|
||||||
<message id="Lms.Admin.ScannerController.last-scan-not-available">Non disponible</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-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-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-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-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-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>
|
<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>
|
<plural case="1">{1} tracce</plural>
|
||||||
</message>
|
</message>
|
||||||
<message id="Lms.user">Utente</message>
|
<message id="Lms.user">Utente</message>
|
||||||
|
<message id="Lms.uuid-invalid">UUID non valido</message>
|
||||||
|
|
||||||
<!--Locale-->
|
<!--Locale-->
|
||||||
<message id="Lms.locale.decimal-point">,</message>
|
<message id="Lms.locale.decimal-point">,</message>
|
||||||
@@ -72,7 +73,9 @@
|
|||||||
<message id="Lms.Admin.MediaLibrary.root-path">Cartella principale</message>
|
<message id="Lms.Admin.MediaLibrary.root-path">Cartella principale</message>
|
||||||
|
|
||||||
<!--Scan settings-->
|
<!--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.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.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.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>
|
<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-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-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-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-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-removing-orphaned-entries">Rimozione voci orfane: {1} voci...</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-scanning-files">Scansione dei file: {1}/{2} ({3}%)...</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>
|
<plural case="2">{1} ścieżek</plural>
|
||||||
</message>
|
</message>
|
||||||
<message id="Lms.user">Użytkownik</message>
|
<message id="Lms.user">Użytkownik</message>
|
||||||
|
<message id="Lms.uuid-invalid">Nieprawidłowy UUID</message>
|
||||||
|
|
||||||
<!--Locale-->
|
<!--Locale-->
|
||||||
<message id="Lms.locale.decimal-point">,</message>
|
<message id="Lms.locale.decimal-point">,</message>
|
||||||
@@ -73,7 +74,9 @@
|
|||||||
<message id="Lms.Admin.MediaLibrary.root-path">Katalog główny</message>
|
<message id="Lms.Admin.MediaLibrary.root-path">Katalog główny</message>
|
||||||
|
|
||||||
<!--Scan settings-->
|
<!--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.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.daily">Codziennie</message>
|
||||||
<message id="Lms.Admin.Database.default-tag-delimiters">Znaki rozdzielające inne oznaczenia</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>
|
<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>
|
<plural case="2">{1} błędów:</plural>
|
||||||
</message>
|
</message>
|
||||||
<message id="Lms.Admin.ScannerController.force-optimize">Wymuś optymalizację bazy danych</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.get-report">Pokaż raport</message>
|
||||||
<message id="Lms.Admin.ScannerController.last-scan">Ostatnie skanowanie</message>
|
<message id="Lms.Admin.ScannerController.last-scan">Ostatnie skanowanie</message>
|
||||||
<message id="Lms.Admin.ScannerController.last-scan-not-available">Niedostępne</message>
|
<message id="Lms.Admin.ScannerController.last-scan-not-available">Niedostępne</message>
|
||||||
@@ -150,6 +153,7 @@
|
|||||||
</message>
|
</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-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-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-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-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>
|
<message id="Lms.Admin.ScannerController.step-scanning-files">Skanowanie plików: {1}/{2} ({3}%)...</message>
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
|
|
||||||
<message id="Lms.user">用户</message>
|
<message id="Lms.user">用户</message>
|
||||||
|
|
||||||
|
|
||||||
<!--Locale-->
|
<!--Locale-->
|
||||||
<message id="Lms.locale.decimal-point">.</message>
|
<message id="Lms.locale.decimal-point">.</message>
|
||||||
<message id="Lms.locale.group-separator">,</message>
|
<message id="Lms.locale.group-separator">,</message>
|
||||||
@@ -73,6 +74,7 @@
|
|||||||
|
|
||||||
<!--Scan settings-->
|
<!--Scan settings-->
|
||||||
|
|
||||||
|
|
||||||
<message id="Lms.Admin.Database.daily">每日</message>
|
<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-discovering-files">检索文件中: {1} 文件</message>
|
||||||
<message id="Lms.Admin.ScannerController.step-fetching-track-features">从 AcousticBrainz 获取音轨特征: {1}/{2} 音轨 ({3}%)...</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-reloading-similarity-engine">重载相似引擎中 {1}%...</message>
|
||||||
|
|
||||||
<message id="Lms.Admin.ScannerController.step-scanning-files">扫描文件中: {1}/{2} 个文件 ({3}%)...</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.ogg_vorbis">Ogg/Vorbis</message>
|
||||||
<message id="Lms.Settings.transcoding-output-format.webm_vorbis">WebM/Vorbis</message>
|
<message id="Lms.Settings.transcoding-output-format.webm_vorbis">WebM/Vorbis</message>
|
||||||
|
|
||||||
|
|
||||||
<!--Wt-->
|
<!--Wt-->
|
||||||
<message id="Wt.WMessageBox.Yes">是</message>
|
<message id="Wt.WMessageBox.Yes">是</message>
|
||||||
<message id="Wt.WMessageBox.No">否</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)
|
if (_precision == Precision::Invalid)
|
||||||
return "";
|
return "";
|
||||||
|
|||||||
@@ -471,7 +471,7 @@ namespace lms::core::stringUtils
|
|||||||
if (dateTime.isValid())
|
if (dateTime.isValid())
|
||||||
{
|
{
|
||||||
// assume UTC
|
// 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 "";
|
return "";
|
||||||
@@ -491,6 +491,9 @@ namespace lms::core::stringUtils
|
|||||||
Wt::WDateTime fromISO8601String(std::string_view dateTime)
|
Wt::WDateTime fromISO8601String(std::string_view dateTime)
|
||||||
{
|
{
|
||||||
// assume UTC
|
// 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");
|
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 fromString(std::string_view str);
|
||||||
static PartialDateTime fromWtDateTime(const Wt::WDateTime& dateTime);
|
static PartialDateTime fromWtDateTime(const Wt::WDateTime& dateTime);
|
||||||
std::string toISO8601String() const;
|
std::string toString() const;
|
||||||
|
|
||||||
constexpr bool isValid() const { return _precision != Precision::Invalid; }
|
constexpr bool isValid() const { return _precision != Precision::Invalid; }
|
||||||
|
|
||||||
|
|||||||
@@ -38,4 +38,7 @@ namespace lms::core
|
|||||||
private:
|
private:
|
||||||
T _value{};
|
T _value{};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
template<typename Tag>
|
||||||
|
using TaggedBool = TaggedType<Tag, bool>;
|
||||||
} // namespace lms::core
|
} // namespace lms::core
|
||||||
@@ -61,21 +61,21 @@ namespace lms::core::stringUtils::tests
|
|||||||
|
|
||||||
TEST(PartialDateTime, stringComparison)
|
TEST(PartialDateTime, stringComparison)
|
||||||
{
|
{
|
||||||
EXPECT_EQ((PartialDateTime{ 1992, 3, 27 }.toISO8601String()), (PartialDateTime{ 1992, 3, 27 }.toISO8601String()));
|
EXPECT_EQ((PartialDateTime{ 1992, 3, 27 }.toString()), (PartialDateTime{ 1992, 3, 27 }.toString()));
|
||||||
EXPECT_EQ((PartialDateTime{ 1992, 3 }.toISO8601String()), (PartialDateTime{ 1992, 3 }.toISO8601String()));
|
EXPECT_EQ((PartialDateTime{ 1992, 3 }.toString()), (PartialDateTime{ 1992, 3 }.toString()));
|
||||||
EXPECT_EQ(PartialDateTime{ 1992 }.toISO8601String(), PartialDateTime{ 1992 }.toISO8601String());
|
EXPECT_EQ(PartialDateTime{ 1992 }.toString(), PartialDateTime{ 1992 }.toString());
|
||||||
EXPECT_NE((PartialDateTime{ 1992, 3, 27 }.toISO8601String()), (PartialDateTime{ 1992, 3 }.toISO8601String()));
|
EXPECT_NE((PartialDateTime{ 1992, 3, 27 }.toString()), (PartialDateTime{ 1992, 3 }.toString()));
|
||||||
EXPECT_NE((PartialDateTime{ 1992, 3, 27 }.toISO8601String()), (PartialDateTime{ 1992 }.toISO8601String()));
|
EXPECT_NE((PartialDateTime{ 1992, 3, 27 }.toString()), (PartialDateTime{ 1992 }.toString()));
|
||||||
EXPECT_NE((PartialDateTime{ 1992, 3 }.toISO8601String()), (PartialDateTime{ 1992 }.toISO8601String()));
|
EXPECT_NE((PartialDateTime{ 1992, 3 }.toString()), (PartialDateTime{ 1992 }.toString()));
|
||||||
EXPECT_NE((PartialDateTime{ 1992, 3, 27 }.toISO8601String()), (PartialDateTime{ 1992, 3, 28 }.toISO8601String()));
|
EXPECT_NE((PartialDateTime{ 1992, 3, 27 }.toString()), (PartialDateTime{ 1992, 3, 28 }.toString()));
|
||||||
EXPECT_NE((PartialDateTime{ 1992, 3, 27 }.toISO8601String()), (PartialDateTime{ 1992, 4, 27 }.toISO8601String()));
|
EXPECT_NE((PartialDateTime{ 1992, 3, 27 }.toString()), (PartialDateTime{ 1992, 4, 27 }.toString()));
|
||||||
EXPECT_NE((PartialDateTime{ 1992, 3, 27 }.toISO8601String()), (PartialDateTime{ 1993, 3, 27 }.toISO8601String()));
|
EXPECT_NE((PartialDateTime{ 1992, 3, 27 }.toString()), (PartialDateTime{ 1993, 3, 27 }.toString()));
|
||||||
EXPECT_GT((PartialDateTime{ 1993, 3, 28 }.toISO8601String()), (PartialDateTime{ 1993, 3, 27 }.toISO8601String()));
|
EXPECT_GT((PartialDateTime{ 1993, 3, 28 }.toString()), (PartialDateTime{ 1993, 3, 27 }.toString()));
|
||||||
EXPECT_GT((PartialDateTime{ 1993, 4 }.toISO8601String()), (PartialDateTime{ 1993, 3, 27 }.toISO8601String()));
|
EXPECT_GT((PartialDateTime{ 1993, 4 }.toString()), (PartialDateTime{ 1993, 3, 27 }.toString()));
|
||||||
EXPECT_GT((PartialDateTime{ 1994 }.toISO8601String()), (PartialDateTime{ 1993, 3, 27 }.toISO8601String()));
|
EXPECT_GT((PartialDateTime{ 1994 }.toString()), (PartialDateTime{ 1993, 3, 27 }.toString()));
|
||||||
EXPECT_LT((PartialDateTime{ 1993, 3, 27 }.toISO8601String()), (PartialDateTime{ 1993, 3, 28 }.toISO8601String()));
|
EXPECT_LT((PartialDateTime{ 1993, 3, 27 }.toString()), (PartialDateTime{ 1993, 3, 28 }.toString()));
|
||||||
EXPECT_LT((PartialDateTime{ 1993, 3, 27 }.toISO8601String()), (PartialDateTime{ 1993, 4 }.toISO8601String()));
|
EXPECT_LT((PartialDateTime{ 1993, 3, 27 }.toString()), (PartialDateTime{ 1993, 4 }.toString()));
|
||||||
EXPECT_LT((PartialDateTime{ 1993, 3, 27 }.toISO8601String()), (PartialDateTime{ 1994 }.toISO8601String()));
|
EXPECT_LT((PartialDateTime{ 1993, 3, 27 }.toString()), (PartialDateTime{ 1994 }.toString()));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(PartialDateTime, stringConversions)
|
TEST(PartialDateTime, stringConversions)
|
||||||
@@ -120,7 +120,7 @@ namespace lms::core::stringUtils::tests
|
|||||||
for (const TestCase& test : tests)
|
for (const TestCase& test : tests)
|
||||||
{
|
{
|
||||||
const PartialDateTime dateTime{ PartialDateTime::fromString(test.input) };
|
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
|
} // 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 } };
|
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 } };
|
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)
|
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-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("2020-01-03"), Wt::WDateTime{});
|
||||||
EXPECT_EQ(fromISO8601String(""), Wt::WDateTime{});
|
EXPECT_EQ(fromISO8601String(""), Wt::WDateTime{});
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ add_library(lmsdatabase STATIC
|
|||||||
impl/Cluster.cpp
|
impl/Cluster.cpp
|
||||||
impl/Db.cpp
|
impl/Db.cpp
|
||||||
impl/Directory.cpp
|
impl/Directory.cpp
|
||||||
|
impl/IdType.cpp
|
||||||
impl/Image.cpp
|
impl/Image.cpp
|
||||||
impl/Listen.cpp
|
impl/Listen.cpp
|
||||||
impl/MediaLibrary.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));
|
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)
|
RangeResults<ArtistId> Artist::findIds(Session& session, const FindParameters& params)
|
||||||
{
|
{
|
||||||
session.checkReadTransaction();
|
session.checkReadTransaction();
|
||||||
@@ -304,6 +280,41 @@ AND NOT EXISTS (
|
|||||||
utils::forEachQueryRangeResult(query, params.range, func);
|
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
|
ObjectPtr<Image> Artist::getImage() const
|
||||||
{
|
{
|
||||||
return ObjectPtr<Image>{ _image };
|
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)
|
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)
|
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
|
Artist::pointer ArtistInfo::getArtist() const
|
||||||
{
|
{
|
||||||
return _artist;
|
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
|
namespace
|
||||||
{
|
{
|
||||||
static constexpr Version LMS_DATABASE_VERSION{ 85 };
|
static constexpr Version LMS_DATABASE_VERSION{ 88 };
|
||||||
}
|
}
|
||||||
|
|
||||||
VersionInfo::VersionInfo()
|
VersionInfo::VersionInfo()
|
||||||
@@ -1159,6 +1159,36 @@ FROM tracklist)");
|
|||||||
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET scan_version = scan_version + 1");
|
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)
|
bool doDbMigration(Session& session)
|
||||||
{
|
{
|
||||||
constexpr std::string_view outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
|
constexpr std::string_view outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
|
||||||
@@ -1220,6 +1250,9 @@ FROM tracklist)");
|
|||||||
{ 82, migrateFromV82 },
|
{ 82, migrateFromV82 },
|
||||||
{ 83, migrateFromV83 },
|
{ 83, migrateFromV83 },
|
||||||
{ 84, migrateFromV84 },
|
{ 84, migrateFromV84 },
|
||||||
|
{ 85, migrateFromV85 },
|
||||||
|
{ 86, migrateFromV86 },
|
||||||
|
{ 87, migrateFromV87 },
|
||||||
};
|
};
|
||||||
|
|
||||||
bool migrationPerformed{};
|
bool migrationPerformed{};
|
||||||
|
|||||||
@@ -26,24 +26,33 @@
|
|||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
|
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
|
#include "traits/IdTypeTraits.hpp"
|
||||||
|
#include "traits/StringViewTraits.hpp"
|
||||||
|
|
||||||
namespace lms::db
|
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();
|
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
|
std::vector<std::string_view> ScanSettings::getExtraTagsToScan() const
|
||||||
@@ -65,13 +74,19 @@ namespace lms::db
|
|||||||
return core::stringUtils::splitEscapedStrings(_defaultTagDelimiters, ';', '\\');
|
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)
|
void ScanSettings::setExtraTagsToScan(std::span<const std::string_view> extraTags)
|
||||||
{
|
{
|
||||||
std::string newTagsToScan{ core::stringUtils::joinStrings(extraTags, ";") };
|
std::string newTagsToScan{ core::stringUtils::joinStrings(extraTags, ";") };
|
||||||
if (newTagsToScan != _extraTagsToScan)
|
if (newTagsToScan != _extraTagsToScan)
|
||||||
incScanVersion();
|
{
|
||||||
|
_extraTagsToScan.swap(newTagsToScan);
|
||||||
_extraTagsToScan = std::move(newTagsToScan);
|
incAudioScanVersion();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ScanSettings::setArtistTagDelimiters(std::span<const std::string_view> delimiters)
|
void ScanSettings::setArtistTagDelimiters(std::span<const std::string_view> delimiters)
|
||||||
@@ -80,7 +95,17 @@ namespace lms::db
|
|||||||
if (tagDelimiters != _artistTagDelimiters)
|
if (tagDelimiters != _artistTagDelimiters)
|
||||||
{
|
{
|
||||||
_artistTagDelimiters.swap(tagDelimiters);
|
_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)
|
if (tagDelimiters != _defaultTagDelimiters)
|
||||||
{
|
{
|
||||||
_defaultTagDelimiters.swap(tagDelimiters);
|
_defaultTagDelimiters.swap(tagDelimiters);
|
||||||
incScanVersion();
|
incAudioScanVersion();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ScanSettings::setSkipSingleReleasePlayLists(bool value)
|
void ScanSettings::setSkipSingleReleasePlayLists(bool value)
|
||||||
{
|
{
|
||||||
if (_skipSingleReleasePlayLists != value)
|
if (_skipSingleReleasePlayLists != value)
|
||||||
{
|
|
||||||
_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
|
} // namespace lms::db
|
||||||
|
|||||||
@@ -180,7 +180,9 @@ namespace lms::db
|
|||||||
// TODO: move this elsewhere
|
// TODO: move this elsewhere
|
||||||
{
|
{
|
||||||
auto uniqueTransaction{ createWriteTransaction() };
|
auto uniqueTransaction{ createWriteTransaction() };
|
||||||
ScanSettings::init(*this);
|
|
||||||
|
if (!ScanSettings::find(*this))
|
||||||
|
create<ScanSettings>();
|
||||||
}
|
}
|
||||||
|
|
||||||
return migrationPerformed;
|
return migrationPerformed;
|
||||||
@@ -195,13 +197,14 @@ namespace lms::db
|
|||||||
auto transaction{ createWriteTransaction() };
|
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_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_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_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_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_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_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_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_user_domain_idx ON auth_token(user_id, domain)");
|
||||||
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_domain_expiry_idx ON auth_token(domain, expiry)");
|
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS auth_token_domain_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 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_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_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_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_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)");
|
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
|
} // 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 }
|
: _type{ type }
|
||||||
, _subType{ subType }
|
, _subType{ subType }
|
||||||
|
, _artistMBIDMatched{ artistMBIDMatched }
|
||||||
, _track{ getDboPtr(track) }
|
, _track{ getDboPtr(track) }
|
||||||
, _artist{ getDboPtr(artist) }
|
, _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();
|
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();
|
session.getDboSession()->flush();
|
||||||
|
|
||||||
return res;
|
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)
|
TrackArtistLink::pointer TrackArtistLink::find(Session& session, TrackArtistLinkId id)
|
||||||
{
|
{
|
||||||
session.checkReadTransaction();
|
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)
|
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;
|
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
|
} // namespace lms::db
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ namespace Wt::Dbo
|
|||||||
if (!dateTime.isValid())
|
if (!dateTime.isValid())
|
||||||
statement->bindNull(column);
|
statement->bindNull(column);
|
||||||
else
|
else
|
||||||
statement->bind(column, dateTime.toISO8601String());
|
statement->bind(column, dateTime.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool read(lms::core::PartialDateTime& dateTime, SqlStatement* statement, int column, int size)
|
static bool read(lms::core::PartialDateTime& dateTime, SqlStatement* statement, int column, int size)
|
||||||
|
|||||||
@@ -134,8 +134,10 @@ namespace lms::db
|
|||||||
// Accessors
|
// Accessors
|
||||||
const std::string& getName() const { return _name; }
|
const std::string& getName() const { return _name; }
|
||||||
const std::string& getSortName() const { return _sortName; }
|
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;
|
ObjectPtr<Image> getImage() const;
|
||||||
|
void visitLinks(std::function<void(const ObjectPtr<TrackArtistLink>& link)> visitor) const;
|
||||||
|
|
||||||
// No artistLinkTypes means get them all
|
// No artistLinkTypes means get them all
|
||||||
RangeResults<ArtistId> findSimilarArtistIds(core::EnumSet<TrackArtistLinkType> artistLinkTypes = {}, std::optional<Range> range = std::nullopt) const;
|
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 void find(Session& session, ArtistId id, const std::function<void(const pointer&)>& func);
|
||||||
static pointer find(Session& session, const std::filesystem::path& path);
|
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 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
|
// getters
|
||||||
const std::filesystem::path& getAbsoluteFilePath() const { return _absoluteFilePath; }
|
const std::filesystem::path& getAbsoluteFilePath() const { return _absoluteFilePath; }
|
||||||
@@ -58,20 +60,26 @@ namespace lms::db
|
|||||||
ObjectPtr<Directory> getDirectory() const;
|
ObjectPtr<Directory> getDirectory() const;
|
||||||
ObjectPtr<Artist> getArtist() const;
|
ObjectPtr<Artist> getArtist() const;
|
||||||
DirectoryId getDirectoryId() const { return _directory.id(); }
|
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 getType() const { return _type; }
|
||||||
std::string_view getGender() const { return _gender; }
|
std::string_view getGender() const { return _gender; }
|
||||||
std::string_view getDisambiguation() const { return _disambiguation; }
|
std::string_view getDisambiguation() const { return _disambiguation; }
|
||||||
std::string_view getBiography() const { return _biography; }
|
std::string_view getBiography() const { return _biography; }
|
||||||
|
bool isMBIDMatched() const { return _MBIDMatched; }
|
||||||
|
|
||||||
// setters
|
// setters
|
||||||
void setAbsoluteFilePath(const std::filesystem::path& filePath);
|
void setAbsoluteFilePath(const std::filesystem::path& filePath);
|
||||||
void setLastWriteTime(Wt::WDateTime time) { _fileLastWrite = time; }
|
void setLastWriteTime(Wt::WDateTime time) { _fileLastWrite = time; }
|
||||||
void setDirectory(ObjectPtr<Directory> directory);
|
void setDirectory(ObjectPtr<Directory> directory);
|
||||||
void setArtist(ObjectPtr<Artist> artist);
|
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 setType(std::string_view type) { _type = type; }
|
||||||
void setGender(std::string_view gender) { _gender = gender; }
|
void setGender(std::string_view gender) { _gender = gender; }
|
||||||
void setDisambiguation(std::string_view disambiguation) { _disambiguation = disambiguation; }
|
void setDisambiguation(std::string_view disambiguation) { _disambiguation = disambiguation; }
|
||||||
void setBiography(std::string_view biography) { _biography = biography; };
|
void setBiography(std::string_view biography) { _biography = biography; };
|
||||||
|
void setMBIDMatched(bool matched) { _MBIDMatched = matched; }
|
||||||
|
|
||||||
template<class Action>
|
template<class Action>
|
||||||
void persist(Action& a)
|
void persist(Action& a)
|
||||||
@@ -79,11 +87,15 @@ namespace lms::db
|
|||||||
Wt::Dbo::field(a, _absoluteFilePath, "absolute_file_path");
|
Wt::Dbo::field(a, _absoluteFilePath, "absolute_file_path");
|
||||||
Wt::Dbo::field(a, _fileLastWrite, "file_last_write");
|
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, _type, "type");
|
||||||
Wt::Dbo::field(a, _gender, "gender");
|
Wt::Dbo::field(a, _gender, "gender");
|
||||||
Wt::Dbo::field(a, _disambiguation, "disambiguation");
|
Wt::Dbo::field(a, _disambiguation, "disambiguation");
|
||||||
Wt::Dbo::field(a, _biography, "biography");
|
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, _directory, "directory", Wt::Dbo::OnDeleteCascade);
|
||||||
Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade);
|
Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade);
|
||||||
}
|
}
|
||||||
@@ -97,11 +109,18 @@ namespace lms::db
|
|||||||
std::string _fileStem;
|
std::string _fileStem;
|
||||||
Wt::WDateTime _fileLastWrite;
|
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 _type;
|
||||||
std::string _gender;
|
std::string _gender;
|
||||||
std::string _disambiguation;
|
std::string _disambiguation;
|
||||||
std::string _biography;
|
std::string _biography;
|
||||||
|
|
||||||
|
bool _MBIDMatched{};
|
||||||
|
|
||||||
Wt::Dbo::ptr<Directory> _directory;
|
Wt::Dbo::ptr<Directory> _directory;
|
||||||
Wt::Dbo::ptr<Artist> _artist;
|
Wt::Dbo::ptr<Artist> _artist;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <span>
|
#include <span>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "database/ClusterId.hpp"
|
#include "database/ClusterId.hpp"
|
||||||
#include "database/LabelId.hpp"
|
#include "database/LabelId.hpp"
|
||||||
|
|||||||
@@ -19,9 +19,6 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <Wt/Dbo/ptr.h>
|
|
||||||
#include <cassert>
|
|
||||||
#include <functional>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
namespace lms::db
|
namespace lms::db
|
||||||
@@ -29,24 +26,19 @@ namespace lms::db
|
|||||||
class IdType
|
class IdType
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
using ValueType = Wt::Dbo::dbo_default_traits::IdType;
|
using ValueType = long long;
|
||||||
|
|
||||||
IdType() = default;
|
IdType();
|
||||||
IdType(ValueType id)
|
IdType(ValueType id);
|
||||||
: _id{ id } {}
|
|
||||||
|
|
||||||
bool isValid() const { return _id != Wt::Dbo::dbo_default_traits::invalidId(); }
|
bool isValid() const;
|
||||||
std::string toString() const
|
std::string toString() const;
|
||||||
{
|
|
||||||
assert(isValid());
|
|
||||||
return std::to_string(_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
ValueType getValue() const { return _id; }
|
ValueType getValue() const { return _id; }
|
||||||
auto operator<=>(const IdType& other) const = default;
|
auto operator<=>(const IdType& other) const = default;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Wt::Dbo::dbo_default_traits::IdType _id{ Wt::Dbo::dbo_default_traits::invalidId() };
|
ValueType _id;
|
||||||
};
|
};
|
||||||
|
|
||||||
#define LMS_DECLARE_IDTYPE(name) \
|
#define LMS_DECLARE_IDTYPE(name) \
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <filesystem>
|
|
||||||
#include <span>
|
#include <span>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
@@ -58,19 +57,22 @@ namespace lms::db
|
|||||||
None,
|
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
|
// Getters
|
||||||
std::size_t getScanVersion() const { return _scanVersion; }
|
std::size_t getAudioScanVersion() const { return _audioScanVersion; }
|
||||||
Wt::WTime getUpdateStartTime() const { return _startTime; }
|
Wt::WTime getUpdateStartTime() const { return _startTime; }
|
||||||
UpdatePeriod getUpdatePeriod() const { return _updatePeriod; }
|
UpdatePeriod getUpdatePeriod() const { return _updatePeriod; }
|
||||||
std::vector<std::string_view> getExtraTagsToScan() const;
|
std::vector<std::string_view> getExtraTagsToScan() const;
|
||||||
SimilarityEngineType getSimilarityEngineType() const { return _similarityEngineType; }
|
SimilarityEngineType getSimilarityEngineType() const { return _similarityEngineType; }
|
||||||
std::vector<std::string> getArtistTagDelimiters() const;
|
std::vector<std::string> getArtistTagDelimiters() const;
|
||||||
std::vector<std::string> getDefaultTagDelimiters() const;
|
std::vector<std::string> getDefaultTagDelimiters() const;
|
||||||
|
std::vector<std::string> getArtistsToNotSplit() const;
|
||||||
bool getSkipSingleReleasePlayLists() const { return _skipSingleReleasePlayLists; }
|
bool getSkipSingleReleasePlayLists() const { return _skipSingleReleasePlayLists; }
|
||||||
|
bool getAllowMBIDArtistMerge() const { return _allowMBIDArtistMerge; }
|
||||||
|
|
||||||
// Setters
|
// Setters
|
||||||
void setUpdateStartTime(Wt::WTime t) { _startTime = t; }
|
void setUpdateStartTime(Wt::WTime t) { _startTime = t; }
|
||||||
@@ -78,31 +80,45 @@ namespace lms::db
|
|||||||
void setExtraTagsToScan(std::span<const std::string_view> extraTags);
|
void setExtraTagsToScan(std::span<const std::string_view> extraTags);
|
||||||
void setSimilarityEngineType(SimilarityEngineType type) { _similarityEngineType = type; }
|
void setSimilarityEngineType(SimilarityEngineType type) { _similarityEngineType = type; }
|
||||||
void setArtistTagDelimiters(std::span<const std::string_view> delimiters);
|
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 setDefaultTagDelimiters(std::span<const std::string_view> delimiters);
|
||||||
void setSkipSingleReleasePlayLists(bool value);
|
void setSkipSingleReleasePlayLists(bool value);
|
||||||
void incScanVersion();
|
void setAllowMBIDArtistMerge(bool value);
|
||||||
|
|
||||||
template<class Action>
|
template<class Action>
|
||||||
void persist(Action& a)
|
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, _startTime, "start_time");
|
||||||
Wt::Dbo::field(a, _updatePeriod, "update_period");
|
Wt::Dbo::field(a, _updatePeriod, "update_period");
|
||||||
Wt::Dbo::field(a, _similarityEngineType, "similarity_engine_type");
|
Wt::Dbo::field(a, _similarityEngineType, "similarity_engine_type");
|
||||||
Wt::Dbo::field(a, _extraTagsToScan, "extra_tags_to_scan");
|
Wt::Dbo::field(a, _extraTagsToScan, "extra_tags_to_scan");
|
||||||
Wt::Dbo::field(a, _artistTagDelimiters, "artist_tag_delimiters");
|
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, _defaultTagDelimiters, "default_tag_delimiters");
|
||||||
Wt::Dbo::field(a, _skipSingleReleasePlayLists, "skip_single_release_playlists");
|
Wt::Dbo::field(a, _skipSingleReleasePlayLists, "skip_single_release_playlists");
|
||||||
|
Wt::Dbo::field(a, _allowMBIDArtistMerge, "allow_mbid_artist_merge");
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
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 };
|
Wt::WTime _startTime = Wt::WTime{ 0, 0, 0 };
|
||||||
UpdatePeriod _updatePeriod{ UpdatePeriod::Never };
|
UpdatePeriod _updatePeriod{ UpdatePeriod::Never };
|
||||||
SimilarityEngineType _similarityEngineType{ SimilarityEngineType::Clusters };
|
SimilarityEngineType _similarityEngineType{ SimilarityEngineType::Clusters };
|
||||||
std::string _extraTagsToScan;
|
std::string _extraTagsToScan;
|
||||||
std::string _artistTagDelimiters;
|
std::string _artistTagDelimiters;
|
||||||
|
std::string _artistsToNotSplit;
|
||||||
std::string _defaultTagDelimiters;
|
std::string _defaultTagDelimiters;
|
||||||
bool _skipSingleReleasePlayLists{ false };
|
bool _skipSingleReleasePlayLists{};
|
||||||
|
bool _allowMBIDArtistMerge{};
|
||||||
};
|
};
|
||||||
} // namespace lms::db
|
} // namespace lms::db
|
||||||
|
|||||||
@@ -80,32 +80,51 @@ namespace lms::db
|
|||||||
};
|
};
|
||||||
|
|
||||||
TrackArtistLink() = default;
|
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, 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 TrackArtistLink::pointer&)>&);
|
static void find(Session& session, const FindParameters& parameters, const std::function<void(const pointer&)>& func);
|
||||||
static pointer find(Session& session, TrackArtistLinkId linkId);
|
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 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<Track> getTrack() const { return _track; }
|
||||||
ObjectPtr<Artist> getArtist() const { return _artist; }
|
ObjectPtr<Artist> getArtist() const { return _artist; }
|
||||||
TrackArtistLinkType getType() const { return _type; }
|
TrackArtistLinkType getType() const { return _type; }
|
||||||
std::string_view getSubType() const { return _subType; }
|
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>
|
template<class Action>
|
||||||
void persist(Action& a)
|
void persist(Action& a)
|
||||||
{
|
{
|
||||||
Wt::Dbo::field(a, _type, "type");
|
Wt::Dbo::field(a, _type, "type");
|
||||||
Wt::Dbo::field(a, _subType, "subtype");
|
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, _track, "track", Wt::Dbo::OnDeleteCascade);
|
||||||
Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade);
|
Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade);
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
TrackArtistLinkType _type;
|
TrackArtistLinkType _type{ TrackArtistLinkType::Artist };
|
||||||
std::string _subType;
|
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<Track> _track;
|
||||||
Wt::Dbo::ptr<Artist> _artist;
|
Wt::Dbo::ptr<Artist> _artist;
|
||||||
|
|||||||
@@ -109,4 +109,103 @@ namespace lms::db::tests
|
|||||||
EXPECT_TRUE(visited);
|
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
|
} // namespace lms::db::tests
|
||||||
@@ -15,10 +15,12 @@ add_executable(test-database
|
|||||||
RatedRelease.cpp
|
RatedRelease.cpp
|
||||||
RatedTrack.cpp
|
RatedTrack.cpp
|
||||||
Release.cpp
|
Release.cpp
|
||||||
|
ScanSettings.cpp
|
||||||
StarredArtist.cpp
|
StarredArtist.cpp
|
||||||
StarredRelease.cpp
|
StarredRelease.cpp
|
||||||
StarredTrack.cpp
|
StarredTrack.cpp
|
||||||
Track.cpp
|
Track.cpp
|
||||||
|
TrackArtistLink.cpp
|
||||||
TrackBookmark.cpp
|
TrackBookmark.cpp
|
||||||
TrackEmbeddedImage.cpp
|
TrackEmbeddedImage.cpp
|
||||||
TrackFeatures.cpp
|
TrackFeatures.cpp
|
||||||
|
|||||||
@@ -31,9 +31,11 @@
|
|||||||
#include "database/RatedArtist.hpp"
|
#include "database/RatedArtist.hpp"
|
||||||
#include "database/RatedRelease.hpp"
|
#include "database/RatedRelease.hpp"
|
||||||
#include "database/RatedTrack.hpp"
|
#include "database/RatedTrack.hpp"
|
||||||
|
#include "database/ScanSettings.hpp"
|
||||||
#include "database/StarredArtist.hpp"
|
#include "database/StarredArtist.hpp"
|
||||||
#include "database/StarredRelease.hpp"
|
#include "database/StarredRelease.hpp"
|
||||||
#include "database/StarredTrack.hpp"
|
#include "database/StarredTrack.hpp"
|
||||||
|
#include "database/TrackArtistLink.hpp"
|
||||||
#include "database/TrackEmbeddedImage.hpp"
|
#include "database/TrackEmbeddedImage.hpp"
|
||||||
#include "database/TrackEmbeddedImageLink.hpp"
|
#include "database/TrackEmbeddedImageLink.hpp"
|
||||||
#include "database/TrackLyrics.hpp"
|
#include "database/TrackLyrics.hpp"
|
||||||
@@ -361,10 +363,12 @@ VALUES
|
|||||||
EXPECT_FALSE(RatedTrack::find(session, RatedTrackId{}));
|
EXPECT_FALSE(RatedTrack::find(session, RatedTrackId{}));
|
||||||
EXPECT_FALSE(Release::find(session, ReleaseId{}));
|
EXPECT_FALSE(Release::find(session, ReleaseId{}));
|
||||||
EXPECT_FALSE(ReleaseType::find(session, ReleaseTypeId{}));
|
EXPECT_FALSE(ReleaseType::find(session, ReleaseTypeId{}));
|
||||||
|
EXPECT_FALSE(ScanSettings::find(session, ScanSettingsId{}));
|
||||||
EXPECT_FALSE(StarredArtist::find(session, StarredArtistId{}));
|
EXPECT_FALSE(StarredArtist::find(session, StarredArtistId{}));
|
||||||
EXPECT_FALSE(StarredRelease::find(session, StarredReleaseId{}));
|
EXPECT_FALSE(StarredRelease::find(session, StarredReleaseId{}));
|
||||||
EXPECT_FALSE(StarredTrack::find(session, StarredTrackId{}));
|
EXPECT_FALSE(StarredTrack::find(session, StarredTrackId{}));
|
||||||
EXPECT_FALSE(Track::find(session, TrackId{}));
|
EXPECT_FALSE(Track::find(session, TrackId{}));
|
||||||
|
EXPECT_FALSE(TrackArtistLink::find(session, TrackArtistLinkId{}));
|
||||||
EXPECT_FALSE(TrackList::find(session, TrackListId{}));
|
EXPECT_FALSE(TrackList::find(session, TrackListId{}));
|
||||||
EXPECT_FALSE(TrackLyrics::find(session, TrackLyricsId{}));
|
EXPECT_FALSE(TrackLyrics::find(session, TrackLyricsId{}));
|
||||||
EXPECT_FALSE(UIState::find(session, UIStateId{}));
|
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
|
target_sources(lmsimage PRIVATE
|
||||||
impl/stb/Image.cpp
|
impl/stb/Image.cpp
|
||||||
|
impl/stb/Exception.cpp
|
||||||
impl/stb/RawImage.cpp
|
impl/stb/RawImage.cpp
|
||||||
impl/stb/StbImage.cpp
|
impl/stb/StbImage.cpp
|
||||||
impl/stb/StbImageResize.cpp
|
impl/stb/StbImageResize.cpp
|
||||||
impl/stb/StbImageWrite.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_compile_options(lmsimage PRIVATE "-DSTB_IMAGE_RESIZE_VERSION=${STB_IMAGE_RESIZE_VERSION}")
|
||||||
target_include_directories(lmsimage PRIVATE ${STB_IMAGE_INCLUDE_DIR})
|
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 <array>
|
||||||
|
|
||||||
|
#include "Exception.hpp"
|
||||||
#include "StbImage.hpp"
|
#include "StbImage.hpp"
|
||||||
#include "StbImageWrite.hpp"
|
#include "StbImageWrite.hpp"
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
#include "RawImage.hpp"
|
#include "RawImage.hpp"
|
||||||
|
|
||||||
|
#include "Exception.hpp"
|
||||||
#include "StbImage.hpp"
|
#include "StbImage.hpp"
|
||||||
#include "StbImageResize.hpp"
|
#include "StbImageResize.hpp"
|
||||||
|
|
||||||
|
|||||||
@@ -19,17 +19,3 @@
|
|||||||
|
|
||||||
#define STB_IMAGE_IMPLEMENTATION
|
#define STB_IMAGE_IMPLEMENTATION
|
||||||
#include "StbImage.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
|
|
||||||
@@ -24,19 +24,3 @@
|
|||||||
#define STBI_FAILURE_USERMSG
|
#define STBI_FAILURE_USERMSG
|
||||||
|
|
||||||
#include <stb_image.h>
|
#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/>.
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
|
|
||||||
namespace lms::image
|
namespace lms::image
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
#include <boost/property_tree/xml_parser.hpp>
|
#include <boost/property_tree/xml_parser.hpp>
|
||||||
|
|
||||||
#include "core/ILogger.hpp"
|
#include "core/ILogger.hpp"
|
||||||
|
#include "core/String.hpp"
|
||||||
|
|
||||||
namespace lms::metadata
|
namespace lms::metadata
|
||||||
{
|
{
|
||||||
@@ -43,12 +44,12 @@ namespace lms::metadata
|
|||||||
|
|
||||||
const auto& artistNode{ root.get_child("artist") };
|
const auto& artistNode{ root.get_child("artist") };
|
||||||
|
|
||||||
artistInfo.mbid = core::UUID::fromString(artistNode.get_optional<std::string>("musicBrainzArtistID").value_or(""));
|
artistInfo.mbid = core::UUID::fromString(core::stringUtils::stringTrim(artistNode.get_optional<std::string>("musicBrainzArtistID").value_or("")));
|
||||||
artistInfo.name = artistNode.get_optional<std::string>("name").value_or("");
|
artistInfo.name = core::stringUtils::stringTrim(artistNode.get_optional<std::string>("name").value_or(""));
|
||||||
artistInfo.sortName = artistNode.get_optional<std::string>("sortname").value_or("");
|
artistInfo.sortName = core::stringUtils::stringTrim(artistNode.get_optional<std::string>("sortname").value_or(""));
|
||||||
artistInfo.type = artistNode.get_optional<std::string>("type").value_or("");
|
artistInfo.type = core::stringUtils::stringTrim(artistNode.get_optional<std::string>("type").value_or(""));
|
||||||
artistInfo.gender = artistNode.get_optional<std::string>("gender").value_or("");
|
artistInfo.gender = core::stringUtils::stringTrim(artistNode.get_optional<std::string>("gender").value_or(""));
|
||||||
artistInfo.disambiguation = artistNode.get_optional<std::string>("disambiguation").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("");
|
artistInfo.biography = artistNode.get_optional<std::string>("biography").value_or("");
|
||||||
|
|
||||||
return artistInfo;
|
return artistInfo;
|
||||||
|
|||||||
@@ -62,36 +62,41 @@ namespace lms::metadata
|
|||||||
}
|
}
|
||||||
|
|
||||||
template<typename T>
|
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;
|
std::vector<T> res;
|
||||||
|
|
||||||
for (const TagType tagType : tagTypes)
|
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) {
|
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))
|
if (value.find(tagDelimiter) != std::string_view::npos)
|
||||||
addTagIfNonEmpty(splitTag);
|
{
|
||||||
|
for (std::string_view splitTag : core::stringUtils::splitString(value, tagDelimiters))
|
||||||
|
addTagIfNonEmpty(res, splitTag);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// no delimiter found, or no delimiter to be used
|
// no delimiter found, or no delimiter to be used
|
||||||
addTagIfNonEmpty(value);
|
addTagIfNonEmpty(res, value);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.empty())
|
if (!res.empty())
|
||||||
@@ -153,11 +158,11 @@ namespace lms::metadata
|
|||||||
std::initializer_list<TagType> artistMBIDTagNames,
|
std::initializer_list<TagType> artistMBIDTagNames,
|
||||||
const AudioFileParserParameters& params)
|
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())
|
if (artistNames.empty())
|
||||||
return {};
|
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<core::UUID> artistMBIDs{ getTagValuesFirstMatchAs<core::UUID>(tagReader, artistMBIDTagNames, params.defaultTagDelimiters) };
|
||||||
|
|
||||||
std::vector<Artist> artists;
|
std::vector<Artist> artists;
|
||||||
@@ -238,6 +243,7 @@ namespace lms::metadata
|
|||||||
// Otherwise, we reconstruct the string using a standard, hardcoded, join
|
// Otherwise, we reconstruct the string using a standard, hardcoded, join
|
||||||
if (artistTag && strIsMatchingArtistNames(*artistTag, artistNames))
|
if (artistTag && strIsMatchingArtistNames(*artistTag, artistNames))
|
||||||
{
|
{
|
||||||
|
// Limitation: this test does not take the whitelist into account
|
||||||
if (!strIsContainingAny(*artistTag, artistTagDelimiters))
|
if (!strIsContainingAny(*artistTag, artistTagDelimiters))
|
||||||
artistDisplayName = *artistTag;
|
artistDisplayName = *artistTag;
|
||||||
}
|
}
|
||||||
@@ -267,54 +273,6 @@ namespace lms::metadata
|
|||||||
|
|
||||||
return std::nullopt;
|
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
|
} // namespace
|
||||||
|
|
||||||
std::unique_ptr<IAudioFileParser> createAudioFileParser(const AudioFileParserParameters& params)
|
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.remixerArtists = getArtists(tagReader, { TagType::Remixers, TagType::Remixer }, { TagType::RemixersSortOrder, TagType::RemixerSortOrder }, {}, _params);
|
||||||
track.performerArtists = getPerformerArtists(tagReader); // artistDelimiters not supported
|
track.performerArtists = getPerformerArtists(tagReader); // artistDelimiters not supported
|
||||||
|
|
||||||
fillMissingMbids(track);
|
|
||||||
|
|
||||||
// If a file has originalDate but no originalYear, set it
|
// If a file has originalDate but no originalYear, set it
|
||||||
if (!track.originalYear)
|
if (!track.originalYear)
|
||||||
track.originalYear = track.originalDate.getYear();
|
track.originalYear = track.originalDate.getYear();
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ namespace lms::metadata
|
|||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
// Mapping to internal avformat names and/or common alternative custom names
|
// 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::AcoustID, { "ACOUSTID_ID", "ACOUSTID ID" } },
|
||||||
{ TagType::Advisory, { "ITUNESADVISORY" } },
|
{ TagType::Advisory, { "ITUNESADVISORY" } },
|
||||||
{ TagType::Album, { "ALBUM", "TALB", "WM/ALBUMTITLE" } },
|
{ TagType::Album, { "ALBUM", "TALB", "WM/ALBUMTITLE" } },
|
||||||
@@ -171,8 +171,8 @@ namespace lms::metadata
|
|||||||
|
|
||||||
void AvFormatTagReader::visitTagValues(TagType tag, TagValueVisitor visitor) const
|
void AvFormatTagReader::visitTagValues(TagType tag, TagValueVisitor visitor) const
|
||||||
{
|
{
|
||||||
auto itTagNames{ tagMapping.find(tag) };
|
auto itTagNames{ avFormatTagMapping.find(tag) };
|
||||||
if (itTagNames == std::cend(tagMapping))
|
if (itTagNames == std::cend(avFormatTagMapping))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
for (const std::string& tagName : itTagNames->second)
|
for (const std::string& tagName : itTagNames->second)
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ namespace lms::metadata
|
|||||||
{
|
{
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
class ParsingFailedException : public Exception
|
class ImageParsingFailedException : public Exception
|
||||||
{
|
{
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -362,7 +362,7 @@ namespace lms::metadata
|
|||||||
if (_file.isNull())
|
if (_file.isNull())
|
||||||
{
|
{
|
||||||
LMS_LOG(METADATA, ERROR, "File " << p << ": parsing failed");
|
LMS_LOG(METADATA, ERROR, "File " << p << ": parsing failed");
|
||||||
throw ParsingFailedException{};
|
throw ImageParsingFailedException{};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,12 +55,12 @@ namespace lms::metadata
|
|||||||
{
|
{
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
class ParsingFailedException : public Exception
|
class TagParsingFailedException : public Exception
|
||||||
{
|
{
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mapping to internal taglib names and/or common alternative custom names
|
// 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::AcoustID, { "ACOUSTID_ID", "ACOUSTID ID" } },
|
||||||
{ TagType::Advisory, { "ITUNESADVISORY" } },
|
{ TagType::Advisory, { "ITUNESADVISORY" } },
|
||||||
{ TagType::Album, { "ALBUM" } },
|
{ TagType::Album, { "ALBUM" } },
|
||||||
@@ -228,13 +228,13 @@ namespace lms::metadata
|
|||||||
if (_file.isNull())
|
if (_file.isNull())
|
||||||
{
|
{
|
||||||
LMS_LOG(METADATA, ERROR, "File " << p << ": parsing failed");
|
LMS_LOG(METADATA, ERROR, "File " << p << ": parsing failed");
|
||||||
throw ParsingFailedException{};
|
throw TagParsingFailedException{};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_file.audioProperties())
|
if (!_file.audioProperties())
|
||||||
{
|
{
|
||||||
LMS_LOG(METADATA, ERROR, "File " << p << ": no audio properties");
|
LMS_LOG(METADATA, ERROR, "File " << p << ": no audio properties");
|
||||||
throw ParsingFailedException{};
|
throw TagParsingFailedException{};
|
||||||
}
|
}
|
||||||
|
|
||||||
computeAudioProperties();
|
computeAudioProperties();
|
||||||
@@ -465,8 +465,8 @@ namespace lms::metadata
|
|||||||
|
|
||||||
void TagLibTagReader::visitTagValues(TagType tag, TagValueVisitor visitor) const
|
void TagLibTagReader::visitTagValues(TagType tag, TagValueVisitor visitor) const
|
||||||
{
|
{
|
||||||
auto itTagNames{ tagMapping.find(tag) };
|
auto itTagNames{ tagLibTagMapping.find(tag) };
|
||||||
if (itTagNames == std::cend(tagMapping))
|
if (itTagNames == std::cend(tagLibTagMapping))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
for (const std::string& tagName : itTagNames->second)
|
for (const std::string& tagName : itTagNames->second)
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
#include <span>
|
#include <span>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
#include <unordered_set>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "core/PartialDateTime.hpp"
|
#include "core/PartialDateTime.hpp"
|
||||||
@@ -213,11 +214,28 @@ namespace lms::metadata
|
|||||||
Accurate,
|
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
|
struct AudioFileParserParameters
|
||||||
{
|
{
|
||||||
ParserBackend backend{ ParserBackend::TagLib };
|
ParserBackend backend{ ParserBackend::TagLib };
|
||||||
ParserReadStyle readStyle{ ParserReadStyle::Average };
|
ParserReadStyle readStyle{ ParserReadStyle::Average };
|
||||||
std::vector<std::string> artistTagDelimiters;
|
std::vector<std::string> artistTagDelimiters;
|
||||||
|
WhiteList artistsToNotSplit;
|
||||||
std::vector<std::string> defaultTagDelimiters;
|
std::vector<std::string> defaultTagDelimiters;
|
||||||
std::vector<std::string> userExtraTags;
|
std::vector<std::string> userExtraTags;
|
||||||
bool debug{};
|
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.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.");
|
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
|
} // namespace lms::metadata::tests
|
||||||
@@ -263,9 +263,9 @@ namespace lms::metadata
|
|||||||
const TestTagReader testTags{
|
const TestTagReader testTags{
|
||||||
{
|
{
|
||||||
{ TagType::Album, { "MyAlbum" } },
|
{ TagType::Album, { "MyAlbum" } },
|
||||||
{ TagType::AlbumArtist, { "AlbumArtist1 / AlbumArtist2" } },
|
{ TagType::AlbumArtist, { "AlbumArtist1 / AlbumArtist2" } },
|
||||||
{ TagType::Artist, { " Artist1 / Artist2 feat. Artist3 " } },
|
{ TagType::Artist, { " Artist1 / Artist2 feat. Artist3 " } },
|
||||||
{ TagType::Genre, { "Genre1 ; Genre2" } },
|
{ TagType::Genre, { "Genre1 ; Genre2" } },
|
||||||
{ TagType::Language, { " Lang1/Lang2 / Lang3" } },
|
{ TagType::Language, { " Lang1/Lang2 / Lang3" } },
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -295,11 +295,60 @@ namespace lms::metadata
|
|||||||
// Release
|
// Release
|
||||||
ASSERT_TRUE(track->medium->release.has_value());
|
ASSERT_TRUE(track->medium->release.has_value());
|
||||||
EXPECT_EQ(track->medium->release->name, "MyAlbum");
|
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[0].name, "AlbumArtist1");
|
||||||
EXPECT_EQ(track->medium->release->artists[1].name, "AlbumArtist2");
|
EXPECT_EQ(track->medium->release->artists[1].name, "AlbumArtist2");
|
||||||
EXPECT_EQ(track->medium->release->artistDisplayName, "AlbumArtist1, 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)
|
TEST(AudioFileParser, customDelimiters_foundInArtist)
|
||||||
{
|
{
|
||||||
const TestTagReader testTags{
|
const TestTagReader testTags{
|
||||||
@@ -605,131 +654,6 @@ namespace lms::metadata
|
|||||||
EXPECT_EQ(track->artistDisplayName, "Artist1, Artist2"); // reconstruct the artist display name
|
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)
|
TEST(AudioFileParser, release_sortNameFallback)
|
||||||
{
|
{
|
||||||
const TestTagReader testTags{
|
const TestTagReader testTags{
|
||||||
|
|||||||
@@ -20,6 +20,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <cassert>
|
||||||
|
#include <memory>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <shared_mutex>
|
#include <shared_mutex>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
@@ -27,7 +29,6 @@
|
|||||||
|
|
||||||
#include "database/ImageId.hpp"
|
#include "database/ImageId.hpp"
|
||||||
#include "database/TrackEmbeddedImageId.hpp"
|
#include "database/TrackEmbeddedImageId.hpp"
|
||||||
#include "database/TrackId.hpp"
|
|
||||||
#include "image/IEncodedImage.hpp"
|
#include "image/IEncodedImage.hpp"
|
||||||
|
|
||||||
namespace lms::cover
|
namespace lms::cover
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
|
||||||
#include "database/UserId.hpp"
|
#include "database/UserId.hpp"
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
#include "database/StarredArtistId.hpp"
|
#include "database/StarredArtistId.hpp"
|
||||||
#include "database/StarredReleaseId.hpp"
|
#include "database/StarredReleaseId.hpp"
|
||||||
#include "database/StarredTrackId.hpp"
|
#include "database/StarredTrackId.hpp"
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ namespace lms::recommendation
|
|||||||
{
|
{
|
||||||
auto transaction{ session.createReadTransaction() };
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
return db::ScanSettings::get(session)->getSimilarityEngineType();
|
return db::ScanSettings::find(session)->getSimilarityEngineType();
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
add_library(lmsscanner STATIC
|
add_library(lmsscanner STATIC
|
||||||
|
impl/helpers/ArtistHelpers.cpp
|
||||||
impl/scanners/ArtistInfoFileScanner.cpp
|
impl/scanners/ArtistInfoFileScanner.cpp
|
||||||
|
impl/scanners/AudioFileScanOperation.cpp
|
||||||
impl/scanners/AudioFileScanner.cpp
|
impl/scanners/AudioFileScanner.cpp
|
||||||
impl/scanners/ImageFileScanner.cpp
|
impl/scanners/ImageFileScanner.cpp
|
||||||
impl/scanners/LyricsFileScanner.cpp
|
impl/scanners/LyricsFileScanner.cpp
|
||||||
impl/scanners/PlayListFileScanner.cpp
|
impl/scanners/PlayListFileScanner.cpp
|
||||||
impl/scanners/Utils.cpp
|
impl/scanners/Utils.cpp
|
||||||
impl/steps/FileScanQueue.cpp
|
impl/steps/FileScanQueue.cpp
|
||||||
|
impl/steps/ScanStepArtistReconciliation.cpp
|
||||||
impl/steps/ScanStepAssociateArtistImages.cpp
|
impl/steps/ScanStepAssociateArtistImages.cpp
|
||||||
impl/steps/ScanStepAssociateExternalLyrics.cpp
|
impl/steps/ScanStepAssociateExternalLyrics.cpp
|
||||||
impl/steps/ScanStepAssociatePlayListTracks.cpp
|
impl/steps/ScanStepAssociatePlayListTracks.cpp
|
||||||
impl/steps/ScanStepAssociateReleaseImages.cpp
|
impl/steps/ScanStepAssociateReleaseImages.cpp
|
||||||
|
impl/steps/ScanStepBase.cpp
|
||||||
impl/steps/ScanStepCheckForDuplicatedFiles.cpp
|
impl/steps/ScanStepCheckForDuplicatedFiles.cpp
|
||||||
impl/steps/ScanStepCheckForRemovedFiles.cpp
|
impl/steps/ScanStepCheckForRemovedFiles.cpp
|
||||||
impl/steps/ScanStepCompact.cpp
|
impl/steps/ScanStepCompact.cpp
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
#include "core/ITraceLogger.hpp"
|
#include "core/ITraceLogger.hpp"
|
||||||
#include "database/MediaLibrary.hpp"
|
#include "database/MediaLibrary.hpp"
|
||||||
#include "database/ScanSettings.hpp"
|
#include "database/ScanSettings.hpp"
|
||||||
|
#include "database/Session.hpp"
|
||||||
|
|
||||||
#include "scanners/ArtistInfoFileScanner.hpp"
|
#include "scanners/ArtistInfoFileScanner.hpp"
|
||||||
#include "scanners/AudioFileScanner.hpp"
|
#include "scanners/AudioFileScanner.hpp"
|
||||||
@@ -35,6 +36,7 @@
|
|||||||
#include "scanners/LyricsFileScanner.hpp"
|
#include "scanners/LyricsFileScanner.hpp"
|
||||||
#include "scanners/PlayListFileScanner.hpp"
|
#include "scanners/PlayListFileScanner.hpp"
|
||||||
|
|
||||||
|
#include "steps/ScanStepArtistReconciliation.hpp"
|
||||||
#include "steps/ScanStepAssociateArtistImages.hpp"
|
#include "steps/ScanStepAssociateArtistImages.hpp"
|
||||||
#include "steps/ScanStepAssociateExternalLyrics.hpp"
|
#include "steps/ScanStepAssociateExternalLyrics.hpp"
|
||||||
#include "steps/ScanStepAssociatePlayListTracks.hpp"
|
#include "steps/ScanStepAssociatePlayListTracks.hpp"
|
||||||
@@ -55,6 +57,9 @@ namespace lms::scanner
|
|||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
|
static constexpr std::string_view currentSettingsName{ "" };
|
||||||
|
static constexpr std::string_view lastScanSettingsName{ "last_scan" };
|
||||||
|
|
||||||
Wt::WDate getNextMonday(Wt::WDate current)
|
Wt::WDate getNextMonday(Wt::WDate current)
|
||||||
{
|
{
|
||||||
do
|
do
|
||||||
@@ -74,6 +79,61 @@ namespace lms::scanner
|
|||||||
|
|
||||||
return current;
|
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
|
} // namespace
|
||||||
|
|
||||||
std::unique_ptr<IScannerService> createScannerService(Db& db)
|
std::unique_ptr<IScannerService> createScannerService(Db& db)
|
||||||
@@ -282,26 +342,7 @@ namespace lms::scanner
|
|||||||
ScanStats& stats{ scanContext.stats };
|
ScanStats& stats{ scanContext.stats };
|
||||||
stats.startTime = Wt::WDateTime::currentDateTime();
|
stats.startTime = Wt::WDateTime::currentDateTime();
|
||||||
|
|
||||||
std::size_t stepIndex{};
|
processScanSteps(scanContext);
|
||||||
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() << "'");
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
{
|
||||||
std::unique_lock lock{ _statusMutex };
|
std::unique_lock lock{ _statusMutex };
|
||||||
@@ -321,6 +362,10 @@ namespace lms::scanner
|
|||||||
_lastCompleteScanStats = stats;
|
_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!");
|
LMS_LOG(DBUPDATER, DEBUG, "Scan not aborted, scheduling next scan!");
|
||||||
scheduleNextScan();
|
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()
|
void ScannerService::refreshScanSettings()
|
||||||
{
|
{
|
||||||
ScannerSettings newSettings{ readSettings() };
|
std::optional<ScannerSettings> newSettings{ readScannerSettings(_db.getTLSSession(), currentSettingsName) };
|
||||||
if (_settings == newSettings)
|
assert(newSettings.has_value());
|
||||||
|
if (_settings == *newSettings)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Scanner settings updated");
|
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) {
|
auto cbFunc{ [this](const ScanStepStats& stats) {
|
||||||
notifyInProgressIfNeeded(stats);
|
notifyInProgressIfNeeded(stats);
|
||||||
} };
|
} };
|
||||||
|
|
||||||
_fileScanners.clear();
|
_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<AudioFileScanner>(_db, _settings));
|
||||||
_fileScanners.emplace_back(std::make_unique<ImageFileScanner>(_db));
|
_fileScanners.emplace_back(std::make_unique<ImageFileScanner>(_db));
|
||||||
_fileScanners.emplace_back(std::make_unique<LyricsFileScanner>(_db));
|
_fileScanners.emplace_back(std::make_unique<LyricsFileScanner>(_db));
|
||||||
@@ -359,6 +442,7 @@ namespace lms::scanner
|
|||||||
|
|
||||||
ScanStepBase::InitParams params{
|
ScanStepBase::InitParams params{
|
||||||
.settings = _settings,
|
.settings = _settings,
|
||||||
|
.lastScanSettings = _lastScanSettings.has_value() ? &(_lastScanSettings.value()) : nullptr,
|
||||||
.progressCallback = cbFunc,
|
.progressCallback = cbFunc,
|
||||||
.abortScan = _abortScan,
|
.abortScan = _abortScan,
|
||||||
.db = _db,
|
.db = _db,
|
||||||
@@ -370,6 +454,7 @@ namespace lms::scanner
|
|||||||
_scanSteps.emplace_back(std::make_unique<ScanStepDiscoverFiles>(params));
|
_scanSteps.emplace_back(std::make_unique<ScanStepDiscoverFiles>(params));
|
||||||
_scanSteps.emplace_back(std::make_unique<ScanStepScanFiles>(params));
|
_scanSteps.emplace_back(std::make_unique<ScanStepScanFiles>(params));
|
||||||
_scanSteps.emplace_back(std::make_unique<ScanStepCheckForRemovedFiles>(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<ScanStepAssociatePlayListTracks>(params));
|
||||||
_scanSteps.emplace_back(std::make_unique<ScanStepUpdateLibraryFields>(params));
|
_scanSteps.emplace_back(std::make_unique<ScanStepUpdateLibraryFields>(params));
|
||||||
_scanSteps.emplace_back(std::make_unique<ScanStepAssociateArtistImages>(params));
|
_scanSteps.emplace_back(std::make_unique<ScanStepAssociateArtistImages>(params));
|
||||||
@@ -382,43 +467,6 @@ namespace lms::scanner
|
|||||||
_scanSteps.emplace_back(std::make_unique<ScanStepCheckForDuplicatedFiles>(params));
|
_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)
|
void ScannerService::notifyInProgress(const ScanStepStats& stepStats)
|
||||||
{
|
{
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ namespace lms::scanner
|
|||||||
{
|
{
|
||||||
class IFileScanner;
|
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
|
class ScannerService : public IScannerService
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -65,12 +68,12 @@ namespace lms::scanner
|
|||||||
|
|
||||||
// Update database (scheduled callback)
|
// Update database (scheduled callback)
|
||||||
void scan(const ScanOptions& scanOptions);
|
void scan(const ScanOptions& scanOptions);
|
||||||
|
void processScanSteps(ScanContext& context);
|
||||||
|
|
||||||
void scanMediaDirectory(const std::filesystem::path& mediaDirectory, bool forceScan, ScanStats& stats);
|
void scanMediaDirectory(const std::filesystem::path& mediaDirectory, bool forceScan, ScanStats& stats);
|
||||||
|
|
||||||
// Helpers
|
// Helpers
|
||||||
void refreshScanSettings();
|
void refreshScanSettings();
|
||||||
ScannerSettings readSettings();
|
|
||||||
|
|
||||||
void notifyInProgressIfNeeded(const ScanStepStats& stats);
|
void notifyInProgressIfNeeded(const ScanStepStats& stats);
|
||||||
void notifyInProgress(const ScanStepStats& stats);
|
void notifyInProgress(const ScanStepStats& stats);
|
||||||
@@ -93,5 +96,6 @@ namespace lms::scanner
|
|||||||
Wt::WDateTime _nextScheduledScan;
|
Wt::WDateTime _nextScheduledScan;
|
||||||
|
|
||||||
ScannerSettings _settings;
|
ScannerSettings _settings;
|
||||||
|
std::optional<ScannerSettings> _lastScanSettings;
|
||||||
};
|
};
|
||||||
} // namespace lms::scanner
|
} // namespace lms::scanner
|
||||||
|
|||||||
@@ -35,14 +35,16 @@ namespace lms::scanner
|
|||||||
|
|
||||||
struct ScannerSettings
|
struct ScannerSettings
|
||||||
{
|
{
|
||||||
std::size_t scanVersion{};
|
std::size_t audioScanVersion{};
|
||||||
Wt::WTime startTime;
|
Wt::WTime startTime;
|
||||||
db::ScanSettings::UpdatePeriod updatePeriod{ db::ScanSettings::UpdatePeriod::Never };
|
db::ScanSettings::UpdatePeriod updatePeriod{ db::ScanSettings::UpdatePeriod::Never };
|
||||||
bool skipDuplicateMBID{};
|
bool skipDuplicateTrackMBID{};
|
||||||
std::vector<std::string> extraTags;
|
std::vector<std::string> extraTags;
|
||||||
std::vector<std::string> artistTagDelimiters;
|
std::vector<std::string> artistTagDelimiters;
|
||||||
|
std::vector<std::string> artistsToNotSplit;
|
||||||
std::vector<std::string> defaultTagDelimiters;
|
std::vector<std::string> defaultTagDelimiters;
|
||||||
bool skipSingleReleasePlayLists{};
|
bool skipSingleReleasePlayLists{};
|
||||||
|
bool allowArtistMBIDFallback{ true }; // TODO false?
|
||||||
|
|
||||||
std::vector<MediaLibraryInfo> mediaLibraries;
|
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 "IFileScanOperation.hpp"
|
||||||
#include "ScanContext.hpp"
|
#include "ScanContext.hpp"
|
||||||
|
#include "ScannerSettings.hpp"
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
|
#include "helpers/ArtistHelpers.hpp"
|
||||||
|
#include "metadata/Types.hpp"
|
||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
{
|
{
|
||||||
@@ -41,10 +44,13 @@ namespace lms::scanner
|
|||||||
class ArtistInfoFileScanOperation : public IFileScanOperation
|
class ArtistInfoFileScanOperation : public IFileScanOperation
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
ArtistInfoFileScanOperation(const FileToScan& file, db::Db& db)
|
ArtistInfoFileScanOperation(const FileToScan& file, const ScannerSettings& settings, db::Db& db)
|
||||||
: _file{ file.file }
|
: _file{ file.file }
|
||||||
, _mediaLibrary{ file.mediaLibrary }
|
, _mediaLibrary{ file.mediaLibrary }
|
||||||
, _db{ db } {}
|
, _settings{ settings }
|
||||||
|
, _db{ db }
|
||||||
|
{
|
||||||
|
}
|
||||||
~ArtistInfoFileScanOperation() override = default;
|
~ArtistInfoFileScanOperation() override = default;
|
||||||
ArtistInfoFileScanOperation(const ArtistInfoFileScanOperation&) = delete;
|
ArtistInfoFileScanOperation(const ArtistInfoFileScanOperation&) = delete;
|
||||||
ArtistInfoFileScanOperation& operator=(const ArtistInfoFileScanOperation&) = delete;
|
ArtistInfoFileScanOperation& operator=(const ArtistInfoFileScanOperation&) = delete;
|
||||||
@@ -59,6 +65,7 @@ namespace lms::scanner
|
|||||||
|
|
||||||
const std::filesystem::path _file;
|
const std::filesystem::path _file;
|
||||||
const MediaLibraryInfo _mediaLibrary;
|
const MediaLibraryInfo _mediaLibrary;
|
||||||
|
const ScannerSettings& _settings;
|
||||||
db::Db& _db;
|
db::Db& _db;
|
||||||
|
|
||||||
std::optional<metadata::ArtistInfo> _parsedArtistInfo;
|
std::optional<metadata::ArtistInfo> _parsedArtistInfo;
|
||||||
@@ -76,12 +83,7 @@ namespace lms::scanner
|
|||||||
}
|
}
|
||||||
|
|
||||||
_parsedArtistInfo = metadata::parseArtistInfo(ifs);
|
_parsedArtistInfo = metadata::parseArtistInfo(ifs);
|
||||||
if (!_parsedArtistInfo->mbid.has_value())
|
if (_parsedArtistInfo->name.empty())
|
||||||
{
|
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Discarding artist info in file " << _file << ": no mbid set");
|
|
||||||
_parsedArtistInfo.reset();
|
|
||||||
}
|
|
||||||
else if (_parsedArtistInfo->name.empty())
|
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, DEBUG, "Discarding artist info in file " << _file << ": no name set");
|
LMS_LOG(DBUPDATER, DEBUG, "Discarding artist info in file " << _file << ": no name set");
|
||||||
_parsedArtistInfo.reset();
|
_parsedArtistInfo.reset();
|
||||||
@@ -125,6 +127,8 @@ namespace lms::scanner
|
|||||||
artistInfo.modify()->setAbsoluteFilePath(_file);
|
artistInfo.modify()->setAbsoluteFilePath(_file);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
artistInfo.modify()->setName(_parsedArtistInfo->name);
|
||||||
|
artistInfo.modify()->setSortName(_parsedArtistInfo->sortName);
|
||||||
artistInfo.modify()->setLastWriteTime(fileInfo->lastWriteTime);
|
artistInfo.modify()->setLastWriteTime(fileInfo->lastWriteTime);
|
||||||
artistInfo.modify()->setType(_parsedArtistInfo->type);
|
artistInfo.modify()->setType(_parsedArtistInfo->type);
|
||||||
artistInfo.modify()->setGender(_parsedArtistInfo->gender);
|
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
|
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));
|
artistInfo.modify()->setDirectory(utils::getOrCreateDirectory(dbSession, _file.parent_path(), mediaLibrary));
|
||||||
|
|
||||||
db::Artist::pointer artist{ db::Artist::find(dbSession, *_parsedArtistInfo->mbid) };
|
const metadata::Artist artistMetadata{ _parsedArtistInfo->mbid, _parsedArtistInfo->name, _parsedArtistInfo->sortName.empty() ? std::nullopt : std::make_optional<std::string>(_parsedArtistInfo->sortName) };
|
||||||
if (!artist)
|
db::Artist::pointer artist{ helpers::getOrCreateArtist(dbSession, artistMetadata, helpers::AllowFallbackOnMBIDEntry{ _settings.allowArtistMBIDFallback }) };
|
||||||
artist = dbSession.create<db::Artist>(_parsedArtistInfo->name, _parsedArtistInfo->mbid);
|
|
||||||
|
|
||||||
artist.modify()->setName(_parsedArtistInfo->name);
|
|
||||||
artist.modify()->setSortName(_parsedArtistInfo->sortName);
|
|
||||||
artistInfo.modify()->setArtist(artist);
|
artistInfo.modify()->setArtist(artist);
|
||||||
|
|
||||||
if (added)
|
if (added)
|
||||||
@@ -155,8 +155,9 @@ namespace lms::scanner
|
|||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
ArtistInfoFileScanner::ArtistInfoFileScanner(db::Db& db)
|
ArtistInfoFileScanner::ArtistInfoFileScanner(const ScannerSettings& settings, db::Db& db)
|
||||||
: _db{ db }
|
: _settings{ settings }
|
||||||
|
, _db{ db }
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,6 +203,6 @@ namespace lms::scanner
|
|||||||
|
|
||||||
std::unique_ptr<IFileScanOperation> ArtistInfoFileScanner::createScanOperation(const FileToScan& fileToScan) const
|
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
|
} // namespace lms::scanner
|
||||||
@@ -31,10 +31,12 @@ namespace lms
|
|||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
{
|
{
|
||||||
|
struct ScannerSettings;
|
||||||
|
|
||||||
class ArtistInfoFileScanner : public IFileScanner
|
class ArtistInfoFileScanner : public IFileScanner
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
ArtistInfoFileScanner(db::Db& db);
|
ArtistInfoFileScanner(const ScannerSettings& _settings, db::Db& db);
|
||||||
~ArtistInfoFileScanner() override = default;
|
~ArtistInfoFileScanner() override = default;
|
||||||
ArtistInfoFileScanner(const ArtistInfoFileScanner&) = delete;
|
ArtistInfoFileScanner(const ArtistInfoFileScanner&) = delete;
|
||||||
ArtistInfoFileScanner& operator=(const ArtistInfoFileScanner&) = delete;
|
ArtistInfoFileScanner& operator=(const ArtistInfoFileScanner&) = delete;
|
||||||
@@ -45,6 +47,7 @@ namespace lms::scanner
|
|||||||
bool needsScan(ScanContext& context, const FileToScan& file) const override;
|
bool needsScan(ScanContext& context, const FileToScan& file) const override;
|
||||||
std::unique_ptr<IFileScanOperation> createScanOperation(const FileToScan& fileToScan) const override;
|
std::unique_ptr<IFileScanOperation> createScanOperation(const FileToScan& fileToScan) const override;
|
||||||
|
|
||||||
|
const ScannerSettings& _settings;
|
||||||
db::Db& _db;
|
db::Db& _db;
|
||||||
};
|
};
|
||||||
} // namespace lms::scanner
|
} // 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 "AudioFileScanner.hpp"
|
||||||
|
|
||||||
#include "core/IConfig.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/Service.hpp"
|
||||||
#include "core/XxHash3.hpp"
|
|
||||||
#include "database/Artist.hpp"
|
|
||||||
#include "database/Cluster.hpp"
|
|
||||||
#include "database/Db.hpp"
|
#include "database/Db.hpp"
|
||||||
#include "database/Directory.hpp"
|
|
||||||
#include "database/MediaLibrary.hpp"
|
#include "database/MediaLibrary.hpp"
|
||||||
#include "database/Release.hpp"
|
|
||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
#include "database/Track.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 "ScanContext.hpp"
|
||||||
#include "ScannerSettings.hpp"
|
#include "ScannerSettings.hpp"
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
#include "metadata/Types.hpp"
|
|
||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
{
|
{
|
||||||
namespace
|
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()
|
metadata::ParserReadStyle getParserReadStyle()
|
||||||
{
|
{
|
||||||
std::string_view readStyle{ core::Service<core::IConfig>::get()->getString("scanner-parser-read-style", "average") };
|
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.userExtraTags = settings.extraTags;
|
||||||
params.artistTagDelimiters = settings.artistTagDelimiters;
|
params.artistTagDelimiters = settings.artistTagDelimiters;
|
||||||
params.defaultTagDelimiters = settings.defaultTagDelimiters;
|
params.defaultTagDelimiters = settings.defaultTagDelimiters;
|
||||||
|
params.artistsToNotSplit.insert(settings.artistsToNotSplit.cbegin(), settings.artistsToNotSplit.end());
|
||||||
params.backend = metadata::ParserBackend::TagLib;
|
params.backend = metadata::ParserBackend::TagLib;
|
||||||
params.readStyle = getParserReadStyle();
|
params.readStyle = getParserReadStyle();
|
||||||
|
|
||||||
@@ -845,7 +63,6 @@ namespace lms::scanner
|
|||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
AudioFileScanner::AudioFileScanner(db::Db& db, const ScannerSettings& settings)
|
AudioFileScanner::AudioFileScanner(db::Db& db, const ScannerSettings& settings)
|
||||||
: _db{ db }
|
: _db{ db }
|
||||||
, _settings{ settings }
|
, _settings{ settings }
|
||||||
@@ -890,7 +107,7 @@ namespace lms::scanner
|
|||||||
const db::Track::pointer track{ db::Track::findByPath(dbSession, file.file) };
|
const db::Track::pointer track{ db::Track::findByPath(dbSession, file.file) };
|
||||||
if (track
|
if (track
|
||||||
&& track->getLastWriteTime() == lastWriteTime
|
&& 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
|
// 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() };
|
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 "core/LiteralString.hpp"
|
||||||
|
|
||||||
#include "MediaLibraryInfo.hpp"
|
#include "FileToScan.hpp"
|
||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
{
|
{
|
||||||
@@ -32,12 +32,6 @@ namespace lms::scanner
|
|||||||
struct ScanContext;
|
struct ScanContext;
|
||||||
struct ScannerSettings;
|
struct ScannerSettings;
|
||||||
|
|
||||||
struct FileToScan
|
|
||||||
{
|
|
||||||
std::filesystem::path file;
|
|
||||||
MediaLibraryInfo mediaLibrary;
|
|
||||||
};
|
|
||||||
|
|
||||||
class IFileScanner
|
class IFileScanner
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ namespace lms::scanner
|
|||||||
|
|
||||||
virtual ScanStep getStep() const = 0;
|
virtual ScanStep getStep() const = 0;
|
||||||
virtual core::LiteralString getStepName() const = 0;
|
virtual core::LiteralString getStepName() const = 0;
|
||||||
|
virtual bool needProcess(const ScanContext& context) const = 0;
|
||||||
virtual void process(ScanContext& context) = 0;
|
virtual void process(ScanContext& context) = 0;
|
||||||
};
|
};
|
||||||
} // namespace lms::scanner
|
} // 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>;
|
using ArtistImageAssociationContainer = std::deque<ArtistImageAssociation>;
|
||||||
|
|
||||||
struct SearchImageContext
|
struct SearchArtistImageContext
|
||||||
{
|
{
|
||||||
db::Session& session;
|
db::Session& session;
|
||||||
db::ArtistId lastRetrievedArtistId;
|
db::ArtistId lastRetrievedArtistId;
|
||||||
@@ -59,7 +59,7 @@ namespace lms::scanner
|
|||||||
std::span<const std::string> artistFileNames;
|
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;
|
db::Image::pointer image;
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ namespace lms::scanner
|
|||||||
return image;
|
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;
|
db::Image::pointer image;
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ namespace lms::scanner
|
|||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
db::Image::pointer searchImageInArtistInfoDirectory(SearchImageContext& searchContext, db::ArtistId artistId)
|
db::Image::pointer searchImageInArtistInfoDirectory(SearchArtistImageContext& searchContext, db::ArtistId artistId)
|
||||||
{
|
{
|
||||||
db::Image::pointer image;
|
db::Image::pointer image;
|
||||||
|
|
||||||
@@ -116,7 +116,7 @@ namespace lms::scanner
|
|||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
db::Image::pointer searchImageInDirectories(SearchImageContext& searchContext, db::ArtistId artistId)
|
db::Image::pointer searchImageInDirectories(SearchArtistImageContext& searchContext, db::ArtistId artistId)
|
||||||
{
|
{
|
||||||
db::Image::pointer image;
|
db::Image::pointer image;
|
||||||
|
|
||||||
@@ -169,7 +169,7 @@ namespace lms::scanner
|
|||||||
return image;
|
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;
|
db::Image::pointer image;
|
||||||
|
|
||||||
@@ -185,7 +185,7 @@ namespace lms::scanner
|
|||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool fetchNextArtistImagesToUpdate(SearchImageContext& searchContext, ArtistImageAssociationContainer& artistImageAssociations)
|
bool fetchNextArtistImagesToUpdate(SearchArtistImageContext& searchContext, ArtistImageAssociationContainer& artistImageAssociations)
|
||||||
{
|
{
|
||||||
const db::ArtistId artistId{ searchContext.lastRetrievedArtistId };
|
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)
|
void ScanStepAssociateArtistImages::process(ScanContext& context)
|
||||||
{
|
{
|
||||||
if (_abortScan)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (context.stats.nbChanges() == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
auto& session{ _db.getTLSSession() };
|
auto& session{ _db.getTLSSession() };
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -269,7 +271,7 @@ namespace lms::scanner
|
|||||||
context.currentStepStats.totalElems = db::Artist::getCount(session);
|
context.currentStepStats.totalElems = db::Artist::getCount(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
SearchImageContext searchContext{
|
SearchArtistImageContext searchContext{
|
||||||
.session = session,
|
.session = session,
|
||||||
.lastRetrievedArtistId = {},
|
.lastRetrievedArtistId = {},
|
||||||
.artistFileNames = _artistFileNames,
|
.artistFileNames = _artistFileNames,
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ namespace lms::scanner
|
|||||||
private:
|
private:
|
||||||
ScanStep getStep() const override { return ScanStep::AssociateArtistImages; }
|
ScanStep getStep() const override { return ScanStep::AssociateArtistImages; }
|
||||||
core::LiteralString getStepName() const override { return "Associate artist images"; }
|
core::LiteralString getStepName() const override { return "Associate artist images"; }
|
||||||
|
bool needProcess(const ScanContext& context) const override;
|
||||||
void process(ScanContext& context) override;
|
void process(ScanContext& context) override;
|
||||||
|
|
||||||
const std::vector<std::string> _artistFileNames;
|
const std::vector<std::string> _artistFileNames;
|
||||||
|
|||||||
@@ -32,9 +32,6 @@ namespace lms::scanner
|
|||||||
{
|
{
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
constexpr std::size_t readBatchSize{ 100 };
|
|
||||||
constexpr std::size_t writeBatchSize{ 20 };
|
|
||||||
|
|
||||||
struct TrackLyricsAssociation
|
struct TrackLyricsAssociation
|
||||||
{
|
{
|
||||||
db::TrackLyricsId trackLyricsId;
|
db::TrackLyricsId trackLyricsId;
|
||||||
@@ -83,6 +80,8 @@ namespace lms::scanner
|
|||||||
|
|
||||||
bool fetchNextTrackLyricsToUpdate(SearchTrackLyricsContext& searchContext, TrackLyricsAssociationContainer& trackLyricsAssociations)
|
bool fetchNextTrackLyricsToUpdate(SearchTrackLyricsContext& searchContext, TrackLyricsAssociationContainer& trackLyricsAssociations)
|
||||||
{
|
{
|
||||||
|
constexpr std::size_t readBatchSize{ 100 };
|
||||||
|
|
||||||
const db::TrackLyricsId trackLyricsId{ searchContext.lastRetrievedTrackLyricsId };
|
const db::TrackLyricsId trackLyricsId{ searchContext.lastRetrievedTrackLyricsId };
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -125,6 +124,8 @@ namespace lms::scanner
|
|||||||
|
|
||||||
void updateTrackLyrics(db::Session& session, TrackLyricsAssociationContainer& lyricsAssociations)
|
void updateTrackLyrics(db::Session& session, TrackLyricsAssociationContainer& lyricsAssociations)
|
||||||
{
|
{
|
||||||
|
constexpr std::size_t writeBatchSize{ 20 };
|
||||||
|
|
||||||
while (!lyricsAssociations.empty())
|
while (!lyricsAssociations.empty())
|
||||||
{
|
{
|
||||||
auto transaction{ session.createWriteTransaction() };
|
auto transaction{ session.createWriteTransaction() };
|
||||||
@@ -138,14 +139,16 @@ namespace lms::scanner
|
|||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
bool ScanStepAssociateExternalLyrics::needProcess(const ScanContext& context) const
|
||||||
|
{
|
||||||
|
if (context.stats.nbChanges() > 0)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
void ScanStepAssociateExternalLyrics::process(ScanContext& context)
|
void ScanStepAssociateExternalLyrics::process(ScanContext& context)
|
||||||
{
|
{
|
||||||
if (_abortScan)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (context.stats.nbChanges() == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
auto& session{ _db.getTLSSession() };
|
auto& session{ _db.getTLSSession() };
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ namespace lms::scanner
|
|||||||
private:
|
private:
|
||||||
ScanStep getStep() const override { return ScanStep::AssociateExternalLyrics; }
|
ScanStep getStep() const override { return ScanStep::AssociateExternalLyrics; }
|
||||||
core::LiteralString getStepName() const override { return "Associate external lyrics"; }
|
core::LiteralString getStepName() const override { return "Associate external lyrics"; }
|
||||||
|
bool needProcess(const ScanContext& context) const override;
|
||||||
void process(ScanContext& context) override;
|
void process(ScanContext& context) override;
|
||||||
};
|
};
|
||||||
} // namespace lms::scanner
|
} // namespace lms::scanner
|
||||||
|
|||||||
@@ -37,9 +37,6 @@ namespace lms::scanner
|
|||||||
{
|
{
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
constexpr std::size_t readBatchSize{ 20 };
|
|
||||||
constexpr std::size_t writeBatchSize{ 5 };
|
|
||||||
|
|
||||||
struct TrackInfo
|
struct TrackInfo
|
||||||
{
|
{
|
||||||
db::TrackId trackId;
|
db::TrackId trackId;
|
||||||
@@ -118,6 +115,8 @@ namespace lms::scanner
|
|||||||
const db::PlayListFileId playListFileIdId{ searchContext.lastRetrievedPlayListFileId };
|
const db::PlayListFileId playListFileIdId{ searchContext.lastRetrievedPlayListFileId };
|
||||||
|
|
||||||
{
|
{
|
||||||
|
constexpr std::size_t readBatchSize{ 20 };
|
||||||
|
|
||||||
auto transaction{ searchContext.session.createReadTransaction() };
|
auto transaction{ searchContext.session.createReadTransaction() };
|
||||||
|
|
||||||
db::PlayListFile::find(searchContext.session, searchContext.lastRetrievedPlayListFileId, readBatchSize, [&](const db::PlayListFile::pointer& playListFile) {
|
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)
|
void updatePlayListFiles(db::Session& session, PlayListFileAssociationContainer& playListFileAssociations)
|
||||||
{
|
{
|
||||||
|
constexpr std::size_t writeBatchSize{ 5 };
|
||||||
|
|
||||||
while (!playListFileAssociations.empty())
|
while (!playListFileAssociations.empty())
|
||||||
{
|
{
|
||||||
auto transaction{ session.createWriteTransaction() };
|
auto transaction{ session.createWriteTransaction() };
|
||||||
@@ -212,14 +213,19 @@ namespace lms::scanner
|
|||||||
}
|
}
|
||||||
} // namespace
|
} // 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)
|
void ScanStepAssociatePlayListTracks::process(ScanContext& context)
|
||||||
{
|
{
|
||||||
if (_abortScan)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (context.stats.nbChanges() == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
auto& session{ _db.getTLSSession() };
|
auto& session{ _db.getTLSSession() };
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ namespace lms::scanner
|
|||||||
private:
|
private:
|
||||||
ScanStep getStep() const override { return ScanStep::AssociatePlayListTracks; }
|
ScanStep getStep() const override { return ScanStep::AssociatePlayListTracks; }
|
||||||
core::LiteralString getStepName() const override { return "Associate playlist tracks"; }
|
core::LiteralString getStepName() const override { return "Associate playlist tracks"; }
|
||||||
|
bool needProcess(const ScanContext& context) const override;
|
||||||
void process(ScanContext& context) override;
|
void process(ScanContext& context) override;
|
||||||
};
|
};
|
||||||
} // namespace lms::scanner
|
} // namespace lms::scanner
|
||||||
|
|||||||
@@ -38,9 +38,6 @@ namespace lms::scanner
|
|||||||
{
|
{
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
constexpr std::size_t readBatchSize{ 100 };
|
|
||||||
constexpr std::size_t writeBatchSize{ 20 };
|
|
||||||
|
|
||||||
struct ReleaseImageAssociation
|
struct ReleaseImageAssociation
|
||||||
{
|
{
|
||||||
db::ReleaseId releaseId;
|
db::ReleaseId releaseId;
|
||||||
@@ -48,7 +45,7 @@ namespace lms::scanner
|
|||||||
};
|
};
|
||||||
using ReleaseImageAssociationContainer = std::deque<ReleaseImageAssociation>;
|
using ReleaseImageAssociationContainer = std::deque<ReleaseImageAssociation>;
|
||||||
|
|
||||||
struct SearchImageContext
|
struct SearchReleaseImageContext
|
||||||
{
|
{
|
||||||
db::Session& session;
|
db::Session& session;
|
||||||
db::ReleaseId lastRetrievedReleaseId;
|
db::ReleaseId lastRetrievedReleaseId;
|
||||||
@@ -56,7 +53,7 @@ namespace lms::scanner
|
|||||||
const std::vector<std::string>& releaseFileNames;
|
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;
|
db::Image::pointer image;
|
||||||
|
|
||||||
@@ -82,7 +79,7 @@ namespace lms::scanner
|
|||||||
return image;
|
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;
|
db::Image::pointer image;
|
||||||
|
|
||||||
@@ -130,11 +127,13 @@ namespace lms::scanner
|
|||||||
return image;
|
return image;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool fetchNextReleaseImagesToUpdate(SearchImageContext& searchContext, ReleaseImageAssociationContainer& releaseImageAssociations)
|
bool fetchNextReleaseImagesToUpdate(SearchReleaseImageContext& searchContext, ReleaseImageAssociationContainer& releaseImageAssociations)
|
||||||
{
|
{
|
||||||
const db::ReleaseId releaseId{ searchContext.lastRetrievedReleaseId };
|
const db::ReleaseId releaseId{ searchContext.lastRetrievedReleaseId };
|
||||||
|
|
||||||
{
|
{
|
||||||
|
constexpr std::size_t readBatchSize{ 100 };
|
||||||
|
|
||||||
auto transaction{ searchContext.session.createReadTransaction() };
|
auto transaction{ searchContext.session.createReadTransaction() };
|
||||||
|
|
||||||
db::Release::find(searchContext.session, searchContext.lastRetrievedReleaseId, readBatchSize, [&](const db::Release::pointer& release) {
|
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)
|
void updateReleaseImages(db::Session& session, ReleaseImageAssociationContainer& imageAssociations)
|
||||||
{
|
{
|
||||||
|
constexpr std::size_t writeBatchSize{ 20 };
|
||||||
|
|
||||||
while (!imageAssociations.empty())
|
while (!imageAssociations.empty())
|
||||||
{
|
{
|
||||||
auto transaction{ session.createWriteTransaction() };
|
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)
|
void ScanStepAssociateReleaseImages::process(ScanContext& context)
|
||||||
{
|
{
|
||||||
if (_abortScan)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (context.stats.nbChanges() == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
auto& session{ _db.getTLSSession() };
|
auto& session{ _db.getTLSSession() };
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -214,7 +217,7 @@ namespace lms::scanner
|
|||||||
context.currentStepStats.totalElems = db::Release::getCount(session);
|
context.currentStepStats.totalElems = db::Release::getCount(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
SearchImageContext searchContext{
|
SearchReleaseImageContext searchContext{
|
||||||
.session = session,
|
.session = session,
|
||||||
.lastRetrievedReleaseId = {},
|
.lastRetrievedReleaseId = {},
|
||||||
.releaseFileNames = _releaseFileNames,
|
.releaseFileNames = _releaseFileNames,
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ namespace lms::scanner
|
|||||||
private:
|
private:
|
||||||
ScanStep getStep() const override { return ScanStep::AssociateReleaseImages; }
|
ScanStep getStep() const override { return ScanStep::AssociateReleaseImages; }
|
||||||
core::LiteralString getStepName() const override { return "Associate release images"; }
|
core::LiteralString getStepName() const override { return "Associate release images"; }
|
||||||
|
bool needProcess(const ScanContext& context) const override;
|
||||||
void process(ScanContext& context) override;
|
void process(ScanContext& context) override;
|
||||||
|
|
||||||
const std::vector<std::string> _releaseFileNames;
|
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
|
struct InitParams
|
||||||
{
|
{
|
||||||
const ScannerSettings& settings;
|
const ScannerSettings& settings;
|
||||||
|
const ScannerSettings* lastScanSettings{};
|
||||||
ProgressCallback progressCallback;
|
ProgressCallback progressCallback;
|
||||||
bool& abortScan;
|
bool& abortScan;
|
||||||
db::Db& db;
|
db::Db& db;
|
||||||
std::span<IFileScanner*> fileScanners;
|
std::span<IFileScanner*> fileScanners;
|
||||||
};
|
};
|
||||||
ScanStepBase(InitParams& initParams)
|
ScanStepBase(InitParams& initParams);
|
||||||
: _settings{ initParams.settings }
|
~ScanStepBase() override;
|
||||||
, _progressCallback{ initParams.progressCallback }
|
|
||||||
, _abortScan{ initParams.abortScan }
|
|
||||||
, _db{ initParams.db }
|
|
||||||
, _fileScanners(std::cbegin(initParams.fileScanners), std::cend(initParams.fileScanners))
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
protected:
|
|
||||||
~ScanStepBase() override = default;
|
|
||||||
ScanStepBase(const ScanStepBase&) = delete;
|
ScanStepBase(const ScanStepBase&) = delete;
|
||||||
ScanStepBase& operator=(const ScanStepBase&) = delete;
|
ScanStepBase& operator=(const ScanStepBase&) = delete;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
const ScannerSettings* getLastScanSettings() const { return _lastScanSettings; }
|
||||||
|
|
||||||
const ScannerSettings& _settings;
|
const ScannerSettings& _settings;
|
||||||
ProgressCallback _progressCallback;
|
ProgressCallback _progressCallback;
|
||||||
bool& _abortScan;
|
bool& _abortScan;
|
||||||
db::Db& _db;
|
db::Db& _db;
|
||||||
std::vector<IFileScanner*> _fileScanners;
|
std::vector<IFileScanner*> _fileScanners;
|
||||||
|
|
||||||
|
private:
|
||||||
|
const ScannerSettings* _lastScanSettings{};
|
||||||
};
|
};
|
||||||
} // namespace lms::scanner
|
} // namespace lms::scanner
|
||||||
|
|||||||
@@ -26,13 +26,16 @@
|
|||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
{
|
{
|
||||||
|
bool ScanStepCheckForDuplicatedFiles::needProcess([[maybe_unused]] const ScanContext& context) const
|
||||||
|
{
|
||||||
|
// Always check for everything
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void ScanStepCheckForDuplicatedFiles::process(ScanContext& context)
|
void ScanStepCheckForDuplicatedFiles::process(ScanContext& context)
|
||||||
{
|
{
|
||||||
using namespace db;
|
using namespace db;
|
||||||
|
|
||||||
if (_abortScan)
|
|
||||||
return;
|
|
||||||
|
|
||||||
Session& session{ _db.getTLSSession() };
|
Session& session{ _db.getTLSSession() };
|
||||||
auto transaction{ session.createReadTransaction() };
|
auto transaction{ session.createReadTransaction() };
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ namespace lms::scanner
|
|||||||
private:
|
private:
|
||||||
core::LiteralString getStepName() const override { return "Check for duplicated files"; }
|
core::LiteralString getStepName() const override { return "Check for duplicated files"; }
|
||||||
ScanStep getStep() const override { return ScanStep::CheckForDuplicatedFiles; }
|
ScanStep getStep() const override { return ScanStep::CheckForDuplicatedFiles; }
|
||||||
|
bool needProcess(const ScanContext& context) const override;
|
||||||
void process(ScanContext& context) override;
|
void process(ScanContext& context) override;
|
||||||
};
|
};
|
||||||
} // namespace lms::scanner
|
} // namespace lms::scanner
|
||||||
|
|||||||
@@ -41,11 +41,14 @@ namespace lms::scanner
|
|||||||
constexpr std::size_t batchSize = 100;
|
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)
|
void ScanStepCheckForRemovedFiles::process(ScanContext& context)
|
||||||
{
|
{
|
||||||
if (_abortScan)
|
|
||||||
return;
|
|
||||||
|
|
||||||
db::Session& session{ _db.getTLSSession() };
|
db::Session& session{ _db.getTLSSession() };
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ namespace lms::scanner
|
|||||||
private:
|
private:
|
||||||
core::LiteralString getStepName() const override { return "Check for removed files"; }
|
core::LiteralString getStepName() const override { return "Check for removed files"; }
|
||||||
ScanStep getStep() const override { return ScanStep::CheckForRemovedFiles; }
|
ScanStep getStep() const override { return ScanStep::CheckForRemovedFiles; }
|
||||||
|
bool needProcess(const ScanContext& context) const override;
|
||||||
void process(ScanContext& context) override;
|
void process(ScanContext& context) override;
|
||||||
|
|
||||||
template<typename Object>
|
template<typename Object>
|
||||||
|
|||||||
@@ -24,10 +24,14 @@
|
|||||||
|
|
||||||
namespace lms::scanner
|
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
|
// Don't auto compact as it may be too annoying to block the whole application for very large databases
|
||||||
if (context.scanOptions.compact)
|
return context.scanOptions.compact;
|
||||||
_db.getTLSSession().vacuum();
|
}
|
||||||
|
|
||||||
|
void ScanStepCompact::process([[maybe_unused]] ScanContext& context)
|
||||||
|
{
|
||||||
|
_db.getTLSSession().vacuum();
|
||||||
}
|
}
|
||||||
} // namespace lms::scanner
|
} // namespace lms::scanner
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ namespace lms::scanner
|
|||||||
private:
|
private:
|
||||||
ScanStep getStep() const override { return ScanStep::Compact; }
|
ScanStep getStep() const override { return ScanStep::Compact; }
|
||||||
core::LiteralString getStepName() const override { return "Compact"; }
|
core::LiteralString getStepName() const override { return "Compact"; }
|
||||||
|
bool needProcess(const ScanContext& context) const override;
|
||||||
void process(ScanContext& context) override;
|
void process(ScanContext& context) override;
|
||||||
};
|
};
|
||||||
} // namespace lms::scanner
|
} // namespace lms::scanner
|
||||||
|
|||||||
@@ -25,13 +25,18 @@
|
|||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
{
|
{
|
||||||
|
bool ScanStepComputeClusterStats::needProcess(const ScanContext& context) const
|
||||||
|
{
|
||||||
|
if (context.stats.nbChanges() > 0)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
void ScanStepComputeClusterStats::process(ScanContext& context)
|
void ScanStepComputeClusterStats::process(ScanContext& context)
|
||||||
{
|
{
|
||||||
using namespace db;
|
using namespace db;
|
||||||
|
|
||||||
if (context.stats.nbChanges() == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
Session& dbSession{ _db.getTLSSession() };
|
Session& dbSession{ _db.getTLSSession() };
|
||||||
|
|
||||||
const std::size_t clusterCount{ [&] {
|
const std::size_t clusterCount{ [&] {
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ namespace lms::scanner
|
|||||||
private:
|
private:
|
||||||
ScanStep getStep() const override { return ScanStep::ComputeClusterStats; }
|
ScanStep getStep() const override { return ScanStep::ComputeClusterStats; }
|
||||||
core::LiteralString getStepName() const override { return "Compute cluster stats"; }
|
core::LiteralString getStepName() const override { return "Compute cluster stats"; }
|
||||||
|
bool needProcess(const ScanContext& context) const override;
|
||||||
void process(ScanContext& context) override;
|
void process(ScanContext& context) override;
|
||||||
};
|
};
|
||||||
} // namespace lms::scanner
|
} // namespace lms::scanner
|
||||||
|
|||||||
@@ -28,6 +28,12 @@
|
|||||||
|
|
||||||
namespace lms::scanner
|
namespace lms::scanner
|
||||||
{
|
{
|
||||||
|
bool ScanStepDiscoverFiles::needProcess([[maybe_unused]] const ScanContext& context) const
|
||||||
|
{
|
||||||
|
// always discover files
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void ScanStepDiscoverFiles::process(ScanContext& context)
|
void ScanStepDiscoverFiles::process(ScanContext& context)
|
||||||
{
|
{
|
||||||
context.stats.totalFileCount = 0;
|
context.stats.totalFileCount = 0;
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ namespace lms::scanner
|
|||||||
private:
|
private:
|
||||||
ScanStep getStep() const override { return ScanStep::DiscoverFiles; }
|
ScanStep getStep() const override { return ScanStep::DiscoverFiles; }
|
||||||
core::LiteralString getStepName() const override { return "Discover files"; }
|
core::LiteralString getStepName() const override { return "Discover files"; }
|
||||||
|
bool needProcess(const ScanContext& context) const override;
|
||||||
void process(ScanContext& context) override;
|
void process(ScanContext& context) override;
|
||||||
};
|
};
|
||||||
} // namespace lms::scanner
|
} // namespace lms::scanner
|
||||||
|
|||||||
@@ -25,32 +25,38 @@
|
|||||||
|
|
||||||
namespace lms::scanner
|
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)
|
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() };
|
_db.getTLSSession().analyzeEntry(entry);
|
||||||
|
context.currentStepStats.processedElems++;
|
||||||
std::vector<std::string> entries;
|
|
||||||
session.retrieveEntriesToAnalyze(entries);
|
|
||||||
context.currentStepStats.totalElems = entries.size();
|
|
||||||
_progressCallback(context.currentStepStats);
|
_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
|
} // namespace lms::scanner
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ namespace lms::scanner
|
|||||||
private:
|
private:
|
||||||
ScanStep getStep() const override { return ScanStep::Optimize; }
|
ScanStep getStep() const override { return ScanStep::Optimize; }
|
||||||
core::LiteralString getStepName() const override { return "Optimize"; }
|
core::LiteralString getStepName() const override { return "Optimize"; }
|
||||||
|
bool needProcess(const ScanContext& context) const override;
|
||||||
void process(ScanContext& context) override;
|
void process(ScanContext& context) override;
|
||||||
};
|
};
|
||||||
} // namespace lms::scanner
|
} // namespace lms::scanner
|
||||||
|
|||||||
@@ -31,6 +31,12 @@
|
|||||||
|
|
||||||
namespace lms::scanner
|
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)
|
void ScanStepRemoveOrphanedDbEntries::process(ScanContext& context)
|
||||||
{
|
{
|
||||||
removeOrphanedClusters(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