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