Implemented a Scrobbling service + ListenBrainz scrobbler. fixes #118

This commit is contained in:
emeric
2021-04-06 19:59:12 +02:00
parent c29c4d576a
commit acf81e3f1a
49 changed files with 1662 additions and 177 deletions
+10 -1
View File
@@ -318,8 +318,16 @@ CREATE TABLE "user_backup" (
}
else if (version == 29)
{
// new field data_time in tracklist_entry (used by async scrobble or to make stats)
_session.execute("ALTER TABLE tracklist_entry ADD date_time TEXT");
_session.execute("ALTER TABLE user ADD listenbrainz_token TEXT");
_session.execute("ALTER TABLE user ADD scrobbler INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(User::defaultScrobbler)) + ")");
_session.execute("ALTER TABLE track ADD recording_mbid TEXT");
_session.execute("DELETE from tracklist WHERE name = ?").bind("__played_tracks__");
// MBID changes
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
ScanSettings::get(*this).modify()->incScanVersion();
}
else
{
@@ -432,6 +440,7 @@ Session::prepareTables()
_session.execute("CREATE INDEX IF NOT EXISTS track_name_idx ON track(name)");
_session.execute("CREATE INDEX IF NOT EXISTS track_name_nocase_idx ON track(name COLLATE NOCASE)");
_session.execute("CREATE INDEX IF NOT EXISTS track_mbid_idx ON track(mbid)");
_session.execute("CREATE INDEX IF NOT EXISTS track_recording_mbid_idx ON track(recording_mbid)");
_session.execute("CREATE INDEX IF NOT EXISTS track_release_idx ON track(release_id)");
_session.execute("CREATE INDEX IF NOT EXISTS track_year_idx ON track(year)");
_session.execute("CREATE INDEX IF NOT EXISTS track_original_year_idx ON track(original_year)");
+2 -2
View File
@@ -229,13 +229,13 @@ Track::getLastWritten(Session& session, std::optional<Wt::WDateTime> after, cons
}
std::vector<Track::pointer>
Track::getAllWithMBIDAndMissingFeatures(Session& session)
Track::getAllWithRecordingMBIDAndMissingFeatures(Session& session)
{
session.checkSharedLocked();
Wt::Dbo::collection<pointer> res = session.getDboSession().query<pointer>
("SELECT t FROM track t")
.where("LENGTH(t.mbid) > 0")
.where("LENGTH(t.recording_mbid) > 0")
.where("NOT EXISTS (SELECT * FROM track_features t_f WHERE t_f.track_id = t.id)");
return std::vector<pointer>(res.begin(), res.end());
}
+19 -31
View File
@@ -29,10 +29,11 @@
#include "database/User.hpp"
#include "database/Track.hpp"
#include "SqlQuery.hpp"
#include "StringViewTraits.hpp"
namespace Database {
TrackList::TrackList(const std::string& name, Type type, bool isPublic, Wt::Dbo::ptr<User> user)
TrackList::TrackList(std::string_view name, Type type, bool isPublic, Wt::Dbo::ptr<User> user)
: _name {name},
_type {type},
_isPublic {isPublic},
@@ -42,7 +43,7 @@ TrackList::TrackList(const std::string& name, Type type, bool isPublic, Wt::Dbo:
}
TrackList::pointer
TrackList::create(Session& session, const std::string& name, Type type, bool isPublic, Wt::Dbo::ptr<User> user)
TrackList::create(Session& session, std::string_view name, Type type, bool isPublic, Wt::Dbo::ptr<User> user)
{
session.checkUniqueLocked();
assert(user);
@@ -54,7 +55,7 @@ TrackList::create(Session& session, const std::string& name, Type type, bool isP
}
TrackList::pointer
TrackList::get(Session& session, const std::string& name, Type type, Wt::Dbo::ptr<User> user)
TrackList::get(Session& session, std::string_view name, Type type, Wt::Dbo::ptr<User> user)
{
session.checkSharedLocked();
assert(user);
@@ -147,22 +148,6 @@ TrackList::getEntries(std::optional<std::size_t> offset, std::optional<std::size
return std::vector<Wt::Dbo::ptr<TrackListEntry>>(entries.begin(), entries.end());
}
std::vector<Wt::Dbo::ptr<TrackListEntry>>
TrackList::getEntriesReverse(std::optional<std::size_t> offset, std::optional<std::size_t> size) const
{
assert(session());
assert(IdIsValid(self()->id()));
Wt::Dbo::collection<Wt::Dbo::ptr<TrackListEntry>> entries =
session()->find<TrackListEntry>()
.where("tracklist_id = ?").bind(self().id())
.orderBy("id DESC")
.limit(size ? static_cast<int>(*size) : -1)
.offset(offset ? static_cast<int>(*offset) : -1);
return std::vector<Wt::Dbo::ptr<TrackListEntry>>(entries.begin(), entries.end());
}
static
Wt::Dbo::Query<Artist::pointer>
createArtistsQuery(Wt::Dbo::Session& session, const std::string& queryStr, IdType tracklistId, const std::set<IdType>& clusterIds, std::optional<TrackArtistLinkType> linkType)
@@ -275,8 +260,9 @@ TrackList::getArtistsReverse(const std::set<IdType>& clusterIds, std::optional<T
assert(session());
assert(IdIsValid(self()->id()));
Wt::Dbo::collection<Artist::pointer> collection = createArtistsQuery(*session(), "SELECT DISTINCT a from artist a", self()->id(), clusterIds, linkType)
.orderBy("p_e.id DESC")
Wt::Dbo::collection<Artist::pointer> collection = createArtistsQuery(*session(), "SELECT a from artist a", self()->id(), clusterIds, linkType)
.groupBy("a.id").having("p_e.date_time = MAX(p_e.date_time)")
.orderBy("p_e.date_time DESC")
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
.offset(range ? static_cast<int>(range->offset) : -1);
@@ -298,8 +284,9 @@ TrackList::getReleasesReverse(const std::set<IdType>& clusterIds, std::optional<
assert(session());
assert(IdIsValid(self()->id()));
Wt::Dbo::collection<Release::pointer> collection = createReleasesQuery(*session(), "SELECT DISTINCT r from release r", self()->id(), clusterIds)
.orderBy("p_e.id DESC")
Wt::Dbo::collection<Release::pointer> collection = createReleasesQuery(*session(), "SELECT r from release r", self()->id(), clusterIds)
.groupBy("r.id").having("p_e.date_time = MAX(p_e.date_time)")
.orderBy("p_e.date_time DESC")
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
.offset(range ? static_cast<int>(range->offset) : -1);
@@ -322,8 +309,8 @@ TrackList::getTracksReverse(const std::set<IdType>& clusterIds, std::optional<Ra
assert(IdIsValid(self()->id()));
Wt::Dbo::collection<Track::pointer> collection = createTracksQuery(*session(), self()->id(), clusterIds)
.orderBy("p_e.id DESC")
.groupBy("t.id")
.groupBy("t.id").having("p_e.date_time = MAX(p_e.date_time)")
.orderBy("p_e.date_time DESC")
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
.offset(range ? static_cast<int>(range->offset) : -1);
@@ -498,21 +485,22 @@ TrackList::getTopTracks(const std::set<IdType>& clusterIds, std::optional<Range>
return res;
}
TrackListEntry::TrackListEntry(Wt::Dbo::ptr<Track> track, Wt::Dbo::ptr<TrackList> tracklist)
: _track(track),
_tracklist(tracklist)
TrackListEntry::TrackListEntry(Wt::Dbo::ptr<Track> track, Wt::Dbo::ptr<TrackList> tracklist, const Wt::WDateTime& dateTime)
: _dateTime {Wt::WDateTime::fromTime_t(dateTime.toTime_t())} // force second resolution
, _track {track}
, _tracklist {tracklist}
{
assert(_dateTime.isValid());
}
TrackListEntry::pointer
TrackListEntry::create(Session& session, Wt::Dbo::ptr<Track> track, Wt::Dbo::ptr<TrackList> tracklist)
TrackListEntry::create(Session& session, Wt::Dbo::ptr<Track> track, Wt::Dbo::ptr<TrackList> tracklist, const Wt::WDateTime& dateTime)
{
session.checkUniqueLocked();
assert(track);
assert(tracklist);
auto res = session.getDboSession().add( std::make_unique<TrackListEntry>( track, tracklist) );
auto res = session.getDboSession().add(std::make_unique<TrackListEntry>( track, tracklist, dateTime));
session.getDboSession().flush();
return res;
-11
View File
@@ -68,7 +68,6 @@ AuthToken::getByValue(Session& session, const std::string& value)
.where("value = ?").bind(value);
}
static const std::string playedListName {"__played_tracks__"};
static const std::string queuedListName {"__queued_tracks__"};
User::User(std::string_view loginName)
@@ -109,7 +108,6 @@ User::create(Session& session, std::string_view loginName)
User::pointer user {session.getDboSession().add(std::make_unique<User>(loginName))};
TrackList::create(session, playedListName, TrackList::Type::Internal, false, user);
TrackList::create(session, queuedListName, TrackList::Type::Internal, false, user);
session.getDboSession().flush();
@@ -143,15 +141,6 @@ User::clearAuthTokens()
_authTokens.clear();
}
Wt::Dbo::ptr<TrackList>
User::getPlayedTrackList(Session& session) const
{
assert(self());
session.checkSharedLocked();
return TrackList::get(session, playedListName, TrackList::Type::Internal, self());
}
Wt::Dbo::ptr<TrackList>
User::getQueuedTrackList(Session& session) const
{
+9 -5
View File
@@ -81,7 +81,7 @@ class Track : public Wt::Dbo::Dbo<Track>
static std::vector<std::pair<IdType, std::filesystem::path>> getAllPaths(Session& session, std::optional<std::size_t> offset = std::nullopt, std::optional<std::size_t> size = std::nullopt);
static std::vector<pointer> getMBIDDuplicates(Session& session);
static std::vector<pointer> getLastWritten(Session& session, std::optional<Wt::WDateTime> after, const std::set<IdType>& clusters, std::optional<Range> range, bool& moreResults);
static std::vector<pointer> getAllWithMBIDAndMissingFeatures(Session& session);
static std::vector<pointer> getAllWithRecordingMBIDAndMissingFeatures(Session& session);
static std::vector<IdType> getAllIdsWithFeatures(Session& session, std::optional<std::size_t> limit = {});
static std::vector<IdType> getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit = {});
static std::vector<pointer> getStarred(Session& session,
@@ -106,7 +106,8 @@ class Track : public Wt::Dbo::Dbo<Track>
void setYear(int year) { _year = year; }
void setOriginalYear(int year) { _originalYear = year; }
void setHasCover(bool hasCover) { _hasCover = hasCover; }
void setMBID(const std::optional<UUID>& MBID) { _MBID = MBID ? MBID->getAsString() : ""; }
void setTrackMBID(const std::optional<UUID>& MBID) { _trackMBID = MBID ? MBID->getAsString() : ""; }
void setRecordingMBID(const std::optional<UUID>& MBID) { _recordingMBID = MBID ? MBID->getAsString() : ""; }
void setCopyright(const std::string& copyright) { _copyright = std::string(copyright, 0, _maxCopyrightLength); }
void setCopyrightURL(const std::string& copyrightURL) { _copyrightURL = std::string(copyrightURL, 0, _maxCopyrightURLLength); }
void setTrackReplayGain(float replayGain) { _trackReplayGain = replayGain; }
@@ -131,7 +132,8 @@ class Track : public Wt::Dbo::Dbo<Track>
Wt::WDateTime getLastWriteTime() const { return _fileLastWrite; }
Wt::WDateTime getAddedTime() const { return _fileAdded; }
bool hasCover() const { return _hasCover; }
std::optional<UUID> getMBID() const { return UUID::fromString(_MBID); }
std::optional<UUID> getTrackMBID() const { return UUID::fromString(_trackMBID); }
std::optional<UUID> getRecordingMBID() const { return UUID::fromString(_recordingMBID); }
std::optional<std::string> getCopyright() const;
std::optional<std::string> getCopyrightURL() const;
std::optional<float> getTrackReplayGain() const { return _trackReplayGain; }
@@ -166,7 +168,8 @@ class Track : public Wt::Dbo::Dbo<Track>
Wt::Dbo::field(a, _fileLastWrite, "file_last_write");
Wt::Dbo::field(a, _fileAdded, "file_added");
Wt::Dbo::field(a, _hasCover, "has_cover");
Wt::Dbo::field(a, _MBID, "mbid");
Wt::Dbo::field(a, _trackMBID, "mbid");
Wt::Dbo::field(a, _recordingMBID, "recording_mbid");
Wt::Dbo::field(a, _copyright, "copyright");
Wt::Dbo::field(a, _copyrightURL, "copyright_url");
Wt::Dbo::field(a, _trackReplayGain, "track_replay_gain");
@@ -201,7 +204,8 @@ class Track : public Wt::Dbo::Dbo<Track>
Wt::WDateTime _fileLastWrite;
Wt::WDateTime _fileAdded;
bool _hasCover {};
std::string _MBID; // Musicbrainz Identifier
std::string _trackMBID;
std::string _recordingMBID;
std::string _copyright;
std::string _copyrightURL;
std::optional<float> _trackReplayGain;
@@ -47,11 +47,11 @@ class TrackList : public Wt::Dbo::Dbo<TrackList>
enum class Type
{
Playlist, // user controlled playlists
Internal, // current playqueue, history
Internal, // internal usage (current playqueue, history, ...)
};
TrackList() = default;
TrackList(const std::string& name, Type type, bool isPublic, Wt::Dbo::ptr<User> user);
TrackList(std::string_view name, Type type, bool isPublic, Wt::Dbo::ptr<User> user);
// Stats utility
std::vector<Wt::Dbo::ptr<Artist>> getTopArtists(const std::set<IdType>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range, bool& moreResults) const;
@@ -59,14 +59,14 @@ class TrackList : public Wt::Dbo::Dbo<TrackList>
std::vector<Wt::Dbo::ptr<Track>> getTopTracks(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const;
// Search utility
static pointer get(Session& session, const std::string& name, Type type, Wt::Dbo::ptr<User> user);
static pointer get(Session& session, std::string_view name, Type type, Wt::Dbo::ptr<User> user);
static pointer getById(Session& session, IdType tracklistId);
static std::vector<pointer> getAll(Session& session);
static std::vector<pointer> getAll(Session& session, Wt::Dbo::ptr<User> user);
static std::vector<pointer> getAll(Session& session, Wt::Dbo::ptr<User> user, Type type);
// Create utility
static pointer create(Session& session, const std::string& name, Type type, bool isPublic, Wt::Dbo::ptr<User> user);
static pointer create(Session& session, std::string_view name, Type type, bool isPublic, Wt::Dbo::ptr<User> user);
// Accessors
std::string getName() const { return _name; }
@@ -84,7 +84,6 @@ class TrackList : public Wt::Dbo::Dbo<TrackList>
std::size_t getCount() const;
Wt::Dbo::ptr<TrackListEntry> getEntry(std::size_t pos) const;
std::vector<Wt::Dbo::ptr<TrackListEntry>> getEntries(std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {}) const;
std::vector<Wt::Dbo::ptr<TrackListEntry>> getEntriesReverse(std::optional<std::size_t> offset = {}, std::optional<std::size_t> size = {}) const;
std::vector<Wt::Dbo::ptr<Artist>> getArtistsReverse(const std::set<IdType>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range, bool& moreResults) const;
std::vector<Wt::Dbo::ptr<Release>> getReleasesReverse(const std::set<IdType>& clusterIds, std::optional<Range> range, bool& moreResults) const;
@@ -131,15 +130,17 @@ class TrackListEntry : public Wt::Dbo::Dbo<TrackListEntry>
using pointer = Wt::Dbo::ptr<TrackListEntry>;
TrackListEntry() = default;
TrackListEntry(Wt::Dbo::ptr<Track> track, Wt::Dbo::ptr<TrackList> tracklist);
TrackListEntry(Wt::Dbo::ptr<Track> track, Wt::Dbo::ptr<TrackList> tracklist, const Wt::WDateTime& dateTime);
// find utility
static pointer getById(Session& session, IdType id);
// Create utility
static pointer create(Session& session, Wt::Dbo::ptr<Track> track, Wt::Dbo::ptr<TrackList> tracklist);
static pointer create(Session& session, Wt::Dbo::ptr<Track> track, Wt::Dbo::ptr<TrackList> tracklist, const Wt::WDateTime& dateTime = Wt::WDateTime::currentDateTime());
// Accessors
Wt::Dbo::ptr<Track> getTrack() const { return _track; }
const Wt::WDateTime& getDateTime() const { return _dateTime; }
template<class Action>
void persist(Action& a)
@@ -19,6 +19,7 @@
#pragma once
#include <cstdint>
#include <Wt/Dbo/ptr.h>
namespace Database
@@ -51,5 +52,23 @@ namespace Database
Writer,
};
// User selectable audio file formats
// Do not change values
enum class AudioFormat
{
MP3 = 1,
OGG_OPUS = 2,
OGG_VORBIS = 3,
WEBM_VORBIS = 4,
MATROSKA_OPUS = 5,
};
using Bitrate = std::uint32_t;
// Do not change enum values!
enum class Scrobbler
{
Internal = 0,
ListenBrainz = 1,
};
}
+13 -15
View File
@@ -26,6 +26,7 @@
#include <Wt/Dbo/Dbo.h>
#include <Wt/WDateTime.h>
#include "utils/UUID.hpp"
#include "Types.hpp"
namespace Database {
@@ -37,19 +38,6 @@ class Session;
class TrackList;
class Track;
// User selectable audio file formats
// Do not change values
enum class AudioFormat
{
MP3 = 1,
OGG_OPUS = 2,
OGG_VORBIS = 3,
WEBM_VORBIS = 4,
MATROSKA_OPUS = 5,
};
using Bitrate = std::size_t;
class User;
class AuthToken
{
@@ -139,6 +127,7 @@ class User : public Wt::Dbo::Dbo<User>
static inline const Bitrate defaultSubsonicTranscodeBitrate {128000};
static inline const UITheme defaultUITheme {UITheme::Dark};
static inline const SubsonicArtistListMode defaultSubsonicArtistListMode {SubsonicArtistListMode::AllArtists};
static inline const Scrobbler defaultScrobbler {Scrobbler::Internal};
User() = default;
@@ -172,6 +161,8 @@ class User : public Wt::Dbo::Dbo<User>
void setUITheme(UITheme uiTheme) { _uiTheme = uiTheme; }
void clearAuthTokens();
void setSubsonicArtistListMode(SubsonicArtistListMode mode) { _subsonicArtistListMode = mode; }
void setScrobbler(Scrobbler scrobbler) { _scrobbler = scrobbler; }
void setListenBrainzToken(const std::optional<UUID>& MBID) { _listenbrainzToken = MBID ? MBID->getAsString() : ""; }
// read
bool isAdmin() const { return _type == Type::ADMIN; }
@@ -184,8 +175,9 @@ class User : public Wt::Dbo::Dbo<User>
bool isRadioSet() const { return _radio; }
UITheme getUITheme() const { return _uiTheme; }
SubsonicArtistListMode getSubsonicArtistListMode() const { return _subsonicArtistListMode; }
Scrobbler getScrobbler() const { return _scrobbler; }
std::optional<UUID> getListenBrainzToken() const { return UUID::fromString(_listenbrainzToken); }
Wt::Dbo::ptr<TrackList> getPlayedTrackList(Session& session) const;
Wt::Dbo::ptr<TrackList> getQueuedTrackList(Session& session) const;
void starArtist(Wt::Dbo::ptr<Artist> artist);
@@ -214,10 +206,14 @@ class User : public Wt::Dbo::Dbo<User>
Wt::Dbo::field(a, _subsonicTranscodeBitrate, "subsonic_transcode_bitrate");
Wt::Dbo::field(a, _subsonicArtistListMode, "subsonic_artist_list_mode");
Wt::Dbo::field(a, _uiTheme, "ui_theme");
// User's dynamic data
Wt::Dbo::field(a, _scrobbler, "scrobbler");
Wt::Dbo::field(a, _listenbrainzToken, "listenbrainz_token");
// UI settings
Wt::Dbo::field(a, _curPlayingTrackPos, "cur_playing_track_pos");
Wt::Dbo::field(a, _repeatAll, "repeat_all");
Wt::Dbo::field(a, _radio, "radio");
Wt::Dbo::hasMany(a, _tracklists, Wt::Dbo::ManyToOne, "user");
Wt::Dbo::hasMany(a, _starredArtists, Wt::Dbo::ManyToMany, "user_artist_starred", "", Wt::Dbo::OnDeleteCascade);
Wt::Dbo::hasMany(a, _starredReleases, Wt::Dbo::ManyToMany, "user_release_starred", "", Wt::Dbo::OnDeleteCascade);
@@ -232,6 +228,8 @@ class User : public Wt::Dbo::Dbo<User>
std::string _passwordHash;
Wt::WDateTime _lastLogin;
UITheme _uiTheme {defaultUITheme};
Scrobbler _scrobbler {defaultScrobbler};
std::string _listenbrainzToken; // Musicbrainz Identifier
// Admin defined settings
Type _type {Type::REGULAR};