Merge branch 'develop' for release v3.52.0
This commit is contained in:
@@ -12,4 +12,6 @@ CheckOptions:
|
||||
value: '1'
|
||||
- key: cppcoreguidelines-avoid-do-while.IgnoreMacros
|
||||
value: '1'
|
||||
- key: performance-unnecessary-value-param.AllowedTypes
|
||||
value: "shared_ptr"
|
||||
...
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ endif()
|
||||
find_package(PkgConfig REQUIRED)
|
||||
find_package(Threads 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)
|
||||
pkg_check_modules(Taglib REQUIRED IMPORTED_TARGET taglib)
|
||||
pkg_check_modules(Config++ REQUIRED IMPORTED_TARGET libconfig++)
|
||||
|
||||
+4
-1
@@ -31,6 +31,7 @@ ARG BUILD_PACKAGES=" \
|
||||
libarchive-dev \
|
||||
libconfig-dev \
|
||||
taglib-dev \
|
||||
sqlite-dev \
|
||||
gtest-dev"
|
||||
|
||||
RUN apk add --no-cache --update ${BUILD_PACKAGES}
|
||||
@@ -86,7 +87,7 @@ RUN \
|
||||
|
||||
RUN \
|
||||
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 install
|
||||
|
||||
@@ -147,11 +148,13 @@ ARG RUNTIME_PACKAGES=" \
|
||||
lame-libs \
|
||||
zlib \
|
||||
boost-filesystem \
|
||||
boost-iostreams \
|
||||
boost-program_options \
|
||||
boost-system \
|
||||
boost-thread \
|
||||
libarchive \
|
||||
libconfig++ \
|
||||
sqlite-libs \
|
||||
taglib"
|
||||
|
||||
ARG LMS_USER=lms
|
||||
|
||||
@@ -11,6 +11,7 @@ OpenSubsonic is an initiative to patch and extend the legacy Subsonic API. You'l
|
||||
## Extra fields
|
||||
The following extra fields are implemented:
|
||||
* `Album` response:
|
||||
* `mediaType`
|
||||
* `played`
|
||||
* `musicBrainzId`
|
||||
* `genres`
|
||||
@@ -22,6 +23,10 @@ The following extra fields are implemented:
|
||||
* `isCompilation`
|
||||
* `discTitles`: discs with no subtitle are omitted
|
||||
* `Child` response:
|
||||
* `bitDepth`
|
||||
* `samplingRate`
|
||||
* `channelCount`
|
||||
* `mediaType`
|
||||
* `played`
|
||||
* `musicBrainzId`: note this is actually the recording MBID when this response refers to a song
|
||||
* `genres`
|
||||
@@ -33,6 +38,7 @@ The following extra fields are implemented:
|
||||
* `moods`
|
||||
* `replayGain`
|
||||
* `Artist` response:
|
||||
* `mediaType`
|
||||
* `musicBrainzId`
|
||||
* `sortName`
|
||||
* `roles`
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
<messages xmlns:if="Wt.WTemplate.conditions">
|
||||
|
||||
<message id="Lms.Admin.ScannerController.template">
|
||||
<form>
|
||||
<legend>${tr:Lms.Admin.ScannerController.scanner}</legend>
|
||||
<div class="row g-3">
|
||||
<div class="col-lg-6">
|
||||
@@ -18,7 +17,7 @@
|
||||
${step-status class="form-control"}
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="{id:last-scan}">
|
||||
<label class="form-label" for="${id:last-scan}">
|
||||
${tr:Lms.Admin.ScannerController.last-scan}
|
||||
</label>
|
||||
<div class="input-group">
|
||||
@@ -27,17 +26,39 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<div class="btn-group">
|
||||
${scan-btn class="btn btn-primary"}
|
||||
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li>${full-scan-btn class="dropdown-item"}</li>
|
||||
</ul>
|
||||
<div class="collapse" id="scanOptions">
|
||||
<div class="card bg-dark">
|
||||
<div class="card-header">
|
||||
${tr:Lms.Admin.ScannerController.scan-options}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="form-check mb-1">
|
||||
${full-scan class="form-check-input"}
|
||||
<label class="form-check-label" for="${id:full-scan}">
|
||||
${tr:Lms.Admin.ScannerController.full-scan}
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check mb-1">
|
||||
${force-optimize class="form-check-input"}
|
||||
<label class="form-check-label" for="${id:force-optimize}">
|
||||
${tr:Lms.Admin.ScannerController.force-optimize}
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-check">
|
||||
${compact class="form-check-input"}
|
||||
<label class="form-check-label" for="${id:compact}">
|
||||
${tr:Lms.Admin.ScannerController.compact}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
</messages>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<legend>${title}</legend>
|
||||
${<if-has-last-login>}
|
||||
<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}
|
||||
</label>
|
||||
<input type="text" readonly="readonly" class="form-control" id="status" value="${last-login}"/>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB |
Executable
+11
@@ -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 |
@@ -89,9 +89,11 @@
|
||||
<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-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.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.last-scan">Last scan</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-mbid">Duplicated track MBID</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.status">Status</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.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-compact">Compacting database...</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-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-scanning-files">Scanning files: {1}/{2} files ({3}%)...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-status">Step status</message>
|
||||
|
||||
@@ -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.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.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.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.last-scan">Dernier scan</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-mbid">Track MBID dupliqué</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.status">Statut</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.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-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-discovering-files">Découverte des fichiers : {1} fichiers</message>
|
||||
<message id="Lms.Admin.ScannerController.step-fetching-track-features">Récupération des métadonnées AcousticBrainz : {1}/{2} fichiers ({3}%)...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-optimize">Optimisation de la base de données... {1}/{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-scanning-files">Scan des fichiers : {1}/{2} fichiers ({3}%)...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-status">Statut de l'étape</message>
|
||||
|
||||
@@ -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.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.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.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.last-scan">Ultima scansione</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-mbid">Track MBID doppio</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.status">Stato</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-discovering-files">File trovati: {1} files</message>
|
||||
<message id="Lms.Admin.ScannerController.step-fetching-track-features">Recupero metadati da AcousticBrainz: {1}/{2} tracce ({3}%)...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-optimize">Ottimizzazione del database... {1}/{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-scanning-files">Scansione files: {1}/{2} files ({3}%)...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-status">Stato passo</message>
|
||||
|
||||
@@ -89,9 +89,11 @@
|
||||
<message id="Lms.Admin.ScannerController.bad-duration">无法获得音轨时间</message>
|
||||
<message id="Lms.Admin.ScannerController.cannot-parse-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.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.last-scan">最后扫描</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-mbid">相同 MBID</message>
|
||||
<message id="Lms.Admin.ScannerController.scan-now">立即扫描</message>
|
||||
|
||||
<message id="Lms.Admin.ScannerController.scanner">扫描器</message>
|
||||
<message id="Lms.Admin.ScannerController.status">状态</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-discovering-files">检索文件中: {1} 文件</message>
|
||||
<message id="Lms.Admin.ScannerController.step-fetching-track-features">从 AcousticBrainz 获取音轨特征: {1}/{2} 音轨 ({3}%)...</message>
|
||||
|
||||
<message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">重载相似引擎中 {1}%...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-scanning-files">扫描文件中: {1}/{2} 个文件 ({3}%)...</message>
|
||||
<message id="Lms.Admin.ScannerController.step-status">当前步骤状态</message>
|
||||
|
||||
Vendored
+8
-8
File diff suppressed because one or more lines are too long
+3
-3
File diff suppressed because one or more lines are too long
@@ -97,17 +97,21 @@ LMS.mediaplayer = function () {
|
||||
_lastStartPlaying = Date.now();
|
||||
}
|
||||
|
||||
let _stopTimer = function() {
|
||||
let _pauseTimer = function() {
|
||||
if (_lastStartPlaying != null) {
|
||||
_playedDuration += Date.now() - _lastStartPlaying;
|
||||
_lastStartPlaying = null;
|
||||
}
|
||||
}
|
||||
|
||||
let _resetTimer = function() {
|
||||
if (_lastStartPlaying != null)
|
||||
_pauseTimer();
|
||||
|
||||
if (_playedDuration > 0) {
|
||||
Wt.emit(_root, "scrobbleListenFinished", _trackId, _playedDuration);
|
||||
_playedDuration = 0;
|
||||
_lastStartPlaying = null;
|
||||
}
|
||||
}
|
||||
|
||||
let _durationToString = function (duration) {
|
||||
@@ -229,7 +233,7 @@ LMS.mediaplayer = function () {
|
||||
navigator.mediaSession.setPositionState({
|
||||
duration: _duration,
|
||||
playbackRate: 1,
|
||||
position: _offset + _elems.audio.currentTime,
|
||||
position: Math.min(_offset + _elems.audio.currentTime, _duration),
|
||||
});
|
||||
|
||||
if (_elems.audio.paused)
|
||||
@@ -276,9 +280,9 @@ LMS.mediaplayer = function () {
|
||||
_elems.audio.addEventListener("playing", _updateMediaSessionState);
|
||||
_elems.audio.addEventListener("pause", _updateMediaSessionState);
|
||||
|
||||
_elems.audio.addEventListener("pause", _stopTimer);
|
||||
_elems.audio.addEventListener("pause", _pauseTimer);
|
||||
_elems.audio.addEventListener("playing", _startTimer);
|
||||
_elems.audio.addEventListener("waiting", _stopTimer);
|
||||
_elems.audio.addEventListener("waiting", _pauseTimer);
|
||||
|
||||
_elems.audio.addEventListener("timeupdate", function() {
|
||||
_elems.progress.style.width = "" + ((_offset + _elems.audio.currentTime) / _duration) * 100 + "%";
|
||||
@@ -286,6 +290,7 @@ LMS.mediaplayer = function () {
|
||||
});
|
||||
|
||||
_elems.audio.addEventListener("ended", function() {
|
||||
_resetTimer();
|
||||
Wt.emit(_root, "playbackEnded");
|
||||
});
|
||||
|
||||
@@ -366,7 +371,6 @@ LMS.mediaplayer = function () {
|
||||
}
|
||||
|
||||
let loadTrack = function(params, autoplay) {
|
||||
_stopTimer();
|
||||
_resetTimer();
|
||||
|
||||
_trackId = params.trackId;
|
||||
|
||||
@@ -281,9 +281,16 @@ namespace lms::av
|
||||
res.emplace();
|
||||
res->index = streamIndex;
|
||||
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->codecName = ::avcodec_get_name(avstream->codecpar->codec_id);
|
||||
assert(!res->codecName.empty()); // doc says it is never NULL
|
||||
res->sampleRate = static_cast<std::size_t>(avstream->codecpar->sample_rate);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -75,6 +75,9 @@ namespace lms::av
|
||||
{
|
||||
size_t index{};
|
||||
std::size_t bitrate{};
|
||||
std::size_t bitsPerSample{};
|
||||
std::size_t channelCount{};
|
||||
std::size_t sampleRate{};
|
||||
DecodingCodec codec;
|
||||
std::string codecName;
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
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_withArg)->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();
|
||||
@@ -25,8 +25,7 @@
|
||||
|
||||
namespace lms::core
|
||||
{
|
||||
std::unique_ptr<IChildProcessManager>
|
||||
createChildProcessManager(boost::asio::io_context& ioContext)
|
||||
std::unique_ptr<IChildProcessManager> createChildProcessManager(boost::asio::io_context& ioContext)
|
||||
{
|
||||
return std::make_unique<ChildProcessManager>(ioContext);
|
||||
}
|
||||
@@ -36,8 +35,7 @@ namespace lms::core
|
||||
{
|
||||
}
|
||||
|
||||
std::unique_ptr<IChildProcess>
|
||||
ChildProcessManager::spawnChildProcess(const std::filesystem::path& path, const IChildProcess::Args& args)
|
||||
std::unique_ptr<IChildProcess> ChildProcessManager::spawnChildProcess(const std::filesystem::path& path, const IChildProcess::Args& args)
|
||||
{
|
||||
return std::make_unique<ChildProcess>(_ioContext, path, args);
|
||||
}
|
||||
|
||||
@@ -68,7 +68,16 @@ namespace lms::core::tracing
|
||||
for (Buffer& buffer : _buffers)
|
||||
_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
|
||||
@@ -81,7 +90,12 @@ namespace lms::core::tracing
|
||||
if (!_currentBuffer)
|
||||
_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
|
||||
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)
|
||||
buffer->currentDurationIndex = 0;
|
||||
buffer->threadId = std::this_thread::get_id();
|
||||
return buffer;
|
||||
}
|
||||
|
||||
@@ -141,7 +156,7 @@ namespace lms::core::tracing
|
||||
os << "\t\t{ ";
|
||||
os << "\"name\" : \"thread_name\", ";
|
||||
os << "\"pid\" : 1, ";
|
||||
os << "\"tid\" : " << threadId << ", ";
|
||||
os << "\"tid\" : " << toTraceThreadId(threadId) << ", ";
|
||||
os << "\"ph\" : \"M\", ";
|
||||
os << "\"args\" : { \"name\" : \"" + threadName + "\" }";
|
||||
os << " }";
|
||||
@@ -154,13 +169,15 @@ namespace lms::core::tracing
|
||||
|
||||
for (Buffer& buffer : _buffers)
|
||||
{
|
||||
const auto threadId{ toTraceThreadId(buffer.threadId) };
|
||||
|
||||
for (std::size_t i{}; i < buffer.currentDurationIndex; ++i)
|
||||
{
|
||||
// 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
|
||||
using clockMicro = std::chrono::duration<double, std::micro>;
|
||||
|
||||
const CompleteEvent& event{ buffer.durationEvents[i] };
|
||||
const CompleteEventEntry& event{ buffer.durationEvents[i] };
|
||||
|
||||
if (first)
|
||||
first = false;
|
||||
@@ -168,13 +185,26 @@ namespace lms::core::tracing
|
||||
os << ", " << std::endl;;
|
||||
|
||||
os << "\t\t{ ";
|
||||
os << "\"name\" : \"" << event.name.c_str() << "\", ";
|
||||
os << "\"cat\" : \"" << event.category.c_str() << "\", ";
|
||||
os << "\"name\" : \"" << event.name << "\", ";
|
||||
os << "\"cat\" : \"" << event.category << "\", ";
|
||||
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 << "\"dur\" : " << std::fixed << std::setprecision(3) << std::chrono::duration_cast<clockMicro>(event.duration).count() << ", ";
|
||||
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 << " }";
|
||||
}
|
||||
}
|
||||
@@ -182,14 +212,16 @@ namespace lms::core::tracing
|
||||
|
||||
os << std::endl;
|
||||
os << "\t]," << std::endl;
|
||||
os << "\t\"meta_cpu_count\" : " << std::thread::hardware_concurrency() << ", " << std::endl;
|
||||
os << "\t\"meta_build_type\" : ";
|
||||
#ifndef NDEBUG
|
||||
os << "\"debug\"";
|
||||
#else
|
||||
os << "\"release\"";
|
||||
#endif
|
||||
os << std::endl;
|
||||
{
|
||||
std::scoped_lock lock{ _metadataMutex };
|
||||
for (const auto& [metadata, value] : _metadata)
|
||||
{
|
||||
os << "\t\"meta_" << metadata << "\": \"";
|
||||
stringUtils::writeJsonEscapedString(os, value);
|
||||
os << "\"," << std::endl;
|
||||
}
|
||||
}
|
||||
os << "\t\"meta_registered_arg_count\" : " << getRegisteredArgCount() << std::endl;
|
||||
os << "}" << std::endl;
|
||||
}
|
||||
|
||||
@@ -198,4 +230,71 @@ namespace lms::core::tracing
|
||||
std::scoped_lock lock{ _threadNameMutex };
|
||||
_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);
|
||||
}
|
||||
}
|
||||
@@ -21,10 +21,12 @@
|
||||
|
||||
#include <array>
|
||||
#include <deque>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
#include <shared_mutex>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "core/ITraceLogger.hpp"
|
||||
|
||||
@@ -42,14 +44,33 @@ namespace lms::core::tracing
|
||||
void write(const CompleteEvent& event) override;
|
||||
void dumpCurrentBuffer(std::ostream& os) 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
|
||||
{
|
||||
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{};
|
||||
};
|
||||
|
||||
@@ -62,9 +83,21 @@ namespace lms::core::tracing
|
||||
|
||||
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::unordered_map<std::thread::id, std::string> _threadNames;
|
||||
|
||||
std::mutex _metadataMutex;
|
||||
std::map<std::string, std::string> _metadata;
|
||||
|
||||
std::mutex _mutex;
|
||||
std::deque<Buffer*> _freeBuffers;
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <ostream>
|
||||
#include <string_view>
|
||||
#include <thread>
|
||||
@@ -34,14 +35,19 @@
|
||||
#define LMS_CONCAT(x, y) LMS_CONCAT_IMPL(x, y)
|
||||
|
||||
#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
|
||||
#define LMS_SCOPED_TRACE(CATEGORY, LEVEL, NAME) (void)0
|
||||
#endif
|
||||
|
||||
#define LMS_SCOPED_TRACE_OVERVIEW(CATEGORY, NAME) LMS_SCOPED_TRACE(CATEGORY, ::lms::core::tracing::Level::Overview, NAME)
|
||||
#define LMS_SCOPED_TRACE_DETAILED(CATEGORY, NAME) LMS_SCOPED_TRACE(CATEGORY, ::lms::core::tracing::Level::Detailed, 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_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
|
||||
{
|
||||
@@ -56,13 +62,15 @@ namespace lms::core::tracing
|
||||
class ITraceLogger
|
||||
{
|
||||
public:
|
||||
using ArgHashType = std::size_t;
|
||||
|
||||
struct CompleteEvent
|
||||
{
|
||||
clock::time_point start;
|
||||
clock::duration duration;
|
||||
std::thread::id threadId;
|
||||
LiteralString name;
|
||||
LiteralString category;
|
||||
std::optional<ArgHashType> arg;
|
||||
};
|
||||
|
||||
virtual ~ITraceLogger() = default;
|
||||
@@ -71,6 +79,9 @@ namespace lms::core::tracing
|
||||
virtual void write(const CompleteEvent& entry) = 0;
|
||||
virtual void dumpCurrentBuffer(std::ostream& os) = 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;
|
||||
@@ -79,16 +90,17 @@ namespace lms::core::tracing
|
||||
class ScopedTrace
|
||||
{
|
||||
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))
|
||||
{
|
||||
_traceLogger = traceLogger;
|
||||
|
||||
_event.start = clock::now();
|
||||
_event.threadId = std::this_thread::get_id();
|
||||
_event.name = name;
|
||||
_event.category = category;
|
||||
if (!argType.empty() && !argValue.empty())
|
||||
_event.arg = traceLogger->registerArg(argType, argValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <ostream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
@@ -32,6 +33,7 @@ namespace lms::core
|
||||
template<std::size_t N>
|
||||
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 std::size_t length() const noexcept { return _str.length(); }
|
||||
constexpr std::string_view str() const noexcept { return _str; }
|
||||
@@ -40,6 +42,12 @@ namespace lms::core
|
||||
private:
|
||||
std::string_view _str;
|
||||
};
|
||||
|
||||
inline std::ostream& operator<<(std::ostream& os, const LiteralString& str)
|
||||
{
|
||||
os << str.str();
|
||||
return os;
|
||||
}
|
||||
}
|
||||
|
||||
namespace std
|
||||
|
||||
@@ -35,8 +35,8 @@ namespace lms::core::tracing::tests
|
||||
{
|
||||
threads.emplace_back([&]
|
||||
{
|
||||
ScopedTrace loggedEvent{ "MyCategory", Level::Overview, "MyEventLogged", traceLogger.get() };
|
||||
ScopedTrace notLoggedEvent{ "MyCategory", Level::Detailed, "MyEventNotLogged", traceLogger.get() };
|
||||
ScopedTrace loggedEvent{ "MyCategory", Level::Overview, "MyEventLogged", "SomeArgType", "SomeArg", traceLogger.get() };
|
||||
ScopedTrace notLoggedEvent{ "MyNotLoggedCategory", Level::Detailed, "MyEventNotLogged", "SomeNotLoggedArgType", "SomeNotLoggedArg", traceLogger.get() };
|
||||
});
|
||||
}
|
||||
|
||||
@@ -47,6 +47,13 @@ namespace lms::core::tracing::tests
|
||||
traceLogger->dumpCurrentBuffer(oss);
|
||||
|
||||
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("MyNotLoggedCategory"), std::string::npos);
|
||||
EXPECT_EQ(oss.str().find("SomeNotLoggedArgType"), std::string::npos);
|
||||
EXPECT_EQ(oss.str().find("SomeNotLoggedArg"), std::string::npos);
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,8 @@ namespace lms::db
|
||||
{
|
||||
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
|
||||
|| params.writtenAfter.isValid()
|
||||
|| params.linkType
|
||||
@@ -49,28 +50,41 @@ namespace lms::db
|
||||
|| params.clusters.size() == 1
|
||||
|| 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");
|
||||
}
|
||||
|
||||
if (params.linkType)
|
||||
query.where("t_a_l.type = ?").bind(*params.linkType);
|
||||
if (params.sortMethod == ArtistSortMethod::LastWritten
|
||||
|| 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())
|
||||
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())
|
||||
{
|
||||
std::vector<std::string> clauses;
|
||||
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 "'");
|
||||
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 "'");
|
||||
query.bind("%" + utils::escapeLikeKeyword(keyword) + "%");
|
||||
@@ -90,48 +104,42 @@ namespace lms::db
|
||||
|
||||
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());
|
||||
}
|
||||
else if (params.clusters.size() > 1)
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "a.id IN (SELECT DISTINCT a.id FROM artist a"
|
||||
" INNER JOIN track t ON t.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";
|
||||
oss << "a.id IN (SELECT DISTINCT t_a_l.artist_id FROM track_artist_link t_a_l"
|
||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t_a_l.track_id";
|
||||
|
||||
WhereClause clusterClause;
|
||||
for (const ClusterId clusterId : params.clusters)
|
||||
{
|
||||
clusterClause.Or(WhereClause("c.id = ?"));
|
||||
clusterClause.Or(WhereClause("t_c.cluster_id = ?"));
|
||||
query.bind(clusterId);
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
if (params.track.isValid())
|
||||
query.where("t.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);
|
||||
query.where("t_a_l.track_id = ?").bind(params.track);
|
||||
|
||||
switch (params.sortMethod)
|
||||
{
|
||||
case ArtistSortMethod::None:
|
||||
break;
|
||||
case ArtistSortMethod::ByName:
|
||||
case ArtistSortMethod::Id:
|
||||
query.orderBy("a.id");
|
||||
break;
|
||||
case ArtistSortMethod::Name:
|
||||
query.orderBy("a.name COLLATE NOCASE");
|
||||
break;
|
||||
case ArtistSortMethod::BySortName:
|
||||
case ArtistSortMethod::SortName:
|
||||
query.orderBy("a.sort_name COLLATE NOCASE");
|
||||
break;
|
||||
case ArtistSortMethod::Random:
|
||||
@@ -146,6 +154,8 @@ namespace lms::db
|
||||
break;
|
||||
}
|
||||
|
||||
query.groupBy("a.id");
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
@@ -174,51 +184,70 @@ namespace lms::db
|
||||
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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 })
|
||||
.orderBy("LENGTH(mbid) DESC"); // put mbid entries first
|
||||
|
||||
return std::vector<Artist::pointer>(res.begin(), res.end());
|
||||
.orderBy("LENGTH(mbid) DESC")); // put mbid entries first
|
||||
}
|
||||
|
||||
Artist::pointer Artist::find(Session& session, const core::UUID& mbid)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)") };
|
||||
return utils::execQuery<ArtistId>(query, range);
|
||||
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::execRangeQuery<ArtistId>(query, range);
|
||||
}
|
||||
|
||||
RangeResults<ArtistId> Artist::findIds(Session& session, const FindParameters& params)
|
||||
@@ -226,7 +255,7 @@ namespace lms::db
|
||||
session.checkReadTransaction();
|
||||
|
||||
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)
|
||||
@@ -234,7 +263,7 @@ namespace lms::db
|
||||
session.checkReadTransaction();
|
||||
|
||||
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)
|
||||
@@ -242,7 +271,7 @@ namespace lms::db
|
||||
session.checkReadTransaction();
|
||||
|
||||
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
|
||||
@@ -286,10 +315,10 @@ namespace lms::db
|
||||
.groupBy("a.id")
|
||||
.orderBy("COUNT(*) DESC, RANDOM()") };
|
||||
|
||||
for (TrackArtistLinkType type : artistLinkTypes)
|
||||
for (const TrackArtistLinkType type : artistLinkTypes)
|
||||
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
|
||||
@@ -304,7 +333,7 @@ namespace lms::db
|
||||
where.And(WhereClause("a.id = ?")).bind(getId().toString());
|
||||
{
|
||||
WhereClause clusterClause;
|
||||
for (ClusterTypeId clusterTypeId : clusterTypeIds)
|
||||
for (const ClusterTypeId clusterTypeId : clusterTypeIds)
|
||||
clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterTypeId.toString());
|
||||
|
||||
where.And(clusterClause);
|
||||
@@ -312,19 +341,17 @@ namespace lms::db
|
||||
oss << " " << where.get();
|
||||
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())
|
||||
query.bind(bindArg);
|
||||
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Cluster>> queryRes = query;
|
||||
|
||||
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)
|
||||
clustersByType[cluster->getType()->getId()].push_back(cluster);
|
||||
}
|
||||
});
|
||||
|
||||
std::vector<std::vector<Cluster::pointer>> res;
|
||||
for (const auto& [clusterTypeId, clusters] : clustersByType)
|
||||
|
||||
@@ -24,38 +24,33 @@
|
||||
#include "database/User.hpp"
|
||||
#include "StringViewTraits.hpp"
|
||||
#include "IdTypeTraits.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
|
||||
AuthToken::AuthToken(std::string_view value, const Wt::WDateTime& expiry, ObjectPtr<User> user)
|
||||
: _value {value}
|
||||
, _expiry {expiry}
|
||||
, _user {getDboPtr(user)}
|
||||
: _value{ value }
|
||||
, _expiry{ expiry }
|
||||
, _user{ getDboPtr(user) }
|
||||
{
|
||||
}
|
||||
|
||||
AuthToken::pointer
|
||||
AuthToken::create(Session& session, 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)
|
||||
{
|
||||
return session.getDboSession().add(std::unique_ptr<AuthToken> {new AuthToken {value, expiry, user}});
|
||||
return session.getDboSession()->add(std::unique_ptr<AuthToken> {new AuthToken{ value, expiry, user }});
|
||||
}
|
||||
|
||||
void
|
||||
AuthToken::removeExpiredTokens(Session& session, const Wt::WDateTime& now)
|
||||
void AuthToken::removeExpiredTokens(Session& session, const Wt::WDateTime& now)
|
||||
{
|
||||
session.checkWriteTransaction();
|
||||
|
||||
session.getDboSession().execute("DELETE FROM auth_token WHERE expiry < ?").bind(now);
|
||||
session.getDboSession()->execute("DELETE FROM auth_token WHERE expiry < ?").bind(now);
|
||||
}
|
||||
|
||||
AuthToken::pointer
|
||||
AuthToken::find(Session& session, std::string_view value)
|
||||
AuthToken::pointer AuthToken::find(Session& session, std::string_view value)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().find<AuthToken>()
|
||||
.where("value = ?").bind(value)
|
||||
.resultValue();
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->find<AuthToken>().where("value = ?").bind(value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "IdTypeTraits.hpp"
|
||||
#include "StringViewTraits.hpp"
|
||||
#include "SqlQuery.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
@@ -38,7 +39,8 @@ namespace lms::db
|
||||
{
|
||||
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())
|
||||
{
|
||||
@@ -55,7 +57,7 @@ namespace lms::db
|
||||
|
||||
assert(!params.clusterType.isValid() || params.clusterTypeName.empty());
|
||||
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())
|
||||
query.where("c_t.name = ?").bind(params.clusterTypeName);
|
||||
|
||||
@@ -86,14 +88,14 @@ namespace lms::db
|
||||
|
||||
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)
|
||||
{
|
||||
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)
|
||||
@@ -101,7 +103,7 @@ namespace lms::db
|
||||
session.checkReadTransaction();
|
||||
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)
|
||||
@@ -109,38 +111,46 @@ namespace lms::db
|
||||
session.checkReadTransaction();
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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")
|
||||
.where("t_c.cluster_id = ?").bind(id).resultValue();
|
||||
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));
|
||||
}
|
||||
|
||||
std::size_t Cluster::computeReleaseCount(Session& session, ClusterId id)
|
||||
{
|
||||
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")
|
||||
.where("t_c.cluster_id = ?").bind(id).resultValue();
|
||||
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));
|
||||
}
|
||||
|
||||
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")
|
||||
.where("c.id = ?").bind(getId()) };
|
||||
|
||||
return utils::execQuery<TrackId>(query, range);
|
||||
return utils::execRangeQuery<TrackId>(query, range);
|
||||
}
|
||||
|
||||
ClusterType::ClusterType(std::string_view name)
|
||||
@@ -165,61 +175,60 @@ namespace lms::db
|
||||
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession().query<ClusterTypeId>(
|
||||
auto query{ session.getDboSession()->query<ClusterTypeId>(
|
||||
"SELECT c_t.id from cluster_type c_t"
|
||||
" LEFT OUTER JOIN cluster c ON c_t.id = c.cluster_type_id")
|
||||
.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)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession().query<ClusterTypeId>(
|
||||
auto query{ session.getDboSession()->query<ClusterTypeId>(
|
||||
"SELECT DISTINCT c_t.id from cluster_type c_t")
|
||||
.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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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
|
||||
@@ -227,9 +236,9 @@ namespace lms::db
|
||||
assert(self());
|
||||
assert(session());
|
||||
|
||||
return session()->find<Cluster>()
|
||||
return utils::fetchQuerySingleResult(session()->find<Cluster>()
|
||||
.where("name = ?").bind(name)
|
||||
.where("cluster_type_id = ?").bind(getId()).resultValue();
|
||||
.where("cluster_type_id = ?").bind(getId()));
|
||||
}
|
||||
|
||||
std::vector<Cluster::pointer> ClusterType::getClusters() const
|
||||
@@ -237,11 +246,8 @@ namespace lms::db
|
||||
assert(self());
|
||||
assert(session());
|
||||
|
||||
auto res = session()->find<Cluster>()
|
||||
return utils::fetchQueryResults<Cluster::pointer>(session()->find<Cluster>()
|
||||
.where("cluster_type_id = ?").bind(getId())
|
||||
.orderBy("name")
|
||||
.resultList();
|
||||
|
||||
return std::vector<Cluster::pointer>(res.begin(), res.end());
|
||||
.orderBy("name"));
|
||||
}
|
||||
} // namespace lms::db
|
||||
|
||||
@@ -25,8 +25,9 @@
|
||||
#include "database/Session.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "core/IConfig.hpp"
|
||||
#include "core/Service.hpp"
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/ITraceLogger.hpp"
|
||||
#include "core/Service.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
@@ -49,7 +50,7 @@ namespace lms::db
|
||||
prepare();
|
||||
}
|
||||
|
||||
~Connection()
|
||||
~Connection() override
|
||||
{
|
||||
// make use of per-connection usage stats to optimize
|
||||
optimize();
|
||||
@@ -66,16 +67,15 @@ namespace lms::db
|
||||
void prepare()
|
||||
{
|
||||
LMS_LOG(DB, DEBUG, "Setting per-connection settings...");
|
||||
executeSql("pragma journal_mode=WAL");
|
||||
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
|
||||
executeSql("PRAGMA journal_mode=WAL");
|
||||
executeSql("PRAGMA synchronous=normal");
|
||||
LMS_LOG(DB, DEBUG, "Setting per-connection settings done!");
|
||||
}
|
||||
|
||||
void 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");
|
||||
}
|
||||
|
||||
|
||||
@@ -29,9 +29,9 @@ namespace lms::db
|
||||
{
|
||||
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_artist_link t_a_l ON t_a_l.artist_id = a.id")
|
||||
.join("listen l ON l.track_id = t.id") };
|
||||
@@ -53,21 +53,18 @@ namespace lms::db
|
||||
if (!params.clusters.empty())
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "a.id IN (SELECT DISTINCT a.id FROM artist a"
|
||||
" INNER JOIN track t ON t.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";
|
||||
oss << "a.id IN (SELECT DISTINCT t_a_l.artist_id FROM track_artist_link t_a_l"
|
||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t_a_l.track_id";
|
||||
|
||||
WhereClause clusterClause;
|
||||
for (auto id : params.clusters)
|
||||
{
|
||||
clusterClause.Or(WhereClause("c.id = ?"));
|
||||
clusterClause.Or(WhereClause("t_c.cluster_id = ?"));
|
||||
query.bind(id);
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
@@ -75,9 +72,9 @@ namespace lms::db
|
||||
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("listen l ON l.track_id = t.id") };
|
||||
|
||||
@@ -120,9 +117,9 @@ namespace lms::db
|
||||
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") };
|
||||
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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") };
|
||||
|
||||
if (parameters.user.isValid())
|
||||
@@ -205,100 +202,98 @@ namespace lms::db
|
||||
if (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)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().find<Listen>()
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->find<Listen>()
|
||||
.where("user_id = ?").bind(userId)
|
||||
.where("track_id = ?").bind(trackId)
|
||||
.where("backend = ?").bind(backend)
|
||||
.where("date_time = ?").bind(Wt::WDateTime::fromTime_t(dateTime.toTime_t()))
|
||||
.resultValue();
|
||||
.where("date_time = ?").bind(Wt::WDateTime::fromTime_t(dateTime.toTime_t())));
|
||||
}
|
||||
|
||||
RangeResults<ArtistId> Listen::getTopArtists(Session& session, const ArtistStatsFindParameters& params)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
auto query{ createArtistsQuery(session.getDboSession(), params) };
|
||||
auto query{ createArtistsQuery(session, params) };
|
||||
|
||||
auto collection{ query
|
||||
.orderBy("COUNT(a.id) DESC")
|
||||
.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)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
auto query{ createReleasesQuery(session.getDboSession(), params)
|
||||
auto query{ createReleasesQuery(session, params)
|
||||
.orderBy("COUNT(r.id) DESC")
|
||||
.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)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
auto query{ createTracksQuery(session.getDboSession(), params)
|
||||
auto query{ createTracksQuery(session, params)
|
||||
.orderBy("COUNT(t.id) DESC")
|
||||
.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)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
auto query{ createArtistsQuery(session.getDboSession(), params)
|
||||
auto query{ createArtistsQuery(session, params)
|
||||
.groupBy("a.id").having("l.date_time = MAX(l.date_time)")
|
||||
.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)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
auto query{ createReleasesQuery(session.getDboSession(), params)
|
||||
auto query{ createReleasesQuery(session, params)
|
||||
.groupBy("r.id").having("l.date_time = MAX(l.date_time)")
|
||||
.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)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
auto query{ createTracksQuery(session.getDboSession(), params)
|
||||
auto query{ createTracksQuery(session, params)
|
||||
.groupBy("t.id").having("l.date_time = MAX(l.date_time)")
|
||||
.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)
|
||||
{
|
||||
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")
|
||||
.where("l.track_id = ?").bind(trackId)
|
||||
.where("l.user_id = ?").bind(userId)
|
||||
.where("l.backend = u.scrobbling_backend")
|
||||
.resultValue();
|
||||
.where("l.backend = u.scrobbling_backend"));
|
||||
}
|
||||
|
||||
std::size_t Listen::getCount(Session& session, UserId userId, ReleaseId releaseId)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().query<int>(
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>(
|
||||
"SELECT IFNULL(MIN(count_result), 0)"
|
||||
" FROM ("
|
||||
" SELECT COUNT(l.track_id) AS count_result"
|
||||
@@ -308,8 +303,7 @@ namespace lms::db
|
||||
" GROUP BY t.id)")
|
||||
.bind(userId)
|
||||
.bind(userId)
|
||||
.bind(releaseId)
|
||||
.resultValue();
|
||||
.bind(releaseId));
|
||||
}
|
||||
|
||||
Listen::pointer Listen::getMostRecentListen(Session& session, UserId userId, ScrobblingBackend backend, ReleaseId releaseId)
|
||||
@@ -317,26 +311,24 @@ namespace lms::db
|
||||
session.checkReadTransaction();
|
||||
|
||||
// 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")
|
||||
.where("t.release_id = ?").bind(releaseId)
|
||||
.where("l.user_id = ?").bind(userId)
|
||||
.where("l.backend = ?").bind(backend)
|
||||
.orderBy("l.date_time DESC")
|
||||
.limit(1)
|
||||
.resultValue();
|
||||
.limit(1));
|
||||
}
|
||||
|
||||
Listen::pointer Listen::getMostRecentListen(Session& session, UserId userId, ScrobblingBackend backend, TrackId trackId)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
// 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.user_id = ?").bind(userId)
|
||||
.where("l.backend = ?").bind(backend)
|
||||
.orderBy("l.date_time DESC")
|
||||
.limit(1)
|
||||
.resultValue();
|
||||
.limit(1));
|
||||
}
|
||||
} // namespace lms::db
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
#include "IdTypeTraits.hpp"
|
||||
#include "PathTraits.hpp"
|
||||
#include "StringViewTraits.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto results{ session.getDboSession().find<MediaLibrary>().resultList() };
|
||||
for (const auto& result : results)
|
||||
func(result);
|
||||
utils::forEachQueryResult(session.getDboSession()->find<MediaLibrary>(), [&](const MediaLibrary::pointer& mediaLibrary)
|
||||
{
|
||||
func(mediaLibrary);
|
||||
});
|
||||
}
|
||||
} // namespace lms::db
|
||||
|
||||
@@ -27,12 +27,14 @@
|
||||
#include "database/User.hpp"
|
||||
#include "core/Exception.hpp"
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/ITraceLogger.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
namespace
|
||||
{
|
||||
static constexpr Version LMS_DATABASE_VERSION{ 54 };
|
||||
static constexpr Version LMS_DATABASE_VERSION{ 57 };
|
||||
}
|
||||
|
||||
VersionInfo::VersionInfo()
|
||||
@@ -43,9 +45,9 @@ namespace lms::db
|
||||
{
|
||||
session.checkWriteTransaction();
|
||||
|
||||
pointer versionInfo{ session.getDboSession().find<VersionInfo>() };
|
||||
pointer versionInfo{ utils::fetchQuerySingleResult(session.getDboSession()->find<VersionInfo>()) };
|
||||
if (!versionInfo)
|
||||
return session.getDboSession().add(std::make_unique<VersionInfo>());
|
||||
return session.getDboSession()->add(std::make_unique<VersionInfo>());
|
||||
|
||||
return versionInfo;
|
||||
}
|
||||
@@ -54,7 +56,7 @@ namespace lms::db
|
||||
{
|
||||
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
|
||||
// Drop Auth mode
|
||||
session.getDboSession().execute(R"(
|
||||
session.getDboSession()->execute(R"(
|
||||
CREATE TABLE IF NOT EXISTS "track_artist_link_backup" (
|
||||
"id" integer primary key autoincrement,
|
||||
"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
|
||||
);
|
||||
))");
|
||||
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("ALTER TABLE track_artist_link_backup RENAME TO 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("ALTER TABLE track_artist_link_backup RENAME TO track_artist_link");
|
||||
}
|
||||
|
||||
static void migrateFromV34(Session& session)
|
||||
{
|
||||
// Add scrobbling state
|
||||
// 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_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_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_track ADD scrobbling_state INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(/*ScrobblingState::PendingAdd*/0)) + ")");
|
||||
}
|
||||
|
||||
static void migrateFromV35(Session& session)
|
||||
{
|
||||
// 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 last_modified_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");
|
||||
}
|
||||
|
||||
static void migrateFromV36(Session& session)
|
||||
{
|
||||
// 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
|
||||
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)
|
||||
{
|
||||
// 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
|
||||
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)
|
||||
{
|
||||
// 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" (
|
||||
"id" integer primary key autoincrement,
|
||||
"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
|
||||
);
|
||||
))");
|
||||
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("ALTER TABLE track_backup RENAME TO 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("ALTER TABLE track_backup RENAME TO track");
|
||||
|
||||
// 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)
|
||||
{
|
||||
// add release type
|
||||
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 primary_type 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
|
||||
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)
|
||||
{
|
||||
// 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 track 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 ''");
|
||||
|
||||
// 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)
|
||||
{
|
||||
// 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_bitrate TO subsonic_default_transcode_bitrate");
|
||||
session.getDboSession().execute("ALTER TABLE user DROP COLUMN subsonic_transcode_enable");
|
||||
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 DROP COLUMN subsonic_transcode_enable");
|
||||
}
|
||||
|
||||
static void migrateFromV42(Session& session)
|
||||
{
|
||||
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_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_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_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_track_user_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_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_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_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_track_user_scrobbler_idx");
|
||||
|
||||
// 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 ADD feedback_backend INTEGER");
|
||||
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 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_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_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 user RENAME COLUMN scrobbler TO scrobbling_backend");
|
||||
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 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 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 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 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)
|
||||
{
|
||||
// add counts in genre table
|
||||
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 track_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
|
||||
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)
|
||||
{
|
||||
// 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
|
||||
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)
|
||||
{
|
||||
// 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)
|
||||
{
|
||||
// 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,
|
||||
"version" integer not null,
|
||||
"name" text not null
|
||||
);)");
|
||||
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("ALTER TABLE cluster_type_backup RENAME TO 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("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
|
||||
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)
|
||||
{
|
||||
// release type, new way
|
||||
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 primary_type");
|
||||
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,
|
||||
"version" integer 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_id" bigint,
|
||||
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_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" on "release_release_type" ("release_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"))");
|
||||
|
||||
// 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)
|
||||
{
|
||||
// Regression for the extra tags not being parsed
|
||||
// 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)
|
||||
{
|
||||
// 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 original_year INTEGER");
|
||||
session.getDboSession()->execute("ALTER TABLE track ADD 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
|
||||
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)
|
||||
{
|
||||
// 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,
|
||||
"version" integer not null,
|
||||
"path" 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'
|
||||
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"
|
||||
FROM scan_settings s_s
|
||||
WHERE id = ?)").bind(scanSettingsId);
|
||||
|
||||
// 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'
|
||||
session.getDboSession().execute(R"(
|
||||
session.getDboSession()->execute(R"(
|
||||
CREATE TABLE IF NOT EXISTS "track_backup" (
|
||||
"id" integer primary key autoincrement,
|
||||
"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
|
||||
session.getDboSession().execute(R"(INSERT INTO track_backup
|
||||
session.getDboSession()->execute(R"(INSERT INTO track_backup
|
||||
SELECT
|
||||
id,
|
||||
version,
|
||||
@@ -399,36 +401,66 @@ SELECT
|
||||
release_id,
|
||||
1
|
||||
FROM track)");
|
||||
session.getDboSession().execute("DROP TABLE track");
|
||||
session.getDboSession().execute("ALTER TABLE track_backup RENAME TO track");
|
||||
session.getDboSession()->execute("DROP TABLE track");
|
||||
session.getDboSession()->execute("ALTER TABLE track_backup RENAME TO track");
|
||||
}
|
||||
|
||||
void migrateFromV51(Session& session)
|
||||
{
|
||||
// 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 default_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 ''");
|
||||
}
|
||||
|
||||
void migrateFromV52(Session& session)
|
||||
{
|
||||
// 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
|
||||
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)
|
||||
{
|
||||
// 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
|
||||
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)" };
|
||||
|
||||
@@ -459,9 +491,14 @@ SELECT
|
||||
{51, migrateFromV51},
|
||||
{52, migrateFromV52},
|
||||
{53, migrateFromV53},
|
||||
{54, migrateFromV54},
|
||||
{55, migrateFromV55},
|
||||
{56, migrateFromV56},
|
||||
};
|
||||
|
||||
bool migrationPerformed{};
|
||||
{
|
||||
LMS_SCOPED_TRACE_OVERVIEW("Database", "Migration");
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
Version version;
|
||||
@@ -484,6 +521,7 @@ SELECT
|
||||
|
||||
while (version < LMS_DATABASE_VERSION)
|
||||
{
|
||||
LMS_SCOPED_TRACE_DETAILED("Database", "MigrationStep");
|
||||
LMS_LOG(DB, INFO, "Migrating database from version " << version << " to " << version + 1 << "...");
|
||||
|
||||
auto itMigrationFunc{ migrationFunctions.find(version) };
|
||||
@@ -493,7 +531,10 @@ SELECT
|
||||
VersionInfo::get(session).modify()->setVersion(++version);
|
||||
|
||||
LMS_LOG(DB, INFO, "Migration complete to version " << version);
|
||||
migrationPerformed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return migrationPerformed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,6 +52,6 @@ namespace lms::db
|
||||
|
||||
namespace Migration
|
||||
{
|
||||
void doDbMigration(Session& session);
|
||||
bool doDbMigration(Session& session); // return true if migration was performed
|
||||
}
|
||||
}
|
||||
|
||||
+113
-107
@@ -40,7 +40,7 @@ namespace lms::db
|
||||
template <typename ResultType>
|
||||
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
|
||||
|| params.sortMethod == ReleaseSortMethod::LastWritten
|
||||
@@ -90,9 +90,13 @@ namespace lms::db
|
||||
if (params.artist.isValid()
|
||||
|| params.sortMethod == ReleaseSortMethod::ArtistNameThenName)
|
||||
{
|
||||
query.join("artist a ON a.id = t_a_l.artist_id")
|
||||
.join("track_artist_link t_a_l ON t_a_l.track_id = t.id")
|
||||
.where("a.id = ?").bind(params.artist);
|
||||
query.join("track_artist_link t_a_l ON t_a_l.track_id = t.id");
|
||||
|
||||
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())
|
||||
{
|
||||
@@ -115,10 +119,9 @@ namespace lms::db
|
||||
{
|
||||
std::ostringstream oss;
|
||||
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 t ON t.release_id = r.id"
|
||||
" WHERE (a.id = ? AND (";
|
||||
" WHERE (t_a_l.artist_id = ? AND (";
|
||||
|
||||
query.bind(params.artist);
|
||||
|
||||
@@ -145,8 +148,7 @@ namespace lms::db
|
||||
else if (params.clusters.size() > 1)
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "r.id IN (SELECT DISTINCT r.id FROM release r"
|
||||
" INNER JOIN track t ON t.release_id = r.id"
|
||||
oss << "r.id IN (SELECT DISTINCT t.release_id FROM track t"
|
||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
|
||||
|
||||
WhereClause clusterClause;
|
||||
@@ -166,6 +168,9 @@ namespace lms::db
|
||||
{
|
||||
case ReleaseSortMethod::None:
|
||||
break;
|
||||
case ReleaseSortMethod::Id:
|
||||
query.orderBy("r.id");
|
||||
break;
|
||||
case ReleaseSortMethod::Name:
|
||||
query.orderBy("r.name COLLATE NOCASE");
|
||||
break;
|
||||
@@ -204,27 +209,23 @@ namespace lms::db
|
||||
|
||||
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)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession()
|
||||
.find<ReleaseType>()
|
||||
.where("id = ?").bind(id)
|
||||
.resultValue();
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->find<ReleaseType>()
|
||||
.where("id = ?").bind(id));
|
||||
}
|
||||
|
||||
ReleaseType::pointer ReleaseType::find(Session& session, std::string_view name)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession()
|
||||
.find<ReleaseType>()
|
||||
.where("name = ?").bind(name)
|
||||
.resultValue();
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->find<ReleaseType>()
|
||||
.where("name = ?").bind(name));
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto res{ session.getDboSession()
|
||||
.query<Wt::Dbo::ptr<Release>>("SELECT DISTINCT r from release r")
|
||||
return utils::fetchQueryResults<Release::pointer>(session.getDboSession()->query<Wt::Dbo::ptr<Release>>("SELECT DISTINCT r from release r")
|
||||
.join("track t ON t.release_id = r.id")
|
||||
.where("r.name = ?").bind(std::string(name, 0, _maxNameLength))
|
||||
.where("t.file_path LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind(utils::escapeLikeKeyword(releaseDirectory.string()) + "%")
|
||||
.resultList() };
|
||||
|
||||
return std::vector<Release::pointer>(res.begin(), res.end());
|
||||
.where("t.absolute_file_path LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind(utils::escapeLikeKeyword(releaseDirectory.string()) + "%"));
|
||||
}
|
||||
|
||||
Release::pointer Release::find(Session& session, const core::UUID& mbid)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession()
|
||||
.find<Release>()
|
||||
.where("mbid = ?").bind(std::string{ mbid.getAsString() })
|
||||
.resultValue();;
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->find<Release>()
|
||||
.where("mbid = ?").bind(mbid.getAsString()));
|
||||
}
|
||||
|
||||
Release::pointer Release::find(Session& session, ReleaseId id)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession()
|
||||
.find<Release>()
|
||||
.where("id = ?").bind(id)
|
||||
.resultValue();
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->find<Release>()
|
||||
.where("id = ?").bind(id));
|
||||
}
|
||||
|
||||
bool Release::exists(Session& session, ReleaseId id)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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") };
|
||||
return utils::execQuery<ReleaseId>(query, range);
|
||||
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::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)
|
||||
@@ -298,15 +313,15 @@ namespace lms::db
|
||||
session.checkReadTransaction();
|
||||
|
||||
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();
|
||||
|
||||
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)
|
||||
@@ -314,41 +329,38 @@ namespace lms::db
|
||||
session.checkReadTransaction();
|
||||
|
||||
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)
|
||||
{
|
||||
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
|
||||
{
|
||||
assert(session());
|
||||
int res{ session()->query<int>("SELECT COUNT(DISTINCT disc_number) FROM track t")
|
||||
.join("release r ON r.id = t.release_id")
|
||||
.where("r.id = ?")
|
||||
.bind(getId()) };
|
||||
|
||||
int res{ utils::fetchQuerySingleResult(session()->query<int>("SELECT COUNT(DISTINCT disc_number) FROM track t")
|
||||
.where("t.release_id = ?").bind(getId()))};
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<DiscInfo> Release::getDiscs() const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
using ResultType = std::tuple<int, std::string>;
|
||||
auto results{ session()->query<ResultType>("SELECT DISTINCT disc_number, disc_subtitle FROM track t")
|
||||
.join("release r ON r.id = t.release_id")
|
||||
.where("r.id = ?")
|
||||
.orderBy("disc_number")
|
||||
.bind(getId())
|
||||
.resultList() };
|
||||
const auto query{ session()->query<ResultType>("SELECT DISTINCT disc_number, disc_subtitle FROM track t")
|
||||
.where("t.release_id = ?").bind(getId())
|
||||
.orderBy("disc_number")};
|
||||
|
||||
std::vector<DiscInfo> discs;
|
||||
for (const auto& res : results)
|
||||
discs.emplace_back(DiscInfo{ static_cast<std::size_t>(std::get<int>(res)), std::get<std::string>(res) });
|
||||
utils::forEachQueryResult(query, [&](ResultType&& res)
|
||||
{
|
||||
discs.emplace_back(DiscInfo{ static_cast<std::size_t>(std::get<int>(res)), std::move(std::get<std::string>(res)) });
|
||||
});
|
||||
|
||||
return discs;
|
||||
}
|
||||
@@ -368,13 +380,12 @@ namespace lms::db
|
||||
assert(session());
|
||||
|
||||
const char* field{ original ? "original_date" : "date" };
|
||||
|
||||
auto dates{ session()->query<Wt::WDate>(
|
||||
std::string {"SELECT "} + "t." + field + " FROM track t INNER JOIN release r ON r.id = t.release_id")
|
||||
.where("r.id = ?")
|
||||
auto query{ (session()->query<Wt::WDate>(std::string {"SELECT "} + "t." + field + " FROM track t")
|
||||
.where("t.release_id = ?")
|
||||
.groupBy(field)
|
||||
.bind(getId())
|
||||
.resultList() };
|
||||
.bind(getId())) };
|
||||
|
||||
const auto dates{ utils::fetchQueryResults(query) };
|
||||
|
||||
// various dates => invalid date
|
||||
if (dates.empty() || dates.size() > 1)
|
||||
@@ -398,15 +409,15 @@ namespace lms::db
|
||||
assert(session());
|
||||
|
||||
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>>(
|
||||
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() };
|
||||
const auto years{ utils::fetchQueryResults(query) };
|
||||
|
||||
// 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 years.front();
|
||||
@@ -416,63 +427,60 @@ namespace lms::db
|
||||
{
|
||||
assert(session());
|
||||
|
||||
Wt::Dbo::collection<std::string> copyrights = session()->query<std::string>
|
||||
("SELECT copyright FROM track t INNER JOIN release r ON r.id = t.release_id")
|
||||
auto query{ session()->query<std::string>("SELECT copyright FROM track t INNER JOIN release r ON r.id = t.release_id")
|
||||
.where("r.id = ?")
|
||||
.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
|
||||
if (values.empty() || values.size() > 1 || values.front().empty())
|
||||
if (copyrights.empty() || copyrights.size() > 1 || copyrights.front().empty())
|
||||
return std::nullopt;
|
||||
|
||||
return values.front();
|
||||
return std::move(copyrights.front());
|
||||
}
|
||||
|
||||
std::optional<std::string> Release::getCopyrightURL() const
|
||||
{
|
||||
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")
|
||||
.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
|
||||
if (values.empty() || values.size() > 1 || values.front().empty())
|
||||
if (copyrights.empty() || copyrights.size() > 1 || copyrights.front().empty())
|
||||
return std::nullopt;
|
||||
|
||||
return values.front();
|
||||
return std::move(copyrights.front());
|
||||
}
|
||||
|
||||
std::size_t Release::getMeanBitrate() const
|
||||
{
|
||||
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("bitrate > 0")
|
||||
.resultValue();
|
||||
.where("bitrate > 0"));
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer> Release::getArtists(TrackArtistLinkType linkType) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
auto res{ session()->query<Wt::Dbo::ptr<Artist>>(
|
||||
"SELECT DISTINCT a FROM artist a"
|
||||
const auto query{ session()->query<Wt::Dbo::ptr<Artist>>(
|
||||
"SELECT a FROM artist a"
|
||||
" 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 release r ON r.id = t.release_id")
|
||||
.where("r.id = ?").bind(getId())
|
||||
.where("t_a_l.type = ?").bind(linkType)
|
||||
.resultList() };
|
||||
" INNER JOIN track t ON t.id = t_a_l.track_id")
|
||||
.where("t.release_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
|
||||
.groupBy("a.id") };
|
||||
|
||||
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
|
||||
@@ -480,7 +488,7 @@ namespace lms::db
|
||||
assert(session());
|
||||
|
||||
// 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"
|
||||
" INNER JOIN track t ON t.release_id = r.id"
|
||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
|
||||
@@ -498,10 +506,9 @@ namespace lms::db
|
||||
.groupBy("r.id")
|
||||
.orderBy("COUNT(*) DESC, RANDOM()")
|
||||
.limit(count ? static_cast<int>(*count) : -1)
|
||||
.offset(offset ? static_cast<int>(*offset) : -1)
|
||||
.resultList() };
|
||||
.offset(offset ? static_cast<int>(*offset) : -1) };
|
||||
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
return utils::fetchQueryResults<Release::pointer>(query);
|
||||
}
|
||||
|
||||
void Release::clearReleaseTypes()
|
||||
@@ -520,14 +527,17 @@ namespace lms::db
|
||||
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
|
||||
@@ -546,20 +556,16 @@ namespace lms::db
|
||||
|
||||
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")
|
||||
.where("r.id = ?").bind(getId()) };
|
||||
|
||||
return query.resultValue();
|
||||
return utils::fetchQuerySingleResult(session()->query<milli>("SELECT COALESCE(SUM(duration), 0) FROM track t")
|
||||
.where("t.release_id = ?").bind(getId()));
|
||||
}
|
||||
|
||||
Wt::WDateTime Release::getLastWritten() const
|
||||
{
|
||||
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")
|
||||
.where("r.id = ?").bind(getId()) };
|
||||
|
||||
return query.resultValue();
|
||||
return utils::fetchQuerySingleResult(session()->query<Wt::WDateTime>("SELECT COALESCE(MAX(file_last_write), '1970-01-01T00:00:00') FROM track t")
|
||||
.where("t.release_id = ?").bind(getId()));
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
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);
|
||||
}
|
||||
oss << " " << where.get();
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include "database/MediaLibrary.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "core/String.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
@@ -34,14 +35,14 @@ namespace lms::db
|
||||
if (pointer settings{ get(session) })
|
||||
return;
|
||||
|
||||
session.getDboSession().add(std::make_unique<ScanSettings>());
|
||||
session.getDboSession()->add(std::make_unique<ScanSettings>());
|
||||
}
|
||||
|
||||
ScanSettings::pointer ScanSettings::get(Session& session)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().find<ScanSettings>().resultValue();
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->find<ScanSettings>());
|
||||
}
|
||||
|
||||
std::vector<std::filesystem::path> ScanSettings::getAudioFileExtensions() const
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
#include "EnumSetTraits.hpp"
|
||||
#include "PathTraits.hpp"
|
||||
#include "Migration.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
@@ -120,7 +121,7 @@ namespace lms::db
|
||||
return ReadTransaction{ _session };
|
||||
}
|
||||
|
||||
void Session::prepareTables()
|
||||
void Session::prepareTablesIfNeeded()
|
||||
{
|
||||
LMS_LOG(DB, INFO, "Preparing tables...");
|
||||
|
||||
@@ -140,86 +141,178 @@ namespace lms::db
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Migration::doDbMigration(*this);
|
||||
|
||||
// Indexes
|
||||
bool Session::migrateSchemaIfNeeded()
|
||||
{
|
||||
const bool migrationPerformed{ Migration::doDbMigration(*this) };
|
||||
|
||||
// TODO: move this elsewhere
|
||||
{
|
||||
auto uniqueTransaction{ createWriteTransaction() };
|
||||
ScanSettings::init(*this);
|
||||
}
|
||||
|
||||
return migrationPerformed;
|
||||
}
|
||||
|
||||
void Session::createIndexesIfNeeded()
|
||||
{
|
||||
LMS_SCOPED_TRACE_OVERVIEW("Database", "IndexCreation");
|
||||
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_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_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_path_idx ON track(file_path)");
|
||||
_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_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_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 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)");
|
||||
|
||||
LMS_LOG(DB, INFO, "Indexes created!");
|
||||
}
|
||||
|
||||
// Singletons
|
||||
void Session::vacuumIfNeeded()
|
||||
{
|
||||
auto uniqueTransaction{ createWriteTransaction() };
|
||||
ScanSettings::init(*this);
|
||||
}
|
||||
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"));
|
||||
}
|
||||
|
||||
void Session::analyze()
|
||||
LMS_LOG(DB, INFO, "page stats: page_count = " << pageCount << ", freelist_count = " << freeListCount);
|
||||
if (freeListCount >= (pageCount / 10))
|
||||
vacuum();
|
||||
}
|
||||
|
||||
void Session::vacuum()
|
||||
{
|
||||
LMS_SCOPED_TRACE_DETAILED("Database", "Analyze");
|
||||
LMS_LOG(DB, INFO, "Analyzing database...");
|
||||
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() };
|
||||
_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
|
||||
|
||||
@@ -38,40 +38,38 @@ namespace lms::db
|
||||
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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")
|
||||
.where("s_a.artist_id = ?").bind(artistId)
|
||||
.where("s_a.user_id = ?").bind(userId)
|
||||
.where("s_a.backend = u.feedback_backend")
|
||||
.resultValue();
|
||||
.where("s_a.backend = u.feedback_backend"));
|
||||
}
|
||||
|
||||
StarredArtist::pointer StarredArtist::find(Session& session, ArtistId artistId, UserId userId, FeedbackBackend backend)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return session.getDboSession().find<StarredArtist>()
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->find<StarredArtist>()
|
||||
.where("artist_id = ?").bind(artistId)
|
||||
.where("user_id = ?").bind(userId)
|
||||
.where("backend = ?").bind(backend)
|
||||
.resultValue();
|
||||
.where("backend = ?").bind(backend));
|
||||
}
|
||||
|
||||
void StarredArtist::setDateTime(const Wt::WDateTime& dateTime)
|
||||
|
||||
@@ -38,40 +38,38 @@ namespace lms::db
|
||||
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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")
|
||||
.where("s_r.release_id = ?").bind(releaseId)
|
||||
.where("s_r.user_id = ?").bind(userId)
|
||||
.where("s_r.backend = u.feedback_backend")
|
||||
.resultValue();
|
||||
.where("s_r.backend = u.feedback_backend"));
|
||||
}
|
||||
|
||||
StarredRelease::pointer StarredRelease::find(Session& session, ReleaseId releaseId, UserId userId, FeedbackBackend backend)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return session.getDboSession().find<StarredRelease>()
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->find<StarredRelease>()
|
||||
.where("release_id = ?").bind(releaseId)
|
||||
.where("user_id = ?").bind(userId)
|
||||
.where("backend = ?").bind(backend)
|
||||
.resultValue();
|
||||
.where("backend = ?").bind(backend));
|
||||
}
|
||||
|
||||
void StarredRelease::setDateTime(const Wt::WDateTime& dateTime)
|
||||
|
||||
@@ -38,56 +38,53 @@ namespace lms::db
|
||||
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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")
|
||||
.where("s_t.track_id = ?").bind(trackId)
|
||||
.where("s_t.user_id = ?").bind(userId)
|
||||
.where("s_t.backend = u.feedback_backend")
|
||||
.resultValue();
|
||||
.where("s_t.backend = u.feedback_backend"));
|
||||
}
|
||||
|
||||
StarredTrack::pointer StarredTrack::find(Session& session, TrackId trackId, UserId userId, FeedbackBackend backend)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return session.getDboSession().find<StarredTrack>()
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->find<StarredTrack>()
|
||||
.where("track_id = ?").bind(trackId)
|
||||
.where("user_id = ?").bind(userId)
|
||||
.where("backend = ?").bind(backend)
|
||||
.resultValue();
|
||||
.where("backend = ?").bind(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("user_id = ?").bind(userId)
|
||||
.where("backend = ?").bind(backend)
|
||||
.resultValue() == 1;
|
||||
.where("backend = ?").bind(backend));
|
||||
}
|
||||
|
||||
RangeResults<StarredTrackId> StarredTrack::find(Session& session, const FindParameters& params)
|
||||
{
|
||||
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)
|
||||
query.where("s_t.backend = ?").bind(*params.backend);
|
||||
@@ -96,7 +93,7 @@ namespace lms::db
|
||||
if (params.user.isValid())
|
||||
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)
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
#include "core/ILogger.hpp"
|
||||
|
||||
#include "IdTypeTraits.hpp"
|
||||
#include "PathTraits.hpp"
|
||||
#include "SqlQuery.hpp"
|
||||
#include "StringViewTraits.hpp"
|
||||
#include "Utils.hpp"
|
||||
@@ -45,7 +46,7 @@ namespace lms::db
|
||||
{
|
||||
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());
|
||||
for (std::string_view keyword : params.keywords)
|
||||
@@ -152,6 +153,9 @@ namespace lms::db
|
||||
{
|
||||
case TrackSortMethod::None:
|
||||
break;
|
||||
case TrackSortMethod::Id:
|
||||
query.orderBy("t.id");
|
||||
break;
|
||||
case TrackSortMethod::LastWritten:
|
||||
query.orderBy("t.file_last_write DESC");
|
||||
break;
|
||||
@@ -195,154 +199,111 @@ namespace lms::db
|
||||
}
|
||||
}
|
||||
|
||||
Track::Track(const std::filesystem::path& p)
|
||||
: _filePath{ p.string() }
|
||||
Track::pointer Track::create(Session& session)
|
||||
{
|
||||
}
|
||||
|
||||
Track::pointer Track::create(Session& session, const std::filesystem::path& p)
|
||||
{
|
||||
return session.getDboSession().add(std::unique_ptr<Track> {new Track{ p }});
|
||||
return session.getDboSession()->add(std::make_unique<Track>());
|
||||
}
|
||||
|
||||
std::size_t Track::getCount(Session& session)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().find<Track>()
|
||||
.where("id = ?").bind(id)
|
||||
.resultValue();
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->find<Track>()
|
||||
.where("id = ?").bind(id));
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
auto collection{ session.getDboSession().find<Track>()
|
||||
auto query{ session.getDboSession()->find<Track>()
|
||||
.orderBy("id")
|
||||
.where("id > ?").bind(lastRetrievedTrack)
|
||||
.limit(static_cast<int>(batchSize) + 1)
|
||||
.resultList() };
|
||||
.limit(static_cast<int>(count)) };
|
||||
|
||||
moreResults = false;
|
||||
if (library.isValid())
|
||||
query.where("media_library_id = ?").bind(library);
|
||||
|
||||
std::size_t count{};
|
||||
for (auto itResult{ collection.begin() }; itResult != collection.end(); ++itResult)
|
||||
utils::forEachQueryResult(query, [&](const Track::pointer& track)
|
||||
{
|
||||
if (count++ == batchSize)
|
||||
{
|
||||
moreResults = true;
|
||||
break;
|
||||
}
|
||||
|
||||
func(*itResult);
|
||||
lastRetrievedTrack = (*itResult)->getId();
|
||||
}
|
||||
func(track);
|
||||
lastRetrievedTrack = track->getId();
|
||||
});
|
||||
}
|
||||
|
||||
bool Track::exists(Session& session, TrackId id)
|
||||
{
|
||||
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)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto res{ session.getDboSession().find<Track>()
|
||||
.where("mbid = ?").bind(std::string {mbid.getAsString()})
|
||||
.resultList() };
|
||||
|
||||
return std::vector<Track::pointer>(res.begin(), res.end());
|
||||
return utils::fetchQueryResults<Track::pointer>(session.getDboSession()->find<Track>()
|
||||
.where("mbid = ?").bind(mbid.getAsString()));
|
||||
}
|
||||
|
||||
std::vector<Track::pointer> Track::findByRecordingMBID(Session& session, const core::UUID& mbid)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto res{ session.getDboSession().find<Track>()
|
||||
.where("recording_mbid = ?").bind(std::string {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;
|
||||
return utils::fetchQueryResults<Track::pointer>(session.getDboSession()->find<Track>()
|
||||
.where("recording_mbid = ?").bind(mbid.getAsString()));
|
||||
}
|
||||
|
||||
RangeResults<TrackId> Track::findIdsTrackMBIDDuplicates(Session& session, std::optional<Range> range)
|
||||
{
|
||||
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") };
|
||||
|
||||
return utils::execQuery<TrackId>(query, range);
|
||||
return utils::execRangeQuery<TrackId>(query, range);
|
||||
}
|
||||
|
||||
RangeResults<TrackId> Track::findIdsWithRecordingMBIDAndMissingFeatures(Session& session, std::optional<Range> range)
|
||||
{
|
||||
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("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
|
||||
{
|
||||
return std::vector<Cluster::pointer>(_clusters.begin(), _clusters.end());
|
||||
return utils::fetchQueryResults<Cluster::pointer>(_clusters.find());
|
||||
}
|
||||
|
||||
std::vector<ClusterId> Track::getClusterIds() const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
auto res{ 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")
|
||||
.where("t.id = ?").bind(getId())
|
||||
.resultList() };
|
||||
const auto query{ session()->query<ClusterId>
|
||||
("SELECT t_c.cluster_id FROM track_cluster t_c")
|
||||
.where("t_c.track_id = ?").bind(getId())
|
||||
.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)
|
||||
@@ -350,7 +311,7 @@ namespace lms::db
|
||||
session.checkReadTransaction();
|
||||
|
||||
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)
|
||||
@@ -358,23 +319,23 @@ namespace lms::db
|
||||
session.checkReadTransaction();
|
||||
|
||||
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();
|
||||
|
||||
auto query{ createQuery<Wt::Dbo::ptr<Track>>(session, params)};
|
||||
utils::execQuery(query, params.range, func);
|
||||
auto query{ createQuery<Wt::Dbo::ptr<Track>>(session, params) };
|
||||
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();
|
||||
|
||||
auto query{ createQuery<Wt::Dbo::ptr<Track>>(session, params)};
|
||||
utils::execQuery(query, params.range, moreResults, func);
|
||||
auto query{ createQuery<Wt::Dbo::ptr<Track>>(session, params) };
|
||||
utils::forEachQueryRangeResult(query, params.range, moreResults, func);
|
||||
}
|
||||
|
||||
RangeResults<TrackId> Track::findSimilarTrackIds(Session& session, const std::vector<TrackId>& tracks, std::optional<Range> range)
|
||||
@@ -390,7 +351,7 @@ namespace lms::db
|
||||
oss << "?";
|
||||
}
|
||||
|
||||
auto query{ session.getDboSession().query<TrackId>(
|
||||
auto query{ session.getDboSession()->query<TrackId>(
|
||||
"SELECT t.id FROM track t"
|
||||
" 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() + "))"
|
||||
@@ -404,7 +365,19 @@ namespace lms::db
|
||||
for (TrackId trackId : tracks)
|
||||
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()
|
||||
@@ -465,8 +438,7 @@ namespace lms::db
|
||||
|
||||
query.where("t.id = ?").bind(getId());
|
||||
|
||||
auto res{ query.resultList() };
|
||||
return std::vector<Artist::pointer>(std::begin(res), std::end(res));
|
||||
return utils::fetchQueryResults<Artist::pointer>(query);
|
||||
}
|
||||
|
||||
std::vector<ArtistId> Track::getArtistIds(core::EnumSet<TrackArtistLinkType> linkTypes) const
|
||||
@@ -476,8 +448,7 @@ namespace lms::db
|
||||
|
||||
std::ostringstream oss;
|
||||
oss <<
|
||||
"SELECT DISTINCT a.id from artist a"
|
||||
" INNER JOIN track_artist_link t_a_l ON a.id = t_a_l.artist_id"
|
||||
"SELECT t_a_l.artist_id FROM track_artist_link t_a_l"
|
||||
" INNER JOIN track t ON t.id = t_a_l.track_id";
|
||||
|
||||
if (!linkTypes.empty())
|
||||
@@ -500,14 +471,14 @@ namespace lms::db
|
||||
query.bind(type);
|
||||
|
||||
query.where("t.id = ?").bind(getId());
|
||||
query.groupBy("t_a_l.artist_id");
|
||||
|
||||
auto res{ query.resultList() };
|
||||
return std::vector<ArtistId>(std::cbegin(res), std::cend(res));
|
||||
return utils::fetchQueryResults(query);
|
||||
}
|
||||
|
||||
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
|
||||
@@ -535,14 +506,12 @@ namespace lms::db
|
||||
for (const std::string& bindArg : where.getBindArgs())
|
||||
query.bind(bindArg);
|
||||
|
||||
auto queryRes{ query.resultList() };
|
||||
|
||||
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)
|
||||
clusters[cluster->getType()->getId()].push_back(cluster);
|
||||
}
|
||||
});
|
||||
|
||||
std::vector<std::vector<Cluster::pointer>> res;
|
||||
for (const auto& [type, clusters] : clusters)
|
||||
|
||||
@@ -30,26 +30,26 @@ namespace lms::db
|
||||
{
|
||||
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();
|
||||
|
||||
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)
|
||||
query.where("t_a_l.type = ?").bind(*params.linkType);
|
||||
|
||||
if (params.track.isValid() || params.release.isValid())
|
||||
query.join("track t ON t.id = t_a_l.track_id");
|
||||
if (params.track.isValid())
|
||||
query.where("t_a_l.track_id = ?").bind(params.track);
|
||||
|
||||
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())
|
||||
{
|
||||
query.join("track t ON t.id = t_a_l.track_id");
|
||||
query.where("t.release_id = ?").bind(params.release);
|
||||
|
||||
if (params.track.isValid())
|
||||
query.where("t.id = ?").bind(params.track);
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
@@ -67,8 +67,8 @@ namespace lms::db
|
||||
{
|
||||
session.checkWriteTransaction();
|
||||
|
||||
TrackArtistLink::pointer res{ session.getDboSession().add(std::make_unique<TrackArtistLink>(track, artist, type, subType)) };
|
||||
session.getDboSession().flush();
|
||||
TrackArtistLink::pointer res{ session.getDboSession()->add(std::make_unique<TrackArtistLink>(track, artist, type, subType)) };
|
||||
session.getDboSession()->flush();
|
||||
|
||||
return res;
|
||||
}
|
||||
@@ -76,36 +76,61 @@ namespace lms::db
|
||||
TrackArtistLink::pointer TrackArtistLink::find(Session& session, TrackArtistLinkId id)
|
||||
{
|
||||
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();
|
||||
|
||||
auto query{ createQuery(session, params) };
|
||||
return utils::execQuery<TrackArtistLinkId>(query, params.range);
|
||||
using ResultType = std::tuple < Wt::Dbo::ptr<TrackArtistLink>, Wt::Dbo::ptr<Artist>>;
|
||||
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto res{ session.getDboSession()
|
||||
.query<TrackArtistLinkType>("SELECT DISTINCT type from track_artist_link")
|
||||
.where("artist_id = ?").bind(artistId)
|
||||
.resultList() };
|
||||
const auto query{ session.getDboSession()->query<TrackArtistLinkType>("SELECT DISTINCT type from track_artist_link")
|
||||
.where("artist_id = ?").bind(artistId) };
|
||||
|
||||
return core::EnumSet<TrackArtistLinkType>(std::begin(res), std::end(res));
|
||||
core::EnumSet<TrackArtistLinkType> res;
|
||||
utils::forEachQueryResult(query, [&](TrackArtistLinkType linkType)
|
||||
{
|
||||
res.insert(linkType);
|
||||
});
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,43 +35,41 @@ namespace lms::db
|
||||
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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) };
|
||||
|
||||
return utils::execQuery<TrackBookmarkId>(query, range);
|
||||
return utils::execRangeQuery<TrackBookmarkId>(query, range);
|
||||
}
|
||||
|
||||
TrackBookmark::pointer TrackBookmark::find(Session& session, UserId userId, TrackId trackId)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().find<TrackBookmark>()
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->find<TrackBookmark>()
|
||||
.where("user_id = ?").bind(userId)
|
||||
.where("track_id = ?").bind(trackId)
|
||||
.resultValue();
|
||||
.where("track_id = ?").bind(trackId));
|
||||
}
|
||||
|
||||
TrackBookmark::pointer TrackBookmark::find(Session& session, TrackBookmarkId id)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().find<TrackBookmark>()
|
||||
.where("id = ?").bind(id)
|
||||
.resultValue();
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->find<TrackBookmark>()
|
||||
.where("id = ?").bind(id));
|
||||
}
|
||||
|
||||
} // namespace lms::db
|
||||
|
||||
@@ -38,41 +38,39 @@ namespace lms::db {
|
||||
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().find<TrackFeatures>()
|
||||
.where("id = ?").bind(id)
|
||||
.resultValue();
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->find<TrackFeatures>()
|
||||
.where("id = ?").bind(id));
|
||||
}
|
||||
|
||||
TrackFeatures::pointer TrackFeatures::find(Session& session, TrackId trackId)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().find<TrackFeatures>()
|
||||
.where("track_id = ?").bind(trackId)
|
||||
.resultValue();
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->find<TrackFeatures>()
|
||||
.where("track_id = ?").bind(trackId));
|
||||
}
|
||||
|
||||
RangeResults<TrackFeaturesId> TrackFeatures::find(Session& session, std::optional<Range> range)
|
||||
{
|
||||
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
|
||||
|
||||
@@ -48,14 +48,14 @@ namespace lms::db
|
||||
|
||||
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)
|
||||
{
|
||||
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();
|
||||
assert(userId.isValid());
|
||||
|
||||
return session.getDboSession().find<TrackList>()
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->find<TrackList>()
|
||||
.where("name = ?").bind(name)
|
||||
.where("type = ?").bind(type)
|
||||
.where("user_id = ?").bind(userId).resultValue();
|
||||
.where("user_id = ?").bind(userId));
|
||||
}
|
||||
|
||||
RangeResults<TrackListId> TrackList::find(Session& session, const FindParameters& params)
|
||||
{
|
||||
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())
|
||||
query.where("t_l.user_id = ?").bind(params.user);
|
||||
@@ -117,14 +117,14 @@ namespace lms::db
|
||||
break;
|
||||
}
|
||||
|
||||
return utils::execQuery<TrackListId>(query, params.range);
|
||||
return utils::execRangeQuery<TrackListId>(query, params.range);
|
||||
}
|
||||
|
||||
TrackList::pointer TrackList::find(Session& session, TrackListId id)
|
||||
{
|
||||
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
|
||||
@@ -156,31 +156,29 @@ namespace lms::db
|
||||
.where("tracklist_id = ?").bind(getId())
|
||||
.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
|
||||
{
|
||||
assert(session());
|
||||
|
||||
return session()->find<TrackListEntry>()
|
||||
return utils::fetchQuerySingleResult(session()->find<TrackListEntry>()
|
||||
.where("tracklist_id = ?").bind(getId())
|
||||
.where("track_id = ?").bind(track->getId())
|
||||
.where("date_time = ?").bind(utils::normalizeDateTime(dateTime))
|
||||
.resultValue();
|
||||
.where("date_time = ?").bind(utils::normalizeDateTime(dateTime)));
|
||||
}
|
||||
|
||||
std::vector<Cluster::pointer> TrackList::getClusters() const
|
||||
{
|
||||
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())
|
||||
.groupBy("c.id")
|
||||
.orderBy("COUNT(c.id) DESC")
|
||||
.resultList() };
|
||||
.orderBy("COUNT(c.id) DESC") };
|
||||
|
||||
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
|
||||
@@ -218,14 +216,12 @@ namespace lms::db
|
||||
query.groupBy("c.id");
|
||||
query.orderBy("COUNT(c.id) DESC");
|
||||
|
||||
auto queryRes{ query.resultList() };
|
||||
|
||||
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)
|
||||
clustersByType[cluster->getType()->getId()].push_back(cluster);
|
||||
}
|
||||
});
|
||||
|
||||
for (const auto& [clusterTypeId, clusters] : clustersByType)
|
||||
res.push_back(clusters);
|
||||
@@ -237,7 +233,7 @@ namespace lms::db
|
||||
{
|
||||
assert(session());
|
||||
|
||||
auto res{ session()->query<Wt::Dbo::ptr<Track>>(
|
||||
auto query{ session()->query<Wt::Dbo::ptr<Track>>(
|
||||
"SELECT t FROM track t"
|
||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
|
||||
" WHERE "
|
||||
@@ -249,20 +245,19 @@ namespace lms::db
|
||||
.groupBy("t.id")
|
||||
.orderBy("COUNT(*) DESC, RANDOM()")
|
||||
.limit(size ? static_cast<int>(*size) : -1)
|
||||
.offset(offset ? static_cast<int>(*offset) : -1)
|
||||
.resultList() };
|
||||
.offset(offset ? static_cast<int>(*offset) : -1) };
|
||||
|
||||
return std::vector<Track::pointer>(res.begin(), res.end());
|
||||
return utils::fetchQueryResults<Track::pointer>(query);
|
||||
}
|
||||
|
||||
std::vector<TrackId> TrackList::getTrackIds() const
|
||||
{
|
||||
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")
|
||||
.where("p.id = ?").bind(getId());
|
||||
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()) };
|
||||
|
||||
return std::vector<TrackId>(res.begin(), res.end());
|
||||
return utils::fetchQueryResults(query);
|
||||
}
|
||||
|
||||
std::chrono::milliseconds TrackList::getDuration() const
|
||||
@@ -271,10 +266,8 @@ namespace lms::db
|
||||
|
||||
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")
|
||||
.where("p_e.tracklist_id = ?").bind(getId()) };
|
||||
|
||||
return query.resultValue();
|
||||
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()));
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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()
|
||||
@@ -310,6 +303,6 @@ namespace lms::db
|
||||
{
|
||||
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
|
||||
|
||||
@@ -80,7 +80,7 @@ namespace lms::db
|
||||
|
||||
void TransactionChecker::checkWriteTransaction(Session& session)
|
||||
{
|
||||
checkWriteTransaction(session.getDboSession());
|
||||
checkWriteTransaction(*session.getDboSession());
|
||||
}
|
||||
|
||||
void TransactionChecker::checkReadTransaction(Wt::Dbo::Session& session)
|
||||
@@ -91,6 +91,6 @@ namespace lms::db
|
||||
|
||||
void TransactionChecker::checkReadTransaction(Session& session)
|
||||
{
|
||||
checkReadTransaction(session.getDboSession());
|
||||
checkReadTransaction(*session.getDboSession());
|
||||
}
|
||||
}
|
||||
@@ -28,8 +28,8 @@
|
||||
#include "StringViewTraits.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace lms::db {
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
User::User(std::string_view loginName)
|
||||
: _loginName{ loginName }
|
||||
{
|
||||
@@ -37,47 +37,46 @@ namespace lms::db {
|
||||
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
query.where("scrobbling_backend = ?").bind(*params.scrobblingBackend);
|
||||
if (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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
return session.getDboSession().find<User>()
|
||||
.where("login_name = ?").bind(name)
|
||||
.resultValue();
|
||||
return utils::fetchQuerySingleResult(session.getDboSession()->find<User>()
|
||||
.where("login_name = ?").bind(name));
|
||||
}
|
||||
|
||||
void User::setSubsonicDefaultTranscodingOutputBitrate(Bitrate bitrate)
|
||||
|
||||
@@ -45,11 +45,75 @@ namespace lms::db::utils
|
||||
}
|
||||
}
|
||||
|
||||
template <typename ResultType, typename Query>
|
||||
RangeResults<ResultType> execQuery(Query& query, std::optional<Range> range)
|
||||
template <typename T>
|
||||
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;
|
||||
|
||||
if (range)
|
||||
@@ -60,16 +124,12 @@ namespace lms::db::utils
|
||||
res.results.reserve(range->size);
|
||||
}
|
||||
|
||||
auto collection{ query.resultList() };
|
||||
for (auto itResult{ collection.begin() }; itResult != collection.end(); ++itResult)
|
||||
{
|
||||
if (range && res.results.size() == range->size)
|
||||
// TODO optim useless last copy
|
||||
res.results = utils::fetchQueryResults<ResultType>(query);
|
||||
if (range && (res.results.size() == range->size + 1))
|
||||
{
|
||||
res.moreResults = true;
|
||||
break;
|
||||
}
|
||||
|
||||
res.results.push_back(std::move(*itResult));
|
||||
res.results.pop_back();
|
||||
}
|
||||
|
||||
res.range.size = res.results.size();
|
||||
@@ -77,29 +137,29 @@ namespace lms::db::utils
|
||||
return res;
|
||||
}
|
||||
|
||||
template <typename ResultType, typename Query>
|
||||
void execQuery(Query& query, std::optional<Range> range, std::function<void(const ResultType&)> func)
|
||||
template <typename Query, typename UnaryFunc>
|
||||
void forEachQueryRangeResult(Query& query, std::optional<Range> range, UnaryFunc&& func)
|
||||
{
|
||||
if (range)
|
||||
applyRange(query, range);
|
||||
|
||||
for (const auto& res : query.resultList())
|
||||
{
|
||||
LMS_SCOPED_TRACE_DETAILED("Database", "ExecQueryResult");
|
||||
func(res);
|
||||
}
|
||||
forEachQueryResult(query, std::forward<UnaryFunc>(func));
|
||||
}
|
||||
|
||||
template <typename ResultType, typename Query>
|
||||
void execQuery(Query& query, std::optional<Range> range, bool& moreResults, std::function<void(const ResultType&)> func)
|
||||
template <typename Query, typename UnaryFunc>
|
||||
void forEachQueryRangeResult(Query& query, std::optional<Range> range, bool& moreResults, UnaryFunc&& func)
|
||||
{
|
||||
using ResultType = typename QueryResultType<Query>::type;
|
||||
|
||||
if (range)
|
||||
applyRange(query, Range{ range->offset, range->size + 1 });
|
||||
|
||||
moreResults = false;
|
||||
|
||||
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)))
|
||||
{
|
||||
@@ -107,11 +167,10 @@ namespace lms::db::utils
|
||||
break;
|
||||
}
|
||||
|
||||
LMS_SCOPED_TRACE_DETAILED("Database", "ExecQueryResult");
|
||||
func(res);
|
||||
func(*it);
|
||||
fetchNextResult<ResultType>(it);
|
||||
}
|
||||
}
|
||||
|
||||
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, ArtistId id);
|
||||
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 void find(Session& session, const FindParameters& parameters, std::function<void(const pointer&)> func);
|
||||
static RangeResults<ArtistId> findIds(Session& session, const FindParameters& parameters);
|
||||
|
||||
@@ -75,7 +75,7 @@ namespace lms::db
|
||||
// Accessors
|
||||
std::string_view getName() const { return _name; }
|
||||
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;
|
||||
std::size_t getReleasesCount() const { return _releaseCount; };
|
||||
|
||||
|
||||
@@ -32,17 +32,16 @@ namespace lms::db
|
||||
using ValueType = Wt::Dbo::dbo_default_traits::IdType;
|
||||
|
||||
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(); }
|
||||
std::string toString() const { assert(isValid()); return std::to_string(_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()};
|
||||
Wt::Dbo::dbo_default_traits::IdType _id{ Wt::Dbo::dbo_default_traits::invalidId() };
|
||||
};
|
||||
|
||||
#define LMS_DECLARE_IDTYPE(name) \
|
||||
@@ -51,6 +50,7 @@ namespace lms::db
|
||||
{ \
|
||||
public: \
|
||||
using IdType::IdType; \
|
||||
auto operator<=>(const name& other) const = default; \
|
||||
};\
|
||||
} \
|
||||
namespace std \
|
||||
|
||||
@@ -31,7 +31,8 @@ namespace lms::db
|
||||
{
|
||||
public:
|
||||
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(); }
|
||||
operator bool() const { return _obj.get(); }
|
||||
|
||||
@@ -122,8 +122,9 @@ namespace lms::db
|
||||
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 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 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 std::size_t getCount(Session& session, const FindParameters& parameters);
|
||||
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;
|
||||
Wt::WDateTime getLastWritten() const;
|
||||
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<std::string> getReleaseTypeNames() const;
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
#include <Wt/Dbo/SqlConnectionPool.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include "core/ITraceLogger.hpp"
|
||||
#include "core/RecursiveSharedMutex.hpp"
|
||||
#include "database/Object.hpp"
|
||||
@@ -41,8 +43,8 @@ namespace lms::db
|
||||
WriteTransaction(const WriteTransaction&) = delete;
|
||||
WriteTransaction& operator=(const WriteTransaction&) = delete;
|
||||
|
||||
std::unique_lock<core::RecursiveSharedMutex> _lock;
|
||||
core::tracing::ScopedTrace _trace{ "Database", core::tracing::Level::Detailed, "WriteTransaction" }; // before actual transaction
|
||||
const std::unique_lock<core::RecursiveSharedMutex> _lock;
|
||||
const core::tracing::ScopedTrace _trace{ "Database", core::tracing::Level::Detailed, "WriteTransaction" }; // before actual transaction
|
||||
Wt::Dbo::Transaction _transaction;
|
||||
};
|
||||
|
||||
@@ -58,7 +60,7 @@ namespace lms::db
|
||||
ReadTransaction(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;
|
||||
};
|
||||
|
||||
@@ -84,12 +86,20 @@ namespace lms::db
|
||||
#endif
|
||||
}
|
||||
|
||||
void analyze();
|
||||
void optimize();
|
||||
// All these methods will acquire transactions
|
||||
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; }
|
||||
|
||||
template <typename Object, typename... Args>
|
||||
@@ -98,7 +108,7 @@ namespace lms::db
|
||||
checkWriteTransaction();
|
||||
|
||||
typename Object::pointer res{ Object::create(*this, std::forward<Args>(args)...) };
|
||||
getDboSession().flush();
|
||||
getDboSession()->flush();
|
||||
|
||||
if (res->hasOnPostCreated())
|
||||
res.modify()->onPostCreated();
|
||||
|
||||
@@ -110,7 +110,7 @@ namespace lms::db
|
||||
static std::size_t getCount(Session& session);
|
||||
static pointer findByPath(Session& session, const std::filesystem::path& p);
|
||||
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 std::vector<pointer> findByRecordingMBID(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<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, bool& moreResults, std::function<void(const Track::pointer&)> func);
|
||||
static RangeResults<PathResult> findPaths(Session& session, std::optional<Range> range = std::nullopt);
|
||||
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, const std::function<void(const Track::pointer&)>& func);
|
||||
static RangeResults<TrackId> findIdsTrackMBIDDuplicates(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 setDiscSubtitle(const std::string& name) { _discSubtitle = name; }
|
||||
void setName(const std::string& name) { _name = std::string(name, 0, _maxNameLength); }
|
||||
void setPath(const std::filesystem::path& filePath) { _filePath = filePath; }
|
||||
void setDuration(std::chrono::milliseconds duration) { _duration = duration; }
|
||||
void setBitrate(std::size_t bitrate) { _bitrate = bitrate; }
|
||||
void setAbsoluteFilePath(const std::filesystem::path& filePath);
|
||||
void setRelativeFilePath(const std::filesystem::path& filePath);
|
||||
void setFileSize(std::size_t fileSize) { _fileSize = fileSize; }
|
||||
void setLastWriteTime(Wt::WDateTime time) { _fileLastWrite = 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 setYear(std::optional<int> year) { _year = year; }
|
||||
void setOriginalDate(const Wt::WDate& date) { _originalDate = date; }
|
||||
@@ -160,16 +164,21 @@ namespace lms::db
|
||||
std::optional<std::size_t> getDiscNumber() const { return _discNumber; }
|
||||
const std::string& getDiscSubtitle() const { return _discSubtitle; }
|
||||
std::string getName() const { return _name; }
|
||||
std::filesystem::path getPath() const { return _filePath; }
|
||||
std::chrono::milliseconds getDuration() const { return _duration; }
|
||||
const std::filesystem::path& getAbsoluteFilePath() const { return _absoluteFilePath; }
|
||||
const std::filesystem::path& getRelativeFilePath() const { return _relativeFilePath; }
|
||||
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::WDate& getDate() const { return _date; }
|
||||
std::optional<int> getYear() const { return _year; }
|
||||
const Wt::WDate& getOriginalDate() const { return _originalDate; }
|
||||
std::optional<int> getOriginalYear() const { return _originalYear; };
|
||||
Wt::WDateTime getLastWriteTime() const { return _fileLastWrite; }
|
||||
Wt::WDateTime getAddedTime() const { return _fileAdded; }
|
||||
const Wt::WDateTime& getLastWriteTime() const { return _fileLastWrite; }
|
||||
const Wt::WDateTime& getAddedTime() const { return _fileAdded; }
|
||||
bool hasCover() const { return _hasCover; }
|
||||
std::optional<core::UUID> getTrackMBID() const { return core::UUID::fromString(_trackMBID); }
|
||||
std::optional<core::UUID> getRecordingMBID() const { return core::UUID::fromString(_recordingMBID); }
|
||||
@@ -200,11 +209,16 @@ namespace lms::db
|
||||
Wt::Dbo::field(a, _name, "name");
|
||||
Wt::Dbo::field(a, _duration, "duration");
|
||||
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, _year, "year");
|
||||
Wt::Dbo::field(a, _originalDate, "original_date");
|
||||
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, _fileAdded, "file_added");
|
||||
Wt::Dbo::field(a, _hasCover, "has_cover");
|
||||
@@ -223,8 +237,7 @@ namespace lms::db
|
||||
|
||||
private:
|
||||
friend class Session;
|
||||
Track(const std::filesystem::path& p);
|
||||
static pointer create(Session& session, const std::filesystem::path& p);
|
||||
static pointer create(Session& session);
|
||||
|
||||
static constexpr std::size_t _maxNameLength{ 256 };
|
||||
static constexpr std::size_t _maxCopyrightLength{ 256 };
|
||||
@@ -236,13 +249,18 @@ namespace lms::db
|
||||
std::optional<int> _totalTrack{};
|
||||
std::string _discSubtitle;
|
||||
std::string _name;
|
||||
int _bitrate{}; // in bps
|
||||
int _bitsPerSample{};
|
||||
int _channelCount{};
|
||||
std::chrono::duration<int, std::milli> _duration{};
|
||||
int _bitrate; // in bps
|
||||
int _sampleRate{};
|
||||
Wt::WDate _date;
|
||||
std::optional<int> _year;
|
||||
Wt::WDate _originalDate;
|
||||
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 _fileAdded;
|
||||
bool _hasCover{};
|
||||
@@ -270,5 +288,3 @@ namespace lms::db
|
||||
std::ostream& operator<<(std::ostream& os, const TrackInfo& trackInfo);
|
||||
}
|
||||
} // namespace lms::db
|
||||
|
||||
|
||||
|
||||
@@ -62,7 +62,8 @@ namespace lms::db
|
||||
TrackArtistLink() = default;
|
||||
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 create(Session& session, ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType = {});
|
||||
static core::EnumSet<TrackArtistLinkType> findUsedTypes(Session& session);
|
||||
|
||||
@@ -108,8 +108,9 @@ namespace lms::db
|
||||
enum class ArtistSortMethod
|
||||
{
|
||||
None,
|
||||
ByName,
|
||||
BySortName,
|
||||
Id,
|
||||
Name,
|
||||
SortName,
|
||||
Random,
|
||||
LastWritten,
|
||||
StarredDateDesc,
|
||||
@@ -118,6 +119,7 @@ namespace lms::db
|
||||
enum class ReleaseSortMethod
|
||||
{
|
||||
None,
|
||||
Id,
|
||||
Name,
|
||||
ArtistNameThenName,
|
||||
Date,
|
||||
@@ -138,6 +140,7 @@ namespace lms::db
|
||||
enum class TrackSortMethod
|
||||
{
|
||||
None,
|
||||
Id,
|
||||
Random,
|
||||
LastWritten,
|
||||
StarredDateDesc,
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
{
|
||||
@@ -139,7 +232,7 @@ namespace lms::db::tests
|
||||
|
||||
TEST_F(DatabaseFixture, Artist_singleTrack)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedTrack track{ session };
|
||||
ScopedArtist artist{ session, "MyArtist" };
|
||||
|
||||
{
|
||||
@@ -209,7 +302,7 @@ namespace lms::db::tests
|
||||
|
||||
TEST_F(DatabaseFixture, Artist_singleTrack_mediaLibrary)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedTrack track{ session };
|
||||
ScopedArtist artist{ session, "MyArtist" };
|
||||
ScopedMediaLibrary library{ session };
|
||||
ScopedMediaLibrary otherLibrary{ session };
|
||||
@@ -242,7 +335,7 @@ namespace lms::db::tests
|
||||
|
||||
TEST_F(DatabaseFixture, Artist_singleTracktMultiRoles)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedTrack track{ session };
|
||||
ScopedArtist artist{ session, "MyArtist" };
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
@@ -301,11 +394,34 @@ namespace lms::db::tests
|
||||
EXPECT_TRUE(types.contains(TrackArtistLinkType::Writer));
|
||||
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)
|
||||
{
|
||||
ScopedTrack track{ session, "track" };
|
||||
ScopedTrack track{ session };
|
||||
ScopedArtist artist1{ session, "artist1" };
|
||||
ScopedArtist artist2{ session, "artist2" };
|
||||
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 }));
|
||||
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)
|
||||
{
|
||||
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() };
|
||||
@@ -409,7 +566,7 @@ namespace lms::db::tests
|
||||
EXPECT_EQ(artists.front()->getId(), artist1.getId());
|
||||
EXPECT_EQ(Artist::find(session, R"(MyArtistFoo)").size(), 0);
|
||||
}
|
||||
{
|
||||
{
|
||||
const auto artists{ Artist::find(session, R"(%MyArtist)") };
|
||||
ASSERT_TRUE(artists.size() == 1);
|
||||
EXPECT_EQ(artists.front()->getId(), artist2.getId());
|
||||
@@ -420,32 +577,32 @@ namespace lms::db::tests
|
||||
ASSERT_TRUE(artists.size() == 1);
|
||||
ASSERT_EQ(artists.front()->getId(), artist3.getId());
|
||||
EXPECT_EQ(Artist::find(session, R"(%CMyArtist)").size(), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
{
|
||||
{
|
||||
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)) };
|
||||
const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setKeywords({"MyArtist%"}).setSortMethod(ArtistSortMethod::Name)) };
|
||||
ASSERT_EQ(artists.results.size(), 2);
|
||||
EXPECT_EQ(artists.results[0], artist1.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);
|
||||
EXPECT_EQ(artists.results[0], artist2.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);
|
||||
EXPECT_EQ(artists.results[0], artist3.getId());
|
||||
}
|
||||
@@ -467,8 +624,8 @@ namespace lms::db::tests
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto allArtistsByName{ Artist::findIds(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::ByName)) };
|
||||
auto allArtistsBySortName{ Artist::findIds(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::BySortName)) };
|
||||
auto allArtistsByName{ Artist::findIds(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::Name)) };
|
||||
auto allArtistsBySortName{ Artist::findIds(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::SortName)) };
|
||||
|
||||
ASSERT_EQ(allArtistsByName.results.size(), 2);
|
||||
EXPECT_EQ(allArtistsByName.results.front(), artistA.getId());
|
||||
@@ -483,8 +640,8 @@ namespace lms::db::tests
|
||||
TEST_F(DatabaseFixture, Artist_nonReleaseTracks)
|
||||
{
|
||||
ScopedArtist artist{ session, "artist" };
|
||||
ScopedTrack track1{ session, "MyTrack1" };
|
||||
ScopedTrack track2{ session, "MyTrack2" };
|
||||
ScopedTrack track1{ session };
|
||||
ScopedTrack track2{ session };
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
|
||||
{
|
||||
@@ -515,7 +672,7 @@ namespace lms::db::tests
|
||||
TEST_F(DatabaseFixture, Artist_findByRelease)
|
||||
{
|
||||
ScopedArtist artist{ session, "artist" };
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedTrack track{ session };
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
|
||||
{
|
||||
|
||||
@@ -86,7 +86,7 @@ namespace lms::db::tests
|
||||
|
||||
TEST_F(DatabaseFixture, Cluster_singleTrack)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedTrack track{ session };
|
||||
ScopedClusterType clusterType{ session, "MyClusterType" };
|
||||
|
||||
{
|
||||
@@ -160,7 +160,7 @@ namespace lms::db::tests
|
||||
|
||||
TEST_F(DatabaseFixture, Cluster_singleTrackWithSeveralClusters)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedTrack track{ session };
|
||||
ScopedClusterType clusterType{ session, "MyClusterType" };
|
||||
|
||||
ScopedCluster cluster1{ session, clusterType.lockAndGet(), "MyCluster1" };
|
||||
@@ -215,7 +215,7 @@ namespace lms::db::tests
|
||||
|
||||
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() };
|
||||
@@ -270,7 +270,7 @@ namespace lms::db::tests
|
||||
|
||||
TEST_F(DatabaseFixture, Cluster_singleTrackSingleReleaseSingleCluster)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrackFile" };
|
||||
ScopedTrack track{ session };
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
|
||||
{
|
||||
@@ -345,7 +345,7 @@ namespace lms::db::tests
|
||||
|
||||
TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiClusters)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrackFile" };
|
||||
ScopedTrack track{ session };
|
||||
ScopedArtist artist{ session, "MyArtist" };
|
||||
ScopedClusterType clusterType{ session, "MyType" };
|
||||
ScopedCluster cluster1{ session, clusterType.lockAndGet(), "Cluster1" };
|
||||
@@ -406,7 +406,7 @@ namespace lms::db::tests
|
||||
|
||||
TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiRolesMultiClusters)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrackFile" };
|
||||
ScopedTrack track{ session };
|
||||
ScopedArtist artist{ session, "MyArtist" };
|
||||
ScopedClusterType clusterType{ session, "MyType" };
|
||||
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
|
||||
@@ -450,7 +450,7 @@ namespace lms::db::tests
|
||||
|
||||
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() };
|
||||
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)
|
||||
{
|
||||
tracks.emplace_back(session, "MyTrack" + std::to_string(i));
|
||||
tracks.emplace_back(session);
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
@@ -515,7 +515,7 @@ namespace lms::db::tests
|
||||
|
||||
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() };
|
||||
@@ -525,7 +525,7 @@ namespace lms::db::tests
|
||||
|
||||
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() };
|
||||
@@ -555,7 +555,7 @@ namespace lms::db::tests
|
||||
|
||||
TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtistSingleCluster)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedTrack track{ session };
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
ScopedArtist artist{ session, "MyArtist" };
|
||||
ScopedClusterType clusterType{ session, "MyType" };
|
||||
@@ -597,7 +597,7 @@ namespace lms::db::tests
|
||||
|
||||
TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtistMultiClusters)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedTrack track{ session };
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
ScopedArtist artist{ session, "MyArtist" };
|
||||
ScopedClusterType clusterType{ session, "MyClusterType" };
|
||||
@@ -636,7 +636,7 @@ namespace lms::db::tests
|
||||
|
||||
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() };
|
||||
|
||||
@@ -669,7 +669,7 @@ namespace lms::db::tests
|
||||
|
||||
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() };
|
||||
|
||||
@@ -729,7 +729,7 @@ namespace lms::db::tests
|
||||
std::list<ScopedTrack> tracks;
|
||||
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() };
|
||||
|
||||
@@ -744,7 +744,7 @@ namespace lms::db::tests
|
||||
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() };
|
||||
TrackArtistLink::create(session, tracks.back().get(), artist3.get(), TrackArtistLinkType::Artist);
|
||||
@@ -810,7 +810,7 @@ namespace lms::db::tests
|
||||
std::list<ScopedTrack> tracks;
|
||||
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() };
|
||||
|
||||
@@ -825,7 +825,7 @@ namespace lms::db::tests
|
||||
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() };
|
||||
tracks.back().get().modify()->setRelease(release3.get());
|
||||
|
||||
@@ -60,8 +60,8 @@ namespace lms::db::tests
|
||||
_tmpDb = std::make_unique<TmpDatabase>();
|
||||
{
|
||||
db::Session s{ _tmpDb->getDb() };
|
||||
s.prepareTables();
|
||||
s.analyze();
|
||||
s.prepareTablesIfNeeded();
|
||||
s.createIndexesIfNeeded();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace lms::db::tests
|
||||
|
||||
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() };
|
||||
|
||||
@@ -60,7 +60,7 @@ namespace lms::db::tests
|
||||
|
||||
TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtist)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedTrack track{ session };
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
ScopedArtist artist{ session, "MyArtist" };
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace lms::db::tests
|
||||
|
||||
TEST_F(DatabaseFixture, Listen_getAll)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedTrack track{ session };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
|
||||
{
|
||||
@@ -59,7 +59,7 @@ namespace lms::db::tests
|
||||
|
||||
TEST_F(DatabaseFixture, Listen_get)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedTrack track{ session };
|
||||
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}} };
|
||||
|
||||
@@ -93,7 +93,7 @@ namespace lms::db::tests
|
||||
|
||||
TEST_F(DatabaseFixture, Listen_get_multi)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedTrack track{ session };
|
||||
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 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)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedTrack track{ session };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
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} };
|
||||
@@ -145,7 +145,7 @@ namespace lms::db::tests
|
||||
|
||||
TEST_F(DatabaseFixture, Listen_getTopArtists)
|
||||
{
|
||||
ScopedTrack track1{ session, "MyTrack" };
|
||||
ScopedTrack track1{ session };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
const Wt::WDateTime dateTime1{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} };
|
||||
ScopedListen listen1{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime1 };
|
||||
@@ -162,7 +162,7 @@ namespace lms::db::tests
|
||||
EXPECT_EQ(artists.moreResults, false);
|
||||
}
|
||||
|
||||
ScopedTrack track2{ session, "MyTrack2" };
|
||||
ScopedTrack track2{ session };
|
||||
ScopedArtist artist1{ session, "MyArtist1" };
|
||||
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)
|
||||
{
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
ScopedTrack track1{ session, "MyTrack1" };
|
||||
ScopedTrack track1{ session };
|
||||
ScopedArtist artist1{ session, "MyArtist1" };
|
||||
ScopedTrack track2{ session, "MyTrack2" };
|
||||
ScopedTrack track2{ session };
|
||||
ScopedArtist artist2{ session, "MyArtist2" };
|
||||
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)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedTrack track{ session };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
ScopedArtist artist{ session, "MyArtist" };
|
||||
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)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedTrack track{ session };
|
||||
ScopedArtist artist{ session, "MyArtist" };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
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)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedTrack track{ session };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} };
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
@@ -453,8 +453,8 @@ namespace lms::db::tests
|
||||
|
||||
TEST_F(DatabaseFixture, Listen_getTopReleases_multi)
|
||||
{
|
||||
ScopedTrack track1{ session, "MyTrack" };
|
||||
ScopedTrack track2{ session, "MyTrack" };
|
||||
ScopedTrack track1{ session };
|
||||
ScopedTrack track2{ session };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} };
|
||||
ScopedListen listen1{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime };
|
||||
@@ -513,7 +513,7 @@ namespace lms::db::tests
|
||||
|
||||
TEST_F(DatabaseFixture, Listen_getTopReleases_cluster)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedTrack track{ session };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} };
|
||||
ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime };
|
||||
@@ -558,7 +558,7 @@ namespace lms::db::tests
|
||||
|
||||
TEST_F(DatabaseFixture, Listen_getTopReleases_mediaLibrary)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedTrack track{ session };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} };
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
@@ -615,7 +615,7 @@ namespace lms::db::tests
|
||||
|
||||
TEST_F(DatabaseFixture, Listen_getTopTracks)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedTrack track{ session };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
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)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedTrack track{ session };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
ScopedArtist artist{ session, "MyArtist" };
|
||||
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)
|
||||
{
|
||||
ScopedTrack track1{ session, "MyTrack1" };
|
||||
ScopedTrack track2{ session, "MyTrack2" };
|
||||
ScopedTrack track1{ session };
|
||||
ScopedTrack track2{ session };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} };
|
||||
ScopedListen listen1{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime };
|
||||
@@ -766,7 +766,7 @@ namespace lms::db::tests
|
||||
|
||||
TEST_F(DatabaseFixture, Listen_getTopTracks_cluster)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedTrack track{ session };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} };
|
||||
ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime };
|
||||
@@ -805,7 +805,7 @@ namespace lms::db::tests
|
||||
|
||||
TEST_F(DatabaseFixture, Listen_getTopTracks_mediaLibrary)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedTrack track{ session };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} };
|
||||
ScopedMediaLibrary library{ session };
|
||||
@@ -860,7 +860,7 @@ namespace lms::db::tests
|
||||
|
||||
TEST_F(DatabaseFixture, Listen_getRecentArtists)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedTrack track{ session };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
ScopedArtist artist{ session, "MyArtist" };
|
||||
|
||||
@@ -937,9 +937,9 @@ namespace lms::db::tests
|
||||
TEST_F(DatabaseFixture, Listen_getRecentArtists_multi)
|
||||
{
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
ScopedTrack track1{ session, "MyTrack1" };
|
||||
ScopedTrack track1{ session };
|
||||
ScopedArtist artist1{ session, "MyArtist1" };
|
||||
ScopedTrack track2{ session, "MyTrack2" };
|
||||
ScopedTrack track2{ session };
|
||||
ScopedArtist artist2{ session, "MyArtist2" };
|
||||
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)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedTrack track{ session };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
ScopedArtist artist{ session, "MyArtist" };
|
||||
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)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedTrack track{ session };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
ScopedArtist artist{ session, "MyArtist" };
|
||||
ScopedMediaLibrary library{ session };
|
||||
@@ -1086,7 +1086,7 @@ namespace lms::db::tests
|
||||
|
||||
TEST_F(DatabaseFixture, Listen_getRecentReleases)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedTrack track{ session };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
|
||||
@@ -1137,7 +1137,7 @@ namespace lms::db::tests
|
||||
|
||||
TEST_F(DatabaseFixture, Listen_getMostRecentRelease)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedTrack track{ session };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
|
||||
@@ -1189,8 +1189,8 @@ namespace lms::db::tests
|
||||
|
||||
TEST_F(DatabaseFixture, Listen_getRecentReleases_multi)
|
||||
{
|
||||
ScopedTrack track1{ session, "MyTrack1" };
|
||||
ScopedTrack track2{ session, "MyTrack2" };
|
||||
ScopedTrack track1{ session };
|
||||
ScopedTrack track2{ session };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
ScopedRelease release1{ session, "MyRelease1" };
|
||||
ScopedRelease release2{ session, "MyRelease2" };
|
||||
@@ -1265,7 +1265,7 @@ namespace lms::db::tests
|
||||
|
||||
TEST_F(DatabaseFixture, Listen_getRecentReleases_cluster)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedTrack track{ session };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
ScopedClusterType clusterType{ session, "MyType" };
|
||||
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
|
||||
@@ -1322,7 +1322,7 @@ namespace lms::db::tests
|
||||
|
||||
TEST_F(DatabaseFixture, Listen_getRecentReleases_mediaLibrary)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedTrack track{ session };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
ScopedMediaLibrary library{ session };
|
||||
@@ -1362,7 +1362,7 @@ namespace lms::db::tests
|
||||
|
||||
TEST_F(DatabaseFixture, Listen_getRecentTracks)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedTrack track{ session };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
|
||||
{
|
||||
@@ -1409,7 +1409,7 @@ namespace lms::db::tests
|
||||
|
||||
TEST_F(DatabaseFixture, Listen_getRecentTracks_mediaLibrary)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedTrack track{ session };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
ScopedMediaLibrary library{ session };
|
||||
ScopedMediaLibrary otherLibrary{ session };
|
||||
@@ -1448,7 +1448,7 @@ namespace lms::db::tests
|
||||
|
||||
TEST_F(DatabaseFixture, Listen_getCount_track)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedTrack track{ session };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
|
||||
{
|
||||
@@ -1482,8 +1482,8 @@ namespace lms::db::tests
|
||||
|
||||
TEST_F(DatabaseFixture, Listen_getCount_release)
|
||||
{
|
||||
ScopedTrack track1{ session, "MyTrack" };
|
||||
ScopedTrack track2{ session, "MyTrack" };
|
||||
ScopedTrack track1{ session };
|
||||
ScopedTrack track2{ session };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
|
||||
@@ -1530,7 +1530,7 @@ namespace lms::db::tests
|
||||
|
||||
TEST_F(DatabaseFixture, Listen_getMostRecentTrack)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedTrack track{ session };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
|
||||
{
|
||||
@@ -1576,8 +1576,8 @@ namespace lms::db::tests
|
||||
|
||||
TEST_F(DatabaseFixture, Listen_getRecentTracks_multi)
|
||||
{
|
||||
ScopedTrack track1{ session, "MyTrack1" };
|
||||
ScopedTrack track2{ session, "MyTrack2" };
|
||||
ScopedTrack track1{ session };
|
||||
ScopedTrack track2{ session };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
|
||||
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)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedTrack track{ session };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
const Wt::WDateTime dateTime{ Wt::WDate {2000, 1, 2}, Wt::WTime {12,0, 1} };
|
||||
ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime };
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedTrack track{ session };
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
@@ -136,7 +229,7 @@ namespace lms::db::tests
|
||||
|
||||
TEST_F(DatabaseFixture, Release_singleTrack_mediaLibrary)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedTrack track{ session };
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
ScopedMediaLibrary library{ session };
|
||||
ScopedMediaLibrary otherLibrary{ session };
|
||||
@@ -164,17 +257,17 @@ namespace lms::db::tests
|
||||
{
|
||||
ScopedRelease release1{ session, "MyRelease" };
|
||||
ScopedRelease release2{ session, "MyRelease" };
|
||||
ScopedTrack track1{ session, "MyTrack" };
|
||||
ScopedTrack track2{ session, "MyTrack" };
|
||||
ScopedTrack track1{ session };
|
||||
ScopedTrack track2{ session };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
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()->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" };
|
||||
|
||||
// filters does not work on orphans
|
||||
ScopedTrack track1{ session, "MyTrack" };
|
||||
ScopedTrack track2{ session, "MyTrack" };
|
||||
ScopedTrack track3{ session, "MyTrack" };
|
||||
ScopedTrack track4{ session, "MyTrack" };
|
||||
ScopedTrack track5{ session, "MyTrack" };
|
||||
ScopedTrack track6{ session, "MyTrack" };
|
||||
ScopedTrack track1{ session };
|
||||
ScopedTrack track2{ session };
|
||||
ScopedTrack track3{ session };
|
||||
ScopedTrack track4{ session };
|
||||
ScopedTrack track5{ session };
|
||||
ScopedTrack track6{ session };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
@@ -269,7 +362,7 @@ namespace lms::db::tests
|
||||
EXPECT_FALSE(release1->getTotalDisc());
|
||||
}
|
||||
|
||||
ScopedTrack track1{ session, "MyTrack" };
|
||||
ScopedTrack track1{ session };
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
@@ -298,7 +391,7 @@ namespace lms::db::tests
|
||||
EXPECT_EQ(*release1->getTotalDisc(), 6);
|
||||
}
|
||||
|
||||
ScopedTrack track2{ session, "MyTrack2" };
|
||||
ScopedTrack track2{ session };
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
@@ -323,7 +416,7 @@ namespace lms::db::tests
|
||||
EXPECT_FALSE(release2->getTotalDisc());
|
||||
}
|
||||
|
||||
ScopedTrack track3{ session, "MyTrack3" };
|
||||
ScopedTrack track3{ session };
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
@@ -350,10 +443,10 @@ namespace lms::db::tests
|
||||
ScopedRelease release1{ session, "MyRelease1" };
|
||||
ScopedRelease release2{ session, "MyRelease2" };
|
||||
|
||||
ScopedTrack track1A{ session, "MyTrack1A" };
|
||||
ScopedTrack track1B{ session, "MyTrack1B" };
|
||||
ScopedTrack track2A{ session, "MyTrack2A" };
|
||||
ScopedTrack track2B{ session, "MyTrack2B" };
|
||||
ScopedTrack track1A{ session };
|
||||
ScopedTrack track1B{ session };
|
||||
ScopedTrack track2A{ session };
|
||||
ScopedTrack track2B{ session };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
@@ -403,10 +496,10 @@ namespace lms::db::tests
|
||||
const Wt::WDate release1Date{ Wt::WDate {1994, 2, 3} };
|
||||
const Wt::WDate release1OriginalDate{ Wt::WDate {1993, 4, 5} };
|
||||
|
||||
ScopedTrack track1A{ session, "MyTrack1A" };
|
||||
ScopedTrack track1B{ session, "MyTrack1B" };
|
||||
ScopedTrack track2A{ session, "MyTrack2A" };
|
||||
ScopedTrack track2B{ session, "MyTrack2B" };
|
||||
ScopedTrack track1A{ session };
|
||||
ScopedTrack track1B{ session };
|
||||
ScopedTrack track2A{ session };
|
||||
ScopedTrack track2B{ session };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
@@ -455,10 +548,10 @@ namespace lms::db::tests
|
||||
const int release1Year{ 1994 };
|
||||
const int release1OriginalYear{ 1993 };
|
||||
|
||||
ScopedTrack track1A{ session, "MyTrack1A" };
|
||||
ScopedTrack track1B{ session, "MyTrack1B" };
|
||||
ScopedTrack track2A{ session, "MyTrack2A" };
|
||||
ScopedTrack track2B{ session, "MyTrack2B" };
|
||||
ScopedTrack track1A{ session };
|
||||
ScopedTrack track1B{ session };
|
||||
ScopedTrack track2A{ session };
|
||||
ScopedTrack track2B{ session };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
@@ -504,7 +597,7 @@ namespace lms::db::tests
|
||||
TEST_F(DatabaseFixture, Release_writtenAfter)
|
||||
{
|
||||
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} };
|
||||
|
||||
@@ -536,7 +629,7 @@ namespace lms::db::tests
|
||||
TEST_F(DatabaseFixture, Release_artist)
|
||||
{
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedTrack track{ session };
|
||||
ScopedArtist artist{ session, "MyArtist" };
|
||||
ScopedArtist artist2{ session, "MyArtist2" };
|
||||
{
|
||||
@@ -597,8 +690,8 @@ namespace lms::db::tests
|
||||
TEST_F(DatabaseFixture, Release_getDiscCount)
|
||||
{
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedTrack track2{ session, "MyTrack2" };
|
||||
ScopedTrack track{ session };
|
||||
ScopedTrack track2{ session };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
@@ -723,8 +816,8 @@ namespace lms::db::tests
|
||||
ScopedRelease release2{ session, "MyRelease2" };
|
||||
const Wt::WDate release2Date{ Wt::WDate {1994, 2, 3} };
|
||||
|
||||
ScopedTrack track1{ session, "MyTrack1" };
|
||||
ScopedTrack track2{ session, "MyTrack2" };
|
||||
ScopedTrack track1{ session };
|
||||
ScopedTrack track2{ session };
|
||||
|
||||
ASSERT_LT(release2Date, release1Date);
|
||||
ASSERT_GT(release2Date, release1OriginalDate);
|
||||
@@ -786,9 +879,9 @@ namespace lms::db::tests
|
||||
TEST_F(DatabaseFixture, Release_meanBitrate)
|
||||
{
|
||||
ScopedRelease release1{ session, "MyRelease1" };
|
||||
ScopedTrack track1{ session, "MyTrack1" };
|
||||
ScopedTrack track2{ session, "MyTrack2" };
|
||||
ScopedTrack track3{ session, "MyTrack3" };
|
||||
ScopedTrack track1{ session };
|
||||
ScopedTrack track2{ session };
|
||||
ScopedTrack track3{ session };
|
||||
|
||||
auto checkExpectedBitrate = [&](std::size_t bitrate)
|
||||
{
|
||||
@@ -820,4 +913,29 @@ namespace lms::db::tests
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ namespace lms::db::tests
|
||||
|
||||
TEST_F(DatabaseFixture, StarredTrack)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedTrack track{ session };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
ScopedUser user2{ session, "MyUser2" };
|
||||
|
||||
@@ -78,7 +78,7 @@ namespace lms::db::tests
|
||||
|
||||
TEST_F(DatabaseFixture, Starredtrack_PendingDestroy)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedTrack track{ session };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
ScopedStarredTrack starredTrack{ session, track.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
|
||||
|
||||
@@ -96,8 +96,8 @@ namespace lms::db::tests
|
||||
|
||||
TEST_F(DatabaseFixture, StarredTrack_dateTime)
|
||||
{
|
||||
ScopedTrack track1{ session, "MyTrack1" };
|
||||
ScopedTrack track2{ session, "MyTrack2" };
|
||||
ScopedTrack track1{ session };
|
||||
ScopedTrack track2{ session };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
|
||||
ScopedStarredTrack starredTrack1{ session, track1.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
|
||||
|
||||
@@ -39,7 +39,7 @@ namespace lms::db::tests
|
||||
}
|
||||
}
|
||||
|
||||
ScopedTrack track{ session, "MyTrackFile" };
|
||||
ScopedTrack track{ session };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
@@ -65,17 +65,23 @@ namespace lms::db::tests
|
||||
|
||||
TEST_F(DatabaseFixture, Track_findByRangedIdBased)
|
||||
{
|
||||
ScopedTrack track1{ session, "MyTrackFile1" };
|
||||
ScopedTrack track2{ session, "MyTrackFile1" };
|
||||
ScopedTrack track3{ session, "MyTrackFile1" };
|
||||
ScopedTrack track1{ session };
|
||||
ScopedTrack track2{ session };
|
||||
ScopedTrack track3{ session };
|
||||
ScopedMediaLibrary library{ session };
|
||||
ScopedMediaLibrary otherLibrary{ session };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track2.get().modify()->setMediaLibrary(library.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
bool moreResults;
|
||||
TrackId lastRetrievedTrackId;
|
||||
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);
|
||||
});
|
||||
@@ -83,45 +89,67 @@ namespace lms::db::tests
|
||||
EXPECT_EQ(visitedTracks[0]->getId(), track1.getId());
|
||||
EXPECT_EQ(visitedTracks[1]->getId(), track2.getId());
|
||||
EXPECT_EQ(visitedTracks[2]->getId(), track3.getId());
|
||||
EXPECT_FALSE(moreResults);
|
||||
EXPECT_EQ(lastRetrievedTrackId, track3.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
bool moreResults;
|
||||
TrackId lastRetrievedTrackId{ track1.getId() };
|
||||
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);
|
||||
});
|
||||
ASSERT_EQ(visitedTracks.size(), 1);
|
||||
EXPECT_EQ(visitedTracks[0]->getId(), track2.getId());
|
||||
EXPECT_TRUE(moreResults);
|
||||
EXPECT_EQ(lastRetrievedTrackId, track2.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
bool moreResults;
|
||||
TrackId lastRetrievedTrackId{ track1.getId() };
|
||||
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);
|
||||
});
|
||||
ASSERT_EQ(visitedTracks.size(), 0);
|
||||
EXPECT_TRUE(moreResults);
|
||||
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)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrackFile" };
|
||||
ScopedTrack track{ session };
|
||||
ScopedMediaLibrary library{ session };
|
||||
ScopedMediaLibrary otherLibrary{ session };
|
||||
|
||||
@@ -145,7 +173,7 @@ namespace lms::db::tests
|
||||
|
||||
TEST_F(DatabaseFixture, Track_noMediaLibrary)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrackFile" };
|
||||
ScopedTrack track{ session };
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
MediaLibrary::pointer mediaLibrary{ track->getMediaLibrary() };
|
||||
@@ -172,8 +200,8 @@ namespace lms::db::tests
|
||||
|
||||
TEST_F(DatabaseFixture, MultipleTracks)
|
||||
{
|
||||
ScopedTrack track1{ session, "MyTrackFile1" };
|
||||
ScopedTrack track2{ session, "MyTrackFile2" };
|
||||
ScopedTrack track1{ session };
|
||||
ScopedTrack track2{ session };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
@@ -186,12 +214,12 @@ namespace lms::db::tests
|
||||
|
||||
TEST_F(DatabaseFixture, MultipleTracksSearchByFilter)
|
||||
{
|
||||
ScopedTrack track1{ session, "" };
|
||||
ScopedTrack track2{ session, "" };
|
||||
ScopedTrack track3{ session, "" };
|
||||
ScopedTrack track4{ session, "" };
|
||||
ScopedTrack track5{ session, "" };
|
||||
ScopedTrack track6{ session, "" };
|
||||
ScopedTrack track1{ session };
|
||||
ScopedTrack track2{ session };
|
||||
ScopedTrack track3{ session };
|
||||
ScopedTrack track4{ session };
|
||||
ScopedTrack track5{ session };
|
||||
ScopedTrack track6{ session };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
@@ -232,7 +260,7 @@ namespace lms::db::tests
|
||||
|
||||
TEST_F(DatabaseFixture, Track_date)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedTrack track{ session };
|
||||
const Wt::WDate date{ 1995, 5, 5 };
|
||||
const Wt::WDate originalDate{ 1994, 2, 2 };
|
||||
{
|
||||
@@ -270,7 +298,7 @@ namespace lms::db::tests
|
||||
|
||||
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} };
|
||||
|
||||
@@ -297,4 +325,45 @@ namespace lms::db::tests
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ namespace lms::db::tests
|
||||
|
||||
TEST_F(DatabaseFixture, TrackBookmark)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedTrack track{ session };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
|
||||
{
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace lms::db::tests
|
||||
|
||||
TEST_F(DatabaseFixture, TrackFeatures)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedTrack track{ session };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
|
||||
{
|
||||
|
||||
@@ -44,7 +44,7 @@ namespace lms::db::tests
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
ScopedTrackList trackList1{ session, "MyTrackList1", 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() };
|
||||
@@ -79,7 +79,7 @@ namespace lms::db::tests
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
ScopedTrackList trackList2{ session, "MyTrackList2", 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() };
|
||||
@@ -131,7 +131,7 @@ namespace lms::db::tests
|
||||
|
||||
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() };
|
||||
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() };
|
||||
ScopedClusterType clusterType{ session, "MyClusterType" };
|
||||
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedTrack track{ session };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
@@ -187,8 +187,8 @@ namespace lms::db::tests
|
||||
{
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
ScopedTrackList trackList{ session, "MyTrackList", TrackListType::Playlist, false, user.lockAndGet() };
|
||||
ScopedTrack track1{ session, "MyTrack" };
|
||||
ScopedTrack track2{ session, "MyTrack" };
|
||||
ScopedTrack track1{ session };
|
||||
ScopedTrack track2{ session };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
|
||||
add_library(lmsimage SHARED
|
||||
impl/SvgImage.cpp
|
||||
)
|
||||
|
||||
target_include_directories(lmsimage INTERFACE
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -22,6 +22,7 @@
|
||||
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
||||
#include <stb_image_write.h>
|
||||
|
||||
#include "core/ITraceLogger.hpp"
|
||||
#include "image/Exception.hpp"
|
||||
#include "RawImage.hpp"
|
||||
|
||||
@@ -29,6 +30,8 @@ namespace lms::image::STB
|
||||
{
|
||||
JPEGImage::JPEGImage(const RawImage& rawImage, unsigned quality)
|
||||
{
|
||||
LMS_SCOPED_TRACE_DETAILED("Image", "WriteJPEG");
|
||||
|
||||
auto writeCb {[](void* ctx, void* writeData, int writeSize)
|
||||
{
|
||||
auto& output {*reinterpret_cast<std::vector<std::byte>*>(ctx)};
|
||||
|
||||
@@ -30,19 +30,21 @@
|
||||
#include <stb_image.h>
|
||||
#include <stb_image_resize.h>
|
||||
|
||||
#include "JPEGImage.hpp"
|
||||
|
||||
#include "core/ITraceLogger.hpp"
|
||||
#include "image/Exception.hpp"
|
||||
#include "JPEGImage.hpp"
|
||||
|
||||
namespace lms::image
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
std::unique_ptr<IRawImage> decodeImage(const std::filesystem::path& path)
|
||||
{
|
||||
LMS_SCOPED_TRACE_DETAILED("Image", "DecodeFile");
|
||||
return std::make_unique<STB::RawImage>(path);
|
||||
}
|
||||
|
||||
@@ -71,6 +73,8 @@ namespace lms::image::STB
|
||||
|
||||
void RawImage::resize(ImageSize width)
|
||||
{
|
||||
LMS_SCOPED_TRACE_DETAILED("Image", "Resize");
|
||||
|
||||
size_t height;
|
||||
if (_width == _height)
|
||||
{
|
||||
|
||||
@@ -28,6 +28,4 @@ namespace lms::image
|
||||
public:
|
||||
using LmsException::LmsException;
|
||||
};
|
||||
|
||||
} // namespace lms::cover
|
||||
|
||||
|
||||
@@ -36,4 +36,3 @@ namespace lms::image
|
||||
virtual std::string_view getMimeType() const = 0;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -19,9 +19,6 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
|
||||
#include "image/IEncodedImage.hpp"
|
||||
|
||||
namespace lms::image
|
||||
@@ -33,9 +30,5 @@ namespace lms::image
|
||||
virtual void resize(ImageSize width) = 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -145,6 +145,17 @@ namespace lms::metadata
|
||||
{
|
||||
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();
|
||||
_metaDataMap = audioFile->getMetaData();
|
||||
_hasEmbeddedCover = audioFile->hasAttachedPictures();
|
||||
|
||||
@@ -40,12 +40,9 @@ namespace lms::metadata
|
||||
void visitTagValues(std::string_view tag, TagValueVisitor visitor) const override;
|
||||
void visitPerformerTags(PerformerVisitor visitor) const override;
|
||||
bool hasEmbeddedCover() const override { return _hasEmbeddedCover; }
|
||||
const AudioProperties& getAudioProperties() const override { return _audioProperties; }
|
||||
|
||||
std::chrono::milliseconds getDuration() const override { return _containerInfo.duration; }
|
||||
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; }
|
||||
|
||||
AudioProperties _audioProperties;
|
||||
av::IAudioFile::MetadataMap _metaDataMap;
|
||||
av::ContainerInfo _containerInfo;
|
||||
bool _hasEmbeddedCover{};
|
||||
|
||||
@@ -153,10 +153,6 @@ namespace lms::metadata
|
||||
virtual void visitPerformerTags(PerformerVisitor visitor) const = 0;
|
||||
|
||||
virtual bool hasEmbeddedCover() 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;
|
||||
virtual const AudioProperties& getAudioProperties() const = 0;
|
||||
};
|
||||
} // namespace lms::metadata
|
||||
|
||||
@@ -235,18 +235,12 @@ namespace lms::metadata
|
||||
{
|
||||
auto track{ std::make_unique<Track>() };
|
||||
|
||||
processAudioProperties(tagReader, *track);
|
||||
track->audioProperties = tagReader.getAudioProperties();
|
||||
processTags(tagReader, *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)
|
||||
{
|
||||
track.hasCover = tagReader.hasEmbeddedCover();
|
||||
|
||||
@@ -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 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);
|
||||
|
||||
std::optional<Medium> getMedium(const ITagReader& tagReader);
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
|
||||
#include "TagLibTagReader.hpp"
|
||||
|
||||
#include <unordered_map>
|
||||
|
||||
#include <taglib/apeproperties.h>
|
||||
#include <taglib/apetag.h>
|
||||
#include <taglib/asffile.h>
|
||||
#include <taglib/id3v2tag.h>
|
||||
@@ -196,6 +199,8 @@ namespace lms::metadata
|
||||
throw ParsingFailedException{};
|
||||
}
|
||||
|
||||
computeAudioProperties();
|
||||
|
||||
_propertyMap = _file.file()->properties();
|
||||
|
||||
// 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
|
||||
{
|
||||
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
|
||||
|
||||
@@ -38,17 +38,16 @@ namespace lms::metadata
|
||||
TagLibTagReader(const TagLibTagReader&) = delete;
|
||||
TagLibTagReader& operator=(const TagLibTagReader&) = delete;
|
||||
|
||||
void computeAudioProperties();
|
||||
void visitTagValues(TagType tag, TagValueVisitor visitor) const override;
|
||||
void visitTagValues(std::string_view tag, TagValueVisitor visitor) const override;
|
||||
void visitPerformerTags(PerformerVisitor visitor) const override;
|
||||
bool hasEmbeddedCover() const override { return _hasEmbeddedCover; }
|
||||
|
||||
std::chrono::milliseconds getDuration() const override;
|
||||
std::size_t getBitrate() const override;
|
||||
std::size_t getBitsPerSample() const override;
|
||||
std::size_t getSampleRate() const override;
|
||||
const AudioProperties& getAudioProperties() const override { return _audioProperties; }
|
||||
|
||||
const TagLib::FileRef _file;
|
||||
AudioProperties _audioProperties;
|
||||
TagLib::PropertyMap _propertyMap; // case-insensitive keys
|
||||
bool _hasEmbeddedCover{};
|
||||
};
|
||||
|
||||
@@ -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
|
||||
{
|
||||
AudioProperties audioProperties;
|
||||
std::optional<core::UUID> mbid;
|
||||
std::optional<core::UUID> recordingMBID;
|
||||
std::string title;
|
||||
@@ -96,8 +106,6 @@ namespace lms::metadata
|
||||
std::vector<std::string> labels;
|
||||
std::vector<std::string> languages;
|
||||
Tags userExtraTags;
|
||||
std::chrono::milliseconds duration{};
|
||||
std::size_t bitrate{};
|
||||
std::optional<int> year{};
|
||||
Wt::WDate date;
|
||||
std::optional<int> originalYear{};
|
||||
|
||||
@@ -88,6 +88,16 @@ namespace lms::metadata
|
||||
|
||||
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->artistDisplayName, "MyArtist1 & MyArtist2");
|
||||
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].sortName, "MyArtist2SortName");
|
||||
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);
|
||||
EXPECT_EQ(track->composerArtists[0].name, "MyComposer1");
|
||||
EXPECT_EQ(track->composerArtists[0].sortName, "MyComposerSortOrder1");
|
||||
@@ -112,7 +121,6 @@ namespace lms::metadata
|
||||
EXPECT_EQ(track->date.year(), 2020);
|
||||
EXPECT_EQ(track->date.month(), 3);
|
||||
EXPECT_EQ(track->date.day(), 4);
|
||||
EXPECT_EQ(track->duration, TestTagReader::trackDuration);
|
||||
EXPECT_FALSE(track->hasCover);
|
||||
ASSERT_EQ(track->genres.size(), 2);
|
||||
EXPECT_EQ(track->genres[0], "Genre1");
|
||||
|
||||
@@ -28,10 +28,14 @@ namespace lms::metadata
|
||||
class TestTagReader : public ITagReader
|
||||
{
|
||||
public:
|
||||
static constexpr std::chrono::milliseconds trackDuration{ 180 };
|
||||
static constexpr std::size_t trackBitrate{ 128000 };
|
||||
static constexpr std::size_t trackBitsPerSample{ 16 };
|
||||
static constexpr std::size_t trackSampleRate{ 44000 };
|
||||
static constexpr AudioProperties audioProperties
|
||||
{
|
||||
.bitrate = 128000,
|
||||
.bitsPerSample = 16,
|
||||
.channelCount = 2,
|
||||
.duration = std::chrono::seconds{180},
|
||||
.sampleRate = 44000,
|
||||
};
|
||||
|
||||
using Tags = std::unordered_map<TagType, 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; };
|
||||
|
||||
std::chrono::milliseconds getDuration() const override { return trackDuration; }
|
||||
std::size_t getBitrate() const override { return trackBitrate; }
|
||||
std::size_t getBitsPerSample() const override { return trackBitsPerSample; }
|
||||
std::size_t getSampleRate() const override { return trackSampleRate; }
|
||||
const AudioProperties& getAudioProperties() const override { return audioProperties; }
|
||||
|
||||
private:
|
||||
const Tags _tags;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
|
||||
add_library(lmsservice-cover SHARED
|
||||
impl/ImageCache.cpp
|
||||
impl/CoverService.cpp
|
||||
)
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
#include "database/Track.hpp"
|
||||
|
||||
#include "image/Exception.hpp"
|
||||
#include "image/IRawImage.hpp"
|
||||
#include "image/Image.hpp"
|
||||
#include "core/IConfig.hpp"
|
||||
#include "core/ILogger.hpp"
|
||||
#include "core/Path.hpp"
|
||||
@@ -63,7 +63,7 @@ namespace lms::cover
|
||||
res = TrackInfo{};
|
||||
|
||||
res->hasCover = track->hasCover();
|
||||
res->trackPath = track->getPath();
|
||||
res->trackPath = track->getAbsoluteFilePath();
|
||||
|
||||
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;
|
||||
|
||||
CoverService::CoverService(db::Db& db,
|
||||
const std::filesystem::path& execPath,
|
||||
const std::filesystem::path& defaultCoverPath)
|
||||
const std::filesystem::path& defaultSvgCoverPath)
|
||||
: _db{ db }
|
||||
, _defaultCoverPath{ defaultCoverPath }
|
||||
, _maxCacheSize{ core::Service<core::IConfig>::get()->getULong("cover-max-cache-size", 30) * 1000 * 1000 }
|
||||
, _cache{ 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 }
|
||||
, _preferredFileNames{ constructPreferredFileNames() }
|
||||
, _artistFileNames{ constructArtistFileNames() }
|
||||
{
|
||||
setJpegQuality(core::Service<core::IConfig>::get()->getULong("cover-jpeg-quality", 75));
|
||||
|
||||
LMS_LOG(COVER, INFO, "Default cover path = '" << _defaultCoverPath.string() << "'");
|
||||
LMS_LOG(COVER, INFO, "Max cache size = " << _maxCacheSize);
|
||||
LMS_LOG(COVER, INFO, "Default cover path = '" << defaultSvgCoverPath.string() << "'");
|
||||
LMS_LOG(COVER, INFO, "Max cache size = " << _cache.getMaxCacheSize());
|
||||
LMS_LOG(COVER, INFO, "Max file size = " << _maxFileSize);
|
||||
LMS_LOG(COVER, INFO, "Preferred file names: " << core::stringUtils::joinStrings(_preferredFileNames, ","));
|
||||
|
||||
#if LMS_SUPPORT_IMAGE_GM
|
||||
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());
|
||||
}
|
||||
_defaultCover = image::readSvgFile(defaultSvgCoverPath); // may throw
|
||||
}
|
||||
|
||||
std::unique_ptr<IEncodedImage> CoverService::getFromAvMediaFile(const av::IAudioFile& input, ImageSize width) const
|
||||
@@ -189,27 +174,9 @@ namespace lms::cover
|
||||
return image;
|
||||
}
|
||||
|
||||
std::shared_ptr<IEncodedImage> CoverService::getDefault(ImageSize width)
|
||||
std::shared_ptr<IEncodedImage> CoverService::getDefaultSvgCover()
|
||||
{
|
||||
{
|
||||
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;
|
||||
}
|
||||
return _defaultCover;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
for (std::string_view filename : preferredFileNames)
|
||||
for (const std::string_view filename : preferredFileNames)
|
||||
{
|
||||
image = tryLoadImageFromFilename(filename);
|
||||
if (image)
|
||||
@@ -300,7 +267,7 @@ namespace lms::cover
|
||||
std::error_code ec;
|
||||
|
||||
std::filesystem::directory_iterator itPath(directoryPath, ec);
|
||||
std::filesystem::directory_iterator itEnd;
|
||||
const std::filesystem::directory_iterator itEnd;
|
||||
while (!ec && itPath != itEnd)
|
||||
{
|
||||
const std::filesystem::path& path{ *itPath };
|
||||
@@ -339,9 +306,9 @@ namespace lms::cover
|
||||
{
|
||||
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)
|
||||
return cover;
|
||||
|
||||
@@ -364,7 +331,7 @@ namespace lms::cover
|
||||
}
|
||||
|
||||
if (cover)
|
||||
saveToCache(cacheEntryDesc, cover);
|
||||
_cache.addImage(cacheEntryDesc, cover);
|
||||
|
||||
return cover;
|
||||
}
|
||||
@@ -372,9 +339,9 @@ namespace lms::cover
|
||||
std::shared_ptr<IEncodedImage> CoverService::getFromRelease(db::ReleaseId releaseId, ImageSize width)
|
||||
{
|
||||
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)
|
||||
return cover;
|
||||
|
||||
@@ -399,7 +366,7 @@ namespace lms::cover
|
||||
const Track::pointer& track{ tracks.results.front() };
|
||||
res = ReleaseInfo{};
|
||||
res->firstTrackId = track->getId();
|
||||
res->releaseDirectory = track->getPath().parent_path();
|
||||
res->releaseDirectory = track->getAbsoluteFilePath().parent_path();
|
||||
}
|
||||
|
||||
return res;
|
||||
@@ -413,7 +380,7 @@ namespace lms::cover
|
||||
}
|
||||
|
||||
if (cover)
|
||||
saveToCache(cacheEntryDesc, cover);
|
||||
_cache.addImage(cacheEntryDesc, cover);
|
||||
|
||||
return cover;
|
||||
}
|
||||
@@ -421,9 +388,9 @@ namespace lms::cover
|
||||
std::shared_ptr<IEncodedImage> CoverService::getFromArtist(db::ArtistId artistId, ImageSize width)
|
||||
{
|
||||
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)
|
||||
return artistImage;
|
||||
|
||||
@@ -457,9 +424,9 @@ namespace lms::cover
|
||||
|
||||
const auto releaseArtists{ Artist::findIds(session, artistFindParams) };
|
||||
if (releaseArtists.results.size() == 1)
|
||||
releasePaths.insert(track->getPath().parent_path());
|
||||
releasePaths.insert(track->getAbsoluteFilePath().parent_path());
|
||||
else
|
||||
multiArtistReleasePaths.insert(track->getPath().parent_path());
|
||||
multiArtistReleasePaths.insert(track->getAbsoluteFilePath().parent_path());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -513,20 +480,13 @@ namespace lms::cover
|
||||
}
|
||||
|
||||
if (artistImage)
|
||||
saveToCache(cacheEntryDesc, artistImage);
|
||||
_cache.addImage(cacheEntryDesc, artistImage);
|
||||
|
||||
return artistImage;
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -536,35 +496,5 @@ namespace lms::cover
|
||||
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
|
||||
|
||||
|
||||
@@ -19,19 +19,14 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <filesystem>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <shared_mutex>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
#include "services/cover/ICoverService.hpp"
|
||||
#include "image/IEncodedImage.hpp"
|
||||
#include "database/Types.hpp"
|
||||
#include "ImageCache.hpp"
|
||||
|
||||
namespace lms::db
|
||||
{
|
||||
@@ -43,57 +38,21 @@ namespace lms::av
|
||||
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
|
||||
{
|
||||
class CoverService : public ICoverService
|
||||
{
|
||||
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& operator=(const CoverService&) = delete;
|
||||
|
||||
private:
|
||||
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> 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 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> 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;
|
||||
|
||||
std::shared_mutex _cacheMutex;
|
||||
std::unordered_map<CacheEntryDesc, std::shared_ptr<image::IEncodedImage>> _cache;
|
||||
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{};
|
||||
ImageCache _cache;
|
||||
std::shared_ptr<image::IEncodedImage> _defaultCover;
|
||||
|
||||
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
|
||||
const std::size_t _maxFileSize;
|
||||
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> 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 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
|
||||
|
||||
|
||||
@@ -3,8 +3,10 @@ add_library(lmsscanner SHARED
|
||||
impl/ScannerService.cpp
|
||||
impl/ScannerStats.cpp
|
||||
impl/ScanStepCheckDuplicatedDbFiles.cpp
|
||||
impl/ScanStepCompact.cpp
|
||||
impl/ScanStepComputeClusterStats.cpp
|
||||
impl/ScanStepDiscoverFiles.cpp
|
||||
impl/ScanStepOptimize.cpp
|
||||
impl/ScanStepRemoveOrphanDbFiles.cpp
|
||||
impl/ScanStepScanFiles.cpp
|
||||
)
|
||||
|
||||
@@ -19,9 +19,10 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "core/LiteralString.hpp"
|
||||
#include "services/scanner/ScannerOptions.hpp"
|
||||
#include "services/scanner/ScannerStats.hpp"
|
||||
|
||||
namespace lms::scanner
|
||||
@@ -32,11 +33,11 @@ namespace lms::scanner
|
||||
virtual ~IScanStep() = default;
|
||||
|
||||
virtual ScanStep getStep() const = 0;
|
||||
virtual std::string_view getStepName() const = 0;
|
||||
virtual core::LiteralString getStepName() const = 0;
|
||||
|
||||
struct ScanContext
|
||||
{
|
||||
const bool forceScan;
|
||||
ScanOptions scanOptions;
|
||||
ScanStats stats;
|
||||
ScanStepStats currentStepStats;
|
||||
};
|
||||
|
||||
@@ -45,7 +45,7 @@ namespace lms::scanner
|
||||
const Track::pointer track{ Track::find(session, trackId) };
|
||||
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.currentStepStats.processedElems++;
|
||||
_progressCallback(context.currentStepStats);
|
||||
|
||||
@@ -29,8 +29,8 @@ namespace lms::scanner
|
||||
using ScanStepBase::ScanStepBase;
|
||||
|
||||
private:
|
||||
std::string_view getStepName() const override { return "Checking for duplicated files"; }
|
||||
ScanStep getStep() const override { return ScanStep::CheckingForDuplicateFiles; }
|
||||
core::LiteralString getStepName() const override { return "Check for duplicated files"; }
|
||||
ScanStep getStep() const override { return ScanStep::CheckForDuplicateFiles; }
|
||||
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++;
|
||||
_progressCallback(context.currentStepStats);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -30,7 +30,7 @@ namespace lms::scanner
|
||||
|
||||
private:
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -29,8 +29,8 @@ namespace lms::scanner
|
||||
using ScanStepBase::ScanStepBase;
|
||||
|
||||
private:
|
||||
ScanStep getStep() const override { return ScanStep::DiscoveringFiles; }
|
||||
std::string_view getStepName() const override { return "DiscoveringFiles"; }
|
||||
ScanStep getStep() const override { return ScanStep::DiscoverFiles; }
|
||||
core::LiteralString getStepName() const override { return "Discover files"; }
|
||||
void process(ScanContext& context) override;
|
||||
};
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user