Merge branch 'develop' for release v3.41.0
This commit is contained in:
@@ -8,10 +8,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check Out Repo
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
@@ -20,10 +20,10 @@ jobs:
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Build (alpine)
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: ./
|
||||
file: ./Dockerfile-build-alpine
|
||||
|
||||
@@ -8,10 +8,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check Out Repo
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
@@ -20,10 +20,10 @@ jobs:
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Build
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: ./
|
||||
file: ./Dockerfile-build-arch
|
||||
|
||||
+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.
|
||||
As root, trust the following debian package provider and add it in your list of repositories:
|
||||
```sh
|
||||
wget https://debian.poupon.dev/apt/debian/epoupon.gpg -P /usr/share/keyrings
|
||||
wget --backups=1 https://debian.poupon.dev/apt/debian/epoupon.gpg -P /usr/share/keyrings
|
||||
echo "deb [signed-by=/usr/share/keyrings/epoupon.gpg] https://debian.poupon.dev/apt/debian bookworm main" > /etc/apt/sources.list.d/epoupon.list
|
||||
```
|
||||
To install or upgrade _LMS_:
|
||||
|
||||
@@ -18,7 +18,7 @@ A [demo instance](http://lms-demo.poupon.dev) is available. Note the administrat
|
||||
* Synchronizing 'love' feedbacks
|
||||
* ReplayGain support
|
||||
* User management, with several [authentication backends](INSTALL.md#authentication-backend)
|
||||
* Subsonic API
|
||||
* [Subsonic/OpenSubsonic API](SUBSONIC.md) support
|
||||
|
||||
## Music discovery
|
||||
_LMS_ provides several ways to help you find the music you like:
|
||||
@@ -34,13 +34,6 @@ _LMS_ provides several ways to help you find the music you like:
|
||||
* Starred _Jazz_ albums
|
||||
* ...
|
||||
|
||||
## Subsonic API
|
||||
The API version implemented is 1.16.0 and has been tested on _Android_ using _Subsonic Player_, _Ultrasonic_, _Symfonium_, and _DSub_.
|
||||
Since _LMS_ uses metadata tags to organize music, a compatibility mode is used to browse the collection when using the directory browsing commands.
|
||||
The Subsonic API is enabled by default.
|
||||
|
||||
__Note__: since _LMS_ may store hashed and salted passwords or may forward authentication requests to external services, it cannot handle the __token authentication__ method. You may need to check your client to make sure to use the __password__ authentication method.
|
||||
|
||||
## About tags
|
||||
_LMS_ relies exclusively on tags to organize your music collection.
|
||||
|
||||
|
||||
+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>
|
||||
<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" 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>
|
||||
</message>
|
||||
|
||||
@@ -491,7 +491,7 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
|
||||
//MP4
|
||||
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()};
|
||||
if (!coverArtList.isEmpty())
|
||||
track.hasCover = true;
|
||||
|
||||
@@ -28,7 +28,33 @@
|
||||
#include "SqlQuery.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace Database {
|
||||
namespace Database
|
||||
{
|
||||
namespace
|
||||
{
|
||||
Wt::Dbo::Query<ClusterId> createQuery(Session& session, const Cluster::FindParameters& params)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query{ session.getDboSession().query<ClusterId>("SELECT DISTINCT c.id FROM cluster c") };
|
||||
|
||||
if (params.track.isValid() || params.release.isValid())
|
||||
{
|
||||
query.join("track_cluster t_c ON t_c.cluster_id = c.id");
|
||||
query.join("track t ON t.id = t_c.track_id");
|
||||
}
|
||||
|
||||
if (params.track.isValid())
|
||||
query.where("t.id = ?").bind(params.track);
|
||||
if (params.release.isValid())
|
||||
query.where("t.release_id = ?").bind(params.release);
|
||||
|
||||
if (params.clusterType.isValid())
|
||||
query.where("c.cluster_type_id = ?").bind(params.clusterType);
|
||||
|
||||
return query;
|
||||
}
|
||||
}
|
||||
|
||||
Cluster::Cluster(ObjectPtr<ClusterType> type, std::string_view name)
|
||||
: _name{ std::string {name, 0, _maxNameLength} },
|
||||
@@ -36,31 +62,27 @@ Cluster::Cluster(ObjectPtr<ClusterType> type, std::string_view name)
|
||||
{
|
||||
}
|
||||
|
||||
Cluster::pointer
|
||||
Cluster::create(Session& session, ObjectPtr<ClusterType> type, std::string_view name)
|
||||
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 }});
|
||||
}
|
||||
|
||||
std::size_t
|
||||
Cluster::getCount(Session& session)
|
||||
std::size_t Cluster::getCount(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM cluster");
|
||||
}
|
||||
|
||||
RangeResults<ClusterId>
|
||||
Cluster::find(Session& session, Range range)
|
||||
RangeResults<ClusterId> Cluster::find(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
auto query {session.getDboSession().query<ClusterId>("SELECT id FROM cluster")};
|
||||
auto query{ createQuery(session, params) };
|
||||
|
||||
return Utils::execQuery(query, range);
|
||||
return Utils::execQuery(query, params.range);
|
||||
}
|
||||
|
||||
RangeResults<ClusterId>
|
||||
Cluster::findOrphans(Session& session, Range range)
|
||||
RangeResults<ClusterId> Cluster::findOrphans(Session& session, Range range)
|
||||
{
|
||||
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)") };
|
||||
@@ -68,22 +90,19 @@ Cluster::findOrphans(Session& session, Range range)
|
||||
return Utils::execQuery(query, range);
|
||||
}
|
||||
|
||||
Cluster::pointer
|
||||
Cluster::find(Session& session, ClusterId id)
|
||||
Cluster::pointer Cluster::find(Session& session, ClusterId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().find<Cluster>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
|
||||
void
|
||||
Cluster::addTrack(ObjectPtr<Track> track)
|
||||
void Cluster::addTrack(ObjectPtr<Track> track)
|
||||
{
|
||||
_tracks.insert(getDboPtr(track));
|
||||
}
|
||||
|
||||
RangeResults<TrackId>
|
||||
Cluster::getTracks(Range range) const
|
||||
RangeResults<TrackId> Cluster::getTracks(Range range) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
@@ -93,8 +112,7 @@ Cluster::getTracks(Range range) const
|
||||
return Utils::execQuery(query, range);
|
||||
}
|
||||
|
||||
std::size_t
|
||||
Cluster::getReleasesCount() const
|
||||
std::size_t Cluster::getReleasesCount() const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
@@ -108,14 +126,12 @@ ClusterType::ClusterType(std::string_view name)
|
||||
{
|
||||
}
|
||||
|
||||
ClusterType::pointer
|
||||
ClusterType::create(Session& session, const std::string& name)
|
||||
ClusterType::pointer ClusterType::create(Session& session, const std::string& name)
|
||||
{
|
||||
return session.getDboSession().add(std::unique_ptr<ClusterType> {new ClusterType{ name }});
|
||||
}
|
||||
|
||||
std::size_t
|
||||
ClusterType::getCount(Session& session)
|
||||
std::size_t ClusterType::getCount(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
@@ -123,8 +139,7 @@ ClusterType::getCount(Session& session)
|
||||
}
|
||||
|
||||
|
||||
RangeResults<ClusterTypeId>
|
||||
ClusterType::findOrphans(Session& session, Range range)
|
||||
RangeResults<ClusterTypeId> ClusterType::findOrphans(Session& session, Range range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
@@ -136,8 +151,7 @@ ClusterType::findOrphans(Session& session, Range range)
|
||||
return Utils::execQuery(query, range);
|
||||
}
|
||||
|
||||
RangeResults<ClusterTypeId>
|
||||
ClusterType::findUsed(Session& session, Range range)
|
||||
RangeResults<ClusterTypeId> ClusterType::findUsed(Session& session, Range range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
@@ -148,24 +162,21 @@ ClusterType::findUsed(Session& session, Range range)
|
||||
return Utils::execQuery(query, range);
|
||||
}
|
||||
|
||||
ClusterType::pointer
|
||||
ClusterType::find(Session& session, const std::string& name)
|
||||
ClusterType::pointer ClusterType::find(Session& session, std::string_view name)
|
||||
{
|
||||
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::find(Session& session, ClusterTypeId id)
|
||||
ClusterType::pointer ClusterType::find(Session& session, ClusterTypeId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().find<ClusterType>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
|
||||
RangeResults<ClusterTypeId>
|
||||
ClusterType::find(Session& session, Range range)
|
||||
RangeResults<ClusterTypeId> ClusterType::find(Session& session, Range range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
@@ -174,8 +185,7 @@ ClusterType::find(Session& session, Range range)
|
||||
return Utils::execQuery(query, range);
|
||||
}
|
||||
|
||||
Cluster::pointer
|
||||
ClusterType::getCluster(const std::string& name) const
|
||||
Cluster::pointer ClusterType::getCluster(const std::string& name) const
|
||||
{
|
||||
assert(self());
|
||||
assert(session());
|
||||
@@ -185,8 +195,7 @@ ClusterType::getCluster(const std::string& name) const
|
||||
.where("cluster_type_id = ?").bind(getId()).resultValue();
|
||||
}
|
||||
|
||||
std::vector<Cluster::pointer>
|
||||
ClusterType::getClusters() const
|
||||
std::vector<Cluster::pointer> ClusterType::getClusters() const
|
||||
{
|
||||
assert(self());
|
||||
assert(session());
|
||||
@@ -198,7 +207,4 @@ ClusterType::getClusters() const
|
||||
|
||||
return std::vector<Cluster::pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
|
||||
} // namespace Database
|
||||
|
||||
|
||||
@@ -35,8 +35,7 @@
|
||||
namespace Database
|
||||
{
|
||||
|
||||
Wt::Dbo::Query<ReleaseId>
|
||||
createQuery(Session& session, const Release::FindParameters& params)
|
||||
Wt::Dbo::Query<ReleaseId> createQuery(Session& session, const Release::FindParameters& params)
|
||||
{
|
||||
auto query{ session.getDboSession().query<ReleaseId>("SELECT DISTINCT r.id from release r") };
|
||||
|
||||
@@ -184,14 +183,12 @@ _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 }});
|
||||
}
|
||||
|
||||
std::vector<Release::pointer>
|
||||
Release::find(Session& session, const std::string& name)
|
||||
std::vector<Release::pointer> Release::find(Session& session, const std::string& name)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
|
||||
@@ -203,8 +200,7 @@ Release::find(Session& session, const std::string& name)
|
||||
return std::vector<Release::pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
Release::pointer
|
||||
Release::find(Session& session, const UUID& mbid)
|
||||
Release::pointer Release::find(Session& session, const UUID& mbid)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
@@ -214,8 +210,7 @@ Release::find(Session& session, const UUID& mbid)
|
||||
.resultValue();;
|
||||
}
|
||||
|
||||
Release::pointer
|
||||
Release::find(Session& session, ReleaseId id)
|
||||
Release::pointer Release::find(Session& session, ReleaseId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
@@ -225,23 +220,20 @@ Release::find(Session& session, ReleaseId id)
|
||||
.resultValue();
|
||||
}
|
||||
|
||||
bool
|
||||
Release::exists(Session& session, ReleaseId id)
|
||||
bool Release::exists(Session& session, ReleaseId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
return session.getDboSession().query<int>("SELECT 1 FROM release").where("id = ?").bind(id).resultValue() == 1;
|
||||
}
|
||||
|
||||
std::size_t
|
||||
Release::getCount(Session& session)
|
||||
std::size_t Release::getCount(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM release");
|
||||
}
|
||||
|
||||
RangeResults<ReleaseId>
|
||||
Release::findOrderedByArtist(Session& session, Range range)
|
||||
RangeResults<ReleaseId> Release::findOrderedByArtist(Session& session, Range range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
@@ -256,8 +248,7 @@ Release::findOrderedByArtist(Session& session, Range range)
|
||||
return Utils::execQuery(query, range);
|
||||
}
|
||||
|
||||
RangeResults<ReleaseId>
|
||||
Release::findOrphans(Session& session, Range range)
|
||||
RangeResults<ReleaseId> Release::findOrphans(Session& session, Range range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
@@ -265,8 +256,7 @@ Release::findOrphans(Session& session, Range range)
|
||||
return Utils::execQuery(query, range);
|
||||
}
|
||||
|
||||
RangeResults<ReleaseId>
|
||||
Release::find(Session& session, const FindParameters& params)
|
||||
RangeResults<ReleaseId> Release::find(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
@@ -275,8 +265,7 @@ Release::find(Session& session, const FindParameters& params)
|
||||
return Utils::execQuery(query, params.range);
|
||||
}
|
||||
|
||||
std::size_t
|
||||
Release::getDiscCount() const
|
||||
std::size_t Release::getDiscCount() const
|
||||
{
|
||||
assert(session());
|
||||
int res{ session()->query<int>("SELECT COUNT(DISTINCT disc_number) FROM track t")
|
||||
@@ -287,8 +276,35 @@ Release::getDiscCount() const
|
||||
return res;
|
||||
}
|
||||
|
||||
std::optional<int>
|
||||
Release::getReleaseYear(bool original) const
|
||||
std::vector<DiscInfo> Release::getDiscs() const
|
||||
{
|
||||
assert(session());
|
||||
using ResultType = std::tuple<int, std::string>;
|
||||
auto results{ session()->query<ResultType>("SELECT DISTINCT disc_number, disc_subtitle FROM track t")
|
||||
.join("release r ON r.id = t.release_id")
|
||||
.where("r.id = ?")
|
||||
.orderBy("disc_number")
|
||||
.bind(getId())
|
||||
.resultList() };
|
||||
|
||||
std::vector<DiscInfo> discs;
|
||||
for (const auto& res : results)
|
||||
discs.emplace_back(DiscInfo{ static_cast<std::size_t>(std::get<int>(res)), std::get<std::string>(res) });
|
||||
|
||||
return discs;
|
||||
}
|
||||
|
||||
Wt::WDate Release::getReleaseDate() const
|
||||
{
|
||||
return getReleaseDate(false);
|
||||
}
|
||||
|
||||
Wt::WDate Release::getOriginalReleaseDate() const
|
||||
{
|
||||
return getReleaseDate(true);
|
||||
}
|
||||
|
||||
Wt::WDate Release::getReleaseDate(bool original) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
@@ -301,20 +317,14 @@ Release::getReleaseYear(bool original) const
|
||||
.bind(getId())
|
||||
.resultList() };
|
||||
|
||||
// various dates => no date
|
||||
// various dates => invalid date
|
||||
if (dates.empty() || dates.size() > 1)
|
||||
return std::nullopt;
|
||||
return {};
|
||||
|
||||
auto date {dates.front().year()};
|
||||
|
||||
if (date > 0)
|
||||
return date;
|
||||
|
||||
return std::nullopt;
|
||||
return dates.front();
|
||||
}
|
||||
|
||||
std::optional<std::string>
|
||||
Release::getCopyright() const
|
||||
std::optional<std::string> Release::getCopyright() const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
@@ -333,8 +343,7 @@ Release::getCopyright() const
|
||||
return values.front();
|
||||
}
|
||||
|
||||
std::optional<std::string>
|
||||
Release::getCopyrightURL() const
|
||||
std::optional<std::string> Release::getCopyrightURL() const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
@@ -353,8 +362,7 @@ Release::getCopyrightURL() const
|
||||
return values.front();
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer>
|
||||
Release::getArtists(TrackArtistLinkType linkType) const
|
||||
std::vector<Artist::pointer> Release::getArtists(TrackArtistLinkType linkType) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
@@ -370,8 +378,7 @@ Release::getArtists(TrackArtistLinkType linkType) const
|
||||
return std::vector<Artist::pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<Release::pointer>
|
||||
Release::getSimilarReleases(std::optional<std::size_t> offset, std::optional<std::size_t> count) const
|
||||
std::vector<Release::pointer> Release::getSimilarReleases(std::optional<std::size_t> offset, std::optional<std::size_t> count) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
@@ -394,21 +401,18 @@ Release::getSimilarReleases(std::optional<std::size_t> offset, std::optional<std
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
bool
|
||||
Release::hasVariousArtists() const
|
||||
bool Release::hasVariousArtists() const
|
||||
{
|
||||
// TODO optimize
|
||||
return getArtists().size() > 1;
|
||||
}
|
||||
|
||||
std::size_t
|
||||
Release::getTracksCount() const
|
||||
std::size_t Release::getTracksCount() const
|
||||
{
|
||||
return _tracks.size();
|
||||
}
|
||||
|
||||
std::chrono::milliseconds
|
||||
Release::getDuration() const
|
||||
std::chrono::milliseconds Release::getDuration() const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
@@ -420,8 +424,7 @@ Release::getDuration() const
|
||||
return query.resultValue();
|
||||
}
|
||||
|
||||
Wt::WDateTime
|
||||
Release::getLastWritten() const
|
||||
Wt::WDateTime Release::getLastWritten() const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
@@ -431,8 +434,7 @@ Release::getLastWritten() const
|
||||
return query.resultValue();
|
||||
}
|
||||
|
||||
std::vector<std::vector<Cluster::pointer>>
|
||||
Release::getClusterGroups(const std::vector<ClusterType::pointer>& clusterTypes, std::size_t size) const
|
||||
std::vector<std::vector<Cluster::pointer>> Release::getClusterGroups(const std::vector<ClusterType::pointer>& clusterTypes, std::size_t size) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
|
||||
@@ -28,10 +28,9 @@
|
||||
|
||||
namespace Database
|
||||
{
|
||||
|
||||
static
|
||||
Wt::Dbo::Query<TrackArtistLinkId>
|
||||
createQuery(Session& session, const TrackArtistLink::FindParameters& params)
|
||||
namespace
|
||||
{
|
||||
Wt::Dbo::Query<TrackArtistLinkId> createQuery(Session& session, const TrackArtistLink::FindParameters& params)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
@@ -43,14 +42,18 @@ namespace Database
|
||||
if (params.track.isValid() || params.release.isValid())
|
||||
query.join("track t ON t.id = t_a_l.track_id");
|
||||
|
||||
if (params.track.isValid())
|
||||
query.where("t.id = ?").bind(params.track);
|
||||
if (params.artist.isValid())
|
||||
query.join("artist a ON a.id = t_a_l.artist_id");
|
||||
|
||||
if (params.release.isValid())
|
||||
query.where("t.release_id = ?").bind(params.release);
|
||||
|
||||
if (params.track.isValid())
|
||||
query.where("t.id = ?").bind(params.track);
|
||||
|
||||
return query;
|
||||
}
|
||||
}
|
||||
|
||||
TrackArtistLink::TrackArtistLink(ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType)
|
||||
: _type{ type }
|
||||
@@ -60,8 +63,7 @@ namespace Database
|
||||
{
|
||||
}
|
||||
|
||||
TrackArtistLink::pointer
|
||||
TrackArtistLink::create(Session& session, ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType)
|
||||
TrackArtistLink::pointer TrackArtistLink::create(Session& session, ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
|
||||
@@ -71,15 +73,13 @@ namespace Database
|
||||
return res;
|
||||
}
|
||||
|
||||
TrackArtistLink::pointer
|
||||
TrackArtistLink::find(Session& session, TrackArtistLinkId id)
|
||||
TrackArtistLink::pointer TrackArtistLink::find(Session& session, TrackArtistLinkId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
return session.getDboSession().find<TrackArtistLink>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
|
||||
RangeResults<TrackArtistLinkId>
|
||||
TrackArtistLink::find(Session& session, const FindParameters& params)
|
||||
RangeResults<TrackArtistLinkId> TrackArtistLink::find(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
@@ -87,9 +87,7 @@ namespace Database
|
||||
return Utils::execQuery(query, params.range);
|
||||
}
|
||||
|
||||
|
||||
EnumSet<TrackArtistLinkType>
|
||||
TrackArtistLink::findUsedTypes(Session& session)
|
||||
EnumSet<TrackArtistLinkType> TrackArtistLink::findUsedTypes(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
@@ -97,5 +95,17 @@ namespace Database
|
||||
|
||||
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,8 +26,9 @@
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
#include <Wt/WDateTime.h>
|
||||
|
||||
#include "services/database/Object.hpp"
|
||||
#include "services/database/ClusterId.hpp"
|
||||
#include "services/database/Object.hpp"
|
||||
#include "services/database/Release.hpp"
|
||||
#include "services/database/TrackId.hpp"
|
||||
#include "services/database/Types.hpp"
|
||||
|
||||
@@ -41,11 +42,24 @@ class Session;
|
||||
class Cluster final : public Object<Cluster, ClusterId>
|
||||
{
|
||||
public:
|
||||
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
|
||||
|
||||
FindParameters& setRange(Range _range) { range = _range; return *this; }
|
||||
FindParameters& setClusterType(ClusterTypeId _clusterType) { clusterType = _clusterType; return *this; }
|
||||
FindParameters& setTrack(TrackId _track) { track = _track; return *this; }
|
||||
FindParameters& setRelease(ReleaseId _release) { release = _release; return *this; }
|
||||
};
|
||||
|
||||
Cluster() = default;
|
||||
|
||||
// Find utility
|
||||
static std::size_t getCount(Session& session);
|
||||
static RangeResults<ClusterId> find(Session& session, Range range);
|
||||
static RangeResults<ClusterId> find(Session& session, const FindParameters& range);
|
||||
static pointer find(Session& session, ClusterId id);
|
||||
static RangeResults<ClusterId> findOrphans(Session& session, Range range);
|
||||
|
||||
@@ -89,7 +103,7 @@ class ClusterType final : public Object<ClusterType, ClusterTypeId>
|
||||
// Getters
|
||||
static std::size_t getCount(Session& session);
|
||||
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 RangeResults<ClusterTypeId> findOrphans(Session& session, Range range);
|
||||
static RangeResults<ClusterTypeId> findUsed(Session& session, Range range);
|
||||
|
||||
@@ -92,15 +92,14 @@ class Release final : public Object<Release, ReleaseId>
|
||||
static RangeResults<ReleaseId> findOrphans(Session& session, Range range); // no track related
|
||||
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;
|
||||
|
||||
// Utility functions (if all tracks have the same values, which is legit to not be the case)
|
||||
std::optional<int> getReleaseYear(bool originalDate = false) const;
|
||||
Wt::WDate getReleaseDate() const;
|
||||
Wt::WDate getOriginalReleaseDate() const;
|
||||
std::optional<std::string> getCopyright() const;
|
||||
std::optional<std::string> getCopyrightURL() const;
|
||||
|
||||
@@ -109,10 +108,12 @@ class Release final : public Object<Release, ReleaseId>
|
||||
std::optional<UUID> getMBID() const { return UUID::fromString(_MBID); }
|
||||
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;
|
||||
|
||||
// Setters
|
||||
void setName(std::string_view name) { _name = name; }
|
||||
@@ -145,6 +146,8 @@ class Release final : public Object<Release, ReleaseId>
|
||||
Release(const std::string& name, const std::optional<UUID>& MBID = {});
|
||||
static pointer create(Session& session, const std::string& name, const std::optional<UUID>& MBID = {});
|
||||
|
||||
Wt::WDate getReleaseDate(bool original) const;
|
||||
|
||||
static constexpr std::size_t _maxNameLength{ 128 };
|
||||
|
||||
std::string _name;
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
|
||||
#include "services/database/ArtistId.hpp"
|
||||
#include "services/database/IdType.hpp"
|
||||
#include "services/database/Object.hpp"
|
||||
#include "services/database/ReleaseId.hpp"
|
||||
@@ -46,13 +47,15 @@ namespace Database
|
||||
{
|
||||
Range range;
|
||||
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
|
||||
ReleaseId release; // artists involved in this release
|
||||
ArtistId artist; // if set, links involved with this artist
|
||||
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& 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& setTrack(TrackId _track) { track = _track; return *this; }
|
||||
};
|
||||
|
||||
TrackArtistLink() = default;
|
||||
@@ -62,6 +65,7 @@ namespace Database
|
||||
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 EnumSet<TrackArtistLinkType> findUsedTypes(Session& session);
|
||||
static EnumSet<TrackArtistLinkType> findUsedTypes(Session& session, ArtistId _artist);
|
||||
|
||||
ObjectPtr<Track> getTrack() const { return _track; }
|
||||
ObjectPtr<Artist> getArtist() const { return _artist; }
|
||||
|
||||
@@ -83,6 +83,12 @@ namespace Database
|
||||
static DateRange fromYearRange(int from, int to);
|
||||
};
|
||||
|
||||
struct DiscInfo
|
||||
{
|
||||
std::size_t position;
|
||||
std::string name;
|
||||
};
|
||||
|
||||
enum class ArtistSortMethod
|
||||
{
|
||||
None,
|
||||
|
||||
@@ -179,6 +179,15 @@ TEST_F(DatabaseFixture, Artist_singleTracktMultiRoles)
|
||||
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::Composer}));
|
||||
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)
|
||||
|
||||
@@ -48,7 +48,7 @@ TEST_F(DatabaseFixture, Cluster)
|
||||
EXPECT_EQ(Cluster::getCount(session), 1);
|
||||
EXPECT_EQ(cluster->getType()->getId(), clusterType.getId());
|
||||
|
||||
auto clusters {Cluster::find(session, Range {})};
|
||||
auto clusters{ Cluster::find(session, Cluster::FindParameters {}) };
|
||||
ASSERT_EQ(clusters.results.size(), 1);
|
||||
EXPECT_EQ(clusters.results.front(), cluster.getId());
|
||||
|
||||
@@ -110,6 +110,13 @@ TEST_F(DatabaseFixture, Cluster_singleTrack)
|
||||
cluster1.get().modify()->addTrack(track.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto clusters{ Cluster::find(session, Cluster::FindParameters {}.setTrack(track.getId())) };
|
||||
ASSERT_EQ(clusters.results.size(), 1);
|
||||
EXPECT_EQ(clusters.results.front(), cluster1.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto clusters{ Cluster::findOrphans(session, Range {}) };
|
||||
@@ -307,6 +314,14 @@ TEST_F(DatabaseFixture, Cluster_singleTrackSingleReleaseSingleCluster)
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
|
||||
auto clusters{ Cluster::find(session, Cluster::FindParameters{}.setRelease(release.getId())) };
|
||||
ASSERT_EQ(clusters.results.size(), 1);
|
||||
EXPECT_EQ(clusters.results.front(), cluster.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
|
||||
|
||||
@@ -350,8 +350,8 @@ TEST_F(DatabaseFixture, MultiTracksSingleReleaseDate)
|
||||
track1A.get().modify()->setOriginalDate(release1OriginalDate);
|
||||
track1B.get().modify()->setOriginalDate(release1OriginalDate);
|
||||
|
||||
EXPECT_EQ(release1.get()->getReleaseYear(), release1Date.year());
|
||||
EXPECT_EQ(release1.get()->getReleaseYear(true), release1OriginalDate.year());
|
||||
EXPECT_EQ(release1.get()->getReleaseDate(), release1Date);
|
||||
EXPECT_EQ(release1.get()->getOriginalReleaseDate(), release1OriginalDate);
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -39,8 +39,7 @@ namespace Scrobbling
|
||||
{
|
||||
using namespace Database;
|
||||
|
||||
std::unique_ptr<IScrobblingService>
|
||||
createScrobblingService(boost::asio::io_context& ioContext, Db& db)
|
||||
std::unique_ptr<IScrobblingService> createScrobblingService(boost::asio::io_context& ioContext, Db& db)
|
||||
{
|
||||
return std::make_unique<ScrobblingService>(ioContext, db);
|
||||
}
|
||||
@@ -59,29 +58,25 @@ namespace Scrobbling
|
||||
LMS_LOG(SCROBBLING, INFO) << "Service stopped!";
|
||||
}
|
||||
|
||||
void
|
||||
ScrobblingService::listenStarted(const Listen& listen)
|
||||
void ScrobblingService::listenStarted(const Listen& listen)
|
||||
{
|
||||
if (std::optional<Scrobbler> scrobbler{ getUserScrobbler(listen.userId) })
|
||||
_scrobblers[*scrobbler]->listenStarted(listen);
|
||||
}
|
||||
|
||||
void
|
||||
ScrobblingService::listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration)
|
||||
void ScrobblingService::listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration)
|
||||
{
|
||||
if (std::optional<Scrobbler> scrobbler{ getUserScrobbler(listen.userId) })
|
||||
_scrobblers[*scrobbler]->listenFinished(listen, duration);
|
||||
}
|
||||
|
||||
void
|
||||
ScrobblingService::addTimedListen(const TimedListen& listen)
|
||||
void ScrobblingService::addTimedListen(const TimedListen& listen)
|
||||
{
|
||||
if (std::optional<Scrobbler> scrobbler{ getUserScrobbler(listen.userId) })
|
||||
_scrobblers[*scrobbler]->addTimedListen(listen);
|
||||
}
|
||||
|
||||
std::optional<Scrobbler>
|
||||
ScrobblingService::getUserScrobbler(UserId userId)
|
||||
std::optional<Scrobbler> ScrobblingService::getUserScrobbler(UserId userId)
|
||||
{
|
||||
std::optional<Scrobbler> scrobbler;
|
||||
|
||||
@@ -93,8 +88,7 @@ namespace Scrobbling
|
||||
return scrobbler;
|
||||
}
|
||||
|
||||
ScrobblingService::ArtistContainer
|
||||
ScrobblingService::getRecentArtists(UserId userId, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range)
|
||||
ScrobblingService::ArtistContainer ScrobblingService::getRecentArtists(UserId userId, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range)
|
||||
{
|
||||
ArtistContainer res;
|
||||
|
||||
@@ -109,8 +103,7 @@ namespace Scrobbling
|
||||
return res;
|
||||
}
|
||||
|
||||
ScrobblingService::ReleaseContainer
|
||||
ScrobblingService::getRecentReleases(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
|
||||
ScrobblingService::ReleaseContainer ScrobblingService::getRecentReleases(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
|
||||
{
|
||||
ReleaseContainer res;
|
||||
|
||||
@@ -125,8 +118,7 @@ namespace Scrobbling
|
||||
return res;
|
||||
}
|
||||
|
||||
ScrobblingService::TrackContainer
|
||||
ScrobblingService::getRecentTracks(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
|
||||
ScrobblingService::TrackContainer ScrobblingService::getRecentTracks(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
|
||||
{
|
||||
TrackContainer res;
|
||||
|
||||
@@ -142,8 +134,7 @@ namespace Scrobbling
|
||||
}
|
||||
|
||||
// Top
|
||||
ScrobblingService::ArtistContainer
|
||||
ScrobblingService::getTopArtists(UserId userId, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range)
|
||||
ScrobblingService::ArtistContainer ScrobblingService::getTopArtists(UserId userId, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range)
|
||||
{
|
||||
ArtistContainer res;
|
||||
|
||||
@@ -158,8 +149,7 @@ namespace Scrobbling
|
||||
return res;
|
||||
}
|
||||
|
||||
ScrobblingService::ReleaseContainer
|
||||
ScrobblingService::getTopReleases(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
|
||||
ScrobblingService::ReleaseContainer ScrobblingService::getTopReleases(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
|
||||
{
|
||||
ReleaseContainer res;
|
||||
|
||||
@@ -174,8 +164,7 @@ namespace Scrobbling
|
||||
return res;
|
||||
}
|
||||
|
||||
ScrobblingService::TrackContainer
|
||||
ScrobblingService::getTopTracks(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
|
||||
ScrobblingService::TrackContainer ScrobblingService::getTopTracks(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
|
||||
{
|
||||
TrackContainer res;
|
||||
|
||||
@@ -190,26 +179,27 @@ namespace Scrobbling
|
||||
return res;
|
||||
}
|
||||
|
||||
void
|
||||
ScrobblingService::star(UserId userId, ArtistId artistId)
|
||||
void ScrobblingService::star(UserId userId, ArtistId artistId)
|
||||
{
|
||||
star<Artist, ArtistId, StarredArtist>(userId, artistId);
|
||||
}
|
||||
|
||||
void
|
||||
ScrobblingService::unstar(UserId userId, ArtistId artistId)
|
||||
void ScrobblingService::unstar(UserId userId, ArtistId artistId)
|
||||
{
|
||||
unstar<Artist, ArtistId, StarredArtist>(userId, artistId);
|
||||
}
|
||||
|
||||
bool
|
||||
ScrobblingService::isStarred(UserId userId, ArtistId artistId)
|
||||
bool ScrobblingService::isStarred(UserId userId, ArtistId artistId)
|
||||
{
|
||||
return isStarred<Artist, ArtistId, StarredArtist>(userId, artistId);
|
||||
}
|
||||
|
||||
ScrobblingService::ArtistContainer
|
||||
ScrobblingService::getStarredArtists(UserId userId, const std::vector<ClusterId>& clusterIds,
|
||||
Wt::WDateTime ScrobblingService::getStarredDateTime(UserId userId, ArtistId artistId)
|
||||
{
|
||||
return getStarredDateTime<Artist, ArtistId, StarredArtist>(userId, artistId);
|
||||
}
|
||||
|
||||
ScrobblingService::ArtistContainer ScrobblingService::getStarredArtists(UserId userId, const std::vector<ClusterId>& clusterIds,
|
||||
std::optional<TrackArtistLinkType> linkType,
|
||||
ArtistSortMethod sortMethod,
|
||||
Range range)
|
||||
@@ -231,26 +221,27 @@ namespace Scrobbling
|
||||
return Artist::find(session, params);
|
||||
}
|
||||
|
||||
void
|
||||
ScrobblingService::star(UserId userId, ReleaseId releaseId)
|
||||
void ScrobblingService::star(UserId userId, ReleaseId releaseId)
|
||||
{
|
||||
star<Release, ReleaseId, StarredRelease>(userId, releaseId);
|
||||
}
|
||||
|
||||
void
|
||||
ScrobblingService::unstar(UserId userId, ReleaseId releaseId)
|
||||
void ScrobblingService::unstar(UserId userId, ReleaseId releaseId)
|
||||
{
|
||||
unstar<Release, ReleaseId, StarredRelease>(userId, releaseId);
|
||||
}
|
||||
|
||||
bool
|
||||
ScrobblingService::isStarred(UserId userId, ReleaseId releaseId)
|
||||
bool ScrobblingService::isStarred(UserId userId, ReleaseId releaseId)
|
||||
{
|
||||
return isStarred<Release, ReleaseId, StarredRelease>(userId, releaseId);
|
||||
}
|
||||
|
||||
ScrobblingService::ReleaseContainer
|
||||
ScrobblingService::getStarredReleases(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
|
||||
Wt::WDateTime ScrobblingService::getStarredDateTime(UserId userId, ReleaseId releaseId)
|
||||
{
|
||||
return getStarredDateTime<Release, ReleaseId, StarredRelease>(userId, releaseId);
|
||||
}
|
||||
|
||||
ScrobblingService::ReleaseContainer ScrobblingService::getStarredReleases(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
|
||||
{
|
||||
auto scrobbler{ getUserScrobbler(userId) };
|
||||
if (!scrobbler)
|
||||
@@ -268,26 +259,27 @@ namespace Scrobbling
|
||||
return Release::find(session, params);
|
||||
}
|
||||
|
||||
void
|
||||
ScrobblingService::star(UserId userId, TrackId trackId)
|
||||
void ScrobblingService::star(UserId userId, TrackId trackId)
|
||||
{
|
||||
star<Track, TrackId, StarredTrack>(userId, trackId);
|
||||
}
|
||||
|
||||
void
|
||||
ScrobblingService::unstar(UserId userId, TrackId trackId)
|
||||
void ScrobblingService::unstar(UserId userId, TrackId trackId)
|
||||
{
|
||||
unstar<Track, TrackId, StarredTrack>(userId, trackId);
|
||||
}
|
||||
|
||||
bool
|
||||
ScrobblingService::isStarred(UserId userId, TrackId trackId)
|
||||
bool ScrobblingService::isStarred(UserId userId, TrackId trackId)
|
||||
{
|
||||
return isStarred<Track, TrackId, StarredTrack>(userId, trackId);
|
||||
}
|
||||
|
||||
ScrobblingService::TrackContainer
|
||||
ScrobblingService::getStarredTracks(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
|
||||
Wt::WDateTime ScrobblingService::getStarredDateTime(UserId userId, TrackId trackId)
|
||||
{
|
||||
return getStarredDateTime<Track, TrackId, StarredTrack>(userId, trackId);
|
||||
}
|
||||
|
||||
ScrobblingService::TrackContainer ScrobblingService::getStarredTracks(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
|
||||
{
|
||||
auto scrobbler{ getUserScrobbler(userId) };
|
||||
if (!scrobbler)
|
||||
|
||||
@@ -68,6 +68,7 @@ namespace Scrobbling
|
||||
void star(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;
|
||||
Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::ArtistId artistId) override;
|
||||
ArtistContainer getStarredArtists(Database::UserId userId,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::TrackArtistLinkType> linkType,
|
||||
@@ -76,12 +77,14 @@ namespace Scrobbling
|
||||
|
||||
void star(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;
|
||||
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 unstar(Database::UserId userId, Database::TrackId trackId) override;
|
||||
bool isStarred(Database::UserId userId, Database::TrackId trackId) 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);
|
||||
@@ -92,6 +95,8 @@ namespace Scrobbling
|
||||
void unstar(Database::UserId userId, ObjIdType id);
|
||||
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
||||
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;
|
||||
std::unordered_map<Database::Scrobbler, std::unique_ptr<IScrobbler>> _scrobblers;
|
||||
|
||||
@@ -28,8 +28,7 @@ namespace Scrobbling
|
||||
using namespace Database;
|
||||
|
||||
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
||||
void
|
||||
ScrobblingService::star(UserId userId, ObjIdType objId)
|
||||
void ScrobblingService::star(UserId userId, ObjIdType objId)
|
||||
{
|
||||
auto scrobbler {getUserScrobbler(userId)};
|
||||
if (!scrobbler)
|
||||
@@ -60,8 +59,7 @@ namespace Scrobbling
|
||||
}
|
||||
|
||||
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
||||
void
|
||||
ScrobblingService::unstar(UserId userId, ObjIdType objId)
|
||||
void ScrobblingService::unstar(UserId userId, ObjIdType objId)
|
||||
{
|
||||
auto scrobbler {getUserScrobbler(userId)};
|
||||
if (!scrobbler)
|
||||
@@ -82,8 +80,7 @@ namespace Scrobbling
|
||||
}
|
||||
|
||||
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
||||
bool
|
||||
ScrobblingService::isStarred(UserId userId, ObjIdType objId)
|
||||
bool ScrobblingService::isStarred(UserId userId, ObjIdType objId)
|
||||
{
|
||||
auto scrobbler {getUserScrobbler(userId)};
|
||||
if (!scrobbler)
|
||||
@@ -96,5 +93,22 @@ namespace Scrobbling
|
||||
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
|
||||
|
||||
|
||||
@@ -55,14 +55,12 @@ namespace Scrobbling
|
||||
: _db{ db }
|
||||
{}
|
||||
|
||||
void
|
||||
InternalScrobbler::listenStarted(const Listen&)
|
||||
void InternalScrobbler::listenStarted(const Listen&)
|
||||
{
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
void
|
||||
InternalScrobbler::listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration)
|
||||
void InternalScrobbler::listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration)
|
||||
{
|
||||
// only record tracks that have been played for at least of few seconds...
|
||||
if (duration && *duration < std::chrono::seconds{ 5 })
|
||||
@@ -71,8 +69,7 @@ namespace Scrobbling
|
||||
addTimedListen({ listen, Wt::WDateTime::currentDateTime() });
|
||||
}
|
||||
|
||||
void
|
||||
InternalScrobbler::addTimedListen(const TimedListen& listen)
|
||||
void InternalScrobbler::addTimedListen(const TimedListen& listen)
|
||||
{
|
||||
Database::Session& session{ _db.getTLSSession() };
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
@@ -92,20 +89,17 @@ namespace Scrobbling
|
||||
dbListen.modify()->setScrobblingState(Database::ScrobblingState::Synchronized);
|
||||
}
|
||||
|
||||
void
|
||||
InternalScrobbler::onStarred(Database::StarredArtistId starredArtistId)
|
||||
void InternalScrobbler::onStarred(Database::StarredArtistId starredArtistId)
|
||||
{
|
||||
::onStarred<Database::StarredArtist>(_db.getTLSSession(), starredArtistId);
|
||||
}
|
||||
|
||||
void
|
||||
InternalScrobbler::onUnstarred(Database::StarredArtistId starredArtistId)
|
||||
void InternalScrobbler::onUnstarred(Database::StarredArtistId starredArtistId)
|
||||
{
|
||||
::onUnstarred<Database::StarredArtist>(_db.getTLSSession(), starredArtistId);
|
||||
}
|
||||
|
||||
void
|
||||
InternalScrobbler::onStarred(Database::StarredReleaseId starredReleaseId)
|
||||
void InternalScrobbler::onStarred(Database::StarredReleaseId starredReleaseId)
|
||||
{
|
||||
::onStarred<Database::StarredRelease>(_db.getTLSSession(), starredReleaseId);
|
||||
}
|
||||
@@ -115,14 +109,12 @@ namespace Scrobbling
|
||||
::onUnstarred<Database::StarredRelease>(_db.getTLSSession(), starredReleaseId);
|
||||
}
|
||||
|
||||
void
|
||||
InternalScrobbler::onStarred(Database::StarredTrackId starredTrackId)
|
||||
void InternalScrobbler::onStarred(Database::StarredTrackId starredTrackId)
|
||||
{
|
||||
::onStarred<Database::StarredTrack>(_db.getTLSSession(), starredTrackId);
|
||||
}
|
||||
|
||||
void
|
||||
InternalScrobbler::onUnstarred(Database::StarredTrackId starredTrackId)
|
||||
void InternalScrobbler::onUnstarred(Database::StarredTrackId starredTrackId)
|
||||
{
|
||||
::onUnstarred<Database::StarredTrack>(_db.getTLSSession(), starredTrackId);
|
||||
}
|
||||
|
||||
@@ -19,11 +19,11 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <boost/asio/io_service.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <boost/asio/io_service.hpp>
|
||||
#include <Wt/WDateTime.h>
|
||||
|
||||
#include "services/scrobbling/Listen.hpp"
|
||||
#include "services/database/ArtistId.hpp"
|
||||
@@ -87,6 +87,7 @@ namespace Scrobbling
|
||||
virtual void star(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 Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::ArtistId artistId) = 0;
|
||||
virtual ArtistContainer getStarredArtists(Database::UserId userId,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::TrackArtistLinkType> linkType,
|
||||
@@ -96,11 +97,13 @@ namespace Scrobbling
|
||||
virtual void star(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 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 unstar(Database::UserId userId, Database::TrackId trackId) = 0;
|
||||
virtual bool isStarred(Database::UserId userId, Database::TrackId artistId) = 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;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,11 +1,32 @@
|
||||
|
||||
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/Scan.cpp
|
||||
impl/Stream.cpp
|
||||
impl/ParameterParsing.cpp
|
||||
impl/SubsonicId.cpp
|
||||
impl/SubsonicResource.cpp
|
||||
impl/SubsonicResponse.cpp
|
||||
impl/Utils.cpp
|
||||
)
|
||||
|
||||
target_include_directories(lmssubsonic INTERFACE
|
||||
@@ -13,6 +34,7 @@ target_include_directories(lmssubsonic INTERFACE
|
||||
)
|
||||
|
||||
target_include_directories(lmssubsonic PRIVATE
|
||||
impl
|
||||
include
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Wt/Http/Request.h>
|
||||
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
#include "services/database/Types.hpp"
|
||||
#include "utils/String.hpp"
|
||||
#include "SubsonicResponse.hpp"
|
||||
@@ -28,8 +33,7 @@ namespace API::Subsonic
|
||||
{
|
||||
|
||||
template<typename T>
|
||||
std::vector<T>
|
||||
getMultiParametersAs(const Wt::Http::ParameterMap& parameterMap, const std::string& paramName)
|
||||
std::vector<T> getMultiParametersAs(const Wt::Http::ParameterMap& parameterMap, const std::string& paramName)
|
||||
{
|
||||
std::vector<T> res;
|
||||
|
||||
@@ -48,8 +52,7 @@ namespace API::Subsonic
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
std::vector<T>
|
||||
getMandatoryMultiParametersAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
|
||||
std::vector<T> getMandatoryMultiParametersAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
|
||||
{
|
||||
std::vector<T> res{ getMultiParametersAs<T>(parameterMap, param) };
|
||||
if (res.empty())
|
||||
@@ -59,8 +62,7 @@ namespace API::Subsonic
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
std::optional<T>
|
||||
getParameterAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
|
||||
std::optional<T> getParameterAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
|
||||
{
|
||||
std::vector<T> params{ getMultiParametersAs<T>(parameterMap, param) };
|
||||
|
||||
@@ -71,8 +73,7 @@ namespace API::Subsonic
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
T
|
||||
getMandatoryParameterAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
|
||||
T getMandatoryParameterAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
|
||||
{
|
||||
auto res{ getParameterAs<T>(parameterMap, param) };
|
||||
if (!res)
|
||||
@@ -81,11 +82,7 @@ namespace API::Subsonic
|
||||
return *res;
|
||||
}
|
||||
|
||||
inline
|
||||
bool
|
||||
hasParameter(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
|
||||
{
|
||||
return parameterMap.find(param) != std::cend(parameterMap);
|
||||
}
|
||||
bool hasParameter(const Wt::Http::ParameterMap& parameterMap, const std::string& param);
|
||||
std::string decodePasswordIfNeeded(const std::string& password);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,8 +22,7 @@
|
||||
namespace StringUtils
|
||||
{
|
||||
template<>
|
||||
std::optional<API::Subsonic::ProtocolVersion>
|
||||
readAs(std::string_view str)
|
||||
std::optional<API::Subsonic::ProtocolVersion> readAs(std::string_view str)
|
||||
{
|
||||
// Expects "X.Y.Z"
|
||||
const auto numbers{ StringUtils::splitString(str, ".") };
|
||||
|
||||
@@ -31,10 +31,12 @@ namespace API::Subsonic
|
||||
};
|
||||
|
||||
static inline constexpr ProtocolVersion defaultServerProtocolVersion{ 1, 16, 0 };
|
||||
static inline constexpr std::string_view serverVersion{ "1" };
|
||||
}
|
||||
|
||||
namespace StringUtils
|
||||
{
|
||||
template<> std::optional<API::Subsonic::ProtocolVersion> readAs(std::string_view str);
|
||||
template<>
|
||||
std::optional<API::Subsonic::ProtocolVersion> readAs(std::string_view str);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,32 +26,27 @@
|
||||
|
||||
namespace API::Subsonic
|
||||
{
|
||||
std::string
|
||||
idToString(Database::ArtistId id)
|
||||
std::string idToString(Database::ArtistId id)
|
||||
{
|
||||
return "ar-" + id.toString();
|
||||
}
|
||||
|
||||
std::string
|
||||
idToString(Database::ReleaseId id)
|
||||
std::string idToString(Database::ReleaseId id)
|
||||
{
|
||||
return "al-" + id.toString();
|
||||
}
|
||||
|
||||
std::string
|
||||
idToString(RootId)
|
||||
std::string idToString(RootId)
|
||||
{
|
||||
return "root";
|
||||
}
|
||||
|
||||
std::string
|
||||
idToString(Database::TrackId id)
|
||||
std::string idToString(Database::TrackId id)
|
||||
{
|
||||
return "tr-" + id.toString();
|
||||
}
|
||||
|
||||
std::string
|
||||
idToString(Database::TrackListId id)
|
||||
std::string idToString(Database::TrackListId id)
|
||||
{
|
||||
return "pl-" + id.toString();
|
||||
}
|
||||
@@ -60,8 +55,7 @@ namespace API::Subsonic
|
||||
namespace StringUtils
|
||||
{
|
||||
template<>
|
||||
std::optional<Database::ArtistId>
|
||||
readAs(std::string_view str)
|
||||
std::optional<Database::ArtistId> readAs(std::string_view str)
|
||||
{
|
||||
std::vector<std::string_view> values{ StringUtils::splitString(str, "-") };
|
||||
if (values.size() != 2)
|
||||
@@ -77,8 +71,7 @@ namespace StringUtils
|
||||
}
|
||||
|
||||
template<>
|
||||
std::optional<Database::ReleaseId>
|
||||
readAs(std::string_view str)
|
||||
std::optional<Database::ReleaseId> readAs(std::string_view str)
|
||||
{
|
||||
std::vector<std::string_view> values{ StringUtils::splitString(str, "-") };
|
||||
if (values.size() != 2)
|
||||
@@ -94,8 +87,7 @@ namespace StringUtils
|
||||
}
|
||||
|
||||
template<>
|
||||
std::optional<API::Subsonic::RootId>
|
||||
readAs(std::string_view str)
|
||||
std::optional<API::Subsonic::RootId> readAs(std::string_view str)
|
||||
{
|
||||
if (str == "root")
|
||||
return API::Subsonic::RootId{};
|
||||
@@ -104,8 +96,7 @@ namespace StringUtils
|
||||
}
|
||||
|
||||
template<>
|
||||
std::optional<Database::TrackId>
|
||||
readAs(std::string_view str)
|
||||
std::optional<Database::TrackId> readAs(std::string_view str)
|
||||
{
|
||||
std::vector<std::string_view> values{ StringUtils::splitString(str, "-") };
|
||||
if (values.size() != 2)
|
||||
@@ -121,8 +112,7 @@ namespace StringUtils
|
||||
}
|
||||
|
||||
template<>
|
||||
std::optional<Database::TrackListId>
|
||||
readAs(std::string_view str)
|
||||
std::optional<Database::TrackListId> readAs(std::string_view str)
|
||||
{
|
||||
std::vector<std::string_view> values{ StringUtils::splitString(str, "-") };
|
||||
if (values.size() != 2)
|
||||
|
||||
@@ -40,23 +40,18 @@ namespace API::Subsonic
|
||||
namespace StringUtils
|
||||
{
|
||||
template<>
|
||||
std::optional<API::Subsonic::RootId>
|
||||
readAs(std::string_view str);
|
||||
std::optional<API::Subsonic::RootId> readAs(std::string_view str);
|
||||
|
||||
template<>
|
||||
std::optional<Database::ArtistId>
|
||||
readAs(std::string_view str);
|
||||
std::optional<Database::ArtistId> readAs(std::string_view str);
|
||||
|
||||
template<>
|
||||
std::optional<Database::ReleaseId>
|
||||
readAs(std::string_view str);
|
||||
std::optional<Database::ReleaseId> readAs(std::string_view str);
|
||||
|
||||
template<>
|
||||
std::optional<Database::TrackId>
|
||||
readAs(std::string_view str);
|
||||
std::optional<Database::TrackId> readAs(std::string_view str);
|
||||
|
||||
template<>
|
||||
std::optional<Database::TrackListId>
|
||||
readAs(std::string_view str);
|
||||
std::optional<Database::TrackListId> readAs(std::string_view str);
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -19,6 +19,7 @@
|
||||
|
||||
#include "SubsonicResponse.hpp"
|
||||
|
||||
#include <cassert>
|
||||
#include <Wt/Json/Array.h>
|
||||
#include <Wt/Json/Object.h>
|
||||
#include <Wt/Json/Value.h>
|
||||
@@ -33,9 +34,7 @@
|
||||
|
||||
namespace API::Subsonic
|
||||
{
|
||||
|
||||
std::string
|
||||
ResponseFormatToMimeType(ResponseFormat format)
|
||||
std::string_view ResponseFormatToMimeType(ResponseFormat format)
|
||||
{
|
||||
switch (format)
|
||||
{
|
||||
@@ -46,84 +45,97 @@ ResponseFormatToMimeType(ResponseFormat format)
|
||||
return "";
|
||||
}
|
||||
|
||||
void
|
||||
Response::Node::setValue(std::string_view value)
|
||||
void Response::Node::setValue(std::string_view value)
|
||||
{
|
||||
if (!_children.empty() || !_childrenArrays.empty())
|
||||
throw LmsException {"Node already has children"};
|
||||
|
||||
assert(_children.empty() && _childrenArrays.empty() && _childrenValues.empty());
|
||||
_value = std::string{ value };
|
||||
}
|
||||
|
||||
void
|
||||
Response::Node::setValue(long long value)
|
||||
void Response::Node::setValue(long long value)
|
||||
{
|
||||
if (!_children.empty() || !_childrenArrays.empty())
|
||||
throw LmsException {"Node already has children"};
|
||||
|
||||
assert(_children.empty() && _childrenArrays.empty() && _childrenValues.empty());
|
||||
_value = value;
|
||||
}
|
||||
|
||||
void
|
||||
Response::Node::setAttribute(std::string_view key, std::string_view value)
|
||||
void Response::Node::setAttribute(std::string_view key, std::string_view value)
|
||||
{
|
||||
_attributes[std::string{ key }] = std::string{ value };
|
||||
}
|
||||
|
||||
void
|
||||
Response::Node::addChild(const std::string& key, Node node)
|
||||
void Response::Node::addChild(const std::string& key, Node node)
|
||||
{
|
||||
if (_value)
|
||||
throw LmsException {"Node already has a value"};
|
||||
|
||||
assert(!_value);
|
||||
_children[key].emplace_back(std::move(node));
|
||||
}
|
||||
|
||||
void
|
||||
Response::Node::addArrayChild(const std::string& key, Node node)
|
||||
void Response::Node::createEmptyArrayChild(std::string_view key)
|
||||
{
|
||||
if (_value)
|
||||
throw LmsException {"Node already has a value"};
|
||||
|
||||
_childrenArrays[key].emplace_back(std::move(node));
|
||||
assert(!_value);
|
||||
_childrenArrays.emplace(key, std::vector<Node>{});
|
||||
}
|
||||
|
||||
void Response::Node::addArrayChild(std::string_view key, Node node)
|
||||
{
|
||||
assert(!_value);
|
||||
_childrenArrays[std::string{ key }].emplace_back(std::move(node));
|
||||
}
|
||||
|
||||
Response::Node&
|
||||
Response::Node::createChild(const std::string& key)
|
||||
void Response::Node::createEmptyArrayValue(std::string_view key)
|
||||
{
|
||||
assert (!_value);
|
||||
_childrenValues.emplace(key, ValuesType{});
|
||||
}
|
||||
|
||||
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 Response::Node::addArrayValue(std::string_view key, long long value)
|
||||
{
|
||||
assert(!_value);
|
||||
auto& values {_childrenValues[std::string{ key }]};
|
||||
values.push_back(value);
|
||||
assert(std::all_of(std::cbegin(values) + 1, std::cend(values), [&](const ValueType& value) {return value.index() == values.front().index();}));
|
||||
}
|
||||
|
||||
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)
|
||||
Response::Node& Response::Node::createArrayChild(const std::string& key)
|
||||
{
|
||||
_childrenArrays[key].emplace_back();
|
||||
return _childrenArrays[key].back();
|
||||
}
|
||||
|
||||
void
|
||||
Response::Node::setVersionAttribute(ProtocolVersion protocolVersion)
|
||||
void Response::Node::setVersionAttribute(ProtocolVersion protocolVersion)
|
||||
{
|
||||
setAttribute("version", std::to_string(protocolVersion.major) + "." + std::to_string(protocolVersion.minor) + "." + std::to_string(protocolVersion.patch));
|
||||
}
|
||||
|
||||
Response
|
||||
Response::createOkResponse(ProtocolVersion protocolVersion)
|
||||
Response Response::createOkResponse(ProtocolVersion protocolVersion)
|
||||
{
|
||||
Response response;
|
||||
Node& responseNode{ response._root.createChild("subsonic-response") };
|
||||
|
||||
responseNode.setAttribute("status", "ok");
|
||||
responseNode.setVersionAttribute(protocolVersion);
|
||||
responseNode.setAttribute("type", "lms"); // non standard field to ease client hacks
|
||||
|
||||
// OpenSubsonic mandatory fields
|
||||
responseNode.setAttribute("type", "lms");
|
||||
responseNode.setAttribute("serverVersion", serverVersion);
|
||||
responseNode.setAttribute("openSubsonic", true);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
Response
|
||||
Response::createFailedResponse(ProtocolVersion protocolVersion, const Error& error)
|
||||
Response Response::createFailedResponse(ProtocolVersion protocolVersion, const Error& error)
|
||||
{
|
||||
Response response;
|
||||
Node& responseNode{ response._root.createChild("subsonic-response") };
|
||||
@@ -139,26 +151,22 @@ Response::createFailedResponse(ProtocolVersion protocolVersion, const Error& err
|
||||
return response;
|
||||
}
|
||||
|
||||
void
|
||||
Response::addNode(const std::string& key, Node node)
|
||||
void Response::addNode(const std::string& key, Node node)
|
||||
{
|
||||
return _root._children["subsonic-response"].front().addChild(key, std::move(node));
|
||||
}
|
||||
|
||||
Response::Node&
|
||||
Response::createNode(const std::string& key)
|
||||
Response::Node& Response::createNode(const std::string& key)
|
||||
{
|
||||
return _root._children["subsonic-response"].front().createChild(key);
|
||||
}
|
||||
|
||||
Response::Node&
|
||||
Response::createArrayNode(const std::string& key)
|
||||
Response::Node& Response::createArrayNode(const std::string& key)
|
||||
{
|
||||
return _root._children["subsonic-response"].front().createArrayChild(key);
|
||||
}
|
||||
|
||||
void
|
||||
Response::write(std::ostream& os, ResponseFormat format)
|
||||
void Response::write(std::ostream& os, ResponseFormat format)
|
||||
{
|
||||
switch (format)
|
||||
{
|
||||
@@ -171,10 +179,9 @@ Response::write(std::ostream& os, ResponseFormat format)
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
Response::writeXML(std::ostream& os)
|
||||
void Response::writeXML(std::ostream& os)
|
||||
{
|
||||
std::function<boost::property_tree::ptree(const Response::Node&)> nodeToPropertyTree = [&] (const Response::Node& node)
|
||||
std::function<boost::property_tree::ptree(const Node&)> nodeToPropertyTree = [&](const Node& node)
|
||||
{
|
||||
boost::property_tree::ptree res;
|
||||
|
||||
@@ -184,35 +191,45 @@ Response::writeXML(std::ostream& os)
|
||||
res.put("<xmlattr>." + itAttribute.first, std::get<std::string>(itAttribute.second));
|
||||
else if (std::holds_alternative<bool>(itAttribute.second))
|
||||
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));
|
||||
}
|
||||
|
||||
auto valueToPropertyTree = [](const Node::ValueType& value)
|
||||
{
|
||||
boost::property_tree::ptree res;
|
||||
std::visit([&](const auto& rawValue)
|
||||
{
|
||||
res.put_value(rawValue);
|
||||
}, value);
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
if (node._value)
|
||||
{
|
||||
const auto& value {*node._value};
|
||||
|
||||
if (std::holds_alternative<std::string>(value))
|
||||
res.put_value(std::get<std::string>(value));
|
||||
else if (std::holds_alternative<bool>(value))
|
||||
res.put_value(std::get<bool>(value));
|
||||
else if (std::holds_alternative<long long>(value))
|
||||
res.put_value(std::get<long long>(value));
|
||||
res = valueToPropertyTree(*node._value);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (auto itChildNode : node._children)
|
||||
for (const auto& [key, childNodes] : node._children)
|
||||
{
|
||||
for (const Response::Node& childNode : itChildNode.second)
|
||||
res.add_child(itChildNode.first, nodeToPropertyTree(childNode));
|
||||
for (const Node& childNode : childNodes)
|
||||
res.add_child(key, 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 )
|
||||
res.add_child(itChildArrayNode.first, nodeToPropertyTree(childNode));
|
||||
for (const auto& [key, childArrayValues] : node._childrenValues)
|
||||
{
|
||||
for (const Response::Node::ValueType& value : childArrayValues)
|
||||
res.add_child(key, valueToPropertyTree(value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,8 +240,7 @@ Response::writeXML(std::ostream& os)
|
||||
boost::property_tree::write_xml(os, root);
|
||||
}
|
||||
|
||||
void
|
||||
Response::writeJSON(std::ostream& os)
|
||||
void Response::writeJSON(std::ostream& os)
|
||||
{
|
||||
namespace Json = Wt::Json;
|
||||
|
||||
@@ -234,14 +250,12 @@ Response::writeJSON(std::ostream& os)
|
||||
|
||||
auto valueToJsonValue{ [](const Node::ValueType& value) -> Json::Value
|
||||
{
|
||||
if (std::holds_alternative<std::string>(value))
|
||||
return Json::Value {std::get<std::string>(value)};
|
||||
else if (std::holds_alternative<bool>(value))
|
||||
return Json::Value {std::get<bool>(value)};
|
||||
else if (std::holds_alternative<long long>(value))
|
||||
return Json::Value {std::get<long long>(value)};
|
||||
|
||||
throw LmsException("Unexpected value type");
|
||||
Json::Value res;
|
||||
std::visit([&](const auto& rawValue)
|
||||
{
|
||||
res = Json::Value{ rawValue };
|
||||
}, value);
|
||||
return res;
|
||||
} };
|
||||
|
||||
for (auto itAttribute : node._attributes)
|
||||
@@ -253,21 +267,28 @@ Response::writeJSON(std::ostream& os)
|
||||
}
|
||||
else
|
||||
{
|
||||
for (auto itChildNode : node._children)
|
||||
for (const auto& [key, childNodes] : node._children)
|
||||
{
|
||||
for (const Response::Node& childNode : itChildNode.second)
|
||||
res[itChildNode.first] = nodeToJsonObject(childNode);
|
||||
for (const Response::Node& childNode : childNodes)
|
||||
res[key] = nodeToJsonObject(childNode);
|
||||
}
|
||||
|
||||
for (auto itChildArrayNode : node._childrenArrays)
|
||||
for (const auto& [key, childArrayNodes] : node._childrenArrays)
|
||||
{
|
||||
const std::vector<Response::Node>& childArrayNodes {itChildArrayNode .second};
|
||||
|
||||
Json::Array array;
|
||||
for (const Response::Node& childNode : childArrayNodes)
|
||||
array.emplace_back(nodeToJsonObject(childNode));
|
||||
|
||||
res[itChildArrayNode.first] = std::move(array);
|
||||
res[key] = std::move(array);
|
||||
}
|
||||
|
||||
for (const auto& [key, childValues] : node._childrenValues)
|
||||
{
|
||||
Json::Array array;
|
||||
for (const Node::ValueType& childValue : childValues)
|
||||
array.emplace_back(valueToJsonValue(childValue));
|
||||
|
||||
res[key] = std::move(array);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ enum class ResponseFormat
|
||||
json,
|
||||
};
|
||||
|
||||
std::string ResponseFormatToMimeType(ResponseFormat format);
|
||||
std::string_view ResponseFormatToMimeType(ResponseFormat format);
|
||||
|
||||
class Error
|
||||
{
|
||||
@@ -198,28 +198,39 @@ class Response
|
||||
{
|
||||
if constexpr (std::is_same<bool, T>::value)
|
||||
_attributes[std::string{ key }] = value;
|
||||
else
|
||||
else if constexpr (std::is_floating_point<T>::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(long long value);
|
||||
Node& createChild(const std::string& key);
|
||||
Node& createArrayChild(const std::string& key);
|
||||
|
||||
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:
|
||||
void setVersionAttribute(ProtocolVersion version);
|
||||
|
||||
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::optional<ValueType> _value;
|
||||
std::map<std::string, std::vector<Node>> _children;
|
||||
std::map<std::string, std::vector<Node>> _childrenArrays;
|
||||
|
||||
using ValuesType = std::vector<ValueType>;
|
||||
std::map<std::string, ValuesType> _childrenValues;
|
||||
};
|
||||
|
||||
static Response createOkResponse(ProtocolVersion protocolVersion);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
+2
-1
@@ -24,9 +24,10 @@
|
||||
|
||||
#include "RequestContext.hpp"
|
||||
|
||||
namespace API::Subsonic::Stream
|
||||
namespace API::Subsonic
|
||||
{
|
||||
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 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);
|
||||
}
|
||||
@@ -27,10 +27,13 @@
|
||||
#include <boost/algorithm/string/join.hpp>
|
||||
#include <boost/algorithm/string.hpp>
|
||||
|
||||
namespace StringUtils {
|
||||
#include <Wt/WDateTime.h>
|
||||
#include <Wt/WDate.h>
|
||||
|
||||
bool
|
||||
readList(const std::string& str, const std::string& separators, std::list<std::string>& results)
|
||||
namespace StringUtils
|
||||
{
|
||||
|
||||
bool readList(const std::string& str, const std::string& separators, std::list<std::string>& results)
|
||||
{
|
||||
std::string curStr;
|
||||
|
||||
@@ -57,22 +60,19 @@ readList(const std::string& str, const std::string& separators, std::list<std::s
|
||||
}
|
||||
|
||||
template<>
|
||||
std::optional<std::string>
|
||||
readAs(std::string_view str)
|
||||
std::optional<std::string> readAs(std::string_view str)
|
||||
{
|
||||
return std::string{ str };
|
||||
}
|
||||
|
||||
template<>
|
||||
std::optional<std::string_view>
|
||||
readAs(std::string_view str)
|
||||
std::optional<std::string_view> readAs(std::string_view str)
|
||||
{
|
||||
return str;
|
||||
}
|
||||
|
||||
template<>
|
||||
std::optional<bool>
|
||||
readAs(std::string_view str)
|
||||
std::optional<bool> readAs(std::string_view str)
|
||||
{
|
||||
if (str == "1" || str == "true")
|
||||
return true;
|
||||
@@ -82,8 +82,7 @@ readAs(std::string_view str)
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::vector<std::string>
|
||||
splitStringCopy(std::string_view string, std::string_view separators)
|
||||
std::vector<std::string> splitStringCopy(std::string_view string, std::string_view separators)
|
||||
{
|
||||
std::string str{ stringTrim(string, separators) };
|
||||
|
||||
@@ -93,8 +92,7 @@ splitStringCopy(std::string_view string, std::string_view separators)
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<std::string_view>
|
||||
splitString(std::string_view str, std::string_view separators)
|
||||
std::vector<std::string_view> splitString(std::string_view str, std::string_view separators)
|
||||
{
|
||||
std::vector<std::string_view> res;
|
||||
|
||||
@@ -116,14 +114,12 @@ splitString(std::string_view str, std::string_view separators)
|
||||
return res;
|
||||
}
|
||||
|
||||
std::string
|
||||
joinStrings(const std::vector<std::string>& strings, const std::string& delimiter)
|
||||
std::string joinStrings(const std::vector<std::string>& strings, const std::string& delimiter)
|
||||
{
|
||||
return boost::algorithm::join(strings, delimiter);
|
||||
}
|
||||
|
||||
std::string_view
|
||||
stringTrim(std::string_view str, std::string_view whitespaces)
|
||||
std::string_view stringTrim(std::string_view str, std::string_view whitespaces)
|
||||
{
|
||||
std::string_view res;
|
||||
|
||||
@@ -139,14 +135,12 @@ stringTrim(std::string_view str, std::string_view whitespaces)
|
||||
return res;
|
||||
}
|
||||
|
||||
std::string_view
|
||||
stringTrimEnd(std::string_view str, std::string_view whitespaces)
|
||||
std::string_view stringTrimEnd(std::string_view str, std::string_view whitespaces)
|
||||
{
|
||||
return str.substr(0, str.find_last_not_of(whitespaces) + 1);
|
||||
}
|
||||
|
||||
std::string
|
||||
stringToLower(std::string_view str)
|
||||
std::string stringToLower(std::string_view str)
|
||||
{
|
||||
std::string res;
|
||||
res.reserve(str.size());
|
||||
@@ -156,14 +150,12 @@ stringToLower(std::string_view str)
|
||||
return res;
|
||||
}
|
||||
|
||||
void
|
||||
stringToLower(std::string& str)
|
||||
void stringToLower(std::string& str)
|
||||
{
|
||||
std::transform(std::cbegin(str), std::cend(str), std::begin(str), [](unsigned char c) { return std::tolower(c);});
|
||||
}
|
||||
|
||||
std::string
|
||||
stringToUpper(const std::string& str)
|
||||
std::string stringToUpper(const std::string& str)
|
||||
{
|
||||
std::string res;
|
||||
res.reserve(str.size());
|
||||
@@ -173,8 +165,7 @@ stringToUpper(const std::string& str)
|
||||
return res;
|
||||
}
|
||||
|
||||
std::string
|
||||
bufferToString(const std::vector<unsigned char>& data)
|
||||
std::string bufferToString(const std::vector<unsigned char>& data)
|
||||
{
|
||||
std::ostringstream oss;
|
||||
|
||||
@@ -186,8 +177,7 @@ bufferToString(const std::vector<unsigned char>& data)
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
void
|
||||
capitalize(std::string& str)
|
||||
void capitalize(std::string& str)
|
||||
{
|
||||
for (auto it{ std::begin(str) }; it != std::end(str); ++it)
|
||||
{
|
||||
@@ -201,8 +191,7 @@ capitalize(std::string& str)
|
||||
}
|
||||
}
|
||||
|
||||
std::string
|
||||
replaceInString(std::string_view str, const std::string& from, const std::string& to)
|
||||
std::string replaceInString(std::string_view str, const std::string& from, const std::string& to)
|
||||
{
|
||||
std::string res{ str };
|
||||
size_t pos = 0;
|
||||
@@ -216,8 +205,7 @@ replaceInString(std::string_view str, const std::string& from, const std::string
|
||||
return res;
|
||||
}
|
||||
|
||||
std::string
|
||||
jsEscape(const std::string& str)
|
||||
std::string jsEscape(const std::string& str)
|
||||
{
|
||||
static const std::unordered_map<char, std::string_view> escapeMap
|
||||
{
|
||||
@@ -247,8 +235,7 @@ jsEscape(const std::string& str)
|
||||
return escaped;
|
||||
}
|
||||
|
||||
std::string
|
||||
escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar)
|
||||
std::string escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar)
|
||||
{
|
||||
std::string res;
|
||||
res.reserve(str.size());
|
||||
@@ -264,14 +251,12 @@ escapeString(std::string_view str, std::string_view charsToEscape, char escapeCh
|
||||
return res;
|
||||
}
|
||||
|
||||
bool
|
||||
stringEndsWith(const std::string& str, const std::string& ending)
|
||||
bool stringEndsWith(const std::string& str, const std::string& ending)
|
||||
{
|
||||
return boost::algorithm::ends_with(str, ending);
|
||||
}
|
||||
|
||||
std::optional<std::string>
|
||||
stringFromHex(const std::string& str)
|
||||
std::optional<std::string> stringFromHex(const std::string& str)
|
||||
{
|
||||
static const char lut[]{ "0123456789ABCDEF" };
|
||||
|
||||
@@ -301,5 +286,16 @@ stringFromHex(const std::string& str)
|
||||
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
|
||||
|
||||
|
||||
@@ -29,49 +29,36 @@
|
||||
#define QUOTEME(x) QUOTEME_1(x)
|
||||
#define QUOTEME_1(x) #x
|
||||
|
||||
namespace Wt
|
||||
{
|
||||
class WDate;
|
||||
class WDateTime;
|
||||
}
|
||||
|
||||
namespace StringUtils {
|
||||
|
||||
[[nodiscard]]
|
||||
std::vector<std::string>
|
||||
splitStringCopy(std::string_view string, std::string_view separators);
|
||||
[[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::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 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 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_view stringTrimEnd(std::string_view str, std::string_view whitespaces = " \t");
|
||||
|
||||
[[nodiscard]]
|
||||
std::string
|
||||
stringToLower(std::string_view str);
|
||||
[[nodiscard]] std::string stringToLower(std::string_view str);
|
||||
|
||||
void
|
||||
stringToLower(std::string& str);
|
||||
void stringToLower(std::string& str);
|
||||
|
||||
[[nodiscard]]
|
||||
std::string
|
||||
stringToUpper(const std::string& str);
|
||||
[[nodiscard]] std::string stringToUpper(const std::string& str);
|
||||
|
||||
[[nodiscard]]
|
||||
std::string
|
||||
bufferToString(const std::vector<unsigned char>& data);
|
||||
[[nodiscard]] std::string bufferToString(const std::vector<unsigned char>& data);
|
||||
|
||||
void
|
||||
capitalize(std::string& str);
|
||||
void capitalize(std::string& str);
|
||||
|
||||
template<typename T>
|
||||
[[nodiscard]]
|
||||
std::optional<T> readAs(std::string_view str)
|
||||
[[nodiscard]] std::optional<T> readAs(std::string_view str)
|
||||
{
|
||||
T res;
|
||||
|
||||
@@ -84,39 +71,25 @@ std::optional<T> readAs(std::string_view str)
|
||||
}
|
||||
|
||||
template<>
|
||||
[[nodiscard]]
|
||||
std::optional<std::string>
|
||||
readAs(std::string_view str);
|
||||
[[nodiscard]] std::optional<std::string> readAs(std::string_view str);
|
||||
|
||||
template<>
|
||||
[[nodiscard]]
|
||||
std::optional<std::string_view>
|
||||
readAs(std::string_view str);
|
||||
[[nodiscard]] std::optional<std::string_view> readAs(std::string_view str);
|
||||
|
||||
template<>
|
||||
[[nodiscard]]
|
||||
std::optional<bool>
|
||||
readAs(std::string_view str);
|
||||
[[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 replaceInString(std::string_view str, const std::string& from, const std::string& to);
|
||||
|
||||
[[nodiscard]]
|
||||
std::string
|
||||
jsEscape(const std::string& str);
|
||||
[[nodiscard]] std::string jsEscape(const std::string& str);
|
||||
|
||||
[[nodiscard]]
|
||||
std::string
|
||||
escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar);
|
||||
[[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]] bool stringEndsWith(const std::string& str, const std::string& ending);
|
||||
|
||||
[[nodiscard]]
|
||||
std::optional<std::string>
|
||||
stringFromHex(const std::string& str);
|
||||
[[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 <Wt/WDateTime.h>
|
||||
#include <Wt/WDate.h>
|
||||
#include <Wt/WTime.h>
|
||||
#include "utils/String.hpp"
|
||||
|
||||
TEST(StringUtils, splitString)
|
||||
@@ -147,3 +150,15 @@ TEST(StringUtils, capitalize)
|
||||
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;
|
||||
params.setClusters(getFilters().getClusterIds());
|
||||
params.setKeywords(getSearchKeywords());
|
||||
params.setLinkType(_linkType);
|
||||
params.setSortMethod(ArtistSortMethod::BySortName);
|
||||
params.setRange(range);
|
||||
|
||||
|
||||
@@ -21,6 +21,10 @@
|
||||
#include <Wt/WAnchor.h>
|
||||
|
||||
#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"
|
||||
|
||||
namespace UserInterface::ArtistListHelpers
|
||||
@@ -33,5 +37,43 @@ namespace UserInterface::ArtistListHelpers
|
||||
|
||||
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 "common/ValueStringModel.hpp"
|
||||
#include "services/database/Object.hpp"
|
||||
#include "services/database/Types.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Artist;
|
||||
}
|
||||
|
||||
namespace UserInterface::ArtistListHelpers
|
||||
namespace UserInterface
|
||||
{
|
||||
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/Session.hpp"
|
||||
#include "services/database/TrackArtistLink.hpp"
|
||||
#include "utils/EnumSet.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
#include "common/ValueStringModel.hpp"
|
||||
#include "common/InfiniteScrollingContainer.hpp"
|
||||
#include "ArtistListHelpers.hpp"
|
||||
#include "Filters.hpp"
|
||||
@@ -37,8 +35,6 @@ using namespace Database;
|
||||
|
||||
namespace UserInterface {
|
||||
|
||||
using ArtistLinkModel = ValueStringModel<std::optional<TrackArtistLinkType>>;
|
||||
|
||||
Artists::Artists(Filters& filters)
|
||||
: Wt::WTemplate {Wt::WString::tr("Lms.Explore.Artists.template")}
|
||||
, _artistCollector {filters, _defaultMode, _maxCount}
|
||||
@@ -71,18 +67,17 @@ Artists::Artists(Filters& filters)
|
||||
bindMenuItem("all", Wt::WString::tr("Lms.Explore.all"), ArtistCollector::Mode::All);
|
||||
|
||||
_linkType = bindNew<Wt::WComboBox>("link-type");
|
||||
_linkType->setModel(std::make_shared<ArtistLinkModel>());
|
||||
_linkType->setModel(ArtistListHelpers::createArtistLinkTypesModel());
|
||||
_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);
|
||||
});
|
||||
refreshArtistLinkTypes();
|
||||
|
||||
LmsApp->getScannerEvents().scanComplete.connect(this, [this](const Scanner::ScanStats& stats)
|
||||
{
|
||||
if (stats.nbChanges())
|
||||
refreshArtistLinkTypes();
|
||||
_linkType->setModel(ArtistListHelpers::createArtistLinkTypesModel());
|
||||
});
|
||||
|
||||
_container = bindNew<InfiniteScrollingContainer>("artists", Wt::WString::tr("Lms.Explore.Artists.template.container"));
|
||||
@@ -120,42 +115,6 @@ Artists::refreshView(std::optional<TrackArtistLinkType> linkType)
|
||||
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
|
||||
Artists::addSome()
|
||||
{
|
||||
|
||||
@@ -42,7 +42,6 @@ namespace UserInterface
|
||||
void refreshView();
|
||||
void refreshView(ArtistCollector::Mode mode);
|
||||
void refreshView(std::optional<Database::TrackArtistLinkType> linkType);
|
||||
void refreshArtistLinkTypes();
|
||||
void addSome();
|
||||
|
||||
static constexpr std::size_t _batchSize {30};
|
||||
|
||||
@@ -66,7 +66,7 @@ namespace UserInterface::ReleaseListHelpers
|
||||
|
||||
if (showYear)
|
||||
{
|
||||
Wt::WString year {ReleaseHelpers::buildReleaseYearString(release->getReleaseYear(), release->getReleaseYear(true))};
|
||||
Wt::WString year {ReleaseHelpers::buildReleaseYearString(release->getReleaseDate(), release->getOriginalReleaseDate())};
|
||||
if (!year.empty())
|
||||
{
|
||||
entry->setCondition("if-has-year", true);
|
||||
@@ -135,18 +135,18 @@ namespace UserInterface::ReleaseHelpers
|
||||
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;
|
||||
|
||||
// Year can be here, but originalYear can't be here without year (enforced by scanner)
|
||||
if (!year)
|
||||
if (!releaseDate.isValid())
|
||||
return res;
|
||||
|
||||
if (originalYear && *originalYear != *year)
|
||||
res = std::to_string(*originalYear) + " (" + std::to_string(*year) + ")";
|
||||
if (originalReleaseDate.isValid() && originalReleaseDate != releaseDate)
|
||||
res = std::to_string(originalReleaseDate.year()) + " (" + std::to_string(releaseDate.year()) + ")";
|
||||
else
|
||||
res = std::to_string(*year);
|
||||
res = std::to_string(releaseDate.year());
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
#include <Wt/WString.h>
|
||||
#include <Wt/WTemplate.h>
|
||||
#include <Wt/WDate.h>
|
||||
#include "services/database/Object.hpp"
|
||||
#include "services/database/Types.hpp"
|
||||
#include "utils/EnumSet.hpp"
|
||||
@@ -43,5 +44,5 @@ namespace UserInterface::ReleaseListHelpers
|
||||
namespace UserInterface::ReleaseHelpers
|
||||
{
|
||||
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);
|
||||
|
||||
Wt::WString year {ReleaseHelpers::buildReleaseYearString(release->getReleaseYear(), release->getReleaseYear(true))};
|
||||
Wt::WString year {ReleaseHelpers::buildReleaseYearString(release->getReleaseDate(), release->getOriginalReleaseDate())};
|
||||
if (!year.empty())
|
||||
{
|
||||
setCondition("if-has-year", true);
|
||||
|
||||
@@ -48,6 +48,14 @@ namespace UserInterface
|
||||
_artists = bindNew<InfiniteScrollingContainer>("artists", Wt::WString::tr("Lms.Explore.Artists.template.container"));
|
||||
_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->onRequestElements.connect([this] { addSomeReleases(); });
|
||||
|
||||
@@ -58,6 +66,12 @@ namespace UserInterface
|
||||
{
|
||||
refreshView();
|
||||
});
|
||||
|
||||
LmsApp->getScannerEvents().scanComplete.connect(this, [this](const Scanner::ScanStats& stats)
|
||||
{
|
||||
if (stats.nbChanges())
|
||||
_artistLinkType->setModel(ArtistListHelpers::createArtistLinkTypesModel());
|
||||
});
|
||||
}
|
||||
|
||||
std::size_t
|
||||
@@ -76,6 +90,13 @@ namespace UserInterface
|
||||
return it->second;
|
||||
}
|
||||
|
||||
void
|
||||
SearchView::refreshView(std::optional<TrackArtistLinkType> linkType)
|
||||
{
|
||||
_artistCollector.setArtistLinkType(linkType);
|
||||
refreshView();
|
||||
}
|
||||
|
||||
void
|
||||
SearchView::refreshView(const Wt::WString& searchText)
|
||||
{
|
||||
|
||||
@@ -19,11 +19,14 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <Wt/WComboBox.h>
|
||||
#include <Wt/WStackedWidget.h>
|
||||
#include <Wt/WTemplate.h>
|
||||
|
||||
#include "services/database/Types.hpp"
|
||||
#include "ArtistCollector.hpp"
|
||||
#include "ReleaseCollector.hpp"
|
||||
#include "TrackCollector.hpp"
|
||||
@@ -67,6 +70,7 @@ namespace UserInterface
|
||||
std::size_t getMaxCount(Mode mode) const;
|
||||
|
||||
void refreshView();
|
||||
void refreshView(std::optional<Database::TrackArtistLinkType> linkType);
|
||||
void addSomeArtists();
|
||||
void addSomeReleases();
|
||||
void addSomeTracks();
|
||||
@@ -77,9 +81,11 @@ namespace UserInterface
|
||||
ReleaseCollector _releaseCollector;
|
||||
TrackCollector _trackCollector;
|
||||
|
||||
InfiniteScrollingContainer* _artists;
|
||||
InfiniteScrollingContainer* _releases;
|
||||
InfiniteScrollingContainer* _tracks;
|
||||
InfiniteScrollingContainer* _artists {};
|
||||
InfiniteScrollingContainer* _releases {};
|
||||
InfiniteScrollingContainer* _tracks {};
|
||||
|
||||
Wt::WComboBox* _artistLinkType {};
|
||||
|
||||
std::vector<InfiniteScrollingContainer*> _results;
|
||||
};
|
||||
|
||||
@@ -109,8 +109,8 @@ getReleasePathName(Database::Release::pointer release)
|
||||
{
|
||||
std::string releaseName;
|
||||
|
||||
if (auto releaseYear {release->getReleaseYear()})
|
||||
releaseName += std::to_string(*releaseYear) + " - ";
|
||||
if (const Wt::WDate releaseDate {release->getReleaseDate()}; releaseDate.isValid())
|
||||
releaseName += std::to_string(releaseDate.year()) + " - ";
|
||||
releaseName += StringUtils::replaceInString(release->getName(), "/", "_");
|
||||
|
||||
return releaseName;
|
||||
|
||||
Reference in New Issue
Block a user