Merge branch 'develop' for release v3.44.0

This commit is contained in:
emeric
2023-11-13 16:08:18 +01:00
134 changed files with 6287 additions and 6664 deletions
+16 -14
View File
@@ -85,6 +85,7 @@
<message id="Lms.Admin.ScannerController.status-in-progress">Scanning: step {1}/{2}</message> <message id="Lms.Admin.ScannerController.status-in-progress">Scanning: step {1}/{2}</message>
<message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">Checking for duplicate files... {1} files</message> <message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">Checking for duplicate files... {1} files</message>
<message id="Lms.Admin.ScannerController.step-checking-for-missing-files">Checking files... {1}%</message> <message id="Lms.Admin.ScannerController.step-checking-for-missing-files">Checking files... {1}%</message>
<message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Computing stats... {1}%</message>
<message id="Lms.Admin.ScannerController.step-discovering-files">Discovering files: {1} files</message> <message id="Lms.Admin.ScannerController.step-discovering-files">Discovering files: {1} files</message>
<message id="Lms.Admin.ScannerController.step-fetching-track-features">Fetching track features from AcousticBrainz: {1}/{2} tracks ({3}%)...</message> <message id="Lms.Admin.ScannerController.step-fetching-track-features">Fetching track features from AcousticBrainz: {1}/{2} tracks ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">Reloading similarity engine: {1}%...</message> <message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">Reloading similarity engine: {1}%...</message>
@@ -244,9 +245,10 @@
<message id="Lms.Settings.backend.listenbrainz">ListenBrainz</message> <message id="Lms.Settings.backend.listenbrainz">ListenBrainz</message>
<message id="Lms.Settings.backend.listenbrainz-token">ListenBrainz API token</message> <message id="Lms.Settings.backend.listenbrainz-token">ListenBrainz API token</message>
<message id="Lms.Settings.change-password">Change password</message> <message id="Lms.Settings.change-password">Change password</message>
<message id="Lms.Settings.default-transcode-bitrate">Default transcode bitrate</message> <message id="Lms.Settings.default-transcoding-output-bitrate">Default transcoding output bitrate</message>
<message id="Lms.Settings.default-transcode-format">Default transcode format</message> <message id="Lms.Settings.default-transcoding-output-format">Default transcoding output format</message>
<message id="Lms.Settings.demo-cannot-save">Cannot save using a demo account!</message> <message id="Lms.Settings.demo-cannot-save">Cannot save using a demo account!</message>
<message id="Lms.Settings.enable-transcoding-by-default">Enable transcoding by default</message>
<message id="Lms.Settings.feedback">Feedback</message> <message id="Lms.Settings.feedback">Feedback</message>
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Settings</message> <message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Settings</message>
<message id="Lms.Settings.password-bad">Bad password</message> <message id="Lms.Settings.password-bad">Bad password</message>
@@ -266,18 +268,18 @@
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">Album artists</message> <message id="Lms.Settings.subsonic-artist-list-mode.release-artists">Album artists</message>
<message id="Lms.Settings.subsonic-artist-list-mode.track-artists">Track artists</message> <message id="Lms.Settings.subsonic-artist-list-mode.track-artists">Track artists</message>
<message id="Lms.Settings.subsonic-api">Subsonic API</message> <message id="Lms.Settings.subsonic-api">Subsonic API</message>
<message id="Lms.Settings.transcode">Transcoding</message> <message id="Lms.Settings.transcoding">Transcoding</message>
<message id="Lms.Settings.transcode-bitrate">Transcode bitrate</message> <message id="Lms.Settings.transcoding-output-bitrate">Transcoding output bitrate</message>
<message id="Lms.Settings.transcode-format">Transcode format</message> <message id="Lms.Settings.transcoding-output-format">Transcoding output format</message>
<message id="Lms.Settings.transcode-format.matroska_opus">Matroska/Opus</message> <message id="Lms.Settings.transcoding-output-format.matroska_opus">Matroska/Opus</message>
<message id="Lms.Settings.transcode-format.mp3">MP3</message> <message id="Lms.Settings.transcoding-output-format.mp3">MP3</message>
<message id="Lms.Settings.transcode-format.ogg_opus">Ogg/Opus</message> <message id="Lms.Settings.transcoding-output-format.ogg_opus">Ogg/Opus</message>
<message id="Lms.Settings.transcode-format.ogg_vorbis">Ogg/Vorbis</message> <message id="Lms.Settings.transcoding-output-format.ogg_vorbis">Ogg/Vorbis</message>
<message id="Lms.Settings.transcode-format.webm_vorbis">WebM/Vorbis</message> <message id="Lms.Settings.transcoding-output-format.webm_vorbis">WebM/Vorbis</message>
<message id="Lms.Settings.transcode-mode">Enable transcoding</message> <message id="Lms.Settings.transcoding-mode">Enable transcoding</message>
<message id="Lms.Settings.transcode-mode.always">Always</message> <message id="Lms.Settings.transcoding-mode.always">Always</message>
<message id="Lms.Settings.transcode-mode.if-format-not-supported">Only when format is not supported</message> <message id="Lms.Settings.transcoding-mode.if-format-not-supported">Only when format is not supported by the browser</message>
<message id="Lms.Settings.transcode-mode.never">Never</message> <message id="Lms.Settings.transcoding-mode.never">Never</message>
<message id="Lms.Settings.settings-saved">New settings saved!</message> <message id="Lms.Settings.settings-saved">New settings saved!</message>
</messages> </messages>
+16 -14
View File
@@ -85,6 +85,7 @@
<message id="Lms.Admin.ScannerController.status-in-progress">En cours de scan : étape {1}/{2}</message> <message id="Lms.Admin.ScannerController.status-in-progress">En cours de scan : étape {1}/{2}</message>
<message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">Vérification des fichiers dupliqués... {1} fichiers</message> <message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">Vérification des fichiers dupliqués... {1} fichiers</message>
<message id="Lms.Admin.ScannerController.step-checking-for-missing-files">Vérification des fichiers... {1}%</message> <message id="Lms.Admin.ScannerController.step-checking-for-missing-files">Vérification des fichiers... {1}%</message>
<message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Calcul des statistiques... {1}%</message>
<message id="Lms.Admin.ScannerController.step-discovering-files">Découverte des fichiers : {1} fichiers</message> <message id="Lms.Admin.ScannerController.step-discovering-files">Découverte des fichiers : {1} fichiers</message>
<message id="Lms.Admin.ScannerController.step-fetching-track-features">Récupération des métadonnées AcousticBrainz : {1}/{2} fichiers ({3}%)...</message> <message id="Lms.Admin.ScannerController.step-fetching-track-features">Récupération des métadonnées AcousticBrainz : {1}/{2} fichiers ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">Rechargement du moteur de recommandation : {1}%...</message> <message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">Rechargement du moteur de recommandation : {1}%...</message>
@@ -244,9 +245,10 @@
<message id="Lms.Settings.backend.listenbrainz">ListenBrainz</message> <message id="Lms.Settings.backend.listenbrainz">ListenBrainz</message>
<message id="Lms.Settings.backend.listenbrainz-token">Jeton d'API ListenBrainz</message> <message id="Lms.Settings.backend.listenbrainz-token">Jeton d'API ListenBrainz</message>
<message id="Lms.Settings.change-password">Changement de mot de passe</message> <message id="Lms.Settings.change-password">Changement de mot de passe</message>
<message id="Lms.Settings.default-transcode-bitrate">Bitrate par defaut du transcodage</message> <message id="Lms.Settings.default-transcoding-output-bitrate">Bitrate par défaut du transcodage</message>
<message id="Lms.Settings.default-transcode-format">Format par defaut du transcodage</message> <message id="Lms.Settings.default-transcoding-output-format">Format par défaut du transcodage</message>
<message id="Lms.Settings.demo-cannot-save">Impossible de sauvegarder en utilisant un compte de démo !</message> <message id="Lms.Settings.demo-cannot-save">Impossible de sauvegarder en utilisant un compte de démo !</message>
<message id="Lms.Settings.enable-transcoding-by-default">Activer par défaut le transcodage</message>
<message id="Lms.Settings.feedback">Feedback</message> <message id="Lms.Settings.feedback">Feedback</message>
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Paramètres</message> <message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Paramètres</message>
<message id="Lms.Settings.password-bad">Mauvais mot de passe</message> <message id="Lms.Settings.password-bad">Mauvais mot de passe</message>
@@ -266,18 +268,18 @@
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">Tous les artistes d'album</message> <message id="Lms.Settings.subsonic-artist-list-mode.release-artists">Tous les artistes d'album</message>
<message id="Lms.Settings.subsonic-artist-list-mode.track-artists">Tous les artistes de piste</message> <message id="Lms.Settings.subsonic-artist-list-mode.track-artists">Tous les artistes de piste</message>
<message id="Lms.Settings.subsonic-api">API Subsonic</message> <message id="Lms.Settings.subsonic-api">API Subsonic</message>
<message id="Lms.Settings.transcode">Transcodage</message> <message id="Lms.Settings.transcoding">Transcodage</message>
<message id="Lms.Settings.transcode-bitrate">Bitrate du transcodage</message> <message id="Lms.Settings.transcoding-output-bitrate">Bitrate du transcodage</message>
<message id="Lms.Settings.transcode-format">Format du transcodage</message> <message id="Lms.Settings.transcoding-output-format">Format du transcodage</message>
<message id="Lms.Settings.transcode-format.matroska_opus">Matroska/Opus</message> <message id="Lms.Settings.transcoding-output-format.matroska_opus">Matroska/Opus</message>
<message id="Lms.Settings.transcode-format.mp3">MP3</message> <message id="Lms.Settings.transcoding-output-format.mp3">MP3</message>
<message id="Lms.Settings.transcode-format.ogg_opus">Ogg/Opus</message> <message id="Lms.Settings.transcoding-output-format.ogg_opus">Ogg/Opus</message>
<message id="Lms.Settings.transcode-format.ogg_vorbis">Ogg/Vorbis</message> <message id="Lms.Settings.transcoding-output-format.ogg_vorbis">Ogg/Vorbis</message>
<message id="Lms.Settings.transcode-format.webm_vorbis">WebM/Vorbis</message> <message id="Lms.Settings.transcoding-output-format.webm_vorbis">WebM/Vorbis</message>
<message id="Lms.Settings.transcode-mode">Appliquer le transcodage</message> <message id="Lms.Settings.transcoding-mode">Appliquer le transcodage</message>
<message id="Lms.Settings.transcode-mode.always">Toujours</message> <message id="Lms.Settings.transcoding-mode.always">Toujours</message>
<message id="Lms.Settings.transcode-mode.if-format-not-supported">Seulement lorsque le format n'est pas pris en charge</message> <message id="Lms.Settings.transcoding-mode.if-format-not-supported">Seulement lorsque le format n'est pas pris en charge par votre navigateur</message>
<message id="Lms.Settings.transcode-mode.never">Jamais</message> <message id="Lms.Settings.transcoding-mode.never">Jamais</message>
<message id="Lms.Settings.settings-saved">Paramètres sauvegardés !</message> <message id="Lms.Settings.settings-saved">Paramètres sauvegardés !</message>
<!--Wt--> <!--Wt-->
+14 -12
View File
@@ -85,6 +85,7 @@
<message id="Lms.Admin.ScannerController.status-in-progress">Scansione: passo {1}/{2}</message> <message id="Lms.Admin.ScannerController.status-in-progress">Scansione: passo {1}/{2}</message>
<message id="Lms.Admin.ScannerController.step-checking-for-missing-files">Controllo file... {1}%</message> <message id="Lms.Admin.ScannerController.step-checking-for-missing-files">Controllo file... {1}%</message>
<message id="Lms.Admin.ScannerController.step-discovering-files">File trovati: {1} files</message> <message id="Lms.Admin.ScannerController.step-discovering-files">File trovati: {1} files</message>
<message id="Lms.Admin.ScannerController.step-fetching-track-features">Recupero metadati da AcousticBrainz: {1}/{2} tracce ({3}%)...</message> <message id="Lms.Admin.ScannerController.step-fetching-track-features">Recupero metadati da AcousticBrainz: {1}/{2} tracce ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">Ricarica motore di tracce simili: {1}%...</message> <message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">Ricarica motore di tracce simili: {1}%...</message>
@@ -248,6 +249,7 @@
<message id="Lms.Settings.demo-cannot-save">Non posso salvare le impostazioni utilizzando l'account demo!</message> <message id="Lms.Settings.demo-cannot-save">Non posso salvare le impostazioni utilizzando l'account demo!</message>
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Impostazioni</message> <message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> Impostazioni</message>
<message id="Lms.Settings.password-bad">Password errata</message> <message id="Lms.Settings.password-bad">Password errata</message>
<message id="Lms.Settings.password-must-fill-old-password">Devi inserire la password attuale</message> <message id="Lms.Settings.password-must-fill-old-password">Devi inserire la password attuale</message>
@@ -266,18 +268,18 @@
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">Artisti album</message> <message id="Lms.Settings.subsonic-artist-list-mode.release-artists">Artisti album</message>
<message id="Lms.Settings.subsonic-artist-list-mode.track-artists">Artisti tracce</message> <message id="Lms.Settings.subsonic-artist-list-mode.track-artists">Artisti tracce</message>
<message id="Lms.Settings.subsonic-api">Subsonic API</message> <message id="Lms.Settings.subsonic-api">Subsonic API</message>
<message id="Lms.Settings.transcode">Transcodifica</message> <message id="Lms.Settings.transcoding">Transcodifica</message>
<message id="Lms.Settings.transcode-bitrate">Bitrate transcodifica</message> <message id="Lms.Settings.transcoding-output-bitrate">Bitrate transcodifica</message>
<message id="Lms.Settings.transcode-format">Formato di transcodifica</message> <message id="Lms.Settings.transcoding-output-format">Formato di transcodifica</message>
<message id="Lms.Settings.transcode-format.matroska_opus">Matroska/Opus</message> <message id="Lms.Settings.transcoding-output-format.matroska_opus">Matroska/Opus</message>
<message id="Lms.Settings.transcode-format.mp3">MP3</message> <message id="Lms.Settings.transcoding-output-format.mp3">MP3</message>
<message id="Lms.Settings.transcode-format.ogg_opus">Ogg/Opus</message> <message id="Lms.Settings.transcoding-output-format.ogg_opus">Ogg/Opus</message>
<message id="Lms.Settings.transcode-format.ogg_vorbis">Ogg/Vorbis</message> <message id="Lms.Settings.transcoding-output-format.ogg_vorbis">Ogg/Vorbis</message>
<message id="Lms.Settings.transcode-format.webm_vorbis">WebM/Vorbis</message> <message id="Lms.Settings.transcoding-output-format.webm_vorbis">WebM/Vorbis</message>
<message id="Lms.Settings.transcode-mode">Abilita transcodifica</message> <message id="Lms.Settings.transcoding-mode">Abilita transcodifica</message>
<message id="Lms.Settings.transcode-mode.always">Sempre</message> <message id="Lms.Settings.transcoding-mode.always">Sempre</message>
<message id="Lms.Settings.transcode-mode.if-format-not-supported">Quando il formato non è supportato</message> <message id="Lms.Settings.transcoding-mode.if-format-not-supported">Quando il formato non è supportato</message>
<message id="Lms.Settings.transcode-mode.never">Mai</message> <message id="Lms.Settings.transcoding-mode.never">Mai</message>
<message id="Lms.Settings.settings-saved">Nuove impostazioni salvate!</message> <message id="Lms.Settings.settings-saved">Nuove impostazioni salvate!</message>
<!--Wt--> <!--Wt-->
+14 -12
View File
@@ -85,6 +85,7 @@
<message id="Lms.Admin.ScannerController.status-in-progress">扫描中: 阶段 {1}/{2}</message> <message id="Lms.Admin.ScannerController.status-in-progress">扫描中: 阶段 {1}/{2}</message>
<message id="Lms.Admin.ScannerController.step-checking-for-missing-files">检查文件中... {1}%</message> <message id="Lms.Admin.ScannerController.step-checking-for-missing-files">检查文件中... {1}%</message>
<message id="Lms.Admin.ScannerController.step-discovering-files">检索文件中: {1} 文件</message> <message id="Lms.Admin.ScannerController.step-discovering-files">检索文件中: {1} 文件</message>
<message id="Lms.Admin.ScannerController.step-fetching-track-features">从 AcousticBrainz 获取音轨特征: {1}/{2} 音轨 ({3}%)...</message> <message id="Lms.Admin.ScannerController.step-fetching-track-features">从 AcousticBrainz 获取音轨特征: {1}/{2} 音轨 ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">重载相似引擎中 {1}%...</message> <message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">重载相似引擎中 {1}%...</message>
@@ -248,6 +249,7 @@
<message id="Lms.Settings.demo-cannot-save">使用演示账号时不能保存!</message> <message id="Lms.Settings.demo-cannot-save">使用演示账号时不能保存!</message>
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> 设置</message> <message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> 设置</message>
<message id="Lms.Settings.password-bad">无效密码</message> <message id="Lms.Settings.password-bad">无效密码</message>
<message id="Lms.Settings.password-must-fill-old-password">必须填写旧密码</message> <message id="Lms.Settings.password-must-fill-old-password">必须填写旧密码</message>
@@ -266,18 +268,18 @@
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">专辑歌手</message> <message id="Lms.Settings.subsonic-artist-list-mode.release-artists">专辑歌手</message>
<message id="Lms.Settings.subsonic-artist-list-mode.track-artists">音轨歌手</message> <message id="Lms.Settings.subsonic-artist-list-mode.track-artists">音轨歌手</message>
<message id="Lms.Settings.subsonic-api">Subsonic 应用程序接口</message> <message id="Lms.Settings.subsonic-api">Subsonic 应用程序接口</message>
<message id="Lms.Settings.transcode">转码</message> <message id="Lms.Settings.transcoding">转码</message>
<message id="Lms.Settings.transcode-bitrate">转码比特率</message> <message id="Lms.Settings.transcoding-output-bitrate">转码比特率</message>
<message id="Lms.Settings.transcode-format">转码格式</message> <message id="Lms.Settings.transcoding-output-format">转码格式</message>
<message id="Lms.Settings.transcode-format.matroska_opus">Matroska/Opus</message> <message id="Lms.Settings.transcoding-output-format.matroska_opus">Matroska/Opus</message>
<message id="Lms.Settings.transcode-format.mp3">MP3</message> <message id="Lms.Settings.transcoding-output-format.mp3">MP3</message>
<message id="Lms.Settings.transcode-format.ogg_opus">Ogg/Opus</message> <message id="Lms.Settings.transcoding-output-format.ogg_opus">Ogg/Opus</message>
<message id="Lms.Settings.transcode-format.ogg_vorbis">Ogg/Vorbis</message> <message id="Lms.Settings.transcoding-output-format.ogg_vorbis">Ogg/Vorbis</message>
<message id="Lms.Settings.transcode-format.webm_vorbis">WebM/Vorbis</message> <message id="Lms.Settings.transcoding-output-format.webm_vorbis">WebM/Vorbis</message>
<message id="Lms.Settings.transcode-mode">启动转码</message> <message id="Lms.Settings.transcoding-mode">启动转码</message>
<message id="Lms.Settings.transcode-mode.always">总是</message> <message id="Lms.Settings.transcoding-mode.always">总是</message>
<message id="Lms.Settings.transcode-mode.if-format-not-supported">仅当格式不支持时使用</message> <message id="Lms.Settings.transcoding-mode.if-format-not-supported">仅当格式不支持时使用</message>
<message id="Lms.Settings.transcode-mode.never">从不</message> <message id="Lms.Settings.transcoding-mode.never">从不</message>
<message id="Lms.Settings.settings-saved">新设置已保存!</message> <message id="Lms.Settings.settings-saved">新设置已保存!</message>
<!--Wt--> <!--Wt-->
+34 -23
View File
@@ -10,33 +10,33 @@
${tr:Lms.Settings.audio-settings-are-local} ${tr:Lms.Settings.audio-settings-are-local}
</div> </div>
<div class="col-lg-4"> <div class="col-lg-4">
<label class="form-label" for="${id:transcode-mode}"> <label class="form-label" for="${id:transcoding-mode}">
${tr:Lms.Settings.transcode-mode} ${tr:Lms.Settings.transcoding-mode}
</label> </label>
${transcode-mode class="form-control"} ${transcoding-mode class="form-control"}
<div class="invalid-feedback"> <div class="invalid-feedback">
${transcode-mode-info} ${transcoding-mode-info}
</div> </div>
</div> </div>
<div class="col-lg-4"> <div class="col-lg-4">
<label class="form-label" for="${id:transcode-format}"> <label class="form-label" for="${id:transcoding-output-format}">
${tr:Lms.Settings.transcode-format} ${tr:Lms.Settings.transcoding-output-format}
</label> </label>
${transcode-format class="form-control"} ${transcoding-output-format class="form-control"}
<div class="invalid-feedback"> <div class="invalid-feedback">
${transcode-format-info} ${transcoding-output-format-info}
</div> </div>
</div> </div>
<div class="col-lg-4"> <div class="col-lg-4">
<label class="form-label" for="${id:transcode-bitrate}"> <label class="form-label" for="${id:transcoding-output-bitrate}">
${tr:Lms.Settings.transcode-bitrate} ${tr:Lms.Settings.transcoding-output-bitrate}
</label> </label>
<div class="input-group mb-3"> <div class="input-group mb-3">
${transcode-bitrate class="form-control"} ${transcoding-output-bitrate class="form-control"}
<span class="input-group-text">kbps</span> <span class="input-group-text">kbps</span>
</div> </div>
<div class="invalid-feedback"> <div class="invalid-feedback">
${transcode-bitrate-info} ${transcoding-output-bitrate-info}
</div> </div>
</div> </div>
<div class="col-lg-4"> <div class="col-lg-4">
@@ -74,25 +74,36 @@
</div> </div>
${<if-has-subsonic-api>} ${<if-has-subsonic-api>}
<legend>${tr:Lms.Settings.subsonic-api}</legend> <legend>${tr:Lms.Settings.subsonic-api}</legend>
<div class="col-lg-6"> <div class="col-12">
<label class="form-label" for="${id:subsonic-transcode-format}"> <div class="form-check">
${tr:Lms.Settings.default-transcode-format} ${subsonic-enable-transcoding-by-default class="form-check-input"}
</label> <label class="form-check-label" for="${id:subsonic-enable-transcoding-by-default}">
${subsonic-transcode-format class="form-control"} ${tr:Lms.Settings.enable-transcoding-by-default}
<div class="invalid-feedback"> </label>
${subsonic-transcode-format-info class="help-block"} <div class="invalid-feedback">
${subsonic-enable-transcoding-by-default-info}
</div>
</div> </div>
</div> </div>
<div class="col-lg-6"> <div class="col-lg-6">
<label class="form-label" for="${id:subsonic-transcode-bitrate}"> <label class="form-label" for="${id:subsonic-transcoding-output-format}">
${tr:Lms.Settings.default-transcode-bitrate} ${tr:Lms.Settings.default-transcoding-output-format}
</label>
${subsonic-transcoding-output-format class="form-control"}
<div class="invalid-feedback">
${subsonic-transcoding-output-format-info class="help-block"}
</div>
</div>
<div class="col-lg-6">
<label class="form-label" for="${id:subsonic-transcoding-output-bitrate}">
${tr:Lms.Settings.default-transcoding-output-bitrate}
</label> </label>
<div class="input-group mb-3"> <div class="input-group mb-3">
${subsonic-transcode-bitrate class="form-control"} ${subsonic-transcoding-output-bitrate class="form-control"}
<span class="input-group-text">kbps</span> <span class="input-group-text">kbps</span>
</div> </div>
<div class="invalid-feedback"> <div class="invalid-feedback">
${subsonic-transcode-bitrate-info class="help-block"} ${subsonic-transcoding-output-bitrate-info class="help-block"}
</div> </div>
</div> </div>
<div class="col-12"> <div class="col-12">
+4 -1
View File
@@ -61,7 +61,10 @@ api-subsonic = true;
# Use this list to make the reported server version to 1.12.0 depending on the client's name # Use this list to make the reported server version to 1.12.0 depending on the client's name
# Main usage is to make auto detections for the 'p' (password) parameter work # Main usage is to make auto detections for the 'p' (password) parameter work
api-subsonic-report-old-server-protocol = ("DSub"); api-subsonic-old-server-protocol-clients = ("DSub");
# List of clients for whom a default cover is served (as they do not have their own)
api-subsonic-default-cover-clients = ("DSub", "substreamer");
# List of clients for whom open subsonic extensions and extra fields are disabled # List of clients for whom open subsonic extensions and extra fields are disabled
api-open-subsonic-disabled-clients = ("DSub"); api-open-subsonic-disabled-clients = ("DSub");
+12 -12
View File
@@ -2,15 +2,15 @@
var LMS = LMS || {}; var LMS = LMS || {};
// Keep in sync with MediaPlayer::TranscodeMode cpp // Keep in sync with MediaPlayer::TranscodingMode cpp
const TranscodeMode = { const TranscodingMode = {
Never: 0, Never: 0,
Always: 1, Always: 1,
IfFormatNotSupported: 2, IfFormatNotSupported: 2,
} }
const Mode = { const Mode = {
Transcode: 1, Transcoding: 1,
File: 2, File: 2,
} }
Object.freeze(Mode); Object.freeze(Mode);
@@ -22,7 +22,7 @@ LMS.mediaplayer = function () {
let _trackId = null; let _trackId = null;
let _duration = 0; let _duration = 0;
let _audioNativeSrc; let _audioNativeSrc;
let _audioTranscodeSrc; let _audioTranscodingSrc;
let _settings = {}; let _settings = {};
let _playedDuration = 0; let _playedDuration = 0;
let _lastStartPlaying = null; let _lastStartPlaying = null;
@@ -236,10 +236,10 @@ LMS.mediaplayer = function () {
let selectedOffset = parseInt(_elems.seek.value, 10); let selectedOffset = parseInt(_elems.seek.value, 10);
switch (mode) { switch (mode) {
case Mode.Transcode: case Mode.Transcoding:
_offset = selectedOffset; _offset = selectedOffset;
_removeAudioSources(); _removeAudioSources();
_addAudioSource(_audioTranscodeSrc + "&offset=" + _offset); _addAudioSource(_audioTranscodingSrc + "&offset=" + _offset);
_elems.audio.load(); _elems.audio.load();
_elems.audio.currentTime = 0; _elems.audio.currentTime = 0;
_playTrack(); _playTrack();
@@ -270,7 +270,7 @@ LMS.mediaplayer = function () {
}); });
_elems.audio.addEventListener("canplay", function() { _elems.audio.addEventListener("canplay", function() {
if (_getAudioMode() == Mode.Transcode) { if (_getAudioMode() == Mode.Transcoding) {
_elems.transcodingActive.style.display = "inline"; _elems.transcodingActive.style.display = "inline";
} }
else { else {
@@ -337,7 +337,7 @@ LMS.mediaplayer = function () {
let _getAudioMode = function() { let _getAudioMode = function() {
if (_elems.audio.currentSrc) { if (_elems.audio.currentSrc) {
if (_elems.audio.currentSrc.includes("format")) if (_elems.audio.currentSrc.includes("format"))
return Mode.Transcode; return Mode.Transcoding;
else else
return Mode.File; return Mode.File;
} }
@@ -353,19 +353,19 @@ LMS.mediaplayer = function () {
_offset = 0; _offset = 0;
_duration = params.duration; _duration = params.duration;
_audioNativeSrc = params.nativeResource; _audioNativeSrc = params.nativeResource;
_audioTranscodeSrc = params.transcodeResource + "&bitrate=" + _settings.transcode.bitrate + "&format=" + _settings.transcode.format; _audioTranscodingSrc = params.transcodingResource + "&bitrate=" + _settings.transcoding.bitrate + "&format=" + _settings.transcoding.format;
_elems.seek.max = _duration; _elems.seek.max = _duration;
_removeAudioSources(); _removeAudioSources();
// ! order is important // ! order is important
if (_settings.transcode.mode == TranscodeMode.Never || _settings.transcode.mode == TranscodeMode.IfFormatNotSupported) if (_settings.transcoding.mode == TranscodingMode.Never || _settings.transcoding.mode == TranscodingMode.IfFormatNotSupported)
{ {
_addAudioSource(_audioNativeSrc); _addAudioSource(_audioNativeSrc);
} }
if (_settings.transcode.mode == TranscodeMode.Always || _settings.transcode.mode == TranscodeMode.IfFormatNotSupported) if (_settings.transcoding.mode == TranscodingMode.Always || _settings.transcoding.mode == TranscodingMode.IfFormatNotSupported)
{ {
_addAudioSource(_audioTranscodeSrc); _addAudioSource(_audioTranscodingSrc);
} }
_elems.audio.load(); _elems.audio.load();
+2 -2
View File
@@ -1,9 +1,9 @@
add_library(lmsav SHARED add_library(lmsav SHARED
impl/AudioFile.cpp impl/AudioFile.cpp
impl/RawResourceHandlerCreator.cpp
impl/Transcoder.cpp impl/Transcoder.cpp
impl/TranscodeResourceHandler.cpp impl/TranscodingResourceHandler.cpp
impl/Types.cpp
) )
target_include_directories(lmsav INTERFACE target_include_directories(lmsav INTERFACE
+243 -215
View File
@@ -32,267 +32,295 @@ extern "C"
#include "utils/Logger.hpp" #include "utils/Logger.hpp"
#include "utils/String.hpp" #include "utils/String.hpp"
namespace Av { namespace Av
static std::string averror_to_string(int error)
{ {
std::array<char, 128> buf = {0}; namespace
{
std::string averror_to_string(int error)
{
std::array<char, 128> buf = { 0 };
if (::av_strerror(error, buf.data(), buf.size()) == 0) if (::av_strerror(error, buf.data(), buf.size()) == 0)
return &buf[0]; return &buf[0];
else else
return "Unknown error"; return "Unknown error";
} }
class AudioFileException : public Av::Exception class AudioFileException : public Av::Exception
{ {
public: public:
AudioFileException(int avError) AudioFileException(int avError)
: Av::Exception {"AudioFileException: " + averror_to_string(avError)} : Av::Exception{ "AudioFileException: " + averror_to_string(avError) }
{} {}
}; };
std::unique_ptr<IAudioFile> void getMetaDataFromDictionnary(AVDictionary* dictionnary, AudioFile::MetadataMap& res)
parseAudioFile(const std::filesystem::path& p) {
{ if (!dictionnary)
return std::make_unique<AudioFile>(p); return;
}
AudioFile::AudioFile(const std::filesystem::path& p) AVDictionaryEntry* tag = NULL;
: _p {p} while ((tag = ::av_dict_get(dictionnary, "", tag, AV_DICT_IGNORE_SUFFIX)))
{ {
int error {avformat_open_input(&_context, _p.string().c_str(), nullptr, nullptr)}; res[StringUtils::stringToUpper(tag->key)] = tag->value;
if (error < 0) }
{ }
LMS_LOG(AV, ERROR) << "Cannot open " << _p.string() << ": " << averror_to_string(error);
throw AudioFileException {error};
}
error = avformat_find_stream_info(_context, nullptr); DecodingCodec avcodecToDecodingCodec(AVCodecID codec)
if (error < 0) {
{ switch (codec)
LMS_LOG(AV, ERROR) << "Cannot find stream information on " << _p.string() << ": " << averror_to_string(error); {
avformat_close_input(&_context); case AV_CODEC_ID_MP3: return DecodingCodec::MP3;
throw AudioFileException {error}; case AV_CODEC_ID_AAC: return DecodingCodec::AAC;
} case AV_CODEC_ID_AC3: return DecodingCodec::AC3;
} case AV_CODEC_ID_VORBIS: return DecodingCodec::VORBIS;
case AV_CODEC_ID_WMAV1: return DecodingCodec::WMAV1;
case AV_CODEC_ID_WMAV2: return DecodingCodec::WMAV2;
case AV_CODEC_ID_FLAC: return DecodingCodec::FLAC;
case AV_CODEC_ID_ALAC: return DecodingCodec::ALAC;
case AV_CODEC_ID_WAVPACK: return DecodingCodec::WAVPACK;
case AV_CODEC_ID_MUSEPACK7: return DecodingCodec::MUSEPACK7;
case AV_CODEC_ID_MUSEPACK8: return DecodingCodec::MUSEPACK8;
case AV_CODEC_ID_APE: return DecodingCodec::APE;
case AV_CODEC_ID_EAC3: return DecodingCodec::EAC3;
case AV_CODEC_ID_MP4ALS: return DecodingCodec::MP4ALS;
case AV_CODEC_ID_OPUS: return DecodingCodec::OPUS;
case AV_CODEC_ID_SHORTEN: return DecodingCodec::SHORTEN;
default:
return DecodingCodec::UNKNOWN;
}
}
}
AudioFile::~AudioFile() std::unique_ptr<IAudioFile> parseAudioFile(const std::filesystem::path& p)
{ {
avformat_close_input(&_context); return std::make_unique<AudioFile>(p);
} }
const std::filesystem::path& AudioFile::AudioFile(const std::filesystem::path& p)
AudioFile::getPath() const : _p{ p }
{ {
return _p; int error{ avformat_open_input(&_context, _p.string().c_str(), nullptr, nullptr) };
} if (error < 0)
{
LMS_LOG(AV, ERROR) << "Cannot open " << _p.string() << ": " << averror_to_string(error);
throw AudioFileException{ error };
}
std::chrono::milliseconds error = avformat_find_stream_info(_context, nullptr);
AudioFile::getDuration() const if (error < 0)
{ {
if (_context->duration == AV_NOPTS_VALUE) LMS_LOG(AV, ERROR) << "Cannot find stream information on " << _p.string() << ": " << averror_to_string(error);
return std::chrono::milliseconds {0}; // TODO estimate avformat_close_input(&_context);
throw AudioFileException{ error };
}
}
return std::chrono::milliseconds {_context->duration / AV_TIME_BASE * 1000}; AudioFile::~AudioFile()
} {
avformat_close_input(&_context);
}
void const std::filesystem::path& AudioFile::getPath() const
getMetaDataFromDictionnary(AVDictionary* dictionnary, AudioFile::MetadataMap& res) {
{ return _p;
if (!dictionnary) }
return;
AVDictionaryEntry *tag = NULL; ContainerInfo AudioFile::getContainerInfo() const
while ((tag = ::av_dict_get(dictionnary, "", tag, AV_DICT_IGNORE_SUFFIX))) {
{ ContainerInfo info;
res[StringUtils::stringToUpper(tag->key)] = tag->value; info.bitrate = _context->bit_rate;
} info.duration = std::chrono::milliseconds{ _context->duration == AV_NOPTS_VALUE ? 0 : _context->duration / AV_TIME_BASE * 1000 };
} info.name = _context->iformat->name;
AudioFile::MetadataMap return info;
AudioFile::getMetaData() const }
{
MetadataMap res;
getMetaDataFromDictionnary(_context->metadata, res); AudioFile::MetadataMap AudioFile::getMetaData() const
{
MetadataMap res;
// HACK for OGG files getMetaDataFromDictionnary(_context->metadata, res);
// If we did not find tags, search metadata in streams
if (res.empty())
{
for (std::size_t i {}; i < _context->nb_streams; ++i)
{
getMetaDataFromDictionnary(_context->streams[i]->metadata, res);
if (!res.empty()) // HACK for OGG files
break; // If we did not find tags, search metadata in streams
} if (res.empty())
} {
for (std::size_t i{}; i < _context->nb_streams; ++i)
{
getMetaDataFromDictionnary(_context->streams[i]->metadata, res);
return res; if (!res.empty())
} break;
}
}
std::vector<StreamInfo> return res;
AudioFile::getStreamInfo() const }
{
std::vector<StreamInfo> res;
for (std::size_t i {}; i < _context->nb_streams; ++i) std::vector<StreamInfo> AudioFile::getStreamInfo() const
{ {
std::optional<StreamInfo> streamInfo {getStreamInfo(i)}; std::vector<StreamInfo> res;
if (streamInfo)
res.emplace_back(std::move(*streamInfo));
}
return res; for (std::size_t i{}; i < _context->nb_streams; ++i)
} {
std::optional<StreamInfo> streamInfo{ getStreamInfo(i) };
if (streamInfo)
res.emplace_back(std::move(*streamInfo));
}
std::optional<std::size_t> return res;
AudioFile::getBestStreamIndex() const }
{
int res = ::av_find_best_stream(_context,
AVMEDIA_TYPE_AUDIO,
-1, // Auto
-1, // Auto
NULL,
0);
if (res < 0) std::optional<std::size_t> AudioFile::getBestStreamIndex() const
return std::nullopt; {
int res = ::av_find_best_stream(_context,
AVMEDIA_TYPE_AUDIO,
-1, // Auto
-1, // Auto
NULL,
0);
return res; if (res < 0)
} return std::nullopt;
std::optional<StreamInfo> return res;
AudioFile::getBestStreamInfo() const }
{
std::optional<StreamInfo> res;
std::optional<std::size_t> bestStreamIndex {getBestStreamIndex()}; std::optional<StreamInfo> AudioFile::getBestStreamInfo() const
if (bestStreamIndex) {
res = getStreamInfo(*bestStreamIndex); std::optional<StreamInfo> res;
return res; std::optional<std::size_t> bestStreamIndex{ getBestStreamIndex() };
} if (bestStreamIndex)
res = getStreamInfo(*bestStreamIndex);
bool return res;
AudioFile::hasAttachedPictures() const }
{
for (std::size_t i = 0; i < _context->nb_streams; ++i)
{
if (_context->streams[i]->disposition & AV_DISPOSITION_ATTACHED_PIC)
return true;
}
return false; bool AudioFile::hasAttachedPictures() const
} {
for (std::size_t i = 0; i < _context->nb_streams; ++i)
{
if (_context->streams[i]->disposition & AV_DISPOSITION_ATTACHED_PIC)
return true;
}
void return false;
AudioFile::visitAttachedPictures(std::function<void(const Picture&)> func) const }
{
static const std::unordered_map<int, std::string> codecMimeMap =
{
{ AV_CODEC_ID_BMP, "image/x-bmp" },
{ AV_CODEC_ID_GIF, "image/gif" },
{ AV_CODEC_ID_MJPEG, "image/jpeg" },
{ AV_CODEC_ID_PNG, "image/png" },
{ AV_CODEC_ID_PNG, "image/x-png" },
{ AV_CODEC_ID_PPM, "image/x-portable-pixmap" },
};
for (std::size_t i = 0; i < _context->nb_streams; ++i) void AudioFile::visitAttachedPictures(std::function<void(const Picture&)> func) const
{ {
AVStream *avstream = _context->streams[i]; static const std::unordered_map<int, std::string> codecMimeMap =
{
{ AV_CODEC_ID_BMP, "image/x-bmp" },
{ AV_CODEC_ID_GIF, "image/gif" },
{ AV_CODEC_ID_MJPEG, "image/jpeg" },
{ AV_CODEC_ID_PNG, "image/png" },
{ AV_CODEC_ID_PNG, "image/x-png" },
{ AV_CODEC_ID_PPM, "image/x-portable-pixmap" },
};
// Skip attached pics for (std::size_t i = 0; i < _context->nb_streams; ++i)
if (!(avstream->disposition & AV_DISPOSITION_ATTACHED_PIC)) {
continue; AVStream* avstream = _context->streams[i];
if (avstream->codecpar == nullptr) // Skip attached pics
{ if (!(avstream->disposition & AV_DISPOSITION_ATTACHED_PIC))
LMS_LOG(AV, ERROR) << "Skipping stream " << i << " since no codecpar is set"; continue;
continue;
}
Picture picture; if (avstream->codecpar == nullptr)
{
LMS_LOG(AV, ERROR) << "Skipping stream " << i << " since no codecpar is set";
continue;
}
auto itMime = codecMimeMap.find(avstream->codecpar->codec_id); Picture picture;
if (itMime != codecMimeMap.end())
{
picture.mimeType = itMime->second;
}
else
{
picture.mimeType = "application/octet-stream";
LMS_LOG(AV, ERROR) << "CODEC ID " << avstream->codecpar->codec_id << " not handled in mime type conversion";
}
const AVPacket& pkt {avstream->attached_pic}; auto itMime = codecMimeMap.find(avstream->codecpar->codec_id);
if (itMime != codecMimeMap.end())
{
picture.mimeType = itMime->second;
}
else
{
picture.mimeType = "application/octet-stream";
LMS_LOG(AV, ERROR) << "CODEC ID " << avstream->codecpar->codec_id << " not handled in mime type conversion";
}
picture.data = reinterpret_cast<const std::byte*>(pkt.data); const AVPacket& pkt{ avstream->attached_pic };
picture.dataSize = pkt.size;
func(picture); picture.data = reinterpret_cast<const std::byte*>(pkt.data);
} picture.dataSize = pkt.size;
}
std::optional<StreamInfo> func(picture);
AudioFile::getStreamInfo(std::size_t streamIndex) const }
{ }
std::optional<StreamInfo> res;
AVStream* avstream { _context->streams[streamIndex]}; std::optional<StreamInfo> AudioFile::getStreamInfo(std::size_t streamIndex) const
assert(avstream); {
std::optional<StreamInfo> res;
if (avstream->disposition & AV_DISPOSITION_ATTACHED_PIC) AVStream* avstream{ _context->streams[streamIndex] };
return res; assert(avstream);
if (!avstream->codecpar) if (avstream->disposition & AV_DISPOSITION_ATTACHED_PIC)
{ return res;
LMS_LOG(AV, ERROR) << "Skipping stream " << streamIndex << " since no codecpar is set";
return res;
}
if (avstream->codecpar->codec_type != AVMEDIA_TYPE_AUDIO) if (!avstream->codecpar)
return res; {
LMS_LOG(AV, ERROR) << "Skipping stream " << streamIndex << " since no codecpar is set";
return res;
}
res.emplace(); if (avstream->codecpar->codec_type != AVMEDIA_TYPE_AUDIO)
res->index = streamIndex; return res;
res->bitrate = static_cast<std::size_t>(avstream->codecpar->bit_rate);
res->codec = ::avcodec_get_name(avstream->codecpar->codec_id);
assert(!res->codec.empty());
return res; res.emplace();
} res->index = streamIndex;
res->bitrate = static_cast<std::size_t>(avstream->codecpar->bit_rate);
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
std::optional<AudioFileFormat> return res;
guessAudioFileFormat(const std::filesystem::path& file) }
{
const AVOutputFormat* format {::av_guess_format(NULL, file.string().c_str(), NULL)};
if (!format || !format->name)
return {};
LMS_LOG(AV, DEBUG) << "File '" << file.string() << "', formats = '" << format->name << "'"; std::string_view getMimeType(const std::filesystem::path& fileExtension)
{
// List should be sync with the demuxers shipped in the lms's docker version
// + the _audioFileExtensions in ScanSettings
static const std::unordered_map<std::filesystem::path, std::string_view> entries
{
{".mp3", "audio/mpeg"},
{".ogg", "audio/ogg"},
{".oga", "audio/ogg"},
{".opus", "audio/opus"},
{".aac", "audio/aac"},
{".alac", "audio/mp4"},
{".m4a", "audio/mp4"},
{".m4b", "audio/mp4"},
{".flac", "audio/flac"},
{".webm", "audio/webm"},
{".wav", "audio/x-wav"},
{".wma", "audio/x-ms-wma"},
{".ape", "audio/x-monkeys-audio"},
{".mpc", "audio/x-musepack"},
{".shn", "audio/x-shn"},
{".aif", "audio/x-aiff"},
{".aiff", "audio/x-aiff"},
{".m3u", "audio/x-mpegurl"},
{".pls", "audio/x-scpls"},
{".dsf", "audio/dsd"},
{".wv", "audio/x-wavpack"},
{".wvp", "audio/x-wavpack"},
{".mka", "audio/x-matroska"},
};
auto formats {StringUtils::splitString(format->name, ",")}; auto it{ entries.find(fileExtension) };
if (formats.size() > 1) if (it == std::cend(entries))
LMS_LOG(AV, INFO) << "File '" << file.string() << "' reported several formats: '" << format->name << "'"; return "";
std::vector<std::string_view> mimeTypes;
if (format->mime_type)
mimeTypes = StringUtils::splitString(format->mime_type, ",");
if (mimeTypes.empty())
LMS_LOG(AV, INFO) << "File '" << file.string() << "', no mime type found!";
else if (mimeTypes.size() > 1)
LMS_LOG(AV, INFO) << "File '" << file.string() << "' reported several mime types: '" << format->mime_type << "'";
AudioFileFormat res;
res.format = formats.front();
res.mimeType = mimeTypes.empty() ? "application/octet-stream" : mimeTypes.front();
return res;
}
return it->second;
}
} // namespace Av } // namespace Av
+21 -23
View File
@@ -17,7 +17,7 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>. * along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/ */
/* This file contains some classes in order to get info from file using the libavconv */ /* This file contains some classes in order to get info from file using the libavconv */
#pragma once #pragma once
@@ -28,32 +28,30 @@ struct AVFormatContext;
namespace Av namespace Av
{ {
class AudioFile final : public IAudioFile class AudioFile final : public IAudioFile
{ {
public: public:
AudioFile(const std::filesystem::path& p); AudioFile(const std::filesystem::path& p);
~AudioFile(); ~AudioFile();
AudioFile(const AudioFile&) = delete; const std::filesystem::path& getPath() const override;
AudioFile(AudioFile&&) = delete; ContainerInfo getContainerInfo() const override;
AudioFile& operator=(const AudioFile&) = delete; MetadataMap getMetaData() const override;
AudioFile& operator=(AudioFile&&) = delete; std::vector<StreamInfo> getStreamInfo() const override;
std::optional<StreamInfo> getBestStreamInfo() const override;
std::optional<std::size_t> getBestStreamIndex() const override;
bool hasAttachedPictures() const override;
void visitAttachedPictures(std::function<void(const Picture&)> func) const override;
const std::filesystem::path& getPath() const override; private:
std::chrono::milliseconds getDuration() const override; AudioFile(const AudioFile&) = delete;
MetadataMap getMetaData() const override; AudioFile& operator=(const AudioFile&) = delete;
std::vector<StreamInfo> getStreamInfo() const override;
std::optional<StreamInfo> getBestStreamInfo() const override;
std::optional<std::size_t> getBestStreamIndex() const override;
bool hasAttachedPictures() const override;
void visitAttachedPictures(std::function<void(const Picture&)> func) const override;
private: std::optional<StreamInfo> getStreamInfo(std::size_t streamIndex) const;
std::optional<StreamInfo> getStreamInfo(std::size_t streamIndex) const;
const std::filesystem::path _p; const std::filesystem::path _p;
AVFormatContext* _context {}; AVFormatContext* _context{};
}; };
} // namespace Av } // namespace Av
@@ -0,0 +1,32 @@
/*
* Copyright (C) 2023 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "av/RawResourceHandlerCreator.hpp"
#include "av/IAudioFile.hpp"
#include "utils/FileResourceHandlerCreator.hpp"
namespace Av
{
std::unique_ptr<IResourceHandler> createRawResourceHandler(const std::filesystem::path& path)
{
std::string_view mimeType{ Av::getMimeType(path.extension()) };
return createFileResourceHandler(path, mimeType.empty() ? "application/octet-stream" : mimeType);
}
}
@@ -1,106 +0,0 @@
/*
* Copyright (C) 2020 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "TranscodeResourceHandler.hpp"
#include "utils/Logger.hpp"
namespace Av
{
namespace
{
std::size_t
doEstimateContentLength(const InputFileParameters& inputFileParameters, const TranscodeParameters& transcodeParameters)
{
const std::size_t estimatedContentLength {transcodeParameters.bitrate / 8 * static_cast<std::size_t>(std::chrono::duration_cast<std::chrono::milliseconds>(inputFileParameters.duration).count()) / 1000};
return estimatedContentLength;
}
}
std::unique_ptr<IResourceHandler>
createTranscodeResourceHandler(const InputFileParameters& inputFileParameters, const TranscodeParameters& transcodeParameters, bool estimateContentLength)
{
return std::make_unique<TranscodeResourceHandler>(inputFileParameters, transcodeParameters, estimateContentLength);
}
// TODO set some nice HTTP return code
TranscodeResourceHandler::TranscodeResourceHandler(const InputFileParameters& inputFileParameters, const TranscodeParameters& transcodeParameters, bool estimateContentLength)
: _estimatedContentLength {estimateContentLength ? std::make_optional(doEstimateContentLength(inputFileParameters, transcodeParameters)) : std::nullopt}
, _transcoder {inputFileParameters, transcodeParameters}
{
if (_estimatedContentLength)
LMS_LOG(TRANSCODE, DEBUG) << "Estimated content length = " << *_estimatedContentLength;
else
LMS_LOG(TRANSCODE, DEBUG) << "Not using estimated content length";
}
Wt::Http::ResponseContinuation*
TranscodeResourceHandler::processRequest(const Wt::Http::Request& /*request*/, Wt::Http::Response& response)
{
if (_estimatedContentLength)
response.setContentLength(*_estimatedContentLength);
response.setMimeType(_transcoder.getOutputMimeType());
LMS_LOG(TRANSCODE, DEBUG) << "Transcoder finished = " << _transcoder.finished() << ", total served bytes = " << _totalServedByteCount << ", mime type = " << _transcoder.getOutputMimeType();
if (_bytesReadyCount > 0)
{
LMS_LOG(TRANSCODE, DEBUG) << "Writing " << _bytesReadyCount << " bytes back to client";
response.out().write(reinterpret_cast<const char *>(&_buffer[0]), _bytesReadyCount);
_totalServedByteCount += _bytesReadyCount;
_bytesReadyCount = 0;
}
if (!_transcoder.finished())
{
Wt::Http::ResponseContinuation *continuation {response.createContinuation()};
continuation->waitForMoreData();
_transcoder.asyncRead(_buffer.data(), _buffer.size(), [=](std::size_t nbBytesRead)
{
LMS_LOG(TRANSCODE, DEBUG) << "Have " << nbBytesRead << " more bytes to send back";
assert(_bytesReadyCount == 0);
_bytesReadyCount = nbBytesRead;
continuation->haveMoreData();
});
return continuation;
}
else
{
// pad with 0 if necessary as duration may not be accurate
if (_estimatedContentLength && *_estimatedContentLength > _totalServedByteCount)
{
const std::size_t padSize {*_estimatedContentLength - _totalServedByteCount};
LMS_LOG(TRANSCODE, DEBUG) << "Adding " << padSize << " padding bytes";
for (std::size_t i {}; i < padSize; ++i)
response.out().put(0);
_totalServedByteCount += padSize;
}
LMS_LOG(TRANSCODE, DEBUG) << "Transcoding finished. Total served byte count = " << _totalServedByteCount;
}
return {};
}
}
+155 -145
View File
@@ -28,181 +28,191 @@
#include "utils/Logger.hpp" #include "utils/Logger.hpp"
#include "utils/Service.hpp" #include "utils/Service.hpp"
namespace Av { namespace Av::Transcoding
#define LOG(sev) LMS_LOG(TRANSCODE, sev) << "[" << _debugId << "] - "
static std::atomic<size_t> globalId {};
static std::filesystem::path ffmpegPath;
void
Transcoder::init()
{ {
ffmpegPath = Service<IConfig>::get()->getPath("ffmpeg-file", "/usr/bin/ffmpeg");
if (!std::filesystem::exists(ffmpegPath))
throw Exception {"File '" + ffmpegPath.string() + "' does not exist!"};
}
Transcoder::Transcoder(const InputFileParameters& inputFileParameters, const TranscodeParameters& transcodeParameters) #define LOG(sev) LMS_LOG(TRANSCODING, sev) << "[" << _debugId << "] - "
: _debugId {globalId++}
, _inputFileParameters {inputFileParameters}
, _transcodeParameters {transcodeParameters}
{
start();
}
Transcoder::~Transcoder() = default; static std::atomic<size_t> globalId{};
static std::filesystem::path ffmpegPath;
void std::string_view formatToMimetype(OutputFormat format)
Transcoder::start() {
{ switch (format)
if (ffmpegPath.empty()) {
init(); case OutputFormat::MP3: return "audio/mpeg";
case OutputFormat::OGG_OPUS: return "audio/opus";
case OutputFormat::MATROSKA_OPUS: return "audio/x-matroska";
case OutputFormat::OGG_VORBIS: return "audio/ogg";
case OutputFormat::WEBM_VORBIS: return "audio/webm";
}
try throw Exception{ "Invalid encoding" };
{ }
if (!std::filesystem::exists(_inputFileParameters.trackPath))
throw Exception {"File '" + _inputFileParameters.trackPath.string() + "' does not exist!"};
else if (!std::filesystem::is_regular_file( _inputFileParameters.trackPath) )
throw Exception {"File '" + _inputFileParameters.trackPath.string() + "' is not regular!"};
}
catch (const std::filesystem::filesystem_error& e)
{
throw Exception {"File error '" + _inputFileParameters.trackPath.string() + "': " + e.what()};
}
LOG(INFO) << "Transcoding file '" << _inputFileParameters.trackPath.string() << "'"; void Transcoder::init()
{
ffmpegPath = Service<IConfig>::get()->getPath("ffmpeg-file", "/usr/bin/ffmpeg");
if (!std::filesystem::exists(ffmpegPath))
throw Exception{ "File '" + ffmpegPath.string() + "' does not exist!" };
}
std::vector<std::string> args; Transcoder::Transcoder(const InputParameters& inputParameters, const OutputParameters& outputParameters)
: _debugId{ globalId++ }
, _inputParameters{ inputParameters }
, _outputParameters{ outputParameters }
{
start();
}
args.emplace_back(ffmpegPath.string()); Transcoder::~Transcoder() = default;
// Make sure: void Transcoder::start()
// - we do not produce anything in the stderr output {
// - we do not rely on input if (ffmpegPath.empty())
// in order not to block the whole forked process init();
args.emplace_back("-loglevel");
args.emplace_back("quiet");
args.emplace_back("-nostdin");
// input Offset try
{ {
args.emplace_back("-ss"); if (!std::filesystem::exists(_inputParameters.trackPath))
throw Exception{ "File '" + _inputParameters.trackPath.string() + "' does not exist!" };
else if (!std::filesystem::is_regular_file(_inputParameters.trackPath))
throw Exception{ "File '" + _inputParameters.trackPath.string() + "' is not regular!" };
}
catch (const std::filesystem::filesystem_error& e)
{
throw Exception{ "File error '" + _inputParameters.trackPath.string() + "': " + e.what() };
}
std::ostringstream oss; LOG(INFO) << "Transcoding file '" << _inputParameters.trackPath.string() << "'";
oss << std::fixed << std::showpoint << std::setprecision(3) << (_transcodeParameters.offset.count() / float {1000});
args.emplace_back(oss.str());
}
// Input file std::vector<std::string> args;
args.emplace_back("-i");
args.emplace_back(_inputFileParameters.trackPath.string());
// Stream mapping, if set args.emplace_back(ffmpegPath.string());
if (_transcodeParameters.stream)
{
args.emplace_back("-map");
args.emplace_back("0:" + std::to_string(*_transcodeParameters.stream));
}
if (_transcodeParameters.stripMetadata) // Make sure:
{ // - we do not produce anything in the stderr output
// Strip metadata // - we do not rely on input
args.emplace_back("-map_metadata"); // in order not to block the whole forked process
args.emplace_back("-1"); args.emplace_back("-loglevel");
} args.emplace_back("quiet");
args.emplace_back("-nostdin");
// Skip video flows (including covers) // input Offset
args.emplace_back("-vn"); {
args.emplace_back("-ss");
// Output bitrates std::ostringstream oss;
args.emplace_back("-b:a"); oss << std::fixed << std::showpoint << std::setprecision(3) << (_outputParameters.offset.count() / float{ 1000 });
args.emplace_back(std::to_string(_transcodeParameters.bitrate)); args.emplace_back(oss.str());
}
// Codecs and formats // Input file
switch (_transcodeParameters.format) args.emplace_back("-i");
{ args.emplace_back(_inputParameters.trackPath.string());
case Format::MP3:
args.emplace_back("-f");
args.emplace_back("mp3");
break;
case Format::OGG_OPUS: // Stream mapping, if set
args.emplace_back("-acodec"); if (_outputParameters.stream)
args.emplace_back("libopus"); {
args.emplace_back("-f"); args.emplace_back("-map");
args.emplace_back("ogg"); args.emplace_back("0:" + std::to_string(*_outputParameters.stream));
break; }
case Format::MATROSKA_OPUS: if (_outputParameters.stripMetadata)
args.emplace_back("-acodec"); {
args.emplace_back("libopus"); // Strip metadata
args.emplace_back("-f"); args.emplace_back("-map_metadata");
args.emplace_back("matroska"); args.emplace_back("-1");
break; }
case Format::OGG_VORBIS: // Skip video flows (including covers)
args.emplace_back("-acodec"); args.emplace_back("-vn");
args.emplace_back("libvorbis");
args.emplace_back("-f");
args.emplace_back("ogg");
break;
case Format::WEBM_VORBIS: // Output bitrates
args.emplace_back("-acodec"); args.emplace_back("-b:a");
args.emplace_back("libvorbis"); args.emplace_back(std::to_string(_outputParameters.bitrate));
args.emplace_back("-f");
args.emplace_back("webm");
break;
default: // Codecs and formats
throw Exception {"Unhandled format (" + std::to_string(static_cast<int>(_transcodeParameters.format)) + ")"}; switch (_outputParameters.format)
} {
case OutputFormat::MP3:
args.emplace_back("-f");
args.emplace_back("mp3");
break;
_outputMimeType = formatToMimetype(_transcodeParameters.format); case OutputFormat::OGG_OPUS:
args.emplace_back("-acodec");
args.emplace_back("libopus");
args.emplace_back("-f");
args.emplace_back("ogg");
break;
args.emplace_back("pipe:1"); case OutputFormat::MATROSKA_OPUS:
args.emplace_back("-acodec");
args.emplace_back("libopus");
args.emplace_back("-f");
args.emplace_back("matroska");
break;
LOG(DEBUG) << "Dumping args (" << args.size() << ")"; case OutputFormat::OGG_VORBIS:
for (const std::string& arg : args) args.emplace_back("-acodec");
LOG(DEBUG) << "Arg = '" << arg << "'"; args.emplace_back("libvorbis");
args.emplace_back("-f");
args.emplace_back("ogg");
break;
// Caution: stdin must have been closed before case OutputFormat::WEBM_VORBIS:
try args.emplace_back("-acodec");
{ args.emplace_back("libvorbis");
_childProcess = Service<IChildProcessManager>::get()->spawnChildProcess(ffmpegPath, args); args.emplace_back("-f");
} args.emplace_back("webm");
catch (ChildProcessException& exception) break;
{
throw Exception {"Cannot execute '" + ffmpegPath.string() + "': " + exception.what()};
}
}
void default:
Transcoder::asyncRead(std::byte* buffer, std::size_t bufferSize, ReadCallback readCallback) throw Exception{ "Unhandled format (" + std::to_string(static_cast<int>(_outputParameters.format)) + ")" };
{ }
assert(_childProcess);
return _childProcess->asyncRead(buffer, bufferSize, [readCallback {std::move(readCallback)}](IChildProcess::ReadResult /*res*/, std::size_t nbBytesRead) _outputMimeType = formatToMimetype(_outputParameters.format);
{
readCallback(nbBytesRead);
});
}
std::size_t args.emplace_back("pipe:1");
Transcoder::readSome(std::byte* buffer, std::size_t bufferSize)
{
assert(_childProcess);
return _childProcess->readSome(buffer, bufferSize); LOG(DEBUG) << "Dumping args (" << args.size() << ")";
} for (const std::string& arg : args)
LOG(DEBUG) << "Arg = '" << arg << "'";
bool // Caution: stdin must have been closed before
Transcoder::finished() const try
{ {
assert(_childProcess); _childProcess = Service<IChildProcessManager>::get()->spawnChildProcess(ffmpegPath, args);
}
catch (ChildProcessException& exception)
{
throw Exception{ "Cannot execute '" + ffmpegPath.string() + "': " + exception.what() };
}
}
return _childProcess->finished(); void Transcoder::asyncRead(std::byte* buffer, std::size_t bufferSize, ReadCallback readCallback)
} {
assert(_childProcess);
} // namespace Transcode return _childProcess->asyncRead(buffer, bufferSize, [readCallback{ std::move(readCallback) }](IChildProcess::ReadResult /*res*/, std::size_t nbBytesRead)
{
readCallback(nbBytesRead);
});
}
std::size_t Transcoder::readSome(std::byte* buffer, std::size_t bufferSize)
{
assert(_childProcess);
return _childProcess->readSome(buffer, bufferSize);
}
bool Transcoder::finished() const
{
assert(_childProcess);
return _childProcess->finished();
}
} // namespace Av::Transcoding
+28 -30
View File
@@ -22,47 +22,45 @@
#include <filesystem> #include <filesystem>
#include <functional> #include <functional>
#include "av/TranscodeParameters.hpp" #include "av/TranscodingParameters.hpp"
#include "av/Types.hpp" #include "av/Types.hpp"
class IChildProcess; class IChildProcess;
namespace Av namespace Av::Transcoding
{ {
class Transcoder class Transcoder
{ {
public: public:
Transcoder(const InputFileParameters& inputFileParameters, const TranscodeParameters& transcodeParameters); Transcoder(const InputParameters& inputParameters, const OutputParameters& outputParameters);
~Transcoder(); ~Transcoder();
Transcoder(const Transcoder&) = delete; Transcoder(const Transcoder&) = delete;
Transcoder& operator=(const Transcoder&) = delete; Transcoder& operator=(const Transcoder&) = delete;
Transcoder(Transcoder&&) = delete; Transcoder(Transcoder&&) = delete;
Transcoder& operator=(Transcoder&&) = delete; Transcoder& operator=(Transcoder&&) = delete;
// non blocking calls // non blocking calls
using ReadCallback = std::function<void(std::size_t nbReadBytes)>; using ReadCallback = std::function<void(std::size_t nbReadBytes)>;
void asyncRead(std::byte* buffer, std::size_t bufferSize, ReadCallback); void asyncRead(std::byte* buffer, std::size_t bufferSize, ReadCallback);
std::size_t readSome(std::byte* buffer, std::size_t bufferSize); std::size_t readSome(std::byte* buffer, std::size_t bufferSize);
const std::string& getOutputMimeType() const { return _outputMimeType; } const std::string& getOutputMimeType() const { return _outputMimeType; }
const TranscodeParameters& getParameters() const { return _transcodeParameters; } const OutputParameters& getOutputParameters() const { return _outputParameters; }
bool finished() const; bool finished() const;
private: private:
static void init(); static void init();
void start(); void start();
const std::size_t _debugId {}; const std::size_t _debugId{};
const InputFileParameters _inputFileParameters; const InputParameters _inputParameters;
const TranscodeParameters _transcodeParameters; const OutputParameters _outputParameters;
std::string _outputMimeType;
std::unique_ptr<IChildProcess> _childProcess; std::unique_ptr<IChildProcess> _childProcess;
};
std::string _outputMimeType;
};
} // namespace Av
} // namespace Av::Transcoding
@@ -0,0 +1,103 @@
/*
* Copyright (C) 2020 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "TranscodingResourceHandler.hpp"
#include "utils/Logger.hpp"
namespace Av::Transcoding
{
namespace
{
std::size_t doEstimateContentLength(const InputParameters& inputParameters, const OutputParameters& outputParameters)
{
const std::size_t estimatedContentLength{ outputParameters.bitrate / 8 * static_cast<std::size_t>(std::chrono::duration_cast<std::chrono::milliseconds>(inputParameters.duration).count()) / 1000 };
return estimatedContentLength;
}
}
std::unique_ptr<IResourceHandler> createResourceHandler(const InputParameters& inputParameters, const OutputParameters& outputParameters, bool estimateContentLength)
{
return std::make_unique<TranscodingResourceHandler>(inputParameters, outputParameters, estimateContentLength);
}
// TODO set some nice HTTP return code
TranscodingResourceHandler::TranscodingResourceHandler(const InputParameters& inputParameters, const OutputParameters& outputParameters, bool estimateContentLength)
: _estimatedContentLength{ estimateContentLength ? std::make_optional(doEstimateContentLength(inputParameters, outputParameters)) : std::nullopt }
, _transcoder{ inputParameters, outputParameters }
{
if (_estimatedContentLength)
LMS_LOG(TRANSCODING, DEBUG) << "Estimated content length = " << *_estimatedContentLength;
else
LMS_LOG(TRANSCODING, DEBUG) << "Not using estimated content length";
}
Wt::Http::ResponseContinuation* TranscodingResourceHandler::processRequest(const Wt::Http::Request& /*request*/, Wt::Http::Response& response)
{
if (_estimatedContentLength)
response.setContentLength(*_estimatedContentLength);
response.setMimeType(_transcoder.getOutputMimeType());
LMS_LOG(TRANSCODING, DEBUG) << "Transcoder finished = " << _transcoder.finished() << ", total served bytes = " << _totalServedByteCount << ", mime type = " << _transcoder.getOutputMimeType();
if (_bytesReadyCount > 0)
{
LMS_LOG(TRANSCODING, DEBUG) << "Writing " << _bytesReadyCount << " bytes back to client";
response.out().write(reinterpret_cast<const char*>(&_buffer[0]), _bytesReadyCount);
_totalServedByteCount += _bytesReadyCount;
_bytesReadyCount = 0;
}
if (!_transcoder.finished())
{
Wt::Http::ResponseContinuation* continuation{ response.createContinuation() };
continuation->waitForMoreData();
_transcoder.asyncRead(_buffer.data(), _buffer.size(), [=](std::size_t nbBytesRead)
{
LMS_LOG(TRANSCODING, DEBUG) << "Have " << nbBytesRead << " more bytes to send back";
assert(_bytesReadyCount == 0);
_bytesReadyCount = nbBytesRead;
continuation->haveMoreData();
});
return continuation;
}
else
{
// pad with 0 if necessary as duration may not be accurate
if (_estimatedContentLength && *_estimatedContentLength > _totalServedByteCount)
{
const std::size_t padSize{ *_estimatedContentLength - _totalServedByteCount };
LMS_LOG(TRANSCODING, DEBUG) << "Adding " << padSize << " padding bytes";
for (std::size_t i{}; i < padSize; ++i)
response.out().put(0);
_totalServedByteCount += padSize;
}
LMS_LOG(TRANSCODING, DEBUG) << "Transcoding finished. Total served byte count = " << _totalServedByteCount;
}
return {};
}
}
@@ -23,27 +23,27 @@
#include <filesystem> #include <filesystem>
#include <optional> #include <optional>
#include "av/TranscodeParameters.hpp" #include "av/TranscodingParameters.hpp"
#include "utils/IResourceHandler.hpp" #include "utils/IResourceHandler.hpp"
#include "Transcoder.hpp" #include "Transcoder.hpp"
namespace Av namespace Av::Transcoding
{ {
class TranscodeResourceHandler final : public IResourceHandler class TranscodingResourceHandler final : public IResourceHandler
{ {
public: public:
TranscodeResourceHandler(const InputFileParameters& inputFileParameters, const TranscodeParameters& parameters, bool estimateContentLength); TranscodingResourceHandler(const InputParameters& inputParameters, const OutputParameters& outputParameters, bool estimateContentLength);
private: private:
Wt::Http::ResponseContinuation* processRequest(const Wt::Http::Request& request, Wt::Http::Response& reponse) override; Wt::Http::ResponseContinuation* processRequest(const Wt::Http::Request& request, Wt::Http::Response& reponse) override;
void abort() override {}; void abort() override {};
static constexpr std::size_t _chunkSize {32768}; static constexpr std::size_t _chunkSize{ 262'144 };
std::optional<std::size_t> _estimatedContentLength; std::optional<std::size_t> _estimatedContentLength;
std::array<std::byte, _chunkSize> _buffer; std::array<std::byte, _chunkSize> _buffer;
std::size_t _bytesReadyCount {}; std::size_t _bytesReadyCount{};
std::size_t _totalServedByteCount {}; std::size_t _totalServedByteCount{};
Transcoder _transcoder; Transcoder _transcoder;
}; };
} }
+66 -34
View File
@@ -17,7 +17,7 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>. * along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/ */
/* This file contains some classes in order to get info from file using the libavconv */ /* This file contains some classes in order to get info from file using the libavconv */
#pragma once #pragma once
@@ -27,52 +27,84 @@
#include <unordered_map> #include <unordered_map>
#include <optional> #include <optional>
#include <string> #include <string>
#include <string_view>
#include <vector> #include <vector>
#include "Types.hpp" #include "Types.hpp"
namespace Av namespace Av
{ {
struct Picture // List should be sync with the codecs shipped in the lms's docker version
{ enum class DecodingCodec
std::string mimeType; {
const std::byte* data {}; UNKNOWN,
std::size_t dataSize {}; MP3,
}; AAC,
AC3,
VORBIS,
WMAV1,
WMAV2,
FLAC, // Flac
ALAC, // Apple Lossless Audio Codec (ALAC)
WAVPACK, // WavPack
MUSEPACK7, // Musepack
MUSEPACK8,
APE, // // Monkey's Audio
EAC3, // Enhanced AC-3
MP4ALS, // MPEG-4 Audio Lossless Coding
OPUS, // Opus
SHORTEN, // Shorten (shn)
// TODO add PCM codecs
};
struct StreamInfo struct Picture
{ {
size_t index {}; std::string mimeType;
std::size_t bitrate {}; const std::byte* data{};
std::string codec; std::size_t dataSize{};
}; };
class IAudioFile struct ContainerInfo
{ {
public: std::size_t bitrate{};
virtual ~IAudioFile() = default; std::string name{};
std::chrono::milliseconds duration{};
};
using MetadataMap = std::unordered_map<std::string, std::string>; struct StreamInfo
{
size_t index{};
std::size_t bitrate{};
DecodingCodec codec;
std::string codecName;
};
virtual const std::filesystem::path& getPath() const = 0; class IAudioFile
virtual std::chrono::milliseconds getDuration() const = 0; {
virtual MetadataMap getMetaData() const = 0; public:
virtual std::vector<StreamInfo> getStreamInfo() const = 0; virtual ~IAudioFile() = default;
virtual std::optional<StreamInfo> getBestStreamInfo() const = 0; // none if failure/unknown
virtual std::optional<std::size_t> getBestStreamIndex() const = 0; // none if failure/unknown
virtual bool hasAttachedPictures() const = 0;
virtual void visitAttachedPictures(std::function<void(const Picture&)> func) const = 0;
};
std::unique_ptr<IAudioFile> parseAudioFile(const std::filesystem::path& p); using MetadataMap = std::unordered_map<std::string, std::string>;
struct AudioFileFormat virtual const std::filesystem::path& getPath() const = 0;
{ virtual ContainerInfo getContainerInfo() const = 0;
std::string mimeType; virtual MetadataMap getMetaData() const = 0;
std::string format; virtual std::vector<StreamInfo> getStreamInfo() const = 0;
}; virtual std::optional<StreamInfo> getBestStreamInfo() const = 0; // none if failure/unknown
virtual std::optional<std::size_t> getBestStreamIndex() const = 0; // none if failure/unknown
virtual bool hasAttachedPictures() const = 0;
virtual void visitAttachedPictures(std::function<void(const Picture&)> func) const = 0;
};
std::optional<AudioFileFormat> guessAudioFileFormat(const std::filesystem::path& file); std::unique_ptr<IAudioFile> parseAudioFile(const std::filesystem::path& p);
struct AudioFileFormat
{
std::string mimeType;
std::string format;
};
std::string_view getMimeType(const std::filesystem::path& fileExtension);
} // namespace Av } // namespace Av
@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2019 Emeric Poupon * Copyright (C) 2023 Emeric Poupon
* *
* This file is part of LMS. * This file is part of LMS.
* *
@@ -17,25 +17,14 @@
* along with LMS. If not, see <http://www.gnu.org/licenses/>. * along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/ */
#include "av/Types.hpp" #pragma once
#include <filesystem>
#include <memory>
#include "utils/IResourceHandler.hpp"
namespace Av namespace Av
{ {
std::unique_ptr<IResourceHandler> createRawResourceHandler(const std::filesystem::path& path);
std::string_view
formatToMimetype(Format format)
{
switch (format)
{
case Format::MP3: return "audio/mpeg";
case Format::OGG_OPUS: return "audio/opus";
case Format::MATROSKA_OPUS: return "audio/x-matroska";
case Format::OGG_VORBIS: return "audio/ogg";
case Format::WEBM_VORBIS: return "audio/webm";
}
throw Exception {"Invalid encoding"};
}
} }
@@ -0,0 +1,56 @@
/*
* 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 <chrono>
#include <filesystem>
#include <optional>
#include "Types.hpp"
namespace Av::Transcoding
{
struct InputParameters
{
std::filesystem::path trackPath;
std::chrono::milliseconds duration; // used to estimate content length
};
enum class OutputFormat
{
MP3,
OGG_OPUS,
MATROSKA_OPUS,
OGG_VORBIS,
WEBM_VORBIS,
};
std::string_view toMimetype(OutputFormat format);
struct OutputParameters
{
OutputFormat format;
std::size_t bitrate{ 128000 };
std::optional<std::size_t> stream; // Id of the stream to be transcoded (auto detect by default)
std::chrono::milliseconds offset{ 0 };
bool stripMetadata{ true };
};
} // namespace Av::Transcoding
@@ -23,11 +23,10 @@
#include "utils/IResourceHandler.hpp" #include "utils/IResourceHandler.hpp"
namespace Av namespace Av::Transcoding
{ {
struct InputFileParameters; struct InputParameters;
struct TranscodeParameters; struct OutputParameters;
std::unique_ptr<IResourceHandler> createTranscodeResourceHandler(const InputFileParameters& inputFileParameters, const TranscodeParameters& parameters, bool estimateContentLength); std::unique_ptr<IResourceHandler> createResourceHandler(const InputParameters& inputParameters, const OutputParameters& outputParameters, bool estimateContentLength);
} }
+7 -21
View File
@@ -19,27 +19,13 @@
#pragma once #pragma once
#include <string_view>
#include "utils/Exception.hpp" #include "utils/Exception.hpp"
namespace Av { namespace Av
{
class Exception : public LmsException class Exception : public LmsException
{ {
public: public:
using LmsException::LmsException; using LmsException::LmsException;
}; };
enum class Format
{
MP3,
OGG_OPUS,
MATROSKA_OPUS,
OGG_VORBIS,
WEBM_VORBIS,
};
std::string_view formatToMimetype(Format format);
} }
+3 -12
View File
@@ -186,18 +186,9 @@ AvFormatParser::parse(const std::filesystem::path& p, bool debug)
{ {
const auto mediaFile {Av::parseAudioFile(p)}; const auto mediaFile {Av::parseAudioFile(p)};
// Stream info Av::ContainerInfo info{ mediaFile->getContainerInfo() };
{ track.duration = info.duration;
std::vector<AudioStream> audioStreams; track.bitrate = info.bitrate;
for (auto stream : mediaFile->getStreamInfo())
{
MetaData::AudioStream audioStream {static_cast<unsigned>(stream.bitrate)};
track.audioStreams.emplace_back(audioStream);
}
}
track.duration = mediaFile->getDuration();
track.hasCover = mediaFile->hasAttachedPictures(); track.hasCover = mediaFile->hasAttachedPictures();
MetaData::Tags tags; MetaData::Tags tags;
+473 -497
View File
@@ -44,503 +44,479 @@
namespace MetaData namespace MetaData
{ {
namespace
// TODO use string_views here for values {
using TagMap = std::map<std::string, std::vector<std::string>>; // TODO use string_views here for values
using TagMap = std::map<std::string, std::vector<std::string>>;
template<typename T>
std::vector<T> template<typename T>
getPropertyValuesFirstMatchAs(const TagMap& tags, std::initializer_list<std::string_view> keys) std::vector<T> getPropertyValuesFirstMatchAs(const TagMap& tags, std::initializer_list<std::string_view> keys)
{ {
std::vector<T> res; std::vector<T> res;
for (std::string_view key : keys) for (std::string_view key : keys)
{ {
const auto itValues {tags.find(std::string {key})}; const auto itValues{ tags.find(std::string {key}) };
if (itValues == std::cend(tags)) if (itValues == std::cend(tags))
continue; continue;
const std::vector<std::string>& values {itValues->second}; const std::vector<std::string>& values{ itValues->second };
if (values.empty()) if (values.empty())
continue; continue;
res.reserve(values.size()); res.reserve(values.size());
for (const auto& value : values) for (const auto& value : values)
{ {
std::optional<T> val {StringUtils::readAs<T>(value)}; std::optional<T> val{ StringUtils::readAs<T>(value) };
if (!val) if (!val)
continue; continue;
res.emplace_back(std::move(*val)); res.emplace_back(std::move(*val));
} }
break; break;
} }
return res; return res;
} }
template <typename T> template <typename T>
std::optional<T> std::optional<T> getPropertyValueFirstMatchAs(const TagMap& tags, std::initializer_list<std::string_view> keys)
getPropertyValueFirstMatchAs(const TagMap& tags, std::initializer_list<std::string_view> keys) {
{ std::optional<T> res;
std::optional<T> res; std::vector<T> values{ getPropertyValuesFirstMatchAs<T>(tags, keys) };
std::vector<T> values {getPropertyValuesFirstMatchAs<T>(tags, keys)}; if (!values.empty())
if (!values.empty()) res = std::move(values.front());
res = std::move(values.front());
return res;
return res; }
}
template <typename T>
template <typename T> std::vector<T> getPropertyValuesAs(const TagMap& tags, std::string_view key)
std::vector<T> {
getPropertyValuesAs(const TagMap& tags, std::string_view key) return getPropertyValuesFirstMatchAs<T>(tags, { key });
{ }
return getPropertyValuesFirstMatchAs<T>(tags, {key});
} template <typename T>
std::optional<T> getPropertyValueAs(const TagMap& tags, std::string_view key)
template <typename T> {
std::optional<T> return getPropertyValueFirstMatchAs<T>(tags, { key });
getPropertyValueAs(const TagMap& tags, std::string_view key) }
{
return getPropertyValueFirstMatchAs<T>(tags, {key}); std::vector<std::string_view> splitAndTrimString(std::string_view str, std::string_view delimiters)
} {
std::vector<std::string_view> strings{ StringUtils::splitString(str, delimiters) };
static for (std::string_view& s : strings)
std::vector<std::string_view> s = StringUtils::stringTrim(s);
splitAndTrimString(std::string_view str, std::string_view delimiters)
{ return strings;
std::vector<std::string_view> strings {StringUtils::splitString(str, delimiters)}; }
for (std::string_view& s : strings)
s = StringUtils::stringTrim(s); std::vector<Artist> getArtists(const TagMap& tags,
std::initializer_list<std::string_view> artistTagNames,
return strings; std::initializer_list<std::string_view> artistSortTagNames,
} std::initializer_list<std::string_view> artistMBIDTagNames
)
static {
std::vector<Artist> const std::vector<std::string_view> artistNames{ getPropertyValuesFirstMatchAs<std::string_view>(tags, artistTagNames) };
getArtists(const TagMap& tags, if (artistNames.empty())
std::initializer_list<std::string_view> artistTagNames, return {};
std::initializer_list<std::string_view> artistSortTagNames,
std::initializer_list<std::string_view> artistMBIDTagNames std::vector<Artist> artists;
) artists.reserve(artistNames.size());
{ std::transform(std::cbegin(artistNames), std::cend(artistNames), std::back_inserter(artists),
const std::vector<std::string_view> artistNames {getPropertyValuesFirstMatchAs<std::string_view>(tags, artistTagNames)}; [&](std::string_view name) { return Artist{ name }; });
if (artistNames.empty())
return {}; {
const std::vector<std::string_view> artistSortNames{ getPropertyValuesFirstMatchAs<std::string_view>(tags, artistSortTagNames) };
std::vector<Artist> artists; if (artistSortNames.size() == artists.size())
artists.reserve(artistNames.size()); {
std::transform(std::cbegin(artistNames), std::cend(artistNames), std::back_inserter(artists), for (std::size_t i{}; i < artistSortNames.size(); ++i)
[&](std::string_view name) { return Artist {name}; }); artists[i].sortName = artistSortNames[i];
}
{ }
const std::vector<std::string_view> artistSortNames {getPropertyValuesFirstMatchAs<std::string_view>(tags, artistSortTagNames)};
if (artistSortNames.size() == artists.size()) {
{ const std::vector<UUID> artistsMBID{ getPropertyValuesFirstMatchAs<UUID>(tags, artistMBIDTagNames) };
for (std::size_t i {}; i < artistSortNames.size(); ++i)
artists[i].sortName = artistSortNames[i]; if (artistNames.size() == artistsMBID.size())
} {
} for (std::size_t i{}; i < artistsMBID.size(); ++i)
artists[i].mbid = artistsMBID[i];
{ }
const std::vector<UUID> artistsMBID {getPropertyValuesFirstMatchAs<UUID>(tags, artistMBIDTagNames)}; }
if (artistNames.size() == artistsMBID.size())
{ return artists;
for (std::size_t i {}; i < artistsMBID.size(); ++i) }
artists[i].mbid = artistsMBID[i];
} PerformerContainer getPerformerArtists(const TagMap& tags, std::initializer_list<std::string_view> artistTagNames)
} {
PerformerContainer performers;
return artists; // picard stores like this: (see https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html#performer)
} // We may hit both styles for the same track
// PERFORMER: artist (role)
static if (const std::vector<std::string_view> artistNames{ getPropertyValuesFirstMatchAs<std::string_view>(tags, artistTagNames) }; !artistNames.empty())
PerformerContainer {
getPerformerArtists(const TagMap& tags, for (std::string_view entry : artistNames)
std::initializer_list<std::string_view> artistTagNames) {
{ Utils::PerformerArtist performer{ Utils::extractPerformerAndRole(entry) };
PerformerContainer performers; StringUtils::capitalize(performer.role);
performers[performer.role].push_back(std::move(performer.artist));
// picard stores like this: (see https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html#performer) }
// We may hit both styles for the same track }
// PERFORMER: artist (role) // PERFORMER:role (MP3)
if (const std::vector<std::string_view> artistNames {getPropertyValuesFirstMatchAs<std::string_view>(tags, artistTagNames)}; !artistNames.empty()) for (const auto& [key, values] : tags)
{ {
for (std::string_view entry : artistNames) if (key.find("PERFORMER:") == 0)
{ {
Utils::PerformerArtist performer {Utils::extractPerformerAndRole(entry)}; std::string performerStr{ key };
StringUtils::capitalize(performer.role); std::string role;
performers[performer.role].push_back(std::move(performer.artist)); if (const std::size_t rolePos{ performerStr.find(':') }; rolePos != std::string::npos)
} {
} role = StringUtils::stringToLower(performerStr.substr(rolePos + 1, performerStr.size() - rolePos + 1));
// PERFORMER:role (MP3) StringUtils::capitalize(role);
for (const auto& [key, values] : tags) }
{
if (key.find("PERFORMER:") == 0) for (const auto& value : values)
{ performers[role].push_back(Artist{ value });
std::string performerStr {key}; }
std::string role; }
if (const std::size_t rolePos {performerStr.find(':')}; rolePos != std::string::npos)
{ return performers;
role = StringUtils::stringToLower(performerStr.substr(rolePos + 1, performerStr.size() - rolePos + 1)); }
StringUtils::capitalize(role);
} std::optional<Release> getRelease(const TagMap& tags)
{
for (const auto& value : values) std::optional<Release> release;
performers[role].push_back(Artist {value});
} auto releaseName{ getPropertyValueAs<std::string>(tags, "ALBUM") };
} if (!releaseName)
return release;
return performers;
} release.emplace();
release->name = std::move(*releaseName);
static release->artistDisplayName = getPropertyValueAs<std::string_view>(tags, "ALBUMARTIST").value_or("");
std::optional<Release> release->mbid = getPropertyValueFirstMatchAs<UUID>(tags, { "MUSICBRAINZ_ALBUMID", "MUSICBRAINZ ALBUM ID", "MUSICBRAINZ/ALBUM ID" });
getRelease(const TagMap& tags) release->artists = getArtists(tags, { "ALBUMARTISTS", "ALBUMARTIST" }, { "ALBUMARTISTSSORT", "ALBUMARTISTSORT" }, { "MUSICBRAINZ_ALBUMARTISTID", "MUSICBRAINZ ALBUM ARTIST ID", "MUSICBRAINZ/ALBUM ARTIST ID" });
{ release->mediumCount = getPropertyValueAs<std::size_t>(tags, "DISCTOTAL");
std::optional<Release> release; if (!release->mediumCount)
{
auto releaseName {getPropertyValueAs<std::string>(tags, "ALBUM")}; // mediumCount may be encoded as "position/count"
if (!releaseName) if (const auto value{ getPropertyValueAs<std::string_view>(tags, "DISCNUMBER") })
return release; {
// Expecting 'Number/Total'
release.emplace(); const std::vector<std::string_view> strings{ StringUtils::splitString(*value, "/") };
release->name = std::move(*releaseName); if (strings.size() == 2)
release->artistDisplayName = getPropertyValueAs<std::string_view>(tags, "ALBUMARTIST").value_or(""); release->mediumCount = StringUtils::readAs<std::size_t>(strings[1]);
release->mbid = getPropertyValueFirstMatchAs<UUID>(tags, {"MUSICBRAINZ_ALBUMID", "MUSICBRAINZ ALBUM ID", "MUSICBRAINZ/ALBUM ID"}); }
release->artists = getArtists(tags, {"ALBUMARTISTS", "ALBUMARTIST"}, {"ALBUMARTISTSSORT", "ALBUMARTISTSORT"}, {"MUSICBRAINZ_ALBUMARTISTID", "MUSICBRAINZ ALBUM ARTIST ID", "MUSICBRAINZ/ALBUM ARTIST ID"}); }
release->mediumCount = getPropertyValueAs<std::size_t>(tags, "DISCTOTAL");
if (!release->mediumCount) release->primaryType = getPropertyValueFirstMatchAs<MetaData::Release::PrimaryType>(tags, { "MUSICBRAINZ_ALBUMTYPE", "RELEASETYPE", "MUSICBRAINZ ALBUM TYPE", "MUSICBRAINZ/ALBUM TYPE" });
{ if (release->primaryType)
// mediumCount may be encoded as "position/count" {
if (const auto value {getPropertyValueAs<std::string_view>(tags, "DISCNUMBER")}) const auto secondaryTypes{ getPropertyValuesFirstMatchAs<MetaData::Release::SecondaryType>(tags, {"MUSICBRAINZ_ALBUMTYPE", "RELEASETYPE", "MUSICBRAINZ ALBUM TYPE", "MUSICBRAINZ/ALBUM TYPE"}) };
{ release->secondaryTypes.assign(std::cbegin(secondaryTypes), std::cend(secondaryTypes));
// Expecting 'Number/Total' }
const std::vector<std::string_view> strings {StringUtils::splitString(*value, "/") };
if (strings.size() == 2) return release;
release->mediumCount = StringUtils::readAs<std::size_t>(strings[1]); }
}
} std::optional<Medium> getMedium(const TagMap& tags)
{
release->primaryType = getPropertyValueFirstMatchAs<MetaData::Release::PrimaryType>(tags, {"MUSICBRAINZ_ALBUMTYPE", "RELEASETYPE", "MUSICBRAINZ ALBUM TYPE", "MUSICBRAINZ/ALBUM TYPE"}); std::optional<Medium> medium;
if (release->primaryType) medium.emplace();
{
const auto secondaryTypes {getPropertyValuesFirstMatchAs<MetaData::Release::SecondaryType>(tags, {"MUSICBRAINZ_ALBUMTYPE", "RELEASETYPE", "MUSICBRAINZ ALBUM TYPE", "MUSICBRAINZ/ALBUM TYPE"})}; medium->type = getPropertyValueAs<std::string>(tags, "MEDIA").value_or("");
release->secondaryTypes.assign(std::cbegin(secondaryTypes), std::cend(secondaryTypes)); medium->name = getPropertyValueFirstMatchAs<std::string>(tags, { "DISCSUBTITLE", "SETSUBTITLE" }).value_or("");
} medium->trackCount = getPropertyValueAs<std::size_t>(tags, "TRACKTOTAL");
if (!medium->trackCount)
return release; {
} // totalTracks may be encoded as "position/count"
if (const auto value{ getPropertyValueAs<std::string_view>(tags, "TRACKNUMBER") })
static {
std::optional<Medium> // Expecting 'Number/Total'
getMedium(const TagMap& tags) const std::vector<std::string_view> strings{ StringUtils::splitString(*value, "/") };
{ if (strings.size() == 2)
std::optional<Medium> medium; medium->trackCount = StringUtils::readAs<std::size_t>(strings[1]);
medium.emplace(); }
}
medium->type = getPropertyValueAs<std::string>(tags, "MEDIA").value_or(""); // Expecting 'Number[/Total]'
medium->name = getPropertyValueFirstMatchAs<std::string>(tags, {"DISCSUBTITLE", "SETSUBTITLE"}).value_or(""); medium->position = getPropertyValueAs<std::size_t>(tags, "DISCNUMBER");
medium->trackCount = getPropertyValueAs<std::size_t>(tags, "TRACKTOTAL"); medium->release = getRelease(tags);
if (!medium->trackCount) medium->replayGain = getPropertyValueAs<float>(tags, "REPLAYGAIN_ALBUM_GAIN");
{
// totalTracks may be encoded as "position/count" if (medium->type.empty()
if (const auto value {getPropertyValueAs<std::string_view>(tags, "TRACKNUMBER")}) && medium->name.empty()
{ && !medium->trackCount
// Expecting 'Number/Total' && !medium->position
const std::vector<std::string_view> strings {StringUtils::splitString(*value, "/") }; && !medium->release
if (strings.size() == 2) && !medium->replayGain)
medium->trackCount = StringUtils::readAs<std::size_t>(strings[1]); {
} medium.reset();
} }
// Expecting 'Number[/Total]'
medium->position = getPropertyValueAs<std::size_t>(tags, "DISCNUMBER"); return medium;
medium->release = getRelease(tags); }
medium->replayGain = getPropertyValueAs<float>(tags, "REPLAYGAIN_ALBUM_GAIN");
TagLib::AudioProperties::ReadStyle readStyleToTagLibReadStyle(ParserReadStyle readStyle)
if (medium->type.empty() {
&& medium->name.empty() switch (readStyle)
&& !medium->trackCount {
&& !medium->position case ParserReadStyle::Fast: return TagLib::AudioProperties::ReadStyle::Fast;
&& !medium->release case ParserReadStyle::Average: return TagLib::AudioProperties::ReadStyle::Average;
&& !medium->replayGain) case ParserReadStyle::Accurate: return TagLib::AudioProperties::ReadStyle::Accurate;
{ }
medium.reset();
} throw LmsException{ "Cannot convert read style" };
}
return medium;
} TagMap constructTagMap(const TagLib::PropertyMap& properties)
{
static TagMap tagMap;
TagLib::AudioProperties::ReadStyle
readStyleToTagLibReadStyle(ParserReadStyle readStyle) for (const auto& [propertyName, propertyValues] : properties)
{ {
switch (readStyle) std::vector<std::string>& values{ tagMap[propertyName.upper().to8Bit(true)] };
{ for (const TagLib::String& propertyValue : propertyValues)
case ParserReadStyle::Fast: return TagLib::AudioProperties::ReadStyle::Fast; {
case ParserReadStyle::Average: return TagLib::AudioProperties::ReadStyle::Average; std::string trimedValue{ StringUtils::stringTrim(propertyValue.to8Bit(true)) };
case ParserReadStyle::Accurate: return TagLib::AudioProperties::ReadStyle::Accurate; if (!trimedValue.empty())
} values.emplace_back(std::move(trimedValue));
}
throw LmsException {"Cannot convert read style"}; }
}
return tagMap;
TagLibParser::TagLibParser(ParserReadStyle readStyle) }
: _readStyle {readStyleToTagLibReadStyle(readStyle)}
{ void mergeTagMaps(TagMap& dst, TagMap&& src)
} {
for (auto&& [tag, values] : src)
void {
TagLibParser::processTag(Track& track, const std::string& tag, const std::vector<std::string>& values, bool debug) if (dst.find(tag) == std::cend(dst))
{ dst[tag] = std::move(values);
if (debug) }
std::cout << "[" << tag << "] = " << StringUtils::joinStrings(values, "*SEP*") << std::endl; }
if (tag.empty() || values.empty()) }
return;
TagLibParser::TagLibParser(ParserReadStyle readStyle)
std::string_view value {values.front()}; : _readStyle{ readStyleToTagLibReadStyle(readStyle) }
{
if (tag == "TITLE") }
track.title = value;
else if (tag == "MUSICBRAINZ_RELEASETRACKID" void TagLibParser::processTag(Track& track, const std::string& tag, const std::vector<std::string>& values, bool debug)
|| tag == "MUSICBRAINZ RELEASE TRACK ID" {
|| tag == "MUSICBRAINZ/RELEASE TRACK ID") if (debug)
{ std::cout << "[" << tag << "] = " << StringUtils::joinStrings(values, "*SEP*") << std::endl;
track.mbid = UUID::fromString(value);
} if (tag.empty() || values.empty())
else if (tag == "MUSICBRAINZ_TRACKID" return;
|| tag == "MUSICBRAINZ TRACK ID"
|| tag == "MUSICBRAINZ/TRACK ID") std::string_view value{ values.front() };
track.recordingMBID = UUID::fromString(value);
else if (tag == "ACOUSTID_ID") if (tag == "TITLE")
track.acoustID = UUID::fromString(value); track.title = value;
else if (tag == "TRACKNUMBER") else if (tag == "MUSICBRAINZ_RELEASETRACKID"
{ || tag == "MUSICBRAINZ RELEASE TRACK ID"
// Expecting 'Number/Total' || tag == "MUSICBRAINZ/RELEASE TRACK ID")
track.position = StringUtils::readAs<std::size_t>(value); {
} track.mbid = UUID::fromString(value);
else if (tag == "DATE") }
{ else if (tag == "MUSICBRAINZ_TRACKID"
// Higher priority than YEAR || tag == "MUSICBRAINZ TRACK ID"
if (const Wt::WDate date {Utils::parseDate(value)}; date.isValid()) || tag == "MUSICBRAINZ/TRACK ID")
track.date = date; track.recordingMBID = UUID::fromString(value);
} else if (tag == "ACOUSTID_ID")
else if (tag == "YEAR" && !track.date.isValid()) track.acoustID = UUID::fromString(value);
{ else if (tag == "TRACKNUMBER")
// lower priority than DATE {
track.date = Utils::parseDate(value); // Expecting 'Number/Total'
} track.position = StringUtils::readAs<std::size_t>(value);
else if (tag == "ORIGINALDATE") }
{ else if (tag == "DATE")
// Higher priority than ORIGINALYEAR {
if (const Wt::WDate date {Utils::parseDate(value)}; date.isValid()) // Higher priority than YEAR
track.originalDate = date; if (const Wt::WDate date{ Utils::parseDate(value) }; date.isValid())
} track.date = date;
else if (tag == "ORIGINALYEAR" && !track.originalDate.isValid()) }
{ else if (tag == "YEAR" && !track.date.isValid())
// Lower priority than ORIGINALDATE {
track.originalDate = Utils::parseDate(value); // lower priority than DATE
} track.date = Utils::parseDate(value);
else if (tag == "METADATA_BLOCK_PICTURE") }
track.hasCover = true; else if (tag == "ORIGINALDATE")
else if (tag == "COPYRIGHT") {
track.copyright = value; // Higher priority than ORIGINALYEAR
else if (tag == "COPYRIGHTURL") if (const Wt::WDate date{ Utils::parseDate(value) }; date.isValid())
track.copyrightURL = value; track.originalDate = date;
else if (tag == "REPLAYGAIN_TRACK_GAIN") }
track.replayGain = StringUtils::readAs<float>(value); else if (tag == "ORIGINALYEAR" && !track.originalDate.isValid())
else if (tag == "ARTIST") {
track.artistDisplayName = value; // Lower priority than ORIGINALDATE
else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end()) track.originalDate = Utils::parseDate(value);
{ }
std::set<std::string> clusterNames; else if (tag == "METADATA_BLOCK_PICTURE")
for (std::string_view valueList : values) track.hasCover = true;
{ else if (tag == "COPYRIGHT")
const std::vector<std::string_view> splittedValues {splitAndTrimString(valueList, "/,;")}; track.copyright = value;
for (std::string_view value : splittedValues) else if (tag == "COPYRIGHTURL")
clusterNames.insert(std::string {value}); track.copyrightURL = value;
} else if (tag == "REPLAYGAIN_TRACK_GAIN")
track.replayGain = StringUtils::readAs<float>(value);
if (!clusterNames.empty()) else if (tag == "ARTIST")
track.tags[tag] = std::move(clusterNames); track.artistDisplayName = value;
} else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end())
} {
std::set<std::string> clusterNames;
static for (std::string_view valueList : values)
TagMap {
constructTagMap(const TagLib::PropertyMap& properties) const std::vector<std::string_view> splittedValues{ splitAndTrimString(valueList, "/,;") };
{ for (std::string_view value : splittedValues)
TagMap tagMap; clusterNames.insert(std::string{ value });
}
for (const auto& [propertyName, propertyValues] : properties)
{ if (!clusterNames.empty())
std::vector<std::string>& values {tagMap[propertyName.upper().to8Bit(true)]}; track.tags[tag] = std::move(clusterNames);
for (const TagLib::String& propertyValue : propertyValues) }
{ }
std::string trimedValue {StringUtils::stringTrim(propertyValue.to8Bit(true))};
if (!trimedValue.empty()) std::optional<Track> TagLibParser::parse(const std::filesystem::path& p, bool debug)
values.emplace_back(std::move(trimedValue)); {
} TagLib::FileRef f{ p.string().c_str(),
} true, // read audio properties
_readStyle };
return tagMap;
} if (f.isNull())
{
static LMS_LOG(METADATA, ERROR) << "File '" << p.string() << "': parsing failed";
void return std::nullopt;
mergeTagMaps(TagMap& dst, TagMap&& src) }
{
for (auto&& [tag, values] : src) Track track;
{
if (dst.find(tag) == std::cend(dst)) if (const TagLib::AudioProperties* properties{ f.audioProperties() })
dst[tag] = std::move(values); {
} track.duration = std::chrono::milliseconds{ properties->lengthInMilliseconds() };
} track.bitrate = static_cast<std::size_t>(properties->bitrate() * 1000);
}
std::optional<Track> else
TagLibParser::parse(const std::filesystem::path& p, bool debug) {
{ LMS_LOG(METADATA, INFO) << "File '" << p.string() << "': no audio properties";
TagLib::FileRef f {p.string().c_str(), return std::nullopt;
true, // read audio properties }
_readStyle};
TagMap tags{ constructTagMap(f.file()->properties()) };
if (f.isNull())
{ auto getAPETags = [&](const TagLib::APE::Tag* apeTag)
LMS_LOG(METADATA, ERROR) << "File '" << p.string() << "': parsing failed"; {
return std::nullopt; if (!apeTag)
} return;
if (!f.audioProperties()) mergeTagMaps(tags, constructTagMap(apeTag->properties()));
{ };
LMS_LOG(METADATA, INFO) << "File '" << p.string() << "': no audio properties";
return std::nullopt; // Not that good embedded pictures handling
}
// WMA
Track track; if (TagLib::ASF::File * asfFile{ dynamic_cast<TagLib::ASF::File*>(f.file()) })
{
{ const TagLib::ASF::Tag* tag{ asfFile->tag() };
const TagLib::AudioProperties *properties {f.audioProperties() }; if (tag)
{
track.duration = std::chrono::milliseconds {properties->lengthInMilliseconds()}; if (tag->attributeListMap().contains("WM/Picture"))
track.hasCover = true;
MetaData::AudioStream audioStream {static_cast<unsigned>(properties->bitrate() * 1000)};
track.audioStreams = {audioStream}; for (const auto& [name, attributeList] : tag->attributeListMap())
} {
std::string strName{ StringUtils::stringToUpper(name.to8Bit(true)) };
TagMap tags {constructTagMap(f.file()->properties())}; if (strName.find("WM/") == 0 || tags.find(strName) != std::cend(tags))
continue;
auto getAPETags = [&](const TagLib::APE::Tag* apeTag)
{ std::vector<std::string> attributes;
if (!apeTag) for (const auto& attribute : attributeList)
return; {
if (attribute.type() == TagLib::ASF::Attribute::AttributeTypes::UnicodeType)
mergeTagMaps(tags, constructTagMap(apeTag->properties())); attributes.emplace_back(attribute.toString().to8Bit(true));
}; }
// Not that good embedded pictures handling if (!attributes.empty())
{
// WMA if (debug)
if (TagLib::ASF::File* asfFile {dynamic_cast<TagLib::ASF::File*>(f.file())}) std::cout << "ASF property: '" << name << "'" << std::endl;
{
const TagLib::ASF::Tag* tag {asfFile->tag()}; tags.emplace(strName, std::move(attributes));
if (tag) }
{ }
if (tag->attributeListMap().contains("WM/Picture")) }
track.hasCover = true; }
// MP3
for (const auto& [name, attributeList] : tag->attributeListMap()) else if (TagLib::MPEG::File * mp3File{ dynamic_cast<TagLib::MPEG::File*>(f.file()) })
{ {
std::string strName {StringUtils::stringToUpper(name.to8Bit(true))}; if (mp3File->ID3v2Tag())
if (strName.find("WM/") == 0 || tags.find(strName) != std::cend(tags)) {
continue; const auto& frameListMap{ mp3File->ID3v2Tag()->frameListMap() };
std::vector<std::string> attributes; if (!frameListMap["APIC"].isEmpty())
for (const auto& attribute : attributeList) track.hasCover = true;
{ if (!frameListMap["TSST"].isEmpty())
if (attribute.type() == TagLib::ASF::Attribute::AttributeTypes::UnicodeType) tags["DISCSUBTITLE"] = { frameListMap["TSST"].front()->toString().to8Bit(true) };
attributes.emplace_back(attribute.toString().to8Bit(true)); }
}
getAPETags(mp3File->APETag());
if (!attributes.empty()) }
{ //MP4
if (debug) else if (TagLib::MP4::File * mp4File{ dynamic_cast<TagLib::MP4::File*>(f.file()) })
std::cout << "ASF property: '" << name << "'" << std::endl; {
TagLib::MP4::Item coverItem{ mp4File->tag()->item("covr") };
tags.emplace(strName, std::move(attributes)); TagLib::MP4::CoverArtList coverArtList{ coverItem.toCoverArtList() };
} if (!coverArtList.isEmpty())
} track.hasCover = true;
} }
} // MPC
// MP3 else if (TagLib::MPC::File * mpcFile{ dynamic_cast<TagLib::MPC::File*>(f.file()) })
else if (TagLib::MPEG::File* mp3File {dynamic_cast<TagLib::MPEG::File*>(f.file())}) {
{ getAPETags(mpcFile->APETag());
if (mp3File->ID3v2Tag()) }
{ // WavPack
const auto& frameListMap {mp3File->ID3v2Tag()->frameListMap()}; else if (TagLib::WavPack::File * wavPackFile{ dynamic_cast<TagLib::WavPack::File*>(f.file()) })
{
if (!frameListMap["APIC"].isEmpty()) getAPETags(wavPackFile->APETag());
track.hasCover = true; }
if (!frameListMap["TSST"].isEmpty()) // FLAC
tags["DISCSUBTITLE"] = {frameListMap["TSST"].front()->toString().to8Bit(true)}; else if (TagLib::FLAC::File * flacFile{ dynamic_cast<TagLib::FLAC::File*>(f.file()) })
} {
if (!flacFile->pictureList().isEmpty())
getAPETags(mp3File->APETag()); track.hasCover = true;
} }
//MP4 else if (TagLib::Ogg::Vorbis::File * vorbisFile{ dynamic_cast<TagLib::Ogg::Vorbis::File*>(f.file()) })
else if (TagLib::MP4::File* mp4File {dynamic_cast<TagLib::MP4::File*>(f.file())}) {
{ if (!vorbisFile->tag()->pictureList().isEmpty())
TagLib::MP4::Item coverItem {mp4File->tag()->item("covr")}; track.hasCover = true;
TagLib::MP4::CoverArtList coverArtList {coverItem.toCoverArtList()}; }
if (!coverArtList.isEmpty()) else if (TagLib::Ogg::Opus::File * opusFile{ dynamic_cast<TagLib::Ogg::Opus::File*>(f.file()) })
track.hasCover = true; {
} if (!opusFile->tag()->pictureList().isEmpty())
// MPC track.hasCover = true;
else if (TagLib::MPC::File* mpcFile {dynamic_cast<TagLib::MPC::File*>(f.file())}) }
{
getAPETags(mpcFile->APETag()); track.medium = getMedium(tags);
} track.artists = getArtists(tags, { "ARTISTS", "ARTIST" }, { "ARTISTSORT" }, { "MUSICBRAINZ_ARTISTID", "MUSICBRAINZ ARTIST ID", "MUSICBRAINZ/ARTIST ID" });
// WavPack track.conductorArtists = getArtists(tags, { "CONDUCTORS", "CONDUCTOR" }, { "CONDUCTORSSORT", "CONDUCTORSORT" }, {});
else if (TagLib::WavPack::File* wavPackFile {dynamic_cast<TagLib::WavPack::File*>(f.file())}) track.composerArtists = getArtists(tags, { "COMPOSERS", "COMPOSER" }, { "COMPOSERSSORT", "COMPOSERSORT" }, {});
{ track.lyricistArtists = getArtists(tags, { "LYRICISTS", "LYRICIST" }, { "LYRICISTSSORT", "LYRICISTSORT" }, {});
getAPETags(wavPackFile->APETag()); track.mixerArtists = getArtists(tags, { "MIXERS", "MIXER" }, { "MIXERSSORT", "MIXERSORT" }, {});
} track.producerArtists = getArtists(tags, { "PRODUCERS", "PRODUCER" }, { "PRODUCERSSORT", "PRODUCERSORT" }, {});
// FLAC track.remixerArtists = getArtists(tags, { "REMIXERS", "REMIXER", "ModifiedBy" }, { "REMIXERSSORT", "REMIXERSORT" }, {});
else if (TagLib::FLAC::File* flacFile {dynamic_cast<TagLib::FLAC::File*>(f.file())}) track.performerArtists = getPerformerArtists(tags, { "PERFORMERS", "PERFORMER" });
{
if (!flacFile->pictureList().isEmpty()) for (const auto& [tag, values] : tags)
track.hasCover = true; processTag(track, tag, values, debug);
}
else if (TagLib::Ogg::Vorbis::File* vorbisFile {dynamic_cast<TagLib::Ogg::Vorbis::File*>(f.file())}) return track;
{ }
if (!vorbisFile->tag()->pictureList().isEmpty())
track.hasCover = true;
}
else if (TagLib::Ogg::Opus::File* opusFile {dynamic_cast<TagLib::Ogg::Opus::File*>(f.file())})
{
if (!opusFile->tag()->pictureList().isEmpty())
track.hasCover = true;
}
track.medium = getMedium(tags);
track.artists = getArtists(tags, {"ARTISTS", "ARTIST"}, {"ARTISTSORT"}, {"MUSICBRAINZ_ARTISTID", "MUSICBRAINZ ARTIST ID", "MUSICBRAINZ/ARTIST ID"});
track.conductorArtists = getArtists(tags, {"CONDUCTORS", "CONDUCTOR"}, {"CONDUCTORSSORT", "CONDUCTORSORT"}, {});
track.composerArtists = getArtists(tags, {"COMPOSERS", "COMPOSER"}, {"COMPOSERSSORT", "COMPOSERSORT"}, {});
track.lyricistArtists = getArtists(tags, {"LYRICISTS", "LYRICIST"}, {"LYRICISTSSORT", "LYRICISTSORT"}, {});
track.mixerArtists = getArtists(tags, {"MIXERS", "MIXER"}, {"MIXERSSORT", "MIXERSORT"}, {});
track.producerArtists = getArtists(tags, {"PRODUCERS", "PRODUCER"}, {"PRODUCERSSORT", "PRODUCERSORT"}, {});
track.remixerArtists = getArtists(tags, {"REMIXERS", "REMIXER", "ModifiedBy"}, {"REMIXERSSORT", "REMIXERSORT"}, {});
track.performerArtists = getPerformerArtists(tags, {"PERFORMERS", "PERFORMER"});
for (const auto& [tag, values] : tags)
processTag(track, tag, values, debug);
return track;
}
} // namespace MetaData } // namespace MetaData
+101 -106
View File
@@ -34,126 +34,121 @@
namespace MetaData namespace MetaData
{ {
using Tags = std::map<std::string /* type */, std::set<std::string> /* names */>; using Tags = std::map<std::string /* type */, std::set<std::string> /* names */>;
// Very simplified version of https://musicbrainz.org/doc/MusicBrainz_Database/Schema // Very simplified version of https://musicbrainz.org/doc/MusicBrainz_Database/Schema
struct Artist struct Artist
{ {
std::optional<UUID> mbid; std::optional<UUID> mbid;
std::string name; std::string name;
std::optional<std::string> sortName; std::optional<std::string> sortName;
Artist(std::string_view _name) : name {_name} {} Artist(std::string_view _name) : name{ _name } {}
Artist(std::optional<UUID> _mbid, std::string_view _name, std::optional<std::string> _sortName) : mbid {std::move(_mbid)}, name {_name}, sortName {std::move(_sortName)} {} Artist(std::optional<UUID> _mbid, std::string_view _name, std::optional<std::string> _sortName) : mbid{ std::move(_mbid) }, name{ _name }, sortName{ std::move(_sortName) } {}
}; };
using PerformerContainer = std::map<std::string /*role*/, std::vector<Artist>>; using PerformerContainer = std::map<std::string /*role*/, std::vector<Artist>>;
struct Release struct Release
{ {
// see https://musicbrainz.org/doc/Release_Group/Type // see https://musicbrainz.org/doc/Release_Group/Type
enum class PrimaryType enum class PrimaryType
{ {
Album, Album,
Single, Single,
EP, EP,
Broadcast, Broadcast,
Other Other
}; };
enum class SecondaryType enum class SecondaryType
{ {
Compilation, Compilation,
Soundtrack, Soundtrack,
Spokenword, Spokenword,
Interview, Interview,
Audiobook, Audiobook,
AudioDrama, AudioDrama,
Live, Live,
Remix, Remix,
DJMix, DJMix,
Mixtape_Street, Mixtape_Street,
Demo, Demo,
}; };
std::optional<UUID> mbid; std::optional<UUID> mbid;
std::string name; std::string name;
std::string artistDisplayName; std::string artistDisplayName;
std::vector<Artist> artists; std::vector<Artist> artists;
std::optional<std::size_t> mediumCount; std::optional<std::size_t> mediumCount;
std::optional<PrimaryType> primaryType; std::optional<PrimaryType> primaryType;
EnumSet<SecondaryType> secondaryTypes; EnumSet<SecondaryType> secondaryTypes;
}; };
struct Medium struct Medium
{ {
std::string type; std::string type;
std::string name; std::string name;
std::optional<Release> release; std::optional<Release> release;
std::optional<std::size_t> position; // in release std::optional<std::size_t> position; // in release
std::optional<std::size_t> trackCount; std::optional<std::size_t> trackCount;
std::optional<float> replayGain; std::optional<float> replayGain;
}; };
struct AudioStream struct Track
{ {
unsigned bitRate; std::optional<UUID> mbid;
}; std::optional<UUID> recordingMBID;
std::string title;
std::optional<Medium> medium;
std::optional<std::size_t> position; // in medium
Tags tags;
std::chrono::milliseconds duration{};
std::size_t bitrate{};
Wt::WDate date;
Wt::WDate originalDate;
bool hasCover{};
std::optional<UUID> acoustID;
std::string copyright;
std::string copyrightURL;
std::optional<float> replayGain;
std::string artistDisplayName;
std::vector<Artist> artists;
std::vector<Artist> conductorArtists;
std::vector<Artist> composerArtists;
std::vector<Artist> lyricistArtists;
std::vector<Artist> mixerArtists;
PerformerContainer performerArtists;
std::vector<Artist> producerArtists;
std::vector<Artist> remixerArtists;
};
struct Track class IParser
{ {
std::optional<UUID> mbid; public:
std::optional<UUID> recordingMBID; virtual ~IParser() = default;
std::string title;
std::optional<Medium> medium;
std::optional<std::size_t> position; // in medium
Tags tags;
std::chrono::milliseconds duration;
Wt::WDate date;
Wt::WDate originalDate;
bool hasCover {};
std::vector<AudioStream> audioStreams;
std::optional<UUID> acoustID;
std::string copyright;
std::string copyrightURL;
std::optional<float> replayGain;
std::string artistDisplayName;
std::vector<Artist> artists;
std::vector<Artist> conductorArtists;
std::vector<Artist> composerArtists;
std::vector<Artist> lyricistArtists;
std::vector<Artist> mixerArtists;
PerformerContainer performerArtists;
std::vector<Artist> producerArtists;
std::vector<Artist> remixerArtists;
};
class IParser virtual std::optional<Track> parse(const std::filesystem::path& p, bool debug = false) = 0;
{
public:
virtual ~IParser() = default;
virtual std::optional<Track> parse(const std::filesystem::path& p, bool debug = false) = 0; void setClusterTypeNames(const std::set<std::string>& clusterTypeNames) { _clusterTypeNames = clusterTypeNames; }
void setClusterTypeNames(const std::set<std::string>& clusterTypeNames) { _clusterTypeNames = clusterTypeNames; } protected:
std::set<std::string> _clusterTypeNames;
};
protected: enum class ParserType
std::set<std::string> _clusterTypeNames; {
}; TagLib,
AvFormat,
};
enum class ParserType enum class ParserReadStyle
{ {
TagLib, Fast,
AvFormat, Average,
}; Accurate,
};
enum class ParserReadStyle std::unique_ptr<IParser> createParser(ParserType parserType, ParserReadStyle parserReadStyle);
{
Fast,
Average,
Accurate,
};
std::unique_ptr<IParser> createParser(ParserType parserType, ParserReadStyle parserReadStyle);
} // namespace MetaData } // namespace MetaData
@@ -345,9 +345,6 @@ namespace Cover
} }
} }
if (!cover)
cover = getDefault(width);
if (cover) if (cover)
saveToCache(cacheEntryDesc, cover); saveToCache(cacheEntryDesc, cover);
@@ -377,8 +374,8 @@ namespace Cover
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
const auto tracks{ Track::find(session, Track::FindParameters {}.setRelease(releaseId).setRange({0, 1}).setSortMethod(TrackSortMethod::Release)) }; // get a track in this release, consider the release is in a single directory
const auto tracks{ Track::find(session, Track::FindParameters {}.setRelease(releaseId).setRange(Range{ 0, 1 }).setSortMethod(TrackSortMethod::Release)) };
if (!tracks.results.empty()) if (!tracks.results.empty())
{ {
const Track::pointer& track{ tracks.results.front() }; const Track::pointer& track{ tracks.results.front() };
@@ -397,9 +394,6 @@ namespace Cover
cover = getFromTrack(session, releaseInfo->firstTrackId, width, false /* no release fallback */); cover = getFromTrack(session, releaseInfo->firstTrackId, width, false /* no release fallback */);
} }
if (!cover)
cover = getDefault(width);
if (cover) if (cover)
saveToCache(cacheEntryDesc, cover); saveToCache(cacheEntryDesc, cover);
+64 -70
View File
@@ -35,103 +35,97 @@
namespace Database namespace Database
{ {
class Session; class Session;
} }
namespace Av namespace Av
{ {
class IAudioFile; class IAudioFile;
} }
namespace Cover namespace Cover
{ {
struct CacheEntryDesc struct CacheEntryDesc
{ {
std::variant<Database::TrackId, Database::ReleaseId> id; std::variant<Database::TrackId, Database::ReleaseId> id;
std::size_t size; std::size_t size;
bool operator==(const CacheEntryDesc& other) const
{
return id == other.id
&& size == other.size;
}
};
bool operator==(const CacheEntryDesc& other) const
{
return id == other.id
&& size == other.size;
}
};
} // ns Cover } // ns Cover
namespace std namespace std
{ {
template<>
template<> class hash<Cover::CacheEntryDesc>
class hash<Cover::CacheEntryDesc> {
{ public:
public: size_t operator()(const Cover::CacheEntryDesc& e) const
size_t operator()(const Cover::CacheEntryDesc& e) const {
{ size_t h{};
size_t h {}; std::visit([&](auto id)
std::visit([&](auto id) {
{ using IdType = std::decay_t<decltype(id)>;
using IdType = std::decay_t<decltype(id)>; h ^= std::hash<IdType>()(id);
h ^= std::hash<IdType>()(id); }, e.id);
}, e.id); h ^= std::hash<std::size_t>()(e.size) << 1;
h ^= std::hash<std::size_t>()(e.size) << 1; return h;
return h; }
} };
};
} // ns std } // ns std
namespace Cover namespace Cover
{ {
class CoverService : public ICoverService class CoverService : public ICoverService
{ {
public: public:
CoverService(Database::Db& db, CoverService(Database::Db& db, const std::filesystem::path& execPath, const std::filesystem::path& defaultCoverPath);
const std::filesystem::path& execPath,
const std::filesystem::path& defaultCoverPath);
CoverService(const CoverService&) = delete; CoverService(const CoverService&) = delete;
CoverService& operator=(const CoverService&) = delete; CoverService& operator=(const CoverService&) = delete;
CoverService(CoverService&&) = delete;
CoverService& operator=(CoverService&&) = delete;
private: private:
std::shared_ptr<Image::IEncodedImage> getFromTrack(Database::TrackId trackId, Image::ImageSize width) override; std::shared_ptr<Image::IEncodedImage> getFromTrack(Database::TrackId trackId, Image::ImageSize width) override;
std::shared_ptr<Image::IEncodedImage> getFromRelease(Database::ReleaseId releaseId, Image::ImageSize width) override; std::shared_ptr<Image::IEncodedImage> getFromRelease(Database::ReleaseId releaseId, Image::ImageSize width) override;
void flushCache() override; std::shared_ptr<Image::IEncodedImage> getDefault(Image::ImageSize width) override;
void setJpegQuality(unsigned quality) override; void flushCache() override;
void setJpegQuality(unsigned quality) override;
std::shared_ptr<Image::IEncodedImage> getFromTrack(Database::Session& dbSession, Database::TrackId trackId, Image::ImageSize width, bool allowReleaseFallback); std::shared_ptr<Image::IEncodedImage> getFromTrack(Database::Session& dbSession, Database::TrackId trackId, Image::ImageSize width, bool allowReleaseFallback);
std::unique_ptr<Image::IEncodedImage> getFromAvMediaFile(const Av::IAudioFile& input, Image::ImageSize width) const; std::unique_ptr<Image::IEncodedImage> getFromAvMediaFile(const Av::IAudioFile& input, Image::ImageSize width) const;
std::unique_ptr<Image::IEncodedImage> getFromCoverFile(const std::filesystem::path& p, Image::ImageSize width) const; std::unique_ptr<Image::IEncodedImage> getFromCoverFile(const std::filesystem::path& p, Image::ImageSize width) const;
std::unique_ptr<Image::IEncodedImage> getFromTrack(const std::filesystem::path& path, Image::ImageSize width) const; std::unique_ptr<Image::IEncodedImage> getFromTrack(const std::filesystem::path& path, Image::ImageSize width) const;
std::multimap<std::string, std::filesystem::path> getCoverPaths(const std::filesystem::path& directoryPath) const; std::multimap<std::string, std::filesystem::path> getCoverPaths(const std::filesystem::path& directoryPath) const;
std::unique_ptr<Image::IEncodedImage> getFromDirectory(const std::filesystem::path& directory, Image::ImageSize width) const; std::unique_ptr<Image::IEncodedImage> getFromDirectory(const std::filesystem::path& directory, Image::ImageSize width) const;
std::unique_ptr<Image::IEncodedImage> getFromSameNamedFile(const std::filesystem::path& filePath, Image::ImageSize width) const; std::unique_ptr<Image::IEncodedImage> getFromSameNamedFile(const std::filesystem::path& filePath, Image::ImageSize width) const;
std::shared_ptr<Image::IEncodedImage> getDefault(Image::ImageSize width);
bool checkCoverFile(const std::filesystem::path& directoryPath) const; bool checkCoverFile(const std::filesystem::path& directoryPath) const;
Database::Db& _db; Database::Db& _db;
std::shared_mutex _cacheMutex; std::shared_mutex _cacheMutex;
std::unordered_map<CacheEntryDesc, std::shared_ptr<Image::IEncodedImage>> _cache; std::unordered_map<CacheEntryDesc, std::shared_ptr<Image::IEncodedImage>> _cache;
std::unordered_map<Image::ImageSize, std::shared_ptr<Image::IEncodedImage>> _defaultCoverCache; std::unordered_map<Image::ImageSize, std::shared_ptr<Image::IEncodedImage>> _defaultCoverCache;
std::atomic<std::size_t> _cacheMisses {}; std::atomic<std::size_t> _cacheMisses{};
std::atomic<std::size_t> _cacheHits {}; std::atomic<std::size_t> _cacheHits{};
std::size_t _cacheSize {}; std::size_t _cacheSize{};
void saveToCache(const CacheEntryDesc& entryDesc, std::shared_ptr<Image::IEncodedImage> image); void saveToCache(const CacheEntryDesc& entryDesc, std::shared_ptr<Image::IEncodedImage> image);
std::shared_ptr<Image::IEncodedImage> loadFromCache(const CacheEntryDesc& entryDesc); std::shared_ptr<Image::IEncodedImage> loadFromCache(const CacheEntryDesc& entryDesc);
const std::filesystem::path _defaultCoverPath; const std::filesystem::path _defaultCoverPath;
const std::size_t _maxCacheSize; const std::size_t _maxCacheSize;
static inline const std::vector<std::filesystem::path> _fileExtensions {".jpg", ".jpeg", ".png", ".bmp"}; // TODO parametrize static inline const std::vector<std::filesystem::path> _fileExtensions{ ".jpg", ".jpeg", ".png", ".bmp" }; // TODO parametrize
const std::size_t _maxFileSize; const std::size_t _maxFileSize;
const std::vector<std::string> _preferredFileNames; const std::vector<std::string> _preferredFileNames;
unsigned _jpegQuality; unsigned _jpegQuality;
}; };
} // namespace Cover } // namespace Cover
@@ -28,27 +28,27 @@
namespace Database namespace Database
{ {
class Db; class Db;
} }
namespace Cover namespace Cover
{ {
class ICoverService class ICoverService
{ {
public: public:
virtual ~ICoverService() = default; virtual ~ICoverService() = default;
virtual std::shared_ptr<Image::IEncodedImage> getFromTrack(Database::TrackId trackId, Image::ImageSize width) = 0; virtual std::shared_ptr<Image::IEncodedImage> getFromTrack(Database::TrackId trackId, Image::ImageSize width) = 0;
virtual std::shared_ptr<Image::IEncodedImage> getFromRelease(Database::ReleaseId releaseId, Image::ImageSize width) = 0; virtual std::shared_ptr<Image::IEncodedImage> getFromRelease(Database::ReleaseId releaseId, Image::ImageSize width) = 0;
virtual void flushCache() = 0; virtual std::shared_ptr<Image::IEncodedImage> getDefault(Image::ImageSize width) = 0;
virtual void setJpegQuality(unsigned quality) = 0; // from 1 to 100 virtual void flushCache() = 0;
};
std::unique_ptr<ICoverService> createCoverService(Database::Db& db, virtual void setJpegQuality(unsigned quality) = 0; // from 1 to 100
const std::filesystem::path& execPath, };
const std::filesystem::path& defaultCoverPath);
std::unique_ptr<ICoverService> createCoverService(Database::Db& db, const std::filesystem::path& execPath, const std::filesystem::path& defaultCoverPath);
} // namespace CoverArt } // namespace CoverArt
+14 -7
View File
@@ -210,12 +210,11 @@ namespace Database
} }
RangeResults<ArtistId> Artist::findOrphanIds(Session& session, Range range) RangeResults<ArtistId> Artist::findOrphanIds(Session& session, std::optional<Range> range)
{ {
session.checkSharedLocked(); session.checkSharedLocked();
auto query{ session.getDboSession().query<ArtistId>("SELECT DISTINCT a.id FROM artist a WHERE NOT EXISTS(SELECT 1 FROM track t INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id WHERE t.id = t_a_l.track_id)") }; auto query{ session.getDboSession().query<ArtistId>("SELECT DISTINCT a.id FROM artist a WHERE NOT EXISTS(SELECT 1 FROM track t INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id WHERE t.id = t_a_l.track_id)") };
return Utils::execQuery<ArtistId>(query, range);
return Utils::execQuery(query, range);
} }
RangeResults<ArtistId> Artist::findIds(Session& session, const FindParameters& params) RangeResults<ArtistId> Artist::findIds(Session& session, const FindParameters& params)
@@ -223,7 +222,7 @@ namespace Database
session.checkSharedLocked(); session.checkSharedLocked();
auto query{ createQuery<ArtistId>(session, params) }; auto query{ createQuery<ArtistId>(session, params) };
return Utils::execQuery(query, params.range); return Utils::execQuery<ArtistId>(query, params.range);
} }
RangeResults<Artist::pointer> Artist::find(Session& session, const FindParameters& params) RangeResults<Artist::pointer> Artist::find(Session& session, const FindParameters& params)
@@ -231,10 +230,18 @@ namespace Database
session.checkSharedLocked(); session.checkSharedLocked();
auto query{ createQuery<Wt::Dbo::ptr<Artist>>(session, params) }; auto query{ createQuery<Wt::Dbo::ptr<Artist>>(session, params) };
return Utils::execQuery(query, params.range); return Utils::execQuery<Artist::pointer>(query, params.range);
} }
RangeResults<ArtistId> Artist::findSimilarArtistIds(EnumSet<TrackArtistLinkType> artistLinkTypes, Range range) const void Artist::find(Session& session, const FindParameters& params, std::function<void(const pointer&)> func)
{
session.checkSharedLocked();
auto query{ createQuery<Wt::Dbo::ptr<Artist>>(session, params) };
Utils::execQuery(query, params.range, func);
}
RangeResults<ArtistId> Artist::findSimilarArtistIds(EnumSet<TrackArtistLinkType> artistLinkTypes, std::optional<Range> range) const
{ {
assert(session()); assert(session());
@@ -278,7 +285,7 @@ namespace Database
for (TrackArtistLinkType type : artistLinkTypes) for (TrackArtistLinkType type : artistLinkTypes)
query.bind(type); query.bind(type);
return Utils::execQuery(query, range); return Utils::execQuery<ArtistId>(query, range);
} }
std::vector<std::vector<Cluster::pointer>> Artist::getClusterGroups(std::vector<ClusterType::pointer> clusterTypes, std::size_t size) const std::vector<std::vector<Cluster::pointer>> Artist::getClusterGroups(std::vector<ClusterType::pointer> clusterTypes, std::size_t size) const
+55 -24
View File
@@ -32,11 +32,12 @@ namespace Database
{ {
namespace namespace
{ {
Wt::Dbo::Query<Cluster::ClusterFindResult> createQuery(Session& session, const Cluster::FindParameters& params) template <typename ResultType>
Wt::Dbo::Query<ResultType> createQuery(Session& session, std::string_view itemToSelect, const Cluster::FindParameters& params)
{ {
session.checkSharedLocked(); session.checkSharedLocked();
auto query{ session.getDboSession().query<Cluster::ClusterFindResult>("SELECT DISTINCT c.id,c.name FROM cluster c") }; auto query{ session.getDboSession().query<ResultType>("SELECT DISTINCT " + std::string{ itemToSelect } + " FROM cluster c") };
if (params.track.isValid() || params.release.isValid()) if (params.track.isValid() || params.release.isValid())
{ {
@@ -54,6 +55,21 @@ namespace Database
return query; return query;
} }
template <typename ResultType>
Wt::Dbo::Query<ResultType> createQuery(Session& session, const Cluster::FindParameters& params)
{
std::string_view itemToSelect;
if constexpr (std::is_same_v<ResultType, ClusterId>)
itemToSelect = "c.id";
else if constexpr (std::is_same_v<ResultType, Wt::Dbo::ptr<Cluster>>)
itemToSelect = "c";
else
static_assert("Unhandled type");
return createQuery<ResultType>(session, itemToSelect, params);
}
} }
Cluster::Cluster(ObjectPtr<ClusterType> type, std::string_view name) Cluster::Cluster(ObjectPtr<ClusterType> type, std::string_view name)
@@ -74,20 +90,28 @@ namespace Database
return session.getDboSession().query<int>("SELECT COUNT(*) FROM cluster"); return session.getDboSession().query<int>("SELECT COUNT(*) FROM cluster");
} }
RangeResults<Cluster::ClusterFindResult> Cluster::find(Session& session, const FindParameters& params) RangeResults<ClusterId> Cluster::findIds(Session& session, const FindParameters& params)
{ {
session.checkSharedLocked(); session.checkSharedLocked();
auto query{ createQuery(session, params) }; auto query{ createQuery<ClusterId>(session, params) };
return Utils::execQuery(query, params.range); return Utils::execQuery<ClusterId>(query, params.range);
} }
RangeResults<ClusterId> Cluster::findOrphans(Session& session, Range range) RangeResults<Cluster::pointer> Cluster::find(Session& session, const FindParameters& params)
{
session.checkSharedLocked();
auto query{ createQuery<Wt::Dbo::ptr<Cluster>>(session, params) };
return Utils::execQuery<Cluster::pointer>(query, params.range);
}
RangeResults<ClusterId> Cluster::findOrphans(Session& session, std::optional<Range> range)
{ {
session.checkSharedLocked(); session.checkSharedLocked();
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(query, range); return Utils::execQuery<ClusterId>(query, range);
} }
Cluster::pointer Cluster::find(Session& session, ClusterId id) Cluster::pointer Cluster::find(Session& session, ClusterId id)
@@ -97,30 +121,37 @@ namespace Database
return session.getDboSession().find<Cluster>().where("id = ?").bind(id).resultValue(); return session.getDboSession().find<Cluster>().where("id = ?").bind(id).resultValue();
} }
std::size_t Cluster::computeTrackCount(Session& session, ClusterId id)
{
session.checkSharedLocked();
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();
}
std::size_t Cluster::computeReleaseCount(Session& session, ClusterId id)
{
session.checkSharedLocked();
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();
}
void Cluster::addTrack(ObjectPtr<Track> track) void Cluster::addTrack(ObjectPtr<Track> track)
{ {
_tracks.insert(getDboPtr(track)); _tracks.insert(getDboPtr(track));
} }
RangeResults<TrackId> Cluster::getTracks(Range range) const RangeResults<TrackId> Cluster::getTracks(std::optional<Range> range) const
{ {
assert(session()); assert(session());
auto query{ session()->query<TrackId>("SELECT t.id FROM track t INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id") auto query{ session()->query<TrackId>("SELECT t.id FROM track t INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id")
.where("c.id = ?").bind(getId()) }; .where("c.id = ?").bind(getId()) };
return Utils::execQuery(query, range); return Utils::execQuery<TrackId>(query, range);
} }
std::size_t Cluster::getReleasesCount() const
{
assert(session());
return session()->query<int>("SELECT COUNT(DISTINCT r.id) FROM release r INNER JOIN track t on t.release_id = r.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")
.where("c.id = ?").bind(getId());
}
ClusterType::ClusterType(std::string_view name) ClusterType::ClusterType(std::string_view name)
: _name{ name } : _name{ name }
{ {
@@ -139,7 +170,7 @@ namespace Database
} }
RangeResults<ClusterTypeId> ClusterType::findOrphans(Session& session, Range range) RangeResults<ClusterTypeId> ClusterType::findOrphans(Session& session, std::optional<Range> range)
{ {
session.checkSharedLocked(); session.checkSharedLocked();
@@ -148,10 +179,10 @@ namespace Database
" LEFT OUTER JOIN cluster c ON c_t.id = c.cluster_type_id") " LEFT OUTER JOIN cluster c ON c_t.id = c.cluster_type_id")
.where("c.id IS NULL") }; .where("c.id IS NULL") };
return Utils::execQuery(query, range); return Utils::execQuery<ClusterTypeId>(query, range);
} }
RangeResults<ClusterTypeId> ClusterType::findUsed(Session& session, Range range) RangeResults<ClusterTypeId> ClusterType::findUsed(Session& session, std::optional<Range> range)
{ {
session.checkSharedLocked(); session.checkSharedLocked();
@@ -159,7 +190,7 @@ namespace Database
"SELECT DISTINCT c_t.id from cluster_type c_t") "SELECT DISTINCT c_t.id from cluster_type c_t")
.join("cluster c ON c_t.id = c.cluster_type_id") }; .join("cluster c ON c_t.id = c.cluster_type_id") };
return Utils::execQuery(query, range); return Utils::execQuery<ClusterTypeId>(query, range);
} }
ClusterType::pointer ClusterType::find(Session& session, std::string_view name) ClusterType::pointer ClusterType::find(Session& session, std::string_view name)
@@ -176,13 +207,13 @@ namespace Database
return session.getDboSession().find<ClusterType>().where("id = ?").bind(id).resultValue(); return session.getDboSession().find<ClusterType>().where("id = ?").bind(id).resultValue();
} }
RangeResults<ClusterTypeId> ClusterType::find(Session& session, Range range) RangeResults<ClusterTypeId> ClusterType::find(Session& session, std::optional<Range> range)
{ {
session.checkSharedLocked(); session.checkSharedLocked();
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(query, range); return Utils::execQuery<ClusterTypeId>(query, range);
} }
Cluster::pointer ClusterType::getCluster(const std::string& name) const Cluster::pointer ClusterType::getCluster(const std::string& name) const
+13 -13
View File
@@ -172,7 +172,7 @@ namespace Database
if (parameters.syncState) if (parameters.syncState)
query.where("sync_state = ?").bind(*parameters.syncState); query.where("sync_state = ?").bind(*parameters.syncState);
return Utils::execQuery(query, parameters.range); return Utils::execQuery<ListenId>(query, parameters.range);
} }
Listen::pointer Listen::find(Session& session, UserId userId, TrackId trackId, ScrobblingBackend backend, const Wt::WDateTime& dateTime) Listen::pointer Listen::find(Session& session, UserId userId, TrackId trackId, ScrobblingBackend backend, const Wt::WDateTime& dateTime)
@@ -187,7 +187,7 @@ namespace Database
.resultValue(); .resultValue();
} }
RangeResults<ArtistId> Listen::getTopArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range) RangeResults<ArtistId> Listen::getTopArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range)
{ {
session.checkSharedLocked(); session.checkSharedLocked();
auto query{ createArtistsQuery(session.getDboSession(), userId, backend, clusterIds, linkType) }; auto query{ createArtistsQuery(session.getDboSession(), userId, backend, clusterIds, linkType) };
@@ -196,57 +196,57 @@ namespace Database
.orderBy("COUNT(a.id) DESC") .orderBy("COUNT(a.id) DESC")
.groupBy("a.id") }; .groupBy("a.id") };
return Utils::execQuery(query, range); return Utils::execQuery<ArtistId>(query, range);
} }
RangeResults<ReleaseId> Listen::getTopReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, Range range) RangeResults<ReleaseId> Listen::getTopReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<Range> range)
{ {
session.checkSharedLocked(); session.checkSharedLocked();
auto query{ createReleasesQuery(session.getDboSession(), userId, backend, clusterIds) auto query{ createReleasesQuery(session.getDboSession(), userId, backend, clusterIds)
.orderBy("COUNT(r.id) DESC") .orderBy("COUNT(r.id) DESC")
.groupBy("r.id") }; .groupBy("r.id") };
return Utils::execQuery(query, range); return Utils::execQuery<ReleaseId>(query, range);
} }
RangeResults<TrackId> Listen::getTopTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, Range range) RangeResults<TrackId> Listen::getTopTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<Range> range)
{ {
session.checkSharedLocked(); session.checkSharedLocked();
auto query{ createTracksQuery(session.getDboSession(), userId, backend, clusterIds) auto query{ createTracksQuery(session.getDboSession(), userId, backend, clusterIds)
.orderBy("COUNT(t.id) DESC") .orderBy("COUNT(t.id) DESC")
.groupBy("t.id") }; .groupBy("t.id") };
return Utils::execQuery(query, range); return Utils::execQuery<TrackId>(query, range);
} }
RangeResults<ArtistId> Listen::getRecentArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range) RangeResults<ArtistId> Listen::getRecentArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range)
{ {
session.checkSharedLocked(); session.checkSharedLocked();
auto query{ createArtistsQuery(session.getDboSession(), userId, backend, clusterIds, linkType) auto query{ createArtistsQuery(session.getDboSession(), userId, backend, clusterIds, linkType)
.groupBy("a.id").having("l.date_time = MAX(l.date_time)") .groupBy("a.id").having("l.date_time = MAX(l.date_time)")
.orderBy("l.date_time DESC") }; .orderBy("l.date_time DESC") };
return Utils::execQuery(query, range); return Utils::execQuery<ArtistId>(query, range);
} }
RangeResults<ReleaseId> Listen::getRecentReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, Range range) RangeResults<ReleaseId> Listen::getRecentReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<Range> range)
{ {
session.checkSharedLocked(); session.checkSharedLocked();
auto query{ createReleasesQuery(session.getDboSession(), userId, backend, clusterIds) auto query{ createReleasesQuery(session.getDboSession(), userId, backend, clusterIds)
.groupBy("r.id").having("l.date_time = MAX(l.date_time)") .groupBy("r.id").having("l.date_time = MAX(l.date_time)")
.orderBy("l.date_time DESC") }; .orderBy("l.date_time DESC") };
return Utils::execQuery(query, range); return Utils::execQuery<ReleaseId>(query, range);
} }
RangeResults<TrackId> Listen::getRecentTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, Range range) RangeResults<TrackId> Listen::getRecentTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<Range> range)
{ {
session.checkSharedLocked(); session.checkSharedLocked();
auto query{ createTracksQuery(session.getDboSession(), userId, backend, clusterIds) auto query{ createTracksQuery(session.getDboSession(), userId, backend, clusterIds)
.groupBy("t.id").having("l.date_time = MAX(l.date_time)") .groupBy("t.id").having("l.date_time = MAX(l.date_time)")
.orderBy("l.date_time DESC") }; .orderBy("l.date_time DESC") };
return Utils::execQuery(query, range); return Utils::execQuery<TrackId>(query, range);
} }
std::size_t Listen::getCount(Session& session, UserId userId, ScrobblingBackend backend, TrackId trackId) std::size_t Listen::getCount(Session& session, UserId userId, ScrobblingBackend backend, TrackId trackId)
@@ -224,6 +224,31 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
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");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(session).modify()->incScanVersion();
}
static void migrateFromV44(Session& session)
{
// add bitrate
session.getDboSession().execute("ALTER TABLE track ADD bitrate INTEGER");
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(session).modify()->incScanVersion();
}
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)) + ")");
}
void doDbMigration(Session& session) void doDbMigration(Session& session)
{ {
static const std::string outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" }; static const std::string outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
@@ -245,6 +270,9 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
{40, migrateFromV40}, {40, migrateFromV40},
{41, migrateFromV41}, {41, migrateFromV41},
{42, migrateFromV42}, {42, migrateFromV42},
{43, migrateFromV43},
{44, migrateFromV44},
{45, migrateFromV45},
}; };
{ {
@@ -26,7 +26,7 @@ namespace Database
class Session; class Session;
using Version = std::size_t; using Version = std::size_t;
static constexpr Version LMS_DATABASE_VERSION{ 43 }; static constexpr Version LMS_DATABASE_VERSION{ 46 };
class VersionInfo class VersionInfo
{ {
public: public:
+26 -9
View File
@@ -256,7 +256,7 @@ namespace Database
return session.getDboSession().query<int>("SELECT COUNT(*) FROM release"); return session.getDboSession().query<int>("SELECT COUNT(*) FROM release");
} }
RangeResults<ReleaseId> Release::findIdsOrderedByArtist(Session& session, Range range) RangeResults<ReleaseId> Release::findIdsOrderedByArtist(Session& session, std::optional<Range> range)
{ {
session.checkSharedLocked(); session.checkSharedLocked();
@@ -268,15 +268,15 @@ namespace Database
" INNER JOIN artist a ON t_a_l.artist_id = a.id") " INNER JOIN artist a ON t_a_l.artist_id = a.id")
.orderBy("a.name COLLATE NOCASE, r.name COLLATE NOCASE") }; .orderBy("a.name COLLATE NOCASE, r.name COLLATE NOCASE") };
return Utils::execQuery(query, range); return Utils::execQuery<ReleaseId>(query, range);
} }
RangeResults<ReleaseId> Release::findOrphanIds(Session& session, Range range) RangeResults<ReleaseId> Release::findOrphanIds(Session& session, std::optional<Range> range)
{ {
session.checkSharedLocked(); session.checkSharedLocked();
auto query{ session.getDboSession().query<ReleaseId>("select r.id from release r LEFT OUTER JOIN Track t ON r.id = t.release_id WHERE t.id IS NULL") }; auto query{ session.getDboSession().query<ReleaseId>("select r.id from release r LEFT OUTER JOIN Track t ON r.id = t.release_id WHERE t.id IS NULL") };
return Utils::execQuery(query, range); return Utils::execQuery<ReleaseId>(query, range);
} }
RangeResults<Release::pointer> Release::find(Session& session, const FindParameters& params) RangeResults<Release::pointer> Release::find(Session& session, const FindParameters& params)
@@ -284,7 +284,15 @@ namespace Database
session.checkSharedLocked(); session.checkSharedLocked();
auto query{ createQuery<Wt::Dbo::ptr<Release>>(session, params) }; auto query{ createQuery<Wt::Dbo::ptr<Release>>(session, params) };
return Utils::execQuery(query, params.range); return Utils::execQuery<pointer>(query, params.range);
}
void Release::find(Session& session, const FindParameters& params, std::function<void(const pointer&)> func)
{
session.checkSharedLocked();
auto query{ createQuery<Wt::Dbo::ptr<Release>>(session, params) };
Utils::execQuery<pointer>(query, params.range, func);
} }
RangeResults<ReleaseId> Release::findIds(Session& session, const FindParameters& params) RangeResults<ReleaseId> Release::findIds(Session& session, const FindParameters& params)
@@ -292,7 +300,7 @@ namespace Database
session.checkSharedLocked(); session.checkSharedLocked();
auto query{ createQuery<ReleaseId>(session, params) }; auto query{ createQuery<ReleaseId>(session, params) };
return Utils::execQuery(query, params.range); return Utils::execQuery<ReleaseId>(query, params.range);
} }
std::size_t Release::getDiscCount() const std::size_t Release::getDiscCount() const
@@ -379,9 +387,8 @@ namespace Database
Wt::Dbo::collection<std::string> copyrights = session()->query<std::string> Wt::Dbo::collection<std::string> copyrights = session()->query<std::string>
("SELECT copyright_url FROM track t INNER JOIN release r ON r.id = t.release_id") ("SELECT copyright_url FROM track t INNER JOIN release r ON r.id = t.release_id")
.where("r.id = ?") .where("r.id = ?").bind(getId())
.groupBy("copyright_url") .groupBy("copyright_url");
.bind(getId());
std::vector<std::string> values(copyrights.begin(), copyrights.end()); std::vector<std::string> values(copyrights.begin(), copyrights.end());
@@ -392,6 +399,16 @@ namespace Database
return values.front(); return values.front();
} }
std::size_t Release::getMeanBitrate() const
{
assert(session());
return session()->query<int>("SELECT COALESCE(AVG(t.bitrate), 0) FROM track t")
.where("release_id = ?").bind(getId())
.where("bitrate > 0")
.resultValue();
}
std::vector<Artist::pointer> Release::getArtists(TrackArtistLinkType linkType) const std::vector<Artist::pointer> Release::getArtists(TrackArtistLinkType linkType) const
{ {
assert(session()); assert(session());
@@ -76,7 +76,7 @@ namespace Database
if (params.user.isValid()) if (params.user.isValid())
query.where("s_t.user_id = ?").bind(params.user); query.where("s_t.user_id = ?").bind(params.user);
return Utils::execQuery(query, params.range); return Utils::execQuery<StarredTrackId>(query, params.range);
} }
void StarredTrack::setDateTime(const Wt::WDateTime& dateTime) void StarredTrack::setDateTime(const Wt::WDateTime& dateTime)
+22 -13
View File
@@ -44,7 +44,8 @@ namespace Database
{ {
session.checkSharedLocked(); session.checkSharedLocked();
auto query{ session.getDboSession().query<ResultType>(params.distinct ? "SELECT DISTINCT " + std::string{ itemToSelect } + " FROM track t" : "SELECT t.id FROM track t") }; std::string selectStatement{ params.distinct ? "SELECT DISTINCT" : "SELECT" };
auto query{ session.getDboSession().query<ResultType>(selectStatement + " " + std::string{ itemToSelect } + " FROM track t") };
assert(params.keywords.empty() || params.name.empty()); assert(params.keywords.empty() || params.name.empty());
for (std::string_view keyword : params.keywords) for (std::string_view keyword : params.keywords)
@@ -129,7 +130,7 @@ namespace Database
query.where("r.name = ?").bind(params.releaseName); query.where("r.name = ?").bind(params.releaseName);
} }
if (params.trackList.isValid()) if (params.trackList.isValid() || params.sortMethod == TrackSortMethod::TrackList)
{ {
query.join("tracklist t_l ON t_l_e.tracklist_id = t_l.id"); query.join("tracklist t_l ON t_l_e.tracklist_id = t_l.id");
query.join("tracklist_entry t_l_e ON t.id = t_l_e.track_id"); query.join("tracklist_entry t_l_e ON t.id = t_l_e.track_id");
@@ -249,7 +250,7 @@ namespace Database
return std::vector<Track::pointer>(res.begin(), res.end()); return std::vector<Track::pointer>(res.begin(), res.end());
} }
RangeResults<Track::PathResult> Track::findPaths(Session& session, Range range) RangeResults<Track::PathResult> Track::findPaths(Session& session, std::optional<Range> range)
{ {
using QueryResultType = std::tuple<TrackId, std::string>; using QueryResultType = std::tuple<TrackId, std::string>;
session.checkSharedLocked(); session.checkSharedLocked();
@@ -257,7 +258,7 @@ namespace Database
// TODO Dbo traits on filesystem // TODO Dbo traits on filesystem
auto query{ session.getDboSession().query<QueryResultType>("SELECT id, file_path FROM track") }; auto query{ session.getDboSession().query<QueryResultType>("SELECT id, file_path FROM track") };
RangeResults<QueryResultType> queryResults{ Utils::execQuery(query, range) }; RangeResults<QueryResultType> queryResults{ Utils::execQuery<QueryResultType>(query, range) };
RangeResults<PathResult> res; RangeResults<PathResult> res;
res.range = queryResults.range; res.range = queryResults.range;
@@ -267,23 +268,23 @@ namespace Database
std::transform(std::cbegin(queryResults.results), std::cend(queryResults.results), std::back_inserter(res.results), std::transform(std::cbegin(queryResults.results), std::cend(queryResults.results), std::back_inserter(res.results),
[](const QueryResultType& queryResult) [](const QueryResultType& queryResult)
{ {
return PathResult{ std::get<0>(queryResult), std::get<1>(queryResult) }; return PathResult{ std::get<TrackId>(queryResult), std::move(std::get<std::string>(queryResult)) };
}); });
return res; return res;
} }
RangeResults<TrackId> Track::findIdsTrackMBIDDuplicates(Session& session, Range range) RangeResults<TrackId> Track::findIdsTrackMBIDDuplicates(Session& session, std::optional<Range> range)
{ {
session.checkSharedLocked(); session.checkSharedLocked();
auto query{ session.getDboSession().query<TrackId>("SELECT track.id FROM track WHERE mbid in (SELECT mbid FROM track WHERE mbid <> '' GROUP BY mbid HAVING COUNT (*) > 1)") auto query{ session.getDboSession().query<TrackId>("SELECT track.id FROM track WHERE mbid in (SELECT mbid FROM track WHERE mbid <> '' GROUP BY mbid HAVING COUNT (*) > 1)")
.orderBy("track.release_id,track.disc_number,track.track_number,track.mbid") }; .orderBy("track.release_id,track.disc_number,track.track_number,track.mbid") };
return Utils::execQuery(query, range); return Utils::execQuery<TrackId>(query, range);
} }
RangeResults<TrackId> Track::findIdsWithRecordingMBIDAndMissingFeatures(Session& session, Range range) RangeResults<TrackId> Track::findIdsWithRecordingMBIDAndMissingFeatures(Session& session, std::optional<Range> range)
{ {
session.checkSharedLocked(); session.checkSharedLocked();
@@ -291,7 +292,7 @@ namespace Database
.where("LENGTH(t.recording_mbid) > 0") .where("LENGTH(t.recording_mbid) > 0")
.where("NOT EXISTS (SELECT * FROM track_features t_f WHERE t_f.track_id = t.id)") }; .where("NOT EXISTS (SELECT * FROM track_features t_f WHERE t_f.track_id = t.id)") };
return Utils::execQuery(query, range); return Utils::execQuery<TrackId>(query, range);
} }
std::vector<Cluster::pointer> Track::getClusters() const std::vector<Cluster::pointer> Track::getClusters() const
@@ -316,7 +317,7 @@ namespace Database
session.checkSharedLocked(); session.checkSharedLocked();
auto query{ createQuery<TrackId>(session, parameters) }; auto query{ createQuery<TrackId>(session, parameters) };
return Utils::execQuery(query, parameters.range); return Utils::execQuery<TrackId>(query, parameters.range);
} }
RangeResults<Track::pointer> Track::find(Session& session, const FindParameters& parameters) RangeResults<Track::pointer> Track::find(Session& session, const FindParameters& parameters)
@@ -324,10 +325,18 @@ namespace Database
session.checkSharedLocked(); session.checkSharedLocked();
auto query{ createQuery<Wt::Dbo::ptr<Track>>(session, parameters) }; auto query{ createQuery<Wt::Dbo::ptr<Track>>(session, parameters) };
return Utils::execQuery(query, parameters.range); return Utils::execQuery<Track::pointer>(query, parameters.range);
} }
RangeResults<TrackId> Track::findSimilarTrackIds(Session& session, const std::vector<TrackId>& tracks, Range range) void Track::find(Session& session, const FindParameters& params, std::function<void(const Track::pointer&)> func)
{
session.checkSharedLocked();
auto query{ createQuery<Wt::Dbo::ptr<Track>>(session, params)};
Utils::execQuery(query, params.range, func);
}
RangeResults<TrackId> Track::findSimilarTrackIds(Session& session, const std::vector<TrackId>& tracks, std::optional<Range> range)
{ {
assert(!tracks.empty()); assert(!tracks.empty());
session.checkSharedLocked(); session.checkSharedLocked();
@@ -354,7 +363,7 @@ namespace Database
for (TrackId trackId : tracks) for (TrackId trackId : tracks)
query.bind(trackId); query.bind(trackId);
return Utils::execQuery(query, range); return Utils::execQuery<TrackId>(query, range);
} }
void Track::clearArtistLinks() void Track::clearArtistLinks()
@@ -84,7 +84,7 @@ namespace Database
session.checkSharedLocked(); session.checkSharedLocked();
auto query{ createQuery(session, params) }; auto query{ createQuery(session, params) };
return Utils::execQuery(query, params.range); return Utils::execQuery<TrackArtistLinkId>(query, params.range);
} }
EnumSet<TrackArtistLinkType> TrackArtistLink::findUsedTypes(Session& session) EnumSet<TrackArtistLinkType> TrackArtistLink::findUsedTypes(Session& session)
@@ -27,57 +27,52 @@
namespace Database { namespace Database {
TrackBookmark::TrackBookmark(ObjectPtr<User> user, ObjectPtr<Track> track) TrackBookmark::TrackBookmark(ObjectPtr<User> user, ObjectPtr<Track> track)
: _user {getDboPtr(user)}, : _user{ getDboPtr(user) },
_track {getDboPtr(track)} _track{ getDboPtr(track) }
{ {
} }
TrackBookmark::pointer TrackBookmark::pointer TrackBookmark::create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track)
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 std::size_t TrackBookmark::getCount(Session& session)
TrackBookmark::getCount(Session& session) {
{ session.checkSharedLocked();
session.checkSharedLocked();
return session.getDboSession().query<int>("SELECT COUNT(*) FROM track_bookmark"); return session.getDboSession().query<int>("SELECT COUNT(*) FROM track_bookmark");
} }
RangeResults<TrackBookmarkId> RangeResults<TrackBookmarkId> TrackBookmark::find(Session& session, UserId userId, std::optional<Range> range)
TrackBookmark::find(Session& session, UserId userId, Range range) {
{ session.checkSharedLocked();
session.checkSharedLocked();
auto query {session.getDboSession().query<TrackBookmarkId>("SELECT id from track_bookmark") auto query{ session.getDboSession().query<TrackBookmarkId>("SELECT id from track_bookmark")
.where("user_id = ?").bind(userId)}; .where("user_id = ?").bind(userId) };
return Utils::execQuery(query, range); return Utils::execQuery<TrackBookmarkId>(query, range);
} }
TrackBookmark::pointer TrackBookmark::pointer TrackBookmark::find(Session& session, UserId userId, TrackId trackId)
TrackBookmark::find(Session& session, UserId userId, TrackId trackId) {
{ session.checkSharedLocked();
session.checkSharedLocked();
return session.getDboSession().find<TrackBookmark>() return session.getDboSession().find<TrackBookmark>()
.where("user_id = ?").bind(userId) .where("user_id = ?").bind(userId)
.where("track_id = ?").bind(trackId) .where("track_id = ?").bind(trackId)
.resultValue(); .resultValue();
} }
TrackBookmark::pointer TrackBookmark::pointer TrackBookmark::find(Session& session, TrackBookmarkId id)
TrackBookmark::find(Session& session, TrackBookmarkId id) {
{ session.checkSharedLocked();
session.checkSharedLocked();
return session.getDboSession().find<TrackBookmark>() return session.getDboSession().find<TrackBookmark>()
.where("id = ?").bind(id) .where("id = ?").bind(id)
.resultValue(); .resultValue();
} }
} // namespace Database } // namespace Database
@@ -30,99 +30,92 @@
namespace Database { namespace Database {
TrackFeatures::TrackFeatures(ObjectPtr<Track> track, const std::string& jsonEncodedFeatures) TrackFeatures::TrackFeatures(ObjectPtr<Track> track, const std::string& jsonEncodedFeatures)
: _data {jsonEncodedFeatures}, : _data{ jsonEncodedFeatures },
_track {getDboPtr(track)} _track{ getDboPtr(track) }
{ {
} }
TrackFeatures::pointer TrackFeatures::pointer TrackFeatures::create(Session& session, ObjectPtr<Track> track, const std::string& jsonEncodedFeatures)
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 std::size_t TrackFeatures::getCount(Session& session)
TrackFeatures::getCount(Session& session) {
{ session.checkSharedLocked();
session.checkSharedLocked();
return session.getDboSession().query<int>("SELECT COUNT(*) FROM track_features"); return session.getDboSession().query<int>("SELECT COUNT(*) FROM track_features");
} }
TrackFeatures::pointer TrackFeatures::pointer TrackFeatures::find(Session& session, TrackFeaturesId id)
TrackFeatures::find(Session& session, TrackFeaturesId id) {
{ session.checkSharedLocked();
session.checkSharedLocked();
return session.getDboSession().find<TrackFeatures>() return session.getDboSession().find<TrackFeatures>()
.where("id = ?").bind(id) .where("id = ?").bind(id)
.resultValue(); .resultValue();
} }
TrackFeatures::pointer TrackFeatures::pointer TrackFeatures::find(Session& session, TrackId trackId)
TrackFeatures::find(Session& session, TrackId trackId) {
{ session.checkSharedLocked();
session.checkSharedLocked();
return session.getDboSession().find<TrackFeatures>() return session.getDboSession().find<TrackFeatures>()
.where("track_id = ?").bind(trackId) .where("track_id = ?").bind(trackId)
.resultValue(); .resultValue();
} }
RangeResults<TrackFeaturesId> RangeResults<TrackFeaturesId> TrackFeatures::find(Session& session, std::optional<Range> range)
TrackFeatures::find(Session& session, Range range) {
{ session.checkSharedLocked();
session.checkSharedLocked();
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(query, range); return Utils::execQuery<TrackFeaturesId>(query, range);
} }
FeatureValues FeatureValues TrackFeatures::getFeatureValues(const FeatureName& featureNode) const
TrackFeatures::getFeatureValues(const FeatureName& featureNode) const {
{ FeatureValuesMap featuresValuesMap{ getFeatureValuesMap({featureNode}) };
FeatureValuesMap featuresValuesMap {getFeatureValuesMap({featureNode})}; return std::move(featuresValuesMap[featureNode]);
return std::move(featuresValuesMap[featureNode]); }
}
FeatureValuesMap FeatureValuesMap TrackFeatures::getFeatureValuesMap(const std::unordered_set<FeatureName>& featureNames) const
TrackFeatures::getFeatureValuesMap(const std::unordered_set<FeatureName>& featureNames) const {
{ FeatureValuesMap res;
FeatureValuesMap res;
try try
{ {
std::istringstream iss {_data}; std::istringstream iss{ _data };
boost::property_tree::ptree root; boost::property_tree::ptree root;
boost::property_tree::read_json(iss, root); boost::property_tree::read_json(iss, root);
for (const FeatureName& featureName : featureNames) for (const FeatureName& featureName : featureNames)
{ {
FeatureValues& featureValues {res[featureName]}; FeatureValues& featureValues{ res[featureName] };
auto node {root.get_child(featureName)}; auto node{ root.get_child(featureName) };
bool hasChildren = false; bool hasChildren = false;
for (const auto& child : node.get_child("")) for (const auto& child : node.get_child(""))
{ {
hasChildren = true; hasChildren = true;
featureValues.push_back(child.second.get_value<double>()); featureValues.push_back(child.second.get_value<double>());
} }
if (!hasChildren) if (!hasChildren)
featureValues.push_back(node.get_value<double>()); featureValues.push_back(node.get_value<double>());
} }
} }
catch (boost::property_tree::ptree_error& error) catch (boost::property_tree::ptree_error& error)
{ {
LMS_LOG(DB, ERROR) << "Track " << _track.id() << ": ptree exception: " << error.what(); LMS_LOG(DB, ERROR) << "Track " << _track.id() << ": ptree exception: " << error.what();
res.clear(); res.clear();
} }
return res; return res;
} }
} // namespace Database } // namespace Database
+293 -659
View File
@@ -33,665 +33,299 @@
#include "IdTypeTraits.hpp" #include "IdTypeTraits.hpp"
#include "Utils.hpp" #include "Utils.hpp"
namespace Database { namespace Database
TrackList::TrackList(std::string_view name, TrackListType type, bool isPublic, ObjectPtr<User> user)
: _name {name}
, _type {type}
, _isPublic {isPublic}
, _creationDateTime {Utils::normalizeDateTime(Wt::WDateTime::currentDateTime())}
, _lastModifiedDateTime {Utils::normalizeDateTime(Wt::WDateTime::currentDateTime())}
, _user {getDboPtr(user)}
{ {
assert(user); TrackList::TrackList(std::string_view name, TrackListType type, bool isPublic, ObjectPtr<User> user)
} : _name{ name }
, _type{ type }
TrackList::pointer , _isPublic{ isPublic }
TrackList::create(Session& session, std::string_view name, TrackListType type, bool isPublic, ObjectPtr<User> user) , _creationDateTime{ Utils::normalizeDateTime(Wt::WDateTime::currentDateTime()) }
{ , _lastModifiedDateTime{ Utils::normalizeDateTime(Wt::WDateTime::currentDateTime()) }
return session.getDboSession().add(std::unique_ptr<TrackList> {new TrackList {name, type, isPublic, user}}); , _user{ getDboPtr(user) }
} {
assert(user);
std::size_t }
TrackList::getCount(Session& session)
{ TrackList::pointer TrackList::create(Session& session, std::string_view name, TrackListType type, bool isPublic, ObjectPtr<User> user)
session.checkSharedLocked(); {
return session.getDboSession().add(std::unique_ptr<TrackList> {new TrackList{ name, type, isPublic, user }});
return session.getDboSession().query<int>("SELECT COUNT(*) FROM tracklist"); }
}
std::size_t TrackList::getCount(Session& session)
{
TrackList::pointer session.checkSharedLocked();
TrackList::find(Session& session, std::string_view name, TrackListType type, UserId userId)
{ return session.getDboSession().query<int>("SELECT COUNT(*) FROM tracklist");
session.checkSharedLocked(); }
assert(userId.isValid());
return session.getDboSession().find<TrackList>() TrackList::pointer TrackList::find(Session& session, std::string_view name, TrackListType type, UserId userId)
.where("name = ?").bind(name) {
.where("type = ?").bind(type) session.checkSharedLocked();
.where("user_id = ?").bind(userId).resultValue(); assert(userId.isValid());
}
return session.getDboSession().find<TrackList>()
RangeResults<TrackListId> .where("name = ?").bind(name)
TrackList::find(Session& session, const FindParameters& params) .where("type = ?").bind(type)
{ .where("user_id = ?").bind(userId).resultValue();
session.checkSharedLocked(); }
auto query {session.getDboSession().query<TrackListId>("SELECT DISTINCT t_l.id FROM tracklist t_l")}; RangeResults<TrackListId> TrackList::find(Session& session, const FindParameters& params)
{
if (params.user.isValid()) session.checkSharedLocked();
query.where("t_l.user_id = ?").bind(params.user);
auto query{ session.getDboSession().query<TrackListId>("SELECT DISTINCT t_l.id FROM tracklist t_l") };
if (params.type)
query.where("t_l.type = ?").bind(*params.type); if (params.user.isValid())
query.where("t_l.user_id = ?").bind(params.user);
if (!params.clusters.empty())
{ if (params.type)
query.join("tracklist_entry t_l_e ON t_l_e.tracklist_id = t_l.id"); query.where("t_l.type = ?").bind(*params.type);
query.join("track t ON t.id = t_l_e.track_id");
if (!params.clusters.empty())
std::ostringstream oss; {
oss << "t.id IN (SELECT DISTINCT t.id FROM track t" query.join("tracklist_entry t_l_e ON t_l_e.tracklist_id = t_l.id");
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id" query.join("track t ON t.id = t_l_e.track_id");
" INNER JOIN cluster c ON c.id = t_c.cluster_id";
std::ostringstream oss;
WhereClause clusterClause; oss << "t.id IN (SELECT DISTINCT t.id FROM track t"
for (const ClusterId clusterId : params.clusters) " INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
{ " INNER JOIN cluster c ON c.id = t_c.cluster_id";
clusterClause.Or(WhereClause("c.id = ?"));
query.bind(clusterId); WhereClause clusterClause;
} for (const ClusterId clusterId : params.clusters)
{
oss << " " << clusterClause.get(); clusterClause.Or(WhereClause("c.id = ?"));
oss << " GROUP BY t.id HAVING COUNT(*) = " << params.clusters.size() << ")"; query.bind(clusterId);
}
query.where(oss.str());
} oss << " " << clusterClause.get();
oss << " GROUP BY t.id HAVING COUNT(*) = " << params.clusters.size() << ")";
switch (params.sortMethod)
{ query.where(oss.str());
case TrackListSortMethod::None: }
break;
case TrackListSortMethod::Name: switch (params.sortMethod)
query.orderBy("t_l.name COLLATE NOCASE"); {
break; case TrackListSortMethod::None:
case TrackListSortMethod::LastModifiedDesc: break;
query.orderBy("t_l.last_modified_date_time DESC"); case TrackListSortMethod::Name:
break; query.orderBy("t_l.name COLLATE NOCASE");
} break;
case TrackListSortMethod::LastModifiedDesc:
return Utils::execQuery(query, params.range); query.orderBy("t_l.last_modified_date_time DESC");
} break;
}
TrackList::pointer
TrackList::find(Session& session, TrackListId id) return Utils::execQuery<TrackListId>(query, params.range);
{ }
session.checkSharedLocked();
TrackList::pointer TrackList::find(Session& session, TrackListId id)
return session.getDboSession().find<TrackList>().where("id = ?").bind(id).resultValue(); {
} session.checkSharedLocked();
bool return session.getDboSession().find<TrackList>().where("id = ?").bind(id).resultValue();
TrackList::isEmpty() const }
{
return _entries.empty(); bool TrackList::isEmpty() const
} {
return _entries.empty();
std::size_t }
TrackList::getCount() const
{ std::size_t TrackList::getCount() const
return _entries.size(); {
} return _entries.size();
}
TrackListEntry::pointer
TrackList::getEntry(std::size_t pos) const TrackListEntry::pointer TrackList::getEntry(std::size_t pos) const
{ {
TrackListEntry::pointer res; TrackListEntry::pointer res;
auto entries = getEntries(Range {pos, 1}); auto entries = getEntries(Range{ pos, 1 });
if (!entries.empty()) if (!entries.empty())
res = entries.front(); res = entries.front();
return res; return res;
} }
std::vector<TrackListEntry::pointer> std::vector<TrackListEntry::pointer> TrackList::getEntries(std::optional<Range> range) const
TrackList::getEntries(std::optional<Range> range) const {
{ assert(session());
assert(session());
auto entries{
auto entries { session()->find<TrackListEntry>()
session()->find<TrackListEntry>() .where("tracklist_id = ?").bind(getId())
.where("tracklist_id = ?").bind(getId()) .orderBy("id")
.orderBy("id") .limit(range ? static_cast<int>(range->size) + 1 : -1)
.limit(range ? static_cast<int>(range->size) + 1 : -1) .offset(range ? static_cast<int>(range->offset) : -1)
.offset(range ? static_cast<int>(range->offset) : -1) .resultList() };
.resultList()};
return std::vector<TrackListEntry::pointer>(entries.begin(), entries.end());
return std::vector<TrackListEntry::pointer>(entries.begin(), entries.end()); }
}
TrackListEntry::pointer TrackList::getEntryByTrackAndDateTime(ObjectPtr<Track> track, const Wt::WDateTime& dateTime) const
TrackListEntry::pointer {
TrackList::getEntryByTrackAndDateTime(ObjectPtr<Track> track, const Wt::WDateTime& dateTime) const assert(session());
{
assert(session()); return session()->find<TrackListEntry>()
.where("tracklist_id = ?").bind(getId())
return session()->find<TrackListEntry>() .where("track_id = ?").bind(track->getId())
.where("tracklist_id = ?").bind(getId()) .where("date_time = ?").bind(Utils::normalizeDateTime(dateTime))
.where("track_id = ?").bind(track->getId()) .resultValue();
.where("date_time = ?").bind(Utils::normalizeDateTime(dateTime)) }
.resultValue();
} std::vector<Cluster::pointer> TrackList::getClusters() const
{
static assert(session());
Wt::Dbo::Query<Wt::Dbo::ptr<Artist>>
createArtistsQuery(Wt::Dbo::Session& session, const std::string& queryStr, TrackListId tracklistId, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType) 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")
{ .where("p.id = ?").bind(getId())
auto query {session.query<Wt::Dbo::ptr<Artist>>(queryStr)}; .groupBy("c.id")
query.join("track t ON t.id = t_a_l.track_id"); .orderBy("COUNT(c.id) DESC")
query.join("track_artist_link t_a_l ON t_a_l.artist_id = a.id"); .resultList() };
query.join("tracklist_entry p_e ON p_e.track_id = t.id");
query.join("tracklist p ON p.id = p_e.tracklist_id"); return std::vector<Cluster::pointer>(res.begin(), res.end());
}
query.where("p.id = ?").bind(tracklistId);
std::vector<std::vector<Cluster::pointer>> TrackList::getClusterGroups(const std::vector<ClusterType::pointer>& clusterTypes, std::size_t size) const
if (linkType) {
query.where("t_a_l.type = ?").bind(*linkType); assert(session());
std::vector<std::vector<Cluster::pointer>> res;
if (!clusterIds.empty())
{ if (clusterTypes.empty())
std::ostringstream oss; return res;
oss << "a.id IN (SELECT DISTINCT a.id FROM artist a"
" INNER JOIN track t ON t.id = t_a_l.track_id" auto query{ session()->query<Wt::Dbo::ptr<Cluster>>("SELECT c from cluster c") };
" 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" query.join("track t ON c.id = t_c.cluster_id")
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"; .join("track_cluster t_c ON t_c.track_id = t.id")
.join("cluster_type c_type ON c.cluster_type_id = c_type.id")
WhereClause clusterClause; .join("tracklist_entry t_l_e ON t_l_e.track_id = t.id")
for (auto id : clusterIds) .join("tracklist t_l ON t_l.id = t_l_e.tracklist_id")
{ .where("t_l.id = ?").bind(getId());
clusterClause.Or(WhereClause("c.id = ?"));
query.bind(id); {
} std::ostringstream oss;
oss << "c_type.id IN (";
oss << " " << clusterClause.get(); bool first{ true };
oss << " GROUP BY t.id,a.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size() << ")"; for (auto clusterType : clusterTypes)
{
query.where(oss.str()); if (!first)
} oss << ", ";
oss << "?";
return query; query.bind(clusterType->getId());
} first = false;
}
static oss << ")";
Wt::Dbo::Query<Wt::Dbo::ptr<Release>> query.where(oss.str());
createReleasesQuery(Wt::Dbo::Session& session, const std::string& queryStr, TrackListId tracklistId, const std::vector<ClusterId>& clusterIds) }
{ query.groupBy("c.id");
auto query {session.query<Wt::Dbo::ptr<Release>>(queryStr)}; query.orderBy("COUNT(c.id) DESC");
query.join("track t ON t.release_id = r.id");
query.join("tracklist_entry p_e ON p_e.track_id = t.id"); auto queryRes{ query.resultList() };
query.join("tracklist p ON p.id = p_e.tracklist_id");
std::map<ClusterTypeId, std::vector<Cluster::pointer>> clustersByType;
query.where("p.id = ?").bind(tracklistId); for (const Wt::Dbo::ptr<Cluster>& cluster : queryRes)
{
if (!clusterIds.empty()) if (clustersByType[cluster->getType()->getId()].size() < size)
{ clustersByType[cluster->getType()->getId()].push_back(cluster);
std::ostringstream oss; }
oss << "r.id IN (SELECT DISTINCT r.id FROM release r"
" INNER JOIN track t ON t.release_id = r.id" for (const auto& [clusterTypeId, clusters] : clustersByType)
" INNER JOIN cluster c ON c.id = t_c.cluster_id" res.push_back(clusters);
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
return res;
WhereClause clusterClause; }
for (ClusterId id : clusterIds)
{ bool TrackList::hasTrack(TrackId trackId) const
clusterClause.Or(WhereClause("c.id = ?")); {
query.bind(id); assert(session());
}
Wt::Dbo::collection<TrackListEntry::pointer> res = session()->query<TrackListEntry::pointer>("SELECT p_e from tracklist_entry p_e INNER JOIN tracklist p ON p_e.tracklist_id = p.id")
oss << " " << clusterClause.get(); .where("p_e.track_id = ?").bind(trackId)
oss << " GROUP BY t.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size() << ")"; .where("p.id = ?").bind(getId());
query.where(oss.str()); return res.size() > 0;
} }
return query; std::vector<Track::pointer> TrackList::getSimilarTracks(std::optional<std::size_t> offset, std::optional<std::size_t> size) const
} {
assert(session());
static
Wt::Dbo::Query<Wt::Dbo::ptr<Track>> auto res{ session()->query<Wt::Dbo::ptr<Track>>(
createTracksQuery(Wt::Dbo::Session& session, TrackListId tracklistId, const std::vector<ClusterId>& clusterIds) "SELECT t FROM track t"
{ " INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
auto query {session.query<Wt::Dbo::ptr<Track>>("SELECT t from track t 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 "
" (t_c.cluster_id IN (SELECT DISTINCT c.id 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 = ?)"
query.where("p.id = ?").bind(tracklistId); " AND t.id NOT IN (SELECT tracklist_t.id FROM track tracklist_t INNER JOIN tracklist_entry t_e ON t_e.track_id = tracklist_t.id WHERE t_e.tracklist_id = ?))"
)
if (!clusterIds.empty()) .bind(getId())
{ .bind(getId())
std::ostringstream oss; .groupBy("t.id")
oss << "t.id IN (SELECT DISTINCT t.id FROM track t" .orderBy("COUNT(*) DESC, RANDOM()")
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id" .limit(size ? static_cast<int>(*size) : -1)
" INNER JOIN cluster c ON c.id = t_c.cluster_id"; .offset(offset ? static_cast<int>(*offset) : -1)
.resultList() };
WhereClause clusterClause;
for (auto id : clusterIds) return std::vector<Track::pointer>(res.begin(), res.end());
{ }
clusterClause.Or(WhereClause("c.id = ?")).bind(id.toString());
query.bind(id); std::vector<TrackId> TrackList::getTrackIds() const
} {
assert(session());
oss << " " << clusterClause.get();
oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size() << ")"; 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());
query.where(oss.str());
} return std::vector<TrackId>(res.begin(), res.end());
}
return query;
} std::chrono::milliseconds TrackList::getDuration() const
{
std::vector<Artist::pointer> assert(session());
TrackList::getArtists(const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, ArtistSortMethod sortMethod, std::optional<Range> range, bool& moreResults) const
{ using milli = std::chrono::duration<int, std::milli>;
assert(session());
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")
auto query {createArtistsQuery(*session(), "SELECT a from artist a", getId(), clusterIds, linkType) .where("p_e.tracklist_id = ?").bind(getId()) };
.groupBy("a.id").having("p_e.date_time = MAX(p_e.date_time)")
.limit(range ? static_cast<int>(range->size) + 1 : -1) return query.resultValue();
.offset(range ? static_cast<int>(range->offset) : -1)}; }
switch (sortMethod) void TrackList::setLastModifiedDateTime(const Wt::WDateTime& dateTime)
{ {
case ArtistSortMethod::None: _lastModifiedDateTime = Utils::normalizeDateTime(dateTime);
break; }
case ArtistSortMethod::ByName:
query.orderBy("a.name COLLATE NOCASE"); TrackListEntry::TrackListEntry(ObjectPtr<Track> track, ObjectPtr<TrackList> tracklist, const Wt::WDateTime& dateTime)
break; : _dateTime{ Utils::normalizeDateTime(dateTime) }
case ArtistSortMethod::BySortName: , _track{ getDboPtr(track) }
query.orderBy("a.sort_name COLLATE NOCASE"); , _tracklist{ getDboPtr(tracklist) }
break; {
case ArtistSortMethod::Random: assert(track);
query.orderBy("RANDOM()"); assert(tracklist);
break; }
case ArtistSortMethod::LastWritten:
case ArtistSortMethod::StarredDateDesc: TrackListEntry::pointer TrackListEntry::create(Session& session, ObjectPtr<Track> track, ObjectPtr<TrackList> tracklist, const Wt::WDateTime& dateTime)
assert(false); // Not implemented! {
break; return session.getDboSession().add(std::unique_ptr<TrackListEntry> {new TrackListEntry{ track, tracklist, dateTime }});
} }
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> collection {query.resultList()}; void TrackListEntry::onPostCreated()
{
auto res {std::vector<Artist::pointer>(collection.begin(), collection.end())}; _tracklist.modify()->setLastModifiedDateTime(Utils::normalizeDateTime(Wt::WDateTime::currentDateTime()));
if (range && res.size() == static_cast<std::size_t>(range->size) + 1) }
{
moreResults = true; void TrackListEntry::onPreRemove()
res.pop_back(); {
} _tracklist.modify()->setLastModifiedDateTime(Utils::normalizeDateTime(Wt::WDateTime::currentDateTime()));
else }
moreResults = false;
TrackListEntry::pointer TrackListEntry::getById(Session& session, TrackListEntryId id)
return res; {
} session.checkSharedLocked();
return session.getDboSession().find<TrackListEntry>().where("id = ?").bind(id).resultValue();
std::vector<ObjectPtr<Release>> }
TrackList::getReleases(const std::vector<ClusterId>& clusterIds, std::optional<Range> range, bool& moreResults) const
{
assert(session());
auto collection {createReleasesQuery(*session(), "SELECT r from release r", getId(), clusterIds)
.groupBy("r.id").having("p_e.date_time = MAX(p_e.date_time)")
.limit(range ? static_cast<int>(range->size) + 1 : -1)
.offset(range ? static_cast<int>(range->offset) : -1)
.resultList()};
std::vector<Release::pointer> res(collection.begin(), collection.end());
if (range && res.size() == static_cast<std::size_t>(range->size) + 1)
{
moreResults = true;
res.pop_back();
}
else
moreResults = false;
return res;
}
std::vector<ObjectPtr<Track>>
TrackList::getTracks(const std::vector<ClusterId>& clusterIds, std::optional<Range> range, bool& moreResults) const
{
assert(session());
auto collection {createTracksQuery(*session(), getId(), clusterIds)
.groupBy("t.id").having("p_e.date_time = MAX(p_e.date_time)")
.limit(range ? static_cast<int>(range->size) + 1 : -1)
.offset(range ? static_cast<int>(range->offset) : -1)
.resultList()};
std::vector<Track::pointer> res(collection.begin(), collection.end());
if (range && res.size() == static_cast<std::size_t>(range->size) + 1)
{
moreResults = true;
res.pop_back();
}
else
moreResults = false;
return res;
}
std::vector<Artist::pointer>
TrackList::getArtistsOrderedByRecentFirst(const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range, bool& moreResults) const
{
assert(session());
auto collection {createArtistsQuery(*session(), "SELECT a from artist a", getId(), clusterIds, linkType)
.groupBy("a.id").having("p_e.date_time = MAX(p_e.date_time)")
.orderBy("p_e.date_time DESC, p_e.id DESC")
.limit(range ? static_cast<int>(range->size) + 1 : -1)
.offset(range ? static_cast<int>(range->offset) : -1)
.resultList()};
auto res {std::vector<Artist::pointer>(collection.begin(), collection.end())};
if (range && res.size() == static_cast<std::size_t>(range->size) + 1)
{
moreResults = true;
res.pop_back();
}
else
moreResults = false;
return res;
}
std::vector<Release::pointer>
TrackList::getReleasesOrderedByRecentFirst(const std::vector<ClusterId>& clusterIds, std::optional<Range> range, bool& moreResults) const
{
assert(session());
auto collection {createReleasesQuery(*session(), "SELECT r from release r", getId(), clusterIds)
.groupBy("r.id").having("p_e.date_time = MAX(p_e.date_time)")
.orderBy("p_e.date_time DESC, p_e.id DESC")
.limit(range ? static_cast<int>(range->size) + 1 : -1)
.offset(range ? static_cast<int>(range->offset) : -1)
.resultList()};
std::vector<Release::pointer> res(collection.begin(), collection.end());
if (range && res.size() == static_cast<std::size_t>(range->size) + 1)
{
moreResults = true;
res.pop_back();
}
else
moreResults = false;
return res;
}
std::vector<Track::pointer>
TrackList::getTracksOrderedByRecentFirst(const std::vector<ClusterId>& clusterIds, std::optional<Range> range, bool& moreResults) const
{
assert(session());
auto collection {createTracksQuery(*session(), getId(), clusterIds)
.groupBy("t.id").having("p_e.date_time = MAX(p_e.date_time)")
.orderBy("p_e.date_time DESC, p_e.id DESC")
.limit(range ? static_cast<int>(range->size) + 1 : -1)
.offset(range ? static_cast<int>(range->offset) : -1)
.resultList()};
std::vector<Track::pointer> res(collection.begin(), collection.end());
if (range && res.size() == static_cast<std::size_t>(range->size) + 1)
{
moreResults = true;
res.pop_back();
}
else
moreResults = false;
return res;
}
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")
.where("p.id = ?").bind(getId())
.groupBy("c.id")
.orderBy("COUNT(c.id) DESC")
.resultList()};
return std::vector<Cluster::pointer>(res.begin(), res.end());
}
std::vector<std::vector<Cluster::pointer>>
TrackList::getClusterGroups(const std::vector<ClusterType::pointer>& clusterTypes, std::size_t size) const
{
assert(session());
std::vector<std::vector<Cluster::pointer>> res;
if (clusterTypes.empty())
return res;
auto query {session()->query<Wt::Dbo::ptr<Cluster>>("SELECT c from cluster c")};
query.join("track t ON c.id = t_c.cluster_id")
.join("track_cluster t_c ON t_c.track_id = t.id")
.join("cluster_type c_type ON c.cluster_type_id = c_type.id")
.join("tracklist_entry t_l_e ON t_l_e.track_id = t.id")
.join("tracklist t_l ON t_l.id = t_l_e.tracklist_id")
.where("t_l.id = ?").bind(getId());
{
std::ostringstream oss;
oss << "c_type.id IN (";
bool first {true};
for (auto clusterType : clusterTypes)
{
if (!first)
oss << ", ";
oss << "?";
query.bind(clusterType ->getId());
first = false;
}
oss << ")";
query.where(oss.str());
}
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)
{
if (clustersByType[cluster->getType()->getId()].size() < size)
clustersByType[cluster->getType()->getId()].push_back(cluster);
}
for (const auto& [clusterTypeId, clusters] : clustersByType)
res.push_back(clusters);
return res;
}
bool
TrackList::hasTrack(TrackId trackId) const
{
assert(session());
Wt::Dbo::collection<TrackListEntry::pointer> res = session()->query<TrackListEntry::pointer>("SELECT p_e from tracklist_entry p_e INNER JOIN tracklist p ON p_e.tracklist_id = p.id")
.where("p_e.track_id = ?").bind(trackId)
.where("p.id = ?").bind(getId());
return res.size() > 0;
}
std::vector<Track::pointer>
TrackList::getSimilarTracks(std::optional<std::size_t> offset, std::optional<std::size_t> size) const
{
assert(session());
auto res {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 "
" (t_c.cluster_id IN (SELECT DISTINCT c.id 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 = ?)"
" AND t.id NOT IN (SELECT tracklist_t.id FROM track tracklist_t INNER JOIN tracklist_entry t_e ON t_e.track_id = tracklist_t.id WHERE t_e.tracklist_id = ?))"
)
.bind(getId())
.bind(getId())
.groupBy("t.id")
.orderBy("COUNT(*) DESC, RANDOM()")
.limit(size ? static_cast<int>(*size) : -1)
.offset(offset ? static_cast<int>(*offset) : -1)
.resultList()};
return std::vector<Track::pointer>(res.begin(), res.end());
}
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());
return std::vector<TrackId>(res.begin(), res.end());
}
std::chrono::milliseconds
TrackList::getDuration() const
{
assert(session());
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();
}
void
TrackList::setLastModifiedDateTime(const Wt::WDateTime& dateTime)
{
_lastModifiedDateTime = Utils::normalizeDateTime(dateTime);
}
std::vector<Artist::pointer>
TrackList::getTopArtists(const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range, bool& moreResults) const
{
assert(session());
auto query {createArtistsQuery(*session(), "SELECT a from artist a", getId(), clusterIds, linkType)};
auto collection {query
.orderBy("COUNT(a.id) DESC")
.groupBy("a.id")
.limit(range ? static_cast<int>(range->size) + 1 : -1)
.offset(range ? static_cast<int>(range->offset) : -1)
.resultList()};
std::vector<Artist::pointer> res(collection.begin(), collection.end());
if (range && res.size() == static_cast<std::size_t>(range->size) + 1)
{
moreResults = true;
res.pop_back();
}
else
moreResults = false;
return res;
}
std::vector<Release::pointer>
TrackList::getTopReleases(const std::vector<ClusterId>& clusterIds, std::optional<Range> range, bool& moreResults) const
{
assert(session());
auto query {createReleasesQuery(*session(), "SELECT r from release r", getId(), clusterIds)};
auto collection {query
.orderBy("COUNT(r.id) DESC")
.groupBy("r.id")
.limit(range ? static_cast<int>(range->size) + 1 : -1)
.offset(range ? static_cast<int>(range->offset) : -1)
.resultList()};
std::vector<Release::pointer> res(collection.begin(), collection.end());
if (range && res.size() == static_cast<std::size_t>(range->size) + 1)
{
moreResults = true;
res.pop_back();
}
else
moreResults = false;
return res;
}
std::vector<Track::pointer>
TrackList::getTopTracks(const std::vector<ClusterId>& clusterIds, std::optional<Range> range, bool& moreResults) const
{
assert(session());
auto query {createTracksQuery(*session(), getId(), clusterIds)};
auto collection {query
.orderBy("COUNT(t.id) DESC")
.groupBy("t.id")
.limit(range ? static_cast<int>(range->size) + 1 : -1)
.offset(range ? static_cast<int>(range->offset) : -1)
.resultList()};
std::vector<Track::pointer> res(collection.begin(), collection.end());
if (range && res.size() == static_cast<std::size_t>(range->size) + 1)
{
moreResults = true;
res.pop_back();
}
else
moreResults = false;
return res;
}
TrackListEntry::TrackListEntry(ObjectPtr<Track> track, ObjectPtr<TrackList> tracklist, const Wt::WDateTime& dateTime)
: _dateTime {Utils::normalizeDateTime(dateTime)}
, _track {getDboPtr(track)}
, _tracklist {getDboPtr(tracklist)}
{
assert(track);
assert(tracklist);
}
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}});
}
void
TrackListEntry::onPostCreated()
{
_tracklist.modify()->setLastModifiedDateTime(Utils::normalizeDateTime(Wt::WDateTime::currentDateTime()));
}
void
TrackListEntry::onPreRemove()
{
_tracklist.modify()->setLastModifiedDateTime(Utils::normalizeDateTime(Wt::WDateTime::currentDateTime()));
}
TrackListEntry::pointer
TrackListEntry::getById(Session& session, TrackListEntryId id)
{
session.checkSharedLocked();
return session.getDboSession().find<TrackListEntry>().where("id = ?").bind(id).resultValue();
}
} // namespace Database } // namespace Database
+3 -3
View File
@@ -58,7 +58,7 @@ namespace Database {
if (params.feedbackBackend) if (params.feedbackBackend)
query.where("feedback_backend = ?").bind(*params.feedbackBackend); query.where("feedback_backend = ?").bind(*params.feedbackBackend);
return Utils::execQuery(query, params.range); return Utils::execQuery<UserId>(query, params.range);
} }
User::pointer User::findDemoUser(Session& session) User::pointer User::findDemoUser(Session& session)
@@ -80,10 +80,10 @@ namespace Database {
.resultValue(); .resultValue();
} }
void User::setSubsonicDefaultTranscodeBitrate(Bitrate bitrate) void User::setSubsonicDefaultTranscodingOutputBitrate(Bitrate bitrate)
{ {
assert(isAudioBitrateAllowed(bitrate)); assert(isAudioBitrateAllowed(bitrate));
_subsonicDefaultTranscodeBitrate = bitrate; _subsonicDefaultTranscodingOutputBitrate = bitrate;
} }
void User::clearAuthTokens() void User::clearAuthTokens()
+41 -43
View File
@@ -19,6 +19,7 @@
#pragma once #pragma once
#include <functional>
#include <string> #include <string>
#include <string_view> #include <string_view>
@@ -30,57 +31,54 @@
namespace Database::Utils namespace Database::Utils
{ {
#define ESCAPE_CHAR_STR "\\" #define ESCAPE_CHAR_STR "\\"
static inline constexpr char escapeChar {'\\'}; static inline constexpr char escapeChar{ '\\' };
std::string escapeLikeKeyword(std::string_view keywords); std::string escapeLikeKeyword(std::string_view keywords);
template <typename T> template <typename Query>
RangeResults<T> void applyRange(Query& query, std::optional<Range> range)
execQuery(Wt::Dbo::Query<T>& query, Range range) {
{ if (range)
RangeResults<T> res; {
query.limit(static_cast<int>(range->size));
query.offset(static_cast<int>(range->offset));
}
}
auto collection {query.limit(range.size ? static_cast<int>(range.size) + 1 : -1) template <typename ResultType, typename Query>
.offset(range.offset ? static_cast<int>(range.offset) : -1) RangeResults<ResultType> execQuery(Query& query, std::optional<Range> range)
.resultList()}; {
RangeResults<ResultType> res;
res.results.assign(collection.begin(), collection.end()); if (range)
if (range.size && res.results.size() == static_cast<std::size_t>(range.size) + 1) applyRange(query, Range{ range->offset, range->size + 1 });
{
res.moreResults = true;
res.results.pop_back();
}
else
res.moreResults = false;
res.range.offset = range.offset; auto collection{ query.resultList() };
res.range.size = res.results.size(); res.results.assign(collection.begin(), collection.end());
return res; if (range && res.results.size() == static_cast<std::size_t>(range->size) + 1)
} {
// TODO may optim by not actually requesting the last one
res.moreResults = true;
res.results.pop_back();
}
else
res.moreResults = false;
template <typename T> res.range.offset = range->offset;
RangeResults<typename T::pointer> res.range.size = res.results.size();
execQuery(Wt::Dbo::Query<Wt::Dbo::ptr<T>>& query, Range range)
{
RangeResults<typename T::pointer> res;
auto collection {query.limit(range.size ? static_cast<int>(range.size) + 1 : -1) return res;
.offset(range.offset ? static_cast<int>(range.offset) : -1) }
.resultList()};
res.results.assign(collection.begin(), collection.end()); template <typename ResultType, typename Query>
if (range.size && res.results.size() == static_cast<std::size_t>(range.size) + 1) void execQuery(Query& query, std::optional<Range> range, std::function<void(const ResultType&)> func)
{ {
res.moreResults = true; if (range)
res.results.pop_back(); applyRange(query, range);
}
else
res.moreResults = false;
res.range.offset = range.offset; for (const auto& res : query.resultList())
res.range.size = res.results.size(); func(res);
return res; }
}
Wt::WDateTime normalizeDateTime(const Wt::WDateTime& dateTime); Wt::WDateTime normalizeDateTime(const Wt::WDateTime& dateTime);
} // namespace Database::Utils } // namespace Database::Utils
@@ -58,7 +58,7 @@ namespace Database
std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords (on either name field OR sort name field) std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords (on either name field OR sort name field)
std::optional<TrackArtistLinkType> linkType; // if set, only artists that have produced at least one track with this link type std::optional<TrackArtistLinkType> linkType; // if set, only artists that have produced at least one track with this link type
ArtistSortMethod sortMethod{ ArtistSortMethod::None }; ArtistSortMethod sortMethod{ ArtistSortMethod::None };
Range range; std::optional<Range> range;
Wt::WDateTime writtenAfter; Wt::WDateTime writtenAfter;
UserId starringUser; // only artists starred by this user UserId starringUser; // only artists starred by this user
std::optional<FeedbackBackend> feedbackBackend; // and for this feedback backend std::optional<FeedbackBackend> feedbackBackend; // and for this feedback backend
@@ -69,7 +69,7 @@ namespace Database
FindParameters& setKeywords(const std::vector<std::string_view>& _keywords) { keywords = _keywords; return *this; } FindParameters& setKeywords(const std::vector<std::string_view>& _keywords) { keywords = _keywords; return *this; }
FindParameters& setLinkType(std::optional<TrackArtistLinkType> _linkType) { linkType = _linkType; return *this; } FindParameters& setLinkType(std::optional<TrackArtistLinkType> _linkType) { linkType = _linkType; return *this; }
FindParameters& setSortMethod(ArtistSortMethod _sortMethod) { sortMethod = _sortMethod; return *this; } FindParameters& setSortMethod(ArtistSortMethod _sortMethod) { sortMethod = _sortMethod; return *this; }
FindParameters& setRange(Range _range) { range = _range; return *this; } FindParameters& setRange(std::optional<Range> _range) { range = _range; return *this; }
FindParameters& setWrittenAfter(const Wt::WDateTime& _after) { writtenAfter = _after; return *this; } FindParameters& setWrittenAfter(const Wt::WDateTime& _after) { writtenAfter = _after; return *this; }
FindParameters& setStarringUser(UserId _user, FeedbackBackend _feedbackBackend) { starringUser = _user; feedbackBackend = _feedbackBackend; return *this; } FindParameters& setStarringUser(UserId _user, FeedbackBackend _feedbackBackend) { starringUser = _user; feedbackBackend = _feedbackBackend; return *this; }
FindParameters& setTrack(TrackId _track) { track = _track; return *this; } FindParameters& setTrack(TrackId _track) { track = _track; return *this; }
@@ -84,8 +84,9 @@ namespace Database
static pointer find(Session& session, ArtistId id); static pointer find(Session& session, ArtistId id);
static std::vector<pointer> find(Session& session, const std::string& name); // exact match on name field static std::vector<pointer> find(Session& session, const std::string& name); // exact match on name field
static RangeResults<pointer> find(Session& session, const FindParameters& parameters); static RangeResults<pointer> find(Session& session, const FindParameters& parameters);
static void find(Session& session, const FindParameters& parameters, std::function<void(const pointer&)> func);
static RangeResults<ArtistId> findIds(Session& session, const FindParameters& parameters); static RangeResults<ArtistId> findIds(Session& session, const FindParameters& parameters);
static RangeResults<ArtistId> findOrphanIds(Session& session, Range range); // No track related static RangeResults<ArtistId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt); // No track related
static bool exists(Session& session, ArtistId id); static bool exists(Session& session, ArtistId id);
// Accessors // Accessors
@@ -94,7 +95,7 @@ namespace Database
std::optional<UUID> getMBID() const { return UUID::fromString(_MBID); } std::optional<UUID> getMBID() const { return UUID::fromString(_MBID); }
// No artistLinkTypes means get them all // No artistLinkTypes means get them all
RangeResults<ArtistId> findSimilarArtistIds(EnumSet<TrackArtistLinkType> artistLinkTypes = {}, Range range = {}) const; RangeResults<ArtistId> findSimilarArtistIds(EnumSet<TrackArtistLinkType> artistLinkTypes = {}, std::optional<Range> range = std::nullopt) const;
// Get the cluster of the tracks made by this artist // Get the cluster of the tracks made by this artist
// Each clusters are grouped by cluster type, sorted by the number of occurence // Each clusters are grouped by cluster type, sorted by the number of occurence
@@ -19,6 +19,7 @@
#pragma once #pragma once
#include <optional>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <tuple> #include <tuple>
@@ -45,12 +46,12 @@ namespace Database {
public: public:
struct FindParameters struct FindParameters
{ {
Range range; std::optional<Range> range;
ClusterTypeId clusterType; // if non empty, clusters that belong to this cluster type ClusterTypeId clusterType; // if non empty, clusters that belong to this cluster type
TrackId track; // if set, clusters involved in this track TrackId track; // if set, clusters involved in this track
ReleaseId release; // if set, clusters involved in this release ReleaseId release; // if set, clusters involved in this release
FindParameters& setRange(Range _range) { range = _range; return *this; } FindParameters& setRange(std::optional<Range> _range) { range = _range; return *this; }
FindParameters& setClusterType(ClusterTypeId _clusterType) { clusterType = _clusterType; return *this; } FindParameters& setClusterType(ClusterTypeId _clusterType) { clusterType = _clusterType; return *this; }
FindParameters& setTrack(TrackId _track) { track = _track; return *this; } FindParameters& setTrack(TrackId _track) { track = _track; return *this; }
FindParameters& setRelease(ReleaseId _release) { release = _release; return *this; } FindParameters& setRelease(ReleaseId _release) { release = _release; return *this; }
@@ -59,26 +60,35 @@ namespace Database {
Cluster() = default; Cluster() = default;
// Find utility // Find utility
// As clusters only have a name, this is an optim to directly get the cluster names
using ClusterFindResult = std::tuple<ClusterId, std::string>;
static std::size_t getCount(Session& session); static std::size_t getCount(Session& session);
static RangeResults<ClusterFindResult> find(Session& session, const FindParameters& range); static RangeResults<ClusterId> findIds(Session& session, const FindParameters& params);
static pointer find(Session& session, ClusterId id); static RangeResults<pointer> find(Session& session, const FindParameters& params);
static RangeResults<ClusterId> findOrphans(Session& session, Range range); static void find(Session& session, const FindParameters& params, std::function<void(const pointer& cluster)> _func);
static pointer find(Session& session, ClusterId id);
static RangeResults<ClusterId> findOrphans(Session& session, std::optional<Range> range = std::nullopt);
// May be very slow
static std::size_t computeTrackCount(Session& session, ClusterId id);
static std::size_t computeReleaseCount(Session& session, ClusterId id);
// Accessors // Accessors
const std::string& getName() const { return _name; } std::string_view getName() const { return _name; }
ObjectPtr<ClusterType> getType() const { return _clusterType; } ObjectPtr<ClusterType> getType() const { return _clusterType; }
std::size_t getTracksCount() const { return _tracks.size(); } std::size_t getTracksCount() const { return _trackCount; }
RangeResults<TrackId> getTracks(Range range) const; RangeResults<TrackId> getTracks(std::optional<Range> range = std::nullopt) const;
std::size_t getReleasesCount() const; std::size_t getReleasesCount() const { return _releaseCount; };
void setReleaseCount(std::size_t releaseCount) { _releaseCount = releaseCount; }
void setTrackCount(std::size_t trackCount) { _trackCount = trackCount; }
void addTrack(ObjectPtr<Track> track); void addTrack(ObjectPtr<Track> track);
template<class Action> template<class Action>
void persist(Action& a) void persist(Action& a)
{ {
Wt::Dbo::field(a, _name, "name"); Wt::Dbo::field(a, _name, "name");
// cached field since queries are too long
Wt::Dbo::field(a, _trackCount, "track_count");
Wt::Dbo::field(a, _releaseCount, "release_count");
Wt::Dbo::belongsTo(a, _clusterType, "cluster_type", Wt::Dbo::OnDeleteCascade); Wt::Dbo::belongsTo(a, _clusterType, "cluster_type", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade); Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade);
@@ -92,6 +102,8 @@ namespace Database {
static const std::size_t _maxNameLength = 128; static const std::size_t _maxNameLength = 128;
std::string _name; std::string _name;
int _trackCount{};
int _releaseCount{};
Wt::Dbo::ptr<ClusterType> _clusterType; Wt::Dbo::ptr<ClusterType> _clusterType;
Wt::Dbo::collection< Wt::Dbo::ptr<Track> > _tracks; Wt::Dbo::collection< Wt::Dbo::ptr<Track> > _tracks;
@@ -105,11 +117,11 @@ namespace Database {
// Getters // Getters
static std::size_t getCount(Session& session); static std::size_t getCount(Session& session);
static RangeResults<ClusterTypeId> find(Session& session, Range range); static RangeResults<ClusterTypeId> find(Session& session, std::optional<Range> range = std::nullopt);
static pointer find(Session& session, std::string_view name); static pointer find(Session& session, std::string_view name);
static pointer find(Session& session, ClusterTypeId id); static pointer find(Session& session, ClusterTypeId id);
static RangeResults<ClusterTypeId> findOrphans(Session& session, Range range); static RangeResults<ClusterTypeId> findOrphans(Session& session, std::optional<Range> range = std::nullopt);
static RangeResults<ClusterTypeId> findUsed(Session& session, Range range); static RangeResults<ClusterTypeId> findUsed(Session& session, std::optional<Range> range = std::nullopt);
static void remove(Session& session, const std::string& name); static void remove(Session& session, const std::string& name);
@@ -19,6 +19,8 @@
#pragma once #pragma once
#include <optional>
#include <Wt/Dbo/Dbo.h> #include <Wt/Dbo/Dbo.h>
#include <Wt/WDateTime.h> #include <Wt/WDateTime.h>
@@ -45,10 +47,10 @@ namespace Database
struct FindParameters struct FindParameters
{ {
UserId user; UserId user;
std::optional<ScrobblingBackend> backend; std::optional<ScrobblingBackend> backend;
std::optional<SyncState> syncState; std::optional<SyncState> syncState;
Range range; std::optional<Range> range;
FindParameters& setUser(UserId _user) { user = _user; return *this; } FindParameters& setUser(UserId _user) { user = _user; return *this; }
FindParameters& setScrobblingBackend(ScrobblingBackend _backend) { backend = _backend; return *this; } FindParameters& setScrobblingBackend(ScrobblingBackend _backend) { backend = _backend; return *this; }
@@ -63,13 +65,13 @@ namespace Database
static RangeResults<ListenId> find(Session& session, const FindParameters& parameters); static RangeResults<ListenId> find(Session& session, const FindParameters& parameters);
// Stats // Stats
static RangeResults<ArtistId> getTopArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range = {}); static RangeResults<ArtistId> getTopArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range = std::nullopt);
static RangeResults<ReleaseId> getTopReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, Range range = {}); static RangeResults<ReleaseId> getTopReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<Range> range = std::nullopt);
static RangeResults<TrackId> getTopTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, Range range = {}); static RangeResults<TrackId> getTopTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<Range> range = std::nullopt);
static RangeResults<ArtistId> getRecentArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range = {}); static RangeResults<ArtistId> getRecentArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range = std::nullopt);
static RangeResults<ReleaseId> getRecentReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, Range range = {}); static RangeResults<ReleaseId> getRecentReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<Range> range = std::nullopt);
static RangeResults<TrackId> getRecentTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, Range range = {}); static RangeResults<TrackId> getRecentTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<Range> range = std::nullopt);
static std::size_t getCount(Session& session, UserId userId, ScrobblingBackend backend, TrackId trackId); static std::size_t getCount(Session& session, UserId userId, ScrobblingBackend backend, TrackId trackId);
static std::size_t getCount(Session& session, UserId userId, ScrobblingBackend backend, ReleaseId trackId); static std::size_t getCount(Session& session, UserId userId, ScrobblingBackend backend, ReleaseId trackId);
@@ -53,7 +53,7 @@ namespace Database
std::vector<ClusterId> clusters; // if non empty, releases that belong to these clusters std::vector<ClusterId> clusters; // if non empty, releases that belong to these clusters
std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords
ReleaseSortMethod sortMethod{ ReleaseSortMethod::None }; ReleaseSortMethod sortMethod{ ReleaseSortMethod::None };
Range range; std::optional<Range> range;
Wt::WDateTime writtenAfter; Wt::WDateTime writtenAfter;
std::optional<DateRange> dateRange; std::optional<DateRange> dateRange;
UserId starringUser; // only releases starred by this user UserId starringUser; // only releases starred by this user
@@ -67,7 +67,7 @@ namespace Database
FindParameters& setClusters(const std::vector<ClusterId>& _clusters) { clusters = _clusters; return *this; } FindParameters& setClusters(const std::vector<ClusterId>& _clusters) { clusters = _clusters; return *this; }
FindParameters& setKeywords(const std::vector<std::string_view>& _keywords) { keywords = _keywords; return *this; } FindParameters& setKeywords(const std::vector<std::string_view>& _keywords) { keywords = _keywords; return *this; }
FindParameters& setSortMethod(ReleaseSortMethod _sortMethod) { sortMethod = _sortMethod; return *this; } FindParameters& setSortMethod(ReleaseSortMethod _sortMethod) { sortMethod = _sortMethod; return *this; }
FindParameters& setRange(Range _range) { range = _range; return *this; } FindParameters& setRange(std::optional<Range> _range) { range = _range; return *this; }
FindParameters& setWrittenAfter(const Wt::WDateTime& _after) { writtenAfter = _after; return *this; } FindParameters& setWrittenAfter(const Wt::WDateTime& _after) { writtenAfter = _after; return *this; }
FindParameters& setDateRange(const std::optional<DateRange>& _dateRange) { dateRange = _dateRange; return *this; } FindParameters& setDateRange(const std::optional<DateRange>& _dateRange) { dateRange = _dateRange; return *this; }
FindParameters& setStarringUser(UserId _user, FeedbackBackend _feedbackBackend) { starringUser = _user; feedbackBackend = _feedbackBackend; return *this; } FindParameters& setStarringUser(UserId _user, FeedbackBackend _feedbackBackend) { starringUser = _user; feedbackBackend = _feedbackBackend; return *this; }
@@ -89,9 +89,10 @@ namespace Database
static std::vector<pointer> find(Session& session, const std::string& name); static std::vector<pointer> find(Session& session, const std::string& name);
static pointer find(Session& session, ReleaseId id); static pointer find(Session& session, ReleaseId id);
static RangeResults<pointer> find(Session& session, const FindParameters& parameters); static RangeResults<pointer> find(Session& session, const FindParameters& parameters);
static void find(Session& session, const FindParameters& parameters, std::function<void(const pointer&)> func);
static RangeResults<ReleaseId> findIds(Session& session, const FindParameters& parameters); static RangeResults<ReleaseId> findIds(Session& session, const FindParameters& parameters);
static RangeResults<ReleaseId> findOrphanIds(Session& session, Range range); // not track related static RangeResults<ReleaseId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt); // not track related
static RangeResults<ReleaseId> findIdsOrderedByArtist(Session& session, Range range); static RangeResults<ReleaseId> findIdsOrderedByArtist(Session& session, std::optional<Range> range = std::nullopt);
// Get the cluster of the tracks that belong to this release // Get the cluster of the tracks that belong to this release
// Each clusters are grouped by cluster type, sorted by the number of occurence (max to min) // Each clusters are grouped by cluster type, sorted by the number of occurence (max to min)
@@ -103,6 +104,7 @@ namespace Database
Wt::WDate getOriginalReleaseDate() const; Wt::WDate getOriginalReleaseDate() const;
std::optional<std::string> getCopyright() const; std::optional<std::string> getCopyright() const;
std::optional<std::string> getCopyrightURL() const; std::optional<std::string> getCopyrightURL() const;
std::size_t getMeanBitrate() const;
// Accessors // Accessors
const std::string& getName() const { return _name; } const std::string& getName() const { return _name; }
@@ -45,11 +45,11 @@ namespace Database
std::optional<FeedbackBackend> backend; // for this backend std::optional<FeedbackBackend> backend; // for this backend
std::optional<SyncState> syncState; // and these states std::optional<SyncState> syncState; // and these states
UserId user; // and this user UserId user; // and this user
Range range; std::optional<Range> range;
FindParameters& setFeedbackBackend(FeedbackBackend _backend, SyncState _syncState) { backend = _backend; syncState = _syncState; return *this; } FindParameters& setFeedbackBackend(FeedbackBackend _backend, SyncState _syncState) { backend = _backend; syncState = _syncState; return *this; }
FindParameters& setUser(UserId _user) { user = _user; return *this; } FindParameters& setUser(UserId _user) { user = _user; return *this; }
FindParameters& setRange(Range _range) { range = _range; return *this; } FindParameters& setRange(std::optional<Range> _range) { range = _range; return *this; }
}; };
// Search utility // Search utility
@@ -46,207 +46,212 @@
namespace Database { namespace Database {
class Artist; class Artist;
class Cluster; class Cluster;
class ClusterType; class ClusterType;
class Release; class Release;
class Session; class Session;
class TrackArtistLink; class TrackArtistLink;
class TrackStats; class TrackStats;
class User; class User;
class Track final : public Object<Track, TrackId> class Track final : public Object<Track, TrackId>
{ {
public: public:
struct FindParameters struct FindParameters
{ {
std::vector<ClusterId> clusters; // if non empty, tracks that belong to these clusters std::vector<ClusterId> clusters; // if non empty, tracks that belong to these clusters
std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords
std::string name; // if non empty, must match this name std::string name; // if non empty, must match this name
TrackSortMethod sortMethod {TrackSortMethod::None}; TrackSortMethod sortMethod{ TrackSortMethod::None };
Range range; std::optional<Range> range;
Wt::WDateTime writtenAfter; Wt::WDateTime writtenAfter;
UserId starringUser; // only tracks starred by this user UserId starringUser; // only tracks starred by this user
std::optional<FeedbackBackend> feedbackBackend; // and for this feedback backend std::optional<FeedbackBackend> feedbackBackend; // and for this feedback backend
ArtistId artist; // only tracks that involve this artist ArtistId artist; // only tracks that involve this artist
std::string artistName; // only tracks that involve this artist name std::string artistName; // only tracks that involve this artist name
EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types
bool nonRelease {}; // only tracks that do not belong to a release bool nonRelease{}; // only tracks that do not belong to a release
ReleaseId release; // matching this release ReleaseId release; // matching this release
std::string releaseName; // matching this release name std::string releaseName; // matching this release name
TrackListId trackList; // matching this trackList TrackListId trackList; // matching this trackList
std::optional<int> trackNumber; // matching this track number std::optional<int> trackNumber; // matching this track number
bool distinct {true}; bool distinct{ true };
FindParameters& setClusters(const std::vector<ClusterId>& _clusters) { clusters = _clusters; return *this; } FindParameters& setClusters(const std::vector<ClusterId>& _clusters) { clusters = _clusters; return *this; }
FindParameters& setKeywords(const std::vector<std::string_view>& _keywords) { keywords = _keywords; return *this; } FindParameters& setKeywords(const std::vector<std::string_view>& _keywords) { keywords = _keywords; return *this; }
FindParameters& setName(std::string_view _name) { name = _name; return *this; } FindParameters& setName(std::string_view _name) { name = _name; return *this; }
FindParameters& setSortMethod(TrackSortMethod _method) { sortMethod = _method; return *this; } FindParameters& setSortMethod(TrackSortMethod _method) { sortMethod = _method; return *this; }
FindParameters& setRange(Range _range) { range = _range; return *this; } FindParameters& setRange(std::optional<Range> _range) { range = _range; return *this; }
FindParameters& setWrittenAfter(const Wt::WDateTime& _after) { writtenAfter = _after; return *this; } FindParameters& setWrittenAfter(const Wt::WDateTime& _after) { writtenAfter = _after; return *this; }
FindParameters& setStarringUser(UserId _user, FeedbackBackend _feedbackBackend) { starringUser = _user; feedbackBackend = _feedbackBackend; return *this; } FindParameters& setStarringUser(UserId _user, FeedbackBackend _feedbackBackend) { starringUser = _user; feedbackBackend = _feedbackBackend; return *this; }
FindParameters& setArtist(ArtistId _artist, EnumSet<TrackArtistLinkType> _trackArtistLinkTypes = {}) { artist = _artist; trackArtistLinkTypes = _trackArtistLinkTypes; return *this; } FindParameters& setArtist(ArtistId _artist, EnumSet<TrackArtistLinkType> _trackArtistLinkTypes = {}) { artist = _artist; trackArtistLinkTypes = _trackArtistLinkTypes; return *this; }
FindParameters& setArtistName(std::string_view _artistName, EnumSet<TrackArtistLinkType> _trackArtistLinkTypes = {}) { artistName = _artistName; trackArtistLinkTypes = _trackArtistLinkTypes; return *this; } FindParameters& setArtistName(std::string_view _artistName, EnumSet<TrackArtistLinkType> _trackArtistLinkTypes = {}) { artistName = _artistName; trackArtistLinkTypes = _trackArtistLinkTypes; return *this; }
FindParameters& setNonRelease(bool _nonRelease) { nonRelease = _nonRelease; return *this; } FindParameters& setNonRelease(bool _nonRelease) { nonRelease = _nonRelease; return *this; }
FindParameters& setRelease(ReleaseId _release) { release = _release; return *this; } FindParameters& setRelease(ReleaseId _release) { release = _release; return *this; }
FindParameters& setReleaseName(std::string_view _releaseName) { releaseName = _releaseName; return *this; } FindParameters& setReleaseName(std::string_view _releaseName) { releaseName = _releaseName; return *this; }
FindParameters& setTrackList(TrackListId _trackList) { trackList = _trackList; return *this; } FindParameters& setTrackList(TrackListId _trackList) { trackList = _trackList; return *this; }
FindParameters& setTrackNumber(int _trackNumber) { trackNumber = _trackNumber; return *this; } FindParameters& setTrackNumber(int _trackNumber) { trackNumber = _trackNumber; return *this; }
FindParameters& setDistinct(bool _distinct) { distinct = _distinct; return *this; } FindParameters& setDistinct(bool _distinct) { distinct = _distinct; return *this; }
}; };
struct PathResult struct PathResult
{ {
TrackId trackId; TrackId trackId;
std::filesystem::path path; std::filesystem::path path;
}; };
Track() = default; Track() = default;
// Find utility functions // Find utility functions
static std::size_t getCount(Session& session); static std::size_t getCount(Session& session);
static pointer findByPath(Session& session, const std::filesystem::path& p); static pointer findByPath(Session& session, const std::filesystem::path& p);
static pointer find(Session& session, TrackId id); static pointer find(Session& session, TrackId id);
static bool exists(Session& session, TrackId id); static bool exists(Session& session, TrackId id);
static std::vector<pointer> findByRecordingMBID(Session& session, const UUID& MBID); static std::vector<pointer> findByRecordingMBID(Session& session, const UUID& MBID);
static std::vector<pointer> findByMBID(Session& session, const UUID& MBID); static std::vector<pointer> findByMBID(Session& session, const UUID& MBID);
static RangeResults<TrackId> findSimilarTrackIds(Session& session, const std::vector<TrackId>& trackIds, Range range); static RangeResults<TrackId> findSimilarTrackIds(Session& session, const std::vector<TrackId>& trackIds, std::optional<Range> range = std::nullopt);
static RangeResults<TrackId> findIds(Session& session, const FindParameters& parameters); static RangeResults<TrackId> findIds(Session& session, const FindParameters& parameters);
static RangeResults<pointer> find(Session& session, const FindParameters& parameters); static RangeResults<pointer> find(Session& session, const FindParameters& parameters);
static RangeResults<PathResult> findPaths(Session& session, Range range); static void find(Session& session, const FindParameters& parameters, std::function<void(const Track::pointer&)> func);
static RangeResults<TrackId> findIdsTrackMBIDDuplicates(Session& session, Range range); static RangeResults<PathResult> findPaths(Session& session, std::optional<Range> range = std::nullopt);
static RangeResults<TrackId> findIdsWithRecordingMBIDAndMissingFeatures(Session& session, Range range); static RangeResults<TrackId> findIdsTrackMBIDDuplicates(Session& session, std::optional<Range> range = std::nullopt);
static RangeResults<TrackId> findIdsWithRecordingMBIDAndMissingFeatures(Session& session, std::optional<Range> range = std::nullopt);
// Accessors // Accessors
void setScanVersion(std::size_t version) { _scanVersion = version; } void setScanVersion(std::size_t version) { _scanVersion = version; }
void setTrackNumber(std::optional<int> num) { _trackNumber = num; } void setTrackNumber(std::optional<int> num) { _trackNumber = num; }
void setDiscNumber(std::optional<int> num) { _discNumber = num; } void setDiscNumber(std::optional<int> num) { _discNumber = num; }
void setTotalTrack(std::optional<int> totalTrack) { _totalTrack = totalTrack; } void setTotalTrack(std::optional<int> totalTrack) { _totalTrack = totalTrack; }
void setDiscSubtitle(const std::string& name) { _discSubtitle = name; } void setDiscSubtitle(const std::string& name) { _discSubtitle = name; }
void setName(const std::string& name) { _name = std::string(name, 0, _maxNameLength); } void setName(const std::string& name) { _name = std::string(name, 0, _maxNameLength); }
void setPath(const std::filesystem::path& filePath) { _filePath = filePath; } void setPath(const std::filesystem::path& filePath) { _filePath = filePath; }
void setDuration(std::chrono::milliseconds duration) { _duration = duration; } void setDuration(std::chrono::milliseconds duration) { _duration = duration; }
void setLastWriteTime(Wt::WDateTime time) { _fileLastWrite = time; } void setBitrate(std::size_t bitrate) { _bitrate = bitrate; }
void setAddedTime(Wt::WDateTime time) { _fileAdded = time; } void setLastWriteTime(Wt::WDateTime time) { _fileLastWrite = time; }
void setDate(const Wt::WDate& date) { _date = date; } void setAddedTime(Wt::WDateTime time) { _fileAdded = time; }
void setOriginalDate(const Wt::WDate& date) { _originalDate = date; } void setDate(const Wt::WDate& date) { _date = date; }
void setHasCover(bool hasCover) { _hasCover = hasCover; } void setOriginalDate(const Wt::WDate& date) { _originalDate = date; }
void setTrackMBID(const std::optional<UUID>& MBID) { _trackMBID = MBID ? MBID->getAsString() : ""; } void setHasCover(bool hasCover) { _hasCover = hasCover; }
void setRecordingMBID(const std::optional<UUID>& MBID) { _recordingMBID = MBID ? MBID->getAsString() : ""; } void setTrackMBID(const std::optional<UUID>& MBID) { _trackMBID = MBID ? MBID->getAsString() : ""; }
void setCopyright(const std::string& copyright) { _copyright = std::string(copyright, 0, _maxCopyrightLength); } void setRecordingMBID(const std::optional<UUID>& MBID) { _recordingMBID = MBID ? MBID->getAsString() : ""; }
void setCopyrightURL(const std::string& copyrightURL) { _copyrightURL = std::string(copyrightURL, 0, _maxCopyrightURLLength); } void setCopyright(const std::string& copyright) { _copyright = std::string(copyright, 0, _maxCopyrightLength); }
void setTrackReplayGain(std::optional<float> replayGain) { _trackReplayGain = replayGain; } void setCopyrightURL(const std::string& copyrightURL) { _copyrightURL = std::string(copyrightURL, 0, _maxCopyrightURLLength); }
void setReleaseReplayGain(std::optional<float> replayGain) { _releaseReplayGain = replayGain; } // may be by disc! void setTrackReplayGain(std::optional<float> replayGain) { _trackReplayGain = replayGain; }
void setArtistDisplayName(std::string_view name) { _artistDisplayName = name; } void setReleaseReplayGain(std::optional<float> replayGain) { _releaseReplayGain = replayGain; } // may be by disc!
void clearArtistLinks(); void setArtistDisplayName(std::string_view name) { _artistDisplayName = name; }
void addArtistLink(const ObjectPtr<TrackArtistLink>& artistLink); void clearArtistLinks();
void setRelease(ObjectPtr<Release> release) { _release = getDboPtr(release); } void addArtistLink(const ObjectPtr<TrackArtistLink>& artistLink);
void setClusters(const std::vector<ObjectPtr<Cluster>>& clusters ); void setRelease(ObjectPtr<Release> release) { _release = getDboPtr(release); }
void setClusters(const std::vector<ObjectPtr<Cluster>>& clusters);
std::size_t getScanVersion() const { return _scanVersion; } std::size_t getScanVersion() const { return _scanVersion; }
std::optional<std::size_t> getTrackNumber() const { return _trackNumber; } std::optional<std::size_t> getTrackNumber() const { return _trackNumber; }
std::optional<std::size_t> getTotalTrack() const { return _totalTrack; } std::optional<std::size_t> getTotalTrack() const { return _totalTrack; }
std::optional<std::size_t> getDiscNumber() const { return _discNumber; } std::optional<std::size_t> getDiscNumber() const { return _discNumber; }
const std::string& getDiscSubtitle() const { return _discSubtitle; } const std::string& getDiscSubtitle() const { return _discSubtitle; }
std::string getName() const { return _name; } std::string getName() const { return _name; }
std::filesystem::path getPath() const { return _filePath; } std::filesystem::path getPath() const { return _filePath; }
std::chrono::milliseconds getDuration() const { return _duration; } std::chrono::milliseconds getDuration() const { return _duration; }
const Wt::WDateTime& getLastWritten() const { return _fileLastWrite; } std::size_t getBitrate() const { return _bitrate; }
std::optional<int> getYear() const; const Wt::WDateTime& getLastWritten() const { return _fileLastWrite; }
std::optional<int> getOriginalYear() const; std::optional<int> getYear() const;
Wt::WDateTime getLastWriteTime() const { return _fileLastWrite; } std::optional<int> getOriginalYear() const;
Wt::WDateTime getAddedTime() const { return _fileAdded; } Wt::WDateTime getLastWriteTime() const { return _fileLastWrite; }
bool hasCover() const { return _hasCover; } Wt::WDateTime getAddedTime() const { return _fileAdded; }
std::optional<UUID> getTrackMBID() const { return UUID::fromString(_trackMBID); } bool hasCover() const { return _hasCover; }
std::optional<UUID> getRecordingMBID() const { return UUID::fromString(_recordingMBID); } std::optional<UUID> getTrackMBID() const { return UUID::fromString(_trackMBID); }
std::optional<std::string> getCopyright() const; std::optional<UUID> getRecordingMBID() const { return UUID::fromString(_recordingMBID); }
std::optional<std::string> getCopyrightURL() const; std::optional<std::string> getCopyright() const;
std::optional<float> getTrackReplayGain() const { return _trackReplayGain; } std::optional<std::string> getCopyrightURL() const;
std::optional<float> getReleaseReplayGain() const { return _releaseReplayGain; } std::optional<float> getTrackReplayGain() const { return _trackReplayGain; }
std::string_view getArtistDisplayName() const { return _artistDisplayName; } std::optional<float> getReleaseReplayGain() const { return _releaseReplayGain; }
// no artistLinkTypes means get all std::string_view getArtistDisplayName() const { return _artistDisplayName; }
std::vector<ObjectPtr<Artist>> getArtists(EnumSet<TrackArtistLinkType> artistLinkTypes) const; // no type means all // no artistLinkTypes means get all
std::vector<ArtistId> getArtistIds(EnumSet<TrackArtistLinkType> artistLinkTypes) const; // no type means all std::vector<ObjectPtr<Artist>> getArtists(EnumSet<TrackArtistLinkType> artistLinkTypes) const; // no type means all
std::vector<ObjectPtr<TrackArtistLink>> getArtistLinks() const; std::vector<ArtistId> getArtistIds(EnumSet<TrackArtistLinkType> artistLinkTypes) const; // no type means all
ObjectPtr<Release> getRelease() const { return _release; } std::vector<ObjectPtr<TrackArtistLink>> getArtistLinks() const;
std::vector<ObjectPtr<Cluster>> getClusters() const; ObjectPtr<Release> getRelease() const { return _release; }
std::vector<ClusterId> getClusterIds() const; std::vector<ObjectPtr<Cluster>> getClusters() const;
std::vector<ClusterId> getClusterIds() const;
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(const std::vector<ObjectPtr<ClusterType>>& clusterTypes, std::size_t size) const; std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(const std::vector<ObjectPtr<ClusterType>>& clusterTypes, std::size_t size) const;
template<class Action> template<class Action>
void persist(Action& a) void persist(Action& a)
{ {
Wt::Dbo::field(a, _scanVersion, "scan_version"); Wt::Dbo::field(a, _scanVersion, "scan_version");
Wt::Dbo::field(a, _trackNumber, "track_number"); Wt::Dbo::field(a, _trackNumber, "track_number");
Wt::Dbo::field(a, _discNumber, "disc_number"); Wt::Dbo::field(a, _discNumber, "disc_number");
Wt::Dbo::field(a, _totalTrack, "total_track"); // here in Track since Release does not have concept of "disc" (yet?) Wt::Dbo::field(a, _totalTrack, "total_track"); // here in Track since Release does not have concept of "disc" (yet?)
Wt::Dbo::field(a, _discSubtitle, "disc_subtitle"); // here in Track since Release does not have concept of "disc" (yet?) Wt::Dbo::field(a, _discSubtitle, "disc_subtitle"); // here in Track since Release does not have concept of "disc" (yet?)
Wt::Dbo::field(a, _name, "name"); Wt::Dbo::field(a, _name, "name");
Wt::Dbo::field(a, _duration, "duration"); Wt::Dbo::field(a, _duration, "duration");
Wt::Dbo::field(a, _date, "date"); Wt::Dbo::field(a, _bitrate, "bitrate");
Wt::Dbo::field(a, _originalDate, "original_date"); Wt::Dbo::field(a, _date, "date");
Wt::Dbo::field(a, _filePath, "file_path"); Wt::Dbo::field(a, _originalDate, "original_date");
Wt::Dbo::field(a, _fileLastWrite, "file_last_write"); Wt::Dbo::field(a, _filePath, "file_path");
Wt::Dbo::field(a, _fileAdded, "file_added"); Wt::Dbo::field(a, _fileLastWrite, "file_last_write");
Wt::Dbo::field(a, _hasCover, "has_cover"); Wt::Dbo::field(a, _fileAdded, "file_added");
Wt::Dbo::field(a, _trackMBID, "mbid"); Wt::Dbo::field(a, _hasCover, "has_cover");
Wt::Dbo::field(a, _recordingMBID, "recording_mbid"); Wt::Dbo::field(a, _trackMBID, "mbid");
Wt::Dbo::field(a, _copyright, "copyright"); Wt::Dbo::field(a, _recordingMBID, "recording_mbid");
Wt::Dbo::field(a, _copyrightURL, "copyright_url"); Wt::Dbo::field(a, _copyright, "copyright");
Wt::Dbo::field(a, _trackReplayGain, "track_replay_gain"); Wt::Dbo::field(a, _copyrightURL, "copyright_url");
Wt::Dbo::field(a, _releaseReplayGain, "release_replay_gain"); // here in Track since Release does not have concept of "disc" (yet?) Wt::Dbo::field(a, _trackReplayGain, "track_replay_gain");
Wt::Dbo::field(a, _artistDisplayName, "artist_display_name"); Wt::Dbo::field(a, _releaseReplayGain, "release_replay_gain"); // here in Track since Release does not have concept of "disc" (yet?)
Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade); Wt::Dbo::field(a, _artistDisplayName, "artist_display_name");
Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "track"); Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade); Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "track");
} Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade);
}
private: private:
friend class ::Database::Session; friend class ::Database::Session;
Track(const std::filesystem::path& p); Track(const std::filesystem::path& p);
static pointer create(Session& session, const std::filesystem::path& p); static pointer create(Session& session, const std::filesystem::path& p);
static constexpr std::size_t _maxNameLength {128}; static constexpr std::size_t _maxNameLength{ 128 };
static constexpr std::size_t _maxCopyrightLength {128}; static constexpr std::size_t _maxCopyrightLength{ 128 };
static constexpr std::size_t _maxCopyrightURLLength {128}; static constexpr std::size_t _maxCopyrightURLLength{ 128 };
int _scanVersion {}; int _scanVersion{};
std::optional<int> _trackNumber {}; std::optional<int> _trackNumber{};
std::optional<int> _discNumber {}; std::optional<int> _discNumber{};
std::optional<int> _totalTrack {}; std::optional<int> _totalTrack{};
std::string _discSubtitle; std::string _discSubtitle;
std::string _name; std::string _name;
std::chrono::duration<int, std::milli> _duration {}; std::chrono::duration<int, std::milli> _duration{};
Wt::WDate _date; int _bitrate; // in bps
Wt::WDate _originalDate; Wt::WDate _date;
std::string _filePath; Wt::WDate _originalDate;
Wt::WDateTime _fileLastWrite; std::string _filePath;
Wt::WDateTime _fileAdded; Wt::WDateTime _fileLastWrite;
bool _hasCover {}; Wt::WDateTime _fileAdded;
std::string _trackMBID; bool _hasCover{};
std::string _recordingMBID; std::string _trackMBID;
std::string _copyright; std::string _recordingMBID;
std::string _copyrightURL; std::string _copyright;
std::optional<float> _trackReplayGain; std::string _copyrightURL;
std::optional<float> _releaseReplayGain; std::optional<float> _trackReplayGain;
std::string _artistDisplayName; std::optional<float> _releaseReplayGain;
std::string _artistDisplayName;
Wt::Dbo::ptr<Release> _release; Wt::Dbo::ptr<Release> _release;
Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks; Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks;
Wt::Dbo::collection<Wt::Dbo::ptr<Cluster>> _clusters; Wt::Dbo::collection<Wt::Dbo::ptr<Cluster>> _clusters;
}; };
namespace Debug namespace Debug
{ {
struct TrackInfo struct TrackInfo
{ {
Session& session; Session& session;
TrackId trackId; TrackId trackId;
}; };
std::ostream& operator<<(std::ostream& os, const TrackInfo& trackInfo); std::ostream& operator<<(std::ostream& os, const TrackInfo& trackInfo);
} }
} // namespace database } // namespace database
@@ -19,6 +19,7 @@
#pragma once #pragma once
#include <optional>
#include <string> #include <string>
#include <string_view> #include <string_view>
@@ -45,13 +46,13 @@ namespace Database
public: public:
struct FindParameters struct FindParameters
{ {
Range range; std::optional<Range> range;
std::optional<TrackArtistLinkType> linkType; // if set, only artists that have produced at least one track with this link type std::optional<TrackArtistLinkType> linkType; // if set, only artists that have produced at least one track with this link type
ArtistId artist; // if set, links involved with this artist ArtistId artist; // if set, links involved with this artist
ReleaseId release; // if set, artists involved in this release ReleaseId release; // if set, artists involved in this release
TrackId track; // if set, artists involved in this track TrackId track; // if set, artists involved in this track
FindParameters& setRange(Range _range) { range = _range; return *this; } FindParameters& setRange(std::optional<Range> _range) { range = _range; return *this; }
FindParameters& setLinkType(std::optional<TrackArtistLinkType> _linkType) { linkType = _linkType; return *this; } FindParameters& setLinkType(std::optional<TrackArtistLinkType> _linkType) { linkType = _linkType; return *this; }
FindParameters& setArtist(ArtistId _artist) { artist = _artist; return *this; } FindParameters& setArtist(ArtistId _artist) { artist = _artist; return *this; }
FindParameters& setRelease(ReleaseId _release) { release = _release; return *this; } FindParameters& setRelease(ReleaseId _release) { release = _release; return *this; }
@@ -20,6 +20,7 @@
#pragma once #pragma once
#include <chrono> #include <chrono>
#include <optional>
#include <Wt/Dbo/Dbo.h> #include <Wt/Dbo/Dbo.h>
@@ -45,7 +46,7 @@ class TrackBookmark final : public Object<TrackBookmark, TrackBookmarkId>
// Find utility functions // Find utility functions
static std::size_t getCount(Session& session); static std::size_t getCount(Session& session);
static pointer find(Session& session, TrackBookmarkId id); static pointer find(Session& session, TrackBookmarkId id);
static RangeResults<TrackBookmarkId> find(Session& session, UserId userId, Range range); static RangeResults<TrackBookmarkId> find(Session& session, UserId userId, std::optional<Range> range = std::nullopt);
static pointer find(Session& session, UserId userId, TrackId trackId); static pointer find(Session& session, UserId userId, TrackId trackId);
// Setters // Setters
@@ -19,6 +19,7 @@
#pragma once #pragma once
#include <optional>
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
#include <unordered_set> #include <unordered_set>
@@ -51,7 +52,7 @@ class TrackFeatures final : public Object<TrackFeatures, TrackFeaturesId>
static std::size_t getCount(Session& session); static std::size_t getCount(Session& session);
static pointer find(Session& session, TrackFeaturesId id); static pointer find(Session& session, TrackFeaturesId id);
static pointer find(Session& session, TrackId trackId); static pointer find(Session& session, TrackId trackId);
static RangeResults<TrackFeaturesId> find(Session& session, Range range); static RangeResults<TrackFeaturesId> find(Session& session, std::optional<Range> range = std::nullopt);
FeatureValues getFeatureValues(const FeatureName& feature) const; FeatureValues getFeatureValues(const FeatureName& feature) const;
FeatureValuesMap getFeatureValuesMap(const std::unordered_set<FeatureName>& featureNames) const; FeatureValuesMap getFeatureValuesMap(const std::unordered_set<FeatureName>& featureNames) const;
@@ -36,151 +36,141 @@
namespace Database { namespace Database {
class Artist; class Artist;
class Cluster; class Cluster;
class ClusterType; class ClusterType;
class Release; class Release;
class Session; class Session;
class Track; class Track;
class TrackListEntry; class TrackListEntry;
class User; class User;
class TrackList final : public Object<TrackList, TrackListId> class TrackList final : public Object<TrackList, TrackListId>
{ {
public: public:
TrackList() = default; TrackList() = default;
// Stats utility // Search utility
std::vector<ObjectPtr<Artist>> getTopArtists(const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range, bool& moreResults) const; struct FindParameters
std::vector<ObjectPtr<Release>> getTopReleases(const std::vector<ClusterId>& clusterIds, std::optional<Range> range, bool& moreResults) const; {
std::vector<ObjectPtr<Track>> getTopTracks(const std::vector<ClusterId>& clusterIds, std::optional<Range> range, bool& moreResults) const; std::vector<ClusterId> clusters; // if non empty, tracklists that have tracks that belong to these clusters
std::optional<Range> range;
std::optional<TrackListType> type;
UserId user; // only tracklists owned by this user
TrackListSortMethod sortMethod{ TrackListSortMethod::None };
// Search utility FindParameters& setClusters(const std::vector<ClusterId>& _clusters) { clusters = _clusters; return *this; }
struct FindParameters FindParameters& setRange(std::optional<Range> _range) { range = _range; return *this; }
{ FindParameters& setType(TrackListType _type) { type = _type; return *this; }
std::vector<ClusterId> clusters; // if non empty, tracklists that have tracks that belong to these clusters FindParameters& setUser(UserId _user) { user = _user; return *this; }
Range range; FindParameters& setSortMethod(TrackListSortMethod _sortMethod) { sortMethod = _sortMethod; return *this; }
std::optional<TrackListType> type; };
UserId user; // only tracklists owned by this user static std::size_t getCount(Session& session);
TrackListSortMethod sortMethod {TrackListSortMethod::None}; static pointer find(Session& session, std::string_view name, TrackListType type, UserId userId);
static pointer find(Session& session, TrackListId tracklistId);
static RangeResults<TrackListId> find(Session& session, const FindParameters& params);
FindParameters& setClusters(const std::vector<ClusterId>& _clusters) { clusters = _clusters; return *this; } // Accessors
FindParameters& setRange(Range _range) { range = _range; return *this; } std::string_view getName() const { return _name; }
FindParameters& setType(TrackListType _type) { type = _type; return *this; } bool isPublic() const { return _isPublic; }
FindParameters& setUser(UserId _user) { user = _user; return *this; } TrackListType getType() const { return _type; }
FindParameters& setSortMethod(TrackListSortMethod _sortMethod) {sortMethod = _sortMethod; return *this; } ObjectPtr<User> getUser() const { return _user; }
};
static std::size_t getCount(Session& session);
static pointer find(Session& session, std::string_view name, TrackListType type, UserId userId);
static pointer find(Session& session, TrackListId tracklistId);
static RangeResults<TrackListId> find(Session& session, const FindParameters& params);
// Accessors // Modifiers
std::string_view getName() const { return _name; } void setName(const std::string& name) { _name = name; }
bool isPublic() const { return _isPublic; } void setIsPublic(bool isPublic) { _isPublic = isPublic; }
TrackListType getType() const { return _type; } void clear() { _entries.clear(); }
ObjectPtr<User> getUser() const { return _user; }
// Modifiers // Get tracks, ordered by position
void setName(const std::string& name) { _name = name; } bool isEmpty() const;
void setIsPublic(bool isPublic) { _isPublic = isPublic; } std::size_t getCount() const;
void clear() { _entries.clear(); } ObjectPtr<TrackListEntry> getEntry(std::size_t pos) const;
std::vector<ObjectPtr<TrackListEntry>> getEntries(std::optional<Range> range = {}) const;
ObjectPtr<TrackListEntry> getEntryByTrackAndDateTime(ObjectPtr<Track> track, const Wt::WDateTime& dateTime) const;
// Get tracks, ordered by position RangeResults<ObjectPtr<Artist>> getArtists(const std::vector<ClusterId>& clusters, std::optional<TrackArtistLinkType> linkType, ArtistSortMethod sortMethod, std::optional<Range> range, bool& moreResults) const;
bool isEmpty() const; RangeResults<ObjectPtr<Release>> getReleases(const std::vector<ClusterId>& clusters, std::optional<Range> range, bool& moreResults) const;
std::size_t getCount() const; RangeResults<ObjectPtr<Track>> getTracks(const std::vector<ClusterId>& clusters, std::optional<Range> range, bool& moreResults) const;
ObjectPtr<TrackListEntry> getEntry(std::size_t pos) const;
std::vector<ObjectPtr<TrackListEntry>> getEntries(std::optional<Range> range = {}) const;
ObjectPtr<TrackListEntry> getEntryByTrackAndDateTime(ObjectPtr<Track> track, const Wt::WDateTime& dateTime) const;
std::vector<ObjectPtr<Artist>> getArtists(const std::vector<ClusterId>& clusters, std::optional<TrackArtistLinkType> linkType, ArtistSortMethod sortMethod, std::optional<Range> range, bool& moreResults) const; std::vector<TrackId> getTrackIds() const;
std::vector<ObjectPtr<Release>> getReleases(const std::vector<ClusterId>& clusters, std::optional<Range> range, bool& moreResults) const; std::chrono::milliseconds getDuration() const;
std::vector<ObjectPtr<Track>> getTracks(const std::vector<ClusterId>& clusters, std::optional<Range> range, bool& moreResults) const;
// Sorted by date time void setLastModifiedDateTime(const Wt::WDateTime& dateTime);
std::vector<ObjectPtr<Artist>> getArtistsOrderedByRecentFirst(const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range, bool& moreResults) const;
std::vector<ObjectPtr<Release>> getReleasesOrderedByRecentFirst(const std::vector<ClusterId>& clusterIds, std::optional<Range> range, bool& moreResults) const;
std::vector<ObjectPtr<Track>> getTracksOrderedByRecentFirst(const std::vector<ClusterId>& clusterIds, std::optional<Range> range, bool& moreResults) const;
std::vector<TrackId> getTrackIds() const; // Get clusters, order by occurence
std::chrono::milliseconds getDuration() const; std::vector<ObjectPtr<Cluster>> getClusters() const;
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(const std::vector<ObjectPtr<ClusterType>>& clusterTypes, std::size_t size) const;
void setLastModifiedDateTime(const Wt::WDateTime& dateTime); bool hasTrack(TrackId trackId) const;
// Get clusters, order by occurence // Ordered from most clusters in common
std::vector<ObjectPtr<Cluster>> getClusters() const; std::vector<ObjectPtr<Track>> getSimilarTracks(std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {}) const;
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(const std::vector<ObjectPtr<ClusterType>>& clusterTypes, std::size_t size) const;
bool hasTrack(TrackId trackId) const; template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _name, "name");
Wt::Dbo::field(a, _type, "type");
Wt::Dbo::field(a, _isPublic, "public");
Wt::Dbo::field(a, _creationDateTime, "creation_date_time");
Wt::Dbo::field(a, _lastModifiedDateTime, "last_modified_date_time");
// Ordered from most clusters in common Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
std::vector<ObjectPtr<Track>> getSimilarTracks(std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {}) const; Wt::Dbo::hasMany(a, _entries, Wt::Dbo::ManyToOne, "tracklist");
}
template<class Action> private:
void persist(Action& a) friend class Session;
{ TrackList(std::string_view name, TrackListType type, bool isPublic, ObjectPtr<User> user);
Wt::Dbo::field(a, _name, "name"); static pointer create(Session& session, std::string_view name, TrackListType type, bool isPublic, ObjectPtr<User> user);
Wt::Dbo::field(a, _type, "type");
Wt::Dbo::field(a, _isPublic, "public");
Wt::Dbo::field(a, _creationDateTime, "creation_date_time");
Wt::Dbo::field(a, _lastModifiedDateTime, "last_modified_date_time");
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade); std::string _name;
Wt::Dbo::hasMany(a, _entries, Wt::Dbo::ManyToOne, "tracklist"); TrackListType _type{ TrackListType::Playlist };
} bool _isPublic{ false };
Wt::WDateTime _creationDateTime;
Wt::WDateTime _lastModifiedDateTime;
private: Wt::Dbo::ptr<User> _user;
friend class Session; Wt::Dbo::collection<Wt::Dbo::ptr<TrackListEntry>> _entries;
TrackList(std::string_view name, TrackListType type, bool isPublic, ObjectPtr<User> user); };
static pointer create(Session& session, std::string_view name, TrackListType type, bool isPublic, ObjectPtr<User> user);
std::string _name; class TrackListEntry final : public Object<TrackListEntry, TrackListEntryId>
TrackListType _type {TrackListType::Playlist}; {
bool _isPublic {false}; public:
Wt::WDateTime _creationDateTime; TrackListEntry() = default;
Wt::WDateTime _lastModifiedDateTime;
Wt::Dbo::ptr<User> _user; bool hasOnPostCreated() const override { return true; }
Wt::Dbo::collection<Wt::Dbo::ptr<TrackListEntry>> _entries; void onPostCreated() override;
};
class TrackListEntry final : public Object<TrackListEntry, TrackListEntryId> bool hasOnPreRemove() const override { return true; }
{ void onPreRemove() override;
public:
TrackListEntry() = default;
bool hasOnPostCreated() const override { return true; } // find utility
void onPostCreated() override; static pointer getById(Session& session, TrackListEntryId id);
bool hasOnPreRemove() const override { return true; } // Accessors
void onPreRemove() override; ObjectPtr<Track> getTrack() const { return _track; }
const Wt::WDateTime& getDateTime() const { return _dateTime; }
// find utility template<class Action>
static pointer getById(Session& session, TrackListEntryId id); void persist(Action& a)
{
Wt::Dbo::field(a, _dateTime, "date_time");
// Accessors Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
ObjectPtr<Track> getTrack() const { return _track; } Wt::Dbo::belongsTo(a, _tracklist, "tracklist", Wt::Dbo::OnDeleteCascade);
const Wt::WDateTime& getDateTime() const { return _dateTime; } }
template<class Action> private:
void persist(Action& a) friend class Session;
{ TrackListEntry(ObjectPtr<Track> track, ObjectPtr<TrackList> tracklist, const Wt::WDateTime& dateTime);
Wt::Dbo::field(a, _dateTime, "date_time"); TrackListEntry(ObjectPtr<Track> track, ObjectPtr<TrackList> tracklist);
static pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<TrackList> tracklist, const Wt::WDateTime& dateTime = {});
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade); Wt::WDateTime _dateTime; // optional date time
Wt::Dbo::belongsTo(a, _tracklist, "tracklist", Wt::Dbo::OnDeleteCascade); Wt::Dbo::ptr<Track> _track;
} Wt::Dbo::ptr<TrackList> _tracklist;
};
private:
friend class Session;
TrackListEntry(ObjectPtr<Track> track, ObjectPtr<TrackList> tracklist, const Wt::WDateTime& dateTime);
TrackListEntry(ObjectPtr<Track> track, ObjectPtr<TrackList> tracklist);
static pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<TrackList> tracklist, const Wt::WDateTime& dateTime = {});
Wt::WDateTime _dateTime; // optional date time
Wt::Dbo::ptr<Track> _track;
Wt::Dbo::ptr<TrackList> _tracklist;
};
} // namespace Database } // namespace Database
@@ -26,208 +26,224 @@
namespace Database namespace Database
{ {
// Caution: do not change enum values if they are set! // Caution: do not change enum values if they are set!
// Request: // Request:
// size = 0 => no size limit! // size = 0 => means we don't want data
// Response (via RangeResults) // Response (via RangeResults)
// size => results size // size => results size
struct Range struct Range
{ {
std::size_t offset {}; std::size_t offset{};
std::size_t size {}; std::size_t size{};
// TODO remove this bool operator==(const Range& rhs) const { return offset == rhs.offset && size == rhs.size; }
operator bool() const { return size != 0; } };
};
template <typename T> // Func must return true to continue iterating
struct RangeResults template <typename Func>
{ void foreachSubRange(Range range, std::size_t subRangeSize, Func&& func)
Range range; {
std::vector<T> results; assert(subRangeSize > 0);
bool moreResults{};
RangeResults getSubRange(Range subRange) Range subRange{ range.offset, std::min(range.size, subRangeSize) };
{ while (subRange.size > 0)
assert(subRange.offset >= range.offset); {
if (!func(subRange))
break;
if (!subRange.size) subRange.offset += subRange.size;
subRange.size = range.size - (subRange.offset - range.offset); subRange.size = std::min(subRangeSize, range.size - (subRange.offset - range.offset));
}
}
subRange.offset = std::min(subRange.offset, range.offset + range.size); template <typename T>
subRange.size = std::min(subRange.size, range.offset + range.size - subRange.offset); struct RangeResults
{
Range range;
std::vector<T> results;
bool moreResults{};
RangeResults subResults; RangeResults getSubRange(Range subRange)
{
assert(subRange.offset >= range.offset);
auto itBegin {std::cbegin(results) + subRange.offset - range.offset}; if (!subRange.size)
auto itEnd {itBegin + subRange.size}; subRange.size = range.size - (subRange.offset - range.offset);
subResults.results.reserve(std::distance(itBegin, itEnd));
std::copy(itBegin, itEnd, std::back_inserter(subResults.results));
subResults.range = subRange; subRange.offset = std::min(subRange.offset, range.offset + range.size);
if (subRange.offset + subRange.size == range.offset + range.size) subRange.size = std::min(subRange.size, range.offset + range.size - subRange.offset);
subResults.moreResults = moreResults;
else
subResults.moreResults = true;
return subResults; RangeResults subResults;
}
};
struct DateRange auto itBegin{ std::cbegin(results) + subRange.offset - range.offset };
{ auto itEnd{ itBegin + subRange.size };
Wt::WDate begin; subResults.results.reserve(std::distance(itBegin, itEnd));
Wt::WDate end; std::copy(itBegin, itEnd, std::back_inserter(subResults.results));
static DateRange fromYearRange(int from, int to); subResults.range = subRange;
}; if (subRange.offset + subRange.size == range.offset + range.size)
subResults.moreResults = moreResults;
else
subResults.moreResults = true;
struct DiscInfo return subResults;
{ }
std::size_t position; };
std::string name;
};
enum class ArtistSortMethod struct DateRange
{ {
None, Wt::WDate begin;
ByName, Wt::WDate end;
BySortName,
Random,
LastWritten,
StarredDateDesc,
};
enum class ReleaseSortMethod static DateRange fromYearRange(int from, int to);
{ };
None,
Name,
Date,
OriginalDate,
OriginalDateDesc,
Random,
LastWritten,
StarredDateDesc,
};
enum class TrackListSortMethod struct DiscInfo
{ {
None, std::size_t position;
Name, std::string name;
LastModifiedDesc, };
};
enum class TrackSortMethod enum class ArtistSortMethod
{ {
None, None,
Random, ByName,
LastWritten, BySortName,
StarredDateDesc, Random,
Name, LastWritten,
DateDescAndRelease, StarredDateDesc,
Release, // order by disc/track number };
TrackList, // order by asc order in tracklist
};
enum class TrackArtistLinkType enum class ReleaseSortMethod
{ {
Artist = 0, // regular track artist None,
Arranger = 1, Name,
Composer = 2, Date,
Conductor = 3, OriginalDate,
Lyricist = 4, OriginalDateDesc,
Mixer = 5, Random,
Performer = 6, LastWritten,
Producer = 7, StarredDateDesc,
ReleaseArtist = 8, };
Remixer = 9,
Writer = 10,
};
// User selectable audio file formats enum class TrackListSortMethod
enum class AudioFormat {
{ None,
MP3 = 1, Name,
OGG_OPUS = 2, LastModifiedDesc,
OGG_VORBIS = 3, };
WEBM_VORBIS = 4,
MATROSKA_OPUS = 5,
};
using Bitrate = std::uint32_t; enum class TrackSortMethod
// Do not remove values! {
void visitAllowedAudioBitrates(std::function<void(Bitrate)>); None,
bool isAudioBitrateAllowed(Bitrate bitrate); Random,
LastWritten,
StarredDateDesc,
Name,
DateDescAndRelease,
Release, // order by disc/track number
TrackList, // order by asc order in tracklist
};
enum class ScrobblingBackend enum class TrackArtistLinkType
{ {
Internal = 0, Artist = 0, // regular track artist
ListenBrainz = 1, Arranger = 1,
}; Composer = 2,
Conductor = 3,
Lyricist = 4,
Mixer = 5,
Performer = 6,
Producer = 7,
ReleaseArtist = 8,
Remixer = 9,
Writer = 10,
};
enum class FeedbackBackend // User selectable transcoding output formats
{ enum class TranscodingOutputFormat
Internal = 0, {
ListenBrainz = 1, MP3 = 1,
}; OGG_OPUS = 2,
OGG_VORBIS = 3,
WEBM_VORBIS = 4,
MATROSKA_OPUS = 5,
};
enum class SyncState using Bitrate = std::uint32_t;
{ // Do not remove values!
PendingAdd = 0, void visitAllowedAudioBitrates(std::function<void(Bitrate)>);
Synchronized = 1, bool isAudioBitrateAllowed(Bitrate bitrate);
PendingRemove = 2,
};
enum class UserType enum class ScrobblingBackend
{ {
REGULAR = 0, Internal = 0,
ADMIN = 1, ListenBrainz = 1,
DEMO = 2, };
};
enum class UITheme enum class FeedbackBackend
{ {
Light = 0, Internal = 0,
Dark = 1, ListenBrainz = 1,
}; };
enum class SubsonicArtistListMode enum class SyncState
{ {
AllArtists = 0, PendingAdd = 0,
ReleaseArtists = 1, Synchronized = 1,
TrackArtists = 2, PendingRemove = 2,
}; };
enum class TrackListType enum class UserType
{ {
Playlist, // user controlled playlists REGULAR = 0,
Internal, // internal usage (current playqueue, history, ...) ADMIN = 1,
}; DEMO = 2,
};
// as defined in https://musicbrainz.org/doc/Release_Group/Type enum class UITheme
enum class ReleaseTypePrimary {
{ Light = 0,
Album, Dark = 1,
Single, };
EP,
Broadcast,
Other,
};
enum class ReleaseTypeSecondary enum class SubsonicArtistListMode
{ {
Compilation, AllArtists = 0,
Soundtrack, ReleaseArtists = 1,
Spokenword, TrackArtists = 2,
Interview, };
Audiobook,
AudioDrama, enum class TrackListType
Live, {
Remix, Playlist, // user controlled playlists
DJMix, Internal, // internal usage (current playqueue, history, ...)
Mixtape_Street, };
Demo,
}; // as defined in https://musicbrainz.org/doc/Release_Group/Type
enum class ReleaseTypePrimary
{
Album,
Single,
EP,
Broadcast,
Other,
};
enum class ReleaseTypeSecondary
{
Compilation,
Soundtrack,
Spokenword,
Interview,
Audiobook,
AudioDrama,
Live,
Remix,
DJMix,
Mixtape_Street,
Demo,
};
} }
@@ -49,17 +49,18 @@ namespace Database {
{ {
std::optional<ScrobblingBackend> scrobblingBackend; std::optional<ScrobblingBackend> scrobblingBackend;
std::optional<FeedbackBackend> feedbackBackend; std::optional<FeedbackBackend> feedbackBackend;
Range range; std::optional<Range> range;
FindParameters& setFeedbackBackend(FeedbackBackend _feedbackBackend) { feedbackBackend = _feedbackBackend; return *this; } FindParameters& setFeedbackBackend(FeedbackBackend _feedbackBackend) { feedbackBackend = _feedbackBackend; return *this; }
FindParameters& setScrobblingBackend(ScrobblingBackend _scrobblingBackend) { scrobblingBackend = _scrobblingBackend; return *this; } FindParameters& setScrobblingBackend(ScrobblingBackend _scrobblingBackend) { scrobblingBackend = _scrobblingBackend; return *this; }
FindParameters& setRange(Range _range) { range = _range; return *this; } FindParameters& setRange(std::optional<Range> _range) { range = _range; return *this; }
}; };
static inline constexpr std::size_t MinNameLength{ 3 }; static inline constexpr std::size_t MinNameLength{ 3 };
static inline constexpr std::size_t MaxNameLength{ 15 }; static inline constexpr std::size_t MaxNameLength{ 15 };
static inline constexpr AudioFormat defaultSubsonicTranscodeFormat{ AudioFormat::OGG_OPUS }; static inline constexpr bool defaultSubsonicEnableTranscodingByDefault{ false };
static inline constexpr Bitrate defaultSubsonicTranscodeBitrate{ 128000 }; static inline constexpr TranscodingOutputFormat defaultSubsonicTranscodingOutputFormat{ TranscodingOutputFormat::OGG_OPUS };
static inline constexpr Bitrate defaultSubsonicTranscodingOutputBitrate{ 128000 };
static inline constexpr UITheme defaultUITheme{ UITheme::Dark }; static inline constexpr UITheme defaultUITheme{ UITheme::Dark };
static inline constexpr SubsonicArtistListMode defaultSubsonicArtistListMode{ SubsonicArtistListMode::AllArtists }; static inline constexpr SubsonicArtistListMode defaultSubsonicArtistListMode{ SubsonicArtistListMode::AllArtists };
static inline constexpr ScrobblingBackend defaultScrobblingBackend{ ScrobblingBackend::Internal }; static inline constexpr ScrobblingBackend defaultScrobblingBackend{ ScrobblingBackend::Internal };
@@ -83,8 +84,9 @@ namespace Database {
void setLastLogin(const Wt::WDateTime& dateTime) { _lastLogin = dateTime; } void setLastLogin(const Wt::WDateTime& dateTime) { _lastLogin = dateTime; }
void setPasswordHash(const PasswordHash& passwordHash) { _passwordSalt = passwordHash.salt; _passwordHash = passwordHash.hash; } void setPasswordHash(const PasswordHash& passwordHash) { _passwordSalt = passwordHash.salt; _passwordHash = passwordHash.hash; }
void setType(UserType type) { _type = type; } void setType(UserType type) { _type = type; }
void setSubsonicDefaultTranscodeFormat(AudioFormat encoding) { _subsonicDefaultTranscodeFormat = encoding; } void setSubsonicEnableTranscodingByDefault(bool value) { _subsonicEnableTranscodingByDefault = value; }
void setSubsonicDefaultTranscodeBitrate(Bitrate bitrate); void setSubsonicDefaultTranscodintOutputFormat(TranscodingOutputFormat encoding) { _subsonicDefaultTranscodingOutputFormat = encoding; }
void setSubsonicDefaultTranscodingOutputBitrate(Bitrate bitrate);
void setCurPlayingTrackPos(std::size_t pos) { _curPlayingTrackPos = pos; } void setCurPlayingTrackPos(std::size_t pos) { _curPlayingTrackPos = pos; }
void setRadio(bool val) { _radio = val; } void setRadio(bool val) { _radio = val; }
void setRepeatAll(bool val) { _repeatAll = val; } void setRepeatAll(bool val) { _repeatAll = val; }
@@ -99,8 +101,9 @@ namespace Database {
bool isAdmin() const { return _type == UserType::ADMIN; } bool isAdmin() const { return _type == UserType::ADMIN; }
bool isDemo() const { return _type == UserType::DEMO; } bool isDemo() const { return _type == UserType::DEMO; }
UserType getType() const { return _type; } UserType getType() const { return _type; }
AudioFormat getSubsonicDefaultTranscodeFormat() const { return _subsonicDefaultTranscodeFormat; } bool getSubsonicEnableTranscodingByDefault() const { return _subsonicEnableTranscodingByDefault; }
Bitrate getSubsonicDefaultTranscodeBitrate() const { return _subsonicDefaultTranscodeBitrate; } TranscodingOutputFormat getSubsonicDefaultTranscodingOutputFormat() const { return _subsonicDefaultTranscodingOutputFormat; }
Bitrate getSubsonicDefaultTranscodingOutputBitrate() const { return _subsonicDefaultTranscodingOutputBitrate; }
std::size_t getCurPlayingTrackPos() const { return _curPlayingTrackPos; } std::size_t getCurPlayingTrackPos() const { return _curPlayingTrackPos; }
bool isRepeatAllSet() const { return _repeatAll; } bool isRepeatAllSet() const { return _repeatAll; }
bool isRadioSet() const { return _radio; } bool isRadioSet() const { return _radio; }
@@ -118,8 +121,9 @@ namespace Database {
Wt::Dbo::field(a, _passwordSalt, "password_salt"); Wt::Dbo::field(a, _passwordSalt, "password_salt");
Wt::Dbo::field(a, _passwordHash, "password_hash"); Wt::Dbo::field(a, _passwordHash, "password_hash");
Wt::Dbo::field(a, _lastLogin, "last_login"); Wt::Dbo::field(a, _lastLogin, "last_login");
Wt::Dbo::field(a, _subsonicDefaultTranscodeFormat, "subsonic_default_transcode_format"); Wt::Dbo::field(a, _subsonicEnableTranscodingByDefault, "subsonic_enable_transcoding_by_default");
Wt::Dbo::field(a, _subsonicDefaultTranscodeBitrate, "subsonic_default_transcode_bitrate"); Wt::Dbo::field(a, _subsonicDefaultTranscodingOutputFormat, "subsonic_default_transcode_format");
Wt::Dbo::field(a, _subsonicDefaultTranscodingOutputBitrate, "subsonic_default_transcode_bitrate");
Wt::Dbo::field(a, _subsonicArtistListMode, "subsonic_artist_list_mode"); Wt::Dbo::field(a, _subsonicArtistListMode, "subsonic_artist_list_mode");
Wt::Dbo::field(a, _uiTheme, "ui_theme"); Wt::Dbo::field(a, _uiTheme, "ui_theme");
Wt::Dbo::field(a, _feedbackBackend, "feedback_backend"); Wt::Dbo::field(a, _feedbackBackend, "feedback_backend");
@@ -153,8 +157,9 @@ namespace Database {
// User defined settings // User defined settings
SubsonicArtistListMode _subsonicArtistListMode{ defaultSubsonicArtistListMode }; SubsonicArtistListMode _subsonicArtistListMode{ defaultSubsonicArtistListMode };
AudioFormat _subsonicDefaultTranscodeFormat{ defaultSubsonicTranscodeFormat }; bool _subsonicEnableTranscodingByDefault{ defaultSubsonicEnableTranscodingByDefault };
int _subsonicDefaultTranscodeBitrate{ defaultSubsonicTranscodeBitrate }; TranscodingOutputFormat _subsonicDefaultTranscodingOutputFormat{ defaultSubsonicTranscodingOutputFormat };
int _subsonicDefaultTranscodingOutputBitrate{ defaultSubsonicTranscodingOutputBitrate };
// User's dynamic data (UI) // User's dynamic data (UI)
int _curPlayingTrackPos{}; // Current track position in queue int _curPlayingTrackPos{}; // Current track position in queue
+15 -3
View File
@@ -51,7 +51,7 @@ TEST_F(DatabaseFixture, Artist)
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId()); EXPECT_EQ(artists.results.front(), artist.getId());
artists = Artist::findOrphanIds(session, Range{}); artists = Artist::findOrphanIds(session);
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId()); EXPECT_EQ(artists.results.front(), artist.getId());
} }
@@ -64,6 +64,18 @@ TEST_F(DatabaseFixture, Artist)
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front()->getId(), artist.getId()); EXPECT_EQ(artists.results.front()->getId(), artist.getId());
} }
{
auto transaction{ session.createSharedTransaction() };
bool visited{};
Artist::find(session, Artist::FindParameters{}, [&](const Artist::pointer& a)
{
visited = true;
EXPECT_EQ(a->getId(), artist.getId());
});
EXPECT_TRUE(visited);
}
} }
TEST_F(DatabaseFixture, Artist_singleTrack) TEST_F(DatabaseFixture, Artist_singleTrack)
@@ -80,7 +92,7 @@ TEST_F(DatabaseFixture, Artist_singleTrack)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
EXPECT_TRUE(Artist::findOrphanIds(session, Range{}).results.empty()); EXPECT_TRUE(Artist::findOrphanIds(session).results.empty());
} }
{ {
@@ -215,7 +227,7 @@ TEST_F(DatabaseFixture, Artist_singleTrackMultiArtists)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
EXPECT_TRUE(Artist::findOrphanIds(session, Range{}).results.empty()); EXPECT_TRUE(Artist::findOrphanIds(session).results.empty());
} }
{ {
+58 -395
View File
@@ -49,26 +49,26 @@ TEST_F(DatabaseFixture, Cluster)
EXPECT_EQ(cluster->getType()->getId(), clusterType.getId()); EXPECT_EQ(cluster->getType()->getId(), clusterType.getId());
{ {
const auto clusters{ Cluster::find(session, Cluster::FindParameters {}) }; const auto clusters{ Cluster::findIds(session, Cluster::FindParameters {}) };
ASSERT_EQ(clusters.results.size(), 1);
EXPECT_EQ(std::get<ClusterId>(clusters.results.front()), cluster.getId());
}
{
const auto clusters{ Cluster::findOrphans(session, Range{}) };
ASSERT_EQ(clusters.results.size(), 1); ASSERT_EQ(clusters.results.size(), 1);
EXPECT_EQ(clusters.results.front(), cluster.getId()); EXPECT_EQ(clusters.results.front(), cluster.getId());
} }
auto clusterTypes{ ClusterType::find(session, Range {}) }; {
const auto clusters{ Cluster::findOrphans(session) };
ASSERT_EQ(clusters.results.size(), 1);
EXPECT_EQ(clusters.results.front(), cluster.getId());
}
auto clusterTypes{ ClusterType::find(session) };
ASSERT_EQ(clusterTypes.results.size(), 1); ASSERT_EQ(clusterTypes.results.size(), 1);
EXPECT_EQ(clusterTypes.results.front(), clusterType.getId()); EXPECT_EQ(clusterTypes.results.front(), clusterType.getId());
clusterTypes = ClusterType::findUsed(session, Range{}); clusterTypes = ClusterType::findUsed(session);
ASSERT_EQ(clusterTypes.results.size(), 1); ASSERT_EQ(clusterTypes.results.size(), 1);
EXPECT_EQ(clusterTypes.results.front(), clusterType.getId()); EXPECT_EQ(clusterTypes.results.front(), clusterType.getId());
clusterTypes = ClusterType::findOrphans(session, Range{}); clusterTypes = ClusterType::findOrphans(session);
EXPECT_TRUE(clusterTypes.results.empty()); EXPECT_TRUE(clusterTypes.results.empty());
} }
} }
@@ -76,11 +76,11 @@ TEST_F(DatabaseFixture, Cluster)
{ {
auto transaction{ session.createUniqueTransaction() }; auto transaction{ session.createUniqueTransaction() };
auto clusterTypes{ ClusterType::findOrphans(session, Range {}) }; auto clusterTypes{ ClusterType::findOrphans(session) };
ASSERT_EQ(clusterTypes.results.size(), 1); ASSERT_EQ(clusterTypes.results.size(), 1);
EXPECT_EQ(clusterTypes.results.front(), clusterType.getId()); EXPECT_EQ(clusterTypes.results.front(), clusterType.getId());
ASSERT_TRUE(ClusterType::findUsed(session, Range{}).results.empty()); ASSERT_TRUE(ClusterType::findUsed(session).results.empty());
} }
} }
@@ -91,8 +91,8 @@ TEST_F(DatabaseFixture, Cluster_singleTrack)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
EXPECT_TRUE(Cluster::findOrphans(session, Range{}).results.empty()); EXPECT_TRUE(Cluster::findOrphans(session).results.empty());
auto clusterTypes{ ClusterType::findOrphans(session, Range {}) }; auto clusterTypes{ ClusterType::findOrphans(session) };
ASSERT_EQ(clusterTypes.results.size(), 1); ASSERT_EQ(clusterTypes.results.size(), 1);
EXPECT_EQ(clusterTypes.results.front(), clusterType.getId()); EXPECT_EQ(clusterTypes.results.front(), clusterType.getId());
} }
@@ -102,10 +102,12 @@ TEST_F(DatabaseFixture, Cluster_singleTrack)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto clusters{ Cluster::findOrphans(session, Range {}) }; auto clusters{ Cluster::findOrphans(session) };
EXPECT_EQ(clusters.results.size(), 2); EXPECT_EQ(clusters.results.size(), 2);
EXPECT_TRUE(track->getClusters().empty()); EXPECT_TRUE(track->getClusters().empty());
EXPECT_TRUE(track->getClusterIds().empty()); EXPECT_TRUE(track->getClusterIds().empty());
EXPECT_EQ(Cluster::computeTrackCount(session, cluster1.getId()), 0);
EXPECT_EQ(Cluster::computeTrackCount(session, cluster2.getId()), 0);
} }
{ {
@@ -116,18 +118,20 @@ TEST_F(DatabaseFixture, Cluster_singleTrack)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto clusters{ Cluster::find(session, Cluster::FindParameters {}.setTrack(track.getId())) }; auto clusters{ Cluster::findIds(session, Cluster::FindParameters {}.setTrack(track.getId())) };
ASSERT_EQ(clusters.results.size(), 1); ASSERT_EQ(clusters.results.size(), 1);
EXPECT_EQ(std::get<ClusterId>(clusters.results.front()), cluster1.getId()); EXPECT_EQ(clusters.results.front(), cluster1.getId());
EXPECT_EQ(Cluster::computeTrackCount(session, cluster1.getId()), 1);
EXPECT_EQ(Cluster::computeTrackCount(session, cluster2.getId()), 0);
} }
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
auto clusters{ Cluster::findOrphans(session, Range {}) }; auto clusters{ Cluster::findOrphans(session) };
ASSERT_EQ(clusters.results.size(), 1); ASSERT_EQ(clusters.results.size(), 1);
EXPECT_EQ(clusters.results.front(), cluster2.getId()); EXPECT_EQ(clusters.results.front(), cluster2.getId());
EXPECT_TRUE(ClusterType::findOrphans(session, Range{}).results.empty()); EXPECT_TRUE(ClusterType::findOrphans(session).results.empty());
} }
{ {
@@ -182,6 +186,8 @@ TEST_F(DatabaseFixture, Cluster_singleTrackWithSeveralClusters)
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setClusters(clusterIds)) }; const auto tracks{ Track::findIds(session, Track::FindParameters{}.setClusters(clusterIds)) };
EXPECT_TRUE(tracks.results.empty()); EXPECT_TRUE(tracks.results.empty());
EXPECT_EQ(Cluster::computeTrackCount(session, cluster1.getId()), 1);
EXPECT_EQ(Cluster::computeTrackCount(session, cluster2.getId()), 0);
} }
{ {
@@ -196,6 +202,8 @@ TEST_F(DatabaseFixture, Cluster_singleTrackWithSeveralClusters)
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setClusters(clusterIds)) }; const auto tracks{ Track::findIds(session, Track::FindParameters{}.setClusters(clusterIds)) };
ASSERT_FALSE(tracks.results.empty()); ASSERT_FALSE(tracks.results.empty());
EXPECT_EQ(tracks.results.front(), track.getId()); EXPECT_EQ(tracks.results.front(), track.getId());
EXPECT_EQ(Cluster::computeTrackCount(session, cluster1.getId()), 1);
EXPECT_EQ(Cluster::computeTrackCount(session, cluster2.getId()), 1);
} }
} }
@@ -217,11 +225,11 @@ TEST_F(DatabaseFixture, Cluster_multiTracks)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
EXPECT_TRUE(Cluster::findOrphans(session, Range{}).results.empty()); EXPECT_TRUE(Cluster::findOrphans(session).results.empty());
EXPECT_EQ(cluster->getTracksCount(), tracks.size()); EXPECT_EQ(Cluster::computeTrackCount(session, cluster.getId()), tracks.size());
for (TrackId trackId : cluster->getTracks(Range{}).results) for (TrackId trackId : cluster->getTracks().results)
{ {
auto it{ std::find_if(std::cbegin(tracks), std::cend(tracks), [&](const ScopedTrack& track) { return trackId == track.getId(); }) }; auto it{ std::find_if(std::cbegin(tracks), std::cend(tracks), [&](const ScopedTrack& track) { return trackId == track.getId(); }) };
EXPECT_TRUE(it != std::cend(tracks)); EXPECT_TRUE(it != std::cend(tracks));
@@ -229,102 +237,6 @@ TEST_F(DatabaseFixture, Cluster_multiTracks)
} }
} }
TEST_F(DatabaseFixture, Cluster_multiTracksMultipleClustersTopRelease)
{
ScopedClusterType clusterType{ session, "ClusterType" };
ScopedCluster cluster1{ session, clusterType.lockAndGet(), "Cluster1" };
ScopedCluster cluster2{ session, clusterType.lockAndGet(), "Cluster2" };
ScopedCluster cluster3{ session, clusterType.lockAndGet(), "Cluster3" };
ScopedTrack trackA{ session, "TrackA" };
ScopedTrack trackB{ session, "TrackB" };
ScopedTrack trackC{ session, "TrackC" };
ScopedRelease releaseA{ session, "ReleaseA" };
ScopedRelease releaseB{ session, "ReleaseB" };
ScopedRelease releaseC{ session, "ReleaseC" };
ScopedUser user{ session, "MyUser" };
ScopedTrackList trackList{ session, "TrackList", TrackListType::Playlist, false, user.lockAndGet() };
{
auto transaction{ session.createSharedTransaction() };
EXPECT_EQ(trackList->getDuration(), std::chrono::seconds{ 0 });
}
{
auto transaction{ session.createUniqueTransaction() };
cluster1.get().modify()->addTrack(trackA.get());
cluster2.get().modify()->addTrack(trackB.get());
cluster2.get().modify()->addTrack(trackC.get());
cluster3.get().modify()->addTrack(trackC.get());
trackA.get().modify()->setRelease(releaseA.get());
trackB.get().modify()->setRelease(releaseB.get());
trackC.get().modify()->setRelease(releaseC.get());
}
{
auto transaction{ session.createUniqueTransaction() };
session.create<TrackListEntry>(trackA.get(), trackList.get());
session.create<TrackListEntry>(trackB.get(), trackList.get());
session.create<TrackListEntry>(trackB.get(), trackList.get());
}
{
auto transaction{ session.createSharedTransaction() };
bool hasMore;
const auto releases{ trackList->getTopReleases({}, std::nullopt, hasMore) };
ASSERT_EQ(releases.size(), 2);
EXPECT_EQ(releases[0]->getId(), releaseB.getId());
EXPECT_EQ(releases[1]->getId(), releaseA.getId());
}
{
auto transaction{ session.createSharedTransaction() };
bool hasMore;
auto releases{ trackList->getTopReleases({cluster1.getId()}, std::nullopt, hasMore) };
ASSERT_EQ(releases.size(), 1);
EXPECT_EQ(releases[0]->getId(), releaseA.getId());
releases = trackList->getTopReleases({ cluster2.getId() }, std::nullopt, hasMore);
ASSERT_EQ(releases.size(), 1);
EXPECT_EQ(releases[0]->getId(), releaseB.getId());
releases = trackList->getTopReleases({ cluster2.getId(), cluster1.getId() }, std::nullopt, hasMore);
EXPECT_TRUE(releases.empty());
releases = trackList->getTopReleases({ cluster2.getId(), cluster3.getId() }, std::nullopt, hasMore);
EXPECT_TRUE(releases.empty());
}
{
auto transaction{ session.createUniqueTransaction() };
session.create<TrackListEntry>(trackC.get(), trackList.get());
session.create<TrackListEntry>(trackC.get(), trackList.get());
session.create<TrackListEntry>(trackC.get(), trackList.get());
}
{
auto transaction{ session.createSharedTransaction() };
bool hasMore;
auto releases{ trackList->getTopReleases({cluster2.getId(), cluster3.getId()}, std::nullopt, hasMore) };
ASSERT_EQ(releases.size(), 1);
EXPECT_EQ(releases[0]->getId(), releaseC.getId());
releases = trackList->getTopReleases({ cluster2.getId() }, std::nullopt, hasMore);
ASSERT_EQ(releases.size(), 2);
EXPECT_EQ(releases[0]->getId(), releaseC.getId());
EXPECT_EQ(releases[1]->getId(), releaseB.getId());
}
}
TEST_F(DatabaseFixture, Cluster_singleTrackSingleReleaseSingleCluster) TEST_F(DatabaseFixture, Cluster_singleTrackSingleReleaseSingleCluster)
{ {
ScopedTrack track{ session, "MyTrackFile" }; ScopedTrack track{ session, "MyTrackFile" };
@@ -332,7 +244,7 @@ TEST_F(DatabaseFixture, Cluster_singleTrackSingleReleaseSingleCluster)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
EXPECT_TRUE(Cluster::findOrphans(session, Range{}).results.empty()); EXPECT_TRUE(Cluster::findOrphans(session).results.empty());
} }
ScopedClusterType clusterType{ session, "MyClusterType" }; ScopedClusterType clusterType{ session, "MyClusterType" };
@@ -341,9 +253,11 @@ TEST_F(DatabaseFixture, Cluster_singleTrackSingleReleaseSingleCluster)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
ASSERT_EQ(Cluster::findOrphans(session, Range{}).results.size(), 2); ASSERT_EQ(Cluster::findOrphans(session).results.size(), 2);
EXPECT_TRUE(Release::find(session, Release::FindParameters{}.setClusters({ unusedCluster.getId() })).results.empty()); EXPECT_TRUE(Release::find(session, Release::FindParameters{}.setClusters({ unusedCluster.getId() })).results.empty());
EXPECT_EQ(Release::find(session, Release::FindParameters{}).results.size(), 1); EXPECT_EQ(Release::find(session, Release::FindParameters{}).results.size(), 1);
EXPECT_EQ(Cluster::computeReleaseCount(session, cluster.getId()), 0);
EXPECT_EQ(Cluster::computeReleaseCount(session, unusedCluster.getId()), 0);
} }
{ {
@@ -357,18 +271,20 @@ TEST_F(DatabaseFixture, Cluster_singleTrackSingleReleaseSingleCluster)
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
{ {
auto clusters{ Cluster::findOrphans(session, Range {}) }; auto clusters{ Cluster::findOrphans(session) };
ASSERT_EQ(clusters.results.size(), 1); ASSERT_EQ(clusters.results.size(), 1);
EXPECT_EQ(clusters.results.front(), unusedCluster.getId()); EXPECT_EQ(clusters.results.front(), unusedCluster.getId());
} }
EXPECT_EQ(Cluster::computeReleaseCount(session, cluster.getId()), 1);
EXPECT_EQ(Cluster::computeReleaseCount(session, unusedCluster.getId()), 0);
} }
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
const auto clusters{ Cluster::find(session, Cluster::FindParameters{}.setRelease(release.getId())) }; const auto clusters{ Cluster::findIds(session, Cluster::FindParameters{}.setRelease(release.getId())) };
ASSERT_EQ(clusters.results.size(), 1); ASSERT_EQ(clusters.results.size(), 1);
EXPECT_EQ(std::get<ClusterId>(clusters.results.front()), cluster.getId()); EXPECT_EQ(clusters.results.front(), cluster.getId());
} }
{ {
@@ -389,10 +305,10 @@ TEST_F(DatabaseFixture, Cluster_singleTrackSingleReleaseSingleCluster)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
EXPECT_EQ(cluster->getReleasesCount(), 1); EXPECT_EQ(Cluster::computeReleaseCount(session, cluster.getId()), 1);
EXPECT_EQ(cluster->getTracksCount(), 1); EXPECT_EQ(Cluster::computeTrackCount(session, cluster.getId()), 1);
EXPECT_EQ(unusedCluster->getReleasesCount(), 0); EXPECT_EQ(Cluster::computeReleaseCount(session, unusedCluster.getId()), 0);
EXPECT_EQ(unusedCluster->getTracksCount(), 0); EXPECT_EQ(Cluster::computeTrackCount(session, unusedCluster.getId()), 0);
} }
} }
@@ -413,10 +329,10 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiClusters)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
EXPECT_TRUE(ClusterType::findOrphans(session, Range{}).results.empty()); EXPECT_TRUE(ClusterType::findOrphans(session).results.empty());
EXPECT_EQ(Cluster::findOrphans(session, Range{}).results.size(), 2); EXPECT_EQ(Cluster::findOrphans(session).results.size(), 2);
EXPECT_TRUE(Release::findOrphanIds(session, Range{}).results.empty()); EXPECT_TRUE(Release::findOrphanIds(session).results.empty());
EXPECT_TRUE(Artist::findOrphanIds(session, Range{}).results.empty()); EXPECT_TRUE(Artist::findOrphanIds(session).results.empty());
} }
{ {
@@ -474,9 +390,9 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiRolesMultiClusters)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
EXPECT_TRUE(Cluster::findOrphans(session, Range{}).results.empty()); EXPECT_TRUE(Cluster::findOrphans(session).results.empty());
EXPECT_TRUE(Release::findOrphanIds(session, Range{}).results.empty()); EXPECT_TRUE(Release::findOrphanIds(session).results.empty());
EXPECT_TRUE(Artist::findOrphanIds(session, Range{}).results.empty()); EXPECT_TRUE(Artist::findOrphanIds(session).results.empty());
} }
{ {
@@ -514,8 +430,8 @@ TEST_F(DatabaseFixture, MultiTracksSingleArtistMultiClusters)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
EXPECT_TRUE(Cluster::findOrphans(session, Range{}).results.empty()); EXPECT_TRUE(Cluster::findOrphans(session).results.empty());
EXPECT_TRUE(Artist::findOrphanIds(session, Range{}).results.empty()); EXPECT_TRUE(Artist::findOrphanIds(session).results.empty());
} }
{ {
@@ -550,7 +466,7 @@ TEST_F(DatabaseFixture, MultipleTracksSingleClusterSimilarity)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
const auto similarTracks{ Track::findSimilarTrackIds(session, {tracks.front().getId()}, Range {}) }; const auto similarTracks{ Track::findSimilarTrackIds(session, {tracks.front().getId()}) };
EXPECT_EQ(similarTracks.results.size(), tracks.size() - 1); EXPECT_EQ(similarTracks.results.size(), tracks.size() - 1);
for (const TrackId similarTrackId : similarTracks.results) for (const TrackId similarTrackId : similarTracks.results)
{ {
@@ -598,7 +514,7 @@ TEST_F(DatabaseFixture, MultipleTracksMultipleClustersSimilarity)
} }
{ {
auto similarTracks{ Track::findSimilarTrackIds(session, {tracks.front().getId()}, Range {}) }; auto similarTracks{ Track::findSimilarTrackIds(session, {tracks.front().getId()}) };
EXPECT_EQ(similarTracks.results.size(), tracks.size() - 1); EXPECT_EQ(similarTracks.results.size(), tracks.size() - 1);
for (const TrackId similarTrackId : similarTracks.results) for (const TrackId similarTrackId : similarTracks.results)
EXPECT_TRUE(std::find_if(std::next(std::cbegin(tracks), 1), std::cend(tracks), [&](const auto& track) { return similarTrackId == track.getId(); }) != std::cend(tracks)); EXPECT_TRUE(std::find_if(std::next(std::cbegin(tracks), 1), std::cend(tracks), [&](const auto& track) { return similarTrackId == track.getId(); }) != std::cend(tracks));
@@ -625,10 +541,10 @@ TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtistSingleCluster)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
EXPECT_TRUE(Cluster::findOrphans(session, Range{}).results.empty()); EXPECT_TRUE(Cluster::findOrphans(session).results.empty());
EXPECT_TRUE(ClusterType::findOrphans(session, Range{}).results.empty()); EXPECT_TRUE(ClusterType::findOrphans(session).results.empty());
EXPECT_TRUE(Artist::findOrphanIds(session, Range{}).results.empty()); EXPECT_TRUE(Artist::findOrphanIds(session).results.empty());
EXPECT_TRUE(Release::findOrphanIds(session, Range{}).results.empty()); EXPECT_TRUE(Release::findOrphanIds(session).results.empty());
} }
{ {
@@ -763,259 +679,6 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClusters)
} }
} }
TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClustersRecentlyPlayed)
{
ScopedUser user{ session, "MyUser" };
ScopedTrackList trackList{ session, "MyTrackList", TrackListType::Playlist, false, user.lockAndGet() };
ScopedClusterType clusterType{ session, "MyClusterType" };
ScopedCluster cluster1{ session, clusterType.lockAndGet(), "MyCluster1" };
ScopedCluster cluster2{ session, clusterType.lockAndGet(), "MyCluster2" };
ScopedCluster cluster3{ session, clusterType.lockAndGet(), "MyCluster3" };
ScopedTrack track1{ session, "MyTrack1" };
ScopedTrack track2{ session, "MyTrack1" };
ScopedArtist artist1{ session, "MyArtist1" };
ScopedArtist artist2{ session, "MyArtist2" };
ScopedRelease release1{ session, "MyRelease1" };
ScopedRelease release2{ session, "MyRelease2" };
const Wt::WDateTime now{ Wt::WDateTime::currentDateTime() };
{
auto transaction{ session.createUniqueTransaction() };
track1.get().modify()->setRelease(release1.get());
track2.get().modify()->setRelease(release2.get());
TrackArtistLink::create(session, track1.get(), artist1.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, track2.get(), artist2.get(), TrackArtistLinkType::Artist);
cluster1.get().modify()->addTrack(track1.get());
cluster2.get().modify()->addTrack(track2.get());
cluster3.get().modify()->addTrack(track1.get());
cluster3.get().modify()->addTrack(track2.get());
}
{
auto transaction{ session.createSharedTransaction() };
bool moreResults{};
EXPECT_TRUE(trackList->getArtistsOrderedByRecentFirst({}, std::nullopt, std::nullopt, moreResults).empty());
EXPECT_TRUE(trackList->getReleasesOrderedByRecentFirst({}, std::nullopt, moreResults).empty());
EXPECT_TRUE(trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults).empty());
}
{
auto transaction{ session.createUniqueTransaction() };
session.create<TrackListEntry>(track1.get(), trackList.get(), now);
}
{
auto transaction{ session.createSharedTransaction() };
bool moreResults{};
const auto artists{ trackList->getArtistsOrderedByRecentFirst({}, std::nullopt, std::nullopt, moreResults) };
ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.front()->getId(), artist1.getId());
const auto releases{ trackList->getReleasesOrderedByRecentFirst({}, std::nullopt, moreResults) };
ASSERT_EQ(releases.size(), 1);
EXPECT_EQ(releases.front()->getId(), release1.getId());
const auto tracks{ trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults) };
ASSERT_EQ(tracks.size(), 1);
EXPECT_EQ(tracks.front()->getId(), track1.getId());
}
{
auto transaction{ session.createSharedTransaction() };
bool moreResults{};
const auto artists{ trackList->getArtistsOrderedByRecentFirst({cluster1.getId()}, std::nullopt, std::nullopt, moreResults) };
ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.front()->getId(), artist1.getId());
const auto releases{ trackList->getReleasesOrderedByRecentFirst({cluster1.getId()}, std::nullopt, moreResults) };
ASSERT_EQ(releases.size(), 1);
EXPECT_EQ(releases.front()->getId(), release1.getId());
const auto tracks{ trackList->getTracksOrderedByRecentFirst({cluster1.getId()}, std::nullopt, moreResults) };
ASSERT_EQ(tracks.size(), 1);
EXPECT_EQ(tracks.front()->getId(), track1.getId());
}
{
auto transaction{ session.createSharedTransaction() };
bool moreResults{};
const auto artists{ trackList->getArtistsOrderedByRecentFirst({cluster3.getId()}, std::nullopt, std::nullopt, moreResults) };
ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.front()->getId(), artist1.getId());
const auto releases{ trackList->getReleasesOrderedByRecentFirst({cluster3.getId()}, std::nullopt, moreResults) };
ASSERT_EQ(releases.size(), 1);
EXPECT_EQ(releases.front()->getId(), release1.getId());
const auto tracks{ trackList->getTracksOrderedByRecentFirst({cluster3.getId()}, std::nullopt, moreResults) };
ASSERT_EQ(tracks.size(), 1);
EXPECT_EQ(tracks.front()->getId(), track1.getId());
}
{
auto transaction{ session.createSharedTransaction() };
bool moreResults{};
const auto artists{ trackList->getArtistsOrderedByRecentFirst({cluster1.getId()}, TrackArtistLinkType::Artist, std::nullopt, moreResults) };
ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.front()->getId(), artist1.getId());
}
{
auto transaction{ session.createSharedTransaction() };
bool moreResults{};
const auto artists{ trackList->getArtistsOrderedByRecentFirst({}, TrackArtistLinkType::Artist, std::nullopt, moreResults) };
ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.front()->getId(), artist1.getId());
}
{
auto transaction{ session.createSharedTransaction() };
bool moreResults{};
EXPECT_TRUE(trackList->getArtistsOrderedByRecentFirst({ cluster2.getId() }, std::nullopt, std::nullopt, moreResults).empty());
EXPECT_TRUE(trackList->getReleasesOrderedByRecentFirst({ cluster2.getId() }, std::nullopt, moreResults).empty());
EXPECT_TRUE(trackList->getTracksOrderedByRecentFirst({ cluster2.getId() }, std::nullopt, moreResults).empty());
EXPECT_TRUE(trackList->getArtistsOrderedByRecentFirst({}, TrackArtistLinkType::ReleaseArtist, std::nullopt, moreResults).empty());
}
{
auto transaction{ session.createUniqueTransaction() };
session.create<TrackListEntry>(track2.get(), trackList.get(), now.addSecs(1));
}
{
auto transaction{ session.createSharedTransaction() };
bool moreResults{};
const auto artists{ trackList->getArtistsOrderedByRecentFirst({}, std::nullopt, std::nullopt, moreResults) };
ASSERT_EQ(artists.size(), 2);
EXPECT_EQ(artists[0]->getId(), artist2.getId());
EXPECT_EQ(artists[1]->getId(), artist1.getId());
const auto releases{ trackList->getReleasesOrderedByRecentFirst({}, std::nullopt, moreResults) };
ASSERT_EQ(releases.size(), 2);
EXPECT_EQ(releases[0]->getId(), release2.getId());
EXPECT_EQ(releases[1]->getId(), release1.getId());
const auto tracks{ trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults) };
ASSERT_EQ(tracks.size(), 2);
EXPECT_EQ(tracks[0]->getId(), track2.getId());
EXPECT_EQ(tracks[1]->getId(), track1.getId());
}
{
auto transaction{ session.createSharedTransaction() };
bool moreResults{};
const auto artists{ trackList->getArtistsOrderedByRecentFirst({cluster3.getId()}, std::nullopt, std::nullopt, moreResults) };
ASSERT_EQ(artists.size(), 2);
EXPECT_EQ(artists[0]->getId(), artist2.getId());
EXPECT_EQ(artists[1]->getId(), artist1.getId());
const auto releases{ trackList->getReleasesOrderedByRecentFirst({cluster3.getId()}, std::nullopt, moreResults) };
ASSERT_EQ(releases.size(), 2);
EXPECT_EQ(releases[0]->getId(), release2.getId());
EXPECT_EQ(releases[1]->getId(), release1.getId());
const auto tracks{ trackList->getTracksOrderedByRecentFirst({cluster3.getId()}, std::nullopt, moreResults) };
ASSERT_EQ(tracks.size(), 2);
EXPECT_EQ(tracks[0]->getId(), track2.getId());
EXPECT_EQ(tracks[1]->getId(), track1.getId());
}
{
auto transaction{ session.createSharedTransaction() };
bool moreResults{};
const auto artists{ trackList->getArtistsOrderedByRecentFirst({cluster1.getId()}, std::nullopt, std::nullopt, moreResults) };
ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.front()->getId(), artist1.getId());
const auto releases{ trackList->getReleasesOrderedByRecentFirst({cluster1.getId()}, std::nullopt, moreResults) };
ASSERT_EQ(releases.size(), 1);
EXPECT_EQ(releases.front()->getId(), release1.getId());
const auto tracks{ trackList->getTracksOrderedByRecentFirst({cluster1.getId()}, std::nullopt, moreResults) };
ASSERT_EQ(tracks.size(), 1);
EXPECT_EQ(tracks.front()->getId(), track1.getId());
}
{
auto transaction{ session.createSharedTransaction() };
bool moreResults{};
const auto artists{ trackList->getArtistsOrderedByRecentFirst({cluster2.getId()}, std::nullopt, std::nullopt, moreResults) };
ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.front()->getId(), artist2.getId());
const auto releases{ trackList->getReleasesOrderedByRecentFirst({cluster2.getId()}, std::nullopt, moreResults) };
ASSERT_EQ(releases.size(), 1);
EXPECT_EQ(releases.front()->getId(), release2.getId());
const auto tracks{ trackList->getTracksOrderedByRecentFirst({cluster2.getId()}, std::nullopt, moreResults) };
ASSERT_EQ(tracks.size(), 1);
EXPECT_EQ(tracks.front()->getId(), track2.getId());
}
{
auto transaction{ session.createUniqueTransaction() };
session.create<TrackListEntry>(track1.get(), trackList.get(), now.addSecs(2));
}
{
auto transaction{ session.createSharedTransaction() };
bool moreResults{};
const auto artists{ trackList->getArtistsOrderedByRecentFirst({}, std::nullopt, std::nullopt, moreResults) };
ASSERT_EQ(artists.size(), 2);
EXPECT_EQ(artists[0]->getId(), artist1.getId());
EXPECT_EQ(artists[1]->getId(), artist2.getId());
const auto releases{ trackList->getReleasesOrderedByRecentFirst({}, std::nullopt, moreResults) };
ASSERT_EQ(releases.size(), 2);
EXPECT_EQ(releases[0]->getId(), release1.getId());
EXPECT_EQ(releases[1]->getId(), release2.getId());
const auto tracks{ trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults) };
ASSERT_EQ(tracks.size(), 2);
EXPECT_EQ(tracks[0]->getId(), track1.getId());
EXPECT_EQ(tracks[1]->getId(), track2.getId());
}
{
auto transaction{ session.createSharedTransaction() };
bool moreResults{};
const auto artists{ trackList->getArtistsOrderedByRecentFirst({cluster3.getId()}, std::nullopt, std::nullopt, moreResults) };
ASSERT_EQ(artists.size(), 2);
EXPECT_EQ(artists[0]->getId(), artist1.getId());
EXPECT_EQ(artists[1]->getId(), artist2.getId());
const auto releases{ trackList->getReleasesOrderedByRecentFirst({cluster3.getId()}, std::nullopt, moreResults) };
ASSERT_EQ(releases.size(), 2);
EXPECT_EQ(releases[0]->getId(), release1.getId());
EXPECT_EQ(releases[1]->getId(), release2.getId());
const auto tracks{ trackList->getTracksOrderedByRecentFirst({cluster3.getId()}, std::nullopt, moreResults) };
ASSERT_EQ(tracks.size(), 2);
EXPECT_EQ(tracks[0]->getId(), track1.getId());
EXPECT_EQ(tracks[1]->getId(), track2.getId());
}
}
TEST_F(DatabaseFixture, MultipleTracksMultipleArtistsMultiClusters) TEST_F(DatabaseFixture, MultipleTracksMultipleArtistsMultiClusters)
{ {
ScopedArtist artist1{ session, "MyArtist1" }; ScopedArtist artist1{ session, "MyArtist1" };
+162 -128
View File
@@ -36,174 +36,208 @@
#include "services/database/User.hpp" #include "services/database/User.hpp"
TmpDatabase::TmpDatabase() TmpDatabase::TmpDatabase()
: _tmpFile {std::tmpnam(nullptr)} : _tmpFile{ std::tmpnam(nullptr) }
, _fileDeleter {_tmpFile} , _fileDeleter{ _tmpFile }
, _db {_tmpFile} , _db{ _tmpFile }
{ {
} }
Database::Db& Database::Db& TmpDatabase::getDb()
TmpDatabase::getDb()
{ {
return _db; return _db;
} }
DatabaseFixture::~DatabaseFixture() DatabaseFixture::~DatabaseFixture()
{ {
testDatabaseEmpty(); testDatabaseEmpty();
} }
void void DatabaseFixture::SetUpTestCase()
DatabaseFixture::SetUpTestCase()
{ {
_tmpDb = std::make_unique<TmpDatabase>(); _tmpDb = std::make_unique<TmpDatabase>();
{ {
Database::Session s {_tmpDb->getDb()}; Database::Session s{ _tmpDb->getDb() };
s.prepareTables(); s.prepareTables();
s.analyze(); s.analyze();
// remove default created entries // remove default created entries
{ {
auto transaction {s.createUniqueTransaction()}; auto transaction{ s.createUniqueTransaction() };
auto clusterTypes {Database::ClusterType::find(s, Database::Range {})};
for (auto clusterTypeId : clusterTypes.results) for (const Database::ClusterTypeId clusterTypeId : Database::ClusterType::find(s).results)
{ {
auto clusterType {Database::ClusterType::find(s, clusterTypeId)}; auto clusterType{ Database::ClusterType::find(s, clusterTypeId) };
clusterType.remove(); clusterType.remove();
} }
} }
} }
} }
void void DatabaseFixture::TearDownTestCase()
DatabaseFixture::TearDownTestCase()
{ {
_tmpDb.reset(); _tmpDb.reset();
} }
void void DatabaseFixture::testDatabaseEmpty()
DatabaseFixture::testDatabaseEmpty()
{ {
using namespace Database; using namespace Database;
auto uniqueTransaction {session.createUniqueTransaction()}; auto uniqueTransaction{ session.createUniqueTransaction() };
EXPECT_EQ(Artist::getCount(session), 0); EXPECT_EQ(Artist::getCount(session), 0);
EXPECT_EQ(Cluster::getCount(session), 0); EXPECT_EQ(Cluster::getCount(session), 0);
EXPECT_EQ(ClusterType::getCount(session), 0); EXPECT_EQ(ClusterType::getCount(session), 0);
EXPECT_EQ(Listen::getCount(session), 0); EXPECT_EQ(Listen::getCount(session), 0);
EXPECT_EQ(Release::getCount(session), 0); EXPECT_EQ(Release::getCount(session), 0);
EXPECT_EQ(StarredArtist::getCount(session), 0); EXPECT_EQ(StarredArtist::getCount(session), 0);
EXPECT_EQ(StarredRelease::getCount(session), 0); EXPECT_EQ(StarredRelease::getCount(session), 0);
EXPECT_EQ(StarredTrack::getCount(session), 0); EXPECT_EQ(StarredTrack::getCount(session), 0);
EXPECT_EQ(Track::getCount(session), 0); EXPECT_EQ(Track::getCount(session), 0);
EXPECT_EQ(TrackBookmark::getCount(session), 0); EXPECT_EQ(TrackBookmark::getCount(session), 0);
EXPECT_EQ(TrackList::getCount(session), 0); EXPECT_EQ(TrackList::getCount(session), 0);
EXPECT_EQ(User::getCount(session), 0); EXPECT_EQ(User::getCount(session), 0);
} }
TEST_F(DatabaseFixture, Common_subRangeEmpty) TEST_F(DatabaseFixture, Common_subRangeEmpty)
{ {
using namespace Database; using namespace Database;
RangeResults<int> results; RangeResults<int> results;
results.range = Range {0, 0}; results.range = Range{ 0, 0 };
results.results = {}; results.results = {};
results.moreResults = false; results.moreResults = false;
{ {
auto subRange {results.getSubRange(Range {0, 0})}; auto subRange{ results.getSubRange(Range {0, 0}) };
EXPECT_FALSE(subRange.moreResults); EXPECT_FALSE(subRange.moreResults);
ASSERT_EQ(subRange.results.size(), 0); ASSERT_EQ(subRange.results.size(), 0);
EXPECT_EQ(subRange.range, Range {}); EXPECT_EQ(subRange.range, Range{});
} }
{ {
auto subRange {results.getSubRange(Range {0, 1})}; auto subRange{ results.getSubRange(Range {0, 1}) };
EXPECT_FALSE(subRange.moreResults); EXPECT_FALSE(subRange.moreResults);
ASSERT_EQ(subRange.results.size(), 0); ASSERT_EQ(subRange.results.size(), 0);
} }
}
TEST_F(DatabaseFixture, Common_subRangeForeach)
{
using namespace Database;
struct TestCase
{
Range range;
std::size_t subRangeSize;
std::vector<Range> expectedSubRanges;
};
TestCase testCases[]
{
{Range{0, 0}, 1, {}},
{Range{1, 0}, 1, {}},
{Range{1, 1}, 1, { Range{ 1,1 } }},
{Range{1, 3}, 1, { Range{ 1,1 }, Range {2,1}, Range{3,1} }},
{Range{0, 100}, 100, { Range{0,100} }},
{Range{0, 50}, 100, { Range{0,50} }},
{Range{100, 200}, 100, { Range{100,100}, Range{200,100} }},
{Range{100, 101}, 100, { Range{100,100}, Range{200,1}}},
{Range{1000, 10}, 100, { Range{1000,10} }},
{Range{1, 100}, 50, { Range{1,50}, Range{51, 50} }},
};
for (const TestCase& test : testCases)
{
std::vector<Range> subRanges;
foreachSubRange(test.range, test.subRangeSize, [&](Range subRange)
{
subRanges.push_back(subRange);
return true;
});
EXPECT_EQ(subRanges, test.expectedSubRanges) << ", test index = " << std::distance(std::cbegin(testCases), &test);
}
} }
TEST_F(DatabaseFixture, Common_IdType) TEST_F(DatabaseFixture, Common_IdType)
{ {
using namespace Database; using namespace Database;
{ {
const IdType id{}; const IdType id{};
EXPECT_FALSE(id.isValid()); EXPECT_FALSE(id.isValid());
} }
{ {
const IdType id{0}; const IdType id{ 0 };
EXPECT_TRUE(id.isValid()); EXPECT_TRUE(id.isValid());
} }
{ {
const IdType id1{0}; const IdType id1{ 0 };
const IdType id2{0}; const IdType id2{ 0 };
EXPECT_EQ(id1, id2); EXPECT_EQ(id1, id2);
} }
{ {
const IdType id1{0}; const IdType id1{ 0 };
const IdType id2{1}; const IdType id2{ 1 };
EXPECT_NE(id1, id2); EXPECT_NE(id1, id2);
EXPECT_LT(id1, id2); EXPECT_LT(id1, id2);
EXPECT_GT(id2, id1); EXPECT_GT(id2, id1);
} }
} }
TEST_F(DatabaseFixture, Common_subRange) TEST_F(DatabaseFixture, Common_subRange)
{ {
using namespace Database; using namespace Database;
RangeResults<int> results; RangeResults<int> results;
results.range = Range {0, 2}; results.range = Range{ 0, 2 };
results.results = {5, 6}; results.results = { 5, 6 };
results.moreResults = false; results.moreResults = false;
{ {
auto subRange {results.getSubRange(Range {0, 1})}; auto subRange{ results.getSubRange(Range {0, 1}) };
EXPECT_TRUE(subRange.moreResults); EXPECT_TRUE(subRange.moreResults);
ASSERT_EQ(subRange.results.size(), 1); ASSERT_EQ(subRange.results.size(), 1);
EXPECT_EQ(subRange.results.front(), 5); EXPECT_EQ(subRange.results.front(), 5);
} }
{ {
auto subRange {results.getSubRange(Range {1, 1})}; auto subRange{ results.getSubRange(Range {1, 1}) };
EXPECT_FALSE(subRange.moreResults); EXPECT_FALSE(subRange.moreResults);
ASSERT_EQ(subRange.results.size(), 1); ASSERT_EQ(subRange.results.size(), 1);
EXPECT_EQ(subRange.results.front(), 6); EXPECT_EQ(subRange.results.front(), 6);
} }
{ {
auto subRange {results.getSubRange(Range {0, 2})}; auto subRange{ results.getSubRange(Range {0, 2}) };
EXPECT_FALSE(subRange.moreResults); EXPECT_FALSE(subRange.moreResults);
ASSERT_EQ(subRange.results.size(), 2); ASSERT_EQ(subRange.results.size(), 2);
EXPECT_EQ(subRange.results.front(), 5); EXPECT_EQ(subRange.results.front(), 5);
EXPECT_EQ(subRange.results.back(), 6); EXPECT_EQ(subRange.results.back(), 6);
} }
{ {
auto subRange {results.getSubRange(Range {})}; auto subRange{ results.getSubRange(Range {}) };
EXPECT_FALSE(subRange.moreResults); EXPECT_FALSE(subRange.moreResults);
ASSERT_EQ(subRange.results.size(), 2); ASSERT_EQ(subRange.results.size(), 2);
EXPECT_EQ(subRange.results.front(), 5); EXPECT_EQ(subRange.results.front(), 5);
EXPECT_EQ(subRange.results.back(), 6); EXPECT_EQ(subRange.results.back(), 6);
EXPECT_EQ(subRange.range, results.range); EXPECT_EQ(subRange.range, results.range);
} }
{ {
auto subRange {results.getSubRange(Range {1, 0})}; auto subRange{ results.getSubRange(Range {1, 0}) };
EXPECT_FALSE(subRange.moreResults); EXPECT_FALSE(subRange.moreResults);
ASSERT_EQ(subRange.results.size(), 1); ASSERT_EQ(subRange.results.size(), 1);
EXPECT_EQ(subRange.results.front(), 6); EXPECT_EQ(subRange.results.front(), 6);
const Range expectedRange {1, 1}; const Range expectedRange{ 1, 1 };
EXPECT_EQ(subRange.range, expectedRange); EXPECT_EQ(subRange.range, expectedRange);
} }
{ {
auto subRange {results.getSubRange(Range {3, 2})}; auto subRange{ results.getSubRange(Range {3, 2}) };
EXPECT_FALSE(subRange.moreResults); EXPECT_FALSE(subRange.moreResults);
ASSERT_EQ(subRange.results.size(), 0); ASSERT_EQ(subRange.results.size(), 0);
const Range expectedRange {2, 0}; const Range expectedRange{ 2, 0 };
EXPECT_EQ(subRange.range, expectedRange); EXPECT_EQ(subRange.range, expectedRange);
} }
} }
@@ -42,8 +42,8 @@ TEST_F(DatabaseFixture, MultiTracksSingleArtistSingleRelease)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
EXPECT_TRUE(Release::findOrphanIds(session, Range{}).results.empty()); EXPECT_TRUE(Release::findOrphanIds(session).results.empty());
EXPECT_TRUE(Artist::findOrphanIds(session, Range{}).results.empty()); EXPECT_TRUE(Artist::findOrphanIds(session).results.empty());
} }
{ {
+52 -4
View File
@@ -40,7 +40,7 @@ TEST_F(DatabaseFixture, Release)
EXPECT_TRUE(Release::exists(session, release.getId())); EXPECT_TRUE(Release::exists(session, release.getId()));
{ {
const auto releases{ Release::findOrphanIds(session, Range {}) }; const auto releases{ Release::findOrphanIds(session) };
ASSERT_EQ(releases.results.size(), 1); ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId()); EXPECT_EQ(releases.results.front(), release.getId());
} }
@@ -57,6 +57,16 @@ TEST_F(DatabaseFixture, Release)
ASSERT_EQ(releases.results.size(), 1); ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front()->getId(), release.getId()); EXPECT_EQ(releases.results.front()->getId(), release.getId());
} }
{
bool visited{};
Release::find(session, Release::FindParameters{}, [&](const Release::pointer& r)
{
visited = true;
EXPECT_EQ(r->getId(), release.getId());
});
EXPECT_TRUE(visited);
}
} }
} }
@@ -76,7 +86,7 @@ TEST_F(DatabaseFixture, Release_singleTrack)
{ {
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
EXPECT_TRUE(Release::findOrphanIds(session, Range{}).results.empty()); EXPECT_TRUE(Release::findOrphanIds(session).results.empty());
const auto tracks{ Track::findIds(session, Track::FindParameters {}.setRelease(release.getId())) }; const auto tracks{ Track::findIds(session, Track::FindParameters {}.setRelease(release.getId())) };
ASSERT_EQ(tracks.results.size(), 1); ASSERT_EQ(tracks.results.size(), 1);
@@ -114,7 +124,7 @@ TEST_F(DatabaseFixture, Release_singleTrack)
const auto tracks{ Track::findIds(session, Track::FindParameters {}.setRelease(release.getId())) }; const auto tracks{ Track::findIds(session, Track::FindParameters {}.setRelease(release.getId())) };
EXPECT_TRUE(tracks.results.empty()); EXPECT_TRUE(tracks.results.empty());
auto releases{ Release::findOrphanIds(session, Range {}) }; auto releases{ Release::findOrphanIds(session) };
ASSERT_EQ(releases.results.size(), 1); ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId()); EXPECT_EQ(releases.results.front(), release.getId());
} }
@@ -537,7 +547,7 @@ TEST_F(DatabaseFixture, Release_releaseType)
} }
} }
TEST_F(DatabaseFixture, ReleaseSortOrder) TEST_F(DatabaseFixture, Release_sortMethod)
{ {
ScopedRelease release1{ session, "MyRelease1" }; ScopedRelease release1{ session, "MyRelease1" };
const Wt::WDate release1Date{ Wt::WDate {2000, 2, 3} }; const Wt::WDate release1Date{ Wt::WDate {2000, 2, 3} };
@@ -606,3 +616,41 @@ TEST_F(DatabaseFixture, ReleaseSortOrder)
} }
} }
TEST_F(DatabaseFixture, Release_meanBitrate)
{
ScopedRelease release1{ session, "MyRelease1" };
ScopedTrack track1{ session, "MyTrack1" };
ScopedTrack track2{ session, "MyTrack2" };
ScopedTrack track3{ session, "MyTrack3" };
auto checkExpectedBitrate = [&](std::size_t bitrate)
{
auto transaction{ session.createSharedTransaction() };
EXPECT_EQ(release1->getMeanBitrate(), bitrate);
};
checkExpectedBitrate(0);
{
auto transaction{ session.createUniqueTransaction() };
track1.get().modify()->setBitrate(128);
track1.get().modify()->setRelease(release1.get());
}
checkExpectedBitrate(128);
{
auto transaction{ session.createUniqueTransaction() };
track2.get().modify()->setBitrate(256);
track2.get().modify()->setRelease(release1.get());
}
checkExpectedBitrate(192);
{
auto transaction{ session.createUniqueTransaction() };
track3.get().modify()->setBitrate(0);
track3.get().modify()->setRelease(release1.get());
}
checkExpectedBitrate(192); // 0 should not be taken into account
}
+16
View File
@@ -31,6 +31,12 @@ TEST_F(DatabaseFixture, Track)
EXPECT_EQ(Track::findIds(session, Track::FindParameters{}).results.size(), 0); EXPECT_EQ(Track::findIds(session, Track::FindParameters{}).results.size(), 0);
EXPECT_EQ(Track::getCount(session), 0); EXPECT_EQ(Track::getCount(session), 0);
EXPECT_FALSE(Track::exists(session, 0)); EXPECT_FALSE(Track::exists(session, 0));
{
bool visited{};
Track::find(session, Track::FindParameters{}, [&](const Track::pointer&) {visited = true;});
EXPECT_FALSE(visited);
}
} }
ScopedTrack track{ session, "MyTrackFile" }; ScopedTrack track{ session, "MyTrackFile" };
@@ -44,6 +50,16 @@ TEST_F(DatabaseFixture, Track)
auto myTrack{ Track::find(session, track.getId()) }; auto myTrack{ Track::find(session, track.getId()) };
ASSERT_TRUE(myTrack); ASSERT_TRUE(myTrack);
EXPECT_EQ(myTrack->getId(), track.getId()); EXPECT_EQ(myTrack->getId(), track.getId());
{
bool visited{};
Track::find(session, Track::FindParameters{}, [&](const Track::pointer& t)
{
visited = true;
EXPECT_EQ(t->getId(), track.getId());
});
EXPECT_TRUE(visited);
}
} }
} }
@@ -49,7 +49,7 @@ TEST_F(DatabaseFixture, TrackBookmark)
EXPECT_EQ(TrackBookmark::getCount(session), 1); EXPECT_EQ(TrackBookmark::getCount(session), 1);
const auto bookmarks {TrackBookmark::find(session, user.getId(), Range {})}; const auto bookmarks {TrackBookmark::find(session, user.getId())};
ASSERT_EQ(bookmarks.results.size(), 1); ASSERT_EQ(bookmarks.results.size(), 1);
EXPECT_EQ(bookmarks.results.front(), bookmark.getId()); EXPECT_EQ(bookmarks.results.front(), bookmark.getId());
} }
@@ -41,7 +41,7 @@ TEST_F(DatabaseFixture, TrackFeatures)
auto transaction {session.createUniqueTransaction()}; auto transaction {session.createUniqueTransaction()};
EXPECT_EQ(TrackFeatures::getCount(session), 1); EXPECT_EQ(TrackFeatures::getCount(session), 1);
auto allTrackFeatures {TrackFeatures::find(session, Range {})}; auto allTrackFeatures {TrackFeatures::find(session)};
ASSERT_EQ(allTrackFeatures.results.size(), 1); ASSERT_EQ(allTrackFeatures.results.size(), 1);
EXPECT_EQ(allTrackFeatures.results.front(), trackFeatures.getId()); EXPECT_EQ(allTrackFeatures.results.front(), trackFeatures.getId());
} }
+140 -437
View File
@@ -25,497 +25,200 @@ using namespace Database;
TEST_F(DatabaseFixture, SingleTrackList) TEST_F(DatabaseFixture, SingleTrackList)
{ {
ScopedUser user {session, "MyUser"}; ScopedUser user{ session, "MyUser" };
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
EXPECT_EQ(TrackList::getCount(session), 0); EXPECT_EQ(TrackList::getCount(session), 0);
} }
ScopedTrackList trackList {session, "MytrackList", TrackListType::Playlist, false, user.lockAndGet()}; ScopedTrackList trackList{ session, "MytrackList", TrackListType::Playlist, false, user.lockAndGet() };
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
EXPECT_EQ(TrackList::getCount(session), 1); EXPECT_EQ(TrackList::getCount(session), 1);
} }
} }
TEST_F(DatabaseFixture, SingleTrackListSingleTrack) TEST_F(DatabaseFixture, SingleTrackListSingleTrack)
{ {
ScopedUser user {session, "MyUser"}; ScopedUser user{ session, "MyUser" };
ScopedTrackList trackList1 {session, "MyTrackList1", TrackListType::Playlist, false, user.lockAndGet()}; ScopedTrackList trackList1{ session, "MyTrackList1", TrackListType::Playlist, false, user.lockAndGet() };
ScopedTrackList trackList2 {session, "MyTrackList2", TrackListType::Playlist, false, user.lockAndGet()}; ScopedTrackList trackList2{ session, "MyTrackList2", TrackListType::Playlist, false, user.lockAndGet() };
ScopedTrack track {session, "MyTrack"}; ScopedTrack track{ session, "MyTrack" };
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
auto tracks {Track::findIds(session, Track::FindParameters {}.setTrackList(trackList1.getId()))}; auto tracks{ Track::findIds(session, Track::FindParameters {}.setTrackList(trackList1.getId())) };
EXPECT_EQ(tracks.results.size(), 0); EXPECT_EQ(tracks.results.size(), 0);
tracks = Track::findIds(session, Track::FindParameters {}.setTrackList(trackList2.getId())); tracks = Track::findIds(session, Track::FindParameters{}.setTrackList(trackList2.getId()));
EXPECT_EQ(tracks.results.size(), 0); EXPECT_EQ(tracks.results.size(), 0);
} }
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction{ session.createUniqueTransaction() };
session.create<TrackListEntry>(track.get(), trackList1.get()); session.create<TrackListEntry>(track.get(), trackList1.get());
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
auto tracks {Track::findIds(session, Track::FindParameters {}.setTrackList(trackList1.getId()))}; auto tracks{ Track::findIds(session, Track::FindParameters {}.setTrackList(trackList1.getId())) };
ASSERT_EQ(tracks.results.size(), 1); ASSERT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results.front(), track.getId()); EXPECT_EQ(tracks.results.front(), track.getId());
tracks = Track::findIds(session, Track::FindParameters {}.setTrackList(trackList2.getId())); tracks = Track::findIds(session, Track::FindParameters{}.setTrackList(trackList2.getId()));
EXPECT_EQ(tracks.results.size(), 0); EXPECT_EQ(tracks.results.size(), 0);
} }
} }
TEST_F(DatabaseFixture, TrackList_SortMethod) TEST_F(DatabaseFixture, TrackList_SortMethod)
{ {
ScopedUser user {session, "MyUser"}; ScopedUser user{ session, "MyUser" };
ScopedTrackList trackList2 {session, "MyTrackList2", TrackListType::Playlist, false, user.lockAndGet()}; ScopedTrackList trackList2{ session, "MyTrackList2", TrackListType::Playlist, false, user.lockAndGet() };
ScopedTrackList trackList1 {session, "MyTrackList1", TrackListType::Playlist, false, user.lockAndGet()}; ScopedTrackList trackList1{ session, "MyTrackList1", TrackListType::Playlist, false, user.lockAndGet() };
ScopedTrack track {session, "MyTrack"}; ScopedTrack track{ session, "MyTrack" };
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
const auto trackLists {TrackList::find(session, TrackList::FindParameters {}.setSortMethod(TrackListSortMethod::Name))}; const auto trackLists{ TrackList::find(session, TrackList::FindParameters {}.setSortMethod(TrackListSortMethod::Name)) };
ASSERT_EQ(trackLists.results.size(), 2); ASSERT_EQ(trackLists.results.size(), 2);
EXPECT_EQ(trackLists.results[0], trackList1.getId()); EXPECT_EQ(trackLists.results[0], trackList1.getId());
EXPECT_EQ(trackLists.results[1], trackList2.getId()); EXPECT_EQ(trackLists.results[1], trackList2.getId());
} }
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction{ session.createUniqueTransaction() };
trackList1.get().modify()->setLastModifiedDateTime(Wt::WDateTime {Wt::WDate {1900,1,1}}); trackList1.get().modify()->setLastModifiedDateTime(Wt::WDateTime{ Wt::WDate {1900,1,1} });
trackList2.get().modify()->setLastModifiedDateTime(Wt::WDateTime {Wt::WDate {1900,1,2}}); trackList2.get().modify()->setLastModifiedDateTime(Wt::WDateTime{ Wt::WDate {1900,1,2} });
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
const auto trackLists {TrackList::find(session, TrackList::FindParameters {}.setSortMethod(TrackListSortMethod::LastModifiedDesc))}; const auto trackLists{ TrackList::find(session, TrackList::FindParameters {}.setSortMethod(TrackListSortMethod::LastModifiedDesc)) };
ASSERT_EQ(trackLists.results.size(), 2); ASSERT_EQ(trackLists.results.size(), 2);
EXPECT_EQ(trackLists.results[0], trackList2.getId()); EXPECT_EQ(trackLists.results[0], trackList2.getId());
EXPECT_EQ(trackLists.results[1], trackList1.getId()); EXPECT_EQ(trackLists.results[1], trackList1.getId());
} }
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction{ session.createUniqueTransaction() };
trackList1.get().modify()->setLastModifiedDateTime(Wt::WDateTime {Wt::WDate {1900,1,2}}); trackList1.get().modify()->setLastModifiedDateTime(Wt::WDateTime{ Wt::WDate {1900,1,2} });
trackList2.get().modify()->setLastModifiedDateTime(Wt::WDateTime {Wt::WDate {1900,1,1}}); trackList2.get().modify()->setLastModifiedDateTime(Wt::WDateTime{ Wt::WDate {1900,1,1} });
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
const auto trackLists {TrackList::find(session, TrackList::FindParameters {}.setSortMethod(TrackListSortMethod::LastModifiedDesc))}; const auto trackLists{ TrackList::find(session, TrackList::FindParameters {}.setSortMethod(TrackListSortMethod::LastModifiedDesc)) };
ASSERT_EQ(trackLists.results.size(), 2); ASSERT_EQ(trackLists.results.size(), 2);
EXPECT_EQ(trackLists.results[0], trackList1.getId()); EXPECT_EQ(trackLists.results[0], trackList1.getId());
EXPECT_EQ(trackLists.results[1], trackList2.getId()); EXPECT_EQ(trackLists.results[1], trackList2.getId());
} }
} }
TEST_F(DatabaseFixture, SingleTrackListMultipleTrack) TEST_F(DatabaseFixture, SingleTrackListMultipleTrack)
{ {
ScopedUser user {session, "MyUser"}; ScopedUser user{ session, "MyUser" };
ScopedTrackList trackList {session, "MytrackList", TrackListType::Playlist, false, user.lockAndGet()}; ScopedTrackList trackList{ session, "MytrackList", TrackListType::Playlist, false, user.lockAndGet() };
std::list<ScopedTrack> tracks; std::list<ScopedTrack> tracks;
for (std::size_t i {}; i < 10; ++i) for (std::size_t i{}; i < 10; ++i)
{ {
tracks.emplace_back(session, "MyTrack" + std::to_string(i)); tracks.emplace_back(session, "MyTrack" + std::to_string(i));
auto transaction {session.createUniqueTransaction()}; auto transaction{ session.createUniqueTransaction() };
session.create<TrackListEntry>(tracks.back().get(), trackList.get()); session.create<TrackListEntry>(tracks.back().get(), trackList.get());
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
ASSERT_EQ(trackList->getCount(), tracks.size()); ASSERT_EQ(trackList->getCount(), tracks.size());
const auto trackIds {trackList->getTrackIds()}; const auto trackIds{ trackList->getTrackIds() };
ASSERT_EQ(trackIds.size(), tracks.size()); ASSERT_EQ(trackIds.size(), tracks.size());
// Same order // Same order
std::size_t i {}; std::size_t i{};
for (const ScopedTrack& track : tracks) for (const ScopedTrack& track : tracks)
EXPECT_EQ(track.getId(), trackIds[i++]); EXPECT_EQ(track.getId(), trackIds[i++]);
} }
{
auto transaction {session.createSharedTransaction()};
bool moreResults {};
const auto reverseTracks {trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults)};
ASSERT_EQ(reverseTracks.size(), tracks.size());
// Reverse order
ASSERT_TRUE(tracks.size() > 0);
std::size_t i {tracks.size() - 1};
for (const ScopedTrack& track : tracks)
EXPECT_EQ(track.getId(), reverseTracks[i--]->getId());
}
}
TEST_F(DatabaseFixture, SingleTrackListMultipleTrackDateTime)
{
ScopedUser user {session, "MyUser"};
ScopedTrackList trackList {session, "MytrackList", TrackListType::Playlist, false, user.lockAndGet()};
ScopedTrack track1 {session, "MyTrack1"};
ScopedTrack track2 {session, "MyTrack2"};
ScopedTrack track3 {session, "MyTrack3"};
{
Wt::WDateTime now {Wt::WDateTime::currentDateTime()};
auto transaction {session.createUniqueTransaction()};
session.create<TrackListEntry>(track1.get(), trackList.get(), now);
session.create<TrackListEntry>(track2.get(), trackList.get(), now.addSecs(-1));
session.create<TrackListEntry>(track3.get(), trackList.get(), now.addSecs(1));
}
{
auto transaction {session.createSharedTransaction()};
bool moreResults;
const auto tracks {trackList.get()->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults)};
ASSERT_EQ(tracks.size(), 3);
EXPECT_EQ(tracks.front()->getId(), track3.getId());
EXPECT_EQ(tracks.back()->getId(), track2.getId());
}
}
TEST_F(DatabaseFixture, SingleTrackListMultipleTrackRecentlyPlayed)
{
ScopedUser user {session, "MyUser"};
ScopedTrackList trackList {session, "MyTrackList", TrackListType::Playlist, false, user.lockAndGet()};
ScopedTrack track1 {session, "MyTrack1"};
ScopedTrack track2 {session, "MyTrack1"};
ScopedArtist artist1 {session, "MyArtist1"};
ScopedArtist artist2 {session, "MyArtist2"};
ScopedRelease release1 {session, "MyRelease1"};
ScopedRelease release2 {session, "MyRelease2"};
const Wt::WDateTime now {Wt::WDateTime::currentDateTime()};
{
auto transaction {session.createUniqueTransaction()};
track1.get().modify()->setRelease(release1.get());
track2.get().modify()->setRelease(release2.get());
TrackArtistLink::create(session, track1.get(), artist1.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, track2.get(), artist2.get(), TrackArtistLinkType::Artist);
}
{
auto transaction {session.createSharedTransaction()};
bool moreResults {};
EXPECT_TRUE(trackList->getArtistsOrderedByRecentFirst({}, std::nullopt, std::nullopt, moreResults).empty());
EXPECT_TRUE(trackList->getReleasesOrderedByRecentFirst({}, std::nullopt, moreResults).empty());
EXPECT_TRUE(trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults).empty());
}
{
auto transaction {session.createUniqueTransaction()};
session.create<TrackListEntry>(track1.get(), trackList.get(), now);
}
{
auto transaction {session.createSharedTransaction()};
bool moreResults {};
const auto artists {trackList->getArtistsOrderedByRecentFirst({}, std::nullopt, std::nullopt, moreResults)};
ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.front()->getId(), artist1.getId());
const auto releases {trackList->getReleasesOrderedByRecentFirst({}, std::nullopt, moreResults)};
ASSERT_EQ(releases.size(), 1);
EXPECT_EQ(releases.front()->getId(), release1.getId());
const auto tracks {trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults)};
EXPECT_EQ(tracks.size(), 1);
}
{
auto transaction {session.createUniqueTransaction()};
session.create<TrackListEntry>(track2.get(), trackList.get(), now.addSecs(1));
}
{
auto transaction {session.createSharedTransaction()};
bool moreResults {};
const auto artists {trackList->getArtistsOrderedByRecentFirst({}, std::nullopt, std::nullopt, moreResults)};
ASSERT_EQ(artists.size(), 2);
EXPECT_EQ(artists[0]->getId(), artist2.getId());
EXPECT_EQ(artists[1]->getId(), artist1.getId());
const auto releases {trackList->getReleasesOrderedByRecentFirst({}, std::nullopt, moreResults)};
ASSERT_EQ(releases.size(), 2);
EXPECT_EQ(releases[0]->getId(), release2.getId());
EXPECT_EQ(releases[1]->getId(), release1.getId());
const auto tracks {trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults)};
ASSERT_EQ(tracks.size(), 2);
EXPECT_EQ(tracks[0]->getId(), track2.getId());
EXPECT_EQ(tracks[1]->getId(), track1.getId());
}
{
auto transaction {session.createUniqueTransaction()};
session.create<TrackListEntry>(track1.get(), trackList.get(), now.addSecs(2));
}
{
auto transaction {session.createSharedTransaction()};
bool moreResults {};
const auto artists {trackList->getArtistsOrderedByRecentFirst({}, std::nullopt, std::nullopt, moreResults)};
ASSERT_EQ(artists.size(), 2);
EXPECT_EQ(artists[0]->getId(), artist1.getId());
EXPECT_EQ(artists[1]->getId(), artist2.getId());
const auto releases {trackList->getReleasesOrderedByRecentFirst({}, std::nullopt, moreResults)};
ASSERT_EQ(releases.size(), 2);
EXPECT_EQ(releases[0]->getId(), release1.getId());
EXPECT_EQ(releases[1]->getId(), release2.getId());
const auto tracks {trackList->getTracksOrderedByRecentFirst({}, std::nullopt, moreResults)};
ASSERT_EQ(tracks.size(), 2);
EXPECT_EQ(tracks[0]->getId(), track1.getId());
EXPECT_EQ(tracks[1]->getId(), track2.getId());
}
}
TEST_F(DatabaseFixture, SingleTrackList_getArtists)
{
ScopedUser user {session, "MyUser"};
ScopedTrackList trackList {session, "MytrackList", TrackListType::Playlist, false, user.lockAndGet()};
ScopedTrack track1 {session, "MyTrack1"};
ScopedTrack track2 {session, "MyTrack2"};
ScopedRelease release {session, "MyRelease"};
ScopedArtist artist1 {session, "MyArtist1"};
ScopedArtist artist2 {session, "MyArtist2"};
{
auto transaction {session.createSharedTransaction()};
EXPECT_EQ(trackList->getCount(), 0);
bool moreResults {};
const auto artists {trackList->getArtists({} /*clusters*/, std::nullopt /* linkType */, ArtistSortMethod::ByName, std::nullopt /* range */, moreResults)};
ASSERT_TRUE(artists.empty());
}
{
auto transaction {session.createUniqueTransaction()};
TrackArtistLink::create(session, track1.get(), artist1.get(), TrackArtistLinkType::Artist);
session.create<TrackListEntry>(track1.get(), trackList.get());
}
{
auto transaction {session.createSharedTransaction()};
EXPECT_EQ(trackList->getCount(), 1);
bool moreResults {};
const auto artists {trackList->getArtists({} /*clusters*/, std::nullopt /* linkType */, ArtistSortMethod::ByName, std::nullopt /* range */, moreResults)};
ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.front()->getId(), artist1.getId());
}
{
auto transaction {session.createSharedTransaction()};
bool moreResults {};
const auto artists {trackList->getArtists({} /*clusters*/, TrackArtistLinkType::ReleaseArtist, ArtistSortMethod::ByName, std::nullopt /* range */, moreResults)};
EXPECT_TRUE(artists.empty());
}
{
auto transaction {session.createSharedTransaction()};
bool moreResults {};
const auto artists {trackList->getArtists({} /*clusters*/, TrackArtistLinkType::Artist, ArtistSortMethod::ByName, std::nullopt /* range */, moreResults)};
ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.front()->getId(), artist1.getId());
}
{
auto transaction {session.createUniqueTransaction()};
TrackArtistLink::create(session, track2.get(), artist2.get(), TrackArtistLinkType::Artist);
session.create<TrackListEntry>(track2.get(), trackList.get());
artist1.get().modify()->setSortName("ZZZ");
artist2.get().modify()->setSortName("AAA");
}
{
auto transaction {session.createSharedTransaction()};
bool moreResults {};
const auto artists {trackList->getArtists({} /*clusters*/, TrackArtistLinkType::Artist, ArtistSortMethod::ByName, std::nullopt /* range */, moreResults)};
ASSERT_EQ(artists.size(), 2);
EXPECT_EQ(artists.front()->getId(), artist1.getId());
EXPECT_EQ(artists.back()->getId(), artist2.getId());
}
{
auto transaction {session.createSharedTransaction()};
bool moreResults {};
const auto artists {trackList->getArtists({} /*clusters*/, TrackArtistLinkType::Artist, ArtistSortMethod::BySortName, std::nullopt /* range */, moreResults)};
ASSERT_EQ(artists.size(), 2);
EXPECT_EQ(artists.front()->getId(), artist2.getId());
EXPECT_EQ(artists.back()->getId(), artist1.getId());
}
}
TEST_F(DatabaseFixture, SingleTrackList_getReleases)
{
ScopedUser user {session, "MyUser"};
ScopedTrackList trackList {session, "MytrackList", TrackListType::Playlist, false, user.lockAndGet()};
ScopedTrack track {session, "MyTrack"};
ScopedRelease release {session, "MyRelease"};
{
auto transaction {session.createSharedTransaction()};
EXPECT_EQ(trackList->getCount(), 0);
bool moreResults {};
const auto releases {trackList->getReleases({} /*clusters*/, std::nullopt /* range */, moreResults)};
ASSERT_TRUE(releases.empty());
}
{
auto transaction {session.createUniqueTransaction()};
track.get().modify()->setRelease(release.get());
session.create<TrackListEntry>(track.get(), trackList.get());
}
{
auto transaction {session.createSharedTransaction()};
ASSERT_EQ(trackList->getCount(), 1);
bool moreResults {};
const auto releases {trackList->getReleases({} /*clusters*/, std::nullopt /* range */, moreResults)};
ASSERT_EQ(releases.size(), 1);
ASSERT_EQ(releases.front()->getId(), release->getId());
}
}
TEST_F(DatabaseFixture, SingleTrackList_getTracks)
{
ScopedUser user {session, "MyUser"};
ScopedTrackList trackList {session, "MytrackList", TrackListType::Playlist, false, user.lockAndGet()};
ScopedTrack track1 {session, "MyTrack1"};
ScopedTrack track2 {session, "MyTrack1"};
{
auto transaction {session.createSharedTransaction()};
EXPECT_EQ(trackList->getCount(), 0);
bool moreResults {};
const auto tracks {trackList->getTracks({} /*clusters*/, std::nullopt /* range */, moreResults)};
ASSERT_TRUE(tracks.empty());
}
{
auto transaction {session.createUniqueTransaction()};
session.create<TrackListEntry>(track1.get(), trackList.get());
session.create<TrackListEntry>(track2.get(), trackList.get());
session.create<TrackListEntry>(track1.get(), trackList.get());
}
{
auto transaction {session.createSharedTransaction()};
ASSERT_EQ(trackList->getCount(), 3);
bool moreResults {};
const auto tracks {trackList->getTracks({} /*clusters*/, std::nullopt /* range */, moreResults)};
ASSERT_EQ(tracks.size(), 2);
ASSERT_EQ(tracks[0]->getId(), track1->getId());
ASSERT_EQ(tracks[1]->getId(), track2->getId());
}
} }
TEST_F(DatabaseFixture, SingleTrackListSingleTrackWithCluster) TEST_F(DatabaseFixture, SingleTrackListSingleTrackWithCluster)
{ {
ScopedUser user {session, "MyUser"}; ScopedUser user{ session, "MyUser" };
ScopedTrackList trackList1 {session, "MyTrackList1", TrackListType::Playlist, false, user.lockAndGet()}; ScopedTrackList trackList1{ session, "MyTrackList1", TrackListType::Playlist, false, user.lockAndGet() };
ScopedTrackList trackList2 {session, "MyTrackList2", TrackListType::Playlist, false, user.lockAndGet()}; ScopedTrackList trackList2{ session, "MyTrackList2", TrackListType::Playlist, false, user.lockAndGet() };
ScopedClusterType clusterType {session, "MyClusterType"}; ScopedClusterType clusterType{ session, "MyClusterType" };
ScopedCluster cluster {session, clusterType.lockAndGet(), "MyCluster"}; ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
ScopedTrack track {session, "MyTrack"}; ScopedTrack track{ session, "MyTrack" };
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
auto trackLists {TrackList::find(session, TrackList::FindParameters {}.setClusters({cluster.getId()}))}; auto trackLists{ TrackList::find(session, TrackList::FindParameters {}.setClusters({cluster.getId()})) };
EXPECT_EQ(trackLists.results.size(), 0); EXPECT_EQ(trackLists.results.size(), 0);
} }
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction{ session.createUniqueTransaction() };
session.create<TrackListEntry>(track.get(), trackList1.get()); session.create<TrackListEntry>(track.get(), trackList1.get());
cluster.get().modify()->addTrack(track.get()); cluster.get().modify()->addTrack(track.get());
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
auto trackLists {TrackList::find(session, TrackList::FindParameters {}.setClusters({cluster.getId()}))}; auto trackLists{ TrackList::find(session, TrackList::FindParameters {}.setClusters({cluster.getId()})) };
ASSERT_EQ(trackLists.results.size(), 1); ASSERT_EQ(trackLists.results.size(), 1);
EXPECT_EQ(trackLists.results.front(), trackList1.getId()); EXPECT_EQ(trackLists.results.front(), trackList1.getId());
} }
} }
TEST_F(DatabaseFixture, SingleTrackList_getEntries) TEST_F(DatabaseFixture, SingleTrackList_getEntries)
{ {
ScopedUser user {session, "MyUser"}; ScopedUser user{ session, "MyUser" };
ScopedTrackList trackList {session, "MyTrackList", TrackListType::Playlist, false, user.lockAndGet()}; ScopedTrackList trackList{ session, "MyTrackList", TrackListType::Playlist, false, user.lockAndGet() };
ScopedTrack track1 {session, "MyTrack"}; ScopedTrack track1{ session, "MyTrack" };
ScopedTrack track2 {session, "MyTrack"}; ScopedTrack track2{ session, "MyTrack" };
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction{ session.createUniqueTransaction() };
session.create<TrackListEntry>(track1.get(), trackList.get()); session.create<TrackListEntry>(track1.get(), trackList.get());
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
auto entries {trackList.get()->getEntries()}; auto entries{ trackList.get()->getEntries() };
ASSERT_EQ(entries.size(), 1); ASSERT_EQ(entries.size(), 1);
EXPECT_EQ(entries.front()->getTrack()->getId(), track1.getId()); EXPECT_EQ(entries.front()->getTrack()->getId(), track1.getId());
} }
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction{ session.createUniqueTransaction() };
session.create<TrackListEntry>(track2.get(), trackList.get()); session.create<TrackListEntry>(track2.get(), trackList.get());
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
auto entries {trackList.get()->getEntries()}; auto entries{ trackList.get()->getEntries() };
ASSERT_EQ(entries.size(), 2); ASSERT_EQ(entries.size(), 2);
EXPECT_EQ(entries[0]->getTrack()->getId(), track1.getId()); EXPECT_EQ(entries[0]->getTrack()->getId(), track1.getId());
EXPECT_EQ(entries[1]->getTrack()->getId(), track2.getId()); EXPECT_EQ(entries[1]->getTrack()->getId(), track2.getId());
} }
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
auto entries {trackList.get()->getEntries(Range {1, 1})}; auto entries{ trackList.get()->getEntries(Range {1, 1}) };
ASSERT_EQ(entries.size(), 1); ASSERT_EQ(entries.size(), 1);
EXPECT_EQ(entries[0]->getTrack()->getId(), track2.getId()); EXPECT_EQ(entries[0]->getTrack()->getId(), track2.getId());
} }
} }
@@ -87,23 +87,23 @@ namespace Feedback
return getStarredDateTime<Artist, ArtistId, StarredArtist>(userId, artistId); return getStarredDateTime<Artist, ArtistId, StarredArtist>(userId, artistId);
} }
FeedbackService::ArtistContainer FeedbackService::getStarredArtists(UserId userId, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, ArtistSortMethod sortMethod, Range range) FeedbackService::ArtistContainer FeedbackService::findStarredArtists(const ArtistFindParameters& params)
{ {
auto backend{ getUserFeedbackBackend(userId) }; auto backend{ getUserFeedbackBackend(params.user) };
if (!backend) if (!backend)
return {}; return {};
Artist::FindParameters params; Artist::FindParameters searchParams;
params.setStarringUser(userId, *backend); searchParams.setStarringUser(params.user, *backend);
params.setClusters(clusterIds); searchParams.setClusters(params.clusters);
params.setLinkType(linkType); searchParams.setLinkType(params.linkType);
params.setSortMethod(sortMethod); searchParams.setSortMethod(params.sortMethod);
params.setRange(range); searchParams.setRange(params.range);
Session& session{ _db.getTLSSession() }; Session& session{ _db.getTLSSession() };
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
return Artist::findIds(session, params); return Artist::findIds(session, searchParams);
} }
void FeedbackService::star(UserId userId, ReleaseId releaseId) void FeedbackService::star(UserId userId, ReleaseId releaseId)
@@ -126,22 +126,22 @@ namespace Feedback
return getStarredDateTime<Release, ReleaseId, StarredRelease>(userId, releaseId); return getStarredDateTime<Release, ReleaseId, StarredRelease>(userId, releaseId);
} }
FeedbackService::ReleaseContainer FeedbackService::getStarredReleases(UserId userId, const std::vector<ClusterId>& clusterIds, Range range) FeedbackService::ReleaseContainer FeedbackService::findStarredReleases(const FindParameters& params)
{ {
auto backend{ getUserFeedbackBackend(userId) }; auto backend{ getUserFeedbackBackend(params.user) };
if (!backend) if (!backend)
return {}; return {};
Release::FindParameters params; Release::FindParameters searchParams;
params.setStarringUser(userId, *backend); searchParams.setStarringUser(params.user, *backend);
params.setClusters(clusterIds); searchParams.setClusters(params.clusters);
params.setSortMethod(ReleaseSortMethod::StarredDateDesc); searchParams.setSortMethod(ReleaseSortMethod::StarredDateDesc);
params.setRange(range); searchParams.setRange(params.range);
Session& session{ _db.getTLSSession() }; Session& session{ _db.getTLSSession() };
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
return Release::findIds(session, params); return Release::findIds(session, searchParams);
} }
void FeedbackService::star(UserId userId, TrackId trackId) void FeedbackService::star(UserId userId, TrackId trackId)
@@ -164,22 +164,22 @@ namespace Feedback
return getStarredDateTime<Track, TrackId, StarredTrack>(userId, trackId); return getStarredDateTime<Track, TrackId, StarredTrack>(userId, trackId);
} }
FeedbackService::TrackContainer FeedbackService::getStarredTracks(UserId userId, const std::vector<ClusterId>& clusterIds, Range range) FeedbackService::TrackContainer FeedbackService::findStarredTracks(const FindParameters& params)
{ {
auto backend{ getUserFeedbackBackend(userId) }; auto backend{ getUserFeedbackBackend(params.user) };
if (!backend) if (!backend)
return {}; return {};
Track::FindParameters params; Track::FindParameters searchParams;
params.setStarringUser(userId, *backend); searchParams.setStarringUser(params.user, *backend);
params.setClusters(clusterIds); searchParams.setClusters(params.clusters);
params.setSortMethod(TrackSortMethod::StarredDateDesc); searchParams.setSortMethod(TrackSortMethod::StarredDateDesc);
params.setRange(range); searchParams.setRange(params.range);
Session& session{ _db.getTLSSession() }; Session& session{ _db.getTLSSession() };
auto transaction{ session.createSharedTransaction() }; auto transaction{ session.createSharedTransaction() };
return Track::findIds(session, params); return Track::findIds(session, searchParams);
} }
} // ns Feedback } // ns Feedback
@@ -46,19 +46,19 @@ namespace Feedback
void unstar(Database::UserId userId, Database::ArtistId artistId) override; void unstar(Database::UserId userId, Database::ArtistId artistId) override;
bool isStarred(Database::UserId userId, Database::ArtistId artistId) override; bool isStarred(Database::UserId userId, Database::ArtistId artistId) override;
Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::ArtistId artistId) override; Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::ArtistId artistId) override;
ArtistContainer getStarredArtists(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, std::optional<Database::TrackArtistLinkType> linkType, Database::ArtistSortMethod sortMethod, Database::Range range) override; ArtistContainer findStarredArtists(const ArtistFindParameters& params) override;
void star(Database::UserId userId, Database::ReleaseId releaseId) override; void star(Database::UserId userId, Database::ReleaseId releaseId) override;
void unstar(Database::UserId userId, Database::ReleaseId releaseId) override; void unstar(Database::UserId userId, Database::ReleaseId releaseId) override;
bool isStarred(Database::UserId userId, Database::ReleaseId releasedId) override; bool isStarred(Database::UserId userId, Database::ReleaseId releasedId) override;
Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::ReleaseId releasedId) override; Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::ReleaseId releasedId) override;
ReleaseContainer getStarredReleases(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) override; ReleaseContainer findStarredReleases(const FindParameters& params) override;
void star(Database::UserId userId, Database::TrackId trackId) override; void star(Database::UserId userId, Database::TrackId trackId) override;
void unstar(Database::UserId userId, Database::TrackId trackId) override; void unstar(Database::UserId userId, Database::TrackId trackId) override;
bool isStarred(Database::UserId userId, Database::TrackId trackId) override; bool isStarred(Database::UserId userId, Database::TrackId trackId) override;
Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::TrackId trackId) override; Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::TrackId trackId) override;
TrackContainer getStarredTracks(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) override; TrackContainer findStarredTracks(const FindParameters& params) override;
std::optional<Database::FeedbackBackend> getUserFeedbackBackend(Database::UserId userId); std::optional<Database::FeedbackBackend> getUserFeedbackBackend(Database::UserId userId);
@@ -48,23 +48,46 @@ namespace Feedback
using ReleaseContainer = Database::RangeResults<Database::ReleaseId>; using ReleaseContainer = Database::RangeResults<Database::ReleaseId>;
using TrackContainer = Database::RangeResults<Database::TrackId>; using TrackContainer = Database::RangeResults<Database::TrackId>;
virtual void star(Database::UserId userId, Database::ArtistId artistId) = 0; struct FindParameters
virtual void unstar(Database::UserId userId, Database::ArtistId artistId) = 0; {
virtual bool isStarred(Database::UserId userId, Database::ArtistId artistId) = 0; Database::UserId user;
virtual Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::ArtistId artistId) = 0; std::vector<Database::ClusterId> clusters; // if non empty, at least one artist that belongs to these clusters
virtual ArtistContainer getStarredArtists(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, std::optional<Database::TrackArtistLinkType> linkType, Database::ArtistSortMethod sortMethod, Database::Range range) = 0; std::optional<Database::Range> range;
FindParameters& setUser(const Database::UserId _user) { user = _user; return *this; }
FindParameters& setClusters(const std::vector<Database::ClusterId>& _clusters) { clusters = _clusters; return *this; }
FindParameters& setRange(std::optional<Database::Range> _range) { range = _range; return *this; }
};
// Artists
struct ArtistFindParameters : public FindParameters
{
std::optional<Database::TrackArtistLinkType> linkType; // if set, only artists that have produced at least one track with this link type
Database::ArtistSortMethod sortMethod{ Database::ArtistSortMethod::None };
ArtistFindParameters& setLinkType(std::optional<Database::TrackArtistLinkType> _linkType) { linkType = _linkType; return *this; }
ArtistFindParameters& setSortMethod(Database::ArtistSortMethod _sortMethod) { sortMethod = _sortMethod; return *this; }
};
virtual void star(Database::UserId userId, Database::ArtistId artistId) = 0;
virtual void unstar(Database::UserId userId, Database::ArtistId artistId) = 0;
virtual bool isStarred(Database::UserId userId, Database::ArtistId artistId) = 0;
virtual Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::ArtistId artistId) = 0;
virtual ArtistContainer findStarredArtists(const ArtistFindParameters& params) = 0;
// Releases
virtual void star(Database::UserId userId, Database::ReleaseId releaseId) = 0; virtual void star(Database::UserId userId, Database::ReleaseId releaseId) = 0;
virtual void unstar(Database::UserId userId, Database::ReleaseId releaseId) = 0; virtual void unstar(Database::UserId userId, Database::ReleaseId releaseId) = 0;
virtual bool isStarred(Database::UserId userId, Database::ReleaseId artistId) = 0; virtual bool isStarred(Database::UserId userId, Database::ReleaseId artistId) = 0;
virtual Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::ReleaseId artistId) = 0; virtual Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::ReleaseId artistId) = 0;
virtual ReleaseContainer getStarredReleases(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) = 0; virtual ReleaseContainer findStarredReleases(const FindParameters& params) = 0;
// Tracks
virtual void star(Database::UserId userId, Database::TrackId trackId) = 0; virtual void star(Database::UserId userId, Database::TrackId trackId) = 0;
virtual void unstar(Database::UserId userId, Database::TrackId trackId) = 0; virtual void unstar(Database::UserId userId, Database::TrackId trackId) = 0;
virtual bool isStarred(Database::UserId userId, Database::TrackId artistId) = 0; virtual bool isStarred(Database::UserId userId, Database::TrackId artistId) = 0;
virtual Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::TrackId artistId) = 0; virtual Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::TrackId artistId) = 0;
virtual TrackContainer getStarredTracks(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) = 0; virtual TrackContainer findStarredTracks(const FindParameters& params) = 0;
}; };
std::unique_ptr<IFeedbackService> createFeedbackService(boost::asio::io_service& ioService, Database::Db& db); std::unique_ptr<IFeedbackService> createFeedbackService(boost::asio::io_service& ioService, Database::Db& db);
@@ -33,240 +33,91 @@
namespace Recommendation namespace Recommendation
{ {
namespace
{
Database::ScanSettings::SimilarityEngineType getSimilarityEngineType(Database::Session& session)
{
auto transaction{ session.createSharedTransaction() };
static return Database::ScanSettings::get(session)->getSimilarityEngineType();
std::string_view }
engineTypeToString(EngineType engineType) }
{
switch (engineType)
{
case EngineType::Clusters: return "clusters";
case EngineType::Features: return "features";
}
throw LmsException {"Internal error"}; std::unique_ptr<IRecommendationService> createRecommendationService(Database::Db& db)
} {
return std::make_unique<RecommendationService>(db);
}
std::unique_ptr<IRecommendationService> RecommendationService::RecommendationService(Database::Db& db)
createRecommendationService(Database::Db& db) : _db{ db }
{ {
return std::make_unique<RecommendationService>(db); load();
} }
RecommendationService::RecommendationService(Database::Db& db) TrackContainer RecommendationService::findSimilarTracks(Database::TrackListId trackListId, std::size_t maxCount) const
: _db {db} {
{ TrackContainer res;
}
TrackContainer if (!_engine)
RecommendationService::findSimilarTracks(Database::TrackListId trackListId, std::size_t maxCount) const return res;
{
TrackContainer res;
std::shared_lock lock {_enginesMutex}; return _engine->findSimilarTracksFromTrackList(trackListId, maxCount);
for (const auto& engineType : _enginePriorities) }
{
auto itEngine {_engines.find(engineType)};
if (itEngine == std::cend(_engines))
continue;
res = itEngine->second->findSimilarTracksFromTrackList(trackListId, maxCount); TrackContainer RecommendationService::findSimilarTracks(const std::vector<Database::TrackId>& trackIds, std::size_t maxCount) const
if (!res.empty()) {
break; TrackContainer res;
}
return res; if (!_engine)
} return res;
TrackContainer return _engine->findSimilarTracks(trackIds, maxCount);
RecommendationService::findSimilarTracks(const std::vector<Database::TrackId>& trackIds, std::size_t maxCount) const }
{
TrackContainer res;
std::shared_lock lock {_enginesMutex}; ReleaseContainer RecommendationService::getSimilarReleases(Database::ReleaseId releaseId, std::size_t maxCount) const
for (EngineType engineType : _enginePriorities) {
{ ReleaseContainer res;
auto itEngine {_engines.find(engineType)};
if (itEngine == std::cend(_engines))
continue;
LMS_LOG(RECOMMENDATION, DEBUG) << "Trying engine '" << engineTypeToString(engineType) << "' to get similar tracks"; if (!_engine)
return res;
const IEngine& engine {*itEngine->second}; return _engine->getSimilarReleases(releaseId, maxCount);;
res = engine.findSimilarTracks(trackIds, maxCount); }
if (!res.empty())
{
LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar tracks using engine '" << engineTypeToString(engineType) << "'";
break;
}
}
return res; ArtistContainer RecommendationService::getSimilarArtists(Database::ArtistId artistId, EnumSet<Database::TrackArtistLinkType> linkTypes, std::size_t maxCount) const
} {
ArtistContainer res;
ReleaseContainer if (!_engine)
RecommendationService::getSimilarReleases(Database::ReleaseId releaseId, std::size_t maxCount) const return res;
{
ReleaseContainer res;
std::shared_lock lock {_enginesMutex}; return _engine->getSimilarArtists(artistId, linkTypes, maxCount);
for (EngineType engineType : _enginePriorities)
{
auto itEngine {_engines.find(engineType)};
if (itEngine == std::cend(_engines))
continue;
LMS_LOG(RECOMMENDATION, DEBUG) << "Trying engine '" << engineTypeToString(engineType) << "' to get similar releases"; return res;
}
const IEngine& engine {*itEngine->second}; void RecommendationService::load()
res = engine.getSimilarReleases(releaseId, maxCount); {
if (!res.empty()) using namespace Database;
{
LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar releases using engine '" << engineTypeToString(engineType) << "'";
break;
}
LMS_LOG(RECOMMENDATION, DEBUG) << "No result using engine '" << engineTypeToString(engineType) << "'"; switch (getSimilarityEngineType(_db.getTLSSession()))
} {
case ScanSettings::SimilarityEngineType::Clusters:
if (_engineType != EngineType::Clusters)
{
_engineType = EngineType::Clusters;
_engine = createClustersEngine(_db);
}
break;
return res; case ScanSettings::SimilarityEngineType::Features:
} case ScanSettings::SimilarityEngineType::None:
_engineType.reset();
ArtistContainer _engine.reset();
RecommendationService::getSimilarArtists(Database::ArtistId artistId, EnumSet<Database::TrackArtistLinkType> linkTypes, std::size_t maxCount) const break;
{ }
ArtistContainer res;
std::shared_lock lock {_enginesMutex};
for (EngineType engineType : _enginePriorities)
{
auto itEngine {_engines.find(engineType)};
if (itEngine == std::cend(_engines))
continue;
LMS_LOG(RECOMMENDATION, DEBUG) << "Trying engine '" << engineTypeToString(engineType) << "' to get similar artists";
const IEngine& engine {*itEngine->second};
res = engine.getSimilarArtists(artistId, linkTypes, maxCount);
if (!res.empty())
{
LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar artists using engine '" << engineTypeToString(engineType) << "'";
return res;
}
}
return res;
}
static
Database::ScanSettings::SimilarityEngineType
getSimilarityEngineType(Database::Session& session)
{
auto transaction {session.createSharedTransaction()};
return Database::ScanSettings::get(session)->getSimilarityEngineType();
}
void
RecommendationService::load(bool forceReload, const ProgressCallback& progressCallback)
{
using namespace Database;
LMS_LOG(RECOMMENDATION, INFO) << "Reloading recommendation engines...";
EngineContainer enginesToLoad;
{
std::unique_lock controlLock {_controlMutex};
{
std::unique_lock lock {_enginesMutex};
_engines.clear();
}
switch (getSimilarityEngineType(_db.getTLSSession()))
{
case ScanSettings::SimilarityEngineType::Clusters:
_enginePriorities = {EngineType::Clusters};
enginesToLoad.try_emplace(EngineType::Clusters, createClustersEngine(_db));
break;
case ScanSettings::SimilarityEngineType::Features:
_enginePriorities = {EngineType::Features, EngineType::Clusters};
// not same order since clusters is faster to load
enginesToLoad.try_emplace(EngineType::Clusters, createClustersEngine(_db));
enginesToLoad.try_emplace(EngineType::Features, createFeaturesEngine(_db));
break;
case ScanSettings::SimilarityEngineType::None:
_enginePriorities.clear();
break;
}
assert(_pendingEngines.empty());
for (auto& [engineType, engine] : enginesToLoad)
_pendingEngines.push_back(engine.get());
}
for (auto& [engineType, engine] : enginesToLoad)
loadPendingEngine(engineType, std::move(engine), forceReload, progressCallback);
_pendingEnginesCondvar.notify_all();
LMS_LOG(RECOMMENDATION, INFO) << "Recommendation engines loaded!";
}
void
RecommendationService::loadPendingEngine(EngineType engineType, std::unique_ptr<IEngine> engine, bool forceReload, const ProgressCallback& progressCallback)
{
if (!_loadCancelled)
{
LMS_LOG(RECOMMENDATION, INFO) << "Initializing engine '" << engineTypeToString(engineType) << "'...";
auto progress {[&](const Progress& progress)
{
progressCallback(progress);
}};
engine->load(forceReload, progressCallback ? progress : ProgressCallback {});
LMS_LOG(RECOMMENDATION, INFO) << "Initializing engine '" << engineTypeToString(engineType) << "': " << (_loadCancelled ? "aborted" : "complete");
}
{
std::scoped_lock lock {_controlMutex};
_pendingEngines.erase(std::find(std::begin(_pendingEngines), std::end(_pendingEngines), engine.get()));
}
if (!_loadCancelled)
{
std::unique_lock lock {_enginesMutex};
_engines.emplace(engineType, std::move(engine));
}
}
void
RecommendationService::cancelLoad()
{
LMS_LOG(RECOMMENDATION, DEBUG) << "Cancelling loading...";
std::unique_lock controlLock {_controlMutex};
assert(!_loadCancelled);
_loadCancelled = true;
LMS_LOG(RECOMMENDATION, DEBUG) << "Still " << _pendingEngines.size() << " pending engines!";
for (IEngine* engine : _pendingEngines)
{
engine->requestCancelLoad();
}
_pendingEnginesCondvar.wait(controlLock, [this] {return _pendingEngines.empty();});
_loadCancelled = false;
LMS_LOG(RECOMMENDATION, DEBUG) << "Cancelling loading DONE";
}
if (_engine)
_engine->load(false);
}
} // ns Similarity } // ns Similarity
@@ -19,67 +19,49 @@
#pragma once #pragma once
#include <condition_variable> #include <optional>
#include <mutex>
#include <shared_mutex>
#include <unordered_map>
#include <vector>
#include "services/recommendation/IRecommendationService.hpp" #include "services/recommendation/IRecommendationService.hpp"
#include "IEngine.hpp" #include "IEngine.hpp"
namespace Database namespace Database
{ {
class Db; class Db;
} }
namespace Recommendation namespace Recommendation
{ {
enum class EngineType enum class EngineType
{ {
Clusters, Clusters,
Features, Features,
}; };
class RecommendationService : public IRecommendationService class RecommendationService : public IRecommendationService
{ {
public: public:
RecommendationService(Database::Db& db); RecommendationService(Database::Db& db);
~RecommendationService() = default; ~RecommendationService() = default;
RecommendationService(const RecommendationService&) = delete; RecommendationService(const RecommendationService&) = delete;
RecommendationService(RecommendationService&&) = delete; RecommendationService& operator=(const RecommendationService&) = delete;
RecommendationService& operator=(const RecommendationService&) = delete;
RecommendationService& operator=(RecommendationService&&) = delete;
private: private:
void load(bool forceReload, const ProgressCallback& progressCallback) override; void load() override;
void cancelLoad() override;
TrackContainer findSimilarTracks(Database::TrackListId tracklistId, std::size_t maxCount) const override; TrackContainer findSimilarTracks(Database::TrackListId tracklistId, std::size_t maxCount) const override;
TrackContainer findSimilarTracks(const std::vector<Database::TrackId>& tracksId, std::size_t maxCount) const override; TrackContainer findSimilarTracks(const std::vector<Database::TrackId>& tracksId, std::size_t maxCount) const override;
ReleaseContainer getSimilarReleases(Database::ReleaseId releaseId, std::size_t maxCount) const override; ReleaseContainer getSimilarReleases(Database::ReleaseId releaseId, std::size_t maxCount) const override;
ArtistContainer getSimilarArtists(Database::ArtistId artistId, EnumSet<Database::TrackArtistLinkType> linkTypes, std::size_t maxCount) const override; ArtistContainer getSimilarArtists(Database::ArtistId artistId, EnumSet<Database::TrackArtistLinkType> linkTypes, std::size_t maxCount) const override;
void setEnginePriorities(const std::vector<EngineType>& engineTypes); void setEnginePriorities(const std::vector<EngineType>& engineTypes);
void clearEngines(); void clearEngines();
void loadPendingEngine(EngineType engineType, std::unique_ptr<IEngine> engine, bool forceReload, const ProgressCallback& progressCallback); void loadPendingEngine(EngineType engineType, std::unique_ptr<IEngine> engine, bool forceReload, const ProgressCallback& progressCallback);
Database::Db& _db; Database::Db& _db;
std::optional<EngineType> _engineType;
std::mutex _controlMutex; std::unique_ptr<IEngine> _engine;
bool _loadCancelled {}; };
using EngineContainer = std::unordered_map<EngineType, std::unique_ptr<IEngine>>;
EngineContainer _engines;
mutable std::shared_mutex _enginesMutex;
std::vector<IEngine*> _pendingEngines;
std::shared_mutex _pendingEnginesMutex;
std::condition_variable _pendingEnginesCondvar;
std::vector<EngineType> _enginePriorities; // ordered by priority
};
} // ns Recommendation } // ns Recommendation
@@ -120,7 +120,7 @@ FeaturesEngine::loadFromTraining(const TrainSettings& trainSettings, const Progr
auto transaction {session.createSharedTransaction()}; auto transaction {session.createSharedTransaction()};
LMS_LOG(RECOMMENDATION, DEBUG) << "Getting Track features..."; LMS_LOG(RECOMMENDATION, DEBUG) << "Getting Track features...";
trackFeaturesIds = TrackFeatures::find(session, Range {}); trackFeaturesIds = TrackFeatures::find(session);
LMS_LOG(RECOMMENDATION, DEBUG) << "Getting Track features DONE (found " << trackFeaturesIds.results.size() << " track features)"; LMS_LOG(RECOMMENDATION, DEBUG) << "Getting Track features DONE (found " << trackFeaturesIds.results.size() << " track features)";
} }
@@ -20,6 +20,7 @@
#pragma once #pragma once
#include <memory> #include <memory>
#include <vector>
#include "utils/EnumSet.hpp" #include "utils/EnumSet.hpp"
#include "services/database/TrackListId.hpp" #include "services/database/TrackListId.hpp"
#include "services/database/Types.hpp" #include "services/database/Types.hpp"
@@ -37,8 +38,7 @@ namespace Recommendation
public: public:
virtual ~IRecommendationService() = default; virtual ~IRecommendationService() = default;
virtual void load(bool forceReload, const ProgressCallback& progressCallback = {}) = 0; virtual void load() = 0;
virtual void cancelLoad() = 0; // wait for cancel done
virtual TrackContainer findSimilarTracks(Database::TrackListId tracklistId, std::size_t maxCount) const = 0; virtual TrackContainer findSimilarTracks(Database::TrackListId tracklistId, std::size_t maxCount) const = 0;
virtual TrackContainer findSimilarTracks(const std::vector<Database::TrackId>& tracksId, std::size_t maxCount) const = 0; virtual TrackContainer findSimilarTracks(const std::vector<Database::TrackId>& tracksId, std::size_t maxCount) const = 0;
+1
View File
@@ -3,6 +3,7 @@ add_library(lmsscanner SHARED
impl/ScannerService.cpp impl/ScannerService.cpp
impl/ScannerStats.cpp impl/ScannerStats.cpp
impl/ScanStepCheckDuplicatedDbFiles.cpp impl/ScanStepCheckDuplicatedDbFiles.cpp
impl/ScanStepComputeClusterStats.cpp
impl/ScanStepDiscoverFiles.cpp impl/ScanStepDiscoverFiles.cpp
impl/ScanStepRemoveOrphanDbFiles.cpp impl/ScanStepRemoveOrphanDbFiles.cpp
impl/ScanStepScanFiles.cpp impl/ScanStepScanFiles.cpp
@@ -37,7 +37,7 @@ namespace Scanner
Session& session {_db.getTLSSession()}; Session& session {_db.getTLSSession()};
auto transaction {session.createSharedTransaction()}; auto transaction {session.createSharedTransaction()};
const RangeResults<TrackId> tracks = Track::findIdsTrackMBIDDuplicates(session, Range {}); const RangeResults<TrackId> tracks = Track::findIdsTrackMBIDDuplicates(session);
for (const TrackId trackId : tracks.results) for (const TrackId trackId : tracks.results)
{ {
const Track::pointer track {Track::find(session, trackId)}; const Track::pointer track {Track::find(session, trackId)};
@@ -0,0 +1,86 @@
/*
* Copyright (C) 2023 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "ScanStepComputeClusterStats.hpp"
#include "services/database/Db.hpp"
#include "services/database/Cluster.hpp"
#include "services/database/Session.hpp"
#include "utils/Logger.hpp"
#include "utils/Path.hpp"
namespace Scanner
{
void ScanStepComputeClusterStats::process(ScanContext& context)
{
using namespace Database;
if (context.stats.nbChanges() == 0)
return;
Session& dbSession{ _db.getTLSSession() };
const std::size_t clusterCount{ [&] {
auto transaction{ dbSession.createSharedTransaction() };
return Cluster::getCount(dbSession);
}() };
context.currentStepStats.totalElems = clusterCount;
foreachSubRange(Range{ 0, clusterCount }, 100, [&](Range range)
{
const std::vector<ClusterId> clusterIds{ [&]
{
Cluster::FindParameters params;
params.setRange(range);
{
auto transaction{ dbSession.createSharedTransaction() };
return std::move(Cluster::findIds(dbSession, params).results);
}
}() };
for (const ClusterId clusterId : clusterIds)
{
std::size_t trackCount;
std::size_t releaseCount;
{
auto transaction{ dbSession.createSharedTransaction() };
trackCount = Cluster::computeTrackCount(dbSession, clusterId);
releaseCount = Cluster::computeReleaseCount(dbSession, clusterId);
}
{
auto transaction{ dbSession.createUniqueTransaction() };
auto cluster{ Cluster::find(dbSession, clusterId) };
cluster.modify()->setTrackCount(trackCount);
cluster.modify()->setReleaseCount(releaseCount);
}
context.currentStepStats.processedElems++;
}
return true;
});
LMS_LOG(DBUPDATER, DEBUG) << "Recomputed stats for " << clusterCount << " clusters!";
}
}
@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2015 Emeric Poupon * Copyright (C) 2023 Emeric Poupon
* *
* This file is part of LMS. * This file is part of LMS.
* *
@@ -19,27 +19,18 @@
#pragma once #pragma once
#include <chrono> #include "ScanStepBase.hpp"
#include <filesystem>
#include <optional>
#include "Types.hpp" namespace Scanner
namespace Av
{ {
struct InputFileParameters class ScanStepComputeClusterStats : public ScanStepBase
{ {
std::filesystem::path trackPath; public:
std::chrono::milliseconds duration; using ScanStepBase::ScanStepBase;
};
struct TranscodeParameters
{
Format format;
std::size_t bitrate {128000};
std::optional<std::size_t> stream; // Id of the stream to be transcoded (auto detect by default)
std::chrono::milliseconds offset {0};
bool stripMetadata {true};
};
} // namespace Av
private:
ScanStep getStep() const override { return ScanStep::ComputeClusterStats; }
std::string_view getStepName() const override { return "Compute cluster stats"; }
void process(ScanContext& context) override;
};
}
@@ -30,174 +30,169 @@
namespace Scanner namespace Scanner
{ {
void void ScanStepRemoveOrphanDbFiles::process(ScanContext& context)
ScanStepRemoveOrphanDbFiles::process(ScanContext& context) {
{ removeOrphanTracks(context);
removeOrphanTracks(context); removeOrphanClusters();
removeOrphanClusters(); removeOrphanArtists();
removeOrphanArtists(); removeOrphanReleases();
removeOrphanReleases(); }
}
void ScanStepRemoveOrphanDbFiles::removeOrphanTracks(ScanContext& context) void ScanStepRemoveOrphanDbFiles::removeOrphanTracks(ScanContext& context)
{ {
using namespace Database; using namespace Database;
if (_abortScan) if (_abortScan)
return; return;
static constexpr std::size_t batchSize {50}; static constexpr std::size_t batchSize{ 50 };
Session& session {_db.getTLSSession()}; Session& session{ _db.getTLSSession() };
LMS_LOG(DBUPDATER, DEBUG) << "Checking tracks to be removed..."; LMS_LOG(DBUPDATER, DEBUG) << "Checking tracks to be removed...";
std::size_t trackCount {}; std::size_t trackCount{};
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
trackCount = Track::getCount(session); trackCount = Track::getCount(session);
} }
LMS_LOG(DBUPDATER, DEBUG) << trackCount << " tracks to be checked..."; LMS_LOG(DBUPDATER, DEBUG) << trackCount << " tracks to be checked...";
context.currentStepStats.totalElems = trackCount; context.currentStepStats.totalElems = trackCount;
RangeResults<Track::PathResult> trackPaths; RangeResults<Track::PathResult> trackPaths;
std::vector<TrackId> tracksToRemove; std::vector<TrackId> tracksToRemove;
// TODO handle only files in context.directory // TODO handle only files in context.directory
for (std::size_t i {trackCount < batchSize ? 0 : trackCount - batchSize}; ; i -= (i > batchSize ? batchSize : i)) for (std::size_t i{ trackCount < batchSize ? 0 : trackCount - batchSize }; ; i -= (i > batchSize ? batchSize : i))
{ {
tracksToRemove.clear(); tracksToRemove.clear();
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
trackPaths = Track::findPaths(session, Range {i, batchSize}); trackPaths = Track::findPaths(session, Range{ i, batchSize });
} }
for (const Track::PathResult& trackPath : trackPaths.results) for (const Track::PathResult& trackPath : trackPaths.results)
{ {
if (_abortScan) if (_abortScan)
return; return;
if (!checkFile(trackPath.path)) if (!checkFile(trackPath.path))
tracksToRemove.push_back(trackPath.trackId); tracksToRemove.push_back(trackPath.trackId);
context.currentStepStats.processedElems++; context.currentStepStats.processedElems++;
} }
if (!tracksToRemove.empty()) if (!tracksToRemove.empty())
{ {
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
for (const TrackId trackId : tracksToRemove) for (const TrackId trackId : tracksToRemove)
{ {
Track::pointer track {Track::find(session, trackId)}; Track::pointer track{ Track::find(session, trackId) };
if (track) if (track)
{ {
track.remove(); track.remove();
context.stats.deletions++; context.stats.deletions++;
} }
} }
} }
_progressCallback(context.currentStepStats); _progressCallback(context.currentStepStats);
if (i == 0) if (i == 0)
break; break;
} }
LMS_LOG(DBUPDATER, DEBUG) << trackCount << " tracks checked!"; LMS_LOG(DBUPDATER, DEBUG) << trackCount << " tracks checked!";
} }
void void ScanStepRemoveOrphanDbFiles::removeOrphanClusters()
ScanStepRemoveOrphanDbFiles::removeOrphanClusters() {
{ using namespace Database;
using namespace Database;
LMS_LOG(DBUPDATER, DEBUG) << "Checking orphan clusters..."; LMS_LOG(DBUPDATER, DEBUG) << "Checking orphan clusters...";
Session& session {_db.getTLSSession()}; Session& session{ _db.getTLSSession() };
auto transaction {session.createUniqueTransaction()}; auto transaction{ session.createUniqueTransaction() };
// Now process orphan Cluster (no track) // Now process orphan Cluster (no track)
auto clusterIds {Cluster::findOrphans(session, Range {})}; auto clusterIds{ Cluster::findOrphans(session) };
for (ClusterId clusterId : clusterIds.results) for (ClusterId clusterId : clusterIds.results)
{ {
Cluster::pointer cluster {Cluster::find(session, clusterId)}; Cluster::pointer cluster{ Cluster::find(session, clusterId) };
LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan cluster '" << cluster->getName() << "'"; LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan cluster '" << cluster->getName() << "'";
cluster.remove(); cluster.remove();
} }
} }
void void ScanStepRemoveOrphanDbFiles::removeOrphanArtists()
ScanStepRemoveOrphanDbFiles::removeOrphanArtists() {
{ using namespace Database;
using namespace Database;
LMS_LOG(DBUPDATER, DEBUG) << "Checking orphan artists..."; LMS_LOG(DBUPDATER, DEBUG) << "Checking orphan artists...";
Session& session {_db.getTLSSession()}; Session& session{ _db.getTLSSession() };
auto transaction {session.createUniqueTransaction()}; auto transaction{ session.createUniqueTransaction() };
auto artistIds {Artist::findOrphanIds(session, Range {})}; auto artistIds{ Artist::findOrphanIds(session) };
for (const ArtistId artistId : artistIds.results) for (const ArtistId artistId : artistIds.results)
{ {
Artist::pointer artist {Artist::find(session, artistId)}; Artist::pointer artist{ Artist::find(session, artistId) };
LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan artist '" << artist->getName() << "'"; LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan artist '" << artist->getName() << "'";
artist.remove(); artist.remove();
} }
} }
void void ScanStepRemoveOrphanDbFiles::removeOrphanReleases()
ScanStepRemoveOrphanDbFiles::removeOrphanReleases() {
{ using namespace Database;
using namespace Database;
LMS_LOG(DBUPDATER, DEBUG) << "Checking orphan releases..."; LMS_LOG(DBUPDATER, DEBUG) << "Checking orphan releases...";
// TODO, by batch // TODO, by batch
Session& session {_db.getTLSSession()}; Session& session{ _db.getTLSSession() };
auto transaction {session.createUniqueTransaction()}; auto transaction{ session.createUniqueTransaction() };
auto releases {Release::findOrphanIds(session, Range {})}; auto releases{ Release::findOrphanIds(session) };
for (const ReleaseId releaseId : releases.results) for (const ReleaseId releaseId : releases.results)
{ {
Release::pointer release {Release::find(session, releaseId)}; Release::pointer release{ Release::find(session, releaseId) };
LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan release '" << release->getName() << "'"; LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan release '" << release->getName() << "'";
release.remove(); release.remove();
} }
} }
bool bool ScanStepRemoveOrphanDbFiles::checkFile(const std::filesystem::path& p)
ScanStepRemoveOrphanDbFiles::checkFile(const std::filesystem::path& p) {
{ try
try {
{ // For each track, make sure the the file still exists
// For each track, make sure the the file still exists // and still belongs to a media directory
// and still belongs to a media directory if (!std::filesystem::exists(p)
if (!std::filesystem::exists( p ) || !std::filesystem::is_regular_file(p))
|| !std::filesystem::is_regular_file( p ) ) {
{ LMS_LOG(DBUPDATER, INFO) << "Removing '" << p.string() << "': missing";
LMS_LOG(DBUPDATER, INFO) << "Removing '" << p.string() << "': missing"; return false;
return false; }
}
if (!PathUtils::isPathInRootPath(p, _settings.mediaDirectory, &excludeDirFileName)) if (!PathUtils::isPathInRootPath(p, _settings.mediaDirectory, &excludeDirFileName))
{ {
LMS_LOG(DBUPDATER, INFO) << "Removing '" << p.string() << "': out of media directory"; LMS_LOG(DBUPDATER, INFO) << "Removing '" << p.string() << "': out of media directory";
return false; return false;
} }
if (!PathUtils::hasFileAnyExtension(p, _settings.supportedExtensions)) if (!PathUtils::hasFileAnyExtension(p, _settings.supportedExtensions))
{ {
LMS_LOG(DBUPDATER, INFO) << "Removing '" << p.string() << "': file format no longer handled"; LMS_LOG(DBUPDATER, INFO) << "Removing '" << p.string() << "': file format no longer handled";
return false; return false;
} }
return true; return true;
} }
catch (std::filesystem::filesystem_error& e) catch (std::filesystem::filesystem_error& e)
{ {
LMS_LOG(DBUPDATER, ERROR) << "Caught exception while checking file '" << p.string() << "': " << e.what(); LMS_LOG(DBUPDATER, ERROR) << "Caught exception while checking file '" << p.string() << "': " << e.what();
return false; return false;
} }
} }
} }
@@ -37,500 +37,486 @@ using namespace Database;
namespace namespace
{ {
Artist::pointer Artist::pointer
createArtist(Session& session, const MetaData::Artist& artistInfo) createArtist(Session& session, const MetaData::Artist& artistInfo)
{ {
Artist::pointer artist {session.create<Artist>(artistInfo.name)}; Artist::pointer artist{ session.create<Artist>(artistInfo.name) };
if (artistInfo.mbid) if (artistInfo.mbid)
artist.modify()->setMBID(*artistInfo.mbid); artist.modify()->setMBID(*artistInfo.mbid);
if (artistInfo.sortName) if (artistInfo.sortName)
artist.modify()->setSortName(*artistInfo.sortName); artist.modify()->setSortName(*artistInfo.sortName);
return artist; return artist;
} }
void void
updateArtistIfNeeded(Artist::pointer artist, const MetaData::Artist& artistInfo) updateArtistIfNeeded(Artist::pointer artist, const MetaData::Artist& artistInfo)
{ {
// Name may have been updated // Name may have been updated
if (artist->getName() != artistInfo.name) if (artist->getName() != artistInfo.name)
{ {
artist.modify()->setName(artistInfo.name); artist.modify()->setName(artistInfo.name);
} }
// Sortname may have been updated // Sortname may have been updated
if (artistInfo.sortName && *artistInfo.sortName != artist->getSortName() ) if (artistInfo.sortName && *artistInfo.sortName != artist->getSortName())
{ {
artist.modify()->setSortName(*artistInfo.sortName); artist.modify()->setSortName(*artistInfo.sortName);
} }
} }
std::vector<Artist::pointer> std::vector<Artist::pointer>
getOrCreateArtists(Session& session, const std::vector<MetaData::Artist>& artistsInfo, bool allowFallbackOnMBIDEntries) getOrCreateArtists(Session& session, const std::vector<MetaData::Artist>& artistsInfo, bool allowFallbackOnMBIDEntries)
{ {
std::vector<Artist::pointer> artists; std::vector<Artist::pointer> artists;
for (const MetaData::Artist& artistInfo : artistsInfo) for (const MetaData::Artist& artistInfo : artistsInfo)
{ {
Artist::pointer artist; Artist::pointer artist;
// First try to get by MBID // First try to get by MBID
if (artistInfo.mbid) if (artistInfo.mbid)
{ {
artist = Artist::find(session, *artistInfo.mbid); artist = Artist::find(session, *artistInfo.mbid);
if (!artist) if (!artist)
artist = createArtist(session, artistInfo); artist = createArtist(session, artistInfo);
else else
updateArtistIfNeeded(artist, artistInfo); updateArtistIfNeeded(artist, artistInfo);
artists.emplace_back(std::move(artist)); artists.emplace_back(std::move(artist));
continue; continue;
} }
// Fall back on artist name (collisions may occur) // Fall back on artist name (collisions may occur)
if (!artistInfo.name.empty()) if (!artistInfo.name.empty())
{ {
for (const Artist::pointer& sameNamedArtist : Artist::find(session, artistInfo.name)) for (const Artist::pointer& sameNamedArtist : Artist::find(session, artistInfo.name))
{ {
// Do not fallback on artist that is correctly tagged // Do not fallback on artist that is correctly tagged
if (!allowFallbackOnMBIDEntries && sameNamedArtist->getMBID()) if (!allowFallbackOnMBIDEntries && sameNamedArtist->getMBID())
continue; continue;
artist = sameNamedArtist; artist = sameNamedArtist;
break; break;
} }
// No Artist found with the same name and without MBID -> creating // No Artist found with the same name and without MBID -> creating
if (!artist) if (!artist)
artist = createArtist(session, artistInfo); artist = createArtist(session, artistInfo);
else else
updateArtistIfNeeded(artist, artistInfo); updateArtistIfNeeded(artist, artistInfo);
artists.emplace_back(std::move(artist)); artists.emplace_back(std::move(artist));
continue; continue;
} }
} }
return artists; return artists;
} }
ReleaseTypePrimary convertReleaseTypePrimary(MetaData::Release::PrimaryType type) ReleaseTypePrimary convertReleaseTypePrimary(MetaData::Release::PrimaryType type)
{ {
switch (type) switch (type)
{ {
case MetaData::Release::PrimaryType::Album: return ReleaseTypePrimary::Album; case MetaData::Release::PrimaryType::Album: return ReleaseTypePrimary::Album;
case MetaData::Release::PrimaryType::Single: return ReleaseTypePrimary::Single; case MetaData::Release::PrimaryType::Single: return ReleaseTypePrimary::Single;
case MetaData::Release::PrimaryType::EP: return ReleaseTypePrimary::EP; case MetaData::Release::PrimaryType::EP: return ReleaseTypePrimary::EP;
case MetaData::Release::PrimaryType::Broadcast: return ReleaseTypePrimary::Broadcast; case MetaData::Release::PrimaryType::Broadcast: return ReleaseTypePrimary::Broadcast;
case MetaData::Release::PrimaryType::Other: return ReleaseTypePrimary::Other; case MetaData::Release::PrimaryType::Other: return ReleaseTypePrimary::Other;
} }
return ReleaseTypePrimary::Other; return ReleaseTypePrimary::Other;
} }
EnumSet<ReleaseTypeSecondary> convertReleaseTypesSecondary(EnumSet<MetaData::Release::SecondaryType> types) EnumSet<ReleaseTypeSecondary> convertReleaseTypesSecondary(EnumSet<MetaData::Release::SecondaryType> types)
{ {
EnumSet<ReleaseTypeSecondary> res; EnumSet<ReleaseTypeSecondary> res;
for (MetaData::Release::SecondaryType type : types) for (MetaData::Release::SecondaryType type : types)
{ {
switch (type) switch (type)
{ {
case MetaData::Release::SecondaryType::Compilation: case MetaData::Release::SecondaryType::Compilation:
res.insert(ReleaseTypeSecondary::Compilation); res.insert(ReleaseTypeSecondary::Compilation);
break; break;
case MetaData::Release::SecondaryType::Soundtrack: case MetaData::Release::SecondaryType::Soundtrack:
res.insert(ReleaseTypeSecondary::Soundtrack); res.insert(ReleaseTypeSecondary::Soundtrack);
break; break;
case MetaData::Release::SecondaryType::Spokenword: case MetaData::Release::SecondaryType::Spokenword:
res.insert(ReleaseTypeSecondary::Spokenword); res.insert(ReleaseTypeSecondary::Spokenword);
break; break;
case MetaData::Release::SecondaryType::Interview: case MetaData::Release::SecondaryType::Interview:
res.insert(ReleaseTypeSecondary::Interview); res.insert(ReleaseTypeSecondary::Interview);
break; break;
case MetaData::Release::SecondaryType::Audiobook: case MetaData::Release::SecondaryType::Audiobook:
res.insert(ReleaseTypeSecondary::Audiobook); res.insert(ReleaseTypeSecondary::Audiobook);
break; break;
case MetaData::Release::SecondaryType::AudioDrama: case MetaData::Release::SecondaryType::AudioDrama:
res.insert(ReleaseTypeSecondary::AudioDrama); res.insert(ReleaseTypeSecondary::AudioDrama);
break; break;
case MetaData::Release::SecondaryType::Live: case MetaData::Release::SecondaryType::Live:
res.insert(ReleaseTypeSecondary::Live); res.insert(ReleaseTypeSecondary::Live);
break; break;
case MetaData::Release::SecondaryType::Remix: case MetaData::Release::SecondaryType::Remix:
res.insert(ReleaseTypeSecondary::Remix); res.insert(ReleaseTypeSecondary::Remix);
break; break;
case MetaData::Release::SecondaryType::DJMix: case MetaData::Release::SecondaryType::DJMix:
res.insert(ReleaseTypeSecondary::DJMix); res.insert(ReleaseTypeSecondary::DJMix);
break; break;
case MetaData::Release::SecondaryType::Mixtape_Street: case MetaData::Release::SecondaryType::Mixtape_Street:
res.insert(ReleaseTypeSecondary::Mixtape_Street); res.insert(ReleaseTypeSecondary::Mixtape_Street);
break; break;
case MetaData::Release::SecondaryType::Demo: case MetaData::Release::SecondaryType::Demo:
res.insert(ReleaseTypeSecondary::Demo); res.insert(ReleaseTypeSecondary::Demo);
break; break;
} }
} }
return res; return res;
} }
void void
updateReleaseIfNeeded(Release::pointer release, const MetaData::Release& releaseInfo) updateReleaseIfNeeded(Release::pointer release, const MetaData::Release& releaseInfo)
{ {
if (release->getName() != releaseInfo.name) if (release->getName() != releaseInfo.name)
release.modify()->setName(releaseInfo.name); release.modify()->setName(releaseInfo.name);
if (release->getTotalDisc() != releaseInfo.mediumCount) if (release->getTotalDisc() != releaseInfo.mediumCount)
release.modify()->setTotalDisc(releaseInfo.mediumCount); release.modify()->setTotalDisc(releaseInfo.mediumCount);
if (releaseInfo.primaryType) if (releaseInfo.primaryType)
{ {
const ReleaseTypePrimary primaryType {convertReleaseTypePrimary(*releaseInfo.primaryType)}; const ReleaseTypePrimary primaryType{ convertReleaseTypePrimary(*releaseInfo.primaryType) };
if (release->getPrimaryType() != primaryType) if (release->getPrimaryType() != primaryType)
release.modify()->setPrimaryType(primaryType); release.modify()->setPrimaryType(primaryType);
} }
const EnumSet<ReleaseTypeSecondary> secondaryTypes{ convertReleaseTypesSecondary(releaseInfo.secondaryTypes) }; const EnumSet<ReleaseTypeSecondary> secondaryTypes{ convertReleaseTypesSecondary(releaseInfo.secondaryTypes) };
if (release->getSecondaryTypes() != secondaryTypes) if (release->getSecondaryTypes() != secondaryTypes)
release.modify()->setSecondaryTypes(secondaryTypes); release.modify()->setSecondaryTypes(secondaryTypes);
if (release->getArtistDisplayName() != releaseInfo.artistDisplayName) if (release->getArtistDisplayName() != releaseInfo.artistDisplayName)
release.modify()->setArtistDisplayName(releaseInfo.artistDisplayName); release.modify()->setArtistDisplayName(releaseInfo.artistDisplayName);
} }
Release::pointer Release::pointer
getOrCreateRelease(Session& session, const MetaData::Release& releaseInfo) getOrCreateRelease(Session& session, const MetaData::Release& releaseInfo)
{ {
Release::pointer release; Release::pointer release;
// First try to get by MBID // First try to get by MBID
if (releaseInfo.mbid) if (releaseInfo.mbid)
{ {
release = Release::find(session, *releaseInfo.mbid); release = Release::find(session, *releaseInfo.mbid);
if (!release) if (!release)
release = session.create<Release>(releaseInfo.name, releaseInfo.mbid); release = session.create<Release>(releaseInfo.name, releaseInfo.mbid);
updateReleaseIfNeeded(release, releaseInfo); updateReleaseIfNeeded(release, releaseInfo);
return release; return release;
} }
// Fall back on release name (collisions may occur) // Fall back on release name (collisions may occur)
if (!releaseInfo.name.empty()) if (!releaseInfo.name.empty())
{ {
for (const Release::pointer& sameNamedRelease : Release::find(session, releaseInfo.name)) for (const Release::pointer& sameNamedRelease : Release::find(session, releaseInfo.name))
{ {
// do not fallback on properly tagged releases // do not fallback on properly tagged releases
if (sameNamedRelease->getMBID()) if (sameNamedRelease->getMBID())
continue; continue;
release = sameNamedRelease; release = sameNamedRelease;
break; break;
} }
// No release found with the same name and without MBID -> creating // No release found with the same name and without MBID -> creating
if (!release) if (!release)
release = session.create<Release>(releaseInfo.name); release = session.create<Release>(releaseInfo.name);
updateReleaseIfNeeded(release, releaseInfo); updateReleaseIfNeeded(release, releaseInfo);
return release; return release;
} }
return Release::pointer{}; return Release::pointer{};
} }
std::vector<Cluster::pointer> std::vector<Cluster::pointer>
getOrCreateClusters(Session& session, const MetaData::Tags& tags) getOrCreateClusters(Session& session, const MetaData::Tags& tags)
{ {
std::vector<Cluster::pointer> clusters; std::vector<Cluster::pointer> clusters;
for (const auto& [tag, values] : tags) for (const auto& [tag, values] : tags)
{ {
auto clusterType = ClusterType::find(session, tag); auto clusterType = ClusterType::find(session, tag);
if (!clusterType) if (!clusterType)
continue; continue;
for (auto clusterName : values) for (auto clusterName : values)
{ {
auto cluster = clusterType->getCluster(clusterName); auto cluster = clusterType->getCluster(clusterName);
if (!cluster) if (!cluster)
cluster = session.create<Cluster>(clusterType, clusterName); cluster = session.create<Cluster>(clusterType, clusterName);
clusters.push_back(cluster); clusters.push_back(cluster);
} }
} }
return clusters; return clusters;
} }
MetaData::ParserReadStyle MetaData::ParserReadStyle
getParserReadStyle() getParserReadStyle()
{ {
std::string_view readStyle {Service<IConfig>::get()->getString("scanner-parser-read-style", "accurate")}; std::string_view readStyle{ Service<IConfig>::get()->getString("scanner-parser-read-style", "accurate") };
if (readStyle == "fast") if (readStyle == "fast")
return MetaData::ParserReadStyle::Fast; return MetaData::ParserReadStyle::Fast;
else if (readStyle == "average") else if (readStyle == "average")
return MetaData::ParserReadStyle::Average; return MetaData::ParserReadStyle::Average;
else if (readStyle == "accurate") else if (readStyle == "accurate")
return MetaData::ParserReadStyle::Accurate; return MetaData::ParserReadStyle::Accurate;
throw LmsException {"Invalid value for 'scanner-parser-read-style'"}; throw LmsException{ "Invalid value for 'scanner-parser-read-style'" };
} }
} // namespace } // namespace
namespace Scanner namespace Scanner
{ {
ScanStepScanFiles::ScanStepScanFiles(InitParams& initParams) ScanStepScanFiles::ScanStepScanFiles(InitParams& initParams)
: ScanStepBase {initParams} : ScanStepBase{ initParams }
, _metadataParser {MetaData::createParser(MetaData::ParserType::TagLib, getParserReadStyle())} // For now, always use TagLib , _metadataParser{ MetaData::createParser(MetaData::ParserType::TagLib, getParserReadStyle()) } // For now, always use TagLib
{ {
} }
void void
ScanStepScanFiles::process(ScanContext& context) ScanStepScanFiles::process(ScanContext& context)
{ {
_metadataParser->setClusterTypeNames(_settings.clusterTypeNames); _metadataParser->setClusterTypeNames(_settings.clusterTypeNames);
context.currentStepStats.totalElems = context.stats.filesScanned; context.currentStepStats.totalElems = context.stats.filesScanned;
PathUtils::exploreFilesRecursive(context.directory, [&](std::error_code ec, const std::filesystem::path& path) PathUtils::exploreFilesRecursive(context.directory, [&](std::error_code ec, const std::filesystem::path& path)
{ {
if (_abortScan) if (_abortScan)
return false; return false;
if (ec) if (ec)
{ {
LMS_LOG(DBUPDATER, ERROR) << "Cannot process entry '" << path.string() << "': " << ec.message(); LMS_LOG(DBUPDATER, ERROR) << "Cannot process entry '" << path.string() << "': " << ec.message();
context.stats.errors.emplace_back(ScanError {path, ScanErrorType::CannotReadFile, ec.message()}); context.stats.errors.emplace_back(ScanError{ path, ScanErrorType::CannotReadFile, ec.message() });
} }
else if (PathUtils::hasFileAnyExtension(path, _settings.supportedExtensions)) else if (PathUtils::hasFileAnyExtension(path, _settings.supportedExtensions))
{ {
scanAudioFile(path, context); scanAudioFile(path, context);
context.currentStepStats.processedElems++; context.currentStepStats.processedElems++;
_progressCallback(context.currentStepStats); _progressCallback(context.currentStepStats);
// optimize the database during scan (if we import a very large database, it may be too late to do it once at end) // optimize the database during scan (if we import a very large database, it may be too late to do it once at end)
if ((context.stats.nbChanges() % 5'000) == 0) if ((context.currentStepStats.processedElems % 1'000) == 0)
_db.getTLSSession().optimize(); _db.getTLSSession().optimize();
} }
return true; return true;
}, &excludeDirFileName); }, &excludeDirFileName);
} }
void void
ScanStepScanFiles::scanAudioFile(const std::filesystem::path& file, ScanContext& context) ScanStepScanFiles::scanAudioFile(const std::filesystem::path& file, ScanContext& context)
{ {
ScanStats& stats {context.stats}; ScanStats& stats{ context.stats };
Wt::WDateTime lastWriteTime; Wt::WDateTime lastWriteTime;
try try
{ {
lastWriteTime = PathUtils::getLastWriteTime(file); lastWriteTime = PathUtils::getLastWriteTime(file);
} }
catch (LmsException& e) catch (LmsException& e)
{ {
LMS_LOG(DBUPDATER, ERROR) << e.what(); LMS_LOG(DBUPDATER, ERROR) << e.what();
stats.skips++; stats.skips++;
return; return;
} }
if (!context.forceScan) if (!context.forceScan)
{ {
// Skip file if last write is the same // Skip file if last write is the same
Database::Session& dbSession {_db.getTLSSession()}; Database::Session& dbSession{ _db.getTLSSession() };
auto transaction {_db.getTLSSession().createSharedTransaction()}; auto transaction{ _db.getTLSSession().createSharedTransaction() };
const Track::pointer track {Track::findByPath(dbSession, file)}; const Track::pointer track{ Track::findByPath(dbSession, file) };
if (track && track->getLastWriteTime().toTime_t() == lastWriteTime.toTime_t() if (track && track->getLastWriteTime().toTime_t() == lastWriteTime.toTime_t()
&& track->getScanVersion() == _settings.scanVersion) && track->getScanVersion() == _settings.scanVersion)
{ {
stats.skips++; stats.skips++;
return; return;
} }
} }
std::optional<MetaData::Track> trackInfo {_metadataParser->parse(file)}; std::optional<MetaData::Track> trackInfo{ _metadataParser->parse(file) };
if (!trackInfo) if (!trackInfo)
{ {
context.stats.errors.emplace_back(file, ScanErrorType::CannotParseFile); context.stats.errors.emplace_back(file, ScanErrorType::CannotParseFile);
return; return;
} }
stats.scans++; stats.scans++;
Database::Session& dbSession {_db.getTLSSession()}; Database::Session& dbSession{ _db.getTLSSession() };
auto uniqueTransaction {dbSession.createUniqueTransaction()}; auto uniqueTransaction{ dbSession.createUniqueTransaction() };
Track::pointer track {Track::findByPath(dbSession, file) }; Track::pointer track{ Track::findByPath(dbSession, file) };
if (trackInfo->mbid && (!track || _settings.skipDuplicateMBID)) if (trackInfo->mbid && (!track || _settings.skipDuplicateMBID))
{ {
std::vector<Track::pointer> duplicateTracks {Track::findByMBID(dbSession, *trackInfo->mbid)}; std::vector<Track::pointer> duplicateTracks{ Track::findByMBID(dbSession, *trackInfo->mbid) };
// find for existing MBIDs as the file may have just been moved // find for existing MBIDs as the file may have just been moved
if (!track && duplicateTracks.size() == 1) if (!track && duplicateTracks.size() == 1)
{ {
Track::pointer otherTrack {duplicateTracks.front()}; Track::pointer otherTrack{ duplicateTracks.front() };
std::error_code ec; std::error_code ec;
if (!std::filesystem::exists(otherTrack->getPath(), ec)) if (!std::filesystem::exists(otherTrack->getPath(), ec))
{ {
LMS_LOG(DBUPDATER, DEBUG) << "Considering track '" << file.string() << "' moved from '" << otherTrack->getPath() << "'"; LMS_LOG(DBUPDATER, DEBUG) << "Considering track '" << file.string() << "' moved from '" << otherTrack->getPath() << "'";
track = otherTrack; track = otherTrack;
track.modify()->setPath(file); track.modify()->setPath(file);
} }
} }
// Skip duplicate track MBID // Skip duplicate track MBID
if (_settings.skipDuplicateMBID) if (_settings.skipDuplicateMBID)
{ {
for (Track::pointer otherTrack : duplicateTracks) for (Track::pointer otherTrack : duplicateTracks)
{ {
// Skip ourselves // Skip ourselves
if (track && track->getId() == otherTrack->getId()) if (track && track->getId() == otherTrack->getId())
continue; continue;
// Skip if duplicate files no longer in media root: as it will be removed later, we will end up with no file // Skip if duplicate files no longer in media root: as it will be removed later, we will end up with no file
if (!PathUtils::isPathInRootPath(file, _settings.mediaDirectory, &excludeDirFileName)) if (!PathUtils::isPathInRootPath(file, _settings.mediaDirectory, &excludeDirFileName))
continue; continue;
LMS_LOG(DBUPDATER, DEBUG) << "Skipped '" << file.string() << "' (similar MBID in '" << otherTrack->getPath().string() << "')"; LMS_LOG(DBUPDATER, DEBUG) << "Skipped '" << file.string() << "' (similar MBID in '" << otherTrack->getPath().string() << "')";
// As this MBID already exists, just remove what we just scanned // As this MBID already exists, just remove what we just scanned
if (track) if (track)
{ {
track.remove(); track.remove();
stats.deletions++; stats.deletions++;
} }
return; return;
} }
} }
} }
// We estimate this is an audio file if: // We estimate this is an audio file if the duration is not null
// - we found a least one audio stream if (trackInfo->duration == std::chrono::milliseconds::zero())
// - the duration is not null {
if (trackInfo->audioStreams.empty()) LMS_LOG(DBUPDATER, DEBUG) << "Skipped '" << file.string() << "' (duration is 0)";
{
LMS_LOG(DBUPDATER, DEBUG) << "Skipped '" << file.string() << "' (no audio stream found)";
// If Track exists here, delete it! // If Track exists here, delete it!
if (track) if (track)
{ {
track.remove(); track.remove();
stats.deletions++; stats.deletions++;
} }
stats.errors.emplace_back(ScanError {file, ScanErrorType::NoAudioTrack}); stats.errors.emplace_back(ScanError{ file, ScanErrorType::BadDuration });
return; return;
} }
if (trackInfo->duration == std::chrono::milliseconds::zero())
{
LMS_LOG(DBUPDATER, DEBUG) << "Skipped '" << file.string() << "' (duration is 0)";
// If Track exists here, delete it! // ***** Title
if (track) std::string title;
{ if (!trackInfo->title.empty())
track.remove(); title = trackInfo->title;
stats.deletions++; else
} {
stats.errors.emplace_back(ScanError {file, ScanErrorType::BadDuration}); // TODO parse file name guess track etc.
return; // For now juste use file name as title
} title = file.filename().string();
}
// ***** Title // If file already exists, update its data
std::string title; // Otherwise, create it
if (!trackInfo->title.empty()) if (!track)
title = trackInfo->title; {
else track = dbSession.create<Track>(file);
{ LMS_LOG(DBUPDATER, DEBUG) << "Adding '" << file.string() << "'";
// TODO parse file name guess track etc. stats.additions++;
// For now juste use file name as title }
title = file.filename().string(); else
} {
LMS_LOG(DBUPDATER, DEBUG) << "Updating '" << file.string() << "'";
// If file already exists, update its data stats.updates++;
// Otherwise, create it }
if (!track)
{
track = dbSession.create<Track>(file);
LMS_LOG(DBUPDATER, DEBUG) << "Adding '" << file.string() << "'";
stats.additions++;
}
else
{
LMS_LOG(DBUPDATER, DEBUG) << "Updating '" << file.string() << "'";
stats.updates++; // Track related data
} assert(track);
// Track related data track.modify()->clearArtistLinks();
assert(track); // Do not fallback on artists with the same name but having a MBID for artist and releaseArtists, as it may be corrected by properly tagging files
for (const Artist::pointer& artist : getOrCreateArtists(dbSession, trackInfo->artists, false))
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, artist, TrackArtistLinkType::Artist));
track.modify()->clearArtistLinks(); if (trackInfo->medium && trackInfo->medium->release)
// Do not fallback on artists with the same name but having a MBID for artist and releaseArtists, as it may be corrected by properly tagging files {
for (const Artist::pointer& artist : getOrCreateArtists(dbSession, trackInfo->artists, false)) for (const Artist::pointer& releaseArtist : getOrCreateArtists(dbSession, trackInfo->medium->release->artists, false))
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, artist, TrackArtistLinkType::Artist)); track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, releaseArtist, TrackArtistLinkType::ReleaseArtist));
}
if (trackInfo->medium && trackInfo->medium->release) // Allow fallbacks on artists with the same name even if they have MBID, since there is no tag to indicate the MBID of these artists
{ // We could ask MusicBrainz to get all the information, but that would heavily slow down the import process
for (const Artist::pointer& releaseArtist : getOrCreateArtists(dbSession, trackInfo->medium->release->artists, false)) for (const Artist::pointer& conductor : getOrCreateArtists(dbSession, trackInfo->conductorArtists, true))
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, releaseArtist, TrackArtistLinkType::ReleaseArtist)); track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, conductor, TrackArtistLinkType::Conductor));
}
// Allow fallbacks on artists with the same name even if they have MBID, since there is no tag to indicate the MBID of these artists for (const Artist::pointer& composer : getOrCreateArtists(dbSession, trackInfo->composerArtists, true))
// We could ask MusicBrainz to get all the information, but that would heavily slow down the import process track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, composer, TrackArtistLinkType::Composer));
for (const Artist::pointer& conductor : getOrCreateArtists(dbSession, trackInfo->conductorArtists, true))
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, conductor, TrackArtistLinkType::Conductor));
for (const Artist::pointer& composer : getOrCreateArtists(dbSession, trackInfo->composerArtists, true)) for (const Artist::pointer& lyricist : getOrCreateArtists(dbSession, trackInfo->lyricistArtists, true))
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, composer, TrackArtistLinkType::Composer)); track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, lyricist, TrackArtistLinkType::Lyricist));
for (const Artist::pointer& lyricist : getOrCreateArtists(dbSession, trackInfo->lyricistArtists, true)) for (const Artist::pointer& mixer : getOrCreateArtists(dbSession, trackInfo->mixerArtists, true))
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, lyricist, TrackArtistLinkType::Lyricist)); track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, mixer, TrackArtistLinkType::Mixer));
for (const Artist::pointer& mixer : getOrCreateArtists(dbSession, trackInfo->mixerArtists, true)) for (const auto& [role, performers] : trackInfo->performerArtists)
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, mixer, TrackArtistLinkType::Mixer)); {
for (const Artist::pointer& performer : getOrCreateArtists(dbSession, performers, true))
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, performer, TrackArtistLinkType::Performer, role));
}
for (const auto& [role, performers] : trackInfo->performerArtists) for (const Artist::pointer& producer : getOrCreateArtists(dbSession, trackInfo->producerArtists, true))
{ track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, producer, TrackArtistLinkType::Producer));
for (const Artist::pointer& performer : getOrCreateArtists(dbSession, performers, true))
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, performer, TrackArtistLinkType::Performer, role));
}
for (const Artist::pointer& producer : getOrCreateArtists(dbSession, trackInfo->producerArtists, true)) for (const Artist::pointer& remixer : getOrCreateArtists(dbSession, trackInfo->remixerArtists, true))
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, producer, TrackArtistLinkType::Producer)); track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, remixer, TrackArtistLinkType::Remixer));
for (const Artist::pointer& remixer : getOrCreateArtists(dbSession, trackInfo->remixerArtists, true)) track.modify()->setScanVersion(_settings.scanVersion);
track.modify()->addArtistLink(TrackArtistLink::create(dbSession, track, remixer, TrackArtistLinkType::Remixer)); if (trackInfo->medium && trackInfo->medium->release)
track.modify()->setRelease(getOrCreateRelease(dbSession, *trackInfo->medium->release));
else
track.modify()->setRelease({});
track.modify()->setTotalTrack(trackInfo->medium ? trackInfo->medium->trackCount : std::nullopt);
track.modify()->setReleaseReplayGain(trackInfo->medium ? trackInfo->medium->replayGain : std::nullopt);
track.modify()->setDiscSubtitle(trackInfo->medium ? trackInfo->medium->name : "");
track.modify()->setClusters(getOrCreateClusters(dbSession, trackInfo->tags));
track.modify()->setLastWriteTime(lastWriteTime);
track.modify()->setName(title);
track.modify()->setDuration(trackInfo->duration);
track.modify()->setBitrate(trackInfo->bitrate);
track.modify()->setAddedTime(Wt::WDateTime::currentDateTime());
track.modify()->setTrackNumber(trackInfo->position);
track.modify()->setDiscNumber(trackInfo->medium ? trackInfo->medium->position : std::nullopt);
track.modify()->setDate(trackInfo->date);
track.modify()->setOriginalDate(trackInfo->originalDate);
track.modify()->setScanVersion(_settings.scanVersion); // If a file has an OriginalYear but no Year, set it to ease filtering
if (trackInfo->medium && trackInfo->medium->release) if (!trackInfo->date.isValid() && trackInfo->originalDate.isValid())
track.modify()->setRelease(getOrCreateRelease(dbSession, *trackInfo->medium->release)); track.modify()->setDate(trackInfo->originalDate);
else
track.modify()->setRelease({});
track.modify()->setTotalTrack(trackInfo->medium ? trackInfo->medium->trackCount : std::nullopt);
track.modify()->setReleaseReplayGain(trackInfo->medium ? trackInfo->medium->replayGain : std::nullopt);
track.modify()->setDiscSubtitle(trackInfo->medium ? trackInfo->medium->name : "");
track.modify()->setClusters(getOrCreateClusters(dbSession, trackInfo->tags));
track.modify()->setLastWriteTime(lastWriteTime);
track.modify()->setName(title);
track.modify()->setDuration(trackInfo->duration);
track.modify()->setAddedTime(Wt::WDateTime::currentDateTime());
track.modify()->setTrackNumber(trackInfo->position);
track.modify()->setDiscNumber(trackInfo->medium ? trackInfo->medium->position : std::nullopt);
track.modify()->setDate(trackInfo->date);
track.modify()->setOriginalDate(trackInfo->originalDate);
// If a file has an OriginalYear but no Year, set it to ease filtering track.modify()->setRecordingMBID(trackInfo->recordingMBID);
if (!trackInfo->date.isValid() && trackInfo->originalDate.isValid()) track.modify()->setTrackMBID(trackInfo->mbid);
track.modify()->setDate(trackInfo->originalDate); if (auto trackFeatures{ TrackFeatures::find(dbSession, track->getId()) })
trackFeatures.remove(); // TODO: only if MBID changed?
track.modify()->setRecordingMBID(trackInfo->recordingMBID); track.modify()->setHasCover(trackInfo->hasCover);
track.modify()->setTrackMBID(trackInfo->mbid); track.modify()->setCopyright(trackInfo->copyright);
if (auto trackFeatures {TrackFeatures::find(dbSession, track->getId())}) track.modify()->setCopyrightURL(trackInfo->copyrightURL);
trackFeatures.remove(); // TODO: only if MBID changed? track.modify()->setTrackReplayGain(trackInfo->replayGain);
track.modify()->setHasCover(trackInfo->hasCover); track.modify()->setArtistDisplayName(trackInfo->artistDisplayName);
track.modify()->setCopyright(trackInfo->copyright); }
track.modify()->setCopyrightURL(trackInfo->copyrightURL);
track.modify()->setTrackReplayGain(trackInfo->replayGain);
track.modify()->setArtistDisplayName(trackInfo->artistDisplayName);
}
} }
+356 -381
View File
@@ -25,7 +25,6 @@
#include "services/database/Cluster.hpp" #include "services/database/Cluster.hpp"
#include "services/database/TrackFeatures.hpp" #include "services/database/TrackFeatures.hpp"
#include "services/database/ScanSettings.hpp" #include "services/database/ScanSettings.hpp"
#include "services/recommendation/IRecommendationService.hpp"
#include "utils/Exception.hpp" #include "utils/Exception.hpp"
#include "utils/IConfig.hpp" #include "utils/IConfig.hpp"
#include "utils/Logger.hpp" #include "utils/Logger.hpp"
@@ -36,387 +35,363 @@
#include "ScanStepDiscoverFiles.hpp" #include "ScanStepDiscoverFiles.hpp"
#include "ScanStepRemoveOrphanDbFiles.hpp" #include "ScanStepRemoveOrphanDbFiles.hpp"
#include "ScanStepScanFiles.hpp" #include "ScanStepScanFiles.hpp"
#include "ScanStepComputeClusterStats.hpp"
using namespace Database; namespace Scanner
namespace {
Wt::WDate
getNextMonday(Wt::WDate current)
{ {
do using namespace Database;
{
current = current.addDays(1); namespace
} while (current.dayOfWeek() != 1); {
Wt::WDate getNextMonday(Wt::WDate current)
return current; {
} do
{
Wt::WDate current = current.addDays(1);
getNextFirstOfMonth(Wt::WDate current) } while (current.dayOfWeek() != 1);
{
do return current;
{ }
current = current.addDays(1);
} while (current.day() != 1); Wt::WDate getNextFirstOfMonth(Wt::WDate current)
{
return current; do
} {
current = current.addDays(1);
} // namespace } while (current.day() != 1);
namespace Scanner { return current;
}
std::unique_ptr<IScannerService> } // namespace
createScannerService(Db& db, Recommendation::IRecommendationService& recommendationService)
{ std::unique_ptr<IScannerService> createScannerService(Db& db)
return std::make_unique<ScannerService>(db, recommendationService); {
} return std::make_unique<ScannerService>(db);
}
ScannerService::ScannerService(Db& db, Recommendation::IRecommendationService& recommendationService)
: _recommendationService {recommendationService} ScannerService::ScannerService(Db& db)
, _db {db} : _db{ db }
, _dbSession {db} , _dbSession{ db }
{ {
_ioService.setThreadCount(1); _ioService.setThreadCount(1);
refreshScanSettings(); refreshScanSettings();
start(); start();
} }
ScannerService::~ScannerService() ScannerService::~ScannerService()
{ {
LMS_LOG(DBUPDATER, INFO) << "Stopping service..."; LMS_LOG(DBUPDATER, INFO) << "Stopping service...";
stop(); stop();
LMS_LOG(DBUPDATER, INFO) << "Service stopped!"; LMS_LOG(DBUPDATER, INFO) << "Service stopped!";
} }
void void ScannerService::start()
ScannerService::start() {
{ std::scoped_lock lock{ _controlMutex };
std::scoped_lock lock {_controlMutex};
_ioService.post([this]
_ioService.post([this] {
{ if (_abortScan)
if (_abortScan) return;
return;
scheduleNextScan();
_recommendationService.load(false, });
[](const Recommendation::Progress& progress)
{ _ioService.start();
LMS_LOG(DBUPDATER, DEBUG) << "Reloading recommendation : " << progress.processedElems << "/" << progress.totalElems; }
});
scheduleNextScan(); void ScannerService::stop()
}); {
std::scoped_lock lock{ _controlMutex };
_ioService.start();
} _abortScan = true;
_scheduleTimer.cancel();
void _ioService.stop();
ScannerService::stop() }
{
std::scoped_lock lock {_controlMutex}; void ScannerService::abortScan()
{
_abortScan = true; LMS_LOG(DBUPDATER, DEBUG) << "Aborting scan...";
_scheduleTimer.cancel(); std::scoped_lock lock{ _controlMutex };
_recommendationService.cancelLoad();
_ioService.stop(); LMS_LOG(DBUPDATER, DEBUG) << "Waiting for the scan to abort...";
}
_abortScan = true;
void _scheduleTimer.cancel();
ScannerService::abortScan() _ioService.stop();
{ LMS_LOG(DBUPDATER, DEBUG) << "Scan abort done!";
LMS_LOG(DBUPDATER, DEBUG) << "Aborting scan...";
std::scoped_lock lock {_controlMutex}; _abortScan = false;
_ioService.start();
LMS_LOG(DBUPDATER, DEBUG) << "Waiting for the scan to abort..."; }
_abortScan = true; void ScannerService::requestImmediateScan(bool force)
_scheduleTimer.cancel(); {
_recommendationService.cancelLoad(); abortScan();
_ioService.stop(); _ioService.post([=]()
LMS_LOG(DBUPDATER, DEBUG) << "Scan abort done!"; {
if (_abortScan)
_abortScan = false; return;
_ioService.start();
} scheduleScan(force);
});
void }
ScannerService::requestImmediateScan(bool force)
{ void ScannerService::requestReload()
abortScan(); {
_ioService.post([=]() abortScan();
{ _ioService.post([=]()
if (_abortScan) {
return; if (_abortScan)
return;
scheduleScan(force);
}); scheduleNextScan();
} });
}
void
ScannerService::requestReload() ScannerService::Status ScannerService::getStatus() const
{ {
abortScan(); Status res;
_ioService.post([=]()
{ std::shared_lock lock{ _statusMutex };
if (_abortScan)
return; res.currentState = _curState;
res.nextScheduledScan = _nextScheduledScan;
scheduleNextScan(); res.lastCompleteScanStats = _lastCompleteScanStats;
}); res.currentScanStepStats = _currentScanStepStats;
}
return res;
ScannerService::Status }
ScannerService::getStatus() const
{ void ScannerService::scheduleNextScan()
Status res; {
LMS_LOG(DBUPDATER, DEBUG) << "Scheduling next scan";
std::shared_lock lock {_statusMutex};
refreshScanSettings();
res.currentState = _curState;
res.nextScheduledScan = _nextScheduledScan; const Wt::WDateTime now{ Wt::WDateTime::currentDateTime() };
res.lastCompleteScanStats = _lastCompleteScanStats;
res.currentScanStepStats = _currentScanStepStats; Wt::WDateTime nextScanDateTime;
switch (_settings.updatePeriod)
return res; {
} case ScanSettings::UpdatePeriod::Daily:
if (now.time() < _settings.startTime)
void nextScanDateTime = { now.date(), _settings.startTime };
ScannerService::scheduleNextScan() else
{ nextScanDateTime = { now.date().addDays(1), _settings.startTime };
LMS_LOG(DBUPDATER, DEBUG) << "Scheduling next scan"; break;
refreshScanSettings(); case ScanSettings::UpdatePeriod::Weekly:
if (now.time() < _settings.startTime && now.date().dayOfWeek() == 1)
const Wt::WDateTime now {Wt::WDateTime::currentDateTime()}; nextScanDateTime = { now.date(), _settings.startTime };
else
Wt::WDateTime nextScanDateTime; nextScanDateTime = { getNextMonday(now.date()), _settings.startTime };
switch (_settings.updatePeriod) break;
{
case ScanSettings::UpdatePeriod::Daily: case ScanSettings::UpdatePeriod::Monthly:
if (now.time() < _settings.startTime) if (now.time() < _settings.startTime && now.date().day() == 1)
nextScanDateTime = {now.date(), _settings.startTime}; nextScanDateTime = { now.date(), _settings.startTime };
else else
nextScanDateTime = {now.date().addDays(1), _settings.startTime}; nextScanDateTime = { getNextFirstOfMonth(now.date()), _settings.startTime };
break; break;
case ScanSettings::UpdatePeriod::Weekly: case ScanSettings::UpdatePeriod::Hourly:
if (now.time() < _settings.startTime && now.date().dayOfWeek() == 1) nextScanDateTime = { now.date(), now.time().addSecs(3600) };
nextScanDateTime = {now.date(), _settings.startTime}; break;
else
nextScanDateTime = {getNextMonday(now.date()), _settings.startTime}; case ScanSettings::UpdatePeriod::Never:
break; LMS_LOG(DBUPDATER, INFO) << "Auto scan disabled!";
break;
case ScanSettings::UpdatePeriod::Monthly: }
if (now.time() < _settings.startTime && now.date().day() == 1)
nextScanDateTime = {now.date(), _settings.startTime}; if (nextScanDateTime.isValid())
else scheduleScan(false, nextScanDateTime);
nextScanDateTime = {getNextFirstOfMonth(now.date()), _settings.startTime};
break; {
std::unique_lock lock{ _statusMutex };
case ScanSettings::UpdatePeriod::Hourly: _curState = nextScanDateTime.isValid() ? State::Scheduled : State::NotScheduled;
nextScanDateTime = {now.date(), now.time().addSecs(3600)}; _nextScheduledScan = nextScanDateTime;
break; }
case ScanSettings::UpdatePeriod::Never: _events.scanScheduled.emit(_nextScheduledScan);
LMS_LOG(DBUPDATER, INFO) << "Auto scan disabled!"; }
break;
} void ScannerService::scheduleScan(bool force, const Wt::WDateTime& dateTime)
{
if (nextScanDateTime.isValid()) auto cb{ [=](boost::system::error_code ec)
scheduleScan(false, nextScanDateTime); {
if (ec)
{ return;
std::unique_lock lock {_statusMutex};
_curState = nextScanDateTime.isValid() ? State::Scheduled : State::NotScheduled; scan(force);
_nextScheduledScan = nextScanDateTime; } };
}
if (dateTime.isNull())
_events.scanScheduled.emit(_nextScheduledScan); {
} LMS_LOG(DBUPDATER, INFO) << "Scheduling next scan right now";
_scheduleTimer.expires_from_now(std::chrono::seconds{ 0 });
void _scheduleTimer.async_wait(cb);
ScannerService::scheduleScan(bool force, const Wt::WDateTime& dateTime) }
{ else
auto cb {[=](boost::system::error_code ec) {
{ std::chrono::system_clock::time_point timePoint{ dateTime.toTimePoint() };
if (ec) std::time_t t{ std::chrono::system_clock::to_time_t(timePoint) };
return; char ctimeStr[26];
scan(force); LMS_LOG(DBUPDATER, INFO) << "Scheduling next scan at " << std::string(::ctime_r(&t, ctimeStr));
}}; _scheduleTimer.expires_at(timePoint);
_scheduleTimer.async_wait(cb);
if (dateTime.isNull()) }
{ }
LMS_LOG(DBUPDATER, INFO) << "Scheduling next scan right now";
_scheduleTimer.expires_from_now(std::chrono::seconds {0}); void ScannerService::scan(bool forceScan)
_scheduleTimer.async_wait(cb); {
} _events.scanStarted.emit();
else
{ {
std::chrono::system_clock::time_point timePoint {dateTime.toTimePoint()}; std::unique_lock lock{ _statusMutex };
std::time_t t {std::chrono::system_clock::to_time_t(timePoint)}; _curState = State::InProgress;
char ctimeStr[26]; _nextScheduledScan = {};
}
LMS_LOG(DBUPDATER, INFO) << "Scheduling next scan at " << std::string(::ctime_r(&t, ctimeStr));
_scheduleTimer.expires_at(timePoint);
_scheduleTimer.async_wait(cb); LMS_LOG(UI, INFO) << "New scan started!";
}
} refreshScanSettings();
void IScanStep::ScanContext scanContext{ _settings.mediaDirectory, forceScan, ScanStats {}, ScanStepStats {} };
ScannerService::scan(bool forceScan) ScanStats& stats{ scanContext.stats };
{ stats.startTime = Wt::WDateTime::currentDateTime();
_events.scanStarted.emit();
for (auto& scanStep : _scanSteps)
{ {
std::unique_lock lock {_statusMutex}; LMS_LOG(DBUPDATER, DEBUG) << "Starting scan step '" << scanStep->getStepName() << "'";
_curState = State::InProgress; scanContext.currentStepStats = ScanStepStats{ Wt::WDateTime::currentDateTime(), scanStep->getStep() };
_nextScheduledScan = {};
} notifyInProgress(scanContext.currentStepStats);
scanStep->process(scanContext);
notifyInProgress(scanContext.currentStepStats);
LMS_LOG(UI, INFO) << "New scan started!"; LMS_LOG(DBUPDATER, DEBUG) << "Completed scan step '" << scanStep->getStepName() << "'";
}
refreshScanSettings();
LMS_LOG(DBUPDATER, INFO) << "Scan " << (_abortScan ? "aborted" : "complete") << ". Changes = " << stats.nbChanges() << " (added = " << stats.additions << ", removed = " << stats.deletions << ", updated = " << stats.updates << "), Not changed = " << stats.skips << ", Scanned = " << stats.scans << " (errors = " << stats.errors.size() << "), features fetched = " << stats.featuresFetched << ", duplicates = " << stats.duplicates.size();
IScanStep::ScanContext scanContext {_settings.mediaDirectory, forceScan, ScanStats {}, ScanStepStats {}};
ScanStats& stats {scanContext.stats}; _dbSession.analyze();
stats.startTime = Wt::WDateTime::currentDateTime();
if (!_abortScan)
for (auto& scanStep : _scanSteps) {
{ stats.stopTime = Wt::WDateTime::currentDateTime();
LMS_LOG(DBUPDATER, DEBUG) << "Starting scan step '" << scanStep->getStepName() << "'"; {
scanContext.currentStepStats = ScanStepStats {Wt::WDateTime::currentDateTime(), scanStep->getStep()}; std::unique_lock lock{ _statusMutex };
notifyInProgress(scanContext.currentStepStats); _lastCompleteScanStats = stats;
scanStep->process(scanContext); _currentScanStepStats.reset();
notifyInProgress(scanContext.currentStepStats); }
LMS_LOG(DBUPDATER, DEBUG) << "Completed scan step '" << scanStep->getStepName() << "'";
} LMS_LOG(DBUPDATER, DEBUG) << "Scan not aborted, scheduling next scan!";
scheduleNextScan();
LMS_LOG(DBUPDATER, INFO) << "Scan " << (_abortScan ? "aborted" : "complete") << ". Changes = " << stats.nbChanges() << " (added = " << stats.additions << ", removed = " << stats.deletions << ", updated = " << stats.updates << "), Not changed = " << stats.skips << ", Scanned = " << stats.scans << " (errors = " << stats.errors.size() << "), features fetched = " << stats.featuresFetched << ", duplicates = " << stats.duplicates.size();
_events.scanComplete.emit(stats);
_dbSession.analyze(); }
else
if (!_abortScan) {
{ LMS_LOG(DBUPDATER, DEBUG) << "Scan aborted, not scheduling next scan!";
stats.stopTime = Wt::WDateTime::currentDateTime();
{ std::unique_lock lock{ _statusMutex };
std::unique_lock lock {_statusMutex};
_curState = State::NotScheduled;
_lastCompleteScanStats = stats; _currentScanStepStats.reset();
_currentScanStepStats.reset(); }
} }
LMS_LOG(DBUPDATER, DEBUG) << "Scan not aborted, scheduling next scan!"; void ScannerService::refreshScanSettings()
scheduleNextScan(); {
ScannerSettings newSettings{ readSettings() };
_events.scanComplete.emit(stats); if (_settings == newSettings)
} return;
else
{ LMS_LOG(DBUPDATER, DEBUG) << "Scanner settings updated";
LMS_LOG(DBUPDATER, DEBUG) << "Scan aborted, not scheduling next scan!"; LMS_LOG(DBUPDATER, DEBUG) << "skipDuplicateMBID = " << newSettings.skipDuplicateMBID;
LMS_LOG(DBUPDATER, DEBUG) << "Using scan settings version " << newSettings.scanVersion;
std::unique_lock lock {_statusMutex};
_settings = std::move(newSettings);
_curState = State::NotScheduled;
_currentScanStepStats.reset(); auto cbFunc{ [this](const ScanStepStats& stats)
} {
} notifyInProgressIfNeeded(stats);
} };
void
ScannerService::refreshScanSettings() ScanStepBase::InitParams params
{ {
ScannerSettings newSettings {readSettings()}; _settings,
if (_settings == newSettings) cbFunc,
return; _abortScan,
_db
LMS_LOG(DBUPDATER, DEBUG) << "Scanner settings updated"; };
LMS_LOG(DBUPDATER, DEBUG) << "skipDuplicateMBID = " << newSettings.skipDuplicateMBID;
LMS_LOG(DBUPDATER, DEBUG) << "Using scan settings version " << newSettings.scanVersion; _scanSteps.clear();
_scanSteps.push_back(std::make_unique<ScanStepDiscoverFiles>(params));
_settings = std::move(newSettings); _scanSteps.push_back(std::make_unique<ScanStepScanFiles>(params));
_scanSteps.push_back(std::make_unique<ScanStepRemoveOrphanDbFiles>(params));
auto cbFunc {[this](const ScanStepStats& stats) _scanSteps.push_back(std::make_unique<ScanStepComputeClusterStats>(params));
{ _scanSteps.push_back(std::make_unique<ScanStepCheckDuplicatedDbFiles>(params));
notifyInProgressIfNeeded(stats); }
}};
ScannerSettings ScannerService::readSettings()
ScanStepBase::InitParams params {
{ ScannerSettings newSettings;
_settings,
cbFunc, newSettings.skipDuplicateMBID = Service<IConfig>::get()->getBool("scanner-skip-duplicate-mbid", false);
_abortScan, {
_db auto transaction{ _dbSession.createSharedTransaction() };
};
const ScanSettings::pointer scanSettings{ ScanSettings::get(_dbSession) };
_scanSteps.clear();
_scanSteps.push_back(std::make_unique<ScanStepDiscoverFiles>(params)); newSettings.scanVersion = scanSettings->getScanVersion();
_scanSteps.push_back(std::make_unique<ScanStepScanFiles>(params)); newSettings.startTime = scanSettings->getUpdateStartTime();
_scanSteps.push_back(std::make_unique<ScanStepRemoveOrphanDbFiles>(params)); newSettings.updatePeriod = scanSettings->getUpdatePeriod();
_scanSteps.push_back(std::make_unique<ScanStepCheckDuplicatedDbFiles>(params));
} {
const auto fileExtensions{ scanSettings->getAudioFileExtensions() };
ScannerSettings newSettings.supportedExtensions.reserve(fileExtensions.size());
ScannerService::readSettings() std::transform(std::cbegin(fileExtensions), std::end(fileExtensions), std::back_inserter(newSettings.supportedExtensions),
{ [](const std::filesystem::path& extension) { return std::filesystem::path{ StringUtils::stringToLower(extension.string()) }; });
ScannerSettings newSettings; }
newSettings.mediaDirectory = scanSettings->getMediaDirectory();
newSettings.skipDuplicateMBID = Service<IConfig>::get()->getBool("scanner-skip-duplicate-mbid", false);
{ const auto clusterTypes = scanSettings->getClusterTypes();
auto transaction {_dbSession.createSharedTransaction()}; std::set<std::string> clusterTypeNames;
const ScanSettings::pointer scanSettings {ScanSettings::get(_dbSession)}; std::transform(std::cbegin(clusterTypes), std::cend(clusterTypes),
std::inserter(clusterTypeNames, clusterTypeNames.begin()),
newSettings.scanVersion = scanSettings->getScanVersion(); [](ClusterType::pointer clusterType) { return clusterType->getName(); });
newSettings.startTime = scanSettings->getUpdateStartTime();
newSettings.updatePeriod = scanSettings->getUpdatePeriod(); newSettings.clusterTypeNames = std::move(clusterTypeNames);
}
{
const auto fileExtensions {scanSettings->getAudioFileExtensions()}; return newSettings;
newSettings.supportedExtensions.reserve(fileExtensions.size()); }
std::transform(std::cbegin(fileExtensions), std::end(fileExtensions), std::back_inserter(newSettings.supportedExtensions),
[](const std::filesystem::path& extension) { return std::filesystem::path{ StringUtils::stringToLower(extension.string()) }; }); void ScannerService::notifyInProgress(const ScanStepStats& stepStats)
} {
newSettings.similarityServiceType = scanSettings->getSimilarityEngineType(); {
newSettings.mediaDirectory = scanSettings->getMediaDirectory(); std::unique_lock lock{ _statusMutex };
_currentScanStepStats = stepStats;
const auto clusterTypes = scanSettings->getClusterTypes(); }
std::set<std::string> clusterTypeNames;
const std::chrono::system_clock::time_point now{ std::chrono::system_clock::now() };
std::transform(std::cbegin(clusterTypes), std::cend(clusterTypes), _events.scanInProgress(stepStats);
std::inserter(clusterTypeNames, clusterTypeNames.begin()), _lastScanInProgressEmit = now;
[](ClusterType::pointer clusterType) { return clusterType->getName(); }); }
newSettings.clusterTypeNames = std::move(clusterTypeNames); void ScannerService::notifyInProgressIfNeeded(const ScanStepStats& stepStats)
} {
std::chrono::system_clock::time_point now{ std::chrono::system_clock::now() };
return newSettings;
} if (std::chrono::duration_cast<std::chrono::seconds>(now - _lastScanInProgressEmit).count() > 1)
notifyInProgress(stepStats);
void }
ScannerService::notifyInProgress(const ScanStepStats& stepStats)
{
{
std::unique_lock lock {_statusMutex};
_currentScanStepStats = stepStats;
}
const std::chrono::system_clock::time_point now {std::chrono::system_clock::now()};
_events.scanInProgress(stepStats);
_lastScanInProgressEmit = now;
}
void
ScannerService::notifyInProgressIfNeeded(const ScanStepStats& stepStats)
{
std::chrono::system_clock::time_point now {std::chrono::system_clock::now()};
if (std::chrono::duration_cast<std::chrono::seconds>(now - _lastScanInProgressEmit).count() > 1)
notifyInProgress(stepStats);
}
} // namespace Scanner } // namespace Scanner
@@ -38,73 +38,65 @@
#include "IScanStep.hpp" #include "IScanStep.hpp"
#include "ScannerSettings.hpp" #include "ScannerSettings.hpp"
namespace Recommendation
{
class IRecommendationService;
}
namespace Scanner namespace Scanner
{ {
class ScannerService : public IScannerService class ScannerService : public IScannerService
{ {
public: public:
ScannerService(Database::Db& db, Recommendation::IRecommendationService& recommendationService); ScannerService(Database::Db& db);
~ScannerService(); ~ScannerService();
ScannerService(const ScannerService&) = delete; ScannerService(const ScannerService&) = delete;
ScannerService(ScannerService&&) = delete; ScannerService& operator=(const ScannerService&) = delete;
ScannerService& operator=(const ScannerService&) = delete;
ScannerService& operator=(ScannerService&&) = delete;
void requestReload() override; void requestReload() override;
void requestImmediateScan(bool force) override; void requestImmediateScan(bool force) override;
Status getStatus() const override; Status getStatus() const override;
Events& getEvents() override { return _events; } Events& getEvents() override { return _events; }
private: private:
void start(); void start();
void stop(); void stop();
// Job handling // Job handling
void scheduleNextScan(); void scheduleNextScan();
void scheduleScan(bool force, const Wt::WDateTime& dateTime = {}); void scheduleScan(bool force, const Wt::WDateTime& dateTime = {});
void abortScan(); void abortScan();
// Update database (scheduled callback) // Update database (scheduled callback)
void scan(bool force); void scan(bool force);
void scanMediaDirectory( const std::filesystem::path& mediaDirectory, bool forceScan, ScanStats& stats); void scanMediaDirectory(const std::filesystem::path& mediaDirectory, bool forceScan, ScanStats& stats);
// Helpers // Helpers
void refreshScanSettings(); void refreshScanSettings();
ScannerSettings readSettings(); ScannerSettings readSettings();
void reloadRecommendationService();
void notifyInProgressIfNeeded(const ScanStepStats& stats); void notifyInProgressIfNeeded(const ScanStepStats& stats);
void notifyInProgress(const ScanStepStats& stats); void notifyInProgress(const ScanStepStats& stats);
void reloadSimilarityEngine(ScanStats& stats); void reloadSimilarityEngine(ScanStats& stats);
Recommendation::IRecommendationService& _recommendationService; std::vector<std::unique_ptr<IScanStep>> _scanSteps;
std::vector<std::unique_ptr<IScanStep>> _scanSteps; std::mutex _controlMutex;
bool _abortScan{};
Wt::WIOService _ioService;
boost::asio::system_timer _scheduleTimer{ _ioService };
Events _events;
std::chrono::system_clock::time_point _lastScanInProgressEmit{};
Database::Db& _db;
Database::Session _dbSession;
std::mutex _controlMutex; mutable std::shared_mutex _statusMutex;
bool _abortScan {}; State _curState{ State::NotScheduled };
Wt::WIOService _ioService; std::optional<ScanStats> _lastCompleteScanStats;
boost::asio::system_timer _scheduleTimer {_ioService}; std::optional<ScanStepStats> _currentScanStepStats;
Events _events; Wt::WDateTime _nextScheduledScan;
std::chrono::system_clock::time_point _lastScanInProgressEmit {};
Database::Db& _db;
Database::Session _dbSession;
mutable std::shared_mutex _statusMutex; ScannerSettings _settings;
State _curState {State::NotScheduled}; };
std::optional<ScanStats> _lastCompleteScanStats;
std::optional<ScanStepStats> _currentScanStepStats;
Wt::WDateTime _nextScheduledScan;
ScannerSettings _settings;
};
} // Scanner } // Scanner
@@ -34,7 +34,6 @@ namespace Scanner
Wt::WTime startTime; Wt::WTime startTime;
Database::ScanSettings::UpdatePeriod updatePeriod {Database::ScanSettings::UpdatePeriod::Never}; Database::ScanSettings::UpdatePeriod updatePeriod {Database::ScanSettings::UpdatePeriod::Never};
std::vector<std::filesystem::path> supportedExtensions; std::vector<std::filesystem::path> supportedExtensions;
Database::ScanSettings::SimilarityEngineType similarityServiceType;
std::filesystem::path mediaDirectory; std::filesystem::path mediaDirectory;
bool skipDuplicateMBID {}; bool skipDuplicateMBID {};
std::set<std::string> clusterTypeNames; std::set<std::string> clusterTypeNames;
@@ -45,7 +44,6 @@ namespace Scanner
&& startTime == rhs.startTime && startTime == rhs.startTime
&& updatePeriod == rhs.updatePeriod && updatePeriod == rhs.updatePeriod
&& supportedExtensions == rhs.supportedExtensions && supportedExtensions == rhs.supportedExtensions
&& similarityServiceType == rhs.similarityServiceType
&& mediaDirectory == rhs.mediaDirectory && mediaDirectory == rhs.mediaDirectory
&& skipDuplicateMBID == rhs.skipDuplicateMBID && skipDuplicateMBID == rhs.skipDuplicateMBID
&& clusterTypeNames == rhs.clusterTypeNames; && clusterTypeNames == rhs.clusterTypeNames;
@@ -29,11 +29,6 @@ namespace Database
class Db; class Db;
} }
namespace Recommendation
{
class IRecommendationService;
}
namespace Scanner namespace Scanner
{ {
@@ -66,7 +61,7 @@ namespace Scanner
virtual Events& getEvents() = 0; virtual Events& getEvents() = 0;
}; };
std::unique_ptr<IScannerService> createScannerService(Database::Db& db, Recommendation::IRecommendationService& recommendationEngine); std::unique_ptr<IScannerService> createScannerService(Database::Db& db);
} // Scanner } // Scanner
@@ -28,80 +28,81 @@
namespace Scanner namespace Scanner
{ {
enum class ScanErrorType enum class ScanErrorType
{ {
CannotReadFile, // cannot read file CannotReadFile, // cannot read file
CannotParseFile, // cannot parse file CannotParseFile, // cannot parse file
NoAudioTrack, // no audio track found NoAudioTrack, // no audio track found
BadDuration, // bad duration BadDuration, // bad duration
}; };
enum class DuplicateReason enum class DuplicateReason
{ {
SameHash, SameHash,
SameTrackMBID, SameTrackMBID,
}; };
struct ScanError struct ScanError
{ {
std::filesystem::path file; std::filesystem::path file;
ScanErrorType error; ScanErrorType error;
std::string systemError; std::string systemError;
ScanError(const std::filesystem::path& file, ScanErrorType error, const std::string& systemError = ""); ScanError(const std::filesystem::path& file, ScanErrorType error, const std::string& systemError = "");
}; };
struct ScanDuplicate struct ScanDuplicate
{ {
Database::TrackId trackId; Database::TrackId trackId;
DuplicateReason reason; DuplicateReason reason;
}; };
enum class ScanStep enum class ScanStep
{ {
DiscoveringFiles, DiscoveringFiles,
ScanningFiles, ScanningFiles,
ChekingForMissingFiles, ChekingForMissingFiles,
CheckingForDuplicateFiles, CheckingForDuplicateFiles,
FetchingTrackFeatures, FetchingTrackFeatures,
ReloadingSimilarityEngine, ReloadingSimilarityEngine,
}; ComputeClusterStats,
static inline constexpr unsigned ScanProgressStepCount {5}; };
static inline constexpr unsigned ScanProgressStepCount{ 6 };
// reduced scan stats // reduced scan stats
struct ScanStepStats struct ScanStepStats
{ {
Wt::WDateTime startTime; Wt::WDateTime startTime;
ScanStep currentStep; ScanStep currentStep;
std::size_t totalElems {}; std::size_t totalElems{};
std::size_t processedElems {}; std::size_t processedElems{};
unsigned progress() const; unsigned progress() const;
}; };
struct ScanStats struct ScanStats
{ {
Wt::WDateTime startTime; Wt::WDateTime startTime;
Wt::WDateTime stopTime; Wt::WDateTime stopTime;
std::size_t filesScanned {}; // Total number of files scanned (estimated) std::size_t filesScanned{}; // Total number of files scanned (estimated)
std::size_t skips {}; // no change since last scan std::size_t skips{}; // no change since last scan
std::size_t scans {}; // actually scanned filed std::size_t scans{}; // actually scanned filed
std::size_t additions {}; // added in DB std::size_t additions{}; // added in DB
std::size_t deletions {}; // removed from DB std::size_t deletions{}; // removed from DB
std::size_t updates {}; // updated file in DB std::size_t updates{}; // updated file in DB
std::size_t featuresFetched {}; // features fetched in DB std::size_t featuresFetched{}; // features fetched in DB
std::vector<ScanError> errors; std::vector<ScanError> errors;
std::vector<ScanDuplicate> duplicates; std::vector<ScanDuplicate> duplicates;
std::size_t nbFiles() const; std::size_t nbFiles() const;
std::size_t nbChanges() const; std::size_t nbChanges() const;
}; };
} // namespace Scanner } // namespace Scanner
+1 -1
View File
@@ -31,7 +31,7 @@ namespace API::Subsonic
}; };
static inline constexpr ProtocolVersion defaultServerProtocolVersion{ 1, 16, 0 }; static inline constexpr ProtocolVersion defaultServerProtocolVersion{ 1, 16, 0 };
static inline constexpr std::string_view serverVersion{ "2" }; static inline constexpr std::string_view serverVersion{ "4" };
} }
namespace StringUtils namespace StringUtils
@@ -42,6 +42,7 @@ namespace API::Subsonic
ClientInfo clientInfo; ClientInfo clientInfo;
ProtocolVersion serverProtocolVersion; ProtocolVersion serverProtocolVersion;
bool enableOpenSubsonic{ true }; bool enableOpenSubsonic{ true };
bool enableDefaultCover{ };
}; };
} }
+31 -3
View File
@@ -66,7 +66,7 @@ namespace API::Subsonic
{ {
std::unordered_map<std::string, ProtocolVersion> res; std::unordered_map<std::string, ProtocolVersion> res;
Service<IConfig>::get()->visitStrings("api-subsonic-report-old-server-protocol", Service<IConfig>::get()->visitStrings("api-subsonic-old-server-protocol-clients",
[&](std::string_view client) [&](std::string_view client)
{ {
res.emplace(std::string{ client }, ProtocolVersion{ 1, 12, 0 }); res.emplace(std::string{ client }, ProtocolVersion{ 1, 12, 0 });
@@ -88,6 +88,19 @@ namespace API::Subsonic
return res; return res;
} }
std::unordered_set<std::string> readDefaultCoverClients()
{
std::unordered_set<std::string> res;
Service<IConfig>::get()->visitStrings("api-subsonic-default-cover-clients",
[&](std::string_view client)
{
res.emplace(std::string{ client });
}, { "DSub", "substreamer" });
return res;
}
std::string parameterMapToDebugString(const Wt::Http::ParameterMap& parameterMap) std::string parameterMapToDebugString(const Wt::Http::ParameterMap& parameterMap)
{ {
auto censorValue = [](const std::string& type, const std::string& value) -> std::string auto censorValue = [](const std::string& type, const std::string& value) -> std::string
@@ -266,6 +279,7 @@ namespace API::Subsonic
SubsonicResource::SubsonicResource(Db& db) SubsonicResource::SubsonicResource(Db& db)
: _serverProtocolVersionsByClient{ readConfigProtocolVersions() } : _serverProtocolVersionsByClient{ readConfigProtocolVersions() }
, _openSubsonicDisabledClients{ readOpenSubsonicDisabledClients() } , _openSubsonicDisabledClients{ readOpenSubsonicDisabledClients() }
, _defaultCoverClients{ readDefaultCoverClients() }
, _db{ db } , _db{ db }
{ {
} }
@@ -378,12 +392,26 @@ namespace API::Subsonic
const ClientInfo clientInfo{ getClientInfo(parameters) }; const ClientInfo clientInfo{ getClientInfo(parameters) };
const Database::UserId userId{ authenticateUser(request, clientInfo) }; const Database::UserId userId{ authenticateUser(request, clientInfo) };
bool enableOpenSubsonic{ _openSubsonicDisabledClients.find(clientInfo.name) == std::cend(_openSubsonicDisabledClients) }; bool enableOpenSubsonic{ _openSubsonicDisabledClients.find(clientInfo.name) == std::cend(_openSubsonicDisabledClients) };
bool enableDefaultCover{ _defaultCoverClients.find(clientInfo.name) != std::cend(_openSubsonicDisabledClients) };
return { parameters, _db.getTLSSession(), userId, clientInfo, getServerProtocolVersion(clientInfo.name), enableOpenSubsonic }; return { parameters, _db.getTLSSession(), userId, clientInfo, getServerProtocolVersion(clientInfo.name), enableOpenSubsonic, enableDefaultCover };
} }
Database::UserId SubsonicResource::authenticateUser(const Wt::Http::Request& request, const ClientInfo& clientInfo) Database::UserId SubsonicResource::authenticateUser(const Wt::Http::Request& request, const ClientInfo& clientInfo)
{ {
// if the request if a continuation, the user is already authenticated
if (request.continuation())
{
Database::Session& session{ _db.getTLSSession() };
auto transaction{ session.createSharedTransaction() };
const auto user{ Database::User::find(session, clientInfo.user) };
if (!user)
throw UserNotAuthorizedError{};
return user->getId();
}
if (auto * authEnvService{ Service<::Auth::IEnvService>::get() }) if (auto * authEnvService{ Service<::Auth::IEnvService>::get() })
{ {
const auto checkResult{ authEnvService->processRequest(request) }; const auto checkResult{ authEnvService->processRequest(request) };
@@ -408,7 +436,7 @@ namespace API::Subsonic
} }
} }
throw InternalErrorGenericError{ "No service avalaible to authenticate user" }; throw InternalErrorGenericError{ "No service available to authenticate user" };
} }
} // namespace api::subsonic } // namespace api::subsonic
@@ -53,6 +53,8 @@ namespace API::Subsonic
const std::unordered_map<std::string, ProtocolVersion> _serverProtocolVersionsByClient; const std::unordered_map<std::string, ProtocolVersion> _serverProtocolVersionsByClient;
const std::unordered_set<std::string> _openSubsonicDisabledClients; const std::unordered_set<std::string> _openSubsonicDisabledClients;
const std::unordered_set<std::string> _defaultCoverClients;
Database::Db& _db; Database::Db& _db;
}; };
@@ -29,6 +29,8 @@
namespace API::Subsonic namespace API::Subsonic
{ {
// Max count expected from all API methods that expose a count
static inline constexpr std::size_t defaultMaxCountSize{ 1000 };
enum class ResponseFormat enum class ResponseFormat
{ {
@@ -185,6 +187,18 @@ namespace API::Subsonic
const std::string _parameterName; const std::string _parameterName;
}; };
class ParameterValueTooHighGenericError : public GenericError
{
public:
ParameterValueTooHighGenericError(std::string_view parameterName, std::size_t max) : _parameterName{ parameterName }, _max{ max } {}
private:
std::string getMessage() const override { return "Parameter '" + _parameterName + "': bad value (max is " + std::to_string(_max) + ")"; }
const std::string _parameterName;
std::size_t _max;
};
class Response class Response
{ {
public: public:
@@ -47,6 +47,8 @@ namespace API::Subsonic
// Optional params // Optional params
const std::size_t size{ getParameterAs<std::size_t>(context.parameters, "size").value_or(10) }; const std::size_t size{ getParameterAs<std::size_t>(context.parameters, "size").value_or(10) };
const std::size_t offset{ getParameterAs<std::size_t>(context.parameters, "offset").value_or(0) }; const std::size_t offset{ getParameterAs<std::size_t>(context.parameters, "offset").value_or(0) };
if (size > defaultMaxCountSize)
throw ParameterValueTooHighGenericError{ "size", defaultMaxCountSize };
const Range range{ offset, size }; const Range range{ offset, size };
@@ -120,7 +122,7 @@ namespace API::Subsonic
// (no seed provided by subsonic, ot it would require to store some kind of context for each user/client when iterating over the random albums) // (no seed provided by subsonic, ot it would require to store some kind of context for each user/client when iterating over the random albums)
Release::FindParameters params; Release::FindParameters params;
params.setSortMethod(ReleaseSortMethod::Random); params.setSortMethod(ReleaseSortMethod::Random);
params.setRange({ 0, size }); params.setRange(Range{ 0, size });
releases = Release::findIds(context.dbSession, params); releases = Release::findIds(context.dbSession, params);
} }
@@ -130,10 +132,15 @@ namespace API::Subsonic
} }
else if (type == "starred") else if (type == "starred")
{ {
releases = feedbackService.getStarredReleases(context.userId, {}, range); Feedback::IFeedbackService::FindParameters params;
params.setUser(context.userId);
params.setRange(range);
releases = feedbackService.findStarredReleases(params);
} }
else else
{
throw NotImplementedGenericError{}; throw NotImplementedGenericError{};
}
Response response{ Response::createOkResponse(context.serverProtocolVersion) }; Response response{ Response::createOkResponse(context.serverProtocolVersion) };
Response::Node& albumListNode{ response.createNode(id3 ? Response::Node::Key{ "albumList2" } : Response::Node::Key{ "albumList" }) }; Response::Node& albumListNode{ response.createNode(id3 ? Response::Node::Key{ "albumList2" } : Response::Node::Key{ "albumList" }) };
@@ -160,19 +167,27 @@ namespace API::Subsonic
Feedback::IFeedbackService& feedbackService{ *Service<Feedback::IFeedbackService>::get() }; Feedback::IFeedbackService& feedbackService{ *Service<Feedback::IFeedbackService>::get() };
for (const ArtistId artistId : feedbackService.getStarredArtists(context.userId, {} /* clusters */, std::nullopt /* linkType */, ArtistSortMethod::BySortName, Range{}).results) Feedback::IFeedbackService::FindParameters findParameters;
findParameters.setUser(context.userId);
{ {
if (auto artist{ Artist::find(context.dbSession, artistId) }) Feedback::IFeedbackService::ArtistFindParameters artistFindParams;
starredNode.addArrayChild("artist", createArtistNode(context, artist, user, id3)); artistFindParams.setUser(context.userId);
artistFindParams.setSortMethod(ArtistSortMethod::BySortName);
for (const ArtistId artistId : feedbackService.findStarredArtists(artistFindParams).results)
{
if (auto artist{ Artist::find(context.dbSession, artistId) })
starredNode.addArrayChild("artist", createArtistNode(context, artist, user, id3));
}
} }
for (const ReleaseId releaseId : feedbackService.getStarredReleases(context.userId, {} /* clusters */, Range{}).results) for (const ReleaseId releaseId : feedbackService.findStarredReleases(findParameters).results)
{ {
if (auto release{ Release::find(context.dbSession, releaseId) }) if (auto release{ Release::find(context.dbSession, releaseId) })
starredNode.addArrayChild("album", createAlbumNode(context, release, user, id3)); starredNode.addArrayChild("album", createAlbumNode(context, release, user, id3));
} }
for (const TrackId trackId : feedbackService.getStarredTracks(context.userId, {} /* clusters */, Range{}).results) for (const TrackId trackId : feedbackService.findStarredTracks(findParameters).results)
{ {
if (auto track{ Track::find(context.dbSession, trackId) }) if (auto track{ Track::find(context.dbSession, trackId) })
starredNode.addArrayChild("song", createSongNode(context, track, user)); starredNode.addArrayChild("song", createSongNode(context, track, user));
@@ -196,7 +211,8 @@ namespace API::Subsonic
{ {
// Optional params // Optional params
std::size_t size{ getParameterAs<std::size_t>(context.parameters, "size").value_or(50) }; std::size_t size{ getParameterAs<std::size_t>(context.parameters, "size").value_or(50) };
size = std::min(size, std::size_t{ 500 }); if (size > defaultMaxCountSize)
throw ParameterValueTooHighGenericError{ "size", defaultMaxCountSize };
auto transaction{ context.dbSession.createSharedTransaction() }; auto transaction{ context.dbSession.createSharedTransaction() };
@@ -204,13 +220,17 @@ namespace API::Subsonic
if (!user) if (!user)
throw UserNotAuthorizedError{}; throw UserNotAuthorizedError{};
const auto tracks{ Track::find(context.dbSession, Track::FindParameters {}.setSortMethod(TrackSortMethod::Random).setRange({0, size})) };
Response response{ Response::createOkResponse(context.serverProtocolVersion) }; Response response{ Response::createOkResponse(context.serverProtocolVersion) };
Response::Node& randomSongsNode{ response.createNode("randomSongs") }; Response::Node& randomSongsNode{ response.createNode("randomSongs") };
for (const Track::pointer& track : tracks.results)
randomSongsNode.addArrayChild("song", createSongNode(context, track, user)); Track::FindParameters params;
params.setSortMethod(TrackSortMethod::Random);
params.setRange(Range{ 0, size });
Track::find(context.dbSession, params, [&](const Track::pointer& track)
{
randomSongsNode.addArrayChild("song", createSongNode(context, track, user));
});
return response; return response;
} }
@@ -221,8 +241,9 @@ namespace API::Subsonic
std::string genre{ getMandatoryParameterAs<std::string>(context.parameters, "genre") }; std::string genre{ getMandatoryParameterAs<std::string>(context.parameters, "genre") };
// Optional params // Optional params
std::size_t size{ getParameterAs<std::size_t>(context.parameters, "count").value_or(10) }; std::size_t count{ getParameterAs<std::size_t>(context.parameters, "count").value_or(10) };
size = std::min(size, std::size_t{ 500 }); if (count > defaultMaxCountSize)
throw ParameterValueTooHighGenericError{"count", defaultMaxCountSize};
std::size_t offset{ getParameterAs<std::size_t>(context.parameters, "offset").value_or(0) }; std::size_t offset{ getParameterAs<std::size_t>(context.parameters, "offset").value_or(0) };
@@ -245,11 +266,12 @@ namespace API::Subsonic
Track::FindParameters params; Track::FindParameters params;
params.setClusters({ cluster->getId() }); params.setClusters({ cluster->getId() });
params.setRange({ offset, size }); params.setRange(Range{ offset, count });
const auto tracks{ Track::find(context.dbSession, params) }; Track::find(context.dbSession, params, [&](const Track::pointer& track)
for (const Track::pointer& track : tracks.results) {
songsByGenreNode.addArrayChild("song", createSongNode(context, track, user)); songsByGenreNode.addArrayChild("song", createSongNode(context, track, user));
});
return response; return response;
} }
@@ -40,7 +40,7 @@ namespace API::Subsonic
if (!user) if (!user)
throw UserNotAuthorizedError{}; throw UserNotAuthorizedError{};
const auto bookmarkIds{ TrackBookmark::find(context.dbSession, user->getId(), Range {}) }; const auto bookmarkIds{ TrackBookmark::find(context.dbSession, user->getId()) };
Response response{ Response::createOkResponse(context.serverProtocolVersion) }; Response response{ Response::createOkResponse(context.serverProtocolVersion) };
Response::Node& bookmarksNode{ response.createNode("bookmarks") }; Response::Node& bookmarksNode{ response.createNode("bookmarks") };
+18 -12
View File
@@ -124,7 +124,7 @@ namespace API::Subsonic
// first pass: dispatch the artists by first letter // first pass: dispatch the artists by first letter
LMS_LOG(API_SUBSONIC, DEBUG) << "GetArtists: fetching all artists..."; LMS_LOG(API_SUBSONIC, DEBUG) << "GetArtists: fetching all artists...";
std::map<char, std::vector<ArtistId>> artistsSortedByFirstChar; std::map<char, std::vector<ArtistId>> artistsSortedByFirstChar;
std::size_t currentArtistOffset{0}; std::size_t currentArtistOffset{ 0 };
constexpr std::size_t batchSize{ 100 }; constexpr std::size_t batchSize{ 100 };
bool hasMoreArtists{ true }; bool hasMoreArtists{ true };
while (hasMoreArtists) while (hasMoreArtists)
@@ -190,7 +190,7 @@ namespace API::Subsonic
{ {
Track::FindParameters params; Track::FindParameters params;
params.setArtist(id); params.setArtist(id);
params.setRange({ 0, meanTrackCountPerArtist }); params.setRange(Range{ 0, meanTrackCountPerArtist });
params.setSortMethod(TrackSortMethod::Random); params.setSortMethod(TrackSortMethod::Random);
const auto artistTracks{ Track::findIds(context.dbSession, params) }; const auto artistTracks{ Track::findIds(context.dbSession, params) };
@@ -221,7 +221,7 @@ namespace API::Subsonic
{ {
Track::FindParameters params; Track::FindParameters params;
params.setRelease(id); params.setRelease(id);
params.setRange({ 0, meanTrackCountPerRelease }); params.setRange(Range{ 0, meanTrackCountPerRelease });
params.setSortMethod(TrackSortMethod::Random); params.setSortMethod(TrackSortMethod::Random);
const auto releaseTracks{ Track::findIds(context.dbSession, params) }; const auto releaseTracks{ Track::findIds(context.dbSession, params) };
@@ -242,6 +242,8 @@ namespace API::Subsonic
{ {
// Optional params // Optional params
std::size_t count{ getParameterAs<std::size_t>(context.parameters, "count").value_or(50) }; std::size_t count{ getParameterAs<std::size_t>(context.parameters, "count").value_or(50) };
if (count > defaultMaxCountSize)
throw ParameterValueTooHighGenericError{ "count", defaultMaxCountSize };
std::vector<TrackId> tracks; std::vector<TrackId> tracks;
@@ -316,9 +318,11 @@ namespace API::Subsonic
directoryNode.setAttribute("id", idToString(RootId{})); directoryNode.setAttribute("id", idToString(RootId{}));
directoryNode.setAttribute("name", "Music"); directoryNode.setAttribute("name", "Music");
const auto rootArtistIds{ Artist::find(context.dbSession, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::BySortName)) }; // TODO: this does not scale when a lot of artists are present
for (const Artist::pointer& artist : rootArtistIds.results) Artist::find(context.dbSession, Artist::FindParameters{}.setSortMethod(ArtistSortMethod::BySortName), [&](const Artist::pointer& artist)
directoryNode.addArrayChild("child", createArtistNode(context, artist, user, false /* no id3 */)); {
directoryNode.addArrayChild("child", createArtistNode(context, artist, user, false /* no id3 */));
});
} }
else if (artistId) else if (artistId)
{ {
@@ -330,9 +334,10 @@ namespace API::Subsonic
directoryNode.setAttribute("name", Utils::makeNameFilesystemCompatible(artist->getName())); directoryNode.setAttribute("name", Utils::makeNameFilesystemCompatible(artist->getName()));
const auto artistReleases{ Release::find(context.dbSession, Release::FindParameters {}.setArtist(*artistId)) }; Release::find(context.dbSession, Release::FindParameters{}.setArtist(*artistId), [&](const Release::pointer& release)
for (const Release::pointer& release : artistReleases.results) {
directoryNode.addArrayChild("child", createAlbumNode(context, release, user, false /* no id3 */)); directoryNode.addArrayChild("child", createAlbumNode(context, release, user, false /* no id3 */));
});
} }
else if (releaseId) else if (releaseId)
{ {
@@ -344,9 +349,10 @@ namespace API::Subsonic
directoryNode.setAttribute("name", Utils::makeNameFilesystemCompatible(release->getName())); directoryNode.setAttribute("name", Utils::makeNameFilesystemCompatible(release->getName()));
const auto tracks{ Track::find(context.dbSession, Track::FindParameters {}.setRelease(*releaseId).setSortMethod(TrackSortMethod::Release)) }; Track::find(context.dbSession, Track::FindParameters{}.setRelease(*releaseId).setSortMethod(TrackSortMethod::Release), [&](const Track::pointer& track)
for (const Track::pointer& track : tracks.results) {
directoryNode.addArrayChild("child", createSongNode(context, track, user)); directoryNode.addArrayChild("child", createSongNode(context, track, user));
});
} }
else else
throw BadParameterGenericError{ "id" }; throw BadParameterGenericError{ "id" };
@@ -20,8 +20,9 @@
#include "MediaRetrieval.hpp" #include "MediaRetrieval.hpp"
#include "av/IAudioFile.hpp" #include "av/IAudioFile.hpp"
#include "av/TranscodeParameters.hpp" #include "av/RawResourceHandlerCreator.hpp"
#include "av/TranscodeResourceHandlerCreator.hpp" #include "av/TranscodingParameters.hpp"
#include "av/TranscodingResourceHandlerCreator.hpp"
#include "av/Types.hpp" #include "av/Types.hpp"
#include "services/cover/ICoverService.hpp" #include "services/cover/ICoverService.hpp"
#include "services/database/Session.hpp" #include "services/database/Session.hpp"
@@ -39,101 +40,148 @@ namespace API::Subsonic
{ {
using namespace Database; using namespace Database;
namespace { namespace
std::optional<Av::Format> subsonicStreamFormatToAvFormat(std::string_view format) {
std::optional<Av::Transcoding::OutputFormat> subsonicStreamFormatToAvOutputFormat(std::string_view format)
{ {
for (const auto& [str, avFormat] : std::initializer_list<std::pair<std::string_view, Av::Format>>{ for (const auto& [str, avFormat] : std::initializer_list<std::pair<std::string_view, Av::Transcoding::OutputFormat>>{
{"mp3", Av::Format::MP3}, {"mp3", Av::Transcoding::OutputFormat::MP3},
{"opus", Av::Format::OGG_OPUS}, {"opus", Av::Transcoding::OutputFormat::OGG_OPUS},
{"vorbis", Av::Format::OGG_VORBIS}, {"vorbis", Av::Transcoding::OutputFormat::OGG_VORBIS},
}) })
{ {
if (StringUtils::stringCaseInsensitiveEqual("str", format)) if (StringUtils::stringCaseInsensitiveEqual(str, format))
return avFormat; return avFormat;
} }
return std::nullopt; return std::nullopt;
} }
Av::Format userTranscodeFormatToAvFormat(AudioFormat format) Av::Transcoding::OutputFormat userTranscodeFormatToAvFormat(Database::TranscodingOutputFormat format)
{ {
switch (format) switch (format)
{ {
case Database::AudioFormat::MP3: return Av::Format::MP3; case Database::TranscodingOutputFormat::MP3: return Av::Transcoding::OutputFormat::MP3;
case Database::AudioFormat::OGG_OPUS: return Av::Format::OGG_OPUS; case Database::TranscodingOutputFormat::OGG_OPUS: return Av::Transcoding::OutputFormat::OGG_OPUS;
case Database::AudioFormat::MATROSKA_OPUS: return Av::Format::MATROSKA_OPUS; case Database::TranscodingOutputFormat::MATROSKA_OPUS: return Av::Transcoding::OutputFormat::MATROSKA_OPUS;
case Database::AudioFormat::OGG_VORBIS: return Av::Format::OGG_VORBIS; case Database::TranscodingOutputFormat::OGG_VORBIS: return Av::Transcoding::OutputFormat::OGG_VORBIS;
case Database::AudioFormat::WEBM_VORBIS: return Av::Format::WEBM_VORBIS; case Database::TranscodingOutputFormat::WEBM_VORBIS: return Av::Transcoding::OutputFormat::WEBM_VORBIS;
} }
return Av::Format::OGG_OPUS; return Av::Transcoding::OutputFormat::OGG_OPUS;
}
bool isCodecCompatibleWithOutputFormat(Av::DecodingCodec codec, Av::Transcoding::OutputFormat outputFormat)
{
switch (outputFormat)
{
case Av::Transcoding::OutputFormat::MP3:
return codec == Av::DecodingCodec::MP3;
case Av::Transcoding::OutputFormat::OGG_OPUS:
case Av::Transcoding::OutputFormat::MATROSKA_OPUS:
return codec == Av::DecodingCodec::OPUS;
case Av::Transcoding::OutputFormat::OGG_VORBIS:
case Av::Transcoding::OutputFormat::WEBM_VORBIS:
return codec == Av::DecodingCodec::VORBIS;
}
return true;
} }
struct StreamParameters struct StreamParameters
{ {
Av::InputFileParameters inputFileParameters; Av::Transcoding::InputParameters inputParameters;
std::optional<Av::TranscodeParameters> transcodeParameters; std::optional<Av::Transcoding::OutputParameters> outputParameters;
bool estimateContentLength{}; bool estimateContentLength{};
}; };
bool isOutputFormatCompatible(const std::filesystem::path& trackPath, Av::Transcoding::OutputFormat outputFormat)
{
try
{
const auto audioFile{ Av::parseAudioFile(trackPath) };
const auto streamInfo{ audioFile->getBestStreamInfo() };
if (!streamInfo)
throw RequestedDataNotFoundError{}; // TODO 404?
return isCodecCompatibleWithOutputFormat(streamInfo->codec, outputFormat);
}
catch (const Av::Exception& e)
{
// TODO 404?
throw RequestedDataNotFoundError{};
}
}
StreamParameters getStreamParameters(RequestContext& context) StreamParameters getStreamParameters(RequestContext& context)
{ {
// Mandatory params // Mandatory params
const TrackId id{ getMandatoryParameterAs<TrackId>(context.parameters, "id") }; const TrackId id{ getMandatoryParameterAs<TrackId>(context.parameters, "id") };
// Optional params // Optional params
std::size_t maxBitRate{ getParameterAs<std::size_t>(context.parameters, "maxBitRate").value_or(0) }; // "If set to zero, no limit is imposed" std::size_t maxBitRate{ getParameterAs<std::size_t>(context.parameters, "maxBitRate").value_or(0) * 1000 }; // "If set to zero, no limit is imposed", given in kpbs
const std::string format{ getParameterAs<std::string>(context.parameters, "format").value_or("") }; const std::string format{ getParameterAs<std::string>(context.parameters, "format").value_or("") };
std::size_t timeOffset{ getParameterAs<std::size_t>(context.parameters, "timeOffset").value_or(0) }; std::size_t timeOffset{ getParameterAs<std::size_t>(context.parameters, "timeOffset").value_or(0) };
bool estimateContentLength{ getParameterAs<bool>(context.parameters, "estimateContentLength").value_or(false) }; bool estimateContentLength{ getParameterAs<bool>(context.parameters, "estimateContentLength").value_or(false) };
StreamParameters parameters; StreamParameters parameters;
parameters.estimateContentLength = estimateContentLength;
auto transaction{ context.dbSession.createSharedTransaction() }; auto transaction{ context.dbSession.createSharedTransaction() };
{
const auto track{ Track::find(context.dbSession, id) };
if (!track)
throw RequestedDataNotFoundError{};
parameters.inputFileParameters.trackPath = track->getPath();
parameters.inputFileParameters.duration = track->getDuration();
}
if (format == "raw") // raw => no transcode
return parameters;
const auto audioFile{ Av::parseAudioFile(parameters.inputFileParameters.trackPath) };
// check if transcode is really needed or not
// same format as requested, bitrate is lower than requested => no need to transcode
if (const auto streamInfo{ audioFile->getBestStreamInfo() })
{
// assume reported codec is "mp3", "opus", "vorbis", etc.
if (StringUtils::stringCaseInsensitiveEqual(streamInfo->codec, format) && (maxBitRate == 0 || (streamInfo->bitrate / 1000) <= maxBitRate))
{
LMS_LOG(API_SUBSONIC, DEBUG) << "stream parameters are compatible with actual file: no transcode";
return parameters;
}
}
const User::pointer user{ User::find(context.dbSession, context.userId) }; const User::pointer user{ User::find(context.dbSession, context.userId) };
if (!user) if (!user)
throw UserNotAuthorizedError{}; throw UserNotAuthorizedError{};
Av::TranscodeParameters& transcodeParameters{ parameters.transcodeParameters.emplace() }; const auto track{ Track::find(context.dbSession, id) };
if (!track)
throw RequestedDataNotFoundError{};
transcodeParameters.stripMetadata = false; // We want clients to use metadata (offline use, replay gain, etc.) parameters.inputParameters.trackPath = track->getPath();
transcodeParameters.offset = std::chrono::seconds{ timeOffset }; parameters.inputParameters.duration = track->getDuration();
parameters.estimateContentLength = estimateContentLength;
if (std::optional<Av::Format> requestedFormat{ subsonicStreamFormatToAvFormat(format) }) if (format == "raw") // raw => no transcoding
transcodeParameters.format = *requestedFormat; return parameters;
else
transcodeParameters.format = userTranscodeFormatToAvFormat(user->getSubsonicDefaultTranscodeFormat());
transcodeParameters.bitrate = user->getSubsonicDefaultTranscodeBitrate(); std::optional<Av::Transcoding::OutputFormat> requestedFormat{ subsonicStreamFormatToAvOutputFormat(format) };
if (maxBitRate != 0) if (!requestedFormat)
transcodeParameters.bitrate = Utils::clamp(transcodeParameters.bitrate, std::size_t{ 48000 }, maxBitRate * 1000); {
if (user->getSubsonicEnableTranscodingByDefault())
requestedFormat = userTranscodeFormatToAvFormat(user->getSubsonicDefaultTranscodingOutputFormat());
}
if (!requestedFormat && (maxBitRate == 0 || track->getBitrate() <= maxBitRate ))
{
LMS_LOG(API_SUBSONIC, DEBUG) << "File's bitrate is compatible with parameters => no transcoding";
return parameters; // no transcoding needed
}
// scan the file to check if its format is compatible with the actual requested format
// same codec => apply max bitrate
// otherwise => apply default bitrate (because we can't really compare bitrates between formats) + max bitrate)
std::size_t bitrate{};
if (requestedFormat && isOutputFormatCompatible(track->getPath(), *requestedFormat))
{
if (maxBitRate == 0 || track->getBitrate() <= maxBitRate)
{
LMS_LOG(API_SUBSONIC, DEBUG) << "File's bitrate and format are compatible with parameters => no transcoding";
return parameters; // no transcoding needed
}
bitrate = maxBitRate;
}
if (!requestedFormat)
requestedFormat = userTranscodeFormatToAvFormat(user->getSubsonicDefaultTranscodingOutputFormat());
if (!bitrate)
bitrate = std::min<std::size_t>(user->getSubsonicDefaultTranscodingOutputBitrate(), maxBitRate);
Av::Transcoding::OutputParameters& outputParameters{ parameters.outputParameters.emplace() };
outputParameters.stripMetadata = false; // We want clients to use metadata (offline use, replay gain, etc.)
outputParameters.offset = std::chrono::seconds{ timeOffset };
outputParameters.format = *requestedFormat;
outputParameters.bitrate = bitrate;
return parameters; return parameters;
} }
@@ -160,7 +208,7 @@ namespace API::Subsonic
trackPath = track->getPath(); trackPath = track->getPath();
} }
resourceHandler = createFileResourceHandler(trackPath); resourceHandler = Av::createRawResourceHandler(trackPath);
} }
else else
{ {
@@ -182,10 +230,10 @@ namespace API::Subsonic
if (!continuation) if (!continuation)
{ {
StreamParameters streamParameters{ getStreamParameters(context) }; StreamParameters streamParameters{ getStreamParameters(context) };
if (streamParameters.transcodeParameters) if (streamParameters.outputParameters)
resourceHandler = Av::createTranscodeResourceHandler(streamParameters.inputFileParameters, *streamParameters.transcodeParameters, streamParameters.estimateContentLength); resourceHandler = Av::Transcoding::createResourceHandler(streamParameters.inputParameters, *streamParameters.outputParameters, streamParameters.estimateContentLength);
else else
resourceHandler = createFileResourceHandler(streamParameters.inputFileParameters.trackPath); resourceHandler = Av::createRawResourceHandler(streamParameters.inputParameters.trackPath);
} }
else else
{ {
@@ -207,8 +255,9 @@ namespace API::Subsonic
// Mandatory params // Mandatory params
const auto trackId{ getParameterAs<TrackId>(context.parameters, "id") }; const auto trackId{ getParameterAs<TrackId>(context.parameters, "id") };
const auto releaseId{ getParameterAs<ReleaseId>(context.parameters, "id") }; const auto releaseId{ getParameterAs<ReleaseId>(context.parameters, "id") };
const auto artistId{ getParameterAs<ArtistId>(context.parameters, "id") };
if (!trackId && !releaseId) if (!trackId && !releaseId && !artistId)
throw BadParameterGenericError{ "id" }; throw BadParameterGenericError{ "id" };
std::size_t size{ getParameterAs<std::size_t>(context.parameters, "size").value_or(1024) }; std::size_t size{ getParameterAs<std::size_t>(context.parameters, "size").value_or(1024) };
@@ -219,6 +268,21 @@ namespace API::Subsonic
cover = Service<Cover::ICoverService>::get()->getFromTrack(*trackId, size); cover = Service<Cover::ICoverService>::get()->getFromTrack(*trackId, size);
else if (releaseId) else if (releaseId)
cover = Service<Cover::ICoverService>::get()->getFromRelease(*releaseId, size); cover = Service<Cover::ICoverService>::get()->getFromRelease(*releaseId, size);
else if (artistId)
{
// TODO handle a placeholder for artists
response.setStatus(404);
return;
}
if (!cover && context.enableDefaultCover)
cover = Service<Cover::ICoverService>::get()->getDefault(size);
if (!cover)
{
response.setStatus(404);
return;
}
response.out().write(reinterpret_cast<const char*>(cover->getData()), cover->getDataSize()); response.out().write(reinterpret_cast<const char*>(cover->getData()), cover->getDataSize());
response.setMimeType(std::string{ cover->getMimeType() }); response.setMimeType(std::string{ cover->getMimeType() });
@@ -57,43 +57,56 @@ namespace API::Subsonic
std::size_t songCount{ getParameterAs<std::size_t>(context.parameters, "songCount").value_or(20) }; std::size_t songCount{ getParameterAs<std::size_t>(context.parameters, "songCount").value_or(20) };
std::size_t songOffset{ getParameterAs<std::size_t>(context.parameters, "songOffset").value_or(0) }; std::size_t songOffset{ getParameterAs<std::size_t>(context.parameters, "songOffset").value_or(0) };
if (artistCount > defaultMaxCountSize)
throw ParameterValueTooHighGenericError{ "artistCount", defaultMaxCountSize };
else if (albumCount > defaultMaxCountSize)
throw ParameterValueTooHighGenericError{ "albumCount", defaultMaxCountSize };
else if (songCount > defaultMaxCountSize)
throw ParameterValueTooHighGenericError{ "songCount", defaultMaxCountSize };
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
Response::Node& searchResult2Node{ response.createNode(id3 ? "searchResult3" : "searchResult2") };
auto transaction{ context.dbSession.createSharedTransaction() }; auto transaction{ context.dbSession.createSharedTransaction() };
User::pointer user{ User::find(context.dbSession, context.userId) }; User::pointer user{ User::find(context.dbSession, context.userId) };
if (!user) if (!user)
throw UserNotAuthorizedError{}; throw UserNotAuthorizedError{};
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
Response::Node& searchResult2Node{ response.createNode(id3 ? "searchResult3" : "searchResult2") };
if (artistCount > 0) if (artistCount > 0)
{ {
Artist::FindParameters params; Artist::FindParameters params;
params.setKeywords(keywords); params.setKeywords(keywords);
params.setRange({ artistOffset, artistCount }); params.setRange(Range{ artistOffset, artistCount });
for (const Artist::pointer& artist : Artist::find(context.dbSession, params).results) Artist::find(context.dbSession, params, [&](const Artist::pointer& artist)
searchResult2Node.addArrayChild("artist", createArtistNode(context, artist, user, id3)); {
searchResult2Node.addArrayChild("artist", createArtistNode(context, artist, user, id3));
});
} }
if (albumCount > 0) if (albumCount > 0)
{ {
Release::FindParameters params; Release::FindParameters params;
params.setKeywords(keywords); params.setKeywords(keywords);
params.setRange({ albumOffset, albumCount }); params.setRange(Range{ albumOffset, albumCount });
for (const Release::pointer& release : Release::find(context.dbSession, params).results) Release::find(context.dbSession, params, [&](const Release::pointer& release)
searchResult2Node.addArrayChild("album", createAlbumNode(context, release, user, id3)); {
searchResult2Node.addArrayChild("album", createAlbumNode(context, release, user, id3));
});
} }
if (songCount > 0) if (songCount > 0)
{ {
Track::FindParameters params; Track::FindParameters params;
params.setKeywords(keywords); params.setKeywords(keywords);
params.setRange({ songOffset, songCount }); params.setRange(Range{ songOffset, songCount });
for (const Track::pointer& track : Track::find(context.dbSession, params).results) Track::find(context.dbSession, params, [&](const Track::pointer& track)
searchResult2Node.addArrayChild("song", createSongNode(context, track, user)); {
searchResult2Node.addArrayChild("song", createSongNode(context, track, user));
});
} }
return response; return response;
+2 -2
View File
@@ -168,7 +168,7 @@ namespace API::Subsonic
params.setClusterType(clusterType->getId()); params.setClusterType(clusterType->getId());
for (const auto& cluster : Cluster::find(context.dbSession, params).results) for (const auto& cluster : Cluster::find(context.dbSession, params).results)
albumNode.addArrayValue(field, std::get<std::string>(cluster)); albumNode.addArrayValue(field, cluster->getName());
} }
} }; } };
@@ -183,7 +183,7 @@ namespace API::Subsonic
params.setClusterType(genreClusterType->getId()); params.setClusterType(genreClusterType->getId());
for (const auto& cluster : Cluster::find(context.dbSession, params).results) for (const auto& cluster : Cluster::find(context.dbSession, params).results)
albumNode.addArrayChild("genres", createItemGenreNode(std::get<std::string>(cluster))); albumNode.addArrayChild("genres", createItemGenreNode(cluster->getName()));
} }
albumNode.createEmptyArrayChild("artists"); albumNode.createEmptyArrayChild("artists");
+16 -9
View File
@@ -21,6 +21,7 @@
#include <string_view> #include <string_view>
#include "av/IAudioFile.hpp"
#include "services/database/Artist.hpp" #include "services/database/Artist.hpp"
#include "services/database/Cluster.hpp" #include "services/database/Cluster.hpp"
#include "services/database/Listen.hpp" #include "services/database/Listen.hpp"
@@ -45,15 +46,15 @@ namespace API::Subsonic
namespace namespace
{ {
std::string_view formatToSuffix(AudioFormat format) std::string_view formatToSuffix(TranscodingOutputFormat format)
{ {
switch (format) switch (format)
{ {
case AudioFormat::MP3: return "mp3"; case TranscodingOutputFormat::MP3: return "mp3";
case AudioFormat::OGG_OPUS: return "opus"; case TranscodingOutputFormat::OGG_OPUS: return "opus";
case AudioFormat::MATROSKA_OPUS: return "mka"; case TranscodingOutputFormat::MATROSKA_OPUS: return "mka";
case AudioFormat::OGG_VORBIS: return "ogg"; case TranscodingOutputFormat::OGG_VORBIS: return "ogg";
case AudioFormat::WEBM_VORBIS: return "webm"; case TranscodingOutputFormat::WEBM_VORBIS: return "webm";
} }
return ""; return "";
@@ -123,7 +124,11 @@ namespace API::Subsonic
trackResponse.setAttribute("suffix", extension.string().substr(1)); trackResponse.setAttribute("suffix", extension.string().substr(1));
} }
trackResponse.setAttribute("transcodedSuffix", formatToSuffix(user->getSubsonicDefaultTranscodeFormat())); {
const std::string fileSuffix{ formatToSuffix(user->getSubsonicDefaultTranscodingOutputFormat()) };
trackResponse.setAttribute("transcodedSuffix", fileSuffix);
trackResponse.setAttribute("transcodedContentType", Av::getMimeType(std::filesystem::path{ "." + fileSuffix }));
}
trackResponse.setAttribute("coverArt", idToString(track->getId())); trackResponse.setAttribute("coverArt", idToString(track->getId()));
@@ -148,8 +153,10 @@ namespace API::Subsonic
} }
trackResponse.setAttribute("duration", std::chrono::duration_cast<std::chrono::seconds>(track->getDuration()).count()); trackResponse.setAttribute("duration", std::chrono::duration_cast<std::chrono::seconds>(track->getDuration()).count());
trackResponse.setAttribute("bitRate", (track->getBitrate() / 1000));
trackResponse.setAttribute("type", "music"); trackResponse.setAttribute("type", "music");
trackResponse.setAttribute("created", StringUtils::toISO8601String(track->getLastWritten())); trackResponse.setAttribute("created", StringUtils::toISO8601String(track->getLastWritten()));
trackResponse.setAttribute("contentType", Av::getMimeType(track->getPath().extension()));
if (const Wt::WDateTime dateTime{ Service<Feedback::IFeedbackService>::get()->getStarredDateTime(user->getId(), track->getId()) }; dateTime.isValid()) if (const Wt::WDateTime dateTime{ Service<Feedback::IFeedbackService>::get()->getStarredDateTime(user->getId(), track->getId()) }; dateTime.isValid())
trackResponse.setAttribute("starred", StringUtils::toISO8601String(dateTime)); trackResponse.setAttribute("starred", StringUtils::toISO8601String(dateTime));
@@ -228,7 +235,7 @@ namespace API::Subsonic
params.setClusterType(clusterType->getId()); params.setClusterType(clusterType->getId());
for (const auto& cluster : Cluster::find(context.dbSession, params).results) for (const auto& cluster : Cluster::find(context.dbSession, params).results)
trackResponse.addArrayValue(field, std::get<std::string>(cluster)); trackResponse.addArrayValue(field, cluster->getName());
} }
} }; } };
@@ -243,7 +250,7 @@ namespace API::Subsonic
params.setClusterType(genreClusterType->getId()); params.setClusterType(genreClusterType->getId());
for (const auto& cluster : Cluster::find(context.dbSession, params).results) for (const auto& cluster : Cluster::find(context.dbSession, params).results)
trackResponse.addArrayChild("genres", createItemGenreNode(std::get<std::string>(cluster))); trackResponse.addArrayChild("genres", createItemGenreNode(cluster->getName()));
} }
trackResponse.addChild("replayGain", createReplayGainNode(track)); trackResponse.addChild("replayGain", createReplayGainNode(track));
+75 -77
View File
@@ -24,108 +24,106 @@
#include "utils/Logger.hpp" #include "utils/Logger.hpp"
std::unique_ptr<IResourceHandler> std::unique_ptr<IResourceHandler>
createFileResourceHandler(const std::filesystem::path& path) createFileResourceHandler(const std::filesystem::path& path, std::string_view mimeType)
{ {
return std::make_unique<FileResourceHandler>(path); return std::make_unique<FileResourceHandler>(path, mimeType);
} }
FileResourceHandler::FileResourceHandler(const std::filesystem::path& path, std::string_view mimeType)
FileResourceHandler::FileResourceHandler(const std::filesystem::path& path) : _path{ path }
: _path {path} , _mimeType{ mimeType }
{ {
} }
Wt::Http::ResponseContinuation* Wt::Http::ResponseContinuation*
FileResourceHandler::processRequest(const Wt::Http::Request& request, Wt::Http::Response& response) FileResourceHandler::processRequest(const Wt::Http::Request& request, Wt::Http::Response& response)
{ {
::uint64_t startByte {_offset}; ::uint64_t startByte{ _offset };
std::ifstream ifs {_path.string().c_str(), std::ios::in | std::ios::binary}; std::ifstream ifs{ _path.string().c_str(), std::ios::in | std::ios::binary };
if (startByte == 0) if (startByte == 0)
{ {
if (!ifs) if (!ifs)
{ {
LMS_LOG(UTILS, ERROR) << "Cannot open file stream for '" << _path.string() << "'"; LMS_LOG(UTILS, ERROR) << "Cannot open file stream for '" << _path.string() << "'";
response.setStatus(404); response.setStatus(404);
return {}; return {};
} }
else
{
response.setStatus(200);
}
ifs.seekg(0, std::ios::end); ifs.seekg(0, std::ios::end);
const ::uint64_t fileSize {static_cast<::uint64_t>(ifs.tellg())}; const ::uint64_t fileSize{ static_cast<::uint64_t>(ifs.tellg()) };
ifs.seekg(0, std::ios::beg); ifs.seekg(0, std::ios::beg);
LMS_LOG(UTILS, DEBUG) << "File '" << _path.string() << "', fileSize = " << fileSize; LMS_LOG(UTILS, DEBUG) << "File '" << _path.string() << "', fileSize = " << fileSize;
const Wt::Http::Request::ByteRangeSpecifier ranges {request.getRanges(fileSize)}; const Wt::Http::Request::ByteRangeSpecifier ranges{ request.getRanges(fileSize) };
if (!ranges.isSatisfiable()) if (!ranges.isSatisfiable())
{ {
std::ostringstream contentRange; std::ostringstream contentRange;
contentRange << "bytes */" << fileSize; contentRange << "bytes */" << fileSize;
response.setStatus(416); // Requested range not satisfiable response.setStatus(416); // Requested range not satisfiable
response.addHeader("Content-Range", contentRange.str()); response.addHeader("Content-Range", contentRange.str());
LMS_LOG(UTILS, DEBUG) << "Range not satisfiable"; LMS_LOG(UTILS, DEBUG) << "Range not satisfiable";
return {}; return {};
} }
if (ranges.size() == 1) if (ranges.size() == 1)
{ {
LMS_LOG(UTILS, DEBUG) << "Range requested = " << ranges[0].firstByte() << "/" << ranges[0].lastByte(); LMS_LOG(UTILS, DEBUG) << "Range requested = " << ranges[0].firstByte() << "/" << ranges[0].lastByte();
response.setStatus(206); response.setStatus(206);
startByte = ranges[0].firstByte(); startByte = ranges[0].firstByte();
_beyondLastByte = ranges[0].lastByte() + 1; _beyondLastByte = ranges[0].lastByte() + 1;
std::ostringstream contentRange; std::ostringstream contentRange;
contentRange << "bytes " << startByte << "-" contentRange << "bytes " << startByte << "-"
<< _beyondLastByte - 1 << "/" << fileSize; << _beyondLastByte - 1 << "/" << fileSize;
response.addHeader("Content-Range", contentRange.str()); response.addHeader("Content-Range", contentRange.str());
response.setContentLength(_beyondLastByte - startByte); response.setContentLength(_beyondLastByte - startByte);
} }
else else
{ {
LMS_LOG(UTILS, DEBUG) << "No range requested"; LMS_LOG(UTILS, DEBUG) << "No range requested";
_beyondLastByte = fileSize; response.setStatus(200);
response.setContentLength(_beyondLastByte); _beyondLastByte = fileSize;
} response.setContentLength(_beyondLastByte);
} }
else if (!ifs)
{
LMS_LOG(UTILS, ERROR) << "Cannot reopen file stream for '" << _path.string() << "'";
return {};
}
ifs.seekg(static_cast<std::istream::pos_type>(startByte)); LMS_LOG(UTILS, DEBUG) << "Mimetype set to '" << _mimeType << "'";
response.setMimeType(_mimeType);
}
else if (!ifs)
{
LMS_LOG(UTILS, ERROR) << "Cannot reopen file stream for '" << _path.string() << "'";
return {};
}
std::vector<char> buf; ifs.seekg(static_cast<std::istream::pos_type>(startByte));
buf.resize(_chunkSize);
::uint64_t restSize = _beyondLastByte - startByte; std::vector<char> buf;
::uint64_t pieceSize = buf.size() > restSize ? restSize : buf.size(); buf.resize(_chunkSize);
ifs.read(&buf[0], pieceSize); ::uint64_t restSize = _beyondLastByte - startByte;
const ::uint64_t actualPieceSize {static_cast<::uint64_t>(ifs.gcount())}; ::uint64_t pieceSize = buf.size() > restSize ? restSize : buf.size();
response.out().write(&buf[0], actualPieceSize);
LMS_LOG(UTILS, DEBUG) << "Written " << actualPieceSize << " bytes"; ifs.read(&buf[0], pieceSize);
const ::uint64_t actualPieceSize{ static_cast<::uint64_t>(ifs.gcount()) };
response.out().write(&buf[0], actualPieceSize);
LMS_LOG(UTILS, DEBUG) << "Progress: " << actualPieceSize << "/" << restSize; LMS_LOG(UTILS, DEBUG) << "Written " << actualPieceSize << " bytes";
if (ifs.good() && actualPieceSize < restSize)
{
_offset = startByte + actualPieceSize;
LMS_LOG(UTILS, DEBUG) << "Job not complete! Next chunk offset = " << _offset;
return response.createContinuation(); LMS_LOG(UTILS, DEBUG) << "Progress: " << actualPieceSize << "/" << restSize;
} if (ifs.good() && actualPieceSize < restSize)
{
_offset = startByte + actualPieceSize;
LMS_LOG(UTILS, DEBUG) << "Job not complete! Next chunk offset = " << _offset;
LMS_LOG(UTILS, DEBUG) << "Job complete!"; return response.createContinuation();
return nullptr; }
LMS_LOG(UTILS, DEBUG) << "Job complete!";
return nullptr;
} }
+12 -9
View File
@@ -20,21 +20,24 @@
#pragma once #pragma once
#include <filesystem> #include <filesystem>
#include <string>
#include <string_view>
#include "utils/IResourceHandler.hpp" #include "utils/IResourceHandler.hpp"
class FileResourceHandler final : public IResourceHandler class FileResourceHandler final : public IResourceHandler
{ {
public: public:
FileResourceHandler(const std::filesystem::path& filePath); FileResourceHandler(const std::filesystem::path& filePath, std::string_view mimeType);
private: private:
Wt::Http::ResponseContinuation* processRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override; Wt::Http::ResponseContinuation* processRequest(const Wt::Http::Request& request, Wt::Http::Response& response) override;
void abort() override {}; void abort() override {};
static constexpr std::size_t _chunkSize {65536}; static constexpr std::size_t _chunkSize{ 262'144 };
std::filesystem::path _path; std::filesystem::path _path;
::uint64_t _beyondLastByte {}; std::string _mimeType;
::uint64_t _offset {}; ::uint64_t _beyondLastByte{};
::uint64_t _offset{};
}; };
+38 -38
View File
@@ -21,59 +21,59 @@
const char* getModuleName(Module mod) const char* getModuleName(Module mod)
{ {
switch (mod) switch (mod)
{ {
case Module::API_SUBSONIC: return "API_SUBSONIC"; case Module::API_SUBSONIC: return "API_SUBSONIC";
case Module::AUTH: return "AUTH"; case Module::AUTH: return "AUTH";
case Module::AV: return "AV"; case Module::AV: return "AV";
case Module::CHILDPROCESS: return "CHILDPROC"; case Module::CHILDPROCESS: return "CHILDPROC";
case Module::COVER: return "COVER"; case Module::COVER: return "COVER";
case Module::DB: return "DB"; case Module::DB: return "DB";
case Module::DBUPDATER: return "DB UPDATER"; case Module::DBUPDATER: return "DB UPDATER";
case Module::FEATURE: return "FEATURE"; case Module::FEATURE: return "FEATURE";
case Module::FEEDBACK: return "FEEDBACK"; case Module::FEEDBACK: return "FEEDBACK";
case Module::HTTP: return "HTTP"; case Module::HTTP: return "HTTP";
case Module::MAIN: return "MAIN"; case Module::MAIN: return "MAIN";
case Module::METADATA: return "METADATA"; case Module::METADATA: return "METADATA";
case Module::REMOTE: return "REMOTE"; case Module::REMOTE: return "REMOTE";
case Module::SCROBBLING: return "SCROBBLING"; case Module::SCROBBLING: return "SCROBBLING";
case Module::SERVICE: return "SERVICE"; case Module::SERVICE: return "SERVICE";
case Module::RECOMMENDATION: return "RECOMMENDATION"; case Module::RECOMMENDATION: return "RECOMMENDATION";
case Module::TRANSCODE: return "TRANSCODE"; case Module::TRANSCODING: return "TRANSCODING";
case Module::UI: return "UI"; case Module::UI: return "UI";
case Module::UTILS: return "UTILS"; case Module::UTILS: return "UTILS";
} }
return ""; return "";
} }
const char* getSeverityName(Severity sev) const char* getSeverityName(Severity sev)
{ {
switch (sev) switch (sev)
{ {
case Severity::FATAL: return "fatal"; case Severity::FATAL: return "fatal";
case Severity::ERROR: return "error"; case Severity::ERROR: return "error";
case Severity::WARNING: return "warning"; case Severity::WARNING: return "warning";
case Severity::INFO: return "info"; case Severity::INFO: return "info";
case Severity::DEBUG: return "debug"; case Severity::DEBUG: return "debug";
} }
return ""; return "";
} }
Log::Log(Logger* logger, Module module, Severity severity) Log::Log(Logger* logger, Module module, Severity severity)
: _module {module}, : _module{ module },
_severity {severity}, _severity{ severity },
_logger {logger} _logger{ logger }
{} {}
Log::~Log() Log::~Log()
{ {
if (_logger) if (_logger)
_logger->processLog(*this); _logger->processLog(*this);
} }
std::string std::string
Log::getMessage() const Log::getMessage() const
{ {
return _oss.str(); return _oss.str();
} }
@@ -21,8 +21,9 @@
#include <filesystem> #include <filesystem>
#include <memory> #include <memory>
#include <string_view>
#include "utils/IResourceHandler.hpp" #include "utils/IResourceHandler.hpp"
std::unique_ptr<IResourceHandler> createFileResourceHandler(const std::filesystem::path& path); std::unique_ptr<IResourceHandler> createFileResourceHandler(const std::filesystem::path& path, std::string_view mimeType);
+1 -1
View File
@@ -51,7 +51,7 @@ enum class Module
SCROBBLING, SCROBBLING,
SERVICE, SERVICE,
RECOMMENDATION, RECOMMENDATION,
TRANSCODE, TRANSCODING,
UI, UI,
UTILS, UTILS,
}; };
+1 -1
View File
@@ -44,7 +44,7 @@ add_executable(lms
ui/explore/TrackListsView.cpp ui/explore/TrackListsView.cpp
ui/explore/TracksView.cpp ui/explore/TracksView.cpp
ui/resource/AudioFileResource.cpp ui/resource/AudioFileResource.cpp
ui/resource/AudioTranscodeResource.cpp ui/resource/AudioTranscodingResource.cpp
ui/resource/CoverResource.cpp ui/resource/CoverResource.cpp
ui/resource/DownloadResource.cpp ui/resource/DownloadResource.cpp
) )
+1 -1
View File
@@ -270,7 +270,7 @@ int main(int argc, char* argv[])
Service<Cover::ICoverService> coverService{ Cover::createCoverService(database, argv[0], server.appRoot() + "/images/unknown-cover.jpg") }; Service<Cover::ICoverService> coverService{ Cover::createCoverService(database, argv[0], server.appRoot() + "/images/unknown-cover.jpg") };
Service<Recommendation::IRecommendationService> recommendationService{ Recommendation::createRecommendationService(database) }; Service<Recommendation::IRecommendationService> recommendationService{ Recommendation::createRecommendationService(database) };
Service<Recommendation::IPlaylistGeneratorService> playlistGeneratorService{ Recommendation::createPlaylistGeneratorService(database, *recommendationService.get()) }; Service<Recommendation::IPlaylistGeneratorService> playlistGeneratorService{ Recommendation::createPlaylistGeneratorService(database, *recommendationService.get()) };
Service<Scanner::IScannerService> scannerService{ Scanner::createScannerService(database, *recommendationService) }; Service<Scanner::IScannerService> scannerService{ Scanner::createScannerService(database) };
scannerService->getEvents().scanComplete.connect([&] scannerService->getEvents().scanComplete.connect([&]
{ {
+1 -1
View File
@@ -49,7 +49,7 @@
#include "explore/Explore.hpp" #include "explore/Explore.hpp"
#include "explore/Filters.hpp" #include "explore/Filters.hpp"
#include "resource/AudioFileResource.hpp" #include "resource/AudioFileResource.hpp"
#include "resource/AudioTranscodeResource.hpp" #include "resource/AudioTranscodingResource.hpp"
#include "resource/DownloadResource.hpp" #include "resource/DownloadResource.hpp"
#include "resource/CoverResource.hpp" #include "resource/CoverResource.hpp"
#include "Auth.hpp" #include "Auth.hpp"

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