Merge branch 'develop' for release v3.52.0

This commit is contained in:
emeric
2024-04-21 20:32:28 +02:00
154 changed files with 3667 additions and 1761 deletions
+2
View File
@@ -12,4 +12,6 @@ CheckOptions:
value: '1' value: '1'
- key: cppcoreguidelines-avoid-do-while.IgnoreMacros - key: cppcoreguidelines-avoid-do-while.IgnoreMacros
value: '1' value: '1'
- key: performance-unnecessary-value-param.AllowedTypes
value: "shared_ptr"
... ...
+1 -1
View File
@@ -20,7 +20,7 @@ endif()
find_package(PkgConfig REQUIRED) find_package(PkgConfig REQUIRED)
find_package(Threads REQUIRED) find_package(Threads REQUIRED)
find_package(Filesystem REQUIRED) find_package(Filesystem REQUIRED)
find_package(Boost REQUIRED COMPONENTS system program_options) find_package(Boost REQUIRED COMPONENTS system program_options iostreams)
find_package(Wt REQUIRED COMPONENTS Wt Dbo DboSqlite3 HTTP) find_package(Wt REQUIRED COMPONENTS Wt Dbo DboSqlite3 HTTP)
pkg_check_modules(Taglib REQUIRED IMPORTED_TARGET taglib) pkg_check_modules(Taglib REQUIRED IMPORTED_TARGET taglib)
pkg_check_modules(Config++ REQUIRED IMPORTED_TARGET libconfig++) pkg_check_modules(Config++ REQUIRED IMPORTED_TARGET libconfig++)
+4 -1
View File
@@ -31,6 +31,7 @@ ARG BUILD_PACKAGES=" \
libarchive-dev \ libarchive-dev \
libconfig-dev \ libconfig-dev \
taglib-dev \ taglib-dev \
sqlite-dev \
gtest-dev" gtest-dev"
RUN apk add --no-cache --update ${BUILD_PACKAGES} RUN apk add --no-cache --update ${BUILD_PACKAGES}
@@ -86,7 +87,7 @@ RUN \
RUN \ RUN \
DIR=/tmp/wt && mkdir -p ${DIR} && cd ${DIR} && \ DIR=/tmp/wt && mkdir -p ${DIR} && cd ${DIR} && \
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${PREFIX} -DBUILD_EXAMPLES=OFF -DENABLE_LIBWTTEST=OFF -DCONNECTOR_FCGI=OFF && \ cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=${PREFIX} -DBUILD_EXAMPLES=OFF -DENABLE_LIBWTTEST=OFF -DCONNECTOR_FCGI=OFF -DUSE_SYSTEM_SQLITE3=ON && \
make -j$(nproc) && \ make -j$(nproc) && \
make install make install
@@ -147,11 +148,13 @@ ARG RUNTIME_PACKAGES=" \
lame-libs \ lame-libs \
zlib \ zlib \
boost-filesystem \ boost-filesystem \
boost-iostreams \
boost-program_options \ boost-program_options \
boost-system \ boost-system \
boost-thread \ boost-thread \
libarchive \ libarchive \
libconfig++ \ libconfig++ \
sqlite-libs \
taglib" taglib"
ARG LMS_USER=lms ARG LMS_USER=lms
+6
View File
@@ -11,6 +11,7 @@ OpenSubsonic is an initiative to patch and extend the legacy Subsonic API. You'l
## Extra fields ## Extra fields
The following extra fields are implemented: The following extra fields are implemented:
* `Album` response: * `Album` response:
* `mediaType`
* `played` * `played`
* `musicBrainzId` * `musicBrainzId`
* `genres` * `genres`
@@ -22,6 +23,10 @@ The following extra fields are implemented:
* `isCompilation` * `isCompilation`
* `discTitles`: discs with no subtitle are omitted * `discTitles`: discs with no subtitle are omitted
* `Child` response: * `Child` response:
* `bitDepth`
* `samplingRate`
* `channelCount`
* `mediaType`
* `played` * `played`
* `musicBrainzId`: note this is actually the recording MBID when this response refers to a song * `musicBrainzId`: note this is actually the recording MBID when this response refers to a song
* `genres` * `genres`
@@ -33,6 +38,7 @@ The following extra fields are implemented:
* `moods` * `moods`
* `replayGain` * `replayGain`
* `Artist` response: * `Artist` response:
* `mediaType`
* `musicBrainzId` * `musicBrainzId`
* `sortName` * `sortName`
* `roles` * `roles`
+53 -32
View File
@@ -2,42 +2,63 @@
<messages xmlns:if="Wt.WTemplate.conditions"> <messages xmlns:if="Wt.WTemplate.conditions">
<message id="Lms.Admin.ScannerController.template"> <message id="Lms.Admin.ScannerController.template">
<form> <legend>${tr:Lms.Admin.ScannerController.scanner}</legend>
<legend>${tr:Lms.Admin.ScannerController.scanner}</legend> <div class="row g-3">
<div class="row g-3"> <div class="col-lg-6">
<div class="col-lg-6"> <label class="form-label" for="${id:status}">
<label class="form-label" for="${id:status}"> ${tr:Lms.Admin.ScannerController.status}
${tr:Lms.Admin.ScannerController.status} </label>
</label> ${status class="form-control"}
${status class="form-control"} </div>
<div class="col-lg-6">
<label class="form-label" for="${id:step-status}">
${tr:Lms.Admin.ScannerController.step-status}
</label>
${step-status class="form-control"}
</div>
<div class="col-12">
<label class="form-label" for="${id:last-scan}">
${tr:Lms.Admin.ScannerController.last-scan}
</label>
<div class="input-group">
${last-scan class="form-control"}
${report-btn class="btn btn-outline-info"}
</div> </div>
<div class="col-lg-6"> </div>
<label class="form-label" for="${id:step-status}"> <div class="col-12">
${tr:Lms.Admin.ScannerController.step-status} <div class="collapse" id="scanOptions">
</label> <div class="card bg-dark">
${step-status class="form-control"} <div class="card-header">
</div> ${tr:Lms.Admin.ScannerController.scan-options}
<div class="col-12"> </div>
<label class="form-label" for="{id:last-scan}"> <div class="card-body">
${tr:Lms.Admin.ScannerController.last-scan} <div class="form-check mb-1">
</label> ${full-scan class="form-check-input"}
<div class="input-group"> <label class="form-check-label" for="${id:full-scan}">
${last-scan class="form-control"} ${tr:Lms.Admin.ScannerController.full-scan}
${report-btn class="btn btn-outline-info"} </label>
</div> </div>
</div> <div class="form-check mb-1">
<div class="col-12"> ${force-optimize class="form-check-input"}
<div class="btn-group"> <label class="form-check-label" for="${id:force-optimize}">
${scan-btn class="btn btn-primary"} ${tr:Lms.Admin.ScannerController.force-optimize}
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false"> </label>
</button> </div>
<ul class="dropdown-menu"> <div class="form-check">
<li>${full-scan-btn class="dropdown-item"}</li> ${compact class="form-check-input"}
</ul> <label class="form-check-label" for="${id:compact}">
${tr:Lms.Admin.ScannerController.compact}
</label>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
</form> <div class="col-12">
${scan-btn class="btn btn-primary me-2"}
<button class="btn btn-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#scanOptions" aria-expanded="false" aria-controls="scanOptions">${tr:Lms.Admin.ScannerController.scan-options}</button>
</div>
</div>
</message> </message>
</messages> </messages>
+1 -1
View File
@@ -6,7 +6,7 @@
<legend>${title}</legend> <legend>${title}</legend>
${<if-has-last-login>} ${<if-has-last-login>}
<div class="col-lg-6"> <div class="col-lg-6">
<label class="form-label" for="{id:last-login}"> <label class="form-label" for="${id:last-login}">
${tr:Lms.Admin.User.last-login} ${tr:Lms.Admin.User.last-login}
</label> </label>
<input type="text" readonly="readonly" class="form-control" id="status" value="${last-login}"/> <input type="text" readonly="readonly" class="form-control" id="status" value="${last-login}"/>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

+11
View File
@@ -0,0 +1,11 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg fill="#c9c9c9" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="800px" height="800px" viewBox="-46.75 -46.75 560.95 560.95" xml:space="preserve" stroke="#c9c9c9" transform="rotate(0)matrix(1, 0, 0, 1, 0, 0)">
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
<g id="SVGRepo_iconCarrier"> <g> <g id="Layer_7_5_"> <path d="M233.728,0C104.641,0,0,104.642,0,233.727c0,129.084,104.641,233.725,233.727,233.725 c129.083,0,233.724-104.641,233.724-233.725C467.451,104.642,362.811,0,233.728,0z M64.964,250.082h-9.692 c0-52.863,19.316-101.268,51.208-138.639c2.288,2.292,4.578,4.575,6.871,6.87C83.2,153.906,64.964,199.893,64.964,250.082z M98.899,250.082h-9.695c0-43.511,15.522-83.45,41.315-114.595l6.886,6.889C113.355,171.742,98.899,209.244,98.899,250.082z M233.728,326.421c-51.194,0-92.698-41.492-92.698-92.694c0-51.199,41.503-92.696,92.698-92.696 c51.199,0,92.693,41.497,92.693,92.696C326.421,284.929,284.927,326.421,233.728,326.421z M333.519,333.59 c24.055-29.363,38.514-66.885,38.514-107.708h9.689c0,43.51-15.524,83.453-41.313,114.597L333.519,333.59z M364.446,364.523 c-2.293-2.288-4.572-4.575-6.873-6.868c30.14-35.606,48.372-81.593,48.372-131.774h9.697 C415.652,278.749,396.337,327.156,364.446,364.523z M265.021,233.727c0,17.28-14.009,31.293-31.293,31.293 c-17.287,0-31.296-14.013-31.296-31.293c0-17.287,14.009-31.295,31.296-31.295C251.012,202.431,265.021,216.439,265.021,233.727z"/> </g> </g> </g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+6 -1
View File
@@ -89,9 +89,11 @@
<message id="Lms.Admin.ScannerController.bad-duration">Cannot get track duration</message> <message id="Lms.Admin.ScannerController.bad-duration">Cannot get track duration</message>
<message id="Lms.Admin.ScannerController.cannot-parse-file">Cannot parse file</message> <message id="Lms.Admin.ScannerController.cannot-parse-file">Cannot parse file</message>
<message id="Lms.Admin.ScannerController.cannot-read-file">Cannot read file</message> <message id="Lms.Admin.ScannerController.cannot-read-file">Cannot read file</message>
<message id="Lms.Admin.ScannerController.compact">Compact the database. <strong>Caution:</strong> this may take a while and will block the whole application during the compact step!</message>
<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-scan-now">Force full rescan now</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.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>
@@ -100,6 +102,7 @@
<message id="Lms.Admin.ScannerController.same-hash">Duplicated file hash</message> <message id="Lms.Admin.ScannerController.same-hash">Duplicated file hash</message>
<message id="Lms.Admin.ScannerController.same-mbid">Duplicated track MBID</message> <message id="Lms.Admin.ScannerController.same-mbid">Duplicated track MBID</message>
<message id="Lms.Admin.ScannerController.scan-now">Scan now</message> <message id="Lms.Admin.ScannerController.scan-now">Scan now</message>
<message id="Lms.Admin.ScannerController.scan-options">Scan options</message>
<message id="Lms.Admin.ScannerController.scanner">Scanner</message> <message id="Lms.Admin.ScannerController.scanner">Scanner</message>
<message id="Lms.Admin.ScannerController.status">Status</message> <message id="Lms.Admin.ScannerController.status">Status</message>
<message id="Lms.Admin.ScannerController.status-not-scheduled">Not scheduled</message> <message id="Lms.Admin.ScannerController.status-not-scheduled">Not scheduled</message>
@@ -107,9 +110,11 @@
<message id="Lms.Admin.ScannerController.status-in-progress">Scanning: step {1}/{2}</message> <message id="Lms.Admin.ScannerController.status-in-progress">Scanning: step {1}/{2}</message>
<message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">Checking for duplicate files... {1} files</message> <message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">Checking for duplicate files... {1} files</message>
<message id="Lms.Admin.ScannerController.step-checking-for-missing-files">Checking files... {1}%</message> <message id="Lms.Admin.ScannerController.step-checking-for-missing-files">Checking files... {1}%</message>
<message id="Lms.Admin.ScannerController.step-compact">Compacting database...</message>
<message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Computing stats... {1}%</message> <message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Computing stats... {1}%</message>
<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}/{2} entries ({3}%)...</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-scanning-files">Scanning files: {1}/{2} files ({3}%)...</message> <message id="Lms.Admin.ScannerController.step-scanning-files">Scanning files: {1}/{2} files ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-status">Step status</message> <message id="Lms.Admin.ScannerController.step-status">Step status</message>
+6 -1
View File
@@ -89,9 +89,11 @@
<message id="Lms.Admin.ScannerController.bad-duration">Impossible de récupérer la durée de la piste</message> <message id="Lms.Admin.ScannerController.bad-duration">Impossible de récupérer la durée de la piste</message>
<message id="Lms.Admin.ScannerController.cannot-parse-file">Impossible d'analyser le fichier</message> <message id="Lms.Admin.ScannerController.cannot-parse-file">Impossible d'analyser le fichier</message>
<message id="Lms.Admin.ScannerController.cannot-read-file">Impossible de lire le fichier</message> <message id="Lms.Admin.ScannerController.cannot-read-file">Impossible de lire le fichier</message>
<message id="Lms.Admin.ScannerController.compact">Compacter la base de données. <strong>Attention :</strong> cette opération peut prendre du temps et va vérouiller l'application pendant toute l'étape de compactage!</message>
<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-scan-now">Forcer un rescan complet</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.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>
@@ -100,6 +102,7 @@
<message id="Lms.Admin.ScannerController.same-hash">Hash dupliqué</message> <message id="Lms.Admin.ScannerController.same-hash">Hash dupliqué</message>
<message id="Lms.Admin.ScannerController.same-mbid">Track MBID dupliqué</message> <message id="Lms.Admin.ScannerController.same-mbid">Track MBID dupliqué</message>
<message id="Lms.Admin.ScannerController.scan-now">Lancer un scan</message> <message id="Lms.Admin.ScannerController.scan-now">Lancer un scan</message>
<message id="Lms.Admin.ScannerController.scan-options">Options de scan</message>
<message id="Lms.Admin.ScannerController.scanner">Scanner</message> <message id="Lms.Admin.ScannerController.scanner">Scanner</message>
<message id="Lms.Admin.ScannerController.status">Statut</message> <message id="Lms.Admin.ScannerController.status">Statut</message>
<message id="Lms.Admin.ScannerController.status-not-scheduled">Non planifié</message> <message id="Lms.Admin.ScannerController.status-not-scheduled">Non planifié</message>
@@ -107,9 +110,11 @@
<message id="Lms.Admin.ScannerController.status-in-progress">En cours de scan : étape {1}/{2}</message> <message id="Lms.Admin.ScannerController.status-in-progress">En cours de scan : étape {1}/{2}</message>
<message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">Vérification des fichiers dupliqués... {1} fichiers</message> <message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">Vérification des fichiers dupliqués... {1} fichiers</message>
<message id="Lms.Admin.ScannerController.step-checking-for-missing-files">Vérification des fichiers... {1}%</message> <message id="Lms.Admin.ScannerController.step-checking-for-missing-files">Vérification des fichiers... {1}%</message>
<message id="Lms.Admin.ScannerController.step-compact">Compactage de la base de données...</message>
<message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Calcul des statistiques... {1}%</message> <message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Calcul des statistiques... {1}%</message>
<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}/{2} entrées ({3}%)...</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-scanning-files">Scan des fichiers : {1}/{2} fichiers ({3}%)...</message> <message id="Lms.Admin.ScannerController.step-scanning-files">Scan des fichiers : {1}/{2} fichiers ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-status">Statut de l'étape</message> <message id="Lms.Admin.ScannerController.step-status">Statut de l'étape</message>
+5 -1
View File
@@ -89,9 +89,11 @@
<message id="Lms.Admin.ScannerController.bad-duration">Non sono stato in grado di determinare la durata della traccia</message> <message id="Lms.Admin.ScannerController.bad-duration">Non sono stato in grado di determinare la durata della traccia</message>
<message id="Lms.Admin.ScannerController.cannot-parse-file">Non in grado di analizzare il file</message> <message id="Lms.Admin.ScannerController.cannot-parse-file">Non in grado di analizzare il file</message>
<message id="Lms.Admin.ScannerController.cannot-read-file">Non in grado di leggere il file</message> <message id="Lms.Admin.ScannerController.cannot-read-file">Non in grado di leggere il file</message>
<message id="Lms.Admin.ScannerController.compact">Compatta il database. <strong>Attenzione:</strong> ciò potrebbe richiedere del tempo e bloccherà l'intera applicazione durante il passaggio di compattazione!</message>
<message id="Lms.Admin.ScannerController.duplicates-header">{1} file duplicati:</message> <message id="Lms.Admin.ScannerController.duplicates-header">{1} file duplicati:</message>
<message id="Lms.Admin.ScannerController.errors-header">{1} errori:</message> <message id="Lms.Admin.ScannerController.errors-header">{1} errori:</message>
<message id="Lms.Admin.ScannerController.force-scan-now">Forza una nuova scansione completa</message> <message id="Lms.Admin.ScannerController.force-optimize">Forza l'ottimizzazione del database</message>
<message id="Lms.Admin.ScannerController.full-scan">Forza la scansione di tutti i file</message>
<message id="Lms.Admin.ScannerController.get-report">Scarica un resoconto</message> <message id="Lms.Admin.ScannerController.get-report">Scarica un resoconto</message>
<message id="Lms.Admin.ScannerController.last-scan">Ultima scansione</message> <message id="Lms.Admin.ScannerController.last-scan">Ultima scansione</message>
<message id="Lms.Admin.ScannerController.last-scan-not-available">Non disponibile</message> <message id="Lms.Admin.ScannerController.last-scan-not-available">Non disponibile</message>
@@ -100,6 +102,7 @@
<message id="Lms.Admin.ScannerController.same-hash">Hash doppio</message> <message id="Lms.Admin.ScannerController.same-hash">Hash doppio</message>
<message id="Lms.Admin.ScannerController.same-mbid">Track MBID doppio</message> <message id="Lms.Admin.ScannerController.same-mbid">Track MBID doppio</message>
<message id="Lms.Admin.ScannerController.scan-now">Scansiona ora</message> <message id="Lms.Admin.ScannerController.scan-now">Scansiona ora</message>
<message id="Lms.Admin.ScannerController.scan-options">Opzioni di scansione</message>
<message id="Lms.Admin.ScannerController.scanner">Scanner</message> <message id="Lms.Admin.ScannerController.scanner">Scanner</message>
<message id="Lms.Admin.ScannerController.status">Stato</message> <message id="Lms.Admin.ScannerController.status">Stato</message>
<message id="Lms.Admin.ScannerController.status-not-scheduled">Non pianificato</message> <message id="Lms.Admin.ScannerController.status-not-scheduled">Non pianificato</message>
@@ -110,6 +113,7 @@
<message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Calcolo statistiche... {1}%</message> <message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Calcolo statistiche... {1}%</message>
<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}/{2} voci ({3}%)...</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-scanning-files">Scansione files: {1}/{2} files ({3}%)...</message> <message id="Lms.Admin.ScannerController.step-scanning-files">Scansione files: {1}/{2} files ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-status">Stato passo</message> <message id="Lms.Admin.ScannerController.step-status">Stato passo</message>
+6 -1
View File
@@ -89,9 +89,11 @@
<message id="Lms.Admin.ScannerController.bad-duration">无法获得音轨时间</message> <message id="Lms.Admin.ScannerController.bad-duration">无法获得音轨时间</message>
<message id="Lms.Admin.ScannerController.cannot-parse-file">无法解析文件</message> <message id="Lms.Admin.ScannerController.cannot-parse-file">无法解析文件</message>
<message id="Lms.Admin.ScannerController.cannot-read-file">无法读取文件</message> <message id="Lms.Admin.ScannerController.cannot-read-file">无法读取文件</message>
<message id="Lms.Admin.ScannerController.duplicates-header">{1} 个重复文件:</message> <message id="Lms.Admin.ScannerController.duplicates-header">{1} 个重复文件:</message>
<message id="Lms.Admin.ScannerController.errors-header">{1} 个错误:</message> <message id="Lms.Admin.ScannerController.errors-header">{1} 个错误:</message>
<message id="Lms.Admin.ScannerController.force-scan-now">立即强制全盘扫描</message>
<message id="Lms.Admin.ScannerController.get-report">生成报告</message> <message id="Lms.Admin.ScannerController.get-report">生成报告</message>
<message id="Lms.Admin.ScannerController.last-scan">最后扫描</message> <message id="Lms.Admin.ScannerController.last-scan">最后扫描</message>
<message id="Lms.Admin.ScannerController.last-scan-not-available">不可用</message> <message id="Lms.Admin.ScannerController.last-scan-not-available">不可用</message>
@@ -100,6 +102,7 @@
<message id="Lms.Admin.ScannerController.same-hash">相同文件哈希值</message> <message id="Lms.Admin.ScannerController.same-hash">相同文件哈希值</message>
<message id="Lms.Admin.ScannerController.same-mbid">相同 MBID</message> <message id="Lms.Admin.ScannerController.same-mbid">相同 MBID</message>
<message id="Lms.Admin.ScannerController.scan-now">立即扫描</message> <message id="Lms.Admin.ScannerController.scan-now">立即扫描</message>
<message id="Lms.Admin.ScannerController.scanner">扫描器</message> <message id="Lms.Admin.ScannerController.scanner">扫描器</message>
<message id="Lms.Admin.ScannerController.status">状态</message> <message id="Lms.Admin.ScannerController.status">状态</message>
<message id="Lms.Admin.ScannerController.status-not-scheduled">无计划</message> <message id="Lms.Admin.ScannerController.status-not-scheduled">无计划</message>
@@ -108,8 +111,10 @@
<message id="Lms.Admin.ScannerController.step-checking-for-missing-files">检查文件中... {1}%</message> <message id="Lms.Admin.ScannerController.step-checking-for-missing-files">检查文件中... {1}%</message>
<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>
<message id="Lms.Admin.ScannerController.step-status">当前步骤状态</message> <message id="Lms.Admin.ScannerController.step-status">当前步骤状态</message>
File diff suppressed because one or more lines are too long
Vendored Regular → Executable
+3 -3
View File
File diff suppressed because one or more lines are too long
+11 -7
View File
@@ -97,17 +97,21 @@ LMS.mediaplayer = function () {
_lastStartPlaying = Date.now(); _lastStartPlaying = Date.now();
} }
let _stopTimer = function() { let _pauseTimer = function() {
if (_lastStartPlaying != null) { if (_lastStartPlaying != null) {
_playedDuration += Date.now() - _lastStartPlaying; _playedDuration += Date.now() - _lastStartPlaying;
_lastStartPlaying = null;
} }
} }
let _resetTimer = function() { let _resetTimer = function() {
if (_lastStartPlaying != null) if (_lastStartPlaying != null)
_pauseTimer();
if (_playedDuration > 0) {
Wt.emit(_root, "scrobbleListenFinished", _trackId, _playedDuration); Wt.emit(_root, "scrobbleListenFinished", _trackId, _playedDuration);
_playedDuration = 0; _playedDuration = 0;
_lastStartPlaying = null; }
} }
let _durationToString = function (duration) { let _durationToString = function (duration) {
@@ -229,7 +233,7 @@ LMS.mediaplayer = function () {
navigator.mediaSession.setPositionState({ navigator.mediaSession.setPositionState({
duration: _duration, duration: _duration,
playbackRate: 1, playbackRate: 1,
position: _offset + _elems.audio.currentTime, position: Math.min(_offset + _elems.audio.currentTime, _duration),
}); });
if (_elems.audio.paused) if (_elems.audio.paused)
@@ -276,9 +280,9 @@ LMS.mediaplayer = function () {
_elems.audio.addEventListener("playing", _updateMediaSessionState); _elems.audio.addEventListener("playing", _updateMediaSessionState);
_elems.audio.addEventListener("pause", _updateMediaSessionState); _elems.audio.addEventListener("pause", _updateMediaSessionState);
_elems.audio.addEventListener("pause", _stopTimer); _elems.audio.addEventListener("pause", _pauseTimer);
_elems.audio.addEventListener("playing", _startTimer); _elems.audio.addEventListener("playing", _startTimer);
_elems.audio.addEventListener("waiting", _stopTimer); _elems.audio.addEventListener("waiting", _pauseTimer);
_elems.audio.addEventListener("timeupdate", function() { _elems.audio.addEventListener("timeupdate", function() {
_elems.progress.style.width = "" + ((_offset + _elems.audio.currentTime) / _duration) * 100 + "%"; _elems.progress.style.width = "" + ((_offset + _elems.audio.currentTime) / _duration) * 100 + "%";
@@ -286,6 +290,7 @@ LMS.mediaplayer = function () {
}); });
_elems.audio.addEventListener("ended", function() { _elems.audio.addEventListener("ended", function() {
_resetTimer();
Wt.emit(_root, "playbackEnded"); Wt.emit(_root, "playbackEnded");
}); });
@@ -366,7 +371,6 @@ LMS.mediaplayer = function () {
} }
let loadTrack = function(params, autoplay) { let loadTrack = function(params, autoplay) {
_stopTimer();
_resetTimer(); _resetTimer();
_trackId = params.trackId; _trackId = params.trackId;
+7
View File
@@ -281,9 +281,16 @@ namespace lms::av
res.emplace(); res.emplace();
res->index = streamIndex; res->index = streamIndex;
res->bitrate = static_cast<std::size_t>(avstream->codecpar->bit_rate); res->bitrate = static_cast<std::size_t>(avstream->codecpar->bit_rate);
res->bitsPerSample = static_cast<std::size_t>(avstream->codecpar->bits_per_coded_sample);
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(59, 24, 100)
res->channelCount = static_cast<std::size_t>(avstream->codecpar->channels);
#else
res->channelCount = static_cast<std::size_t>(avstream->codecpar->ch_layout.nb_channels);
#endif
res->codec = avcodecToDecodingCodec(avstream->codecpar->codec_id); res->codec = avcodecToDecodingCodec(avstream->codecpar->codec_id);
res->codecName = ::avcodec_get_name(avstream->codecpar->codec_id); res->codecName = ::avcodec_get_name(avstream->codecpar->codec_id);
assert(!res->codecName.empty()); // doc says it is never NULL assert(!res->codecName.empty()); // doc says it is never NULL
res->sampleRate = static_cast<std::size_t>(avstream->codecpar->sample_rate);
return res; return res;
} }
+3
View File
@@ -75,6 +75,9 @@ namespace lms::av
{ {
size_t index{}; size_t index{};
std::size_t bitrate{}; std::size_t bitrate{};
std::size_t bitsPerSample{};
std::size_t channelCount{};
std::size_t sampleRate{};
DecodingCodec codec; DecodingCodec codec;
std::string codecName; std::string codecName;
}; };
+26
View File
@@ -39,6 +39,14 @@ namespace lms::core
} }
} }
static void BM_TraceLogger_Overview_withArg(benchmark::State& state)
{
for (auto _ : state)
{
LMS_SCOPED_TRACE_OVERVIEW_WITH_ARG("Cat", "Test", "ArgType", "My arg that can be very very long, and even as long as needed");
}
}
static void BM_TraceLogger_Detailed(benchmark::State& state) static void BM_TraceLogger_Detailed(benchmark::State& state)
{ {
for (auto _ : state) for (auto _ : state)
@@ -48,8 +56,26 @@ namespace lms::core
} }
} }
static void BM_TraceLogger_Detailed_withArg(benchmark::State& state)
{
auto someExpensiveArgComputation{ []() -> std::string
{
std::this_thread::sleep_for(std::chrono::microseconds{ 1 });
return "foo";
} };
for (auto _ : state)
{
// Should do nothing (and cost nothing)
LMS_SCOPED_TRACE_DETAILED_WITH_ARG("Cat", "Test", "ArgType", someExpensiveArgComputation());
}
}
BENCHMARK(BM_TraceLogger_Overview)->Threads(1)->Threads(std::thread::hardware_concurrency()); BENCHMARK(BM_TraceLogger_Overview)->Threads(1)->Threads(std::thread::hardware_concurrency());
BENCHMARK(BM_TraceLogger_Overview_withArg)->Threads(1)->Threads(std::thread::hardware_concurrency());
BENCHMARK(BM_TraceLogger_Detailed)->Threads(1)->Threads(std::thread::hardware_concurrency()); BENCHMARK(BM_TraceLogger_Detailed)->Threads(1)->Threads(std::thread::hardware_concurrency());
BENCHMARK(BM_TraceLogger_Detailed_withArg)->Threads(1)->Threads(std::thread::hardware_concurrency());
} }
BENCHMARK_MAIN(); BENCHMARK_MAIN();
+2 -4
View File
@@ -25,8 +25,7 @@
namespace lms::core namespace lms::core
{ {
std::unique_ptr<IChildProcessManager> std::unique_ptr<IChildProcessManager> createChildProcessManager(boost::asio::io_context& ioContext)
createChildProcessManager(boost::asio::io_context& ioContext)
{ {
return std::make_unique<ChildProcessManager>(ioContext); return std::make_unique<ChildProcessManager>(ioContext);
} }
@@ -36,8 +35,7 @@ namespace lms::core
{ {
} }
std::unique_ptr<IChildProcess> std::unique_ptr<IChildProcess> ChildProcessManager::spawnChildProcess(const std::filesystem::path& path, const IChildProcess::Args& args)
ChildProcessManager::spawnChildProcess(const std::filesystem::path& path, const IChildProcess::Args& args)
{ {
return std::make_unique<ChildProcess>(_ioContext, path, args); return std::make_unique<ChildProcess>(_ioContext, path, args);
} }
+114 -15
View File
@@ -68,7 +68,16 @@ namespace lms::core::tracing
for (Buffer& buffer : _buffers) for (Buffer& buffer : _buffers)
_freeBuffers.push_back(&buffer); _freeBuffers.push_back(&buffer);
LMS_LOG(UTILS, INFO, "TraceLogger: using " << _buffers.size() << " buffers. Buffer size = " << std::to_string(BufferSize)); LMS_LOG(UTILS, INFO, "TraceLogger: using " << _buffers.size() << " buffers. Buffer size = " << std::to_string(BufferSize) << ", entry size = " << sizeof(CompleteEventEntry) << ", entry count per buffer = " << Buffer::CompleteEventCount);
setMetadata("cpu_count", std::to_string(std::thread::hardware_concurrency()));
setMetadata("build_type",
#ifndef NDEBUG
"debug"
#else
"release"
#endif
);
} }
bool TraceLogger::isLevelActive(Level level) const bool TraceLogger::isLevelActive(Level level) const
@@ -81,7 +90,12 @@ namespace lms::core::tracing
if (!_currentBuffer) if (!_currentBuffer)
_currentBuffer = acquireBuffer(); _currentBuffer = acquireBuffer();
_currentBuffer->durationEvents[_currentBuffer->currentDurationIndex] = event; CompleteEventEntry& entry{ _currentBuffer->durationEvents[_currentBuffer->currentDurationIndex] };
entry.start = event.start;
entry.duration = event.duration;
entry.name = event.name.c_str();
entry.category = event.category.c_str();
entry.arg = event.arg.value_or(invalidHash);
// update the index after writing the event, in case another thread wants to dump // update the index after writing the event, in case another thread wants to dump
if (++_currentBuffer->currentDurationIndex == _currentBuffer->durationEvents.size()) if (++_currentBuffer->currentDurationIndex == _currentBuffer->durationEvents.size())
@@ -110,6 +124,7 @@ namespace lms::core::tracing
// Empty new buffer only now (we want to keep history on released buffers since we dump them) // Empty new buffer only now (we want to keep history on released buffers since we dump them)
buffer->currentDurationIndex = 0; buffer->currentDurationIndex = 0;
buffer->threadId = std::this_thread::get_id();
return buffer; return buffer;
} }
@@ -141,7 +156,7 @@ namespace lms::core::tracing
os << "\t\t{ "; os << "\t\t{ ";
os << "\"name\" : \"thread_name\", "; os << "\"name\" : \"thread_name\", ";
os << "\"pid\" : 1, "; os << "\"pid\" : 1, ";
os << "\"tid\" : " << threadId << ", "; os << "\"tid\" : " << toTraceThreadId(threadId) << ", ";
os << "\"ph\" : \"M\", "; os << "\"ph\" : \"M\", ";
os << "\"args\" : { \"name\" : \"" + threadName + "\" }"; os << "\"args\" : { \"name\" : \"" + threadName + "\" }";
os << " }"; os << " }";
@@ -154,13 +169,15 @@ namespace lms::core::tracing
for (Buffer& buffer : _buffers) for (Buffer& buffer : _buffers)
{ {
const auto threadId{ toTraceThreadId(buffer.threadId) };
for (std::size_t i{}; i < buffer.currentDurationIndex; ++i) for (std::size_t i{}; i < buffer.currentDurationIndex; ++i)
{ {
// Looks like tracing viewer is not pleased when nested event start at the same timestamp // Looks like tracing viewer is not pleased when nested event start at the same timestamp
// Hence the double representation as the microsecond unit is not precise enough // Hence the double representation as the microsecond unit is not precise enough
using clockMicro = std::chrono::duration<double, std::micro>; using clockMicro = std::chrono::duration<double, std::micro>;
const CompleteEvent& event{ buffer.durationEvents[i] }; const CompleteEventEntry& event{ buffer.durationEvents[i] };
if (first) if (first)
first = false; first = false;
@@ -168,13 +185,26 @@ namespace lms::core::tracing
os << ", " << std::endl;; os << ", " << std::endl;;
os << "\t\t{ "; os << "\t\t{ ";
os << "\"name\" : \"" << event.name.c_str() << "\", "; os << "\"name\" : \"" << event.name << "\", ";
os << "\"cat\" : \"" << event.category.c_str() << "\", "; os << "\"cat\" : \"" << event.category << "\", ";
os << "\"pid\": 1, "; os << "\"pid\": 1, ";
os << "\"tid\" : " << event.threadId << ", "; os << "\"tid\" : " << threadId << ", ";
os << "\"ts\" : " << std::fixed << std::setprecision(3) << std::chrono::duration_cast<clockMicro>(event.start - _start).count() << ", "; os << "\"ts\" : " << std::fixed << std::setprecision(3) << std::chrono::duration_cast<clockMicro>(event.start - _start).count() << ", ";
os << "\"dur\" : " << std::fixed << std::setprecision(3) << std::chrono::duration_cast<clockMicro>(event.duration).count() << ", "; os << "\"dur\" : " << std::fixed << std::setprecision(3) << std::chrono::duration_cast<clockMicro>(event.duration).count() << ", ";
os << "\"ph\" : \"X\""; os << "\"ph\" : \"X\"";
if (event.arg != invalidHash)
{
ArgEntryMap::const_iterator itArgEntry;
{
std::shared_lock lock{ _argMutex };
itArgEntry = _argEntries.find(event.arg);
assert(itArgEntry != _argEntries.cend());
}
os << ", \"args\" : { \"" << itArgEntry->second.type.c_str() << "\" : \"";
stringUtils::writeJsonEscapedString(os, std::string_view{ itArgEntry->second.value });
os << "\" }";
}
os << " }"; os << " }";
} }
} }
@@ -182,14 +212,16 @@ namespace lms::core::tracing
os << std::endl; os << std::endl;
os << "\t]," << std::endl; os << "\t]," << std::endl;
os << "\t\"meta_cpu_count\" : " << std::thread::hardware_concurrency() << ", " << std::endl; {
os << "\t\"meta_build_type\" : "; std::scoped_lock lock{ _metadataMutex };
#ifndef NDEBUG for (const auto& [metadata, value] : _metadata)
os << "\"debug\""; {
#else os << "\t\"meta_" << metadata << "\": \"";
os << "\"release\""; stringUtils::writeJsonEscapedString(os, value);
#endif os << "\"," << std::endl;
os << std::endl; }
}
os << "\t\"meta_registered_arg_count\" : " << getRegisteredArgCount() << std::endl;
os << "}" << std::endl; os << "}" << std::endl;
} }
@@ -198,4 +230,71 @@ namespace lms::core::tracing
std::scoped_lock lock{ _threadNameMutex }; std::scoped_lock lock{ _threadNameMutex };
_threadNames.emplace(id, threadName); _threadNames.emplace(id, threadName);
} }
TraceLogger::ArgHashType TraceLogger::computeArgHash(LiteralString type, std::string_view value)
{
ArgHashType res{};
res ^= std::hash<std::string_view>{}(type.str());
res ^= std::hash<std::string_view>{}(value);
return res;
}
TraceLogger::ArgHashType TraceLogger::registerArg(LiteralString argType, std::string_view argValue)
{
const ArgHashType hash{ computeArgHash(argType, argValue) };
assert(hash != invalidHash);
{
const std::shared_lock lock{ _argMutex };
auto itArgEntry{ _argEntries.find(hash) };
if (itArgEntry != std::cend(_argEntries))
{
assert(itArgEntry->second.type == argType);
assert(itArgEntry->second.value == argValue);
return hash;
}
}
{
const std::unique_lock lock{ _argMutex };
auto itArgEntry{ _argEntries.find(hash) };
if (itArgEntry != std::cend(_argEntries))
{
assert(itArgEntry->second.type == argType);
assert(itArgEntry->second.value == argValue);
return hash;
}
_argEntries.emplace(hash, ArgEntry{ argType, std::string{ argValue } });
return hash;
}
}
void TraceLogger::setMetadata(std::string_view metadata, std::string_view value)
{
const std::scoped_lock lock{ _metadataMutex };
_metadata[std::string{ metadata }] = value;
}
std::size_t TraceLogger::getRegisteredArgCount() const
{
const std::shared_lock lock{ _argMutex };
return _argEntries.size();
}
std::uint32_t TraceLogger::toTraceThreadId(std::thread::id threadId)
{
// Pefetto UI does not accept 64bits thread ids
std::ostringstream oss;
oss << threadId;
std::istringstream iss{ oss.str() };
std::uint64_t id;
iss >> id;
return static_cast<std::uint32_t>(id);
}
} }
+37 -4
View File
@@ -21,10 +21,12 @@
#include <array> #include <array>
#include <deque> #include <deque>
#include <map>
#include <mutex> #include <mutex>
#include <vector> #include <shared_mutex>
#include <thread> #include <thread>
#include <unordered_map> #include <unordered_map>
#include <vector>
#include "core/ITraceLogger.hpp" #include "core/ITraceLogger.hpp"
@@ -42,14 +44,33 @@ namespace lms::core::tracing
void write(const CompleteEvent& event) override; void write(const CompleteEvent& event) override;
void dumpCurrentBuffer(std::ostream& os) override; void dumpCurrentBuffer(std::ostream& os) override;
void setThreadName(std::thread::id id, std::string_view threadName) override; void setThreadName(std::thread::id id, std::string_view threadName) override;
ArgHashType registerArg(LiteralString argType, std::string_view argValue) override;
void setMetadata(std::string_view metadata, std::string_view value) override;
static constexpr std::size_t BufferSize{ 32 * 1024 }; std::size_t getRegisteredArgCount() const;
static ArgHashType computeArgHash(LiteralString type, std::string_view value);
static std::uint32_t toTraceThreadId(std::thread::id threadId);
static constexpr std::size_t BufferSize{ 64 * 1024 };
// Same as ComplteEvent, but compacted
struct CompleteEventEntry
{
clock::time_point start;
clock::duration duration;
const char* name;
const char* category;
ArgHashType arg;
};
static constexpr ArgHashType invalidHash{ 0 };
struct alignas(64) Buffer struct alignas(64) Buffer
{ {
static constexpr std::size_t CompleteEventCount{ BufferSize / sizeof(CompleteEvent) }; static constexpr std::size_t CompleteEventCount{ BufferSize / sizeof(CompleteEventEntry) };
std::array<CompleteEvent, CompleteEventCount> durationEvents; std::thread::id threadId;
std::array<CompleteEventEntry, CompleteEventCount> durationEvents;
std::atomic<std::size_t> currentDurationIndex{}; std::atomic<std::size_t> currentDurationIndex{};
}; };
@@ -62,9 +83,21 @@ namespace lms::core::tracing
std::vector<Buffer> _buffers; // allocated once during construction std::vector<Buffer> _buffers; // allocated once during construction
mutable std::shared_mutex _argMutex;
struct ArgEntry
{
LiteralString type;
std::string value;
};
using ArgEntryMap = std::unordered_map<ArgHashType, ArgEntry>;
ArgEntryMap _argEntries; // collisions not handled
std::mutex _threadNameMutex; std::mutex _threadNameMutex;
std::unordered_map<std::thread::id, std::string> _threadNames; std::unordered_map<std::thread::id, std::string> _threadNames;
std::mutex _metadataMutex;
std::map<std::string, std::string> _metadata;
std::mutex _mutex; std::mutex _mutex;
std::deque<Buffer*> _freeBuffers; std::deque<Buffer*> _freeBuffers;
+19 -7
View File
@@ -21,6 +21,7 @@
#include <chrono> #include <chrono>
#include <memory> #include <memory>
#include <optional>
#include <ostream> #include <ostream>
#include <string_view> #include <string_view>
#include <thread> #include <thread>
@@ -34,14 +35,19 @@
#define LMS_CONCAT(x, y) LMS_CONCAT_IMPL(x, y) #define LMS_CONCAT(x, y) LMS_CONCAT_IMPL(x, y)
#if LMS_SUPPORT_TRACING #if LMS_SUPPORT_TRACING
#define LMS_SCOPED_TRACE(CATEGORY, LEVEL, NAME) ::lms::core::tracing::ScopedTrace LMS_CONCAT(ScopedTrace_, __LINE__){ CATEGORY, LEVEL, NAME } #define LMS_SCOPED_TRACE(CATEGORY, LEVEL, NAME, ARGTYPE, ARGVALUE) \
std::optional<::lms::core::tracing::ScopedTrace> LMS_CONCAT(ScopedTrace_, __LINE__); \
if (::lms::core::tracing::ITraceLogger* traceLogger{ ::lms::core::Service<::lms::core::tracing::ITraceLogger>::get() }; traceLogger && traceLogger->isLevelActive(LEVEL)) \
LMS_CONCAT(ScopedTrace_, __LINE__).emplace(CATEGORY, LEVEL, NAME, ARGTYPE, ARGVALUE, traceLogger);
#else #else
#define LMS_SCOPED_TRACE(CATEGORY, LEVEL, NAME) (void)0 #define LMS_SCOPED_TRACE(CATEGORY, LEVEL, NAME) (void)0
#endif #endif
#define LMS_SCOPED_TRACE_OVERVIEW(CATEGORY, NAME) LMS_SCOPED_TRACE(CATEGORY, ::lms::core::tracing::Level::Overview, NAME) #define LMS_SCOPED_TRACE_OVERVIEW_WITH_ARG(CATEGORY, NAME, ARGTYPE, ARGVALUE) LMS_SCOPED_TRACE(CATEGORY, ::lms::core::tracing::Level::Overview, NAME, ARGTYPE, ARGVALUE)
#define LMS_SCOPED_TRACE_DETAILED(CATEGORY, NAME) LMS_SCOPED_TRACE(CATEGORY, ::lms::core::tracing::Level::Detailed, NAME) #define LMS_SCOPED_TRACE_DETAILED_WITH_ARG(CATEGORY, NAME, ARGTYPE, ARGVALUE) LMS_SCOPED_TRACE(CATEGORY, ::lms::core::tracing::Level::Detailed, NAME, ARGTYPE, ARGVALUE)
#define LMS_SCOPED_TRACE_OVERVIEW(CATEGORY, NAME) LMS_SCOPED_TRACE_OVERVIEW_WITH_ARG(CATEGORY, NAME, "", "")
#define LMS_SCOPED_TRACE_DETAILED(CATEGORY, NAME) LMS_SCOPED_TRACE_DETAILED_WITH_ARG(CATEGORY, NAME, "", "")
namespace lms::core::tracing namespace lms::core::tracing
{ {
@@ -56,13 +62,15 @@ namespace lms::core::tracing
class ITraceLogger class ITraceLogger
{ {
public: public:
using ArgHashType = std::size_t;
struct CompleteEvent struct CompleteEvent
{ {
clock::time_point start; clock::time_point start;
clock::duration duration; clock::duration duration;
std::thread::id threadId;
LiteralString name; LiteralString name;
LiteralString category; LiteralString category;
std::optional<ArgHashType> arg;
}; };
virtual ~ITraceLogger() = default; virtual ~ITraceLogger() = default;
@@ -71,6 +79,9 @@ namespace lms::core::tracing
virtual void write(const CompleteEvent& entry) = 0; virtual void write(const CompleteEvent& entry) = 0;
virtual void dumpCurrentBuffer(std::ostream& os) = 0; virtual void dumpCurrentBuffer(std::ostream& os) = 0;
virtual void setThreadName(std::thread::id id, std::string_view threadName) = 0; virtual void setThreadName(std::thread::id id, std::string_view threadName) = 0;
virtual void setMetadata(std::string_view metadata, std::string_view value) = 0;
virtual ArgHashType registerArg(LiteralString argType, std::string_view argValue) = 0;
}; };
static constexpr std::size_t MinBufferSizeInMBytes = 16; static constexpr std::size_t MinBufferSizeInMBytes = 16;
@@ -79,16 +90,17 @@ namespace lms::core::tracing
class ScopedTrace class ScopedTrace
{ {
public: public:
ScopedTrace(LiteralString category, Level level, LiteralString name, ITraceLogger* traceLogger = Service<ITraceLogger>::get()) ScopedTrace(LiteralString category, Level level, LiteralString name, LiteralString argType = {}, std::string_view argValue = {}, ITraceLogger* traceLogger = Service<ITraceLogger>::get())
{ {
if (traceLogger && traceLogger->isLevelActive(level)) if (traceLogger && traceLogger->isLevelActive(level))
{ {
_traceLogger = traceLogger; _traceLogger = traceLogger;
_event.start = clock::now(); _event.start = clock::now();
_event.threadId = std::this_thread::get_id();
_event.name = name; _event.name = name;
_event.category = category; _event.category = category;
if (!argType.empty() && !argValue.empty())
_event.arg = traceLogger->registerArg(argType, argValue);
} }
else else
{ {
@@ -20,6 +20,7 @@
#pragma once #pragma once
#include <cstddef> #include <cstddef>
#include <ostream>
#include <string> #include <string>
#include <string_view> #include <string_view>
@@ -32,6 +33,7 @@ namespace lms::core
template<std::size_t N> template<std::size_t N>
constexpr LiteralString(const char(&str)[N]) noexcept : _str{ str, N - 1 } { static_assert(N > 0); } constexpr LiteralString(const char(&str)[N]) noexcept : _str{ str, N - 1 } { static_assert(N > 0); }
constexpr bool empty() const noexcept { return _str.empty(); }
constexpr const char* c_str() const noexcept { return _str.data(); } constexpr const char* c_str() const noexcept { return _str.data(); }
constexpr std::size_t length() const noexcept { return _str.length(); } constexpr std::size_t length() const noexcept { return _str.length(); }
constexpr std::string_view str() const noexcept { return _str; } constexpr std::string_view str() const noexcept { return _str; }
@@ -40,6 +42,12 @@ namespace lms::core
private: private:
std::string_view _str; std::string_view _str;
}; };
inline std::ostream& operator<<(std::ostream& os, const LiteralString& str)
{
os << str.str();
return os;
}
} }
namespace std namespace std
+9 -2
View File
@@ -35,8 +35,8 @@ namespace lms::core::tracing::tests
{ {
threads.emplace_back([&] threads.emplace_back([&]
{ {
ScopedTrace loggedEvent{ "MyCategory", Level::Overview, "MyEventLogged", traceLogger.get() }; ScopedTrace loggedEvent{ "MyCategory", Level::Overview, "MyEventLogged", "SomeArgType", "SomeArg", traceLogger.get() };
ScopedTrace notLoggedEvent{ "MyCategory", Level::Detailed, "MyEventNotLogged", traceLogger.get() }; ScopedTrace notLoggedEvent{ "MyNotLoggedCategory", Level::Detailed, "MyEventNotLogged", "SomeNotLoggedArgType", "SomeNotLoggedArg", traceLogger.get() };
}); });
} }
@@ -47,6 +47,13 @@ namespace lms::core::tracing::tests
traceLogger->dumpCurrentBuffer(oss); traceLogger->dumpCurrentBuffer(oss);
EXPECT_NE(oss.str().find("MyEventLogged"), std::string::npos); EXPECT_NE(oss.str().find("MyEventLogged"), std::string::npos);
EXPECT_NE(oss.str().find("MyCategory"), std::string::npos);
EXPECT_NE(oss.str().find("SomeArgType"), std::string::npos);
EXPECT_NE(oss.str().find("SomeArg"), std::string::npos);
EXPECT_EQ(oss.str().find("MyEventNotLogged"), std::string::npos); EXPECT_EQ(oss.str().find("MyEventNotLogged"), std::string::npos);
EXPECT_EQ(oss.str().find("MyNotLoggedCategory"), std::string::npos);
EXPECT_EQ(oss.str().find("SomeNotLoggedArgType"), std::string::npos);
EXPECT_EQ(oss.str().find("SomeNotLoggedArg"), std::string::npos);
} }
} }
+79 -52
View File
@@ -40,7 +40,8 @@ namespace lms::db
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ session.getDboSession().query<ResultType>("SELECT DISTINCT " + std::string{ itemToSelect } + " FROM artist a") }; // TODO remove distinct and use group by
auto query{ session.getDboSession()->query<ResultType>("SELECT " + std::string{ itemToSelect } + " FROM artist a") };
if (params.sortMethod == ArtistSortMethod::LastWritten if (params.sortMethod == ArtistSortMethod::LastWritten
|| params.writtenAfter.isValid() || params.writtenAfter.isValid()
|| params.linkType || params.linkType
@@ -49,28 +50,41 @@ namespace lms::db
|| params.clusters.size() == 1 || params.clusters.size() == 1
|| params.mediaLibrary.isValid()) || params.mediaLibrary.isValid())
{ {
query.join("track t ON t.id = t_a_l.track_id");
query.join("track_artist_link t_a_l ON t_a_l.artist_id = a.id"); query.join("track_artist_link t_a_l ON t_a_l.artist_id = a.id");
} }
if (params.linkType) if (params.sortMethod == ArtistSortMethod::LastWritten
query.where("t_a_l.type = ?").bind(*params.linkType); || params.writtenAfter.isValid()
|| params.release.isValid()
|| params.mediaLibrary.isValid())
{
query.join("track t ON t.id = t_a_l.track_id");
if (params.writtenAfter.isValid()) if (params.writtenAfter.isValid())
query.where("t.file_last_write > ?").bind(params.writtenAfter); query.where("t.file_last_write > ?").bind(params.writtenAfter);
if (params.release.isValid())
query.where("t.release_id = ?").bind(params.release);
if (params.mediaLibrary.isValid())
query.where("t.media_library_id = ?").bind(params.mediaLibrary);
}
if (params.linkType)
query.where("+t_a_l.type = ?").bind(*params.linkType); // Exclude this since the query planner does not do a good job when db is not analyzed
if (!params.keywords.empty()) if (!params.keywords.empty())
{ {
std::vector<std::string> clauses; std::vector<std::string> clauses;
std::vector<std::string> sortClauses; std::vector<std::string> sortClauses;
for (std::string_view keyword : params.keywords) for (const std::string_view keyword : params.keywords)
{ {
clauses.push_back("a.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'"); clauses.push_back("a.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'");
query.bind("%" + utils::escapeLikeKeyword(keyword) + "%"); query.bind("%" + utils::escapeLikeKeyword(keyword) + "%");
} }
for (std::string_view keyword : params.keywords) for (const std::string_view keyword : params.keywords)
{ {
sortClauses.push_back("a.sort_name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'"); sortClauses.push_back("a.sort_name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'");
query.bind("%" + utils::escapeLikeKeyword(keyword) + "%"); query.bind("%" + utils::escapeLikeKeyword(keyword) + "%");
@@ -90,48 +104,42 @@ namespace lms::db
if (params.clusters.size() == 1) if (params.clusters.size() == 1)
{ {
query.join("track_cluster t_c ON t_c.track_id = t.id") query.join("track_cluster t_c ON t_c.track_id = t_a_l.track_id")
.where("t_c.cluster_id = ?").bind(params.clusters.front()); .where("t_c.cluster_id = ?").bind(params.clusters.front());
} }
else if (params.clusters.size() > 1) else if (params.clusters.size() > 1)
{ {
std::ostringstream oss; std::ostringstream oss;
oss << "a.id IN (SELECT DISTINCT a.id FROM artist a" oss << "a.id IN (SELECT DISTINCT t_a_l.artist_id FROM track_artist_link t_a_l"
" INNER JOIN track t ON t.id = t_a_l.track_id" " INNER JOIN track_cluster t_c ON t_c.track_id = t_a_l.track_id";
" INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id"
" INNER JOIN cluster c ON c.id = t_c.cluster_id"
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
WhereClause clusterClause; WhereClause clusterClause;
for (const ClusterId clusterId : params.clusters) for (const ClusterId clusterId : params.clusters)
{ {
clusterClause.Or(WhereClause("c.id = ?")); clusterClause.Or(WhereClause("t_c.cluster_id = ?"));
query.bind(clusterId); query.bind(clusterId);
} }
oss << " " << clusterClause.get(); oss << " " << clusterClause.get();
oss << " GROUP BY t.id,a.id HAVING COUNT(DISTINCT c.id) = " << params.clusters.size() << ")"; oss << " GROUP BY t_a_l.track_id,t_a_l.artist_id HAVING COUNT(DISTINCT t_c.cluster_id) = " << params.clusters.size() << ")";
query.where(oss.str()); query.where(oss.str());
} }
if (params.track.isValid()) if (params.track.isValid())
query.where("t.id = ?").bind(params.track); query.where("t_a_l.track_id = ?").bind(params.track);
if (params.release.isValid())
query.where("t.release_id = ?").bind(params.release);
if (params.mediaLibrary.isValid())
query.where("t.media_library_id = ?").bind(params.mediaLibrary);
switch (params.sortMethod) switch (params.sortMethod)
{ {
case ArtistSortMethod::None: case ArtistSortMethod::None:
break; break;
case ArtistSortMethod::ByName: case ArtistSortMethod::Id:
query.orderBy("a.id");
break;
case ArtistSortMethod::Name:
query.orderBy("a.name COLLATE NOCASE"); query.orderBy("a.name COLLATE NOCASE");
break; break;
case ArtistSortMethod::BySortName: case ArtistSortMethod::SortName:
query.orderBy("a.sort_name COLLATE NOCASE"); query.orderBy("a.sort_name COLLATE NOCASE");
break; break;
case ArtistSortMethod::Random: case ArtistSortMethod::Random:
@@ -146,6 +154,8 @@ namespace lms::db
break; break;
} }
query.groupBy("a.id");
return query; return query;
} }
@@ -153,7 +163,7 @@ namespace lms::db
Wt::Dbo::Query<ResultType> createQuery(Session& session, const Artist::FindParameters& params) Wt::Dbo::Query<ResultType> createQuery(Session& session, const Artist::FindParameters& params)
{ {
std::string_view itemToSelect; std::string_view itemToSelect;
if constexpr (std::is_same_v<ResultType, ArtistId>) if constexpr (std::is_same_v<ResultType, ArtistId>)
itemToSelect = "a.id"; itemToSelect = "a.id";
else if constexpr (std::is_same_v<ResultType, Wt::Dbo::ptr<Artist>>) else if constexpr (std::is_same_v<ResultType, Wt::Dbo::ptr<Artist>>)
@@ -174,51 +184,70 @@ namespace lms::db
Artist::pointer Artist::create(Session& session, const std::string& name, const std::optional<core::UUID>& MBID) Artist::pointer Artist::create(Session& session, const std::string& name, const std::optional<core::UUID>& MBID)
{ {
return session.getDboSession().add(std::unique_ptr<Artist>{ new Artist{ name, MBID } }); return session.getDboSession()->add(std::unique_ptr<Artist>{ new Artist{ name, MBID } });
} }
std::size_t Artist::getCount(Session& session) std::size_t Artist::getCount(Session& session)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().query<int>("SELECT COUNT(*) FROM artist"); return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM artist"));
}
void Artist::find(Session& session, ArtistId& lastRetrievedArtist, std::size_t count, const std::function<void(const Artist::pointer&)>& func, MediaLibraryId library)
{
session.checkReadTransaction();
auto query{ session.getDboSession()->query<Wt::Dbo::ptr<Artist>>("SELECT a FROM artist a")
.orderBy("a.id")
.where("a.id > ?").bind(lastRetrievedArtist)
.limit(static_cast<int>(count)) };
if (library.isValid())
{
// Faster than using joins
query.where("EXISTS (SELECT 1 FROM track_artist_link t_a_l JOIN track t ON t.id = t_a_l.track_id WHERE t_a_l.artist_id = a.id AND t.media_library_id = ?)").bind(library);
}
utils::forEachQueryResult(query, [&](const Artist::pointer& artist)
{
func(artist);
lastRetrievedArtist = artist->getId();
});
} }
std::vector<Artist::pointer> Artist::find(Session& session, std::string_view name) std::vector<Artist::pointer> Artist::find(Session& session, std::string_view name)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> res = session.getDboSession().find<Artist>() return utils::fetchQueryResults<Artist::pointer>(session.getDboSession()->find<Artist>()
.where("name = ?").bind(std::string{ name, 0, _maxNameLength }) .where("name = ?").bind(std::string{ name, 0, _maxNameLength })
.orderBy("LENGTH(mbid) DESC"); // put mbid entries first .orderBy("LENGTH(mbid) DESC")); // put mbid entries first
return std::vector<Artist::pointer>(res.begin(), res.end());
} }
Artist::pointer Artist::find(Session& session, const core::UUID& mbid) Artist::pointer Artist::find(Session& session, const core::UUID& mbid)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().find<Artist>().where("mbid = ?").bind(std::string{ mbid.getAsString() }).resultValue(); return utils::fetchQuerySingleResult(session.getDboSession()->find<Artist>().where("mbid = ?").bind(std::string{ mbid.getAsString() }));
} }
Artist::pointer Artist::find(Session& session, ArtistId id) Artist::pointer Artist::find(Session& session, ArtistId id)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().find<Artist>().where("id = ?").bind(id).resultValue(); return utils::fetchQuerySingleResult(session.getDboSession()->find<Artist>().where("id = ?").bind(id));
} }
bool Artist::exists(Session& session, ArtistId id) bool Artist::exists(Session& session, ArtistId id)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().query<int>("SELECT 1 FROM artist").where("id = ?").bind(id).resultValue() == 1; 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) RangeResults<ArtistId> Artist::findOrphanIds(Session& session, std::optional<Range> range)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ session.getDboSession().query<ArtistId>("SELECT DISTINCT a.id FROM artist a WHERE NOT EXISTS(SELECT 1 FROM track t INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id WHERE t.id = t_a_l.track_id)") }; auto query{ session.getDboSession()->query<ArtistId>("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)") };
return utils::execQuery<ArtistId>(query, range); return utils::execRangeQuery<ArtistId>(query, range);
} }
RangeResults<ArtistId> Artist::findIds(Session& session, const FindParameters& params) RangeResults<ArtistId> Artist::findIds(Session& session, const FindParameters& params)
@@ -226,7 +255,7 @@ namespace lms::db
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ createQuery<ArtistId>(session, params) }; auto query{ createQuery<ArtistId>(session, params) };
return utils::execQuery<ArtistId>(query, params.range); return utils::execRangeQuery<ArtistId>(query, params.range);
} }
RangeResults<Artist::pointer> Artist::find(Session& session, const FindParameters& params) RangeResults<Artist::pointer> Artist::find(Session& session, const FindParameters& params)
@@ -234,7 +263,7 @@ namespace lms::db
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ createQuery<Wt::Dbo::ptr<Artist>>(session, params) }; auto query{ createQuery<Wt::Dbo::ptr<Artist>>(session, params) };
return utils::execQuery<Artist::pointer>(query, params.range); return utils::execRangeQuery<Artist::pointer>(query, params.range);
} }
void Artist::find(Session& session, const FindParameters& params, std::function<void(const pointer&)> func) void Artist::find(Session& session, const FindParameters& params, std::function<void(const pointer&)> func)
@@ -242,7 +271,7 @@ namespace lms::db
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ createQuery<Wt::Dbo::ptr<Artist>>(session, params) }; auto query{ createQuery<Wt::Dbo::ptr<Artist>>(session, params) };
utils::execQuery(query, params.range, func); utils::forEachQueryRangeResult(query, params.range, func);
} }
RangeResults<ArtistId> Artist::findSimilarArtistIds(core::EnumSet<TrackArtistLinkType> artistLinkTypes, std::optional<Range> range) const RangeResults<ArtistId> Artist::findSimilarArtistIds(core::EnumSet<TrackArtistLinkType> artistLinkTypes, std::optional<Range> range) const
@@ -286,10 +315,10 @@ namespace lms::db
.groupBy("a.id") .groupBy("a.id")
.orderBy("COUNT(*) DESC, RANDOM()") }; .orderBy("COUNT(*) DESC, RANDOM()") };
for (TrackArtistLinkType type : artistLinkTypes) for (const TrackArtistLinkType type : artistLinkTypes)
query.bind(type); query.bind(type);
return utils::execQuery<ArtistId>(query, range); return utils::execRangeQuery<ArtistId>(query, range);
} }
std::vector<std::vector<Cluster::pointer>> Artist::getClusterGroups(std::vector<ClusterTypeId> clusterTypeIds, std::size_t size) const std::vector<std::vector<Cluster::pointer>> Artist::getClusterGroups(std::vector<ClusterTypeId> clusterTypeIds, std::size_t size) const
@@ -304,7 +333,7 @@ namespace lms::db
where.And(WhereClause("a.id = ?")).bind(getId().toString()); where.And(WhereClause("a.id = ?")).bind(getId().toString());
{ {
WhereClause clusterClause; WhereClause clusterClause;
for (ClusterTypeId clusterTypeId : clusterTypeIds) for (const ClusterTypeId clusterTypeId : clusterTypeIds)
clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterTypeId.toString()); clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterTypeId.toString());
where.And(clusterClause); where.And(clusterClause);
@@ -312,19 +341,17 @@ namespace lms::db
oss << " " << where.get(); oss << " " << where.get();
oss << "GROUP BY c.id ORDER BY COUNT(DISTINCT c.id) DESC"; oss << "GROUP BY c.id ORDER BY COUNT(DISTINCT c.id) DESC";
Wt::Dbo::Query<Wt::Dbo::ptr<Cluster>> query = session()->query<Wt::Dbo::ptr<Cluster>>(oss.str()); Wt::Dbo::Query<Wt::Dbo::ptr<Cluster>> query{ session()->query<Wt::Dbo::ptr<Cluster>>(oss.str()) };
for (const std::string& bindArg : where.getBindArgs()) for (const std::string& bindArg : where.getBindArgs())
query.bind(bindArg); query.bind(bindArg);
Wt::Dbo::collection<Wt::Dbo::ptr<Cluster>> queryRes = query;
std::map<ClusterTypeId, std::vector<Cluster::pointer>> clustersByType; std::map<ClusterTypeId, std::vector<Cluster::pointer>> clustersByType;
for (Cluster::pointer cluster : queryRes) utils::forEachQueryResult(query, [&](const Cluster::pointer& cluster)
{ {
if (clustersByType[cluster->getType()->getId()].size() < size) if (clustersByType[cluster->getType()->getId()].size() < size)
clustersByType[cluster->getType()->getId()].push_back(cluster); clustersByType[cluster->getType()->getId()].push_back(cluster);
} });
std::vector<std::vector<Cluster::pointer>> res; std::vector<std::vector<Cluster::pointer>> res;
for (const auto& [clusterTypeId, clusters] : clustersByType) for (const auto& [clusterTypeId, clusters] : clustersByType)
+21 -26
View File
@@ -24,38 +24,33 @@
#include "database/User.hpp" #include "database/User.hpp"
#include "StringViewTraits.hpp" #include "StringViewTraits.hpp"
#include "IdTypeTraits.hpp" #include "IdTypeTraits.hpp"
#include "Utils.hpp"
namespace lms::db namespace lms::db
{ {
AuthToken::AuthToken(std::string_view value, const Wt::WDateTime& expiry, ObjectPtr<User> user)
: _value{ value }
, _expiry{ expiry }
, _user{ getDboPtr(user) }
{
}
AuthToken::AuthToken(std::string_view value, const Wt::WDateTime& expiry, ObjectPtr<User> user) AuthToken::pointer AuthToken::create(Session& session, std::string_view value, const Wt::WDateTime& expiry, ObjectPtr<User> user)
: _value {value} {
, _expiry {expiry} return session.getDboSession()->add(std::unique_ptr<AuthToken> {new AuthToken{ value, expiry, user }});
, _user {getDboPtr(user)} }
{
}
AuthToken::pointer void AuthToken::removeExpiredTokens(Session& session, const Wt::WDateTime& now)
AuthToken::create(Session& session, std::string_view value, const Wt::WDateTime& expiry, ObjectPtr<User> user) {
{ session.checkWriteTransaction();
return session.getDboSession().add(std::unique_ptr<AuthToken> {new AuthToken {value, expiry, user}});
}
void session.getDboSession()->execute("DELETE FROM auth_token WHERE expiry < ?").bind(now);
AuthToken::removeExpiredTokens(Session& session, const Wt::WDateTime& now) }
{
session.checkWriteTransaction();
session.getDboSession().execute("DELETE FROM auth_token WHERE expiry < ?").bind(now); AuthToken::pointer AuthToken::find(Session& session, std::string_view value)
} {
session.checkReadTransaction();
AuthToken::pointer return utils::fetchQuerySingleResult(session.getDboSession()->find<AuthToken>().where("value = ?").bind(value));
AuthToken::find(Session& session, std::string_view value) }
{
session.checkReadTransaction();
return session.getDboSession().find<AuthToken>()
.where("value = ?").bind(value)
.resultValue();
}
} }
+40 -34
View File
@@ -26,6 +26,7 @@
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/Track.hpp" #include "database/Track.hpp"
#include "IdTypeTraits.hpp" #include "IdTypeTraits.hpp"
#include "StringViewTraits.hpp"
#include "SqlQuery.hpp" #include "SqlQuery.hpp"
#include "Utils.hpp" #include "Utils.hpp"
@@ -38,7 +39,8 @@ namespace lms::db
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ session.getDboSession().query<ResultType>("SELECT DISTINCT " + std::string{ itemToSelect } + " FROM cluster c") }; auto query{ session.getDboSession()->query<ResultType>("SELECT " + std::string{ itemToSelect } + " FROM cluster c") };
query.groupBy("c.id");
if (params.track.isValid() || params.release.isValid()) if (params.track.isValid() || params.release.isValid())
{ {
@@ -55,7 +57,7 @@ namespace lms::db
assert(!params.clusterType.isValid() || params.clusterTypeName.empty()); assert(!params.clusterType.isValid() || params.clusterTypeName.empty());
if (params.clusterType.isValid()) if (params.clusterType.isValid())
query.where("c.cluster_type_id = ?").bind(params.clusterType); query.where("+c.cluster_type_id = ?").bind(params.clusterType); // Exclude this since the query planner does not do a good job when db is not analyzed
else if (!params.clusterTypeName.empty()) else if (!params.clusterTypeName.empty())
query.where("c_t.name = ?").bind(params.clusterTypeName); query.where("c_t.name = ?").bind(params.clusterTypeName);
@@ -66,7 +68,7 @@ namespace lms::db
Wt::Dbo::Query<ResultType> createQuery(Session& session, const Cluster::FindParameters& params) Wt::Dbo::Query<ResultType> createQuery(Session& session, const Cluster::FindParameters& params)
{ {
std::string_view itemToSelect; std::string_view itemToSelect;
if constexpr (std::is_same_v<ResultType, ClusterId>) if constexpr (std::is_same_v<ResultType, ClusterId>)
itemToSelect = "c.id"; itemToSelect = "c.id";
else if constexpr (std::is_same_v<ResultType, Wt::Dbo::ptr<Cluster>>) else if constexpr (std::is_same_v<ResultType, Wt::Dbo::ptr<Cluster>>)
@@ -86,22 +88,22 @@ namespace lms::db
Cluster::pointer Cluster::create(Session& session, ObjectPtr<ClusterType> type, std::string_view name) Cluster::pointer Cluster::create(Session& session, ObjectPtr<ClusterType> type, std::string_view name)
{ {
return session.getDboSession().add(std::unique_ptr<Cluster> {new Cluster{ type, name }}); return session.getDboSession()->add(std::unique_ptr<Cluster> {new Cluster{ type, name }});
} }
std::size_t Cluster::getCount(Session& session) std::size_t Cluster::getCount(Session& session)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().query<int>("SELECT COUNT(*) FROM cluster"); return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM cluster"));
} }
RangeResults<ClusterId> Cluster::findIds(Session& session, const FindParameters& params) RangeResults<ClusterId> Cluster::findIds(Session& session, const FindParameters& params)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ createQuery<ClusterId>(session, params) }; auto query{ createQuery<ClusterId>(session, params) };
return utils::execQuery<ClusterId>(query, params.range); return utils::execRangeQuery<ClusterId>(query, params.range);
} }
RangeResults<Cluster::pointer> Cluster::find(Session& session, const FindParameters& params) RangeResults<Cluster::pointer> Cluster::find(Session& session, const FindParameters& params)
@@ -109,38 +111,46 @@ namespace lms::db
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ createQuery<Wt::Dbo::ptr<Cluster>>(session, params) }; auto query{ createQuery<Wt::Dbo::ptr<Cluster>>(session, params) };
return utils::execQuery<Cluster::pointer>(query, params.range); return utils::execRangeQuery<Cluster::pointer>(query, params.range);
}
void Cluster::find(Session& session, const FindParameters& params, std::function<void(const pointer& cluster)> _func)
{
session.checkReadTransaction();
auto query{ createQuery<Wt::Dbo::ptr<Cluster>>(session, params) };
return utils::forEachQueryResult(query, _func);
} }
RangeResults<ClusterId> Cluster::findOrphanIds(Session& session, std::optional<Range> range) RangeResults<ClusterId> Cluster::findOrphanIds(Session& session, std::optional<Range> range)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ session.getDboSession().query<ClusterId>("SELECT DISTINCT c.id FROM cluster c WHERE NOT EXISTS(SELECT 1 FROM track_cluster t_c WHERE t_c.cluster_id = c.id)") }; auto query{ session.getDboSession()->query<ClusterId>("SELECT DISTINCT c.id FROM cluster c WHERE NOT EXISTS(SELECT 1 FROM track_cluster t_c WHERE t_c.cluster_id = c.id)") };
return utils::execQuery<ClusterId>(query, range); return utils::execRangeQuery<ClusterId>(query, range);
} }
Cluster::pointer Cluster::find(Session& session, ClusterId id) Cluster::pointer Cluster::find(Session& session, ClusterId id)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().find<Cluster>().where("id = ?").bind(id).resultValue(); return utils::fetchQuerySingleResult(session.getDboSession()->find<Cluster>().where("id = ?").bind(id));
} }
std::size_t Cluster::computeTrackCount(Session& session, ClusterId id) std::size_t Cluster::computeTrackCount(Session& session, ClusterId id)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().query<int>("SELECT COUNT(t.id) FROM track t INNER JOIN track_cluster t_c ON t_c.track_id = t.id") return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(t.id) FROM track t INNER JOIN track_cluster t_c ON t_c.track_id = t.id")
.where("t_c.cluster_id = ?").bind(id).resultValue(); .where("t_c.cluster_id = ?").bind(id));
} }
std::size_t Cluster::computeReleaseCount(Session& session, ClusterId id) std::size_t Cluster::computeReleaseCount(Session& session, ClusterId id)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().query<int>("SELECT COUNT(DISTINCT r.id) FROM release r INNER JOIN track t on t.release_id = r.id INNER JOIN track_cluster t_c ON t_c.track_id = t.id") return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(DISTINCT r.id) FROM release r INNER JOIN track t on t.release_id = r.id INNER JOIN track_cluster t_c ON t_c.track_id = t.id")
.where("t_c.cluster_id = ?").bind(id).resultValue(); .where("t_c.cluster_id = ?").bind(id));
} }
void Cluster::addTrack(ObjectPtr<Track> track) void Cluster::addTrack(ObjectPtr<Track> track)
@@ -155,7 +165,7 @@ namespace lms::db
auto query{ session()->query<TrackId>("SELECT t.id FROM track t INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id") auto query{ session()->query<TrackId>("SELECT t.id FROM track t INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id")
.where("c.id = ?").bind(getId()) }; .where("c.id = ?").bind(getId()) };
return utils::execQuery<TrackId>(query, range); return utils::execRangeQuery<TrackId>(query, range);
} }
ClusterType::ClusterType(std::string_view name) ClusterType::ClusterType(std::string_view name)
@@ -165,61 +175,60 @@ namespace lms::db
ClusterType::pointer ClusterType::create(Session& session, std::string_view name) ClusterType::pointer ClusterType::create(Session& session, std::string_view name)
{ {
return session.getDboSession().add(std::unique_ptr<ClusterType> {new ClusterType{ name }}); return session.getDboSession()->add(std::unique_ptr<ClusterType> {new ClusterType{ name }});
} }
std::size_t ClusterType::getCount(Session& session) std::size_t ClusterType::getCount(Session& session)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().query<int>("SELECT COUNT(*) FROM cluster_type"); return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM cluster_type"));
} }
RangeResults<ClusterTypeId> ClusterType::findOrphanIds(Session& session, std::optional<Range> range) RangeResults<ClusterTypeId> ClusterType::findOrphanIds(Session& session, std::optional<Range> range)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ session.getDboSession().query<ClusterTypeId>( auto query{ session.getDboSession()->query<ClusterTypeId>(
"SELECT c_t.id from cluster_type c_t" "SELECT c_t.id from cluster_type c_t"
" LEFT OUTER JOIN cluster c ON c_t.id = c.cluster_type_id") " LEFT OUTER JOIN cluster c ON c_t.id = c.cluster_type_id")
.where("c.id IS NULL") }; .where("c.id IS NULL") };
return utils::execQuery<ClusterTypeId>(query, range); return utils::execRangeQuery<ClusterTypeId>(query, range);
} }
RangeResults<ClusterTypeId> ClusterType::findUsed(Session& session, std::optional<Range> range) RangeResults<ClusterTypeId> ClusterType::findUsed(Session& session, std::optional<Range> range)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ session.getDboSession().query<ClusterTypeId>( auto query{ session.getDboSession()->query<ClusterTypeId>(
"SELECT DISTINCT c_t.id from cluster_type c_t") "SELECT DISTINCT c_t.id from cluster_type c_t")
.join("cluster c ON c_t.id = c.cluster_type_id") }; .join("cluster c ON c_t.id = c.cluster_type_id") };
return utils::execQuery<ClusterTypeId>(query, range); return utils::execRangeQuery<ClusterTypeId>(query, range);
} }
ClusterType::pointer ClusterType::find(Session& session, std::string_view name) ClusterType::pointer ClusterType::find(Session& session, std::string_view name)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().find<ClusterType>().where("name = ?").bind(std::string{ name }).resultValue(); return utils::fetchQuerySingleResult(session.getDboSession()->find<ClusterType>().where("name = ?").bind(name));
} }
ClusterType::pointer ClusterType::find(Session& session, ClusterTypeId id) ClusterType::pointer ClusterType::find(Session& session, ClusterTypeId id)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().find<ClusterType>().where("id = ?").bind(id).resultValue(); return utils::fetchQuerySingleResult(session.getDboSession()->find<ClusterType>().where("id = ?").bind(id));
} }
RangeResults<ClusterTypeId> ClusterType::findIds(Session& session, std::optional<Range> range) RangeResults<ClusterTypeId> ClusterType::findIds(Session& session, std::optional<Range> range)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ session.getDboSession().query<ClusterTypeId>("SELECT id from cluster_type") }; auto query{ session.getDboSession()->query<ClusterTypeId>("SELECT id from cluster_type") };
return utils::execQuery<ClusterTypeId>(query, range); return utils::execRangeQuery<ClusterTypeId>(query, range);
} }
Cluster::pointer ClusterType::getCluster(const std::string& name) const Cluster::pointer ClusterType::getCluster(const std::string& name) const
@@ -227,9 +236,9 @@ namespace lms::db
assert(self()); assert(self());
assert(session()); assert(session());
return session()->find<Cluster>() return utils::fetchQuerySingleResult(session()->find<Cluster>()
.where("name = ?").bind(name) .where("name = ?").bind(name)
.where("cluster_type_id = ?").bind(getId()).resultValue(); .where("cluster_type_id = ?").bind(getId()));
} }
std::vector<Cluster::pointer> ClusterType::getClusters() const std::vector<Cluster::pointer> ClusterType::getClusters() const
@@ -237,11 +246,8 @@ namespace lms::db
assert(self()); assert(self());
assert(session()); assert(session());
auto res = session()->find<Cluster>() return utils::fetchQueryResults<Cluster::pointer>(session()->find<Cluster>()
.where("cluster_type_id = ?").bind(getId()) .where("cluster_type_id = ?").bind(getId())
.orderBy("name") .orderBy("name"));
.resultList();
return std::vector<Cluster::pointer>(res.begin(), res.end());
} }
} // namespace lms::db } // namespace lms::db
+6 -6
View File
@@ -25,8 +25,9 @@
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/User.hpp" #include "database/User.hpp"
#include "core/IConfig.hpp" #include "core/IConfig.hpp"
#include "core/Service.hpp"
#include "core/ILogger.hpp" #include "core/ILogger.hpp"
#include "core/ITraceLogger.hpp"
#include "core/Service.hpp"
namespace lms::db namespace lms::db
{ {
@@ -49,7 +50,7 @@ namespace lms::db
prepare(); prepare();
} }
~Connection() ~Connection() override
{ {
// make use of per-connection usage stats to optimize // make use of per-connection usage stats to optimize
optimize(); optimize();
@@ -66,16 +67,15 @@ namespace lms::db
void prepare() void prepare()
{ {
LMS_LOG(DB, DEBUG, "Setting per-connection settings..."); LMS_LOG(DB, DEBUG, "Setting per-connection settings...");
executeSql("pragma journal_mode=WAL"); executeSql("PRAGMA journal_mode=WAL");
executeSql("pragma synchronous=normal"); executeSql("PRAGMA synchronous=normal");
executeSql("pragma analysis_limit=2000"); // to help make analyze command faster, 1000 does not seem to be enough to speed up all queries
LMS_LOG(DB, DEBUG, "Setting per-connection settings done!"); LMS_LOG(DB, DEBUG, "Setting per-connection settings done!");
} }
void optimize() void optimize()
{ {
LMS_LOG(DB, DEBUG, "connection close: Running pragma optimize..."); LMS_LOG(DB, DEBUG, "connection close: Running pragma optimize...");
executeSql("pragma optimize"); executeSql("PRAGMA optimize");
LMS_LOG(DB, DEBUG, "connection close: pragma optimize complete"); LMS_LOG(DB, DEBUG, "connection close: pragma optimize complete");
} }
+37 -45
View File
@@ -29,9 +29,9 @@ namespace lms::db
{ {
namespace namespace
{ {
Wt::Dbo::Query<ArtistId> createArtistsQuery(Wt::Dbo::Session& session, const Listen::ArtistStatsFindParameters& params) Wt::Dbo::Query<ArtistId> createArtistsQuery(Session& session, const Listen::ArtistStatsFindParameters& params)
{ {
auto query{ session.query<ArtistId>("SELECT a.id from artist a") auto query{ session.getDboSession()->query<ArtistId>("SELECT a.id from artist a")
.join("track t ON t.id = t_a_l.track_id") .join("track t ON t.id = t_a_l.track_id")
.join("track_artist_link t_a_l ON t_a_l.artist_id = a.id") .join("track_artist_link t_a_l ON t_a_l.artist_id = a.id")
.join("listen l ON l.track_id = t.id") }; .join("listen l ON l.track_id = t.id") };
@@ -53,21 +53,18 @@ namespace lms::db
if (!params.clusters.empty()) if (!params.clusters.empty())
{ {
std::ostringstream oss; std::ostringstream oss;
oss << "a.id IN (SELECT DISTINCT a.id FROM artist a" oss << "a.id IN (SELECT DISTINCT t_a_l.artist_id FROM track_artist_link t_a_l"
" INNER JOIN track t ON t.id = t_a_l.track_id" " INNER JOIN track_cluster t_c ON t_c.track_id = t_a_l.track_id";
" INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id"
" INNER JOIN cluster c ON c.id = t_c.cluster_id"
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
WhereClause clusterClause; WhereClause clusterClause;
for (auto id : params.clusters) for (auto id : params.clusters)
{ {
clusterClause.Or(WhereClause("c.id = ?")); clusterClause.Or(WhereClause("t_c.cluster_id = ?"));
query.bind(id); query.bind(id);
} }
oss << " " << clusterClause.get(); oss << " " << clusterClause.get();
oss << " GROUP BY t.id,a.id HAVING COUNT(DISTINCT c.id) = " << params.clusters.size() << ")"; oss << " GROUP BY t_a_l.track_id,t_a_l.artist_id HAVING COUNT(DISTINCT t_c.cluster_id) = " << params.clusters.size() << ")";
query.where(oss.str()); query.where(oss.str());
} }
@@ -75,9 +72,9 @@ namespace lms::db
return query; return query;
} }
Wt::Dbo::Query<ReleaseId> createReleasesQuery(Wt::Dbo::Session& session, const Listen::StatsFindParameters& params) Wt::Dbo::Query<ReleaseId> createReleasesQuery(Session& session, const Listen::StatsFindParameters& params)
{ {
auto query{ session.query<ReleaseId>("SELECT r.id from release r") auto query{ session.getDboSession()->query<ReleaseId>("SELECT r.id from release r")
.join("track t ON t.release_id = r.id") .join("track t ON t.release_id = r.id")
.join("listen l ON l.track_id = t.id") }; .join("listen l ON l.track_id = t.id") };
@@ -120,9 +117,9 @@ namespace lms::db
return query; return query;
} }
Wt::Dbo::Query<TrackId> createTracksQuery(Wt::Dbo::Session& session, const Listen::StatsFindParameters& params) Wt::Dbo::Query<TrackId> createTracksQuery(Session& session, const Listen::StatsFindParameters& params)
{ {
auto query{ session.query<TrackId>("SELECT t.id from track t") auto query{ session.getDboSession()->query<TrackId>("SELECT t.id from track t")
.join("listen l ON l.track_id = t.id") }; .join("listen l ON l.track_id = t.id") };
if (params.user.isValid()) if (params.user.isValid())
@@ -174,26 +171,26 @@ namespace lms::db
Listen::pointer Listen::create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track, ScrobblingBackend backend, const Wt::WDateTime& dateTime) Listen::pointer Listen::create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track, ScrobblingBackend backend, const Wt::WDateTime& dateTime)
{ {
session.checkWriteTransaction(); session.checkWriteTransaction();
return session.getDboSession().add(std::unique_ptr<Listen> {new Listen{ user, track, backend, dateTime }}); return session.getDboSession()->add(std::unique_ptr<Listen> {new Listen{ user, track, backend, dateTime }});
} }
std::size_t Listen::getCount(Session& session) std::size_t Listen::getCount(Session& session)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().query<int>("SELECT COUNT(*) FROM listen"); return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM listen"));
} }
Listen::pointer Listen::find(Session& session, ListenId id) Listen::pointer Listen::find(Session& session, ListenId id)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().find<Listen>().where("id = ?").bind(id).resultValue(); return utils::fetchQuerySingleResult(session.getDboSession()->find<Listen>().where("id = ?").bind(id));
} }
RangeResults<ListenId> Listen::find(Session& session, const FindParameters& parameters) RangeResults<ListenId> Listen::find(Session& session, const FindParameters& parameters)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ session.getDboSession().query<ListenId>("SELECT id FROM listen") auto query{ session.getDboSession()->query<ListenId>("SELECT id FROM listen")
.orderBy("date_time") }; .orderBy("date_time") };
if (parameters.user.isValid()) if (parameters.user.isValid())
@@ -205,100 +202,98 @@ namespace lms::db
if (parameters.syncState) if (parameters.syncState)
query.where("sync_state = ?").bind(*parameters.syncState); query.where("sync_state = ?").bind(*parameters.syncState);
return utils::execQuery<ListenId>(query, parameters.range); return utils::execRangeQuery<ListenId>(query, parameters.range);
} }
Listen::pointer Listen::find(Session& session, UserId userId, TrackId trackId, ScrobblingBackend backend, const Wt::WDateTime& dateTime) Listen::pointer Listen::find(Session& session, UserId userId, TrackId trackId, ScrobblingBackend backend, const Wt::WDateTime& dateTime)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().find<Listen>() return utils::fetchQuerySingleResult(session.getDboSession()->find<Listen>()
.where("user_id = ?").bind(userId) .where("user_id = ?").bind(userId)
.where("track_id = ?").bind(trackId) .where("track_id = ?").bind(trackId)
.where("backend = ?").bind(backend) .where("backend = ?").bind(backend)
.where("date_time = ?").bind(Wt::WDateTime::fromTime_t(dateTime.toTime_t())) .where("date_time = ?").bind(Wt::WDateTime::fromTime_t(dateTime.toTime_t())));
.resultValue();
} }
RangeResults<ArtistId> Listen::getTopArtists(Session& session, const ArtistStatsFindParameters& params) RangeResults<ArtistId> Listen::getTopArtists(Session& session, const ArtistStatsFindParameters& params)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ createArtistsQuery(session.getDboSession(), params) }; auto query{ createArtistsQuery(session, params) };
auto collection{ query auto collection{ query
.orderBy("COUNT(a.id) DESC") .orderBy("COUNT(a.id) DESC")
.groupBy("a.id") }; .groupBy("a.id") };
return utils::execQuery<ArtistId>(query, params.range); return utils::execRangeQuery<ArtistId>(query, params.range);
} }
RangeResults<ReleaseId> Listen::getTopReleases(Session& session, const StatsFindParameters& params) RangeResults<ReleaseId> Listen::getTopReleases(Session& session, const StatsFindParameters& params)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ createReleasesQuery(session.getDboSession(), params) auto query{ createReleasesQuery(session, params)
.orderBy("COUNT(r.id) DESC") .orderBy("COUNT(r.id) DESC")
.groupBy("r.id") }; .groupBy("r.id") };
return utils::execQuery<ReleaseId>(query, params.range); return utils::execRangeQuery<ReleaseId>(query, params.range);
} }
RangeResults<TrackId> Listen::getTopTracks(Session& session, const StatsFindParameters& params) RangeResults<TrackId> Listen::getTopTracks(Session& session, const StatsFindParameters& params)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ createTracksQuery(session.getDboSession(), params) auto query{ createTracksQuery(session, params)
.orderBy("COUNT(t.id) DESC") .orderBy("COUNT(t.id) DESC")
.groupBy("t.id") }; .groupBy("t.id") };
return utils::execQuery<TrackId>(query, params.range); return utils::execRangeQuery<TrackId>(query, params.range);
} }
RangeResults<ArtistId> Listen::getRecentArtists(Session& session, const ArtistStatsFindParameters& params) RangeResults<ArtistId> Listen::getRecentArtists(Session& session, const ArtistStatsFindParameters& params)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ createArtistsQuery(session.getDboSession(), params) auto query{ createArtistsQuery(session, params)
.groupBy("a.id").having("l.date_time = MAX(l.date_time)") .groupBy("a.id").having("l.date_time = MAX(l.date_time)")
.orderBy("l.date_time DESC") }; .orderBy("l.date_time DESC") };
return utils::execQuery<ArtistId>(query, params.range); return utils::execRangeQuery<ArtistId>(query, params.range);
} }
RangeResults<ReleaseId> Listen::getRecentReleases(Session& session, const StatsFindParameters& params) RangeResults<ReleaseId> Listen::getRecentReleases(Session& session, const StatsFindParameters& params)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ createReleasesQuery(session.getDboSession(), params) auto query{ createReleasesQuery(session, params)
.groupBy("r.id").having("l.date_time = MAX(l.date_time)") .groupBy("r.id").having("l.date_time = MAX(l.date_time)")
.orderBy("l.date_time DESC") }; .orderBy("l.date_time DESC") };
return utils::execQuery<ReleaseId>(query, params.range); return utils::execRangeQuery<ReleaseId>(query, params.range);
} }
RangeResults<TrackId> Listen::getRecentTracks(Session& session, const StatsFindParameters& params) RangeResults<TrackId> Listen::getRecentTracks(Session& session, const StatsFindParameters& params)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ createTracksQuery(session.getDboSession(), params) auto query{ createTracksQuery(session, params)
.groupBy("t.id").having("l.date_time = MAX(l.date_time)") .groupBy("t.id").having("l.date_time = MAX(l.date_time)")
.orderBy("l.date_time DESC") }; .orderBy("l.date_time DESC") };
return utils::execQuery<TrackId>(query, params.range); return utils::execRangeQuery<TrackId>(query, params.range);
} }
std::size_t Listen::getCount(Session& session, UserId userId, TrackId trackId) std::size_t Listen::getCount(Session& session, UserId userId, TrackId trackId)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().query<int>("SELECT COUNT(*) from listen l") return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) from listen l")
.join("user u ON u.id = l.user_id") .join("user u ON u.id = l.user_id")
.where("l.track_id = ?").bind(trackId) .where("l.track_id = ?").bind(trackId)
.where("l.user_id = ?").bind(userId) .where("l.user_id = ?").bind(userId)
.where("l.backend = u.scrobbling_backend") .where("l.backend = u.scrobbling_backend"));
.resultValue();
} }
std::size_t Listen::getCount(Session& session, UserId userId, ReleaseId releaseId) std::size_t Listen::getCount(Session& session, UserId userId, ReleaseId releaseId)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().query<int>( return utils::fetchQuerySingleResult(session.getDboSession()->query<int>(
"SELECT IFNULL(MIN(count_result), 0)" "SELECT IFNULL(MIN(count_result), 0)"
" FROM (" " FROM ("
" SELECT COUNT(l.track_id) AS count_result" " SELECT COUNT(l.track_id) AS count_result"
@@ -308,8 +303,7 @@ namespace lms::db
" GROUP BY t.id)") " GROUP BY t.id)")
.bind(userId) .bind(userId)
.bind(userId) .bind(userId)
.bind(releaseId) .bind(releaseId));
.resultValue();
} }
Listen::pointer Listen::getMostRecentListen(Session& session, UserId userId, ScrobblingBackend backend, ReleaseId releaseId) Listen::pointer Listen::getMostRecentListen(Session& session, UserId userId, ScrobblingBackend backend, ReleaseId releaseId)
@@ -317,26 +311,24 @@ namespace lms::db
session.checkReadTransaction(); session.checkReadTransaction();
// TODO not pending remove? // TODO not pending remove?
return session.getDboSession().query<Wt::Dbo::ptr<Listen>>("SELECT l from listen l") return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Listen>>("SELECT l from listen l")
.join("track t ON l.track_id = t.id") .join("track t ON l.track_id = t.id")
.where("t.release_id = ?").bind(releaseId) .where("t.release_id = ?").bind(releaseId)
.where("l.user_id = ?").bind(userId) .where("l.user_id = ?").bind(userId)
.where("l.backend = ?").bind(backend) .where("l.backend = ?").bind(backend)
.orderBy("l.date_time DESC") .orderBy("l.date_time DESC")
.limit(1) .limit(1));
.resultValue();
} }
Listen::pointer Listen::getMostRecentListen(Session& session, UserId userId, ScrobblingBackend backend, TrackId trackId) Listen::pointer Listen::getMostRecentListen(Session& session, UserId userId, ScrobblingBackend backend, TrackId trackId)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
// TODO not pending remove? // TODO not pending remove?
return session.getDboSession().query<Wt::Dbo::ptr<Listen>>("SELECT l from listen l") return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<Listen>>("SELECT l from listen l")
.where("l.track_id = ?").bind(trackId) .where("l.track_id = ?").bind(trackId)
.where("l.user_id = ?").bind(userId) .where("l.user_id = ?").bind(userId)
.where("l.backend = ?").bind(backend) .where("l.backend = ?").bind(backend)
.orderBy("l.date_time DESC") .orderBy("l.date_time DESC")
.limit(1) .limit(1));
.resultValue();
} }
} // namespace lms::db } // namespace lms::db
+10 -8
View File
@@ -25,6 +25,7 @@
#include "IdTypeTraits.hpp" #include "IdTypeTraits.hpp"
#include "PathTraits.hpp" #include "PathTraits.hpp"
#include "StringViewTraits.hpp" #include "StringViewTraits.hpp"
#include "Utils.hpp"
namespace lms::db namespace lms::db
{ {
@@ -36,43 +37,44 @@ namespace lms::db
MediaLibrary::pointer MediaLibrary::create(Session& session, const std::filesystem::path& p, std::string_view name) MediaLibrary::pointer MediaLibrary::create(Session& session, const std::filesystem::path& p, std::string_view name)
{ {
return session.getDboSession().add(std::unique_ptr<MediaLibrary>{ new MediaLibrary{ p, name } }); return session.getDboSession()->add(std::unique_ptr<MediaLibrary>{ new MediaLibrary{ p, name } });
} }
std::size_t MediaLibrary::getCount(Session& session) std::size_t MediaLibrary::getCount(Session& session)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().query<int>("SELECT COUNT(*) FROM media_library"); return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM media_library"));
} }
MediaLibrary::pointer MediaLibrary::find(Session& session, MediaLibraryId id) MediaLibrary::pointer MediaLibrary::find(Session& session, MediaLibraryId id)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().find<MediaLibrary>().where("id = ?").bind(id).resultValue(); return utils::fetchQuerySingleResult(session.getDboSession()->find<MediaLibrary>().where("id = ?").bind(id));
} }
MediaLibrary::pointer MediaLibrary::find(Session& session, std::string_view name) MediaLibrary::pointer MediaLibrary::find(Session& session, std::string_view name)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().find<MediaLibrary>().where("name = ?").bind(name).resultValue(); return utils::fetchQuerySingleResult(session.getDboSession()->find<MediaLibrary>().where("name = ?").bind(name));
} }
MediaLibrary::pointer MediaLibrary::find(Session& session, const std::filesystem::path& p) MediaLibrary::pointer MediaLibrary::find(Session& session, const std::filesystem::path& p)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().find<MediaLibrary>().where("path = ?").bind(p).resultValue(); return utils::fetchQuerySingleResult(session.getDboSession()->find<MediaLibrary>().where("path = ?").bind(p));
} }
void MediaLibrary::find(Session& session, std::function<void(const MediaLibrary::pointer&)> func) void MediaLibrary::find(Session& session, std::function<void(const MediaLibrary::pointer&)> func)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto results{ session.getDboSession().find<MediaLibrary>().resultList() }; utils::forEachQueryResult(session.getDboSession()->find<MediaLibrary>(), [&](const MediaLibrary::pointer& mediaLibrary)
for (const auto& result : results) {
func(result); func(mediaLibrary);
});
} }
} // namespace lms::db } // namespace lms::db
+130 -89
View File
@@ -27,12 +27,14 @@
#include "database/User.hpp" #include "database/User.hpp"
#include "core/Exception.hpp" #include "core/Exception.hpp"
#include "core/ILogger.hpp" #include "core/ILogger.hpp"
#include "core/ITraceLogger.hpp"
#include "Utils.hpp"
namespace lms::db namespace lms::db
{ {
namespace namespace
{ {
static constexpr Version LMS_DATABASE_VERSION{ 54 }; static constexpr Version LMS_DATABASE_VERSION{ 57 };
} }
VersionInfo::VersionInfo() VersionInfo::VersionInfo()
@@ -43,9 +45,9 @@ namespace lms::db
{ {
session.checkWriteTransaction(); session.checkWriteTransaction();
pointer versionInfo{ session.getDboSession().find<VersionInfo>() }; pointer versionInfo{ utils::fetchQuerySingleResult(session.getDboSession()->find<VersionInfo>()) };
if (!versionInfo) if (!versionInfo)
return session.getDboSession().add(std::make_unique<VersionInfo>()); return session.getDboSession()->add(std::make_unique<VersionInfo>());
return versionInfo; return versionInfo;
} }
@@ -54,7 +56,7 @@ namespace lms::db
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().find<VersionInfo>(); return utils::fetchQuerySingleResult(session.getDboSession()->find<VersionInfo>());
} }
} }
@@ -85,7 +87,7 @@ namespace lms::db::Migration
{ {
// remove name from track_artist_link // remove name from track_artist_link
// Drop Auth mode // Drop Auth mode
session.getDboSession().execute(R"( session.getDboSession()->execute(R"(
CREATE TABLE IF NOT EXISTS "track_artist_link_backup" ( CREATE TABLE IF NOT EXISTS "track_artist_link_backup" (
"id" integer primary key autoincrement, "id" integer primary key autoincrement,
"version" integer not null, "version" integer not null,
@@ -96,49 +98,49 @@ CREATE TABLE IF NOT EXISTS "track_artist_link_backup" (
constraint "fk_track_artist_link_artist" foreign key ("artist_id") references "artist" ("id") on delete cascade deferrable initially deferred constraint "fk_track_artist_link_artist" foreign key ("artist_id") references "artist" ("id") on delete cascade deferrable initially deferred
); );
))"); ))");
session.getDboSession().execute("INSERT INTO track_artist_link_backup SELECT id, version, type, track_id, artist_id FROM track_artist_link"); session.getDboSession()->execute("INSERT INTO track_artist_link_backup SELECT id, version, type, track_id, artist_id FROM track_artist_link");
session.getDboSession().execute("DROP TABLE track_artist_link"); session.getDboSession()->execute("DROP TABLE track_artist_link");
session.getDboSession().execute("ALTER TABLE track_artist_link_backup RENAME TO track_artist_link"); session.getDboSession()->execute("ALTER TABLE track_artist_link_backup RENAME TO track_artist_link");
} }
static void migrateFromV34(Session& session) static void migrateFromV34(Session& session)
{ {
// Add scrobbling state // Add scrobbling state
// By default, everything needs to be sent // By default, everything needs to be sent
session.getDboSession().execute("ALTER TABLE starred_artist ADD scrobbling_state INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(/*ScrobblingState::PendingAdd*/0)) + ")"); session.getDboSession()->execute("ALTER TABLE starred_artist ADD scrobbling_state INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(/*ScrobblingState::PendingAdd*/0)) + ")");
session.getDboSession().execute("ALTER TABLE starred_release ADD scrobbling_state INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(/*ScrobblingState::PendingAdd*/0)) + ")"); session.getDboSession()->execute("ALTER TABLE starred_release ADD scrobbling_state INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(/*ScrobblingState::PendingAdd*/0)) + ")");
session.getDboSession().execute("ALTER TABLE starred_track ADD scrobbling_state INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(/*ScrobblingState::PendingAdd*/0)) + ")"); session.getDboSession()->execute("ALTER TABLE starred_track ADD scrobbling_state INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(/*ScrobblingState::PendingAdd*/0)) + ")");
} }
static void migrateFromV35(Session& session) static void migrateFromV35(Session& session)
{ {
// Add creattion/last modif date time for tracklists // Add creattion/last modif date time for tracklists
session.getDboSession().execute("ALTER TABLE tracklist ADD creation_date_time TEXT"); session.getDboSession()->execute("ALTER TABLE tracklist ADD creation_date_time TEXT");
session.getDboSession().execute("ALTER TABLE tracklist ADD last_modified_date_time TEXT"); session.getDboSession()->execute("ALTER TABLE tracklist ADD last_modified_date_time TEXT");
} }
static void migrateFromV36(Session& session) static void migrateFromV36(Session& session)
{ {
// Increased precision for track durations (now in milliseconds instead of secodns) // Increased precision for track durations (now in milliseconds instead of secodns)
// Just increment the scan version of the settings to make the next scheduled scan rescan everything // Just increment the scan version of the settings to make the next scheduled scan rescan everything
session.getDboSession().execute("UPDATE scan_settings SET scan_version = scan_version + 1"); session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
} }
static void migrateFromV37(Session& session) static void migrateFromV37(Session& session)
{ {
// Support Performer tags (via subtypes) // Support Performer tags (via subtypes)
session.getDboSession().execute("ALTER TABLE track_artist_link ADD subtype TEXT"); session.getDboSession()->execute("ALTER TABLE track_artist_link ADD subtype TEXT");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything // Just increment the scan version of the settings to make the next scheduled scan rescan everything
session.getDboSession().execute("UPDATE scan_settings SET scan_version = scan_version + 1"); session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
} }
static void migrateFromV38(Session& session) static void migrateFromV38(Session& session)
{ {
// migrate release-specific tags from Track to Release // migrate release-specific tags from Track to Release
session.getDboSession().execute("ALTER TABLE release ADD total_disc INTEGER"); session.getDboSession()->execute("ALTER TABLE release ADD total_disc INTEGER");
session.getDboSession().execute(R"( session.getDboSession()->execute(R"(
CREATE TABLE IF NOT EXISTS "track_backup" ( CREATE TABLE IF NOT EXISTS "track_backup" (
"id" integer primary key autoincrement, "id" integer primary key autoincrement,
"version" integer not null, "version" integer not null,
@@ -165,177 +167,177 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
constraint "fk_track_release" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred constraint "fk_track_release" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred
); );
))"); ))");
session.getDboSession().execute("INSERT INTO track_backup SELECT id, version, scan_version, track_number, disc_number, total_track, disc_subtitle, name, duration, date, original_date, file_path, file_last_write, file_added, has_cover, mbid, recording_mbid, copyright, copyright_url, track_replay_gain, release_replay_gain, release_id FROM track"); session.getDboSession()->execute("INSERT INTO track_backup SELECT id, version, scan_version, track_number, disc_number, total_track, disc_subtitle, name, duration, date, original_date, file_path, file_last_write, file_added, has_cover, mbid, recording_mbid, copyright, copyright_url, track_replay_gain, release_replay_gain, release_id FROM track");
session.getDboSession().execute("DROP TABLE track"); session.getDboSession()->execute("DROP TABLE track");
session.getDboSession().execute("ALTER TABLE track_backup RENAME TO track"); session.getDboSession()->execute("ALTER TABLE track_backup RENAME TO track");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything // Just increment the scan version of the settings to make the next scheduled scan rescan everything
session.getDboSession().execute("UPDATE scan_settings SET scan_version = scan_version + 1"); session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
} }
static void migrateFromV39(Session& session) static void migrateFromV39(Session& session)
{ {
// add release type // add release type
session.getDboSession().execute("ALTER TABLE release ADD primary_type INTEGER"); session.getDboSession()->execute("ALTER TABLE release ADD primary_type INTEGER");
session.getDboSession().execute("ALTER TABLE release ADD secondary_types INTEGER"); session.getDboSession()->execute("ALTER TABLE release ADD secondary_types INTEGER");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything // Just increment the scan version of the settings to make the next scheduled scan rescan everything
session.getDboSession().execute("UPDATE scan_settings SET scan_version = scan_version + 1"); session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
} }
static void migrateFromV40(Session& session) static void migrateFromV40(Session& session)
{ {
// add artist_display_name in Release and Track // add artist_display_name in Release and Track
session.getDboSession().execute("ALTER TABLE release ADD artist_display_name TEXT NOT NULL DEFAULT ''"); session.getDboSession()->execute("ALTER TABLE release ADD artist_display_name TEXT NOT NULL DEFAULT ''");
session.getDboSession().execute("ALTER TABLE track ADD artist_display_name TEXT NOT NULL DEFAULT ''"); session.getDboSession()->execute("ALTER TABLE track ADD artist_display_name TEXT NOT NULL DEFAULT ''");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything // Just increment the scan version of the settings to make the next scheduled scan rescan everything
session.getDboSession().execute("UPDATE scan_settings SET scan_version = scan_version + 1"); session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
} }
static void migrateFromV41(Session& session) static void migrateFromV41(Session& session)
{ {
// add artist_display_name in Release and Track // add artist_display_name in Release and Track
session.getDboSession().execute("ALTER TABLE user RENAME COLUMN subsonic_transcode_format TO subsonic_default_transcode_format"); session.getDboSession()->execute("ALTER TABLE user RENAME COLUMN subsonic_transcode_format TO subsonic_default_transcode_format");
session.getDboSession().execute("ALTER TABLE user RENAME COLUMN subsonic_transcode_bitrate TO subsonic_default_transcode_bitrate"); session.getDboSession()->execute("ALTER TABLE user RENAME COLUMN subsonic_transcode_bitrate TO subsonic_default_transcode_bitrate");
session.getDboSession().execute("ALTER TABLE user DROP COLUMN subsonic_transcode_enable"); session.getDboSession()->execute("ALTER TABLE user DROP COLUMN subsonic_transcode_enable");
} }
static void migrateFromV42(Session& session) static void migrateFromV42(Session& session)
{ {
session.getDboSession().execute("DROP INDEX IF EXISTS listen_scrobbler_idx"); session.getDboSession()->execute("DROP INDEX IF EXISTS listen_scrobbler_idx");
session.getDboSession().execute("DROP INDEX IF EXISTS listen_user_scrobbler_idx"); session.getDboSession()->execute("DROP INDEX IF EXISTS listen_user_scrobbler_idx");
session.getDboSession().execute("DROP INDEX IF EXISTS listen_user_track_scrobbler_date_time_idx"); session.getDboSession()->execute("DROP INDEX IF EXISTS listen_user_track_scrobbler_date_time_idx");
session.getDboSession().execute("DROP INDEX IF EXISTS starred_artist_user_scrobbler_idx"); session.getDboSession()->execute("DROP INDEX IF EXISTS starred_artist_user_scrobbler_idx");
session.getDboSession().execute("DROP INDEX IF EXISTS starred_artist_artist_user_scrobbler_idx"); session.getDboSession()->execute("DROP INDEX IF EXISTS starred_artist_artist_user_scrobbler_idx");
session.getDboSession().execute("DROP INDEX IF EXISTS starred_release_user_scrobbler_idx"); session.getDboSession()->execute("DROP INDEX IF EXISTS starred_release_user_scrobbler_idx");
session.getDboSession().execute("DROP INDEX IF EXISTS starred_release_release_user_scrobbler_idx"); session.getDboSession()->execute("DROP INDEX IF EXISTS starred_release_release_user_scrobbler_idx");
session.getDboSession().execute("DROP INDEX IF EXISTS starred_track_user_scrobbler_idx"); session.getDboSession()->execute("DROP INDEX IF EXISTS starred_track_user_scrobbler_idx");
session.getDboSession().execute("DROP INDEX IF EXISTS starred_track_track_user_scrobbler_idx"); session.getDboSession()->execute("DROP INDEX IF EXISTS starred_track_track_user_scrobbler_idx");
// New feedback service that now handles the star/unstar stuff (that was previously handled by the scrobbling service) // New feedback service that now handles the star/unstar stuff (that was previously handled by the scrobbling service)
session.getDboSession().execute("ALTER TABLE user RENAME COLUMN scrobbler TO scrobbling_backend"); session.getDboSession()->execute("ALTER TABLE user RENAME COLUMN scrobbler TO scrobbling_backend");
session.getDboSession().execute("ALTER TABLE user ADD feedback_backend INTEGER"); session.getDboSession()->execute("ALTER TABLE user ADD feedback_backend INTEGER");
session.getDboSession().execute("ALTER TABLE listen RENAME COLUMN scrobbler TO backend"); session.getDboSession()->execute("ALTER TABLE listen RENAME COLUMN scrobbler TO backend");
session.getDboSession().execute("ALTER TABLE listen RENAME COLUMN scrobbling_state TO sync_state"); session.getDboSession()->execute("ALTER TABLE listen RENAME COLUMN scrobbling_state TO sync_state");
session.getDboSession().execute("ALTER TABLE starred_artist RENAME COLUMN scrobbler TO backend"); session.getDboSession()->execute("ALTER TABLE starred_artist RENAME COLUMN scrobbler TO backend");
session.getDboSession().execute("ALTER TABLE starred_artist RENAME COLUMN scrobbling_state TO sync_state"); session.getDboSession()->execute("ALTER TABLE starred_artist RENAME COLUMN scrobbling_state TO sync_state");
session.getDboSession().execute("ALTER TABLE starred_release RENAME COLUMN scrobbler TO backend"); session.getDboSession()->execute("ALTER TABLE starred_release RENAME COLUMN scrobbler TO backend");
session.getDboSession().execute("ALTER TABLE starred_release RENAME COLUMN scrobbling_state TO sync_state"); session.getDboSession()->execute("ALTER TABLE starred_release RENAME COLUMN scrobbling_state TO sync_state");
session.getDboSession().execute("ALTER TABLE starred_track RENAME COLUMN scrobbler TO backend"); session.getDboSession()->execute("ALTER TABLE starred_track RENAME COLUMN scrobbler TO backend");
session.getDboSession().execute("ALTER TABLE starred_track RENAME COLUMN scrobbling_state TO sync_state"); session.getDboSession()->execute("ALTER TABLE starred_track RENAME COLUMN scrobbling_state TO sync_state");
session.getDboSession().execute("UPDATE user SET feedback_backend = scrobbling_backend"); session.getDboSession()->execute("UPDATE user SET feedback_backend = scrobbling_backend");
} }
static void migrateFromV43(Session& session) static void migrateFromV43(Session& session)
{ {
// add counts in genre table // add counts in genre table
session.getDboSession().execute("ALTER TABLE cluster ADD track_count INTEGER"); session.getDboSession()->execute("ALTER TABLE cluster ADD track_count INTEGER");
session.getDboSession().execute("ALTER TABLE cluster ADD release_count INTEGER"); session.getDboSession()->execute("ALTER TABLE cluster ADD release_count INTEGER");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything // Just increment the scan version of the settings to make the next scheduled scan rescan everything
session.getDboSession().execute("UPDATE scan_settings SET scan_version = scan_version + 1"); session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
} }
static void migrateFromV44(Session& session) static void migrateFromV44(Session& session)
{ {
// add bitrate // add bitrate
session.getDboSession().execute("ALTER TABLE track ADD bitrate INTEGER NOT NULL DEFAULT 0"); session.getDboSession()->execute("ALTER TABLE track ADD bitrate INTEGER NOT NULL DEFAULT 0");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything // Just increment the scan version of the settings to make the next scheduled scan rescan everything
session.getDboSession().execute("UPDATE scan_settings SET scan_version = scan_version + 1"); session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
} }
void migrateFromV45(Session& session) void migrateFromV45(Session& session)
{ {
// add subsonic_enable_transcoding_by_default, default is disabled // add subsonic_enable_transcoding_by_default, default is disabled
session.getDboSession().execute("ALTER TABLE user ADD subsonic_enable_transcoding_by_default INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(/*User::defaultSubsonicEnableTranscodingByDefault*/0)) + ")"); session.getDboSession()->execute("ALTER TABLE user ADD subsonic_enable_transcoding_by_default INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(/*User::defaultSubsonicEnableTranscodingByDefault*/0)) + ")");
} }
void migrateFromV46(Session& session) void migrateFromV46(Session& session)
{ {
// add extra tags to parse // add extra tags to parse
session.getDboSession().execute(R"(CREATE TABLE IF NOT EXISTS "cluster_type_backup" ( session.getDboSession()->execute(R"(CREATE TABLE IF NOT EXISTS "cluster_type_backup" (
"id" integer primary key autoincrement, "id" integer primary key autoincrement,
"version" integer not null, "version" integer not null,
"name" text not null "name" text not null
);)"); );)");
session.getDboSession().execute("INSERT INTO cluster_type_backup SELECT id, version, name FROM cluster_type"); session.getDboSession()->execute("INSERT INTO cluster_type_backup SELECT id, version, name FROM cluster_type");
session.getDboSession().execute("DROP TABLE cluster_type"); session.getDboSession()->execute("DROP TABLE cluster_type");
session.getDboSession().execute("ALTER TABLE cluster_type_backup RENAME TO cluster_type"); session.getDboSession()->execute("ALTER TABLE cluster_type_backup RENAME TO cluster_type");
session.getDboSession().execute("ALTER TABLE scan_settings ADD COLUMN extra_tags_to_scan TEXT"); session.getDboSession()->execute("ALTER TABLE scan_settings ADD COLUMN extra_tags_to_scan TEXT");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything // Just increment the scan version of the settings to make the next scheduled scan rescan everything
session.getDboSession().execute("UPDATE scan_settings SET scan_version = scan_version + 1"); session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
} }
void migrateFromV47(Session& session) void migrateFromV47(Session& session)
{ {
// release type, new way // release type, new way
session.getDboSession().execute("ALTER TABLE release DROP primary_type"); session.getDboSession()->execute("ALTER TABLE release DROP primary_type");
session.getDboSession().execute("ALTER TABLE release DROP secondary_types"); session.getDboSession()->execute("ALTER TABLE release DROP secondary_types");
session.getDboSession().execute(R"(CREATE TABLE IF NOT EXISTS "release_type" ( session.getDboSession()->execute(R"(CREATE TABLE IF NOT EXISTS "release_type" (
"id" integer primary key autoincrement, "id" integer primary key autoincrement,
"version" integer not null, "version" integer not null,
"name" text not null))"); "name" text not null))");
session.getDboSession().execute(R"(CREATE TABLE IF NOT EXISTS "release_release_type" ( session.getDboSession()->execute(R"(CREATE TABLE IF NOT EXISTS "release_release_type" (
"release_type_id" bigint, "release_type_id" bigint,
"release_id" bigint, "release_id" bigint,
primary key ("release_type_id", "release_id"), primary key ("release_type_id", "release_id"),
constraint "fk_release_release_type_key1" foreign key ("release_type_id") references "release_type" ("id") on delete cascade deferrable initially deferred, constraint "fk_release_release_type_key1" foreign key ("release_type_id") references "release_type" ("id") on delete cascade deferrable initially deferred,
constraint "fk_release_release_type_key2" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred constraint "fk_release_release_type_key2" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred
))"); ))");
session.getDboSession().execute(R"(CREATE INDEX "release_release_type_release_type" on "release_release_type" ("release_type_id"))"); session.getDboSession()->execute(R"(CREATE INDEX "release_release_type_release_type" on "release_release_type" ("release_type_id"))");
session.getDboSession().execute(R"(CREATE INDEX "release_release_type_release" on "release_release_type" ("release_id"))"); session.getDboSession()->execute(R"(CREATE INDEX "release_release_type_release" on "release_release_type" ("release_id"))");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything // Just increment the scan version of the settings to make the next scheduled scan rescan everything
session.getDboSession().execute("UPDATE scan_settings SET scan_version = scan_version + 1"); session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
} }
void migrateFromV48(Session& session) void migrateFromV48(Session& session)
{ {
// Regression for the extra tags not being parsed // Regression for the extra tags not being parsed
// Just increment the scan version of the settings to make the next scheduled scan rescan everything // Just increment the scan version of the settings to make the next scheduled scan rescan everything
session.getDboSession().execute("UPDATE scan_settings SET scan_version = scan_version + 1"); session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
} }
void migrateFromV49(Session& session) void migrateFromV49(Session& session)
{ {
// Add year / originalYear fields, as date / originalDate are not enough (we don't want a wrong date but year or nothing) // Add year / originalYear fields, as date / originalDate are not enough (we don't want a wrong date but year or nothing)
session.getDboSession().execute("ALTER TABLE track ADD year INTEGER"); session.getDboSession()->execute("ALTER TABLE track ADD year INTEGER");
session.getDboSession().execute("ALTER TABLE track ADD original_year INTEGER"); session.getDboSession()->execute("ALTER TABLE track ADD original_year INTEGER");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything // Just increment the scan version of the settings to make the next scheduled scan rescan everything
session.getDboSession().execute("UPDATE scan_settings SET scan_version = scan_version + 1"); session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
} }
void migrateFromV50(Session& session) void migrateFromV50(Session& session)
{ {
// MediaLibrary support // MediaLibrary support
session.getDboSession().execute(R"(CREATE TABLE IF NOT EXISTS "media_library" ( session.getDboSession()->execute(R"(CREATE TABLE IF NOT EXISTS "media_library" (
"id" integer primary key autoincrement, "id" integer primary key autoincrement,
"version" integer not null, "version" integer not null,
"path" text not null, "path" text not null,
"name" text not null "name" text not null
))"); ))");
const int scanSettingsId{ session.getDboSession().query<int>("SELECT id FROM scan_settings") }; const int scanSettingsId{ session.getDboSession()->query<int>("SELECT id FROM scan_settings") };
// Convert the existing media_directory in the scan_settings table to a media_library with id '1' // Convert the existing media_directory in the scan_settings table to a media_library with id '1'
session.getDboSession().execute(R"(INSERT INTO "media_library" ("id", "version", "path", "name") session.getDboSession()->execute(R"(INSERT INTO "media_library" ("id", "version", "path", "name")
SELECT 1, 0, s_s.media_directory, "Main" SELECT 1, 0, s_s.media_directory, "Main"
FROM scan_settings s_s FROM scan_settings s_s
WHERE id = ?)").bind(scanSettingsId); WHERE id = ?)").bind(scanSettingsId);
// Remove the outdated column in scan_settings // Remove the outdated column in scan_settings
session.getDboSession().execute("ALTER TABLE scan_settings DROP media_directory"); session.getDboSession()->execute("ALTER TABLE scan_settings DROP media_directory");
// Add the media_library column in tracks, with id '1' // Add the media_library column in tracks, with id '1'
session.getDboSession().execute(R"( session.getDboSession()->execute(R"(
CREATE TABLE IF NOT EXISTS "track_backup" ( CREATE TABLE IF NOT EXISTS "track_backup" (
"id" integer primary key autoincrement, "id" integer primary key autoincrement,
"version" integer not null, "version" integer not null,
@@ -369,7 +371,7 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
))"); ))");
// Migrate data, with the new media_library_id field set to 1 // Migrate data, with the new media_library_id field set to 1
session.getDboSession().execute(R"(INSERT INTO track_backup session.getDboSession()->execute(R"(INSERT INTO track_backup
SELECT SELECT
id, id,
version, version,
@@ -399,36 +401,66 @@ SELECT
release_id, release_id,
1 1
FROM track)"); FROM track)");
session.getDboSession().execute("DROP TABLE track"); session.getDboSession()->execute("DROP TABLE track");
session.getDboSession().execute("ALTER TABLE track_backup RENAME TO track"); session.getDboSession()->execute("ALTER TABLE track_backup RENAME TO track");
} }
void migrateFromV51(Session& session) void migrateFromV51(Session& session)
{ {
// Add custom artist tag delimiters, no need to rescan since it has no effect when empty // Add custom artist tag delimiters, no need to rescan since it has no effect when empty
session.getDboSession().execute("ALTER TABLE scan_settings ADD artist_tag_delimiters TEXT NOT NULL DEFAULT ''"); session.getDboSession()->execute("ALTER TABLE scan_settings ADD artist_tag_delimiters TEXT NOT NULL DEFAULT ''");
session.getDboSession().execute("ALTER TABLE scan_settings ADD default_tag_delimiters TEXT NOT NULL DEFAULT ''"); session.getDboSession()->execute("ALTER TABLE scan_settings ADD default_tag_delimiters TEXT NOT NULL DEFAULT ''");
} }
void migrateFromV52(Session& session) void migrateFromV52(Session& session)
{ {
// Add sort name for releases // Add sort name for releases
session.getDboSession().execute("ALTER TABLE release ADD sort_name TEXT NOT NULL DEFAULT ''"); session.getDboSession()->execute("ALTER TABLE release ADD sort_name TEXT NOT NULL DEFAULT ''");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything // Just increment the scan version of the settings to make the next scheduled scan rescan everything
session.getDboSession().execute("UPDATE scan_settings SET scan_version = scan_version + 1"); session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
} }
void migrateFromV53(Session& session) void migrateFromV53(Session& session)
{ {
// Add release group mbid // Add release group mbid
session.getDboSession().execute("ALTER TABLE release ADD group_mbid TEXT NOT NULL DEFAULT ''"); session.getDboSession()->execute("ALTER TABLE release ADD group_mbid TEXT NOT NULL DEFAULT ''");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything // Just increment the scan version of the settings to make the next scheduled scan rescan everything
session.getDboSession().execute("UPDATE scan_settings SET scan_version = scan_version + 1"); session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
} }
void doDbMigration(Session& session) void migrateFromV54(Session& session)
{
// Add file size + relative file path
session.getDboSession()->execute("ALTER TABLE track RENAME COLUMN file_path TO absolute_file_path");
session.getDboSession()->execute("ALTER TABLE track ADD file_size BIGINT NOT NULL DEFAULT(0)");
session.getDboSession()->execute("ALTER TABLE track ADD relative_file_path TEXT NOT NULL DEFAULT ''");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
}
void migrateFromV55(Session& session)
{
// Add bitsPerSample, channelCount and sampleRate
session.getDboSession()->execute("ALTER TABLE track ADD bits_per_sample INTEGER NOT NULL DEFAULT(0)");
session.getDboSession()->execute("ALTER TABLE track ADD channel_count INTEGER NOT NULL DEFAULT(0)");
session.getDboSession()->execute("ALTER TABLE track ADD sample_rate INTEGER NOT NULL DEFAULT(0)");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
session.getDboSession()->execute("UPDATE scan_settings SET scan_version = scan_version + 1");
}
void migrateFromV56(Session& session)
{
// Make sure we remove all the previoulsy created index, the createIndexesIfNeeded will recreate them all
std::vector<std::string> indexeNames{ utils::fetchQueryResults(session.getDboSession()->query<std::string>(R"(SELECT name FROM sqlite_master WHERE type = 'index' AND name LIKE '%_idx')")) };
for (const auto& indexName : indexeNames)
session.getDboSession()->execute("DROP INDEX " + indexName);
}
bool doDbMigration(Session& session)
{ {
static const std::string outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" }; static const std::string outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
@@ -459,9 +491,14 @@ SELECT
{51, migrateFromV51}, {51, migrateFromV51},
{52, migrateFromV52}, {52, migrateFromV52},
{53, migrateFromV53}, {53, migrateFromV53},
{54, migrateFromV54},
{55, migrateFromV55},
{56, migrateFromV56},
}; };
bool migrationPerformed{};
{ {
LMS_SCOPED_TRACE_OVERVIEW("Database", "Migration");
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
Version version; Version version;
@@ -484,6 +521,7 @@ SELECT
while (version < LMS_DATABASE_VERSION) while (version < LMS_DATABASE_VERSION)
{ {
LMS_SCOPED_TRACE_DETAILED("Database", "MigrationStep");
LMS_LOG(DB, INFO, "Migrating database from version " << version << " to " << version + 1 << "..."); LMS_LOG(DB, INFO, "Migrating database from version " << version << " to " << version + 1 << "...");
auto itMigrationFunc{ migrationFunctions.find(version) }; auto itMigrationFunc{ migrationFunctions.find(version) };
@@ -493,7 +531,10 @@ SELECT
VersionInfo::get(session).modify()->setVersion(++version); VersionInfo::get(session).modify()->setVersion(++version);
LMS_LOG(DB, INFO, "Migration complete to version " << version); LMS_LOG(DB, INFO, "Migration complete to version " << version);
migrationPerformed = true;
} }
} }
return migrationPerformed;
} }
} }
+1 -1
View File
@@ -52,6 +52,6 @@ namespace lms::db
namespace Migration namespace Migration
{ {
void doDbMigration(Session& session); bool doDbMigration(Session& session); // return true if migration was performed
} }
} }
+117 -111
View File
@@ -40,7 +40,7 @@ namespace lms::db
template <typename ResultType> template <typename ResultType>
Wt::Dbo::Query<ResultType> createQuery(Session& session, std::string_view itemToSelect, const Release::FindParameters& params) Wt::Dbo::Query<ResultType> createQuery(Session& session, std::string_view itemToSelect, const Release::FindParameters& params)
{ {
auto query{ session.getDboSession().query<ResultType>("SELECT " + std::string{ itemToSelect } + " from release r") }; auto query{ session.getDboSession()->query<ResultType>("SELECT " + std::string{ itemToSelect } + " from release r") };
if (params.sortMethod == ReleaseSortMethod::ArtistNameThenName if (params.sortMethod == ReleaseSortMethod::ArtistNameThenName
|| params.sortMethod == ReleaseSortMethod::LastWritten || params.sortMethod == ReleaseSortMethod::LastWritten
@@ -87,12 +87,16 @@ namespace lms::db
.where("s_r.sync_state <> ?").bind(SyncState::PendingRemove); .where("s_r.sync_state <> ?").bind(SyncState::PendingRemove);
} }
if (params.artist.isValid() if (params.artist.isValid()
|| params.sortMethod == ReleaseSortMethod::ArtistNameThenName) || params.sortMethod == ReleaseSortMethod::ArtistNameThenName)
{ {
query.join("artist a ON a.id = t_a_l.artist_id") query.join("track_artist_link t_a_l ON t_a_l.track_id = t.id");
.join("track_artist_link t_a_l ON t_a_l.track_id = t.id")
.where("a.id = ?").bind(params.artist); if (params.artist.isValid())
query.where("t_a_l.artist_id = ?").bind(params.artist);
if (params.sortMethod == ReleaseSortMethod::ArtistNameThenName)
query.join("artist a ON a.id = t_a_l.artist_id");
if (!params.trackArtistLinkTypes.empty()) if (!params.trackArtistLinkTypes.empty())
{ {
@@ -115,10 +119,9 @@ namespace lms::db
{ {
std::ostringstream oss; std::ostringstream oss;
oss << "r.id NOT IN (SELECT DISTINCT r.id FROM release r" oss << "r.id NOT IN (SELECT DISTINCT r.id FROM release r"
" INNER JOIN artist a ON a.id = t_a_l.artist_id"
" INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id" " INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id"
" INNER JOIN track t ON t.release_id = r.id" " INNER JOIN track t ON t.release_id = r.id"
" WHERE (a.id = ? AND ("; " WHERE (t_a_l.artist_id = ? AND (";
query.bind(params.artist); query.bind(params.artist);
@@ -145,8 +148,7 @@ namespace lms::db
else if (params.clusters.size() > 1) else if (params.clusters.size() > 1)
{ {
std::ostringstream oss; std::ostringstream oss;
oss << "r.id IN (SELECT DISTINCT r.id FROM release r" oss << "r.id IN (SELECT DISTINCT t.release_id FROM track t"
" INNER JOIN track t ON t.release_id = r.id"
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"; " INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
WhereClause clusterClause; WhereClause clusterClause;
@@ -166,6 +168,9 @@ namespace lms::db
{ {
case ReleaseSortMethod::None: case ReleaseSortMethod::None:
break; break;
case ReleaseSortMethod::Id:
query.orderBy("r.id");
break;
case ReleaseSortMethod::Name: case ReleaseSortMethod::Name:
query.orderBy("r.name COLLATE NOCASE"); query.orderBy("r.name COLLATE NOCASE");
break; break;
@@ -204,27 +209,23 @@ namespace lms::db
ReleaseType::pointer ReleaseType::create(Session& session, std::string_view name) ReleaseType::pointer ReleaseType::create(Session& session, std::string_view name)
{ {
return session.getDboSession().add(std::unique_ptr<ReleaseType> {new ReleaseType{ name }}); return session.getDboSession()->add(std::unique_ptr<ReleaseType> {new ReleaseType{ name }});
} }
ReleaseType::pointer ReleaseType::find(Session& session, ReleaseTypeId id) ReleaseType::pointer ReleaseType::find(Session& session, ReleaseTypeId id)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession() return utils::fetchQuerySingleResult(session.getDboSession()->find<ReleaseType>()
.find<ReleaseType>() .where("id = ?").bind(id));
.where("id = ?").bind(id)
.resultValue();
} }
ReleaseType::pointer ReleaseType::find(Session& session, std::string_view name) ReleaseType::pointer ReleaseType::find(Session& session, std::string_view name)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession() return utils::fetchQuerySingleResult(session.getDboSession()->find<ReleaseType>()
.find<ReleaseType>() .where("name = ?").bind(name));
.where("name = ?").bind(name)
.resultValue();
} }
Release::Release(const std::string& name, const std::optional<core::UUID>& MBID) Release::Release(const std::string& name, const std::optional<core::UUID>& MBID)
@@ -235,62 +236,76 @@ namespace lms::db
Release::pointer Release::create(Session& session, const std::string& name, const std::optional<core::UUID>& MBID) Release::pointer Release::create(Session& session, const std::string& name, const std::optional<core::UUID>& MBID)
{ {
return session.getDboSession().add(std::unique_ptr<Release> {new Release{ name, MBID }}); return session.getDboSession()->add(std::unique_ptr<Release> {new Release{ name, MBID }});
} }
std::vector<Release::pointer> Release::find(Session& session, const std::string& name, const std::filesystem::path& releaseDirectory) std::vector<Release::pointer> Release::find(Session& session, const std::string& name, const std::filesystem::path& releaseDirectory)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto res{ session.getDboSession() return utils::fetchQueryResults<Release::pointer>(session.getDboSession()->query<Wt::Dbo::ptr<Release>>("SELECT DISTINCT r from release r")
.query<Wt::Dbo::ptr<Release>>("SELECT DISTINCT r from release r") .join("track t ON t.release_id = r.id")
.join("track t ON t.release_id = r.id") .where("r.name = ?").bind(std::string(name, 0, _maxNameLength))
.where("r.name = ?").bind(std::string(name, 0, _maxNameLength)) .where("t.absolute_file_path LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind(utils::escapeLikeKeyword(releaseDirectory.string()) + "%"));
.where("t.file_path LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind(utils::escapeLikeKeyword(releaseDirectory.string()) + "%")
.resultList() };
return std::vector<Release::pointer>(res.begin(), res.end());
} }
Release::pointer Release::find(Session& session, const core::UUID& mbid) Release::pointer Release::find(Session& session, const core::UUID& mbid)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession() return utils::fetchQuerySingleResult(session.getDboSession()->find<Release>()
.find<Release>() .where("mbid = ?").bind(mbid.getAsString()));
.where("mbid = ?").bind(std::string{ mbid.getAsString() })
.resultValue();;
} }
Release::pointer Release::find(Session& session, ReleaseId id) Release::pointer Release::find(Session& session, ReleaseId id)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession() return utils::fetchQuerySingleResult(session.getDboSession()->find<Release>()
.find<Release>() .where("id = ?").bind(id));
.where("id = ?").bind(id)
.resultValue();
} }
bool Release::exists(Session& session, ReleaseId id) bool Release::exists(Session& session, ReleaseId id)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().query<int>("SELECT 1 FROM release").where("id = ?").bind(id).resultValue() == 1; return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT 1 FROM release").where("id = ?").bind(id)) == 1;
} }
std::size_t Release::getCount(Session& session) std::size_t Release::getCount(Session& session)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().query<int>("SELECT COUNT(*) FROM release"); return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM release"));
} }
RangeResults<ReleaseId> Release::findOrphanIds(Session& session, std::optional<Range> range) RangeResults<ReleaseId> Release::findOrphanIds(Session& session, std::optional<Range> range)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ session.getDboSession().query<ReleaseId>("select r.id from release r LEFT OUTER JOIN Track t ON r.id = t.release_id WHERE t.id IS NULL") }; auto query{ session.getDboSession()->query<ReleaseId>("select r.id from release r LEFT OUTER JOIN Track t ON r.id = t.release_id WHERE t.id IS NULL") };
return utils::execQuery<ReleaseId>(query, range); return utils::execRangeQuery<ReleaseId>(query, range);
}
void Release::find(Session& session, ReleaseId& lastRetrievedRelease, std::size_t count, const std::function<void(const Release::pointer&)>& func, MediaLibraryId library)
{
session.checkReadTransaction();
auto query{ session.getDboSession()->query<Wt::Dbo::ptr<Release>>("SELECT r FROM release r")
.orderBy("r.id")
.where("r.id > ?").bind(lastRetrievedRelease)
.limit(static_cast<int>(count)) };
if (library.isValid())
{
// Faster than using joins
query.where("EXISTS (SELECT 1 FROM track t WHERE t.release_id = r.id AND t.media_library_id = ?)").bind(library);
}
utils::forEachQueryResult(query, [&](const Release::pointer& release)
{
func(release);
lastRetrievedRelease = release->getId();
});
} }
RangeResults<Release::pointer> Release::find(Session& session, const FindParameters& params) RangeResults<Release::pointer> Release::find(Session& session, const FindParameters& params)
@@ -298,15 +313,15 @@ namespace lms::db
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ createQuery<Wt::Dbo::ptr<Release>>(session, "DISTINCT r", params) }; auto query{ createQuery<Wt::Dbo::ptr<Release>>(session, "DISTINCT r", params) };
return utils::execQuery<pointer>(query, params.range); return utils::execRangeQuery<pointer>(query, params.range);
} }
void Release::find(Session& session, const FindParameters& params, std::function<void(const pointer&)> func) void Release::find(Session& session, const FindParameters& params, const std::function<void(const pointer&)>& func)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ createQuery<Wt::Dbo::ptr<Release>>(session, "DISTINCT r", params) }; auto query{ createQuery<Wt::Dbo::ptr<Release>>(session, "DISTINCT r", params) };
utils::execQuery<pointer>(query, params.range, func); utils::forEachQueryRangeResult(query, params.range, func);
} }
RangeResults<ReleaseId> Release::findIds(Session& session, const FindParameters& params) RangeResults<ReleaseId> Release::findIds(Session& session, const FindParameters& params)
@@ -314,41 +329,38 @@ namespace lms::db
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ createQuery<ReleaseId>(session, "DISTINCT r.id", params) }; auto query{ createQuery<ReleaseId>(session, "DISTINCT r.id", params) };
return utils::execQuery<ReleaseId>(query, params.range); return utils::execRangeQuery<ReleaseId>(query, params.range);
} }
std::size_t Release::getCount(Session& session, const FindParameters& params) std::size_t Release::getCount(Session& session, const FindParameters& params)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return createQuery<int>(session, "COUNT(DISTINCT r.id)", params).resultValue(); return utils::fetchQuerySingleResult(createQuery<int>(session, "COUNT(r.id)", params));
} }
std::size_t Release::getDiscCount() const std::size_t Release::getDiscCount() const
{ {
assert(session()); assert(session());
int res{ session()->query<int>("SELECT COUNT(DISTINCT disc_number) FROM track t") int res{ utils::fetchQuerySingleResult(session()->query<int>("SELECT COUNT(DISTINCT disc_number) FROM track t")
.join("release r ON r.id = t.release_id") .where("t.release_id = ?").bind(getId()))};
.where("r.id = ?")
.bind(getId()) };
return res; return res;
} }
std::vector<DiscInfo> Release::getDiscs() const std::vector<DiscInfo> Release::getDiscs() const
{ {
assert(session()); assert(session());
using ResultType = std::tuple<int, std::string>; using ResultType = std::tuple<int, std::string>;
auto results{ session()->query<ResultType>("SELECT DISTINCT disc_number, disc_subtitle FROM track t") const auto query{ session()->query<ResultType>("SELECT DISTINCT disc_number, disc_subtitle FROM track t")
.join("release r ON r.id = t.release_id") .where("t.release_id = ?").bind(getId())
.where("r.id = ?") .orderBy("disc_number")};
.orderBy("disc_number")
.bind(getId())
.resultList() };
std::vector<DiscInfo> discs; std::vector<DiscInfo> discs;
for (const auto& res : results) utils::forEachQueryResult(query, [&](ResultType&& res)
discs.emplace_back(DiscInfo{ static_cast<std::size_t>(std::get<int>(res)), std::get<std::string>(res) }); {
discs.emplace_back(DiscInfo{ static_cast<std::size_t>(std::get<int>(res)), std::move(std::get<std::string>(res)) });
});
return discs; return discs;
} }
@@ -368,13 +380,12 @@ namespace lms::db
assert(session()); assert(session());
const char* field{ original ? "original_date" : "date" }; const char* field{ original ? "original_date" : "date" };
auto query{ (session()->query<Wt::WDate>(std::string {"SELECT "} + "t." + field + " FROM track t")
auto dates{ session()->query<Wt::WDate>( .where("t.release_id = ?")
std::string {"SELECT "} + "t." + field + " FROM track t INNER JOIN release r ON r.id = t.release_id")
.where("r.id = ?")
.groupBy(field) .groupBy(field)
.bind(getId()) .bind(getId())) };
.resultList() };
const auto dates{ utils::fetchQueryResults(query) };
// various dates => invalid date // various dates => invalid date
if (dates.empty() || dates.size() > 1) if (dates.empty() || dates.size() > 1)
@@ -382,7 +393,7 @@ namespace lms::db
return dates.front(); return dates.front();
} }
std::optional<int> Release::getYear() const std::optional<int> Release::getYear() const
{ {
return getYear(false); return getYear(false);
@@ -398,15 +409,15 @@ namespace lms::db
assert(session()); assert(session());
const char* field{ original ? "original_year" : "year" }; const char* field{ original ? "original_year" : "year" };
auto query{ session()->query<std::optional<int>>(std::string {"SELECT "} + "t." + field + " FROM track t")
.where("t.release_id = ?").bind(getId())
.groupBy(field) };
auto years{ session()->query<std::optional<int>>( const auto years{ utils::fetchQueryResults(query) };
std::string {"SELECT "} + "t." + field + " FROM track t INNER JOIN release r ON r.id = t.release_id")
.where("r.id = ?").bind(getId())
.groupBy(field)
.resultList() };
// various years => invalid years // various years => invalid years
if (years.empty() || years.size() > 1) const std::size_t count{ years.size() };
if (count == 0 || count > 1)
return std::nullopt; return std::nullopt;
return years.front(); return years.front();
@@ -416,63 +427,60 @@ namespace lms::db
{ {
assert(session()); assert(session());
Wt::Dbo::collection<std::string> copyrights = session()->query<std::string> auto query{ session()->query<std::string>("SELECT copyright FROM track t INNER JOIN release r ON r.id = t.release_id")
("SELECT copyright FROM track t INNER JOIN release r ON r.id = t.release_id")
.where("r.id = ?") .where("r.id = ?")
.groupBy("copyright") .groupBy("copyright")
.bind(getId()); .bind(getId()) };
std::vector<std::string> values(copyrights.begin(), copyrights.end()); const auto copyrights{ utils::fetchQueryResults(query) };
// various copyrights => no copyright // various copyrights => no copyright
if (values.empty() || values.size() > 1 || values.front().empty()) if (copyrights.empty() || copyrights.size() > 1 || copyrights.front().empty())
return std::nullopt; return std::nullopt;
return values.front(); return std::move(copyrights.front());
} }
std::optional<std::string> Release::getCopyrightURL() const std::optional<std::string> Release::getCopyrightURL() const
{ {
assert(session()); assert(session());
Wt::Dbo::collection<std::string> copyrights = session()->query<std::string> const auto query{ session()->query<std::string>
("SELECT copyright_url FROM track t INNER JOIN release r ON r.id = t.release_id") ("SELECT copyright_url FROM track t INNER JOIN release r ON r.id = t.release_id")
.where("r.id = ?").bind(getId()) .where("r.id = ?").bind(getId())
.groupBy("copyright_url"); .groupBy("copyright_url") };
std::vector<std::string> values(copyrights.begin(), copyrights.end()); const auto copyrights{ utils::fetchQueryResults(query) };
// various copyright URLs => no copyright URL // various copyright URLs => no copyright URL
if (values.empty() || values.size() > 1 || values.front().empty()) if (copyrights.empty() || copyrights.size() > 1 || copyrights.front().empty())
return std::nullopt; return std::nullopt;
return values.front(); return std::move(copyrights.front());
} }
std::size_t Release::getMeanBitrate() const std::size_t Release::getMeanBitrate() const
{ {
assert(session()); assert(session());
return session()->query<int>("SELECT COALESCE(AVG(t.bitrate), 0) FROM track t") return utils::fetchQuerySingleResult(session()->query<int>("SELECT COALESCE(AVG(t.bitrate), 0) FROM track t")
.where("release_id = ?").bind(getId()) .where("release_id = ?").bind(getId())
.where("bitrate > 0") .where("bitrate > 0"));
.resultValue();
} }
std::vector<Artist::pointer> Release::getArtists(TrackArtistLinkType linkType) const std::vector<Artist::pointer> Release::getArtists(TrackArtistLinkType linkType) const
{ {
assert(session()); assert(session());
auto res{ session()->query<Wt::Dbo::ptr<Artist>>( const auto query{ session()->query<Wt::Dbo::ptr<Artist>>(
"SELECT DISTINCT a FROM artist a" "SELECT a FROM artist a"
" INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id" " INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id"
" INNER JOIN track t ON t.id = t_a_l.track_id" " INNER JOIN track t ON t.id = t_a_l.track_id")
" INNER JOIN release r ON r.id = t.release_id") .where("t.release_id = ?").bind(getId())
.where("r.id = ?").bind(getId()) .where("+t_a_l.type = ?").bind(linkType) // adding + since the query planner does not a good job when analyze is not performed
.where("t_a_l.type = ?").bind(linkType) .groupBy("a.id") };
.resultList() };
return std::vector<Artist::pointer>(res.begin(), res.end()); return utils::fetchQueryResults<Artist::pointer>(query);
} }
std::vector<Release::pointer> Release::getSimilarReleases(std::optional<std::size_t> offset, std::optional<std::size_t> count) const std::vector<Release::pointer> Release::getSimilarReleases(std::optional<std::size_t> offset, std::optional<std::size_t> count) const
@@ -480,7 +488,7 @@ namespace lms::db
assert(session()); assert(session());
// Select the similar releases using the 5 most used clusters of the release // Select the similar releases using the 5 most used clusters of the release
auto res{ session()->query<Wt::Dbo::ptr<Release>>( auto query{ session()->query<Wt::Dbo::ptr<Release>>(
"SELECT r FROM release r" "SELECT r FROM release r"
" INNER JOIN track t ON t.release_id = r.id" " INNER JOIN track t ON t.release_id = r.id"
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id" " INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
@@ -498,10 +506,9 @@ namespace lms::db
.groupBy("r.id") .groupBy("r.id")
.orderBy("COUNT(*) DESC, RANDOM()") .orderBy("COUNT(*) DESC, RANDOM()")
.limit(count ? static_cast<int>(*count) : -1) .limit(count ? static_cast<int>(*count) : -1)
.offset(offset ? static_cast<int>(*offset) : -1) .offset(offset ? static_cast<int>(*offset) : -1) };
.resultList() };
return std::vector<pointer>(res.begin(), res.end()); return utils::fetchQueryResults<Release::pointer>(query);
} }
void Release::clearReleaseTypes() void Release::clearReleaseTypes()
@@ -520,14 +527,17 @@ namespace lms::db
return getArtists().size() > 1; return getArtists().size() > 1;
} }
std::size_t Release::getTracksCount() const std::size_t Release::getTrackCount() const
{ {
return _tracks.size(); assert(session());
return utils::fetchQuerySingleResult(session()->query<int>("SELECT COUNT(t.id) FROM track t INNER JOIN release r ON r.id = t.release_id")
.where("r.id = ?").bind(getId()));
} }
std::vector<ObjectPtr<ReleaseType>> Release::getReleaseTypes() const std::vector<ReleaseType::pointer> Release::getReleaseTypes() const
{ {
return std::vector<ObjectPtr<ReleaseType>>(_releaseTypes.begin(), _releaseTypes.end()); // TODO remove?
return utils::fetchQueryResults<ReleaseType::pointer>(_releaseTypes.find());
} }
std::vector<std::string> Release::getReleaseTypeNames() const std::vector<std::string> Release::getReleaseTypeNames() const
@@ -546,20 +556,16 @@ namespace lms::db
using milli = std::chrono::duration<int, std::milli>; using milli = std::chrono::duration<int, std::milli>;
Wt::Dbo::Query<milli> query{ session()->query<milli>("SELECT COALESCE(SUM(duration), 0) FROM track t INNER JOIN release r ON t.release_id = r.id") return utils::fetchQuerySingleResult(session()->query<milli>("SELECT COALESCE(SUM(duration), 0) FROM track t")
.where("r.id = ?").bind(getId()) }; .where("t.release_id = ?").bind(getId()));
return query.resultValue();
} }
Wt::WDateTime Release::getLastWritten() const Wt::WDateTime Release::getLastWritten() const
{ {
assert(session()); assert(session());
Wt::Dbo::Query<Wt::WDateTime> query{ session()->query<Wt::WDateTime>("SELECT COALESCE(MAX(file_last_write), '1970-01-01T00:00:00') FROM track t INNER JOIN release r ON t.release_id = r.id") return utils::fetchQuerySingleResult(session()->query<Wt::WDateTime>("SELECT COALESCE(MAX(file_last_write), '1970-01-01T00:00:00') FROM track t")
.where("r.id = ?").bind(getId()) }; .where("t.release_id = ?").bind(getId()));
return query.resultValue();
} }
std::vector<std::vector<Cluster::pointer>> Release::getClusterGroups(const std::vector<ClusterTypeId>& clusterTypeIds, std::size_t size) const std::vector<std::vector<Cluster::pointer>> Release::getClusterGroups(const std::vector<ClusterTypeId>& clusterTypeIds, std::size_t size) const
@@ -570,13 +576,13 @@ namespace lms::db
std::ostringstream oss; std::ostringstream oss;
oss << "SELECT c from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN cluster_type c_type ON c.cluster_type_id = c_type.id INNER JOIN release r ON t.release_id = r.id "; oss << "SELECT c from cluster c INNER JOIN track_cluster t_c ON t_c.cluster_id = c.id INNER JOIN track t ON t.id = t_c.track_id ";
where.And(WhereClause("r.id = ?")).bind(getId().toString()); where.And(WhereClause("t.release_id = ?")).bind(getId().toString());
{ {
WhereClause clusterClause; WhereClause clusterClause;
for (const ClusterTypeId clusterTypeId : clusterTypeIds) for (const ClusterTypeId clusterTypeId : clusterTypeIds)
clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterTypeId.toString()); clusterClause.Or(WhereClause("c.cluster_type_id = ?")).bind(clusterTypeId.toString());
where.And(clusterClause); where.And(clusterClause);
} }
oss << " " << where.get(); oss << " " << where.get();
+3 -2
View File
@@ -24,6 +24,7 @@
#include "database/MediaLibrary.hpp" #include "database/MediaLibrary.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "core/String.hpp" #include "core/String.hpp"
#include "Utils.hpp"
namespace lms::db namespace lms::db
{ {
@@ -34,14 +35,14 @@ namespace lms::db
if (pointer settings{ get(session) }) if (pointer settings{ get(session) })
return; return;
session.getDboSession().add(std::make_unique<ScanSettings>()); session.getDboSession()->add(std::make_unique<ScanSettings>());
} }
ScanSettings::pointer ScanSettings::get(Session& session) ScanSettings::pointer ScanSettings::get(Session& session)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().find<ScanSettings>().resultValue(); return utils::fetchQuerySingleResult(session.getDboSession()->find<ScanSettings>());
} }
std::vector<std::filesystem::path> ScanSettings::getAudioFileExtensions() const std::vector<std::filesystem::path> ScanSettings::getAudioFileExtensions() const
+163 -70
View File
@@ -46,6 +46,7 @@
#include "EnumSetTraits.hpp" #include "EnumSetTraits.hpp"
#include "PathTraits.hpp" #include "PathTraits.hpp"
#include "Migration.hpp" #include "Migration.hpp"
#include "Utils.hpp"
namespace lms::db namespace lms::db
{ {
@@ -120,7 +121,7 @@ namespace lms::db
return ReadTransaction{ _session }; return ReadTransaction{ _session };
} }
void Session::prepareTables() void Session::prepareTablesIfNeeded()
{ {
LMS_LOG(DB, INFO, "Preparing tables..."); LMS_LOG(DB, INFO, "Preparing tables...");
@@ -140,86 +141,178 @@ namespace lms::db
throw e; throw e;
} }
} }
}
Migration::doDbMigration(*this); bool Session::migrateSchemaIfNeeded()
{
const bool migrationPerformed{ Migration::doDbMigration(*this) };
// Indexes // TODO: move this elsewhere
{
auto transaction{ createWriteTransaction() };
_session.execute("CREATE INDEX IF NOT EXISTS artist_name_idx ON artist(name)");
_session.execute("CREATE INDEX IF NOT EXISTS artist_sort_name_nocase_idx ON artist(sort_name COLLATE NOCASE)");
_session.execute("CREATE INDEX IF NOT EXISTS artist_mbid_idx ON artist(mbid)");
_session.execute("CREATE INDEX IF NOT EXISTS auth_token_user_idx ON auth_token(user_id)");
_session.execute("CREATE INDEX IF NOT EXISTS auth_token_expiry_idx ON auth_token(expiry)");
_session.execute("CREATE INDEX IF NOT EXISTS auth_token_value_idx ON auth_token(value)");
_session.execute("CREATE INDEX IF NOT EXISTS cluster_name_idx ON cluster(name)");
_session.execute("CREATE INDEX IF NOT EXISTS cluster_cluster_type_idx ON cluster(cluster_type_id)");
_session.execute("CREATE INDEX IF NOT EXISTS cluster_type_name_idx ON cluster_type(name)");
_session.execute("CREATE INDEX IF NOT EXISTS release_name_idx ON release(name)");
_session.execute("CREATE INDEX IF NOT EXISTS release_name_nocase_idx ON release(name COLLATE NOCASE)");
_session.execute("CREATE INDEX IF NOT EXISTS release_mbid_idx ON release(mbid)");
_session.execute("CREATE INDEX IF NOT EXISTS release_type_name_idx ON release_type(name)");
_session.execute("CREATE INDEX IF NOT EXISTS track_id_idx ON track(id)");
_session.execute("CREATE INDEX IF NOT EXISTS track_path_idx ON track(file_path)");
_session.execute("CREATE INDEX IF NOT EXISTS track_name_idx ON track(name)");
_session.execute("CREATE INDEX IF NOT EXISTS track_name_nocase_idx ON track(name COLLATE NOCASE)");
_session.execute("CREATE INDEX IF NOT EXISTS track_mbid_idx ON track(mbid)");
_session.execute("CREATE INDEX IF NOT EXISTS track_recording_mbid_idx ON track(recording_mbid)");
_session.execute("CREATE INDEX IF NOT EXISTS track_release_idx ON track(release_id)");
_session.execute("CREATE INDEX IF NOT EXISTS track_file_last_write_idx ON track(file_last_write)");
_session.execute("CREATE INDEX IF NOT EXISTS track_date_idx ON track(date)");
_session.execute("CREATE INDEX IF NOT EXISTS track_year_idx ON track(year)");
_session.execute("CREATE INDEX IF NOT EXISTS track_original_date_idx ON track(original_date)");
_session.execute("CREATE INDEX IF NOT EXISTS track_original_year_idx ON track(original_year)");
_session.execute("CREATE INDEX IF NOT EXISTS track_media_library_idx ON track(media_library_id)");
_session.execute("CREATE INDEX IF NOT EXISTS tracklist_name_idx ON tracklist(name)");
_session.execute("CREATE INDEX IF NOT EXISTS tracklist_user_idx ON tracklist(user_id)");
_session.execute("CREATE INDEX IF NOT EXISTS track_features_track_idx ON track_features(track_id)");
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_artist_idx ON track_artist_link(artist_id)");
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_track_idx ON track_artist_link(track_id)");
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_type_idx ON track_artist_link(type)");
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_artist_type_idx ON track_artist_link(artist_id,type)");
_session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_idx ON track_bookmark(user_id)");
_session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_track_idx ON track_bookmark(user_id,track_id)");
_session.execute("CREATE INDEX IF NOT EXISTS listen_backend_idx ON listen(backend)");
_session.execute("CREATE INDEX IF NOT EXISTS listen_user_backend_idx ON listen(user_id,backend)");
_session.execute("CREATE INDEX IF NOT EXISTS listen_track_user_backend_idx ON listen(track_id,user_id,backend)");
_session.execute("CREATE INDEX IF NOT EXISTS listen_user_track_backend_date_time_idx ON listen(user_id,track_id,backend,date_time)");
_session.execute("CREATE INDEX IF NOT EXISTS starred_artist_user_backend_idx ON starred_artist(user_id,backend)");
_session.execute("CREATE INDEX IF NOT EXISTS starred_artist_artist_user_backend_idx ON starred_artist(artist_id,user_id,backend)");
_session.execute("CREATE INDEX IF NOT EXISTS starred_release_user_backend_idx ON starred_release(user_id,backend)");
_session.execute("CREATE INDEX IF NOT EXISTS starred_release_release_user_backend_idx ON starred_release(release_id,user_id,backend)");
_session.execute("CREATE INDEX IF NOT EXISTS starred_track_user_backend_idx ON starred_track(user_id,backend)");
_session.execute("CREATE INDEX IF NOT EXISTS starred_track_track_user_backend_idx ON starred_track(track_id,user_id,backend)");
}
// Singletons
{ {
auto uniqueTransaction{ createWriteTransaction() }; auto uniqueTransaction{ createWriteTransaction() };
ScanSettings::init(*this); ScanSettings::init(*this);
} }
return migrationPerformed;
} }
void Session::analyze() void Session::createIndexesIfNeeded()
{ {
LMS_SCOPED_TRACE_DETAILED("Database", "Analyze"); LMS_SCOPED_TRACE_OVERVIEW("Database", "IndexCreation");
LMS_LOG(DB, INFO, "Analyzing database..."); LMS_LOG(DB, INFO, "Creating indexes... This may take a while...");
auto transaction{ createWriteTransaction() };
_session.execute("CREATE INDEX IF NOT EXISTS artist_id_idx ON artist(id)");
_session.execute("CREATE INDEX IF NOT EXISTS artist_name_idx ON artist(name)");
_session.execute("CREATE INDEX IF NOT EXISTS artist_sort_name_nocase_idx ON artist(sort_name COLLATE NOCASE)");
_session.execute("CREATE INDEX IF NOT EXISTS artist_mbid_idx ON artist(mbid)");
_session.execute("CREATE INDEX IF NOT EXISTS auth_token_user_idx ON auth_token(user_id)");
_session.execute("CREATE INDEX IF NOT EXISTS auth_token_expiry_idx ON auth_token(expiry)");
_session.execute("CREATE INDEX IF NOT EXISTS auth_token_value_idx ON auth_token(value)");
_session.execute("CREATE INDEX IF NOT EXISTS cluster_cluster_type_idx ON cluster(cluster_type_id)");
_session.execute("CREATE INDEX IF NOT EXISTS cluster_name_idx ON cluster(name)");
_session.execute("CREATE INDEX IF NOT EXISTS cluster_type_name_idx ON cluster_type(name)");
_session.execute("CREATE INDEX IF NOT EXISTS listen_backend_idx ON listen(backend)");
_session.execute("CREATE INDEX IF NOT EXISTS listen_id_idx ON listen(id)");
_session.execute("CREATE INDEX IF NOT EXISTS listen_user_backend_idx ON listen(user_id,backend)");
_session.execute("CREATE INDEX IF NOT EXISTS listen_user_backend_date_time ON listen(user_id, backend, date_time DESC)");
_session.execute("CREATE INDEX IF NOT EXISTS listen_track_user_backend_idx ON listen(track_id,user_id,backend)");
_session.execute("CREATE INDEX IF NOT EXISTS listen_user_track_backend_date_time_idx ON listen(user_id,track_id,backend,date_time)");
_session.execute("CREATE INDEX IF NOT EXISTS release_id_idx ON release(id)");
_session.execute("CREATE INDEX IF NOT EXISTS release_name_idx ON release(name)");
_session.execute("CREATE INDEX IF NOT EXISTS release_name_nocase_idx ON release(name COLLATE NOCASE)");
_session.execute("CREATE INDEX IF NOT EXISTS release_mbid_idx ON release(mbid)");
_session.execute("CREATE INDEX IF NOT EXISTS release_type_name_idx ON release_type(name)");
_session.execute("CREATE INDEX IF NOT EXISTS track_id_idx ON track(id)");
_session.execute("CREATE INDEX IF NOT EXISTS track_absolute_path_idx ON track(absolute_file_path)");
_session.execute("CREATE INDEX IF NOT EXISTS track_name_idx ON track(name)");
_session.execute("CREATE INDEX IF NOT EXISTS track_name_nocase_idx ON track(name COLLATE NOCASE)");
_session.execute("CREATE INDEX IF NOT EXISTS track_mbid_idx ON track(mbid)");
_session.execute("CREATE INDEX IF NOT EXISTS track_recording_mbid_idx ON track(recording_mbid)");
_session.execute("CREATE INDEX IF NOT EXISTS track_release_idx ON track(release_id)");
_session.execute("CREATE INDEX IF NOT EXISTS track_release_file_last_write_idx ON track(release_id, file_last_write)");
_session.execute("CREATE INDEX IF NOT EXISTS track_release_year_idx ON track(release_id, year)");
_session.execute("CREATE INDEX IF NOT EXISTS track_file_last_write_idx ON track(file_last_write)");
_session.execute("CREATE INDEX IF NOT EXISTS track_date_idx ON track(date)");
_session.execute("CREATE INDEX IF NOT EXISTS track_year_idx ON track(year)");
_session.execute("CREATE INDEX IF NOT EXISTS track_original_date_idx ON track(original_date)");
_session.execute("CREATE INDEX IF NOT EXISTS track_original_year_idx ON track(original_year)");
_session.execute("CREATE INDEX IF NOT EXISTS track_media_library_idx ON track(media_library_id)");
_session.execute("CREATE INDEX IF NOT EXISTS track_media_library_release_idx ON track(media_library_id, release_id)");
_session.execute("CREATE INDEX IF NOT EXISTS tracklist_name_idx ON tracklist(name)");
_session.execute("CREATE INDEX IF NOT EXISTS tracklist_user_idx ON tracklist(user_id)");
_session.execute("CREATE INDEX IF NOT EXISTS track_features_track_idx ON track_features(track_id)");
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_artist_idx ON track_artist_link(artist_id)");
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_artist_track_idx ON track_artist_link(artist_id, track_id)");
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_artist_type_idx ON track_artist_link(artist_id,type)");
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_track_idx ON track_artist_link(track_id)");
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_track_artist_idx ON track_artist_link(track_id, artist_id)");
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_track_type_idx ON track_artist_link(track_id,type)");
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_type_idx ON track_artist_link(type)");
_session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_idx ON track_bookmark(user_id)");
_session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_track_idx ON track_bookmark(user_id,track_id)");
_session.execute("CREATE INDEX IF NOT EXISTS starred_artist_user_backend_idx ON starred_artist(user_id,backend)");
_session.execute("CREATE INDEX IF NOT EXISTS starred_artist_artist_user_backend_idx ON starred_artist(artist_id,user_id,backend)");
_session.execute("CREATE INDEX IF NOT EXISTS starred_release_user_backend_idx ON starred_release(user_id,backend)");
_session.execute("CREATE INDEX IF NOT EXISTS starred_release_release_user_backend_idx ON starred_release(release_id,user_id,backend)");
_session.execute("CREATE INDEX IF NOT EXISTS starred_track_user_backend_idx ON starred_track(user_id,backend)");
_session.execute("CREATE INDEX IF NOT EXISTS starred_track_track_user_backend_idx ON starred_track(track_id,user_id,backend)");
LMS_LOG(DB, INFO, "Indexes created!");
}
void Session::vacuumIfNeeded()
{
long pageCount{};
long freeListCount{};
{
auto transaction{ createReadTransaction() };
pageCount = utils::fetchQuerySingleResult(_session.query<long>("SELECT page_count FROM pragma_page_count"));
freeListCount = utils::fetchQuerySingleResult(_session.query<long>("SELECT freelist_count FROM pragma_freelist_count"));
}
LMS_LOG(DB, INFO, "page stats: page_count = " << pageCount << ", freelist_count = " << freeListCount);
if (freeListCount >= (pageCount / 10))
vacuum();
}
void Session::vacuum()
{
LMS_SCOPED_TRACE_OVERVIEW("Database", "Vacuum");
LMS_LOG(DB, INFO, "Performing vacuum... This may take a while...");
// We manually take a lock here since vacuum cannot be inside a transaction
{
std::unique_lock lock{ _db.getMutex() };
_db.executeSql("VACUUM");
}
LMS_LOG(DB, INFO, "Vacuum complete!");
}
void Session::refreshTracingLoggerStats()
{
auto* traceLogger{ core::Service<core::tracing::ITraceLogger>::get() };
if (!traceLogger)
return;
auto transaction{ createReadTransaction() };
traceLogger->setMetadata("db_artist_count", std::to_string(db::Artist::getCount(*this)));
traceLogger->setMetadata("db_cluster_count", std::to_string(db::Cluster::getCount(*this)));
traceLogger->setMetadata("db_cluster_type_count", std::to_string(db::ClusterType::getCount(*this)));
traceLogger->setMetadata("db_starred_artist_count", std::to_string(db::StarredArtist::getCount(*this)));
traceLogger->setMetadata("db_starred_release_count", std::to_string(db::StarredRelease::getCount(*this)));
traceLogger->setMetadata("db_starred_track_count", std::to_string(db::StarredTrack::getCount(*this)));
traceLogger->setMetadata("db_track_bookmark_count", std::to_string(db::TrackBookmark::getCount(*this)));
traceLogger->setMetadata("db_listen_count", std::to_string(db::Listen::getCount(*this)));
traceLogger->setMetadata("db_release_count", std::to_string(db::Release::getCount(*this)));
traceLogger->setMetadata("db_track_count", std::to_string(db::Track::getCount(*this)));
}
void Session::fullAnalyze()
{
LMS_SCOPED_TRACE_OVERVIEW("Database", "Analyze");
LMS_LOG(DB, INFO, "Performing database analyze... This may take a while...");
// first select all the tables and indexes, and then analyze one by one in order to not have a big lock
std::vector<std::string> entries;
retrieveEntriesToAnalyze(entries);
for (const std::string& entry : entries)
analyzeEntry(entry);
LMS_LOG(DB, INFO, "Analyze complete!");
}
void Session::retrieveEntriesToAnalyze(std::vector<std::string>& entryList)
{
auto transaction{ createReadTransaction() };
entryList = utils::fetchQueryResults(_session.query<std::string>("SELECT name FROM sqlite_master WHERE type='table' OR type ='index'"));
}
void Session::analyzeEntry(const std::string& entry)
{
LMS_SCOPED_TRACE_DETAILED_WITH_ARG("Database", "AnalyzeEntry", "Entry", entry);
LMS_LOG(DB, DEBUG, "Analyzing " << entry);
{ {
auto transaction{ createWriteTransaction() }; auto transaction{ createWriteTransaction() };
_session.execute("ANALYZE"); _session.execute("ANALYZE " + entry);
} }
LMS_LOG(DB, INFO, "Database Analyze complete"); LMS_LOG(DB, DEBUG, "Analyzing " << entry << ": done!");
} }
void Session::optimize()
{
LMS_SCOPED_TRACE_DETAILED("Database", "Optimize");
LMS_LOG(DB, INFO, "Optimizing database...");
{
auto transaction{ createWriteTransaction() };
_session.execute("PRAGMA optimize");
}
LMS_LOG(DB, INFO, "Database optimizing complete");
}
} // namespace lms::db } // namespace lms::db
+7 -9
View File
@@ -38,40 +38,38 @@ namespace lms::db
StarredArtist::pointer StarredArtist::create(Session& session, ObjectPtr<Artist> artist, ObjectPtr<User> user, FeedbackBackend backend) StarredArtist::pointer StarredArtist::create(Session& session, ObjectPtr<Artist> artist, ObjectPtr<User> user, FeedbackBackend backend)
{ {
return session.getDboSession().add(std::unique_ptr<StarredArtist> {new StarredArtist{ artist, user, backend }}); return session.getDboSession()->add(std::unique_ptr<StarredArtist> {new StarredArtist{ artist, user, backend }});
} }
std::size_t StarredArtist::getCount(Session& session) std::size_t StarredArtist::getCount(Session& session)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().query<int>("SELECT COUNT(*) FROM starred_artist"); return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM starred_artist"));
} }
StarredArtist::pointer StarredArtist::find(Session& session, StarredArtistId id) StarredArtist::pointer StarredArtist::find(Session& session, StarredArtistId id)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().find<StarredArtist>().where("id = ?").bind(id).resultValue(); return utils::fetchQuerySingleResult(session.getDboSession()->find<StarredArtist>().where("id = ?").bind(id));
} }
StarredArtist::pointer StarredArtist::find(Session& session, ArtistId artistId, UserId userId) StarredArtist::pointer StarredArtist::find(Session& session, ArtistId artistId, UserId userId)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().query<Wt::Dbo::ptr<StarredArtist>>("SELECT s_a from starred_artist s_a") return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<StarredArtist>>("SELECT s_a from starred_artist s_a")
.join("user u ON u.id = s_a.user_id") .join("user u ON u.id = s_a.user_id")
.where("s_a.artist_id = ?").bind(artistId) .where("s_a.artist_id = ?").bind(artistId)
.where("s_a.user_id = ?").bind(userId) .where("s_a.user_id = ?").bind(userId)
.where("s_a.backend = u.feedback_backend") .where("s_a.backend = u.feedback_backend"));
.resultValue();
} }
StarredArtist::pointer StarredArtist::find(Session& session, ArtistId artistId, UserId userId, FeedbackBackend backend) StarredArtist::pointer StarredArtist::find(Session& session, ArtistId artistId, UserId userId, FeedbackBackend backend)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().find<StarredArtist>() return utils::fetchQuerySingleResult(session.getDboSession()->find<StarredArtist>()
.where("artist_id = ?").bind(artistId) .where("artist_id = ?").bind(artistId)
.where("user_id = ?").bind(userId) .where("user_id = ?").bind(userId)
.where("backend = ?").bind(backend) .where("backend = ?").bind(backend));
.resultValue();
} }
void StarredArtist::setDateTime(const Wt::WDateTime& dateTime) void StarredArtist::setDateTime(const Wt::WDateTime& dateTime)
+7 -9
View File
@@ -38,40 +38,38 @@ namespace lms::db
StarredRelease::pointer StarredRelease::create(Session& session, ObjectPtr<Release> release, ObjectPtr<User> user, FeedbackBackend backend) StarredRelease::pointer StarredRelease::create(Session& session, ObjectPtr<Release> release, ObjectPtr<User> user, FeedbackBackend backend)
{ {
return session.getDboSession().add(std::unique_ptr<StarredRelease>{new StarredRelease{ release, user, backend }}); return session.getDboSession()->add(std::unique_ptr<StarredRelease>{new StarredRelease{ release, user, backend }});
} }
std::size_t StarredRelease::getCount(Session& session) std::size_t StarredRelease::getCount(Session& session)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().query<int>("SELECT COUNT(*) FROM starred_release"); return session.getDboSession()->query<int>("SELECT COUNT(*) FROM starred_release");
} }
StarredRelease::pointer StarredRelease::find(Session& session, StarredReleaseId id) StarredRelease::pointer StarredRelease::find(Session& session, StarredReleaseId id)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().find<StarredRelease>().where("id = ?").bind(id).resultValue(); return utils::fetchQuerySingleResult(session.getDboSession()->find<StarredRelease>().where("id = ?").bind(id));
} }
StarredRelease::pointer StarredRelease::find(Session& session, ReleaseId releaseId, UserId userId) StarredRelease::pointer StarredRelease::find(Session& session, ReleaseId releaseId, UserId userId)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().query<Wt::Dbo::ptr<StarredRelease>>("SELECT s_r from starred_release s_r") return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<StarredRelease>>("SELECT s_r from starred_release s_r")
.join("user u ON u.id = s_r.user_id") .join("user u ON u.id = s_r.user_id")
.where("s_r.release_id = ?").bind(releaseId) .where("s_r.release_id = ?").bind(releaseId)
.where("s_r.user_id = ?").bind(userId) .where("s_r.user_id = ?").bind(userId)
.where("s_r.backend = u.feedback_backend") .where("s_r.backend = u.feedback_backend"));
.resultValue();
} }
StarredRelease::pointer StarredRelease::find(Session& session, ReleaseId releaseId, UserId userId, FeedbackBackend backend) StarredRelease::pointer StarredRelease::find(Session& session, ReleaseId releaseId, UserId userId, FeedbackBackend backend)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().find<StarredRelease>() return utils::fetchQuerySingleResult(session.getDboSession()->find<StarredRelease>()
.where("release_id = ?").bind(releaseId) .where("release_id = ?").bind(releaseId)
.where("user_id = ?").bind(userId) .where("user_id = ?").bind(userId)
.where("backend = ?").bind(backend) .where("backend = ?").bind(backend));
.resultValue();
} }
void StarredRelease::setDateTime(const Wt::WDateTime& dateTime) void StarredRelease::setDateTime(const Wt::WDateTime& dateTime)
+11 -14
View File
@@ -38,56 +38,53 @@ namespace lms::db
StarredTrack::pointer StarredTrack::create(Session& session, ObjectPtr<Track> track, ObjectPtr<User> user, FeedbackBackend backend) StarredTrack::pointer StarredTrack::create(Session& session, ObjectPtr<Track> track, ObjectPtr<User> user, FeedbackBackend backend)
{ {
return session.getDboSession().add(std::unique_ptr<StarredTrack> {new StarredTrack{ track, user, backend }}); return session.getDboSession()->add(std::unique_ptr<StarredTrack> {new StarredTrack{ track, user, backend }});
} }
std::size_t StarredTrack::getCount(Session& session) std::size_t StarredTrack::getCount(Session& session)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().query<int>("SELECT COUNT(*) FROM starred_track"); return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM starred_track"));
} }
StarredTrack::pointer StarredTrack::find(Session& session, StarredTrackId id) StarredTrack::pointer StarredTrack::find(Session& session, StarredTrackId id)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().find<StarredTrack>().where("id = ?").bind(id).resultValue(); return utils::fetchQuerySingleResult(session.getDboSession()->find<StarredTrack>().where("id = ?").bind(id));
} }
StarredTrack::pointer StarredTrack::find(Session& session, TrackId trackId, UserId userId) StarredTrack::pointer StarredTrack::find(Session& session, TrackId trackId, UserId userId)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().query<Wt::Dbo::ptr<StarredTrack>>("SELECT s_t from starred_track s_t") return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<StarredTrack>>("SELECT s_t from starred_track s_t")
.join("user u ON u.id = s_t.user_id") .join("user u ON u.id = s_t.user_id")
.where("s_t.track_id = ?").bind(trackId) .where("s_t.track_id = ?").bind(trackId)
.where("s_t.user_id = ?").bind(userId) .where("s_t.user_id = ?").bind(userId)
.where("s_t.backend = u.feedback_backend") .where("s_t.backend = u.feedback_backend"));
.resultValue();
} }
StarredTrack::pointer StarredTrack::find(Session& session, TrackId trackId, UserId userId, FeedbackBackend backend) StarredTrack::pointer StarredTrack::find(Session& session, TrackId trackId, UserId userId, FeedbackBackend backend)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().find<StarredTrack>() return utils::fetchQuerySingleResult(session.getDboSession()->find<StarredTrack>()
.where("track_id = ?").bind(trackId) .where("track_id = ?").bind(trackId)
.where("user_id = ?").bind(userId) .where("user_id = ?").bind(userId)
.where("backend = ?").bind(backend) .where("backend = ?").bind(backend));
.resultValue();
} }
bool StarredTrack::exists(Session& session, TrackId trackId, UserId userId, FeedbackBackend backend) bool StarredTrack::exists(Session& session, TrackId trackId, UserId userId, FeedbackBackend backend)
{ {
return session.getDboSession().query<int>("SELECT 1 from starred_track") return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT 1 from starred_track")
.where("track_id = ?").bind(trackId) .where("track_id = ?").bind(trackId)
.where("user_id = ?").bind(userId) .where("user_id = ?").bind(userId)
.where("backend = ?").bind(backend) .where("backend = ?").bind(backend));
.resultValue() == 1;
} }
RangeResults<StarredTrackId> StarredTrack::find(Session& session, const FindParameters& params) RangeResults<StarredTrackId> StarredTrack::find(Session& session, const FindParameters& params)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ session.getDboSession().query<StarredTrackId>("SELECT DISTINCT s_t.id FROM starred_track s_t") }; auto query{ session.getDboSession()->query<StarredTrackId>("SELECT DISTINCT s_t.id FROM starred_track s_t") };
if (params.backend) if (params.backend)
query.where("s_t.backend = ?").bind(*params.backend); query.where("s_t.backend = ?").bind(*params.backend);
@@ -96,7 +93,7 @@ namespace lms::db
if (params.user.isValid()) if (params.user.isValid())
query.where("s_t.user_id = ?").bind(params.user); query.where("s_t.user_id = ?").bind(params.user);
return utils::execQuery<StarredTrackId>(query, params.range); return utils::execRangeQuery<StarredTrackId>(query, params.range);
} }
void StarredTrack::setDateTime(const Wt::WDateTime& dateTime) void StarredTrack::setDateTime(const Wt::WDateTime& dateTime)
+69 -100
View File
@@ -32,6 +32,7 @@
#include "core/ILogger.hpp" #include "core/ILogger.hpp"
#include "IdTypeTraits.hpp" #include "IdTypeTraits.hpp"
#include "PathTraits.hpp"
#include "SqlQuery.hpp" #include "SqlQuery.hpp"
#include "StringViewTraits.hpp" #include "StringViewTraits.hpp"
#include "Utils.hpp" #include "Utils.hpp"
@@ -45,7 +46,7 @@ namespace lms::db
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ session.getDboSession().query<ResultType>("SELECT " + std::string{ itemToSelect } + " FROM track t") }; auto query{ session.getDboSession()->query<ResultType>("SELECT " + std::string{ itemToSelect } + " FROM track t") };
assert(params.keywords.empty() || params.name.empty()); assert(params.keywords.empty() || params.name.empty());
for (std::string_view keyword : params.keywords) for (std::string_view keyword : params.keywords)
@@ -152,6 +153,9 @@ namespace lms::db
{ {
case TrackSortMethod::None: case TrackSortMethod::None:
break; break;
case TrackSortMethod::Id:
query.orderBy("t.id");
break;
case TrackSortMethod::LastWritten: case TrackSortMethod::LastWritten:
query.orderBy("t.file_last_write DESC"); query.orderBy("t.file_last_write DESC");
break; break;
@@ -178,12 +182,12 @@ namespace lms::db
return query; return query;
} }
template <typename ResultType> template <typename ResultType>
Wt::Dbo::Query<ResultType> createQuery(Session& session, const Track::FindParameters& params) Wt::Dbo::Query<ResultType> createQuery(Session& session, const Track::FindParameters& params)
{ {
std::string_view itemToSelect; std::string_view itemToSelect;
if constexpr (std::is_same_v<ResultType, TrackId>) if constexpr (std::is_same_v<ResultType, TrackId>)
itemToSelect = "t.id"; itemToSelect = "t.id";
else if constexpr (std::is_same_v<ResultType, Wt::Dbo::ptr<Track>>) else if constexpr (std::is_same_v<ResultType, Wt::Dbo::ptr<Track>>)
@@ -195,154 +199,111 @@ namespace lms::db
} }
} }
Track::Track(const std::filesystem::path& p) Track::pointer Track::create(Session& session)
: _filePath{ p.string() }
{ {
} return session.getDboSession()->add(std::make_unique<Track>());
Track::pointer Track::create(Session& session, const std::filesystem::path& p)
{
return session.getDboSession().add(std::unique_ptr<Track> {new Track{ p }});
} }
std::size_t Track::getCount(Session& session) std::size_t Track::getCount(Session& session)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().query<int>("SELECT COUNT(*) FROM track"); return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM track"));
} }
Track::pointer Track::findByPath(Session& session, const std::filesystem::path& p) Track::pointer Track::findByPath(Session& session, const std::filesystem::path& p)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().find<Track>().where("file_path = ?").bind(p.string()).resultValue(); return utils::fetchQuerySingleResult(session.getDboSession()->find<Track>().where("absolute_file_path = ?").bind(p.string()));
} }
Track::pointer Track::find(Session& session, TrackId id) Track::pointer Track::find(Session& session, TrackId id)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().find<Track>() return utils::fetchQuerySingleResult(session.getDboSession()->find<Track>()
.where("id = ?").bind(id) .where("id = ?").bind(id));
.resultValue();
} }
void Track::find(Session& session, TrackId& lastRetrievedTrack, std::size_t batchSize, bool& moreResults, const std::function<void(const Track::pointer&)>& func) void Track::find(Session& session, TrackId& lastRetrievedTrack, std::size_t count, const std::function<void(const Track::pointer&)>& func, MediaLibraryId library)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto collection{ session.getDboSession().find<Track>() auto query{ session.getDboSession()->find<Track>()
.orderBy("id") .orderBy("id")
.where("id > ?").bind(lastRetrievedTrack) .where("id > ?").bind(lastRetrievedTrack)
.limit(static_cast<int>(batchSize) + 1) .limit(static_cast<int>(count)) };
.resultList() };
moreResults = false; if (library.isValid())
query.where("media_library_id = ?").bind(library);
std::size_t count{}; utils::forEachQueryResult(query, [&](const Track::pointer& track)
for (auto itResult{ collection.begin() }; itResult != collection.end(); ++itResult)
{
if (count++ == batchSize)
{ {
moreResults = true; func(track);
break; lastRetrievedTrack = track->getId();
} });
func(*itResult);
lastRetrievedTrack = (*itResult)->getId();
}
} }
bool Track::exists(Session& session, TrackId id) bool Track::exists(Session& session, TrackId id)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().query<int>("SELECT 1 from track").where("id = ?").bind(id).resultValue() == 1; return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT 1 from track").where("id = ?").bind(id)) == 1;
} }
std::vector<Track::pointer> Track::findByMBID(Session& session, const core::UUID& mbid) std::vector<Track::pointer> Track::findByMBID(Session& session, const core::UUID& mbid)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto res{ session.getDboSession().find<Track>() return utils::fetchQueryResults<Track::pointer>(session.getDboSession()->find<Track>()
.where("mbid = ?").bind(std::string {mbid.getAsString()}) .where("mbid = ?").bind(mbid.getAsString()));
.resultList() };
return std::vector<Track::pointer>(res.begin(), res.end());
} }
std::vector<Track::pointer> Track::findByRecordingMBID(Session& session, const core::UUID& mbid) std::vector<Track::pointer> Track::findByRecordingMBID(Session& session, const core::UUID& mbid)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto res{ session.getDboSession().find<Track>() return utils::fetchQueryResults<Track::pointer>(session.getDboSession()->find<Track>()
.where("recording_mbid = ?").bind(std::string {mbid.getAsString()}) .where("recording_mbid = ?").bind(mbid.getAsString()));
.resultList() };
return std::vector<Track::pointer>(res.begin(), res.end());
}
RangeResults<Track::PathResult> Track::findPaths(Session& session, std::optional<Range> range)
{
using QueryResultType = std::tuple<TrackId, std::string>;
session.checkReadTransaction();
// TODO Dbo traits on filesystem
auto query{ session.getDboSession().query<QueryResultType>("SELECT id, file_path FROM track") };
RangeResults<QueryResultType> queryResults{ utils::execQuery<QueryResultType>(query, range) };
RangeResults<PathResult> res;
res.range = queryResults.range;
res.moreResults = queryResults.moreResults;
res.results.reserve(queryResults.results.size());
std::transform(std::cbegin(queryResults.results), std::cend(queryResults.results), std::back_inserter(res.results),
[](const QueryResultType& queryResult)
{
return PathResult{ std::get<TrackId>(queryResult), std::move(std::get<std::string>(queryResult)) };
});
return res;
} }
RangeResults<TrackId> Track::findIdsTrackMBIDDuplicates(Session& session, std::optional<Range> range) RangeResults<TrackId> Track::findIdsTrackMBIDDuplicates(Session& session, std::optional<Range> range)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ session.getDboSession().query<TrackId>("SELECT track.id FROM track WHERE mbid in (SELECT mbid FROM track WHERE mbid <> '' GROUP BY mbid HAVING COUNT (*) > 1)") auto query{ session.getDboSession()->query<TrackId>("SELECT track.id FROM track WHERE mbid in (SELECT mbid FROM track WHERE mbid <> '' GROUP BY mbid HAVING COUNT (*) > 1)")
.orderBy("track.release_id,track.disc_number,track.track_number,track.mbid") }; .orderBy("track.release_id,track.disc_number,track.track_number,track.mbid") };
return utils::execQuery<TrackId>(query, range); return utils::execRangeQuery<TrackId>(query, range);
} }
RangeResults<TrackId> Track::findIdsWithRecordingMBIDAndMissingFeatures(Session& session, std::optional<Range> range) RangeResults<TrackId> Track::findIdsWithRecordingMBIDAndMissingFeatures(Session& session, std::optional<Range> range)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ session.getDboSession().query<TrackId>("SELECT t.id FROM track t") auto query{ session.getDboSession()->query<TrackId>("SELECT t.id FROM track t")
.where("LENGTH(t.recording_mbid) > 0") .where("LENGTH(t.recording_mbid) > 0")
.where("NOT EXISTS (SELECT * FROM track_features t_f WHERE t_f.track_id = t.id)") }; .where("NOT EXISTS (SELECT * FROM track_features t_f WHERE t_f.track_id = t.id)") };
return utils::execQuery<TrackId>(query, range); return utils::execRangeQuery<TrackId>(query, range);
} }
std::vector<Cluster::pointer> Track::getClusters() const std::vector<Cluster::pointer> Track::getClusters() const
{ {
return std::vector<Cluster::pointer>(_clusters.begin(), _clusters.end()); return utils::fetchQueryResults<Cluster::pointer>(_clusters.find());
} }
std::vector<ClusterId> Track::getClusterIds() const std::vector<ClusterId> Track::getClusterIds() const
{ {
assert(session()); assert(session());
auto res{ session()->query<ClusterId> const auto query{ session()->query<ClusterId>
("SELECT DISTINCT c.id FROM cluster c INNER JOIN track_cluster t_c ON t_c.cluster_id = c.id INNER JOIN track t ON t.id = t_c.track_id") ("SELECT t_c.cluster_id FROM track_cluster t_c")
.where("t.id = ?").bind(getId()) .where("t_c.track_id = ?").bind(getId())
.resultList() }; .groupBy("t_c.cluster_id") };
return std::vector<ClusterId>(res.begin(), res.end()); return utils::fetchQueryResults(query);
} }
RangeResults<TrackId> Track::findIds(Session& session, const FindParameters& parameters) RangeResults<TrackId> Track::findIds(Session& session, const FindParameters& parameters)
@@ -350,7 +311,7 @@ namespace lms::db
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ createQuery<TrackId>(session, parameters) }; auto query{ createQuery<TrackId>(session, parameters) };
return utils::execQuery<TrackId>(query, parameters.range); return utils::execRangeQuery<TrackId>(query, parameters.range);
} }
RangeResults<Track::pointer> Track::find(Session& session, const FindParameters& parameters) RangeResults<Track::pointer> Track::find(Session& session, const FindParameters& parameters)
@@ -358,23 +319,23 @@ namespace lms::db
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ createQuery<Wt::Dbo::ptr<Track>>(session, parameters) }; auto query{ createQuery<Wt::Dbo::ptr<Track>>(session, parameters) };
return utils::execQuery<Track::pointer>(query, parameters.range); return utils::execRangeQuery<Track::pointer>(query, parameters.range);
} }
void Track::find(Session& session, const FindParameters& params, std::function<void(const Track::pointer&)> func) void Track::find(Session& session, const FindParameters& params, const std::function<void(const Track::pointer&)>& func)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ createQuery<Wt::Dbo::ptr<Track>>(session, params)}; auto query{ createQuery<Wt::Dbo::ptr<Track>>(session, params) };
utils::execQuery(query, params.range, func); utils::forEachQueryRangeResult(query, params.range, func);
} }
void Track::find(Session& session, const FindParameters& params, bool& moreResults, std::function<void(const Track::pointer&)> func) void Track::find(Session& session, const FindParameters& params, bool& moreResults, const std::function<void(const Track::pointer&)>& func)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ createQuery<Wt::Dbo::ptr<Track>>(session, params)}; auto query{ createQuery<Wt::Dbo::ptr<Track>>(session, params) };
utils::execQuery(query, params.range, moreResults, func); utils::forEachQueryRangeResult(query, params.range, moreResults, func);
} }
RangeResults<TrackId> Track::findSimilarTrackIds(Session& session, const std::vector<TrackId>& tracks, std::optional<Range> range) RangeResults<TrackId> Track::findSimilarTrackIds(Session& session, const std::vector<TrackId>& tracks, std::optional<Range> range)
@@ -390,7 +351,7 @@ namespace lms::db
oss << "?"; oss << "?";
} }
auto query{ session.getDboSession().query<TrackId>( auto query{ session.getDboSession()->query<TrackId>(
"SELECT t.id FROM track t" "SELECT t.id FROM track t"
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id" " INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
" AND t_c.cluster_id IN (SELECT DISTINCT c.id FROM cluster c INNER JOIN track_cluster t_c ON t_c.cluster_id = c.id WHERE t_c.track_id IN (" + oss.str() + "))" " AND t_c.cluster_id IN (SELECT DISTINCT c.id FROM cluster c INNER JOIN track_cluster t_c ON t_c.cluster_id = c.id WHERE t_c.track_id IN (" + oss.str() + "))"
@@ -404,7 +365,19 @@ namespace lms::db
for (TrackId trackId : tracks) for (TrackId trackId : tracks)
query.bind(trackId); query.bind(trackId);
return utils::execQuery<TrackId>(query, range); return utils::execRangeQuery<TrackId>(query, range);
}
void Track::setAbsoluteFilePath(const std::filesystem::path& filePath)
{
assert(filePath.is_absolute());
_absoluteFilePath = filePath;
}
void Track::setRelativeFilePath(const std::filesystem::path& filePath)
{
assert(filePath.is_relative());
_relativeFilePath = filePath;
} }
void Track::clearArtistLinks() void Track::clearArtistLinks()
@@ -465,8 +438,7 @@ namespace lms::db
query.where("t.id = ?").bind(getId()); query.where("t.id = ?").bind(getId());
auto res{ query.resultList() }; return utils::fetchQueryResults<Artist::pointer>(query);
return std::vector<Artist::pointer>(std::begin(res), std::end(res));
} }
std::vector<ArtistId> Track::getArtistIds(core::EnumSet<TrackArtistLinkType> linkTypes) const std::vector<ArtistId> Track::getArtistIds(core::EnumSet<TrackArtistLinkType> linkTypes) const
@@ -476,8 +448,7 @@ namespace lms::db
std::ostringstream oss; std::ostringstream oss;
oss << oss <<
"SELECT DISTINCT a.id from artist a" "SELECT t_a_l.artist_id FROM track_artist_link t_a_l"
" INNER JOIN track_artist_link t_a_l ON a.id = t_a_l.artist_id"
" INNER JOIN track t ON t.id = t_a_l.track_id"; " INNER JOIN track t ON t.id = t_a_l.track_id";
if (!linkTypes.empty()) if (!linkTypes.empty())
@@ -500,14 +471,14 @@ namespace lms::db
query.bind(type); query.bind(type);
query.where("t.id = ?").bind(getId()); query.where("t.id = ?").bind(getId());
query.groupBy("t_a_l.artist_id");
auto res{ query.resultList() }; return utils::fetchQueryResults(query);
return std::vector<ArtistId>(std::cbegin(res), std::cend(res));
} }
std::vector<TrackArtistLink::pointer> Track::getArtistLinks() const std::vector<TrackArtistLink::pointer> Track::getArtistLinks() const
{ {
return std::vector<TrackArtistLink::pointer>(_trackArtistLinks.begin(), _trackArtistLinks.end()); return utils::fetchQueryResults<TrackArtistLink::pointer>(_trackArtistLinks.find());
} }
std::vector<std::vector<Cluster::pointer>> Track::getClusterGroups(const std::vector<ClusterTypeId>& clusterTypeIds, std::size_t size) const std::vector<std::vector<Cluster::pointer>> Track::getClusterGroups(const std::vector<ClusterTypeId>& clusterTypeIds, std::size_t size) const
@@ -535,14 +506,12 @@ namespace lms::db
for (const std::string& bindArg : where.getBindArgs()) for (const std::string& bindArg : where.getBindArgs())
query.bind(bindArg); query.bind(bindArg);
auto queryRes{ query.resultList() };
std::map<ClusterTypeId, std::vector<Cluster::pointer>> clusters; std::map<ClusterTypeId, std::vector<Cluster::pointer>> clusters;
for (const Wt::Dbo::ptr<Cluster>& cluster : queryRes) utils::forEachQueryResult(query, [&](const Cluster::pointer& cluster)
{ {
if (clusters[cluster->getType()->getId()].size() < size) if (clusters[cluster->getType()->getId()].size() < size)
clusters[cluster->getType()->getId()].push_back(cluster); clusters[cluster->getType()->getId()].push_back(cluster);
} });
std::vector<std::vector<Cluster::pointer>> res; std::vector<std::vector<Cluster::pointer>> res;
for (const auto& [type, clusters] : clusters) for (const auto& [type, clusters] : clusters)
+47 -22
View File
@@ -30,26 +30,26 @@ namespace lms::db
{ {
namespace namespace
{ {
Wt::Dbo::Query<TrackArtistLinkId> createQuery(Session& session, const TrackArtistLink::FindParameters& params) Wt::Dbo::Query<Wt::Dbo::ptr<TrackArtistLink>> createQuery(Session& session, const TrackArtistLink::FindParameters& params)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ session.getDboSession().query<TrackArtistLinkId>("SELECT DISTINCT t_a_l.id FROM track_artist_link t_a_l") }; auto query{ session.getDboSession()->query<Wt::Dbo::ptr<TrackArtistLink>>("SELECT t_a_l FROM track_artist_link t_a_l") };
if (params.linkType) if (params.linkType)
query.where("t_a_l.type = ?").bind(*params.linkType); query.where("t_a_l.type = ?").bind(*params.linkType);
if (params.track.isValid() || params.release.isValid()) if (params.track.isValid())
query.join("track t ON t.id = t_a_l.track_id"); query.where("t_a_l.track_id = ?").bind(params.track);
if (params.artist.isValid()) if (params.artist.isValid())
query.join("artist a ON a.id = t_a_l.artist_id"); query.where("t_a_l.artist_id = ?").bind(params.artist);
if (params.release.isValid()) if (params.release.isValid())
{
query.join("track t ON t.id = t_a_l.track_id");
query.where("t.release_id = ?").bind(params.release); query.where("t.release_id = ?").bind(params.release);
}
if (params.track.isValid())
query.where("t.id = ?").bind(params.track);
return query; return query;
} }
@@ -67,8 +67,8 @@ namespace lms::db
{ {
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)) };
session.getDboSession().flush(); session.getDboSession()->flush();
return res; return res;
} }
@@ -76,36 +76,61 @@ namespace lms::db
TrackArtistLink::pointer TrackArtistLink::find(Session& session, TrackArtistLinkId id) TrackArtistLink::pointer TrackArtistLink::find(Session& session, TrackArtistLinkId id)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().find<TrackArtistLink>().where("id = ?").bind(id).resultValue(); return utils::fetchQuerySingleResult(session.getDboSession()->find<TrackArtistLink>().where("id = ?").bind(id));
} }
RangeResults<TrackArtistLinkId> TrackArtistLink::find(Session& session, const FindParameters& params) void TrackArtistLink::find(Session& session, TrackId trackId, const std::function<void(const TrackArtistLink::pointer& link, const ObjectPtr<Artist>& artist)>& func)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ createQuery(session, params) }; using ResultType = std::tuple < Wt::Dbo::ptr<TrackArtistLink>, Wt::Dbo::ptr<Artist>>;
return utils::execQuery<TrackArtistLinkId>(query, params.range);
const auto query{ session.getDboSession()->query<ResultType>("SELECT t_a_l, a FROM track_artist_link t_a_l")
.join("artist a ON t_a_l.artist_id = a.id")
.where("t_a_l.track_id = ?").bind(trackId) };
utils::forEachQueryResult(query, [&](const ResultType& result)
{
func(std::get<Wt::Dbo::ptr<TrackArtistLink>>(result), std::get<Wt::Dbo::ptr<Artist>>(result));
});
}
void TrackArtistLink::find(Session& session, const FindParameters& parameters, const std::function<void(const TrackArtistLink::pointer&)>& func)
{
const auto query{ createQuery(session, parameters) };
utils::forEachQueryResult(query, [&](const TrackArtistLink::pointer& link)
{
func(link);
});
} }
core::EnumSet<TrackArtistLinkType> TrackArtistLink::findUsedTypes(Session& session) core::EnumSet<TrackArtistLinkType> TrackArtistLink::findUsedTypes(Session& session)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto res{ session.getDboSession().query<TrackArtistLinkType>("SELECT DISTINCT type from track_artist_link").resultList() }; const auto query{ session.getDboSession()->query<TrackArtistLinkType>("SELECT DISTINCT type from track_artist_link") };
return core::EnumSet<TrackArtistLinkType>(std::begin(res), std::end(res)); core::EnumSet<TrackArtistLinkType> res;
utils::forEachQueryResult(query, [&](TrackArtistLinkType linkType)
{
res.insert(linkType);
});
return res;
} }
core::EnumSet<TrackArtistLinkType> TrackArtistLink::findUsedTypes(Session& session, ArtistId artistId) core::EnumSet<TrackArtistLinkType> TrackArtistLink::findUsedTypes(Session& session, ArtistId artistId)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto res{ session.getDboSession() const auto query{ session.getDboSession()->query<TrackArtistLinkType>("SELECT DISTINCT type from track_artist_link")
.query<TrackArtistLinkType>("SELECT DISTINCT type from track_artist_link") .where("artist_id = ?").bind(artistId) };
.where("artist_id = ?").bind(artistId)
.resultList() };
return core::EnumSet<TrackArtistLinkType>(std::begin(res), std::end(res)); core::EnumSet<TrackArtistLinkType> res;
utils::forEachQueryResult(query, [&](TrackArtistLinkType linkType)
{
res.insert(linkType);
});
return res;
} }
} }
+8 -10
View File
@@ -35,43 +35,41 @@ namespace lms::db
TrackBookmark::pointer TrackBookmark::create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track) TrackBookmark::pointer TrackBookmark::create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track)
{ {
return session.getDboSession().add(std::unique_ptr<TrackBookmark> {new TrackBookmark{ user, track }}); return session.getDboSession()->add(std::unique_ptr<TrackBookmark> {new TrackBookmark{ user, track }});
} }
std::size_t TrackBookmark::getCount(Session& session) std::size_t TrackBookmark::getCount(Session& session)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().query<int>("SELECT COUNT(*) FROM track_bookmark"); return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM track_bookmark"));
} }
RangeResults<TrackBookmarkId> TrackBookmark::find(Session& session, UserId userId, std::optional<Range> range) RangeResults<TrackBookmarkId> TrackBookmark::find(Session& session, UserId userId, std::optional<Range> range)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ session.getDboSession().query<TrackBookmarkId>("SELECT id from track_bookmark") auto query{ session.getDboSession()->query<TrackBookmarkId>("SELECT id from track_bookmark")
.where("user_id = ?").bind(userId) }; .where("user_id = ?").bind(userId) };
return utils::execQuery<TrackBookmarkId>(query, range); return utils::execRangeQuery<TrackBookmarkId>(query, range);
} }
TrackBookmark::pointer TrackBookmark::find(Session& session, UserId userId, TrackId trackId) TrackBookmark::pointer TrackBookmark::find(Session& session, UserId userId, TrackId trackId)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().find<TrackBookmark>() return utils::fetchQuerySingleResult(session.getDboSession()->find<TrackBookmark>()
.where("user_id = ?").bind(userId) .where("user_id = ?").bind(userId)
.where("track_id = ?").bind(trackId) .where("track_id = ?").bind(trackId));
.resultValue();
} }
TrackBookmark::pointer TrackBookmark::find(Session& session, TrackBookmarkId id) TrackBookmark::pointer TrackBookmark::find(Session& session, TrackBookmarkId id)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().find<TrackBookmark>() return utils::fetchQuerySingleResult(session.getDboSession()->find<TrackBookmark>()
.where("id = ?").bind(id) .where("id = ?").bind(id));
.resultValue();
} }
} // namespace lms::db } // namespace lms::db
+8 -10
View File
@@ -38,41 +38,39 @@ namespace lms::db {
TrackFeatures::pointer TrackFeatures::create(Session& session, ObjectPtr<Track> track, const std::string& jsonEncodedFeatures) TrackFeatures::pointer TrackFeatures::create(Session& session, ObjectPtr<Track> track, const std::string& jsonEncodedFeatures)
{ {
return session.getDboSession().add(std::unique_ptr<TrackFeatures> {new TrackFeatures{ track, jsonEncodedFeatures }}); return session.getDboSession()->add(std::unique_ptr<TrackFeatures> {new TrackFeatures{ track, jsonEncodedFeatures }});
} }
std::size_t TrackFeatures::getCount(Session& session) std::size_t TrackFeatures::getCount(Session& session)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().query<int>("SELECT COUNT(*) FROM track_features"); return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM track_features"));
} }
TrackFeatures::pointer TrackFeatures::find(Session& session, TrackFeaturesId id) TrackFeatures::pointer TrackFeatures::find(Session& session, TrackFeaturesId id)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().find<TrackFeatures>() return utils::fetchQuerySingleResult(session.getDboSession()->find<TrackFeatures>()
.where("id = ?").bind(id) .where("id = ?").bind(id));
.resultValue();
} }
TrackFeatures::pointer TrackFeatures::find(Session& session, TrackId trackId) TrackFeatures::pointer TrackFeatures::find(Session& session, TrackId trackId)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().find<TrackFeatures>() return utils::fetchQuerySingleResult(session.getDboSession()->find<TrackFeatures>()
.where("track_id = ?").bind(trackId) .where("track_id = ?").bind(trackId));
.resultValue();
} }
RangeResults<TrackFeaturesId> TrackFeatures::find(Session& session, std::optional<Range> range) RangeResults<TrackFeaturesId> TrackFeatures::find(Session& session, std::optional<Range> range)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ session.getDboSession().query<TrackFeaturesId>("SELECT id from track_features") }; auto query{ session.getDboSession()->query<TrackFeaturesId>("SELECT id from track_features") };
return utils::execQuery<TrackFeaturesId>(query, range); return utils::execRangeQuery<TrackFeaturesId>(query, range);
} }
FeatureValues TrackFeatures::getFeatureValues(const FeatureName& featureNode) const FeatureValues TrackFeatures::getFeatureValues(const FeatureName& featureNode) const
+28 -35
View File
@@ -48,14 +48,14 @@ namespace lms::db
TrackList::pointer TrackList::create(Session& session, std::string_view name, TrackListType type, bool isPublic, ObjectPtr<User> user) TrackList::pointer TrackList::create(Session& session, std::string_view name, TrackListType type, bool isPublic, ObjectPtr<User> user)
{ {
return session.getDboSession().add(std::unique_ptr<TrackList> {new TrackList{ name, type, isPublic, user }}); return session.getDboSession()->add(std::unique_ptr<TrackList> {new TrackList{ name, type, isPublic, user }});
} }
std::size_t TrackList::getCount(Session& session) std::size_t TrackList::getCount(Session& session)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().query<int>("SELECT COUNT(*) FROM tracklist"); return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM tracklist"));
} }
@@ -64,17 +64,17 @@ namespace lms::db
session.checkReadTransaction(); session.checkReadTransaction();
assert(userId.isValid()); assert(userId.isValid());
return session.getDboSession().find<TrackList>() return utils::fetchQuerySingleResult(session.getDboSession()->find<TrackList>()
.where("name = ?").bind(name) .where("name = ?").bind(name)
.where("type = ?").bind(type) .where("type = ?").bind(type)
.where("user_id = ?").bind(userId).resultValue(); .where("user_id = ?").bind(userId));
} }
RangeResults<TrackListId> TrackList::find(Session& session, const FindParameters& params) RangeResults<TrackListId> TrackList::find(Session& session, const FindParameters& params)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ session.getDboSession().query<TrackListId>("SELECT DISTINCT t_l.id FROM tracklist t_l") }; auto query{ session.getDboSession()->query<TrackListId>("SELECT DISTINCT t_l.id FROM tracklist t_l") };
if (params.user.isValid()) if (params.user.isValid())
query.where("t_l.user_id = ?").bind(params.user); query.where("t_l.user_id = ?").bind(params.user);
@@ -117,14 +117,14 @@ namespace lms::db
break; break;
} }
return utils::execQuery<TrackListId>(query, params.range); return utils::execRangeQuery<TrackListId>(query, params.range);
} }
TrackList::pointer TrackList::find(Session& session, TrackListId id) TrackList::pointer TrackList::find(Session& session, TrackListId id)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().find<TrackList>().where("id = ?").bind(id).resultValue(); return utils::fetchQuerySingleResult(session.getDboSession()->find<TrackList>().where("id = ?").bind(id));
} }
bool TrackList::isEmpty() const bool TrackList::isEmpty() const
@@ -156,31 +156,29 @@ namespace lms::db
.where("tracklist_id = ?").bind(getId()) .where("tracklist_id = ?").bind(getId())
.orderBy("id") }; .orderBy("id") };
return utils::execQuery<TrackListEntry::pointer>(query, range); return utils::execRangeQuery<TrackListEntry::pointer>(query, range);
} }
TrackListEntry::pointer TrackList::getEntryByTrackAndDateTime(ObjectPtr<Track> track, const Wt::WDateTime& dateTime) const TrackListEntry::pointer TrackList::getEntryByTrackAndDateTime(ObjectPtr<Track> track, const Wt::WDateTime& dateTime) const
{ {
assert(session()); assert(session());
return session()->find<TrackListEntry>() return utils::fetchQuerySingleResult(session()->find<TrackListEntry>()
.where("tracklist_id = ?").bind(getId()) .where("tracklist_id = ?").bind(getId())
.where("track_id = ?").bind(track->getId()) .where("track_id = ?").bind(track->getId())
.where("date_time = ?").bind(utils::normalizeDateTime(dateTime)) .where("date_time = ?").bind(utils::normalizeDateTime(dateTime)));
.resultValue();
} }
std::vector<Cluster::pointer> TrackList::getClusters() const std::vector<Cluster::pointer> TrackList::getClusters() const
{ {
assert(session()); assert(session());
auto res{ session()->query<Wt::Dbo::ptr<Cluster>>("SELECT c from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN tracklist_entry p_e ON p_e.track_id = t.id INNER JOIN tracklist p ON p.id = p_e.tracklist_id") const auto query{ session()->query<Wt::Dbo::ptr<Cluster>>("SELECT c from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN tracklist_entry p_e ON p_e.track_id = t.id INNER JOIN tracklist p ON p.id = p_e.tracklist_id")
.where("p.id = ?").bind(getId()) .where("p.id = ?").bind(getId())
.groupBy("c.id") .groupBy("c.id")
.orderBy("COUNT(c.id) DESC") .orderBy("COUNT(c.id) DESC") };
.resultList() };
return std::vector<Cluster::pointer>(res.begin(), res.end()); return utils::fetchQueryResults<Cluster::pointer>(query);
} }
std::vector<std::vector<Cluster::pointer>> TrackList::getClusterGroups(const std::vector<ClusterTypeId>& clusterTypeIds, std::size_t size) const std::vector<std::vector<Cluster::pointer>> TrackList::getClusterGroups(const std::vector<ClusterTypeId>& clusterTypeIds, std::size_t size) const
@@ -218,14 +216,12 @@ namespace lms::db
query.groupBy("c.id"); query.groupBy("c.id");
query.orderBy("COUNT(c.id) DESC"); query.orderBy("COUNT(c.id) DESC");
auto queryRes{ query.resultList() };
std::map<ClusterTypeId, std::vector<Cluster::pointer>> clustersByType; std::map<ClusterTypeId, std::vector<Cluster::pointer>> clustersByType;
for (const Wt::Dbo::ptr<Cluster>& cluster : queryRes) utils::forEachQueryResult(query, [&](const Cluster::pointer& cluster)
{ {
if (clustersByType[cluster->getType()->getId()].size() < size) if (clustersByType[cluster->getType()->getId()].size() < size)
clustersByType[cluster->getType()->getId()].push_back(cluster); clustersByType[cluster->getType()->getId()].push_back(cluster);
} });
for (const auto& [clusterTypeId, clusters] : clustersByType) for (const auto& [clusterTypeId, clusters] : clustersByType)
res.push_back(clusters); res.push_back(clusters);
@@ -237,7 +233,7 @@ namespace lms::db
{ {
assert(session()); assert(session());
auto res{ session()->query<Wt::Dbo::ptr<Track>>( auto query{ session()->query<Wt::Dbo::ptr<Track>>(
"SELECT t FROM track t" "SELECT t FROM track t"
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id" " INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
" WHERE " " WHERE "
@@ -249,20 +245,19 @@ namespace lms::db
.groupBy("t.id") .groupBy("t.id")
.orderBy("COUNT(*) DESC, RANDOM()") .orderBy("COUNT(*) DESC, RANDOM()")
.limit(size ? static_cast<int>(*size) : -1) .limit(size ? static_cast<int>(*size) : -1)
.offset(offset ? static_cast<int>(*offset) : -1) .offset(offset ? static_cast<int>(*offset) : -1) };
.resultList() };
return std::vector<Track::pointer>(res.begin(), res.end()); return utils::fetchQueryResults<Track::pointer>(query);
} }
std::vector<TrackId> TrackList::getTrackIds() const std::vector<TrackId> TrackList::getTrackIds() const
{ {
assert(session()); assert(session());
Wt::Dbo::collection<TrackId> res = session()->query<TrackId>("SELECT p_e.track_id from tracklist_entry p_e INNER JOIN tracklist p ON p_e.tracklist_id = p.id") auto query{ session()->query<TrackId>("SELECT p_e.track_id from tracklist_entry p_e INNER JOIN tracklist p ON p_e.tracklist_id = p.id")
.where("p.id = ?").bind(getId()); .where("p.id = ?").bind(getId()) };
return std::vector<TrackId>(res.begin(), res.end()); return utils::fetchQueryResults(query);
} }
std::chrono::milliseconds TrackList::getDuration() const std::chrono::milliseconds TrackList::getDuration() const
@@ -271,10 +266,8 @@ namespace lms::db
using milli = std::chrono::duration<int, std::milli>; using milli = std::chrono::duration<int, std::milli>;
Wt::Dbo::Query<milli> query{ session()->query<milli>("SELECT COALESCE(SUM(duration), 0) FROM track t INNER JOIN tracklist_entry p_e ON t.id = p_e.track_id") return utils::fetchQuerySingleResult(session()->query<milli>("SELECT COALESCE(SUM(duration), 0) FROM track t INNER JOIN tracklist_entry p_e ON t.id = p_e.track_id")
.where("p_e.tracklist_id = ?").bind(getId()) }; .where("p_e.tracklist_id = ?").bind(getId()));
return query.resultValue();
} }
void TrackList::setLastModifiedDateTime(const Wt::WDateTime& dateTime) void TrackList::setLastModifiedDateTime(const Wt::WDateTime& dateTime)
@@ -293,7 +286,7 @@ namespace lms::db
TrackListEntry::pointer TrackListEntry::create(Session& session, ObjectPtr<Track> track, ObjectPtr<TrackList> tracklist, const Wt::WDateTime& dateTime) TrackListEntry::pointer TrackListEntry::create(Session& session, ObjectPtr<Track> track, ObjectPtr<TrackList> tracklist, const Wt::WDateTime& dateTime)
{ {
return session.getDboSession().add(std::unique_ptr<TrackListEntry> {new TrackListEntry{ track, tracklist, dateTime }}); return session.getDboSession()->add(std::unique_ptr<TrackListEntry> {new TrackListEntry{ track, tracklist, dateTime }});
} }
void TrackListEntry::onPostCreated() void TrackListEntry::onPostCreated()
@@ -310,6 +303,6 @@ namespace lms::db
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().find<TrackListEntry>().where("id = ?").bind(id).resultValue(); return utils::fetchQuerySingleResult(session.getDboSession()->find<TrackListEntry>().where("id = ?").bind(id));
} }
} // namespace lms::db } // namespace lms::db
@@ -80,7 +80,7 @@ namespace lms::db
void TransactionChecker::checkWriteTransaction(Session& session) void TransactionChecker::checkWriteTransaction(Session& session)
{ {
checkWriteTransaction(session.getDboSession()); checkWriteTransaction(*session.getDboSession());
} }
void TransactionChecker::checkReadTransaction(Wt::Dbo::Session& session) void TransactionChecker::checkReadTransaction(Wt::Dbo::Session& session)
@@ -91,6 +91,6 @@ namespace lms::db
void TransactionChecker::checkReadTransaction(Session& session) void TransactionChecker::checkReadTransaction(Session& session)
{ {
checkReadTransaction(session.getDboSession()); checkReadTransaction(*session.getDboSession());
} }
} }
+10 -11
View File
@@ -28,8 +28,8 @@
#include "StringViewTraits.hpp" #include "StringViewTraits.hpp"
#include "Utils.hpp" #include "Utils.hpp"
namespace lms::db { namespace lms::db
{
User::User(std::string_view loginName) User::User(std::string_view loginName)
: _loginName{ loginName } : _loginName{ loginName }
{ {
@@ -37,47 +37,46 @@ namespace lms::db {
User::pointer User::create(Session& session, std::string_view loginName) User::pointer User::create(Session& session, std::string_view loginName)
{ {
return session.getDboSession().add(std::unique_ptr<User> {new User{ loginName }}); return session.getDboSession()->add(std::unique_ptr<User> {new User{ loginName }});
} }
std::size_t User::getCount(Session& session) std::size_t User::getCount(Session& session)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().query<int>("SELECT COUNT(*) FROM user"); return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM user"));
} }
RangeResults<UserId> User::find(Session& session, const FindParameters& params) RangeResults<UserId> User::find(Session& session, const FindParameters& params)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ session.getDboSession().query<UserId>("SELECT id FROM user") }; auto query{ session.getDboSession()->query<UserId>("SELECT id FROM user") };
if (params.scrobblingBackend) if (params.scrobblingBackend)
query.where("scrobbling_backend = ?").bind(*params.scrobblingBackend); query.where("scrobbling_backend = ?").bind(*params.scrobblingBackend);
if (params.feedbackBackend) if (params.feedbackBackend)
query.where("feedback_backend = ?").bind(*params.feedbackBackend); query.where("feedback_backend = ?").bind(*params.feedbackBackend);
return utils::execQuery<UserId>(query, params.range); return utils::execRangeQuery<UserId>(query, params.range);
} }
User::pointer User::findDemoUser(Session& session) User::pointer User::findDemoUser(Session& session)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
return session.getDboSession().find<User>().where("type = ?").bind(UserType::DEMO).resultValue(); return utils::fetchQuerySingleResult(session.getDboSession()->find<User>().where("type = ?").bind(UserType::DEMO));
} }
User::pointer User::find(Session& session, UserId id) User::pointer User::find(Session& session, UserId id)
{ {
return session.getDboSession().find<User>().where("id = ?").bind(id).resultValue(); return utils::fetchQuerySingleResult(session.getDboSession()->find<User>().where("id = ?").bind(id));
} }
User::pointer User::find(Session& session, std::string_view name) User::pointer User::find(Session& session, std::string_view name)
{ {
return session.getDboSession().find<User>() return utils::fetchQuerySingleResult(session.getDboSession()->find<User>()
.where("login_name = ?").bind(name) .where("login_name = ?").bind(name));
.resultValue();
} }
void User::setSubsonicDefaultTranscodingOutputBitrate(Bitrate bitrate) void User::setSubsonicDefaultTranscodingOutputBitrate(Bitrate bitrate)
+85 -26
View File
@@ -45,11 +45,75 @@ namespace lms::db::utils
} }
} }
template <typename ResultType, typename Query> template <typename T>
RangeResults<ResultType> execQuery(Query& query, std::optional<Range> range) auto fetchFirstResult(const Wt::Dbo::collection<T>& collection)
{ {
LMS_SCOPED_TRACE_DETAILED("Database", "ExecQueryRange"); LMS_SCOPED_TRACE_DETAILED("Database", "FetchFirstResult");
return collection.begin();
}
template <typename T>
void fetchNextResult(typename Wt::Dbo::collection<T>::const_iterator& it)
{
LMS_SCOPED_TRACE_DETAILED("Database", "FetchNextResult");
it++;
}
template <typename T, typename Func>
void forEachResult(const Wt::Dbo::collection<T>& collection, Func&& func)
{
typename Wt::Dbo::collection<T>::const_iterator it{ fetchFirstResult(collection) };
while (it != collection.end())
{
func(*it);
fetchNextResult<T>(it);
}
}
template <typename T>
struct QueryResultType;
template <class ResultType, typename BindStrategy>
struct QueryResultType<Wt::Dbo::Query<ResultType, BindStrategy>>
{
using type = ResultType;
};
template <typename Query, typename UnaryFunc>
void forEachQueryResult(const Query& query, UnaryFunc&& func)
{
LMS_SCOPED_TRACE_DETAILED_WITH_ARG("Database", "ForEachQueryResult", "Query", query.asString());
forEachResult(query.resultList(), std::forward<UnaryFunc>(func));
}
template <typename T, typename Query>
std::vector<T> fetchQueryResults(const Query& query)
{
LMS_SCOPED_TRACE_DETAILED_WITH_ARG("Database", "FetchQueryResults", "Query", query.asString());
auto collection{ query.resultList() };
return std::vector<T>(collection.begin(), collection.end());
}
template <typename Query>
std::vector<typename QueryResultType<Query>::type> fetchQueryResults(const Query& query)
{
LMS_SCOPED_TRACE_DETAILED_WITH_ARG("Database", "FetchQueryResults", "Query", query.asString());
auto collection{ query.resultList() };
return std::vector<typename QueryResultType<Query>::type>(collection.begin(), collection.end());
}
template <typename Query>
auto fetchQuerySingleResult(const Query& query)
{
LMS_SCOPED_TRACE_DETAILED_WITH_ARG("Database", "FetchQuerySingleResult", "Query", query.asString());
return query.resultValue();
}
template <typename ResultType, typename Query>
RangeResults<ResultType> execRangeQuery(Query& query, const std::optional<Range> range)
{
RangeResults<ResultType> res; RangeResults<ResultType> res;
if (range) if (range)
@@ -60,16 +124,12 @@ namespace lms::db::utils
res.results.reserve(range->size); res.results.reserve(range->size);
} }
auto collection{ query.resultList() }; // TODO optim useless last copy
for (auto itResult{ collection.begin() }; itResult != collection.end(); ++itResult) res.results = utils::fetchQueryResults<ResultType>(query);
if (range && (res.results.size() == range->size + 1))
{ {
if (range && res.results.size() == range->size) res.moreResults = true;
{ res.results.pop_back();
res.moreResults = true;
break;
}
res.results.push_back(std::move(*itResult));
} }
res.range.size = res.results.size(); res.range.size = res.results.size();
@@ -77,29 +137,29 @@ namespace lms::db::utils
return res; return res;
} }
template <typename ResultType, typename Query> template <typename Query, typename UnaryFunc>
void execQuery(Query& query, std::optional<Range> range, std::function<void(const ResultType&)> func) void forEachQueryRangeResult(Query& query, std::optional<Range> range, UnaryFunc&& func)
{ {
if (range) if (range)
applyRange(query, range); applyRange(query, range);
for (const auto& res : query.resultList()) forEachQueryResult(query, std::forward<UnaryFunc>(func));
{
LMS_SCOPED_TRACE_DETAILED("Database", "ExecQueryResult");
func(res);
}
} }
template <typename ResultType, typename Query> template <typename Query, typename UnaryFunc>
void execQuery(Query& query, std::optional<Range> range, bool& moreResults, std::function<void(const ResultType&)> func) void forEachQueryRangeResult(Query& query, std::optional<Range> range, bool& moreResults, UnaryFunc&& func)
{ {
using ResultType = typename QueryResultType<Query>::type;
if (range) if (range)
applyRange(query, Range{ range->offset, range->size + 1 }); applyRange(query, Range{ range->offset, range->size + 1 });
moreResults = false; moreResults = false;
std::size_t count{}; std::size_t count{};
for (const auto& res : query.resultList()) const auto collection{ query.resultList() };
auto it{ fetchFirstResult(collection) };
while (it != collection.end())
{ {
if (range && (count++ == static_cast<std::size_t>(range->size))) if (range && (count++ == static_cast<std::size_t>(range->size)))
{ {
@@ -107,11 +167,10 @@ namespace lms::db::utils
break; break;
} }
LMS_SCOPED_TRACE_DETAILED("Database", "ExecQueryResult"); func(*it);
func(res); fetchNextResult<ResultType>(it);
} }
} }
Wt::WDateTime normalizeDateTime(const Wt::WDateTime& dateTime); Wt::WDateTime normalizeDateTime(const Wt::WDateTime& dateTime);
} }
@@ -86,6 +86,7 @@ namespace lms::db
static pointer find(Session& session, const core::UUID& MBID); static pointer find(Session& session, const core::UUID& MBID);
static pointer find(Session& session, ArtistId id); static pointer find(Session& session, ArtistId id);
static std::vector<pointer> find(Session& session, std::string_view name); // exact match on name field static std::vector<pointer> find(Session& session, std::string_view name); // exact match on name field
static void find(Session& session, ArtistId& lastRetrievedArtist, std::size_t count, const std::function<void(const Artist::pointer&)>& func, MediaLibraryId library = {});
static RangeResults<pointer> find(Session& session, const FindParameters& parameters); static RangeResults<pointer> find(Session& session, const FindParameters& parameters);
static void find(Session& session, const FindParameters& parameters, std::function<void(const pointer&)> func); static void find(Session& session, const FindParameters& parameters, std::function<void(const pointer&)> func);
static RangeResults<ArtistId> findIds(Session& session, const FindParameters& parameters); static RangeResults<ArtistId> findIds(Session& session, const FindParameters& parameters);
@@ -75,7 +75,7 @@ namespace lms::db
// Accessors // Accessors
std::string_view getName() const { return _name; } std::string_view getName() const { return _name; }
ObjectPtr<ClusterType> getType() const { return _clusterType; } ObjectPtr<ClusterType> getType() const { return _clusterType; }
std::size_t getTracksCount() const { return _trackCount; } std::size_t getTrackCount() const { return _trackCount; }
RangeResults<TrackId> getTracks(std::optional<Range> range = std::nullopt) const; RangeResults<TrackId> getTracks(std::optional<Range> range = std::nullopt) const;
std::size_t getReleasesCount() const { return _releaseCount; }; std::size_t getReleasesCount() const { return _releaseCount; };
+33 -33
View File
@@ -26,44 +26,44 @@
namespace lms::db namespace lms::db
{ {
class IdType class IdType
{ {
public: public:
using ValueType = Wt::Dbo::dbo_default_traits::IdType; using ValueType = Wt::Dbo::dbo_default_traits::IdType;
IdType() = default; IdType() = default;
IdType(ValueType id) : _id {id} { assert(isValid()); } IdType(ValueType id) : _id{ id } { assert(isValid()); }
bool isValid() const { return _id != Wt::Dbo::dbo_default_traits::invalidId(); } bool isValid() const { return _id != Wt::Dbo::dbo_default_traits::invalidId(); }
std::string toString() const { assert(isValid()); return std::to_string(_id); } 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:
Wt::Dbo::dbo_default_traits::IdType _id{ Wt::Dbo::dbo_default_traits::invalidId() };
private: };
Wt::Dbo::dbo_default_traits::IdType _id {Wt::Dbo::dbo_default_traits::invalidId()};
};
#define LMS_DECLARE_IDTYPE(name) \ #define LMS_DECLARE_IDTYPE(name) \
namespace lms::db { \ namespace lms::db { \
class name : public IdType \ class name : public IdType \
{ \ { \
public: \ public: \
using IdType::IdType; \ using IdType::IdType; \
};\ auto operator<=>(const name& other) const = default; \
} \ };\
namespace std \ } \
{ \ namespace std \
template<> \ { \
class hash<lms::db::name> \ template<> \
{ \ class hash<lms::db::name> \
public: \ { \
size_t operator()(lms::db::name id) const \ public: \
{ \ size_t operator()(lms::db::name id) const \
return std::hash<lms::db::name::ValueType>()(id.getValue()); \ { \
} \ return std::hash<lms::db::name::ValueType>()(id.getValue()); \
}; \ } \
} // ns std }; \
} // ns std
} // namespace lms::db } // namespace lms::db
@@ -31,7 +31,8 @@ namespace lms::db
{ {
public: public:
ObjectPtr() = default; ObjectPtr() = default;
ObjectPtr(Wt::Dbo::ptr<T> obj) : _obj{ obj } {} ObjectPtr(const Wt::Dbo::ptr<T>& obj) : _obj{ obj } {}
ObjectPtr(Wt::Dbo::ptr<T>&& obj) : _obj{ std::move(obj) } {}
const T* operator->() const { return _obj.get(); } const T* operator->() const { return _obj.get(); }
operator bool() const { return _obj.get(); } operator bool() const { return _obj.get(); }
@@ -122,8 +122,9 @@ namespace lms::db
static pointer find(Session& session, const core::UUID& MBID); static pointer find(Session& session, const core::UUID& MBID);
static std::vector<pointer> find(Session& session, const std::string& name, const std::filesystem::path& releaseDirectory); static std::vector<pointer> find(Session& session, const std::string& name, const std::filesystem::path& releaseDirectory);
static pointer find(Session& session, ReleaseId id); static pointer find(Session& session, ReleaseId id);
static void find(Session& session, ReleaseId& lastRetrievedRelease, std::size_t count, const std::function<void(const Release::pointer&)>& func, MediaLibraryId library = {});
static RangeResults<pointer> find(Session& session, const FindParameters& parameters); static RangeResults<pointer> find(Session& session, const FindParameters& parameters);
static void find(Session& session, const FindParameters& parameters, std::function<void(const pointer&)> func); static void find(Session& session, const FindParameters& parameters, const std::function<void(const pointer&)>& func);
static RangeResults<ReleaseId> findIds(Session& session, const FindParameters& parameters); static RangeResults<ReleaseId> findIds(Session& session, const FindParameters& parameters);
static std::size_t getCount(Session& session, const FindParameters& parameters); static std::size_t getCount(Session& session, const FindParameters& parameters);
static RangeResults<ReleaseId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt); // not track related static RangeResults<ReleaseId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt); // not track related
@@ -153,7 +154,7 @@ namespace lms::db
std::chrono::milliseconds getDuration() const; std::chrono::milliseconds getDuration() const;
Wt::WDateTime getLastWritten() const; Wt::WDateTime getLastWritten() const;
std::string_view getArtistDisplayName() const { return _artistDisplayName; } std::string_view getArtistDisplayName() const { return _artistDisplayName; }
std::size_t getTracksCount() const; std::size_t getTrackCount() const;
std::vector<ObjectPtr<ReleaseType>> getReleaseTypes() const; std::vector<ObjectPtr<ReleaseType>> getReleaseTypes() const;
std::vector<std::string> getReleaseTypeNames() const; std::vector<std::string> getReleaseTypeNames() const;
+18 -8
View File
@@ -22,6 +22,8 @@
#include <Wt/Dbo/Dbo.h> #include <Wt/Dbo/Dbo.h>
#include <Wt/Dbo/SqlConnectionPool.h> #include <Wt/Dbo/SqlConnectionPool.h>
#include <string>
#include <vector>
#include "core/ITraceLogger.hpp" #include "core/ITraceLogger.hpp"
#include "core/RecursiveSharedMutex.hpp" #include "core/RecursiveSharedMutex.hpp"
#include "database/Object.hpp" #include "database/Object.hpp"
@@ -41,8 +43,8 @@ namespace lms::db
WriteTransaction(const WriteTransaction&) = delete; WriteTransaction(const WriteTransaction&) = delete;
WriteTransaction& operator=(const WriteTransaction&) = delete; WriteTransaction& operator=(const WriteTransaction&) = delete;
std::unique_lock<core::RecursiveSharedMutex> _lock; const std::unique_lock<core::RecursiveSharedMutex> _lock;
core::tracing::ScopedTrace _trace{ "Database", core::tracing::Level::Detailed, "WriteTransaction" }; // before actual transaction const core::tracing::ScopedTrace _trace{ "Database", core::tracing::Level::Detailed, "WriteTransaction" }; // before actual transaction
Wt::Dbo::Transaction _transaction; Wt::Dbo::Transaction _transaction;
}; };
@@ -58,7 +60,7 @@ namespace lms::db
ReadTransaction(const ReadTransaction&) = delete; ReadTransaction(const ReadTransaction&) = delete;
ReadTransaction& operator=(const ReadTransaction&) = delete; ReadTransaction& operator=(const ReadTransaction&) = delete;
core::tracing::ScopedTrace _trace{ "Database", core::tracing::Level::Detailed, "ReadTransaction" }; // before actual transaction const core::tracing::ScopedTrace _trace{ "Database", core::tracing::Level::Detailed, "ReadTransaction" }; // before actual transaction
Wt::Dbo::Transaction _transaction; Wt::Dbo::Transaction _transaction;
}; };
@@ -84,12 +86,20 @@ namespace lms::db
#endif #endif
} }
void analyze(); // All these methods will acquire transactions
void optimize(); void fullAnalyze(); // helper for retrieveEntriesToAnalyze + analyzeEntry
void retrieveEntriesToAnalyze(std::vector<std::string>& entryList);
void analyzeEntry(const std::string& entry);
void prepareTables(); // need to run only once at startup void prepareTablesIfNeeded(); // need to run only once at startup
bool migrateSchemaIfNeeded(); // returns true if migration was performed
void createIndexesIfNeeded();
void vacuumIfNeeded();
void vacuum();
void refreshTracingLoggerStats();
Wt::Dbo::Session& getDboSession() { return _session; } // returning a ptr here to ease further wrapping using operator->
Wt::Dbo::Session* getDboSession() { return &_session; }
Db& getDb() { return _db; } Db& getDb() { return _db; }
template <typename Object, typename... Args> template <typename Object, typename... Args>
@@ -98,7 +108,7 @@ namespace lms::db
checkWriteTransaction(); checkWriteTransaction();
typename Object::pointer res{ Object::create(*this, std::forward<Args>(args)...) }; typename Object::pointer res{ Object::create(*this, std::forward<Args>(args)...) };
getDboSession().flush(); getDboSession()->flush();
if (res->hasOnPostCreated()) if (res->hasOnPostCreated())
res.modify()->onPostCreated(); res.modify()->onPostCreated();
+51 -35
View File
@@ -110,7 +110,7 @@ namespace lms::db
static std::size_t getCount(Session& session); static std::size_t getCount(Session& session);
static pointer findByPath(Session& session, const std::filesystem::path& p); static pointer findByPath(Session& session, const std::filesystem::path& p);
static pointer find(Session& session, TrackId id); static pointer find(Session& session, TrackId id);
static void find(Session& session, TrackId& lastRetrievedTrack, std::size_t batchSize, bool& moreResults, const std::function<void(const Track::pointer&)>& func); static void find(Session& session, TrackId& lastRetrievedTrack, std::size_t count, const std::function<void(const Track::pointer&)>& func, MediaLibraryId library = {});
static bool exists(Session& session, TrackId id); static bool exists(Session& session, TrackId id);
static std::vector<pointer> findByRecordingMBID(Session& session, const core::UUID& MBID); static std::vector<pointer> findByRecordingMBID(Session& session, const core::UUID& MBID);
static std::vector<pointer> findByMBID(Session& session, const core::UUID& MBID); static std::vector<pointer> findByMBID(Session& session, const core::UUID& MBID);
@@ -118,9 +118,8 @@ namespace lms::db
static RangeResults<TrackId> findIds(Session& session, const FindParameters& parameters); static RangeResults<TrackId> findIds(Session& session, const FindParameters& parameters);
static RangeResults<pointer> find(Session& session, const FindParameters& parameters); static RangeResults<pointer> find(Session& session, const FindParameters& parameters);
static void find(Session& session, const FindParameters& parameters, std::function<void(const Track::pointer&)> func); static void find(Session& session, const FindParameters& parameters, const std::function<void(const Track::pointer&)>& func);
static void find(Session& session, const FindParameters& parameters, bool& moreResults, std::function<void(const Track::pointer&)> func); static void find(Session& session, const FindParameters& parameters, bool& moreResults, const std::function<void(const Track::pointer&)>& func);
static RangeResults<PathResult> findPaths(Session& session, std::optional<Range> range = std::nullopt);
static RangeResults<TrackId> findIdsTrackMBIDDuplicates(Session& session, std::optional<Range> range = std::nullopt); static RangeResults<TrackId> findIdsTrackMBIDDuplicates(Session& session, std::optional<Range> range = std::nullopt);
static RangeResults<TrackId> findIdsWithRecordingMBIDAndMissingFeatures(Session& session, std::optional<Range> range = std::nullopt); static RangeResults<TrackId> findIdsWithRecordingMBIDAndMissingFeatures(Session& session, std::optional<Range> range = std::nullopt);
@@ -131,11 +130,16 @@ namespace lms::db
void setTotalTrack(std::optional<int> totalTrack) { _totalTrack = totalTrack; } void setTotalTrack(std::optional<int> totalTrack) { _totalTrack = totalTrack; }
void setDiscSubtitle(const std::string& name) { _discSubtitle = name; } void setDiscSubtitle(const std::string& name) { _discSubtitle = name; }
void setName(const std::string& name) { _name = std::string(name, 0, _maxNameLength); } void setName(const std::string& name) { _name = std::string(name, 0, _maxNameLength); }
void setPath(const std::filesystem::path& filePath) { _filePath = filePath; } void setAbsoluteFilePath(const std::filesystem::path& filePath);
void setDuration(std::chrono::milliseconds duration) { _duration = duration; } void setRelativeFilePath(const std::filesystem::path& filePath);
void setBitrate(std::size_t bitrate) { _bitrate = bitrate; } void setFileSize(std::size_t fileSize) { _fileSize = fileSize; }
void setLastWriteTime(Wt::WDateTime time) { _fileLastWrite = time; } void setLastWriteTime(Wt::WDateTime time) { _fileLastWrite = time; }
void setAddedTime(Wt::WDateTime time) { _fileAdded = time; } void setAddedTime(Wt::WDateTime time) { _fileAdded = time; }
void setBitrate(std::size_t bitrate) { _bitrate = bitrate; }
void setBitsPerSample(std::size_t bitsPerSample) { _bitsPerSample = bitsPerSample; }
void setDuration(std::chrono::milliseconds duration) { _duration = duration; }
void setChannelCount(std::size_t channelCount) { _channelCount = channelCount; }
void setSampleRate(std::size_t channelCount) { _sampleRate = channelCount; }
void setDate(const Wt::WDate& date) { _date = date; } void setDate(const Wt::WDate& date) { _date = date; }
void setYear(std::optional<int> year) { _year = year; } void setYear(std::optional<int> year) { _year = year; }
void setOriginalDate(const Wt::WDate& date) { _originalDate = date; } void setOriginalDate(const Wt::WDate& date) { _originalDate = date; }
@@ -154,32 +158,37 @@ namespace lms::db
void setClusters(const std::vector<ObjectPtr<Cluster>>& clusters); void setClusters(const std::vector<ObjectPtr<Cluster>>& clusters);
void setMediaLibrary(ObjectPtr<MediaLibrary> mediaLibrary) { _mediaLibrary = getDboPtr(mediaLibrary); } void setMediaLibrary(ObjectPtr<MediaLibrary> mediaLibrary) { _mediaLibrary = getDboPtr(mediaLibrary); }
std::size_t getScanVersion() const { return _scanVersion; } std::size_t getScanVersion() const { return _scanVersion; }
std::optional<std::size_t> getTrackNumber() const { return _trackNumber; } std::optional<std::size_t> getTrackNumber() const { return _trackNumber; }
std::optional<std::size_t> getTotalTrack() const { return _totalTrack; } std::optional<std::size_t> getTotalTrack() const { return _totalTrack; }
std::optional<std::size_t> getDiscNumber() const { return _discNumber; } std::optional<std::size_t> getDiscNumber() const { return _discNumber; }
const std::string& getDiscSubtitle() const { return _discSubtitle; } const std::string& getDiscSubtitle() const { return _discSubtitle; }
std::string getName() const { return _name; } std::string getName() const { return _name; }
std::filesystem::path getPath() const { return _filePath; } const std::filesystem::path& getAbsoluteFilePath() const { return _absoluteFilePath; }
std::chrono::milliseconds getDuration() const { return _duration; } const std::filesystem::path& getRelativeFilePath() const { return _relativeFilePath; }
std::size_t getBitrate() const { return _bitrate; } long long getFileSize() const { return _fileSize; }
std::size_t getBitrate() const { return _bitrate; }
std::size_t getBitsPerSample() const { return _bitsPerSample; }
std::size_t getChannelCount() const { return _channelCount; }
std::chrono::milliseconds getDuration() const { return _duration; }
std::size_t getSampleRate() const { return _sampleRate; }
const Wt::WDateTime& getLastWritten() const { return _fileLastWrite; } const Wt::WDateTime& getLastWritten() const { return _fileLastWrite; }
const Wt::WDate& getDate() const { return _date; } const Wt::WDate& getDate() const { return _date; }
std::optional<int> getYear() const { return _year; } std::optional<int> getYear() const { return _year; }
const Wt::WDate& getOriginalDate() const { return _originalDate; } const Wt::WDate& getOriginalDate() const { return _originalDate; }
std::optional<int> getOriginalYear() const { return _originalYear; }; std::optional<int> getOriginalYear() const { return _originalYear; };
Wt::WDateTime getLastWriteTime() const { return _fileLastWrite; } const Wt::WDateTime& getLastWriteTime() const { return _fileLastWrite; }
Wt::WDateTime getAddedTime() const { return _fileAdded; } const Wt::WDateTime& getAddedTime() const { return _fileAdded; }
bool hasCover() const { return _hasCover; } bool hasCover() const { return _hasCover; }
std::optional<core::UUID> getTrackMBID() const { return core::UUID::fromString(_trackMBID); } std::optional<core::UUID> getTrackMBID() const { return core::UUID::fromString(_trackMBID); }
std::optional<core::UUID> getRecordingMBID() const { return core::UUID::fromString(_recordingMBID); } std::optional<core::UUID> getRecordingMBID() const { return core::UUID::fromString(_recordingMBID); }
std::optional<std::string> getCopyright() const; std::optional<std::string> getCopyright() const;
std::optional<std::string> getCopyrightURL() const; std::optional<std::string> getCopyrightURL() const;
std::optional<float> getTrackReplayGain() const { return _trackReplayGain; } std::optional<float> getTrackReplayGain() const { return _trackReplayGain; }
std::optional<float> getReleaseReplayGain() const { return _releaseReplayGain; } std::optional<float> getReleaseReplayGain() const { return _releaseReplayGain; }
std::string_view getArtistDisplayName() const { return _artistDisplayName; } std::string_view getArtistDisplayName() const { return _artistDisplayName; }
// no artistLinkTypes means get all // no artistLinkTypes means get all
std::vector<ObjectPtr<Artist>> getArtists(core::EnumSet<TrackArtistLinkType> artistLinkTypes) const; // no type means all std::vector<ObjectPtr<Artist>> getArtists(core::EnumSet<TrackArtistLinkType> artistLinkTypes) const; // no type means all
std::vector<ArtistId> getArtistIds(core::EnumSet<TrackArtistLinkType> artistLinkTypes) const; // no type means all std::vector<ArtistId> getArtistIds(core::EnumSet<TrackArtistLinkType> artistLinkTypes) const; // no type means all
std::vector<ObjectPtr<TrackArtistLink>> getArtistLinks() const; std::vector<ObjectPtr<TrackArtistLink>> getArtistLinks() const;
ObjectPtr<Release> getRelease() const { return _release; } ObjectPtr<Release> getRelease() const { return _release; }
@@ -200,11 +209,16 @@ namespace lms::db
Wt::Dbo::field(a, _name, "name"); Wt::Dbo::field(a, _name, "name");
Wt::Dbo::field(a, _duration, "duration"); Wt::Dbo::field(a, _duration, "duration");
Wt::Dbo::field(a, _bitrate, "bitrate"); Wt::Dbo::field(a, _bitrate, "bitrate");
Wt::Dbo::field(a, _bitsPerSample, "bits_per_sample");
Wt::Dbo::field(a, _channelCount, "channel_count");
Wt::Dbo::field(a, _sampleRate, "sample_rate");
Wt::Dbo::field(a, _date, "date"); Wt::Dbo::field(a, _date, "date");
Wt::Dbo::field(a, _year, "year"); Wt::Dbo::field(a, _year, "year");
Wt::Dbo::field(a, _originalDate, "original_date"); Wt::Dbo::field(a, _originalDate, "original_date");
Wt::Dbo::field(a, _originalYear, "original_year"); Wt::Dbo::field(a, _originalYear, "original_year");
Wt::Dbo::field(a, _filePath, "file_path"); Wt::Dbo::field(a, _absoluteFilePath, "absolute_file_path");
Wt::Dbo::field(a, _relativeFilePath, "relative_file_path");
Wt::Dbo::field(a, _fileSize, "file_size");
Wt::Dbo::field(a, _fileLastWrite, "file_last_write"); Wt::Dbo::field(a, _fileLastWrite, "file_last_write");
Wt::Dbo::field(a, _fileAdded, "file_added"); Wt::Dbo::field(a, _fileAdded, "file_added");
Wt::Dbo::field(a, _hasCover, "has_cover"); Wt::Dbo::field(a, _hasCover, "has_cover");
@@ -223,8 +237,7 @@ namespace lms::db
private: private:
friend class Session; friend class Session;
Track(const std::filesystem::path& p); static pointer create(Session& session);
static pointer create(Session& session, const std::filesystem::path& p);
static constexpr std::size_t _maxNameLength{ 256 }; static constexpr std::size_t _maxNameLength{ 256 };
static constexpr std::size_t _maxCopyrightLength{ 256 }; static constexpr std::size_t _maxCopyrightLength{ 256 };
@@ -236,13 +249,18 @@ namespace lms::db
std::optional<int> _totalTrack{}; std::optional<int> _totalTrack{};
std::string _discSubtitle; std::string _discSubtitle;
std::string _name; std::string _name;
int _bitrate{}; // in bps
int _bitsPerSample{};
int _channelCount{};
std::chrono::duration<int, std::milli> _duration{}; std::chrono::duration<int, std::milli> _duration{};
int _bitrate; // in bps int _sampleRate{};
Wt::WDate _date; Wt::WDate _date;
std::optional<int> _year; std::optional<int> _year;
Wt::WDate _originalDate; Wt::WDate _originalDate;
std::optional<int> _originalYear; std::optional<int> _originalYear;
std::string _filePath; std::filesystem::path _absoluteFilePath; // full path
std::filesystem::path _relativeFilePath; // relative to root (that may be deleted)
long long _fileSize{};
Wt::WDateTime _fileLastWrite; Wt::WDateTime _fileLastWrite;
Wt::WDateTime _fileAdded; Wt::WDateTime _fileAdded;
bool _hasCover{}; bool _hasCover{};
@@ -270,5 +288,3 @@ namespace lms::db
std::ostream& operator<<(std::ostream& os, const TrackInfo& trackInfo); std::ostream& operator<<(std::ostream& os, const TrackInfo& trackInfo);
} }
} // namespace lms::db } // namespace lms::db
@@ -62,7 +62,8 @@ namespace lms::db
TrackArtistLink() = default; TrackArtistLink() = default;
TrackArtistLink(ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType); TrackArtistLink(ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType);
static RangeResults<TrackArtistLinkId> find(Session& session, const FindParameters& parameters); static void find(Session& session, TrackId trackId, const std::function<void(const TrackArtistLink::pointer&, const ObjectPtr<Artist>&)>&);
static void find(Session& session, const FindParameters& parameters, const std::function<void(const TrackArtistLink::pointer&)>&);
static 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 pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType = {});
static core::EnumSet<TrackArtistLinkType> findUsedTypes(Session& session); static core::EnumSet<TrackArtistLinkType> findUsedTypes(Session& session);
+5 -2
View File
@@ -108,8 +108,9 @@ namespace lms::db
enum class ArtistSortMethod enum class ArtistSortMethod
{ {
None, None,
ByName, Id,
BySortName, Name,
SortName,
Random, Random,
LastWritten, LastWritten,
StarredDateDesc, StarredDateDesc,
@@ -118,6 +119,7 @@ namespace lms::db
enum class ReleaseSortMethod enum class ReleaseSortMethod
{ {
None, None,
Id,
Name, Name,
ArtistNameThenName, ArtistNameThenName,
Date, Date,
@@ -138,6 +140,7 @@ namespace lms::db
enum class TrackSortMethod enum class TrackSortMethod
{ {
None, None,
Id,
Random, Random,
LastWritten, LastWritten,
StarredDateDesc, StarredDateDesc,
+194 -37
View File
@@ -77,6 +77,99 @@ namespace lms::db::tests
} }
} }
TEST_F(DatabaseFixture, Artist_findByRangedIdBased)
{
ScopedTrack track1{ session };
ScopedTrack track2a{ session };
ScopedTrack track2b{ session };
ScopedTrack track3{ session };
ScopedArtist artist1{ session, "MyArtist1" };
ScopedArtist artist2{ session, "MyArtist2" };
ScopedArtist artist3{ session, "MyArtist3" };
ScopedMediaLibrary library{ session };
ScopedMediaLibrary otherLibrary{ session };
{
auto transaction{ session.createWriteTransaction() };
track2a.get().modify()->setMediaLibrary(library.get());
track2b.get().modify()->setMediaLibrary(library.get());
TrackArtistLink::create(session, track1.get(), artist1.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, track2a.get(), artist2.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, track2b.get(), artist2.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, track3.get(), artist3.get(), TrackArtistLinkType::Artist);
}
{
auto transaction{ session.createReadTransaction() };
ArtistId lastRetrievedId;
std::vector<Artist::pointer> visitedArtists;
Artist::find(session, lastRetrievedId, 10, [&](const Artist::pointer& artist)
{
visitedArtists.push_back(artist);
});
ASSERT_EQ(visitedArtists.size(), 3);
EXPECT_EQ(visitedArtists[0]->getId(), artist1.getId());
EXPECT_EQ(visitedArtists[1]->getId(), artist2.getId());
EXPECT_EQ(visitedArtists[2]->getId(), artist3.getId());
EXPECT_EQ(lastRetrievedId, artist3.getId());
}
{
auto transaction{ session.createReadTransaction() };
ArtistId lastRetrievedId{ artist1.getId() };
std::vector<Artist::pointer> visitedArtists;
Artist::find(session, lastRetrievedId, 1, [&](const Artist::pointer& artist)
{
visitedArtists.push_back(artist);
});
ASSERT_EQ(visitedArtists.size(), 1);
EXPECT_EQ(visitedArtists[0]->getId(), artist2.getId());
EXPECT_EQ(lastRetrievedId, artist2.getId());
}
{
auto transaction{ session.createReadTransaction() };
ArtistId lastRetrievedId{ artist1.getId() };
std::vector<Artist::pointer> visitedArtists;
Artist::find(session, lastRetrievedId, 0, [&](const Artist::pointer& artist)
{
visitedArtists.push_back(artist);
});
ASSERT_EQ(visitedArtists.size(), 0);
EXPECT_EQ(lastRetrievedId, artist1.getId());
}
{
auto transaction{ session.createReadTransaction() };
ArtistId lastRetrievedId;
std::vector<Artist::pointer> visitedArtists;
Artist::find(session, lastRetrievedId, 10, [&](const Artist::pointer& artist)
{
visitedArtists.push_back(artist);
}, otherLibrary.getId());
ASSERT_EQ(visitedArtists.size(), 0);
EXPECT_EQ(lastRetrievedId, ArtistId{});
}
{
auto transaction{ session.createReadTransaction() };
ArtistId lastRetrievedId;
std::vector<Artist::pointer> visitedArtists;
Artist::find(session, lastRetrievedId, 10, [&](const Artist::pointer& artist)
{
visitedArtists.push_back(artist);
}, library.getId());
ASSERT_EQ(visitedArtists.size(), 1);
EXPECT_EQ(visitedArtists[0]->getId(), artist2.getId());
EXPECT_EQ(lastRetrievedId, artist2.getId());
}
}
TEST_F(DatabaseFixture, MultipleArtists) TEST_F(DatabaseFixture, MultipleArtists)
{ {
{ {
@@ -139,7 +232,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Artist_singleTrack) TEST_F(DatabaseFixture, Artist_singleTrack)
{ {
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
ScopedArtist artist{ session, "MyArtist" }; ScopedArtist artist{ session, "MyArtist" };
{ {
@@ -209,7 +302,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Artist_singleTrack_mediaLibrary) TEST_F(DatabaseFixture, Artist_singleTrack_mediaLibrary)
{ {
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
ScopedArtist artist{ session, "MyArtist" }; ScopedArtist artist{ session, "MyArtist" };
ScopedMediaLibrary library{ session }; ScopedMediaLibrary library{ session };
ScopedMediaLibrary otherLibrary{ session }; ScopedMediaLibrary otherLibrary{ session };
@@ -242,7 +335,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Artist_singleTracktMultiRoles) TEST_F(DatabaseFixture, Artist_singleTracktMultiRoles)
{ {
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
ScopedArtist artist{ session, "MyArtist" }; ScopedArtist artist{ session, "MyArtist" };
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
@@ -301,11 +394,34 @@ namespace lms::db::tests
EXPECT_TRUE(types.contains(TrackArtistLinkType::Writer)); EXPECT_TRUE(types.contains(TrackArtistLinkType::Writer));
EXPECT_FALSE(types.contains(TrackArtistLinkType::Composer)); EXPECT_FALSE(types.contains(TrackArtistLinkType::Composer));
} }
{
auto transaction{ session.createReadTransaction() };
std::vector<TrackArtistLink::pointer> visitedLinks;
TrackArtistLink::find(session, TrackArtistLink::FindParameters{}.setTrack(track.getId()), [&](const TrackArtistLink::pointer& link)
{
visitedLinks.push_back(link);
});
ASSERT_EQ(visitedLinks.size(), 3);
EXPECT_EQ(visitedLinks[0]->getArtist()->getId(), artist.getId());
EXPECT_EQ(visitedLinks[1]->getArtist()->getId(), artist.getId());
EXPECT_EQ(visitedLinks[2]->getArtist()->getId(), artist.getId());
auto containsType = [&](TrackArtistLinkType type)
{
return std::any_of(std::cbegin(visitedLinks), std::cend(visitedLinks), [type](const TrackArtistLink::pointer& link) { return link->getType() == type;});
};
EXPECT_TRUE(containsType(TrackArtistLinkType::Artist));
EXPECT_TRUE(containsType(TrackArtistLinkType::ReleaseArtist));
EXPECT_TRUE(containsType(TrackArtistLinkType::Writer));
}
} }
TEST_F(DatabaseFixture, Artist_singleTrackMultiArtists) TEST_F(DatabaseFixture, Artist_singleTrackMultiArtists)
{ {
ScopedTrack track{ session, "track" }; ScopedTrack track{ session };
ScopedArtist artist1{ session, "artist1" }; ScopedArtist artist1{ session, "artist1" };
ScopedArtist artist2{ session, "artist2" }; ScopedArtist artist2{ session, "artist2" };
ASSERT_NE(artist1.getId(), artist2.getId()); ASSERT_NE(artist1.getId(), artist2.getId());
@@ -360,12 +476,53 @@ namespace lms::db::tests
tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist2->getId(), { TrackArtistLinkType::Artist })); tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist2->getId(), { TrackArtistLinkType::Artist }));
EXPECT_EQ(tracks.results.size(), 1); EXPECT_EQ(tracks.results.size(), 1);
} }
{
auto transaction{ session.createReadTransaction() };
std::vector<TrackArtistLink::pointer> visitedLinks;
TrackArtistLink::find(session, TrackArtistLink::FindParameters{}.setTrack(track.getId()), [&](const TrackArtistLink::pointer& link)
{
visitedLinks.push_back(link);
});
ASSERT_EQ(visitedLinks.size(), 2);
EXPECT_EQ(visitedLinks[0]->getArtist()->getId(), artist1.getId());
EXPECT_EQ(visitedLinks[1]->getArtist()->getId(), artist2.getId());
}
{
auto transaction{ session.createReadTransaction() };
std::vector<TrackArtistLink::pointer> visitedLinks;
TrackArtistLink::find(session, TrackArtistLink::FindParameters{}.setArtist(artist2.getId()), [&](const TrackArtistLink::pointer& link)
{
visitedLinks.push_back(link);
});
ASSERT_EQ(visitedLinks.size(), 1);
EXPECT_EQ(visitedLinks[0]->getArtist()->getId(), artist2.getId());
EXPECT_EQ(visitedLinks[0]->getTrack()->getId(), track.getId());
}
{
auto transaction{ session.createReadTransaction() };
std::vector<std::pair<TrackArtistLink::pointer, Artist::pointer>> visitedEntries;
TrackArtistLink::find(session, track.getId(), [&](const TrackArtistLink::pointer& link, const Artist::pointer& artist)
{
visitedEntries.push_back(std::make_pair(link, artist));
});
ASSERT_EQ(visitedEntries.size(), 2);
EXPECT_EQ(visitedEntries[0].first->getArtist()->getId(), artist1.getId());
EXPECT_EQ(visitedEntries[0].second->getId(), artist1.getId());
EXPECT_EQ(visitedEntries[1].first->getArtist()->getId(), artist2.getId());
EXPECT_EQ(visitedEntries[1].second->getId(), artist2.getId());
}
} }
TEST_F(DatabaseFixture, Artist_findByName) TEST_F(DatabaseFixture, Artist_findByName)
{ {
ScopedArtist artist{ session, "AAA" }; ScopedArtist artist{ session, "AAA" };
ScopedTrack track{ session, "MyTrack" }; // filters does not work on orphans ScopedTrack track{ session }; // filters does not work on orphans
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
@@ -409,46 +566,46 @@ namespace lms::db::tests
EXPECT_EQ(artists.front()->getId(), artist1.getId()); EXPECT_EQ(artists.front()->getId(), artist1.getId());
EXPECT_EQ(Artist::find(session, R"(MyArtistFoo)").size(), 0); EXPECT_EQ(Artist::find(session, R"(MyArtistFoo)").size(), 0);
} }
{ {
const auto artists{ Artist::find(session, R"(%MyArtist)") }; const auto artists{ Artist::find(session, R"(%MyArtist)") };
ASSERT_TRUE(artists.size() == 1); ASSERT_TRUE(artists.size() == 1);
EXPECT_EQ(artists.front()->getId(), artist2.getId()); EXPECT_EQ(artists.front()->getId(), artist2.getId());
EXPECT_EQ(Artist::find(session, R"(FooMyArtist)").size(), 0); EXPECT_EQ(Artist::find(session, R"(FooMyArtist)").size(), 0);
}
{
const auto artists{ Artist::find(session, R"(%_MyArtist)") };
ASSERT_TRUE(artists.size() == 1);
ASSERT_EQ(artists.front()->getId(), artist3.getId());
EXPECT_EQ(Artist::find(session, R"(%CMyArtist)").size(), 0);
}
} }
{ {
const auto artists{ Artist::find(session, R"(%_MyArtist)") }; auto transaction{ session.createReadTransaction() };
ASSERT_TRUE(artists.size() == 1); {
ASSERT_EQ(artists.front()->getId(), artist3.getId()); const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setKeywords({"MyArtist"})) };
EXPECT_EQ(Artist::find(session, R"(%CMyArtist)").size(), 0); EXPECT_EQ(artists.results.size(), 6);
}
} }
{ {
auto transaction{ session.createReadTransaction() }; const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setKeywords({"MyArtist%"}).setSortMethod(ArtistSortMethod::Name)) };
{ ASSERT_EQ(artists.results.size(), 2);
const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setKeywords({"MyArtist"})) };
EXPECT_EQ(artists.results.size(), 6);
}
{
const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setKeywords({"MyArtist%"}).setSortMethod(ArtistSortMethod::ByName)) };
ASSERT_EQ(artists.results.size(), 2);
EXPECT_EQ(artists.results[0], artist1.getId()); EXPECT_EQ(artists.results[0], artist1.getId());
EXPECT_EQ(artists.results[1], artist4.getId()); EXPECT_EQ(artists.results[1], artist4.getId());
} }
{ {
const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setKeywords({"%MyArtist"}).setSortMethod(ArtistSortMethod::ByName)) }; const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setKeywords({"%MyArtist"}).setSortMethod(ArtistSortMethod::Name)) };
ASSERT_EQ(artists.results.size(), 2); ASSERT_EQ(artists.results.size(), 2);
EXPECT_EQ(artists.results[0], artist2.getId()); EXPECT_EQ(artists.results[0], artist2.getId());
EXPECT_EQ(artists.results[1], artist5.getId()); EXPECT_EQ(artists.results[1], artist5.getId());
} }
{ {
const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setKeywords({"_MyArtist"}).setSortMethod(ArtistSortMethod::ByName)) }; const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setKeywords({"_MyArtist"}).setSortMethod(ArtistSortMethod::Name)) };
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results[0], artist3.getId()); EXPECT_EQ(artists.results[0], artist3.getId());
} }
} }
} }
@@ -467,8 +624,8 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
auto allArtistsByName{ Artist::findIds(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::ByName)) }; auto allArtistsByName{ Artist::findIds(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::Name)) };
auto allArtistsBySortName{ Artist::findIds(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::BySortName)) }; auto allArtistsBySortName{ Artist::findIds(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::SortName)) };
ASSERT_EQ(allArtistsByName.results.size(), 2); ASSERT_EQ(allArtistsByName.results.size(), 2);
EXPECT_EQ(allArtistsByName.results.front(), artistA.getId()); EXPECT_EQ(allArtistsByName.results.front(), artistA.getId());
@@ -483,8 +640,8 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Artist_nonReleaseTracks) TEST_F(DatabaseFixture, Artist_nonReleaseTracks)
{ {
ScopedArtist artist{ session, "artist" }; ScopedArtist artist{ session, "artist" };
ScopedTrack track1{ session, "MyTrack1" }; ScopedTrack track1{ session };
ScopedTrack track2{ session, "MyTrack2" }; ScopedTrack track2{ session };
ScopedRelease release{ session, "MyRelease" }; ScopedRelease release{ session, "MyRelease" };
{ {
@@ -515,7 +672,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Artist_findByRelease) TEST_F(DatabaseFixture, Artist_findByRelease)
{ {
ScopedArtist artist{ session, "artist" }; ScopedArtist artist{ session, "artist" };
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
ScopedRelease release{ session, "MyRelease" }; ScopedRelease release{ session, "MyRelease" };
{ {
+18 -18
View File
@@ -86,7 +86,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Cluster_singleTrack) TEST_F(DatabaseFixture, Cluster_singleTrack)
{ {
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
ScopedClusterType clusterType{ session, "MyClusterType" }; ScopedClusterType clusterType{ session, "MyClusterType" };
{ {
@@ -160,7 +160,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Cluster_singleTrackWithSeveralClusters) TEST_F(DatabaseFixture, Cluster_singleTrackWithSeveralClusters)
{ {
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
ScopedClusterType clusterType{ session, "MyClusterType" }; ScopedClusterType clusterType{ session, "MyClusterType" };
ScopedCluster cluster1{ session, clusterType.lockAndGet(), "MyCluster1" }; ScopedCluster cluster1{ session, clusterType.lockAndGet(), "MyCluster1" };
@@ -215,7 +215,7 @@ namespace lms::db::tests
for (std::size_t i{}; i < 10; ++i) for (std::size_t i{}; i < 10; ++i)
{ {
tracks.emplace_back(session, "MyTrack" + std::to_string(i)); tracks.emplace_back(session);
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
@@ -270,7 +270,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Cluster_singleTrackSingleReleaseSingleCluster) TEST_F(DatabaseFixture, Cluster_singleTrackSingleReleaseSingleCluster)
{ {
ScopedTrack track{ session, "MyTrackFile" }; ScopedTrack track{ session };
ScopedRelease release{ session, "MyRelease" }; ScopedRelease release{ session, "MyRelease" };
{ {
@@ -345,7 +345,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiClusters) TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiClusters)
{ {
ScopedTrack track{ session, "MyTrackFile" }; ScopedTrack track{ session };
ScopedArtist artist{ session, "MyArtist" }; ScopedArtist artist{ session, "MyArtist" };
ScopedClusterType clusterType{ session, "MyType" }; ScopedClusterType clusterType{ session, "MyType" };
ScopedCluster cluster1{ session, clusterType.lockAndGet(), "Cluster1" }; ScopedCluster cluster1{ session, clusterType.lockAndGet(), "Cluster1" };
@@ -406,7 +406,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiRolesMultiClusters) TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiRolesMultiClusters)
{ {
ScopedTrack track{ session, "MyTrackFile" }; ScopedTrack track{ session };
ScopedArtist artist{ session, "MyArtist" }; ScopedArtist artist{ session, "MyArtist" };
ScopedClusterType clusterType{ session, "MyType" }; ScopedClusterType clusterType{ session, "MyType" };
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" }; ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
@@ -450,7 +450,7 @@ namespace lms::db::tests
for (std::size_t i{}; i < nbTracks; ++i) for (std::size_t i{}; i < nbTracks; ++i)
{ {
tracks.emplace_back(session, "MyTrackFile" + std::to_string(i)); tracks.emplace_back(session);
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
TrackArtistLink::create(session, tracks.back().get(), artist.get(), TrackArtistLinkType::Artist); TrackArtistLink::create(session, tracks.back().get(), artist.get(), TrackArtistLinkType::Artist);
@@ -486,7 +486,7 @@ namespace lms::db::tests
for (std::size_t i{}; i < 10; ++i) for (std::size_t i{}; i < 10; ++i)
{ {
tracks.emplace_back(session, "MyTrack" + std::to_string(i)); tracks.emplace_back(session);
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
@@ -515,7 +515,7 @@ namespace lms::db::tests
for (std::size_t i{}; i < 5; ++i) for (std::size_t i{}; i < 5; ++i)
{ {
tracks.emplace_back(session, "MyTrack" + std::to_string(i)); tracks.emplace_back(session);
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
@@ -525,7 +525,7 @@ namespace lms::db::tests
for (std::size_t i{ 5 }; i < 10; ++i) for (std::size_t i{ 5 }; i < 10; ++i)
{ {
tracks.emplace_back(session, "MyTrack" + std::to_string(i)); tracks.emplace_back(session);
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
@@ -555,7 +555,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtistSingleCluster) TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtistSingleCluster)
{ {
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
ScopedRelease release{ session, "MyRelease" }; ScopedRelease release{ session, "MyRelease" };
ScopedArtist artist{ session, "MyArtist" }; ScopedArtist artist{ session, "MyArtist" };
ScopedClusterType clusterType{ session, "MyType" }; ScopedClusterType clusterType{ session, "MyType" };
@@ -597,7 +597,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtistMultiClusters) TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtistMultiClusters)
{ {
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
ScopedRelease release{ session, "MyRelease" }; ScopedRelease release{ session, "MyRelease" };
ScopedArtist artist{ session, "MyArtist" }; ScopedArtist artist{ session, "MyArtist" };
ScopedClusterType clusterType{ session, "MyClusterType" }; ScopedClusterType clusterType{ session, "MyClusterType" };
@@ -636,7 +636,7 @@ namespace lms::db::tests
for (std::size_t i{}; i < 20; ++i) for (std::size_t i{}; i < 20; ++i)
{ {
tracks.emplace_back(session, "MyTrack" + std::to_string(i)); tracks.emplace_back(session);
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
@@ -669,7 +669,7 @@ namespace lms::db::tests
for (std::size_t i{}; i < 20; ++i) for (std::size_t i{}; i < 20; ++i)
{ {
tracks.emplace_back(session, "MyTrack" + std::to_string(i)); tracks.emplace_back(session);
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
@@ -729,7 +729,7 @@ namespace lms::db::tests
std::list<ScopedTrack> tracks; std::list<ScopedTrack> tracks;
for (std::size_t i{}; i < 10; ++i) for (std::size_t i{}; i < 10; ++i)
{ {
tracks.emplace_back(session, "MyTrack" + std::to_string(i)); tracks.emplace_back(session);
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
@@ -744,7 +744,7 @@ namespace lms::db::tests
cluster1.get().modify()->addTrack(tracks.back().get()); cluster1.get().modify()->addTrack(tracks.back().get());
} }
tracks.emplace_back(session, "MyTrack" + std::to_string(tracks.size())); tracks.emplace_back(session);
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
TrackArtistLink::create(session, tracks.back().get(), artist3.get(), TrackArtistLinkType::Artist); TrackArtistLink::create(session, tracks.back().get(), artist3.get(), TrackArtistLinkType::Artist);
@@ -810,7 +810,7 @@ namespace lms::db::tests
std::list<ScopedTrack> tracks; std::list<ScopedTrack> tracks;
for (std::size_t i{}; i < 10; ++i) for (std::size_t i{}; i < 10; ++i)
{ {
tracks.emplace_back(session, "MyTrack" + std::to_string(i)); tracks.emplace_back(session);
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
@@ -825,7 +825,7 @@ namespace lms::db::tests
cluster1.get().modify()->addTrack(tracks.back().get()); cluster1.get().modify()->addTrack(tracks.back().get());
} }
tracks.emplace_back(session, "MyTrack" + std::to_string(tracks.size())); tracks.emplace_back(session);
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
tracks.back().get().modify()->setRelease(release3.get()); tracks.back().get().modify()->setRelease(release3.get());
+2 -2
View File
@@ -60,8 +60,8 @@ namespace lms::db::tests
_tmpDb = std::make_unique<TmpDatabase>(); _tmpDb = std::make_unique<TmpDatabase>();
{ {
db::Session s{ _tmpDb->getDb() }; db::Session s{ _tmpDb->getDb() };
s.prepareTables(); s.prepareTablesIfNeeded();
s.analyze(); s.createIndexesIfNeeded();
} }
} }
+2 -2
View File
@@ -32,7 +32,7 @@ namespace lms::db::tests
for (std::size_t i{}; i < nbTracks; ++i) for (std::size_t i{}; i < nbTracks; ++i)
{ {
tracks.emplace_back(session, "MyTrack" + std::to_string(i)); tracks.emplace_back(session);
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
@@ -60,7 +60,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtist) TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtist)
{ {
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
ScopedRelease release{ session, "MyRelease" }; ScopedRelease release{ session, "MyRelease" };
ScopedArtist artist{ session, "MyArtist" }; ScopedArtist artist{ session, "MyArtist" };
+41 -41
View File
@@ -26,7 +26,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Listen_getAll) TEST_F(DatabaseFixture, Listen_getAll)
{ {
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
{ {
@@ -59,7 +59,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Listen_get) TEST_F(DatabaseFixture, Listen_get)
{ {
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12, 0, 1}} }; ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12, 0, 1}} };
@@ -93,7 +93,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Listen_get_multi) TEST_F(DatabaseFixture, Listen_get_multi)
{ {
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
ScopedListen listen3{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12, 0, 3}} }; ScopedListen listen3{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12, 0, 3}} };
ScopedListen listen1{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12, 0, 1}} }; ScopedListen listen1{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12, 0, 1}} };
@@ -112,7 +112,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Listen_get_byDateTime) TEST_F(DatabaseFixture, Listen_get_byDateTime)
{ {
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
const Wt::WDateTime dateTime1{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} }; const Wt::WDateTime dateTime1{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} };
const Wt::WDateTime dateTime2{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 2} }; const Wt::WDateTime dateTime2{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 2} };
@@ -145,7 +145,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Listen_getTopArtists) TEST_F(DatabaseFixture, Listen_getTopArtists)
{ {
ScopedTrack track1{ session, "MyTrack" }; ScopedTrack track1{ session };
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
const Wt::WDateTime dateTime1{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} }; const Wt::WDateTime dateTime1{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} };
ScopedListen listen1{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime1 }; ScopedListen listen1{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime1 };
@@ -162,7 +162,7 @@ namespace lms::db::tests
EXPECT_EQ(artists.moreResults, false); EXPECT_EQ(artists.moreResults, false);
} }
ScopedTrack track2{ session, "MyTrack2" }; ScopedTrack track2{ session };
ScopedArtist artist1{ session, "MyArtist1" }; ScopedArtist artist1{ session, "MyArtist1" };
ScopedListen listen2{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime1.addSecs(1) }; ScopedListen listen2{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime1.addSecs(1) };
@@ -226,9 +226,9 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Listen_getTopArtists_multi) TEST_F(DatabaseFixture, Listen_getTopArtists_multi)
{ {
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
ScopedTrack track1{ session, "MyTrack1" }; ScopedTrack track1{ session };
ScopedArtist artist1{ session, "MyArtist1" }; ScopedArtist artist1{ session, "MyArtist1" };
ScopedTrack track2{ session, "MyTrack2" }; ScopedTrack track2{ session };
ScopedArtist artist2{ session, "MyArtist2" }; ScopedArtist artist2{ session, "MyArtist2" };
const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} }; const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} };
@@ -293,7 +293,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Listen_getTopArtists_cluster) TEST_F(DatabaseFixture, Listen_getTopArtists_cluster)
{ {
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
ScopedArtist artist{ session, "MyArtist" }; ScopedArtist artist{ session, "MyArtist" };
const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} }; const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} };
@@ -337,7 +337,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Listen_getTopArtists_mediaLibrary) TEST_F(DatabaseFixture, Listen_getTopArtists_mediaLibrary)
{ {
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
ScopedArtist artist{ session, "MyArtist" }; ScopedArtist artist{ session, "MyArtist" };
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
const Wt::WDateTime dateTime1{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} }; const Wt::WDateTime dateTime1{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} };
@@ -403,7 +403,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Listen_getTopReleases) TEST_F(DatabaseFixture, Listen_getTopReleases)
{ {
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} }; const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} };
ScopedRelease release{ session, "MyRelease" }; ScopedRelease release{ session, "MyRelease" };
@@ -453,8 +453,8 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Listen_getTopReleases_multi) TEST_F(DatabaseFixture, Listen_getTopReleases_multi)
{ {
ScopedTrack track1{ session, "MyTrack" }; ScopedTrack track1{ session };
ScopedTrack track2{ session, "MyTrack" }; ScopedTrack track2{ session };
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} }; const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} };
ScopedListen listen1{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime }; ScopedListen listen1{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime };
@@ -513,7 +513,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Listen_getTopReleases_cluster) TEST_F(DatabaseFixture, Listen_getTopReleases_cluster)
{ {
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} }; const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} };
ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime }; ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime };
@@ -558,7 +558,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Listen_getTopReleases_mediaLibrary) TEST_F(DatabaseFixture, Listen_getTopReleases_mediaLibrary)
{ {
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} }; const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} };
ScopedRelease release{ session, "MyRelease" }; ScopedRelease release{ session, "MyRelease" };
@@ -615,7 +615,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Listen_getTopTracks) TEST_F(DatabaseFixture, Listen_getTopTracks)
{ {
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} }; const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} };
@@ -660,7 +660,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Listen_getTopTracks_artist) TEST_F(DatabaseFixture, Listen_getTopTracks_artist)
{ {
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
ScopedArtist artist{ session, "MyArtist" }; ScopedArtist artist{ session, "MyArtist" };
const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} }; const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} };
@@ -714,8 +714,8 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Listen_getTopTrack_multi) TEST_F(DatabaseFixture, Listen_getTopTrack_multi)
{ {
ScopedTrack track1{ session, "MyTrack1" }; ScopedTrack track1{ session };
ScopedTrack track2{ session, "MyTrack2" }; ScopedTrack track2{ session };
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} }; const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} };
ScopedListen listen1{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime }; ScopedListen listen1{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime };
@@ -766,7 +766,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Listen_getTopTracks_cluster) TEST_F(DatabaseFixture, Listen_getTopTracks_cluster)
{ {
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} }; const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} };
ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime }; ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime };
@@ -805,7 +805,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Listen_getTopTracks_mediaLibrary) TEST_F(DatabaseFixture, Listen_getTopTracks_mediaLibrary)
{ {
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} }; const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} };
ScopedMediaLibrary library{ session }; ScopedMediaLibrary library{ session };
@@ -860,7 +860,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Listen_getRecentArtists) TEST_F(DatabaseFixture, Listen_getRecentArtists)
{ {
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
ScopedArtist artist{ session, "MyArtist" }; ScopedArtist artist{ session, "MyArtist" };
@@ -937,9 +937,9 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Listen_getRecentArtists_multi) TEST_F(DatabaseFixture, Listen_getRecentArtists_multi)
{ {
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
ScopedTrack track1{ session, "MyTrack1" }; ScopedTrack track1{ session };
ScopedArtist artist1{ session, "MyArtist1" }; ScopedArtist artist1{ session, "MyArtist1" };
ScopedTrack track2{ session, "MyTrack2" }; ScopedTrack track2{ session };
ScopedArtist artist2{ session, "MyArtist2" }; ScopedArtist artist2{ session, "MyArtist2" };
const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} }; const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} };
@@ -1003,7 +1003,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Listen_getRecentArtists_cluster) TEST_F(DatabaseFixture, Listen_getRecentArtists_cluster)
{ {
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
ScopedArtist artist{ session, "MyArtist" }; ScopedArtist artist{ session, "MyArtist" };
const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} }; const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} };
@@ -1048,7 +1048,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Listen_getRecentArtists_mediaLibrary) TEST_F(DatabaseFixture, Listen_getRecentArtists_mediaLibrary)
{ {
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
ScopedArtist artist{ session, "MyArtist" }; ScopedArtist artist{ session, "MyArtist" };
ScopedMediaLibrary library{ session }; ScopedMediaLibrary library{ session };
@@ -1086,7 +1086,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Listen_getRecentReleases) TEST_F(DatabaseFixture, Listen_getRecentReleases)
{ {
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
ScopedRelease release{ session, "MyRelease" }; ScopedRelease release{ session, "MyRelease" };
@@ -1137,7 +1137,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Listen_getMostRecentRelease) TEST_F(DatabaseFixture, Listen_getMostRecentRelease)
{ {
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
ScopedRelease release{ session, "MyRelease" }; ScopedRelease release{ session, "MyRelease" };
@@ -1189,8 +1189,8 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Listen_getRecentReleases_multi) TEST_F(DatabaseFixture, Listen_getRecentReleases_multi)
{ {
ScopedTrack track1{ session, "MyTrack1" }; ScopedTrack track1{ session };
ScopedTrack track2{ session, "MyTrack2" }; ScopedTrack track2{ session };
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
ScopedRelease release1{ session, "MyRelease1" }; ScopedRelease release1{ session, "MyRelease1" };
ScopedRelease release2{ session, "MyRelease2" }; ScopedRelease release2{ session, "MyRelease2" };
@@ -1265,7 +1265,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Listen_getRecentReleases_cluster) TEST_F(DatabaseFixture, Listen_getRecentReleases_cluster)
{ {
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
ScopedClusterType clusterType{ session, "MyType" }; ScopedClusterType clusterType{ session, "MyType" };
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" }; ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
@@ -1322,7 +1322,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Listen_getRecentReleases_mediaLibrary) TEST_F(DatabaseFixture, Listen_getRecentReleases_mediaLibrary)
{ {
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
ScopedRelease release{ session, "MyRelease" }; ScopedRelease release{ session, "MyRelease" };
ScopedMediaLibrary library{ session }; ScopedMediaLibrary library{ session };
@@ -1362,7 +1362,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Listen_getRecentTracks) TEST_F(DatabaseFixture, Listen_getRecentTracks)
{ {
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
{ {
@@ -1409,7 +1409,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Listen_getRecentTracks_mediaLibrary) TEST_F(DatabaseFixture, Listen_getRecentTracks_mediaLibrary)
{ {
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
ScopedMediaLibrary library{ session }; ScopedMediaLibrary library{ session };
ScopedMediaLibrary otherLibrary{ session }; ScopedMediaLibrary otherLibrary{ session };
@@ -1448,7 +1448,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Listen_getCount_track) TEST_F(DatabaseFixture, Listen_getCount_track)
{ {
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
{ {
@@ -1482,8 +1482,8 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Listen_getCount_release) TEST_F(DatabaseFixture, Listen_getCount_release)
{ {
ScopedTrack track1{ session, "MyTrack" }; ScopedTrack track1{ session };
ScopedTrack track2{ session, "MyTrack" }; ScopedTrack track2{ session };
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
ScopedRelease release{ session, "MyRelease" }; ScopedRelease release{ session, "MyRelease" };
@@ -1530,7 +1530,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Listen_getMostRecentTrack) TEST_F(DatabaseFixture, Listen_getMostRecentTrack)
{ {
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
{ {
@@ -1576,8 +1576,8 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Listen_getRecentTracks_multi) TEST_F(DatabaseFixture, Listen_getRecentTracks_multi)
{ {
ScopedTrack track1{ session, "MyTrack1" }; ScopedTrack track1{ session };
ScopedTrack track2{ session, "MyTrack2" }; ScopedTrack track2{ session };
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
const Wt::WDateTime dateTime{ Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1} }; const Wt::WDateTime dateTime{ Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1} };
@@ -1644,7 +1644,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Listen_getRecentTracks_cluster) TEST_F(DatabaseFixture, Listen_getRecentTracks_cluster)
{ {
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
const Wt::WDateTime dateTime{ Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1} }; const Wt::WDateTime dateTime{ Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1} };
ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime }; ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime };
+154 -36
View File
@@ -74,12 +74,105 @@ namespace lms::db::tests
} }
} }
TEST_F(DatabaseFixture, Release_findByRangedIdBased)
{
ScopedTrack track1{ session };
ScopedTrack track2a{ session };
ScopedTrack track2b{ session };
ScopedTrack track3{ session };
ScopedRelease release1{ session, "MyRelease1" };
ScopedRelease release2{ session, "MyRelease2" };
ScopedRelease release3{ session, "MyRelease3" };
ScopedMediaLibrary library{ session };
ScopedMediaLibrary otherLibrary{ session };
{
auto transaction{ session.createWriteTransaction() };
track2a.get().modify()->setMediaLibrary(library.get());
track2b.get().modify()->setMediaLibrary(library.get());
track1.get().modify()->setRelease(release1.get());
track2a.get().modify()->setRelease(release2.get());
track2b.get().modify()->setRelease(release2.get());
track3.get().modify()->setRelease(release3.get());
}
{
auto transaction{ session.createReadTransaction() };
ReleaseId lastRetrievedId;
std::vector<Release::pointer> visitedReleases;
Release::find(session, lastRetrievedId, 10, [&](const Release::pointer& release)
{
visitedReleases.push_back(release);
});
ASSERT_EQ(visitedReleases.size(), 3);
EXPECT_EQ(visitedReleases[0]->getId(), release1.getId());
EXPECT_EQ(visitedReleases[1]->getId(), release2.getId());
EXPECT_EQ(visitedReleases[2]->getId(), release3.getId());
EXPECT_EQ(lastRetrievedId, release3.getId());
}
{
auto transaction{ session.createReadTransaction() };
ReleaseId lastRetrievedId{ release1.getId() };
std::vector<Release::pointer> visitedReleases;
Release::find(session, lastRetrievedId, 1, [&](const Release::pointer& release)
{
visitedReleases.push_back(release);
});
ASSERT_EQ(visitedReleases.size(), 1);
EXPECT_EQ(visitedReleases[0]->getId(), release2.getId());
EXPECT_EQ(lastRetrievedId, release2.getId());
}
{
auto transaction{ session.createReadTransaction() };
ReleaseId lastRetrievedId{ release1.getId() };
std::vector<Release::pointer> visitedReleases;
Release::find(session, lastRetrievedId, 0, [&](const Release::pointer& release)
{
visitedReleases.push_back(release);
});
ASSERT_EQ(visitedReleases.size(), 0);
EXPECT_EQ(lastRetrievedId, release1.getId());
}
{
auto transaction{ session.createReadTransaction() };
ReleaseId lastRetrievedId;
std::vector<Release::pointer> visitedReleases;
Release::find(session, lastRetrievedId, 10, [&](const Release::pointer& release)
{
visitedReleases.push_back(release);
}, otherLibrary.getId());
ASSERT_EQ(visitedReleases.size(), 0);
EXPECT_EQ(lastRetrievedId, ReleaseId{});
}
{
auto transaction{ session.createReadTransaction() };
ReleaseId lastRetrievedId;
std::vector<Release::pointer> visitedReleases;
Release::find(session, lastRetrievedId, 10, [&](const Release::pointer& release)
{
visitedReleases.push_back(release);
}, library.getId());
ASSERT_EQ(visitedReleases.size(), 1);
EXPECT_EQ(visitedReleases[0]->getId(), release2.getId());
EXPECT_EQ(lastRetrievedId, release2.getId());
}
}
TEST_F(DatabaseFixture, Release_singleTrack) TEST_F(DatabaseFixture, Release_singleTrack)
{ {
ScopedRelease release{ session, "MyRelease" }; ScopedRelease release{ session, "MyRelease" };
{ {
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
@@ -136,7 +229,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Release_singleTrack_mediaLibrary) TEST_F(DatabaseFixture, Release_singleTrack_mediaLibrary)
{ {
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
ScopedRelease release{ session, "MyRelease" }; ScopedRelease release{ session, "MyRelease" };
ScopedMediaLibrary library{ session }; ScopedMediaLibrary library{ session };
ScopedMediaLibrary otherLibrary{ session }; ScopedMediaLibrary otherLibrary{ session };
@@ -164,17 +257,17 @@ namespace lms::db::tests
{ {
ScopedRelease release1{ session, "MyRelease" }; ScopedRelease release1{ session, "MyRelease" };
ScopedRelease release2{ session, "MyRelease" }; ScopedRelease release2{ session, "MyRelease" };
ScopedTrack track1{ session, "MyTrack" }; ScopedTrack track1{ session };
ScopedTrack track2{ session, "MyTrack" }; ScopedTrack track2{ session };
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
track1.get().modify()->setRelease(release1.get()); track1.get().modify()->setRelease(release1.get());
track1.get().modify()->setPath("/tmp/foo/foo.mp3"); track1.get().modify()->setAbsoluteFilePath("/tmp/foo/foo.mp3");
track2.get().modify()->setRelease(release2.get()); track2.get().modify()->setRelease(release2.get());
track2.get().modify()->setPath("/tmp/bar/bar.mp3"); track2.get().modify()->setAbsoluteFilePath("/tmp/bar/bar.mp3");
} }
{ {
@@ -203,12 +296,12 @@ namespace lms::db::tests
ScopedRelease release6{ session, "_yRelease" }; ScopedRelease release6{ session, "_yRelease" };
// filters does not work on orphans // filters does not work on orphans
ScopedTrack track1{ session, "MyTrack" }; ScopedTrack track1{ session };
ScopedTrack track2{ session, "MyTrack" }; ScopedTrack track2{ session };
ScopedTrack track3{ session, "MyTrack" }; ScopedTrack track3{ session };
ScopedTrack track4{ session, "MyTrack" }; ScopedTrack track4{ session };
ScopedTrack track5{ session, "MyTrack" }; ScopedTrack track5{ session };
ScopedTrack track6{ session, "MyTrack" }; ScopedTrack track6{ session };
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
@@ -269,7 +362,7 @@ namespace lms::db::tests
EXPECT_FALSE(release1->getTotalDisc()); EXPECT_FALSE(release1->getTotalDisc());
} }
ScopedTrack track1{ session, "MyTrack" }; ScopedTrack track1{ session };
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
@@ -298,7 +391,7 @@ namespace lms::db::tests
EXPECT_EQ(*release1->getTotalDisc(), 6); EXPECT_EQ(*release1->getTotalDisc(), 6);
} }
ScopedTrack track2{ session, "MyTrack2" }; ScopedTrack track2{ session };
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
@@ -323,7 +416,7 @@ namespace lms::db::tests
EXPECT_FALSE(release2->getTotalDisc()); EXPECT_FALSE(release2->getTotalDisc());
} }
ScopedTrack track3{ session, "MyTrack3" }; ScopedTrack track3{ session };
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
@@ -350,10 +443,10 @@ namespace lms::db::tests
ScopedRelease release1{ session, "MyRelease1" }; ScopedRelease release1{ session, "MyRelease1" };
ScopedRelease release2{ session, "MyRelease2" }; ScopedRelease release2{ session, "MyRelease2" };
ScopedTrack track1A{ session, "MyTrack1A" }; ScopedTrack track1A{ session };
ScopedTrack track1B{ session, "MyTrack1B" }; ScopedTrack track1B{ session };
ScopedTrack track2A{ session, "MyTrack2A" }; ScopedTrack track2A{ session };
ScopedTrack track2B{ session, "MyTrack2B" }; ScopedTrack track2B{ session };
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
@@ -403,10 +496,10 @@ namespace lms::db::tests
const Wt::WDate release1Date{ Wt::WDate {1994, 2, 3} }; const Wt::WDate release1Date{ Wt::WDate {1994, 2, 3} };
const Wt::WDate release1OriginalDate{ Wt::WDate {1993, 4, 5} }; const Wt::WDate release1OriginalDate{ Wt::WDate {1993, 4, 5} };
ScopedTrack track1A{ session, "MyTrack1A" }; ScopedTrack track1A{ session };
ScopedTrack track1B{ session, "MyTrack1B" }; ScopedTrack track1B{ session };
ScopedTrack track2A{ session, "MyTrack2A" }; ScopedTrack track2A{ session };
ScopedTrack track2B{ session, "MyTrack2B" }; ScopedTrack track2B{ session };
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
@@ -455,10 +548,10 @@ namespace lms::db::tests
const int release1Year{ 1994 }; const int release1Year{ 1994 };
const int release1OriginalYear{ 1993 }; const int release1OriginalYear{ 1993 };
ScopedTrack track1A{ session, "MyTrack1A" }; ScopedTrack track1A{ session };
ScopedTrack track1B{ session, "MyTrack1B" }; ScopedTrack track1B{ session };
ScopedTrack track2A{ session, "MyTrack2A" }; ScopedTrack track2A{ session };
ScopedTrack track2B{ session, "MyTrack2B" }; ScopedTrack track2B{ session };
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
@@ -504,7 +597,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Release_writtenAfter) TEST_F(DatabaseFixture, Release_writtenAfter)
{ {
ScopedRelease release{ session, "MyRelease" }; ScopedRelease release{ session, "MyRelease" };
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
const Wt::WDateTime dateTime{ Wt::WDate {1950, 1, 1}, Wt::WTime {12, 30, 20} }; const Wt::WDateTime dateTime{ Wt::WDate {1950, 1, 1}, Wt::WTime {12, 30, 20} };
@@ -536,7 +629,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Release_artist) TEST_F(DatabaseFixture, Release_artist)
{ {
ScopedRelease release{ session, "MyRelease" }; ScopedRelease release{ session, "MyRelease" };
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
ScopedArtist artist{ session, "MyArtist" }; ScopedArtist artist{ session, "MyArtist" };
ScopedArtist artist2{ session, "MyArtist2" }; ScopedArtist artist2{ session, "MyArtist2" };
{ {
@@ -597,8 +690,8 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Release_getDiscCount) TEST_F(DatabaseFixture, Release_getDiscCount)
{ {
ScopedRelease release{ session, "MyRelease" }; ScopedRelease release{ session, "MyRelease" };
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
ScopedTrack track2{ session, "MyTrack2" }; ScopedTrack track2{ session };
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
@@ -723,8 +816,8 @@ namespace lms::db::tests
ScopedRelease release2{ session, "MyRelease2" }; ScopedRelease release2{ session, "MyRelease2" };
const Wt::WDate release2Date{ Wt::WDate {1994, 2, 3} }; const Wt::WDate release2Date{ Wt::WDate {1994, 2, 3} };
ScopedTrack track1{ session, "MyTrack1" }; ScopedTrack track1{ session };
ScopedTrack track2{ session, "MyTrack2" }; ScopedTrack track2{ session };
ASSERT_LT(release2Date, release1Date); ASSERT_LT(release2Date, release1Date);
ASSERT_GT(release2Date, release1OriginalDate); ASSERT_GT(release2Date, release1OriginalDate);
@@ -786,9 +879,9 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Release_meanBitrate) TEST_F(DatabaseFixture, Release_meanBitrate)
{ {
ScopedRelease release1{ session, "MyRelease1" }; ScopedRelease release1{ session, "MyRelease1" };
ScopedTrack track1{ session, "MyTrack1" }; ScopedTrack track1{ session };
ScopedTrack track2{ session, "MyTrack2" }; ScopedTrack track2{ session };
ScopedTrack track3{ session, "MyTrack3" }; ScopedTrack track3{ session };
auto checkExpectedBitrate = [&](std::size_t bitrate) auto checkExpectedBitrate = [&](std::size_t bitrate)
{ {
@@ -820,4 +913,29 @@ namespace lms::db::tests
} }
checkExpectedBitrate(192); // 0 should not be taken into account checkExpectedBitrate(192); // 0 should not be taken into account
} }
TEST_F(DatabaseFixture, Release_trackCount)
{
ScopedRelease release1{ session, "MyRelease1" };
ScopedRelease release2{ session, "MyRelease2" };
ScopedRelease release3{ session, "MyRelease2" };
ScopedTrack track1{ session };
ScopedTrack track2{ session };
ScopedTrack track3{ session };
{
auto transaction{ session.createWriteTransaction() };
track1.get().modify()->setRelease(release1.get());
track2.get().modify()->setRelease(release1.get());
track3.get().modify()->setRelease(release2.get());
}
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(release1->getTrackCount(), 2);
EXPECT_EQ(release2->getTrackCount(), 1);
EXPECT_EQ(release3->getTrackCount(), 0);
}
}
} }
+4 -4
View File
@@ -26,7 +26,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, StarredTrack) TEST_F(DatabaseFixture, StarredTrack)
{ {
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
ScopedUser user2{ session, "MyUser2" }; ScopedUser user2{ session, "MyUser2" };
@@ -78,7 +78,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Starredtrack_PendingDestroy) TEST_F(DatabaseFixture, Starredtrack_PendingDestroy)
{ {
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
ScopedStarredTrack starredTrack{ session, track.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal }; ScopedStarredTrack starredTrack{ session, track.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
@@ -96,8 +96,8 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, StarredTrack_dateTime) TEST_F(DatabaseFixture, StarredTrack_dateTime)
{ {
ScopedTrack track1{ session, "MyTrack1" }; ScopedTrack track1{ session };
ScopedTrack track2{ session, "MyTrack2" }; ScopedTrack track2{ session };
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
ScopedStarredTrack starredTrack1{ session, track1.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal }; ScopedStarredTrack starredTrack1{ session, track1.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
+94 -25
View File
@@ -39,7 +39,7 @@ namespace lms::db::tests
} }
} }
ScopedTrack track{ session, "MyTrackFile" }; ScopedTrack track{ session };
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
@@ -65,17 +65,23 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Track_findByRangedIdBased) TEST_F(DatabaseFixture, Track_findByRangedIdBased)
{ {
ScopedTrack track1{ session, "MyTrackFile1" }; ScopedTrack track1{ session };
ScopedTrack track2{ session, "MyTrackFile1" }; ScopedTrack track2{ session };
ScopedTrack track3{ session, "MyTrackFile1" }; ScopedTrack track3{ session };
ScopedMediaLibrary library{ session };
ScopedMediaLibrary otherLibrary{ session };
{
auto transaction{ session.createWriteTransaction() };
track2.get().modify()->setMediaLibrary(library.get());
}
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
bool moreResults;
TrackId lastRetrievedTrackId; TrackId lastRetrievedTrackId;
std::vector<Track::pointer> visitedTracks; std::vector<Track::pointer> visitedTracks;
Track::find(session, lastRetrievedTrackId, 10, moreResults, [&](const Track::pointer& track) Track::find(session, lastRetrievedTrackId, 10, [&](const Track::pointer& track)
{ {
visitedTracks.push_back(track); visitedTracks.push_back(track);
}); });
@@ -83,45 +89,67 @@ namespace lms::db::tests
EXPECT_EQ(visitedTracks[0]->getId(), track1.getId()); EXPECT_EQ(visitedTracks[0]->getId(), track1.getId());
EXPECT_EQ(visitedTracks[1]->getId(), track2.getId()); EXPECT_EQ(visitedTracks[1]->getId(), track2.getId());
EXPECT_EQ(visitedTracks[2]->getId(), track3.getId()); EXPECT_EQ(visitedTracks[2]->getId(), track3.getId());
EXPECT_FALSE(moreResults);
EXPECT_EQ(lastRetrievedTrackId, track3.getId()); EXPECT_EQ(lastRetrievedTrackId, track3.getId());
} }
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
bool moreResults;
TrackId lastRetrievedTrackId{ track1.getId() }; TrackId lastRetrievedTrackId{ track1.getId() };
std::vector<Track::pointer> visitedTracks; std::vector<Track::pointer> visitedTracks;
Track::find(session, lastRetrievedTrackId, 1, moreResults, [&](const Track::pointer& track) Track::find(session, lastRetrievedTrackId, 1, [&](const Track::pointer& track)
{ {
visitedTracks.push_back(track); visitedTracks.push_back(track);
}); });
ASSERT_EQ(visitedTracks.size(), 1); ASSERT_EQ(visitedTracks.size(), 1);
EXPECT_EQ(visitedTracks[0]->getId(), track2.getId()); EXPECT_EQ(visitedTracks[0]->getId(), track2.getId());
EXPECT_TRUE(moreResults);
EXPECT_EQ(lastRetrievedTrackId, track2.getId()); EXPECT_EQ(lastRetrievedTrackId, track2.getId());
} }
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
bool moreResults;
TrackId lastRetrievedTrackId{ track1.getId() }; TrackId lastRetrievedTrackId{ track1.getId() };
std::vector<Track::pointer> visitedTracks; std::vector<Track::pointer> visitedTracks;
Track::find(session, lastRetrievedTrackId, 0, moreResults, [&](const Track::pointer& track) Track::find(session, lastRetrievedTrackId, 0, [&](const Track::pointer& track)
{ {
visitedTracks.push_back(track); visitedTracks.push_back(track);
}); });
ASSERT_EQ(visitedTracks.size(), 0); ASSERT_EQ(visitedTracks.size(), 0);
EXPECT_TRUE(moreResults);
EXPECT_EQ(lastRetrievedTrackId, track1.getId()); EXPECT_EQ(lastRetrievedTrackId, track1.getId());
} }
{
auto transaction{ session.createReadTransaction() };
TrackId lastRetrievedTrackId{};
std::vector<Track::pointer> visitedTracks;
Track::find(session, lastRetrievedTrackId, 10, [&](const Track::pointer& track)
{
visitedTracks.push_back(track);
}, otherLibrary.getId());
ASSERT_EQ(visitedTracks.size(), 0);
EXPECT_EQ(lastRetrievedTrackId, TrackId{});
}
{
auto transaction{ session.createReadTransaction() };
TrackId lastRetrievedTrackId{};
std::vector<Track::pointer> visitedTracks;
Track::find(session, lastRetrievedTrackId, 10, [&](const Track::pointer& track)
{
visitedTracks.push_back(track);
}, library.getId());
ASSERT_EQ(visitedTracks.size(), 1);
EXPECT_EQ(visitedTracks[0]->getId(), track2.getId());
EXPECT_EQ(lastRetrievedTrackId, track2.getId());
}
} }
TEST_F(DatabaseFixture, Track_MediaLibrary) TEST_F(DatabaseFixture, Track_MediaLibrary)
{ {
ScopedTrack track{ session, "MyTrackFile" }; ScopedTrack track{ session };
ScopedMediaLibrary library{ session }; ScopedMediaLibrary library{ session };
ScopedMediaLibrary otherLibrary{ session }; ScopedMediaLibrary otherLibrary{ session };
@@ -145,7 +173,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Track_noMediaLibrary) TEST_F(DatabaseFixture, Track_noMediaLibrary)
{ {
ScopedTrack track{ session, "MyTrackFile" }; ScopedTrack track{ session };
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
MediaLibrary::pointer mediaLibrary{ track->getMediaLibrary() }; MediaLibrary::pointer mediaLibrary{ track->getMediaLibrary() };
@@ -172,8 +200,8 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, MultipleTracks) TEST_F(DatabaseFixture, MultipleTracks)
{ {
ScopedTrack track1{ session, "MyTrackFile1" }; ScopedTrack track1{ session };
ScopedTrack track2{ session, "MyTrackFile2" }; ScopedTrack track2{ session };
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
@@ -186,12 +214,12 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, MultipleTracksSearchByFilter) TEST_F(DatabaseFixture, MultipleTracksSearchByFilter)
{ {
ScopedTrack track1{ session, "" }; ScopedTrack track1{ session };
ScopedTrack track2{ session, "" }; ScopedTrack track2{ session };
ScopedTrack track3{ session, "" }; ScopedTrack track3{ session };
ScopedTrack track4{ session, "" }; ScopedTrack track4{ session };
ScopedTrack track5{ session, "" }; ScopedTrack track5{ session };
ScopedTrack track6{ session, "" }; ScopedTrack track6{ session };
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
@@ -232,7 +260,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Track_date) TEST_F(DatabaseFixture, Track_date)
{ {
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
const Wt::WDate date{ 1995, 5, 5 }; const Wt::WDate date{ 1995, 5, 5 };
const Wt::WDate originalDate{ 1994, 2, 2 }; const Wt::WDate originalDate{ 1994, 2, 2 };
{ {
@@ -270,7 +298,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, Track_writtenAfter) TEST_F(DatabaseFixture, Track_writtenAfter)
{ {
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
const Wt::WDateTime dateTime{ Wt::WDate {1950, 1, 1}, Wt::WTime {12, 30, 20} }; const Wt::WDateTime dateTime{ Wt::WDate {1950, 1, 1}, Wt::WTime {12, 30, 20} };
@@ -297,4 +325,45 @@ namespace lms::db::tests
EXPECT_EQ(tracks.results.size(), 0); EXPECT_EQ(tracks.results.size(), 0);
} }
} }
TEST_F(DatabaseFixture, Track_path)
{
ScopedTrack track{ session };
{
auto transaction{ session.createWriteTransaction() };
track.get().modify()->setAbsoluteFilePath("/root/foo/file.path");
track.get().modify()->setRelativeFilePath("foo/file.path");
}
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(track->getAbsoluteFilePath(), "/root/foo/file.path");
EXPECT_EQ(track->getRelativeFilePath(), "foo/file.path");
}
}
TEST_F(DatabaseFixture, Track_audioProperties)
{
ScopedTrack track{ session };
{
auto transaction{ session.createWriteTransaction() };
track.get().modify()->setBitrate(128000);
track.get().modify()->setBitsPerSample(16);
track.get().modify()->setDuration(std::chrono::minutes{ 3 });
track.get().modify()->setChannelCount(2);
track.get().modify()->setSampleRate(44100);
}
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(track->getBitrate(), 128000);
EXPECT_EQ(track->getBitsPerSample(), 16);
EXPECT_EQ(track->getDuration(), std::chrono::minutes{ 3 });
EXPECT_EQ(track->getChannelCount(), 2);
EXPECT_EQ(track->getSampleRate(), 44100);
}
}
} }
+1 -1
View File
@@ -27,7 +27,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, TrackBookmark) TEST_F(DatabaseFixture, TrackBookmark)
{ {
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
{ {
+1 -1
View File
@@ -27,7 +27,7 @@ namespace lms::db::tests
TEST_F(DatabaseFixture, TrackFeatures) TEST_F(DatabaseFixture, TrackFeatures)
{ {
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
{ {
+6 -6
View File
@@ -44,7 +44,7 @@ namespace lms::db::tests
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
ScopedTrackList trackList1{ session, "MyTrackList1", TrackListType::Playlist, false, user.lockAndGet() }; ScopedTrackList trackList1{ session, "MyTrackList1", TrackListType::Playlist, false, user.lockAndGet() };
ScopedTrackList trackList2{ session, "MyTrackList2", TrackListType::Playlist, false, user.lockAndGet() }; ScopedTrackList trackList2{ session, "MyTrackList2", TrackListType::Playlist, false, user.lockAndGet() };
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
@@ -79,7 +79,7 @@ namespace lms::db::tests
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
ScopedTrackList trackList2{ session, "MyTrackList2", TrackListType::Playlist, false, user.lockAndGet() }; ScopedTrackList trackList2{ session, "MyTrackList2", TrackListType::Playlist, false, user.lockAndGet() };
ScopedTrackList trackList1{ session, "MyTrackList1", TrackListType::Playlist, false, user.lockAndGet() }; ScopedTrackList trackList1{ session, "MyTrackList1", TrackListType::Playlist, false, user.lockAndGet() };
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
@@ -131,7 +131,7 @@ namespace lms::db::tests
for (std::size_t i{}; i < 10; ++i) for (std::size_t i{}; i < 10; ++i)
{ {
tracks.emplace_back(session, "MyTrack" + std::to_string(i)); tracks.emplace_back(session);
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
session.create<TrackListEntry>(tracks.back().get(), trackList.get()); session.create<TrackListEntry>(tracks.back().get(), trackList.get());
@@ -158,7 +158,7 @@ namespace lms::db::tests
ScopedTrackList trackList2{ session, "MyTrackList2", TrackListType::Playlist, false, user.lockAndGet() }; ScopedTrackList trackList2{ session, "MyTrackList2", TrackListType::Playlist, false, user.lockAndGet() };
ScopedClusterType clusterType{ session, "MyClusterType" }; ScopedClusterType clusterType{ session, "MyClusterType" };
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" }; ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
ScopedTrack track{ session, "MyTrack" }; ScopedTrack track{ session };
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
@@ -187,8 +187,8 @@ namespace lms::db::tests
{ {
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
ScopedTrackList trackList{ session, "MyTrackList", TrackListType::Playlist, false, user.lockAndGet() }; ScopedTrackList trackList{ session, "MyTrackList", TrackListType::Playlist, false, user.lockAndGet() };
ScopedTrack track1{ session, "MyTrack" }; ScopedTrack track1{ session };
ScopedTrack track2{ session, "MyTrack" }; ScopedTrack track2{ session };
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
+1
View File
@@ -1,5 +1,6 @@
add_library(lmsimage SHARED add_library(lmsimage SHARED
impl/SvgImage.cpp
) )
target_include_directories(lmsimage INTERFACE target_include_directories(lmsimage INTERFACE
+53
View File
@@ -0,0 +1,53 @@
/*
* Copyright (C) 2015 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 "SvgImage.hpp"
#include <fstream>
#include "core/ITraceLogger.hpp"
#include "image/Exception.hpp"
namespace lms::image
{
std::unique_ptr<IEncodedImage> readSvgFile(const std::filesystem::path& p)
{
LMS_SCOPED_TRACE_DETAILED("Image", "ReadSVG");
if (p.extension() != ".svg")
throw Exception{ "Unexpected file extension: '" + p.extension().string() + "', expected .svg" };
std::ifstream ifs{ p.string(), std::ios::binary };
if (!ifs.is_open())
throw Exception{ "Cannot open file '" + p.string() + "' for reading purpose" };
std::vector<std::byte> data;
// read file content
ifs.seekg(0, std::ios::end);
std::streamsize size = ifs.tellg();
if (size < 0)
throw Exception{ "Cannot determine file size for '" + p.string() + "'" };
ifs.seekg(0, std::ios::beg);
data.resize(size);
if (!ifs.read(reinterpret_cast<char*>(data.data()), size))
throw Exception{ "Cannot read file content for '" + p.string() + "'" };
return std::make_unique<SvgImage>(std::move(data));
}
}
+42
View File
@@ -0,0 +1,42 @@
/*
* Copyright (C) 2015 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 <memory>
#include <vector>
#include "image/IEncodedImage.hpp"
namespace lms::image
{
class SvgImage : public IEncodedImage
{
public:
SvgImage(std::vector<std::byte>&& data) : _data{ std::move(data) } {}
const std::byte* getData() const { return &_data.front(); }
std::size_t getDataSize() const { return _data.size(); }
std::string_view getMimeType() const { return "image/svg+xml"; }
private:
const std::vector<std::byte> _data;
};
}
+3
View File
@@ -22,6 +22,7 @@
#define STB_IMAGE_WRITE_IMPLEMENTATION #define STB_IMAGE_WRITE_IMPLEMENTATION
#include <stb_image_write.h> #include <stb_image_write.h>
#include "core/ITraceLogger.hpp"
#include "image/Exception.hpp" #include "image/Exception.hpp"
#include "RawImage.hpp" #include "RawImage.hpp"
@@ -29,6 +30,8 @@ namespace lms::image::STB
{ {
JPEGImage::JPEGImage(const RawImage& rawImage, unsigned quality) JPEGImage::JPEGImage(const RawImage& rawImage, unsigned quality)
{ {
LMS_SCOPED_TRACE_DETAILED("Image", "WriteJPEG");
auto writeCb {[](void* ctx, void* writeData, int writeSize) auto writeCb {[](void* ctx, void* writeData, int writeSize)
{ {
auto& output {*reinterpret_cast<std::vector<std::byte>*>(ctx)}; auto& output {*reinterpret_cast<std::vector<std::byte>*>(ctx)};
+6 -2
View File
@@ -30,19 +30,21 @@
#include <stb_image.h> #include <stb_image.h>
#include <stb_image_resize.h> #include <stb_image_resize.h>
#include "JPEGImage.hpp" #include "core/ITraceLogger.hpp"
#include "image/Exception.hpp" #include "image/Exception.hpp"
#include "JPEGImage.hpp"
namespace lms::image namespace lms::image
{ {
std::unique_ptr<IRawImage> decodeImage(const std::byte* encodedData, std::size_t encodedDataSize) std::unique_ptr<IRawImage> decodeImage(const std::byte* encodedData, std::size_t encodedDataSize)
{ {
LMS_SCOPED_TRACE_DETAILED("Image", "DecodeBuffer");
return std::make_unique<STB::RawImage>(encodedData, encodedDataSize); return std::make_unique<STB::RawImage>(encodedData, encodedDataSize);
} }
std::unique_ptr<IRawImage> decodeImage(const std::filesystem::path& path) std::unique_ptr<IRawImage> decodeImage(const std::filesystem::path& path)
{ {
LMS_SCOPED_TRACE_DETAILED("Image", "DecodeFile");
return std::make_unique<STB::RawImage>(path); return std::make_unique<STB::RawImage>(path);
} }
@@ -71,6 +73,8 @@ namespace lms::image::STB
void RawImage::resize(ImageSize width) void RawImage::resize(ImageSize width)
{ {
LMS_SCOPED_TRACE_DETAILED("Image", "Resize");
size_t height; size_t height;
if (_width == _height) if (_width == _height)
{ {
+5 -7
View File
@@ -23,11 +23,9 @@
namespace lms::image namespace lms::image
{ {
class Exception : public core::LmsException class Exception : public core::LmsException
{ {
public: public:
using LmsException::LmsException; using LmsException::LmsException;
}; };
} // namespace lms::cover } // namespace lms::cover
+10 -11
View File
@@ -24,16 +24,15 @@
namespace lms::image namespace lms::image
{ {
using ImageSize = std::size_t; using ImageSize = std::size_t;
class IEncodedImage class IEncodedImage
{ {
public: public:
virtual ~IEncodedImage() = default; virtual ~IEncodedImage() = default;
virtual const std::byte* getData() const = 0;
virtual std::size_t getDataSize() const = 0;
virtual std::string_view getMimeType() const = 0;
};
}
virtual const std::byte* getData() const = 0;
virtual std::size_t getDataSize() const = 0;
virtual std::string_view getMimeType() const = 0;
};
}
+7 -14
View File
@@ -19,23 +19,16 @@
#pragma once #pragma once
#include <filesystem>
#include <memory>
#include "image/IEncodedImage.hpp" #include "image/IEncodedImage.hpp"
namespace lms::image namespace lms::image
{ {
class IRawImage class IRawImage
{ {
public: public:
virtual ~IRawImage() = default; virtual ~IRawImage() = default;
virtual void resize(ImageSize width) = 0; virtual void resize(ImageSize width) = 0;
virtual std::unique_ptr<IEncodedImage> encodeToJPEG(unsigned quality) const = 0; virtual std::unique_ptr<IEncodedImage> encodeToJPEG(unsigned quality) const = 0;
}; };
void init(const std::filesystem::path& path);
std::unique_ptr<IRawImage> decodeImage(const std::byte* encodedData, std::size_t encodedDataSize);
std::unique_ptr<IRawImage> decodeImage(const std::filesystem::path& path);
} }
+34
View File
@@ -0,0 +1,34 @@
/*
* 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 <memory>
#include "image/IEncodedImage.hpp"
#include "image/IRawImage.hpp"
namespace lms::image
{
void init(const std::filesystem::path& path);
std::unique_ptr<IRawImage> decodeImage(const std::byte* encodedData, std::size_t encodedDataSize);
std::unique_ptr<IRawImage> decodeImage(const std::filesystem::path& path);
std::unique_ptr<IEncodedImage> readSvgFile(const std::filesystem::path& path);
}
@@ -144,6 +144,17 @@ namespace lms::metadata
try try
{ {
const auto audioFile{ av::parseAudioFile(p) }; const auto audioFile{ av::parseAudioFile(p) };
_audioProperties.duration = audioFile->getContainerInfo().duration;
const auto bestAudioStream{ audioFile->getBestStreamInfo() };
if (bestAudioStream)
{
_audioProperties.bitrate = bestAudioStream->bitrate;
_audioProperties.bitsPerSample = bestAudioStream->bitsPerSample;
_audioProperties.channelCount = bestAudioStream->channelCount;
_audioProperties.sampleRate = bestAudioStream->sampleRate;
}
_containerInfo = audioFile->getContainerInfo(); _containerInfo = audioFile->getContainerInfo();
_metaDataMap = audioFile->getMetaData(); _metaDataMap = audioFile->getMetaData();
+2 -5
View File
@@ -40,12 +40,9 @@ namespace lms::metadata
void visitTagValues(std::string_view tag, TagValueVisitor visitor) const override; void visitTagValues(std::string_view tag, TagValueVisitor visitor) const override;
void visitPerformerTags(PerformerVisitor visitor) const override; void visitPerformerTags(PerformerVisitor visitor) const override;
bool hasEmbeddedCover() const override { return _hasEmbeddedCover; } bool hasEmbeddedCover() const override { return _hasEmbeddedCover; }
const AudioProperties& getAudioProperties() const override { return _audioProperties; }
std::chrono::milliseconds getDuration() const override { return _containerInfo.duration; } AudioProperties _audioProperties;
std::size_t getBitrate() const override { return _containerInfo.bitrate; }
std::size_t getBitsPerSample() const override { return 0; }
std::size_t getSampleRate() const override { return 0; }
av::IAudioFile::MetadataMap _metaDataMap; av::IAudioFile::MetadataMap _metaDataMap;
av::ContainerInfo _containerInfo; av::ContainerInfo _containerInfo;
bool _hasEmbeddedCover{}; bool _hasEmbeddedCover{};
+1 -5
View File
@@ -153,10 +153,6 @@ namespace lms::metadata
virtual void visitPerformerTags(PerformerVisitor visitor) const = 0; virtual void visitPerformerTags(PerformerVisitor visitor) const = 0;
virtual bool hasEmbeddedCover() const = 0; virtual bool hasEmbeddedCover() const = 0;
virtual const AudioProperties& getAudioProperties() const = 0;
virtual std::chrono::milliseconds getDuration() const = 0;
virtual std::size_t getBitrate() const = 0;
virtual std::size_t getBitsPerSample() const = 0;
virtual std::size_t getSampleRate() const = 0;
}; };
} // namespace lms::metadata } // namespace lms::metadata
+1 -7
View File
@@ -235,18 +235,12 @@ namespace lms::metadata
{ {
auto track{ std::make_unique<Track>() }; auto track{ std::make_unique<Track>() };
processAudioProperties(tagReader, *track); track->audioProperties = tagReader.getAudioProperties();
processTags(tagReader, *track); processTags(tagReader, *track);
return track; return track;
} }
void Parser::processAudioProperties(const ITagReader& tagReader, Track& track)
{
track.duration = tagReader.getDuration();
track.bitrate = tagReader.getBitrate();
}
void Parser::processTags(const ITagReader& tagReader, Track& track) void Parser::processTags(const ITagReader& tagReader, Track& track)
{ {
track.hasCover = tagReader.hasEmbeddedCover(); track.hasCover = tagReader.hasEmbeddedCover();
-1
View File
@@ -37,7 +37,6 @@ namespace lms::metadata
void setArtistTagDelimiters(std::span<const std::string> delimiters) override { _artistTagDelimiters.assign(std::cbegin(delimiters), std::cend(delimiters)); } void setArtistTagDelimiters(std::span<const std::string> delimiters) override { _artistTagDelimiters.assign(std::cbegin(delimiters), std::cend(delimiters)); }
void setDefaultTagDelimiters(std::span<const std::string> delimiters) override { _defaultTagDelimiters.assign(std::cbegin(delimiters), std::cend(delimiters)); } void setDefaultTagDelimiters(std::span<const std::string> delimiters) override { _defaultTagDelimiters.assign(std::cbegin(delimiters), std::cend(delimiters)); }
void processAudioProperties(const ITagReader& reader, Track& track);
void processTags(const ITagReader& reader, Track& track); void processTags(const ITagReader& reader, Track& track);
std::optional<Medium> getMedium(const ITagReader& tagReader); std::optional<Medium> getMedium(const ITagReader& tagReader);
+27 -20
View File
@@ -19,6 +19,9 @@
#include "TagLibTagReader.hpp" #include "TagLibTagReader.hpp"
#include <unordered_map>
#include <taglib/apeproperties.h>
#include <taglib/apetag.h> #include <taglib/apetag.h>
#include <taglib/asffile.h> #include <taglib/asffile.h>
#include <taglib/id3v2tag.h> #include <taglib/id3v2tag.h>
@@ -196,6 +199,8 @@ namespace lms::metadata
throw ParsingFailedException{}; throw ParsingFailedException{};
} }
computeAudioProperties();
_propertyMap = _file.file()->properties(); _propertyMap = _file.file()->properties();
// Some tags may not be known by TagLib // Some tags may not be known by TagLib
@@ -303,6 +308,28 @@ namespace lms::metadata
} }
} }
void TagLibTagReader::computeAudioProperties()
{
const TagLib::AudioProperties* properties{ _file.audioProperties() };
// Common properties
_audioProperties.bitrate = static_cast<std::size_t>(properties->bitrate() * 1000);
_audioProperties.channelCount = static_cast<std::size_t>(_file.audioProperties()->channels());
_audioProperties.duration = std::chrono::milliseconds{ properties->lengthInMilliseconds() };
_audioProperties.sampleRate = static_cast<std::size_t>(properties->sampleRate());
if (const auto * apeProperties{ dynamic_cast<const TagLib::APE::Properties*>(properties) })
_audioProperties.bitsPerSample = apeProperties->bitsPerSample();
if (const auto * asfProperties{ dynamic_cast<const TagLib::ASF::Properties*>(properties) })
_audioProperties.bitsPerSample = asfProperties->bitsPerSample();
else if (const auto * flacProperties{ dynamic_cast<const TagLib::FLAC::Properties*>(properties) })
_audioProperties.bitsPerSample = flacProperties->bitsPerSample();
else if (const auto * mp4Properties{ dynamic_cast<const TagLib::MP4::Properties*>(properties) })
_audioProperties.bitsPerSample = mp4Properties->bitsPerSample();
else if (const auto * wavePackProperties{ dynamic_cast<const TagLib::WavPack::Properties*>(properties) })
_audioProperties.bitsPerSample = wavePackProperties->bitsPerSample();
}
void TagLibTagReader::visitTagValues(TagType tag, TagValueVisitor visitor) const void TagLibTagReader::visitTagValues(TagType tag, TagValueVisitor visitor) const
{ {
auto itTagNames{ tagMapping.find(tag) }; auto itTagNames{ tagMapping.find(tag) };
@@ -360,24 +387,4 @@ namespace lms::metadata
} }
} }
} }
std::chrono::milliseconds TagLibTagReader::getDuration() const
{
return std::chrono::milliseconds{ _file.audioProperties()->lengthInMilliseconds() };
}
std::size_t TagLibTagReader::getBitrate() const
{
return static_cast<std::size_t>(_file.audioProperties()->bitrate() * 1000);
}
std::size_t TagLibTagReader::getBitsPerSample() const
{
return 0; // TODO
}
std::size_t TagLibTagReader::getSampleRate() const
{
return static_cast<std::size_t>(_file.audioProperties()->sampleRate());
}
} // namespace lms::metadata } // namespace lms::metadata
+3 -4
View File
@@ -38,17 +38,16 @@ namespace lms::metadata
TagLibTagReader(const TagLibTagReader&) = delete; TagLibTagReader(const TagLibTagReader&) = delete;
TagLibTagReader& operator=(const TagLibTagReader&) = delete; TagLibTagReader& operator=(const TagLibTagReader&) = delete;
void computeAudioProperties();
void visitTagValues(TagType tag, TagValueVisitor visitor) const override; void visitTagValues(TagType tag, TagValueVisitor visitor) const override;
void visitTagValues(std::string_view tag, TagValueVisitor visitor) const override; void visitTagValues(std::string_view tag, TagValueVisitor visitor) const override;
void visitPerformerTags(PerformerVisitor visitor) const override; void visitPerformerTags(PerformerVisitor visitor) const override;
bool hasEmbeddedCover() const override { return _hasEmbeddedCover; } bool hasEmbeddedCover() const override { return _hasEmbeddedCover; }
std::chrono::milliseconds getDuration() const override; const AudioProperties& getAudioProperties() const override { return _audioProperties; }
std::size_t getBitrate() const override;
std::size_t getBitsPerSample() const override;
std::size_t getSampleRate() const override;
const TagLib::FileRef _file; const TagLib::FileRef _file;
AudioProperties _audioProperties;
TagLib::PropertyMap _propertyMap; // case-insensitive keys TagLib::PropertyMap _propertyMap; // case-insensitive keys
bool _hasEmbeddedCover{}; bool _hasEmbeddedCover{};
}; };
+10 -2
View File
@@ -83,8 +83,18 @@ namespace lms::metadata
} }
}; };
struct AudioProperties
{
std::size_t bitrate{};
std::size_t bitsPerSample{};
std::size_t channelCount{};
std::chrono::milliseconds duration{};
std::size_t sampleRate{};
};
struct Track struct Track
{ {
AudioProperties audioProperties;
std::optional<core::UUID> mbid; std::optional<core::UUID> mbid;
std::optional<core::UUID> recordingMBID; std::optional<core::UUID> recordingMBID;
std::string title; std::string title;
@@ -96,8 +106,6 @@ namespace lms::metadata
std::vector<std::string> labels; std::vector<std::string> labels;
std::vector<std::string> languages; std::vector<std::string> languages;
Tags userExtraTags; Tags userExtraTags;
std::chrono::milliseconds duration{};
std::size_t bitrate{};
std::optional<int> year{}; std::optional<int> year{};
Wt::WDate date; Wt::WDate date;
std::optional<int> originalYear{}; std::optional<int> originalYear{};
+10 -2
View File
@@ -88,6 +88,16 @@ namespace lms::metadata
std::unique_ptr<Track> track{ parser.parse(testTags) }; std::unique_ptr<Track> track{ parser.parse(testTags) };
// Audio properties
{
const AudioProperties& audioProperties{ testTags.getAudioProperties() };
EXPECT_EQ(track->audioProperties.bitrate, audioProperties.bitrate);
EXPECT_EQ(track->audioProperties.bitsPerSample, audioProperties.bitsPerSample);
EXPECT_EQ(track->audioProperties.channelCount, audioProperties.channelCount);
EXPECT_EQ(track->audioProperties.duration, audioProperties.duration);
EXPECT_EQ(track->audioProperties.sampleRate, audioProperties.sampleRate);
}
EXPECT_EQ(track->acoustID, core::UUID::fromString("e987a441-e134-4960-8019-274eddacc418")); EXPECT_EQ(track->acoustID, core::UUID::fromString("e987a441-e134-4960-8019-274eddacc418"));
EXPECT_EQ(track->artistDisplayName, "MyArtist1 & MyArtist2"); EXPECT_EQ(track->artistDisplayName, "MyArtist1 & MyArtist2");
ASSERT_EQ(track->artists.size(), 2); ASSERT_EQ(track->artists.size(), 2);
@@ -97,7 +107,6 @@ namespace lms::metadata
EXPECT_EQ(track->artists[1].name, "MyArtist2"); EXPECT_EQ(track->artists[1].name, "MyArtist2");
EXPECT_EQ(track->artists[1].sortName, "MyArtist2SortName"); EXPECT_EQ(track->artists[1].sortName, "MyArtist2SortName");
EXPECT_EQ(track->artists[1].mbid, core::UUID::fromString("5e2cf87f-c8d7-4504-8a86-954dc0840229")); EXPECT_EQ(track->artists[1].mbid, core::UUID::fromString("5e2cf87f-c8d7-4504-8a86-954dc0840229"));
EXPECT_EQ(track->bitrate, TestTagReader::trackBitrate);
ASSERT_EQ(track->composerArtists.size(), 2); ASSERT_EQ(track->composerArtists.size(), 2);
EXPECT_EQ(track->composerArtists[0].name, "MyComposer1"); EXPECT_EQ(track->composerArtists[0].name, "MyComposer1");
EXPECT_EQ(track->composerArtists[0].sortName, "MyComposerSortOrder1"); EXPECT_EQ(track->composerArtists[0].sortName, "MyComposerSortOrder1");
@@ -112,7 +121,6 @@ namespace lms::metadata
EXPECT_EQ(track->date.year(), 2020); EXPECT_EQ(track->date.year(), 2020);
EXPECT_EQ(track->date.month(), 3); EXPECT_EQ(track->date.month(), 3);
EXPECT_EQ(track->date.day(), 4); EXPECT_EQ(track->date.day(), 4);
EXPECT_EQ(track->duration, TestTagReader::trackDuration);
EXPECT_FALSE(track->hasCover); EXPECT_FALSE(track->hasCover);
ASSERT_EQ(track->genres.size(), 2); ASSERT_EQ(track->genres.size(), 2);
EXPECT_EQ(track->genres[0], "Genre1"); EXPECT_EQ(track->genres[0], "Genre1");
+9 -8
View File
@@ -28,10 +28,14 @@ namespace lms::metadata
class TestTagReader : public ITagReader class TestTagReader : public ITagReader
{ {
public: public:
static constexpr std::chrono::milliseconds trackDuration{ 180 }; static constexpr AudioProperties audioProperties
static constexpr std::size_t trackBitrate{ 128000 }; {
static constexpr std::size_t trackBitsPerSample{ 16 }; .bitrate = 128000,
static constexpr std::size_t trackSampleRate{ 44000 }; .bitsPerSample = 16,
.channelCount = 2,
.duration = std::chrono::seconds{180},
.sampleRate = 44000,
};
using Tags = std::unordered_map<TagType, std::vector<std::string_view>>; using Tags = std::unordered_map<TagType, std::vector<std::string_view>>;
using Performers = std::unordered_map<std::string_view, std::vector<std::string_view>>; using Performers = std::unordered_map<std::string_view, std::vector<std::string_view>>;
@@ -73,10 +77,7 @@ namespace lms::metadata
bool hasEmbeddedCover() const override { return false; }; bool hasEmbeddedCover() const override { return false; };
std::chrono::milliseconds getDuration() const override { return trackDuration; } const AudioProperties& getAudioProperties() const override { return audioProperties; }
std::size_t getBitrate() const override { return trackBitrate; }
std::size_t getBitsPerSample() const override { return trackBitsPerSample; }
std::size_t getSampleRate() const override { return trackSampleRate; }
private: private:
const Tags _tags; const Tags _tags;
+1
View File
@@ -1,5 +1,6 @@
add_library(lmsservice-cover SHARED add_library(lmsservice-cover SHARED
impl/ImageCache.cpp
impl/CoverService.cpp impl/CoverService.cpp
) )
+25 -95
View File
@@ -30,7 +30,7 @@
#include "database/Track.hpp" #include "database/Track.hpp"
#include "image/Exception.hpp" #include "image/Exception.hpp"
#include "image/IRawImage.hpp" #include "image/Image.hpp"
#include "core/IConfig.hpp" #include "core/IConfig.hpp"
#include "core/ILogger.hpp" #include "core/ILogger.hpp"
#include "core/Path.hpp" #include "core/Path.hpp"
@@ -63,7 +63,7 @@ namespace lms::cover
res = TrackInfo{}; res = TrackInfo{};
res->hasCover = track->hasCover(); res->hasCover = track->hasCover();
res->trackPath = track->getPath(); res->trackPath = track->getAbsoluteFilePath();
if (const db::Release::pointer & release{ track->getRelease() }) if (const db::Release::pointer & release{ track->getRelease() })
{ {
@@ -107,44 +107,29 @@ namespace lms::cover
} }
} }
std::unique_ptr<ICoverService> createCoverService(db::Db& db, const std::filesystem::path& execPath, const std::filesystem::path& defaultCoverPath) std::unique_ptr<ICoverService> createCoverService(db::Db& db, const std::filesystem::path& defaultSvgCoverPath)
{ {
return std::make_unique<CoverService>(db, execPath, defaultCoverPath); return std::make_unique<CoverService>(db, defaultSvgCoverPath);
} }
using namespace image; using namespace image;
CoverService::CoverService(db::Db& db, CoverService::CoverService(db::Db& db,
const std::filesystem::path& execPath, const std::filesystem::path& defaultSvgCoverPath)
const std::filesystem::path& defaultCoverPath)
: _db{ db } : _db{ db }
, _defaultCoverPath{ defaultCoverPath } , _cache{ core::Service<core::IConfig>::get()->getULong("cover-max-cache-size", 30) * 1000 * 1000 }
, _maxCacheSize{ core::Service<core::IConfig>::get()->getULong("cover-max-cache-size", 30) * 1000 * 1000 }
, _maxFileSize{ core::Service<core::IConfig>::get()->getULong("cover-max-file-size", 10) * 1000 * 1000 } , _maxFileSize{ core::Service<core::IConfig>::get()->getULong("cover-max-file-size", 10) * 1000 * 1000 }
, _preferredFileNames{ constructPreferredFileNames() } , _preferredFileNames{ constructPreferredFileNames() }
, _artistFileNames{ constructArtistFileNames() } , _artistFileNames{ constructArtistFileNames() }
{ {
setJpegQuality(core::Service<core::IConfig>::get()->getULong("cover-jpeg-quality", 75)); setJpegQuality(core::Service<core::IConfig>::get()->getULong("cover-jpeg-quality", 75));
LMS_LOG(COVER, INFO, "Default cover path = '" << _defaultCoverPath.string() << "'"); LMS_LOG(COVER, INFO, "Default cover path = '" << defaultSvgCoverPath.string() << "'");
LMS_LOG(COVER, INFO, "Max cache size = " << _maxCacheSize); LMS_LOG(COVER, INFO, "Max cache size = " << _cache.getMaxCacheSize());
LMS_LOG(COVER, INFO, "Max file size = " << _maxFileSize); LMS_LOG(COVER, INFO, "Max file size = " << _maxFileSize);
LMS_LOG(COVER, INFO, "Preferred file names: " << core::stringUtils::joinStrings(_preferredFileNames, ",")); LMS_LOG(COVER, INFO, "Preferred file names: " << core::stringUtils::joinStrings(_preferredFileNames, ","));
#if LMS_SUPPORT_IMAGE_GM _defaultCover = image::readSvgFile(defaultSvgCoverPath); // may throw
GraphicsMagick::init(execPath);
#else
(void)execPath;
#endif
try
{
getDefault(512);
}
catch (const image::Exception& e)
{
throw core::LmsException("Cannot read default cover file '" + _defaultCoverPath.string() + "': " + e.what());
}
} }
std::unique_ptr<IEncodedImage> CoverService::getFromAvMediaFile(const av::IAudioFile& input, ImageSize width) const std::unique_ptr<IEncodedImage> CoverService::getFromAvMediaFile(const av::IAudioFile& input, ImageSize width) const
@@ -189,27 +174,9 @@ namespace lms::cover
return image; return image;
} }
std::shared_ptr<IEncodedImage> CoverService::getDefault(ImageSize width) std::shared_ptr<IEncodedImage> CoverService::getDefaultSvgCover()
{ {
{ return _defaultCover;
std::shared_lock lock{ _cacheMutex };
if (auto it{ _defaultCoverCache.find(width) }; it != std::cend(_defaultCoverCache))
return it->second;
}
{
std::unique_lock lock{ _cacheMutex };
if (auto it{ _defaultCoverCache.find(width) }; it != std::cend(_defaultCoverCache))
return it->second;
std::shared_ptr<IEncodedImage> image{ getFromCoverFile(_defaultCoverPath, width) };
_defaultCoverCache[width] = image;
LMS_LOG(COVER, DEBUG, "Default cache entries = " << _defaultCoverCache.size());
return image;
}
} }
std::unique_ptr<IEncodedImage> CoverService::getFromDirectory(const std::filesystem::path& directory, ImageSize width, const std::vector<std::string>& preferredFileNames, bool allowPickRandom) const std::unique_ptr<IEncodedImage> CoverService::getFromDirectory(const std::filesystem::path& directory, ImageSize width, const std::vector<std::string>& preferredFileNames, bool allowPickRandom) const
@@ -232,7 +199,7 @@ namespace lms::cover
std::unique_ptr<IEncodedImage> image; std::unique_ptr<IEncodedImage> image;
for (std::string_view filename : preferredFileNames) for (const std::string_view filename : preferredFileNames)
{ {
image = tryLoadImageFromFilename(filename); image = tryLoadImageFromFilename(filename);
if (image) if (image)
@@ -300,7 +267,7 @@ namespace lms::cover
std::error_code ec; std::error_code ec;
std::filesystem::directory_iterator itPath(directoryPath, ec); std::filesystem::directory_iterator itPath(directoryPath, ec);
std::filesystem::directory_iterator itEnd; const std::filesystem::directory_iterator itEnd;
while (!ec && itPath != itEnd) while (!ec && itPath != itEnd)
{ {
const std::filesystem::path& path{ *itPath }; const std::filesystem::path& path{ *itPath };
@@ -339,9 +306,9 @@ namespace lms::cover
{ {
using namespace db; using namespace db;
const CacheEntryDesc cacheEntryDesc{ trackId, width }; const ImageCache::EntryDesc cacheEntryDesc{ trackId, width };
std::shared_ptr<IEncodedImage> cover{ loadFromCache(cacheEntryDesc) }; std::shared_ptr<IEncodedImage> cover{ _cache.getImage(cacheEntryDesc) };
if (cover) if (cover)
return cover; return cover;
@@ -364,7 +331,7 @@ namespace lms::cover
} }
if (cover) if (cover)
saveToCache(cacheEntryDesc, cover); _cache.addImage(cacheEntryDesc, cover);
return cover; return cover;
} }
@@ -372,9 +339,9 @@ namespace lms::cover
std::shared_ptr<IEncodedImage> CoverService::getFromRelease(db::ReleaseId releaseId, ImageSize width) std::shared_ptr<IEncodedImage> CoverService::getFromRelease(db::ReleaseId releaseId, ImageSize width)
{ {
using namespace db; using namespace db;
const CacheEntryDesc cacheEntryDesc{ releaseId, width }; const ImageCache::EntryDesc cacheEntryDesc{ releaseId, width };
std::shared_ptr<IEncodedImage> cover{ loadFromCache(cacheEntryDesc) }; std::shared_ptr<IEncodedImage> cover{ _cache.getImage(cacheEntryDesc) };
if (cover) if (cover)
return cover; return cover;
@@ -399,7 +366,7 @@ namespace lms::cover
const Track::pointer& track{ tracks.results.front() }; const Track::pointer& track{ tracks.results.front() };
res = ReleaseInfo{}; res = ReleaseInfo{};
res->firstTrackId = track->getId(); res->firstTrackId = track->getId();
res->releaseDirectory = track->getPath().parent_path(); res->releaseDirectory = track->getAbsoluteFilePath().parent_path();
} }
return res; return res;
@@ -413,7 +380,7 @@ namespace lms::cover
} }
if (cover) if (cover)
saveToCache(cacheEntryDesc, cover); _cache.addImage(cacheEntryDesc, cover);
return cover; return cover;
} }
@@ -421,9 +388,9 @@ namespace lms::cover
std::shared_ptr<IEncodedImage> CoverService::getFromArtist(db::ArtistId artistId, ImageSize width) std::shared_ptr<IEncodedImage> CoverService::getFromArtist(db::ArtistId artistId, ImageSize width)
{ {
using namespace db; using namespace db;
const CacheEntryDesc cacheEntryDesc{ artistId, width }; const ImageCache::EntryDesc cacheEntryDesc{ artistId, width };
std::shared_ptr<IEncodedImage> artistImage{ loadFromCache(cacheEntryDesc) }; std::shared_ptr<IEncodedImage> artistImage{ _cache.getImage(cacheEntryDesc) };
if (artistImage) if (artistImage)
return artistImage; return artistImage;
@@ -457,9 +424,9 @@ namespace lms::cover
const auto releaseArtists{ Artist::findIds(session, artistFindParams) }; const auto releaseArtists{ Artist::findIds(session, artistFindParams) };
if (releaseArtists.results.size() == 1) if (releaseArtists.results.size() == 1)
releasePaths.insert(track->getPath().parent_path()); releasePaths.insert(track->getAbsoluteFilePath().parent_path());
else else
multiArtistReleasePaths.insert(track->getPath().parent_path()); multiArtistReleasePaths.insert(track->getAbsoluteFilePath().parent_path());
}); });
} }
@@ -513,20 +480,13 @@ namespace lms::cover
} }
if (artistImage) if (artistImage)
saveToCache(cacheEntryDesc, artistImage); _cache.addImage(cacheEntryDesc, artistImage);
return artistImage; return artistImage;
} }
void CoverService::flushCache() void CoverService::flushCache()
{ {
std::unique_lock lock{ _cacheMutex };
LMS_LOG(COVER, DEBUG, "Cache stats: hits = " << _cacheHits << ", misses = " << _cacheMisses << ", nb entries = " << _cache.size() << ", size = " << _cacheSize);
_cacheHits = 0;
_cacheMisses = 0;
_cacheSize = 0;
_cache.clear();
} }
void CoverService::setJpegQuality(unsigned quality) void CoverService::setJpegQuality(unsigned quality)
@@ -536,35 +496,5 @@ namespace lms::cover
LMS_LOG(COVER, INFO, "JPEG export quality = " << _jpegQuality); LMS_LOG(COVER, INFO, "JPEG export quality = " << _jpegQuality);
} }
void CoverService::saveToCache(const CacheEntryDesc& entryDesc, std::shared_ptr<IEncodedImage> image)
{
std::unique_lock lock{ _cacheMutex };
while (_cacheSize + image->getDataSize() > _maxCacheSize && !_cache.empty())
{
auto itRandom{ core::random::pickRandom(_cache) };
_cacheSize -= itRandom->second->getDataSize();
_cache.erase(itRandom);
}
_cacheSize += image->getDataSize();
_cache[entryDesc] = image;
}
std::shared_ptr<IEncodedImage> CoverService::loadFromCache(const CacheEntryDesc& entryDesc)
{
std::shared_lock lock{ _cacheMutex };
auto it{ _cache.find(entryDesc) };
if (it == std::cend(_cache))
{
++_cacheMisses;
return nullptr;
}
++_cacheHits;
return it->second;
}
} // namespace lms::cover } // namespace lms::cover
+7 -57
View File
@@ -19,19 +19,14 @@
#pragma once #pragma once
#include <atomic>
#include <filesystem> #include <filesystem>
#include <map> #include <map>
#include <optional>
#include <shared_mutex>
#include <string_view>
#include <unordered_map>
#include <variant>
#include <vector> #include <vector>
#include "services/cover/ICoverService.hpp" #include "services/cover/ICoverService.hpp"
#include "image/IEncodedImage.hpp" #include "image/IEncodedImage.hpp"
#include "database/Types.hpp" #include "database/Types.hpp"
#include "ImageCache.hpp"
namespace lms::db namespace lms::db
{ {
@@ -43,57 +38,21 @@ namespace lms::av
class IAudioFile; class IAudioFile;
} }
namespace lms::cover
{
struct CacheEntryDesc
{
std::variant<db::ArtistId, db::ReleaseId, db::TrackId> id;
std::size_t size;
bool operator==(const CacheEntryDesc& other) const
{
return id == other.id
&& size == other.size;
}
};
} // ns Cover
namespace std
{
template<>
class hash<lms::cover::CacheEntryDesc>
{
public:
size_t operator()(const lms::cover::CacheEntryDesc& e) const
{
size_t h{};
std::visit([&](auto id)
{
using IdType = std::decay_t<decltype(id)>;
h ^= std::hash<IdType>{}(id);
}, e.id);
h ^= std::hash<std::size_t>{}(e.size) << 1;
return h;
}
};
} // ns std
namespace lms::cover namespace lms::cover
{ {
class CoverService : public ICoverService class CoverService : public ICoverService
{ {
public: public:
CoverService(db::Db& db, const std::filesystem::path& execPath, const std::filesystem::path& defaultCoverPath); CoverService(db::Db& db, const std::filesystem::path& defaultSvgCoverPath);
private:
CoverService(const CoverService&) = delete; CoverService(const CoverService&) = delete;
CoverService& operator=(const CoverService&) = delete; CoverService& operator=(const CoverService&) = delete;
private:
std::shared_ptr<image::IEncodedImage> getFromTrack(db::TrackId trackId, image::ImageSize width) override; std::shared_ptr<image::IEncodedImage> getFromTrack(db::TrackId trackId, image::ImageSize width) override;
std::shared_ptr<image::IEncodedImage> getFromRelease(db::ReleaseId releaseId, image::ImageSize width) override; std::shared_ptr<image::IEncodedImage> getFromRelease(db::ReleaseId releaseId, image::ImageSize width) override;
std::shared_ptr<image::IEncodedImage> getFromArtist(db::ArtistId artistId, image::ImageSize width) override; std::shared_ptr<image::IEncodedImage> getFromArtist(db::ArtistId artistId, image::ImageSize width) override;
std::shared_ptr<image::IEncodedImage> getDefault(image::ImageSize width) override; std::shared_ptr<image::IEncodedImage> getDefaultSvgCover() override;
void flushCache() override; void flushCache() override;
void setJpegQuality(unsigned quality) override; void setJpegQuality(unsigned quality) override;
@@ -106,22 +65,13 @@ namespace lms::cover
std::unique_ptr<image::IEncodedImage> getFromDirectory(const std::filesystem::path& directory, image::ImageSize width, const std::vector<std::string>& preferredFileNames, bool allowPickRandom) const; std::unique_ptr<image::IEncodedImage> getFromDirectory(const std::filesystem::path& directory, image::ImageSize width, const std::vector<std::string>& preferredFileNames, bool allowPickRandom) const;
std::unique_ptr<image::IEncodedImage> getFromSameNamedFile(const std::filesystem::path& filePath, image::ImageSize width) const; std::unique_ptr<image::IEncodedImage> getFromSameNamedFile(const std::filesystem::path& filePath, image::ImageSize width) const;
bool checkCoverFile(const std::filesystem::path& directoryPath) const; bool checkCoverFile(const std::filesystem::path& filePath) const;
db::Db& _db; db::Db& _db;
std::shared_mutex _cacheMutex; ImageCache _cache;
std::unordered_map<CacheEntryDesc, std::shared_ptr<image::IEncodedImage>> _cache; std::shared_ptr<image::IEncodedImage> _defaultCover;
std::unordered_map<image::ImageSize, std::shared_ptr<image::IEncodedImage>> _defaultCoverCache;
std::atomic<std::size_t> _cacheMisses{};
std::atomic<std::size_t> _cacheHits{};
std::size_t _cacheSize{};
void saveToCache(const CacheEntryDesc& entryDesc, std::shared_ptr<image::IEncodedImage> image);
std::shared_ptr<image::IEncodedImage> loadFromCache(const CacheEntryDesc& entryDesc);
const std::filesystem::path _defaultCoverPath;
const std::size_t _maxCacheSize;
static inline const std::vector<std::filesystem::path> _fileExtensions{ ".jpg", ".jpeg", ".png", ".bmp" }; // TODO parametrize static inline const std::vector<std::filesystem::path> _fileExtensions{ ".jpg", ".jpeg", ".png", ".bmp" }; // TODO parametrize
const std::size_t _maxFileSize; const std::size_t _maxFileSize;
const std::vector<std::string> _preferredFileNames; const std::vector<std::string> _preferredFileNames;
@@ -0,0 +1,73 @@
/*
* Copyright (C) 2015 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 "ImageCache.hpp"
#include <mutex>
#include "core/Random.hpp"
#include "core/ILogger.hpp"
namespace lms::cover
{
ImageCache::ImageCache(std::size_t maxCacheSize)
: _maxCacheSize{ maxCacheSize }
{}
void ImageCache::addImage(const EntryDesc& entryDesc, std::shared_ptr<image::IEncodedImage> image)
{
const std::unique_lock lock{ _mutex };
while (_cacheSize + image->getDataSize() > _maxCacheSize && !_cache.empty())
{
auto itRandom{ core::random::pickRandom(_cache) };
_cacheSize -= itRandom->second->getDataSize();
_cache.erase(itRandom);
}
_cacheSize += image->getDataSize();
_cache[entryDesc] = image;
}
std::shared_ptr<image::IEncodedImage> ImageCache::getImage(const EntryDesc& entryDesc) const
{
const std::shared_lock lock{ _mutex };
const auto it{ _cache.find(entryDesc) };
if (it == std::cend(_cache))
{
++_cacheMisses;
return nullptr;
}
++_cacheHits;
return it->second;
}
void ImageCache::flush()
{
const std::unique_lock lock{ _mutex };
LMS_LOG(COVER, DEBUG, "Cache stats: hits = " << _cacheHits.load() << ", misses = " << _cacheMisses.load() << ", nb entries = " << _cache.size() << ", size = " << _cacheSize);
_cacheHits = 0;
_cacheMisses = 0;
_cacheSize = 0;
_cache.clear();
}
}
@@ -0,0 +1,72 @@
/*
* Copyright (C) 2015 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 <atomic>
#include <shared_mutex>
#include <unordered_map>
#include <variant>
#include "database/ArtistId.hpp"
#include "database/ReleaseId.hpp"
#include "database/TrackId.hpp"
#include "image/IEncodedImage.hpp"
namespace lms::cover
{
class ImageCache
{
public:
ImageCache(std::size_t maxCacheSize);
struct EntryDesc
{
using VariantType = std::variant<db::ArtistId, db::ReleaseId, db::TrackId>;
VariantType id;
std::size_t size;
bool operator==(const EntryDesc& other) const = default;
};
std::size_t getMaxCacheSize() const { return _maxCacheSize; }
void addImage(const EntryDesc& entryDesc, std::shared_ptr<image::IEncodedImage> image);
std::shared_ptr<image::IEncodedImage> getImage(const EntryDesc& entryDesc) const;
void flush();
private:
const std::size_t _maxCacheSize;
mutable std::shared_mutex _mutex;
struct EntryHasher
{
std::size_t operator()(const EntryDesc& entry) const
{
return std::hash<EntryDesc::VariantType>{}(entry.id) ^ std::hash<std::size_t>{}(entry.size);
}
};
std::unordered_map<EntryDesc, std::shared_ptr<image::IEncodedImage>, EntryHasher> _cache;
std::size_t _cacheSize{};
mutable std::atomic<std::size_t> _cacheMisses{};
mutable std::atomic<std::size_t> _cacheHits{};
};
}
@@ -43,14 +43,14 @@ namespace lms::cover
virtual std::shared_ptr<image::IEncodedImage> getFromRelease(db::ReleaseId releaseId, image::ImageSize width) = 0; virtual std::shared_ptr<image::IEncodedImage> getFromRelease(db::ReleaseId releaseId, image::ImageSize width) = 0;
virtual std::shared_ptr<image::IEncodedImage> getFromArtist(db::ArtistId artistId, image::ImageSize width) = 0; virtual std::shared_ptr<image::IEncodedImage> getFromArtist(db::ArtistId artistId, image::ImageSize width) = 0;
virtual std::shared_ptr<image::IEncodedImage> getDefault(image::ImageSize width) = 0; virtual std::shared_ptr<image::IEncodedImage> getDefaultSvgCover() = 0;
virtual void flushCache() = 0; virtual void flushCache() = 0;
virtual void setJpegQuality(unsigned quality) = 0; // from 1 to 100 virtual void setJpegQuality(unsigned quality) = 0; // from 1 to 100
}; };
std::unique_ptr<ICoverService> createCoverService(db::Db& db, const std::filesystem::path& execPath, const std::filesystem::path& defaultCoverPath); std::unique_ptr<ICoverService> createCoverService(db::Db& db, const std::filesystem::path& defaultSvgCoverPath);
} // namespace lms::coverArt } // namespace lms::coverArt
+2
View File
@@ -3,8 +3,10 @@ add_library(lmsscanner SHARED
impl/ScannerService.cpp impl/ScannerService.cpp
impl/ScannerStats.cpp impl/ScannerStats.cpp
impl/ScanStepCheckDuplicatedDbFiles.cpp impl/ScanStepCheckDuplicatedDbFiles.cpp
impl/ScanStepCompact.cpp
impl/ScanStepComputeClusterStats.cpp impl/ScanStepComputeClusterStats.cpp
impl/ScanStepDiscoverFiles.cpp impl/ScanStepDiscoverFiles.cpp
impl/ScanStepOptimize.cpp
impl/ScanStepRemoveOrphanDbFiles.cpp impl/ScanStepRemoveOrphanDbFiles.cpp
impl/ScanStepScanFiles.cpp impl/ScanStepScanFiles.cpp
) )
+4 -3
View File
@@ -19,9 +19,10 @@
#pragma once #pragma once
#include <string_view>
#include <vector> #include <vector>
#include "core/LiteralString.hpp"
#include "services/scanner/ScannerOptions.hpp"
#include "services/scanner/ScannerStats.hpp" #include "services/scanner/ScannerStats.hpp"
namespace lms::scanner namespace lms::scanner
@@ -32,11 +33,11 @@ namespace lms::scanner
virtual ~IScanStep() = default; virtual ~IScanStep() = default;
virtual ScanStep getStep() const = 0; virtual ScanStep getStep() const = 0;
virtual std::string_view getStepName() const = 0; virtual core::LiteralString getStepName() const = 0;
struct ScanContext struct ScanContext
{ {
const bool forceScan; ScanOptions scanOptions;
ScanStats stats; ScanStats stats;
ScanStepStats currentStepStats; ScanStepStats currentStepStats;
}; };
@@ -45,7 +45,7 @@ namespace lms::scanner
const Track::pointer track{ Track::find(session, trackId) }; const Track::pointer track{ Track::find(session, trackId) };
if (auto trackMBID{ track->getTrackMBID() }) if (auto trackMBID{ track->getTrackMBID() })
{ {
LMS_LOG(DBUPDATER, INFO, "Found duplicated track MBID [" << trackMBID->getAsString() << "], file: " << track->getPath().string() << " - " << track->getName()); LMS_LOG(DBUPDATER, INFO, "Found duplicated track MBID [" << trackMBID->getAsString() << "], file: " << track->getAbsoluteFilePath().string() << " - " << track->getName());
context.stats.duplicates.emplace_back(ScanDuplicate{ track->getId(), DuplicateReason::SameTrackMBID }); context.stats.duplicates.emplace_back(ScanDuplicate{ track->getId(), DuplicateReason::SameTrackMBID });
context.currentStepStats.processedElems++; context.currentStepStats.processedElems++;
_progressCallback(context.currentStepStats); _progressCallback(context.currentStepStats);
@@ -29,8 +29,8 @@ namespace lms::scanner
using ScanStepBase::ScanStepBase; using ScanStepBase::ScanStepBase;
private: private:
std::string_view getStepName() const override { return "Checking for duplicated files"; } core::LiteralString getStepName() const override { return "Check for duplicated files"; }
ScanStep getStep() const override { return ScanStep::CheckingForDuplicateFiles; } ScanStep getStep() const override { return ScanStep::CheckForDuplicateFiles; }
void process(ScanContext& context) override; void process(ScanContext& context) override;
}; };
} }
@@ -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/>.
*/
#include "ScanStepCompact.hpp"
#include "database/Db.hpp"
#include "database/Session.hpp"
namespace lms::scanner
{
void ScanStepCompact::process(ScanContext& context)
{
// Don't auto compact as it may be too annoying to block the whole application
if (context.scanOptions.compact)
_db.getTLSSession().vacuum();
}
}
@@ -0,0 +1,36 @@
/*
* 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 "ScanStepBase.hpp"
namespace lms::scanner
{
class ScanStepCompact : public ScanStepBase
{
public:
using ScanStepBase::ScanStepBase;
private:
ScanStep getStep() const override { return ScanStep::Compact; }
core::LiteralString getStepName() const override { return "Compact"; }
void process(ScanContext& context) override;
};
}
@@ -79,6 +79,7 @@ namespace lms::scanner
} }
context.currentStepStats.processedElems++; context.currentStepStats.processedElems++;
_progressCallback(context.currentStepStats);
} }
return true; return true;
@@ -30,7 +30,7 @@ namespace lms::scanner
private: private:
ScanStep getStep() const override { return ScanStep::ComputeClusterStats; } ScanStep getStep() const override { return ScanStep::ComputeClusterStats; }
std::string_view getStepName() const override { return "Compute cluster stats"; } core::LiteralString getStepName() const override { return "Compute cluster stats"; }
void process(ScanContext& context) override; void process(ScanContext& context) override;
}; };
} }
@@ -29,8 +29,8 @@ namespace lms::scanner
using ScanStepBase::ScanStepBase; using ScanStepBase::ScanStepBase;
private: private:
ScanStep getStep() const override { return ScanStep::DiscoveringFiles; } ScanStep getStep() const override { return ScanStep::DiscoverFiles; }
std::string_view getStepName() const override { return "DiscoveringFiles"; } core::LiteralString getStepName() const override { return "Discover files"; }
void process(ScanContext& context) override; void process(ScanContext& context) override;
}; };
} }

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