Merge branch 'develop' for release v3.41.0
This commit is contained in:
@@ -8,10 +8,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check Out Repo
|
- name: Check Out Repo
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Cache Docker layers
|
- name: Cache Docker layers
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: /tmp/.buildx-cache
|
path: /tmp/.buildx-cache
|
||||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||||
@@ -20,10 +20,10 @@ jobs:
|
|||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
- name: Build (alpine)
|
- name: Build (alpine)
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
context: ./
|
context: ./
|
||||||
file: ./Dockerfile-build-alpine
|
file: ./Dockerfile-build-alpine
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check Out Repo
|
- name: Check Out Repo
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Cache Docker layers
|
- name: Cache Docker layers
|
||||||
uses: actions/cache@v2
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: /tmp/.buildx-cache
|
path: /tmp/.buildx-cache
|
||||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||||
@@ -20,10 +20,10 @@ jobs:
|
|||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
id: buildx
|
id: buildx
|
||||||
uses: docker/setup-buildx-action@v1
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v3
|
||||||
with:
|
with:
|
||||||
context: ./
|
context: ./
|
||||||
file: ./Dockerfile-build-arch
|
file: ./Dockerfile-build-arch
|
||||||
|
|||||||
+1
-1
@@ -19,7 +19,7 @@ _Docker_ images are available, please see detailed instructions on https://hub.d
|
|||||||
_Bookworm_ packages are provided for _amd64_ architectures.
|
_Bookworm_ packages are provided for _amd64_ architectures.
|
||||||
As root, trust the following debian package provider and add it in your list of repositories:
|
As root, trust the following debian package provider and add it in your list of repositories:
|
||||||
```sh
|
```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
|
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_:
|
To install or upgrade _LMS_:
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ A [demo instance](http://lms-demo.poupon.dev) is available. Note the administrat
|
|||||||
* Synchronizing 'love' feedbacks
|
* Synchronizing 'love' feedbacks
|
||||||
* ReplayGain support
|
* ReplayGain support
|
||||||
* User management, with several [authentication backends](INSTALL.md#authentication-backend)
|
* User management, with several [authentication backends](INSTALL.md#authentication-backend)
|
||||||
* Subsonic API
|
* [Subsonic/OpenSubsonic API](SUBSONIC.md) support
|
||||||
|
|
||||||
## Music discovery
|
## Music discovery
|
||||||
_LMS_ provides several ways to help you find the music you like:
|
_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
|
* 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
|
## About tags
|
||||||
_LMS_ relies exclusively on tags to organize your music collection.
|
_LMS_ relies exclusively on tags to organize your music collection.
|
||||||
|
|
||||||
|
|||||||
+36
@@ -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/)
|
||||||
+8
-1
@@ -16,7 +16,14 @@
|
|||||||
</ul>
|
</ul>
|
||||||
<div class="tab-content" id="myTabContent">
|
<div class="tab-content" id="myTabContent">
|
||||||
<div class="tab-pane show active" id="releases" role="tabpanel" aria-labelledby="releases-tab">${releases}</div>
|
<div class="tab-pane show active" id="releases" role="tabpanel" aria-labelledby="releases-tab">${releases}</div>
|
||||||
<div class="tab-pane" id="artists" role="tabpanel" aria-labelledby="artists-tab">${artists}</div>
|
<div class="tab-pane" id="artists" role="tabpanel" aria-labelledby="artists-tab">
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-lg-3">
|
||||||
|
${link-type class="form-select"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${artists}
|
||||||
|
</div>
|
||||||
<div class="tab-pane" id="tracks" role="tabpanel" aria-labelledby="tracks-tab">${tracks}</div>
|
<div class="tab-pane" id="tracks" role="tabpanel" aria-labelledby="tracks-tab">${tracks}</div>
|
||||||
</div>
|
</div>
|
||||||
</message>
|
</message>
|
||||||
|
|||||||
@@ -491,7 +491,7 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
|
|||||||
//MP4
|
//MP4
|
||||||
else if (TagLib::MP4::File* mp4File {dynamic_cast<TagLib::MP4::File*>(f.file())})
|
else if (TagLib::MP4::File* mp4File {dynamic_cast<TagLib::MP4::File*>(f.file())})
|
||||||
{
|
{
|
||||||
auto& coverItem {mp4File->tag()->itemListMap()["covr"]};
|
TagLib::MP4::Item coverItem {mp4File->tag()->item("covr")};
|
||||||
TagLib::MP4::CoverArtList coverArtList {coverItem.toCoverArtList()};
|
TagLib::MP4::CoverArtList coverArtList {coverItem.toCoverArtList()};
|
||||||
if (!coverArtList.isEmpty())
|
if (!coverArtList.isEmpty())
|
||||||
track.hasCover = true;
|
track.hasCover = true;
|
||||||
|
|||||||
@@ -28,177 +28,183 @@
|
|||||||
#include "SqlQuery.hpp"
|
#include "SqlQuery.hpp"
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
|
|
||||||
namespace Database {
|
namespace Database
|
||||||
|
|
||||||
Cluster::Cluster(ObjectPtr<ClusterType> type, std::string_view name)
|
|
||||||
: _name {std::string {name, 0, _maxNameLength}},
|
|
||||||
_clusterType {getDboPtr(type)}
|
|
||||||
{
|
{
|
||||||
}
|
namespace
|
||||||
|
{
|
||||||
|
Wt::Dbo::Query<ClusterId> createQuery(Session& session, const Cluster::FindParameters& params)
|
||||||
|
{
|
||||||
|
session.checkSharedLocked();
|
||||||
|
|
||||||
Cluster::pointer
|
auto query{ session.getDboSession().query<ClusterId>("SELECT DISTINCT c.id FROM cluster c") };
|
||||||
Cluster::create(Session& session, ObjectPtr<ClusterType> type, std::string_view name)
|
|
||||||
{
|
|
||||||
return session.getDboSession().add(std::unique_ptr<Cluster> {new Cluster {type, name}});
|
|
||||||
}
|
|
||||||
|
|
||||||
std::size_t
|
if (params.track.isValid() || params.release.isValid())
|
||||||
Cluster::getCount(Session& session)
|
{
|
||||||
{
|
query.join("track_cluster t_c ON t_c.cluster_id = c.id");
|
||||||
session.checkSharedLocked();
|
query.join("track t ON t.id = t_c.track_id");
|
||||||
|
}
|
||||||
|
|
||||||
return session.getDboSession().query<int>("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<ClusterId>
|
if (params.clusterType.isValid())
|
||||||
Cluster::find(Session& session, Range range)
|
query.where("c.cluster_type_id = ?").bind(params.clusterType);
|
||||||
{
|
|
||||||
session.checkSharedLocked();
|
|
||||||
auto query {session.getDboSession().query<ClusterId>("SELECT id FROM cluster")};
|
|
||||||
|
|
||||||
return Utils::execQuery(query, range);
|
return query;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
RangeResults<ClusterId>
|
Cluster::Cluster(ObjectPtr<ClusterType> type, std::string_view name)
|
||||||
Cluster::findOrphans(Session& session, Range range)
|
: _name{ std::string {name, 0, _maxNameLength} },
|
||||||
{
|
_clusterType{ getDboPtr(type) }
|
||||||
session.checkSharedLocked();
|
{
|
||||||
auto query {session.getDboSession().query<ClusterId>("SELECT DISTINCT c.id FROM cluster c WHERE NOT EXISTS(SELECT 1 FROM track_cluster t_c WHERE t_c.cluster_id = c.id)")};
|
}
|
||||||
|
|
||||||
return Utils::execQuery(query, range);
|
Cluster::pointer Cluster::create(Session& session, ObjectPtr<ClusterType> type, std::string_view name)
|
||||||
}
|
{
|
||||||
|
return session.getDboSession().add(std::unique_ptr<Cluster> {new Cluster{ type, name }});
|
||||||
|
}
|
||||||
|
|
||||||
Cluster::pointer
|
std::size_t Cluster::getCount(Session& session)
|
||||||
Cluster::find(Session& session, ClusterId id)
|
{
|
||||||
{
|
session.checkSharedLocked();
|
||||||
session.checkSharedLocked();
|
|
||||||
|
|
||||||
return session.getDboSession().find<Cluster>().where("id = ?").bind(id).resultValue();
|
return session.getDboSession().query<int>("SELECT COUNT(*) FROM cluster");
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
RangeResults<ClusterId> Cluster::find(Session& session, const FindParameters& params)
|
||||||
Cluster::addTrack(ObjectPtr<Track> track)
|
{
|
||||||
{
|
session.checkSharedLocked();
|
||||||
_tracks.insert(getDboPtr(track));
|
auto query{ createQuery(session, params) };
|
||||||
}
|
|
||||||
|
|
||||||
RangeResults<TrackId>
|
return Utils::execQuery(query, params.range);
|
||||||
Cluster::getTracks(Range range) const
|
}
|
||||||
{
|
|
||||||
assert(session());
|
|
||||||
|
|
||||||
auto query {session()->query<TrackId>("SELECT t.id FROM track t INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id")
|
RangeResults<ClusterId> Cluster::findOrphans(Session& session, Range range)
|
||||||
.where("c.id = ?").bind(getId())};
|
{
|
||||||
|
session.checkSharedLocked();
|
||||||
|
auto query{ session.getDboSession().query<ClusterId>("SELECT DISTINCT c.id FROM cluster c WHERE NOT EXISTS(SELECT 1 FROM track_cluster t_c WHERE t_c.cluster_id = c.id)") };
|
||||||
|
|
||||||
return Utils::execQuery(query, range);
|
return Utils::execQuery(query, range);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::size_t
|
Cluster::pointer Cluster::find(Session& session, ClusterId id)
|
||||||
Cluster::getReleasesCount() const
|
{
|
||||||
{
|
session.checkSharedLocked();
|
||||||
assert(session());
|
|
||||||
|
|
||||||
return session()->query<int>("SELECT COUNT(DISTINCT r.id) FROM release r INNER JOIN track t on t.release_id = r.id INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id")
|
return session.getDboSession().find<Cluster>().where("id = ?").bind(id).resultValue();
|
||||||
.where("c.id = ?").bind(getId());
|
}
|
||||||
}
|
|
||||||
|
void Cluster::addTrack(ObjectPtr<Track> track)
|
||||||
|
{
|
||||||
|
_tracks.insert(getDboPtr(track));
|
||||||
|
}
|
||||||
|
|
||||||
|
RangeResults<TrackId> Cluster::getTracks(Range range) const
|
||||||
|
{
|
||||||
|
assert(session());
|
||||||
|
|
||||||
|
auto query{ session()->query<TrackId>("SELECT t.id FROM track t INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id")
|
||||||
|
.where("c.id = ?").bind(getId()) };
|
||||||
|
|
||||||
|
return Utils::execQuery(query, range);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t Cluster::getReleasesCount() const
|
||||||
|
{
|
||||||
|
assert(session());
|
||||||
|
|
||||||
|
return session()->query<int>("SELECT COUNT(DISTINCT r.id) FROM release r INNER JOIN track t on t.release_id = r.id INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id")
|
||||||
|
.where("c.id = ?").bind(getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
ClusterType::ClusterType(std::string_view name)
|
ClusterType::ClusterType(std::string_view name)
|
||||||
: _name {name}
|
: _name{ name }
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
ClusterType::pointer
|
ClusterType::pointer ClusterType::create(Session& session, const std::string& name)
|
||||||
ClusterType::create(Session& session, const std::string& name)
|
{
|
||||||
{
|
return session.getDboSession().add(std::unique_ptr<ClusterType> {new ClusterType{ name }});
|
||||||
return session.getDboSession().add(std::unique_ptr<ClusterType> {new ClusterType {name}});
|
}
|
||||||
}
|
|
||||||
|
|
||||||
std::size_t
|
std::size_t ClusterType::getCount(Session& session)
|
||||||
ClusterType::getCount(Session& session)
|
{
|
||||||
{
|
session.checkSharedLocked();
|
||||||
session.checkSharedLocked();
|
|
||||||
|
|
||||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM cluster_type");
|
return session.getDboSession().query<int>("SELECT COUNT(*) FROM cluster_type");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
RangeResults<ClusterTypeId>
|
RangeResults<ClusterTypeId> ClusterType::findOrphans(Session& session, Range range)
|
||||||
ClusterType::findOrphans(Session& session, Range range)
|
{
|
||||||
{
|
session.checkSharedLocked();
|
||||||
session.checkSharedLocked();
|
|
||||||
|
|
||||||
auto query {session.getDboSession().query<ClusterTypeId>(
|
auto query{ session.getDboSession().query<ClusterTypeId>(
|
||||||
"SELECT c_t.id from cluster_type c_t"
|
"SELECT c_t.id from cluster_type c_t"
|
||||||
" LEFT OUTER JOIN cluster c ON c_t.id = c.cluster_type_id")
|
" LEFT OUTER JOIN cluster c ON c_t.id = c.cluster_type_id")
|
||||||
.where("c.id IS NULL")};
|
.where("c.id IS NULL") };
|
||||||
|
|
||||||
return Utils::execQuery(query, range);
|
return Utils::execQuery(query, range);
|
||||||
}
|
}
|
||||||
|
|
||||||
RangeResults<ClusterTypeId>
|
RangeResults<ClusterTypeId> ClusterType::findUsed(Session& session, Range range)
|
||||||
ClusterType::findUsed(Session& session, Range range)
|
{
|
||||||
{
|
session.checkSharedLocked();
|
||||||
session.checkSharedLocked();
|
|
||||||
|
|
||||||
auto query {session.getDboSession().query<ClusterTypeId>(
|
auto query{ session.getDboSession().query<ClusterTypeId>(
|
||||||
"SELECT DISTINCT c_t.id from cluster_type c_t")
|
"SELECT DISTINCT c_t.id from cluster_type c_t")
|
||||||
.join("cluster c ON c_t.id = c.cluster_type_id")};
|
.join("cluster c ON c_t.id = c.cluster_type_id") };
|
||||||
|
|
||||||
return Utils::execQuery(query, range);
|
return Utils::execQuery(query, range);
|
||||||
}
|
}
|
||||||
|
|
||||||
ClusterType::pointer
|
ClusterType::pointer ClusterType::find(Session& session, std::string_view name)
|
||||||
ClusterType::find(Session& session, const std::string& name)
|
{
|
||||||
{
|
session.checkSharedLocked();
|
||||||
session.checkSharedLocked();
|
|
||||||
|
|
||||||
return session.getDboSession().find<ClusterType>().where("name = ?").bind(name).resultValue();
|
return session.getDboSession().find<ClusterType>().where("name = ?").bind(std::string{ name }).resultValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
ClusterType::pointer
|
ClusterType::pointer ClusterType::find(Session& session, ClusterTypeId id)
|
||||||
ClusterType::find(Session& session, ClusterTypeId id)
|
{
|
||||||
{
|
session.checkSharedLocked();
|
||||||
session.checkSharedLocked();
|
|
||||||
|
|
||||||
return session.getDboSession().find<ClusterType>().where("id = ?").bind(id).resultValue();
|
return session.getDboSession().find<ClusterType>().where("id = ?").bind(id).resultValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
RangeResults<ClusterTypeId>
|
RangeResults<ClusterTypeId> ClusterType::find(Session& session, Range range)
|
||||||
ClusterType::find(Session& session, Range range)
|
{
|
||||||
{
|
session.checkSharedLocked();
|
||||||
session.checkSharedLocked();
|
|
||||||
|
|
||||||
auto query {session.getDboSession().query<ClusterTypeId>("SELECT id from cluster_type")};
|
auto query{ session.getDboSession().query<ClusterTypeId>("SELECT id from cluster_type") };
|
||||||
|
|
||||||
return Utils::execQuery(query, range);
|
return Utils::execQuery(query, range);
|
||||||
}
|
}
|
||||||
|
|
||||||
Cluster::pointer
|
Cluster::pointer ClusterType::getCluster(const std::string& name) const
|
||||||
ClusterType::getCluster(const std::string& name) const
|
{
|
||||||
{
|
assert(self());
|
||||||
assert(self());
|
assert(session());
|
||||||
assert(session());
|
|
||||||
|
|
||||||
return session()->find<Cluster>()
|
return session()->find<Cluster>()
|
||||||
.where("name = ?").bind(name)
|
.where("name = ?").bind(name)
|
||||||
.where("cluster_type_id = ?").bind(getId()).resultValue();
|
.where("cluster_type_id = ?").bind(getId()).resultValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<Cluster::pointer>
|
std::vector<Cluster::pointer> ClusterType::getClusters() const
|
||||||
ClusterType::getClusters() const
|
{
|
||||||
{
|
assert(self());
|
||||||
assert(self());
|
assert(session());
|
||||||
assert(session());
|
|
||||||
|
|
||||||
auto res = session()->find<Cluster>()
|
|
||||||
.where("cluster_type_id = ?").bind(getId())
|
|
||||||
.orderBy("name")
|
|
||||||
.resultList();
|
|
||||||
|
|
||||||
return std::vector<Cluster::pointer>(res.begin(), res.end());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
auto res = session()->find<Cluster>()
|
||||||
|
.where("cluster_type_id = ?").bind(getId())
|
||||||
|
.orderBy("name")
|
||||||
|
.resultList();
|
||||||
|
|
||||||
|
return std::vector<Cluster::pointer>(res.begin(), res.end());
|
||||||
|
}
|
||||||
} // namespace Database
|
} // namespace Database
|
||||||
|
|
||||||
|
|||||||
@@ -35,441 +35,443 @@
|
|||||||
namespace Database
|
namespace Database
|
||||||
{
|
{
|
||||||
|
|
||||||
Wt::Dbo::Query<ReleaseId>
|
Wt::Dbo::Query<ReleaseId> createQuery(Session& session, const Release::FindParameters& params)
|
||||||
createQuery(Session& session, const Release::FindParameters& params)
|
{
|
||||||
{
|
auto query{ session.getDboSession().query<ReleaseId>("SELECT DISTINCT r.id from release r") };
|
||||||
auto query {session.getDboSession().query<ReleaseId>("SELECT DISTINCT r.id from release r")};
|
|
||||||
|
if (params.sortMethod == ReleaseSortMethod::LastWritten
|
||||||
if (params.sortMethod == ReleaseSortMethod::LastWritten
|
|| params.sortMethod == ReleaseSortMethod::Date
|
||||||
|| params.sortMethod == ReleaseSortMethod::Date
|
|| params.sortMethod == ReleaseSortMethod::OriginalDate
|
||||||
|| params.sortMethod == ReleaseSortMethod::OriginalDate
|
|| params.sortMethod == ReleaseSortMethod::OriginalDateDesc
|
||||||
|| params.sortMethod == ReleaseSortMethod::OriginalDateDesc
|
|| params.writtenAfter.isValid()
|
||||||
|| params.writtenAfter.isValid()
|
|| params.dateRange
|
||||||
|| params.dateRange
|
|| params.artist.isValid())
|
||||||
|| params.artist.isValid())
|
{
|
||||||
{
|
query.join("track t ON t.release_id = r.id");
|
||||||
query.join("track t ON t.release_id = r.id");
|
}
|
||||||
}
|
|
||||||
|
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.dateRange)
|
||||||
if (params.dateRange)
|
{
|
||||||
{
|
query.where("t.date >= ?").bind(params.dateRange->begin);
|
||||||
query.where("t.date >= ?").bind(params.dateRange->begin);
|
query.where("t.date <= ?").bind(params.dateRange->end);
|
||||||
query.where("t.date <= ?").bind(params.dateRange->end);
|
}
|
||||||
}
|
|
||||||
|
for (std::string_view keyword : params.keywords)
|
||||||
for (std::string_view keyword : params.keywords)
|
query.where("r.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + Utils::escapeLikeKeyword(keyword) + "%");
|
||||||
query.where("r.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + Utils::escapeLikeKeyword(keyword) + "%");
|
|
||||||
|
if (params.starringUser.isValid())
|
||||||
if (params.starringUser.isValid())
|
{
|
||||||
{
|
assert(params.scrobbler);
|
||||||
assert(params.scrobbler);
|
query.join("starred_release s_r ON s_r.release_id = r.id")
|
||||||
query.join("starred_release s_r ON s_r.release_id = r.id")
|
.where("s_r.user_id = ?").bind(params.starringUser)
|
||||||
.where("s_r.user_id = ?").bind(params.starringUser)
|
.where("s_r.scrobbler = ?").bind(*params.scrobbler)
|
||||||
.where("s_r.scrobbler = ?").bind(*params.scrobbler)
|
.where("s_r.scrobbling_state <> ?").bind(ScrobblingState::PendingRemove);
|
||||||
.where("s_r.scrobbling_state <> ?").bind(ScrobblingState::PendingRemove);
|
}
|
||||||
}
|
|
||||||
|
if (params.artist.isValid())
|
||||||
if (params.artist.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 = ?").bind(params.artist);
|
||||||
.where("a.id = ?").bind(params.artist);
|
|
||||||
|
if (!params.trackArtistLinkTypes.empty())
|
||||||
if (!params.trackArtistLinkTypes.empty())
|
{
|
||||||
{
|
std::ostringstream oss;
|
||||||
std::ostringstream oss;
|
|
||||||
|
bool first{ true };
|
||||||
bool first {true};
|
for (TrackArtistLinkType linkType : params.trackArtistLinkTypes)
|
||||||
for (TrackArtistLinkType linkType : params.trackArtistLinkTypes)
|
{
|
||||||
{
|
if (!first)
|
||||||
if (!first)
|
oss << " OR ";
|
||||||
oss << " OR ";
|
oss << "t_a_l.type = ?";
|
||||||
oss << "t_a_l.type = ?";
|
query.bind(linkType);
|
||||||
query.bind(linkType);
|
|
||||||
|
first = false;
|
||||||
first = false;
|
}
|
||||||
}
|
query.where(oss.str());
|
||||||
query.where(oss.str());
|
}
|
||||||
}
|
|
||||||
|
if (!params.excludedTrackArtistLinkTypes.empty())
|
||||||
if (!params.excludedTrackArtistLinkTypes.empty())
|
{
|
||||||
{
|
std::ostringstream oss;
|
||||||
std::ostringstream oss;
|
oss << "r.id NOT IN (SELECT DISTINCT r.id FROM release r"
|
||||||
oss << "r.id NOT IN (SELECT DISTINCT r.id FROM release r"
|
" INNER JOIN artist a ON a.id = t_a_l.artist_id"
|
||||||
" INNER JOIN artist a ON a.id = t_a_l.artist_id"
|
" INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id"
|
||||||
" 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"
|
||||||
" INNER JOIN track t ON t.release_id = r.id"
|
" WHERE (a.id = ? AND (";
|
||||||
" WHERE (a.id = ? AND (";
|
|
||||||
|
query.bind(params.artist);
|
||||||
query.bind(params.artist);
|
|
||||||
|
bool first{ true };
|
||||||
bool first {true};
|
for (const TrackArtistLinkType linkType : params.excludedTrackArtistLinkTypes)
|
||||||
for (const TrackArtistLinkType linkType : params.excludedTrackArtistLinkTypes)
|
{
|
||||||
{
|
if (!first)
|
||||||
if (!first)
|
oss << " OR ";
|
||||||
oss << " OR ";
|
oss << "t_a_l.type = ?";
|
||||||
oss << "t_a_l.type = ?";
|
query.bind(linkType);
|
||||||
query.bind(linkType);
|
|
||||||
|
first = false;
|
||||||
first = false;
|
}
|
||||||
}
|
oss << ")))";
|
||||||
oss << ")))";
|
query.where(oss.str());
|
||||||
query.where(oss.str());
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
if (!params.clusters.empty())
|
||||||
if (!params.clusters.empty())
|
{
|
||||||
{
|
std::ostringstream oss;
|
||||||
std::ostringstream oss;
|
oss << "r.id IN (SELECT DISTINCT r.id FROM release r"
|
||||||
oss << "r.id IN (SELECT DISTINCT r.id FROM release r"
|
" INNER JOIN track t ON t.release_id = r.id"
|
||||||
" INNER JOIN track t ON t.release_id = r.id"
|
" INNER JOIN cluster c ON c.id = t_c.cluster_id"
|
||||||
" INNER JOIN cluster c ON c.id = t_c.cluster_id"
|
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
|
||||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
|
|
||||||
|
WhereClause clusterClause;
|
||||||
WhereClause clusterClause;
|
for (const ClusterId clusterId : params.clusters)
|
||||||
for (const ClusterId clusterId : params.clusters)
|
{
|
||||||
{
|
clusterClause.Or(WhereClause("c.id = ?"));
|
||||||
clusterClause.Or(WhereClause("c.id = ?"));
|
query.bind(clusterId);
|
||||||
query.bind(clusterId);
|
}
|
||||||
}
|
|
||||||
|
oss << " " << clusterClause.get();
|
||||||
oss << " " << clusterClause.get();
|
oss << " GROUP BY t.id HAVING COUNT(*) = " << params.clusters.size() << ")";
|
||||||
oss << " GROUP BY t.id HAVING COUNT(*) = " << params.clusters.size() << ")";
|
|
||||||
|
query.where(oss.str());
|
||||||
query.where(oss.str());
|
}
|
||||||
}
|
|
||||||
|
if (params.primaryType)
|
||||||
if (params.primaryType)
|
query.where("primary_type = ?").bind(*params.primaryType);
|
||||||
query.where("primary_type = ?").bind(*params.primaryType);
|
if (!params.secondaryTypes.empty())
|
||||||
if (!params.secondaryTypes.empty())
|
query.where("secondary_type = ?").bind(params.secondaryTypes);
|
||||||
query.where("secondary_type = ?").bind(params.secondaryTypes);
|
|
||||||
|
switch (params.sortMethod)
|
||||||
switch (params.sortMethod)
|
{
|
||||||
{
|
case ReleaseSortMethod::None:
|
||||||
case ReleaseSortMethod::None:
|
break;
|
||||||
break;
|
case ReleaseSortMethod::Name:
|
||||||
case ReleaseSortMethod::Name:
|
query.orderBy("r.name COLLATE NOCASE");
|
||||||
query.orderBy("r.name COLLATE NOCASE");
|
break;
|
||||||
break;
|
case ReleaseSortMethod::Random:
|
||||||
case ReleaseSortMethod::Random:
|
query.orderBy("RANDOM()");
|
||||||
query.orderBy("RANDOM()");
|
break;
|
||||||
break;
|
case ReleaseSortMethod::LastWritten:
|
||||||
case ReleaseSortMethod::LastWritten:
|
query.orderBy("t.file_last_write DESC");
|
||||||
query.orderBy("t.file_last_write DESC");
|
break;
|
||||||
break;
|
case ReleaseSortMethod::Date:
|
||||||
case ReleaseSortMethod::Date:
|
query.orderBy("t.date, r.name COLLATE NOCASE");
|
||||||
query.orderBy("t.date, r.name COLLATE NOCASE");
|
break;
|
||||||
break;
|
case ReleaseSortMethod::OriginalDate:
|
||||||
case ReleaseSortMethod::OriginalDate:
|
query.orderBy("CASE WHEN t.original_date IS NULL THEN t.date ELSE t.original_date END, t.date, r.name COLLATE NOCASE");
|
||||||
query.orderBy("CASE WHEN t.original_date IS NULL THEN t.date ELSE t.original_date END, t.date, r.name COLLATE NOCASE");
|
break;
|
||||||
break;
|
case ReleaseSortMethod::OriginalDateDesc:
|
||||||
case ReleaseSortMethod::OriginalDateDesc:
|
query.orderBy("CASE WHEN t.original_date IS NULL THEN t.date ELSE t.original_date END DESC, t.date, r.name COLLATE NOCASE");
|
||||||
query.orderBy("CASE WHEN t.original_date IS NULL THEN t.date ELSE t.original_date END DESC, t.date, r.name COLLATE NOCASE");
|
break;
|
||||||
break;
|
case ReleaseSortMethod::StarredDateDesc:
|
||||||
case ReleaseSortMethod::StarredDateDesc:
|
assert(params.starringUser.isValid());
|
||||||
assert(params.starringUser.isValid());
|
query.orderBy("s_r.date_time DESC");
|
||||||
query.orderBy("s_r.date_time DESC");
|
break;
|
||||||
break;
|
}
|
||||||
}
|
|
||||||
|
return query;
|
||||||
return query;
|
}
|
||||||
}
|
|
||||||
|
Release::Release(const std::string& name, const std::optional<UUID>& MBID)
|
||||||
Release::Release(const std::string& name, const std::optional<UUID>& MBID)
|
: _name{ std::string(name, 0 , _maxNameLength) },
|
||||||
: _name {std::string(name, 0 , _maxNameLength)},
|
_MBID{ MBID ? MBID->getAsString() : "" }
|
||||||
_MBID {MBID ? MBID->getAsString() : ""}
|
{
|
||||||
{
|
}
|
||||||
}
|
|
||||||
|
Release::pointer Release::create(Session& session, const std::string& name, const std::optional<UUID>& MBID)
|
||||||
Release::pointer
|
{
|
||||||
Release::create(Session& session, const std::string& name, const std::optional<UUID>& MBID)
|
return session.getDboSession().add(std::unique_ptr<Release> {new Release{ name, MBID }});
|
||||||
{
|
}
|
||||||
return session.getDboSession().add(std::unique_ptr<Release> {new Release {name, MBID}});
|
|
||||||
}
|
std::vector<Release::pointer> Release::find(Session& session, const std::string& name)
|
||||||
|
{
|
||||||
std::vector<Release::pointer>
|
session.checkUniqueLocked();
|
||||||
Release::find(Session& session, const std::string& name)
|
|
||||||
{
|
auto res{ session.getDboSession()
|
||||||
session.checkUniqueLocked();
|
.find<Release>()
|
||||||
|
.where("name = ?").bind(std::string(name, 0, _maxNameLength))
|
||||||
auto res {session.getDboSession()
|
.resultList() };
|
||||||
.find<Release>()
|
|
||||||
.where("name = ?").bind( std::string(name, 0, _maxNameLength) )
|
return std::vector<Release::pointer>(res.begin(), res.end());
|
||||||
.resultList()};
|
}
|
||||||
|
|
||||||
return std::vector<Release::pointer>(res.begin(), res.end());
|
Release::pointer Release::find(Session& session, const UUID& mbid)
|
||||||
}
|
{
|
||||||
|
session.checkSharedLocked();
|
||||||
Release::pointer
|
|
||||||
Release::find(Session& session, const UUID& mbid)
|
return session.getDboSession()
|
||||||
{
|
.find<Release>()
|
||||||
session.checkSharedLocked();
|
.where("mbid = ?").bind(std::string{ mbid.getAsString() })
|
||||||
|
.resultValue();;
|
||||||
return session.getDboSession()
|
}
|
||||||
.find<Release>()
|
|
||||||
.where("mbid = ?").bind(std::string {mbid.getAsString()})
|
Release::pointer Release::find(Session& session, ReleaseId id)
|
||||||
.resultValue();;
|
{
|
||||||
}
|
session.checkSharedLocked();
|
||||||
|
|
||||||
Release::pointer
|
return session.getDboSession()
|
||||||
Release::find(Session& session, ReleaseId id)
|
.find<Release>()
|
||||||
{
|
.where("id = ?").bind(id)
|
||||||
session.checkSharedLocked();
|
.resultValue();
|
||||||
|
}
|
||||||
return session.getDboSession()
|
|
||||||
.find<Release>()
|
bool Release::exists(Session& session, ReleaseId id)
|
||||||
.where("id = ?").bind(id)
|
{
|
||||||
.resultValue();
|
session.checkSharedLocked();
|
||||||
}
|
return session.getDboSession().query<int>("SELECT 1 FROM release").where("id = ?").bind(id).resultValue() == 1;
|
||||||
|
}
|
||||||
bool
|
|
||||||
Release::exists(Session& session, ReleaseId id)
|
std::size_t Release::getCount(Session& session)
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
return session.getDboSession().query<int>("SELECT 1 FROM release").where("id = ?").bind(id).resultValue() == 1;
|
|
||||||
}
|
return session.getDboSession().query<int>("SELECT COUNT(*) FROM release");
|
||||||
|
}
|
||||||
std::size_t
|
|
||||||
Release::getCount(Session& session)
|
RangeResults<ReleaseId> Release::findOrderedByArtist(Session& session, Range range)
|
||||||
{
|
{
|
||||||
session.checkSharedLocked();
|
session.checkSharedLocked();
|
||||||
|
|
||||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM release");
|
// TODO merge with find
|
||||||
}
|
auto query{ session.getDboSession().query<ReleaseId>(
|
||||||
|
"SELECT DISTINCT r.id FROM release r"
|
||||||
RangeResults<ReleaseId>
|
" INNER JOIN track t ON r.id = t.release_id"
|
||||||
Release::findOrderedByArtist(Session& session, Range range)
|
" INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id"
|
||||||
{
|
" INNER JOIN artist a ON t_a_l.artist_id = a.id")
|
||||||
session.checkSharedLocked();
|
.orderBy("a.name COLLATE NOCASE, r.name COLLATE NOCASE") };
|
||||||
|
|
||||||
// TODO merge with find
|
return Utils::execQuery(query, range);
|
||||||
auto query {session.getDboSession().query<ReleaseId>(
|
}
|
||||||
"SELECT DISTINCT r.id FROM release r"
|
|
||||||
" INNER JOIN track t ON r.id = t.release_id"
|
RangeResults<ReleaseId> Release::findOrphans(Session& session, Range range)
|
||||||
" INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id"
|
{
|
||||||
" INNER JOIN artist a ON t_a_l.artist_id = a.id")
|
session.checkSharedLocked();
|
||||||
.orderBy("a.name COLLATE NOCASE, r.name COLLATE NOCASE")};
|
|
||||||
|
auto query{ session.getDboSession().query<ReleaseId>("select r.id from release r LEFT OUTER JOIN Track t ON r.id = t.release_id WHERE t.id IS NULL") };
|
||||||
return Utils::execQuery(query, range);
|
return Utils::execQuery(query, range);
|
||||||
}
|
}
|
||||||
|
|
||||||
RangeResults<ReleaseId>
|
RangeResults<ReleaseId> Release::find(Session& session, const FindParameters& params)
|
||||||
Release::findOrphans(Session& session, Range range)
|
{
|
||||||
{
|
session.checkSharedLocked();
|
||||||
session.checkSharedLocked();
|
|
||||||
|
auto query{ createQuery(session, params) };
|
||||||
auto query {session.getDboSession().query<ReleaseId>("select r.id from release r LEFT OUTER JOIN Track t ON r.id = t.release_id WHERE t.id IS NULL")};
|
|
||||||
return Utils::execQuery(query, range);
|
return Utils::execQuery(query, params.range);
|
||||||
}
|
}
|
||||||
|
|
||||||
RangeResults<ReleaseId>
|
std::size_t Release::getDiscCount() const
|
||||||
Release::find(Session& session, const FindParameters& params)
|
{
|
||||||
{
|
assert(session());
|
||||||
session.checkSharedLocked();
|
int res{ session()->query<int>("SELECT COUNT(DISTINCT disc_number) FROM track t")
|
||||||
|
.join("release r ON r.id = t.release_id")
|
||||||
auto query {createQuery(session, params)};
|
.where("r.id = ?")
|
||||||
|
.bind(getId()) };
|
||||||
return Utils::execQuery(query, params.range);
|
|
||||||
}
|
return res;
|
||||||
|
}
|
||||||
std::size_t
|
|
||||||
Release::getDiscCount() const
|
std::vector<DiscInfo> Release::getDiscs() const
|
||||||
{
|
{
|
||||||
assert(session());
|
assert(session());
|
||||||
int res {session()->query<int>("SELECT COUNT(DISTINCT disc_number) FROM track t")
|
using ResultType = std::tuple<int, std::string>;
|
||||||
.join("release r ON r.id = t.release_id")
|
auto results{ session()->query<ResultType>("SELECT DISTINCT disc_number, disc_subtitle FROM track t")
|
||||||
.where("r.id = ?")
|
.join("release r ON r.id = t.release_id")
|
||||||
.bind(getId())};
|
.where("r.id = ?")
|
||||||
|
.orderBy("disc_number")
|
||||||
return res;
|
.bind(getId())
|
||||||
}
|
.resultList() };
|
||||||
|
|
||||||
std::optional<int>
|
std::vector<DiscInfo> discs;
|
||||||
Release::getReleaseYear(bool original) const
|
for (const auto& res : results)
|
||||||
{
|
discs.emplace_back(DiscInfo{ static_cast<std::size_t>(std::get<int>(res)), std::get<std::string>(res) });
|
||||||
assert(session());
|
|
||||||
|
return discs;
|
||||||
const char* field {original ? "original_date" : "date"};
|
}
|
||||||
|
|
||||||
auto dates {session()->query<Wt::WDate>(
|
Wt::WDate Release::getReleaseDate() const
|
||||||
std::string {"SELECT "} + "t." + field + " FROM track t INNER JOIN release r ON r.id = t.release_id")
|
{
|
||||||
.where("r.id = ?")
|
return getReleaseDate(false);
|
||||||
.groupBy(field)
|
}
|
||||||
.bind(getId())
|
|
||||||
.resultList()};
|
Wt::WDate Release::getOriginalReleaseDate() const
|
||||||
|
{
|
||||||
// various dates => no date
|
return getReleaseDate(true);
|
||||||
if (dates.empty() || dates.size() > 1)
|
}
|
||||||
return std::nullopt;
|
|
||||||
|
Wt::WDate Release::getReleaseDate(bool original) const
|
||||||
auto date {dates.front().year()};
|
{
|
||||||
|
assert(session());
|
||||||
if (date > 0)
|
|
||||||
return date;
|
const char* field{ original ? "original_date" : "date" };
|
||||||
|
|
||||||
return std::nullopt;
|
auto dates{ session()->query<Wt::WDate>(
|
||||||
}
|
std::string {"SELECT "} + "t." + field + " FROM track t INNER JOIN release r ON r.id = t.release_id")
|
||||||
|
.where("r.id = ?")
|
||||||
std::optional<std::string>
|
.groupBy(field)
|
||||||
Release::getCopyright() const
|
.bind(getId())
|
||||||
{
|
.resultList() };
|
||||||
assert(session());
|
|
||||||
|
// various dates => invalid date
|
||||||
Wt::Dbo::collection<std::string> copyrights = session()->query<std::string>
|
if (dates.empty() || dates.size() > 1)
|
||||||
("SELECT copyright FROM track t INNER JOIN release r ON r.id = t.release_id")
|
return {};
|
||||||
.where("r.id = ?")
|
|
||||||
.groupBy("copyright")
|
return dates.front();
|
||||||
.bind(getId());
|
}
|
||||||
|
|
||||||
std::vector<std::string> values(copyrights.begin(), copyrights.end());
|
std::optional<std::string> Release::getCopyright() const
|
||||||
|
{
|
||||||
// various copyrights => no copyright
|
assert(session());
|
||||||
if (values.empty() || values.size() > 1 || values.front().empty())
|
|
||||||
return std::nullopt;
|
Wt::Dbo::collection<std::string> copyrights = session()->query<std::string>
|
||||||
|
("SELECT copyright FROM track t INNER JOIN release r ON r.id = t.release_id")
|
||||||
return values.front();
|
.where("r.id = ?")
|
||||||
}
|
.groupBy("copyright")
|
||||||
|
.bind(getId());
|
||||||
std::optional<std::string>
|
|
||||||
Release::getCopyrightURL() const
|
std::vector<std::string> values(copyrights.begin(), copyrights.end());
|
||||||
{
|
|
||||||
assert(session());
|
// various copyrights => no copyright
|
||||||
|
if (values.empty() || values.size() > 1 || values.front().empty())
|
||||||
Wt::Dbo::collection<std::string> copyrights = session()->query<std::string>
|
return std::nullopt;
|
||||||
("SELECT copyright_url FROM track t INNER JOIN release r ON r.id = t.release_id")
|
|
||||||
.where("r.id = ?")
|
return values.front();
|
||||||
.groupBy("copyright_url")
|
}
|
||||||
.bind(getId());
|
|
||||||
|
std::optional<std::string> Release::getCopyrightURL() const
|
||||||
std::vector<std::string> values(copyrights.begin(), copyrights.end());
|
{
|
||||||
|
assert(session());
|
||||||
// various copyright URLs => no copyright URL
|
|
||||||
if (values.empty() || values.size() > 1 || values.front().empty())
|
Wt::Dbo::collection<std::string> copyrights = session()->query<std::string>
|
||||||
return std::nullopt;
|
("SELECT copyright_url FROM track t INNER JOIN release r ON r.id = t.release_id")
|
||||||
|
.where("r.id = ?")
|
||||||
return values.front();
|
.groupBy("copyright_url")
|
||||||
}
|
.bind(getId());
|
||||||
|
|
||||||
std::vector<Artist::pointer>
|
std::vector<std::string> values(copyrights.begin(), copyrights.end());
|
||||||
Release::getArtists(TrackArtistLinkType linkType) const
|
|
||||||
{
|
// various copyright URLs => no copyright URL
|
||||||
assert(session());
|
if (values.empty() || values.size() > 1 || values.front().empty())
|
||||||
|
return std::nullopt;
|
||||||
auto res {session()->query<Wt::Dbo::ptr<Artist>>(
|
|
||||||
"SELECT DISTINCT a FROM artist a"
|
return values.front();
|
||||||
" INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id"
|
}
|
||||||
" INNER JOIN track t ON t.id = t_a_l.track_id"
|
|
||||||
" INNER JOIN release r ON r.id = t.release_id")
|
std::vector<Artist::pointer> Release::getArtists(TrackArtistLinkType linkType) const
|
||||||
.where("r.id = ?").bind(getId())
|
{
|
||||||
.where("t_a_l.type = ?").bind(linkType)
|
assert(session());
|
||||||
.resultList()};
|
|
||||||
|
auto res{ session()->query<Wt::Dbo::ptr<Artist>>(
|
||||||
return std::vector<Artist::pointer>(res.begin(), res.end());
|
"SELECT DISTINCT a FROM artist a"
|
||||||
}
|
" INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id"
|
||||||
|
" INNER JOIN track t ON t.id = t_a_l.track_id"
|
||||||
std::vector<Release::pointer>
|
" INNER JOIN release r ON r.id = t.release_id")
|
||||||
Release::getSimilarReleases(std::optional<std::size_t> offset, std::optional<std::size_t> count) const
|
.where("r.id = ?").bind(getId())
|
||||||
{
|
.where("t_a_l.type = ?").bind(linkType)
|
||||||
assert(session());
|
.resultList() };
|
||||||
|
|
||||||
auto res {session()->query<Wt::Dbo::ptr<Release>>(
|
return std::vector<Artist::pointer>(res.begin(), res.end());
|
||||||
"SELECT r FROM release r"
|
}
|
||||||
" INNER JOIN track t ON t.release_id = r.id"
|
|
||||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
|
std::vector<Release::pointer> Release::getSimilarReleases(std::optional<std::size_t> offset, std::optional<std::size_t> count) const
|
||||||
" WHERE "
|
{
|
||||||
" t_c.cluster_id IN (SELECT c.id from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN release r ON r.id = t.release_id WHERE r.id = ?)"
|
assert(session());
|
||||||
" AND r.id <> ?"
|
|
||||||
)
|
auto res{ session()->query<Wt::Dbo::ptr<Release>>(
|
||||||
.bind(getId())
|
"SELECT r FROM release r"
|
||||||
.bind(getId())
|
" INNER JOIN track t ON t.release_id = r.id"
|
||||||
.groupBy("r.id")
|
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
|
||||||
.orderBy("COUNT(*) DESC, RANDOM()")
|
" WHERE "
|
||||||
.limit(count ? static_cast<int>(*count) : -1)
|
" t_c.cluster_id IN (SELECT c.id from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN release r ON r.id = t.release_id WHERE r.id = ?)"
|
||||||
.offset(offset ? static_cast<int>(*offset) : -1)
|
" AND r.id <> ?"
|
||||||
.resultList()};
|
)
|
||||||
|
.bind(getId())
|
||||||
return std::vector<pointer>(res.begin(), res.end());
|
.bind(getId())
|
||||||
}
|
.groupBy("r.id")
|
||||||
|
.orderBy("COUNT(*) DESC, RANDOM()")
|
||||||
bool
|
.limit(count ? static_cast<int>(*count) : -1)
|
||||||
Release::hasVariousArtists() const
|
.offset(offset ? static_cast<int>(*offset) : -1)
|
||||||
{
|
.resultList() };
|
||||||
// TODO optimize
|
|
||||||
return getArtists().size() > 1;
|
return std::vector<pointer>(res.begin(), res.end());
|
||||||
}
|
}
|
||||||
|
|
||||||
std::size_t
|
bool Release::hasVariousArtists() const
|
||||||
Release::getTracksCount() const
|
{
|
||||||
{
|
// TODO optimize
|
||||||
return _tracks.size();
|
return getArtists().size() > 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::chrono::milliseconds
|
std::size_t Release::getTracksCount() const
|
||||||
Release::getDuration() const
|
{
|
||||||
{
|
return _tracks.size();
|
||||||
assert(session());
|
}
|
||||||
|
|
||||||
using milli = std::chrono::duration<int, std::milli>;
|
std::chrono::milliseconds Release::getDuration() const
|
||||||
|
{
|
||||||
Wt::Dbo::Query<milli> query {session()->query<milli>("SELECT COALESCE(SUM(duration), 0) FROM track t INNER JOIN release r ON t.release_id = r.id")
|
assert(session());
|
||||||
.where("r.id = ?").bind(getId())};
|
|
||||||
|
using milli = std::chrono::duration<int, std::milli>;
|
||||||
return query.resultValue();
|
|
||||||
}
|
Wt::Dbo::Query<milli> query{ session()->query<milli>("SELECT COALESCE(SUM(duration), 0) FROM track t INNER JOIN release r ON t.release_id = r.id")
|
||||||
|
.where("r.id = ?").bind(getId()) };
|
||||||
Wt::WDateTime
|
|
||||||
Release::getLastWritten() const
|
return query.resultValue();
|
||||||
{
|
}
|
||||||
assert(session());
|
|
||||||
|
Wt::WDateTime Release::getLastWritten() const
|
||||||
Wt::Dbo::Query<Wt::WDateTime> query {session()->query<Wt::WDateTime>("SELECT COALESCE(MAX(file_last_write), '1970-01-01T00:00:00') FROM track t INNER JOIN release r ON t.release_id = r.id")
|
{
|
||||||
.where("r.id = ?").bind(getId())};
|
assert(session());
|
||||||
|
|
||||||
return query.resultValue();
|
Wt::Dbo::Query<Wt::WDateTime> query{ session()->query<Wt::WDateTime>("SELECT COALESCE(MAX(file_last_write), '1970-01-01T00:00:00') FROM track t INNER JOIN release r ON t.release_id = r.id")
|
||||||
}
|
.where("r.id = ?").bind(getId()) };
|
||||||
|
|
||||||
std::vector<std::vector<Cluster::pointer>>
|
return query.resultValue();
|
||||||
Release::getClusterGroups(const std::vector<ClusterType::pointer>& clusterTypes, std::size_t size) const
|
}
|
||||||
{
|
|
||||||
assert(session());
|
std::vector<std::vector<Cluster::pointer>> Release::getClusterGroups(const std::vector<ClusterType::pointer>& clusterTypes, std::size_t size) const
|
||||||
|
{
|
||||||
WhereClause where;
|
assert(session());
|
||||||
|
|
||||||
std::ostringstream oss;
|
WhereClause where;
|
||||||
|
|
||||||
oss << "SELECT c from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN cluster_type c_type ON c.cluster_type_id = c_type.id INNER JOIN release r ON t.release_id = r.id ";
|
std::ostringstream oss;
|
||||||
|
|
||||||
where.And(WhereClause("r.id = ?")).bind(getId().toString());
|
oss << "SELECT c from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN cluster_type c_type ON c.cluster_type_id = c_type.id INNER JOIN release r ON t.release_id = r.id ";
|
||||||
{
|
|
||||||
WhereClause clusterClause;
|
where.And(WhereClause("r.id = ?")).bind(getId().toString());
|
||||||
for (auto clusterType : clusterTypes)
|
{
|
||||||
clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterType->getId().toString());
|
WhereClause clusterClause;
|
||||||
where.And(clusterClause);
|
for (auto clusterType : clusterTypes)
|
||||||
}
|
clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterType->getId().toString());
|
||||||
oss << " " << where.get();
|
where.And(clusterClause);
|
||||||
oss << " GROUP BY c.id ORDER BY COUNT(c.id) DESC";
|
}
|
||||||
|
oss << " " << where.get();
|
||||||
auto query {session()->query<Wt::Dbo::ptr<Cluster>>(oss.str())};
|
oss << " GROUP BY c.id ORDER BY COUNT(c.id) DESC";
|
||||||
for (const std::string& bindArg : where.getBindArgs())
|
|
||||||
query.bind(bindArg);
|
auto query{ session()->query<Wt::Dbo::ptr<Cluster>>(oss.str()) };
|
||||||
|
for (const std::string& bindArg : where.getBindArgs())
|
||||||
auto queryRes {query.resultList()};
|
query.bind(bindArg);
|
||||||
|
|
||||||
std::map<ClusterTypeId, std::vector<Cluster::pointer>> clustersByType;
|
auto queryRes{ query.resultList() };
|
||||||
for (const Wt::Dbo::ptr<Cluster>& cluster : queryRes)
|
|
||||||
{
|
std::map<ClusterTypeId, std::vector<Cluster::pointer>> clustersByType;
|
||||||
if (clustersByType[cluster->getType()->getId()].size() < size)
|
for (const Wt::Dbo::ptr<Cluster>& cluster : queryRes)
|
||||||
clustersByType[cluster->getType()->getId()].push_back(cluster);
|
{
|
||||||
}
|
if (clustersByType[cluster->getType()->getId()].size() < size)
|
||||||
|
clustersByType[cluster->getType()->getId()].push_back(cluster);
|
||||||
std::vector<std::vector<Cluster::pointer>> res;
|
}
|
||||||
for (const auto& [clusterTypeId, clusters] : clustersByType)
|
|
||||||
res.push_back(clusters);
|
std::vector<std::vector<Cluster::pointer>> res;
|
||||||
|
for (const auto& [clusterTypeId, clusters] : clustersByType)
|
||||||
return res;
|
res.push_back(clusters);
|
||||||
}
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace Database
|
} // namespace Database
|
||||||
|
|||||||
@@ -28,74 +28,84 @@
|
|||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
{
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
Wt::Dbo::Query<TrackArtistLinkId> createQuery(Session& session, const TrackArtistLink::FindParameters& params)
|
||||||
|
{
|
||||||
|
session.checkSharedLocked();
|
||||||
|
|
||||||
static
|
auto query{ session.getDboSession().query<TrackArtistLinkId>("SELECT DISTINCT t_a_l.id FROM track_artist_link t_a_l") };
|
||||||
Wt::Dbo::Query<TrackArtistLinkId>
|
|
||||||
createQuery(Session& session, const TrackArtistLink::FindParameters& params)
|
|
||||||
{
|
|
||||||
session.checkSharedLocked();
|
|
||||||
|
|
||||||
auto query {session.getDboSession().query<TrackArtistLinkId>("SELECT DISTINCT t_a_l.id FROM track_artist_link t_a_l")};
|
if (params.linkType)
|
||||||
|
query.where("t_a_l.type = ?").bind(*params.linkType);
|
||||||
|
|
||||||
if (params.linkType)
|
if (params.track.isValid() || params.release.isValid())
|
||||||
query.where("t_a_l.type = ?").bind(*params.linkType);
|
query.join("track t ON t.id = t_a_l.track_id");
|
||||||
|
|
||||||
if (params.track.isValid() || params.release.isValid())
|
if (params.artist.isValid())
|
||||||
query.join("track t ON t.id = t_a_l.track_id");
|
query.join("artist a ON a.id = t_a_l.artist_id");
|
||||||
|
|
||||||
if (params.track.isValid())
|
if (params.release.isValid())
|
||||||
query.where("t.id = ?").bind(params.track);
|
query.where("t.release_id = ?").bind(params.release);
|
||||||
|
|
||||||
if (params.release.isValid())
|
if (params.track.isValid())
|
||||||
query.where("t.release_id = ?").bind(params.release);
|
query.where("t.id = ?").bind(params.track);
|
||||||
|
|
||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TrackArtistLink::TrackArtistLink(ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType)
|
TrackArtistLink::TrackArtistLink(ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType)
|
||||||
: _type {type}
|
: _type{ type }
|
||||||
, _subType {subType}
|
, _subType{ subType }
|
||||||
, _track {getDboPtr(track)}
|
, _track{ getDboPtr(track) }
|
||||||
, _artist {getDboPtr(artist)}
|
, _artist{ getDboPtr(artist) }
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackArtistLink::pointer
|
TrackArtistLink::pointer TrackArtistLink::create(Session& session, ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType)
|
||||||
TrackArtistLink::create(Session& session, ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType)
|
{
|
||||||
{
|
session.checkUniqueLocked();
|
||||||
session.checkUniqueLocked();
|
|
||||||
|
|
||||||
TrackArtistLink::pointer res {session.getDboSession().add(std::make_unique<TrackArtistLink>(track, artist, type, subType))};
|
TrackArtistLink::pointer res{ session.getDboSession().add(std::make_unique<TrackArtistLink>(track, artist, type, subType)) };
|
||||||
session.getDboSession().flush();
|
session.getDboSession().flush();
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackArtistLink::pointer
|
TrackArtistLink::pointer TrackArtistLink::find(Session& session, TrackArtistLinkId id)
|
||||||
TrackArtistLink::find(Session& session, TrackArtistLinkId id)
|
{
|
||||||
{
|
session.checkSharedLocked();
|
||||||
session.checkSharedLocked();
|
return session.getDboSession().find<TrackArtistLink>().where("id = ?").bind(id).resultValue();
|
||||||
return session.getDboSession().find<TrackArtistLink>().where("id = ?").bind(id).resultValue();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
RangeResults<TrackArtistLinkId>
|
RangeResults<TrackArtistLinkId> TrackArtistLink::find(Session& session, const FindParameters& params)
|
||||||
TrackArtistLink::find(Session& session, const FindParameters& params)
|
{
|
||||||
{
|
session.checkSharedLocked();
|
||||||
session.checkSharedLocked();
|
|
||||||
|
|
||||||
auto query {createQuery(session, params)};
|
auto query{ createQuery(session, params) };
|
||||||
return Utils::execQuery(query, params.range);
|
return Utils::execQuery(query, params.range);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EnumSet<TrackArtistLinkType> TrackArtistLink::findUsedTypes(Session& session)
|
||||||
|
{
|
||||||
|
session.checkSharedLocked();
|
||||||
|
|
||||||
EnumSet<TrackArtistLinkType>
|
auto res{ session.getDboSession().query<TrackArtistLinkType>("SELECT DISTINCT type from track_artist_link").resultList() };
|
||||||
TrackArtistLink::findUsedTypes(Session& session)
|
|
||||||
{
|
|
||||||
session.checkSharedLocked();
|
|
||||||
|
|
||||||
auto res {session.getDboSession().query<TrackArtistLinkType>("SELECT DISTINCT type from track_artist_link").resultList()};
|
return EnumSet<TrackArtistLinkType>(std::begin(res), std::end(res));
|
||||||
|
}
|
||||||
|
|
||||||
return EnumSet<TrackArtistLinkType>(std::begin(res), std::end(res));
|
EnumSet<TrackArtistLinkType> TrackArtistLink::findUsedTypes(Session& session, ArtistId artistId)
|
||||||
}
|
{
|
||||||
|
session.checkSharedLocked();
|
||||||
|
|
||||||
|
auto res{ session.getDboSession()
|
||||||
|
.query<TrackArtistLinkType>("SELECT DISTINCT type from track_artist_link")
|
||||||
|
.where("artist_id = ?").bind(artistId)
|
||||||
|
.resultList() };
|
||||||
|
|
||||||
|
return EnumSet<TrackArtistLinkType>(std::begin(res), std::end(res));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,100 +26,114 @@
|
|||||||
#include <Wt/Dbo/Dbo.h>
|
#include <Wt/Dbo/Dbo.h>
|
||||||
#include <Wt/WDateTime.h>
|
#include <Wt/WDateTime.h>
|
||||||
|
|
||||||
#include "services/database/Object.hpp"
|
|
||||||
#include "services/database/ClusterId.hpp"
|
#include "services/database/ClusterId.hpp"
|
||||||
|
#include "services/database/Object.hpp"
|
||||||
|
#include "services/database/Release.hpp"
|
||||||
#include "services/database/TrackId.hpp"
|
#include "services/database/TrackId.hpp"
|
||||||
#include "services/database/Types.hpp"
|
#include "services/database/Types.hpp"
|
||||||
|
|
||||||
namespace Database {
|
namespace Database {
|
||||||
|
|
||||||
class Track;
|
class Track;
|
||||||
class ClusterType;
|
class ClusterType;
|
||||||
class ScanSettings;
|
class ScanSettings;
|
||||||
class Session;
|
class Session;
|
||||||
|
|
||||||
class Cluster final : public Object<Cluster, ClusterId>
|
class Cluster final : public Object<Cluster, ClusterId>
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
Cluster() = default;
|
struct FindParameters
|
||||||
|
{
|
||||||
|
Range range;
|
||||||
|
ClusterTypeId clusterType; // if non empty, clusters that belong to this cluster type
|
||||||
|
TrackId track; // if set, clusters involved in this track
|
||||||
|
ReleaseId release; // if set, clusters involved in this release
|
||||||
|
|
||||||
// Find utility
|
FindParameters& setRange(Range _range) { range = _range; return *this; }
|
||||||
static std::size_t getCount(Session& session);
|
FindParameters& setClusterType(ClusterTypeId _clusterType) { clusterType = _clusterType; return *this; }
|
||||||
static RangeResults<ClusterId> find(Session& session, Range range);
|
FindParameters& setTrack(TrackId _track) { track = _track; return *this; }
|
||||||
static pointer find(Session& session, ClusterId id);
|
FindParameters& setRelease(ReleaseId _release) { release = _release; return *this; }
|
||||||
static RangeResults<ClusterId> findOrphans(Session& session, Range range);
|
};
|
||||||
|
|
||||||
// Accessors
|
Cluster() = default;
|
||||||
const std::string& getName() const { return _name; }
|
|
||||||
ObjectPtr<ClusterType> getType() const { return _clusterType; }
|
|
||||||
std::size_t getTracksCount() const { return _tracks.size(); }
|
|
||||||
RangeResults<TrackId> getTracks(Range range) const;
|
|
||||||
std::size_t getReleasesCount() const;
|
|
||||||
|
|
||||||
void addTrack(ObjectPtr<Track> track);
|
// Find utility
|
||||||
|
static std::size_t getCount(Session& session);
|
||||||
|
static RangeResults<ClusterId> find(Session& session, const FindParameters& range);
|
||||||
|
static pointer find(Session& session, ClusterId id);
|
||||||
|
static RangeResults<ClusterId> findOrphans(Session& session, Range range);
|
||||||
|
|
||||||
template<class Action>
|
// Accessors
|
||||||
void persist(Action& a)
|
const std::string& getName() const { return _name; }
|
||||||
{
|
ObjectPtr<ClusterType> getType() const { return _clusterType; }
|
||||||
Wt::Dbo::field(a, _name, "name");
|
std::size_t getTracksCount() const { return _tracks.size(); }
|
||||||
|
RangeResults<TrackId> getTracks(Range range) const;
|
||||||
|
std::size_t getReleasesCount() const;
|
||||||
|
|
||||||
Wt::Dbo::belongsTo(a, _clusterType, "cluster_type", Wt::Dbo::OnDeleteCascade);
|
void addTrack(ObjectPtr<Track> track);
|
||||||
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade);
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
template<class Action>
|
||||||
friend class Session;
|
void persist(Action& a)
|
||||||
Cluster(ObjectPtr<ClusterType> type, std::string_view name);
|
{
|
||||||
static pointer create(Session& session, ObjectPtr<ClusterType> type, std::string_view name);
|
Wt::Dbo::field(a, _name, "name");
|
||||||
|
|
||||||
static const std::size_t _maxNameLength = 128;
|
Wt::Dbo::belongsTo(a, _clusterType, "cluster_type", Wt::Dbo::OnDeleteCascade);
|
||||||
|
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade);
|
||||||
|
}
|
||||||
|
|
||||||
std::string _name;
|
private:
|
||||||
|
friend class Session;
|
||||||
|
Cluster(ObjectPtr<ClusterType> type, std::string_view name);
|
||||||
|
static pointer create(Session& session, ObjectPtr<ClusterType> type, std::string_view name);
|
||||||
|
|
||||||
Wt::Dbo::ptr<ClusterType> _clusterType;
|
static const std::size_t _maxNameLength = 128;
|
||||||
Wt::Dbo::collection< Wt::Dbo::ptr<Track> > _tracks;
|
|
||||||
};
|
std::string _name;
|
||||||
|
|
||||||
|
Wt::Dbo::ptr<ClusterType> _clusterType;
|
||||||
|
Wt::Dbo::collection< Wt::Dbo::ptr<Track> > _tracks;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
class ClusterType final : public Object<ClusterType, ClusterTypeId>
|
class ClusterType final : public Object<ClusterType, ClusterTypeId>
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
ClusterType() = default;
|
ClusterType() = default;
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
static std::size_t getCount(Session& session);
|
static std::size_t getCount(Session& session);
|
||||||
static RangeResults<ClusterTypeId> find(Session& session, Range range);
|
static RangeResults<ClusterTypeId> find(Session& session, Range range);
|
||||||
static pointer find(Session& session, const std::string& name);
|
static pointer find(Session& session, std::string_view name);
|
||||||
static pointer find(Session& session, ClusterTypeId id);
|
static pointer find(Session& session, ClusterTypeId id);
|
||||||
static RangeResults<ClusterTypeId> findOrphans(Session& session, Range range);
|
static RangeResults<ClusterTypeId> findOrphans(Session& session, Range range);
|
||||||
static RangeResults<ClusterTypeId> findUsed(Session& session, Range range);
|
static RangeResults<ClusterTypeId> findUsed(Session& session, Range range);
|
||||||
|
|
||||||
static void remove(Session& session, const std::string& name);
|
static void remove(Session& session, const std::string& name);
|
||||||
|
|
||||||
// Accessors
|
// Accessors
|
||||||
const std::string& getName() const { return _name; }
|
const std::string& getName() const { return _name; }
|
||||||
std::vector<Cluster::pointer> getClusters() const;
|
std::vector<Cluster::pointer> getClusters() const;
|
||||||
Cluster::pointer getCluster(const std::string& name) const;
|
Cluster::pointer getCluster(const std::string& name) const;
|
||||||
|
|
||||||
template<class Action>
|
template<class Action>
|
||||||
void persist(Action& a)
|
void persist(Action& a)
|
||||||
{
|
{
|
||||||
Wt::Dbo::field(a, _name, "name");
|
Wt::Dbo::field(a, _name, "name");
|
||||||
Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToOne, "cluster_type");
|
Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToOne, "cluster_type");
|
||||||
Wt::Dbo::belongsTo(a, _scanSettings, "scan_settings", Wt::Dbo::OnDeleteCascade);
|
Wt::Dbo::belongsTo(a, _scanSettings, "scan_settings", Wt::Dbo::OnDeleteCascade);
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
friend class Session;
|
friend class Session;
|
||||||
ClusterType(std::string_view name);
|
ClusterType(std::string_view name);
|
||||||
static pointer create(Session& session, const std::string& name);
|
static pointer create(Session& session, const std::string& name);
|
||||||
|
|
||||||
static const std::size_t _maxNameLength = 128;
|
static const std::size_t _maxNameLength = 128;
|
||||||
|
|
||||||
std::string _name;
|
std::string _name;
|
||||||
Wt::Dbo::collection< Wt::Dbo::ptr<Cluster> > _clusters;
|
Wt::Dbo::collection< Wt::Dbo::ptr<Cluster> > _clusters;
|
||||||
Wt::Dbo::ptr<ScanSettings> _scanSettings;
|
Wt::Dbo::ptr<ScanSettings> _scanSettings;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Database
|
} // namespace Database
|
||||||
|
|
||||||
|
|||||||
@@ -37,124 +37,127 @@
|
|||||||
namespace Database
|
namespace Database
|
||||||
{
|
{
|
||||||
|
|
||||||
class Artist;
|
class Artist;
|
||||||
class Cluster;
|
class Cluster;
|
||||||
class ClusterType;
|
class ClusterType;
|
||||||
class Release;
|
class Release;
|
||||||
class Session;
|
class Session;
|
||||||
class Track;
|
class Track;
|
||||||
class User;
|
class User;
|
||||||
|
|
||||||
class Release final : public Object<Release, ReleaseId>
|
class Release final : public Object<Release, ReleaseId>
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
struct FindParameters
|
struct FindParameters
|
||||||
{
|
{
|
||||||
std::vector<ClusterId> clusters; // if non empty, releases that belong to these clusters
|
std::vector<ClusterId> clusters; // if non empty, releases that belong to these clusters
|
||||||
std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords
|
std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords
|
||||||
ReleaseSortMethod sortMethod {ReleaseSortMethod::None};
|
ReleaseSortMethod sortMethod{ ReleaseSortMethod::None };
|
||||||
Range range;
|
Range range;
|
||||||
Wt::WDateTime writtenAfter;
|
Wt::WDateTime writtenAfter;
|
||||||
std::optional<DateRange> dateRange;
|
std::optional<DateRange> dateRange;
|
||||||
UserId starringUser; // only releases starred by this user
|
UserId starringUser; // only releases starred by this user
|
||||||
std::optional<Scrobbler> scrobbler; // and for this scrobbler
|
std::optional<Scrobbler> scrobbler; // and for this scrobbler
|
||||||
ArtistId artist; // only releases that involved this user
|
ArtistId artist; // only releases that involved this user
|
||||||
EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types
|
EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types
|
||||||
EnumSet<TrackArtistLinkType> excludedTrackArtistLinkTypes; // but not for these link types
|
EnumSet<TrackArtistLinkType> excludedTrackArtistLinkTypes; // but not for these link types
|
||||||
std::optional<ReleaseTypePrimary> primaryType; // if, set, matching this primary type
|
std::optional<ReleaseTypePrimary> primaryType; // if, set, matching this primary type
|
||||||
EnumSet<ReleaseTypeSecondary> secondaryTypes; // Matching all this (if any)
|
EnumSet<ReleaseTypeSecondary> secondaryTypes; // Matching all this (if any)
|
||||||
|
|
||||||
FindParameters& setClusters(const std::vector<ClusterId>& _clusters) { clusters = _clusters; return *this; }
|
FindParameters& setClusters(const std::vector<ClusterId>& _clusters) { clusters = _clusters; return *this; }
|
||||||
FindParameters& setKeywords(const std::vector<std::string_view>& _keywords) { keywords = _keywords; return *this; }
|
FindParameters& setKeywords(const std::vector<std::string_view>& _keywords) { keywords = _keywords; return *this; }
|
||||||
FindParameters& setSortMethod(ReleaseSortMethod _sortMethod) {sortMethod = _sortMethod; return *this; }
|
FindParameters& setSortMethod(ReleaseSortMethod _sortMethod) { sortMethod = _sortMethod; return *this; }
|
||||||
FindParameters& setRange(Range _range) {range = _range; return *this; }
|
FindParameters& setRange(Range _range) { range = _range; return *this; }
|
||||||
FindParameters& setWrittenAfter(const Wt::WDateTime& _after) {writtenAfter = _after; return *this; }
|
FindParameters& setWrittenAfter(const Wt::WDateTime& _after) { writtenAfter = _after; return *this; }
|
||||||
FindParameters& setDateRange(const std::optional<DateRange>& _dateRange) {dateRange = _dateRange; return *this; }
|
FindParameters& setDateRange(const std::optional<DateRange>& _dateRange) { dateRange = _dateRange; return *this; }
|
||||||
FindParameters& setStarringUser(UserId _user, Scrobbler _scrobbler) { starringUser = _user; scrobbler = _scrobbler; return *this; }
|
FindParameters& setStarringUser(UserId _user, Scrobbler _scrobbler) { starringUser = _user; scrobbler = _scrobbler; return *this; }
|
||||||
FindParameters& setArtist(ArtistId _artist, EnumSet<TrackArtistLinkType> _trackArtistLinkTypes = {}, EnumSet<TrackArtistLinkType> _excludedTrackArtistLinkTypes = {})
|
FindParameters& setArtist(ArtistId _artist, EnumSet<TrackArtistLinkType> _trackArtistLinkTypes = {}, EnumSet<TrackArtistLinkType> _excludedTrackArtistLinkTypes = {})
|
||||||
{
|
{
|
||||||
artist = _artist;
|
artist = _artist;
|
||||||
trackArtistLinkTypes = _trackArtistLinkTypes;
|
trackArtistLinkTypes = _trackArtistLinkTypes;
|
||||||
excludedTrackArtistLinkTypes = _excludedTrackArtistLinkTypes;
|
excludedTrackArtistLinkTypes = _excludedTrackArtistLinkTypes;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Release() = default;
|
Release() = default;
|
||||||
|
|
||||||
// Accessors
|
// Accessors
|
||||||
static std::size_t getCount(Session& session);
|
static std::size_t getCount(Session& session);
|
||||||
static bool exists(Session& session, ReleaseId id);
|
static bool exists(Session& session, ReleaseId id);
|
||||||
static pointer find(Session& session, const UUID& MBID);
|
static pointer find(Session& session, const UUID& MBID);
|
||||||
static std::vector<pointer> find(Session& session, const std::string& name);
|
static std::vector<pointer> find(Session& session, const std::string& name);
|
||||||
static pointer find(Session& session, ReleaseId id);
|
static pointer find(Session& session, ReleaseId id);
|
||||||
static RangeResults<ReleaseId> find(Session& session, const FindParameters& parameters);
|
static RangeResults<ReleaseId> find(Session& session, const FindParameters& parameters);
|
||||||
static RangeResults<ReleaseId> findOrphans(Session& session, Range range); // no track related
|
static RangeResults<ReleaseId> findOrphans(Session& session, Range range); // no track related
|
||||||
static RangeResults<ReleaseId> findOrderedByArtist(Session& session, Range range);
|
static RangeResults<ReleaseId> findOrderedByArtist(Session& session, Range range);
|
||||||
|
|
||||||
std::size_t getTracksCount() const;
|
// Get the cluster of the tracks that belong to this release
|
||||||
|
// Each clusters are grouped by cluster type, sorted by the number of occurence (max to min)
|
||||||
|
// size is the max number of cluster per cluster type
|
||||||
|
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(const std::vector<ObjectPtr<ClusterType>>& clusterTypes, std::size_t size) const;
|
||||||
|
|
||||||
// Get the cluster of the tracks that belong to this release
|
// Utility functions (if all tracks have the same values, which is legit to not be the case)
|
||||||
// Each clusters are grouped by cluster type, sorted by the number of occurence (max to min)
|
Wt::WDate getReleaseDate() const;
|
||||||
// size is the max number of cluster per cluster type
|
Wt::WDate getOriginalReleaseDate() const;
|
||||||
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(const std::vector<ObjectPtr<ClusterType>>& clusterTypes, std::size_t size) const;
|
std::optional<std::string> getCopyright() const;
|
||||||
|
std::optional<std::string> getCopyrightURL() const;
|
||||||
|
|
||||||
// Utility functions (if all tracks have the same values, which is legit to not be the case)
|
// Accessors
|
||||||
std::optional<int> getReleaseYear(bool originalDate = false) const;
|
const std::string& getName() const { return _name; }
|
||||||
std::optional<std::string> getCopyright() const;
|
std::optional<UUID> getMBID() const { return UUID::fromString(_MBID); }
|
||||||
std::optional<std::string> getCopyrightURL() const;
|
std::optional<std::size_t> getTotalDisc() const { return _totalDisc; }
|
||||||
|
std::size_t getDiscCount() const; // may not be total disc (if incomplete for example)
|
||||||
|
std::vector<DiscInfo> getDiscs() const;
|
||||||
|
std::chrono::milliseconds getDuration() const;
|
||||||
|
Wt::WDateTime getLastWritten() const;
|
||||||
|
std::optional<ReleaseTypePrimary> getPrimaryType() const { return _primaryType; }
|
||||||
|
EnumSet<ReleaseTypeSecondary> getSecondaryTypes() const { return _secondaryTypes; }
|
||||||
|
std::size_t getTracksCount() const;
|
||||||
|
|
||||||
// Accessors
|
// Setters
|
||||||
const std::string& getName() const { return _name; }
|
void setName(std::string_view name) { _name = name; }
|
||||||
std::optional<UUID> getMBID() const { return UUID::fromString(_MBID); }
|
void setMBID(const std::optional<UUID>& mbid) { _MBID = mbid ? mbid->getAsString() : ""; }
|
||||||
std::optional<std::size_t> getTotalDisc() const { return _totalDisc; }
|
void setTotalDisc(std::optional<int> totalDisc) { _totalDisc = totalDisc; }
|
||||||
std::size_t getDiscCount() const; // may not be total disc (if incomplete for example)
|
void setPrimaryType(std::optional<ReleaseTypePrimary> type) { _primaryType = type; }
|
||||||
std::chrono::milliseconds getDuration() const;
|
void setSecondaryTypes(EnumSet<ReleaseTypeSecondary> types) { _secondaryTypes = types; }
|
||||||
Wt::WDateTime getLastWritten() const;
|
|
||||||
std::optional<ReleaseTypePrimary> getPrimaryType() const { return _primaryType; }
|
|
||||||
EnumSet<ReleaseTypeSecondary> getSecondaryTypes() const { return _secondaryTypes; }
|
|
||||||
|
|
||||||
// Setters
|
// Get the artists of this release
|
||||||
void setName(std::string_view name) { _name = name; }
|
std::vector<ObjectPtr<Artist>> getArtists(TrackArtistLinkType type = TrackArtistLinkType::Artist) const;
|
||||||
void setMBID(const std::optional<UUID>& mbid) { _MBID = mbid ? mbid->getAsString() : ""; }
|
std::vector<ObjectPtr<Artist>> getReleaseArtists() const { return getArtists(TrackArtistLinkType::ReleaseArtist); }
|
||||||
void setTotalDisc(std::optional<int> totalDisc) { _totalDisc = totalDisc; }
|
bool hasVariousArtists() const;
|
||||||
void setPrimaryType(std::optional<ReleaseTypePrimary> type) { _primaryType = type; }
|
std::vector<pointer> getSimilarReleases(std::optional<std::size_t> offset = {}, std::optional<std::size_t> count = {}) const;
|
||||||
void setSecondaryTypes(EnumSet<ReleaseTypeSecondary> types) { _secondaryTypes = types; }
|
|
||||||
|
|
||||||
// Get the artists of this release
|
|
||||||
std::vector<ObjectPtr<Artist>> getArtists(TrackArtistLinkType type = TrackArtistLinkType::Artist) const;
|
|
||||||
std::vector<ObjectPtr<Artist>> getReleaseArtists() const { return getArtists(TrackArtistLinkType::ReleaseArtist); }
|
|
||||||
bool hasVariousArtists() const;
|
|
||||||
std::vector<pointer> getSimilarReleases(std::optional<std::size_t> offset = {}, std::optional<std::size_t> count = {}) const;
|
|
||||||
|
|
||||||
|
|
||||||
template<class Action>
|
template<class Action>
|
||||||
void persist(Action& a)
|
void persist(Action& a)
|
||||||
{
|
{
|
||||||
Wt::Dbo::field(a, _name, "name");
|
Wt::Dbo::field(a, _name, "name");
|
||||||
Wt::Dbo::field(a, _MBID, "mbid");
|
Wt::Dbo::field(a, _MBID, "mbid");
|
||||||
Wt::Dbo::field(a, _totalDisc, "total_disc");
|
Wt::Dbo::field(a, _totalDisc, "total_disc");
|
||||||
Wt::Dbo::field(a, _primaryType, "primary_type");
|
Wt::Dbo::field(a, _primaryType, "primary_type");
|
||||||
Wt::Dbo::field(a, _secondaryTypes, "secondary_types");
|
Wt::Dbo::field(a, _secondaryTypes, "secondary_types");
|
||||||
|
|
||||||
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToOne, "release");
|
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToOne, "release");
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
friend class Session;
|
friend class Session;
|
||||||
Release(const std::string& name, const std::optional<UUID>& MBID = {});
|
Release(const std::string& name, const std::optional<UUID>& MBID = {});
|
||||||
static pointer create(Session& session, const std::string& name, const std::optional<UUID>& MBID = {});
|
static pointer create(Session& session, const std::string& name, const std::optional<UUID>& MBID = {});
|
||||||
|
|
||||||
static constexpr std::size_t _maxNameLength {128};
|
Wt::WDate getReleaseDate(bool original) const;
|
||||||
|
|
||||||
std::string _name;
|
static constexpr std::size_t _maxNameLength{ 128 };
|
||||||
std::string _MBID;
|
|
||||||
std::optional<int> _totalDisc {};
|
|
||||||
std::optional<ReleaseTypePrimary> _primaryType;
|
|
||||||
EnumSet<ReleaseTypeSecondary> _secondaryTypes;
|
|
||||||
|
|
||||||
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> _tracks; // Tracks in the release
|
std::string _name;
|
||||||
};
|
std::string _MBID;
|
||||||
|
std::optional<int> _totalDisc{};
|
||||||
|
std::optional<ReleaseTypePrimary> _primaryType;
|
||||||
|
EnumSet<ReleaseTypeSecondary> _secondaryTypes;
|
||||||
|
|
||||||
|
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> _tracks; // Tracks in the release
|
||||||
|
};
|
||||||
|
|
||||||
} // namespace Database
|
} // namespace Database
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
|
|
||||||
#include <Wt/Dbo/Dbo.h>
|
#include <Wt/Dbo/Dbo.h>
|
||||||
|
|
||||||
|
#include "services/database/ArtistId.hpp"
|
||||||
#include "services/database/IdType.hpp"
|
#include "services/database/IdType.hpp"
|
||||||
#include "services/database/Object.hpp"
|
#include "services/database/Object.hpp"
|
||||||
#include "services/database/ReleaseId.hpp"
|
#include "services/database/ReleaseId.hpp"
|
||||||
@@ -35,55 +36,58 @@ LMS_DECLARE_IDTYPE(TrackArtistLinkId)
|
|||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
{
|
{
|
||||||
class Artist;
|
class Artist;
|
||||||
class Session;
|
class Session;
|
||||||
class Track;
|
class Track;
|
||||||
|
|
||||||
class TrackArtistLink final : public Object<TrackArtistLink, TrackArtistLinkId>
|
class TrackArtistLink final : public Object<TrackArtistLink, TrackArtistLinkId>
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
struct FindParameters
|
struct FindParameters
|
||||||
{
|
{
|
||||||
Range range;
|
Range range;
|
||||||
std::optional<TrackArtistLinkType> linkType; // if set, only artists that have produced at least one track with this link type
|
std::optional<TrackArtistLinkType> linkType; // if set, only artists that have produced at least one track with this link type
|
||||||
TrackId track; // artists involved in this track
|
ArtistId artist; // if set, links involved with this artist
|
||||||
ReleaseId release; // artists involved in this release
|
ReleaseId release; // if set, artists involved in this release
|
||||||
|
TrackId track; // if set, artists involved in this track
|
||||||
|
|
||||||
FindParameters& setRange(Range _range) {range = _range; return *this; }
|
FindParameters& setRange(Range _range) { range = _range; return *this; }
|
||||||
FindParameters& setLinkType(std::optional<TrackArtistLinkType> _linkType) { linkType = _linkType; return *this; }
|
FindParameters& setLinkType(std::optional<TrackArtistLinkType> _linkType) { linkType = _linkType; return *this; }
|
||||||
FindParameters& setTrack(TrackId _track) { track = _track; return *this; }
|
FindParameters& setArtist(ArtistId _artist) { artist = _artist; return *this; }
|
||||||
FindParameters& setRelease(ReleaseId _release) { release = _release; return *this; }
|
FindParameters& setRelease(ReleaseId _release) { release = _release; return *this; }
|
||||||
};
|
FindParameters& setTrack(TrackId _track) { track = _track; return *this; }
|
||||||
|
};
|
||||||
|
|
||||||
TrackArtistLink() = default;
|
TrackArtistLink() = default;
|
||||||
TrackArtistLink(ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType);
|
TrackArtistLink(ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType);
|
||||||
|
|
||||||
static RangeResults<TrackArtistLinkId> find(Session& session, const FindParameters& parameters);
|
static RangeResults<TrackArtistLinkId> find(Session& session, const FindParameters& parameters);
|
||||||
static pointer find(Session& session, TrackArtistLinkId linkId);
|
static pointer find(Session& session, TrackArtistLinkId linkId);
|
||||||
static pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType = {});
|
static pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType = {});
|
||||||
static EnumSet<TrackArtistLinkType> findUsedTypes(Session& session);
|
static EnumSet<TrackArtistLinkType> findUsedTypes(Session& session);
|
||||||
|
static EnumSet<TrackArtistLinkType> findUsedTypes(Session& session, ArtistId _artist);
|
||||||
|
|
||||||
ObjectPtr<Track> getTrack() const { return _track; }
|
ObjectPtr<Track> getTrack() const { return _track; }
|
||||||
ObjectPtr<Artist> getArtist() const { return _artist; }
|
ObjectPtr<Artist> getArtist() const { return _artist; }
|
||||||
TrackArtistLinkType getType() const { return _type; }
|
TrackArtistLinkType getType() const { return _type; }
|
||||||
std::string_view getSubType() const { return _subType; }
|
std::string_view getSubType() const { return _subType; }
|
||||||
|
|
||||||
template<class Action>
|
template<class Action>
|
||||||
void persist(Action& a)
|
void persist(Action& a)
|
||||||
{
|
{
|
||||||
Wt::Dbo::field(a, _type, "type");
|
Wt::Dbo::field(a, _type, "type");
|
||||||
Wt::Dbo::field(a, _subType, "subtype");
|
Wt::Dbo::field(a, _subType, "subtype");
|
||||||
|
|
||||||
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
|
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
|
||||||
Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade);
|
Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade);
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
TrackArtistLinkType _type;
|
TrackArtistLinkType _type;
|
||||||
std::string _subType;
|
std::string _subType;
|
||||||
|
|
||||||
Wt::Dbo::ptr<Track> _track;
|
Wt::Dbo::ptr<Track> _track;
|
||||||
Wt::Dbo::ptr<Artist> _artist;
|
Wt::Dbo::ptr<Artist> _artist;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -83,6 +83,12 @@ namespace Database
|
|||||||
static DateRange fromYearRange(int from, int to);
|
static DateRange fromYearRange(int from, int to);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct DiscInfo
|
||||||
|
{
|
||||||
|
std::size_t position;
|
||||||
|
std::string name;
|
||||||
|
};
|
||||||
|
|
||||||
enum class ArtistSortMethod
|
enum class ArtistSortMethod
|
||||||
{
|
{
|
||||||
None,
|
None,
|
||||||
|
|||||||
@@ -179,6 +179,15 @@ TEST_F(DatabaseFixture, Artist_singleTracktMultiRoles)
|
|||||||
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::Composer}));
|
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::Composer}));
|
||||||
EXPECT_EQ(tracks.results.size(), 0);
|
EXPECT_EQ(tracks.results.size(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
EnumSet<TrackArtistLinkType> types{ TrackArtistLink::findUsedTypes(session, artist.getId()) };
|
||||||
|
EXPECT_TRUE(types.contains(TrackArtistLinkType::ReleaseArtist));
|
||||||
|
EXPECT_TRUE(types.contains(TrackArtistLinkType::Artist));
|
||||||
|
EXPECT_TRUE(types.contains(TrackArtistLinkType::Writer));
|
||||||
|
EXPECT_FALSE(types.contains(TrackArtistLinkType::Composer));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(DatabaseFixture, Artist_singleTrackMultiArtists)
|
TEST_F(DatabaseFixture, Artist_singleTrackMultiArtists)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -350,8 +350,8 @@ TEST_F(DatabaseFixture, MultiTracksSingleReleaseDate)
|
|||||||
track1A.get().modify()->setOriginalDate(release1OriginalDate);
|
track1A.get().modify()->setOriginalDate(release1OriginalDate);
|
||||||
track1B.get().modify()->setOriginalDate(release1OriginalDate);
|
track1B.get().modify()->setOriginalDate(release1OriginalDate);
|
||||||
|
|
||||||
EXPECT_EQ(release1.get()->getReleaseYear(), release1Date.year());
|
EXPECT_EQ(release1.get()->getReleaseDate(), release1Date);
|
||||||
EXPECT_EQ(release1.get()->getReleaseYear(true), release1OriginalDate.year());
|
EXPECT_EQ(release1.get()->getOriginalReleaseDate(), release1OriginalDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -30,33 +30,33 @@
|
|||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
{
|
{
|
||||||
class Session;
|
class Session;
|
||||||
class TrackList;
|
class TrackList;
|
||||||
class User;
|
class User;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace Scrobbling
|
namespace Scrobbling
|
||||||
{
|
{
|
||||||
class IScrobbler
|
class IScrobbler
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
virtual ~IScrobbler() = default;
|
virtual ~IScrobbler() = default;
|
||||||
|
|
||||||
// Listens
|
// Listens
|
||||||
virtual void listenStarted(const Listen& listen) = 0;
|
virtual void listenStarted(const Listen& listen) = 0;
|
||||||
virtual void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) = 0;
|
virtual void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) = 0;
|
||||||
virtual void addTimedListen(const TimedListen& listen) = 0;
|
virtual void addTimedListen(const TimedListen& listen) = 0;
|
||||||
|
|
||||||
// Feedbacks
|
// Feedbacks
|
||||||
virtual void onStarred(Database::StarredArtistId) = 0;
|
virtual void onStarred(Database::StarredArtistId) = 0;
|
||||||
virtual void onUnstarred(Database::StarredArtistId) = 0;
|
virtual void onUnstarred(Database::StarredArtistId) = 0;
|
||||||
virtual void onStarred(Database::StarredReleaseId) = 0;
|
virtual void onStarred(Database::StarredReleaseId) = 0;
|
||||||
virtual void onUnstarred(Database::StarredReleaseId) = 0;
|
virtual void onUnstarred(Database::StarredReleaseId) = 0;
|
||||||
virtual void onStarred(Database::StarredTrackId) = 0;
|
virtual void onStarred(Database::StarredTrackId) = 0;
|
||||||
virtual void onUnstarred(Database::StarredTrackId) = 0;
|
virtual void onUnstarred(Database::StarredTrackId) = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
std::unique_ptr<IScrobbler> createScrobbler(std::string_view backendName);
|
std::unique_ptr<IScrobbler> createScrobbler(std::string_view backendName);
|
||||||
|
|
||||||
} // ns Scrobbling
|
} // ns Scrobbling
|
||||||
|
|
||||||
|
|||||||
@@ -37,272 +37,264 @@
|
|||||||
|
|
||||||
namespace Scrobbling
|
namespace Scrobbling
|
||||||
{
|
{
|
||||||
using namespace Database;
|
using namespace Database;
|
||||||
|
|
||||||
std::unique_ptr<IScrobblingService>
|
std::unique_ptr<IScrobblingService> createScrobblingService(boost::asio::io_context& ioContext, Db& db)
|
||||||
createScrobblingService(boost::asio::io_context& ioContext, Db& db)
|
{
|
||||||
{
|
return std::make_unique<ScrobblingService>(ioContext, db);
|
||||||
return std::make_unique<ScrobblingService>(ioContext, db);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
ScrobblingService::ScrobblingService(boost::asio::io_context& ioContext, Db& db)
|
ScrobblingService::ScrobblingService(boost::asio::io_context& ioContext, Db& db)
|
||||||
: _db {db}
|
: _db{ db }
|
||||||
{
|
{
|
||||||
LMS_LOG(SCROBBLING, INFO) << "Starting service...";
|
LMS_LOG(SCROBBLING, INFO) << "Starting service...";
|
||||||
_scrobblers.emplace(Scrobbler::Internal, std::make_unique<InternalScrobbler>(_db));
|
_scrobblers.emplace(Scrobbler::Internal, std::make_unique<InternalScrobbler>(_db));
|
||||||
_scrobblers.emplace(Scrobbler::ListenBrainz, std::make_unique<ListenBrainz::Scrobbler>(ioContext, _db));
|
_scrobblers.emplace(Scrobbler::ListenBrainz, std::make_unique<ListenBrainz::Scrobbler>(ioContext, _db));
|
||||||
LMS_LOG(SCROBBLING, INFO) << "Service started!";
|
LMS_LOG(SCROBBLING, INFO) << "Service started!";
|
||||||
}
|
}
|
||||||
|
|
||||||
ScrobblingService::~ScrobblingService()
|
ScrobblingService::~ScrobblingService()
|
||||||
{
|
{
|
||||||
LMS_LOG(SCROBBLING, INFO) << "Service stopped!";
|
LMS_LOG(SCROBBLING, INFO) << "Service stopped!";
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void ScrobblingService::listenStarted(const Listen& listen)
|
||||||
ScrobblingService::listenStarted(const Listen& listen)
|
{
|
||||||
{
|
if (std::optional<Scrobbler> scrobbler{ getUserScrobbler(listen.userId) })
|
||||||
if (std::optional<Scrobbler> scrobbler {getUserScrobbler(listen.userId)})
|
_scrobblers[*scrobbler]->listenStarted(listen);
|
||||||
_scrobblers[*scrobbler]->listenStarted(listen);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void ScrobblingService::listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration)
|
||||||
ScrobblingService::listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration)
|
{
|
||||||
{
|
if (std::optional<Scrobbler> scrobbler{ getUserScrobbler(listen.userId) })
|
||||||
if (std::optional<Scrobbler> scrobbler {getUserScrobbler(listen.userId)})
|
_scrobblers[*scrobbler]->listenFinished(listen, duration);
|
||||||
_scrobblers[*scrobbler]->listenFinished(listen, duration);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void ScrobblingService::addTimedListen(const TimedListen& listen)
|
||||||
ScrobblingService::addTimedListen(const TimedListen& listen)
|
{
|
||||||
{
|
if (std::optional<Scrobbler> scrobbler{ getUserScrobbler(listen.userId) })
|
||||||
if (std::optional<Scrobbler> scrobbler {getUserScrobbler(listen.userId)})
|
_scrobblers[*scrobbler]->addTimedListen(listen);
|
||||||
_scrobblers[*scrobbler]->addTimedListen(listen);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
std::optional<Scrobbler>
|
std::optional<Scrobbler> ScrobblingService::getUserScrobbler(UserId userId)
|
||||||
ScrobblingService::getUserScrobbler(UserId userId)
|
{
|
||||||
{
|
std::optional<Scrobbler> scrobbler;
|
||||||
std::optional<Scrobbler> scrobbler;
|
|
||||||
|
|
||||||
Session& session {_db.getTLSSession()};
|
Session& session{ _db.getTLSSession() };
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction{ session.createSharedTransaction() };
|
||||||
if (const User::pointer user {User::find(session, userId)})
|
if (const User::pointer user{ User::find(session, userId) })
|
||||||
scrobbler = user->getScrobbler();
|
scrobbler = user->getScrobbler();
|
||||||
|
|
||||||
return scrobbler;
|
return scrobbler;
|
||||||
}
|
}
|
||||||
|
|
||||||
ScrobblingService::ArtistContainer
|
ScrobblingService::ArtistContainer ScrobblingService::getRecentArtists(UserId userId, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range)
|
||||||
ScrobblingService::getRecentArtists(UserId userId, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range)
|
{
|
||||||
{
|
ArtistContainer res;
|
||||||
ArtistContainer res;
|
|
||||||
|
|
||||||
auto scrobbler {getUserScrobbler(userId)};
|
auto scrobbler{ getUserScrobbler(userId) };
|
||||||
if (!scrobbler)
|
if (!scrobbler)
|
||||||
return res;
|
return res;
|
||||||
|
|
||||||
Session& session {_db.getTLSSession()};
|
Session& session{ _db.getTLSSession() };
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction{ session.createSharedTransaction() };
|
||||||
|
|
||||||
res = Database::Listen::getRecentArtists(session, userId, *scrobbler, clusterIds, linkType, range);
|
res = Database::Listen::getRecentArtists(session, userId, *scrobbler, clusterIds, linkType, range);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
ScrobblingService::ReleaseContainer
|
ScrobblingService::ReleaseContainer ScrobblingService::getRecentReleases(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
|
||||||
ScrobblingService::getRecentReleases(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
|
{
|
||||||
{
|
ReleaseContainer res;
|
||||||
ReleaseContainer res;
|
|
||||||
|
|
||||||
auto scrobbler {getUserScrobbler(userId)};
|
auto scrobbler{ getUserScrobbler(userId) };
|
||||||
if (!scrobbler)
|
if (!scrobbler)
|
||||||
return res;
|
return res;
|
||||||
|
|
||||||
Session& session {_db.getTLSSession()};
|
Session& session{ _db.getTLSSession() };
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction{ session.createSharedTransaction() };
|
||||||
|
|
||||||
res = Database::Listen::getRecentReleases(session, userId, *scrobbler, clusterIds, range);
|
res = Database::Listen::getRecentReleases(session, userId, *scrobbler, clusterIds, range);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
ScrobblingService::TrackContainer
|
ScrobblingService::TrackContainer ScrobblingService::getRecentTracks(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
|
||||||
ScrobblingService::getRecentTracks(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
|
{
|
||||||
{
|
TrackContainer res;
|
||||||
TrackContainer res;
|
|
||||||
|
|
||||||
auto scrobbler {getUserScrobbler(userId)};
|
auto scrobbler{ getUserScrobbler(userId) };
|
||||||
if (!scrobbler)
|
if (!scrobbler)
|
||||||
return res;
|
return res;
|
||||||
|
|
||||||
Session& session {_db.getTLSSession()};
|
Session& session{ _db.getTLSSession() };
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction{ session.createSharedTransaction() };
|
||||||
|
|
||||||
res = Database::Listen::getRecentTracks(session, userId, *scrobbler, clusterIds, range);
|
res = Database::Listen::getRecentTracks(session, userId, *scrobbler, clusterIds, range);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Top
|
// Top
|
||||||
ScrobblingService::ArtistContainer
|
ScrobblingService::ArtistContainer ScrobblingService::getTopArtists(UserId userId, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range)
|
||||||
ScrobblingService::getTopArtists(UserId userId, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range)
|
{
|
||||||
{
|
ArtistContainer res;
|
||||||
ArtistContainer res;
|
|
||||||
|
|
||||||
auto scrobbler {getUserScrobbler(userId)};
|
auto scrobbler{ getUserScrobbler(userId) };
|
||||||
if (!scrobbler)
|
if (!scrobbler)
|
||||||
return res;
|
return res;
|
||||||
|
|
||||||
Session& session {_db.getTLSSession()};
|
Session& session{ _db.getTLSSession() };
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction{ session.createSharedTransaction() };
|
||||||
|
|
||||||
res = Database::Listen::getTopArtists(session, userId, *scrobbler, clusterIds, linkType, range);
|
res = Database::Listen::getTopArtists(session, userId, *scrobbler, clusterIds, linkType, range);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
ScrobblingService::ReleaseContainer
|
ScrobblingService::ReleaseContainer ScrobblingService::getTopReleases(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
|
||||||
ScrobblingService::getTopReleases(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
|
{
|
||||||
{
|
ReleaseContainer res;
|
||||||
ReleaseContainer res;
|
|
||||||
|
|
||||||
auto scrobbler {getUserScrobbler(userId)};
|
auto scrobbler{ getUserScrobbler(userId) };
|
||||||
if (!scrobbler)
|
if (!scrobbler)
|
||||||
return res;
|
return res;
|
||||||
|
|
||||||
Session& session {_db.getTLSSession()};
|
Session& session{ _db.getTLSSession() };
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction{ session.createSharedTransaction() };
|
||||||
|
|
||||||
res = Database::Listen::getTopReleases(session, userId, *scrobbler, clusterIds, range);
|
res = Database::Listen::getTopReleases(session, userId, *scrobbler, clusterIds, range);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
ScrobblingService::TrackContainer
|
ScrobblingService::TrackContainer ScrobblingService::getTopTracks(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
|
||||||
ScrobblingService::getTopTracks(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
|
{
|
||||||
{
|
TrackContainer res;
|
||||||
TrackContainer res;
|
|
||||||
|
|
||||||
auto scrobbler {getUserScrobbler(userId)};
|
auto scrobbler{ getUserScrobbler(userId) };
|
||||||
if (!scrobbler)
|
if (!scrobbler)
|
||||||
return res;
|
return res;
|
||||||
|
|
||||||
Session& session {_db.getTLSSession()};
|
Session& session{ _db.getTLSSession() };
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction{ session.createSharedTransaction() };
|
||||||
|
|
||||||
res = Database::Listen::getTopTracks(session, userId, *scrobbler, clusterIds, range);
|
res = Database::Listen::getTopTracks(session, userId, *scrobbler, clusterIds, range);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void ScrobblingService::star(UserId userId, ArtistId artistId)
|
||||||
ScrobblingService::star(UserId userId, ArtistId artistId)
|
{
|
||||||
{
|
star<Artist, ArtistId, StarredArtist>(userId, artistId);
|
||||||
star<Artist, ArtistId, StarredArtist>(userId, artistId);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void ScrobblingService::unstar(UserId userId, ArtistId artistId)
|
||||||
ScrobblingService::unstar(UserId userId, ArtistId artistId)
|
{
|
||||||
{
|
unstar<Artist, ArtistId, StarredArtist>(userId, artistId);
|
||||||
unstar<Artist, ArtistId, StarredArtist>(userId, artistId);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
bool ScrobblingService::isStarred(UserId userId, ArtistId artistId)
|
||||||
ScrobblingService::isStarred(UserId userId, ArtistId artistId)
|
{
|
||||||
{
|
return isStarred<Artist, ArtistId, StarredArtist>(userId, artistId);
|
||||||
return isStarred<Artist, ArtistId, StarredArtist>(userId, artistId);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
ScrobblingService::ArtistContainer
|
Wt::WDateTime ScrobblingService::getStarredDateTime(UserId userId, ArtistId artistId)
|
||||||
ScrobblingService::getStarredArtists(UserId userId, const std::vector<ClusterId>& clusterIds,
|
{
|
||||||
std::optional<TrackArtistLinkType> linkType,
|
return getStarredDateTime<Artist, ArtistId, StarredArtist>(userId, artistId);
|
||||||
ArtistSortMethod sortMethod,
|
}
|
||||||
Range range)
|
|
||||||
{
|
|
||||||
auto scrobbler {getUserScrobbler(userId)};
|
|
||||||
if (!scrobbler)
|
|
||||||
return {};
|
|
||||||
|
|
||||||
Artist::FindParameters params;
|
ScrobblingService::ArtistContainer ScrobblingService::getStarredArtists(UserId userId, const std::vector<ClusterId>& clusterIds,
|
||||||
params.setStarringUser(userId, *scrobbler);
|
std::optional<TrackArtistLinkType> linkType,
|
||||||
params.setClusters(clusterIds);
|
ArtistSortMethod sortMethod,
|
||||||
params.setLinkType(linkType);
|
Range range)
|
||||||
params.setSortMethod(sortMethod);
|
{
|
||||||
params.setRange(range);
|
auto scrobbler{ getUserScrobbler(userId) };
|
||||||
|
if (!scrobbler)
|
||||||
|
return {};
|
||||||
|
|
||||||
Session& session {_db.getTLSSession()};
|
Artist::FindParameters params;
|
||||||
auto transaction {session.createSharedTransaction()};
|
params.setStarringUser(userId, *scrobbler);
|
||||||
|
params.setClusters(clusterIds);
|
||||||
|
params.setLinkType(linkType);
|
||||||
|
params.setSortMethod(sortMethod);
|
||||||
|
params.setRange(range);
|
||||||
|
|
||||||
return Artist::find(session, params);
|
Session& session{ _db.getTLSSession() };
|
||||||
}
|
auto transaction{ session.createSharedTransaction() };
|
||||||
|
|
||||||
void
|
return Artist::find(session, params);
|
||||||
ScrobblingService::star(UserId userId, ReleaseId releaseId)
|
}
|
||||||
{
|
|
||||||
star<Release, ReleaseId, StarredRelease>(userId, releaseId);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void ScrobblingService::star(UserId userId, ReleaseId releaseId)
|
||||||
ScrobblingService::unstar(UserId userId, ReleaseId releaseId)
|
{
|
||||||
{
|
star<Release, ReleaseId, StarredRelease>(userId, releaseId);
|
||||||
unstar<Release, ReleaseId, StarredRelease>(userId, releaseId);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
void ScrobblingService::unstar(UserId userId, ReleaseId releaseId)
|
||||||
ScrobblingService::isStarred(UserId userId, ReleaseId releaseId)
|
{
|
||||||
{
|
unstar<Release, ReleaseId, StarredRelease>(userId, releaseId);
|
||||||
return isStarred<Release, ReleaseId, StarredRelease>(userId, releaseId);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
ScrobblingService::ReleaseContainer
|
bool ScrobblingService::isStarred(UserId userId, ReleaseId releaseId)
|
||||||
ScrobblingService::getStarredReleases(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
|
{
|
||||||
{
|
return isStarred<Release, ReleaseId, StarredRelease>(userId, releaseId);
|
||||||
auto scrobbler {getUserScrobbler(userId)};
|
}
|
||||||
if (!scrobbler)
|
|
||||||
return {};
|
|
||||||
|
|
||||||
Release::FindParameters params;
|
Wt::WDateTime ScrobblingService::getStarredDateTime(UserId userId, ReleaseId releaseId)
|
||||||
params.setStarringUser(userId, *scrobbler);
|
{
|
||||||
params.setClusters(clusterIds);
|
return getStarredDateTime<Release, ReleaseId, StarredRelease>(userId, releaseId);
|
||||||
params.setSortMethod(ReleaseSortMethod::StarredDateDesc);
|
}
|
||||||
params.setRange(range);
|
|
||||||
|
|
||||||
Session& session {_db.getTLSSession()};
|
ScrobblingService::ReleaseContainer ScrobblingService::getStarredReleases(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
|
||||||
auto transaction {session.createSharedTransaction()};
|
{
|
||||||
|
auto scrobbler{ getUserScrobbler(userId) };
|
||||||
|
if (!scrobbler)
|
||||||
|
return {};
|
||||||
|
|
||||||
return Release::find(session, params);
|
Release::FindParameters params;
|
||||||
}
|
params.setStarringUser(userId, *scrobbler);
|
||||||
|
params.setClusters(clusterIds);
|
||||||
|
params.setSortMethod(ReleaseSortMethod::StarredDateDesc);
|
||||||
|
params.setRange(range);
|
||||||
|
|
||||||
void
|
Session& session{ _db.getTLSSession() };
|
||||||
ScrobblingService::star(UserId userId, TrackId trackId)
|
auto transaction{ session.createSharedTransaction() };
|
||||||
{
|
|
||||||
star<Track, TrackId, StarredTrack>(userId, trackId);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
return Release::find(session, params);
|
||||||
ScrobblingService::unstar(UserId userId, TrackId trackId)
|
}
|
||||||
{
|
|
||||||
unstar<Track, TrackId, StarredTrack>(userId, trackId);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
void ScrobblingService::star(UserId userId, TrackId trackId)
|
||||||
ScrobblingService::isStarred(UserId userId, TrackId trackId)
|
{
|
||||||
{
|
star<Track, TrackId, StarredTrack>(userId, trackId);
|
||||||
return isStarred<Track, TrackId, StarredTrack>(userId, trackId);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
ScrobblingService::TrackContainer
|
void ScrobblingService::unstar(UserId userId, TrackId trackId)
|
||||||
ScrobblingService::getStarredTracks(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
|
{
|
||||||
{
|
unstar<Track, TrackId, StarredTrack>(userId, trackId);
|
||||||
auto scrobbler {getUserScrobbler(userId)};
|
}
|
||||||
if (!scrobbler)
|
|
||||||
return {};
|
|
||||||
|
|
||||||
Track::FindParameters params;
|
bool ScrobblingService::isStarred(UserId userId, TrackId trackId)
|
||||||
params.setStarringUser(userId, *scrobbler);
|
{
|
||||||
params.setClusters(clusterIds);
|
return isStarred<Track, TrackId, StarredTrack>(userId, trackId);
|
||||||
params.setSortMethod(TrackSortMethod::StarredDateDesc);
|
}
|
||||||
params.setRange(range);
|
|
||||||
|
|
||||||
Session& session {_db.getTLSSession()};
|
Wt::WDateTime ScrobblingService::getStarredDateTime(UserId userId, TrackId trackId)
|
||||||
auto transaction {session.createSharedTransaction()};
|
{
|
||||||
|
return getStarredDateTime<Track, TrackId, StarredTrack>(userId, trackId);
|
||||||
|
}
|
||||||
|
|
||||||
return Track::find(session, params);
|
ScrobblingService::TrackContainer ScrobblingService::getStarredTracks(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
|
||||||
}
|
{
|
||||||
|
auto scrobbler{ getUserScrobbler(userId) };
|
||||||
|
if (!scrobbler)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
Track::FindParameters params;
|
||||||
|
params.setStarringUser(userId, *scrobbler);
|
||||||
|
params.setClusters(clusterIds);
|
||||||
|
params.setSortMethod(TrackSortMethod::StarredDateDesc);
|
||||||
|
params.setRange(range);
|
||||||
|
|
||||||
|
Session& session{ _db.getTLSSession() };
|
||||||
|
auto transaction{ session.createSharedTransaction() };
|
||||||
|
|
||||||
|
return Track::find(session, params);
|
||||||
|
}
|
||||||
} // ns Scrobbling
|
} // ns Scrobbling
|
||||||
|
|
||||||
|
|||||||
@@ -28,74 +28,79 @@
|
|||||||
|
|
||||||
namespace Scrobbling
|
namespace Scrobbling
|
||||||
{
|
{
|
||||||
class ScrobblingService : public IScrobblingService
|
class ScrobblingService : public IScrobblingService
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
ScrobblingService(boost::asio::io_context& ioContext, Database::Db& db);
|
ScrobblingService(boost::asio::io_context& ioContext, Database::Db& db);
|
||||||
~ScrobblingService();
|
~ScrobblingService();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void listenStarted(const Listen& listen) override;
|
void listenStarted(const Listen& listen) override;
|
||||||
void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) override;
|
void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) override;
|
||||||
void addTimedListen(const TimedListen& listen) override;
|
void addTimedListen(const TimedListen& listen) override;
|
||||||
|
|
||||||
ArtistContainer getRecentArtists(Database::UserId userId,
|
ArtistContainer getRecentArtists(Database::UserId userId,
|
||||||
const std::vector<Database::ClusterId>& clusterIds,
|
const std::vector<Database::ClusterId>& clusterIds,
|
||||||
std::optional<Database::TrackArtistLinkType> linkType,
|
std::optional<Database::TrackArtistLinkType> linkType,
|
||||||
Database::Range range) override;
|
Database::Range range) override;
|
||||||
|
|
||||||
ReleaseContainer getRecentReleases(Database::UserId userId,
|
ReleaseContainer getRecentReleases(Database::UserId userId,
|
||||||
const std::vector<Database::ClusterId>& clusterIds,
|
const std::vector<Database::ClusterId>& clusterIds,
|
||||||
Database::Range range) override;
|
Database::Range range) override;
|
||||||
|
|
||||||
TrackContainer getRecentTracks(Database::UserId userId,
|
TrackContainer getRecentTracks(Database::UserId userId,
|
||||||
const std::vector<Database::ClusterId>& clusterIds,
|
const std::vector<Database::ClusterId>& clusterIds,
|
||||||
Database::Range range) override;
|
Database::Range range) override;
|
||||||
|
|
||||||
ArtistContainer getTopArtists(Database::UserId userId,
|
ArtistContainer getTopArtists(Database::UserId userId,
|
||||||
const std::vector<Database::ClusterId>& clusterIds,
|
const std::vector<Database::ClusterId>& clusterIds,
|
||||||
std::optional<Database::TrackArtistLinkType> linkType,
|
std::optional<Database::TrackArtistLinkType> linkType,
|
||||||
Database::Range range) override;
|
Database::Range range) override;
|
||||||
|
|
||||||
ReleaseContainer getTopReleases(Database::UserId userId,
|
ReleaseContainer getTopReleases(Database::UserId userId,
|
||||||
const std::vector<Database::ClusterId>& clusterIds,
|
const std::vector<Database::ClusterId>& clusterIds,
|
||||||
Database::Range range) override;
|
Database::Range range) override;
|
||||||
|
|
||||||
TrackContainer getTopTracks(Database::UserId userId,
|
TrackContainer getTopTracks(Database::UserId userId,
|
||||||
const std::vector<Database::ClusterId>& clusterIds,
|
const std::vector<Database::ClusterId>& clusterIds,
|
||||||
Database::Range range) override;
|
Database::Range range) override;
|
||||||
|
|
||||||
void star(Database::UserId userId, Database::ArtistId artistId) override;
|
void star(Database::UserId userId, Database::ArtistId artistId) override;
|
||||||
void unstar(Database::UserId userId, Database::ArtistId artistId) override;
|
void unstar(Database::UserId userId, Database::ArtistId artistId) override;
|
||||||
bool isStarred(Database::UserId userId, Database::ArtistId artistId) override;
|
bool isStarred(Database::UserId userId, Database::ArtistId artistId) override;
|
||||||
ArtistContainer getStarredArtists(Database::UserId userId,
|
Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::ArtistId artistId) override;
|
||||||
const std::vector<Database::ClusterId>& clusterIds,
|
ArtistContainer getStarredArtists(Database::UserId userId,
|
||||||
std::optional<Database::TrackArtistLinkType> linkType,
|
const std::vector<Database::ClusterId>& clusterIds,
|
||||||
Database::ArtistSortMethod sortMethod,
|
std::optional<Database::TrackArtistLinkType> linkType,
|
||||||
Database::Range range) override;
|
Database::ArtistSortMethod sortMethod,
|
||||||
|
Database::Range range) override;
|
||||||
|
|
||||||
void star(Database::UserId userId, Database::ReleaseId releaseId) override;
|
void star(Database::UserId userId, Database::ReleaseId releaseId) override;
|
||||||
void unstar(Database::UserId userId, Database::ReleaseId releaseId) override;
|
void unstar(Database::UserId userId, Database::ReleaseId releaseId) override;
|
||||||
bool isStarred(Database::UserId userId, Database::ReleaseId artistId) override;
|
bool isStarred(Database::UserId userId, Database::ReleaseId releasedId) override;
|
||||||
ReleaseContainer getStarredReleases(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) override;
|
Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::ReleaseId releasedId) override;
|
||||||
|
ReleaseContainer getStarredReleases(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) override;
|
||||||
|
|
||||||
void star(Database::UserId userId, Database::TrackId trackId) override;
|
void star(Database::UserId userId, Database::TrackId trackId) override;
|
||||||
void unstar(Database::UserId userId, Database::TrackId trackId) override;
|
void unstar(Database::UserId userId, Database::TrackId trackId) override;
|
||||||
bool isStarred(Database::UserId userId, Database::TrackId trackId) override;
|
bool isStarred(Database::UserId userId, Database::TrackId trackId) override;
|
||||||
TrackContainer getStarredTracks(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) override;
|
Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::TrackId trackId) override;
|
||||||
|
TrackContainer getStarredTracks(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) override;
|
||||||
|
|
||||||
std::optional<Database::Scrobbler> getUserScrobbler(Database::UserId userId);
|
std::optional<Database::Scrobbler> getUserScrobbler(Database::UserId userId);
|
||||||
|
|
||||||
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
||||||
void star(Database::UserId userId, ObjIdType id);
|
void star(Database::UserId userId, ObjIdType id);
|
||||||
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
||||||
void unstar(Database::UserId userId, ObjIdType id);
|
void unstar(Database::UserId userId, ObjIdType id);
|
||||||
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
||||||
bool isStarred(Database::UserId userId, ObjIdType id);
|
bool isStarred(Database::UserId userId, ObjIdType id);
|
||||||
|
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
||||||
|
Wt::WDateTime getStarredDateTime(Database::UserId userId, ObjIdType id);
|
||||||
|
|
||||||
Database::Db& _db;
|
Database::Db& _db;
|
||||||
std::unordered_map<Database::Scrobbler, std::unique_ptr<IScrobbler>> _scrobblers;
|
std::unordered_map<Database::Scrobbler, std::unique_ptr<IScrobbler>> _scrobblers;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // ns Scrobbling
|
} // ns Scrobbling
|
||||||
|
|
||||||
|
|||||||
@@ -25,76 +25,90 @@
|
|||||||
|
|
||||||
namespace Scrobbling
|
namespace Scrobbling
|
||||||
{
|
{
|
||||||
using namespace Database;
|
using namespace Database;
|
||||||
|
|
||||||
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
||||||
void
|
void ScrobblingService::star(UserId userId, ObjIdType objId)
|
||||||
ScrobblingService::star(UserId userId, ObjIdType objId)
|
{
|
||||||
{
|
auto scrobbler {getUserScrobbler(userId)};
|
||||||
auto scrobbler {getUserScrobbler(userId)};
|
if (!scrobbler)
|
||||||
if (!scrobbler)
|
return;
|
||||||
return;
|
|
||||||
|
|
||||||
typename StarredObjType::IdType starredObjId;
|
typename StarredObjType::IdType starredObjId;
|
||||||
{
|
{
|
||||||
Session& session {_db.getTLSSession()};
|
Session& session {_db.getTLSSession()};
|
||||||
auto transaction {session.createUniqueTransaction()};
|
auto transaction {session.createUniqueTransaction()};
|
||||||
|
|
||||||
typename StarredObjType::pointer starredObj {StarredObjType::find(session, objId, userId, *scrobbler)};
|
typename StarredObjType::pointer starredObj {StarredObjType::find(session, objId, userId, *scrobbler)};
|
||||||
if (!starredObj)
|
if (!starredObj)
|
||||||
{
|
{
|
||||||
const typename ObjType::pointer obj {ObjType::find(session, objId)};
|
const typename ObjType::pointer obj {ObjType::find(session, objId)};
|
||||||
if (!obj)
|
if (!obj)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const User::pointer user {User::find(session, userId)};
|
const User::pointer user {User::find(session, userId)};
|
||||||
if (!user)
|
if (!user)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
starredObj = session.create<StarredObjType>(obj, user, *scrobbler);
|
starredObj = session.create<StarredObjType>(obj, user, *scrobbler);
|
||||||
}
|
}
|
||||||
starredObj.modify()->setDateTime(Wt::WDateTime::currentDateTime());
|
starredObj.modify()->setDateTime(Wt::WDateTime::currentDateTime());
|
||||||
starredObjId = starredObj->getId();
|
starredObjId = starredObj->getId();
|
||||||
}
|
}
|
||||||
_scrobblers[*scrobbler]->onStarred(starredObjId);
|
_scrobblers[*scrobbler]->onStarred(starredObjId);
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
||||||
void
|
void ScrobblingService::unstar(UserId userId, ObjIdType objId)
|
||||||
ScrobblingService::unstar(UserId userId, ObjIdType objId)
|
{
|
||||||
{
|
auto scrobbler {getUserScrobbler(userId)};
|
||||||
auto scrobbler {getUserScrobbler(userId)};
|
if (!scrobbler)
|
||||||
if (!scrobbler)
|
return;
|
||||||
return;
|
|
||||||
|
|
||||||
typename StarredObjType::IdType starredObjId;
|
typename StarredObjType::IdType starredObjId;
|
||||||
{
|
{
|
||||||
Session& session {_db.getTLSSession()};
|
Session& session {_db.getTLSSession()};
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
typename StarredObjType::pointer starredObj {StarredObjType::find(session, objId, userId, *scrobbler)};
|
typename StarredObjType::pointer starredObj {StarredObjType::find(session, objId, userId, *scrobbler)};
|
||||||
if (!starredObj)
|
if (!starredObj)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
starredObjId = starredObj->getId();
|
starredObjId = starredObj->getId();
|
||||||
}
|
}
|
||||||
_scrobblers[*scrobbler]->onUnstarred(starredObjId);
|
_scrobblers[*scrobbler]->onUnstarred(starredObjId);
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
||||||
bool
|
bool ScrobblingService::isStarred(UserId userId, ObjIdType objId)
|
||||||
ScrobblingService::isStarred(UserId userId, ObjIdType objId)
|
{
|
||||||
{
|
auto scrobbler {getUserScrobbler(userId)};
|
||||||
auto scrobbler {getUserScrobbler(userId)};
|
if (!scrobbler)
|
||||||
if (!scrobbler)
|
return false;
|
||||||
return false;
|
|
||||||
|
|
||||||
Session& session {_db.getTLSSession()};
|
Session& session {_db.getTLSSession()};
|
||||||
auto transaction {session.createSharedTransaction()};
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
typename StarredObjType::pointer starredObj {StarredObjType::find(session, objId, userId, *scrobbler)};
|
typename StarredObjType::pointer starredObj {StarredObjType::find(session, objId, userId, *scrobbler)};
|
||||||
return starredObj && (starredObj->getScrobblingState() != ScrobblingState::PendingRemove);
|
return starredObj && (starredObj->getScrobblingState() != ScrobblingState::PendingRemove);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
||||||
|
Wt::WDateTime ScrobblingService::getStarredDateTime(UserId userId, ObjIdType objId)
|
||||||
|
{
|
||||||
|
auto scrobbler {getUserScrobbler(userId)};
|
||||||
|
if (!scrobbler)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
Session& session {_db.getTLSSession()};
|
||||||
|
auto transaction {session.createSharedTransaction()};
|
||||||
|
|
||||||
|
typename StarredObjType::pointer starredObj {StarredObjType::find(session, objId, userId, *scrobbler)};
|
||||||
|
if (starredObj && (starredObj->getScrobblingState() != ScrobblingState::PendingRemove))
|
||||||
|
return starredObj->getDateTime();
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
} // ns Scrobbling
|
} // ns Scrobbling
|
||||||
|
|
||||||
|
|||||||
@@ -30,101 +30,93 @@
|
|||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
template <typename StarredObjType>
|
template <typename StarredObjType>
|
||||||
void onStarred(Database::Session& session, typename StarredObjType::IdType id)
|
void onStarred(Database::Session& session, typename StarredObjType::IdType id)
|
||||||
{
|
{
|
||||||
auto transaction {session.createUniqueTransaction()};
|
auto transaction{ session.createUniqueTransaction() };
|
||||||
|
|
||||||
if (auto starredObj {StarredObjType::find(session, id)})
|
if (auto starredObj{ StarredObjType::find(session, id) })
|
||||||
starredObj.modify()->setScrobblingState(Database::ScrobblingState::Synchronized);
|
starredObj.modify()->setScrobblingState(Database::ScrobblingState::Synchronized);
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename StarredObjType>
|
template <typename StarredObjType>
|
||||||
void onUnstarred(Database::Session& session, typename StarredObjType::IdType id)
|
void onUnstarred(Database::Session& session, typename StarredObjType::IdType id)
|
||||||
{
|
{
|
||||||
auto transaction {session.createUniqueTransaction()};
|
auto transaction{ session.createUniqueTransaction() };
|
||||||
|
|
||||||
if (auto starredObj {StarredObjType::find(session, id)})
|
if (auto starredObj{ StarredObjType::find(session, id) })
|
||||||
starredObj.remove();
|
starredObj.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace Scrobbling
|
namespace Scrobbling
|
||||||
{
|
{
|
||||||
InternalScrobbler::InternalScrobbler(Database::Db& db)
|
InternalScrobbler::InternalScrobbler(Database::Db& db)
|
||||||
: _db {db}
|
: _db{ db }
|
||||||
{}
|
{}
|
||||||
|
|
||||||
void
|
void InternalScrobbler::listenStarted(const Listen&)
|
||||||
InternalScrobbler::listenStarted(const Listen&)
|
{
|
||||||
{
|
// nothing to do
|
||||||
// nothing to do
|
}
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void InternalScrobbler::listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration)
|
||||||
InternalScrobbler::listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration)
|
{
|
||||||
{
|
// only record tracks that have been played for at least of few seconds...
|
||||||
// only record tracks that have been played for at least of few seconds...
|
if (duration && *duration < std::chrono::seconds{ 5 })
|
||||||
if (duration && *duration < std::chrono::seconds {5})
|
return;
|
||||||
return;
|
|
||||||
|
|
||||||
addTimedListen({listen, Wt::WDateTime::currentDateTime()});
|
addTimedListen({ listen, Wt::WDateTime::currentDateTime() });
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void InternalScrobbler::addTimedListen(const TimedListen& listen)
|
||||||
InternalScrobbler::addTimedListen(const TimedListen& listen)
|
{
|
||||||
{
|
Database::Session& session{ _db.getTLSSession() };
|
||||||
Database::Session& session {_db.getTLSSession()};
|
auto transaction{ session.createUniqueTransaction() };
|
||||||
auto transaction {session.createUniqueTransaction()};
|
|
||||||
|
|
||||||
if (Database::Listen::find(session, listen.userId, listen.trackId, Database::Scrobbler::Internal, listen.listenedAt))
|
if (Database::Listen::find(session, listen.userId, listen.trackId, Database::Scrobbler::Internal, listen.listenedAt))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const Database::User::pointer user {Database::User::find(session, listen.userId)};
|
const Database::User::pointer user{ Database::User::find(session, listen.userId) };
|
||||||
if (!user)
|
if (!user)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const Database::Track::pointer track {Database::Track::find(session, listen.trackId)};
|
const Database::Track::pointer track{ Database::Track::find(session, listen.trackId) };
|
||||||
if (!track)
|
if (!track)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
auto dbListen {session.create<Database::Listen>(user, track, Database::Scrobbler::Internal, listen.listenedAt)};
|
auto dbListen{ session.create<Database::Listen>(user, track, Database::Scrobbler::Internal, listen.listenedAt) };
|
||||||
dbListen.modify()->setScrobblingState(Database::ScrobblingState::Synchronized);
|
dbListen.modify()->setScrobblingState(Database::ScrobblingState::Synchronized);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void InternalScrobbler::onStarred(Database::StarredArtistId starredArtistId)
|
||||||
InternalScrobbler::onStarred(Database::StarredArtistId starredArtistId)
|
{
|
||||||
{
|
::onStarred<Database::StarredArtist>(_db.getTLSSession(), starredArtistId);
|
||||||
::onStarred<Database::StarredArtist>(_db.getTLSSession(), starredArtistId);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void InternalScrobbler::onUnstarred(Database::StarredArtistId starredArtistId)
|
||||||
InternalScrobbler::onUnstarred(Database::StarredArtistId starredArtistId)
|
{
|
||||||
{
|
::onUnstarred<Database::StarredArtist>(_db.getTLSSession(), starredArtistId);
|
||||||
::onUnstarred<Database::StarredArtist>(_db.getTLSSession(), starredArtistId);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void InternalScrobbler::onStarred(Database::StarredReleaseId starredReleaseId)
|
||||||
InternalScrobbler::onStarred(Database::StarredReleaseId starredReleaseId)
|
{
|
||||||
{
|
::onStarred<Database::StarredRelease>(_db.getTLSSession(), starredReleaseId);
|
||||||
::onStarred<Database::StarredRelease>(_db.getTLSSession(), starredReleaseId);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
void InternalScrobbler::onUnstarred(Database::StarredReleaseId starredReleaseId)
|
void InternalScrobbler::onUnstarred(Database::StarredReleaseId starredReleaseId)
|
||||||
{
|
{
|
||||||
::onUnstarred<Database::StarredRelease>(_db.getTLSSession(), starredReleaseId);
|
::onUnstarred<Database::StarredRelease>(_db.getTLSSession(), starredReleaseId);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void InternalScrobbler::onStarred(Database::StarredTrackId starredTrackId)
|
||||||
InternalScrobbler::onStarred(Database::StarredTrackId starredTrackId)
|
{
|
||||||
{
|
::onStarred<Database::StarredTrack>(_db.getTLSSession(), starredTrackId);
|
||||||
::onStarred<Database::StarredTrack>(_db.getTLSSession(), starredTrackId);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void InternalScrobbler::onUnstarred(Database::StarredTrackId starredTrackId)
|
||||||
InternalScrobbler::onUnstarred(Database::StarredTrackId starredTrackId)
|
{
|
||||||
{
|
::onUnstarred<Database::StarredTrack>(_db.getTLSSession(), starredTrackId);
|
||||||
::onUnstarred<Database::StarredTrack>(_db.getTLSSession(), starredTrackId);
|
}
|
||||||
}
|
|
||||||
} // Scrobbling
|
} // Scrobbling
|
||||||
|
|
||||||
|
|||||||
@@ -23,30 +23,30 @@
|
|||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
{
|
{
|
||||||
class Db;
|
class Db;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace Scrobbling
|
namespace Scrobbling
|
||||||
{
|
{
|
||||||
class InternalScrobbler final : public IScrobbler
|
class InternalScrobbler final : public IScrobbler
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
InternalScrobbler(Database::Db& db);
|
InternalScrobbler(Database::Db& db);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// IScrobbler
|
// IScrobbler
|
||||||
void listenStarted(const Listen& listen) override;
|
void listenStarted(const Listen& listen) override;
|
||||||
void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) override;
|
void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) override;
|
||||||
void addTimedListen(const TimedListen& listen) override;
|
void addTimedListen(const TimedListen& listen) override;
|
||||||
|
|
||||||
void onStarred(Database::StarredArtistId) override;
|
void onStarred(Database::StarredArtistId) override;
|
||||||
void onUnstarred(Database::StarredArtistId) override;
|
void onUnstarred(Database::StarredArtistId) override;
|
||||||
void onStarred(Database::StarredReleaseId) override;
|
void onStarred(Database::StarredReleaseId) override;
|
||||||
void onUnstarred(Database::StarredReleaseId) override;
|
void onUnstarred(Database::StarredReleaseId) override;
|
||||||
void onStarred(Database::StarredTrackId) override;
|
void onStarred(Database::StarredTrackId) override;
|
||||||
void onUnstarred(Database::StarredTrackId) override;
|
void onUnstarred(Database::StarredTrackId) override;
|
||||||
|
|
||||||
Database::Db& _db;
|
Database::Db& _db;
|
||||||
};
|
};
|
||||||
} // Scrobbling
|
} // Scrobbling
|
||||||
|
|
||||||
|
|||||||
@@ -23,9 +23,9 @@
|
|||||||
|
|
||||||
namespace Scrobbling
|
namespace Scrobbling
|
||||||
{
|
{
|
||||||
class Exception : public LmsException
|
class Exception : public LmsException
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
using LmsException::LmsException;
|
using LmsException::LmsException;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,11 +19,11 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <boost/asio/io_service.hpp>
|
|
||||||
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <boost/asio/io_service.hpp>
|
||||||
|
#include <Wt/WDateTime.h>
|
||||||
|
|
||||||
#include "services/scrobbling/Listen.hpp"
|
#include "services/scrobbling/Listen.hpp"
|
||||||
#include "services/database/ArtistId.hpp"
|
#include "services/database/ArtistId.hpp"
|
||||||
@@ -34,77 +34,80 @@
|
|||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
{
|
{
|
||||||
class Db;
|
class Db;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace Scrobbling
|
namespace Scrobbling
|
||||||
{
|
{
|
||||||
|
|
||||||
class IScrobblingService
|
class IScrobblingService
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
virtual ~IScrobblingService() = default;
|
virtual ~IScrobblingService() = default;
|
||||||
|
|
||||||
// Scrobbling
|
// Scrobbling
|
||||||
virtual void listenStarted(const Listen& listen) = 0;
|
virtual void listenStarted(const Listen& listen) = 0;
|
||||||
virtual void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> playedDuration = std::nullopt) = 0;
|
virtual void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> playedDuration = std::nullopt) = 0;
|
||||||
|
|
||||||
virtual void addTimedListen(const TimedListen& listen) = 0;
|
virtual void addTimedListen(const TimedListen& listen) = 0;
|
||||||
|
|
||||||
// Stats
|
// Stats
|
||||||
using ArtistContainer = Database::RangeResults<Database::ArtistId>;
|
using ArtistContainer = Database::RangeResults<Database::ArtistId>;
|
||||||
using ReleaseContainer = Database::RangeResults<Database::ReleaseId>;
|
using ReleaseContainer = Database::RangeResults<Database::ReleaseId>;
|
||||||
using TrackContainer = Database::RangeResults<Database::TrackId>;
|
using TrackContainer = Database::RangeResults<Database::TrackId>;
|
||||||
// From most recent to oldest
|
// From most recent to oldest
|
||||||
virtual ArtistContainer getRecentArtists(Database::UserId userId,
|
virtual ArtistContainer getRecentArtists(Database::UserId userId,
|
||||||
const std::vector<Database::ClusterId>& clusterIds,
|
const std::vector<Database::ClusterId>& clusterIds,
|
||||||
std::optional<Database::TrackArtistLinkType> linkType,
|
std::optional<Database::TrackArtistLinkType> linkType,
|
||||||
Database::Range range) = 0;
|
Database::Range range) = 0;
|
||||||
|
|
||||||
virtual ReleaseContainer getRecentReleases(Database::UserId userId,
|
virtual ReleaseContainer getRecentReleases(Database::UserId userId,
|
||||||
const std::vector<Database::ClusterId>& clusterIds,
|
const std::vector<Database::ClusterId>& clusterIds,
|
||||||
Database::Range range) = 0;
|
Database::Range range) = 0;
|
||||||
|
|
||||||
virtual TrackContainer getRecentTracks(Database::UserId userId,
|
virtual TrackContainer getRecentTracks(Database::UserId userId,
|
||||||
const std::vector<Database::ClusterId>& clusterIds,
|
const std::vector<Database::ClusterId>& clusterIds,
|
||||||
Database::Range range) = 0;
|
Database::Range range) = 0;
|
||||||
|
|
||||||
// Top
|
// Top
|
||||||
virtual ArtistContainer getTopArtists(Database::UserId userId,
|
virtual ArtistContainer getTopArtists(Database::UserId userId,
|
||||||
const std::vector<Database::ClusterId>& clusterIds,
|
const std::vector<Database::ClusterId>& clusterIds,
|
||||||
std::optional<Database::TrackArtistLinkType> linkType,
|
std::optional<Database::TrackArtistLinkType> linkType,
|
||||||
Database::Range) = 0;
|
Database::Range) = 0;
|
||||||
|
|
||||||
virtual ReleaseContainer getTopReleases(Database::UserId userId,
|
virtual ReleaseContainer getTopReleases(Database::UserId userId,
|
||||||
const std::vector<Database::ClusterId>& clusterIds,
|
const std::vector<Database::ClusterId>& clusterIds,
|
||||||
Database::Range range) = 0;
|
Database::Range range) = 0;
|
||||||
|
|
||||||
virtual TrackContainer getTopTracks(Database::UserId userId,
|
virtual TrackContainer getTopTracks(Database::UserId userId,
|
||||||
const std::vector<Database::ClusterId>& clusterIds,
|
const std::vector<Database::ClusterId>& clusterIds,
|
||||||
Database::Range range) = 0;
|
Database::Range range) = 0;
|
||||||
|
|
||||||
// Star
|
// Star
|
||||||
virtual void star(Database::UserId userId, Database::ArtistId artistId) = 0;
|
virtual void star(Database::UserId userId, Database::ArtistId artistId) = 0;
|
||||||
virtual void unstar(Database::UserId userId, Database::ArtistId artistId) = 0;
|
virtual void unstar(Database::UserId userId, Database::ArtistId artistId) = 0;
|
||||||
virtual bool isStarred(Database::UserId userId, Database::ArtistId artistId) = 0;
|
virtual bool isStarred(Database::UserId userId, Database::ArtistId artistId) = 0;
|
||||||
virtual ArtistContainer getStarredArtists(Database::UserId userId,
|
virtual Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::ArtistId artistId) = 0;
|
||||||
const std::vector<Database::ClusterId>& clusterIds,
|
virtual ArtistContainer getStarredArtists(Database::UserId userId,
|
||||||
std::optional<Database::TrackArtistLinkType> linkType,
|
const std::vector<Database::ClusterId>& clusterIds,
|
||||||
Database::ArtistSortMethod sortMethod,
|
std::optional<Database::TrackArtistLinkType> linkType,
|
||||||
Database::Range range) = 0;
|
Database::ArtistSortMethod sortMethod,
|
||||||
|
Database::Range range) = 0;
|
||||||
|
|
||||||
virtual void star(Database::UserId userId, Database::ReleaseId releaseId) = 0;
|
virtual void star(Database::UserId userId, Database::ReleaseId releaseId) = 0;
|
||||||
virtual void unstar(Database::UserId userId, Database::ReleaseId releaseId) = 0;
|
virtual void unstar(Database::UserId userId, Database::ReleaseId releaseId) = 0;
|
||||||
virtual bool isStarred(Database::UserId userId, Database::ReleaseId artistId) = 0;
|
virtual bool isStarred(Database::UserId userId, Database::ReleaseId artistId) = 0;
|
||||||
virtual ReleaseContainer getStarredReleases(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) = 0;
|
virtual Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::ReleaseId artistId) = 0;
|
||||||
|
virtual ReleaseContainer getStarredReleases(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) = 0;
|
||||||
|
|
||||||
virtual void star(Database::UserId userId, Database::TrackId trackId) = 0;
|
virtual void star(Database::UserId userId, Database::TrackId trackId) = 0;
|
||||||
virtual void unstar(Database::UserId userId, Database::TrackId trackId) = 0;
|
virtual void unstar(Database::UserId userId, Database::TrackId trackId) = 0;
|
||||||
virtual bool isStarred(Database::UserId userId, Database::TrackId artistId) = 0;
|
virtual bool isStarred(Database::UserId userId, Database::TrackId artistId) = 0;
|
||||||
virtual TrackContainer getStarredTracks(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) = 0;
|
virtual Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::TrackId artistId) = 0;
|
||||||
};
|
virtual TrackContainer getStarredTracks(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
std::unique_ptr<IScrobblingService> createScrobblingService(boost::asio::io_service& ioService, Database::Db& db);
|
std::unique_ptr<IScrobblingService> createScrobblingService(boost::asio::io_service& ioService, Database::Db& db);
|
||||||
|
|
||||||
} // ns Scrobbling
|
} // ns Scrobbling
|
||||||
|
|
||||||
|
|||||||
@@ -26,15 +26,15 @@
|
|||||||
|
|
||||||
namespace Scrobbling
|
namespace Scrobbling
|
||||||
{
|
{
|
||||||
struct Listen
|
struct Listen
|
||||||
{
|
{
|
||||||
Database::UserId userId {};
|
Database::UserId userId{};
|
||||||
Database::TrackId trackId {};
|
Database::TrackId trackId{};
|
||||||
};
|
};
|
||||||
|
|
||||||
struct TimedListen : public Listen
|
struct TimedListen : public Listen
|
||||||
{
|
{
|
||||||
Wt::WDateTime listenedAt;
|
Wt::WDateTime listenedAt;
|
||||||
};
|
};
|
||||||
} // ns Scrobbling
|
} // ns Scrobbling
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,32 @@
|
|||||||
|
|
||||||
add_library(lmssubsonic SHARED
|
add_library(lmssubsonic SHARED
|
||||||
|
impl/entrypoints/AlbumSongLists.cpp
|
||||||
|
impl/entrypoints/Bookmarks.cpp
|
||||||
|
impl/entrypoints/Browsing.cpp
|
||||||
|
impl/entrypoints/MediaAnnotation.cpp
|
||||||
|
impl/entrypoints/MediaLibraryScanning.cpp
|
||||||
|
impl/entrypoints/MediaRetrieval.cpp
|
||||||
|
impl/entrypoints/Playlists.cpp
|
||||||
|
impl/entrypoints/Searching.cpp
|
||||||
|
impl/entrypoints/System.cpp
|
||||||
|
impl/entrypoints/UserManagement.cpp
|
||||||
|
impl/responses/Album.cpp
|
||||||
|
impl/responses/Artist.cpp
|
||||||
|
impl/responses/Bookmark.cpp
|
||||||
|
impl/responses/Contributor.cpp
|
||||||
|
impl/responses/DiscTitle.cpp
|
||||||
|
impl/responses/ItemGenre.cpp
|
||||||
|
impl/responses/Genre.cpp
|
||||||
|
impl/responses/Playlist.cpp
|
||||||
|
impl/responses/ReplayGain.cpp
|
||||||
|
impl/responses/Song.cpp
|
||||||
|
impl/responses/User.cpp
|
||||||
impl/ProtocolVersion.cpp
|
impl/ProtocolVersion.cpp
|
||||||
impl/Scan.cpp
|
impl/ParameterParsing.cpp
|
||||||
impl/Stream.cpp
|
|
||||||
impl/SubsonicId.cpp
|
impl/SubsonicId.cpp
|
||||||
impl/SubsonicResource.cpp
|
impl/SubsonicResource.cpp
|
||||||
impl/SubsonicResponse.cpp
|
impl/SubsonicResponse.cpp
|
||||||
|
impl/Utils.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(lmssubsonic INTERFACE
|
target_include_directories(lmssubsonic INTERFACE
|
||||||
@@ -13,6 +34,7 @@ target_include_directories(lmssubsonic INTERFACE
|
|||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(lmssubsonic PRIVATE
|
target_include_directories(lmssubsonic PRIVATE
|
||||||
|
impl
|
||||||
include
|
include
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -24,11 +24,11 @@
|
|||||||
|
|
||||||
namespace API::Subsonic
|
namespace API::Subsonic
|
||||||
{
|
{
|
||||||
struct ClientInfo
|
struct ClientInfo
|
||||||
{
|
{
|
||||||
std::string name;
|
std::string name;
|
||||||
std::string user;
|
std::string user;
|
||||||
std::string password;
|
std::string password;
|
||||||
ProtocolVersion version;
|
ProtocolVersion version;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "ParameterParsing.hpp"
|
||||||
|
|
||||||
|
namespace API::Subsonic
|
||||||
|
{
|
||||||
|
bool hasParameter(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
|
||||||
|
{
|
||||||
|
return parameterMap.find(param) != std::cend(parameterMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string decodePasswordIfNeeded(const std::string& password)
|
||||||
|
{
|
||||||
|
if (password.find("enc:") == 0)
|
||||||
|
{
|
||||||
|
auto decodedPassword{ StringUtils::stringFromHex(password.substr(4)) };
|
||||||
|
if (!decodedPassword)
|
||||||
|
return password; // fallback on plain password
|
||||||
|
|
||||||
|
return *decodedPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,10 +16,15 @@
|
|||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <Wt/Http/Request.h>
|
#include <Wt/Http/Request.h>
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
#include "services/database/Types.hpp"
|
#include "services/database/Types.hpp"
|
||||||
#include "utils/String.hpp"
|
#include "utils/String.hpp"
|
||||||
#include "SubsonicResponse.hpp"
|
#include "SubsonicResponse.hpp"
|
||||||
@@ -27,65 +32,57 @@
|
|||||||
namespace API::Subsonic
|
namespace API::Subsonic
|
||||||
{
|
{
|
||||||
|
|
||||||
template<typename T>
|
template<typename T>
|
||||||
std::vector<T>
|
std::vector<T> getMultiParametersAs(const Wt::Http::ParameterMap& parameterMap, const std::string& paramName)
|
||||||
getMultiParametersAs(const Wt::Http::ParameterMap& parameterMap, const std::string& paramName)
|
{
|
||||||
{
|
std::vector<T> res;
|
||||||
std::vector<T> res;
|
|
||||||
|
|
||||||
auto it = parameterMap.find(paramName);
|
auto it = parameterMap.find(paramName);
|
||||||
if (it == parameterMap.end())
|
if (it == parameterMap.end())
|
||||||
return res;
|
return res;
|
||||||
|
|
||||||
for (const std::string& param : it->second)
|
for (const std::string& param : it->second)
|
||||||
{
|
{
|
||||||
auto value {StringUtils::readAs<T>(param)};
|
auto value{ StringUtils::readAs<T>(param) };
|
||||||
if (value)
|
if (value)
|
||||||
res.emplace_back(std::move(*value));
|
res.emplace_back(std::move(*value));
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
template<typename T>
|
template<typename T>
|
||||||
std::vector<T>
|
std::vector<T> getMandatoryMultiParametersAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
|
||||||
getMandatoryMultiParametersAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
|
{
|
||||||
{
|
std::vector<T> res{ getMultiParametersAs<T>(parameterMap, param) };
|
||||||
std::vector<T> res {getMultiParametersAs<T>(parameterMap, param)};
|
if (res.empty())
|
||||||
if (res.empty())
|
throw RequiredParameterMissingError{ param };
|
||||||
throw RequiredParameterMissingError {param};
|
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
template<typename T>
|
template<typename T>
|
||||||
std::optional<T>
|
std::optional<T> getParameterAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
|
||||||
getParameterAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
|
{
|
||||||
{
|
std::vector<T> params{ getMultiParametersAs<T>(parameterMap, param) };
|
||||||
std::vector<T> params {getMultiParametersAs<T>(parameterMap, param)};
|
|
||||||
|
|
||||||
if (params.size() != 1)
|
if (params.size() != 1)
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
|
|
||||||
return T { std::move(params.front()) };
|
return T{ std::move(params.front()) };
|
||||||
}
|
}
|
||||||
|
|
||||||
template<typename T>
|
template<typename T>
|
||||||
T
|
T getMandatoryParameterAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
|
||||||
getMandatoryParameterAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
|
{
|
||||||
{
|
auto res{ getParameterAs<T>(parameterMap, param) };
|
||||||
auto res {getParameterAs<T>(parameterMap, param)};
|
if (!res)
|
||||||
if (!res)
|
throw RequiredParameterMissingError{ param };
|
||||||
throw RequiredParameterMissingError {param};
|
|
||||||
|
|
||||||
return *res;
|
return *res;
|
||||||
}
|
}
|
||||||
|
|
||||||
inline
|
bool hasParameter(const Wt::Http::ParameterMap& parameterMap, const std::string& param);
|
||||||
bool
|
std::string decodePasswordIfNeeded(const std::string& password);
|
||||||
hasParameter(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
|
|
||||||
{
|
|
||||||
return parameterMap.find(param) != std::cend(parameterMap);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,36 +21,35 @@
|
|||||||
|
|
||||||
namespace StringUtils
|
namespace StringUtils
|
||||||
{
|
{
|
||||||
template<>
|
template<>
|
||||||
std::optional<API::Subsonic::ProtocolVersion>
|
std::optional<API::Subsonic::ProtocolVersion> readAs(std::string_view str)
|
||||||
readAs(std::string_view str)
|
{
|
||||||
{
|
// Expects "X.Y.Z"
|
||||||
// Expects "X.Y.Z"
|
const auto numbers{ StringUtils::splitString(str, ".") };
|
||||||
const auto numbers {StringUtils::splitString(str, ".")};
|
if (numbers.size() < 2 || numbers.size() > 3)
|
||||||
if (numbers.size() < 2 || numbers.size() > 3)
|
return std::nullopt;
|
||||||
return std::nullopt;
|
|
||||||
|
|
||||||
API::Subsonic::ProtocolVersion version;
|
API::Subsonic::ProtocolVersion version;
|
||||||
|
|
||||||
auto number {StringUtils::readAs<unsigned>(numbers[0])};
|
auto number{ StringUtils::readAs<unsigned>(numbers[0]) };
|
||||||
if (!number)
|
if (!number)
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
version.major = *number;
|
version.major = *number;
|
||||||
|
|
||||||
number = {StringUtils::readAs<unsigned>(numbers[1])};
|
number = { StringUtils::readAs<unsigned>(numbers[1]) };
|
||||||
if (!number)
|
if (!number)
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
version.minor = *number;
|
version.minor = *number;
|
||||||
|
|
||||||
if (numbers.size() == 3)
|
if (numbers.size() == 3)
|
||||||
{
|
{
|
||||||
number = {StringUtils::readAs<unsigned>(numbers[2])};
|
number = { StringUtils::readAs<unsigned>(numbers[2]) };
|
||||||
if (!number)
|
if (!number)
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
version.patch = *number;
|
version.patch = *number;
|
||||||
}
|
}
|
||||||
|
|
||||||
return version;
|
return version;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,18 +23,20 @@
|
|||||||
|
|
||||||
namespace API::Subsonic
|
namespace API::Subsonic
|
||||||
{
|
{
|
||||||
struct ProtocolVersion
|
struct ProtocolVersion
|
||||||
{
|
{
|
||||||
unsigned major {};
|
unsigned major{};
|
||||||
unsigned minor {};
|
unsigned minor{};
|
||||||
unsigned patch {};
|
unsigned patch{};
|
||||||
};
|
};
|
||||||
|
|
||||||
static inline constexpr ProtocolVersion defaultServerProtocolVersion {1, 16, 0};
|
static inline constexpr ProtocolVersion defaultServerProtocolVersion{ 1, 16, 0 };
|
||||||
|
static inline constexpr std::string_view serverVersion{ "1" };
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace StringUtils
|
namespace StringUtils
|
||||||
{
|
{
|
||||||
template<> std::optional<API::Subsonic::ProtocolVersion> readAs(std::string_view str);
|
template<>
|
||||||
|
std::optional<API::Subsonic::ProtocolVersion> readAs(std::string_view str);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,18 +29,18 @@
|
|||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
{
|
{
|
||||||
class Session;
|
class Session;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace API::Subsonic
|
namespace API::Subsonic
|
||||||
{
|
{
|
||||||
struct RequestContext
|
struct RequestContext
|
||||||
{
|
{
|
||||||
const Wt::Http::ParameterMap& parameters;
|
const Wt::Http::ParameterMap& parameters;
|
||||||
Database::Session& dbSession;
|
Database::Session& dbSession;
|
||||||
Database::UserId userId;
|
Database::UserId userId;
|
||||||
ClientInfo clientInfo;
|
ClientInfo clientInfo;
|
||||||
ProtocolVersion serverProtocolVersion;
|
ProtocolVersion serverProtocolVersion;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,72 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2020 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "Scan.hpp"
|
|
||||||
|
|
||||||
#include "services/scanner/IScannerService.hpp"
|
|
||||||
#include "utils/Service.hpp"
|
|
||||||
|
|
||||||
namespace API::Subsonic::Scan
|
|
||||||
{
|
|
||||||
using namespace Scanner;
|
|
||||||
|
|
||||||
static
|
|
||||||
Response::Node
|
|
||||||
createStatusResponseNode()
|
|
||||||
{
|
|
||||||
Response::Node statusResponse;
|
|
||||||
|
|
||||||
const IScannerService::Status scanStatus {Service<IScannerService>::get()->getStatus()};
|
|
||||||
|
|
||||||
statusResponse.setAttribute("scanning", scanStatus.currentState == IScannerService::State::InProgress);
|
|
||||||
if (scanStatus.currentState == IScannerService::State::InProgress)
|
|
||||||
{
|
|
||||||
std::size_t count{};
|
|
||||||
|
|
||||||
if (scanStatus.currentScanStepStats && scanStatus.currentScanStepStats->currentStep == ScanStep::ScanningFiles)
|
|
||||||
count = scanStatus.currentScanStepStats->processedElems;
|
|
||||||
|
|
||||||
statusResponse.setAttribute("count", count);
|
|
||||||
}
|
|
||||||
|
|
||||||
return statusResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Response
|
|
||||||
handleGetScanStatus(RequestContext& context)
|
|
||||||
{
|
|
||||||
Response response {Response::createOkResponse(context.serverProtocolVersion)};
|
|
||||||
response.addNode("scanStatus", createStatusResponseNode());
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
Response
|
|
||||||
handleStartScan(RequestContext& context)
|
|
||||||
{
|
|
||||||
Service<IScannerService>::get()->requestImmediateScan(false);
|
|
||||||
|
|
||||||
Response response {Response::createOkResponse(context.serverProtocolVersion)};
|
|
||||||
response.addNode("scanStatus", createStatusResponseNode());
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2020 Emeric Poupon
|
|
||||||
*
|
|
||||||
* This file is part of LMS.
|
|
||||||
*
|
|
||||||
* LMS is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* LMS is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "Stream.hpp"
|
|
||||||
|
|
||||||
#include "av/TranscodeParameters.hpp"
|
|
||||||
#include "av/TranscodeResourceHandlerCreator.hpp"
|
|
||||||
#include "av/Types.hpp"
|
|
||||||
#include "services/database/Session.hpp"
|
|
||||||
#include "services/database/Track.hpp"
|
|
||||||
#include "services/database/User.hpp"
|
|
||||||
#include "utils/IResourceHandler.hpp"
|
|
||||||
#include "utils/Logger.hpp"
|
|
||||||
#include "utils/FileResourceHandlerCreator.hpp"
|
|
||||||
#include "utils/Utils.hpp"
|
|
||||||
#include "ParameterParsing.hpp"
|
|
||||||
#include "SubsonicId.hpp"
|
|
||||||
|
|
||||||
using namespace Database;
|
|
||||||
|
|
||||||
namespace API::Subsonic::Stream
|
|
||||||
{
|
|
||||||
|
|
||||||
static
|
|
||||||
Av::Format
|
|
||||||
userTranscodeFormatToAvFormat(AudioFormat format)
|
|
||||||
{
|
|
||||||
switch (format)
|
|
||||||
{
|
|
||||||
case AudioFormat::MP3: return Av::Format::MP3;
|
|
||||||
case AudioFormat::OGG_OPUS: return Av::Format::OGG_OPUS;
|
|
||||||
case AudioFormat::MATROSKA_OPUS: return Av::Format::MATROSKA_OPUS;
|
|
||||||
case AudioFormat::OGG_VORBIS: return Av::Format::OGG_VORBIS;
|
|
||||||
case AudioFormat::WEBM_VORBIS: return Av::Format::WEBM_VORBIS;
|
|
||||||
default: return Av::Format::OGG_OPUS;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct StreamParameters
|
|
||||||
{
|
|
||||||
Av::InputFileParameters inputFileParameters;
|
|
||||||
std::optional<Av::TranscodeParameters> transcodeParameters;
|
|
||||||
bool estimateContentLength {};
|
|
||||||
};
|
|
||||||
|
|
||||||
static
|
|
||||||
StreamParameters
|
|
||||||
getStreamParameters(RequestContext& context)
|
|
||||||
{
|
|
||||||
// Mandatory params
|
|
||||||
const TrackId id {getMandatoryParameterAs<TrackId>(context.parameters, "id")};
|
|
||||||
|
|
||||||
// Optional params
|
|
||||||
std::optional<std::size_t> maxBitRate {getParameterAs<std::size_t>(context.parameters, "maxBitRate")};
|
|
||||||
std::optional<std::string> format {getParameterAs<std::string>(context.parameters, "format")};
|
|
||||||
bool estimateContentLength {getParameterAs<bool>(context.parameters, "estimateContentLength").value_or(false)};
|
|
||||||
|
|
||||||
StreamParameters parameters;
|
|
||||||
|
|
||||||
parameters.estimateContentLength = estimateContentLength;
|
|
||||||
|
|
||||||
auto transaction {context.dbSession.createSharedTransaction()};
|
|
||||||
|
|
||||||
{
|
|
||||||
auto track {Track::find(context.dbSession, id)};
|
|
||||||
if (!track)
|
|
||||||
throw RequestedDataNotFoundError {};
|
|
||||||
|
|
||||||
parameters.inputFileParameters.trackPath = track->getPath();
|
|
||||||
parameters.inputFileParameters.duration = track->getDuration();
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
const User::pointer user {User::find(context.dbSession, context.userId)};
|
|
||||||
if (!user)
|
|
||||||
throw UserNotAuthorizedError {};
|
|
||||||
|
|
||||||
// format = "raw" => no transcode. Other format values will be ignored
|
|
||||||
const bool transcode {(!format || (*format != "raw")) && user->getSubsonicTranscodeEnable()};
|
|
||||||
if (transcode)
|
|
||||||
{
|
|
||||||
std::size_t bitRate {user->getSubsonicTranscodeBitrate() / 1000};
|
|
||||||
|
|
||||||
// "If set to zero, no limit is imposed"
|
|
||||||
if (maxBitRate && *maxBitRate != 0)
|
|
||||||
bitRate = Utils::clamp(*maxBitRate, std::size_t {48}, bitRate);
|
|
||||||
|
|
||||||
Av::TranscodeParameters transcodeParameters;
|
|
||||||
|
|
||||||
transcodeParameters.bitrate = bitRate * 1000;
|
|
||||||
transcodeParameters.format = userTranscodeFormatToAvFormat(user->getSubsonicTranscodeFormat());
|
|
||||||
transcodeParameters.stripMetadata = false; // We want clients to use metadata (offline use, replay gain, etc.)
|
|
||||||
|
|
||||||
parameters.transcodeParameters = std::move(transcodeParameters);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return parameters;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
handleDownload(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response)
|
|
||||||
{
|
|
||||||
std::shared_ptr<IResourceHandler> resourceHandler;
|
|
||||||
|
|
||||||
Wt::Http::ResponseContinuation* continuation {request.continuation()};
|
|
||||||
if (!continuation)
|
|
||||||
{
|
|
||||||
// Mandatory params
|
|
||||||
Database::TrackId id {getMandatoryParameterAs<Database::TrackId>(context.parameters, "id")};
|
|
||||||
|
|
||||||
std::filesystem::path trackPath;
|
|
||||||
{
|
|
||||||
auto transaction {context.dbSession.createSharedTransaction()};
|
|
||||||
|
|
||||||
auto track {Track::find(context.dbSession, id)};
|
|
||||||
if (!track)
|
|
||||||
throw RequestedDataNotFoundError {};
|
|
||||||
|
|
||||||
trackPath = track->getPath();
|
|
||||||
}
|
|
||||||
|
|
||||||
resourceHandler = createFileResourceHandler(trackPath);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
resourceHandler = Wt::cpp17::any_cast<std::shared_ptr<IResourceHandler>>(continuation->data());
|
|
||||||
}
|
|
||||||
|
|
||||||
continuation = resourceHandler->processRequest(request, response);
|
|
||||||
if (continuation)
|
|
||||||
continuation->setData(resourceHandler);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
handleStream(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response)
|
|
||||||
{
|
|
||||||
std::shared_ptr<IResourceHandler> resourceHandler;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Wt::Http::ResponseContinuation* continuation = request.continuation();
|
|
||||||
if (!continuation)
|
|
||||||
{
|
|
||||||
StreamParameters streamParameters {getStreamParameters(context)};
|
|
||||||
if (streamParameters.transcodeParameters)
|
|
||||||
resourceHandler = Av::createTranscodeResourceHandler(streamParameters.inputFileParameters, *streamParameters.transcodeParameters, streamParameters.estimateContentLength);
|
|
||||||
else
|
|
||||||
resourceHandler = createFileResourceHandler(streamParameters.inputFileParameters.trackPath);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
resourceHandler = Wt::cpp17::any_cast<std::shared_ptr<IResourceHandler>>(continuation->data());
|
|
||||||
}
|
|
||||||
|
|
||||||
continuation = resourceHandler->processRequest(request, response);
|
|
||||||
if (continuation)
|
|
||||||
continuation->setData(resourceHandler);
|
|
||||||
}
|
|
||||||
catch (const Av::Exception& e)
|
|
||||||
{
|
|
||||||
LMS_LOG(API_SUBSONIC, ERROR) << "Caught Av exception: " << e.what();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace API::Subsonic::Stream
|
|
||||||
@@ -26,114 +26,104 @@
|
|||||||
|
|
||||||
namespace API::Subsonic
|
namespace API::Subsonic
|
||||||
{
|
{
|
||||||
std::string
|
std::string idToString(Database::ArtistId id)
|
||||||
idToString(Database::ArtistId id)
|
{
|
||||||
{
|
return "ar-" + id.toString();
|
||||||
return "ar-" + id.toString();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
std::string
|
std::string idToString(Database::ReleaseId id)
|
||||||
idToString(Database::ReleaseId id)
|
{
|
||||||
{
|
return "al-" + id.toString();
|
||||||
return "al-" + id.toString();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
std::string
|
std::string idToString(RootId)
|
||||||
idToString(RootId)
|
{
|
||||||
{
|
return "root";
|
||||||
return "root";
|
}
|
||||||
}
|
|
||||||
|
|
||||||
std::string
|
std::string idToString(Database::TrackId id)
|
||||||
idToString(Database::TrackId id)
|
{
|
||||||
{
|
return "tr-" + id.toString();
|
||||||
return "tr-" + id.toString();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
std::string
|
std::string idToString(Database::TrackListId id)
|
||||||
idToString(Database::TrackListId id)
|
{
|
||||||
{
|
return "pl-" + id.toString();
|
||||||
return "pl-" + id.toString();
|
}
|
||||||
}
|
|
||||||
} // namespace API::Subsonic
|
} // namespace API::Subsonic
|
||||||
|
|
||||||
namespace StringUtils
|
namespace StringUtils
|
||||||
{
|
{
|
||||||
template<>
|
template<>
|
||||||
std::optional<Database::ArtistId>
|
std::optional<Database::ArtistId> readAs(std::string_view str)
|
||||||
readAs(std::string_view str)
|
{
|
||||||
{
|
std::vector<std::string_view> values{ StringUtils::splitString(str, "-") };
|
||||||
std::vector<std::string_view> values {StringUtils::splitString(str, "-")};
|
if (values.size() != 2)
|
||||||
if (values.size() != 2)
|
return std::nullopt;
|
||||||
return std::nullopt;
|
|
||||||
|
|
||||||
if (values[0] != "ar")
|
if (values[0] != "ar")
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
|
|
||||||
if (const auto value {StringUtils::readAs<Database::ArtistId::ValueType>(values[1])})
|
if (const auto value{ StringUtils::readAs<Database::ArtistId::ValueType>(values[1]) })
|
||||||
return Database::ArtistId {*value};
|
return Database::ArtistId{ *value };
|
||||||
|
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
template<>
|
template<>
|
||||||
std::optional<Database::ReleaseId>
|
std::optional<Database::ReleaseId> readAs(std::string_view str)
|
||||||
readAs(std::string_view str)
|
{
|
||||||
{
|
std::vector<std::string_view> values{ StringUtils::splitString(str, "-") };
|
||||||
std::vector<std::string_view> values {StringUtils::splitString(str, "-")};
|
if (values.size() != 2)
|
||||||
if (values.size() != 2)
|
return std::nullopt;
|
||||||
return std::nullopt;
|
|
||||||
|
|
||||||
if (values[0] != "al")
|
if (values[0] != "al")
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
|
|
||||||
if (const auto value {StringUtils::readAs<Database::ReleaseId::ValueType>(values[1])})
|
if (const auto value{ StringUtils::readAs<Database::ReleaseId::ValueType>(values[1]) })
|
||||||
return Database::ReleaseId {*value};
|
return Database::ReleaseId{ *value };
|
||||||
|
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
template<>
|
template<>
|
||||||
std::optional<API::Subsonic::RootId>
|
std::optional<API::Subsonic::RootId> readAs(std::string_view str)
|
||||||
readAs(std::string_view str)
|
{
|
||||||
{
|
if (str == "root")
|
||||||
if (str == "root")
|
return API::Subsonic::RootId{};
|
||||||
return API::Subsonic::RootId {};
|
|
||||||
|
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
template<>
|
template<>
|
||||||
std::optional<Database::TrackId>
|
std::optional<Database::TrackId> readAs(std::string_view str)
|
||||||
readAs(std::string_view str)
|
{
|
||||||
{
|
std::vector<std::string_view> values{ StringUtils::splitString(str, "-") };
|
||||||
std::vector<std::string_view> values {StringUtils::splitString(str, "-")};
|
if (values.size() != 2)
|
||||||
if (values.size() != 2)
|
return std::nullopt;
|
||||||
return std::nullopt;
|
|
||||||
|
|
||||||
if (values[0] != "tr")
|
if (values[0] != "tr")
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
|
|
||||||
if (const auto value {StringUtils::readAs<Database::TrackId::ValueType>(values[1])})
|
if (const auto value{ StringUtils::readAs<Database::TrackId::ValueType>(values[1]) })
|
||||||
return Database::TrackId {*value};
|
return Database::TrackId{ *value };
|
||||||
|
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
template<>
|
template<>
|
||||||
std::optional<Database::TrackListId>
|
std::optional<Database::TrackListId> readAs(std::string_view str)
|
||||||
readAs(std::string_view str)
|
{
|
||||||
{
|
std::vector<std::string_view> values{ StringUtils::splitString(str, "-") };
|
||||||
std::vector<std::string_view> values {StringUtils::splitString(str, "-")};
|
if (values.size() != 2)
|
||||||
if (values.size() != 2)
|
return std::nullopt;
|
||||||
return std::nullopt;
|
|
||||||
|
|
||||||
if (values[0] != "pl")
|
if (values[0] != "pl")
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
|
|
||||||
if (const auto value {StringUtils::readAs<Database::TrackListId::ValueType>(values[1])})
|
if (const auto value{ StringUtils::readAs<Database::TrackListId::ValueType>(values[1]) })
|
||||||
return Database::TrackListId {*value};
|
return Database::TrackListId{ *value };
|
||||||
|
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,36 +27,31 @@
|
|||||||
|
|
||||||
namespace API::Subsonic
|
namespace API::Subsonic
|
||||||
{
|
{
|
||||||
struct RootId {};
|
struct RootId {};
|
||||||
|
|
||||||
std::string idToString(Database::ArtistId id);
|
std::string idToString(Database::ArtistId id);
|
||||||
std::string idToString(Database::ReleaseId id);
|
std::string idToString(Database::ReleaseId id);
|
||||||
std::string idToString(Database::TrackId id);
|
std::string idToString(Database::TrackId id);
|
||||||
std::string idToString(Database::TrackListId id);
|
std::string idToString(Database::TrackListId id);
|
||||||
std::string idToString(RootId);
|
std::string idToString(RootId);
|
||||||
} // namespace API::Subsonic
|
} // namespace API::Subsonic
|
||||||
|
|
||||||
// Used to parse parameters
|
// Used to parse parameters
|
||||||
namespace StringUtils
|
namespace StringUtils
|
||||||
{
|
{
|
||||||
template<>
|
template<>
|
||||||
std::optional<API::Subsonic::RootId>
|
std::optional<API::Subsonic::RootId> readAs(std::string_view str);
|
||||||
readAs(std::string_view str);
|
|
||||||
|
|
||||||
template<>
|
template<>
|
||||||
std::optional<Database::ArtistId>
|
std::optional<Database::ArtistId> readAs(std::string_view str);
|
||||||
readAs(std::string_view str);
|
|
||||||
|
|
||||||
template<>
|
template<>
|
||||||
std::optional<Database::ReleaseId>
|
std::optional<Database::ReleaseId> readAs(std::string_view str);
|
||||||
readAs(std::string_view str);
|
|
||||||
|
|
||||||
template<>
|
template<>
|
||||||
std::optional<Database::TrackId>
|
std::optional<Database::TrackId> readAs(std::string_view str);
|
||||||
readAs(std::string_view str);
|
|
||||||
|
|
||||||
template<>
|
template<>
|
||||||
std::optional<Database::TrackListId>
|
std::optional<Database::TrackListId> readAs(std::string_view str);
|
||||||
readAs(std::string_view str);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -30,28 +30,28 @@
|
|||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
{
|
{
|
||||||
class Db;
|
class Db;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace API::Subsonic
|
namespace API::Subsonic
|
||||||
{
|
{
|
||||||
|
|
||||||
class SubsonicResource final : public Wt::WResource
|
class SubsonicResource final : public Wt::WResource
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
SubsonicResource(Database::Db& db);
|
SubsonicResource(Database::Db& db);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void handleRequest(const Wt::Http::Request &request, Wt::Http::Response &response) override;
|
void handleRequest(const Wt::Http::Request &request, Wt::Http::Response &response) override;
|
||||||
ProtocolVersion getServerProtocolVersion(const std::string& clientName) const;
|
ProtocolVersion getServerProtocolVersion(const std::string& clientName) const;
|
||||||
|
|
||||||
static void checkProtocolVersion(ProtocolVersion client, ProtocolVersion server);
|
static void checkProtocolVersion(ProtocolVersion client, ProtocolVersion server);
|
||||||
ClientInfo getClientInfo(const Wt::Http::ParameterMap& parameters);
|
ClientInfo getClientInfo(const Wt::Http::ParameterMap& parameters);
|
||||||
RequestContext buildRequestContext(const Wt::Http::Request& request);
|
RequestContext buildRequestContext(const Wt::Http::Request& request);
|
||||||
Database::UserId authenticateUser(const Wt::Http::Request& request, const ClientInfo& clientInfo);
|
Database::UserId authenticateUser(const Wt::Http::Request& request, const ClientInfo& clientInfo);
|
||||||
|
|
||||||
const std::unordered_map<std::string, ProtocolVersion> _serverProtocolVersionsByClient;
|
const std::unordered_map<std::string, ProtocolVersion> _serverProtocolVersionsByClient;
|
||||||
Database::Db& _db;
|
Database::Db& _db;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
#include "SubsonicResponse.hpp"
|
#include "SubsonicResponse.hpp"
|
||||||
|
|
||||||
|
#include <cassert>
|
||||||
#include <Wt/Json/Array.h>
|
#include <Wt/Json/Array.h>
|
||||||
#include <Wt/Json/Object.h>
|
#include <Wt/Json/Object.h>
|
||||||
#include <Wt/Json/Value.h>
|
#include <Wt/Json/Value.h>
|
||||||
@@ -33,250 +34,270 @@
|
|||||||
|
|
||||||
namespace API::Subsonic
|
namespace API::Subsonic
|
||||||
{
|
{
|
||||||
|
std::string_view ResponseFormatToMimeType(ResponseFormat format)
|
||||||
|
{
|
||||||
|
switch (format)
|
||||||
|
{
|
||||||
|
case ResponseFormat::xml: return "text/xml";
|
||||||
|
case ResponseFormat::json: return "application/json";
|
||||||
|
}
|
||||||
|
|
||||||
std::string
|
return "";
|
||||||
ResponseFormatToMimeType(ResponseFormat format)
|
}
|
||||||
{
|
|
||||||
switch (format)
|
|
||||||
{
|
|
||||||
case ResponseFormat::xml: return "text/xml";
|
|
||||||
case ResponseFormat::json: return "application/json";
|
|
||||||
}
|
|
||||||
|
|
||||||
return "";
|
void Response::Node::setValue(std::string_view value)
|
||||||
}
|
{
|
||||||
|
assert(_children.empty() && _childrenArrays.empty() && _childrenValues.empty());
|
||||||
|
_value = std::string{ value };
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void Response::Node::setValue(long long value)
|
||||||
Response::Node::setValue(std::string_view value)
|
{
|
||||||
{
|
assert(_children.empty() && _childrenArrays.empty() && _childrenValues.empty());
|
||||||
if (!_children.empty() || !_childrenArrays.empty())
|
_value = value;
|
||||||
throw LmsException {"Node already has children"};
|
}
|
||||||
|
|
||||||
_value = std::string {value};
|
void Response::Node::setAttribute(std::string_view key, std::string_view value)
|
||||||
}
|
{
|
||||||
|
_attributes[std::string{ key }] = std::string{ value };
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void Response::Node::addChild(const std::string& key, Node node)
|
||||||
Response::Node::setValue(long long value)
|
{
|
||||||
{
|
assert(!_value);
|
||||||
if (!_children.empty() || !_childrenArrays.empty())
|
_children[key].emplace_back(std::move(node));
|
||||||
throw LmsException {"Node already has children"};
|
}
|
||||||
|
|
||||||
_value = value;
|
void Response::Node::createEmptyArrayChild(std::string_view key)
|
||||||
}
|
{
|
||||||
|
assert(!_value);
|
||||||
|
_childrenArrays.emplace(key, std::vector<Node>{});
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void Response::Node::addArrayChild(std::string_view key, Node node)
|
||||||
Response::Node::setAttribute(std::string_view key, std::string_view value)
|
{
|
||||||
{
|
assert(!_value);
|
||||||
_attributes[std::string {key}] = std::string {value};
|
_childrenArrays[std::string{ key }].emplace_back(std::move(node));
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void Response::Node::createEmptyArrayValue(std::string_view key)
|
||||||
Response::Node::addChild(const std::string& key, Node node)
|
{
|
||||||
{
|
assert (!_value);
|
||||||
if (_value)
|
_childrenValues.emplace(key, ValuesType{});
|
||||||
throw LmsException {"Node already has a value"};
|
}
|
||||||
|
|
||||||
_children[key].emplace_back(std::move(node));
|
void Response::Node::addArrayValue(std::string_view key, std::string_view value)
|
||||||
}
|
{
|
||||||
|
assert(!_value);
|
||||||
|
auto& values{ _childrenValues[std::string{ key }] };
|
||||||
|
values.push_back(std::string{ value });
|
||||||
|
assert(std::all_of(std::cbegin(values) + 1, std::cend(values), [&](const ValueType& value) {return value.index() == values.front().index();}));
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void Response::Node::addArrayValue(std::string_view key, long long value)
|
||||||
Response::Node::addArrayChild(const std::string& key, Node node)
|
{
|
||||||
{
|
assert(!_value);
|
||||||
if (_value)
|
auto& values {_childrenValues[std::string{ key }]};
|
||||||
throw LmsException {"Node already has a value"};
|
values.push_back(value);
|
||||||
|
assert(std::all_of(std::cbegin(values) + 1, std::cend(values), [&](const ValueType& value) {return value.index() == values.front().index();}));
|
||||||
|
}
|
||||||
|
|
||||||
_childrenArrays[key].emplace_back(std::move(node));
|
Response::Node& Response::Node::createChild(const std::string& key)
|
||||||
}
|
{
|
||||||
|
_children[key].emplace_back();
|
||||||
|
return _children[key].back();
|
||||||
|
}
|
||||||
|
|
||||||
|
Response::Node& Response::Node::createArrayChild(const std::string& key)
|
||||||
|
{
|
||||||
|
_childrenArrays[key].emplace_back();
|
||||||
|
return _childrenArrays[key].back();
|
||||||
|
}
|
||||||
|
|
||||||
Response::Node&
|
void Response::Node::setVersionAttribute(ProtocolVersion protocolVersion)
|
||||||
Response::Node::createChild(const std::string& key)
|
{
|
||||||
{
|
setAttribute("version", std::to_string(protocolVersion.major) + "." + std::to_string(protocolVersion.minor) + "." + std::to_string(protocolVersion.patch));
|
||||||
_children[key].emplace_back();
|
}
|
||||||
return _children[key].back();
|
|
||||||
}
|
|
||||||
|
|
||||||
Response::Node&
|
Response Response::createOkResponse(ProtocolVersion protocolVersion)
|
||||||
Response::Node::createArrayChild(const std::string& key)
|
{
|
||||||
{
|
Response response;
|
||||||
_childrenArrays[key].emplace_back();
|
Node& responseNode{ response._root.createChild("subsonic-response") };
|
||||||
return _childrenArrays[key].back();
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
responseNode.setAttribute("status", "ok");
|
||||||
Response::Node::setVersionAttribute(ProtocolVersion protocolVersion)
|
responseNode.setVersionAttribute(protocolVersion);
|
||||||
{
|
|
||||||
setAttribute("version", std::to_string(protocolVersion.major) + "." + std::to_string(protocolVersion.minor) + "." + std::to_string(protocolVersion.patch));
|
|
||||||
}
|
|
||||||
|
|
||||||
Response
|
// OpenSubsonic mandatory fields
|
||||||
Response::createOkResponse(ProtocolVersion protocolVersion)
|
responseNode.setAttribute("type", "lms");
|
||||||
{
|
responseNode.setAttribute("serverVersion", serverVersion);
|
||||||
Response response;
|
responseNode.setAttribute("openSubsonic", true);
|
||||||
Node& responseNode {response._root.createChild("subsonic-response")};
|
|
||||||
|
|
||||||
responseNode.setAttribute("status", "ok");
|
return response;
|
||||||
responseNode.setVersionAttribute(protocolVersion);
|
}
|
||||||
responseNode.setAttribute("type", "lms"); // non standard field to ease client hacks
|
|
||||||
|
|
||||||
return response;
|
Response Response::createFailedResponse(ProtocolVersion protocolVersion, const Error& error)
|
||||||
}
|
{
|
||||||
|
Response response;
|
||||||
|
Node& responseNode{ response._root.createChild("subsonic-response") };
|
||||||
|
|
||||||
Response
|
responseNode.setAttribute("status", "failed");
|
||||||
Response::createFailedResponse(ProtocolVersion protocolVersion, const Error& error)
|
responseNode.setVersionAttribute(protocolVersion);
|
||||||
{
|
responseNode.setAttribute("type", "lms"); // non standard field to ease client hacks
|
||||||
Response response;
|
|
||||||
Node& responseNode {response._root.createChild("subsonic-response")};
|
|
||||||
|
|
||||||
responseNode.setAttribute("status", "failed");
|
Node& errorNode{ responseNode.createChild("error") };
|
||||||
responseNode.setVersionAttribute(protocolVersion);
|
errorNode.setAttribute("code", static_cast<int>(error.getCode()));
|
||||||
responseNode.setAttribute("type", "lms"); // non standard field to ease client hacks
|
errorNode.setAttribute("message", error.getMessage());
|
||||||
|
|
||||||
Node& errorNode {responseNode.createChild("error")};
|
return response;
|
||||||
errorNode.setAttribute("code", static_cast<int>(error.getCode()));
|
}
|
||||||
errorNode.setAttribute("message", error.getMessage());
|
|
||||||
|
|
||||||
return response;
|
void Response::addNode(const std::string& key, Node node)
|
||||||
}
|
{
|
||||||
|
return _root._children["subsonic-response"].front().addChild(key, std::move(node));
|
||||||
|
}
|
||||||
|
|
||||||
void
|
Response::Node& Response::createNode(const std::string& key)
|
||||||
Response::addNode(const std::string& key, Node node)
|
{
|
||||||
{
|
return _root._children["subsonic-response"].front().createChild(key);
|
||||||
return _root._children["subsonic-response"].front().addChild(key, std::move(node));
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Response::Node&
|
Response::Node& Response::createArrayNode(const std::string& key)
|
||||||
Response::createNode(const std::string& key)
|
{
|
||||||
{
|
return _root._children["subsonic-response"].front().createArrayChild(key);
|
||||||
return _root._children["subsonic-response"].front().createChild(key);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Response::Node&
|
void Response::write(std::ostream& os, ResponseFormat format)
|
||||||
Response::createArrayNode(const std::string& key)
|
{
|
||||||
{
|
switch (format)
|
||||||
return _root._children["subsonic-response"].front().createArrayChild(key);
|
{
|
||||||
}
|
case ResponseFormat::xml:
|
||||||
|
writeXML(os);
|
||||||
|
break;
|
||||||
|
case ResponseFormat::json:
|
||||||
|
writeJSON(os);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void Response::writeXML(std::ostream& os)
|
||||||
Response::write(std::ostream& os, ResponseFormat format)
|
{
|
||||||
{
|
std::function<boost::property_tree::ptree(const Node&)> nodeToPropertyTree = [&](const Node& node)
|
||||||
switch (format)
|
{
|
||||||
{
|
boost::property_tree::ptree res;
|
||||||
case ResponseFormat::xml:
|
|
||||||
writeXML(os);
|
|
||||||
break;
|
|
||||||
case ResponseFormat::json:
|
|
||||||
writeJSON(os);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
for (auto itAttribute : node._attributes)
|
||||||
Response::writeXML(std::ostream& os)
|
{
|
||||||
{
|
if (std::holds_alternative<std::string>(itAttribute.second))
|
||||||
std::function<boost::property_tree::ptree(const Response::Node&)> nodeToPropertyTree = [&] (const Response::Node& node)
|
res.put("<xmlattr>." + itAttribute.first, std::get<std::string>(itAttribute.second));
|
||||||
{
|
else if (std::holds_alternative<bool>(itAttribute.second))
|
||||||
boost::property_tree::ptree res;
|
res.put("<xmlattr>." + itAttribute.first, std::get<bool>(itAttribute.second));
|
||||||
|
else if (std::holds_alternative<float>(itAttribute.second))
|
||||||
|
res.put("<xmlattr>." + itAttribute.first, std::get<float>(itAttribute.second));
|
||||||
|
else if (std::holds_alternative<long long>(itAttribute.second))
|
||||||
|
res.put("<xmlattr>." + itAttribute.first, std::get<long long>(itAttribute.second));
|
||||||
|
}
|
||||||
|
|
||||||
for (auto itAttribute : node._attributes)
|
auto valueToPropertyTree = [](const Node::ValueType& value)
|
||||||
{
|
{
|
||||||
if (std::holds_alternative<std::string>(itAttribute.second))
|
boost::property_tree::ptree res;
|
||||||
res.put("<xmlattr>." + itAttribute.first, std::get<std::string>(itAttribute.second));
|
std::visit([&](const auto& rawValue)
|
||||||
else if (std::holds_alternative<bool>(itAttribute.second))
|
{
|
||||||
res.put("<xmlattr>." + itAttribute.first, std::get<bool>(itAttribute.second));
|
res.put_value(rawValue);
|
||||||
else if (std::holds_alternative<long long>(itAttribute.second))
|
}, value);
|
||||||
res.put("<xmlattr>." + itAttribute.first, std::get<long long>(itAttribute.second));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node._value)
|
return res;
|
||||||
{
|
};
|
||||||
const auto& value {*node._value};
|
|
||||||
|
|
||||||
if (std::holds_alternative<std::string>(value))
|
if (node._value)
|
||||||
res.put_value(std::get<std::string>(value));
|
{
|
||||||
else if (std::holds_alternative<bool>(value))
|
res = valueToPropertyTree(*node._value);
|
||||||
res.put_value(std::get<bool>(value));
|
}
|
||||||
else if (std::holds_alternative<long long>(value))
|
else
|
||||||
res.put_value(std::get<long long>(value));
|
{
|
||||||
}
|
for (const auto& [key, childNodes] : node._children)
|
||||||
else
|
{
|
||||||
{
|
for (const Node& childNode : childNodes)
|
||||||
for (auto itChildNode : node._children)
|
res.add_child(key, nodeToPropertyTree(childNode));
|
||||||
{
|
}
|
||||||
for (const Response::Node& childNode : itChildNode.second)
|
|
||||||
res.add_child(itChildNode.first, nodeToPropertyTree(childNode));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto itChildArrayNode : node._childrenArrays)
|
for (const auto& [key, childArrayNodes] : node._childrenArrays)
|
||||||
{
|
{
|
||||||
const std::vector<Response::Node>& childArrayNodes {itChildArrayNode.second};
|
for (const Node& childNode : childArrayNodes)
|
||||||
|
res.add_child(key, nodeToPropertyTree(childNode));
|
||||||
|
}
|
||||||
|
|
||||||
for (const Response::Node& childNode : childArrayNodes )
|
for (const auto& [key, childArrayValues] : node._childrenValues)
|
||||||
res.add_child(itChildArrayNode.first, nodeToPropertyTree(childNode));
|
{
|
||||||
}
|
for (const Response::Node::ValueType& value : childArrayValues)
|
||||||
}
|
res.add_child(key, valueToPropertyTree(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
boost::property_tree::ptree root {nodeToPropertyTree(_root)};
|
boost::property_tree::ptree root{ nodeToPropertyTree(_root) };
|
||||||
boost::property_tree::write_xml(os, root);
|
boost::property_tree::write_xml(os, root);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void Response::writeJSON(std::ostream& os)
|
||||||
Response::writeJSON(std::ostream& os)
|
{
|
||||||
{
|
namespace Json = Wt::Json;
|
||||||
namespace Json = Wt::Json;
|
|
||||||
|
|
||||||
std::function<Json::Object(const Response::Node&)> nodeToJsonObject = [&] (const Response::Node& node)
|
std::function<Json::Object(const Response::Node&)> nodeToJsonObject = [&](const Response::Node& node)
|
||||||
{
|
{
|
||||||
Json::Object res;
|
Json::Object res;
|
||||||
|
|
||||||
auto valueToJsonValue {[](const Node::ValueType& value) -> Json::Value
|
auto valueToJsonValue{ [](const Node::ValueType& value) -> Json::Value
|
||||||
{
|
{
|
||||||
if (std::holds_alternative<std::string>(value))
|
Json::Value res;
|
||||||
return Json::Value {std::get<std::string>(value)};
|
std::visit([&](const auto& rawValue)
|
||||||
else if (std::holds_alternative<bool>(value))
|
{
|
||||||
return Json::Value {std::get<bool>(value)};
|
res = Json::Value{ rawValue };
|
||||||
else if (std::holds_alternative<long long>(value))
|
}, value);
|
||||||
return Json::Value {std::get<long long>(value)};
|
return res;
|
||||||
|
} };
|
||||||
|
|
||||||
throw LmsException("Unexpected value type");
|
for (auto itAttribute : node._attributes)
|
||||||
}};
|
res[itAttribute.first] = valueToJsonValue(itAttribute.second);
|
||||||
|
|
||||||
for (auto itAttribute : node._attributes)
|
if (node._value)
|
||||||
res[itAttribute.first] = valueToJsonValue(itAttribute.second);
|
{
|
||||||
|
res["value"] = valueToJsonValue(*node._value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (const auto& [key, childNodes] : node._children)
|
||||||
|
{
|
||||||
|
for (const Response::Node& childNode : childNodes)
|
||||||
|
res[key] = nodeToJsonObject(childNode);
|
||||||
|
}
|
||||||
|
|
||||||
if (node._value)
|
for (const auto& [key, childArrayNodes] : node._childrenArrays)
|
||||||
{
|
{
|
||||||
res["value"] = valueToJsonValue(*node._value);
|
Json::Array array;
|
||||||
}
|
for (const Response::Node& childNode : childArrayNodes)
|
||||||
else
|
array.emplace_back(nodeToJsonObject(childNode));
|
||||||
{
|
|
||||||
for (auto itChildNode : node._children)
|
|
||||||
{
|
|
||||||
for (const Response::Node& childNode : itChildNode.second)
|
|
||||||
res[itChildNode.first] = nodeToJsonObject(childNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto itChildArrayNode : node._childrenArrays)
|
res[key] = std::move(array);
|
||||||
{
|
}
|
||||||
const std::vector<Response::Node>& childArrayNodes {itChildArrayNode .second};
|
|
||||||
|
|
||||||
Json::Array array;
|
for (const auto& [key, childValues] : node._childrenValues)
|
||||||
for (const Response::Node& childNode : childArrayNodes )
|
{
|
||||||
array.emplace_back(nodeToJsonObject(childNode));
|
Json::Array array;
|
||||||
|
for (const Node::ValueType& childValue : childValues)
|
||||||
|
array.emplace_back(valueToJsonValue(childValue));
|
||||||
|
|
||||||
res[itChildArrayNode.first] = std::move(array);
|
res[key] = std::move(array);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
};
|
};
|
||||||
|
|
||||||
Json::Object root {nodeToJsonObject(_root)};
|
Json::Object root{ nodeToJsonObject(_root) };
|
||||||
os << Json::serialize(root);
|
os << Json::serialize(root);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
|||||||
@@ -30,220 +30,231 @@
|
|||||||
namespace API::Subsonic
|
namespace API::Subsonic
|
||||||
{
|
{
|
||||||
|
|
||||||
enum class ResponseFormat
|
enum class ResponseFormat
|
||||||
{
|
{
|
||||||
xml,
|
xml,
|
||||||
json,
|
json,
|
||||||
};
|
};
|
||||||
|
|
||||||
std::string ResponseFormatToMimeType(ResponseFormat format);
|
std::string_view ResponseFormatToMimeType(ResponseFormat format);
|
||||||
|
|
||||||
class Error
|
class Error
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
enum class Code
|
enum class Code
|
||||||
{
|
{
|
||||||
Generic = 0,
|
Generic = 0,
|
||||||
RequiredParameterMissing = 10,
|
RequiredParameterMissing = 10,
|
||||||
ClientMustUpgrade = 20,
|
ClientMustUpgrade = 20,
|
||||||
ServerMustUpgrade = 30,
|
ServerMustUpgrade = 30,
|
||||||
WrongUsernameOrPassword = 40,
|
WrongUsernameOrPassword = 40,
|
||||||
TokenAuthenticationNotSupportedForLDAPUsers = 41,
|
TokenAuthenticationNotSupportedForLDAPUsers = 41,
|
||||||
UserNotAuthorized = 50,
|
UserNotAuthorized = 50,
|
||||||
RequestedDataNotFound = 70,
|
RequestedDataNotFound = 70,
|
||||||
};
|
};
|
||||||
|
|
||||||
Error(Code code) : _code {code} {}
|
Error(Code code) : _code{ code } {}
|
||||||
|
|
||||||
virtual std::string getMessage() const = 0;
|
virtual std::string getMessage() const = 0;
|
||||||
|
|
||||||
Code getCode() const { return _code; }
|
Code getCode() const { return _code; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const Code _code;
|
const Code _code;
|
||||||
};
|
};
|
||||||
|
|
||||||
class GenericError : public Error
|
class GenericError : public Error
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
GenericError() : Error {Code::Generic} {}
|
GenericError() : Error{ Code::Generic } {}
|
||||||
};
|
};
|
||||||
|
|
||||||
class RequiredParameterMissingError : public Error
|
class RequiredParameterMissingError : public Error
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
RequiredParameterMissingError(std::string_view param)
|
RequiredParameterMissingError(std::string_view param)
|
||||||
: Error {Code::RequiredParameterMissing}
|
: Error{ Code::RequiredParameterMissing }
|
||||||
, _param {param}
|
, _param{ param }
|
||||||
{}
|
{}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::string getMessage() const override { return "Required parameter '" + _param + "' is missing."; }
|
std::string getMessage() const override { return "Required parameter '" + _param + "' is missing."; }
|
||||||
std::string _param;
|
std::string _param;
|
||||||
};
|
};
|
||||||
|
|
||||||
class ClientMustUpgradeError : public Error
|
class ClientMustUpgradeError : public Error
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
ClientMustUpgradeError() : Error {Code::ClientMustUpgrade} {}
|
ClientMustUpgradeError() : Error{ Code::ClientMustUpgrade } {}
|
||||||
private:
|
private:
|
||||||
std::string getMessage() const override { return "Incompatible Subsonic REST protocol version. Client must upgrade."; }
|
std::string getMessage() const override { return "Incompatible Subsonic REST protocol version. Client must upgrade."; }
|
||||||
};
|
};
|
||||||
|
|
||||||
class ServerMustUpgradeError : public Error
|
class ServerMustUpgradeError : public Error
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
ServerMustUpgradeError() : Error {Code::ServerMustUpgrade} {}
|
ServerMustUpgradeError() : Error{ Code::ServerMustUpgrade } {}
|
||||||
private:
|
private:
|
||||||
std::string getMessage() const override { return "Incompatible Subsonic REST protocol version. Server must upgrade."; }
|
std::string getMessage() const override { return "Incompatible Subsonic REST protocol version. Server must upgrade."; }
|
||||||
};
|
};
|
||||||
|
|
||||||
class WrongUsernameOrPasswordError : public Error
|
class WrongUsernameOrPasswordError : public Error
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
WrongUsernameOrPasswordError() : Error {Code::WrongUsernameOrPassword} {}
|
WrongUsernameOrPasswordError() : Error{ Code::WrongUsernameOrPassword } {}
|
||||||
private:
|
private:
|
||||||
std::string getMessage() const override { return "Wrong username or password."; }
|
std::string getMessage() const override { return "Wrong username or password."; }
|
||||||
};
|
};
|
||||||
|
|
||||||
class TokenAuthenticationNotSupportedForLDAPUsersError : public Error
|
class TokenAuthenticationNotSupportedForLDAPUsersError : public Error
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
TokenAuthenticationNotSupportedForLDAPUsersError() : Error {Code::TokenAuthenticationNotSupportedForLDAPUsers} {}
|
TokenAuthenticationNotSupportedForLDAPUsersError() : Error{ Code::TokenAuthenticationNotSupportedForLDAPUsers } {}
|
||||||
private:
|
private:
|
||||||
std::string getMessage() const override { return "Token authentication not supported for LDAP users."; }
|
std::string getMessage() const override { return "Token authentication not supported for LDAP users."; }
|
||||||
};
|
};
|
||||||
|
|
||||||
class UserNotAuthorizedError : public Error
|
class UserNotAuthorizedError : public Error
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
UserNotAuthorizedError () : Error {Code::UserNotAuthorized} {}
|
UserNotAuthorizedError() : Error{ Code::UserNotAuthorized } {}
|
||||||
private:
|
private:
|
||||||
std::string getMessage() const override { return "User is not authorized for the given operation."; }
|
std::string getMessage() const override { return "User is not authorized for the given operation."; }
|
||||||
};
|
};
|
||||||
|
|
||||||
class RequestedDataNotFoundError : public Error
|
class RequestedDataNotFoundError : public Error
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
RequestedDataNotFoundError() : Error {Code::RequestedDataNotFound} {}
|
RequestedDataNotFoundError() : Error{ Code::RequestedDataNotFound } {}
|
||||||
private:
|
private:
|
||||||
std::string getMessage() const override { return "The requested data was not found."; }
|
std::string getMessage() const override { return "The requested data was not found."; }
|
||||||
};
|
};
|
||||||
|
|
||||||
class InternalErrorGenericError : public GenericError
|
class InternalErrorGenericError : public GenericError
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
InternalErrorGenericError(const std::string& message) : _message {message} {}
|
InternalErrorGenericError(const std::string& message) : _message{ message } {}
|
||||||
private:
|
private:
|
||||||
std::string getMessage() const override { return "Internal error: " + _message; }
|
std::string getMessage() const override { return "Internal error: " + _message; }
|
||||||
const std::string _message;
|
const std::string _message;
|
||||||
};
|
};
|
||||||
|
|
||||||
class LoginThrottledGenericError : public GenericError
|
class LoginThrottledGenericError : public GenericError
|
||||||
{
|
{
|
||||||
std::string getMessage() const override { return "Login throttled, too many attempts"; }
|
std::string getMessage() const override { return "Login throttled, too many attempts"; }
|
||||||
};
|
};
|
||||||
|
|
||||||
class NotImplementedGenericError : public GenericError
|
class NotImplementedGenericError : public GenericError
|
||||||
{
|
{
|
||||||
std::string getMessage() const override { return "Not implemented"; }
|
std::string getMessage() const override { return "Not implemented"; }
|
||||||
};
|
};
|
||||||
|
|
||||||
class UnknownEntryPointGenericError : public GenericError
|
class UnknownEntryPointGenericError : public GenericError
|
||||||
{
|
{
|
||||||
std::string getMessage() const override { return "Unknown API method"; }
|
std::string getMessage() const override { return "Unknown API method"; }
|
||||||
};
|
};
|
||||||
|
|
||||||
class PasswordTooWeakGenericError : public GenericError
|
class PasswordTooWeakGenericError : public GenericError
|
||||||
{
|
{
|
||||||
std::string getMessage() const override { return "Password too weak"; }
|
std::string getMessage() const override { return "Password too weak"; }
|
||||||
};
|
};
|
||||||
|
|
||||||
class PasswordMustMatchLoginNameGenericError : public GenericError
|
class PasswordMustMatchLoginNameGenericError : public GenericError
|
||||||
{
|
{
|
||||||
std::string getMessage() const override { return "Password must match login name"; }
|
std::string getMessage() const override { return "Password must match login name"; }
|
||||||
};
|
};
|
||||||
|
|
||||||
class DemoUserCannotChangePasswordGenericError : public GenericError
|
class DemoUserCannotChangePasswordGenericError : public GenericError
|
||||||
{
|
{
|
||||||
std::string getMessage() const override { return "Demo user cannot change its password"; }
|
std::string getMessage() const override { return "Demo user cannot change its password"; }
|
||||||
};
|
};
|
||||||
|
|
||||||
class UserAlreadyExistsGenericError : public GenericError
|
class UserAlreadyExistsGenericError : public GenericError
|
||||||
{
|
{
|
||||||
std::string getMessage() const override { return "User already exists"; }
|
std::string getMessage() const override { return "User already exists"; }
|
||||||
};
|
};
|
||||||
|
|
||||||
class BadParameterGenericError : public GenericError
|
class BadParameterGenericError : public GenericError
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
BadParameterGenericError(const std::string& parameterName) : _parameterName {parameterName} {}
|
BadParameterGenericError(const std::string& parameterName) : _parameterName{ parameterName } {}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::string getMessage() const override { return "Parameter '" + _parameterName + "': bad value"; }
|
std::string getMessage() const override { return "Parameter '" + _parameterName + "': bad value"; }
|
||||||
|
|
||||||
const std::string _parameterName;
|
const std::string _parameterName;
|
||||||
};
|
};
|
||||||
|
|
||||||
class Response
|
class Response
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
class Node
|
class Node
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
void setAttribute(std::string_view key, std::string_view value);
|
void setAttribute(std::string_view key, std::string_view value);
|
||||||
|
|
||||||
template <typename T, std::enable_if_t<std::is_arithmetic<T>::value>* = nullptr>
|
template <typename T, std::enable_if_t<std::is_arithmetic<T>::value>* = nullptr>
|
||||||
void setAttribute(std::string_view key, T value)
|
void setAttribute(std::string_view key, T value)
|
||||||
{
|
{
|
||||||
if constexpr (std::is_same<bool, T>::value)
|
if constexpr (std::is_same<bool, T>::value)
|
||||||
_attributes[std::string {key}] = value;
|
_attributes[std::string{ key }] = value;
|
||||||
else
|
else if constexpr (std::is_floating_point<T>::value)
|
||||||
_attributes[std::string {key}] = static_cast<long long>(value);
|
_attributes[std::string{ key }] = static_cast<float>(value);
|
||||||
}
|
else if constexpr (std::is_integral<T>::value)
|
||||||
|
_attributes[std::string{ key }] = static_cast<long long>(value);
|
||||||
|
else
|
||||||
|
static_assert("Unhandled type");
|
||||||
|
}
|
||||||
|
|
||||||
// A Node has either a value or some children
|
// A Node has either a single value or an array of values or some children
|
||||||
void setValue(std::string_view value);
|
void setValue(std::string_view value);
|
||||||
void setValue(long long value);
|
void setValue(long long value);
|
||||||
Node& createChild(const std::string& key);
|
Node& createChild(const std::string& key);
|
||||||
Node& createArrayChild(const std::string& key);
|
Node& createArrayChild(const std::string& key);
|
||||||
|
|
||||||
void addChild(const std::string& key, Node node);
|
void addChild(const std::string& key, Node node);
|
||||||
void addArrayChild(const std::string& key, Node node);
|
void createEmptyArrayChild(std::string_view key);
|
||||||
|
void addArrayChild(std::string_view key, Node node);
|
||||||
|
void createEmptyArrayValue(std::string_view key);
|
||||||
|
void addArrayValue(std::string_view key, std::string_view value);
|
||||||
|
void addArrayValue(std::string_view key, long long value);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void setVersionAttribute(ProtocolVersion version);
|
void setVersionAttribute(ProtocolVersion version);
|
||||||
|
|
||||||
friend class Response;
|
friend class Response;
|
||||||
using ValueType = std::variant<std::string, bool, long long>;
|
using ValueType = std::variant<std::string, bool, float, long long>;
|
||||||
std::map<std::string, ValueType> _attributes;
|
std::map<std::string, ValueType> _attributes;
|
||||||
std::optional<ValueType> _value;
|
std::optional<ValueType> _value;
|
||||||
std::map<std::string, std::vector<Node>> _children;
|
std::map<std::string, std::vector<Node>> _children;
|
||||||
std::map<std::string, std::vector<Node>> _childrenArrays;
|
std::map<std::string, std::vector<Node>> _childrenArrays;
|
||||||
};
|
|
||||||
|
|
||||||
static Response createOkResponse(ProtocolVersion protocolVersion);
|
using ValuesType = std::vector<ValueType>;
|
||||||
static Response createFailedResponse(ProtocolVersion protocolVersion, const Error& error);
|
std::map<std::string, ValuesType> _childrenValues;
|
||||||
|
};
|
||||||
|
|
||||||
virtual ~Response() {}
|
static Response createOkResponse(ProtocolVersion protocolVersion);
|
||||||
Response(const Response&) = delete;
|
static Response createFailedResponse(ProtocolVersion protocolVersion, const Error& error);
|
||||||
Response& operator=(const Response&) = delete;
|
|
||||||
Response(Response&&) = default;
|
|
||||||
Response& operator=(Response&&) = default;
|
|
||||||
|
|
||||||
void addNode(const std::string& key, Node node);
|
virtual ~Response() {}
|
||||||
Node& createNode(const std::string& key);
|
Response(const Response&) = delete;
|
||||||
Node& createArrayNode(const std::string& key);
|
Response& operator=(const Response&) = delete;
|
||||||
|
Response(Response&&) = default;
|
||||||
|
Response& operator=(Response&&) = default;
|
||||||
|
|
||||||
void write(std::ostream& os, ResponseFormat format);
|
void addNode(const std::string& key, Node node);
|
||||||
|
Node& createNode(const std::string& key);
|
||||||
|
Node& createArrayNode(const std::string& key);
|
||||||
|
|
||||||
private:
|
void write(std::ostream& os, ResponseFormat format);
|
||||||
void writeJSON(std::ostream& os);
|
|
||||||
void writeXML(std::ostream& os);
|
|
||||||
|
|
||||||
Response() = default;
|
private:
|
||||||
Node _root;
|
void writeJSON(std::ostream& os);
|
||||||
};
|
void writeXML(std::ostream& os);
|
||||||
|
|
||||||
|
Response() = default;
|
||||||
|
Node _root;
|
||||||
|
};
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "Utils.hpp"
|
||||||
|
|
||||||
|
#include "utils/Service.hpp"
|
||||||
|
#include "utils/String.hpp"
|
||||||
|
#include "services/auth/IPasswordService.hpp"
|
||||||
|
#include "SubsonicResponse.hpp"
|
||||||
|
|
||||||
|
namespace API::Subsonic::Utils
|
||||||
|
{
|
||||||
|
void checkSetPasswordImplemented()
|
||||||
|
{
|
||||||
|
Auth::IPasswordService* passwordService{ Service<Auth::IPasswordService>::get() };
|
||||||
|
if (!passwordService || !passwordService->canSetPasswords())
|
||||||
|
throw NotImplementedGenericError{};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string makeNameFilesystemCompatible(const std::string& name)
|
||||||
|
{
|
||||||
|
return StringUtils::replaceInString(name, "/", "_");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace API::Subsonic::Utils
|
||||||
|
{
|
||||||
|
void checkSetPasswordImplemented();
|
||||||
|
std::string makeNameFilesystemCompatible(const std::string& name);
|
||||||
|
}
|
||||||
@@ -0,0 +1,270 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "AlbumSongLists.hpp"
|
||||||
|
|
||||||
|
#include "services/database/Artist.hpp"
|
||||||
|
#include "services/database/Cluster.hpp"
|
||||||
|
#include "services/database/Release.hpp"
|
||||||
|
#include "services/database/Session.hpp"
|
||||||
|
#include "services/database/Track.hpp"
|
||||||
|
#include "services/database/User.hpp"
|
||||||
|
#include "services/scrobbling/IScrobblingService.hpp"
|
||||||
|
#include "responses/Album.hpp"
|
||||||
|
#include "responses/Artist.hpp"
|
||||||
|
#include "responses/Song.hpp"
|
||||||
|
#include "utils/Service.hpp"
|
||||||
|
#include "ParameterParsing.hpp"
|
||||||
|
|
||||||
|
namespace API::Subsonic
|
||||||
|
{
|
||||||
|
using namespace Database;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
Response handleGetAlbumListRequestCommon(const RequestContext& context, bool id3)
|
||||||
|
{
|
||||||
|
// Mandatory params
|
||||||
|
const std::string type{ getMandatoryParameterAs<std::string>(context.parameters, "type") };
|
||||||
|
|
||||||
|
// Optional params
|
||||||
|
const std::size_t size{ getParameterAs<std::size_t>(context.parameters, "size").value_or(10) };
|
||||||
|
const std::size_t offset{ getParameterAs<std::size_t>(context.parameters, "offset").value_or(0) };
|
||||||
|
|
||||||
|
const Range range{ offset, size };
|
||||||
|
|
||||||
|
RangeResults<ReleaseId> releases;
|
||||||
|
Scrobbling::IScrobblingService& scrobbling{ *Service<Scrobbling::IScrobblingService>::get() };
|
||||||
|
|
||||||
|
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||||
|
|
||||||
|
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||||
|
if (!user)
|
||||||
|
throw UserNotAuthorizedError{};
|
||||||
|
|
||||||
|
if (type == "alphabeticalByName")
|
||||||
|
{
|
||||||
|
Release::FindParameters params;
|
||||||
|
params.setSortMethod(ReleaseSortMethod::Name);
|
||||||
|
params.setRange(range);
|
||||||
|
|
||||||
|
releases = Release::find(context.dbSession, params);
|
||||||
|
}
|
||||||
|
else if (type == "alphabeticalByArtist")
|
||||||
|
{
|
||||||
|
releases = Release::findOrderedByArtist(context.dbSession, range);
|
||||||
|
}
|
||||||
|
else if (type == "byGenre")
|
||||||
|
{
|
||||||
|
// Mandatory param
|
||||||
|
const std::string genre{ getMandatoryParameterAs<std::string>(context.parameters, "genre") };
|
||||||
|
|
||||||
|
if (const ClusterType::pointer clusterType{ ClusterType::find(context.dbSession, "GENRE") })
|
||||||
|
{
|
||||||
|
if (const Cluster::pointer cluster{ clusterType->getCluster(genre) })
|
||||||
|
{
|
||||||
|
Release::FindParameters params;
|
||||||
|
params.setClusters({ cluster->getId() });
|
||||||
|
params.setSortMethod(ReleaseSortMethod::Name);
|
||||||
|
params.setRange(range);
|
||||||
|
|
||||||
|
releases = Release::find(context.dbSession, params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (type == "byYear")
|
||||||
|
{
|
||||||
|
const int fromYear{ getMandatoryParameterAs<int>(context.parameters, "fromYear") };
|
||||||
|
const int toYear{ getMandatoryParameterAs<int>(context.parameters, "toYear") };
|
||||||
|
|
||||||
|
Release::FindParameters params;
|
||||||
|
params.setSortMethod(ReleaseSortMethod::Date);
|
||||||
|
params.setRange(range);
|
||||||
|
params.setDateRange(DateRange::fromYearRange(fromYear, toYear));
|
||||||
|
|
||||||
|
releases = Release::find(context.dbSession, params);
|
||||||
|
}
|
||||||
|
else if (type == "frequent")
|
||||||
|
{
|
||||||
|
releases = scrobbling.getTopReleases(context.userId, {}, range);
|
||||||
|
}
|
||||||
|
else if (type == "newest")
|
||||||
|
{
|
||||||
|
Release::FindParameters params;
|
||||||
|
params.setSortMethod(ReleaseSortMethod::LastWritten);
|
||||||
|
params.setRange(range);
|
||||||
|
|
||||||
|
releases = Release::find(context.dbSession, params);
|
||||||
|
}
|
||||||
|
else if (type == "random")
|
||||||
|
{
|
||||||
|
// Random results are paginated, but there is no acceptable way to handle the pagination params without repeating some albums
|
||||||
|
// (no seed provided by subsonic, ot it would require to store some kind of context for each user/client when iterating over the random albums)
|
||||||
|
Release::FindParameters params;
|
||||||
|
params.setSortMethod(ReleaseSortMethod::Random);
|
||||||
|
params.setRange({ 0, size });
|
||||||
|
|
||||||
|
releases = Release::find(context.dbSession, params);
|
||||||
|
}
|
||||||
|
else if (type == "recent")
|
||||||
|
{
|
||||||
|
releases = scrobbling.getRecentReleases(context.userId, {}, range);
|
||||||
|
}
|
||||||
|
else if (type == "starred")
|
||||||
|
{
|
||||||
|
releases = scrobbling.getStarredReleases(context.userId, {}, range);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
throw NotImplementedGenericError{};
|
||||||
|
|
||||||
|
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||||
|
Response::Node& albumListNode{ response.createNode(id3 ? "albumList2" : "albumList") };
|
||||||
|
|
||||||
|
for (const ReleaseId releaseId : releases.results)
|
||||||
|
{
|
||||||
|
const Release::pointer release{ Release::find(context.dbSession, releaseId) };
|
||||||
|
albumListNode.addArrayChild("album", createAlbumNode(release, context.dbSession, user, id3));
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
Response handleGetStarredRequestCommon(RequestContext& context, bool id3)
|
||||||
|
{
|
||||||
|
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||||
|
|
||||||
|
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||||
|
if (!user)
|
||||||
|
throw UserNotAuthorizedError{};
|
||||||
|
|
||||||
|
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||||
|
Response::Node& starredNode{ response.createNode(id3 ? "starred2" : "starred") };
|
||||||
|
|
||||||
|
Scrobbling::IScrobblingService& scrobbling{ *Service<Scrobbling::IScrobblingService>::get() };
|
||||||
|
|
||||||
|
for (const ArtistId artistId : scrobbling.getStarredArtists(context.userId, {} /* clusters */, std::nullopt /* linkType */, ArtistSortMethod::BySortName, Range{}).results)
|
||||||
|
{
|
||||||
|
if (auto artist{ Artist::find(context.dbSession, artistId) })
|
||||||
|
starredNode.addArrayChild("artist", createArtistNode(artist, context.dbSession, user, id3));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const ReleaseId releaseId : scrobbling.getStarredReleases(context.userId, {} /* clusters */, Range{}).results)
|
||||||
|
{
|
||||||
|
if (auto release{ Release::find(context.dbSession, releaseId) })
|
||||||
|
starredNode.addArrayChild("album", createAlbumNode(release, context.dbSession, user, id3));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const TrackId trackId : scrobbling.getStarredTracks(context.userId, {} /* clusters */, Range{}).results)
|
||||||
|
{
|
||||||
|
if (auto track{ Track::find(context.dbSession, trackId) })
|
||||||
|
starredNode.addArrayChild("song", createSongNode(track, context.dbSession, user));
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
Response handleGetAlbumListRequest(RequestContext& context)
|
||||||
|
{
|
||||||
|
return handleGetAlbumListRequestCommon(context, false /* no id3 */);
|
||||||
|
}
|
||||||
|
|
||||||
|
Response handleGetAlbumList2Request(RequestContext& context)
|
||||||
|
{
|
||||||
|
return handleGetAlbumListRequestCommon(context, true /* id3 */);
|
||||||
|
}
|
||||||
|
|
||||||
|
Response handleGetRandomSongsRequest(RequestContext& context)
|
||||||
|
{
|
||||||
|
// Optional params
|
||||||
|
std::size_t size{ getParameterAs<std::size_t>(context.parameters, "size").value_or(50) };
|
||||||
|
size = std::min(size, std::size_t{ 500 });
|
||||||
|
|
||||||
|
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||||
|
|
||||||
|
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||||
|
if (!user)
|
||||||
|
throw UserNotAuthorizedError{};
|
||||||
|
|
||||||
|
const auto trackIds{ Track::find(context.dbSession, Track::FindParameters {}.setSortMethod(TrackSortMethod::Random).setRange({0, size})) };
|
||||||
|
|
||||||
|
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||||
|
|
||||||
|
Response::Node& randomSongsNode{ response.createNode("randomSongs") };
|
||||||
|
for (const TrackId trackId : trackIds.results)
|
||||||
|
{
|
||||||
|
const Track::pointer track{ Track::find(context.dbSession, trackId) };
|
||||||
|
randomSongsNode.addArrayChild("song", createSongNode(track, context.dbSession, user));
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
Response handleGetSongsByGenreRequest(RequestContext& context)
|
||||||
|
{
|
||||||
|
// Mandatory params
|
||||||
|
std::string genre{ getMandatoryParameterAs<std::string>(context.parameters, "genre") };
|
||||||
|
|
||||||
|
// Optional params
|
||||||
|
std::size_t size{ getParameterAs<std::size_t>(context.parameters, "count").value_or(10) };
|
||||||
|
size = std::min(size, std::size_t{ 500 });
|
||||||
|
|
||||||
|
std::size_t offset{ getParameterAs<std::size_t>(context.parameters, "offset").value_or(0) };
|
||||||
|
|
||||||
|
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||||
|
|
||||||
|
auto clusterType{ ClusterType::find(context.dbSession, "GENRE") };
|
||||||
|
if (!clusterType)
|
||||||
|
throw RequestedDataNotFoundError{};
|
||||||
|
|
||||||
|
auto cluster{ clusterType->getCluster(genre) };
|
||||||
|
if (!cluster)
|
||||||
|
throw RequestedDataNotFoundError{};
|
||||||
|
|
||||||
|
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||||
|
if (!user)
|
||||||
|
throw UserNotAuthorizedError{};
|
||||||
|
|
||||||
|
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||||
|
Response::Node& songsByGenreNode{ response.createNode("songsByGenre") };
|
||||||
|
|
||||||
|
Track::FindParameters params;
|
||||||
|
params.setClusters({ cluster->getId() });
|
||||||
|
params.setRange({ offset, size });
|
||||||
|
|
||||||
|
auto trackIds{ Track::find(context.dbSession, params) };
|
||||||
|
for (const TrackId trackId : trackIds.results)
|
||||||
|
{
|
||||||
|
const Track::pointer track{ Track::find(context.dbSession, trackId) };
|
||||||
|
songsByGenreNode.addArrayChild("song", createSongNode(track, context.dbSession, user));
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
Response handleGetStarredRequest(RequestContext& context)
|
||||||
|
{
|
||||||
|
return handleGetStarredRequestCommon(context, false /* no id3 */);
|
||||||
|
}
|
||||||
|
|
||||||
|
Response handleGetStarred2Request(RequestContext& context)
|
||||||
|
{
|
||||||
|
return handleGetStarredRequestCommon(context, true /* id3 */);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "RequestContext.hpp"
|
||||||
|
#include "SubsonicResponse.hpp"
|
||||||
|
|
||||||
|
namespace API::Subsonic
|
||||||
|
{
|
||||||
|
Response handleGetAlbumListRequest(RequestContext& context);
|
||||||
|
Response handleGetAlbumList2Request(RequestContext& context);
|
||||||
|
Response handleGetRandomSongsRequest(RequestContext& context);
|
||||||
|
Response handleGetSongsByGenreRequest(RequestContext& context);
|
||||||
|
Response handleGetStarredRequest(RequestContext& context);
|
||||||
|
Response handleGetStarred2Request(RequestContext& context);
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "Bookmarks.hpp"
|
||||||
|
|
||||||
|
#include "services/database/Session.hpp"
|
||||||
|
#include "services/database/User.hpp"
|
||||||
|
#include "services/database/Track.hpp"
|
||||||
|
#include "services/database/TrackBookmark.hpp"
|
||||||
|
#include "responses/Bookmark.hpp"
|
||||||
|
#include "responses/Song.hpp"
|
||||||
|
#include "ParameterParsing.hpp"
|
||||||
|
#include "SubsonicId.hpp"
|
||||||
|
|
||||||
|
namespace API::Subsonic
|
||||||
|
{
|
||||||
|
using namespace Database;
|
||||||
|
|
||||||
|
Response handleGetBookmarks(RequestContext& context)
|
||||||
|
{
|
||||||
|
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||||
|
|
||||||
|
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||||
|
if (!user)
|
||||||
|
throw UserNotAuthorizedError{};
|
||||||
|
|
||||||
|
const auto bookmarkIds{ TrackBookmark::find(context.dbSession, user->getId(), Range {}) };
|
||||||
|
|
||||||
|
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||||
|
Response::Node& bookmarksNode{ response.createNode("bookmarks") };
|
||||||
|
|
||||||
|
for (const TrackBookmarkId bookmarkId : bookmarkIds.results)
|
||||||
|
{
|
||||||
|
const TrackBookmark::pointer bookmark{ TrackBookmark::find(context.dbSession, bookmarkId) };
|
||||||
|
Response::Node bookmarkNode{ createBookmarkNode(bookmark) };
|
||||||
|
bookmarkNode.addArrayChild("entry", createSongNode(bookmark->getTrack(), context.dbSession, user));
|
||||||
|
|
||||||
|
bookmarksNode.addArrayChild("bookmark", std::move(bookmarkNode));
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
Response handleCreateBookmark(RequestContext& context)
|
||||||
|
{
|
||||||
|
// Mandatory params
|
||||||
|
TrackId trackId{ getMandatoryParameterAs<TrackId>(context.parameters, "id") };
|
||||||
|
unsigned long position{ getMandatoryParameterAs<unsigned long>(context.parameters, "position") };
|
||||||
|
const std::optional<std::string> comment{ getParameterAs<std::string>(context.parameters, "comment") };
|
||||||
|
|
||||||
|
auto transaction{ context.dbSession.createUniqueTransaction() };
|
||||||
|
|
||||||
|
const User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||||
|
if (!user)
|
||||||
|
throw UserNotAuthorizedError{};
|
||||||
|
|
||||||
|
const Track::pointer track{ Track::find(context.dbSession, trackId) };
|
||||||
|
if (!track)
|
||||||
|
throw RequestedDataNotFoundError{};
|
||||||
|
|
||||||
|
// Replace any existing bookmark
|
||||||
|
auto bookmark{ TrackBookmark::find(context.dbSession, user->getId(), trackId) };
|
||||||
|
if (!bookmark)
|
||||||
|
bookmark = context.dbSession.create<TrackBookmark>(user, track);
|
||||||
|
|
||||||
|
bookmark.modify()->setOffset(std::chrono::milliseconds{ position });
|
||||||
|
if (comment)
|
||||||
|
bookmark.modify()->setComment(*comment);
|
||||||
|
|
||||||
|
return Response::createOkResponse(context.serverProtocolVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
Response handleDeleteBookmark(RequestContext& context)
|
||||||
|
{
|
||||||
|
// Mandatory params
|
||||||
|
TrackId trackId{ getMandatoryParameterAs<TrackId>(context.parameters, "id") };
|
||||||
|
|
||||||
|
auto transaction{ context.dbSession.createUniqueTransaction() };
|
||||||
|
|
||||||
|
auto bookmark{ TrackBookmark::find(context.dbSession, context.userId, trackId) };
|
||||||
|
if (!bookmark)
|
||||||
|
throw RequestedDataNotFoundError{};
|
||||||
|
|
||||||
|
bookmark.remove();
|
||||||
|
|
||||||
|
return Response::createOkResponse(context.serverProtocolVersion);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "RequestContext.hpp"
|
||||||
|
#include "SubsonicResponse.hpp"
|
||||||
|
|
||||||
|
namespace API::Subsonic
|
||||||
|
{
|
||||||
|
Response handleGetBookmarks(RequestContext& context);
|
||||||
|
Response handleCreateBookmark(RequestContext& context);
|
||||||
|
Response handleDeleteBookmark(RequestContext& context);
|
||||||
|
}
|
||||||
@@ -0,0 +1,465 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "Browsing.hpp"
|
||||||
|
|
||||||
|
#include "services/database/Artist.hpp"
|
||||||
|
#include "services/database/Cluster.hpp"
|
||||||
|
#include "services/database/Session.hpp"
|
||||||
|
#include "services/database/Release.hpp"
|
||||||
|
#include "services/database/Track.hpp"
|
||||||
|
#include "services/database/User.hpp"
|
||||||
|
#include "services/recommendation/IRecommendationService.hpp"
|
||||||
|
#include "utils/Random.hpp"
|
||||||
|
#include "utils/Service.hpp"
|
||||||
|
#include "responses/Album.hpp"
|
||||||
|
#include "responses/Artist.hpp"
|
||||||
|
#include "responses/Genre.hpp"
|
||||||
|
#include "responses/Song.hpp"
|
||||||
|
#include "ParameterParsing.hpp"
|
||||||
|
#include "SubsonicId.hpp"
|
||||||
|
#include "Utils.hpp"
|
||||||
|
|
||||||
|
namespace API::Subsonic
|
||||||
|
{
|
||||||
|
using namespace Database;
|
||||||
|
|
||||||
|
static const std::string_view reportedDummyDate{ "2000-01-01T00:00:00" };
|
||||||
|
static const unsigned long long reportedDummyDateULong{ 946684800000ULL }; // 2000-01-01T00:00:00 UTC
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
Response handleGetArtistInfoRequestCommon(RequestContext& context, bool id3)
|
||||||
|
{
|
||||||
|
// Mandatory params
|
||||||
|
ArtistId id{ getMandatoryParameterAs<ArtistId>(context.parameters, "id") };
|
||||||
|
|
||||||
|
// Optional params
|
||||||
|
std::size_t count{ getParameterAs<std::size_t>(context.parameters, "count").value_or(20) };
|
||||||
|
|
||||||
|
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||||
|
Response::Node& artistInfoNode{ response.createNode(id3 ? "artistInfo2" : "artistInfo") };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||||
|
|
||||||
|
const Artist::pointer artist{ Artist::find(context.dbSession, id) };
|
||||||
|
if (!artist)
|
||||||
|
throw RequestedDataNotFoundError{};
|
||||||
|
|
||||||
|
std::optional<UUID> artistMBID{ artist->getMBID() };
|
||||||
|
if (artistMBID)
|
||||||
|
artistInfoNode.createChild("musicBrainzId").setValue(artistMBID->getAsString());
|
||||||
|
}
|
||||||
|
|
||||||
|
auto similarArtistsId{ Service<Recommendation::IRecommendationService>::get()->getSimilarArtists(id, {TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist}, count) };
|
||||||
|
|
||||||
|
{
|
||||||
|
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||||
|
|
||||||
|
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||||
|
if (!user)
|
||||||
|
throw UserNotAuthorizedError{};
|
||||||
|
|
||||||
|
for (const ArtistId similarArtistId : similarArtistsId)
|
||||||
|
{
|
||||||
|
const Artist::pointer similarArtist{ Artist::find(context.dbSession, similarArtistId) };
|
||||||
|
if (similarArtist)
|
||||||
|
artistInfoNode.addArrayChild("similarArtist", createArtistNode(similarArtist, context.dbSession, user, id3));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
Response handleGetArtistsRequestCommon(RequestContext& context, bool id3)
|
||||||
|
{
|
||||||
|
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||||
|
|
||||||
|
Response::Node& artistsNode{ response.createNode(id3 ? "artists" : "indexes") };
|
||||||
|
artistsNode.setAttribute("ignoredArticles", "");
|
||||||
|
artistsNode.setAttribute("lastModified", reportedDummyDateULong);
|
||||||
|
|
||||||
|
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||||
|
|
||||||
|
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||||
|
if (!user)
|
||||||
|
throw UserNotAuthorizedError{};
|
||||||
|
|
||||||
|
Artist::FindParameters parameters;
|
||||||
|
parameters.setSortMethod(ArtistSortMethod::BySortName);
|
||||||
|
switch (user->getSubsonicArtistListMode())
|
||||||
|
{
|
||||||
|
case SubsonicArtistListMode::AllArtists:
|
||||||
|
break;
|
||||||
|
case SubsonicArtistListMode::ReleaseArtists:
|
||||||
|
parameters.setLinkType(TrackArtistLinkType::ReleaseArtist);
|
||||||
|
break;
|
||||||
|
case SubsonicArtistListMode::TrackArtists:
|
||||||
|
parameters.setLinkType(TrackArtistLinkType::Artist);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::map<char, std::vector<Artist::pointer>> artistsSortedByFirstChar;
|
||||||
|
const RangeResults<ArtistId> artists{ Artist::find(context.dbSession, parameters) };
|
||||||
|
for (const ArtistId artistId : artists.results)
|
||||||
|
{
|
||||||
|
const Artist::pointer artist{ Artist::find(context.dbSession, artistId) };
|
||||||
|
const std::string& sortName{ artist->getSortName() };
|
||||||
|
|
||||||
|
char sortChar;
|
||||||
|
if (sortName.empty() || !std::isalpha(sortName[0]))
|
||||||
|
sortChar = '?';
|
||||||
|
else
|
||||||
|
sortChar = std::toupper(sortName[0]);
|
||||||
|
|
||||||
|
artistsSortedByFirstChar[sortChar].push_back(artist);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
for (const auto& [sortChar, artists] : artistsSortedByFirstChar)
|
||||||
|
{
|
||||||
|
Response::Node& indexNode{ artistsNode.createArrayChild("index") };
|
||||||
|
indexNode.setAttribute("name", std::string{ sortChar });
|
||||||
|
|
||||||
|
for (const Artist::pointer& artist : artists)
|
||||||
|
indexNode.addArrayChild("artist", createArtistNode(artist, context.dbSession, user, id3));
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<TrackId> findSimilarSongs(RequestContext& context, ArtistId artistId, std::size_t count)
|
||||||
|
{
|
||||||
|
// API says: "Returns a random collection of songs from the given artist and similar artists"
|
||||||
|
const std::size_t similarArtistCount{ count / 5 };
|
||||||
|
std::vector<ArtistId> artistIds{ Service<Recommendation::IRecommendationService>::get()->getSimilarArtists(artistId, {TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist}, similarArtistCount) };
|
||||||
|
artistIds.push_back(artistId);
|
||||||
|
|
||||||
|
const std::size_t meanTrackCountPerArtist{ (count / artistIds.size()) + 1 };
|
||||||
|
|
||||||
|
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||||
|
|
||||||
|
std::vector<TrackId> tracks;
|
||||||
|
tracks.reserve(count);
|
||||||
|
|
||||||
|
for (const ArtistId id : artistIds)
|
||||||
|
{
|
||||||
|
Track::FindParameters params;
|
||||||
|
params.setArtist(id);
|
||||||
|
params.setRange({ 0, meanTrackCountPerArtist });
|
||||||
|
params.setSortMethod(TrackSortMethod::Random);
|
||||||
|
|
||||||
|
const auto artistTracks{ Track::find(context.dbSession, params) };
|
||||||
|
tracks.insert(std::end(tracks),
|
||||||
|
std::begin(artistTracks.results),
|
||||||
|
std::end(artistTracks.results));
|
||||||
|
}
|
||||||
|
|
||||||
|
return tracks;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<TrackId> findSimilarSongs(RequestContext& context, ReleaseId releaseId, std::size_t count)
|
||||||
|
{
|
||||||
|
// API says: "Returns a random collection of songs from the given artist and similar artists"
|
||||||
|
// so let's extend this for release
|
||||||
|
const std::size_t similarReleaseCount{ count / 5 };
|
||||||
|
std::vector<ReleaseId> releaseIds{ Service<Recommendation::IRecommendationService>::get()->getSimilarReleases(releaseId, similarReleaseCount) };
|
||||||
|
releaseIds.push_back(releaseId);
|
||||||
|
|
||||||
|
const std::size_t meanTrackCountPerRelease{ (count / releaseIds.size()) + 1 };
|
||||||
|
|
||||||
|
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||||
|
|
||||||
|
std::vector<TrackId> tracks;
|
||||||
|
tracks.reserve(count);
|
||||||
|
|
||||||
|
for (const ReleaseId id : releaseIds)
|
||||||
|
{
|
||||||
|
Track::FindParameters params;
|
||||||
|
params.setRelease(id);
|
||||||
|
params.setRange({ 0, meanTrackCountPerRelease });
|
||||||
|
params.setSortMethod(TrackSortMethod::Random);
|
||||||
|
|
||||||
|
const auto releaseTracks{ Track::find(context.dbSession, params) };
|
||||||
|
tracks.insert(std::end(tracks),
|
||||||
|
std::begin(releaseTracks.results),
|
||||||
|
std::end(releaseTracks.results));
|
||||||
|
}
|
||||||
|
|
||||||
|
return tracks;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<TrackId> findSimilarSongs(RequestContext&, TrackId trackId, std::size_t count)
|
||||||
|
{
|
||||||
|
return Service<Recommendation::IRecommendationService>::get()->findSimilarTracks({ trackId }, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
Response handleGetSimilarSongsRequestCommon(RequestContext& context, bool id3)
|
||||||
|
{
|
||||||
|
// Optional params
|
||||||
|
std::size_t count{ getParameterAs<std::size_t>(context.parameters, "count").value_or(50) };
|
||||||
|
|
||||||
|
std::vector<TrackId> tracks;
|
||||||
|
|
||||||
|
if (const auto artistId{ getParameterAs<ArtistId>(context.parameters, "id") })
|
||||||
|
tracks = findSimilarSongs(context, *artistId, count);
|
||||||
|
else if (const auto releaseId{ getParameterAs<ReleaseId>(context.parameters, "id") })
|
||||||
|
tracks = findSimilarSongs(context, *releaseId, count);
|
||||||
|
else if (const auto trackId{ getParameterAs<TrackId>(context.parameters, "id") })
|
||||||
|
tracks = findSimilarSongs(context, *trackId, count);
|
||||||
|
else
|
||||||
|
throw BadParameterGenericError{ "id" };
|
||||||
|
|
||||||
|
Random::shuffleContainer(tracks);
|
||||||
|
|
||||||
|
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||||
|
|
||||||
|
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||||
|
if (!user)
|
||||||
|
throw UserNotAuthorizedError{};
|
||||||
|
|
||||||
|
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||||
|
Response::Node& similarSongsNode{ response.createNode(id3 ? "similarSongs2" : "similarSongs") };
|
||||||
|
for (const TrackId trackId : tracks)
|
||||||
|
{
|
||||||
|
const Track::pointer track{ Track::find(context.dbSession, trackId) };
|
||||||
|
similarSongsNode.addArrayChild("song", createSongNode(track, context.dbSession, user));
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
Response handleGetMusicFoldersRequest(RequestContext& context)
|
||||||
|
{
|
||||||
|
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||||
|
Response::Node& musicFoldersNode{ response.createNode("musicFolders") };
|
||||||
|
|
||||||
|
Response::Node& musicFolderNode{ musicFoldersNode.createArrayChild("musicFolder") };
|
||||||
|
musicFolderNode.setAttribute("id", "0");
|
||||||
|
musicFolderNode.setAttribute("name", "Music");
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
Response handleGetIndexesRequest(RequestContext& context)
|
||||||
|
{
|
||||||
|
return handleGetArtistsRequestCommon(context, false /* no id3 */);
|
||||||
|
}
|
||||||
|
|
||||||
|
Response handleGetMusicDirectoryRequest(RequestContext& context)
|
||||||
|
{
|
||||||
|
// Mandatory params
|
||||||
|
const auto artistId{ getParameterAs<ArtistId>(context.parameters, "id") };
|
||||||
|
const auto releaseId{ getParameterAs<ReleaseId>(context.parameters, "id") };
|
||||||
|
const auto root{ getParameterAs<RootId>(context.parameters, "id") };
|
||||||
|
|
||||||
|
if (!root && !artistId && !releaseId)
|
||||||
|
throw BadParameterGenericError{ "id" };
|
||||||
|
|
||||||
|
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||||
|
Response::Node& directoryNode{ response.createNode("directory") };
|
||||||
|
|
||||||
|
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||||
|
|
||||||
|
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||||
|
if (!user)
|
||||||
|
throw UserNotAuthorizedError{};
|
||||||
|
|
||||||
|
if (root)
|
||||||
|
{
|
||||||
|
directoryNode.setAttribute("id", idToString(RootId{}));
|
||||||
|
directoryNode.setAttribute("name", "Music");
|
||||||
|
|
||||||
|
auto rootArtistIds{ Artist::find(context.dbSession, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::BySortName)) };
|
||||||
|
for (const ArtistId rootArtistId : rootArtistIds.results)
|
||||||
|
{
|
||||||
|
const Artist::pointer artist{ Artist::find(context.dbSession, rootArtistId) };
|
||||||
|
directoryNode.addArrayChild("child", createArtistNode(artist, context.dbSession, user, false /* no id3 */));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (artistId)
|
||||||
|
{
|
||||||
|
directoryNode.setAttribute("id", idToString(*artistId));
|
||||||
|
|
||||||
|
auto artist{ Artist::find(context.dbSession, *artistId) };
|
||||||
|
if (!artist)
|
||||||
|
throw RequestedDataNotFoundError{};
|
||||||
|
|
||||||
|
directoryNode.setAttribute("name", Utils::makeNameFilesystemCompatible(artist->getName()));
|
||||||
|
|
||||||
|
const auto artistReleases{ Release::find(context.dbSession, Release::FindParameters {}.setArtist(*artistId)) };
|
||||||
|
for (const ReleaseId artistReleaseId : artistReleases.results)
|
||||||
|
{
|
||||||
|
const Release::pointer release{ Release::find(context.dbSession, artistReleaseId) };
|
||||||
|
directoryNode.addArrayChild("child", createAlbumNode(release, context.dbSession, user, false /* no id3 */));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (releaseId)
|
||||||
|
{
|
||||||
|
directoryNode.setAttribute("id", idToString(*releaseId));
|
||||||
|
|
||||||
|
auto release{ Release::find(context.dbSession, *releaseId) };
|
||||||
|
if (!release)
|
||||||
|
throw RequestedDataNotFoundError{};
|
||||||
|
|
||||||
|
directoryNode.setAttribute("name", Utils::makeNameFilesystemCompatible(release->getName()));
|
||||||
|
|
||||||
|
const auto tracks{ Track::find(context.dbSession, Track::FindParameters {}.setRelease(*releaseId).setSortMethod(TrackSortMethod::Release)) };
|
||||||
|
for (const TrackId trackId : tracks.results)
|
||||||
|
{
|
||||||
|
const Track::pointer track{ Track::find(context.dbSession, trackId) };
|
||||||
|
directoryNode.addArrayChild("child", createSongNode(track, context.dbSession, user));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
throw BadParameterGenericError{ "id" };
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
Response handleGetGenresRequest(RequestContext& context)
|
||||||
|
{
|
||||||
|
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||||
|
|
||||||
|
Response::Node& genresNode{ response.createNode("genres") };
|
||||||
|
|
||||||
|
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||||
|
|
||||||
|
const ClusterType::pointer clusterType{ ClusterType::find(context.dbSession, "GENRE") };
|
||||||
|
if (clusterType)
|
||||||
|
{
|
||||||
|
const auto clusters{ clusterType->getClusters() };
|
||||||
|
|
||||||
|
for (const Cluster::pointer& cluster : clusters)
|
||||||
|
genresNode.addArrayChild("genre", createGenreNode(cluster));
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
Response handleGetArtistsRequest(RequestContext& context)
|
||||||
|
{
|
||||||
|
return handleGetArtistsRequestCommon(context, true /* id3 */);
|
||||||
|
}
|
||||||
|
|
||||||
|
Response handleGetArtistRequest(RequestContext& context)
|
||||||
|
{
|
||||||
|
// Mandatory params
|
||||||
|
ArtistId id{ getMandatoryParameterAs<ArtistId>(context.parameters, "id") };
|
||||||
|
|
||||||
|
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||||
|
|
||||||
|
const Artist::pointer artist{ Artist::find(context.dbSession, id) };
|
||||||
|
if (!artist)
|
||||||
|
throw RequestedDataNotFoundError{};
|
||||||
|
|
||||||
|
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||||
|
if (!user)
|
||||||
|
throw UserNotAuthorizedError{};
|
||||||
|
|
||||||
|
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||||
|
Response::Node artistNode{ createArtistNode(artist, context.dbSession, user, true /* id3 */) };
|
||||||
|
|
||||||
|
const auto releases{ Release::find(context.dbSession, Release::FindParameters {}.setArtist(artist->getId())) };
|
||||||
|
for (const ReleaseId releaseId : releases.results)
|
||||||
|
{
|
||||||
|
const Release::pointer release{ Release::find(context.dbSession, releaseId) };
|
||||||
|
artistNode.addArrayChild("album", createAlbumNode(release, context.dbSession, user, true /* id3 */));
|
||||||
|
}
|
||||||
|
|
||||||
|
response.addNode("artist", std::move(artistNode));
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
Response handleGetAlbumRequest(RequestContext& context)
|
||||||
|
{
|
||||||
|
// Mandatory params
|
||||||
|
ReleaseId id{ getMandatoryParameterAs<ReleaseId>(context.parameters, "id") };
|
||||||
|
|
||||||
|
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||||
|
|
||||||
|
Release::pointer release{ Release::find(context.dbSession, id) };
|
||||||
|
if (!release)
|
||||||
|
throw RequestedDataNotFoundError{};
|
||||||
|
|
||||||
|
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||||
|
if (!user)
|
||||||
|
throw UserNotAuthorizedError{};
|
||||||
|
|
||||||
|
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||||
|
Response::Node albumNode{ createAlbumNode(release, context.dbSession, user, true /* id3 */) };
|
||||||
|
|
||||||
|
const auto tracks{ Track::find(context.dbSession, Track::FindParameters {}.setRelease(id).setSortMethod(TrackSortMethod::Release)) };
|
||||||
|
for (const TrackId trackId : tracks.results)
|
||||||
|
{
|
||||||
|
const Track::pointer track{ Track::find(context.dbSession, trackId) };
|
||||||
|
albumNode.addArrayChild("song", createSongNode(track, context.dbSession, user));
|
||||||
|
}
|
||||||
|
|
||||||
|
response.addNode("album", std::move(albumNode));
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
Response handleGetSongRequest(RequestContext& context)
|
||||||
|
{
|
||||||
|
// Mandatory params
|
||||||
|
TrackId id{ getMandatoryParameterAs<TrackId>(context.parameters, "id") };
|
||||||
|
|
||||||
|
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||||
|
|
||||||
|
const Track::pointer track{ Track::find(context.dbSession, id) };
|
||||||
|
if (!track)
|
||||||
|
throw RequestedDataNotFoundError{};
|
||||||
|
|
||||||
|
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||||
|
if (!user)
|
||||||
|
throw UserNotAuthorizedError{};
|
||||||
|
|
||||||
|
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||||
|
response.addNode("song", createSongNode(track, context.dbSession, user));
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
Response handleGetArtistInfoRequest(RequestContext& context)
|
||||||
|
{
|
||||||
|
return handleGetArtistInfoRequestCommon(context, false /* no id3 */);
|
||||||
|
}
|
||||||
|
|
||||||
|
Response handleGetArtistInfo2Request(RequestContext& context)
|
||||||
|
{
|
||||||
|
return handleGetArtistInfoRequestCommon(context, true /* id3 */);
|
||||||
|
}
|
||||||
|
|
||||||
|
Response handleGetSimilarSongsRequest(RequestContext& context)
|
||||||
|
{
|
||||||
|
return handleGetSimilarSongsRequestCommon(context, false /* no id3 */);
|
||||||
|
}
|
||||||
|
|
||||||
|
Response handleGetSimilarSongs2Request(RequestContext& context)
|
||||||
|
{
|
||||||
|
return handleGetSimilarSongsRequestCommon(context, true /* id3 */);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "RequestContext.hpp"
|
||||||
|
#include "SubsonicResponse.hpp"
|
||||||
|
|
||||||
|
namespace API::Subsonic
|
||||||
|
{
|
||||||
|
Response handleGetMusicFoldersRequest(RequestContext& context);
|
||||||
|
Response handleGetIndexesRequest(RequestContext& context);
|
||||||
|
Response handleGetMusicDirectoryRequest(RequestContext& context);
|
||||||
|
Response handleGetGenresRequest(RequestContext& context);
|
||||||
|
Response handleGetArtistsRequest(RequestContext& context);
|
||||||
|
Response handleGetArtistRequest(RequestContext& context);
|
||||||
|
Response handleGetAlbumRequest(RequestContext& context);
|
||||||
|
Response handleGetSongRequest(RequestContext& context);
|
||||||
|
Response handleGetArtistInfoRequest(RequestContext& context);
|
||||||
|
Response handleGetArtistInfo2Request(RequestContext& context);
|
||||||
|
Response handleGetSimilarSongsRequest(RequestContext& context);
|
||||||
|
Response handleGetSimilarSongs2Request(RequestContext& context);
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "MediaAnnotation.hpp"
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "services/database/ArtistId.hpp"
|
||||||
|
#include "services/database/ReleaseId.hpp"
|
||||||
|
#include "services/database/TrackId.hpp"
|
||||||
|
#include "services/scrobbling/IScrobblingService.hpp"
|
||||||
|
#include "utils/Service.hpp"
|
||||||
|
#include "ParameterParsing.hpp"
|
||||||
|
#include "SubsonicId.hpp"
|
||||||
|
|
||||||
|
namespace API::Subsonic
|
||||||
|
{
|
||||||
|
using namespace Database;
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
struct StarParameters
|
||||||
|
{
|
||||||
|
std::vector<ArtistId> artistIds;
|
||||||
|
std::vector<ReleaseId> releaseIds;
|
||||||
|
std::vector<TrackId> trackIds;
|
||||||
|
};
|
||||||
|
|
||||||
|
StarParameters getStarParameters(const Wt::Http::ParameterMap& parameters)
|
||||||
|
{
|
||||||
|
StarParameters res;
|
||||||
|
|
||||||
|
// TODO handle parameters for legacy file browsing
|
||||||
|
res.trackIds = getMultiParametersAs<TrackId>(parameters, "id");
|
||||||
|
res.artistIds = getMultiParametersAs<ArtistId>(parameters, "artistId");
|
||||||
|
res.releaseIds = getMultiParametersAs<ReleaseId>(parameters, "albumId");
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Response handleStarRequest(RequestContext& context)
|
||||||
|
{
|
||||||
|
StarParameters params{ getStarParameters(context.parameters) };
|
||||||
|
|
||||||
|
for (const ArtistId id : params.artistIds)
|
||||||
|
Service<Scrobbling::IScrobblingService>::get()->star(context.userId, id);
|
||||||
|
|
||||||
|
for (const ReleaseId id : params.releaseIds)
|
||||||
|
Service<Scrobbling::IScrobblingService>::get()->star(context.userId, id);
|
||||||
|
|
||||||
|
for (const TrackId id : params.trackIds)
|
||||||
|
Service<Scrobbling::IScrobblingService>::get()->star(context.userId, id);
|
||||||
|
|
||||||
|
return Response::createOkResponse(context.serverProtocolVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
Response handleUnstarRequest(RequestContext& context)
|
||||||
|
{
|
||||||
|
StarParameters params{ getStarParameters(context.parameters) };
|
||||||
|
|
||||||
|
for (const ArtistId id : params.artistIds)
|
||||||
|
Service<Scrobbling::IScrobblingService>::get()->unstar(context.userId, id);
|
||||||
|
|
||||||
|
for (const ReleaseId id : params.releaseIds)
|
||||||
|
Service<Scrobbling::IScrobblingService>::get()->unstar(context.userId, id);
|
||||||
|
|
||||||
|
for (const TrackId id : params.trackIds)
|
||||||
|
Service<Scrobbling::IScrobblingService>::get()->unstar(context.userId, id);
|
||||||
|
|
||||||
|
return Response::createOkResponse(context.serverProtocolVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
Response handleScrobble(RequestContext& context)
|
||||||
|
{
|
||||||
|
const std::vector<TrackId> ids{ getMandatoryMultiParametersAs<TrackId>(context.parameters, "id") };
|
||||||
|
const std::vector<unsigned long> times{ getMultiParametersAs<unsigned long>(context.parameters, "time") };
|
||||||
|
const bool submission{ getParameterAs<bool>(context.parameters, "submission").value_or(true) };
|
||||||
|
|
||||||
|
// playing now => no time to be provided
|
||||||
|
if (!submission && !times.empty())
|
||||||
|
throw BadParameterGenericError{ "time" };
|
||||||
|
|
||||||
|
// playing now => only one at a time
|
||||||
|
if (!submission && ids.size() > 1)
|
||||||
|
throw BadParameterGenericError{ "id" };
|
||||||
|
|
||||||
|
// if multiple submissions, must have times
|
||||||
|
if (ids.size() > 1 && ids.size() != times.size())
|
||||||
|
throw BadParameterGenericError{ "time" };
|
||||||
|
|
||||||
|
if (!submission)
|
||||||
|
{
|
||||||
|
Service<Scrobbling::IScrobblingService>::get()->listenStarted({ context.userId, ids.front() });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (times.empty())
|
||||||
|
{
|
||||||
|
Service<Scrobbling::IScrobblingService>::get()->listenFinished({ context.userId, ids.front() });
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (std::size_t i{}; i < ids.size(); ++i)
|
||||||
|
{
|
||||||
|
const TrackId trackId{ ids[i] };
|
||||||
|
const unsigned long time{ times[i] };
|
||||||
|
Service<Scrobbling::IScrobblingService>::get()->addTimedListen({ {context.userId, trackId}, Wt::WDateTime::fromTime_t(static_cast<std::time_t>(time / 1000)) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response::createOkResponse(context.serverProtocolVersion);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "RequestContext.hpp"
|
||||||
|
#include "SubsonicResponse.hpp"
|
||||||
|
|
||||||
|
namespace API::Subsonic
|
||||||
|
{
|
||||||
|
Response handleStarRequest(RequestContext& context);
|
||||||
|
Response handleUnstarRequest(RequestContext& context);
|
||||||
|
Response handleScrobble(RequestContext& context);
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "MediaLibraryScanning.hpp"
|
||||||
|
|
||||||
|
#include "services/scanner/IScannerService.hpp"
|
||||||
|
#include "utils/Service.hpp"
|
||||||
|
|
||||||
|
namespace API::Subsonic::Scan
|
||||||
|
{
|
||||||
|
using namespace Scanner;
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
Response::Node
|
||||||
|
createStatusResponseNode()
|
||||||
|
{
|
||||||
|
Response::Node statusResponse;
|
||||||
|
|
||||||
|
const IScannerService::Status scanStatus{ Service<IScannerService>::get()->getStatus() };
|
||||||
|
|
||||||
|
statusResponse.setAttribute("scanning", scanStatus.currentState == IScannerService::State::InProgress);
|
||||||
|
if (scanStatus.currentState == IScannerService::State::InProgress)
|
||||||
|
{
|
||||||
|
std::size_t count{};
|
||||||
|
|
||||||
|
if (scanStatus.currentScanStepStats && scanStatus.currentScanStepStats->currentStep == ScanStep::ScanningFiles)
|
||||||
|
count = scanStatus.currentScanStepStats->processedElems;
|
||||||
|
|
||||||
|
statusResponse.setAttribute("count", count);
|
||||||
|
}
|
||||||
|
|
||||||
|
return statusResponse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Response handleGetScanStatus(RequestContext& context)
|
||||||
|
{
|
||||||
|
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||||
|
response.addNode("scanStatus", createStatusResponseNode());
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
Response handleStartScan(RequestContext& context)
|
||||||
|
{
|
||||||
|
Service<IScannerService>::get()->requestImmediateScan(false);
|
||||||
|
|
||||||
|
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||||
|
response.addNode("scanStatus", createStatusResponseNode());
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
+2
-2
@@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
namespace API::Subsonic::Scan
|
namespace API::Subsonic::Scan
|
||||||
{
|
{
|
||||||
Response handleGetScanStatus(RequestContext& context);
|
Response handleGetScanStatus(RequestContext& context);
|
||||||
Response handleStartScan(RequestContext& context);
|
Response handleStartScan(RequestContext& context);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "MediaRetrieval.hpp"
|
||||||
|
|
||||||
|
#include "av/TranscodeParameters.hpp"
|
||||||
|
#include "av/TranscodeResourceHandlerCreator.hpp"
|
||||||
|
#include "av/Types.hpp"
|
||||||
|
#include "services/cover/ICoverService.hpp"
|
||||||
|
#include "services/database/Session.hpp"
|
||||||
|
#include "services/database/Track.hpp"
|
||||||
|
#include "services/database/User.hpp"
|
||||||
|
#include "utils/IResourceHandler.hpp"
|
||||||
|
#include "utils/Logger.hpp"
|
||||||
|
#include "utils/FileResourceHandlerCreator.hpp"
|
||||||
|
#include "utils/Utils.hpp"
|
||||||
|
#include "ParameterParsing.hpp"
|
||||||
|
#include "SubsonicId.hpp"
|
||||||
|
|
||||||
|
using namespace Database;
|
||||||
|
|
||||||
|
namespace API::Subsonic
|
||||||
|
{
|
||||||
|
namespace {
|
||||||
|
Av::Format userTranscodeFormatToAvFormat(AudioFormat format)
|
||||||
|
{
|
||||||
|
switch (format)
|
||||||
|
{
|
||||||
|
case AudioFormat::MP3: return Av::Format::MP3;
|
||||||
|
case AudioFormat::OGG_OPUS: return Av::Format::OGG_OPUS;
|
||||||
|
case AudioFormat::MATROSKA_OPUS: return Av::Format::MATROSKA_OPUS;
|
||||||
|
case AudioFormat::OGG_VORBIS: return Av::Format::OGG_VORBIS;
|
||||||
|
case AudioFormat::WEBM_VORBIS: return Av::Format::WEBM_VORBIS;
|
||||||
|
default: return Av::Format::OGG_OPUS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct StreamParameters
|
||||||
|
{
|
||||||
|
Av::InputFileParameters inputFileParameters;
|
||||||
|
std::optional<Av::TranscodeParameters> transcodeParameters;
|
||||||
|
bool estimateContentLength{};
|
||||||
|
};
|
||||||
|
|
||||||
|
StreamParameters getStreamParameters(RequestContext& context)
|
||||||
|
{
|
||||||
|
// Mandatory params
|
||||||
|
const TrackId id{ getMandatoryParameterAs<TrackId>(context.parameters, "id") };
|
||||||
|
|
||||||
|
// Optional params
|
||||||
|
std::optional<std::size_t> maxBitRate{ getParameterAs<std::size_t>(context.parameters, "maxBitRate") };
|
||||||
|
const std::optional<std::string> format{ getParameterAs<std::string>(context.parameters, "format") };
|
||||||
|
const std::optional<std::size_t> timeOffset{ getParameterAs<std::size_t>(context.parameters, "timeOffset") };
|
||||||
|
bool estimateContentLength{ getParameterAs<bool>(context.parameters, "estimateContentLength").value_or(false) };
|
||||||
|
|
||||||
|
StreamParameters parameters;
|
||||||
|
|
||||||
|
parameters.estimateContentLength = estimateContentLength;
|
||||||
|
|
||||||
|
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||||
|
|
||||||
|
{
|
||||||
|
const auto track{ Track::find(context.dbSession, id) };
|
||||||
|
if (!track)
|
||||||
|
throw RequestedDataNotFoundError{};
|
||||||
|
|
||||||
|
parameters.inputFileParameters.trackPath = track->getPath();
|
||||||
|
parameters.inputFileParameters.duration = track->getDuration();
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||||
|
if (!user)
|
||||||
|
throw UserNotAuthorizedError{};
|
||||||
|
|
||||||
|
// format = "raw" => no transcode. Other format values will be ignored
|
||||||
|
const bool transcode{ (!format || (*format != "raw")) && user->getSubsonicTranscodeEnable() };
|
||||||
|
if (transcode)
|
||||||
|
{
|
||||||
|
std::size_t bitRate{ user->getSubsonicTranscodeBitrate() / 1000 };
|
||||||
|
|
||||||
|
// "If set to zero, no limit is imposed"
|
||||||
|
if (maxBitRate && *maxBitRate != 0)
|
||||||
|
bitRate = Utils::clamp(*maxBitRate, std::size_t{ 48 }, bitRate);
|
||||||
|
|
||||||
|
Av::TranscodeParameters transcodeParameters;
|
||||||
|
|
||||||
|
transcodeParameters.bitrate = bitRate * 1000;
|
||||||
|
transcodeParameters.format = userTranscodeFormatToAvFormat(user->getSubsonicTranscodeFormat());
|
||||||
|
transcodeParameters.stripMetadata = false; // We want clients to use metadata (offline use, replay gain, etc.)
|
||||||
|
transcodeParameters.offset = std::chrono::seconds{ timeOffset ? *timeOffset : 0 };
|
||||||
|
|
||||||
|
parameters.transcodeParameters = std::move(transcodeParameters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parameters;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleDownload(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response)
|
||||||
|
{
|
||||||
|
std::shared_ptr<IResourceHandler> resourceHandler;
|
||||||
|
|
||||||
|
Wt::Http::ResponseContinuation* continuation{ request.continuation() };
|
||||||
|
if (!continuation)
|
||||||
|
{
|
||||||
|
// Mandatory params
|
||||||
|
Database::TrackId id{ getMandatoryParameterAs<Database::TrackId>(context.parameters, "id") };
|
||||||
|
|
||||||
|
std::filesystem::path trackPath;
|
||||||
|
{
|
||||||
|
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||||
|
|
||||||
|
auto track{ Track::find(context.dbSession, id) };
|
||||||
|
if (!track)
|
||||||
|
throw RequestedDataNotFoundError{};
|
||||||
|
|
||||||
|
trackPath = track->getPath();
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceHandler = createFileResourceHandler(trackPath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
resourceHandler = Wt::cpp17::any_cast<std::shared_ptr<IResourceHandler>>(continuation->data());
|
||||||
|
}
|
||||||
|
|
||||||
|
continuation = resourceHandler->processRequest(request, response);
|
||||||
|
if (continuation)
|
||||||
|
continuation->setData(resourceHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleStream(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response)
|
||||||
|
{
|
||||||
|
std::shared_ptr<IResourceHandler> resourceHandler;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Wt::Http::ResponseContinuation* continuation = request.continuation();
|
||||||
|
if (!continuation)
|
||||||
|
{
|
||||||
|
StreamParameters streamParameters{ getStreamParameters(context) };
|
||||||
|
if (streamParameters.transcodeParameters)
|
||||||
|
resourceHandler = Av::createTranscodeResourceHandler(streamParameters.inputFileParameters, *streamParameters.transcodeParameters, streamParameters.estimateContentLength);
|
||||||
|
else
|
||||||
|
resourceHandler = createFileResourceHandler(streamParameters.inputFileParameters.trackPath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
resourceHandler = Wt::cpp17::any_cast<std::shared_ptr<IResourceHandler>>(continuation->data());
|
||||||
|
}
|
||||||
|
|
||||||
|
continuation = resourceHandler->processRequest(request, response);
|
||||||
|
if (continuation)
|
||||||
|
continuation->setData(resourceHandler);
|
||||||
|
}
|
||||||
|
catch (const Av::Exception& e)
|
||||||
|
{
|
||||||
|
LMS_LOG(API_SUBSONIC, ERROR) << "Caught Av exception: " << e.what();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleGetCoverArt(RequestContext& context, const Wt::Http::Request& /*request*/, Wt::Http::Response& response)
|
||||||
|
{
|
||||||
|
// Mandatory params
|
||||||
|
const auto trackId{ getParameterAs<TrackId>(context.parameters, "id") };
|
||||||
|
const auto releaseId{ getParameterAs<ReleaseId>(context.parameters, "id") };
|
||||||
|
|
||||||
|
if (!trackId && !releaseId)
|
||||||
|
throw BadParameterGenericError{ "id" };
|
||||||
|
|
||||||
|
std::size_t size{ getParameterAs<std::size_t>(context.parameters, "size").value_or(1024) };
|
||||||
|
size = ::Utils::clamp(size, std::size_t{ 32 }, std::size_t{ 2048 });
|
||||||
|
|
||||||
|
std::shared_ptr<Image::IEncodedImage> cover;
|
||||||
|
if (trackId)
|
||||||
|
cover = Service<Cover::ICoverService>::get()->getFromTrack(*trackId, size);
|
||||||
|
else if (releaseId)
|
||||||
|
cover = Service<Cover::ICoverService>::get()->getFromRelease(*releaseId, size);
|
||||||
|
|
||||||
|
response.out().write(reinterpret_cast<const char*>(cover->getData()), cover->getDataSize());
|
||||||
|
response.setMimeType(std::string{ cover->getMimeType() });
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace API::Subsonic
|
||||||
+4
-3
@@ -24,9 +24,10 @@
|
|||||||
|
|
||||||
#include "RequestContext.hpp"
|
#include "RequestContext.hpp"
|
||||||
|
|
||||||
namespace API::Subsonic::Stream
|
namespace API::Subsonic
|
||||||
{
|
{
|
||||||
void handleDownload(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response);
|
void handleDownload(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response);
|
||||||
void handleStream(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response);
|
void handleStream(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response);
|
||||||
|
void handleGetCoverArt(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "Playlists.hpp"
|
||||||
|
|
||||||
|
#include "services/database/Session.hpp"
|
||||||
|
#include "services/database/Track.hpp"
|
||||||
|
#include "services/database/TrackList.hpp"
|
||||||
|
#include "services/database/User.hpp"
|
||||||
|
#include "responses/Playlist.hpp"
|
||||||
|
#include "responses/Song.hpp"
|
||||||
|
#include "ParameterParsing.hpp"
|
||||||
|
#include "SubsonicId.hpp"
|
||||||
|
|
||||||
|
namespace API::Subsonic
|
||||||
|
{
|
||||||
|
using namespace Database;
|
||||||
|
|
||||||
|
Response handleGetPlaylistsRequest(RequestContext& context)
|
||||||
|
{
|
||||||
|
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||||
|
|
||||||
|
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||||
|
Response::Node& playlistsNode{ response.createNode("playlists") };
|
||||||
|
|
||||||
|
TrackList::FindParameters params;
|
||||||
|
params.setUser(context.userId);
|
||||||
|
params.setType(TrackListType::Playlist);
|
||||||
|
|
||||||
|
auto tracklistIds{ TrackList::find(context.dbSession, params) };
|
||||||
|
for (const TrackListId trackListId : tracklistIds.results)
|
||||||
|
{
|
||||||
|
const TrackList::pointer trackList{ TrackList::find(context.dbSession, trackListId) };
|
||||||
|
playlistsNode.addArrayChild("playlist", createPlaylistNode(trackList, context.dbSession));
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
Response handleGetPlaylistRequest(RequestContext& context)
|
||||||
|
{
|
||||||
|
// Mandatory params
|
||||||
|
TrackListId trackListId{ getMandatoryParameterAs<TrackListId>(context.parameters, "id") };
|
||||||
|
|
||||||
|
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||||
|
|
||||||
|
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||||
|
if (!user)
|
||||||
|
throw UserNotAuthorizedError{};
|
||||||
|
|
||||||
|
TrackList::pointer tracklist{ TrackList::find(context.dbSession, trackListId) };
|
||||||
|
if (!tracklist)
|
||||||
|
throw RequestedDataNotFoundError{};
|
||||||
|
|
||||||
|
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||||
|
Response::Node playlistNode{ createPlaylistNode(tracklist, context.dbSession) };
|
||||||
|
|
||||||
|
auto entries{ tracklist->getEntries() };
|
||||||
|
for (const TrackListEntry::pointer& entry : entries)
|
||||||
|
playlistNode.addArrayChild("entry", createSongNode(entry->getTrack(), context.dbSession, user));
|
||||||
|
|
||||||
|
response.addNode("playlist", playlistNode);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
Response handleCreatePlaylistRequest(RequestContext& context)
|
||||||
|
{
|
||||||
|
// Optional params
|
||||||
|
const auto id{ getParameterAs<TrackListId>(context.parameters, "playlistId") };
|
||||||
|
auto name{ getParameterAs<std::string>(context.parameters, "name") };
|
||||||
|
|
||||||
|
std::vector<TrackId> trackIds{ getMultiParametersAs<TrackId>(context.parameters, "songId") };
|
||||||
|
|
||||||
|
if (!name && !id)
|
||||||
|
throw RequiredParameterMissingError{ "name or id" };
|
||||||
|
|
||||||
|
auto transaction{ context.dbSession.createUniqueTransaction() };
|
||||||
|
|
||||||
|
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||||
|
if (!user)
|
||||||
|
throw UserNotAuthorizedError{};
|
||||||
|
|
||||||
|
TrackList::pointer tracklist;
|
||||||
|
if (id)
|
||||||
|
{
|
||||||
|
tracklist = TrackList::find(context.dbSession, *id);
|
||||||
|
if (!tracklist
|
||||||
|
|| tracklist->getUser() != user
|
||||||
|
|| tracklist->getType() != TrackListType::Playlist)
|
||||||
|
{
|
||||||
|
throw RequestedDataNotFoundError{};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name)
|
||||||
|
tracklist.modify()->setName(*name);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
tracklist = context.dbSession.create<TrackList>(*name, TrackListType::Playlist, false, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const TrackId trackId : trackIds)
|
||||||
|
{
|
||||||
|
Track::pointer track{ Track::find(context.dbSession, trackId) };
|
||||||
|
if (!track)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
context.dbSession.create<TrackListEntry>(track, tracklist);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response::createOkResponse(context.serverProtocolVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
Response handleUpdatePlaylistRequest(RequestContext& context)
|
||||||
|
{
|
||||||
|
// Mandatory params
|
||||||
|
TrackListId id{ getMandatoryParameterAs<TrackListId>(context.parameters, "playlistId") };
|
||||||
|
|
||||||
|
// Optional parameters
|
||||||
|
auto name{ getParameterAs<std::string>(context.parameters, "name") };
|
||||||
|
auto isPublic{ getParameterAs<bool>(context.parameters, "public") };
|
||||||
|
|
||||||
|
std::vector<TrackId> trackIdsToAdd{ getMultiParametersAs<TrackId>(context.parameters, "songIdToAdd") };
|
||||||
|
std::vector<std::size_t> trackPositionsToRemove{ getMultiParametersAs<std::size_t>(context.parameters, "songIndexToRemove") };
|
||||||
|
|
||||||
|
auto transaction{ context.dbSession.createUniqueTransaction() };
|
||||||
|
|
||||||
|
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||||
|
if (!user)
|
||||||
|
throw UserNotAuthorizedError{};
|
||||||
|
|
||||||
|
TrackList::pointer tracklist{ TrackList::find(context.dbSession, id) };
|
||||||
|
if (!tracklist
|
||||||
|
|| tracklist->getUser() != user
|
||||||
|
|| tracklist->getType() != TrackListType::Playlist)
|
||||||
|
{
|
||||||
|
throw RequestedDataNotFoundError{};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name)
|
||||||
|
tracklist.modify()->setName(*name);
|
||||||
|
|
||||||
|
if (isPublic)
|
||||||
|
tracklist.modify()->setIsPublic(*isPublic);
|
||||||
|
|
||||||
|
{
|
||||||
|
// Remove from end to make indexes stable
|
||||||
|
std::sort(std::begin(trackPositionsToRemove), std::end(trackPositionsToRemove), std::greater<std::size_t>());
|
||||||
|
|
||||||
|
for (std::size_t trackPositionToRemove : trackPositionsToRemove)
|
||||||
|
{
|
||||||
|
auto entry{ tracklist->getEntry(trackPositionToRemove) };
|
||||||
|
if (entry)
|
||||||
|
entry.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add tracks
|
||||||
|
for (const TrackId trackIdToAdd : trackIdsToAdd)
|
||||||
|
{
|
||||||
|
Track::pointer track{ Track::find(context.dbSession, trackIdToAdd) };
|
||||||
|
if (!track)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
context.dbSession.create<TrackListEntry>(track, tracklist);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response::createOkResponse(context.serverProtocolVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
Response handleDeletePlaylistRequest(RequestContext& context)
|
||||||
|
{
|
||||||
|
TrackListId id{ getMandatoryParameterAs<TrackListId>(context.parameters, "id") };
|
||||||
|
|
||||||
|
auto transaction{ context.dbSession.createUniqueTransaction() };
|
||||||
|
|
||||||
|
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||||
|
if (!user)
|
||||||
|
throw UserNotAuthorizedError{};
|
||||||
|
|
||||||
|
TrackList::pointer tracklist{ TrackList::find(context.dbSession, id) };
|
||||||
|
if (!tracklist
|
||||||
|
|| tracklist->getUser() != user
|
||||||
|
|| tracklist->getType() != TrackListType::Playlist)
|
||||||
|
{
|
||||||
|
throw RequestedDataNotFoundError{};
|
||||||
|
}
|
||||||
|
|
||||||
|
tracklist.remove();
|
||||||
|
|
||||||
|
return Response::createOkResponse(context.serverProtocolVersion);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "RequestContext.hpp"
|
||||||
|
#include "SubsonicResponse.hpp"
|
||||||
|
|
||||||
|
namespace API::Subsonic
|
||||||
|
{
|
||||||
|
Response handleGetPlaylistsRequest(RequestContext& context);
|
||||||
|
Response handleGetPlaylistRequest(RequestContext& context);
|
||||||
|
Response handleCreatePlaylistRequest(RequestContext& context);
|
||||||
|
Response handleUpdatePlaylistRequest(RequestContext& context);
|
||||||
|
Response handleDeletePlaylistRequest(RequestContext& context);
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2020 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "Searching.hpp"
|
||||||
|
|
||||||
|
#include "services/database/Artist.hpp"
|
||||||
|
#include "services/database/Release.hpp"
|
||||||
|
#include "services/database/Session.hpp"
|
||||||
|
#include "services/database/Track.hpp"
|
||||||
|
#include "services/database/User.hpp"
|
||||||
|
#include "responses/Album.hpp"
|
||||||
|
#include "responses/Artist.hpp"
|
||||||
|
#include "responses/Song.hpp"
|
||||||
|
#include "ParameterParsing.hpp"
|
||||||
|
|
||||||
|
#include "ParameterParsing.hpp"
|
||||||
|
|
||||||
|
namespace API::Subsonic
|
||||||
|
{
|
||||||
|
using namespace Database;
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
Response handleSearchRequestCommon(RequestContext& context, bool id3)
|
||||||
|
{
|
||||||
|
// Mandatory params
|
||||||
|
std::string queryString{ getMandatoryParameterAs<std::string>(context.parameters, "query") };
|
||||||
|
std::string_view query{ queryString };
|
||||||
|
|
||||||
|
// Symfonium adds extra ""
|
||||||
|
if (context.clientInfo.name == "Symfonium")
|
||||||
|
query = StringUtils::stringTrim(query, "\"");
|
||||||
|
|
||||||
|
std::vector<std::string_view> keywords{ StringUtils::splitString(query, " ") };
|
||||||
|
|
||||||
|
// Optional params
|
||||||
|
std::size_t artistCount{ getParameterAs<std::size_t>(context.parameters, "artistCount").value_or(20) };
|
||||||
|
std::size_t artistOffset{ getParameterAs<std::size_t>(context.parameters, "artistOffset").value_or(0) };
|
||||||
|
std::size_t albumCount{ getParameterAs<std::size_t>(context.parameters, "albumCount").value_or(20) };
|
||||||
|
std::size_t albumOffset{ getParameterAs<std::size_t>(context.parameters, "albumOffset").value_or(0) };
|
||||||
|
std::size_t songCount{ getParameterAs<std::size_t>(context.parameters, "songCount").value_or(20) };
|
||||||
|
std::size_t songOffset{ getParameterAs<std::size_t>(context.parameters, "songOffset").value_or(0) };
|
||||||
|
|
||||||
|
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||||
|
|
||||||
|
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||||
|
if (!user)
|
||||||
|
throw UserNotAuthorizedError{};
|
||||||
|
|
||||||
|
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||||
|
Response::Node& searchResult2Node{ response.createNode(id3 ? "searchResult3" : "searchResult2") };
|
||||||
|
|
||||||
|
if (artistCount > 0)
|
||||||
|
{
|
||||||
|
Artist::FindParameters params;
|
||||||
|
params.setKeywords(keywords);
|
||||||
|
params.setSortMethod(ArtistSortMethod::BySortName);
|
||||||
|
params.setRange({ artistOffset, artistCount });
|
||||||
|
|
||||||
|
RangeResults<ArtistId> artistIds{ Artist::find(context.dbSession, params) };
|
||||||
|
for (const ArtistId artistId : artistIds.results)
|
||||||
|
{
|
||||||
|
const auto artist{ Artist::find(context.dbSession, artistId) };
|
||||||
|
searchResult2Node.addArrayChild("artist", createArtistNode(artist, context.dbSession, user, id3));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (albumCount > 0)
|
||||||
|
{
|
||||||
|
Release::FindParameters params;
|
||||||
|
params.setKeywords(keywords);
|
||||||
|
params.setSortMethod(ReleaseSortMethod::Name);
|
||||||
|
params.setRange({ albumOffset, albumCount });
|
||||||
|
|
||||||
|
RangeResults<ReleaseId> releaseIds{ Release::find(context.dbSession, params) };
|
||||||
|
for (const ReleaseId releaseId : releaseIds.results)
|
||||||
|
{
|
||||||
|
const auto release{ Release::find(context.dbSession, releaseId) };
|
||||||
|
searchResult2Node.addArrayChild("album", createAlbumNode(release, context.dbSession, user, id3));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (songCount > 0)
|
||||||
|
{
|
||||||
|
Track::FindParameters params;
|
||||||
|
params.setKeywords(keywords);
|
||||||
|
params.setRange({ songOffset, songCount });
|
||||||
|
|
||||||
|
RangeResults<TrackId> trackIds{ Track::find(context.dbSession, params) };
|
||||||
|
for (const TrackId trackId : trackIds.results)
|
||||||
|
{
|
||||||
|
const auto track{ Track::find(context.dbSession, trackId) };
|
||||||
|
searchResult2Node.addArrayChild("song", createSongNode(track, context.dbSession, user));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Response handleSearch2Request(RequestContext& context)
|
||||||
|
{
|
||||||
|
return handleSearchRequestCommon(context, false /* no id3 */);
|
||||||
|
}
|
||||||
|
|
||||||
|
Response handleSearch3Request(RequestContext& context)
|
||||||
|
{
|
||||||
|
return handleSearchRequestCommon(context, true /* id3 */);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "RequestContext.hpp"
|
||||||
|
#include "SubsonicResponse.hpp"
|
||||||
|
|
||||||
|
namespace API::Subsonic
|
||||||
|
{
|
||||||
|
Response handleSearch2Request(RequestContext& context);
|
||||||
|
Response handleSearch3Request(RequestContext& context);
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
#include "entrypoints/System.hpp"
|
||||||
|
|
||||||
|
namespace API::Subsonic
|
||||||
|
{
|
||||||
|
Response handlePingRequest(RequestContext& context)
|
||||||
|
{
|
||||||
|
return Response::createOkResponse(context.serverProtocolVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
Response handleGetLicenseRequest(RequestContext& context)
|
||||||
|
{
|
||||||
|
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||||
|
|
||||||
|
Response::Node& licenseNode{ response.createNode("license") };
|
||||||
|
licenseNode.setAttribute("licenseExpires", "2025-09-03T14:46:43");
|
||||||
|
licenseNode.setAttribute("email", "foo@bar.com");
|
||||||
|
licenseNode.setAttribute("valid", true);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
Response handleGetOpenSubsonicExtensions(RequestContext& context)
|
||||||
|
{
|
||||||
|
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||||
|
|
||||||
|
{
|
||||||
|
Response::Node& transcodeOffsetNode{ response.createArrayNode("openSubsonicExtensions") };
|
||||||
|
transcodeOffsetNode.setAttribute("name", "transcodeOffset");
|
||||||
|
transcodeOffsetNode.addArrayValue("versions", 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "RequestContext.hpp"
|
||||||
|
#include "SubsonicResponse.hpp"
|
||||||
|
|
||||||
|
namespace API::Subsonic
|
||||||
|
{
|
||||||
|
Response handlePingRequest(RequestContext& context);
|
||||||
|
Response handleGetLicenseRequest(RequestContext& context);
|
||||||
|
Response handleGetOpenSubsonicExtensions(RequestContext& context);
|
||||||
|
}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
#include "UserManagement.hpp"
|
||||||
|
|
||||||
|
#include "services/database/Session.hpp"
|
||||||
|
#include "services/database/User.hpp"
|
||||||
|
#include "services/auth/IPasswordService.hpp"
|
||||||
|
#include "utils/Service.hpp"
|
||||||
|
#include "responses/User.hpp"
|
||||||
|
#include "ParameterParsing.hpp"
|
||||||
|
#include "Utils.hpp"
|
||||||
|
|
||||||
|
namespace API::Subsonic
|
||||||
|
{
|
||||||
|
using namespace Database;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
void checkUserIsMySelfOrAdmin(RequestContext& context, const std::string& username)
|
||||||
|
{
|
||||||
|
User::pointer currentUser{ User::find(context.dbSession, context.userId) };
|
||||||
|
if (!currentUser)
|
||||||
|
throw RequestedDataNotFoundError{};
|
||||||
|
|
||||||
|
if (currentUser->getLoginName() != username && !currentUser->isAdmin())
|
||||||
|
throw UserNotAuthorizedError{};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Response handleGetUserRequest(RequestContext& context)
|
||||||
|
{
|
||||||
|
std::string username{ getMandatoryParameterAs<std::string>(context.parameters, "username") };
|
||||||
|
|
||||||
|
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||||
|
|
||||||
|
checkUserIsMySelfOrAdmin(context, username);
|
||||||
|
|
||||||
|
const User::pointer user{ User::find(context.dbSession, username) };
|
||||||
|
if (!user)
|
||||||
|
throw RequestedDataNotFoundError{};
|
||||||
|
|
||||||
|
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||||
|
response.addNode("user", createUserNode(user));
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
Response handleGetUsersRequest(RequestContext& context)
|
||||||
|
{
|
||||||
|
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||||
|
|
||||||
|
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||||
|
Response::Node& usersNode{ response.createNode("users") };
|
||||||
|
|
||||||
|
const auto userIds{ User::find(context.dbSession, User::FindParameters {}) };
|
||||||
|
for (const UserId userId : userIds.results)
|
||||||
|
{
|
||||||
|
const User::pointer user{ User::find(context.dbSession, userId) };
|
||||||
|
usersNode.addArrayChild("user", createUserNode(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
Response handleCreateUserRequest(RequestContext& context)
|
||||||
|
{
|
||||||
|
std::string username{ getMandatoryParameterAs<std::string>(context.parameters, "username") };
|
||||||
|
std::string password{ decodePasswordIfNeeded(getMandatoryParameterAs<std::string>(context.parameters, "password")) };
|
||||||
|
// Just ignore all the other fields as we don't handle them
|
||||||
|
|
||||||
|
Database::UserId userId;
|
||||||
|
{
|
||||||
|
auto transaction{ context.dbSession.createUniqueTransaction() };
|
||||||
|
|
||||||
|
User::pointer user{ User::find(context.dbSession, username) };
|
||||||
|
if (user)
|
||||||
|
throw UserAlreadyExistsGenericError{};
|
||||||
|
|
||||||
|
user = context.dbSession.create<User>(username);
|
||||||
|
userId = user->getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto removeCreatedUser{ [&]()
|
||||||
|
{
|
||||||
|
auto transaction {context.dbSession.createUniqueTransaction()};
|
||||||
|
User::pointer user {User::find(context.dbSession, userId)};
|
||||||
|
if (user)
|
||||||
|
user.remove();
|
||||||
|
} };
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Service<Auth::IPasswordService>::get()->setPassword(userId, password);
|
||||||
|
}
|
||||||
|
catch (const Auth::PasswordMustMatchLoginNameException&)
|
||||||
|
{
|
||||||
|
removeCreatedUser();
|
||||||
|
throw PasswordMustMatchLoginNameGenericError{};
|
||||||
|
}
|
||||||
|
catch (const Auth::PasswordTooWeakException&)
|
||||||
|
{
|
||||||
|
removeCreatedUser();
|
||||||
|
throw PasswordTooWeakGenericError{};
|
||||||
|
}
|
||||||
|
catch (const Auth::Exception& exception)
|
||||||
|
{
|
||||||
|
removeCreatedUser();
|
||||||
|
throw UserNotAuthorizedError{};
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response::createOkResponse(context.serverProtocolVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
Response handleDeleteUserRequest(RequestContext& context)
|
||||||
|
{
|
||||||
|
std::string username{ getMandatoryParameterAs<std::string>(context.parameters, "username") };
|
||||||
|
|
||||||
|
auto transaction{ context.dbSession.createUniqueTransaction() };
|
||||||
|
|
||||||
|
User::pointer user{ User::find(context.dbSession, username) };
|
||||||
|
if (!user)
|
||||||
|
throw RequestedDataNotFoundError{};
|
||||||
|
|
||||||
|
// cannot delete ourself
|
||||||
|
if (user->getId() == context.userId)
|
||||||
|
throw UserNotAuthorizedError{};
|
||||||
|
|
||||||
|
user.remove();
|
||||||
|
|
||||||
|
return Response::createOkResponse(context.serverProtocolVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
Response handleUpdateUserRequest(RequestContext& context)
|
||||||
|
{
|
||||||
|
std::string username{ getMandatoryParameterAs<std::string>(context.parameters, "username") };
|
||||||
|
std::optional<std::string> password{ getParameterAs<std::string>(context.parameters, "password") };
|
||||||
|
|
||||||
|
UserId userId;
|
||||||
|
{
|
||||||
|
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||||
|
|
||||||
|
User::pointer user{ User::find(context.dbSession, username) };
|
||||||
|
if (!user)
|
||||||
|
throw RequestedDataNotFoundError{};
|
||||||
|
|
||||||
|
userId = user->getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (password)
|
||||||
|
{
|
||||||
|
Utils::checkSetPasswordImplemented();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Service<::Auth::IPasswordService>()->setPassword(userId, decodePasswordIfNeeded(*password));
|
||||||
|
}
|
||||||
|
catch (const Auth::PasswordMustMatchLoginNameException&)
|
||||||
|
{
|
||||||
|
throw PasswordMustMatchLoginNameGenericError{};
|
||||||
|
}
|
||||||
|
catch (const Auth::PasswordTooWeakException&)
|
||||||
|
{
|
||||||
|
throw PasswordTooWeakGenericError{};
|
||||||
|
}
|
||||||
|
catch (const Auth::Exception&)
|
||||||
|
{
|
||||||
|
throw UserNotAuthorizedError{};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response::createOkResponse(context.serverProtocolVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
Response handleChangePassword(RequestContext& context)
|
||||||
|
{
|
||||||
|
std::string username{ getMandatoryParameterAs<std::string>(context.parameters, "username") };
|
||||||
|
std::string password{ decodePasswordIfNeeded(getMandatoryParameterAs<std::string>(context.parameters, "password")) };
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Database::UserId userId;
|
||||||
|
{
|
||||||
|
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||||
|
|
||||||
|
checkUserIsMySelfOrAdmin(context, username);
|
||||||
|
|
||||||
|
User::pointer user{ User::find(context.dbSession, username) };
|
||||||
|
if (!user)
|
||||||
|
throw UserNotAuthorizedError{};
|
||||||
|
|
||||||
|
userId = user->getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
Service<Auth::IPasswordService>::get()->setPassword(userId, password);
|
||||||
|
}
|
||||||
|
catch (const Auth::PasswordMustMatchLoginNameException&)
|
||||||
|
{
|
||||||
|
throw PasswordMustMatchLoginNameGenericError{};
|
||||||
|
}
|
||||||
|
catch (const Auth::PasswordTooWeakException&)
|
||||||
|
{
|
||||||
|
throw PasswordTooWeakGenericError{};
|
||||||
|
}
|
||||||
|
catch (const Auth::Exception& authException)
|
||||||
|
{
|
||||||
|
throw UserNotAuthorizedError{};
|
||||||
|
}
|
||||||
|
|
||||||
|
return Response::createOkResponse(context.serverProtocolVersion);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "RequestContext.hpp"
|
||||||
|
#include "SubsonicResponse.hpp"
|
||||||
|
|
||||||
|
namespace API::Subsonic
|
||||||
|
{
|
||||||
|
Response handleGetUserRequest(RequestContext& context);
|
||||||
|
Response handleGetUsersRequest(RequestContext& context);
|
||||||
|
Response handleCreateUserRequest(RequestContext& context);
|
||||||
|
Response handleUpdateUserRequest(RequestContext& context);
|
||||||
|
Response handleDeleteUserRequest(RequestContext& context);
|
||||||
|
Response handleChangePassword(RequestContext& context);
|
||||||
|
}
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "responses/Album.hpp"
|
||||||
|
|
||||||
|
#include "services/database/Cluster.hpp"
|
||||||
|
#include "services/database/Artist.hpp"
|
||||||
|
#include "services/database/Release.hpp"
|
||||||
|
#include "services/database/User.hpp"
|
||||||
|
#include "services/scrobbling/IScrobblingService.hpp"
|
||||||
|
#include "utils/Service.hpp"
|
||||||
|
#include "utils/String.hpp"
|
||||||
|
|
||||||
|
#include "responses/Artist.hpp"
|
||||||
|
#include "responses/DiscTitle.hpp"
|
||||||
|
#include "responses/ItemGenre.hpp"
|
||||||
|
#include "SubsonicId.hpp"
|
||||||
|
|
||||||
|
namespace API::Subsonic
|
||||||
|
{
|
||||||
|
using namespace Database;
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
std::string_view toString(ReleaseTypePrimary releaseType)
|
||||||
|
{
|
||||||
|
switch (releaseType)
|
||||||
|
{
|
||||||
|
case ReleaseTypePrimary::Album: return "album";
|
||||||
|
case ReleaseTypePrimary::Broadcast: return "broadcast";
|
||||||
|
case ReleaseTypePrimary::EP: return "ep";
|
||||||
|
case ReleaseTypePrimary::Single: return "single";
|
||||||
|
case ReleaseTypePrimary::Other: return "other";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string_view toString(ReleaseTypeSecondary releaseType)
|
||||||
|
{
|
||||||
|
switch (releaseType)
|
||||||
|
{
|
||||||
|
case ReleaseTypeSecondary::Audiobook: return "audiobook";
|
||||||
|
case ReleaseTypeSecondary::AudioDrama: return "audiodrama";
|
||||||
|
case ReleaseTypeSecondary::Compilation: return "compilation";
|
||||||
|
case ReleaseTypeSecondary::Demo: return "demo";
|
||||||
|
case ReleaseTypeSecondary::DJMix: return "djmix";
|
||||||
|
case ReleaseTypeSecondary::Interview: return "interview";
|
||||||
|
case ReleaseTypeSecondary::Live: return "live";
|
||||||
|
case ReleaseTypeSecondary::Mixtape_Street: return "mixtapestreet";
|
||||||
|
case ReleaseTypeSecondary::Remix: return "remix";
|
||||||
|
case ReleaseTypeSecondary::Soundtrack: return "soundtrack";
|
||||||
|
case ReleaseTypeSecondary::Spokenword: return "soundtrack";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Response::Node createAlbumNode(const Release::pointer& release, Session& dbSession, const User::pointer& user, bool id3)
|
||||||
|
{
|
||||||
|
Response::Node albumNode;
|
||||||
|
|
||||||
|
if (id3) {
|
||||||
|
albumNode.setAttribute("name", release->getName());
|
||||||
|
albumNode.setAttribute("songCount", release->getTracksCount());
|
||||||
|
albumNode.setAttribute(
|
||||||
|
"duration", std::chrono::duration_cast<std::chrono::seconds>(
|
||||||
|
release->getDuration())
|
||||||
|
.count());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
albumNode.setAttribute("title", release->getName());
|
||||||
|
albumNode.setAttribute("isDir", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
albumNode.setAttribute("created", StringUtils::toISO8601String(release->getLastWritten()));
|
||||||
|
albumNode.setAttribute("id", idToString(release->getId()));
|
||||||
|
albumNode.setAttribute("coverArt", idToString(release->getId()));
|
||||||
|
if (const Wt::WDate releaseDate{ release->getReleaseDate() }; releaseDate.isValid())
|
||||||
|
albumNode.setAttribute("year", releaseDate.year());
|
||||||
|
|
||||||
|
auto artists{ release->getReleaseArtists() };
|
||||||
|
if (artists.empty())
|
||||||
|
artists = release->getArtists();
|
||||||
|
|
||||||
|
if (artists.empty() && !id3)
|
||||||
|
{
|
||||||
|
albumNode.setAttribute("parent", idToString(RootId{}));
|
||||||
|
}
|
||||||
|
else if (!artists.empty())
|
||||||
|
{
|
||||||
|
albumNode.setAttribute("artist", Utils::joinArtistNames(artists));
|
||||||
|
|
||||||
|
if (artists.size() == 1)
|
||||||
|
{
|
||||||
|
albumNode.setAttribute(id3 ? "artistId" : "parent", idToString(artists.front()->getId()));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!id3)
|
||||||
|
albumNode.setAttribute("parent", idToString(RootId{}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report the first GENRE for this track
|
||||||
|
if (ClusterType::pointer clusterType{ ClusterType::find(dbSession, "GENRE") })
|
||||||
|
{
|
||||||
|
auto clusters{ release->getClusterGroups({clusterType}, 1) };
|
||||||
|
if (!clusters.empty() && !clusters.front().empty())
|
||||||
|
albumNode.setAttribute("genre", clusters.front().front()->getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (const Wt::WDateTime dateTime{ Service<Scrobbling::IScrobblingService>::get()->getStarredDateTime(user->getId(), release->getId()) }; dateTime.isValid())
|
||||||
|
albumNode.setAttribute("starred", StringUtils::toISO8601String(dateTime)); // TODO report correct date/time
|
||||||
|
|
||||||
|
// OpenSubsonic specific fields (must always be set)
|
||||||
|
if (!id3)
|
||||||
|
albumNode.setAttribute("mediaType", "album");
|
||||||
|
|
||||||
|
{
|
||||||
|
std::optional<UUID> mbid{ release->getMBID() };
|
||||||
|
albumNode.setAttribute("musicBrainzId", mbid ? mbid->getAsString() : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto addClusters{ [&](std::string_view field, std::string_view clusterTypeName)
|
||||||
|
{
|
||||||
|
albumNode.createEmptyArrayValue(field);
|
||||||
|
|
||||||
|
ClusterType::pointer clusterType{ ClusterType::find(dbSession, clusterTypeName) };
|
||||||
|
if (clusterType)
|
||||||
|
{
|
||||||
|
Cluster::FindParameters params;
|
||||||
|
params.setRelease(release->getId());
|
||||||
|
params.setClusterType(clusterType->getId());
|
||||||
|
|
||||||
|
for (const ClusterId clusterId : Cluster::find(dbSession, params).results)
|
||||||
|
{
|
||||||
|
Cluster::pointer cluster{ Cluster::find(dbSession, clusterId) };
|
||||||
|
if (cluster)
|
||||||
|
albumNode.addArrayValue(field, cluster->getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} };
|
||||||
|
|
||||||
|
addClusters("moods", "MOOD");
|
||||||
|
|
||||||
|
// Genres
|
||||||
|
{
|
||||||
|
albumNode.createEmptyArrayChild("genres");
|
||||||
|
|
||||||
|
ClusterType::pointer clusterType{ ClusterType::find(dbSession, "GENRE") };
|
||||||
|
if (clusterType)
|
||||||
|
{
|
||||||
|
Cluster::FindParameters params;
|
||||||
|
params.setRelease(release->getId());
|
||||||
|
params.setClusterType(clusterType->getId());
|
||||||
|
|
||||||
|
for (const ClusterId clusterId : Cluster::find(dbSession, params).results)
|
||||||
|
{
|
||||||
|
Cluster::pointer cluster{ Cluster::find(dbSession, clusterId) };
|
||||||
|
if (cluster)
|
||||||
|
albumNode.addArrayChild("genres", createItemGenreNode(cluster));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
albumNode.createEmptyArrayChild("artists");
|
||||||
|
for (const Artist::pointer& artist : release->getReleaseArtists())
|
||||||
|
albumNode.addArrayChild("artists", createArtistNode(artist));
|
||||||
|
|
||||||
|
{
|
||||||
|
const Wt::WDate originalReleaseDate{ release->getOriginalReleaseDate() };
|
||||||
|
albumNode.setAttribute("originalReleaseDate", originalReleaseDate.isValid() ? StringUtils::toISO8601String(originalReleaseDate) : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
albumNode.setAttribute("isCompilation", release->getSecondaryTypes().contains(ReleaseTypeSecondary::Compilation));
|
||||||
|
albumNode.createEmptyArrayValue("releaseTypes");
|
||||||
|
if (auto releaseType{ release->getPrimaryType() })
|
||||||
|
albumNode.addArrayValue("releaseTypes", toString(*releaseType));
|
||||||
|
for (const ReleaseTypeSecondary releaseType : release->getSecondaryTypes())
|
||||||
|
albumNode.addArrayValue("releaseTypes", toString(releaseType));
|
||||||
|
|
||||||
|
// disc titles
|
||||||
|
albumNode.createEmptyArrayChild("discTitles");
|
||||||
|
for (const DiscInfo& discInfo : release->getDiscs())
|
||||||
|
{
|
||||||
|
if (!discInfo.name.empty())
|
||||||
|
albumNode.addArrayChild("discTitles", createDiscTitle(discInfo));
|
||||||
|
}
|
||||||
|
|
||||||
|
return albumNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "services/database/Object.hpp"
|
||||||
|
#include "SubsonicResponse.hpp"
|
||||||
|
|
||||||
|
namespace Database
|
||||||
|
{
|
||||||
|
class Release;
|
||||||
|
class User;
|
||||||
|
class Session;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace API::Subsonic
|
||||||
|
{
|
||||||
|
Response::Node createAlbumNode(const Database::ObjectPtr<Database::Release>& release, Database::Session& dbSession, const Database::ObjectPtr<Database::User>& user, bool id3);
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "responses/Artist.hpp"
|
||||||
|
|
||||||
|
#include "services/database/Artist.hpp"
|
||||||
|
#include "services/database/Release.hpp"
|
||||||
|
#include "services/database/TrackArtistLink.hpp"
|
||||||
|
#include "services/database/User.hpp"
|
||||||
|
#include "services/scrobbling/IScrobblingService.hpp"
|
||||||
|
#include "utils/Service.hpp"
|
||||||
|
#include "utils/String.hpp"
|
||||||
|
|
||||||
|
#include "SubsonicId.hpp"
|
||||||
|
|
||||||
|
namespace API::Subsonic
|
||||||
|
{
|
||||||
|
using namespace Database;
|
||||||
|
|
||||||
|
namespace Utils
|
||||||
|
{
|
||||||
|
std::string joinArtistNames(const std::vector<Artist::pointer>& artists)
|
||||||
|
{
|
||||||
|
if (artists.size() == 1)
|
||||||
|
return artists.front()->getName();
|
||||||
|
|
||||||
|
std::vector<std::string> names;
|
||||||
|
names.resize(artists.size());
|
||||||
|
|
||||||
|
std::transform(std::cbegin(artists), std::cend(artists), std::begin(names),
|
||||||
|
[](const Artist::pointer& artist)
|
||||||
|
{
|
||||||
|
return artist->getName();
|
||||||
|
});
|
||||||
|
|
||||||
|
return StringUtils::joinStrings(names, ", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string_view toString(TrackArtistLinkType type)
|
||||||
|
{
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case TrackArtistLinkType::Arranger: return "arranger";
|
||||||
|
case TrackArtistLinkType::Artist: return "artist";
|
||||||
|
case TrackArtistLinkType::Composer: return "composer";
|
||||||
|
case TrackArtistLinkType::Conductor: return "conductor";
|
||||||
|
case TrackArtistLinkType::Lyricist: return "lyricist";
|
||||||
|
case TrackArtistLinkType::Mixer: return "mixer";
|
||||||
|
case TrackArtistLinkType::Performer: return "performer";
|
||||||
|
case TrackArtistLinkType::Producer: return "producer";
|
||||||
|
case TrackArtistLinkType::ReleaseArtist: return "albumartist";
|
||||||
|
case TrackArtistLinkType::Remixer: return "remixer";
|
||||||
|
case TrackArtistLinkType::Writer: return "writer";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Response::Node createArtistNode(const Artist::pointer& artist, Session& session, const User::pointer& user, bool id3)
|
||||||
|
{
|
||||||
|
Response::Node artistNode{ createArtistNode(artist) };
|
||||||
|
|
||||||
|
artistNode.setAttribute("id", idToString(artist->getId()));
|
||||||
|
artistNode.setAttribute("name", artist->getName());
|
||||||
|
|
||||||
|
if (id3)
|
||||||
|
{
|
||||||
|
const auto releases{ Release::find(session, Release::FindParameters {}.setArtist(artist->getId())) };
|
||||||
|
artistNode.setAttribute("albumCount", releases.results.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (const Wt::WDateTime dateTime{ Service<Scrobbling::IScrobblingService>::get()->getStarredDateTime(user->getId(), artist->getId()) }; dateTime.isValid())
|
||||||
|
artistNode.setAttribute("starred", StringUtils::toISO8601String(dateTime));
|
||||||
|
|
||||||
|
// OpenSubsonic specific fields (must always be set)
|
||||||
|
if (!id3)
|
||||||
|
artistNode.setAttribute("mediaType", "artist");
|
||||||
|
|
||||||
|
{
|
||||||
|
std::optional<UUID> mbid{ artist->getMBID() };
|
||||||
|
artistNode.setAttribute("musicBrainzId", mbid ? mbid->getAsString() : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
artistNode.setAttribute("sortName", artist->getSortName());
|
||||||
|
|
||||||
|
// roles
|
||||||
|
Response::Node roles;
|
||||||
|
artistNode.createEmptyArrayValue("roles");
|
||||||
|
for (const TrackArtistLinkType linkType : TrackArtistLink::findUsedTypes(session, artist->getId()))
|
||||||
|
artistNode.addArrayValue("roles", Utils::toString(linkType));
|
||||||
|
|
||||||
|
return artistNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
Response::Node createArtistNode(const Artist::pointer& artist)
|
||||||
|
{
|
||||||
|
Response::Node artistNode;
|
||||||
|
|
||||||
|
artistNode.setAttribute("id", idToString(artist->getId()));
|
||||||
|
artistNode.setAttribute("name", artist->getName());
|
||||||
|
|
||||||
|
return artistNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include "services/database/Object.hpp"
|
||||||
|
#include "services/database/Types.hpp"
|
||||||
|
#include "SubsonicResponse.hpp"
|
||||||
|
|
||||||
|
namespace Database
|
||||||
|
{
|
||||||
|
class Artist;
|
||||||
|
class User;
|
||||||
|
class Session;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace API::Subsonic
|
||||||
|
{
|
||||||
|
namespace Utils
|
||||||
|
{
|
||||||
|
std::string joinArtistNames(const std::vector<Database::ObjectPtr<Database::Artist>>& artists);
|
||||||
|
std::string_view toString(Database::TrackArtistLinkType type);
|
||||||
|
}
|
||||||
|
Response::Node createArtistNode(const Database::ObjectPtr<Database::Artist>& artist, Database::Session& session, const Database::ObjectPtr<Database::User>& user, bool id3);
|
||||||
|
Response::Node createArtistNode(const Database::ObjectPtr<Database::Artist>& artist); // only minimal info
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "responses/Bookmark.hpp"
|
||||||
|
|
||||||
|
#include "services/database/TrackBookmark.hpp"
|
||||||
|
#include "services/database/User.hpp"
|
||||||
|
|
||||||
|
namespace API::Subsonic
|
||||||
|
{
|
||||||
|
static const std::string_view reportedDummyDate{ "2000-01-01T00:00:00" };
|
||||||
|
|
||||||
|
Response::Node createBookmarkNode(const Database::ObjectPtr<Database::TrackBookmark>& trackBookmark)
|
||||||
|
{
|
||||||
|
Response::Node trackBookmarkNode;
|
||||||
|
|
||||||
|
trackBookmarkNode.setAttribute("position", trackBookmark->getOffset().count());
|
||||||
|
if (!trackBookmark->getComment().empty())
|
||||||
|
trackBookmarkNode.setAttribute("comment", trackBookmark->getComment());
|
||||||
|
trackBookmarkNode.setAttribute("created", reportedDummyDate);
|
||||||
|
trackBookmarkNode.setAttribute("changed", reportedDummyDate);
|
||||||
|
trackBookmarkNode.setAttribute("username", trackBookmark->getUser()->getLoginName());
|
||||||
|
|
||||||
|
return trackBookmarkNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "services/database/Object.hpp"
|
||||||
|
#include "SubsonicResponse.hpp"
|
||||||
|
|
||||||
|
namespace Database
|
||||||
|
{
|
||||||
|
class TrackBookmark;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace API::Subsonic
|
||||||
|
{
|
||||||
|
Response::Node createBookmarkNode(const Database::ObjectPtr<Database::TrackBookmark>& bookmark);
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "responses/Contributor.hpp"
|
||||||
|
|
||||||
|
#include "services/database/Object.hpp"
|
||||||
|
#include "services/database/TrackArtistLink.hpp"
|
||||||
|
#include "SubsonicResponse.hpp"
|
||||||
|
#include "responses/Artist.hpp"
|
||||||
|
|
||||||
|
namespace API::Subsonic
|
||||||
|
{
|
||||||
|
Response::Node createContributorNode(const Database::ObjectPtr<Database::TrackArtistLink>& trackArtistLink)
|
||||||
|
{
|
||||||
|
Response::Node contributorNode;
|
||||||
|
|
||||||
|
contributorNode.setAttribute("role", Utils::toString(trackArtistLink->getType()));
|
||||||
|
if (!trackArtistLink->getSubType().empty())
|
||||||
|
contributorNode.setAttribute("subRole", trackArtistLink->getSubType());
|
||||||
|
contributorNode.addChild("artist", createArtistNode(trackArtistLink->getArtist()));
|
||||||
|
|
||||||
|
return contributorNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "services/database/Object.hpp"
|
||||||
|
#include "SubsonicResponse.hpp"
|
||||||
|
|
||||||
|
namespace Database
|
||||||
|
{
|
||||||
|
class TrackArtistLink;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace API::Subsonic
|
||||||
|
{
|
||||||
|
Response::Node createContributorNode(const Database::ObjectPtr<Database::TrackArtistLink>& trackArtistLink);
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "responses/DiscTitle.hpp"
|
||||||
|
|
||||||
|
namespace API::Subsonic
|
||||||
|
{
|
||||||
|
Response::Node createDiscTitle(const Database::DiscInfo& discInfo)
|
||||||
|
{
|
||||||
|
Response::Node discTitleNode;
|
||||||
|
|
||||||
|
discTitleNode.setAttribute("disc", discInfo.position);
|
||||||
|
discTitleNode.setAttribute("title", discInfo.name);
|
||||||
|
|
||||||
|
return discTitleNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "services/database/Types.hpp"
|
||||||
|
#include "SubsonicResponse.hpp"
|
||||||
|
|
||||||
|
namespace API::Subsonic
|
||||||
|
{
|
||||||
|
Response::Node createDiscTitle(const Database::DiscInfo& discInfo);
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "responses/Genre.hpp"
|
||||||
|
|
||||||
|
#include "services/database/Cluster.hpp"
|
||||||
|
|
||||||
|
namespace API::Subsonic
|
||||||
|
{
|
||||||
|
Response::Node createGenreNode(const Database::Cluster::pointer& cluster)
|
||||||
|
{
|
||||||
|
Response::Node clusterNode;
|
||||||
|
|
||||||
|
clusterNode.setValue(cluster->getName());
|
||||||
|
clusterNode.setAttribute("songCount", cluster->getTracksCount());
|
||||||
|
clusterNode.setAttribute("albumCount", cluster->getReleasesCount());
|
||||||
|
|
||||||
|
return clusterNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "services/database/Object.hpp"
|
||||||
|
#include "SubsonicResponse.hpp"
|
||||||
|
|
||||||
|
namespace Database
|
||||||
|
{
|
||||||
|
class Cluster;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace API::Subsonic
|
||||||
|
{
|
||||||
|
Response::Node createGenreNode(const Database::ObjectPtr<Database::Cluster>& cluster);
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "responses/ItemGenre.hpp"
|
||||||
|
|
||||||
|
#include "services/database/Cluster.hpp"
|
||||||
|
|
||||||
|
namespace API::Subsonic
|
||||||
|
{
|
||||||
|
Response::Node createItemGenreNode(const Database::Cluster::pointer& cluster)
|
||||||
|
{
|
||||||
|
Response::Node genreNode;
|
||||||
|
|
||||||
|
genreNode.setAttribute("name", cluster->getName());
|
||||||
|
|
||||||
|
return genreNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "services/database/Object.hpp"
|
||||||
|
#include "SubsonicResponse.hpp"
|
||||||
|
|
||||||
|
namespace Database
|
||||||
|
{
|
||||||
|
class Cluster;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace API::Subsonic
|
||||||
|
{
|
||||||
|
Response::Node createItemGenreNode(const Database::ObjectPtr<Database::Cluster>& cluster);
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "Playlist.hpp"
|
||||||
|
|
||||||
|
#include "services/database/TrackList.hpp"
|
||||||
|
#include "services/database/User.hpp"
|
||||||
|
#include "SubsonicId.hpp"
|
||||||
|
|
||||||
|
namespace API::Subsonic
|
||||||
|
{
|
||||||
|
using namespace Database;
|
||||||
|
|
||||||
|
static const std::string_view reportedDummyDate{ "2000-01-01T00:00:00" };
|
||||||
|
|
||||||
|
Response::Node createPlaylistNode(const TrackList::pointer& tracklist, Session&)
|
||||||
|
{
|
||||||
|
Response::Node playlistNode;
|
||||||
|
|
||||||
|
playlistNode.setAttribute("id", idToString(tracklist->getId()));
|
||||||
|
playlistNode.setAttribute("name", tracklist->getName());
|
||||||
|
playlistNode.setAttribute("songCount", tracklist->getCount());
|
||||||
|
playlistNode.setAttribute("duration", std::chrono::duration_cast<std::chrono::seconds>(tracklist->getDuration()).count());
|
||||||
|
playlistNode.setAttribute("public", tracklist->isPublic());
|
||||||
|
playlistNode.setAttribute("created", reportedDummyDate);
|
||||||
|
playlistNode.setAttribute("owner", tracklist->getUser()->getLoginName());
|
||||||
|
|
||||||
|
return playlistNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "services/database/Object.hpp"
|
||||||
|
#include "SubsonicResponse.hpp"
|
||||||
|
|
||||||
|
namespace Database
|
||||||
|
{
|
||||||
|
class TrackList;
|
||||||
|
class Session;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace API::Subsonic
|
||||||
|
{
|
||||||
|
Response::Node createPlaylistNode(const Database::ObjectPtr<Database::TrackList>& tracklist, Database::Session& session);
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "responses/ReplayGain.hpp"
|
||||||
|
|
||||||
|
#include "services/database/Track.hpp"
|
||||||
|
|
||||||
|
namespace API::Subsonic
|
||||||
|
{
|
||||||
|
Response::Node createReplayGainNode(const Database::ObjectPtr<Database::Track>& track)
|
||||||
|
{
|
||||||
|
Response::Node replayGainNode;
|
||||||
|
|
||||||
|
if (const auto trackReplayGain{ track->getTrackReplayGain() })
|
||||||
|
replayGainNode.setAttribute("trackGain", *trackReplayGain);
|
||||||
|
|
||||||
|
if (const auto releaseReplayGain{ track->getReleaseReplayGain() })
|
||||||
|
replayGainNode.setAttribute("albumGain", *releaseReplayGain);
|
||||||
|
|
||||||
|
return replayGainNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "services/database/Object.hpp"
|
||||||
|
#include "SubsonicResponse.hpp"
|
||||||
|
|
||||||
|
namespace Database
|
||||||
|
{
|
||||||
|
class Track;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace API::Subsonic
|
||||||
|
{
|
||||||
|
Response::Node createReplayGainNode(const Database::ObjectPtr<Database::Track>& track);
|
||||||
|
}
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "responses/Song.hpp"
|
||||||
|
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
#include "services/database/Artist.hpp"
|
||||||
|
#include "services/database/Cluster.hpp"
|
||||||
|
#include "services/database/Release.hpp"
|
||||||
|
#include "services/database/Track.hpp"
|
||||||
|
#include "services/database/TrackArtistLink.hpp"
|
||||||
|
#include "services/database/User.hpp"
|
||||||
|
#include "services/scrobbling/IScrobblingService.hpp"
|
||||||
|
#include "utils/Service.hpp"
|
||||||
|
#include "utils/String.hpp"
|
||||||
|
#include "responses/Artist.hpp"
|
||||||
|
#include "responses/Contributor.hpp"
|
||||||
|
#include "responses/ItemGenre.hpp"
|
||||||
|
#include "responses/ReplayGain.hpp"
|
||||||
|
#include "SubsonicId.hpp"
|
||||||
|
#include "Utils.hpp"
|
||||||
|
|
||||||
|
namespace API::Subsonic
|
||||||
|
{
|
||||||
|
using namespace Database;
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
std::string_view formatToSuffix(AudioFormat format)
|
||||||
|
{
|
||||||
|
switch (format)
|
||||||
|
{
|
||||||
|
case AudioFormat::MP3: return "mp3";
|
||||||
|
case AudioFormat::OGG_OPUS: return "opus";
|
||||||
|
case AudioFormat::MATROSKA_OPUS: return "mka";
|
||||||
|
case AudioFormat::OGG_VORBIS: return "ogg";
|
||||||
|
case AudioFormat::WEBM_VORBIS: return "webm";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string getTrackPath(const Track::pointer& track)
|
||||||
|
{
|
||||||
|
std::string path;
|
||||||
|
|
||||||
|
// The track path has to be relative from the root
|
||||||
|
|
||||||
|
const auto release{ track->getRelease() };
|
||||||
|
if (release)
|
||||||
|
{
|
||||||
|
auto artists{ release->getReleaseArtists() };
|
||||||
|
if (artists.empty())
|
||||||
|
artists = release->getArtists();
|
||||||
|
|
||||||
|
if (artists.size() > 1)
|
||||||
|
path = "Various Artists/";
|
||||||
|
else if (artists.size() == 1)
|
||||||
|
path = Utils::makeNameFilesystemCompatible(artists.front()->getName()) + "/";
|
||||||
|
|
||||||
|
path += Utils::makeNameFilesystemCompatible(track->getRelease()->getName()) + "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (track->getDiscNumber())
|
||||||
|
path += std::to_string(*track->getDiscNumber()) + "-";
|
||||||
|
if (track->getTrackNumber())
|
||||||
|
path += std::to_string(*track->getTrackNumber()) + "-";
|
||||||
|
|
||||||
|
path += Utils::makeNameFilesystemCompatible(track->getName());
|
||||||
|
|
||||||
|
if (track->getPath().has_extension())
|
||||||
|
path += track->getPath().extension();
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Response::Node createSongNode(const Track::pointer& track, Session& dbSession, const User::pointer& user)
|
||||||
|
{
|
||||||
|
Response::Node trackResponse;
|
||||||
|
|
||||||
|
trackResponse.setAttribute("id", idToString(track->getId()));
|
||||||
|
trackResponse.setAttribute("isDir", false);
|
||||||
|
trackResponse.setAttribute("title", track->getName());
|
||||||
|
if (track->getTrackNumber())
|
||||||
|
trackResponse.setAttribute("track", *track->getTrackNumber());
|
||||||
|
if (track->getDiscNumber())
|
||||||
|
trackResponse.setAttribute("discNumber", *track->getDiscNumber());
|
||||||
|
if (track->getYear())
|
||||||
|
trackResponse.setAttribute("year", *track->getYear());
|
||||||
|
|
||||||
|
trackResponse.setAttribute("path", getTrackPath(track));
|
||||||
|
{
|
||||||
|
std::error_code ec;
|
||||||
|
const auto fileSize{ std::filesystem::file_size(track->getPath(), ec) };
|
||||||
|
if (!ec)
|
||||||
|
trackResponse.setAttribute("size", fileSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (track->getPath().has_extension())
|
||||||
|
{
|
||||||
|
auto extension{ track->getPath().extension() };
|
||||||
|
trackResponse.setAttribute("suffix", extension.string().substr(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user->getSubsonicTranscodeEnable())
|
||||||
|
trackResponse.setAttribute("transcodedSuffix", formatToSuffix(user->getSubsonicTranscodeFormat()));
|
||||||
|
|
||||||
|
trackResponse.setAttribute("coverArt", idToString(track->getId()));
|
||||||
|
|
||||||
|
const std::vector<Artist::pointer>& artists{ track->getArtists({TrackArtistLinkType::Artist}) };
|
||||||
|
if (!artists.empty())
|
||||||
|
{
|
||||||
|
trackResponse.setAttribute("artist", Utils::joinArtistNames(artists));
|
||||||
|
|
||||||
|
if (artists.size() == 1)
|
||||||
|
trackResponse.setAttribute("artistId", idToString(artists.front()->getId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (track->getRelease())
|
||||||
|
{
|
||||||
|
trackResponse.setAttribute("album", track->getRelease()->getName());
|
||||||
|
trackResponse.setAttribute("albumId", idToString(track->getRelease()->getId()));
|
||||||
|
trackResponse.setAttribute("parent", idToString(track->getRelease()->getId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
trackResponse.setAttribute("duration", std::chrono::duration_cast<std::chrono::seconds>(track->getDuration()).count());
|
||||||
|
trackResponse.setAttribute("type", "music");
|
||||||
|
trackResponse.setAttribute("created", StringUtils::toISO8601String(track->getLastWritten()));
|
||||||
|
|
||||||
|
if (const Wt::WDateTime dateTime{ Service<Scrobbling::IScrobblingService>::get()->getStarredDateTime(user->getId(), track->getId()) }; dateTime.isValid())
|
||||||
|
trackResponse.setAttribute("starred", StringUtils::toISO8601String(dateTime));
|
||||||
|
|
||||||
|
// Report the first GENRE for this track
|
||||||
|
if (ClusterType::pointer genreClusterType{ ClusterType::find(dbSession, "GENRE") })
|
||||||
|
{
|
||||||
|
auto clusters{ track->getClusterGroups({genreClusterType}, 1) };
|
||||||
|
if (!clusters.empty() && !clusters.front().empty())
|
||||||
|
trackResponse.setAttribute("genre", clusters.front().front()->getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenSubsonic specific fields (must always be set)
|
||||||
|
trackResponse.setAttribute("mediaType", "song");
|
||||||
|
|
||||||
|
{
|
||||||
|
std::optional<UUID> mbid{ track->getRecordingMBID() };
|
||||||
|
trackResponse.setAttribute("musicBrainzId", mbid ? mbid->getAsString() : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
trackResponse.createEmptyArrayChild("contributors");
|
||||||
|
{
|
||||||
|
TrackArtistLink::FindParameters params;
|
||||||
|
params.setTrack(track->getId());
|
||||||
|
|
||||||
|
for (const TrackArtistLinkId linkId : TrackArtistLink::find(dbSession, params).results)
|
||||||
|
{
|
||||||
|
TrackArtistLink::pointer link{ TrackArtistLink::find(dbSession, linkId) };
|
||||||
|
// Don't report artists nor release artists as they are set in dedicated fields
|
||||||
|
if (link && link->getType() != TrackArtistLinkType::Artist && link->getType() != TrackArtistLinkType::ReleaseArtist)
|
||||||
|
trackResponse.addArrayChild("contributors", createContributorNode(link));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto addArtistLinks{ [&](std::string_view nodeName, TrackArtistLinkType type)
|
||||||
|
{
|
||||||
|
trackResponse.createEmptyArrayChild(nodeName);
|
||||||
|
|
||||||
|
TrackArtistLink::FindParameters params;
|
||||||
|
params.setTrack(track->getId());
|
||||||
|
params.setLinkType(type);
|
||||||
|
|
||||||
|
for (const TrackArtistLinkId linkId : TrackArtistLink::find(dbSession, params).results)
|
||||||
|
{
|
||||||
|
TrackArtistLink::pointer link{ TrackArtistLink::find(dbSession, linkId) };
|
||||||
|
if (link)
|
||||||
|
trackResponse.addArrayChild(nodeName, createArtistNode(link->getArtist()));
|
||||||
|
}
|
||||||
|
} };
|
||||||
|
|
||||||
|
addArtistLinks("artists", TrackArtistLinkType::Artist);
|
||||||
|
addArtistLinks("albumartists", TrackArtistLinkType::ReleaseArtist);
|
||||||
|
|
||||||
|
auto addClusters{ [&](std::string_view field, std::string_view clusterTypeName)
|
||||||
|
{
|
||||||
|
trackResponse.createEmptyArrayValue(field);
|
||||||
|
|
||||||
|
ClusterType::pointer clusterType{ ClusterType::find(dbSession, clusterTypeName) };
|
||||||
|
if (clusterType)
|
||||||
|
{
|
||||||
|
Cluster::FindParameters params;
|
||||||
|
params.setTrack(track->getId());
|
||||||
|
params.setClusterType(clusterType->getId());
|
||||||
|
|
||||||
|
for (const ClusterId clusterId : Cluster::find(dbSession, params).results)
|
||||||
|
{
|
||||||
|
Cluster::pointer cluster {Cluster::find(dbSession, clusterId)};
|
||||||
|
if (cluster)
|
||||||
|
trackResponse.addArrayValue(field, cluster->getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} };
|
||||||
|
|
||||||
|
addClusters("moods", "MOOD");
|
||||||
|
|
||||||
|
// Genres
|
||||||
|
{
|
||||||
|
trackResponse.createEmptyArrayChild("genres");
|
||||||
|
|
||||||
|
ClusterType::pointer clusterType{ ClusterType::find(dbSession, "GENRE") };
|
||||||
|
if (clusterType)
|
||||||
|
{
|
||||||
|
Cluster::FindParameters params;
|
||||||
|
params.setTrack(track->getId());
|
||||||
|
params.setClusterType(clusterType->getId());
|
||||||
|
|
||||||
|
for (const ClusterId clusterId : Cluster::find(dbSession, params).results)
|
||||||
|
{
|
||||||
|
Cluster::pointer cluster{ Cluster::find(dbSession, clusterId) };
|
||||||
|
if (cluster)
|
||||||
|
trackResponse.addArrayChild("genres", createItemGenreNode(cluster));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trackResponse.addChild("replayGain", createReplayGainNode(track));
|
||||||
|
|
||||||
|
return trackResponse;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "services/database/Object.hpp"
|
||||||
|
#include "SubsonicResponse.hpp"
|
||||||
|
|
||||||
|
namespace Database
|
||||||
|
{
|
||||||
|
class Track;
|
||||||
|
class User;
|
||||||
|
class Session;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace API::Subsonic
|
||||||
|
{
|
||||||
|
Response::Node createSongNode(const Database::ObjectPtr<Database::Track>& track, Database::Session& session, const Database::ObjectPtr<Database::User>& user);
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "responses/User.hpp"
|
||||||
|
|
||||||
|
#include "services/database/User.hpp"
|
||||||
|
|
||||||
|
namespace API::Subsonic
|
||||||
|
{
|
||||||
|
using namespace Database;
|
||||||
|
|
||||||
|
Response::Node createUserNode(const User::pointer& user)
|
||||||
|
{
|
||||||
|
Response::Node userNode;
|
||||||
|
|
||||||
|
userNode.setAttribute("username", user->getLoginName());
|
||||||
|
userNode.setAttribute("scrobblingEnabled", true);
|
||||||
|
userNode.setAttribute("adminRole", user->isAdmin());
|
||||||
|
userNode.setAttribute("settingsRole", true);
|
||||||
|
userNode.setAttribute("downloadRole", true);
|
||||||
|
userNode.setAttribute("uploadRole", false);
|
||||||
|
userNode.setAttribute("playlistRole", true);
|
||||||
|
userNode.setAttribute("coverArtRole", false);
|
||||||
|
userNode.setAttribute("commentRole", false);
|
||||||
|
userNode.setAttribute("podcastRole", false);
|
||||||
|
userNode.setAttribute("streamRole", true);
|
||||||
|
userNode.setAttribute("jukeboxRole", false);
|
||||||
|
userNode.setAttribute("shareRole", false);
|
||||||
|
|
||||||
|
Response::Node folder;
|
||||||
|
folder.setValue("0");
|
||||||
|
userNode.addArrayChild("folder", std::move(folder));
|
||||||
|
|
||||||
|
return userNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 Emeric Poupon
|
||||||
|
*
|
||||||
|
* This file is part of LMS.
|
||||||
|
*
|
||||||
|
* LMS is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* LMS is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "services/database/Object.hpp"
|
||||||
|
#include "SubsonicResponse.hpp"
|
||||||
|
|
||||||
|
namespace Database
|
||||||
|
{
|
||||||
|
class User;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace API::Subsonic
|
||||||
|
{
|
||||||
|
Response::Node createUserNode(const Database::ObjectPtr<Database::User>& user);
|
||||||
|
}
|
||||||
+210
-214
@@ -27,279 +27,275 @@
|
|||||||
#include <boost/algorithm/string/join.hpp>
|
#include <boost/algorithm/string/join.hpp>
|
||||||
#include <boost/algorithm/string.hpp>
|
#include <boost/algorithm/string.hpp>
|
||||||
|
|
||||||
namespace StringUtils {
|
#include <Wt/WDateTime.h>
|
||||||
|
#include <Wt/WDate.h>
|
||||||
|
|
||||||
bool
|
namespace StringUtils
|
||||||
readList(const std::string& str, const std::string& separators, std::list<std::string>& results)
|
|
||||||
{
|
{
|
||||||
std::string curStr;
|
|
||||||
|
|
||||||
for (char c : str)
|
bool readList(const std::string& str, const std::string& separators, std::list<std::string>& results)
|
||||||
{
|
{
|
||||||
if (separators.find(c) != std::string::npos) {
|
std::string curStr;
|
||||||
if (!curStr.empty()) {
|
|
||||||
results.push_back(curStr);
|
|
||||||
curStr.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (curStr.empty() && std::isspace(c))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
curStr.push_back(c);
|
for (char c : str)
|
||||||
}
|
{
|
||||||
}
|
if (separators.find(c) != std::string::npos) {
|
||||||
|
if (!curStr.empty()) {
|
||||||
|
results.push_back(curStr);
|
||||||
|
curStr.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (curStr.empty() && std::isspace(c))
|
||||||
|
continue;
|
||||||
|
|
||||||
if (!curStr.empty())
|
curStr.push_back(c);
|
||||||
results.push_back(curStr);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return !str.empty();
|
if (!curStr.empty())
|
||||||
}
|
results.push_back(curStr);
|
||||||
|
|
||||||
template<>
|
return !str.empty();
|
||||||
std::optional<std::string>
|
}
|
||||||
readAs(std::string_view str)
|
|
||||||
{
|
|
||||||
return std::string {str};
|
|
||||||
}
|
|
||||||
|
|
||||||
template<>
|
template<>
|
||||||
std::optional<std::string_view>
|
std::optional<std::string> readAs(std::string_view str)
|
||||||
readAs(std::string_view str)
|
{
|
||||||
{
|
return std::string{ str };
|
||||||
return str;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
template<>
|
template<>
|
||||||
std::optional<bool>
|
std::optional<std::string_view> readAs(std::string_view str)
|
||||||
readAs(std::string_view str)
|
{
|
||||||
{
|
return str;
|
||||||
if (str == "1" || str == "true")
|
}
|
||||||
return true;
|
|
||||||
else if (str == "0" || str == "false")
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return std::nullopt;
|
template<>
|
||||||
}
|
std::optional<bool> readAs(std::string_view str)
|
||||||
|
{
|
||||||
|
if (str == "1" || str == "true")
|
||||||
|
return true;
|
||||||
|
else if (str == "0" || str == "false")
|
||||||
|
return false;
|
||||||
|
|
||||||
std::vector<std::string>
|
return std::nullopt;
|
||||||
splitStringCopy(std::string_view string, std::string_view separators)
|
}
|
||||||
{
|
|
||||||
std::string str {stringTrim(string, separators)};
|
|
||||||
|
|
||||||
std::vector<std::string> res;
|
std::vector<std::string> splitStringCopy(std::string_view string, std::string_view separators)
|
||||||
boost::algorithm::split(res, str, boost::is_any_of(separators), boost::token_compress_on);
|
{
|
||||||
|
std::string str{ stringTrim(string, separators) };
|
||||||
|
|
||||||
return res;
|
std::vector<std::string> res;
|
||||||
}
|
boost::algorithm::split(res, str, boost::is_any_of(separators), boost::token_compress_on);
|
||||||
|
|
||||||
std::vector<std::string_view>
|
return res;
|
||||||
splitString(std::string_view str, std::string_view separators)
|
}
|
||||||
{
|
|
||||||
std::vector<std::string_view> res;
|
|
||||||
|
|
||||||
std::string_view::size_type strBegin {};
|
std::vector<std::string_view> splitString(std::string_view str, std::string_view separators)
|
||||||
|
{
|
||||||
|
std::vector<std::string_view> res;
|
||||||
|
|
||||||
while ((strBegin = str.find_first_not_of(separators, strBegin)) != std::string_view::npos)
|
std::string_view::size_type strBegin{};
|
||||||
{
|
|
||||||
auto strEnd {str.find_first_of(separators, strBegin + 1)};
|
|
||||||
if (strEnd == std::string_view::npos)
|
|
||||||
{
|
|
||||||
res.push_back(str.substr(strBegin, str.size() - strBegin));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
res.push_back(str.substr(strBegin, strEnd - strBegin));
|
while ((strBegin = str.find_first_not_of(separators, strBegin)) != std::string_view::npos)
|
||||||
strBegin = strEnd + 1;
|
{
|
||||||
}
|
auto strEnd{ str.find_first_of(separators, strBegin + 1) };
|
||||||
|
if (strEnd == std::string_view::npos)
|
||||||
|
{
|
||||||
|
res.push_back(str.substr(strBegin, str.size() - strBegin));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
return res;
|
res.push_back(str.substr(strBegin, strEnd - strBegin));
|
||||||
}
|
strBegin = strEnd + 1;
|
||||||
|
}
|
||||||
|
|
||||||
std::string
|
return res;
|
||||||
joinStrings(const std::vector<std::string>& strings, const std::string& delimiter)
|
}
|
||||||
{
|
|
||||||
return boost::algorithm::join(strings, delimiter);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string_view
|
std::string joinStrings(const std::vector<std::string>& strings, const std::string& delimiter)
|
||||||
stringTrim(std::string_view str, std::string_view whitespaces)
|
{
|
||||||
{
|
return boost::algorithm::join(strings, delimiter);
|
||||||
std::string_view res;
|
}
|
||||||
|
|
||||||
const auto strBegin = str.find_first_not_of(whitespaces);
|
std::string_view stringTrim(std::string_view str, std::string_view whitespaces)
|
||||||
if (strBegin != std::string_view::npos)
|
{
|
||||||
{
|
std::string_view res;
|
||||||
const auto strEnd {str.find_last_not_of(whitespaces)};
|
|
||||||
const auto strRange {strEnd - strBegin + 1};
|
|
||||||
|
|
||||||
res = str.substr(strBegin, strRange);
|
const auto strBegin = str.find_first_not_of(whitespaces);
|
||||||
}
|
if (strBegin != std::string_view::npos)
|
||||||
|
{
|
||||||
|
const auto strEnd{ str.find_last_not_of(whitespaces) };
|
||||||
|
const auto strRange{ strEnd - strBegin + 1 };
|
||||||
|
|
||||||
return res;
|
res = str.substr(strBegin, strRange);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string_view
|
return res;
|
||||||
stringTrimEnd(std::string_view str, std::string_view whitespaces)
|
}
|
||||||
{
|
|
||||||
return str.substr(0, str.find_last_not_of(whitespaces) + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string
|
std::string_view stringTrimEnd(std::string_view str, std::string_view whitespaces)
|
||||||
stringToLower(std::string_view str)
|
{
|
||||||
{
|
return str.substr(0, str.find_last_not_of(whitespaces) + 1);
|
||||||
std::string res;
|
}
|
||||||
res.reserve(str.size());
|
|
||||||
|
|
||||||
std::transform(std::cbegin(str), std::cend(str), std::back_inserter(res), [](unsigned char c) { return std::tolower(c);});
|
std::string stringToLower(std::string_view str)
|
||||||
|
{
|
||||||
|
std::string res;
|
||||||
|
res.reserve(str.size());
|
||||||
|
|
||||||
return res;
|
std::transform(std::cbegin(str), std::cend(str), std::back_inserter(res), [](unsigned char c) { return std::tolower(c);});
|
||||||
}
|
|
||||||
|
|
||||||
void
|
return res;
|
||||||
stringToLower(std::string& str)
|
}
|
||||||
{
|
|
||||||
std::transform(std::cbegin(str), std::cend(str), std::begin(str), [](unsigned char c) { return std::tolower(c);});
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string
|
void stringToLower(std::string& str)
|
||||||
stringToUpper(const std::string& str)
|
{
|
||||||
{
|
std::transform(std::cbegin(str), std::cend(str), std::begin(str), [](unsigned char c) { return std::tolower(c);});
|
||||||
std::string res;
|
}
|
||||||
res.reserve(str.size());
|
|
||||||
|
|
||||||
std::transform(std::cbegin(str), std::cend(str), std::back_inserter(res), [](char c) { return std::toupper(c);});
|
std::string stringToUpper(const std::string& str)
|
||||||
|
{
|
||||||
|
std::string res;
|
||||||
|
res.reserve(str.size());
|
||||||
|
|
||||||
return res;
|
std::transform(std::cbegin(str), std::cend(str), std::back_inserter(res), [](char c) { return std::toupper(c);});
|
||||||
}
|
|
||||||
|
|
||||||
std::string
|
return res;
|
||||||
bufferToString(const std::vector<unsigned char>& data)
|
}
|
||||||
{
|
|
||||||
std::ostringstream oss;
|
|
||||||
|
|
||||||
for (unsigned char c : data)
|
std::string bufferToString(const std::vector<unsigned char>& data)
|
||||||
{
|
{
|
||||||
oss << std::setw(2) << std::setfill('0') << std::hex << (int)c;
|
std::ostringstream oss;
|
||||||
}
|
|
||||||
|
|
||||||
return oss.str();
|
for (unsigned char c : data)
|
||||||
}
|
{
|
||||||
|
oss << std::setw(2) << std::setfill('0') << std::hex << (int)c;
|
||||||
|
}
|
||||||
|
|
||||||
void
|
return oss.str();
|
||||||
capitalize(std::string& str)
|
}
|
||||||
{
|
|
||||||
for (auto it {std::begin(str)}; it != std::end(str); ++it)
|
|
||||||
{
|
|
||||||
if (std::isspace(*it))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (std::isalpha(*it))
|
void capitalize(std::string& str)
|
||||||
*it = std::toupper(*it);
|
{
|
||||||
|
for (auto it{ std::begin(str) }; it != std::end(str); ++it)
|
||||||
|
{
|
||||||
|
if (std::isspace(*it))
|
||||||
|
continue;
|
||||||
|
|
||||||
break;
|
if (std::isalpha(*it))
|
||||||
}
|
*it = std::toupper(*it);
|
||||||
}
|
|
||||||
|
|
||||||
std::string
|
break;
|
||||||
replaceInString(std::string_view str, const std::string& from, const std::string& to)
|
}
|
||||||
{
|
}
|
||||||
std::string res {str};
|
|
||||||
size_t pos = 0;
|
|
||||||
|
|
||||||
while ((pos = res.find(from, pos)) != std::string::npos)
|
std::string replaceInString(std::string_view str, const std::string& from, const std::string& to)
|
||||||
{
|
{
|
||||||
res.replace(pos, from.length(), to);
|
std::string res{ str };
|
||||||
pos += to.length();
|
size_t pos = 0;
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
while ((pos = res.find(from, pos)) != std::string::npos)
|
||||||
}
|
{
|
||||||
|
res.replace(pos, from.length(), to);
|
||||||
|
pos += to.length();
|
||||||
|
}
|
||||||
|
|
||||||
std::string
|
return res;
|
||||||
jsEscape(const std::string& str)
|
}
|
||||||
{
|
|
||||||
static const std::unordered_map<char, std::string_view> escapeMap
|
|
||||||
{
|
|
||||||
{ '\\', "\\\\" },
|
|
||||||
{ '\n', "\\n" },
|
|
||||||
{ '\r', "\\r" },
|
|
||||||
{ '\t', "\\t" },
|
|
||||||
{ '"', "\\\"" },
|
|
||||||
{ '\'', "\\\'" },
|
|
||||||
};
|
|
||||||
|
|
||||||
std::string escaped;
|
std::string jsEscape(const std::string& str)
|
||||||
escaped.reserve(str.length());
|
{
|
||||||
|
static const std::unordered_map<char, std::string_view> escapeMap
|
||||||
|
{
|
||||||
|
{ '\\', "\\\\" },
|
||||||
|
{ '\n', "\\n" },
|
||||||
|
{ '\r', "\\r" },
|
||||||
|
{ '\t', "\\t" },
|
||||||
|
{ '"', "\\\"" },
|
||||||
|
{ '\'', "\\\'" },
|
||||||
|
};
|
||||||
|
|
||||||
for (const char c : str)
|
std::string escaped;
|
||||||
{
|
escaped.reserve(str.length());
|
||||||
auto it {escapeMap.find(c)};
|
|
||||||
if (it == std::cend(escapeMap))
|
|
||||||
{
|
|
||||||
escaped += c;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
escaped += it->second;
|
for (const char c : str)
|
||||||
}
|
{
|
||||||
|
auto it{ escapeMap.find(c) };
|
||||||
|
if (it == std::cend(escapeMap))
|
||||||
|
{
|
||||||
|
escaped += c;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
return escaped;
|
escaped += it->second;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string
|
return escaped;
|
||||||
escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar)
|
}
|
||||||
{
|
|
||||||
std::string res;
|
|
||||||
res.reserve(str.size());
|
|
||||||
|
|
||||||
for (const char c : str)
|
std::string escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar)
|
||||||
{
|
{
|
||||||
if (std::any_of(std::cbegin(charsToEscape), std::cend(charsToEscape), [c](char charToEscape) { return c == charToEscape; }))
|
std::string res;
|
||||||
res += escapeChar;
|
res.reserve(str.size());
|
||||||
|
|
||||||
res += c;
|
for (const char c : str)
|
||||||
}
|
{
|
||||||
|
if (std::any_of(std::cbegin(charsToEscape), std::cend(charsToEscape), [c](char charToEscape) { return c == charToEscape; }))
|
||||||
|
res += escapeChar;
|
||||||
|
|
||||||
return res;
|
res += c;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
return res;
|
||||||
stringEndsWith(const std::string& str, const std::string& ending)
|
}
|
||||||
{
|
|
||||||
return boost::algorithm::ends_with(str, ending);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::optional<std::string>
|
bool stringEndsWith(const std::string& str, const std::string& ending)
|
||||||
stringFromHex(const std::string& str)
|
{
|
||||||
{
|
return boost::algorithm::ends_with(str, ending);
|
||||||
static const char lut[] {"0123456789ABCDEF"};
|
}
|
||||||
|
|
||||||
if (str.length() % 2 != 0)
|
std::optional<std::string> stringFromHex(const std::string& str)
|
||||||
return std::nullopt;
|
{
|
||||||
|
static const char lut[]{ "0123456789ABCDEF" };
|
||||||
|
|
||||||
std::string res;
|
if (str.length() % 2 != 0)
|
||||||
res.reserve(str.length() / 2);
|
return std::nullopt;
|
||||||
|
|
||||||
auto it {std::cbegin(str)};
|
std::string res;
|
||||||
while (it != std::cend(str))
|
res.reserve(str.length() / 2);
|
||||||
{
|
|
||||||
unsigned val {};
|
|
||||||
|
|
||||||
auto itHigh {std::lower_bound(std::cbegin(lut), std::cend(lut), std::toupper(*(it++)))};
|
auto it{ std::cbegin(str) };
|
||||||
auto itLow {std::lower_bound(std::cbegin(lut), std::cend(lut), std::toupper(*(it++)))};
|
while (it != std::cend(str))
|
||||||
|
{
|
||||||
|
unsigned val{};
|
||||||
|
|
||||||
if (itHigh == std::cend(lut) || itLow == std::cend(lut))
|
auto itHigh{ std::lower_bound(std::cbegin(lut), std::cend(lut), std::toupper(*(it++))) };
|
||||||
return {};
|
auto itLow{ std::lower_bound(std::cbegin(lut), std::cend(lut), std::toupper(*(it++))) };
|
||||||
|
|
||||||
val = std::distance(std::cbegin(lut), itHigh) << 4;
|
if (itHigh == std::cend(lut) || itLow == std::cend(lut))
|
||||||
val += std::distance(std::cbegin(lut), itLow );
|
return {};
|
||||||
|
|
||||||
res.push_back(static_cast<char>(val));
|
val = std::distance(std::cbegin(lut), itHigh) << 4;
|
||||||
}
|
val += std::distance(std::cbegin(lut), itLow);
|
||||||
|
|
||||||
return res;
|
res.push_back(static_cast<char>(val));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string toISO8601String(const Wt::WDateTime& dateTime)
|
||||||
|
{
|
||||||
|
// assume UTC
|
||||||
|
return dateTime.toString("yyyy-MM-ddThh:mm:ss.zzz", false).toUTF8();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string toISO8601String(const Wt::WDate& date)
|
||||||
|
{
|
||||||
|
// assume UTC
|
||||||
|
return date.toString("yyyy-MM-dd").toUTF8();
|
||||||
|
}
|
||||||
} // StringUtils
|
} // StringUtils
|
||||||
|
|
||||||
|
|||||||
@@ -29,94 +29,67 @@
|
|||||||
#define QUOTEME(x) QUOTEME_1(x)
|
#define QUOTEME(x) QUOTEME_1(x)
|
||||||
#define QUOTEME_1(x) #x
|
#define QUOTEME_1(x) #x
|
||||||
|
|
||||||
namespace StringUtils {
|
namespace Wt
|
||||||
|
|
||||||
[[nodiscard]]
|
|
||||||
std::vector<std::string>
|
|
||||||
splitStringCopy(std::string_view string, std::string_view separators);
|
|
||||||
|
|
||||||
[[nodiscard]]
|
|
||||||
std::vector<std::string_view>
|
|
||||||
splitString(std::string_view string, std::string_view separators);
|
|
||||||
|
|
||||||
[[nodiscard]]
|
|
||||||
std::string
|
|
||||||
joinStrings(const std::vector<std::string>& strings, const std::string& delimiter);
|
|
||||||
|
|
||||||
[[nodiscard]]
|
|
||||||
std::string_view
|
|
||||||
stringTrim(std::string_view str, std::string_view whitespaces = " \t");
|
|
||||||
|
|
||||||
[[nodiscard]]
|
|
||||||
std::string_view
|
|
||||||
stringTrimEnd(std::string_view str, std::string_view whitespaces = " \t");
|
|
||||||
|
|
||||||
[[nodiscard]]
|
|
||||||
std::string
|
|
||||||
stringToLower(std::string_view str);
|
|
||||||
|
|
||||||
void
|
|
||||||
stringToLower(std::string& str);
|
|
||||||
|
|
||||||
[[nodiscard]]
|
|
||||||
std::string
|
|
||||||
stringToUpper(const std::string& str);
|
|
||||||
|
|
||||||
[[nodiscard]]
|
|
||||||
std::string
|
|
||||||
bufferToString(const std::vector<unsigned char>& data);
|
|
||||||
|
|
||||||
void
|
|
||||||
capitalize(std::string& str);
|
|
||||||
|
|
||||||
template<typename T>
|
|
||||||
[[nodiscard]]
|
|
||||||
std::optional<T> readAs(std::string_view str)
|
|
||||||
{
|
{
|
||||||
T res;
|
class WDate;
|
||||||
|
class WDateTime;
|
||||||
std::istringstream iss {std::string {str}};
|
|
||||||
iss >> res;
|
|
||||||
if (iss.fail())
|
|
||||||
return std::nullopt;
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
template<>
|
namespace StringUtils {
|
||||||
[[nodiscard]]
|
|
||||||
std::optional<std::string>
|
|
||||||
readAs(std::string_view str);
|
|
||||||
|
|
||||||
template<>
|
[[nodiscard]] std::vector<std::string> splitStringCopy(std::string_view string, std::string_view separators);
|
||||||
[[nodiscard]]
|
|
||||||
std::optional<std::string_view>
|
|
||||||
readAs(std::string_view str);
|
|
||||||
|
|
||||||
template<>
|
[[nodiscard]] std::vector<std::string_view> splitString(std::string_view string, std::string_view separators);
|
||||||
[[nodiscard]]
|
|
||||||
std::optional<bool>
|
|
||||||
readAs(std::string_view str);
|
|
||||||
|
|
||||||
[[nodiscard]]
|
[[nodiscard]] std::string joinStrings(const std::vector<std::string>& strings, const std::string& delimiter);
|
||||||
std::string
|
|
||||||
replaceInString(std::string_view str, const std::string& from, const std::string& to);
|
|
||||||
|
|
||||||
[[nodiscard]]
|
[[nodiscard]] std::string_view stringTrim(std::string_view str, std::string_view whitespaces = " \t");
|
||||||
std::string
|
|
||||||
jsEscape(const std::string& str);
|
|
||||||
|
|
||||||
[[nodiscard]]
|
[[nodiscard]] std::string_view stringTrimEnd(std::string_view str, std::string_view whitespaces = " \t");
|
||||||
std::string
|
|
||||||
escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar);
|
|
||||||
|
|
||||||
[[nodiscard]]
|
[[nodiscard]] std::string stringToLower(std::string_view str);
|
||||||
bool
|
|
||||||
stringEndsWith(const std::string& str, const std::string& ending);
|
|
||||||
|
|
||||||
[[nodiscard]]
|
void stringToLower(std::string& str);
|
||||||
std::optional<std::string>
|
|
||||||
stringFromHex(const std::string& str);
|
|
||||||
|
|
||||||
} // StringUtils
|
[[nodiscard]] std::string stringToUpper(const std::string& str);
|
||||||
|
|
||||||
|
[[nodiscard]] std::string bufferToString(const std::vector<unsigned char>& data);
|
||||||
|
|
||||||
|
void capitalize(std::string& str);
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
[[nodiscard]] std::optional<T> readAs(std::string_view str)
|
||||||
|
{
|
||||||
|
T res;
|
||||||
|
|
||||||
|
std::istringstream iss{ std::string {str} };
|
||||||
|
iss >> res;
|
||||||
|
if (iss.fail())
|
||||||
|
return std::nullopt;
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<>
|
||||||
|
[[nodiscard]] std::optional<std::string> readAs(std::string_view str);
|
||||||
|
|
||||||
|
template<>
|
||||||
|
[[nodiscard]] std::optional<std::string_view> readAs(std::string_view str);
|
||||||
|
|
||||||
|
template<>
|
||||||
|
[[nodiscard]] std::optional<bool> readAs(std::string_view str);
|
||||||
|
|
||||||
|
[[nodiscard]] std::string replaceInString(std::string_view str, const std::string& from, const std::string& to);
|
||||||
|
|
||||||
|
[[nodiscard]] std::string jsEscape(const std::string& str);
|
||||||
|
|
||||||
|
[[nodiscard]] std::string escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar);
|
||||||
|
|
||||||
|
[[nodiscard]] bool stringEndsWith(const std::string& str, const std::string& ending);
|
||||||
|
|
||||||
|
[[nodiscard]] std::optional<std::string> stringFromHex(const std::string& str);
|
||||||
|
|
||||||
|
[[nodiscard]] std::string toISO8601String(const Wt::WDateTime& dateTime);
|
||||||
|
[[nodiscard]] std::string toISO8601String(const Wt::WDate& date);
|
||||||
|
|
||||||
|
} // StringUtils
|
||||||
@@ -19,6 +19,9 @@
|
|||||||
|
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <Wt/WDateTime.h>
|
||||||
|
#include <Wt/WDate.h>
|
||||||
|
#include <Wt/WTime.h>
|
||||||
#include "utils/String.hpp"
|
#include "utils/String.hpp"
|
||||||
|
|
||||||
TEST(StringUtils, splitString)
|
TEST(StringUtils, splitString)
|
||||||
@@ -147,3 +150,15 @@ TEST(StringUtils, capitalize)
|
|||||||
EXPECT_EQ(str, test.expectedOutput) << " str was '" << test.input << "'";
|
EXPECT_EQ(str, test.expectedOutput) << " str was '" << test.input << "'";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(Stringutils, date)
|
||||||
|
{
|
||||||
|
const Wt::WDate date{ 2020, 01, 03 };
|
||||||
|
EXPECT_EQ(StringUtils::toISO8601String(date), "2020-01-03");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(Stringutils, dateTime)
|
||||||
|
{
|
||||||
|
const Wt::WDateTime dateTime{ Wt::WDate {2020, 01, 03 }, Wt::WTime{9, 8, 11, 75} };
|
||||||
|
EXPECT_EQ(StringUtils::toISO8601String(dateTime), "2020-01-03T09:08:11.075");
|
||||||
|
}
|
||||||
@@ -78,6 +78,7 @@ namespace UserInterface
|
|||||||
Artist::FindParameters params;
|
Artist::FindParameters params;
|
||||||
params.setClusters(getFilters().getClusterIds());
|
params.setClusters(getFilters().getClusterIds());
|
||||||
params.setKeywords(getSearchKeywords());
|
params.setKeywords(getSearchKeywords());
|
||||||
|
params.setLinkType(_linkType);
|
||||||
params.setSortMethod(ArtistSortMethod::BySortName);
|
params.setSortMethod(ArtistSortMethod::BySortName);
|
||||||
params.setRange(range);
|
params.setRange(range);
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,10 @@
|
|||||||
#include <Wt/WAnchor.h>
|
#include <Wt/WAnchor.h>
|
||||||
|
|
||||||
#include "services/database/Artist.hpp"
|
#include "services/database/Artist.hpp"
|
||||||
|
#include "services/database/Session.hpp"
|
||||||
|
#include "services/database/TrackArtistLink.hpp"
|
||||||
|
#include "utils/EnumSet.hpp"
|
||||||
|
#include "LmsApplication.hpp"
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
|
|
||||||
namespace UserInterface::ArtistListHelpers
|
namespace UserInterface::ArtistListHelpers
|
||||||
@@ -33,5 +37,43 @@ namespace UserInterface::ArtistListHelpers
|
|||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<ArtistLinkTypesModel>
|
||||||
|
createArtistLinkTypesModel()
|
||||||
|
{
|
||||||
|
using namespace Database;
|
||||||
|
|
||||||
|
std::unique_ptr<ArtistLinkTypesModel> linkTypesModel {std::make_unique<ArtistLinkTypesModel>()};
|
||||||
|
|
||||||
|
EnumSet<TrackArtistLinkType> usedLinkTypes;
|
||||||
|
{
|
||||||
|
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
||||||
|
usedLinkTypes = TrackArtistLink::findUsedTypes(LmsApp->getDbSession());
|
||||||
|
}
|
||||||
|
|
||||||
|
auto addTypeIfUsed {[&](TrackArtistLinkType linkType, std::string_view stringKey)
|
||||||
|
{
|
||||||
|
if (!usedLinkTypes.contains(linkType))
|
||||||
|
return;
|
||||||
|
|
||||||
|
linkTypesModel->add(Wt::WString::trn(std::string {stringKey}, 2), linkType);
|
||||||
|
}};
|
||||||
|
|
||||||
|
// add default one first (none)
|
||||||
|
linkTypesModel->add(Wt::WString::tr("Lms.Explore.Artists.linktype-all"), std::nullopt);
|
||||||
|
|
||||||
|
// TODO: sort by translated strings
|
||||||
|
addTypeIfUsed(TrackArtistLinkType::Artist, "Lms.Explore.Artists.linktype-artist");
|
||||||
|
addTypeIfUsed(TrackArtistLinkType::ReleaseArtist, "Lms.Explore.Artists.linktype-releaseartist");
|
||||||
|
addTypeIfUsed(TrackArtistLinkType::Composer, "Lms.Explore.Artists.linktype-composer");
|
||||||
|
addTypeIfUsed(TrackArtistLinkType::Conductor, "Lms.Explore.Artists.linktype-conductor");
|
||||||
|
addTypeIfUsed(TrackArtistLinkType::Lyricist, "Lms.Explore.Artists.linktype-lyricist");
|
||||||
|
addTypeIfUsed(TrackArtistLinkType::Mixer, "Lms.Explore.Artists.linktype-mixer");
|
||||||
|
addTypeIfUsed(TrackArtistLinkType::Performer, "Lms.Explore.Artists.linktype-performer");
|
||||||
|
addTypeIfUsed(TrackArtistLinkType::Producer, "Lms.Explore.Artists.linktype-producer");
|
||||||
|
addTypeIfUsed(TrackArtistLinkType::Remixer, "Lms.Explore.Artists.linktype-remixer");
|
||||||
|
|
||||||
|
return linkTypesModel;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,15 +23,23 @@
|
|||||||
|
|
||||||
#include <Wt/WTemplate.h>
|
#include <Wt/WTemplate.h>
|
||||||
|
|
||||||
|
#include "common/ValueStringModel.hpp"
|
||||||
#include "services/database/Object.hpp"
|
#include "services/database/Object.hpp"
|
||||||
|
#include "services/database/Types.hpp"
|
||||||
|
|
||||||
namespace Database
|
namespace Database
|
||||||
{
|
{
|
||||||
class Artist;
|
class Artist;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace UserInterface::ArtistListHelpers
|
namespace UserInterface
|
||||||
{
|
{
|
||||||
std::unique_ptr<Wt::WTemplate> createEntry(const Database::ObjectPtr<Database::Artist>& artist);
|
using ArtistLinkTypesModel = ValueStringModel<std::optional<Database::TrackArtistLinkType>>;
|
||||||
|
|
||||||
|
namespace ArtistListHelpers
|
||||||
|
{
|
||||||
|
std::unique_ptr<Wt::WTemplate> createEntry(const Database::ObjectPtr<Database::Artist>& artist);
|
||||||
|
std::unique_ptr<ArtistLinkTypesModel> createArtistLinkTypesModel();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,10 +24,8 @@
|
|||||||
#include "services/database/Artist.hpp"
|
#include "services/database/Artist.hpp"
|
||||||
#include "services/database/Session.hpp"
|
#include "services/database/Session.hpp"
|
||||||
#include "services/database/TrackArtistLink.hpp"
|
#include "services/database/TrackArtistLink.hpp"
|
||||||
#include "utils/EnumSet.hpp"
|
|
||||||
#include "utils/Logger.hpp"
|
#include "utils/Logger.hpp"
|
||||||
|
|
||||||
#include "common/ValueStringModel.hpp"
|
|
||||||
#include "common/InfiniteScrollingContainer.hpp"
|
#include "common/InfiniteScrollingContainer.hpp"
|
||||||
#include "ArtistListHelpers.hpp"
|
#include "ArtistListHelpers.hpp"
|
||||||
#include "Filters.hpp"
|
#include "Filters.hpp"
|
||||||
@@ -37,8 +35,6 @@ using namespace Database;
|
|||||||
|
|
||||||
namespace UserInterface {
|
namespace UserInterface {
|
||||||
|
|
||||||
using ArtistLinkModel = ValueStringModel<std::optional<TrackArtistLinkType>>;
|
|
||||||
|
|
||||||
Artists::Artists(Filters& filters)
|
Artists::Artists(Filters& filters)
|
||||||
: Wt::WTemplate {Wt::WString::tr("Lms.Explore.Artists.template")}
|
: Wt::WTemplate {Wt::WString::tr("Lms.Explore.Artists.template")}
|
||||||
, _artistCollector {filters, _defaultMode, _maxCount}
|
, _artistCollector {filters, _defaultMode, _maxCount}
|
||||||
@@ -71,18 +67,17 @@ Artists::Artists(Filters& filters)
|
|||||||
bindMenuItem("all", Wt::WString::tr("Lms.Explore.all"), ArtistCollector::Mode::All);
|
bindMenuItem("all", Wt::WString::tr("Lms.Explore.all"), ArtistCollector::Mode::All);
|
||||||
|
|
||||||
_linkType = bindNew<Wt::WComboBox>("link-type");
|
_linkType = bindNew<Wt::WComboBox>("link-type");
|
||||||
_linkType->setModel(std::make_shared<ArtistLinkModel>());
|
_linkType->setModel(ArtistListHelpers::createArtistLinkTypesModel());
|
||||||
_linkType->changed().connect([this]
|
_linkType->changed().connect([this]
|
||||||
{
|
{
|
||||||
const std::optional<TrackArtistLinkType> linkType {static_cast<ArtistLinkModel*>(_linkType->model().get())->getValue(_linkType->currentIndex())};
|
const std::optional<TrackArtistLinkType> linkType {static_cast<ArtistLinkTypesModel*>(_linkType->model().get())->getValue(_linkType->currentIndex())};
|
||||||
refreshView(linkType);
|
refreshView(linkType);
|
||||||
});
|
});
|
||||||
refreshArtistLinkTypes();
|
|
||||||
|
|
||||||
LmsApp->getScannerEvents().scanComplete.connect(this, [this](const Scanner::ScanStats& stats)
|
LmsApp->getScannerEvents().scanComplete.connect(this, [this](const Scanner::ScanStats& stats)
|
||||||
{
|
{
|
||||||
if (stats.nbChanges())
|
if (stats.nbChanges())
|
||||||
refreshArtistLinkTypes();
|
_linkType->setModel(ArtistListHelpers::createArtistLinkTypesModel());
|
||||||
});
|
});
|
||||||
|
|
||||||
_container = bindNew<InfiniteScrollingContainer>("artists", Wt::WString::tr("Lms.Explore.Artists.template.container"));
|
_container = bindNew<InfiniteScrollingContainer>("artists", Wt::WString::tr("Lms.Explore.Artists.template.container"));
|
||||||
@@ -120,42 +115,6 @@ Artists::refreshView(std::optional<TrackArtistLinkType> linkType)
|
|||||||
refreshView();
|
refreshView();
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
|
||||||
Artists::refreshArtistLinkTypes()
|
|
||||||
{
|
|
||||||
std::shared_ptr<ArtistLinkModel> linkTypeModel {std::static_pointer_cast<ArtistLinkModel>(_linkType->model())};
|
|
||||||
|
|
||||||
EnumSet<TrackArtistLinkType> usedLinkTypes;
|
|
||||||
{
|
|
||||||
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
|
|
||||||
usedLinkTypes = TrackArtistLink::findUsedTypes(LmsApp->getDbSession());
|
|
||||||
}
|
|
||||||
|
|
||||||
auto addTypeIfUsed {[&](TrackArtistLinkType linkType, std::string_view stringKey)
|
|
||||||
{
|
|
||||||
if (!usedLinkTypes.contains(linkType))
|
|
||||||
return;
|
|
||||||
|
|
||||||
linkTypeModel->add(Wt::WString::trn(std::string {stringKey}, 2), linkType);
|
|
||||||
}};
|
|
||||||
|
|
||||||
linkTypeModel->clear();
|
|
||||||
|
|
||||||
// add default one first (none)
|
|
||||||
linkTypeModel->add(Wt::WString::tr("Lms.Explore.Artists.linktype-all"), std::nullopt);
|
|
||||||
|
|
||||||
// TODO: sort by translated strings
|
|
||||||
addTypeIfUsed(TrackArtistLinkType::Artist, "Lms.Explore.Artists.linktype-artist");
|
|
||||||
addTypeIfUsed(TrackArtistLinkType::ReleaseArtist, "Lms.Explore.Artists.linktype-releaseartist");
|
|
||||||
addTypeIfUsed(TrackArtistLinkType::Composer, "Lms.Explore.Artists.linktype-composer");
|
|
||||||
addTypeIfUsed(TrackArtistLinkType::Conductor, "Lms.Explore.Artists.linktype-conductor");
|
|
||||||
addTypeIfUsed(TrackArtistLinkType::Lyricist, "Lms.Explore.Artists.linktype-lyricist");
|
|
||||||
addTypeIfUsed(TrackArtistLinkType::Mixer, "Lms.Explore.Artists.linktype-mixer");
|
|
||||||
addTypeIfUsed(TrackArtistLinkType::Performer, "Lms.Explore.Artists.linktype-performer");
|
|
||||||
addTypeIfUsed(TrackArtistLinkType::Producer, "Lms.Explore.Artists.linktype-producer");
|
|
||||||
addTypeIfUsed(TrackArtistLinkType::Remixer, "Lms.Explore.Artists.linktype-remixer");
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void
|
||||||
Artists::addSome()
|
Artists::addSome()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ namespace UserInterface
|
|||||||
void refreshView();
|
void refreshView();
|
||||||
void refreshView(ArtistCollector::Mode mode);
|
void refreshView(ArtistCollector::Mode mode);
|
||||||
void refreshView(std::optional<Database::TrackArtistLinkType> linkType);
|
void refreshView(std::optional<Database::TrackArtistLinkType> linkType);
|
||||||
void refreshArtistLinkTypes();
|
|
||||||
void addSome();
|
void addSome();
|
||||||
|
|
||||||
static constexpr std::size_t _batchSize {30};
|
static constexpr std::size_t _batchSize {30};
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ namespace UserInterface::ReleaseListHelpers
|
|||||||
|
|
||||||
if (showYear)
|
if (showYear)
|
||||||
{
|
{
|
||||||
Wt::WString year {ReleaseHelpers::buildReleaseYearString(release->getReleaseYear(), release->getReleaseYear(true))};
|
Wt::WString year {ReleaseHelpers::buildReleaseYearString(release->getReleaseDate(), release->getOriginalReleaseDate())};
|
||||||
if (!year.empty())
|
if (!year.empty())
|
||||||
{
|
{
|
||||||
entry->setCondition("if-has-year", true);
|
entry->setCondition("if-has-year", true);
|
||||||
@@ -135,18 +135,18 @@ namespace UserInterface::ReleaseHelpers
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
Wt::WString buildReleaseYearString(std::optional<int> year, std::optional<int> originalYear)
|
Wt::WString buildReleaseYearString(const Wt::WDate& releaseDate, const Wt::WDate& originalReleaseDate)
|
||||||
{
|
{
|
||||||
Wt::WString res;
|
Wt::WString res;
|
||||||
|
|
||||||
// Year can be here, but originalYear can't be here without year (enforced by scanner)
|
// Year can be here, but originalYear can't be here without year (enforced by scanner)
|
||||||
if (!year)
|
if (!releaseDate.isValid())
|
||||||
return res;
|
return res;
|
||||||
|
|
||||||
if (originalYear && *originalYear != *year)
|
if (originalReleaseDate.isValid() && originalReleaseDate != releaseDate)
|
||||||
res = std::to_string(*originalYear) + " (" + std::to_string(*year) + ")";
|
res = std::to_string(originalReleaseDate.year()) + " (" + std::to_string(releaseDate.year()) + ")";
|
||||||
else
|
else
|
||||||
res = std::to_string(*year);
|
res = std::to_string(releaseDate.year());
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
|
|
||||||
#include <Wt/WString.h>
|
#include <Wt/WString.h>
|
||||||
#include <Wt/WTemplate.h>
|
#include <Wt/WTemplate.h>
|
||||||
|
#include <Wt/WDate.h>
|
||||||
#include "services/database/Object.hpp"
|
#include "services/database/Object.hpp"
|
||||||
#include "services/database/Types.hpp"
|
#include "services/database/Types.hpp"
|
||||||
#include "utils/EnumSet.hpp"
|
#include "utils/EnumSet.hpp"
|
||||||
@@ -43,5 +44,5 @@ namespace UserInterface::ReleaseListHelpers
|
|||||||
namespace UserInterface::ReleaseHelpers
|
namespace UserInterface::ReleaseHelpers
|
||||||
{
|
{
|
||||||
Wt::WString buildReleaseTypeString(Database::ReleaseTypePrimary primaryType, EnumSet<Database::ReleaseTypeSecondary> secondaryTypes);
|
Wt::WString buildReleaseTypeString(Database::ReleaseTypePrimary primaryType, EnumSet<Database::ReleaseTypeSecondary> secondaryTypes);
|
||||||
Wt::WString buildReleaseYearString(std::optional<int> year, std::optional<int> originalYear);
|
Wt::WString buildReleaseYearString(const Wt::WDate& releaseDate, const Wt::WDate& originalReleaseDate);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -248,7 +248,7 @@ Release::refreshView()
|
|||||||
|
|
||||||
bindString("name", Wt::WString::fromUTF8(release->getName()), Wt::TextFormat::Plain);
|
bindString("name", Wt::WString::fromUTF8(release->getName()), Wt::TextFormat::Plain);
|
||||||
|
|
||||||
Wt::WString year {ReleaseHelpers::buildReleaseYearString(release->getReleaseYear(), release->getReleaseYear(true))};
|
Wt::WString year {ReleaseHelpers::buildReleaseYearString(release->getReleaseDate(), release->getOriginalReleaseDate())};
|
||||||
if (!year.empty())
|
if (!year.empty())
|
||||||
{
|
{
|
||||||
setCondition("if-has-year", true);
|
setCondition("if-has-year", true);
|
||||||
|
|||||||
@@ -48,6 +48,14 @@ namespace UserInterface
|
|||||||
_artists = bindNew<InfiniteScrollingContainer>("artists", Wt::WString::tr("Lms.Explore.Artists.template.container"));
|
_artists = bindNew<InfiniteScrollingContainer>("artists", Wt::WString::tr("Lms.Explore.Artists.template.container"));
|
||||||
_artists->onRequestElements.connect([this] { addSomeArtists(); });
|
_artists->onRequestElements.connect([this] { addSomeArtists(); });
|
||||||
|
|
||||||
|
_artistLinkType = bindNew<Wt::WComboBox>("link-type");
|
||||||
|
_artistLinkType->setModel(ArtistListHelpers::createArtistLinkTypesModel());
|
||||||
|
_artistLinkType->changed().connect([this]
|
||||||
|
{
|
||||||
|
const std::optional<TrackArtistLinkType> linkType {static_cast<ArtistLinkTypesModel*>(_artistLinkType->model().get())->getValue(_artistLinkType->currentIndex())};
|
||||||
|
refreshView(linkType);
|
||||||
|
});
|
||||||
|
|
||||||
_releases = bindNew<InfiniteScrollingContainer>("releases", Wt::WString::tr("Lms.Explore.Releases.template.container"));
|
_releases = bindNew<InfiniteScrollingContainer>("releases", Wt::WString::tr("Lms.Explore.Releases.template.container"));
|
||||||
_releases->onRequestElements.connect([this] { addSomeReleases(); });
|
_releases->onRequestElements.connect([this] { addSomeReleases(); });
|
||||||
|
|
||||||
@@ -58,6 +66,12 @@ namespace UserInterface
|
|||||||
{
|
{
|
||||||
refreshView();
|
refreshView();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
LmsApp->getScannerEvents().scanComplete.connect(this, [this](const Scanner::ScanStats& stats)
|
||||||
|
{
|
||||||
|
if (stats.nbChanges())
|
||||||
|
_artistLinkType->setModel(ArtistListHelpers::createArtistLinkTypesModel());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
std::size_t
|
std::size_t
|
||||||
@@ -76,6 +90,13 @@ namespace UserInterface
|
|||||||
return it->second;
|
return it->second;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
SearchView::refreshView(std::optional<TrackArtistLinkType> linkType)
|
||||||
|
{
|
||||||
|
_artistCollector.setArtistLinkType(linkType);
|
||||||
|
refreshView();
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
SearchView::refreshView(const Wt::WString& searchText)
|
SearchView::refreshView(const Wt::WString& searchText)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -19,11 +19,14 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include <Wt/WComboBox.h>
|
||||||
#include <Wt/WStackedWidget.h>
|
#include <Wt/WStackedWidget.h>
|
||||||
#include <Wt/WTemplate.h>
|
#include <Wt/WTemplate.h>
|
||||||
|
|
||||||
|
#include "services/database/Types.hpp"
|
||||||
#include "ArtistCollector.hpp"
|
#include "ArtistCollector.hpp"
|
||||||
#include "ReleaseCollector.hpp"
|
#include "ReleaseCollector.hpp"
|
||||||
#include "TrackCollector.hpp"
|
#include "TrackCollector.hpp"
|
||||||
@@ -67,6 +70,7 @@ namespace UserInterface
|
|||||||
std::size_t getMaxCount(Mode mode) const;
|
std::size_t getMaxCount(Mode mode) const;
|
||||||
|
|
||||||
void refreshView();
|
void refreshView();
|
||||||
|
void refreshView(std::optional<Database::TrackArtistLinkType> linkType);
|
||||||
void addSomeArtists();
|
void addSomeArtists();
|
||||||
void addSomeReleases();
|
void addSomeReleases();
|
||||||
void addSomeTracks();
|
void addSomeTracks();
|
||||||
@@ -77,9 +81,11 @@ namespace UserInterface
|
|||||||
ReleaseCollector _releaseCollector;
|
ReleaseCollector _releaseCollector;
|
||||||
TrackCollector _trackCollector;
|
TrackCollector _trackCollector;
|
||||||
|
|
||||||
InfiniteScrollingContainer* _artists;
|
InfiniteScrollingContainer* _artists {};
|
||||||
InfiniteScrollingContainer* _releases;
|
InfiniteScrollingContainer* _releases {};
|
||||||
InfiniteScrollingContainer* _tracks;
|
InfiniteScrollingContainer* _tracks {};
|
||||||
|
|
||||||
|
Wt::WComboBox* _artistLinkType {};
|
||||||
|
|
||||||
std::vector<InfiniteScrollingContainer*> _results;
|
std::vector<InfiniteScrollingContainer*> _results;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -109,8 +109,8 @@ getReleasePathName(Database::Release::pointer release)
|
|||||||
{
|
{
|
||||||
std::string releaseName;
|
std::string releaseName;
|
||||||
|
|
||||||
if (auto releaseYear {release->getReleaseYear()})
|
if (const Wt::WDate releaseDate {release->getReleaseDate()}; releaseDate.isValid())
|
||||||
releaseName += std::to_string(*releaseYear) + " - ";
|
releaseName += std::to_string(releaseDate.year()) + " - ";
|
||||||
releaseName += StringUtils::replaceInString(release->getName(), "/", "_");
|
releaseName += StringUtils::replaceInString(release->getName(), "/", "_");
|
||||||
|
|
||||||
return releaseName;
|
return releaseName;
|
||||||
|
|||||||
Reference in New Issue
Block a user