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,177 +28,183 @@
|
||||
#include "SqlQuery.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
Cluster::Cluster(ObjectPtr<ClusterType> type, std::string_view name)
|
||||
: _name {std::string {name, 0, _maxNameLength}},
|
||||
_clusterType {getDboPtr(type)}
|
||||
namespace Database
|
||||
{
|
||||
}
|
||||
namespace
|
||||
{
|
||||
Wt::Dbo::Query<ClusterId> createQuery(Session& session, const Cluster::FindParameters& params)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
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}});
|
||||
}
|
||||
auto query{ session.getDboSession().query<ClusterId>("SELECT DISTINCT c.id FROM cluster c") };
|
||||
|
||||
std::size_t
|
||||
Cluster::getCount(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
if (params.track.isValid() || params.release.isValid())
|
||||
{
|
||||
query.join("track_cluster t_c ON t_c.cluster_id = c.id");
|
||||
query.join("track t ON t.id = t_c.track_id");
|
||||
}
|
||||
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM cluster");
|
||||
}
|
||||
if (params.track.isValid())
|
||||
query.where("t.id = ?").bind(params.track);
|
||||
if (params.release.isValid())
|
||||
query.where("t.release_id = ?").bind(params.release);
|
||||
|
||||
RangeResults<ClusterId>
|
||||
Cluster::find(Session& session, Range range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
auto query {session.getDboSession().query<ClusterId>("SELECT id FROM cluster")};
|
||||
if (params.clusterType.isValid())
|
||||
query.where("c.cluster_type_id = ?").bind(params.clusterType);
|
||||
|
||||
return Utils::execQuery(query, range);
|
||||
}
|
||||
return query;
|
||||
}
|
||||
}
|
||||
|
||||
RangeResults<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)")};
|
||||
Cluster::Cluster(ObjectPtr<ClusterType> type, std::string_view name)
|
||||
: _name{ std::string {name, 0, _maxNameLength} },
|
||||
_clusterType{ getDboPtr(type) }
|
||||
{
|
||||
}
|
||||
|
||||
return Utils::execQuery(query, range);
|
||||
}
|
||||
Cluster::pointer Cluster::create(Session& session, ObjectPtr<ClusterType> type, std::string_view name)
|
||||
{
|
||||
return session.getDboSession().add(std::unique_ptr<Cluster> {new Cluster{ type, name }});
|
||||
}
|
||||
|
||||
Cluster::pointer
|
||||
Cluster::find(Session& session, ClusterId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
std::size_t Cluster::getCount(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().find<Cluster>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM cluster");
|
||||
}
|
||||
|
||||
void
|
||||
Cluster::addTrack(ObjectPtr<Track> track)
|
||||
{
|
||||
_tracks.insert(getDboPtr(track));
|
||||
}
|
||||
RangeResults<ClusterId> Cluster::find(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
auto query{ createQuery(session, params) };
|
||||
|
||||
RangeResults<TrackId>
|
||||
Cluster::getTracks(Range range) const
|
||||
{
|
||||
assert(session());
|
||||
return Utils::execQuery(query, params.range);
|
||||
}
|
||||
|
||||
auto query {session()->query<TrackId>("SELECT t.id FROM track t INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id")
|
||||
.where("c.id = ?").bind(getId())};
|
||||
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)") };
|
||||
|
||||
return Utils::execQuery(query, range);
|
||||
}
|
||||
return Utils::execQuery(query, range);
|
||||
}
|
||||
|
||||
std::size_t
|
||||
Cluster::getReleasesCount() const
|
||||
{
|
||||
assert(session());
|
||||
Cluster::pointer Cluster::find(Session& session, ClusterId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session()->query<int>("SELECT COUNT(DISTINCT r.id) FROM release r INNER JOIN track t on t.release_id = r.id INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id")
|
||||
.where("c.id = ?").bind(getId());
|
||||
}
|
||||
return session.getDboSession().find<Cluster>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
|
||||
void Cluster::addTrack(ObjectPtr<Track> track)
|
||||
{
|
||||
_tracks.insert(getDboPtr(track));
|
||||
}
|
||||
|
||||
RangeResults<TrackId> Cluster::getTracks(Range range) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
auto query{ session()->query<TrackId>("SELECT t.id FROM track t INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id")
|
||||
.where("c.id = ?").bind(getId()) };
|
||||
|
||||
return Utils::execQuery(query, range);
|
||||
}
|
||||
|
||||
std::size_t Cluster::getReleasesCount() const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
return session()->query<int>("SELECT COUNT(DISTINCT r.id) FROM release r INNER JOIN track t on t.release_id = r.id INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id")
|
||||
.where("c.id = ?").bind(getId());
|
||||
}
|
||||
|
||||
|
||||
ClusterType::ClusterType(std::string_view name)
|
||||
: _name {name}
|
||||
{
|
||||
}
|
||||
ClusterType::ClusterType(std::string_view name)
|
||||
: _name{ name }
|
||||
{
|
||||
}
|
||||
|
||||
ClusterType::pointer
|
||||
ClusterType::create(Session& session, const std::string& name)
|
||||
{
|
||||
return session.getDboSession().add(std::unique_ptr<ClusterType> {new ClusterType {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)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
std::size_t ClusterType::getCount(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM cluster_type");
|
||||
}
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM cluster_type");
|
||||
}
|
||||
|
||||
|
||||
RangeResults<ClusterTypeId>
|
||||
ClusterType::findOrphans(Session& session, Range range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
RangeResults<ClusterTypeId> ClusterType::findOrphans(Session& session, Range range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {session.getDboSession().query<ClusterTypeId>(
|
||||
"SELECT c_t.id from cluster_type c_t"
|
||||
" LEFT OUTER JOIN cluster c ON c_t.id = c.cluster_type_id")
|
||||
.where("c.id IS NULL")};
|
||||
auto query{ session.getDboSession().query<ClusterTypeId>(
|
||||
"SELECT c_t.id from cluster_type c_t"
|
||||
" LEFT OUTER JOIN cluster c ON c_t.id = c.cluster_type_id")
|
||||
.where("c.id IS NULL") };
|
||||
|
||||
return Utils::execQuery(query, range);
|
||||
}
|
||||
return Utils::execQuery(query, range);
|
||||
}
|
||||
|
||||
RangeResults<ClusterTypeId>
|
||||
ClusterType::findUsed(Session& session, Range range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
RangeResults<ClusterTypeId> ClusterType::findUsed(Session& session, Range range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {session.getDboSession().query<ClusterTypeId>(
|
||||
"SELECT DISTINCT c_t.id from cluster_type c_t")
|
||||
.join("cluster c ON c_t.id = c.cluster_type_id")};
|
||||
auto query{ session.getDboSession().query<ClusterTypeId>(
|
||||
"SELECT DISTINCT c_t.id from cluster_type c_t")
|
||||
.join("cluster c ON c_t.id = c.cluster_type_id") };
|
||||
|
||||
return Utils::execQuery(query, range);
|
||||
}
|
||||
return Utils::execQuery(query, range);
|
||||
}
|
||||
|
||||
ClusterType::pointer
|
||||
ClusterType::find(Session& session, const std::string& name)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
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)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
ClusterType::pointer ClusterType::find(Session& session, ClusterTypeId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().find<ClusterType>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
return session.getDboSession().find<ClusterType>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
|
||||
RangeResults<ClusterTypeId>
|
||||
ClusterType::find(Session& session, Range range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
RangeResults<ClusterTypeId> ClusterType::find(Session& session, Range range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {session.getDboSession().query<ClusterTypeId>("SELECT id from cluster_type")};
|
||||
auto query{ session.getDboSession().query<ClusterTypeId>("SELECT id from cluster_type") };
|
||||
|
||||
return Utils::execQuery(query, range);
|
||||
}
|
||||
return Utils::execQuery(query, range);
|
||||
}
|
||||
|
||||
Cluster::pointer
|
||||
ClusterType::getCluster(const std::string& name) const
|
||||
{
|
||||
assert(self());
|
||||
assert(session());
|
||||
Cluster::pointer ClusterType::getCluster(const std::string& name) const
|
||||
{
|
||||
assert(self());
|
||||
assert(session());
|
||||
|
||||
return session()->find<Cluster>()
|
||||
.where("name = ?").bind(name)
|
||||
.where("cluster_type_id = ?").bind(getId()).resultValue();
|
||||
}
|
||||
return session()->find<Cluster>()
|
||||
.where("name = ?").bind(name)
|
||||
.where("cluster_type_id = ?").bind(getId()).resultValue();
|
||||
}
|
||||
|
||||
std::vector<Cluster::pointer>
|
||||
ClusterType::getClusters() const
|
||||
{
|
||||
assert(self());
|
||||
assert(session());
|
||||
|
||||
auto res = session()->find<Cluster>()
|
||||
.where("cluster_type_id = ?").bind(getId())
|
||||
.orderBy("name")
|
||||
.resultList();
|
||||
|
||||
return std::vector<Cluster::pointer>(res.begin(), res.end());
|
||||
}
|
||||
std::vector<Cluster::pointer> ClusterType::getClusters() const
|
||||
{
|
||||
assert(self());
|
||||
assert(session());
|
||||
|
||||
auto res = session()->find<Cluster>()
|
||||
.where("cluster_type_id = ?").bind(getId())
|
||||
.orderBy("name")
|
||||
.resultList();
|
||||
|
||||
return std::vector<Cluster::pointer>(res.begin(), res.end());
|
||||
}
|
||||
} // namespace Database
|
||||
|
||||
|
||||
@@ -35,441 +35,443 @@
|
||||
namespace Database
|
||||
{
|
||||
|
||||
Wt::Dbo::Query<ReleaseId>
|
||||
createQuery(Session& session, const Release::FindParameters& params)
|
||||
{
|
||||
auto query {session.getDboSession().query<ReleaseId>("SELECT DISTINCT r.id from release r")};
|
||||
|
||||
if (params.sortMethod == ReleaseSortMethod::LastWritten
|
||||
|| params.sortMethod == ReleaseSortMethod::Date
|
||||
|| params.sortMethod == ReleaseSortMethod::OriginalDate
|
||||
|| params.sortMethod == ReleaseSortMethod::OriginalDateDesc
|
||||
|| params.writtenAfter.isValid()
|
||||
|| params.dateRange
|
||||
|| params.artist.isValid())
|
||||
{
|
||||
query.join("track t ON t.release_id = r.id");
|
||||
}
|
||||
|
||||
if (params.writtenAfter.isValid())
|
||||
query.where("t.file_last_write > ?").bind(params.writtenAfter);
|
||||
|
||||
if (params.dateRange)
|
||||
{
|
||||
query.where("t.date >= ?").bind(params.dateRange->begin);
|
||||
query.where("t.date <= ?").bind(params.dateRange->end);
|
||||
}
|
||||
|
||||
for (std::string_view keyword : params.keywords)
|
||||
query.where("r.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + Utils::escapeLikeKeyword(keyword) + "%");
|
||||
|
||||
if (params.starringUser.isValid())
|
||||
{
|
||||
assert(params.scrobbler);
|
||||
query.join("starred_release s_r ON s_r.release_id = r.id")
|
||||
.where("s_r.user_id = ?").bind(params.starringUser)
|
||||
.where("s_r.scrobbler = ?").bind(*params.scrobbler)
|
||||
.where("s_r.scrobbling_state <> ?").bind(ScrobblingState::PendingRemove);
|
||||
}
|
||||
|
||||
if (params.artist.isValid())
|
||||
{
|
||||
query.join("artist a ON a.id = t_a_l.artist_id")
|
||||
.join("track_artist_link t_a_l ON t_a_l.track_id = t.id")
|
||||
.where("a.id = ?").bind(params.artist);
|
||||
|
||||
if (!params.trackArtistLinkTypes.empty())
|
||||
{
|
||||
std::ostringstream oss;
|
||||
|
||||
bool first {true};
|
||||
for (TrackArtistLinkType linkType : params.trackArtistLinkTypes)
|
||||
{
|
||||
if (!first)
|
||||
oss << " OR ";
|
||||
oss << "t_a_l.type = ?";
|
||||
query.bind(linkType);
|
||||
|
||||
first = false;
|
||||
}
|
||||
query.where(oss.str());
|
||||
}
|
||||
|
||||
if (!params.excludedTrackArtistLinkTypes.empty())
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "r.id NOT IN (SELECT DISTINCT r.id FROM release r"
|
||||
" INNER JOIN artist a ON a.id = t_a_l.artist_id"
|
||||
" INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id"
|
||||
" INNER JOIN track t ON t.release_id = r.id"
|
||||
" WHERE (a.id = ? AND (";
|
||||
|
||||
query.bind(params.artist);
|
||||
|
||||
bool first {true};
|
||||
for (const TrackArtistLinkType linkType : params.excludedTrackArtistLinkTypes)
|
||||
{
|
||||
if (!first)
|
||||
oss << " OR ";
|
||||
oss << "t_a_l.type = ?";
|
||||
query.bind(linkType);
|
||||
|
||||
first = false;
|
||||
}
|
||||
oss << ")))";
|
||||
query.where(oss.str());
|
||||
}
|
||||
}
|
||||
|
||||
if (!params.clusters.empty())
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "r.id IN (SELECT DISTINCT r.id FROM release r"
|
||||
" INNER JOIN track t ON t.release_id = r.id"
|
||||
" INNER JOIN cluster c ON c.id = t_c.cluster_id"
|
||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
|
||||
|
||||
WhereClause clusterClause;
|
||||
for (const ClusterId clusterId : params.clusters)
|
||||
{
|
||||
clusterClause.Or(WhereClause("c.id = ?"));
|
||||
query.bind(clusterId);
|
||||
}
|
||||
|
||||
oss << " " << clusterClause.get();
|
||||
oss << " GROUP BY t.id HAVING COUNT(*) = " << params.clusters.size() << ")";
|
||||
|
||||
query.where(oss.str());
|
||||
}
|
||||
|
||||
if (params.primaryType)
|
||||
query.where("primary_type = ?").bind(*params.primaryType);
|
||||
if (!params.secondaryTypes.empty())
|
||||
query.where("secondary_type = ?").bind(params.secondaryTypes);
|
||||
|
||||
switch (params.sortMethod)
|
||||
{
|
||||
case ReleaseSortMethod::None:
|
||||
break;
|
||||
case ReleaseSortMethod::Name:
|
||||
query.orderBy("r.name COLLATE NOCASE");
|
||||
break;
|
||||
case ReleaseSortMethod::Random:
|
||||
query.orderBy("RANDOM()");
|
||||
break;
|
||||
case ReleaseSortMethod::LastWritten:
|
||||
query.orderBy("t.file_last_write DESC");
|
||||
break;
|
||||
case ReleaseSortMethod::Date:
|
||||
query.orderBy("t.date, r.name COLLATE NOCASE");
|
||||
break;
|
||||
case ReleaseSortMethod::OriginalDate:
|
||||
query.orderBy("CASE WHEN t.original_date IS NULL THEN t.date ELSE t.original_date END, t.date, r.name COLLATE NOCASE");
|
||||
break;
|
||||
case ReleaseSortMethod::OriginalDateDesc:
|
||||
query.orderBy("CASE WHEN t.original_date IS NULL THEN t.date ELSE t.original_date END DESC, t.date, r.name COLLATE NOCASE");
|
||||
break;
|
||||
case ReleaseSortMethod::StarredDateDesc:
|
||||
assert(params.starringUser.isValid());
|
||||
query.orderBy("s_r.date_time DESC");
|
||||
break;
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
Release::Release(const std::string& name, const std::optional<UUID>& MBID)
|
||||
: _name {std::string(name, 0 , _maxNameLength)},
|
||||
_MBID {MBID ? MBID->getAsString() : ""}
|
||||
{
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
|
||||
auto res {session.getDboSession()
|
||||
.find<Release>()
|
||||
.where("name = ?").bind( std::string(name, 0, _maxNameLength) )
|
||||
.resultList()};
|
||||
|
||||
return std::vector<Release::pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
Release::pointer
|
||||
Release::find(Session& session, const UUID& mbid)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession()
|
||||
.find<Release>()
|
||||
.where("mbid = ?").bind(std::string {mbid.getAsString()})
|
||||
.resultValue();;
|
||||
}
|
||||
|
||||
Release::pointer
|
||||
Release::find(Session& session, ReleaseId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession()
|
||||
.find<Release>()
|
||||
.where("id = ?").bind(id)
|
||||
.resultValue();
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM release");
|
||||
}
|
||||
|
||||
RangeResults<ReleaseId>
|
||||
Release::findOrderedByArtist(Session& session, Range range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
// TODO merge with find
|
||||
auto query {session.getDboSession().query<ReleaseId>(
|
||||
"SELECT DISTINCT r.id FROM release r"
|
||||
" INNER JOIN track t ON r.id = t.release_id"
|
||||
" INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id"
|
||||
" INNER JOIN artist a ON t_a_l.artist_id = a.id")
|
||||
.orderBy("a.name COLLATE NOCASE, r.name COLLATE NOCASE")};
|
||||
|
||||
return Utils::execQuery(query, range);
|
||||
}
|
||||
|
||||
RangeResults<ReleaseId>
|
||||
Release::findOrphans(Session& session, Range range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {session.getDboSession().query<ReleaseId>("select r.id from release r LEFT OUTER JOIN Track t ON r.id = t.release_id WHERE t.id IS NULL")};
|
||||
return Utils::execQuery(query, range);
|
||||
}
|
||||
|
||||
RangeResults<ReleaseId>
|
||||
Release::find(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {createQuery(session, params)};
|
||||
|
||||
return Utils::execQuery(query, params.range);
|
||||
}
|
||||
|
||||
std::size_t
|
||||
Release::getDiscCount() const
|
||||
{
|
||||
assert(session());
|
||||
int res {session()->query<int>("SELECT COUNT(DISTINCT disc_number) FROM track t")
|
||||
.join("release r ON r.id = t.release_id")
|
||||
.where("r.id = ?")
|
||||
.bind(getId())};
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::optional<int>
|
||||
Release::getReleaseYear(bool original) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
const char* field {original ? "original_date" : "date"};
|
||||
|
||||
auto dates {session()->query<Wt::WDate>(
|
||||
std::string {"SELECT "} + "t." + field + " FROM track t INNER JOIN release r ON r.id = t.release_id")
|
||||
.where("r.id = ?")
|
||||
.groupBy(field)
|
||||
.bind(getId())
|
||||
.resultList()};
|
||||
|
||||
// various dates => no date
|
||||
if (dates.empty() || dates.size() > 1)
|
||||
return std::nullopt;
|
||||
|
||||
auto date {dates.front().year()};
|
||||
|
||||
if (date > 0)
|
||||
return date;
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<std::string>
|
||||
Release::getCopyright() const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
Wt::Dbo::collection<std::string> copyrights = session()->query<std::string>
|
||||
("SELECT copyright FROM track t INNER JOIN release r ON r.id = t.release_id")
|
||||
.where("r.id = ?")
|
||||
.groupBy("copyright")
|
||||
.bind(getId());
|
||||
|
||||
std::vector<std::string> values(copyrights.begin(), copyrights.end());
|
||||
|
||||
// various copyrights => no copyright
|
||||
if (values.empty() || values.size() > 1 || values.front().empty())
|
||||
return std::nullopt;
|
||||
|
||||
return values.front();
|
||||
}
|
||||
|
||||
std::optional<std::string>
|
||||
Release::getCopyrightURL() const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
Wt::Dbo::collection<std::string> copyrights = session()->query<std::string>
|
||||
("SELECT copyright_url FROM track t INNER JOIN release r ON r.id = t.release_id")
|
||||
.where("r.id = ?")
|
||||
.groupBy("copyright_url")
|
||||
.bind(getId());
|
||||
|
||||
std::vector<std::string> values(copyrights.begin(), copyrights.end());
|
||||
|
||||
// various copyright URLs => no copyright URL
|
||||
if (values.empty() || values.size() > 1 || values.front().empty())
|
||||
return std::nullopt;
|
||||
|
||||
return values.front();
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer>
|
||||
Release::getArtists(TrackArtistLinkType linkType) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
auto res {session()->query<Wt::Dbo::ptr<Artist>>(
|
||||
"SELECT DISTINCT a FROM artist a"
|
||||
" INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id"
|
||||
" INNER JOIN track t ON t.id = t_a_l.track_id"
|
||||
" INNER JOIN release r ON r.id = t.release_id")
|
||||
.where("r.id = ?").bind(getId())
|
||||
.where("t_a_l.type = ?").bind(linkType)
|
||||
.resultList()};
|
||||
|
||||
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
|
||||
{
|
||||
assert(session());
|
||||
|
||||
auto res {session()->query<Wt::Dbo::ptr<Release>>(
|
||||
"SELECT r FROM release r"
|
||||
" INNER JOIN track t ON t.release_id = r.id"
|
||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
|
||||
" WHERE "
|
||||
" t_c.cluster_id IN (SELECT c.id from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN release r ON r.id = t.release_id WHERE r.id = ?)"
|
||||
" AND r.id <> ?"
|
||||
)
|
||||
.bind(getId())
|
||||
.bind(getId())
|
||||
.groupBy("r.id")
|
||||
.orderBy("COUNT(*) DESC, RANDOM()")
|
||||
.limit(count ? static_cast<int>(*count) : -1)
|
||||
.offset(offset ? static_cast<int>(*offset) : -1)
|
||||
.resultList()};
|
||||
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
bool
|
||||
Release::hasVariousArtists() const
|
||||
{
|
||||
// TODO optimize
|
||||
return getArtists().size() > 1;
|
||||
}
|
||||
|
||||
std::size_t
|
||||
Release::getTracksCount() const
|
||||
{
|
||||
return _tracks.size();
|
||||
}
|
||||
|
||||
std::chrono::milliseconds
|
||||
Release::getDuration() const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
using milli = std::chrono::duration<int, std::milli>;
|
||||
|
||||
Wt::Dbo::Query<milli> query {session()->query<milli>("SELECT COALESCE(SUM(duration), 0) FROM track t INNER JOIN release r ON t.release_id = r.id")
|
||||
.where("r.id = ?").bind(getId())};
|
||||
|
||||
return query.resultValue();
|
||||
}
|
||||
|
||||
Wt::WDateTime
|
||||
Release::getLastWritten() const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
Wt::Dbo::Query<Wt::WDateTime> query {session()->query<Wt::WDateTime>("SELECT COALESCE(MAX(file_last_write), '1970-01-01T00:00:00') FROM track t INNER JOIN release r ON t.release_id = r.id")
|
||||
.where("r.id = ?").bind(getId())};
|
||||
|
||||
return query.resultValue();
|
||||
}
|
||||
|
||||
std::vector<std::vector<Cluster::pointer>>
|
||||
Release::getClusterGroups(const std::vector<ClusterType::pointer>& clusterTypes, std::size_t size) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
WhereClause where;
|
||||
|
||||
std::ostringstream oss;
|
||||
|
||||
oss << "SELECT c from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN cluster_type c_type ON c.cluster_type_id = c_type.id INNER JOIN release r ON t.release_id = r.id ";
|
||||
|
||||
where.And(WhereClause("r.id = ?")).bind(getId().toString());
|
||||
{
|
||||
WhereClause clusterClause;
|
||||
for (auto clusterType : clusterTypes)
|
||||
clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterType->getId().toString());
|
||||
where.And(clusterClause);
|
||||
}
|
||||
oss << " " << where.get();
|
||||
oss << " GROUP BY c.id ORDER BY COUNT(c.id) DESC";
|
||||
|
||||
auto query {session()->query<Wt::Dbo::ptr<Cluster>>(oss.str())};
|
||||
for (const std::string& bindArg : where.getBindArgs())
|
||||
query.bind(bindArg);
|
||||
|
||||
auto queryRes {query.resultList()};
|
||||
|
||||
std::map<ClusterTypeId, std::vector<Cluster::pointer>> clustersByType;
|
||||
for (const Wt::Dbo::ptr<Cluster>& cluster : queryRes)
|
||||
{
|
||||
if (clustersByType[cluster->getType()->getId()].size() < size)
|
||||
clustersByType[cluster->getType()->getId()].push_back(cluster);
|
||||
}
|
||||
|
||||
std::vector<std::vector<Cluster::pointer>> res;
|
||||
for (const auto& [clusterTypeId, clusters] : clustersByType)
|
||||
res.push_back(clusters);
|
||||
|
||||
return res;
|
||||
}
|
||||
Wt::Dbo::Query<ReleaseId> createQuery(Session& session, const Release::FindParameters& params)
|
||||
{
|
||||
auto query{ session.getDboSession().query<ReleaseId>("SELECT DISTINCT r.id from release r") };
|
||||
|
||||
if (params.sortMethod == ReleaseSortMethod::LastWritten
|
||||
|| params.sortMethod == ReleaseSortMethod::Date
|
||||
|| params.sortMethod == ReleaseSortMethod::OriginalDate
|
||||
|| params.sortMethod == ReleaseSortMethod::OriginalDateDesc
|
||||
|| params.writtenAfter.isValid()
|
||||
|| params.dateRange
|
||||
|| params.artist.isValid())
|
||||
{
|
||||
query.join("track t ON t.release_id = r.id");
|
||||
}
|
||||
|
||||
if (params.writtenAfter.isValid())
|
||||
query.where("t.file_last_write > ?").bind(params.writtenAfter);
|
||||
|
||||
if (params.dateRange)
|
||||
{
|
||||
query.where("t.date >= ?").bind(params.dateRange->begin);
|
||||
query.where("t.date <= ?").bind(params.dateRange->end);
|
||||
}
|
||||
|
||||
for (std::string_view keyword : params.keywords)
|
||||
query.where("r.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + Utils::escapeLikeKeyword(keyword) + "%");
|
||||
|
||||
if (params.starringUser.isValid())
|
||||
{
|
||||
assert(params.scrobbler);
|
||||
query.join("starred_release s_r ON s_r.release_id = r.id")
|
||||
.where("s_r.user_id = ?").bind(params.starringUser)
|
||||
.where("s_r.scrobbler = ?").bind(*params.scrobbler)
|
||||
.where("s_r.scrobbling_state <> ?").bind(ScrobblingState::PendingRemove);
|
||||
}
|
||||
|
||||
if (params.artist.isValid())
|
||||
{
|
||||
query.join("artist a ON a.id = t_a_l.artist_id")
|
||||
.join("track_artist_link t_a_l ON t_a_l.track_id = t.id")
|
||||
.where("a.id = ?").bind(params.artist);
|
||||
|
||||
if (!params.trackArtistLinkTypes.empty())
|
||||
{
|
||||
std::ostringstream oss;
|
||||
|
||||
bool first{ true };
|
||||
for (TrackArtistLinkType linkType : params.trackArtistLinkTypes)
|
||||
{
|
||||
if (!first)
|
||||
oss << " OR ";
|
||||
oss << "t_a_l.type = ?";
|
||||
query.bind(linkType);
|
||||
|
||||
first = false;
|
||||
}
|
||||
query.where(oss.str());
|
||||
}
|
||||
|
||||
if (!params.excludedTrackArtistLinkTypes.empty())
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "r.id NOT IN (SELECT DISTINCT r.id FROM release r"
|
||||
" INNER JOIN artist a ON a.id = t_a_l.artist_id"
|
||||
" INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id"
|
||||
" INNER JOIN track t ON t.release_id = r.id"
|
||||
" WHERE (a.id = ? AND (";
|
||||
|
||||
query.bind(params.artist);
|
||||
|
||||
bool first{ true };
|
||||
for (const TrackArtistLinkType linkType : params.excludedTrackArtistLinkTypes)
|
||||
{
|
||||
if (!first)
|
||||
oss << " OR ";
|
||||
oss << "t_a_l.type = ?";
|
||||
query.bind(linkType);
|
||||
|
||||
first = false;
|
||||
}
|
||||
oss << ")))";
|
||||
query.where(oss.str());
|
||||
}
|
||||
}
|
||||
|
||||
if (!params.clusters.empty())
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "r.id IN (SELECT DISTINCT r.id FROM release r"
|
||||
" INNER JOIN track t ON t.release_id = r.id"
|
||||
" INNER JOIN cluster c ON c.id = t_c.cluster_id"
|
||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
|
||||
|
||||
WhereClause clusterClause;
|
||||
for (const ClusterId clusterId : params.clusters)
|
||||
{
|
||||
clusterClause.Or(WhereClause("c.id = ?"));
|
||||
query.bind(clusterId);
|
||||
}
|
||||
|
||||
oss << " " << clusterClause.get();
|
||||
oss << " GROUP BY t.id HAVING COUNT(*) = " << params.clusters.size() << ")";
|
||||
|
||||
query.where(oss.str());
|
||||
}
|
||||
|
||||
if (params.primaryType)
|
||||
query.where("primary_type = ?").bind(*params.primaryType);
|
||||
if (!params.secondaryTypes.empty())
|
||||
query.where("secondary_type = ?").bind(params.secondaryTypes);
|
||||
|
||||
switch (params.sortMethod)
|
||||
{
|
||||
case ReleaseSortMethod::None:
|
||||
break;
|
||||
case ReleaseSortMethod::Name:
|
||||
query.orderBy("r.name COLLATE NOCASE");
|
||||
break;
|
||||
case ReleaseSortMethod::Random:
|
||||
query.orderBy("RANDOM()");
|
||||
break;
|
||||
case ReleaseSortMethod::LastWritten:
|
||||
query.orderBy("t.file_last_write DESC");
|
||||
break;
|
||||
case ReleaseSortMethod::Date:
|
||||
query.orderBy("t.date, r.name COLLATE NOCASE");
|
||||
break;
|
||||
case ReleaseSortMethod::OriginalDate:
|
||||
query.orderBy("CASE WHEN t.original_date IS NULL THEN t.date ELSE t.original_date END, t.date, r.name COLLATE NOCASE");
|
||||
break;
|
||||
case ReleaseSortMethod::OriginalDateDesc:
|
||||
query.orderBy("CASE WHEN t.original_date IS NULL THEN t.date ELSE t.original_date END DESC, t.date, r.name COLLATE NOCASE");
|
||||
break;
|
||||
case ReleaseSortMethod::StarredDateDesc:
|
||||
assert(params.starringUser.isValid());
|
||||
query.orderBy("s_r.date_time DESC");
|
||||
break;
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
Release::Release(const std::string& name, const std::optional<UUID>& MBID)
|
||||
: _name{ std::string(name, 0 , _maxNameLength) },
|
||||
_MBID{ MBID ? MBID->getAsString() : "" }
|
||||
{
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
|
||||
auto res{ session.getDboSession()
|
||||
.find<Release>()
|
||||
.where("name = ?").bind(std::string(name, 0, _maxNameLength))
|
||||
.resultList() };
|
||||
|
||||
return std::vector<Release::pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
Release::pointer Release::find(Session& session, const UUID& mbid)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession()
|
||||
.find<Release>()
|
||||
.where("mbid = ?").bind(std::string{ mbid.getAsString() })
|
||||
.resultValue();;
|
||||
}
|
||||
|
||||
Release::pointer Release::find(Session& session, ReleaseId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession()
|
||||
.find<Release>()
|
||||
.where("id = ?").bind(id)
|
||||
.resultValue();
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM release");
|
||||
}
|
||||
|
||||
RangeResults<ReleaseId> Release::findOrderedByArtist(Session& session, Range range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
// TODO merge with find
|
||||
auto query{ session.getDboSession().query<ReleaseId>(
|
||||
"SELECT DISTINCT r.id FROM release r"
|
||||
" INNER JOIN track t ON r.id = t.release_id"
|
||||
" INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id"
|
||||
" INNER JOIN artist a ON t_a_l.artist_id = a.id")
|
||||
.orderBy("a.name COLLATE NOCASE, r.name COLLATE NOCASE") };
|
||||
|
||||
return Utils::execQuery(query, range);
|
||||
}
|
||||
|
||||
RangeResults<ReleaseId> Release::findOrphans(Session& session, Range range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query{ session.getDboSession().query<ReleaseId>("select r.id from release r LEFT OUTER JOIN Track t ON r.id = t.release_id WHERE t.id IS NULL") };
|
||||
return Utils::execQuery(query, range);
|
||||
}
|
||||
|
||||
RangeResults<ReleaseId> Release::find(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query{ createQuery(session, params) };
|
||||
|
||||
return Utils::execQuery(query, params.range);
|
||||
}
|
||||
|
||||
std::size_t Release::getDiscCount() const
|
||||
{
|
||||
assert(session());
|
||||
int res{ session()->query<int>("SELECT COUNT(DISTINCT disc_number) FROM track t")
|
||||
.join("release r ON r.id = t.release_id")
|
||||
.where("r.id = ?")
|
||||
.bind(getId()) };
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
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());
|
||||
|
||||
const char* field{ original ? "original_date" : "date" };
|
||||
|
||||
auto dates{ session()->query<Wt::WDate>(
|
||||
std::string {"SELECT "} + "t." + field + " FROM track t INNER JOIN release r ON r.id = t.release_id")
|
||||
.where("r.id = ?")
|
||||
.groupBy(field)
|
||||
.bind(getId())
|
||||
.resultList() };
|
||||
|
||||
// various dates => invalid date
|
||||
if (dates.empty() || dates.size() > 1)
|
||||
return {};
|
||||
|
||||
return dates.front();
|
||||
}
|
||||
|
||||
std::optional<std::string> Release::getCopyright() const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
Wt::Dbo::collection<std::string> copyrights = session()->query<std::string>
|
||||
("SELECT copyright FROM track t INNER JOIN release r ON r.id = t.release_id")
|
||||
.where("r.id = ?")
|
||||
.groupBy("copyright")
|
||||
.bind(getId());
|
||||
|
||||
std::vector<std::string> values(copyrights.begin(), copyrights.end());
|
||||
|
||||
// various copyrights => no copyright
|
||||
if (values.empty() || values.size() > 1 || values.front().empty())
|
||||
return std::nullopt;
|
||||
|
||||
return values.front();
|
||||
}
|
||||
|
||||
std::optional<std::string> Release::getCopyrightURL() const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
Wt::Dbo::collection<std::string> copyrights = session()->query<std::string>
|
||||
("SELECT copyright_url FROM track t INNER JOIN release r ON r.id = t.release_id")
|
||||
.where("r.id = ?")
|
||||
.groupBy("copyright_url")
|
||||
.bind(getId());
|
||||
|
||||
std::vector<std::string> values(copyrights.begin(), copyrights.end());
|
||||
|
||||
// various copyright URLs => no copyright URL
|
||||
if (values.empty() || values.size() > 1 || values.front().empty())
|
||||
return std::nullopt;
|
||||
|
||||
return values.front();
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer> Release::getArtists(TrackArtistLinkType linkType) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
auto res{ session()->query<Wt::Dbo::ptr<Artist>>(
|
||||
"SELECT DISTINCT a FROM artist a"
|
||||
" INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id"
|
||||
" INNER JOIN track t ON t.id = t_a_l.track_id"
|
||||
" INNER JOIN release r ON r.id = t.release_id")
|
||||
.where("r.id = ?").bind(getId())
|
||||
.where("t_a_l.type = ?").bind(linkType)
|
||||
.resultList() };
|
||||
|
||||
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
|
||||
{
|
||||
assert(session());
|
||||
|
||||
auto res{ session()->query<Wt::Dbo::ptr<Release>>(
|
||||
"SELECT r FROM release r"
|
||||
" INNER JOIN track t ON t.release_id = r.id"
|
||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
|
||||
" WHERE "
|
||||
" t_c.cluster_id IN (SELECT c.id from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN release r ON r.id = t.release_id WHERE r.id = ?)"
|
||||
" AND r.id <> ?"
|
||||
)
|
||||
.bind(getId())
|
||||
.bind(getId())
|
||||
.groupBy("r.id")
|
||||
.orderBy("COUNT(*) DESC, RANDOM()")
|
||||
.limit(count ? static_cast<int>(*count) : -1)
|
||||
.offset(offset ? static_cast<int>(*offset) : -1)
|
||||
.resultList() };
|
||||
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
bool Release::hasVariousArtists() const
|
||||
{
|
||||
// TODO optimize
|
||||
return getArtists().size() > 1;
|
||||
}
|
||||
|
||||
std::size_t Release::getTracksCount() const
|
||||
{
|
||||
return _tracks.size();
|
||||
}
|
||||
|
||||
std::chrono::milliseconds Release::getDuration() const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
using milli = std::chrono::duration<int, std::milli>;
|
||||
|
||||
Wt::Dbo::Query<milli> query{ session()->query<milli>("SELECT COALESCE(SUM(duration), 0) FROM track t INNER JOIN release r ON t.release_id = r.id")
|
||||
.where("r.id = ?").bind(getId()) };
|
||||
|
||||
return query.resultValue();
|
||||
}
|
||||
|
||||
Wt::WDateTime Release::getLastWritten() const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
Wt::Dbo::Query<Wt::WDateTime> query{ session()->query<Wt::WDateTime>("SELECT COALESCE(MAX(file_last_write), '1970-01-01T00:00:00') FROM track t INNER JOIN release r ON t.release_id = r.id")
|
||||
.where("r.id = ?").bind(getId()) };
|
||||
|
||||
return query.resultValue();
|
||||
}
|
||||
|
||||
std::vector<std::vector<Cluster::pointer>> Release::getClusterGroups(const std::vector<ClusterType::pointer>& clusterTypes, std::size_t size) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
WhereClause where;
|
||||
|
||||
std::ostringstream oss;
|
||||
|
||||
oss << "SELECT c from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN cluster_type c_type ON c.cluster_type_id = c_type.id INNER JOIN release r ON t.release_id = r.id ";
|
||||
|
||||
where.And(WhereClause("r.id = ?")).bind(getId().toString());
|
||||
{
|
||||
WhereClause clusterClause;
|
||||
for (auto clusterType : clusterTypes)
|
||||
clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterType->getId().toString());
|
||||
where.And(clusterClause);
|
||||
}
|
||||
oss << " " << where.get();
|
||||
oss << " GROUP BY c.id ORDER BY COUNT(c.id) DESC";
|
||||
|
||||
auto query{ session()->query<Wt::Dbo::ptr<Cluster>>(oss.str()) };
|
||||
for (const std::string& bindArg : where.getBindArgs())
|
||||
query.bind(bindArg);
|
||||
|
||||
auto queryRes{ query.resultList() };
|
||||
|
||||
std::map<ClusterTypeId, std::vector<Cluster::pointer>> clustersByType;
|
||||
for (const Wt::Dbo::ptr<Cluster>& cluster : queryRes)
|
||||
{
|
||||
if (clustersByType[cluster->getType()->getId()].size() < size)
|
||||
clustersByType[cluster->getType()->getId()].push_back(cluster);
|
||||
}
|
||||
|
||||
std::vector<std::vector<Cluster::pointer>> res;
|
||||
for (const auto& [clusterTypeId, clusters] : clustersByType)
|
||||
res.push_back(clusters);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
} // namespace Database
|
||||
|
||||
@@ -28,74 +28,84 @@
|
||||
|
||||
namespace Database
|
||||
{
|
||||
namespace
|
||||
{
|
||||
Wt::Dbo::Query<TrackArtistLinkId> createQuery(Session& session, const TrackArtistLink::FindParameters& params)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
static
|
||||
Wt::Dbo::Query<TrackArtistLinkId>
|
||||
createQuery(Session& session, const TrackArtistLink::FindParameters& params)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
auto query{ session.getDboSession().query<TrackArtistLinkId>("SELECT DISTINCT t_a_l.id FROM track_artist_link t_a_l") };
|
||||
|
||||
auto query {session.getDboSession().query<TrackArtistLinkId>("SELECT DISTINCT t_a_l.id FROM track_artist_link t_a_l")};
|
||||
if (params.linkType)
|
||||
query.where("t_a_l.type = ?").bind(*params.linkType);
|
||||
|
||||
if (params.linkType)
|
||||
query.where("t_a_l.type = ?").bind(*params.linkType);
|
||||
if (params.track.isValid() || params.release.isValid())
|
||||
query.join("track t ON t.id = t_a_l.track_id");
|
||||
|
||||
if (params.track.isValid() || params.release.isValid())
|
||||
query.join("track t ON t.id = t_a_l.track_id");
|
||||
if (params.artist.isValid())
|
||||
query.join("artist a ON a.id = t_a_l.artist_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.release.isValid())
|
||||
query.where("t.release_id = ?").bind(params.release);
|
||||
if (params.track.isValid())
|
||||
query.where("t.id = ?").bind(params.track);
|
||||
|
||||
return query;
|
||||
}
|
||||
return query;
|
||||
}
|
||||
}
|
||||
|
||||
TrackArtistLink::TrackArtistLink(ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType)
|
||||
: _type {type}
|
||||
, _subType {subType}
|
||||
, _track {getDboPtr(track)}
|
||||
, _artist {getDboPtr(artist)}
|
||||
{
|
||||
}
|
||||
TrackArtistLink::TrackArtistLink(ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType)
|
||||
: _type{ type }
|
||||
, _subType{ subType }
|
||||
, _track{ getDboPtr(track) }
|
||||
, _artist{ getDboPtr(artist) }
|
||||
{
|
||||
}
|
||||
|
||||
TrackArtistLink::pointer
|
||||
TrackArtistLink::create(Session& session, ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
TrackArtistLink::pointer TrackArtistLink::create(Session& session, ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
|
||||
TrackArtistLink::pointer res {session.getDboSession().add(std::make_unique<TrackArtistLink>(track, artist, type, subType))};
|
||||
session.getDboSession().flush();
|
||||
TrackArtistLink::pointer res{ session.getDboSession().add(std::make_unique<TrackArtistLink>(track, artist, type, subType)) };
|
||||
session.getDboSession().flush();
|
||||
|
||||
return res;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
TrackArtistLink::pointer
|
||||
TrackArtistLink::find(Session& session, TrackArtistLinkId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
return session.getDboSession().find<TrackArtistLink>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
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)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
RangeResults<TrackArtistLinkId> TrackArtistLink::find(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {createQuery(session, params)};
|
||||
return Utils::execQuery(query, params.range);
|
||||
}
|
||||
auto query{ createQuery(session, params) };
|
||||
return Utils::execQuery(query, params.range);
|
||||
}
|
||||
|
||||
EnumSet<TrackArtistLinkType> TrackArtistLink::findUsedTypes(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
EnumSet<TrackArtistLinkType>
|
||||
TrackArtistLink::findUsedTypes(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
auto res{ session.getDboSession().query<TrackArtistLinkType>("SELECT DISTINCT type from track_artist_link").resultList() };
|
||||
|
||||
auto res {session.getDboSession().query<TrackArtistLinkType>("SELECT DISTINCT type from track_artist_link").resultList()};
|
||||
return EnumSet<TrackArtistLinkType>(std::begin(res), std::end(res));
|
||||
}
|
||||
|
||||
return EnumSet<TrackArtistLinkType>(std::begin(res), std::end(res));
|
||||
}
|
||||
EnumSet<TrackArtistLinkType> TrackArtistLink::findUsedTypes(Session& session, ArtistId artistId)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto res{ session.getDboSession()
|
||||
.query<TrackArtistLinkType>("SELECT DISTINCT type from track_artist_link")
|
||||
.where("artist_id = ?").bind(artistId)
|
||||
.resultList() };
|
||||
|
||||
return EnumSet<TrackArtistLinkType>(std::begin(res), std::end(res));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,100 +26,114 @@
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
#include <Wt/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"
|
||||
|
||||
namespace Database {
|
||||
|
||||
class Track;
|
||||
class ClusterType;
|
||||
class ScanSettings;
|
||||
class Session;
|
||||
class Track;
|
||||
class ClusterType;
|
||||
class ScanSettings;
|
||||
class Session;
|
||||
|
||||
class Cluster final : public Object<Cluster, ClusterId>
|
||||
{
|
||||
public:
|
||||
Cluster() = default;
|
||||
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
|
||||
|
||||
// Find utility
|
||||
static std::size_t getCount(Session& session);
|
||||
static RangeResults<ClusterId> find(Session& session, Range range);
|
||||
static pointer find(Session& session, ClusterId id);
|
||||
static RangeResults<ClusterId> findOrphans(Session& session, Range range);
|
||||
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; }
|
||||
};
|
||||
|
||||
// Accessors
|
||||
const std::string& getName() const { return _name; }
|
||||
ObjectPtr<ClusterType> getType() const { return _clusterType; }
|
||||
std::size_t getTracksCount() const { return _tracks.size(); }
|
||||
RangeResults<TrackId> getTracks(Range range) const;
|
||||
std::size_t getReleasesCount() const;
|
||||
Cluster() = default;
|
||||
|
||||
void addTrack(ObjectPtr<Track> track);
|
||||
// Find utility
|
||||
static std::size_t getCount(Session& session);
|
||||
static RangeResults<ClusterId> find(Session& session, const FindParameters& range);
|
||||
static pointer find(Session& session, ClusterId id);
|
||||
static RangeResults<ClusterId> findOrphans(Session& session, Range range);
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _name, "name");
|
||||
// Accessors
|
||||
const std::string& getName() const { return _name; }
|
||||
ObjectPtr<ClusterType> getType() const { return _clusterType; }
|
||||
std::size_t getTracksCount() const { return _tracks.size(); }
|
||||
RangeResults<TrackId> getTracks(Range range) const;
|
||||
std::size_t getReleasesCount() const;
|
||||
|
||||
Wt::Dbo::belongsTo(a, _clusterType, "cluster_type", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
void addTrack(ObjectPtr<Track> track);
|
||||
|
||||
private:
|
||||
friend class Session;
|
||||
Cluster(ObjectPtr<ClusterType> type, std::string_view name);
|
||||
static pointer create(Session& session, ObjectPtr<ClusterType> type, std::string_view name);
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _name, "name");
|
||||
|
||||
static const std::size_t _maxNameLength = 128;
|
||||
Wt::Dbo::belongsTo(a, _clusterType, "cluster_type", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
|
||||
std::string _name;
|
||||
private:
|
||||
friend class Session;
|
||||
Cluster(ObjectPtr<ClusterType> type, std::string_view name);
|
||||
static pointer create(Session& session, ObjectPtr<ClusterType> type, std::string_view name);
|
||||
|
||||
Wt::Dbo::ptr<ClusterType> _clusterType;
|
||||
Wt::Dbo::collection< Wt::Dbo::ptr<Track> > _tracks;
|
||||
};
|
||||
static const std::size_t _maxNameLength = 128;
|
||||
|
||||
std::string _name;
|
||||
|
||||
Wt::Dbo::ptr<ClusterType> _clusterType;
|
||||
Wt::Dbo::collection< Wt::Dbo::ptr<Track> > _tracks;
|
||||
};
|
||||
|
||||
|
||||
class ClusterType final : public Object<ClusterType, ClusterTypeId>
|
||||
{
|
||||
public:
|
||||
ClusterType() = default;
|
||||
class ClusterType final : public Object<ClusterType, ClusterTypeId>
|
||||
{
|
||||
public:
|
||||
ClusterType() = default;
|
||||
|
||||
// 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, ClusterTypeId id);
|
||||
static RangeResults<ClusterTypeId> findOrphans(Session& session, Range range);
|
||||
static RangeResults<ClusterTypeId> findUsed(Session& session, Range range);
|
||||
// Getters
|
||||
static std::size_t getCount(Session& session);
|
||||
static RangeResults<ClusterTypeId> find(Session& session, Range range);
|
||||
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);
|
||||
|
||||
static void remove(Session& session, const std::string& name);
|
||||
static void remove(Session& session, const std::string& name);
|
||||
|
||||
// Accessors
|
||||
const std::string& getName() const { return _name; }
|
||||
std::vector<Cluster::pointer> getClusters() const;
|
||||
Cluster::pointer getCluster(const std::string& name) const;
|
||||
// Accessors
|
||||
const std::string& getName() const { return _name; }
|
||||
std::vector<Cluster::pointer> getClusters() const;
|
||||
Cluster::pointer getCluster(const std::string& name) const;
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _name, "name");
|
||||
Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToOne, "cluster_type");
|
||||
Wt::Dbo::belongsTo(a, _scanSettings, "scan_settings", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _name, "name");
|
||||
Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToOne, "cluster_type");
|
||||
Wt::Dbo::belongsTo(a, _scanSettings, "scan_settings", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
|
||||
private:
|
||||
friend class Session;
|
||||
ClusterType(std::string_view name);
|
||||
static pointer create(Session& session, const std::string& name);
|
||||
private:
|
||||
friend class Session;
|
||||
ClusterType(std::string_view name);
|
||||
static pointer create(Session& session, const std::string& name);
|
||||
|
||||
static const std::size_t _maxNameLength = 128;
|
||||
static const std::size_t _maxNameLength = 128;
|
||||
|
||||
std::string _name;
|
||||
Wt::Dbo::collection< Wt::Dbo::ptr<Cluster> > _clusters;
|
||||
Wt::Dbo::ptr<ScanSettings> _scanSettings;
|
||||
};
|
||||
std::string _name;
|
||||
Wt::Dbo::collection< Wt::Dbo::ptr<Cluster> > _clusters;
|
||||
Wt::Dbo::ptr<ScanSettings> _scanSettings;
|
||||
};
|
||||
|
||||
} // namespace Database
|
||||
|
||||
|
||||
@@ -37,124 +37,127 @@
|
||||
namespace Database
|
||||
{
|
||||
|
||||
class Artist;
|
||||
class Cluster;
|
||||
class ClusterType;
|
||||
class Release;
|
||||
class Session;
|
||||
class Track;
|
||||
class User;
|
||||
class Artist;
|
||||
class Cluster;
|
||||
class ClusterType;
|
||||
class Release;
|
||||
class Session;
|
||||
class Track;
|
||||
class User;
|
||||
|
||||
class Release final : public Object<Release, ReleaseId>
|
||||
{
|
||||
public:
|
||||
struct FindParameters
|
||||
{
|
||||
std::vector<ClusterId> clusters; // if non empty, releases that belong to these clusters
|
||||
std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords
|
||||
ReleaseSortMethod sortMethod {ReleaseSortMethod::None};
|
||||
Range range;
|
||||
Wt::WDateTime writtenAfter;
|
||||
std::optional<DateRange> dateRange;
|
||||
UserId starringUser; // only releases starred by this user
|
||||
std::optional<Scrobbler> scrobbler; // and for this scrobbler
|
||||
ArtistId artist; // only releases that involved this user
|
||||
EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types
|
||||
EnumSet<TrackArtistLinkType> excludedTrackArtistLinkTypes; // but not for these link types
|
||||
std::optional<ReleaseTypePrimary> primaryType; // if, set, matching this primary type
|
||||
EnumSet<ReleaseTypeSecondary> secondaryTypes; // Matching all this (if any)
|
||||
class Release final : public Object<Release, ReleaseId>
|
||||
{
|
||||
public:
|
||||
struct FindParameters
|
||||
{
|
||||
std::vector<ClusterId> clusters; // if non empty, releases that belong to these clusters
|
||||
std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords
|
||||
ReleaseSortMethod sortMethod{ ReleaseSortMethod::None };
|
||||
Range range;
|
||||
Wt::WDateTime writtenAfter;
|
||||
std::optional<DateRange> dateRange;
|
||||
UserId starringUser; // only releases starred by this user
|
||||
std::optional<Scrobbler> scrobbler; // and for this scrobbler
|
||||
ArtistId artist; // only releases that involved this user
|
||||
EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types
|
||||
EnumSet<TrackArtistLinkType> excludedTrackArtistLinkTypes; // but not for these link types
|
||||
std::optional<ReleaseTypePrimary> primaryType; // if, set, matching this primary type
|
||||
EnumSet<ReleaseTypeSecondary> secondaryTypes; // Matching all this (if any)
|
||||
|
||||
FindParameters& setClusters(const std::vector<ClusterId>& _clusters) { clusters = _clusters; return *this; }
|
||||
FindParameters& setKeywords(const std::vector<std::string_view>& _keywords) { keywords = _keywords; return *this; }
|
||||
FindParameters& setSortMethod(ReleaseSortMethod _sortMethod) {sortMethod = _sortMethod; return *this; }
|
||||
FindParameters& setRange(Range _range) {range = _range; return *this; }
|
||||
FindParameters& setWrittenAfter(const Wt::WDateTime& _after) {writtenAfter = _after; return *this; }
|
||||
FindParameters& setDateRange(const std::optional<DateRange>& _dateRange) {dateRange = _dateRange; return *this; }
|
||||
FindParameters& setStarringUser(UserId _user, Scrobbler _scrobbler) { starringUser = _user; scrobbler = _scrobbler; return *this; }
|
||||
FindParameters& setArtist(ArtistId _artist, EnumSet<TrackArtistLinkType> _trackArtistLinkTypes = {}, EnumSet<TrackArtistLinkType> _excludedTrackArtistLinkTypes = {})
|
||||
{
|
||||
artist = _artist;
|
||||
trackArtistLinkTypes = _trackArtistLinkTypes;
|
||||
excludedTrackArtistLinkTypes = _excludedTrackArtistLinkTypes;
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
FindParameters& setClusters(const std::vector<ClusterId>& _clusters) { clusters = _clusters; return *this; }
|
||||
FindParameters& setKeywords(const std::vector<std::string_view>& _keywords) { keywords = _keywords; return *this; }
|
||||
FindParameters& setSortMethod(ReleaseSortMethod _sortMethod) { sortMethod = _sortMethod; return *this; }
|
||||
FindParameters& setRange(Range _range) { range = _range; return *this; }
|
||||
FindParameters& setWrittenAfter(const Wt::WDateTime& _after) { writtenAfter = _after; return *this; }
|
||||
FindParameters& setDateRange(const std::optional<DateRange>& _dateRange) { dateRange = _dateRange; return *this; }
|
||||
FindParameters& setStarringUser(UserId _user, Scrobbler _scrobbler) { starringUser = _user; scrobbler = _scrobbler; return *this; }
|
||||
FindParameters& setArtist(ArtistId _artist, EnumSet<TrackArtistLinkType> _trackArtistLinkTypes = {}, EnumSet<TrackArtistLinkType> _excludedTrackArtistLinkTypes = {})
|
||||
{
|
||||
artist = _artist;
|
||||
trackArtistLinkTypes = _trackArtistLinkTypes;
|
||||
excludedTrackArtistLinkTypes = _excludedTrackArtistLinkTypes;
|
||||
return *this;
|
||||
}
|
||||
};
|
||||
|
||||
Release() = default;
|
||||
Release() = default;
|
||||
|
||||
// Accessors
|
||||
static std::size_t getCount(Session& session);
|
||||
static bool exists(Session& session, ReleaseId id);
|
||||
static pointer find(Session& session, const UUID& MBID);
|
||||
static std::vector<pointer> find(Session& session, const std::string& name);
|
||||
static pointer find(Session& session, ReleaseId id);
|
||||
static RangeResults<ReleaseId> find(Session& session, const FindParameters& parameters);
|
||||
static RangeResults<ReleaseId> findOrphans(Session& session, Range range); // no track related
|
||||
static RangeResults<ReleaseId> findOrderedByArtist(Session& session, Range range);
|
||||
// Accessors
|
||||
static std::size_t getCount(Session& session);
|
||||
static bool exists(Session& session, ReleaseId id);
|
||||
static pointer find(Session& session, const UUID& MBID);
|
||||
static std::vector<pointer> find(Session& session, const std::string& name);
|
||||
static pointer find(Session& session, ReleaseId id);
|
||||
static RangeResults<ReleaseId> find(Session& session, const FindParameters& parameters);
|
||||
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;
|
||||
|
||||
// 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)
|
||||
Wt::WDate getReleaseDate() const;
|
||||
Wt::WDate getOriginalReleaseDate() const;
|
||||
std::optional<std::string> getCopyright() const;
|
||||
std::optional<std::string> getCopyrightURL() const;
|
||||
|
||||
// Utility functions (if all tracks have the same values, which is legit to not be the case)
|
||||
std::optional<int> getReleaseYear(bool originalDate = false) const;
|
||||
std::optional<std::string> getCopyright() const;
|
||||
std::optional<std::string> getCopyrightURL() const;
|
||||
// Accessors
|
||||
const std::string& getName() const { return _name; }
|
||||
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;
|
||||
|
||||
// Accessors
|
||||
const std::string& getName() const { return _name; }
|
||||
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::chrono::milliseconds getDuration() const;
|
||||
Wt::WDateTime getLastWritten() const;
|
||||
std::optional<ReleaseTypePrimary> getPrimaryType() const { return _primaryType; }
|
||||
EnumSet<ReleaseTypeSecondary> getSecondaryTypes() const { return _secondaryTypes; }
|
||||
// Setters
|
||||
void setName(std::string_view name) { _name = name; }
|
||||
void setMBID(const std::optional<UUID>& mbid) { _MBID = mbid ? mbid->getAsString() : ""; }
|
||||
void setTotalDisc(std::optional<int> totalDisc) { _totalDisc = totalDisc; }
|
||||
void setPrimaryType(std::optional<ReleaseTypePrimary> type) { _primaryType = type; }
|
||||
void setSecondaryTypes(EnumSet<ReleaseTypeSecondary> types) { _secondaryTypes = types; }
|
||||
|
||||
// Setters
|
||||
void setName(std::string_view name) { _name = name; }
|
||||
void setMBID(const std::optional<UUID>& mbid) { _MBID = mbid ? mbid->getAsString() : ""; }
|
||||
void setTotalDisc(std::optional<int> totalDisc) { _totalDisc = totalDisc; }
|
||||
void setPrimaryType(std::optional<ReleaseTypePrimary> type) { _primaryType = type; }
|
||||
void setSecondaryTypes(EnumSet<ReleaseTypeSecondary> types) { _secondaryTypes = types; }
|
||||
|
||||
// Get the artists of this release
|
||||
std::vector<ObjectPtr<Artist>> getArtists(TrackArtistLinkType type = TrackArtistLinkType::Artist) const;
|
||||
std::vector<ObjectPtr<Artist>> getReleaseArtists() const { return getArtists(TrackArtistLinkType::ReleaseArtist); }
|
||||
bool hasVariousArtists() const;
|
||||
std::vector<pointer> getSimilarReleases(std::optional<std::size_t> offset = {}, std::optional<std::size_t> count = {}) const;
|
||||
// Get the artists of this release
|
||||
std::vector<ObjectPtr<Artist>> getArtists(TrackArtistLinkType type = TrackArtistLinkType::Artist) const;
|
||||
std::vector<ObjectPtr<Artist>> getReleaseArtists() const { return getArtists(TrackArtistLinkType::ReleaseArtist); }
|
||||
bool hasVariousArtists() const;
|
||||
std::vector<pointer> getSimilarReleases(std::optional<std::size_t> offset = {}, std::optional<std::size_t> count = {}) const;
|
||||
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _name, "name");
|
||||
Wt::Dbo::field(a, _MBID, "mbid");
|
||||
Wt::Dbo::field(a, _totalDisc, "total_disc");
|
||||
Wt::Dbo::field(a, _primaryType, "primary_type");
|
||||
Wt::Dbo::field(a, _secondaryTypes, "secondary_types");
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _name, "name");
|
||||
Wt::Dbo::field(a, _MBID, "mbid");
|
||||
Wt::Dbo::field(a, _totalDisc, "total_disc");
|
||||
Wt::Dbo::field(a, _primaryType, "primary_type");
|
||||
Wt::Dbo::field(a, _secondaryTypes, "secondary_types");
|
||||
|
||||
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToOne, "release");
|
||||
}
|
||||
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToOne, "release");
|
||||
}
|
||||
|
||||
private:
|
||||
friend class Session;
|
||||
Release(const std::string& name, const std::optional<UUID>& MBID = {});
|
||||
static pointer create(Session& session, const std::string& name, const std::optional<UUID>& MBID = {});
|
||||
private:
|
||||
friend class Session;
|
||||
Release(const std::string& name, const std::optional<UUID>& MBID = {});
|
||||
static pointer create(Session& session, const std::string& name, const std::optional<UUID>& MBID = {});
|
||||
|
||||
static constexpr std::size_t _maxNameLength {128};
|
||||
Wt::WDate getReleaseDate(bool original) const;
|
||||
|
||||
std::string _name;
|
||||
std::string _MBID;
|
||||
std::optional<int> _totalDisc {};
|
||||
std::optional<ReleaseTypePrimary> _primaryType;
|
||||
EnumSet<ReleaseTypeSecondary> _secondaryTypes;
|
||||
static constexpr std::size_t _maxNameLength{ 128 };
|
||||
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> _tracks; // Tracks in the release
|
||||
};
|
||||
std::string _name;
|
||||
std::string _MBID;
|
||||
std::optional<int> _totalDisc{};
|
||||
std::optional<ReleaseTypePrimary> _primaryType;
|
||||
EnumSet<ReleaseTypeSecondary> _secondaryTypes;
|
||||
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> _tracks; // Tracks in the release
|
||||
};
|
||||
|
||||
} // namespace Database
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -35,55 +36,58 @@ LMS_DECLARE_IDTYPE(TrackArtistLinkId)
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Artist;
|
||||
class Session;
|
||||
class Track;
|
||||
class Artist;
|
||||
class Session;
|
||||
class Track;
|
||||
|
||||
class TrackArtistLink final : public Object<TrackArtistLink, TrackArtistLinkId>
|
||||
{
|
||||
public:
|
||||
struct FindParameters
|
||||
{
|
||||
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
|
||||
class TrackArtistLink final : public Object<TrackArtistLink, TrackArtistLinkId>
|
||||
{
|
||||
public:
|
||||
struct FindParameters
|
||||
{
|
||||
Range range;
|
||||
std::optional<TrackArtistLinkType> linkType; // if set, only artists that have produced at least one track with this link type
|
||||
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& setRelease(ReleaseId _release) { release = _release; return *this; }
|
||||
};
|
||||
FindParameters& setRange(Range _range) { range = _range; return *this; }
|
||||
FindParameters& setLinkType(std::optional<TrackArtistLinkType> _linkType) { linkType = _linkType; 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;
|
||||
TrackArtistLink(ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType);
|
||||
TrackArtistLink() = default;
|
||||
TrackArtistLink(ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType);
|
||||
|
||||
static RangeResults<TrackArtistLinkId> find(Session& session, const FindParameters& parameters);
|
||||
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 RangeResults<TrackArtistLinkId> find(Session& session, const FindParameters& parameters);
|
||||
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; }
|
||||
TrackArtistLinkType getType() const { return _type; }
|
||||
std::string_view getSubType() const { return _subType; }
|
||||
ObjectPtr<Track> getTrack() const { return _track; }
|
||||
ObjectPtr<Artist> getArtist() const { return _artist; }
|
||||
TrackArtistLinkType getType() const { return _type; }
|
||||
std::string_view getSubType() const { return _subType; }
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _type, "type");
|
||||
Wt::Dbo::field(a, _subType, "subtype");
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _type, "type");
|
||||
Wt::Dbo::field(a, _subType, "subtype");
|
||||
|
||||
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
|
||||
private:
|
||||
TrackArtistLinkType _type;
|
||||
std::string _subType;
|
||||
private:
|
||||
TrackArtistLinkType _type;
|
||||
std::string _subType;
|
||||
|
||||
Wt::Dbo::ptr<Track> _track;
|
||||
Wt::Dbo::ptr<Artist> _artist;
|
||||
};
|
||||
Wt::Dbo::ptr<Track> _track;
|
||||
Wt::Dbo::ptr<Artist> _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)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -30,33 +30,33 @@
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Session;
|
||||
class TrackList;
|
||||
class User;
|
||||
class Session;
|
||||
class TrackList;
|
||||
class User;
|
||||
}
|
||||
|
||||
namespace Scrobbling
|
||||
{
|
||||
class IScrobbler
|
||||
{
|
||||
public:
|
||||
virtual ~IScrobbler() = default;
|
||||
class IScrobbler
|
||||
{
|
||||
public:
|
||||
virtual ~IScrobbler() = default;
|
||||
|
||||
// Listens
|
||||
virtual void listenStarted(const Listen& listen) = 0;
|
||||
virtual void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) = 0;
|
||||
virtual void addTimedListen(const TimedListen& listen) = 0;
|
||||
// Listens
|
||||
virtual void listenStarted(const Listen& listen) = 0;
|
||||
virtual void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) = 0;
|
||||
virtual void addTimedListen(const TimedListen& listen) = 0;
|
||||
|
||||
// Feedbacks
|
||||
virtual void onStarred(Database::StarredArtistId) = 0;
|
||||
virtual void onUnstarred(Database::StarredArtistId) = 0;
|
||||
virtual void onStarred(Database::StarredReleaseId) = 0;
|
||||
virtual void onUnstarred(Database::StarredReleaseId) = 0;
|
||||
virtual void onStarred(Database::StarredTrackId) = 0;
|
||||
virtual void onUnstarred(Database::StarredTrackId) = 0;
|
||||
};
|
||||
// Feedbacks
|
||||
virtual void onStarred(Database::StarredArtistId) = 0;
|
||||
virtual void onUnstarred(Database::StarredArtistId) = 0;
|
||||
virtual void onStarred(Database::StarredReleaseId) = 0;
|
||||
virtual void onUnstarred(Database::StarredReleaseId) = 0;
|
||||
virtual void onStarred(Database::StarredTrackId) = 0;
|
||||
virtual void onUnstarred(Database::StarredTrackId) = 0;
|
||||
};
|
||||
|
||||
std::unique_ptr<IScrobbler> createScrobbler(std::string_view backendName);
|
||||
std::unique_ptr<IScrobbler> createScrobbler(std::string_view backendName);
|
||||
|
||||
} // ns Scrobbling
|
||||
|
||||
|
||||
@@ -37,272 +37,264 @@
|
||||
|
||||
namespace Scrobbling
|
||||
{
|
||||
using namespace Database;
|
||||
using namespace Database;
|
||||
|
||||
std::unique_ptr<IScrobblingService>
|
||||
createScrobblingService(boost::asio::io_context& ioContext, Db& db)
|
||||
{
|
||||
return std::make_unique<ScrobblingService>(ioContext, db);
|
||||
}
|
||||
std::unique_ptr<IScrobblingService> createScrobblingService(boost::asio::io_context& ioContext, Db& db)
|
||||
{
|
||||
return std::make_unique<ScrobblingService>(ioContext, db);
|
||||
}
|
||||
|
||||
ScrobblingService::ScrobblingService(boost::asio::io_context& ioContext, Db& db)
|
||||
: _db {db}
|
||||
{
|
||||
LMS_LOG(SCROBBLING, INFO) << "Starting service...";
|
||||
_scrobblers.emplace(Scrobbler::Internal, std::make_unique<InternalScrobbler>(_db));
|
||||
_scrobblers.emplace(Scrobbler::ListenBrainz, std::make_unique<ListenBrainz::Scrobbler>(ioContext, _db));
|
||||
LMS_LOG(SCROBBLING, INFO) << "Service started!";
|
||||
}
|
||||
ScrobblingService::ScrobblingService(boost::asio::io_context& ioContext, Db& db)
|
||||
: _db{ db }
|
||||
{
|
||||
LMS_LOG(SCROBBLING, INFO) << "Starting service...";
|
||||
_scrobblers.emplace(Scrobbler::Internal, std::make_unique<InternalScrobbler>(_db));
|
||||
_scrobblers.emplace(Scrobbler::ListenBrainz, std::make_unique<ListenBrainz::Scrobbler>(ioContext, _db));
|
||||
LMS_LOG(SCROBBLING, INFO) << "Service started!";
|
||||
}
|
||||
|
||||
ScrobblingService::~ScrobblingService()
|
||||
{
|
||||
LMS_LOG(SCROBBLING, INFO) << "Service stopped!";
|
||||
}
|
||||
ScrobblingService::~ScrobblingService()
|
||||
{
|
||||
LMS_LOG(SCROBBLING, INFO) << "Service stopped!";
|
||||
}
|
||||
|
||||
void
|
||||
ScrobblingService::listenStarted(const Listen& listen)
|
||||
{
|
||||
if (std::optional<Scrobbler> scrobbler {getUserScrobbler(listen.userId)})
|
||||
_scrobblers[*scrobbler]->listenStarted(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)
|
||||
{
|
||||
if (std::optional<Scrobbler> scrobbler {getUserScrobbler(listen.userId)})
|
||||
_scrobblers[*scrobbler]->listenFinished(listen, 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)
|
||||
{
|
||||
if (std::optional<Scrobbler> scrobbler {getUserScrobbler(listen.userId)})
|
||||
_scrobblers[*scrobbler]->addTimedListen(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> scrobbler;
|
||||
std::optional<Scrobbler> ScrobblingService::getUserScrobbler(UserId userId)
|
||||
{
|
||||
std::optional<Scrobbler> scrobbler;
|
||||
|
||||
Session& session {_db.getTLSSession()};
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
if (const User::pointer user {User::find(session, userId)})
|
||||
scrobbler = user->getScrobbler();
|
||||
Session& session{ _db.getTLSSession() };
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
if (const User::pointer user{ User::find(session, userId) })
|
||||
scrobbler = user->getScrobbler();
|
||||
|
||||
return scrobbler;
|
||||
}
|
||||
return scrobbler;
|
||||
}
|
||||
|
||||
ScrobblingService::ArtistContainer
|
||||
ScrobblingService::getRecentArtists(UserId userId, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range)
|
||||
{
|
||||
ArtistContainer res;
|
||||
ScrobblingService::ArtistContainer ScrobblingService::getRecentArtists(UserId userId, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range)
|
||||
{
|
||||
ArtistContainer res;
|
||||
|
||||
auto scrobbler {getUserScrobbler(userId)};
|
||||
if (!scrobbler)
|
||||
return res;
|
||||
auto scrobbler{ getUserScrobbler(userId) };
|
||||
if (!scrobbler)
|
||||
return res;
|
||||
|
||||
Session& session {_db.getTLSSession()};
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
Session& session{ _db.getTLSSession() };
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
|
||||
res = Database::Listen::getRecentArtists(session, userId, *scrobbler, clusterIds, linkType, range);
|
||||
return res;
|
||||
}
|
||||
res = Database::Listen::getRecentArtists(session, userId, *scrobbler, clusterIds, linkType, range);
|
||||
return res;
|
||||
}
|
||||
|
||||
ScrobblingService::ReleaseContainer
|
||||
ScrobblingService::getRecentReleases(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
|
||||
{
|
||||
ReleaseContainer res;
|
||||
ScrobblingService::ReleaseContainer ScrobblingService::getRecentReleases(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
|
||||
{
|
||||
ReleaseContainer res;
|
||||
|
||||
auto scrobbler {getUserScrobbler(userId)};
|
||||
if (!scrobbler)
|
||||
return res;
|
||||
auto scrobbler{ getUserScrobbler(userId) };
|
||||
if (!scrobbler)
|
||||
return res;
|
||||
|
||||
Session& session {_db.getTLSSession()};
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
Session& session{ _db.getTLSSession() };
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
|
||||
res = Database::Listen::getRecentReleases(session, userId, *scrobbler, clusterIds, range);
|
||||
return res;
|
||||
}
|
||||
res = Database::Listen::getRecentReleases(session, userId, *scrobbler, clusterIds, range);
|
||||
return res;
|
||||
}
|
||||
|
||||
ScrobblingService::TrackContainer
|
||||
ScrobblingService::getRecentTracks(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
|
||||
{
|
||||
TrackContainer res;
|
||||
ScrobblingService::TrackContainer ScrobblingService::getRecentTracks(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
|
||||
{
|
||||
TrackContainer res;
|
||||
|
||||
auto scrobbler {getUserScrobbler(userId)};
|
||||
if (!scrobbler)
|
||||
return res;
|
||||
auto scrobbler{ getUserScrobbler(userId) };
|
||||
if (!scrobbler)
|
||||
return res;
|
||||
|
||||
Session& session {_db.getTLSSession()};
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
Session& session{ _db.getTLSSession() };
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
|
||||
res = Database::Listen::getRecentTracks(session, userId, *scrobbler, clusterIds, range);
|
||||
return res;
|
||||
}
|
||||
res = Database::Listen::getRecentTracks(session, userId, *scrobbler, clusterIds, range);
|
||||
return res;
|
||||
}
|
||||
|
||||
// Top
|
||||
ScrobblingService::ArtistContainer
|
||||
ScrobblingService::getTopArtists(UserId userId, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range)
|
||||
{
|
||||
ArtistContainer res;
|
||||
// Top
|
||||
ScrobblingService::ArtistContainer ScrobblingService::getTopArtists(UserId userId, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range)
|
||||
{
|
||||
ArtistContainer res;
|
||||
|
||||
auto scrobbler {getUserScrobbler(userId)};
|
||||
if (!scrobbler)
|
||||
return res;
|
||||
auto scrobbler{ getUserScrobbler(userId) };
|
||||
if (!scrobbler)
|
||||
return res;
|
||||
|
||||
Session& session {_db.getTLSSession()};
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
Session& session{ _db.getTLSSession() };
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
|
||||
res = Database::Listen::getTopArtists(session, userId, *scrobbler, clusterIds, linkType, range);
|
||||
return res;
|
||||
}
|
||||
res = Database::Listen::getTopArtists(session, userId, *scrobbler, clusterIds, linkType, range);
|
||||
return res;
|
||||
}
|
||||
|
||||
ScrobblingService::ReleaseContainer
|
||||
ScrobblingService::getTopReleases(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
|
||||
{
|
||||
ReleaseContainer res;
|
||||
ScrobblingService::ReleaseContainer ScrobblingService::getTopReleases(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
|
||||
{
|
||||
ReleaseContainer res;
|
||||
|
||||
auto scrobbler {getUserScrobbler(userId)};
|
||||
if (!scrobbler)
|
||||
return res;
|
||||
auto scrobbler{ getUserScrobbler(userId) };
|
||||
if (!scrobbler)
|
||||
return res;
|
||||
|
||||
Session& session {_db.getTLSSession()};
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
Session& session{ _db.getTLSSession() };
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
|
||||
res = Database::Listen::getTopReleases(session, userId, *scrobbler, clusterIds, range);
|
||||
return res;
|
||||
}
|
||||
res = Database::Listen::getTopReleases(session, userId, *scrobbler, clusterIds, range);
|
||||
return res;
|
||||
}
|
||||
|
||||
ScrobblingService::TrackContainer
|
||||
ScrobblingService::getTopTracks(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
|
||||
{
|
||||
TrackContainer res;
|
||||
ScrobblingService::TrackContainer ScrobblingService::getTopTracks(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
|
||||
{
|
||||
TrackContainer res;
|
||||
|
||||
auto scrobbler {getUserScrobbler(userId)};
|
||||
if (!scrobbler)
|
||||
return res;
|
||||
auto scrobbler{ getUserScrobbler(userId) };
|
||||
if (!scrobbler)
|
||||
return res;
|
||||
|
||||
Session& session {_db.getTLSSession()};
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
Session& session{ _db.getTLSSession() };
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
|
||||
res = Database::Listen::getTopTracks(session, userId, *scrobbler, clusterIds, range);
|
||||
return res;
|
||||
}
|
||||
res = Database::Listen::getTopTracks(session, userId, *scrobbler, clusterIds, range);
|
||||
return res;
|
||||
}
|
||||
|
||||
void
|
||||
ScrobblingService::star(UserId userId, ArtistId artistId)
|
||||
{
|
||||
star<Artist, ArtistId, StarredArtist>(userId, artistId);
|
||||
}
|
||||
void ScrobblingService::star(UserId userId, ArtistId artistId)
|
||||
{
|
||||
star<Artist, ArtistId, StarredArtist>(userId, artistId);
|
||||
}
|
||||
|
||||
void
|
||||
ScrobblingService::unstar(UserId userId, ArtistId artistId)
|
||||
{
|
||||
unstar<Artist, ArtistId, StarredArtist>(userId, artistId);
|
||||
}
|
||||
void ScrobblingService::unstar(UserId userId, ArtistId artistId)
|
||||
{
|
||||
unstar<Artist, ArtistId, StarredArtist>(userId, artistId);
|
||||
}
|
||||
|
||||
bool
|
||||
ScrobblingService::isStarred(UserId userId, ArtistId artistId)
|
||||
{
|
||||
return isStarred<Artist, ArtistId, StarredArtist>(userId, 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,
|
||||
std::optional<TrackArtistLinkType> linkType,
|
||||
ArtistSortMethod sortMethod,
|
||||
Range range)
|
||||
{
|
||||
auto scrobbler {getUserScrobbler(userId)};
|
||||
if (!scrobbler)
|
||||
return {};
|
||||
Wt::WDateTime ScrobblingService::getStarredDateTime(UserId userId, ArtistId artistId)
|
||||
{
|
||||
return getStarredDateTime<Artist, ArtistId, StarredArtist>(userId, artistId);
|
||||
}
|
||||
|
||||
Artist::FindParameters params;
|
||||
params.setStarringUser(userId, *scrobbler);
|
||||
params.setClusters(clusterIds);
|
||||
params.setLinkType(linkType);
|
||||
params.setSortMethod(sortMethod);
|
||||
params.setRange(range);
|
||||
ScrobblingService::ArtistContainer ScrobblingService::getStarredArtists(UserId userId, const std::vector<ClusterId>& clusterIds,
|
||||
std::optional<TrackArtistLinkType> linkType,
|
||||
ArtistSortMethod sortMethod,
|
||||
Range range)
|
||||
{
|
||||
auto scrobbler{ getUserScrobbler(userId) };
|
||||
if (!scrobbler)
|
||||
return {};
|
||||
|
||||
Session& session {_db.getTLSSession()};
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
Artist::FindParameters params;
|
||||
params.setStarringUser(userId, *scrobbler);
|
||||
params.setClusters(clusterIds);
|
||||
params.setLinkType(linkType);
|
||||
params.setSortMethod(sortMethod);
|
||||
params.setRange(range);
|
||||
|
||||
return Artist::find(session, params);
|
||||
}
|
||||
Session& session{ _db.getTLSSession() };
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
|
||||
void
|
||||
ScrobblingService::star(UserId userId, ReleaseId releaseId)
|
||||
{
|
||||
star<Release, ReleaseId, StarredRelease>(userId, releaseId);
|
||||
}
|
||||
return Artist::find(session, params);
|
||||
}
|
||||
|
||||
void
|
||||
ScrobblingService::unstar(UserId userId, ReleaseId releaseId)
|
||||
{
|
||||
unstar<Release, ReleaseId, StarredRelease>(userId, releaseId);
|
||||
}
|
||||
void ScrobblingService::star(UserId userId, ReleaseId releaseId)
|
||||
{
|
||||
star<Release, ReleaseId, StarredRelease>(userId, releaseId);
|
||||
}
|
||||
|
||||
bool
|
||||
ScrobblingService::isStarred(UserId userId, ReleaseId releaseId)
|
||||
{
|
||||
return isStarred<Release, ReleaseId, StarredRelease>(userId, releaseId);
|
||||
}
|
||||
void ScrobblingService::unstar(UserId userId, ReleaseId releaseId)
|
||||
{
|
||||
unstar<Release, ReleaseId, StarredRelease>(userId, releaseId);
|
||||
}
|
||||
|
||||
ScrobblingService::ReleaseContainer
|
||||
ScrobblingService::getStarredReleases(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
|
||||
{
|
||||
auto scrobbler {getUserScrobbler(userId)};
|
||||
if (!scrobbler)
|
||||
return {};
|
||||
bool ScrobblingService::isStarred(UserId userId, ReleaseId releaseId)
|
||||
{
|
||||
return isStarred<Release, ReleaseId, StarredRelease>(userId, releaseId);
|
||||
}
|
||||
|
||||
Release::FindParameters params;
|
||||
params.setStarringUser(userId, *scrobbler);
|
||||
params.setClusters(clusterIds);
|
||||
params.setSortMethod(ReleaseSortMethod::StarredDateDesc);
|
||||
params.setRange(range);
|
||||
Wt::WDateTime ScrobblingService::getStarredDateTime(UserId userId, ReleaseId releaseId)
|
||||
{
|
||||
return getStarredDateTime<Release, ReleaseId, StarredRelease>(userId, releaseId);
|
||||
}
|
||||
|
||||
Session& session {_db.getTLSSession()};
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
ScrobblingService::ReleaseContainer ScrobblingService::getStarredReleases(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
|
||||
{
|
||||
auto scrobbler{ getUserScrobbler(userId) };
|
||||
if (!scrobbler)
|
||||
return {};
|
||||
|
||||
return Release::find(session, params);
|
||||
}
|
||||
Release::FindParameters params;
|
||||
params.setStarringUser(userId, *scrobbler);
|
||||
params.setClusters(clusterIds);
|
||||
params.setSortMethod(ReleaseSortMethod::StarredDateDesc);
|
||||
params.setRange(range);
|
||||
|
||||
void
|
||||
ScrobblingService::star(UserId userId, TrackId trackId)
|
||||
{
|
||||
star<Track, TrackId, StarredTrack>(userId, trackId);
|
||||
}
|
||||
Session& session{ _db.getTLSSession() };
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
|
||||
void
|
||||
ScrobblingService::unstar(UserId userId, TrackId trackId)
|
||||
{
|
||||
unstar<Track, TrackId, StarredTrack>(userId, trackId);
|
||||
}
|
||||
return Release::find(session, params);
|
||||
}
|
||||
|
||||
bool
|
||||
ScrobblingService::isStarred(UserId userId, TrackId trackId)
|
||||
{
|
||||
return isStarred<Track, TrackId, StarredTrack>(userId, trackId);
|
||||
}
|
||||
void ScrobblingService::star(UserId userId, TrackId trackId)
|
||||
{
|
||||
star<Track, TrackId, StarredTrack>(userId, trackId);
|
||||
}
|
||||
|
||||
ScrobblingService::TrackContainer
|
||||
ScrobblingService::getStarredTracks(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
|
||||
{
|
||||
auto scrobbler {getUserScrobbler(userId)};
|
||||
if (!scrobbler)
|
||||
return {};
|
||||
void ScrobblingService::unstar(UserId userId, TrackId trackId)
|
||||
{
|
||||
unstar<Track, TrackId, StarredTrack>(userId, trackId);
|
||||
}
|
||||
|
||||
Track::FindParameters params;
|
||||
params.setStarringUser(userId, *scrobbler);
|
||||
params.setClusters(clusterIds);
|
||||
params.setSortMethod(TrackSortMethod::StarredDateDesc);
|
||||
params.setRange(range);
|
||||
bool ScrobblingService::isStarred(UserId userId, TrackId trackId)
|
||||
{
|
||||
return isStarred<Track, TrackId, StarredTrack>(userId, trackId);
|
||||
}
|
||||
|
||||
Session& session {_db.getTLSSession()};
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
Wt::WDateTime ScrobblingService::getStarredDateTime(UserId userId, TrackId trackId)
|
||||
{
|
||||
return getStarredDateTime<Track, TrackId, StarredTrack>(userId, trackId);
|
||||
}
|
||||
|
||||
return Track::find(session, params);
|
||||
}
|
||||
ScrobblingService::TrackContainer ScrobblingService::getStarredTracks(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
|
||||
{
|
||||
auto scrobbler{ getUserScrobbler(userId) };
|
||||
if (!scrobbler)
|
||||
return {};
|
||||
|
||||
Track::FindParameters params;
|
||||
params.setStarringUser(userId, *scrobbler);
|
||||
params.setClusters(clusterIds);
|
||||
params.setSortMethod(TrackSortMethod::StarredDateDesc);
|
||||
params.setRange(range);
|
||||
|
||||
Session& session{ _db.getTLSSession() };
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
|
||||
return Track::find(session, params);
|
||||
}
|
||||
} // ns Scrobbling
|
||||
|
||||
|
||||
@@ -28,74 +28,79 @@
|
||||
|
||||
namespace Scrobbling
|
||||
{
|
||||
class ScrobblingService : public IScrobblingService
|
||||
{
|
||||
public:
|
||||
ScrobblingService(boost::asio::io_context& ioContext, Database::Db& db);
|
||||
~ScrobblingService();
|
||||
class ScrobblingService : public IScrobblingService
|
||||
{
|
||||
public:
|
||||
ScrobblingService(boost::asio::io_context& ioContext, Database::Db& db);
|
||||
~ScrobblingService();
|
||||
|
||||
private:
|
||||
void listenStarted(const Listen& listen) override;
|
||||
void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) override;
|
||||
void addTimedListen(const TimedListen& listen) override;
|
||||
private:
|
||||
void listenStarted(const Listen& listen) override;
|
||||
void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) override;
|
||||
void addTimedListen(const TimedListen& listen) override;
|
||||
|
||||
ArtistContainer getRecentArtists(Database::UserId userId,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::TrackArtistLinkType> linkType,
|
||||
Database::Range range) override;
|
||||
ArtistContainer getRecentArtists(Database::UserId userId,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::TrackArtistLinkType> linkType,
|
||||
Database::Range range) override;
|
||||
|
||||
ReleaseContainer getRecentReleases(Database::UserId userId,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
Database::Range range) override;
|
||||
ReleaseContainer getRecentReleases(Database::UserId userId,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
Database::Range range) override;
|
||||
|
||||
TrackContainer getRecentTracks(Database::UserId userId,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
Database::Range range) override;
|
||||
TrackContainer getRecentTracks(Database::UserId userId,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
Database::Range range) override;
|
||||
|
||||
ArtistContainer getTopArtists(Database::UserId userId,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::TrackArtistLinkType> linkType,
|
||||
Database::Range range) override;
|
||||
ArtistContainer getTopArtists(Database::UserId userId,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::TrackArtistLinkType> linkType,
|
||||
Database::Range range) override;
|
||||
|
||||
ReleaseContainer getTopReleases(Database::UserId userId,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
Database::Range range) override;
|
||||
ReleaseContainer getTopReleases(Database::UserId userId,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
Database::Range range) override;
|
||||
|
||||
TrackContainer getTopTracks(Database::UserId userId,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
Database::Range range) override;
|
||||
TrackContainer getTopTracks(Database::UserId userId,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
Database::Range range) override;
|
||||
|
||||
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;
|
||||
ArtistContainer getStarredArtists(Database::UserId userId,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::TrackArtistLinkType> linkType,
|
||||
Database::ArtistSortMethod sortMethod,
|
||||
Database::Range range) override;
|
||||
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,
|
||||
Database::ArtistSortMethod sortMethod,
|
||||
Database::Range range) override;
|
||||
|
||||
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;
|
||||
ReleaseContainer getStarredReleases(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) override;
|
||||
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 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;
|
||||
TrackContainer getStarredTracks(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);
|
||||
std::optional<Database::Scrobbler> getUserScrobbler(Database::UserId userId);
|
||||
|
||||
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
||||
void star(Database::UserId userId, ObjIdType id);
|
||||
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
||||
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>
|
||||
void star(Database::UserId userId, ObjIdType id);
|
||||
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
||||
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;
|
||||
};
|
||||
Database::Db& _db;
|
||||
std::unordered_map<Database::Scrobbler, std::unique_ptr<IScrobbler>> _scrobblers;
|
||||
};
|
||||
|
||||
} // ns Scrobbling
|
||||
|
||||
|
||||
@@ -25,76 +25,90 @@
|
||||
|
||||
namespace Scrobbling
|
||||
{
|
||||
using namespace Database;
|
||||
using namespace Database;
|
||||
|
||||
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
||||
void
|
||||
ScrobblingService::star(UserId userId, ObjIdType objId)
|
||||
{
|
||||
auto scrobbler {getUserScrobbler(userId)};
|
||||
if (!scrobbler)
|
||||
return;
|
||||
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
||||
void ScrobblingService::star(UserId userId, ObjIdType objId)
|
||||
{
|
||||
auto scrobbler {getUserScrobbler(userId)};
|
||||
if (!scrobbler)
|
||||
return;
|
||||
|
||||
typename StarredObjType::IdType starredObjId;
|
||||
{
|
||||
Session& session {_db.getTLSSession()};
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
typename StarredObjType::IdType starredObjId;
|
||||
{
|
||||
Session& session {_db.getTLSSession()};
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
typename StarredObjType::pointer starredObj {StarredObjType::find(session, objId, userId, *scrobbler)};
|
||||
if (!starredObj)
|
||||
{
|
||||
const typename ObjType::pointer obj {ObjType::find(session, objId)};
|
||||
if (!obj)
|
||||
return;
|
||||
typename StarredObjType::pointer starredObj {StarredObjType::find(session, objId, userId, *scrobbler)};
|
||||
if (!starredObj)
|
||||
{
|
||||
const typename ObjType::pointer obj {ObjType::find(session, objId)};
|
||||
if (!obj)
|
||||
return;
|
||||
|
||||
const User::pointer user {User::find(session, userId)};
|
||||
if (!user)
|
||||
return;
|
||||
const User::pointer user {User::find(session, userId)};
|
||||
if (!user)
|
||||
return;
|
||||
|
||||
starredObj = session.create<StarredObjType>(obj, user, *scrobbler);
|
||||
}
|
||||
starredObj.modify()->setDateTime(Wt::WDateTime::currentDateTime());
|
||||
starredObjId = starredObj->getId();
|
||||
}
|
||||
_scrobblers[*scrobbler]->onStarred(starredObjId);
|
||||
}
|
||||
starredObj = session.create<StarredObjType>(obj, user, *scrobbler);
|
||||
}
|
||||
starredObj.modify()->setDateTime(Wt::WDateTime::currentDateTime());
|
||||
starredObjId = starredObj->getId();
|
||||
}
|
||||
_scrobblers[*scrobbler]->onStarred(starredObjId);
|
||||
}
|
||||
|
||||
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
||||
void
|
||||
ScrobblingService::unstar(UserId userId, ObjIdType objId)
|
||||
{
|
||||
auto scrobbler {getUserScrobbler(userId)};
|
||||
if (!scrobbler)
|
||||
return;
|
||||
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
||||
void ScrobblingService::unstar(UserId userId, ObjIdType objId)
|
||||
{
|
||||
auto scrobbler {getUserScrobbler(userId)};
|
||||
if (!scrobbler)
|
||||
return;
|
||||
|
||||
typename StarredObjType::IdType starredObjId;
|
||||
{
|
||||
Session& session {_db.getTLSSession()};
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
typename StarredObjType::IdType starredObjId;
|
||||
{
|
||||
Session& session {_db.getTLSSession()};
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
typename StarredObjType::pointer starredObj {StarredObjType::find(session, objId, userId, *scrobbler)};
|
||||
if (!starredObj)
|
||||
return;
|
||||
typename StarredObjType::pointer starredObj {StarredObjType::find(session, objId, userId, *scrobbler)};
|
||||
if (!starredObj)
|
||||
return;
|
||||
|
||||
starredObjId = starredObj->getId();
|
||||
}
|
||||
_scrobblers[*scrobbler]->onUnstarred(starredObjId);
|
||||
}
|
||||
starredObjId = starredObj->getId();
|
||||
}
|
||||
_scrobblers[*scrobbler]->onUnstarred(starredObjId);
|
||||
}
|
||||
|
||||
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
||||
bool
|
||||
ScrobblingService::isStarred(UserId userId, ObjIdType objId)
|
||||
{
|
||||
auto scrobbler {getUserScrobbler(userId)};
|
||||
if (!scrobbler)
|
||||
return false;
|
||||
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
||||
bool ScrobblingService::isStarred(UserId userId, ObjIdType objId)
|
||||
{
|
||||
auto scrobbler {getUserScrobbler(userId)};
|
||||
if (!scrobbler)
|
||||
return false;
|
||||
|
||||
Session& session {_db.getTLSSession()};
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
Session& session {_db.getTLSSession()};
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
typename StarredObjType::pointer starredObj {StarredObjType::find(session, objId, userId, *scrobbler)};
|
||||
return starredObj && (starredObj->getScrobblingState() != ScrobblingState::PendingRemove);
|
||||
}
|
||||
typename StarredObjType::pointer starredObj {StarredObjType::find(session, objId, userId, *scrobbler)};
|
||||
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
|
||||
|
||||
|
||||
@@ -30,101 +30,93 @@
|
||||
|
||||
namespace
|
||||
{
|
||||
template <typename StarredObjType>
|
||||
void onStarred(Database::Session& session, typename StarredObjType::IdType id)
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
template <typename StarredObjType>
|
||||
void onStarred(Database::Session& session, typename StarredObjType::IdType id)
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
|
||||
if (auto starredObj {StarredObjType::find(session, id)})
|
||||
starredObj.modify()->setScrobblingState(Database::ScrobblingState::Synchronized);
|
||||
}
|
||||
if (auto starredObj{ StarredObjType::find(session, id) })
|
||||
starredObj.modify()->setScrobblingState(Database::ScrobblingState::Synchronized);
|
||||
}
|
||||
|
||||
template <typename StarredObjType>
|
||||
void onUnstarred(Database::Session& session, typename StarredObjType::IdType id)
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
template <typename StarredObjType>
|
||||
void onUnstarred(Database::Session& session, typename StarredObjType::IdType id)
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
|
||||
if (auto starredObj {StarredObjType::find(session, id)})
|
||||
starredObj.remove();
|
||||
}
|
||||
if (auto starredObj{ StarredObjType::find(session, id) })
|
||||
starredObj.remove();
|
||||
}
|
||||
}
|
||||
|
||||
namespace Scrobbling
|
||||
{
|
||||
InternalScrobbler::InternalScrobbler(Database::Db& db)
|
||||
: _db {db}
|
||||
{}
|
||||
InternalScrobbler::InternalScrobbler(Database::Db& db)
|
||||
: _db{ db }
|
||||
{}
|
||||
|
||||
void
|
||||
InternalScrobbler::listenStarted(const Listen&)
|
||||
{
|
||||
// nothing to do
|
||||
}
|
||||
void InternalScrobbler::listenStarted(const Listen&)
|
||||
{
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
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})
|
||||
return;
|
||||
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 })
|
||||
return;
|
||||
|
||||
addTimedListen({listen, Wt::WDateTime::currentDateTime()});
|
||||
}
|
||||
addTimedListen({ listen, Wt::WDateTime::currentDateTime() });
|
||||
}
|
||||
|
||||
void
|
||||
InternalScrobbler::addTimedListen(const TimedListen& listen)
|
||||
{
|
||||
Database::Session& session {_db.getTLSSession()};
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
void InternalScrobbler::addTimedListen(const TimedListen& listen)
|
||||
{
|
||||
Database::Session& session{ _db.getTLSSession() };
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
|
||||
if (Database::Listen::find(session, listen.userId, listen.trackId, Database::Scrobbler::Internal, listen.listenedAt))
|
||||
return;
|
||||
if (Database::Listen::find(session, listen.userId, listen.trackId, Database::Scrobbler::Internal, listen.listenedAt))
|
||||
return;
|
||||
|
||||
const Database::User::pointer user {Database::User::find(session, listen.userId)};
|
||||
if (!user)
|
||||
return;
|
||||
const Database::User::pointer user{ Database::User::find(session, listen.userId) };
|
||||
if (!user)
|
||||
return;
|
||||
|
||||
const Database::Track::pointer track {Database::Track::find(session, listen.trackId)};
|
||||
if (!track)
|
||||
return;
|
||||
const Database::Track::pointer track{ Database::Track::find(session, listen.trackId) };
|
||||
if (!track)
|
||||
return;
|
||||
|
||||
auto dbListen {session.create<Database::Listen>(user, track, Database::Scrobbler::Internal, listen.listenedAt)};
|
||||
dbListen.modify()->setScrobblingState(Database::ScrobblingState::Synchronized);
|
||||
}
|
||||
auto dbListen{ session.create<Database::Listen>(user, track, Database::Scrobbler::Internal, listen.listenedAt) };
|
||||
dbListen.modify()->setScrobblingState(Database::ScrobblingState::Synchronized);
|
||||
}
|
||||
|
||||
void
|
||||
InternalScrobbler::onStarred(Database::StarredArtistId starredArtistId)
|
||||
{
|
||||
::onStarred<Database::StarredArtist>(_db.getTLSSession(), starredArtistId);
|
||||
}
|
||||
void InternalScrobbler::onStarred(Database::StarredArtistId starredArtistId)
|
||||
{
|
||||
::onStarred<Database::StarredArtist>(_db.getTLSSession(), starredArtistId);
|
||||
}
|
||||
|
||||
void
|
||||
InternalScrobbler::onUnstarred(Database::StarredArtistId starredArtistId)
|
||||
{
|
||||
::onUnstarred<Database::StarredArtist>(_db.getTLSSession(), starredArtistId);
|
||||
}
|
||||
void InternalScrobbler::onUnstarred(Database::StarredArtistId starredArtistId)
|
||||
{
|
||||
::onUnstarred<Database::StarredArtist>(_db.getTLSSession(), starredArtistId);
|
||||
}
|
||||
|
||||
void
|
||||
InternalScrobbler::onStarred(Database::StarredReleaseId starredReleaseId)
|
||||
{
|
||||
::onStarred<Database::StarredRelease>(_db.getTLSSession(), starredReleaseId);
|
||||
}
|
||||
void InternalScrobbler::onStarred(Database::StarredReleaseId starredReleaseId)
|
||||
{
|
||||
::onStarred<Database::StarredRelease>(_db.getTLSSession(), starredReleaseId);
|
||||
}
|
||||
|
||||
void InternalScrobbler::onUnstarred(Database::StarredReleaseId starredReleaseId)
|
||||
{
|
||||
::onUnstarred<Database::StarredRelease>(_db.getTLSSession(), starredReleaseId);
|
||||
}
|
||||
void InternalScrobbler::onUnstarred(Database::StarredReleaseId starredReleaseId)
|
||||
{
|
||||
::onUnstarred<Database::StarredRelease>(_db.getTLSSession(), starredReleaseId);
|
||||
}
|
||||
|
||||
void
|
||||
InternalScrobbler::onStarred(Database::StarredTrackId starredTrackId)
|
||||
{
|
||||
::onStarred<Database::StarredTrack>(_db.getTLSSession(), starredTrackId);
|
||||
}
|
||||
void InternalScrobbler::onStarred(Database::StarredTrackId starredTrackId)
|
||||
{
|
||||
::onStarred<Database::StarredTrack>(_db.getTLSSession(), starredTrackId);
|
||||
}
|
||||
|
||||
void
|
||||
InternalScrobbler::onUnstarred(Database::StarredTrackId starredTrackId)
|
||||
{
|
||||
::onUnstarred<Database::StarredTrack>(_db.getTLSSession(), starredTrackId);
|
||||
}
|
||||
void InternalScrobbler::onUnstarred(Database::StarredTrackId starredTrackId)
|
||||
{
|
||||
::onUnstarred<Database::StarredTrack>(_db.getTLSSession(), starredTrackId);
|
||||
}
|
||||
} // Scrobbling
|
||||
|
||||
|
||||
@@ -23,30 +23,30 @@
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Db;
|
||||
class Db;
|
||||
}
|
||||
|
||||
namespace Scrobbling
|
||||
{
|
||||
class InternalScrobbler final : public IScrobbler
|
||||
{
|
||||
public:
|
||||
InternalScrobbler(Database::Db& db);
|
||||
class InternalScrobbler final : public IScrobbler
|
||||
{
|
||||
public:
|
||||
InternalScrobbler(Database::Db& db);
|
||||
|
||||
private:
|
||||
// IScrobbler
|
||||
void listenStarted(const Listen& listen) override;
|
||||
void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) override;
|
||||
void addTimedListen(const TimedListen& listen) override;
|
||||
private:
|
||||
// IScrobbler
|
||||
void listenStarted(const Listen& listen) override;
|
||||
void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) override;
|
||||
void addTimedListen(const TimedListen& listen) override;
|
||||
|
||||
void onStarred(Database::StarredArtistId) override;
|
||||
void onUnstarred(Database::StarredArtistId) override;
|
||||
void onStarred(Database::StarredReleaseId) override;
|
||||
void onUnstarred(Database::StarredReleaseId) override;
|
||||
void onStarred(Database::StarredTrackId) override;
|
||||
void onUnstarred(Database::StarredTrackId) override;
|
||||
void onStarred(Database::StarredArtistId) override;
|
||||
void onUnstarred(Database::StarredArtistId) override;
|
||||
void onStarred(Database::StarredReleaseId) override;
|
||||
void onUnstarred(Database::StarredReleaseId) override;
|
||||
void onStarred(Database::StarredTrackId) override;
|
||||
void onUnstarred(Database::StarredTrackId) override;
|
||||
|
||||
Database::Db& _db;
|
||||
};
|
||||
Database::Db& _db;
|
||||
};
|
||||
} // Scrobbling
|
||||
|
||||
|
||||
@@ -23,9 +23,9 @@
|
||||
|
||||
namespace Scrobbling
|
||||
{
|
||||
class Exception : public LmsException
|
||||
{
|
||||
public:
|
||||
using LmsException::LmsException;
|
||||
};
|
||||
class Exception : public LmsException
|
||||
{
|
||||
public:
|
||||
using LmsException::LmsException;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -34,77 +34,80 @@
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Db;
|
||||
class Db;
|
||||
}
|
||||
|
||||
namespace Scrobbling
|
||||
{
|
||||
|
||||
class IScrobblingService
|
||||
{
|
||||
public:
|
||||
virtual ~IScrobblingService() = default;
|
||||
class IScrobblingService
|
||||
{
|
||||
public:
|
||||
virtual ~IScrobblingService() = default;
|
||||
|
||||
// Scrobbling
|
||||
virtual void listenStarted(const Listen& listen) = 0;
|
||||
virtual void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> playedDuration = std::nullopt) = 0;
|
||||
// Scrobbling
|
||||
virtual void listenStarted(const Listen& listen) = 0;
|
||||
virtual void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> playedDuration = std::nullopt) = 0;
|
||||
|
||||
virtual void addTimedListen(const TimedListen& listen) = 0;
|
||||
virtual void addTimedListen(const TimedListen& listen) = 0;
|
||||
|
||||
// Stats
|
||||
using ArtistContainer = Database::RangeResults<Database::ArtistId>;
|
||||
using ReleaseContainer = Database::RangeResults<Database::ReleaseId>;
|
||||
using TrackContainer = Database::RangeResults<Database::TrackId>;
|
||||
// From most recent to oldest
|
||||
virtual ArtistContainer getRecentArtists(Database::UserId userId,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::TrackArtistLinkType> linkType,
|
||||
Database::Range range) = 0;
|
||||
// Stats
|
||||
using ArtistContainer = Database::RangeResults<Database::ArtistId>;
|
||||
using ReleaseContainer = Database::RangeResults<Database::ReleaseId>;
|
||||
using TrackContainer = Database::RangeResults<Database::TrackId>;
|
||||
// From most recent to oldest
|
||||
virtual ArtistContainer getRecentArtists(Database::UserId userId,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::TrackArtistLinkType> linkType,
|
||||
Database::Range range) = 0;
|
||||
|
||||
virtual ReleaseContainer getRecentReleases(Database::UserId userId,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
Database::Range range) = 0;
|
||||
virtual ReleaseContainer getRecentReleases(Database::UserId userId,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
Database::Range range) = 0;
|
||||
|
||||
virtual TrackContainer getRecentTracks(Database::UserId userId,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
Database::Range range) = 0;
|
||||
virtual TrackContainer getRecentTracks(Database::UserId userId,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
Database::Range range) = 0;
|
||||
|
||||
// Top
|
||||
virtual ArtistContainer getTopArtists(Database::UserId userId,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::TrackArtistLinkType> linkType,
|
||||
Database::Range) = 0;
|
||||
// Top
|
||||
virtual ArtistContainer getTopArtists(Database::UserId userId,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::TrackArtistLinkType> linkType,
|
||||
Database::Range) = 0;
|
||||
|
||||
virtual ReleaseContainer getTopReleases(Database::UserId userId,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
Database::Range range) = 0;
|
||||
virtual ReleaseContainer getTopReleases(Database::UserId userId,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
Database::Range range) = 0;
|
||||
|
||||
virtual TrackContainer getTopTracks(Database::UserId userId,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
Database::Range range) = 0;
|
||||
virtual TrackContainer getTopTracks(Database::UserId userId,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
Database::Range range) = 0;
|
||||
|
||||
// Star
|
||||
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 ArtistContainer getStarredArtists(Database::UserId userId,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::TrackArtistLinkType> linkType,
|
||||
Database::ArtistSortMethod sortMethod,
|
||||
Database::Range range) = 0;
|
||||
// Star
|
||||
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,
|
||||
Database::ArtistSortMethod sortMethod,
|
||||
Database::Range range) = 0;
|
||||
|
||||
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 ReleaseContainer getStarredReleases(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) = 0;
|
||||
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 TrackContainer getStarredTracks(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;
|
||||
};
|
||||
|
||||
std::unique_ptr<IScrobblingService> createScrobblingService(boost::asio::io_service& ioService, Database::Db& db);
|
||||
std::unique_ptr<IScrobblingService> createScrobblingService(boost::asio::io_service& ioService, Database::Db& db);
|
||||
|
||||
} // ns Scrobbling
|
||||
|
||||
|
||||
@@ -26,15 +26,15 @@
|
||||
|
||||
namespace Scrobbling
|
||||
{
|
||||
struct Listen
|
||||
{
|
||||
Database::UserId userId {};
|
||||
Database::TrackId trackId {};
|
||||
};
|
||||
struct Listen
|
||||
{
|
||||
Database::UserId userId{};
|
||||
Database::TrackId trackId{};
|
||||
};
|
||||
|
||||
struct TimedListen : public Listen
|
||||
{
|
||||
Wt::WDateTime listenedAt;
|
||||
};
|
||||
struct TimedListen : public Listen
|
||||
{
|
||||
Wt::WDateTime listenedAt;
|
||||
};
|
||||
} // ns Scrobbling
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -24,11 +24,11 @@
|
||||
|
||||
namespace API::Subsonic
|
||||
{
|
||||
struct ClientInfo
|
||||
{
|
||||
std::string name;
|
||||
std::string user;
|
||||
std::string password;
|
||||
ProtocolVersion version;
|
||||
};
|
||||
struct ClientInfo
|
||||
{
|
||||
std::string name;
|
||||
std::string user;
|
||||
std::string password;
|
||||
ProtocolVersion version;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "ParameterParsing.hpp"
|
||||
|
||||
namespace API::Subsonic
|
||||
{
|
||||
bool hasParameter(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
|
||||
{
|
||||
return parameterMap.find(param) != std::cend(parameterMap);
|
||||
}
|
||||
|
||||
std::string decodePasswordIfNeeded(const std::string& password)
|
||||
{
|
||||
if (password.find("enc:") == 0)
|
||||
{
|
||||
auto decodedPassword{ StringUtils::stringFromHex(password.substr(4)) };
|
||||
if (!decodedPassword)
|
||||
return password; // fallback on plain password
|
||||
|
||||
return *decodedPassword;
|
||||
}
|
||||
|
||||
return password;
|
||||
}
|
||||
}
|
||||
@@ -16,10 +16,15 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* 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"
|
||||
@@ -27,65 +32,57 @@
|
||||
namespace API::Subsonic
|
||||
{
|
||||
|
||||
template<typename T>
|
||||
std::vector<T>
|
||||
getMultiParametersAs(const Wt::Http::ParameterMap& parameterMap, const std::string& paramName)
|
||||
{
|
||||
std::vector<T> res;
|
||||
template<typename T>
|
||||
std::vector<T> getMultiParametersAs(const Wt::Http::ParameterMap& parameterMap, const std::string& paramName)
|
||||
{
|
||||
std::vector<T> res;
|
||||
|
||||
auto it = parameterMap.find(paramName);
|
||||
if (it == parameterMap.end())
|
||||
return res;
|
||||
auto it = parameterMap.find(paramName);
|
||||
if (it == parameterMap.end())
|
||||
return res;
|
||||
|
||||
for (const std::string& param : it->second)
|
||||
{
|
||||
auto value {StringUtils::readAs<T>(param)};
|
||||
if (value)
|
||||
res.emplace_back(std::move(*value));
|
||||
}
|
||||
for (const std::string& param : it->second)
|
||||
{
|
||||
auto value{ StringUtils::readAs<T>(param) };
|
||||
if (value)
|
||||
res.emplace_back(std::move(*value));
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
std::vector<T>
|
||||
getMandatoryMultiParametersAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
|
||||
{
|
||||
std::vector<T> res {getMultiParametersAs<T>(parameterMap, param)};
|
||||
if (res.empty())
|
||||
throw RequiredParameterMissingError {param};
|
||||
template<typename T>
|
||||
std::vector<T> getMandatoryMultiParametersAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
|
||||
{
|
||||
std::vector<T> res{ getMultiParametersAs<T>(parameterMap, param) };
|
||||
if (res.empty())
|
||||
throw RequiredParameterMissingError{ param };
|
||||
|
||||
return res;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
std::optional<T>
|
||||
getParameterAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
|
||||
{
|
||||
std::vector<T> params {getMultiParametersAs<T>(parameterMap, param)};
|
||||
template<typename T>
|
||||
std::optional<T> getParameterAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
|
||||
{
|
||||
std::vector<T> params{ getMultiParametersAs<T>(parameterMap, param) };
|
||||
|
||||
if (params.size() != 1)
|
||||
return std::nullopt;
|
||||
if (params.size() != 1)
|
||||
return std::nullopt;
|
||||
|
||||
return T { std::move(params.front()) };
|
||||
}
|
||||
return T{ std::move(params.front()) };
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
T
|
||||
getMandatoryParameterAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
|
||||
{
|
||||
auto res {getParameterAs<T>(parameterMap, param)};
|
||||
if (!res)
|
||||
throw RequiredParameterMissingError {param};
|
||||
template<typename T>
|
||||
T getMandatoryParameterAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
|
||||
{
|
||||
auto res{ getParameterAs<T>(parameterMap, param) };
|
||||
if (!res)
|
||||
throw RequiredParameterMissingError{ param };
|
||||
|
||||
return *res;
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,36 +21,35 @@
|
||||
|
||||
namespace StringUtils
|
||||
{
|
||||
template<>
|
||||
std::optional<API::Subsonic::ProtocolVersion>
|
||||
readAs(std::string_view str)
|
||||
{
|
||||
// Expects "X.Y.Z"
|
||||
const auto numbers {StringUtils::splitString(str, ".")};
|
||||
if (numbers.size() < 2 || numbers.size() > 3)
|
||||
return std::nullopt;
|
||||
template<>
|
||||
std::optional<API::Subsonic::ProtocolVersion> readAs(std::string_view str)
|
||||
{
|
||||
// Expects "X.Y.Z"
|
||||
const auto numbers{ StringUtils::splitString(str, ".") };
|
||||
if (numbers.size() < 2 || numbers.size() > 3)
|
||||
return std::nullopt;
|
||||
|
||||
API::Subsonic::ProtocolVersion version;
|
||||
API::Subsonic::ProtocolVersion version;
|
||||
|
||||
auto number {StringUtils::readAs<unsigned>(numbers[0])};
|
||||
if (!number)
|
||||
return std::nullopt;
|
||||
version.major = *number;
|
||||
auto number{ StringUtils::readAs<unsigned>(numbers[0]) };
|
||||
if (!number)
|
||||
return std::nullopt;
|
||||
version.major = *number;
|
||||
|
||||
number = {StringUtils::readAs<unsigned>(numbers[1])};
|
||||
if (!number)
|
||||
return std::nullopt;
|
||||
version.minor = *number;
|
||||
number = { StringUtils::readAs<unsigned>(numbers[1]) };
|
||||
if (!number)
|
||||
return std::nullopt;
|
||||
version.minor = *number;
|
||||
|
||||
if (numbers.size() == 3)
|
||||
{
|
||||
number = {StringUtils::readAs<unsigned>(numbers[2])};
|
||||
if (!number)
|
||||
return std::nullopt;
|
||||
version.patch = *number;
|
||||
}
|
||||
if (numbers.size() == 3)
|
||||
{
|
||||
number = { StringUtils::readAs<unsigned>(numbers[2]) };
|
||||
if (!number)
|
||||
return std::nullopt;
|
||||
version.patch = *number;
|
||||
}
|
||||
|
||||
return version;
|
||||
}
|
||||
return version;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,18 +23,20 @@
|
||||
|
||||
namespace API::Subsonic
|
||||
{
|
||||
struct ProtocolVersion
|
||||
{
|
||||
unsigned major {};
|
||||
unsigned minor {};
|
||||
unsigned patch {};
|
||||
};
|
||||
struct ProtocolVersion
|
||||
{
|
||||
unsigned major{};
|
||||
unsigned minor{};
|
||||
unsigned patch{};
|
||||
};
|
||||
|
||||
static inline constexpr ProtocolVersion defaultServerProtocolVersion {1, 16, 0};
|
||||
static inline constexpr ProtocolVersion defaultServerProtocolVersion{ 1, 16, 0 };
|
||||
static inline constexpr std::string_view serverVersion{ "1" };
|
||||
}
|
||||
|
||||
namespace StringUtils
|
||||
{
|
||||
template<> std::optional<API::Subsonic::ProtocolVersion> readAs(std::string_view str);
|
||||
template<>
|
||||
std::optional<API::Subsonic::ProtocolVersion> readAs(std::string_view str);
|
||||
}
|
||||
|
||||
|
||||
@@ -29,18 +29,18 @@
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Session;
|
||||
class Session;
|
||||
}
|
||||
|
||||
namespace API::Subsonic
|
||||
{
|
||||
struct RequestContext
|
||||
{
|
||||
const Wt::Http::ParameterMap& parameters;
|
||||
Database::Session& dbSession;
|
||||
Database::UserId userId;
|
||||
ClientInfo clientInfo;
|
||||
ProtocolVersion serverProtocolVersion;
|
||||
};
|
||||
struct RequestContext
|
||||
{
|
||||
const Wt::Http::ParameterMap& parameters;
|
||||
Database::Session& dbSession;
|
||||
Database::UserId userId;
|
||||
ClientInfo clientInfo;
|
||||
ProtocolVersion serverProtocolVersion;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "Scan.hpp"
|
||||
|
||||
#include "services/scanner/IScannerService.hpp"
|
||||
#include "utils/Service.hpp"
|
||||
|
||||
namespace API::Subsonic::Scan
|
||||
{
|
||||
using namespace Scanner;
|
||||
|
||||
static
|
||||
Response::Node
|
||||
createStatusResponseNode()
|
||||
{
|
||||
Response::Node statusResponse;
|
||||
|
||||
const IScannerService::Status scanStatus {Service<IScannerService>::get()->getStatus()};
|
||||
|
||||
statusResponse.setAttribute("scanning", scanStatus.currentState == IScannerService::State::InProgress);
|
||||
if (scanStatus.currentState == IScannerService::State::InProgress)
|
||||
{
|
||||
std::size_t count{};
|
||||
|
||||
if (scanStatus.currentScanStepStats && scanStatus.currentScanStepStats->currentStep == ScanStep::ScanningFiles)
|
||||
count = scanStatus.currentScanStepStats->processedElems;
|
||||
|
||||
statusResponse.setAttribute("count", count);
|
||||
}
|
||||
|
||||
return statusResponse;
|
||||
}
|
||||
|
||||
|
||||
Response
|
||||
handleGetScanStatus(RequestContext& context)
|
||||
{
|
||||
Response response {Response::createOkResponse(context.serverProtocolVersion)};
|
||||
response.addNode("scanStatus", createStatusResponseNode());
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
Response
|
||||
handleStartScan(RequestContext& context)
|
||||
{
|
||||
Service<IScannerService>::get()->requestImmediateScan(false);
|
||||
|
||||
Response response {Response::createOkResponse(context.serverProtocolVersion)};
|
||||
response.addNode("scanStatus", createStatusResponseNode());
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,182 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "Stream.hpp"
|
||||
|
||||
#include "av/TranscodeParameters.hpp"
|
||||
#include "av/TranscodeResourceHandlerCreator.hpp"
|
||||
#include "av/Types.hpp"
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/Track.hpp"
|
||||
#include "services/database/User.hpp"
|
||||
#include "utils/IResourceHandler.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/FileResourceHandlerCreator.hpp"
|
||||
#include "utils/Utils.hpp"
|
||||
#include "ParameterParsing.hpp"
|
||||
#include "SubsonicId.hpp"
|
||||
|
||||
using namespace Database;
|
||||
|
||||
namespace API::Subsonic::Stream
|
||||
{
|
||||
|
||||
static
|
||||
Av::Format
|
||||
userTranscodeFormatToAvFormat(AudioFormat format)
|
||||
{
|
||||
switch (format)
|
||||
{
|
||||
case AudioFormat::MP3: return Av::Format::MP3;
|
||||
case AudioFormat::OGG_OPUS: return Av::Format::OGG_OPUS;
|
||||
case AudioFormat::MATROSKA_OPUS: return Av::Format::MATROSKA_OPUS;
|
||||
case AudioFormat::OGG_VORBIS: return Av::Format::OGG_VORBIS;
|
||||
case AudioFormat::WEBM_VORBIS: return Av::Format::WEBM_VORBIS;
|
||||
default: return Av::Format::OGG_OPUS;
|
||||
}
|
||||
}
|
||||
|
||||
struct StreamParameters
|
||||
{
|
||||
Av::InputFileParameters inputFileParameters;
|
||||
std::optional<Av::TranscodeParameters> transcodeParameters;
|
||||
bool estimateContentLength {};
|
||||
};
|
||||
|
||||
static
|
||||
StreamParameters
|
||||
getStreamParameters(RequestContext& context)
|
||||
{
|
||||
// Mandatory params
|
||||
const TrackId id {getMandatoryParameterAs<TrackId>(context.parameters, "id")};
|
||||
|
||||
// Optional params
|
||||
std::optional<std::size_t> maxBitRate {getParameterAs<std::size_t>(context.parameters, "maxBitRate")};
|
||||
std::optional<std::string> format {getParameterAs<std::string>(context.parameters, "format")};
|
||||
bool estimateContentLength {getParameterAs<bool>(context.parameters, "estimateContentLength").value_or(false)};
|
||||
|
||||
StreamParameters parameters;
|
||||
|
||||
parameters.estimateContentLength = estimateContentLength;
|
||||
|
||||
auto transaction {context.dbSession.createSharedTransaction()};
|
||||
|
||||
{
|
||||
auto track {Track::find(context.dbSession, id)};
|
||||
if (!track)
|
||||
throw RequestedDataNotFoundError {};
|
||||
|
||||
parameters.inputFileParameters.trackPath = track->getPath();
|
||||
parameters.inputFileParameters.duration = track->getDuration();
|
||||
}
|
||||
|
||||
{
|
||||
const User::pointer user {User::find(context.dbSession, context.userId)};
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError {};
|
||||
|
||||
// format = "raw" => no transcode. Other format values will be ignored
|
||||
const bool transcode {(!format || (*format != "raw")) && user->getSubsonicTranscodeEnable()};
|
||||
if (transcode)
|
||||
{
|
||||
std::size_t bitRate {user->getSubsonicTranscodeBitrate() / 1000};
|
||||
|
||||
// "If set to zero, no limit is imposed"
|
||||
if (maxBitRate && *maxBitRate != 0)
|
||||
bitRate = Utils::clamp(*maxBitRate, std::size_t {48}, bitRate);
|
||||
|
||||
Av::TranscodeParameters transcodeParameters;
|
||||
|
||||
transcodeParameters.bitrate = bitRate * 1000;
|
||||
transcodeParameters.format = userTranscodeFormatToAvFormat(user->getSubsonicTranscodeFormat());
|
||||
transcodeParameters.stripMetadata = false; // We want clients to use metadata (offline use, replay gain, etc.)
|
||||
|
||||
parameters.transcodeParameters = std::move(transcodeParameters);
|
||||
}
|
||||
}
|
||||
|
||||
return parameters;
|
||||
}
|
||||
|
||||
void
|
||||
handleDownload(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response)
|
||||
{
|
||||
std::shared_ptr<IResourceHandler> resourceHandler;
|
||||
|
||||
Wt::Http::ResponseContinuation* continuation {request.continuation()};
|
||||
if (!continuation)
|
||||
{
|
||||
// Mandatory params
|
||||
Database::TrackId id {getMandatoryParameterAs<Database::TrackId>(context.parameters, "id")};
|
||||
|
||||
std::filesystem::path trackPath;
|
||||
{
|
||||
auto transaction {context.dbSession.createSharedTransaction()};
|
||||
|
||||
auto track {Track::find(context.dbSession, id)};
|
||||
if (!track)
|
||||
throw RequestedDataNotFoundError {};
|
||||
|
||||
trackPath = track->getPath();
|
||||
}
|
||||
|
||||
resourceHandler = createFileResourceHandler(trackPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
resourceHandler = Wt::cpp17::any_cast<std::shared_ptr<IResourceHandler>>(continuation->data());
|
||||
}
|
||||
|
||||
continuation = resourceHandler->processRequest(request, response);
|
||||
if (continuation)
|
||||
continuation->setData(resourceHandler);
|
||||
}
|
||||
|
||||
void
|
||||
handleStream(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response)
|
||||
{
|
||||
std::shared_ptr<IResourceHandler> resourceHandler;
|
||||
|
||||
try
|
||||
{
|
||||
Wt::Http::ResponseContinuation* continuation = request.continuation();
|
||||
if (!continuation)
|
||||
{
|
||||
StreamParameters streamParameters {getStreamParameters(context)};
|
||||
if (streamParameters.transcodeParameters)
|
||||
resourceHandler = Av::createTranscodeResourceHandler(streamParameters.inputFileParameters, *streamParameters.transcodeParameters, streamParameters.estimateContentLength);
|
||||
else
|
||||
resourceHandler = createFileResourceHandler(streamParameters.inputFileParameters.trackPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
resourceHandler = Wt::cpp17::any_cast<std::shared_ptr<IResourceHandler>>(continuation->data());
|
||||
}
|
||||
|
||||
continuation = resourceHandler->processRequest(request, response);
|
||||
if (continuation)
|
||||
continuation->setData(resourceHandler);
|
||||
}
|
||||
catch (const Av::Exception& e)
|
||||
{
|
||||
LMS_LOG(API_SUBSONIC, ERROR) << "Caught Av exception: " << e.what();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace API::Subsonic::Stream
|
||||
@@ -26,114 +26,104 @@
|
||||
|
||||
namespace API::Subsonic
|
||||
{
|
||||
std::string
|
||||
idToString(Database::ArtistId id)
|
||||
{
|
||||
return "ar-" + id.toString();
|
||||
}
|
||||
std::string idToString(Database::ArtistId id)
|
||||
{
|
||||
return "ar-" + id.toString();
|
||||
}
|
||||
|
||||
std::string
|
||||
idToString(Database::ReleaseId id)
|
||||
{
|
||||
return "al-" + id.toString();
|
||||
}
|
||||
std::string idToString(Database::ReleaseId id)
|
||||
{
|
||||
return "al-" + id.toString();
|
||||
}
|
||||
|
||||
std::string
|
||||
idToString(RootId)
|
||||
{
|
||||
return "root";
|
||||
}
|
||||
std::string idToString(RootId)
|
||||
{
|
||||
return "root";
|
||||
}
|
||||
|
||||
std::string
|
||||
idToString(Database::TrackId id)
|
||||
{
|
||||
return "tr-" + id.toString();
|
||||
}
|
||||
std::string idToString(Database::TrackId id)
|
||||
{
|
||||
return "tr-" + id.toString();
|
||||
}
|
||||
|
||||
std::string
|
||||
idToString(Database::TrackListId id)
|
||||
{
|
||||
return "pl-" + id.toString();
|
||||
}
|
||||
std::string idToString(Database::TrackListId id)
|
||||
{
|
||||
return "pl-" + id.toString();
|
||||
}
|
||||
} // namespace API::Subsonic
|
||||
|
||||
namespace StringUtils
|
||||
{
|
||||
template<>
|
||||
std::optional<Database::ArtistId>
|
||||
readAs(std::string_view str)
|
||||
{
|
||||
std::vector<std::string_view> values {StringUtils::splitString(str, "-")};
|
||||
if (values.size() != 2)
|
||||
return std::nullopt;
|
||||
template<>
|
||||
std::optional<Database::ArtistId> readAs(std::string_view str)
|
||||
{
|
||||
std::vector<std::string_view> values{ StringUtils::splitString(str, "-") };
|
||||
if (values.size() != 2)
|
||||
return std::nullopt;
|
||||
|
||||
if (values[0] != "ar")
|
||||
return std::nullopt;
|
||||
if (values[0] != "ar")
|
||||
return std::nullopt;
|
||||
|
||||
if (const auto value {StringUtils::readAs<Database::ArtistId::ValueType>(values[1])})
|
||||
return Database::ArtistId {*value};
|
||||
if (const auto value{ StringUtils::readAs<Database::ArtistId::ValueType>(values[1]) })
|
||||
return Database::ArtistId{ *value };
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
template<>
|
||||
std::optional<Database::ReleaseId>
|
||||
readAs(std::string_view str)
|
||||
{
|
||||
std::vector<std::string_view> values {StringUtils::splitString(str, "-")};
|
||||
if (values.size() != 2)
|
||||
return std::nullopt;
|
||||
template<>
|
||||
std::optional<Database::ReleaseId> readAs(std::string_view str)
|
||||
{
|
||||
std::vector<std::string_view> values{ StringUtils::splitString(str, "-") };
|
||||
if (values.size() != 2)
|
||||
return std::nullopt;
|
||||
|
||||
if (values[0] != "al")
|
||||
return std::nullopt;
|
||||
if (values[0] != "al")
|
||||
return std::nullopt;
|
||||
|
||||
if (const auto value {StringUtils::readAs<Database::ReleaseId::ValueType>(values[1])})
|
||||
return Database::ReleaseId {*value};
|
||||
if (const auto value{ StringUtils::readAs<Database::ReleaseId::ValueType>(values[1]) })
|
||||
return Database::ReleaseId{ *value };
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
template<>
|
||||
std::optional<API::Subsonic::RootId>
|
||||
readAs(std::string_view str)
|
||||
{
|
||||
if (str == "root")
|
||||
return API::Subsonic::RootId {};
|
||||
template<>
|
||||
std::optional<API::Subsonic::RootId> readAs(std::string_view str)
|
||||
{
|
||||
if (str == "root")
|
||||
return API::Subsonic::RootId{};
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
template<>
|
||||
std::optional<Database::TrackId>
|
||||
readAs(std::string_view str)
|
||||
{
|
||||
std::vector<std::string_view> values {StringUtils::splitString(str, "-")};
|
||||
if (values.size() != 2)
|
||||
return std::nullopt;
|
||||
template<>
|
||||
std::optional<Database::TrackId> readAs(std::string_view str)
|
||||
{
|
||||
std::vector<std::string_view> values{ StringUtils::splitString(str, "-") };
|
||||
if (values.size() != 2)
|
||||
return std::nullopt;
|
||||
|
||||
if (values[0] != "tr")
|
||||
return std::nullopt;
|
||||
if (values[0] != "tr")
|
||||
return std::nullopt;
|
||||
|
||||
if (const auto value {StringUtils::readAs<Database::TrackId::ValueType>(values[1])})
|
||||
return Database::TrackId {*value};
|
||||
if (const auto value{ StringUtils::readAs<Database::TrackId::ValueType>(values[1]) })
|
||||
return Database::TrackId{ *value };
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
template<>
|
||||
std::optional<Database::TrackListId>
|
||||
readAs(std::string_view str)
|
||||
{
|
||||
std::vector<std::string_view> values {StringUtils::splitString(str, "-")};
|
||||
if (values.size() != 2)
|
||||
return std::nullopt;
|
||||
template<>
|
||||
std::optional<Database::TrackListId> readAs(std::string_view str)
|
||||
{
|
||||
std::vector<std::string_view> values{ StringUtils::splitString(str, "-") };
|
||||
if (values.size() != 2)
|
||||
return std::nullopt;
|
||||
|
||||
if (values[0] != "pl")
|
||||
return std::nullopt;
|
||||
if (values[0] != "pl")
|
||||
return std::nullopt;
|
||||
|
||||
if (const auto value {StringUtils::readAs<Database::TrackListId::ValueType>(values[1])})
|
||||
return Database::TrackListId {*value};
|
||||
if (const auto value{ StringUtils::readAs<Database::TrackListId::ValueType>(values[1]) })
|
||||
return Database::TrackListId{ *value };
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,36 +27,31 @@
|
||||
|
||||
namespace API::Subsonic
|
||||
{
|
||||
struct RootId {};
|
||||
struct RootId {};
|
||||
|
||||
std::string idToString(Database::ArtistId id);
|
||||
std::string idToString(Database::ReleaseId id);
|
||||
std::string idToString(Database::TrackId id);
|
||||
std::string idToString(Database::TrackListId id);
|
||||
std::string idToString(RootId);
|
||||
std::string idToString(Database::ArtistId id);
|
||||
std::string idToString(Database::ReleaseId id);
|
||||
std::string idToString(Database::TrackId id);
|
||||
std::string idToString(Database::TrackListId id);
|
||||
std::string idToString(RootId);
|
||||
} // namespace API::Subsonic
|
||||
|
||||
// Used to parse parameters
|
||||
namespace StringUtils
|
||||
{
|
||||
template<>
|
||||
std::optional<API::Subsonic::RootId>
|
||||
readAs(std::string_view str);
|
||||
template<>
|
||||
std::optional<API::Subsonic::RootId> readAs(std::string_view str);
|
||||
|
||||
template<>
|
||||
std::optional<Database::ArtistId>
|
||||
readAs(std::string_view str);
|
||||
template<>
|
||||
std::optional<Database::ArtistId> readAs(std::string_view str);
|
||||
|
||||
template<>
|
||||
std::optional<Database::ReleaseId>
|
||||
readAs(std::string_view str);
|
||||
template<>
|
||||
std::optional<Database::ReleaseId> readAs(std::string_view str);
|
||||
|
||||
template<>
|
||||
std::optional<Database::TrackId>
|
||||
readAs(std::string_view str);
|
||||
template<>
|
||||
std::optional<Database::TrackId> readAs(std::string_view str);
|
||||
|
||||
template<>
|
||||
std::optional<Database::TrackListId>
|
||||
readAs(std::string_view str);
|
||||
template<>
|
||||
std::optional<Database::TrackListId> readAs(std::string_view str);
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -30,28 +30,28 @@
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Db;
|
||||
class Db;
|
||||
}
|
||||
|
||||
namespace API::Subsonic
|
||||
{
|
||||
|
||||
class SubsonicResource final : public Wt::WResource
|
||||
{
|
||||
public:
|
||||
SubsonicResource(Database::Db& db);
|
||||
class SubsonicResource final : public Wt::WResource
|
||||
{
|
||||
public:
|
||||
SubsonicResource(Database::Db& db);
|
||||
|
||||
private:
|
||||
void handleRequest(const Wt::Http::Request &request, Wt::Http::Response &response) override;
|
||||
ProtocolVersion getServerProtocolVersion(const std::string& clientName) const;
|
||||
private:
|
||||
void handleRequest(const Wt::Http::Request &request, Wt::Http::Response &response) override;
|
||||
ProtocolVersion getServerProtocolVersion(const std::string& clientName) const;
|
||||
|
||||
static void checkProtocolVersion(ProtocolVersion client, ProtocolVersion server);
|
||||
ClientInfo getClientInfo(const Wt::Http::ParameterMap& parameters);
|
||||
RequestContext buildRequestContext(const Wt::Http::Request& request);
|
||||
Database::UserId authenticateUser(const Wt::Http::Request& request, const ClientInfo& clientInfo);
|
||||
static void checkProtocolVersion(ProtocolVersion client, ProtocolVersion server);
|
||||
ClientInfo getClientInfo(const Wt::Http::ParameterMap& parameters);
|
||||
RequestContext buildRequestContext(const Wt::Http::Request& request);
|
||||
Database::UserId authenticateUser(const Wt::Http::Request& request, const ClientInfo& clientInfo);
|
||||
|
||||
const std::unordered_map<std::string, ProtocolVersion> _serverProtocolVersionsByClient;
|
||||
Database::Db& _db;
|
||||
};
|
||||
const std::unordered_map<std::string, ProtocolVersion> _serverProtocolVersionsByClient;
|
||||
Database::Db& _db;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -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,250 +34,270 @@
|
||||
|
||||
namespace API::Subsonic
|
||||
{
|
||||
std::string_view ResponseFormatToMimeType(ResponseFormat format)
|
||||
{
|
||||
switch (format)
|
||||
{
|
||||
case ResponseFormat::xml: return "text/xml";
|
||||
case ResponseFormat::json: return "application/json";
|
||||
}
|
||||
|
||||
std::string
|
||||
ResponseFormatToMimeType(ResponseFormat format)
|
||||
{
|
||||
switch (format)
|
||||
{
|
||||
case ResponseFormat::xml: return "text/xml";
|
||||
case ResponseFormat::json: return "application/json";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
void Response::Node::setValue(std::string_view value)
|
||||
{
|
||||
assert(_children.empty() && _childrenArrays.empty() && _childrenValues.empty());
|
||||
_value = std::string{ value };
|
||||
}
|
||||
|
||||
void
|
||||
Response::Node::setValue(std::string_view value)
|
||||
{
|
||||
if (!_children.empty() || !_childrenArrays.empty())
|
||||
throw LmsException {"Node already has children"};
|
||||
void Response::Node::setValue(long long value)
|
||||
{
|
||||
assert(_children.empty() && _childrenArrays.empty() && _childrenValues.empty());
|
||||
_value = value;
|
||||
}
|
||||
|
||||
_value = std::string {value};
|
||||
}
|
||||
void Response::Node::setAttribute(std::string_view key, std::string_view value)
|
||||
{
|
||||
_attributes[std::string{ key }] = std::string{ value };
|
||||
}
|
||||
|
||||
void
|
||||
Response::Node::setValue(long long value)
|
||||
{
|
||||
if (!_children.empty() || !_childrenArrays.empty())
|
||||
throw LmsException {"Node already has children"};
|
||||
void Response::Node::addChild(const std::string& key, Node node)
|
||||
{
|
||||
assert(!_value);
|
||||
_children[key].emplace_back(std::move(node));
|
||||
}
|
||||
|
||||
_value = value;
|
||||
}
|
||||
void Response::Node::createEmptyArrayChild(std::string_view key)
|
||||
{
|
||||
assert(!_value);
|
||||
_childrenArrays.emplace(key, std::vector<Node>{});
|
||||
}
|
||||
|
||||
void
|
||||
Response::Node::setAttribute(std::string_view key, std::string_view value)
|
||||
{
|
||||
_attributes[std::string {key}] = std::string {value};
|
||||
}
|
||||
void Response::Node::addArrayChild(std::string_view key, Node node)
|
||||
{
|
||||
assert(!_value);
|
||||
_childrenArrays[std::string{ key }].emplace_back(std::move(node));
|
||||
}
|
||||
|
||||
void
|
||||
Response::Node::addChild(const std::string& key, Node node)
|
||||
{
|
||||
if (_value)
|
||||
throw LmsException {"Node already has a value"};
|
||||
void Response::Node::createEmptyArrayValue(std::string_view key)
|
||||
{
|
||||
assert (!_value);
|
||||
_childrenValues.emplace(key, ValuesType{});
|
||||
}
|
||||
|
||||
_children[key].emplace_back(std::move(node));
|
||||
}
|
||||
void Response::Node::addArrayValue(std::string_view key, std::string_view value)
|
||||
{
|
||||
assert(!_value);
|
||||
auto& values{ _childrenValues[std::string{ key }] };
|
||||
values.push_back(std::string{ value });
|
||||
assert(std::all_of(std::cbegin(values) + 1, std::cend(values), [&](const ValueType& value) {return value.index() == values.front().index();}));
|
||||
}
|
||||
|
||||
void
|
||||
Response::Node::addArrayChild(const std::string& key, Node node)
|
||||
{
|
||||
if (_value)
|
||||
throw LmsException {"Node already has a value"};
|
||||
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();}));
|
||||
}
|
||||
|
||||
_childrenArrays[key].emplace_back(std::move(node));
|
||||
}
|
||||
Response::Node& Response::Node::createChild(const std::string& key)
|
||||
{
|
||||
_children[key].emplace_back();
|
||||
return _children[key].back();
|
||||
}
|
||||
|
||||
Response::Node& Response::Node::createArrayChild(const std::string& key)
|
||||
{
|
||||
_childrenArrays[key].emplace_back();
|
||||
return _childrenArrays[key].back();
|
||||
}
|
||||
|
||||
Response::Node&
|
||||
Response::Node::createChild(const std::string& key)
|
||||
{
|
||||
_children[key].emplace_back();
|
||||
return _children[key].back();
|
||||
}
|
||||
void Response::Node::setVersionAttribute(ProtocolVersion protocolVersion)
|
||||
{
|
||||
setAttribute("version", std::to_string(protocolVersion.major) + "." + std::to_string(protocolVersion.minor) + "." + std::to_string(protocolVersion.patch));
|
||||
}
|
||||
|
||||
Response::Node&
|
||||
Response::Node::createArrayChild(const std::string& key)
|
||||
{
|
||||
_childrenArrays[key].emplace_back();
|
||||
return _childrenArrays[key].back();
|
||||
}
|
||||
Response Response::createOkResponse(ProtocolVersion protocolVersion)
|
||||
{
|
||||
Response response;
|
||||
Node& responseNode{ response._root.createChild("subsonic-response") };
|
||||
|
||||
void
|
||||
Response::Node::setVersionAttribute(ProtocolVersion protocolVersion)
|
||||
{
|
||||
setAttribute("version", std::to_string(protocolVersion.major) + "." + std::to_string(protocolVersion.minor) + "." + std::to_string(protocolVersion.patch));
|
||||
}
|
||||
responseNode.setAttribute("status", "ok");
|
||||
responseNode.setVersionAttribute(protocolVersion);
|
||||
|
||||
Response
|
||||
Response::createOkResponse(ProtocolVersion protocolVersion)
|
||||
{
|
||||
Response response;
|
||||
Node& responseNode {response._root.createChild("subsonic-response")};
|
||||
// OpenSubsonic mandatory fields
|
||||
responseNode.setAttribute("type", "lms");
|
||||
responseNode.setAttribute("serverVersion", serverVersion);
|
||||
responseNode.setAttribute("openSubsonic", true);
|
||||
|
||||
responseNode.setAttribute("status", "ok");
|
||||
responseNode.setVersionAttribute(protocolVersion);
|
||||
responseNode.setAttribute("type", "lms"); // non standard field to ease client hacks
|
||||
return response;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
Response Response::createFailedResponse(ProtocolVersion protocolVersion, const Error& error)
|
||||
{
|
||||
Response response;
|
||||
Node& responseNode{ response._root.createChild("subsonic-response") };
|
||||
|
||||
Response
|
||||
Response::createFailedResponse(ProtocolVersion protocolVersion, const Error& error)
|
||||
{
|
||||
Response response;
|
||||
Node& responseNode {response._root.createChild("subsonic-response")};
|
||||
responseNode.setAttribute("status", "failed");
|
||||
responseNode.setVersionAttribute(protocolVersion);
|
||||
responseNode.setAttribute("type", "lms"); // non standard field to ease client hacks
|
||||
|
||||
responseNode.setAttribute("status", "failed");
|
||||
responseNode.setVersionAttribute(protocolVersion);
|
||||
responseNode.setAttribute("type", "lms"); // non standard field to ease client hacks
|
||||
Node& errorNode{ responseNode.createChild("error") };
|
||||
errorNode.setAttribute("code", static_cast<int>(error.getCode()));
|
||||
errorNode.setAttribute("message", error.getMessage());
|
||||
|
||||
Node& errorNode {responseNode.createChild("error")};
|
||||
errorNode.setAttribute("code", static_cast<int>(error.getCode()));
|
||||
errorNode.setAttribute("message", error.getMessage());
|
||||
return response;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
void Response::addNode(const std::string& key, Node node)
|
||||
{
|
||||
return _root._children["subsonic-response"].front().addChild(key, std::move(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)
|
||||
{
|
||||
return _root._children["subsonic-response"].front().createChild(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)
|
||||
{
|
||||
return _root._children["subsonic-response"].front().createArrayChild(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)
|
||||
{
|
||||
switch (format)
|
||||
{
|
||||
case ResponseFormat::xml:
|
||||
writeXML(os);
|
||||
break;
|
||||
case ResponseFormat::json:
|
||||
writeJSON(os);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
Response::write(std::ostream& os, ResponseFormat format)
|
||||
{
|
||||
switch (format)
|
||||
{
|
||||
case ResponseFormat::xml:
|
||||
writeXML(os);
|
||||
break;
|
||||
case ResponseFormat::json:
|
||||
writeJSON(os);
|
||||
break;
|
||||
}
|
||||
}
|
||||
void Response::writeXML(std::ostream& os)
|
||||
{
|
||||
std::function<boost::property_tree::ptree(const Node&)> nodeToPropertyTree = [&](const Node& node)
|
||||
{
|
||||
boost::property_tree::ptree res;
|
||||
|
||||
void
|
||||
Response::writeXML(std::ostream& os)
|
||||
{
|
||||
std::function<boost::property_tree::ptree(const Response::Node&)> nodeToPropertyTree = [&] (const Response::Node& node)
|
||||
{
|
||||
boost::property_tree::ptree res;
|
||||
for (auto itAttribute : node._attributes)
|
||||
{
|
||||
if (std::holds_alternative<std::string>(itAttribute.second))
|
||||
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));
|
||||
}
|
||||
|
||||
for (auto itAttribute : node._attributes)
|
||||
{
|
||||
if (std::holds_alternative<std::string>(itAttribute.second))
|
||||
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<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);
|
||||
|
||||
if (node._value)
|
||||
{
|
||||
const auto& value {*node._value};
|
||||
return res;
|
||||
};
|
||||
|
||||
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));
|
||||
}
|
||||
else
|
||||
{
|
||||
for (auto itChildNode : node._children)
|
||||
{
|
||||
for (const Response::Node& childNode : itChildNode.second)
|
||||
res.add_child(itChildNode.first, nodeToPropertyTree(childNode));
|
||||
}
|
||||
if (node._value)
|
||||
{
|
||||
res = valueToPropertyTree(*node._value);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (const auto& [key, childNodes] : node._children)
|
||||
{
|
||||
for (const Node& childNode : childNodes)
|
||||
res.add_child(key, nodeToPropertyTree(childNode));
|
||||
}
|
||||
|
||||
for (auto itChildArrayNode : node._childrenArrays)
|
||||
{
|
||||
const std::vector<Response::Node>& childArrayNodes {itChildArrayNode.second};
|
||||
for (const auto& [key, childArrayNodes] : node._childrenArrays)
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
return res;
|
||||
};
|
||||
|
||||
boost::property_tree::ptree root {nodeToPropertyTree(_root)};
|
||||
boost::property_tree::write_xml(os, root);
|
||||
}
|
||||
boost::property_tree::ptree root{ nodeToPropertyTree(_root) };
|
||||
boost::property_tree::write_xml(os, root);
|
||||
}
|
||||
|
||||
void
|
||||
Response::writeJSON(std::ostream& os)
|
||||
{
|
||||
namespace Json = Wt::Json;
|
||||
void Response::writeJSON(std::ostream& os)
|
||||
{
|
||||
namespace Json = Wt::Json;
|
||||
|
||||
std::function<Json::Object(const Response::Node&)> nodeToJsonObject = [&] (const Response::Node& node)
|
||||
{
|
||||
Json::Object res;
|
||||
std::function<Json::Object(const Response::Node&)> nodeToJsonObject = [&](const Response::Node& node)
|
||||
{
|
||||
Json::Object res;
|
||||
|
||||
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)};
|
||||
auto valueToJsonValue{ [](const Node::ValueType& value) -> Json::Value
|
||||
{
|
||||
Json::Value res;
|
||||
std::visit([&](const auto& rawValue)
|
||||
{
|
||||
res = Json::Value{ rawValue };
|
||||
}, value);
|
||||
return res;
|
||||
} };
|
||||
|
||||
throw LmsException("Unexpected value type");
|
||||
}};
|
||||
for (auto itAttribute : node._attributes)
|
||||
res[itAttribute.first] = valueToJsonValue(itAttribute.second);
|
||||
|
||||
for (auto itAttribute : node._attributes)
|
||||
res[itAttribute.first] = valueToJsonValue(itAttribute.second);
|
||||
if (node._value)
|
||||
{
|
||||
res["value"] = valueToJsonValue(*node._value);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (const auto& [key, childNodes] : node._children)
|
||||
{
|
||||
for (const Response::Node& childNode : childNodes)
|
||||
res[key] = nodeToJsonObject(childNode);
|
||||
}
|
||||
|
||||
if (node._value)
|
||||
{
|
||||
res["value"] = valueToJsonValue(*node._value);
|
||||
}
|
||||
else
|
||||
{
|
||||
for (auto itChildNode : node._children)
|
||||
{
|
||||
for (const Response::Node& childNode : itChildNode.second)
|
||||
res[itChildNode.first] = nodeToJsonObject(childNode);
|
||||
}
|
||||
for (const auto& [key, childArrayNodes] : node._childrenArrays)
|
||||
{
|
||||
Json::Array array;
|
||||
for (const Response::Node& childNode : childArrayNodes)
|
||||
array.emplace_back(nodeToJsonObject(childNode));
|
||||
|
||||
for (auto itChildArrayNode : node._childrenArrays)
|
||||
{
|
||||
const std::vector<Response::Node>& childArrayNodes {itChildArrayNode .second};
|
||||
res[key] = std::move(array);
|
||||
}
|
||||
|
||||
Json::Array array;
|
||||
for (const Response::Node& childNode : childArrayNodes )
|
||||
array.emplace_back(nodeToJsonObject(childNode));
|
||||
for (const auto& [key, childValues] : node._childrenValues)
|
||||
{
|
||||
Json::Array array;
|
||||
for (const Node::ValueType& childValue : childValues)
|
||||
array.emplace_back(valueToJsonValue(childValue));
|
||||
|
||||
res[itChildArrayNode.first] = std::move(array);
|
||||
}
|
||||
}
|
||||
res[key] = std::move(array);
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
return res;
|
||||
};
|
||||
|
||||
Json::Object root {nodeToJsonObject(_root)};
|
||||
os << Json::serialize(root);
|
||||
}
|
||||
Json::Object root{ nodeToJsonObject(_root) };
|
||||
os << Json::serialize(root);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
@@ -30,220 +30,231 @@
|
||||
namespace API::Subsonic
|
||||
{
|
||||
|
||||
enum class ResponseFormat
|
||||
{
|
||||
xml,
|
||||
json,
|
||||
};
|
||||
enum class ResponseFormat
|
||||
{
|
||||
xml,
|
||||
json,
|
||||
};
|
||||
|
||||
std::string ResponseFormatToMimeType(ResponseFormat format);
|
||||
std::string_view ResponseFormatToMimeType(ResponseFormat format);
|
||||
|
||||
class Error
|
||||
{
|
||||
public:
|
||||
enum class Code
|
||||
{
|
||||
Generic = 0,
|
||||
RequiredParameterMissing = 10,
|
||||
ClientMustUpgrade = 20,
|
||||
ServerMustUpgrade = 30,
|
||||
WrongUsernameOrPassword = 40,
|
||||
TokenAuthenticationNotSupportedForLDAPUsers = 41,
|
||||
UserNotAuthorized = 50,
|
||||
RequestedDataNotFound = 70,
|
||||
};
|
||||
class Error
|
||||
{
|
||||
public:
|
||||
enum class Code
|
||||
{
|
||||
Generic = 0,
|
||||
RequiredParameterMissing = 10,
|
||||
ClientMustUpgrade = 20,
|
||||
ServerMustUpgrade = 30,
|
||||
WrongUsernameOrPassword = 40,
|
||||
TokenAuthenticationNotSupportedForLDAPUsers = 41,
|
||||
UserNotAuthorized = 50,
|
||||
RequestedDataNotFound = 70,
|
||||
};
|
||||
|
||||
Error(Code code) : _code {code} {}
|
||||
Error(Code code) : _code{ code } {}
|
||||
|
||||
virtual std::string getMessage() const = 0;
|
||||
virtual std::string getMessage() const = 0;
|
||||
|
||||
Code getCode() const { return _code; }
|
||||
Code getCode() const { return _code; }
|
||||
|
||||
private:
|
||||
const Code _code;
|
||||
};
|
||||
private:
|
||||
const Code _code;
|
||||
};
|
||||
|
||||
class GenericError : public Error
|
||||
{
|
||||
public:
|
||||
GenericError() : Error {Code::Generic} {}
|
||||
};
|
||||
class GenericError : public Error
|
||||
{
|
||||
public:
|
||||
GenericError() : Error{ Code::Generic } {}
|
||||
};
|
||||
|
||||
class RequiredParameterMissingError : public Error
|
||||
{
|
||||
public:
|
||||
RequiredParameterMissingError(std::string_view param)
|
||||
: Error {Code::RequiredParameterMissing}
|
||||
, _param {param}
|
||||
{}
|
||||
class RequiredParameterMissingError : public Error
|
||||
{
|
||||
public:
|
||||
RequiredParameterMissingError(std::string_view param)
|
||||
: Error{ Code::RequiredParameterMissing }
|
||||
, _param{ param }
|
||||
{}
|
||||
|
||||
private:
|
||||
std::string getMessage() const override { return "Required parameter '" + _param + "' is missing."; }
|
||||
std::string _param;
|
||||
};
|
||||
private:
|
||||
std::string getMessage() const override { return "Required parameter '" + _param + "' is missing."; }
|
||||
std::string _param;
|
||||
};
|
||||
|
||||
class ClientMustUpgradeError : public Error
|
||||
{
|
||||
public:
|
||||
ClientMustUpgradeError() : Error {Code::ClientMustUpgrade} {}
|
||||
private:
|
||||
std::string getMessage() const override { return "Incompatible Subsonic REST protocol version. Client must upgrade."; }
|
||||
};
|
||||
class ClientMustUpgradeError : public Error
|
||||
{
|
||||
public:
|
||||
ClientMustUpgradeError() : Error{ Code::ClientMustUpgrade } {}
|
||||
private:
|
||||
std::string getMessage() const override { return "Incompatible Subsonic REST protocol version. Client must upgrade."; }
|
||||
};
|
||||
|
||||
class ServerMustUpgradeError : public Error
|
||||
{
|
||||
public:
|
||||
ServerMustUpgradeError() : Error {Code::ServerMustUpgrade} {}
|
||||
private:
|
||||
std::string getMessage() const override { return "Incompatible Subsonic REST protocol version. Server must upgrade."; }
|
||||
};
|
||||
class ServerMustUpgradeError : public Error
|
||||
{
|
||||
public:
|
||||
ServerMustUpgradeError() : Error{ Code::ServerMustUpgrade } {}
|
||||
private:
|
||||
std::string getMessage() const override { return "Incompatible Subsonic REST protocol version. Server must upgrade."; }
|
||||
};
|
||||
|
||||
class WrongUsernameOrPasswordError : public Error
|
||||
{
|
||||
public:
|
||||
WrongUsernameOrPasswordError() : Error {Code::WrongUsernameOrPassword} {}
|
||||
private:
|
||||
std::string getMessage() const override { return "Wrong username or password."; }
|
||||
};
|
||||
class WrongUsernameOrPasswordError : public Error
|
||||
{
|
||||
public:
|
||||
WrongUsernameOrPasswordError() : Error{ Code::WrongUsernameOrPassword } {}
|
||||
private:
|
||||
std::string getMessage() const override { return "Wrong username or password."; }
|
||||
};
|
||||
|
||||
class TokenAuthenticationNotSupportedForLDAPUsersError : public Error
|
||||
{
|
||||
public:
|
||||
TokenAuthenticationNotSupportedForLDAPUsersError() : Error {Code::TokenAuthenticationNotSupportedForLDAPUsers} {}
|
||||
private:
|
||||
std::string getMessage() const override { return "Token authentication not supported for LDAP users."; }
|
||||
};
|
||||
class TokenAuthenticationNotSupportedForLDAPUsersError : public Error
|
||||
{
|
||||
public:
|
||||
TokenAuthenticationNotSupportedForLDAPUsersError() : Error{ Code::TokenAuthenticationNotSupportedForLDAPUsers } {}
|
||||
private:
|
||||
std::string getMessage() const override { return "Token authentication not supported for LDAP users."; }
|
||||
};
|
||||
|
||||
class UserNotAuthorizedError : public Error
|
||||
{
|
||||
public:
|
||||
UserNotAuthorizedError () : Error {Code::UserNotAuthorized} {}
|
||||
private:
|
||||
std::string getMessage() const override { return "User is not authorized for the given operation."; }
|
||||
};
|
||||
class UserNotAuthorizedError : public Error
|
||||
{
|
||||
public:
|
||||
UserNotAuthorizedError() : Error{ Code::UserNotAuthorized } {}
|
||||
private:
|
||||
std::string getMessage() const override { return "User is not authorized for the given operation."; }
|
||||
};
|
||||
|
||||
class RequestedDataNotFoundError : public Error
|
||||
{
|
||||
public:
|
||||
RequestedDataNotFoundError() : Error {Code::RequestedDataNotFound} {}
|
||||
private:
|
||||
std::string getMessage() const override { return "The requested data was not found."; }
|
||||
};
|
||||
class RequestedDataNotFoundError : public Error
|
||||
{
|
||||
public:
|
||||
RequestedDataNotFoundError() : Error{ Code::RequestedDataNotFound } {}
|
||||
private:
|
||||
std::string getMessage() const override { return "The requested data was not found."; }
|
||||
};
|
||||
|
||||
class InternalErrorGenericError : public GenericError
|
||||
{
|
||||
public:
|
||||
InternalErrorGenericError(const std::string& message) : _message {message} {}
|
||||
private:
|
||||
std::string getMessage() const override { return "Internal error: " + _message; }
|
||||
const std::string _message;
|
||||
};
|
||||
class InternalErrorGenericError : public GenericError
|
||||
{
|
||||
public:
|
||||
InternalErrorGenericError(const std::string& message) : _message{ message } {}
|
||||
private:
|
||||
std::string getMessage() const override { return "Internal error: " + _message; }
|
||||
const std::string _message;
|
||||
};
|
||||
|
||||
class LoginThrottledGenericError : public GenericError
|
||||
{
|
||||
std::string getMessage() const override { return "Login throttled, too many attempts"; }
|
||||
};
|
||||
class LoginThrottledGenericError : public GenericError
|
||||
{
|
||||
std::string getMessage() const override { return "Login throttled, too many attempts"; }
|
||||
};
|
||||
|
||||
class NotImplementedGenericError : public GenericError
|
||||
{
|
||||
std::string getMessage() const override { return "Not implemented"; }
|
||||
};
|
||||
class NotImplementedGenericError : public GenericError
|
||||
{
|
||||
std::string getMessage() const override { return "Not implemented"; }
|
||||
};
|
||||
|
||||
class UnknownEntryPointGenericError : public GenericError
|
||||
{
|
||||
std::string getMessage() const override { return "Unknown API method"; }
|
||||
};
|
||||
class UnknownEntryPointGenericError : public GenericError
|
||||
{
|
||||
std::string getMessage() const override { return "Unknown API method"; }
|
||||
};
|
||||
|
||||
class PasswordTooWeakGenericError : public GenericError
|
||||
{
|
||||
std::string getMessage() const override { return "Password too weak"; }
|
||||
};
|
||||
class PasswordTooWeakGenericError : public GenericError
|
||||
{
|
||||
std::string getMessage() const override { return "Password too weak"; }
|
||||
};
|
||||
|
||||
class PasswordMustMatchLoginNameGenericError : public GenericError
|
||||
{
|
||||
std::string getMessage() const override { return "Password must match login name"; }
|
||||
};
|
||||
class PasswordMustMatchLoginNameGenericError : public GenericError
|
||||
{
|
||||
std::string getMessage() const override { return "Password must match login name"; }
|
||||
};
|
||||
|
||||
class DemoUserCannotChangePasswordGenericError : public GenericError
|
||||
{
|
||||
std::string getMessage() const override { return "Demo user cannot change its password"; }
|
||||
};
|
||||
class DemoUserCannotChangePasswordGenericError : public GenericError
|
||||
{
|
||||
std::string getMessage() const override { return "Demo user cannot change its password"; }
|
||||
};
|
||||
|
||||
class UserAlreadyExistsGenericError : public GenericError
|
||||
{
|
||||
std::string getMessage() const override { return "User already exists"; }
|
||||
};
|
||||
class UserAlreadyExistsGenericError : public GenericError
|
||||
{
|
||||
std::string getMessage() const override { return "User already exists"; }
|
||||
};
|
||||
|
||||
class BadParameterGenericError : public GenericError
|
||||
{
|
||||
public:
|
||||
BadParameterGenericError(const std::string& parameterName) : _parameterName {parameterName} {}
|
||||
class BadParameterGenericError : public GenericError
|
||||
{
|
||||
public:
|
||||
BadParameterGenericError(const std::string& parameterName) : _parameterName{ parameterName } {}
|
||||
|
||||
private:
|
||||
std::string getMessage() const override { return "Parameter '" + _parameterName + "': bad value"; }
|
||||
private:
|
||||
std::string getMessage() const override { return "Parameter '" + _parameterName + "': bad value"; }
|
||||
|
||||
const std::string _parameterName;
|
||||
};
|
||||
const std::string _parameterName;
|
||||
};
|
||||
|
||||
class Response
|
||||
{
|
||||
public:
|
||||
class Node
|
||||
{
|
||||
public:
|
||||
void setAttribute(std::string_view key, std::string_view value);
|
||||
class Response
|
||||
{
|
||||
public:
|
||||
class Node
|
||||
{
|
||||
public:
|
||||
void setAttribute(std::string_view key, std::string_view value);
|
||||
|
||||
template <typename T, std::enable_if_t<std::is_arithmetic<T>::value>* = nullptr>
|
||||
void setAttribute(std::string_view key, T value)
|
||||
{
|
||||
if constexpr (std::is_same<bool, T>::value)
|
||||
_attributes[std::string {key}] = value;
|
||||
else
|
||||
_attributes[std::string {key}] = static_cast<long long>(value);
|
||||
}
|
||||
template <typename T, std::enable_if_t<std::is_arithmetic<T>::value>* = nullptr>
|
||||
void setAttribute(std::string_view key, T value)
|
||||
{
|
||||
if constexpr (std::is_same<bool, T>::value)
|
||||
_attributes[std::string{ key }] = value;
|
||||
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
|
||||
void setValue(std::string_view value);
|
||||
void setValue(long long value);
|
||||
Node& createChild(const std::string& key);
|
||||
Node& createArrayChild(const std::string& key);
|
||||
// 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 addChild(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);
|
||||
private:
|
||||
void setVersionAttribute(ProtocolVersion version);
|
||||
|
||||
friend class Response;
|
||||
using ValueType = std::variant<std::string, bool, 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;
|
||||
};
|
||||
friend class Response;
|
||||
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;
|
||||
|
||||
static Response createOkResponse(ProtocolVersion protocolVersion);
|
||||
static Response createFailedResponse(ProtocolVersion protocolVersion, const Error& error);
|
||||
using ValuesType = std::vector<ValueType>;
|
||||
std::map<std::string, ValuesType> _childrenValues;
|
||||
};
|
||||
|
||||
virtual ~Response() {}
|
||||
Response(const Response&) = delete;
|
||||
Response& operator=(const Response&) = delete;
|
||||
Response(Response&&) = default;
|
||||
Response& operator=(Response&&) = default;
|
||||
static Response createOkResponse(ProtocolVersion protocolVersion);
|
||||
static Response createFailedResponse(ProtocolVersion protocolVersion, const Error& error);
|
||||
|
||||
void addNode(const std::string& key, Node node);
|
||||
Node& createNode(const std::string& key);
|
||||
Node& createArrayNode(const std::string& key);
|
||||
virtual ~Response() {}
|
||||
Response(const Response&) = delete;
|
||||
Response& operator=(const Response&) = delete;
|
||||
Response(Response&&) = default;
|
||||
Response& operator=(Response&&) = default;
|
||||
|
||||
void write(std::ostream& os, ResponseFormat format);
|
||||
void addNode(const std::string& key, Node node);
|
||||
Node& createNode(const std::string& key);
|
||||
Node& createArrayNode(const std::string& key);
|
||||
|
||||
private:
|
||||
void writeJSON(std::ostream& os);
|
||||
void writeXML(std::ostream& os);
|
||||
void write(std::ostream& os, ResponseFormat format);
|
||||
|
||||
Response() = default;
|
||||
Node _root;
|
||||
};
|
||||
private:
|
||||
void writeJSON(std::ostream& os);
|
||||
void writeXML(std::ostream& os);
|
||||
|
||||
Response() = default;
|
||||
Node _root;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "Utils.hpp"
|
||||
|
||||
#include "utils/Service.hpp"
|
||||
#include "utils/String.hpp"
|
||||
#include "services/auth/IPasswordService.hpp"
|
||||
#include "SubsonicResponse.hpp"
|
||||
|
||||
namespace API::Subsonic::Utils
|
||||
{
|
||||
void checkSetPasswordImplemented()
|
||||
{
|
||||
Auth::IPasswordService* passwordService{ Service<Auth::IPasswordService>::get() };
|
||||
if (!passwordService || !passwordService->canSetPasswords())
|
||||
throw NotImplementedGenericError{};
|
||||
}
|
||||
|
||||
std::string makeNameFilesystemCompatible(const std::string& name)
|
||||
{
|
||||
return StringUtils::replaceInString(name, "/", "_");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace API::Subsonic::Utils
|
||||
{
|
||||
void checkSetPasswordImplemented();
|
||||
std::string makeNameFilesystemCompatible(const std::string& name);
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "AlbumSongLists.hpp"
|
||||
|
||||
#include "services/database/Artist.hpp"
|
||||
#include "services/database/Cluster.hpp"
|
||||
#include "services/database/Release.hpp"
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/Track.hpp"
|
||||
#include "services/database/User.hpp"
|
||||
#include "services/scrobbling/IScrobblingService.hpp"
|
||||
#include "responses/Album.hpp"
|
||||
#include "responses/Artist.hpp"
|
||||
#include "responses/Song.hpp"
|
||||
#include "utils/Service.hpp"
|
||||
#include "ParameterParsing.hpp"
|
||||
|
||||
namespace API::Subsonic
|
||||
{
|
||||
using namespace Database;
|
||||
|
||||
namespace {
|
||||
Response handleGetAlbumListRequestCommon(const RequestContext& context, bool id3)
|
||||
{
|
||||
// Mandatory params
|
||||
const std::string type{ getMandatoryParameterAs<std::string>(context.parameters, "type") };
|
||||
|
||||
// Optional params
|
||||
const std::size_t size{ getParameterAs<std::size_t>(context.parameters, "size").value_or(10) };
|
||||
const std::size_t offset{ getParameterAs<std::size_t>(context.parameters, "offset").value_or(0) };
|
||||
|
||||
const Range range{ offset, size };
|
||||
|
||||
RangeResults<ReleaseId> releases;
|
||||
Scrobbling::IScrobblingService& scrobbling{ *Service<Scrobbling::IScrobblingService>::get() };
|
||||
|
||||
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||
|
||||
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError{};
|
||||
|
||||
if (type == "alphabeticalByName")
|
||||
{
|
||||
Release::FindParameters params;
|
||||
params.setSortMethod(ReleaseSortMethod::Name);
|
||||
params.setRange(range);
|
||||
|
||||
releases = Release::find(context.dbSession, params);
|
||||
}
|
||||
else if (type == "alphabeticalByArtist")
|
||||
{
|
||||
releases = Release::findOrderedByArtist(context.dbSession, range);
|
||||
}
|
||||
else if (type == "byGenre")
|
||||
{
|
||||
// Mandatory param
|
||||
const std::string genre{ getMandatoryParameterAs<std::string>(context.parameters, "genre") };
|
||||
|
||||
if (const ClusterType::pointer clusterType{ ClusterType::find(context.dbSession, "GENRE") })
|
||||
{
|
||||
if (const Cluster::pointer cluster{ clusterType->getCluster(genre) })
|
||||
{
|
||||
Release::FindParameters params;
|
||||
params.setClusters({ cluster->getId() });
|
||||
params.setSortMethod(ReleaseSortMethod::Name);
|
||||
params.setRange(range);
|
||||
|
||||
releases = Release::find(context.dbSession, params);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (type == "byYear")
|
||||
{
|
||||
const int fromYear{ getMandatoryParameterAs<int>(context.parameters, "fromYear") };
|
||||
const int toYear{ getMandatoryParameterAs<int>(context.parameters, "toYear") };
|
||||
|
||||
Release::FindParameters params;
|
||||
params.setSortMethod(ReleaseSortMethod::Date);
|
||||
params.setRange(range);
|
||||
params.setDateRange(DateRange::fromYearRange(fromYear, toYear));
|
||||
|
||||
releases = Release::find(context.dbSession, params);
|
||||
}
|
||||
else if (type == "frequent")
|
||||
{
|
||||
releases = scrobbling.getTopReleases(context.userId, {}, range);
|
||||
}
|
||||
else if (type == "newest")
|
||||
{
|
||||
Release::FindParameters params;
|
||||
params.setSortMethod(ReleaseSortMethod::LastWritten);
|
||||
params.setRange(range);
|
||||
|
||||
releases = Release::find(context.dbSession, params);
|
||||
}
|
||||
else if (type == "random")
|
||||
{
|
||||
// Random results are paginated, but there is no acceptable way to handle the pagination params without repeating some albums
|
||||
// (no seed provided by subsonic, ot it would require to store some kind of context for each user/client when iterating over the random albums)
|
||||
Release::FindParameters params;
|
||||
params.setSortMethod(ReleaseSortMethod::Random);
|
||||
params.setRange({ 0, size });
|
||||
|
||||
releases = Release::find(context.dbSession, params);
|
||||
}
|
||||
else if (type == "recent")
|
||||
{
|
||||
releases = scrobbling.getRecentReleases(context.userId, {}, range);
|
||||
}
|
||||
else if (type == "starred")
|
||||
{
|
||||
releases = scrobbling.getStarredReleases(context.userId, {}, range);
|
||||
}
|
||||
else
|
||||
throw NotImplementedGenericError{};
|
||||
|
||||
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||
Response::Node& albumListNode{ response.createNode(id3 ? "albumList2" : "albumList") };
|
||||
|
||||
for (const ReleaseId releaseId : releases.results)
|
||||
{
|
||||
const Release::pointer release{ Release::find(context.dbSession, releaseId) };
|
||||
albumListNode.addArrayChild("album", createAlbumNode(release, context.dbSession, user, id3));
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
Response handleGetStarredRequestCommon(RequestContext& context, bool id3)
|
||||
{
|
||||
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||
|
||||
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError{};
|
||||
|
||||
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||
Response::Node& starredNode{ response.createNode(id3 ? "starred2" : "starred") };
|
||||
|
||||
Scrobbling::IScrobblingService& scrobbling{ *Service<Scrobbling::IScrobblingService>::get() };
|
||||
|
||||
for (const ArtistId artistId : scrobbling.getStarredArtists(context.userId, {} /* clusters */, std::nullopt /* linkType */, ArtistSortMethod::BySortName, Range{}).results)
|
||||
{
|
||||
if (auto artist{ Artist::find(context.dbSession, artistId) })
|
||||
starredNode.addArrayChild("artist", createArtistNode(artist, context.dbSession, user, id3));
|
||||
}
|
||||
|
||||
for (const ReleaseId releaseId : scrobbling.getStarredReleases(context.userId, {} /* clusters */, Range{}).results)
|
||||
{
|
||||
if (auto release{ Release::find(context.dbSession, releaseId) })
|
||||
starredNode.addArrayChild("album", createAlbumNode(release, context.dbSession, user, id3));
|
||||
}
|
||||
|
||||
for (const TrackId trackId : scrobbling.getStarredTracks(context.userId, {} /* clusters */, Range{}).results)
|
||||
{
|
||||
if (auto track{ Track::find(context.dbSession, trackId) })
|
||||
starredNode.addArrayChild("song", createSongNode(track, context.dbSession, user));
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Response handleGetAlbumListRequest(RequestContext& context)
|
||||
{
|
||||
return handleGetAlbumListRequestCommon(context, false /* no id3 */);
|
||||
}
|
||||
|
||||
Response handleGetAlbumList2Request(RequestContext& context)
|
||||
{
|
||||
return handleGetAlbumListRequestCommon(context, true /* id3 */);
|
||||
}
|
||||
|
||||
Response handleGetRandomSongsRequest(RequestContext& context)
|
||||
{
|
||||
// Optional params
|
||||
std::size_t size{ getParameterAs<std::size_t>(context.parameters, "size").value_or(50) };
|
||||
size = std::min(size, std::size_t{ 500 });
|
||||
|
||||
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||
|
||||
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError{};
|
||||
|
||||
const auto trackIds{ Track::find(context.dbSession, Track::FindParameters {}.setSortMethod(TrackSortMethod::Random).setRange({0, size})) };
|
||||
|
||||
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||
|
||||
Response::Node& randomSongsNode{ response.createNode("randomSongs") };
|
||||
for (const TrackId trackId : trackIds.results)
|
||||
{
|
||||
const Track::pointer track{ Track::find(context.dbSession, trackId) };
|
||||
randomSongsNode.addArrayChild("song", createSongNode(track, context.dbSession, user));
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
Response handleGetSongsByGenreRequest(RequestContext& context)
|
||||
{
|
||||
// Mandatory params
|
||||
std::string genre{ getMandatoryParameterAs<std::string>(context.parameters, "genre") };
|
||||
|
||||
// Optional params
|
||||
std::size_t size{ getParameterAs<std::size_t>(context.parameters, "count").value_or(10) };
|
||||
size = std::min(size, std::size_t{ 500 });
|
||||
|
||||
std::size_t offset{ getParameterAs<std::size_t>(context.parameters, "offset").value_or(0) };
|
||||
|
||||
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||
|
||||
auto clusterType{ ClusterType::find(context.dbSession, "GENRE") };
|
||||
if (!clusterType)
|
||||
throw RequestedDataNotFoundError{};
|
||||
|
||||
auto cluster{ clusterType->getCluster(genre) };
|
||||
if (!cluster)
|
||||
throw RequestedDataNotFoundError{};
|
||||
|
||||
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError{};
|
||||
|
||||
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||
Response::Node& songsByGenreNode{ response.createNode("songsByGenre") };
|
||||
|
||||
Track::FindParameters params;
|
||||
params.setClusters({ cluster->getId() });
|
||||
params.setRange({ offset, size });
|
||||
|
||||
auto trackIds{ Track::find(context.dbSession, params) };
|
||||
for (const TrackId trackId : trackIds.results)
|
||||
{
|
||||
const Track::pointer track{ Track::find(context.dbSession, trackId) };
|
||||
songsByGenreNode.addArrayChild("song", createSongNode(track, context.dbSession, user));
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
Response handleGetStarredRequest(RequestContext& context)
|
||||
{
|
||||
return handleGetStarredRequestCommon(context, false /* no id3 */);
|
||||
}
|
||||
|
||||
Response handleGetStarred2Request(RequestContext& context)
|
||||
{
|
||||
return handleGetStarredRequestCommon(context, true /* id3 */);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "RequestContext.hpp"
|
||||
#include "SubsonicResponse.hpp"
|
||||
|
||||
namespace API::Subsonic
|
||||
{
|
||||
Response handleGetAlbumListRequest(RequestContext& context);
|
||||
Response handleGetAlbumList2Request(RequestContext& context);
|
||||
Response handleGetRandomSongsRequest(RequestContext& context);
|
||||
Response handleGetSongsByGenreRequest(RequestContext& context);
|
||||
Response handleGetStarredRequest(RequestContext& context);
|
||||
Response handleGetStarred2Request(RequestContext& context);
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "Bookmarks.hpp"
|
||||
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/User.hpp"
|
||||
#include "services/database/Track.hpp"
|
||||
#include "services/database/TrackBookmark.hpp"
|
||||
#include "responses/Bookmark.hpp"
|
||||
#include "responses/Song.hpp"
|
||||
#include "ParameterParsing.hpp"
|
||||
#include "SubsonicId.hpp"
|
||||
|
||||
namespace API::Subsonic
|
||||
{
|
||||
using namespace Database;
|
||||
|
||||
Response handleGetBookmarks(RequestContext& context)
|
||||
{
|
||||
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||
|
||||
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError{};
|
||||
|
||||
const auto bookmarkIds{ TrackBookmark::find(context.dbSession, user->getId(), Range {}) };
|
||||
|
||||
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||
Response::Node& bookmarksNode{ response.createNode("bookmarks") };
|
||||
|
||||
for (const TrackBookmarkId bookmarkId : bookmarkIds.results)
|
||||
{
|
||||
const TrackBookmark::pointer bookmark{ TrackBookmark::find(context.dbSession, bookmarkId) };
|
||||
Response::Node bookmarkNode{ createBookmarkNode(bookmark) };
|
||||
bookmarkNode.addArrayChild("entry", createSongNode(bookmark->getTrack(), context.dbSession, user));
|
||||
|
||||
bookmarksNode.addArrayChild("bookmark", std::move(bookmarkNode));
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
Response handleCreateBookmark(RequestContext& context)
|
||||
{
|
||||
// Mandatory params
|
||||
TrackId trackId{ getMandatoryParameterAs<TrackId>(context.parameters, "id") };
|
||||
unsigned long position{ getMandatoryParameterAs<unsigned long>(context.parameters, "position") };
|
||||
const std::optional<std::string> comment{ getParameterAs<std::string>(context.parameters, "comment") };
|
||||
|
||||
auto transaction{ context.dbSession.createUniqueTransaction() };
|
||||
|
||||
const User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError{};
|
||||
|
||||
const Track::pointer track{ Track::find(context.dbSession, trackId) };
|
||||
if (!track)
|
||||
throw RequestedDataNotFoundError{};
|
||||
|
||||
// Replace any existing bookmark
|
||||
auto bookmark{ TrackBookmark::find(context.dbSession, user->getId(), trackId) };
|
||||
if (!bookmark)
|
||||
bookmark = context.dbSession.create<TrackBookmark>(user, track);
|
||||
|
||||
bookmark.modify()->setOffset(std::chrono::milliseconds{ position });
|
||||
if (comment)
|
||||
bookmark.modify()->setComment(*comment);
|
||||
|
||||
return Response::createOkResponse(context.serverProtocolVersion);
|
||||
}
|
||||
|
||||
Response handleDeleteBookmark(RequestContext& context)
|
||||
{
|
||||
// Mandatory params
|
||||
TrackId trackId{ getMandatoryParameterAs<TrackId>(context.parameters, "id") };
|
||||
|
||||
auto transaction{ context.dbSession.createUniqueTransaction() };
|
||||
|
||||
auto bookmark{ TrackBookmark::find(context.dbSession, context.userId, trackId) };
|
||||
if (!bookmark)
|
||||
throw RequestedDataNotFoundError{};
|
||||
|
||||
bookmark.remove();
|
||||
|
||||
return Response::createOkResponse(context.serverProtocolVersion);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "RequestContext.hpp"
|
||||
#include "SubsonicResponse.hpp"
|
||||
|
||||
namespace API::Subsonic
|
||||
{
|
||||
Response handleGetBookmarks(RequestContext& context);
|
||||
Response handleCreateBookmark(RequestContext& context);
|
||||
Response handleDeleteBookmark(RequestContext& context);
|
||||
}
|
||||
@@ -0,0 +1,465 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "Browsing.hpp"
|
||||
|
||||
#include "services/database/Artist.hpp"
|
||||
#include "services/database/Cluster.hpp"
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/Release.hpp"
|
||||
#include "services/database/Track.hpp"
|
||||
#include "services/database/User.hpp"
|
||||
#include "services/recommendation/IRecommendationService.hpp"
|
||||
#include "utils/Random.hpp"
|
||||
#include "utils/Service.hpp"
|
||||
#include "responses/Album.hpp"
|
||||
#include "responses/Artist.hpp"
|
||||
#include "responses/Genre.hpp"
|
||||
#include "responses/Song.hpp"
|
||||
#include "ParameterParsing.hpp"
|
||||
#include "SubsonicId.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace API::Subsonic
|
||||
{
|
||||
using namespace Database;
|
||||
|
||||
static const std::string_view reportedDummyDate{ "2000-01-01T00:00:00" };
|
||||
static const unsigned long long reportedDummyDateULong{ 946684800000ULL }; // 2000-01-01T00:00:00 UTC
|
||||
|
||||
namespace
|
||||
{
|
||||
Response handleGetArtistInfoRequestCommon(RequestContext& context, bool id3)
|
||||
{
|
||||
// Mandatory params
|
||||
ArtistId id{ getMandatoryParameterAs<ArtistId>(context.parameters, "id") };
|
||||
|
||||
// Optional params
|
||||
std::size_t count{ getParameterAs<std::size_t>(context.parameters, "count").value_or(20) };
|
||||
|
||||
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||
Response::Node& artistInfoNode{ response.createNode(id3 ? "artistInfo2" : "artistInfo") };
|
||||
|
||||
{
|
||||
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||
|
||||
const Artist::pointer artist{ Artist::find(context.dbSession, id) };
|
||||
if (!artist)
|
||||
throw RequestedDataNotFoundError{};
|
||||
|
||||
std::optional<UUID> artistMBID{ artist->getMBID() };
|
||||
if (artistMBID)
|
||||
artistInfoNode.createChild("musicBrainzId").setValue(artistMBID->getAsString());
|
||||
}
|
||||
|
||||
auto similarArtistsId{ Service<Recommendation::IRecommendationService>::get()->getSimilarArtists(id, {TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist}, count) };
|
||||
|
||||
{
|
||||
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||
|
||||
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError{};
|
||||
|
||||
for (const ArtistId similarArtistId : similarArtistsId)
|
||||
{
|
||||
const Artist::pointer similarArtist{ Artist::find(context.dbSession, similarArtistId) };
|
||||
if (similarArtist)
|
||||
artistInfoNode.addArrayChild("similarArtist", createArtistNode(similarArtist, context.dbSession, user, id3));
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
Response handleGetArtistsRequestCommon(RequestContext& context, bool id3)
|
||||
{
|
||||
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||
|
||||
Response::Node& artistsNode{ response.createNode(id3 ? "artists" : "indexes") };
|
||||
artistsNode.setAttribute("ignoredArticles", "");
|
||||
artistsNode.setAttribute("lastModified", reportedDummyDateULong);
|
||||
|
||||
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||
|
||||
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError{};
|
||||
|
||||
Artist::FindParameters parameters;
|
||||
parameters.setSortMethod(ArtistSortMethod::BySortName);
|
||||
switch (user->getSubsonicArtistListMode())
|
||||
{
|
||||
case SubsonicArtistListMode::AllArtists:
|
||||
break;
|
||||
case SubsonicArtistListMode::ReleaseArtists:
|
||||
parameters.setLinkType(TrackArtistLinkType::ReleaseArtist);
|
||||
break;
|
||||
case SubsonicArtistListMode::TrackArtists:
|
||||
parameters.setLinkType(TrackArtistLinkType::Artist);
|
||||
break;
|
||||
}
|
||||
|
||||
std::map<char, std::vector<Artist::pointer>> artistsSortedByFirstChar;
|
||||
const RangeResults<ArtistId> artists{ Artist::find(context.dbSession, parameters) };
|
||||
for (const ArtistId artistId : artists.results)
|
||||
{
|
||||
const Artist::pointer artist{ Artist::find(context.dbSession, artistId) };
|
||||
const std::string& sortName{ artist->getSortName() };
|
||||
|
||||
char sortChar;
|
||||
if (sortName.empty() || !std::isalpha(sortName[0]))
|
||||
sortChar = '?';
|
||||
else
|
||||
sortChar = std::toupper(sortName[0]);
|
||||
|
||||
artistsSortedByFirstChar[sortChar].push_back(artist);
|
||||
}
|
||||
|
||||
|
||||
for (const auto& [sortChar, artists] : artistsSortedByFirstChar)
|
||||
{
|
||||
Response::Node& indexNode{ artistsNode.createArrayChild("index") };
|
||||
indexNode.setAttribute("name", std::string{ sortChar });
|
||||
|
||||
for (const Artist::pointer& artist : artists)
|
||||
indexNode.addArrayChild("artist", createArtistNode(artist, context.dbSession, user, id3));
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
std::vector<TrackId> findSimilarSongs(RequestContext& context, ArtistId artistId, std::size_t count)
|
||||
{
|
||||
// API says: "Returns a random collection of songs from the given artist and similar artists"
|
||||
const std::size_t similarArtistCount{ count / 5 };
|
||||
std::vector<ArtistId> artistIds{ Service<Recommendation::IRecommendationService>::get()->getSimilarArtists(artistId, {TrackArtistLinkType::Artist, TrackArtistLinkType::ReleaseArtist}, similarArtistCount) };
|
||||
artistIds.push_back(artistId);
|
||||
|
||||
const std::size_t meanTrackCountPerArtist{ (count / artistIds.size()) + 1 };
|
||||
|
||||
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||
|
||||
std::vector<TrackId> tracks;
|
||||
tracks.reserve(count);
|
||||
|
||||
for (const ArtistId id : artistIds)
|
||||
{
|
||||
Track::FindParameters params;
|
||||
params.setArtist(id);
|
||||
params.setRange({ 0, meanTrackCountPerArtist });
|
||||
params.setSortMethod(TrackSortMethod::Random);
|
||||
|
||||
const auto artistTracks{ Track::find(context.dbSession, params) };
|
||||
tracks.insert(std::end(tracks),
|
||||
std::begin(artistTracks.results),
|
||||
std::end(artistTracks.results));
|
||||
}
|
||||
|
||||
return tracks;
|
||||
}
|
||||
|
||||
std::vector<TrackId> findSimilarSongs(RequestContext& context, ReleaseId releaseId, std::size_t count)
|
||||
{
|
||||
// API says: "Returns a random collection of songs from the given artist and similar artists"
|
||||
// so let's extend this for release
|
||||
const std::size_t similarReleaseCount{ count / 5 };
|
||||
std::vector<ReleaseId> releaseIds{ Service<Recommendation::IRecommendationService>::get()->getSimilarReleases(releaseId, similarReleaseCount) };
|
||||
releaseIds.push_back(releaseId);
|
||||
|
||||
const std::size_t meanTrackCountPerRelease{ (count / releaseIds.size()) + 1 };
|
||||
|
||||
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||
|
||||
std::vector<TrackId> tracks;
|
||||
tracks.reserve(count);
|
||||
|
||||
for (const ReleaseId id : releaseIds)
|
||||
{
|
||||
Track::FindParameters params;
|
||||
params.setRelease(id);
|
||||
params.setRange({ 0, meanTrackCountPerRelease });
|
||||
params.setSortMethod(TrackSortMethod::Random);
|
||||
|
||||
const auto releaseTracks{ Track::find(context.dbSession, params) };
|
||||
tracks.insert(std::end(tracks),
|
||||
std::begin(releaseTracks.results),
|
||||
std::end(releaseTracks.results));
|
||||
}
|
||||
|
||||
return tracks;
|
||||
}
|
||||
|
||||
std::vector<TrackId> findSimilarSongs(RequestContext&, TrackId trackId, std::size_t count)
|
||||
{
|
||||
return Service<Recommendation::IRecommendationService>::get()->findSimilarTracks({ trackId }, count);
|
||||
}
|
||||
|
||||
Response handleGetSimilarSongsRequestCommon(RequestContext& context, bool id3)
|
||||
{
|
||||
// Optional params
|
||||
std::size_t count{ getParameterAs<std::size_t>(context.parameters, "count").value_or(50) };
|
||||
|
||||
std::vector<TrackId> tracks;
|
||||
|
||||
if (const auto artistId{ getParameterAs<ArtistId>(context.parameters, "id") })
|
||||
tracks = findSimilarSongs(context, *artistId, count);
|
||||
else if (const auto releaseId{ getParameterAs<ReleaseId>(context.parameters, "id") })
|
||||
tracks = findSimilarSongs(context, *releaseId, count);
|
||||
else if (const auto trackId{ getParameterAs<TrackId>(context.parameters, "id") })
|
||||
tracks = findSimilarSongs(context, *trackId, count);
|
||||
else
|
||||
throw BadParameterGenericError{ "id" };
|
||||
|
||||
Random::shuffleContainer(tracks);
|
||||
|
||||
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||
|
||||
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError{};
|
||||
|
||||
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||
Response::Node& similarSongsNode{ response.createNode(id3 ? "similarSongs2" : "similarSongs") };
|
||||
for (const TrackId trackId : tracks)
|
||||
{
|
||||
const Track::pointer track{ Track::find(context.dbSession, trackId) };
|
||||
similarSongsNode.addArrayChild("song", createSongNode(track, context.dbSession, user));
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Response handleGetMusicFoldersRequest(RequestContext& context)
|
||||
{
|
||||
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||
Response::Node& musicFoldersNode{ response.createNode("musicFolders") };
|
||||
|
||||
Response::Node& musicFolderNode{ musicFoldersNode.createArrayChild("musicFolder") };
|
||||
musicFolderNode.setAttribute("id", "0");
|
||||
musicFolderNode.setAttribute("name", "Music");
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
Response handleGetIndexesRequest(RequestContext& context)
|
||||
{
|
||||
return handleGetArtistsRequestCommon(context, false /* no id3 */);
|
||||
}
|
||||
|
||||
Response handleGetMusicDirectoryRequest(RequestContext& context)
|
||||
{
|
||||
// Mandatory params
|
||||
const auto artistId{ getParameterAs<ArtistId>(context.parameters, "id") };
|
||||
const auto releaseId{ getParameterAs<ReleaseId>(context.parameters, "id") };
|
||||
const auto root{ getParameterAs<RootId>(context.parameters, "id") };
|
||||
|
||||
if (!root && !artistId && !releaseId)
|
||||
throw BadParameterGenericError{ "id" };
|
||||
|
||||
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||
Response::Node& directoryNode{ response.createNode("directory") };
|
||||
|
||||
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||
|
||||
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError{};
|
||||
|
||||
if (root)
|
||||
{
|
||||
directoryNode.setAttribute("id", idToString(RootId{}));
|
||||
directoryNode.setAttribute("name", "Music");
|
||||
|
||||
auto rootArtistIds{ Artist::find(context.dbSession, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::BySortName)) };
|
||||
for (const ArtistId rootArtistId : rootArtistIds.results)
|
||||
{
|
||||
const Artist::pointer artist{ Artist::find(context.dbSession, rootArtistId) };
|
||||
directoryNode.addArrayChild("child", createArtistNode(artist, context.dbSession, user, false /* no id3 */));
|
||||
}
|
||||
}
|
||||
else if (artistId)
|
||||
{
|
||||
directoryNode.setAttribute("id", idToString(*artistId));
|
||||
|
||||
auto artist{ Artist::find(context.dbSession, *artistId) };
|
||||
if (!artist)
|
||||
throw RequestedDataNotFoundError{};
|
||||
|
||||
directoryNode.setAttribute("name", Utils::makeNameFilesystemCompatible(artist->getName()));
|
||||
|
||||
const auto artistReleases{ Release::find(context.dbSession, Release::FindParameters {}.setArtist(*artistId)) };
|
||||
for (const ReleaseId artistReleaseId : artistReleases.results)
|
||||
{
|
||||
const Release::pointer release{ Release::find(context.dbSession, artistReleaseId) };
|
||||
directoryNode.addArrayChild("child", createAlbumNode(release, context.dbSession, user, false /* no id3 */));
|
||||
}
|
||||
}
|
||||
else if (releaseId)
|
||||
{
|
||||
directoryNode.setAttribute("id", idToString(*releaseId));
|
||||
|
||||
auto release{ Release::find(context.dbSession, *releaseId) };
|
||||
if (!release)
|
||||
throw RequestedDataNotFoundError{};
|
||||
|
||||
directoryNode.setAttribute("name", Utils::makeNameFilesystemCompatible(release->getName()));
|
||||
|
||||
const auto tracks{ Track::find(context.dbSession, Track::FindParameters {}.setRelease(*releaseId).setSortMethod(TrackSortMethod::Release)) };
|
||||
for (const TrackId trackId : tracks.results)
|
||||
{
|
||||
const Track::pointer track{ Track::find(context.dbSession, trackId) };
|
||||
directoryNode.addArrayChild("child", createSongNode(track, context.dbSession, user));
|
||||
}
|
||||
}
|
||||
else
|
||||
throw BadParameterGenericError{ "id" };
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
Response handleGetGenresRequest(RequestContext& context)
|
||||
{
|
||||
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||
|
||||
Response::Node& genresNode{ response.createNode("genres") };
|
||||
|
||||
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||
|
||||
const ClusterType::pointer clusterType{ ClusterType::find(context.dbSession, "GENRE") };
|
||||
if (clusterType)
|
||||
{
|
||||
const auto clusters{ clusterType->getClusters() };
|
||||
|
||||
for (const Cluster::pointer& cluster : clusters)
|
||||
genresNode.addArrayChild("genre", createGenreNode(cluster));
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
Response handleGetArtistsRequest(RequestContext& context)
|
||||
{
|
||||
return handleGetArtistsRequestCommon(context, true /* id3 */);
|
||||
}
|
||||
|
||||
Response handleGetArtistRequest(RequestContext& context)
|
||||
{
|
||||
// Mandatory params
|
||||
ArtistId id{ getMandatoryParameterAs<ArtistId>(context.parameters, "id") };
|
||||
|
||||
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||
|
||||
const Artist::pointer artist{ Artist::find(context.dbSession, id) };
|
||||
if (!artist)
|
||||
throw RequestedDataNotFoundError{};
|
||||
|
||||
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError{};
|
||||
|
||||
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||
Response::Node artistNode{ createArtistNode(artist, context.dbSession, user, true /* id3 */) };
|
||||
|
||||
const auto releases{ Release::find(context.dbSession, Release::FindParameters {}.setArtist(artist->getId())) };
|
||||
for (const ReleaseId releaseId : releases.results)
|
||||
{
|
||||
const Release::pointer release{ Release::find(context.dbSession, releaseId) };
|
||||
artistNode.addArrayChild("album", createAlbumNode(release, context.dbSession, user, true /* id3 */));
|
||||
}
|
||||
|
||||
response.addNode("artist", std::move(artistNode));
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
Response handleGetAlbumRequest(RequestContext& context)
|
||||
{
|
||||
// Mandatory params
|
||||
ReleaseId id{ getMandatoryParameterAs<ReleaseId>(context.parameters, "id") };
|
||||
|
||||
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||
|
||||
Release::pointer release{ Release::find(context.dbSession, id) };
|
||||
if (!release)
|
||||
throw RequestedDataNotFoundError{};
|
||||
|
||||
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError{};
|
||||
|
||||
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||
Response::Node albumNode{ createAlbumNode(release, context.dbSession, user, true /* id3 */) };
|
||||
|
||||
const auto tracks{ Track::find(context.dbSession, Track::FindParameters {}.setRelease(id).setSortMethod(TrackSortMethod::Release)) };
|
||||
for (const TrackId trackId : tracks.results)
|
||||
{
|
||||
const Track::pointer track{ Track::find(context.dbSession, trackId) };
|
||||
albumNode.addArrayChild("song", createSongNode(track, context.dbSession, user));
|
||||
}
|
||||
|
||||
response.addNode("album", std::move(albumNode));
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
Response handleGetSongRequest(RequestContext& context)
|
||||
{
|
||||
// Mandatory params
|
||||
TrackId id{ getMandatoryParameterAs<TrackId>(context.parameters, "id") };
|
||||
|
||||
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||
|
||||
const Track::pointer track{ Track::find(context.dbSession, id) };
|
||||
if (!track)
|
||||
throw RequestedDataNotFoundError{};
|
||||
|
||||
User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError{};
|
||||
|
||||
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||
response.addNode("song", createSongNode(track, context.dbSession, user));
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
Response handleGetArtistInfoRequest(RequestContext& context)
|
||||
{
|
||||
return handleGetArtistInfoRequestCommon(context, false /* no id3 */);
|
||||
}
|
||||
|
||||
Response handleGetArtistInfo2Request(RequestContext& context)
|
||||
{
|
||||
return handleGetArtistInfoRequestCommon(context, true /* id3 */);
|
||||
}
|
||||
|
||||
Response handleGetSimilarSongsRequest(RequestContext& context)
|
||||
{
|
||||
return handleGetSimilarSongsRequestCommon(context, false /* no id3 */);
|
||||
}
|
||||
|
||||
Response handleGetSimilarSongs2Request(RequestContext& context)
|
||||
{
|
||||
return handleGetSimilarSongsRequestCommon(context, true /* id3 */);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "RequestContext.hpp"
|
||||
#include "SubsonicResponse.hpp"
|
||||
|
||||
namespace API::Subsonic
|
||||
{
|
||||
Response handleGetMusicFoldersRequest(RequestContext& context);
|
||||
Response handleGetIndexesRequest(RequestContext& context);
|
||||
Response handleGetMusicDirectoryRequest(RequestContext& context);
|
||||
Response handleGetGenresRequest(RequestContext& context);
|
||||
Response handleGetArtistsRequest(RequestContext& context);
|
||||
Response handleGetArtistRequest(RequestContext& context);
|
||||
Response handleGetAlbumRequest(RequestContext& context);
|
||||
Response handleGetSongRequest(RequestContext& context);
|
||||
Response handleGetArtistInfoRequest(RequestContext& context);
|
||||
Response handleGetArtistInfo2Request(RequestContext& context);
|
||||
Response handleGetSimilarSongsRequest(RequestContext& context);
|
||||
Response handleGetSimilarSongs2Request(RequestContext& context);
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "MediaAnnotation.hpp"
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "services/database/ArtistId.hpp"
|
||||
#include "services/database/ReleaseId.hpp"
|
||||
#include "services/database/TrackId.hpp"
|
||||
#include "services/scrobbling/IScrobblingService.hpp"
|
||||
#include "utils/Service.hpp"
|
||||
#include "ParameterParsing.hpp"
|
||||
#include "SubsonicId.hpp"
|
||||
|
||||
namespace API::Subsonic
|
||||
{
|
||||
using namespace Database;
|
||||
|
||||
namespace
|
||||
{
|
||||
struct StarParameters
|
||||
{
|
||||
std::vector<ArtistId> artistIds;
|
||||
std::vector<ReleaseId> releaseIds;
|
||||
std::vector<TrackId> trackIds;
|
||||
};
|
||||
|
||||
StarParameters getStarParameters(const Wt::Http::ParameterMap& parameters)
|
||||
{
|
||||
StarParameters res;
|
||||
|
||||
// TODO handle parameters for legacy file browsing
|
||||
res.trackIds = getMultiParametersAs<TrackId>(parameters, "id");
|
||||
res.artistIds = getMultiParametersAs<ArtistId>(parameters, "artistId");
|
||||
res.releaseIds = getMultiParametersAs<ReleaseId>(parameters, "albumId");
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
Response handleStarRequest(RequestContext& context)
|
||||
{
|
||||
StarParameters params{ getStarParameters(context.parameters) };
|
||||
|
||||
for (const ArtistId id : params.artistIds)
|
||||
Service<Scrobbling::IScrobblingService>::get()->star(context.userId, id);
|
||||
|
||||
for (const ReleaseId id : params.releaseIds)
|
||||
Service<Scrobbling::IScrobblingService>::get()->star(context.userId, id);
|
||||
|
||||
for (const TrackId id : params.trackIds)
|
||||
Service<Scrobbling::IScrobblingService>::get()->star(context.userId, id);
|
||||
|
||||
return Response::createOkResponse(context.serverProtocolVersion);
|
||||
}
|
||||
|
||||
Response handleUnstarRequest(RequestContext& context)
|
||||
{
|
||||
StarParameters params{ getStarParameters(context.parameters) };
|
||||
|
||||
for (const ArtistId id : params.artistIds)
|
||||
Service<Scrobbling::IScrobblingService>::get()->unstar(context.userId, id);
|
||||
|
||||
for (const ReleaseId id : params.releaseIds)
|
||||
Service<Scrobbling::IScrobblingService>::get()->unstar(context.userId, id);
|
||||
|
||||
for (const TrackId id : params.trackIds)
|
||||
Service<Scrobbling::IScrobblingService>::get()->unstar(context.userId, id);
|
||||
|
||||
return Response::createOkResponse(context.serverProtocolVersion);
|
||||
}
|
||||
|
||||
Response handleScrobble(RequestContext& context)
|
||||
{
|
||||
const std::vector<TrackId> ids{ getMandatoryMultiParametersAs<TrackId>(context.parameters, "id") };
|
||||
const std::vector<unsigned long> times{ getMultiParametersAs<unsigned long>(context.parameters, "time") };
|
||||
const bool submission{ getParameterAs<bool>(context.parameters, "submission").value_or(true) };
|
||||
|
||||
// playing now => no time to be provided
|
||||
if (!submission && !times.empty())
|
||||
throw BadParameterGenericError{ "time" };
|
||||
|
||||
// playing now => only one at a time
|
||||
if (!submission && ids.size() > 1)
|
||||
throw BadParameterGenericError{ "id" };
|
||||
|
||||
// if multiple submissions, must have times
|
||||
if (ids.size() > 1 && ids.size() != times.size())
|
||||
throw BadParameterGenericError{ "time" };
|
||||
|
||||
if (!submission)
|
||||
{
|
||||
Service<Scrobbling::IScrobblingService>::get()->listenStarted({ context.userId, ids.front() });
|
||||
}
|
||||
else
|
||||
{
|
||||
if (times.empty())
|
||||
{
|
||||
Service<Scrobbling::IScrobblingService>::get()->listenFinished({ context.userId, ids.front() });
|
||||
}
|
||||
else
|
||||
{
|
||||
for (std::size_t i{}; i < ids.size(); ++i)
|
||||
{
|
||||
const TrackId trackId{ ids[i] };
|
||||
const unsigned long time{ times[i] };
|
||||
Service<Scrobbling::IScrobblingService>::get()->addTimedListen({ {context.userId, trackId}, Wt::WDateTime::fromTime_t(static_cast<std::time_t>(time / 1000)) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Response::createOkResponse(context.serverProtocolVersion);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "RequestContext.hpp"
|
||||
#include "SubsonicResponse.hpp"
|
||||
|
||||
namespace API::Subsonic
|
||||
{
|
||||
Response handleStarRequest(RequestContext& context);
|
||||
Response handleUnstarRequest(RequestContext& context);
|
||||
Response handleScrobble(RequestContext& context);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "MediaLibraryScanning.hpp"
|
||||
|
||||
#include "services/scanner/IScannerService.hpp"
|
||||
#include "utils/Service.hpp"
|
||||
|
||||
namespace API::Subsonic::Scan
|
||||
{
|
||||
using namespace Scanner;
|
||||
|
||||
namespace
|
||||
{
|
||||
Response::Node
|
||||
createStatusResponseNode()
|
||||
{
|
||||
Response::Node statusResponse;
|
||||
|
||||
const IScannerService::Status scanStatus{ Service<IScannerService>::get()->getStatus() };
|
||||
|
||||
statusResponse.setAttribute("scanning", scanStatus.currentState == IScannerService::State::InProgress);
|
||||
if (scanStatus.currentState == IScannerService::State::InProgress)
|
||||
{
|
||||
std::size_t count{};
|
||||
|
||||
if (scanStatus.currentScanStepStats && scanStatus.currentScanStepStats->currentStep == ScanStep::ScanningFiles)
|
||||
count = scanStatus.currentScanStepStats->processedElems;
|
||||
|
||||
statusResponse.setAttribute("count", count);
|
||||
}
|
||||
|
||||
return statusResponse;
|
||||
}
|
||||
}
|
||||
|
||||
Response handleGetScanStatus(RequestContext& context)
|
||||
{
|
||||
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||
response.addNode("scanStatus", createStatusResponseNode());
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
Response handleStartScan(RequestContext& context)
|
||||
{
|
||||
Service<IScannerService>::get()->requestImmediateScan(false);
|
||||
|
||||
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
|
||||
response.addNode("scanStatus", createStatusResponseNode());
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -24,7 +24,7 @@
|
||||
|
||||
namespace API::Subsonic::Scan
|
||||
{
|
||||
Response handleGetScanStatus(RequestContext& context);
|
||||
Response handleStartScan(RequestContext& context);
|
||||
Response handleGetScanStatus(RequestContext& context);
|
||||
Response handleStartScan(RequestContext& context);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "MediaRetrieval.hpp"
|
||||
|
||||
#include "av/TranscodeParameters.hpp"
|
||||
#include "av/TranscodeResourceHandlerCreator.hpp"
|
||||
#include "av/Types.hpp"
|
||||
#include "services/cover/ICoverService.hpp"
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/Track.hpp"
|
||||
#include "services/database/User.hpp"
|
||||
#include "utils/IResourceHandler.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/FileResourceHandlerCreator.hpp"
|
||||
#include "utils/Utils.hpp"
|
||||
#include "ParameterParsing.hpp"
|
||||
#include "SubsonicId.hpp"
|
||||
|
||||
using namespace Database;
|
||||
|
||||
namespace API::Subsonic
|
||||
{
|
||||
namespace {
|
||||
Av::Format userTranscodeFormatToAvFormat(AudioFormat format)
|
||||
{
|
||||
switch (format)
|
||||
{
|
||||
case AudioFormat::MP3: return Av::Format::MP3;
|
||||
case AudioFormat::OGG_OPUS: return Av::Format::OGG_OPUS;
|
||||
case AudioFormat::MATROSKA_OPUS: return Av::Format::MATROSKA_OPUS;
|
||||
case AudioFormat::OGG_VORBIS: return Av::Format::OGG_VORBIS;
|
||||
case AudioFormat::WEBM_VORBIS: return Av::Format::WEBM_VORBIS;
|
||||
default: return Av::Format::OGG_OPUS;
|
||||
}
|
||||
}
|
||||
|
||||
struct StreamParameters
|
||||
{
|
||||
Av::InputFileParameters inputFileParameters;
|
||||
std::optional<Av::TranscodeParameters> transcodeParameters;
|
||||
bool estimateContentLength{};
|
||||
};
|
||||
|
||||
StreamParameters getStreamParameters(RequestContext& context)
|
||||
{
|
||||
// Mandatory params
|
||||
const TrackId id{ getMandatoryParameterAs<TrackId>(context.parameters, "id") };
|
||||
|
||||
// Optional params
|
||||
std::optional<std::size_t> maxBitRate{ getParameterAs<std::size_t>(context.parameters, "maxBitRate") };
|
||||
const std::optional<std::string> format{ getParameterAs<std::string>(context.parameters, "format") };
|
||||
const std::optional<std::size_t> timeOffset{ getParameterAs<std::size_t>(context.parameters, "timeOffset") };
|
||||
bool estimateContentLength{ getParameterAs<bool>(context.parameters, "estimateContentLength").value_or(false) };
|
||||
|
||||
StreamParameters parameters;
|
||||
|
||||
parameters.estimateContentLength = estimateContentLength;
|
||||
|
||||
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||
|
||||
{
|
||||
const auto track{ Track::find(context.dbSession, id) };
|
||||
if (!track)
|
||||
throw RequestedDataNotFoundError{};
|
||||
|
||||
parameters.inputFileParameters.trackPath = track->getPath();
|
||||
parameters.inputFileParameters.duration = track->getDuration();
|
||||
}
|
||||
|
||||
{
|
||||
const User::pointer user{ User::find(context.dbSession, context.userId) };
|
||||
if (!user)
|
||||
throw UserNotAuthorizedError{};
|
||||
|
||||
// format = "raw" => no transcode. Other format values will be ignored
|
||||
const bool transcode{ (!format || (*format != "raw")) && user->getSubsonicTranscodeEnable() };
|
||||
if (transcode)
|
||||
{
|
||||
std::size_t bitRate{ user->getSubsonicTranscodeBitrate() / 1000 };
|
||||
|
||||
// "If set to zero, no limit is imposed"
|
||||
if (maxBitRate && *maxBitRate != 0)
|
||||
bitRate = Utils::clamp(*maxBitRate, std::size_t{ 48 }, bitRate);
|
||||
|
||||
Av::TranscodeParameters transcodeParameters;
|
||||
|
||||
transcodeParameters.bitrate = bitRate * 1000;
|
||||
transcodeParameters.format = userTranscodeFormatToAvFormat(user->getSubsonicTranscodeFormat());
|
||||
transcodeParameters.stripMetadata = false; // We want clients to use metadata (offline use, replay gain, etc.)
|
||||
transcodeParameters.offset = std::chrono::seconds{ timeOffset ? *timeOffset : 0 };
|
||||
|
||||
parameters.transcodeParameters = std::move(transcodeParameters);
|
||||
}
|
||||
}
|
||||
|
||||
return parameters;
|
||||
}
|
||||
}
|
||||
|
||||
void handleDownload(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response)
|
||||
{
|
||||
std::shared_ptr<IResourceHandler> resourceHandler;
|
||||
|
||||
Wt::Http::ResponseContinuation* continuation{ request.continuation() };
|
||||
if (!continuation)
|
||||
{
|
||||
// Mandatory params
|
||||
Database::TrackId id{ getMandatoryParameterAs<Database::TrackId>(context.parameters, "id") };
|
||||
|
||||
std::filesystem::path trackPath;
|
||||
{
|
||||
auto transaction{ context.dbSession.createSharedTransaction() };
|
||||
|
||||
auto track{ Track::find(context.dbSession, id) };
|
||||
if (!track)
|
||||
throw RequestedDataNotFoundError{};
|
||||
|
||||
trackPath = track->getPath();
|
||||
}
|
||||
|
||||
resourceHandler = createFileResourceHandler(trackPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
resourceHandler = Wt::cpp17::any_cast<std::shared_ptr<IResourceHandler>>(continuation->data());
|
||||
}
|
||||
|
||||
continuation = resourceHandler->processRequest(request, response);
|
||||
if (continuation)
|
||||
continuation->setData(resourceHandler);
|
||||
}
|
||||
|
||||
void handleStream(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response)
|
||||
{
|
||||
std::shared_ptr<IResourceHandler> resourceHandler;
|
||||
|
||||
try
|
||||
{
|
||||
Wt::Http::ResponseContinuation* continuation = request.continuation();
|
||||
if (!continuation)
|
||||
{
|
||||
StreamParameters streamParameters{ getStreamParameters(context) };
|
||||
if (streamParameters.transcodeParameters)
|
||||
resourceHandler = Av::createTranscodeResourceHandler(streamParameters.inputFileParameters, *streamParameters.transcodeParameters, streamParameters.estimateContentLength);
|
||||
else
|
||||
resourceHandler = createFileResourceHandler(streamParameters.inputFileParameters.trackPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
resourceHandler = Wt::cpp17::any_cast<std::shared_ptr<IResourceHandler>>(continuation->data());
|
||||
}
|
||||
|
||||
continuation = resourceHandler->processRequest(request, response);
|
||||
if (continuation)
|
||||
continuation->setData(resourceHandler);
|
||||
}
|
||||
catch (const Av::Exception& e)
|
||||
{
|
||||
LMS_LOG(API_SUBSONIC, ERROR) << "Caught Av exception: " << e.what();
|
||||
}
|
||||
}
|
||||
|
||||
void handleGetCoverArt(RequestContext& context, const Wt::Http::Request& /*request*/, Wt::Http::Response& response)
|
||||
{
|
||||
// Mandatory params
|
||||
const auto trackId{ getParameterAs<TrackId>(context.parameters, "id") };
|
||||
const auto releaseId{ getParameterAs<ReleaseId>(context.parameters, "id") };
|
||||
|
||||
if (!trackId && !releaseId)
|
||||
throw BadParameterGenericError{ "id" };
|
||||
|
||||
std::size_t size{ getParameterAs<std::size_t>(context.parameters, "size").value_or(1024) };
|
||||
size = ::Utils::clamp(size, std::size_t{ 32 }, std::size_t{ 2048 });
|
||||
|
||||
std::shared_ptr<Image::IEncodedImage> cover;
|
||||
if (trackId)
|
||||
cover = Service<Cover::ICoverService>::get()->getFromTrack(*trackId, size);
|
||||
else if (releaseId)
|
||||
cover = Service<Cover::ICoverService>::get()->getFromRelease(*releaseId, size);
|
||||
|
||||
response.out().write(reinterpret_cast<const char*>(cover->getData()), cover->getDataSize());
|
||||
response.setMimeType(std::string{ cover->getMimeType() });
|
||||
}
|
||||
|
||||
} // namespace API::Subsonic
|
||||
+4
-3
@@ -24,9 +24,10 @@
|
||||
|
||||
#include "RequestContext.hpp"
|
||||
|
||||
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 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);
|
||||
}
|
||||
+210
-214
@@ -27,279 +27,275 @@
|
||||
#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
|
||||
{
|
||||
std::string curStr;
|
||||
|
||||
for (char c : str)
|
||||
{
|
||||
if (separators.find(c) != std::string::npos) {
|
||||
if (!curStr.empty()) {
|
||||
results.push_back(curStr);
|
||||
curStr.clear();
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (curStr.empty() && std::isspace(c))
|
||||
continue;
|
||||
bool readList(const std::string& str, const std::string& separators, std::list<std::string>& results)
|
||||
{
|
||||
std::string curStr;
|
||||
|
||||
curStr.push_back(c);
|
||||
}
|
||||
}
|
||||
for (char c : str)
|
||||
{
|
||||
if (separators.find(c) != std::string::npos) {
|
||||
if (!curStr.empty()) {
|
||||
results.push_back(curStr);
|
||||
curStr.clear();
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (curStr.empty() && std::isspace(c))
|
||||
continue;
|
||||
|
||||
if (!curStr.empty())
|
||||
results.push_back(curStr);
|
||||
curStr.push_back(c);
|
||||
}
|
||||
}
|
||||
|
||||
return !str.empty();
|
||||
}
|
||||
if (!curStr.empty())
|
||||
results.push_back(curStr);
|
||||
|
||||
template<>
|
||||
std::optional<std::string>
|
||||
readAs(std::string_view str)
|
||||
{
|
||||
return std::string {str};
|
||||
}
|
||||
return !str.empty();
|
||||
}
|
||||
|
||||
template<>
|
||||
std::optional<std::string_view>
|
||||
readAs(std::string_view str)
|
||||
{
|
||||
return str;
|
||||
}
|
||||
template<>
|
||||
std::optional<std::string> readAs(std::string_view str)
|
||||
{
|
||||
return std::string{ str };
|
||||
}
|
||||
|
||||
template<>
|
||||
std::optional<bool>
|
||||
readAs(std::string_view str)
|
||||
{
|
||||
if (str == "1" || str == "true")
|
||||
return true;
|
||||
else if (str == "0" || str == "false")
|
||||
return false;
|
||||
template<>
|
||||
std::optional<std::string_view> readAs(std::string_view str)
|
||||
{
|
||||
return str;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
template<>
|
||||
std::optional<bool> readAs(std::string_view str)
|
||||
{
|
||||
if (str == "1" || str == "true")
|
||||
return true;
|
||||
else if (str == "0" || str == "false")
|
||||
return false;
|
||||
|
||||
std::vector<std::string>
|
||||
splitStringCopy(std::string_view string, std::string_view separators)
|
||||
{
|
||||
std::string str {stringTrim(string, separators)};
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::vector<std::string> res;
|
||||
boost::algorithm::split(res, str, boost::is_any_of(separators), boost::token_compress_on);
|
||||
std::vector<std::string> splitStringCopy(std::string_view string, std::string_view separators)
|
||||
{
|
||||
std::string str{ stringTrim(string, separators) };
|
||||
|
||||
return res;
|
||||
}
|
||||
std::vector<std::string> res;
|
||||
boost::algorithm::split(res, str, boost::is_any_of(separators), boost::token_compress_on);
|
||||
|
||||
std::vector<std::string_view>
|
||||
splitString(std::string_view str, std::string_view separators)
|
||||
{
|
||||
std::vector<std::string_view> res;
|
||||
return res;
|
||||
}
|
||||
|
||||
std::string_view::size_type strBegin {};
|
||||
std::vector<std::string_view> splitString(std::string_view str, std::string_view separators)
|
||||
{
|
||||
std::vector<std::string_view> res;
|
||||
|
||||
while ((strBegin = str.find_first_not_of(separators, strBegin)) != std::string_view::npos)
|
||||
{
|
||||
auto strEnd {str.find_first_of(separators, strBegin + 1)};
|
||||
if (strEnd == std::string_view::npos)
|
||||
{
|
||||
res.push_back(str.substr(strBegin, str.size() - strBegin));
|
||||
break;
|
||||
}
|
||||
std::string_view::size_type strBegin{};
|
||||
|
||||
res.push_back(str.substr(strBegin, strEnd - strBegin));
|
||||
strBegin = strEnd + 1;
|
||||
}
|
||||
while ((strBegin = str.find_first_not_of(separators, strBegin)) != std::string_view::npos)
|
||||
{
|
||||
auto strEnd{ str.find_first_of(separators, strBegin + 1) };
|
||||
if (strEnd == std::string_view::npos)
|
||||
{
|
||||
res.push_back(str.substr(strBegin, str.size() - strBegin));
|
||||
break;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
res.push_back(str.substr(strBegin, strEnd - strBegin));
|
||||
strBegin = strEnd + 1;
|
||||
}
|
||||
|
||||
std::string
|
||||
joinStrings(const std::vector<std::string>& strings, const std::string& delimiter)
|
||||
{
|
||||
return boost::algorithm::join(strings, delimiter);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
std::string_view
|
||||
stringTrim(std::string_view str, std::string_view whitespaces)
|
||||
{
|
||||
std::string_view res;
|
||||
std::string joinStrings(const std::vector<std::string>& strings, const std::string& delimiter)
|
||||
{
|
||||
return boost::algorithm::join(strings, delimiter);
|
||||
}
|
||||
|
||||
const auto strBegin = str.find_first_not_of(whitespaces);
|
||||
if (strBegin != std::string_view::npos)
|
||||
{
|
||||
const auto strEnd {str.find_last_not_of(whitespaces)};
|
||||
const auto strRange {strEnd - strBegin + 1};
|
||||
std::string_view stringTrim(std::string_view str, std::string_view whitespaces)
|
||||
{
|
||||
std::string_view res;
|
||||
|
||||
res = str.substr(strBegin, strRange);
|
||||
}
|
||||
const auto strBegin = str.find_first_not_of(whitespaces);
|
||||
if (strBegin != std::string_view::npos)
|
||||
{
|
||||
const auto strEnd{ str.find_last_not_of(whitespaces) };
|
||||
const auto strRange{ strEnd - strBegin + 1 };
|
||||
|
||||
return res;
|
||||
}
|
||||
res = str.substr(strBegin, strRange);
|
||||
}
|
||||
|
||||
std::string_view
|
||||
stringTrimEnd(std::string_view str, std::string_view whitespaces)
|
||||
{
|
||||
return str.substr(0, str.find_last_not_of(whitespaces) + 1);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
std::string
|
||||
stringToLower(std::string_view str)
|
||||
{
|
||||
std::string res;
|
||||
res.reserve(str.size());
|
||||
std::string_view stringTrimEnd(std::string_view str, std::string_view whitespaces)
|
||||
{
|
||||
return str.substr(0, str.find_last_not_of(whitespaces) + 1);
|
||||
}
|
||||
|
||||
std::transform(std::cbegin(str), std::cend(str), std::back_inserter(res), [](unsigned char c) { return std::tolower(c);});
|
||||
std::string stringToLower(std::string_view str)
|
||||
{
|
||||
std::string res;
|
||||
res.reserve(str.size());
|
||||
|
||||
return res;
|
||||
}
|
||||
std::transform(std::cbegin(str), std::cend(str), std::back_inserter(res), [](unsigned char c) { return std::tolower(c);});
|
||||
|
||||
void
|
||||
stringToLower(std::string& str)
|
||||
{
|
||||
std::transform(std::cbegin(str), std::cend(str), std::begin(str), [](unsigned char c) { return std::tolower(c);});
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
std::string
|
||||
stringToUpper(const std::string& str)
|
||||
{
|
||||
std::string res;
|
||||
res.reserve(str.size());
|
||||
void stringToLower(std::string& str)
|
||||
{
|
||||
std::transform(std::cbegin(str), std::cend(str), std::begin(str), [](unsigned char c) { return std::tolower(c);});
|
||||
}
|
||||
|
||||
std::transform(std::cbegin(str), std::cend(str), std::back_inserter(res), [](char c) { return std::toupper(c);});
|
||||
std::string stringToUpper(const std::string& str)
|
||||
{
|
||||
std::string res;
|
||||
res.reserve(str.size());
|
||||
|
||||
return res;
|
||||
}
|
||||
std::transform(std::cbegin(str), std::cend(str), std::back_inserter(res), [](char c) { return std::toupper(c);});
|
||||
|
||||
std::string
|
||||
bufferToString(const std::vector<unsigned char>& data)
|
||||
{
|
||||
std::ostringstream oss;
|
||||
return res;
|
||||
}
|
||||
|
||||
for (unsigned char c : data)
|
||||
{
|
||||
oss << std::setw(2) << std::setfill('0') << std::hex << (int)c;
|
||||
}
|
||||
std::string bufferToString(const std::vector<unsigned char>& data)
|
||||
{
|
||||
std::ostringstream oss;
|
||||
|
||||
return oss.str();
|
||||
}
|
||||
for (unsigned char c : data)
|
||||
{
|
||||
oss << std::setw(2) << std::setfill('0') << std::hex << (int)c;
|
||||
}
|
||||
|
||||
void
|
||||
capitalize(std::string& str)
|
||||
{
|
||||
for (auto it {std::begin(str)}; it != std::end(str); ++it)
|
||||
{
|
||||
if (std::isspace(*it))
|
||||
continue;
|
||||
return oss.str();
|
||||
}
|
||||
|
||||
if (std::isalpha(*it))
|
||||
*it = std::toupper(*it);
|
||||
void capitalize(std::string& str)
|
||||
{
|
||||
for (auto it{ std::begin(str) }; it != std::end(str); ++it)
|
||||
{
|
||||
if (std::isspace(*it))
|
||||
continue;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (std::isalpha(*it))
|
||||
*it = std::toupper(*it);
|
||||
|
||||
std::string
|
||||
replaceInString(std::string_view str, const std::string& from, const std::string& to)
|
||||
{
|
||||
std::string res {str};
|
||||
size_t pos = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
while ((pos = res.find(from, pos)) != std::string::npos)
|
||||
{
|
||||
res.replace(pos, from.length(), to);
|
||||
pos += to.length();
|
||||
}
|
||||
std::string replaceInString(std::string_view str, const std::string& from, const std::string& to)
|
||||
{
|
||||
std::string res{ str };
|
||||
size_t pos = 0;
|
||||
|
||||
return res;
|
||||
}
|
||||
while ((pos = res.find(from, pos)) != std::string::npos)
|
||||
{
|
||||
res.replace(pos, from.length(), to);
|
||||
pos += to.length();
|
||||
}
|
||||
|
||||
std::string
|
||||
jsEscape(const std::string& str)
|
||||
{
|
||||
static const std::unordered_map<char, std::string_view> escapeMap
|
||||
{
|
||||
{ '\\', "\\\\" },
|
||||
{ '\n', "\\n" },
|
||||
{ '\r', "\\r" },
|
||||
{ '\t', "\\t" },
|
||||
{ '"', "\\\"" },
|
||||
{ '\'', "\\\'" },
|
||||
};
|
||||
return res;
|
||||
}
|
||||
|
||||
std::string escaped;
|
||||
escaped.reserve(str.length());
|
||||
std::string jsEscape(const std::string& str)
|
||||
{
|
||||
static const std::unordered_map<char, std::string_view> escapeMap
|
||||
{
|
||||
{ '\\', "\\\\" },
|
||||
{ '\n', "\\n" },
|
||||
{ '\r', "\\r" },
|
||||
{ '\t', "\\t" },
|
||||
{ '"', "\\\"" },
|
||||
{ '\'', "\\\'" },
|
||||
};
|
||||
|
||||
for (const char c : str)
|
||||
{
|
||||
auto it {escapeMap.find(c)};
|
||||
if (it == std::cend(escapeMap))
|
||||
{
|
||||
escaped += c;
|
||||
continue;
|
||||
}
|
||||
std::string escaped;
|
||||
escaped.reserve(str.length());
|
||||
|
||||
escaped += it->second;
|
||||
}
|
||||
for (const char c : str)
|
||||
{
|
||||
auto it{ escapeMap.find(c) };
|
||||
if (it == std::cend(escapeMap))
|
||||
{
|
||||
escaped += c;
|
||||
continue;
|
||||
}
|
||||
|
||||
return escaped;
|
||||
}
|
||||
escaped += it->second;
|
||||
}
|
||||
|
||||
std::string
|
||||
escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar)
|
||||
{
|
||||
std::string res;
|
||||
res.reserve(str.size());
|
||||
return escaped;
|
||||
}
|
||||
|
||||
for (const char c : str)
|
||||
{
|
||||
if (std::any_of(std::cbegin(charsToEscape), std::cend(charsToEscape), [c](char charToEscape) { return c == charToEscape; }))
|
||||
res += escapeChar;
|
||||
std::string escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar)
|
||||
{
|
||||
std::string res;
|
||||
res.reserve(str.size());
|
||||
|
||||
res += c;
|
||||
}
|
||||
for (const char c : str)
|
||||
{
|
||||
if (std::any_of(std::cbegin(charsToEscape), std::cend(charsToEscape), [c](char charToEscape) { return c == charToEscape; }))
|
||||
res += escapeChar;
|
||||
|
||||
return res;
|
||||
}
|
||||
res += c;
|
||||
}
|
||||
|
||||
bool
|
||||
stringEndsWith(const std::string& str, const std::string& ending)
|
||||
{
|
||||
return boost::algorithm::ends_with(str, ending);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
std::optional<std::string>
|
||||
stringFromHex(const std::string& str)
|
||||
{
|
||||
static const char lut[] {"0123456789ABCDEF"};
|
||||
bool stringEndsWith(const std::string& str, const std::string& ending)
|
||||
{
|
||||
return boost::algorithm::ends_with(str, ending);
|
||||
}
|
||||
|
||||
if (str.length() % 2 != 0)
|
||||
return std::nullopt;
|
||||
std::optional<std::string> stringFromHex(const std::string& str)
|
||||
{
|
||||
static const char lut[]{ "0123456789ABCDEF" };
|
||||
|
||||
std::string res;
|
||||
res.reserve(str.length() / 2);
|
||||
if (str.length() % 2 != 0)
|
||||
return std::nullopt;
|
||||
|
||||
auto it {std::cbegin(str)};
|
||||
while (it != std::cend(str))
|
||||
{
|
||||
unsigned val {};
|
||||
std::string res;
|
||||
res.reserve(str.length() / 2);
|
||||
|
||||
auto itHigh {std::lower_bound(std::cbegin(lut), std::cend(lut), std::toupper(*(it++)))};
|
||||
auto itLow {std::lower_bound(std::cbegin(lut), std::cend(lut), std::toupper(*(it++)))};
|
||||
auto it{ std::cbegin(str) };
|
||||
while (it != std::cend(str))
|
||||
{
|
||||
unsigned val{};
|
||||
|
||||
if (itHigh == std::cend(lut) || itLow == std::cend(lut))
|
||||
return {};
|
||||
auto itHigh{ std::lower_bound(std::cbegin(lut), std::cend(lut), std::toupper(*(it++))) };
|
||||
auto itLow{ std::lower_bound(std::cbegin(lut), std::cend(lut), std::toupper(*(it++))) };
|
||||
|
||||
val = std::distance(std::cbegin(lut), itHigh) << 4;
|
||||
val += std::distance(std::cbegin(lut), itLow );
|
||||
if (itHigh == std::cend(lut) || itLow == std::cend(lut))
|
||||
return {};
|
||||
|
||||
res.push_back(static_cast<char>(val));
|
||||
}
|
||||
val = std::distance(std::cbegin(lut), itHigh) << 4;
|
||||
val += std::distance(std::cbegin(lut), itLow);
|
||||
|
||||
return res;
|
||||
}
|
||||
res.push_back(static_cast<char>(val));
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::string toISO8601String(const Wt::WDateTime& dateTime)
|
||||
{
|
||||
// assume UTC
|
||||
return dateTime.toString("yyyy-MM-ddThh:mm:ss.zzz", false).toUTF8();
|
||||
}
|
||||
|
||||
std::string toISO8601String(const Wt::WDate& date)
|
||||
{
|
||||
// assume UTC
|
||||
return date.toString("yyyy-MM-dd").toUTF8();
|
||||
}
|
||||
} // StringUtils
|
||||
|
||||
|
||||
@@ -29,94 +29,67 @@
|
||||
#define QUOTEME(x) QUOTEME_1(x)
|
||||
#define QUOTEME_1(x) #x
|
||||
|
||||
namespace StringUtils {
|
||||
|
||||
[[nodiscard]]
|
||||
std::vector<std::string>
|
||||
splitStringCopy(std::string_view string, std::string_view separators);
|
||||
|
||||
[[nodiscard]]
|
||||
std::vector<std::string_view>
|
||||
splitString(std::string_view string, std::string_view separators);
|
||||
|
||||
[[nodiscard]]
|
||||
std::string
|
||||
joinStrings(const std::vector<std::string>& strings, const std::string& delimiter);
|
||||
|
||||
[[nodiscard]]
|
||||
std::string_view
|
||||
stringTrim(std::string_view str, std::string_view whitespaces = " \t");
|
||||
|
||||
[[nodiscard]]
|
||||
std::string_view
|
||||
stringTrimEnd(std::string_view str, std::string_view whitespaces = " \t");
|
||||
|
||||
[[nodiscard]]
|
||||
std::string
|
||||
stringToLower(std::string_view str);
|
||||
|
||||
void
|
||||
stringToLower(std::string& str);
|
||||
|
||||
[[nodiscard]]
|
||||
std::string
|
||||
stringToUpper(const std::string& str);
|
||||
|
||||
[[nodiscard]]
|
||||
std::string
|
||||
bufferToString(const std::vector<unsigned char>& data);
|
||||
|
||||
void
|
||||
capitalize(std::string& str);
|
||||
|
||||
template<typename T>
|
||||
[[nodiscard]]
|
||||
std::optional<T> readAs(std::string_view str)
|
||||
namespace Wt
|
||||
{
|
||||
T res;
|
||||
|
||||
std::istringstream iss {std::string {str}};
|
||||
iss >> res;
|
||||
if (iss.fail())
|
||||
return std::nullopt;
|
||||
|
||||
return res;
|
||||
class WDate;
|
||||
class WDateTime;
|
||||
}
|
||||
|
||||
template<>
|
||||
[[nodiscard]]
|
||||
std::optional<std::string>
|
||||
readAs(std::string_view str);
|
||||
namespace StringUtils {
|
||||
|
||||
template<>
|
||||
[[nodiscard]]
|
||||
std::optional<std::string_view>
|
||||
readAs(std::string_view str);
|
||||
[[nodiscard]] std::vector<std::string> splitStringCopy(std::string_view string, std::string_view separators);
|
||||
|
||||
template<>
|
||||
[[nodiscard]]
|
||||
std::optional<bool>
|
||||
readAs(std::string_view str);
|
||||
[[nodiscard]] std::vector<std::string_view> splitString(std::string_view string, std::string_view separators);
|
||||
|
||||
[[nodiscard]]
|
||||
std::string
|
||||
replaceInString(std::string_view str, const std::string& from, const std::string& to);
|
||||
[[nodiscard]] std::string joinStrings(const std::vector<std::string>& strings, const std::string& delimiter);
|
||||
|
||||
[[nodiscard]]
|
||||
std::string
|
||||
jsEscape(const std::string& str);
|
||||
[[nodiscard]] std::string_view stringTrim(std::string_view str, std::string_view whitespaces = " \t");
|
||||
|
||||
[[nodiscard]]
|
||||
std::string
|
||||
escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar);
|
||||
[[nodiscard]] std::string_view stringTrimEnd(std::string_view str, std::string_view whitespaces = " \t");
|
||||
|
||||
[[nodiscard]]
|
||||
bool
|
||||
stringEndsWith(const std::string& str, const std::string& ending);
|
||||
[[nodiscard]] std::string stringToLower(std::string_view str);
|
||||
|
||||
[[nodiscard]]
|
||||
std::optional<std::string>
|
||||
stringFromHex(const std::string& str);
|
||||
void stringToLower(std::string& str);
|
||||
|
||||
} // StringUtils
|
||||
[[nodiscard]] std::string stringToUpper(const std::string& str);
|
||||
|
||||
[[nodiscard]] std::string bufferToString(const std::vector<unsigned char>& data);
|
||||
|
||||
void capitalize(std::string& str);
|
||||
|
||||
template<typename T>
|
||||
[[nodiscard]] std::optional<T> readAs(std::string_view str)
|
||||
{
|
||||
T res;
|
||||
|
||||
std::istringstream iss{ std::string {str} };
|
||||
iss >> res;
|
||||
if (iss.fail())
|
||||
return std::nullopt;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
template<>
|
||||
[[nodiscard]] std::optional<std::string> readAs(std::string_view str);
|
||||
|
||||
template<>
|
||||
[[nodiscard]] std::optional<std::string_view> readAs(std::string_view str);
|
||||
|
||||
template<>
|
||||
[[nodiscard]] std::optional<bool> readAs(std::string_view str);
|
||||
|
||||
[[nodiscard]] std::string replaceInString(std::string_view str, const std::string& from, const std::string& to);
|
||||
|
||||
[[nodiscard]] std::string jsEscape(const std::string& str);
|
||||
|
||||
[[nodiscard]] std::string escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar);
|
||||
|
||||
[[nodiscard]] bool stringEndsWith(const std::string& str, const std::string& ending);
|
||||
|
||||
[[nodiscard]] std::optional<std::string> stringFromHex(const std::string& str);
|
||||
|
||||
[[nodiscard]] std::string toISO8601String(const Wt::WDateTime& dateTime);
|
||||
[[nodiscard]] std::string toISO8601String(const Wt::WDate& date);
|
||||
|
||||
} // StringUtils
|
||||
@@ -19,6 +19,9 @@
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <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
|
||||
{
|
||||
std::unique_ptr<Wt::WTemplate> createEntry(const Database::ObjectPtr<Database::Artist>& artist);
|
||||
using ArtistLinkTypesModel = ValueStringModel<std::optional<Database::TrackArtistLinkType>>;
|
||||
|
||||
namespace ArtistListHelpers
|
||||
{
|
||||
std::unique_ptr<Wt::WTemplate> createEntry(const Database::ObjectPtr<Database::Artist>& artist);
|
||||
std::unique_ptr<ArtistLinkTypesModel> createArtistLinkTypesModel();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,10 +24,8 @@
|
||||
#include "services/database/Artist.hpp"
|
||||
#include "services/database/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