Merge branch 'develop' for release v3.28.0
This commit is contained in:
@@ -35,7 +35,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="hidden-xs col-sm-2 col-md-2 Lms-player-duration text-muted">
|
||||
<a id="lms-transcoding-active" class="Lms-player-btn Lms-btn" data-toggle="tooltip" title="${tr:Lms.Player.transcoding-active}" style="visibility:hidden"><i class="fa fa-fw fa-info-circle" aria-hidden="true"></i></a>
|
||||
<span id="lms-transcoding-active" class="Lms-player-btn Lms-btn" data-toggle="tooltip" title="${tr:Lms.Player.transcoding-active}" style="visibility:hidden"><i class="fa fa-fw fa-info-circle" aria-hidden="true"></i></span>
|
||||
<span id="lms-mp-curtime">--</span> / <span id="lms-mp-duration">--</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
<message id="Lms.Admin.ScannerController.last-scan-status">Scanned {1} files in {2} on {3} ({4} errors, {5} duplicates)</message>
|
||||
<message id="Lms.Admin.ScannerController.no-audio-track">No audio track</message>
|
||||
<message id="Lms.Admin.ScannerController.same-hash">Duplicated file hash</message>
|
||||
<message id="Lms.Admin.ScannerController.same-mbid">Duplicated MBID</message>
|
||||
<message id="Lms.Admin.ScannerController.same-mbid">Duplicated recording MBID</message>
|
||||
<message id="Lms.Admin.ScannerController.scan-now">Scan now</message>
|
||||
<message id="Lms.Admin.ScannerController.scanner">Scanner</message>
|
||||
<message id="Lms.Admin.ScannerController.status">Status</message>
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
<message id="Lms.Admin.ScannerController.last-scan-status">{1} fichiers scannés en {2} le {3} ({4} erreurs, {5} duplicatas)</message>
|
||||
<message id="Lms.Admin.ScannerController.no-audio-track">Pas de piste audio</message>
|
||||
<message id="Lms.Admin.ScannerController.same-hash">Hash dupliqué</message>
|
||||
<message id="Lms.Admin.ScannerController.same-mbid">MBID dupliqué</message>
|
||||
<message id="Lms.Admin.ScannerController.same-mbid">Recording MBID dupliqué</message>
|
||||
<message id="Lms.Admin.ScannerController.scan-now">Lancer un scan</message>
|
||||
<message id="Lms.Admin.ScannerController.scanner">Scanner</message>
|
||||
<message id="Lms.Admin.ScannerController.status">Statut</message>
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
<message id="Lms.Admin.ScannerController.last-scan-status">Scansionati {1} files in {2} su {3} ({4} errori, {5} duplicati)</message>
|
||||
<message id="Lms.Admin.ScannerController.no-audio-track">Nessuna traccia audio</message>
|
||||
<message id="Lms.Admin.ScannerController.same-hash">Hash doppio</message>
|
||||
<message id="Lms.Admin.ScannerController.same-mbid">MBID doppio</message>
|
||||
<message id="Lms.Admin.ScannerController.same-mbid">Recording MBID doppio</message>
|
||||
<message id="Lms.Admin.ScannerController.scan-now">Scansiona ora</message>
|
||||
<message id="Lms.Admin.ScannerController.scanner">Scanner</message>
|
||||
<message id="Lms.Admin.ScannerController.status">Stato</message>
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
<message id="Lms.Admin.ScannerController.last-scan-status">已扫描 {1} 文件 在 {2} 和 {3} ({4} 错误, {5} 副本)</message>
|
||||
<message id="Lms.Admin.ScannerController.no-audio-track">没有音轨</message>
|
||||
<message id="Lms.Admin.ScannerController.same-hash">复制文件哈希值</message>
|
||||
<message id="Lms.Admin.ScannerController.same-mbid">复制 MBID</message>
|
||||
<message id="Lms.Admin.ScannerController.same-mbid">复制 Recording MBID</message>
|
||||
<message id="Lms.Admin.ScannerController.scan-now">立即扫描</message>
|
||||
<message id="Lms.Admin.ScannerController.scanner">扫描器</message>
|
||||
<message id="Lms.Admin.ScannerController.status">状态</message>
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
<h3><div class="Lms-releasename text-primary">${name}</div></h3>
|
||||
${<if-has-release-artists>}${artists}${</if-has-release-artists>}
|
||||
${<if-has-various-release-artists>}<div class="Lms-artistname text-muted">${tr:Lms.Explore.various-artists}</div>${</if-has-various-release-artists>}
|
||||
<div class="text-muted"><small>${<if-has-year>}${year}${</if-has-year>}${<if-has-orig-year>} (${orig-year})${</if-has-orig-year>}</small></div>
|
||||
<div class="text-muted"><small>${<if-has-year>}${year}${<if-has-orig-year>} (${orig-year})${</if-has-orig-year>} · ${</if-has-year>}${duration}</small></div>
|
||||
${clusters}
|
||||
${play-btn class="Lms-explore-btn Lms-btn"}${more-btn class="Lms-explore-btn Lms-btn"}
|
||||
</div>
|
||||
|
||||
@@ -70,3 +70,6 @@ cover-max-cache-size = 30;
|
||||
|
||||
# JPEG quality for covers (range is 1-100)
|
||||
cover-jpeg-quality = 75;
|
||||
|
||||
# Set to true if you want to hide duplicate tracks
|
||||
scanner-skip-duplicate-recording-mbid = false;
|
||||
|
||||
+1
-1
@@ -184,7 +184,7 @@ a.Lms-artistname:hover, a.Lms-artistname:focus {
|
||||
}
|
||||
|
||||
.Lms-explore-release-entry {
|
||||
min-height: 34px;
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
.Lms-explore-release-entry:hover .Lms-explore-release-entry-controls {
|
||||
|
||||
@@ -4,8 +4,5 @@ add_subdirectory(libs)
|
||||
add_subdirectory(lms)
|
||||
add_subdirectory(tools)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_subdirectory(test)
|
||||
endif()
|
||||
|
||||
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
|
||||
add_subdirectory(auth)
|
||||
add_subdirectory(av)
|
||||
add_subdirectory(cover)
|
||||
add_subdirectory(database)
|
||||
add_subdirectory(image)
|
||||
add_subdirectory(metadata)
|
||||
add_subdirectory(recommendation)
|
||||
add_subdirectory(scanner)
|
||||
add_subdirectory(scrobbling)
|
||||
add_subdirectory(services)
|
||||
add_subdirectory(som)
|
||||
add_subdirectory(subsonic)
|
||||
add_subdirectory(utils)
|
||||
|
||||
|
||||
|
||||
@@ -119,7 +119,6 @@ Transcoder::start()
|
||||
args.emplace_back("-b:a");
|
||||
args.emplace_back(std::to_string(_parameters.bitrate));
|
||||
|
||||
|
||||
// Codecs and formats
|
||||
switch (_parameters.format)
|
||||
{
|
||||
|
||||
@@ -24,16 +24,15 @@
|
||||
|
||||
#include "Types.hpp"
|
||||
|
||||
namespace Av {
|
||||
|
||||
struct TranscodeParameters
|
||||
namespace Av
|
||||
{
|
||||
Format format;
|
||||
std::size_t bitrate {128000};
|
||||
std::optional<std::size_t> stream; // Id of the stream to be transcoded (auto detect by default)
|
||||
std::chrono::milliseconds offset {0};
|
||||
bool stripMetadata {true};
|
||||
};
|
||||
|
||||
struct TranscodeParameters
|
||||
{
|
||||
Format format;
|
||||
std::size_t bitrate {128000};
|
||||
std::optional<std::size_t> stream; // Id of the stream to be transcoded (auto detect by default)
|
||||
std::chrono::milliseconds offset {0};
|
||||
bool stripMetadata {true};
|
||||
};
|
||||
} // namespace Av
|
||||
|
||||
|
||||
@@ -33,12 +33,11 @@ namespace Av {
|
||||
|
||||
enum class Format
|
||||
{
|
||||
// Values are important and must not be changed (stored in the UI's localstorage)
|
||||
MP3 = 0,
|
||||
OGG_OPUS = 1,
|
||||
MATROSKA_OPUS = 2,
|
||||
OGG_VORBIS = 3,
|
||||
WEBM_VORBIS = 4,
|
||||
MP3,
|
||||
OGG_OPUS,
|
||||
MATROSKA_OPUS,
|
||||
OGG_VORBIS,
|
||||
WEBM_VORBIS,
|
||||
};
|
||||
|
||||
std::string_view formatToMimetype(Format format);
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
|
||||
add_library(lmscover SHARED
|
||||
impl/CoverArtGrabber.cpp
|
||||
)
|
||||
|
||||
target_include_directories(lmscover INTERFACE
|
||||
include
|
||||
)
|
||||
|
||||
target_include_directories(lmscover PRIVATE
|
||||
include
|
||||
impl
|
||||
)
|
||||
|
||||
target_link_libraries(lmscover PRIVATE
|
||||
lmsav
|
||||
)
|
||||
|
||||
target_link_libraries(lmscover PUBLIC
|
||||
lmsdatabase
|
||||
lmsutils
|
||||
std::filesystem
|
||||
)
|
||||
|
||||
if (IMAGE_LIBRARY STREQUAL STB)
|
||||
target_sources(lmscover PRIVATE
|
||||
impl/stb/JPEGImage.cpp
|
||||
impl/stb/RawImage.cpp
|
||||
)
|
||||
target_compile_options(lmscover PRIVATE "-DLMS_SUPPORT_IMAGE_STB")
|
||||
target_include_directories(lmscover PRIVATE ${STB_INCLUDE_DIR})
|
||||
elseif (IMAGE_LIBRARY STREQUAL GraphicsMagick++)
|
||||
target_sources(lmscover PRIVATE
|
||||
impl/graphicsmagick/JPEGImage.cpp
|
||||
impl/graphicsmagick/RawImage.cpp
|
||||
)
|
||||
target_compile_options(lmscover PRIVATE "-DLMS_SUPPORT_IMAGE_GM")
|
||||
target_link_libraries(lmscover PRIVATE PkgConfig::GraphicsMagick++)
|
||||
else ()
|
||||
message(FATAL_ERROR "Invalid IMAGE_LIBRARY provided")
|
||||
endif()
|
||||
|
||||
install(TARGETS lmscover DESTINATION lib)
|
||||
|
||||
@@ -1,508 +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 "database/Session.hpp"
|
||||
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <string_view>
|
||||
|
||||
#include "utils/Exception.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Cluster.hpp"
|
||||
#include "database/Db.hpp"
|
||||
#include "database/Release.hpp"
|
||||
#include "database/ScanSettings.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "database/TrackBookmark.hpp"
|
||||
#include "database/TrackArtistLink.hpp"
|
||||
#include "database/TrackList.hpp"
|
||||
#include "database/TrackFeatures.hpp"
|
||||
#include "database/User.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
|
||||
using Version = std::size_t;
|
||||
static constexpr Version LMS_DATABASE_VERSION {31};
|
||||
|
||||
class VersionInfo
|
||||
{
|
||||
public:
|
||||
using pointer = Wt::Dbo::ptr<VersionInfo>;
|
||||
|
||||
static VersionInfo::pointer getOrCreate(Session& session)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
|
||||
pointer versionInfo {session.getDboSession().find<VersionInfo>()};
|
||||
if (!versionInfo)
|
||||
return session.getDboSession().add(std::make_unique<VersionInfo>());
|
||||
|
||||
return versionInfo;
|
||||
}
|
||||
|
||||
static VersionInfo::pointer get(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().find<VersionInfo>();
|
||||
}
|
||||
|
||||
Version getVersion() const { return _version; }
|
||||
void setVersion(Version version) { _version = static_cast<int>(version); }
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _version, "db_version");
|
||||
}
|
||||
|
||||
private:
|
||||
int _version {LMS_DATABASE_VERSION};
|
||||
};
|
||||
|
||||
void
|
||||
Session::doDatabaseMigrationIfNeeded()
|
||||
{
|
||||
static const std::string outdatedMsg {"Outdated database, please rebuild it (delete the .db file and restart)"};
|
||||
|
||||
Db::ScopedNoForeignKeys noPragmaKeys {_db};
|
||||
|
||||
while (1)
|
||||
{
|
||||
auto uniqueTransaction {createUniqueTransaction()};
|
||||
|
||||
Version version;
|
||||
try
|
||||
{
|
||||
version = VersionInfo::getOrCreate(*this)->getVersion();
|
||||
LMS_LOG(DB, INFO) << "Database version = " << version << ", LMS binary version = " << LMS_DATABASE_VERSION;
|
||||
if (version == LMS_DATABASE_VERSION)
|
||||
{
|
||||
LMS_LOG(DB, DEBUG) << "Lms database version " << LMS_DATABASE_VERSION << ": up to date!";
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
LMS_LOG(DB, ERROR) << "Cannot get database version info: " << e.what();
|
||||
throw LmsException {outdatedMsg};
|
||||
}
|
||||
|
||||
LMS_LOG(DB, INFO) << "Migrating database from version " << version << "...";
|
||||
|
||||
if (version == 5)
|
||||
{
|
||||
_session.execute("DELETE FROM auth_token"); // format has changed
|
||||
}
|
||||
else if (version == 6)
|
||||
{
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(*this).modify()->incScanVersion();
|
||||
}
|
||||
else if (version == 7)
|
||||
{
|
||||
_session.execute("DROP TABLE similarity_settings");
|
||||
_session.execute("DROP TABLE similarity_settings_feature");
|
||||
_session.execute("ALTER TABLE scan_settings ADD similarity_engine_type INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(ScanSettings::RecommendationEngineType::Clusters)) + ")");
|
||||
}
|
||||
else if (version == 8)
|
||||
{
|
||||
// Better cover handling, need to rescan the whole files
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(*this).modify()->incScanVersion();
|
||||
}
|
||||
else if (version == 9)
|
||||
{
|
||||
_session.execute(R"(
|
||||
CREATE TABLE IF NOT EXISTS "track_bookmark" (
|
||||
"id" integer primary key autoincrement,
|
||||
"version" integer not null,
|
||||
"offset" integer,
|
||||
"comment" text not null,
|
||||
"track_id" bigint,
|
||||
"user_id" bigint,
|
||||
constraint "fk_track_bookmark_track" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred,
|
||||
constraint "fk_track_bookmark_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred
|
||||
);)");
|
||||
}
|
||||
else if (version == 10)
|
||||
{
|
||||
ScanSettings::get(*this).modify()->addAudioFileExtension(".m4b");
|
||||
ScanSettings::get(*this).modify()->addAudioFileExtension(".alac");
|
||||
}
|
||||
else if (version == 11)
|
||||
{
|
||||
// Sanitize bad MBID, need to rescan the whole files
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(*this).modify()->incScanVersion();
|
||||
}
|
||||
else if (version == 12)
|
||||
{
|
||||
// Artist and release that have a badly parsed name but a MBID had no chance to updat the name
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(*this).modify()->incScanVersion();
|
||||
}
|
||||
else if (version == 13)
|
||||
{
|
||||
// Always store UUID in lower case + better WMA parsing
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(*this).modify()->incScanVersion();
|
||||
}
|
||||
else if (version == 14)
|
||||
{
|
||||
// SortName now set from metadata
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(*this).modify()->incScanVersion();
|
||||
}
|
||||
else if (version == 15)
|
||||
{
|
||||
_session.execute("ALTER TABLE user ADD ui_theme INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(User::defaultUITheme)) + ")");
|
||||
}
|
||||
else if (version == 16)
|
||||
{
|
||||
_session.execute("ALTER TABLE track ADD total_disc INTEGER NOT NULL DEFAULT(0)");
|
||||
_session.execute("ALTER TABLE track ADD total_track INTEGER NOT NULL DEFAULT(0)");
|
||||
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(*this).modify()->incScanVersion();
|
||||
}
|
||||
else if (version == 17)
|
||||
{
|
||||
// Drop colums total_disc/total_track from release
|
||||
_session.execute(R"(
|
||||
CREATE TABLE "release_backup" (
|
||||
"id" integer primary key autoincrement,
|
||||
"version" integer not null,
|
||||
"name" text not null,
|
||||
"mbid" text not null
|
||||
))");
|
||||
_session.execute("INSERT INTO release_backup SELECT id,version,name,mbid FROM release");
|
||||
_session.execute("DROP TABLE release");
|
||||
_session.execute("ALTER TABLE release_backup RENAME TO release");
|
||||
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(*this).modify()->incScanVersion();
|
||||
}
|
||||
else if (version == 18)
|
||||
{
|
||||
_session.execute(R"(
|
||||
CREATE TABLE IF NOT EXISTS "subsonic_settings" (
|
||||
"id" integer primary key autoincrement,
|
||||
"version" integer not null,
|
||||
"api_enabled" boolean not null,
|
||||
"artist_list_mode" integer not null
|
||||
))");
|
||||
}
|
||||
else if (version == 19)
|
||||
{
|
||||
_session.execute(R"(
|
||||
CREATE TABLE "user_backup" (
|
||||
"id" integer primary key autoincrement,
|
||||
"version" integer not null,
|
||||
"type" integer not null,
|
||||
"login_name" text not null,
|
||||
"password_salt" text not null,
|
||||
"password_hash" text not null,
|
||||
"last_login" text,
|
||||
"subsonic_transcode_enable" boolean not null,
|
||||
"subsonic_transcode_format" integer not null,
|
||||
"subsonic_transcode_bitrate" integer not null,
|
||||
"subsonic_artist_list_mode" integer not null,
|
||||
"ui_theme" integer not null,
|
||||
"cur_playing_track_pos" integer not null,
|
||||
"repeat_all" boolean not null,
|
||||
"radio" boolean not null
|
||||
))");
|
||||
_session.execute(std::string {"INSERT INTO user_backup SELECT id, version, type, login_name, password_salt, password_hash, last_login, "}
|
||||
+ (User::defaultSubsonicTranscodeEnable ? "1" : "0")
|
||||
+ ", " + std::to_string(static_cast<int>(User::defaultSubsonicTranscodeFormat))
|
||||
+ ", " + std::to_string(User::defaultSubsonicTranscodeBitrate)
|
||||
+ ", " + std::to_string(static_cast<int>(User::defaultSubsonicArtistListMode))
|
||||
+ ", ui_theme, cur_playing_track_pos, repeat_all, radio FROM user");
|
||||
_session.execute("DROP TABLE user");
|
||||
_session.execute("ALTER TABLE user_backup RENAME TO user");
|
||||
}
|
||||
else if (version == 20)
|
||||
{
|
||||
_session.execute("DROP TABLE subsonic_settings");
|
||||
}
|
||||
else if (version == 21)
|
||||
{
|
||||
_session.execute("ALTER TABLE track ADD track_replay_gain REAL");
|
||||
_session.execute("ALTER TABLE track ADD release_replay_gain REAL");
|
||||
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(*this).modify()->incScanVersion();
|
||||
}
|
||||
else if (version == 22)
|
||||
{
|
||||
_session.execute("ALTER TABLE track ADD disc_subtitle TEXT NOT NULL DEFAULT ''");
|
||||
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(*this).modify()->incScanVersion();
|
||||
}
|
||||
else if (version == 23)
|
||||
{
|
||||
// Better cover detection
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(*this).modify()->incScanVersion();
|
||||
}
|
||||
else if (version == 24)
|
||||
{
|
||||
// User's AuthMode
|
||||
_session.execute("ALTER TABLE user ADD auth_mode INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(/*User::defaultAuthMode*/0)) + ")");
|
||||
}
|
||||
else if (version == 25)
|
||||
{
|
||||
// Better cover detection
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(*this).modify()->incScanVersion();
|
||||
}
|
||||
else if (version == 26)
|
||||
{
|
||||
// Composer, mixer, etc. support
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(*this).modify()->incScanVersion();
|
||||
}
|
||||
else if (version == 27)
|
||||
{
|
||||
// Composer, mixer, etc. support, now fallback on MBID tagged entries as there is no mean to provide MBID by tags for these kinf od artists
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(*this).modify()->incScanVersion();
|
||||
}
|
||||
else if (version == 28)
|
||||
{
|
||||
// Drop Auth mode
|
||||
_session.execute(R"(
|
||||
CREATE TABLE "user_backup" (
|
||||
"id" integer primary key autoincrement,
|
||||
"version" integer not null,
|
||||
"type" integer not null,
|
||||
"login_name" text not null,
|
||||
"password_salt" text not null,
|
||||
"password_hash" text not null,
|
||||
"last_login" text,
|
||||
"subsonic_transcode_enable" boolean not null,
|
||||
"subsonic_transcode_format" integer not null,
|
||||
"subsonic_transcode_bitrate" integer not null,
|
||||
"subsonic_artist_list_mode" integer not null,
|
||||
"ui_theme" integer not null,
|
||||
"cur_playing_track_pos" integer not null,
|
||||
"repeat_all" boolean not null,
|
||||
"radio" boolean not null
|
||||
))");
|
||||
_session.execute("INSERT INTO user_backup SELECT id, version, type, login_name, password_salt, password_hash, last_login, subsonic_transcode_enable, subsonic_transcode_format, subsonic_transcode_bitrate, subsonic_artist_list_mode, ui_theme, cur_playing_track_pos, repeat_all, radio FROM user");
|
||||
_session.execute("DROP TABLE user");
|
||||
_session.execute("ALTER TABLE user_backup RENAME TO user");
|
||||
}
|
||||
else if (version == 29)
|
||||
{
|
||||
_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 if (version == 30)
|
||||
{
|
||||
// drop "year" and "original_year" (rescan needed to convert them into dates)
|
||||
_session.execute(R"(
|
||||
CREATE TABLE "track_backup" (
|
||||
"id" integer primary key autoincrement,
|
||||
"version" integer not null,
|
||||
"scan_version" integer not null,
|
||||
"track_number" integer not null,
|
||||
"disc_number" integer not null,
|
||||
"name" text not null,
|
||||
"duration" integer,
|
||||
"date" integer text,
|
||||
"original_date" integer text,
|
||||
"file_path" text not null,
|
||||
"file_last_write" text,
|
||||
"file_added" text,
|
||||
"has_cover" boolean not null,
|
||||
"mbid" text not null,
|
||||
"copyright" text not null,
|
||||
"copyright_url" text not null,
|
||||
"release_id" bigint, total_disc INTEGER NOT NULL DEFAULT(0), total_track INTEGER NOT NULL DEFAULT(0), track_replay_gain REAL, release_replay_gain REAL, disc_subtitle TEXT NOT NULL DEFAULT '', recording_mbid TEXT,
|
||||
constraint "fk_track_release" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred
|
||||
))");
|
||||
_session.execute("INSERT INTO track_backup SELECT id, version, scan_version, track_number, disc_number, name, duration, \"1900-01-01\", \"1900-01-01\", file_path, file_last_write, file_added, has_cover, mbid, copyright, copyright_url, release_id, total_disc, total_track, track_replay_gain, release_replay_gain, disc_subtitle, recording_mbid FROM track");
|
||||
_session.execute("DROP TABLE track");
|
||||
_session.execute("ALTER TABLE track_backup RENAME TO track");
|
||||
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(*this).modify()->incScanVersion();
|
||||
}
|
||||
else
|
||||
{
|
||||
LMS_LOG(DB, ERROR) << "Database version " << version << " cannot be handled using migration";
|
||||
throw LmsException { LMS_DATABASE_VERSION > version ? outdatedMsg : "Server binary outdated, please upgrade it to handle this database"};
|
||||
}
|
||||
|
||||
VersionInfo::get(*this).modify()->setVersion(++version);
|
||||
}
|
||||
}
|
||||
|
||||
Session::Session(Db& db)
|
||||
: _db {db}
|
||||
{
|
||||
_session.setConnectionPool(_db.getConnectionPool());
|
||||
|
||||
_session.mapClass<VersionInfo>("version_info");
|
||||
_session.mapClass<Artist>("artist");
|
||||
_session.mapClass<AuthToken>("auth_token");
|
||||
_session.mapClass<Cluster>("cluster");
|
||||
_session.mapClass<ClusterType>("cluster_type");
|
||||
_session.mapClass<Release>("release");
|
||||
_session.mapClass<ScanSettings>("scan_settings");
|
||||
_session.mapClass<Track>("track");
|
||||
_session.mapClass<TrackBookmark>("track_bookmark");
|
||||
_session.mapClass<TrackArtistLink>("track_artist_link");
|
||||
_session.mapClass<TrackFeatures>("track_features");
|
||||
_session.mapClass<TrackList>("tracklist");
|
||||
_session.mapClass<TrackListEntry>("tracklist_entry");
|
||||
_session.mapClass<User>("user");
|
||||
}
|
||||
|
||||
|
||||
enum class OwnedLock
|
||||
{
|
||||
None,
|
||||
Shared,
|
||||
Unique,
|
||||
};
|
||||
|
||||
UniqueTransaction::UniqueTransaction(RecursiveSharedMutex& mutex, Wt::Dbo::Session& session)
|
||||
: _lock {mutex},
|
||||
_transaction {session}
|
||||
{
|
||||
}
|
||||
|
||||
SharedTransaction::SharedTransaction(RecursiveSharedMutex& mutex, Wt::Dbo::Session& session)
|
||||
: _lock {mutex},
|
||||
_transaction {session}
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
Session::checkUniqueLocked()
|
||||
{
|
||||
// assert(lockDebug[&_db.getMutex()] == OwnedLock::Unique);
|
||||
}
|
||||
|
||||
void
|
||||
Session::checkSharedLocked()
|
||||
{
|
||||
// assert(lockDebug[&_db.getMutex()] != OwnedLock::None);
|
||||
}
|
||||
|
||||
UniqueTransaction
|
||||
Session::createUniqueTransaction()
|
||||
{
|
||||
return UniqueTransaction {_db.getMutex(), _session};
|
||||
}
|
||||
|
||||
SharedTransaction
|
||||
Session::createSharedTransaction()
|
||||
{
|
||||
return SharedTransaction {_db.getMutex(), _session};
|
||||
}
|
||||
|
||||
void
|
||||
Session::prepareTables()
|
||||
{
|
||||
// Creation case
|
||||
try {
|
||||
_session.createTables();
|
||||
|
||||
LMS_LOG(DB, INFO) << "Tables created";
|
||||
}
|
||||
catch (Wt::Dbo::Exception& e)
|
||||
{
|
||||
LMS_LOG(DB, ERROR) << "Cannot create tables: " << e.what();
|
||||
}
|
||||
|
||||
doDatabaseMigrationIfNeeded();
|
||||
|
||||
// Indexes
|
||||
{
|
||||
auto uniqueTransaction {createUniqueTransaction()};
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS artist_name_idx ON artist(name)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS artist_sort_name_nocase_idx ON artist(sort_name COLLATE NOCASE)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS artist_mbid_idx ON artist(mbid)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS auth_token_user_idx ON auth_token(user_id)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS auth_token_expiry_idx ON auth_token(expiry)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS auth_token_value_idx ON auth_token(value)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS cluster_name_idx ON cluster(name)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS cluster_cluster_type_idx ON cluster(cluster_type_id)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS cluster_type_name_idx ON cluster_type(name)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS release_name_idx ON release(name)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS release_name_nocase_idx ON release(name COLLATE NOCASE)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS release_mbid_idx ON release(mbid)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS track_file_last_write_idx ON track(file_last_write)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS track_path_idx ON track(file_path)");
|
||||
_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_date_idx ON track(date)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS track_original_date_idx ON track(original_date)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS tracklist_name_idx ON tracklist(name)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS tracklist_user_idx ON tracklist(user_id)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS track_features_track_idx ON track_features(track_id)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_artist_idx ON track_artist_link(artist_id)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_name_idx ON track_artist_link(name)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_track_idx ON track_artist_link(track_id)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_type_idx ON track_artist_link(type)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_idx ON track_bookmark(user_id)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_track_idx ON track_bookmark(user_id,track_id)");
|
||||
}
|
||||
|
||||
// Initial settings tables
|
||||
{
|
||||
auto uniqueTransaction {createUniqueTransaction()};
|
||||
|
||||
ScanSettings::init(*this);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
Session::optimize()
|
||||
{
|
||||
LMS_LOG(DB, DEBUG) << "Optimizing db...";
|
||||
{
|
||||
auto uniqueTransaction {createUniqueTransaction()};
|
||||
_session.execute("ANALYZE");
|
||||
}
|
||||
LMS_LOG(DB, DEBUG) << "Optimized db!";
|
||||
}
|
||||
|
||||
} // namespace Database
|
||||
@@ -1,225 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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 "database/User.hpp"
|
||||
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Release.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "database/TrackList.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "StringViewTraits.hpp"
|
||||
#include "Traits.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
|
||||
AuthToken::AuthToken(const std::string& value, const Wt::WDateTime& expiry, ObjectPtr<User> user)
|
||||
: _value {value}
|
||||
, _expiry {expiry}
|
||||
, _user {getDboPtr(user)}
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
AuthToken::pointer
|
||||
AuthToken::create(Session& session, const std::string& value, const Wt::WDateTime& expiry, ObjectPtr<User> user)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
|
||||
AuthToken::pointer res {session.getDboSession().add(std::make_unique<AuthToken>(value, expiry, user))};
|
||||
session.getDboSession().flush();
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
void
|
||||
AuthToken::removeExpiredTokens(Session& session, const Wt::WDateTime& now)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
|
||||
session.getDboSession().execute
|
||||
("DELETE FROM auth_token WHERE expiry < ?").bind(now);
|
||||
}
|
||||
|
||||
AuthToken::pointer
|
||||
AuthToken::getByValue(Session& session, const std::string& value)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().find<AuthToken>()
|
||||
.where("value = ?").bind(value)
|
||||
.resultValue();
|
||||
}
|
||||
|
||||
static const std::string queuedListName {"__queued_tracks__"};
|
||||
|
||||
User::User(std::string_view loginName)
|
||||
: _loginName {loginName}
|
||||
{
|
||||
}
|
||||
|
||||
std::vector<User::pointer>
|
||||
User::getAll(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto res {session.getDboSession().find<User>().resultList()};
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<UserId>
|
||||
User::getAllIds(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto res {session.getDboSession().query<UserId>("SELECT id FROM user").resultList()};
|
||||
return std::vector<UserId>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
User::pointer
|
||||
User::getDemo(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().find<User>().where("type = ?").bind(UserType::DEMO).resultValue();
|
||||
}
|
||||
|
||||
std::size_t
|
||||
User::getCount(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM user");
|
||||
}
|
||||
|
||||
User::pointer
|
||||
User::create(Session& session, std::string_view loginName)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
|
||||
User::pointer user {session.getDboSession().add(std::make_unique<User>(loginName))};
|
||||
|
||||
TrackList::create(session, queuedListName, TrackList::Type::Internal, false, user);
|
||||
|
||||
session.getDboSession().flush();
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
User::pointer
|
||||
User::getById(Session& session, UserId id)
|
||||
{
|
||||
return session.getDboSession().find<User>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
|
||||
User::pointer
|
||||
User::getByLoginName(Session& session, std::string_view name)
|
||||
{
|
||||
return session.getDboSession().find<User>()
|
||||
.where("login_name = ?").bind(name)
|
||||
.resultValue();
|
||||
}
|
||||
|
||||
void
|
||||
User::setSubsonicTranscodeBitrate(Bitrate bitrate)
|
||||
{
|
||||
assert(audioTranscodeAllowedBitrates.find(bitrate) != audioTranscodeAllowedBitrates.cend());
|
||||
_subsonicTranscodeBitrate = bitrate;
|
||||
}
|
||||
|
||||
void
|
||||
User::clearAuthTokens()
|
||||
{
|
||||
_authTokens.clear();
|
||||
}
|
||||
|
||||
TrackList::pointer
|
||||
User::getQueuedTrackList(Session& session) const
|
||||
{
|
||||
assert(self());
|
||||
session.checkSharedLocked();
|
||||
|
||||
return TrackList::get(session, queuedListName, TrackList::Type::Internal, self());
|
||||
}
|
||||
|
||||
void
|
||||
User::starArtist(ObjectPtr<Artist> artist)
|
||||
{
|
||||
if (_starredArtists.count(getDboPtr(artist)) == 0)
|
||||
_starredArtists.insert(getDboPtr(artist));
|
||||
}
|
||||
|
||||
void
|
||||
User::unstarArtist(ObjectPtr<Artist> artist)
|
||||
{
|
||||
if (_starredArtists.count(getDboPtr(artist)) != 0)
|
||||
_starredArtists.erase(getDboPtr(artist));
|
||||
}
|
||||
|
||||
bool
|
||||
User::hasStarredArtist(ObjectPtr<Artist> artist) const
|
||||
{
|
||||
return _starredArtists.count(getDboPtr(artist)) != 0;
|
||||
}
|
||||
|
||||
void
|
||||
User::starRelease(ObjectPtr<Release> release)
|
||||
{
|
||||
if (_starredReleases.count(getDboPtr(release)) == 0)
|
||||
_starredReleases.insert(getDboPtr(release));
|
||||
}
|
||||
|
||||
void
|
||||
User::unstarRelease(ObjectPtr<Release> release)
|
||||
{
|
||||
if (_starredReleases.count(getDboPtr(release)) != 0)
|
||||
_starredReleases.erase(getDboPtr(release));
|
||||
}
|
||||
|
||||
bool
|
||||
User::hasStarredRelease(ObjectPtr<Release> release) const
|
||||
{
|
||||
return _starredReleases.count(getDboPtr(release)) != 0;
|
||||
}
|
||||
|
||||
void
|
||||
User::starTrack(ObjectPtr<Track> track)
|
||||
{
|
||||
if (_starredTracks.count(getDboPtr(track)) == 0)
|
||||
_starredTracks.insert(getDboPtr(track));
|
||||
}
|
||||
|
||||
void
|
||||
User::unstarTrack(ObjectPtr<Track> track)
|
||||
{
|
||||
if (_starredTracks.count(getDboPtr(track)) != 0)
|
||||
_starredTracks.erase(getDboPtr(track));
|
||||
}
|
||||
|
||||
bool
|
||||
User::hasStarredTrack(ObjectPtr<Track> track) const
|
||||
{
|
||||
return _starredTracks.count(getDboPtr(track)) != 0;
|
||||
}
|
||||
|
||||
} // namespace Database
|
||||
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 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 <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include <Wt/WDateTime.h>
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
|
||||
#include "database/Types.hpp"
|
||||
#include "utils/EnumSet.hpp"
|
||||
#include "utils/UUID.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
|
||||
class Cluster;
|
||||
class ClusterType;
|
||||
class Release;
|
||||
class Session;
|
||||
class Track;
|
||||
class TrackArtistLink;
|
||||
class User;
|
||||
|
||||
class Artist : public Object<Artist, ArtistId>
|
||||
{
|
||||
public:
|
||||
enum class SortMethod
|
||||
{
|
||||
None,
|
||||
ByName,
|
||||
BySortName,
|
||||
};
|
||||
|
||||
Artist() = default;
|
||||
Artist(const std::string& name, const std::optional<UUID>& MBID = {});
|
||||
|
||||
// Accessors
|
||||
static pointer getByMBID(Session& session, const UUID& MBID);
|
||||
static pointer getById(Session& session, ArtistId id);
|
||||
static std::vector<pointer> getByName(Session& session, const std::string& name); // exact match on name field
|
||||
static std::vector<pointer> getByClusters(Session& session,
|
||||
const std::vector<ClusterId>& clusters, // at least one track that belongs to these clusters
|
||||
SortMethod sortMethod
|
||||
);
|
||||
static std::vector<pointer> getByFilter(Session& session,
|
||||
const std::vector<ClusterId>& clusters, // if non empty, at least one artist that belongs to these clusters
|
||||
const std::vector<std::string_view>& keywords, // if non empty, name must match all of these keywords (name + sort name fields)
|
||||
std::optional<TrackArtistLinkType> linkType, // if set, only artists that have produced at least one track with this link type
|
||||
SortMethod sortMethod,
|
||||
std::optional<Range> range,
|
||||
bool& moreExpected);
|
||||
|
||||
static std::vector<pointer> getAll(Session& session);
|
||||
static std::vector<pointer> getAll(Session& session, SortMethod sortMethod);
|
||||
static std::vector<pointer> getAll(Session& session, SortMethod sortMethod, std::optional<Range> range, bool& moreResults);
|
||||
static std::vector<ArtistId> getAllIds(Session& session);
|
||||
static std::vector<ArtistId> getAllIdsRandom(Session& session, const std::vector<ClusterId>& clusters, std::optional<TrackArtistLinkType> linkType, std::optional<std::size_t> size = {});
|
||||
static std::vector<pointer> getAllOrphans(Session& session); // No track related
|
||||
static std::vector<pointer> getLastWritten(Session& session,
|
||||
std::optional<Wt::WDateTime> after,
|
||||
const std::vector<ClusterId>& clusters,
|
||||
std::optional<TrackArtistLinkType> linkType, // if set, only artists that have produced at least one track with this link type
|
||||
std::optional<Range>,
|
||||
bool& moreResults);
|
||||
static std::vector<ArtistId> getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit = {});
|
||||
static std::vector<pointer> getStarred(Session& session,
|
||||
ObjectPtr<User> user,
|
||||
const std::vector<ClusterId>& clusters,
|
||||
std::optional<TrackArtistLinkType> linkType, // if set, only artists that have produced at least one track with this link type
|
||||
SortMethod sortMethod,
|
||||
std::optional<Range>, bool& moreResults);
|
||||
|
||||
// Accessors
|
||||
const std::string& getName() const { return _name; }
|
||||
const std::string& getSortName() const { return _sortName; }
|
||||
std::optional<UUID> getMBID() const { return UUID::fromString(_MBID); }
|
||||
|
||||
std::vector<ObjectPtr<Release>> getReleases(const std::vector<ClusterId>& clusterIds = {}) const; // if non empty, get the releases that match all these clusters
|
||||
std::size_t getReleaseCount() const;
|
||||
std::vector<ObjectPtr<Track>> getTracks(std::optional<TrackArtistLinkType> linkType = {}) const;
|
||||
bool hasNonReleaseTracks(std::optional<TrackArtistLinkType> linkType = std::nullopt) const;
|
||||
std::vector<ObjectPtr<Track>> getNonReleaseTracks(std::optional<TrackArtistLinkType> linkType, std::optional<Range> range, bool& moreResults) const;
|
||||
std::vector<ObjectPtr<Track>> getRandomTracks(std::optional<std::size_t> count) const;
|
||||
|
||||
// No artistLinkTypes means get them all
|
||||
std::vector<pointer> getSimilarArtists(EnumSet<TrackArtistLinkType> artistLinkTypes = {}, std::optional<Range> range = std::nullopt) const;
|
||||
|
||||
// Get the cluster of the tracks made by this artist
|
||||
// Each clusters are grouped by cluster type, sorted by the number of occurence
|
||||
// size is the max number of cluster per cluster type
|
||||
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(std::vector<ObjectPtr<ClusterType>> clusterTypes, std::size_t size) const;
|
||||
|
||||
void setName(std::string_view name) { _name = name; }
|
||||
void setMBID(const std::optional<UUID>& mbid) { _MBID = mbid ? mbid->getAsString() : ""; }
|
||||
void setSortName(const std::string& sortName);
|
||||
|
||||
// Create
|
||||
static pointer create(Session& session, const std::string& name, const std::optional<UUID>& UUID = {});
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _name, "name");
|
||||
Wt::Dbo::field(a, _sortName, "sort_name");
|
||||
Wt::Dbo::field(a, _MBID, "mbid");
|
||||
|
||||
Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "artist");
|
||||
Wt::Dbo::hasMany(a, _starringUsers, Wt::Dbo::ManyToMany, "user_release_starred", "", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
static const std::size_t _maxNameLength = 128;
|
||||
|
||||
std::string _name;
|
||||
std::string _sortName;
|
||||
std::string _MBID; // Musicbrainz Identifier
|
||||
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks; // Tracks involving this artist
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<User>> _starringUsers; // Users that starred this artist
|
||||
};
|
||||
|
||||
} // namespace Database
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 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 <cstdint>
|
||||
#include <cassert>
|
||||
#include <functional>
|
||||
#include <Wt/Dbo/ptr.h>
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class IdType
|
||||
{
|
||||
public:
|
||||
using ValueType = Wt::Dbo::dbo_default_traits::IdType;
|
||||
|
||||
IdType() = default;
|
||||
IdType(ValueType id) : _id {id} { assert(isValid()); }
|
||||
|
||||
bool isValid() const { return _id != Wt::Dbo::dbo_default_traits::invalidId(); }
|
||||
std::string toString() const { assert(isValid()); return std::to_string(_id); }
|
||||
|
||||
ValueType getValue() const { return _id; }
|
||||
|
||||
bool operator==(IdType other) const { return other._id == _id; }
|
||||
bool operator!=(IdType other) const { return !(*this == other); }
|
||||
bool operator<(IdType other) const { return other._id < _id; }
|
||||
|
||||
private:
|
||||
Wt::Dbo::dbo_default_traits::IdType _id {Wt::Dbo::dbo_default_traits::invalidId()};
|
||||
};
|
||||
|
||||
struct Range
|
||||
{
|
||||
std::size_t offset {};
|
||||
std::size_t limit {};
|
||||
};
|
||||
|
||||
enum class TrackArtistLinkType
|
||||
{
|
||||
Artist, // regular artist
|
||||
Arranger,
|
||||
Composer,
|
||||
Conductor,
|
||||
Lyricist,
|
||||
Mixer,
|
||||
Performer,
|
||||
Producer,
|
||||
ReleaseArtist,
|
||||
Remixer,
|
||||
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,
|
||||
};
|
||||
|
||||
// Do not change enum values!
|
||||
enum class UserType
|
||||
{
|
||||
REGULAR = 0,
|
||||
ADMIN = 1,
|
||||
DEMO = 2,
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
class ObjectPtr
|
||||
{
|
||||
public:
|
||||
ObjectPtr() = default;
|
||||
ObjectPtr(Wt::Dbo::ptr<T> obj) : _obj {obj} {}
|
||||
|
||||
const T* operator->() const { return _obj.get(); }
|
||||
operator bool() const { return _obj.get(); }
|
||||
bool operator!() const { return !_obj.get(); }
|
||||
|
||||
auto modify() { return _obj.modify(); }
|
||||
void remove() { _obj.remove(); }
|
||||
|
||||
private:
|
||||
template <typename, typename> friend class Object;
|
||||
Wt::Dbo::ptr<T> _obj;
|
||||
};
|
||||
|
||||
template <typename T, typename ObjectIdType>
|
||||
class Object : public Wt::Dbo::Dbo<T>
|
||||
{
|
||||
static_assert(std::is_base_of_v<Database::IdType, ObjectIdType>);
|
||||
static_assert(!std::is_same_v<Database::IdType, ObjectIdType>);
|
||||
|
||||
public:
|
||||
using pointer = ObjectPtr<T>;
|
||||
using IdType = ObjectIdType;
|
||||
|
||||
IdType getId() const { return Wt::Dbo::Dbo<T>::self()->Wt::Dbo::template Dbo<T>::id(); }
|
||||
|
||||
// catch some misuses
|
||||
typename Wt::Dbo::dbo_traits<T>::IdType id() const = delete;
|
||||
|
||||
protected:
|
||||
// Can get raw dbo ptr only from Objects
|
||||
template <typename SomeObject>
|
||||
static
|
||||
Wt::Dbo::ptr<SomeObject> getDboPtr(ObjectPtr<SomeObject> ptr) { return ptr._obj; }
|
||||
};
|
||||
}
|
||||
|
||||
// TODO factorize hash with std::enable_if
|
||||
#define LMS_DECLARE_IDTYPE(name) \
|
||||
namespace Database { \
|
||||
class name : public IdType \
|
||||
{ \
|
||||
public: \
|
||||
using IdType::IdType; \
|
||||
};\
|
||||
} \
|
||||
namespace std \
|
||||
{ \
|
||||
template<> \
|
||||
class hash<Database::name> \
|
||||
{ \
|
||||
public: \
|
||||
size_t operator()(Database::name id) const \
|
||||
{ \
|
||||
return std::hash<Database::name::ValueType>()(id.getValue()); \
|
||||
} \
|
||||
}; \
|
||||
} // ns std
|
||||
|
||||
LMS_DECLARE_IDTYPE(ArtistId)
|
||||
LMS_DECLARE_IDTYPE(AuthTokenId)
|
||||
LMS_DECLARE_IDTYPE(ClusterId)
|
||||
LMS_DECLARE_IDTYPE(ClusterTypeId)
|
||||
LMS_DECLARE_IDTYPE(ReleaseId)
|
||||
LMS_DECLARE_IDTYPE(ScanSettingsId)
|
||||
LMS_DECLARE_IDTYPE(TrackArtistLinkId)
|
||||
LMS_DECLARE_IDTYPE(TrackBookmarkId)
|
||||
LMS_DECLARE_IDTYPE(TrackFeaturesId)
|
||||
LMS_DECLARE_IDTYPE(TrackId)
|
||||
LMS_DECLARE_IDTYPE(TrackListId)
|
||||
LMS_DECLARE_IDTYPE(TrackListEntryId)
|
||||
LMS_DECLARE_IDTYPE(UserId)
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
|
||||
add_library(lmsimage SHARED
|
||||
)
|
||||
|
||||
target_include_directories(lmsimage INTERFACE
|
||||
include
|
||||
)
|
||||
|
||||
target_include_directories(lmsimage PRIVATE
|
||||
include
|
||||
impl
|
||||
)
|
||||
|
||||
target_link_libraries(lmsimage PUBLIC
|
||||
lmsutils
|
||||
std::filesystem
|
||||
)
|
||||
|
||||
if (IMAGE_LIBRARY STREQUAL STB)
|
||||
target_sources(lmsimage PRIVATE
|
||||
impl/stb/JPEGImage.cpp
|
||||
impl/stb/RawImage.cpp
|
||||
)
|
||||
target_compile_options(lmsimage PRIVATE "-DLMS_SUPPORT_IMAGE_STB")
|
||||
target_include_directories(lmsimage PRIVATE ${STB_INCLUDE_DIR})
|
||||
elseif (IMAGE_LIBRARY STREQUAL GraphicsMagick++)
|
||||
target_sources(lmsimage PRIVATE
|
||||
impl/graphicsmagick/JPEGImage.cpp
|
||||
impl/graphicsmagick/RawImage.cpp
|
||||
)
|
||||
target_compile_options(lmsimage PRIVATE "-DLMS_SUPPORT_IMAGE_GM")
|
||||
target_link_libraries(lmsimage PRIVATE PkgConfig::GraphicsMagick++)
|
||||
else ()
|
||||
message(FATAL_ERROR "Invalid IMAGE_LIBRARY provided")
|
||||
endif()
|
||||
|
||||
install(TARGETS lmsimage DESTINATION lib)
|
||||
|
||||
+2
-2
@@ -19,11 +19,11 @@
|
||||
|
||||
#include "JPEGImage.hpp"
|
||||
|
||||
#include "Exception.hpp"
|
||||
#include "RawImage.hpp"
|
||||
#include "image/Exception.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
namespace CoverArt::GraphicsMagick
|
||||
namespace Image::GraphicsMagick
|
||||
{
|
||||
JPEGImage::JPEGImage(const RawImage& rawImage, unsigned quality)
|
||||
{
|
||||
+2
-2
@@ -25,9 +25,9 @@
|
||||
|
||||
#include <Magick++.h>
|
||||
|
||||
#include "cover/IEncodedImage.hpp"
|
||||
#include "image/IEncodedImage.hpp"
|
||||
|
||||
namespace CoverArt::GraphicsMagick
|
||||
namespace Image::GraphicsMagick
|
||||
{
|
||||
class RawImage;
|
||||
class JPEGImage : public IEncodedImage
|
||||
+30
-16
@@ -21,30 +21,44 @@
|
||||
|
||||
#include <magick/resource.h>
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
#include "JPEGImage.hpp"
|
||||
#include "Exception.hpp"
|
||||
#include "image/Exception.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
namespace CoverArt::GraphicsMagick {
|
||||
|
||||
void
|
||||
init(const std::filesystem::path& path)
|
||||
namespace Image
|
||||
{
|
||||
Magick::InitializeMagick(path.string().c_str());
|
||||
std::unique_ptr<IRawImage> decodeImage(const std::byte* encodedData, std::size_t encodedDataSize)
|
||||
{
|
||||
return std::make_unique<GraphicsMagick::RawImage>(encodedData, encodedDataSize);
|
||||
}
|
||||
|
||||
if (auto nbThreads {MagickLib::GetMagickResourceLimit(MagickLib::ThreadsResource)}; nbThreads != 1)
|
||||
LMS_LOG(COVER, WARNING) << "Consider setting env var OMP_NUM_THREADS=1 to save resources";
|
||||
std::unique_ptr<IRawImage> decodeImage(const std::filesystem::path& path)
|
||||
{
|
||||
return std::make_unique<GraphicsMagick::RawImage>(path);
|
||||
}
|
||||
|
||||
if (!MagickLib::SetMagickResourceLimit(MagickLib::ThreadsResource, 1))
|
||||
LMS_LOG(COVER, ERROR) << "Cannot set Magick thread resource limit to 1!";
|
||||
void
|
||||
init(const std::filesystem::path& path)
|
||||
{
|
||||
Magick::InitializeMagick(path.string().c_str());
|
||||
|
||||
if (!MagickLib::SetMagickResourceLimit(MagickLib::DiskResource, 0))
|
||||
LMS_LOG(COVER, ERROR) << "Cannot set Magick disk resource limit to 0!";
|
||||
if (auto nbThreads {MagickLib::GetMagickResourceLimit(MagickLib::ThreadsResource)}; nbThreads != 1)
|
||||
LMS_LOG(COVER, WARNING) << "Consider setting env var OMP_NUM_THREADS=1 to save resources";
|
||||
|
||||
LMS_LOG(COVER, INFO) << "Magick threads resource limit = " << GetMagickResourceLimit(MagickLib::ThreadsResource);
|
||||
LMS_LOG(COVER, INFO) << "Magick Disk resource limit = " << GetMagickResourceLimit(MagickLib::DiskResource);
|
||||
if (!MagickLib::SetMagickResourceLimit(MagickLib::ThreadsResource, 1))
|
||||
LMS_LOG(COVER, ERROR) << "Cannot set Magick thread resource limit to 1!";
|
||||
|
||||
if (!MagickLib::SetMagickResourceLimit(MagickLib::DiskResource, 0))
|
||||
LMS_LOG(COVER, ERROR) << "Cannot set Magick disk resource limit to 0!";
|
||||
|
||||
LMS_LOG(COVER, INFO) << "Magick threads resource limit = " << GetMagickResourceLimit(MagickLib::ThreadsResource);
|
||||
LMS_LOG(COVER, INFO) << "Magick Disk resource limit = " << GetMagickResourceLimit(MagickLib::DiskResource);
|
||||
}
|
||||
}
|
||||
|
||||
namespace Image::GraphicsMagick
|
||||
{
|
||||
|
||||
RawImage::RawImage(const std::byte* encodedData, std::size_t encodedDataSize)
|
||||
{
|
||||
try
|
||||
@@ -116,5 +130,5 @@ RawImage::getMagickImage() const
|
||||
return _image;
|
||||
}
|
||||
|
||||
} // namespace CoverArt::GraphicsMagick
|
||||
} // namespace Image::GraphicsMagick
|
||||
|
||||
+4
-6
@@ -28,14 +28,12 @@
|
||||
#include <cstddef>
|
||||
#include <filesystem>
|
||||
|
||||
#include "cover/IEncodedImage.hpp"
|
||||
#include "IRawImage.hpp"
|
||||
#include "image/IEncodedImage.hpp"
|
||||
#include "image/IRawImage.hpp"
|
||||
|
||||
namespace CoverArt::GraphicsMagick
|
||||
namespace Image::GraphicsMagick
|
||||
{
|
||||
void init(const std::filesystem::path& path);
|
||||
|
||||
class RawImage : IRawImage
|
||||
class RawImage : public IRawImage
|
||||
{
|
||||
public:
|
||||
RawImage(const std::byte* encodedData, std::size_t encodedDataSize);
|
||||
@@ -22,10 +22,10 @@
|
||||
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
||||
#include <stb/stb_image_write.h>
|
||||
|
||||
#include "image/Exception.hpp"
|
||||
#include "RawImage.hpp"
|
||||
#include "Exception.hpp"
|
||||
|
||||
namespace CoverArt::STB
|
||||
namespace Image::STB
|
||||
{
|
||||
JPEGImage::JPEGImage(const RawImage& rawImage, unsigned quality)
|
||||
{
|
||||
@@ -21,9 +21,9 @@
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "cover/IEncodedImage.hpp"
|
||||
#include "image/IEncodedImage.hpp"
|
||||
|
||||
namespace CoverArt::STB
|
||||
namespace Image::STB
|
||||
{
|
||||
class RawImage;
|
||||
class JPEGImage : public IEncodedImage
|
||||
@@ -30,9 +30,27 @@
|
||||
|
||||
#include "JPEGImage.hpp"
|
||||
|
||||
#include "Exception.hpp"
|
||||
#include "image/Exception.hpp"
|
||||
|
||||
namespace CoverArt::STB
|
||||
namespace Image
|
||||
{
|
||||
std::unique_ptr<IRawImage> decodeImage(const std::byte* encodedData, std::size_t encodedDataSize)
|
||||
{
|
||||
return std::make_unique<STB::RawImage>(encodedData, encodedDataSize);
|
||||
}
|
||||
|
||||
std::unique_ptr<IRawImage> decodeImage(const std::filesystem::path& path)
|
||||
{
|
||||
return std::make_unique<STB::RawImage>(path);
|
||||
}
|
||||
|
||||
void
|
||||
init(const std::filesystem::path&)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
namespace Image::STB
|
||||
{
|
||||
RawImage::RawImage(const std::byte* encodedData, std::size_t encodedDataSize)
|
||||
{
|
||||
@@ -26,10 +26,10 @@
|
||||
#include <cstddef>
|
||||
#include <filesystem>
|
||||
|
||||
#include "cover/IEncodedImage.hpp"
|
||||
#include "IRawImage.hpp"
|
||||
#include "image/IEncodedImage.hpp"
|
||||
#include "image/IRawImage.hpp"
|
||||
|
||||
namespace CoverArt::STB
|
||||
namespace Image::STB
|
||||
{
|
||||
class RawImage : public IRawImage
|
||||
{
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
#include "utils/Exception.hpp"
|
||||
|
||||
namespace CoverArt
|
||||
namespace Image
|
||||
{
|
||||
// internal use only
|
||||
class ImageException : public LmsException
|
||||
@@ -30,5 +30,5 @@ namespace CoverArt
|
||||
using LmsException::LmsException;
|
||||
};
|
||||
|
||||
} // namespace CoverArt
|
||||
} // namespace Cover
|
||||
|
||||
+2
-2
@@ -22,7 +22,7 @@
|
||||
#include <cstddef>
|
||||
#include <string_view>
|
||||
|
||||
namespace CoverArt
|
||||
namespace Image
|
||||
{
|
||||
using ImageSize = std::size_t;
|
||||
|
||||
@@ -36,5 +36,5 @@ namespace CoverArt
|
||||
virtual std::string_view getMimeType() const = 0;
|
||||
};
|
||||
|
||||
} // namespace CoverArt
|
||||
} // namespace Cover
|
||||
|
||||
@@ -19,11 +19,12 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
|
||||
#include "cover/IEncodedImage.hpp"
|
||||
#include "image/IEncodedImage.hpp"
|
||||
|
||||
namespace CoverArt
|
||||
namespace Image
|
||||
{
|
||||
class IRawImage
|
||||
{
|
||||
@@ -32,5 +33,9 @@ namespace CoverArt
|
||||
virtual void resize(ImageSize width) = 0;
|
||||
virtual std::unique_ptr<IEncodedImage> encodeToJPEG(unsigned quality) const = 0;
|
||||
};
|
||||
|
||||
void init(const std::filesystem::path& path);
|
||||
std::unique_ptr<IRawImage> decodeImage(const std::byte* encodedData, std::size_t encodedDataSize);
|
||||
std::unique_ptr<IRawImage> decodeImage(const std::filesystem::path& path);
|
||||
}
|
||||
|
||||
@@ -14,5 +14,7 @@ target_link_libraries(test-metadata PRIVATE
|
||||
GTest::GTest
|
||||
)
|
||||
|
||||
gtest_discover_tests(test-metadata)
|
||||
if (NOT CMAKE_CROSSCOMPILING)
|
||||
gtest_discover_tests(test-metadata)
|
||||
endif()
|
||||
|
||||
|
||||
@@ -1,290 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2019 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 "Engine.hpp"
|
||||
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "ClustersClassifierCreator.hpp"
|
||||
#include "FeaturesEngineCreator.hpp"
|
||||
|
||||
#include "database/Db.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/ScanSettings.hpp"
|
||||
#include "utils/Exception.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
namespace Recommendation {
|
||||
|
||||
|
||||
static
|
||||
std::unique_ptr<IClassifier>
|
||||
createClassifier(ClassifierType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case ClassifierType::Clusters:
|
||||
return createClustersClassifier();
|
||||
break;
|
||||
|
||||
case ClassifierType::Features:
|
||||
return createFeaturesEngine();
|
||||
break;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
std::unique_ptr<IEngine>
|
||||
createEngine(Database::Db& db)
|
||||
{
|
||||
return std::make_unique<Engine>(db);
|
||||
}
|
||||
|
||||
Engine::Engine(Database::Db& db)
|
||||
: _db {db}
|
||||
{
|
||||
}
|
||||
|
||||
Engine::TrackContainer
|
||||
Engine::getSimilarTracksFromTrackList(Database::Session& session, Database::TrackListId trackListId, std::size_t maxCount)
|
||||
{
|
||||
TrackContainer res;
|
||||
|
||||
std::shared_lock lock {_classifiersMutex};
|
||||
for (const auto& classifierName : _classifierPriorities)
|
||||
{
|
||||
auto itClassifier {_classifiers.find(classifierName)};
|
||||
if (itClassifier == std::cend(_classifiers))
|
||||
continue;
|
||||
|
||||
res = itClassifier->second->getSimilarTracksFromTrackList(session, trackListId, maxCount);
|
||||
if (!res.empty())
|
||||
break;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
Engine::TrackContainer
|
||||
Engine::getSimilarTracks(Database::Session& dbSession, const std::vector<Database::TrackId>& trackIds, std::size_t maxCount)
|
||||
{
|
||||
TrackContainer res;
|
||||
|
||||
std::shared_lock lock {_classifiersMutex};
|
||||
for (ClassifierType classifierType : _classifierPriorities)
|
||||
{
|
||||
auto itClassifier {_classifiers.find(classifierType)};
|
||||
if (itClassifier == std::cend(_classifiers))
|
||||
continue;
|
||||
|
||||
const IClassifier& classifier {*itClassifier->second};
|
||||
res = classifier.getSimilarTracks(dbSession, trackIds, maxCount);
|
||||
if (!res.empty())
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar tracks using classifier '" << classifier.getName() << "'";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
Engine::ReleaseContainer
|
||||
Engine::getSimilarReleases(Database::Session& dbSession, Database::ReleaseId releaseId, std::size_t maxCount)
|
||||
{
|
||||
ReleaseContainer res;
|
||||
|
||||
std::shared_lock lock {_classifiersMutex};
|
||||
for (ClassifierType classifierType : _classifierPriorities)
|
||||
{
|
||||
auto itClassifier {_classifiers.find(classifierType)};
|
||||
if (itClassifier == std::cend(_classifiers))
|
||||
continue;
|
||||
|
||||
const IClassifier& classifier {*itClassifier->second};
|
||||
res = classifier.getSimilarReleases(dbSession, releaseId, maxCount);
|
||||
if (!res.empty())
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar releases using classifier '" << classifier.getName() << "'";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
Engine::ArtistContainer
|
||||
Engine::getSimilarArtists(Database::Session& dbSession, Database::ArtistId artistId, EnumSet<Database::TrackArtistLinkType> linkTypes, std::size_t maxCount)
|
||||
{
|
||||
ArtistContainer res;
|
||||
|
||||
std::shared_lock lock {_classifiersMutex};
|
||||
for (ClassifierType classifierType : _classifierPriorities)
|
||||
{
|
||||
auto itClassifier {_classifiers.find(classifierType)};
|
||||
if (itClassifier == std::cend(_classifiers))
|
||||
continue;
|
||||
|
||||
const IClassifier& classifier {*itClassifier->second};
|
||||
res = classifier.getSimilarArtists(dbSession, artistId, linkTypes, maxCount);
|
||||
if (!res.empty())
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Got " << res.size() << " similar artists using classifier '" << classifier.getName() << "'";
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
static
|
||||
Database::ScanSettings::RecommendationEngineType
|
||||
getRecommendationEngineType(Database::Session& session)
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
return Database::ScanSettings::get(session)->getRecommendationEngineType();
|
||||
}
|
||||
|
||||
void
|
||||
Engine::load(bool forceReload, const ProgressCallback& progressCallback)
|
||||
{
|
||||
using namespace Database;
|
||||
|
||||
LMS_LOG(RECOMMENDATION, INFO) << "Reloading recommendation engines...";
|
||||
struct ClassifierWithType
|
||||
{
|
||||
ClassifierType type;
|
||||
std::unique_ptr<IClassifier> classifier;
|
||||
};
|
||||
|
||||
std::vector<ClassifierWithType> classifiers;
|
||||
auto addClassifier {[&](ClassifierType type)
|
||||
{
|
||||
classifiers.emplace_back(ClassifierWithType {type, createClassifier(type)});
|
||||
}};
|
||||
|
||||
switch (getRecommendationEngineType(_db.getTLSSession()))
|
||||
{
|
||||
case ScanSettings::RecommendationEngineType::Clusters:
|
||||
setClassifierPriorities({ClassifierType::Clusters});
|
||||
addClassifier(ClassifierType::Clusters);
|
||||
break;
|
||||
case ScanSettings::RecommendationEngineType::Features:
|
||||
setClassifierPriorities({ClassifierType::Features, ClassifierType::Clusters});
|
||||
// not same order since clusters is faster to load
|
||||
addClassifier(ClassifierType::Clusters);
|
||||
addClassifier(ClassifierType::Features);
|
||||
break;
|
||||
}
|
||||
|
||||
assert(_pendingClassifiers.empty());
|
||||
clearClassifiers();
|
||||
|
||||
{
|
||||
std::scoped_lock lock {_controlMutex};
|
||||
|
||||
std::transform(std::cbegin(classifiers), std::cend(classifiers), std::inserter(_pendingClassifiers, std::end(_pendingClassifiers)),
|
||||
[](auto& classifier) { return classifier.classifier.get(); });
|
||||
}
|
||||
|
||||
for (ClassifierWithType& classifier : classifiers)
|
||||
loadClassifier(std::move(classifier.classifier), classifier.type, forceReload, progressCallback);
|
||||
|
||||
LMS_LOG(RECOMMENDATION, INFO) << "Recommendation engines loaded!";
|
||||
}
|
||||
|
||||
void
|
||||
Engine::setClassifierPriorities(const std::vector<ClassifierType>& classifierPriorities)
|
||||
{
|
||||
std::unique_lock<std::shared_mutex> lock {_classifiersMutex};
|
||||
|
||||
_classifierPriorities = classifierPriorities;
|
||||
}
|
||||
|
||||
void
|
||||
Engine::clearClassifiers()
|
||||
{
|
||||
std::unique_lock lock {_classifiersMutex};
|
||||
|
||||
_classifiers.clear();
|
||||
}
|
||||
|
||||
void
|
||||
Engine::loadClassifier(std::unique_ptr<IClassifier> classifier,
|
||||
ClassifierType classifierType,
|
||||
bool forceReload,
|
||||
const ProgressCallback& progressCallback)
|
||||
{
|
||||
IClassifier* rawClassifier {classifier.get()};
|
||||
|
||||
bool res {};
|
||||
if (!_loadCancelled)
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, INFO) << "Initializing classifier '" << classifier->getName() << "'...";
|
||||
|
||||
auto progress {[&](IClassifier::Progress progress)
|
||||
{
|
||||
progressCallback(Progress {progress.processedElems, progress.totalElems});
|
||||
}};
|
||||
|
||||
res = classifier->load(_db.getTLSSession(), forceReload, progressCallback ? progress : IClassifier::ProgressCallback {});
|
||||
|
||||
LMS_LOG(RECOMMENDATION, INFO) << "Initializing classifier '" << classifier->getName() << "': " << (res ? "SUCCESS" : "FAILURE");
|
||||
}
|
||||
|
||||
if (res)
|
||||
{
|
||||
std::unique_lock lock {_classifiersMutex};
|
||||
|
||||
_classifiers.emplace(classifierType, std::move(classifier));
|
||||
}
|
||||
|
||||
{
|
||||
std::scoped_lock lock {_controlMutex};
|
||||
|
||||
_pendingClassifiers.erase(rawClassifier);
|
||||
}
|
||||
|
||||
_pendingClassifiersCondvar.notify_one();
|
||||
}
|
||||
|
||||
void
|
||||
Engine::cancelLoad()
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Cancelling loading...";
|
||||
|
||||
std::unique_lock lock {_controlMutex};
|
||||
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Still " << _pendingClassifiers.size() << " pending classifiers!";
|
||||
|
||||
_loadCancelled = true;
|
||||
|
||||
for (IClassifier* classifier : _pendingClassifiers)
|
||||
classifier->requestCancelLoad();
|
||||
|
||||
_pendingClassifiersCondvar.wait(lock, [this] {return _pendingClassifiers.empty();});
|
||||
_loadCancelled = false;
|
||||
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Cancelling loading DONE";
|
||||
}
|
||||
|
||||
} // ns Similarity
|
||||
@@ -1,86 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2019 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 <condition_variable>
|
||||
#include <shared_mutex>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "recommendation/IEngine.hpp"
|
||||
#include "IClassifier.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Db;
|
||||
}
|
||||
|
||||
namespace Recommendation
|
||||
{
|
||||
enum class ClassifierType
|
||||
{
|
||||
Clusters,
|
||||
Features,
|
||||
};
|
||||
|
||||
class Engine : public IEngine
|
||||
{
|
||||
public:
|
||||
Engine(Database::Db& db);
|
||||
~Engine() = default;
|
||||
|
||||
Engine(const Engine&) = delete;
|
||||
Engine(Engine&&) = delete;
|
||||
Engine& operator=(const Engine&) = delete;
|
||||
Engine& operator=(Engine&&) = delete;
|
||||
|
||||
private:
|
||||
void load(bool forceReload, const ProgressCallback& progressCallback) override;
|
||||
void cancelLoad() override;
|
||||
void requestCancelLoad() override {};
|
||||
|
||||
ResultContainer<Database::TrackId> getSimilarTracksFromTrackList(Database::Session& session, Database::TrackListId tracklistId, std::size_t maxCount) override;
|
||||
ResultContainer<Database::TrackId> getSimilarTracks(Database::Session& session, const std::vector<Database::TrackId>& tracksId, std::size_t maxCount) override;
|
||||
ResultContainer<Database::ReleaseId> getSimilarReleases(Database::Session& session, Database::ReleaseId releaseId, std::size_t maxCount) override;
|
||||
ResultContainer<Database::ArtistId> getSimilarArtists(Database::Session& session,
|
||||
Database::ArtistId artistId,
|
||||
EnumSet<Database::TrackArtistLinkType> linkTypes,
|
||||
std::size_t maxCount) override;
|
||||
|
||||
void setClassifierPriorities(const std::vector<ClassifierType>& classifierTypes);
|
||||
void clearClassifiers();
|
||||
void loadClassifier(std::unique_ptr<IClassifier> classifier, ClassifierType classifierType, bool forceReload, const ProgressCallback& progressCallback);
|
||||
|
||||
Database::Db& _db;
|
||||
|
||||
std::mutex _controlMutex;
|
||||
bool _loadCancelled {};
|
||||
std::condition_variable _pendingClassifiersCondvar;
|
||||
std::unordered_set<IClassifier*> _pendingClassifiers;
|
||||
|
||||
std::shared_mutex _classifiersMutex;
|
||||
using ClassifierContainer = std::unordered_map<ClassifierType, std::unique_ptr<IClassifier>>;
|
||||
ClassifierContainer _classifiers;
|
||||
std::vector<ClassifierType> _classifierPriorities; // ordered by priority
|
||||
};
|
||||
|
||||
} // ns Recommendation
|
||||
|
||||
@@ -1,65 +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/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "database/Types.hpp"
|
||||
#include "recommendation/IRecommendation.hpp"
|
||||
#include "utils/EnumSet.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Session;
|
||||
}
|
||||
|
||||
namespace Recommendation
|
||||
{
|
||||
|
||||
class IClassifier : public IRecommendation
|
||||
{
|
||||
public:
|
||||
virtual ~IClassifier() = default;
|
||||
|
||||
virtual std::string_view getName() const = 0;
|
||||
|
||||
struct Progress
|
||||
{
|
||||
std::size_t totalElems {};
|
||||
std::size_t processedElems {};
|
||||
};
|
||||
using ProgressCallback = std::function<void(const Progress&)>;
|
||||
virtual bool load(Database::Session& session, bool forceReload, const ProgressCallback& progressCallback) = 0;
|
||||
virtual void requestCancelLoad() = 0;
|
||||
|
||||
template <typename IdType>
|
||||
using ResultContainer = std::vector<IdType>;
|
||||
|
||||
virtual ResultContainer<Database::TrackId> getSimilarTracksFromTrackList(Database::Session& session, Database::TrackListId tracklistId, std::size_t maxCount) const = 0;
|
||||
virtual ResultContainer<Database::TrackId> getSimilarTracks(Database::Session& session, const std::vector<Database::TrackId>& tracksId, std::size_t maxCount) const = 0;
|
||||
virtual ResultContainer<Database::ReleaseId> getSimilarReleases(Database::Session& session, Database::ReleaseId releaseId, std::size_t maxCount) const = 0;
|
||||
virtual ResultContainer<Database::ArtistId> getSimilarArtists(Database::Session& session,
|
||||
Database::ArtistId artistId,
|
||||
EnumSet<Database::TrackArtistLinkType> linkTypes, std::size_t maxCount) const = 0;
|
||||
};
|
||||
|
||||
} // ns Recommendation
|
||||
@@ -1,103 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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 "ClustersClassifier.hpp"
|
||||
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Cluster.hpp"
|
||||
#include "database/Release.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "database/TrackList.hpp"
|
||||
|
||||
namespace Recommendation {
|
||||
|
||||
std::unique_ptr<IClassifier> createClustersClassifier()
|
||||
{
|
||||
return std::make_unique<ClusterClassifier>();
|
||||
}
|
||||
|
||||
IClassifier::ResultContainer<Database::TrackId>
|
||||
ClusterClassifier::getSimilarTracks(Database::Session& dbSession, const std::vector<Database::TrackId>& trackIds, std::size_t maxCount) const
|
||||
{
|
||||
ResultContainer<Database::TrackId> res;
|
||||
|
||||
auto transaction {dbSession.createSharedTransaction()};
|
||||
|
||||
const auto tracks {Database::Track::getSimilarTracks(dbSession, trackIds, 0, maxCount)};
|
||||
std::transform(std::cbegin(tracks), std::cend(tracks), std::back_inserter(res), [](const auto& track) { return track->getId(); });
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
IClassifier::ResultContainer<Database::TrackId>
|
||||
ClusterClassifier::getSimilarTracksFromTrackList(Database::Session& session, Database::TrackListId tracklistId, std::size_t maxCount) const
|
||||
{
|
||||
ResultContainer<Database::TrackId> res;
|
||||
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
const Database::TrackList::pointer trackList {Database::TrackList::getById(session, tracklistId)};
|
||||
if (!trackList)
|
||||
return res;
|
||||
|
||||
const auto tracks {trackList->getSimilarTracks(0, maxCount)};
|
||||
std::transform(std::cbegin(tracks), std::cend(tracks), std::back_inserter(res), [](const auto& track) { return track->getId(); });
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
IClassifier::ResultContainer<Database::ReleaseId>
|
||||
ClusterClassifier::getSimilarReleases(Database::Session& dbSession, Database::ReleaseId releaseId, std::size_t maxCount) const
|
||||
{
|
||||
ResultContainer<Database::ReleaseId> res;
|
||||
|
||||
auto transaction {dbSession.createSharedTransaction()};
|
||||
|
||||
auto release {Database::Release::getById(dbSession, releaseId)};
|
||||
if (!release)
|
||||
return res;
|
||||
|
||||
const auto releases {release->getSimilarReleases(0, maxCount)};
|
||||
std::transform(std::cbegin(releases), std::cend(releases), std::back_inserter(res), [](const auto& release) { return release->getId(); });
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
IClassifier::ResultContainer<Database::ArtistId>
|
||||
ClusterClassifier::getSimilarArtists(Database::Session& dbSession,
|
||||
Database::ArtistId artistId,
|
||||
EnumSet<Database::TrackArtistLinkType> artistLinkTypes,
|
||||
std::size_t maxCount) const
|
||||
{
|
||||
ResultContainer<Database::ArtistId> res;
|
||||
|
||||
auto transaction {dbSession.createSharedTransaction()};
|
||||
|
||||
auto artist {Database::Artist::getById(dbSession, artistId)};
|
||||
if (!artist)
|
||||
return res;
|
||||
|
||||
const auto artists {artist->getSimilarArtists(artistLinkTypes, Database::Range {0, maxCount})};
|
||||
std::transform(std::cbegin(artists), std::cend(artists), std::back_inserter(res), [](const auto& artist) { return artist->getId(); });
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
} // namespace Recommendation
|
||||
@@ -1,53 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2018 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 "IClassifier.hpp"
|
||||
|
||||
namespace Recommendation
|
||||
{
|
||||
|
||||
class ClusterClassifier : public IClassifier
|
||||
{
|
||||
public:
|
||||
ClusterClassifier() = default;
|
||||
ClusterClassifier(const ClusterClassifier&) = delete;
|
||||
ClusterClassifier(ClusterClassifier&&) = delete;
|
||||
ClusterClassifier& operator=(const ClusterClassifier&) = delete;
|
||||
ClusterClassifier& operator=(ClusterClassifier&&) = delete;
|
||||
|
||||
private:
|
||||
|
||||
std::string_view getName() const override { return "Clusters"; }
|
||||
|
||||
bool load(Database::Session&, bool, const ProgressCallback&) override { return true; }
|
||||
void requestCancelLoad() override {}
|
||||
|
||||
ResultContainer<Database::TrackId> getSimilarTracksFromTrackList(Database::Session& session, Database::TrackListId tracklistId, std::size_t maxCount) const override;
|
||||
ResultContainer<Database::TrackId> getSimilarTracks(Database::Session& session, const std::vector<Database::TrackId>& tracksId, std::size_t maxCount) const override;
|
||||
ResultContainer<Database::ReleaseId> getSimilarReleases(Database::Session& session, Database::ReleaseId releaseId, std::size_t maxCount) const override;
|
||||
ResultContainer<Database::ArtistId> getSimilarArtists(Database::Session& session,
|
||||
Database::ArtistId artistId,
|
||||
EnumSet<Database::TrackArtistLinkType> linkTypes,
|
||||
std::size_t maxCount) const override;
|
||||
};
|
||||
|
||||
} // namespace Recommendation
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2019 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 <functional>
|
||||
#include <memory>
|
||||
#include "database/Types.hpp"
|
||||
#include "utils/EnumSet.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Db;
|
||||
class Session;
|
||||
}
|
||||
|
||||
namespace Recommendation
|
||||
{
|
||||
class IEngine
|
||||
{
|
||||
public:
|
||||
virtual ~IEngine() = default;
|
||||
|
||||
struct Progress
|
||||
{
|
||||
std::size_t totalElems {};
|
||||
std::size_t processedElems {};
|
||||
};
|
||||
using ProgressCallback = std::function<void(const Progress&)>;
|
||||
virtual void load(bool forceReload, const ProgressCallback& progressCallback = {}) = 0;
|
||||
virtual void cancelLoad() = 0;
|
||||
|
||||
template <typename IdType>
|
||||
using ResultContainer = std::vector<IdType>;
|
||||
|
||||
using ArtistContainer = ResultContainer<Database::ArtistId>;
|
||||
using ReleaseContainer = ResultContainer<Database::ReleaseId>;
|
||||
using TrackContainer = ResultContainer<Database::TrackId>;
|
||||
|
||||
virtual TrackContainer getSimilarTracksFromTrackList(Database::Session& session, Database::TrackListId tracklistId, std::size_t maxCount) = 0;
|
||||
virtual TrackContainer getSimilarTracks(Database::Session& session, const std::vector<Database::TrackId>& tracksId, std::size_t maxCount) = 0;
|
||||
virtual ReleaseContainer getSimilarReleases(Database::Session& session, Database::ReleaseId releaseId, std::size_t maxCount) = 0;
|
||||
virtual ArtistContainer getSimilarArtists(Database::Session& session, Database::ArtistId artistId, EnumSet<Database::TrackArtistLinkType> linkTypes, std::size_t maxCount) = 0;
|
||||
|
||||
protected:
|
||||
virtual void requestCancelLoad() = 0;
|
||||
};
|
||||
|
||||
std::unique_ptr<IEngine> createEngine(Database::Db& db);
|
||||
|
||||
} // ns Recommendation
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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 <chrono>
|
||||
#include <shared_mutex>
|
||||
#include <optional>
|
||||
#include <unordered_set>
|
||||
|
||||
#include <Wt/WDateTime.h>
|
||||
#include <Wt/WIOService.h>
|
||||
#include <Wt/WSignal.h>
|
||||
|
||||
#include <boost/asio/system_timer.hpp>
|
||||
|
||||
#include "database/Types.hpp"
|
||||
#include "database/ScanSettings.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "metadata/IParser.hpp"
|
||||
#include "scanner/IScanner.hpp"
|
||||
#include "utils/Path.hpp"
|
||||
|
||||
class UUID;
|
||||
|
||||
namespace Recommendation
|
||||
{
|
||||
class IEngine;
|
||||
}
|
||||
|
||||
namespace Scanner {
|
||||
|
||||
class Scanner : public IScanner
|
||||
{
|
||||
public:
|
||||
Scanner(Database::Db& db, Recommendation::IEngine& recommendationEngine);
|
||||
~Scanner();
|
||||
|
||||
Scanner(const Scanner&) = delete;
|
||||
Scanner(Scanner&&) = delete;
|
||||
Scanner& operator=(const Scanner&) = delete;
|
||||
Scanner& operator=(Scanner&&) = delete;
|
||||
|
||||
void requestReload() override;
|
||||
void requestImmediateScan(bool force) override;
|
||||
|
||||
Status getStatus() const override;
|
||||
Events& getEvents() override { return _events; }
|
||||
|
||||
private:
|
||||
void start();
|
||||
void stop();
|
||||
|
||||
// Job handling
|
||||
void scheduleNextScan();
|
||||
void scheduleScan(bool force, const Wt::WDateTime& dateTime = {});
|
||||
|
||||
void abortScan();
|
||||
|
||||
// Update database (scheduled callback)
|
||||
void scan(bool force);
|
||||
|
||||
void scanMediaDirectory( const std::filesystem::path& mediaDirectory, bool forceScan, ScanStats& stats);
|
||||
bool fetchTrackFeatures(Database::TrackId trackId, const UUID& MBID);
|
||||
void fetchTrackFeatures(ScanStats& stats);
|
||||
|
||||
// Helpers
|
||||
void refreshScanSettings();
|
||||
|
||||
void countAllFiles(ScanStats& stats);
|
||||
void removeMissingTracks(ScanStats& stats);
|
||||
void removeOrphanEntries();
|
||||
void checkDuplicatedAudioFiles(ScanStats& stats);
|
||||
void scanAudioFile(const std::filesystem::path& file, bool forceScan, ScanStats& stats);
|
||||
void notifyInProgressIfNeeded(const ScanStepStats& stats);
|
||||
void notifyInProgress(const ScanStepStats& stats);
|
||||
void reloadSimilarityEngine(ScanStats& stats);
|
||||
|
||||
Recommendation::IEngine& _recommendationEngine;
|
||||
|
||||
std::mutex _controlMutex;
|
||||
std::atomic<bool> _abortScan {};
|
||||
Wt::WIOService _ioService;
|
||||
boost::asio::system_timer _scheduleTimer {_ioService};
|
||||
Events _events;
|
||||
std::chrono::system_clock::time_point _lastScanInProgressEmit {};
|
||||
Database::Session _dbSession;
|
||||
std::unique_ptr<MetaData::IParser> _metadataParser;
|
||||
|
||||
mutable std::shared_mutex _statusMutex;
|
||||
State _curState {State::NotScheduled};
|
||||
std::optional<ScanStats> _lastCompleteScanStats;
|
||||
std::optional<ScanStepStats> _currentScanStepStats;
|
||||
Wt::WDateTime _nextScheduledScan;
|
||||
|
||||
// Current scan settings
|
||||
std::size_t _scanVersion {};
|
||||
Wt::WTime _startTime;
|
||||
Database::ScanSettings::UpdatePeriod _updatePeriod {Database::ScanSettings::UpdatePeriod::Never};
|
||||
std::unordered_set<std::filesystem::path> _fileExtensions;
|
||||
std::filesystem::path _mediaDirectory;
|
||||
Database::ScanSettings::RecommendationEngineType _recommendationEngineType;
|
||||
};
|
||||
|
||||
} // Scanner
|
||||
|
||||
@@ -1,186 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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 "Scrobbling.hpp"
|
||||
|
||||
#include "database/Db.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/TrackList.hpp"
|
||||
#include "database/User.hpp"
|
||||
|
||||
#include "internal/InternalScrobbler.hpp"
|
||||
#include "listenbrainz/ListenBrainzScrobbler.hpp"
|
||||
|
||||
namespace Scrobbling
|
||||
{
|
||||
std::unique_ptr<IScrobbling>
|
||||
createScrobbling(boost::asio::io_context& ioContext, Database::Db& db)
|
||||
{
|
||||
return std::make_unique<Scrobbling>(ioContext, db);
|
||||
}
|
||||
|
||||
Scrobbling::Scrobbling(boost::asio::io_context& ioContext, Database::Db& db)
|
||||
: _db {db}
|
||||
{
|
||||
_scrobblers.emplace(Database::Scrobbler::Internal, std::make_unique<InternalScrobbler>(_db));
|
||||
_scrobblers.emplace(Database::Scrobbler::ListenBrainz, std::make_unique<ListenBrainz::Scrobbler>(ioContext, _db));
|
||||
}
|
||||
|
||||
void
|
||||
Scrobbling::listenStarted(const Listen& listen)
|
||||
{
|
||||
if (std::optional<Database::Scrobbler> scrobbler {getUserScrobbler(listen.userId)})
|
||||
_scrobblers[*scrobbler]->listenStarted(listen);
|
||||
}
|
||||
|
||||
void
|
||||
Scrobbling::listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration)
|
||||
{
|
||||
if (std::optional<Database::Scrobbler> scrobbler {getUserScrobbler(listen.userId)})
|
||||
_scrobblers[*scrobbler]->listenFinished(listen, duration);
|
||||
}
|
||||
|
||||
void
|
||||
Scrobbling::addTimedListen(const TimedListen& listen)
|
||||
{
|
||||
if (std::optional<Database::Scrobbler> scrobbler {getUserScrobbler(listen.userId)})
|
||||
_scrobblers[*scrobbler]->addTimedListen(listen);
|
||||
}
|
||||
|
||||
std::optional<Database::Scrobbler>
|
||||
Scrobbling::getUserScrobbler(Database::UserId userId)
|
||||
{
|
||||
std::optional<Database::Scrobbler> scrobbler;
|
||||
|
||||
Database::Session& session {_db.getTLSSession()};
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
if (const Database::User::pointer user {Database::User::getById(session, userId)})
|
||||
scrobbler = user->getScrobbler();
|
||||
|
||||
return scrobbler;
|
||||
}
|
||||
|
||||
std::vector<Database::ObjectPtr<Database::Artist>>
|
||||
Scrobbling::getRecentArtists(Database::Session& session,
|
||||
Database::ObjectPtr<Database::User> user,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::TrackArtistLinkType> linkType,
|
||||
std::optional<Database::Range> range,
|
||||
bool& moreResults)
|
||||
{
|
||||
const Database::ObjectPtr<Database::TrackList> history {getListensTrackList(session, user)};
|
||||
|
||||
std::vector<Database::ObjectPtr<Database::Artist>> res;
|
||||
if (history)
|
||||
res = history->getArtistsReverse(clusterIds, linkType, range, moreResults);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<Database::ObjectPtr<Database::Release>>
|
||||
Scrobbling::getRecentReleases(Database::Session& session,
|
||||
Database::ObjectPtr<Database::User> user,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::Range> range,
|
||||
bool& moreResults)
|
||||
{
|
||||
const Database::ObjectPtr<Database::TrackList> history {getListensTrackList(session, user)};
|
||||
|
||||
std::vector<Database::ObjectPtr<Database::Release>> res;
|
||||
if (history)
|
||||
res = history->getReleasesReverse(clusterIds, range, moreResults);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<Database::ObjectPtr<Database::Track>>
|
||||
Scrobbling::getRecentTracks(Database::Session& session,
|
||||
Database::ObjectPtr<Database::User> user,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::Range> range,
|
||||
bool& moreResults)
|
||||
{
|
||||
const Database::ObjectPtr<Database::TrackList> history {getListensTrackList(session, user)};
|
||||
|
||||
std::vector<Database::ObjectPtr<Database::Track>> res;
|
||||
if (history)
|
||||
res = history->getTracksReverse(clusterIds, range, moreResults);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
// Top
|
||||
std::vector<Database::ObjectPtr<Database::Artist>>
|
||||
Scrobbling::getTopArtists(Database::Session& session,
|
||||
Database::ObjectPtr<Database::User> user,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::TrackArtistLinkType> linkType,
|
||||
std::optional<Database::Range> range,
|
||||
bool& moreResults)
|
||||
{
|
||||
const Database::ObjectPtr<Database::TrackList> history {getListensTrackList(session, user)};
|
||||
|
||||
std::vector<Database::ObjectPtr<Database::Artist>> res;
|
||||
if (history)
|
||||
res = history->getTopArtists(clusterIds, linkType, range, moreResults);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<Database::ObjectPtr<Database::Release>>
|
||||
Scrobbling::getTopReleases(Database::Session& session,
|
||||
Database::ObjectPtr<Database::User> user,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::Range> range,
|
||||
bool& moreResults)
|
||||
{
|
||||
const Database::ObjectPtr<Database::TrackList> history {getListensTrackList(session, user)};
|
||||
|
||||
std::vector<Database::ObjectPtr<Database::Release>> res;
|
||||
if (history)
|
||||
res = history->getTopReleases(clusterIds, range, moreResults);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<Database::ObjectPtr<Database::Track>>
|
||||
Scrobbling::getTopTracks(Database::Session& session,
|
||||
Database::ObjectPtr<Database::User> user,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::Range> range,
|
||||
bool& moreResults)
|
||||
{
|
||||
const Database::ObjectPtr<Database::TrackList> history {getListensTrackList(session, user)};
|
||||
|
||||
std::vector<Database::ObjectPtr<Database::Track>> res;
|
||||
if (history)
|
||||
res = history->getTopTracks(clusterIds, range, moreResults);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
Database::ObjectPtr<Database::TrackList>
|
||||
Scrobbling::getListensTrackList(Database::Session& session, Database::ObjectPtr<Database::User> user)
|
||||
{
|
||||
return _scrobblers[user->getScrobbler()]->getListensTrackList(session, user);
|
||||
}
|
||||
|
||||
} // ns Scrobbling
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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 <memory>
|
||||
#include <optional>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "scrobbling/IScrobbling.hpp"
|
||||
#include "IScrobbler.hpp"
|
||||
|
||||
namespace Scrobbling
|
||||
{
|
||||
class Scrobbling : public IScrobbling
|
||||
{
|
||||
public:
|
||||
Scrobbling(boost::asio::io_context& ioContext, Database::Db& db);
|
||||
|
||||
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;
|
||||
|
||||
std::vector<Database::ObjectPtr<Database::Artist>> getRecentArtists(Database::Session& session,
|
||||
Database::ObjectPtr<Database::User> user,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::TrackArtistLinkType> linkType,
|
||||
std::optional<Database::Range> range,
|
||||
bool& moreResults) override;
|
||||
|
||||
std::vector<Database::ObjectPtr<Database::Release>> getRecentReleases(Database::Session& session,
|
||||
Database::ObjectPtr<Database::User> user,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::Range> range,
|
||||
bool& moreResults) override;
|
||||
|
||||
std::vector<Database::ObjectPtr<Database::Track>> getRecentTracks(Database::Session& session,
|
||||
Database::ObjectPtr<Database::User> user,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::Range> range,
|
||||
bool& moreResults) override;
|
||||
|
||||
std::vector<Database::ObjectPtr<Database::Artist>> getTopArtists(Database::Session& session,
|
||||
Database::ObjectPtr<Database::User> user,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::TrackArtistLinkType> linkType,
|
||||
std::optional<Database::Range> range,
|
||||
bool& moreResults) override;
|
||||
|
||||
std::vector<Database::ObjectPtr<Database::Release>> getTopReleases(Database::Session& session,
|
||||
Database::ObjectPtr<Database::User> user,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::Range> range,
|
||||
bool& moreResults) override;
|
||||
|
||||
std::vector<Database::ObjectPtr<Database::Track>> getTopTracks(Database::Session& session,
|
||||
Database::ObjectPtr<Database::User> user,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::Range> range,
|
||||
bool& moreResults) override;
|
||||
|
||||
Database::ObjectPtr<Database::TrackList> getListensTrackList(Database::Session& session, Database::ObjectPtr<Database::User> user);
|
||||
|
||||
std::optional<Database::Scrobbler> getUserScrobbler(Database::UserId userId);
|
||||
|
||||
Database::Db& _db;
|
||||
std::unordered_map<Database::Scrobbler, std::unique_ptr<IScrobbler>> _scrobblers;
|
||||
};
|
||||
|
||||
} // ns Scrobbling
|
||||
|
||||
@@ -1,239 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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 "ListenBrainzScrobbler.hpp"
|
||||
|
||||
#include <Wt/Json/Array.h>
|
||||
#include <Wt/Json/Object.h>
|
||||
#include <Wt/Json/Value.h>
|
||||
#include <Wt/Json/Serializer.h>
|
||||
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Db.hpp"
|
||||
#include "database/Release.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "database/TrackList.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "utils/IConfig.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/Service.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
#define LOG(sev) LMS_LOG(SCROBBLING, sev) << "[listenbrainz] - "
|
||||
|
||||
namespace
|
||||
{
|
||||
bool
|
||||
canBeScrobbled(Database::Session& session, Database::TrackId trackId, std::chrono::seconds duration)
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
const Database::Track::pointer track {Database::Track::getById(session, trackId)};
|
||||
if (!track)
|
||||
return false;
|
||||
|
||||
const bool res {duration >= std::chrono::minutes(4) || (duration >= track->getDuration() / 2)};
|
||||
if (!res)
|
||||
LOG(DEBUG) << "Track cannot be scrobbled since played duration is too short: " << duration.count() << "s, total duration = " << std::chrono::duration_cast<std::chrono::seconds>(track->getDuration()).count() << "s";
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::optional<Wt::Json::Object>
|
||||
listenToJsonPayload(Database::Session& session, const Scrobbling::Listen& listen, const Wt::WDateTime& timePoint)
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
const Database::Track::pointer track {Database::Track::getById(session, listen.trackId)};
|
||||
if (!track)
|
||||
return std::nullopt;
|
||||
|
||||
auto artists {track->getArtists({Database::TrackArtistLinkType::Artist})};
|
||||
if (artists.empty())
|
||||
artists = track->getArtists({Database::TrackArtistLinkType::ReleaseArtist});
|
||||
|
||||
if (artists.empty())
|
||||
{
|
||||
LOG(DEBUG) << "Track cannot be scrobbled since it does not have any artist";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
Wt::Json::Object additionalInfo;
|
||||
additionalInfo["listening_from"] = "LMS";
|
||||
if (track->getRelease())
|
||||
{
|
||||
if (auto MBID {track->getRelease()->getMBID()})
|
||||
additionalInfo["release_mbid"] = Wt::Json::Value {std::string {MBID->getAsString()}};
|
||||
}
|
||||
|
||||
{
|
||||
Wt::Json::Array artistMBIDs;
|
||||
for (const Database::Artist::pointer& artist : artists)
|
||||
{
|
||||
if (auto MBID {artist->getMBID()})
|
||||
artistMBIDs.push_back(Wt::Json::Value {std::string {MBID->getAsString()}});
|
||||
}
|
||||
|
||||
if (!artistMBIDs.empty())
|
||||
additionalInfo["artist_mbids"] = std::move(artistMBIDs);
|
||||
}
|
||||
|
||||
if (auto MBID {track->getTrackMBID()})
|
||||
additionalInfo["track_mbid"] = Wt::Json::Value {std::string {MBID->getAsString()}};
|
||||
|
||||
if (auto MBID {track->getRecordingMBID()})
|
||||
additionalInfo["recording_mbid"] = Wt::Json::Value {std::string {MBID->getAsString()}};
|
||||
|
||||
if (const std::optional<std::size_t> trackNumber {track->getTrackNumber()})
|
||||
additionalInfo["tracknumber"] = Wt::Json::Value {static_cast<long long int>(*trackNumber)};
|
||||
|
||||
Wt::Json::Object trackMetadata;
|
||||
trackMetadata["additional_info"] = std::move(additionalInfo);
|
||||
trackMetadata["artist_name"] = Wt::Json::Value {artists.front()->getName()};
|
||||
trackMetadata["track_name"] = Wt::Json::Value {track->getName()};
|
||||
if (track->getRelease())
|
||||
trackMetadata["release_name"] = Wt::Json::Value {track->getRelease()->getName()};
|
||||
|
||||
Wt::Json::Object payload;
|
||||
payload["track_metadata"] = std::move(trackMetadata);
|
||||
if (timePoint.isValid())
|
||||
payload["listened_at"] = Wt::Json::Value {static_cast<long long int>(timePoint.toTime_t())};
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
std::string
|
||||
listenToJsonString(Database::Session& session, const Scrobbling::Listen& listen, const Wt::WDateTime& timePoint, std::string_view listenType)
|
||||
{
|
||||
std::string res;
|
||||
|
||||
std::optional<Wt::Json::Object> payload {listenToJsonPayload(session, listen, timePoint)};
|
||||
if (!payload)
|
||||
return res;
|
||||
|
||||
Wt::Json::Object root;
|
||||
root["listen_type"] = Wt::Json::Value {std::string {listenType}};
|
||||
root["payload"] = Wt::Json::Array {std::move(*payload)};
|
||||
|
||||
res = Wt::Json::serialize(root);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
namespace Scrobbling::ListenBrainz
|
||||
{
|
||||
Scrobbler::Scrobbler(boost::asio::io_context& ioContext, Database::Db& db)
|
||||
: _ioContext {ioContext}
|
||||
, _db {db}
|
||||
, _sendQueue {_ioContext, Service<IConfig>::get()->getString("listenbrainz-api-base-url", "https://api.listenbrainz.org")}
|
||||
, _listensSynchronizer {_ioContext, db, _sendQueue}
|
||||
{
|
||||
LOG(INFO) << "Starting ListenBrainz scrobbler... API endpoint = '" << _sendQueue.getAPIBaseURL();
|
||||
}
|
||||
|
||||
Scrobbler::~Scrobbler()
|
||||
{
|
||||
LOG(INFO) << "Stopped ListenBrainz scrobbler!";
|
||||
}
|
||||
|
||||
void
|
||||
Scrobbler::listenStarted(const Listen& listen)
|
||||
{
|
||||
enqueListen(listen, Wt::WDateTime {});
|
||||
}
|
||||
|
||||
void
|
||||
Scrobbler::listenFinished(const Listen& listen, std::optional<std::chrono::seconds> duration)
|
||||
{
|
||||
if (duration && !canBeScrobbled(_db.getTLSSession(), listen.trackId, *duration))
|
||||
return;
|
||||
|
||||
const Listen timedListen {listen};
|
||||
const Wt::WDateTime now {Wt::WDateTime::currentDateTime()};
|
||||
|
||||
enqueListen(timedListen, now);
|
||||
}
|
||||
|
||||
void
|
||||
Scrobbler::addTimedListen(const TimedListen& listen)
|
||||
{
|
||||
assert(listen.listenedAt.isValid());
|
||||
enqueListen(listen, listen.listenedAt);
|
||||
}
|
||||
|
||||
Database::TrackList::pointer
|
||||
Scrobbler::getListensTrackList(Database::Session& session, Database::User::pointer user)
|
||||
{
|
||||
return Utils::getListensTrackList(session, user);
|
||||
}
|
||||
|
||||
void
|
||||
Scrobbler::enqueListen(const Listen& listen, const Wt::WDateTime& timePoint)
|
||||
{
|
||||
std::optional<SendQueue::RequestData> requestData {createSubmitListenRequestData(listen, timePoint)};
|
||||
if (!requestData)
|
||||
return;
|
||||
|
||||
SendQueue::Request submitListen {std::move(*requestData)};
|
||||
if (timePoint.isValid())
|
||||
{
|
||||
submitListen.setPriority(SendQueue::Request::Priority::Normal);
|
||||
submitListen.setOnSuccessFunc([=](std::string_view)
|
||||
{
|
||||
_listensSynchronizer.saveListen(TimedListen {listen, timePoint});
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
// We want "listen now" to appear as soon as possible
|
||||
submitListen.setPriority(SendQueue::Request::Priority::High);
|
||||
}
|
||||
|
||||
_sendQueue.enqueueRequest(std::move(submitListen));
|
||||
}
|
||||
|
||||
std::optional<SendQueue::RequestData>
|
||||
Scrobbler::createSubmitListenRequestData(const Listen& listen, const Wt::WDateTime& timePoint)
|
||||
{
|
||||
Database::Session& session {_db.getTLSSession()};
|
||||
|
||||
const std::optional<UUID> listenBrainzToken {Utils::getListenBrainzToken(session, listen.userId)};
|
||||
if (!listenBrainzToken)
|
||||
return std::nullopt;
|
||||
|
||||
SendQueue::RequestData requestData;
|
||||
requestData.endpoint = "/1/submit-listens";
|
||||
requestData.type = SendQueue::RequestData::Type::POST;
|
||||
|
||||
std::string bodyText {listenToJsonString(session, listen, timePoint, timePoint.isValid() ? "single" : "playing_now")};
|
||||
if (bodyText.empty())
|
||||
{
|
||||
LOG(DEBUG) << "Cannot convert listen to json: skipping";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
requestData.message.addBodyText(bodyText);
|
||||
requestData.message.addHeader("Authorization", "Token " + std::string {listenBrainzToken->getAsString()});
|
||||
requestData.message.addHeader("Content-Type", "application/json");
|
||||
|
||||
return requestData;
|
||||
}
|
||||
} // namespace Scrobbling::ListenBrainz
|
||||
|
||||
@@ -1,524 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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 "ListenBrainzScrobbler.hpp"
|
||||
|
||||
#include <boost/asio/bind_executor.hpp>
|
||||
#include <Wt/Json/Array.h>
|
||||
#include <Wt/Json/Object.h>
|
||||
#include <Wt/Json/Value.h>
|
||||
#include <Wt/Json/Serializer.h>
|
||||
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Db.hpp"
|
||||
#include "database/Release.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "database/TrackList.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "scrobbling/Exception.hpp"
|
||||
#include "utils/IConfig.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/Service.hpp"
|
||||
|
||||
#include "Utils.hpp"
|
||||
|
||||
#define LOG(sev) LMS_LOG(SCROBBLING, sev) << "[listenbrainz Synchronizer] - "
|
||||
|
||||
namespace
|
||||
{
|
||||
using namespace Scrobbling::ListenBrainz;
|
||||
|
||||
SendQueue::RequestData
|
||||
createValidateTokenRequestData(std::string_view authToken)
|
||||
{
|
||||
SendQueue::RequestData requestData;
|
||||
requestData.type = SendQueue::RequestData::Type::GET;
|
||||
requestData.endpoint = "/1/validate-token";
|
||||
requestData.headers = { {"Authorization", "Token " + std::string {authToken}} };
|
||||
|
||||
return requestData;
|
||||
}
|
||||
|
||||
std::string
|
||||
parseValidateToken(std::string_view msgBody)
|
||||
{
|
||||
std::string listenBrainzUserName;
|
||||
|
||||
Wt::Json::ParseError error;
|
||||
Wt::Json::Object root;
|
||||
if (!Wt::Json::parse(std::string {msgBody}, root, error))
|
||||
{
|
||||
LOG(ERROR) << "Cannot parse 'validate-token' result: " << error.what();
|
||||
return listenBrainzUserName;
|
||||
}
|
||||
|
||||
if (!root.get("valid").orIfNull(false))
|
||||
{
|
||||
LOG(INFO) << "Invalid listenbrainz user";
|
||||
return listenBrainzUserName;
|
||||
}
|
||||
|
||||
listenBrainzUserName = root.get("user_name").orIfNull("");
|
||||
return listenBrainzUserName;
|
||||
}
|
||||
|
||||
SendQueue::RequestData
|
||||
createListenCountRequestData(std::string_view listenBrainzUserName)
|
||||
{
|
||||
LOG(DEBUG) << "Getting listen count for listenbrainz user '" << listenBrainzUserName << "'";
|
||||
|
||||
SendQueue::RequestData requestData;
|
||||
requestData.type = SendQueue::RequestData::Type::GET;
|
||||
requestData.endpoint = "/1/user/" + std::string {listenBrainzUserName} + "/listen-count";
|
||||
|
||||
return requestData;
|
||||
}
|
||||
|
||||
std::optional<std::size_t>
|
||||
parseListenCount(std::string_view msgBody)
|
||||
{
|
||||
try
|
||||
{
|
||||
Wt::Json::Object root;
|
||||
Wt::Json::parse(std::string {msgBody}, root);
|
||||
|
||||
const Wt::Json::Object& payload {static_cast<const Wt::Json::Object&>(root.get("payload"))};
|
||||
return static_cast<int>(payload.get("count"));
|
||||
}
|
||||
catch (const Wt::WException& e)
|
||||
{
|
||||
LOG(ERROR) << "Cannot parse listen count response: " << e.what();
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
SendQueue::RequestData
|
||||
createGetListensRequestData(std::string_view listenBrainzUserName, const Wt::WDateTime& maxDateTime)
|
||||
{
|
||||
LOG(DEBUG) << "Getting listens for listenbrainz user '" << listenBrainzUserName << "' with max_ts = " << maxDateTime.toString();
|
||||
|
||||
SendQueue::RequestData requestData;
|
||||
requestData.type = SendQueue::RequestData::Type::GET;
|
||||
requestData.endpoint = "/1/user/" + std::string {listenBrainzUserName} + "/listens?max_ts=" + std::to_string(maxDateTime.toTime_t());
|
||||
|
||||
return requestData;
|
||||
}
|
||||
|
||||
Database::Track::pointer
|
||||
tryMatchListen(Database::Session& session, const Wt::Json::Object& metadata)
|
||||
{
|
||||
Database::Track::pointer track;
|
||||
|
||||
// first try to get the associated track using MBIDs, and then fallback on names
|
||||
if (metadata.type("additional_info") == Wt::Json::Type::Object)
|
||||
{
|
||||
const Wt::Json::Object& additionalInfo = metadata.get("additional_info");
|
||||
if (std::optional<UUID> recordingMBID {UUID::fromString(additionalInfo.get("recording_mbid").orIfNull(""))})
|
||||
{
|
||||
const auto tracks {Database::Track::getByRecordingMBID(session, *recordingMBID)};
|
||||
// if duplicated files, do not record it (let the user correct its database)
|
||||
if (tracks.size() == 1)
|
||||
track = tracks.front();
|
||||
}
|
||||
}
|
||||
|
||||
if (track)
|
||||
return track;
|
||||
|
||||
// these fields are mandatory
|
||||
const std::string trackName {static_cast<std::string>(metadata.get("track_name"))};
|
||||
const std::string releaseName {static_cast<std::string>(metadata.get("release_name"))};
|
||||
|
||||
auto tracks {Database::Track::getByNameAndReleaseName(session, trackName, releaseName)};
|
||||
if (tracks.size() > 1)
|
||||
{
|
||||
tracks.erase(std::remove_if(std::begin(tracks), std::end(tracks),
|
||||
[&](const Database::Track::pointer track)
|
||||
{
|
||||
if (std::string artistName {metadata.get("artist_name").orIfNull("")}; !artistName.empty())
|
||||
{
|
||||
const auto& artists {track->getArtists({Database::TrackArtistLinkType::Artist})};
|
||||
if (std::none_of(std::begin(artists), std::end(artists), [&](const Database::Artist::pointer& artist) { return artist->getName() == artistName; }))
|
||||
return true;
|
||||
}
|
||||
if (metadata.type("additional_info") == Wt::Json::Type::Object)
|
||||
{
|
||||
const Wt::Json::Object& additionalInfo = metadata.get("additional_info");
|
||||
if (track->getTrackNumber())
|
||||
{
|
||||
int otherTrackNumber {additionalInfo.get("tracknumber").orIfNull(-1)};
|
||||
if (otherTrackNumber > 0 && static_cast<std::size_t>(otherTrackNumber) != *track->getTrackNumber())
|
||||
return true;
|
||||
}
|
||||
|
||||
if (auto releaseMBID {track->getRelease()->getMBID()})
|
||||
{
|
||||
if (std::optional<UUID> otherReleaseMBID {UUID::fromString(additionalInfo.get("release_mbid").orIfNull(""))})
|
||||
{
|
||||
if (otherReleaseMBID->getAsString() != releaseMBID->getAsString())
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}), std::end(tracks));
|
||||
}
|
||||
|
||||
if (tracks.size() == 1)
|
||||
track = tracks.front();
|
||||
|
||||
return track;
|
||||
}
|
||||
|
||||
struct ParseGetListensResult
|
||||
{
|
||||
Wt::WDateTime oldestEntry;
|
||||
std::size_t listenCount{};
|
||||
std::vector<Scrobbling::TimedListen> matchedListens;
|
||||
};
|
||||
ParseGetListensResult
|
||||
parseGetListens(Database::Session& session, std::string_view msgBody, Database::UserId userId)
|
||||
{
|
||||
ParseGetListensResult result;
|
||||
|
||||
try
|
||||
{
|
||||
Wt::Json::Object root;
|
||||
Wt::Json::parse(std::string {msgBody}, root);
|
||||
|
||||
const Wt::Json::Object& payload = root.get("payload");
|
||||
const Wt::Json::Array& listens = payload.get("listens");
|
||||
|
||||
LOG(DEBUG) << "Got " << listens.size() << " listens";
|
||||
|
||||
if (listens.empty())
|
||||
return result;
|
||||
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
for (const Wt::Json::Value& value : listens)
|
||||
{
|
||||
const Wt::Json::Object& listen = value;
|
||||
const Wt::WDateTime listenedAt {Wt::WDateTime::fromTime_t(static_cast<int>(listen.get("listened_at")))};
|
||||
const Wt::Json::Object& metadata = listen.get("track_metadata");
|
||||
|
||||
if (!listenedAt.isValid())
|
||||
{
|
||||
LOG(ERROR) << "bad listened_at field!";
|
||||
continue;
|
||||
}
|
||||
|
||||
result.listenCount++;
|
||||
if (!result.oldestEntry.isValid())
|
||||
result.oldestEntry = listenedAt;
|
||||
else if (listenedAt < result.oldestEntry)
|
||||
result.oldestEntry = listenedAt;
|
||||
|
||||
if (const Database::Track::pointer track {tryMatchListen(session, metadata)})
|
||||
result.matchedListens.emplace_back(Scrobbling::TimedListen {{userId, track->getId()}, listenedAt});
|
||||
}
|
||||
}
|
||||
catch (const Wt::WException& error)
|
||||
{
|
||||
LOG(ERROR) << "Cannot parse 'get-listens' result: " << error.what();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
namespace Scrobbling::ListenBrainz
|
||||
{
|
||||
ListensSynchronizer::ListensSynchronizer(boost::asio::io_context& ioContext, Database::Db& db, SendQueue& sendQueue)
|
||||
: _ioContext {ioContext}
|
||||
, _db {db}
|
||||
, _sendQueue {sendQueue}
|
||||
, _maxSyncListenCount {Service<IConfig>::get()->getULong("listenbrainz-max-sync-listen-count", 1000)}
|
||||
, _syncListensPeriod {Service<IConfig>::get()->getULong("listenbrainz-sync-listens-period-hours", 1)}
|
||||
{
|
||||
LOG(INFO) << "Starting Listens synchronizer, maxSyncListenCount = " << _maxSyncListenCount << ", _syncListensPeriod = " << _syncListensPeriod.count() << " hours";
|
||||
|
||||
scheduleGetListens(std::chrono::seconds {30});
|
||||
}
|
||||
|
||||
void
|
||||
ListensSynchronizer::saveListen(const TimedListen& listen)
|
||||
{
|
||||
_strand.dispatch([=]
|
||||
{
|
||||
Database::Session& session {_db.getTLSSession()};
|
||||
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
const Database::User::pointer user {Database::User::getById(session, listen.userId)};
|
||||
if (!user)
|
||||
return;
|
||||
|
||||
const Database::Track::pointer track {Database::Track::getById(session, listen.trackId)};
|
||||
if (!track)
|
||||
return;
|
||||
|
||||
Database::TrackListEntry::create(session, track, Utils::getOrCreateListensTrackList(session, user), listen.listenedAt);
|
||||
|
||||
UserContext& context {getUserContext(listen.userId)};
|
||||
if (context.listenCount)
|
||||
(*context.listenCount)++;
|
||||
});
|
||||
}
|
||||
|
||||
ListensSynchronizer::UserContext&
|
||||
ListensSynchronizer::getUserContext(Database::UserId userId)
|
||||
{
|
||||
auto itContext {_userContexts.find(userId)};
|
||||
if (itContext == std::cend(_userContexts))
|
||||
{
|
||||
auto [itNewContext, inserted] {_userContexts.emplace(userId, userId)};
|
||||
itContext = itNewContext;
|
||||
}
|
||||
|
||||
return itContext->second;
|
||||
}
|
||||
|
||||
bool
|
||||
ListensSynchronizer::isFetching() const
|
||||
{
|
||||
return std::any_of(std::cbegin(_userContexts), std::cend(_userContexts), [](const auto& contextEntry)
|
||||
{
|
||||
const auto& [userId, context] {contextEntry};
|
||||
return context.fetching;
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
ListensSynchronizer::scheduleGetListens(std::chrono::seconds fromNow)
|
||||
{
|
||||
if (_syncListensPeriod.count() == 0 || _maxSyncListenCount == 0)
|
||||
return;
|
||||
|
||||
LOG(DEBUG) << "Scheduled sync in " << fromNow.count() << " seconds...";
|
||||
_getListensTimer.expires_after(fromNow);
|
||||
_getListensTimer.async_wait(boost::asio::bind_executor(_strand, [this] (const boost::system::error_code& ec)
|
||||
{
|
||||
if (ec == boost::asio::error::operation_aborted)
|
||||
{
|
||||
LOG(DEBUG) << "getListens aborted";
|
||||
return;
|
||||
}
|
||||
else if (ec)
|
||||
{
|
||||
throw Exception {"GetListens timer failure: " + std::string {ec.message()} };
|
||||
}
|
||||
|
||||
startGetListens();
|
||||
}));
|
||||
}
|
||||
|
||||
void
|
||||
ListensSynchronizer::startGetListens()
|
||||
{
|
||||
LOG(DEBUG) << "GetListens started!!!";
|
||||
|
||||
assert(!isFetching());
|
||||
|
||||
std::vector<Database::UserId> userIds;
|
||||
{
|
||||
Database::Session& session {_db.getTLSSession()};
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
userIds = Database::User::getAllIds(_db.getTLSSession());
|
||||
}
|
||||
|
||||
for (const Database::UserId userId : userIds)
|
||||
{
|
||||
if (Utils::getListenBrainzToken(_db.getTLSSession(), userId))
|
||||
startGetListens(getUserContext(userId));
|
||||
}
|
||||
|
||||
if (!isFetching())
|
||||
scheduleGetListens(_syncListensPeriod);
|
||||
}
|
||||
|
||||
void
|
||||
ListensSynchronizer::startGetListens(UserContext& context)
|
||||
{
|
||||
context.fetching = true;
|
||||
context.listenBrainzUserName = "";
|
||||
context.maxDateTime = {};
|
||||
context.fetchedListenCount = 0;
|
||||
context.matchedListenCount = 0;
|
||||
context.importedListenCount = 0;
|
||||
|
||||
enqueValidateToken(context);
|
||||
}
|
||||
|
||||
void
|
||||
ListensSynchronizer::onGetListensEnded(UserContext& context)
|
||||
{
|
||||
_strand.dispatch([this, &context]
|
||||
{
|
||||
LOG(DEBUG) << "Fetch done for user " << context.userId.getValue() << ", fetched: " << context.fetchedListenCount << ", matched: " << context.matchedListenCount << ", imported: " << context.importedListenCount;
|
||||
context.fetching = false;
|
||||
|
||||
if (!isFetching())
|
||||
scheduleGetListens(_syncListensPeriod);
|
||||
});
|
||||
}
|
||||
|
||||
void
|
||||
ListensSynchronizer::enqueValidateToken(UserContext& context)
|
||||
{
|
||||
assert(context.listenBrainzUserName.empty());
|
||||
|
||||
std::optional<SendQueue::RequestData> requestData {createValidateTokenRequestData(context.userId)};
|
||||
if (!requestData)
|
||||
{
|
||||
onGetListensEnded(context);
|
||||
return;
|
||||
}
|
||||
|
||||
SendQueue::Request validateTokenRequest {std::move(*requestData)};
|
||||
validateTokenRequest.setOnSuccessFunc([this, &context] (std::string_view msgBody)
|
||||
{
|
||||
context.listenBrainzUserName = parseValidateToken(msgBody);
|
||||
if (context.listenBrainzUserName.empty())
|
||||
{
|
||||
onGetListensEnded(context);
|
||||
return;
|
||||
}
|
||||
enqueGetListenCount(context);
|
||||
});
|
||||
validateTokenRequest.setOnFailureFunc([this, &context]
|
||||
{
|
||||
onGetListensEnded(context);
|
||||
});
|
||||
|
||||
validateTokenRequest.setPriority(SendQueue::Request::Priority::Low);
|
||||
_sendQueue.enqueueRequest(std::move(validateTokenRequest));
|
||||
}
|
||||
|
||||
void
|
||||
ListensSynchronizer::enqueGetListenCount(UserContext& context)
|
||||
{
|
||||
assert(!context.listenBrainzUserName.empty());
|
||||
|
||||
SendQueue::Request getListenCountRequest {createListenCountRequestData(context.listenBrainzUserName)};
|
||||
getListenCountRequest.setOnSuccessFunc([=, &context] (std::string_view msgBody)
|
||||
{
|
||||
const auto listenCount = parseListenCount(msgBody);
|
||||
if (listenCount)
|
||||
LOG(DEBUG) << "Listen count for listenbrainz user '" << context.listenBrainzUserName << "' = " << *listenCount;
|
||||
|
||||
bool needSync {listenCount && (!context.listenCount || *context.listenCount != *listenCount)};
|
||||
context.listenCount = listenCount;
|
||||
|
||||
if (!needSync)
|
||||
{
|
||||
onGetListensEnded(context);
|
||||
return;
|
||||
}
|
||||
|
||||
context.maxDateTime = Wt::WDateTime::currentDateTime();
|
||||
enqueGetListens(context);
|
||||
});
|
||||
getListenCountRequest.setOnFailureFunc([this, &context]
|
||||
{
|
||||
onGetListensEnded(context);
|
||||
});
|
||||
|
||||
getListenCountRequest.setPriority(SendQueue::Request::Priority::Low);
|
||||
_sendQueue.enqueueRequest(std::move(getListenCountRequest));
|
||||
}
|
||||
|
||||
void
|
||||
ListensSynchronizer::enqueGetListens(UserContext& context)
|
||||
{
|
||||
assert(!context.listenBrainzUserName.empty());
|
||||
|
||||
SendQueue::Request getListensRequest {::createGetListensRequestData(context.listenBrainzUserName, context.maxDateTime)};
|
||||
getListensRequest.setOnSuccessFunc([=, &context] (std::string_view msgBody)
|
||||
{
|
||||
processGetListensResponse(msgBody, context);
|
||||
if (context.fetchedListenCount >= _maxSyncListenCount || !context.maxDateTime.isValid())
|
||||
{
|
||||
onGetListensEnded(context);
|
||||
return;
|
||||
}
|
||||
|
||||
enqueGetListens(context);
|
||||
});
|
||||
getListensRequest.setOnFailureFunc([=, &context]
|
||||
{
|
||||
onGetListensEnded(context);
|
||||
});
|
||||
|
||||
getListensRequest.setPriority(SendQueue::Request::Priority::Low);
|
||||
_sendQueue.enqueueRequest(std::move(getListensRequest));
|
||||
}
|
||||
|
||||
std::optional<SendQueue::RequestData>
|
||||
ListensSynchronizer::createValidateTokenRequestData(Database::UserId userId)
|
||||
{
|
||||
Database::Session& session {_db.getTLSSession()};
|
||||
|
||||
const std::optional<UUID> listenBrainzToken {Utils::getListenBrainzToken(session, userId)};
|
||||
if (!listenBrainzToken)
|
||||
return std::nullopt;
|
||||
|
||||
return ::createValidateTokenRequestData(listenBrainzToken->getAsString());
|
||||
}
|
||||
|
||||
void
|
||||
ListensSynchronizer::processGetListensResponse(std::string_view msgBody, UserContext& context)
|
||||
{
|
||||
Database::Session& session {_db.getTLSSession()};
|
||||
|
||||
const ParseGetListensResult parseResult {parseGetListens(session, msgBody, context.userId)};
|
||||
|
||||
context.fetchedListenCount += parseResult.listenCount;
|
||||
context.matchedListenCount += parseResult.matchedListens.size();
|
||||
context.maxDateTime = parseResult.oldestEntry;
|
||||
|
||||
if (parseResult.matchedListens.empty())
|
||||
return;
|
||||
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
Database::User::pointer user {Database::User::getById(session, context.userId)};
|
||||
if (!user)
|
||||
return;
|
||||
|
||||
Database::TrackList::pointer tracklist {Utils::getOrCreateListensTrackList(session, user)};
|
||||
|
||||
for (const TimedListen& listen : parseResult.matchedListens)
|
||||
{
|
||||
const Database::Track::pointer track {Database::Track::getById(session, listen.trackId)};
|
||||
if (!track)
|
||||
continue;
|
||||
|
||||
if (!tracklist->getEntryByTrackAndDateTime(track, listen.listenedAt))
|
||||
{
|
||||
context.importedListenCount++;
|
||||
Database::TrackListEntry::create(session, track, tracklist, listen.listenedAt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Scrobbling::ListenBrainz
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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 <string_view>
|
||||
|
||||
#include "database/Session.hpp"
|
||||
#include "database/TrackList.hpp"
|
||||
#include "database/User.hpp"
|
||||
|
||||
static constexpr std::string_view historyTracklistName {"__scrobbler_listenbrainz_history__"};
|
||||
|
||||
namespace Scrobbling::ListenBrainz::Utils
|
||||
{
|
||||
std::optional<UUID>
|
||||
getListenBrainzToken(Database::Session& session, Database::UserId userId)
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
const Database::User::pointer user {Database::User::getById(session, userId)};
|
||||
if (!user)
|
||||
return std::nullopt;
|
||||
|
||||
if (user->getScrobbler() != Database::Scrobbler::ListenBrainz)
|
||||
return std::nullopt;
|
||||
|
||||
return user->getListenBrainzToken();
|
||||
}
|
||||
|
||||
Database::TrackList::pointer
|
||||
getListensTrackList(Database::Session& session, Database::User::pointer user)
|
||||
{
|
||||
return Database::TrackList::get(session, historyTracklistName, Database::TrackList::Type::Internal, user);
|
||||
}
|
||||
|
||||
Database::TrackList::pointer
|
||||
getOrCreateListensTrackList(Database::Session& session, Database::User::pointer user)
|
||||
{
|
||||
Database::TrackList::pointer tracklist {getListensTrackList(session, user)};
|
||||
if (!tracklist)
|
||||
tracklist = Database::TrackList::create(session, historyTracklistName, Database::TrackList::Type::Internal, false, user);
|
||||
|
||||
return tracklist;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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 <Wt/Dbo/ptr.h>
|
||||
#include "utils/UUID.hpp"
|
||||
|
||||
#include "database/Types.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Session;
|
||||
class TrackList;
|
||||
class User;
|
||||
}
|
||||
|
||||
namespace Scrobbling::ListenBrainz::Utils
|
||||
{
|
||||
std::optional<UUID> getListenBrainzToken(Database::Session& session, Database::UserId userId);
|
||||
Database::ObjectPtr<Database::TrackList> getOrCreateListensTrackList(Database::Session& session, Database::ObjectPtr<Database::User> user);
|
||||
Database::ObjectPtr<Database::TrackList> getListensTrackList(Database::Session& session, Database::ObjectPtr<Database::User> user);
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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 <boost/asio/io_service.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include <Wt/WDateTime.h>
|
||||
|
||||
#include "scrobbling/Listen.hpp"
|
||||
#include "database/Types.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Artist;
|
||||
class Db;
|
||||
class Release;
|
||||
class Session;
|
||||
class Track;
|
||||
class User;
|
||||
}
|
||||
|
||||
namespace Scrobbling
|
||||
{
|
||||
|
||||
class IScrobbling
|
||||
{
|
||||
public:
|
||||
virtual ~IScrobbling() = default;
|
||||
|
||||
// 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;
|
||||
|
||||
// Stats
|
||||
// From most recent to oldest
|
||||
virtual std::vector<Database::ObjectPtr<Database::Artist>> getRecentArtists(Database::Session& session,
|
||||
Database::ObjectPtr<Database::User> user,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::TrackArtistLinkType> linkType,
|
||||
std::optional<Database::Range> range,
|
||||
bool& moreResults) = 0;
|
||||
|
||||
virtual std::vector<Database::ObjectPtr<Database::Release>> getRecentReleases(Database::Session& session,
|
||||
Database::ObjectPtr<Database::User> user,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::Range> range,
|
||||
bool& moreResults) = 0;
|
||||
|
||||
virtual std::vector<Database::ObjectPtr<Database::Track>> getRecentTracks(Database::Session& session,
|
||||
Database::ObjectPtr<Database::User> user,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::Range> range,
|
||||
bool& moreResults) = 0;
|
||||
|
||||
// Top
|
||||
virtual std::vector<Database::ObjectPtr<Database::Artist>> getTopArtists(Database::Session& session,
|
||||
Database::ObjectPtr<Database::User> user,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::TrackArtistLinkType> linkType,
|
||||
std::optional<Database::Range> range,
|
||||
bool& moreResults) = 0;
|
||||
|
||||
virtual std::vector<Database::ObjectPtr<Database::Release>> getTopReleases(Database::Session& session,
|
||||
Database::ObjectPtr<Database::User> user,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::Range> range,
|
||||
bool& moreResults) = 0;
|
||||
|
||||
virtual std::vector<Database::ObjectPtr<Database::Track>> getTopTracks(Database::Session& session,
|
||||
Database::ObjectPtr<Database::User> user,
|
||||
const std::vector<Database::ClusterId>& clusterIds,
|
||||
std::optional<Database::Range> range,
|
||||
bool& moreResults) = 0;
|
||||
};
|
||||
|
||||
std::unique_ptr<IScrobbling> createScrobbling(boost::asio::io_service& ioService, Database::Db& db);
|
||||
|
||||
} // ns Scrobbling
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
add_subdirectory(auth)
|
||||
add_subdirectory(cover)
|
||||
add_subdirectory(database)
|
||||
add_subdirectory(recommendation)
|
||||
add_subdirectory(scanner)
|
||||
add_subdirectory(scrobbling)
|
||||
+26
-10
@@ -19,25 +19,33 @@
|
||||
|
||||
#include "AuthServiceBase.hpp"
|
||||
|
||||
#include "database/Session.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "services/database/Db.hpp"
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/User.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
namespace Auth
|
||||
{
|
||||
Database::UserId
|
||||
AuthServiceBase::getOrCreateUser(Database::Session& session, std::string_view loginName)
|
||||
using namespace Database;
|
||||
|
||||
AuthServiceBase::AuthServiceBase(Db& db)
|
||||
: _db {db}
|
||||
{}
|
||||
|
||||
UserId
|
||||
AuthServiceBase::getOrCreateUser(std::string_view loginName)
|
||||
{
|
||||
Session& session {getDbSession()};
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
Database::User::pointer user {Database::User::getByLoginName(session, loginName)};
|
||||
User::pointer user {User::find(session, loginName)};
|
||||
if (!user)
|
||||
{
|
||||
const Database::UserType type {Database::User::getCount(session) == 0 ? Database::UserType::ADMIN : Database::UserType::REGULAR};
|
||||
const UserType type {User::getCount(session) == 0 ? UserType::ADMIN : UserType::REGULAR};
|
||||
|
||||
LMS_LOG(AUTH, DEBUG) << "Creating user '" << loginName << "', admin = " << (type == Database::UserType::ADMIN);
|
||||
LMS_LOG(AUTH, DEBUG) << "Creating user '" << loginName << "', admin = " << (type == UserType::ADMIN);
|
||||
|
||||
user = Database::User::create(session, loginName);
|
||||
user = User::create(session, loginName);
|
||||
user.modify()->setType(type);
|
||||
}
|
||||
|
||||
@@ -45,11 +53,19 @@ namespace Auth
|
||||
}
|
||||
|
||||
void
|
||||
AuthServiceBase::onUserAuthenticated(Database::Session& session, Database::UserId userId)
|
||||
AuthServiceBase::onUserAuthenticated(UserId userId)
|
||||
{
|
||||
Session& session {getDbSession()};
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
Database::User::pointer user {Database::User::getById(session, userId)};
|
||||
|
||||
User::pointer user {User::find(session, userId)};
|
||||
if (user)
|
||||
user.modify()->setLastLogin(Wt::WDateTime::currentDateTime());
|
||||
}
|
||||
|
||||
Session&
|
||||
AuthServiceBase::getDbSession()
|
||||
{
|
||||
return _db.getTLSSession();
|
||||
}
|
||||
}
|
||||
+11
-3
@@ -20,10 +20,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
#include "database/Types.hpp"
|
||||
#include "services/database/UserId.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Db;
|
||||
class Session;
|
||||
}
|
||||
|
||||
@@ -32,7 +33,14 @@ namespace Auth
|
||||
class AuthServiceBase
|
||||
{
|
||||
protected:
|
||||
Database::UserId getOrCreateUser(Database::Session& session, std::string_view loginName);
|
||||
void onUserAuthenticated(Database::Session& session, Database::UserId userId);
|
||||
AuthServiceBase(Database::Db& db);
|
||||
|
||||
Database::UserId getOrCreateUser(std::string_view loginName);
|
||||
void onUserAuthenticated(Database::UserId userId);
|
||||
|
||||
Database::Session& getDbSession();
|
||||
|
||||
private:
|
||||
Database::Db& _db;
|
||||
};
|
||||
}
|
||||
+23
-17
@@ -23,36 +23,40 @@
|
||||
#include <Wt/Auth/PasswordStrengthValidator.h>
|
||||
#include <Wt/WRandom.h>
|
||||
|
||||
#include "auth/Types.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "services/auth/Types.hpp"
|
||||
#include "services/database/AuthToken.hpp"
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/User.hpp"
|
||||
#include "utils/Exception.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
namespace Auth
|
||||
{
|
||||
|
||||
std::unique_ptr<IAuthTokenService> createAuthTokenService(std::size_t maxThrottlerEntries)
|
||||
std::unique_ptr<IAuthTokenService> createAuthTokenService(Database::Db& db, std::size_t maxThrottlerEntries)
|
||||
{
|
||||
return std::make_unique<AuthTokenService>(maxThrottlerEntries);
|
||||
return std::make_unique<AuthTokenService>(db, maxThrottlerEntries);
|
||||
}
|
||||
|
||||
static const Wt::Auth::SHA1HashFunction sha1Function;
|
||||
|
||||
AuthTokenService::AuthTokenService(std::size_t maxThrottlerEntries)
|
||||
: _loginThrottler {maxThrottlerEntries}
|
||||
AuthTokenService::AuthTokenService(Database::Db& db, std::size_t maxThrottlerEntries)
|
||||
: AuthServiceBase {db}
|
||||
, _loginThrottler {maxThrottlerEntries}
|
||||
{
|
||||
}
|
||||
|
||||
std::string
|
||||
AuthTokenService::createAuthToken(Database::Session& session, Database::UserId userId, const Wt::WDateTime& expiry)
|
||||
AuthTokenService::createAuthToken(Database::UserId userId, const Wt::WDateTime& expiry)
|
||||
{
|
||||
const std::string secret {Wt::WRandom::generateId(32)};
|
||||
const std::string secretHash {sha1Function.compute(secret, {})};
|
||||
|
||||
Database::Session& session {getDbSession()};
|
||||
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
Database::User::pointer user {Database::User::getById(session, userId)};
|
||||
Database::User::pointer user {Database::User::find(session, userId)};
|
||||
if (!user)
|
||||
throw Exception {"User deleted"};
|
||||
|
||||
@@ -66,15 +70,15 @@ namespace Auth
|
||||
return secret;
|
||||
}
|
||||
|
||||
static
|
||||
std::optional<AuthTokenService::AuthTokenProcessResult::AuthTokenInfo>
|
||||
processAuthToken(Database::Session& session, std::string_view secret)
|
||||
AuthTokenService::processAuthToken(std::string_view secret)
|
||||
{
|
||||
const std::string secretHash {sha1Function.compute(std::string {secret}, {})};
|
||||
|
||||
Database::Session& session {getDbSession()};
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
Database::AuthToken::pointer authToken {Database::AuthToken::getByValue(session, secretHash)};
|
||||
Database::AuthToken::pointer authToken {Database::AuthToken::find(session, secretHash)};
|
||||
if (!authToken)
|
||||
return std::nullopt;
|
||||
|
||||
@@ -93,7 +97,7 @@ namespace Auth
|
||||
}
|
||||
|
||||
AuthTokenService::AuthTokenProcessResult
|
||||
AuthTokenService::processAuthToken(Database::Session& session, const boost::asio::ip::address& clientAddress, std::string_view tokenValue)
|
||||
AuthTokenService::processAuthToken(const boost::asio::ip::address& clientAddress, std::string_view tokenValue)
|
||||
{
|
||||
// Do not waste too much resource on brute force attacks (optim)
|
||||
{
|
||||
@@ -103,7 +107,7 @@ namespace Auth
|
||||
return AuthTokenProcessResult {AuthTokenProcessResult::State::Throttled};
|
||||
}
|
||||
|
||||
auto res {Auth::processAuthToken(session, tokenValue)};
|
||||
auto res {processAuthToken(tokenValue)};
|
||||
{
|
||||
std::unique_lock lock {_mutex};
|
||||
|
||||
@@ -117,17 +121,19 @@ namespace Auth
|
||||
}
|
||||
|
||||
_loginThrottler.onGoodClientAttempt(clientAddress);
|
||||
onUserAuthenticated(session, res->userId);
|
||||
onUserAuthenticated(res->userId);
|
||||
return AuthTokenProcessResult {AuthTokenProcessResult::State::Granted, std::move(*res)};
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
AuthTokenService::clearAuthTokens(Database::Session& session, Database::UserId userId)
|
||||
AuthTokenService::clearAuthTokens(Database::UserId userId)
|
||||
{
|
||||
Database::Session& session {getDbSession()};
|
||||
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
Database::User::pointer user {Database::User::getById(session, userId)};
|
||||
Database::User::pointer user {Database::User::find(session, userId)};
|
||||
if (!user)
|
||||
throw Exception {"User deleted"};
|
||||
|
||||
+7
-5
@@ -21,7 +21,7 @@
|
||||
|
||||
#include <shared_mutex>
|
||||
|
||||
#include "auth/IAuthTokenService.hpp"
|
||||
#include "services/auth/IAuthTokenService.hpp"
|
||||
#include "AuthServiceBase.hpp"
|
||||
#include "LoginThrottler.hpp"
|
||||
|
||||
@@ -35,7 +35,7 @@ namespace Auth
|
||||
class AuthTokenService : public IAuthTokenService, public AuthServiceBase
|
||||
{
|
||||
public:
|
||||
AuthTokenService(std::size_t maxThrottlerEntries);
|
||||
AuthTokenService(Database::Db& db, std::size_t maxThrottlerEntries);
|
||||
|
||||
AuthTokenService(const AuthTokenService&) = delete;
|
||||
AuthTokenService& operator=(const AuthTokenService&) = delete;
|
||||
@@ -43,9 +43,11 @@ namespace Auth
|
||||
AuthTokenService& operator=(AuthTokenService&&) = delete;
|
||||
|
||||
private:
|
||||
AuthTokenProcessResult processAuthToken(Database::Session& session, const boost::asio::ip::address& clientAddress, std::string_view tokenValue) override;
|
||||
std::string createAuthToken(Database::Session& session, Database::UserId userId, const Wt::WDateTime& expiry) override;
|
||||
void clearAuthTokens(Database::Session& session, Database::UserId userId) override;
|
||||
AuthTokenProcessResult processAuthToken(const boost::asio::ip::address& clientAddress, std::string_view tokenValue) override;
|
||||
std::string createAuthToken(Database::UserId userId, const Wt::WDateTime& expiry) override;
|
||||
void clearAuthTokens(Database::UserId userId) override;
|
||||
|
||||
std::optional<AuthTokenService::AuthTokenProcessResult::AuthTokenInfo> processAuthToken(std::string_view secret);
|
||||
|
||||
std::shared_mutex _mutex;
|
||||
LoginThrottler _loginThrottler;
|
||||
@@ -17,18 +17,18 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "auth/IEnvService.hpp"
|
||||
#include "services/auth/IEnvService.hpp"
|
||||
|
||||
#include "auth/Types.hpp"
|
||||
#include "services/auth/Types.hpp"
|
||||
#include "http-headers/HttpHeadersEnvService.hpp"
|
||||
|
||||
namespace Auth
|
||||
{
|
||||
std::unique_ptr<IEnvService>
|
||||
createEnvService(std::string_view backendName)
|
||||
createEnvService(std::string_view backendName, Database::Db& db)
|
||||
{
|
||||
if (backendName == "http-headers")
|
||||
return std::make_unique<HttpHeadersEnvService>();
|
||||
return std::make_unique<HttpHeadersEnvService>(db);
|
||||
|
||||
throw Exception {"Authentication backend '" + std::string {backendName} + "' is not supported!"};
|
||||
}
|
||||
+14
-17
@@ -28,27 +28,24 @@
|
||||
#include "utils/NetAddress.hpp"
|
||||
#include "utils/Exception.hpp"
|
||||
|
||||
namespace Auth {
|
||||
|
||||
class LoginThrottler
|
||||
namespace Auth
|
||||
{
|
||||
public:
|
||||
LoginThrottler(std::size_t maxEntries) : _maxEntries {maxEntries} {}
|
||||
class LoginThrottler
|
||||
{
|
||||
public:
|
||||
LoginThrottler(std::size_t maxEntries) : _maxEntries {maxEntries} {}
|
||||
|
||||
// user must lock these calls to avoid races
|
||||
bool isClientThrottled(const boost::asio::ip::address& address) const;
|
||||
void onBadClientAttempt(const boost::asio::ip::address& address);
|
||||
void onGoodClientAttempt(const boost::asio::ip::address& address);
|
||||
// user must lock these calls to avoid races
|
||||
bool isClientThrottled(const boost::asio::ip::address& address) const;
|
||||
void onBadClientAttempt(const boost::asio::ip::address& address);
|
||||
void onGoodClientAttempt(const boost::asio::ip::address& address);
|
||||
|
||||
private:
|
||||
|
||||
void removeOutdatedEntries();
|
||||
|
||||
const std::size_t _maxEntries;
|
||||
|
||||
std::unordered_map<boost::asio::ip::address, Wt::WDateTime> _attemptsInfo;
|
||||
};
|
||||
private:
|
||||
void removeOutdatedEntries();
|
||||
|
||||
const std::size_t _maxEntries;
|
||||
|
||||
std::unordered_map<boost::asio::ip::address, Wt::WDateTime> _attemptsInfo;
|
||||
};
|
||||
} // Auth
|
||||
|
||||
+13
-15
@@ -27,9 +27,9 @@
|
||||
#include "pam/PAMPasswordService.hpp"
|
||||
#endif // LMS_SUPPORT_PAM
|
||||
|
||||
#include "auth/Types.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "services/auth/Types.hpp"
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/User.hpp"
|
||||
#include "utils/Exception.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
@@ -39,29 +39,27 @@ namespace Auth
|
||||
static const Wt::Auth::SHA1HashFunction sha1Function;
|
||||
|
||||
std::unique_ptr<IPasswordService>
|
||||
createPasswordService(std::string_view passwordAuthenticationBackend, std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService)
|
||||
createPasswordService(std::string_view passwordAuthenticationBackend, Database::Db& db, std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService)
|
||||
{
|
||||
if (passwordAuthenticationBackend == "internal")
|
||||
return std::make_unique<InternalPasswordService>(maxThrottlerEntries, authTokenService);
|
||||
return std::make_unique<InternalPasswordService>(db, maxThrottlerEntries, authTokenService);
|
||||
#ifdef LMS_SUPPORT_PAM
|
||||
else if (passwordAuthenticationBackend == "pam")
|
||||
return std::make_unique<PAMPasswordService>(maxThrottlerEntries, authTokenService);
|
||||
return std::make_unique<PAMPasswordService>(db, maxThrottlerEntries, authTokenService);
|
||||
#endif // LMS_SUPPORT_PAM
|
||||
|
||||
throw Exception {"Authentication backend '" + std::string {passwordAuthenticationBackend} + "' is not supported!"};
|
||||
}
|
||||
|
||||
PasswordServiceBase::PasswordServiceBase(std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService)
|
||||
: _loginThrottler {maxThrottlerEntries}
|
||||
PasswordServiceBase::PasswordServiceBase(Database::Db& db, std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService)
|
||||
: AuthServiceBase {db}
|
||||
, _loginThrottler {maxThrottlerEntries}
|
||||
, _authTokenService {authTokenService}
|
||||
{
|
||||
}
|
||||
|
||||
PasswordServiceBase::CheckResult
|
||||
PasswordServiceBase::checkUserPassword(Database::Session& session,
|
||||
const boost::asio::ip::address& clientAddress,
|
||||
std::string_view loginName,
|
||||
std::string_view password)
|
||||
PasswordServiceBase::checkUserPassword(const boost::asio::ip::address& clientAddress, std::string_view loginName, std::string_view password)
|
||||
{
|
||||
LMS_LOG(AUTH, DEBUG) << "Checking password for user '" << loginName << "'";
|
||||
|
||||
@@ -73,7 +71,7 @@ namespace Auth
|
||||
return {CheckResult::State::Throttled};
|
||||
}
|
||||
|
||||
const bool match {checkUserPassword(session, loginName, password)};
|
||||
const bool match {checkUserPassword(loginName, password)};
|
||||
{
|
||||
std::unique_lock lock {_mutex};
|
||||
|
||||
@@ -84,8 +82,8 @@ namespace Auth
|
||||
{
|
||||
_loginThrottler.onGoodClientAttempt(clientAddress);
|
||||
|
||||
const Database::UserId userId {getOrCreateUser(session, loginName)};
|
||||
onUserAuthenticated(session, userId);
|
||||
const Database::UserId userId {getOrCreateUser(loginName)};
|
||||
onUserAuthenticated(userId);
|
||||
return {CheckResult::State::Granted, userId};
|
||||
}
|
||||
else
|
||||
+7
-9
@@ -21,12 +21,13 @@
|
||||
|
||||
#include <shared_mutex>
|
||||
|
||||
#include "auth/IPasswordService.hpp"
|
||||
#include "services/auth/IPasswordService.hpp"
|
||||
#include "AuthServiceBase.hpp"
|
||||
#include "LoginThrottler.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Db;
|
||||
class Session;
|
||||
}
|
||||
|
||||
@@ -36,7 +37,7 @@ namespace Auth
|
||||
class PasswordServiceBase : public IPasswordService, public AuthServiceBase
|
||||
{
|
||||
public:
|
||||
PasswordServiceBase(std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService);
|
||||
PasswordServiceBase(Database::Db& db, std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService);
|
||||
|
||||
PasswordServiceBase(const PasswordServiceBase&) = delete;
|
||||
PasswordServiceBase& operator=(const PasswordServiceBase&) = delete;
|
||||
@@ -47,14 +48,11 @@ namespace Auth
|
||||
IAuthTokenService& getAuthTokenService() { return _authTokenService; }
|
||||
|
||||
private:
|
||||
virtual bool checkUserPassword(Database::Session& session,
|
||||
std::string_view loginName,
|
||||
std::string_view password) = 0;
|
||||
virtual bool checkUserPassword(std::string_view loginName, std::string_view password) = 0;
|
||||
|
||||
CheckResult checkUserPassword(Database::Session& session,
|
||||
const boost::asio::ip::address& clientAddress,
|
||||
std::string_view loginName,
|
||||
std::string_view password) override;
|
||||
CheckResult checkUserPassword(const boost::asio::ip::address& clientAddress,
|
||||
std::string_view loginName,
|
||||
std::string_view password) override;
|
||||
|
||||
std::shared_mutex _mutex;
|
||||
LoginThrottler _loginThrottler;
|
||||
+9
-8
@@ -28,14 +28,15 @@
|
||||
namespace Auth
|
||||
{
|
||||
|
||||
HttpHeadersEnvService::HttpHeadersEnvService()
|
||||
: _fieldName {Service<IConfig>::get()->getString("http-headers-login-field", "X-Forwarded-User")}
|
||||
HttpHeadersEnvService::HttpHeadersEnvService(Database::Db& db)
|
||||
: AuthServiceBase {db}
|
||||
, _fieldName {Service<IConfig>::get()->getString("http-headers-login-field", "X-Forwarded-User")}
|
||||
{
|
||||
LMS_LOG(AUTH, INFO) << "Using http header field = '" << _fieldName << "'";
|
||||
}
|
||||
|
||||
HttpHeadersEnvService::CheckResult
|
||||
HttpHeadersEnvService::processEnv(Database::Session& session, const Wt::WEnvironment& env)
|
||||
HttpHeadersEnvService::processEnv(const Wt::WEnvironment& env)
|
||||
{
|
||||
const std::string loginName {env.headerValue(_fieldName)};
|
||||
if (loginName.empty())
|
||||
@@ -43,13 +44,13 @@ namespace Auth
|
||||
|
||||
LMS_LOG(AUTH, DEBUG) << "Extracted login name = '" << loginName << "' from HTTP header";
|
||||
|
||||
const Database::UserId userId {getOrCreateUser(session, loginName)};
|
||||
onUserAuthenticated(session, userId);
|
||||
const Database::UserId userId {getOrCreateUser(loginName)};
|
||||
onUserAuthenticated(userId);
|
||||
return {CheckResult::State::Granted, userId};
|
||||
}
|
||||
|
||||
HttpHeadersEnvService::CheckResult
|
||||
HttpHeadersEnvService::processRequest(Database::Session& session, const Wt::Http::Request& request)
|
||||
HttpHeadersEnvService::processRequest(const Wt::Http::Request& request)
|
||||
{
|
||||
const std::string loginName {request.headerValue(_fieldName)};
|
||||
if (loginName.empty())
|
||||
@@ -57,8 +58,8 @@ namespace Auth
|
||||
|
||||
LMS_LOG(AUTH, DEBUG) << "Extracted login name = '" << loginName << "' from HTTP header";
|
||||
|
||||
const Database::UserId userId {getOrCreateUser(session, loginName)};
|
||||
onUserAuthenticated(session, userId);
|
||||
const Database::UserId userId {getOrCreateUser(loginName)};
|
||||
onUserAuthenticated(userId);
|
||||
return {CheckResult::State::Granted, userId};
|
||||
}
|
||||
|
||||
+4
-4
@@ -19,7 +19,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "auth/IEnvService.hpp"
|
||||
#include "services/auth/IEnvService.hpp"
|
||||
#include "AuthServiceBase.hpp"
|
||||
|
||||
namespace Auth
|
||||
@@ -27,11 +27,11 @@ namespace Auth
|
||||
class HttpHeadersEnvService : public IEnvService, public AuthServiceBase
|
||||
{
|
||||
public:
|
||||
HttpHeadersEnvService();
|
||||
HttpHeadersEnvService(Database::Db& db);
|
||||
|
||||
private:
|
||||
CheckResult processEnv(Database::Session& session, const Wt::WEnvironment& env) override;
|
||||
CheckResult processRequest(Database::Session& session, const Wt::Http::Request& request) override;
|
||||
CheckResult processEnv(const Wt::WEnvironment& env) override;
|
||||
CheckResult processRequest(const Wt::Http::Request& request) override;
|
||||
|
||||
std::string _fieldName;
|
||||
};
|
||||
+13
-13
@@ -20,17 +20,17 @@
|
||||
#include "InternalPasswordService.hpp"
|
||||
#include <Wt/WRandom.h>
|
||||
|
||||
#include "auth/IAuthTokenService.hpp"
|
||||
#include "auth/Types.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "services/auth/IAuthTokenService.hpp"
|
||||
#include "services/auth/Types.hpp"
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/User.hpp"
|
||||
#include "utils/Exception.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
namespace Auth
|
||||
{
|
||||
InternalPasswordService::InternalPasswordService(std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService)
|
||||
: PasswordServiceBase {maxThrottlerEntries, authTokenService}
|
||||
InternalPasswordService::InternalPasswordService(Database::Db& db, std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService)
|
||||
: PasswordServiceBase {db, maxThrottlerEntries, authTokenService}
|
||||
{
|
||||
_validator.setMinimumLength(Wt::Auth::PasswordStrengthType::OneCharClass, 4);
|
||||
_validator.setMinimumLength(Wt::Auth::PasswordStrengthType::TwoCharClass, 4);
|
||||
@@ -42,17 +42,16 @@ namespace Auth
|
||||
}
|
||||
|
||||
bool
|
||||
InternalPasswordService::checkUserPassword(Database::Session& session,
|
||||
std::string_view loginName,
|
||||
std::string_view password)
|
||||
InternalPasswordService::checkUserPassword(std::string_view loginName, std::string_view password)
|
||||
{
|
||||
LMS_LOG(AUTH, DEBUG) << "Checking internal password for user '" << loginName << "'";
|
||||
|
||||
Database::User::PasswordHash passwordHash;
|
||||
{
|
||||
Database::Session& session {getDbSession()};
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
const Database::User::pointer user {Database::User::getByLoginName(session, loginName)};
|
||||
const Database::User::pointer user {Database::User::find(session, loginName)};
|
||||
if (!user)
|
||||
{
|
||||
LMS_LOG(AUTH, DEBUG) << "hashing random stuff";
|
||||
@@ -96,13 +95,14 @@ namespace Auth
|
||||
}
|
||||
|
||||
void
|
||||
InternalPasswordService::setPassword(Database::Session& session, Database::UserId userId, std::string_view newPassword)
|
||||
InternalPasswordService::setPassword(Database::UserId userId, std::string_view newPassword)
|
||||
{
|
||||
const Database::User::PasswordHash passwordHash {hashPassword(newPassword)};
|
||||
|
||||
Database::Session& session {getDbSession()};
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
|
||||
Database::User::pointer user {Database::User::getById(session, userId)};
|
||||
Database::User::pointer user {Database::User::find(session, userId)};
|
||||
if (!user)
|
||||
throw Exception {"User not found!"};
|
||||
|
||||
@@ -117,7 +117,7 @@ namespace Auth
|
||||
}
|
||||
|
||||
user.modify()->setPasswordHash(passwordHash);
|
||||
getAuthTokenService().clearAuthTokens(session, userId);
|
||||
getAuthTokenService().clearAuthTokens(userId);
|
||||
}
|
||||
|
||||
Database::User::PasswordHash
|
||||
+4
-6
@@ -22,7 +22,7 @@
|
||||
#include <Wt/Auth/HashFunction.h>
|
||||
#include <Wt/Auth/PasswordStrengthValidator.h>
|
||||
|
||||
#include "database/User.hpp"
|
||||
#include "services/database/User.hpp"
|
||||
#include "PasswordServiceBase.hpp"
|
||||
#include "LoginThrottler.hpp"
|
||||
|
||||
@@ -33,16 +33,14 @@ namespace Auth
|
||||
class InternalPasswordService : public PasswordServiceBase
|
||||
{
|
||||
public:
|
||||
InternalPasswordService(std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService);
|
||||
InternalPasswordService(Database::Db& db, std::size_t maxThrottlerEntries, IAuthTokenService& authTokenService);
|
||||
|
||||
private:
|
||||
bool checkUserPassword(Database::Session& session,
|
||||
std::string_view loginName,
|
||||
std::string_view password) override;
|
||||
bool checkUserPassword(std::string_view loginName, std::string_view password) override;
|
||||
|
||||
bool canSetPasswords() const override;
|
||||
PasswordAcceptabilityResult checkPasswordAcceptability(std::string_view loginName, const PasswordValidationContext& context) const override;
|
||||
void setPassword(Database::Session& session, Database::UserId userId, std::string_view newPassword) override;
|
||||
void setPassword(Database::UserId userId, std::string_view newPassword) override;
|
||||
|
||||
Database::User::PasswordHash hashPassword(std::string_view password) const;
|
||||
void hashRandomPassword() const;
|
||||
+4
-4
@@ -26,8 +26,8 @@
|
||||
#include <cstring>
|
||||
#include <security/pam_appl.h>
|
||||
|
||||
#include "auth/Types.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "services/auth/Types.hpp"
|
||||
#include "services/database/Session.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
namespace Auth
|
||||
@@ -161,7 +161,7 @@ namespace Auth
|
||||
};
|
||||
|
||||
bool
|
||||
PAMPasswordService::checkUserPassword(Database::Session& /*session*/, std::string_view loginName, std::string_view password)
|
||||
PAMPasswordService::checkUserPassword(std::string_view loginName, std::string_view password)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -193,7 +193,7 @@ namespace Auth
|
||||
}
|
||||
|
||||
void
|
||||
PAMPasswordService::setPassword(Database::Session&, Database::UserId, std::string_view)
|
||||
PAMPasswordService::setPassword(Database::UserId, std::string_view)
|
||||
{
|
||||
throw NotImplementedException {};
|
||||
}
|
||||
+2
-7
@@ -31,14 +31,9 @@ namespace Auth
|
||||
using PasswordServiceBase::PasswordServiceBase;
|
||||
|
||||
private:
|
||||
bool checkUserPassword(Database::Session& session,
|
||||
std::string_view loginName,
|
||||
std::string_view password) override;
|
||||
|
||||
bool checkUserPassword(std::string_view loginName,std::string_view password) override;
|
||||
bool canSetPasswords() const override;
|
||||
PasswordAcceptabilityResult checkPasswordAcceptability(std::string_view loginName, const PasswordValidationContext& context) const override;
|
||||
void setPassword(Database::Session& session,
|
||||
Database::UserId userId,
|
||||
std::string_view newPassword) override;
|
||||
void setPassword(Database::UserId userId, std::string_view newPassword) override;
|
||||
};
|
||||
}
|
||||
+6
-6
@@ -27,11 +27,11 @@
|
||||
#include <boost/asio/ip/address.hpp>
|
||||
#include <Wt/WDateTime.h>
|
||||
|
||||
#include "database/Types.hpp"
|
||||
#include "services/database/UserId.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Session;
|
||||
class Db;
|
||||
class User;
|
||||
}
|
||||
|
||||
@@ -63,12 +63,12 @@ namespace Auth
|
||||
};
|
||||
|
||||
// Provided token is only accepted once
|
||||
virtual AuthTokenProcessResult processAuthToken(Database::Session& session, const boost::asio::ip::address& clientAddress, std::string_view tokenValue) = 0;
|
||||
virtual AuthTokenProcessResult processAuthToken(const boost::asio::ip::address& clientAddress, std::string_view tokenValue) = 0;
|
||||
|
||||
// Returns a one time token
|
||||
virtual std::string createAuthToken(Database::Session& session, Database::UserId userid, const Wt::WDateTime& expiry) = 0;
|
||||
virtual void clearAuthTokens(Database::Session& session, Database::UserId userid) = 0;
|
||||
virtual std::string createAuthToken(Database::UserId userid, const Wt::WDateTime& expiry) = 0;
|
||||
virtual void clearAuthTokens(Database::UserId userid) = 0;
|
||||
};
|
||||
|
||||
std::unique_ptr<IAuthTokenService> createAuthTokenService(std::size_t maxThrottlerEntryCount);
|
||||
std::unique_ptr<IAuthTokenService> createAuthTokenService(Database::Db& db, std::size_t maxThrottlerEntryCount);
|
||||
}
|
||||
+5
-4
@@ -22,10 +22,11 @@
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include "database/Types.hpp"
|
||||
#include "services/database/UserId.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Db;
|
||||
class Session;
|
||||
}
|
||||
|
||||
@@ -60,9 +61,9 @@ namespace Auth
|
||||
std::optional<Database::UserId> userId {};
|
||||
};
|
||||
|
||||
virtual CheckResult processEnv(Database::Session& session, const Wt::WEnvironment& env) = 0;
|
||||
virtual CheckResult processRequest(Database::Session& session, const Wt::Http::Request& request) = 0;
|
||||
virtual CheckResult processEnv(const Wt::WEnvironment& env) = 0;
|
||||
virtual CheckResult processRequest(const Wt::Http::Request& request) = 0;
|
||||
};
|
||||
|
||||
std::unique_ptr<IEnvService> createEnvService(std::string_view backendName);
|
||||
std::unique_ptr<IEnvService> createEnvService(std::string_view backendName, Database::Db& db);
|
||||
} // namespace Auth
|
||||
+9
-10
@@ -21,16 +21,16 @@
|
||||
|
||||
#include <string_view>
|
||||
|
||||
#include <boost/asio/ip/address.hpp>
|
||||
#include <Wt/WDateTime.h>
|
||||
#include <Wt/Dbo/ptr.h>
|
||||
|
||||
#include <boost/asio/ip/address.hpp>
|
||||
#include "auth/Types.hpp"
|
||||
#include "database/Types.hpp"
|
||||
#include "services/auth/Types.hpp"
|
||||
#include "services/database/UserId.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Session;
|
||||
class Db;
|
||||
class User;
|
||||
}
|
||||
|
||||
@@ -56,10 +56,9 @@ namespace Auth
|
||||
std::optional<Database::UserId> userId {};
|
||||
std::optional<Wt::WDateTime> expiry {};
|
||||
};
|
||||
virtual CheckResult checkUserPassword(Database::Session& session,
|
||||
const boost::asio::ip::address& clientAddress,
|
||||
std::string_view loginName,
|
||||
std::string_view password) = 0;
|
||||
virtual CheckResult checkUserPassword(const boost::asio::ip::address& clientAddress,
|
||||
std::string_view loginName,
|
||||
std::string_view password) = 0;
|
||||
|
||||
virtual bool canSetPasswords() const = 0;
|
||||
|
||||
@@ -70,9 +69,9 @@ namespace Auth
|
||||
MustMatchLoginName,
|
||||
};
|
||||
virtual PasswordAcceptabilityResult checkPasswordAcceptability(std::string_view password, const PasswordValidationContext& context) const = 0;
|
||||
virtual void setPassword(Database::Session& session, Database::UserId userId, std::string_view newPassword) = 0;
|
||||
virtual void setPassword(Database::UserId userId, std::string_view newPassword) = 0;
|
||||
};
|
||||
|
||||
std::unique_ptr<IPasswordService> createPasswordService(std::string_view authPasswordBackend, std::size_t maxThrottlerEntryCount, IAuthTokenService& authTokenService);
|
||||
std::unique_ptr<IPasswordService> createPasswordService(std::string_view authPasswordBackend, Database::Db& db, std::size_t maxThrottlerEntryCount, IAuthTokenService& authTokenService);
|
||||
}
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include "database/Types.hpp"
|
||||
#include "services/database/Types.hpp"
|
||||
#include "utils/Exception.hpp"
|
||||
|
||||
namespace Auth
|
||||
@@ -0,0 +1,28 @@
|
||||
|
||||
add_library(lmsservice-cover SHARED
|
||||
impl/CoverService.cpp
|
||||
)
|
||||
|
||||
target_include_directories(lmsservice-cover INTERFACE
|
||||
include
|
||||
)
|
||||
|
||||
target_include_directories(lmsservice-cover PRIVATE
|
||||
include
|
||||
impl
|
||||
)
|
||||
|
||||
target_link_libraries(lmsservice-cover PRIVATE
|
||||
lmsav
|
||||
lmsimage
|
||||
)
|
||||
|
||||
target_link_libraries(lmsservice-cover PUBLIC
|
||||
lmsdatabase
|
||||
lmsimage
|
||||
lmsutils
|
||||
std::filesystem
|
||||
)
|
||||
|
||||
install(TARGETS lmsservice-cover DESTINATION lib)
|
||||
|
||||
+61
-56
@@ -17,26 +17,21 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "CoverArtGrabber.hpp"
|
||||
#include "CoverService.hpp"
|
||||
|
||||
#include "av/IAudioFile.hpp"
|
||||
|
||||
#include "database/Release.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Track.hpp"
|
||||
|
||||
#if LMS_SUPPORT_IMAGE_STB
|
||||
#include "stb/RawImage.hpp"
|
||||
using RawImage = CoverArt::STB::RawImage;
|
||||
#elif LMS_SUPPORT_IMAGE_GM
|
||||
#include "graphicsmagick/RawImage.hpp"
|
||||
using RawImage = CoverArt::GraphicsMagick::RawImage;
|
||||
#endif
|
||||
#include "services/database/Db.hpp"
|
||||
#include "services/database/Release.hpp"
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/Track.hpp"
|
||||
|
||||
#include "image/Exception.hpp"
|
||||
#include "image/IRawImage.hpp"
|
||||
#include "utils/IConfig.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/Random.hpp"
|
||||
#include "utils/Utils.hpp"
|
||||
#include "Exception.hpp"
|
||||
|
||||
namespace
|
||||
{
|
||||
@@ -55,7 +50,7 @@ namespace
|
||||
|
||||
auto transaction {dbSession.createSharedTransaction()};
|
||||
|
||||
const Database::Track::pointer track {Database::Track::getById(dbSession, trackId)};
|
||||
const Database::Track::pointer track {Database::Track::find(dbSession, trackId)};
|
||||
if (!track)
|
||||
return res;
|
||||
|
||||
@@ -75,7 +70,9 @@ namespace
|
||||
}
|
||||
}
|
||||
|
||||
namespace CoverArt {
|
||||
namespace Cover {
|
||||
|
||||
using namespace Image;
|
||||
|
||||
static
|
||||
bool
|
||||
@@ -84,28 +81,26 @@ isFileSupported(const std::filesystem::path& file, const std::vector<std::filesy
|
||||
return (std::find(std::cbegin(extensions), std::cend(extensions), file.extension()) != std::cend(extensions));
|
||||
}
|
||||
|
||||
std::unique_ptr<IGrabber>
|
||||
createGrabber(const std::filesystem::path& execPath,
|
||||
const std::filesystem::path& defaultCoverPath,
|
||||
std::size_t maxCacheSize, std::size_t maxFileSize, unsigned jpegQuality)
|
||||
std::unique_ptr<ICoverService>
|
||||
createCoverService(Database::Db& db, const std::filesystem::path& execPath, const std::filesystem::path& defaultCoverPath)
|
||||
{
|
||||
return std::make_unique<Grabber>(execPath, defaultCoverPath, maxCacheSize, maxFileSize, jpegQuality);
|
||||
return std::make_unique<CoverService>(db, execPath, defaultCoverPath);
|
||||
}
|
||||
|
||||
Grabber::Grabber(const std::filesystem::path& execPath,
|
||||
const std::filesystem::path& defaultCoverPath,
|
||||
std::size_t maxCacheSize,
|
||||
std::size_t maxFileSize,
|
||||
unsigned jpegQuality)
|
||||
: _defaultCoverPath {defaultCoverPath}
|
||||
, _maxCacheSize {maxCacheSize}
|
||||
, _maxFileSize {maxFileSize}
|
||||
, _jpegQuality {Utils::clamp<unsigned>(jpegQuality, 1, 100)}
|
||||
CoverService::CoverService(Database::Db& db,
|
||||
const std::filesystem::path& execPath,
|
||||
const std::filesystem::path& defaultCoverPath)
|
||||
: _db {db}
|
||||
, _defaultCoverPath {defaultCoverPath}
|
||||
, _maxCacheSize {Service<IConfig>::get()->getULong("cover-max-cache-size", 30) * 1000 * 1000}
|
||||
, _maxFileSize {Service<IConfig>::get()->getULong("cover-max-file-size", 10) * 1000 * 1000}
|
||||
|
||||
{
|
||||
setJpegQuality(Service<IConfig>::get()->getULong("cover-jpeg-quality", 75));
|
||||
|
||||
LMS_LOG(COVER, INFO) << "Default cover path = '" << _defaultCoverPath.string() << "'";
|
||||
LMS_LOG(COVER, INFO) << "Max cache size = " << _maxCacheSize;
|
||||
LMS_LOG(COVER, INFO) << "Max file size = " << _maxFileSize;
|
||||
LMS_LOG(COVER, INFO) << "JPEG export quality = " << _jpegQuality;
|
||||
|
||||
#if LMS_SUPPORT_IMAGE_GM
|
||||
GraphicsMagick::init(execPath);
|
||||
@@ -117,14 +112,14 @@ Grabber::Grabber(const std::filesystem::path& execPath,
|
||||
{
|
||||
getDefault(512);
|
||||
}
|
||||
catch (const ImageException& e)
|
||||
catch (const Image::ImageException& e)
|
||||
{
|
||||
throw LmsException("Cannot read default cover file '" + _defaultCoverPath.string() + "': " + e.what());
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<IEncodedImage>
|
||||
Grabber::getFromAvMediaFile(const Av::IAudioFile& input, ImageSize width) const
|
||||
CoverService::getFromAvMediaFile(const Av::IAudioFile& input, ImageSize width) const
|
||||
{
|
||||
std::unique_ptr<IEncodedImage> image;
|
||||
|
||||
@@ -135,11 +130,11 @@ Grabber::getFromAvMediaFile(const Av::IAudioFile& input, ImageSize width) const
|
||||
|
||||
try
|
||||
{
|
||||
RawImage rawImage {picture.data, picture.dataSize};
|
||||
rawImage.resize(width);
|
||||
image = rawImage.encodeToJPEG(_jpegQuality);
|
||||
std::unique_ptr<IRawImage> rawImage {decodeImage(picture.data, picture.dataSize)};
|
||||
rawImage->resize(width);
|
||||
image = rawImage->encodeToJPEG(_jpegQuality);
|
||||
}
|
||||
catch (const ImageException& e)
|
||||
catch (const Image::ImageException& e)
|
||||
{
|
||||
LMS_LOG(COVER, ERROR) << "Cannot read embedded cover: " << e.what();
|
||||
}
|
||||
@@ -149,15 +144,15 @@ Grabber::getFromAvMediaFile(const Av::IAudioFile& input, ImageSize width) const
|
||||
}
|
||||
|
||||
std::unique_ptr<IEncodedImage>
|
||||
Grabber::getFromCoverFile(const std::filesystem::path& p, ImageSize width) const
|
||||
CoverService::getFromCoverFile(const std::filesystem::path& p, ImageSize width) const
|
||||
{
|
||||
std::unique_ptr<IEncodedImage> image;
|
||||
|
||||
try
|
||||
{
|
||||
RawImage rawImage {p};
|
||||
rawImage.resize(width);
|
||||
image = rawImage.encodeToJPEG(_jpegQuality);
|
||||
std::unique_ptr<IRawImage> rawImage {decodeImage(p)};
|
||||
rawImage->resize(width);
|
||||
image = rawImage->encodeToJPEG(_jpegQuality);
|
||||
}
|
||||
catch (const ImageException& e)
|
||||
{
|
||||
@@ -168,7 +163,7 @@ Grabber::getFromCoverFile(const std::filesystem::path& p, ImageSize width) const
|
||||
}
|
||||
|
||||
std::shared_ptr<IEncodedImage>
|
||||
Grabber::getDefault(ImageSize width)
|
||||
CoverService::getDefault(ImageSize width)
|
||||
{
|
||||
{
|
||||
std::shared_lock lock {_cacheMutex};
|
||||
@@ -192,7 +187,7 @@ Grabber::getDefault(ImageSize width)
|
||||
}
|
||||
|
||||
std::unique_ptr<IEncodedImage>
|
||||
Grabber::getFromDirectory(const std::filesystem::path& directory, ImageSize width) const
|
||||
CoverService::getFromDirectory(const std::filesystem::path& directory, ImageSize width) const
|
||||
{
|
||||
const std::multimap<std::string, std::filesystem::path> coverPaths {getCoverPaths(directory)};
|
||||
|
||||
@@ -231,7 +226,7 @@ Grabber::getFromDirectory(const std::filesystem::path& directory, ImageSize widt
|
||||
}
|
||||
|
||||
std::unique_ptr<IEncodedImage>
|
||||
Grabber::getFromSameNamedFile(const std::filesystem::path& filePath, ImageSize width) const
|
||||
CoverService::getFromSameNamedFile(const std::filesystem::path& filePath, ImageSize width) const
|
||||
{
|
||||
std::unique_ptr<IEncodedImage> res;
|
||||
|
||||
@@ -252,7 +247,7 @@ Grabber::getFromSameNamedFile(const std::filesystem::path& filePath, ImageSize w
|
||||
}
|
||||
|
||||
bool
|
||||
Grabber::checkCoverFile(const std::filesystem::path& filePath) const
|
||||
CoverService::checkCoverFile(const std::filesystem::path& filePath) const
|
||||
{
|
||||
std::error_code ec;
|
||||
|
||||
@@ -275,7 +270,7 @@ Grabber::checkCoverFile(const std::filesystem::path& filePath) const
|
||||
}
|
||||
|
||||
std::multimap<std::string, std::filesystem::path>
|
||||
Grabber::getCoverPaths(const std::filesystem::path& directoryPath) const
|
||||
CoverService::getCoverPaths(const std::filesystem::path& directoryPath) const
|
||||
{
|
||||
std::multimap<std::string, std::filesystem::path> res;
|
||||
std::error_code ec;
|
||||
@@ -296,7 +291,7 @@ Grabber::getCoverPaths(const std::filesystem::path& directoryPath) const
|
||||
}
|
||||
|
||||
std::unique_ptr<IEncodedImage>
|
||||
Grabber::getFromTrack(const std::filesystem::path& p, ImageSize width) const
|
||||
CoverService::getFromTrack(const std::filesystem::path& p, ImageSize width) const
|
||||
{
|
||||
std::unique_ptr<IEncodedImage> image;
|
||||
|
||||
@@ -313,13 +308,13 @@ Grabber::getFromTrack(const std::filesystem::path& p, ImageSize width) const
|
||||
}
|
||||
|
||||
std::shared_ptr<IEncodedImage>
|
||||
Grabber::getFromTrack(Database::Session& dbSession, Database::TrackId trackId, ImageSize width)
|
||||
CoverService::getFromTrack(Database::TrackId trackId, ImageSize width)
|
||||
{
|
||||
return getFromTrack(dbSession, trackId, width, true /* allow release fallback*/);
|
||||
return getFromTrack(_db.getTLSSession(), trackId, width, true /* allow release fallback*/);
|
||||
}
|
||||
|
||||
std::shared_ptr<IEncodedImage>
|
||||
Grabber::getFromTrack(Database::Session& dbSession, Database::TrackId trackId, ImageSize width, bool allowReleaseFallback)
|
||||
CoverService::getFromTrack(Database::Session& dbSession, Database::TrackId trackId, ImageSize width, bool allowReleaseFallback)
|
||||
{
|
||||
using namespace Database;
|
||||
|
||||
@@ -338,7 +333,7 @@ Grabber::getFromTrack(Database::Session& dbSession, Database::TrackId trackId, I
|
||||
cover = getFromSameNamedFile(trackInfo->trackPath, width);
|
||||
|
||||
if (!cover && trackInfo->releaseId && allowReleaseFallback)
|
||||
cover = getFromRelease(dbSession, *trackInfo->releaseId, width);
|
||||
cover = getFromRelease(*trackInfo->releaseId, width);
|
||||
|
||||
if (!cover && trackInfo->isMultiDisc)
|
||||
{
|
||||
@@ -357,7 +352,7 @@ Grabber::getFromTrack(Database::Session& dbSession, Database::TrackId trackId, I
|
||||
}
|
||||
|
||||
std::shared_ptr<IEncodedImage>
|
||||
Grabber::getFromRelease(Database::Session& session, Database::ReleaseId releaseId, ImageSize width)
|
||||
CoverService::getFromRelease(Database::ReleaseId releaseId, ImageSize width)
|
||||
{
|
||||
const CacheEntryDesc cacheEntryDesc {releaseId, width};
|
||||
|
||||
@@ -371,13 +366,15 @@ Grabber::getFromRelease(Database::Session& session, Database::ReleaseId releaseI
|
||||
std::filesystem::path releaseDirectory;
|
||||
};
|
||||
|
||||
Database::Session& session {_db.getTLSSession()};
|
||||
|
||||
auto getReleaseInfo {[&]
|
||||
{
|
||||
std::optional<ReleaseInfo> res;
|
||||
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
if (const Database::Release::pointer release {Database::Release::getById(session, releaseId)})
|
||||
if (const Database::Release::pointer release {Database::Release::find(session, releaseId)})
|
||||
{
|
||||
if (const auto firstTrack {release->getFirstTrack()})
|
||||
{
|
||||
@@ -407,7 +404,7 @@ Grabber::getFromRelease(Database::Session& session, Database::ReleaseId releaseI
|
||||
}
|
||||
|
||||
void
|
||||
Grabber::flushCache()
|
||||
CoverService::flushCache()
|
||||
{
|
||||
std::unique_lock lock {_cacheMutex};
|
||||
|
||||
@@ -419,7 +416,15 @@ Grabber::flushCache()
|
||||
}
|
||||
|
||||
void
|
||||
Grabber::saveToCache(const CacheEntryDesc& entryDesc, std::shared_ptr<IEncodedImage> image)
|
||||
CoverService::setJpegQuality(unsigned quality)
|
||||
{
|
||||
_jpegQuality = Utils::clamp<unsigned>(quality, 1, 100);
|
||||
|
||||
LMS_LOG(COVER, INFO) << "JPEG export quality = " << _jpegQuality;
|
||||
}
|
||||
|
||||
void
|
||||
CoverService::saveToCache(const CacheEntryDesc& entryDesc, std::shared_ptr<IEncodedImage> image)
|
||||
{
|
||||
std::unique_lock lock {_cacheMutex};
|
||||
|
||||
@@ -435,7 +440,7 @@ Grabber::saveToCache(const CacheEntryDesc& entryDesc, std::shared_ptr<IEncodedIm
|
||||
}
|
||||
|
||||
std::shared_ptr<IEncodedImage>
|
||||
Grabber::loadFromCache(const CacheEntryDesc& entryDesc)
|
||||
CoverService::loadFromCache(const CacheEntryDesc& entryDesc)
|
||||
{
|
||||
std::shared_lock lock {_cacheMutex};
|
||||
|
||||
@@ -450,5 +455,5 @@ Grabber::loadFromCache(const CacheEntryDesc& entryDesc)
|
||||
return it->second;
|
||||
}
|
||||
|
||||
} // namespace CoverArt
|
||||
} // namespace Cover
|
||||
|
||||
+34
-33
@@ -29,9 +29,9 @@
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
#include "cover/ICoverArtGrabber.hpp"
|
||||
#include "cover/IEncodedImage.hpp"
|
||||
#include "database/Types.hpp"
|
||||
#include "services/cover/ICoverService.hpp"
|
||||
#include "image/IEncodedImage.hpp"
|
||||
#include "services/database/Types.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
@@ -43,7 +43,7 @@ namespace Av
|
||||
class IAudioFile;
|
||||
}
|
||||
|
||||
namespace CoverArt
|
||||
namespace Cover
|
||||
{
|
||||
struct CacheEntryDesc
|
||||
{
|
||||
@@ -57,16 +57,16 @@ namespace CoverArt
|
||||
}
|
||||
};
|
||||
|
||||
} // ns CoverArt
|
||||
} // ns Cover
|
||||
|
||||
namespace std
|
||||
{
|
||||
|
||||
template<>
|
||||
class hash<CoverArt::CacheEntryDesc>
|
||||
class hash<Cover::CacheEntryDesc>
|
||||
{
|
||||
public:
|
||||
size_t operator()(const CoverArt::CacheEntryDesc& e) const
|
||||
size_t operator()(const Cover::CacheEntryDesc& e) const
|
||||
{
|
||||
size_t h {};
|
||||
std::visit([&](auto id)
|
||||
@@ -81,56 +81,57 @@ namespace std
|
||||
|
||||
} // ns std
|
||||
|
||||
namespace CoverArt
|
||||
namespace Cover
|
||||
{
|
||||
class Grabber : public IGrabber
|
||||
class CoverService : public ICoverService
|
||||
{
|
||||
public:
|
||||
Grabber(const std::filesystem::path& execPath,
|
||||
const std::filesystem::path& defaultCoverPath,
|
||||
std::size_t maxCacheEntries,
|
||||
std::size_t maxFileSize,
|
||||
unsigned jpegQuality);
|
||||
CoverService(Database::Db& db,
|
||||
const std::filesystem::path& execPath,
|
||||
const std::filesystem::path& defaultCoverPath);
|
||||
|
||||
Grabber(const Grabber&) = delete;
|
||||
Grabber& operator=(const Grabber&) = delete;
|
||||
Grabber(Grabber&&) = delete;
|
||||
Grabber& operator=(Grabber&&) = delete;
|
||||
CoverService(const CoverService&) = delete;
|
||||
CoverService& operator=(const CoverService&) = delete;
|
||||
CoverService(CoverService&&) = delete;
|
||||
CoverService& operator=(CoverService&&) = delete;
|
||||
|
||||
private:
|
||||
std::shared_ptr<IEncodedImage> getFromTrack(Database::Session& dbSession, Database::TrackId trackId, ImageSize width) override;
|
||||
std::shared_ptr<IEncodedImage> getFromRelease(Database::Session& dbSession, Database::ReleaseId releaseId, ImageSize width) override;
|
||||
std::shared_ptr<Image::IEncodedImage> getFromTrack(Database::TrackId trackId, Image::ImageSize width) override;
|
||||
std::shared_ptr<Image::IEncodedImage> getFromRelease(Database::ReleaseId releaseId, Image::ImageSize width) override;
|
||||
void flushCache() override;
|
||||
void setJpegQuality(unsigned quality) override;
|
||||
|
||||
std::shared_ptr<IEncodedImage> getFromTrack(Database::Session& dbSession, Database::TrackId trackId, ImageSize width, bool allowReleaseFallback);
|
||||
std::unique_ptr<IEncodedImage> getFromAvMediaFile(const Av::IAudioFile& input, ImageSize width) const;
|
||||
std::unique_ptr<IEncodedImage> getFromCoverFile(const std::filesystem::path& p, ImageSize width) const;
|
||||
std::shared_ptr<Image::IEncodedImage> getFromTrack(Database::Session& dbSession, Database::TrackId trackId, Image::ImageSize width, bool allowReleaseFallback);
|
||||
std::unique_ptr<Image::IEncodedImage> getFromAvMediaFile(const Av::IAudioFile& input, Image::ImageSize width) const;
|
||||
std::unique_ptr<Image::IEncodedImage> getFromCoverFile(const std::filesystem::path& p, Image::ImageSize width) const;
|
||||
|
||||
std::unique_ptr<IEncodedImage> getFromTrack(const std::filesystem::path& path, ImageSize width) const;
|
||||
std::unique_ptr<Image::IEncodedImage> getFromTrack(const std::filesystem::path& path, Image::ImageSize width) const;
|
||||
std::multimap<std::string, std::filesystem::path> getCoverPaths(const std::filesystem::path& directoryPath) const;
|
||||
std::unique_ptr<IEncodedImage> getFromDirectory(const std::filesystem::path& directory, ImageSize width) const;
|
||||
std::unique_ptr<IEncodedImage> getFromSameNamedFile(const std::filesystem::path& filePath, ImageSize width) const;
|
||||
std::shared_ptr<IEncodedImage> getDefault(ImageSize width);
|
||||
std::unique_ptr<Image::IEncodedImage> getFromDirectory(const std::filesystem::path& directory, Image::ImageSize width) const;
|
||||
std::unique_ptr<Image::IEncodedImage> getFromSameNamedFile(const std::filesystem::path& filePath, Image::ImageSize width) const;
|
||||
std::shared_ptr<Image::IEncodedImage> getDefault(Image::ImageSize width);
|
||||
|
||||
bool checkCoverFile(const std::filesystem::path& directoryPath) const;
|
||||
|
||||
Database::Db& _db;
|
||||
|
||||
std::shared_mutex _cacheMutex;
|
||||
std::unordered_map<CacheEntryDesc, std::shared_ptr<IEncodedImage>> _cache;
|
||||
std::unordered_map<ImageSize, std::shared_ptr<IEncodedImage>> _defaultCoverCache;
|
||||
std::unordered_map<CacheEntryDesc, std::shared_ptr<Image::IEncodedImage>> _cache;
|
||||
std::unordered_map<Image::ImageSize, std::shared_ptr<Image::IEncodedImage>> _defaultCoverCache;
|
||||
std::atomic<std::size_t> _cacheMisses {};
|
||||
std::atomic<std::size_t> _cacheHits {};
|
||||
std::size_t _cacheSize {};
|
||||
|
||||
void saveToCache(const CacheEntryDesc& entryDesc, std::shared_ptr<IEncodedImage> image);
|
||||
std::shared_ptr<IEncodedImage> loadFromCache(const CacheEntryDesc& entryDesc);
|
||||
void saveToCache(const CacheEntryDesc& entryDesc, std::shared_ptr<Image::IEncodedImage> image);
|
||||
std::shared_ptr<Image::IEncodedImage> loadFromCache(const CacheEntryDesc& entryDesc);
|
||||
|
||||
const std::filesystem::path _defaultCoverPath;
|
||||
const std::size_t _maxCacheSize;
|
||||
static inline const std::vector<std::filesystem::path> _fileExtensions {".jpg", ".jpeg", ".png", ".bmp"}; // TODO parametrize
|
||||
const std::size_t _maxFileSize;
|
||||
static inline const std::vector<std::string> _preferredFileNames {"cover", "front"}; // TODO parametrize
|
||||
const unsigned _jpegQuality;
|
||||
unsigned _jpegQuality;
|
||||
};
|
||||
|
||||
} // namespace CoverArt
|
||||
} // namespace Cover
|
||||
|
||||
+14
-13
@@ -22,32 +22,33 @@
|
||||
#include <filesystem>
|
||||
#include <memory>
|
||||
|
||||
#include "database/Types.hpp"
|
||||
#include "cover/IEncodedImage.hpp"
|
||||
#include "services/database/ReleaseId.hpp"
|
||||
#include "services/database/TrackId.hpp"
|
||||
#include "image/IEncodedImage.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Session;
|
||||
class Db;
|
||||
}
|
||||
|
||||
namespace CoverArt
|
||||
namespace Cover
|
||||
{
|
||||
class IGrabber
|
||||
class ICoverService
|
||||
{
|
||||
public:
|
||||
virtual ~IGrabber() = default;
|
||||
virtual ~ICoverService() = default;
|
||||
|
||||
virtual std::shared_ptr<IEncodedImage> getFromTrack(Database::Session& dbSession, Database::TrackId trackId, ImageSize width) = 0;
|
||||
virtual std::shared_ptr<IEncodedImage> getFromRelease(Database::Session& dbSession, Database::ReleaseId releaseId, ImageSize width) = 0;
|
||||
virtual std::shared_ptr<Image::IEncodedImage> getFromTrack(Database::TrackId trackId, Image::ImageSize width) = 0;
|
||||
virtual std::shared_ptr<Image::IEncodedImage> getFromRelease(Database::ReleaseId releaseId, Image::ImageSize width) = 0;
|
||||
|
||||
virtual void flushCache() = 0;
|
||||
|
||||
virtual void setJpegQuality(unsigned quality) = 0; // from 1 to 100
|
||||
};
|
||||
|
||||
std::unique_ptr<IGrabber> createGrabber(const std::filesystem::path& execPath,
|
||||
const std::filesystem::path& defaultCoverPath,
|
||||
std::size_t maxCacheEntries,
|
||||
std::size_t maxFileSize,
|
||||
unsigned jpegQuality);
|
||||
std::unique_ptr<ICoverService> createCoverService(Database::Db& db,
|
||||
const std::filesystem::path& execPath,
|
||||
const std::filesystem::path& defaultCoverPath);
|
||||
|
||||
} // namespace CoverArt
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
|
||||
add_library(lmsdatabase SHARED
|
||||
impl/Artist.cpp
|
||||
impl/AuthToken.cpp
|
||||
impl/Cluster.cpp
|
||||
impl/Db.cpp
|
||||
impl/Listen.cpp
|
||||
impl/Migration.cpp
|
||||
impl/TrackArtistLink.cpp
|
||||
impl/TrackFeatures.cpp
|
||||
impl/TrackList.cpp
|
||||
impl/Release.cpp
|
||||
impl/ScanSettings.cpp
|
||||
impl/Session.cpp
|
||||
impl/StarredArtist.cpp
|
||||
impl/StarredRelease.cpp
|
||||
impl/StarredTrack.cpp
|
||||
impl/SqlQuery.cpp
|
||||
impl/Track.cpp
|
||||
impl/TrackBookmark.cpp
|
||||
impl/Types.cpp
|
||||
impl/User.cpp
|
||||
impl/Utils.cpp
|
||||
)
|
||||
@@ -36,3 +42,6 @@ target_link_libraries(lmsdatabase PUBLIC
|
||||
|
||||
install(TARGETS lmsdatabase DESTINATION lib)
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_subdirectory(test)
|
||||
endif()
|
||||
@@ -16,19 +16,19 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "database/Artist.hpp"
|
||||
#include "services/database/Artist.hpp"
|
||||
|
||||
#include <Wt/Dbo/WtSqlTraits.h>
|
||||
|
||||
#include "database/Cluster.hpp"
|
||||
#include "database/Release.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "database/User.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 "utils/Logger.hpp"
|
||||
#include "SqlQuery.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "Traits.hpp"
|
||||
#include "IdTypeTraits.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
@@ -40,8 +40,16 @@ _MBID {MBID ? MBID->getAsString() : ""}
|
||||
{
|
||||
}
|
||||
|
||||
std::size_t
|
||||
Artist::getCount(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM artist");
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer>
|
||||
Artist::getByName(Session& session, const std::string& name)
|
||||
Artist::find(Session& session, const std::string& name)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
@@ -53,19 +61,26 @@ Artist::getByName(Session& session, const std::string& name)
|
||||
}
|
||||
|
||||
Artist::pointer
|
||||
Artist::getByMBID(Session& session, const UUID& mbid)
|
||||
Artist::find(Session& session, const UUID& mbid)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
return session.getDboSession().find<Artist>().where("mbid = ?").bind(std::string {mbid.getAsString()}).resultValue();
|
||||
}
|
||||
|
||||
Artist::pointer
|
||||
Artist::getById(Session& session, ArtistId id)
|
||||
Artist::find(Session& session, ArtistId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
return session.getDboSession().find<Artist>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
|
||||
bool
|
||||
Artist::exists(Session& session, ArtistId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
return session.getDboSession().query<int>("SELECT 1 FROM artist").where("id = ?").bind(id).resultValue() == 1;
|
||||
}
|
||||
|
||||
Artist::pointer
|
||||
Artist::create(Session& session, const std::string& name, const std::optional<UUID>& MBID)
|
||||
{
|
||||
@@ -77,36 +92,37 @@ Artist::create(Session& session, const std::string& name, const std::optional<UU
|
||||
return res;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
static
|
||||
Wt::Dbo::Query<T>
|
||||
createQuery(Session& session,
|
||||
const std::string& queryStr,
|
||||
const std::vector<ClusterId>& clusterIds,
|
||||
const std::vector<std::string_view>& keywords,
|
||||
std::optional<TrackArtistLinkType> linkType)
|
||||
Wt::Dbo::Query<ArtistId>
|
||||
createQuery(Session& session, const Artist::FindParameters& params)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {session.getDboSession().query<T>(queryStr)};
|
||||
query.join("track t ON t.id = t_a_l.track_id");
|
||||
query.join("track_artist_link t_a_l ON t_a_l.artist_id = a.id");
|
||||
auto query {session.getDboSession().query<ArtistId>("SELECT DISTINCT a.id FROM artist a")};
|
||||
if (params.sortMethod == ArtistSortMethod::LastWritten || params.writtenAfter.isValid() || params.linkType)
|
||||
{
|
||||
query.join("track t ON t.id = t_a_l.track_id");
|
||||
query.join("track_artist_link t_a_l ON t_a_l.artist_id = a.id");
|
||||
}
|
||||
|
||||
if (linkType)
|
||||
query.where("t_a_l.type = ?").bind(*linkType);
|
||||
if (params.linkType)
|
||||
query.where("t_a_l.type = ?").bind(*params.linkType);
|
||||
|
||||
if (!keywords.empty())
|
||||
if (params.writtenAfter.isValid())
|
||||
query.where("t.file_last_write > ?").bind(params.writtenAfter);
|
||||
|
||||
if (!params.keywords.empty())
|
||||
{
|
||||
std::vector<std::string> clauses;
|
||||
std::vector<std::string> sortClauses;
|
||||
|
||||
for (std::string_view keyword : keywords)
|
||||
for (std::string_view keyword : params.keywords)
|
||||
{
|
||||
clauses.push_back("a.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'");
|
||||
query.bind("%" + escapeLikeKeyword(keyword) + "%");
|
||||
}
|
||||
|
||||
for (std::string_view keyword : keywords)
|
||||
for (std::string_view keyword : params.keywords)
|
||||
{
|
||||
sortClauses.push_back("a.sort_name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'");
|
||||
query.bind("%" + escapeLikeKeyword(keyword) + "%");
|
||||
@@ -115,7 +131,15 @@ createQuery(Session& session,
|
||||
query.where("(" + StringUtils::joinStrings(clauses, " AND ") + ") OR (" + StringUtils::joinStrings(sortClauses, " AND ") + ")");
|
||||
}
|
||||
|
||||
if (!clusterIds.empty())
|
||||
if (params.starringUser.isValid())
|
||||
{
|
||||
assert(params.scrobbler);
|
||||
query.join("starred_artist s_a ON s_a.artist_id = a.id")
|
||||
.where("s_a.user_id = ?").bind(params.starringUser)
|
||||
.where("s_a.scrobbler = ?").bind(*params.scrobbler);
|
||||
}
|
||||
|
||||
if (!params.clusters.empty())
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "a.id IN (SELECT DISTINCT a.id FROM artist a"
|
||||
@@ -125,268 +149,59 @@ createQuery(Session& session,
|
||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
|
||||
|
||||
WhereClause clusterClause;
|
||||
for (const ClusterId clusterId : clusterIds)
|
||||
for (const ClusterId clusterId : params.clusters)
|
||||
{
|
||||
clusterClause.Or(WhereClause("c.id = ?"));
|
||||
query.bind(clusterId);
|
||||
}
|
||||
|
||||
oss << " " << clusterClause.get();
|
||||
oss << " GROUP BY t.id,a.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size() << ")";
|
||||
oss << " GROUP BY t.id,a.id HAVING COUNT(DISTINCT c.id) = " << params.clusters.size() << ")";
|
||||
|
||||
query.where(oss.str());
|
||||
}
|
||||
|
||||
switch (params.sortMethod)
|
||||
{
|
||||
case ArtistSortMethod::None:
|
||||
break;
|
||||
case ArtistSortMethod::ByName:
|
||||
query.orderBy("a.name COLLATE NOCASE");
|
||||
break;
|
||||
case ArtistSortMethod::BySortName:
|
||||
query.orderBy("a.sort_name COLLATE NOCASE");
|
||||
break;
|
||||
case ArtistSortMethod::Random:
|
||||
query.orderBy("RANDOM()");
|
||||
break;
|
||||
case ArtistSortMethod::LastWritten:
|
||||
query.orderBy("t.file_last_write DESC");
|
||||
break;
|
||||
case ArtistSortMethod::StarredDateDesc:
|
||||
assert(params.starringUser.isValid());
|
||||
query.orderBy("s_a.date_time DESC");
|
||||
break;
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer>
|
||||
Artist::getAll(Session& session)
|
||||
RangeResults<ArtistId>
|
||||
Artist::findAllOrphans(Session& session, Range range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
auto query {session.getDboSession().query<ArtistId>("SELECT DISTINCT a.id FROM artist a WHERE NOT EXISTS(SELECT 1 FROM track t INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id WHERE t.id = t_a_l.track_id)")};
|
||||
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> res = session.getDboSession().find<Artist>();
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
return execQuery(query, range);
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer>
|
||||
Artist::getAll(Session& session, SortMethod sortMethod)
|
||||
RangeResults<ArtistId>
|
||||
Artist::find(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {session.getDboSession().find<Artist>()};
|
||||
switch (sortMethod)
|
||||
{
|
||||
case Artist::SortMethod::None:
|
||||
break;
|
||||
case Artist::SortMethod::ByName:
|
||||
query.orderBy("name COLLATE NOCASE");
|
||||
break;
|
||||
case Artist::SortMethod::BySortName:
|
||||
query.orderBy("sort_name COLLATE NOCASE");
|
||||
break;
|
||||
}
|
||||
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> res = query;
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
|
||||
std::vector<Artist::pointer>
|
||||
Artist::getAll(Session& session, SortMethod sortMethod, std::optional<Range> range, bool& moreResults)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {createQuery<Wt::Dbo::ptr<Artist>>(session, "SELECT a FROM Artist a", {}, {}, std::nullopt)};
|
||||
|
||||
switch (sortMethod)
|
||||
{
|
||||
case Artist::SortMethod::None:
|
||||
break;
|
||||
case Artist::SortMethod::ByName:
|
||||
query.orderBy("a.name COLLATE NOCASE");
|
||||
break;
|
||||
case Artist::SortMethod::BySortName:
|
||||
query.orderBy("a.sort_name COLLATE NOCASE");
|
||||
break;
|
||||
}
|
||||
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> collection = query
|
||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1);
|
||||
|
||||
std::vector<Artist::pointer> res (collection.begin(), collection.end());
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
res.pop_back();
|
||||
}
|
||||
else
|
||||
moreResults = false;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<ArtistId>
|
||||
Artist::getAllIds(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<ArtistId> res = session.getDboSession().query<ArtistId>("SELECT id FROM artist");
|
||||
return std::vector<ArtistId>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<ArtistId>
|
||||
Artist::getAllIdsRandom(Session& session, const std::vector<ClusterId>& clusters, std::optional<TrackArtistLinkType> linkType, std::optional<std::size_t> size)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {createQuery<ArtistId>(session, "SELECT DISTINCT a.id from artist a", clusters, {}, linkType)};
|
||||
|
||||
Wt::Dbo::collection<ArtistId> res = query
|
||||
.orderBy("RANDOM()")
|
||||
.limit(size ? static_cast<int>(*size) : -1);
|
||||
|
||||
return std::vector<ArtistId>(res.begin(), res.end());
|
||||
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer>
|
||||
Artist::getAllOrphans(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> res {session.getDboSession().query<Wt::Dbo::ptr<Artist>>("SELECT DISTINCT a FROM artist a WHERE NOT EXISTS(SELECT 1 FROM track t INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id WHERE t.id = t_a_l.track_id)")};
|
||||
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<ArtistId>
|
||||
Artist::getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<ArtistId> res = session.getDboSession().query<ArtistId>
|
||||
("SELECT DISTINCT a.id FROM artist a"
|
||||
" INNER JOIN track t ON t.id = t_a_l.track_id INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.id"
|
||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id")
|
||||
.limit(limit ? static_cast<int>(*limit) : -1);
|
||||
|
||||
return std::vector<ArtistId>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer>
|
||||
Artist::getByClusters(Session& session, const std::vector<ClusterId>& clusters, SortMethod sortMethod)
|
||||
{
|
||||
assert(!clusters.empty());
|
||||
|
||||
session.checkSharedLocked();
|
||||
bool more{};
|
||||
return getByFilter(session, clusters, {}, std::nullopt, sortMethod, std::nullopt, more);
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer>
|
||||
Artist::getByFilter(Session& session,
|
||||
const std::vector<ClusterId>& clusters,
|
||||
const std::vector<std::string_view>& keywords,
|
||||
std::optional<TrackArtistLinkType> linkType,
|
||||
SortMethod sortMethod,
|
||||
std::optional<Range> range,
|
||||
bool& moreResults)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {createQuery<Wt::Dbo::ptr<Artist>>(session, "SELECT DISTINCT a from artist a", clusters, keywords, linkType)};
|
||||
switch (sortMethod)
|
||||
{
|
||||
case Artist::SortMethod::None:
|
||||
break;
|
||||
case Artist::SortMethod::ByName:
|
||||
query.orderBy("a.name COLLATE NOCASE");
|
||||
break;
|
||||
case Artist::SortMethod::BySortName:
|
||||
query.orderBy("a.sort_name COLLATE NOCASE");
|
||||
break;
|
||||
}
|
||||
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> collection = query
|
||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1);
|
||||
|
||||
std::vector<pointer> res (collection.begin(), collection.end());
|
||||
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
res.pop_back();
|
||||
}
|
||||
else
|
||||
moreResults = false;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer>
|
||||
Artist::getLastWritten(Session& session,
|
||||
std::optional<Wt::WDateTime> after,
|
||||
const std::vector<ClusterId>& clusters,
|
||||
std::optional<TrackArtistLinkType> linkType,
|
||||
std::optional<Range> range, bool& moreResults)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {createQuery<Wt::Dbo::ptr<Artist>>(session, "SELECT DISTINCT a from artist a", clusters, {}, linkType)};
|
||||
|
||||
if (after)
|
||||
query.where("t.file_last_write > ?").bind(*after);
|
||||
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> collection = query
|
||||
.orderBy("t.file_last_write DESC")
|
||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1);
|
||||
|
||||
std::vector<pointer> res (collection.begin(), collection.end());
|
||||
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
res.pop_back();
|
||||
}
|
||||
else
|
||||
moreResults = false;
|
||||
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer>
|
||||
Artist::getStarred(Session& session,
|
||||
User::pointer user,
|
||||
const std::vector<ClusterId>& clusters,
|
||||
std::optional<TrackArtistLinkType> linkType,
|
||||
SortMethod sortMethod,
|
||||
std::optional<Range> range, bool& moreResults)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {createQuery<Wt::Dbo::ptr<Artist>>(session, "SELECT DISTINCT a from artist a", clusters, {}, linkType)};
|
||||
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "a.id IN (SELECT DISTINCT a.id FROM artist a"
|
||||
" INNER JOIN user_artist_starred uas ON uas.artist_id = a.id"
|
||||
" INNER JOIN user u ON u.id = uas.user_id WHERE u.id = ?)";
|
||||
|
||||
query.bind(user->getId());
|
||||
query.where(oss.str());
|
||||
}
|
||||
|
||||
switch (sortMethod)
|
||||
{
|
||||
case Artist::SortMethod::None:
|
||||
break;
|
||||
case Artist::SortMethod::ByName:
|
||||
query.orderBy("name COLLATE NOCASE");
|
||||
break;
|
||||
case Artist::SortMethod::BySortName:
|
||||
query.orderBy("sort_name COLLATE NOCASE");
|
||||
break;
|
||||
}
|
||||
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> collection = query
|
||||
.groupBy("a.id")
|
||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1);
|
||||
|
||||
std::vector<pointer> res (collection.begin(), collection.end());
|
||||
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
res.pop_back();
|
||||
}
|
||||
else
|
||||
moreResults = false;
|
||||
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
auto query {createQuery(session, params)};
|
||||
return execQuery(query, params.range);
|
||||
}
|
||||
|
||||
std::vector<Release::pointer>
|
||||
@@ -456,32 +271,19 @@ Artist::getTracks(std::optional<TrackArtistLinkType> linkType) const
|
||||
return std::vector<Track::pointer>(tracks.begin(), tracks.end());
|
||||
}
|
||||
|
||||
std::vector<Track::pointer>
|
||||
Artist::getNonReleaseTracks(std::optional<TrackArtistLinkType> linkType, std::optional<Range> range, bool& moreResults) const
|
||||
RangeResults<Track::pointer>
|
||||
Artist::getNonReleaseTracks(std::optional<TrackArtistLinkType> linkType, Range range) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
auto query {session()->query<Wt::Dbo::ptr<Track>>("SELECT t FROM track t 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")
|
||||
.where("a.id = ?").bind(getId())
|
||||
.where("t.release_id is NULL")
|
||||
.orderBy("t.name")
|
||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1)};
|
||||
|
||||
.orderBy("t.name")};
|
||||
if (linkType)
|
||||
query.where("t_a_l.type = ?").bind(*linkType);
|
||||
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Track>> tracks {query.resultList()};
|
||||
std::vector<Track::pointer> res(tracks.begin(), tracks.end());
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
res.pop_back();
|
||||
}
|
||||
else
|
||||
moreResults = false;
|
||||
|
||||
return res;
|
||||
return execQuery(query, range);
|
||||
}
|
||||
|
||||
bool
|
||||
@@ -511,14 +313,14 @@ Artist::getRandomTracks(std::optional<std::size_t> count) const
|
||||
return std::vector<Track::pointer>(tracks.begin(), tracks.end());
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer>
|
||||
Artist::getSimilarArtists(EnumSet<TrackArtistLinkType> artistLinkTypes, std::optional<Range> range) const
|
||||
RangeResults<ArtistId>
|
||||
Artist::findSimilarArtists(EnumSet<TrackArtistLinkType> artistLinkTypes, Range range) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
std::ostringstream oss;
|
||||
oss <<
|
||||
"SELECT a FROM artist a"
|
||||
"SELECT a.id 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 track_cluster t_c ON t_c.track_id = t.id"
|
||||
@@ -547,19 +349,15 @@ Artist::getSimilarArtists(EnumSet<TrackArtistLinkType> artistLinkTypes, std::opt
|
||||
oss << ")";
|
||||
}
|
||||
|
||||
Wt::Dbo::Query<Wt::Dbo::ptr<Artist>> query {session()->query<Wt::Dbo::ptr<Artist>>(oss.str())
|
||||
auto query {session()->query<ArtistId>(oss.str())
|
||||
.bind(getId())
|
||||
.bind(getId())
|
||||
.groupBy("a.id")
|
||||
.orderBy("COUNT(*) DESC, RANDOM()")
|
||||
.limit(range ? static_cast<int>(range->limit) : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1)};
|
||||
|
||||
.orderBy("COUNT(*) DESC, RANDOM()")};
|
||||
for (TrackArtistLinkType type : artistLinkTypes)
|
||||
query.bind(type);
|
||||
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> res {query.resultList()};
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
return execQuery(query, range);
|
||||
}
|
||||
|
||||
std::vector<std::vector<Cluster::pointer>>
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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 "services/database/AuthToken.hpp"
|
||||
|
||||
#include <Wt/Dbo/WtSqlTraits.h>
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/User.hpp"
|
||||
#include "StringViewTraits.hpp"
|
||||
#include "IdTypeTraits.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
|
||||
AuthToken::AuthToken(std::string_view value, const Wt::WDateTime& expiry, ObjectPtr<User> user)
|
||||
: _value {value}
|
||||
, _expiry {expiry}
|
||||
, _user {getDboPtr(user)}
|
||||
{
|
||||
}
|
||||
|
||||
AuthToken::pointer
|
||||
AuthToken::create(Session& session, std::string_view value, const Wt::WDateTime& expiry, ObjectPtr<User> user)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
|
||||
AuthToken::pointer res {session.getDboSession().add(std::make_unique<AuthToken>(value, expiry, user))};
|
||||
session.getDboSession().flush();
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
void
|
||||
AuthToken::removeExpiredTokens(Session& session, const Wt::WDateTime& now)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
|
||||
session.getDboSession().execute("DELETE FROM auth_token WHERE expiry < ?").bind(now);
|
||||
}
|
||||
|
||||
AuthToken::pointer
|
||||
AuthToken::find(Session& session, std::string_view value)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().find<AuthToken>()
|
||||
.where("value = ?").bind(value)
|
||||
.resultValue();
|
||||
}
|
||||
}
|
||||
@@ -17,15 +17,16 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "database/Cluster.hpp"
|
||||
#include "services/database/Cluster.hpp"
|
||||
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Release.hpp"
|
||||
#include "database/ScanSettings.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "services/database/Artist.hpp"
|
||||
#include "services/database/Release.hpp"
|
||||
#include "services/database/ScanSettings.hpp"
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/Track.hpp"
|
||||
#include "IdTypeTraits.hpp"
|
||||
#include "SqlQuery.hpp"
|
||||
#include "Traits.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
@@ -35,6 +36,14 @@ Cluster::Cluster(ObjectPtr<ClusterType> type, std::string_view name)
|
||||
{
|
||||
}
|
||||
|
||||
std::size_t
|
||||
Cluster::getCount(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM cluster");
|
||||
}
|
||||
|
||||
Cluster::pointer
|
||||
Cluster::create(Session& session, ObjectPtr<ClusterType> type, std::string_view name)
|
||||
{
|
||||
@@ -46,25 +55,26 @@ Cluster::create(Session& session, ObjectPtr<ClusterType> type, std::string_view
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<Cluster::pointer>
|
||||
Cluster::getAll(Session& session)
|
||||
RangeResults<ClusterId>
|
||||
Cluster::find(Session& session, Range range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
auto query {session.getDboSession().query<ClusterId>("SELECT id FROM cluster")};
|
||||
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Cluster>> res {session.getDboSession().find<Cluster>()};
|
||||
return std::vector<Cluster::pointer>(res.begin(), res.end());
|
||||
return execQuery(query, range);
|
||||
}
|
||||
|
||||
std::vector<Cluster::pointer>
|
||||
Cluster::getAllOrphans(Session& session)
|
||||
RangeResults<ClusterId>
|
||||
Cluster::findOrphans(Session& session, Range range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
auto res {session.getDboSession().query<Wt::Dbo::ptr<Cluster>>("SELECT DISTINCT c FROM cluster c WHERE NOT EXISTS(SELECT 1 FROM track_cluster t_c WHERE t_c.cluster_id = c.id)").resultList()};
|
||||
return std::vector<Cluster::pointer>(res.begin(), res.end());
|
||||
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 execQuery(query, range);
|
||||
}
|
||||
|
||||
Cluster::pointer
|
||||
Cluster::getById(Session& session, ClusterId id)
|
||||
Cluster::find(Session& session, ClusterId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
@@ -77,29 +87,15 @@ Cluster::addTrack(ObjectPtr<Track> track)
|
||||
_tracks.insert(getDboPtr(track));
|
||||
}
|
||||
|
||||
std::vector<Track::pointer>
|
||||
Cluster::getTracks(std::optional<std::size_t> offset, std::optional<std::size_t> limit) const
|
||||
RangeResults<TrackId>
|
||||
Cluster::getTracks(Range range) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
auto res {session()->query<Wt::Dbo::ptr<Track>>("SELECT t 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())
|
||||
.offset(offset ? static_cast<int>(*offset) : -1)
|
||||
.limit(limit ? static_cast<int>(*limit) : -1)
|
||||
.resultList()};
|
||||
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 std::vector<Track::pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<TrackId>
|
||||
Cluster::getTrackIds() const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
Wt::Dbo::collection<TrackId> res = session()->query<TrackId>("SELECT t_c.track_id FROM track_cluster t_c INNER JOIN cluster c ON c.id = t_c.cluster_id")
|
||||
.where("c.id = ?").bind(getId());
|
||||
|
||||
return std::vector<TrackId>(res.begin(), res.end());
|
||||
return execQuery(query, range);
|
||||
}
|
||||
|
||||
std::size_t
|
||||
@@ -117,33 +113,42 @@ ClusterType::ClusterType(std::string_view name)
|
||||
{
|
||||
}
|
||||
|
||||
std::vector<ClusterType::pointer>
|
||||
ClusterType::getAllOrphans(Session& session)
|
||||
std::size_t
|
||||
ClusterType::getCount(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<ClusterType>> res = session.getDboSession().query<Wt::Dbo::ptr<ClusterType>>(
|
||||
"SELECT c_t from cluster_type c_t"
|
||||
" LEFT OUTER JOIN cluster c ON c_t.id = c.cluster_type_id")
|
||||
.where("c.id IS NULL");
|
||||
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM cluster_type");
|
||||
}
|
||||
|
||||
std::vector<ClusterType::pointer>
|
||||
ClusterType::getAllUsed(Session& session)
|
||||
|
||||
RangeResults<ClusterTypeId>
|
||||
ClusterType::findOrphans(Session& session, Range range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<ClusterType>> res = session.getDboSession().query<Wt::Dbo::ptr<ClusterType>>(
|
||||
"SELECT DISTINCT c_t from cluster_type c_t")
|
||||
.join("cluster c ON c_t.id = c.cluster_type_id");
|
||||
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 std::vector<pointer>(res.begin(), res.end());
|
||||
return execQuery(query, range);
|
||||
}
|
||||
|
||||
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")};
|
||||
|
||||
return execQuery(query, range);
|
||||
}
|
||||
|
||||
ClusterType::pointer
|
||||
ClusterType::getByName(Session& session, const std::string& name)
|
||||
ClusterType::find(Session& session, const std::string& name)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
@@ -151,20 +156,21 @@ ClusterType::getByName(Session& session, const std::string& name)
|
||||
}
|
||||
|
||||
ClusterType::pointer
|
||||
ClusterType::getById(Session& session, ClusterTypeId id)
|
||||
ClusterType::find(Session& session, ClusterTypeId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().find<ClusterType>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
|
||||
std::vector<ClusterType::pointer>
|
||||
ClusterType::getAll(Session& session)
|
||||
RangeResults<ClusterTypeId>
|
||||
ClusterType::find(Session& session, Range range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto res {session.getDboSession().find<ClusterType>().resultList()};
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
auto query {session.getDboSession().query<ClusterTypeId>("SELECT id from cluster_type")};
|
||||
|
||||
return execQuery(query, range);
|
||||
}
|
||||
|
||||
ClusterType::pointer
|
||||
@@ -17,19 +17,19 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "database/Db.hpp"
|
||||
#include "services/database/Db.hpp"
|
||||
|
||||
#include <Wt/Dbo/FixedSqlConnectionPool.h>
|
||||
#include <Wt/Dbo/backend/Sqlite3.h>
|
||||
|
||||
#include "database/Session.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/User.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
// Session living class handling the database and the login
|
||||
Db::Db(const std::filesystem::path& dbPath)
|
||||
Db::Db(const std::filesystem::path& dbPath, std::size_t connectionCount)
|
||||
{
|
||||
LMS_LOG(DB, INFO) << "Creating connection pool on file " << dbPath.string();
|
||||
|
||||
@@ -38,7 +38,7 @@ Db::Db(const std::filesystem::path& dbPath)
|
||||
connection->executeSql("pragma journal_mode=WAL");
|
||||
connection->executeSql("pragma synchronous=normal");
|
||||
|
||||
auto connectionPool = std::make_unique<Wt::Dbo::FixedSqlConnectionPool>(std::move(connection), 10);
|
||||
auto connectionPool = std::make_unique<Wt::Dbo::FixedSqlConnectionPool>(std::move(connection), connectionCount);
|
||||
connectionPool->setTimeout(std::chrono::seconds(10));
|
||||
|
||||
_connectionPool = std::move(connectionPool);
|
||||
+1
-1
@@ -22,7 +22,7 @@
|
||||
#include <type_traits>
|
||||
#include <Wt/Dbo/StdSqlTraits.h>
|
||||
|
||||
#include "database/Types.hpp"
|
||||
#include "services/database/Types.hpp"
|
||||
|
||||
namespace Wt::Dbo
|
||||
{
|
||||
@@ -0,0 +1,292 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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 "services/database/Listen.hpp"
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/Track.hpp"
|
||||
#include "services/database/User.hpp"
|
||||
#include "IdTypeTraits.hpp"
|
||||
#include "SqlQuery.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace
|
||||
{
|
||||
using namespace Database;
|
||||
|
||||
Wt::Dbo::Query<ArtistId>
|
||||
createArtistsQuery(Wt::Dbo::Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType)
|
||||
{
|
||||
auto query {session.query<ArtistId>("SELECT a.id from artist a")
|
||||
.join("track t ON t.id = t_a_l.track_id")
|
||||
.join("track_artist_link t_a_l ON t_a_l.artist_id = a.id")
|
||||
.join("listen l ON l.track_id = t.id")
|
||||
.where("l.user_id = ?").bind(userId)
|
||||
.where("l.scrobbler = ?").bind(scrobbler)};
|
||||
|
||||
if (linkType)
|
||||
query.where("t_a_l.type = ?").bind(*linkType);
|
||||
|
||||
if (!clusterIds.empty())
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "a.id IN (SELECT DISTINCT a.id FROM artist a"
|
||||
" INNER JOIN track t ON t.id = t_a_l.track_id"
|
||||
" INNER JOIN track_artist_link t_a_l ON t_a_l.artist_id = a.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 (auto id : clusterIds)
|
||||
{
|
||||
clusterClause.Or(WhereClause("c.id = ?"));
|
||||
query.bind(id);
|
||||
}
|
||||
|
||||
oss << " " << clusterClause.get();
|
||||
oss << " GROUP BY t.id,a.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size() << ")";
|
||||
|
||||
query.where(oss.str());
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
Wt::Dbo::Query<ReleaseId>
|
||||
createReleasesQuery(Wt::Dbo::Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds)
|
||||
{
|
||||
auto query {session.query<ReleaseId>("SELECT r.id from release r")
|
||||
.join("track t ON t.release_id = r.id")
|
||||
.join("listen l ON l.track_id = t.id")
|
||||
.where("l.user_id = ?").bind(userId)
|
||||
.where("l.scrobbler = ?").bind(scrobbler)};
|
||||
|
||||
if (!clusterIds.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 (ClusterId id : clusterIds)
|
||||
{
|
||||
clusterClause.Or(WhereClause("c.id = ?"));
|
||||
query.bind(id);
|
||||
}
|
||||
|
||||
oss << " " << clusterClause.get();
|
||||
oss << " GROUP BY t.id HAVING COUNT(DISTINCT c.id) = " << clusterIds.size() << ")";
|
||||
|
||||
query.where(oss.str());
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
Wt::Dbo::Query<TrackId>
|
||||
createTracksQuery(Wt::Dbo::Session& session, UserId userId, Scrobbler scrobbler, const std::vector<ClusterId>& clusterIds)
|
||||
{
|
||||
auto query {session.query<TrackId>("SELECT t.id from track t")
|
||||
.join("listen l ON l.track_id = t.id")
|
||||
.where("l.user_id = ?").bind(userId)
|
||||
.where("l.scrobbler = ?").bind(scrobbler)};
|
||||
|
||||
if (!clusterIds.empty())
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "t.id IN (SELECT DISTINCT t.id FROM track t"
|
||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
|
||||
" INNER JOIN cluster c ON c.id = t_c.cluster_id";
|
||||
|
||||
WhereClause clusterClause;
|
||||
for (auto id : clusterIds)
|
||||
{
|
||||
clusterClause.Or(WhereClause("c.id = ?")).bind(id.toString());
|
||||
query.bind(id);
|
||||
}
|
||||
|
||||
oss << " " << clusterClause.get();
|
||||
oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size() << ")";
|
||||
|
||||
query.where(oss.str());
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
}
|
||||
|
||||
namespace Database
|
||||
{
|
||||
Listen::Listen(ObjectPtr<User> user, ObjectPtr<Track> track, Scrobbler scrobbler, const Wt::WDateTime& dateTime)
|
||||
: _dateTime {Wt::WDateTime::fromTime_t(dateTime.toTime_t())}
|
||||
, _scrobbler {scrobbler}
|
||||
, _user {getDboPtr(user)}
|
||||
, _track {getDboPtr(track)}
|
||||
{}
|
||||
|
||||
std::size_t
|
||||
Listen::getCount(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM listen");
|
||||
}
|
||||
|
||||
Listen::pointer
|
||||
Listen::find(Session& session, ListenId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
return session.getDboSession().find<Listen>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
|
||||
RangeResults<ListenId>
|
||||
Listen::find(Session& session, const FindParameters& parameters)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {session.getDboSession().query<ListenId>("SELECT id FROM listen")
|
||||
.orderBy("date_time")};
|
||||
|
||||
if (parameters.user.isValid())
|
||||
query.where("user_id = ?").bind(parameters.user);
|
||||
|
||||
if (parameters.scrobbler)
|
||||
query.where("scrobbler = ?").bind(*parameters.scrobbler);
|
||||
|
||||
if (parameters.scrobblingState)
|
||||
query.where("scrobbling_state = ?").bind(*parameters.scrobblingState);
|
||||
|
||||
return execQuery(query, parameters.range);
|
||||
}
|
||||
|
||||
Listen::pointer
|
||||
Listen::find(Session& session, UserId userId, TrackId trackId, Scrobbler scrobbler, const Wt::WDateTime& dateTime)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().find<Listen>()
|
||||
.where("user_id = ?").bind(userId)
|
||||
.where("track_id = ?").bind(trackId)
|
||||
.where("scrobbler = ?").bind(scrobbler)
|
||||
.where("date_time = ?").bind(Wt::WDateTime::fromTime_t(dateTime.toTime_t()))
|
||||
.resultValue();
|
||||
}
|
||||
|
||||
Listen::pointer
|
||||
Listen::create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track, Scrobbler scrobbler, const Wt::WDateTime& dateTime)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
|
||||
Listen::pointer res {session.getDboSession().add(std::make_unique<Listen>(user, track, scrobbler, dateTime))};
|
||||
session.getDboSession().flush();
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
RangeResults<ArtistId>
|
||||
Listen::getTopArtists(Session& session,
|
||||
UserId userId,
|
||||
Scrobbler scrobbler,
|
||||
const std::vector<ClusterId>& clusterIds,
|
||||
std::optional<TrackArtistLinkType> linkType,
|
||||
Range range)
|
||||
{
|
||||
auto query {createArtistsQuery(session.getDboSession(), userId, scrobbler, clusterIds, linkType)};
|
||||
|
||||
auto collection {query
|
||||
.orderBy("COUNT(a.id) DESC")
|
||||
.groupBy("a.id")};
|
||||
|
||||
return execQuery(query, range);
|
||||
}
|
||||
|
||||
RangeResults<ReleaseId>
|
||||
Listen::getTopReleases(Session& session,
|
||||
UserId userId,
|
||||
Scrobbler scrobbler,
|
||||
const std::vector<ClusterId>& clusterIds,
|
||||
Range range)
|
||||
{
|
||||
auto query {createReleasesQuery(session.getDboSession(), userId, scrobbler, clusterIds)
|
||||
.orderBy("COUNT(r.id) DESC")
|
||||
.groupBy("r.id")};
|
||||
|
||||
return execQuery(query, range);
|
||||
}
|
||||
|
||||
RangeResults<TrackId>
|
||||
Listen::getTopTracks(Session& session,
|
||||
UserId userId,
|
||||
Scrobbler scrobbler,
|
||||
const std::vector<ClusterId>& clusterIds,
|
||||
Range range)
|
||||
{
|
||||
auto query {createTracksQuery(session.getDboSession(), userId, scrobbler, clusterIds)
|
||||
.orderBy("COUNT(t.id) DESC")
|
||||
.groupBy("t.id")};
|
||||
|
||||
return execQuery(query, range);
|
||||
}
|
||||
|
||||
RangeResults<ArtistId>
|
||||
Listen::getRecentArtists(Session& session,
|
||||
UserId userId,
|
||||
Scrobbler scrobbler,
|
||||
const std::vector<ClusterId>& clusterIds,
|
||||
std::optional<TrackArtistLinkType> linkType,
|
||||
Range range)
|
||||
{
|
||||
auto query {createArtistsQuery(session.getDboSession(), userId, scrobbler, clusterIds, linkType)
|
||||
.groupBy("a.id").having("l.date_time = MAX(l.date_time)")
|
||||
.orderBy("l.date_time DESC")};
|
||||
|
||||
return execQuery(query, range);
|
||||
}
|
||||
|
||||
RangeResults<ReleaseId>
|
||||
Listen::getRecentReleases(Session& session,
|
||||
UserId userId,
|
||||
Scrobbler scrobbler,
|
||||
const std::vector<ClusterId>& clusterIds,
|
||||
Range range)
|
||||
{
|
||||
auto query {createReleasesQuery(session.getDboSession(), userId, scrobbler, clusterIds)
|
||||
.groupBy("r.id").having("l.date_time = MAX(l.date_time)")
|
||||
.orderBy("l.date_time DESC")};
|
||||
|
||||
return execQuery(query, range);
|
||||
}
|
||||
|
||||
RangeResults<TrackId>
|
||||
Listen::getRecentTracks(Session& session,
|
||||
UserId userId,
|
||||
Scrobbler scrobbler,
|
||||
const std::vector<ClusterId>& clusterIds,
|
||||
Range range)
|
||||
{
|
||||
auto query {createTracksQuery(session.getDboSession(), userId, scrobbler, clusterIds)
|
||||
.groupBy("t.id").having("l.date_time = MAX(l.date_time)")
|
||||
.orderBy("l.date_time DESC")};
|
||||
|
||||
return execQuery(query, range);
|
||||
}
|
||||
|
||||
|
||||
} // namespace Database
|
||||
|
||||
@@ -0,0 +1,631 @@
|
||||
/*
|
||||
* 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 "Migration.hpp"
|
||||
|
||||
#include <Wt/Dbo/WtSqlTraits.h>
|
||||
|
||||
#include "services/database/Db.hpp"
|
||||
#include "services/database/ScanSettings.hpp"
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/User.hpp"
|
||||
#include "utils/Exception.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
VersionInfo::pointer
|
||||
VersionInfo::getOrCreate(Session& session)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
|
||||
pointer versionInfo {session.getDboSession().find<VersionInfo>()};
|
||||
if (!versionInfo)
|
||||
return session.getDboSession().add(std::make_unique<VersionInfo>());
|
||||
|
||||
return versionInfo;
|
||||
}
|
||||
|
||||
VersionInfo::pointer
|
||||
VersionInfo::get(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().find<VersionInfo>();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
namespace Database::Migration
|
||||
{
|
||||
class ScopedNoForeignKeys
|
||||
{
|
||||
public:
|
||||
ScopedNoForeignKeys(Db& db) : _db {db}
|
||||
{
|
||||
_db.executeSql("PRAGMA foreign_keys=OFF");
|
||||
}
|
||||
~ScopedNoForeignKeys()
|
||||
{
|
||||
_db.executeSql("PRAGMA foreign_keys=ON");
|
||||
}
|
||||
|
||||
ScopedNoForeignKeys(const ScopedNoForeignKeys&) = delete;
|
||||
ScopedNoForeignKeys(ScopedNoForeignKeys&&) = delete;
|
||||
ScopedNoForeignKeys& operator=(const ScopedNoForeignKeys&) = delete;
|
||||
ScopedNoForeignKeys& operator=(ScopedNoForeignKeys&&) = delete;
|
||||
|
||||
private:
|
||||
Db& _db;
|
||||
};
|
||||
|
||||
static
|
||||
std::string
|
||||
dateTimeToDbFormat(const Wt::WDateTime& dateTime)
|
||||
{
|
||||
return dateTime.toString("yyyy'-'MM'-'dd'T'hh':'mm':'ss'.000'", false).toUTF8();
|
||||
}
|
||||
|
||||
static
|
||||
void
|
||||
migrateFromV5(Session& session)
|
||||
{
|
||||
session.getDboSession().execute("DELETE FROM auth_token"); // format has changed
|
||||
}
|
||||
|
||||
static
|
||||
void
|
||||
migrateFromV6(Session& session)
|
||||
{
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(session).modify()->incScanVersion();
|
||||
}
|
||||
|
||||
|
||||
static
|
||||
void
|
||||
migrateFromV7(Session& session)
|
||||
{
|
||||
session.getDboSession().execute("DROP TABLE similarity_settings");
|
||||
session.getDboSession().execute("DROP TABLE similarity_settings_feature");
|
||||
session.getDboSession().execute("ALTER TABLE scan_settings ADD similarity_engine_type INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(ScanSettings::RecommendationEngineType::Clusters)) + ")");
|
||||
}
|
||||
|
||||
static
|
||||
void
|
||||
migrateFromV8(Session& session)
|
||||
{
|
||||
// Better cover handling, need to rescan the whole files
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(session).modify()->incScanVersion();
|
||||
}
|
||||
|
||||
static
|
||||
void
|
||||
migrateFromV9(Session& session)
|
||||
{
|
||||
session.getDboSession().execute(R"(
|
||||
CREATE TABLE IF NOT EXISTS "track_bookmark" (
|
||||
"id" integer primary key autoincrement,
|
||||
"version" integer not null,
|
||||
"offset" integer,
|
||||
"comment" text not null,
|
||||
"track_id" bigint,
|
||||
"user_id" bigint,
|
||||
constraint "fk_track_bookmark_track" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred,
|
||||
constraint "fk_track_bookmark_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred
|
||||
);)");
|
||||
}
|
||||
|
||||
static
|
||||
void
|
||||
migrateFromV10(Session& session)
|
||||
{
|
||||
ScanSettings::get(session).modify()->addAudioFileExtension(".m4b");
|
||||
ScanSettings::get(session).modify()->addAudioFileExtension(".alac");
|
||||
}
|
||||
|
||||
static
|
||||
void
|
||||
migrateFromV11(Session& session)
|
||||
{
|
||||
// Sanitize bad MBID, need to rescan the whole files
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(session).modify()->incScanVersion();
|
||||
}
|
||||
|
||||
static
|
||||
void
|
||||
migrateFromV12(Session& session)
|
||||
{
|
||||
// Artist and release that have a badly parsed name but a MBID had no chance to updat the name
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(session).modify()->incScanVersion();
|
||||
}
|
||||
|
||||
static
|
||||
void
|
||||
migrateFromV13(Session& session)
|
||||
{
|
||||
// Always store UUID in lower case + better WMA parsing
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(session).modify()->incScanVersion();
|
||||
}
|
||||
|
||||
static
|
||||
void
|
||||
migrateFromV14(Session& session)
|
||||
{
|
||||
// SortName now set from metadata
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(session).modify()->incScanVersion();
|
||||
}
|
||||
|
||||
static
|
||||
void
|
||||
migrateFromV15(Session& session)
|
||||
{
|
||||
session.getDboSession().execute("ALTER TABLE user ADD ui_theme INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(User::defaultUITheme)) + ")");
|
||||
}
|
||||
|
||||
static
|
||||
void
|
||||
migrateFromV16(Session& session)
|
||||
{
|
||||
session.getDboSession().execute("ALTER TABLE track ADD total_disc INTEGER NOT NULL DEFAULT(0)");
|
||||
session.getDboSession().execute("ALTER TABLE track ADD total_track INTEGER NOT NULL DEFAULT(0)");
|
||||
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(session).modify()->incScanVersion();
|
||||
}
|
||||
|
||||
static
|
||||
void
|
||||
migrateFromV17(Session& session)
|
||||
{
|
||||
// Drop colums total_disc/total_track from release
|
||||
session.getDboSession().execute(R"(
|
||||
CREATE TABLE "release_backup" (
|
||||
"id" integer primary key autoincrement,
|
||||
"version" integer not null,
|
||||
"name" text not null,
|
||||
"mbid" text not null
|
||||
))");
|
||||
session.getDboSession().execute("INSERT INTO release_backup SELECT id,version,name,mbid FROM release");
|
||||
session.getDboSession().execute("DROP TABLE release");
|
||||
session.getDboSession().execute("ALTER TABLE release_backup RENAME TO release");
|
||||
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(session).modify()->incScanVersion();
|
||||
}
|
||||
|
||||
|
||||
static
|
||||
void
|
||||
migrateFromV18(Session& session)
|
||||
{
|
||||
session.getDboSession().execute(R"(
|
||||
CREATE TABLE IF NOT EXISTS "subsonic_settings" (
|
||||
"id" integer primary key autoincrement,
|
||||
"version" integer not null,
|
||||
"api_enabled" boolean not null,
|
||||
"artist_list_mode" integer not null
|
||||
))");
|
||||
}
|
||||
|
||||
static
|
||||
void
|
||||
migrateFromV19(Session& session)
|
||||
{
|
||||
session.getDboSession().execute(R"(
|
||||
CREATE TABLE "user_backup" (
|
||||
"id" integer primary key autoincrement,
|
||||
"version" integer not null,
|
||||
"type" integer not null,
|
||||
"login_name" text not null,
|
||||
"password_salt" text not null,
|
||||
"password_hash" text not null,
|
||||
"last_login" text,
|
||||
"subsonic_transcode_enable" boolean not null,
|
||||
"subsonic_transcode_format" integer not null,
|
||||
"subsonic_transcode_bitrate" integer not null,
|
||||
"subsonic_artist_list_mode" integer not null,
|
||||
"ui_theme" integer not null,
|
||||
"cur_playing_track_pos" integer not null,
|
||||
"repeat_all" boolean not null,
|
||||
"radio" boolean not null
|
||||
))");
|
||||
session.getDboSession().execute(std::string {"INSERT INTO user_backup SELECT id, version, type, login_name, password_salt, password_hash, last_login, "}
|
||||
+ (User::defaultSubsonicTranscodeEnable ? "1" : "0")
|
||||
+ ", " + std::to_string(static_cast<int>(User::defaultSubsonicTranscodeFormat))
|
||||
+ ", " + std::to_string(User::defaultSubsonicTranscodeBitrate)
|
||||
+ ", " + std::to_string(static_cast<int>(User::defaultSubsonicArtistListMode))
|
||||
+ ", ui_theme, cur_playing_track_pos, repeat_all, radio FROM user");
|
||||
session.getDboSession().execute("DROP TABLE user");
|
||||
session.getDboSession().execute("ALTER TABLE user_backup RENAME TO user");
|
||||
}
|
||||
|
||||
static
|
||||
void
|
||||
migrateFromV20(Session& session)
|
||||
{
|
||||
session.getDboSession().execute("DROP TABLE subsonic_settings");
|
||||
}
|
||||
|
||||
static
|
||||
void
|
||||
migrateFromV21(Session& session)
|
||||
{
|
||||
session.getDboSession().execute("ALTER TABLE track ADD track_replay_gain REAL");
|
||||
session.getDboSession().execute("ALTER TABLE track ADD release_replay_gain REAL");
|
||||
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(session).modify()->incScanVersion();
|
||||
}
|
||||
|
||||
|
||||
static
|
||||
void
|
||||
migrateFromV22(Session& session)
|
||||
{
|
||||
session.getDboSession().execute("ALTER TABLE track ADD disc_subtitle TEXT NOT NULL DEFAULT ''");
|
||||
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(session).modify()->incScanVersion();
|
||||
}
|
||||
|
||||
|
||||
static
|
||||
void
|
||||
migrateFromV23(Session& session)
|
||||
{
|
||||
// Better cover detection
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(session).modify()->incScanVersion();
|
||||
}
|
||||
|
||||
static
|
||||
void
|
||||
migrateFromV24(Session& session)
|
||||
{
|
||||
// User's AuthMode
|
||||
session.getDboSession().execute("ALTER TABLE user ADD auth_mode INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(/*User::defaultAuthMode*/0)) + ")");
|
||||
}
|
||||
|
||||
static
|
||||
void
|
||||
migrateFromV25(Session& session)
|
||||
{
|
||||
// Better cover detection
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(session).modify()->incScanVersion();
|
||||
}
|
||||
|
||||
static
|
||||
void
|
||||
migrateFromV26(Session& session)
|
||||
{
|
||||
// Composer, mixer, etc. support
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(session).modify()->incScanVersion();
|
||||
}
|
||||
|
||||
static
|
||||
void
|
||||
migrateFromV27(Session& session)
|
||||
{
|
||||
// Composer, mixer, etc. support, now fallback on MBID tagged entries as there is no mean to provide MBID by tags for these kinf od artists
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(session).modify()->incScanVersion();
|
||||
}
|
||||
|
||||
static
|
||||
void
|
||||
migrateFromV28(Session& session)
|
||||
{
|
||||
// Drop Auth mode
|
||||
session.getDboSession().execute(R"(
|
||||
CREATE TABLE "user_backup" (
|
||||
"id" integer primary key autoincrement,
|
||||
"version" integer not null,
|
||||
"type" integer not null,
|
||||
"login_name" text not null,
|
||||
"password_salt" text not null,
|
||||
"password_hash" text not null,
|
||||
"last_login" text,
|
||||
"subsonic_transcode_enable" boolean not null,
|
||||
"subsonic_transcode_format" integer not null,
|
||||
"subsonic_transcode_bitrate" integer not null,
|
||||
"subsonic_artist_list_mode" integer not null,
|
||||
"ui_theme" integer not null,
|
||||
"cur_playing_track_pos" integer not null,
|
||||
"repeat_all" boolean not null,
|
||||
"radio" boolean not null
|
||||
))");
|
||||
session.getDboSession().execute("INSERT INTO user_backup SELECT id, version, type, login_name, password_salt, password_hash, last_login, subsonic_transcode_enable, subsonic_transcode_format, subsonic_transcode_bitrate, subsonic_artist_list_mode, ui_theme, cur_playing_track_pos, repeat_all, radio FROM user");
|
||||
session.getDboSession().execute("DROP TABLE user");
|
||||
session.getDboSession().execute("ALTER TABLE user_backup RENAME TO user");
|
||||
}
|
||||
|
||||
static
|
||||
void
|
||||
migrateFromV29(Session& session)
|
||||
{
|
||||
session.getDboSession().execute("ALTER TABLE tracklist_entry ADD date_time TEXT");
|
||||
session.getDboSession().execute("ALTER TABLE user ADD listenbrainz_token TEXT");
|
||||
session.getDboSession().execute("ALTER TABLE user ADD scrobbler INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(User::defaultScrobbler)) + ")");
|
||||
session.getDboSession().execute("ALTER TABLE track ADD recording_mbid TEXT");
|
||||
|
||||
session.getDboSession().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(session).modify()->incScanVersion();
|
||||
}
|
||||
|
||||
static
|
||||
void
|
||||
migrateFromV30(Session& session)
|
||||
{
|
||||
// drop "year" and "original_year" (rescan needed to convert them into dates)
|
||||
session.getDboSession().execute(R"(
|
||||
CREATE TABLE "track_backup" (
|
||||
"id" integer primary key autoincrement,
|
||||
"version" integer not null,
|
||||
"scan_version" integer not null,
|
||||
"track_number" integer not null,
|
||||
"disc_number" integer not null,
|
||||
"name" text not null,
|
||||
"duration" integer,
|
||||
"date" integer text,
|
||||
"original_date" integer text,
|
||||
"file_path" text not null,
|
||||
"file_last_write" text,
|
||||
"file_added" text,
|
||||
"has_cover" boolean not null,
|
||||
"mbid" text not null,
|
||||
"copyright" text not null,
|
||||
"copyright_url" text not null,
|
||||
"release_id" bigint, total_disc INTEGER NOT NULL DEFAULT(0), total_track INTEGER NOT NULL DEFAULT(0), track_replay_gain REAL, release_replay_gain REAL, disc_subtitle TEXT NOT NULL DEFAULT '', recording_mbid TEXT,
|
||||
constraint "fk_track_release" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred
|
||||
))");
|
||||
session.getDboSession().execute("INSERT INTO track_backup SELECT id, version, scan_version, track_number, disc_number, name, duration, \"1900-01-01\", \"1900-01-01\", file_path, file_last_write, file_added, has_cover, mbid, copyright, copyright_url, release_id, total_disc, total_track, track_replay_gain, release_replay_gain, disc_subtitle, recording_mbid FROM track");
|
||||
session.getDboSession().execute("DROP TABLE track");
|
||||
session.getDboSession().execute("ALTER TABLE track_backup RENAME TO track");
|
||||
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(session).modify()->incScanVersion();
|
||||
}
|
||||
|
||||
static
|
||||
void
|
||||
migrateFromV31(Session& session)
|
||||
{
|
||||
// new star system, using dedicated entries per scrobbler and date time
|
||||
session.getDboSession().execute(R"(
|
||||
CREATE TABLE "starred_artist" (
|
||||
"id" integer primary key autoincrement,
|
||||
"version" integer not null,
|
||||
"scrobbler" integer not null,
|
||||
"date_time" text,
|
||||
"artist_id" bigint,
|
||||
"user_id" bigint,
|
||||
constraint "fk_starred_artist_artist" foreign key ("artist_id") references "artist" ("id") on delete cascade deferrable initially deferred,
|
||||
constraint "fk_starred_artist_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred
|
||||
))");
|
||||
|
||||
session.getDboSession().execute(R"(
|
||||
CREATE TABLE "starred_release" (
|
||||
"id" integer primary key autoincrement,
|
||||
"version" integer not null,
|
||||
"scrobbler" integer not null,
|
||||
"date_time" text,
|
||||
"release_id" bigint,
|
||||
"user_id" bigint,
|
||||
constraint "fk_starred_release_release" foreign key ("release_id") references "release" ("id") on delete cascade deferrable initially deferred,
|
||||
constraint "fk_starred_release_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred
|
||||
))");
|
||||
|
||||
session.getDboSession().execute(R"(
|
||||
CREATE TABLE "starred_track" (
|
||||
"id" integer primary key autoincrement,
|
||||
"version" integer not null,
|
||||
"scrobbler" integer not null,
|
||||
"date_time" text,
|
||||
"track_id" bigint,
|
||||
"user_id" bigint,
|
||||
constraint "fk_starred_track_track" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred,
|
||||
constraint "fk_starred_track_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred
|
||||
))");
|
||||
|
||||
// Can't migrate using class mapping as mapping may evolve in the future
|
||||
|
||||
// use time_t to avoid rounding issues later
|
||||
const std::string now {dateTimeToDbFormat(Wt::WDateTime::fromTime_t(Wt::WDateTime::currentDateTime().toTime_t()))};
|
||||
|
||||
std::map<IdType::ValueType, Scrobbler> userScrobblers;
|
||||
auto getScrobbler {[&](IdType::ValueType userId)
|
||||
{
|
||||
auto itScrobbler {userScrobblers.find(userId)};
|
||||
if (itScrobbler != std::cend(userScrobblers))
|
||||
return itScrobbler->second;
|
||||
|
||||
auto query {session.getDboSession().query<Scrobbler>("SELECT scrobbler FROM user WHERE id = ?").bind(userId)};
|
||||
auto [itInserted, inserted] {userScrobblers.emplace(userId, query.resultValue())};
|
||||
assert(inserted);
|
||||
return itInserted->second;
|
||||
}};
|
||||
|
||||
auto migrateStarEntries {[&session, &getScrobbler, &now](const std::string& colName, const std::string& oldTableName, const std::string& newTableName)
|
||||
{
|
||||
using UserIdObjectId = std::tuple<IdType::ValueType /* userId */, IdType::ValueType /* entryId */>;
|
||||
|
||||
std::vector<UserIdObjectId> starredEntries;
|
||||
auto query {session.getDboSession().query<UserIdObjectId>("SELECT user_id, " + colName + " from " + oldTableName)};
|
||||
auto results {query.resultList()};
|
||||
|
||||
LMS_LOG(DB, INFO) << "Found " << results.size() << " " << colName << " to migrate";
|
||||
|
||||
for (const auto& [userId, entryId] : results)
|
||||
{
|
||||
session.getDboSession().execute("INSERT INTO " + newTableName + " ('version', 'scrobbler', 'date_time', '" + colName + "', 'user_id') VALUES (?, ?, ?, ?, ?)")
|
||||
.bind(0)
|
||||
.bind(getScrobbler(userId))
|
||||
.bind(now)
|
||||
.bind(entryId)
|
||||
.bind(userId);
|
||||
}
|
||||
|
||||
session.getDboSession().execute("DROP TABLE " + oldTableName);
|
||||
}};
|
||||
|
||||
migrateStarEntries("artist_id","user_artist_starred", "starred_artist");
|
||||
migrateStarEntries("release_id","user_release_starred", "starred_release");
|
||||
migrateStarEntries("track_id","user_track_starred", "starred_track");
|
||||
|
||||
// new listen system, no longer using tracklists
|
||||
session.getDboSession().execute(R"(
|
||||
CREATE TABLE "listen" (
|
||||
"id" integer primary key autoincrement,
|
||||
"version" integer not null,
|
||||
"date_time" text,
|
||||
"scrobbler" integer not null,
|
||||
"scrobbling_state" integer not null,
|
||||
"track_id" bigint,
|
||||
"user_id" bigint,
|
||||
constraint "fk_listen_track" foreign key ("track_id") references "track" ("id") on delete cascade deferrable initially deferred,
|
||||
constraint "fk_listen_user" foreign key ("user_id") references "user" ("id") on delete cascade deferrable initially deferred
|
||||
))");
|
||||
|
||||
auto migrateListens {[&session](const std::string& trackListName, Scrobbler scrobbler)
|
||||
{
|
||||
using UserIdObjectId = std::tuple<IdType::ValueType /* userId */, IdType::ValueType /* trackId */, Wt::WDateTime>;
|
||||
|
||||
std::vector<UserIdObjectId> listens;
|
||||
auto query {session.getDboSession().query<UserIdObjectId>("SELECT t_l.user_id, t_l_e.track_id, t_l_e.date_time FROM tracklist t_l")
|
||||
.join("tracklist_entry t_l_e ON t_l_e.tracklist_id = t_l.id")
|
||||
.where("t_l.name = ?").bind(trackListName)};
|
||||
auto results {query.resultList()};
|
||||
listens.reserve(results.size());
|
||||
|
||||
LMS_LOG(DB, INFO) << "Found " << results.size() << " listens in " << trackListName;
|
||||
|
||||
for (const auto& [userId, trackId, dateTime] : results)
|
||||
{
|
||||
session.getDboSession().execute("INSERT INTO listen ('version', 'date_time', 'scrobbler', 'scrobbling_state', 'track_id', 'user_id') VALUES (?, ?, ?, ?, ?, ?)")
|
||||
.bind(0)
|
||||
.bind(dateTimeToDbFormat(dateTime))
|
||||
.bind(scrobbler)
|
||||
.bind(ScrobblingState::Synchronized) // consider sync is done to avoid duplicate submissions
|
||||
.bind(trackId)
|
||||
.bind(userId);
|
||||
}
|
||||
}};
|
||||
|
||||
migrateListens("__scrobbler_internal_history__", Scrobbler::Internal);
|
||||
migrateListens("__scrobbler_listenbrainz_history__", Scrobbler::ListenBrainz);
|
||||
}
|
||||
|
||||
static
|
||||
void
|
||||
migrateFromV32(Session& session)
|
||||
{
|
||||
ScanSettings::get(session).modify()->addAudioFileExtension(".wv");
|
||||
}
|
||||
|
||||
void
|
||||
doDbMigration(Session& session)
|
||||
{
|
||||
static const std::string outdatedMsg {"Outdated database, please rebuild it (delete the .db file and restart)"};
|
||||
|
||||
ScopedNoForeignKeys noPragmaKeys {session.getDb()};
|
||||
|
||||
using MigrationFunction = std::function<void(Session&)>;
|
||||
|
||||
const std::map<unsigned, MigrationFunction> migrationFunctions
|
||||
{
|
||||
{5, migrateFromV5},
|
||||
{6, migrateFromV6},
|
||||
{7, migrateFromV7},
|
||||
{8, migrateFromV8},
|
||||
{9, migrateFromV9},
|
||||
{10, migrateFromV10},
|
||||
{11, migrateFromV11},
|
||||
{12, migrateFromV12},
|
||||
{13, migrateFromV13},
|
||||
{14, migrateFromV14},
|
||||
{15, migrateFromV15},
|
||||
{16, migrateFromV16},
|
||||
{17, migrateFromV17},
|
||||
{18, migrateFromV18},
|
||||
{19, migrateFromV19},
|
||||
{20, migrateFromV20},
|
||||
{21, migrateFromV21},
|
||||
{22, migrateFromV22},
|
||||
{23, migrateFromV23},
|
||||
{24, migrateFromV24},
|
||||
{25, migrateFromV25},
|
||||
{26, migrateFromV26},
|
||||
{27, migrateFromV27},
|
||||
{28, migrateFromV28},
|
||||
{29, migrateFromV29},
|
||||
{30, migrateFromV30},
|
||||
{31, migrateFromV31},
|
||||
{32, migrateFromV32},
|
||||
};
|
||||
|
||||
while (1)
|
||||
{
|
||||
auto uniqueTransaction {session.createUniqueTransaction()};
|
||||
|
||||
Version version;
|
||||
try
|
||||
{
|
||||
version = VersionInfo::getOrCreate(session)->getVersion();
|
||||
LMS_LOG(DB, INFO) << "Database version = " << version << ", LMS binary version = " << LMS_DATABASE_VERSION;
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
LMS_LOG(DB, ERROR) << "Cannot get database version info: " << e.what();
|
||||
throw LmsException {outdatedMsg};
|
||||
}
|
||||
|
||||
if (version == LMS_DATABASE_VERSION)
|
||||
{
|
||||
LMS_LOG(DB, DEBUG) << "Lms database version " << LMS_DATABASE_VERSION << ": up to date!";
|
||||
return;
|
||||
}
|
||||
else if (version > LMS_DATABASE_VERSION)
|
||||
{
|
||||
throw LmsException {"Server binary outdated, please upgrade it to handle this database"};
|
||||
}
|
||||
|
||||
if (version < migrationFunctions.begin()->first)
|
||||
throw LmsException {outdatedMsg};
|
||||
|
||||
LMS_LOG(DB, INFO) << "Migrating database from version " << version << "...";
|
||||
|
||||
auto itMigrationFunc {migrationFunctions.find(version)};
|
||||
assert(itMigrationFunc != std::cend(migrationFunctions));
|
||||
itMigrationFunc->second(session);
|
||||
|
||||
VersionInfo::get(session).modify()->setVersion(++version);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Session;
|
||||
|
||||
using Version = std::size_t;
|
||||
static constexpr Version LMS_DATABASE_VERSION {33};
|
||||
class VersionInfo
|
||||
{
|
||||
public:
|
||||
using pointer = Wt::Dbo::ptr<VersionInfo>;
|
||||
|
||||
static VersionInfo::pointer getOrCreate(Session& session);
|
||||
static VersionInfo::pointer get(Session& session);
|
||||
|
||||
Version getVersion() const { return _version; }
|
||||
void setVersion(Version version) { _version = static_cast<int>(version); }
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _version, "db_version");
|
||||
}
|
||||
|
||||
private:
|
||||
int _version {LMS_DATABASE_VERSION};
|
||||
};
|
||||
|
||||
namespace Migration
|
||||
{
|
||||
void doDbMigration(Session& session);
|
||||
}
|
||||
}
|
||||
@@ -17,39 +17,56 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "database/Release.hpp"
|
||||
#include "services/database/Release.hpp"
|
||||
|
||||
#include <Wt/Dbo/WtSqlTraits.h>
|
||||
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Cluster.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "services/database/Artist.hpp"
|
||||
#include "services/database/Cluster.hpp"
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/Track.hpp"
|
||||
#include "services/database/User.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "SqlQuery.hpp"
|
||||
#include "Traits.hpp"
|
||||
#include "IdTypeTraits.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
|
||||
template <typename T>
|
||||
static
|
||||
Wt::Dbo::Query<T>
|
||||
createQuery(Session& session,
|
||||
const std::string& queryStr,
|
||||
const std::vector<ClusterId>& clusterIds,
|
||||
const std::vector<std::string_view>& keywords)
|
||||
Wt::Dbo::Query<ReleaseId>
|
||||
createQuery(Session& session, const Release::FindParameters& params)
|
||||
{
|
||||
auto query {session.getDboSession().query<ReleaseId>("SELECT DISTINCT r.id from release r")};
|
||||
|
||||
auto query {session.getDboSession().query<T>(queryStr)};
|
||||
query.join("track t ON t.release_id = r.id");
|
||||
if (params.sortMethod == ReleaseSortMethod::LastWritten
|
||||
|| params.writtenAfter.isValid()
|
||||
|| params.dateRange)
|
||||
{
|
||||
query.join("track t ON t.release_id = r.id");
|
||||
}
|
||||
|
||||
for (std::string_view keyword : keywords)
|
||||
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("%" + escapeLikeKeyword(keyword) + "%");
|
||||
|
||||
if (!clusterIds.empty())
|
||||
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);
|
||||
}
|
||||
|
||||
if (!params.clusters.empty())
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "r.id IN (SELECT DISTINCT r.id FROM release r"
|
||||
@@ -58,18 +75,40 @@ createQuery(Session& session,
|
||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id";
|
||||
|
||||
WhereClause clusterClause;
|
||||
for (const ClusterId clusterId : clusterIds)
|
||||
for (const ClusterId clusterId : params.clusters)
|
||||
{
|
||||
clusterClause.Or(WhereClause("c.id = ?"));
|
||||
query.bind(clusterId);
|
||||
}
|
||||
|
||||
oss << " " << clusterClause.get();
|
||||
oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size() << ")";
|
||||
oss << " GROUP BY t.id HAVING COUNT(*) = " << params.clusters.size() << ")";
|
||||
|
||||
query.where(oss.str());
|
||||
}
|
||||
|
||||
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::StarredDateDesc:
|
||||
assert(params.starringUser.isValid());
|
||||
query.orderBy("s_r.date_time DESC");
|
||||
break;
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
@@ -80,7 +119,7 @@ _MBID {MBID ? MBID->getAsString() : ""}
|
||||
}
|
||||
|
||||
std::vector<Release::pointer>
|
||||
Release::getByName(Session& session, const std::string& name)
|
||||
Release::find(Session& session, const std::string& name)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
|
||||
@@ -93,7 +132,7 @@ Release::getByName(Session& session, const std::string& name)
|
||||
}
|
||||
|
||||
Release::pointer
|
||||
Release::getByMBID(Session& session, const UUID& mbid)
|
||||
Release::find(Session& session, const UUID& mbid)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
@@ -104,7 +143,7 @@ Release::getByMBID(Session& session, const UUID& mbid)
|
||||
}
|
||||
|
||||
Release::pointer
|
||||
Release::getById(Session& session, ReleaseId id)
|
||||
Release::find(Session& session, ReleaseId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
@@ -114,6 +153,13 @@ Release::getById(Session& session, ReleaseId 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;
|
||||
}
|
||||
|
||||
Release::pointer
|
||||
Release::create(Session& session, const std::string& name, const std::optional<UUID>& MBID)
|
||||
{
|
||||
@@ -130,230 +176,44 @@ Release::getCount(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().find<Release>().resultList().size();
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM release");
|
||||
}
|
||||
|
||||
std::vector<Release::pointer>
|
||||
Release::getAll(Session& session, std::optional<Range> range)
|
||||
RangeResults<ReleaseId>
|
||||
Release::findOrderedByArtist(Session& session, Range range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto res {session.getDboSession().find<Release>()
|
||||
.offset(range ? static_cast<int>(range->offset) : -1)
|
||||
.limit(range ? static_cast<int>(range->limit) : -1)
|
||||
.orderBy("name COLLATE NOCASE")
|
||||
.resultList()};
|
||||
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<ReleaseId>
|
||||
Release::getAllIds(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<ReleaseId> res = session.getDboSession().query<ReleaseId>("SELECT id FROM release");
|
||||
return std::vector<ReleaseId>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<Release::pointer>
|
||||
Release::getAllOrderedByArtist(Session& session, std::optional<std::size_t> offset, std::optional<std::size_t> size)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto res {session.getDboSession().query<Wt::Dbo::ptr<Release>>(
|
||||
"SELECT DISTINCT r FROM release r"
|
||||
// TODO merge with execQuery
|
||||
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")
|
||||
.offset(offset ? static_cast<int>(*offset) : -1)
|
||||
.limit(size ? static_cast<int>(*size) : -1)
|
||||
.orderBy("a.name COLLATE NOCASE, r.name COLLATE NOCASE")
|
||||
.resultList()};
|
||||
.orderBy("a.name COLLATE NOCASE, r.name COLLATE NOCASE")};
|
||||
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
return execQuery(query, range);
|
||||
}
|
||||
|
||||
std::vector<Release::pointer>
|
||||
Release::getAllRandom(Session& session, const std::vector<ClusterId>& clusterIds, std::optional<std::size_t> size)
|
||||
RangeResults<ReleaseId>
|
||||
Release::findOrphans(Session& session, Range range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {createQuery<Wt::Dbo::ptr<Release>>(session, "SELECT DISTINCT r from release r", clusterIds, {})};
|
||||
auto res {query
|
||||
.orderBy("RANDOM()")
|
||||
.limit(size ? static_cast<int>(*size) : -1)
|
||||
.resultList()};
|
||||
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
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 execQuery(query, range);
|
||||
}
|
||||
|
||||
std::vector<ReleaseId>
|
||||
Release::getAllIdsRandom(Session& session, const std::vector<ClusterId>& clusterIds, std::optional<std::size_t> size)
|
||||
RangeResults<ReleaseId>
|
||||
Release::find(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {createQuery<ReleaseId>(session, "SELECT DISTINCT r.id from release r", clusterIds, {})};
|
||||
auto query {createQuery(session, params)};
|
||||
|
||||
Wt::Dbo::collection<ReleaseId> res = query
|
||||
.orderBy("RANDOM()")
|
||||
.limit(size ? static_cast<int>(*size) : -1);
|
||||
|
||||
return std::vector<ReleaseId>(res.begin(), res.end());
|
||||
return execQuery(query, params.range);
|
||||
}
|
||||
|
||||
|
||||
std::vector<Release::pointer>
|
||||
Release::getAllOrphans(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto res {session.getDboSession().query<Wt::Dbo::ptr<Release>>("select r from release r LEFT OUTER JOIN Track t ON r.id = t.release_id WHERE t.id IS NULL").resultList()};
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<Release::pointer>
|
||||
Release::getLastWritten(Session& session,
|
||||
std::optional<Wt::WDateTime> after,
|
||||
const std::vector<ClusterId>& clusterIds,
|
||||
std::optional<Range> range,
|
||||
bool& moreResults)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {createQuery<Wt::Dbo::ptr<Release>>(session, "SELECT r from release r", clusterIds, {})};
|
||||
if (after)
|
||||
query.where("t.file_last_write > ?").bind(after);
|
||||
|
||||
auto collection {query
|
||||
.orderBy("t.file_last_write DESC")
|
||||
.groupBy("r.id")
|
||||
.offset(range ? static_cast<int>(range->offset) : -1)
|
||||
.limit(range ? static_cast<int>(range->limit) + 1: -1)
|
||||
.resultList()};
|
||||
|
||||
std::vector<pointer> res(collection.begin(), collection.end());
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
res.pop_back();
|
||||
}
|
||||
else
|
||||
moreResults = false;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<Release::pointer>
|
||||
Release::getByYear(Session& session, int yearFrom, int yearTo, std::optional<Range> range)
|
||||
{
|
||||
auto res {session.getDboSession().query<Wt::Dbo::ptr<Release>>
|
||||
("SELECT DISTINCT r from release r INNER JOIN track t ON r.id = t.release_id")
|
||||
.where("t.date >= ?").bind(Wt::WDate {yearFrom, 1, 1})
|
||||
.where("t.date <= ?").bind(Wt::WDate {yearTo, 12, 31})
|
||||
.orderBy("t.date, r.name COLLATE NOCASE")
|
||||
.offset(range ? static_cast<int>(range->offset) : -1)
|
||||
.limit(range ? static_cast<int>(range->limit) : -1)
|
||||
.resultList()};
|
||||
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<Release::pointer>
|
||||
Release::getStarred(Session& session,
|
||||
User::pointer user,
|
||||
const std::vector<ClusterId>& clusterIds,
|
||||
std::optional<Range> range,
|
||||
bool& moreResults)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {createQuery<Wt::Dbo::ptr<Release>>(session, "SELECT r from release r", clusterIds, {})};
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "r.id IN (SELECT DISTINCT r.id FROM release r"
|
||||
" INNER JOIN user_release_starred urs ON urs.release_id = r.id"
|
||||
" INNER JOIN user u ON u.id = urs.user_id WHERE u.id = ?)";
|
||||
|
||||
query.bind(user->getId());
|
||||
query.where(oss.str());
|
||||
}
|
||||
|
||||
auto collection {query
|
||||
.groupBy("r.id")
|
||||
.orderBy("r.name COLLATE NOCASE")
|
||||
.offset(range ? static_cast<int>(range->offset) : -1)
|
||||
.limit(range ? static_cast<int>(range->limit) + 1: -1)
|
||||
.resultList()};
|
||||
|
||||
std::vector<pointer> res(collection.begin(), collection.end());
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
res.pop_back();
|
||||
}
|
||||
else
|
||||
moreResults = false;
|
||||
|
||||
return res;
|
||||
|
||||
|
||||
}
|
||||
|
||||
std::vector<Release::pointer>
|
||||
Release::getByClusters(Session& session, const std::vector<ClusterId>& clusters)
|
||||
{
|
||||
assert(!clusters.empty());
|
||||
|
||||
session.checkSharedLocked();
|
||||
|
||||
bool moreResults;
|
||||
return getByFilter(session, clusters, {}, std::nullopt, moreResults);
|
||||
}
|
||||
|
||||
std::vector<Release::pointer>
|
||||
Release::getByFilter(Session& session,
|
||||
const std::vector<ClusterId>& clusterIds,
|
||||
const std::vector<std::string_view>& keywords,
|
||||
std::optional<Range> range,
|
||||
bool& moreResults)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto collection {createQuery<Wt::Dbo::ptr<Release>>(session, "SELECT r from release r", clusterIds, keywords)
|
||||
.groupBy("r.id")
|
||||
.orderBy("r.name COLLATE NOCASE")
|
||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1)
|
||||
.resultList()};
|
||||
|
||||
std::vector<pointer> res(collection.begin(), collection.end());
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
res.pop_back();
|
||||
}
|
||||
else
|
||||
moreResults = false;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<ReleaseId>
|
||||
Release::getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<ReleaseId> res = session.getDboSession().query<ReleaseId>
|
||||
("SELECT DISTINCT r.id 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")
|
||||
.limit(limit ? static_cast<int>(*limit) : -1);
|
||||
|
||||
return std::vector<ReleaseId>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
|
||||
std::optional<std::size_t>
|
||||
Release::getTotalTrack(void) const
|
||||
{
|
||||
+4
-4
@@ -17,7 +17,7 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "database/ScanSettings.hpp"
|
||||
#include "services/database/ScanSettings.hpp"
|
||||
|
||||
#include <Wt/Dbo/WtSqlTraits.h>
|
||||
|
||||
@@ -25,8 +25,8 @@
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/String.hpp"
|
||||
|
||||
#include "database/Cluster.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "services/database/Cluster.hpp"
|
||||
#include "services/database/Session.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -111,7 +111,7 @@ ScanSettings::setClusterTypes(Session& session, const std::set<std::string>& clu
|
||||
// Create any missing cluster type
|
||||
for (const std::string& clusterTypeName : clusterTypeNames)
|
||||
{
|
||||
ClusterType::pointer clusterType {ClusterType::getByName(session, clusterTypeName)};
|
||||
ClusterType::pointer clusterType {ClusterType::find(session, clusterTypeName)};
|
||||
if (!clusterType)
|
||||
{
|
||||
LMS_LOG(DB, INFO) << "Creating cluster type " << clusterTypeName;
|
||||
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
* 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 "services/database/Session.hpp"
|
||||
|
||||
#include <cassert>
|
||||
|
||||
#include "utils/Exception.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
#include "services/database/Artist.hpp"
|
||||
#include "services/database/AuthToken.hpp"
|
||||
#include "services/database/Cluster.hpp"
|
||||
#include "services/database/Db.hpp"
|
||||
#include "services/database/Listen.hpp"
|
||||
#include "services/database/Release.hpp"
|
||||
#include "services/database/ScanSettings.hpp"
|
||||
#include "services/database/StarredArtist.hpp"
|
||||
#include "services/database/StarredRelease.hpp"
|
||||
#include "services/database/StarredTrack.hpp"
|
||||
#include "services/database/Track.hpp"
|
||||
#include "services/database/TrackBookmark.hpp"
|
||||
#include "services/database/TrackArtistLink.hpp"
|
||||
#include "services/database/TrackList.hpp"
|
||||
#include "services/database/TrackFeatures.hpp"
|
||||
#include "services/database/User.hpp"
|
||||
#include "Migration.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
|
||||
Session::Session(Db& db)
|
||||
: _db {db}
|
||||
{
|
||||
_session.setConnectionPool(_db.getConnectionPool());
|
||||
|
||||
_session.mapClass<VersionInfo>("version_info");
|
||||
_session.mapClass<Artist>("artist");
|
||||
_session.mapClass<AuthToken>("auth_token");
|
||||
_session.mapClass<Cluster>("cluster");
|
||||
_session.mapClass<ClusterType>("cluster_type");
|
||||
_session.mapClass<Listen>("listen");
|
||||
_session.mapClass<Release>("release");
|
||||
_session.mapClass<ScanSettings>("scan_settings");
|
||||
_session.mapClass<StarredArtist>("starred_artist");
|
||||
_session.mapClass<StarredRelease>("starred_release");
|
||||
_session.mapClass<StarredTrack>("starred_track");
|
||||
_session.mapClass<Track>("track");
|
||||
_session.mapClass<TrackBookmark>("track_bookmark");
|
||||
_session.mapClass<TrackArtistLink>("track_artist_link");
|
||||
_session.mapClass<TrackFeatures>("track_features");
|
||||
_session.mapClass<TrackList>("tracklist");
|
||||
_session.mapClass<TrackListEntry>("tracklist_entry");
|
||||
_session.mapClass<User>("user");
|
||||
}
|
||||
|
||||
UniqueTransaction::UniqueTransaction(RecursiveSharedMutex& mutex, Wt::Dbo::Session& session)
|
||||
: _lock {mutex},
|
||||
_transaction {session}
|
||||
{
|
||||
}
|
||||
|
||||
SharedTransaction::SharedTransaction(RecursiveSharedMutex& mutex, Wt::Dbo::Session& session)
|
||||
: _lock {mutex},
|
||||
_transaction {session}
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
Session::checkUniqueLocked()
|
||||
{
|
||||
assert(_db.getMutex().isUniqueLocked());
|
||||
}
|
||||
|
||||
void
|
||||
Session::checkSharedLocked()
|
||||
{
|
||||
assert(_db.getMutex().isSharedLocked());
|
||||
}
|
||||
|
||||
UniqueTransaction
|
||||
Session::createUniqueTransaction()
|
||||
{
|
||||
return UniqueTransaction {_db.getMutex(), _session};
|
||||
}
|
||||
|
||||
SharedTransaction
|
||||
Session::createSharedTransaction()
|
||||
{
|
||||
return SharedTransaction {_db.getMutex(), _session};
|
||||
}
|
||||
|
||||
void
|
||||
Session::prepareTables()
|
||||
{
|
||||
// Creation case
|
||||
try
|
||||
{
|
||||
_session.createTables();
|
||||
|
||||
LMS_LOG(DB, INFO) << "Tables created";
|
||||
}
|
||||
catch (Wt::Dbo::Exception& e)
|
||||
{
|
||||
LMS_LOG(DB, ERROR) << "Cannot create tables: " << e.what();
|
||||
}
|
||||
|
||||
Migration::doDbMigration(*this);
|
||||
|
||||
// Indexes
|
||||
{
|
||||
auto uniqueTransaction {createUniqueTransaction()};
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS artist_name_idx ON artist(name)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS artist_sort_name_nocase_idx ON artist(sort_name COLLATE NOCASE)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS artist_mbid_idx ON artist(mbid)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS auth_token_user_idx ON auth_token(user_id)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS auth_token_expiry_idx ON auth_token(expiry)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS auth_token_value_idx ON auth_token(value)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS cluster_name_idx ON cluster(name)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS cluster_cluster_type_idx ON cluster(cluster_type_id)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS cluster_type_name_idx ON cluster_type(name)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS release_name_idx ON release(name)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS release_name_nocase_idx ON release(name COLLATE NOCASE)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS release_mbid_idx ON release(mbid)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS track_file_last_write_idx ON track(file_last_write)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS track_path_idx ON track(file_path)");
|
||||
_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_date_idx ON track(date)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS track_original_date_idx ON track(original_date)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS tracklist_name_idx ON tracklist(name)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS tracklist_user_idx ON tracklist(user_id)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS track_features_track_idx ON track_features(track_id)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_artist_idx ON track_artist_link(artist_id)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_name_idx ON track_artist_link(name)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_track_idx ON track_artist_link(track_id)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS track_artist_link_type_idx ON track_artist_link(type)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_idx ON track_bookmark(user_id)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS track_bookmark_user_track_idx ON track_bookmark(user_id,track_id)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS listen_scrobbler_idx ON listen(scrobbler)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS listen_user_scrobbler_idx ON listen(user_id,scrobbler)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS starred_artist_user_scrobbler_idx ON starred_artist(user_id,scrobbler)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS starred_release_user_scrobbler_idx ON starred_release(user_id,scrobbler)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS starred_track_user_scrobbler_idx ON starred_track(user_id,scrobbler)");
|
||||
}
|
||||
|
||||
// Initial settings tables
|
||||
{
|
||||
auto uniqueTransaction {createUniqueTransaction()};
|
||||
|
||||
ScanSettings::init(*this);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
Session::optimize()
|
||||
{
|
||||
LMS_LOG(DB, DEBUG) << "Optimizing db...";
|
||||
{
|
||||
auto uniqueTransaction {createUniqueTransaction()};
|
||||
_session.execute("ANALYZE");
|
||||
}
|
||||
LMS_LOG(DB, DEBUG) << "Optimized db!";
|
||||
}
|
||||
|
||||
} // namespace Database
|
||||
@@ -26,7 +26,6 @@
|
||||
class WhereClause
|
||||
{
|
||||
public:
|
||||
|
||||
WhereClause() {}
|
||||
WhereClause(const std::string& clause) { _clause = clause; }
|
||||
|
||||
@@ -40,16 +39,13 @@ class WhereClause
|
||||
const std::list<std::string>& getBindArgs(void) const {return _bindArgs;}
|
||||
|
||||
private:
|
||||
|
||||
std::string _clause; // WHERE clause
|
||||
std::list<std::string> _bindArgs;
|
||||
|
||||
};
|
||||
|
||||
class InnerJoinClause
|
||||
{
|
||||
public:
|
||||
|
||||
InnerJoinClause() {}
|
||||
InnerJoinClause(const std::string& clause);
|
||||
|
||||
@@ -57,7 +53,6 @@ class InnerJoinClause
|
||||
std::string get() const { return _clause;}
|
||||
|
||||
private:
|
||||
|
||||
std::string _clause;
|
||||
};
|
||||
|
||||
@@ -73,7 +68,6 @@ class GroupByStatement
|
||||
|
||||
private:
|
||||
std::string _statement; // SELECT statement
|
||||
|
||||
};
|
||||
|
||||
class SelectStatement
|
||||
@@ -87,14 +81,12 @@ class SelectStatement
|
||||
std::string get() const;
|
||||
|
||||
private:
|
||||
|
||||
std::list<std::string> _statement;
|
||||
};
|
||||
|
||||
class FromClause
|
||||
{
|
||||
public:
|
||||
|
||||
FromClause() {}
|
||||
FromClause(const std::string& clause);
|
||||
|
||||
@@ -103,15 +95,12 @@ class FromClause
|
||||
std::string get() const;
|
||||
|
||||
private:
|
||||
|
||||
std::list<std::string> _clause;
|
||||
|
||||
};
|
||||
|
||||
class SqlQuery
|
||||
{
|
||||
public:
|
||||
|
||||
SelectStatement& select(void) { return _selectStatement;}
|
||||
SelectStatement& select(const std::string& statement) { _selectStatement = SelectStatement(statement); return _selectStatement; }
|
||||
FromClause& from(void) { return _fromClause; }
|
||||
@@ -122,10 +111,9 @@ class SqlQuery
|
||||
GroupByStatement& groupBy(void) { return _groupByStatement; }
|
||||
const GroupByStatement& groupBy(void) const { return _groupByStatement; }
|
||||
|
||||
std::string get(void) const;
|
||||
std::string get() const;
|
||||
|
||||
private:
|
||||
|
||||
SelectStatement _selectStatement; // SELECT statement
|
||||
InnerJoinClause _innerJoinClause; // INNER JOIN
|
||||
FromClause _fromClause; // FROM tables
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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 "services/database/StarredArtist.hpp"
|
||||
|
||||
#include <Wt/Dbo/WtSqlTraits.h>
|
||||
|
||||
#include "services/database/Artist.hpp"
|
||||
#include "services/database/User.hpp"
|
||||
#include "IdTypeTraits.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
StarredArtist::StarredArtist(ObjectPtr<Artist> artist, ObjectPtr<User> user, Scrobbler scrobbler)
|
||||
: _scrobbler {scrobbler}
|
||||
, _artist {getDboPtr(artist)}
|
||||
, _user {getDboPtr(user)}
|
||||
{
|
||||
}
|
||||
|
||||
std::size_t
|
||||
StarredArtist::getCount(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM starred_artist");
|
||||
}
|
||||
|
||||
StarredArtist::pointer
|
||||
StarredArtist::find(Session& session, StarredArtistId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
return session.getDboSession().find<StarredArtist>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
|
||||
StarredArtist::pointer
|
||||
StarredArtist::find(Session& session, ArtistId artistId, UserId userId, Scrobbler scrobbler)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
return session.getDboSession().find<StarredArtist>()
|
||||
.where("artist_id = ?").bind(artistId)
|
||||
.where("user_id = ?").bind(userId)
|
||||
.where("scrobbler = ?").bind(scrobbler)
|
||||
.resultValue();
|
||||
}
|
||||
|
||||
StarredArtist::pointer
|
||||
StarredArtist::create(Session& session, ObjectPtr<Artist> artist, ObjectPtr<User> user, Scrobbler scrobbler)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
|
||||
StarredArtist::pointer res {session.getDboSession().add(std::make_unique<StarredArtist>(artist, user, scrobbler))};
|
||||
session.getDboSession().flush();
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
void
|
||||
StarredArtist::setDateTime(const Wt::WDateTime& dateTime)
|
||||
{
|
||||
_dateTime = normalizeDateTime(dateTime);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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 "services/database/StarredRelease.hpp"
|
||||
|
||||
#include <Wt/Dbo/WtSqlTraits.h>
|
||||
|
||||
#include "services/database/Release.hpp"
|
||||
#include "services/database/User.hpp"
|
||||
#include "IdTypeTraits.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
StarredRelease::StarredRelease(ObjectPtr<Release> release, ObjectPtr<User> user, Scrobbler scrobbler)
|
||||
: _scrobbler {scrobbler}
|
||||
, _release {getDboPtr(release)}
|
||||
, _user {getDboPtr(user)}
|
||||
{
|
||||
}
|
||||
|
||||
std::size_t
|
||||
StarredRelease::getCount(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM starred_release");
|
||||
}
|
||||
|
||||
StarredRelease::pointer
|
||||
StarredRelease::find(Session& session, StarredReleaseId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
return session.getDboSession().find<StarredRelease>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
|
||||
StarredRelease::pointer
|
||||
StarredRelease::find(Session& session, ReleaseId releaseId, UserId userId, Scrobbler scrobbler)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
return session.getDboSession().find<StarredRelease>()
|
||||
.where("release_id = ?").bind(releaseId)
|
||||
.where("user_id = ?").bind(userId)
|
||||
.where("scrobbler = ?").bind(scrobbler)
|
||||
.resultValue();
|
||||
}
|
||||
|
||||
StarredRelease::pointer
|
||||
StarredRelease::create(Session& session, ObjectPtr<Release> release, ObjectPtr<User> user, Scrobbler scrobbler)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
|
||||
StarredRelease::pointer res {session.getDboSession().add(std::make_unique<StarredRelease>(release, user, scrobbler))};
|
||||
session.getDboSession().flush();
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
void
|
||||
StarredRelease::setDateTime(const Wt::WDateTime& dateTime)
|
||||
{
|
||||
_dateTime = normalizeDateTime(dateTime);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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 "services/database/StarredTrack.hpp"
|
||||
|
||||
#include <Wt/Dbo/WtSqlTraits.h>
|
||||
|
||||
#include "services/database/Track.hpp"
|
||||
#include "services/database/User.hpp"
|
||||
#include "IdTypeTraits.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
StarredTrack::StarredTrack(ObjectPtr<Track> track, ObjectPtr<User> user, Scrobbler scrobbler)
|
||||
: _scrobbler {scrobbler}
|
||||
, _track {getDboPtr(track)}
|
||||
, _user {getDboPtr(user)}
|
||||
{
|
||||
}
|
||||
|
||||
std::size_t
|
||||
StarredTrack::getCount(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM starred_track");
|
||||
}
|
||||
|
||||
StarredTrack::pointer
|
||||
StarredTrack::find(Session& session, StarredTrackId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
return session.getDboSession().find<StarredTrack>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
|
||||
StarredTrack::pointer
|
||||
StarredTrack::find(Session& session, TrackId trackId, UserId userId, Scrobbler scrobbler)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
return session.getDboSession().find<StarredTrack>()
|
||||
.where("track_id = ?").bind(trackId)
|
||||
.where("user_id = ?").bind(userId)
|
||||
.where("scrobbler = ?").bind(scrobbler)
|
||||
.resultValue();
|
||||
}
|
||||
|
||||
StarredTrack::pointer
|
||||
StarredTrack::create(Session& session, ObjectPtr<Track> track, ObjectPtr<User> user, Scrobbler scrobbler)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
|
||||
StarredTrack::pointer res {session.getDboSession().add(std::make_unique<StarredTrack>(track, user, scrobbler))};
|
||||
session.getDboSession().flush();
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
void
|
||||
StarredTrack::setDateTime(const Wt::WDateTime& dateTime)
|
||||
{
|
||||
_dateTime = normalizeDateTime(dateTime);
|
||||
}
|
||||
}
|
||||
@@ -17,42 +17,49 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "database/Track.hpp"
|
||||
#include "services/database/Track.hpp"
|
||||
|
||||
#include <Wt/Dbo/WtSqlTraits.h>
|
||||
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Cluster.hpp"
|
||||
#include "database/Release.hpp"
|
||||
#include "database/TrackArtistLink.hpp"
|
||||
#include "database/TrackFeatures.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "services/database/Artist.hpp"
|
||||
#include "services/database/Cluster.hpp"
|
||||
#include "services/database/Release.hpp"
|
||||
#include "services/database/TrackArtistLink.hpp"
|
||||
#include "services/database/TrackFeatures.hpp"
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/User.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
#include "IdTypeTraits.hpp"
|
||||
#include "SqlQuery.hpp"
|
||||
#include "StringViewTraits.hpp"
|
||||
#include "Traits.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
template <typename T>
|
||||
static
|
||||
Wt::Dbo::Query<T>
|
||||
createQuery(Session& session,
|
||||
const std::string& queryStr,
|
||||
const std::vector<ClusterId>& clusterIds,
|
||||
const std::vector<std::string_view>& keywords)
|
||||
Wt::Dbo::Query<TrackId>
|
||||
createQuery(Session& session, const Track::FindParameters& params)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {session.getDboSession().query<T>(queryStr)};
|
||||
auto query {session.getDboSession().query<TrackId>("SELECT t.id from track t")};
|
||||
|
||||
for (std::string_view keyword : keywords)
|
||||
for (std::string_view keyword : params.keywords)
|
||||
query.where("t.name LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind("%" + escapeLikeKeyword(keyword) + "%");
|
||||
|
||||
if (!clusterIds.empty())
|
||||
if (params.writtenAfter.isValid())
|
||||
query.where("t.file_last_write > ?").bind(params.writtenAfter);
|
||||
|
||||
if (params.starringUser.isValid())
|
||||
{
|
||||
assert(params.scrobbler);
|
||||
query.join("starred_track s_t ON s_t.track_id = t.id")
|
||||
.where("s_t.user_id = ?").bind(params.starringUser)
|
||||
.where("s_t.scrobbler = ?").bind(*params.scrobbler);
|
||||
}
|
||||
|
||||
if (!params.clusters.empty())
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "t.id IN (SELECT DISTINCT t.id FROM track t"
|
||||
@@ -60,18 +67,34 @@ createQuery(Session& session,
|
||||
" INNER JOIN cluster c ON c.id = t_c.cluster_id";
|
||||
|
||||
WhereClause clusterClause;
|
||||
for (const ClusterId clusterId : clusterIds)
|
||||
for (const ClusterId clusterId : params.clusters)
|
||||
{
|
||||
clusterClause.Or(WhereClause("c.id = ?"));
|
||||
query.bind(clusterId);
|
||||
}
|
||||
|
||||
oss << " " << clusterClause.get();
|
||||
oss << " GROUP BY t.id HAVING COUNT(*) = " << clusterIds.size() << ")";
|
||||
oss << " GROUP BY t.id HAVING COUNT(*) = " << params.clusters.size() << ")";
|
||||
|
||||
query.where(oss.str());
|
||||
}
|
||||
|
||||
switch (params.sortMethod)
|
||||
{
|
||||
case TrackSortMethod::None:
|
||||
break;
|
||||
case TrackSortMethod::LastWritten:
|
||||
query.orderBy("t.file_last_write DESC");
|
||||
break;
|
||||
case TrackSortMethod::Random:
|
||||
query.orderBy("RANDOM()");
|
||||
break;
|
||||
case TrackSortMethod::StarredDateDesc:
|
||||
assert(params.starringUser.isValid());
|
||||
query.orderBy("s_t.date_time DESC");
|
||||
break;
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
@@ -80,94 +103,6 @@ Track::Track(const std::filesystem::path& p)
|
||||
{
|
||||
}
|
||||
|
||||
std::size_t
|
||||
Track::getCount(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM track");
|
||||
}
|
||||
|
||||
std::vector<Track::pointer>
|
||||
Track::getAll(Session& session, std::optional<std::size_t> limit)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto res {session.getDboSession().find<Track>()
|
||||
.limit(limit ? static_cast<int>(*limit) : -1)
|
||||
.resultList()};
|
||||
|
||||
return std::vector<Track::pointer>(std::cbegin(res), std::cend(res));
|
||||
}
|
||||
|
||||
std::vector<Track::pointer>
|
||||
Track::getAllRandom(Session& session, const std::vector<ClusterId>& clusterIds, std::optional<std::size_t> limit)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {createQuery<Wt::Dbo::ptr<Track>>(session, "SELECT t from track t", clusterIds, {})};
|
||||
auto collection {query
|
||||
.orderBy("RANDOM()")
|
||||
.limit(limit ? static_cast<int>(*limit) + 1: -1)
|
||||
.resultList()};
|
||||
|
||||
return std::vector<pointer>(collection.begin(), collection.end());
|
||||
}
|
||||
|
||||
std::vector<TrackId>
|
||||
Track::getAllIdsRandom(Session& session, const std::vector<ClusterId>& clusterIds, std::optional<std::size_t> limit)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {createQuery<TrackId>(session, "SELECT t.id from track t", clusterIds, {})};
|
||||
|
||||
Wt::Dbo::collection<TrackId> collection = query
|
||||
.orderBy("RANDOM()")
|
||||
.limit(limit ? static_cast<int>(*limit) + 1: -1);
|
||||
|
||||
return std::vector<TrackId>(collection.begin(), collection.end());
|
||||
}
|
||||
|
||||
|
||||
std::vector<TrackId>
|
||||
Track::getAllIds(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<TrackId> res = session.getDboSession().query<TrackId>("SELECT id FROM track");
|
||||
return std::vector<TrackId>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
Track::pointer
|
||||
Track::getByPath(Session& session, const std::filesystem::path& p)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().find<Track>().where("file_path = ?").bind(p.string()).resultValue();
|
||||
}
|
||||
|
||||
Track::pointer
|
||||
Track::getById(Session& session, TrackId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().find<Track>()
|
||||
.where("id = ?").bind(id)
|
||||
.resultValue();
|
||||
}
|
||||
|
||||
std::vector<Track::pointer>
|
||||
Track::getByRecordingMBID(Session& session, const UUID& mbid)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto res {session.getDboSession().find<Track>()
|
||||
.where("recording_mbid = ?").bind(std::string {mbid.getAsString()})
|
||||
.resultList()};
|
||||
|
||||
return std::vector<Track::pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
Track::pointer
|
||||
Track::create(Session& session, const std::filesystem::path& p)
|
||||
{
|
||||
@@ -179,140 +114,98 @@ Track::create(Session& session, const std::filesystem::path& p)
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<std::pair<TrackId, std::filesystem::path>>
|
||||
Track::getAllPaths(Session& session, std::optional<std::size_t> offset, std::optional<std::size_t> size)
|
||||
std::size_t
|
||||
Track::getCount(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM track");
|
||||
}
|
||||
|
||||
Track::pointer
|
||||
Track::findByPath(Session& session, const std::filesystem::path& p)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().find<Track>().where("file_path = ?").bind(p.string()).resultValue();
|
||||
}
|
||||
|
||||
Track::pointer
|
||||
Track::find(Session& session, TrackId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().find<Track>()
|
||||
.where("id = ?").bind(id)
|
||||
.resultValue();
|
||||
}
|
||||
|
||||
bool
|
||||
Track::exists(Session& session, TrackId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().query<int>("SELECT 1 from track").where("id = ?").bind(id).resultValue() == 1;
|
||||
}
|
||||
|
||||
std::vector<Track::pointer>
|
||||
Track::findByRecordingMBID(Session& session, const UUID& mbid)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto res {session.getDboSession().find<Track>()
|
||||
.where("recording_mbid = ?").bind(std::string {mbid.getAsString()})
|
||||
.resultList()};
|
||||
|
||||
return std::vector<Track::pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
RangeResults<Track::PathResult>
|
||||
Track::findPaths(Session& session, Range range)
|
||||
{
|
||||
using QueryResultType = std::tuple<TrackId, std::string>;
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<QueryResultType> queryRes = session.getDboSession().query<QueryResultType>("SELECT id,file_path FROM track")
|
||||
.limit(size ? static_cast<int>(*size) + 1 : -1)
|
||||
.offset(offset ? static_cast<int>(*offset) : -1);
|
||||
// TODO Dbo traits on filesystem
|
||||
auto query {session.getDboSession().query<QueryResultType>("SELECT id, file_path FROM track")};
|
||||
|
||||
std::vector<std::pair<TrackId, std::filesystem::path>> result;
|
||||
result.reserve(queryRes.size());
|
||||
RangeResults<QueryResultType> queryResults {execQuery(query, range)};
|
||||
|
||||
std::transform(std::begin(queryRes), std::end(queryRes), std::back_inserter(result),
|
||||
RangeResults<PathResult> res;
|
||||
res.range = queryResults.range;
|
||||
res.moreResults = queryResults.moreResults;
|
||||
res.results.reserve(queryResults.results.size());
|
||||
|
||||
std::transform(std::cbegin(queryResults.results), std::cend(queryResults.results), std::back_inserter(res.results),
|
||||
[](const QueryResultType& queryResult)
|
||||
{
|
||||
return std::make_pair(std::get<0>(queryResult), std::get<1>(queryResult));
|
||||
return PathResult {std::get<0>(queryResult), std::get<1>(queryResult)};
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<Track::pointer>
|
||||
Track::getMBIDDuplicates(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto res {session.getDboSession().query<Wt::Dbo::ptr<Track>>( "SELECT track FROM track WHERE mbid in (SELECT mbid FROM track WHERE mbid <> '' GROUP BY mbid HAVING COUNT (*) > 1)")
|
||||
.orderBy("track.release_id,track.disc_number,track.track_number,track.mbid")
|
||||
.resultList()};
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<Track::pointer>
|
||||
Track::getLastWritten(Session& session, std::optional<Wt::WDateTime> after, const std::vector<ClusterId>& clusterIds, std::optional<Range> range, bool& moreResults)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {createQuery<Wt::Dbo::ptr<Track>>(session, "SELECT t from track t", clusterIds, {})};
|
||||
if (after)
|
||||
query.where("t.file_last_write > ?").bind(after);
|
||||
|
||||
auto collection {query
|
||||
.orderBy("t.file_last_write DESC")
|
||||
.groupBy("t.id")
|
||||
.offset(range ? static_cast<int>(range->offset) : -1)
|
||||
.limit(range ? static_cast<int>(range->limit) + 1: -1)
|
||||
.resultList()};
|
||||
|
||||
std::vector<pointer> res(collection.begin(), collection.end());
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
res.pop_back();
|
||||
}
|
||||
else
|
||||
moreResults = false;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<Track::pointer>
|
||||
Track::getAllWithRecordingMBIDAndMissingFeatures(Session& session)
|
||||
RangeResults<TrackId>
|
||||
Track::findRecordingMBIDDuplicates(Session& session, Range range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto res {session.getDboSession().query<Wt::Dbo::ptr<Track>>
|
||||
("SELECT t FROM track t")
|
||||
auto query {session.getDboSession().query<TrackId>( "SELECT track.id FROM track WHERE recording_mbid in (SELECT recording_mbid FROM track WHERE recording_mbid <> '' GROUP BY recording_mbid HAVING COUNT (*) > 1)")
|
||||
.orderBy("track.release_id,track.disc_number,track.track_number,track.recording_mbid")};
|
||||
|
||||
return execQuery(query, range);
|
||||
}
|
||||
|
||||
RangeResults<TrackId>
|
||||
Track::findWithRecordingMBIDAndMissingFeatures(Session& session, Range range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {session.getDboSession().query<TrackId>("SELECT t.id FROM track t")
|
||||
.where("LENGTH(t.recording_mbid) > 0")
|
||||
.where("NOT EXISTS (SELECT * FROM track_features t_f WHERE t_f.track_id = t.id)")
|
||||
.resultList()};
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
.where("NOT EXISTS (SELECT * FROM track_features t_f WHERE t_f.track_id = t.id)")};
|
||||
|
||||
std::vector<TrackId>
|
||||
Track::getAllIdsWithFeatures(Session& session, std::optional<std::size_t> limit)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<TrackId> res = session.getDboSession().query<TrackId>
|
||||
("SELECT t.id FROM track t")
|
||||
.where("EXISTS (SELECT * from track_features t_f WHERE t_f.track_id = t.id)")
|
||||
.limit(limit ? static_cast<int>(*limit) : -1);
|
||||
|
||||
return std::vector<TrackId>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<TrackId>
|
||||
Track::getAllIdsWithClusters(Session& session, std::optional<std::size_t> limit)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<TrackId> res = session.getDboSession().query<TrackId>
|
||||
("SELECT DISTINCT t.id FROM track t"
|
||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id")
|
||||
.limit(limit ? static_cast<int>(*limit) : -1);
|
||||
|
||||
return std::vector<TrackId>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<Track::pointer>
|
||||
Track::getStarred(Session& session,
|
||||
ObjectPtr<User> user,
|
||||
const std::vector<ClusterId>& clusterIds,
|
||||
std::optional<Range> range, bool& moreResults)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {createQuery<Wt::Dbo::ptr<Track>>(session, "SELECT t from track t", clusterIds, {})};
|
||||
{
|
||||
std::ostringstream oss;
|
||||
oss << "t.id IN (SELECT DISTINCT t.id FROM track t"
|
||||
" INNER JOIN user_track_starred uts ON uts.track_id = t.id"
|
||||
" INNER JOIN user u ON u.id = uts.user_id WHERE u.id = ?)";
|
||||
|
||||
query.bind(user->getId().toString());
|
||||
query.where(oss.str());
|
||||
}
|
||||
|
||||
auto collection {query
|
||||
.offset(range ? static_cast<int>(range->offset) : -1)
|
||||
.limit(range ? static_cast<int>(range->limit) + 1: -1)
|
||||
.resultList()};
|
||||
|
||||
std::vector<pointer> res(collection.begin(), collection.end());
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
res.pop_back();
|
||||
}
|
||||
else
|
||||
moreResults = false;
|
||||
|
||||
return res;
|
||||
return execQuery(query, range);
|
||||
}
|
||||
|
||||
std::vector<Cluster::pointer>
|
||||
@@ -334,56 +227,31 @@ Track::getClusterIds() const
|
||||
return std::vector<ClusterId>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
bool
|
||||
Track::hasTrackFeatures() const
|
||||
{
|
||||
return (_trackFeatures.lock() != Wt::Dbo::ptr<Database::TrackFeatures> {});
|
||||
}
|
||||
|
||||
std::vector<Track::pointer>
|
||||
Track::getByFilter(Session& session,
|
||||
const std::vector<ClusterId>& clusterIds,
|
||||
const std::vector<std::string_view>& keywords,
|
||||
std::optional<Range> range,
|
||||
bool& moreResults)
|
||||
RangeResults<TrackId>
|
||||
Track::find(Session& session, const FindParameters& parameters)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto collection {createQuery<Wt::Dbo::ptr<Track>>(session, "SELECT t from track t", clusterIds, keywords)
|
||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1)
|
||||
.resultList()};
|
||||
auto query {createQuery(session, parameters)};
|
||||
|
||||
std::vector<pointer> res(collection.begin(), collection.end());
|
||||
if (range && (res.size() == static_cast<std::size_t>(range->limit) + 1))
|
||||
{
|
||||
moreResults = true;
|
||||
res.pop_back();
|
||||
}
|
||||
else
|
||||
moreResults = false;
|
||||
|
||||
return res;
|
||||
return execQuery(query, parameters.range);
|
||||
}
|
||||
|
||||
std::vector<Track::pointer>
|
||||
Track::getByNameAndReleaseName(Session& session, std::string_view trackName, std::string_view releaseName)
|
||||
RangeResults<TrackId>
|
||||
Track::findByNameAndReleaseName(Session& session, std::string_view trackName, std::string_view releaseName)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto res {session.getDboSession().query<Wt::Dbo::ptr<Track>>("SELECT t from track t")
|
||||
auto query {session.getDboSession().query<TrackId>("SELECT t.id from track t")
|
||||
.join("release r ON t.release_id = r.id")
|
||||
.where("t.name = ?").bind(trackName)
|
||||
.where("r.name = ?").bind(releaseName)
|
||||
.resultList()};
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
.where("r.name = ?").bind(releaseName)};
|
||||
|
||||
return execQuery(query, Range {});
|
||||
}
|
||||
|
||||
std::vector<Track::pointer>
|
||||
Track::getSimilarTracks(Session& session,
|
||||
const std::vector<TrackId>& tracks,
|
||||
std::optional<std::size_t> offset,
|
||||
std::optional<std::size_t> size)
|
||||
RangeResults<TrackId>
|
||||
Track::findSimilarTracks(Session& session, const std::vector<TrackId>& tracks, Range range)
|
||||
{
|
||||
assert(!tracks.empty());
|
||||
session.checkSharedLocked();
|
||||
@@ -396,15 +264,13 @@ Track::getSimilarTracks(Session& session,
|
||||
oss << "?";
|
||||
}
|
||||
|
||||
auto query {session.getDboSession().query<Wt::Dbo::ptr<Track>>(
|
||||
"SELECT t FROM track t"
|
||||
auto query {session.getDboSession().query<TrackId>(
|
||||
"SELECT t.id FROM track t"
|
||||
" INNER JOIN track_cluster t_c ON t_c.track_id = t.id"
|
||||
" AND t_c.cluster_id IN (SELECT c.id FROM cluster c INNER JOIN track_cluster t_c ON t_c.cluster_id = c.id WHERE t_c.track_id IN (" + oss.str() + "))"
|
||||
" AND t.id NOT IN (" + oss.str() + ")")
|
||||
.groupBy("t.id")
|
||||
.orderBy("COUNT(*) DESC, RANDOM()")
|
||||
.limit(size ? static_cast<int>(*size) : -1)
|
||||
.offset(offset ? static_cast<int>(*offset) : -1)};
|
||||
.orderBy("COUNT(*) DESC, RANDOM()")};
|
||||
|
||||
for (TrackId trackId : tracks)
|
||||
query.bind(trackId);
|
||||
@@ -412,22 +278,7 @@ Track::getSimilarTracks(Session& session,
|
||||
for (TrackId trackId : tracks)
|
||||
query.bind(trackId);
|
||||
|
||||
auto res {query.resultList()};
|
||||
return std::vector<pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<Track::pointer>
|
||||
Track::getByClusters(Session& session, const std::vector<ClusterId>& clusters)
|
||||
{
|
||||
assert(!clusters.empty());
|
||||
session.checkSharedLocked();
|
||||
|
||||
bool moreResults;
|
||||
return getByFilter(session,
|
||||
clusters,
|
||||
{}, // keywords
|
||||
std::nullopt, // range
|
||||
moreResults);
|
||||
return execQuery(query, range);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -450,12 +301,6 @@ Track::setClusters(const std::vector<ObjectPtr<Cluster>>& clusters)
|
||||
_clusters.insert(getDboPtr(cluster));
|
||||
}
|
||||
|
||||
void
|
||||
Track::setFeatures(const ObjectPtr<TrackFeatures>& features)
|
||||
{
|
||||
_trackFeatures = getDboPtr(features);
|
||||
}
|
||||
|
||||
std::optional<std::size_t>
|
||||
Track::getTrackNumber() const
|
||||
{
|
||||
@@ -585,12 +430,6 @@ Track::getArtistLinks() const
|
||||
return std::vector<TrackArtistLink::pointer>(_trackArtistLinks.begin(), _trackArtistLinks.end());
|
||||
}
|
||||
|
||||
ObjectPtr<TrackFeatures>
|
||||
Track::getTrackFeatures() const
|
||||
{
|
||||
return _trackFeatures.lock();
|
||||
}
|
||||
|
||||
std::vector<std::vector<Cluster::pointer>>
|
||||
Track::getClusterGroups(const std::vector<ClusterType::pointer>& clusterTypes, std::size_t size) const
|
||||
{
|
||||
+6
-6
@@ -17,13 +17,13 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "database/TrackArtistLink.hpp"
|
||||
#include "services/database/TrackArtistLink.hpp"
|
||||
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "services/database/Artist.hpp"
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/Track.hpp"
|
||||
|
||||
#include "Traits.hpp"
|
||||
#include "IdTypeTraits.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
@@ -46,7 +46,7 @@ TrackArtistLink::create(Session& session, ObjectPtr<Track> track, ObjectPtr<Arti
|
||||
}
|
||||
|
||||
EnumSet<TrackArtistLinkType>
|
||||
TrackArtistLink::getUsedTypes(Session& session)
|
||||
TrackArtistLink::findUsedTypes(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
+24
-25
@@ -17,12 +17,13 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "database/TrackBookmark.hpp"
|
||||
#include "services/database/TrackBookmark.hpp"
|
||||
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "Traits.hpp"
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/Track.hpp"
|
||||
#include "services/database/User.hpp"
|
||||
#include "IdTypeTraits.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
@@ -32,6 +33,15 @@ _track {getDboPtr(track)}
|
||||
{
|
||||
}
|
||||
|
||||
std::size_t
|
||||
TrackBookmark::getCount(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM track_bookmark");
|
||||
}
|
||||
|
||||
|
||||
TrackBookmark::pointer
|
||||
TrackBookmark::create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track)
|
||||
{
|
||||
@@ -43,40 +53,30 @@ TrackBookmark::create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> t
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<TrackBookmark::pointer>
|
||||
TrackBookmark::getAll(Session& session)
|
||||
RangeResults<TrackBookmarkId>
|
||||
TrackBookmark::find(Session& session, UserId userId, Range range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto res {session.getDboSession().find<TrackBookmark>().resultList()};
|
||||
return std::vector<TrackBookmark::pointer>(std::cbegin(res), std::cend(res));
|
||||
}
|
||||
auto query {session.getDboSession().query<TrackBookmarkId>("SELECT id from track_bookmark")
|
||||
.where("user_id = ?").bind(userId)};
|
||||
|
||||
std::vector<TrackBookmark::pointer>
|
||||
TrackBookmark::getByUser(Session& session, User::pointer user)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto res {session.getDboSession().find<TrackBookmark>()
|
||||
.where("user_id = ?").bind(user->getId())
|
||||
.resultList()};
|
||||
|
||||
return std::vector<TrackBookmark::pointer>(std::cbegin(res), std::cend(res));
|
||||
return execQuery(query, range);
|
||||
}
|
||||
|
||||
TrackBookmark::pointer
|
||||
TrackBookmark::getByUser(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track)
|
||||
TrackBookmark::find(Session& session, UserId userId, TrackId trackId)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().find<TrackBookmark>()
|
||||
.where("user_id = ?").bind(user->getId())
|
||||
.where("track_id = ?").bind(track->getId())
|
||||
.where("user_id = ?").bind(userId)
|
||||
.where("track_id = ?").bind(trackId)
|
||||
.resultValue();
|
||||
}
|
||||
|
||||
TrackBookmark::pointer
|
||||
TrackBookmark::getById(Session& session, TrackBookmarkId id)
|
||||
TrackBookmark::find(Session& session, TrackBookmarkId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
@@ -85,6 +85,5 @@ TrackBookmark::getById(Session& session, TrackBookmarkId id)
|
||||
.resultValue();
|
||||
}
|
||||
|
||||
|
||||
} // namespace Database
|
||||
|
||||
+53
-8
@@ -17,14 +17,16 @@
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "database/TrackFeatures.hpp"
|
||||
#include "services/database/TrackFeatures.hpp"
|
||||
|
||||
#include <boost/property_tree/ptree.hpp>
|
||||
#include <boost/property_tree/json_parser.hpp>
|
||||
|
||||
#include "database/Session.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/Track.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "IdTypeTraits.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
@@ -34,11 +36,53 @@ _track {getDboPtr(track)}
|
||||
{
|
||||
}
|
||||
|
||||
std::size_t
|
||||
TrackFeatures::getCount(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM track_features");
|
||||
}
|
||||
|
||||
TrackFeatures::pointer
|
||||
TrackFeatures::find(Session& session, TrackFeaturesId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().find<TrackFeatures>()
|
||||
.where("id = ?").bind(id)
|
||||
.resultValue();
|
||||
}
|
||||
|
||||
TrackFeatures::pointer
|
||||
TrackFeatures::find(Session& session, TrackId trackId)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().find<TrackFeatures>()
|
||||
.where("track_id = ?").bind(trackId)
|
||||
.resultValue();
|
||||
}
|
||||
|
||||
RangeResults<TrackFeaturesId>
|
||||
TrackFeatures::find(Session& session, Range range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {session.getDboSession().query<TrackFeaturesId>("SELECT id from track_features")};
|
||||
|
||||
return execQuery(query, range);
|
||||
}
|
||||
|
||||
TrackFeatures::pointer
|
||||
TrackFeatures::create(Session& session, ObjectPtr<Track> track, const std::string& jsonEncodedFeatures)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
return session.getDboSession().add(std::make_unique<TrackFeatures>(track, jsonEncodedFeatures));
|
||||
|
||||
TrackFeatures::pointer res {session.getDboSession().add(std::make_unique<TrackFeatures>(track, jsonEncodedFeatures))};
|
||||
session.getDboSession().flush();
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
FeatureValues
|
||||
@@ -51,6 +95,8 @@ TrackFeatures::getFeatureValues(const FeatureName& featureNode) const
|
||||
FeatureValuesMap
|
||||
TrackFeatures::getFeatureValuesMap(const std::unordered_set<FeatureName>& featureNames) const
|
||||
{
|
||||
FeatureValuesMap res;
|
||||
|
||||
try
|
||||
{
|
||||
std::istringstream iss {_data};
|
||||
@@ -58,7 +104,6 @@ TrackFeatures::getFeatureValuesMap(const std::unordered_set<FeatureName>& featur
|
||||
|
||||
boost::property_tree::read_json(iss, root);
|
||||
|
||||
FeatureValuesMap res;
|
||||
for (const FeatureName& featureName : featureNames)
|
||||
{
|
||||
FeatureValues& featureValues {res[featureName]};
|
||||
@@ -75,14 +120,14 @@ TrackFeatures::getFeatureValuesMap(const std::unordered_set<FeatureName>& featur
|
||||
if (!hasChildren)
|
||||
featureValues.push_back(node.get_value<double>());
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
catch (boost::property_tree::ptree_error& error)
|
||||
{
|
||||
LMS_LOG(DB, ERROR) << "Track " << _track.id() << ": ptree exception: " << error.what();
|
||||
return {};
|
||||
res.clear();
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
} // namespace Database
|
||||
+146
-59
@@ -16,21 +16,22 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "database/TrackList.hpp"
|
||||
#include "services/database/TrackList.hpp"
|
||||
|
||||
#include <cassert>
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
|
||||
#include "database/Artist.hpp"
|
||||
#include "database/Cluster.hpp"
|
||||
#include "database/Release.hpp"
|
||||
#include "database/Session.hpp"
|
||||
#include "database/User.hpp"
|
||||
#include "database/Track.hpp"
|
||||
#include "services/database/Artist.hpp"
|
||||
#include "services/database/Cluster.hpp"
|
||||
#include "services/database/Release.hpp"
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/User.hpp"
|
||||
#include "services/database/Track.hpp"
|
||||
#include "SqlQuery.hpp"
|
||||
#include "StringViewTraits.hpp"
|
||||
#include "Traits.hpp"
|
||||
#include "IdTypeTraits.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
@@ -40,7 +41,6 @@ TrackList::TrackList(std::string_view name, Type type, bool isPublic, ObjectPtr<
|
||||
_isPublic {isPublic},
|
||||
_user {getDboPtr(user)}
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
TrackList::pointer
|
||||
@@ -55,56 +55,54 @@ TrackList::create(Session& session, std::string_view name, Type type, bool isPub
|
||||
return res;
|
||||
}
|
||||
|
||||
TrackList::pointer
|
||||
TrackList::get(Session& session, std::string_view name, Type type, ObjectPtr<User> user)
|
||||
std::size_t
|
||||
TrackList::getCount(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
assert(user);
|
||||
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM tracklist");
|
||||
}
|
||||
|
||||
|
||||
TrackList::pointer
|
||||
TrackList::find(Session& session, std::string_view name, Type type, UserId userId)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
assert(userId.isValid());
|
||||
|
||||
return session.getDboSession().find<TrackList>()
|
||||
.where("name = ?").bind(name)
|
||||
.where("type = ?").bind(type)
|
||||
.where("user_id = ?").bind(user->getId()).resultValue();
|
||||
.where("user_id = ?").bind(userId).resultValue();
|
||||
}
|
||||
|
||||
std::vector<TrackList::pointer>
|
||||
TrackList::getAll(Session& session)
|
||||
RangeResults<TrackListId>
|
||||
TrackList::find(Session& session, UserId userId, Range range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto res = session.getDboSession().find<TrackList>().resultList();
|
||||
return std::vector<TrackList::pointer>(res.begin(), res.end());
|
||||
auto query {session.getDboSession().query<TrackListId>("SELECT id FROM tracklist")
|
||||
.where("user_id = ?").bind(userId)
|
||||
.orderBy("name COLLATE NOCASE")};
|
||||
|
||||
return execQuery(query, range);
|
||||
}
|
||||
|
||||
std::vector<TrackList::pointer>
|
||||
TrackList::getAll(Session& session, ObjectPtr<User> user)
|
||||
RangeResults<TrackListId>
|
||||
TrackList::find(Session& session, UserId userId, Type type, Range range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto res {session.getDboSession().find<TrackList>()
|
||||
.where("user_id = ?").bind(user->getId())
|
||||
.orderBy("name COLLATE NOCASE")
|
||||
.resultList()};
|
||||
|
||||
return std::vector<TrackList::pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<TrackList::pointer>
|
||||
TrackList::getAll(Session& session, ObjectPtr<User> user, Type type)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto res {session.getDboSession().find<TrackList>()
|
||||
.where("user_id = ?").bind(user->getId())
|
||||
auto query {session.getDboSession().query<TrackListId>("SELECT id FROM tracklist")
|
||||
.where("user_id = ?").bind(userId)
|
||||
.where("type = ?").bind(type)
|
||||
.orderBy("name COLLATE NOCASE")
|
||||
.resultList()};
|
||||
.orderBy("name COLLATE NOCASE")};
|
||||
|
||||
return std::vector<TrackList::pointer>(res.begin(), res.end());
|
||||
return execQuery(query, range);
|
||||
}
|
||||
|
||||
TrackList::pointer
|
||||
TrackList::getById(Session& session, TrackListId id)
|
||||
TrackList::find(Session& session, TrackListId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
@@ -159,7 +157,7 @@ TrackList::getEntryByTrackAndDateTime(ObjectPtr<Track> track, const Wt::WDateTim
|
||||
return session()->find<TrackListEntry>()
|
||||
.where("tracklist_id = ?").bind(getId())
|
||||
.where("track_id = ?").bind(track->getId())
|
||||
.where("date_time = ?").bind(Wt::WDateTime::fromTime_t(dateTime.toTime_t()))
|
||||
.where("date_time = ?").bind(normalizeDateTime(dateTime))
|
||||
.resultValue();
|
||||
}
|
||||
|
||||
@@ -270,19 +268,109 @@ createTracksQuery(Wt::Dbo::Session& session, TrackListId tracklistId, const std:
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer>
|
||||
TrackList::getArtistsReverse(const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range, bool& moreResults) const
|
||||
TrackList::getArtists(const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, ArtistSortMethod sortMethod, std::optional<Range> range, bool& moreResults) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
auto query {createArtistsQuery(*session(), "SELECT a from artist a", getId(), clusterIds, linkType)
|
||||
.groupBy("a.id").having("p_e.date_time = MAX(p_e.date_time)")
|
||||
.limit(range ? static_cast<int>(range->size) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1)};
|
||||
|
||||
switch (sortMethod)
|
||||
{
|
||||
case ArtistSortMethod::None:
|
||||
break;
|
||||
case ArtistSortMethod::ByName:
|
||||
query.orderBy("a.name COLLATE NOCASE");
|
||||
break;
|
||||
case ArtistSortMethod::BySortName:
|
||||
query.orderBy("a.sort_name COLLATE NOCASE");
|
||||
break;
|
||||
case ArtistSortMethod::Random:
|
||||
query.orderBy("RANDOM()");
|
||||
break;
|
||||
case ArtistSortMethod::LastWritten:
|
||||
case ArtistSortMethod::StarredDateDesc:
|
||||
assert(false); // Not implemented!
|
||||
break;
|
||||
}
|
||||
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> collection {query.resultList()};
|
||||
|
||||
auto res {std::vector<Artist::pointer>(collection.begin(), collection.end())};
|
||||
if (range && res.size() == static_cast<std::size_t>(range->size) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
res.pop_back();
|
||||
}
|
||||
else
|
||||
moreResults = false;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
std::vector<ObjectPtr<Release>>
|
||||
TrackList::getReleases(const std::vector<ClusterId>& clusterIds, std::optional<Range> range, bool& moreResults) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
auto collection {createReleasesQuery(*session(), "SELECT r from release r", getId(), clusterIds)
|
||||
.groupBy("r.id").having("p_e.date_time = MAX(p_e.date_time)")
|
||||
.limit(range ? static_cast<int>(range->size) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1)
|
||||
.resultList()};
|
||||
|
||||
std::vector<Release::pointer> res(collection.begin(), collection.end());
|
||||
if (range && res.size() == static_cast<std::size_t>(range->size) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
res.pop_back();
|
||||
}
|
||||
else
|
||||
moreResults = false;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<ObjectPtr<Track>>
|
||||
TrackList::getTracks(const std::vector<ClusterId>& clusterIds, std::optional<Range> range, bool& moreResults) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
auto collection {createTracksQuery(*session(), getId(), clusterIds)
|
||||
.groupBy("t.id").having("p_e.date_time = MAX(p_e.date_time)")
|
||||
.limit(range ? static_cast<int>(range->size) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1)
|
||||
.resultList()};
|
||||
|
||||
std::vector<Track::pointer> res(collection.begin(), collection.end());
|
||||
if (range && res.size() == static_cast<std::size_t>(range->size) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
res.pop_back();
|
||||
}
|
||||
else
|
||||
moreResults = false;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer>
|
||||
TrackList::getArtistsOrderedByRecentFirst(const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range, bool& moreResults) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
auto collection {createArtistsQuery(*session(), "SELECT a from artist a", getId(), 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)
|
||||
.orderBy("p_e.date_time DESC, p_e.id DESC")
|
||||
.limit(range ? static_cast<int>(range->size) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1)
|
||||
.resultList()};
|
||||
|
||||
auto res {std::vector<Artist::pointer>(collection.begin(), collection.end())};
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
if (range && res.size() == static_cast<std::size_t>(range->size) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
res.pop_back();
|
||||
@@ -294,19 +382,19 @@ TrackList::getArtistsReverse(const std::vector<ClusterId>& clusterIds, std::opti
|
||||
}
|
||||
|
||||
std::vector<Release::pointer>
|
||||
TrackList::getReleasesReverse(const std::vector<ClusterId>& clusterIds, std::optional<Range> range, bool& moreResults) const
|
||||
TrackList::getReleasesOrderedByRecentFirst(const std::vector<ClusterId>& clusterIds, std::optional<Range> range, bool& moreResults) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
auto collection {createReleasesQuery(*session(), "SELECT r from release r", getId(), 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)
|
||||
.orderBy("p_e.date_time DESC, p_e.id DESC")
|
||||
.limit(range ? static_cast<int>(range->size) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1)
|
||||
.resultList()};
|
||||
|
||||
std::vector<Release::pointer> res(collection.begin(), collection.end());
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
if (range && res.size() == static_cast<std::size_t>(range->size) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
res.pop_back();
|
||||
@@ -318,19 +406,19 @@ TrackList::getReleasesReverse(const std::vector<ClusterId>& clusterIds, std::opt
|
||||
}
|
||||
|
||||
std::vector<Track::pointer>
|
||||
TrackList::getTracksReverse(const std::vector<ClusterId>& clusterIds, std::optional<Range> range, bool& moreResults) const
|
||||
TrackList::getTracksOrderedByRecentFirst(const std::vector<ClusterId>& clusterIds, std::optional<Range> range, bool& moreResults) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
auto collection {createTracksQuery(*session(), getId(), clusterIds)
|
||||
.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)
|
||||
.orderBy("p_e.date_time DESC, p_e.id DESC")
|
||||
.limit(range ? static_cast<int>(range->size) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1)
|
||||
.resultList()};
|
||||
|
||||
std::vector<Track::pointer> res(collection.begin(), collection.end());
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
if (range && res.size() == static_cast<std::size_t>(range->size) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
res.pop_back();
|
||||
@@ -424,13 +512,13 @@ TrackList::getTopArtists(const std::vector<ClusterId>& clusterIds, std::optional
|
||||
auto collection {query
|
||||
.orderBy("COUNT(a.id) DESC")
|
||||
.groupBy("a.id")
|
||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||
.limit(range ? static_cast<int>(range->size) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1)
|
||||
.resultList()};
|
||||
|
||||
std::vector<Artist::pointer> res(collection.begin(), collection.end());
|
||||
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
if (range && res.size() == static_cast<std::size_t>(range->size) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
res.pop_back();
|
||||
@@ -450,12 +538,12 @@ TrackList::getTopReleases(const std::vector<ClusterId>& clusterIds, std::optiona
|
||||
auto collection {query
|
||||
.orderBy("COUNT(r.id) DESC")
|
||||
.groupBy("r.id")
|
||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||
.limit(range ? static_cast<int>(range->size) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1)
|
||||
.resultList()};
|
||||
|
||||
std::vector<Release::pointer> res(collection.begin(), collection.end());
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
if (range && res.size() == static_cast<std::size_t>(range->size) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
res.pop_back();
|
||||
@@ -475,12 +563,12 @@ TrackList::getTopTracks(const std::vector<ClusterId>& clusterIds, std::optional<
|
||||
auto collection {query
|
||||
.orderBy("COUNT(t.id) DESC")
|
||||
.groupBy("t.id")
|
||||
.limit(range ? static_cast<int>(range->limit) + 1 : -1)
|
||||
.limit(range ? static_cast<int>(range->size) + 1 : -1)
|
||||
.offset(range ? static_cast<int>(range->offset) : -1)
|
||||
.resultList()};
|
||||
|
||||
std::vector<Track::pointer> res(collection.begin(), collection.end());
|
||||
if (range && res.size() == static_cast<std::size_t>(range->limit) + 1)
|
||||
if (range && res.size() == static_cast<std::size_t>(range->size) + 1)
|
||||
{
|
||||
moreResults = true;
|
||||
res.pop_back();
|
||||
@@ -492,11 +580,10 @@ TrackList::getTopTracks(const std::vector<ClusterId>& clusterIds, std::optional<
|
||||
}
|
||||
|
||||
TrackListEntry::TrackListEntry(ObjectPtr<Track> track, ObjectPtr<TrackList> tracklist, const Wt::WDateTime& dateTime)
|
||||
: _dateTime {Wt::WDateTime::fromTime_t(dateTime.toTime_t())} // force second resolution
|
||||
: _dateTime {normalizeDateTime(dateTime)}
|
||||
, _track {getDboPtr(track)}
|
||||
, _tracklist {getDboPtr(tracklist)}
|
||||
{
|
||||
assert(_dateTime.isValid());
|
||||
}
|
||||
|
||||
TrackListEntry::pointer
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (C) 2015 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 "services/database/Types.hpp"
|
||||
|
||||
#include <set>
|
||||
|
||||
namespace Database
|
||||
{
|
||||
static const std::set<Bitrate> allowedAudioBitrates
|
||||
{
|
||||
64000,
|
||||
96000,
|
||||
128000,
|
||||
192000,
|
||||
320000,
|
||||
};
|
||||
|
||||
void visitAllowedAudioBitrates(std::function<void(Bitrate)> func)
|
||||
{
|
||||
for (Bitrate bitrate : allowedAudioBitrates)
|
||||
func(bitrate);
|
||||
}
|
||||
|
||||
bool isAudioBitrateAllowed(Bitrate bitrate)
|
||||
{
|
||||
return allowedAudioBitrates.find(bitrate) != std::cend(allowedAudioBitrates);
|
||||
}
|
||||
|
||||
DateRange
|
||||
DateRange::fromYearRange(int from, int to)
|
||||
{
|
||||
return DateRange {{from, 1, 1}, {to, 12, 31}};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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 "services/database/User.hpp"
|
||||
|
||||
#include "services/database/Artist.hpp"
|
||||
#include "services/database/Release.hpp"
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/Track.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "IdTypeTraits.hpp"
|
||||
#include "StringViewTraits.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace Database {
|
||||
|
||||
User::User(std::string_view loginName)
|
||||
: _loginName {loginName}
|
||||
{
|
||||
}
|
||||
|
||||
std::size_t
|
||||
User::getCount(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM user");
|
||||
}
|
||||
|
||||
RangeResults<UserId>
|
||||
User::find(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
auto query {session.getDboSession().query<UserId>("SELECT id FROM user")};
|
||||
|
||||
if (params.scrobbler)
|
||||
query.where("scrobbler = ?").bind(*params.scrobbler);
|
||||
|
||||
return execQuery(query, params.range);
|
||||
}
|
||||
|
||||
User::pointer
|
||||
User::findDemoUser(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().find<User>().where("type = ?").bind(UserType::DEMO).resultValue();
|
||||
}
|
||||
|
||||
User::pointer
|
||||
User::create(Session& session, std::string_view loginName)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
|
||||
User::pointer user {session.getDboSession().add(std::make_unique<User>(loginName))};
|
||||
session.getDboSession().flush();
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
User::pointer
|
||||
User::find(Session& session, UserId id)
|
||||
{
|
||||
return session.getDboSession().find<User>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
|
||||
User::pointer
|
||||
User::find(Session& session, std::string_view name)
|
||||
{
|
||||
return session.getDboSession().find<User>()
|
||||
.where("login_name = ?").bind(name)
|
||||
.resultValue();
|
||||
}
|
||||
|
||||
void
|
||||
User::setSubsonicTranscodeBitrate(Bitrate bitrate)
|
||||
{
|
||||
assert(isAudioBitrateAllowed(bitrate));
|
||||
_subsonicTranscodeBitrate = bitrate;
|
||||
}
|
||||
|
||||
void
|
||||
User::clearAuthTokens()
|
||||
{
|
||||
_authTokens.clear();
|
||||
}
|
||||
|
||||
} // namespace Database
|
||||
|
||||
|
||||
@@ -29,5 +29,12 @@ namespace Database
|
||||
return StringUtils::escapeString(keyword, "%_", escapeChar);
|
||||
}
|
||||
|
||||
Wt::WDateTime
|
||||
normalizeDateTime(const Wt::WDateTime& dateTime)
|
||||
{
|
||||
// force second resolution
|
||||
return Wt::WDateTime::fromTime_t(dateTime.toTime_t());
|
||||
}
|
||||
|
||||
} // namespace Database
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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 <string_view>
|
||||
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
#include <Wt/WDateTime.h>
|
||||
|
||||
#include "services/database/Types.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
#define ESCAPE_CHAR_STR "\\"
|
||||
static inline constexpr char escapeChar {'\\'};
|
||||
std::string escapeLikeKeyword(std::string_view keywords);
|
||||
|
||||
template <typename T>
|
||||
RangeResults<T>
|
||||
execQuery(Wt::Dbo::Query<T>& query, Range range)
|
||||
{
|
||||
RangeResults<T> res;
|
||||
|
||||
auto collection {query.limit(range.size ? static_cast<int>(range.size) + 1 : -1)
|
||||
.offset(range.offset ? static_cast<int>(range.offset) : -1)
|
||||
.resultList()};
|
||||
|
||||
res.results.assign(collection.begin(), collection.end());
|
||||
if (range.size && res.results.size() == static_cast<std::size_t>(range.size) + 1)
|
||||
{
|
||||
res.moreResults = true;
|
||||
res.results.pop_back();
|
||||
}
|
||||
else
|
||||
res.moreResults = false;
|
||||
|
||||
res.range.offset = range.offset;
|
||||
res.range.size = res.results.size();
|
||||
return res;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
RangeResults<typename T::pointer>
|
||||
execQuery(Wt::Dbo::Query<Wt::Dbo::ptr<T>>& query, Range range)
|
||||
{
|
||||
RangeResults<typename T::pointer> res;
|
||||
|
||||
auto collection {query.limit(range.size ? static_cast<int>(range.size) + 1 : -1)
|
||||
.offset(range.offset ? static_cast<int>(range.offset) : -1)
|
||||
.resultList()};
|
||||
|
||||
res.results.assign(collection.begin(), collection.end());
|
||||
if (range.size && res.results.size() == static_cast<std::size_t>(range.size) + 1)
|
||||
{
|
||||
res.moreResults = true;
|
||||
res.results.pop_back();
|
||||
}
|
||||
else
|
||||
res.moreResults = false;
|
||||
|
||||
res.range.offset = range.offset;
|
||||
res.range.size = res.results.size();
|
||||
return res;
|
||||
}
|
||||
|
||||
Wt::WDateTime normalizeDateTime(const Wt::WDateTime& dateTime);
|
||||
} // namespace Database
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* Copyright (C) 2015 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 <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include <Wt/WDateTime.h>
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
|
||||
#include "services/database/ArtistId.hpp"
|
||||
#include "services/database/ClusterId.hpp"
|
||||
#include "services/database/Object.hpp"
|
||||
#include "services/database/Types.hpp"
|
||||
#include "services/database/UserId.hpp"
|
||||
#include "utils/EnumSet.hpp"
|
||||
#include "utils/UUID.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
|
||||
class Cluster;
|
||||
class ClusterType;
|
||||
class Release;
|
||||
class Session;
|
||||
class StarredArtist;
|
||||
class Track;
|
||||
class TrackArtistLink;
|
||||
class User;
|
||||
|
||||
class Artist : public Object<Artist, ArtistId>
|
||||
{
|
||||
public:
|
||||
struct FindParameters
|
||||
{
|
||||
std::vector<ClusterId> clusters; // if non empty, at least one artist that belongs to these clusters
|
||||
std::vector<std::string_view> keywords; // if non empty, name must match all of these keywords (on either name field OR sort name field)
|
||||
std::optional<TrackArtistLinkType> linkType; // if set, only artists that have produced at least one track with this link type
|
||||
ArtistSortMethod sortMethod {ArtistSortMethod::None};
|
||||
Range range;
|
||||
Wt::WDateTime writtenAfter;
|
||||
UserId starringUser; // only artists starred by this user
|
||||
std::optional<Scrobbler> scrobbler; // and for this scrobbler
|
||||
|
||||
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& setLinkType(std::optional<TrackArtistLinkType> _linkType) { linkType = _linkType; return *this; }
|
||||
FindParameters& setSortMethod(ArtistSortMethod _sortMethod) {sortMethod = _sortMethod; return *this; }
|
||||
FindParameters& setRange(Range _range) {range = _range; return *this; }
|
||||
FindParameters& setWrittenAfter(const Wt::WDateTime& _after) { writtenAfter = _after; return *this; }
|
||||
FindParameters& setStarringUser(UserId _user, Scrobbler _scrobbler) { starringUser = _user; scrobbler = _scrobbler; return *this; }
|
||||
};
|
||||
|
||||
Artist() = default;
|
||||
Artist(const std::string& name, const std::optional<UUID>& MBID = {});
|
||||
|
||||
// Accessors
|
||||
static std::size_t getCount(Session& session);
|
||||
static pointer find(Session& session, const UUID& MBID);
|
||||
static pointer find(Session& session, ArtistId id);
|
||||
static std::vector<pointer> find(Session& session, const std::string& name); // exact match on name field
|
||||
static RangeResults<ArtistId> find(Session& session, const FindParameters& parameters);
|
||||
static RangeResults<ArtistId> findAllOrphans(Session& session, Range range); // No track related
|
||||
static bool exists(Session& session, ArtistId id);
|
||||
|
||||
|
||||
// Accessors
|
||||
const std::string& getName() const { return _name; }
|
||||
const std::string& getSortName() const { return _sortName; }
|
||||
std::optional<UUID> getMBID() const { return UUID::fromString(_MBID); }
|
||||
|
||||
std::vector<ObjectPtr<Release>> getReleases(const std::vector<ClusterId>& clusterIds = {}) const; // if non empty, get the releases that match all these clusters
|
||||
std::size_t getReleaseCount() const;
|
||||
std::vector<ObjectPtr<Track>> getTracks(std::optional<TrackArtistLinkType> linkType = {}) const;
|
||||
bool hasNonReleaseTracks(std::optional<TrackArtistLinkType> linkType = std::nullopt) const;
|
||||
RangeResults<ObjectPtr<Track>> getNonReleaseTracks(std::optional<TrackArtistLinkType> linkType, Range range) const;
|
||||
std::vector<ObjectPtr<Track>> getRandomTracks(std::optional<std::size_t> count) const;
|
||||
|
||||
// No artistLinkTypes means get them all
|
||||
RangeResults<ArtistId> findSimilarArtists(EnumSet<TrackArtistLinkType> artistLinkTypes = {}, Range range = {}) const;
|
||||
|
||||
// Get the cluster of the tracks made by this artist
|
||||
// Each clusters are grouped by cluster type, sorted by the number of occurence
|
||||
// size is the max number of cluster per cluster type
|
||||
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(std::vector<ObjectPtr<ClusterType>> clusterTypes, std::size_t size) const;
|
||||
|
||||
void setName(std::string_view name) { _name = name; }
|
||||
void setMBID(const std::optional<UUID>& mbid) { _MBID = mbid ? mbid->getAsString() : ""; }
|
||||
void setSortName(const std::string& sortName);
|
||||
|
||||
// Create
|
||||
static pointer create(Session& session, const std::string& name, const std::optional<UUID>& UUID = {});
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _name, "name");
|
||||
Wt::Dbo::field(a, _sortName, "sort_name");
|
||||
Wt::Dbo::field(a, _MBID, "mbid");
|
||||
|
||||
Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "artist");
|
||||
Wt::Dbo::hasMany(a, _starredArtists, Wt::Dbo::ManyToMany, "user_starred_artists", "", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
|
||||
private:
|
||||
static const std::size_t _maxNameLength = 128;
|
||||
|
||||
std::string _name;
|
||||
std::string _sortName;
|
||||
std::string _MBID; // Musicbrainz Identifier
|
||||
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks; // Tracks involving this artist
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<StarredArtist>> _starredArtists; // starred entries for this artist
|
||||
};
|
||||
|
||||
} // namespace Database
|
||||
|
||||
+2
-15
@@ -19,20 +19,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "database/Types.hpp"
|
||||
#include "utils/EnumSet.hpp"
|
||||
#include "services/database/IdType.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Db;
|
||||
class Session;
|
||||
}
|
||||
|
||||
namespace Recommendation
|
||||
{
|
||||
class IRecommendation
|
||||
{
|
||||
};
|
||||
|
||||
} // ns Recommendation
|
||||
LMS_DECLARE_IDTYPE(ArtistId)
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright (C) 2013 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_view>
|
||||
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
#include <Wt/WDateTime.h>
|
||||
|
||||
#include "services/database/AuthTokenId.hpp"
|
||||
#include "services/database/Object.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Session;
|
||||
|
||||
class User;
|
||||
class AuthToken : public Object<AuthToken, AuthTokenId>
|
||||
{
|
||||
public:
|
||||
AuthToken() = default;
|
||||
AuthToken(std::string_view value, const Wt::WDateTime& expiry, ObjectPtr<User> user);
|
||||
|
||||
// Utility
|
||||
static pointer create(Session& session, std::string_view value, const Wt::WDateTime&expiry, ObjectPtr<User> user);
|
||||
static void removeExpiredTokens(Session& session, const Wt::WDateTime& now);
|
||||
static pointer find(Session& session, std::string_view value);
|
||||
|
||||
// Accessors
|
||||
const Wt::WDateTime& getExpiry() const { return _expiry; }
|
||||
ObjectPtr<User> getUser() const { return _user; }
|
||||
const std::string& getValue() const { return _value; }
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
{
|
||||
Wt::Dbo::field(a, _value, "value");
|
||||
Wt::Dbo::field(a, _expiry, "expiry");
|
||||
Wt::Dbo::belongsTo(a, _user, "user", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
|
||||
private:
|
||||
std::string _value;
|
||||
Wt::WDateTime _expiry;
|
||||
Wt::Dbo::ptr<User> _user;
|
||||
};
|
||||
} // namespace Databas'
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user