Merge branch 'develop' for release v3.41.0

This commit is contained in:
emeric
2023-10-11 20:33:20 +02:00
99 changed files with 7093 additions and 5485 deletions
+4 -4
View File
@@ -8,10 +8,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check Out Repo - name: Check Out Repo
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Cache Docker layers - name: Cache Docker layers
uses: actions/cache@v2 uses: actions/cache@v3
with: with:
path: /tmp/.buildx-cache path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }} key: ${{ runner.os }}-buildx-${{ github.sha }}
@@ -20,10 +20,10 @@ jobs:
- name: Set up Docker Buildx - name: Set up Docker Buildx
id: buildx id: buildx
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v2
- name: Build (alpine) - name: Build (alpine)
uses: docker/build-push-action@v2 uses: docker/build-push-action@v3
with: with:
context: ./ context: ./
file: ./Dockerfile-build-alpine file: ./Dockerfile-build-alpine
+4 -4
View File
@@ -8,10 +8,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check Out Repo - name: Check Out Repo
uses: actions/checkout@v2 uses: actions/checkout@v3
- name: Cache Docker layers - name: Cache Docker layers
uses: actions/cache@v2 uses: actions/cache@v3
with: with:
path: /tmp/.buildx-cache path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }} key: ${{ runner.os }}-buildx-${{ github.sha }}
@@ -20,10 +20,10 @@ jobs:
- name: Set up Docker Buildx - name: Set up Docker Buildx
id: buildx id: buildx
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@v2
- name: Build - name: Build
uses: docker/build-push-action@v2 uses: docker/build-push-action@v3
with: with:
context: ./ context: ./
file: ./Dockerfile-build-arch file: ./Dockerfile-build-arch
+1 -1
View File
@@ -19,7 +19,7 @@ _Docker_ images are available, please see detailed instructions on https://hub.d
_Bookworm_ packages are provided for _amd64_ architectures. _Bookworm_ packages are provided for _amd64_ architectures.
As root, trust the following debian package provider and add it in your list of repositories: As root, trust the following debian package provider and add it in your list of repositories:
```sh ```sh
wget https://debian.poupon.dev/apt/debian/epoupon.gpg -P /usr/share/keyrings wget --backups=1 https://debian.poupon.dev/apt/debian/epoupon.gpg -P /usr/share/keyrings
echo "deb [signed-by=/usr/share/keyrings/epoupon.gpg] https://debian.poupon.dev/apt/debian bookworm main" > /etc/apt/sources.list.d/epoupon.list echo "deb [signed-by=/usr/share/keyrings/epoupon.gpg] https://debian.poupon.dev/apt/debian bookworm main" > /etc/apt/sources.list.d/epoupon.list
``` ```
To install or upgrade _LMS_: To install or upgrade _LMS_:
+1 -8
View File
@@ -18,7 +18,7 @@ A [demo instance](http://lms-demo.poupon.dev) is available. Note the administrat
* Synchronizing 'love' feedbacks * Synchronizing 'love' feedbacks
* ReplayGain support * ReplayGain support
* User management, with several [authentication backends](INSTALL.md#authentication-backend) * User management, with several [authentication backends](INSTALL.md#authentication-backend)
* Subsonic API * [Subsonic/OpenSubsonic API](SUBSONIC.md) support
## Music discovery ## Music discovery
_LMS_ provides several ways to help you find the music you like: _LMS_ provides several ways to help you find the music you like:
@@ -34,13 +34,6 @@ _LMS_ provides several ways to help you find the music you like:
* Starred _Jazz_ albums * Starred _Jazz_ albums
* ... * ...
## Subsonic API
The API version implemented is 1.16.0 and has been tested on _Android_ using _Subsonic Player_, _Ultrasonic_, _Symfonium_, and _DSub_.
Since _LMS_ uses metadata tags to organize music, a compatibility mode is used to browse the collection when using the directory browsing commands.
The Subsonic API is enabled by default.
__Note__: since _LMS_ may store hashed and salted passwords or may forward authentication requests to external services, it cannot handle the __token authentication__ method. You may need to check your client to make sure to use the __password__ authentication method.
## About tags ## About tags
_LMS_ relies exclusively on tags to organize your music collection. _LMS_ relies exclusively on tags to organize your music collection.
+36
View File
@@ -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
View File
@@ -16,7 +16,14 @@
</ul> </ul>
<div class="tab-content" id="myTabContent"> <div class="tab-content" id="myTabContent">
<div class="tab-pane show active" id="releases" role="tabpanel" aria-labelledby="releases-tab">${releases}</div> <div class="tab-pane show active" id="releases" role="tabpanel" aria-labelledby="releases-tab">${releases}</div>
<div class="tab-pane" id="artists" role="tabpanel" aria-labelledby="artists-tab">${artists}</div> <div class="tab-pane" id="artists" role="tabpanel" aria-labelledby="artists-tab">
<div class="row mb-3">
<div class="col-lg-3">
${link-type class="form-select"}
</div>
</div>
${artists}
</div>
<div class="tab-pane" id="tracks" role="tabpanel" aria-labelledby="tracks-tab">${tracks}</div> <div class="tab-pane" id="tracks" role="tabpanel" aria-labelledby="tracks-tab">${tracks}</div>
</div> </div>
</message> </message>
+1 -1
View File
@@ -491,7 +491,7 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
//MP4 //MP4
else if (TagLib::MP4::File* mp4File {dynamic_cast<TagLib::MP4::File*>(f.file())}) else if (TagLib::MP4::File* mp4File {dynamic_cast<TagLib::MP4::File*>(f.file())})
{ {
auto& coverItem {mp4File->tag()->itemListMap()["covr"]}; TagLib::MP4::Item coverItem {mp4File->tag()->item("covr")};
TagLib::MP4::CoverArtList coverArtList {coverItem.toCoverArtList()}; TagLib::MP4::CoverArtList coverArtList {coverItem.toCoverArtList()};
if (!coverArtList.isEmpty()) if (!coverArtList.isEmpty())
track.hasCover = true; track.hasCover = true;
+139 -133
View File
@@ -28,177 +28,183 @@
#include "SqlQuery.hpp" #include "SqlQuery.hpp"
#include "Utils.hpp" #include "Utils.hpp"
namespace Database { namespace Database
Cluster::Cluster(ObjectPtr<ClusterType> type, std::string_view name)
: _name {std::string {name, 0, _maxNameLength}},
_clusterType {getDboPtr(type)}
{ {
} namespace
{
Wt::Dbo::Query<ClusterId> createQuery(Session& session, const Cluster::FindParameters& params)
{
session.checkSharedLocked();
Cluster::pointer auto query{ session.getDboSession().query<ClusterId>("SELECT DISTINCT c.id FROM cluster c") };
Cluster::create(Session& session, ObjectPtr<ClusterType> type, std::string_view name)
{
return session.getDboSession().add(std::unique_ptr<Cluster> {new Cluster {type, name}});
}
std::size_t if (params.track.isValid() || params.release.isValid())
Cluster::getCount(Session& session) {
{ query.join("track_cluster t_c ON t_c.cluster_id = c.id");
session.checkSharedLocked(); query.join("track t ON t.id = t_c.track_id");
}
return session.getDboSession().query<int>("SELECT COUNT(*) FROM cluster"); if (params.track.isValid())
} query.where("t.id = ?").bind(params.track);
if (params.release.isValid())
query.where("t.release_id = ?").bind(params.release);
RangeResults<ClusterId> if (params.clusterType.isValid())
Cluster::find(Session& session, Range range) query.where("c.cluster_type_id = ?").bind(params.clusterType);
{
session.checkSharedLocked();
auto query {session.getDboSession().query<ClusterId>("SELECT id FROM cluster")};
return Utils::execQuery(query, range); return query;
} }
}
RangeResults<ClusterId> Cluster::Cluster(ObjectPtr<ClusterType> type, std::string_view name)
Cluster::findOrphans(Session& session, Range range) : _name{ std::string {name, 0, _maxNameLength} },
{ _clusterType{ getDboPtr(type) }
session.checkSharedLocked(); {
auto query {session.getDboSession().query<ClusterId>("SELECT DISTINCT c.id FROM cluster c WHERE NOT EXISTS(SELECT 1 FROM track_cluster t_c WHERE t_c.cluster_id = c.id)")}; }
return Utils::execQuery(query, range); Cluster::pointer Cluster::create(Session& session, ObjectPtr<ClusterType> type, std::string_view name)
} {
return session.getDboSession().add(std::unique_ptr<Cluster> {new Cluster{ type, name }});
}
Cluster::pointer std::size_t Cluster::getCount(Session& session)
Cluster::find(Session& session, ClusterId id) {
{ session.checkSharedLocked();
session.checkSharedLocked();
return session.getDboSession().find<Cluster>().where("id = ?").bind(id).resultValue(); return session.getDboSession().query<int>("SELECT COUNT(*) FROM cluster");
} }
void RangeResults<ClusterId> Cluster::find(Session& session, const FindParameters& params)
Cluster::addTrack(ObjectPtr<Track> track) {
{ session.checkSharedLocked();
_tracks.insert(getDboPtr(track)); auto query{ createQuery(session, params) };
}
RangeResults<TrackId> return Utils::execQuery(query, params.range);
Cluster::getTracks(Range range) const }
{
assert(session());
auto query {session()->query<TrackId>("SELECT t.id FROM track t INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id") RangeResults<ClusterId> Cluster::findOrphans(Session& session, Range range)
.where("c.id = ?").bind(getId())}; {
session.checkSharedLocked();
auto query{ session.getDboSession().query<ClusterId>("SELECT DISTINCT c.id FROM cluster c WHERE NOT EXISTS(SELECT 1 FROM track_cluster t_c WHERE t_c.cluster_id = c.id)") };
return Utils::execQuery(query, range); return Utils::execQuery(query, range);
} }
std::size_t Cluster::pointer Cluster::find(Session& session, ClusterId id)
Cluster::getReleasesCount() const {
{ session.checkSharedLocked();
assert(session());
return session()->query<int>("SELECT COUNT(DISTINCT r.id) FROM release r INNER JOIN track t on t.release_id = r.id INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id") return session.getDboSession().find<Cluster>().where("id = ?").bind(id).resultValue();
.where("c.id = ?").bind(getId()); }
}
void Cluster::addTrack(ObjectPtr<Track> track)
{
_tracks.insert(getDboPtr(track));
}
RangeResults<TrackId> Cluster::getTracks(Range range) const
{
assert(session());
auto query{ session()->query<TrackId>("SELECT t.id FROM track t INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id")
.where("c.id = ?").bind(getId()) };
return Utils::execQuery(query, range);
}
std::size_t Cluster::getReleasesCount() const
{
assert(session());
return session()->query<int>("SELECT COUNT(DISTINCT r.id) FROM release r INNER JOIN track t on t.release_id = r.id INNER JOIN cluster c ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id")
.where("c.id = ?").bind(getId());
}
ClusterType::ClusterType(std::string_view name) ClusterType::ClusterType(std::string_view name)
: _name {name} : _name{ name }
{ {
} }
ClusterType::pointer ClusterType::pointer ClusterType::create(Session& session, const std::string& name)
ClusterType::create(Session& session, const std::string& name) {
{ return session.getDboSession().add(std::unique_ptr<ClusterType> {new ClusterType{ name }});
return session.getDboSession().add(std::unique_ptr<ClusterType> {new ClusterType {name}}); }
}
std::size_t std::size_t ClusterType::getCount(Session& session)
ClusterType::getCount(Session& session) {
{ session.checkSharedLocked();
session.checkSharedLocked();
return session.getDboSession().query<int>("SELECT COUNT(*) FROM cluster_type"); return session.getDboSession().query<int>("SELECT COUNT(*) FROM cluster_type");
} }
RangeResults<ClusterTypeId> RangeResults<ClusterTypeId> ClusterType::findOrphans(Session& session, Range range)
ClusterType::findOrphans(Session& session, Range range) {
{ session.checkSharedLocked();
session.checkSharedLocked();
auto query {session.getDboSession().query<ClusterTypeId>( auto query{ session.getDboSession().query<ClusterTypeId>(
"SELECT c_t.id from cluster_type c_t" "SELECT c_t.id from cluster_type c_t"
" LEFT OUTER JOIN cluster c ON c_t.id = c.cluster_type_id") " LEFT OUTER JOIN cluster c ON c_t.id = c.cluster_type_id")
.where("c.id IS NULL")}; .where("c.id IS NULL") };
return Utils::execQuery(query, range); return Utils::execQuery(query, range);
} }
RangeResults<ClusterTypeId> RangeResults<ClusterTypeId> ClusterType::findUsed(Session& session, Range range)
ClusterType::findUsed(Session& session, Range range) {
{ session.checkSharedLocked();
session.checkSharedLocked();
auto query {session.getDboSession().query<ClusterTypeId>( auto query{ session.getDboSession().query<ClusterTypeId>(
"SELECT DISTINCT c_t.id from cluster_type c_t") "SELECT DISTINCT c_t.id from cluster_type c_t")
.join("cluster c ON c_t.id = c.cluster_type_id")}; .join("cluster c ON c_t.id = c.cluster_type_id") };
return Utils::execQuery(query, range); return Utils::execQuery(query, range);
} }
ClusterType::pointer ClusterType::pointer ClusterType::find(Session& session, std::string_view name)
ClusterType::find(Session& session, const std::string& name) {
{ session.checkSharedLocked();
session.checkSharedLocked();
return session.getDboSession().find<ClusterType>().where("name = ?").bind(name).resultValue(); return session.getDboSession().find<ClusterType>().where("name = ?").bind(std::string{ name }).resultValue();
} }
ClusterType::pointer ClusterType::pointer ClusterType::find(Session& session, ClusterTypeId id)
ClusterType::find(Session& session, ClusterTypeId id) {
{ session.checkSharedLocked();
session.checkSharedLocked();
return session.getDboSession().find<ClusterType>().where("id = ?").bind(id).resultValue(); return session.getDboSession().find<ClusterType>().where("id = ?").bind(id).resultValue();
} }
RangeResults<ClusterTypeId> RangeResults<ClusterTypeId> ClusterType::find(Session& session, Range range)
ClusterType::find(Session& session, Range range) {
{ session.checkSharedLocked();
session.checkSharedLocked();
auto query {session.getDboSession().query<ClusterTypeId>("SELECT id from cluster_type")}; auto query{ session.getDboSession().query<ClusterTypeId>("SELECT id from cluster_type") };
return Utils::execQuery(query, range); return Utils::execQuery(query, range);
} }
Cluster::pointer Cluster::pointer ClusterType::getCluster(const std::string& name) const
ClusterType::getCluster(const std::string& name) const {
{ assert(self());
assert(self()); assert(session());
assert(session());
return session()->find<Cluster>() return session()->find<Cluster>()
.where("name = ?").bind(name) .where("name = ?").bind(name)
.where("cluster_type_id = ?").bind(getId()).resultValue(); .where("cluster_type_id = ?").bind(getId()).resultValue();
} }
std::vector<Cluster::pointer> std::vector<Cluster::pointer> ClusterType::getClusters() const
ClusterType::getClusters() const {
{ assert(self());
assert(self()); assert(session());
assert(session());
auto res = session()->find<Cluster>()
.where("cluster_type_id = ?").bind(getId())
.orderBy("name")
.resultList();
return std::vector<Cluster::pointer>(res.begin(), res.end());
}
auto res = session()->find<Cluster>()
.where("cluster_type_id = ?").bind(getId())
.orderBy("name")
.resultList();
return std::vector<Cluster::pointer>(res.begin(), res.end());
}
} // namespace Database } // namespace Database
+438 -436
View File
@@ -35,441 +35,443 @@
namespace Database namespace Database
{ {
Wt::Dbo::Query<ReleaseId> Wt::Dbo::Query<ReleaseId> createQuery(Session& session, const Release::FindParameters& params)
createQuery(Session& session, const Release::FindParameters& params) {
{ auto query{ session.getDboSession().query<ReleaseId>("SELECT DISTINCT r.id from release r") };
auto query {session.getDboSession().query<ReleaseId>("SELECT DISTINCT r.id from release r")};
if (params.sortMethod == ReleaseSortMethod::LastWritten
if (params.sortMethod == ReleaseSortMethod::LastWritten || params.sortMethod == ReleaseSortMethod::Date
|| params.sortMethod == ReleaseSortMethod::Date || params.sortMethod == ReleaseSortMethod::OriginalDate
|| params.sortMethod == ReleaseSortMethod::OriginalDate || params.sortMethod == ReleaseSortMethod::OriginalDateDesc
|| params.sortMethod == ReleaseSortMethod::OriginalDateDesc || params.writtenAfter.isValid()
|| params.writtenAfter.isValid() || params.dateRange
|| params.dateRange || params.artist.isValid())
|| params.artist.isValid()) {
{ query.join("track t ON t.release_id = r.id");
query.join("track t ON t.release_id = r.id"); }
}
if (params.writtenAfter.isValid())
if (params.writtenAfter.isValid()) query.where("t.file_last_write > ?").bind(params.writtenAfter);
query.where("t.file_last_write > ?").bind(params.writtenAfter);
if (params.dateRange)
if (params.dateRange) {
{ query.where("t.date >= ?").bind(params.dateRange->begin);
query.where("t.date >= ?").bind(params.dateRange->begin); query.where("t.date <= ?").bind(params.dateRange->end);
query.where("t.date <= ?").bind(params.dateRange->end); }
}
for (std::string_view keyword : params.keywords)
for (std::string_view keyword : params.keywords) query.where("r.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + Utils::escapeLikeKeyword(keyword) + "%");
query.where("r.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + Utils::escapeLikeKeyword(keyword) + "%");
if (params.starringUser.isValid())
if (params.starringUser.isValid()) {
{ assert(params.scrobbler);
assert(params.scrobbler); query.join("starred_release s_r ON s_r.release_id = r.id")
query.join("starred_release s_r ON s_r.release_id = r.id") .where("s_r.user_id = ?").bind(params.starringUser)
.where("s_r.user_id = ?").bind(params.starringUser) .where("s_r.scrobbler = ?").bind(*params.scrobbler)
.where("s_r.scrobbler = ?").bind(*params.scrobbler) .where("s_r.scrobbling_state <> ?").bind(ScrobblingState::PendingRemove);
.where("s_r.scrobbling_state <> ?").bind(ScrobblingState::PendingRemove); }
}
if (params.artist.isValid())
if (params.artist.isValid()) {
{ query.join("artist a ON a.id = t_a_l.artist_id")
query.join("artist a ON a.id = t_a_l.artist_id") .join("track_artist_link t_a_l ON t_a_l.track_id = t.id")
.join("track_artist_link t_a_l ON t_a_l.track_id = t.id") .where("a.id = ?").bind(params.artist);
.where("a.id = ?").bind(params.artist);
if (!params.trackArtistLinkTypes.empty())
if (!params.trackArtistLinkTypes.empty()) {
{ std::ostringstream oss;
std::ostringstream oss;
bool first{ true };
bool first {true}; for (TrackArtistLinkType linkType : params.trackArtistLinkTypes)
for (TrackArtistLinkType linkType : params.trackArtistLinkTypes) {
{ if (!first)
if (!first) oss << " OR ";
oss << " OR "; oss << "t_a_l.type = ?";
oss << "t_a_l.type = ?"; query.bind(linkType);
query.bind(linkType);
first = false;
first = false; }
} query.where(oss.str());
query.where(oss.str()); }
}
if (!params.excludedTrackArtistLinkTypes.empty())
if (!params.excludedTrackArtistLinkTypes.empty()) {
{ std::ostringstream oss;
std::ostringstream oss; oss << "r.id NOT IN (SELECT DISTINCT r.id FROM release r"
oss << "r.id NOT IN (SELECT DISTINCT r.id FROM release r" " INNER JOIN artist a ON a.id = t_a_l.artist_id"
" INNER JOIN artist a ON a.id = t_a_l.artist_id" " INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id"
" INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id" " INNER JOIN track t ON t.release_id = r.id"
" INNER JOIN track t ON t.release_id = r.id" " WHERE (a.id = ? AND (";
" WHERE (a.id = ? AND (";
query.bind(params.artist);
query.bind(params.artist);
bool first{ true };
bool first {true}; for (const TrackArtistLinkType linkType : params.excludedTrackArtistLinkTypes)
for (const TrackArtistLinkType linkType : params.excludedTrackArtistLinkTypes) {
{ if (!first)
if (!first) oss << " OR ";
oss << " OR "; oss << "t_a_l.type = ?";
oss << "t_a_l.type = ?"; query.bind(linkType);
query.bind(linkType);
first = false;
first = false; }
} oss << ")))";
oss << ")))"; query.where(oss.str());
query.where(oss.str()); }
} }
}
if (!params.clusters.empty())
if (!params.clusters.empty()) {
{ std::ostringstream oss;
std::ostringstream oss; oss << "r.id IN (SELECT DISTINCT r.id FROM release r"
oss << "r.id IN (SELECT DISTINCT r.id FROM release r" " INNER JOIN track t ON t.release_id = r.id"
" INNER JOIN track t ON t.release_id = r.id" " INNER JOIN cluster c ON c.id = t_c.cluster_id"
" INNER JOIN cluster c ON c.id = t_c.cluster_id" " INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
WhereClause clusterClause;
WhereClause clusterClause; for (const ClusterId clusterId : params.clusters)
for (const ClusterId clusterId : params.clusters) {
{ clusterClause.Or(WhereClause("c.id = ?"));
clusterClause.Or(WhereClause("c.id = ?")); query.bind(clusterId);
query.bind(clusterId); }
}
oss << " " << clusterClause.get();
oss << " " << clusterClause.get(); oss << " GROUP BY t.id HAVING COUNT(*) = " << params.clusters.size() << ")";
oss << " GROUP BY t.id HAVING COUNT(*) = " << params.clusters.size() << ")";
query.where(oss.str());
query.where(oss.str()); }
}
if (params.primaryType)
if (params.primaryType) query.where("primary_type = ?").bind(*params.primaryType);
query.where("primary_type = ?").bind(*params.primaryType); if (!params.secondaryTypes.empty())
if (!params.secondaryTypes.empty()) query.where("secondary_type = ?").bind(params.secondaryTypes);
query.where("secondary_type = ?").bind(params.secondaryTypes);
switch (params.sortMethod)
switch (params.sortMethod) {
{ case ReleaseSortMethod::None:
case ReleaseSortMethod::None: break;
break; case ReleaseSortMethod::Name:
case ReleaseSortMethod::Name: query.orderBy("r.name COLLATE NOCASE");
query.orderBy("r.name COLLATE NOCASE"); break;
break; case ReleaseSortMethod::Random:
case ReleaseSortMethod::Random: query.orderBy("RANDOM()");
query.orderBy("RANDOM()"); break;
break; case ReleaseSortMethod::LastWritten:
case ReleaseSortMethod::LastWritten: query.orderBy("t.file_last_write DESC");
query.orderBy("t.file_last_write DESC"); break;
break; case ReleaseSortMethod::Date:
case ReleaseSortMethod::Date: query.orderBy("t.date, r.name COLLATE NOCASE");
query.orderBy("t.date, r.name COLLATE NOCASE"); break;
break; case ReleaseSortMethod::OriginalDate:
case ReleaseSortMethod::OriginalDate: query.orderBy("CASE WHEN t.original_date IS NULL THEN t.date ELSE t.original_date END, t.date, r.name COLLATE NOCASE");
query.orderBy("CASE WHEN t.original_date IS NULL THEN t.date ELSE t.original_date END, t.date, r.name COLLATE NOCASE"); break;
break; case ReleaseSortMethod::OriginalDateDesc:
case ReleaseSortMethod::OriginalDateDesc: query.orderBy("CASE WHEN t.original_date IS NULL THEN t.date ELSE t.original_date END DESC, t.date, r.name COLLATE NOCASE");
query.orderBy("CASE WHEN t.original_date IS NULL THEN t.date ELSE t.original_date END DESC, t.date, r.name COLLATE NOCASE"); break;
break; case ReleaseSortMethod::StarredDateDesc:
case ReleaseSortMethod::StarredDateDesc: assert(params.starringUser.isValid());
assert(params.starringUser.isValid()); query.orderBy("s_r.date_time DESC");
query.orderBy("s_r.date_time DESC"); break;
break; }
}
return query;
return query; }
}
Release::Release(const std::string& name, const std::optional<UUID>& MBID)
Release::Release(const std::string& name, const std::optional<UUID>& MBID) : _name{ std::string(name, 0 , _maxNameLength) },
: _name {std::string(name, 0 , _maxNameLength)}, _MBID{ MBID ? MBID->getAsString() : "" }
_MBID {MBID ? MBID->getAsString() : ""} {
{ }
}
Release::pointer Release::create(Session& session, const std::string& name, const std::optional<UUID>& MBID)
Release::pointer {
Release::create(Session& session, const std::string& name, const std::optional<UUID>& MBID) return session.getDboSession().add(std::unique_ptr<Release> {new Release{ name, MBID }});
{ }
return session.getDboSession().add(std::unique_ptr<Release> {new Release {name, MBID}});
} std::vector<Release::pointer> Release::find(Session& session, const std::string& name)
{
std::vector<Release::pointer> session.checkUniqueLocked();
Release::find(Session& session, const std::string& name)
{ auto res{ session.getDboSession()
session.checkUniqueLocked(); .find<Release>()
.where("name = ?").bind(std::string(name, 0, _maxNameLength))
auto res {session.getDboSession() .resultList() };
.find<Release>()
.where("name = ?").bind( std::string(name, 0, _maxNameLength) ) return std::vector<Release::pointer>(res.begin(), res.end());
.resultList()}; }
return std::vector<Release::pointer>(res.begin(), res.end()); Release::pointer Release::find(Session& session, const UUID& mbid)
} {
session.checkSharedLocked();
Release::pointer
Release::find(Session& session, const UUID& mbid) return session.getDboSession()
{ .find<Release>()
session.checkSharedLocked(); .where("mbid = ?").bind(std::string{ mbid.getAsString() })
.resultValue();;
return session.getDboSession() }
.find<Release>()
.where("mbid = ?").bind(std::string {mbid.getAsString()}) Release::pointer Release::find(Session& session, ReleaseId id)
.resultValue();; {
} session.checkSharedLocked();
Release::pointer return session.getDboSession()
Release::find(Session& session, ReleaseId id) .find<Release>()
{ .where("id = ?").bind(id)
session.checkSharedLocked(); .resultValue();
}
return session.getDboSession()
.find<Release>() bool Release::exists(Session& session, ReleaseId id)
.where("id = ?").bind(id) {
.resultValue(); session.checkSharedLocked();
} return session.getDboSession().query<int>("SELECT 1 FROM release").where("id = ?").bind(id).resultValue() == 1;
}
bool
Release::exists(Session& session, ReleaseId id) std::size_t Release::getCount(Session& session)
{ {
session.checkSharedLocked(); session.checkSharedLocked();
return session.getDboSession().query<int>("SELECT 1 FROM release").where("id = ?").bind(id).resultValue() == 1;
} return session.getDboSession().query<int>("SELECT COUNT(*) FROM release");
}
std::size_t
Release::getCount(Session& session) RangeResults<ReleaseId> Release::findOrderedByArtist(Session& session, Range range)
{ {
session.checkSharedLocked(); session.checkSharedLocked();
return session.getDboSession().query<int>("SELECT COUNT(*) FROM release"); // TODO merge with find
} auto query{ session.getDboSession().query<ReleaseId>(
"SELECT DISTINCT r.id FROM release r"
RangeResults<ReleaseId> " INNER JOIN track t ON r.id = t.release_id"
Release::findOrderedByArtist(Session& session, Range range) " INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id"
{ " INNER JOIN artist a ON t_a_l.artist_id = a.id")
session.checkSharedLocked(); .orderBy("a.name COLLATE NOCASE, r.name COLLATE NOCASE") };
// TODO merge with find return Utils::execQuery(query, range);
auto query {session.getDboSession().query<ReleaseId>( }
"SELECT DISTINCT r.id FROM release r"
" INNER JOIN track t ON r.id = t.release_id" RangeResults<ReleaseId> Release::findOrphans(Session& session, Range range)
" INNER JOIN track_artist_link t_a_l ON t_a_l.track_id = t.id" {
" INNER JOIN artist a ON t_a_l.artist_id = a.id") session.checkSharedLocked();
.orderBy("a.name COLLATE NOCASE, r.name COLLATE NOCASE")};
auto query{ session.getDboSession().query<ReleaseId>("select r.id from release r LEFT OUTER JOIN Track t ON r.id = t.release_id WHERE t.id IS NULL") };
return Utils::execQuery(query, range); return Utils::execQuery(query, range);
} }
RangeResults<ReleaseId> RangeResults<ReleaseId> Release::find(Session& session, const FindParameters& params)
Release::findOrphans(Session& session, Range range) {
{ session.checkSharedLocked();
session.checkSharedLocked();
auto query{ createQuery(session, params) };
auto query {session.getDboSession().query<ReleaseId>("select r.id from release r LEFT OUTER JOIN Track t ON r.id = t.release_id WHERE t.id IS NULL")};
return Utils::execQuery(query, range); return Utils::execQuery(query, params.range);
} }
RangeResults<ReleaseId> std::size_t Release::getDiscCount() const
Release::find(Session& session, const FindParameters& params) {
{ assert(session());
session.checkSharedLocked(); int res{ session()->query<int>("SELECT COUNT(DISTINCT disc_number) FROM track t")
.join("release r ON r.id = t.release_id")
auto query {createQuery(session, params)}; .where("r.id = ?")
.bind(getId()) };
return Utils::execQuery(query, params.range);
} return res;
}
std::size_t
Release::getDiscCount() const std::vector<DiscInfo> Release::getDiscs() const
{ {
assert(session()); assert(session());
int res {session()->query<int>("SELECT COUNT(DISTINCT disc_number) FROM track t") using ResultType = std::tuple<int, std::string>;
.join("release r ON r.id = t.release_id") auto results{ session()->query<ResultType>("SELECT DISTINCT disc_number, disc_subtitle FROM track t")
.where("r.id = ?") .join("release r ON r.id = t.release_id")
.bind(getId())}; .where("r.id = ?")
.orderBy("disc_number")
return res; .bind(getId())
} .resultList() };
std::optional<int> std::vector<DiscInfo> discs;
Release::getReleaseYear(bool original) const for (const auto& res : results)
{ discs.emplace_back(DiscInfo{ static_cast<std::size_t>(std::get<int>(res)), std::get<std::string>(res) });
assert(session());
return discs;
const char* field {original ? "original_date" : "date"}; }
auto dates {session()->query<Wt::WDate>( Wt::WDate Release::getReleaseDate() const
std::string {"SELECT "} + "t." + field + " FROM track t INNER JOIN release r ON r.id = t.release_id") {
.where("r.id = ?") return getReleaseDate(false);
.groupBy(field) }
.bind(getId())
.resultList()}; Wt::WDate Release::getOriginalReleaseDate() const
{
// various dates => no date return getReleaseDate(true);
if (dates.empty() || dates.size() > 1) }
return std::nullopt;
Wt::WDate Release::getReleaseDate(bool original) const
auto date {dates.front().year()}; {
assert(session());
if (date > 0)
return date; const char* field{ original ? "original_date" : "date" };
return std::nullopt; auto dates{ session()->query<Wt::WDate>(
} std::string {"SELECT "} + "t." + field + " FROM track t INNER JOIN release r ON r.id = t.release_id")
.where("r.id = ?")
std::optional<std::string> .groupBy(field)
Release::getCopyright() const .bind(getId())
{ .resultList() };
assert(session());
// various dates => invalid date
Wt::Dbo::collection<std::string> copyrights = session()->query<std::string> if (dates.empty() || dates.size() > 1)
("SELECT copyright FROM track t INNER JOIN release r ON r.id = t.release_id") return {};
.where("r.id = ?")
.groupBy("copyright") return dates.front();
.bind(getId()); }
std::vector<std::string> values(copyrights.begin(), copyrights.end()); std::optional<std::string> Release::getCopyright() const
{
// various copyrights => no copyright assert(session());
if (values.empty() || values.size() > 1 || values.front().empty())
return std::nullopt; Wt::Dbo::collection<std::string> copyrights = session()->query<std::string>
("SELECT copyright FROM track t INNER JOIN release r ON r.id = t.release_id")
return values.front(); .where("r.id = ?")
} .groupBy("copyright")
.bind(getId());
std::optional<std::string>
Release::getCopyrightURL() const std::vector<std::string> values(copyrights.begin(), copyrights.end());
{
assert(session()); // various copyrights => no copyright
if (values.empty() || values.size() > 1 || values.front().empty())
Wt::Dbo::collection<std::string> copyrights = session()->query<std::string> return std::nullopt;
("SELECT copyright_url FROM track t INNER JOIN release r ON r.id = t.release_id")
.where("r.id = ?") return values.front();
.groupBy("copyright_url") }
.bind(getId());
std::optional<std::string> Release::getCopyrightURL() const
std::vector<std::string> values(copyrights.begin(), copyrights.end()); {
assert(session());
// various copyright URLs => no copyright URL
if (values.empty() || values.size() > 1 || values.front().empty()) Wt::Dbo::collection<std::string> copyrights = session()->query<std::string>
return std::nullopt; ("SELECT copyright_url FROM track t INNER JOIN release r ON r.id = t.release_id")
.where("r.id = ?")
return values.front(); .groupBy("copyright_url")
} .bind(getId());
std::vector<Artist::pointer> std::vector<std::string> values(copyrights.begin(), copyrights.end());
Release::getArtists(TrackArtistLinkType linkType) const
{ // various copyright URLs => no copyright URL
assert(session()); if (values.empty() || values.size() > 1 || values.front().empty())
return std::nullopt;
auto res {session()->query<Wt::Dbo::ptr<Artist>>(
"SELECT DISTINCT a FROM artist a" return values.front();
" INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id" }
" INNER JOIN track t ON t.id = t_a_l.track_id"
" INNER JOIN release r ON r.id = t.release_id") std::vector<Artist::pointer> Release::getArtists(TrackArtistLinkType linkType) const
.where("r.id = ?").bind(getId()) {
.where("t_a_l.type = ?").bind(linkType) assert(session());
.resultList()};
auto res{ session()->query<Wt::Dbo::ptr<Artist>>(
return std::vector<Artist::pointer>(res.begin(), res.end()); "SELECT DISTINCT a FROM artist a"
} " INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id"
" INNER JOIN track t ON t.id = t_a_l.track_id"
std::vector<Release::pointer> " INNER JOIN release r ON r.id = t.release_id")
Release::getSimilarReleases(std::optional<std::size_t> offset, std::optional<std::size_t> count) const .where("r.id = ?").bind(getId())
{ .where("t_a_l.type = ?").bind(linkType)
assert(session()); .resultList() };
auto res {session()->query<Wt::Dbo::ptr<Release>>( return std::vector<Artist::pointer>(res.begin(), res.end());
"SELECT r FROM release r" }
" INNER JOIN track t ON t.release_id = r.id"
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id" std::vector<Release::pointer> Release::getSimilarReleases(std::optional<std::size_t> offset, std::optional<std::size_t> count) const
" WHERE " {
" t_c.cluster_id IN (SELECT c.id from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN release r ON r.id = t.release_id WHERE r.id = ?)" assert(session());
" AND r.id <> ?"
) auto res{ session()->query<Wt::Dbo::ptr<Release>>(
.bind(getId()) "SELECT r FROM release r"
.bind(getId()) " INNER JOIN track t ON t.release_id = r.id"
.groupBy("r.id") " INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
.orderBy("COUNT(*) DESC, RANDOM()") " WHERE "
.limit(count ? static_cast<int>(*count) : -1) " t_c.cluster_id IN (SELECT c.id from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN release r ON r.id = t.release_id WHERE r.id = ?)"
.offset(offset ? static_cast<int>(*offset) : -1) " AND r.id <> ?"
.resultList()}; )
.bind(getId())
return std::vector<pointer>(res.begin(), res.end()); .bind(getId())
} .groupBy("r.id")
.orderBy("COUNT(*) DESC, RANDOM()")
bool .limit(count ? static_cast<int>(*count) : -1)
Release::hasVariousArtists() const .offset(offset ? static_cast<int>(*offset) : -1)
{ .resultList() };
// TODO optimize
return getArtists().size() > 1; return std::vector<pointer>(res.begin(), res.end());
} }
std::size_t bool Release::hasVariousArtists() const
Release::getTracksCount() const {
{ // TODO optimize
return _tracks.size(); return getArtists().size() > 1;
} }
std::chrono::milliseconds std::size_t Release::getTracksCount() const
Release::getDuration() const {
{ return _tracks.size();
assert(session()); }
using milli = std::chrono::duration<int, std::milli>; std::chrono::milliseconds Release::getDuration() const
{
Wt::Dbo::Query<milli> query {session()->query<milli>("SELECT COALESCE(SUM(duration), 0) FROM track t INNER JOIN release r ON t.release_id = r.id") assert(session());
.where("r.id = ?").bind(getId())};
using milli = std::chrono::duration<int, std::milli>;
return query.resultValue();
} Wt::Dbo::Query<milli> query{ session()->query<milli>("SELECT COALESCE(SUM(duration), 0) FROM track t INNER JOIN release r ON t.release_id = r.id")
.where("r.id = ?").bind(getId()) };
Wt::WDateTime
Release::getLastWritten() const return query.resultValue();
{ }
assert(session());
Wt::WDateTime Release::getLastWritten() const
Wt::Dbo::Query<Wt::WDateTime> query {session()->query<Wt::WDateTime>("SELECT COALESCE(MAX(file_last_write), '1970-01-01T00:00:00') FROM track t INNER JOIN release r ON t.release_id = r.id") {
.where("r.id = ?").bind(getId())}; assert(session());
return query.resultValue(); Wt::Dbo::Query<Wt::WDateTime> query{ session()->query<Wt::WDateTime>("SELECT COALESCE(MAX(file_last_write), '1970-01-01T00:00:00') FROM track t INNER JOIN release r ON t.release_id = r.id")
} .where("r.id = ?").bind(getId()) };
std::vector<std::vector<Cluster::pointer>> return query.resultValue();
Release::getClusterGroups(const std::vector<ClusterType::pointer>& clusterTypes, std::size_t size) const }
{
assert(session()); std::vector<std::vector<Cluster::pointer>> Release::getClusterGroups(const std::vector<ClusterType::pointer>& clusterTypes, std::size_t size) const
{
WhereClause where; assert(session());
std::ostringstream oss; WhereClause where;
oss << "SELECT c from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN cluster_type c_type ON c.cluster_type_id = c_type.id INNER JOIN release r ON t.release_id = r.id "; std::ostringstream oss;
where.And(WhereClause("r.id = ?")).bind(getId().toString()); oss << "SELECT c from cluster c INNER JOIN track t ON c.id = t_c.cluster_id INNER JOIN track_cluster t_c ON t_c.track_id = t.id INNER JOIN cluster_type c_type ON c.cluster_type_id = c_type.id INNER JOIN release r ON t.release_id = r.id ";
{
WhereClause clusterClause; where.And(WhereClause("r.id = ?")).bind(getId().toString());
for (auto clusterType : clusterTypes) {
clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterType->getId().toString()); WhereClause clusterClause;
where.And(clusterClause); for (auto clusterType : clusterTypes)
} clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterType->getId().toString());
oss << " " << where.get(); where.And(clusterClause);
oss << " GROUP BY c.id ORDER BY COUNT(c.id) DESC"; }
oss << " " << where.get();
auto query {session()->query<Wt::Dbo::ptr<Cluster>>(oss.str())}; oss << " GROUP BY c.id ORDER BY COUNT(c.id) DESC";
for (const std::string& bindArg : where.getBindArgs())
query.bind(bindArg); auto query{ session()->query<Wt::Dbo::ptr<Cluster>>(oss.str()) };
for (const std::string& bindArg : where.getBindArgs())
auto queryRes {query.resultList()}; query.bind(bindArg);
std::map<ClusterTypeId, std::vector<Cluster::pointer>> clustersByType; auto queryRes{ query.resultList() };
for (const Wt::Dbo::ptr<Cluster>& cluster : queryRes)
{ std::map<ClusterTypeId, std::vector<Cluster::pointer>> clustersByType;
if (clustersByType[cluster->getType()->getId()].size() < size) for (const Wt::Dbo::ptr<Cluster>& cluster : queryRes)
clustersByType[cluster->getType()->getId()].push_back(cluster); {
} if (clustersByType[cluster->getType()->getId()].size() < size)
clustersByType[cluster->getType()->getId()].push_back(cluster);
std::vector<std::vector<Cluster::pointer>> res; }
for (const auto& [clusterTypeId, clusters] : clustersByType)
res.push_back(clusters); std::vector<std::vector<Cluster::pointer>> res;
for (const auto& [clusterTypeId, clusters] : clustersByType)
return res; res.push_back(clusters);
}
return res;
}
} // namespace Database } // namespace Database
@@ -28,74 +28,84 @@
namespace Database namespace Database
{ {
namespace
{
Wt::Dbo::Query<TrackArtistLinkId> createQuery(Session& session, const TrackArtistLink::FindParameters& params)
{
session.checkSharedLocked();
static auto query{ session.getDboSession().query<TrackArtistLinkId>("SELECT DISTINCT t_a_l.id FROM track_artist_link t_a_l") };
Wt::Dbo::Query<TrackArtistLinkId>
createQuery(Session& session, const TrackArtistLink::FindParameters& params)
{
session.checkSharedLocked();
auto query {session.getDboSession().query<TrackArtistLinkId>("SELECT DISTINCT t_a_l.id FROM track_artist_link t_a_l")}; if (params.linkType)
query.where("t_a_l.type = ?").bind(*params.linkType);
if (params.linkType) if (params.track.isValid() || params.release.isValid())
query.where("t_a_l.type = ?").bind(*params.linkType); query.join("track t ON t.id = t_a_l.track_id");
if (params.track.isValid() || params.release.isValid()) if (params.artist.isValid())
query.join("track t ON t.id = t_a_l.track_id"); query.join("artist a ON a.id = t_a_l.artist_id");
if (params.track.isValid()) if (params.release.isValid())
query.where("t.id = ?").bind(params.track); query.where("t.release_id = ?").bind(params.release);
if (params.release.isValid()) if (params.track.isValid())
query.where("t.release_id = ?").bind(params.release); query.where("t.id = ?").bind(params.track);
return query; return query;
} }
}
TrackArtistLink::TrackArtistLink(ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType) TrackArtistLink::TrackArtistLink(ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType)
: _type {type} : _type{ type }
, _subType {subType} , _subType{ subType }
, _track {getDboPtr(track)} , _track{ getDboPtr(track) }
, _artist {getDboPtr(artist)} , _artist{ getDboPtr(artist) }
{ {
} }
TrackArtistLink::pointer TrackArtistLink::pointer TrackArtistLink::create(Session& session, ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType)
TrackArtistLink::create(Session& session, ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType) {
{ session.checkUniqueLocked();
session.checkUniqueLocked();
TrackArtistLink::pointer res {session.getDboSession().add(std::make_unique<TrackArtistLink>(track, artist, type, subType))}; TrackArtistLink::pointer res{ session.getDboSession().add(std::make_unique<TrackArtistLink>(track, artist, type, subType)) };
session.getDboSession().flush(); session.getDboSession().flush();
return res; return res;
} }
TrackArtistLink::pointer TrackArtistLink::pointer TrackArtistLink::find(Session& session, TrackArtistLinkId id)
TrackArtistLink::find(Session& session, TrackArtistLinkId id) {
{ session.checkSharedLocked();
session.checkSharedLocked(); return session.getDboSession().find<TrackArtistLink>().where("id = ?").bind(id).resultValue();
return session.getDboSession().find<TrackArtistLink>().where("id = ?").bind(id).resultValue(); }
}
RangeResults<TrackArtistLinkId> RangeResults<TrackArtistLinkId> TrackArtistLink::find(Session& session, const FindParameters& params)
TrackArtistLink::find(Session& session, const FindParameters& params) {
{ session.checkSharedLocked();
session.checkSharedLocked();
auto query {createQuery(session, params)}; auto query{ createQuery(session, params) };
return Utils::execQuery(query, params.range); return Utils::execQuery(query, params.range);
} }
EnumSet<TrackArtistLinkType> TrackArtistLink::findUsedTypes(Session& session)
{
session.checkSharedLocked();
EnumSet<TrackArtistLinkType> auto res{ session.getDboSession().query<TrackArtistLinkType>("SELECT DISTINCT type from track_artist_link").resultList() };
TrackArtistLink::findUsedTypes(Session& session)
{
session.checkSharedLocked();
auto res {session.getDboSession().query<TrackArtistLinkType>("SELECT DISTINCT type from track_artist_link").resultList()}; return EnumSet<TrackArtistLinkType>(std::begin(res), std::end(res));
}
return EnumSet<TrackArtistLinkType>(std::begin(res), std::end(res)); EnumSet<TrackArtistLinkType> TrackArtistLink::findUsedTypes(Session& session, ArtistId artistId)
} {
session.checkSharedLocked();
auto res{ session.getDboSession()
.query<TrackArtistLinkType>("SELECT DISTINCT type from track_artist_link")
.where("artist_id = ?").bind(artistId)
.resultList() };
return EnumSet<TrackArtistLinkType>(std::begin(res), std::end(res));
}
} }
@@ -26,100 +26,114 @@
#include <Wt/Dbo/Dbo.h> #include <Wt/Dbo/Dbo.h>
#include <Wt/WDateTime.h> #include <Wt/WDateTime.h>
#include "services/database/Object.hpp"
#include "services/database/ClusterId.hpp" #include "services/database/ClusterId.hpp"
#include "services/database/Object.hpp"
#include "services/database/Release.hpp"
#include "services/database/TrackId.hpp" #include "services/database/TrackId.hpp"
#include "services/database/Types.hpp" #include "services/database/Types.hpp"
namespace Database { namespace Database {
class Track; class Track;
class ClusterType; class ClusterType;
class ScanSettings; class ScanSettings;
class Session; class Session;
class Cluster final : public Object<Cluster, ClusterId> class Cluster final : public Object<Cluster, ClusterId>
{ {
public: public:
Cluster() = default; struct FindParameters
{
Range range;
ClusterTypeId clusterType; // if non empty, clusters that belong to this cluster type
TrackId track; // if set, clusters involved in this track
ReleaseId release; // if set, clusters involved in this release
// Find utility FindParameters& setRange(Range _range) { range = _range; return *this; }
static std::size_t getCount(Session& session); FindParameters& setClusterType(ClusterTypeId _clusterType) { clusterType = _clusterType; return *this; }
static RangeResults<ClusterId> find(Session& session, Range range); FindParameters& setTrack(TrackId _track) { track = _track; return *this; }
static pointer find(Session& session, ClusterId id); FindParameters& setRelease(ReleaseId _release) { release = _release; return *this; }
static RangeResults<ClusterId> findOrphans(Session& session, Range range); };
// Accessors Cluster() = default;
const std::string& getName() const { return _name; }
ObjectPtr<ClusterType> getType() const { return _clusterType; }
std::size_t getTracksCount() const { return _tracks.size(); }
RangeResults<TrackId> getTracks(Range range) const;
std::size_t getReleasesCount() const;
void addTrack(ObjectPtr<Track> track); // Find utility
static std::size_t getCount(Session& session);
static RangeResults<ClusterId> find(Session& session, const FindParameters& range);
static pointer find(Session& session, ClusterId id);
static RangeResults<ClusterId> findOrphans(Session& session, Range range);
template<class Action> // Accessors
void persist(Action& a) const std::string& getName() const { return _name; }
{ ObjectPtr<ClusterType> getType() const { return _clusterType; }
Wt::Dbo::field(a, _name, "name"); std::size_t getTracksCount() const { return _tracks.size(); }
RangeResults<TrackId> getTracks(Range range) const;
std::size_t getReleasesCount() const;
Wt::Dbo::belongsTo(a, _clusterType, "cluster_type", Wt::Dbo::OnDeleteCascade); void addTrack(ObjectPtr<Track> track);
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade);
}
private: template<class Action>
friend class Session; void persist(Action& a)
Cluster(ObjectPtr<ClusterType> type, std::string_view name); {
static pointer create(Session& session, ObjectPtr<ClusterType> type, std::string_view name); Wt::Dbo::field(a, _name, "name");
static const std::size_t _maxNameLength = 128; Wt::Dbo::belongsTo(a, _clusterType, "cluster_type", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade);
}
std::string _name; private:
friend class Session;
Cluster(ObjectPtr<ClusterType> type, std::string_view name);
static pointer create(Session& session, ObjectPtr<ClusterType> type, std::string_view name);
Wt::Dbo::ptr<ClusterType> _clusterType; static const std::size_t _maxNameLength = 128;
Wt::Dbo::collection< Wt::Dbo::ptr<Track> > _tracks;
}; std::string _name;
Wt::Dbo::ptr<ClusterType> _clusterType;
Wt::Dbo::collection< Wt::Dbo::ptr<Track> > _tracks;
};
class ClusterType final : public Object<ClusterType, ClusterTypeId> class ClusterType final : public Object<ClusterType, ClusterTypeId>
{ {
public: public:
ClusterType() = default; ClusterType() = default;
// Getters // Getters
static std::size_t getCount(Session& session); static std::size_t getCount(Session& session);
static RangeResults<ClusterTypeId> find(Session& session, Range range); static RangeResults<ClusterTypeId> find(Session& session, Range range);
static pointer find(Session& session, const std::string& name); static pointer find(Session& session, std::string_view name);
static pointer find(Session& session, ClusterTypeId id); static pointer find(Session& session, ClusterTypeId id);
static RangeResults<ClusterTypeId> findOrphans(Session& session, Range range); static RangeResults<ClusterTypeId> findOrphans(Session& session, Range range);
static RangeResults<ClusterTypeId> findUsed(Session& session, Range range); static RangeResults<ClusterTypeId> findUsed(Session& session, Range range);
static void remove(Session& session, const std::string& name); static void remove(Session& session, const std::string& name);
// Accessors // Accessors
const std::string& getName() const { return _name; } const std::string& getName() const { return _name; }
std::vector<Cluster::pointer> getClusters() const; std::vector<Cluster::pointer> getClusters() const;
Cluster::pointer getCluster(const std::string& name) const; Cluster::pointer getCluster(const std::string& name) const;
template<class Action> template<class Action>
void persist(Action& a) void persist(Action& a)
{ {
Wt::Dbo::field(a, _name, "name"); Wt::Dbo::field(a, _name, "name");
Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToOne, "cluster_type"); Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToOne, "cluster_type");
Wt::Dbo::belongsTo(a, _scanSettings, "scan_settings", Wt::Dbo::OnDeleteCascade); Wt::Dbo::belongsTo(a, _scanSettings, "scan_settings", Wt::Dbo::OnDeleteCascade);
} }
private: private:
friend class Session; friend class Session;
ClusterType(std::string_view name); ClusterType(std::string_view name);
static pointer create(Session& session, const std::string& name); static pointer create(Session& session, const std::string& name);
static const std::size_t _maxNameLength = 128; static const std::size_t _maxNameLength = 128;
std::string _name; std::string _name;
Wt::Dbo::collection< Wt::Dbo::ptr<Cluster> > _clusters; Wt::Dbo::collection< Wt::Dbo::ptr<Cluster> > _clusters;
Wt::Dbo::ptr<ScanSettings> _scanSettings; Wt::Dbo::ptr<ScanSettings> _scanSettings;
}; };
} // namespace Database } // namespace Database
@@ -37,124 +37,127 @@
namespace Database namespace Database
{ {
class Artist; class Artist;
class Cluster; class Cluster;
class ClusterType; class ClusterType;
class Release; class Release;
class Session; class Session;
class Track; class Track;
class User; class User;
class Release final : public Object<Release, ReleaseId> class Release final : public Object<Release, ReleaseId>
{ {
public: public:
struct FindParameters struct FindParameters
{ {
std::vector<ClusterId> clusters; // if non empty, releases that belong to these clusters std::vector<ClusterId> clusters; // if non empty, releases that belong to these clusters
std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords
ReleaseSortMethod sortMethod {ReleaseSortMethod::None}; ReleaseSortMethod sortMethod{ ReleaseSortMethod::None };
Range range; Range range;
Wt::WDateTime writtenAfter; Wt::WDateTime writtenAfter;
std::optional<DateRange> dateRange; std::optional<DateRange> dateRange;
UserId starringUser; // only releases starred by this user UserId starringUser; // only releases starred by this user
std::optional<Scrobbler> scrobbler; // and for this scrobbler std::optional<Scrobbler> scrobbler; // and for this scrobbler
ArtistId artist; // only releases that involved this user ArtistId artist; // only releases that involved this user
EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types
EnumSet<TrackArtistLinkType> excludedTrackArtistLinkTypes; // but not for these link types EnumSet<TrackArtistLinkType> excludedTrackArtistLinkTypes; // but not for these link types
std::optional<ReleaseTypePrimary> primaryType; // if, set, matching this primary type std::optional<ReleaseTypePrimary> primaryType; // if, set, matching this primary type
EnumSet<ReleaseTypeSecondary> secondaryTypes; // Matching all this (if any) EnumSet<ReleaseTypeSecondary> secondaryTypes; // Matching all this (if any)
FindParameters& setClusters(const std::vector<ClusterId>& _clusters) { clusters = _clusters; return *this; } FindParameters& setClusters(const std::vector<ClusterId>& _clusters) { clusters = _clusters; return *this; }
FindParameters& setKeywords(const std::vector<std::string_view>& _keywords) { keywords = _keywords; return *this; } FindParameters& setKeywords(const std::vector<std::string_view>& _keywords) { keywords = _keywords; return *this; }
FindParameters& setSortMethod(ReleaseSortMethod _sortMethod) {sortMethod = _sortMethod; return *this; } FindParameters& setSortMethod(ReleaseSortMethod _sortMethod) { sortMethod = _sortMethod; return *this; }
FindParameters& setRange(Range _range) {range = _range; return *this; } FindParameters& setRange(Range _range) { range = _range; return *this; }
FindParameters& setWrittenAfter(const Wt::WDateTime& _after) {writtenAfter = _after; return *this; } FindParameters& setWrittenAfter(const Wt::WDateTime& _after) { writtenAfter = _after; return *this; }
FindParameters& setDateRange(const std::optional<DateRange>& _dateRange) {dateRange = _dateRange; return *this; } FindParameters& setDateRange(const std::optional<DateRange>& _dateRange) { dateRange = _dateRange; return *this; }
FindParameters& setStarringUser(UserId _user, Scrobbler _scrobbler) { starringUser = _user; scrobbler = _scrobbler; return *this; } FindParameters& setStarringUser(UserId _user, Scrobbler _scrobbler) { starringUser = _user; scrobbler = _scrobbler; return *this; }
FindParameters& setArtist(ArtistId _artist, EnumSet<TrackArtistLinkType> _trackArtistLinkTypes = {}, EnumSet<TrackArtistLinkType> _excludedTrackArtistLinkTypes = {}) FindParameters& setArtist(ArtistId _artist, EnumSet<TrackArtistLinkType> _trackArtistLinkTypes = {}, EnumSet<TrackArtistLinkType> _excludedTrackArtistLinkTypes = {})
{ {
artist = _artist; artist = _artist;
trackArtistLinkTypes = _trackArtistLinkTypes; trackArtistLinkTypes = _trackArtistLinkTypes;
excludedTrackArtistLinkTypes = _excludedTrackArtistLinkTypes; excludedTrackArtistLinkTypes = _excludedTrackArtistLinkTypes;
return *this; return *this;
} }
}; };
Release() = default; Release() = default;
// Accessors // Accessors
static std::size_t getCount(Session& session); static std::size_t getCount(Session& session);
static bool exists(Session& session, ReleaseId id); static bool exists(Session& session, ReleaseId id);
static pointer find(Session& session, const UUID& MBID); static pointer find(Session& session, const UUID& MBID);
static std::vector<pointer> find(Session& session, const std::string& name); static std::vector<pointer> find(Session& session, const std::string& name);
static pointer find(Session& session, ReleaseId id); static pointer find(Session& session, ReleaseId id);
static RangeResults<ReleaseId> find(Session& session, const FindParameters& parameters); static RangeResults<ReleaseId> find(Session& session, const FindParameters& parameters);
static RangeResults<ReleaseId> findOrphans(Session& session, Range range); // no track related static RangeResults<ReleaseId> findOrphans(Session& session, Range range); // no track related
static RangeResults<ReleaseId> findOrderedByArtist(Session& session, Range range); static RangeResults<ReleaseId> findOrderedByArtist(Session& session, Range range);
std::size_t getTracksCount() const; // Get the cluster of the tracks that belong to this release
// Each clusters are grouped by cluster type, sorted by the number of occurence (max to min)
// size is the max number of cluster per cluster type
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(const std::vector<ObjectPtr<ClusterType>>& clusterTypes, std::size_t size) const;
// Get the cluster of the tracks that belong to this release // Utility functions (if all tracks have the same values, which is legit to not be the case)
// Each clusters are grouped by cluster type, sorted by the number of occurence (max to min) Wt::WDate getReleaseDate() const;
// size is the max number of cluster per cluster type Wt::WDate getOriginalReleaseDate() const;
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(const std::vector<ObjectPtr<ClusterType>>& clusterTypes, std::size_t size) const; std::optional<std::string> getCopyright() const;
std::optional<std::string> getCopyrightURL() const;
// Utility functions (if all tracks have the same values, which is legit to not be the case) // Accessors
std::optional<int> getReleaseYear(bool originalDate = false) const; const std::string& getName() const { return _name; }
std::optional<std::string> getCopyright() const; std::optional<UUID> getMBID() const { return UUID::fromString(_MBID); }
std::optional<std::string> getCopyrightURL() const; std::optional<std::size_t> getTotalDisc() const { return _totalDisc; }
std::size_t getDiscCount() const; // may not be total disc (if incomplete for example)
std::vector<DiscInfo> getDiscs() const;
std::chrono::milliseconds getDuration() const;
Wt::WDateTime getLastWritten() const;
std::optional<ReleaseTypePrimary> getPrimaryType() const { return _primaryType; }
EnumSet<ReleaseTypeSecondary> getSecondaryTypes() const { return _secondaryTypes; }
std::size_t getTracksCount() const;
// Accessors // Setters
const std::string& getName() const { return _name; } void setName(std::string_view name) { _name = name; }
std::optional<UUID> getMBID() const { return UUID::fromString(_MBID); } void setMBID(const std::optional<UUID>& mbid) { _MBID = mbid ? mbid->getAsString() : ""; }
std::optional<std::size_t> getTotalDisc() const { return _totalDisc; } void setTotalDisc(std::optional<int> totalDisc) { _totalDisc = totalDisc; }
std::size_t getDiscCount() const; // may not be total disc (if incomplete for example) void setPrimaryType(std::optional<ReleaseTypePrimary> type) { _primaryType = type; }
std::chrono::milliseconds getDuration() const; void setSecondaryTypes(EnumSet<ReleaseTypeSecondary> types) { _secondaryTypes = types; }
Wt::WDateTime getLastWritten() const;
std::optional<ReleaseTypePrimary> getPrimaryType() const { return _primaryType; }
EnumSet<ReleaseTypeSecondary> getSecondaryTypes() const { return _secondaryTypes; }
// Setters // Get the artists of this release
void setName(std::string_view name) { _name = name; } std::vector<ObjectPtr<Artist>> getArtists(TrackArtistLinkType type = TrackArtistLinkType::Artist) const;
void setMBID(const std::optional<UUID>& mbid) { _MBID = mbid ? mbid->getAsString() : ""; } std::vector<ObjectPtr<Artist>> getReleaseArtists() const { return getArtists(TrackArtistLinkType::ReleaseArtist); }
void setTotalDisc(std::optional<int> totalDisc) { _totalDisc = totalDisc; } bool hasVariousArtists() const;
void setPrimaryType(std::optional<ReleaseTypePrimary> type) { _primaryType = type; } std::vector<pointer> getSimilarReleases(std::optional<std::size_t> offset = {}, std::optional<std::size_t> count = {}) const;
void setSecondaryTypes(EnumSet<ReleaseTypeSecondary> types) { _secondaryTypes = types; }
// Get the artists of this release
std::vector<ObjectPtr<Artist>> getArtists(TrackArtistLinkType type = TrackArtistLinkType::Artist) const;
std::vector<ObjectPtr<Artist>> getReleaseArtists() const { return getArtists(TrackArtistLinkType::ReleaseArtist); }
bool hasVariousArtists() const;
std::vector<pointer> getSimilarReleases(std::optional<std::size_t> offset = {}, std::optional<std::size_t> count = {}) const;
template<class Action> template<class Action>
void persist(Action& a) void persist(Action& a)
{ {
Wt::Dbo::field(a, _name, "name"); Wt::Dbo::field(a, _name, "name");
Wt::Dbo::field(a, _MBID, "mbid"); Wt::Dbo::field(a, _MBID, "mbid");
Wt::Dbo::field(a, _totalDisc, "total_disc"); Wt::Dbo::field(a, _totalDisc, "total_disc");
Wt::Dbo::field(a, _primaryType, "primary_type"); Wt::Dbo::field(a, _primaryType, "primary_type");
Wt::Dbo::field(a, _secondaryTypes, "secondary_types"); Wt::Dbo::field(a, _secondaryTypes, "secondary_types");
Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToOne, "release"); Wt::Dbo::hasMany(a, _tracks, Wt::Dbo::ManyToOne, "release");
} }
private: private:
friend class Session; friend class Session;
Release(const std::string& name, const std::optional<UUID>& MBID = {}); Release(const std::string& name, const std::optional<UUID>& MBID = {});
static pointer create(Session& session, const std::string& name, const std::optional<UUID>& MBID = {}); static pointer create(Session& session, const std::string& name, const std::optional<UUID>& MBID = {});
static constexpr std::size_t _maxNameLength {128}; Wt::WDate getReleaseDate(bool original) const;
std::string _name; static constexpr std::size_t _maxNameLength{ 128 };
std::string _MBID;
std::optional<int> _totalDisc {};
std::optional<ReleaseTypePrimary> _primaryType;
EnumSet<ReleaseTypeSecondary> _secondaryTypes;
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> _tracks; // Tracks in the release std::string _name;
}; std::string _MBID;
std::optional<int> _totalDisc{};
std::optional<ReleaseTypePrimary> _primaryType;
EnumSet<ReleaseTypeSecondary> _secondaryTypes;
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> _tracks; // Tracks in the release
};
} // namespace Database } // namespace Database
@@ -24,6 +24,7 @@
#include <Wt/Dbo/Dbo.h> #include <Wt/Dbo/Dbo.h>
#include "services/database/ArtistId.hpp"
#include "services/database/IdType.hpp" #include "services/database/IdType.hpp"
#include "services/database/Object.hpp" #include "services/database/Object.hpp"
#include "services/database/ReleaseId.hpp" #include "services/database/ReleaseId.hpp"
@@ -35,55 +36,58 @@ LMS_DECLARE_IDTYPE(TrackArtistLinkId)
namespace Database namespace Database
{ {
class Artist; class Artist;
class Session; class Session;
class Track; class Track;
class TrackArtistLink final : public Object<TrackArtistLink, TrackArtistLinkId> class TrackArtistLink final : public Object<TrackArtistLink, TrackArtistLinkId>
{ {
public: public:
struct FindParameters struct FindParameters
{ {
Range range; Range range;
std::optional<TrackArtistLinkType> linkType; // if set, only artists that have produced at least one track with this link type std::optional<TrackArtistLinkType> linkType; // if set, only artists that have produced at least one track with this link type
TrackId track; // artists involved in this track ArtistId artist; // if set, links involved with this artist
ReleaseId release; // artists involved in this release ReleaseId release; // if set, artists involved in this release
TrackId track; // if set, artists involved in this track
FindParameters& setRange(Range _range) {range = _range; return *this; } FindParameters& setRange(Range _range) { range = _range; return *this; }
FindParameters& setLinkType(std::optional<TrackArtistLinkType> _linkType) { linkType = _linkType; return *this; } FindParameters& setLinkType(std::optional<TrackArtistLinkType> _linkType) { linkType = _linkType; return *this; }
FindParameters& setTrack(TrackId _track) { track = _track; return *this; } FindParameters& setArtist(ArtistId _artist) { artist = _artist; return *this; }
FindParameters& setRelease(ReleaseId _release) { release = _release; return *this; } FindParameters& setRelease(ReleaseId _release) { release = _release; return *this; }
}; FindParameters& setTrack(TrackId _track) { track = _track; return *this; }
};
TrackArtistLink() = default; TrackArtistLink() = default;
TrackArtistLink(ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType); TrackArtistLink(ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType);
static RangeResults<TrackArtistLinkId> find(Session& session, const FindParameters& parameters); static RangeResults<TrackArtistLinkId> find(Session& session, const FindParameters& parameters);
static pointer find(Session& session, TrackArtistLinkId linkId); static pointer find(Session& session, TrackArtistLinkId linkId);
static pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType = {}); static pointer create(Session& session, ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType = {});
static EnumSet<TrackArtistLinkType> findUsedTypes(Session& session); static EnumSet<TrackArtistLinkType> findUsedTypes(Session& session);
static EnumSet<TrackArtistLinkType> findUsedTypes(Session& session, ArtistId _artist);
ObjectPtr<Track> getTrack() const { return _track; } ObjectPtr<Track> getTrack() const { return _track; }
ObjectPtr<Artist> getArtist() const { return _artist; } ObjectPtr<Artist> getArtist() const { return _artist; }
TrackArtistLinkType getType() const { return _type; } TrackArtistLinkType getType() const { return _type; }
std::string_view getSubType() const { return _subType; } std::string_view getSubType() const { return _subType; }
template<class Action> template<class Action>
void persist(Action& a) void persist(Action& a)
{ {
Wt::Dbo::field(a, _type, "type"); Wt::Dbo::field(a, _type, "type");
Wt::Dbo::field(a, _subType, "subtype"); Wt::Dbo::field(a, _subType, "subtype");
Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade); Wt::Dbo::belongsTo(a, _track, "track", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade); Wt::Dbo::belongsTo(a, _artist, "artist", Wt::Dbo::OnDeleteCascade);
} }
private: private:
TrackArtistLinkType _type; TrackArtistLinkType _type;
std::string _subType; std::string _subType;
Wt::Dbo::ptr<Track> _track; Wt::Dbo::ptr<Track> _track;
Wt::Dbo::ptr<Artist> _artist; Wt::Dbo::ptr<Artist> _artist;
}; };
} }
@@ -83,6 +83,12 @@ namespace Database
static DateRange fromYearRange(int from, int to); static DateRange fromYearRange(int from, int to);
}; };
struct DiscInfo
{
std::size_t position;
std::string name;
};
enum class ArtistSortMethod enum class ArtistSortMethod
{ {
None, None,
@@ -179,6 +179,15 @@ TEST_F(DatabaseFixture, Artist_singleTracktMultiRoles)
tracks = Track::find(session, Track::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::Composer})); tracks = Track::find(session, Track::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::Composer}));
EXPECT_EQ(tracks.results.size(), 0); EXPECT_EQ(tracks.results.size(), 0);
} }
{
auto transaction {session.createSharedTransaction()};
EnumSet<TrackArtistLinkType> types{ TrackArtistLink::findUsedTypes(session, artist.getId()) };
EXPECT_TRUE(types.contains(TrackArtistLinkType::ReleaseArtist));
EXPECT_TRUE(types.contains(TrackArtistLinkType::Artist));
EXPECT_TRUE(types.contains(TrackArtistLinkType::Writer));
EXPECT_FALSE(types.contains(TrackArtistLinkType::Composer));
}
} }
TEST_F(DatabaseFixture, Artist_singleTrackMultiArtists) TEST_F(DatabaseFixture, Artist_singleTrackMultiArtists)
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -350,8 +350,8 @@ TEST_F(DatabaseFixture, MultiTracksSingleReleaseDate)
track1A.get().modify()->setOriginalDate(release1OriginalDate); track1A.get().modify()->setOriginalDate(release1OriginalDate);
track1B.get().modify()->setOriginalDate(release1OriginalDate); track1B.get().modify()->setOriginalDate(release1OriginalDate);
EXPECT_EQ(release1.get()->getReleaseYear(), release1Date.year()); EXPECT_EQ(release1.get()->getReleaseDate(), release1Date);
EXPECT_EQ(release1.get()->getReleaseYear(true), release1OriginalDate.year()); EXPECT_EQ(release1.get()->getOriginalReleaseDate(), release1OriginalDate);
} }
{ {
@@ -30,33 +30,33 @@
namespace Database namespace Database
{ {
class Session; class Session;
class TrackList; class TrackList;
class User; class User;
} }
namespace Scrobbling namespace Scrobbling
{ {
class IScrobbler class IScrobbler
{ {
public: public:
virtual ~IScrobbler() = default; virtual ~IScrobbler() = default;
// Listens // Listens
virtual void listenStarted(const Listen& listen) = 0; virtual void listenStarted(const Listen& listen) = 0;
virtual void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) = 0; virtual void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) = 0;
virtual void addTimedListen(const TimedListen& listen) = 0; virtual void addTimedListen(const TimedListen& listen) = 0;
// Feedbacks // Feedbacks
virtual void onStarred(Database::StarredArtistId) = 0; virtual void onStarred(Database::StarredArtistId) = 0;
virtual void onUnstarred(Database::StarredArtistId) = 0; virtual void onUnstarred(Database::StarredArtistId) = 0;
virtual void onStarred(Database::StarredReleaseId) = 0; virtual void onStarred(Database::StarredReleaseId) = 0;
virtual void onUnstarred(Database::StarredReleaseId) = 0; virtual void onUnstarred(Database::StarredReleaseId) = 0;
virtual void onStarred(Database::StarredTrackId) = 0; virtual void onStarred(Database::StarredTrackId) = 0;
virtual void onUnstarred(Database::StarredTrackId) = 0; virtual void onUnstarred(Database::StarredTrackId) = 0;
}; };
std::unique_ptr<IScrobbler> createScrobbler(std::string_view backendName); std::unique_ptr<IScrobbler> createScrobbler(std::string_view backendName);
} // ns Scrobbling } // ns Scrobbling
@@ -37,272 +37,264 @@
namespace Scrobbling namespace Scrobbling
{ {
using namespace Database; using namespace Database;
std::unique_ptr<IScrobblingService> std::unique_ptr<IScrobblingService> createScrobblingService(boost::asio::io_context& ioContext, Db& db)
createScrobblingService(boost::asio::io_context& ioContext, Db& db) {
{ return std::make_unique<ScrobblingService>(ioContext, db);
return std::make_unique<ScrobblingService>(ioContext, db); }
}
ScrobblingService::ScrobblingService(boost::asio::io_context& ioContext, Db& db) ScrobblingService::ScrobblingService(boost::asio::io_context& ioContext, Db& db)
: _db {db} : _db{ db }
{ {
LMS_LOG(SCROBBLING, INFO) << "Starting service..."; LMS_LOG(SCROBBLING, INFO) << "Starting service...";
_scrobblers.emplace(Scrobbler::Internal, std::make_unique<InternalScrobbler>(_db)); _scrobblers.emplace(Scrobbler::Internal, std::make_unique<InternalScrobbler>(_db));
_scrobblers.emplace(Scrobbler::ListenBrainz, std::make_unique<ListenBrainz::Scrobbler>(ioContext, _db)); _scrobblers.emplace(Scrobbler::ListenBrainz, std::make_unique<ListenBrainz::Scrobbler>(ioContext, _db));
LMS_LOG(SCROBBLING, INFO) << "Service started!"; LMS_LOG(SCROBBLING, INFO) << "Service started!";
} }
ScrobblingService::~ScrobblingService() ScrobblingService::~ScrobblingService()
{ {
LMS_LOG(SCROBBLING, INFO) << "Service stopped!"; LMS_LOG(SCROBBLING, INFO) << "Service stopped!";
} }
void void ScrobblingService::listenStarted(const Listen& listen)
ScrobblingService::listenStarted(const Listen& listen) {
{ if (std::optional<Scrobbler> scrobbler{ getUserScrobbler(listen.userId) })
if (std::optional<Scrobbler> scrobbler {getUserScrobbler(listen.userId)}) _scrobblers[*scrobbler]->listenStarted(listen);
_scrobblers[*scrobbler]->listenStarted(listen); }
}
void void ScrobblingService::listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration)
ScrobblingService::listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) {
{ if (std::optional<Scrobbler> scrobbler{ getUserScrobbler(listen.userId) })
if (std::optional<Scrobbler> scrobbler {getUserScrobbler(listen.userId)}) _scrobblers[*scrobbler]->listenFinished(listen, duration);
_scrobblers[*scrobbler]->listenFinished(listen, duration); }
}
void void ScrobblingService::addTimedListen(const TimedListen& listen)
ScrobblingService::addTimedListen(const TimedListen& listen) {
{ if (std::optional<Scrobbler> scrobbler{ getUserScrobbler(listen.userId) })
if (std::optional<Scrobbler> scrobbler {getUserScrobbler(listen.userId)}) _scrobblers[*scrobbler]->addTimedListen(listen);
_scrobblers[*scrobbler]->addTimedListen(listen); }
}
std::optional<Scrobbler> std::optional<Scrobbler> ScrobblingService::getUserScrobbler(UserId userId)
ScrobblingService::getUserScrobbler(UserId userId) {
{ std::optional<Scrobbler> scrobbler;
std::optional<Scrobbler> scrobbler;
Session& session {_db.getTLSSession()}; Session& session{ _db.getTLSSession() };
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
if (const User::pointer user {User::find(session, userId)}) if (const User::pointer user{ User::find(session, userId) })
scrobbler = user->getScrobbler(); scrobbler = user->getScrobbler();
return scrobbler; return scrobbler;
} }
ScrobblingService::ArtistContainer ScrobblingService::ArtistContainer ScrobblingService::getRecentArtists(UserId userId, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range)
ScrobblingService::getRecentArtists(UserId userId, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range) {
{ ArtistContainer res;
ArtistContainer res;
auto scrobbler {getUserScrobbler(userId)}; auto scrobbler{ getUserScrobbler(userId) };
if (!scrobbler) if (!scrobbler)
return res; return res;
Session& session {_db.getTLSSession()}; Session& session{ _db.getTLSSession() };
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
res = Database::Listen::getRecentArtists(session, userId, *scrobbler, clusterIds, linkType, range); res = Database::Listen::getRecentArtists(session, userId, *scrobbler, clusterIds, linkType, range);
return res; return res;
} }
ScrobblingService::ReleaseContainer ScrobblingService::ReleaseContainer ScrobblingService::getRecentReleases(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
ScrobblingService::getRecentReleases(UserId userId, const std::vector<ClusterId>& clusterIds, Range range) {
{ ReleaseContainer res;
ReleaseContainer res;
auto scrobbler {getUserScrobbler(userId)}; auto scrobbler{ getUserScrobbler(userId) };
if (!scrobbler) if (!scrobbler)
return res; return res;
Session& session {_db.getTLSSession()}; Session& session{ _db.getTLSSession() };
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
res = Database::Listen::getRecentReleases(session, userId, *scrobbler, clusterIds, range); res = Database::Listen::getRecentReleases(session, userId, *scrobbler, clusterIds, range);
return res; return res;
} }
ScrobblingService::TrackContainer ScrobblingService::TrackContainer ScrobblingService::getRecentTracks(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
ScrobblingService::getRecentTracks(UserId userId, const std::vector<ClusterId>& clusterIds, Range range) {
{ TrackContainer res;
TrackContainer res;
auto scrobbler {getUserScrobbler(userId)}; auto scrobbler{ getUserScrobbler(userId) };
if (!scrobbler) if (!scrobbler)
return res; return res;
Session& session {_db.getTLSSession()}; Session& session{ _db.getTLSSession() };
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
res = Database::Listen::getRecentTracks(session, userId, *scrobbler, clusterIds, range); res = Database::Listen::getRecentTracks(session, userId, *scrobbler, clusterIds, range);
return res; return res;
} }
// Top // Top
ScrobblingService::ArtistContainer ScrobblingService::ArtistContainer ScrobblingService::getTopArtists(UserId userId, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range)
ScrobblingService::getTopArtists(UserId userId, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, Range range) {
{ ArtistContainer res;
ArtistContainer res;
auto scrobbler {getUserScrobbler(userId)}; auto scrobbler{ getUserScrobbler(userId) };
if (!scrobbler) if (!scrobbler)
return res; return res;
Session& session {_db.getTLSSession()}; Session& session{ _db.getTLSSession() };
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
res = Database::Listen::getTopArtists(session, userId, *scrobbler, clusterIds, linkType, range); res = Database::Listen::getTopArtists(session, userId, *scrobbler, clusterIds, linkType, range);
return res; return res;
} }
ScrobblingService::ReleaseContainer ScrobblingService::ReleaseContainer ScrobblingService::getTopReleases(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
ScrobblingService::getTopReleases(UserId userId, const std::vector<ClusterId>& clusterIds, Range range) {
{ ReleaseContainer res;
ReleaseContainer res;
auto scrobbler {getUserScrobbler(userId)}; auto scrobbler{ getUserScrobbler(userId) };
if (!scrobbler) if (!scrobbler)
return res; return res;
Session& session {_db.getTLSSession()}; Session& session{ _db.getTLSSession() };
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
res = Database::Listen::getTopReleases(session, userId, *scrobbler, clusterIds, range); res = Database::Listen::getTopReleases(session, userId, *scrobbler, clusterIds, range);
return res; return res;
} }
ScrobblingService::TrackContainer ScrobblingService::TrackContainer ScrobblingService::getTopTracks(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
ScrobblingService::getTopTracks(UserId userId, const std::vector<ClusterId>& clusterIds, Range range) {
{ TrackContainer res;
TrackContainer res;
auto scrobbler {getUserScrobbler(userId)}; auto scrobbler{ getUserScrobbler(userId) };
if (!scrobbler) if (!scrobbler)
return res; return res;
Session& session {_db.getTLSSession()}; Session& session{ _db.getTLSSession() };
auto transaction {session.createSharedTransaction()}; auto transaction{ session.createSharedTransaction() };
res = Database::Listen::getTopTracks(session, userId, *scrobbler, clusterIds, range); res = Database::Listen::getTopTracks(session, userId, *scrobbler, clusterIds, range);
return res; return res;
} }
void void ScrobblingService::star(UserId userId, ArtistId artistId)
ScrobblingService::star(UserId userId, ArtistId artistId) {
{ star<Artist, ArtistId, StarredArtist>(userId, artistId);
star<Artist, ArtistId, StarredArtist>(userId, artistId); }
}
void void ScrobblingService::unstar(UserId userId, ArtistId artistId)
ScrobblingService::unstar(UserId userId, ArtistId artistId) {
{ unstar<Artist, ArtistId, StarredArtist>(userId, artistId);
unstar<Artist, ArtistId, StarredArtist>(userId, artistId); }
}
bool bool ScrobblingService::isStarred(UserId userId, ArtistId artistId)
ScrobblingService::isStarred(UserId userId, ArtistId artistId) {
{ return isStarred<Artist, ArtistId, StarredArtist>(userId, artistId);
return isStarred<Artist, ArtistId, StarredArtist>(userId, artistId); }
}
ScrobblingService::ArtistContainer Wt::WDateTime ScrobblingService::getStarredDateTime(UserId userId, ArtistId artistId)
ScrobblingService::getStarredArtists(UserId userId, const std::vector<ClusterId>& clusterIds, {
std::optional<TrackArtistLinkType> linkType, return getStarredDateTime<Artist, ArtistId, StarredArtist>(userId, artistId);
ArtistSortMethod sortMethod, }
Range range)
{
auto scrobbler {getUserScrobbler(userId)};
if (!scrobbler)
return {};
Artist::FindParameters params; ScrobblingService::ArtistContainer ScrobblingService::getStarredArtists(UserId userId, const std::vector<ClusterId>& clusterIds,
params.setStarringUser(userId, *scrobbler); std::optional<TrackArtistLinkType> linkType,
params.setClusters(clusterIds); ArtistSortMethod sortMethod,
params.setLinkType(linkType); Range range)
params.setSortMethod(sortMethod); {
params.setRange(range); auto scrobbler{ getUserScrobbler(userId) };
if (!scrobbler)
return {};
Session& session {_db.getTLSSession()}; Artist::FindParameters params;
auto transaction {session.createSharedTransaction()}; params.setStarringUser(userId, *scrobbler);
params.setClusters(clusterIds);
params.setLinkType(linkType);
params.setSortMethod(sortMethod);
params.setRange(range);
return Artist::find(session, params); Session& session{ _db.getTLSSession() };
} auto transaction{ session.createSharedTransaction() };
void return Artist::find(session, params);
ScrobblingService::star(UserId userId, ReleaseId releaseId) }
{
star<Release, ReleaseId, StarredRelease>(userId, releaseId);
}
void void ScrobblingService::star(UserId userId, ReleaseId releaseId)
ScrobblingService::unstar(UserId userId, ReleaseId releaseId) {
{ star<Release, ReleaseId, StarredRelease>(userId, releaseId);
unstar<Release, ReleaseId, StarredRelease>(userId, releaseId); }
}
bool void ScrobblingService::unstar(UserId userId, ReleaseId releaseId)
ScrobblingService::isStarred(UserId userId, ReleaseId releaseId) {
{ unstar<Release, ReleaseId, StarredRelease>(userId, releaseId);
return isStarred<Release, ReleaseId, StarredRelease>(userId, releaseId); }
}
ScrobblingService::ReleaseContainer bool ScrobblingService::isStarred(UserId userId, ReleaseId releaseId)
ScrobblingService::getStarredReleases(UserId userId, const std::vector<ClusterId>& clusterIds, Range range) {
{ return isStarred<Release, ReleaseId, StarredRelease>(userId, releaseId);
auto scrobbler {getUserScrobbler(userId)}; }
if (!scrobbler)
return {};
Release::FindParameters params; Wt::WDateTime ScrobblingService::getStarredDateTime(UserId userId, ReleaseId releaseId)
params.setStarringUser(userId, *scrobbler); {
params.setClusters(clusterIds); return getStarredDateTime<Release, ReleaseId, StarredRelease>(userId, releaseId);
params.setSortMethod(ReleaseSortMethod::StarredDateDesc); }
params.setRange(range);
Session& session {_db.getTLSSession()}; ScrobblingService::ReleaseContainer ScrobblingService::getStarredReleases(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
auto transaction {session.createSharedTransaction()}; {
auto scrobbler{ getUserScrobbler(userId) };
if (!scrobbler)
return {};
return Release::find(session, params); Release::FindParameters params;
} params.setStarringUser(userId, *scrobbler);
params.setClusters(clusterIds);
params.setSortMethod(ReleaseSortMethod::StarredDateDesc);
params.setRange(range);
void Session& session{ _db.getTLSSession() };
ScrobblingService::star(UserId userId, TrackId trackId) auto transaction{ session.createSharedTransaction() };
{
star<Track, TrackId, StarredTrack>(userId, trackId);
}
void return Release::find(session, params);
ScrobblingService::unstar(UserId userId, TrackId trackId) }
{
unstar<Track, TrackId, StarredTrack>(userId, trackId);
}
bool void ScrobblingService::star(UserId userId, TrackId trackId)
ScrobblingService::isStarred(UserId userId, TrackId trackId) {
{ star<Track, TrackId, StarredTrack>(userId, trackId);
return isStarred<Track, TrackId, StarredTrack>(userId, trackId); }
}
ScrobblingService::TrackContainer void ScrobblingService::unstar(UserId userId, TrackId trackId)
ScrobblingService::getStarredTracks(UserId userId, const std::vector<ClusterId>& clusterIds, Range range) {
{ unstar<Track, TrackId, StarredTrack>(userId, trackId);
auto scrobbler {getUserScrobbler(userId)}; }
if (!scrobbler)
return {};
Track::FindParameters params; bool ScrobblingService::isStarred(UserId userId, TrackId trackId)
params.setStarringUser(userId, *scrobbler); {
params.setClusters(clusterIds); return isStarred<Track, TrackId, StarredTrack>(userId, trackId);
params.setSortMethod(TrackSortMethod::StarredDateDesc); }
params.setRange(range);
Session& session {_db.getTLSSession()}; Wt::WDateTime ScrobblingService::getStarredDateTime(UserId userId, TrackId trackId)
auto transaction {session.createSharedTransaction()}; {
return getStarredDateTime<Track, TrackId, StarredTrack>(userId, trackId);
}
return Track::find(session, params); ScrobblingService::TrackContainer ScrobblingService::getStarredTracks(UserId userId, const std::vector<ClusterId>& clusterIds, Range range)
} {
auto scrobbler{ getUserScrobbler(userId) };
if (!scrobbler)
return {};
Track::FindParameters params;
params.setStarringUser(userId, *scrobbler);
params.setClusters(clusterIds);
params.setSortMethod(TrackSortMethod::StarredDateDesc);
params.setRange(range);
Session& session{ _db.getTLSSession() };
auto transaction{ session.createSharedTransaction() };
return Track::find(session, params);
}
} // ns Scrobbling } // ns Scrobbling
@@ -28,74 +28,79 @@
namespace Scrobbling namespace Scrobbling
{ {
class ScrobblingService : public IScrobblingService class ScrobblingService : public IScrobblingService
{ {
public: public:
ScrobblingService(boost::asio::io_context& ioContext, Database::Db& db); ScrobblingService(boost::asio::io_context& ioContext, Database::Db& db);
~ScrobblingService(); ~ScrobblingService();
private: private:
void listenStarted(const Listen& listen) override; void listenStarted(const Listen& listen) override;
void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) override; void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) override;
void addTimedListen(const TimedListen& listen) override; void addTimedListen(const TimedListen& listen) override;
ArtistContainer getRecentArtists(Database::UserId userId, ArtistContainer getRecentArtists(Database::UserId userId,
const std::vector<Database::ClusterId>& clusterIds, const std::vector<Database::ClusterId>& clusterIds,
std::optional<Database::TrackArtistLinkType> linkType, std::optional<Database::TrackArtistLinkType> linkType,
Database::Range range) override; Database::Range range) override;
ReleaseContainer getRecentReleases(Database::UserId userId, ReleaseContainer getRecentReleases(Database::UserId userId,
const std::vector<Database::ClusterId>& clusterIds, const std::vector<Database::ClusterId>& clusterIds,
Database::Range range) override; Database::Range range) override;
TrackContainer getRecentTracks(Database::UserId userId, TrackContainer getRecentTracks(Database::UserId userId,
const std::vector<Database::ClusterId>& clusterIds, const std::vector<Database::ClusterId>& clusterIds,
Database::Range range) override; Database::Range range) override;
ArtistContainer getTopArtists(Database::UserId userId, ArtistContainer getTopArtists(Database::UserId userId,
const std::vector<Database::ClusterId>& clusterIds, const std::vector<Database::ClusterId>& clusterIds,
std::optional<Database::TrackArtistLinkType> linkType, std::optional<Database::TrackArtistLinkType> linkType,
Database::Range range) override; Database::Range range) override;
ReleaseContainer getTopReleases(Database::UserId userId, ReleaseContainer getTopReleases(Database::UserId userId,
const std::vector<Database::ClusterId>& clusterIds, const std::vector<Database::ClusterId>& clusterIds,
Database::Range range) override; Database::Range range) override;
TrackContainer getTopTracks(Database::UserId userId, TrackContainer getTopTracks(Database::UserId userId,
const std::vector<Database::ClusterId>& clusterIds, const std::vector<Database::ClusterId>& clusterIds,
Database::Range range) override; Database::Range range) override;
void star(Database::UserId userId, Database::ArtistId artistId) override; void star(Database::UserId userId, Database::ArtistId artistId) override;
void unstar(Database::UserId userId, Database::ArtistId artistId) override; void unstar(Database::UserId userId, Database::ArtistId artistId) override;
bool isStarred(Database::UserId userId, Database::ArtistId artistId) override; bool isStarred(Database::UserId userId, Database::ArtistId artistId) override;
ArtistContainer getStarredArtists(Database::UserId userId, Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::ArtistId artistId) override;
const std::vector<Database::ClusterId>& clusterIds, ArtistContainer getStarredArtists(Database::UserId userId,
std::optional<Database::TrackArtistLinkType> linkType, const std::vector<Database::ClusterId>& clusterIds,
Database::ArtistSortMethod sortMethod, std::optional<Database::TrackArtistLinkType> linkType,
Database::Range range) override; Database::ArtistSortMethod sortMethod,
Database::Range range) override;
void star(Database::UserId userId, Database::ReleaseId releaseId) override; void star(Database::UserId userId, Database::ReleaseId releaseId) override;
void unstar(Database::UserId userId, Database::ReleaseId releaseId) override; void unstar(Database::UserId userId, Database::ReleaseId releaseId) override;
bool isStarred(Database::UserId userId, Database::ReleaseId artistId) override; bool isStarred(Database::UserId userId, Database::ReleaseId releasedId) override;
ReleaseContainer getStarredReleases(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) override; Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::ReleaseId releasedId) override;
ReleaseContainer getStarredReleases(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) override;
void star(Database::UserId userId, Database::TrackId trackId) override; void star(Database::UserId userId, Database::TrackId trackId) override;
void unstar(Database::UserId userId, Database::TrackId trackId) override; void unstar(Database::UserId userId, Database::TrackId trackId) override;
bool isStarred(Database::UserId userId, Database::TrackId trackId) override; bool isStarred(Database::UserId userId, Database::TrackId trackId) override;
TrackContainer getStarredTracks(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) override; Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::TrackId trackId) override;
TrackContainer getStarredTracks(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) override;
std::optional<Database::Scrobbler> getUserScrobbler(Database::UserId userId); std::optional<Database::Scrobbler> getUserScrobbler(Database::UserId userId);
template <typename ObjType, typename ObjIdType, typename StarredObjType> template <typename ObjType, typename ObjIdType, typename StarredObjType>
void star(Database::UserId userId, ObjIdType id); void star(Database::UserId userId, ObjIdType id);
template <typename ObjType, typename ObjIdType, typename StarredObjType> template <typename ObjType, typename ObjIdType, typename StarredObjType>
void unstar(Database::UserId userId, ObjIdType id); void unstar(Database::UserId userId, ObjIdType id);
template <typename ObjType, typename ObjIdType, typename StarredObjType> template <typename ObjType, typename ObjIdType, typename StarredObjType>
bool isStarred(Database::UserId userId, ObjIdType id); bool isStarred(Database::UserId userId, ObjIdType id);
template <typename ObjType, typename ObjIdType, typename StarredObjType>
Wt::WDateTime getStarredDateTime(Database::UserId userId, ObjIdType id);
Database::Db& _db; Database::Db& _db;
std::unordered_map<Database::Scrobbler, std::unique_ptr<IScrobbler>> _scrobblers; std::unordered_map<Database::Scrobbler, std::unique_ptr<IScrobbler>> _scrobblers;
}; };
} // ns Scrobbling } // ns Scrobbling
@@ -25,76 +25,90 @@
namespace Scrobbling namespace Scrobbling
{ {
using namespace Database; using namespace Database;
template <typename ObjType, typename ObjIdType, typename StarredObjType> template <typename ObjType, typename ObjIdType, typename StarredObjType>
void void ScrobblingService::star(UserId userId, ObjIdType objId)
ScrobblingService::star(UserId userId, ObjIdType objId) {
{ auto scrobbler {getUserScrobbler(userId)};
auto scrobbler {getUserScrobbler(userId)}; if (!scrobbler)
if (!scrobbler) return;
return;
typename StarredObjType::IdType starredObjId; typename StarredObjType::IdType starredObjId;
{ {
Session& session {_db.getTLSSession()}; Session& session {_db.getTLSSession()};
auto transaction {session.createUniqueTransaction()}; auto transaction {session.createUniqueTransaction()};
typename StarredObjType::pointer starredObj {StarredObjType::find(session, objId, userId, *scrobbler)}; typename StarredObjType::pointer starredObj {StarredObjType::find(session, objId, userId, *scrobbler)};
if (!starredObj) if (!starredObj)
{ {
const typename ObjType::pointer obj {ObjType::find(session, objId)}; const typename ObjType::pointer obj {ObjType::find(session, objId)};
if (!obj) if (!obj)
return; return;
const User::pointer user {User::find(session, userId)}; const User::pointer user {User::find(session, userId)};
if (!user) if (!user)
return; return;
starredObj = session.create<StarredObjType>(obj, user, *scrobbler); starredObj = session.create<StarredObjType>(obj, user, *scrobbler);
} }
starredObj.modify()->setDateTime(Wt::WDateTime::currentDateTime()); starredObj.modify()->setDateTime(Wt::WDateTime::currentDateTime());
starredObjId = starredObj->getId(); starredObjId = starredObj->getId();
} }
_scrobblers[*scrobbler]->onStarred(starredObjId); _scrobblers[*scrobbler]->onStarred(starredObjId);
} }
template <typename ObjType, typename ObjIdType, typename StarredObjType> template <typename ObjType, typename ObjIdType, typename StarredObjType>
void void ScrobblingService::unstar(UserId userId, ObjIdType objId)
ScrobblingService::unstar(UserId userId, ObjIdType objId) {
{ auto scrobbler {getUserScrobbler(userId)};
auto scrobbler {getUserScrobbler(userId)}; if (!scrobbler)
if (!scrobbler) return;
return;
typename StarredObjType::IdType starredObjId; typename StarredObjType::IdType starredObjId;
{ {
Session& session {_db.getTLSSession()}; Session& session {_db.getTLSSession()};
auto transaction {session.createSharedTransaction()}; auto transaction {session.createSharedTransaction()};
typename StarredObjType::pointer starredObj {StarredObjType::find(session, objId, userId, *scrobbler)}; typename StarredObjType::pointer starredObj {StarredObjType::find(session, objId, userId, *scrobbler)};
if (!starredObj) if (!starredObj)
return; return;
starredObjId = starredObj->getId(); starredObjId = starredObj->getId();
} }
_scrobblers[*scrobbler]->onUnstarred(starredObjId); _scrobblers[*scrobbler]->onUnstarred(starredObjId);
} }
template <typename ObjType, typename ObjIdType, typename StarredObjType> template <typename ObjType, typename ObjIdType, typename StarredObjType>
bool bool ScrobblingService::isStarred(UserId userId, ObjIdType objId)
ScrobblingService::isStarred(UserId userId, ObjIdType objId) {
{ auto scrobbler {getUserScrobbler(userId)};
auto scrobbler {getUserScrobbler(userId)}; if (!scrobbler)
if (!scrobbler) return false;
return false;
Session& session {_db.getTLSSession()}; Session& session {_db.getTLSSession()};
auto transaction {session.createSharedTransaction()}; auto transaction {session.createSharedTransaction()};
typename StarredObjType::pointer starredObj {StarredObjType::find(session, objId, userId, *scrobbler)}; typename StarredObjType::pointer starredObj {StarredObjType::find(session, objId, userId, *scrobbler)};
return starredObj && (starredObj->getScrobblingState() != ScrobblingState::PendingRemove); return starredObj && (starredObj->getScrobblingState() != ScrobblingState::PendingRemove);
} }
template <typename ObjType, typename ObjIdType, typename StarredObjType>
Wt::WDateTime ScrobblingService::getStarredDateTime(UserId userId, ObjIdType objId)
{
auto scrobbler {getUserScrobbler(userId)};
if (!scrobbler)
return {};
Session& session {_db.getTLSSession()};
auto transaction {session.createSharedTransaction()};
typename StarredObjType::pointer starredObj {StarredObjType::find(session, objId, userId, *scrobbler)};
if (starredObj && (starredObj->getScrobblingState() != ScrobblingState::PendingRemove))
return starredObj->getDateTime();
return {};
}
} // ns Scrobbling } // ns Scrobbling
@@ -30,101 +30,93 @@
namespace namespace
{ {
template <typename StarredObjType> template <typename StarredObjType>
void onStarred(Database::Session& session, typename StarredObjType::IdType id) void onStarred(Database::Session& session, typename StarredObjType::IdType id)
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction{ session.createUniqueTransaction() };
if (auto starredObj {StarredObjType::find(session, id)}) if (auto starredObj{ StarredObjType::find(session, id) })
starredObj.modify()->setScrobblingState(Database::ScrobblingState::Synchronized); starredObj.modify()->setScrobblingState(Database::ScrobblingState::Synchronized);
} }
template <typename StarredObjType> template <typename StarredObjType>
void onUnstarred(Database::Session& session, typename StarredObjType::IdType id) void onUnstarred(Database::Session& session, typename StarredObjType::IdType id)
{ {
auto transaction {session.createUniqueTransaction()}; auto transaction{ session.createUniqueTransaction() };
if (auto starredObj {StarredObjType::find(session, id)}) if (auto starredObj{ StarredObjType::find(session, id) })
starredObj.remove(); starredObj.remove();
} }
} }
namespace Scrobbling namespace Scrobbling
{ {
InternalScrobbler::InternalScrobbler(Database::Db& db) InternalScrobbler::InternalScrobbler(Database::Db& db)
: _db {db} : _db{ db }
{} {}
void void InternalScrobbler::listenStarted(const Listen&)
InternalScrobbler::listenStarted(const Listen&) {
{ // nothing to do
// nothing to do }
}
void void InternalScrobbler::listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration)
InternalScrobbler::listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) {
{ // only record tracks that have been played for at least of few seconds...
// only record tracks that have been played for at least of few seconds... if (duration && *duration < std::chrono::seconds{ 5 })
if (duration && *duration < std::chrono::seconds {5}) return;
return;
addTimedListen({listen, Wt::WDateTime::currentDateTime()}); addTimedListen({ listen, Wt::WDateTime::currentDateTime() });
} }
void void InternalScrobbler::addTimedListen(const TimedListen& listen)
InternalScrobbler::addTimedListen(const TimedListen& listen) {
{ Database::Session& session{ _db.getTLSSession() };
Database::Session& session {_db.getTLSSession()}; auto transaction{ session.createUniqueTransaction() };
auto transaction {session.createUniqueTransaction()};
if (Database::Listen::find(session, listen.userId, listen.trackId, Database::Scrobbler::Internal, listen.listenedAt)) if (Database::Listen::find(session, listen.userId, listen.trackId, Database::Scrobbler::Internal, listen.listenedAt))
return; return;
const Database::User::pointer user {Database::User::find(session, listen.userId)}; const Database::User::pointer user{ Database::User::find(session, listen.userId) };
if (!user) if (!user)
return; return;
const Database::Track::pointer track {Database::Track::find(session, listen.trackId)}; const Database::Track::pointer track{ Database::Track::find(session, listen.trackId) };
if (!track) if (!track)
return; return;
auto dbListen {session.create<Database::Listen>(user, track, Database::Scrobbler::Internal, listen.listenedAt)}; auto dbListen{ session.create<Database::Listen>(user, track, Database::Scrobbler::Internal, listen.listenedAt) };
dbListen.modify()->setScrobblingState(Database::ScrobblingState::Synchronized); dbListen.modify()->setScrobblingState(Database::ScrobblingState::Synchronized);
} }
void void InternalScrobbler::onStarred(Database::StarredArtistId starredArtistId)
InternalScrobbler::onStarred(Database::StarredArtistId starredArtistId) {
{ ::onStarred<Database::StarredArtist>(_db.getTLSSession(), starredArtistId);
::onStarred<Database::StarredArtist>(_db.getTLSSession(), starredArtistId); }
}
void void InternalScrobbler::onUnstarred(Database::StarredArtistId starredArtistId)
InternalScrobbler::onUnstarred(Database::StarredArtistId starredArtistId) {
{ ::onUnstarred<Database::StarredArtist>(_db.getTLSSession(), starredArtistId);
::onUnstarred<Database::StarredArtist>(_db.getTLSSession(), starredArtistId); }
}
void void InternalScrobbler::onStarred(Database::StarredReleaseId starredReleaseId)
InternalScrobbler::onStarred(Database::StarredReleaseId starredReleaseId) {
{ ::onStarred<Database::StarredRelease>(_db.getTLSSession(), starredReleaseId);
::onStarred<Database::StarredRelease>(_db.getTLSSession(), starredReleaseId); }
}
void InternalScrobbler::onUnstarred(Database::StarredReleaseId starredReleaseId) void InternalScrobbler::onUnstarred(Database::StarredReleaseId starredReleaseId)
{ {
::onUnstarred<Database::StarredRelease>(_db.getTLSSession(), starredReleaseId); ::onUnstarred<Database::StarredRelease>(_db.getTLSSession(), starredReleaseId);
} }
void void InternalScrobbler::onStarred(Database::StarredTrackId starredTrackId)
InternalScrobbler::onStarred(Database::StarredTrackId starredTrackId) {
{ ::onStarred<Database::StarredTrack>(_db.getTLSSession(), starredTrackId);
::onStarred<Database::StarredTrack>(_db.getTLSSession(), starredTrackId); }
}
void void InternalScrobbler::onUnstarred(Database::StarredTrackId starredTrackId)
InternalScrobbler::onUnstarred(Database::StarredTrackId starredTrackId) {
{ ::onUnstarred<Database::StarredTrack>(_db.getTLSSession(), starredTrackId);
::onUnstarred<Database::StarredTrack>(_db.getTLSSession(), starredTrackId); }
}
} // Scrobbling } // Scrobbling
@@ -23,30 +23,30 @@
namespace Database namespace Database
{ {
class Db; class Db;
} }
namespace Scrobbling namespace Scrobbling
{ {
class InternalScrobbler final : public IScrobbler class InternalScrobbler final : public IScrobbler
{ {
public: public:
InternalScrobbler(Database::Db& db); InternalScrobbler(Database::Db& db);
private: private:
// IScrobbler // IScrobbler
void listenStarted(const Listen& listen) override; void listenStarted(const Listen& listen) override;
void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) override; void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration) override;
void addTimedListen(const TimedListen& listen) override; void addTimedListen(const TimedListen& listen) override;
void onStarred(Database::StarredArtistId) override; void onStarred(Database::StarredArtistId) override;
void onUnstarred(Database::StarredArtistId) override; void onUnstarred(Database::StarredArtistId) override;
void onStarred(Database::StarredReleaseId) override; void onStarred(Database::StarredReleaseId) override;
void onUnstarred(Database::StarredReleaseId) override; void onUnstarred(Database::StarredReleaseId) override;
void onStarred(Database::StarredTrackId) override; void onStarred(Database::StarredTrackId) override;
void onUnstarred(Database::StarredTrackId) override; void onUnstarred(Database::StarredTrackId) override;
Database::Db& _db; Database::Db& _db;
}; };
} // Scrobbling } // Scrobbling
@@ -23,9 +23,9 @@
namespace Scrobbling namespace Scrobbling
{ {
class Exception : public LmsException class Exception : public LmsException
{ {
public: public:
using LmsException::LmsException; using LmsException::LmsException;
}; };
} }
@@ -19,11 +19,11 @@
#pragma once #pragma once
#include <boost/asio/io_service.hpp>
#include <chrono> #include <chrono>
#include <memory> #include <memory>
#include <optional> #include <optional>
#include <boost/asio/io_service.hpp>
#include <Wt/WDateTime.h>
#include "services/scrobbling/Listen.hpp" #include "services/scrobbling/Listen.hpp"
#include "services/database/ArtistId.hpp" #include "services/database/ArtistId.hpp"
@@ -34,77 +34,80 @@
namespace Database namespace Database
{ {
class Db; class Db;
} }
namespace Scrobbling namespace Scrobbling
{ {
class IScrobblingService class IScrobblingService
{ {
public: public:
virtual ~IScrobblingService() = default; virtual ~IScrobblingService() = default;
// Scrobbling // Scrobbling
virtual void listenStarted(const Listen& listen) = 0; virtual void listenStarted(const Listen& listen) = 0;
virtual void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> playedDuration = std::nullopt) = 0; virtual void listenFinished(const Listen& listen, std::optional<std::chrono::seconds> playedDuration = std::nullopt) = 0;
virtual void addTimedListen(const TimedListen& listen) = 0; virtual void addTimedListen(const TimedListen& listen) = 0;
// Stats // Stats
using ArtistContainer = Database::RangeResults<Database::ArtistId>; using ArtistContainer = Database::RangeResults<Database::ArtistId>;
using ReleaseContainer = Database::RangeResults<Database::ReleaseId>; using ReleaseContainer = Database::RangeResults<Database::ReleaseId>;
using TrackContainer = Database::RangeResults<Database::TrackId>; using TrackContainer = Database::RangeResults<Database::TrackId>;
// From most recent to oldest // From most recent to oldest
virtual ArtistContainer getRecentArtists(Database::UserId userId, virtual ArtistContainer getRecentArtists(Database::UserId userId,
const std::vector<Database::ClusterId>& clusterIds, const std::vector<Database::ClusterId>& clusterIds,
std::optional<Database::TrackArtistLinkType> linkType, std::optional<Database::TrackArtistLinkType> linkType,
Database::Range range) = 0; Database::Range range) = 0;
virtual ReleaseContainer getRecentReleases(Database::UserId userId, virtual ReleaseContainer getRecentReleases(Database::UserId userId,
const std::vector<Database::ClusterId>& clusterIds, const std::vector<Database::ClusterId>& clusterIds,
Database::Range range) = 0; Database::Range range) = 0;
virtual TrackContainer getRecentTracks(Database::UserId userId, virtual TrackContainer getRecentTracks(Database::UserId userId,
const std::vector<Database::ClusterId>& clusterIds, const std::vector<Database::ClusterId>& clusterIds,
Database::Range range) = 0; Database::Range range) = 0;
// Top // Top
virtual ArtistContainer getTopArtists(Database::UserId userId, virtual ArtistContainer getTopArtists(Database::UserId userId,
const std::vector<Database::ClusterId>& clusterIds, const std::vector<Database::ClusterId>& clusterIds,
std::optional<Database::TrackArtistLinkType> linkType, std::optional<Database::TrackArtistLinkType> linkType,
Database::Range) = 0; Database::Range) = 0;
virtual ReleaseContainer getTopReleases(Database::UserId userId, virtual ReleaseContainer getTopReleases(Database::UserId userId,
const std::vector<Database::ClusterId>& clusterIds, const std::vector<Database::ClusterId>& clusterIds,
Database::Range range) = 0; Database::Range range) = 0;
virtual TrackContainer getTopTracks(Database::UserId userId, virtual TrackContainer getTopTracks(Database::UserId userId,
const std::vector<Database::ClusterId>& clusterIds, const std::vector<Database::ClusterId>& clusterIds,
Database::Range range) = 0; Database::Range range) = 0;
// Star // Star
virtual void star(Database::UserId userId, Database::ArtistId artistId) = 0; virtual void star(Database::UserId userId, Database::ArtistId artistId) = 0;
virtual void unstar(Database::UserId userId, Database::ArtistId artistId) = 0; virtual void unstar(Database::UserId userId, Database::ArtistId artistId) = 0;
virtual bool isStarred(Database::UserId userId, Database::ArtistId artistId) = 0; virtual bool isStarred(Database::UserId userId, Database::ArtistId artistId) = 0;
virtual ArtistContainer getStarredArtists(Database::UserId userId, virtual Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::ArtistId artistId) = 0;
const std::vector<Database::ClusterId>& clusterIds, virtual ArtistContainer getStarredArtists(Database::UserId userId,
std::optional<Database::TrackArtistLinkType> linkType, const std::vector<Database::ClusterId>& clusterIds,
Database::ArtistSortMethod sortMethod, std::optional<Database::TrackArtistLinkType> linkType,
Database::Range range) = 0; Database::ArtistSortMethod sortMethod,
Database::Range range) = 0;
virtual void star(Database::UserId userId, Database::ReleaseId releaseId) = 0; virtual void star(Database::UserId userId, Database::ReleaseId releaseId) = 0;
virtual void unstar(Database::UserId userId, Database::ReleaseId releaseId) = 0; virtual void unstar(Database::UserId userId, Database::ReleaseId releaseId) = 0;
virtual bool isStarred(Database::UserId userId, Database::ReleaseId artistId) = 0; virtual bool isStarred(Database::UserId userId, Database::ReleaseId artistId) = 0;
virtual ReleaseContainer getStarredReleases(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) = 0; virtual Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::ReleaseId artistId) = 0;
virtual ReleaseContainer getStarredReleases(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) = 0;
virtual void star(Database::UserId userId, Database::TrackId trackId) = 0; virtual void star(Database::UserId userId, Database::TrackId trackId) = 0;
virtual void unstar(Database::UserId userId, Database::TrackId trackId) = 0; virtual void unstar(Database::UserId userId, Database::TrackId trackId) = 0;
virtual bool isStarred(Database::UserId userId, Database::TrackId artistId) = 0; virtual bool isStarred(Database::UserId userId, Database::TrackId artistId) = 0;
virtual TrackContainer getStarredTracks(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) = 0; virtual Wt::WDateTime getStarredDateTime(Database::UserId userId, Database::TrackId artistId) = 0;
}; virtual TrackContainer getStarredTracks(Database::UserId userId, const std::vector<Database::ClusterId>& clusterIds, Database::Range range) = 0;
};
std::unique_ptr<IScrobblingService> createScrobblingService(boost::asio::io_service& ioService, Database::Db& db); std::unique_ptr<IScrobblingService> createScrobblingService(boost::asio::io_service& ioService, Database::Db& db);
} // ns Scrobbling } // ns Scrobbling
@@ -26,15 +26,15 @@
namespace Scrobbling namespace Scrobbling
{ {
struct Listen struct Listen
{ {
Database::UserId userId {}; Database::UserId userId{};
Database::TrackId trackId {}; Database::TrackId trackId{};
}; };
struct TimedListen : public Listen struct TimedListen : public Listen
{ {
Wt::WDateTime listenedAt; Wt::WDateTime listenedAt;
}; };
} // ns Scrobbling } // ns Scrobbling
+24 -2
View File
@@ -1,11 +1,32 @@
add_library(lmssubsonic SHARED add_library(lmssubsonic SHARED
impl/entrypoints/AlbumSongLists.cpp
impl/entrypoints/Bookmarks.cpp
impl/entrypoints/Browsing.cpp
impl/entrypoints/MediaAnnotation.cpp
impl/entrypoints/MediaLibraryScanning.cpp
impl/entrypoints/MediaRetrieval.cpp
impl/entrypoints/Playlists.cpp
impl/entrypoints/Searching.cpp
impl/entrypoints/System.cpp
impl/entrypoints/UserManagement.cpp
impl/responses/Album.cpp
impl/responses/Artist.cpp
impl/responses/Bookmark.cpp
impl/responses/Contributor.cpp
impl/responses/DiscTitle.cpp
impl/responses/ItemGenre.cpp
impl/responses/Genre.cpp
impl/responses/Playlist.cpp
impl/responses/ReplayGain.cpp
impl/responses/Song.cpp
impl/responses/User.cpp
impl/ProtocolVersion.cpp impl/ProtocolVersion.cpp
impl/Scan.cpp impl/ParameterParsing.cpp
impl/Stream.cpp
impl/SubsonicId.cpp impl/SubsonicId.cpp
impl/SubsonicResource.cpp impl/SubsonicResource.cpp
impl/SubsonicResponse.cpp impl/SubsonicResponse.cpp
impl/Utils.cpp
) )
target_include_directories(lmssubsonic INTERFACE target_include_directories(lmssubsonic INTERFACE
@@ -13,6 +34,7 @@ target_include_directories(lmssubsonic INTERFACE
) )
target_include_directories(lmssubsonic PRIVATE target_include_directories(lmssubsonic PRIVATE
impl
include include
) )
+7 -7
View File
@@ -24,11 +24,11 @@
namespace API::Subsonic namespace API::Subsonic
{ {
struct ClientInfo struct ClientInfo
{ {
std::string name; std::string name;
std::string user; std::string user;
std::string password; std::string password;
ProtocolVersion version; ProtocolVersion version;
}; };
} }
@@ -0,0 +1,42 @@
/*
* Copyright (C) 2023 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "ParameterParsing.hpp"
namespace API::Subsonic
{
bool hasParameter(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
{
return parameterMap.find(param) != std::cend(parameterMap);
}
std::string decodePasswordIfNeeded(const std::string& password)
{
if (password.find("enc:") == 0)
{
auto decodedPassword{ StringUtils::stringFromHex(password.substr(4)) };
if (!decodedPassword)
return password; // fallback on plain password
return *decodedPassword;
}
return password;
}
}
+46 -49
View File
@@ -16,10 +16,15 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>. * along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/ */
#pragma once #pragma once
#include <Wt/Http/Request.h> #include <Wt/Http/Request.h>
#include <optional>
#include <vector>
#include <string>
#include "services/database/Types.hpp" #include "services/database/Types.hpp"
#include "utils/String.hpp" #include "utils/String.hpp"
#include "SubsonicResponse.hpp" #include "SubsonicResponse.hpp"
@@ -27,65 +32,57 @@
namespace API::Subsonic namespace API::Subsonic
{ {
template<typename T> template<typename T>
std::vector<T> std::vector<T> getMultiParametersAs(const Wt::Http::ParameterMap& parameterMap, const std::string& paramName)
getMultiParametersAs(const Wt::Http::ParameterMap& parameterMap, const std::string& paramName) {
{ std::vector<T> res;
std::vector<T> res;
auto it = parameterMap.find(paramName); auto it = parameterMap.find(paramName);
if (it == parameterMap.end()) if (it == parameterMap.end())
return res; return res;
for (const std::string& param : it->second) for (const std::string& param : it->second)
{ {
auto value {StringUtils::readAs<T>(param)}; auto value{ StringUtils::readAs<T>(param) };
if (value) if (value)
res.emplace_back(std::move(*value)); res.emplace_back(std::move(*value));
} }
return res; return res;
} }
template<typename T> template<typename T>
std::vector<T> std::vector<T> getMandatoryMultiParametersAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
getMandatoryMultiParametersAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param) {
{ std::vector<T> res{ getMultiParametersAs<T>(parameterMap, param) };
std::vector<T> res {getMultiParametersAs<T>(parameterMap, param)}; if (res.empty())
if (res.empty()) throw RequiredParameterMissingError{ param };
throw RequiredParameterMissingError {param};
return res; return res;
} }
template<typename T> template<typename T>
std::optional<T> std::optional<T> getParameterAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
getParameterAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param) {
{ std::vector<T> params{ getMultiParametersAs<T>(parameterMap, param) };
std::vector<T> params {getMultiParametersAs<T>(parameterMap, param)};
if (params.size() != 1) if (params.size() != 1)
return std::nullopt; return std::nullopt;
return T { std::move(params.front()) }; return T{ std::move(params.front()) };
} }
template<typename T> template<typename T>
T T getMandatoryParameterAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
getMandatoryParameterAs(const Wt::Http::ParameterMap& parameterMap, const std::string& param) {
{ auto res{ getParameterAs<T>(parameterMap, param) };
auto res {getParameterAs<T>(parameterMap, param)}; if (!res)
if (!res) throw RequiredParameterMissingError{ param };
throw RequiredParameterMissingError {param};
return *res; return *res;
} }
inline bool hasParameter(const Wt::Http::ParameterMap& parameterMap, const std::string& param);
bool std::string decodePasswordIfNeeded(const std::string& password);
hasParameter(const Wt::Http::ParameterMap& parameterMap, const std::string& param)
{
return parameterMap.find(param) != std::cend(parameterMap);
}
} }
+25 -26
View File
@@ -21,36 +21,35 @@
namespace StringUtils namespace StringUtils
{ {
template<> template<>
std::optional<API::Subsonic::ProtocolVersion> std::optional<API::Subsonic::ProtocolVersion> readAs(std::string_view str)
readAs(std::string_view str) {
{ // Expects "X.Y.Z"
// Expects "X.Y.Z" const auto numbers{ StringUtils::splitString(str, ".") };
const auto numbers {StringUtils::splitString(str, ".")}; if (numbers.size() < 2 || numbers.size() > 3)
if (numbers.size() < 2 || numbers.size() > 3) return std::nullopt;
return std::nullopt;
API::Subsonic::ProtocolVersion version; API::Subsonic::ProtocolVersion version;
auto number {StringUtils::readAs<unsigned>(numbers[0])}; auto number{ StringUtils::readAs<unsigned>(numbers[0]) };
if (!number) if (!number)
return std::nullopt; return std::nullopt;
version.major = *number; version.major = *number;
number = {StringUtils::readAs<unsigned>(numbers[1])}; number = { StringUtils::readAs<unsigned>(numbers[1]) };
if (!number) if (!number)
return std::nullopt; return std::nullopt;
version.minor = *number; version.minor = *number;
if (numbers.size() == 3) if (numbers.size() == 3)
{ {
number = {StringUtils::readAs<unsigned>(numbers[2])}; number = { StringUtils::readAs<unsigned>(numbers[2]) };
if (!number) if (!number)
return std::nullopt; return std::nullopt;
version.patch = *number; version.patch = *number;
} }
return version; return version;
} }
} }
+10 -8
View File
@@ -23,18 +23,20 @@
namespace API::Subsonic namespace API::Subsonic
{ {
struct ProtocolVersion struct ProtocolVersion
{ {
unsigned major {}; unsigned major{};
unsigned minor {}; unsigned minor{};
unsigned patch {}; unsigned patch{};
}; };
static inline constexpr ProtocolVersion defaultServerProtocolVersion {1, 16, 0}; static inline constexpr ProtocolVersion defaultServerProtocolVersion{ 1, 16, 0 };
static inline constexpr std::string_view serverVersion{ "1" };
} }
namespace StringUtils namespace StringUtils
{ {
template<> std::optional<API::Subsonic::ProtocolVersion> readAs(std::string_view str); template<>
std::optional<API::Subsonic::ProtocolVersion> readAs(std::string_view str);
} }
+9 -9
View File
@@ -29,18 +29,18 @@
namespace Database namespace Database
{ {
class Session; class Session;
} }
namespace API::Subsonic namespace API::Subsonic
{ {
struct RequestContext struct RequestContext
{ {
const Wt::Http::ParameterMap& parameters; const Wt::Http::ParameterMap& parameters;
Database::Session& dbSession; Database::Session& dbSession;
Database::UserId userId; Database::UserId userId;
ClientInfo clientInfo; ClientInfo clientInfo;
ProtocolVersion serverProtocolVersion; ProtocolVersion serverProtocolVersion;
}; };
} }
-72
View File
@@ -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;
}
}
-182
View File
@@ -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
+75 -85
View File
@@ -26,114 +26,104 @@
namespace API::Subsonic namespace API::Subsonic
{ {
std::string std::string idToString(Database::ArtistId id)
idToString(Database::ArtistId id) {
{ return "ar-" + id.toString();
return "ar-" + id.toString(); }
}
std::string std::string idToString(Database::ReleaseId id)
idToString(Database::ReleaseId id) {
{ return "al-" + id.toString();
return "al-" + id.toString(); }
}
std::string std::string idToString(RootId)
idToString(RootId) {
{ return "root";
return "root"; }
}
std::string std::string idToString(Database::TrackId id)
idToString(Database::TrackId id) {
{ return "tr-" + id.toString();
return "tr-" + id.toString(); }
}
std::string std::string idToString(Database::TrackListId id)
idToString(Database::TrackListId id) {
{ return "pl-" + id.toString();
return "pl-" + id.toString(); }
}
} // namespace API::Subsonic } // namespace API::Subsonic
namespace StringUtils namespace StringUtils
{ {
template<> template<>
std::optional<Database::ArtistId> std::optional<Database::ArtistId> readAs(std::string_view str)
readAs(std::string_view str) {
{ std::vector<std::string_view> values{ StringUtils::splitString(str, "-") };
std::vector<std::string_view> values {StringUtils::splitString(str, "-")}; if (values.size() != 2)
if (values.size() != 2) return std::nullopt;
return std::nullopt;
if (values[0] != "ar") if (values[0] != "ar")
return std::nullopt; return std::nullopt;
if (const auto value {StringUtils::readAs<Database::ArtistId::ValueType>(values[1])}) if (const auto value{ StringUtils::readAs<Database::ArtistId::ValueType>(values[1]) })
return Database::ArtistId {*value}; return Database::ArtistId{ *value };
return std::nullopt; return std::nullopt;
} }
template<> template<>
std::optional<Database::ReleaseId> std::optional<Database::ReleaseId> readAs(std::string_view str)
readAs(std::string_view str) {
{ std::vector<std::string_view> values{ StringUtils::splitString(str, "-") };
std::vector<std::string_view> values {StringUtils::splitString(str, "-")}; if (values.size() != 2)
if (values.size() != 2) return std::nullopt;
return std::nullopt;
if (values[0] != "al") if (values[0] != "al")
return std::nullopt; return std::nullopt;
if (const auto value {StringUtils::readAs<Database::ReleaseId::ValueType>(values[1])}) if (const auto value{ StringUtils::readAs<Database::ReleaseId::ValueType>(values[1]) })
return Database::ReleaseId {*value}; return Database::ReleaseId{ *value };
return std::nullopt; return std::nullopt;
} }
template<> template<>
std::optional<API::Subsonic::RootId> std::optional<API::Subsonic::RootId> readAs(std::string_view str)
readAs(std::string_view str) {
{ if (str == "root")
if (str == "root") return API::Subsonic::RootId{};
return API::Subsonic::RootId {};
return std::nullopt; return std::nullopt;
} }
template<> template<>
std::optional<Database::TrackId> std::optional<Database::TrackId> readAs(std::string_view str)
readAs(std::string_view str) {
{ std::vector<std::string_view> values{ StringUtils::splitString(str, "-") };
std::vector<std::string_view> values {StringUtils::splitString(str, "-")}; if (values.size() != 2)
if (values.size() != 2) return std::nullopt;
return std::nullopt;
if (values[0] != "tr") if (values[0] != "tr")
return std::nullopt; return std::nullopt;
if (const auto value {StringUtils::readAs<Database::TrackId::ValueType>(values[1])}) if (const auto value{ StringUtils::readAs<Database::TrackId::ValueType>(values[1]) })
return Database::TrackId {*value}; return Database::TrackId{ *value };
return std::nullopt; return std::nullopt;
} }
template<> template<>
std::optional<Database::TrackListId> std::optional<Database::TrackListId> readAs(std::string_view str)
readAs(std::string_view str) {
{ std::vector<std::string_view> values{ StringUtils::splitString(str, "-") };
std::vector<std::string_view> values {StringUtils::splitString(str, "-")}; if (values.size() != 2)
if (values.size() != 2) return std::nullopt;
return std::nullopt;
if (values[0] != "pl") if (values[0] != "pl")
return std::nullopt; return std::nullopt;
if (const auto value {StringUtils::readAs<Database::TrackListId::ValueType>(values[1])}) if (const auto value{ StringUtils::readAs<Database::TrackListId::ValueType>(values[1]) })
return Database::TrackListId {*value}; return Database::TrackListId{ *value };
return std::nullopt; return std::nullopt;
} }
} }
+16 -21
View File
@@ -27,36 +27,31 @@
namespace API::Subsonic namespace API::Subsonic
{ {
struct RootId {}; struct RootId {};
std::string idToString(Database::ArtistId id); std::string idToString(Database::ArtistId id);
std::string idToString(Database::ReleaseId id); std::string idToString(Database::ReleaseId id);
std::string idToString(Database::TrackId id); std::string idToString(Database::TrackId id);
std::string idToString(Database::TrackListId id); std::string idToString(Database::TrackListId id);
std::string idToString(RootId); std::string idToString(RootId);
} // namespace API::Subsonic } // namespace API::Subsonic
// Used to parse parameters // Used to parse parameters
namespace StringUtils namespace StringUtils
{ {
template<> template<>
std::optional<API::Subsonic::RootId> std::optional<API::Subsonic::RootId> readAs(std::string_view str);
readAs(std::string_view str);
template<> template<>
std::optional<Database::ArtistId> std::optional<Database::ArtistId> readAs(std::string_view str);
readAs(std::string_view str);
template<> template<>
std::optional<Database::ReleaseId> std::optional<Database::ReleaseId> readAs(std::string_view str);
readAs(std::string_view str);
template<> template<>
std::optional<Database::TrackId> std::optional<Database::TrackId> readAs(std::string_view str);
readAs(std::string_view str);
template<> template<>
std::optional<Database::TrackListId> std::optional<Database::TrackListId> readAs(std::string_view str);
readAs(std::string_view str);
} }
File diff suppressed because it is too large Load Diff
+15 -15
View File
@@ -30,28 +30,28 @@
namespace Database namespace Database
{ {
class Db; class Db;
} }
namespace API::Subsonic namespace API::Subsonic
{ {
class SubsonicResource final : public Wt::WResource class SubsonicResource final : public Wt::WResource
{ {
public: public:
SubsonicResource(Database::Db& db); SubsonicResource(Database::Db& db);
private: private:
void handleRequest(const Wt::Http::Request &request, Wt::Http::Response &response) override; void handleRequest(const Wt::Http::Request &request, Wt::Http::Response &response) override;
ProtocolVersion getServerProtocolVersion(const std::string& clientName) const; ProtocolVersion getServerProtocolVersion(const std::string& clientName) const;
static void checkProtocolVersion(ProtocolVersion client, ProtocolVersion server); static void checkProtocolVersion(ProtocolVersion client, ProtocolVersion server);
ClientInfo getClientInfo(const Wt::Http::ParameterMap& parameters); ClientInfo getClientInfo(const Wt::Http::ParameterMap& parameters);
RequestContext buildRequestContext(const Wt::Http::Request& request); RequestContext buildRequestContext(const Wt::Http::Request& request);
Database::UserId authenticateUser(const Wt::Http::Request& request, const ClientInfo& clientInfo); Database::UserId authenticateUser(const Wt::Http::Request& request, const ClientInfo& clientInfo);
const std::unordered_map<std::string, ProtocolVersion> _serverProtocolVersionsByClient; const std::unordered_map<std::string, ProtocolVersion> _serverProtocolVersionsByClient;
Database::Db& _db; Database::Db& _db;
}; };
} // namespace } // namespace
+220 -199
View File
@@ -19,6 +19,7 @@
#include "SubsonicResponse.hpp" #include "SubsonicResponse.hpp"
#include <cassert>
#include <Wt/Json/Array.h> #include <Wt/Json/Array.h>
#include <Wt/Json/Object.h> #include <Wt/Json/Object.h>
#include <Wt/Json/Value.h> #include <Wt/Json/Value.h>
@@ -33,250 +34,270 @@
namespace API::Subsonic namespace API::Subsonic
{ {
std::string_view ResponseFormatToMimeType(ResponseFormat format)
{
switch (format)
{
case ResponseFormat::xml: return "text/xml";
case ResponseFormat::json: return "application/json";
}
std::string return "";
ResponseFormatToMimeType(ResponseFormat format) }
{
switch (format)
{
case ResponseFormat::xml: return "text/xml";
case ResponseFormat::json: return "application/json";
}
return ""; void Response::Node::setValue(std::string_view value)
} {
assert(_children.empty() && _childrenArrays.empty() && _childrenValues.empty());
_value = std::string{ value };
}
void void Response::Node::setValue(long long value)
Response::Node::setValue(std::string_view value) {
{ assert(_children.empty() && _childrenArrays.empty() && _childrenValues.empty());
if (!_children.empty() || !_childrenArrays.empty()) _value = value;
throw LmsException {"Node already has children"}; }
_value = std::string {value}; void Response::Node::setAttribute(std::string_view key, std::string_view value)
} {
_attributes[std::string{ key }] = std::string{ value };
}
void void Response::Node::addChild(const std::string& key, Node node)
Response::Node::setValue(long long value) {
{ assert(!_value);
if (!_children.empty() || !_childrenArrays.empty()) _children[key].emplace_back(std::move(node));
throw LmsException {"Node already has children"}; }
_value = value; void Response::Node::createEmptyArrayChild(std::string_view key)
} {
assert(!_value);
_childrenArrays.emplace(key, std::vector<Node>{});
}
void void Response::Node::addArrayChild(std::string_view key, Node node)
Response::Node::setAttribute(std::string_view key, std::string_view value) {
{ assert(!_value);
_attributes[std::string {key}] = std::string {value}; _childrenArrays[std::string{ key }].emplace_back(std::move(node));
} }
void void Response::Node::createEmptyArrayValue(std::string_view key)
Response::Node::addChild(const std::string& key, Node node) {
{ assert (!_value);
if (_value) _childrenValues.emplace(key, ValuesType{});
throw LmsException {"Node already has a value"}; }
_children[key].emplace_back(std::move(node)); void Response::Node::addArrayValue(std::string_view key, std::string_view value)
} {
assert(!_value);
auto& values{ _childrenValues[std::string{ key }] };
values.push_back(std::string{ value });
assert(std::all_of(std::cbegin(values) + 1, std::cend(values), [&](const ValueType& value) {return value.index() == values.front().index();}));
}
void void Response::Node::addArrayValue(std::string_view key, long long value)
Response::Node::addArrayChild(const std::string& key, Node node) {
{ assert(!_value);
if (_value) auto& values {_childrenValues[std::string{ key }]};
throw LmsException {"Node already has a value"}; values.push_back(value);
assert(std::all_of(std::cbegin(values) + 1, std::cend(values), [&](const ValueType& value) {return value.index() == values.front().index();}));
}
_childrenArrays[key].emplace_back(std::move(node)); Response::Node& Response::Node::createChild(const std::string& key)
} {
_children[key].emplace_back();
return _children[key].back();
}
Response::Node& Response::Node::createArrayChild(const std::string& key)
{
_childrenArrays[key].emplace_back();
return _childrenArrays[key].back();
}
Response::Node& void Response::Node::setVersionAttribute(ProtocolVersion protocolVersion)
Response::Node::createChild(const std::string& key) {
{ setAttribute("version", std::to_string(protocolVersion.major) + "." + std::to_string(protocolVersion.minor) + "." + std::to_string(protocolVersion.patch));
_children[key].emplace_back(); }
return _children[key].back();
}
Response::Node& Response Response::createOkResponse(ProtocolVersion protocolVersion)
Response::Node::createArrayChild(const std::string& key) {
{ Response response;
_childrenArrays[key].emplace_back(); Node& responseNode{ response._root.createChild("subsonic-response") };
return _childrenArrays[key].back();
}
void responseNode.setAttribute("status", "ok");
Response::Node::setVersionAttribute(ProtocolVersion protocolVersion) responseNode.setVersionAttribute(protocolVersion);
{
setAttribute("version", std::to_string(protocolVersion.major) + "." + std::to_string(protocolVersion.minor) + "." + std::to_string(protocolVersion.patch));
}
Response // OpenSubsonic mandatory fields
Response::createOkResponse(ProtocolVersion protocolVersion) responseNode.setAttribute("type", "lms");
{ responseNode.setAttribute("serverVersion", serverVersion);
Response response; responseNode.setAttribute("openSubsonic", true);
Node& responseNode {response._root.createChild("subsonic-response")};
responseNode.setAttribute("status", "ok"); return response;
responseNode.setVersionAttribute(protocolVersion); }
responseNode.setAttribute("type", "lms"); // non standard field to ease client hacks
return response; Response Response::createFailedResponse(ProtocolVersion protocolVersion, const Error& error)
} {
Response response;
Node& responseNode{ response._root.createChild("subsonic-response") };
Response responseNode.setAttribute("status", "failed");
Response::createFailedResponse(ProtocolVersion protocolVersion, const Error& error) responseNode.setVersionAttribute(protocolVersion);
{ responseNode.setAttribute("type", "lms"); // non standard field to ease client hacks
Response response;
Node& responseNode {response._root.createChild("subsonic-response")};
responseNode.setAttribute("status", "failed"); Node& errorNode{ responseNode.createChild("error") };
responseNode.setVersionAttribute(protocolVersion); errorNode.setAttribute("code", static_cast<int>(error.getCode()));
responseNode.setAttribute("type", "lms"); // non standard field to ease client hacks errorNode.setAttribute("message", error.getMessage());
Node& errorNode {responseNode.createChild("error")}; return response;
errorNode.setAttribute("code", static_cast<int>(error.getCode())); }
errorNode.setAttribute("message", error.getMessage());
return response; void Response::addNode(const std::string& key, Node node)
} {
return _root._children["subsonic-response"].front().addChild(key, std::move(node));
}
void Response::Node& Response::createNode(const std::string& key)
Response::addNode(const std::string& key, Node node) {
{ return _root._children["subsonic-response"].front().createChild(key);
return _root._children["subsonic-response"].front().addChild(key, std::move(node)); }
}
Response::Node& Response::Node& Response::createArrayNode(const std::string& key)
Response::createNode(const std::string& key) {
{ return _root._children["subsonic-response"].front().createArrayChild(key);
return _root._children["subsonic-response"].front().createChild(key); }
}
Response::Node& void Response::write(std::ostream& os, ResponseFormat format)
Response::createArrayNode(const std::string& key) {
{ switch (format)
return _root._children["subsonic-response"].front().createArrayChild(key); {
} case ResponseFormat::xml:
writeXML(os);
break;
case ResponseFormat::json:
writeJSON(os);
break;
}
}
void void Response::writeXML(std::ostream& os)
Response::write(std::ostream& os, ResponseFormat format) {
{ std::function<boost::property_tree::ptree(const Node&)> nodeToPropertyTree = [&](const Node& node)
switch (format) {
{ boost::property_tree::ptree res;
case ResponseFormat::xml:
writeXML(os);
break;
case ResponseFormat::json:
writeJSON(os);
break;
}
}
void for (auto itAttribute : node._attributes)
Response::writeXML(std::ostream& os) {
{ if (std::holds_alternative<std::string>(itAttribute.second))
std::function<boost::property_tree::ptree(const Response::Node&)> nodeToPropertyTree = [&] (const Response::Node& node) res.put("<xmlattr>." + itAttribute.first, std::get<std::string>(itAttribute.second));
{ else if (std::holds_alternative<bool>(itAttribute.second))
boost::property_tree::ptree res; res.put("<xmlattr>." + itAttribute.first, std::get<bool>(itAttribute.second));
else if (std::holds_alternative<float>(itAttribute.second))
res.put("<xmlattr>." + itAttribute.first, std::get<float>(itAttribute.second));
else if (std::holds_alternative<long long>(itAttribute.second))
res.put("<xmlattr>." + itAttribute.first, std::get<long long>(itAttribute.second));
}
for (auto itAttribute : node._attributes) auto valueToPropertyTree = [](const Node::ValueType& value)
{ {
if (std::holds_alternative<std::string>(itAttribute.second)) boost::property_tree::ptree res;
res.put("<xmlattr>." + itAttribute.first, std::get<std::string>(itAttribute.second)); std::visit([&](const auto& rawValue)
else if (std::holds_alternative<bool>(itAttribute.second)) {
res.put("<xmlattr>." + itAttribute.first, std::get<bool>(itAttribute.second)); res.put_value(rawValue);
else if (std::holds_alternative<long long>(itAttribute.second)) }, value);
res.put("<xmlattr>." + itAttribute.first, std::get<long long>(itAttribute.second));
}
if (node._value) return res;
{ };
const auto& value {*node._value};
if (std::holds_alternative<std::string>(value)) if (node._value)
res.put_value(std::get<std::string>(value)); {
else if (std::holds_alternative<bool>(value)) res = valueToPropertyTree(*node._value);
res.put_value(std::get<bool>(value)); }
else if (std::holds_alternative<long long>(value)) else
res.put_value(std::get<long long>(value)); {
} for (const auto& [key, childNodes] : node._children)
else {
{ for (const Node& childNode : childNodes)
for (auto itChildNode : node._children) res.add_child(key, nodeToPropertyTree(childNode));
{ }
for (const Response::Node& childNode : itChildNode.second)
res.add_child(itChildNode.first, nodeToPropertyTree(childNode));
}
for (auto itChildArrayNode : node._childrenArrays) for (const auto& [key, childArrayNodes] : node._childrenArrays)
{ {
const std::vector<Response::Node>& childArrayNodes {itChildArrayNode.second}; for (const Node& childNode : childArrayNodes)
res.add_child(key, nodeToPropertyTree(childNode));
}
for (const Response::Node& childNode : childArrayNodes ) for (const auto& [key, childArrayValues] : node._childrenValues)
res.add_child(itChildArrayNode.first, nodeToPropertyTree(childNode)); {
} for (const Response::Node::ValueType& value : childArrayValues)
} res.add_child(key, valueToPropertyTree(value));
}
}
return res; return res;
}; };
boost::property_tree::ptree root {nodeToPropertyTree(_root)}; boost::property_tree::ptree root{ nodeToPropertyTree(_root) };
boost::property_tree::write_xml(os, root); boost::property_tree::write_xml(os, root);
} }
void void Response::writeJSON(std::ostream& os)
Response::writeJSON(std::ostream& os) {
{ namespace Json = Wt::Json;
namespace Json = Wt::Json;
std::function<Json::Object(const Response::Node&)> nodeToJsonObject = [&] (const Response::Node& node) std::function<Json::Object(const Response::Node&)> nodeToJsonObject = [&](const Response::Node& node)
{ {
Json::Object res; Json::Object res;
auto valueToJsonValue {[](const Node::ValueType& value) -> Json::Value auto valueToJsonValue{ [](const Node::ValueType& value) -> Json::Value
{ {
if (std::holds_alternative<std::string>(value)) Json::Value res;
return Json::Value {std::get<std::string>(value)}; std::visit([&](const auto& rawValue)
else if (std::holds_alternative<bool>(value)) {
return Json::Value {std::get<bool>(value)}; res = Json::Value{ rawValue };
else if (std::holds_alternative<long long>(value)) }, value);
return Json::Value {std::get<long long>(value)}; return res;
} };
throw LmsException("Unexpected value type"); for (auto itAttribute : node._attributes)
}}; res[itAttribute.first] = valueToJsonValue(itAttribute.second);
for (auto itAttribute : node._attributes) if (node._value)
res[itAttribute.first] = valueToJsonValue(itAttribute.second); {
res["value"] = valueToJsonValue(*node._value);
}
else
{
for (const auto& [key, childNodes] : node._children)
{
for (const Response::Node& childNode : childNodes)
res[key] = nodeToJsonObject(childNode);
}
if (node._value) for (const auto& [key, childArrayNodes] : node._childrenArrays)
{ {
res["value"] = valueToJsonValue(*node._value); Json::Array array;
} for (const Response::Node& childNode : childArrayNodes)
else array.emplace_back(nodeToJsonObject(childNode));
{
for (auto itChildNode : node._children)
{
for (const Response::Node& childNode : itChildNode.second)
res[itChildNode.first] = nodeToJsonObject(childNode);
}
for (auto itChildArrayNode : node._childrenArrays) res[key] = std::move(array);
{ }
const std::vector<Response::Node>& childArrayNodes {itChildArrayNode .second};
Json::Array array; for (const auto& [key, childValues] : node._childrenValues)
for (const Response::Node& childNode : childArrayNodes ) {
array.emplace_back(nodeToJsonObject(childNode)); Json::Array array;
for (const Node::ValueType& childValue : childValues)
array.emplace_back(valueToJsonValue(childValue));
res[itChildArrayNode.first] = std::move(array); res[key] = std::move(array);
} }
} }
return res; return res;
}; };
Json::Object root {nodeToJsonObject(_root)}; Json::Object root{ nodeToJsonObject(_root) };
os << Json::serialize(root); os << Json::serialize(root);
} }
} // namespace } // namespace
+187 -176
View File
@@ -30,220 +30,231 @@
namespace API::Subsonic namespace API::Subsonic
{ {
enum class ResponseFormat enum class ResponseFormat
{ {
xml, xml,
json, json,
}; };
std::string ResponseFormatToMimeType(ResponseFormat format); std::string_view ResponseFormatToMimeType(ResponseFormat format);
class Error class Error
{ {
public: public:
enum class Code enum class Code
{ {
Generic = 0, Generic = 0,
RequiredParameterMissing = 10, RequiredParameterMissing = 10,
ClientMustUpgrade = 20, ClientMustUpgrade = 20,
ServerMustUpgrade = 30, ServerMustUpgrade = 30,
WrongUsernameOrPassword = 40, WrongUsernameOrPassword = 40,
TokenAuthenticationNotSupportedForLDAPUsers = 41, TokenAuthenticationNotSupportedForLDAPUsers = 41,
UserNotAuthorized = 50, UserNotAuthorized = 50,
RequestedDataNotFound = 70, RequestedDataNotFound = 70,
}; };
Error(Code code) : _code {code} {} Error(Code code) : _code{ code } {}
virtual std::string getMessage() const = 0; virtual std::string getMessage() const = 0;
Code getCode() const { return _code; } Code getCode() const { return _code; }
private: private:
const Code _code; const Code _code;
}; };
class GenericError : public Error class GenericError : public Error
{ {
public: public:
GenericError() : Error {Code::Generic} {} GenericError() : Error{ Code::Generic } {}
}; };
class RequiredParameterMissingError : public Error class RequiredParameterMissingError : public Error
{ {
public: public:
RequiredParameterMissingError(std::string_view param) RequiredParameterMissingError(std::string_view param)
: Error {Code::RequiredParameterMissing} : Error{ Code::RequiredParameterMissing }
, _param {param} , _param{ param }
{} {}
private: private:
std::string getMessage() const override { return "Required parameter '" + _param + "' is missing."; } std::string getMessage() const override { return "Required parameter '" + _param + "' is missing."; }
std::string _param; std::string _param;
}; };
class ClientMustUpgradeError : public Error class ClientMustUpgradeError : public Error
{ {
public: public:
ClientMustUpgradeError() : Error {Code::ClientMustUpgrade} {} ClientMustUpgradeError() : Error{ Code::ClientMustUpgrade } {}
private: private:
std::string getMessage() const override { return "Incompatible Subsonic REST protocol version. Client must upgrade."; } std::string getMessage() const override { return "Incompatible Subsonic REST protocol version. Client must upgrade."; }
}; };
class ServerMustUpgradeError : public Error class ServerMustUpgradeError : public Error
{ {
public: public:
ServerMustUpgradeError() : Error {Code::ServerMustUpgrade} {} ServerMustUpgradeError() : Error{ Code::ServerMustUpgrade } {}
private: private:
std::string getMessage() const override { return "Incompatible Subsonic REST protocol version. Server must upgrade."; } std::string getMessage() const override { return "Incompatible Subsonic REST protocol version. Server must upgrade."; }
}; };
class WrongUsernameOrPasswordError : public Error class WrongUsernameOrPasswordError : public Error
{ {
public: public:
WrongUsernameOrPasswordError() : Error {Code::WrongUsernameOrPassword} {} WrongUsernameOrPasswordError() : Error{ Code::WrongUsernameOrPassword } {}
private: private:
std::string getMessage() const override { return "Wrong username or password."; } std::string getMessage() const override { return "Wrong username or password."; }
}; };
class TokenAuthenticationNotSupportedForLDAPUsersError : public Error class TokenAuthenticationNotSupportedForLDAPUsersError : public Error
{ {
public: public:
TokenAuthenticationNotSupportedForLDAPUsersError() : Error {Code::TokenAuthenticationNotSupportedForLDAPUsers} {} TokenAuthenticationNotSupportedForLDAPUsersError() : Error{ Code::TokenAuthenticationNotSupportedForLDAPUsers } {}
private: private:
std::string getMessage() const override { return "Token authentication not supported for LDAP users."; } std::string getMessage() const override { return "Token authentication not supported for LDAP users."; }
}; };
class UserNotAuthorizedError : public Error class UserNotAuthorizedError : public Error
{ {
public: public:
UserNotAuthorizedError () : Error {Code::UserNotAuthorized} {} UserNotAuthorizedError() : Error{ Code::UserNotAuthorized } {}
private: private:
std::string getMessage() const override { return "User is not authorized for the given operation."; } std::string getMessage() const override { return "User is not authorized for the given operation."; }
}; };
class RequestedDataNotFoundError : public Error class RequestedDataNotFoundError : public Error
{ {
public: public:
RequestedDataNotFoundError() : Error {Code::RequestedDataNotFound} {} RequestedDataNotFoundError() : Error{ Code::RequestedDataNotFound } {}
private: private:
std::string getMessage() const override { return "The requested data was not found."; } std::string getMessage() const override { return "The requested data was not found."; }
}; };
class InternalErrorGenericError : public GenericError class InternalErrorGenericError : public GenericError
{ {
public: public:
InternalErrorGenericError(const std::string& message) : _message {message} {} InternalErrorGenericError(const std::string& message) : _message{ message } {}
private: private:
std::string getMessage() const override { return "Internal error: " + _message; } std::string getMessage() const override { return "Internal error: " + _message; }
const std::string _message; const std::string _message;
}; };
class LoginThrottledGenericError : public GenericError class LoginThrottledGenericError : public GenericError
{ {
std::string getMessage() const override { return "Login throttled, too many attempts"; } std::string getMessage() const override { return "Login throttled, too many attempts"; }
}; };
class NotImplementedGenericError : public GenericError class NotImplementedGenericError : public GenericError
{ {
std::string getMessage() const override { return "Not implemented"; } std::string getMessage() const override { return "Not implemented"; }
}; };
class UnknownEntryPointGenericError : public GenericError class UnknownEntryPointGenericError : public GenericError
{ {
std::string getMessage() const override { return "Unknown API method"; } std::string getMessage() const override { return "Unknown API method"; }
}; };
class PasswordTooWeakGenericError : public GenericError class PasswordTooWeakGenericError : public GenericError
{ {
std::string getMessage() const override { return "Password too weak"; } std::string getMessage() const override { return "Password too weak"; }
}; };
class PasswordMustMatchLoginNameGenericError : public GenericError class PasswordMustMatchLoginNameGenericError : public GenericError
{ {
std::string getMessage() const override { return "Password must match login name"; } std::string getMessage() const override { return "Password must match login name"; }
}; };
class DemoUserCannotChangePasswordGenericError : public GenericError class DemoUserCannotChangePasswordGenericError : public GenericError
{ {
std::string getMessage() const override { return "Demo user cannot change its password"; } std::string getMessage() const override { return "Demo user cannot change its password"; }
}; };
class UserAlreadyExistsGenericError : public GenericError class UserAlreadyExistsGenericError : public GenericError
{ {
std::string getMessage() const override { return "User already exists"; } std::string getMessage() const override { return "User already exists"; }
}; };
class BadParameterGenericError : public GenericError class BadParameterGenericError : public GenericError
{ {
public: public:
BadParameterGenericError(const std::string& parameterName) : _parameterName {parameterName} {} BadParameterGenericError(const std::string& parameterName) : _parameterName{ parameterName } {}
private: private:
std::string getMessage() const override { return "Parameter '" + _parameterName + "': bad value"; } std::string getMessage() const override { return "Parameter '" + _parameterName + "': bad value"; }
const std::string _parameterName; const std::string _parameterName;
}; };
class Response class Response
{ {
public: public:
class Node class Node
{ {
public: public:
void setAttribute(std::string_view key, std::string_view value); void setAttribute(std::string_view key, std::string_view value);
template <typename T, std::enable_if_t<std::is_arithmetic<T>::value>* = nullptr> template <typename T, std::enable_if_t<std::is_arithmetic<T>::value>* = nullptr>
void setAttribute(std::string_view key, T value) void setAttribute(std::string_view key, T value)
{ {
if constexpr (std::is_same<bool, T>::value) if constexpr (std::is_same<bool, T>::value)
_attributes[std::string {key}] = value; _attributes[std::string{ key }] = value;
else else if constexpr (std::is_floating_point<T>::value)
_attributes[std::string {key}] = static_cast<long long>(value); _attributes[std::string{ key }] = static_cast<float>(value);
} else if constexpr (std::is_integral<T>::value)
_attributes[std::string{ key }] = static_cast<long long>(value);
else
static_assert("Unhandled type");
}
// A Node has either a value or some children // A Node has either a single value or an array of values or some children
void setValue(std::string_view value); void setValue(std::string_view value);
void setValue(long long value); void setValue(long long value);
Node& createChild(const std::string& key); Node& createChild(const std::string& key);
Node& createArrayChild(const std::string& key); Node& createArrayChild(const std::string& key);
void addChild(const std::string& key, Node node); void addChild(const std::string& key, Node node);
void addArrayChild(const std::string& key, Node node); void createEmptyArrayChild(std::string_view key);
void addArrayChild(std::string_view key, Node node);
void createEmptyArrayValue(std::string_view key);
void addArrayValue(std::string_view key, std::string_view value);
void addArrayValue(std::string_view key, long long value);
private: private:
void setVersionAttribute(ProtocolVersion version); void setVersionAttribute(ProtocolVersion version);
friend class Response; friend class Response;
using ValueType = std::variant<std::string, bool, long long>; using ValueType = std::variant<std::string, bool, float, long long>;
std::map<std::string, ValueType> _attributes; std::map<std::string, ValueType> _attributes;
std::optional<ValueType> _value; std::optional<ValueType> _value;
std::map<std::string, std::vector<Node>> _children; std::map<std::string, std::vector<Node>> _children;
std::map<std::string, std::vector<Node>> _childrenArrays; std::map<std::string, std::vector<Node>> _childrenArrays;
};
static Response createOkResponse(ProtocolVersion protocolVersion); using ValuesType = std::vector<ValueType>;
static Response createFailedResponse(ProtocolVersion protocolVersion, const Error& error); std::map<std::string, ValuesType> _childrenValues;
};
virtual ~Response() {} static Response createOkResponse(ProtocolVersion protocolVersion);
Response(const Response&) = delete; static Response createFailedResponse(ProtocolVersion protocolVersion, const Error& error);
Response& operator=(const Response&) = delete;
Response(Response&&) = default;
Response& operator=(Response&&) = default;
void addNode(const std::string& key, Node node); virtual ~Response() {}
Node& createNode(const std::string& key); Response(const Response&) = delete;
Node& createArrayNode(const std::string& key); Response& operator=(const Response&) = delete;
Response(Response&&) = default;
Response& operator=(Response&&) = default;
void write(std::ostream& os, ResponseFormat format); void addNode(const std::string& key, Node node);
Node& createNode(const std::string& key);
Node& createArrayNode(const std::string& key);
private: void write(std::ostream& os, ResponseFormat format);
void writeJSON(std::ostream& os);
void writeXML(std::ostream& os);
Response() = default; private:
Node _root; void writeJSON(std::ostream& os);
}; void writeXML(std::ostream& os);
Response() = default;
Node _root;
};
} // namespace } // namespace
+41
View File
@@ -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, "/", "_");
}
}
+28
View File
@@ -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;
}
}
@@ -24,7 +24,7 @@
namespace API::Subsonic::Scan namespace API::Subsonic::Scan
{ {
Response handleGetScanStatus(RequestContext& context); Response handleGetScanStatus(RequestContext& context);
Response handleStartScan(RequestContext& context); Response handleStartScan(RequestContext& context);
} }
@@ -0,0 +1,202 @@
/*
* Copyright (C) 2020 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "MediaRetrieval.hpp"
#include "av/TranscodeParameters.hpp"
#include "av/TranscodeResourceHandlerCreator.hpp"
#include "av/Types.hpp"
#include "services/cover/ICoverService.hpp"
#include "services/database/Session.hpp"
#include "services/database/Track.hpp"
#include "services/database/User.hpp"
#include "utils/IResourceHandler.hpp"
#include "utils/Logger.hpp"
#include "utils/FileResourceHandlerCreator.hpp"
#include "utils/Utils.hpp"
#include "ParameterParsing.hpp"
#include "SubsonicId.hpp"
using namespace Database;
namespace API::Subsonic
{
namespace {
Av::Format userTranscodeFormatToAvFormat(AudioFormat format)
{
switch (format)
{
case AudioFormat::MP3: return Av::Format::MP3;
case AudioFormat::OGG_OPUS: return Av::Format::OGG_OPUS;
case AudioFormat::MATROSKA_OPUS: return Av::Format::MATROSKA_OPUS;
case AudioFormat::OGG_VORBIS: return Av::Format::OGG_VORBIS;
case AudioFormat::WEBM_VORBIS: return Av::Format::WEBM_VORBIS;
default: return Av::Format::OGG_OPUS;
}
}
struct StreamParameters
{
Av::InputFileParameters inputFileParameters;
std::optional<Av::TranscodeParameters> transcodeParameters;
bool estimateContentLength{};
};
StreamParameters getStreamParameters(RequestContext& context)
{
// Mandatory params
const TrackId id{ getMandatoryParameterAs<TrackId>(context.parameters, "id") };
// Optional params
std::optional<std::size_t> maxBitRate{ getParameterAs<std::size_t>(context.parameters, "maxBitRate") };
const std::optional<std::string> format{ getParameterAs<std::string>(context.parameters, "format") };
const std::optional<std::size_t> timeOffset{ getParameterAs<std::size_t>(context.parameters, "timeOffset") };
bool estimateContentLength{ getParameterAs<bool>(context.parameters, "estimateContentLength").value_or(false) };
StreamParameters parameters;
parameters.estimateContentLength = estimateContentLength;
auto transaction{ context.dbSession.createSharedTransaction() };
{
const auto track{ Track::find(context.dbSession, id) };
if (!track)
throw RequestedDataNotFoundError{};
parameters.inputFileParameters.trackPath = track->getPath();
parameters.inputFileParameters.duration = track->getDuration();
}
{
const User::pointer user{ User::find(context.dbSession, context.userId) };
if (!user)
throw UserNotAuthorizedError{};
// format = "raw" => no transcode. Other format values will be ignored
const bool transcode{ (!format || (*format != "raw")) && user->getSubsonicTranscodeEnable() };
if (transcode)
{
std::size_t bitRate{ user->getSubsonicTranscodeBitrate() / 1000 };
// "If set to zero, no limit is imposed"
if (maxBitRate && *maxBitRate != 0)
bitRate = Utils::clamp(*maxBitRate, std::size_t{ 48 }, bitRate);
Av::TranscodeParameters transcodeParameters;
transcodeParameters.bitrate = bitRate * 1000;
transcodeParameters.format = userTranscodeFormatToAvFormat(user->getSubsonicTranscodeFormat());
transcodeParameters.stripMetadata = false; // We want clients to use metadata (offline use, replay gain, etc.)
transcodeParameters.offset = std::chrono::seconds{ timeOffset ? *timeOffset : 0 };
parameters.transcodeParameters = std::move(transcodeParameters);
}
}
return parameters;
}
}
void handleDownload(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response)
{
std::shared_ptr<IResourceHandler> resourceHandler;
Wt::Http::ResponseContinuation* continuation{ request.continuation() };
if (!continuation)
{
// Mandatory params
Database::TrackId id{ getMandatoryParameterAs<Database::TrackId>(context.parameters, "id") };
std::filesystem::path trackPath;
{
auto transaction{ context.dbSession.createSharedTransaction() };
auto track{ Track::find(context.dbSession, id) };
if (!track)
throw RequestedDataNotFoundError{};
trackPath = track->getPath();
}
resourceHandler = createFileResourceHandler(trackPath);
}
else
{
resourceHandler = Wt::cpp17::any_cast<std::shared_ptr<IResourceHandler>>(continuation->data());
}
continuation = resourceHandler->processRequest(request, response);
if (continuation)
continuation->setData(resourceHandler);
}
void handleStream(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response)
{
std::shared_ptr<IResourceHandler> resourceHandler;
try
{
Wt::Http::ResponseContinuation* continuation = request.continuation();
if (!continuation)
{
StreamParameters streamParameters{ getStreamParameters(context) };
if (streamParameters.transcodeParameters)
resourceHandler = Av::createTranscodeResourceHandler(streamParameters.inputFileParameters, *streamParameters.transcodeParameters, streamParameters.estimateContentLength);
else
resourceHandler = createFileResourceHandler(streamParameters.inputFileParameters.trackPath);
}
else
{
resourceHandler = Wt::cpp17::any_cast<std::shared_ptr<IResourceHandler>>(continuation->data());
}
continuation = resourceHandler->processRequest(request, response);
if (continuation)
continuation->setData(resourceHandler);
}
catch (const Av::Exception& e)
{
LMS_LOG(API_SUBSONIC, ERROR) << "Caught Av exception: " << e.what();
}
}
void handleGetCoverArt(RequestContext& context, const Wt::Http::Request& /*request*/, Wt::Http::Response& response)
{
// Mandatory params
const auto trackId{ getParameterAs<TrackId>(context.parameters, "id") };
const auto releaseId{ getParameterAs<ReleaseId>(context.parameters, "id") };
if (!trackId && !releaseId)
throw BadParameterGenericError{ "id" };
std::size_t size{ getParameterAs<std::size_t>(context.parameters, "size").value_or(1024) };
size = ::Utils::clamp(size, std::size_t{ 32 }, std::size_t{ 2048 });
std::shared_ptr<Image::IEncodedImage> cover;
if (trackId)
cover = Service<Cover::ICoverService>::get()->getFromTrack(*trackId, size);
else if (releaseId)
cover = Service<Cover::ICoverService>::get()->getFromRelease(*releaseId, size);
response.out().write(reinterpret_cast<const char*>(cover->getData()), cover->getDataSize());
response.setMimeType(std::string{ cover->getMimeType() });
}
} // namespace API::Subsonic
@@ -24,9 +24,10 @@
#include "RequestContext.hpp" #include "RequestContext.hpp"
namespace API::Subsonic::Stream namespace API::Subsonic
{ {
void handleDownload(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response); void handleDownload(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response);
void handleStream(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response); void handleStream(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response);
void handleGetCoverArt(RequestContext& context, const Wt::Http::Request& request, Wt::Http::Response& response);
} }
@@ -0,0 +1,210 @@
/*
* Copyright (C) 2023 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Playlists.hpp"
#include "services/database/Session.hpp"
#include "services/database/Track.hpp"
#include "services/database/TrackList.hpp"
#include "services/database/User.hpp"
#include "responses/Playlist.hpp"
#include "responses/Song.hpp"
#include "ParameterParsing.hpp"
#include "SubsonicId.hpp"
namespace API::Subsonic
{
using namespace Database;
Response handleGetPlaylistsRequest(RequestContext& context)
{
auto transaction{ context.dbSession.createSharedTransaction() };
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
Response::Node& playlistsNode{ response.createNode("playlists") };
TrackList::FindParameters params;
params.setUser(context.userId);
params.setType(TrackListType::Playlist);
auto tracklistIds{ TrackList::find(context.dbSession, params) };
for (const TrackListId trackListId : tracklistIds.results)
{
const TrackList::pointer trackList{ TrackList::find(context.dbSession, trackListId) };
playlistsNode.addArrayChild("playlist", createPlaylistNode(trackList, context.dbSession));
}
return response;
}
Response handleGetPlaylistRequest(RequestContext& context)
{
// Mandatory params
TrackListId trackListId{ getMandatoryParameterAs<TrackListId>(context.parameters, "id") };
auto transaction{ context.dbSession.createSharedTransaction() };
User::pointer user{ User::find(context.dbSession, context.userId) };
if (!user)
throw UserNotAuthorizedError{};
TrackList::pointer tracklist{ TrackList::find(context.dbSession, trackListId) };
if (!tracklist)
throw RequestedDataNotFoundError{};
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
Response::Node playlistNode{ createPlaylistNode(tracklist, context.dbSession) };
auto entries{ tracklist->getEntries() };
for (const TrackListEntry::pointer& entry : entries)
playlistNode.addArrayChild("entry", createSongNode(entry->getTrack(), context.dbSession, user));
response.addNode("playlist", playlistNode);
return response;
}
Response handleCreatePlaylistRequest(RequestContext& context)
{
// Optional params
const auto id{ getParameterAs<TrackListId>(context.parameters, "playlistId") };
auto name{ getParameterAs<std::string>(context.parameters, "name") };
std::vector<TrackId> trackIds{ getMultiParametersAs<TrackId>(context.parameters, "songId") };
if (!name && !id)
throw RequiredParameterMissingError{ "name or id" };
auto transaction{ context.dbSession.createUniqueTransaction() };
User::pointer user{ User::find(context.dbSession, context.userId) };
if (!user)
throw UserNotAuthorizedError{};
TrackList::pointer tracklist;
if (id)
{
tracklist = TrackList::find(context.dbSession, *id);
if (!tracklist
|| tracklist->getUser() != user
|| tracklist->getType() != TrackListType::Playlist)
{
throw RequestedDataNotFoundError{};
}
if (name)
tracklist.modify()->setName(*name);
}
else
{
tracklist = context.dbSession.create<TrackList>(*name, TrackListType::Playlist, false, user);
}
for (const TrackId trackId : trackIds)
{
Track::pointer track{ Track::find(context.dbSession, trackId) };
if (!track)
continue;
context.dbSession.create<TrackListEntry>(track, tracklist);
}
return Response::createOkResponse(context.serverProtocolVersion);
}
Response handleUpdatePlaylistRequest(RequestContext& context)
{
// Mandatory params
TrackListId id{ getMandatoryParameterAs<TrackListId>(context.parameters, "playlistId") };
// Optional parameters
auto name{ getParameterAs<std::string>(context.parameters, "name") };
auto isPublic{ getParameterAs<bool>(context.parameters, "public") };
std::vector<TrackId> trackIdsToAdd{ getMultiParametersAs<TrackId>(context.parameters, "songIdToAdd") };
std::vector<std::size_t> trackPositionsToRemove{ getMultiParametersAs<std::size_t>(context.parameters, "songIndexToRemove") };
auto transaction{ context.dbSession.createUniqueTransaction() };
User::pointer user{ User::find(context.dbSession, context.userId) };
if (!user)
throw UserNotAuthorizedError{};
TrackList::pointer tracklist{ TrackList::find(context.dbSession, id) };
if (!tracklist
|| tracklist->getUser() != user
|| tracklist->getType() != TrackListType::Playlist)
{
throw RequestedDataNotFoundError{};
}
if (name)
tracklist.modify()->setName(*name);
if (isPublic)
tracklist.modify()->setIsPublic(*isPublic);
{
// Remove from end to make indexes stable
std::sort(std::begin(trackPositionsToRemove), std::end(trackPositionsToRemove), std::greater<std::size_t>());
for (std::size_t trackPositionToRemove : trackPositionsToRemove)
{
auto entry{ tracklist->getEntry(trackPositionToRemove) };
if (entry)
entry.remove();
}
}
// Add tracks
for (const TrackId trackIdToAdd : trackIdsToAdd)
{
Track::pointer track{ Track::find(context.dbSession, trackIdToAdd) };
if (!track)
continue;
context.dbSession.create<TrackListEntry>(track, tracklist);
}
return Response::createOkResponse(context.serverProtocolVersion);
}
Response handleDeletePlaylistRequest(RequestContext& context)
{
TrackListId id{ getMandatoryParameterAs<TrackListId>(context.parameters, "id") };
auto transaction{ context.dbSession.createUniqueTransaction() };
User::pointer user{ User::find(context.dbSession, context.userId) };
if (!user)
throw UserNotAuthorizedError{};
TrackList::pointer tracklist{ TrackList::find(context.dbSession, id) };
if (!tracklist
|| tracklist->getUser() != user
|| tracklist->getType() != TrackListType::Playlist)
{
throw RequestedDataNotFoundError{};
}
tracklist.remove();
return Response::createOkResponse(context.serverProtocolVersion);
}
}
@@ -0,0 +1,32 @@
/*
* Copyright (C) 2023 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "RequestContext.hpp"
#include "SubsonicResponse.hpp"
namespace API::Subsonic
{
Response handleGetPlaylistsRequest(RequestContext& context);
Response handleGetPlaylistRequest(RequestContext& context);
Response handleCreatePlaylistRequest(RequestContext& context);
Response handleUpdatePlaylistRequest(RequestContext& context);
Response handleDeletePlaylistRequest(RequestContext& context);
}
@@ -0,0 +1,127 @@
/*
* Copyright (C) 2020 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Searching.hpp"
#include "services/database/Artist.hpp"
#include "services/database/Release.hpp"
#include "services/database/Session.hpp"
#include "services/database/Track.hpp"
#include "services/database/User.hpp"
#include "responses/Album.hpp"
#include "responses/Artist.hpp"
#include "responses/Song.hpp"
#include "ParameterParsing.hpp"
#include "ParameterParsing.hpp"
namespace API::Subsonic
{
using namespace Database;
namespace
{
Response handleSearchRequestCommon(RequestContext& context, bool id3)
{
// Mandatory params
std::string queryString{ getMandatoryParameterAs<std::string>(context.parameters, "query") };
std::string_view query{ queryString };
// Symfonium adds extra ""
if (context.clientInfo.name == "Symfonium")
query = StringUtils::stringTrim(query, "\"");
std::vector<std::string_view> keywords{ StringUtils::splitString(query, " ") };
// Optional params
std::size_t artistCount{ getParameterAs<std::size_t>(context.parameters, "artistCount").value_or(20) };
std::size_t artistOffset{ getParameterAs<std::size_t>(context.parameters, "artistOffset").value_or(0) };
std::size_t albumCount{ getParameterAs<std::size_t>(context.parameters, "albumCount").value_or(20) };
std::size_t albumOffset{ getParameterAs<std::size_t>(context.parameters, "albumOffset").value_or(0) };
std::size_t songCount{ getParameterAs<std::size_t>(context.parameters, "songCount").value_or(20) };
std::size_t songOffset{ getParameterAs<std::size_t>(context.parameters, "songOffset").value_or(0) };
auto transaction{ context.dbSession.createSharedTransaction() };
User::pointer user{ User::find(context.dbSession, context.userId) };
if (!user)
throw UserNotAuthorizedError{};
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
Response::Node& searchResult2Node{ response.createNode(id3 ? "searchResult3" : "searchResult2") };
if (artistCount > 0)
{
Artist::FindParameters params;
params.setKeywords(keywords);
params.setSortMethod(ArtistSortMethod::BySortName);
params.setRange({ artistOffset, artistCount });
RangeResults<ArtistId> artistIds{ Artist::find(context.dbSession, params) };
for (const ArtistId artistId : artistIds.results)
{
const auto artist{ Artist::find(context.dbSession, artistId) };
searchResult2Node.addArrayChild("artist", createArtistNode(artist, context.dbSession, user, id3));
}
}
if (albumCount > 0)
{
Release::FindParameters params;
params.setKeywords(keywords);
params.setSortMethod(ReleaseSortMethod::Name);
params.setRange({ albumOffset, albumCount });
RangeResults<ReleaseId> releaseIds{ Release::find(context.dbSession, params) };
for (const ReleaseId releaseId : releaseIds.results)
{
const auto release{ Release::find(context.dbSession, releaseId) };
searchResult2Node.addArrayChild("album", createAlbumNode(release, context.dbSession, user, id3));
}
}
if (songCount > 0)
{
Track::FindParameters params;
params.setKeywords(keywords);
params.setRange({ songOffset, songCount });
RangeResults<TrackId> trackIds{ Track::find(context.dbSession, params) };
for (const TrackId trackId : trackIds.results)
{
const auto track{ Track::find(context.dbSession, trackId) };
searchResult2Node.addArrayChild("song", createSongNode(track, context.dbSession, user));
}
}
return response;
}
}
Response handleSearch2Request(RequestContext& context)
{
return handleSearchRequestCommon(context, false /* no id3 */);
}
Response handleSearch3Request(RequestContext& context)
{
return handleSearchRequestCommon(context, true /* id3 */);
}
}
@@ -0,0 +1,29 @@
/*
* Copyright (C) 2023 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "RequestContext.hpp"
#include "SubsonicResponse.hpp"
namespace API::Subsonic
{
Response handleSearch2Request(RequestContext& context);
Response handleSearch3Request(RequestContext& context);
}
@@ -0,0 +1,34 @@
#include "entrypoints/System.hpp"
namespace API::Subsonic
{
Response handlePingRequest(RequestContext& context)
{
return Response::createOkResponse(context.serverProtocolVersion);
}
Response handleGetLicenseRequest(RequestContext& context)
{
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
Response::Node& licenseNode{ response.createNode("license") };
licenseNode.setAttribute("licenseExpires", "2025-09-03T14:46:43");
licenseNode.setAttribute("email", "foo@bar.com");
licenseNode.setAttribute("valid", true);
return response;
}
Response handleGetOpenSubsonicExtensions(RequestContext& context)
{
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
{
Response::Node& transcodeOffsetNode{ response.createArrayNode("openSubsonicExtensions") };
transcodeOffsetNode.setAttribute("name", "transcodeOffset");
transcodeOffsetNode.addArrayValue("versions", 1);
}
return response;
};
}
@@ -0,0 +1,30 @@
/*
* Copyright (C) 2023 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "RequestContext.hpp"
#include "SubsonicResponse.hpp"
namespace API::Subsonic
{
Response handlePingRequest(RequestContext& context);
Response handleGetLicenseRequest(RequestContext& context);
Response handleGetOpenSubsonicExtensions(RequestContext& context);
}
@@ -0,0 +1,208 @@
#include "UserManagement.hpp"
#include "services/database/Session.hpp"
#include "services/database/User.hpp"
#include "services/auth/IPasswordService.hpp"
#include "utils/Service.hpp"
#include "responses/User.hpp"
#include "ParameterParsing.hpp"
#include "Utils.hpp"
namespace API::Subsonic
{
using namespace Database;
namespace {
void checkUserIsMySelfOrAdmin(RequestContext& context, const std::string& username)
{
User::pointer currentUser{ User::find(context.dbSession, context.userId) };
if (!currentUser)
throw RequestedDataNotFoundError{};
if (currentUser->getLoginName() != username && !currentUser->isAdmin())
throw UserNotAuthorizedError{};
}
}
Response handleGetUserRequest(RequestContext& context)
{
std::string username{ getMandatoryParameterAs<std::string>(context.parameters, "username") };
auto transaction{ context.dbSession.createSharedTransaction() };
checkUserIsMySelfOrAdmin(context, username);
const User::pointer user{ User::find(context.dbSession, username) };
if (!user)
throw RequestedDataNotFoundError{};
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
response.addNode("user", createUserNode(user));
return response;
}
Response handleGetUsersRequest(RequestContext& context)
{
auto transaction{ context.dbSession.createSharedTransaction() };
Response response{ Response::createOkResponse(context.serverProtocolVersion) };
Response::Node& usersNode{ response.createNode("users") };
const auto userIds{ User::find(context.dbSession, User::FindParameters {}) };
for (const UserId userId : userIds.results)
{
const User::pointer user{ User::find(context.dbSession, userId) };
usersNode.addArrayChild("user", createUserNode(user));
}
return response;
}
Response handleCreateUserRequest(RequestContext& context)
{
std::string username{ getMandatoryParameterAs<std::string>(context.parameters, "username") };
std::string password{ decodePasswordIfNeeded(getMandatoryParameterAs<std::string>(context.parameters, "password")) };
// Just ignore all the other fields as we don't handle them
Database::UserId userId;
{
auto transaction{ context.dbSession.createUniqueTransaction() };
User::pointer user{ User::find(context.dbSession, username) };
if (user)
throw UserAlreadyExistsGenericError{};
user = context.dbSession.create<User>(username);
userId = user->getId();
}
auto removeCreatedUser{ [&]()
{
auto transaction {context.dbSession.createUniqueTransaction()};
User::pointer user {User::find(context.dbSession, userId)};
if (user)
user.remove();
} };
try
{
Service<Auth::IPasswordService>::get()->setPassword(userId, password);
}
catch (const Auth::PasswordMustMatchLoginNameException&)
{
removeCreatedUser();
throw PasswordMustMatchLoginNameGenericError{};
}
catch (const Auth::PasswordTooWeakException&)
{
removeCreatedUser();
throw PasswordTooWeakGenericError{};
}
catch (const Auth::Exception& exception)
{
removeCreatedUser();
throw UserNotAuthorizedError{};
}
return Response::createOkResponse(context.serverProtocolVersion);
}
Response handleDeleteUserRequest(RequestContext& context)
{
std::string username{ getMandatoryParameterAs<std::string>(context.parameters, "username") };
auto transaction{ context.dbSession.createUniqueTransaction() };
User::pointer user{ User::find(context.dbSession, username) };
if (!user)
throw RequestedDataNotFoundError{};
// cannot delete ourself
if (user->getId() == context.userId)
throw UserNotAuthorizedError{};
user.remove();
return Response::createOkResponse(context.serverProtocolVersion);
}
Response handleUpdateUserRequest(RequestContext& context)
{
std::string username{ getMandatoryParameterAs<std::string>(context.parameters, "username") };
std::optional<std::string> password{ getParameterAs<std::string>(context.parameters, "password") };
UserId userId;
{
auto transaction{ context.dbSession.createSharedTransaction() };
User::pointer user{ User::find(context.dbSession, username) };
if (!user)
throw RequestedDataNotFoundError{};
userId = user->getId();
}
if (password)
{
Utils::checkSetPasswordImplemented();
try
{
Service<::Auth::IPasswordService>()->setPassword(userId, decodePasswordIfNeeded(*password));
}
catch (const Auth::PasswordMustMatchLoginNameException&)
{
throw PasswordMustMatchLoginNameGenericError{};
}
catch (const Auth::PasswordTooWeakException&)
{
throw PasswordTooWeakGenericError{};
}
catch (const Auth::Exception&)
{
throw UserNotAuthorizedError{};
}
}
return Response::createOkResponse(context.serverProtocolVersion);
}
Response handleChangePassword(RequestContext& context)
{
std::string username{ getMandatoryParameterAs<std::string>(context.parameters, "username") };
std::string password{ decodePasswordIfNeeded(getMandatoryParameterAs<std::string>(context.parameters, "password")) };
try
{
Database::UserId userId;
{
auto transaction{ context.dbSession.createSharedTransaction() };
checkUserIsMySelfOrAdmin(context, username);
User::pointer user{ User::find(context.dbSession, username) };
if (!user)
throw UserNotAuthorizedError{};
userId = user->getId();
}
Service<Auth::IPasswordService>::get()->setPassword(userId, password);
}
catch (const Auth::PasswordMustMatchLoginNameException&)
{
throw PasswordMustMatchLoginNameGenericError{};
}
catch (const Auth::PasswordTooWeakException&)
{
throw PasswordTooWeakGenericError{};
}
catch (const Auth::Exception& authException)
{
throw UserNotAuthorizedError{};
}
return Response::createOkResponse(context.serverProtocolVersion);
}
}
@@ -0,0 +1,33 @@
/*
* Copyright (C) 2023 Emeric Poupon
*
* This file is part of LMS.
*
* LMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* LMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "RequestContext.hpp"
#include "SubsonicResponse.hpp"
namespace API::Subsonic
{
Response handleGetUserRequest(RequestContext& context);
Response handleGetUsersRequest(RequestContext& context);
Response handleCreateUserRequest(RequestContext& context);
Response handleUpdateUserRequest(RequestContext& context);
Response handleDeleteUserRequest(RequestContext& context);
Response handleChangePassword(RequestContext& context);
}
+211
View File
@@ -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);
}
+121
View File
@@ -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);
}
+246
View File
@@ -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;
}
}
+35
View File
@@ -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);
}
+52
View File
@@ -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;
}
}
+33
View File
@@ -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
View File
@@ -27,279 +27,275 @@
#include <boost/algorithm/string/join.hpp> #include <boost/algorithm/string/join.hpp>
#include <boost/algorithm/string.hpp> #include <boost/algorithm/string.hpp>
namespace StringUtils { #include <Wt/WDateTime.h>
#include <Wt/WDate.h>
bool namespace StringUtils
readList(const std::string& str, const std::string& separators, std::list<std::string>& results)
{ {
std::string curStr;
for (char c : str) bool readList(const std::string& str, const std::string& separators, std::list<std::string>& results)
{ {
if (separators.find(c) != std::string::npos) { std::string curStr;
if (!curStr.empty()) {
results.push_back(curStr);
curStr.clear();
}
}
else {
if (curStr.empty() && std::isspace(c))
continue;
curStr.push_back(c); for (char c : str)
} {
} if (separators.find(c) != std::string::npos) {
if (!curStr.empty()) {
results.push_back(curStr);
curStr.clear();
}
}
else {
if (curStr.empty() && std::isspace(c))
continue;
if (!curStr.empty()) curStr.push_back(c);
results.push_back(curStr); }
}
return !str.empty(); if (!curStr.empty())
} results.push_back(curStr);
template<> return !str.empty();
std::optional<std::string> }
readAs(std::string_view str)
{
return std::string {str};
}
template<> template<>
std::optional<std::string_view> std::optional<std::string> readAs(std::string_view str)
readAs(std::string_view str) {
{ return std::string{ str };
return str; }
}
template<> template<>
std::optional<bool> std::optional<std::string_view> readAs(std::string_view str)
readAs(std::string_view str) {
{ return str;
if (str == "1" || str == "true") }
return true;
else if (str == "0" || str == "false")
return false;
return std::nullopt; template<>
} std::optional<bool> readAs(std::string_view str)
{
if (str == "1" || str == "true")
return true;
else if (str == "0" || str == "false")
return false;
std::vector<std::string> return std::nullopt;
splitStringCopy(std::string_view string, std::string_view separators) }
{
std::string str {stringTrim(string, separators)};
std::vector<std::string> res; std::vector<std::string> splitStringCopy(std::string_view string, std::string_view separators)
boost::algorithm::split(res, str, boost::is_any_of(separators), boost::token_compress_on); {
std::string str{ stringTrim(string, separators) };
return res; std::vector<std::string> res;
} boost::algorithm::split(res, str, boost::is_any_of(separators), boost::token_compress_on);
std::vector<std::string_view> return res;
splitString(std::string_view str, std::string_view separators) }
{
std::vector<std::string_view> res;
std::string_view::size_type strBegin {}; std::vector<std::string_view> splitString(std::string_view str, std::string_view separators)
{
std::vector<std::string_view> res;
while ((strBegin = str.find_first_not_of(separators, strBegin)) != std::string_view::npos) std::string_view::size_type strBegin{};
{
auto strEnd {str.find_first_of(separators, strBegin + 1)};
if (strEnd == std::string_view::npos)
{
res.push_back(str.substr(strBegin, str.size() - strBegin));
break;
}
res.push_back(str.substr(strBegin, strEnd - strBegin)); while ((strBegin = str.find_first_not_of(separators, strBegin)) != std::string_view::npos)
strBegin = strEnd + 1; {
} auto strEnd{ str.find_first_of(separators, strBegin + 1) };
if (strEnd == std::string_view::npos)
{
res.push_back(str.substr(strBegin, str.size() - strBegin));
break;
}
return res; res.push_back(str.substr(strBegin, strEnd - strBegin));
} strBegin = strEnd + 1;
}
std::string return res;
joinStrings(const std::vector<std::string>& strings, const std::string& delimiter) }
{
return boost::algorithm::join(strings, delimiter);
}
std::string_view std::string joinStrings(const std::vector<std::string>& strings, const std::string& delimiter)
stringTrim(std::string_view str, std::string_view whitespaces) {
{ return boost::algorithm::join(strings, delimiter);
std::string_view res; }
const auto strBegin = str.find_first_not_of(whitespaces); std::string_view stringTrim(std::string_view str, std::string_view whitespaces)
if (strBegin != std::string_view::npos) {
{ std::string_view res;
const auto strEnd {str.find_last_not_of(whitespaces)};
const auto strRange {strEnd - strBegin + 1};
res = str.substr(strBegin, strRange); const auto strBegin = str.find_first_not_of(whitespaces);
} if (strBegin != std::string_view::npos)
{
const auto strEnd{ str.find_last_not_of(whitespaces) };
const auto strRange{ strEnd - strBegin + 1 };
return res; res = str.substr(strBegin, strRange);
} }
std::string_view return res;
stringTrimEnd(std::string_view str, std::string_view whitespaces) }
{
return str.substr(0, str.find_last_not_of(whitespaces) + 1);
}
std::string std::string_view stringTrimEnd(std::string_view str, std::string_view whitespaces)
stringToLower(std::string_view str) {
{ return str.substr(0, str.find_last_not_of(whitespaces) + 1);
std::string res; }
res.reserve(str.size());
std::transform(std::cbegin(str), std::cend(str), std::back_inserter(res), [](unsigned char c) { return std::tolower(c);}); std::string stringToLower(std::string_view str)
{
std::string res;
res.reserve(str.size());
return res; std::transform(std::cbegin(str), std::cend(str), std::back_inserter(res), [](unsigned char c) { return std::tolower(c);});
}
void return res;
stringToLower(std::string& str) }
{
std::transform(std::cbegin(str), std::cend(str), std::begin(str), [](unsigned char c) { return std::tolower(c);});
}
std::string void stringToLower(std::string& str)
stringToUpper(const std::string& str) {
{ std::transform(std::cbegin(str), std::cend(str), std::begin(str), [](unsigned char c) { return std::tolower(c);});
std::string res; }
res.reserve(str.size());
std::transform(std::cbegin(str), std::cend(str), std::back_inserter(res), [](char c) { return std::toupper(c);}); std::string stringToUpper(const std::string& str)
{
std::string res;
res.reserve(str.size());
return res; std::transform(std::cbegin(str), std::cend(str), std::back_inserter(res), [](char c) { return std::toupper(c);});
}
std::string return res;
bufferToString(const std::vector<unsigned char>& data) }
{
std::ostringstream oss;
for (unsigned char c : data) std::string bufferToString(const std::vector<unsigned char>& data)
{ {
oss << std::setw(2) << std::setfill('0') << std::hex << (int)c; std::ostringstream oss;
}
return oss.str(); for (unsigned char c : data)
} {
oss << std::setw(2) << std::setfill('0') << std::hex << (int)c;
}
void return oss.str();
capitalize(std::string& str) }
{
for (auto it {std::begin(str)}; it != std::end(str); ++it)
{
if (std::isspace(*it))
continue;
if (std::isalpha(*it)) void capitalize(std::string& str)
*it = std::toupper(*it); {
for (auto it{ std::begin(str) }; it != std::end(str); ++it)
{
if (std::isspace(*it))
continue;
break; if (std::isalpha(*it))
} *it = std::toupper(*it);
}
std::string break;
replaceInString(std::string_view str, const std::string& from, const std::string& to) }
{ }
std::string res {str};
size_t pos = 0;
while ((pos = res.find(from, pos)) != std::string::npos) std::string replaceInString(std::string_view str, const std::string& from, const std::string& to)
{ {
res.replace(pos, from.length(), to); std::string res{ str };
pos += to.length(); size_t pos = 0;
}
return res; while ((pos = res.find(from, pos)) != std::string::npos)
} {
res.replace(pos, from.length(), to);
pos += to.length();
}
std::string return res;
jsEscape(const std::string& str) }
{
static const std::unordered_map<char, std::string_view> escapeMap
{
{ '\\', "\\\\" },
{ '\n', "\\n" },
{ '\r', "\\r" },
{ '\t', "\\t" },
{ '"', "\\\"" },
{ '\'', "\\\'" },
};
std::string escaped; std::string jsEscape(const std::string& str)
escaped.reserve(str.length()); {
static const std::unordered_map<char, std::string_view> escapeMap
{
{ '\\', "\\\\" },
{ '\n', "\\n" },
{ '\r', "\\r" },
{ '\t', "\\t" },
{ '"', "\\\"" },
{ '\'', "\\\'" },
};
for (const char c : str) std::string escaped;
{ escaped.reserve(str.length());
auto it {escapeMap.find(c)};
if (it == std::cend(escapeMap))
{
escaped += c;
continue;
}
escaped += it->second; for (const char c : str)
} {
auto it{ escapeMap.find(c) };
if (it == std::cend(escapeMap))
{
escaped += c;
continue;
}
return escaped; escaped += it->second;
} }
std::string return escaped;
escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar) }
{
std::string res;
res.reserve(str.size());
for (const char c : str) std::string escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar)
{ {
if (std::any_of(std::cbegin(charsToEscape), std::cend(charsToEscape), [c](char charToEscape) { return c == charToEscape; })) std::string res;
res += escapeChar; res.reserve(str.size());
res += c; for (const char c : str)
} {
if (std::any_of(std::cbegin(charsToEscape), std::cend(charsToEscape), [c](char charToEscape) { return c == charToEscape; }))
res += escapeChar;
return res; res += c;
} }
bool return res;
stringEndsWith(const std::string& str, const std::string& ending) }
{
return boost::algorithm::ends_with(str, ending);
}
std::optional<std::string> bool stringEndsWith(const std::string& str, const std::string& ending)
stringFromHex(const std::string& str) {
{ return boost::algorithm::ends_with(str, ending);
static const char lut[] {"0123456789ABCDEF"}; }
if (str.length() % 2 != 0) std::optional<std::string> stringFromHex(const std::string& str)
return std::nullopt; {
static const char lut[]{ "0123456789ABCDEF" };
std::string res; if (str.length() % 2 != 0)
res.reserve(str.length() / 2); return std::nullopt;
auto it {std::cbegin(str)}; std::string res;
while (it != std::cend(str)) res.reserve(str.length() / 2);
{
unsigned val {};
auto itHigh {std::lower_bound(std::cbegin(lut), std::cend(lut), std::toupper(*(it++)))}; auto it{ std::cbegin(str) };
auto itLow {std::lower_bound(std::cbegin(lut), std::cend(lut), std::toupper(*(it++)))}; while (it != std::cend(str))
{
unsigned val{};
if (itHigh == std::cend(lut) || itLow == std::cend(lut)) auto itHigh{ std::lower_bound(std::cbegin(lut), std::cend(lut), std::toupper(*(it++))) };
return {}; auto itLow{ std::lower_bound(std::cbegin(lut), std::cend(lut), std::toupper(*(it++))) };
val = std::distance(std::cbegin(lut), itHigh) << 4; if (itHigh == std::cend(lut) || itLow == std::cend(lut))
val += std::distance(std::cbegin(lut), itLow ); return {};
res.push_back(static_cast<char>(val)); val = std::distance(std::cbegin(lut), itHigh) << 4;
} val += std::distance(std::cbegin(lut), itLow);
return res; res.push_back(static_cast<char>(val));
} }
return res;
}
std::string toISO8601String(const Wt::WDateTime& dateTime)
{
// assume UTC
return dateTime.toString("yyyy-MM-ddThh:mm:ss.zzz", false).toUTF8();
}
std::string toISO8601String(const Wt::WDate& date)
{
// assume UTC
return date.toString("yyyy-MM-dd").toUTF8();
}
} // StringUtils } // StringUtils
+52 -79
View File
@@ -29,94 +29,67 @@
#define QUOTEME(x) QUOTEME_1(x) #define QUOTEME(x) QUOTEME_1(x)
#define QUOTEME_1(x) #x #define QUOTEME_1(x) #x
namespace StringUtils { namespace Wt
[[nodiscard]]
std::vector<std::string>
splitStringCopy(std::string_view string, std::string_view separators);
[[nodiscard]]
std::vector<std::string_view>
splitString(std::string_view string, std::string_view separators);
[[nodiscard]]
std::string
joinStrings(const std::vector<std::string>& strings, const std::string& delimiter);
[[nodiscard]]
std::string_view
stringTrim(std::string_view str, std::string_view whitespaces = " \t");
[[nodiscard]]
std::string_view
stringTrimEnd(std::string_view str, std::string_view whitespaces = " \t");
[[nodiscard]]
std::string
stringToLower(std::string_view str);
void
stringToLower(std::string& str);
[[nodiscard]]
std::string
stringToUpper(const std::string& str);
[[nodiscard]]
std::string
bufferToString(const std::vector<unsigned char>& data);
void
capitalize(std::string& str);
template<typename T>
[[nodiscard]]
std::optional<T> readAs(std::string_view str)
{ {
T res; class WDate;
class WDateTime;
std::istringstream iss {std::string {str}};
iss >> res;
if (iss.fail())
return std::nullopt;
return res;
} }
template<> namespace StringUtils {
[[nodiscard]]
std::optional<std::string>
readAs(std::string_view str);
template<> [[nodiscard]] std::vector<std::string> splitStringCopy(std::string_view string, std::string_view separators);
[[nodiscard]]
std::optional<std::string_view>
readAs(std::string_view str);
template<> [[nodiscard]] std::vector<std::string_view> splitString(std::string_view string, std::string_view separators);
[[nodiscard]]
std::optional<bool>
readAs(std::string_view str);
[[nodiscard]] [[nodiscard]] std::string joinStrings(const std::vector<std::string>& strings, const std::string& delimiter);
std::string
replaceInString(std::string_view str, const std::string& from, const std::string& to);
[[nodiscard]] [[nodiscard]] std::string_view stringTrim(std::string_view str, std::string_view whitespaces = " \t");
std::string
jsEscape(const std::string& str);
[[nodiscard]] [[nodiscard]] std::string_view stringTrimEnd(std::string_view str, std::string_view whitespaces = " \t");
std::string
escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar);
[[nodiscard]] [[nodiscard]] std::string stringToLower(std::string_view str);
bool
stringEndsWith(const std::string& str, const std::string& ending);
[[nodiscard]] void stringToLower(std::string& str);
std::optional<std::string>
stringFromHex(const std::string& str);
} // StringUtils [[nodiscard]] std::string stringToUpper(const std::string& str);
[[nodiscard]] std::string bufferToString(const std::vector<unsigned char>& data);
void capitalize(std::string& str);
template<typename T>
[[nodiscard]] std::optional<T> readAs(std::string_view str)
{
T res;
std::istringstream iss{ std::string {str} };
iss >> res;
if (iss.fail())
return std::nullopt;
return res;
}
template<>
[[nodiscard]] std::optional<std::string> readAs(std::string_view str);
template<>
[[nodiscard]] std::optional<std::string_view> readAs(std::string_view str);
template<>
[[nodiscard]] std::optional<bool> readAs(std::string_view str);
[[nodiscard]] std::string replaceInString(std::string_view str, const std::string& from, const std::string& to);
[[nodiscard]] std::string jsEscape(const std::string& str);
[[nodiscard]] std::string escapeString(std::string_view str, std::string_view charsToEscape, char escapeChar);
[[nodiscard]] bool stringEndsWith(const std::string& str, const std::string& ending);
[[nodiscard]] std::optional<std::string> stringFromHex(const std::string& str);
[[nodiscard]] std::string toISO8601String(const Wt::WDateTime& dateTime);
[[nodiscard]] std::string toISO8601String(const Wt::WDate& date);
} // StringUtils
+15
View File
@@ -19,6 +19,9 @@
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <Wt/WDateTime.h>
#include <Wt/WDate.h>
#include <Wt/WTime.h>
#include "utils/String.hpp" #include "utils/String.hpp"
TEST(StringUtils, splitString) TEST(StringUtils, splitString)
@@ -147,3 +150,15 @@ TEST(StringUtils, capitalize)
EXPECT_EQ(str, test.expectedOutput) << " str was '" << test.input << "'"; EXPECT_EQ(str, test.expectedOutput) << " str was '" << test.input << "'";
} }
} }
TEST(Stringutils, date)
{
const Wt::WDate date{ 2020, 01, 03 };
EXPECT_EQ(StringUtils::toISO8601String(date), "2020-01-03");
}
TEST(Stringutils, dateTime)
{
const Wt::WDateTime dateTime{ Wt::WDate {2020, 01, 03 }, Wt::WTime{9, 8, 11, 75} };
EXPECT_EQ(StringUtils::toISO8601String(dateTime), "2020-01-03T09:08:11.075");
}
+1
View File
@@ -78,6 +78,7 @@ namespace UserInterface
Artist::FindParameters params; Artist::FindParameters params;
params.setClusters(getFilters().getClusterIds()); params.setClusters(getFilters().getClusterIds());
params.setKeywords(getSearchKeywords()); params.setKeywords(getSearchKeywords());
params.setLinkType(_linkType);
params.setSortMethod(ArtistSortMethod::BySortName); params.setSortMethod(ArtistSortMethod::BySortName);
params.setRange(range); params.setRange(range);
+42
View File
@@ -21,6 +21,10 @@
#include <Wt/WAnchor.h> #include <Wt/WAnchor.h>
#include "services/database/Artist.hpp" #include "services/database/Artist.hpp"
#include "services/database/Session.hpp"
#include "services/database/TrackArtistLink.hpp"
#include "utils/EnumSet.hpp"
#include "LmsApplication.hpp"
#include "Utils.hpp" #include "Utils.hpp"
namespace UserInterface::ArtistListHelpers namespace UserInterface::ArtistListHelpers
@@ -33,5 +37,43 @@ namespace UserInterface::ArtistListHelpers
return res; return res;
} }
std::unique_ptr<ArtistLinkTypesModel>
createArtistLinkTypesModel()
{
using namespace Database;
std::unique_ptr<ArtistLinkTypesModel> linkTypesModel {std::make_unique<ArtistLinkTypesModel>()};
EnumSet<TrackArtistLinkType> usedLinkTypes;
{
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
usedLinkTypes = TrackArtistLink::findUsedTypes(LmsApp->getDbSession());
}
auto addTypeIfUsed {[&](TrackArtistLinkType linkType, std::string_view stringKey)
{
if (!usedLinkTypes.contains(linkType))
return;
linkTypesModel->add(Wt::WString::trn(std::string {stringKey}, 2), linkType);
}};
// add default one first (none)
linkTypesModel->add(Wt::WString::tr("Lms.Explore.Artists.linktype-all"), std::nullopt);
// TODO: sort by translated strings
addTypeIfUsed(TrackArtistLinkType::Artist, "Lms.Explore.Artists.linktype-artist");
addTypeIfUsed(TrackArtistLinkType::ReleaseArtist, "Lms.Explore.Artists.linktype-releaseartist");
addTypeIfUsed(TrackArtistLinkType::Composer, "Lms.Explore.Artists.linktype-composer");
addTypeIfUsed(TrackArtistLinkType::Conductor, "Lms.Explore.Artists.linktype-conductor");
addTypeIfUsed(TrackArtistLinkType::Lyricist, "Lms.Explore.Artists.linktype-lyricist");
addTypeIfUsed(TrackArtistLinkType::Mixer, "Lms.Explore.Artists.linktype-mixer");
addTypeIfUsed(TrackArtistLinkType::Performer, "Lms.Explore.Artists.linktype-performer");
addTypeIfUsed(TrackArtistLinkType::Producer, "Lms.Explore.Artists.linktype-producer");
addTypeIfUsed(TrackArtistLinkType::Remixer, "Lms.Explore.Artists.linktype-remixer");
return linkTypesModel;
}
} }
+10 -2
View File
@@ -23,15 +23,23 @@
#include <Wt/WTemplate.h> #include <Wt/WTemplate.h>
#include "common/ValueStringModel.hpp"
#include "services/database/Object.hpp" #include "services/database/Object.hpp"
#include "services/database/Types.hpp"
namespace Database namespace Database
{ {
class Artist; class Artist;
} }
namespace UserInterface::ArtistListHelpers namespace UserInterface
{ {
std::unique_ptr<Wt::WTemplate> createEntry(const Database::ObjectPtr<Database::Artist>& artist); using ArtistLinkTypesModel = ValueStringModel<std::optional<Database::TrackArtistLinkType>>;
namespace ArtistListHelpers
{
std::unique_ptr<Wt::WTemplate> createEntry(const Database::ObjectPtr<Database::Artist>& artist);
std::unique_ptr<ArtistLinkTypesModel> createArtistLinkTypesModel();
}
} }
+3 -44
View File
@@ -24,10 +24,8 @@
#include "services/database/Artist.hpp" #include "services/database/Artist.hpp"
#include "services/database/Session.hpp" #include "services/database/Session.hpp"
#include "services/database/TrackArtistLink.hpp" #include "services/database/TrackArtistLink.hpp"
#include "utils/EnumSet.hpp"
#include "utils/Logger.hpp" #include "utils/Logger.hpp"
#include "common/ValueStringModel.hpp"
#include "common/InfiniteScrollingContainer.hpp" #include "common/InfiniteScrollingContainer.hpp"
#include "ArtistListHelpers.hpp" #include "ArtistListHelpers.hpp"
#include "Filters.hpp" #include "Filters.hpp"
@@ -37,8 +35,6 @@ using namespace Database;
namespace UserInterface { namespace UserInterface {
using ArtistLinkModel = ValueStringModel<std::optional<TrackArtistLinkType>>;
Artists::Artists(Filters& filters) Artists::Artists(Filters& filters)
: Wt::WTemplate {Wt::WString::tr("Lms.Explore.Artists.template")} : Wt::WTemplate {Wt::WString::tr("Lms.Explore.Artists.template")}
, _artistCollector {filters, _defaultMode, _maxCount} , _artistCollector {filters, _defaultMode, _maxCount}
@@ -71,18 +67,17 @@ Artists::Artists(Filters& filters)
bindMenuItem("all", Wt::WString::tr("Lms.Explore.all"), ArtistCollector::Mode::All); bindMenuItem("all", Wt::WString::tr("Lms.Explore.all"), ArtistCollector::Mode::All);
_linkType = bindNew<Wt::WComboBox>("link-type"); _linkType = bindNew<Wt::WComboBox>("link-type");
_linkType->setModel(std::make_shared<ArtistLinkModel>()); _linkType->setModel(ArtistListHelpers::createArtistLinkTypesModel());
_linkType->changed().connect([this] _linkType->changed().connect([this]
{ {
const std::optional<TrackArtistLinkType> linkType {static_cast<ArtistLinkModel*>(_linkType->model().get())->getValue(_linkType->currentIndex())}; const std::optional<TrackArtistLinkType> linkType {static_cast<ArtistLinkTypesModel*>(_linkType->model().get())->getValue(_linkType->currentIndex())};
refreshView(linkType); refreshView(linkType);
}); });
refreshArtistLinkTypes();
LmsApp->getScannerEvents().scanComplete.connect(this, [this](const Scanner::ScanStats& stats) LmsApp->getScannerEvents().scanComplete.connect(this, [this](const Scanner::ScanStats& stats)
{ {
if (stats.nbChanges()) if (stats.nbChanges())
refreshArtistLinkTypes(); _linkType->setModel(ArtistListHelpers::createArtistLinkTypesModel());
}); });
_container = bindNew<InfiniteScrollingContainer>("artists", Wt::WString::tr("Lms.Explore.Artists.template.container")); _container = bindNew<InfiniteScrollingContainer>("artists", Wt::WString::tr("Lms.Explore.Artists.template.container"));
@@ -120,42 +115,6 @@ Artists::refreshView(std::optional<TrackArtistLinkType> linkType)
refreshView(); refreshView();
} }
void
Artists::refreshArtistLinkTypes()
{
std::shared_ptr<ArtistLinkModel> linkTypeModel {std::static_pointer_cast<ArtistLinkModel>(_linkType->model())};
EnumSet<TrackArtistLinkType> usedLinkTypes;
{
auto transaction {LmsApp->getDbSession().createSharedTransaction()};
usedLinkTypes = TrackArtistLink::findUsedTypes(LmsApp->getDbSession());
}
auto addTypeIfUsed {[&](TrackArtistLinkType linkType, std::string_view stringKey)
{
if (!usedLinkTypes.contains(linkType))
return;
linkTypeModel->add(Wt::WString::trn(std::string {stringKey}, 2), linkType);
}};
linkTypeModel->clear();
// add default one first (none)
linkTypeModel->add(Wt::WString::tr("Lms.Explore.Artists.linktype-all"), std::nullopt);
// TODO: sort by translated strings
addTypeIfUsed(TrackArtistLinkType::Artist, "Lms.Explore.Artists.linktype-artist");
addTypeIfUsed(TrackArtistLinkType::ReleaseArtist, "Lms.Explore.Artists.linktype-releaseartist");
addTypeIfUsed(TrackArtistLinkType::Composer, "Lms.Explore.Artists.linktype-composer");
addTypeIfUsed(TrackArtistLinkType::Conductor, "Lms.Explore.Artists.linktype-conductor");
addTypeIfUsed(TrackArtistLinkType::Lyricist, "Lms.Explore.Artists.linktype-lyricist");
addTypeIfUsed(TrackArtistLinkType::Mixer, "Lms.Explore.Artists.linktype-mixer");
addTypeIfUsed(TrackArtistLinkType::Performer, "Lms.Explore.Artists.linktype-performer");
addTypeIfUsed(TrackArtistLinkType::Producer, "Lms.Explore.Artists.linktype-producer");
addTypeIfUsed(TrackArtistLinkType::Remixer, "Lms.Explore.Artists.linktype-remixer");
}
void void
Artists::addSome() Artists::addSome()
{ {
-1
View File
@@ -42,7 +42,6 @@ namespace UserInterface
void refreshView(); void refreshView();
void refreshView(ArtistCollector::Mode mode); void refreshView(ArtistCollector::Mode mode);
void refreshView(std::optional<Database::TrackArtistLinkType> linkType); void refreshView(std::optional<Database::TrackArtistLinkType> linkType);
void refreshArtistLinkTypes();
void addSome(); void addSome();
static constexpr std::size_t _batchSize {30}; static constexpr std::size_t _batchSize {30};
+6 -6
View File
@@ -66,7 +66,7 @@ namespace UserInterface::ReleaseListHelpers
if (showYear) if (showYear)
{ {
Wt::WString year {ReleaseHelpers::buildReleaseYearString(release->getReleaseYear(), release->getReleaseYear(true))}; Wt::WString year {ReleaseHelpers::buildReleaseYearString(release->getReleaseDate(), release->getOriginalReleaseDate())};
if (!year.empty()) if (!year.empty())
{ {
entry->setCondition("if-has-year", true); entry->setCondition("if-has-year", true);
@@ -135,18 +135,18 @@ namespace UserInterface::ReleaseHelpers
return res; return res;
} }
Wt::WString buildReleaseYearString(std::optional<int> year, std::optional<int> originalYear) Wt::WString buildReleaseYearString(const Wt::WDate& releaseDate, const Wt::WDate& originalReleaseDate)
{ {
Wt::WString res; Wt::WString res;
// Year can be here, but originalYear can't be here without year (enforced by scanner) // Year can be here, but originalYear can't be here without year (enforced by scanner)
if (!year) if (!releaseDate.isValid())
return res; return res;
if (originalYear && *originalYear != *year) if (originalReleaseDate.isValid() && originalReleaseDate != releaseDate)
res = std::to_string(*originalYear) + " (" + std::to_string(*year) + ")"; res = std::to_string(originalReleaseDate.year()) + " (" + std::to_string(releaseDate.year()) + ")";
else else
res = std::to_string(*year); res = std::to_string(releaseDate.year());
return res; return res;
} }
+2 -1
View File
@@ -24,6 +24,7 @@
#include <Wt/WString.h> #include <Wt/WString.h>
#include <Wt/WTemplate.h> #include <Wt/WTemplate.h>
#include <Wt/WDate.h>
#include "services/database/Object.hpp" #include "services/database/Object.hpp"
#include "services/database/Types.hpp" #include "services/database/Types.hpp"
#include "utils/EnumSet.hpp" #include "utils/EnumSet.hpp"
@@ -43,5 +44,5 @@ namespace UserInterface::ReleaseListHelpers
namespace UserInterface::ReleaseHelpers namespace UserInterface::ReleaseHelpers
{ {
Wt::WString buildReleaseTypeString(Database::ReleaseTypePrimary primaryType, EnumSet<Database::ReleaseTypeSecondary> secondaryTypes); Wt::WString buildReleaseTypeString(Database::ReleaseTypePrimary primaryType, EnumSet<Database::ReleaseTypeSecondary> secondaryTypes);
Wt::WString buildReleaseYearString(std::optional<int> year, std::optional<int> originalYear); Wt::WString buildReleaseYearString(const Wt::WDate& releaseDate, const Wt::WDate& originalReleaseDate);
} }
+1 -1
View File
@@ -248,7 +248,7 @@ Release::refreshView()
bindString("name", Wt::WString::fromUTF8(release->getName()), Wt::TextFormat::Plain); bindString("name", Wt::WString::fromUTF8(release->getName()), Wt::TextFormat::Plain);
Wt::WString year {ReleaseHelpers::buildReleaseYearString(release->getReleaseYear(), release->getReleaseYear(true))}; Wt::WString year {ReleaseHelpers::buildReleaseYearString(release->getReleaseDate(), release->getOriginalReleaseDate())};
if (!year.empty()) if (!year.empty())
{ {
setCondition("if-has-year", true); setCondition("if-has-year", true);
+21
View File
@@ -48,6 +48,14 @@ namespace UserInterface
_artists = bindNew<InfiniteScrollingContainer>("artists", Wt::WString::tr("Lms.Explore.Artists.template.container")); _artists = bindNew<InfiniteScrollingContainer>("artists", Wt::WString::tr("Lms.Explore.Artists.template.container"));
_artists->onRequestElements.connect([this] { addSomeArtists(); }); _artists->onRequestElements.connect([this] { addSomeArtists(); });
_artistLinkType = bindNew<Wt::WComboBox>("link-type");
_artistLinkType->setModel(ArtistListHelpers::createArtistLinkTypesModel());
_artistLinkType->changed().connect([this]
{
const std::optional<TrackArtistLinkType> linkType {static_cast<ArtistLinkTypesModel*>(_artistLinkType->model().get())->getValue(_artistLinkType->currentIndex())};
refreshView(linkType);
});
_releases = bindNew<InfiniteScrollingContainer>("releases", Wt::WString::tr("Lms.Explore.Releases.template.container")); _releases = bindNew<InfiniteScrollingContainer>("releases", Wt::WString::tr("Lms.Explore.Releases.template.container"));
_releases->onRequestElements.connect([this] { addSomeReleases(); }); _releases->onRequestElements.connect([this] { addSomeReleases(); });
@@ -58,6 +66,12 @@ namespace UserInterface
{ {
refreshView(); refreshView();
}); });
LmsApp->getScannerEvents().scanComplete.connect(this, [this](const Scanner::ScanStats& stats)
{
if (stats.nbChanges())
_artistLinkType->setModel(ArtistListHelpers::createArtistLinkTypesModel());
});
} }
std::size_t std::size_t
@@ -76,6 +90,13 @@ namespace UserInterface
return it->second; return it->second;
} }
void
SearchView::refreshView(std::optional<TrackArtistLinkType> linkType)
{
_artistCollector.setArtistLinkType(linkType);
refreshView();
}
void void
SearchView::refreshView(const Wt::WString& searchText) SearchView::refreshView(const Wt::WString& searchText)
{ {
+9 -3
View File
@@ -19,11 +19,14 @@
#pragma once #pragma once
#include <optional>
#include <unordered_map> #include <unordered_map>
#include <Wt/WComboBox.h>
#include <Wt/WStackedWidget.h> #include <Wt/WStackedWidget.h>
#include <Wt/WTemplate.h> #include <Wt/WTemplate.h>
#include "services/database/Types.hpp"
#include "ArtistCollector.hpp" #include "ArtistCollector.hpp"
#include "ReleaseCollector.hpp" #include "ReleaseCollector.hpp"
#include "TrackCollector.hpp" #include "TrackCollector.hpp"
@@ -67,6 +70,7 @@ namespace UserInterface
std::size_t getMaxCount(Mode mode) const; std::size_t getMaxCount(Mode mode) const;
void refreshView(); void refreshView();
void refreshView(std::optional<Database::TrackArtistLinkType> linkType);
void addSomeArtists(); void addSomeArtists();
void addSomeReleases(); void addSomeReleases();
void addSomeTracks(); void addSomeTracks();
@@ -77,9 +81,11 @@ namespace UserInterface
ReleaseCollector _releaseCollector; ReleaseCollector _releaseCollector;
TrackCollector _trackCollector; TrackCollector _trackCollector;
InfiniteScrollingContainer* _artists; InfiniteScrollingContainer* _artists {};
InfiniteScrollingContainer* _releases; InfiniteScrollingContainer* _releases {};
InfiniteScrollingContainer* _tracks; InfiniteScrollingContainer* _tracks {};
Wt::WComboBox* _artistLinkType {};
std::vector<InfiniteScrollingContainer*> _results; std::vector<InfiniteScrollingContainer*> _results;
}; };
+2 -2
View File
@@ -109,8 +109,8 @@ getReleasePathName(Database::Release::pointer release)
{ {
std::string releaseName; std::string releaseName;
if (auto releaseYear {release->getReleaseYear()}) if (const Wt::WDate releaseDate {release->getReleaseDate()}; releaseDate.isValid())
releaseName += std::to_string(*releaseYear) + " - "; releaseName += std::to_string(releaseDate.year()) + " - ";
releaseName += StringUtils::replaceInString(release->getName(), "/", "_"); releaseName += StringUtils::replaceInString(release->getName(), "/", "_");
return releaseName; return releaseName;