diff --git a/.github/workflows/build-alpine.yml b/.github/workflows/build-alpine.yml
index a6d4874b..2c38d50c 100644
--- a/.github/workflows/build-alpine.yml
+++ b/.github/workflows/build-alpine.yml
@@ -8,10 +8,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check Out Repo
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
- name: Cache Docker layers
- uses: actions/cache@v2
+ uses: actions/cache@v3
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
@@ -20,10 +20,10 @@ jobs:
- name: Set up Docker Buildx
id: buildx
- uses: docker/setup-buildx-action@v1
+ uses: docker/setup-buildx-action@v2
- name: Build (alpine)
- uses: docker/build-push-action@v2
+ uses: docker/build-push-action@v3
with:
context: ./
file: ./Dockerfile-build-alpine
diff --git a/.github/workflows/build-arch.yml b/.github/workflows/build-arch.yml
index ea06617c..d9fee967 100644
--- a/.github/workflows/build-arch.yml
+++ b/.github/workflows/build-arch.yml
@@ -8,10 +8,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check Out Repo
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
- name: Cache Docker layers
- uses: actions/cache@v2
+ uses: actions/cache@v3
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
@@ -20,10 +20,10 @@ jobs:
- name: Set up Docker Buildx
id: buildx
- uses: docker/setup-buildx-action@v1
+ uses: docker/setup-buildx-action@v2
- name: Build
- uses: docker/build-push-action@v2
+ uses: docker/build-push-action@v3
with:
context: ./
file: ./Dockerfile-build-arch
diff --git a/INSTALL.md b/INSTALL.md
index 79022910..0e3d7b83 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -19,7 +19,7 @@ _Docker_ images are available, please see detailed instructions on https://hub.d
_Bookworm_ packages are provided for _amd64_ architectures.
As root, trust the following debian package provider and add it in your list of repositories:
```sh
-wget https://debian.poupon.dev/apt/debian/epoupon.gpg -P /usr/share/keyrings
+wget --backups=1 https://debian.poupon.dev/apt/debian/epoupon.gpg -P /usr/share/keyrings
echo "deb [signed-by=/usr/share/keyrings/epoupon.gpg] https://debian.poupon.dev/apt/debian bookworm main" > /etc/apt/sources.list.d/epoupon.list
```
To install or upgrade _LMS_:
diff --git a/README.md b/README.md
index 11ffd899..e21def02 100644
--- a/README.md
+++ b/README.md
@@ -18,7 +18,7 @@ A [demo instance](http://lms-demo.poupon.dev) is available. Note the administrat
* Synchronizing 'love' feedbacks
* ReplayGain support
* User management, with several [authentication backends](INSTALL.md#authentication-backend)
-* Subsonic API
+* [Subsonic/OpenSubsonic API](SUBSONIC.md) support
## Music discovery
_LMS_ provides several ways to help you find the music you like:
@@ -34,13 +34,6 @@ _LMS_ provides several ways to help you find the music you like:
* Starred _Jazz_ albums
* ...
-## Subsonic API
-The API version implemented is 1.16.0 and has been tested on _Android_ using _Subsonic Player_, _Ultrasonic_, _Symfonium_, and _DSub_.
-Since _LMS_ uses metadata tags to organize music, a compatibility mode is used to browse the collection when using the directory browsing commands.
-The Subsonic API is enabled by default.
-
-__Note__: since _LMS_ may store hashed and salted passwords or may forward authentication requests to external services, it cannot handle the __token authentication__ method. You may need to check your client to make sure to use the __password__ authentication method.
-
## About tags
_LMS_ relies exclusively on tags to organize your music collection.
diff --git a/SUBSONIC.md b/SUBSONIC.md
new file mode 100644
index 00000000..7bb4b93c
--- /dev/null
+++ b/SUBSONIC.md
@@ -0,0 +1,36 @@
+# Subsonic API
+The API version implemented is 1.16.0 and has been tested on _Android_ using _Subsonic Player_, _Ultrasonic_, _Symfonium_, and _DSub_.
+Since _LMS_ uses metadata tags to organize music, a compatibility mode is used to browse the collection when using the directory browsing commands.
+The Subsonic API is enabled by default.
+
+__Note__: since _LMS_ may store hashed and salted passwords or may forward authentication requests to external services, it cannot handle the __token authentication__ method. You may need to check your client to make sure to use the __password__ authentication method.
+
+# OpenSubsonic API
+OpenSubsonic is an initiative to patch and extend the legacy Subsonic API. You'll find more details in the [official documentation](https://opensubsonic.netlify.app/)
+
+## Extra fields
+The following extra fields are implemented:
+* `Album` response:
+ * `musicBrainzId`
+ * `genres`
+ * `artists`
+ * `releaseTypes`
+ * `moods`
+ * `originalReleaseDate`
+ * `isCompilation`
+ * `discTitles`: discs with no subtitle are omitted
+* `Child` response:
+ * `musicBrainzId`: note this is actually the recording MBID when this response refers to a song
+ * `genres`
+ * `artists`
+ * `albumArtists`
+ * `contributors`
+ * `moods`
+ * `replayGain`
+* `Artist` response:
+ * `musicBrainzId`
+ * `sortName`
+ * `roles`
+
+## Supported extensions
+* [Transcode offset](https://opensubsonic.netlify.app/docs/extensions/transcodeoffset/)
diff --git a/approot/search.xml b/approot/search.xml
index fc357d26..4f248d70 100644
--- a/approot/search.xml
+++ b/approot/search.xml
@@ -16,7 +16,14 @@
${releases}
-
${artists}
+
+
+
+ ${link-type class="form-select"}
+
+
+ ${artists}
+
${tracks}
diff --git a/src/libs/metadata/impl/TagLibParser.cpp b/src/libs/metadata/impl/TagLibParser.cpp
index dad243ed..d5fb733b 100644
--- a/src/libs/metadata/impl/TagLibParser.cpp
+++ b/src/libs/metadata/impl/TagLibParser.cpp
@@ -491,7 +491,7 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
//MP4
else if (TagLib::MP4::File* mp4File {dynamic_cast(f.file())})
{
- auto& coverItem {mp4File->tag()->itemListMap()["covr"]};
+ TagLib::MP4::Item coverItem {mp4File->tag()->item("covr")};
TagLib::MP4::CoverArtList coverArtList {coverItem.toCoverArtList()};
if (!coverArtList.isEmpty())
track.hasCover = true;
diff --git a/src/libs/services/database/impl/Cluster.cpp b/src/libs/services/database/impl/Cluster.cpp
index ed01031a..6acc874a 100644
--- a/src/libs/services/database/impl/Cluster.cpp
+++ b/src/libs/services/database/impl/Cluster.cpp
@@ -28,177 +28,183 @@
#include "SqlQuery.hpp"
#include "Utils.hpp"
-namespace Database {
-
-Cluster::Cluster(ObjectPtr type, std::string_view name)
- : _name {std::string {name, 0, _maxNameLength}},
- _clusterType {getDboPtr(type)}
+namespace Database
{
-}
+ namespace
+ {
+ Wt::Dbo::Query createQuery(Session& session, const Cluster::FindParameters& params)
+ {
+ session.checkSharedLocked();
-Cluster::pointer
-Cluster::create(Session& session, ObjectPtr type, std::string_view name)
-{
- return session.getDboSession().add(std::unique_ptr {new Cluster {type, name}});
-}
+ auto query{ session.getDboSession().query("SELECT DISTINCT c.id FROM cluster c") };
-std::size_t
-Cluster::getCount(Session& session)
-{
- session.checkSharedLocked();
+ if (params.track.isValid() || params.release.isValid())
+ {
+ query.join("track_cluster t_c ON t_c.cluster_id = c.id");
+ query.join("track t ON t.id = t_c.track_id");
+ }
- return session.getDboSession().query("SELECT COUNT(*) FROM cluster");
-}
+ if (params.track.isValid())
+ query.where("t.id = ?").bind(params.track);
+ if (params.release.isValid())
+ query.where("t.release_id = ?").bind(params.release);
-RangeResults
-Cluster::find(Session& session, Range range)
-{
- session.checkSharedLocked();
- auto query {session.getDboSession().query("SELECT id FROM cluster")};
+ if (params.clusterType.isValid())
+ query.where("c.cluster_type_id = ?").bind(params.clusterType);
- return Utils::execQuery(query, range);
-}
+ return query;
+ }
+ }
-RangeResults
-Cluster::findOrphans(Session& session, Range range)
-{
- session.checkSharedLocked();
- auto query {session.getDboSession().query("SELECT DISTINCT c.id FROM cluster c WHERE NOT EXISTS(SELECT 1 FROM track_cluster t_c WHERE t_c.cluster_id = c.id)")};
+ Cluster::Cluster(ObjectPtr type, std::string_view name)
+ : _name{ std::string {name, 0, _maxNameLength} },
+ _clusterType{ getDboPtr(type) }
+ {
+ }
- return Utils::execQuery(query, range);
-}
+ Cluster::pointer Cluster::create(Session& session, ObjectPtr type, std::string_view name)
+ {
+ return session.getDboSession().add(std::unique_ptr {new Cluster{ type, name }});
+ }
-Cluster::pointer
-Cluster::find(Session& session, ClusterId id)
-{
- session.checkSharedLocked();
+ std::size_t Cluster::getCount(Session& session)
+ {
+ session.checkSharedLocked();
- return session.getDboSession().find().where("id = ?").bind(id).resultValue();
-}
+ return session.getDboSession().query("SELECT COUNT(*) FROM cluster");
+ }
-void
-Cluster::addTrack(ObjectPtr