Merge branch 'develop' for release v3.74.0

This commit is contained in:
emeric
2026-01-04 19:13:46 +01:00
100 changed files with 2392 additions and 1243 deletions
+51
View File
@@ -0,0 +1,51 @@
name: Basic Build (macOS)
on: [pull_request]
jobs:
build-macos:
runs-on: macos-14
env:
PKG_CONFIG_PATH: /opt/homebrew/lib/pkgconfig:/opt/homebrew/share/pkgconfig
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: |
brew update
brew install pkg-config cmake boost ffmpeg libconfig taglib libarchive xxhash pugixml googletest openssl git
- name: Build and install Wt
run: |
WT_VERSION=4.12.1
git clone https://github.com/emweb/wt.git /tmp/wt
cd /tmp/wt
git checkout ${WT_VERSION}
cmake -B build -S . \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=/usr/local \
-DBUILD_EXAMPLES=OFF \
-DENABLE_LIBWTTEST=OFF \
-DCONNECTOR_FCGI=OFF
cmake --build build --parallel 2
sudo cmake --install build
- name: Install Stb headers
run: |
git clone https://github.com/nothings/stb.git /opt/homebrew/include/stb
- name: Configure LMS
run: |
export PKG_CONFIG_PATH="$(brew --prefix libarchive)/lib/pkgconfig:$PKG_CONFIG_PATH"
cmake -S . -B build \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_UNITY_BUILD=ON \
-DBUILD_TESTING=ON \
-DSTB_IMAGE_INCLUDE_DIR=/opt/homebrew/include/stb
- name: Build LMS
run: cmake --build build --parallel 2
- name: Test LMS
run: ctest --test-dir build --output-on-failure
+1 -1
View File
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.12) cmake_minimum_required(VERSION 3.12)
project(lms VERSION 3.73.0) project(lms VERSION 3.74.0)
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake/modules/) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake/modules/)
+2 -2
View File
@@ -102,10 +102,10 @@ If a setting is not present in the configuration file, a hardcoded default value
## Authentication backends ## Authentication backends
You can define which authentication backend to be used thanks to the `authentication-backend` option: You can define which authentication backend to be used thanks to the `authentication-backend` option:
* `internal` (default): _LMS_ uses an internal database to store users and their associated passwords (salted and hashed using [Bcrypt](https://en.wikipedia.org/wiki/Bcrypt)). Only the admin user can create, edit or remove other users. * `internal` (default): _LMS_ uses an internal database to store users and their associated passwords (salted and hashed using [Bcrypt](https://en.wikipedia.org/wiki/Bcrypt)). Only the admin user can create, edit or remove other users.
* `PAM`: the user/password authentication request is forwarded to PAM (see the default [PAM configuration file](conf/pam/lms) provided). * `PAM`: the user/password authentication request is forwarded to PAM. A default example PAM configuration file is provided at [`conf/pam/lms`](conf/pam/lms), which demonstrates how to authenticate using system passwords. **Note:** this file is just a base example; depending on your PAM setup, you may need to adjust the service file or PAM rules to ensure proper privileges.
* `http-headers`: _LMS_ uses a configurable HTTP header field, typically set by a reverse proxy to handle [SSO](https://en.wikipedia.org/wiki/Single_sign-on), to extract the login name. You can customize the field to be used using the `http-headers-login-field` option. * `http-headers`: _LMS_ uses a configurable HTTP header field, typically set by a reverse proxy to handle [SSO](https://en.wikipedia.org/wiki/Single_sign-on), to extract the login name. You can customize the field to be used using the `http-headers-login-field` option.
__Note__: the first created user is the admin user. __Note__: the first created user is the admin user.
#### `internal` backend: reset admin password ### `internal` backend: reset admin password
Open the the database file located in `/var/lms/lms.db` using `sqlite3`: Open the the database file located in `/var/lms/lms.db` using `sqlite3`:
```sh ```sh
sqlite3 /var/lms/lms.db sqlite3 /var/lms/lms.db
+2 -1
View File
@@ -49,7 +49,8 @@ _LMS_ supports an Artist information folder to manage metadata and images for ar
The folder must follow a structure defined by Kodi, as detailed [here](https://kodi.wiki/view/Artist_information_folder). `artist.nfo` files are used to define additional artist information such as biography, sort name, and MusicBrainz ArtistID. See the format [here](https://kodi.wiki/view/NFO_files/Artists). The folder must follow a structure defined by Kodi, as detailed [here](https://kodi.wiki/view/Artist_information_folder). `artist.nfo` files are used to define additional artist information such as biography, sort name, and MusicBrainz ArtistID. See the format [here](https://kodi.wiki/view/NFO_files/Artists).
__Note__: If no name is provided in the `artist.nfo` file, the name of the containing folder is used. The canonical artist name used by _LMS_ is the one specified in the `artist.nfo` file; If an `artist.nfo` exists but does not provide a name, the name of the containing folder is used.
If no artist info file is provided, _LMS_ will pick the artist name found on the latest release.
### Filtering ### Filtering
It is possible to apply global filters on your collection using `genre`, `mood`, `grouping`, `language`, `codec`, and by music library. More tags, including custom ones, can be added in the database administration settings. It is possible to apply global filters on your collection using `genre`, `mood`, `grouping`, `language`, `codec`, and by music library. More tags, including custom ones, can be added in the database administration settings.
+4
View File
@@ -34,7 +34,9 @@ The following extra fields are implemented:
* `musicBrainzId` * `musicBrainzId`
* `originalReleaseDate` * `originalReleaseDate`
* `recordLabels` * `recordLabels`
* `releaseDate`
* `releaseTypes` * `releaseTypes`
* `sortName`
* `userRating` * `userRating`
* `version` * `version`
* `Child` response: * `Child` response:
@@ -64,5 +66,7 @@ The following extra fields are implemented:
* [API Key Authentication](https://opensubsonic.netlify.app/docs/extensions/apikeyauth/) * [API Key Authentication](https://opensubsonic.netlify.app/docs/extensions/apikeyauth/)
* [getPodcastEpisode](https://opensubsonic.netlify.app/docs/extensions/getpodcastepisode/) * [getPodcastEpisode](https://opensubsonic.netlify.app/docs/extensions/getpodcastepisode/)
* [HTTP form POST](https://opensubsonic.netlify.app/docs/extensions/formpost/) * [HTTP form POST](https://opensubsonic.netlify.app/docs/extensions/formpost/)
* [Index based Queue](https://opensubsonic.netlify.app/docs/extensions/indexbasedqueue/)
* [Transcode offset](https://opensubsonic.netlify.app/docs/extensions/transcodeoffset/) * [Transcode offset](https://opensubsonic.netlify.app/docs/extensions/transcodeoffset/)
* [Song Lyrics](https://opensubsonic.netlify.app/docs/extensions/songlyrics/) * [Song Lyrics](https://opensubsonic.netlify.app/docs/extensions/songlyrics/)
* [Transcoding](https://opensubsonic.netlify.app/docs/extensions/transcoding/)
+1 -1
View File
@@ -15,7 +15,7 @@
${</if-has-biography>} ${</if-has-biography>}
<div class="btn-group"> <div class="btn-group">
${play-btn class="btn btn-primary"} ${play-btn class="btn btn-primary"}
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false"/> <button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false" aria-label="${tr:Lms.more}"/>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li>${play-next class="dropdown-item"}</li> <li>${play-next class="dropdown-item"}</li>
<li>${play-last class="dropdown-item"}</li> <li>${play-last class="dropdown-item"}</li>
+14 -12
View File
@@ -4,7 +4,7 @@
<message id="Lms.Explore.Artists.template"> <message id="Lms.Explore.Artists.template">
<div class="d-flex justify-content-between align-items-center mb-3"> <div class="d-flex justify-content-between align-items-center mb-3">
${link-type class="me-1"} ${artist-type class="me-1"}
<div class="d-flex"> <div class="d-flex">
${search class="form-control form-control-sm me-1" type="search"} ${search class="form-control form-control-sm me-1" type="search"}
${sort-mode} ${sort-mode}
@@ -29,22 +29,24 @@
${loading-indicator} ${loading-indicator}
</message> </message>
<message id="Lms.Explore.Artists.template.track-artist-link-type-selector"> <message id="Lms.Explore.Artists.template.artist-type-selector">
<div class="dropdown"> <div class="dropdown">
<button class="btn btn-sm btn-outline-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false"> <button class="btn btn-sm btn-outline-primary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
${selected-item} ${selected-item}
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li>${link-type-all class="dropdown-item"}</li> <li>${artist-type-all class="dropdown-item"}</li>
<li>${link-type-artist class="dropdown-item"}</li> <li class="dropdown-divider"></li>
<li>${link-type-releaseartist class="dropdown-item"}</li> <li>${artist-type-release-artist class="dropdown-item"}</li>
<li>${link-type-composer class="dropdown-item"}</li> <li class="dropdown-divider"></li>
<li>${link-type-conductor class="dropdown-item"}</li> <li>${artist-type-track-artist class="dropdown-item"}</li>
<li>${link-type-lyricist class="dropdown-item"}</li> <li>${artist-type-composer class="dropdown-item"}</li>
<li>${link-type-mixer class="dropdown-item"}</li> <li>${artist-type-conductor class="dropdown-item"}</li>
<li>${link-type-performer class="dropdown-item"}</li> <li>${artist-type-lyricist class="dropdown-item"}</li>
<li>${link-type-producer class="dropdown-item"}</li> <li>${artist-type-mixer class="dropdown-item"}</li>
<li>${link-type-remixer class="dropdown-item"}</li> <li>${artist-type-performer class="dropdown-item"}</li>
<li>${artist-type-producer class="dropdown-item"}</li>
<li>${artist-type-remixer class="dropdown-item"}</li>
</ul> </ul>
</div> </div>
</message> </message>
+2 -1
View File
@@ -58,13 +58,14 @@
${selected-item} ${selected-item}
</button> </button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li>${all class="dropdown-item"}</li>
<li class="dropdown-divider"></li>
<li>${random class="dropdown-item"}</li> <li>${random class="dropdown-item"}</li>
<li>${starred class="dropdown-item"}</li> <li>${starred class="dropdown-item"}</li>
<li>${recently-played class="dropdown-item"}</li> <li>${recently-played class="dropdown-item"}</li>
<li>${most-played class="dropdown-item"}</li> <li>${most-played class="dropdown-item"}</li>
<li>${recently-added class="dropdown-item"}</li> <li>${recently-added class="dropdown-item"}</li>
<li>${recently-modified class="dropdown-item"}</li> <li>${recently-modified class="dropdown-item"}</li>
<li>${all class="dropdown-item"}</li>
</ul> </ul>
</div> </div>
</message> </message>
+2 -2
View File
@@ -3,11 +3,11 @@
<message id="Lms.main.template"> <message id="Lms.main.template">
${navbar} ${navbar}
<div class="container"> <main class="container">
${contents} ${contents}
${notifications class="toast-container Lms-notification-container p-3"} ${notifications class="toast-container Lms-notification-container p-3"}
${modal} ${modal}
</div> </main>
${player class="fixed-bottom bg-dark Lms-player"} ${player class="fixed-bottom bg-dark Lms-player"}
</message> </message>
+9 -7
View File
@@ -5,9 +5,9 @@
<message id="Lms.MediaPlayer.template"> <message id="Lms.MediaPlayer.template">
<audio id="lms-mp-audio"> <audio id="lms-mp-audio">
</audio> </audio>
<div class="container"> <div class="container" role="region" aria-label="${tr:Lms.Player.now-playing}">
<div class="p-1 my-1 position-relative"> <div class="p-1 my-1 position-relative">
<input id="lms-mp-seek" class="Lms-player-seek-common Lms-player-seek" type="range" min="0" max="100" step="1" value="0"/> <input id="lms-mp-seek" class="Lms-player-seek-common Lms-player-seek" type="range" min="0" max="100" step="1" value="0" aria-label="${tr:Lms.Player.seek}"/>
<div class="progress Lms-player-seek-common bg-secondary rounded"> <div class="progress Lms-player-seek-common bg-secondary rounded">
<div id="lms-mp-progress" class="progress-bar" role="progressbar" aria-valuenow="20" aria-valuemin="0" aria-valuemax="100" style="width: 0%"></div> <div id="lms-mp-progress" class="progress-bar" role="progressbar" aria-valuenow="20" aria-valuemin="0" aria-valuemax="100" style="width: 0%"></div>
</div> </div>
@@ -15,23 +15,25 @@
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div class="py-2 pe-2 d-inline-flex"> <div class="py-2 pe-2 d-inline-flex">
<button id="lms-mp-previous" class="btn btn-sm btn-outline-primary border-0"> <button id="lms-mp-previous" class="btn btn-sm btn-outline-primary border-0" aria-label="${tr:Lms.Player.previous}">
<i class="fa fa-fw fa-step-backward"/> <i class="fa fa-fw fa-step-backward"/>
</button> </button>
<button id="lms-mp-playpause" class="btn btn-outline-primary border-0"> <button id="lms-mp-playpause" class="btn btn-outline-primary border-0" aria-label="${tr:Lms.Player.play-pause}">
<i class="fa fa-fw fa-play"/> <i class="fa fa-fw fa-play"/>
</button> </button>
<button id="lms-mp-next" class="btn btn-sm btn-outline-primary border-0"> <button id="lms-mp-next" class="btn btn-sm btn-outline-primary border-0" aria-label="${tr:Lms.Player.next}">
<i class="fa fa-fw fa-step-forward"/> <i class="fa fa-fw fa-step-forward"/>
</button> </button>
</div> </div>
<div class="flex-fill text-center text-truncate"> <div class="flex-fill text-center text-truncate">
${title class="d-block text-truncate"} ${title class="d-block text-truncate"}
${artist class="d-inline text-decoration-none text-truncate link-success"} <span>
${artist class="d-inline text-decoration-none text-truncate"}
<span class="d-none d-md-inline"> <span class="d-none d-md-inline">
${separator} ${separator}
${release class="text-decoration-none text-truncate link-success"} ${release class="text-decoration-none text-truncate link-success"}
</span> </span>
</span>
</div> </div>
<div class="p-2 d-none d-sm-block text-muted text-nowrap"> <div class="p-2 d-none d-sm-block text-muted text-nowrap">
<span id="lms-transcoding-active" title="${tr:Lms.Player.transcoding-active}" class="p-2" style="display:none"><i class="fa fa-fw fa-info-circle" aria-hidden="true"></i></span> <span id="lms-transcoding-active" title="${tr:Lms.Player.transcoding-active}" class="p-2" style="display:none"><i class="fa fa-fw fa-info-circle" aria-hidden="true"></i></span>
@@ -39,7 +41,7 @@
</div> </div>
<div class="p-2 d-none d-md-flex align-items-center Lms-player-volume-container"> <div class="p-2 d-none d-md-flex align-items-center Lms-player-volume-container">
<i id="lms-mp-volume" class="fa fa-fw fa-volume-up link-primary me-1"></i> <i id="lms-mp-volume" class="fa fa-fw fa-volume-up link-primary me-1"></i>
<input id ="lms-mp-volume-slider" class="form-range" type="range" min="0" max="1" step="0.01" value="0.8"/> <input id ="lms-mp-volume-slider" class="form-range" type="range" min="0" max="1" step="0.01" value="0.8" aria-label="${tr:Lms.Player.volume}"/>
</div> </div>
<div class="py-2 ps-2"> <div class="py-2 ps-2">
${playqueue-btn class="btn btn-outline-primary border-0"} ${playqueue-btn class="btn btn-outline-primary border-0"}
+24 -15
View File
@@ -9,12 +9,14 @@
<message id="Lms.clusters">Tags</message> <message id="Lms.clusters">Tags</message>
<message id="Lms.create">Create</message> <message id="Lms.create">Create</message>
<message id="Lms.delete">Delete</message> <message id="Lms.delete">Delete</message>
<message id="Lms.delete-item">Delete {1}</message>
<message id="Lms.discard">Discard</message> <message id="Lms.discard">Discard</message>
<message id="Lms.edit">Edit</message> <message id="Lms.edit">Edit</message>
<message id="Lms.field-must-be-in-upper-case">This field must be in upper case</message> <message id="Lms.field-must-be-in-upper-case">This field must be in upper case</message>
<message id="Lms.loading">Loading...</message> <message id="Lms.loading">Loading...</message>
<message id="Lms.login">Login</message> <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.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> Logout</message>
<message id="Lms.more">More...</message>
<message id="Lms.ok">Ok</message> <message id="Lms.ok">Ok</message>
<message id="Lms.password">Password</message> <message id="Lms.password">Password</message>
<message id="Lms.password-bad-login-combination">Bad login / password combination</message> <message id="Lms.password-bad-login-combination">Bad login / password combination</message>
@@ -25,6 +27,7 @@
<message id="Lms.password-old">Old password</message> <message id="Lms.password-old">Old password</message>
<message id="Lms.password-too-weak">Password too weak</message> <message id="Lms.password-too-weak">Password too weak</message>
<message id="Lms.passwords-dont-match">Passwords don't match</message> <message id="Lms.passwords-dont-match">Passwords don't match</message>
<message id="Lms.play-item">Play {1}</message>
<message id="Lms.quit-other-session">Another session has been open. Reopen this one?</message> <message id="Lms.quit-other-session">Another session has been open. Reopen this one?</message>
<message id="Lms.save">Save</message> <message id="Lms.save">Save</message>
<message id="Lms.settings-saved">Settings saved!</message> <message id="Lms.settings-saved">Settings saved!</message>
@@ -145,8 +148,6 @@
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Checking for removed files... {1}%</message> <message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Checking for removed files... {1}%</message>
<message id="Lms.Admin.ScannerController.step-compact">Compacting database...</message> <message id="Lms.Admin.ScannerController.step-compact">Compacting database...</message>
<message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Computing stats... {1}%</message> <message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Computing stats... {1}%</message>
<message id="Lms.Admin.ScannerController.step-discovering-files">Discovering files: {1} files</message>
<message id="Lms.Admin.ScannerController.step-fetching-track-features">Fetching track features from AcousticBrainz: {1}/{2} tracks ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-optimize">Optimizing database... {1}%...</message> <message id="Lms.Admin.ScannerController.step-optimize">Optimizing database... {1}%...</message>
<message id="Lms.Admin.ScannerController.step-reconciliate-artists">Reconciliating artists: {1} entries...</message> <message id="Lms.Admin.ScannerController.step-reconciliate-artists">Reconciliating artists: {1} entries...</message>
<message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">Reloading similarity engine: {1}%...</message> <message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">Reloading similarity engine: {1}%...</message>
@@ -198,6 +199,7 @@
<message id="Lms.Explore.artists">Artists</message> <message id="Lms.Explore.artists">Artists</message>
<message id="Lms.Explore.bitrate">Bitrate</message> <message id="Lms.Explore.bitrate">Bitrate</message>
<message id="Lms.Explore.codec">Codec</message> <message id="Lms.Explore.codec">Codec</message>
<message id="Lms.Explore.cover-art">Cover art</message>
<message id="Lms.Explore.download">Download</message> <message id="Lms.Explore.download">Download</message>
<message id="Lms.Explore.duration">Duration</message> <message id="Lms.Explore.duration">Duration</message>
<message id="Lms.Explore.filter-added">Filter added</message> <message id="Lms.Explore.filter-added">Filter added</message>
@@ -221,6 +223,7 @@
<message id="Lms.Explore.releases">Albums</message> <message id="Lms.Explore.releases">Albums</message>
<message id="Lms.Explore.search">Search</message> <message id="Lms.Explore.search">Search</message>
<message id="Lms.Explore.star">Star</message> <message id="Lms.Explore.star">Star</message>
<message id="Lms.Explore.star-item">Star {1}</message>
<message id="Lms.Explore.starred">Starred</message> <message id="Lms.Explore.starred">Starred</message>
<message id="Lms.Explore.track-info">Track information</message> <message id="Lms.Explore.track-info">Track information</message>
<message id="Lms.Explore.track-lyrics">Lyrics</message> <message id="Lms.Explore.track-lyrics">Lyrics</message>
@@ -237,43 +240,43 @@
<message id="Lms.Explore.Artist.similar-artists">Similar artists</message> <message id="Lms.Explore.Artist.similar-artists">Similar artists</message>
<!--Explore:Artists--> <!--Explore:Artists-->
<message id="Lms.Explore.Artists.linktype-all">All artists</message> <message id="Lms.Explore.Artists.all">All artists</message>
<message id="Lms.Explore.Artists.linktype-artist"> <message id="Lms.Explore.Artists.composer">
<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="0">Composer</plural>
<plural case="1">Composers</plural> <plural case="1">Composers</plural>
</message> </message>
<message id="Lms.Explore.Artists.linktype-conductor"> <message id="Lms.Explore.Artists.conductor">
<plural case="0">Conductor</plural> <plural case="0">Conductor</plural>
<plural case="1">Conductors</plural> <plural case="1">Conductors</plural>
</message> </message>
<message id="Lms.Explore.Artists.linktype-lyricist"> <message id="Lms.Explore.Artists.lyricist">
<plural case="0">Lyricist</plural> <plural case="0">Lyricist</plural>
<plural case="1">Lyricists</plural> <plural case="1">Lyricists</plural>
</message> </message>
<message id="Lms.Explore.Artists.linktype-mixer"> <message id="Lms.Explore.Artists.mixer">
<plural case="0">Mixer</plural> <plural case="0">Mixer</plural>
<plural case="1">Mixers</plural> <plural case="1">Mixers</plural>
</message> </message>
<message id="Lms.Explore.Artists.linktype-performer"> <message id="Lms.Explore.Artists.performer">
<plural case="0">Performer</plural> <plural case="0">Performer</plural>
<plural case="1">Performers</plural> <plural case="1">Performers</plural>
</message> </message>
<message id="Lms.Explore.Artists.linktype-producer"> <message id="Lms.Explore.Artists.producer">
<plural case="0">Producer</plural> <plural case="0">Producer</plural>
<plural case="1">Producers</plural> <plural case="1">Producers</plural>
</message> </message>
<message id="Lms.Explore.Artists.linktype-releaseartist"> <message id="Lms.Explore.Artists.release-artist">
<plural case="0">Album artist</plural> <plural case="0">Album artist</plural>
<plural case="1">Album artists</plural> <plural case="1">Album artists</plural>
</message> </message>
<message id="Lms.Explore.Artists.linktype-remixer"> <message id="Lms.Explore.Artists.remixer">
<plural case="0">Remixer</plural> <plural case="0">Remixer</plural>
<plural case="1">Remixers</plural> <plural case="1">Remixers</plural>
</message> </message>
<message id="Lms.Explore.Artists.track-artist">
<plural case="0">Track artist</plural>
<plural case="1">Track artists</plural>
</message>
<!--Explore:Release--> <!--Explore:Release-->
<message id="Lms.Explore.Release.copyright">Copyright</message> <message id="Lms.Explore.Release.copyright">Copyright</message>
@@ -309,6 +312,12 @@
<!--Player--> <!--Player-->
<message id="Lms.Player.transcoding-active">Transcoding active</message> <message id="Lms.Player.transcoding-active">Transcoding active</message>
<message id="Lms.Player.now-playing">Now playing</message>
<message id="Lms.Player.previous">Previous</message>
<message id="Lms.Player.play-pause">Play / pause</message>
<message id="Lms.Player.next">Next</message>
<message id="Lms.Player.seek">Seek</message>
<message id="Lms.Player.volume">Volume</message>
<!--Playqueue--> <!--Playqueue-->
<message id="Lms.PlayQueue.clear">Clear</message> <message id="Lms.PlayQueue.clear">Clear</message>
+24 -14
View File
@@ -9,12 +9,14 @@
<message id="Lms.clusters">Etiquetas</message> <message id="Lms.clusters">Etiquetas</message>
<message id="Lms.create">Crear</message> <message id="Lms.create">Crear</message>
<message id="Lms.delete">Eliminar</message> <message id="Lms.delete">Eliminar</message>
<message id="Lms.delete-item">Eliminar {1}</message>
<message id="Lms.discard">Descartar</message> <message id="Lms.discard">Descartar</message>
<message id="Lms.edit">Editar</message> <message id="Lms.edit">Editar</message>
<message id="Lms.field-must-be-in-upper-case">Este campo debe estar en mayúsculas</message> <message id="Lms.field-must-be-in-upper-case">Este campo debe estar en mayúsculas</message>
<message id="Lms.loading">Cargando...</message> <message id="Lms.loading">Cargando...</message>
<message id="Lms.login">Usuario</message> <message id="Lms.login">Usuario</message>
<message id="Lms.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> Salir</message> <message id="Lms.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> Salir</message>
<message id="Lms.more">Más...</message>
<message id="Lms.ok">Ok</message> <message id="Lms.ok">Ok</message>
<message id="Lms.password">Contraseña</message> <message id="Lms.password">Contraseña</message>
<message id="Lms.password-bad-login-combination">Combinación incorrecta de usuario / contraseña</message> <message id="Lms.password-bad-login-combination">Combinación incorrecta de usuario / contraseña</message>
@@ -25,6 +27,7 @@
<message id="Lms.password-old">Contraseña antigua</message> <message id="Lms.password-old">Contraseña antigua</message>
<message id="Lms.password-too-weak">La contraseña es demasiado débil</message> <message id="Lms.password-too-weak">La contraseña es demasiado débil</message>
<message id="Lms.passwords-dont-match">Las contraseñas no coinciden</message> <message id="Lms.passwords-dont-match">Las contraseñas no coinciden</message>
<message id="Lms.play-item">Reproducir {1}</message>
<message id="Lms.quit-other-session">Se ha abierto otra sesión. ¿Reabrir la actúal?</message> <message id="Lms.quit-other-session">Se ha abierto otra sesión. ¿Reabrir la actúal?</message>
<message id="Lms.save">Guardar</message> <message id="Lms.save">Guardar</message>
<message id="Lms.settings-saved">¡Ajustes guardados!</message> <message id="Lms.settings-saved">¡Ajustes guardados!</message>
@@ -145,7 +148,6 @@
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Comprobando ficheros eliminados... {1}%</message> <message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Comprobando ficheros eliminados... {1}%</message>
<message id="Lms.Admin.ScannerController.step-compact">Compactando la base de datos...</message> <message id="Lms.Admin.ScannerController.step-compact">Compactando la base de datos...</message>
<message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Calculando estadísticas... {1}%</message> <message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Calculando estadísticas... {1}%</message>
<message id="Lms.Admin.ScannerController.step-fetching-track-features">Accediendo a las características de la pista desde AcousticBrainz : {1}/{2} ficheros ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-optimize">Optimizando la base de datos... {1}%...</message> <message id="Lms.Admin.ScannerController.step-optimize">Optimizando la base de datos... {1}%...</message>
<message id="Lms.Admin.ScannerController.step-reconciliate-artists">Reconciliando artistas: {1} entradas...</message> <message id="Lms.Admin.ScannerController.step-reconciliate-artists">Reconciliando artistas: {1} entradas...</message>
<message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">Recargando el motor de similitud: {1}%...</message> <message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">Recargando el motor de similitud: {1}%...</message>
@@ -197,6 +199,7 @@
<message id="Lms.Explore.artists">Artistas</message> <message id="Lms.Explore.artists">Artistas</message>
<message id="Lms.Explore.bitrate">Bitrate</message> <message id="Lms.Explore.bitrate">Bitrate</message>
<message id="Lms.Explore.codec">Codec</message> <message id="Lms.Explore.codec">Codec</message>
<message id="Lms.Explore.cover-art">Carátula</message>
<message id="Lms.Explore.download">Descargar</message> <message id="Lms.Explore.download">Descargar</message>
<message id="Lms.Explore.duration">Duración</message> <message id="Lms.Explore.duration">Duración</message>
<message id="Lms.Explore.filter-added">Filtro añadido</message> <message id="Lms.Explore.filter-added">Filtro añadido</message>
@@ -220,6 +223,7 @@
<message id="Lms.Explore.releases">Álbumes</message> <message id="Lms.Explore.releases">Álbumes</message>
<message id="Lms.Explore.search">Buscar</message> <message id="Lms.Explore.search">Buscar</message>
<message id="Lms.Explore.star">Añadir a favoritos</message> <message id="Lms.Explore.star">Añadir a favoritos</message>
<message id="Lms.Explore.star-item">Agregar {1} a favoritos</message>
<message id="Lms.Explore.starred">Favoritos</message> <message id="Lms.Explore.starred">Favoritos</message>
<message id="Lms.Explore.track-info">Información sobre la pista</message> <message id="Lms.Explore.track-info">Información sobre la pista</message>
<message id="Lms.Explore.track-lyrics">Letras</message> <message id="Lms.Explore.track-lyrics">Letras</message>
@@ -236,43 +240,43 @@
<message id="Lms.Explore.Artist.similar-artists">Artistas similares</message> <message id="Lms.Explore.Artist.similar-artists">Artistas similares</message>
<!--Explore:Artists--> <!--Explore:Artists-->
<message id="Lms.Explore.Artists.linktype-all">Todos los artistas</message> <message id="Lms.Explore.Artists.all">Todos los artistas</message>
<message id="Lms.Explore.Artists.linktype-artist"> <message id="Lms.Explore.Artists.composer">
<plural case="0">Artista de la pista</plural>
<plural case="1">Artistas de la pista</plural>
</message>
<message id="Lms.Explore.Artists.linktype-composer">
<plural case="0">Compositor</plural> <plural case="0">Compositor</plural>
<plural case="1">Compositores</plural> <plural case="1">Compositores</plural>
</message> </message>
<message id="Lms.Explore.Artists.linktype-conductor"> <message id="Lms.Explore.Artists.conductor">
<plural case="0">Director de orquesta</plural> <plural case="0">Director de orquesta</plural>
<plural case="1">Directores de orquesta</plural> <plural case="1">Directores de orquesta</plural>
</message> </message>
<message id="Lms.Explore.Artists.linktype-lyricist"> <message id="Lms.Explore.Artists.lyricist">
<plural case="0">Letrista</plural> <plural case="0">Letrista</plural>
<plural case="1">Letristas</plural> <plural case="1">Letristas</plural>
</message> </message>
<message id="Lms.Explore.Artists.linktype-mixer"> <message id="Lms.Explore.Artists.mixer">
<plural case="0">Mezclador</plural> <plural case="0">Mezclador</plural>
<plural case="1">Mezcladores</plural> <plural case="1">Mezcladores</plural>
</message> </message>
<message id="Lms.Explore.Artists.linktype-performer"> <message id="Lms.Explore.Artists.performer">
<plural case="0">Intérprete</plural> <plural case="0">Intérprete</plural>
<plural case="1">Intérpretes</plural> <plural case="1">Intérpretes</plural>
</message> </message>
<message id="Lms.Explore.Artists.linktype-producer"> <message id="Lms.Explore.Artists.producer">
<plural case="0">Productor</plural> <plural case="0">Productor</plural>
<plural case="1">Productores</plural> <plural case="1">Productores</plural>
</message> </message>
<message id="Lms.Explore.Artists.linktype-releaseartist"> <message id="Lms.Explore.Artists.release-artist">
<plural case="0">Artista del álbum</plural> <plural case="0">Artista del álbum</plural>
<plural case="1">Artistas del álbum</plural> <plural case="1">Artistas del álbum</plural>
</message> </message>
<message id="Lms.Explore.Artists.linktype-remixer"> <message id="Lms.Explore.Artists.remixer">
<plural case="0">Remezclador</plural> <plural case="0">Remezclador</plural>
<plural case="1">Remezcladores</plural> <plural case="1">Remezcladores</plural>
</message> </message>
<message id="Lms.Explore.Artists.track-artist">
<plural case="0">Artista de la pista</plural>
<plural case="1">Artistas de la pista</plural>
</message>
<!--Explore:Release--> <!--Explore:Release-->
<message id="Lms.Explore.Release.copyright">Copyright</message> <message id="Lms.Explore.Release.copyright">Copyright</message>
@@ -308,6 +312,12 @@
<!--Player--> <!--Player-->
<message id="Lms.Player.transcoding-active">Transcodificación activa</message> <message id="Lms.Player.transcoding-active">Transcodificación activa</message>
<message id="Lms.Player.now-playing">Reproduciendo ahora</message>
<message id="Lms.Player.previous">Anterior</message>
<message id="Lms.Player.play-pause">Reproducir / pausar</message>
<message id="Lms.Player.next">Siguiente</message>
<message id="Lms.Player.seek">Búsqueda</message>
<message id="Lms.Player.volume">Volumen</message>
<!--Playqueue--> <!--Playqueue-->
<message id="Lms.PlayQueue.clear">Vaciar la lista</message> <message id="Lms.PlayQueue.clear">Vaciar la lista</message>
+24 -14
View File
@@ -9,12 +9,14 @@
<message id="Lms.clusters">Tags</message> <message id="Lms.clusters">Tags</message>
<message id="Lms.create">Créer</message> <message id="Lms.create">Créer</message>
<message id="Lms.delete">Supprimer</message> <message id="Lms.delete">Supprimer</message>
<message id="Lms.delete-item">Supprimer {1}</message>
<message id="Lms.discard">Annuler</message> <message id="Lms.discard">Annuler</message>
<message id="Lms.edit">Editer</message> <message id="Lms.edit">Editer</message>
<message id="Lms.field-must-be-in-upper-case">Ce champ doit être en majuscule</message> <message id="Lms.field-must-be-in-upper-case">Ce champ doit être en majuscule</message>
<message id="Lms.loading">Chargement...</message> <message id="Lms.loading">Chargement...</message>
<message id="Lms.login">Login</message> <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.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> Quitter</message>
<message id="Lms.more">Plus...</message>
<message id="Lms.ok">Ok</message> <message id="Lms.ok">Ok</message>
<message id="Lms.password">Mot de passe</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-bad-login-combination">Mauvaise combinaison login / mot de passe</message>
@@ -25,6 +27,7 @@
<message id="Lms.password-old">Ancien mot de passe</message> <message id="Lms.password-old">Ancien mot de passe</message>
<message id="Lms.password-too-weak">Mot de passe trop faible</message> <message id="Lms.password-too-weak">Mot de passe trop faible</message>
<message id="Lms.passwords-dont-match">Les mots de passe ne correspondent pas</message> <message id="Lms.passwords-dont-match">Les mots de passe ne correspondent pas</message>
<message id="Lms.play-item">Lire {1}</message>
<message id="Lms.quit-other-session">Une autre session a été ouverte. Reouvrir celle-ci ?</message> <message id="Lms.quit-other-session">Une autre session a été ouverte. Reouvrir celle-ci ?</message>
<message id="Lms.save">Sauvegarder</message> <message id="Lms.save">Sauvegarder</message>
<message id="Lms.settings-saved">Paramètres sauvegardés!</message> <message id="Lms.settings-saved">Paramètres sauvegardés!</message>
@@ -145,7 +148,6 @@
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Vérification des fichiers supprimés... {1}%</message> <message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Vérification des fichiers supprimés... {1}%</message>
<message id="Lms.Admin.ScannerController.step-compact">Compactage de la base de données...</message> <message id="Lms.Admin.ScannerController.step-compact">Compactage de la base de données...</message>
<message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Calcul des statistiques... {1}%</message> <message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Calcul des statistiques... {1}%</message>
<message id="Lms.Admin.ScannerController.step-fetching-track-features">Récupération des métadonnées AcousticBrainz : {1}/{2} fichiers ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-optimize">Optimisation de la base de données... {1}%...</message> <message id="Lms.Admin.ScannerController.step-optimize">Optimisation de la base de données... {1}%...</message>
<message id="Lms.Admin.ScannerController.step-reconciliate-artists">Reconciliation des artistes: {1} entrées...</message> <message id="Lms.Admin.ScannerController.step-reconciliate-artists">Reconciliation des artistes: {1} entrées...</message>
<message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">Rechargement du moteur de recommandation : {1}%...</message> <message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">Rechargement du moteur de recommandation : {1}%...</message>
@@ -197,6 +199,7 @@
<message id="Lms.Explore.artists">Artistes</message> <message id="Lms.Explore.artists">Artistes</message>
<message id="Lms.Explore.bitrate">Bitrate</message> <message id="Lms.Explore.bitrate">Bitrate</message>
<message id="Lms.Explore.codec">Codec</message> <message id="Lms.Explore.codec">Codec</message>
<message id="Lms.Explore.cover-art">Pochette</message>
<message id="Lms.Explore.download">Télécharger</message> <message id="Lms.Explore.download">Télécharger</message>
<message id="Lms.Explore.duration">Durée</message> <message id="Lms.Explore.duration">Durée</message>
<message id="Lms.Explore.filter-added">Filtre ajouté</message> <message id="Lms.Explore.filter-added">Filtre ajouté</message>
@@ -220,6 +223,7 @@
<message id="Lms.Explore.releases">Albums</message> <message id="Lms.Explore.releases">Albums</message>
<message id="Lms.Explore.search">Rechercher</message> <message id="Lms.Explore.search">Rechercher</message>
<message id="Lms.Explore.star">Ajouter aux favoris</message> <message id="Lms.Explore.star">Ajouter aux favoris</message>
<message id="Lms.Explore.star-item">Ajouter {1} aux favoris</message>
<message id="Lms.Explore.starred">Favoris</message> <message id="Lms.Explore.starred">Favoris</message>
<message id="Lms.Explore.track-info">Informations sur la piste</message> <message id="Lms.Explore.track-info">Informations sur la piste</message>
<message id="Lms.Explore.track-lyrics">Paroles</message> <message id="Lms.Explore.track-lyrics">Paroles</message>
@@ -236,43 +240,43 @@
<message id="Lms.Explore.Artist.similar-artists">Artistes similaires</message> <message id="Lms.Explore.Artist.similar-artists">Artistes similaires</message>
<!--Explore:Artists--> <!--Explore:Artists-->
<message id="Lms.Explore.Artists.linktype-all">Tous les artistes</message> <message id="Lms.Explore.Artists.all">Tous les artistes</message>
<message id="Lms.Explore.Artists.linktype-artist"> <message id="Lms.Explore.Artists.composer">
<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="0">Compositeur</plural>
<plural case="1">Compositeurs</plural> <plural case="1">Compositeurs</plural>
</message> </message>
<message id="Lms.Explore.Artists.linktype-conductor"> <message id="Lms.Explore.Artists.conductor">
<plural case="0">Chef d'orchestre</plural> <plural case="0">Chef d'orchestre</plural>
<plural case="1">Chefs d'orchestre</plural> <plural case="1">Chefs d'orchestre</plural>
</message> </message>
<message id="Lms.Explore.Artists.linktype-lyricist"> <message id="Lms.Explore.Artists.lyricist">
<plural case="0">Parolier</plural> <plural case="0">Parolier</plural>
<plural case="1">Paroliers</plural> <plural case="1">Paroliers</plural>
</message> </message>
<message id="Lms.Explore.Artists.linktype-mixer"> <message id="Lms.Explore.Artists.mixer">
<plural case="0">Mixeur</plural> <plural case="0">Mixeur</plural>
<plural case="1">Mixeurs</plural> <plural case="1">Mixeurs</plural>
</message> </message>
<message id="Lms.Explore.Artists.linktype-performer"> <message id="Lms.Explore.Artists.performer">
<plural case="0">Interprète</plural> <plural case="0">Interprète</plural>
<plural case="1">Interprètes</plural> <plural case="1">Interprètes</plural>
</message> </message>
<message id="Lms.Explore.Artists.linktype-producer"> <message id="Lms.Explore.Artists.producer">
<plural case="0">Producteur</plural> <plural case="0">Producteur</plural>
<plural case="1">Producteurs</plural> <plural case="1">Producteurs</plural>
</message> </message>
<message id="Lms.Explore.Artists.linktype-releaseartist"> <message id="Lms.Explore.Artists.release-artist">
<plural case="0">Artiste d'album</plural> <plural case="0">Artiste d'album</plural>
<plural case="1">Artistes d'album</plural> <plural case="1">Artistes d'album</plural>
</message> </message>
<message id="Lms.Explore.Artists.linktype-remixer"> <message id="Lms.Explore.Artists.remixer">
<plural case="0">Remixeur</plural> <plural case="0">Remixeur</plural>
<plural case="1">Remixeurs</plural> <plural case="1">Remixeurs</plural>
</message> </message>
<message id="Lms.Explore.Artists.track-artist">
<plural case="0">Artiste de piste</plural>
<plural case="1">Artistes de piste</plural>
</message>
<!--Explore:Release--> <!--Explore:Release-->
<message id="Lms.Explore.Release.copyright">Copyright</message> <message id="Lms.Explore.Release.copyright">Copyright</message>
@@ -308,6 +312,12 @@
<!--Player--> <!--Player-->
<message id="Lms.Player.transcoding-active">Transcodage actif</message> <message id="Lms.Player.transcoding-active">Transcodage actif</message>
<message id="Lms.Player.now-playing">En cours de lecture</message>
<message id="Lms.Player.previous">Précédent</message>
<message id="Lms.Player.play-pause">Lecture / pause</message>
<message id="Lms.Player.next">Suivant</message>
<message id="Lms.Player.seek">Navigation</message>
<message id="Lms.Player.volume">Volume</message>
<!--Playqueue--> <!--Playqueue-->
<message id="Lms.PlayQueue.clear">Effacer</message> <message id="Lms.PlayQueue.clear">Effacer</message>
+41 -31
View File
@@ -9,12 +9,14 @@
<message id="Lms.clusters">Tags</message> <message id="Lms.clusters">Tags</message>
<message id="Lms.create">Crea</message> <message id="Lms.create">Crea</message>
<message id="Lms.delete">Elimina</message> <message id="Lms.delete">Elimina</message>
<message id="Lms.delete-item">Elimina {1}</message>
<message id="Lms.discard">Annulla</message> <message id="Lms.discard">Annulla</message>
<message id="Lms.edit">Modifica</message> <message id="Lms.edit">Modifica</message>
<message id="Lms.field-must-be-in-upper-case">Questo campo deve essere in maiuscolo</message> <message id="Lms.field-must-be-in-upper-case">Questo campo deve essere in maiuscolo</message>
<message id="Lms.loading">Caricamento...</message> <message id="Lms.loading">Caricamento...</message>
<message id="Lms.login">Login</message> <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.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> Logout</message>
<message id="Lms.more">Altro...</message>
<message id="Lms.ok">Ok</message> <message id="Lms.ok">Ok</message>
<message id="Lms.password">Password</message> <message id="Lms.password">Password</message>
<message id="Lms.password-bad-login-combination">Errata combinazione di Login / Password </message> <message id="Lms.password-bad-login-combination">Errata combinazione di Login / Password </message>
@@ -25,12 +27,13 @@
<message id="Lms.password-old">Vecchia password</message> <message id="Lms.password-old">Vecchia password</message>
<message id="Lms.password-too-weak">La password è troppo debole</message> <message id="Lms.password-too-weak">La password è troppo debole</message>
<message id="Lms.passwords-dont-match">Le password non corrispondono</message> <message id="Lms.passwords-dont-match">Le password non corrispondono</message>
<message id="Lms.play-item">Riproduci {1}</message>
<message id="Lms.quit-other-session">Un'altra sessione è stata aperta. Vuoi riaprire questa?</message> <message id="Lms.quit-other-session">Un'altra sessione è stata aperta. Vuoi riaprire questa?</message>
<message id="Lms.save">Salva</message> <message id="Lms.save">Salva</message>
<message id="Lms.settings-saved">Impostazioni salvate!</message> <message id="Lms.settings-saved">Impostazioni salvate!</message>
<message id="Lms.track-count"> <message id="Lms.track-count">
<plural case="0">{1} traccia</plural> <plural case="0">{1} traccia</plural>
<plural case="1">{1} tracce</plural> <plural case="1">{1} tracce</plural>
</message> </message>
<message id="Lms.user">Utente</message> <message id="Lms.user">Utente</message>
<message id="Lms.uuid-invalid">UUID non valido</message> <message id="Lms.uuid-invalid">UUID non valido</message>
@@ -145,7 +148,6 @@
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Controllo file... {1}%</message> <message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Controllo file... {1}%</message>
<message id="Lms.Admin.ScannerController.step-compact">Compattazione del database...</message> <message id="Lms.Admin.ScannerController.step-compact">Compattazione del database...</message>
<message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Calcolo statistiche... {1}%</message> <message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Calcolo statistiche... {1}%</message>
<message id="Lms.Admin.ScannerController.step-fetching-track-features">Recupero metadati da AcousticBrainz: {1}/{2} tracce ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-optimize">Ottimizzazione del database... {1}%...</message> <message id="Lms.Admin.ScannerController.step-optimize">Ottimizzazione del database... {1}%...</message>
<message id="Lms.Admin.ScannerController.step-reconciliate-artists">Riconciliazione artisti: {1} voci...</message> <message id="Lms.Admin.ScannerController.step-reconciliate-artists">Riconciliazione artisti: {1} voci...</message>
<message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">Ricarica motore di tracce simili: {1}%...</message> <message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">Ricarica motore di tracce simili: {1}%...</message>
@@ -197,6 +199,7 @@
<message id="Lms.Explore.artists">Artisti</message> <message id="Lms.Explore.artists">Artisti</message>
<message id="Lms.Explore.bitrate">Bitrate</message> <message id="Lms.Explore.bitrate">Bitrate</message>
<message id="Lms.Explore.codec">Codec</message> <message id="Lms.Explore.codec">Codec</message>
<message id="Lms.Explore.cover-art">Copertina</message>
<message id="Lms.Explore.download">Download</message> <message id="Lms.Explore.download">Download</message>
<message id="Lms.Explore.duration">Durata</message> <message id="Lms.Explore.duration">Durata</message>
<message id="Lms.Explore.filter-added">Filtro aggiunto</message> <message id="Lms.Explore.filter-added">Filtro aggiunto</message>
@@ -220,6 +223,7 @@
<message id="Lms.Explore.releases">Album</message> <message id="Lms.Explore.releases">Album</message>
<message id="Lms.Explore.search">Ricerca</message> <message id="Lms.Explore.search">Ricerca</message>
<message id="Lms.Explore.star">Aggiungi ai preferiti</message> <message id="Lms.Explore.star">Aggiungi ai preferiti</message>
<message id="Lms.Explore.star-item">Aggiungi {1} ai preferiti</message>
<message id="Lms.Explore.starred">Preferiti</message> <message id="Lms.Explore.starred">Preferiti</message>
<message id="Lms.Explore.track-info">Informazioni sulla traccia</message> <message id="Lms.Explore.track-info">Informazioni sulla traccia</message>
<message id="Lms.Explore.track-lyrics">Testo</message> <message id="Lms.Explore.track-lyrics">Testo</message>
@@ -236,42 +240,42 @@
<message id="Lms.Explore.Artist.similar-artists">Artisti simili</message> <message id="Lms.Explore.Artist.similar-artists">Artisti simili</message>
<!--Explore:Artists--> <!--Explore:Artists-->
<message id="Lms.Explore.Artists.linktype-all">Tutti gli artisti</message> <message id="Lms.Explore.Artists.all">Tutti gli artisti</message>
<message id="Lms.Explore.Artists.linktype-artist"> <message id="Lms.Explore.Artists.composer">
<plural case="0">Artista della traccia</plural> <plural case="0">Compositore</plural>
<plural case="1">Artisti della traccia</plural> <plural case="1">Compositori</plural>
</message> </message>
<message id="Lms.Explore.Artists.linktype-composer"> <message id="Lms.Explore.Artists.conductor">
<plural case="0">Compositore</plural> <plural case="0">Direttore d'orchestra</plural>
<plural case="1">Compositori</plural> <plural case="1">Direttori d'orchestra</plural>
</message> </message>
<message id="Lms.Explore.Artists.linktype-conductor"> <message id="Lms.Explore.Artists.lyricist">
<plural case="0">Direttore d'orchestra</plural> <plural case="0">Paroliere</plural>
<plural case="1">Direttori d'orchestra</plural> <plural case="1">Parolieri</plural>
</message> </message>
<message id="Lms.Explore.Artists.linktype-lyricist"> <message id="Lms.Explore.Artists.mixer">
<plural case="0">Paroliere</plural> <plural case="0">Tecnico del mixaggio</plural>
<plural case="1">Parolieri</plural> <plural case="1">Tecnici del mixaggio</plural>
</message> </message>
<message id="Lms.Explore.Artists.linktype-mixer"> <message id="Lms.Explore.Artists.performer">
<plural case="0">Tecnico del mixaggio</plural> <plural case="0">Interprete</plural>
<plural case="1">Tecnici del mixaggio</plural> <plural case="1">Interpreti</plural>
</message> </message>
<message id="Lms.Explore.Artists.linktype-performer"> <message id="Lms.Explore.Artists.producer">
<plural case="0">Interprete</plural> <plural case="0">Produttore</plural>
<plural case="1">Interpreti</plural> <plural case="1">Produttori</plural>
</message> </message>
<message id="Lms.Explore.Artists.linktype-producer"> <message id="Lms.Explore.Artists.release-artist">
<plural case="0">Produttore</plural> <plural case="0">Artista dell'album</plural>
<plural case="1">Produttori</plural> <plural case="1">Artisti dell'album</plural>
</message> </message>
<message id="Lms.Explore.Artists.linktype-releaseartist"> <message id="Lms.Explore.Artists.remixer">
<plural case="0">Artista dell'album</plural> <plural case="0">Remixer</plural>
<plural case="1">Artisti dell'album</plural> <plural case="1">Remixers</plural>
</message> </message>
<message id="Lms.Explore.Artists.linktype-remixer"> <message id="Lms.Explore.Artists.track-artist">
<plural case="0">Remixer</plural> <plural case="0">Artista della traccia</plural>
<plural case="1">Remixers</plural> <plural case="1">Artisti della traccia</plural>
</message> </message>
<!--Explore:Release--> <!--Explore:Release-->
@@ -308,6 +312,12 @@
<!--Player--> <!--Player-->
<message id="Lms.Player.transcoding-active">Transcodifica attiva</message> <message id="Lms.Player.transcoding-active">Transcodifica attiva</message>
<message id="Lms.Player.now-playing">In riproduzione</message>
<message id="Lms.Player.previous">Precedente</message>
<message id="Lms.Player.play-pause">Riproduci / pausa</message>
<message id="Lms.Player.next">Successivo</message>
<message id="Lms.Player.seek">Navigazione</message>
<message id="Lms.Player.volume">Volume</message>
<!--Playqueue--> <!--Playqueue-->
<message id="Lms.PlayQueue.clear">Cancella</message> <message id="Lms.PlayQueue.clear">Cancella</message>
+25 -15
View File
@@ -9,12 +9,14 @@
<message id="Lms.clusters">Znaczniki</message> <message id="Lms.clusters">Znaczniki</message>
<message id="Lms.create">Utwórz</message> <message id="Lms.create">Utwórz</message>
<message id="Lms.delete">Usuń</message> <message id="Lms.delete">Usuń</message>
<message id="Lms.delete-item">Usuń {1}</message>
<message id="Lms.discard">Odrzuć</message> <message id="Lms.discard">Odrzuć</message>
<message id="Lms.edit">Zmień</message> <message id="Lms.edit">Zmień</message>
<message id="Lms.field-must-be-in-upper-case">Wartość musi się zaczynać wielką literą</message> <message id="Lms.field-must-be-in-upper-case">Wartość musi się zaczynać wielką literą</message>
<message id="Lms.loading">Wczytywanie...</message> <message id="Lms.loading">Wczytywanie...</message>
<message id="Lms.login">Login</message> <message id="Lms.login">Login</message>
<message id="Lms.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> Wyloguj</message> <message id="Lms.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> Wyloguj</message>
<message id="Lms.more">Więcej...</message>
<message id="Lms.ok">Ok</message> <message id="Lms.ok">Ok</message>
<message id="Lms.password">Hasło</message> <message id="Lms.password">Hasło</message>
<message id="Lms.password-bad-login-combination">Błędny login lub hasło</message> <message id="Lms.password-bad-login-combination">Błędny login lub hasło</message>
@@ -25,6 +27,7 @@
<message id="Lms.password-old">Stare hasło</message> <message id="Lms.password-old">Stare hasło</message>
<message id="Lms.password-too-weak">Hasło zbyt słabe</message> <message id="Lms.password-too-weak">Hasło zbyt słabe</message>
<message id="Lms.passwords-dont-match">Hasła się nie zgadzają</message> <message id="Lms.passwords-dont-match">Hasła się nie zgadzają</message>
<message id="Lms.play-item">Odtwórz {1}</message>
<message id="Lms.quit-other-session">Rozpoczęto inną sesję. Odtworzyć tę?</message> <message id="Lms.quit-other-session">Rozpoczęto inną sesję. Odtworzyć tę?</message>
<message id="Lms.save">Zapisz</message> <message id="Lms.save">Zapisz</message>
<message id="Lms.settings-saved">Ustawienia zapisane!</message> <message id="Lms.settings-saved">Ustawienia zapisane!</message>
@@ -158,7 +161,6 @@
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Sprawdzanie plików... {1}%</message> <message id="Lms.Admin.ScannerController.step-checking-for-removed-files">Sprawdzanie plików... {1}%</message>
<message id="Lms.Admin.ScannerController.step-compact">Prasowanie bazy danych...</message> <message id="Lms.Admin.ScannerController.step-compact">Prasowanie bazy danych...</message>
<message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Obliczanie statystyk... {1}%</message> <message id="Lms.Admin.ScannerController.step-compute-cluster-stats">Obliczanie statystyk... {1}%</message>
<message id="Lms.Admin.ScannerController.step-fetching-track-features">Pobieranie danych o ścieżce z AcousticBrainz: {1}/{2} ścieżek ({3}%)...</message>
<message id="Lms.Admin.ScannerController.step-optimize">Optymalizowanie bazy danych... {1}%...</message> <message id="Lms.Admin.ScannerController.step-optimize">Optymalizowanie bazy danych... {1}%...</message>
<message id="Lms.Admin.ScannerController.step-reconciliate-artists">Uzgodnianie artystów: {1} wpisów...</message> <message id="Lms.Admin.ScannerController.step-reconciliate-artists">Uzgodnianie artystów: {1} wpisów...</message>
<message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">Przeładowywanie silnika podobieństw: {1}%...</message> <message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">Przeładowywanie silnika podobieństw: {1}%...</message>
@@ -214,6 +216,7 @@
<message id="Lms.Explore.artists">Artyści</message> <message id="Lms.Explore.artists">Artyści</message>
<message id="Lms.Explore.bitrate">Przepływność</message> <message id="Lms.Explore.bitrate">Przepływność</message>
<message id="Lms.Explore.codec">Kodek</message> <message id="Lms.Explore.codec">Kodek</message>
<message id="Lms.Explore.cover-art">Okładka</message>
<message id="Lms.Explore.download">Pobierz</message> <message id="Lms.Explore.download">Pobierz</message>
<message id="Lms.Explore.duration">Długość</message> <message id="Lms.Explore.duration">Długość</message>
<message id="Lms.Explore.filter-added">Filtr dodany</message> <message id="Lms.Explore.filter-added">Filtr dodany</message>
@@ -237,6 +240,7 @@
<message id="Lms.Explore.releases">Albumy</message> <message id="Lms.Explore.releases">Albumy</message>
<message id="Lms.Explore.search">Szukaj</message> <message id="Lms.Explore.search">Szukaj</message>
<message id="Lms.Explore.star">Wyróżnij</message> <message id="Lms.Explore.star">Wyróżnij</message>
<message id="Lms.Explore.star-item">Dodaj {1} do ulubionych</message>
<message id="Lms.Explore.starred">Wyróżnione</message> <message id="Lms.Explore.starred">Wyróżnione</message>
<message id="Lms.Explore.track-info">Informacje o ścieżce</message> <message id="Lms.Explore.track-info">Informacje o ścieżce</message>
<message id="Lms.Explore.track-lyrics">Tekst</message> <message id="Lms.Explore.track-lyrics">Tekst</message>
@@ -253,52 +257,52 @@
<message id="Lms.Explore.Artist.similar-artists">Podobni artyści</message> <message id="Lms.Explore.Artist.similar-artists">Podobni artyści</message>
<!--Explore:Artists--> <!--Explore:Artists-->
<message id="Lms.Explore.Artists.linktype-all">Wszyscy artyści</message> <message id="Lms.Explore.Artists.all">Wszyscy artyści</message>
<message id="Lms.Explore.Artists.linktype-artist"> <message id="Lms.Explore.Artists.composer">
<plural case="0">Artysta ze ścieżki</plural>
<plural case="1">Artyści ze ścieżki</plural>
<plural case="2">Artyści ze ścieżki</plural>
</message>
<message id="Lms.Explore.Artists.linktype-composer">
<plural case="0">Kompozytor</plural> <plural case="0">Kompozytor</plural>
<plural case="1">Kompozytorzy</plural> <plural case="1">Kompozytorzy</plural>
<plural case="2">Kompozytorzy</plural> <plural case="2">Kompozytorzy</plural>
</message> </message>
<message id="Lms.Explore.Artists.linktype-conductor"> <message id="Lms.Explore.Artists.conductor">
<plural case="0">Dyrygent</plural> <plural case="0">Dyrygent</plural>
<plural case="1">Dyrygenci</plural> <plural case="1">Dyrygenci</plural>
<plural case="2">Dyrygenci</plural> <plural case="2">Dyrygenci</plural>
</message> </message>
<message id="Lms.Explore.Artists.linktype-lyricist"> <message id="Lms.Explore.Artists.lyricist">
<plural case="0">Tekściarz</plural> <plural case="0">Tekściarz</plural>
<plural case="1">Tekściarze</plural> <plural case="1">Tekściarze</plural>
<plural case="2">Tekściarze</plural> <plural case="2">Tekściarze</plural>
</message> </message>
<message id="Lms.Explore.Artists.linktype-mixer"> <message id="Lms.Explore.Artists.mixer">
<plural case="0">Realizator</plural> <plural case="0">Realizator</plural>
<plural case="1">Realizatorzy</plural> <plural case="1">Realizatorzy</plural>
<plural case="2">Realizatorzy</plural> <plural case="2">Realizatorzy</plural>
</message> </message>
<message id="Lms.Explore.Artists.linktype-performer"> <message id="Lms.Explore.Artists.performer">
<plural case="0">Wykonawca</plural> <plural case="0">Wykonawca</plural>
<plural case="1">Wykonawcy</plural> <plural case="1">Wykonawcy</plural>
<plural case="2">Wykonawcy</plural> <plural case="2">Wykonawcy</plural>
</message> </message>
<message id="Lms.Explore.Artists.linktype-producer"> <message id="Lms.Explore.Artists.producer">
<plural case="0">Producent</plural> <plural case="0">Producent</plural>
<plural case="1">Producenci</plural> <plural case="1">Producenci</plural>
<plural case="2">Producenci</plural> <plural case="2">Producenci</plural>
</message> </message>
<message id="Lms.Explore.Artists.linktype-releaseartist"> <message id="Lms.Explore.Artists.release-artist">
<plural case="0">Artysta z albumu</plural> <plural case="0">Artysta z albumu</plural>
<plural case="1">Artyści z albumu</plural> <plural case="1">Artyści z albumu</plural>
<plural case="2">Artyści z albumu</plural> <plural case="2">Artyści z albumu</plural>
</message> </message>
<message id="Lms.Explore.Artists.linktype-remixer"> <message id="Lms.Explore.Artists.remixer">
<plural case="0">Remixer</plural> <plural case="0">Remixer</plural>
<plural case="1">Remikserzy</plural> <plural case="1">Remikserzy</plural>
<plural case="2">Remikserzy</plural> <plural case="2">Remikserzy</plural>
</message> </message>
<message id="Lms.Explore.Artists.track-artist">
<plural case="0">Artysta ze ścieżki</plural>
<plural case="1">Artyści ze ścieżki</plural>
<plural case="2">Artyści ze ścieżki</plural>
</message>
<!--Explore:Release--> <!--Explore:Release-->
<message id="Lms.Explore.Release.copyright">Prawa autorskie</message> <message id="Lms.Explore.Release.copyright">Prawa autorskie</message>
@@ -334,6 +338,12 @@
<!--Player--> <!--Player-->
<message id="Lms.Player.transcoding-active">Transkodowanie włączone</message> <message id="Lms.Player.transcoding-active">Transkodowanie włączone</message>
<message id="Lms.Player.now-playing">Teraz odtwarzane</message>
<message id="Lms.Player.previous">Poprzedni</message>
<message id="Lms.Player.play-pause">Odtwarzaj / pauza</message>
<message id="Lms.Player.next">Następny</message>
<message id="Lms.Player.seek">Przewijanie</message>
<message id="Lms.Player.volume">Głośność</message>
<!--Playqueue--> <!--Playqueue-->
<message id="Lms.PlayQueue.clear">Wyczyść</message> <message id="Lms.PlayQueue.clear">Wyczyść</message>
+155 -162
View File
@@ -9,13 +9,15 @@
<message id="Lms.clusters">标签</message> <message id="Lms.clusters">标签</message>
<message id="Lms.create">新建</message> <message id="Lms.create">新建</message>
<message id="Lms.delete">删除</message> <message id="Lms.delete">删除</message>
<message id="Lms.delete-item">删除 {1}</message>
<message id="Lms.discard">撤销</message> <message id="Lms.discard">撤销</message>
<message id="Lms.edit">编辑</message> <message id="Lms.edit">编辑</message>
<message id="Lms.field-must-be-in-upper-case">此字段必须为大写</message>
<message id="Lms.loading">加载中...</message> <message id="Lms.loading">加载中...</message>
<message id="Lms.login">登陆</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.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> 注销</message>
<message id="Lms.more">更多...</message>
<message id="Lms.ok">确定</message>
<message id="Lms.password">密码</message> <message id="Lms.password">密码</message>
<message id="Lms.password-bad-login-combination">用户名或密码无效</message> <message id="Lms.password-bad-login-combination">用户名或密码无效</message>
<message id="Lms.password-client-throttled">登录已被限制,请稍后再试</message> <message id="Lms.password-client-throttled">登录已被限制,请稍后再试</message>
@@ -25,14 +27,13 @@
<message id="Lms.password-old">旧密码</message> <message id="Lms.password-old">旧密码</message>
<message id="Lms.password-too-weak">密码太弱</message> <message id="Lms.password-too-weak">密码太弱</message>
<message id="Lms.passwords-dont-match">密码不匹配</message> <message id="Lms.passwords-dont-match">密码不匹配</message>
<message id="Lms.play-item">播放 {1}</message>
<message id="Lms.quit-other-session">另一个会话已打开,重新打开这一个?</message> <message id="Lms.quit-other-session">另一个会话已打开,重新打开这一个?</message>
<message id="Lms.save">保存</message> <message id="Lms.save">保存</message>
<message id="Lms.settings-saved">设置已保存!</message>
<message id="Lms.track-count">{1} 首曲目</message>
<message id="Lms.user">用户</message> <message id="Lms.user">用户</message>
<message id="Lms.uuid-invalid">无效的 UUID</message>
<!--Locale--> <!--Locale-->
@@ -52,115 +53,117 @@
<message id="Lms.Error.user-not-found">用户未找到</message> <message id="Lms.Error.user-not-found">用户未找到</message>
<!--Administration--> <!--Administration-->
<message id="Lms.Admin.menu-about"><i class="fa fa-fw fa-question-circle" aria-hidden="true"></i> 关于</message>
<message id="Lms.Admin.menu-debug-tools"><i class="fa fa-fw fa-bug" aria-hidden="true"></i> 调试工具</message>
<message id="Lms.Admin.menu-media-libraries"><i class="fa fa-fw fa-database" aria-hidden="true"></i> 媒体库</message>
<message id="Lms.Admin.menu-scan-settings"><i class="fa fa-fw fa-cogs" aria-hidden="true"></i> 扫描设置</message>
<message id="Lms.Admin.menu-scanner"><i class="fa fa-fw fa-wrench" aria-hidden="true"></i> 扫描器</message>
<message id="Lms.Admin.menu-users"><i class="fa fa-fw fa-users" aria-hidden="true"></i> 用户</message>
<!--About--> <!--About-->
<message id="Lms.Admin.About.about">关于</message>
<message id="Lms.Admin.About.homepage">主页</message>
<message id="Lms.Admin.About.version">版本</message>
<!--MediaLibraries--> <!--MediaLibraries-->
<message id="Lms.Admin.MediaLibraries.media-libraries">音乐库</message>
<message id="Lms.Admin.MediaLibrary.create-library">创建库</message>
<message id="Lms.Admin.MediaLibrary.del-library-confirm">确定删除该库?</message>
<message id="Lms.Admin.MediaLibrary.edit-library">编辑库</message>
<message id="Lms.Admin.MediaLibrary.library-created">库已创建!</message>
<message id="Lms.Admin.MediaLibrary.library-deleted">库已删除!</message>
<message id="Lms.Admin.MediaLibrary.name">库名称</message>
<message id="Lms.Admin.MediaLibrary.name-already-exists">库名称已存在</message>
<message id="Lms.Admin.MediaLibrary.path-must-be-absolute">路径必须为绝对路径</message>
<message id="Lms.Admin.MediaLibrary.path-must-not-overlap">路径不得与其他库重叠</message>
<message id="Lms.Admin.MediaLibrary.path-must-be-existing-directory">路径必须为已存在的目录</message>
<message id="Lms.Admin.MediaLibrary.root-path">根目录</message>
<!--Scan settings--> <!--Scan settings-->
<message id="Lms.Admin.Database.allow-mbid-artist-merge">允许将没有 MBID 的艺术家合并到有 MBID 的艺术家</message>
<message id="Lms.Admin.Database.artist-image-fallback-to-release">如果没有艺术家图片,则使用专辑封面</message>
<message id="Lms.Admin.Database.artist-tag-delimiters">用于拆分艺术家标签的分隔符</message>
<message id="Lms.Admin.Database.artists-to-not-split">不使用分隔符拆分的艺术家(每行一个艺术家)</message>
<message id="Lms.Admin.Database.daily">每日</message> <message id="Lms.Admin.Database.daily">每日</message>
<message id="Lms.Admin.Database.default-tag-delimiters">用于拆分其他标签的分隔符</message>
<message id="Lms.Admin.Database.extra-tags-to-scan">额外扫描的标签</message>
<message id="Lms.Admin.Database.hourly">每小时</message> <message id="Lms.Admin.Database.hourly">每小时</message>
<message id="Lms.Admin.Database.immediate-scan">立即扫描!</message> <message id="Lms.Admin.Database.immediate-scan">立即扫描!</message>
<message id="Lms.Admin.Database.misc">其他</message>
<message id="Lms.Admin.Database.monthly">每月</message> <message id="Lms.Admin.Database.monthly">每月</message>
<message id="Lms.Admin.Database.never">从不</message> <message id="Lms.Admin.Database.never">从不</message>
<message id="Lms.Admin.Database.scan-aborted">扫描已中止!</message>
<message id="Lms.Admin.Database.scan-complete">扫描完成:总文件 {1},添加文件 {2},更新文件 {3},删除文件 {4},重复文件 {5},错误文件 {6}</message> <message id="Lms.Admin.Database.scan-complete">扫描完成:总文件 {1},添加文件 {2},更新文件 {3},删除文件 {4},重复文件 {5},错误文件 {6}</message>
<message id="Lms.Admin.Database.scan-launched">扫描已完成!</message> <message id="Lms.Admin.Database.scan-launched">扫描已完成!</message>
<message id="Lms.Admin.Database.scan-settings">扫描选项</message> <message id="Lms.Admin.Database.scan-settings">扫描选项</message>
<message id="Lms.Admin.Database.similarity-engine-type">相似度引擎</message>
<message id="Lms.Admin.Database.similarity-engine-type.clusters">基于标签</message>
<message id="Lms.Admin.Database.similarity-engine-type.none"></message>
<message id="Lms.Admin.Database.skip-single-release-playlists">跳过仅包含同一专辑曲目的播放列表</message>
<message id="Lms.Admin.Database.tag-delimiter-must-not-contain-only-spaces">标签分隔符不能仅由空格组成</message>
<message id="Lms.Admin.Database.tag-parsing">标签解析</message>
<message id="Lms.Admin.Database.update-period">更新周期</message> <message id="Lms.Admin.Database.update-period">更新周期</message>
<message id="Lms.Admin.Database.update-start-time">更新开始时间</message> <message id="Lms.Admin.Database.update-start-time">更新开始时间</message>
<message id="Lms.Admin.Database.weekly">每周</message> <message id="Lms.Admin.Database.weekly">每周</message>
<!--Scanner Controller--> <!--Scanner Controller-->
<message id="Lms.Admin.ScannerController.bad-duration">无法获得音轨时间</message> <message id="Lms.Admin.ScannerController.bad-duration">无法获得音轨时间</message>
<message id="Lms.Admin.ScannerController.bad-embedded-image">无法读取索引 {1} 处的图片:{2}</message>
<message id="Lms.Admin.ScannerController.cannot-read-artist-info-file">无法解析艺术家信息文件</message>
<message id="Lms.Admin.ScannerController.cannot-read-audio-file">无法解析文件</message> <message id="Lms.Admin.ScannerController.cannot-read-audio-file">无法解析文件</message>
<message id="Lms.Admin.ScannerController.cannot-read-file">无法读取文件({1}</message>
<message id="Lms.Admin.ScannerController.cannot-read-file">无法读取文件 ({1})</message> <message id="Lms.Admin.ScannerController.cannot-read-file">无法读取文件 ({1})</message>
<message id="Lms.Admin.ScannerController.cannot-read-lyrics-file">无法解析歌词文件</message>
<message id="Lms.Admin.ScannerController.cannot-read-playlist-file">无法解析播放列表文件</message>
<message id="Lms.Admin.ScannerController.compact">压缩数据库。<strong>注意:</strong>此过程可能需要一段时间,并且在压缩步骤期间会阻塞整个应用程序!</message>
<message id="Lms.Admin.ScannerController.duplicates-header">{1} 个重复文件:</message> <message id="Lms.Admin.ScannerController.duplicates-header">{1} 个重复文件:</message>
<message id="Lms.Admin.ScannerController.errors-header">{1} 个错误:</message> <message id="Lms.Admin.ScannerController.errors-header">{1} 个错误:</message>
<message id="Lms.Admin.ScannerController.force-optimize">强制数据库优化</message>
<message id="Lms.Admin.ScannerController.full-scan">强制扫描所有文件</message>
<message id="Lms.Admin.ScannerController.get-report">生成报告</message> <message id="Lms.Admin.ScannerController.get-report">生成报告</message>
<message id="Lms.Admin.ScannerController.last-scan">最后扫描</message> <message id="Lms.Admin.ScannerController.last-scan">最后扫描</message>
<message id="Lms.Admin.ScannerController.last-scan-not-available">不可用</message> <message id="Lms.Admin.ScannerController.last-scan-not-available">不可用</message>
<message id="Lms.Admin.ScannerController.last-scan-status">已扫描 {1} 个文件,用时 {2},在 {3} 的 {4} (UTC) - {5} 个错误,{6} 个重复项</message> <message id="Lms.Admin.ScannerController.last-scan-status">已扫描 {1} 个文件,用时 {2},在 {3} 的 {4} (UTC) - {5} 个错误,{6} 个重复项</message>
<message id="Lms.Admin.ScannerController.missing-artist-name">缺少艺术家名称</message>
<message id="Lms.Admin.ScannerController.playlist-all-pathes-missing">无法找到曲目路径</message>
<message id="Lms.Admin.ScannerController.playlist-path-missing">无法找到路径 "{1}"</message>
<message id="Lms.Admin.ScannerController.no-audio-track">没有音轨</message> <message id="Lms.Admin.ScannerController.no-audio-track">没有音轨</message>
<message id="Lms.Admin.ScannerController.same-hash">相同文件哈希值</message> <message id="Lms.Admin.ScannerController.same-hash">相同文件哈希值</message>
<message id="Lms.Admin.ScannerController.same-mbid">相同 MBID</message> <message id="Lms.Admin.ScannerController.same-mbid">相同 MBID</message>
<message id="Lms.Admin.ScannerController.scan-now">立即扫描</message> <message id="Lms.Admin.ScannerController.scan-now">立即扫描</message>
<message id="Lms.Admin.ScannerController.scan-options">扫描选项</message>
<message id="Lms.Admin.ScannerController.scanner">扫描器</message> <message id="Lms.Admin.ScannerController.scanner">扫描器</message>
<message id="Lms.Admin.ScannerController.status">状态</message> <message id="Lms.Admin.ScannerController.status">状态</message>
<message id="Lms.Admin.ScannerController.status-not-scheduled">无计划</message> <message id="Lms.Admin.ScannerController.status-not-scheduled">无计划</message>
<message id="Lms.Admin.ScannerController.status-scheduled">计划于 {2} 在 {1} (UTC)</message> <message id="Lms.Admin.ScannerController.status-scheduled">计划于 {2} 在 {1} (UTC)</message>
<message id="Lms.Admin.ScannerController.status-in-progress">扫描中: 阶段 {1}/{2}</message> <message id="Lms.Admin.ScannerController.status-in-progress">扫描中: 阶段 {1}/{2}</message>
<message id="Lms.Admin.ScannerController.step-associating-artist-images">关联艺术家图片:{1}%...</message>
<message id="Lms.Admin.ScannerController.step-associating-external-lyrics">关联外部歌词:{1}%...</message>
<message id="Lms.Admin.ScannerController.step-associating-playlist-tracks">关联播放列表曲目:{1}%...</message>
<message id="Lms.Admin.ScannerController.step-associating-release-images">关联专辑封面:{1}%...</message>
<message id="Lms.Admin.ScannerController.step-associating-track-images">关联曲目图片:{1}%...</message>
<message id="Lms.Admin.ScannerController.step-checking-for-duplicate-files">检查重复文件... {1} 个文件</message>
<message id="Lms.Admin.ScannerController.step-checking-for-removed-files">检查文件中... {1}%</message> <message id="Lms.Admin.ScannerController.step-checking-for-removed-files">检查文件中... {1}%</message>
<message id="Lms.Admin.ScannerController.step-compact">正在压缩数据库...</message>
<message id="Lms.Admin.ScannerController.step-compute-cluster-stats">正在计算统计信息... {1}%</message>
<message id="Lms.Admin.ScannerController.step-fetching-track-features">从 AcousticBrainz 获取音轨特征: {1}/{2} 音轨 ({3}%)...</message> <message id="Lms.Admin.ScannerController.step-optimize">正在优化数据库... {1}%...</message>
<message id="Lms.Admin.ScannerController.step-reconciliate-artists">整理艺术家:{1} 条记录...</message>
<message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">重载相似引擎中 {1}%...</message> <message id="Lms.Admin.ScannerController.step-reloading-similarity-engine">重载相似引擎中 {1}%...</message>
<message id="Lms.Admin.ScannerController.step-removing-orphaned-entries">移除孤立条目:{1} 条记录...</message>
<message id="Lms.Admin.ScannerController.step-scanning-files">库分析:{1} 个文件...</message>
<message id="Lms.Admin.ScannerController.step-status">当前步骤状态</message> <message id="Lms.Admin.ScannerController.step-status">当前步骤状态</message>
<message id="Lms.Admin.ScannerController.step-updating-library-fields">更新库字段:{1} 条记录</message>
<!--Debug Tools--> <!--Debug Tools-->
<message id="Lms.Admin.DebugTools.debug-tools">调试工具</message>
<!--Db--> <!--Db-->
<message id="Lms.Admin.DebugTools.Db.export-query-plans">导出查询计划</message>
<message id="Lms.Admin.DebugTools.Db.db">数据库</message>
<!--Tracing--> <!--Tracing-->
<message id="Lms.Admin.DebugTools.Tracing.export-current-buffer">导出跟踪</message>
<message id="Lms.Admin.DebugTools.Tracing.tracing">跟踪</message>
<!--Users--> <!--Users-->
<message id="Lms.Admin.Users.add">新建用户</message> <message id="Lms.Admin.Users.add">新建用户</message>
@@ -192,34 +195,36 @@
<message id="Lms.Explore.add-filter">添加过滤器</message> <message id="Lms.Explore.add-filter">添加过滤器</message>
<message id="Lms.Explore.all">全部</message> <message id="Lms.Explore.all">全部</message>
<message id="Lms.Explore.artists">歌手</message> <message id="Lms.Explore.artists">歌手</message>
<message id="Lms.Explore.bitrate">比特率</message>
<message id="Lms.Explore.codec">编码器</message>
<message id="Lms.Explore.cover-art">封面</message>
<message id="Lms.Explore.download">下载</message> <message id="Lms.Explore.download">下载</message>
<message id="Lms.Explore.duration">时长</message>
<message id="Lms.Explore.filter-added">过滤器已添加</message> <message id="Lms.Explore.filter-added">过滤器已添加</message>
<message id="Lms.Explore.filters">过滤器</message> <message id="Lms.Explore.filters">过滤器</message>
<message id="Lms.Explore.label">厂牌</message>
<message id="Lms.Explore.media-library">音乐库</message>
<message id="Lms.Explore.most-played">播放最多</message> <message id="Lms.Explore.most-played">播放最多</message>
<message id="Lms.Explore.musicbrainz-artist">MusicBrainz 歌手</message> <message id="Lms.Explore.musicbrainz-artist">MusicBrainz 歌手</message>
<message id="Lms.Explore.musicbrainz-release">MusicBrainz 专辑</message> <message id="Lms.Explore.musicbrainz-release">MusicBrainz 专辑</message>
<message id="Lms.Explore.play">播放</message> <message id="Lms.Explore.play">播放</message>
<message id="Lms.Explore.play-last">最后播放</message> <message id="Lms.Explore.play-last">最后播放</message>
<message id="Lms.Explore.play-next">下一首播放</message>
<message id="Lms.Explore.play-shuffled">随机播放</message> <message id="Lms.Explore.play-shuffled">随机播放</message>
<message id="Lms.Explore.playcount">播放次数</message>
<message id="Lms.Explore.random">随机</message> <message id="Lms.Explore.random">随机</message>
<message id="Lms.Explore.recently-added">最近添加</message> <message id="Lms.Explore.recently-added">最近添加</message>
<message id="Lms.Explore.recently-modified">最近更改</message> <message id="Lms.Explore.recently-modified">最近更改</message>
<message id="Lms.Explore.recently-played">最近播放</message> <message id="Lms.Explore.recently-played">最近播放</message>
<message id="Lms.Explore.release-info">专辑信息</message>
<message id="Lms.Explore.release-type">专辑类型</message>
<message id="Lms.Explore.releases">专辑</message> <message id="Lms.Explore.releases">专辑</message>
<message id="Lms.Explore.search">搜索</message> <message id="Lms.Explore.search">搜索</message>
<message id="Lms.Explore.star">收藏</message> <message id="Lms.Explore.star">收藏</message>
<message id="Lms.Explore.star-item">将 {1} 加入收藏</message>
<message id="Lms.Explore.starred">已收藏</message> <message id="Lms.Explore.starred">已收藏</message>
<message id="Lms.Explore.track-info">曲目信息</message>
<message id="Lms.Explore.track-lyrics">歌词</message>
<message id="Lms.Explore.tracklists">播放列表</message> <message id="Lms.Explore.tracklists">播放列表</message>
<message id="Lms.Explore.tracks">音轨</message> <message id="Lms.Explore.tracks">音轨</message>
<message id="Lms.Explore.type">类型</message> <message id="Lms.Explore.type">类型</message>
@@ -229,82 +234,61 @@
<!--Explore:Artist--> <!--Explore:Artist-->
<message id="Lms.Explore.Artist.appears-on">出现于</message> <message id="Lms.Explore.Artist.appears-on">出现于</message>
<message id="Lms.Explore.Artist.biography">简介</message>
<message id="Lms.Explore.Artist.similar-artists">相似歌手</message> <message id="Lms.Explore.Artist.similar-artists">相似歌手</message>
<!--Explore:Artists--> <!--Explore:Artists-->
<message id="Lms.Explore.Artists.linktype-all">所有歌手</message> <message id="Lms.Explore.Artists.all">所有艺术家</message>
<message id="Lms.Explore.Artists.linktype-artist">音轨歌手</message> <message id="Lms.Explore.Artists.composer">作曲者</message>
<message id="Lms.Explore.Artists.conductor">指挥</message>
<message id="Lms.Explore.Artists.lyricist">作词者</message>
<message id="Lms.Explore.Artists.mixer">混音师</message>
<message id="Lms.Explore.Artists.performer">表演者</message>
<message id="Lms.Explore.Artists.producer">制作人</message>
<message id="Lms.Explore.Artists.release-artist">专辑艺术家</message>
<message id="Lms.Explore.Artists.remixer">混音师</message>
<message id="Lms.Explore.Artists.track-artist">曲目艺术家</message>
<!--Explore:Release--> <!--Explore:Release-->
<message id="Lms.Explore.Release.copyright">版权所有</message> <message id="Lms.Explore.Release.copyright">版权所有</message>
<message id="Lms.Explore.Release.disc">唱片 {1}</message> <message id="Lms.Explore.Release.disc">唱片 {1}</message>
<message id="Lms.Explore.Release.other-versions">其他版本</message>
<message id="Lms.Explore.Release.similar-releases">相似专辑</message> <message id="Lms.Explore.Release.similar-releases">相似专辑</message>
<message id="Lms.Explore.Release.type">类型</message>
<message id="Lms.Explore.Release.type-primary-album">专辑</message>
<message id="Lms.Explore.Release.type-primary-broadcast">广播</message>
<message id="Lms.Explore.Release.type-primary-ep">EP</message>
<message id="Lms.Explore.Release.type-primary-other">其他</message>
<message id="Lms.Explore.Release.type-primary-single">单曲</message>
<message id="Lms.Explore.Release.type-secondary-audiobook">有声书</message>
<message id="Lms.Explore.Release.type-secondary-audiodrama">音频剧</message>
<message id="Lms.Explore.Release.type-secondary-compilation">精选集</message>
<message id="Lms.Explore.Release.type-secondary-demo">样本</message>
<message id="Lms.Explore.Release.type-secondary-djmix">DJ 混音</message>
<message id="Lms.Explore.Release.type-secondary-field-recording">实地录音</message>
<message id="Lms.Explore.Release.type-secondary-interview">访谈</message>
<message id="Lms.Explore.Release.type-secondary-live">现场</message>
<message id="Lms.Explore.Release.type-secondary-mixtape-street">混音带 / 街头</message>
<message id="Lms.Explore.Release.type-secondary-remix">混音</message>
<message id="Lms.Explore.Release.type-secondary-soundtrack">原声带</message>
<message id="Lms.Explore.Release.type-secondary-spokenword">朗诵</message>
<!--Explore:TrackLists--> <!--Explore:TrackLists-->
<message id="Lms.Explore.TrackLists.del-tracklist-confirm">删除播放列表?</message> <message id="Lms.Explore.TrackLists.del-tracklist-confirm">删除播放列表?</message>
<message id="Lms.Explore.TrackLists.type-owned">我的播放列表</message>
<message id="Lms.Explore.TrackLists.type-shared">共享播放列表</message>
<!--Explore:Search--> <!--Explore:Search-->
<message id="Lms.Explore.Search.search-placeholder">搜索...</message> <message id="Lms.Explore.Search.search-placeholder">搜索...</message>
<!--Player--> <!--Player-->
<message id="Lms.Player.transcoding-active">转码已启用</message> <message id="Lms.Player.transcoding-active">转码已启用</message>
<message id="Lms.Player.now-playing">正在播放</message>
<message id="Lms.Player.previous">上一首</message>
<message id="Lms.Player.play-pause">播放 / 暂停</message>
<message id="Lms.Player.next">下一首</message>
<message id="Lms.Player.seek">进度调整</message>
<message id="Lms.Player.volume">音量</message>
<!--Playqueue--> <!--Playqueue-->
<message id="Lms.PlayQueue.clear">清除</message> <message id="Lms.PlayQueue.clear">清除</message>
@@ -319,29 +303,29 @@
<message id="Lms.PlayHistory.playhistory">播放历史</message> <message id="Lms.PlayHistory.playhistory">播放历史</message>
<!--Settings--> <!--Settings-->
<message id="Lms.Settings.artist-release-sort-method">艺术家专辑排序方法</message>
<message id="Lms.Settings.audio">音频</message> <message id="Lms.Settings.audio">音频</message>
<message id="Lms.Settings.audio-settings-are-local">这些音频设置仅保存在您的本地浏览器</message> <message id="Lms.Settings.audio-settings-are-local">这些音频设置仅保存在您的本地浏览器</message>
<message id="Lms.Settings.backend.internal">内部</message>
<message id="Lms.Settings.backend.listenbrainz">ListenBrainz</message>
<message id="Lms.Settings.backend.listenbrainz-token">ListenBrainz API 令牌</message>
<message id="Lms.Settings.change-password">更改密码</message> <message id="Lms.Settings.change-password">更改密码</message>
<message id="Lms.Settings.date-asc">发行日期(升序)</message>
<message id="Lms.Settings.date-desc">发行日期(降序)</message>
<message id="Lms.Settings.default-transcoding-output-bitrate">默认转码输出比特率</message>
<message id="Lms.Settings.default-transcoding-output-format">默认转码输出格式</message>
<message id="Lms.Settings.demo-cannot-save">使用演示账号时不能保存!</message> <message id="Lms.Settings.demo-cannot-save">使用演示账号时不能保存!</message>
<message id="Lms.Settings.enable-inline-artist-relationships">在专辑中内联显示艺术家关系</message>
<message id="Lms.Settings.enable-transcoding-by-default">默认启用转码</message>
<message id="Lms.Settings.feedback">反馈</message>
<message id="Lms.Settings.inline-artist-relationships">要显示的艺术家关系类型</message>
<message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> 设置</message> <message id="Lms.Settings.menu-settings"><i class="fa fa-fw fa-cog" aria-hidden="true"></i> 设置</message>
<message id="Lms.Settings.name">专辑名称</message>
<message id="Lms.Settings.original-date-asc">原始发行日期(升序)</message>
<message id="Lms.Settings.original-date-desc">原始发行日期(降序)</message>
<message id="Lms.Settings.password-bad">无效密码</message> <message id="Lms.Settings.password-bad">无效密码</message>
<message id="Lms.Settings.password-must-fill-old-password">必须填写旧密码</message> <message id="Lms.Settings.password-must-fill-old-password">必须填写旧密码</message>
<message id="Lms.Settings.regen-token">生成</message>
<message id="Lms.Settings.replaygain-mode">ReplayGain 模式</message> <message id="Lms.Settings.replaygain-mode">ReplayGain 模式</message>
<message id="Lms.Settings.replaygain-mode.none">没有 ReplayGain</message> <message id="Lms.Settings.replaygain-mode.none">没有 ReplayGain</message>
<message id="Lms.Settings.replaygain-mode.auto">自动模式</message> <message id="Lms.Settings.replaygain-mode.auto">自动模式</message>
@@ -349,17 +333,17 @@
<message id="Lms.Settings.replaygain-mode.release">专辑</message> <message id="Lms.Settings.replaygain-mode.release">专辑</message>
<message id="Lms.Settings.replaygain-preamp">ReplayGain 放大器</message> <message id="Lms.Settings.replaygain-preamp">ReplayGain 放大器</message>
<message id="Lms.Settings.replaygain-preamp-no-rg-info">ReplayGain 放大器 (如果没有信息)</message> <message id="Lms.Settings.replaygain-preamp-no-rg-info">ReplayGain 放大器 (如果没有信息)</message>
<message id="Lms.Settings.scrobbling">记录播放</message>
<message id="Lms.Settings.services">服务</message>
<message id="Lms.Settings.settings">设置</message>
<message id="Lms.Settings.settings-saved">新设置已保存!</message> <message id="Lms.Settings.settings-saved">新设置已保存!</message>
<message id="Lms.Settings.subsonic-artist-list-mode">歌手列表模式</message> <message id="Lms.Settings.subsonic-artist-list-mode">歌手列表模式</message>
<message id="Lms.Settings.subsonic-artist-list-mode.all-artists">所有歌手</message> <message id="Lms.Settings.subsonic-artist-list-mode.all-artists">所有歌手</message>
<message id="Lms.Settings.subsonic-artist-list-mode.release-artists">专辑歌手</message> <message id="Lms.Settings.subsonic-artist-list-mode.release-artists">专辑歌手</message>
<message id="Lms.Settings.subsonic-artist-list-mode.track-artists">音轨歌手</message> <message id="Lms.Settings.subsonic-artist-list-mode.track-artists">音轨歌手</message>
<message id="Lms.Settings.subsonic-api">Subsonic 应用程序接口</message> <message id="Lms.Settings.subsonic-api">Subsonic 应用程序接口</message>
<message id="Lms.Settings.subsonic-token">OpenSubsonic API 密钥</message>
<message id="Lms.Settings.subsonic-token-usage">对于不支持 “API 密钥认证” 扩展的客户端,请将此 API 密钥用作密码</message>
<message id="Lms.Settings.transcoding">转码</message> <message id="Lms.Settings.transcoding">转码</message>
<message id="Lms.Settings.transcoding-mode">启动转码</message> <message id="Lms.Settings.transcoding-mode">启动转码</message>
<message id="Lms.Settings.transcoding-mode.always">总是</message> <message id="Lms.Settings.transcoding-mode.always">总是</message>
@@ -370,9 +354,18 @@
<message id="Lms.Settings.transcoding-output-format.mp3">MP3</message> <message id="Lms.Settings.transcoding-output-format.mp3">MP3</message>
<message id="Lms.Settings.transcoding-output-format.ogg_opus">Ogg/Opus</message> <message id="Lms.Settings.transcoding-output-format.ogg_opus">Ogg/Opus</message>
<message id="Lms.Settings.transcoding-output-format.ogg_vorbis">Ogg/Vorbis</message> <message id="Lms.Settings.transcoding-output-format.ogg_vorbis">Ogg/Vorbis</message>
<message id="Lms.Settings.user-interface">用户界面</message>
<!--Wt--> <!--Wt-->
<message id="Wt.WDateTime.LessThanASecond">不到一秒</message>
<message id="Wt.WDateTime.seconds">{1} 秒</message>
<message id="Wt.WDateTime.minutes">{1} 分钟</message>
<message id="Wt.WDateTime.hours">{1} 小时</message>
<message id="Wt.WDateTime.days">{1} 天</message>
<message id="Wt.WDateTime.weeks">{1} 周</message>
<message id="Wt.WDateTime.months">{1} 个月</message>
<message id="Wt.WDateTime.years">{1} 年</message>
<message id="Wt.WDateTime.null"></message>
<message id="Wt.WMessageBox.Yes"></message> <message id="Wt.WMessageBox.Yes"></message>
<message id="Wt.WMessageBox.No"></message> <message id="Wt.WMessageBox.No"></message>
<message id="Wt.WValidator.Invalid">此字段不能为空</message> <message id="Wt.WValidator.Invalid">此字段不能为空</message>
+1 -1
View File
@@ -18,7 +18,7 @@
${clusters class="mb-2"} ${clusters class="mb-2"}
<div class="btn-group"> <div class="btn-group">
${play-btn class="btn btn-primary"} ${play-btn class="btn btn-primary"}
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false"/> <button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false" aria-label="${tr:Lms.more}"/>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li>${play-next class="dropdown-item"}</li> <li>${play-next class="dropdown-item"}</li>
<li>${play-last class="dropdown-item"}</li> <li>${play-last class="dropdown-item"}</li>
+1 -1
View File
@@ -6,7 +6,7 @@
<div class="d-flex justify-content-between align-items-start mb-3"> <div class="d-flex justify-content-between align-items-start mb-3">
<div class="btn-group me-1"> <div class="btn-group me-1">
${play-btn class="btn btn-primary"} ${play-btn class="btn btn-primary"}
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false"/> <button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false" aria-label="${tr:Lms.more}"/>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li>${play-next class="dropdown-item"}</li> <li>${play-next class="dropdown-item"}</li>
<li>${play-last class="dropdown-item"}</li> <li>${play-last class="dropdown-item"}</li>
+1 -1
View File
@@ -7,7 +7,7 @@
<div class="d-flex justify-content-between align-items-start mb-3"> <div class="d-flex justify-content-between align-items-start mb-3">
<div class="btn-group me-1"> <div class="btn-group me-1">
${play-btn class="btn btn-primary"} ${play-btn class="btn btn-primary"}
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false"/> <button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown" aria-expanded="false" aria-label="${tr:Lms.more}"/>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li>${play-next class="dropdown-item"}</li> <li>${play-next class="dropdown-item"}</li>
<li>${play-last class="dropdown-item"}</li> <li>${play-last class="dropdown-item"}</li>
+10 -1
View File
@@ -37,13 +37,15 @@ extern "C"
#include "audio/Exception.hpp" #include "audio/Exception.hpp"
#define LMS_FFMPEG_HAS_AV_DICT_ITERATE (LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(57, 37, 100))
namespace lms::audio::ffmpeg namespace lms::audio::ffmpeg
{ {
namespace namespace
{ {
std::string averror_to_string(int error) std::string averror_to_string(int error)
{ {
std::array<char, 128> buf = { 0 }; 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.data(); return buf.data();
@@ -65,9 +67,16 @@ namespace lms::audio::ffmpeg
if (!dictionnary) if (!dictionnary)
return; return;
#if LMS_FFMPEG_HAS_AV_DICT_ITERATE
const AVDictionaryEntry* tag{ NULL }; const AVDictionaryEntry* tag{ NULL };
while ((tag = av_dict_iterate(dictionnary, tag))) while ((tag = av_dict_iterate(dictionnary, tag)))
res[core::stringUtils::stringToUpper(tag->key)] = tag->value; res[core::stringUtils::stringToUpper(tag->key)] = tag->value;
#else
AVDictionaryEntry* tag{};
while ((tag = av_dict_get(dictionnary, "", tag, AV_DICT_IGNORE_SUFFIX)))
res[core::stringUtils::stringToUpper(tag->key)] = tag->value;
#endif // LMS_FFMPEG_HAS_AV_DICT_ITERATE
} }
std::optional<core::media::Container> avdemuxerToContainerType(std::string_view name) std::optional<core::media::Container> avdemuxerToContainerType(std::string_view name)
+1 -3
View File
@@ -37,7 +37,6 @@ namespace lms::core
{ ".ape", "audio/x-monkeys-audio" }, { ".ape", "audio/x-monkeys-audio" },
{ ".dff", "audio/x-dsd-dff" }, { ".dff", "audio/x-dsd-dff" },
{ ".dsdiff", "audio/x-dsd-diff" }, { ".dsdiff", "audio/x-dsd-diff" },
{ ".dsf", "audio/x-dsd" },
{ ".dsf", "audio/x-dsd-dsf" }, { ".dsf", "audio/x-dsd-dsf" },
{ ".dts", "audio/vnd.dts" }, { ".dts", "audio/vnd.dts" },
{ ".dtshd", "audio/vnd.dts.hd" }, { ".dtshd", "audio/vnd.dts.hd" },
@@ -47,12 +46,11 @@ namespace lms::core
{ ".m4a", "audio/mp4" }, { ".m4a", "audio/mp4" },
{ ".m4b", "audio/mp4" }, { ".m4b", "audio/mp4" },
{ ".mka", "audio/x-matroska" }, { ".mka", "audio/x-matroska" },
{ ".mka", "audio/x-matroska" },
{ ".mp3", "audio/mpeg" }, { ".mp3", "audio/mpeg" },
{ ".mpc", "audio/x-musepack" }, { ".mpc", "audio/x-musepack" },
{ ".oga", "audio/ogg" }, { ".oga", "audio/ogg" },
{ ".ogg", "audio/ogg" }, { ".ogg", "audio/ogg" },
{ ".opus", "audio/opus" }, { ".opus", "audio/ogg" },
{ ".pls", "audio/x-scpls" }, { ".pls", "audio/x-scpls" },
{ ".shn", "audio/x-shn" }, { ".shn", "audio/x-shn" },
{ ".tta", "audio/x-tta" }, { ".tta", "audio/x-tta" },
+1 -1
View File
@@ -56,7 +56,7 @@ namespace lms::core::media
switch (codec) switch (codec)
{ {
case Codec::Opus: case Codec::Opus:
return "audio/opus"; return "audio/ogg; codecs=\"opus\"";
case Codec::Vorbis: case Codec::Vorbis:
return "audio/ogg; codecs=\"vorbis\""; return "audio/ogg; codecs=\"vorbis\"";
case Codec::FLAC: case Codec::FLAC:
+6
View File
@@ -36,4 +36,10 @@ namespace lms::core::utils
{ {
using Ts::operator()...; using Ts::operator()...;
}; };
template<class... Ts>
overloads(Ts...) -> overloads<Ts...>;
template<typename>
constexpr bool dependent_false_v{};
} // namespace lms::core::utils } // namespace lms::core::utils
+4 -3
View File
@@ -14,9 +14,7 @@ add_library(lmsdatabase STATIC
impl/objects/PlayQueue.cpp impl/objects/PlayQueue.cpp
impl/objects/Podcast.cpp impl/objects/Podcast.cpp
impl/objects/PodcastEpisode.cpp impl/objects/PodcastEpisode.cpp
impl/objects/TrackArtistLink.cpp impl/objects/ReleaseArtistLink.cpp
impl/objects/TrackFeatures.cpp
impl/objects/TrackList.cpp
impl/objects/RatedArtist.cpp impl/objects/RatedArtist.cpp
impl/objects/RatedRelease.cpp impl/objects/RatedRelease.cpp
impl/objects/RatedTrack.cpp impl/objects/RatedTrack.cpp
@@ -26,9 +24,12 @@ add_library(lmsdatabase STATIC
impl/objects/StarredRelease.cpp impl/objects/StarredRelease.cpp
impl/objects/StarredTrack.cpp impl/objects/StarredTrack.cpp
impl/objects/Track.cpp impl/objects/Track.cpp
impl/objects/TrackArtistLink.cpp
impl/objects/TrackBookmark.cpp impl/objects/TrackBookmark.cpp
impl/objects/TrackEmbeddedImage.cpp impl/objects/TrackEmbeddedImage.cpp
impl/objects/TrackEmbeddedImageLink.cpp impl/objects/TrackEmbeddedImageLink.cpp
impl/objects/TrackFeatures.cpp
impl/objects/TrackList.cpp
impl/objects/TrackLyrics.cpp impl/objects/TrackLyrics.cpp
impl/objects/Types.cpp impl/objects/Types.cpp
impl/objects/UIState.cpp impl/objects/UIState.cpp
+27 -1
View File
@@ -35,7 +35,7 @@ namespace lms::db
{ {
namespace namespace
{ {
static constexpr Version LMS_DATABASE_VERSION{ 102 }; static constexpr Version LMS_DATABASE_VERSION{ 103 };
} }
VersionInfo::VersionInfo() VersionInfo::VersionInfo()
@@ -1681,6 +1681,31 @@ FROM track)");
utils::executeCommand(*session.getDboSession(), "ALTER TABLE podcast_episode ADD bits_per_sample INTEGER"); utils::executeCommand(*session.getDboSession(), "ALTER TABLE podcast_episode ADD bits_per_sample INTEGER");
} }
void migrateFromV102(Session& session)
{
// New link to artists from releases
utils::executeCommand(*session.getDboSession(), R"(CREATE TABLE IF NOT EXISTS "release_artist_link" (
"id" integer primary key autoincrement,
"version" integer not null,
"artist_name" text not null,
"artist_sort_name" text not null,
"artist_mbid_matched" boolean not null,
"release_id" bigint,
"artist_id" bigint,
constraint "fk_release_artist_link_release" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred,
constraint "fk_release_artist_link_artist" foreign key ("artist_id") references "artist" ("id") on delete cascade deferrable initially deferred
))");
// Remove all artist links where the link type is TrackArtistLinkType::ReleaseArtist = 8
utils::executeCommand(*session.getDboSession(), R"(DELETE FROM track_artist_link WHERE type = 8)");
// Remove outdated UI state entries
utils::executeCommand(*session.getDboSession(), R"(DELETE FROM ui_state WHERE item = 'artists_link_type')");
// Just increment the scan version of the settings to make the next scan rescan all audio files
utils::executeCommand(*session.getDboSession(), "UPDATE scan_settings SET audio_scan_version = audio_scan_version + 1");
}
bool doDbMigration(Session& session) bool doDbMigration(Session& session)
{ {
constexpr std::string_view outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" }; constexpr std::string_view outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
@@ -1759,6 +1784,7 @@ FROM track)");
{ 99, migrateFromV99 }, { 99, migrateFromV99 },
{ 100, migrateFromV100 }, { 100, migrateFromV100 },
{ 101, migrateFromV101 }, { 101, migrateFromV101 },
{ 102, migrateFromV102 },
}; };
bool migrationPerformed{}; bool migrationPerformed{};
+6
View File
@@ -42,6 +42,7 @@
#include "database/objects/RatedRelease.hpp" #include "database/objects/RatedRelease.hpp"
#include "database/objects/RatedTrack.hpp" #include "database/objects/RatedTrack.hpp"
#include "database/objects/Release.hpp" #include "database/objects/Release.hpp"
#include "database/objects/ReleaseArtistLink.hpp"
#include "database/objects/ScanSettings.hpp" #include "database/objects/ScanSettings.hpp"
#include "database/objects/StarredArtist.hpp" #include "database/objects/StarredArtist.hpp"
#include "database/objects/StarredRelease.hpp" #include "database/objects/StarredRelease.hpp"
@@ -94,6 +95,7 @@ namespace lms::db
_session.mapClass<RatedRelease>("rated_release"); _session.mapClass<RatedRelease>("rated_release");
_session.mapClass<RatedTrack>("rated_track"); _session.mapClass<RatedTrack>("rated_track");
_session.mapClass<Release>("release"); _session.mapClass<Release>("release");
_session.mapClass<ReleaseArtistLink>("release_artist_link");
_session.mapClass<ReleaseType>("release_type"); _session.mapClass<ReleaseType>("release_type");
_session.mapClass<ScanSettings>("scan_settings"); _session.mapClass<ScanSettings>("scan_settings");
_session.mapClass<StarredArtist>("starred_artist"); _session.mapClass<StarredArtist>("starred_artist");
@@ -259,6 +261,10 @@ namespace lms::db
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_sort_name_idx ON release(sort_name)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_sort_name_idx ON release(sort_name)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_sort_name_nocase_idx ON release(sort_name COLLATE NOCASE)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_sort_name_nocase_idx ON release(sort_name COLLATE NOCASE)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_artist_link_id_idx ON release_artist_link(id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_artist_link_artist_idx ON release_artist_link(artist_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_artist_link_release_idx ON release_artist_link(release_id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_type_id_idx ON release_type(id)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_type_id_idx ON release_type(id)");
utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_type_name_idx ON release_type(name COLLATE NOCASE)"); utils::executeCommand(_session, "CREATE INDEX IF NOT EXISTS release_type_name_idx ON release_type(name COLLATE NOCASE)");
+13 -10
View File
@@ -27,9 +27,7 @@
#include "database/objects/Cluster.hpp" #include "database/objects/Cluster.hpp"
#include "database/objects/Directory.hpp" #include "database/objects/Directory.hpp"
#include "database/objects/Release.hpp" #include "database/objects/Release.hpp"
#include "database/objects/StarredArtist.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "database/objects/TrackArtistLink.hpp"
#include "database/objects/User.hpp" #include "database/objects/User.hpp"
#include "SqlQuery.hpp" #include "SqlQuery.hpp"
@@ -53,9 +51,8 @@ namespace lms::db
if (params.sortMethod == ArtistSortMethod::LastWrittenDesc if (params.sortMethod == ArtistSortMethod::LastWrittenDesc
|| params.sortMethod == ArtistSortMethod::AddedDesc || params.sortMethod == ArtistSortMethod::AddedDesc
|| params.writtenAfter.isValid() || params.writtenAfter.isValid()
|| params.linkType || params.trackArtistLinkType.has_value()
|| params.track.isValid() || params.track.isValid()
|| params.release.isValid()
|| params.filters.clusters.size() == 1 || params.filters.clusters.size() == 1
|| params.filters.codec.has_value() || params.filters.codec.has_value()
|| params.filters.mediaLibrary.isValid() || params.filters.mediaLibrary.isValid()
@@ -68,7 +65,6 @@ namespace lms::db
if (params.sortMethod == ArtistSortMethod::LastWrittenDesc if (params.sortMethod == ArtistSortMethod::LastWrittenDesc
|| params.sortMethod == ArtistSortMethod::AddedDesc || params.sortMethod == ArtistSortMethod::AddedDesc
|| params.writtenAfter.isValid() || params.writtenAfter.isValid()
|| params.release.isValid()
|| params.filters.codec.has_value() || params.filters.codec.has_value()
|| params.filters.mediaLibrary.isValid() || params.filters.mediaLibrary.isValid()
|| params.filters.label.isValid() || params.filters.label.isValid()
@@ -79,9 +75,6 @@ namespace lms::db
if (params.writtenAfter.isValid()) if (params.writtenAfter.isValid())
query.where("t.file_last_write > ?").bind(params.writtenAfter); query.where("t.file_last_write > ?").bind(params.writtenAfter);
if (params.release.isValid())
query.where("t.release_id = ?").bind(params.release);
if (params.filters.codec.has_value()) if (params.filters.codec.has_value())
query.where("t.codec = ?").bind(detail::getDbCodec(*params.filters.codec)); query.where("t.codec = ?").bind(detail::getDbCodec(*params.filters.codec));
@@ -101,8 +94,11 @@ namespace lms::db
} }
} }
if (params.linkType) if (params.releaseArtistsOnly)
query.where("+t_a_l.type = ?").bind(*params.linkType); // Exclude this since the query planner does not do a good job when db is not analyzed query.join("release_artist_link r_a_l ON r_a_l.artist_id = a.id");
if (params.trackArtistLinkType.has_value())
query.where("+t_a_l.type = ?").bind(*params.trackArtistLinkType); // Exclude this since the query planner does not do a good job when db is not analyzed
if (!params.keywords.empty()) if (!params.keywords.empty())
{ {
@@ -318,6 +314,7 @@ namespace lms::db
RangeResults<ArtistId> Artist::findOrphanIds(Session& session, std::optional<Range> range) RangeResults<ArtistId> Artist::findOrphanIds(Session& session, std::optional<Range> range)
{ {
// TODO extend with release artists
session.checkReadTransaction(); session.checkReadTransaction();
auto query{ session.getDboSession()->query<ArtistId>(R"(SELECT DISTINCT a.id FROM artist a auto query{ session.getDboSession()->query<ArtistId>(R"(SELECT DISTINCT a.id FROM artist a
WHERE NOT EXISTS ( WHERE NOT EXISTS (
@@ -327,6 +324,12 @@ WHERE NOT EXISTS (
ON t_a_l.artist_id = a.id ON t_a_l.artist_id = a.id
WHERE t.id = t_a_l.track_id WHERE t.id = t_a_l.track_id
) )
AND NOT EXISTS (
SELECT 1
FROM release r
INNER JOIN release_artist_link r_a_l
ON r_a_l.artist_id = a.id
WHERE r.id = r_a_l.release_id)
AND NOT EXISTS ( AND NOT EXISTS (
SELECT 1 SELECT 1
FROM artist_info ai FROM artist_info ai
+16 -3
View File
@@ -42,7 +42,8 @@ namespace lms::db
for (std::string_view keyword : params.keywords) for (std::string_view keyword : params.keywords)
query.where("d.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + utils::escapeForLikeKeyword(keyword) + "%"); query.where("d.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + utils::escapeForLikeKeyword(keyword) + "%");
if (params.artist.isValid() if (params.trackArtist.isValid()
|| params.releaseArtist.isValid()
|| params.release.isValid() || params.release.isValid()
|| params.medium.isValid()) || params.medium.isValid())
{ {
@@ -62,12 +63,24 @@ namespace lms::db
if (params.release.isValid()) if (params.release.isValid())
query.where("t.release_id = ?").bind(params.release); query.where("t.release_id = ?").bind(params.release);
if (params.artist.isValid()) if (params.releaseArtist.isValid())
{ {
assert(!params.trackArtist.isValid());
query.join("artist a ON a.id = r_a_l.artist_id")
.join("release_artist_link r_a_l ON r_a_l.release_id = t.release_id")
.where("a.id = ?")
.bind(params.releaseArtist);
}
if (params.trackArtist.isValid())
{
assert(!params.releaseArtist.isValid());
query.join("artist a ON a.id = t_a_l.artist_id") query.join("artist a ON a.id = t_a_l.artist_id")
.join("track_artist_link t_a_l ON t_a_l.track_id = t.id") .join("track_artist_link t_a_l ON t_a_l.track_id = t.id")
.where("a.id = ?") .where("a.id = ?")
.bind(params.artist); .bind(params.trackArtist);
if (!params.trackArtistLinkTypes.empty()) if (!params.trackArtistLinkTypes.empty())
{ {
+7 -3
View File
@@ -52,7 +52,8 @@ namespace lms::db
if (params.filters.mediaLibrary.isValid() if (params.filters.mediaLibrary.isValid()
|| params.filters.codec.has_value() || params.filters.codec.has_value()
|| params.filters.label.isValid() || params.filters.label.isValid()
|| params.filters.releaseType.isValid()) || params.filters.releaseType.isValid()
|| params.trackArtistLinkType.has_value())
{ {
query.join("track t ON t.id = t_a_l.track_id"); query.join("track t ON t.id = t_a_l.track_id");
@@ -75,8 +76,11 @@ namespace lms::db
} }
} }
if (params.linkType) if (params.releaseArtistsOnly)
query.where("t_a_l.type = ?").bind(*params.linkType); query.join("release_artist_link r_a_l ON r_a_l.artist_id = a.id");
if (params.trackArtistLinkType.has_value())
query.where("t_a_l.type = ?").bind(params.trackArtistLinkType.value());
if (!params.filters.clusters.empty()) if (!params.filters.clusters.empty())
{ {
+81 -34
View File
@@ -31,6 +31,7 @@
#include "database/objects/Directory.hpp" #include "database/objects/Directory.hpp"
#include "database/objects/MediaLibrary.hpp" #include "database/objects/MediaLibrary.hpp"
#include "database/objects/Medium.hpp" #include "database/objects/Medium.hpp"
#include "database/objects/ReleaseArtistLink.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "database/objects/TrackArtistLink.hpp" #include "database/objects/TrackArtistLink.hpp"
#include "database/objects/TrackEmbeddedImage.hpp" #include "database/objects/TrackEmbeddedImage.hpp"
@@ -72,7 +73,7 @@ namespace lms::db
|| params.sortMethod == ReleaseSortMethod::OriginalDateDesc || params.sortMethod == ReleaseSortMethod::OriginalDateDesc
|| params.writtenAfter.isValid() || params.writtenAfter.isValid()
|| params.dateRange || params.dateRange
|| params.artist.isValid() || params.trackArtist.isValid()
|| params.filters.clusters.size() == 1 || params.filters.clusters.size() == 1
|| params.filters.mediaLibrary.isValid() || params.filters.mediaLibrary.isValid()
|| params.filters.codec.has_value() || params.filters.codec.has_value()
@@ -141,13 +142,22 @@ namespace lms::db
.bind(SyncState::PendingRemove); .bind(SyncState::PendingRemove);
} }
if (params.artist.isValid() if (params.artist.isValid())
{
assert(!params.trackArtist.isValid());
query.join("release_artist_link r_a_l ON r_a_l.release_id = r.id");
query.where("r_a_l.artist_id = ?").bind(params.artist);
}
if (params.trackArtist.isValid()
|| params.sortMethod == ReleaseSortMethod::ArtistNameThenName) || params.sortMethod == ReleaseSortMethod::ArtistNameThenName)
{ {
assert(!params.artist.isValid());
query.join("track_artist_link t_a_l ON t_a_l.track_id = t.id"); query.join("track_artist_link t_a_l ON t_a_l.track_id = t.id");
if (params.artist.isValid()) if (params.trackArtist.isValid())
query.where("t_a_l.artist_id = ?").bind(params.artist); query.where("t_a_l.artist_id = ?").bind(params.trackArtist);
if (params.sortMethod == ReleaseSortMethod::ArtistNameThenName) if (params.sortMethod == ReleaseSortMethod::ArtistNameThenName)
query.join("artist a ON a.id = t_a_l.artist_id"); query.join("artist a ON a.id = t_a_l.artist_id");
@@ -168,30 +178,6 @@ namespace lms::db
} }
query.where(oss.str()); query.where(oss.str());
} }
if (!params.excludedTrackArtistLinkTypes.empty())
{
std::ostringstream oss;
oss << "r.id NOT IN (SELECT DISTINCT r.id FROM release r"
" INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id"
" INNER JOIN track t ON t.release_id = r.id"
" WHERE (t_a_l.artist_id = ? AND (";
query.bind(params.artist);
bool first{ true };
for (const TrackArtistLinkType linkType : params.excludedTrackArtistLinkTypes)
{
if (!first)
oss << " OR ";
oss << "t_a_l.type = ?";
query.bind(linkType);
first = false;
}
oss << ")))";
query.where(oss.str());
}
} }
if (params.filters.clusters.size() == 1) if (params.filters.clusters.size() == 1)
@@ -272,7 +258,7 @@ namespace lms::db
} }
template<typename ResultType> template<typename ResultType>
Wt::Dbo::Query<ResultType> createArtistQuery(Wt::Dbo::Session& session, std::string_view itemToSelect, ReleaseId releaseId, TrackArtistLinkType linkType) Wt::Dbo::Query<ResultType> createTrackArtistQuery(Wt::Dbo::Session& session, std::string_view itemToSelect, ReleaseId releaseId, TrackArtistLinkType linkType)
{ {
auto query{ session.query<ResultType>("SELECT " + std::string{ itemToSelect } + " from artist a") auto query{ session.query<ResultType>("SELECT " + std::string{ itemToSelect } + " from artist a")
.join("track_artist_link t_a_l ON t_a_l.artist_id = a.id") .join("track_artist_link t_a_l ON t_a_l.artist_id = a.id")
@@ -700,19 +686,50 @@ namespace lms::db
return res; return res;
} }
std::vector<Artist::pointer> Release::getArtists(TrackArtistLinkType linkType) const std::vector<ObjectPtr<Artist>> Release::getArtists() const
{ {
assert(session()); assert(session());
const auto query{ createArtistQuery<Wt::Dbo::ptr<Artist>>(*session(), "a", getId(), linkType) }; auto query{ session()->query<Wt::Dbo::ptr<Artist>>("SELECT a from artist a") };
query.join("release_artist_link r_a_l ON r_a_l.artist_id = a.id");
query.where("r_a_l.release_id = ?").bind(getId());
query.groupBy("a.id");
return utils::fetchQueryResults<Artist::pointer>(query); return utils::fetchQueryResults<Artist::pointer>(query);
} }
std::vector<ArtistId> Release::getArtistIds(TrackArtistLinkType linkType) const bool Release::hasArtist(ArtistId artistId) const
{ {
assert(session()); assert(session());
const auto query{ createArtistQuery<ArtistId>(*session(), "a.id", getId(), linkType) }; auto query{ session()->query<int>("SELECT COUNT(1) FROM release_artist_link r_a_l") };
query.where("r_a_l.release_id = ?").bind(getId());
query.where("r_a_l.artist_id = ?").bind(artistId);
return utils::fetchQuerySingleResult(query) > 0;
}
std::vector<Artist::pointer> Release::getTrackArtists(TrackArtistLinkType type) const
{
assert(session());
const auto query{ createTrackArtistQuery<Wt::Dbo::ptr<Artist>>(*session(), "a", getId(), type) };
return utils::fetchQueryResults<Artist::pointer>(query);
}
void Release::visitTrackArtists(TrackArtistLinkType type, std::function<void(const ObjectPtr<Artist>&)> visitor) const
{
assert(session());
const auto query{ createTrackArtistQuery<Wt::Dbo::ptr<Artist>>(*session(), "a", getId(), type) };
return utils::forEachQueryResult(query, visitor);
}
std::vector<ArtistId> Release::getTrackArtistIds(TrackArtistLinkType linkType) const
{
assert(session());
const auto query{ createTrackArtistQuery<ArtistId>(*session(), "a.id", getId(), linkType) };
return utils::fetchQueryResults(query); return utils::fetchQueryResults(query);
} }
@@ -765,6 +782,36 @@ namespace lms::db
return utils::fetchQueryResults<Medium::pointer>(query); return utils::fetchQueryResults<Medium::pointer>(query);
} }
std::vector<ObjectPtr<ReleaseArtistLink>> Release::getArtistLinks() const
{
return utils::fetchQueryResults<ReleaseArtistLink::pointer>(_releaseArtistLinks.find());
}
void Release::visitArtistLinks(const std::function<void(const ReleaseArtistLink::pointer& artistLink)>& visitor) const
{
return utils::forEachQueryResult(_releaseArtistLinks.find(), visitor);
}
void Release::visitTrackArtistLinks(TrackArtistLinkType linkType, const std::function<void(const ObjectPtr<TrackArtistLink>& artistLink)>& visitor) const
{
auto query{ session()->query<Wt::Dbo::ptr<TrackArtistLink>>("SELECT t_a_l from track_artist_link t_a_l") };
query.join("track t ON t.id = t_a_l.track_id");
query.where("t.release_id = ?").bind(getId());
query.where("t_a_l.type = ?").bind(linkType);
utils::forEachQueryResult(query, visitor);
}
void Release::clearArtistLinks()
{
_releaseArtistLinks.clear();
}
void Release::addArtistLink(const ObjectPtr<ReleaseArtistLink>& artistLink)
{
_releaseArtistLinks.insert(getDboPtr(artistLink));
}
void Release::clearLabels() void Release::clearLabels()
{ {
_labels.clear(); _labels.clear();
@@ -803,7 +850,7 @@ namespace lms::db
bool Release::hasVariousArtists() const bool Release::hasVariousArtists() const
{ {
// TODO optimize // TODO optimize
return getArtists().size() > 1; return getTrackArtists().size() > 1;
} }
std::size_t Release::getTrackCount() const std::size_t Release::getTrackCount() const
@@ -0,0 +1,157 @@
/*
* Copyright (C) 2025 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "database/objects/ReleaseArtistLink.hpp"
#include <Wt/Dbo/Impl.h>
#include "core/ILogger.hpp"
#include "database/Session.hpp"
#include "database/objects/Artist.hpp"
#include "database/objects/Release.hpp"
#include "database/objects/Track.hpp"
#include "Utils.hpp"
#include "traits/IdTypeTraits.hpp"
DBO_INSTANTIATE_TEMPLATES(lms::db::ReleaseArtistLink)
namespace lms::db
{
namespace
{
Wt::Dbo::Query<Wt::Dbo::ptr<ReleaseArtistLink>> createQuery(Session& session, const ReleaseArtistLink::FindParameters& params)
{
session.checkReadTransaction();
auto query{ session.getDboSession()->query<Wt::Dbo::ptr<ReleaseArtistLink>>("SELECT r_a_l FROM release_artist_link r_a_l") };
if (params.release.isValid())
query.where("r_a_l.release_id = ?").bind(params.release);
if (params.artist.isValid())
query.where("r_a_l.artist_id = ?").bind(params.artist);
if (params.sortMethod == ReleaseArtistLinkSortMethod::OriginalDateDesc)
query.join("track t ON t.release_id = r_a_l.release_id");
if (params.mbidMatched)
query.where("r_a_l.artist_mbid_matched = ?").bind(*params.mbidMatched);
switch (params.sortMethod)
{
case ReleaseArtistLinkSortMethod::None:
break;
case ReleaseArtistLinkSortMethod::OriginalDateDesc:
query.orderBy("COALESCE(t.original_date, t.date) DESC");
break;
}
return query;
}
} // namespace
ReleaseArtistLink::ReleaseArtistLink(const ObjectPtr<Release>& release, const ObjectPtr<Artist>& artist, bool artistMBIDMatched)
: _artistMBIDMatched{ artistMBIDMatched }
, _release{ getDboPtr(release) }
, _artist{ getDboPtr(artist) }
{
}
ReleaseArtistLink::pointer ReleaseArtistLink::create(Session& session, const ObjectPtr<Release>& release, const ObjectPtr<Artist>& artist, bool artistMBIDMatched)
{
session.checkWriteTransaction();
return session.getDboSession()->add(std::make_unique<ReleaseArtistLink>(release, artist, artistMBIDMatched));
}
std::size_t ReleaseArtistLink::getCount(Session& session)
{
session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM track_artist_link"));
}
ReleaseArtistLink::pointer ReleaseArtistLink::find(Session& session, ReleaseArtistLinkId id)
{
session.checkReadTransaction();
return utils::fetchQuerySingleResult(session.getDboSession()->query<Wt::Dbo::ptr<ReleaseArtistLink>>("SELECT r_a_l from release_artist_link r_a_l").where("r_a_l.id = ?").bind(id));
}
void ReleaseArtistLink::find(Session& session, const FindParameters& params, std::function<void(const pointer&)> func)
{
auto query{ createQuery(session, params) };
utils::forEachQueryRangeResult(query, params.range, func);
}
void ReleaseArtistLink::findArtistNameNoLongerMatch(Session& session, std::optional<Range> range, const std::function<void(const ReleaseArtistLink::pointer&)>& func)
{
session.checkReadTransaction();
auto query{ session.getDboSession()->query<Wt::Dbo::ptr<ReleaseArtistLink>>("SELECT r_a_l from release_artist_link r_a_l") };
query.join("artist a ON r_a_l.artist_id = a.id");
query.where("r_a_l.artist_mbid_matched = FALSE");
query.where("r_a_l.artist_name <> a.name");
utils::applyRange(query, range);
utils::forEachQueryResult(query, [&](const ReleaseArtistLink::pointer& link) {
func(link);
});
}
void ReleaseArtistLink::findWithArtistNameAmbiguity(Session& session, std::optional<Range> range, bool allowArtistMBIDFallback, const std::function<void(const ReleaseArtistLink::pointer&)>& func)
{
session.checkReadTransaction();
auto query{ session.getDboSession()->query<Wt::Dbo::ptr<ReleaseArtistLink>>("SELECT r_a_l from release_artist_link r_a_l") };
query.join("artist a ON r_a_l.artist_id = a.id");
query.where("r_a_l.artist_mbid_matched = FALSE");
if (!allowArtistMBIDFallback)
{
query.where("a.mbid <> ''");
}
else
{
query.where(R"(
(a.mbid <> '' AND EXISTS (SELECT 1 FROM artist a2 WHERE a2.name = a.name AND a2.mbid <> '' AND a2.mbid <> a.mbid))
OR (a.mbid = '' AND (SELECT COUNT(*) FROM artist a2 WHERE a2.name = a.name AND a2.mbid <> '') = 1))");
}
utils::applyRange(query, range);
utils::forEachQueryResult(query, [&](const ReleaseArtistLink::pointer& link) {
func(link);
});
}
void ReleaseArtistLink::setArtist(ObjectPtr<Artist> artist)
{
_artist = getDboPtr(artist);
}
void ReleaseArtistLink::setArtistName(std::string_view artistName)
{
_artistName.assign(artistName, 0, Artist::maxNameLength);
LMS_LOG_IF(DB, WARNING, artistName.size() > Artist::maxNameLength, "Artist link name too long, truncated to '" << _artistName << "'");
}
void ReleaseArtistLink::setArtistSortName(std::string_view artistSortName)
{
_artistSortName.assign(artistSortName, 0, Artist::maxNameLength);
LMS_LOG_IF(DB, WARNING, artistSortName.size() > Artist::maxNameLength, "Artist link sort name too long, truncated to '" << _artistSortName << "'");
}
} // namespace lms::db
+21
View File
@@ -728,6 +728,27 @@ namespace lms::db
return utils::fetchQueryResults<TrackArtistLink::pointer>(_trackArtistLinks.find()); return utils::fetchQueryResults<TrackArtistLink::pointer>(_trackArtistLinks.find());
} }
void Track::visitArtistLinks(const std::function<void(const ObjectPtr<TrackArtistLink>& artistLink)>& visitor) const
{
utils::forEachQueryResult(_trackArtistLinks.find(), visitor);
}
std::vector<ObjectPtr<TrackArtistLink>> Track::getArtistLinks(TrackArtistLinkType type) const
{
std::vector<ObjectPtr<TrackArtistLink>> links;
visitArtistLinks(type, [&links](const ObjectPtr<TrackArtistLink>& artistLink) { links.push_back(artistLink); });
return links;
}
void Track::visitArtistLinks(TrackArtistLinkType type, const std::function<void(const ObjectPtr<TrackArtistLink>& artistLink)>& visitor) const
{
auto query{ session()->query<Wt::Dbo::ptr<TrackArtistLink>>("SELECT t_a_l from track_artist_link t_a_l") };
query.where("t_a_l.track_id = ?").bind(getId());
query.where("t_a_l.type = ?").bind(type);
return utils::forEachQueryResult(query, visitor);
}
std::vector<std::vector<Cluster::pointer>> Track::getClusterGroups(const std::vector<ClusterTypeId>& clusterTypeIds, std::size_t size) const std::vector<std::vector<Cluster::pointer>> Track::getClusterGroups(const std::vector<ClusterTypeId>& clusterTypeIds, std::size_t size) const
{ {
assert(self()); assert(self());
@@ -88,10 +88,12 @@ namespace lms::db
{ {
session.checkWriteTransaction(); session.checkWriteTransaction();
TrackArtistLink::pointer res{ session.getDboSession()->add(std::make_unique<TrackArtistLink>(track, artist, type, subType, artistMBIDMatched)) }; return session.getDboSession()->add(std::make_unique<TrackArtistLink>(track, artist, type, subType, artistMBIDMatched));
session.getDboSession()->flush(); }
return res; TrackArtistLink::pointer TrackArtistLink::create(Session& session, const ObjectPtr<Track>& track, const ObjectPtr<Artist>& artist, TrackArtistLinkType type, bool artistMBIDMatched)
{
return create(session, track, artist, type, std::string_view{}, artistMBIDMatched);
} }
std::size_t TrackArtistLink::getCount(Session& session) std::size_t TrackArtistLink::getCount(Session& session)
@@ -101,11 +103,6 @@ namespace lms::db
return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM track_artist_link")); return utils::fetchQuerySingleResult(session.getDboSession()->query<int>("SELECT COUNT(*) FROM track_artist_link"));
} }
TrackArtistLink::pointer TrackArtistLink::create(Session& session, const ObjectPtr<Track>& track, const ObjectPtr<Artist>& artist, TrackArtistLinkType type, bool artistMBIDMatched)
{
return create(session, track, artist, type, std::string_view{}, artistMBIDMatched);
}
TrackArtistLink::pointer TrackArtistLink::find(Session& session, TrackArtistLinkId id) TrackArtistLink::pointer TrackArtistLink::find(Session& session, TrackArtistLinkId id)
{ {
session.checkReadTransaction(); session.checkReadTransaction();
@@ -125,10 +122,10 @@ namespace lms::db
}); });
} }
void TrackArtistLink::find(Session& session, const FindParameters& parameters, const std::function<void(const TrackArtistLink::pointer&)>& func) void TrackArtistLink::find(Session& session, const FindParameters& params, const std::function<void(const TrackArtistLink::pointer&)>& func)
{ {
auto query{ createQuery(session, parameters) }; auto query{ createQuery(session, params) };
utils::forEachQueryRangeResult(query, parameters.range, func); utils::forEachQueryRangeResult(query, params.range, func);
} }
core::EnumSet<TrackArtistLinkType> TrackArtistLink::findUsedTypes(Session& session, ArtistId artistId) core::EnumSet<TrackArtistLinkType> TrackArtistLink::findUsedTypes(Session& session, ArtistId artistId)
-2
View File
@@ -43,8 +43,6 @@ namespace lms::db
return "performer"; return "performer";
case TrackArtistLinkType::Producer: case TrackArtistLinkType::Producer:
return "producer"; return "producer";
case TrackArtistLinkType::ReleaseArtist:
return "albumartist";
case TrackArtistLinkType::Remixer: case TrackArtistLinkType::Remixer:
return "remixer"; return "remixer";
case TrackArtistLinkType::Writer: case TrackArtistLinkType::Writer:
@@ -31,12 +31,12 @@
#include "core/EnumSet.hpp" #include "core/EnumSet.hpp"
#include "core/UUID.hpp" #include "core/UUID.hpp"
#include "database/IdRange.hpp" #include "database/IdRange.hpp"
#include "database/Object.hpp" #include "database/Object.hpp"
#include "database/Types.hpp" #include "database/Types.hpp"
#include "database/objects/ArtistId.hpp" #include "database/objects/ArtistId.hpp"
#include "database/objects/ArtworkId.hpp" #include "database/objects/ArtworkId.hpp"
#include "database/objects/ClusterId.hpp"
#include "database/objects/Filters.hpp" #include "database/objects/Filters.hpp"
#include "database/objects/MediaLibraryId.hpp" #include "database/objects/MediaLibraryId.hpp"
#include "database/objects/ReleaseId.hpp" #include "database/objects/ReleaseId.hpp"
@@ -51,9 +51,7 @@ namespace lms::db
class ClusterType; class ClusterType;
class Release; class Release;
class Session; class Session;
class StarredArtist;
class Track; class Track;
class TrackArtistLink;
class User; class User;
class Artist final : public Object<Artist, ArtistId> class Artist final : public Object<Artist, ArtistId>
@@ -64,15 +62,15 @@ namespace lms::db
struct FindParameters struct FindParameters
{ {
Filters filters; Filters filters;
std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords (on either name field OR sort name field) std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords (on either name field OR sort name field)
std::optional<TrackArtistLinkType> linkType; // if set, only artists that have produced at least one track with this link type bool releaseArtistsOnly{}; // if set, only release artists
std::optional<TrackArtistLinkType> trackArtistLinkType; // if set, only artists that have produced at least one track with this link type
ArtistSortMethod sortMethod{ ArtistSortMethod::None }; ArtistSortMethod sortMethod{ ArtistSortMethod::None };
std::optional<Range> range; std::optional<Range> range;
Wt::WDateTime writtenAfter; Wt::WDateTime writtenAfter;
UserId starringUser; // only artists starred by this user UserId starringUser; // only artists starred by this user
std::optional<FeedbackBackend> feedbackBackend; // and for this feedback backend std::optional<FeedbackBackend> feedbackBackend; // and for this feedback backend
TrackId track; // artists involved in this track TrackId track; // artists involved in this track
ReleaseId release; // artists involved in this release
FindParameters& setFilters(const Filters& _filters) FindParameters& setFilters(const Filters& _filters)
{ {
@@ -84,9 +82,14 @@ namespace lms::db
keywords = _keywords; keywords = _keywords;
return *this; return *this;
} }
FindParameters& setLinkType(std::optional<TrackArtistLinkType> _linkType) FindParameters& setReleaseArtistsOnly(bool _releaseArtistsOnly)
{ {
linkType = _linkType; releaseArtistsOnly = _releaseArtistsOnly;
return *this;
}
FindParameters& setTrackArtistLinkType(std::optional<TrackArtistLinkType> _trackArtistLinkType)
{
trackArtistLinkType = _trackArtistLinkType;
return *this; return *this;
} }
FindParameters& setSortMethod(ArtistSortMethod _sortMethod) FindParameters& setSortMethod(ArtistSortMethod _sortMethod)
@@ -115,11 +118,6 @@ namespace lms::db
track = _track; track = _track;
return *this; return *this;
} }
FindParameters& setRelease(ReleaseId _release)
{
release = _release;
return *this;
}
}; };
Artist() = default; Artist() = default;
@@ -149,7 +147,6 @@ namespace lms::db
bool hasMBID() const; bool hasMBID() const;
ObjectPtr<Artwork> getPreferredArtwork() const; ObjectPtr<Artwork> getPreferredArtwork() const;
ArtworkId getPreferredArtworkId() const; ArtworkId getPreferredArtworkId() const;
void visitLinks(std::function<void(const ObjectPtr<TrackArtistLink>& link)> visitor) const;
// No artistLinkTypes means get them all // No artistLinkTypes means get them all
RangeResults<ArtistId> findSimilarArtistIds(core::EnumSet<TrackArtistLinkType> artistLinkTypes = {}, std::optional<Range> range = std::nullopt) const; RangeResults<ArtistId> findSimilarArtistIds(core::EnumSet<TrackArtistLinkType> artistLinkTypes = {}, std::optional<Range> range = std::nullopt) const;
@@ -172,8 +169,6 @@ namespace lms::db
Wt::Dbo::field(a, _mbid, "mbid"); Wt::Dbo::field(a, _mbid, "mbid");
Wt::Dbo::belongsTo(a, _preferredArtwork, "preferred_artwork", Wt::Dbo::OnDeleteSetNull); Wt::Dbo::belongsTo(a, _preferredArtwork, "preferred_artwork", Wt::Dbo::OnDeleteSetNull);
Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "artist");
Wt::Dbo::hasMany(a, _starredArtists, Wt::Dbo::ManyToMany, "user_starred_artists", "", Wt::Dbo::OnDeleteCascade);
} }
private: private:
@@ -187,8 +182,5 @@ namespace lms::db
std::string _mbid; // Musicbrainz Identifier std::string _mbid; // Musicbrainz Identifier
Wt::Dbo::ptr<Artwork> _preferredArtwork; Wt::Dbo::ptr<Artwork> _preferredArtwork;
Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks; // Tracks involving this artist
Wt::Dbo::collection<Wt::Dbo::ptr<StarredArtist>> _starredArtists; // starred entries for this artist
}; };
} // namespace lms::db } // namespace lms::db
@@ -53,10 +53,11 @@ namespace lms::db
{ {
std::optional<Range> range; std::optional<Range> range;
std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords
ArtistId artist; // only directoies that involve this artist ArtistId trackArtist; // only directories that involve this track artist
core::EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types
MediumId medium; // only directories that involve this medium MediumId medium; // only directories that involve this medium
ReleaseId release; // only directories that involve this release ReleaseId release; // only directories that involve this release
core::EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types ArtistId releaseArtist; // only directories that involve this release artist
DirectoryId parentDirectory; // If set, directories that have this parent DirectoryId parentDirectory; // If set, directories that have this parent
bool withNoTrack{}; // If set, directories that do not contain any track bool withNoTrack{}; // If set, directories that do not contain any track
MediaLibraryId mediaLibrary; // If set, directories in this library MediaLibraryId mediaLibrary; // If set, directories in this library
@@ -72,12 +73,18 @@ namespace lms::db
keywords = _keywords; keywords = _keywords;
return *this; return *this;
} }
FindParameters& setArtist(ArtistId _artist, core::EnumSet<TrackArtistLinkType> _trackArtistLinkTypes = {}) FindParameters& setTrackArtist(ArtistId _artist, core::EnumSet<TrackArtistLinkType> _trackArtistLinkTypes = {})
{ {
artist = _artist; trackArtist = _artist;
trackArtistLinkTypes = _trackArtistLinkTypes; trackArtistLinkTypes = _trackArtistLinkTypes;
return *this; return *this;
} }
FindParameters& setReleaseArtist(ArtistId _artist)
{
releaseArtist = _artist;
return *this;
}
FindParameters& setMedium(MediumId _medium) FindParameters& setMedium(MediumId _medium)
{ {
medium = _medium; medium = _medium;
@@ -124,11 +124,17 @@ namespace lms::db
struct ArtistStatsFindParameters : public StatsFindParameters struct ArtistStatsFindParameters : public StatsFindParameters
{ {
std::optional<TrackArtistLinkType> linkType; // if set, only artists that have produced at least one track with this link type std::optional<TrackArtistLinkType> trackArtistLinkType; // if set, only artists that have produced at least one track with this link type
bool releaseArtistsOnly{};
ArtistStatsFindParameters& setLinkType(std::optional<TrackArtistLinkType> _linkType) ArtistStatsFindParameters& setReleaseArtistsOnly(bool _releaseArtistsOnly)
{ {
linkType = _linkType; releaseArtistsOnly = _releaseArtistsOnly;
return *this;
}
ArtistStatsFindParameters& setTrackArtistLinkType(std::optional<TrackArtistLinkType> _trackArtistLinkType)
{
trackArtistLinkType = _trackArtistLinkType;
return *this; return *this;
} }
}; };
@@ -56,8 +56,10 @@ namespace lms::db
class ClusterType; class ClusterType;
class Medium; class Medium;
class Release; class Release;
class ReleaseArtistLink;
class Session; class Session;
class Track; class Track;
class TrackArtistLink;
class User; class User;
class Country final : public Object<Country, CountryId> class Country final : public Object<Country, CountryId>
@@ -167,15 +169,15 @@ namespace lms::db
std::optional<Range> range; std::optional<Range> range;
Wt::WDateTime writtenAfter; Wt::WDateTime writtenAfter;
std::optional<YearRange> dateRange; std::optional<YearRange> dateRange;
UserId starringUser; // only releases starred by this user UserId starringUser; // only releases starred by this user
std::optional<FeedbackBackend> feedbackBackend; // and for this backend std::optional<FeedbackBackend> feedbackBackend; // and for this backend
ArtistId artist; // only releases that involved this user ArtistId artist; // only releases by this release artist
core::EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types ArtistId trackArtist; // only releases that involved this track artist
core::EnumSet<TrackArtistLinkType> excludedTrackArtistLinkTypes; // but not for these link types core::EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types, if set
std::string releaseType; // If set, releases that has this release type std::string releaseType; // If set, releases that has this release type
std::optional<core::UUID> releaseGroupMBID; // If set, releases that belong to this release group std::optional<core::UUID> releaseGroupMBID; // If set, releases that belong to this release group
DirectoryId directory; // if set, releases in this directory (cannot be set with parent directory) DirectoryId directory; // if set, releases in this directory (cannot be set with parent directory)
DirectoryId parentDirectory; // if set, releases in this parent directory (cannot be set with directory) DirectoryId parentDirectory; // if set, releases in this parent directory (cannot be set with directory)
FindParameters& setFilters(const Filters& _filters) FindParameters& setFilters(const Filters& _filters)
{ {
@@ -218,11 +220,15 @@ namespace lms::db
feedbackBackend = _feedbackBackend; feedbackBackend = _feedbackBackend;
return *this; return *this;
} }
FindParameters& setArtist(ArtistId _artist, core::EnumSet<TrackArtistLinkType> _trackArtistLinkTypes = {}, core::EnumSet<TrackArtistLinkType> _excludedTrackArtistLinkTypes = {}) FindParameters& setArtist(ArtistId _artist)
{ {
artist = _artist; artist = _artist;
return *this;
}
FindParameters& setTrackArtist(ArtistId _trackArtist, core::EnumSet<TrackArtistLinkType> _trackArtistLinkTypes = {})
{
trackArtist = _trackArtist;
trackArtistLinkTypes = _trackArtistLinkTypes; trackArtistLinkTypes = _trackArtistLinkTypes;
excludedTrackArtistLinkTypes = _excludedTrackArtistLinkTypes;
return *this; return *this;
} }
FindParameters& setReleaseType(std::string_view _releaseType) FindParameters& setReleaseType(std::string_view _releaseType)
@@ -305,6 +311,9 @@ namespace lms::db
ObjectPtr<Artwork> getPreferredArtwork() const; ObjectPtr<Artwork> getPreferredArtwork() const;
ArtworkId getPreferredArtworkId() const; ArtworkId getPreferredArtworkId() const;
std::vector<ObjectPtr<Medium>> getMediums() const; std::vector<ObjectPtr<Medium>> getMediums() const;
std::vector<ObjectPtr<ReleaseArtistLink>> getArtistLinks() const;
void visitArtistLinks(const std::function<void(const ObjectPtr<ReleaseArtistLink>& artistLink)>& visitor) const;
void visitTrackArtistLinks(TrackArtistLinkType linkType, const std::function<void(const ObjectPtr<TrackArtistLink>& artistLink)>& visitor) const;
// Setters // Setters
void setName(std::string_view name) { _name = name; } void setName(std::string_view name) { _name = name; }
@@ -313,6 +322,8 @@ namespace lms::db
void setGroupMBID(const std::optional<core::UUID>& mbid) { _groupMBID = mbid ? mbid->getAsString() : ""; } void setGroupMBID(const std::optional<core::UUID>& mbid) { _groupMBID = mbid ? mbid->getAsString() : ""; }
void setTotalDisc(std::optional<int> totalDisc) { _totalDisc = totalDisc; } void setTotalDisc(std::optional<int> totalDisc) { _totalDisc = totalDisc; }
void setArtistDisplayName(std::string_view name) { _artistDisplayName = name; } void setArtistDisplayName(std::string_view name) { _artistDisplayName = name; }
void clearArtistLinks();
void addArtistLink(const ObjectPtr<ReleaseArtistLink>& artistLink);
void setCompilation(bool value) { _isCompilation = value; } void setCompilation(bool value) { _isCompilation = value; }
void clearLabels(); void clearLabels();
void clearCountries(); void clearCountries();
@@ -325,9 +336,11 @@ namespace lms::db
void setPreferredArtwork(ObjectPtr<Artwork> artwork); void setPreferredArtwork(ObjectPtr<Artwork> artwork);
// Get the artists of this release // Get the artists of this release
std::vector<ObjectPtr<Artist>> getArtists(TrackArtistLinkType type = TrackArtistLinkType::Artist) const; std::vector<ObjectPtr<Artist>> getArtists() const;
std::vector<ArtistId> getArtistIds(TrackArtistLinkType type = TrackArtistLinkType::Artist) const; bool hasArtist(ArtistId artistId) const;
std::vector<ObjectPtr<Artist>> getReleaseArtists() const { return getArtists(TrackArtistLinkType::ReleaseArtist); } std::vector<ObjectPtr<Artist>> getTrackArtists(TrackArtistLinkType type = TrackArtistLinkType::Artist) const;
void visitTrackArtists(TrackArtistLinkType type, std::function<void(const ObjectPtr<Artist>&)> visitor) const;
std::vector<ArtistId> getTrackArtistIds(TrackArtistLinkType type = TrackArtistLinkType::Artist) const;
bool hasVariousArtists() const; bool hasVariousArtists() const;
std::vector<pointer> getSimilarReleases(std::optional<std::size_t> offset = {}, std::optional<std::size_t> count = {}) const; std::vector<pointer> getSimilarReleases(std::optional<std::size_t> offset = {}, std::optional<std::size_t> count = {}) const;
@@ -345,6 +358,7 @@ namespace lms::db
Wt::Dbo::field(a, _comment, "comment"); Wt::Dbo::field(a, _comment, "comment");
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToOne, "release"); Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToOne, "release");
Wt::Dbo::hasMany(a, _releaseArtistLinks, Wt::Dbo::ManyToOne, "release");
Wt::Dbo::belongsTo(a, _preferredArtwork, "preferred_artwork", Wt::Dbo::OnDeleteSetNull); Wt::Dbo::belongsTo(a, _preferredArtwork, "preferred_artwork", Wt::Dbo::OnDeleteSetNull);
Wt::Dbo::hasMany(a, _labels, Wt::Dbo::ManyToMany, "release_label", "", Wt::Dbo::OnDeleteCascade); Wt::Dbo::hasMany(a, _labels, Wt::Dbo::ManyToMany, "release_label", "", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::hasMany(a, _releaseTypes, Wt::Dbo::ManyToMany, "release_release_type", "", Wt::Dbo::OnDeleteCascade); Wt::Dbo::hasMany(a, _releaseTypes, Wt::Dbo::ManyToMany, "release_release_type", "", Wt::Dbo::OnDeleteCascade);
@@ -372,6 +386,7 @@ namespace lms::db
std::string _comment; std::string _comment;
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> _tracks; Wt::Dbo::collection<Wt::Dbo::ptr<Track>> _tracks;
Wt::Dbo::collection<Wt::Dbo::ptr<ReleaseArtistLink>> _releaseArtistLinks;
Wt::Dbo::ptr<Artwork> _preferredArtwork; Wt::Dbo::ptr<Artwork> _preferredArtwork;
Wt::Dbo::collection<Wt::Dbo::ptr<Label>> _labels; Wt::Dbo::collection<Wt::Dbo::ptr<Label>> _labels;
Wt::Dbo::collection<Wt::Dbo::ptr<ReleaseType>> _releaseTypes; Wt::Dbo::collection<Wt::Dbo::ptr<ReleaseType>> _releaseTypes;
@@ -0,0 +1,127 @@
/*
* Copyright (C) 2025 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <string>
#include <string_view>
#include <Wt/Dbo/Field.h>
#include "database/IdType.hpp"
#include "database/Object.hpp"
#include "database/Types.hpp"
#include "database/objects/ArtistId.hpp"
#include "database/objects/ReleaseId.hpp"
#include "database/objects/Types.hpp"
LMS_DECLARE_IDTYPE(ReleaseArtistLinkId)
namespace lms::db
{
class Artist;
class Session;
class Release;
class ReleaseArtistLink final : public Object<ReleaseArtistLink, ReleaseArtistLinkId>
{
public:
struct FindParameters
{
std::optional<Range> range;
ArtistId artist; // if set, links involved with this artist
ReleaseId release; // if set, artists involved in this release
std::optional<bool> mbidMatched;
ReleaseArtistLinkSortMethod sortMethod{ ReleaseArtistLinkSortMethod::None };
FindParameters& setRange(std::optional<Range> _range)
{
range = _range;
return *this;
}
FindParameters& setArtist(ArtistId _artist)
{
artist = _artist;
return *this;
}
FindParameters& setRelease(ReleaseId _release)
{
release = _release;
return *this;
}
FindParameters& setMBIDMatched(std::optional<bool> _mbidMatched)
{
mbidMatched = _mbidMatched;
return *this;
}
FindParameters& setSortMethod(ReleaseArtistLinkSortMethod _method)
{
sortMethod = _method;
return *this;
}
};
ReleaseArtistLink() = default;
ReleaseArtistLink(const ObjectPtr<Release>& release, const ObjectPtr<Artist>& artist, bool artistMBIDMatched);
static pointer find(Session& session, ReleaseArtistLinkId linkId);
static void find(Session& session, const FindParameters& params, std::function<void(const pointer&)> func);
static std::size_t getCount(Session& session);
static void findArtistNameNoLongerMatch(Session& session, std::optional<Range> range, const std::function<void(const pointer&)>& func);
static void findWithArtistNameAmbiguity(Session& session, std::optional<Range> range, bool allowArtistMBIDFallback, const std::function<void(const pointer&)>& func);
// accessors
ObjectPtr<Release> getRelease() const { return _release; }
ObjectPtr<Artist> getArtist() const { return _artist; }
ArtistId getArtistId() const { return _artist.id(); }
std::string_view getArtistName() const { return _artistName; }
std::string_view getArtistSortName() const { return _artistSortName; }
bool isArtistMBIDMatched() const { return _artistMBIDMatched; }
// setters
void setArtist(ObjectPtr<Artist> artist);
void setArtistName(std::string_view artistName);
void setArtistSortName(std::string_view artistSortName);
template<class Action>
void persist(Action& a)
{
Wt::Dbo::field(a, _artistName, "artist_name");
Wt::Dbo::field(a, _artistSortName, "artist_sort_name");
Wt::Dbo::field(a, _artistMBIDMatched, "artist_mbid_matched");
Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade);
}
private:
friend class Session;
static pointer create(Session& session, const ObjectPtr<Release>& release, const ObjectPtr<Artist>& artist, bool artistMBIDMatched);
TrackArtistLinkType _type{ TrackArtistLinkType::Artist };
std::string _subType;
std::string _artistName; // as it was in the tags
std::string _artistSortName; // as it was in the tags
bool _artistMBIDMatched{};
Wt::Dbo::ptr<Release> _release;
Wt::Dbo::ptr<Artist> _artist;
};
} // namespace lms::db
@@ -307,6 +307,10 @@ namespace lms::db
std::vector<ObjectPtr<Artist>> getArtists(core::EnumSet<TrackArtistLinkType> artistLinkTypes) const; // no type means all std::vector<ObjectPtr<Artist>> getArtists(core::EnumSet<TrackArtistLinkType> artistLinkTypes) const; // no type means all
std::vector<ArtistId> getArtistIds(core::EnumSet<TrackArtistLinkType> artistLinkTypes) const; // no type means all std::vector<ArtistId> getArtistIds(core::EnumSet<TrackArtistLinkType> artistLinkTypes) const; // no type means all
std::vector<ObjectPtr<TrackArtistLink>> getArtistLinks() const; std::vector<ObjectPtr<TrackArtistLink>> getArtistLinks() const;
void visitArtistLinks(const std::function<void(const ObjectPtr<TrackArtistLink>& artistLink)>& visitor) const;
std::vector<ObjectPtr<TrackArtistLink>> getArtistLinks(TrackArtistLinkType type) const;
void visitArtistLinks(TrackArtistLinkType type, const std::function<void(const ObjectPtr<TrackArtistLink>& artistLink)>& visitor) const;
ReleaseId getReleaseId() const { return _release.id(); } ReleaseId getReleaseId() const { return _release.id(); }
ObjectPtr<Release> getRelease() const { return _release; } ObjectPtr<Release> getRelease() const { return _release; }
MediumId getMediumId() const { return _medium.id(); } MediumId getMediumId() const { return _medium.id(); }
@@ -320,7 +324,7 @@ namespace lms::db
ObjectPtr<Artwork> getPreferredMediaArtwork() const; ObjectPtr<Artwork> getPreferredMediaArtwork() const;
ArtworkId getPreferredMediaArtworkId() const; ArtworkId getPreferredMediaArtworkId() const;
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(const std::vector<ClusterTypeId>& clusterTypes, std::size_t size) const; std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(const std::vector<ClusterTypeId>& clusterTypeIds, std::size_t size) const;
template<class Action> template<class Action>
void persist(Action& a) void persist(Action& a)
@@ -97,11 +97,9 @@ namespace lms::db
TrackArtistLink(const ObjectPtr<Track>& track, const ObjectPtr<Artist>& artist, TrackArtistLinkType type, std::string_view subType, bool artistMBIDMatched); TrackArtistLink(const ObjectPtr<Track>& track, const ObjectPtr<Artist>& artist, TrackArtistLinkType type, std::string_view subType, bool artistMBIDMatched);
static void find(Session& session, TrackId trackId, const std::function<void(const pointer&, const ObjectPtr<Artist>&)>& func); static void find(Session& session, TrackId trackId, const std::function<void(const pointer&, const ObjectPtr<Artist>&)>& func);
static void find(Session& session, const FindParameters& parameters, const std::function<void(const pointer&)>& func); static void find(Session& session, const FindParameters& params, const std::function<void(const pointer&)>& func);
static pointer find(Session& session, TrackArtistLinkId linkId); static pointer find(Session& session, TrackArtistLinkId linkId);
static std::size_t getCount(Session& session); static std::size_t getCount(Session& session);
static pointer create(Session& session, const ObjectPtr<Track>& track, const ObjectPtr<Artist>& artist, TrackArtistLinkType type, std::string_view subType, bool artistMBIDMatched = false);
static pointer create(Session& session, const ObjectPtr<Track>& track, const ObjectPtr<Artist>& artist, TrackArtistLinkType type, bool artistMBIDMatched = false);
static core::EnumSet<TrackArtistLinkType> findUsedTypes(Session& session, ArtistId _artist); static core::EnumSet<TrackArtistLinkType> findUsedTypes(Session& session, ArtistId _artist);
static void findArtistNameNoLongerMatch(Session& session, std::optional<Range> range, const std::function<void(const pointer&)>& func); static void findArtistNameNoLongerMatch(Session& session, std::optional<Range> range, const std::function<void(const pointer&)>& func);
static void findWithArtistNameAmbiguity(Session& session, std::optional<Range> range, bool allowArtistMBIDFallback, const std::function<void(const pointer&)>& func); static void findWithArtistNameAmbiguity(Session& session, std::optional<Range> range, bool allowArtistMBIDFallback, const std::function<void(const pointer&)>& func);
@@ -109,6 +107,7 @@ namespace lms::db
// accessors // accessors
ObjectPtr<Track> getTrack() const { return _track; } ObjectPtr<Track> getTrack() const { return _track; }
ObjectPtr<Artist> getArtist() const { return _artist; } ObjectPtr<Artist> getArtist() const { return _artist; }
ArtistId getArtistId() const { return _artist.id(); }
TrackArtistLinkType getType() const { return _type; } TrackArtistLinkType getType() const { return _type; }
std::string_view getSubType() const { return _subType; } std::string_view getSubType() const { return _subType; }
std::string_view getArtistName() const { return _artistName; } std::string_view getArtistName() const { return _artistName; }
@@ -134,6 +133,10 @@ namespace lms::db
} }
private: private:
friend class Session;
static pointer create(Session& session, const ObjectPtr<Track>& track, const ObjectPtr<Artist>& artist, TrackArtistLinkType type, std::string_view subType, bool artistMBIDMatched = false);
static pointer create(Session& session, const ObjectPtr<Track>& track, const ObjectPtr<Artist>& artist, TrackArtistLinkType type, bool artistMBIDMatched = false);
TrackArtistLinkType _type{ TrackArtistLinkType::Artist }; TrackArtistLinkType _type{ TrackArtistLinkType::Artist };
std::string _subType; std::string _subType;
std::string _artistName; // as it was in the tags std::string _artistName; // as it was in the tags
@@ -86,6 +86,12 @@ namespace lms::db
PubDateDesc, PubDateDesc,
}; };
enum class ReleaseArtistLinkSortMethod
{
None,
OriginalDateDesc,
};
enum class ReleaseSortMethod enum class ReleaseSortMethod
{ {
None, None,
@@ -165,7 +171,7 @@ namespace lms::db
Mixer = 5, Mixer = 5,
Performer = 6, Performer = 6,
Producer = 7, Producer = 7,
ReleaseArtist = 8, // ReleaseArtist = 8,
Remixer = 9, Remixer = 9,
Writer = 10, Writer = 10,
}; };
+118 -87
View File
@@ -21,6 +21,7 @@
#include "database/objects/Artwork.hpp" #include "database/objects/Artwork.hpp"
#include "database/objects/Image.hpp" #include "database/objects/Image.hpp"
#include "database/objects/ReleaseArtistLink.hpp"
namespace lms::db::tests namespace lms::db::tests
{ {
@@ -98,10 +99,10 @@ namespace lms::db::tests
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
track2a.get().modify()->setMediaLibrary(library.get()); track2a.get().modify()->setMediaLibrary(library.get());
track2b.get().modify()->setMediaLibrary(library.get()); track2b.get().modify()->setMediaLibrary(library.get());
TrackArtistLink::create(session, track1.get(), artist1.get(), TrackArtistLinkType::Artist); session.create<TrackArtistLink>(track1.get(), artist1.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, track2a.get(), artist2.get(), TrackArtistLinkType::Artist); session.create<TrackArtistLink>(track2a.get(), artist2.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, track2b.get(), artist2.get(), TrackArtistLinkType::Artist); session.create<TrackArtistLink>(track2b.get(), artist2.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, track3.get(), artist3.get(), TrackArtistLinkType::Artist); session.create<TrackArtistLink>(track3.get(), artist3.get(), TrackArtistLinkType::Artist);
} }
{ {
@@ -243,7 +244,7 @@ namespace lms::db::tests
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
track.get().modify()->setName("MyTrackName"); track.get().modify()->setName("MyTrackName");
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); session.create<TrackArtistLink>(track.get(), artist.get(), TrackArtistLinkType::Artist);
} }
{ {
@@ -258,13 +259,39 @@ namespace lms::db::tests
ASSERT_EQ(artists.size(), 1); ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.front()->getId(), artist.getId()); EXPECT_EQ(artists.front()->getId(), artist.getId());
{
bool visited{};
track->visitArtistLinks([&](const db::TrackArtistLink::pointer& artistLink) {
visited = true;
EXPECT_EQ(artistLink->getArtistId(), artist.getId());
});
EXPECT_TRUE(visited);
}
{
bool visited{};
track->visitArtistLinks(TrackArtistLinkType::Artist, [&](const db::TrackArtistLink::pointer& artistLink) {
visited = true;
EXPECT_EQ(artistLink->getArtistId(), artist.getId());
});
EXPECT_TRUE(visited);
}
{
bool visited{};
track->visitArtistLinks(TrackArtistLinkType::Conductor, [&](const db::TrackArtistLink::pointer&) {
visited = true;
});
EXPECT_FALSE(visited);
}
ASSERT_EQ(track->getArtistLinks().size(), 1); ASSERT_EQ(track->getArtistLinks().size(), 1);
auto artistLink{ track->getArtistLinks().front() }; auto artistLink{ track->getArtistLinks().front() };
EXPECT_EQ(artistLink->getTrack()->getId(), track.getId()); EXPECT_EQ(artistLink->getTrack()->getId(), track.getId());
EXPECT_EQ(artistLink->getArtist()->getId(), artist.getId()); EXPECT_EQ(artistLink->getArtist()->getId(), artist.getId());
ASSERT_EQ(track->getArtists({ TrackArtistLinkType::Artist }).size(), 1); ASSERT_EQ(track->getArtists({ TrackArtistLinkType::Artist }).size(), 1);
EXPECT_EQ(track->getArtists({ TrackArtistLinkType::ReleaseArtist }).size(), 0); EXPECT_EQ(track->getArtists({ TrackArtistLinkType::Composer }).size(), 0);
EXPECT_EQ(track->getArtists({}).size(), 1); EXPECT_EQ(track->getArtists({}).size(), 1);
} }
@@ -276,7 +303,7 @@ namespace lms::db::tests
EXPECT_EQ(artists.front(), artist.getId()); EXPECT_EQ(artists.front(), artist.getId());
ASSERT_EQ(track->getArtistIds({ TrackArtistLinkType::Artist }).size(), 1); ASSERT_EQ(track->getArtistIds({ TrackArtistLinkType::Artist }).size(), 1);
EXPECT_EQ(track->getArtistIds({ TrackArtistLinkType::ReleaseArtist }).size(), 0); EXPECT_EQ(track->getArtistIds({ TrackArtistLinkType::Composer }).size(), 0);
EXPECT_EQ(track->getArtistIds({}).size(), 1); EXPECT_EQ(track->getArtistIds({}).size(), 1);
} }
@@ -302,6 +329,32 @@ namespace lms::db::tests
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId()); EXPECT_EQ(artists.results.front(), artist.getId());
} }
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(Artist::find(session, Artist::FindParameters{}.setReleaseArtistsOnly(true)).results.size(), 0);
}
}
TEST_F(DatabaseFixture, Artist_singleRelease)
{
ScopedRelease release{ session, "MyRelease" };
ScopedArtist artist{ session, "MyArtist" };
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(Artist::findOrphanIds(session).results, std::vector<db::ArtistId>{ artist.getId() });
}
{
auto transaction{ session.createWriteTransaction() };
session.create<ReleaseArtistLink>(release.get(), artist.get(), false);
}
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(Artist::findOrphanIds(session).results.size(), 0);
}
} }
TEST_F(DatabaseFixture, Artist_singleTrack_mediaLibrary) TEST_F(DatabaseFixture, Artist_singleTrack_mediaLibrary)
@@ -315,7 +368,7 @@ namespace lms::db::tests
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
track.get().modify()->setName("MyTrackName"); track.get().modify()->setName("MyTrackName");
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); session.create<TrackArtistLink>(track.get(), artist.get(), TrackArtistLinkType::Artist);
track.get().modify()->setMediaLibrary(library.get()); track.get().modify()->setMediaLibrary(library.get());
} }
{ {
@@ -344,9 +397,9 @@ namespace lms::db::tests
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); session.create<TrackArtistLink>(track.get(), artist.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::ReleaseArtist); session.create<TrackArtistLink>(track.get(), artist.get(), TrackArtistLinkType::Producer);
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Writer); session.create<TrackArtistLink>(track.get(), artist.get(), TrackArtistLinkType::Writer);
} }
{ {
@@ -357,10 +410,12 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}).results.size(), 1); EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}).results.size(), 1);
EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setLinkType(TrackArtistLinkType::Artist)).results.size(), 1); EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setTrackArtistLinkType(TrackArtistLinkType::Artist)).results.size(), 1);
EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setLinkType(TrackArtistLinkType::ReleaseArtist)).results.size(), 1); EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setTrackArtistLinkType(TrackArtistLinkType::Producer)).results.size(), 1);
EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setLinkType(TrackArtistLinkType::Writer)).results.size(), 1); EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setTrackArtistLinkType(TrackArtistLinkType::Writer)).results.size(), 1);
EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setLinkType(TrackArtistLinkType::Composer)).results.size(), 0); EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setTrackArtistLinkType(TrackArtistLinkType::Composer)).results.size(), 0);
EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setReleaseArtistsOnly(true)).results.size(), 0);
} }
{ {
@@ -370,7 +425,7 @@ namespace lms::db::tests
ASSERT_EQ(artists.size(), 1); ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.front()->getId(), artist.getId()); EXPECT_EQ(artists.front()->getId(), artist.getId());
artists = track->getArtists({ TrackArtistLinkType::ReleaseArtist }); artists = track->getArtists({ TrackArtistLinkType::Producer });
ASSERT_EQ(artists.size(), 1); ASSERT_EQ(artists.size(), 1);
EXPECT_EQ(artists.front()->getId(), artist.getId()); EXPECT_EQ(artists.front()->getId(), artist.getId());
@@ -379,7 +434,7 @@ namespace lms::db::tests
auto tracks{ Track::findIds(session, Track::FindParameters{}.setArtist(artist.getId())) }; auto tracks{ Track::findIds(session, Track::FindParameters{}.setArtist(artist.getId())) };
EXPECT_EQ(tracks.results.size(), 1); EXPECT_EQ(tracks.results.size(), 1);
tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::ReleaseArtist })); tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Producer }));
EXPECT_EQ(tracks.results.size(), 1); EXPECT_EQ(tracks.results.size(), 1);
tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Artist })); tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Artist }));
EXPECT_EQ(tracks.results.size(), 1); EXPECT_EQ(tracks.results.size(), 1);
@@ -393,7 +448,7 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
core::EnumSet<TrackArtistLinkType> types{ TrackArtistLink::findUsedTypes(session, artist.getId()) }; core::EnumSet<TrackArtistLinkType> types{ TrackArtistLink::findUsedTypes(session, artist.getId()) };
EXPECT_TRUE(types.contains(TrackArtistLinkType::ReleaseArtist)); EXPECT_TRUE(types.contains(TrackArtistLinkType::Producer));
EXPECT_TRUE(types.contains(TrackArtistLinkType::Artist)); EXPECT_TRUE(types.contains(TrackArtistLinkType::Artist));
EXPECT_TRUE(types.contains(TrackArtistLinkType::Writer)); EXPECT_TRUE(types.contains(TrackArtistLinkType::Writer));
EXPECT_FALSE(types.contains(TrackArtistLinkType::Composer)); EXPECT_FALSE(types.contains(TrackArtistLinkType::Composer));
@@ -416,7 +471,7 @@ namespace lms::db::tests
}; };
EXPECT_TRUE(containsType(TrackArtistLinkType::Artist)); EXPECT_TRUE(containsType(TrackArtistLinkType::Artist));
EXPECT_TRUE(containsType(TrackArtistLinkType::ReleaseArtist)); EXPECT_TRUE(containsType(TrackArtistLinkType::Producer));
EXPECT_TRUE(containsType(TrackArtistLinkType::Writer)); EXPECT_TRUE(containsType(TrackArtistLinkType::Writer));
} }
} }
@@ -431,8 +486,8 @@ namespace lms::db::tests
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
TrackArtistLink::create(session, track.get(), artist1.get(), TrackArtistLinkType::Artist); session.create<TrackArtistLink>(track.get(), artist1.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, track.get(), artist2.get(), TrackArtistLinkType::Artist); session.create<TrackArtistLink>(track.get(), artist2.get(), TrackArtistLinkType::Artist);
} }
{ {
@@ -450,7 +505,7 @@ namespace lms::db::tests
EXPECT_EQ(track->getArtists({}).size(), 2); EXPECT_EQ(track->getArtists({}).size(), 2);
EXPECT_EQ(track->getArtists({ TrackArtistLinkType::Artist }).size(), 2); EXPECT_EQ(track->getArtists({ TrackArtistLinkType::Artist }).size(), 2);
EXPECT_EQ(track->getArtists({ TrackArtistLinkType::ReleaseArtist }).size(), 0); EXPECT_EQ(track->getArtists({ TrackArtistLinkType::Arranger }).size(), 0);
EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}).results.size(), 2); EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}).results.size(), 2);
EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setSortMethod(ArtistSortMethod::Random)).results.size(), 2); EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setSortMethod(ArtistSortMethod::Random)).results.size(), 2);
} }
@@ -466,13 +521,13 @@ namespace lms::db::tests
ASSERT_EQ(tracks.results.size(), 1); ASSERT_EQ(tracks.results.size(), 1);
EXPECT_EQ(tracks.results.front(), track->getId()); EXPECT_EQ(tracks.results.front(), track->getId());
tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist1->getId(), { TrackArtistLinkType::ReleaseArtist })); tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist1->getId(), { TrackArtistLinkType::Arranger }));
EXPECT_EQ(tracks.results.size(), 0); EXPECT_EQ(tracks.results.size(), 0);
tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist1->getId(), { TrackArtistLinkType::Artist })); tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist1->getId(), { TrackArtistLinkType::Artist }));
EXPECT_EQ(tracks.results.size(), 1); EXPECT_EQ(tracks.results.size(), 1);
tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist2->getId(), { TrackArtistLinkType::ReleaseArtist })); tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist2->getId(), { TrackArtistLinkType::Arranger }));
EXPECT_EQ(tracks.results.size(), 0); EXPECT_EQ(tracks.results.size(), 0);
tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist2->getId(), { TrackArtistLinkType::Artist })); tracks = Track::findIds(session, Track::FindParameters{}.setArtist(artist2->getId(), { TrackArtistLinkType::Artist }));
@@ -518,6 +573,27 @@ namespace lms::db::tests
} }
} }
TEST_F(DatabaseFixture, Artist_findByReleaseArtistLinks)
{
ScopedRelease release{ session, "release" };
ScopedArtist artist{ session, "artist" };
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(Artist::find(session, Artist::FindParameters{}.setReleaseArtistsOnly(true)).results.size(), 0);
}
{
auto transaction{ session.createWriteTransaction() };
session.create<ReleaseArtistLink>(release.get(), artist.get(), false);
}
{
auto transaction{ session.createReadTransaction() };
EXPECT_EQ(Artist::find(session, Artist::FindParameters{}.setReleaseArtistsOnly(true)).results.size(), 1);
}
}
TEST_F(DatabaseFixture, Artist_findByCodec) TEST_F(DatabaseFixture, Artist_findByCodec)
{ {
ScopedArtist artist1{ session, "A" }; ScopedArtist artist1{ session, "A" };
@@ -527,8 +603,8 @@ namespace lms::db::tests
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
TrackArtistLink::create(session, track1.get(), artist1.get(), TrackArtistLinkType::Artist); session.create<TrackArtistLink>(track1.get(), artist1.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, track2.get(), artist2.get(), TrackArtistLinkType::Artist); session.create<TrackArtistLink>(track2.get(), artist2.get(), TrackArtistLinkType::Artist);
track1.get().modify()->setCodec(core::media::Codec::MP3); track1.get().modify()->setCodec(core::media::Codec::MP3);
track2.get().modify()->setCodec(core::media::Codec::FLAC); track2.get().modify()->setCodec(core::media::Codec::FLAC);
@@ -554,7 +630,7 @@ namespace lms::db::tests
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
artist.get().modify()->setSortName("ZZZ"); artist.get().modify()->setSortName("ZZZ");
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); session.create<TrackArtistLink>(track.get(), artist.get(), TrackArtistLinkType::Artist);
} }
{ {
@@ -768,8 +844,8 @@ namespace lms::db::tests
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
TrackArtistLink::create(session, track1.get(), artist.get(), TrackArtistLinkType::Artist); session.create<TrackArtistLink>(track1.get(), artist.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, track2.get(), artist.get(), TrackArtistLinkType::Artist); session.create<TrackArtistLink>(track2.get(), artist.get(), TrackArtistLinkType::Artist);
track1.get().modify()->setRelease(release.get()); track1.get().modify()->setRelease(release.get());
} }
@@ -783,51 +859,6 @@ namespace lms::db::tests
} }
} }
TEST_F(DatabaseFixture, Artist_findByRelease)
{
ScopedArtist artist{ session, "artist" };
ScopedTrack track1{ session };
ScopedTrack track2{ session };
ScopedRelease release{ session, "MyRelease" };
{
auto transaction{ session.createReadTransaction() };
const auto artists{ Artist::findIds(session, Artist::FindParameters{}.setRelease(release.getId())) };
EXPECT_EQ(artists.results.size(), 0);
}
{
auto transaction{ session.createWriteTransaction() };
TrackArtistLink::create(session, track1.get(), artist.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, track2.get(), artist.get(), TrackArtistLinkType::Artist);
}
{
auto transaction{ session.createReadTransaction() };
const auto artists{ Artist::findIds(session, Artist::FindParameters{}.setRelease(release.getId())) };
EXPECT_EQ(artists.results.size(), 0);
}
{
auto transaction{ session.createWriteTransaction() };
track1.get().modify()->setRelease(release.get());
track2.get().modify()->setRelease(release.get());
}
{
auto transaction{ session.createReadTransaction() };
const auto artists{ Artist::findIds(session, Artist::FindParameters{}.setRelease(release.getId())) };
ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId());
}
{
auto transaction{ session.createReadTransaction() };
const std::size_t count{ Release::getCount(session, Release::FindParameters{}.setArtist(artist.getId())) };
EXPECT_EQ(count, 1);
}
}
TEST_F(DatabaseFixture, Artist_artwork) TEST_F(DatabaseFixture, Artist_artwork)
{ {
ScopedImage image{ session, "/image1.jpg" }; ScopedImage image{ session, "/image1.jpg" };
@@ -875,11 +906,11 @@ namespace lms::db::tests
trackD1.get().modify()->setAddedTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 3 } }); trackD1.get().modify()->setAddedTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 3 } });
trackA2.get().modify()->setAddedTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 4 } }); trackA2.get().modify()->setAddedTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 4 } });
TrackArtistLink::create(session, trackA1.get(), artistA.get(), TrackArtistLinkType::Artist); session.create<TrackArtistLink>(trackA1.get(), artistA.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, trackA2.get(), artistA.get(), TrackArtistLinkType::Producer); session.create<TrackArtistLink>(trackA2.get(), artistA.get(), TrackArtistLinkType::Producer);
TrackArtistLink::create(session, trackB1.get(), artistB.get(), TrackArtistLinkType::Artist); session.create<TrackArtistLink>(trackB1.get(), artistB.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, trackC1.get(), artistC.get(), TrackArtistLinkType::Artist); session.create<TrackArtistLink>(trackC1.get(), artistC.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, trackD1.get(), artistD.get(), TrackArtistLinkType::Artist); session.create<TrackArtistLink>(trackD1.get(), artistD.get(), TrackArtistLinkType::Artist);
} }
{ {
@@ -915,11 +946,11 @@ namespace lms::db::tests
trackD1.get().modify()->setLastWriteTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 3 } }); trackD1.get().modify()->setLastWriteTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 3 } });
trackA2.get().modify()->setLastWriteTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 4 } }); trackA2.get().modify()->setLastWriteTime(Wt::WDateTime{ Wt::WDate{ 2021, 1, 4 } });
TrackArtistLink::create(session, trackA1.get(), artistA.get(), TrackArtistLinkType::Artist); session.create<TrackArtistLink>(trackA1.get(), artistA.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, trackA2.get(), artistA.get(), TrackArtistLinkType::Producer); session.create<TrackArtistLink>(trackA2.get(), artistA.get(), TrackArtistLinkType::Producer);
TrackArtistLink::create(session, trackB1.get(), artistB.get(), TrackArtistLinkType::Artist); session.create<TrackArtistLink>(trackB1.get(), artistB.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, trackC1.get(), artistC.get(), TrackArtistLinkType::Artist); session.create<TrackArtistLink>(trackC1.get(), artistC.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, trackD1.get(), artistD.get(), TrackArtistLinkType::Artist); session.create<TrackArtistLink>(trackD1.get(), artistD.get(), TrackArtistLinkType::Artist);
} }
{ {
@@ -976,16 +1007,16 @@ namespace lms::db::tests
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
{ {
auto link{ TrackArtistLink::create(session, trackA1.get(), artistA.get(), TrackArtistLinkType::Artist, true) }; auto link{ session.create<TrackArtistLink>(trackA1.get(), artistA.get(), TrackArtistLinkType::Artist, true) };
link.modify()->setArtistName("ArtistA"); link.modify()->setArtistName("ArtistA");
} }
{ {
auto link{ TrackArtistLink::create(session, trackA2.get(), artistA.get(), TrackArtistLinkType::Artist, true) }; auto link{ session.create<TrackArtistLink>(trackA2.get(), artistA.get(), TrackArtistLinkType::Artist, true) };
link.modify()->setArtistName("AlternateArtistA"); link.modify()->setArtistName("AlternateArtistA");
} }
{ {
auto link{ TrackArtistLink::create(session, trackB1.get(), artistB.get(), TrackArtistLinkType::Artist, true) }; auto link{ session.create<TrackArtistLink>(trackB1.get(), artistB.get(), TrackArtistLinkType::Artist, true) };
link.modify()->setArtistName("ArtistB"); link.modify()->setArtistName("ArtistB");
} }
} }
+1
View File
@@ -18,6 +18,7 @@ add_executable(test-database
RatedRelease.cpp RatedRelease.cpp
RatedTrack.cpp RatedTrack.cpp
Release.cpp Release.cpp
ReleaseArtistLink.cpp
ScanSettings.cpp ScanSettings.cpp
StarredArtist.cpp StarredArtist.cpp
StarredRelease.cpp StarredRelease.cpp
+15 -15
View File
@@ -445,7 +445,7 @@ namespace lms::db::tests
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
auto trackArtistLink{ TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist) }; auto trackArtistLink{ session.create<TrackArtistLink>(track.get(), artist.get(), TrackArtistLinkType::Artist) };
cluster1.get().modify()->addTrack(track.get()); cluster1.get().modify()->addTrack(track.get());
} }
@@ -505,8 +505,8 @@ namespace lms::db::tests
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); session.create<TrackArtistLink>(track.get(), artist.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::ReleaseArtist); session.create<TrackArtistLink>(track.get(), artist.get(), TrackArtistLinkType::Mixer);
cluster.get().modify()->addTrack(track.get()); cluster.get().modify()->addTrack(track.get());
} }
@@ -544,7 +544,7 @@ namespace lms::db::tests
tracks.emplace_back(session); tracks.emplace_back(session);
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
TrackArtistLink::create(session, tracks.back().get(), artist.get(), TrackArtistLinkType::Artist); session.create<TrackArtistLink>(tracks.back().get(), artist.get(), TrackArtistLinkType::Artist);
for (auto& cluster : clusters) for (auto& cluster : clusters)
cluster.get().modify()->addTrack(tracks.back().get()); cluster.get().modify()->addTrack(tracks.back().get());
@@ -654,7 +654,7 @@ namespace lms::db::tests
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); session.create<TrackArtistLink>(track.get(), artist.get(), TrackArtistLinkType::Artist);
track.get().modify()->setRelease(release.get()); track.get().modify()->setRelease(release.get());
cluster.get().modify()->addTrack(track.get()); cluster.get().modify()->addTrack(track.get());
} }
@@ -675,11 +675,11 @@ namespace lms::db::tests
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front(), artist.getId()); EXPECT_EQ(artists.results.front(), artist.getId());
auto releases{ Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId())) }; auto releases{ Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId())) };
ASSERT_EQ(releases.results.size(), 1); ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId()); EXPECT_EQ(releases.results.front(), release.getId());
releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId()).setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ cluster.getId() }))); releases = Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId()).setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ cluster.getId() })));
ASSERT_EQ(releases.results.size(), 1); ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId()); EXPECT_EQ(releases.results.front(), release.getId());
} }
@@ -697,7 +697,7 @@ namespace lms::db::tests
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
auto trackArtistLink{ TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist) }; auto trackArtistLink{ session.create<TrackArtistLink>(track.get(), artist.get(), TrackArtistLinkType::Artist) };
track.get().modify()->setRelease(release.get()); track.get().modify()->setRelease(release.get());
cluster1.get().modify()->addTrack(track.get()); cluster1.get().modify()->addTrack(track.get());
cluster2.get().modify()->addTrack(track.get()); cluster2.get().modify()->addTrack(track.get());
@@ -706,11 +706,11 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
auto releases{ Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId())) }; auto releases{ Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId())) };
ASSERT_EQ(releases.results.size(), 1); ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId()); EXPECT_EQ(releases.results.front(), release.getId());
releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId()).setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ cluster1.getId(), cluster2.getId() }))); releases = Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId()).setFilters(Filters{}.setClusters(std::initializer_list<ClusterId>{ cluster1.getId(), cluster2.getId() })));
ASSERT_EQ(releases.results.size(), 1); ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId()); EXPECT_EQ(releases.results.front(), release.getId());
} }
@@ -822,10 +822,10 @@ namespace lms::db::tests
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
if (i < 5) if (i < 5)
TrackArtistLink::create(session, tracks.back().get(), artist1.get(), TrackArtistLinkType::Artist); session.create<TrackArtistLink>(tracks.back().get(), artist1.get(), TrackArtistLinkType::Artist);
else else
{ {
TrackArtistLink::create(session, tracks.back().get(), artist2.get(), TrackArtistLinkType::Artist); session.create<TrackArtistLink>(tracks.back().get(), artist2.get(), TrackArtistLinkType::Artist);
cluster2.get().modify()->addTrack(tracks.back().get()); cluster2.get().modify()->addTrack(tracks.back().get());
} }
@@ -835,7 +835,7 @@ namespace lms::db::tests
tracks.emplace_back(session); tracks.emplace_back(session);
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
TrackArtistLink::create(session, tracks.back().get(), artist3.get(), TrackArtistLinkType::Artist); session.create<TrackArtistLink>(tracks.back().get(), artist3.get(), TrackArtistLinkType::Artist);
cluster2.get().modify()->addTrack(tracks.back().get()); cluster2.get().modify()->addTrack(tracks.back().get());
} }
@@ -855,12 +855,12 @@ namespace lms::db::tests
} }
{ {
auto artists{ artist1->findSimilarArtistIds({ TrackArtistLinkType::ReleaseArtist }) }; auto artists{ artist1->findSimilarArtistIds({ TrackArtistLinkType::Lyricist }) };
EXPECT_EQ(artists.results.size(), 0); EXPECT_EQ(artists.results.size(), 0);
} }
{ {
auto artists{ artist1->findSimilarArtistIds({ TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist }) }; auto artists{ artist1->findSimilarArtistIds({ TrackArtistLinkType::Artist, TrackArtistLinkType::Lyricist }) };
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results.front(), artist2.getId()); EXPECT_EQ(artists.results.front(), artist2.getId());
} }
+5 -5
View File
@@ -36,7 +36,7 @@ namespace lms::db::tests
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
TrackArtistLink::create(session, tracks.back().get(), artist.get(), TrackArtistLinkType::Artist); session.create<TrackArtistLink>(tracks.back().get(), artist.get(), TrackArtistLinkType::Artist);
tracks.back().get().modify()->setRelease(release.get()); tracks.back().get().modify()->setRelease(release.get());
} }
@@ -49,7 +49,7 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
auto releases{ Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId())) }; auto releases{ Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId())) };
ASSERT_EQ(releases.results.size(), 1); ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId()); EXPECT_EQ(releases.results.front(), release.getId());
@@ -67,18 +67,18 @@ namespace lms::db::tests
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
auto trackArtistLink{ TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist) }; auto trackArtistLink{ session.create<TrackArtistLink>(track.get(), artist.get(), TrackArtistLinkType::Artist) };
track.get().modify()->setRelease(release.get()); track.get().modify()->setRelease(release.get());
} }
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
auto releases{ Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId())) }; auto releases{ Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId())) };
ASSERT_EQ(releases.results.size(), 1); ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId()); EXPECT_EQ(releases.results.front(), release.getId());
auto artists{ release->getArtists() }; auto artists{ release->getTrackArtists() };
ASSERT_EQ(artists.size(), 1); ASSERT_EQ(artists.size(), 1);
ASSERT_EQ(artists.front()->getId(), artist.getId()); ASSERT_EQ(artists.front()->getId(), artist.getId());
} }
+73 -15
View File
@@ -18,6 +18,7 @@
*/ */
#include "database/objects/Listen.hpp" #include "database/objects/Listen.hpp"
#include "database/objects/ReleaseArtistLink.hpp"
#include "Common.hpp" #include "Common.hpp"
@@ -170,7 +171,7 @@ namespace lms::db::tests
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
TrackArtistLink::create(session, track2.get(), artist1.get(), TrackArtistLinkType::Artist); session.create<TrackArtistLink>(track2.get(), artist1.get(), TrackArtistLinkType::Artist);
} }
{ {
@@ -200,7 +201,7 @@ namespace lms::db::tests
Listen::ArtistStatsFindParameters params; Listen::ArtistStatsFindParameters params;
params.setUser(user->getId()); params.setUser(user->getId());
params.setScrobblingBackend(ScrobblingBackend::ListenBrainz); params.setScrobblingBackend(ScrobblingBackend::ListenBrainz);
params.setLinkType(TrackArtistLinkType::Producer); params.setTrackArtistLinkType(TrackArtistLinkType::Producer);
auto artists{ Listen::getTopArtists(session, params) }; auto artists{ Listen::getTopArtists(session, params) };
EXPECT_EQ(artists.results.size(), 0); EXPECT_EQ(artists.results.size(), 0);
@@ -235,8 +236,8 @@ namespace lms::db::tests
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
TrackArtistLink::create(session, track1.get(), artist1.get(), TrackArtistLinkType::Artist); session.create<TrackArtistLink>(track1.get(), artist1.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, track2.get(), artist2.get(), TrackArtistLinkType::Artist); session.create<TrackArtistLink>(track2.get(), artist2.get(), TrackArtistLinkType::Artist);
} }
{ {
@@ -304,7 +305,7 @@ namespace lms::db::tests
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); session.create<TrackArtistLink>(track.get(), artist.get(), TrackArtistLinkType::Artist);
} }
{ {
@@ -362,7 +363,7 @@ namespace lms::db::tests
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); session.create<TrackArtistLink>(track.get(), artist.get(), TrackArtistLinkType::Artist);
track.get().modify()->setMediaLibrary(library.get()); track.get().modify()->setMediaLibrary(library.get());
} }
@@ -695,7 +696,7 @@ namespace lms::db::tests
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); session.create<TrackArtistLink>(track.get(), artist.get(), TrackArtistLinkType::Artist);
} }
{ {
@@ -859,7 +860,50 @@ namespace lms::db::tests
} }
} }
TEST_F(DatabaseFixture, Listen_getRecentArtists) TEST_F(DatabaseFixture, Listen_getRecentReleaseArtists)
{
ScopedTrack track{ session };
ScopedUser user{ session, "MyUser" };
ScopedArtist artist{ session, "MyArtist" };
ScopedRelease release{ session, "MyRelease" };
{
auto transaction{ session.createWriteTransaction() };
session.create<TrackArtistLink>(track.get(), artist.get(), TrackArtistLinkType::Artist);
session.create<ReleaseArtistLink>(release.get(), artist.get(), false);
}
{
auto transaction{ session.createReadTransaction() };
Listen::ArtistStatsFindParameters params;
params.setUser(user->getId());
params.setScrobblingBackend(ScrobblingBackend::Internal);
params.setReleaseArtistsOnly(true);
auto artists{ Listen::getRecentArtists(session, params) };
EXPECT_EQ(artists.results.size(), 0);
EXPECT_EQ(artists.moreResults, false);
}
const Wt::WDateTime dateTime{ Wt::WDate{ 2000, 1, 2 }, Wt::WTime{ 12, 0, 1 } };
ScopedListen listen1{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime };
{
auto transaction{ session.createReadTransaction() };
Listen::ArtistStatsFindParameters params;
params.setUser(user->getId());
params.setScrobblingBackend(ScrobblingBackend::Internal);
params.setReleaseArtistsOnly(true);
auto artists{ Listen::getRecentArtists(session, params) };
ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results[0], artist->getId());
}
}
TEST_F(DatabaseFixture, Listen_getRecentTrackArtists)
{ {
ScopedTrack track{ session }; ScopedTrack track{ session };
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
@@ -867,7 +911,7 @@ namespace lms::db::tests
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); session.create<TrackArtistLink>(track.get(), artist.get(), TrackArtistLinkType::Artist);
} }
{ {
@@ -896,6 +940,20 @@ namespace lms::db::tests
ASSERT_EQ(artists.results.size(), 1); ASSERT_EQ(artists.results.size(), 1);
EXPECT_EQ(artists.results[0], artist->getId()); EXPECT_EQ(artists.results[0], artist->getId());
} }
{
auto transaction{ session.createReadTransaction() };
Listen::ArtistStatsFindParameters params;
params.setUser(user->getId());
params.setScrobblingBackend(ScrobblingBackend::Internal);
params.setReleaseArtistsOnly(true);
auto artists{ Listen::getRecentArtists(session, params) };
EXPECT_EQ(artists.results.size(), 0);
EXPECT_EQ(artists.moreResults, false);
}
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
@@ -912,7 +970,7 @@ namespace lms::db::tests
Listen::ArtistStatsFindParameters params; Listen::ArtistStatsFindParameters params;
params.setUser(user->getId()); params.setUser(user->getId());
params.setScrobblingBackend(ScrobblingBackend::Internal); params.setScrobblingBackend(ScrobblingBackend::Internal);
params.setLinkType(TrackArtistLinkType::Producer); params.setTrackArtistLinkType(TrackArtistLinkType::Producer);
auto artists{ Listen::getRecentArtists(session, params) }; auto artists{ Listen::getRecentArtists(session, params) };
EXPECT_EQ(artists.results.size(), 0); EXPECT_EQ(artists.results.size(), 0);
@@ -935,7 +993,7 @@ namespace lms::db::tests
} }
} }
TEST_F(DatabaseFixture, Listen_getRecentArtists_multi) TEST_F(DatabaseFixture, Listen_getRecentTrackArtists_multi)
{ {
ScopedUser user{ session, "MyUser" }; ScopedUser user{ session, "MyUser" };
ScopedTrack track1{ session }; ScopedTrack track1{ session };
@@ -946,8 +1004,8 @@ namespace lms::db::tests
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
TrackArtistLink::create(session, track1.get(), artist1.get(), TrackArtistLinkType::Artist); session.create<TrackArtistLink>(track1.get(), artist1.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, track2.get(), artist2.get(), TrackArtistLinkType::Artist); session.create<TrackArtistLink>(track2.get(), artist2.get(), TrackArtistLinkType::Artist);
} }
{ {
@@ -1014,7 +1072,7 @@ namespace lms::db::tests
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); session.create<TrackArtistLink>(track.get(), artist.get(), TrackArtistLinkType::Artist);
} }
{ {
@@ -1056,7 +1114,7 @@ namespace lms::db::tests
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); session.create<TrackArtistLink>(track.get(), artist.get(), TrackArtistLinkType::Artist);
track.get().modify()->setMediaLibrary(library.get()); track.get().modify()->setMediaLibrary(library.get());
} }
+2
View File
@@ -34,6 +34,7 @@
#include "database/objects/RatedArtist.hpp" #include "database/objects/RatedArtist.hpp"
#include "database/objects/RatedRelease.hpp" #include "database/objects/RatedRelease.hpp"
#include "database/objects/RatedTrack.hpp" #include "database/objects/RatedTrack.hpp"
#include "database/objects/ReleaseArtistLink.hpp"
#include "database/objects/ScanSettings.hpp" #include "database/objects/ScanSettings.hpp"
#include "database/objects/StarredArtist.hpp" #include "database/objects/StarredArtist.hpp"
#include "database/objects/StarredRelease.hpp" #include "database/objects/StarredRelease.hpp"
@@ -368,6 +369,7 @@ VALUES
EXPECT_FALSE(RatedRelease::find(session, RatedReleaseId{})); EXPECT_FALSE(RatedRelease::find(session, RatedReleaseId{}));
EXPECT_FALSE(RatedTrack::find(session, RatedTrackId{})); EXPECT_FALSE(RatedTrack::find(session, RatedTrackId{}));
EXPECT_FALSE(Release::find(session, ReleaseId{})); EXPECT_FALSE(Release::find(session, ReleaseId{}));
EXPECT_FALSE(ReleaseArtistLink::find(session, ReleaseArtistLinkId{}));
EXPECT_FALSE(ReleaseType::find(session, ReleaseTypeId{})); EXPECT_FALSE(ReleaseType::find(session, ReleaseTypeId{}));
EXPECT_FALSE(ScanSettings::find(session, ScanSettingsId{})); EXPECT_FALSE(ScanSettings::find(session, ScanSettingsId{}));
EXPECT_FALSE(StarredArtist::find(session, StarredArtistId{})); EXPECT_FALSE(StarredArtist::find(session, StarredArtistId{}));
+111 -50
View File
@@ -23,6 +23,8 @@
#include "database/objects/Artwork.hpp" #include "database/objects/Artwork.hpp"
#include "database/objects/Image.hpp" #include "database/objects/Image.hpp"
#include "database/objects/Medium.hpp" #include "database/objects/Medium.hpp"
#include "database/objects/ReleaseArtistLink.hpp"
#include "database/objects/Types.hpp"
namespace lms::db::tests namespace lms::db::tests
{ {
@@ -689,7 +691,7 @@ namespace lms::db::tests
} }
} }
TEST_F(DatabaseFixture, Release_artist) TEST_F(DatabaseFixture, Release_trackArtist)
{ {
ScopedRelease release{ session, "MyRelease" }; ScopedRelease release{ session, "MyRelease" };
ScopedTrack track{ session }; ScopedTrack track{ session };
@@ -703,21 +705,21 @@ namespace lms::db::tests
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
auto releases{ Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Artist })) }; auto releases{ Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId(), { TrackArtistLinkType::Artist })) };
EXPECT_EQ(releases.results.size(), 0); EXPECT_EQ(releases.results.size(), 0);
EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Artist })), 0); EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setTrackArtist(artist.getId(), { TrackArtistLinkType::Artist })), 0);
EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setArtist(artist.getId())), 0); EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setTrackArtist(artist.getId())), 0);
releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist2.getId(), { TrackArtistLinkType::Artist })); releases = Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist2.getId(), { TrackArtistLinkType::Artist }));
EXPECT_EQ(releases.results.size(), 0); EXPECT_EQ(releases.results.size(), 0);
EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setArtist(artist2.getId(), { TrackArtistLinkType::Artist })), 0); EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setTrackArtist(artist2.getId(), { TrackArtistLinkType::Artist })), 0);
EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setArtist(artist2.getId())), 0); EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setTrackArtist(artist2.getId())), 0);
} }
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist); session.create<TrackArtistLink>(track.get(), artist.get(), TrackArtistLinkType::Artist);
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Producer); session.create<TrackArtistLink>(track.get(), artist.get(), TrackArtistLinkType::Producer);
} }
{ {
@@ -725,83 +727,124 @@ namespace lms::db::tests
EXPECT_EQ(Release::getCount(session, Release::FindParameters{}), 1); EXPECT_EQ(Release::getCount(session, Release::FindParameters{}), 1);
auto releases{ Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Artist })) }; auto releases{ Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId(), { TrackArtistLinkType::Artist })) };
ASSERT_EQ(releases.results.size(), 1); ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId()); EXPECT_EQ(releases.results.front(), release.getId());
EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Artist })), 1); EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setTrackArtist(artist.getId(), { TrackArtistLinkType::Artist })), 1);
EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Remixer })), 0); EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setTrackArtist(artist.getId(), { TrackArtistLinkType::Remixer })), 0);
releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Artist, TrackArtistLinkType::Mixer })); releases = Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId(), { TrackArtistLinkType::Artist, TrackArtistLinkType::Mixer }));
EXPECT_EQ(releases.results.size(), 1); EXPECT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId()); EXPECT_EQ(releases.results.front(), release.getId());
EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Artist, TrackArtistLinkType::Mixer })), 1); EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setTrackArtist(artist.getId(), { TrackArtistLinkType::Artist, TrackArtistLinkType::Mixer })), 1);
releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist2.getId(), { TrackArtistLinkType::Artist })); releases = Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist2.getId(), { TrackArtistLinkType::Artist }));
EXPECT_EQ(releases.results.size(), 0); EXPECT_EQ(releases.results.size(), 0);
releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist2.getId())); releases = Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist2.getId()));
EXPECT_EQ(releases.results.size(), 0); EXPECT_EQ(releases.results.size(), 0);
releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::ReleaseArtist, TrackArtistLinkType::Artist })); releases = Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId(), { TrackArtistLinkType::Writer, TrackArtistLinkType::Artist }));
ASSERT_EQ(releases.results.size(), 1); ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId()); EXPECT_EQ(releases.results.front(), release.getId());
releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId())); releases = Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId()));
ASSERT_EQ(releases.results.size(), 1); ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId()); EXPECT_EQ(releases.results.front(), release.getId());
EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setArtist(artist.getId())), 1); EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setTrackArtist(artist.getId())), 1);
EXPECT_EQ(release->getTrackArtists().size(), 1);
EXPECT_EQ(release->getTrackArtists(TrackArtistLinkType::Artist).size(), 1);
EXPECT_EQ(release->getTrackArtists(TrackArtistLinkType::Conductor).size(), 0);
releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Composer })); releases = Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId(), { TrackArtistLinkType::Composer }));
EXPECT_EQ(releases.results.size(), 0); EXPECT_EQ(releases.results.size(), 0);
releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::Composer, TrackArtistLinkType::Mixer })); releases = Release::findIds(session, Release::FindParameters{}.setTrackArtist(artist.getId(), { TrackArtistLinkType::Composer, TrackArtistLinkType::Mixer }));
EXPECT_EQ(releases.results.size(), 0);
releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId(), {}, { TrackArtistLinkType::Artist }));
EXPECT_EQ(releases.results.size(), 0);
releases = Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId(), {}, { TrackArtistLinkType::Artist, TrackArtistLinkType::Composer }));
EXPECT_EQ(releases.results.size(), 0); EXPECT_EQ(releases.results.size(), 0);
} }
}
TEST_F(DatabaseFixture, Release_releaseArtist)
{
ScopedRelease release{ session, "MyRelease" };
ScopedTrack track{ session };
ScopedArtist artist{ session, "MyArtist" };
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
const auto releases{ Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::ReleaseArtist })) }; {
bool visited{};
release->visitTrackArtistLinks(TrackArtistLinkType::Artist, [&](const db::TrackArtistLink::pointer& link) {
visited = true;
EXPECT_EQ(link->getArtistId(), artist.getId());
});
EXPECT_TRUE(visited);
}
{
bool visited{};
release->visitTrackArtistLinks(TrackArtistLinkType::Conductor, [&](const db::TrackArtistLink::pointer&) {
visited = true;
});
EXPECT_FALSE(visited);
}
}
}
TEST_F(DatabaseFixture, Release_artist)
{
ScopedRelease release{ session, "MyRelease" };
ScopedArtist artist1{ session, "MyArtist" };
ScopedArtist artist2{ session, "MyArtist" };
ScopedArtist artist3{ session, "MyArtist" };
{
auto transaction{ session.createReadTransaction() };
const auto releases{ Release::findIds(session, Release::FindParameters{}.setArtist(artist1.getId())) };
EXPECT_EQ(releases.results.size(), 0); EXPECT_EQ(releases.results.size(), 0);
EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::ReleaseArtist })), 0); EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setArtist(artist1.getId())), 0);
EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setArtist(artist.getId())), 0); EXPECT_EQ(release->getTrackArtists(TrackArtistLinkType::Conductor).size(), 0);
EXPECT_EQ(release->getArtists(TrackArtistLinkType::ReleaseArtist).size(), 0); EXPECT_EQ(release->getArtistLinks().size(), 0);
EXPECT_EQ(release->getArtistIds(TrackArtistLinkType::ReleaseArtist).size(), 0);
bool visited{};
release->visitArtistLinks([&](const db::ReleaseArtistLink::pointer&) {
visited = true;
});
EXPECT_FALSE(visited);
} }
{ {
auto transaction{ session.createWriteTransaction() }; auto transaction{ session.createWriteTransaction() };
track.get().modify()->setRelease(release.get()); auto link1{ session.create<ReleaseArtistLink>(release.get(), artist1.get(), false) };
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::ReleaseArtist); auto link2{ session.create<ReleaseArtistLink>(release.get(), artist2.get(), true) };
link2.modify()->setArtistName("MyArtistAlternateName");
link2.modify()->setArtistSortName("MyArtistAlternateSortName");
} }
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
auto artists{ release->getArtists(TrackArtistLinkType::ReleaseArtist) }; const auto links{ release->getArtistLinks() };
ASSERT_EQ(artists.size(), 1); ASSERT_EQ(links.size(), 2);
EXPECT_EQ(artists.front()->getId(), artist.getId()); EXPECT_EQ(links[0]->getArtistId(), artist1.getId());
EXPECT_EQ(links[0]->getArtistName(), "");
EXPECT_EQ(links[0]->getArtistSortName(), "");
EXPECT_EQ(links[1]->getArtistId(), artist2.getId());
EXPECT_EQ(links[1]->getArtistName(), "MyArtistAlternateName");
EXPECT_EQ(links[1]->getArtistSortName(), "MyArtistAlternateSortName");
EXPECT_EQ(links[1]->isArtistMBIDMatched(), true);
} }
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
auto artists{ release->getArtistIds(TrackArtistLinkType::ReleaseArtist) }; std::vector<db::ReleaseArtistLink::pointer> links;
ASSERT_EQ(artists.size(), 1); release->visitArtistLinks([&](const db::ReleaseArtistLink::pointer& artistLink) {
EXPECT_EQ(artists.front(), artist.getId()); links.push_back(artistLink);
});
ASSERT_EQ(links.size(), 2);
EXPECT_EQ(links[0]->getArtistId(), artist1.getId());
EXPECT_EQ(links[0]->getArtistName(), "");
EXPECT_EQ(links[0]->getArtistSortName(), "");
EXPECT_EQ(links[1]->getArtistId(), artist2.getId());
EXPECT_EQ(links[1]->getArtistName(), "MyArtistAlternateName");
EXPECT_EQ(links[1]->getArtistSortName(), "MyArtistAlternateSortName");
EXPECT_EQ(links[1]->isArtistMBIDMatched(), true);
} }
{ {
@@ -809,13 +852,31 @@ namespace lms::db::tests
EXPECT_EQ(Release::getCount(session, Release::FindParameters{}), 1); EXPECT_EQ(Release::getCount(session, Release::FindParameters{}), 1);
const auto releases{ Release::findIds(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::ReleaseArtist })) }; const auto releases{ Release::findIds(session, Release::FindParameters{}.setArtist(artist1.getId())) };
ASSERT_EQ(releases.results.size(), 1); ASSERT_EQ(releases.results.size(), 1);
EXPECT_EQ(releases.results.front(), release.getId()); EXPECT_EQ(releases.results.front(), release.getId());
EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setArtist(artist.getId(), { TrackArtistLinkType::ReleaseArtist })), 1); EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setTrackArtist(artist1.getId())), 0);
EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setArtist(artist.getId())), 1); EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setArtist(artist1.getId())), 1);
EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setTrackArtist(artist2.getId())), 0);
EXPECT_EQ(Release::getCount(session, Release::FindParameters{}.setArtist(artist2.getId())), 1);
}
{
auto transaction{ session.createReadTransaction() };
const auto artists{ release->getArtists() };
EXPECT_EQ(artists.size(), 2);
EXPECT_EQ(artists[0]->getId(), artist1.getId());
EXPECT_EQ(artists[1]->getId(), artist2.getId());
}
{
auto transaction{ session.createReadTransaction() };
EXPECT_TRUE(release->hasArtist(artist1.getId()));
EXPECT_FALSE(release->hasArtist(artist3.getId()));
} }
} }
TEST_F(DatabaseFixture, Release_isCompilation) TEST_F(DatabaseFixture, Release_isCompilation)
{ {
ScopedRelease release{ session, "MyRelease" }; ScopedRelease release{ session, "MyRelease" };
@@ -0,0 +1,67 @@
/*
* Copyright (C) 2021 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Common.hpp"
#include "database/objects/ReleaseArtistLink.hpp"
namespace lms::db::tests
{
TEST_F(DatabaseFixture, ReleaseArtistLink)
{
ScopedRelease release{ session, "MyRelease" };
ScopedTrack track{ session };
ScopedArtist artist{ session, "MyArtist" };
{
auto transaction{ session.createReadTransaction() };
ReleaseArtistLink::FindParameters params;
params.setRelease(release.getId());
bool visited{};
ReleaseArtistLink::find(session, params, [&](const ReleaseArtistLink::pointer&) {
visited = true;
});
EXPECT_FALSE(visited);
}
{
auto transaction{ session.createWriteTransaction() };
track.get().modify()->setRelease(release.get());
session.create<ReleaseArtistLink>(release.get(), artist.get(), false);
}
{
auto transaction{ session.createReadTransaction() };
ReleaseArtistLink::FindParameters params;
params.setRelease(release.getId());
params.setMBIDMatched(false);
bool visited{};
ReleaseArtistLink::find(session, params, [&](const ReleaseArtistLink::pointer& link) {
visited = true;
EXPECT_EQ(link->getArtistId(), artist.getId());
});
EXPECT_TRUE(visited);
}
}
} // namespace lms::db::tests
@@ -100,7 +100,7 @@ namespace lms::feedback
searchParams.setFilters(params.filters); searchParams.setFilters(params.filters);
searchParams.setStarringUser(params.user, *backend); searchParams.setStarringUser(params.user, *backend);
searchParams.setKeywords(params.keywords); searchParams.setKeywords(params.keywords);
searchParams.setLinkType(params.linkType); searchParams.setTrackArtistLinkType(params.trackArtistLinkType);
searchParams.setSortMethod(params.sortMethod); searchParams.setSortMethod(params.sortMethod);
searchParams.setRange(params.range); searchParams.setRange(params.range);
@@ -81,12 +81,18 @@ namespace lms::feedback
// Artists // Artists
struct ArtistFindParameters : public FindParameters struct ArtistFindParameters : public FindParameters
{ {
std::optional<db::TrackArtistLinkType> linkType; // if set, only artists that have produced at least one track with this link type std::optional<db::TrackArtistLinkType> trackArtistLinkType; // if set, only artists that have produced at least one track with this link type
db::ArtistSortMethod sortMethod{ db::ArtistSortMethod::None }; db::ArtistSortMethod sortMethod{ db::ArtistSortMethod::None };
std::optional<bool> releaseArtistsOnly;
ArtistFindParameters& setLinkType(std::optional<db::TrackArtistLinkType> _linkType) ArtistFindParameters& setReleaseArtistsOnly(std::optional<bool> _releaseArtistsOnly)
{ {
linkType = _linkType; releaseArtistsOnly = _releaseArtistsOnly;
return *this;
}
ArtistFindParameters& setTrackArtistLinkType(std::optional<db::TrackArtistLinkType> _trackArtistLinkType)
{
trackArtistLinkType = _trackArtistLinkType;
return *this; return *this;
} }
ArtistFindParameters& setSortMethod(db::ArtistSortMethod _sortMethod) ArtistFindParameters& setSortMethod(db::ArtistSortMethod _sortMethod)
@@ -38,6 +38,7 @@
#include "database/objects/MediaLibrary.hpp" #include "database/objects/MediaLibrary.hpp"
#include "database/objects/Medium.hpp" #include "database/objects/Medium.hpp"
#include "database/objects/Release.hpp" #include "database/objects/Release.hpp"
#include "database/objects/ReleaseArtistLink.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "database/objects/TrackArtistLink.hpp" #include "database/objects/TrackArtistLink.hpp"
#include "database/objects/TrackEmbeddedImage.hpp" #include "database/objects/TrackEmbeddedImage.hpp"
@@ -80,6 +81,20 @@ namespace lms::scanner
createTrackArtistLinks(session, track, linkType, noRole, artists, allowArtistMBIDFallback); createTrackArtistLinks(session, track, linkType, noRole, artists, allowArtistMBIDFallback);
} }
void createReleaseArtistLinks(db::Session& session, const db::Release::pointer& dbRelease, std::span<const Artist> artists, helpers::AllowFallbackOnMBIDEntry allowArtistMBIDFallback)
{
for (const Artist& artist : artists)
{
db::Artist::pointer dbArtist{ helpers::getOrCreateArtist(session, artist, allowArtistMBIDFallback) };
const bool matchedUsingMbid{ artist.mbid.has_value() && dbArtist->getMBID() == artist.mbid };
db::ReleaseArtistLink::pointer link{ session.create<db::ReleaseArtistLink>(dbRelease, dbArtist, matchedUsingMbid) };
link.modify()->setArtistName(artist.name);
if (artist.sortName)
link.modify()->setArtistSortName(*artist.sortName);
}
}
db::ReleaseType::pointer getOrCreateReleaseType(db::Session& session, std::string_view name) db::ReleaseType::pointer getOrCreateReleaseType(db::Session& session, std::string_view name)
{ {
db::ReleaseType::pointer releaseType{ db::ReleaseType::find(session, name) }; db::ReleaseType::pointer releaseType{ db::ReleaseType::find(session, name) };
@@ -107,7 +122,46 @@ namespace lms::scanner
return label; return label;
} }
void updateReleaseIfNeeded(db::Session& session, db::Release::pointer dbRelease, const Release& release) bool needUpdateReleaseArtists(const db::Release::pointer& dbRelease, const Release& release)
{
const std::vector<db::ReleaseArtistLink::pointer> dbArtistLinks{ dbRelease->getArtistLinks() };
if (dbArtistLinks.size() != release.artists.size())
return true;
// Must be in same order
for (std::size_t i{}; i < dbArtistLinks.size(); ++i)
{
const db::ReleaseArtistLink::pointer& dbArtistLink{ dbArtistLinks[i] };
const Artist& artist{ release.artists[i] };
if (dbArtistLink->getArtistName() != artist.name)
return true;
if (dbArtistLink->getArtistSortName() != artist.sortName)
return true;
if (!dbArtistLink->isArtistMBIDMatched() && artist.mbid)
return true;
if (dbArtistLink->isArtistMBIDMatched() && !artist.mbid)
return true;
if (artist.mbid)
{
const db::Artist::pointer dbArtist{ dbArtistLink->getArtist() };
if (!dbArtist)
return true;
if (dbArtistLink->getArtist()->getMBID() != artist.mbid)
return true;
}
}
return false;
}
void updateReleaseIfNeeded(db::Session& session, db::Release::pointer dbRelease, const Release& release, helpers::AllowFallbackOnMBIDEntry allowFallback)
{ {
if (dbRelease->getName() != release.name) if (dbRelease->getName() != release.name)
dbRelease.modify()->setName(release.name); dbRelease.modify()->setName(release.name);
@@ -143,6 +197,11 @@ namespace lms::scanner
for (std::string_view label : release.labels) for (std::string_view label : release.labels)
dbRelease.modify()->addLabel(getOrCreateLabel(session, label)); dbRelease.modify()->addLabel(getOrCreateLabel(session, label));
} }
if (needUpdateReleaseArtists(dbRelease, release))
{
dbRelease.modify()->clearArtistLinks();
createReleaseArtistLinks(session, dbRelease, release.artists, allowFallback);
}
} }
// Compare release level info // Compare release level info
@@ -157,7 +216,7 @@ namespace lms::scanner
&& dbCandidateRelease->getBarcode() == release.barcode; && dbCandidateRelease->getBarcode() == release.barcode;
} }
db::Release::pointer getOrCreateRelease(db::Session& session, const Release& release, const db::Directory::pointer& currentDirectory) db::Release::pointer getOrCreateRelease(db::Session& session, const Release& release, const db::Directory::pointer& currentDirectory, helpers::AllowFallbackOnMBIDEntry allowFallback)
{ {
db::Release::pointer dbRelease; db::Release::pointer dbRelease;
@@ -224,7 +283,7 @@ namespace lms::scanner
if (!dbRelease) if (!dbRelease)
dbRelease = session.create<db::Release>(release.name); dbRelease = session.create<db::Release>(release.name);
updateReleaseIfNeeded(session, dbRelease, release); updateReleaseIfNeeded(session, dbRelease, release, allowFallback);
return dbRelease; return dbRelease;
} }
@@ -671,13 +730,24 @@ namespace lms::scanner
db::Directory::pointer directory{ utils::getOrCreateDirectory(dbSession, getFilePath().parent_path(), mediaLibrary) }; db::Directory::pointer directory{ utils::getOrCreateDirectory(dbSession, getFilePath().parent_path(), mediaLibrary) };
track.modify()->setDirectory(directory); track.modify()->setDirectory(directory);
track.modify()->clearArtistLinks();
const helpers::AllowFallbackOnMBIDEntry allowFallback{ getScannerSettings().allowArtistMBIDFallback }; const helpers::AllowFallbackOnMBIDEntry allowFallback{ getScannerSettings().allowArtistMBIDFallback };
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Artist, _file->track.artists, allowFallback);
if (_file->track.medium && _file->track.medium->release)
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::ReleaseArtist, _file->track.medium->release->artists, allowFallback);
// For now, alway tie a medium to a release, and a release mst have at least one medium, even if no disc number is set
if (_file->track.medium && _file->track.medium->release)
{
db::Release::pointer release{ getOrCreateRelease(dbSession, *_file->track.medium->release, directory, allowFallback) };
assert(release);
track.modify()->setRelease(release);
track.modify()->setMedium(getOrCreateMedium(dbSession, *_file->track.medium, release));
}
else
{
track.modify()->setRelease({});
track.modify()->setMedium({});
}
track.modify()->clearArtistLinks();
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Artist, _file->track.artists, allowFallback);
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Conductor, _file->track.conductorArtists, allowFallback); createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Conductor, _file->track.conductorArtists, allowFallback);
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Composer, _file->track.composerArtists, allowFallback); createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Composer, _file->track.composerArtists, allowFallback);
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Lyricist, _file->track.lyricistArtists, allowFallback); createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Lyricist, _file->track.lyricistArtists, allowFallback);
@@ -688,19 +758,6 @@ namespace lms::scanner
for (const auto& [role, performers] : _file->track.performerArtists) for (const auto& [role, performers] : _file->track.performerArtists)
createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Performer, role, performers, allowFallback); createTrackArtistLinks(dbSession, track, db::TrackArtistLinkType::Performer, role, performers, allowFallback);
// For now, alway tie a medium to a release, and a release mst have at least one medium, even if no disc number is set
if (_file->track.medium && _file->track.medium->release)
{
db::Release::pointer release{ getOrCreateRelease(dbSession, *_file->track.medium->release, directory) };
assert(release);
track.modify()->setRelease(release);
track.modify()->setMedium(getOrCreateMedium(dbSession, *_file->track.medium, release));
}
else
{
track.modify()->setRelease({});
track.modify()->setMedium({});
}
track.modify()->setClusters(getOrCreateClusters(dbSession, _file->track)); track.modify()->setClusters(getOrCreateClusters(dbSession, _file->track));
track.modify()->setName(title); track.modify()->setName(title);
track.modify()->setTrackNumber(_file->track.position); track.modify()->setTrackNumber(_file->track.position);
@@ -28,6 +28,7 @@
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/objects/Artist.hpp" #include "database/objects/Artist.hpp"
#include "database/objects/ArtistInfo.hpp" #include "database/objects/ArtistInfo.hpp"
#include "database/objects/ReleaseArtistLink.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "database/objects/TrackArtistLink.hpp" #include "database/objects/TrackArtistLink.hpp"
#include "database/objects/TrackList.hpp" #include "database/objects/TrackList.hpp"
@@ -75,23 +76,46 @@ namespace lms::scanner
artistInfo.modify()->setArtist(newArtist); artistInfo.modify()->setArtist(newArtist);
} }
db::TrackArtistLink::pointer getMostRecentMBIDArtistLink(db::Session& session, db::ArtistId artistId, std::optional<db::TrackArtistLinkType> linkType = std::nullopt) struct ArtistReference
{ {
std::string name;
std::string sortName;
};
std::optional<ArtistReference> getMostRecentReleaseArtistReference(db::Session& session, db::ArtistId artistId)
{
std::optional<ArtistReference> ref;
db::ReleaseArtistLink::FindParameters params;
params.setArtist(artistId);
params.setSortMethod(db::ReleaseArtistLinkSortMethod::OriginalDateDesc);
params.setMBIDMatched(true);
params.setRange(db::Range{ .offset = 0, .size = 1 });
db::ReleaseArtistLink::pointer foundLink;
db::ReleaseArtistLink::find(session, params, [&](const db::ReleaseArtistLink::pointer& link) {
ref = ArtistReference{ .name = std::string{ link->getArtistName() }, .sortName = std::string{ link->getArtistSortName() } };
});
return ref;
}
std::optional<ArtistReference> getMostRecentTrackArtistReference(db::Session& session, db::ArtistId artistId)
{
std::optional<ArtistReference> ref;
db::TrackArtistLink::FindParameters params; db::TrackArtistLink::FindParameters params;
params.setArtist(artistId); params.setArtist(artistId);
params.setLinkType(linkType);
params.setSortMethod(db::TrackArtistLinkSortMethod::OriginalDateDesc); params.setSortMethod(db::TrackArtistLinkSortMethod::OriginalDateDesc);
params.setMBIDMatched(true); params.setMBIDMatched(true);
params.setRange(db::Range{ .offset = 0, .size = 1 }); params.setRange(db::Range{ .offset = 0, .size = 1 });
db::TrackArtistLink::pointer foundLink; db::TrackArtistLink::pointer foundLink;
db::TrackArtistLink::find(session, params, [&](const db::TrackArtistLink::pointer& link) { db::TrackArtistLink::find(session, params, [&](const db::TrackArtistLink::pointer& link) {
foundLink = link; ref = ArtistReference{ .name = std::string{ link->getArtistName() }, .sortName = std::string{ link->getArtistSortName() } };
}); });
return foundLink; return ref;
} }
} // namespace } // namespace
bool ScanStepArtistReconciliation::needProcess([[maybe_unused]] const ScanContext& context) const bool ScanStepArtistReconciliation::needProcess([[maybe_unused]] const ScanContext& context) const
@@ -173,24 +197,24 @@ namespace lms::scanner
if (hasArtistInfo) if (hasArtistInfo)
continue; continue;
db::TrackArtistLink::pointer artistMostRecentLink{ getMostRecentMBIDArtistLink(session, artist->getId(), db::TrackArtistLinkType::ReleaseArtist) }; std::optional<ArtistReference> mostRecentArtistRef{ getMostRecentReleaseArtistReference(session, artist->getId()) };
if (!artistMostRecentLink) if (!mostRecentArtistRef)
artistMostRecentLink = getMostRecentMBIDArtistLink(session, artist->getId()); mostRecentArtistRef = getMostRecentTrackArtistReference(session, artist->getId());
if (!artistMostRecentLink) if (!mostRecentArtistRef)
{ {
LMS_LOG(DBUPDATER, DEBUG, "Unable to fix name discrepancy for artist " << artist << ": no link found!"); LMS_LOG(DBUPDATER, DEBUG, "Unable to fix name discrepancy for artist " << artist << ": no link found!");
continue; continue;
} }
if (artistMostRecentLink->getArtistName() != artist->getName()) if (mostRecentArtistRef->name != artist->getName())
{ {
ArtistToUpdate& artistToUpdate{ artistsToUpdate.emplace_back() }; ArtistToUpdate& artistToUpdate{ artistsToUpdate.emplace_back() };
artistToUpdate.artist = artist; artistToUpdate.artist = artist;
artistToUpdate.newName = artistMostRecentLink->getArtistName(); artistToUpdate.newName = mostRecentArtistRef->name;
artistToUpdate.newSortName = artistMostRecentLink->getArtistSortName(); artistToUpdate.newSortName = mostRecentArtistRef->sortName;
LMS_LOG(DBUPDATER, DEBUG, "Updating artist " << artist << " name to '" << artistToUpdate.newName << "' using most recent '" << db::trackArtistLinkTypeToString(artistMostRecentLink->getType()) << "' link reference"); LMS_LOG(DBUPDATER, DEBUG, "Updating artist " << artist << " name to '" << artistToUpdate.newName << "' using most recent artist link reference");
} }
} }
@@ -126,7 +126,7 @@ namespace lms::scanner
std::set<std::filesystem::path> releasePaths; std::set<std::filesystem::path> releasePaths;
db::Directory::FindParameters params; db::Directory::FindParameters params;
params.setArtist(artistId, { db::TrackArtistLinkType::ReleaseArtist }); params.setReleaseArtist(artistId);
db::Directory::find(session, params, [&](const db::Directory::pointer& directory) { db::Directory::find(session, params, [&](const db::Directory::pointer& directory) {
releasePaths.insert(directory->getAbsolutePath()); releasePaths.insert(directory->getAbsolutePath());
@@ -176,7 +176,7 @@ namespace lms::scanner
db::Artwork::pointer artwork; db::Artwork::pointer artwork;
db::Release::FindParameters params; db::Release::FindParameters params;
params.setArtist(artist->getId(), { db::TrackArtistLinkType::ReleaseArtist }); params.setArtist(artist->getId());
params.setSortMethod(db::ReleaseSortMethod::OriginalDate); params.setSortMethod(db::ReleaseSortMethod::OriginalDate);
db::Release::find(session, params, [&](const db::Release::pointer& release) { db::Release::find(session, params, [&](const db::Release::pointer& release) {
@@ -50,6 +50,9 @@ namespace lms::scanner
{ {
IdType objectId; IdType objectId;
std::filesystem::path file; std::filesystem::path file;
FileToCheck(IdType _objectId, const std::filesystem::path& _file)
: objectId{ _objectId }, file{ _file } {}
}; };
template<typename IdType> template<typename IdType>
@@ -54,7 +54,6 @@ namespace lms::scanner
CheckForRemovedFiles, CheckForRemovedFiles,
ComputeClusterStats, ComputeClusterStats,
Compact, Compact,
FetchTrackFeatures,
Optimize, Optimize,
ReconciliateArtists, ReconciliateArtists,
ReloadSimilarityEngine, ReloadSimilarityEngine,
@@ -51,7 +51,8 @@ namespace lms::scrobbling
db::Listen::ArtistStatsFindParameters convertToListenFindParameters(const ScrobblingService::ArtistFindParameters& params) db::Listen::ArtistStatsFindParameters convertToListenFindParameters(const ScrobblingService::ArtistFindParameters& params)
{ {
return db::Listen::ArtistStatsFindParameters{ convertToListenFindParameters(static_cast<const ScrobblingService::FindParameters&>(params)), params.linkType }; db::Listen::ArtistStatsFindParameters listenFindParams{ convertToListenFindParameters(static_cast<const ScrobblingService::FindParameters&>(params)), params.linkType, params.releaseArtistsOnly };
return listenFindParams;
} }
} // namespace } // namespace
@@ -36,6 +36,7 @@
#include "database/objects/Listen.hpp" #include "database/objects/Listen.hpp"
#include "database/objects/Release.hpp" #include "database/objects/Release.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "database/objects/TrackArtistLink.hpp"
#include "database/objects/User.hpp" #include "database/objects/User.hpp"
#include "services/scrobbling/Exception.hpp" #include "services/scrobbling/Exception.hpp"
@@ -46,6 +47,27 @@ namespace lms::scrobbling::listenBrainz
{ {
namespace namespace
{ {
struct Artist
{
std::string name;
std::optional<core::UUID> mbid;
};
std::vector<Artist> getTrackArtists(const db::Track::pointer& track)
{
std::vector<Artist> artists;
for (const db::TrackArtistLink::pointer& trackArtistLink : track->getArtistLinks(db::TrackArtistLinkType::Artist))
{
const auto trackArtist{ trackArtistLink->getArtist() };
if (!trackArtist)
continue;
artists.emplace_back(Artist{ std::string{ trackArtistLink->getArtistName() }, trackArtist->getMBID() });
}
return artists;
}
std::optional<Wt::Json::Object> listenToJsonPayload(db::Session& session, const scrobbling::Listen& listen, const Wt::WDateTime& timePoint) std::optional<Wt::Json::Object> listenToJsonPayload(db::Session& session, const scrobbling::Listen& listen, const Wt::WDateTime& timePoint)
{ {
auto transaction{ session.createReadTransaction() }; auto transaction{ session.createReadTransaction() };
@@ -54,10 +76,7 @@ namespace lms::scrobbling::listenBrainz
if (!track) if (!track)
return std::nullopt; return std::nullopt;
auto artists{ track->getArtists({ db::TrackArtistLinkType::Artist }) }; const std::vector<Artist> artists{ getTrackArtists(track) };
if (artists.empty())
artists = track->getArtists({ db::TrackArtistLinkType::ReleaseArtist });
if (artists.empty()) if (artists.empty())
{ {
LOG(DEBUG, "Track cannot be scrobbled since it does not have any artist"); LOG(DEBUG, "Track cannot be scrobbled since it does not have any artist");
@@ -77,10 +96,10 @@ namespace lms::scrobbling::listenBrainz
{ {
Wt::Json::Array artistMBIDs; Wt::Json::Array artistMBIDs;
for (const db::Artist::pointer& artist : artists) for (const Artist& artist : artists)
{ {
if (auto MBID{ artist->getMBID() }) if (artist.mbid)
artistMBIDs.push_back(Wt::Json::Value{ std::string{ MBID->getAsString() } }); artistMBIDs.push_back(Wt::Json::Value{ std::string{ artist.mbid->getAsString() } });
} }
if (!artistMBIDs.empty()) if (!artistMBIDs.empty())
@@ -97,8 +97,14 @@ namespace lms::scrobbling
{ {
std::optional<db::TrackArtistLinkType> linkType; // if set, only artists that have produced at least one track with this link type std::optional<db::TrackArtistLinkType> linkType; // if set, only artists that have produced at least one track with this link type
db::ArtistSortMethod sortMethod{ db::ArtistSortMethod::None }; db::ArtistSortMethod sortMethod{ db::ArtistSortMethod::None };
bool releaseArtistsOnly;
ArtistFindParameters& setLinkType(std::optional<db::TrackArtistLinkType> _linkType) ArtistFindParameters& setReleaseArtistsOnly(bool _releaseArtistsOnly)
{
releaseArtistsOnly = _releaseArtistsOnly;
return *this;
}
ArtistFindParameters& setTrackArtistLinkType(std::optional<db::TrackArtistLinkType> _linkType)
{ {
linkType = _linkType; linkType = _linkType;
return *this; return *this;
+17 -13
View File
@@ -109,7 +109,7 @@ namespace lms::api::subsonic
{ {
// API says: "Returns a random collection of songs from the given artist and similar artists" // API says: "Returns a random collection of songs from the given artist and similar artists"
const std::size_t similarArtistCount{ count / 5 }; const std::size_t similarArtistCount{ count / 5 };
std::vector<ArtistId> artistIds{ core::Service<recommendation::IRecommendationService>::get()->getSimilarArtists(artistId, { TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist }, similarArtistCount) }; std::vector<ArtistId> artistIds{ core::Service<recommendation::IRecommendationService>::get()->getSimilarArtists(artistId, { TrackArtistLinkType::Artist }, similarArtistCount) };
artistIds.push_back(artistId); artistIds.push_back(artistId);
const std::size_t meanTrackCountPerArtist{ (count / artistIds.size()) + 1 }; const std::size_t meanTrackCountPerArtist{ (count / artistIds.size()) + 1 };
@@ -392,16 +392,16 @@ namespace lms::api::subsonic
case SubsonicArtistListMode::AllArtists: case SubsonicArtistListMode::AllArtists:
break; break;
case SubsonicArtistListMode::ReleaseArtists: case SubsonicArtistListMode::ReleaseArtists:
parameters.setLinkType(TrackArtistLinkType::ReleaseArtist); parameters.setReleaseArtistsOnly(true);
break; break;
case SubsonicArtistListMode::TrackArtists: case SubsonicArtistListMode::TrackArtists:
parameters.setLinkType(TrackArtistLinkType::Artist); parameters.setTrackArtistLinkType(TrackArtistLinkType::Artist);
break; break;
} }
} }
parameters.filters.setMediaLibrary(mediaLibrary); parameters.filters.setMediaLibrary(mediaLibrary);
// This endpoint does not scale: make sort lived transactions in order not to block the whole application // This endpoint does not scale: make short lived transactions in order not to block the whole application
// first pass: dispatch the artists by first letter // first pass: dispatch the artists by first letter
LMS_LOG(API_SUBSONIC, DEBUG, "GetArtists: fetching all artists..."); LMS_LOG(API_SUBSONIC, DEBUG, "GetArtists: fetching all artists...");
@@ -419,12 +419,7 @@ namespace lms::api::subsonic
{ {
std::string_view sortName{ artist->getSortName() }; std::string_view sortName{ artist->getSortName() };
char sortChar; const char sortChar{ (sortName.empty() || !std::isalpha(sortName[0])) ? '#' : static_cast<char>(std::toupper(sortName[0])) };
if (sortName.empty() || !std::isalpha(sortName[0]))
sortChar = '#';
else
sortChar = std::toupper(sortName[0]);
artistsSortedByFirstChar[sortChar].push_back(artist->getId()); artistsSortedByFirstChar[sortChar].push_back(artist->getId());
} }
@@ -465,9 +460,18 @@ namespace lms::api::subsonic
Response response{ Response::createOkResponse(context.getServerProtocolVersion()) }; Response response{ Response::createOkResponse(context.getServerProtocolVersion()) };
Response::Node artistNode{ createArtistNode(context, artist) }; Response::Node artistNode{ createArtistNode(context, artist) };
const auto releases{ Release::find(context.getDbSession(), Release::FindParameters{}.setArtist(artist->getId())) }; auto addRelease{ [&](const Release::pointer& release) {
for (const Release::pointer& release : releases.results)
artistNode.addArrayChild("album", createAlbumNode(context, release, true /* id3 */)); artistNode.addArrayChild("album", createAlbumNode(context, release, true /* id3 */));
} };
Release::find(context.getDbSession(), Release::FindParameters{}.setArtist(artist->getId()), [&](const db::Release::pointer& release) {
addRelease(release);
});
Release::find(context.getDbSession(), Release::FindParameters{}.setTrackArtist(artist->getId()), [&](const db::Release::pointer& release) {
if (!release->hasArtist(id))
addRelease(release);
});
response.addNode("artist", std::move(artistNode)); response.addNode("artist", std::move(artistNode));
@@ -561,7 +565,7 @@ namespace lms::api::subsonic
}); });
} }
auto similarArtistsId{ core::Service<recommendation::IRecommendationService>::get()->getSimilarArtists(id, { TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist }, count) }; auto similarArtistsId{ core::Service<recommendation::IRecommendationService>::get()->getSimilarArtists(id, { TrackArtistLinkType::Artist }, count) };
{ {
auto transaction{ context.getDbSession().createReadTransaction() }; auto transaction{ context.getDbSession().createReadTransaction() };
@@ -26,6 +26,7 @@
#include "core/ILogger.hpp" #include "core/ILogger.hpp"
#include "core/IResourceHandler.hpp" #include "core/IResourceHandler.hpp"
#include "core/String.hpp" #include "core/String.hpp"
#include "core/media/Codec.hpp"
#include "core/media/MimeType.hpp" #include "core/media/MimeType.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
@@ -52,14 +53,15 @@ namespace lms::api::subsonic
{ {
struct OutputFormat struct OutputFormat
{ {
std::string name; std::string_view name;
core::media::Container container; core::media::Container container;
core::media::Codec codec; core::media::Codec codec;
}; };
constexpr std::array outputFormats{ constexpr std::array outputFormats{
OutputFormat{ "mp3", core::media::Container::MPEG, core::media::Codec::MP3 }, OutputFormat{ .name = "mp3", .container = core::media::Container::MPEG, .codec = core::media::Codec::MP3 },
OutputFormat{ "opus", core::media::Container::Ogg, core::media::Codec::Opus }, OutputFormat{ .name = "opus", .container = core::media::Container::Ogg, .codec = core::media::Codec::Opus },
OutputFormat{ "vorbis", core::media::Container::Ogg, core::media::Codec::Vorbis }, OutputFormat{ .name = "vorbis", .container = core::media::Container::Ogg, .codec = core::media::Codec::Vorbis },
OutputFormat{ .name = "flac", .container = core::media::Container::FLAC, .codec = core::media::Codec::FLAC },
}; };
std::optional<OutputFormat> getOutputFormatByName(std::string_view format) std::optional<OutputFormat> getOutputFormatByName(std::string_view format)
@@ -80,7 +82,7 @@ namespace lms::api::subsonic
return res; return res;
} }
std::optional<OutputFormat> userTranscodeFormatToOutputFormat(db::TranscodingOutputFormat format) OutputFormat userTranscodeFormatToOutputFormat(db::TranscodingOutputFormat format)
{ {
std::optional<OutputFormat> res; std::optional<OutputFormat> res;
@@ -97,7 +99,11 @@ namespace lms::api::subsonic
break; break;
} }
return res; if (!res)
throw InternalErrorGenericError{ "User's default transcoding format is invalid" };
LMS_LOG(API_SUBSONIC, DEBUG, "Using user's default transcoding format: " << res->name);
return *res;
} }
struct StreamParameters struct StreamParameters
@@ -119,44 +125,67 @@ namespace lms::api::subsonic
const AudioFileId audioId{ trackId ? AudioFileId{ *trackId } : AudioFileId{ *podcastEpisodeId } }; const AudioFileId audioId{ trackId ? AudioFileId{ *trackId } : AudioFileId{ *podcastEpisodeId } };
// Optional params // Optional params
std::size_t maxBitRate{ getParameterAs<std::size_t>(context.getParameters(), "maxBitRate").value_or(0) * 1000 }; // "If set to zero, no limit is imposed", given in kpbs const std::size_t maxBitRate{ getParameterAs<std::size_t>(context.getParameters(), "maxBitRate").value_or(0) * 1000 }; // "If set to zero, no limit is imposed", given in kpbs
const std::string format{ getParameterAs<std::string>(context.getParameters(), "format").value_or("") }; const std::string format{ getParameterAs<std::string>(context.getParameters(), "format").value_or("") };
std::size_t timeOffset{ getParameterAs<std::size_t>(context.getParameters(), "timeOffset").value_or(0) }; const std::size_t timeOffset{ getParameterAs<std::size_t>(context.getParameters(), "timeOffset").value_or(0) };
bool estimateContentLength{ getParameterAs<bool>(context.getParameters(), "estimateContentLength").value_or(false) }; const bool estimateContentLength{ getParameterAs<bool>(context.getParameters(), "estimateContentLength").value_or(false) };
const AudioFileInfo audioFileInfo{ getAudioFileInfo(context.getDbSession(), audioId) }; const AudioFileInfo audioFileInfo{ getAudioFileInfo(context.getDbSession(), audioId) };
StreamParameters parameters; StreamParameters parameters;
parameters.audioProperties = audioFileInfo.audioProperties;
parameters.filePath = audioFileInfo.path; parameters.filePath = audioFileInfo.path;
parameters.estimateContentLength = estimateContentLength; parameters.estimateContentLength = estimateContentLength;
if (format == "raw") // raw => no transcoding if (format == "raw") // raw => no transcoding
return parameters; // TODO: what if offset is not 0? return parameters; // TODO: what if offset is not 0?
std::optional<OutputFormat> requestedFormat{ getOutputFormatByName(format) }; std::optional<OutputFormat> requestedFormat;
if (!format.empty())
{
requestedFormat = getOutputFormatByName(format);
if (!requestedFormat)
LMS_LOG(API_SUBSONIC, ERROR, "Format '" << format << "' is not recognized: ignoring");
}
if (!requestedFormat) if (!requestedFormat)
{ {
if (context.getUser()->getSubsonicEnableTranscodingByDefault()) if (context.getUser()->getSubsonicEnableTranscodingByDefault())
requestedFormat = userTranscodeFormatToOutputFormat(context.getUser()->getSubsonicDefaultTranscodingOutputFormat()); requestedFormat = userTranscodeFormatToOutputFormat(context.getUser()->getSubsonicDefaultTranscodingOutputFormat());
} }
// Extra checks when requesting a lossless format
if (requestedFormat && core::media::getCodecDesc(requestedFormat->codec).isLossless)
{
if (!core::media::getCodecDesc(audioFileInfo.audioProperties.codec).isLossless && maxBitRate > 0)
throw BadParameterGenericError{ "maxBitRate", "Cannot limit bitrate when requesting a lossless format from a lossy source" };
if (core::media::getCodecDesc(audioFileInfo.audioProperties.codec).isLossless && maxBitRate > 0 && audioFileInfo.audioProperties.bitrate > maxBitRate)
throw BadParameterGenericError{ "maxBitRate", "Cannot honor maxBitRate, source is lossless with higher bitrate (" + std::to_string(audioFileInfo.audioProperties.bitrate) + " bps)" };
// we accept to transcode from a lossy format to a lossless one
}
if (!requestedFormat && (maxBitRate == 0 || audioFileInfo.audioProperties.bitrate <= maxBitRate)) if (!requestedFormat && (maxBitRate == 0 || audioFileInfo.audioProperties.bitrate <= maxBitRate))
{ {
LMS_LOG(API_SUBSONIC, DEBUG, "File's bitrate is compatible with parameters => no transcoding"); LMS_LOG(API_SUBSONIC, DEBUG, "File's bitrate (" << audioFileInfo.audioProperties.bitrate << " bps) is compatible with parameters => no transcoding");
return parameters; // no transcoding needed return parameters; // no transcoding needed
} }
// Check if the input file is compatible with the actual requested format // Check if the input file is compatible with the requested format
// same codec => apply max bitrate std::optional<std::size_t> bitrate;
// otherwise => apply default bitrate (because we can't really compare bitrates between formats) + max bitrate)
std::size_t bitrate{};
if (requestedFormat && requestedFormat->container == audioFileInfo.audioProperties.container && requestedFormat->codec == audioFileInfo.audioProperties.codec) if (requestedFormat && requestedFormat->container == audioFileInfo.audioProperties.container && requestedFormat->codec == audioFileInfo.audioProperties.codec)
{ {
// same codec => check if compatible with max bitrate
if (maxBitRate == 0 || audioFileInfo.audioProperties.bitrate <= maxBitRate) if (maxBitRate == 0 || audioFileInfo.audioProperties.bitrate <= maxBitRate)
{ {
LMS_LOG(API_SUBSONIC, DEBUG, "File's bitrate and format are compatible with parameters => no transcoding"); LMS_LOG(API_SUBSONIC, DEBUG, "File's bitrate (" << audioFileInfo.audioProperties.bitrate << " bps) and format are compatible with parameters => no transcoding");
return parameters; // no transcoding needed return parameters; // no transcoding needed
} }
// otherwise => apply requested bitrate
assert(maxBitRate > 0);
bitrate = maxBitRate; bitrate = maxBitRate;
} }
@@ -165,10 +194,26 @@ namespace lms::api::subsonic
if (!requestedFormat) // no format provided => use user's default if (!requestedFormat) // no format provided => use user's default
requestedFormat = userTranscodeFormatToOutputFormat(context.getUser()->getSubsonicDefaultTranscodingOutputFormat()); requestedFormat = userTranscodeFormatToOutputFormat(context.getUser()->getSubsonicDefaultTranscodingOutputFormat());
if (!bitrate) // no bitrate provided => use user's default
bitrate = context.getUser()->getSubsonicDefaultTranscodingOutputBitrate(); assert(requestedFormat);
if (maxBitRate) // but still honor provided maxBitRate, if any if (!core::media::getCodecDesc(requestedFormat->codec).isLossless)
bitrate = std::min<std::size_t>(bitrate, maxBitRate); {
// Try to keep a bitrate as close as possible to the source one
// If the source is lossless, we need to pick a default bitrate
if (core::media::getCodecDesc(audioFileInfo.audioProperties.codec).isLossless)
{
bitrate = context.getUser()->getSubsonicDefaultTranscodingOutputBitrate();
LMS_LOG(API_SUBSONIC, DEBUG, "Source is lossless, selecting user's default transcoding bitrate (" << *bitrate << " bps) for transcoding");
}
else
{
bitrate = audioFileInfo.audioProperties.bitrate;
LMS_LOG(API_SUBSONIC, DEBUG, "Source is lossy, selecting source bitrate (" << *bitrate << " bps) for transcoding");
}
if (maxBitRate > 0) // Honor provided maxBitRate, if any
bitrate = std::min<std::size_t>(*bitrate, maxBitRate);
}
transcodeParameters.inputParameters.filePath = audioFileInfo.path; transcodeParameters.inputParameters.filePath = audioFileInfo.path;
transcodeParameters.inputParameters.audioProperties = audioFileInfo.audioProperties; transcodeParameters.inputParameters.audioProperties = audioFileInfo.audioProperties;
@@ -181,6 +226,8 @@ namespace lms::api::subsonic
transcodeParameters.outputParameters.stripMetadata = false; // We want clients to use metadata (offline use, replay gain, etc.) transcodeParameters.outputParameters.stripMetadata = false; // We want clients to use metadata (offline use, replay gain, etc.)
LMS_LOG(API_SUBSONIC, DEBUG, "Transcoding to format '" << requestedFormat->name << "'" << (bitrate ? (" with bitrate " + std::to_string(*bitrate) + " bps") : ""));
return parameters; return parameters;
} }
} // namespace } // namespace
@@ -26,7 +26,7 @@
namespace lms::api::subsonic namespace lms::api::subsonic
{ {
struct RequestContext; class RequestContext;
Response handleGetTranscodeDecision(RequestContext& context); Response handleGetTranscodeDecision(RequestContext& context);
void handleGetTranscodeStream(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response); void handleGetTranscodeStream(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response);
@@ -43,6 +43,13 @@ namespace lms::api::subsonic
Clock::time_point addedTimePoint; Clock::time_point addedTimePoint;
AudioFileId audioFileId; AudioFileId audioFileId;
StreamDetails targetStreamInfo; StreamDetails targetStreamInfo;
Entry(Clock::time_point _addedTimePoint, AudioFileId _audioFileId, const StreamDetails& _targetStreamInfo)
: addedTimePoint{ _addedTimePoint }
, audioFileId{ _audioFileId }
, targetStreamInfo{ _targetStreamInfo }
{
}
}; };
virtual core::UUID add(AudioFileId audioFileId, const StreamDetails& targetStreamInfo) = 0; virtual core::UUID add(AudioFileId audioFileId, const StreamDetails& targetStreamInfo) = 0;
@@ -26,6 +26,7 @@
#include <Wt/Json/Parser.h> #include <Wt/Json/Parser.h>
#include "core/String.hpp" #include "core/String.hpp"
#include "core/Utils.hpp"
#include "SubsonicResponse.hpp" #include "SubsonicResponse.hpp"
@@ -61,7 +62,7 @@ namespace lms::api::subsonic
} }
else else
{ {
static_assert(false, "Unhandled type"); static_assert(core::utils::dependent_false_v<T>, "Unhandled type");
} }
return static_cast<T>(value); return static_cast<T>(value);
@@ -105,7 +106,7 @@ namespace lms::api::subsonic
} }
else else
{ {
static_assert(false, "Unhandled type"); static_assert(core::utils::dependent_false_v<T>, "Unhandled type");
} }
res.emplace_back(static_cast<T>(item)); res.emplace_back(static_cast<T>(item));
+31 -15
View File
@@ -29,6 +29,7 @@
#include "database/objects/Directory.hpp" #include "database/objects/Directory.hpp"
#include "database/objects/Medium.hpp" #include "database/objects/Medium.hpp"
#include "database/objects/Release.hpp" #include "database/objects/Release.hpp"
#include "database/objects/ReleaseArtistLink.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "database/objects/User.hpp" #include "database/objects/User.hpp"
#include "services/feedback/IFeedbackService.hpp" #include "services/feedback/IFeedbackService.hpp"
@@ -99,23 +100,38 @@ namespace lms::api::subsonic
else if (const auto year{ release->getYear() }) else if (const auto year{ release->getYear() })
albumNode.setAttribute("year", *year); albumNode.setAttribute("year", *year);
auto artists{ release->getReleaseArtists() }; struct Artist
if (artists.empty())
artists = release->getArtists();
if (!artists.empty())
{ {
if (!release->getArtistDisplayName().empty()) std::string name;
albumNode.setAttribute("artist", release->getArtistDisplayName()); std::string id;
else };
albumNode.setAttribute("artist", utils::joinArtistNames(artists)); std::optional<Artist> artist;
if (artists.size() == 1) const auto artistLinks{ release->getArtistLinks() };
if (!artistLinks.empty())
{
artist = Artist{ .name = std::string{ release->getArtistDisplayName() }, .id = {} };
if (artistLinks.size() == 1)
artist->id = idToString(artistLinks.front()->getArtistId());
}
else
{
if (const auto trackArtists{ release->getTrackArtists(TrackArtistLinkType::Artist) }; !trackArtists.empty())
{ {
albumNode.setAttribute("artistId", idToString(artists.front()->getId())); if (trackArtists.size() > 1)
artist = Artist{ .name = "Various Artists", .id = {} };
else if (trackArtists.size() == 1)
artist = Artist{ .name = trackArtists.front()->getName(), .id = idToString(trackArtists.front()->getId()) };
} }
} }
if (artist)
{
albumNode.setAttribute("artist", artist->name);
if (!artist->id.empty())
albumNode.setAttribute("artistId", artist->id);
}
albumNode.setAttribute("playCount", core::Service<scrobbling::IScrobblingService>::get()->getCount(context.getUser()->getId(), release->getId())); albumNode.setAttribute("playCount", core::Service<scrobbling::IScrobblingService>::get()->getCount(context.getUser()->getId(), release->getId()));
// Report the first GENRE for this track // Report the first GENRE for this track
@@ -182,16 +198,16 @@ namespace lms::api::subsonic
if (id3) if (id3)
{ {
albumNode.createEmptyArrayChild("artists"); albumNode.createEmptyArrayChild("artists");
for (const Artist::pointer& artist : release->getReleaseArtists()) for (const db::ReleaseArtistLink::pointer& artistLink : artistLinks)
albumNode.addArrayChild("artists", createArtistNode(artist)); albumNode.addArrayChild("artists", createMinimalArtistNode(artistLink));
albumNode.setAttribute("displayArtist", release->getArtistDisplayName()); albumNode.setAttribute("displayArtist", release->getArtistDisplayName());
} }
else else
{ {
albumNode.createEmptyArrayChild("albumArtists"); albumNode.createEmptyArrayChild("albumArtists");
for (const Artist::pointer& artist : release->getReleaseArtists()) for (const db::ReleaseArtistLink::pointer& artistLink : artistLinks)
albumNode.addArrayChild("albumArtists", createArtistNode(artist)); albumNode.addArrayChild("albumArtists", createMinimalArtistNode(artistLink));
albumNode.setAttribute("displayAlbumArtist", release->getArtistDisplayName()); albumNode.setAttribute("displayAlbumArtist", release->getArtistDisplayName());
+34 -6
View File
@@ -26,6 +26,7 @@
#include "database/objects/Artist.hpp" #include "database/objects/Artist.hpp"
#include "database/objects/Artwork.hpp" #include "database/objects/Artwork.hpp"
#include "database/objects/Release.hpp" #include "database/objects/Release.hpp"
#include "database/objects/ReleaseArtistLink.hpp"
#include "database/objects/TrackArtistLink.hpp" #include "database/objects/TrackArtistLink.hpp"
#include "database/objects/User.hpp" #include "database/objects/User.hpp"
#include "services/feedback/IFeedbackService.hpp" #include "services/feedback/IFeedbackService.hpp"
@@ -66,18 +67,25 @@ namespace lms::api::subsonic
{ {
LMS_SCOPED_TRACE_DETAILED("Subsonic", "CreateArtist"); LMS_SCOPED_TRACE_DETAILED("Subsonic", "CreateArtist");
Response::Node artistNode{ createArtistNode(artist) }; Response::Node artistNode{ createMinimalArtistNode(artist) };
artistNode.setAttribute("id", idToString(artist->getId()));
artistNode.setAttribute("name", artist->getName());
if (const auto artwork{ artist->getPreferredArtwork() }) if (const auto artwork{ artist->getPreferredArtwork() })
{ {
CoverArtId coverArtId{ artwork->getId(), artwork->getLastWrittenTime().toTime_t() }; CoverArtId coverArtId{ artwork->getId(), artwork->getLastWrittenTime().toTime_t() };
artistNode.setAttribute("coverArt", idToString(coverArtId)); artistNode.setAttribute("coverArt", idToString(coverArtId));
} }
const std::size_t count{ Release::getCount(context.getDbSession(), Release::FindParameters{}.setArtist(artist->getId())) }; {
artistNode.setAttribute("albumCount", count); std::size_t count{ Release::getCount(context.getDbSession(), Release::FindParameters{}.setArtist(artist->getId())) };
// TODO: not very efficient
Release::find(context.getDbSession(), Release::FindParameters{}.setTrackArtist(artist->getId()), [&](const db::Release::pointer& release) {
if (!release->hasArtist(artist->getId()))
count++;
});
artistNode.setAttribute("albumCount", count);
}
if (const Wt::WDateTime dateTime{ core::Service<feedback::IFeedbackService>::get()->getStarredDateTime(context.getUser()->getId(), artist->getId()) }; dateTime.isValid()) if (const Wt::WDateTime dateTime{ core::Service<feedback::IFeedbackService>::get()->getStarredDateTime(context.getUser()->getId(), artist->getId()) }; dateTime.isValid())
artistNode.setAttribute("starred", core::stringUtils::toISO8601String(dateTime)); artistNode.setAttribute("starred", core::stringUtils::toISO8601String(dateTime));
@@ -107,7 +115,7 @@ namespace lms::api::subsonic
return artistNode; return artistNode;
} }
Response::Node createArtistNode(const Artist::pointer& artist) Response::Node createMinimalArtistNode(const Artist::pointer& artist)
{ {
Response::Node artistNode; Response::Node artistNode;
@@ -116,4 +124,24 @@ namespace lms::api::subsonic
return artistNode; return artistNode;
} }
Response::Node createMinimalArtistNode(const db::ObjectPtr<db::ReleaseArtistLink>& artistLink)
{
Response::Node artistNode;
artistNode.setAttribute("id", idToString(artistLink->getArtistId()));
artistNode.setAttribute("name", artistLink->getArtistName());
return artistNode;
}
Response::Node createMinimalArtistNode(const db::ObjectPtr<db::TrackArtistLink>& artistLink)
{
Response::Node artistNode;
artistNode.setAttribute("id", idToString(artistLink->getArtistId()));
artistNode.setAttribute("name", artistLink->getArtistName());
return artistNode;
}
} // namespace lms::api::subsonic } // namespace lms::api::subsonic
+5 -1
View File
@@ -30,6 +30,8 @@
namespace lms::db namespace lms::db
{ {
class Artist; class Artist;
class ReleaseArtistLink;
class TrackArtistLink;
class User; class User;
class Session; class Session;
} // namespace lms::db } // namespace lms::db
@@ -45,5 +47,7 @@ namespace lms::api::subsonic
} // namespace utils } // namespace utils
Response::Node createArtistNode(RequestContext& context, const db::ObjectPtr<db::Artist>& artist); Response::Node createArtistNode(RequestContext& context, const db::ObjectPtr<db::Artist>& artist);
Response::Node createArtistNode(const db::ObjectPtr<db::Artist>& artist); // only minimal info Response::Node createMinimalArtistNode(const db::ObjectPtr<db::Artist>& artist); // only minimal info
Response::Node createMinimalArtistNode(const db::ObjectPtr<db::ReleaseArtistLink>& artistLink); // only minimal info
Response::Node createMinimalArtistNode(const db::ObjectPtr<db::TrackArtistLink>& artistLink); // only minimal info
} // namespace lms::api::subsonic } // namespace lms::api::subsonic
@@ -27,14 +27,14 @@
namespace lms::api::subsonic namespace lms::api::subsonic
{ {
Response::Node createContributorNode(const db::ObjectPtr<db::TrackArtistLink>& trackArtistLink, const db::ObjectPtr<db::Artist>& artist) Response::Node createContributorNode(const db::ObjectPtr<db::TrackArtistLink>& trackArtistLink)
{ {
Response::Node contributorNode; Response::Node contributorNode;
contributorNode.setAttribute("role", utils::toString(trackArtistLink->getType())); contributorNode.setAttribute("role", utils::toString(trackArtistLink->getType()));
if (!trackArtistLink->getSubType().empty()) if (!trackArtistLink->getSubType().empty())
contributorNode.setAttribute("subRole", trackArtistLink->getSubType()); contributorNode.setAttribute("subRole", trackArtistLink->getSubType());
contributorNode.addChild("artist", createArtistNode(artist)); contributorNode.addChild("artist", createMinimalArtistNode(trackArtistLink));
return contributorNode; return contributorNode;
} }
@@ -31,5 +31,5 @@ namespace lms::db
namespace lms::api::subsonic namespace lms::api::subsonic
{ {
Response::Node createContributorNode(const db::ObjectPtr<db::TrackArtistLink>& trackArtistLink, const db::ObjectPtr<db::Artist>& artist); Response::Node createContributorNode(const db::ObjectPtr<db::TrackArtistLink>& trackArtistLink);
} }
+14 -8
View File
@@ -27,6 +27,7 @@
#include "core/MimeTypes.hpp" #include "core/MimeTypes.hpp"
#include "core/Service.hpp" #include "core/Service.hpp"
#include "core/String.hpp" #include "core/String.hpp"
#include "database/Types.hpp" #include "database/Types.hpp"
#include "database/objects/Artist.hpp" #include "database/objects/Artist.hpp"
#include "database/objects/Artwork.hpp" #include "database/objects/Artwork.hpp"
@@ -35,6 +36,7 @@
#include "database/objects/MediaLibrary.hpp" #include "database/objects/MediaLibrary.hpp"
#include "database/objects/Medium.hpp" #include "database/objects/Medium.hpp"
#include "database/objects/Release.hpp" #include "database/objects/Release.hpp"
#include "database/objects/ReleaseArtistLink.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "database/objects/TrackArtistLink.hpp" #include "database/objects/TrackArtistLink.hpp"
#include "database/objects/User.hpp" #include "database/objects/User.hpp"
@@ -194,23 +196,27 @@ namespace lms::api::subsonic
} }
{ {
trackResponse.createEmptyArrayChild("albumartists"); trackResponse.createEmptyArrayChild("albumArtists");
trackResponse.createEmptyArrayChild("artists"); trackResponse.createEmptyArrayChild("artists");
trackResponse.createEmptyArrayChild("contributors"); trackResponse.createEmptyArrayChild("contributors");
db::TrackArtistLink::find(context.getDbSession(), track->getId(), [&](const db::TrackArtistLink::pointer& link, const db::Artist::pointer& artist) { track->visitArtistLinks([&](const db::TrackArtistLink::pointer& artistLink) {
switch (link->getType()) switch (artistLink->getType())
{ {
case db::TrackArtistLinkType::Artist: case db::TrackArtistLinkType::Artist:
trackResponse.addArrayChild("artists", createArtistNode(artist)); trackResponse.addArrayChild("artists", createMinimalArtistNode(artistLink));
break;
case db::TrackArtistLinkType::ReleaseArtist:
trackResponse.addArrayChild("albumartists", createArtistNode(artist));
break; break;
default: default:
trackResponse.addArrayChild("contributors", createContributorNode(link, artist)); trackResponse.addArrayChild("contributors", createContributorNode(artistLink));
} }
}); });
if (release)
{
release->visitArtistLinks([&](const db::ReleaseArtistLink::pointer& artistLink) {
trackResponse.addArrayChild("albumArtists", createMinimalArtistNode(artistLink));
});
}
} }
trackResponse.setAttribute("displayArtist", track->getArtistDisplayName()); trackResponse.setAttribute("displayArtist", track->getArtistDisplayName());
+55 -64
View File
@@ -100,9 +100,9 @@ namespace lms::api::subsonic
.platform = "TestPlatform", .platform = "TestPlatform",
.maxAudioBitrate = 512'000, .maxAudioBitrate = 512'000,
.maxTranscodingAudioBitrate = 256'000, .maxTranscodingAudioBitrate = 256'000,
.directPlayProfiles = { { .directPlayProfiles = {
{ .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = 2 }, { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = 2 },
} }, },
.transcodingProfiles = { .transcodingProfiles = {
{ .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 }, { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 },
}, },
@@ -130,9 +130,9 @@ namespace lms::api::subsonic
.platform = "TestPlatform", .platform = "TestPlatform",
.maxAudioBitrate = 512'000, .maxAudioBitrate = 512'000,
.maxTranscodingAudioBitrate = 96'000, .maxTranscodingAudioBitrate = 96'000,
.directPlayProfiles = { { .directPlayProfiles = {
{ .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = 2 }, { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = 2 },
} }, },
.transcodingProfiles = { .transcodingProfiles = {
{ .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 }, { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 },
}, },
@@ -160,9 +160,9 @@ namespace lms::api::subsonic
.platform = "TestPlatform", .platform = "TestPlatform",
.maxAudioBitrate = 96'000, .maxAudioBitrate = 96'000,
.maxTranscodingAudioBitrate = 96'000, .maxTranscodingAudioBitrate = 96'000,
.directPlayProfiles = { { .directPlayProfiles = {
{ .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = 2 }, { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = 2 },
} }, },
.transcodingProfiles = { .transcodingProfiles = {
{ .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 }, { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 },
}, },
@@ -190,9 +190,9 @@ namespace lms::api::subsonic
.platform = "TestPlatform", .platform = "TestPlatform",
.maxAudioBitrate = 96'000, .maxAudioBitrate = 96'000,
.maxTranscodingAudioBitrate = 96'000, .maxTranscodingAudioBitrate = 96'000,
.directPlayProfiles = { { .directPlayProfiles = {
{ .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = 2 }, { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = 2 },
} }, },
.transcodingProfiles = { .transcodingProfiles = {
{ .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 }, { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 },
}, },
@@ -220,9 +220,9 @@ namespace lms::api::subsonic
.platform = "TestPlatform", .platform = "TestPlatform",
.maxAudioBitrate = 320'000, .maxAudioBitrate = 320'000,
.maxTranscodingAudioBitrate = 320'000, .maxTranscodingAudioBitrate = 320'000,
.directPlayProfiles = { { .directPlayProfiles = {
{ .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = 2 }, { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = 2 },
} }, },
.transcodingProfiles = { .transcodingProfiles = {
{ .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 }, { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 },
}, },
@@ -250,7 +250,7 @@ namespace lms::api::subsonic
.platform = "TestPlatform", .platform = "TestPlatform",
.maxAudioBitrate = 320'000, .maxAudioBitrate = 320'000,
.maxTranscodingAudioBitrate = 320'000, .maxTranscodingAudioBitrate = 320'000,
.directPlayProfiles = { { { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = 2 } } }, .directPlayProfiles = { { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = 2 } },
.transcodingProfiles = { { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 } }, .transcodingProfiles = { { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 } },
.codecProfiles = { { .type = "AudioCodec", .name = "mp3", .limitations = {} } }, .codecProfiles = { { .type = "AudioCodec", .name = "mp3", .limitations = {} } },
}, },
@@ -274,9 +274,9 @@ namespace lms::api::subsonic
.platform = "TestPlatform", .platform = "TestPlatform",
.maxAudioBitrate = 320'000, .maxAudioBitrate = 320'000,
.maxTranscodingAudioBitrate = 320'000, .maxTranscodingAudioBitrate = 320'000,
.directPlayProfiles = { { .directPlayProfiles = {
{ .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = std::nullopt }, { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = std::nullopt },
} }, },
.transcodingProfiles = { .transcodingProfiles = {
{ .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = std::nullopt }, { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = std::nullopt },
}, },
@@ -302,9 +302,9 @@ namespace lms::api::subsonic
.platform = "TestPlatform", .platform = "TestPlatform",
.maxAudioBitrate = 320'000, .maxAudioBitrate = 320'000,
.maxTranscodingAudioBitrate = 320'000, .maxTranscodingAudioBitrate = 320'000,
.directPlayProfiles = { { .directPlayProfiles = {
{ .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = std::nullopt }, { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = std::nullopt },
} }, },
.transcodingProfiles = { .transcodingProfiles = {
{ .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = std::nullopt }, { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = std::nullopt },
}, },
@@ -330,9 +330,9 @@ namespace lms::api::subsonic
.platform = "TestPlatform", .platform = "TestPlatform",
.maxAudioBitrate = 1'000'000, .maxAudioBitrate = 1'000'000,
.maxTranscodingAudioBitrate = 320'000, .maxTranscodingAudioBitrate = 320'000,
.directPlayProfiles = { { .directPlayProfiles = {
{ .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = std::nullopt }, { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = std::nullopt },
} }, },
.transcodingProfiles = { .transcodingProfiles = {
{ .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = std::nullopt }, { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = std::nullopt },
}, },
@@ -356,7 +356,6 @@ namespace lms::api::subsonic
.clientInfo = { .name = "TestClient", .platform = "TestPlatform", .maxAudioBitrate = std::nullopt, .maxTranscodingAudioBitrate = std::nullopt, .directPlayProfiles = { { .clientInfo = { .name = "TestClient", .platform = "TestPlatform", .maxAudioBitrate = std::nullopt, .maxTranscodingAudioBitrate = std::nullopt, .directPlayProfiles = { {
{ .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = std::nullopt }, { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = { "http" }, .maxAudioChannels = std::nullopt },
} }, } },
.transcodingProfiles = { .transcodingProfiles = {
{ .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = std::nullopt }, { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = std::nullopt },
}, },
@@ -381,9 +380,9 @@ namespace lms::api::subsonic
.platform = "TestPlatform", .platform = "TestPlatform",
.maxAudioBitrate = 1'000'000, .maxAudioBitrate = 1'000'000,
.maxTranscodingAudioBitrate = 320'000, .maxTranscodingAudioBitrate = 320'000,
.directPlayProfiles = { { .directPlayProfiles = {
{ .containers = { "mp4", "flac", "mp3" }, .audioCodecs = {}, .protocols = {}, .maxAudioChannels = std::nullopt }, { .containers = { "mp4", "flac", "mp3" }, .audioCodecs = {}, .protocols = {}, .maxAudioChannels = std::nullopt },
} }, },
.transcodingProfiles = {}, .transcodingProfiles = {},
.codecProfiles = {}, .codecProfiles = {},
}, },
@@ -407,9 +406,9 @@ namespace lms::api::subsonic
.platform = "TestPlatform", .platform = "TestPlatform",
.maxAudioBitrate = 1'000'000, .maxAudioBitrate = 1'000'000,
.maxTranscodingAudioBitrate = 320'000, .maxTranscodingAudioBitrate = 320'000,
.directPlayProfiles = { { .directPlayProfiles = {
{ .containers = {}, .audioCodecs = { "mp3" }, .protocols = {}, .maxAudioChannels = std::nullopt }, { .containers = {}, .audioCodecs = { "mp3" }, .protocols = {}, .maxAudioChannels = std::nullopt },
} }, },
.transcodingProfiles = {}, .transcodingProfiles = {},
.codecProfiles = {}, .codecProfiles = {},
}, },
@@ -433,10 +432,10 @@ namespace lms::api::subsonic
.platform = "Android", .platform = "Android",
.maxAudioBitrate = 320'000, .maxAudioBitrate = 320'000,
.maxTranscodingAudioBitrate = 320'000, .maxTranscodingAudioBitrate = 320'000,
.directPlayProfiles = { { .directPlayProfiles = {
{ .containers = { "flac" }, .audioCodecs = { "flac" }, .protocols = {}, .maxAudioChannels = 32 }, { .containers = { "flac" }, .audioCodecs = { "flac" }, .protocols = {}, .maxAudioChannels = 32 },
} }, },
.transcodingProfiles = { { { .container = "ogg", .audioCodec = "opus", .protocol = "http", .maxAudioChannels = std::nullopt }, { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 } } }, .transcodingProfiles = { { .container = "ogg", .audioCodec = "opus", .protocol = "http", .maxAudioChannels = std::nullopt }, { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 } },
.codecProfiles = {}, .codecProfiles = {},
}, },
.source = { .source = {
@@ -459,22 +458,20 @@ namespace lms::api::subsonic
.platform = "UPnP", .platform = "UPnP",
.maxAudioBitrate = 1'000'000, .maxAudioBitrate = 1'000'000,
.maxTranscodingAudioBitrate = 1'000'000, .maxTranscodingAudioBitrate = 1'000'000,
.directPlayProfiles = { { .directPlayProfiles = {
{ .containers = { "flac" }, .audioCodecs = {}, .protocols = {}, .maxAudioChannels = std::nullopt }, { .containers = { "flac" }, .audioCodecs = {}, .protocols = {}, .maxAudioChannels = std::nullopt },
{ .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = {}, .maxAudioChannels = std::nullopt }, { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = {}, .maxAudioChannels = std::nullopt },
{ .containers = { "m4a", "mp4" }, .audioCodecs = { "aac" }, .protocols = {}, .maxAudioChannels = std::nullopt }, { .containers = { "m4a", "mp4" }, .audioCodecs = { "aac" }, .protocols = {}, .maxAudioChannels = std::nullopt },
} }, },
.transcodingProfiles = { { .transcodingProfiles = {
{ .container = "flac", .audioCodec = "flac", .protocol = "http", .maxAudioChannels = 6 }, { .container = "flac", .audioCodec = "flac", .protocol = "http", .maxAudioChannels = 6 },
{ .container = "aac", .audioCodec = "aac", .protocol = "http", .maxAudioChannels = 6 }, { .container = "aac", .audioCodec = "aac", .protocol = "http", .maxAudioChannels = 6 },
{ .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 }, { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 },
} }, },
.codecProfiles = { .codecProfiles = {
{ { .type = "AudioCodec", .name = "flac", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } },
{ .type = "AudioCodec", .name = "flac", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } }, { .type = "AudioCodec", .name = "vorbis", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } },
{ .type = "AudioCodec", .name = "vorbis", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } }, { .type = "AudioCodec", .name = "opus", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } },
{ .type = "AudioCodec", .name = "opus", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } },
},
}, },
}, },
.source = { .source = {
@@ -497,23 +494,21 @@ namespace lms::api::subsonic
.platform = "UPnP", .platform = "UPnP",
.maxAudioBitrate = std::nullopt, .maxAudioBitrate = std::nullopt,
.maxTranscodingAudioBitrate = std::nullopt, .maxTranscodingAudioBitrate = std::nullopt,
.directPlayProfiles = { { .directPlayProfiles = {
{ .containers = { "opus", "ogg", "oga", "aac", "webma", "webm", "wav", "flac", "mka" }, .audioCodecs = {}, .protocols = {}, .maxAudioChannels = std::nullopt }, { .containers = { "opus", "ogg", "oga", "aac", "webma", "webm", "wav", "flac", "mka" }, .audioCodecs = {}, .protocols = {}, .maxAudioChannels = std::nullopt },
{ .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = {}, .maxAudioChannels = std::nullopt }, { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = {}, .maxAudioChannels = std::nullopt },
{ .containers = { "m4a", "mp4" }, .audioCodecs = { "aac" }, .protocols = {}, .maxAudioChannels = std::nullopt }, { .containers = { "m4a", "mp4" }, .audioCodecs = { "aac" }, .protocols = {}, .maxAudioChannels = std::nullopt },
} }, },
.transcodingProfiles = { { .transcodingProfiles = {
{ .container = "flac", .audioCodec = "flac", .protocol = "http", .maxAudioChannels = 6 }, { .container = "flac", .audioCodec = "flac", .protocol = "http", .maxAudioChannels = 6 },
{ .container = "mp4", .audioCodec = "aac", .protocol = "http", .maxAudioChannels = 6 }, { .container = "mp4", .audioCodec = "aac", .protocol = "http", .maxAudioChannels = 6 },
{ .container = "aac", .audioCodec = "aac", .protocol = "http", .maxAudioChannels = 6 }, { .container = "aac", .audioCodec = "aac", .protocol = "http", .maxAudioChannels = 6 },
{ .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 }, { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 },
} }, },
.codecProfiles = { .codecProfiles = {
{ { .type = "AudioCodec", .name = "flac", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } },
{ .type = "AudioCodec", .name = "flac", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } }, { .type = "AudioCodec", .name = "vorbis", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } },
{ .type = "AudioCodec", .name = "vorbis", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } }, { .type = "AudioCodec", .name = "opus", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } },
{ .type = "AudioCodec", .name = "opus", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } },
},
}, },
}, },
.source = { .source = {
@@ -536,22 +531,20 @@ namespace lms::api::subsonic
.platform = "UPnP", .platform = "UPnP",
.maxAudioBitrate = 1'000'000, .maxAudioBitrate = 1'000'000,
.maxTranscodingAudioBitrate = 1'000'000, .maxTranscodingAudioBitrate = 1'000'000,
.directPlayProfiles = { { .directPlayProfiles = {
{ .containers = { "flac" }, .audioCodecs = {}, .protocols = {}, .maxAudioChannels = std::nullopt }, { .containers = { "flac" }, .audioCodecs = {}, .protocols = {}, .maxAudioChannels = std::nullopt },
{ .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = {}, .maxAudioChannels = std::nullopt }, { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = {}, .maxAudioChannels = std::nullopt },
{ .containers = { "m4a", "mp4" }, .audioCodecs = { "aac" }, .protocols = {}, .maxAudioChannels = std::nullopt }, { .containers = { "m4a", "mp4" }, .audioCodecs = { "aac" }, .protocols = {}, .maxAudioChannels = std::nullopt },
} }, },
.transcodingProfiles = { { .transcodingProfiles = {
{ .container = "flac", .audioCodec = "flac", .protocol = "http", .maxAudioChannels = 6 }, { .container = "flac", .audioCodec = "flac", .protocol = "http", .maxAudioChannels = 6 },
{ .container = "aac", .audioCodec = "aac", .protocol = "http", .maxAudioChannels = 6 }, { .container = "aac", .audioCodec = "aac", .protocol = "http", .maxAudioChannels = 6 },
{ .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 }, { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 },
} }, },
.codecProfiles = { .codecProfiles = {
{ { .type = "AudioCodec", .name = "flac", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } },
{ .type = "AudioCodec", .name = "flac", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } }, { .type = "AudioCodec", .name = "vorbis", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } },
{ .type = "AudioCodec", .name = "vorbis", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } }, { .type = "AudioCodec", .name = "opus", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } },
{ .type = "AudioCodec", .name = "opus", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } },
},
}, },
}, },
.source = { .source = {
@@ -574,22 +567,20 @@ namespace lms::api::subsonic
.platform = "UPnP", .platform = "UPnP",
.maxAudioBitrate = 1'000'000, .maxAudioBitrate = 1'000'000,
.maxTranscodingAudioBitrate = 1'000'000, .maxTranscodingAudioBitrate = 1'000'000,
.directPlayProfiles = { { .directPlayProfiles = {
{ .containers = { "flac" }, .audioCodecs = {}, .protocols = {}, .maxAudioChannels = std::nullopt }, { .containers = { "flac" }, .audioCodecs = {}, .protocols = {}, .maxAudioChannels = std::nullopt },
{ .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = {}, .maxAudioChannels = std::nullopt }, { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = {}, .maxAudioChannels = std::nullopt },
{ .containers = { "m4a", "mp4" }, .audioCodecs = { "aac" }, .protocols = {}, .maxAudioChannels = std::nullopt }, { .containers = { "m4a", "mp4" }, .audioCodecs = { "aac" }, .protocols = {}, .maxAudioChannels = std::nullopt },
} }, },
.transcodingProfiles = { { .transcodingProfiles = {
{ .container = "flac", .audioCodec = "flac", .protocol = "http", .maxAudioChannels = 6 }, { .container = "flac", .audioCodec = "flac", .protocol = "http", .maxAudioChannels = 6 },
{ .container = "aac", .audioCodec = "aac", .protocol = "http", .maxAudioChannels = 6 }, { .container = "aac", .audioCodec = "aac", .protocol = "http", .maxAudioChannels = 6 },
{ .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 }, { .container = "mp3", .audioCodec = "mp3", .protocol = "http", .maxAudioChannels = 2 },
} }, },
.codecProfiles = { .codecProfiles = {
{ { .type = "AudioCodec", .name = "flac", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } },
{ .type = "AudioCodec", .name = "flac", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } }, { .type = "AudioCodec", .name = "vorbis", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } },
{ .type = "AudioCodec", .name = "vorbis", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } }, { .type = "AudioCodec", .name = "opus", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } },
{ .type = "AudioCodec", .name = "opus", .limitations = { { .name = Limitation::Type::AudioSamplerate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "48000" }, .required = true } } },
},
}, },
}, },
.source = { .source = {
@@ -612,11 +603,11 @@ namespace lms::api::subsonic
.platform = "TestPlatform", .platform = "TestPlatform",
.maxAudioBitrate = 512'000, .maxAudioBitrate = 512'000,
.maxTranscodingAudioBitrate = 96'000, .maxTranscodingAudioBitrate = 96'000,
.directPlayProfiles = { { .directPlayProfiles = {
{ .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = {}, .maxAudioChannels = 2 }, { .containers = { "mp3" }, .audioCodecs = { "mp3" }, .protocols = {}, .maxAudioChannels = 2 },
} }, },
.transcodingProfiles = { .transcodingProfiles = {
{ { .container = "mp3", .audioCodec = "mp3", .protocol = { "http" }, .maxAudioChannels = 2 } }, { .container = "mp3", .audioCodec = "mp3", .protocol = { "http" }, .maxAudioChannels = 2 },
}, },
.codecProfiles = { { .type = "AudioCodec", .name = "mp3", .limitations = { .codecProfiles = { { .type = "AudioCodec", .name = "mp3", .limitations = {
{ .name = Limitation::Type::AudioBitrate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "96000" }, .required = true }, { .name = Limitation::Type::AudioBitrate, .comparison = Limitation::ComparisonOperator::LessThanEqual, .values = { "96000" }, .required = true },
+1
View File
@@ -37,6 +37,7 @@ add_executable(lms
ui/common/UUIDValidator.cpp ui/common/UUIDValidator.cpp
ui/explore/ArtistCollector.cpp ui/explore/ArtistCollector.cpp
ui/explore/ArtistListHelpers.cpp ui/explore/ArtistListHelpers.cpp
ui/explore/ArtistType.cpp
ui/explore/ArtistView.cpp ui/explore/ArtistView.cpp
ui/explore/ArtistsView.cpp ui/explore/ArtistsView.cpp
ui/explore/DatabaseCollectorBase.cpp ui/explore/DatabaseCollectorBase.cpp
+11 -18
View File
@@ -35,6 +35,7 @@
#include "database/objects/Release.hpp" #include "database/objects/Release.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "database/objects/TrackList.hpp" #include "database/objects/TrackList.hpp"
#include "database/objects/Types.hpp"
#include "database/objects/User.hpp" #include "database/objects/User.hpp"
#include "LmsApplication.hpp" #include "LmsApplication.hpp"
@@ -194,7 +195,7 @@ namespace lms::ui
_audioFileResource = std::make_unique<AudioFileResource>(); _audioFileResource = std::make_unique<AudioFileResource>();
_title = bindNew<Wt::WText>("title"); _title = bindNew<Wt::WText>("title");
_artist = bindNew<Wt::WAnchor>("artist"); _artists = bindNew<Wt::WContainerWidget>("artist");
_release = bindNew<Wt::WAnchor>("release"); _release = bindNew<Wt::WAnchor>("release");
_separator = bindNew<Wt::WText>("separator"); _separator = bindNew<Wt::WText>("separator");
_playQueue = bindNew<Wt::WPushButton>("playqueue-btn", Wt::WString::tr("Lms.MediaPlayer.template.playqueue-btn").arg(0), Wt::TextFormat::XHTML); _playQueue = bindNew<Wt::WPushButton>("playqueue-btn", Wt::WString::tr("Lms.MediaPlayer.template.playqueue-btn").arg(0), Wt::TextFormat::XHTML);
@@ -237,10 +238,12 @@ namespace lms::ui
if (!track) if (!track)
return; return;
const auto release{ track->getRelease() };
const std::string transcodingResource{ _audioTranscodingResource->getUrl(trackId) }; const std::string transcodingResource{ _audioTranscodingResource->getUrl(trackId) };
const std::string nativeResource{ _audioFileResource->getUrl(trackId) }; const std::string nativeResource{ _audioFileResource->getUrl(trackId) };
const auto artists{ track->getArtists({ db::TrackArtistLinkType::Artist }) }; const auto artistDisplayInfo{ utils::computeArtistDisplayInfo(track, db::TrackArtistLinkType::Artist) };
oss oss
<< "var params = {" << "var params = {"
@@ -250,8 +253,8 @@ namespace lms::ui
<< " duration: " << std::chrono::duration_cast<std::chrono::duration<float>>(track->getDuration()).count() << "," << " duration: " << std::chrono::duration_cast<std::chrono::duration<float>>(track->getDuration()).count() << ","
<< " replayGain: " << replayGain << "," << " replayGain: " << replayGain << ","
<< " title: \"" << core::stringUtils::jsEscape(track->getName()) << "\"," << " title: \"" << core::stringUtils::jsEscape(track->getName()) << "\","
<< " artist: \"" << (!artists.empty() ? core::stringUtils::jsEscape(track->getArtistDisplayName()) : "") << "\"," << " artist: \"" << (!artistDisplayInfo.displayName.empty() ? core::stringUtils::jsEscape(track->getArtistDisplayName()) : "") << "\","
<< " release: \"" << (track->getRelease() ? core::stringUtils::jsEscape(track->getRelease()->getName()) : "") << "\","; << " release: \"" << (release ? core::stringUtils::jsEscape(release->getName()) : "") << "\",";
db::ArtworkId artworkId{ track->getPreferredMediaArtworkId() }; db::ArtworkId artworkId{ track->getPreferredMediaArtworkId() };
if (!artworkId.isValid()) if (!artworkId.isValid())
@@ -281,22 +284,12 @@ namespace lms::ui
_title->setTextFormat(Wt::TextFormat::Plain); _title->setTextFormat(Wt::TextFormat::Plain);
_title->setText(Wt::WString::fromUTF8(track->getName())); _title->setText(Wt::WString::fromUTF8(track->getName()));
bool needSeparator{ true }; _artists->clear();
_artists->addWidget(utils::createArtistsAnchors(artistDisplayInfo));
if (!artists.empty()) bool needSeparator{ !artistDisplayInfo.displayName.empty() };
{
_artist->setTextFormat(Wt::TextFormat::Plain);
_artist->setText(Wt::WString::fromUTF8(artists.front()->getName()));
_artist->setLink(utils::createArtistLink(artists.front()));
}
else
{
_artist->setText("");
_artist->setLink({});
needSeparator = false;
}
if (const db::Release::pointer release{ track->getRelease() }) if (release)
{ {
_release->setTextFormat(Wt::TextFormat::Plain); _release->setTextFormat(Wt::TextFormat::Plain);
_release->setText(Wt::WString::fromUTF8(std::string{ release->getName() })); _release->setText(Wt::WString::fromUTF8(std::string{ release->getName() }));
+2 -1
View File
@@ -22,6 +22,7 @@
#include <optional> #include <optional>
#include <Wt/WAnchor.h> #include <Wt/WAnchor.h>
#include <Wt/WContainerWidget.h>
#include <Wt/WJavaScript.h> #include <Wt/WJavaScript.h>
#include <Wt/WPushButton.h> #include <Wt/WPushButton.h>
#include <Wt/WTemplate.h> #include <Wt/WTemplate.h>
@@ -126,7 +127,7 @@ namespace lms::ui
Wt::WText* _title{}; Wt::WText* _title{};
Wt::WAnchor* _release{}; Wt::WAnchor* _release{};
Wt::WText* _separator{}; Wt::WText* _separator{};
Wt::WAnchor* _artist{}; Wt::WContainerWidget* _artists{};
Wt::WPushButton* _playQueue{}; Wt::WPushButton* _playQueue{};
}; };
} // namespace lms::ui } // namespace lms::ui
+4 -2
View File
@@ -487,8 +487,8 @@ namespace lms::ui
if (!artists.empty()) if (!artists.empty())
{ {
entry->setCondition("if-has-artists", true); entry->setCondition("if-has-artists", true);
entry->bindWidget("artists", utils::createArtistAnchorList(artists)); entry->bindWidget("artists", utils::createArtistsAnchors(track, db::TrackArtistLinkType::Artist));
entry->bindWidget("artists-md", utils::createArtistAnchorList(artists)); entry->bindWidget("artists-md", utils::createArtistsAnchors(track, db::TrackArtistLinkType::Artist));
} }
db::ArtworkId artworkId{ track->getPreferredMediaArtworkId() }; db::ArtworkId artworkId{ track->getPreferredMediaArtworkId() };
@@ -520,6 +520,7 @@ namespace lms::ui
entry->bindString("duration", utils::durationToString(track->getDuration()), Wt::TextFormat::Plain); entry->bindString("duration", utils::durationToString(track->getDuration()), Wt::TextFormat::Plain);
Wt::WPushButton* playBtn{ entry->bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.template.play-btn"), Wt::TextFormat::XHTML) }; Wt::WPushButton* playBtn{ entry->bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.template.play-btn"), Wt::TextFormat::XHTML) };
playBtn->setAttributeValue("aria-label", Wt::WString::tr("Lms.play-item").arg(track->getName()));
playBtn->clicked().connect([this, entry] { playBtn->clicked().connect([this, entry] {
const std::optional<std::size_t> pos{ _entriesContainer->getIndexOf(*entry) }; const std::optional<std::size_t> pos{ _entriesContainer->getIndexOf(*entry) };
if (pos) if (pos)
@@ -527,6 +528,7 @@ namespace lms::ui
}); });
Wt::WPushButton* delBtn{ entry->bindNew<Wt::WPushButton>("del-btn", Wt::WString::tr("Lms.template.delete-btn"), Wt::TextFormat::XHTML) }; Wt::WPushButton* delBtn{ entry->bindNew<Wt::WPushButton>("del-btn", Wt::WString::tr("Lms.template.delete-btn"), Wt::TextFormat::XHTML) };
delBtn->setAttributeValue("aria-label", Wt::WString::tr("Lms.delete-item").arg(track->getName()));
delBtn->setToolTip(Wt::WString::tr("Lms.delete")); delBtn->setToolTip(Wt::WString::tr("Lms.delete"));
delBtn->clicked().connect([this, tracklistEntryId, entry] { delBtn->clicked().connect([this, tracklistEntryId, entry] {
// Remove the entry n both the widget tree and the playqueue // Remove the entry n both the widget tree and the playqueue
+7 -7
View File
@@ -457,13 +457,13 @@ namespace lms::ui
_artistReleaseSortMethodModel->add(Wt::WString::tr("Lms.Settings.name"), db::ReleaseSortMethod::Name); _artistReleaseSortMethodModel->add(Wt::WString::tr("Lms.Settings.name"), db::ReleaseSortMethod::Name);
_artistRelationshipsModel = std::make_shared<ArtistRelationshipsModel>(); _artistRelationshipsModel = std::make_shared<ArtistRelationshipsModel>();
_artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.Artists.linktype-composer", 2), db::TrackArtistLinkType::Composer); _artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.Artists.composer", 2), db::TrackArtistLinkType::Composer);
_artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.Artists.linktype-conductor", 2), db::TrackArtistLinkType::Conductor); _artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.Artists.conductor", 2), db::TrackArtistLinkType::Conductor);
_artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.Artists.linktype-lyricist", 2), db::TrackArtistLinkType::Lyricist); _artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.Artists.lyricist", 2), db::TrackArtistLinkType::Lyricist);
_artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.Artists.linktype-mixer", 2), db::TrackArtistLinkType::Mixer); _artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.Artists.mixer", 2), db::TrackArtistLinkType::Mixer);
_artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.Artists.linktype-performer", 2), db::TrackArtistLinkType::Performer); _artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.Artists.performer", 2), db::TrackArtistLinkType::Performer);
_artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.Artists.linktype-producer", 2), db::TrackArtistLinkType::Producer); _artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.Artists.producer", 2), db::TrackArtistLinkType::Producer);
_artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.Artists.linktype-remixer", 2), db::TrackArtistLinkType::Remixer); _artistRelationshipsModel->add(Wt::WString::trn("Lms.Explore.Artists.remixer", 2), db::TrackArtistLinkType::Remixer);
_transcodingModeModeModel = std::make_shared<TranscodingModeModel>(); _transcodingModeModeModel = std::make_shared<TranscodingModeModel>();
_transcodingModeModeModel->add(Wt::WString::tr("Lms.Settings.transcoding-mode.always"), MediaPlayer::Settings::Transcoding::Mode::Always); _transcodingModeModeModel->add(Wt::WString::tr("Lms.Settings.transcoding-mode.always"), MediaPlayer::Settings::Transcoding::Mode::Always);
+1 -1
View File
@@ -35,7 +35,7 @@ namespace lms::ui::state
} // namespace detail } // namespace detail
template<typename T> template<typename T>
void writeValue(std::string_view item, std::optional<T> value) void writeValue(std::string_view item, const std::optional<T>& value)
{ {
if (value.has_value()) if (value.has_value())
{ {
+237 -106
View File
@@ -19,6 +19,7 @@
#include "Utils.hpp" #include "Utils.hpp"
#include <algorithm>
#include <iomanip> #include <iomanip>
#include <sstream> #include <sstream>
@@ -30,8 +31,10 @@
#include "database/objects/Cluster.hpp" #include "database/objects/Cluster.hpp"
#include "database/objects/Image.hpp" #include "database/objects/Image.hpp"
#include "database/objects/Release.hpp" #include "database/objects/Release.hpp"
#include "database/objects/ReleaseArtistLink.hpp"
#include "database/objects/ScanSettings.hpp" #include "database/objects/ScanSettings.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "database/objects/TrackArtistLink.hpp"
#include "database/objects/TrackList.hpp" #include "database/objects/TrackList.hpp"
#include "LmsApplication.hpp" #include "LmsApplication.hpp"
@@ -47,9 +50,32 @@ namespace lms::ui::utils
auto image{ std::make_unique<Wt::WImage>() }; auto image{ std::make_unique<Wt::WImage>() };
image->setStyleClass("Lms-cover img-fluid"); // HACK image->setStyleClass("Lms-cover img-fluid"); // HACK
image->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)"); // HACK image->setAttributeValue("onload", LmsApp->javaScriptClass() + ".onLoadCover(this)"); // HACK
image->setAlternateText(Wt::WString::tr("Lms.Explore.cover-art"));
return image; return image;
} }
Wt::WLink createArtistLink(const db::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(const db::Artist::pointer& artist, std::string_view displayName, bool setText)
{
auto res{ std::make_unique<Wt::WAnchor>(createArtistLink(artist)) };
if (setText)
{
res->setTextFormat(Wt::TextFormat::Plain);
res->setText(Wt::WString::fromUTF8(std::string{ displayName }));
res->setToolTip(Wt::WString::fromUTF8(std::string{ artist->getName() }), Wt::TextFormat::Plain);
}
return res;
}
} // namespace } // namespace
std::string durationToString(std::chrono::milliseconds msDuration) std::string durationToString(std::chrono::milliseconds msDuration)
@@ -173,138 +199,38 @@ namespace lms::ui::utils
return clusterContainer; return clusterContainer;
} }
std::unique_ptr<Wt::WContainerWidget> createArtistAnchorList(const std::vector<db::ArtistId>& artistIds, std::string_view cssAnchorClass) std::unique_ptr<Wt::WContainerWidget> createArtistAnchorList(const std::vector<db::Artist::pointer>& artists, std::string_view cssAnchorClass)
{ {
using namespace db;
std::unique_ptr<Wt::WContainerWidget> artistContainer{ std::make_unique<Wt::WContainerWidget>() }; std::unique_ptr<Wt::WContainerWidget> artistContainer{ std::make_unique<Wt::WContainerWidget>() };
bool firstArtist{ true }; bool firstArtist{ true };
auto transaction{ LmsApp->getDbSession().createReadTransaction() }; for (const auto& artist : artists)
for (const ArtistId artistId : artistIds)
{ {
const Artist::pointer artist{ Artist::find(LmsApp->getDbSession(), artistId) };
if (!artist)
continue;
if (!firstArtist) if (!firstArtist)
artistContainer->addNew<Wt::WText>(" · "); artistContainer->addNew<Wt::WText>(" · ");
firstArtist = false;
auto anchor{ createArtistAnchor(artist) }; auto anchor{ createArtistAnchor(artist) };
anchor->addStyleClass("text-decoration-none"); // hack anchor->addStyleClass("text-decoration-none"); // hack
anchor->addStyleClass(std::string{ cssAnchorClass }); anchor->addStyleClass(std::string{ cssAnchorClass });
artistContainer->addWidget(std::move(anchor)); artistContainer->addWidget(std::move(anchor));
firstArtist = false;
} }
return artistContainer; return artistContainer;
} }
std::unique_ptr<Wt::WContainerWidget> createArtistDisplayNameWithAnchors(std::string_view displayName, const std::vector<db::ArtistId>& artistIds, std::string_view cssAnchorClass)
{
using namespace db;
std::size_t matchCount{};
std::string_view::size_type currentOffset{};
auto result{ std::make_unique<Wt::WContainerWidget>() };
auto transaction{ LmsApp->getDbSession().createReadTransaction() };
// consider order is guaranteed + we will likely succeed
for (const ArtistId artistId : artistIds)
{
const Artist::pointer artist{ Artist::find(LmsApp->getDbSession(), artistId) };
if (!artist)
break;
const auto pos{ displayName.find(artist->getName(), currentOffset) };
if (pos == std::string_view::npos)
break;
assert(pos >= currentOffset);
if (pos != currentOffset)
result->addNew<Wt::WText>(std::string{ displayName.substr(currentOffset, pos - currentOffset) }, Wt::TextFormat::Plain);
auto anchor{ createArtistAnchor(artist) };
anchor->addStyleClass("text-decoration-none"); // hack
anchor->addStyleClass(std::string{ cssAnchorClass }); // hack
result->addWidget(std::move(anchor));
currentOffset = pos + artist->getName().size();
matchCount += 1;
}
if (matchCount == artistIds.size())
{
const std::string_view remainingStr{ displayName.substr(currentOffset) };
if (!remainingStr.empty())
result->addNew<Wt::WText>(std::string{ remainingStr }, Wt::TextFormat::Plain);
}
else
result = createArtistAnchorList(artistIds, cssAnchorClass);
return result;
}
std::unique_ptr<Wt::WContainerWidget> createArtistsAnchorsForRelease(db::ObjectPtr<db::Release> release, db::ArtistId omitIfMatchThisArtist, std::string_view cssAnchorClass)
{
using namespace db;
if (const std::vector<ArtistId> releaseArtists{ release->getArtistIds(TrackArtistLinkType::ReleaseArtist) }; !releaseArtists.empty())
{
if (releaseArtists.size() == 1 && releaseArtists.front() == omitIfMatchThisArtist)
return {};
return createArtistDisplayNameWithAnchors(release->getArtistDisplayName(), releaseArtists, cssAnchorClass);
}
const auto artists{ release->getArtistIds(TrackArtistLinkType::Artist) };
if (artists.size() == 1)
{
if (artists.front() == omitIfMatchThisArtist)
return {};
return createArtistAnchorList({ artists.front() }, cssAnchorClass);
}
if (artists.size() > 1)
{
auto res{ std::make_unique<Wt::WContainerWidget>() };
res->addNew<Wt::WText>(Wt::WString::tr("Lms.Explore.various-artists"));
return res;
}
return {};
}
Wt::WLink createArtistLink(db::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(db::Artist::pointer artist, bool setText) std::unique_ptr<Wt::WAnchor> createArtistAnchor(db::Artist::pointer artist, bool setText)
{ {
auto res = std::make_unique<Wt::WAnchor>(createArtistLink(artist)); return createArtistAnchor(artist, artist->getName(), setText);
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(db::Release::pointer release) Wt::WLink createReleaseLink(db::Release::pointer release)
{ {
if (const auto mbid{ release->getMBID() }) if (const auto mbid{ release->getMBID() })
return Wt::WLink{ Wt::LinkType::InternalPath, "/release/mbid/" + std::string{ mbid->getAsString() } }; return Wt::WLink{ Wt::LinkType::InternalPath, "/release/mbid/" + std::string{ mbid->getAsString() } };
else
return Wt::WLink{ Wt::LinkType::InternalPath, "/release/" + release->getId().toString() }; return Wt::WLink{ Wt::LinkType::InternalPath, "/release/" + release->getId().toString() };
} }
std::unique_ptr<Wt::WAnchor> createReleaseAnchor(db::Release::pointer release, bool setText) std::unique_ptr<Wt::WAnchor> createReleaseAnchor(db::Release::pointer release, bool setText)
@@ -343,4 +269,209 @@ namespace lms::ui::utils
return res; return res;
} }
ArtistDisplayInfo computeArtistDisplayInfo(db::ObjectPtr<db::Release> release)
{
ArtistDisplayInfo res;
res.displayName = release->getArtistDisplayName();
release->visitArtistLinks([&res](const db::ObjectPtr<db::ReleaseArtistLink>& artistLink) {
res.entries.emplace_back(ArtistDisplayInfo::Entry{ .displayName = std::string{ artistLink->getArtistName() }, .artist = artistLink->getArtist() });
});
// If no release artists, fallback on track artists, only if there are only 1 artist, otherwise just put various-artists
if (res.entries.empty())
{
res.displayName.clear();
const auto trackArtists{ release->getTrackArtists(db::TrackArtistLinkType::Artist) };
if (trackArtists.size() > 1)
res.displayName = Wt::WString::tr("Lms.Explore.various-artists").toUTF8();
else
res.entries.emplace_back(ArtistDisplayInfo::Entry{ .displayName = std::string{ trackArtists[0]->getName() }, .artist = trackArtists[0] });
}
return res;
}
ArtistDisplayInfo computeArtistDisplayInfo(db::ObjectPtr<db::Track> track, db::TrackArtistLinkType linkType)
{
ArtistDisplayInfo res;
res.displayName = track->getArtistDisplayName();
track->visitArtistLinks(linkType, [&res](const db::ObjectPtr<db::TrackArtistLink>& artistLink) {
res.entries.emplace_back(ArtistDisplayInfo::Entry{ .displayName = std::string{ artistLink->getArtistName() }, .artist = artistLink->getArtist() });
});
return res;
}
std::unique_ptr<Wt::WContainerWidget> createArtistsAnchors(const ArtistDisplayInfo& artistDisplayInfo, std::string_view cssAnchorClass)
{
auto result{ std::make_unique<Wt::WContainerWidget>() };
result->setInline(true); // TODO: use a template for that?
std::size_t matchCount{};
std::string_view::size_type currentOffset{};
if (artistDisplayInfo.entries.empty())
{
if (!artistDisplayInfo.displayName.empty())
result->addNew<Wt::WText>(std::string{ artistDisplayInfo.displayName }, Wt::TextFormat::Plain);
return result;
}
// consider order is guaranteed + we will likely succeed
for (const ArtistDisplayInfo::Entry& entry : artistDisplayInfo.entries)
{
const auto pos{ artistDisplayInfo.displayName.find(entry.displayName, currentOffset) };
if (pos == std::string_view::npos)
break;
assert(pos >= currentOffset);
if (pos != currentOffset)
result->addNew<Wt::WText>(std::string{ artistDisplayInfo.displayName.substr(currentOffset, pos - currentOffset) }, Wt::TextFormat::Plain);
auto anchor{ createArtistAnchor(entry.artist, entry.displayName, true) };
anchor->addStyleClass("text-decoration-none"); // hack
anchor->addStyleClass(std::string{ cssAnchorClass }); // hack
result->addWidget(std::move(anchor));
currentOffset = pos + entry.displayName.size();
matchCount += 1;
}
if (matchCount == artistDisplayInfo.entries.size())
{
const std::string_view remainingStr{ artistDisplayInfo.displayName.substr(currentOffset) };
if (!remainingStr.empty())
result->addNew<Wt::WText>(std::string{ remainingStr }, Wt::TextFormat::Plain);
}
else
{
std::vector<db::Artist::pointer> artists;
std::transform(std::cbegin(artistDisplayInfo.entries), std::cend(artistDisplayInfo.entries), std::back_inserter(artists), [](const ArtistDisplayInfo::Entry& entry) {
return entry.artist;
});
result = createArtistAnchorList(artists, cssAnchorClass);
}
return result;
}
std::unique_ptr<Wt::WContainerWidget> createArtistsAnchors(db::ObjectPtr<db::Track> track, db::TrackArtistLinkType linkType, std::string_view cssAnchorClass)
{
const auto computeArtistDisplayInfo{ utils::computeArtistDisplayInfo(track, linkType) };
return utils::createArtistsAnchors(computeArtistDisplayInfo, cssAnchorClass);
}
std::unique_ptr<Wt::WContainerWidget> createArtistsAnchors(db::ObjectPtr<db::Release> release, std::string_view cssAnchorClass)
{
const auto computeArtistDisplayInfo{ utils::computeArtistDisplayInfo(release) };
return utils::createArtistsAnchors(computeArtistDisplayInfo, cssAnchorClass);
}
std::map<Wt::WString, std::vector<db::ObjectPtr<db::Artist>>> getArtistsByRole(db::ObjectPtr<db::Track> track, core::EnumSet<db::TrackArtistLinkType> artistLinkTypes)
{
std::map<Wt::WString, std::vector<db::Artist::pointer>> artistMap;
auto addArtists = [&](db::TrackArtistLinkType linkType, const char* type) {
if (!artistLinkTypes.empty() && !artistLinkTypes.contains(linkType))
return;
std::vector<db::Artist::pointer> artists;
track->visitArtistLinks(linkType, [&](const auto& link) {
artists.push_back(link->getArtist());
});
if (artists.empty())
return;
Wt::WString typeStr{ Wt::WString::trn(type, artists.size()) };
artistMap[typeStr] = std::move(artists);
};
std::vector<db::ObjectPtr<db::Artist>> rolelessPerformers;
auto addPerformerArtists = [&] {
if (!artistLinkTypes.empty() && !artistLinkTypes.contains(db::TrackArtistLinkType::Performer))
return;
track->visitArtistLinks(db::TrackArtistLinkType::Performer, [&](const auto& link) {
if (link->getSubType().empty())
rolelessPerformers.push_back(link->getArtist());
else
artistMap[std::string{ link->getSubType() }].push_back(link->getArtist());
});
};
addArtists(db::TrackArtistLinkType::Composer, "Lms.Explore.Artists.composer");
addArtists(db::TrackArtistLinkType::Conductor, "Lms.Explore.Artists.conductor");
addArtists(db::TrackArtistLinkType::Lyricist, "Lms.Explore.Artists.lyricist");
addArtists(db::TrackArtistLinkType::Mixer, "Lms.Explore.Artists.mixer");
addArtists(db::TrackArtistLinkType::Remixer, "Lms.Explore.Artists.remixer");
addArtists(db::TrackArtistLinkType::Producer, "Lms.Explore.Artists.producer");
addPerformerArtists();
if (!rolelessPerformers.empty())
{
Wt::WString performersStr{ Wt::WString::trn("Lms.Explore.Artists.performer", rolelessPerformers.size()) };
artistMap[performersStr] = std::move(rolelessPerformers);
}
return artistMap;
}
std::map<Wt::WString, std::vector<db::ObjectPtr<db::Artist>>> getTrackArtistsByRole(db::ObjectPtr<db::Release> release)
{
std::map<Wt::WString, std::vector<db::Artist::pointer>> artistMap;
auto addArtists = [&](db::TrackArtistLinkType linkType, const char* type) {
std::vector<db::Artist::pointer> artists;
release->visitTrackArtists(linkType, [&](const auto& artist) {
artists.push_back(artist);
});
if (artists.empty())
return;
Wt::WString typeStr{ Wt::WString::trn(type, artists.size()) };
artistMap[typeStr] = std::move(artists);
};
std::vector<db::ObjectPtr<db::Artist>> rolelessPerformers;
auto addPerformerArtists = [&] {
release->visitTrackArtistLinks(db::TrackArtistLinkType::Performer, [&](const db::TrackArtistLink::pointer& link) {
if (link->getSubType().empty())
{
if (std::find(std::cbegin(rolelessPerformers), std::cend(rolelessPerformers), link->getArtist()) == std::cend(rolelessPerformers))
rolelessPerformers.push_back(link->getArtist());
}
else
{
auto& artists{ artistMap[std::string{ link->getSubType() }] };
if (std::find(std::cbegin(artists), std::cend(artists), link->getArtist()) == std::cend(artists))
artists.push_back(link->getArtist());
}
});
};
addArtists(db::TrackArtistLinkType::Composer, "Lms.Explore.Artists.composer");
addArtists(db::TrackArtistLinkType::Conductor, "Lms.Explore.Artists.conductor");
addArtists(db::TrackArtistLinkType::Lyricist, "Lms.Explore.Artists.lyricist");
addArtists(db::TrackArtistLinkType::Mixer, "Lms.Explore.Artists.mixer");
addArtists(db::TrackArtistLinkType::Remixer, "Lms.Explore.Artists.remixer");
addArtists(db::TrackArtistLinkType::Producer, "Lms.Explore.Artists.producer");
addPerformerArtists();
if (!rolelessPerformers.empty())
{
Wt::WString performersStr{ Wt::WString::trn("Lms.Explore.Artists.performer", rolelessPerformers.size()) };
artistMap[performersStr] = std::move(rolelessPerformers);
}
return artistMap;
}
} // namespace lms::ui::utils } // namespace lms::ui::utils
+29 -11
View File
@@ -20,21 +20,22 @@
#pragma once #pragma once
#include <chrono> #include <chrono>
#include <map>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <vector>
#include <Wt/WContainerWidget.h> #include <Wt/WContainerWidget.h>
#include <Wt/WImage.h> #include <Wt/WImage.h>
#include <Wt/WInteractWidget.h> #include <Wt/WInteractWidget.h>
#include <Wt/WString.h> #include <Wt/WString.h>
#include "database/Object.hpp" #include "core/EnumSet.hpp"
#include "database/objects/ArtistId.hpp"
#include "database/objects/ClusterId.hpp" #include "database/Object.hpp"
#include "database/objects/ReleaseId.hpp" #include "database/objects/ClusterId.hpp"
#include "database/objects/TrackId.hpp" #include "database/objects/Types.hpp"
#include "database/Types.hpp"
#include "resource/ArtworkResource.hpp" #include "resource/ArtworkResource.hpp"
namespace lms::db namespace lms::db
@@ -64,13 +65,30 @@ namespace lms::ui::utils
std::unique_ptr<Wt::WInteractWidget> createFilterCluster(db::ClusterId clusterId, bool canDelete = false); std::unique_ptr<Wt::WInteractWidget> createFilterCluster(db::ClusterId clusterId, bool canDelete = false);
std::unique_ptr<Wt::WContainerWidget> createFilterClustersForTrack(db::ObjectPtr<db::Track> track, Filters& filters); std::unique_ptr<Wt::WContainerWidget> createFilterClustersForTrack(db::ObjectPtr<db::Track> track, Filters& filters);
std::unique_ptr<Wt::WContainerWidget> createArtistAnchorList(const std::vector<db::ArtistId>& artistIds, std::string_view cssAnchorClass = "link-success");
std::unique_ptr<Wt::WContainerWidget> createArtistDisplayNameWithAnchors(std::string_view displayName, const std::vector<db::ArtistId>& artistIds, std::string_view cssAnchorClass = "link-success");
std::unique_ptr<Wt::WContainerWidget> createArtistsAnchorsForRelease(db::ObjectPtr<db::Release> release, db::ArtistId omitIfMatchThisArtist = {}, std::string_view cssAnchorClass = "link-success");
Wt::WLink createArtistLink(db::ObjectPtr<db::Artist> artist);
std::unique_ptr<Wt::WAnchor> createArtistAnchor(db::ObjectPtr<db::Artist> artist, bool setText = true); std::unique_ptr<Wt::WAnchor> createArtistAnchor(db::ObjectPtr<db::Artist> artist, bool setText = true);
Wt::WLink createReleaseLink(db::ObjectPtr<db::Release> release); Wt::WLink createReleaseLink(db::ObjectPtr<db::Release> release);
std::unique_ptr<Wt::WAnchor> createReleaseAnchor(db::ObjectPtr<db::Release> release, bool setText = true); std::unique_ptr<Wt::WAnchor> createReleaseAnchor(db::ObjectPtr<db::Release> release, bool setText = true);
std::unique_ptr<Wt::WAnchor> createTrackListAnchor(db::ObjectPtr<db::TrackList> trackList, bool setText = true); std::unique_ptr<Wt::WAnchor> createTrackListAnchor(db::ObjectPtr<db::TrackList> trackList, bool setText = true);
std::unique_ptr<Wt::WContainerWidget> createArtistAnchorList(const std::vector<db::ObjectPtr<db::Artist>>& artists, std::string_view cssAnchorClass = "link-success");
struct ArtistDisplayInfo
{
std::string displayName;
struct Entry
{
std::string displayName;
db::ObjectPtr<db::Artist> artist;
};
std::vector<Entry> entries;
};
ArtistDisplayInfo computeArtistDisplayInfo(db::ObjectPtr<db::Release> release);
ArtistDisplayInfo computeArtistDisplayInfo(db::ObjectPtr<db::Track> track, db::TrackArtistLinkType linkType);
std::unique_ptr<Wt::WContainerWidget> createArtistsAnchors(const ArtistDisplayInfo& artistDisplayInfo, std::string_view cssAnchorClass = "link-success");
std::unique_ptr<Wt::WContainerWidget> createArtistsAnchors(db::ObjectPtr<db::Track> track, db::TrackArtistLinkType linkType, std::string_view cssAnchorClass = "link-success");
std::unique_ptr<Wt::WContainerWidget> createArtistsAnchors(db::ObjectPtr<db::Release> release, std::string_view cssAnchorClass = "link-success");
std::map<Wt::WString, std::vector<db::ObjectPtr<db::Artist>>> getArtistsByRole(db::ObjectPtr<db::Track> track, core::EnumSet<db::TrackArtistLinkType> artistLinkTypes = {});
std::map<Wt::WString, std::vector<db::ObjectPtr<db::Artist>>> getTrackArtistsByRole(db::ObjectPtr<db::Release> release);
} // namespace lms::ui::utils } // namespace lms::ui::utils
-7
View File
@@ -212,13 +212,6 @@ namespace lms::ui
.arg(stepStats.progress())); .arg(stepStats.progress()));
break; break;
case ScanStep::FetchTrackFeatures:
_stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-fetching-track-features")
.arg(stepStats.processedElems)
.arg(stepStats.totalElems)
.arg(stepStats.progress()));
break;
case ScanStep::Optimize: case ScanStep::Optimize:
_stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-optimize") _stepStatus->setText(Wt::WString::tr("Lms.Admin.ScannerController.step-optimize")
.arg(stepStats.progress())); .arg(stepStats.progress()));
+39 -8
View File
@@ -20,14 +20,16 @@
#include "ArtistCollector.hpp" #include "ArtistCollector.hpp"
#include "core/Service.hpp" #include "core/Service.hpp"
#include "core/Utils.hpp"
#include "database/Session.hpp" #include "database/Session.hpp"
#include "database/objects/Artist.hpp" #include "database/objects/Artist.hpp"
#include "database/objects/TrackList.hpp" #include "database/objects/TrackList.hpp"
#include "database/objects/Types.hpp"
#include "database/objects/User.hpp" #include "database/objects/User.hpp"
#include "services/feedback/IFeedbackService.hpp" #include "services/feedback/IFeedbackService.hpp"
#include "services/scrobbling/IScrobblingService.hpp" #include "services/scrobbling/IScrobblingService.hpp"
#include "Filters.hpp"
#include "LmsApplication.hpp" #include "LmsApplication.hpp"
namespace lms::ui namespace lms::ui
@@ -53,9 +55,14 @@ namespace lms::ui
params.setFilters(getDbFilters()); params.setFilters(getDbFilters());
params.setUser(LmsApp->getUserId()); params.setUser(LmsApp->getUserId());
params.setKeywords(getSearchKeywords()); params.setKeywords(getSearchKeywords());
params.setLinkType(_linkType);
params.setSortMethod(db::ArtistSortMethod::StarredDateDesc); params.setSortMethod(db::ArtistSortMethod::StarredDateDesc);
params.setRange(range); params.setRange(range);
std::visit(core::utils::overloads{
[&](AllArtistsTag) {},
[&](ReleaseArtistsTag) { params.setReleaseArtistsOnly(true); },
[&](db::TrackArtistLinkType trackArtistLinkType) { params.setTrackArtistLinkType(trackArtistLinkType); } },
_artistType);
artists = feedbackService.findStarredArtists(params); artists = feedbackService.findStarredArtists(params);
break; break;
} }
@@ -66,8 +73,12 @@ namespace lms::ui
params.setUser(LmsApp->getUserId()); params.setUser(LmsApp->getUserId());
params.setFilters(getDbFilters()); params.setFilters(getDbFilters());
params.setKeywords(getSearchKeywords()); params.setKeywords(getSearchKeywords());
params.setLinkType(_linkType);
params.setRange(range); params.setRange(range);
std::visit(core::utils::overloads{
[&](AllArtistsTag) {},
[&](ReleaseArtistsTag) { params.setReleaseArtistsOnly(true); },
[&](db::TrackArtistLinkType trackArtistLinkType) { params.setTrackArtistLinkType(trackArtistLinkType); } },
_artistType);
artists = scrobblingService.getRecentArtists(params); artists = scrobblingService.getRecentArtists(params);
break; break;
@@ -79,8 +90,12 @@ namespace lms::ui
params.setUser(LmsApp->getUserId()); params.setUser(LmsApp->getUserId());
params.setFilters(getDbFilters()); params.setFilters(getDbFilters());
params.setKeywords(getSearchKeywords()); params.setKeywords(getSearchKeywords());
params.setLinkType(_linkType);
params.setRange(range); params.setRange(range);
std::visit(core::utils::overloads{
[&](AllArtistsTag) {},
[&](ReleaseArtistsTag) { params.setReleaseArtistsOnly(true); },
[&](db::TrackArtistLinkType trackArtistLinkType) { params.setTrackArtistLinkType(trackArtistLinkType); } },
_artistType);
artists = scrobblingService.getTopArtists(params); artists = scrobblingService.getTopArtists(params);
break; break;
@@ -91,9 +106,13 @@ namespace lms::ui
db::Artist::FindParameters params; db::Artist::FindParameters params;
params.setFilters(getDbFilters()); params.setFilters(getDbFilters());
params.setKeywords(getSearchKeywords()); params.setKeywords(getSearchKeywords());
params.setLinkType(_linkType);
params.setSortMethod(db::ArtistSortMethod::AddedDesc); params.setSortMethod(db::ArtistSortMethod::AddedDesc);
params.setRange(range); params.setRange(range);
std::visit(core::utils::overloads{
[&](AllArtistsTag) {},
[&](ReleaseArtistsTag) { params.setReleaseArtistsOnly(true); },
[&](db::TrackArtistLinkType trackArtistLinkType) { params.setTrackArtistLinkType(trackArtistLinkType); } },
_artistType);
{ {
auto transaction{ LmsApp->getDbSession().createReadTransaction() }; auto transaction{ LmsApp->getDbSession().createReadTransaction() };
@@ -107,9 +126,13 @@ namespace lms::ui
db::Artist::FindParameters params; db::Artist::FindParameters params;
params.setFilters(getDbFilters()); params.setFilters(getDbFilters());
params.setKeywords(getSearchKeywords()); params.setKeywords(getSearchKeywords());
params.setLinkType(_linkType);
params.setSortMethod(db::ArtistSortMethod::LastWrittenDesc); params.setSortMethod(db::ArtistSortMethod::LastWrittenDesc);
params.setRange(range); params.setRange(range);
std::visit(core::utils::overloads{
[&](AllArtistsTag) {},
[&](ReleaseArtistsTag) { params.setReleaseArtistsOnly(true); },
[&](db::TrackArtistLinkType trackArtistLinkType) { params.setTrackArtistLinkType(trackArtistLinkType); } },
_artistType);
{ {
auto transaction{ LmsApp->getDbSession().createReadTransaction() }; auto transaction{ LmsApp->getDbSession().createReadTransaction() };
@@ -123,9 +146,13 @@ namespace lms::ui
db::Artist::FindParameters params; db::Artist::FindParameters params;
params.setFilters(getDbFilters()); params.setFilters(getDbFilters());
params.setKeywords(getSearchKeywords()); params.setKeywords(getSearchKeywords());
params.setLinkType(_linkType);
params.setSortMethod(db::ArtistSortMethod::SortName); params.setSortMethod(db::ArtistSortMethod::SortName);
params.setRange(range); params.setRange(range);
std::visit(core::utils::overloads{
[&](AllArtistsTag) {},
[&](ReleaseArtistsTag) { params.setReleaseArtistsOnly(true); },
[&](db::TrackArtistLinkType trackArtistLinkType) { params.setTrackArtistLinkType(trackArtistLinkType); } },
_artistType);
{ {
auto transaction{ LmsApp->getDbSession().createReadTransaction() }; auto transaction{ LmsApp->getDbSession().createReadTransaction() };
@@ -150,9 +177,13 @@ namespace lms::ui
db::Artist::FindParameters params; db::Artist::FindParameters params;
params.setFilters(getDbFilters()); params.setFilters(getDbFilters());
params.setKeywords(getSearchKeywords()); params.setKeywords(getSearchKeywords());
params.setLinkType(_linkType);
params.setSortMethod(db::ArtistSortMethod::Random); params.setSortMethod(db::ArtistSortMethod::Random);
params.setRange(db::Range{ 0, getMaxCount() }); params.setRange(db::Range{ 0, getMaxCount() });
std::visit(core::utils::overloads{
[&](AllArtistsTag) {},
[&](ReleaseArtistsTag) { params.setReleaseArtistsOnly(true); },
[&](db::TrackArtistLinkType trackArtistLinkType) { params.setTrackArtistLinkType(trackArtistLinkType); } },
_artistType);
{ {
auto transaction{ LmsApp->getDbSession().createReadTransaction() }; auto transaction{ LmsApp->getDbSession().createReadTransaction() };
+3 -3
View File
@@ -22,8 +22,8 @@
#include <optional> #include <optional>
#include "database/objects/ArtistId.hpp" #include "database/objects/ArtistId.hpp"
#include "database/objects/Types.hpp"
#include "ArtistType.hpp"
#include "DatabaseCollectorBase.hpp" #include "DatabaseCollectorBase.hpp"
namespace lms::db namespace lms::db
@@ -40,11 +40,11 @@ namespace lms::ui
db::RangeResults<db::ArtistId> get(std::optional<db::Range> range = std::nullopt); db::RangeResults<db::ArtistId> get(std::optional<db::Range> range = std::nullopt);
void reset() { _randomArtists.reset(); } void reset() { _randomArtists.reset(); }
void setArtistLinkType(std::optional<db::TrackArtistLinkType> linkType) { _linkType = linkType; } void setArtistType(ArtistType artistType) { _artistType = artistType; }
private: private:
db::RangeResults<db::ArtistId> getRandomArtists(Range range); db::RangeResults<db::ArtistId> getRandomArtists(Range range);
std::optional<db::RangeResults<db::ArtistId>> _randomArtists; std::optional<db::RangeResults<db::ArtistId>> _randomArtists;
std::optional<db::TrackArtistLinkType> _linkType; ArtistType _artistType;
}; };
} // namespace lms::ui } // namespace lms::ui
+56
View File
@@ -0,0 +1,56 @@
/*
* Copyright (C) 2024 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "ArtistType.hpp"
#include "core/Utils.hpp"
namespace lms::core::stringUtils
{
template<>
std::optional<lms::ui::ArtistType> readAs(std::string_view str)
{
if (str == "all")
return lms::ui::AllArtistsTag{};
if (str == "release")
return lms::ui::ReleaseArtistsTag{};
using UnderlyingType = std::underlying_type_t<lms::db::TrackArtistLinkType>;
std::optional<UnderlyingType> underlyingValue{ readAs<UnderlyingType>(str) };
if (!underlyingValue)
return std::nullopt;
return lms::db::TrackArtistLinkType{ *underlyingValue };
}
} // namespace lms::core::stringUtils
namespace std
{
std::string to_string(const lms::ui::ArtistType& artistType)
{
std::string res;
std::visit(lms::core::utils::overloads{
[&](lms::ui::AllArtistsTag) { res = "all"; },
[&](lms::ui::ReleaseArtistsTag) { res = "release"; },
[&](lms::db::TrackArtistLinkType linkType) { res = std::to_string(static_cast<std::underlying_type_t<lms::db::TrackArtistLinkType>>(linkType)); } },
artistType);
return res;
}
} // namespace std
+52
View File
@@ -0,0 +1,52 @@
/*
* Copyright (C) 2024 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <variant>
#include "core/String.hpp"
#include "database/objects/Types.hpp"
namespace lms::ui
{
struct AllArtistsTag
{
bool operator==(const AllArtistsTag&) const = default;
};
struct ReleaseArtistsTag
{
bool operator==(const ReleaseArtistsTag&) const = default;
};
using ArtistType = std::variant<AllArtistsTag, ReleaseArtistsTag, db::TrackArtistLinkType>;
} // namespace lms::ui
namespace lms::core::stringUtils
{
template<>
[[nodiscard]] std::optional<lms::ui::ArtistType> readAs(std::string_view str);
}
namespace std
{
[[nodiscard]] std::string to_string(const lms::ui::ArtistType& artistType);
};
+45
View File
@@ -0,0 +1,45 @@
/*
* Copyright (C) 2024 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "ArtistType.hpp"
#include "DropDownMenuSelector.hpp"
namespace lms::ui
{
class ArtistTypeSelector : public DropDownMenuSelector<ArtistType>
{
public:
ArtistTypeSelector(ArtistType defaultArtistType)
: DropDownMenuSelector{ Wt::WString::tr("Lms.Explore.Artists.template.artist-type-selector"), defaultArtistType }
{
bindItem("artist-type-all", Wt::WString::tr("Lms.Explore.Artists.all"), AllArtistsTag{});
bindItem("artist-type-release-artist", Wt::WString::trn("Lms.Explore.Artists.release-artist", 2), ReleaseArtistsTag{});
bindItem("artist-type-track-artist", Wt::WString::trn("Lms.Explore.Artists.track-artist", 2), db::TrackArtistLinkType::Artist);
bindItem("artist-type-composer", Wt::WString::trn("Lms.Explore.Artists.composer", 2), db::TrackArtistLinkType::Composer);
bindItem("artist-type-conductor", Wt::WString::trn("Lms.Explore.Artists.conductor", 2), db::TrackArtistLinkType::Conductor);
bindItem("artist-type-lyricist", Wt::WString::trn("Lms.Explore.Artists.lyricist", 2), db::TrackArtistLinkType::Lyricist);
bindItem("artist-type-mixer", Wt::WString::trn("Lms.Explore.Artists.mixer", 2), db::TrackArtistLinkType::Mixer);
bindItem("artist-type-performer", Wt::WString::trn("Lms.Explore.Artists.performer", 2), db::TrackArtistLinkType::Performer);
bindItem("artist-type-producer", Wt::WString::trn("Lms.Explore.Artists.producer", 2), db::TrackArtistLinkType::Producer);
bindItem("artist-type-remixer", Wt::WString::trn("Lms.Explore.Artists.remixer", 2), db::TrackArtistLinkType::Remixer);
}
};
} // namespace lms::ui
+31 -38
View File
@@ -97,11 +97,12 @@ namespace lms::ui
clear(); clear();
_artistId = {}; _artistId = {};
_trackContainer = nullptr; _trackContainer = nullptr;
_artistName = {};
if (!artistId) if (!artistId)
throw ArtistNotFoundException{}; throw ArtistNotFoundException{};
const auto similarArtistIds{ core::Service<recommendation::IRecommendationService>::get()->getSimilarArtists(*artistId, { db::TrackArtistLinkType::Artist, db::TrackArtistLinkType::ReleaseArtist }, 6) }; const auto similarArtistIds{ core::Service<recommendation::IRecommendationService>::get()->getSimilarArtists(*artistId, { db::TrackArtistLinkType::Artist }, 6) };
auto transaction{ LmsApp->getDbSession().createReadTransaction() }; auto transaction{ LmsApp->getDbSession().createReadTransaction() };
@@ -111,6 +112,7 @@ namespace lms::ui
LmsApp->setTitle(artist->getName()); LmsApp->setTitle(artist->getName());
_artistId = *artistId; _artistId = *artistId;
_artistName = artist->getName();
refreshArtwork(artist->getPreferredArtworkId()); refreshArtwork(artist->getPreferredArtworkId());
refreshArtistInfo(); refreshArtistInfo();
@@ -139,7 +141,7 @@ namespace lms::ui
} }
} }
bindString("name", Wt::WString::fromUTF8(artist->getName()), Wt::TextFormat::Plain); bindString("name", Wt::WString::fromUTF8(_artistName), Wt::TextFormat::Plain);
bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.Explore.play"), Wt::TextFormat::XHTML) bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.Explore.play"), Wt::TextFormat::XHTML)
->clicked() ->clicked()
@@ -230,20 +232,24 @@ namespace lms::ui
_releaseContainers.clear(); _releaseContainers.clear();
db::Release::FindParameters params; db::Release::FindParameters params;
params.setArtist(_artistId, { db::TrackArtistLinkType::ReleaseArtist }, {}); params.setArtist(_artistId);
params.setSortMethod(LmsApp->getUser()->getUIArtistReleaseSortMethod()); params.setSortMethod(LmsApp->getUser()->getUIArtistReleaseSortMethod());
const auto releases{ db::Release::findIds(LmsApp->getDbSession(), params) }; // first pass: gather all ids and sort by release type
if (!releases.results.empty()) bool hasReleaseWithDifferentName{};
{ db::Release::find(LmsApp->getDbSession(), params, [&](const db::Release::pointer& release) {
// first pass: gather all ids and sort by release type ReleaseType releaseType{ parseReleaseType(release->getReleaseTypeNames()) };
for (const db::ReleaseId releaseId : releases.results) _releaseContainers[releaseType].releases.push_back(release->getId());
{
const db::Release::pointer release{ db::Release::find(LmsApp->getDbSession(), releaseId) };
ReleaseType releaseType{ parseReleaseType(release->getReleaseTypeNames()) }; if (release->getArtistDisplayName() != _artistName)
_releaseContainers[releaseType].releases.push_back(releaseId); hasReleaseWithDifferentName = true;
} });
if (!_releaseContainers.empty())
{
core::EnumSet displayOptions{ releaseListHelpers::DisplayOptions::ShowYearAndOriginalYear };
if (hasReleaseWithDifferentName)
displayOptions.insert(releaseListHelpers::DisplayOptions::ShowArtist);
// second pass: construct widgets // second pass: construct widgets
Wt::WContainerWidget* releaseContainers{ bindNew<Wt::WContainerWidget>("release-containers") }; Wt::WContainerWidget* releaseContainers{ bindNew<Wt::WContainerWidget>("release-containers") };
@@ -253,8 +259,8 @@ namespace lms::ui
releaseContainer->bindString("release-type", releaseHelpers::buildReleaseTypeString(releaseType)); releaseContainer->bindString("release-type", releaseHelpers::buildReleaseTypeString(releaseType));
releases.container = releaseContainer->bindNew<InfiniteScrollingContainer>("releases", Wt::WString::tr("Lms.Explore.Releases.template.container")); releases.container = releaseContainer->bindNew<InfiniteScrollingContainer>("releases", Wt::WString::tr("Lms.Explore.Releases.template.container"));
releases.container->onRequestElements.connect(this, [this, &releases = releases] { releases.container->onRequestElements.connect(this, [&, displayOptions] {
addSomeReleases(releases); addSomeReleases(releases, displayOptions);
}); });
} }
} }
@@ -266,35 +272,24 @@ namespace lms::ui
void Artist::refreshAppearsOnReleases() void Artist::refreshAppearsOnReleases()
{ {
constexpr core::EnumSet<db::TrackArtistLinkType> types{
db::TrackArtistLinkType::Artist,
db::TrackArtistLinkType::Arranger,
db::TrackArtistLinkType::Composer,
db::TrackArtistLinkType::Conductor,
db::TrackArtistLinkType::Lyricist,
db::TrackArtistLinkType::Mixer,
db::TrackArtistLinkType::Performer,
db::TrackArtistLinkType::Producer,
db::TrackArtistLinkType::Remixer,
db::TrackArtistLinkType::Writer,
};
_appearsOnReleaseContainer = {}; _appearsOnReleaseContainer = {};
db::Release::FindParameters params; db::Release::FindParameters params;
params.setFilters(_filters.getDbFilters()); params.setTrackArtist(_artistId);
params.setArtist(_artistId, types, { db::TrackArtistLinkType::ReleaseArtist });
params.setSortMethod(db::ReleaseSortMethod::OriginalDateDesc); params.setSortMethod(db::ReleaseSortMethod::OriginalDateDesc);
const auto releases{ db::Release::findIds(LmsApp->getDbSession(), params) }; db::Release::find(LmsApp->getDbSession(), params, [this](const db::Release::pointer& release) {
if (!releases.results.empty()) if (!release->hasArtist(_artistId))
_appearsOnReleaseContainer.releases.push_back(release->getId());
});
if (!_appearsOnReleaseContainer.releases.empty())
{ {
Wt::WTemplate* releaseContainer{ bindNew<Wt::WTemplate>("appears-on-releases", Wt::WString::tr("Lms.Explore.Artist.template.release-container")) }; Wt::WTemplate* releaseContainer{ bindNew<Wt::WTemplate>("appears-on-releases", Wt::WString::tr("Lms.Explore.Artist.template.release-container")) };
releaseContainer->bindString("release-type", Wt::WString::tr("Lms.Explore.Artist.appears-on")); releaseContainer->bindString("release-type", Wt::WString::tr("Lms.Explore.Artist.appears-on"));
_appearsOnReleaseContainer.releases = releases.results;
_appearsOnReleaseContainer.container = releaseContainer->bindNew<InfiniteScrollingContainer>("releases", Wt::WString::tr("Lms.Explore.Releases.template.container")); _appearsOnReleaseContainer.container = releaseContainer->bindNew<InfiniteScrollingContainer>("releases", Wt::WString::tr("Lms.Explore.Releases.template.container"));
_appearsOnReleaseContainer.container->onRequestElements.connect(this, [this] { _appearsOnReleaseContainer.container->onRequestElements.connect(this, [this] {
addSomeReleases(_appearsOnReleaseContainer); addSomeReleases(_appearsOnReleaseContainer, { releaseListHelpers::DisplayOptions::ShowYearAndOriginalYear, releaseListHelpers::DisplayOptions::ShowArtist });
}); });
} }
else else
@@ -343,7 +338,7 @@ namespace lms::ui
} }
} }
void Artist::addSomeReleases(ReleaseContainer& releaseContainer) void Artist::addSomeReleases(ReleaseContainer& releaseContainer, core::EnumSet<releaseListHelpers::DisplayOptions> displayOptions)
{ {
auto transaction{ LmsApp->getDbSession().createReadTransaction() }; auto transaction{ LmsApp->getDbSession().createReadTransaction() };
@@ -352,7 +347,7 @@ namespace lms::ui
for (std::size_t i{ static_cast<std::size_t>(releaseContainer.container->getCount()) }; i < releaseContainer.releases.size(); ++i) for (std::size_t i{ static_cast<std::size_t>(releaseContainer.container->getCount()) }; i < releaseContainer.releases.size(); ++i)
{ {
const db::Release::pointer release{ db::Release::find(LmsApp->getDbSession(), releaseContainer.releases[i]) }; const db::Release::pointer release{ db::Release::find(LmsApp->getDbSession(), releaseContainer.releases[i]) };
releaseContainer.container->add(releaseListHelpers::createEntryForArtist(release, artist)); releaseContainer.container->add(releaseListHelpers::createEntry(release, displayOptions));
} }
} }
@@ -366,7 +361,6 @@ namespace lms::ui
const db::Range range{ static_cast<std::size_t>(_trackContainer->getCount()), _tracksBatchSize }; const db::Range range{ static_cast<std::size_t>(_trackContainer->getCount()), _tracksBatchSize };
db::Track::FindParameters params; db::Track::FindParameters params;
params.setFilters(_filters.getDbFilters());
params.setArtist(_artistId); params.setArtist(_artistId);
params.setRange(range); params.setRange(range);
params.setSortMethod(db::TrackSortMethod::Name); params.setSortMethod(db::TrackSortMethod::Name);
@@ -390,5 +384,4 @@ namespace lms::ui
return areTracksAdded; return areTracksAdded;
} }
} // namespace lms::ui } // namespace lms::ui
+5 -1
View File
@@ -21,11 +21,14 @@
#include <map> #include <map>
#include "core/EnumSet.hpp"
#include "database/Object.hpp" #include "database/Object.hpp"
#include "database/objects/ArtistId.hpp" #include "database/objects/ArtistId.hpp"
#include "database/objects/ArtworkId.hpp" #include "database/objects/ArtworkId.hpp"
#include "database/objects/ReleaseId.hpp" #include "database/objects/ReleaseId.hpp"
#include "ReleaseHelpers.hpp"
#include "ReleaseTypes.hpp" #include "ReleaseTypes.hpp"
#include "common/Template.hpp" #include "common/Template.hpp"
@@ -57,7 +60,7 @@ namespace lms::ui
void refreshLinks(const db::ObjectPtr<db::Artist>& artist); void refreshLinks(const db::ObjectPtr<db::Artist>& artist);
struct ReleaseContainer; struct ReleaseContainer;
void addSomeReleases(ReleaseContainer& releaseContainer); void addSomeReleases(ReleaseContainer& releaseContainer, core::EnumSet<releaseListHelpers::DisplayOptions> displayOptions);
bool addSomeNonReleaseTracks(); bool addSomeNonReleaseTracks();
static constexpr std::size_t _releasesBatchSize{ 6 }; static constexpr std::size_t _releasesBatchSize{ 6 };
static constexpr std::size_t _tracksBatchSize{ 6 }; static constexpr std::size_t _tracksBatchSize{ 6 };
@@ -76,5 +79,6 @@ namespace lms::ui
ReleaseContainer _appearsOnReleaseContainer{}; ReleaseContainer _appearsOnReleaseContainer{};
InfiniteScrollingContainer* _trackContainer{}; InfiniteScrollingContainer* _trackContainer{};
db::ArtistId _artistId{}; db::ArtistId _artistId{};
std::string _artistName;
}; };
} // namespace lms::ui } // namespace lms::ui
+11 -9
View File
@@ -27,12 +27,14 @@
#include "database/objects/TrackArtistLink.hpp" #include "database/objects/TrackArtistLink.hpp"
#include "ArtistListHelpers.hpp" #include "ArtistListHelpers.hpp"
#include "ArtistTypeSelector.hpp"
#include "Filters.hpp" #include "Filters.hpp"
#include "LmsApplication.hpp" #include "LmsApplication.hpp"
#include "SortModeSelector.hpp" #include "SortModeSelector.hpp"
#include "State.hpp" #include "State.hpp"
#include "TrackArtistLinkTypeSelector.hpp"
#include "common/InfiniteScrollingContainer.hpp" #include "common/InfiniteScrollingContainer.hpp"
#include "database/objects/Types.hpp"
#include "explore/ArtistType.hpp"
namespace lms::ui namespace lms::ui
{ {
@@ -61,13 +63,13 @@ namespace lms::ui
} }
{ {
const std::optional<db::TrackArtistLinkType> linkType{ state::readValue<db::TrackArtistLinkType>("artists_link_type") }; ArtistType artistType{ state::readValue<ArtistType>("artists_type").value_or(_defaultArtistType) };
_artistCollector.setArtistLinkType(linkType); _artistCollector.setArtistType(artistType);
TrackArtistLinkTypeSelector* linkTypeSelector{ bindNew<TrackArtistLinkTypeSelector>("link-type", linkType) }; ArtistTypeSelector* artistTypeSelector{ bindNew<ArtistTypeSelector>("artist-type", artistType) };
linkTypeSelector->itemSelected.connect([this](std::optional<db::TrackArtistLinkType> newLinkType) { artistTypeSelector->itemSelected.connect([this](ArtistType newArtistType) {
state::writeValue<db::TrackArtistLinkType>("artists_link_type", newLinkType); state::writeValue<ArtistType>("artists_type", newArtistType);
refreshView(newLinkType); refreshView(newArtistType);
}); });
} }
@@ -95,9 +97,9 @@ namespace lms::ui
refreshView(); refreshView();
} }
void Artists::refreshView(std::optional<db::TrackArtistLinkType> linkType) void Artists::refreshView(ArtistType artistType)
{ {
_artistCollector.setArtistLinkType(linkType); _artistCollector.setArtistType(artistType);
refreshView(); refreshView();
} }
+3 -5
View File
@@ -19,14 +19,11 @@
#pragma once #pragma once
#include <optional>
#include <Wt/WComboBox.h> #include <Wt/WComboBox.h>
#include <Wt/WTemplate.h> #include <Wt/WTemplate.h>
#include "database/Types.hpp"
#include "ArtistCollector.hpp" #include "ArtistCollector.hpp"
#include "ArtistType.hpp"
#include "common/Template.hpp" #include "common/Template.hpp"
namespace lms::ui namespace lms::ui
@@ -42,7 +39,7 @@ namespace lms::ui
private: private:
void refreshView(); void refreshView();
void refreshView(ArtistCollector::Mode mode); void refreshView(ArtistCollector::Mode mode);
void refreshView(std::optional<db::TrackArtistLinkType> linkType); void refreshView(ArtistType artistType);
void refreshView(const Wt::WString& searchText); void refreshView(const Wt::WString& searchText);
void addSome(); void addSome();
@@ -53,5 +50,6 @@ namespace lms::ui
InfiniteScrollingContainer* _container{}; InfiniteScrollingContainer* _container{};
ArtistCollector _artistCollector; ArtistCollector _artistCollector;
static constexpr ArtistCollector::Mode _defaultSortMode{ ArtistCollector::Mode::Random }; static constexpr ArtistCollector::Mode _defaultSortMode{ ArtistCollector::Mode::Random };
static constexpr ArtistType _defaultArtistType{ ReleaseArtistsTag{} };
}; };
} // namespace lms::ui } // namespace lms::ui
+39 -66
View File
@@ -34,80 +34,53 @@
namespace lms::ui::releaseListHelpers namespace lms::ui::releaseListHelpers
{ {
namespace std::unique_ptr<Wt::WTemplate> createEntry(const db::Release::pointer& release, core::EnumSet<DisplayOptions> options)
{ {
enum class ReleaseOptions auto entry{ std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Releases.template.entry-grid")) };
entry->bindWidget("release-name", utils::createReleaseAnchor(release));
entry->addFunction("tr", &Wt::WTemplate::Functions::tr);
{ {
ShowArtist, Wt::WAnchor* anchor{ entry->bindWidget("cover", utils::createReleaseAnchor(release, false)) };
ShowYearAndOriginalYear,
ShowYear,
};
std::unique_ptr<Wt::WTemplate> createEntryInternal(const db::Release::pointer& release, const db::Artist::pointer& artist, core::EnumSet<ReleaseOptions> options) std::unique_ptr<Wt::WImage> image;
{ if (release->getPreferredArtworkId().isValid())
auto entry{ std::make_unique<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Releases.template.entry-grid")) }; image = utils::createArtworkImage(release->getPreferredArtworkId(), ArtworkResource::DefaultArtworkType::Release, ArtworkResource::Size::Large);
else
image = utils::createDefaultArtworkImage(ArtworkResource::DefaultArtworkType::Release);
entry->bindWidget("release-name", utils::createReleaseAnchor(release)); image->addStyleClass("Lms-cover-release Lms-cover-anchor rounded"); // hack
entry->addFunction("tr", &Wt::WTemplate::Functions::tr); anchor->setImage(std::move(image));
{
Wt::WAnchor* anchor{ entry->bindWidget("cover", utils::createReleaseAnchor(release, false)) };
std::unique_ptr<Wt::WImage> image;
if (release->getPreferredArtworkId().isValid())
image = utils::createArtworkImage(release->getPreferredArtworkId(), ArtworkResource::DefaultArtworkType::Release, ArtworkResource::Size::Large);
else
image = utils::createDefaultArtworkImage(ArtworkResource::DefaultArtworkType::Release);
image->addStyleClass("Lms-cover-release Lms-cover-anchor rounded"); // hack
anchor->setImage(std::move(image));
}
if (options.contains(ReleaseOptions::ShowArtist))
{
auto artistAnchors{ utils::createArtistsAnchorsForRelease(release, artist ? artist->getId() : db::ArtistId{}, "link-secondary") };
if (artistAnchors)
{
entry->setCondition("if-has-artist", true);
entry->bindWidget("artist-name", std::move(artistAnchors));
}
}
if (options.contains(ReleaseOptions::ShowYearAndOriginalYear))
{
Wt::WString year{ releaseHelpers::buildReleaseYearString(release->getYear(), release->getOriginalYear()) };
if (!year.empty())
{
entry->setCondition("if-has-year", true);
entry->bindString("year", year, Wt::TextFormat::Plain);
}
}
else if (options.contains(ReleaseOptions::ShowYear))
{
if (release->getYear())
{
entry->setCondition("if-has-year", true);
entry->bindInt("year", *release->getYear());
}
}
return entry;
} }
} // namespace
std::unique_ptr<Wt::WTemplate> createEntry(const db::Release::pointer& release) if (options.contains(DisplayOptions::ShowArtist))
{ {
return createEntryInternal(release, db::Artist::pointer{}, core::EnumSet<ReleaseOptions>{ ReleaseOptions::ShowArtist }); utils::ArtistDisplayInfo artistDisplayInfo{ utils::computeArtistDisplayInfo(release) };
}
std::unique_ptr<Wt::WTemplate> createEntryForArtist(const db::Release::pointer& release, const db::Artist::pointer& artist) entry->setCondition("if-has-artist", true);
{ entry->bindWidget("artist-name", utils::createArtistsAnchors(artistDisplayInfo, "link-secondary"));
return createEntryInternal(release, artist, core::EnumSet<ReleaseOptions>{ ReleaseOptions::ShowArtist, ReleaseOptions::ShowYearAndOriginalYear }); }
}
std::unique_ptr<Wt::WTemplate> createEntryForOtherVersions(const db::ObjectPtr<db::Release>& release) if (options.contains(DisplayOptions::ShowYearAndOriginalYear))
{ {
return createEntryInternal(release, db::Artist::pointer{}, core::EnumSet<ReleaseOptions>{ ReleaseOptions::ShowYear }); Wt::WString year{ releaseHelpers::buildReleaseYearString(release->getYear(), release->getOriginalYear()) };
if (!year.empty())
{
entry->setCondition("if-has-year", true);
entry->bindString("year", year, Wt::TextFormat::Plain);
}
}
else if (options.contains(DisplayOptions::ShowYear))
{
if (release->getYear())
{
entry->setCondition("if-has-year", true);
entry->bindInt("year", *release->getYear());
}
}
return entry;
} }
} // namespace lms::ui::releaseListHelpers } // namespace lms::ui::releaseListHelpers
+8 -3
View File
@@ -38,9 +38,14 @@ namespace lms::db
namespace lms::ui::releaseListHelpers namespace lms::ui::releaseListHelpers
{ {
std::unique_ptr<Wt::WTemplate> createEntry(const db::ObjectPtr<db::Release>& release); enum class DisplayOptions
std::unique_ptr<Wt::WTemplate> createEntryForArtist(const db::ObjectPtr<db::Release>& release, const db::ObjectPtr<db::Artist>& artist); {
std::unique_ptr<Wt::WTemplate> createEntryForOtherVersions(const db::ObjectPtr<db::Release>& release); ShowArtist,
ShowYearAndOriginalYear,
ShowYear,
};
std::unique_ptr<Wt::WTemplate> createEntry(const db::ObjectPtr<db::Release>& release, core::EnumSet<DisplayOptions> options);
} // namespace lms::ui::releaseListHelpers } // namespace lms::ui::releaseListHelpers
namespace lms::ui::releaseHelpers namespace lms::ui::releaseHelpers
+38 -73
View File
@@ -78,53 +78,14 @@ namespace lms::ui
releaseInfo->bindString("release-type", releaseHelpers::buildReleaseTypeString(parseReleaseType(releaseTypeNames))); releaseInfo->bindString("release-type", releaseHelpers::buildReleaseTypeString(parseReleaseType(releaseTypeNames)));
} }
std::map<Wt::WString, std::set<db::ArtistId>> artistMap; if (const auto artistsByRole{ utils::getTrackArtistsByRole(release) }; !artistsByRole.empty())
auto addArtists = [&](db::TrackArtistLinkType linkType, const char* type) {
db::Artist::FindParameters params;
params.setRelease(releaseId);
params.setLinkType(linkType);
const auto artistIds{ db::Artist::findIds(LmsApp->getDbSession(), params) };
if (artistIds.results.empty())
return;
Wt::WString typeStr{ Wt::WString::trn(type, artistIds.results.size()) };
for (db::ArtistId artistId : artistIds.results)
artistMap[typeStr].insert(artistId);
};
auto addPerformerArtists = [&] {
db::TrackArtistLink::FindParameters params;
params.setRelease(releaseId);
params.setLinkType(db::TrackArtistLinkType::Performer);
db::TrackArtistLink::find(LmsApp->getDbSession(), params, [&](const db::TrackArtistLink::pointer& link) {
artistMap[std::string{ link->getSubType() }].insert(link->getArtist()->getId());
});
};
addArtists(db::TrackArtistLinkType::Composer, "Lms.Explore.Artists.linktype-composer");
addArtists(db::TrackArtistLinkType::Conductor, "Lms.Explore.Artists.linktype-conductor");
addArtists(db::TrackArtistLinkType::Lyricist, "Lms.Explore.Artists.linktype-lyricist");
addArtists(db::TrackArtistLinkType::Mixer, "Lms.Explore.Artists.linktype-mixer");
addArtists(db::TrackArtistLinkType::Remixer, "Lms.Explore.Artists.linktype-remixer");
addArtists(db::TrackArtistLinkType::Producer, "Lms.Explore.Artists.linktype-producer");
addPerformerArtists();
if (auto itRolelessPerformers{ artistMap.find("") }; itRolelessPerformers != std::cend(artistMap))
{
Wt::WString performersStr{ Wt::WString::trn("Lms.Explore.Artists.linktype-performer", itRolelessPerformers->second.size()) };
artistMap[performersStr] = std::move(itRolelessPerformers->second);
artistMap.erase(itRolelessPerformers);
}
if (!artistMap.empty())
{ {
releaseInfo->setCondition("if-has-artist", true); releaseInfo->setCondition("if-has-artist", true);
Wt::WContainerWidget* artistTable{ releaseInfo->bindNew<Wt::WContainerWidget>("artist-table") }; Wt::WContainerWidget* artistTable{ releaseInfo->bindNew<Wt::WContainerWidget>("artist-table") };
for (const auto& [role, artistIds] : artistMap) for (const auto& [role, artists] : artistsByRole)
{ {
std::unique_ptr<Wt::WContainerWidget> artistContainer{ utils::createArtistAnchorList(std::vector(std::cbegin(artistIds), std::cend(artistIds))) }; std::unique_ptr<Wt::WContainerWidget> artistContainer{ utils::createArtistAnchorList(artists) };
auto artistsEntry{ std::make_unique<Template>(Wt::WString::tr("Lms.Explore.template.info.artists")) }; auto artistsEntry{ std::make_unique<Template>(Wt::WString::tr("Lms.Explore.template.info.artists")) };
artistsEntry->bindString("type", role); artistsEntry->bindString("type", role);
artistsEntry->bindWidget("artist-container", std::move(artistContainer)); artistsEntry->bindWidget("artist-container", std::move(artistContainer));
@@ -146,7 +107,7 @@ namespace lms::ui
{ {
if (!codecStr.empty()) if (!codecStr.empty())
codecStr += " · "; codecStr += " · ";
codecStr += core::media::getCodecDesc(codec).name.str(); codecStr += core::media::getCodecDesc(codec).longName.str();
} }
if (!codecStr.empty()) if (!codecStr.empty())
@@ -190,7 +151,7 @@ namespace lms::ui
return core::stringUtils::readAs<db::ReleaseId::ValueType>(wApp->internalPathNextPart("/release/")); return core::stringUtils::readAs<db::ReleaseId::ValueType>(wApp->internalPathNextPart("/release/"));
} }
void fillTrackArtistLinks(Wt::WTemplate* trackEntry, db::TrackId trackId) void fillTrackArtistLinks(Wt::WTemplate* trackEntry, const db::Track::pointer& track)
{ {
const db::User::pointer user{ LmsApp->getUser() }; const db::User::pointer user{ LmsApp->getUser() };
if (!user->getUIEnableInlineArtistRelationships()) if (!user->getUIEnableInlineArtistRelationships())
@@ -200,36 +161,31 @@ namespace lms::ui
if (inlineArtistRelationships.empty()) if (inlineArtistRelationships.empty())
return; return;
const std::map<Wt::WString, std::set<db::ArtistId>> artistsByRole{ TrackListHelpers::getArtistsByRole(trackId, inlineArtistRelationships) }; const std::map<Wt::WString, std::vector<db::Artist::pointer>> artistsByRole{ utils::getArtistsByRole(track, inlineArtistRelationships) };
if (artistsByRole.empty()) if (artistsByRole.empty())
return; return;
trackEntry->setCondition("if-has-artist-links", true); trackEntry->setCondition("if-has-artist-links", true);
Wt::WContainerWidget* artistLinksContainer = trackEntry->bindNew<Wt::WContainerWidget>("artist-links"); Wt::WContainerWidget* artistLinksContainer{ trackEntry->bindNew<Wt::WContainerWidget>("artist-links") };
for (const auto& [role, artists] : artistsByRole) for (const auto& [role, artists] : artistsByRole)
{ {
Wt::WTemplate* artistLinkEntry{ artistLinksContainer->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Release.template.artist-links-entry")) }; Wt::WTemplate* artistLinkEntry{ artistLinksContainer->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Release.template.artist-links-entry")) };
artistLinkEntry->bindString("role", role, Wt::TextFormat::Plain); artistLinkEntry->bindString("role", role, Wt::TextFormat::Plain);
artistLinkEntry->bindWidget("anchors", utils::createArtistAnchorList(std::vector<db::ArtistId>(std::cbegin(artists), std::cend(artists)))); artistLinkEntry->bindWidget("anchors", utils::createArtistAnchorList(artists));
} }
} }
bool shouldDisplayTrackArtists(db::ReleaseId releaseId) bool shouldDisplayTrackArtists(const db::Release::pointer& release)
{ {
bool res{ true }; bool res{ true };
db::Artist::FindParameters params; // TODO, just compare artist display names
params.setRelease(releaseId); const auto trackArtists{ release->getTrackArtists(db::TrackArtistLinkType::Artist) };
params.setLinkType(db::TrackArtistLinkType::ReleaseArtist); if (trackArtists.size() == 1)
auto releaseArtists{ db::Artist::findIds(LmsApp->getDbSession(), params) };
params.setLinkType(db::TrackArtistLinkType::Artist);
auto trackArtists{ db::Artist::findIds(LmsApp->getDbSession(), params) };
if (trackArtists.results.size() == 1)
{ {
if (releaseArtists.results.empty() || trackArtists.results == releaseArtists.results) const auto releaseArtists{ release->getArtists() };
if (releaseArtists.empty() || trackArtists == releaseArtists)
res = false; res = false;
} }
@@ -409,8 +365,7 @@ namespace lms::ui
void Release::refreshReleaseArtists(const db::Release::pointer& release) void Release::refreshReleaseArtists(const db::Release::pointer& release)
{ {
auto container{ utils::createArtistsAnchorsForRelease(release) }; if (auto container{ utils::createArtistsAnchors(release) })
if (container)
{ {
setCondition("if-has-release-artists", true); setCondition("if-has-release-artists", true);
bindWidget("artists", std::move(container)); bindWidget("artists", std::move(container));
@@ -424,7 +379,7 @@ namespace lms::ui
if (mediums.empty()) if (mediums.empty())
return; return;
const bool displayTrackArtists{ shouldDisplayTrackArtists(_releaseId) }; const bool displayTrackArtists{ shouldDisplayTrackArtists(release) };
const auto totalDisc{ release->getTotalDisc() }; const auto totalDisc{ release->getTotalDisc() };
const bool createDiscs{ mediums.size() > 1 const bool createDiscs{ mediums.size() > 1
@@ -456,16 +411,16 @@ namespace lms::ui
if (displayTrackArtists) if (displayTrackArtists)
{ {
const auto artists{ track->getArtistIds({ db::TrackArtistLinkType::Artist }) }; const auto artistDisplayInfo{ utils::computeArtistDisplayInfo(track, db::TrackArtistLinkType::Artist) };
if (!artists.empty()) if (!artistDisplayInfo.entries.empty())
{ {
entry->setCondition("if-has-artists", true); entry->setCondition("if-has-artists", true);
entry->bindWidget("artists", utils::createArtistDisplayNameWithAnchors(track->getArtistDisplayName(), artists)); entry->bindWidget("artists", utils::createArtistsAnchors(artistDisplayInfo));
entry->bindWidget("artists-md", utils::createArtistDisplayNameWithAnchors(track->getArtistDisplayName(), artists)); entry->bindWidget("artists-md", utils::createArtistsAnchors(artistDisplayInfo));
} }
} }
fillTrackArtistLinks(entry, track->getId()); fillTrackArtistLinks(entry, track);
auto trackNumber{ track->getTrackNumber() }; auto trackNumber{ track->getTrackNumber() };
if (trackNumber) if (trackNumber)
@@ -475,12 +430,14 @@ namespace lms::ui
} }
Wt::WPushButton* playBtn{ entry->bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.template.play-btn"), Wt::TextFormat::XHTML) }; Wt::WPushButton* playBtn{ entry->bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.template.play-btn"), Wt::TextFormat::XHTML) };
playBtn->setAttributeValue("aria-label", Wt::WString::tr("Lms.play-item").arg(track->getName()));
playBtn->clicked().connect([this, trackId] { playBtn->clicked().connect([this, trackId] {
_playQueueController.playTrackInRelease(trackId); _playQueueController.playTrackInRelease(trackId);
}); });
{ {
entry->bindNew<Wt::WPushButton>("more-btn", Wt::WString::tr("Lms.template.more-btn"), Wt::TextFormat::XHTML); entry->bindNew<Wt::WPushButton>("more-btn", Wt::WString::tr("Lms.template.more-btn"), Wt::TextFormat::XHTML)
->setAttributeValue("aria-label", Wt::WString::tr("Lms.more"));
entry->bindNew<Wt::WPushButton>("play", Wt::WString::tr("Lms.Explore.play")) entry->bindNew<Wt::WPushButton>("play", Wt::WString::tr("Lms.Explore.play"))
->clicked() ->clicked()
.connect([this, trackId] { .connect([this, trackId] {
@@ -501,6 +458,8 @@ namespace lms::ui
auto isStarred{ [=] { return core::Service<feedback::IFeedbackService>::get()->isStarred(LmsApp->getUserId(), trackId); } }; auto isStarred{ [=] { return core::Service<feedback::IFeedbackService>::get()->isStarred(LmsApp->getUserId(), trackId); } };
Wt::WPushButton* starBtn{ entry->bindNew<Wt::WPushButton>("star-btn", Wt::WString::tr(isStarred() ? "Lms.template.unstar-btn" : "Lms.template.star-btn"), Wt::TextFormat::XHTML) }; Wt::WPushButton* starBtn{ entry->bindNew<Wt::WPushButton>("star-btn", Wt::WString::tr(isStarred() ? "Lms.template.unstar-btn" : "Lms.template.star-btn"), Wt::TextFormat::XHTML) };
starBtn->setAttributeValue("aria-pressed", isStarred() ? "true" : "false");
starBtn->setAttributeValue("aria-label", Wt::WString::tr("Lms.Explore.star-item").arg(track->getName()));
Wt::WPushButton* starMenuEntry{ entry->bindNew<Wt::WPushButton>("star", Wt::WString::tr(isStarred() ? "Lms.Explore.unstar" : "Lms.Explore.star")) }; Wt::WPushButton* starMenuEntry{ entry->bindNew<Wt::WPushButton>("star", Wt::WString::tr(isStarred() ? "Lms.Explore.unstar" : "Lms.Explore.star")) };
auto toggle{ [=] { auto toggle{ [=] {
@@ -511,12 +470,14 @@ namespace lms::ui
core::Service<feedback::IFeedbackService>::get()->unstar(LmsApp->getUserId(), trackId); core::Service<feedback::IFeedbackService>::get()->unstar(LmsApp->getUserId(), trackId);
starMenuEntry->setText(Wt::WString::tr("Lms.Explore.star")); starMenuEntry->setText(Wt::WString::tr("Lms.Explore.star"));
starBtn->setText(Wt::WString::tr("Lms.template.star-btn")); starBtn->setText(Wt::WString::tr("Lms.template.star-btn"));
starBtn->setAttributeValue("aria-pressed", "false");
} }
else else
{ {
core::Service<feedback::IFeedbackService>::get()->star(LmsApp->getUserId(), trackId); core::Service<feedback::IFeedbackService>::get()->star(LmsApp->getUserId(), trackId);
starMenuEntry->setText(Wt::WString::tr("Lms.Explore.unstar")); starMenuEntry->setText(Wt::WString::tr("Lms.Explore.unstar"));
starBtn->setText(Wt::WString::tr("Lms.template.unstar-btn")); starBtn->setText(Wt::WString::tr("Lms.template.unstar-btn"));
starBtn->setAttributeValue("aria-pressed", "true");
} }
} }; } };
@@ -622,7 +583,7 @@ namespace lms::ui
if (!otherVersionRelease) if (!otherVersionRelease)
continue; continue;
container->addWidget(releaseListHelpers::createEntryForOtherVersions(otherVersionRelease)); container->addWidget(releaseListHelpers::createEntry(otherVersionRelease, { releaseListHelpers::DisplayOptions::ShowYear }));
} }
} }
@@ -640,12 +601,11 @@ namespace lms::ui
if (!similarRelease) if (!similarRelease)
continue; continue;
similarReleasesContainer->addWidget(releaseListHelpers::createEntry(similarRelease)); similarReleasesContainer->addWidget(releaseListHelpers::createEntry(similarRelease, { releaseListHelpers::DisplayOptions::ShowArtist }));
} }
} }
std::unique_ptr<Wt::WTemplate> Release::createDisc(const db::Medium::pointer& medium) std::unique_ptr<Wt::WTemplate> Release::createDisc(const db::Medium::pointer& medium)
{ {
const db::MediumId mediumId{ medium->getId() }; const db::MediumId mediumId{ medium->getId() };
@@ -665,16 +625,21 @@ namespace lms::ui
disc->bindWidget<Wt::WImage>("artwork", std::move(image)); disc->bindWidget<Wt::WImage>("artwork", std::move(image));
} }
Wt::WString discTitle;
if (medium->getName().empty()) if (medium->getName().empty())
disc->bindNew<Wt::WText>("disc-title", Wt::WString::tr("Lms.Explore.Release.disc").arg(medium->getPosition() ? *medium->getPosition() : 1 /* TODO */)); discTitle = Wt::WString::tr("Lms.Explore.Release.disc").arg(medium->getPosition() ? *medium->getPosition() : 1 /* TODO */);
else else
disc->bindString("disc-title", Wt::WString::fromUTF8(std::string{ medium->getName() }), Wt::TextFormat::Plain); discTitle = Wt::WString::fromUTF8(std::string{ medium->getName() });
disc->bindNew<Wt::WText>("disc-title", discTitle, Wt::TextFormat::Plain);
Wt::WPushButton* playBtn{ disc->bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.template.play-btn"), Wt::TextFormat::XHTML) }; Wt::WPushButton* playBtn{ disc->bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.template.play-btn"), Wt::TextFormat::XHTML) };
playBtn->setAttributeValue("aria-label", Wt::WString::tr("Lms.play-item").arg(discTitle));
playBtn->clicked().connect([this, mediumId] { playBtn->clicked().connect([this, mediumId] {
_playQueueController.processCommand(PlayQueueController::Command::Play, mediumId); _playQueueController.processCommand(PlayQueueController::Command::Play, mediumId);
}); });
disc->bindNew<Wt::WPushButton>("more-btn", Wt::WString::tr("Lms.template.more-btn"), Wt::TextFormat::XHTML); disc->bindNew<Wt::WPushButton>("more-btn", Wt::WString::tr("Lms.template.more-btn"), Wt::TextFormat::XHTML)
->setAttributeValue("aria-label", Wt::WString::tr("Lms.more"));
;
disc->bindNew<Wt::WPushButton>("play", Wt::WString::tr("Lms.Explore.play")) disc->bindNew<Wt::WPushButton>("play", Wt::WString::tr("Lms.Explore.play"))
->clicked() ->clicked()
.connect([this, mediumId] { .connect([this, mediumId] {
+1 -1
View File
@@ -122,7 +122,7 @@ namespace lms::ui
for (const db::ReleaseId releaseId : releaseIds.results) for (const db::ReleaseId releaseId : releaseIds.results)
{ {
if (const db::Release::pointer release{ db::Release::find(LmsApp->getDbSession(), releaseId) }) if (const db::Release::pointer release{ db::Release::find(LmsApp->getDbSession(), releaseId) })
_container->add(releaseListHelpers::createEntry(release)); _container->add(releaseListHelpers::createEntry(release, { releaseListHelpers::DisplayOptions::ShowArtist }));
} }
} }
@@ -1,48 +0,0 @@
/*
* Copyright (C) 2024 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include <optional>
#include "database/Types.hpp"
#include "DropDownMenuSelector.hpp"
namespace lms::ui
{
class TrackArtistLinkTypeSelector : public DropDownMenuSelector<std::optional<db::TrackArtistLinkType>>
{
public:
TrackArtistLinkTypeSelector(std::optional<db::TrackArtistLinkType> defaultLinkType)
: DropDownMenuSelector{ Wt::WString::tr("Lms.Explore.Artists.template.track-artist-link-type-selector"), defaultLinkType }
{
bindItem("link-type-all", Wt::WString::tr("Lms.Explore.Artists.linktype-all"), std::nullopt);
bindItem("link-type-artist", Wt::WString::trn("Lms.Explore.Artists.linktype-artist", 2), db::TrackArtistLinkType::Artist);
bindItem("link-type-releaseartist", Wt::WString::trn("Lms.Explore.Artists.linktype-releaseartist", 2), db::TrackArtistLinkType::ReleaseArtist);
bindItem("link-type-composer", Wt::WString::trn("Lms.Explore.Artists.linktype-composer", 2), db::TrackArtistLinkType::Composer);
bindItem("link-type-conductor", Wt::WString::trn("Lms.Explore.Artists.linktype-conductor", 2), db::TrackArtistLinkType::Conductor);
bindItem("link-type-lyricist", Wt::WString::trn("Lms.Explore.Artists.linktype-lyricist", 2), db::TrackArtistLinkType::Lyricist);
bindItem("link-type-mixer", Wt::WString::trn("Lms.Explore.Artists.linktype-mixer", 2), db::TrackArtistLinkType::Mixer);
bindItem("link-type-performer", Wt::WString::trn("Lms.Explore.Artists.linktype-performer", 2), db::TrackArtistLinkType::Performer);
bindItem("link-type-producer", Wt::WString::trn("Lms.Explore.Artists.linktype-producer", 2), db::TrackArtistLinkType::Producer);
bindItem("link-type-remixer", Wt::WString::trn("Lms.Explore.Artists.linktype-remixer", 2), db::TrackArtistLinkType::Remixer);
}
};
} // namespace lms::ui
+17 -63
View File
@@ -47,58 +47,6 @@
namespace lms::ui::TrackListHelpers namespace lms::ui::TrackListHelpers
{ {
std::map<Wt::WString, std::set<db::ArtistId>> getArtistsByRole(db::TrackId trackId, core::EnumSet<db::TrackArtistLinkType> artistLinkTypes)
{
std::map<Wt::WString, std::set<db::ArtistId>> artistMap;
auto addArtists = [&](db::TrackArtistLinkType linkType, const char* type) {
if (!artistLinkTypes.contains(linkType))
return;
db::Artist::FindParameters params;
params.setTrack(trackId);
params.setLinkType(linkType);
const auto artistIds{ db::Artist::findIds(LmsApp->getDbSession(), params) };
if (artistIds.results.empty())
return;
Wt::WString typeStr{ Wt::WString::trn(type, artistIds.results.size()) };
for (db::ArtistId artistId : artistIds.results)
artistMap[typeStr].insert(artistId);
};
auto addPerformerArtists = [&] {
if (!artistLinkTypes.contains(db::TrackArtistLinkType::Performer))
return;
db::TrackArtistLink::FindParameters params;
params.setTrack(trackId);
params.setLinkType(db::TrackArtistLinkType::Performer);
db::TrackArtistLink::find(LmsApp->getDbSession(), params, [&](const db::TrackArtistLink::pointer& link) {
artistMap[std::string{ link->getSubType() }].insert(link->getArtist()->getId());
});
};
addArtists(db::TrackArtistLinkType::Composer, "Lms.Explore.Artists.linktype-composer");
addArtists(db::TrackArtistLinkType::Conductor, "Lms.Explore.Artists.linktype-conductor");
addArtists(db::TrackArtistLinkType::Lyricist, "Lms.Explore.Artists.linktype-lyricist");
addArtists(db::TrackArtistLinkType::Mixer, "Lms.Explore.Artists.linktype-mixer");
addArtists(db::TrackArtistLinkType::Remixer, "Lms.Explore.Artists.linktype-remixer");
addArtists(db::TrackArtistLinkType::Producer, "Lms.Explore.Artists.linktype-producer");
addPerformerArtists();
if (auto itRolelessPerformers{ artistMap.find("") }; itRolelessPerformers != std::cend(artistMap))
{
Wt::WString performersStr{ Wt::WString::trn("Lms.Explore.Artists.linktype-performer", itRolelessPerformers->second.size()) };
artistMap[performersStr] = std::move(itRolelessPerformers->second);
artistMap.erase(itRolelessPerformers);
}
return artistMap;
}
void showTrackInfoModal(db::TrackId trackId, Filters& filters) void showTrackInfoModal(db::TrackId trackId, Filters& filters)
{ {
auto transaction{ LmsApp->getDbSession().createReadTransaction() }; auto transaction{ LmsApp->getDbSession().createReadTransaction() };
@@ -111,18 +59,18 @@ namespace lms::ui::TrackListHelpers
Wt::WWidget* trackInfoPtr{ trackInfo.get() }; Wt::WWidget* trackInfoPtr{ trackInfo.get() };
trackInfo->addFunction("tr", &Wt::WTemplate::Functions::tr); trackInfo->addFunction("tr", &Wt::WTemplate::Functions::tr);
std::map<Wt::WString, std::set<db::ArtistId>> artistMap{ getArtistsByRole(trackId) }; const auto artistsByRole{ utils::getArtistsByRole(track) };
if (!artistMap.empty()) if (!artistsByRole.empty())
{ {
trackInfo->setCondition("if-has-artist", true); trackInfo->setCondition("if-has-artist", true);
Wt::WContainerWidget* artistTable{ trackInfo->bindNew<Wt::WContainerWidget>("artist-table") }; Wt::WContainerWidget* artistTable{ trackInfo->bindNew<Wt::WContainerWidget>("artist-table") };
for (const auto& [role, artistIds] : artistMap) for (const auto& [role, artists] : artistsByRole)
{ {
std::unique_ptr<Wt::WContainerWidget> artistContainer{ utils::createArtistAnchorList(std::vector(std::cbegin(artistIds), std::cend(artistIds))) };
auto artistsEntry{ std::make_unique<Template>(Wt::WString::tr("Lms.Explore.template.info.artists")) }; auto artistsEntry{ std::make_unique<Template>(Wt::WString::tr("Lms.Explore.template.info.artists")) };
artistsEntry->bindString("type", role, Wt::TextFormat::Plain); artistsEntry->bindString("type", role, Wt::TextFormat::Plain);
artistsEntry->bindWidget("artist-container", std::move(artistContainer)); artistsEntry->bindWidget("artist-container", utils::createArtistAnchorList(std::vector(std::cbegin(artists), std::cend(artists))));
artistTable->addWidget(std::move(artistsEntry)); artistTable->addWidget(std::move(artistsEntry));
} }
} }
@@ -130,7 +78,7 @@ namespace lms::ui::TrackListHelpers
if (const auto codec{ track->getCodec() }) if (const auto codec{ track->getCodec() })
{ {
trackInfo->setCondition("if-has-codec", true); trackInfo->setCondition("if-has-codec", true);
trackInfo->bindString("codec", core::media::getCodecDesc(*codec).name.c_str(), Wt::TextFormat::Plain); trackInfo->bindString("codec", core::media::getCodecDesc(*codec).longName.c_str(), Wt::TextFormat::Plain);
} }
trackInfo->bindString("duration", utils::durationToString(track->getDuration())); trackInfo->bindString("duration", utils::durationToString(track->getDuration()));
@@ -216,12 +164,12 @@ namespace lms::ui::TrackListHelpers
const db::Release::pointer release{ track->getRelease() }; const db::Release::pointer release{ track->getRelease() };
const db::TrackId trackId{ track->getId() }; const db::TrackId trackId{ track->getId() };
const auto artists{ track->getArtistIds({ db::TrackArtistLinkType::Artist }) }; const auto artistDisplayInfo{ utils::computeArtistDisplayInfo(track, db::TrackArtistLinkType::Artist) };
if (!artists.empty()) if (!artistDisplayInfo.entries.empty())
{ {
entry->setCondition("if-has-artists", true); entry->setCondition("if-has-artists", true);
entry->bindWidget("artists", utils::createArtistDisplayNameWithAnchors(track->getArtistDisplayName(), artists)); entry->bindWidget("artists", utils::createArtistsAnchors(artistDisplayInfo));
entry->bindWidget("artists-md", utils::createArtistDisplayNameWithAnchors(track->getArtistDisplayName(), artists)); entry->bindWidget("artists-md", utils::createArtistsAnchors(artistDisplayInfo));
} }
std::unique_ptr<Wt::WImage> image; std::unique_ptr<Wt::WImage> image;
@@ -250,11 +198,13 @@ namespace lms::ui::TrackListHelpers
entry->bindString("duration", utils::durationToString(track->getDuration()), Wt::TextFormat::Plain); entry->bindString("duration", utils::durationToString(track->getDuration()), Wt::TextFormat::Plain);
Wt::WPushButton* playBtn{ entry->bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.template.play-btn"), Wt::TextFormat::XHTML) }; Wt::WPushButton* playBtn{ entry->bindNew<Wt::WPushButton>("play-btn", Wt::WString::tr("Lms.template.play-btn"), Wt::TextFormat::XHTML) };
playBtn->setAttributeValue("aria-label", Wt::WString::tr("Lms.play-item").arg(track->getName()));
playBtn->clicked().connect([trackId, &playQueueController] { playBtn->clicked().connect([trackId, &playQueueController] {
playQueueController.processCommand(PlayQueueController::Command::Play, { trackId }); playQueueController.processCommand(PlayQueueController::Command::Play, { trackId });
}); });
entry->bindNew<Wt::WPushButton>("more-btn", Wt::WString::tr("Lms.template.more-btn"), Wt::TextFormat::XHTML); entry->bindNew<Wt::WPushButton>("more-btn", Wt::WString::tr("Lms.template.more-btn"), Wt::TextFormat::XHTML)
->setAttributeValue("aria-label", Wt::WString::tr("Lms.more"));
entry->bindNew<Wt::WPushButton>("play", Wt::WString::tr("Lms.Explore.play")) entry->bindNew<Wt::WPushButton>("play", Wt::WString::tr("Lms.Explore.play"))
->clicked() ->clicked()
@@ -276,6 +226,8 @@ namespace lms::ui::TrackListHelpers
auto isStarred{ [=] { return core::Service<feedback::IFeedbackService>::get()->isStarred(LmsApp->getUserId(), trackId); } }; auto isStarred{ [=] { return core::Service<feedback::IFeedbackService>::get()->isStarred(LmsApp->getUserId(), trackId); } };
Wt::WPushButton* starBtn{ entry->bindNew<Wt::WPushButton>("star-btn", Wt::WString::tr(isStarred() ? "Lms.template.unstar-btn" : "Lms.template.star-btn"), Wt::TextFormat::XHTML) }; Wt::WPushButton* starBtn{ entry->bindNew<Wt::WPushButton>("star-btn", Wt::WString::tr(isStarred() ? "Lms.template.unstar-btn" : "Lms.template.star-btn"), Wt::TextFormat::XHTML) };
starBtn->setAttributeValue("aria-label", Wt::WString::tr("Lms.Explore.star-item").arg(track->getName()));
starBtn->setAttributeValue("aria-pressed", isStarred() ? "true" : "false");
Wt::WPushButton* starMenuEntry{ entry->bindNew<Wt::WPushButton>("star", Wt::WString::tr(isStarred() ? "Lms.Explore.unstar" : "Lms.Explore.star")) }; Wt::WPushButton* starMenuEntry{ entry->bindNew<Wt::WPushButton>("star", Wt::WString::tr(isStarred() ? "Lms.Explore.unstar" : "Lms.Explore.star")) };
auto toggle{ [=] { auto toggle{ [=] {
@@ -286,12 +238,14 @@ namespace lms::ui::TrackListHelpers
core::Service<feedback::IFeedbackService>::get()->unstar(LmsApp->getUserId(), trackId); core::Service<feedback::IFeedbackService>::get()->unstar(LmsApp->getUserId(), trackId);
starMenuEntry->setText(Wt::WString::tr("Lms.Explore.star")); starMenuEntry->setText(Wt::WString::tr("Lms.Explore.star"));
starBtn->setText(Wt::WString::tr("Lms.template.star-btn")); starBtn->setText(Wt::WString::tr("Lms.template.star-btn"));
starBtn->setAttributeValue("aria-pressed", "false");
} }
else else
{ {
core::Service<feedback::IFeedbackService>::get()->star(LmsApp->getUserId(), trackId); core::Service<feedback::IFeedbackService>::get()->star(LmsApp->getUserId(), trackId);
starMenuEntry->setText(Wt::WString::tr("Lms.Explore.unstar")); starMenuEntry->setText(Wt::WString::tr("Lms.Explore.unstar"));
starBtn->setText(Wt::WString::tr("Lms.template.unstar-btn")); starBtn->setText(Wt::WString::tr("Lms.template.unstar-btn"));
starBtn->setAttributeValue("aria-pressed", "true");
} }
} }; } };
-11
View File
@@ -46,17 +46,6 @@ namespace lms::ui
namespace lms::ui::TrackListHelpers namespace lms::ui::TrackListHelpers
{ {
inline constexpr core::EnumSet<db::TrackArtistLinkType> AllArtistRoles{
db::TrackArtistLinkType::Composer,
db::TrackArtistLinkType::Conductor,
db::TrackArtistLinkType::Lyricist,
db::TrackArtistLinkType::Mixer,
db::TrackArtistLinkType::Remixer,
db::TrackArtistLinkType::Performer,
db::TrackArtistLinkType::Producer,
};
std::map<Wt::WString, std::set<db::ArtistId>> getArtistsByRole(db::TrackId trackId, core::EnumSet<db::TrackArtistLinkType> artistLinkTypes = AllArtistRoles);
void showTrackInfoModal(db::TrackId trackId, Filters& filters); void showTrackInfoModal(db::TrackId trackId, Filters& filters);
void showTrackLyricsModal(db::TrackId trackId); void showTrackLyricsModal(db::TrackId trackId);
std::unique_ptr<Wt::WWidget> createEntry(const db::ObjectPtr<db::Track>& track, PlayQueueController& playQueueController, Filters& filters); std::unique_ptr<Wt::WWidget> createEntry(const db::ObjectPtr<db::Track>& track, PlayQueueController& playQueueController, Filters& filters);
+7 -5
View File
@@ -32,7 +32,7 @@
#include "LmsApplication.hpp" #include "LmsApplication.hpp"
#define DL_RESOURCE_LOG(severity, message) LMS_LOG(UI, severity, "Download resource: " << message) #define LMS_DL_RESOURCE_LOG(severity, message) LMS_LOG(UI, severity, "Download resource: " << message)
namespace lms::ui namespace lms::ui
{ {
@@ -72,7 +72,7 @@ namespace lms::ui
} }
catch (zip::Exception& exception) catch (zip::Exception& exception)
{ {
DL_RESOURCE_LOG(ERROR, "Zipper exception: " << exception.what()); LMS_DL_RESOURCE_LOG(ERROR, "Zipper exception: " << exception.what());
} }
} }
@@ -89,9 +89,9 @@ namespace lms::ui
std::vector<db::ObjectPtr<db::Artist>> artists; std::vector<db::ObjectPtr<db::Artist>> artists;
artists = release->getReleaseArtists(); artists = release->getArtists();
if (artists.empty()) if (artists.empty())
artists = release->getArtists(); artists = release->getTrackArtists();
if (artists.size() > 1) if (artists.size() > 1)
releaseArtistName = Wt::WString::tr("Lms.Explore.various-artists").toUTF8(); releaseArtistName = Wt::WString::tr("Lms.Explore.various-artists").toUTF8();
@@ -220,7 +220,7 @@ namespace lms::ui
const db::Track::pointer track{ db::Track::find(LmsApp->getDbSession(), _trackId) }; const db::Track::pointer track{ db::Track::find(LmsApp->getDbSession(), _trackId) };
if (!track) if (!track)
{ {
DL_RESOURCE_LOG(DEBUG, "Cannot find track"); LMS_DL_RESOURCE_LOG(DEBUG, "Cannot find track");
return {}; return {};
} }
@@ -247,3 +247,5 @@ namespace lms::ui
return detail::createZipper(tracks.results); return detail::createZipper(tracks.results);
} }
} // namespace lms::ui } // namespace lms::ui
#undef LMS_DL_RESOURCE_LOG
+3 -2
View File
@@ -39,6 +39,7 @@
#include "database/objects/MediaLibrary.hpp" #include "database/objects/MediaLibrary.hpp"
#include "database/objects/Medium.hpp" #include "database/objects/Medium.hpp"
#include "database/objects/Release.hpp" #include "database/objects/Release.hpp"
#include "database/objects/ReleaseArtistLink.hpp"
#include "database/objects/Track.hpp" #include "database/objects/Track.hpp"
#include "database/objects/TrackArtistLink.hpp" #include "database/objects/TrackArtistLink.hpp"
#include "database/objects/TrackEmbeddedImage.hpp" #include "database/objects/TrackEmbeddedImage.hpp"
@@ -118,8 +119,8 @@ namespace lms
if (mediaLibrary) if (mediaLibrary)
track.modify()->setMediaLibrary(mediaLibrary); track.modify()->setMediaLibrary(mediaLibrary);
TrackArtistLink::create(context.session, track, artist, TrackArtistLinkType::Artist); context.session.create<TrackArtistLink>(track, artist, TrackArtistLinkType::Artist);
TrackArtistLink::create(context.session, track, artist, TrackArtistLinkType::ReleaseArtist); context.session.create<ReleaseArtistLink>(release, artist, false);
if (!trackEmbeddedImages.empty()) if (!trackEmbeddedImages.empty())
context.session.create<TrackEmbeddedImageLink>(track, *core::random::pickRandom(trackEmbeddedImages)); context.session.create<TrackEmbeddedImageLink>(track, *core::random::pickRandom(trackEmbeddedImages));
@@ -113,7 +113,7 @@ namespace lms
}; };
std::cout << "Processing artist '" << artistToString(artistId) << "'" << std::endl; std::cout << "Processing artist '" << artistToString(artistId) << "'" << std::endl;
for (ArtistId similarArtistId : recommendationService.getSimilarArtists(artistId, { TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist }, maxSimilarityCount)) for (ArtistId similarArtistId : recommendationService.getSimilarArtists(artistId, { TrackArtistLinkType::Artist }, maxSimilarityCount))
{ {
std::cout << "\t- Similar artist '" << artistToString(similarArtistId) << "'" << std::endl; std::cout << "\t- Similar artist '" << artistToString(similarArtistId) << "'" << std::endl;
} }