Merge branch 'develop' for release v3.33.0

This commit is contained in:
emeric
2022-11-25 20:33:15 +01:00
40 changed files with 764 additions and 189 deletions
+1 -1
View File
@@ -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)
+4 -4
View File
@@ -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">
+12
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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>
+43
View File
@@ -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>
+1 -1
View File
@@ -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>
+60
View File
@@ -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>
+51 -23
View File
@@ -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 {};
+4 -1
View File
@@ -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 {};
};
+6 -4
View File
@@ -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;
};
+11 -1
View File
@@ -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 {};
+47 -5
View File
@@ -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());
}
}
-65
View File
@@ -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)
{
-10
View File
@@ -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; }
-3
View File
@@ -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
+4 -3
View File
@@ -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
{
+2 -2
View File
@@ -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));
+126
View File
@@ -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
View File
@@ -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);
}
+6 -6
View File
@@ -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")));
+2 -2
View File
@@ -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;
}
+1 -1
View File
@@ -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;
}
+2 -1
View File
@@ -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");
+1
View File
@@ -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")};
+3 -4
View File
@@ -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)
+86 -2
View File
@@ -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));
}
}
}
+2 -1
View File
@@ -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));
}
}
+1
View File
@@ -72,6 +72,7 @@ namespace UserInterface
void addSomeTracks();
PlayQueueController& _playQueueController;
Filters& _filters;
ArtistCollector _artistCollector;
ReleaseCollector _releaseCollector;
TrackCollector _trackCollector;
+76 -4
View File
@@ -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);
+4 -1
View File
@@ -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
+1 -1
View File
@@ -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);
+1 -1
View File
@@ -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);
+2 -1
View File
@@ -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);
+1
View File
@@ -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 {};