Merge branch 'develop' for release v3.44.0
This commit is contained in:
+16
-14
@@ -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
@@ -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
@@ -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
@@ -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-->
|
||||||
|
|||||||
+32
-21
@@ -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 class="form-check-label" for="${id:subsonic-enable-transcoding-by-default}">
|
||||||
|
${tr:Lms.Settings.enable-transcoding-by-default}
|
||||||
</label>
|
</label>
|
||||||
${subsonic-transcode-format class="form-control"}
|
|
||||||
<div class="invalid-feedback">
|
<div class="invalid-feedback">
|
||||||
${subsonic-transcode-format-info class="help-block"}
|
${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
@@ -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
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -32,9 +32,11 @@ 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)
|
namespace
|
||||||
|
{
|
||||||
|
std::string averror_to_string(int error)
|
||||||
{
|
{
|
||||||
std::array<char, 128> buf = { 0 };
|
std::array<char, 128> buf = { 0 };
|
||||||
|
|
||||||
@@ -52,8 +54,45 @@ class AudioFileException : public Av::Exception
|
|||||||
{}
|
{}
|
||||||
};
|
};
|
||||||
|
|
||||||
std::unique_ptr<IAudioFile>
|
void getMetaDataFromDictionnary(AVDictionary* dictionnary, AudioFile::MetadataMap& res)
|
||||||
parseAudioFile(const std::filesystem::path& p)
|
{
|
||||||
|
if (!dictionnary)
|
||||||
|
return;
|
||||||
|
|
||||||
|
AVDictionaryEntry* tag = NULL;
|
||||||
|
while ((tag = ::av_dict_get(dictionnary, "", tag, AV_DICT_IGNORE_SUFFIX)))
|
||||||
|
{
|
||||||
|
res[StringUtils::stringToUpper(tag->key)] = tag->value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DecodingCodec avcodecToDecodingCodec(AVCodecID codec)
|
||||||
|
{
|
||||||
|
switch (codec)
|
||||||
|
{
|
||||||
|
case AV_CODEC_ID_MP3: return DecodingCodec::MP3;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<IAudioFile> parseAudioFile(const std::filesystem::path& p)
|
||||||
{
|
{
|
||||||
return std::make_unique<AudioFile>(p);
|
return std::make_unique<AudioFile>(p);
|
||||||
}
|
}
|
||||||
@@ -82,36 +121,22 @@ AudioFile::~AudioFile()
|
|||||||
avformat_close_input(&_context);
|
avformat_close_input(&_context);
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::filesystem::path&
|
const std::filesystem::path& AudioFile::getPath() const
|
||||||
AudioFile::getPath() const
|
|
||||||
{
|
{
|
||||||
return _p;
|
return _p;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::chrono::milliseconds
|
ContainerInfo AudioFile::getContainerInfo() const
|
||||||
AudioFile::getDuration() const
|
|
||||||
{
|
{
|
||||||
if (_context->duration == AV_NOPTS_VALUE)
|
ContainerInfo info;
|
||||||
return std::chrono::milliseconds {0}; // TODO estimate
|
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;
|
||||||
|
|
||||||
return std::chrono::milliseconds {_context->duration / AV_TIME_BASE * 1000};
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
AudioFile::MetadataMap AudioFile::getMetaData() const
|
||||||
getMetaDataFromDictionnary(AVDictionary* dictionnary, AudioFile::MetadataMap& res)
|
|
||||||
{
|
|
||||||
if (!dictionnary)
|
|
||||||
return;
|
|
||||||
|
|
||||||
AVDictionaryEntry *tag = NULL;
|
|
||||||
while ((tag = ::av_dict_get(dictionnary, "", tag, AV_DICT_IGNORE_SUFFIX)))
|
|
||||||
{
|
|
||||||
res[StringUtils::stringToUpper(tag->key)] = tag->value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AudioFile::MetadataMap
|
|
||||||
AudioFile::getMetaData() const
|
|
||||||
{
|
{
|
||||||
MetadataMap res;
|
MetadataMap res;
|
||||||
|
|
||||||
@@ -133,8 +158,7 @@ AudioFile::getMetaData() const
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<StreamInfo>
|
std::vector<StreamInfo> AudioFile::getStreamInfo() const
|
||||||
AudioFile::getStreamInfo() const
|
|
||||||
{
|
{
|
||||||
std::vector<StreamInfo> res;
|
std::vector<StreamInfo> res;
|
||||||
|
|
||||||
@@ -148,8 +172,7 @@ AudioFile::getStreamInfo() const
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<std::size_t>
|
std::optional<std::size_t> AudioFile::getBestStreamIndex() const
|
||||||
AudioFile::getBestStreamIndex() const
|
|
||||||
{
|
{
|
||||||
int res = ::av_find_best_stream(_context,
|
int res = ::av_find_best_stream(_context,
|
||||||
AVMEDIA_TYPE_AUDIO,
|
AVMEDIA_TYPE_AUDIO,
|
||||||
@@ -164,8 +187,7 @@ AudioFile::getBestStreamIndex() const
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<StreamInfo>
|
std::optional<StreamInfo> AudioFile::getBestStreamInfo() const
|
||||||
AudioFile::getBestStreamInfo() const
|
|
||||||
{
|
{
|
||||||
std::optional<StreamInfo> res;
|
std::optional<StreamInfo> res;
|
||||||
|
|
||||||
@@ -176,8 +198,7 @@ AudioFile::getBestStreamInfo() const
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool AudioFile::hasAttachedPictures() const
|
||||||
AudioFile::hasAttachedPictures() const
|
|
||||||
{
|
{
|
||||||
for (std::size_t i = 0; i < _context->nb_streams; ++i)
|
for (std::size_t i = 0; i < _context->nb_streams; ++i)
|
||||||
{
|
{
|
||||||
@@ -188,8 +209,7 @@ AudioFile::hasAttachedPictures() const
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void AudioFile::visitAttachedPictures(std::function<void(const Picture&)> func) const
|
||||||
AudioFile::visitAttachedPictures(std::function<void(const Picture&)> func) const
|
|
||||||
{
|
{
|
||||||
static const std::unordered_map<int, std::string> codecMimeMap =
|
static const std::unordered_map<int, std::string> codecMimeMap =
|
||||||
{
|
{
|
||||||
@@ -237,8 +257,7 @@ AudioFile::visitAttachedPictures(std::function<void(const Picture&)> func) const
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<StreamInfo>
|
std::optional<StreamInfo> AudioFile::getStreamInfo(std::size_t streamIndex) const
|
||||||
AudioFile::getStreamInfo(std::size_t streamIndex) const
|
|
||||||
{
|
{
|
||||||
std::optional<StreamInfo> res;
|
std::optional<StreamInfo> res;
|
||||||
|
|
||||||
@@ -260,39 +279,48 @@ AudioFile::getStreamInfo(std::size_t streamIndex) const
|
|||||||
res.emplace();
|
res.emplace();
|
||||||
res->index = streamIndex;
|
res->index = streamIndex;
|
||||||
res->bitrate = static_cast<std::size_t>(avstream->codecpar->bit_rate);
|
res->bitrate = static_cast<std::size_t>(avstream->codecpar->bit_rate);
|
||||||
res->codec = ::avcodec_get_name(avstream->codecpar->codec_id);
|
res->codec = avcodecToDecodingCodec(avstream->codecpar->codec_id);
|
||||||
assert(!res->codec.empty());
|
res->codecName = ::avcodec_get_name(avstream->codecpar->codec_id);
|
||||||
|
assert(!res->codecName.empty()); // doc says it is never NULL
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<AudioFileFormat>
|
std::string_view getMimeType(const std::filesystem::path& fileExtension)
|
||||||
guessAudioFileFormat(const std::filesystem::path& file)
|
|
||||||
{
|
{
|
||||||
const AVOutputFormat* format {::av_guess_format(NULL, file.string().c_str(), NULL)};
|
// List should be sync with the demuxers shipped in the lms's docker version
|
||||||
if (!format || !format->name)
|
// + the _audioFileExtensions in ScanSettings
|
||||||
return {};
|
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"},
|
||||||
|
};
|
||||||
|
|
||||||
LMS_LOG(AV, DEBUG) << "File '" << file.string() << "', formats = '" << format->name << "'";
|
auto it{ entries.find(fileExtension) };
|
||||||
|
if (it == std::cend(entries))
|
||||||
|
return "";
|
||||||
|
|
||||||
auto formats {StringUtils::splitString(format->name, ",")};
|
return it->second;
|
||||||
if (formats.size() > 1)
|
|
||||||
LMS_LOG(AV, INFO) << "File '" << file.string() << "' reported several formats: '" << format->name << "'";
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Av
|
} // namespace Av
|
||||||
|
|||||||
@@ -34,13 +34,8 @@ namespace Av
|
|||||||
AudioFile(const std::filesystem::path& p);
|
AudioFile(const std::filesystem::path& p);
|
||||||
~AudioFile();
|
~AudioFile();
|
||||||
|
|
||||||
AudioFile(const AudioFile&) = delete;
|
|
||||||
AudioFile(AudioFile&&) = delete;
|
|
||||||
AudioFile& operator=(const AudioFile&) = delete;
|
|
||||||
AudioFile& operator=(AudioFile&&) = delete;
|
|
||||||
|
|
||||||
const std::filesystem::path& getPath() const override;
|
const std::filesystem::path& getPath() const override;
|
||||||
std::chrono::milliseconds getDuration() const override;
|
ContainerInfo getContainerInfo() const override;
|
||||||
MetadataMap getMetaData() const override;
|
MetadataMap getMetaData() const override;
|
||||||
std::vector<StreamInfo> getStreamInfo() const override;
|
std::vector<StreamInfo> getStreamInfo() const override;
|
||||||
std::optional<StreamInfo> getBestStreamInfo() const override;
|
std::optional<StreamInfo> getBestStreamInfo() const override;
|
||||||
@@ -49,6 +44,9 @@ namespace Av
|
|||||||
void visitAttachedPictures(std::function<void(const Picture&)> func) const override;
|
void visitAttachedPictures(std::function<void(const Picture&)> func) const override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
AudioFile(const AudioFile&) = delete;
|
||||||
|
AudioFile& operator=(const AudioFile&) = delete;
|
||||||
|
|
||||||
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;
|
||||||
|
|||||||
@@ -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 {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -28,50 +28,63 @@
|
|||||||
#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 << "] - "
|
#define LOG(sev) LMS_LOG(TRANSCODING, sev) << "[" << _debugId << "] - "
|
||||||
|
|
||||||
static std::atomic<size_t> globalId{};
|
static std::atomic<size_t> globalId{};
|
||||||
static std::filesystem::path ffmpegPath;
|
static std::filesystem::path ffmpegPath;
|
||||||
|
|
||||||
void
|
std::string_view formatToMimetype(OutputFormat format)
|
||||||
Transcoder::init()
|
{
|
||||||
|
switch (format)
|
||||||
|
{
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Exception{ "Invalid encoding" };
|
||||||
|
}
|
||||||
|
|
||||||
|
void Transcoder::init()
|
||||||
{
|
{
|
||||||
ffmpegPath = Service<IConfig>::get()->getPath("ffmpeg-file", "/usr/bin/ffmpeg");
|
ffmpegPath = Service<IConfig>::get()->getPath("ffmpeg-file", "/usr/bin/ffmpeg");
|
||||||
if (!std::filesystem::exists(ffmpegPath))
|
if (!std::filesystem::exists(ffmpegPath))
|
||||||
throw Exception{ "File '" + ffmpegPath.string() + "' does not exist!" };
|
throw Exception{ "File '" + ffmpegPath.string() + "' does not exist!" };
|
||||||
}
|
}
|
||||||
|
|
||||||
Transcoder::Transcoder(const InputFileParameters& inputFileParameters, const TranscodeParameters& transcodeParameters)
|
Transcoder::Transcoder(const InputParameters& inputParameters, const OutputParameters& outputParameters)
|
||||||
: _debugId{ globalId++ }
|
: _debugId{ globalId++ }
|
||||||
, _inputFileParameters {inputFileParameters}
|
, _inputParameters{ inputParameters }
|
||||||
, _transcodeParameters {transcodeParameters}
|
, _outputParameters{ outputParameters }
|
||||||
{
|
{
|
||||||
start();
|
start();
|
||||||
}
|
}
|
||||||
|
|
||||||
Transcoder::~Transcoder() = default;
|
Transcoder::~Transcoder() = default;
|
||||||
|
|
||||||
void
|
void Transcoder::start()
|
||||||
Transcoder::start()
|
|
||||||
{
|
{
|
||||||
if (ffmpegPath.empty())
|
if (ffmpegPath.empty())
|
||||||
init();
|
init();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!std::filesystem::exists(_inputFileParameters.trackPath))
|
if (!std::filesystem::exists(_inputParameters.trackPath))
|
||||||
throw Exception {"File '" + _inputFileParameters.trackPath.string() + "' does not exist!"};
|
throw Exception{ "File '" + _inputParameters.trackPath.string() + "' does not exist!" };
|
||||||
else if (!std::filesystem::is_regular_file( _inputFileParameters.trackPath) )
|
else if (!std::filesystem::is_regular_file(_inputParameters.trackPath))
|
||||||
throw Exception {"File '" + _inputFileParameters.trackPath.string() + "' is not regular!"};
|
throw Exception{ "File '" + _inputParameters.trackPath.string() + "' is not regular!" };
|
||||||
}
|
}
|
||||||
catch (const std::filesystem::filesystem_error& e)
|
catch (const std::filesystem::filesystem_error& e)
|
||||||
{
|
{
|
||||||
throw Exception {"File error '" + _inputFileParameters.trackPath.string() + "': " + e.what()};
|
throw Exception{ "File error '" + _inputParameters.trackPath.string() + "': " + e.what() };
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG(INFO) << "Transcoding file '" << _inputFileParameters.trackPath.string() << "'";
|
LOG(INFO) << "Transcoding file '" << _inputParameters.trackPath.string() << "'";
|
||||||
|
|
||||||
std::vector<std::string> args;
|
std::vector<std::string> args;
|
||||||
|
|
||||||
@@ -90,22 +103,22 @@ Transcoder::start()
|
|||||||
args.emplace_back("-ss");
|
args.emplace_back("-ss");
|
||||||
|
|
||||||
std::ostringstream oss;
|
std::ostringstream oss;
|
||||||
oss << std::fixed << std::showpoint << std::setprecision(3) << (_transcodeParameters.offset.count() / float {1000});
|
oss << std::fixed << std::showpoint << std::setprecision(3) << (_outputParameters.offset.count() / float{ 1000 });
|
||||||
args.emplace_back(oss.str());
|
args.emplace_back(oss.str());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Input file
|
// Input file
|
||||||
args.emplace_back("-i");
|
args.emplace_back("-i");
|
||||||
args.emplace_back(_inputFileParameters.trackPath.string());
|
args.emplace_back(_inputParameters.trackPath.string());
|
||||||
|
|
||||||
// Stream mapping, if set
|
// Stream mapping, if set
|
||||||
if (_transcodeParameters.stream)
|
if (_outputParameters.stream)
|
||||||
{
|
{
|
||||||
args.emplace_back("-map");
|
args.emplace_back("-map");
|
||||||
args.emplace_back("0:" + std::to_string(*_transcodeParameters.stream));
|
args.emplace_back("0:" + std::to_string(*_outputParameters.stream));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_transcodeParameters.stripMetadata)
|
if (_outputParameters.stripMetadata)
|
||||||
{
|
{
|
||||||
// Strip metadata
|
// Strip metadata
|
||||||
args.emplace_back("-map_metadata");
|
args.emplace_back("-map_metadata");
|
||||||
@@ -117,38 +130,38 @@ Transcoder::start()
|
|||||||
|
|
||||||
// Output bitrates
|
// Output bitrates
|
||||||
args.emplace_back("-b:a");
|
args.emplace_back("-b:a");
|
||||||
args.emplace_back(std::to_string(_transcodeParameters.bitrate));
|
args.emplace_back(std::to_string(_outputParameters.bitrate));
|
||||||
|
|
||||||
// Codecs and formats
|
// Codecs and formats
|
||||||
switch (_transcodeParameters.format)
|
switch (_outputParameters.format)
|
||||||
{
|
{
|
||||||
case Format::MP3:
|
case OutputFormat::MP3:
|
||||||
args.emplace_back("-f");
|
args.emplace_back("-f");
|
||||||
args.emplace_back("mp3");
|
args.emplace_back("mp3");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Format::OGG_OPUS:
|
case OutputFormat::OGG_OPUS:
|
||||||
args.emplace_back("-acodec");
|
args.emplace_back("-acodec");
|
||||||
args.emplace_back("libopus");
|
args.emplace_back("libopus");
|
||||||
args.emplace_back("-f");
|
args.emplace_back("-f");
|
||||||
args.emplace_back("ogg");
|
args.emplace_back("ogg");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Format::MATROSKA_OPUS:
|
case OutputFormat::MATROSKA_OPUS:
|
||||||
args.emplace_back("-acodec");
|
args.emplace_back("-acodec");
|
||||||
args.emplace_back("libopus");
|
args.emplace_back("libopus");
|
||||||
args.emplace_back("-f");
|
args.emplace_back("-f");
|
||||||
args.emplace_back("matroska");
|
args.emplace_back("matroska");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Format::OGG_VORBIS:
|
case OutputFormat::OGG_VORBIS:
|
||||||
args.emplace_back("-acodec");
|
args.emplace_back("-acodec");
|
||||||
args.emplace_back("libvorbis");
|
args.emplace_back("libvorbis");
|
||||||
args.emplace_back("-f");
|
args.emplace_back("-f");
|
||||||
args.emplace_back("ogg");
|
args.emplace_back("ogg");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Format::WEBM_VORBIS:
|
case OutputFormat::WEBM_VORBIS:
|
||||||
args.emplace_back("-acodec");
|
args.emplace_back("-acodec");
|
||||||
args.emplace_back("libvorbis");
|
args.emplace_back("libvorbis");
|
||||||
args.emplace_back("-f");
|
args.emplace_back("-f");
|
||||||
@@ -156,10 +169,10 @@ Transcoder::start()
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw Exception {"Unhandled format (" + std::to_string(static_cast<int>(_transcodeParameters.format)) + ")"};
|
throw Exception{ "Unhandled format (" + std::to_string(static_cast<int>(_outputParameters.format)) + ")" };
|
||||||
}
|
}
|
||||||
|
|
||||||
_outputMimeType = formatToMimetype(_transcodeParameters.format);
|
_outputMimeType = formatToMimetype(_outputParameters.format);
|
||||||
|
|
||||||
args.emplace_back("pipe:1");
|
args.emplace_back("pipe:1");
|
||||||
|
|
||||||
@@ -178,8 +191,7 @@ Transcoder::start()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void Transcoder::asyncRead(std::byte* buffer, std::size_t bufferSize, ReadCallback readCallback)
|
||||||
Transcoder::asyncRead(std::byte* buffer, std::size_t bufferSize, ReadCallback readCallback)
|
|
||||||
{
|
{
|
||||||
assert(_childProcess);
|
assert(_childProcess);
|
||||||
|
|
||||||
@@ -189,20 +201,18 @@ Transcoder::asyncRead(std::byte* buffer, std::size_t bufferSize, ReadCallback re
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
std::size_t
|
std::size_t Transcoder::readSome(std::byte* buffer, std::size_t bufferSize)
|
||||||
Transcoder::readSome(std::byte* buffer, std::size_t bufferSize)
|
|
||||||
{
|
{
|
||||||
assert(_childProcess);
|
assert(_childProcess);
|
||||||
|
|
||||||
return _childProcess->readSome(buffer, bufferSize);
|
return _childProcess->readSome(buffer, bufferSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool Transcoder::finished() const
|
||||||
Transcoder::finished() const
|
|
||||||
{
|
{
|
||||||
assert(_childProcess);
|
assert(_childProcess);
|
||||||
|
|
||||||
return _childProcess->finished();
|
return _childProcess->finished();
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Transcode
|
} // namespace Av::Transcoding
|
||||||
|
|||||||
@@ -22,17 +22,17 @@
|
|||||||
#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;
|
||||||
@@ -46,7 +46,7 @@ namespace Av
|
|||||||
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;
|
||||||
|
|
||||||
@@ -56,13 +56,11 @@ namespace Av
|
|||||||
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 {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
+5
-5
@@ -23,22 +23,22 @@
|
|||||||
#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{};
|
||||||
@@ -27,12 +27,36 @@
|
|||||||
#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
|
||||||
{
|
{
|
||||||
|
// List should be sync with the codecs shipped in the lms's docker version
|
||||||
|
enum class DecodingCodec
|
||||||
|
{
|
||||||
|
UNKNOWN,
|
||||||
|
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 Picture
|
struct Picture
|
||||||
{
|
{
|
||||||
std::string mimeType;
|
std::string mimeType;
|
||||||
@@ -40,11 +64,19 @@ namespace Av
|
|||||||
std::size_t dataSize{};
|
std::size_t dataSize{};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct ContainerInfo
|
||||||
|
{
|
||||||
|
std::size_t bitrate{};
|
||||||
|
std::string name{};
|
||||||
|
std::chrono::milliseconds duration{};
|
||||||
|
};
|
||||||
|
|
||||||
struct StreamInfo
|
struct StreamInfo
|
||||||
{
|
{
|
||||||
size_t index{};
|
size_t index{};
|
||||||
std::size_t bitrate{};
|
std::size_t bitrate{};
|
||||||
std::string codec;
|
DecodingCodec codec;
|
||||||
|
std::string codecName;
|
||||||
};
|
};
|
||||||
|
|
||||||
class IAudioFile
|
class IAudioFile
|
||||||
@@ -55,7 +87,7 @@ namespace Av
|
|||||||
using MetadataMap = std::unordered_map<std::string, std::string>;
|
using MetadataMap = std::unordered_map<std::string, std::string>;
|
||||||
|
|
||||||
virtual const std::filesystem::path& getPath() const = 0;
|
virtual const std::filesystem::path& getPath() const = 0;
|
||||||
virtual std::chrono::milliseconds getDuration() const = 0;
|
virtual ContainerInfo getContainerInfo() const = 0;
|
||||||
virtual MetadataMap getMetaData() const = 0;
|
virtual MetadataMap getMetaData() const = 0;
|
||||||
virtual std::vector<StreamInfo> getStreamInfo() const = 0;
|
virtual std::vector<StreamInfo> getStreamInfo() const = 0;
|
||||||
virtual std::optional<StreamInfo> getBestStreamInfo() const = 0; // none if failure/unknown
|
virtual std::optional<StreamInfo> getBestStreamInfo() const = 0; // none if failure/unknown
|
||||||
@@ -72,7 +104,7 @@ namespace Av
|
|||||||
std::string format;
|
std::string format;
|
||||||
};
|
};
|
||||||
|
|
||||||
std::optional<AudioFileFormat> guessAudioFileFormat(const std::filesystem::path& file);
|
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
|
||||||
|
|
||||||
+4
-5
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -44,13 +44,13 @@
|
|||||||
|
|
||||||
namespace MetaData
|
namespace MetaData
|
||||||
{
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
// TODO use string_views here for values
|
// TODO use string_views here for values
|
||||||
using TagMap = std::map<std::string, std::vector<std::string>>;
|
using TagMap = std::map<std::string, std::vector<std::string>>;
|
||||||
|
|
||||||
template<typename T>
|
template<typename T>
|
||||||
std::vector<T>
|
std::vector<T> getPropertyValuesFirstMatchAs(const TagMap& tags, std::initializer_list<std::string_view> keys)
|
||||||
getPropertyValuesFirstMatchAs(const TagMap& tags, std::initializer_list<std::string_view> keys)
|
|
||||||
{
|
{
|
||||||
std::vector<T> res;
|
std::vector<T> res;
|
||||||
|
|
||||||
@@ -82,8 +82,7 @@ getPropertyValuesFirstMatchAs(const TagMap& tags, std::initializer_list<std::str
|
|||||||
}
|
}
|
||||||
|
|
||||||
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) };
|
||||||
@@ -94,22 +93,18 @@ getPropertyValueFirstMatchAs(const TagMap& tags, std::initializer_list<std::stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
template <typename T>
|
template <typename T>
|
||||||
std::vector<T>
|
std::vector<T> getPropertyValuesAs(const TagMap& tags, std::string_view key)
|
||||||
getPropertyValuesAs(const TagMap& tags, std::string_view key)
|
|
||||||
{
|
{
|
||||||
return getPropertyValuesFirstMatchAs<T>(tags, { key });
|
return getPropertyValuesFirstMatchAs<T>(tags, { key });
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename T>
|
template <typename T>
|
||||||
std::optional<T>
|
std::optional<T> getPropertyValueAs(const TagMap& tags, std::string_view key)
|
||||||
getPropertyValueAs(const TagMap& tags, std::string_view key)
|
|
||||||
{
|
{
|
||||||
return getPropertyValueFirstMatchAs<T>(tags, { key });
|
return getPropertyValueFirstMatchAs<T>(tags, { key });
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
std::vector<std::string_view> splitAndTrimString(std::string_view str, std::string_view delimiters)
|
||||||
std::vector<std::string_view>
|
|
||||||
splitAndTrimString(std::string_view str, std::string_view delimiters)
|
|
||||||
{
|
{
|
||||||
std::vector<std::string_view> strings{ StringUtils::splitString(str, delimiters) };
|
std::vector<std::string_view> strings{ StringUtils::splitString(str, delimiters) };
|
||||||
for (std::string_view& s : strings)
|
for (std::string_view& s : strings)
|
||||||
@@ -118,9 +113,7 @@ splitAndTrimString(std::string_view str, std::string_view delimiters)
|
|||||||
return strings;
|
return strings;
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
std::vector<Artist> getArtists(const TagMap& tags,
|
||||||
std::vector<Artist>
|
|
||||||
getArtists(const TagMap& tags,
|
|
||||||
std::initializer_list<std::string_view> artistTagNames,
|
std::initializer_list<std::string_view> artistTagNames,
|
||||||
std::initializer_list<std::string_view> artistSortTagNames,
|
std::initializer_list<std::string_view> artistSortTagNames,
|
||||||
std::initializer_list<std::string_view> artistMBIDTagNames
|
std::initializer_list<std::string_view> artistMBIDTagNames
|
||||||
@@ -158,10 +151,7 @@ getArtists(const TagMap& tags,
|
|||||||
return artists;
|
return artists;
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
PerformerContainer getPerformerArtists(const TagMap& tags, std::initializer_list<std::string_view> artistTagNames)
|
||||||
PerformerContainer
|
|
||||||
getPerformerArtists(const TagMap& tags,
|
|
||||||
std::initializer_list<std::string_view> artistTagNames)
|
|
||||||
{
|
{
|
||||||
PerformerContainer performers;
|
PerformerContainer performers;
|
||||||
|
|
||||||
@@ -198,9 +188,7 @@ getPerformerArtists(const TagMap& tags,
|
|||||||
return performers;
|
return performers;
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
std::optional<Release> getRelease(const TagMap& tags)
|
||||||
std::optional<Release>
|
|
||||||
getRelease(const TagMap& tags)
|
|
||||||
{
|
{
|
||||||
std::optional<Release> release;
|
std::optional<Release> release;
|
||||||
|
|
||||||
@@ -236,9 +224,7 @@ getRelease(const TagMap& tags)
|
|||||||
return release;
|
return release;
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
std::optional<Medium> getMedium(const TagMap& tags)
|
||||||
std::optional<Medium>
|
|
||||||
getMedium(const TagMap& tags)
|
|
||||||
{
|
{
|
||||||
std::optional<Medium> medium;
|
std::optional<Medium> medium;
|
||||||
medium.emplace();
|
medium.emplace();
|
||||||
@@ -275,9 +261,7 @@ getMedium(const TagMap& tags)
|
|||||||
return medium;
|
return medium;
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
TagLib::AudioProperties::ReadStyle readStyleToTagLibReadStyle(ParserReadStyle readStyle)
|
||||||
TagLib::AudioProperties::ReadStyle
|
|
||||||
readStyleToTagLibReadStyle(ParserReadStyle readStyle)
|
|
||||||
{
|
{
|
||||||
switch (readStyle)
|
switch (readStyle)
|
||||||
{
|
{
|
||||||
@@ -289,13 +273,41 @@ readStyleToTagLibReadStyle(ParserReadStyle readStyle)
|
|||||||
throw LmsException{ "Cannot convert read style" };
|
throw LmsException{ "Cannot convert read style" };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TagMap constructTagMap(const TagLib::PropertyMap& properties)
|
||||||
|
{
|
||||||
|
TagMap tagMap;
|
||||||
|
|
||||||
|
for (const auto& [propertyName, propertyValues] : properties)
|
||||||
|
{
|
||||||
|
std::vector<std::string>& values{ tagMap[propertyName.upper().to8Bit(true)] };
|
||||||
|
for (const TagLib::String& propertyValue : propertyValues)
|
||||||
|
{
|
||||||
|
std::string trimedValue{ StringUtils::stringTrim(propertyValue.to8Bit(true)) };
|
||||||
|
if (!trimedValue.empty())
|
||||||
|
values.emplace_back(std::move(trimedValue));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tagMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
void mergeTagMaps(TagMap& dst, TagMap&& src)
|
||||||
|
{
|
||||||
|
for (auto&& [tag, values] : src)
|
||||||
|
{
|
||||||
|
if (dst.find(tag) == std::cend(dst))
|
||||||
|
dst[tag] = std::move(values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
TagLibParser::TagLibParser(ParserReadStyle readStyle)
|
TagLibParser::TagLibParser(ParserReadStyle readStyle)
|
||||||
: _readStyle{ readStyleToTagLibReadStyle(readStyle) }
|
: _readStyle{ readStyleToTagLibReadStyle(readStyle) }
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void TagLibParser::processTag(Track& track, const std::string& tag, const std::vector<std::string>& values, bool debug)
|
||||||
TagLibParser::processTag(Track& track, const std::string& tag, const std::vector<std::string>& values, bool debug)
|
|
||||||
{
|
{
|
||||||
if (debug)
|
if (debug)
|
||||||
std::cout << "[" << tag << "] = " << StringUtils::joinStrings(values, "*SEP*") << std::endl;
|
std::cout << "[" << tag << "] = " << StringUtils::joinStrings(values, "*SEP*") << std::endl;
|
||||||
@@ -371,39 +383,7 @@ TagLibParser::processTag(Track& track, const std::string& tag, const std::vector
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
std::optional<Track> TagLibParser::parse(const std::filesystem::path& p, bool debug)
|
||||||
TagMap
|
|
||||||
constructTagMap(const TagLib::PropertyMap& properties)
|
|
||||||
{
|
|
||||||
TagMap tagMap;
|
|
||||||
|
|
||||||
for (const auto& [propertyName, propertyValues] : properties)
|
|
||||||
{
|
|
||||||
std::vector<std::string>& values {tagMap[propertyName.upper().to8Bit(true)]};
|
|
||||||
for (const TagLib::String& propertyValue : propertyValues)
|
|
||||||
{
|
|
||||||
std::string trimedValue {StringUtils::stringTrim(propertyValue.to8Bit(true))};
|
|
||||||
if (!trimedValue.empty())
|
|
||||||
values.emplace_back(std::move(trimedValue));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return tagMap;
|
|
||||||
}
|
|
||||||
|
|
||||||
static
|
|
||||||
void
|
|
||||||
mergeTagMaps(TagMap& dst, TagMap&& src)
|
|
||||||
{
|
|
||||||
for (auto&& [tag, values] : src)
|
|
||||||
{
|
|
||||||
if (dst.find(tag) == std::cend(dst))
|
|
||||||
dst[tag] = std::move(values);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::optional<Track>
|
|
||||||
TagLibParser::parse(const std::filesystem::path& p, bool debug)
|
|
||||||
{
|
{
|
||||||
TagLib::FileRef f{ p.string().c_str(),
|
TagLib::FileRef f{ p.string().c_str(),
|
||||||
true, // read audio properties
|
true, // read audio properties
|
||||||
@@ -415,23 +395,19 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
|
|||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!f.audioProperties())
|
Track track;
|
||||||
|
|
||||||
|
if (const TagLib::AudioProperties* properties{ f.audioProperties() })
|
||||||
|
{
|
||||||
|
track.duration = std::chrono::milliseconds{ properties->lengthInMilliseconds() };
|
||||||
|
track.bitrate = static_cast<std::size_t>(properties->bitrate() * 1000);
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
LMS_LOG(METADATA, INFO) << "File '" << p.string() << "': no audio properties";
|
LMS_LOG(METADATA, INFO) << "File '" << p.string() << "': no audio properties";
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
Track track;
|
|
||||||
|
|
||||||
{
|
|
||||||
const TagLib::AudioProperties *properties {f.audioProperties() };
|
|
||||||
|
|
||||||
track.duration = std::chrono::milliseconds {properties->lengthInMilliseconds()};
|
|
||||||
|
|
||||||
MetaData::AudioStream audioStream {static_cast<unsigned>(properties->bitrate() * 1000)};
|
|
||||||
track.audioStreams = {audioStream};
|
|
||||||
}
|
|
||||||
|
|
||||||
TagMap tags{ constructTagMap(f.file()->properties()) };
|
TagMap tags{ constructTagMap(f.file()->properties()) };
|
||||||
|
|
||||||
auto getAPETags = [&](const TagLib::APE::Tag* apeTag)
|
auto getAPETags = [&](const TagLib::APE::Tag* apeTag)
|
||||||
|
|||||||
@@ -96,11 +96,6 @@ namespace MetaData
|
|||||||
std::optional<float> replayGain;
|
std::optional<float> replayGain;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct AudioStream
|
|
||||||
{
|
|
||||||
unsigned bitRate;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Track
|
struct Track
|
||||||
{
|
{
|
||||||
std::optional<UUID> mbid;
|
std::optional<UUID> mbid;
|
||||||
@@ -109,11 +104,11 @@ namespace MetaData
|
|||||||
std::optional<Medium> medium;
|
std::optional<Medium> medium;
|
||||||
std::optional<std::size_t> position; // in medium
|
std::optional<std::size_t> position; // in medium
|
||||||
Tags tags;
|
Tags tags;
|
||||||
std::chrono::milliseconds duration;
|
std::chrono::milliseconds duration{};
|
||||||
|
std::size_t bitrate{};
|
||||||
Wt::WDate date;
|
Wt::WDate date;
|
||||||
Wt::WDate originalDate;
|
Wt::WDate originalDate;
|
||||||
bool hasCover{};
|
bool hasCover{};
|
||||||
std::vector<AudioStream> audioStreams;
|
|
||||||
std::optional<UUID> acoustID;
|
std::optional<UUID> acoustID;
|
||||||
std::string copyright;
|
std::string copyright;
|
||||||
std::string copyrightURL;
|
std::string copyrightURL;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -56,12 +56,10 @@ namespace Cover
|
|||||||
&& size == other.size;
|
&& size == other.size;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
} // ns Cover
|
} // ns Cover
|
||||||
|
|
||||||
namespace std
|
namespace std
|
||||||
{
|
{
|
||||||
|
|
||||||
template<>
|
template<>
|
||||||
class hash<Cover::CacheEntryDesc>
|
class hash<Cover::CacheEntryDesc>
|
||||||
{
|
{
|
||||||
@@ -86,18 +84,15 @@ 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;
|
||||||
|
std::shared_ptr<Image::IEncodedImage> getDefault(Image::ImageSize width) override;
|
||||||
void flushCache() override;
|
void flushCache() override;
|
||||||
void setJpegQuality(unsigned quality) override;
|
void setJpegQuality(unsigned quality) override;
|
||||||
|
|
||||||
@@ -109,7 +104,6 @@ namespace Cover
|
|||||||
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;
|
||||||
|
|
||||||
|
|||||||
@@ -41,14 +41,14 @@ namespace Cover
|
|||||||
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 std::shared_ptr<Image::IEncodedImage> getDefault(Image::ImageSize width) = 0;
|
||||||
|
|
||||||
virtual void flushCache() = 0;
|
virtual void flushCache() = 0;
|
||||||
|
|
||||||
virtual void setJpegQuality(unsigned quality) = 0; // from 1 to 100
|
virtual void setJpegQuality(unsigned quality) = 0; // from 1 to 100
|
||||||
};
|
};
|
||||||
|
|
||||||
std::unique_ptr<ICoverService> createCoverService(Database::Db& db,
|
std::unique_ptr<ICoverService> createCoverService(Database::Db& db, const std::filesystem::path& execPath, const std::filesystem::path& defaultCoverPath);
|
||||||
const std::filesystem::path& execPath,
|
|
||||||
const std::filesystem::path& defaultCoverPath);
|
|
||||||
|
|
||||||
} // namespace CoverArt
|
} // namespace CoverArt
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -33,33 +33,29 @@ _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();
|
||||||
|
|
||||||
@@ -69,8 +65,7 @@ TrackBookmark::find(Session& session, UserId userId, TrackId trackId)
|
|||||||
.resultValue();
|
.resultValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackBookmark::pointer
|
TrackBookmark::pointer TrackBookmark::find(Session& session, TrackBookmarkId id)
|
||||||
TrackBookmark::find(Session& session, TrackBookmarkId id)
|
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
|
|||||||
@@ -36,22 +36,19 @@ _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();
|
||||||
|
|
||||||
@@ -60,8 +57,7 @@ TrackFeatures::find(Session& session, TrackFeaturesId id)
|
|||||||
.resultValue();
|
.resultValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackFeatures::pointer
|
TrackFeatures::pointer TrackFeatures::find(Session& session, TrackId trackId)
|
||||||
TrackFeatures::find(Session& session, TrackId trackId)
|
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
@@ -70,25 +66,22 @@ TrackFeatures::find(Session& session, TrackId 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;
|
||||||
|
|
||||||
|
|||||||
@@ -33,8 +33,8 @@
|
|||||||
#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)
|
TrackList::TrackList(std::string_view name, TrackListType type, bool isPublic, ObjectPtr<User> user)
|
||||||
: _name{ name }
|
: _name{ name }
|
||||||
, _type{ type }
|
, _type{ type }
|
||||||
@@ -46,14 +46,12 @@ TrackList::TrackList(std::string_view name, TrackListType type, bool isPublic, O
|
|||||||
assert(user);
|
assert(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackList::pointer
|
TrackList::pointer TrackList::create(Session& session, std::string_view name, TrackListType type, bool isPublic, ObjectPtr<User> user)
|
||||||
TrackList::create(Session& session, std::string_view name, TrackListType type, bool isPublic, ObjectPtr<User> user)
|
|
||||||
{
|
{
|
||||||
return session.getDboSession().add(std::unique_ptr<TrackList> {new TrackList{ name, type, isPublic, user }});
|
return session.getDboSession().add(std::unique_ptr<TrackList> {new TrackList{ name, type, isPublic, user }});
|
||||||
}
|
}
|
||||||
|
|
||||||
std::size_t
|
std::size_t TrackList::getCount(Session& session)
|
||||||
TrackList::getCount(Session& session)
|
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
@@ -61,8 +59,7 @@ TrackList::getCount(Session& session)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
TrackList::pointer
|
TrackList::pointer TrackList::find(Session& session, std::string_view name, TrackListType type, UserId userId)
|
||||||
TrackList::find(Session& session, std::string_view name, TrackListType type, UserId userId)
|
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
assert(userId.isValid());
|
assert(userId.isValid());
|
||||||
@@ -73,8 +70,7 @@ TrackList::find(Session& session, std::string_view name, TrackListType type, Use
|
|||||||
.where("user_id = ?").bind(userId).resultValue();
|
.where("user_id = ?").bind(userId).resultValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
RangeResults<TrackListId>
|
RangeResults<TrackListId> TrackList::find(Session& session, const FindParameters& params)
|
||||||
TrackList::find(Session& session, const FindParameters& params)
|
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
@@ -121,31 +117,27 @@ TrackList::find(Session& session, const FindParameters& params)
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Utils::execQuery(query, params.range);
|
return Utils::execQuery<TrackListId>(query, params.range);
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackList::pointer
|
TrackList::pointer TrackList::find(Session& session, TrackListId id)
|
||||||
TrackList::find(Session& session, TrackListId id)
|
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
return session.getDboSession().find<TrackList>().where("id = ?").bind(id).resultValue();
|
return session.getDboSession().find<TrackList>().where("id = ?").bind(id).resultValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool TrackList::isEmpty() const
|
||||||
TrackList::isEmpty() const
|
|
||||||
{
|
{
|
||||||
return _entries.empty();
|
return _entries.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::size_t
|
std::size_t TrackList::getCount() const
|
||||||
TrackList::getCount() const
|
|
||||||
{
|
{
|
||||||
return _entries.size();
|
return _entries.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackListEntry::pointer
|
TrackListEntry::pointer TrackList::getEntry(std::size_t pos) const
|
||||||
TrackList::getEntry(std::size_t pos) const
|
|
||||||
{
|
{
|
||||||
TrackListEntry::pointer res;
|
TrackListEntry::pointer res;
|
||||||
|
|
||||||
@@ -156,8 +148,7 @@ TrackList::getEntry(std::size_t pos) const
|
|||||||
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());
|
||||||
|
|
||||||
@@ -172,8 +163,7 @@ TrackList::getEntries(std::optional<Range> range) const
|
|||||||
return std::vector<TrackListEntry::pointer>(entries.begin(), entries.end());
|
return std::vector<TrackListEntry::pointer>(entries.begin(), entries.end());
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackListEntry::pointer
|
TrackListEntry::pointer TrackList::getEntryByTrackAndDateTime(ObjectPtr<Track> track, const Wt::WDateTime& dateTime) const
|
||||||
TrackList::getEntryByTrackAndDateTime(ObjectPtr<Track> track, const Wt::WDateTime& dateTime) const
|
|
||||||
{
|
{
|
||||||
assert(session());
|
assert(session());
|
||||||
|
|
||||||
@@ -184,276 +174,7 @@ TrackList::getEntryByTrackAndDateTime(ObjectPtr<Track> track, const Wt::WDateTim
|
|||||||
.resultValue();
|
.resultValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
std::vector<Cluster::pointer> TrackList::getClusters() const
|
||||||
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 query {session.query<Wt::Dbo::ptr<Artist>>(queryStr)};
|
|
||||||
query.join("track t ON t.id = t_a_l.track_id");
|
|
||||||
query.join("track_artist_link t_a_l ON t_a_l.artist_id = a.id");
|
|
||||||
query.join("tracklist_entry p_e ON p_e.track_id = t.id");
|
|
||||||
query.join("tracklist p ON p.id = p_e.tracklist_id");
|
|
||||||
|
|
||||||
query.where("p.id = ?").bind(tracklistId);
|
|
||||||
|
|
||||||
if (linkType)
|
|
||||||
query.where("t_a_l.type = ?").bind(*linkType);
|
|
||||||
|
|
||||||
if (!clusterIds.empty())
|
|
||||||
{
|
|
||||||
std::ostringstream oss;
|
|
||||||
oss << "a.id IN (SELECT DISTINCT a.id FROM artist a"
|
|
||||||
" INNER JOIN track t ON t.id = t_a_l.track_id"
|
|
||||||
" INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id"
|
|
||||||
" INNER JOIN cluster c ON c.id = t_c.cluster_id"
|
|
||||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
|
|
||||||
|
|
||||||
WhereClause clusterClause;
|
|
||||||
for (auto id : clusterIds)
|
|
||||||
{
|
|
||||||
clusterClause.Or(WhereClause("c.id = ?"));
|
|
||||||
query.bind(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
oss << " " << clusterClause.get();
|
|
||||||
oss << " GROUP BY t.id,a.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size() << ")";
|
|
||||||
|
|
||||||
query.where(oss.str());
|
|
||||||
}
|
|
||||||
|
|
||||||
return query;
|
|
||||||
}
|
|
||||||
|
|
||||||
static
|
|
||||||
Wt::Dbo::Query<Wt::Dbo::ptr<Release>>
|
|
||||||
createReleasesQuery(Wt::Dbo::Session& session, const std::string& queryStr, TrackListId tracklistId, const std::vector<ClusterId>& clusterIds)
|
|
||||||
{
|
|
||||||
auto query {session.query<Wt::Dbo::ptr<Release>>(queryStr)};
|
|
||||||
query.join("track t ON t.release_id = r.id");
|
|
||||||
query.join("tracklist_entry p_e ON p_e.track_id = t.id");
|
|
||||||
query.join("tracklist p ON p.id = p_e.tracklist_id");
|
|
||||||
|
|
||||||
query.where("p.id = ?").bind(tracklistId);
|
|
||||||
|
|
||||||
if (!clusterIds.empty())
|
|
||||||
{
|
|
||||||
std::ostringstream oss;
|
|
||||||
oss << "r.id IN (SELECT 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";
|
|
||||||
|
|
||||||
WhereClause clusterClause;
|
|
||||||
for (ClusterId id : clusterIds)
|
|
||||||
{
|
|
||||||
clusterClause.Or(WhereClause("c.id = ?"));
|
|
||||||
query.bind(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
oss << " " << clusterClause.get();
|
|
||||||
oss << " GROUP BY t.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size() << ")";
|
|
||||||
|
|
||||||
query.where(oss.str());
|
|
||||||
}
|
|
||||||
|
|
||||||
return query;
|
|
||||||
}
|
|
||||||
|
|
||||||
static
|
|
||||||
Wt::Dbo::Query<Wt::Dbo::ptr<Track>>
|
|
||||||
createTracksQuery(Wt::Dbo::Session& session, TrackListId tracklistId, const std::vector<ClusterId>& clusterIds)
|
|
||||||
{
|
|
||||||
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")};
|
|
||||||
|
|
||||||
query.where("p.id = ?").bind(tracklistId);
|
|
||||||
|
|
||||||
if (!clusterIds.empty())
|
|
||||||
{
|
|
||||||
std::ostringstream oss;
|
|
||||||
oss << "t.id IN (SELECT DISTINCT t.id FROM track t"
|
|
||||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
|
|
||||||
" INNER JOIN cluster c ON c.id = t_c.cluster_id";
|
|
||||||
|
|
||||||
WhereClause clusterClause;
|
|
||||||
for (auto id : clusterIds)
|
|
||||||
{
|
|
||||||
clusterClause.Or(WhereClause("c.id = ?")).bind(id.toString());
|
|
||||||
query.bind(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
oss << " " << clusterClause.get();
|
|
||||||
oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size() << ")";
|
|
||||||
|
|
||||||
query.where(oss.str());
|
|
||||||
}
|
|
||||||
|
|
||||||
return query;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<Artist::pointer>
|
|
||||||
TrackList::getArtists(const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, ArtistSortMethod sortMethod, std::optional<Range> range, bool& moreResults) const
|
|
||||||
{
|
|
||||||
assert(session());
|
|
||||||
|
|
||||||
auto query {createArtistsQuery(*session(), "SELECT a from artist a", getId(), clusterIds, linkType)
|
|
||||||
.groupBy("a.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)};
|
|
||||||
|
|
||||||
switch (sortMethod)
|
|
||||||
{
|
|
||||||
case ArtistSortMethod::None:
|
|
||||||
break;
|
|
||||||
case ArtistSortMethod::ByName:
|
|
||||||
query.orderBy("a.name COLLATE NOCASE");
|
|
||||||
break;
|
|
||||||
case ArtistSortMethod::BySortName:
|
|
||||||
query.orderBy("a.sort_name COLLATE NOCASE");
|
|
||||||
break;
|
|
||||||
case ArtistSortMethod::Random:
|
|
||||||
query.orderBy("RANDOM()");
|
|
||||||
break;
|
|
||||||
case ArtistSortMethod::LastWritten:
|
|
||||||
case ArtistSortMethod::StarredDateDesc:
|
|
||||||
assert(false); // Not implemented!
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> collection {query.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<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());
|
assert(session());
|
||||||
|
|
||||||
@@ -466,8 +187,7 @@ TrackList::getClusters() const
|
|||||||
return std::vector<Cluster::pointer>(res.begin(), res.end());
|
return std::vector<Cluster::pointer>(res.begin(), res.end());
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::vector<Cluster::pointer>>
|
std::vector<std::vector<Cluster::pointer>> TrackList::getClusterGroups(const std::vector<ClusterType::pointer>& clusterTypes, std::size_t size) const
|
||||||
TrackList::getClusterGroups(const std::vector<ClusterType::pointer>& clusterTypes, std::size_t size) const
|
|
||||||
{
|
{
|
||||||
assert(session());
|
assert(session());
|
||||||
std::vector<std::vector<Cluster::pointer>> res;
|
std::vector<std::vector<Cluster::pointer>> res;
|
||||||
@@ -517,8 +237,7 @@ TrackList::getClusterGroups(const std::vector<ClusterType::pointer>& clusterType
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool TrackList::hasTrack(TrackId trackId) const
|
||||||
TrackList::hasTrack(TrackId trackId) const
|
|
||||||
{
|
{
|
||||||
assert(session());
|
assert(session());
|
||||||
|
|
||||||
@@ -529,8 +248,7 @@ TrackList::hasTrack(TrackId trackId) const
|
|||||||
return res.size() > 0;
|
return res.size() > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Track::pointer>
|
std::vector<Track::pointer> TrackList::getSimilarTracks(std::optional<std::size_t> offset, std::optional<std::size_t> size) const
|
||||||
TrackList::getSimilarTracks(std::optional<std::size_t> offset, std::optional<std::size_t> size) const
|
|
||||||
{
|
{
|
||||||
assert(session());
|
assert(session());
|
||||||
|
|
||||||
@@ -552,8 +270,7 @@ TrackList::getSimilarTracks(std::optional<std::size_t> offset, std::optional<std
|
|||||||
return std::vector<Track::pointer>(res.begin(), res.end());
|
return std::vector<Track::pointer>(res.begin(), res.end());
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<TrackId>
|
std::vector<TrackId> TrackList::getTrackIds() const
|
||||||
TrackList::getTrackIds() const
|
|
||||||
{
|
{
|
||||||
assert(session());
|
assert(session());
|
||||||
|
|
||||||
@@ -563,8 +280,7 @@ TrackList::getTrackIds() const
|
|||||||
return std::vector<TrackId>(res.begin(), res.end());
|
return std::vector<TrackId>(res.begin(), res.end());
|
||||||
}
|
}
|
||||||
|
|
||||||
std::chrono::milliseconds
|
std::chrono::milliseconds TrackList::getDuration() const
|
||||||
TrackList::getDuration() const
|
|
||||||
{
|
{
|
||||||
assert(session());
|
assert(session());
|
||||||
|
|
||||||
@@ -576,89 +292,11 @@ TrackList::getDuration() const
|
|||||||
return query.resultValue();
|
return query.resultValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void TrackList::setLastModifiedDateTime(const Wt::WDateTime& dateTime)
|
||||||
TrackList::setLastModifiedDateTime(const Wt::WDateTime& dateTime)
|
|
||||||
{
|
{
|
||||||
_lastModifiedDateTime = Utils::normalizeDateTime(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)
|
TrackListEntry::TrackListEntry(ObjectPtr<Track> track, ObjectPtr<TrackList> tracklist, const Wt::WDateTime& dateTime)
|
||||||
: _dateTime{ Utils::normalizeDateTime(dateTime) }
|
: _dateTime{ Utils::normalizeDateTime(dateTime) }
|
||||||
, _track{ getDboPtr(track) }
|
, _track{ getDboPtr(track) }
|
||||||
@@ -668,26 +306,22 @@ TrackListEntry::TrackListEntry(ObjectPtr<Track> track, ObjectPtr<TrackList> trac
|
|||||||
assert(tracklist);
|
assert(tracklist);
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackListEntry::pointer
|
TrackListEntry::pointer TrackListEntry::create(Session& session, ObjectPtr<Track> track, ObjectPtr<TrackList> tracklist, const Wt::WDateTime& dateTime)
|
||||||
TrackListEntry::create(Session& session, ObjectPtr<Track> track, ObjectPtr<TrackList> tracklist, const Wt::WDateTime& dateTime)
|
|
||||||
{
|
{
|
||||||
return session.getDboSession().add(std::unique_ptr<TrackListEntry> {new TrackListEntry{ track, tracklist, dateTime }});
|
return session.getDboSession().add(std::unique_ptr<TrackListEntry> {new TrackListEntry{ track, tracklist, dateTime }});
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void TrackListEntry::onPostCreated()
|
||||||
TrackListEntry::onPostCreated()
|
|
||||||
{
|
{
|
||||||
_tracklist.modify()->setLastModifiedDateTime(Utils::normalizeDateTime(Wt::WDateTime::currentDateTime()));
|
_tracklist.modify()->setLastModifiedDateTime(Utils::normalizeDateTime(Wt::WDateTime::currentDateTime()));
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void TrackListEntry::onPreRemove()
|
||||||
TrackListEntry::onPreRemove()
|
|
||||||
{
|
{
|
||||||
_tracklist.modify()->setLastModifiedDateTime(Utils::normalizeDateTime(Wt::WDateTime::currentDateTime()));
|
_tracklist.modify()->setLastModifiedDateTime(Utils::normalizeDateTime(Wt::WDateTime::currentDateTime()));
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackListEntry::pointer
|
TrackListEntry::pointer TrackListEntry::getById(Session& session, TrackListEntryId id)
|
||||||
TrackListEntry::getById(Session& session, TrackListEntryId id)
|
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
|
||||||
@@ -33,52 +34,49 @@ namespace Database::Utils
|
|||||||
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)
|
|
||||||
{
|
{
|
||||||
RangeResults<T> res;
|
if (range)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
|
||||||
|
if (range)
|
||||||
|
applyRange(query, Range{ range->offset, range->size + 1 });
|
||||||
|
|
||||||
|
auto collection{ query.resultList() };
|
||||||
res.results.assign(collection.begin(), collection.end());
|
res.results.assign(collection.begin(), collection.end());
|
||||||
if (range.size && res.results.size() == static_cast<std::size_t>(range.size) + 1)
|
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.moreResults = true;
|
||||||
res.results.pop_back();
|
res.results.pop_back();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
res.moreResults = false;
|
res.moreResults = false;
|
||||||
|
|
||||||
res.range.offset = range.offset;
|
res.range.offset = range->offset;
|
||||||
res.range.size = res.results.size();
|
res.range.size = res.results.size();
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename T>
|
template <typename ResultType, typename Query>
|
||||||
RangeResults<typename T::pointer>
|
void execQuery(Query& query, std::optional<Range> range, std::function<void(const ResultType&)> func)
|
||||||
execQuery(Wt::Dbo::Query<Wt::Dbo::ptr<T>>& query, Range range)
|
|
||||||
{
|
{
|
||||||
RangeResults<typename T::pointer> res;
|
if (range)
|
||||||
|
applyRange(query, range);
|
||||||
|
|
||||||
auto collection {query.limit(range.size ? static_cast<int>(range.size) + 1 : -1)
|
for (const auto& res : query.resultList())
|
||||||
.offset(range.offset ? static_cast<int>(range.offset) : -1)
|
func(res);
|
||||||
.resultList()};
|
|
||||||
|
|
||||||
res.results.assign(collection.begin(), collection.end());
|
|
||||||
if (range.size && res.results.size() == static_cast<std::size_t>(range.size) + 1)
|
|
||||||
{
|
|
||||||
res.moreResults = true;
|
|
||||||
res.results.pop_back();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
res.moreResults = false;
|
|
||||||
|
|
||||||
res.range.offset = range.offset;
|
|
||||||
res.range.size = res.results.size();
|
|
||||||
return res;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Wt::WDateTime normalizeDateTime(const Wt::WDateTime& dateTime);
|
Wt::WDateTime normalizeDateTime(const Wt::WDateTime& dateTime);
|
||||||
|
|||||||
@@ -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 RangeResults<pointer> find(Session& session, const FindParameters& params);
|
||||||
|
static void find(Session& session, const FindParameters& params, std::function<void(const pointer& cluster)> _func);
|
||||||
static pointer find(Session& session, ClusterId id);
|
static pointer find(Session& session, ClusterId id);
|
||||||
static RangeResults<ClusterId> findOrphans(Session& session, Range range);
|
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>
|
||||||
|
|
||||||
@@ -48,7 +50,7 @@ namespace Database
|
|||||||
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
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ class Track final : public Object<Track, TrackId>
|
|||||||
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
|
||||||
@@ -82,7 +82,7 @@ class Track final : public Object<Track, TrackId>
|
|||||||
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; }
|
||||||
@@ -110,13 +110,14 @@ class Track final : public Object<Track, TrackId>
|
|||||||
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; }
|
||||||
@@ -127,6 +128,7 @@ class Track final : public Object<Track, TrackId>
|
|||||||
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 setBitrate(std::size_t bitrate) { _bitrate = bitrate; }
|
||||||
void setLastWriteTime(Wt::WDateTime time) { _fileLastWrite = time; }
|
void setLastWriteTime(Wt::WDateTime time) { _fileLastWrite = time; }
|
||||||
void setAddedTime(Wt::WDateTime time) { _fileAdded = time; }
|
void setAddedTime(Wt::WDateTime time) { _fileAdded = time; }
|
||||||
void setDate(const Wt::WDate& date) { _date = date; }
|
void setDate(const Wt::WDate& date) { _date = date; }
|
||||||
@@ -152,6 +154,7 @@ class Track final : public Object<Track, TrackId>
|
|||||||
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; }
|
||||||
|
std::size_t getBitrate() const { return _bitrate; }
|
||||||
const Wt::WDateTime& getLastWritten() const { return _fileLastWrite; }
|
const Wt::WDateTime& getLastWritten() const { return _fileLastWrite; }
|
||||||
std::optional<int> getYear() const;
|
std::optional<int> getYear() const;
|
||||||
std::optional<int> getOriginalYear() const;
|
std::optional<int> getOriginalYear() const;
|
||||||
@@ -185,6 +188,7 @@ class Track final : public Object<Track, TrackId>
|
|||||||
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, _bitrate, "bitrate");
|
||||||
Wt::Dbo::field(a, _date, "date");
|
Wt::Dbo::field(a, _date, "date");
|
||||||
Wt::Dbo::field(a, _originalDate, "original_date");
|
Wt::Dbo::field(a, _originalDate, "original_date");
|
||||||
Wt::Dbo::field(a, _filePath, "file_path");
|
Wt::Dbo::field(a, _filePath, "file_path");
|
||||||
@@ -219,6 +223,7 @@ class Track final : public Object<Track, TrackId>
|
|||||||
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{};
|
||||||
|
int _bitrate; // in bps
|
||||||
Wt::WDate _date;
|
Wt::WDate _date;
|
||||||
Wt::WDate _originalDate;
|
Wt::WDate _originalDate;
|
||||||
std::string _filePath;
|
std::string _filePath;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -50,22 +50,17 @@ class TrackList final : public Object<TrackList, TrackListId>
|
|||||||
public:
|
public:
|
||||||
TrackList() = default;
|
TrackList() = default;
|
||||||
|
|
||||||
// Stats utility
|
|
||||||
std::vector<ObjectPtr<Artist>> getTopArtists(const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range, bool& moreResults) const;
|
|
||||||
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;
|
|
||||||
|
|
||||||
// Search utility
|
// Search utility
|
||||||
struct FindParameters
|
struct FindParameters
|
||||||
{
|
{
|
||||||
std::vector<ClusterId> clusters; // if non empty, tracklists that have tracks that belong to these clusters
|
std::vector<ClusterId> clusters; // if non empty, tracklists that have tracks that belong to these clusters
|
||||||
Range range;
|
std::optional<Range> range;
|
||||||
std::optional<TrackListType> type;
|
std::optional<TrackListType> type;
|
||||||
UserId user; // only tracklists owned by this user
|
UserId user; // only tracklists owned by this user
|
||||||
TrackListSortMethod sortMethod{ TrackListSortMethod::None };
|
TrackListSortMethod sortMethod{ TrackListSortMethod::None };
|
||||||
|
|
||||||
FindParameters& setClusters(const std::vector<ClusterId>& _clusters) { clusters = _clusters; return *this; }
|
FindParameters& setClusters(const std::vector<ClusterId>& _clusters) { clusters = _clusters; return *this; }
|
||||||
FindParameters& setRange(Range _range) { range = _range; return *this; }
|
FindParameters& setRange(std::optional<Range> _range) { range = _range; return *this; }
|
||||||
FindParameters& setType(TrackListType _type) { type = _type; return *this; }
|
FindParameters& setType(TrackListType _type) { type = _type; return *this; }
|
||||||
FindParameters& setUser(UserId _user) { user = _user; return *this; }
|
FindParameters& setUser(UserId _user) { user = _user; return *this; }
|
||||||
FindParameters& setSortMethod(TrackListSortMethod _sortMethod) { sortMethod = _sortMethod; return *this; }
|
FindParameters& setSortMethod(TrackListSortMethod _sortMethod) { sortMethod = _sortMethod; return *this; }
|
||||||
@@ -93,14 +88,9 @@ class TrackList final : public Object<TrackList, TrackListId>
|
|||||||
std::vector<ObjectPtr<TrackListEntry>> getEntries(std::optional<Range> range = {}) const;
|
std::vector<ObjectPtr<TrackListEntry>> getEntries(std::optional<Range> range = {}) const;
|
||||||
ObjectPtr<TrackListEntry> getEntryByTrackAndDateTime(ObjectPtr<Track> track, const Wt::WDateTime& dateTime) 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;
|
RangeResults<ObjectPtr<Artist>> getArtists(const std::vector<ClusterId>& clusters, std::optional<TrackArtistLinkType> linkType, ArtistSortMethod sortMethod, std::optional<Range> range, bool& moreResults) const;
|
||||||
std::vector<ObjectPtr<Release>> getReleases(const std::vector<ClusterId>& clusters, std::optional<Range> range, bool& moreResults) const;
|
RangeResults<ObjectPtr<Release>> getReleases(const std::vector<ClusterId>& clusters, std::optional<Range> range, bool& moreResults) const;
|
||||||
std::vector<ObjectPtr<Track>> getTracks(const std::vector<ClusterId>& clusters, std::optional<Range> range, bool& moreResults) const;
|
RangeResults<ObjectPtr<Track>> getTracks(const std::vector<ClusterId>& clusters, std::optional<Range> range, bool& moreResults) const;
|
||||||
|
|
||||||
// Sorted by date time
|
|
||||||
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;
|
std::vector<TrackId> getTrackIds() const;
|
||||||
std::chrono::milliseconds getDuration() const;
|
std::chrono::milliseconds getDuration() const;
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ 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
|
||||||
@@ -37,10 +37,26 @@ namespace Database
|
|||||||
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; }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Func must return true to continue iterating
|
||||||
|
template <typename Func>
|
||||||
|
void foreachSubRange(Range range, std::size_t subRangeSize, Func&& func)
|
||||||
|
{
|
||||||
|
assert(subRangeSize > 0);
|
||||||
|
|
||||||
|
Range subRange{ range.offset, std::min(range.size, subRangeSize) };
|
||||||
|
while (subRange.size > 0)
|
||||||
|
{
|
||||||
|
if (!func(subRange))
|
||||||
|
break;
|
||||||
|
|
||||||
|
subRange.offset += subRange.size;
|
||||||
|
subRange.size = std::min(subRangeSize, range.size - (subRange.offset - range.offset));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
template <typename T>
|
template <typename T>
|
||||||
struct RangeResults
|
struct RangeResults
|
||||||
{
|
{
|
||||||
@@ -145,8 +161,8 @@ namespace Database
|
|||||||
Writer = 10,
|
Writer = 10,
|
||||||
};
|
};
|
||||||
|
|
||||||
// User selectable audio file formats
|
// User selectable transcoding output formats
|
||||||
enum class AudioFormat
|
enum class TranscodingOutputFormat
|
||||||
{
|
{
|
||||||
MP3 = 1,
|
MP3 = 1,
|
||||||
OGG_OPUS = 2,
|
OGG_OPUS = 2,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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" };
|
||||||
|
|||||||
@@ -42,8 +42,7 @@ TmpDatabase::TmpDatabase()
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
Database::Db&
|
Database::Db& TmpDatabase::getDb()
|
||||||
TmpDatabase::getDb()
|
|
||||||
{
|
{
|
||||||
return _db;
|
return _db;
|
||||||
}
|
}
|
||||||
@@ -53,8 +52,7 @@ DatabaseFixture::~DatabaseFixture()
|
|||||||
testDatabaseEmpty();
|
testDatabaseEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void DatabaseFixture::SetUpTestCase()
|
||||||
DatabaseFixture::SetUpTestCase()
|
|
||||||
{
|
{
|
||||||
_tmpDb = std::make_unique<TmpDatabase>();
|
_tmpDb = std::make_unique<TmpDatabase>();
|
||||||
{
|
{
|
||||||
@@ -65,8 +63,8 @@ DatabaseFixture::SetUpTestCase()
|
|||||||
// 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();
|
||||||
@@ -75,14 +73,12 @@ DatabaseFixture::SetUpTestCase()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void DatabaseFixture::TearDownTestCase()
|
||||||
DatabaseFixture::TearDownTestCase()
|
|
||||||
{
|
{
|
||||||
_tmpDb.reset();
|
_tmpDb.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void DatabaseFixture::testDatabaseEmpty()
|
||||||
DatabaseFixture::testDatabaseEmpty()
|
|
||||||
{
|
{
|
||||||
using namespace Database;
|
using namespace Database;
|
||||||
|
|
||||||
@@ -124,6 +120,44 @@ TEST_F(DatabaseFixture, Common_subRangeEmpty)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,303 +149,6 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrack)
|
|||||||
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)
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|
||||||
|
struct FindParameters
|
||||||
|
{
|
||||||
|
Database::UserId user;
|
||||||
|
std::vector<Database::ClusterId> clusters; // if non empty, at least one artist that belongs to these clusters
|
||||||
|
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 star(Database::UserId userId, Database::ArtistId artistId) = 0;
|
||||||
virtual void unstar(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 bool isStarred(Database::UserId userId, Database::ArtistId artistId) = 0;
|
||||||
virtual Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::ArtistId artistId) = 0;
|
virtual Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::ArtistId artistId) = 0;
|
||||||
virtual ArtistContainer getStarredArtists(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, std::optional<Database::TrackArtistLinkType> linkType, Database::ArtistSortMethod sortMethod, Database::Range range) = 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,22 +33,17 @@
|
|||||||
|
|
||||||
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)
|
||||||
}
|
|
||||||
|
|
||||||
std::unique_ptr<IRecommendationService>
|
|
||||||
createRecommendationService(Database::Db& db)
|
|
||||||
{
|
{
|
||||||
return std::make_unique<RecommendationService>(db);
|
return std::make_unique<RecommendationService>(db);
|
||||||
}
|
}
|
||||||
@@ -56,217 +51,73 @@ namespace Recommendation
|
|||||||
RecommendationService::RecommendationService(Database::Db& db)
|
RecommendationService::RecommendationService(Database::Db& db)
|
||||||
: _db{ db }
|
: _db{ db }
|
||||||
{
|
{
|
||||||
|
load();
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackContainer
|
TrackContainer RecommendationService::findSimilarTracks(Database::TrackListId trackListId, std::size_t maxCount) const
|
||||||
RecommendationService::findSimilarTracks(Database::TrackListId trackListId, std::size_t maxCount) const
|
|
||||||
{
|
{
|
||||||
TrackContainer res;
|
TrackContainer res;
|
||||||
|
|
||||||
std::shared_lock lock {_enginesMutex};
|
if (!_engine)
|
||||||
for (const auto& engineType : _enginePriorities)
|
|
||||||
{
|
|
||||||
auto itEngine {_engines.find(engineType)};
|
|
||||||
if (itEngine == std::cend(_engines))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
res = itEngine->second->findSimilarTracksFromTrackList(trackListId, maxCount);
|
|
||||||
if (!res.empty())
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
|
|
||||||
|
return _engine->findSimilarTracksFromTrackList(trackListId, maxCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackContainer
|
TrackContainer RecommendationService::findSimilarTracks(const std::vector<Database::TrackId>& trackIds, std::size_t maxCount) const
|
||||||
RecommendationService::findSimilarTracks(const std::vector<Database::TrackId>& trackIds, std::size_t maxCount) const
|
|
||||||
{
|
{
|
||||||
TrackContainer res;
|
TrackContainer res;
|
||||||
|
|
||||||
std::shared_lock lock {_enginesMutex};
|
if (!_engine)
|
||||||
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 tracks";
|
|
||||||
|
|
||||||
const IEngine& engine {*itEngine->second};
|
|
||||||
res = engine.findSimilarTracks(trackIds, maxCount);
|
|
||||||
if (!res.empty())
|
|
||||||
{
|
|
||||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar tracks using engine '" << engineTypeToString(engineType) << "'";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
|
|
||||||
|
return _engine->findSimilarTracks(trackIds, maxCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
ReleaseContainer
|
ReleaseContainer RecommendationService::getSimilarReleases(Database::ReleaseId releaseId, std::size_t maxCount) const
|
||||||
RecommendationService::getSimilarReleases(Database::ReleaseId releaseId, std::size_t maxCount) const
|
|
||||||
{
|
{
|
||||||
ReleaseContainer res;
|
ReleaseContainer res;
|
||||||
|
|
||||||
std::shared_lock lock {_enginesMutex};
|
if (!_engine)
|
||||||
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";
|
|
||||||
|
|
||||||
const IEngine& engine {*itEngine->second};
|
|
||||||
res = engine.getSimilarReleases(releaseId, maxCount);
|
|
||||||
if (!res.empty())
|
|
||||||
{
|
|
||||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar releases using engine '" << engineTypeToString(engineType) << "'";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
LMS_LOG(RECOMMENDATION, DEBUG) << "No result using engine '" << engineTypeToString(engineType) << "'";
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
|
|
||||||
|
return _engine->getSimilarReleases(releaseId, maxCount);;
|
||||||
}
|
}
|
||||||
|
|
||||||
ArtistContainer
|
ArtistContainer RecommendationService::getSimilarArtists(Database::ArtistId artistId, EnumSet<Database::TrackArtistLinkType> linkTypes, std::size_t maxCount) const
|
||||||
RecommendationService::getSimilarArtists(Database::ArtistId artistId, EnumSet<Database::TrackArtistLinkType> linkTypes, std::size_t maxCount) const
|
|
||||||
{
|
{
|
||||||
ArtistContainer res;
|
ArtistContainer res;
|
||||||
|
|
||||||
std::shared_lock lock {_enginesMutex};
|
if (!_engine)
|
||||||
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;
|
||||||
}
|
|
||||||
}
|
return _engine->getSimilarArtists(artistId, linkTypes, maxCount);
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
static
|
void RecommendationService::load()
|
||||||
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;
|
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()))
|
switch (getSimilarityEngineType(_db.getTLSSession()))
|
||||||
{
|
{
|
||||||
case ScanSettings::SimilarityEngineType::Clusters:
|
case ScanSettings::SimilarityEngineType::Clusters:
|
||||||
_enginePriorities = {EngineType::Clusters};
|
if (_engineType != EngineType::Clusters)
|
||||||
enginesToLoad.try_emplace(EngineType::Clusters, createClustersEngine(_db));
|
{
|
||||||
|
_engineType = EngineType::Clusters;
|
||||||
|
_engine = createClustersEngine(_db);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ScanSettings::SimilarityEngineType::Features:
|
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:
|
case ScanSettings::SimilarityEngineType::None:
|
||||||
_enginePriorities.clear();
|
_engineType.reset();
|
||||||
|
_engine.reset();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
assert(_pendingEngines.empty());
|
if (_engine)
|
||||||
for (auto& [engineType, engine] : enginesToLoad)
|
_engine->load(false);
|
||||||
_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";
|
|
||||||
}
|
|
||||||
|
|
||||||
} // ns Similarity
|
} // ns Similarity
|
||||||
|
|||||||
@@ -19,11 +19,7 @@
|
|||||||
|
|
||||||
#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"
|
||||||
@@ -48,13 +44,10 @@ namespace Recommendation
|
|||||||
~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;
|
||||||
@@ -66,19 +59,8 @@ namespace Recommendation
|
|||||||
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)";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -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;
|
||||||
|
|||||||
@@ -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!";
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
-22
@@ -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
|
||||||
|
{
|
||||||
|
class ScanStepComputeClusterStats : public ScanStepBase
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using ScanStepBase::ScanStepBase;
|
||||||
|
|
||||||
namespace Av
|
private:
|
||||||
{
|
ScanStep getStep() const override { return ScanStep::ComputeClusterStats; }
|
||||||
struct InputFileParameters
|
std::string_view getStepName() const override { return "Compute cluster stats"; }
|
||||||
{
|
void process(ScanContext& context) override;
|
||||||
std::filesystem::path trackPath;
|
|
||||||
std::chrono::milliseconds duration;
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
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
|
|
||||||
|
|
||||||
@@ -30,8 +30,7 @@
|
|||||||
|
|
||||||
namespace Scanner
|
namespace Scanner
|
||||||
{
|
{
|
||||||
void
|
void ScanStepRemoveOrphanDbFiles::process(ScanContext& context)
|
||||||
ScanStepRemoveOrphanDbFiles::process(ScanContext& context)
|
|
||||||
{
|
{
|
||||||
removeOrphanTracks(context);
|
removeOrphanTracks(context);
|
||||||
removeOrphanClusters();
|
removeOrphanClusters();
|
||||||
@@ -108,8 +107,7 @@ namespace Scanner
|
|||||||
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;
|
||||||
|
|
||||||
@@ -118,7 +116,7 @@ namespace Scanner
|
|||||||
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) };
|
||||||
@@ -127,8 +125,7 @@ namespace Scanner
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void ScanStepRemoveOrphanDbFiles::removeOrphanArtists()
|
||||||
ScanStepRemoveOrphanDbFiles::removeOrphanArtists()
|
|
||||||
{
|
{
|
||||||
using namespace Database;
|
using namespace Database;
|
||||||
|
|
||||||
@@ -137,7 +134,7 @@ namespace Scanner
|
|||||||
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) };
|
||||||
@@ -146,8 +143,7 @@ namespace Scanner
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void ScanStepRemoveOrphanDbFiles::removeOrphanReleases()
|
||||||
ScanStepRemoveOrphanDbFiles::removeOrphanReleases()
|
|
||||||
{
|
{
|
||||||
using namespace Database;
|
using namespace Database;
|
||||||
|
|
||||||
@@ -157,7 +153,7 @@ namespace Scanner
|
|||||||
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) };
|
||||||
@@ -166,8 +162,7 @@ namespace Scanner
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool ScanStepRemoveOrphanDbFiles::checkFile(const std::filesystem::path& p)
|
||||||
ScanStepRemoveOrphanDbFiles::checkFile(const std::filesystem::path& p)
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -309,7 +309,7 @@ namespace Scanner
|
|||||||
_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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,22 +405,7 @@ namespace Scanner
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
|
||||||
// - the duration is not null
|
|
||||||
if (trackInfo->audioStreams.empty())
|
|
||||||
{
|
|
||||||
LMS_LOG(DBUPDATER, DEBUG) << "Skipped '" << file.string() << "' (no audio stream found)";
|
|
||||||
|
|
||||||
// If Track exists here, delete it!
|
|
||||||
if (track)
|
|
||||||
{
|
|
||||||
track.remove();
|
|
||||||
stats.deletions++;
|
|
||||||
}
|
|
||||||
stats.errors.emplace_back(ScanError {file, ScanErrorType::NoAudioTrack});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (trackInfo->duration == std::chrono::milliseconds::zero())
|
if (trackInfo->duration == std::chrono::milliseconds::zero())
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, DEBUG) << "Skipped '" << file.string() << "' (duration is 0)";
|
LMS_LOG(DBUPDATER, DEBUG) << "Skipped '" << file.string() << "' (duration is 0)";
|
||||||
@@ -513,6 +498,7 @@ namespace Scanner
|
|||||||
track.modify()->setLastWriteTime(lastWriteTime);
|
track.modify()->setLastWriteTime(lastWriteTime);
|
||||||
track.modify()->setName(title);
|
track.modify()->setName(title);
|
||||||
track.modify()->setDuration(trackInfo->duration);
|
track.modify()->setDuration(trackInfo->duration);
|
||||||
|
track.modify()->setBitrate(trackInfo->bitrate);
|
||||||
track.modify()->setAddedTime(Wt::WDateTime::currentDateTime());
|
track.modify()->setAddedTime(Wt::WDateTime::currentDateTime());
|
||||||
track.modify()->setTrackNumber(trackInfo->position);
|
track.modify()->setTrackNumber(trackInfo->position);
|
||||||
track.modify()->setDiscNumber(trackInfo->medium ? trackInfo->medium->position : std::nullopt);
|
track.modify()->setDiscNumber(trackInfo->medium ? trackInfo->medium->position : std::nullopt);
|
||||||
|
|||||||
@@ -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,13 +35,15 @@
|
|||||||
#include "ScanStepDiscoverFiles.hpp"
|
#include "ScanStepDiscoverFiles.hpp"
|
||||||
#include "ScanStepRemoveOrphanDbFiles.hpp"
|
#include "ScanStepRemoveOrphanDbFiles.hpp"
|
||||||
#include "ScanStepScanFiles.hpp"
|
#include "ScanStepScanFiles.hpp"
|
||||||
|
#include "ScanStepComputeClusterStats.hpp"
|
||||||
|
|
||||||
|
namespace Scanner
|
||||||
|
{
|
||||||
using namespace Database;
|
using namespace Database;
|
||||||
|
|
||||||
namespace {
|
namespace
|
||||||
|
{
|
||||||
Wt::WDate
|
Wt::WDate getNextMonday(Wt::WDate current)
|
||||||
getNextMonday(Wt::WDate current)
|
|
||||||
{
|
{
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
@@ -52,8 +53,7 @@ getNextMonday(Wt::WDate current)
|
|||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
|
|
||||||
Wt::WDate
|
Wt::WDate getNextFirstOfMonth(Wt::WDate current)
|
||||||
getNextFirstOfMonth(Wt::WDate current)
|
|
||||||
{
|
{
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
@@ -62,20 +62,15 @@ getNextFirstOfMonth(Wt::WDate current)
|
|||||||
|
|
||||||
return current;
|
return current;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
namespace Scanner {
|
std::unique_ptr<IScannerService> createScannerService(Db& db)
|
||||||
|
|
||||||
std::unique_ptr<IScannerService>
|
|
||||||
createScannerService(Db& db, Recommendation::IRecommendationService& recommendationService)
|
|
||||||
{
|
{
|
||||||
return std::make_unique<ScannerService>(db, recommendationService);
|
return std::make_unique<ScannerService>(db);
|
||||||
}
|
}
|
||||||
|
|
||||||
ScannerService::ScannerService(Db& db, Recommendation::IRecommendationService& recommendationService)
|
ScannerService::ScannerService(Db& db)
|
||||||
: _recommendationService {recommendationService}
|
: _db{ db }
|
||||||
, _db {db}
|
|
||||||
, _dbSession{ db }
|
, _dbSession{ db }
|
||||||
{
|
{
|
||||||
_ioService.setThreadCount(1);
|
_ioService.setThreadCount(1);
|
||||||
@@ -92,8 +87,7 @@ ScannerService::~ScannerService()
|
|||||||
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 };
|
||||||
|
|
||||||
@@ -102,30 +96,22 @@ ScannerService::start()
|
|||||||
if (_abortScan)
|
if (_abortScan)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_recommendationService.load(false,
|
|
||||||
[](const Recommendation::Progress& progress)
|
|
||||||
{
|
|
||||||
LMS_LOG(DBUPDATER, DEBUG) << "Reloading recommendation : " << progress.processedElems << "/" << progress.totalElems;
|
|
||||||
});
|
|
||||||
scheduleNextScan();
|
scheduleNextScan();
|
||||||
});
|
});
|
||||||
|
|
||||||
_ioService.start();
|
_ioService.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void ScannerService::stop()
|
||||||
ScannerService::stop()
|
|
||||||
{
|
{
|
||||||
std::scoped_lock lock{ _controlMutex };
|
std::scoped_lock lock{ _controlMutex };
|
||||||
|
|
||||||
_abortScan = true;
|
_abortScan = true;
|
||||||
_scheduleTimer.cancel();
|
_scheduleTimer.cancel();
|
||||||
_recommendationService.cancelLoad();
|
|
||||||
_ioService.stop();
|
_ioService.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void ScannerService::abortScan()
|
||||||
ScannerService::abortScan()
|
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, DEBUG) << "Aborting scan...";
|
LMS_LOG(DBUPDATER, DEBUG) << "Aborting scan...";
|
||||||
std::scoped_lock lock{ _controlMutex };
|
std::scoped_lock lock{ _controlMutex };
|
||||||
@@ -134,7 +120,6 @@ ScannerService::abortScan()
|
|||||||
|
|
||||||
_abortScan = true;
|
_abortScan = true;
|
||||||
_scheduleTimer.cancel();
|
_scheduleTimer.cancel();
|
||||||
_recommendationService.cancelLoad();
|
|
||||||
_ioService.stop();
|
_ioService.stop();
|
||||||
LMS_LOG(DBUPDATER, DEBUG) << "Scan abort done!";
|
LMS_LOG(DBUPDATER, DEBUG) << "Scan abort done!";
|
||||||
|
|
||||||
@@ -142,8 +127,7 @@ ScannerService::abortScan()
|
|||||||
_ioService.start();
|
_ioService.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void ScannerService::requestImmediateScan(bool force)
|
||||||
ScannerService::requestImmediateScan(bool force)
|
|
||||||
{
|
{
|
||||||
abortScan();
|
abortScan();
|
||||||
_ioService.post([=]()
|
_ioService.post([=]()
|
||||||
@@ -155,8 +139,7 @@ ScannerService::requestImmediateScan(bool force)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void ScannerService::requestReload()
|
||||||
ScannerService::requestReload()
|
|
||||||
{
|
{
|
||||||
abortScan();
|
abortScan();
|
||||||
_ioService.post([=]()
|
_ioService.post([=]()
|
||||||
@@ -168,8 +151,7 @@ ScannerService::requestReload()
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ScannerService::Status
|
ScannerService::Status ScannerService::getStatus() const
|
||||||
ScannerService::getStatus() const
|
|
||||||
{
|
{
|
||||||
Status res;
|
Status res;
|
||||||
|
|
||||||
@@ -183,8 +165,7 @@ ScannerService::getStatus() const
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void ScannerService::scheduleNextScan()
|
||||||
ScannerService::scheduleNextScan()
|
|
||||||
{
|
{
|
||||||
LMS_LOG(DBUPDATER, DEBUG) << "Scheduling next scan";
|
LMS_LOG(DBUPDATER, DEBUG) << "Scheduling next scan";
|
||||||
|
|
||||||
@@ -237,8 +218,7 @@ ScannerService::scheduleNextScan()
|
|||||||
_events.scanScheduled.emit(_nextScheduledScan);
|
_events.scanScheduled.emit(_nextScheduledScan);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void ScannerService::scheduleScan(bool force, const Wt::WDateTime& dateTime)
|
||||||
ScannerService::scheduleScan(bool force, const Wt::WDateTime& dateTime)
|
|
||||||
{
|
{
|
||||||
auto cb{ [=](boost::system::error_code ec)
|
auto cb{ [=](boost::system::error_code ec)
|
||||||
{
|
{
|
||||||
@@ -266,8 +246,7 @@ ScannerService::scheduleScan(bool force, const Wt::WDateTime& dateTime)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void ScannerService::scan(bool forceScan)
|
||||||
ScannerService::scan(bool forceScan)
|
|
||||||
{
|
{
|
||||||
_events.scanStarted.emit();
|
_events.scanStarted.emit();
|
||||||
|
|
||||||
@@ -327,8 +306,7 @@ ScannerService::scan(bool forceScan)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void ScannerService::refreshScanSettings()
|
||||||
ScannerService::refreshScanSettings()
|
|
||||||
{
|
{
|
||||||
ScannerSettings newSettings{ readSettings() };
|
ScannerSettings newSettings{ readSettings() };
|
||||||
if (_settings == newSettings)
|
if (_settings == newSettings)
|
||||||
@@ -357,11 +335,11 @@ ScannerService::refreshScanSettings()
|
|||||||
_scanSteps.push_back(std::make_unique<ScanStepDiscoverFiles>(params));
|
_scanSteps.push_back(std::make_unique<ScanStepDiscoverFiles>(params));
|
||||||
_scanSteps.push_back(std::make_unique<ScanStepScanFiles>(params));
|
_scanSteps.push_back(std::make_unique<ScanStepScanFiles>(params));
|
||||||
_scanSteps.push_back(std::make_unique<ScanStepRemoveOrphanDbFiles>(params));
|
_scanSteps.push_back(std::make_unique<ScanStepRemoveOrphanDbFiles>(params));
|
||||||
|
_scanSteps.push_back(std::make_unique<ScanStepComputeClusterStats>(params));
|
||||||
_scanSteps.push_back(std::make_unique<ScanStepCheckDuplicatedDbFiles>(params));
|
_scanSteps.push_back(std::make_unique<ScanStepCheckDuplicatedDbFiles>(params));
|
||||||
}
|
}
|
||||||
|
|
||||||
ScannerSettings
|
ScannerSettings ScannerService::readSettings()
|
||||||
ScannerService::readSettings()
|
|
||||||
{
|
{
|
||||||
ScannerSettings newSettings;
|
ScannerSettings newSettings;
|
||||||
|
|
||||||
@@ -381,7 +359,6 @@ ScannerService::readSettings()
|
|||||||
std::transform(std::cbegin(fileExtensions), std::end(fileExtensions), std::back_inserter(newSettings.supportedExtensions),
|
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()) }; });
|
[](const std::filesystem::path& extension) { return std::filesystem::path{ StringUtils::stringToLower(extension.string()) }; });
|
||||||
}
|
}
|
||||||
newSettings.similarityServiceType = scanSettings->getSimilarityEngineType();
|
|
||||||
newSettings.mediaDirectory = scanSettings->getMediaDirectory();
|
newSettings.mediaDirectory = scanSettings->getMediaDirectory();
|
||||||
|
|
||||||
const auto clusterTypes = scanSettings->getClusterTypes();
|
const auto clusterTypes = scanSettings->getClusterTypes();
|
||||||
@@ -397,8 +374,7 @@ ScannerService::readSettings()
|
|||||||
return newSettings;
|
return newSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void ScannerService::notifyInProgress(const ScanStepStats& stepStats)
|
||||||
ScannerService::notifyInProgress(const ScanStepStats& stepStats)
|
|
||||||
{
|
{
|
||||||
{
|
{
|
||||||
std::unique_lock lock{ _statusMutex };
|
std::unique_lock lock{ _statusMutex };
|
||||||
@@ -410,8 +386,7 @@ ScannerService::notifyInProgress(const ScanStepStats& stepStats)
|
|||||||
_lastScanInProgressEmit = now;
|
_lastScanInProgressEmit = now;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void ScannerService::notifyInProgressIfNeeded(const ScanStepStats& stepStats)
|
||||||
ScannerService::notifyInProgressIfNeeded(const ScanStepStats& stepStats)
|
|
||||||
{
|
{
|
||||||
std::chrono::system_clock::time_point now{ std::chrono::system_clock::now() };
|
std::chrono::system_clock::time_point now{ std::chrono::system_clock::now() };
|
||||||
|
|
||||||
|
|||||||
@@ -38,23 +38,16 @@
|
|||||||
#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;
|
||||||
@@ -80,13 +73,12 @@ namespace Scanner
|
|||||||
// 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;
|
std::mutex _controlMutex;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -65,8 +65,9 @@ namespace Scanner
|
|||||||
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
|
||||||
|
|||||||
@@ -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{ };
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
{
|
||||||
|
Feedback::IFeedbackService::ArtistFindParameters artistFindParams;
|
||||||
|
artistFindParams.setUser(context.userId);
|
||||||
|
artistFindParams.setSortMethod(ArtistSortMethod::BySortName);
|
||||||
|
for (const ArtistId artistId : feedbackService.findStarredArtists(artistFindParams).results)
|
||||||
{
|
{
|
||||||
if (auto artist{ Artist::find(context.dbSession, artistId) })
|
if (auto artist{ Artist::find(context.dbSession, artistId) })
|
||||||
starredNode.addArrayChild("artist", createArtistNode(context, artist, user, id3));
|
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)
|
|
||||||
|
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));
|
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") };
|
||||||
|
|||||||
@@ -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)
|
|
||||||
{
|
{
|
||||||
for (const auto& [str, avFormat] : std::initializer_list<std::pair<std::string_view, Av::Format>>{
|
std::optional<Av::Transcoding::OutputFormat> subsonicStreamFormatToAvOutputFormat(std::string_view format)
|
||||||
{"mp3", Av::Format::MP3},
|
{
|
||||||
{"opus", Av::Format::OGG_OPUS},
|
for (const auto& [str, avFormat] : std::initializer_list<std::pair<std::string_view, Av::Transcoding::OutputFormat>>{
|
||||||
{"vorbis", Av::Format::OGG_VORBIS},
|
{"mp3", Av::Transcoding::OutputFormat::MP3},
|
||||||
|
{"opus", Av::Transcoding::OutputFormat::OGG_OPUS},
|
||||||
|
{"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;
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -24,18 +24,17 @@
|
|||||||
#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)
|
||||||
{
|
{
|
||||||
@@ -50,10 +49,6 @@ FileResourceHandler::processRequest(const Wt::Http::Request& request, Wt::Http::
|
|||||||
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()) };
|
||||||
@@ -92,9 +87,13 @@ FileResourceHandler::processRequest(const Wt::Http::Request& request, Wt::Http::
|
|||||||
{
|
{
|
||||||
LMS_LOG(UTILS, DEBUG) << "No range requested";
|
LMS_LOG(UTILS, DEBUG) << "No range requested";
|
||||||
|
|
||||||
|
response.setStatus(200);
|
||||||
_beyondLastByte = fileSize;
|
_beyondLastByte = fileSize;
|
||||||
response.setContentLength(_beyondLastByte);
|
response.setContentLength(_beyondLastByte);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LMS_LOG(UTILS, DEBUG) << "Mimetype set to '" << _mimeType << "'";
|
||||||
|
response.setMimeType(_mimeType);
|
||||||
}
|
}
|
||||||
else if (!ifs)
|
else if (!ifs)
|
||||||
{
|
{
|
||||||
@@ -128,4 +127,3 @@ FileResourceHandler::processRequest(const Wt::Http::Request& request, Wt::Http::
|
|||||||
LMS_LOG(UTILS, DEBUG) << "Job complete!";
|
LMS_LOG(UTILS, DEBUG) << "Job complete!";
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,20 +20,23 @@
|
|||||||
#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;
|
||||||
|
std::string _mimeType;
|
||||||
::uint64_t _beyondLastByte{};
|
::uint64_t _beyondLastByte{};
|
||||||
::uint64_t _offset{};
|
::uint64_t _offset{};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ const char* getModuleName(Module mod)
|
|||||||
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";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ enum class Module
|
|||||||
SCROBBLING,
|
SCROBBLING,
|
||||||
SERVICE,
|
SERVICE,
|
||||||
RECOMMENDATION,
|
RECOMMENDATION,
|
||||||
TRANSCODE,
|
TRANSCODING,
|
||||||
UI,
|
UI,
|
||||||
UTILS,
|
UTILS,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
@@ -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([&]
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user