Merge branch 'develop' for release v3.33.0
This commit is contained in:
+1
-1
@@ -20,7 +20,7 @@ find_package(Wt REQUIRED COMPONENTS Wt Dbo DboSqlite3 HTTP)
|
||||
pkg_check_modules(Taglib REQUIRED IMPORTED_TARGET taglib)
|
||||
pkg_check_modules(Config++ REQUIRED IMPORTED_TARGET libconfig++)
|
||||
pkg_check_modules(GraphicsMagick++ IMPORTED_TARGET GraphicsMagick++)
|
||||
pkg_check_modules(LIBAV IMPORTED_TARGET libavutil libavformat)
|
||||
pkg_check_modules(LIBAV IMPORTED_TARGET libavcodec libavutil libavformat)
|
||||
find_package(PAM)
|
||||
find_package(STB)
|
||||
|
||||
|
||||
@@ -44,12 +44,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="${id:tags}">
|
||||
${tr:Lms.Admin.Database.tags}
|
||||
<label class="form-label" for="${id:clusters}">
|
||||
${tr:Lms.clusters}
|
||||
</label>
|
||||
${tags class="form-control"}
|
||||
${clusters class="form-control"}
|
||||
<div class="invalid-feedback">
|
||||
${tags-info}
|
||||
${clusters-info}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
|
||||
@@ -41,4 +41,16 @@
|
||||
</div>
|
||||
</message>
|
||||
|
||||
<message id="Lms.Explore.template.info.artists">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-4 col-sm-3">
|
||||
${type}
|
||||
</div>
|
||||
<div class="col">
|
||||
${artist-container data-bs-dismiss="modal"}
|
||||
</div>
|
||||
</div>
|
||||
</message>
|
||||
|
||||
|
||||
</messages>
|
||||
|
||||
+39
-8
@@ -7,6 +7,7 @@
|
||||
<message id="Lms.administration">Administration</message>
|
||||
<message id="Lms.apply">Apply</message>
|
||||
<message id="Lms.cancel">Cancel</message>
|
||||
<message id="Lms.clusters">Tags</message>
|
||||
<message id="Lms.create">Create</message>
|
||||
<message id="Lms.delete">Delete</message>
|
||||
<message id="Lms.discard">Discard</message>
|
||||
@@ -15,6 +16,7 @@
|
||||
<message id="Lms.login">Login</message>
|
||||
<message id="Lms.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> Logout</message>
|
||||
<message id="Lms.not-a-directory">Not a directory</message>
|
||||
<message id="Lms.ok">Ok</message>
|
||||
<message id="Lms.password">Password</message>
|
||||
<message id="Lms.password-bad-login-combination">Bad login / password combination</message>
|
||||
<message id="Lms.password-client-throttled">Login throttled, please try again later</message>
|
||||
@@ -58,7 +60,6 @@
|
||||
<message id="Lms.Admin.Database.scan-launched">Scan launched!</message>
|
||||
<message id="Lms.Admin.Database.scan-options">Scan options</message>
|
||||
<message id="Lms.Admin.Database.settings-saved">New settings saved!</message>
|
||||
<message id="Lms.Admin.Database.tags">Tags</message>
|
||||
<message id="Lms.Admin.Database.update-period">Update period</message>
|
||||
<message id="Lms.Admin.Database.update-start-time">Update start time</message>
|
||||
<message id="Lms.Admin.Database.weekly">Weekly</message>
|
||||
@@ -119,7 +120,10 @@
|
||||
<message id="Lms.Explore.add-filter">Add filter</message>
|
||||
<message id="Lms.Explore.all">All</message>
|
||||
<message id="Lms.Explore.artists">Artists</message>
|
||||
<message id="Lms.Explore.bitrate">Bitrate</message>
|
||||
<message id="Lms.Explore.codec">Codec</message>
|
||||
<message id="Lms.Explore.download">Download</message>
|
||||
<message id="Lms.Explore.duration">Duration</message>
|
||||
<message id="Lms.Explore.filter-added">Filter added</message>
|
||||
<message id="Lms.Explore.filters">Filters</message>
|
||||
<message id="Lms.Explore.most-played">Most played</message>
|
||||
@@ -132,10 +136,12 @@
|
||||
<message id="Lms.Explore.recently-added">Recently added</message>
|
||||
<message id="Lms.Explore.recently-modified">Recently modified</message>
|
||||
<message id="Lms.Explore.recently-played">Recently played</message>
|
||||
<message id="Lms.Explore.release-info">Release information</message>
|
||||
<message id="Lms.Explore.releases">Albums</message>
|
||||
<message id="Lms.Explore.search">Search</message>
|
||||
<message id="Lms.Explore.star">Star</message>
|
||||
<message id="Lms.Explore.starred">Starred</message>
|
||||
<message id="Lms.Explore.track-info">Track information</message>
|
||||
<message id="Lms.Explore.tracklists">Playlists</message>
|
||||
<message id="Lms.Explore.tracks">Tracks</message>
|
||||
<message id="Lms.Explore.type">Type</message>
|
||||
@@ -149,13 +155,38 @@
|
||||
|
||||
<!--Explore:Artists-->
|
||||
<message id="Lms.Explore.Artists.linktype-all">All artists</message>
|
||||
<message id="Lms.Explore.Artists.linktype-artist">Track artists</message>
|
||||
<message id="Lms.Explore.Artists.linktype-composer">Composers</message>
|
||||
<message id="Lms.Explore.Artists.linktype-lyricist">Lyricists</message>
|
||||
<message id="Lms.Explore.Artists.linktype-mixer">Mixers</message>
|
||||
<message id="Lms.Explore.Artists.linktype-producer">Producers</message>
|
||||
<message id="Lms.Explore.Artists.linktype-releaseartist">Album artists</message>
|
||||
<message id="Lms.Explore.Artists.linktype-remixer">Remixers</message>
|
||||
<message id="Lms.Explore.Artists.linktype-artist">
|
||||
<plural case="0">Track artist</plural>
|
||||
<plural case="1">Track artists</plural>
|
||||
</message>
|
||||
<message id="Lms.Explore.Artists.linktype-composer">
|
||||
<plural case="0">Composer</plural>
|
||||
<plural case="1">Composers</plural>
|
||||
</message>
|
||||
<message id="Lms.Explore.Artists.linktype-conductor">
|
||||
<plural case="0">Conductor</plural>
|
||||
<plural case="1">Conductors</plural>
|
||||
</message>
|
||||
<message id="Lms.Explore.Artists.linktype-lyricist">
|
||||
<plural case="0">Lyricist</plural>
|
||||
<plural case="1">Lyricists</plural>
|
||||
</message>
|
||||
<message id="Lms.Explore.Artists.linktype-mixer">
|
||||
<plural case="0">Mixer</plural>
|
||||
<plural case="1">Mixers</plural>
|
||||
</message>
|
||||
<message id="Lms.Explore.Artists.linktype-producer">
|
||||
<plural case="0">Producer</plural>
|
||||
<plural case="1">Producers</plural>
|
||||
</message>
|
||||
<message id="Lms.Explore.Artists.linktype-releaseartist">
|
||||
<plural case="0">Album artists</plural>
|
||||
<plural case="1">Album artist</plural>
|
||||
</message>
|
||||
<message id="Lms.Explore.Artists.linktype-remixer">
|
||||
<plural case="0">Remixers</plural>
|
||||
<plural case="1">Remixer</plural>
|
||||
</message>
|
||||
|
||||
<!--Explore:Release-->
|
||||
<message id="Lms.Explore.Release.similar-releases">Similar albums</message>
|
||||
|
||||
+39
-8
@@ -7,6 +7,7 @@
|
||||
<message id="Lms.administration">Administration</message>
|
||||
<message id="Lms.apply">Appliquer</message>
|
||||
<message id="Lms.cancel">Annuler</message>
|
||||
<message id="Lms.clusters">Tags</message>
|
||||
<message id="Lms.create">Créer</message>
|
||||
<message id="Lms.delete">Supprimer</message>
|
||||
<message id="Lms.discard">Annuler</message>
|
||||
@@ -15,6 +16,7 @@
|
||||
<message id="Lms.login">Login</message>
|
||||
<message id="Lms.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> Quitter</message>
|
||||
<message id="Lms.not-a-directory">N'est pas un répertoire</message>
|
||||
<message id="Lms.ok">Ok</message>
|
||||
<message id="Lms.password">Mot de passe</message>
|
||||
<message id="Lms.password-bad-login-combination">Mauvaise combinaison login / mot de passe</message>
|
||||
<message id="Lms.password-client-throttled">Trop de tentatives de connexion, veuillez réessayer plus tard</message>
|
||||
@@ -58,7 +60,6 @@
|
||||
<message id="Lms.Admin.Database.scan-launched">Scan lancé !</message>
|
||||
<message id="Lms.Admin.Database.scan-options">Options </message>
|
||||
<message id="Lms.Admin.Database.settings-saved">Nouveaux paramètres sauvegardés !</message>
|
||||
<message id="Lms.Admin.Database.tags">Tags</message>
|
||||
<message id="Lms.Admin.Database.update-period">Périodicité des mises à jour</message>
|
||||
<message id="Lms.Admin.Database.update-start-time">Heure de départ de la mise à jour</message>
|
||||
<message id="Lms.Admin.Database.weekly">Toutes les semaines</message>
|
||||
@@ -119,7 +120,10 @@
|
||||
<message id="Lms.Explore.add-filter">Ajouter filtre</message>
|
||||
<message id="Lms.Explore.all">Tous</message>
|
||||
<message id="Lms.Explore.artists">Artistes</message>
|
||||
<message id="Lms.Explore.bitrate">Bitrate</message>
|
||||
<message id="Lms.Explore.codec">Codec</message>
|
||||
<message id="Lms.Explore.download">Télécharger</message>
|
||||
<message id="Lms.Explore.duration">Durée</message>
|
||||
<message id="Lms.Explore.filter-added">Filtre ajouté</message>
|
||||
<message id="Lms.Explore.filters">Filtres</message>
|
||||
<message id="Lms.Explore.most-played">Plus joués</message>
|
||||
@@ -132,10 +136,12 @@
|
||||
<message id="Lms.Explore.recently-added">Ajoutés récemment</message>
|
||||
<message id="Lms.Explore.recently-modified">Modifiés récemment</message>
|
||||
<message id="Lms.Explore.recently-played">Joués récemment</message>
|
||||
<message id="Lms.Explore.release-info">Informations sur l'album</message>
|
||||
<message id="Lms.Explore.releases">Albums</message>
|
||||
<message id="Lms.Explore.search">Rechercher</message>
|
||||
<message id="Lms.Explore.star">Ajouter aux favoris</message>
|
||||
<message id="Lms.Explore.starred">Favoris</message>
|
||||
<message id="Lms.Explore.track-info">Informations sur la piste</message>
|
||||
<message id="Lms.Explore.tracklists">Playlists</message>
|
||||
<message id="Lms.Explore.tracks">Pistes</message>
|
||||
<message id="Lms.Explore.type">Type</message>
|
||||
@@ -149,13 +155,38 @@
|
||||
|
||||
<!--Explore:Artists-->
|
||||
<message id="Lms.Explore.Artists.linktype-all">Tous les artistes</message>
|
||||
<message id="Lms.Explore.Artists.linktype-artist">Artistes de piste</message>
|
||||
<message id="Lms.Explore.Artists.linktype-composer">Compositeurs</message>
|
||||
<message id="Lms.Explore.Artists.linktype-lyricist">Paroliers</message>
|
||||
<message id="Lms.Explore.Artists.linktype-mixer">Mixers</message>
|
||||
<message id="Lms.Explore.Artists.linktype-producer">Producteurs</message>
|
||||
<message id="Lms.Explore.Artists.linktype-releaseartist">Artistes d'album</message>
|
||||
<message id="Lms.Explore.Artists.linktype-remixer">Remixers</message>
|
||||
<message id="Lms.Explore.Artists.linktype-artist">
|
||||
<plural case="0">Artiste de piste</plural>
|
||||
<plural case="1">Artistes de piste</plural>
|
||||
</message>
|
||||
<message id="Lms.Explore.Artists.linktype-composer">
|
||||
<plural case="0">Compositeur</plural>
|
||||
<plural case="1">Compositeurs</plural>
|
||||
</message>
|
||||
<message id="Lms.Explore.Artists.linktype-conductor">
|
||||
<plural case="0">Chef d'orchestre</plural>
|
||||
<plural case="1">Chefs d'orchestre</plural>
|
||||
</message>
|
||||
<message id="Lms.Explore.Artists.linktype-lyricist">
|
||||
<plural case="0">Parolier</plural>
|
||||
<plural case="1">Paroliers</plural>
|
||||
</message>
|
||||
<message id="Lms.Explore.Artists.linktype-mixer">
|
||||
<plural case="0">Mixeur</plural>
|
||||
<plural case="1">Mixeurs</plural>
|
||||
</message>
|
||||
<message id="Lms.Explore.Artists.linktype-producer">
|
||||
<plural case="0">Producteur</plural>
|
||||
<plural case="1">Producteurs</plural>
|
||||
</message>
|
||||
<message id="Lms.Explore.Artists.linktype-releaseartist">
|
||||
<plural case="0">Artiste d'album</plural>
|
||||
<plural case="1">Artistes d'album</plural>
|
||||
</message>
|
||||
<message id="Lms.Explore.Artists.linktype-remixer">
|
||||
<plural case="0">Remixeur</plural>
|
||||
<plural case="1">Remixeurs</plural>
|
||||
</message>
|
||||
|
||||
<!--Explore:Release-->
|
||||
<message id="Lms.Explore.Release.similar-releases">Albums similaires</message>
|
||||
|
||||
+40
-8
@@ -7,6 +7,7 @@
|
||||
<message id="Lms.administration">Amministrazione</message>
|
||||
<message id="Lms.apply">Applica</message>
|
||||
<message id="Lms.cancel">Annulla</message>
|
||||
<message id="Lms.clusters">Tags</message>
|
||||
<message id="Lms.create">Crea</message>
|
||||
<message id="Lms.del">Elimina</message>
|
||||
<message id="Lms.discard">Annulla</message>
|
||||
@@ -15,6 +16,7 @@
|
||||
<message id="Lms.login">Login</message>
|
||||
<message id="Lms.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> Logout</message>
|
||||
<message id="Lms.not-a-directory">Non è una directory</message>
|
||||
|
||||
<message id="Lms.password">Password</message>
|
||||
<message id="Lms.password-bad-login-combination">Errata combinazione di Login / Password </message>
|
||||
<message id="Lms.password-client-throttled">Superati i tentativi di accesso, riprova più tardi</message>
|
||||
@@ -58,7 +60,6 @@
|
||||
<message id="Lms.Admin.Database.scan-launched">Scansione avviata!</message>
|
||||
<message id="Lms.Admin.Database.scan-options">Impostazioni scansione</message>
|
||||
<message id="Lms.Admin.Database.settings-saved">Impostazioni salvate!</message>
|
||||
<message id="Lms.Admin.Database.tags">Tags</message>
|
||||
<message id="Lms.Admin.Database.update-period">Frequenza di aggiornamento</message>
|
||||
<message id="Lms.Admin.Database.update-start-time">Orario di aggiornamento</message>
|
||||
<message id="Lms.Admin.Database.weekly">Settimanale</message>
|
||||
@@ -119,7 +120,10 @@
|
||||
<message id="Lms.Explore.add-filter">Aggiungi filtro</message>
|
||||
<message id="Lms.Explore.all">Tutti</message>
|
||||
<message id="Lms.Explore.artists">Artisti</message>
|
||||
|
||||
|
||||
<message id="Lms.Explore.download">Download</message>
|
||||
|
||||
<message id="Lms.Explore.filter-added">Filtro aggiunto</message>
|
||||
<message id="Lms.Explore.filters">Filtri</message>
|
||||
<message id="Lms.Explore.most-played">Più ascoltati</message>
|
||||
@@ -132,10 +136,13 @@
|
||||
<message id="Lms.Explore.recently-added">Aggiunti di recente</message>
|
||||
|
||||
<message id="Lms.Explore.recently-played">Riprodotti di recente</message>
|
||||
|
||||
<message id="Lms.Explore.releases">Album</message>
|
||||
<message id="Lms.Explore.search">Ricerca</message>
|
||||
<message id="Lms.Explore.star">Aggiungi ai preferiti</message>
|
||||
<message id="Lms.Explore.starred">Preferiti</message>
|
||||
|
||||
|
||||
<message id="Lms.Explore.tracks">Tracce</message>
|
||||
<message id="Lms.Explore.type">Tipo</message>
|
||||
<message id="Lms.Explore.unstar">Rimuovi dai preferiti</message>
|
||||
@@ -148,13 +155,38 @@
|
||||
|
||||
<!--Explore:Artists-->
|
||||
<message id="Lms.Explore.Artists.linktype-all">Tutti gli artisti</message>
|
||||
<message id="Lms.Explore.Artists.linktype-artist">Artisti tracce</message>
|
||||
<message id="Lms.Explore.Artists.linktype-composer">Compositori</message>
|
||||
<message id="Lms.Explore.Artists.linktype-lyricist">Scrittori</message>
|
||||
<message id="Lms.Explore.Artists.linktype-mixer">Mixers</message>
|
||||
<message id="Lms.Explore.Artists.linktype-producer">Produttori</message>
|
||||
<message id="Lms.Explore.Artists.linktype-releaseartist">Artisti album</message>
|
||||
<message id="Lms.Explore.Artists.linktype-remixer">Remixers</message>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!--Explore:Release-->
|
||||
<message id="Lms.Explore.Release.similar-releases">Album simili</message>
|
||||
|
||||
+47
-10
@@ -7,14 +7,16 @@
|
||||
<message id="Lms.administration">管理</message>
|
||||
<message id="Lms.apply">应用</message>
|
||||
<message id="Lms.cancel">取消</message>
|
||||
<message id="Lms.clusters">标签</message>
|
||||
<message id="Lms.create">新建</message>
|
||||
<message id="Lms.del">删除</message>
|
||||
<message id="Lms.delete">删除</message>
|
||||
<message id="Lms.discard">撤销</message>
|
||||
<message id="Lms.edit">编辑</message>
|
||||
<message id="Lms.loading">加载中...</message>
|
||||
<message id="Lms.login">登陆</message>
|
||||
<message id="Lms.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> 注销</message>
|
||||
<message id="Lms.not-a-directory">不是一个目录</message>
|
||||
|
||||
<message id="Lms.password">密码</message>
|
||||
<message id="Lms.password-bad-login-combination">用户名或密码无效</message>
|
||||
<message id="Lms.password-client-throttled">登录已被限制,请稍后再试</message>
|
||||
@@ -26,7 +28,10 @@
|
||||
<message id="Lms.passwords-dont-match">密码不匹配</message>
|
||||
<message id="Lms.quit-other-session">另一个会话已打开,重新打开这一个?</message>
|
||||
<message id="Lms.save">保存</message>
|
||||
<message id="Lms.track-count">{1} 个音轨</message>
|
||||
|
||||
|
||||
|
||||
|
||||
<message id="Lms.user">用户</message>
|
||||
|
||||
<!--Errors-->
|
||||
@@ -55,7 +60,6 @@
|
||||
<message id="Lms.Admin.Database.scan-launched">扫描已完成!</message>
|
||||
<message id="Lms.Admin.Database.scan-options">扫描选项</message>
|
||||
<message id="Lms.Admin.Database.settings-saved">新设置已保存!</message>
|
||||
<message id="Lms.Admin.Database.tags">标签</message>
|
||||
<message id="Lms.Admin.Database.update-period">更新周期</message>
|
||||
<message id="Lms.Admin.Database.update-start-time">更新开始时间</message>
|
||||
<message id="Lms.Admin.Database.weekly">每周</message>
|
||||
@@ -116,7 +120,10 @@
|
||||
<message id="Lms.Explore.add-filter">添加过滤器</message>
|
||||
<message id="Lms.Explore.all">全部</message>
|
||||
<message id="Lms.Explore.artists">歌手</message>
|
||||
|
||||
|
||||
<message id="Lms.Explore.download">下载</message>
|
||||
|
||||
<message id="Lms.Explore.filter-added">过滤器已添加</message>
|
||||
<message id="Lms.Explore.filters">过滤器</message>
|
||||
<message id="Lms.Explore.most-played">播放最多</message>
|
||||
@@ -129,10 +136,12 @@
|
||||
<message id="Lms.Explore.recently-added">最近添加</message>
|
||||
<message id="Lms.Explore.recently-modified">最近更改</message>
|
||||
<message id="Lms.Explore.recently-played">最近播放</message>
|
||||
|
||||
<message id="Lms.Explore.releases">专辑</message>
|
||||
<message id="Lms.Explore.search">搜索</message>
|
||||
<message id="Lms.Explore.star">收藏</message>
|
||||
<message id="Lms.Explore.starred">已收藏</message>
|
||||
|
||||
<message id="Lms.Explore.tracklists">播放列表</message>
|
||||
<message id="Lms.Explore.tracks">音轨</message>
|
||||
<message id="Lms.Explore.type">类型</message>
|
||||
@@ -147,12 +156,37 @@
|
||||
<!--Explore:Artists-->
|
||||
<message id="Lms.Explore.Artists.linktype-all">所有歌手</message>
|
||||
<message id="Lms.Explore.Artists.linktype-artist">音轨歌手</message>
|
||||
<message id="Lms.Explore.Artists.linktype-composer">作曲</message>
|
||||
<message id="Lms.Explore.Artists.linktype-lyricist">作词</message>
|
||||
<message id="Lms.Explore.Artists.linktype-mixer">混响</message>
|
||||
<message id="Lms.Explore.Artists.linktype-producer">制作人</message>
|
||||
<message id="Lms.Explore.Artists.linktype-releaseartist">专辑歌手</message>
|
||||
<message id="Lms.Explore.Artists.linktype-remixer">混音师</message>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<!--Explore:Release-->
|
||||
<message id="Lms.Explore.Release.similar-releases">相似专辑</message>
|
||||
@@ -171,7 +205,10 @@
|
||||
<!--Playqueue-->
|
||||
<message id="Lms.PlayQueue.clear">清除</message>
|
||||
<message id="Lms.PlayQueue.create-tracklist">创建新播放列表</message>
|
||||
<message id="Lms.PlayQueue.nb-tracks-added">已添加 {1} 音轨</message>
|
||||
|
||||
|
||||
|
||||
|
||||
<message id="Lms.PlayQueue.playqueue">播放队列</message>
|
||||
<message id="Lms.PlayQueue.playqueue-full">播放队列已满!</message>
|
||||
<message id="Lms.PlayQueue.radio-mode">广播模式</message>
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
<li><a href="${mbid-link}" target="_blank" class="dropdown-item">${tr:Lms.Explore.musicbrainz-release}</a></li>
|
||||
${</if-has-mbid>}
|
||||
<li>${download class="dropdown-item"}</li>
|
||||
<li>${release-info class="dropdown-item"}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -77,10 +78,52 @@
|
||||
<li>${play-last class="dropdown-item"}</li>
|
||||
<li>${star class="dropdown-item"}</li>
|
||||
<li>${download class="dropdown-item"}</li>
|
||||
<li>${track-info class="dropdown-item"}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</message>
|
||||
|
||||
<message id="Lms.Explore.Release.template.release-info">
|
||||
<div class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">${tr:Lms.Explore.release-info}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="container-fluid d-grid gap-2">
|
||||
${artist-table class="d-grid gap-2"}
|
||||
${<if-has-codec>}
|
||||
<div class="row align-items-center">
|
||||
<div class="col-4 col-sm-3">
|
||||
${tr:Lms.Explore.codec}
|
||||
</div>
|
||||
<div class="col">
|
||||
${codec}
|
||||
</div>
|
||||
</div>
|
||||
${</if-has-codec>}
|
||||
${<if-has-bitrate>}
|
||||
<div class="row align-items-center">
|
||||
<div class="col-4 col-sm-3">
|
||||
${tr:Lms.Explore.bitrate}
|
||||
</div>
|
||||
<div class="col">
|
||||
${bitrate}
|
||||
</div>
|
||||
</div>
|
||||
${</if-has-bitrate>}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
${ok-btn class="btn btn-primary"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</message>
|
||||
|
||||
</messages>
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<div class="col-lg-4">
|
||||
<label class="form-label" for="${id:transcode-bitrate}">
|
||||
${tr:Lms.Settings.transcode-bitrate}
|
||||
</label>
|
||||
; </label>
|
||||
<div class="input-group mb-3">
|
||||
${transcode-bitrate class="form-control"}
|
||||
<span class="input-group-text">kbps</span>
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
<li>${play-last class="dropdown-item"}</li>
|
||||
<li>${star class="dropdown-item"}</li>
|
||||
<li>${download class="dropdown-item"}</li>
|
||||
<li>${track-info class="dropdown-item"}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -67,4 +68,63 @@
|
||||
${artist class="text-decoration-none link-secondary"}
|
||||
</message>
|
||||
|
||||
<message id="Lms.Explore.Tracks.template.track-info">
|
||||
<div class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">${tr:Lms.Explore.track-info}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="container-fluid d-grid gap-2">
|
||||
${<if-has-clusters>}
|
||||
<div class="row align-items-center">
|
||||
<div class="col-4 col-sm-3">
|
||||
${tr:Lms.clusters}
|
||||
</div>
|
||||
<div class="col">
|
||||
${clusters}
|
||||
</div>
|
||||
</div>
|
||||
${</if-has-clusters>}
|
||||
${artist-table class="d-grid gap-2"}
|
||||
<div class="row align-items-center">
|
||||
<div class="col-4 col-sm-3">
|
||||
${tr:Lms.Explore.duration}
|
||||
</div>
|
||||
<div class="col">
|
||||
${duration}
|
||||
</div>
|
||||
</div>
|
||||
${<if-has-codec>}
|
||||
<div class="row align-items-center">
|
||||
<div class="col-4 col-sm-3">
|
||||
${tr:Lms.Explore.codec}
|
||||
</div>
|
||||
<div class="col">
|
||||
${codec}
|
||||
</div>
|
||||
</div>
|
||||
${</if-has-codec>}
|
||||
${<if-has-bitrate>}
|
||||
<div class="row align-items-center">
|
||||
<div class="col-4 col-sm-3">
|
||||
${tr:Lms.Explore.bitrate}
|
||||
</div>
|
||||
<div class="col">
|
||||
${bitrate}
|
||||
</div>
|
||||
</div>
|
||||
${</if-has-bitrate>}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
${ok-btn class="btn btn-primary"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</message>
|
||||
|
||||
</messages>
|
||||
|
||||
@@ -38,7 +38,7 @@ static std::string averror_to_string(int error)
|
||||
{
|
||||
std::array<char, 128> buf = {0};
|
||||
|
||||
if (av_strerror(error, buf.data(), buf.size()) == 0)
|
||||
if (::av_strerror(error, buf.data(), buf.size()) == 0)
|
||||
return &buf[0];
|
||||
else
|
||||
return "Unknown error";
|
||||
@@ -104,7 +104,7 @@ getMetaDataFromDictionnary(AVDictionary* dictionnary, AudioFile::MetadataMap& re
|
||||
return;
|
||||
|
||||
AVDictionaryEntry *tag = NULL;
|
||||
while ((tag = av_dict_get(dictionnary, "", tag, AV_DICT_IGNORE_SUFFIX)))
|
||||
while ((tag = ::av_dict_get(dictionnary, "", tag, AV_DICT_IGNORE_SUFFIX)))
|
||||
{
|
||||
res[StringUtils::stringToUpper(tag->key)] = tag->value;
|
||||
}
|
||||
@@ -140,31 +140,18 @@ AudioFile::getStreamInfo() const
|
||||
|
||||
for (std::size_t i {}; i < _context->nb_streams; ++i)
|
||||
{
|
||||
AVStream* avstream { _context->streams[i]};
|
||||
|
||||
// Skip attached pics
|
||||
if (avstream->disposition & AV_DISPOSITION_ATTACHED_PIC)
|
||||
continue;
|
||||
|
||||
if (!avstream->codecpar)
|
||||
{
|
||||
LMS_LOG(AV, ERROR) << "Skipping stream " << i << " since no codecpar is set";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (avstream->codecpar->codec_type != AVMEDIA_TYPE_AUDIO)
|
||||
continue;
|
||||
|
||||
res.push_back( {i, static_cast<std::size_t>(avstream->codecpar->bit_rate)} );
|
||||
std::optional<StreamInfo> streamInfo {getStreamInfo(i)};
|
||||
if (streamInfo)
|
||||
res.emplace_back(std::move(*streamInfo));
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::optional<std::size_t>
|
||||
AudioFile::getBestStream() const
|
||||
AudioFile::getBestStreamIndex() const
|
||||
{
|
||||
int res = av_find_best_stream(_context,
|
||||
int res = ::av_find_best_stream(_context,
|
||||
AVMEDIA_TYPE_AUDIO,
|
||||
-1, // Auto
|
||||
-1, // Auto
|
||||
@@ -177,6 +164,18 @@ AudioFile::getBestStream() const
|
||||
return res;
|
||||
}
|
||||
|
||||
std::optional<StreamInfo>
|
||||
AudioFile::getBestStreamInfo() const
|
||||
{
|
||||
std::optional<StreamInfo> res;
|
||||
|
||||
std::optional<std::size_t> bestStreamIndex {getBestStreamIndex()};
|
||||
if (bestStreamIndex)
|
||||
res = getStreamInfo(*bestStreamIndex);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
bool
|
||||
AudioFile::hasAttachedPictures() const
|
||||
{
|
||||
@@ -238,10 +237,39 @@ AudioFile::visitAttachedPictures(std::function<void(const Picture&)> func) const
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<AudioFileFormat>
|
||||
guessMediaFileFormat(const std::filesystem::path& file)
|
||||
std::optional<StreamInfo>
|
||||
AudioFile::getStreamInfo(std::size_t streamIndex) const
|
||||
{
|
||||
const AVOutputFormat* format {av_guess_format(NULL,file.string().c_str(),NULL)};
|
||||
std::optional<StreamInfo> res;
|
||||
|
||||
AVStream* avstream { _context->streams[streamIndex]};
|
||||
assert(avstream);
|
||||
|
||||
if (avstream->disposition & AV_DISPOSITION_ATTACHED_PIC)
|
||||
return res;
|
||||
|
||||
if (!avstream->codecpar)
|
||||
{
|
||||
LMS_LOG(AV, ERROR) << "Skipping stream " << streamIndex << " since no codecpar is set";
|
||||
return res;
|
||||
}
|
||||
|
||||
if (avstream->codecpar->codec_type != AVMEDIA_TYPE_AUDIO)
|
||||
return res;
|
||||
|
||||
res.emplace();
|
||||
res->index = streamIndex;
|
||||
res->bitrate = static_cast<std::size_t>(avstream->codecpar->bit_rate);
|
||||
res->codec = ::avcodec_get_name(avstream->codecpar->codec_id);
|
||||
assert(!res->codec.empty());
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::optional<AudioFileFormat>
|
||||
guessAudioFileFormat(const std::filesystem::path& file)
|
||||
{
|
||||
const AVOutputFormat* format {::av_guess_format(NULL, file.string().c_str(), NULL)};
|
||||
if (!format || !format->name)
|
||||
return {};
|
||||
|
||||
|
||||
@@ -43,11 +43,14 @@ namespace Av
|
||||
std::chrono::milliseconds getDuration() const override;
|
||||
MetadataMap getMetaData() const override;
|
||||
std::vector<StreamInfo> getStreamInfo() const override;
|
||||
std::optional<std::size_t> getBestStream() const override;
|
||||
std::optional<StreamInfo> getBestStreamInfo() const override;
|
||||
std::optional<std::size_t> getBestStreamIndex() const override;
|
||||
bool hasAttachedPictures() const override;
|
||||
void visitAttachedPictures(std::function<void(const Picture&)> func) const override;
|
||||
|
||||
private:
|
||||
std::optional<StreamInfo> getStreamInfo(std::size_t streamIndex) const;
|
||||
|
||||
const std::filesystem::path _p;
|
||||
AVFormatContext* _context {};
|
||||
};
|
||||
|
||||
@@ -37,13 +37,14 @@ namespace Av
|
||||
{
|
||||
std::string mimeType;
|
||||
const std::byte* data {};
|
||||
std::size_t dataSize;
|
||||
std::size_t dataSize {};
|
||||
};
|
||||
|
||||
struct StreamInfo
|
||||
{
|
||||
size_t id;
|
||||
std::size_t bitrate;
|
||||
size_t index {};
|
||||
std::size_t bitrate {};
|
||||
std::string codec;
|
||||
};
|
||||
|
||||
class IAudioFile
|
||||
@@ -57,7 +58,8 @@ namespace Av
|
||||
virtual std::chrono::milliseconds getDuration() const = 0;
|
||||
virtual MetadataMap getMetaData() const = 0;
|
||||
virtual std::vector<StreamInfo> getStreamInfo() const = 0;
|
||||
virtual std::optional<std::size_t> getBestStream() const = 0; // none if failure/unknown
|
||||
virtual std::optional<StreamInfo> getBestStreamInfo() const = 0; // none if failure/unknown
|
||||
virtual std::optional<std::size_t> getBestStreamIndex() const = 0; // none if failure/unknown
|
||||
virtual bool hasAttachedPictures() const = 0;
|
||||
virtual void visitAttachedPictures(std::function<void(const Picture&)> func) const = 0;
|
||||
};
|
||||
|
||||
@@ -94,7 +94,11 @@ createQuery(Session& session, const Artist::FindParameters& params)
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {session.getDboSession().query<ArtistId>("SELECT DISTINCT a.id FROM artist a")};
|
||||
if (params.sortMethod == ArtistSortMethod::LastWritten || params.writtenAfter.isValid() || params.linkType)
|
||||
if (params.sortMethod == ArtistSortMethod::LastWritten
|
||||
|| params.writtenAfter.isValid()
|
||||
|| params.linkType
|
||||
|| params.track.isValid()
|
||||
|| params.release.isValid())
|
||||
{
|
||||
query.join("track t ON t.id = t_a_l.track_id");
|
||||
query.join("track_artist_link t_a_l ON t_a_l.artist_id = a.id");
|
||||
@@ -157,6 +161,12 @@ createQuery(Session& session, const Artist::FindParameters& params)
|
||||
query.where(oss.str());
|
||||
}
|
||||
|
||||
if (params.track.isValid())
|
||||
query.where("t.id = ?").bind(params.track);
|
||||
|
||||
if (params.release.isValid())
|
||||
query.where("t.release_id = ?").bind(params.release);
|
||||
|
||||
switch (params.sortMethod)
|
||||
{
|
||||
case ArtistSortMethod::None:
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
#include "services/database/ReleaseId.hpp"
|
||||
#include "services/database/Types.hpp"
|
||||
#include "services/database/UserId.hpp"
|
||||
#include "services/database/TrackId.hpp"
|
||||
#include "utils/EnumSet.hpp"
|
||||
#include "utils/UUID.hpp"
|
||||
|
||||
@@ -61,6 +62,8 @@ class Artist : public Object<Artist, ArtistId>
|
||||
Wt::WDateTime writtenAfter;
|
||||
UserId starringUser; // only artists starred by this user
|
||||
std::optional<Scrobbler> scrobbler; // and for this scrobbler
|
||||
TrackId track; // artists involved in this track
|
||||
ReleaseId release; // artists involved in this release
|
||||
|
||||
FindParameters& setClusters(const std::vector<ClusterId>& _clusters) { clusters = _clusters; return *this; }
|
||||
FindParameters& setKeywords(const std::vector<std::string_view>& _keywords) { keywords = _keywords; return *this; }
|
||||
@@ -69,6 +72,8 @@ class Artist : public Object<Artist, ArtistId>
|
||||
FindParameters& setRange(Range _range) {range = _range; return *this; }
|
||||
FindParameters& setWrittenAfter(const Wt::WDateTime& _after) { writtenAfter = _after; return *this; }
|
||||
FindParameters& setStarringUser(UserId _user, Scrobbler _scrobbler) { starringUser = _user; scrobbler = _scrobbler; return *this; }
|
||||
FindParameters& setTrack(TrackId _track) { track = _track; return *this; }
|
||||
FindParameters& setRelease(ReleaseId _release) { release = _release; return *this; }
|
||||
};
|
||||
|
||||
Artist() = default;
|
||||
@@ -112,7 +117,7 @@ class Artist : public Object<Artist, ArtistId>
|
||||
}
|
||||
|
||||
private:
|
||||
static const std::size_t _maxNameLength = 128;
|
||||
static constexpr std::size_t _maxNameLength {128};
|
||||
|
||||
friend class Session;
|
||||
// Create
|
||||
|
||||
@@ -134,7 +134,7 @@ class Release : public Object<Release, ReleaseId>
|
||||
Release(const std::string& name, const std::optional<UUID>& MBID = {});
|
||||
static pointer create(Session& session, const std::string& name, const std::optional<UUID>& MBID = {});
|
||||
|
||||
static const std::size_t _maxNameLength {128};
|
||||
static constexpr std::size_t _maxNameLength {128};
|
||||
|
||||
std::string _name;
|
||||
std::string _MBID;
|
||||
|
||||
@@ -207,9 +207,9 @@ class Track : public Object<Track, TrackId>
|
||||
Track(const std::filesystem::path& p);
|
||||
static pointer create(Session& session, const std::filesystem::path& p);
|
||||
|
||||
static const std::size_t _maxNameLength = 128;
|
||||
static const std::size_t _maxCopyrightLength = 128;
|
||||
static const std::size_t _maxCopyrightURLLength = 128;
|
||||
static constexpr std::size_t _maxNameLength {128};
|
||||
static constexpr std::size_t _maxCopyrightLength {128};
|
||||
static constexpr std::size_t _maxCopyrightURLLength {128};
|
||||
|
||||
int _scanVersion {};
|
||||
int _trackNumber {};
|
||||
|
||||
@@ -92,21 +92,27 @@ TEST_F(DatabaseFixture, Artist_singleTrack)
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
auto tracks {Track::find(session, Track::FindParameters{}.setName("MyTrackName").setArtistName("MyArtist"))};
|
||||
ASSERT_EQ(tracks.results.size(), 1);
|
||||
EXPECT_EQ(tracks.results.front(), track.getId());
|
||||
}
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
auto tracks {Track::find(session, Track::FindParameters{}.setName("MyTrackName").setArtistName("MyArtistFoo"))};
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
}
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
auto tracks {Track::find(session, Track::FindParameters{}.setName("MyTrackNameFoo").setArtistName("MyArtist"))};
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
}
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
auto artists {Artist::find(session, Artist::FindParameters{}.setTrack(track->getId()))};
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
EXPECT_EQ(artists.results.front(), artist.getId());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Artist_singleTracktMultiRoles)
|
||||
@@ -198,7 +204,7 @@ TEST_F(DatabaseFixture, Artist_singleTrackMultiArtists)
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
auto tracks {Track::find(session, Track::FindParameters {}.setArtist(artist1->getId()))};
|
||||
ASSERT_EQ(tracks.results.size(), 1);
|
||||
@@ -366,9 +372,45 @@ TEST_F(DatabaseFixture, Artist_nonReleaseTracks)
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
const auto tracks {Track::find(session, Track::FindParameters {}.setArtist(artist->getId()).setNonRelease(true))};
|
||||
const auto tracks {Track::find(session, Track::FindParameters {}.setArtist(artist.getId()).setNonRelease(true))};
|
||||
ASSERT_EQ(tracks.results.size(), 1);
|
||||
EXPECT_EQ(tracks.results.front(), track2.getId());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Artist_findByRelease)
|
||||
{
|
||||
ScopedArtist artist {session, "artist"};
|
||||
ScopedTrack track {session, "MyTrack"};
|
||||
ScopedRelease release{session, "MyRelease"};
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
const auto artists {Artist::find(session, Artist::FindParameters {}.setRelease(release.getId()))};
|
||||
EXPECT_EQ(artists.results.size(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
const auto artists {Artist::find(session, Artist::FindParameters {}.setRelease(release.getId()))};
|
||||
EXPECT_EQ(artists.results.size(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
track.get().modify()->setRelease(release.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
const auto artists {Artist::find(session, Artist::FindParameters {}.setRelease(release.getId()))};
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
EXPECT_EQ(artists.results.front(), artist.getId());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -278,71 +278,6 @@ LmsApplication::finalize()
|
||||
preQuit().emit();
|
||||
}
|
||||
|
||||
Wt::WLink
|
||||
LmsApplication::createArtistLink(Database::Artist::pointer artist)
|
||||
{
|
||||
if (const auto mbid {artist->getMBID()})
|
||||
return Wt::WLink {Wt::LinkType::InternalPath, "/artist/mbid/" + std::string {mbid->getAsString()}};
|
||||
else
|
||||
return Wt::WLink {Wt::LinkType::InternalPath, "/artist/" + artist->getId().toString()};
|
||||
}
|
||||
|
||||
std::unique_ptr<Wt::WAnchor>
|
||||
LmsApplication::createArtistAnchor(Database::Artist::pointer artist, bool addText)
|
||||
{
|
||||
auto res = std::make_unique<Wt::WAnchor>(createArtistLink(artist));
|
||||
|
||||
if (addText)
|
||||
{
|
||||
res->setTextFormat(Wt::TextFormat::Plain);
|
||||
res->setText(Wt::WString::fromUTF8(artist->getName()));
|
||||
res->setToolTip(Wt::WString::fromUTF8(artist->getName()), Wt::TextFormat::Plain);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
Wt::WLink
|
||||
LmsApplication::createReleaseLink(Database::Release::pointer release)
|
||||
{
|
||||
if (const auto mbid {release->getMBID()})
|
||||
return Wt::WLink {Wt::LinkType::InternalPath, "/release/mbid/" + std::string {mbid->getAsString()}};
|
||||
else
|
||||
return Wt::WLink {Wt::LinkType::InternalPath, "/release/" + release->getId().toString()};
|
||||
}
|
||||
|
||||
std::unique_ptr<Wt::WAnchor>
|
||||
LmsApplication::createReleaseAnchor(Database::Release::pointer release, bool addText)
|
||||
{
|
||||
auto res = std::make_unique<Wt::WAnchor>(createReleaseLink(release));
|
||||
|
||||
if (addText)
|
||||
{
|
||||
res->setTextFormat(Wt::TextFormat::Plain);
|
||||
res->setText(Wt::WString::fromUTF8(release->getName()));
|
||||
res->setToolTip(Wt::WString::fromUTF8(release->getName()), Wt::TextFormat::Plain);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::unique_ptr<Wt::WAnchor>
|
||||
LmsApplication::createTrackListAnchor(Database::TrackList::pointer trackList, bool addText)
|
||||
{
|
||||
Wt::WLink link {Wt::LinkType::InternalPath, "/tracklist/" + trackList->getId().toString()};
|
||||
auto res {std::make_unique<Wt::WAnchor>(link)};
|
||||
|
||||
if (addText)
|
||||
{
|
||||
const Wt::WString name {Wt::WString::fromUTF8(std::string {trackList->getName()})};
|
||||
res->setTextFormat(Wt::TextFormat::Plain);
|
||||
res->setText(name);
|
||||
res->setToolTip(name, Wt::TextFormat::Plain);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
void
|
||||
LmsApplication::handleException(LmsApplicationException& e)
|
||||
{
|
||||
|
||||
@@ -31,12 +31,8 @@
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Artist;
|
||||
class Cluster;
|
||||
class Db;
|
||||
class Release;
|
||||
class Session;
|
||||
class TrackList;
|
||||
class User;
|
||||
}
|
||||
|
||||
@@ -80,12 +76,6 @@ class LmsApplication : public Wt::WApplication
|
||||
// Used to classify the message sent to the user
|
||||
void notifyMsg(Notification::Type type, const Wt::WString& category, const Wt::WString& message, std::chrono::milliseconds duration = std::chrono::milliseconds {5000});
|
||||
|
||||
static Wt::WLink createArtistLink(Database::ObjectPtr<Database::Artist> artist);
|
||||
static std::unique_ptr<Wt::WAnchor> createArtistAnchor(Database::ObjectPtr<Database::Artist> artist, bool addText = true);
|
||||
static Wt::WLink createReleaseLink(Database::ObjectPtr<Database::Release> release);
|
||||
static std::unique_ptr<Wt::WAnchor> createReleaseAnchor(Database::ObjectPtr<Database::Release> release, bool addText = true);
|
||||
static std::unique_ptr<Wt::WAnchor> createTrackListAnchor(Database::ObjectPtr<Database::TrackList> trackList, bool addText = true);
|
||||
|
||||
MediaPlayer& getMediaPlayer() const { return *_mediaPlayer; }
|
||||
PlayQueue& getPlayQueue() const { return *_playQueue; }
|
||||
ModalManager& getModalManager() const { return *_modalManager; }
|
||||
|
||||
@@ -16,9 +16,6 @@ namespace UserInterface
|
||||
LmsTheme::init(Wt::WApplication* app) const
|
||||
{
|
||||
app->require("js/bootstrap.bundle.min.js");
|
||||
Wt::WString v = app->metaHeader(Wt::MetaHeaderType::Meta, "viewport");
|
||||
if (v.empty())
|
||||
app->addMetaHeader("viewport", "width=device-width, initial-scale=1, user-scalable=no");
|
||||
}
|
||||
|
||||
std::string
|
||||
|
||||
@@ -42,6 +42,7 @@
|
||||
#include "utils/Utils.hpp"
|
||||
|
||||
#include "LmsApplication.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
@@ -155,7 +156,7 @@ replayGainPreAmpGainFromString(const std::string& str)
|
||||
if (!value)
|
||||
return std::nullopt;
|
||||
|
||||
return Utils::clamp(*value, (double)MediaPlayer::Settings::ReplayGain::minPreAmpGain, (double)MediaPlayer::Settings::ReplayGain::maxPreAmpGain);
|
||||
return ::Utils::clamp(*value, (double)MediaPlayer::Settings::ReplayGain::minPreAmpGain, (double)MediaPlayer::Settings::ReplayGain::maxPreAmpGain);
|
||||
}
|
||||
|
||||
static MediaPlayer::Settings settingsfromJSString(const std::string& strSettings)
|
||||
@@ -281,7 +282,7 @@ MediaPlayer::loadTrack(Database::TrackId trackId, bool play, float replayGain)
|
||||
{
|
||||
_artist->setTextFormat(Wt::TextFormat::Plain);
|
||||
_artist->setText(Wt::WString::fromUTF8(artists.front()->getName()));
|
||||
_artist->setLink(LmsApp->createArtistLink(artists.front()));
|
||||
_artist->setLink(Utils::createArtistLink(artists.front()));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -293,7 +294,7 @@ MediaPlayer::loadTrack(Database::TrackId trackId, bool play, float replayGain)
|
||||
{
|
||||
_release->setTextFormat(Wt::TextFormat::Plain);
|
||||
_release->setText(Wt::WString::fromUTF8(track->getRelease()->getName()));
|
||||
_release->setLink(LmsApp->createReleaseLink(track->getRelease()));
|
||||
_release->setLink(Utils::createReleaseLink(track->getRelease()));
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -500,9 +500,9 @@ PlayQueue::addEntry(const Database::TrackListEntry::pointer& tracklistEntry)
|
||||
if (release)
|
||||
{
|
||||
entry->setCondition("if-has-release", true);
|
||||
entry->bindWidget("release", LmsApplication::createReleaseAnchor(release));
|
||||
entry->bindWidget("release", Utils::createReleaseAnchor(release));
|
||||
{
|
||||
Wt::WAnchor* anchor {entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false))};
|
||||
Wt::WAnchor* anchor {entry->bindWidget("cover", Utils::createReleaseAnchor(release, false))};
|
||||
auto cover {Utils::createCover(release->getId(), CoverResource::Size::Small)};
|
||||
cover->addStyleClass("Lms-cover-track Lms-cover-anchor"); // HACK
|
||||
anchor->setImage(std::move(cover));
|
||||
|
||||
@@ -22,10 +22,17 @@
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
|
||||
#include <Wt/WAnchor.h>
|
||||
#include <Wt/WText.h>
|
||||
|
||||
#include "services/database/Artist.hpp"
|
||||
#include "services/database/Cluster.hpp"
|
||||
#include "services/database/Release.hpp"
|
||||
#include "services/database/ScanSettings.hpp"
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/Track.hpp"
|
||||
#include "services/database/TrackList.hpp"
|
||||
#include "explore/Filters.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
|
||||
namespace UserInterface::Utils
|
||||
@@ -112,4 +119,123 @@ namespace UserInterface::Utils
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::unique_ptr<Wt::WContainerWidget>
|
||||
createArtistContainer(const std::vector<Database::ArtistId>& artistIds)
|
||||
{
|
||||
using namespace Database;
|
||||
|
||||
std::unique_ptr<Wt::WContainerWidget> artistContainer {std::make_unique<Wt::WContainerWidget>()};
|
||||
|
||||
bool firstArtist {true};
|
||||
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
for (const ArtistId artistId : artistIds)
|
||||
{
|
||||
const Artist::pointer artist {Artist::find(LmsApp->getDbSession(), artistId)};
|
||||
if (!artist)
|
||||
continue;
|
||||
|
||||
if (!firstArtist)
|
||||
artistContainer->addNew<Wt::WText>(" · ");
|
||||
|
||||
auto anchor {createArtistAnchor(artist)};
|
||||
anchor->addStyleClass("link-success text-decoration-none"); // hack
|
||||
artistContainer->addWidget(std::move(anchor));
|
||||
firstArtist = false;
|
||||
}
|
||||
|
||||
return artistContainer;
|
||||
}
|
||||
|
||||
Wt::WLink
|
||||
createArtistLink(Database::Artist::pointer artist)
|
||||
{
|
||||
if (const auto mbid {artist->getMBID()})
|
||||
return Wt::WLink {Wt::LinkType::InternalPath, "/artist/mbid/" + std::string {mbid->getAsString()}};
|
||||
else
|
||||
return Wt::WLink {Wt::LinkType::InternalPath, "/artist/" + artist->getId().toString()};
|
||||
}
|
||||
|
||||
std::unique_ptr<Wt::WAnchor>
|
||||
createArtistAnchor(Database::Artist::pointer artist, bool setText)
|
||||
{
|
||||
auto res = std::make_unique<Wt::WAnchor>(createArtistLink(artist));
|
||||
|
||||
if (setText)
|
||||
{
|
||||
res->setTextFormat(Wt::TextFormat::Plain);
|
||||
res->setText(Wt::WString::fromUTF8(artist->getName()));
|
||||
res->setToolTip(Wt::WString::fromUTF8(artist->getName()), Wt::TextFormat::Plain);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
Wt::WLink
|
||||
createReleaseLink(Database::Release::pointer release)
|
||||
{
|
||||
if (const auto mbid {release->getMBID()})
|
||||
return Wt::WLink {Wt::LinkType::InternalPath, "/release/mbid/" + std::string {mbid->getAsString()}};
|
||||
else
|
||||
return Wt::WLink {Wt::LinkType::InternalPath, "/release/" + release->getId().toString()};
|
||||
}
|
||||
|
||||
std::unique_ptr<Wt::WAnchor>
|
||||
createReleaseAnchor(Database::Release::pointer release, bool setText)
|
||||
{
|
||||
auto res = std::make_unique<Wt::WAnchor>(createReleaseLink(release));
|
||||
|
||||
if (setText)
|
||||
{
|
||||
res->setTextFormat(Wt::TextFormat::Plain);
|
||||
res->setText(Wt::WString::fromUTF8(release->getName()));
|
||||
res->setToolTip(Wt::WString::fromUTF8(release->getName()), Wt::TextFormat::Plain);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::unique_ptr<Wt::WAnchor>
|
||||
createTrackListAnchor(Database::TrackList::pointer trackList, bool setText)
|
||||
{
|
||||
Wt::WLink link {Wt::LinkType::InternalPath, "/tracklist/" + trackList->getId().toString()};
|
||||
auto res {std::make_unique<Wt::WAnchor>(link)};
|
||||
|
||||
if (setText)
|
||||
{
|
||||
const Wt::WString name {Wt::WString::fromUTF8(std::string {trackList->getName()})};
|
||||
res->setTextFormat(Wt::TextFormat::Plain);
|
||||
res->setText(name);
|
||||
res->setToolTip(name, Wt::TextFormat::Plain);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::unique_ptr<Wt::WContainerWidget>
|
||||
createClustersForTrack(Database::Track::pointer track, Filters& filters)
|
||||
{
|
||||
using namespace Database;
|
||||
|
||||
std::unique_ptr<Wt::WContainerWidget> clusterContainer {std::make_unique<Wt::WContainerWidget>()};
|
||||
|
||||
const auto clusterTypes {ScanSettings::get(LmsApp->getDbSession())->getClusterTypes()};
|
||||
const auto clusterGroups {track->getClusterGroups(clusterTypes, 3)};
|
||||
|
||||
for (const auto& clusters : clusterGroups)
|
||||
{
|
||||
for (const Cluster::pointer& cluster : clusters)
|
||||
{
|
||||
const ClusterId clusterId {cluster->getId()};
|
||||
Wt::WInteractWidget* entry {clusterContainer->addWidget(Utils::createCluster(clusterId))};
|
||||
entry->clicked().connect([&filters, clusterId]
|
||||
{
|
||||
filters.add(clusterId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return clusterContainer;
|
||||
}
|
||||
}
|
||||
|
||||
+28
-2
@@ -22,22 +22,48 @@
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
|
||||
#include <Wt/WContainerWidget.h>
|
||||
#include <Wt/WImage.h>
|
||||
#include <Wt/WInteractWidget.h>
|
||||
|
||||
#include "services/database/ArtistId.hpp"
|
||||
#include "services/database/ClusterId.hpp"
|
||||
#include "services/database/Object.hpp"
|
||||
#include "services/database/ReleaseId.hpp"
|
||||
#include "services/database/TrackId.hpp"
|
||||
|
||||
#include "resource/CoverResource.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Artist;
|
||||
class Cluster;
|
||||
class Release;
|
||||
class Track;
|
||||
class TrackList;
|
||||
}
|
||||
|
||||
namespace UserInterface
|
||||
{
|
||||
class Filters;
|
||||
}
|
||||
|
||||
namespace UserInterface::Utils
|
||||
{
|
||||
std::string
|
||||
durationToString(std::chrono::milliseconds msDuration);
|
||||
std::string durationToString(std::chrono::milliseconds msDuration);
|
||||
|
||||
std::unique_ptr<Wt::WImage> createCover(Database::ReleaseId releaseId, CoverResource::Size size);
|
||||
std::unique_ptr<Wt::WImage> createCover(Database::TrackId trackId, CoverResource::Size size);
|
||||
|
||||
std::unique_ptr<Wt::WInteractWidget> createCluster(Database::ClusterId clusterId, bool canDelete = false);
|
||||
|
||||
std::unique_ptr<Wt::WContainerWidget> createArtistContainer(const std::vector<Database::ArtistId>& artists);
|
||||
|
||||
Wt::WLink createArtistLink(Database::ObjectPtr<Database::Artist> artist);
|
||||
std::unique_ptr<Wt::WAnchor> createArtistAnchor(Database::ObjectPtr<Database::Artist> artist, bool setText = true);
|
||||
Wt::WLink createReleaseLink(Database::ObjectPtr<Database::Release> release);
|
||||
std::unique_ptr<Wt::WAnchor> createReleaseAnchor(Database::ObjectPtr<Database::Release> release, bool setText = true);
|
||||
std::unique_ptr<Wt::WAnchor> createTrackListAnchor(Database::ObjectPtr<Database::TrackList> trackList, bool setText = true);
|
||||
|
||||
std::unique_ptr<Wt::WContainerWidget> createClustersForTrack(Database::ObjectPtr<Database::Track> track, Filters& filters);
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ class DatabaseSettingsModel : public Wt::WFormModel
|
||||
static inline constexpr Field UpdatePeriodField {"update-period"};
|
||||
static inline constexpr Field UpdateStartTimeField {"update-start-time"};
|
||||
static inline constexpr Field RecommendationEngineTypeField {"recommendation-engine-type"};
|
||||
static inline constexpr Field TagsField {"tags"};
|
||||
static inline constexpr Field ClustersField {"clusters"};
|
||||
|
||||
using UpdatePeriodModel = ValueStringModel<ScanSettings::UpdatePeriod>;
|
||||
|
||||
@@ -65,7 +65,7 @@ class DatabaseSettingsModel : public Wt::WFormModel
|
||||
addField(UpdatePeriodField);
|
||||
addField(UpdateStartTimeField);
|
||||
addField(RecommendationEngineTypeField);
|
||||
addField(TagsField);
|
||||
addField(ClustersField);
|
||||
|
||||
auto dirValidator {createDirectoryValidator()};
|
||||
dirValidator->setMandatory(true);
|
||||
@@ -114,7 +114,7 @@ class DatabaseSettingsModel : public Wt::WFormModel
|
||||
{
|
||||
std::vector<std::string> names;
|
||||
std::transform(clusterTypes.begin(), clusterTypes.end(), std::back_inserter(names), [](auto clusterType) { return clusterType->getName(); });
|
||||
setValue(TagsField, StringUtils::joinStrings(names, " "));
|
||||
setValue(ClustersField, StringUtils::joinStrings(names, " "));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ class DatabaseSettingsModel : public Wt::WFormModel
|
||||
if (recommendationEngineTypeRow)
|
||||
scanSettings.modify()->setRecommendationEngineType(_recommendationEngineTypeModel->getValue(*recommendationEngineTypeRow));
|
||||
|
||||
auto clusterTypes {StringUtils::splitStringCopy(valueText(TagsField).toUTF8(), " ")};
|
||||
auto clusterTypes {StringUtils::splitStringCopy(valueText(ClustersField).toUTF8(), " ")};
|
||||
scanSettings.modify()->setClusterTypes(LmsApp->getDbSession(), std::set<std::string>(clusterTypes.begin(), clusterTypes.end()));
|
||||
}
|
||||
|
||||
@@ -215,8 +215,8 @@ DatabaseSettingsView::refreshView()
|
||||
recommendationEngineType->setModel(model->recommendationEngineTypeModel());
|
||||
t->setFormWidget(DatabaseSettingsModel::RecommendationEngineTypeField, std::move(recommendationEngineType));
|
||||
|
||||
// Tags
|
||||
t->setFormWidget(DatabaseSettingsModel::TagsField, std::make_unique<Wt::WLineEdit>());
|
||||
// Clusters
|
||||
t->setFormWidget(DatabaseSettingsModel::ClustersField, std::make_unique<Wt::WLineEdit>());
|
||||
|
||||
// Buttons
|
||||
Wt::WPushButton *saveBtn = t->bindWidget("apply-btn", std::make_unique<Wt::WPushButton>(Wt::WString::tr("Lms.apply")));
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
#include <Wt/WAnchor.h>
|
||||
|
||||
#include "services/database/Artist.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace UserInterface::ArtistListHelpers
|
||||
{
|
||||
@@ -29,7 +29,7 @@ namespace UserInterface::ArtistListHelpers
|
||||
createEntry(const Database::ObjectPtr<Database::Artist>& artist)
|
||||
{
|
||||
auto res {std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Artists.template.entry"))};
|
||||
res->bindWidget("name", LmsApplication::createArtistAnchor(artist));
|
||||
res->bindWidget("name", Utils::createArtistAnchor(artist));
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -335,7 +335,7 @@ Artist::addSomeNonReleaseTracks()
|
||||
}
|
||||
|
||||
const Track::pointer track {Track::find(LmsApp->getDbSession(), trackId)};
|
||||
_trackContainer->add(TrackListHelpers::createEntry(track, _playQueueController));
|
||||
_trackContainer->add(TrackListHelpers::createEntry(track, _playQueueController, _filters));
|
||||
|
||||
areTracksAdded = true;
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ Artists::refreshArtistLinkTypes()
|
||||
if (!usedLinkTypes.contains(linkType))
|
||||
return;
|
||||
|
||||
linkTypeModel->add(Wt::WString::tr(std::string {stringKey}), linkType);
|
||||
linkTypeModel->add(Wt::WString::trn(std::string {stringKey}, 2), linkType);
|
||||
}};
|
||||
|
||||
linkTypeModel->clear();
|
||||
@@ -148,6 +148,7 @@ Artists::refreshArtistLinkTypes()
|
||||
addTypeIfUsed(TrackArtistLinkType::Artist, "Lms.Explore.Artists.linktype-artist");
|
||||
addTypeIfUsed(TrackArtistLinkType::ReleaseArtist, "Lms.Explore.Artists.linktype-releaseartist");
|
||||
addTypeIfUsed(TrackArtistLinkType::Composer, "Lms.Explore.Artists.linktype-composer");
|
||||
addTypeIfUsed(TrackArtistLinkType::Conductor, "Lms.Explore.Artists.linktype-conductor");
|
||||
addTypeIfUsed(TrackArtistLinkType::Lyricist, "Lms.Explore.Artists.linktype-lyricist");
|
||||
addTypeIfUsed(TrackArtistLinkType::Mixer, "Lms.Explore.Artists.linktype-mixer");
|
||||
addTypeIfUsed(TrackArtistLinkType::Producer, "Lms.Explore.Artists.linktype-producer");
|
||||
|
||||
@@ -41,6 +41,7 @@ Filters::showDialog()
|
||||
auto dialog {std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.template.add-filter"))};
|
||||
Wt::WWidget* dialogPtr {dialog.get()};
|
||||
dialog->addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
dialog->addFunction("id", &Wt::WTemplate::Functions::id);
|
||||
|
||||
Wt::WComboBox* typeCombo {dialog->bindNew<Wt::WComboBox>("type")};
|
||||
Wt::WComboBox* valueCombo {dialog->bindNew<Wt::WComboBox>("value")};
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
#include "services/database/Artist.hpp"
|
||||
#include "services/database/Release.hpp"
|
||||
|
||||
#include "LmsApplication.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
using namespace Database;
|
||||
@@ -39,11 +38,11 @@ namespace UserInterface::ReleaseListHelpers
|
||||
{
|
||||
auto entry {std::make_unique<Wt::WTemplate>(Wt::WString::tr(templateKey))};
|
||||
|
||||
entry->bindWidget("release-name", LmsApplication::createReleaseAnchor(release));
|
||||
entry->bindWidget("release-name", Utils::createReleaseAnchor(release));
|
||||
entry->addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
|
||||
{
|
||||
Wt::WAnchor* anchor {entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false))};
|
||||
Wt::WAnchor* anchor {entry->bindWidget("cover", Utils::createReleaseAnchor(release, false))};
|
||||
auto cover {Utils::createCover(release->getId(), CoverResource::Size::Large)};
|
||||
cover->addStyleClass("Lms-cover-release Lms-cover-anchor");
|
||||
anchor->setImage(std::move(cover));
|
||||
@@ -62,7 +61,7 @@ namespace UserInterface::ReleaseListHelpers
|
||||
else if (artists.size() == 1 && !isSameArtist)
|
||||
{
|
||||
entry->setCondition("if-has-artist", true);
|
||||
entry->bindWidget("artist-name", LmsApplication::createArtistAnchor(artists.front()));
|
||||
entry->bindWidget("artist-name", Utils::createArtistAnchor(artists.front()));
|
||||
}
|
||||
|
||||
if (showYear)
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
#include <Wt/WImage.h>
|
||||
#include <Wt/WPushButton.h>
|
||||
|
||||
#include "av/IAudioFile.hpp"
|
||||
#include "services/database/Artist.hpp"
|
||||
#include "services/database/Cluster.hpp"
|
||||
#include "services/database/Release.hpp"
|
||||
#include "services/database/ScanSettings.hpp"
|
||||
@@ -37,15 +39,88 @@
|
||||
#include "explore/Filters.hpp"
|
||||
#include "explore/PlayQueueController.hpp"
|
||||
#include "explore/ReleaseListHelpers.hpp"
|
||||
#include "explore/TrackListHelpers.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
#include "LmsApplicationException.hpp"
|
||||
#include "MediaPlayer.hpp"
|
||||
#include "ModalManager.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
using namespace Database;
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
void
|
||||
showReleaseInfoModal(Database::ReleaseId releaseId)
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
const Database::Release::pointer release {Database::Release::find(LmsApp->getDbSession(), releaseId)};
|
||||
if (!release)
|
||||
return;
|
||||
|
||||
auto releaseInfo {std::make_unique<Template>(Wt::WString::tr("Lms.Explore.Release.template.release-info"))};
|
||||
Wt::WWidget* releaseInfoPtr {releaseInfo.get()};
|
||||
releaseInfo->addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
|
||||
Wt::WContainerWidget* artistTable {releaseInfo->bindNew<Wt::WContainerWidget>("artist-table")};
|
||||
|
||||
auto addArtists = [&](TrackArtistLinkType linkType, const char* type)
|
||||
{
|
||||
Artist::FindParameters params;
|
||||
params.setRelease(releaseId);
|
||||
params.setLinkType(linkType);
|
||||
const auto artistIds {Artist::find(LmsApp->getDbSession(), params)};
|
||||
if (artistIds.results.empty())
|
||||
return;
|
||||
|
||||
std::unique_ptr<Wt::WContainerWidget> artistContainer {Utils::createArtistContainer(artistIds.results)};
|
||||
auto artistsEntry {std::make_unique<Template>(Wt::WString::tr("Lms.Explore.template.info.artists"))};
|
||||
artistsEntry->bindString("type", Wt::WString::trn(type, artistContainer->count()));
|
||||
artistsEntry->bindWidget("artist-container", std::move(artistContainer));
|
||||
artistTable->addWidget(std::move(artistsEntry));
|
||||
};
|
||||
|
||||
addArtists(TrackArtistLinkType::Composer, "Lms.Explore.Artists.linktype-composer");
|
||||
addArtists(TrackArtistLinkType::Conductor, "Lms.Explore.Artists.linktype-conductor");
|
||||
addArtists(TrackArtistLinkType::Lyricist, "Lms.Explore.Artists.linktype-lyricist");
|
||||
addArtists(TrackArtistLinkType::Mixer, "Lms.Explore.Artists.linktype-mixer");
|
||||
addArtists(TrackArtistLinkType::Remixer, "Lms.Explore.Artists.linktype-remixer");
|
||||
addArtists(TrackArtistLinkType::Producer, "Lms.Explore.Artists.linktype-producer");
|
||||
|
||||
// TODO: save in DB and mean all this
|
||||
for (TrackId trackId : Track::find(LmsApp->getDbSession(), Track::FindParameters {}.setRelease(releaseId).setRange(Range {0, 1})).results)
|
||||
{
|
||||
const Track::pointer track {Track::find(LmsApp->getDbSession(), trackId)};
|
||||
if (!track)
|
||||
continue;
|
||||
|
||||
if (const auto audioFile {Av::parseAudioFile(track->getPath())})
|
||||
{
|
||||
const std::optional<Av::StreamInfo> audioStream {audioFile->getBestStreamInfo()};
|
||||
if (audioStream)
|
||||
{
|
||||
releaseInfo->setCondition("if-has-codec", true);
|
||||
releaseInfo->bindString("codec", audioStream->codec);
|
||||
if (audioStream->bitrate)
|
||||
{
|
||||
releaseInfo->setCondition("if-has-bitrate", true);
|
||||
releaseInfo->bindString("bitrate", std::to_string(audioStream->bitrate / 1000) + " kbps");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Wt::WPushButton* okBtn {releaseInfo->bindNew<Wt::WPushButton>("ok-btn", Wt::WString::tr("Lms.ok"))};
|
||||
okBtn->clicked().connect([=]
|
||||
{
|
||||
LmsApp->getModalManager().dispose(releaseInfoPtr);
|
||||
});
|
||||
|
||||
LmsApp->getModalManager().show(std::move(releaseInfo));
|
||||
}
|
||||
|
||||
Release::Release(Filters& filters, PlayQueueController& playQueueController)
|
||||
: Template {Wt::WString::tr("Lms.Explore.Release.template")}
|
||||
, _filters {filters}
|
||||
@@ -176,6 +251,12 @@ Release::refreshView()
|
||||
bindNew<Wt::WPushButton>("download", Wt::WString::tr("Lms.Explore.download"))
|
||||
->setLink(Wt::WLink {std::make_unique<DownloadReleaseResource>(*releaseId)});
|
||||
|
||||
bindNew<Wt::WPushButton>("release-info", Wt::WString::tr("Lms.Explore.release-info"))
|
||||
->clicked().connect([=]
|
||||
{
|
||||
showReleaseInfoModal(*releaseId);
|
||||
});
|
||||
|
||||
{
|
||||
auto isStarred {[=] { return Service<Scrobbling::IScrobblingService>::get()->isStarred(LmsApp->getUserId(), *releaseId); }};
|
||||
|
||||
@@ -270,7 +351,7 @@ Release::refreshView()
|
||||
{
|
||||
if (!firstArtist)
|
||||
artistsContainer->addNew<Wt::WText>(" · ");
|
||||
auto anchor {LmsApplication::createArtistAnchor(artist)};
|
||||
auto anchor {Utils::createArtistAnchor(artist)};
|
||||
anchor->addStyleClass("link-success text-decoration-none"); // hack
|
||||
artistsContainer->addWidget(std::move(anchor));
|
||||
firstArtist = false;
|
||||
@@ -319,6 +400,9 @@ Release::refreshView()
|
||||
|
||||
entry->bindNew<Wt::WPushButton>("download", Wt::WString::tr("Lms.Explore.download"))
|
||||
->setLink(Wt::WLink {std::make_unique<DownloadTrackResource>(trackId)});
|
||||
|
||||
entry->bindNew<Wt::WPushButton>("track-info", Wt::WString::tr("Lms.Explore.track-info"))
|
||||
->clicked().connect([=] { TrackListHelpers::showTrackInfoModal(trackId, _filters); });
|
||||
}
|
||||
|
||||
entry->bindString("duration", Utils::durationToString(track->getDuration()), Wt::TextFormat::Plain);
|
||||
@@ -361,7 +445,7 @@ Release::refreshReleaseArtists(const Database::Release::pointer& release)
|
||||
for (const auto& artist : artists)
|
||||
{
|
||||
Wt::WTemplate* artistTemplate {artistsContainer->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Release.template.entry-release-artist"))};
|
||||
artistTemplate->bindWidget("artist", LmsApplication::createArtistAnchor(artist));
|
||||
artistTemplate->bindWidget("artist", Utils::createArtistAnchor(artist));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ namespace UserInterface
|
||||
SearchView::SearchView(Filters& filters, PlayQueueController& playQueueController)
|
||||
: Wt::WTemplate {Wt::WString::tr("Lms.Explore.Search.template")}
|
||||
, _playQueueController {playQueueController}
|
||||
, _filters {filters}
|
||||
, _artistCollector {filters, ArtistCollector::Mode::Search, getMaxCount(Mode::Artist)}
|
||||
, _releaseCollector {filters, ReleaseCollector::Mode::Search, getMaxCount(Mode::Release)}
|
||||
, _trackCollector {filters, TrackCollector::Mode::Search, getMaxCount(Mode::Track)}
|
||||
@@ -152,7 +153,7 @@ namespace UserInterface
|
||||
for (const TrackId trackId : trackIds.results)
|
||||
{
|
||||
const Track::pointer track {Track::find(LmsApp->getDbSession(), trackId)};
|
||||
_tracks->add(TrackListHelpers::createEntry(track, _playQueueController));
|
||||
_tracks->add(TrackListHelpers::createEntry(track, _playQueueController, _filters));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -72,6 +72,7 @@ namespace UserInterface
|
||||
void addSomeTracks();
|
||||
|
||||
PlayQueueController& _playQueueController;
|
||||
Filters& _filters;
|
||||
ArtistCollector _artistCollector;
|
||||
ReleaseCollector _releaseCollector;
|
||||
TrackCollector _trackCollector;
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include <Wt/WImage.h>
|
||||
#include <Wt/WPushButton.h>
|
||||
|
||||
#include "av/IAudioFile.hpp"
|
||||
#include "services/database/Artist.hpp"
|
||||
#include "services/database/Release.hpp"
|
||||
#include "services/scrobbling/IScrobblingService.hpp"
|
||||
@@ -36,29 +37,97 @@
|
||||
#include "resource/CoverResource.hpp"
|
||||
#include "LmsApplication.hpp"
|
||||
#include "MediaPlayer.hpp"
|
||||
#include "ModalManager.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
using namespace Database;
|
||||
|
||||
namespace UserInterface::TrackListHelpers
|
||||
{
|
||||
void
|
||||
showTrackInfoModal(Database::TrackId trackId, Filters& filters)
|
||||
{
|
||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||
|
||||
const Database::Track::pointer track {Track::find(LmsApp->getDbSession(), trackId)};
|
||||
if (!track)
|
||||
return;
|
||||
|
||||
auto trackInfo {std::make_unique<Template>(Wt::WString::tr("Lms.Explore.Tracks.template.track-info"))};
|
||||
Wt::WWidget* trackInfoPtr {trackInfo.get()};
|
||||
trackInfo->addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||
|
||||
Wt::WContainerWidget* artistTable {trackInfo->bindNew<Wt::WContainerWidget>("artist-table")};
|
||||
|
||||
auto addArtists = [&](TrackArtistLinkType linkType, const char* type)
|
||||
{
|
||||
Artist::FindParameters params;
|
||||
params.setTrack(trackId);
|
||||
params.setLinkType(linkType);
|
||||
const auto artistIds {Artist::find(LmsApp->getDbSession(), params)};
|
||||
if (artistIds.results.empty())
|
||||
return;
|
||||
|
||||
std::unique_ptr<Wt::WContainerWidget> artistContainer {Utils::createArtistContainer(artistIds.results)};
|
||||
auto artistsEntry {std::make_unique<Template>(Wt::WString::tr("Lms.Explore.template.info.artists"))};
|
||||
artistsEntry->bindString("type", Wt::WString::trn(type, artistContainer->count()));
|
||||
artistsEntry->bindWidget("artist-container", std::move(artistContainer));
|
||||
artistTable->addWidget(std::move(artistsEntry));
|
||||
};
|
||||
|
||||
addArtists(TrackArtistLinkType::Composer, "Lms.Explore.Artists.linktype-composer");
|
||||
addArtists(TrackArtistLinkType::Conductor, "Lms.Explore.Artists.linktype-conductor");
|
||||
addArtists(TrackArtistLinkType::Lyricist, "Lms.Explore.Artists.linktype-lyricist");
|
||||
addArtists(TrackArtistLinkType::Mixer, "Lms.Explore.Artists.linktype-mixer");
|
||||
addArtists(TrackArtistLinkType::Remixer, "Lms.Explore.Artists.linktype-remixer");
|
||||
addArtists(TrackArtistLinkType::Producer, "Lms.Explore.Artists.linktype-producer");
|
||||
|
||||
if (const auto audioFile {Av::parseAudioFile(track->getPath())})
|
||||
{
|
||||
const std::optional<Av::StreamInfo> audioStream {audioFile->getBestStreamInfo()};
|
||||
if (audioStream)
|
||||
{
|
||||
trackInfo->setCondition("if-has-codec", true);
|
||||
trackInfo->bindString("codec", audioStream->codec);
|
||||
if (audioStream->bitrate)
|
||||
{
|
||||
trackInfo->setCondition("if-has-bitrate", true);
|
||||
trackInfo->bindString("bitrate", std::to_string(audioStream->bitrate / 1000) + " kbps");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trackInfo->bindString("duration", Utils::durationToString(track->getDuration()));
|
||||
|
||||
Wt::WContainerWidget* clusterContainer {trackInfo->bindWidget("clusters", Utils::createClustersForTrack(track, filters))};
|
||||
if (clusterContainer->count() > 0)
|
||||
trackInfo->setCondition("if-has-clusters", true);
|
||||
|
||||
Wt::WPushButton* okBtn {trackInfo->bindNew<Wt::WPushButton>("ok-btn", Wt::WString::tr("Lms.ok"))};
|
||||
okBtn->clicked().connect([=]
|
||||
{
|
||||
LmsApp->getModalManager().dispose(trackInfoPtr);
|
||||
});
|
||||
|
||||
LmsApp->getModalManager().show(std::move(trackInfo));
|
||||
}
|
||||
|
||||
std::unique_ptr<Wt::WWidget>
|
||||
createEntry(const Database::ObjectPtr<Database::Track>& track, PlayQueueController& playQueueController)
|
||||
createEntry(const Database::ObjectPtr<Database::Track>& track, PlayQueueController& playQueueController, Filters& filters)
|
||||
{
|
||||
auto entry {std::make_unique<Template>(Wt::WString::tr("Lms.Explore.Tracks.template.entry"))};
|
||||
auto* entryPtr {entry.get()};
|
||||
|
||||
entry->bindString("name", Wt::WString::fromUTF8(track->getName()), Wt::TextFormat::Plain);
|
||||
|
||||
const auto artists {track->getArtists({TrackArtistLinkType::Artist})};
|
||||
const Release::pointer release {track->getRelease()};
|
||||
const TrackId trackId {track->getId()};
|
||||
|
||||
if (track->getRelease())
|
||||
{
|
||||
entry->setCondition("if-has-release", true);
|
||||
entry->bindWidget("release", LmsApplication::createReleaseAnchor(track->getRelease()));
|
||||
Wt::WAnchor* anchor {entry->bindWidget("cover", LmsApplication::createReleaseAnchor(release, false))};
|
||||
entry->bindWidget("release", Utils::createReleaseAnchor(track->getRelease()));
|
||||
Wt::WAnchor* anchor {entry->bindWidget("cover", Utils::createReleaseAnchor(release, false))};
|
||||
auto cover {Utils::createCover(release->getId(), CoverResource::Size::Small)};
|
||||
cover->addStyleClass("Lms-cover-track Lms-cover-anchor"); // HACK
|
||||
anchor->setImage(std::move((cover)));
|
||||
@@ -109,6 +178,9 @@ namespace UserInterface::TrackListHelpers
|
||||
entry->bindNew<Wt::WPushButton>("download", Wt::WString::tr("Lms.Explore.download"))
|
||||
->setLink(Wt::WLink {std::make_unique<DownloadTrackResource>(trackId)});
|
||||
|
||||
entry->bindNew<Wt::WPushButton>("track-info", Wt::WString::tr("Lms.Explore.track-info"))
|
||||
->clicked().connect([trackId, &filters] { showTrackInfoModal(trackId, filters); });
|
||||
|
||||
LmsApp->getMediaPlayer().trackLoaded.connect(entryPtr, [=] (Database::TrackId loadedTrackId)
|
||||
{
|
||||
entryPtr->toggleStyleClass("Lms-entry-playing", loadedTrackId == trackId);
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
#include <Wt/WWidget.h>
|
||||
#include "services/database/Object.hpp"
|
||||
#include "services/database/TrackId.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
@@ -32,10 +33,12 @@ namespace Database
|
||||
namespace UserInterface
|
||||
{
|
||||
class PlayQueueController;
|
||||
class Filters;
|
||||
}
|
||||
|
||||
namespace UserInterface::TrackListHelpers
|
||||
{
|
||||
std::unique_ptr<Wt::WWidget> createEntry(const Database::ObjectPtr<Database::Track>& track, PlayQueueController& playQueueController);
|
||||
void showTrackInfoModal(Database::TrackId trackId, Filters& filters);
|
||||
std::unique_ptr<Wt::WWidget> createEntry(const Database::ObjectPtr<Database::Track>& track, PlayQueueController& playQueueController, Filters& filters);
|
||||
} // namespace UserInterface
|
||||
|
||||
|
||||
@@ -199,7 +199,7 @@ namespace UserInterface
|
||||
for (const TrackId trackId : trackIds.results)
|
||||
{
|
||||
if (const Track::pointer track {Track::find(LmsApp->getDbSession(), trackId)})
|
||||
_container->add(TrackListHelpers::createEntry(track, _playQueueController));
|
||||
_container->add(TrackListHelpers::createEntry(track, _playQueueController, _filters));
|
||||
}
|
||||
|
||||
_container->setHasMore(trackIds.moreResults && _container->getCount() < _maxCount);
|
||||
|
||||
@@ -136,7 +136,7 @@ namespace UserInterface
|
||||
const TrackListId trackListId {trackList->getId()};
|
||||
|
||||
WTemplate* entry {_container->addNew<Template>(Wt::WString::tr("Lms.Explore.TrackLists.template.entry"))};
|
||||
entry->bindWidget("name", LmsApplication::createTrackListAnchor(trackList));
|
||||
entry->bindWidget("name", Utils::createTrackListAnchor(trackList));
|
||||
|
||||
assert(_trackListWidgets.find(trackListId) == std::cend(_trackListWidgets));
|
||||
_trackListWidgets.emplace(trackListId, entry);
|
||||
|
||||
@@ -37,6 +37,7 @@ namespace UserInterface {
|
||||
|
||||
Tracks::Tracks(Filters& filters, PlayQueueController& playQueueController)
|
||||
: Template {Wt::WString::tr("Lms.Explore.Tracks.template")}
|
||||
, _filters {filters}
|
||||
, _playQueueController {playQueueController}
|
||||
, _trackCollector {filters, _defaultMode, _maxCount}
|
||||
{
|
||||
@@ -124,7 +125,7 @@ Tracks::addSome()
|
||||
for (const TrackId trackId : trackIds.results)
|
||||
{
|
||||
if (const Track::pointer track {Track::find(LmsApp->getDbSession(), trackId)})
|
||||
_container->add(TrackListHelpers::createEntry(track, _playQueueController));
|
||||
_container->add(TrackListHelpers::createEntry(track, _playQueueController, _filters));
|
||||
}
|
||||
|
||||
_container->setHasMore(trackIds.moreResults);
|
||||
|
||||
@@ -46,6 +46,7 @@ namespace UserInterface
|
||||
static constexpr std::size_t _batchSize {6};
|
||||
static constexpr std::size_t _maxCount {8000};
|
||||
|
||||
Filters& _filters;
|
||||
PlayQueueController& _playQueueController;
|
||||
Wt::WWidget* _currentActiveItem {};
|
||||
InfiniteScrollingContainer* _container {};
|
||||
|
||||
Reference in New Issue
Block a user