Merge branch 'develop' for release v3.45.0
This commit is contained in:
@@ -40,13 +40,12 @@ __Note__: depending on your database size and/or your hardware, the tag-based re
|
||||
_LMS_ relies exclusively on tags to organize your music collection.
|
||||
|
||||
### Filtering
|
||||
You can specify the tags you want to use to filter your collection. By default, `genre`, `albumgrouping`, `mood` and `albummood` tags are used.
|
||||
In the administration settings, you can set whatever tags you want, even custom tags.
|
||||
It is possible to apply global filters on your collection using `genre`, `mood`, `grouping` and `language` tags. More tags can be added in the database administration settings, even custom tags.
|
||||
|
||||
__Note__: you can use the `lms-metadata` tool to have an idea of the tags parsed by _LMS_ using [TagLib](https://github.com/taglib/taglib).
|
||||
|
||||
### Multiple album artists
|
||||
_LMS_ requires the `ALBUMARTISTS` and `ALBUMARTISTSSORT` tags to properly handle multiple album artists on the same album. As they are a custom tags, you may need to setup your favorite tagger to add them.
|
||||
_LMS_ requires the `albumartists` and `albumartistssort` tags to properly handle multiple album artists on the same album. As they are a custom tags, you may need to setup your favorite tagger to add them.
|
||||
|
||||
__Note__: if you use [Picard](https://picard.musicbrainz.org/), add the following script to include these tags:
|
||||
```
|
||||
|
||||
@@ -44,12 +44,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label" for="${id:clusters}">
|
||||
${tr:Lms.clusters}
|
||||
<label class="form-label" for="${id:extra-tags-to-scan}">
|
||||
${tr:Lms.Admin.Database.extra-tags-to-scan}
|
||||
</label>
|
||||
${clusters class="form-control"}
|
||||
${extra-tags-to-scan class="form-control"}
|
||||
<div class="invalid-feedback">
|
||||
${clusters-info}
|
||||
${extra-tags-to-scan-info}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
<message id="Lms.delete">Delete</message>
|
||||
<message id="Lms.discard">Discard</message>
|
||||
<message id="Lms.edit">Edit</message>
|
||||
<message id="Lms.field-must-be-in-upper-case">This field must be in upper case</message>
|
||||
<message id="Lms.loading">Loading...</message>
|
||||
<message id="Lms.login">Login</message>
|
||||
<message id="Lms.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> Logout</message>
|
||||
@@ -46,6 +47,7 @@
|
||||
<!--Administration-->
|
||||
<message id="Lms.Admin.Database.daily">Daily</message>
|
||||
<message id="Lms.Admin.Database.database">Music collection</message>
|
||||
<message id="Lms.Admin.Database.extra-tags-to-scan">Extra tags to scan (use <kbd>;</kbd> as separator)</message>
|
||||
<message id="Lms.Admin.Database.hourly">Hourly</message>
|
||||
<message id="Lms.Admin.Database.immediate-scan">Scan now!</message>
|
||||
<message id="Lms.Admin.Database.monthly">Monthly</message>
|
||||
@@ -135,6 +137,7 @@
|
||||
<message id="Lms.Explore.play-last">Play last</message>
|
||||
<message id="Lms.Explore.play-next">Play next</message>
|
||||
<message id="Lms.Explore.play-shuffled">Play shuffled</message>
|
||||
<message id="Lms.Explore.playcount">Play count</message>
|
||||
<message id="Lms.Explore.random">Random</message>
|
||||
<message id="Lms.Explore.recently-added">Recently added</message>
|
||||
<message id="Lms.Explore.recently-modified">Recently modified</message>
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
<message id="Lms.delete">Supprimer</message>
|
||||
<message id="Lms.discard">Annuler</message>
|
||||
<message id="Lms.edit">Editer</message>
|
||||
<message id="Lms.field-must-be-in-upper-case">Ce champ doit être en majuscule</message>
|
||||
<message id="Lms.loading">Chargement...</message>
|
||||
<message id="Lms.login">Login</message>
|
||||
<message id="Lms.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> Quitter</message>
|
||||
@@ -46,6 +47,7 @@
|
||||
<!--Administration-->
|
||||
<message id="Lms.Admin.Database.daily">Tous les jours</message>
|
||||
<message id="Lms.Admin.Database.database">Collection de musiques</message>
|
||||
<message id="Lms.Admin.Database.extra-tags-to-scan">Tags supplémentaires à scanner (utiliser <kbd>;</kbd> comme séparateur)</message>
|
||||
<message id="Lms.Admin.Database.hourly">Toutes les heures</message>
|
||||
<message id="Lms.Admin.Database.immediate-scan">Scanner maintenant !</message>
|
||||
<message id="Lms.Admin.Database.monthly">Tous les mois</message>
|
||||
@@ -135,6 +137,7 @@
|
||||
<message id="Lms.Explore.play-last">Jouer en dernier</message>
|
||||
<message id="Lms.Explore.play-next">Jouer ensuite</message>
|
||||
<message id="Lms.Explore.play-shuffled">Jouer aléatoirement</message>
|
||||
<message id="Lms.Explore.playcount">Nombre d'écoutes</message>
|
||||
<message id="Lms.Explore.random">Aléatoire</message>
|
||||
<message id="Lms.Explore.recently-added">Ajoutés récemment</message>
|
||||
<message id="Lms.Explore.recently-modified">Modifiés récemment</message>
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
<message id="Lms.delete">Elimina</message>
|
||||
<message id="Lms.discard">Annulla</message>
|
||||
<message id="Lms.edit">Modifica</message>
|
||||
|
||||
<message id="Lms.loading">Caricamento...</message>
|
||||
<message id="Lms.login">Login</message>
|
||||
<message id="Lms.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> Logout</message>
|
||||
@@ -46,6 +47,7 @@
|
||||
<!--Administration-->
|
||||
<message id="Lms.Admin.Database.daily">Giornaliera</message>
|
||||
<message id="Lms.Admin.Database.database">Raccolta musicale</message>
|
||||
|
||||
<message id="Lms.Admin.Database.hourly">Ogni ora</message>
|
||||
<message id="Lms.Admin.Database.immediate-scan">Scansiona ora!</message>
|
||||
<message id="Lms.Admin.Database.monthly">Mensile</message>
|
||||
@@ -135,6 +137,7 @@
|
||||
<message id="Lms.Explore.play-last">Aggiungi alla coda di riproduzione</message>
|
||||
|
||||
<message id="Lms.Explore.play-shuffled">Aggiungi alla coda di riproduzione mischiando</message>
|
||||
|
||||
<message id="Lms.Explore.random">Casuali</message>
|
||||
<message id="Lms.Explore.recently-added">Aggiunti di recente</message>
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
<message id="Lms.delete">删除</message>
|
||||
<message id="Lms.discard">撤销</message>
|
||||
<message id="Lms.edit">编辑</message>
|
||||
|
||||
<message id="Lms.loading">加载中...</message>
|
||||
<message id="Lms.login">登陆</message>
|
||||
<message id="Lms.logout"><i class="fa fa-fw fa-sign-out" aria-hidden="true"></i> 注销</message>
|
||||
@@ -46,6 +47,7 @@
|
||||
<!--Administration-->
|
||||
<message id="Lms.Admin.Database.daily">每日</message>
|
||||
<message id="Lms.Admin.Database.database">音乐收藏</message>
|
||||
|
||||
<message id="Lms.Admin.Database.hourly">每小时</message>
|
||||
<message id="Lms.Admin.Database.immediate-scan">立即扫描!</message>
|
||||
<message id="Lms.Admin.Database.monthly">每月</message>
|
||||
@@ -135,6 +137,7 @@
|
||||
<message id="Lms.Explore.play-last">最后播放</message>
|
||||
|
||||
<message id="Lms.Explore.play-shuffled">随机播放</message>
|
||||
|
||||
<message id="Lms.Explore.random">随机</message>
|
||||
<message id="Lms.Explore.recently-added">最近添加</message>
|
||||
<message id="Lms.Explore.recently-modified">最近更改</message>
|
||||
|
||||
@@ -137,6 +137,14 @@
|
||||
</div>
|
||||
</div>
|
||||
${</if-has-bitrate>}
|
||||
<div class="row align-items-center">
|
||||
<div class="col-4">
|
||||
${tr:Lms.Explore.playcount}
|
||||
</div>
|
||||
<div class="col">
|
||||
${playcount}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
|
||||
@@ -133,6 +133,14 @@
|
||||
</div>
|
||||
</div>
|
||||
${</if-has-bitrate>}
|
||||
<div class="row align-items-center">
|
||||
<div class="col-4">
|
||||
${tr:Lms.Explore.playcount}
|
||||
</div>
|
||||
<div class="col">
|
||||
${playcount}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
|
||||
+4
-2
@@ -10,8 +10,10 @@ ffmpeg-file = "/usr/bin/ffmpeg";
|
||||
# Log files, empty means stdout
|
||||
log-file = "";
|
||||
access-log-file = "";
|
||||
# Logger configuration, see log-config in https://webtoolkit.eu/wt/doc/reference/html/overview.html#config_general
|
||||
log-config = "* -debug -info:WebRequest";
|
||||
# Minimum severity, can be "debug", "info", "warning", "error" or "fatal"
|
||||
log-min-severity = "info";
|
||||
# Output db queries on stdout
|
||||
db-show-queries = false;
|
||||
|
||||
# Listen port/addr of the web server
|
||||
listen-port = 5082;
|
||||
|
||||
@@ -31,7 +31,7 @@ extern "C"
|
||||
#include <map>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
#include "utils/String.hpp"
|
||||
|
||||
namespace Av
|
||||
@@ -105,14 +105,14 @@ namespace Av
|
||||
int error{ avformat_open_input(&_context, _p.string().c_str(), nullptr, nullptr) };
|
||||
if (error < 0)
|
||||
{
|
||||
LMS_LOG(AV, ERROR) << "Cannot open " << _p.string() << ": " << averror_to_string(error);
|
||||
LMS_LOG(AV, ERROR, "Cannot open " << _p.string() << ": " << averror_to_string(error));
|
||||
throw AudioFileException{ error };
|
||||
}
|
||||
|
||||
error = avformat_find_stream_info(_context, nullptr);
|
||||
if (error < 0)
|
||||
{
|
||||
LMS_LOG(AV, ERROR) << "Cannot find stream information on " << _p.string() << ": " << averror_to_string(error);
|
||||
LMS_LOG(AV, ERROR, "Cannot find stream information on " << _p.string() << ": " << averror_to_string(error));
|
||||
avformat_close_input(&_context);
|
||||
throw AudioFileException{ error };
|
||||
}
|
||||
@@ -233,7 +233,7 @@ namespace Av
|
||||
|
||||
if (avstream->codecpar == nullptr)
|
||||
{
|
||||
LMS_LOG(AV, ERROR) << "Skipping stream " << i << " since no codecpar is set";
|
||||
LMS_LOG(AV, ERROR, "Skipping stream " << i << " since no codecpar is set");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -247,7 +247,7 @@ namespace Av
|
||||
else
|
||||
{
|
||||
picture.mimeType = "application/octet-stream";
|
||||
LMS_LOG(AV, ERROR) << "CODEC ID " << avstream->codecpar->codec_id << " not handled in mime type conversion";
|
||||
LMS_LOG(AV, ERROR, "CODEC ID " << avstream->codecpar->codec_id << " not handled in mime type conversion");
|
||||
}
|
||||
|
||||
const AVPacket& pkt{ avstream->attached_pic };
|
||||
@@ -271,7 +271,7 @@ namespace Av
|
||||
|
||||
if (!avstream->codecpar)
|
||||
{
|
||||
LMS_LOG(AV, ERROR) << "Skipping stream " << streamIndex << " since no codecpar is set";
|
||||
LMS_LOG(AV, ERROR, "Skipping stream " << streamIndex << " since no codecpar is set");
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -293,7 +293,7 @@ namespace Av
|
||||
// List should be sync with the demuxers shipped in the lms's docker version
|
||||
// + the _audioFileExtensions in ScanSettings
|
||||
// std::filesystem::path does not seem to have std::hash specialization on freebsd
|
||||
static const std::map<std::filesystem::path, std::string_view> entries
|
||||
static const std::unordered_map<std::string, std::string_view> entries
|
||||
{
|
||||
{".mp3", "audio/mpeg"},
|
||||
{".ogg", "audio/ogg"},
|
||||
@@ -320,7 +320,7 @@ namespace Av
|
||||
{".mka", "audio/x-matroska"},
|
||||
};
|
||||
|
||||
auto it{ entries.find(fileExtension) };
|
||||
auto it{ entries.find(StringUtils::stringToLower(fileExtension.string())) };
|
||||
if (it == std::cend(entries))
|
||||
return "";
|
||||
|
||||
|
||||
@@ -25,13 +25,13 @@
|
||||
#include "utils/IChildProcessManager.hpp"
|
||||
#include "utils/IConfig.hpp"
|
||||
#include "utils/Path.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
#include "utils/Service.hpp"
|
||||
|
||||
namespace Av::Transcoding
|
||||
{
|
||||
|
||||
#define LOG(sev) LMS_LOG(TRANSCODING, sev) << "[" << _debugId << "] - "
|
||||
#define LOG(severity, message) LMS_LOG(TRANSCODING, severity, "[" << _debugId << "] - " << message)
|
||||
|
||||
static std::atomic<size_t> globalId{};
|
||||
static std::filesystem::path ffmpegPath;
|
||||
@@ -84,7 +84,7 @@ namespace Av::Transcoding
|
||||
throw Exception{ "File error '" + _inputParameters.trackPath.string() + "': " + e.what() };
|
||||
}
|
||||
|
||||
LOG(INFO) << "Transcoding file '" << _inputParameters.trackPath.string() << "'";
|
||||
LOG(INFO, "Transcoding file '" << _inputParameters.trackPath.string() << "'");
|
||||
|
||||
std::vector<std::string> args;
|
||||
|
||||
@@ -176,9 +176,9 @@ namespace Av::Transcoding
|
||||
|
||||
args.emplace_back("pipe:1");
|
||||
|
||||
LOG(DEBUG) << "Dumping args (" << args.size() << ")";
|
||||
LOG(DEBUG, "Dumping args (" << args.size() << ")");
|
||||
for (const std::string& arg : args)
|
||||
LOG(DEBUG) << "Arg = '" << arg << "'";
|
||||
LOG(DEBUG, "Arg = '" << arg << "'");
|
||||
|
||||
// Caution: stdin must have been closed before
|
||||
try
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
*/
|
||||
|
||||
#include "TranscodingResourceHandler.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
|
||||
namespace Av::Transcoding
|
||||
{
|
||||
@@ -43,9 +43,9 @@ namespace Av::Transcoding
|
||||
, _transcoder{ inputParameters, outputParameters }
|
||||
{
|
||||
if (_estimatedContentLength)
|
||||
LMS_LOG(TRANSCODING, DEBUG) << "Estimated content length = " << *_estimatedContentLength;
|
||||
LMS_LOG(TRANSCODING, DEBUG, "Estimated content length = " << *_estimatedContentLength);
|
||||
else
|
||||
LMS_LOG(TRANSCODING, DEBUG) << "Not using estimated content length";
|
||||
LMS_LOG(TRANSCODING, DEBUG, "Not using estimated content length");
|
||||
}
|
||||
|
||||
Wt::Http::ResponseContinuation* TranscodingResourceHandler::processRequest(const Wt::Http::Request& /*request*/, Wt::Http::Response& response)
|
||||
@@ -53,11 +53,11 @@ namespace Av::Transcoding
|
||||
if (_estimatedContentLength)
|
||||
response.setContentLength(*_estimatedContentLength);
|
||||
response.setMimeType(_transcoder.getOutputMimeType());
|
||||
LMS_LOG(TRANSCODING, DEBUG) << "Transcoder finished = " << _transcoder.finished() << ", total served bytes = " << _totalServedByteCount << ", mime type = " << _transcoder.getOutputMimeType();
|
||||
LMS_LOG(TRANSCODING, DEBUG, "Transcoder finished = " << _transcoder.finished() << ", total served bytes = " << _totalServedByteCount << ", mime type = " << _transcoder.getOutputMimeType());
|
||||
|
||||
if (_bytesReadyCount > 0)
|
||||
{
|
||||
LMS_LOG(TRANSCODING, DEBUG) << "Writing " << _bytesReadyCount << " bytes back to client";
|
||||
LMS_LOG(TRANSCODING, DEBUG, "Writing " << _bytesReadyCount << " bytes back to client");
|
||||
|
||||
response.out().write(reinterpret_cast<const char*>(&_buffer[0]), _bytesReadyCount);
|
||||
_totalServedByteCount += _bytesReadyCount;
|
||||
@@ -70,7 +70,7 @@ namespace Av::Transcoding
|
||||
continuation->waitForMoreData();
|
||||
_transcoder.asyncRead(_buffer.data(), _buffer.size(), [=](std::size_t nbBytesRead)
|
||||
{
|
||||
LMS_LOG(TRANSCODING, DEBUG) << "Have " << nbBytesRead << " more bytes to send back";
|
||||
LMS_LOG(TRANSCODING, DEBUG, "Have " << nbBytesRead << " more bytes to send back");
|
||||
|
||||
assert(_bytesReadyCount == 0);
|
||||
_bytesReadyCount = nbBytesRead;
|
||||
@@ -86,7 +86,7 @@ namespace Av::Transcoding
|
||||
{
|
||||
const std::size_t padSize{ *_estimatedContentLength - _totalServedByteCount };
|
||||
|
||||
LMS_LOG(TRANSCODING, DEBUG) << "Adding " << padSize << " padding bytes";
|
||||
LMS_LOG(TRANSCODING, DEBUG, "Adding " << padSize << " padding bytes");
|
||||
|
||||
for (std::size_t i{}; i < padSize; ++i)
|
||||
response.out().put(0);
|
||||
@@ -94,7 +94,7 @@ namespace Av::Transcoding
|
||||
_totalServedByteCount += padSize;
|
||||
}
|
||||
|
||||
LMS_LOG(TRANSCODING, DEBUG) << "Transcoding finished. Total served byte count = " << _totalServedByteCount;
|
||||
LMS_LOG(TRANSCODING, DEBUG, "Transcoding finished. Total served byte count = " << _totalServedByteCount);
|
||||
}
|
||||
|
||||
return {};
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
#include "RawImage.hpp"
|
||||
#include "image/Exception.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
|
||||
namespace Image::GraphicsMagick
|
||||
{
|
||||
@@ -36,7 +36,7 @@ namespace Image::GraphicsMagick
|
||||
}
|
||||
catch (Magick::Exception& e)
|
||||
{
|
||||
LMS_LOG(COVER, ERROR) << "Caught Magick exception: " << e.what();
|
||||
LMS_LOG(COVER, ERROR, "Caught Magick exception: " << e.what());
|
||||
throw ImageException {std::string {"Magick read error: "} + e.what()};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
#include "JPEGImage.hpp"
|
||||
#include "image/Exception.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
|
||||
namespace Image
|
||||
{
|
||||
@@ -43,16 +43,16 @@ namespace Image
|
||||
Magick::InitializeMagick(path.string().c_str());
|
||||
|
||||
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, WARNING, "Consider setting env var OMP_NUM_THREADS=1 to save resources");
|
||||
|
||||
if (!MagickLib::SetMagickResourceLimit(MagickLib::ThreadsResource, 1))
|
||||
LMS_LOG(COVER, ERROR) << "Cannot set Magick thread resource limit to 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, 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);
|
||||
LMS_LOG(COVER, INFO, "Magick threads resource limit = " << GetMagickResourceLimit(MagickLib::ThreadsResource));
|
||||
LMS_LOG(COVER, INFO, "Magick Disk resource limit = " << GetMagickResourceLimit(MagickLib::DiskResource));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,16 +68,16 @@ RawImage::RawImage(const std::byte* encodedData, std::size_t encodedDataSize)
|
||||
}
|
||||
catch (Magick::WarningCoder& e)
|
||||
{
|
||||
LMS_LOG(COVER, WARNING) << "Caught Magick WarningCoder: " << e.what();
|
||||
LMS_LOG(COVER, WARNING, "Caught Magick WarningCoder: " << e.what());
|
||||
}
|
||||
catch (Magick::Warning& e)
|
||||
{
|
||||
LMS_LOG(COVER, WARNING) << "Caught Magick warning: " << e.what();
|
||||
LMS_LOG(COVER, WARNING, "Caught Magick warning: " << e.what());
|
||||
throw ImageException {std::string {"Magick read warning: "} + e.what()};
|
||||
}
|
||||
catch (Magick::Exception& e)
|
||||
{
|
||||
LMS_LOG(COVER, ERROR) << "Caught Magick exception: " << e.what();
|
||||
LMS_LOG(COVER, ERROR, "Caught Magick exception: " << e.what());
|
||||
throw ImageException {std::string {"Magick read error: "} + e.what()};
|
||||
}
|
||||
}
|
||||
@@ -90,16 +90,16 @@ RawImage::RawImage(const std::filesystem::path& p)
|
||||
}
|
||||
catch (Magick::WarningCoder& e)
|
||||
{
|
||||
LMS_LOG(COVER, WARNING) << "Caught Magick WarningCoder: " << e.what();
|
||||
LMS_LOG(COVER, WARNING, "Caught Magick WarningCoder: " << e.what());
|
||||
}
|
||||
catch (Magick::Warning& e)
|
||||
{
|
||||
LMS_LOG(COVER, WARNING) << "Caught Magick warning: " << e.what();
|
||||
LMS_LOG(COVER, WARNING, "Caught Magick warning: " << e.what());
|
||||
throw ImageException {std::string {"Magick read warning: "} + e.what()};
|
||||
}
|
||||
catch (Magick::Exception& e)
|
||||
{
|
||||
LMS_LOG(COVER, ERROR) << "Caught Magick exception: " << e.what();
|
||||
LMS_LOG(COVER, ERROR, "Caught Magick exception: " << e.what());
|
||||
throw ImageException {std::string {"Magick read error: "} + e.what()};
|
||||
}
|
||||
}
|
||||
@@ -113,7 +113,7 @@ RawImage::resize(ImageSize width)
|
||||
}
|
||||
catch (Magick::Exception& e)
|
||||
{
|
||||
LMS_LOG(COVER, ERROR) << "Caught Magick exception while resizing: " << e.what();
|
||||
LMS_LOG(COVER, ERROR, "Caught Magick exception while resizing: " << e.what());
|
||||
throw ImageException {std::string {"Magick resize error: "} + e.what()};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,240 +23,228 @@
|
||||
#include <iostream>
|
||||
|
||||
#include "av/IAudioFile.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
#include "utils/String.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
namespace MetaData
|
||||
{
|
||||
namespace
|
||||
{
|
||||
template <typename T>
|
||||
std::optional<T> findFirstValueOfAs(const Av::IAudioFile::MetadataMap& metadataMap, std::initializer_list<std::string> tags)
|
||||
{
|
||||
auto it = std::find_first_of(std::cbegin(metadataMap), std::cend(metadataMap), std::cbegin(tags), std::cend(tags), [](const auto& it, const auto& str) { return it.first == str; });
|
||||
if (it == std::cend(metadataMap))
|
||||
return std::nullopt;
|
||||
|
||||
template <typename T>
|
||||
std::optional<T>
|
||||
findFirstValueOfAs(const Av::IAudioFile::MetadataMap& metadataMap, std::initializer_list<std::string> tags)
|
||||
{
|
||||
auto it = std::find_first_of(std::cbegin(metadataMap), std::cend(metadataMap), std::cbegin(tags), std::cend(tags), [](const auto& it, const auto& str) { return it.first == str; });
|
||||
if (it == std::cend(metadataMap))
|
||||
return std::nullopt;
|
||||
return StringUtils::readAs<T>(StringUtils::stringTrim(it->second));
|
||||
}
|
||||
|
||||
return StringUtils::readAs<T>(StringUtils::stringTrim(it->second));
|
||||
}
|
||||
template <>
|
||||
std::optional<std::vector<UUID>> findFirstValueOfAs(const Av::IAudioFile::MetadataMap& metadataMap, std::initializer_list<std::string> tags)
|
||||
{
|
||||
std::optional<std::string> str{ findFirstValueOfAs<std::string>(metadataMap, tags) };
|
||||
if (!str)
|
||||
return std::nullopt;
|
||||
|
||||
template <>
|
||||
std::optional<std::vector<UUID>>
|
||||
findFirstValueOfAs(const Av::IAudioFile::MetadataMap& metadataMap, std::initializer_list<std::string> tags)
|
||||
{
|
||||
std::optional<std::string> str {findFirstValueOfAs<std::string>(metadataMap, tags)};
|
||||
if (!str)
|
||||
return std::nullopt;
|
||||
const std::vector<std::string_view> strUuids{ StringUtils::splitString(*str, "/") };
|
||||
std::vector<UUID> res;
|
||||
|
||||
const std::vector<std::string_view> strUuids {StringUtils::splitString(*str, "/")};
|
||||
std::vector<UUID> res;
|
||||
for (std::string_view strUuid : strUuids)
|
||||
{
|
||||
std::optional<UUID> uuid{ UUID::fromString(strUuid) };
|
||||
if (!uuid)
|
||||
return std::nullopt;
|
||||
|
||||
for (std::string_view strUuid : strUuids)
|
||||
{
|
||||
std::optional<UUID> uuid {UUID::fromString(strUuid)};
|
||||
if (!uuid)
|
||||
return std::nullopt;
|
||||
res.push_back(std::move(*uuid));
|
||||
}
|
||||
|
||||
res.push_back(std::move(*uuid));
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
std::vector<Artist> getReleaseArtists(const Av::IAudioFile::MetadataMap& metadataMap)
|
||||
{
|
||||
std::vector<Artist> res;
|
||||
|
||||
static
|
||||
std::vector<Artist>
|
||||
getReleaseArtists(const Av::IAudioFile::MetadataMap& metadataMap)
|
||||
{
|
||||
std::vector<Artist> res;
|
||||
auto name{ findFirstValueOfAs<std::string>(metadataMap, {"ALBUM_ARTIST"}) };
|
||||
if (!name)
|
||||
return res;
|
||||
|
||||
auto name {findFirstValueOfAs<std::string>(metadataMap, {"ALBUM_ARTIST"})};
|
||||
if (!name)
|
||||
return res;
|
||||
auto mbid{ findFirstValueOfAs<UUID>(metadataMap, {"MUSICBRAINZ ALBUM ARTIST ID", "MUSICBRAINZ/ALBUM ARTIST ID"}) };
|
||||
|
||||
auto mbid {findFirstValueOfAs<UUID>(metadataMap, {"MUSICBRAINZ ALBUM ARTIST ID", "MUSICBRAINZ/ALBUM ARTIST ID"})};
|
||||
return { Artist {mbid, *name, std::nullopt} };
|
||||
}
|
||||
|
||||
return {Artist {mbid, *name, std::nullopt} };
|
||||
}
|
||||
std::vector<Artist> getArtists(const Av::IAudioFile::MetadataMap& metadataMap)
|
||||
{
|
||||
std::vector<Artist> artists;
|
||||
|
||||
static
|
||||
std::vector<Artist>
|
||||
getArtists(const Av::IAudioFile::MetadataMap& metadataMap)
|
||||
{
|
||||
std::vector<Artist> artists;
|
||||
std::vector<std::string_view> artistNames;
|
||||
if (metadataMap.find("ARTISTS") != metadataMap.end())
|
||||
{
|
||||
artistNames = StringUtils::splitString(metadataMap.find("ARTISTS")->second, "/;");
|
||||
}
|
||||
else if (metadataMap.find("ARTIST") != metadataMap.end())
|
||||
{
|
||||
artistNames = { metadataMap.find("ARTIST")->second };
|
||||
}
|
||||
|
||||
std::vector<std::string_view> artistNames;
|
||||
if (metadataMap.find("ARTISTS") != metadataMap.end())
|
||||
{
|
||||
artistNames = StringUtils::splitString(metadataMap.find("ARTISTS")->second, "/;");
|
||||
}
|
||||
else if (metadataMap.find("ARTIST") != metadataMap.end())
|
||||
{
|
||||
artistNames = {metadataMap.find("ARTIST")->second};
|
||||
}
|
||||
auto artistMBIDs{ findFirstValueOfAs<std::vector<UUID>>(metadataMap, {"MUSICBRAINZ ARTIST ID", "MUSICBRAINZ_ARTISTID", "MUSICBRAINZ/ARTIST ID"}) };
|
||||
|
||||
auto artistMBIDs {findFirstValueOfAs<std::vector<UUID>>(metadataMap, {"MUSICBRAINZ ARTIST ID", "MUSICBRAINZ_ARTISTID", "MUSICBRAINZ/ARTIST ID"})};
|
||||
for (std::size_t i{}; i < artistNames.size(); ++i)
|
||||
{
|
||||
if (artistMBIDs && artistNames.size() == artistMBIDs->size())
|
||||
artists.emplace_back(Artist{ (*artistMBIDs)[i], artistNames[i], std::nullopt });
|
||||
else
|
||||
artists.emplace_back(Artist{ std::nullopt, artistNames[i], std::nullopt });
|
||||
}
|
||||
|
||||
for (std::size_t i {}; i < artistNames.size(); ++i)
|
||||
{
|
||||
if (artistMBIDs && artistNames.size() == artistMBIDs->size())
|
||||
artists.emplace_back(Artist {(*artistMBIDs)[i], artistNames[i], std::nullopt});
|
||||
else
|
||||
artists.emplace_back(Artist {std::nullopt, artistNames[i], std::nullopt});
|
||||
}
|
||||
return artists;
|
||||
}
|
||||
|
||||
return artists;
|
||||
}
|
||||
std::optional<Release> getRelease(const Av::IAudioFile::MetadataMap& metadataMap)
|
||||
{
|
||||
std::optional<Release> res;
|
||||
|
||||
static
|
||||
std::optional<Release>
|
||||
getRelease(const Av::IAudioFile::MetadataMap& metadataMap)
|
||||
{
|
||||
std::optional<Release> res;
|
||||
std::optional<std::string> releaseName{ findFirstValueOfAs<std::string>(metadataMap, {"ALBUM", "TALB", "WM/ALBUMTITLE"}) };
|
||||
if (!releaseName)
|
||||
return res;
|
||||
|
||||
std::optional<std::string> releaseName {findFirstValueOfAs<std::string>(metadataMap, {"ALBUM", "TALB", "WM/ALBUMTITLE"})};
|
||||
if (!releaseName)
|
||||
return res;
|
||||
res.emplace();
|
||||
res->name = std::move(*releaseName);
|
||||
res->mbid = findFirstValueOfAs<UUID>(metadataMap, { "MUSICBRAINZ ALBUM ID", "MUSICBRAINZ_ALBUMID", "MUSICBRAINZ/ALBUM ID" });
|
||||
res->artists = getReleaseArtists(metadataMap);
|
||||
res->mediumCount = findFirstValueOfAs<std::size_t>(metadataMap, { "TOTALDISCS", "DISCTOTAL" });
|
||||
if (!res->mediumCount)
|
||||
{
|
||||
// mediumCount may be encoded as position/count
|
||||
if (const auto value{ findFirstValueOfAs<std::string>(metadataMap, {"TPOS", "DISC", "DISK", "DISCNUMBER", "WM/PARTOFSET"}) })
|
||||
{
|
||||
// Expecting 'Number/Total'
|
||||
const std::vector<std::string_view> strings{ StringUtils::splitString(*value, "/") };
|
||||
if (strings.size() == 2)
|
||||
res->mediumCount = StringUtils::readAs<std::size_t>(strings[1]);
|
||||
}
|
||||
}
|
||||
|
||||
res.emplace();
|
||||
res->name = std::move(*releaseName);
|
||||
res->mbid = findFirstValueOfAs<UUID>(metadataMap, {"MUSICBRAINZ ALBUM ID", "MUSICBRAINZ_ALBUMID", "MUSICBRAINZ/ALBUM ID"});
|
||||
res->artists = getReleaseArtists(metadataMap);
|
||||
res->mediumCount = findFirstValueOfAs<std::size_t>(metadataMap, {"TOTALDISCS", "DISCTOTAL"});
|
||||
if (!res->mediumCount)
|
||||
{
|
||||
// mediumCount may be encoded as position/count
|
||||
if (const auto value {findFirstValueOfAs<std::string>(metadataMap, {"TPOS", "DISC", "DISK", "DISCNUMBER", "WM/PARTOFSET"})})
|
||||
{
|
||||
// Expecting 'Number/Total'
|
||||
const std::vector<std::string_view> strings {StringUtils::splitString(*value, "/") };
|
||||
if (strings.size() == 2)
|
||||
res->mediumCount = StringUtils::readAs<std::size_t>(strings[1]);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
std::optional<Medium> getMedium(const Av::IAudioFile::MetadataMap& metadataMap)
|
||||
{
|
||||
std::optional<Medium> res;
|
||||
res.emplace();
|
||||
|
||||
static
|
||||
std::optional<Medium>
|
||||
getMedium(const Av::IAudioFile::MetadataMap& metadataMap)
|
||||
{
|
||||
std::optional<Medium> res;
|
||||
res.emplace();
|
||||
res->type = findFirstValueOfAs<std::string>(metadataMap, { "TMED", "MEDIA", "WM/MEDIA" }).value_or("");
|
||||
res->name = findFirstValueOfAs<std::string>(metadataMap, { "TSST", "DISCSUBTITLE", "SETSUBTITLE" }).value_or("");
|
||||
res->trackCount = findFirstValueOfAs<std::size_t>(metadataMap, { "TOTALTRACKS", "TRACKTOTAL" });
|
||||
if (!res->trackCount)
|
||||
{
|
||||
// totalTracks may be encoded as "position/count"
|
||||
if (const auto value{ findFirstValueOfAs<std::string>(metadataMap, {"TRCK", "TRACK", "TRACKNUMBER", "TRKN", "WM/TRACKNUMBER"}) })
|
||||
{
|
||||
// Expecting 'Number/Total'
|
||||
const std::vector<std::string_view> strings{ StringUtils::splitString(*value, "/") };
|
||||
if (strings.size() == 2)
|
||||
res->trackCount = StringUtils::readAs<std::size_t>(strings[1]);
|
||||
}
|
||||
}
|
||||
|
||||
res->type = findFirstValueOfAs<std::string>(metadataMap, {"TMED", "MEDIA", "WM/MEDIA"}).value_or("");
|
||||
res->name = findFirstValueOfAs<std::string>(metadataMap, {"TSST", "DISCSUBTITLE", "SETSUBTITLE"}).value_or("");
|
||||
res->trackCount = findFirstValueOfAs<std::size_t>(metadataMap, {"TOTALTRACKS", "TRACKTOTAL"});
|
||||
if (!res->trackCount)
|
||||
{
|
||||
// totalTracks may be encoded as "position/count"
|
||||
if (const auto value {findFirstValueOfAs<std::string>(metadataMap, {"TRCK", "TRACK", "TRACKNUMBER", "TRKN", "WM/TRACKNUMBER"})})
|
||||
{
|
||||
// Expecting 'Number/Total'
|
||||
const std::vector<std::string_view> strings {StringUtils::splitString(*value, "/") };
|
||||
if (strings.size() == 2)
|
||||
res->trackCount = StringUtils::readAs<std::size_t>(strings[1]);
|
||||
}
|
||||
}
|
||||
// position may be encoded in TPOS/DISC/DISK as "position/count". Expecting 'Number[/Total]'
|
||||
res->position = findFirstValueOfAs<std::size_t>(metadataMap, { "TPOS", "DISC", "DISK", "DISCNUMBER", "WM/PARTOFSET" });
|
||||
res->release = getRelease(metadataMap);
|
||||
|
||||
// position may be encoded in TPOS/DISC/DISK as "position/count". Expecting 'Number[/Total]'
|
||||
res->position = findFirstValueOfAs<std::size_t>(metadataMap, {"TPOS", "DISC", "DISK", "DISCNUMBER", "WM/PARTOFSET"});
|
||||
res->release = getRelease(metadataMap);
|
||||
if (res->type.empty()
|
||||
&& res->name.empty()
|
||||
&& !res->trackCount
|
||||
&& !res->position
|
||||
&& !res->release
|
||||
&& !res->replayGain)
|
||||
{
|
||||
res.reset();
|
||||
}
|
||||
|
||||
if (res->type.empty()
|
||||
&& res->name.empty()
|
||||
&& !res->trackCount
|
||||
&& !res->position
|
||||
&& !res->release
|
||||
&& !res->replayGain)
|
||||
{
|
||||
res.reset();
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
std::optional<Track> AvFormatParser::parse(const std::filesystem::path& p, bool debug)
|
||||
{
|
||||
Track track;
|
||||
|
||||
std::optional<Track>
|
||||
AvFormatParser::parse(const std::filesystem::path& p, bool debug)
|
||||
{
|
||||
Track track;
|
||||
try
|
||||
{
|
||||
const auto mediaFile{ Av::parseAudioFile(p) };
|
||||
|
||||
try
|
||||
{
|
||||
const auto mediaFile {Av::parseAudioFile(p)};
|
||||
Av::ContainerInfo info{ mediaFile->getContainerInfo() };
|
||||
track.duration = info.duration;
|
||||
track.bitrate = info.bitrate;
|
||||
track.hasCover = mediaFile->hasAttachedPictures();
|
||||
|
||||
Av::ContainerInfo info{ mediaFile->getContainerInfo() };
|
||||
track.duration = info.duration;
|
||||
track.bitrate = info.bitrate;
|
||||
track.hasCover = mediaFile->hasAttachedPictures();
|
||||
MetaData::Tags tags;
|
||||
|
||||
MetaData::Tags tags;
|
||||
const Av::IAudioFile::MetadataMap metadataMap{ mediaFile->getMetaData() };
|
||||
|
||||
const Av::IAudioFile::MetadataMap metadataMap {mediaFile->getMetaData()};
|
||||
track.artists = getArtists(metadataMap);
|
||||
track.medium = getMedium(metadataMap);
|
||||
|
||||
track.artists = getArtists(metadataMap);
|
||||
track.medium = getMedium(metadataMap);
|
||||
for (const auto& [tag, value] : metadataMap)
|
||||
{
|
||||
if (debug)
|
||||
std::cout << "TAG = " << tag << ", VAL = " << value << std::endl;
|
||||
|
||||
for (const auto& [tag, value] : metadataMap)
|
||||
{
|
||||
if (debug)
|
||||
std::cout << "TAG = " << tag << ", VAL = " << value << std::endl;
|
||||
if (tag == "TITLE")
|
||||
track.title = value;
|
||||
else if (tag == "TRACK")
|
||||
{
|
||||
// Expecting 'Number/Total'
|
||||
track.position = StringUtils::readAs<std::size_t>(value);
|
||||
}
|
||||
else if (tag == "DATE"
|
||||
|| tag == "YEAR"
|
||||
|| tag == "WM/YEAR")
|
||||
{
|
||||
track.date = Utils::parseDate(value);
|
||||
}
|
||||
else if (tag == "TDOR" // Original release time (ID3v2 2.4)
|
||||
|| tag == "TORY") // Original release year
|
||||
{
|
||||
track.originalDate = Utils::parseDate(value);
|
||||
}
|
||||
else if (tag == "ACOUSTID ID")
|
||||
{
|
||||
track.acoustID = UUID::fromString(value);
|
||||
}
|
||||
else if (tag == "MUSICBRAINZ RELEASE TRACK ID"
|
||||
|| tag == "MUSICBRAINZ_RELEASETRACKID")
|
||||
{
|
||||
track.mbid = UUID::fromString(value);
|
||||
}
|
||||
else if (tag == "MUSICBRAINZ_TRACKID"
|
||||
|| tag == "MUSICBRAINZ/TRACK ID")
|
||||
{
|
||||
track.recordingMBID = UUID::fromString(value);
|
||||
}
|
||||
else if (std::find(std::cbegin(_extraTags), std::cend(_extraTags), tag) != std::cend(_extraTags))
|
||||
{
|
||||
const std::vector<std::string_view> tagValues{ StringUtils::splitString(value, "/,;") };
|
||||
|
||||
if (tag == "TITLE")
|
||||
track.title = value;
|
||||
else if (tag == "TRACK")
|
||||
{
|
||||
// Expecting 'Number/Total'
|
||||
track.position = StringUtils::readAs<std::size_t>(value);
|
||||
}
|
||||
else if (tag == "DATE"
|
||||
|| tag == "YEAR"
|
||||
|| tag == "WM/YEAR")
|
||||
{
|
||||
track.date = Utils::parseDate(value);
|
||||
}
|
||||
else if (tag == "TDOR" // Original release time (ID3v2 2.4)
|
||||
|| tag == "TORY") // Original release year
|
||||
{
|
||||
track.originalDate = Utils::parseDate(value);
|
||||
}
|
||||
else if (tag == "ACOUSTID ID")
|
||||
{
|
||||
track.acoustID = UUID::fromString(value);
|
||||
}
|
||||
else if (tag == "MUSICBRAINZ RELEASE TRACK ID"
|
||||
|| tag == "MUSICBRAINZ_RELEASETRACKID")
|
||||
{
|
||||
track.mbid = UUID::fromString(value);
|
||||
}
|
||||
else if (tag == "MUSICBRAINZ_TRACKID"
|
||||
|| tag == "MUSICBRAINZ/TRACK ID")
|
||||
{
|
||||
track.recordingMBID = UUID::fromString(value);
|
||||
}
|
||||
else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end())
|
||||
{
|
||||
const std::vector<std::string_view> clusterNames {StringUtils::splitString(value, "/,;")};
|
||||
if (!tagValues.empty())
|
||||
{
|
||||
std::set<std::string> values;
|
||||
std::transform(std::cbegin(tagValues), std::cend(tagValues), std::inserter(values, std::begin(values)), [](std::string_view v) { return std::string{ v }; });
|
||||
track.tags[tag] = std::move(values);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Av::Exception& e)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
if (!clusterNames.empty())
|
||||
{
|
||||
std::set<std::string> values;
|
||||
std::transform(std::cbegin(clusterNames), std::cend(clusterNames),
|
||||
std::inserter(values, std::begin(values)),
|
||||
[](std::string_view clusterName) { return std::string {clusterName}; });
|
||||
track.tags[tag] = std::move(values);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Av::Exception& e)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return track;
|
||||
}
|
||||
return track;
|
||||
}
|
||||
|
||||
} // namespace MetaData
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
#include "metadata/IParser.hpp"
|
||||
|
||||
#include "utils/Exception.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
|
||||
#include "AvFormatParser.hpp"
|
||||
#include "TagLibParser.hpp"
|
||||
@@ -34,10 +34,10 @@ namespace MetaData
|
||||
switch (parserType)
|
||||
{
|
||||
case ParserType::TagLib:
|
||||
LMS_LOG(METADATA, INFO) << "Creating TagLib parser with read style = " << Utils::readStyleToString(parserReadStyle);
|
||||
LMS_LOG(METADATA, INFO, "Creating TagLib parser with read style = " << Utils::readStyleToString(parserReadStyle));
|
||||
return std::make_unique<TagLibParser>(parserReadStyle);
|
||||
case ParserType::AvFormat:
|
||||
LMS_LOG(METADATA, INFO) << "Creating AvFormat parser";
|
||||
LMS_LOG(METADATA, INFO, "Creating AvFormat parser");
|
||||
return std::make_unique<AvFormatParser>();
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
|
||||
#include "utils/IConfig.hpp"
|
||||
#include "utils/Exception.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
#include "utils/Service.hpp"
|
||||
#include "utils/String.hpp"
|
||||
#include "Utils.hpp"
|
||||
@@ -368,18 +368,18 @@ namespace MetaData
|
||||
track.replayGain = StringUtils::readAs<float>(value);
|
||||
else if (tag == "ARTIST")
|
||||
track.artistDisplayName = value;
|
||||
else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end())
|
||||
else if (std::find(std::cbegin(_extraTags), std::cend(_extraTags), tag) != std::cend(_extraTags))
|
||||
{
|
||||
std::set<std::string> clusterNames;
|
||||
std::set<std::string> tagValues;
|
||||
for (std::string_view valueList : values)
|
||||
{
|
||||
const std::vector<std::string_view> splittedValues{ splitAndTrimString(valueList, "/,;") };
|
||||
const std::vector<std::string_view> splittedValues{ splitAndTrimString(valueList, "/,;") }; // handle possibily bad split tags
|
||||
for (std::string_view value : splittedValues)
|
||||
clusterNames.insert(std::string{ value });
|
||||
tagValues.insert(std::string{ value });
|
||||
}
|
||||
|
||||
if (!clusterNames.empty())
|
||||
track.tags[tag] = std::move(clusterNames);
|
||||
if (!tagValues.empty())
|
||||
track.tags[tag] = std::move(tagValues);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -391,7 +391,7 @@ namespace MetaData
|
||||
|
||||
if (f.isNull())
|
||||
{
|
||||
LMS_LOG(METADATA, ERROR) << "File '" << p.string() << "': parsing failed";
|
||||
LMS_LOG(METADATA, ERROR, "File '" << p.string() << "': parsing failed");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
@@ -404,7 +404,7 @@ namespace MetaData
|
||||
}
|
||||
else
|
||||
{
|
||||
LMS_LOG(METADATA, INFO) << "File '" << p.string() << "': no audio properties";
|
||||
LMS_LOG(METADATA, INFO, "File '" << p.string() << "': no audio properties");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
namespace MetaData
|
||||
{
|
||||
using Tags = std::map<std::string /* type */, std::set<std::string> /* names */>;
|
||||
using Tags = std::map<std::string /* type */, std::set<std::string> /* values */>;
|
||||
|
||||
// Very simplified version of https://musicbrainz.org/doc/MusicBrainz_Database/Schema
|
||||
|
||||
@@ -131,10 +131,10 @@ namespace MetaData
|
||||
|
||||
virtual std::optional<Track> parse(const std::filesystem::path& p, bool debug = false) = 0;
|
||||
|
||||
void setClusterTypeNames(const std::set<std::string>& clusterTypeNames) { _clusterTypeNames = clusterTypeNames; }
|
||||
void setExtraTags(const std::vector<std::string>& extraTags) { _extraTags = std::set(extraTags.cbegin(), extraTags.cend()); }
|
||||
|
||||
protected:
|
||||
std::set<std::string> _clusterTypeNames;
|
||||
std::set<std::string> _extraTags;
|
||||
};
|
||||
|
||||
enum class ParserType
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
#include "services/database/Db.hpp"
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/User.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
|
||||
namespace Auth
|
||||
{
|
||||
@@ -36,14 +36,14 @@ namespace Auth
|
||||
AuthServiceBase::getOrCreateUser(std::string_view loginName)
|
||||
{
|
||||
Session& session {getDbSession()};
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
auto transaction {session.createWriteTransaction()};
|
||||
|
||||
User::pointer user {User::find(session, loginName)};
|
||||
if (!user)
|
||||
{
|
||||
const UserType type {User::getCount(session) == 0 ? UserType::ADMIN : UserType::REGULAR};
|
||||
|
||||
LMS_LOG(AUTH, DEBUG) << "Creating user '" << loginName << "', admin = " << (type == UserType::ADMIN);
|
||||
LMS_LOG(AUTH, DEBUG, "Creating user '" << loginName << "', admin = " << (type == UserType::ADMIN));
|
||||
|
||||
user = session.create<User>(loginName);
|
||||
user.modify()->setType(type);
|
||||
@@ -56,7 +56,7 @@ namespace Auth
|
||||
AuthServiceBase::onUserAuthenticated(UserId userId)
|
||||
{
|
||||
Session& session {getDbSession()};
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
auto transaction {session.createWriteTransaction()};
|
||||
|
||||
User::pointer user {User::find(session, userId)};
|
||||
if (user)
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/User.hpp"
|
||||
#include "utils/Exception.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
|
||||
namespace Auth
|
||||
{
|
||||
@@ -54,7 +54,7 @@ namespace Auth
|
||||
|
||||
Database::Session& session {getDbSession()};
|
||||
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
auto transaction {session.createWriteTransaction()};
|
||||
|
||||
Database::User::pointer user {Database::User::find(session, userId)};
|
||||
if (!user)
|
||||
@@ -62,7 +62,7 @@ namespace Auth
|
||||
|
||||
Database::AuthToken::pointer authToken {session.create<Database::AuthToken>(secretHash, expiry, user)};
|
||||
|
||||
LMS_LOG(UI, DEBUG) << "Created auth token for user '" << user->getLoginName() << "', expiry = " << expiry.toString();
|
||||
LMS_LOG(UI, DEBUG, "Created auth token for user '" << user->getLoginName() << "', expiry = " << expiry.toString());
|
||||
|
||||
if (user->getAuthTokensCount() >= 50)
|
||||
Database::AuthToken::removeExpiredTokens(session, Wt::WDateTime::currentDateTime());
|
||||
@@ -76,7 +76,7 @@ namespace Auth
|
||||
const std::string secretHash {sha1Function.compute(std::string {secret}, {})};
|
||||
|
||||
Database::Session& session {getDbSession()};
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
auto transaction {session.createWriteTransaction()};
|
||||
|
||||
Database::AuthToken::pointer authToken {Database::AuthToken::find(session, secretHash)};
|
||||
if (!authToken)
|
||||
@@ -88,7 +88,7 @@ namespace Auth
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
LMS_LOG(UI, DEBUG) << "Found auth token for user '" << authToken->getUser()->getLoginName() << "'!";
|
||||
LMS_LOG(UI, DEBUG, "Found auth token for user '" << authToken->getUser()->getLoginName() << "'!");
|
||||
|
||||
AuthTokenService::AuthTokenProcessResult::AuthTokenInfo res {authToken->getUser()->getId(), authToken->getExpiry()};
|
||||
authToken.remove();
|
||||
@@ -131,7 +131,7 @@ namespace Auth
|
||||
{
|
||||
Database::Session& session {getDbSession()};
|
||||
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
auto transaction {session.createWriteTransaction()};
|
||||
|
||||
Database::User::pointer user {Database::User::find(session, userId)};
|
||||
if (!user)
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
#include "LoginThrottler.hpp"
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
#include "utils/Random.hpp"
|
||||
|
||||
namespace Auth {
|
||||
@@ -81,10 +81,10 @@ LoginThrottler::onBadClientAttempt(const boost::asio::ip::address& address)
|
||||
|
||||
attemptInfo.badConsecutiveAttemptCount += 1;
|
||||
|
||||
LMS_LOG(AUTH, DEBUG) << "Registering bad attempt for '" << clientAddress.to_string() << "', consecutive bad attempts count = " << attemptInfo.badConsecutiveAttemptCount;
|
||||
LMS_LOG(AUTH, DEBUG, "Registering bad attempt for '" << clientAddress.to_string() << "', consecutive bad attempts count = " << attemptInfo.badConsecutiveAttemptCount);
|
||||
if (attemptInfo.badConsecutiveAttemptCount >= _maxBadConsecutiveAttemptCount)
|
||||
{
|
||||
LMS_LOG(AUTH, DEBUG) << "Throttling '" << clientAddress.to_string() << "'";
|
||||
LMS_LOG(AUTH, DEBUG, "Throttling '" << clientAddress.to_string() << "'");
|
||||
attemptInfo.nextAttempt = now.addMSecs(std::chrono::duration_cast<std::chrono::milliseconds>(_throttlingDuration).count());
|
||||
}
|
||||
else
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/User.hpp"
|
||||
#include "utils/Exception.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
|
||||
namespace Auth
|
||||
{
|
||||
@@ -61,7 +61,7 @@ namespace Auth
|
||||
PasswordServiceBase::CheckResult
|
||||
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 << "'";
|
||||
LMS_LOG(AUTH, DEBUG, "Checking password for user '" << loginName << "'");
|
||||
|
||||
// Do not waste too much resource on brute force attacks (optim)
|
||||
{
|
||||
|
||||
@@ -22,46 +22,41 @@
|
||||
#include <Wt/WEnvironment.h>
|
||||
|
||||
#include "utils/IConfig.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
#include "utils/Service.hpp"
|
||||
|
||||
namespace Auth
|
||||
{
|
||||
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::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(const Wt::WEnvironment& env)
|
||||
{
|
||||
const std::string loginName{ env.headerValue(_fieldName) };
|
||||
if (loginName.empty())
|
||||
return { CheckResult::State::Denied };
|
||||
|
||||
HttpHeadersEnvService::CheckResult
|
||||
HttpHeadersEnvService::processEnv(const Wt::WEnvironment& env)
|
||||
{
|
||||
const std::string loginName {env.headerValue(_fieldName)};
|
||||
if (loginName.empty())
|
||||
return {CheckResult::State::Denied};
|
||||
LMS_LOG(AUTH, DEBUG, "Extracted login name = '" << loginName << "' from HTTP header");
|
||||
|
||||
LMS_LOG(AUTH, DEBUG) << "Extracted login name = '" << loginName << "' from HTTP header";
|
||||
const Database::UserId userId{ getOrCreateUser(loginName) };
|
||||
onUserAuthenticated(userId);
|
||||
return { CheckResult::State::Granted, userId };
|
||||
}
|
||||
|
||||
const Database::UserId userId {getOrCreateUser(loginName)};
|
||||
onUserAuthenticated(userId);
|
||||
return {CheckResult::State::Granted, userId};
|
||||
}
|
||||
HttpHeadersEnvService::CheckResult HttpHeadersEnvService::processRequest(const Wt::Http::Request& request)
|
||||
{
|
||||
const std::string loginName{ request.headerValue(_fieldName) };
|
||||
if (loginName.empty())
|
||||
return { CheckResult::State::Denied };
|
||||
|
||||
HttpHeadersEnvService::CheckResult
|
||||
HttpHeadersEnvService::processRequest(const Wt::Http::Request& request)
|
||||
{
|
||||
const std::string loginName {request.headerValue(_fieldName)};
|
||||
if (loginName.empty())
|
||||
return {CheckResult::State::Denied};
|
||||
|
||||
LMS_LOG(AUTH, DEBUG) << "Extracted login name = '" << loginName << "' from HTTP header";
|
||||
|
||||
const Database::UserId userId {getOrCreateUser(loginName)};
|
||||
onUserAuthenticated(userId);
|
||||
return {CheckResult::State::Granted, userId};
|
||||
}
|
||||
LMS_LOG(AUTH, DEBUG, "Extracted login name = '" << loginName << "' from HTTP header");
|
||||
|
||||
const Database::UserId userId{ getOrCreateUser(loginName) };
|
||||
onUserAuthenticated(userId);
|
||||
return { CheckResult::State::Granted, userId };
|
||||
}
|
||||
} // namespace Auth
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/User.hpp"
|
||||
#include "utils/Exception.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
|
||||
namespace Auth
|
||||
{
|
||||
@@ -44,17 +44,17 @@ namespace Auth
|
||||
bool
|
||||
InternalPasswordService::checkUserPassword(std::string_view loginName, std::string_view password)
|
||||
{
|
||||
LMS_LOG(AUTH, DEBUG) << "Checking internal password for user '" << loginName << "'";
|
||||
LMS_LOG(AUTH, DEBUG, "Checking internal password for user '" << loginName << "'");
|
||||
|
||||
Database::User::PasswordHash passwordHash;
|
||||
{
|
||||
Database::Session& session {getDbSession()};
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
auto transaction {session.createReadTransaction()};
|
||||
|
||||
const Database::User::pointer user {Database::User::find(session, loginName)};
|
||||
if (!user)
|
||||
{
|
||||
LMS_LOG(AUTH, DEBUG) << "hashing random stuff";
|
||||
LMS_LOG(AUTH, DEBUG, "hashing random stuff");
|
||||
// hash random stuff here to waste some time
|
||||
hashRandomPassword();
|
||||
return false;
|
||||
@@ -100,7 +100,7 @@ namespace Auth
|
||||
const Database::User::PasswordHash passwordHash {hashPassword(newPassword)};
|
||||
|
||||
Database::Session& session {getDbSession()};
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
auto transaction {session.createWriteTransaction()};
|
||||
|
||||
Database::User::pointer user {Database::User::find(session, userId)};
|
||||
if (!user)
|
||||
|
||||
@@ -28,175 +28,173 @@
|
||||
|
||||
#include "services/auth/Types.hpp"
|
||||
#include "services/database/Session.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
|
||||
namespace Auth
|
||||
{
|
||||
class PAMError
|
||||
{
|
||||
public:
|
||||
PAMError(std::string_view msg, pam_handle_t *pamh, int err)
|
||||
{
|
||||
_errorMsg = std::string {msg} + ": " + pam_strerror(pamh, err);
|
||||
}
|
||||
namespace
|
||||
{
|
||||
class PAMError
|
||||
{
|
||||
public:
|
||||
PAMError(std::string_view msg, pam_handle_t* pamh, int err)
|
||||
{
|
||||
_errorMsg = std::string{ msg } + ": " + pam_strerror(pamh, err);
|
||||
}
|
||||
|
||||
std::string_view message() const { return _errorMsg; }
|
||||
std::string_view message() const { return _errorMsg; }
|
||||
|
||||
private:
|
||||
std::string _errorMsg;
|
||||
};
|
||||
private:
|
||||
std::string _errorMsg;
|
||||
};
|
||||
|
||||
class PAMContext
|
||||
{
|
||||
public:
|
||||
PAMContext(std::string_view loginName)
|
||||
{
|
||||
int err {pam_start("lms", std::string {loginName}.c_str(), &_conv, &_pamh)};
|
||||
if (err != PAM_SUCCESS)
|
||||
throw PAMError {"start failed", _pamh, err};
|
||||
}
|
||||
class PAMContext
|
||||
{
|
||||
public:
|
||||
PAMContext(std::string_view loginName)
|
||||
{
|
||||
int err{ pam_start("lms", std::string {loginName}.c_str(), &_conv, &_pamh) };
|
||||
if (err != PAM_SUCCESS)
|
||||
throw PAMError{ "start failed", _pamh, err };
|
||||
}
|
||||
|
||||
~PAMContext()
|
||||
{
|
||||
int err {pam_end(_pamh, 0)};
|
||||
if (err != PAM_SUCCESS)
|
||||
LMS_LOG(AUTH, ERROR) << "end failed: " << pam_strerror(_pamh, err);
|
||||
}
|
||||
~PAMContext()
|
||||
{
|
||||
int err{ pam_end(_pamh, 0) };
|
||||
if (err != PAM_SUCCESS)
|
||||
LMS_LOG(AUTH, ERROR, "end failed: " << pam_strerror(_pamh, err));
|
||||
}
|
||||
|
||||
void authenticate(std::string_view password)
|
||||
{
|
||||
AuthenticateConvContext authContext {password};
|
||||
ScopedConvContextSetter scopedContext {*this, authContext};
|
||||
void authenticate(std::string_view password)
|
||||
{
|
||||
AuthenticateConvContext authContext{ password };
|
||||
ScopedConvContextSetter scopedContext{ *this, authContext };
|
||||
|
||||
int err {pam_authenticate(_pamh, 0)};
|
||||
if (err != PAM_SUCCESS)
|
||||
throw PAMError {"authenticate failed", _pamh, err};
|
||||
}
|
||||
int err{ pam_authenticate(_pamh, 0) };
|
||||
if (err != PAM_SUCCESS)
|
||||
throw PAMError{ "authenticate failed", _pamh, err };
|
||||
}
|
||||
|
||||
void validateAccount()
|
||||
{
|
||||
int err {pam_acct_mgmt(_pamh, PAM_SILENT)};
|
||||
if (err != PAM_SUCCESS)
|
||||
throw PAMError {"acct_mgmt failed", _pamh, err};
|
||||
}
|
||||
void validateAccount()
|
||||
{
|
||||
int err{ pam_acct_mgmt(_pamh, PAM_SILENT) };
|
||||
if (err != PAM_SUCCESS)
|
||||
throw PAMError{ "acct_mgmt failed", _pamh, err };
|
||||
}
|
||||
|
||||
private:
|
||||
class ConvContext
|
||||
{
|
||||
public:
|
||||
virtual ~ConvContext() = default;
|
||||
};
|
||||
private:
|
||||
class ConvContext
|
||||
{
|
||||
public:
|
||||
virtual ~ConvContext() = default;
|
||||
};
|
||||
|
||||
class AuthenticateConvContext final : public ConvContext
|
||||
{
|
||||
public:
|
||||
AuthenticateConvContext(std::string_view password) : _password {password} {}
|
||||
class AuthenticateConvContext final : public ConvContext
|
||||
{
|
||||
public:
|
||||
AuthenticateConvContext(std::string_view password) : _password{ password } {}
|
||||
|
||||
std::string_view getPassword() const { return _password; }
|
||||
std::string_view getPassword() const { return _password; }
|
||||
|
||||
private:
|
||||
std::string_view _password;
|
||||
};
|
||||
private:
|
||||
std::string_view _password;
|
||||
};
|
||||
|
||||
class ScopedConvContextSetter
|
||||
{
|
||||
public:
|
||||
ScopedConvContextSetter(PAMContext& pamContext, ConvContext& convContext)
|
||||
: _pamContext {pamContext}
|
||||
{
|
||||
_pamContext._convContext = &convContext;
|
||||
}
|
||||
class ScopedConvContextSetter
|
||||
{
|
||||
public:
|
||||
ScopedConvContextSetter(PAMContext& pamContext, ConvContext& convContext)
|
||||
: _pamContext{ pamContext }
|
||||
{
|
||||
_pamContext._convContext = &convContext;
|
||||
}
|
||||
|
||||
~ScopedConvContextSetter()
|
||||
{
|
||||
_pamContext._convContext = nullptr;
|
||||
}
|
||||
~ScopedConvContextSetter()
|
||||
{
|
||||
_pamContext._convContext = nullptr;
|
||||
}
|
||||
|
||||
ScopedConvContextSetter(const ScopedConvContextSetter&) = delete;
|
||||
ScopedConvContextSetter(ScopedConvContextSetter&&) = delete;
|
||||
ScopedConvContextSetter& operator=(const ScopedConvContextSetter&) = delete;
|
||||
ScopedConvContextSetter& operator=(ScopedConvContextSetter&&) = delete;
|
||||
ScopedConvContextSetter(const ScopedConvContextSetter&) = delete;
|
||||
ScopedConvContextSetter(ScopedConvContextSetter&&) = delete;
|
||||
ScopedConvContextSetter& operator=(const ScopedConvContextSetter&) = delete;
|
||||
ScopedConvContextSetter& operator=(ScopedConvContextSetter&&) = delete;
|
||||
|
||||
private:
|
||||
PAMContext& _pamContext;
|
||||
};
|
||||
private:
|
||||
PAMContext& _pamContext;
|
||||
};
|
||||
|
||||
static int conv(int msgCount, const pam_message** msgs, pam_response** resps, void* userData)
|
||||
{
|
||||
if (msgCount < 1)
|
||||
return PAM_CONV_ERR;
|
||||
if (!resps || !msgs || !userData)
|
||||
return PAM_CONV_ERR;
|
||||
|
||||
static int conv(int msgCount, const pam_message** msgs, pam_response** resps, void* userData)
|
||||
{
|
||||
if (msgCount < 1)
|
||||
return PAM_CONV_ERR;
|
||||
if (!resps || !msgs || !userData)
|
||||
return PAM_CONV_ERR;
|
||||
PAMContext& context{ *static_cast<PAMContext*>(userData) };
|
||||
|
||||
PAMContext& context {*static_cast<PAMContext*>(userData)};
|
||||
AuthenticateConvContext* authenticateContext = dynamic_cast<AuthenticateConvContext*>(context._convContext);
|
||||
if (!authenticateContext)
|
||||
{
|
||||
LMS_LOG(AUTH, ERROR, "Unexpected conv!");
|
||||
return PAM_CONV_ERR;
|
||||
}
|
||||
|
||||
AuthenticateConvContext* authenticateContext = dynamic_cast<AuthenticateConvContext*>(context._convContext);
|
||||
if (!authenticateContext)
|
||||
{
|
||||
LMS_LOG(AUTH, ERROR) << "Unexpected conv!";
|
||||
return PAM_CONV_ERR;
|
||||
}
|
||||
// Only expect a PAM_PROMPT_ECHO_OFF msg
|
||||
if (msgCount != 1 || msgs[0]->msg_style != PAM_PROMPT_ECHO_OFF)
|
||||
{
|
||||
LMS_LOG(AUTH, ERROR, "Unexpected conv message. Count = " << msgCount);
|
||||
return PAM_CONV_ERR;
|
||||
}
|
||||
|
||||
// Only expect a PAM_PROMPT_ECHO_OFF msg
|
||||
if (msgCount != 1 || msgs[0]->msg_style != PAM_PROMPT_ECHO_OFF)
|
||||
{
|
||||
LMS_LOG(AUTH, ERROR) << "Unexpected conv message. Count = " << msgCount;
|
||||
return PAM_CONV_ERR;
|
||||
}
|
||||
pam_response* response{ static_cast<pam_response*>(malloc(sizeof(pam_response))) };
|
||||
if (!response)
|
||||
return PAM_CONV_ERR;
|
||||
|
||||
pam_response* response {static_cast<pam_response*>(malloc(sizeof(pam_response)))};
|
||||
if (!response)
|
||||
return PAM_CONV_ERR;
|
||||
response->resp = strdup(std::string{ authenticateContext->getPassword() }.c_str());
|
||||
|
||||
response->resp = strdup(std::string {authenticateContext->getPassword()}.c_str());
|
||||
*resps = response;
|
||||
return PAM_SUCCESS;
|
||||
}
|
||||
|
||||
*resps = response;
|
||||
return PAM_SUCCESS;
|
||||
}
|
||||
ConvContext* _convContext{};
|
||||
pam_conv _conv{ &PAMContext::conv, this };
|
||||
pam_handle_t* _pamh{};
|
||||
};
|
||||
}
|
||||
|
||||
ConvContext* _convContext {};
|
||||
pam_conv _conv {&PAMContext::conv, this};
|
||||
pam_handle_t *_pamh {};
|
||||
};
|
||||
bool PAMPasswordService::checkUserPassword(std::string_view loginName, std::string_view password)
|
||||
{
|
||||
try
|
||||
{
|
||||
LMS_LOG(AUTH, DEBUG, "Checking PAM password for user '" << loginName << "'");
|
||||
PAMContext pamContext{ loginName };
|
||||
|
||||
bool
|
||||
PAMPasswordService::checkUserPassword(std::string_view loginName, std::string_view password)
|
||||
{
|
||||
try
|
||||
{
|
||||
LMS_LOG(AUTH, DEBUG) << "Checking PAM password for user '" << loginName << "'";
|
||||
PAMContext pamContext {loginName};
|
||||
pamContext.authenticate(password);
|
||||
pamContext.validateAccount();
|
||||
|
||||
pamContext.authenticate(password);
|
||||
pamContext.validateAccount();
|
||||
return true;
|
||||
}
|
||||
catch (const PAMError& error)
|
||||
{
|
||||
LMS_LOG(AUTH, ERROR, "PAM error: " << error.message());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (const PAMError& error)
|
||||
{
|
||||
LMS_LOG(AUTH, ERROR) << "PAM error: " << error.message();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
bool PAMPasswordService::canSetPasswords() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool
|
||||
PAMPasswordService::canSetPasswords() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
IPasswordService::PasswordAcceptabilityResult PAMPasswordService::checkPasswordAcceptability(std::string_view, const PasswordValidationContext&) const
|
||||
{
|
||||
throw NotImplementedException{};
|
||||
}
|
||||
|
||||
IPasswordService::PasswordAcceptabilityResult
|
||||
PAMPasswordService::checkPasswordAcceptability(std::string_view, const PasswordValidationContext&) const
|
||||
{
|
||||
throw NotImplementedException {};
|
||||
}
|
||||
|
||||
void
|
||||
PAMPasswordService::setPassword(Database::UserId, std::string_view)
|
||||
{
|
||||
throw NotImplementedException {};
|
||||
}
|
||||
void PAMPasswordService::setPassword(Database::UserId, std::string_view)
|
||||
{
|
||||
throw NotImplementedException{};
|
||||
}
|
||||
|
||||
} // namespace Auth
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
#include "image/Exception.hpp"
|
||||
#include "image/IRawImage.hpp"
|
||||
#include "utils/IConfig.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
#include "utils/Random.hpp"
|
||||
#include "utils/String.hpp"
|
||||
#include "utils/Utils.hpp"
|
||||
@@ -51,7 +51,7 @@ namespace Cover
|
||||
{
|
||||
std::optional<TrackInfo> res;
|
||||
|
||||
auto transaction{ dbSession.createSharedTransaction() };
|
||||
auto transaction{ dbSession.createReadTransaction() };
|
||||
|
||||
const Database::Track::pointer track{ Database::Track::find(dbSession, trackId) };
|
||||
if (!track)
|
||||
@@ -110,10 +110,10 @@ namespace Cover
|
||||
{
|
||||
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) << "Preferred file names: " << StringUtils::joinStrings(_preferredFileNames, ",");
|
||||
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, "Preferred file names: " << StringUtils::joinStrings(_preferredFileNames, ","));
|
||||
|
||||
#if LMS_SUPPORT_IMAGE_GM
|
||||
GraphicsMagick::init(execPath);
|
||||
@@ -148,7 +148,7 @@ namespace Cover
|
||||
}
|
||||
catch (const Image::ImageException& e)
|
||||
{
|
||||
LMS_LOG(COVER, ERROR) << "Cannot read embedded cover: " << e.what();
|
||||
LMS_LOG(COVER, ERROR, "Cannot read embedded cover: " << e.what());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -167,7 +167,7 @@ namespace Cover
|
||||
}
|
||||
catch (const ImageException& e)
|
||||
{
|
||||
LMS_LOG(COVER, ERROR) << "Cannot read cover in file '" << p.string() << "': " << e.what();
|
||||
LMS_LOG(COVER, ERROR, "Cannot read cover in file '" << p.string() << "': " << e.what());
|
||||
}
|
||||
|
||||
return image;
|
||||
@@ -190,7 +190,7 @@ namespace Cover
|
||||
|
||||
std::shared_ptr<IEncodedImage> image{ getFromCoverFile(_defaultCoverPath, width) };
|
||||
_defaultCoverCache[width] = image;
|
||||
LMS_LOG(COVER, DEBUG) << "Default cache entries = " << _defaultCoverCache.size();
|
||||
LMS_LOG(COVER, DEBUG, "Default cache entries = " << _defaultCoverCache.size());
|
||||
|
||||
return image;
|
||||
}
|
||||
@@ -269,7 +269,7 @@ namespace Cover
|
||||
|
||||
if (std::filesystem::file_size(filePath, ec) > _maxFileSize && !ec)
|
||||
{
|
||||
LMS_LOG(COVER, INFO) << "Cover file '" << filePath.string() << " is too big (" << std::filesystem::file_size(filePath, ec) << "), limit is " << _maxFileSize;
|
||||
LMS_LOG(COVER, INFO, "Cover file '" << filePath.string() << " is too big (" << std::filesystem::file_size(filePath, ec) << "), limit is " << _maxFileSize);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -306,7 +306,7 @@ namespace Cover
|
||||
}
|
||||
catch (Av::Exception& e)
|
||||
{
|
||||
LMS_LOG(COVER, ERROR) << "Cannot get covers from track " << p.string() << ": " << e.what();
|
||||
LMS_LOG(COVER, ERROR, "Cannot get covers from track " << p.string() << ": " << e.what());
|
||||
}
|
||||
|
||||
return image;
|
||||
@@ -372,7 +372,7 @@ namespace Cover
|
||||
{
|
||||
std::optional<ReleaseInfo> res;
|
||||
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
// get a track in this release, consider the release is in a single directory
|
||||
const auto tracks{ Track::find(session, Track::FindParameters {}.setRelease(releaseId).setRange(Range{ 0, 1 }).setSortMethod(TrackSortMethod::Release)) };
|
||||
@@ -404,7 +404,7 @@ namespace Cover
|
||||
{
|
||||
std::unique_lock lock{ _cacheMutex };
|
||||
|
||||
LMS_LOG(COVER, DEBUG) << "Cache stats: hits = " << _cacheHits << ", misses = " << _cacheMisses << ", nb entries = " << _cache.size() << ", size = " << _cacheSize;
|
||||
LMS_LOG(COVER, DEBUG, "Cache stats: hits = " << _cacheHits << ", misses = " << _cacheMisses << ", nb entries = " << _cache.size() << ", size = " << _cacheSize);
|
||||
_cacheHits = 0;
|
||||
_cacheMisses = 0;
|
||||
_cacheSize = 0;
|
||||
@@ -415,7 +415,7 @@ namespace Cover
|
||||
{
|
||||
_jpegQuality = Utils::clamp<unsigned>(quality, 1, 100);
|
||||
|
||||
LMS_LOG(COVER, INFO) << "JPEG export quality = " << _jpegQuality;
|
||||
LMS_LOG(COVER, INFO, "JPEG export quality = " << _jpegQuality);
|
||||
}
|
||||
|
||||
void CoverService::saveToCache(const CacheEntryDesc& entryDesc, std::shared_ptr<IEncodedImage> image)
|
||||
|
||||
@@ -17,6 +17,7 @@ add_library(lmsdatabase SHARED
|
||||
impl/SqlQuery.cpp
|
||||
impl/Track.cpp
|
||||
impl/TrackBookmark.cpp
|
||||
impl/TransactionChecker.cpp
|
||||
impl/Types.cpp
|
||||
impl/User.cpp
|
||||
impl/Utils.cpp
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/Track.hpp"
|
||||
#include "services/database/User.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
#include "SqlQuery.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "EnumSetTraits.hpp"
|
||||
@@ -38,7 +38,7 @@ namespace Database
|
||||
template <typename ResultType>
|
||||
Wt::Dbo::Query<ResultType> createQuery(Session& session, std::string_view itemToSelect, const Artist::FindParameters& params)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession().query<ResultType>("SELECT DISTINCT " + std::string{ itemToSelect } + " FROM artist a") };
|
||||
if (params.sortMethod == ArtistSortMethod::LastWritten
|
||||
@@ -175,14 +175,14 @@ namespace Database
|
||||
|
||||
std::size_t Artist::getCount(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM artist");
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer> Artist::find(Session& session, const std::string& name)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<Artist>> res = session.getDboSession().find<Artist>()
|
||||
.where("name = ?").bind(std::string{ name, 0, _maxNameLength })
|
||||
@@ -193,33 +193,33 @@ namespace Database
|
||||
|
||||
Artist::pointer Artist::find(Session& session, const UUID& mbid)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
return session.getDboSession().find<Artist>().where("mbid = ?").bind(std::string{ mbid.getAsString() }).resultValue();
|
||||
}
|
||||
|
||||
Artist::pointer Artist::find(Session& session, ArtistId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
return session.getDboSession().find<Artist>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
|
||||
bool Artist::exists(Session& session, ArtistId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
return session.getDboSession().query<int>("SELECT 1 FROM artist").where("id = ?").bind(id).resultValue() == 1;
|
||||
}
|
||||
|
||||
|
||||
RangeResults<ArtistId> Artist::findOrphanIds(Session& session, std::optional<Range> range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
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)") };
|
||||
return Utils::execQuery<ArtistId>(query, range);
|
||||
}
|
||||
|
||||
RangeResults<ArtistId> Artist::findIds(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ createQuery<ArtistId>(session, params) };
|
||||
return Utils::execQuery<ArtistId>(query, params.range);
|
||||
@@ -227,7 +227,7 @@ namespace Database
|
||||
|
||||
RangeResults<Artist::pointer> Artist::find(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ createQuery<Wt::Dbo::ptr<Artist>>(session, params) };
|
||||
return Utils::execQuery<Artist::pointer>(query, params.range);
|
||||
@@ -235,7 +235,7 @@ namespace Database
|
||||
|
||||
void Artist::find(Session& session, const FindParameters& params, std::function<void(const pointer&)> func)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ createQuery<Wt::Dbo::ptr<Artist>>(session, params) };
|
||||
Utils::execQuery(query, params.range, func);
|
||||
@@ -288,7 +288,7 @@ namespace Database
|
||||
return Utils::execQuery<ArtistId>(query, range);
|
||||
}
|
||||
|
||||
std::vector<std::vector<Cluster::pointer>> Artist::getClusterGroups(std::vector<ClusterType::pointer> clusterTypes, std::size_t size) const
|
||||
std::vector<std::vector<Cluster::pointer>> Artist::getClusterGroups(std::vector<ClusterTypeId> clusterTypeIds, std::size_t size) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
@@ -300,8 +300,8 @@ namespace Database
|
||||
where.And(WhereClause("a.id = ?")).bind(getId().toString());
|
||||
{
|
||||
WhereClause clusterClause;
|
||||
for (auto clusterType : clusterTypes)
|
||||
clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterType->getId().toString());
|
||||
for (ClusterTypeId clusterTypeId : clusterTypeIds)
|
||||
clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterTypeId.toString());
|
||||
|
||||
where.And(clusterClause);
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ namespace Database
|
||||
void
|
||||
AuthToken::removeExpiredTokens(Session& session, const Wt::WDateTime& now)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
session.checkWriteTransaction();
|
||||
|
||||
session.getDboSession().execute("DELETE FROM auth_token WHERE expiry < ?").bind(now);
|
||||
}
|
||||
@@ -52,7 +52,7 @@ namespace Database
|
||||
AuthToken::pointer
|
||||
AuthToken::find(Session& session, std::string_view value)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().find<AuthToken>()
|
||||
.where("value = ?").bind(value)
|
||||
|
||||
@@ -35,7 +35,7 @@ namespace Database
|
||||
template <typename ResultType>
|
||||
Wt::Dbo::Query<ResultType> createQuery(Session& session, std::string_view itemToSelect, const Cluster::FindParameters& params)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession().query<ResultType>("SELECT DISTINCT " + std::string{ itemToSelect } + " FROM cluster c") };
|
||||
|
||||
@@ -44,14 +44,19 @@ namespace Database
|
||||
query.join("track_cluster t_c ON t_c.cluster_id = c.id");
|
||||
query.join("track t ON t.id = t_c.track_id");
|
||||
}
|
||||
if (!params.clusterTypeName.empty())
|
||||
query.join("cluster_type c_t ON c_t.id = c.cluster_type_id");
|
||||
|
||||
if (params.track.isValid())
|
||||
query.where("t.id = ?").bind(params.track);
|
||||
if (params.release.isValid())
|
||||
query.where("t.release_id = ?").bind(params.release);
|
||||
|
||||
assert(!params.clusterType.isValid() || params.clusterTypeName.empty());
|
||||
if (params.clusterType.isValid())
|
||||
query.where("c.cluster_type_id = ?").bind(params.clusterType);
|
||||
else if (!params.clusterTypeName.empty())
|
||||
query.where("c_t.name = ?").bind(params.clusterTypeName);
|
||||
|
||||
return query;
|
||||
}
|
||||
@@ -85,14 +90,14 @@ namespace Database
|
||||
|
||||
std::size_t Cluster::getCount(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM cluster");
|
||||
}
|
||||
|
||||
RangeResults<ClusterId> Cluster::findIds(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
auto query{ createQuery<ClusterId>(session, params) };
|
||||
|
||||
return Utils::execQuery<ClusterId>(query, params.range);
|
||||
@@ -100,15 +105,15 @@ namespace Database
|
||||
|
||||
RangeResults<Cluster::pointer> Cluster::find(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
auto query{ createQuery<Wt::Dbo::ptr<Cluster>>(session, params) };
|
||||
|
||||
return Utils::execQuery<Cluster::pointer>(query, params.range);
|
||||
}
|
||||
|
||||
RangeResults<ClusterId> Cluster::findOrphans(Session& session, std::optional<Range> range)
|
||||
RangeResults<ClusterId> Cluster::findOrphanIds(Session& session, std::optional<Range> range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
auto query{ session.getDboSession().query<ClusterId>("SELECT DISTINCT c.id FROM cluster c WHERE NOT EXISTS(SELECT 1 FROM track_cluster t_c WHERE t_c.cluster_id = c.id)") };
|
||||
|
||||
return Utils::execQuery<ClusterId>(query, range);
|
||||
@@ -116,14 +121,14 @@ namespace Database
|
||||
|
||||
Cluster::pointer Cluster::find(Session& session, ClusterId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().find<Cluster>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
|
||||
std::size_t Cluster::computeTrackCount(Session& session, ClusterId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().query<int>("SELECT COUNT(t.id) FROM track t INNER JOIN track_cluster t_c ON t_c.track_id = t.id")
|
||||
.where("t_c.cluster_id = ?").bind(id).resultValue();
|
||||
@@ -131,7 +136,7 @@ namespace Database
|
||||
|
||||
std::size_t Cluster::computeReleaseCount(Session& session, ClusterId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().query<int>("SELECT COUNT(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")
|
||||
.where("t_c.cluster_id = ?").bind(id).resultValue();
|
||||
@@ -157,22 +162,22 @@ namespace Database
|
||||
{
|
||||
}
|
||||
|
||||
ClusterType::pointer ClusterType::create(Session& session, const std::string& name)
|
||||
ClusterType::pointer ClusterType::create(Session& session, std::string_view name)
|
||||
{
|
||||
return session.getDboSession().add(std::unique_ptr<ClusterType> {new ClusterType{ name }});
|
||||
}
|
||||
|
||||
std::size_t ClusterType::getCount(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM cluster_type");
|
||||
}
|
||||
|
||||
|
||||
RangeResults<ClusterTypeId> ClusterType::findOrphans(Session& session, std::optional<Range> range)
|
||||
RangeResults<ClusterTypeId> ClusterType::findOrphanIds(Session& session, std::optional<Range> range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession().query<ClusterTypeId>(
|
||||
"SELECT c_t.id from cluster_type c_t"
|
||||
@@ -184,7 +189,7 @@ namespace Database
|
||||
|
||||
RangeResults<ClusterTypeId> ClusterType::findUsed(Session& session, std::optional<Range> range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession().query<ClusterTypeId>(
|
||||
"SELECT DISTINCT c_t.id from cluster_type c_t")
|
||||
@@ -195,21 +200,21 @@ namespace Database
|
||||
|
||||
ClusterType::pointer ClusterType::find(Session& session, std::string_view name)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().find<ClusterType>().where("name = ?").bind(std::string{ name }).resultValue();
|
||||
}
|
||||
|
||||
ClusterType::pointer ClusterType::find(Session& session, ClusterTypeId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().find<ClusterType>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
|
||||
RangeResults<ClusterTypeId> ClusterType::find(Session& session, std::optional<Range> range)
|
||||
RangeResults<ClusterTypeId> ClusterType::findIds(Session& session, std::optional<Range> range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession().query<ClusterTypeId>("SELECT id from cluster_type") };
|
||||
|
||||
|
||||
@@ -24,7 +24,9 @@
|
||||
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/User.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/IConfig.hpp"
|
||||
#include "utils/Service.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
@@ -63,18 +65,18 @@ namespace Database
|
||||
|
||||
void prepare()
|
||||
{
|
||||
LMS_LOG(DB, DEBUG) << "Setting per-connection settings...";
|
||||
LMS_LOG(DB, DEBUG, "Setting per-connection settings...");
|
||||
executeSql("pragma journal_mode=WAL");
|
||||
executeSql("pragma synchronous=normal");
|
||||
executeSql("pragma analysis_limit=2000"); // to help make analyze command faster, 1000 does not seem to be enough to speed up all queries
|
||||
LMS_LOG(DB, DEBUG) << "Setting per-connection settings done!";
|
||||
LMS_LOG(DB, DEBUG, "Setting per-connection settings done!");
|
||||
}
|
||||
|
||||
void optimize()
|
||||
{
|
||||
LMS_LOG(DB, DEBUG) << "connection close: Running pragma optimize...";
|
||||
LMS_LOG(DB, DEBUG, "connection close: Running pragma optimize...");
|
||||
executeSql("pragma optimize");
|
||||
LMS_LOG(DB, DEBUG) << "connection close: pragma optimize complete";
|
||||
LMS_LOG(DB, DEBUG, "connection close: pragma optimize complete");
|
||||
}
|
||||
|
||||
std::filesystem::path _dbPath;
|
||||
@@ -84,10 +86,11 @@ namespace Database
|
||||
// Session living class handling the database and the login
|
||||
Db::Db(const std::filesystem::path& dbPath, std::size_t connectionCount)
|
||||
{
|
||||
LMS_LOG(DB, INFO) << "Creating connection pool on file " << dbPath.string();
|
||||
LMS_LOG(DB, INFO, "Creating connection pool on file " << dbPath.string());
|
||||
|
||||
auto connection{ std::make_unique<Connection>(dbPath.string()) };
|
||||
// connection->setProperty("show-queries", "true");
|
||||
if (IConfig * config{ Service<IConfig>::get() })// may not be here on testU
|
||||
connection->setProperty("show-queries", config->getBool("db-show-queries", false) ? "true" : "false");
|
||||
|
||||
auto connectionPool{ std::make_unique<Wt::Dbo::FixedSqlConnectionPool>(std::move(connection), connectionCount) };
|
||||
connectionPool->setTimeout(std::chrono::seconds{ 10 });
|
||||
|
||||
@@ -140,25 +140,25 @@ namespace Database
|
||||
|
||||
Listen::pointer Listen::create(Session& session, ObjectPtr<User> user, ObjectPtr<Track> track, ScrobblingBackend backend, const Wt::WDateTime& dateTime)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
session.checkWriteTransaction();
|
||||
return session.getDboSession().add(std::unique_ptr<Listen> {new Listen{ user, track, backend, dateTime }});
|
||||
}
|
||||
|
||||
std::size_t Listen::getCount(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM listen");
|
||||
}
|
||||
|
||||
Listen::pointer Listen::find(Session& session, ListenId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
return session.getDboSession().find<Listen>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
|
||||
RangeResults<ListenId> Listen::find(Session& session, const FindParameters& parameters)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession().query<ListenId>("SELECT id FROM listen")
|
||||
.orderBy("date_time") };
|
||||
@@ -177,7 +177,7 @@ namespace Database
|
||||
|
||||
Listen::pointer Listen::find(Session& session, UserId userId, TrackId trackId, ScrobblingBackend backend, const Wt::WDateTime& dateTime)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().find<Listen>()
|
||||
.where("user_id = ?").bind(userId)
|
||||
@@ -189,7 +189,7 @@ namespace Database
|
||||
|
||||
RangeResults<ArtistId> Listen::getTopArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
auto query{ createArtistsQuery(session.getDboSession(), userId, backend, clusterIds, linkType) };
|
||||
|
||||
auto collection{ query
|
||||
@@ -201,7 +201,7 @@ namespace Database
|
||||
|
||||
RangeResults<ReleaseId> Listen::getTopReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<Range> range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
auto query{ createReleasesQuery(session.getDboSession(), userId, backend, clusterIds)
|
||||
.orderBy("COUNT(r.id) DESC")
|
||||
.groupBy("r.id") };
|
||||
@@ -211,7 +211,7 @@ namespace Database
|
||||
|
||||
RangeResults<TrackId> Listen::getTopTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<Range> range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
auto query{ createTracksQuery(session.getDboSession(), userId, backend, clusterIds)
|
||||
.orderBy("COUNT(t.id) DESC")
|
||||
.groupBy("t.id") };
|
||||
@@ -221,7 +221,7 @@ namespace Database
|
||||
|
||||
RangeResults<ArtistId> Listen::getRecentArtists(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<TrackArtistLinkType> linkType, std::optional<Range> range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
auto query{ createArtistsQuery(session.getDboSession(), userId, backend, clusterIds, linkType)
|
||||
.groupBy("a.id").having("l.date_time = MAX(l.date_time)")
|
||||
.orderBy("l.date_time DESC") };
|
||||
@@ -231,7 +231,7 @@ namespace Database
|
||||
|
||||
RangeResults<ReleaseId> Listen::getRecentReleases(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<Range> range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
auto query{ createReleasesQuery(session.getDboSession(), userId, backend, clusterIds)
|
||||
.groupBy("r.id").having("l.date_time = MAX(l.date_time)")
|
||||
.orderBy("l.date_time DESC") };
|
||||
@@ -241,7 +241,7 @@ namespace Database
|
||||
|
||||
RangeResults<TrackId> Listen::getRecentTracks(Session& session, UserId userId, ScrobblingBackend backend, const std::vector<ClusterId>& clusterIds, std::optional<Range> range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
auto query{ createTracksQuery(session.getDboSession(), userId, backend, clusterIds)
|
||||
.groupBy("t.id").having("l.date_time = MAX(l.date_time)")
|
||||
.orderBy("l.date_time DESC") };
|
||||
@@ -251,7 +251,7 @@ namespace Database
|
||||
|
||||
std::size_t Listen::getCount(Session& session, UserId userId, ScrobblingBackend backend, TrackId trackId)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) from listen l")
|
||||
.where("l.track_id = ?").bind(trackId)
|
||||
@@ -262,7 +262,7 @@ namespace Database
|
||||
|
||||
std::size_t Listen::getCount(Session& session, UserId userId, ScrobblingBackend backend, ReleaseId releaseId)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().query<int>("SELECT IFNULL(MIN(count_result), 0)"
|
||||
" FROM ("
|
||||
@@ -279,7 +279,7 @@ namespace Database
|
||||
|
||||
Listen::pointer Listen::getMostRecentListen(Session& session, UserId userId, ScrobblingBackend backend, ReleaseId releaseId)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
// TODO not pending remove?
|
||||
return session.getDboSession().query<Wt::Dbo::ptr<Listen>>("SELECT l from listen l")
|
||||
@@ -294,7 +294,7 @@ namespace Database
|
||||
|
||||
Listen::pointer Listen::getMostRecentListen(Session& session, UserId userId, ScrobblingBackend backend, TrackId trackId)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
// TODO not pending remove?
|
||||
return session.getDboSession().query<Wt::Dbo::ptr<Listen>>("SELECT l from listen l")
|
||||
.where("l.track_id = ?").bind(trackId)
|
||||
|
||||
@@ -26,13 +26,13 @@
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/User.hpp"
|
||||
#include "utils/Exception.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
VersionInfo::pointer VersionInfo::getOrCreate(Session& session)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
session.checkWriteTransaction();
|
||||
|
||||
pointer versionInfo{ session.getDboSession().find<VersionInfo>() };
|
||||
if (!versionInfo)
|
||||
@@ -43,7 +43,7 @@ namespace Database
|
||||
|
||||
VersionInfo::pointer VersionInfo::get(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().find<VersionInfo>();
|
||||
}
|
||||
@@ -249,6 +249,24 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
|
||||
session.getDboSession().execute("ALTER TABLE user ADD subsonic_enable_transcoding_by_default INTEGER NOT NULL DEFAULT(" + std::to_string(static_cast<int>(/*User::defaultSubsonicEnableTranscodingByDefault*/0)) + ")");
|
||||
}
|
||||
|
||||
void migrateFromV46(Session& session)
|
||||
{
|
||||
// add extra tags to parse
|
||||
session.getDboSession().execute(R"(CREATE TABLE IF NOT EXISTS "cluster_type_backup" (
|
||||
"id" integer primary key autoincrement,
|
||||
"version" integer not null,
|
||||
"name" text not null
|
||||
);)");
|
||||
session.getDboSession().execute("INSERT INTO cluster_type_backup SELECT id, version, name FROM cluster_type");
|
||||
session.getDboSession().execute("DROP TABLE cluster_type");
|
||||
session.getDboSession().execute("ALTER TABLE cluster_type_backup RENAME TO cluster_type");
|
||||
|
||||
session.getDboSession().execute("ALTER TABLE scan_settings ADD COLUMN extra_tags_to_scan TEXT");
|
||||
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(session).modify()->incScanVersion();
|
||||
}
|
||||
|
||||
void doDbMigration(Session& session)
|
||||
{
|
||||
static const std::string outdatedMsg{ "Outdated database, please rebuild it (delete the .db file and restart)" };
|
||||
@@ -273,20 +291,21 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
|
||||
{43, migrateFromV43},
|
||||
{44, migrateFromV44},
|
||||
{45, migrateFromV45},
|
||||
{46, migrateFromV46},
|
||||
};
|
||||
|
||||
{
|
||||
auto uniqueTransaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
Version version;
|
||||
try
|
||||
{
|
||||
version = VersionInfo::getOrCreate(session)->getVersion();
|
||||
LMS_LOG(DB, INFO) << "Database version = " << version << ", LMS binary version = " << LMS_DATABASE_VERSION;
|
||||
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();
|
||||
LMS_LOG(DB, ERROR, "Cannot get database version info: " << e.what());
|
||||
throw LmsException{ outdatedMsg };
|
||||
}
|
||||
|
||||
@@ -298,7 +317,7 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
|
||||
|
||||
while (version < LMS_DATABASE_VERSION)
|
||||
{
|
||||
LMS_LOG(DB, INFO) << "Migrating database from version " << version << " to " << version + 1 << "...";
|
||||
LMS_LOG(DB, INFO, "Migrating database from version " << version << " to " << version + 1 << "...");
|
||||
|
||||
auto itMigrationFunc{ migrationFunctions.find(version) };
|
||||
assert(itMigrationFunc != std::cend(migrationFunctions));
|
||||
@@ -306,7 +325,7 @@ CREATE TABLE IF NOT EXISTS "track_backup" (
|
||||
|
||||
VersionInfo::get(session).modify()->setVersion(++version);
|
||||
|
||||
LMS_LOG(DB, INFO) << "Migration complete to version " << version;
|
||||
LMS_LOG(DB, INFO, "Migration complete to version " << version);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace Database
|
||||
class Session;
|
||||
|
||||
using Version = std::size_t;
|
||||
static constexpr Version LMS_DATABASE_VERSION{ 46 };
|
||||
static constexpr Version LMS_DATABASE_VERSION{ 47 };
|
||||
class VersionInfo
|
||||
{
|
||||
public:
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/Track.hpp"
|
||||
#include "services/database/User.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
#include "SqlQuery.hpp"
|
||||
#include "EnumSetTraits.hpp"
|
||||
#include "IdTypeTraits.hpp"
|
||||
@@ -39,7 +39,7 @@ namespace Database
|
||||
template <typename ResultType>
|
||||
Wt::Dbo::Query<ResultType> createQuery(Session& session, std::string_view itemToSelect, const Release::FindParameters& params)
|
||||
{
|
||||
auto query{ session.getDboSession().query<ResultType>("SELECT DISTINCT " + std::string{ itemToSelect } + " from release r") };
|
||||
auto query{ session.getDboSession().query<ResultType>("SELECT " + std::string{ itemToSelect } + " from release r") };
|
||||
|
||||
if (params.sortMethod == ReleaseSortMethod::LastWritten
|
||||
|| params.sortMethod == ReleaseSortMethod::Date
|
||||
@@ -183,21 +183,6 @@ namespace Database
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
template <typename ResultType>
|
||||
Wt::Dbo::Query<ResultType> createQuery(Session& session, const Release::FindParameters& params)
|
||||
{
|
||||
std::string_view itemToSelect;
|
||||
|
||||
if constexpr (std::is_same_v<ResultType, ReleaseId>)
|
||||
itemToSelect = "r.id";
|
||||
else if constexpr (std::is_same_v<ResultType, Wt::Dbo::ptr<Release>>)
|
||||
itemToSelect = "r";
|
||||
else
|
||||
static_assert("Unhandled type");
|
||||
|
||||
return createQuery<ResultType>(session, itemToSelect, params);
|
||||
}
|
||||
}
|
||||
|
||||
Release::Release(const std::string& name, const std::optional<UUID>& MBID)
|
||||
@@ -211,13 +196,15 @@ namespace Database
|
||||
return session.getDboSession().add(std::unique_ptr<Release> {new Release{ name, MBID }});
|
||||
}
|
||||
|
||||
std::vector<Release::pointer> Release::find(Session& session, const std::string& name)
|
||||
std::vector<Release::pointer> Release::find(Session& session, const std::string& name, const std::filesystem::path& releaseDirectory)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto res{ session.getDboSession()
|
||||
.find<Release>()
|
||||
.where("name = ?").bind(std::string(name, 0, _maxNameLength))
|
||||
.query<Wt::Dbo::ptr<Release>>("SELECT DISTINCT r from release r")
|
||||
.join("track t ON t.release_id = r.id")
|
||||
.where("r.name = ?").bind(std::string(name, 0, _maxNameLength))
|
||||
.where("t.file_path LIKE ? ESCAPE '" ESCAPE_CHAR_STR "'").bind(Utils::escapeLikeKeyword(releaseDirectory.string()) + "%")
|
||||
.resultList() };
|
||||
|
||||
return std::vector<Release::pointer>(res.begin(), res.end());
|
||||
@@ -225,7 +212,7 @@ namespace Database
|
||||
|
||||
Release::pointer Release::find(Session& session, const UUID& mbid)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession()
|
||||
.find<Release>()
|
||||
@@ -235,7 +222,7 @@ namespace Database
|
||||
|
||||
Release::pointer Release::find(Session& session, ReleaseId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession()
|
||||
.find<Release>()
|
||||
@@ -245,20 +232,20 @@ namespace Database
|
||||
|
||||
bool Release::exists(Session& session, ReleaseId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
return session.getDboSession().query<int>("SELECT 1 FROM release").where("id = ?").bind(id).resultValue() == 1;
|
||||
}
|
||||
|
||||
std::size_t Release::getCount(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM release");
|
||||
}
|
||||
|
||||
RangeResults<ReleaseId> Release::findIdsOrderedByArtist(Session& session, std::optional<Range> range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
// TODO merge with find
|
||||
auto query{ session.getDboSession().query<ReleaseId>(
|
||||
@@ -273,7 +260,7 @@ namespace Database
|
||||
|
||||
RangeResults<ReleaseId> Release::findOrphanIds(Session& session, std::optional<Range> range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession().query<ReleaseId>("select r.id from release r LEFT OUTER JOIN Track t ON r.id = t.release_id WHERE t.id IS NULL") };
|
||||
return Utils::execQuery<ReleaseId>(query, range);
|
||||
@@ -281,28 +268,35 @@ namespace Database
|
||||
|
||||
RangeResults<Release::pointer> Release::find(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ createQuery<Wt::Dbo::ptr<Release>>(session, params) };
|
||||
auto query{ createQuery<Wt::Dbo::ptr<Release>>(session, "DISTINCT r", params) };
|
||||
return Utils::execQuery<pointer>(query, params.range);
|
||||
}
|
||||
|
||||
void Release::find(Session& session, const FindParameters& params, std::function<void(const pointer&)> func)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ createQuery<Wt::Dbo::ptr<Release>>(session, params) };
|
||||
auto query{ createQuery<Wt::Dbo::ptr<Release>>(session, "DISTINCT r", params) };
|
||||
Utils::execQuery<pointer>(query, params.range, func);
|
||||
}
|
||||
|
||||
RangeResults<ReleaseId> Release::findIds(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ createQuery<ReleaseId>(session, params) };
|
||||
auto query{ createQuery<ReleaseId>(session, "DISTINCT r.id", params) };
|
||||
return Utils::execQuery<ReleaseId>(query, params.range);
|
||||
}
|
||||
|
||||
std::size_t Release::getCount(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
return createQuery<int>(session, "COUNT(DISTINCT r.id)", params).resultValue();
|
||||
}
|
||||
|
||||
std::size_t Release::getDiscCount() const
|
||||
{
|
||||
assert(session());
|
||||
@@ -487,7 +481,7 @@ namespace Database
|
||||
return query.resultValue();
|
||||
}
|
||||
|
||||
std::vector<std::vector<Cluster::pointer>> Release::getClusterGroups(const std::vector<ClusterType::pointer>& clusterTypes, std::size_t size) const
|
||||
std::vector<std::vector<Cluster::pointer>> Release::getClusterGroups(const std::vector<ClusterTypeId>& clusterTypeIds, std::size_t size) const
|
||||
{
|
||||
assert(session());
|
||||
|
||||
@@ -500,8 +494,8 @@ namespace Database
|
||||
where.And(WhereClause("r.id = ?")).bind(getId().toString());
|
||||
{
|
||||
WhereClause clusterClause;
|
||||
for (auto clusterType : clusterTypes)
|
||||
clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterType->getId().toString());
|
||||
for (const ClusterTypeId clusterTypeId : clusterTypeIds)
|
||||
clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterTypeId.toString());
|
||||
where.And(clusterClause);
|
||||
}
|
||||
oss << " " << where.get();
|
||||
|
||||
@@ -22,131 +22,58 @@
|
||||
#include <Wt/Dbo/WtSqlTraits.h>
|
||||
|
||||
#include "utils/Path.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
#include "utils/String.hpp"
|
||||
|
||||
#include "services/database/Cluster.hpp"
|
||||
#include "services/database/Session.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
const std::set<std::string> defaultClusterTypeNames =
|
||||
namespace Database
|
||||
{
|
||||
"GENRE",
|
||||
"ALBUMGROUPING",
|
||||
"MOOD",
|
||||
"ALBUMMOOD",
|
||||
};
|
||||
ScanSettings::pointer ScanSettings::get(Session& session)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
|
||||
}
|
||||
return session.getDboSession().find<ScanSettings>().resultValue();
|
||||
}
|
||||
|
||||
namespace Database {
|
||||
std::vector<std::filesystem::path> ScanSettings::getAudioFileExtensions() const
|
||||
{
|
||||
const auto extensions{ StringUtils::splitString(_audioFileExtensions, " ") };
|
||||
|
||||
void
|
||||
ScanSettings::init(Session& session)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
std::vector<std::filesystem::path> res(std::cbegin(extensions), std::cend(extensions));
|
||||
std::sort(std::begin(res), std::end(res));
|
||||
res.erase(std::unique(std::begin(res), std::end(res)), std::end(res));
|
||||
|
||||
pointer settings {get(session)};
|
||||
if (settings)
|
||||
return;
|
||||
return res;
|
||||
}
|
||||
|
||||
settings = session.getDboSession().add(std::make_unique<ScanSettings>());
|
||||
settings.modify()->setClusterTypes(session, defaultClusterTypeNames );
|
||||
}
|
||||
void ScanSettings::addAudioFileExtension(const std::filesystem::path& ext)
|
||||
{
|
||||
_audioFileExtensions += " " + ext.string();
|
||||
}
|
||||
|
||||
ScanSettings::pointer
|
||||
ScanSettings::get(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
std::vector<std::string_view> ScanSettings::getExtraTagsToScan() const
|
||||
{
|
||||
return StringUtils::splitString(_extraTagsToScan, ";");
|
||||
}
|
||||
|
||||
return session.getDboSession().find<ScanSettings>().resultValue();
|
||||
}
|
||||
void ScanSettings::setMediaDirectory(const std::filesystem::path& p)
|
||||
{
|
||||
_mediaDirectory = StringUtils::stringTrimEnd(p.string(), "/\\");
|
||||
}
|
||||
|
||||
std::vector<std::filesystem::path>
|
||||
ScanSettings::getAudioFileExtensions() const
|
||||
{
|
||||
const auto extensions {StringUtils::splitString(_audioFileExtensions, " ")};
|
||||
void ScanSettings::setExtraTagsToScan(const std::vector<std::string_view>& extraTags)
|
||||
{
|
||||
std::string newTagsToScan{ StringUtils::joinStrings(extraTags, ";") };
|
||||
if (newTagsToScan != _extraTagsToScan)
|
||||
incScanVersion();
|
||||
|
||||
std::vector<std::filesystem::path> res (std::cbegin(extensions), std::cend(extensions));
|
||||
std::sort(std::begin(res), std::end(res));
|
||||
res.erase(std::unique( std::begin(res), std::end(res)), std::end(res));
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
void
|
||||
ScanSettings::addAudioFileExtension(const std::filesystem::path& ext)
|
||||
{
|
||||
_audioFileExtensions += " " + ext.string();
|
||||
}
|
||||
|
||||
std::vector<ClusterType::pointer>
|
||||
ScanSettings::getClusterTypes() const
|
||||
{
|
||||
return std::vector<ClusterType::pointer>(std::cbegin(_clusterTypes), std::cend(_clusterTypes));
|
||||
}
|
||||
|
||||
void
|
||||
ScanSettings::setMediaDirectory(const std::filesystem::path& p)
|
||||
{
|
||||
_mediaDirectory = StringUtils::stringTrimEnd(p.string(), "/\\");
|
||||
}
|
||||
|
||||
template <typename It>
|
||||
std::set<std::string> getNames(It begin, It end)
|
||||
{
|
||||
std::set<std::string> names;
|
||||
std::transform(begin, end, std::inserter(names, std::cbegin(names)),
|
||||
[](const ClusterType::pointer& clusterType)
|
||||
{
|
||||
return clusterType->getName();
|
||||
});
|
||||
|
||||
return names;
|
||||
}
|
||||
|
||||
void
|
||||
ScanSettings::setClusterTypes(Session& session, const std::set<std::string>& clusterTypeNames)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
|
||||
bool needRescan {};
|
||||
|
||||
// Create any missing cluster type
|
||||
for (const std::string& clusterTypeName : clusterTypeNames)
|
||||
{
|
||||
ClusterType::pointer clusterType {ClusterType::find(session, clusterTypeName)};
|
||||
if (!clusterType)
|
||||
{
|
||||
LMS_LOG(DB, INFO) << "Creating cluster type " << clusterTypeName;
|
||||
clusterType = session.create<ClusterType>(clusterTypeName);
|
||||
_clusterTypes.insert(getDboPtr(clusterType));
|
||||
|
||||
needRescan = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete no longer existing cluster types
|
||||
for (Wt::Dbo::ptr<ClusterType> clusterType : _clusterTypes)
|
||||
{
|
||||
if (std::none_of(clusterTypeNames.begin(), clusterTypeNames.end(),
|
||||
[clusterType](const std::string& name) { return name == clusterType->getName(); }))
|
||||
{
|
||||
LMS_LOG(DB, INFO) << "Deleting cluster type " << clusterType->getName();
|
||||
clusterType.remove();
|
||||
}
|
||||
}
|
||||
|
||||
if (needRescan)
|
||||
_scanVersion += 1;
|
||||
}
|
||||
|
||||
void
|
||||
ScanSettings::incScanVersion()
|
||||
{
|
||||
_scanVersion += 1;
|
||||
}
|
||||
_extraTagsToScan = std::move(newTagsToScan);
|
||||
}
|
||||
|
||||
void ScanSettings::incScanVersion()
|
||||
{
|
||||
_scanVersion += 1;
|
||||
}
|
||||
} // namespace Database
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
#include <cassert>
|
||||
|
||||
#include "utils/Exception.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
|
||||
#include "services/database/Artist.hpp"
|
||||
#include "services/database/AuthToken.hpp"
|
||||
@@ -39,6 +39,7 @@
|
||||
#include "services/database/TrackArtistLink.hpp"
|
||||
#include "services/database/TrackList.hpp"
|
||||
#include "services/database/TrackFeatures.hpp"
|
||||
#include "services/database/TransactionChecker.hpp"
|
||||
#include "services/database/User.hpp"
|
||||
#include "EnumSetTraits.hpp"
|
||||
#include "Migration.hpp"
|
||||
@@ -46,6 +47,29 @@
|
||||
namespace Database
|
||||
{
|
||||
|
||||
WriteTransaction::WriteTransaction(RecursiveSharedMutex& mutex, Wt::Dbo::Session& session)
|
||||
: _lock{ mutex },
|
||||
_transaction{ session }
|
||||
{
|
||||
TransactionChecker::pushWriteTransaction(_transaction.session());
|
||||
}
|
||||
|
||||
WriteTransaction::~WriteTransaction()
|
||||
{
|
||||
TransactionChecker::popWriteTransaction(_transaction.session());
|
||||
}
|
||||
|
||||
ReadTransaction::ReadTransaction(Wt::Dbo::Session& session)
|
||||
: _transaction{ session }
|
||||
{
|
||||
TransactionChecker::pushReadTransaction(_transaction.session());
|
||||
}
|
||||
|
||||
ReadTransaction::~ReadTransaction()
|
||||
{
|
||||
TransactionChecker::popReadTransaction(_transaction.session());
|
||||
}
|
||||
|
||||
Session::Session(Db& db)
|
||||
: _db{ db }
|
||||
{
|
||||
@@ -71,54 +95,32 @@ namespace Database
|
||||
_session.mapClass<User>("user");
|
||||
}
|
||||
|
||||
UniqueTransaction::UniqueTransaction(RecursiveSharedMutex& mutex, Wt::Dbo::Session& session)
|
||||
: _lock{ mutex },
|
||||
_transaction{ session }
|
||||
WriteTransaction Session::createWriteTransaction()
|
||||
{
|
||||
return WriteTransaction{ _db.getMutex(), _session };
|
||||
}
|
||||
|
||||
SharedTransaction::SharedTransaction(RecursiveSharedMutex& mutex, Wt::Dbo::Session& session)
|
||||
: _lock{ mutex },
|
||||
_transaction{ session }
|
||||
ReadTransaction Session::createReadTransaction()
|
||||
{
|
||||
}
|
||||
|
||||
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 };
|
||||
return ReadTransaction{ _session };
|
||||
}
|
||||
|
||||
void Session::prepareTables()
|
||||
{
|
||||
LMS_LOG(DB, INFO) << "Preparing tables...";
|
||||
LMS_LOG(DB, INFO, "Preparing tables...");
|
||||
|
||||
// Initial creation case
|
||||
try
|
||||
{
|
||||
_session.createTables();
|
||||
LMS_LOG(DB, INFO) << "Tables created";
|
||||
LMS_LOG(DB, INFO, "Tables created");
|
||||
}
|
||||
catch (Wt::Dbo::Exception& e)
|
||||
{
|
||||
LMS_LOG(DB, DEBUG) << "Cannot create tables: " << e.what();
|
||||
LMS_LOG(DB, DEBUG, "Cannot create tables: " << e.what());
|
||||
if (std::string_view{ e.what() }.find("already exists") == std::string_view::npos)
|
||||
{
|
||||
LMS_LOG(DB, ERROR) << "Cannot create tables: " << e.what();
|
||||
LMS_LOG(DB, ERROR, "Cannot create tables: " << e.what());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@@ -127,7 +129,7 @@ namespace Database
|
||||
|
||||
// Indexes
|
||||
{
|
||||
auto uniqueTransaction{ createUniqueTransaction() };
|
||||
auto transaction{ createWriteTransaction() };
|
||||
_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)");
|
||||
@@ -169,33 +171,26 @@ namespace Database
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS starred_track_user_backend_idx ON starred_track(user_id,backend)");
|
||||
_session.execute("CREATE INDEX IF NOT EXISTS starred_track_track_user_backend_idx ON starred_track(track_id,user_id,backend)");
|
||||
}
|
||||
|
||||
// Initial settings tables
|
||||
{
|
||||
auto uniqueTransaction{ createUniqueTransaction() };
|
||||
|
||||
ScanSettings::init(*this);
|
||||
}
|
||||
}
|
||||
|
||||
void Session::analyze()
|
||||
{
|
||||
LMS_LOG(DB, INFO) << "Analyzing database...";
|
||||
LMS_LOG(DB, INFO, "Analyzing database...");
|
||||
{
|
||||
auto uniqueTransaction{ createUniqueTransaction() };
|
||||
auto transaction{ createWriteTransaction() };
|
||||
_session.execute("ANALYZE");
|
||||
}
|
||||
LMS_LOG(DB, INFO) << "Database Analyze complete";
|
||||
LMS_LOG(DB, INFO, "Database Analyze complete");
|
||||
}
|
||||
|
||||
void Session::optimize()
|
||||
{
|
||||
LMS_LOG(DB, INFO) << "Optimizing database...";
|
||||
LMS_LOG(DB, INFO, "Optimizing database...");
|
||||
{
|
||||
auto uniqueTransaction{ createUniqueTransaction() };
|
||||
auto transaction{ createWriteTransaction() };
|
||||
_session.execute("PRAGMA optimize");
|
||||
}
|
||||
LMS_LOG(DB, INFO) << "Database optimizing complete";
|
||||
LMS_LOG(DB, INFO, "Database optimizing complete");
|
||||
}
|
||||
|
||||
} // namespace Database
|
||||
|
||||
@@ -67,11 +67,11 @@ WhereClause::get() const
|
||||
}
|
||||
|
||||
WhereClause&
|
||||
WhereClause::bind(const std::string& bindArg)
|
||||
WhereClause::bind(std::string_view bindArg)
|
||||
{
|
||||
assert(_bindArgs.size() < static_cast<std::size_t>(std::count(_clause.begin(), _clause.end(), '?')));
|
||||
|
||||
_bindArgs.push_back(bindArg);
|
||||
_bindArgs.push_back(std::string{ bindArg });
|
||||
|
||||
return *this;
|
||||
}
|
||||
@@ -102,8 +102,8 @@ SelectStatement::And(const std::string& statement)
|
||||
{
|
||||
_statement.push_back(statement);
|
||||
|
||||
_statement.sort();
|
||||
_statement.unique();
|
||||
std::sort(_statement.begin(), _statement.end());
|
||||
_statement.erase(std::unique(_statement.begin(), _statement.end()), _statement.end());
|
||||
|
||||
return *this;
|
||||
}
|
||||
@@ -113,7 +113,7 @@ SelectStatement::get() const
|
||||
{
|
||||
std::string res = "SELECT ";
|
||||
|
||||
for (std::list<std::string>::const_iterator it = _statement.begin(); it != _statement.end(); ++it)
|
||||
for (auto it = _statement.begin(); it != _statement.end(); ++it)
|
||||
{
|
||||
if (it != _statement.begin())
|
||||
res += ",";
|
||||
@@ -150,8 +150,8 @@ FromClause::And(const FromClause& clause)
|
||||
_clause.push_back(fromClause);
|
||||
}
|
||||
|
||||
_clause.sort();
|
||||
_clause.unique();
|
||||
std::sort(_clause.begin(), _clause.end());
|
||||
_clause.erase(std::unique(_clause.begin(), _clause.end()), _clause.end());
|
||||
|
||||
return *this;
|
||||
}
|
||||
@@ -164,7 +164,7 @@ FromClause::get() const
|
||||
if (!_clause.empty())
|
||||
{
|
||||
oss << "FROM ";
|
||||
for (std::list<std::string>::const_iterator it = _clause.begin(); it != _clause.end(); ++it) {
|
||||
for (auto it = _clause.begin(); it != _clause.end(); ++it) {
|
||||
if (it != _clause.begin())
|
||||
oss << ",";
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <list>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
|
||||
@@ -33,14 +33,14 @@ class WhereClause
|
||||
WhereClause& Or(const WhereClause& clause);
|
||||
|
||||
// Arguments binding (for each '?' in where clause)
|
||||
WhereClause& bind(const std::string& arg);
|
||||
WhereClause& bind(std::string_view arg);
|
||||
|
||||
std::string get() const;
|
||||
const std::list<std::string>& getBindArgs() const {return _bindArgs;}
|
||||
const std::vector<std::string>& getBindArgs() const {return _bindArgs;}
|
||||
|
||||
private:
|
||||
std::string _clause; // WHERE clause
|
||||
std::list<std::string> _bindArgs;
|
||||
std::vector<std::string> _bindArgs;
|
||||
};
|
||||
|
||||
class InnerJoinClause
|
||||
@@ -81,7 +81,7 @@ class SelectStatement
|
||||
std::string get() const;
|
||||
|
||||
private:
|
||||
std::list<std::string> _statement;
|
||||
std::vector<std::string> _statement;
|
||||
};
|
||||
|
||||
class FromClause
|
||||
@@ -95,7 +95,7 @@ class FromClause
|
||||
std::string get() const;
|
||||
|
||||
private:
|
||||
std::list<std::string> _clause;
|
||||
std::vector<std::string> _clause;
|
||||
};
|
||||
|
||||
class SqlQuery
|
||||
|
||||
@@ -43,19 +43,30 @@ namespace Database
|
||||
|
||||
std::size_t StarredArtist::getCount(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM starred_artist");
|
||||
}
|
||||
|
||||
StarredArtist::pointer StarredArtist::find(Session& session, StarredArtistId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
return session.getDboSession().find<StarredArtist>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
|
||||
StarredArtist::pointer StarredArtist::find(Session& session, ArtistId artistId, UserId userId)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return session.getDboSession().query<Wt::Dbo::ptr<StarredArtist>>("SELECT s_a from starred_artist s_a")
|
||||
.join("user u ON u.id = s_a.user_id")
|
||||
.where("s_a.artist_id = ?").bind(artistId)
|
||||
.where("s_a.user_id = ?").bind(userId)
|
||||
.where("s_a.backend = u.feedback_backend")
|
||||
.resultValue();
|
||||
}
|
||||
|
||||
StarredArtist::pointer StarredArtist::find(Session& session, ArtistId artistId, UserId userId, FeedbackBackend backend)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
return session.getDboSession().find<StarredArtist>()
|
||||
.where("artist_id = ?").bind(artistId)
|
||||
.where("user_id = ?").bind(userId)
|
||||
|
||||
@@ -43,19 +43,30 @@ namespace Database
|
||||
|
||||
std::size_t StarredRelease::getCount(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM starred_release");
|
||||
}
|
||||
|
||||
StarredRelease::pointer StarredRelease::find(Session& session, StarredReleaseId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
return session.getDboSession().find<StarredRelease>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
|
||||
StarredRelease::pointer StarredRelease::find(Session& session, ReleaseId releaseId, UserId userId)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return session.getDboSession().query<Wt::Dbo::ptr<StarredRelease>>("SELECT s_r from starred_release s_r")
|
||||
.join("user u ON u.id = s_r.user_id")
|
||||
.where("s_r.release_id = ?").bind(releaseId)
|
||||
.where("s_r.user_id = ?").bind(userId)
|
||||
.where("s_r.backend = u.feedback_backend")
|
||||
.resultValue();
|
||||
}
|
||||
|
||||
StarredRelease::pointer StarredRelease::find(Session& session, ReleaseId releaseId, UserId userId, FeedbackBackend backend)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
return session.getDboSession().find<StarredRelease>()
|
||||
.where("release_id = ?").bind(releaseId)
|
||||
.where("user_id = ?").bind(userId)
|
||||
|
||||
@@ -43,19 +43,30 @@ namespace Database
|
||||
|
||||
std::size_t StarredTrack::getCount(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM starred_track");
|
||||
}
|
||||
|
||||
StarredTrack::pointer StarredTrack::find(Session& session, StarredTrackId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
return session.getDboSession().find<StarredTrack>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
|
||||
StarredTrack::pointer StarredTrack::find(Session& session, TrackId trackId, UserId userId)
|
||||
{
|
||||
session.checkReadTransaction();
|
||||
return session.getDboSession().query<Wt::Dbo::ptr<StarredTrack>>("SELECT s_t from starred_track s_t")
|
||||
.join("user u ON u.id = s_t.user_id")
|
||||
.where("s_t.track_id = ?").bind(trackId)
|
||||
.where("s_t.user_id = ?").bind(userId)
|
||||
.where("s_t.backend = u.feedback_backend")
|
||||
.resultValue();
|
||||
}
|
||||
|
||||
StarredTrack::pointer StarredTrack::find(Session& session, TrackId trackId, UserId userId, FeedbackBackend backend)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
return session.getDboSession().find<StarredTrack>()
|
||||
.where("track_id = ?").bind(trackId)
|
||||
.where("user_id = ?").bind(userId)
|
||||
@@ -63,9 +74,18 @@ namespace Database
|
||||
.resultValue();
|
||||
}
|
||||
|
||||
bool StarredTrack::exists(Session& session, TrackId trackId, UserId userId, FeedbackBackend backend)
|
||||
{
|
||||
return session.getDboSession().query<int>("SELECT 1 from starred_track")
|
||||
.where("track_id = ?").bind(trackId)
|
||||
.where("user_id = ?").bind(userId)
|
||||
.where("backend = ?").bind(backend)
|
||||
.resultValue() == 1;
|
||||
}
|
||||
|
||||
RangeResults<StarredTrackId> StarredTrack::find(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession().query<StarredTrackId>("SELECT DISTINCT s_t.id FROM starred_track s_t") };
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
#include "services/database/TrackFeatures.hpp"
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/User.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
|
||||
#include "IdTypeTraits.hpp"
|
||||
#include "SqlQuery.hpp"
|
||||
@@ -42,7 +42,7 @@ namespace Database
|
||||
template <typename ResultType>
|
||||
Wt::Dbo::Query<ResultType> createQuery(Session& session, std::string_view itemToSelect, const Track::FindParameters& params)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
std::string selectStatement{ params.distinct ? "SELECT DISTINCT" : "SELECT" };
|
||||
auto query{ session.getDboSession().query<ResultType>(selectStatement + " " + std::string{ itemToSelect } + " FROM track t") };
|
||||
@@ -200,21 +200,21 @@ namespace Database
|
||||
std::size_t
|
||||
Track::getCount(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM track");
|
||||
}
|
||||
|
||||
Track::pointer Track::findByPath(Session& session, const std::filesystem::path& p)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().find<Track>().where("file_path = ?").bind(p.string()).resultValue();
|
||||
}
|
||||
|
||||
Track::pointer Track::find(Session& session, TrackId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().find<Track>()
|
||||
.where("id = ?").bind(id)
|
||||
@@ -223,14 +223,14 @@ namespace Database
|
||||
|
||||
bool Track::exists(Session& session, TrackId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().query<int>("SELECT 1 from track").where("id = ?").bind(id).resultValue() == 1;
|
||||
}
|
||||
|
||||
std::vector<Track::pointer> Track::findByMBID(Session& session, const UUID& mbid)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto res{ session.getDboSession().find<Track>()
|
||||
.where("mbid = ?").bind(std::string {mbid.getAsString()})
|
||||
@@ -241,7 +241,7 @@ namespace Database
|
||||
|
||||
std::vector<Track::pointer> Track::findByRecordingMBID(Session& session, const UUID& mbid)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto res{ session.getDboSession().find<Track>()
|
||||
.where("recording_mbid = ?").bind(std::string {mbid.getAsString()})
|
||||
@@ -253,7 +253,7 @@ namespace Database
|
||||
RangeResults<Track::PathResult> Track::findPaths(Session& session, std::optional<Range> range)
|
||||
{
|
||||
using QueryResultType = std::tuple<TrackId, std::string>;
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
// TODO Dbo traits on filesystem
|
||||
auto query{ session.getDboSession().query<QueryResultType>("SELECT id, file_path FROM track") };
|
||||
@@ -276,7 +276,7 @@ namespace Database
|
||||
|
||||
RangeResults<TrackId> Track::findIdsTrackMBIDDuplicates(Session& session, std::optional<Range> range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession().query<TrackId>("SELECT track.id 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") };
|
||||
@@ -286,7 +286,7 @@ namespace Database
|
||||
|
||||
RangeResults<TrackId> Track::findIdsWithRecordingMBIDAndMissingFeatures(Session& session, std::optional<Range> range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession().query<TrackId>("SELECT t.id FROM track t")
|
||||
.where("LENGTH(t.recording_mbid) > 0")
|
||||
@@ -314,7 +314,7 @@ namespace Database
|
||||
|
||||
RangeResults<TrackId> Track::findIds(Session& session, const FindParameters& parameters)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ createQuery<TrackId>(session, parameters) };
|
||||
return Utils::execQuery<TrackId>(query, parameters.range);
|
||||
@@ -322,7 +322,7 @@ namespace Database
|
||||
|
||||
RangeResults<Track::pointer> Track::find(Session& session, const FindParameters& parameters)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ createQuery<Wt::Dbo::ptr<Track>>(session, parameters) };
|
||||
return Utils::execQuery<Track::pointer>(query, parameters.range);
|
||||
@@ -330,7 +330,7 @@ namespace Database
|
||||
|
||||
void Track::find(Session& session, const FindParameters& params, std::function<void(const Track::pointer&)> func)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ createQuery<Wt::Dbo::ptr<Track>>(session, params)};
|
||||
Utils::execQuery(query, params.range, func);
|
||||
@@ -339,7 +339,7 @@ namespace Database
|
||||
RangeResults<TrackId> Track::findSimilarTrackIds(Session& session, const std::vector<TrackId>& tracks, std::optional<Range> range)
|
||||
{
|
||||
assert(!tracks.empty());
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
std::ostringstream oss;
|
||||
for (std::size_t i{}; i < tracks.size(); ++i)
|
||||
@@ -479,7 +479,7 @@ namespace Database
|
||||
return std::vector<TrackArtistLink::pointer>(_trackArtistLinks.begin(), _trackArtistLinks.end());
|
||||
}
|
||||
|
||||
std::vector<std::vector<Cluster::pointer>> Track::getClusterGroups(const std::vector<ClusterType::pointer>& clusterTypes, std::size_t size) const
|
||||
std::vector<std::vector<Cluster::pointer>> Track::getClusterGroups(const std::vector<ClusterTypeId>& clusterTypeIds, std::size_t size) const
|
||||
{
|
||||
assert(self());
|
||||
assert(session());
|
||||
@@ -493,8 +493,8 @@ namespace Database
|
||||
where.And(WhereClause("t.id = ?")).bind(getId().toString());
|
||||
{
|
||||
WhereClause clusterClause;
|
||||
for (auto clusterType : clusterTypes)
|
||||
clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterType->getId().toString());
|
||||
for (ClusterTypeId clusterTypeId : clusterTypeIds)
|
||||
clusterClause.Or(WhereClause("c_type.id = ?")).bind(clusterTypeId.toString());
|
||||
where.And(clusterClause);
|
||||
}
|
||||
oss << " " << where.get();
|
||||
@@ -514,8 +514,8 @@ namespace Database
|
||||
}
|
||||
|
||||
std::vector<std::vector<Cluster::pointer>> res;
|
||||
for (auto cluster_list : clusters)
|
||||
res.push_back(cluster_list.second);
|
||||
for (const auto& [type, clusters] : clusters)
|
||||
res.push_back(clusters);
|
||||
|
||||
return res;
|
||||
}
|
||||
@@ -524,7 +524,7 @@ namespace Database
|
||||
{
|
||||
std::ostream& operator<<(std::ostream& os, const TrackInfo& trackInfo)
|
||||
{
|
||||
auto transaction{ trackInfo.session.createSharedTransaction() };
|
||||
auto transaction{ trackInfo.session.createReadTransaction() };
|
||||
|
||||
const Track::pointer track{ Track::find(trackInfo.session, trackInfo.trackId) };
|
||||
if (track)
|
||||
@@ -536,7 +536,7 @@ namespace Database
|
||||
for (auto artist : track->getArtists({ TrackArtistLinkType::Artist }))
|
||||
os << " - " << artist->getName();
|
||||
for (auto cluster : track->getClusters())
|
||||
os << " {" + cluster->getType()->getName() << "-" << cluster->getName() << "}";
|
||||
os << " {" << cluster->getType()->getName() << "-" << cluster->getName() << "}";
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace Database
|
||||
{
|
||||
Wt::Dbo::Query<TrackArtistLinkId> createQuery(Session& session, const TrackArtistLink::FindParameters& params)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession().query<TrackArtistLinkId>("SELECT DISTINCT t_a_l.id FROM track_artist_link t_a_l") };
|
||||
|
||||
@@ -65,7 +65,7 @@ namespace Database
|
||||
|
||||
TrackArtistLink::pointer TrackArtistLink::create(Session& session, ObjectPtr<Track> track, ObjectPtr<Artist> artist, TrackArtistLinkType type, std::string_view subType)
|
||||
{
|
||||
session.checkUniqueLocked();
|
||||
session.checkWriteTransaction();
|
||||
|
||||
TrackArtistLink::pointer res{ session.getDboSession().add(std::make_unique<TrackArtistLink>(track, artist, type, subType)) };
|
||||
session.getDboSession().flush();
|
||||
@@ -75,13 +75,13 @@ namespace Database
|
||||
|
||||
TrackArtistLink::pointer TrackArtistLink::find(Session& session, TrackArtistLinkId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
return session.getDboSession().find<TrackArtistLink>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
|
||||
RangeResults<TrackArtistLinkId> TrackArtistLink::find(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ createQuery(session, params) };
|
||||
return Utils::execQuery<TrackArtistLinkId>(query, params.range);
|
||||
@@ -89,7 +89,7 @@ namespace Database
|
||||
|
||||
EnumSet<TrackArtistLinkType> TrackArtistLink::findUsedTypes(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto res{ session.getDboSession().query<TrackArtistLinkType>("SELECT DISTINCT type from track_artist_link").resultList() };
|
||||
|
||||
@@ -98,7 +98,7 @@ namespace Database
|
||||
|
||||
EnumSet<TrackArtistLinkType> TrackArtistLink::findUsedTypes(Session& session, ArtistId artistId)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto res{ session.getDboSession()
|
||||
.query<TrackArtistLinkType>("SELECT DISTINCT type from track_artist_link")
|
||||
|
||||
@@ -40,14 +40,14 @@ namespace Database {
|
||||
|
||||
std::size_t TrackBookmark::getCount(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM track_bookmark");
|
||||
}
|
||||
|
||||
RangeResults<TrackBookmarkId> TrackBookmark::find(Session& session, UserId userId, std::optional<Range> range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession().query<TrackBookmarkId>("SELECT id from track_bookmark")
|
||||
.where("user_id = ?").bind(userId) };
|
||||
@@ -57,7 +57,7 @@ namespace Database {
|
||||
|
||||
TrackBookmark::pointer TrackBookmark::find(Session& session, UserId userId, TrackId trackId)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().find<TrackBookmark>()
|
||||
.where("user_id = ?").bind(userId)
|
||||
@@ -67,7 +67,7 @@ namespace Database {
|
||||
|
||||
TrackBookmark::pointer TrackBookmark::find(Session& session, TrackBookmarkId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().find<TrackBookmark>()
|
||||
.where("id = ?").bind(id)
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/Track.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
#include "IdTypeTraits.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
@@ -43,14 +43,14 @@ namespace Database {
|
||||
|
||||
std::size_t TrackFeatures::getCount(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM track_features");
|
||||
}
|
||||
|
||||
TrackFeatures::pointer TrackFeatures::find(Session& session, TrackFeaturesId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().find<TrackFeatures>()
|
||||
.where("id = ?").bind(id)
|
||||
@@ -59,7 +59,7 @@ namespace Database {
|
||||
|
||||
TrackFeatures::pointer TrackFeatures::find(Session& session, TrackId trackId)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().find<TrackFeatures>()
|
||||
.where("track_id = ?").bind(trackId)
|
||||
@@ -68,7 +68,7 @@ namespace Database {
|
||||
|
||||
RangeResults<TrackFeaturesId> TrackFeatures::find(Session& session, std::optional<Range> range)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession().query<TrackFeaturesId>("SELECT id from track_features") };
|
||||
|
||||
@@ -111,7 +111,7 @@ namespace Database {
|
||||
}
|
||||
catch (boost::property_tree::ptree_error& error)
|
||||
{
|
||||
LMS_LOG(DB, ERROR) << "Track " << _track.id() << ": ptree exception: " << error.what();
|
||||
LMS_LOG(DB, ERROR, "Track " << _track.id() << ": ptree exception: " << error.what());
|
||||
res.clear();
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
#include <cassert>
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
|
||||
#include "services/database/Artist.hpp"
|
||||
#include "services/database/Cluster.hpp"
|
||||
@@ -53,7 +53,7 @@ namespace Database
|
||||
|
||||
std::size_t TrackList::getCount(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM tracklist");
|
||||
}
|
||||
@@ -61,7 +61,7 @@ namespace Database
|
||||
|
||||
TrackList::pointer TrackList::find(Session& session, std::string_view name, TrackListType type, UserId userId)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
assert(userId.isValid());
|
||||
|
||||
return session.getDboSession().find<TrackList>()
|
||||
@@ -72,7 +72,7 @@ namespace Database
|
||||
|
||||
RangeResults<TrackListId> TrackList::find(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession().query<TrackListId>("SELECT DISTINCT t_l.id FROM tracklist t_l") };
|
||||
|
||||
@@ -122,7 +122,7 @@ namespace Database
|
||||
|
||||
TrackList::pointer TrackList::find(Session& session, TrackListId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().find<TrackList>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
@@ -187,12 +187,12 @@ namespace Database
|
||||
return std::vector<Cluster::pointer>(res.begin(), res.end());
|
||||
}
|
||||
|
||||
std::vector<std::vector<Cluster::pointer>> TrackList::getClusterGroups(const std::vector<ClusterType::pointer>& clusterTypes, std::size_t size) const
|
||||
std::vector<std::vector<Cluster::pointer>> TrackList::getClusterGroups(const std::vector<ClusterTypeId>& clusterTypeIds, std::size_t size) const
|
||||
{
|
||||
assert(session());
|
||||
std::vector<std::vector<Cluster::pointer>> res;
|
||||
|
||||
if (clusterTypes.empty())
|
||||
if (clusterTypeIds.empty())
|
||||
return res;
|
||||
|
||||
auto query{ session()->query<Wt::Dbo::ptr<Cluster>>("SELECT c from cluster c") };
|
||||
@@ -208,12 +208,12 @@ namespace Database
|
||||
std::ostringstream oss;
|
||||
oss << "c_type.id IN (";
|
||||
bool first{ true };
|
||||
for (auto clusterType : clusterTypes)
|
||||
for (ClusterTypeId clusterTypeId : clusterTypeIds)
|
||||
{
|
||||
if (!first)
|
||||
oss << ", ";
|
||||
oss << "?";
|
||||
query.bind(clusterType->getId());
|
||||
query.bind(clusterTypeId);
|
||||
first = false;
|
||||
}
|
||||
oss << ")";
|
||||
@@ -323,7 +323,7 @@ namespace Database
|
||||
|
||||
TrackListEntry::pointer TrackListEntry::getById(Session& session, TrackListEntryId id)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().find<TrackListEntry>().where("id = ?").bind(id).resultValue();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "services/database/TransactionChecker.hpp"
|
||||
|
||||
#include <cassert>
|
||||
|
||||
#include "services/database/Session.hpp"
|
||||
|
||||
#if !defined(NDEBUG)
|
||||
#define LMS_CHECK_TRANSACTION_ACCESSES 1
|
||||
#else
|
||||
#define LMS_CHECK_TRANSACTION_ACCESSES 0
|
||||
#endif
|
||||
|
||||
namespace Database
|
||||
{
|
||||
#if LMS_CHECK_TRANSACTION_ACCESSES
|
||||
namespace
|
||||
{
|
||||
struct StackEntry
|
||||
{
|
||||
TransactionChecker::TransactionType type;
|
||||
Wt::Dbo::Session* session{};
|
||||
};
|
||||
|
||||
static thread_local std::vector<StackEntry> transactionStack;
|
||||
}
|
||||
#endif
|
||||
|
||||
void TransactionChecker::pushWriteTransaction(Wt::Dbo::Session& session)
|
||||
{
|
||||
pushTransaction(TransactionType::Write, session);
|
||||
}
|
||||
|
||||
void TransactionChecker::pushReadTransaction(Wt::Dbo::Session& session)
|
||||
{
|
||||
pushTransaction(TransactionType::Read, session);
|
||||
}
|
||||
|
||||
void TransactionChecker::popWriteTransaction(Wt::Dbo::Session& session)
|
||||
{
|
||||
popTransaction(TransactionType::Write, session);
|
||||
}
|
||||
|
||||
void TransactionChecker::popReadTransaction(Wt::Dbo::Session& session)
|
||||
{
|
||||
popTransaction(TransactionType::Read, session);
|
||||
}
|
||||
|
||||
void TransactionChecker::pushTransaction([[maybe_unused]] TransactionType type, [[maybe_unused]] Wt::Dbo::Session& session)
|
||||
{
|
||||
#if LMS_CHECK_TRANSACTION_ACCESSES
|
||||
assert(transactionStack.empty() || transactionStack.back().session == &session);
|
||||
transactionStack.push_back(StackEntry{ type, &session });
|
||||
#endif // LMS_CHECK_TRANSACTION_ACCESSES
|
||||
}
|
||||
|
||||
void TransactionChecker::popTransaction([[maybe_unused]] TransactionType type, [[maybe_unused]] Wt::Dbo::Session& session)
|
||||
{
|
||||
#if LMS_CHECK_TRANSACTION_ACCESSES
|
||||
|
||||
assert(!transactionStack.empty());
|
||||
assert(transactionStack.back().type == type);
|
||||
assert(transactionStack.back().session == &session);
|
||||
transactionStack.pop_back();
|
||||
#endif // LMS_CHECK_TRANSACTION_ACCESSES
|
||||
}
|
||||
|
||||
void TransactionChecker::checkWriteTransaction([[maybe_unused]] Wt::Dbo::Session& session)
|
||||
{
|
||||
assert(!transactionStack.empty());
|
||||
assert(transactionStack.back().type == TransactionType::Write);
|
||||
assert(transactionStack.back().session == &session);
|
||||
}
|
||||
|
||||
void TransactionChecker::checkWriteTransaction(Session& session)
|
||||
{
|
||||
checkWriteTransaction(session.getDboSession());
|
||||
}
|
||||
|
||||
void TransactionChecker::checkReadTransaction([[maybe_unused]] Wt::Dbo::Session& session)
|
||||
{
|
||||
assert(!transactionStack.empty());
|
||||
assert(transactionStack.back().session == &session);
|
||||
}
|
||||
|
||||
void TransactionChecker::checkReadTransaction(Session& session)
|
||||
{
|
||||
checkReadTransaction(session.getDboSession());
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@
|
||||
#include "services/database/Release.hpp"
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/Track.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
#include "IdTypeTraits.hpp"
|
||||
#include "StringViewTraits.hpp"
|
||||
#include "Utils.hpp"
|
||||
@@ -42,14 +42,14 @@ namespace Database {
|
||||
|
||||
std::size_t User::getCount(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM user");
|
||||
}
|
||||
|
||||
RangeResults<UserId> User::find(Session& session, const FindParameters& params)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
auto query{ session.getDboSession().query<UserId>("SELECT id FROM user") };
|
||||
|
||||
@@ -63,7 +63,7 @@ namespace Database {
|
||||
|
||||
User::pointer User::findDemoUser(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
session.checkReadTransaction();
|
||||
|
||||
return session.getDboSession().find<User>().where("type = ?").bind(UserType::DEMO).resultValue();
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ namespace Database
|
||||
// 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;
|
||||
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(std::vector<ClusterTypeId> clusterTypeIds, std::size_t size) const;
|
||||
|
||||
void setName(std::string_view name) { _name = name; }
|
||||
void setMBID(const std::optional<UUID>& mbid) { _MBID = mbid ? mbid->getAsString() : ""; }
|
||||
|
||||
@@ -38,7 +38,6 @@ namespace Database {
|
||||
|
||||
class Track;
|
||||
class ClusterType;
|
||||
class ScanSettings;
|
||||
class Session;
|
||||
|
||||
class Cluster final : public Object<Cluster, ClusterId>
|
||||
@@ -47,12 +46,14 @@ namespace Database {
|
||||
struct FindParameters
|
||||
{
|
||||
std::optional<Range> range;
|
||||
ClusterTypeId clusterType; // if non empty, clusters that belong to this cluster type
|
||||
ClusterTypeId clusterType; // if non empty, clusters that belong to this cluster type
|
||||
std::string clusterTypeName; // if non empty, clusters that belong to this cluster type
|
||||
TrackId track; // if set, clusters involved in this track
|
||||
ReleaseId release; // if set, clusters involved in this release
|
||||
|
||||
FindParameters& setRange(std::optional<Range> _range) { range = _range; return *this; }
|
||||
FindParameters& setClusterType(ClusterTypeId _clusterType) { clusterType = _clusterType; return *this; }
|
||||
FindParameters& setClusterTypeName(std::string_view _name) { clusterTypeName = _name; return *this; }
|
||||
FindParameters& setTrack(TrackId _track) { track = _track; return *this; }
|
||||
FindParameters& setRelease(ReleaseId _release) { release = _release; return *this; }
|
||||
};
|
||||
@@ -65,7 +66,7 @@ namespace Database {
|
||||
static RangeResults<pointer> find(Session& session, const FindParameters& params);
|
||||
static void find(Session& session, const FindParameters& params, std::function<void(const pointer& cluster)> _func);
|
||||
static pointer find(Session& session, ClusterId id);
|
||||
static RangeResults<ClusterId> findOrphans(Session& session, std::optional<Range> range = std::nullopt);
|
||||
static RangeResults<ClusterId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt);
|
||||
|
||||
// May be very slow
|
||||
static std::size_t computeTrackCount(Session& session, ClusterId id);
|
||||
@@ -117,16 +118,16 @@ namespace Database {
|
||||
|
||||
// Getters
|
||||
static std::size_t getCount(Session& session);
|
||||
static RangeResults<ClusterTypeId> find(Session& session, std::optional<Range> range = std::nullopt);
|
||||
static RangeResults<ClusterTypeId> findIds(Session& session, std::optional<Range> range = std::nullopt);
|
||||
static pointer find(Session& session, std::string_view name);
|
||||
static pointer find(Session& session, ClusterTypeId id);
|
||||
static RangeResults<ClusterTypeId> findOrphans(Session& session, std::optional<Range> range = std::nullopt);
|
||||
static RangeResults<ClusterTypeId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt);
|
||||
static RangeResults<ClusterTypeId> findUsed(Session& session, std::optional<Range> range = std::nullopt);
|
||||
|
||||
static void remove(Session& session, const std::string& name);
|
||||
|
||||
// Accessors
|
||||
const std::string& getName() const { return _name; }
|
||||
std::string_view getName() const { return _name; }
|
||||
std::vector<Cluster::pointer> getClusters() const;
|
||||
Cluster::pointer getCluster(const std::string& name) const;
|
||||
|
||||
@@ -135,19 +136,17 @@ namespace Database {
|
||||
{
|
||||
Wt::Dbo::field(a, _name, "name");
|
||||
Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToOne, "cluster_type");
|
||||
Wt::Dbo::belongsTo(a, _scanSettings, "scan_settings", Wt::Dbo::OnDeleteCascade);
|
||||
}
|
||||
|
||||
private:
|
||||
friend class Session;
|
||||
ClusterType(std::string_view name);
|
||||
static pointer create(Session& session, const std::string& name);
|
||||
static pointer create(Session& session, std::string_view name);
|
||||
|
||||
static const std::size_t _maxNameLength = 128;
|
||||
|
||||
std::string _name;
|
||||
Wt::Dbo::collection< Wt::Dbo::ptr<Cluster> > _clusters;
|
||||
Wt::Dbo::ptr<ScanSettings> _scanSettings;
|
||||
};
|
||||
|
||||
} // namespace Database
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include <Wt/WSignal.h>
|
||||
#include <Wt/Dbo/ptr.h>
|
||||
#include "services/database/IdType.hpp"
|
||||
#include "services/database/TransactionChecker.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
@@ -38,9 +39,11 @@ namespace Database
|
||||
bool operator==(const ObjectPtr& other) const { return _obj == other._obj; }
|
||||
bool operator!=(const ObjectPtr& other) const { return other._obj != _obj; }
|
||||
|
||||
auto modify() { return _obj.modify(); }
|
||||
auto modify() { TransactionChecker::checkWriteTransaction(*_obj.session()); return _obj.modify(); }
|
||||
void remove()
|
||||
{
|
||||
TransactionChecker::checkWriteTransaction(*_obj.session());
|
||||
|
||||
if (_obj->hasOnPreRemove())
|
||||
_obj.modify()->onPreRemove();
|
||||
_obj.remove();
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
@@ -61,9 +62,9 @@ namespace Database
|
||||
ArtistId artist; // only releases that involved this user
|
||||
EnumSet<TrackArtistLinkType> trackArtistLinkTypes; // and for these link types
|
||||
EnumSet<TrackArtistLinkType> excludedTrackArtistLinkTypes; // but not for these link types
|
||||
std::optional<ReleaseTypePrimary> primaryType; // if, set, matching this primary type
|
||||
std::optional<ReleaseTypePrimary> primaryType; // if set, matching this primary type
|
||||
EnumSet<ReleaseTypeSecondary> secondaryTypes; // Matching all this (if any)
|
||||
|
||||
|
||||
FindParameters& setClusters(const std::vector<ClusterId>& _clusters) { clusters = _clusters; return *this; }
|
||||
FindParameters& setKeywords(const std::vector<std::string_view>& _keywords) { keywords = _keywords; return *this; }
|
||||
FindParameters& setSortMethod(ReleaseSortMethod _sortMethod) { sortMethod = _sortMethod; return *this; }
|
||||
@@ -86,18 +87,19 @@ namespace Database
|
||||
static std::size_t getCount(Session& session);
|
||||
static bool exists(Session& session, ReleaseId id);
|
||||
static pointer find(Session& session, const UUID& MBID);
|
||||
static std::vector<pointer> find(Session& session, const std::string& name);
|
||||
static std::vector<pointer> find(Session& session, const std::string& name, const std::filesystem::path& releaseDirectory);
|
||||
static pointer find(Session& session, ReleaseId id);
|
||||
static RangeResults<pointer> find(Session& session, const FindParameters& parameters);
|
||||
static void find(Session& session, const FindParameters& parameters, std::function<void(const pointer&)> func);
|
||||
static RangeResults<ReleaseId> findIds(Session& session, const FindParameters& parameters);
|
||||
static std::size_t getCount(Session& session, const FindParameters& parameters);
|
||||
static RangeResults<ReleaseId> findOrphanIds(Session& session, std::optional<Range> range = std::nullopt); // not track related
|
||||
static RangeResults<ReleaseId> findIdsOrderedByArtist(Session& session, std::optional<Range> range = std::nullopt);
|
||||
|
||||
// Get the cluster of the tracks that belong to this release
|
||||
// Each clusters are grouped by cluster type, sorted by the number of occurence (max to min)
|
||||
// size is the max number of cluster per cluster type
|
||||
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(const std::vector<ObjectPtr<ClusterType>>& clusterTypes, std::size_t size) const;
|
||||
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(const std::vector<ClusterTypeId>& clusterTypeIds, std::size_t size) const;
|
||||
|
||||
// Utility functions (if all tracks have the same values, which is legit to not be the case)
|
||||
Wt::WDate getReleaseDate() const;
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include <Wt/Dbo/Dbo.h>
|
||||
@@ -30,9 +32,8 @@
|
||||
|
||||
LMS_DECLARE_IDTYPE(ScanSettingsId)
|
||||
|
||||
namespace Database {
|
||||
|
||||
class ClusterType;
|
||||
namespace Database
|
||||
{
|
||||
class Session;
|
||||
|
||||
class ScanSettings final : public Object<ScanSettings, ScanSettingsId>
|
||||
@@ -65,7 +66,7 @@ namespace Database {
|
||||
std::filesystem::path getMediaDirectory() const { return _mediaDirectory; }
|
||||
Wt::WTime getUpdateStartTime() const { return _startTime; }
|
||||
UpdatePeriod getUpdatePeriod() const { return _updatePeriod; }
|
||||
std::vector<ObjectPtr<ClusterType>> getClusterTypes() const;
|
||||
std::vector<std::string_view> getExtraTagsToScan() const;
|
||||
std::vector<std::filesystem::path> getAudioFileExtensions() const;
|
||||
SimilarityEngineType getSimilarityEngineType() const { return _similarityEngineType; }
|
||||
|
||||
@@ -74,7 +75,7 @@ namespace Database {
|
||||
void setMediaDirectory(const std::filesystem::path& p);
|
||||
void setUpdateStartTime(Wt::WTime t) { _startTime = t; }
|
||||
void setUpdatePeriod(UpdatePeriod p) { _updatePeriod = p; }
|
||||
void setClusterTypes(Session& session, const std::set<std::string>& clusterTypeNames);
|
||||
void setExtraTagsToScan(const std::vector<std::string_view>& extraTags);
|
||||
void setSimilarityEngineType(SimilarityEngineType type) { _similarityEngineType = type; }
|
||||
void incScanVersion();
|
||||
|
||||
@@ -87,20 +88,17 @@ namespace Database {
|
||||
Wt::Dbo::field(a, _updatePeriod, "update_period");
|
||||
Wt::Dbo::field(a, _audioFileExtensions, "audio_file_extensions");
|
||||
Wt::Dbo::field(a, _similarityEngineType, "similarity_engine_type");
|
||||
Wt::Dbo::hasMany(a, _clusterTypes, Wt::Dbo::ManyToOne, "scan_settings");
|
||||
Wt::Dbo::field(a, _extraTagsToScan, "extra_tags_to_scan");
|
||||
}
|
||||
|
||||
private:
|
||||
int _scanVersion{};
|
||||
std::string _mediaDirectory;
|
||||
Wt::WTime _startTime = Wt::WTime{ 0,0,0 };
|
||||
UpdatePeriod _updatePeriod{ UpdatePeriod::Never };
|
||||
int _scanVersion{};
|
||||
std::string _mediaDirectory;
|
||||
Wt::WTime _startTime = Wt::WTime{ 0,0,0 };
|
||||
UpdatePeriod _updatePeriod{ UpdatePeriod::Never };
|
||||
SimilarityEngineType _similarityEngineType{ SimilarityEngineType::Clusters };
|
||||
std::string _audioFileExtensions{ ".alac .mp3 .ogg .oga .aac .m4a .m4b .flac .wav .wma .aif .aiff .ape .mpc .shn .opus .wv" };
|
||||
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<ClusterType>> _clusterTypes;
|
||||
std::string _extraTagsToScan;
|
||||
};
|
||||
|
||||
|
||||
} // namespace Database
|
||||
|
||||
|
||||
@@ -24,27 +24,37 @@
|
||||
|
||||
#include "utils/RecursiveSharedMutex.hpp"
|
||||
#include "services/database/Object.hpp"
|
||||
|
||||
#include "services/database/TransactionChecker.hpp"
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class UniqueTransaction
|
||||
class WriteTransaction
|
||||
{
|
||||
public:
|
||||
~WriteTransaction();
|
||||
private:
|
||||
friend class Session;
|
||||
UniqueTransaction(RecursiveSharedMutex& mutex, Wt::Dbo::Session& session);
|
||||
WriteTransaction(RecursiveSharedMutex& mutex, Wt::Dbo::Session& session);
|
||||
|
||||
WriteTransaction(const WriteTransaction&) = delete;
|
||||
WriteTransaction& operator=(const WriteTransaction&) = delete;
|
||||
|
||||
std::unique_lock<RecursiveSharedMutex> _lock;
|
||||
Wt::Dbo::Transaction _transaction;
|
||||
};
|
||||
|
||||
class SharedTransaction
|
||||
class ReadTransaction
|
||||
{
|
||||
public:
|
||||
~ReadTransaction();
|
||||
private:
|
||||
friend class Session;
|
||||
SharedTransaction(RecursiveSharedMutex& mutex, Wt::Dbo::Session& session);
|
||||
ReadTransaction(Wt::Dbo::Session& session);
|
||||
|
||||
|
||||
ReadTransaction(const ReadTransaction&) = delete;
|
||||
ReadTransaction& operator=(const ReadTransaction&) = delete;
|
||||
|
||||
std::shared_lock<RecursiveSharedMutex> _lock;
|
||||
Wt::Dbo::Transaction _transaction;
|
||||
};
|
||||
|
||||
@@ -54,11 +64,11 @@ namespace Database
|
||||
public:
|
||||
Session(Db& database);
|
||||
|
||||
[[nodiscard]] UniqueTransaction createUniqueTransaction();
|
||||
[[nodiscard]] SharedTransaction createSharedTransaction();
|
||||
[[nodiscard]] WriteTransaction createWriteTransaction();
|
||||
[[nodiscard]] ReadTransaction createReadTransaction();
|
||||
|
||||
void checkUniqueLocked();
|
||||
void checkSharedLocked();
|
||||
void checkWriteTransaction() { TransactionChecker::checkWriteTransaction(_session); }
|
||||
void checkReadTransaction() { TransactionChecker::checkReadTransaction(_session); }
|
||||
|
||||
void analyze();
|
||||
void optimize();
|
||||
@@ -71,7 +81,7 @@ namespace Database
|
||||
template <typename Object, typename... Args>
|
||||
typename Object::pointer create(Args&&... args)
|
||||
{
|
||||
checkUniqueLocked();
|
||||
TransactionChecker::checkWriteTransaction(_session);
|
||||
|
||||
typename Object::pointer res{ Object::create(*this, std::forward<Args>(args)...) };
|
||||
getDboSession().flush();
|
||||
@@ -90,5 +100,3 @@ namespace Database
|
||||
Wt::Dbo::Session _session;
|
||||
};
|
||||
} // namespace Database
|
||||
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ namespace Database
|
||||
// Search utility
|
||||
static std::size_t getCount(Session& session);
|
||||
static pointer find(Session& session, StarredArtistId id);
|
||||
static pointer find(Session& session, ArtistId artistId, UserId userId); // current backend
|
||||
static pointer find(Session& session, ArtistId artistId, UserId userId, FeedbackBackend backend);
|
||||
|
||||
// Accessors
|
||||
|
||||
@@ -42,6 +42,7 @@ namespace Database
|
||||
// Search utility
|
||||
static std::size_t getCount(Session& session);
|
||||
static pointer find(Session& session, StarredReleaseId id);
|
||||
static pointer find(Session& session, ReleaseId releaseId, UserId userId); // current feedback backend
|
||||
static pointer find(Session& session, ReleaseId releaseId, UserId userId, FeedbackBackend backend);
|
||||
|
||||
// Accessors
|
||||
|
||||
@@ -55,7 +55,9 @@ namespace Database
|
||||
// Search utility
|
||||
static std::size_t getCount(Session& session);
|
||||
static pointer find(Session& session, StarredTrackId id);
|
||||
static pointer find(Session& session, TrackId trackId, UserId userId); // current feedback backend
|
||||
static pointer find(Session& session, TrackId trackId, UserId userId, FeedbackBackend backend);
|
||||
static bool exists(Session& session, TrackId trackId, UserId userId, FeedbackBackend backend);
|
||||
static RangeResults<StarredTrackId> find(Session& session, const FindParameters& findParams);
|
||||
|
||||
// Accessors
|
||||
|
||||
@@ -176,7 +176,7 @@ namespace Database {
|
||||
std::vector<ObjectPtr<Cluster>> getClusters() const;
|
||||
std::vector<ClusterId> getClusterIds() const;
|
||||
|
||||
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(const std::vector<ObjectPtr<ClusterType>>& clusterTypes, std::size_t size) const;
|
||||
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(const std::vector<ClusterTypeId>& clusterTypes, std::size_t size) const;
|
||||
|
||||
template<class Action>
|
||||
void persist(Action& a)
|
||||
|
||||
@@ -99,7 +99,7 @@ namespace Database {
|
||||
|
||||
// Get clusters, order by occurence
|
||||
std::vector<ObjectPtr<Cluster>> getClusters() const;
|
||||
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(const std::vector<ObjectPtr<ClusterType>>& clusterTypes, std::size_t size) const;
|
||||
std::vector<std::vector<ObjectPtr<Cluster>>> getClusterGroups(const std::vector<ClusterTypeId>& clusterTypeIds, std::size_t size) const;
|
||||
|
||||
bool hasTrack(TrackId trackId) const;
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright (C) 2023 Emeric Poupon
|
||||
*
|
||||
* This file is part of LMS.
|
||||
*
|
||||
* LMS is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* LMS is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with LMS. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <Wt/Dbo/Session.h>
|
||||
|
||||
namespace Database
|
||||
{
|
||||
class Session;
|
||||
|
||||
class TransactionChecker
|
||||
{
|
||||
public:
|
||||
enum class TransactionType
|
||||
{
|
||||
Read,
|
||||
Write,
|
||||
};
|
||||
|
||||
static void pushWriteTransaction(Wt::Dbo::Session& session);
|
||||
static void pushReadTransaction(Wt::Dbo::Session& session);
|
||||
|
||||
static void popWriteTransaction(Wt::Dbo::Session& session);
|
||||
static void popReadTransaction(Wt::Dbo::Session& session);
|
||||
|
||||
static void checkWriteTransaction(Wt::Dbo::Session& session);
|
||||
static void checkWriteTransaction(Session& session);
|
||||
static void checkReadTransaction(Wt::Dbo::Session& session);
|
||||
static void checkReadTransaction(Session& session);
|
||||
|
||||
private:
|
||||
static void pushTransaction(TransactionType type, Wt::Dbo::Session& session);
|
||||
static void popTransaction(TransactionType type, Wt::Dbo::Session& session);
|
||||
};
|
||||
}
|
||||
@@ -24,7 +24,7 @@ using namespace Database;
|
||||
TEST_F(DatabaseFixture, Artist)
|
||||
{
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_FALSE(Artist::exists(session, 35));
|
||||
EXPECT_FALSE(Artist::exists(session, 0));
|
||||
EXPECT_FALSE(Artist::exists(session, 1));
|
||||
@@ -34,7 +34,7 @@ TEST_F(DatabaseFixture, Artist)
|
||||
ScopedArtist artist{ session, "MyArtist" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
EXPECT_TRUE(artist.get());
|
||||
EXPECT_FALSE(!artist.get());
|
||||
@@ -45,7 +45,7 @@ TEST_F(DatabaseFixture, Artist)
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto artists{ Artist::findIds(session, Artist::FindParameters {}) };
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
@@ -58,7 +58,7 @@ TEST_F(DatabaseFixture, Artist)
|
||||
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto artists{ Artist::find(session, Artist::FindParameters {}) };
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
@@ -66,7 +66,7 @@ TEST_F(DatabaseFixture, Artist)
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
bool visited{};
|
||||
Artist::find(session, Artist::FindParameters{}, [&](const Artist::pointer& a)
|
||||
@@ -84,19 +84,19 @@ TEST_F(DatabaseFixture, Artist_singleTrack)
|
||||
ScopedArtist artist{ session, "MyArtist" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
track.get().modify()->setName("MyTrackName");
|
||||
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_TRUE(Artist::findOrphanIds(session).results.empty());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto artists{ track->getArtists({TrackArtistLinkType::Artist}) };
|
||||
ASSERT_EQ(artists.size(), 1);
|
||||
@@ -113,7 +113,7 @@ TEST_F(DatabaseFixture, Artist_singleTrack)
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto artists{ track->getArtistIds({TrackArtistLinkType::Artist}) };
|
||||
ASSERT_EQ(artists.size(), 1);
|
||||
@@ -125,23 +125,23 @@ TEST_F(DatabaseFixture, Artist_singleTrack)
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackName").setArtistName("MyArtist")) };
|
||||
ASSERT_EQ(tracks.results.size(), 1);
|
||||
EXPECT_EQ(tracks.results.front(), track.getId());
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackName").setArtistName("MyArtistFoo")) };
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackNameFoo").setArtistName("MyArtist")) };
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
auto artists{ Artist::findIds(session, Artist::FindParameters{}.setTrack(track->getId())) };
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
EXPECT_EQ(artists.results.front(), artist.getId());
|
||||
@@ -153,7 +153,7 @@ TEST_F(DatabaseFixture, Artist_singleTracktMultiRoles)
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedArtist artist{ session, "MyArtist" };
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
|
||||
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::ReleaseArtist);
|
||||
@@ -161,12 +161,12 @@ TEST_F(DatabaseFixture, Artist_singleTracktMultiRoles)
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_TRUE(Artist::findOrphanIds(session, Range{}).results.empty());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}).results.size(), 1);
|
||||
EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setLinkType(TrackArtistLinkType::Artist)).results.size(), 1);
|
||||
EXPECT_EQ(Artist::findIds(session, Artist::FindParameters{}.setLinkType(TrackArtistLinkType::ReleaseArtist)).results.size(), 1);
|
||||
@@ -175,7 +175,7 @@ TEST_F(DatabaseFixture, Artist_singleTracktMultiRoles)
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto artists{ track->getArtists({TrackArtistLinkType::Artist}) };
|
||||
ASSERT_EQ(artists.size(), 1);
|
||||
@@ -202,7 +202,7 @@ TEST_F(DatabaseFixture, Artist_singleTracktMultiRoles)
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EnumSet<TrackArtistLinkType> types{ TrackArtistLink::findUsedTypes(session, artist.getId()) };
|
||||
EXPECT_TRUE(types.contains(TrackArtistLinkType::ReleaseArtist));
|
||||
EXPECT_TRUE(types.contains(TrackArtistLinkType::Artist));
|
||||
@@ -219,19 +219,19 @@ TEST_F(DatabaseFixture, Artist_singleTrackMultiArtists)
|
||||
ASSERT_NE(artist1.getId(), artist2.getId());
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
TrackArtistLink::create(session, track.get(), artist1.get(), TrackArtistLinkType::Artist);
|
||||
TrackArtistLink::create(session, track.get(), artist2.get(), TrackArtistLinkType::Artist);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_TRUE(Artist::findOrphanIds(session).results.empty());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto artists{ track->getArtists({TrackArtistLinkType::Artist}) };
|
||||
ASSERT_EQ(artists.size(), 2);
|
||||
@@ -246,7 +246,7 @@ TEST_F(DatabaseFixture, Artist_singleTrackMultiArtists)
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto tracks{ Track::findIds(session, Track::FindParameters {}.setArtist(artist1->getId())) };
|
||||
ASSERT_EQ(tracks.results.size(), 1);
|
||||
@@ -276,13 +276,13 @@ TEST_F(DatabaseFixture, Artist_findByName)
|
||||
ScopedTrack track{ session, "MyTrack" }; // filters does not work on orphans
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
artist.get().modify()->setSortName("ZZZ");
|
||||
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
EXPECT_TRUE(Artist::findIds(session, Artist::FindParameters{}.setKeywords({ "N" })).results.empty());
|
||||
|
||||
@@ -310,7 +310,7 @@ TEST_F(DatabaseFixture, Artist_findByNameEscaped)
|
||||
ScopedArtist artist6{ session, R"(%AMyArtist)" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
{
|
||||
const auto artists{ Artist::find(session, R"(MyArtist%)") };
|
||||
ASSERT_TRUE(artists.size() == 1);
|
||||
@@ -332,7 +332,7 @@ TEST_F(DatabaseFixture, Artist_findByNameEscaped)
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
{
|
||||
const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setKeywords({"MyArtist"})) };
|
||||
EXPECT_EQ(artists.results.size(), 6);
|
||||
@@ -366,14 +366,14 @@ TEST_F(DatabaseFixture, Artist_sortMethod)
|
||||
ScopedArtist artistB{ session, "artistB" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
artistA.get().modify()->setSortName("sortNameB");
|
||||
artistB.get().modify()->setSortName("sortNameA");
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto allArtistsByName{ Artist::findIds(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::ByName)) };
|
||||
auto allArtistsBySortName{ Artist::findIds(session, Artist::FindParameters {}.setSortMethod(ArtistSortMethod::BySortName)) };
|
||||
@@ -396,14 +396,14 @@ TEST_F(DatabaseFixture, Artist_nonReleaseTracks)
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto tracks{ Track::findIds(session, Track::FindParameters {}.setNonRelease(true).setArtist(artist->getId())) };
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
TrackArtistLink::create(session, track1.get(), artist.get(), TrackArtistLinkType::Artist);
|
||||
TrackArtistLink::create(session, track2.get(), artist.get(), TrackArtistLinkType::Artist);
|
||||
@@ -412,7 +412,7 @@ TEST_F(DatabaseFixture, Artist_nonReleaseTracks)
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto tracks{ Track::findIds(session, Track::FindParameters {}.setArtist(artist.getId()).setNonRelease(true)) };
|
||||
ASSERT_EQ(tracks.results.size(), 1);
|
||||
@@ -427,29 +427,29 @@ TEST_F(DatabaseFixture, Artist_findByRelease)
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setRelease(release.getId())) };
|
||||
EXPECT_EQ(artists.results.size(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setRelease(release.getId())) };
|
||||
EXPECT_EQ(artists.results.size(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track.get().modify()->setRelease(release.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
const auto artists{ Artist::findIds(session, Artist::FindParameters {}.setRelease(release.getId())) };
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
EXPECT_EQ(artists.results.front(), artist.getId());
|
||||
|
||||
@@ -27,7 +27,7 @@ using namespace Database;
|
||||
TEST_F(DatabaseFixture, Cluster)
|
||||
{
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
EXPECT_EQ(Cluster::getCount(session), 0);
|
||||
EXPECT_EQ(ClusterType::getCount(session), 0);
|
||||
}
|
||||
@@ -35,7 +35,7 @@ TEST_F(DatabaseFixture, Cluster)
|
||||
ScopedClusterType clusterType{ session, "MyType" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
EXPECT_EQ(ClusterType::getCount(session), 1);
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ TEST_F(DatabaseFixture, Cluster)
|
||||
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
EXPECT_EQ(Cluster::getCount(session), 1);
|
||||
EXPECT_EQ(cluster->getType()->getId(), clusterType.getId());
|
||||
@@ -55,12 +55,12 @@ TEST_F(DatabaseFixture, Cluster)
|
||||
}
|
||||
|
||||
{
|
||||
const auto clusters{ Cluster::findOrphans(session) };
|
||||
const auto clusters{ Cluster::findOrphanIds(session) };
|
||||
ASSERT_EQ(clusters.results.size(), 1);
|
||||
EXPECT_EQ(clusters.results.front(), cluster.getId());
|
||||
}
|
||||
|
||||
auto clusterTypes{ ClusterType::find(session) };
|
||||
auto clusterTypes{ ClusterType::findIds(session) };
|
||||
ASSERT_EQ(clusterTypes.results.size(), 1);
|
||||
EXPECT_EQ(clusterTypes.results.front(), clusterType.getId());
|
||||
|
||||
@@ -68,15 +68,15 @@ TEST_F(DatabaseFixture, Cluster)
|
||||
ASSERT_EQ(clusterTypes.results.size(), 1);
|
||||
EXPECT_EQ(clusterTypes.results.front(), clusterType.getId());
|
||||
|
||||
clusterTypes = ClusterType::findOrphans(session);
|
||||
clusterTypes = ClusterType::findOrphanIds(session);
|
||||
EXPECT_TRUE(clusterTypes.results.empty());
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
auto clusterTypes{ ClusterType::findOrphans(session) };
|
||||
auto clusterTypes{ ClusterType::findOrphanIds(session) };
|
||||
ASSERT_EQ(clusterTypes.results.size(), 1);
|
||||
EXPECT_EQ(clusterTypes.results.front(), clusterType.getId());
|
||||
|
||||
@@ -90,9 +90,9 @@ TEST_F(DatabaseFixture, Cluster_singleTrack)
|
||||
ScopedClusterType clusterType{ session, "MyClusterType" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
EXPECT_TRUE(Cluster::findOrphans(session).results.empty());
|
||||
auto clusterTypes{ ClusterType::findOrphans(session) };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_TRUE(Cluster::findOrphanIds(session).results.empty());
|
||||
auto clusterTypes{ ClusterType::findOrphanIds(session) };
|
||||
ASSERT_EQ(clusterTypes.results.size(), 1);
|
||||
EXPECT_EQ(clusterTypes.results.front(), clusterType.getId());
|
||||
}
|
||||
@@ -101,8 +101,8 @@ TEST_F(DatabaseFixture, Cluster_singleTrack)
|
||||
ScopedCluster cluster2{ session, clusterType.lockAndGet(), "MyCluster2" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto clusters{ Cluster::findOrphans(session) };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
auto clusters{ Cluster::findOrphanIds(session) };
|
||||
EXPECT_EQ(clusters.results.size(), 2);
|
||||
EXPECT_TRUE(track->getClusters().empty());
|
||||
EXPECT_TRUE(track->getClusterIds().empty());
|
||||
@@ -111,13 +111,13 @@ TEST_F(DatabaseFixture, Cluster_singleTrack)
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
cluster1.get().modify()->addTrack(track.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
auto clusters{ Cluster::findIds(session, Cluster::FindParameters {}.setTrack(track.getId())) };
|
||||
ASSERT_EQ(clusters.results.size(), 1);
|
||||
EXPECT_EQ(clusters.results.front(), cluster1.getId());
|
||||
@@ -126,16 +126,16 @@ TEST_F(DatabaseFixture, Cluster_singleTrack)
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto clusters{ Cluster::findOrphans(session) };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
auto clusters{ Cluster::findOrphanIds(session) };
|
||||
ASSERT_EQ(clusters.results.size(), 1);
|
||||
EXPECT_EQ(clusters.results.front(), cluster2.getId());
|
||||
|
||||
EXPECT_TRUE(ClusterType::findOrphans(session).results.empty());
|
||||
EXPECT_TRUE(ClusterType::findOrphanIds(session).results.empty());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto tracks{ Track::findIds(session, Track::FindParameters {}.setClusters({cluster1.getId()})) };
|
||||
ASSERT_EQ(tracks.results.size(), 1);
|
||||
@@ -146,7 +146,7 @@ TEST_F(DatabaseFixture, Cluster_singleTrack)
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto clusters{ track->getClusters() };
|
||||
ASSERT_EQ(clusters.size(), 1);
|
||||
@@ -169,20 +169,20 @@ TEST_F(DatabaseFixture, Cluster_singleTrackWithSeveralClusters)
|
||||
const std::vector<ClusterId> clusterIds{ cluster1.getId(), cluster2.getId() };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setClusters(clusterIds)) };
|
||||
EXPECT_TRUE(tracks.results.empty());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
cluster1.get().modify()->addTrack(track.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setClusters(clusterIds)) };
|
||||
EXPECT_TRUE(tracks.results.empty());
|
||||
@@ -191,13 +191,13 @@ TEST_F(DatabaseFixture, Cluster_singleTrackWithSeveralClusters)
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
cluster2.get().modify()->addTrack(track.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto tracks{ Track::findIds(session, Track::FindParameters{}.setClusters(clusterIds)) };
|
||||
ASSERT_FALSE(tracks.results.empty());
|
||||
@@ -218,14 +218,14 @@ TEST_F(DatabaseFixture, Cluster_multiTracks)
|
||||
tracks.emplace_back(session, "MyTrack" + std::to_string(i));
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
cluster.get().modify()->addTrack(tracks.back().get());
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
EXPECT_TRUE(Cluster::findOrphans(session).results.empty());
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_TRUE(Cluster::findOrphanIds(session).results.empty());
|
||||
|
||||
EXPECT_EQ(Cluster::computeTrackCount(session, cluster.getId()), tracks.size());
|
||||
|
||||
@@ -237,14 +237,45 @@ TEST_F(DatabaseFixture, Cluster_multiTracks)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
TEST_F(DatabaseFixture, ClusterType_singleTrack)
|
||||
{
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_TRUE(Cluster::find(session, Cluster::FindParameters{}).results.empty());
|
||||
EXPECT_TRUE(Cluster::find(session, Cluster::FindParameters{}.setClusterTypeName("Foo")).results.empty());
|
||||
}
|
||||
|
||||
ScopedClusterType clusterType{ session, "MyClusterType" };
|
||||
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
auto clusters {Cluster::findIds(session, Cluster::FindParameters{}).results};
|
||||
ASSERT_EQ(clusters.size(), 1);
|
||||
EXPECT_EQ(clusters.front(), cluster.getId());
|
||||
|
||||
clusters = Cluster::findIds(session, Cluster::FindParameters{}.setClusterType(clusterType.getId())).results;
|
||||
ASSERT_EQ(clusters.size(), 1);
|
||||
EXPECT_EQ(clusters.front(), cluster.getId());
|
||||
|
||||
clusters = Cluster::findIds(session, Cluster::FindParameters{}.setClusterTypeName("Foo")).results;
|
||||
EXPECT_EQ(clusters.size(), 0);
|
||||
|
||||
clusters = Cluster::findIds(session, Cluster::FindParameters{}.setClusterTypeName("MyClusterType")).results;
|
||||
ASSERT_EQ(clusters.size(), 1);
|
||||
EXPECT_EQ(clusters.front(), cluster.getId());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Cluster_singleTrackSingleReleaseSingleCluster)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrackFile" };
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
EXPECT_TRUE(Cluster::findOrphans(session).results.empty());
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_TRUE(Cluster::findOrphanIds(session).results.empty());
|
||||
}
|
||||
|
||||
ScopedClusterType clusterType{ session, "MyClusterType" };
|
||||
@@ -252,8 +283,8 @@ TEST_F(DatabaseFixture, Cluster_singleTrackSingleReleaseSingleCluster)
|
||||
ScopedCluster unusedCluster{ session, clusterType.lockAndGet(), "MyClusterUnused" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
ASSERT_EQ(Cluster::findOrphans(session).results.size(), 2);
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
ASSERT_EQ(Cluster::findOrphanIds(session).results.size(), 2);
|
||||
EXPECT_TRUE(Release::find(session, Release::FindParameters{}.setClusters({ unusedCluster.getId() })).results.empty());
|
||||
EXPECT_EQ(Release::find(session, Release::FindParameters{}).results.size(), 1);
|
||||
EXPECT_EQ(Cluster::computeReleaseCount(session, cluster.getId()), 0);
|
||||
@@ -261,17 +292,17 @@ TEST_F(DatabaseFixture, Cluster_singleTrackSingleReleaseSingleCluster)
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
track.get().modify()->setRelease(release.get());
|
||||
cluster.get().modify()->addTrack(track.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
{
|
||||
auto clusters{ Cluster::findOrphans(session) };
|
||||
auto clusters{ Cluster::findOrphanIds(session) };
|
||||
ASSERT_EQ(clusters.results.size(), 1);
|
||||
EXPECT_EQ(clusters.results.front(), unusedCluster.getId());
|
||||
}
|
||||
@@ -280,7 +311,7 @@ TEST_F(DatabaseFixture, Cluster_singleTrackSingleReleaseSingleCluster)
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto clusters{ Cluster::findIds(session, Cluster::FindParameters{}.setRelease(release.getId())) };
|
||||
ASSERT_EQ(clusters.results.size(), 1);
|
||||
@@ -288,7 +319,7 @@ TEST_F(DatabaseFixture, Cluster_singleTrackSingleReleaseSingleCluster)
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto releases{ Release::findIds(session, Release::FindParameters {}.setClusters({cluster.getId()})) };
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
@@ -296,14 +327,14 @@ TEST_F(DatabaseFixture, Cluster_singleTrackSingleReleaseSingleCluster)
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto releases{ Release::findIds(session, Release::FindParameters {}.setClusters({unusedCluster.getId()})) };
|
||||
EXPECT_EQ(releases.results.size(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
EXPECT_EQ(Cluster::computeReleaseCount(session, cluster.getId()), 1);
|
||||
EXPECT_EQ(Cluster::computeTrackCount(session, cluster.getId()), 1);
|
||||
@@ -321,28 +352,28 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiClusters)
|
||||
ScopedCluster cluster2{ session, clusterType.lockAndGet(), "Cluster2" };
|
||||
ScopedCluster cluster3{ session, clusterType.lockAndGet(), "Cluster3" };
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
auto trackArtistLink{ TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist) };
|
||||
cluster1.get().modify()->addTrack(track.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
EXPECT_TRUE(ClusterType::findOrphans(session).results.empty());
|
||||
EXPECT_EQ(Cluster::findOrphans(session).results.size(), 2);
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_TRUE(ClusterType::findOrphanIds(session).results.empty());
|
||||
EXPECT_EQ(Cluster::findOrphanIds(session).results.size(), 2);
|
||||
EXPECT_TRUE(Release::findOrphanIds(session).results.empty());
|
||||
EXPECT_TRUE(Artist::findOrphanIds(session).results.empty());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(track->getClusters().size(), 1);
|
||||
EXPECT_EQ(track->getClusterIds().size(), 1);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
auto artists{ Artist::findIds(session, Artist::FindParameters {}.setClusters({cluster1.getId()})) };
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
@@ -355,7 +386,7 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiClusters)
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto artists{ Artist::findIds(session, Artist::FindParameters {}.setClusters({cluster1.getId()})) };
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
@@ -381,7 +412,7 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiRolesMultiClusters)
|
||||
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
|
||||
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::ReleaseArtist);
|
||||
@@ -389,14 +420,14 @@ TEST_F(DatabaseFixture, SingleTrackSingleArtistMultiRolesMultiClusters)
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
EXPECT_TRUE(Cluster::findOrphans(session).results.empty());
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_TRUE(Cluster::findOrphanIds(session).results.empty());
|
||||
EXPECT_TRUE(Release::findOrphanIds(session).results.empty());
|
||||
EXPECT_TRUE(Artist::findOrphanIds(session).results.empty());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto artists{ Artist::findIds(session, Artist::FindParameters {}.setClusters({cluster.getId()})) };
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
@@ -421,7 +452,7 @@ TEST_F(DatabaseFixture, MultiTracksSingleArtistMultiClusters)
|
||||
{
|
||||
tracks.emplace_back(session, "MyTrackFile" + std::to_string(i));
|
||||
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
TrackArtistLink::create(session, tracks.back().get(), artist.get(), TrackArtistLinkType::Artist);
|
||||
|
||||
for (auto& cluster : clusters)
|
||||
@@ -429,13 +460,13 @@ TEST_F(DatabaseFixture, MultiTracksSingleArtistMultiClusters)
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
EXPECT_TRUE(Cluster::findOrphans(session).results.empty());
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_TRUE(Cluster::findOrphanIds(session).results.empty());
|
||||
EXPECT_TRUE(Artist::findOrphanIds(session).results.empty());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
std::vector<ClusterId> clusterIds;
|
||||
std::transform(std::cbegin(clusters), std::cend(clusters), std::back_inserter(clusterIds), [](const ScopedCluster& cluster) { return cluster.getId(); });
|
||||
@@ -458,13 +489,13 @@ TEST_F(DatabaseFixture, MultipleTracksSingleClusterSimilarity)
|
||||
tracks.emplace_back(session, "MyTrack" + std::to_string(i));
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
cluster.get().modify()->addTrack(tracks.back().get());
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto similarTracks{ Track::findSimilarTrackIds(session, {tracks.front().getId()}) };
|
||||
EXPECT_EQ(similarTracks.results.size(), tracks.size() - 1);
|
||||
@@ -487,7 +518,7 @@ TEST_F(DatabaseFixture, MultipleTracksMultipleClustersSimilarity)
|
||||
tracks.emplace_back(session, "MyTrack" + std::to_string(i));
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
cluster1.get().modify()->addTrack(tracks.back().get());
|
||||
}
|
||||
}
|
||||
@@ -497,14 +528,14 @@ TEST_F(DatabaseFixture, MultipleTracksMultipleClustersSimilarity)
|
||||
tracks.emplace_back(session, "MyTrack" + std::to_string(i));
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
cluster1.get().modify()->addTrack(tracks.back().get());
|
||||
cluster2.get().modify()->addTrack(tracks.back().get());
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
{
|
||||
auto similarTracks{ Track::findSimilarTrackIds(session, {tracks.back().getId()}, Range {0, 4}) };
|
||||
@@ -531,7 +562,7 @@ TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtistSingleCluster)
|
||||
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
|
||||
track.get().modify()->setRelease(release.get());
|
||||
@@ -539,16 +570,16 @@ TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtistSingleCluster)
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
EXPECT_TRUE(Cluster::findOrphans(session).results.empty());
|
||||
EXPECT_TRUE(ClusterType::findOrphans(session).results.empty());
|
||||
EXPECT_TRUE(Cluster::findOrphanIds(session).results.empty());
|
||||
EXPECT_TRUE(ClusterType::findOrphanIds(session).results.empty());
|
||||
EXPECT_TRUE(Artist::findOrphanIds(session).results.empty());
|
||||
EXPECT_TRUE(Release::findOrphanIds(session).results.empty());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto artists{ Artist::findIds(session, Artist::FindParameters {}.setClusters({cluster.getId()})) };
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
@@ -574,7 +605,7 @@ TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtistMultiClusters)
|
||||
ScopedCluster cluster2{ session, clusterType.lockAndGet(), "MyCluster2" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
auto trackArtistLink{ TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist) };
|
||||
track.get().modify()->setRelease(release.get());
|
||||
@@ -583,7 +614,7 @@ TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtistMultiClusters)
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto releases{ Release::findIds(session, Release::FindParameters {}.setArtist(artist.getId())) };
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
@@ -607,7 +638,7 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackSingleCluster)
|
||||
{
|
||||
tracks.emplace_back(session, "MyTrack" + std::to_string(i));
|
||||
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
if (i < 5)
|
||||
session.create<TrackListEntry>(tracks.back().get(), trackList.get());
|
||||
@@ -617,7 +648,7 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackSingleCluster)
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto similarTracks{ trackList->getSimilarTracks() };
|
||||
EXPECT_EQ(similarTracks.size(), 5);
|
||||
@@ -640,7 +671,7 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClusters)
|
||||
{
|
||||
tracks.emplace_back(session, "MyTrack" + std::to_string(i));
|
||||
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
if (i < 5)
|
||||
session.create<TrackListEntry>(tracks.back().get(), trackList.get());
|
||||
@@ -657,7 +688,7 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrackMultiClusters)
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
{
|
||||
const auto similarTracks{ trackList->getSimilarTracks(0, 5) };
|
||||
@@ -689,7 +720,7 @@ TEST_F(DatabaseFixture, MultipleTracksMultipleArtistsMultiClusters)
|
||||
ScopedCluster cluster2{ session, clusterType.lockAndGet(), "MyCluster2" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_TRUE(artist1->findSimilarArtistIds().results.empty());
|
||||
EXPECT_TRUE(artist2->findSimilarArtistIds().results.empty());
|
||||
EXPECT_TRUE(artist3->findSimilarArtistIds().results.empty());
|
||||
@@ -700,7 +731,7 @@ TEST_F(DatabaseFixture, MultipleTracksMultipleArtistsMultiClusters)
|
||||
{
|
||||
tracks.emplace_back(session, "MyTrack" + std::to_string(i));
|
||||
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
if (i < 5)
|
||||
TrackArtistLink::create(session, tracks.back().get(), artist1.get(), TrackArtistLinkType::Artist);
|
||||
@@ -715,13 +746,13 @@ TEST_F(DatabaseFixture, MultipleTracksMultipleArtistsMultiClusters)
|
||||
|
||||
tracks.emplace_back(session, "MyTrack" + std::to_string(tracks.size()));
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
TrackArtistLink::create(session, tracks.back().get(), artist3.get(), TrackArtistLinkType::Artist);
|
||||
cluster2.get().modify()->addTrack(tracks.back().get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
{
|
||||
auto artists{ artist1->findSimilarArtistIds() };
|
||||
@@ -770,7 +801,7 @@ TEST_F(DatabaseFixture, MultipleTracksMultipleReleasesMultiClusters)
|
||||
ScopedCluster cluster2{ session, clusterType.lockAndGet(), "MyCluster2" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_TRUE(release1->getSimilarReleases().empty());
|
||||
EXPECT_TRUE(release2->getSimilarReleases().empty());
|
||||
EXPECT_TRUE(release3->getSimilarReleases().empty());
|
||||
@@ -781,7 +812,7 @@ TEST_F(DatabaseFixture, MultipleTracksMultipleReleasesMultiClusters)
|
||||
{
|
||||
tracks.emplace_back(session, "MyTrack" + std::to_string(i));
|
||||
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
if (i < 5)
|
||||
tracks.back().get().modify()->setRelease(release1.get());
|
||||
@@ -796,13 +827,13 @@ TEST_F(DatabaseFixture, MultipleTracksMultipleReleasesMultiClusters)
|
||||
|
||||
tracks.emplace_back(session, "MyTrack" + std::to_string(tracks.size()));
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
tracks.back().get().modify()->setRelease(release3.get());
|
||||
cluster2.get().modify()->addTrack(tracks.back().get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
{
|
||||
auto releases{ release1->getSimilarReleases() };
|
||||
|
||||
@@ -59,17 +59,6 @@ void DatabaseFixture::SetUpTestCase()
|
||||
Database::Session s{ _tmpDb->getDb() };
|
||||
s.prepareTables();
|
||||
s.analyze();
|
||||
|
||||
// remove default created entries
|
||||
{
|
||||
auto transaction{ s.createUniqueTransaction() };
|
||||
|
||||
for (const Database::ClusterTypeId clusterTypeId : Database::ClusterType::find(s).results)
|
||||
{
|
||||
auto clusterType{ Database::ClusterType::find(s, clusterTypeId) };
|
||||
clusterType.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +71,7 @@ void DatabaseFixture::testDatabaseEmpty()
|
||||
{
|
||||
using namespace Database;
|
||||
|
||||
auto uniqueTransaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
EXPECT_EQ(Artist::getCount(session), 0);
|
||||
EXPECT_EQ(Cluster::getCount(session), 0);
|
||||
|
||||
@@ -49,7 +49,7 @@ class ScopedEntity
|
||||
ScopedEntity(Database::Session& session, Args&& ...args)
|
||||
: _session {session}
|
||||
{
|
||||
auto transaction {_session.createUniqueTransaction()};
|
||||
auto transaction {_session.createWriteTransaction()};
|
||||
|
||||
auto entity {_session.create<T>(std::forward<Args>(args)...)};
|
||||
EXPECT_TRUE(entity);
|
||||
@@ -58,7 +58,7 @@ class ScopedEntity
|
||||
|
||||
~ScopedEntity()
|
||||
{
|
||||
auto transaction {_session.createUniqueTransaction()};
|
||||
auto transaction {_session.createWriteTransaction()};
|
||||
|
||||
auto entity {T::find(_session, _id)};
|
||||
// could not be here due to "on delete cascade" constraints...
|
||||
@@ -73,13 +73,13 @@ class ScopedEntity
|
||||
|
||||
typename T::pointer lockAndGet()
|
||||
{
|
||||
auto transaction {_session.createSharedTransaction()};
|
||||
auto transaction {_session.createReadTransaction()};
|
||||
return get();
|
||||
}
|
||||
|
||||
typename T::pointer get()
|
||||
{
|
||||
_session.checkSharedLocked();
|
||||
_session.checkReadTransaction();
|
||||
|
||||
auto entity {T::find(_session, _id)};
|
||||
EXPECT_TRUE(entity);
|
||||
|
||||
@@ -34,20 +34,20 @@ TEST_F(DatabaseFixture, MultiTracksSingleArtistSingleRelease)
|
||||
{
|
||||
tracks.emplace_back(session, "MyTrack" + std::to_string(i));
|
||||
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
TrackArtistLink::create(session, tracks.back().get(), artist.get(), TrackArtistLinkType::Artist);
|
||||
tracks.back().get().modify()->setRelease(release.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_TRUE(Release::findOrphanIds(session).results.empty());
|
||||
EXPECT_TRUE(Artist::findOrphanIds(session).results.empty());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto releases{ Release::findIds(session, Release::FindParameters {}.setArtist(artist.getId())) };
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
@@ -65,14 +65,14 @@ TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtist)
|
||||
ScopedArtist artist{ session, "MyArtist" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
auto trackArtistLink{ TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist) };
|
||||
track.get().modify()->setRelease(release.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
auto releases{ Release::findIds(session, Release::FindParameters {}.setArtist(artist.getId())) };
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
@@ -87,7 +87,7 @@ TEST_F(DatabaseFixture, SingleTrackSingleReleaseSingleArtist)
|
||||
TEST_F(DatabaseFixture, SingleUser)
|
||||
{
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_TRUE(User::find(session, User::FindParameters{}).results.empty());
|
||||
EXPECT_EQ(User::getCount(session), 0);
|
||||
}
|
||||
@@ -95,7 +95,7 @@ TEST_F(DatabaseFixture, SingleUser)
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
EXPECT_EQ(User::find(session, User::FindParameters{}).results.size(), 1);
|
||||
EXPECT_EQ(User::getCount(session), 1);
|
||||
|
||||
@@ -30,7 +30,7 @@ TEST_F(DatabaseFixture, Listen_getAll)
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
EXPECT_EQ(Listen::getCount(session), 0);
|
||||
}
|
||||
@@ -38,20 +38,20 @@ TEST_F(DatabaseFixture, Listen_getAll)
|
||||
ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12, 0, 1}} };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
EXPECT_EQ(Listen::getCount(session), 1);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
ScopedListen listen2{ session, user.get(), track.get(), ScrobblingBackend::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{13, 0, 1}} };
|
||||
|
||||
EXPECT_EQ(Listen::getCount(session), 2);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
EXPECT_EQ(Listen::getCount(session), 1);
|
||||
}
|
||||
@@ -64,14 +64,14 @@ TEST_F(DatabaseFixture, Listen_get)
|
||||
ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12, 0, 1}} };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto listens{ Listen::find(session, Listen::FindParameters{}.setUser(user->getId()).setScrobblingBackend(ScrobblingBackend::ListenBrainz)) };
|
||||
EXPECT_EQ(listens.results.size(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
{
|
||||
auto listens{ Listen::find(session, Listen::FindParameters{}.setUser(user->getId()).setScrobblingBackend(ScrobblingBackend::Internal)) };
|
||||
@@ -100,7 +100,7 @@ TEST_F(DatabaseFixture, Listen_get_multi)
|
||||
ScopedListen listen2{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, Wt::WDateTime {Wt::WDate{2000, 1, 2}, Wt::WTime{12, 0, 2}} };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto listens{ Listen::find(session, Listen::FindParameters{}.setUser(user->getId()).setScrobblingBackend(ScrobblingBackend::Internal)) };
|
||||
ASSERT_EQ(listens.results.size(), 3);
|
||||
@@ -122,7 +122,7 @@ TEST_F(DatabaseFixture, Listen_get_byDateTime)
|
||||
ScopedListen listen2{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime2 };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
{
|
||||
Listen::pointer listen{ Listen::find(session, user.getId(), track.getId(), ScrobblingBackend::Internal, dateTime1) };
|
||||
@@ -151,7 +151,7 @@ TEST_F(DatabaseFixture, Listen_getTopArtists)
|
||||
ScopedListen listen1{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime1 };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto artists{ Listen::getTopArtists(session, user->getId(), ScrobblingBackend::Internal, {}, std::nullopt) };
|
||||
EXPECT_EQ(artists.results.size(), 0);
|
||||
@@ -163,26 +163,26 @@ TEST_F(DatabaseFixture, Listen_getTopArtists)
|
||||
ScopedListen listen2{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime1.addSecs(1) };
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
TrackArtistLink::create(session, track2.get(), artist1.get(), TrackArtistLinkType::Artist);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto artists{ Listen::getTopArtists(session, user->getId(), ScrobblingBackend::Internal, {}, std::nullopt) };
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
EXPECT_EQ(artists.results[0], artist1->getId());
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto artists{ Listen::getTopArtists(session, user->getId(), ScrobblingBackend::ListenBrainz, {}, std::nullopt) };
|
||||
ASSERT_EQ(artists.results.size(), 0);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto artists{ Listen::getTopArtists(session, user->getId(), ScrobblingBackend::Internal, {}, TrackArtistLinkType::Producer) };
|
||||
EXPECT_EQ(artists.results.size(), 0);
|
||||
@@ -192,7 +192,7 @@ TEST_F(DatabaseFixture, Listen_getTopArtists)
|
||||
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto artists{ Listen::getTopArtists(session, user->getId(), ScrobblingBackend::Internal, {cluster->getId()}, std::nullopt) };
|
||||
EXPECT_EQ(artists.results.size(), 0);
|
||||
@@ -210,13 +210,13 @@ TEST_F(DatabaseFixture, Listen_getTopArtists_multi)
|
||||
const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} };
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
TrackArtistLink::create(session, track1.get(), artist1.get(), TrackArtistLinkType::Artist);
|
||||
TrackArtistLink::create(session, track2.get(), artist2.get(), TrackArtistLinkType::Artist);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto artists{ Listen::getTopArtists(session, user->getId(), ScrobblingBackend::Internal, {}, std::nullopt) };
|
||||
EXPECT_EQ(artists.results.size(), 0);
|
||||
@@ -224,7 +224,7 @@ TEST_F(DatabaseFixture, Listen_getTopArtists_multi)
|
||||
|
||||
ScopedListen listen1{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(1) };
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto artists{ Listen::getTopArtists(session, user->getId(), ScrobblingBackend::Internal, {}, std::nullopt) };
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
@@ -233,7 +233,7 @@ TEST_F(DatabaseFixture, Listen_getTopArtists_multi)
|
||||
ScopedListen listen2{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(2) };
|
||||
ScopedListen listen3{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(3) };
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto artists{ Listen::getTopArtists(session, user->getId(), ScrobblingBackend::Internal, {}, std::nullopt) };
|
||||
ASSERT_EQ(artists.results.size(), 2);
|
||||
@@ -241,7 +241,7 @@ TEST_F(DatabaseFixture, Listen_getTopArtists_multi)
|
||||
EXPECT_EQ(artists.results[1], artist1->getId());
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto artists{ Listen::getTopArtists(session, user->getId(), ScrobblingBackend::Internal, {}, std::nullopt, Range {0, 1}) };
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
@@ -261,22 +261,22 @@ TEST_F(DatabaseFixture, Listen_getTopArtists_cluster)
|
||||
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto artists{ Listen::getTopArtists(session, user->getId(), ScrobblingBackend::Internal, {cluster.getId()}, std::nullopt) };
|
||||
ASSERT_EQ(artists.results.size(), 0);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
cluster.get().modify()->addTrack(track.get());
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto artists{ Listen::getTopArtists(session, user->getId(), ScrobblingBackend::Internal, {cluster.getId()}, std::nullopt) };
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
@@ -291,12 +291,12 @@ TEST_F(DatabaseFixture, Listen_getTopReleases)
|
||||
const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} };
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track.get().modify()->setRelease(release.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto releases{ Listen::getTopReleases(session, user->getId(), ScrobblingBackend::Internal, {}) };
|
||||
EXPECT_EQ(releases.moreResults, false);
|
||||
@@ -306,7 +306,7 @@ TEST_F(DatabaseFixture, Listen_getTopReleases)
|
||||
ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto releases{ Listen::getTopReleases(session, user->getId(), ScrobblingBackend::Internal, {}) };
|
||||
EXPECT_EQ(releases.moreResults, false);
|
||||
@@ -314,7 +314,7 @@ TEST_F(DatabaseFixture, Listen_getTopReleases)
|
||||
EXPECT_EQ(releases.results[0], release.getId());
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto releases{ Listen::getTopReleases(session, user->getId(), ScrobblingBackend::ListenBrainz, {}) };
|
||||
EXPECT_EQ(releases.moreResults, false);
|
||||
@@ -333,13 +333,13 @@ TEST_F(DatabaseFixture, Listen_getTopReleases_multi)
|
||||
ScopedRelease release2{ session, "MyRelease2" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track1.get().modify()->setRelease(release1.get());
|
||||
track2.get().modify()->setRelease(release2.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto releases{ Listen::getTopReleases(session, user->getId(), ScrobblingBackend::Internal, {}) };
|
||||
EXPECT_EQ(releases.moreResults, false);
|
||||
@@ -349,7 +349,7 @@ TEST_F(DatabaseFixture, Listen_getTopReleases_multi)
|
||||
ScopedListen listen2{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime };
|
||||
ScopedListen listen3{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime };
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto releases{ Listen::getTopReleases(session, user->getId(), ScrobblingBackend::Internal, {}) };
|
||||
EXPECT_EQ(releases.moreResults, false);
|
||||
@@ -360,7 +360,7 @@ TEST_F(DatabaseFixture, Listen_getTopReleases_multi)
|
||||
ScopedListen listen4{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime };
|
||||
ScopedListen listen5{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime };
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto releases{ Listen::getTopReleases(session, user->getId(), ScrobblingBackend::Internal, {}) };
|
||||
EXPECT_EQ(releases.moreResults, false);
|
||||
@@ -381,23 +381,23 @@ TEST_F(DatabaseFixture, Listen_getTopReleases_cluster)
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track.get().modify()->setRelease(release.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto releases{ Listen::getTopReleases(session, user->getId(), ScrobblingBackend::Internal, {cluster.getId()}) };
|
||||
EXPECT_EQ(releases.results.size(), 0);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
cluster.get().modify()->addTrack(track.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto releases{ Listen::getTopReleases(session, user->getId(), ScrobblingBackend::Internal, {cluster.getId()}) };
|
||||
EXPECT_EQ(releases.results.size(), 1);
|
||||
@@ -412,7 +412,7 @@ TEST_F(DatabaseFixture, Listen_getTopTracks)
|
||||
const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto tracks{ Listen::getTopTracks(session, user->getId(), ScrobblingBackend::Internal, {}) };
|
||||
EXPECT_EQ(tracks.moreResults, false);
|
||||
@@ -422,7 +422,7 @@ TEST_F(DatabaseFixture, Listen_getTopTracks)
|
||||
ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto tracks{ Listen::getTopTracks(session, user->getId(), ScrobblingBackend::Internal, {}) };
|
||||
EXPECT_EQ(tracks.moreResults, false);
|
||||
@@ -430,7 +430,7 @@ TEST_F(DatabaseFixture, Listen_getTopTracks)
|
||||
EXPECT_EQ(tracks.results[0], track.getId());
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto tracks{ Listen::getTopTracks(session, user->getId(), ScrobblingBackend::ListenBrainz, {}) };
|
||||
EXPECT_EQ(tracks.moreResults, false);
|
||||
@@ -447,7 +447,7 @@ TEST_F(DatabaseFixture, Listen_getTopTrack_multi)
|
||||
ScopedListen listen1{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto tracks{ Listen::getTopTracks(session, user->getId(), ScrobblingBackend::Internal, {}) };
|
||||
EXPECT_EQ(tracks.moreResults, false);
|
||||
@@ -457,7 +457,7 @@ TEST_F(DatabaseFixture, Listen_getTopTrack_multi)
|
||||
ScopedListen listen2{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime };
|
||||
ScopedListen listen3{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime };
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto tracks{ Listen::getTopTracks(session, user->getId(), ScrobblingBackend::Internal, {}) };
|
||||
EXPECT_EQ(tracks.moreResults, false);
|
||||
@@ -468,7 +468,7 @@ TEST_F(DatabaseFixture, Listen_getTopTrack_multi)
|
||||
ScopedListen listen4{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime };
|
||||
ScopedListen listen5{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime };
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto tracks{ Listen::getTopTracks(session, user->getId(), ScrobblingBackend::Internal, {}) };
|
||||
EXPECT_EQ(tracks.moreResults, false);
|
||||
@@ -488,18 +488,18 @@ TEST_F(DatabaseFixture, Listen_getTopTracks_cluster)
|
||||
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto tracks{ Listen::getTopTracks(session, user->getId(), ScrobblingBackend::Internal, {cluster.getId()}) };
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
cluster.get().modify()->addTrack(track.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto tracks{ Listen::getTopTracks(session, user->getId(), ScrobblingBackend::Internal, {cluster.getId()}) };
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
@@ -514,12 +514,12 @@ TEST_F(DatabaseFixture, Listen_getRecentArtists)
|
||||
ScopedArtist artist{ session, "MyArtist" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto artists{ Listen::getRecentArtists(session, user->getId(), ScrobblingBackend::Internal, {}, std::nullopt) };
|
||||
EXPECT_EQ(artists.results.size(), 0);
|
||||
@@ -530,20 +530,20 @@ TEST_F(DatabaseFixture, Listen_getRecentArtists)
|
||||
ScopedListen listen1{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto artists{ Listen::getRecentArtists(session, user->getId(), ScrobblingBackend::Internal, {}, std::nullopt) };
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
EXPECT_EQ(artists.results[0], artist->getId());
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto artists{ Listen::getRecentArtists(session, user->getId(), ScrobblingBackend::ListenBrainz, {}, std::nullopt) };
|
||||
ASSERT_EQ(artists.results.size(), 0);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto artists{ Listen::getRecentArtists(session, user->getId(), ScrobblingBackend::Internal, {}, TrackArtistLinkType::Producer) };
|
||||
EXPECT_EQ(artists.results.size(), 0);
|
||||
@@ -553,7 +553,7 @@ TEST_F(DatabaseFixture, Listen_getRecentArtists)
|
||||
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto artists{ Listen::getRecentArtists(session, user->getId(), ScrobblingBackend::Internal, {cluster->getId()}, std::nullopt) };
|
||||
EXPECT_EQ(artists.results.size(), 0);
|
||||
@@ -571,13 +571,13 @@ TEST_F(DatabaseFixture, Listen_getRecentArtists_multi)
|
||||
const Wt::WDateTime dateTime{ Wt::WDate{2000, 1, 2}, Wt::WTime{12,0, 1} };
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
TrackArtistLink::create(session, track1.get(), artist1.get(), TrackArtistLinkType::Artist);
|
||||
TrackArtistLink::create(session, track2.get(), artist2.get(), TrackArtistLinkType::Artist);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto artists{ Listen::getRecentArtists(session, user->getId(), ScrobblingBackend::Internal, {}, std::nullopt) };
|
||||
EXPECT_EQ(artists.results.size(), 0);
|
||||
@@ -585,7 +585,7 @@ TEST_F(DatabaseFixture, Listen_getRecentArtists_multi)
|
||||
|
||||
ScopedListen listen1{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(1) };
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto artists{ Listen::getRecentArtists(session, user->getId(), ScrobblingBackend::Internal, {}, std::nullopt) };
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
@@ -593,7 +593,7 @@ TEST_F(DatabaseFixture, Listen_getRecentArtists_multi)
|
||||
}
|
||||
ScopedListen listen2{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(2) };
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto artists{ Listen::getRecentArtists(session, user->getId(), ScrobblingBackend::Internal, {}, std::nullopt) };
|
||||
ASSERT_EQ(artists.results.size(), 2);
|
||||
@@ -602,7 +602,7 @@ TEST_F(DatabaseFixture, Listen_getRecentArtists_multi)
|
||||
}
|
||||
ScopedListen listen3{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(-1) };
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto artists{ Listen::getRecentArtists(session, user->getId(), ScrobblingBackend::Internal, {}, std::nullopt, Range {0, 1}) };
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
@@ -622,22 +622,22 @@ TEST_F(DatabaseFixture, Listen_getRecentArtists_cluster)
|
||||
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto artists{ Listen::getRecentArtists(session, user->getId(), ScrobblingBackend::Internal, {cluster.getId()}, std::nullopt) };
|
||||
ASSERT_EQ(artists.results.size(), 0);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
cluster.get().modify()->addTrack(track.get());
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto artists{ Listen::getRecentArtists(session, user->getId(), ScrobblingBackend::Internal, {cluster.getId()}, std::nullopt) };
|
||||
ASSERT_EQ(artists.results.size(), 1);
|
||||
@@ -652,12 +652,12 @@ TEST_F(DatabaseFixture, Listen_getRecentReleases)
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track.get().modify()->setRelease(release.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto releases{ Listen::getRecentReleases(session, user->getId(), ScrobblingBackend::Internal, {}) };
|
||||
EXPECT_EQ(releases.moreResults, false);
|
||||
@@ -668,7 +668,7 @@ TEST_F(DatabaseFixture, Listen_getRecentReleases)
|
||||
ScopedListen listen1{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto releases{ Listen::getRecentReleases(session, user->getId(), ScrobblingBackend::Internal, {}) };
|
||||
EXPECT_EQ(releases.moreResults, false);
|
||||
@@ -676,7 +676,7 @@ TEST_F(DatabaseFixture, Listen_getRecentReleases)
|
||||
EXPECT_EQ(releases.results[0], release.getId());
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto releases{ Listen::getRecentReleases(session, user->getId(), ScrobblingBackend::ListenBrainz, {}) };
|
||||
EXPECT_EQ(releases.moreResults, false);
|
||||
@@ -692,12 +692,12 @@ TEST_F(DatabaseFixture, Listen_getMostRecentRelease)
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track.get().modify()->setRelease(release.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto listen{ Listen::getMostRecentListen(session, user->getId(), ScrobblingBackend::Internal, release.getId()) };
|
||||
EXPECT_FALSE(listen);
|
||||
@@ -707,7 +707,7 @@ TEST_F(DatabaseFixture, Listen_getMostRecentRelease)
|
||||
ScopedListen listen1{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime1 };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto listen{ Listen::getMostRecentListen(session, user->getId(), ScrobblingBackend::Internal, release.getId()) };
|
||||
EXPECT_TRUE(listen);
|
||||
@@ -718,7 +718,7 @@ TEST_F(DatabaseFixture, Listen_getMostRecentRelease)
|
||||
ScopedListen listen2{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime2 };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto listen{ Listen::getMostRecentListen(session, user->getId(), ScrobblingBackend::Internal, release.getId()) };
|
||||
EXPECT_TRUE(listen);
|
||||
@@ -729,7 +729,7 @@ TEST_F(DatabaseFixture, Listen_getMostRecentRelease)
|
||||
ScopedListen listen3{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime3 };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto listen{ Listen::getMostRecentListen(session, user->getId(), ScrobblingBackend::Internal, release.getId()) };
|
||||
EXPECT_TRUE(listen);
|
||||
@@ -746,7 +746,7 @@ TEST_F(DatabaseFixture, Listen_getRecentReleases_multi)
|
||||
ScopedRelease release2{ session, "MyRelease2" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track1.get().modify()->setRelease(release1.get());
|
||||
track2.get().modify()->setRelease(release2.get());
|
||||
}
|
||||
@@ -755,7 +755,7 @@ TEST_F(DatabaseFixture, Listen_getRecentReleases_multi)
|
||||
ScopedListen listen1{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto releases{ Listen::getRecentReleases(session, user->getId(), ScrobblingBackend::Internal, {}) };
|
||||
EXPECT_EQ(releases.moreResults, false);
|
||||
@@ -765,7 +765,7 @@ TEST_F(DatabaseFixture, Listen_getRecentReleases_multi)
|
||||
|
||||
ScopedListen listen2{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(1) };
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto releases{ Listen::getRecentReleases(session, user->getId(), ScrobblingBackend::Internal, {}) };
|
||||
EXPECT_EQ(releases.moreResults, false);
|
||||
@@ -776,7 +776,7 @@ TEST_F(DatabaseFixture, Listen_getRecentReleases_multi)
|
||||
|
||||
ScopedListen listen3{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(2) };
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto releases{ Listen::getRecentReleases(session, user->getId(), ScrobblingBackend::Internal, {}) };
|
||||
EXPECT_EQ(releases.moreResults, false);
|
||||
@@ -787,7 +787,7 @@ TEST_F(DatabaseFixture, Listen_getRecentReleases_multi)
|
||||
|
||||
ScopedListen listen4{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(-1) };
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto releases{ Listen::getRecentReleases(session, user->getId(), ScrobblingBackend::Internal, {}) };
|
||||
EXPECT_EQ(releases.moreResults, false);
|
||||
@@ -806,11 +806,11 @@ TEST_F(DatabaseFixture, Listen_getRecentReleases_cluster)
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track.get().modify()->setRelease(release.get());
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto releases{ Listen::getRecentReleases(session, user->getId(), ScrobblingBackend::Internal, {cluster.getId()}) };
|
||||
EXPECT_EQ(releases.results.size(), 0);
|
||||
@@ -820,18 +820,18 @@ TEST_F(DatabaseFixture, Listen_getRecentReleases_cluster)
|
||||
ScopedListen listen{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto releases{ Listen::getRecentReleases(session, user->getId(), ScrobblingBackend::Internal, {cluster.getId()}) };
|
||||
EXPECT_EQ(releases.results.size(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
cluster.get().modify()->addTrack(track.get());
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto releases{ Listen::getRecentReleases(session, user->getId(), ScrobblingBackend::Internal, {cluster.getId()}) };
|
||||
EXPECT_EQ(releases.results.size(), 1);
|
||||
@@ -845,7 +845,7 @@ TEST_F(DatabaseFixture, Listen_getRecentTracks)
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto tracks{ Listen::getRecentTracks(session, user->getId(), ScrobblingBackend::Internal, {}) };
|
||||
EXPECT_EQ(tracks.moreResults, false);
|
||||
@@ -856,7 +856,7 @@ TEST_F(DatabaseFixture, Listen_getRecentTracks)
|
||||
ScopedListen listen1{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto tracks{ Listen::getRecentTracks(session, user->getId(), ScrobblingBackend::Internal, {}) };
|
||||
EXPECT_EQ(tracks.moreResults, false);
|
||||
@@ -865,7 +865,7 @@ TEST_F(DatabaseFixture, Listen_getRecentTracks)
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto tracks{ Listen::getRecentTracks(session, user->getId(), ScrobblingBackend::ListenBrainz, {}) };
|
||||
EXPECT_EQ(tracks.moreResults, false);
|
||||
@@ -879,7 +879,7 @@ TEST_F(DatabaseFixture, Listen_getCount_track)
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const std::size_t count{ Listen::getCount(session, user->getId(), ScrobblingBackend::Internal, track.getId()) };
|
||||
EXPECT_EQ(count, 0);
|
||||
@@ -889,7 +889,7 @@ TEST_F(DatabaseFixture, Listen_getCount_track)
|
||||
ScopedListen listen1{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime1 };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const std::size_t count{ Listen::getCount(session, user->getId(), ScrobblingBackend::Internal, track.getId()) };
|
||||
EXPECT_EQ(count, 1);
|
||||
@@ -905,7 +905,7 @@ TEST_F(DatabaseFixture, Listen_getCount_release)
|
||||
|
||||
auto getReleaseListenCount{ [&]
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
return Listen::getCount(session, user->getId(), ScrobblingBackend::Internal, release.getId());
|
||||
} };
|
||||
|
||||
@@ -917,14 +917,14 @@ TEST_F(DatabaseFixture, Listen_getCount_release)
|
||||
EXPECT_EQ(getReleaseListenCount(), 0);
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track1.get().modify()->setRelease(release.get());
|
||||
}
|
||||
|
||||
EXPECT_EQ(getReleaseListenCount(), 1);
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track2.get().modify()->setRelease(release.get());
|
||||
}
|
||||
|
||||
@@ -944,7 +944,7 @@ TEST_F(DatabaseFixture, Listen_getMostRecentTrack)
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto listen{ Listen::getMostRecentListen(session, user->getId(), ScrobblingBackend::Internal, track.getId()) };
|
||||
EXPECT_FALSE(listen);
|
||||
@@ -954,7 +954,7 @@ TEST_F(DatabaseFixture, Listen_getMostRecentTrack)
|
||||
ScopedListen listen1{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime1 };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto listen{ Listen::getMostRecentListen(session, user->getId(), ScrobblingBackend::Internal, track.getId()) };
|
||||
EXPECT_TRUE(listen);
|
||||
@@ -965,7 +965,7 @@ TEST_F(DatabaseFixture, Listen_getMostRecentTrack)
|
||||
ScopedListen listen2{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime2 };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto listen{ Listen::getMostRecentListen(session, user->getId(), ScrobblingBackend::Internal, track.getId()) };
|
||||
EXPECT_TRUE(listen);
|
||||
@@ -976,7 +976,7 @@ TEST_F(DatabaseFixture, Listen_getMostRecentTrack)
|
||||
ScopedListen listen3{ session, user.lockAndGet(), track.lockAndGet(), ScrobblingBackend::Internal, dateTime3 };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto listen{ Listen::getMostRecentListen(session, user->getId(), ScrobblingBackend::Internal, track.getId()) };
|
||||
EXPECT_TRUE(listen);
|
||||
@@ -994,7 +994,7 @@ TEST_F(DatabaseFixture, Listen_getRecentTracks_multi)
|
||||
ScopedListen listen1{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto tracks{ Listen::getRecentTracks(session, user->getId(), ScrobblingBackend::Internal, {}) };
|
||||
EXPECT_EQ(tracks.moreResults, false);
|
||||
@@ -1004,7 +1004,7 @@ TEST_F(DatabaseFixture, Listen_getRecentTracks_multi)
|
||||
|
||||
ScopedListen listen2{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(1) };
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto tracks{ Listen::getRecentTracks(session, user->getId(), ScrobblingBackend::Internal, {}) };
|
||||
EXPECT_EQ(tracks.moreResults, false);
|
||||
@@ -1015,7 +1015,7 @@ TEST_F(DatabaseFixture, Listen_getRecentTracks_multi)
|
||||
|
||||
ScopedListen listen3{ session, user.lockAndGet(), track2.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(2) };
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto tracks{ Listen::getRecentTracks(session, user->getId(), ScrobblingBackend::Internal, {}) };
|
||||
EXPECT_EQ(tracks.moreResults, false);
|
||||
@@ -1026,7 +1026,7 @@ TEST_F(DatabaseFixture, Listen_getRecentTracks_multi)
|
||||
|
||||
ScopedListen listen4{ session, user.lockAndGet(), track1.lockAndGet(), ScrobblingBackend::Internal, dateTime.addSecs(-1) };
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto tracks{ Listen::getRecentTracks(session, user->getId(), ScrobblingBackend::Internal, {}) };
|
||||
EXPECT_EQ(tracks.moreResults, false);
|
||||
@@ -1046,18 +1046,18 @@ TEST_F(DatabaseFixture, Listen_getRecentTracks_cluster)
|
||||
ScopedCluster cluster{ session, clusterType.lockAndGet(), "MyCluster" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto tracks{ Listen::getRecentTracks(session, user->getId(), ScrobblingBackend::Internal, {cluster.getId()}) };
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
cluster.get().modify()->addTrack(track.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto tracks{ Listen::getRecentTracks(session, user->getId(), ScrobblingBackend::Internal, {cluster.getId()}) };
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
|
||||
@@ -24,9 +24,10 @@ using namespace Database;
|
||||
TEST_F(DatabaseFixture, Release)
|
||||
{
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
EXPECT_EQ(Release::getCount(session), 0);
|
||||
EXPECT_EQ(Release::getCount(session, Release::FindParameters{}), 0);
|
||||
EXPECT_FALSE(Release::exists(session, 0));
|
||||
EXPECT_FALSE(Release::exists(session, 1));
|
||||
}
|
||||
@@ -34,9 +35,10 @@ TEST_F(DatabaseFixture, Release)
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
EXPECT_EQ(Release::getCount(session), 1);
|
||||
EXPECT_EQ(Release::getCount(session, Release::FindParameters{}), 1);
|
||||
EXPECT_TRUE(Release::exists(session, release.getId()));
|
||||
|
||||
{
|
||||
@@ -77,7 +79,7 @@ TEST_F(DatabaseFixture, Release_singleTrack)
|
||||
{
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
track.get().modify()->setRelease(release.get());
|
||||
track.get().modify()->setName("MyTrackName");
|
||||
@@ -85,7 +87,7 @@ TEST_F(DatabaseFixture, Release_singleTrack)
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_TRUE(Release::findOrphanIds(session).results.empty());
|
||||
|
||||
const auto tracks{ Track::findIds(session, Track::FindParameters {}.setRelease(release.getId())) };
|
||||
@@ -94,32 +96,32 @@ TEST_F(DatabaseFixture, Release_singleTrack)
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
ASSERT_TRUE(track->getRelease());
|
||||
EXPECT_EQ(track->getRelease()->getId(), release.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackName").setReleaseName("MyReleaseName")) };
|
||||
ASSERT_EQ(tracks.results.size(), 1);
|
||||
EXPECT_EQ(tracks.results.front(), track.getId());
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackName").setReleaseName("MyReleaseFoo")) };
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
auto tracks{ Track::findIds(session, Track::FindParameters{}.setName("MyTrackFoo").setReleaseName("MyReleaseName")) };
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
const auto tracks{ Track::findIds(session, Track::FindParameters {}.setRelease(release.getId())) };
|
||||
EXPECT_TRUE(tracks.results.empty());
|
||||
@@ -130,6 +132,39 @@ TEST_F(DatabaseFixture, Release_singleTrack)
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Release_findByNameAndPath)
|
||||
{
|
||||
ScopedRelease release1{ session, "MyRelease" };
|
||||
ScopedRelease release2{ session, "MyRelease" };
|
||||
ScopedTrack track1{ session, "MyTrack" };
|
||||
ScopedTrack track2{ session, "MyTrack" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
track1.get().modify()->setRelease(release1.get());
|
||||
track1.get().modify()->setPath("/tmp/foo/foo.mp3");
|
||||
|
||||
track2.get().modify()->setRelease(release2.get());
|
||||
track2.get().modify()->setPath("/tmp/bar/bar.mp3");
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
{
|
||||
const auto releases{ Release::find(session, "MyRelease", "/tmp/foo") };
|
||||
ASSERT_EQ(releases.size(), 1);
|
||||
EXPECT_EQ(releases.front()->getId(), release1.getId());
|
||||
}
|
||||
|
||||
{
|
||||
const auto releases{ Release::find(session, "MyRelease", "/tmp/bar") };
|
||||
ASSERT_EQ(releases.size(), 1);
|
||||
EXPECT_EQ(releases.front()->getId(), release2.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, MulitpleReleaseSearchByName)
|
||||
{
|
||||
ScopedRelease release1{ session, "MyRelease" };
|
||||
@@ -148,7 +183,7 @@ TEST_F(DatabaseFixture, MulitpleReleaseSearchByName)
|
||||
ScopedTrack track6{ session, "MyTrack" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
track1.get().modify()->setRelease(release1.get());
|
||||
track2.get().modify()->setRelease(release2.get());
|
||||
@@ -159,7 +194,7 @@ TEST_F(DatabaseFixture, MulitpleReleaseSearchByName)
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
{
|
||||
const auto releases{ Release::findIds(session, Release::FindParameters {}.setKeywords({"Release"})) };
|
||||
@@ -201,33 +236,33 @@ TEST_F(DatabaseFixture, MultiTracksSingleReleaseTotalDiscTrack)
|
||||
ScopedRelease release1{ session, "MyRelease" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
EXPECT_FALSE(release1->getTotalDisc());
|
||||
}
|
||||
|
||||
ScopedTrack track1{ session, "MyTrack" };
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
track1.get().modify()->setRelease(release1.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
EXPECT_FALSE(release1->getTotalDisc());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
track1.get().modify()->setTotalTrack(36);
|
||||
release1.get().modify()->setTotalDisc(6);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
ASSERT_TRUE(track1->getTotalTrack());
|
||||
EXPECT_EQ(*track1->getTotalTrack(), 36);
|
||||
@@ -237,7 +272,7 @@ TEST_F(DatabaseFixture, MultiTracksSingleReleaseTotalDiscTrack)
|
||||
|
||||
ScopedTrack track2{ session, "MyTrack2" };
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
track2.get().modify()->setRelease(release1.get());
|
||||
track2.get().modify()->setTotalTrack(37);
|
||||
@@ -245,7 +280,7 @@ TEST_F(DatabaseFixture, MultiTracksSingleReleaseTotalDiscTrack)
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
ASSERT_TRUE(track1->getTotalTrack());
|
||||
EXPECT_EQ(*track1->getTotalTrack(), 36);
|
||||
@@ -255,21 +290,21 @@ TEST_F(DatabaseFixture, MultiTracksSingleReleaseTotalDiscTrack)
|
||||
|
||||
ScopedRelease release2{ session, "MyRelease2" };
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
EXPECT_FALSE(release2->getTotalDisc());
|
||||
}
|
||||
|
||||
ScopedTrack track3{ session, "MyTrack3" };
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
track3.get().modify()->setRelease(release2.get());
|
||||
track3.get().modify()->setTotalTrack(7);
|
||||
release2.get().modify()->setTotalDisc(5);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
ASSERT_TRUE(track1->getTotalTrack());
|
||||
EXPECT_EQ(*track1->getTotalTrack(), 36);
|
||||
@@ -293,14 +328,14 @@ TEST_F(DatabaseFixture, MultiTracksSingleReleaseFirstTrack)
|
||||
ScopedTrack track2B{ session, "MyTrack2B" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
EXPECT_TRUE(Track::findIds(session, Track::FindParameters{}.setRelease(release1.getId())).results.empty());
|
||||
EXPECT_TRUE(Track::findIds(session, Track::FindParameters{}.setRelease(release2.getId())).results.empty());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
track1A.get().modify()->setRelease(release1.get());
|
||||
track1B.get().modify()->setRelease(release1.get());
|
||||
@@ -317,7 +352,7 @@ TEST_F(DatabaseFixture, MultiTracksSingleReleaseFirstTrack)
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
{
|
||||
const auto tracks{ Track::findIds(session, Track::FindParameters {}.setRelease(release1.getId()).setSortMethod(TrackSortMethod::Release)) };
|
||||
@@ -346,14 +381,14 @@ TEST_F(DatabaseFixture, MultiTracksSingleReleaseDate)
|
||||
ScopedTrack track2B{ session, "MyTrack2B" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto releases{ Release::findIds(session, Release::FindParameters {}.setDateRange(DateRange::fromYearRange(0, 3000))) };
|
||||
EXPECT_EQ(releases.results.size(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
track1A.get().modify()->setRelease(release1.get());
|
||||
track1B.get().modify()->setRelease(release1.get());
|
||||
@@ -371,7 +406,7 @@ TEST_F(DatabaseFixture, MultiTracksSingleReleaseDate)
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto releases{ Release::findIds(session, Release::FindParameters {}.setDateRange(DateRange::fromYearRange(1950, 2000))) };
|
||||
ASSERT_EQ(releases.results.size(), 1);
|
||||
@@ -394,25 +429,25 @@ TEST_F(DatabaseFixture, Release_writtenAfter)
|
||||
const Wt::WDateTime dateTime{ Wt::WDate {1950, 1, 1}, Wt::WTime {12, 30, 20} };
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track.get().modify()->setLastWriteTime(dateTime);
|
||||
track.get().modify()->setRelease(release.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
const auto releases{ Release::findIds(session, Release::FindParameters {}) };
|
||||
EXPECT_EQ(releases.results.size(), 1);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
const auto releases{ Release::findIds(session, Release::FindParameters {}.setWrittenAfter(dateTime.addSecs(-1))) };
|
||||
EXPECT_EQ(releases.results.size(), 1);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
const auto releases{ Release::findIds(session, Release::FindParameters {}.setWrittenAfter(dateTime.addSecs(+1))) };
|
||||
EXPECT_EQ(releases.results.size(), 0);
|
||||
}
|
||||
@@ -425,12 +460,12 @@ TEST_F(DatabaseFixture, Release_artist)
|
||||
ScopedArtist artist{ session, "MyArtist" };
|
||||
ScopedArtist artist2{ session, "MyArtist2" };
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track.get().modify()->setRelease(release.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto releases{ Release::findIds(session, Release::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::Artist})) };
|
||||
EXPECT_EQ(releases.results.size(), 0);
|
||||
@@ -440,7 +475,7 @@ TEST_F(DatabaseFixture, Release_artist)
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
TrackArtistLink::create(session, track.get(), artist.get(), TrackArtistLinkType::Artist);
|
||||
|
||||
auto releases{ Release::findIds(session, Release::FindParameters {}.setArtist(artist.getId(), {TrackArtistLinkType::Artist})) };
|
||||
@@ -486,40 +521,40 @@ TEST_F(DatabaseFixture, Release_getDiscCount)
|
||||
ScopedTrack track2{ session, "MyTrack2" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(release.get()->getDiscCount(), 0);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track.get().modify()->setRelease(release.get());
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(release.get()->getDiscCount(), 0);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track.get().modify()->setDiscNumber(5);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(release.get()->getDiscCount(), 1);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track2.get().modify()->setRelease(release.get());
|
||||
track2.get().modify()->setDiscNumber(5);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(release.get()->getDiscCount(), 1);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track2.get().modify()->setDiscNumber(6);
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(release.get()->getDiscCount(), 2);
|
||||
}
|
||||
}
|
||||
@@ -529,19 +564,19 @@ TEST_F(DatabaseFixture, Release_releaseType)
|
||||
ScopedRelease release{ session, "MyRelease" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(release.get()->getPrimaryType(), std::nullopt);
|
||||
EXPECT_EQ(release.get()->getSecondaryTypes(), EnumSet<ReleaseTypeSecondary> {});
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
release.get().modify()->setPrimaryType({ ReleaseTypePrimary::Album });
|
||||
release.get().modify()->setSecondaryTypes({ ReleaseTypeSecondary::Compilation });
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(release.get()->getPrimaryType(), ReleaseTypePrimary::Album);
|
||||
EXPECT_TRUE(release.get()->getSecondaryTypes().contains(ReleaseTypeSecondary::Compilation));
|
||||
}
|
||||
@@ -563,7 +598,7 @@ TEST_F(DatabaseFixture, Release_sortMethod)
|
||||
ASSERT_GT(release2Date, release1OriginalDate);
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
track1.get().modify()->setRelease(release1.get());
|
||||
track1.get().modify()->setOriginalDate(release1OriginalDate);
|
||||
@@ -574,7 +609,7 @@ TEST_F(DatabaseFixture, Release_sortMethod)
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto releases{ Release::findIds(session, Release::FindParameters {}.setSortMethod(ReleaseSortMethod::Name)) };
|
||||
ASSERT_EQ(releases.results.size(), 2);
|
||||
@@ -583,14 +618,14 @@ TEST_F(DatabaseFixture, Release_sortMethod)
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto releases{ Release::findIds(session, Release::FindParameters {}.setSortMethod(ReleaseSortMethod::Random)) };
|
||||
ASSERT_EQ(releases.results.size(), 2);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto releases{ Release::findIds(session, Release::FindParameters {}.setSortMethod(ReleaseSortMethod::Date)) };
|
||||
ASSERT_EQ(releases.results.size(), 2);
|
||||
@@ -599,7 +634,7 @@ TEST_F(DatabaseFixture, Release_sortMethod)
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto releases{ Release::findIds(session, Release::FindParameters {}.setSortMethod(ReleaseSortMethod::OriginalDate)) };
|
||||
ASSERT_EQ(releases.results.size(), 2);
|
||||
@@ -607,7 +642,7 @@ TEST_F(DatabaseFixture, Release_sortMethod)
|
||||
EXPECT_EQ(releases.results.back(), release2.getId());
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto releases{ Release::findIds(session, Release::FindParameters {}.setSortMethod(ReleaseSortMethod::OriginalDateDesc)) };
|
||||
ASSERT_EQ(releases.results.size(), 2);
|
||||
@@ -616,7 +651,6 @@ TEST_F(DatabaseFixture, Release_sortMethod)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
TEST_F(DatabaseFixture, Release_meanBitrate)
|
||||
{
|
||||
ScopedRelease release1{ session, "MyRelease1" };
|
||||
@@ -626,14 +660,14 @@ TEST_F(DatabaseFixture, Release_meanBitrate)
|
||||
|
||||
auto checkExpectedBitrate = [&](std::size_t bitrate)
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(release1->getMeanBitrate(), bitrate);
|
||||
};
|
||||
|
||||
checkExpectedBitrate(0);
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track1.get().modify()->setBitrate(128);
|
||||
track1.get().modify()->setRelease(release1.get());
|
||||
}
|
||||
@@ -641,14 +675,14 @@ TEST_F(DatabaseFixture, Release_meanBitrate)
|
||||
checkExpectedBitrate(128);
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track2.get().modify()->setBitrate(256);
|
||||
track2.get().modify()->setRelease(release1.get());
|
||||
}
|
||||
checkExpectedBitrate(192);
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track3.get().modify()->setBitrate(0);
|
||||
track3.get().modify()->setRelease(release1.get());
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ TEST_F(DatabaseFixture, StarredArtist)
|
||||
ScopedUser user2{ session, "MyUser2" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto starredArtist{ StarredArtist::find(session, artist->getId(), user->getId(), FeedbackBackend::Internal) };
|
||||
EXPECT_FALSE(starredArtist);
|
||||
@@ -43,7 +43,7 @@ TEST_F(DatabaseFixture, StarredArtist)
|
||||
|
||||
ScopedStarredArtist starredArtist{ session, artist.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto gotArtist{ StarredArtist::find(session, artist->getId(), user->getId(), FeedbackBackend::Internal) };
|
||||
EXPECT_EQ(gotArtist->getId(), starredArtist->getId());
|
||||
@@ -51,7 +51,7 @@ TEST_F(DatabaseFixture, StarredArtist)
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto artists{ Artist::findIds(session, Artist::FindParameters {}) };
|
||||
EXPECT_EQ(artists.results.size(), 1);
|
||||
@@ -62,6 +62,29 @@ TEST_F(DatabaseFixture, StarredArtist)
|
||||
artists = Artist::findIds(session, Artist::FindParameters{}.setStarringUser(user2.getId(), FeedbackBackend::Internal));
|
||||
EXPECT_EQ(artists.results.size(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
user.get().modify()->setFeedbackBackend(FeedbackBackend::ListenBrainz);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto gotArtist{ StarredArtist::find(session, artist->getId(), user->getId()) };
|
||||
EXPECT_EQ(gotArtist, Artist::pointer{});
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
user.get().modify()->setFeedbackBackend(FeedbackBackend::Internal);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
auto gotArtist{ StarredArtist::find(session, artist->getId(), user->getId()) };
|
||||
EXPECT_EQ(gotArtist->getId(), starredArtist->getId());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, StarredArtist_PendingDestroy)
|
||||
@@ -71,7 +94,7 @@ TEST_F(DatabaseFixture, StarredArtist_PendingDestroy)
|
||||
ScopedStarredArtist starredArtist{ session, artist.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
auto artists{ Artist::findIds(session, Artist::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal)) };
|
||||
EXPECT_EQ(artists.results.size(), 1);
|
||||
@@ -94,14 +117,14 @@ TEST_F(DatabaseFixture, StarredArtist_dateTime)
|
||||
const Wt::WDateTime dateTime{ Wt::WDate {1950, 1, 2}, Wt::WTime {12, 30, 1} };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto artists{ Artist::find(session, Artist::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal)) };
|
||||
EXPECT_EQ(artists.results.size(), 2);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
starredArtist1.get().modify()->setDateTime(dateTime);
|
||||
starredArtist2.get().modify()->setDateTime(dateTime.addSecs(-1));
|
||||
@@ -112,7 +135,7 @@ TEST_F(DatabaseFixture, StarredArtist_dateTime)
|
||||
EXPECT_EQ(artists.results[1], starredArtist2->getArtist()->getId());
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
starredArtist1.get().modify()->setDateTime(dateTime);
|
||||
starredArtist2.get().modify()->setDateTime(dateTime.addSecs(1));
|
||||
|
||||
@@ -31,7 +31,7 @@ TEST_F(DatabaseFixture, StarredRelease)
|
||||
ScopedUser user2{ session, "MyUser2" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto starredRelease{ StarredRelease::find(session, release->getId(), user->getId(), FeedbackBackend::Internal) };
|
||||
EXPECT_FALSE(starredRelease);
|
||||
@@ -43,7 +43,7 @@ TEST_F(DatabaseFixture, StarredRelease)
|
||||
|
||||
ScopedStarredRelease starredRelease{ session, release.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto gotRelease{ StarredRelease::find(session, release->getId(), user->getId(), FeedbackBackend::Internal) };
|
||||
EXPECT_EQ(gotRelease->getId(), starredRelease->getId());
|
||||
@@ -51,7 +51,7 @@ TEST_F(DatabaseFixture, StarredRelease)
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto releases{ Release::find(session, Release::FindParameters {}) };
|
||||
EXPECT_EQ(releases.results.size(), 1);
|
||||
@@ -62,6 +62,18 @@ TEST_F(DatabaseFixture, StarredRelease)
|
||||
releases = Release::find(session, Release::FindParameters{}.setStarringUser(user2.getId(), FeedbackBackend::Internal));
|
||||
EXPECT_EQ(releases.results.size(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
user.get().modify()->setFeedbackBackend(FeedbackBackend::ListenBrainz);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto gotRelease{ StarredRelease::find(session, release->getId(), user->getId()) };
|
||||
EXPECT_EQ(gotRelease, StarredRelease::pointer{});
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Starredrelease_PendingDestroy)
|
||||
@@ -71,7 +83,7 @@ TEST_F(DatabaseFixture, Starredrelease_PendingDestroy)
|
||||
ScopedStarredRelease starredRelease{ session, release.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
auto releases{ Release::find(session, Release::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal)) };
|
||||
EXPECT_EQ(releases.results.size(), 1);
|
||||
@@ -94,14 +106,14 @@ TEST_F(DatabaseFixture, StarredRelease_dateTime)
|
||||
const Wt::WDateTime dateTime{ Wt::WDate {1950, 1, 2}, Wt::WTime {12, 30, 1} };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto releases{ Release::findIds(session, Release::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal)) };
|
||||
EXPECT_EQ(releases.results.size(), 2);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
starredRelease1.get().modify()->setDateTime(dateTime);
|
||||
starredRelease2.get().modify()->setDateTime(dateTime.addSecs(-1));
|
||||
@@ -112,7 +124,7 @@ TEST_F(DatabaseFixture, StarredRelease_dateTime)
|
||||
EXPECT_EQ(releases.results[1], starredRelease2->getRelease()->getId());
|
||||
}
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
starredRelease1.get().modify()->setDateTime(dateTime);
|
||||
starredRelease2.get().modify()->setDateTime(dateTime.addSecs(1));
|
||||
|
||||
@@ -26,98 +26,110 @@ using ScopedStarredTrack = ScopedEntity<Database::StarredTrack>;
|
||||
|
||||
TEST_F(DatabaseFixture, StarredTrack)
|
||||
{
|
||||
ScopedTrack track {session, "MyTrack"};
|
||||
ScopedUser user {session, "MyUser"};
|
||||
ScopedUser user2 {session, "MyUser2"};
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
ScopedUser user2{ session, "MyUser2" };
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
auto transaction {session.createReadTransaction()};
|
||||
|
||||
auto starredTrack {StarredTrack::find(session, track->getId(), user->getId(), FeedbackBackend::Internal)};
|
||||
auto starredTrack{ StarredTrack::find(session, track->getId(), user->getId(), FeedbackBackend::Internal) };
|
||||
EXPECT_FALSE(starredTrack);
|
||||
EXPECT_EQ(StarredTrack::getCount(session), 0);
|
||||
|
||||
auto tracks {Track::findIds(session, Track::FindParameters {})};
|
||||
auto tracks{ Track::findIds(session, Track::FindParameters {}) };
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
}
|
||||
|
||||
ScopedStarredTrack starredTrack {session, track.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal};
|
||||
ScopedStarredTrack starredTrack{ session, track.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
auto transaction {session.createReadTransaction()};
|
||||
|
||||
auto gotTrack {StarredTrack::find(session, track->getId(), user->getId(), FeedbackBackend::Internal)};
|
||||
auto gotTrack{ StarredTrack::find(session, track->getId(), user->getId(), FeedbackBackend::Internal) };
|
||||
EXPECT_EQ(gotTrack->getId(), starredTrack->getId());
|
||||
EXPECT_EQ(StarredTrack::getCount(session), 1);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
auto transaction {session.createReadTransaction()};
|
||||
|
||||
auto tracks {Track::findIds(session, Track::FindParameters {})};
|
||||
auto tracks{ Track::findIds(session, Track::FindParameters {}) };
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
|
||||
tracks = Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal));
|
||||
tracks = Track::findIds(session, Track::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal));
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
|
||||
tracks = Track::findIds(session, Track::FindParameters {}.setStarringUser(user2.getId(), FeedbackBackend::Internal));
|
||||
tracks = Track::findIds(session, Track::FindParameters{}.setStarringUser(user2.getId(), FeedbackBackend::Internal));
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
user.get().modify()->setFeedbackBackend(FeedbackBackend::ListenBrainz);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto gotRelease{ StarredTrack::find(session, track->getId(), user->getId()) };
|
||||
EXPECT_EQ(gotRelease, StarredTrack::pointer{});
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, Starredtrack_PendingDestroy)
|
||||
{
|
||||
ScopedTrack track {session, "MyTrack"};
|
||||
ScopedUser user {session, "MyUser"};
|
||||
ScopedStarredTrack starredTrack {session, track.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal};
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
ScopedStarredTrack starredTrack{ session, track.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
|
||||
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
auto transaction {session.createWriteTransaction()};
|
||||
|
||||
auto tracks {Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal))};
|
||||
auto tracks{ Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal)) };
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
|
||||
starredTrack.get().modify()->setSyncState(SyncState::PendingRemove);
|
||||
tracks = Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal));
|
||||
tracks = Track::findIds(session, Track::FindParameters{}.setStarringUser(user.getId(), FeedbackBackend::Internal));
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseFixture, StarredTrack_dateTime)
|
||||
{
|
||||
ScopedTrack track1 {session, "MyTrack1"};
|
||||
ScopedTrack track2 {session, "MyTrack2"};
|
||||
ScopedUser user {session, "MyUser"};
|
||||
ScopedTrack track1{ session, "MyTrack1" };
|
||||
ScopedTrack track2{ session, "MyTrack2" };
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
|
||||
ScopedStarredTrack starredTrack1 {session, track1.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal};
|
||||
ScopedStarredTrack starredTrack2 {session, track2.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal};
|
||||
ScopedStarredTrack starredTrack1{ session, track1.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
|
||||
ScopedStarredTrack starredTrack2{ session, track2.lockAndGet(), user.lockAndGet(), FeedbackBackend::Internal };
|
||||
|
||||
const Wt::WDateTime dateTime {Wt::WDate {1950, 1, 2}, Wt::WTime {12, 30, 1}};
|
||||
const Wt::WDateTime dateTime{ Wt::WDate {1950, 1, 2}, Wt::WTime {12, 30, 1} };
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
auto transaction {session.createReadTransaction()};
|
||||
|
||||
auto tracks {Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal))};
|
||||
auto tracks{ Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal)) };
|
||||
EXPECT_EQ(tracks.results.size(), 2);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
auto transaction {session.createWriteTransaction()};
|
||||
|
||||
starredTrack1.get().modify()->setDateTime(dateTime);
|
||||
starredTrack2.get().modify()->setDateTime(dateTime.addSecs(-1));
|
||||
|
||||
auto tracks {Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(TrackSortMethod::StarredDateDesc))};
|
||||
auto tracks{ Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(TrackSortMethod::StarredDateDesc)) };
|
||||
ASSERT_EQ(tracks.results.size(), 2);
|
||||
EXPECT_EQ(tracks.results[0], starredTrack1->getTrack()->getId());
|
||||
EXPECT_EQ(tracks.results[1], starredTrack2->getTrack()->getId());
|
||||
}
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
auto transaction {session.createWriteTransaction()};
|
||||
|
||||
starredTrack1.get().modify()->setDateTime(dateTime);
|
||||
starredTrack2.get().modify()->setDateTime(dateTime.addSecs(1));
|
||||
|
||||
auto tracks {Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(TrackSortMethod::StarredDateDesc))};
|
||||
auto tracks{ Track::findIds(session, Track::FindParameters {}.setStarringUser(user.getId(), FeedbackBackend::Internal).setSortMethod(TrackSortMethod::StarredDateDesc)) };
|
||||
ASSERT_EQ(tracks.results.size(), 2);
|
||||
EXPECT_EQ(tracks.results[0], starredTrack2->getTrack()->getId());
|
||||
EXPECT_EQ(tracks.results[1], starredTrack1->getTrack()->getId());
|
||||
|
||||
@@ -26,7 +26,7 @@ using namespace Database;
|
||||
TEST_F(DatabaseFixture, Track)
|
||||
{
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(Track::find(session, Track::FindParameters{}).results.size(), 0);
|
||||
EXPECT_EQ(Track::findIds(session, Track::FindParameters{}).results.size(), 0);
|
||||
EXPECT_EQ(Track::getCount(session), 0);
|
||||
@@ -42,7 +42,7 @@ TEST_F(DatabaseFixture, Track)
|
||||
ScopedTrack track{ session, "MyTrackFile" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
EXPECT_EQ(Track::find(session, Track::FindParameters{}).results.size(), 1);
|
||||
EXPECT_EQ(Track::getCount(session), 1);
|
||||
@@ -69,7 +69,7 @@ TEST_F(DatabaseFixture, MultipleTracks)
|
||||
ScopedTrack track2{ session, "MyTrackFile2" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
EXPECT_TRUE(track1.getId() != track2.getId());
|
||||
EXPECT_TRUE(track1.get() != track2.get());
|
||||
@@ -87,7 +87,7 @@ TEST_F(DatabaseFixture, MultipleTracksSearchByFilter)
|
||||
ScopedTrack track6{ session, "" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track1.get().modify()->setName("MyTrack");
|
||||
track2.get().modify()->setName("MyTrack%");
|
||||
track3.get().modify()->setName("MyTrack%Foo");
|
||||
@@ -97,7 +97,7 @@ TEST_F(DatabaseFixture, MultipleTracksSearchByFilter)
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
{
|
||||
const auto tracks{ Track::findIds(session, Track::FindParameters {}.setKeywords({"Track"})) };
|
||||
@@ -128,19 +128,19 @@ TEST_F(DatabaseFixture, Track_date)
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(track->getYear(), std::nullopt);
|
||||
EXPECT_EQ(track->getOriginalYear(), std::nullopt);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track.get().modify()->setDate(Wt::WDate{ 1995, 5, 5 });
|
||||
track.get().modify()->setOriginalDate(Wt::WDate{ 1994, 2, 2 });
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(track->getYear(), 1995);
|
||||
EXPECT_EQ(track->getOriginalYear(), 1994);
|
||||
}
|
||||
@@ -153,24 +153,24 @@ TEST_F(DatabaseFixture, Track_writtenAfter)
|
||||
const Wt::WDateTime dateTime{ Wt::WDate {1950, 1, 1}, Wt::WTime {12, 30, 20} };
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
track.get().modify()->setLastWriteTime(dateTime);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
const auto tracks{ Track::findIds(session, Track::FindParameters {}) };
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
const auto tracks{ Track::findIds(session, Track::FindParameters {}.setWrittenAfter(dateTime.addSecs(-1))) };
|
||||
EXPECT_EQ(tracks.results.size(), 1);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
const auto tracks{ Track::findIds(session, Track::FindParameters {}.setWrittenAfter(dateTime.addSecs(+1))) };
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
}
|
||||
|
||||
@@ -31,21 +31,21 @@ TEST_F(DatabaseFixture, TrackBookmark)
|
||||
ScopedUser user {session, "MyUser"};
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
auto transaction {session.createReadTransaction()};
|
||||
EXPECT_EQ(TrackBookmark::getCount(session), 0);
|
||||
}
|
||||
|
||||
ScopedTrackBookmark bookmark {session, user.lockAndGet(), track.lockAndGet()};
|
||||
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
auto transaction {session.createWriteTransaction()};
|
||||
|
||||
bookmark.get().modify()->setComment("MyComment");
|
||||
bookmark.get().modify()->setOffset(std::chrono::milliseconds {5});
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
auto transaction {session.createReadTransaction()};
|
||||
|
||||
EXPECT_EQ(TrackBookmark::getCount(session), 1);
|
||||
|
||||
@@ -54,7 +54,7 @@ TEST_F(DatabaseFixture, TrackBookmark)
|
||||
EXPECT_EQ(bookmarks.results.front(), bookmark.getId());
|
||||
}
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
auto transaction {session.createReadTransaction()};
|
||||
|
||||
auto userBookmark {TrackBookmark::find(session, user.getId(), track.getId())};
|
||||
ASSERT_TRUE(userBookmark);
|
||||
|
||||
@@ -31,14 +31,14 @@ TEST_F(DatabaseFixture, TrackFeatures)
|
||||
ScopedUser user {session, "MyUser"};
|
||||
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
auto transaction {session.createReadTransaction()};
|
||||
EXPECT_EQ(TrackFeatures::getCount(session), 0);
|
||||
}
|
||||
|
||||
ScopedTrackFeatures trackFeatures {session, track.lockAndGet(), ""};
|
||||
|
||||
{
|
||||
auto transaction {session.createUniqueTransaction()};
|
||||
auto transaction {session.createWriteTransaction()};
|
||||
EXPECT_EQ(TrackFeatures::getCount(session), 1);
|
||||
|
||||
auto allTrackFeatures {TrackFeatures::find(session)};
|
||||
|
||||
@@ -27,14 +27,14 @@ TEST_F(DatabaseFixture, SingleTrackList)
|
||||
{
|
||||
ScopedUser user{ session, "MyUser" };
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(TrackList::getCount(session), 0);
|
||||
}
|
||||
|
||||
ScopedTrackList trackList{ session, "MytrackList", TrackListType::Playlist, false, user.lockAndGet() };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
EXPECT_EQ(TrackList::getCount(session), 1);
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ TEST_F(DatabaseFixture, SingleTrackListSingleTrack)
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto tracks{ Track::findIds(session, Track::FindParameters {}.setTrackList(trackList1.getId())) };
|
||||
EXPECT_EQ(tracks.results.size(), 0);
|
||||
@@ -57,13 +57,13 @@ TEST_F(DatabaseFixture, SingleTrackListSingleTrack)
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
session.create<TrackListEntry>(track.get(), trackList1.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto tracks{ Track::findIds(session, Track::FindParameters {}.setTrackList(trackList1.getId())) };
|
||||
ASSERT_EQ(tracks.results.size(), 1);
|
||||
@@ -82,7 +82,7 @@ TEST_F(DatabaseFixture, TrackList_SortMethod)
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto trackLists{ TrackList::find(session, TrackList::FindParameters {}.setSortMethod(TrackListSortMethod::Name)) };
|
||||
ASSERT_EQ(trackLists.results.size(), 2);
|
||||
@@ -91,14 +91,14 @@ TEST_F(DatabaseFixture, TrackList_SortMethod)
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
trackList1.get().modify()->setLastModifiedDateTime(Wt::WDateTime{ Wt::WDate {1900,1,1} });
|
||||
trackList2.get().modify()->setLastModifiedDateTime(Wt::WDateTime{ Wt::WDate {1900,1,2} });
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto trackLists{ TrackList::find(session, TrackList::FindParameters {}.setSortMethod(TrackListSortMethod::LastModifiedDesc)) };
|
||||
ASSERT_EQ(trackLists.results.size(), 2);
|
||||
@@ -107,14 +107,14 @@ TEST_F(DatabaseFixture, TrackList_SortMethod)
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
trackList1.get().modify()->setLastModifiedDateTime(Wt::WDateTime{ Wt::WDate {1900,1,2} });
|
||||
trackList2.get().modify()->setLastModifiedDateTime(Wt::WDateTime{ Wt::WDate {1900,1,1} });
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const auto trackLists{ TrackList::find(session, TrackList::FindParameters {}.setSortMethod(TrackListSortMethod::LastModifiedDesc)) };
|
||||
ASSERT_EQ(trackLists.results.size(), 2);
|
||||
@@ -133,12 +133,12 @@ TEST_F(DatabaseFixture, SingleTrackListMultipleTrack)
|
||||
{
|
||||
tracks.emplace_back(session, "MyTrack" + std::to_string(i));
|
||||
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
session.create<TrackListEntry>(tracks.back().get(), trackList.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
ASSERT_EQ(trackList->getCount(), tracks.size());
|
||||
const auto trackIds{ trackList->getTrackIds() };
|
||||
@@ -161,21 +161,21 @@ TEST_F(DatabaseFixture, SingleTrackListSingleTrackWithCluster)
|
||||
ScopedTrack track{ session, "MyTrack" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto trackLists{ TrackList::find(session, TrackList::FindParameters {}.setClusters({cluster.getId()})) };
|
||||
EXPECT_EQ(trackLists.results.size(), 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
session.create<TrackListEntry>(track.get(), trackList1.get());
|
||||
cluster.get().modify()->addTrack(track.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
auto trackLists{ TrackList::find(session, TrackList::FindParameters {}.setClusters({cluster.getId()})) };
|
||||
ASSERT_EQ(trackLists.results.size(), 1);
|
||||
@@ -191,24 +191,24 @@ TEST_F(DatabaseFixture, SingleTrackList_getEntries)
|
||||
ScopedTrack track2{ session, "MyTrack" };
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
session.create<TrackListEntry>(track1.get(), trackList.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
auto entries{ trackList.get()->getEntries() };
|
||||
ASSERT_EQ(entries.size(), 1);
|
||||
EXPECT_EQ(entries.front()->getTrack()->getId(), track1.getId());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
session.create<TrackListEntry>(track2.get(), trackList.get());
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
auto entries{ trackList.get()->getEntries() };
|
||||
ASSERT_EQ(entries.size(), 2);
|
||||
EXPECT_EQ(entries[0]->getTrack()->getId(), track1.getId());
|
||||
@@ -216,7 +216,7 @@ TEST_F(DatabaseFixture, SingleTrackList_getEntries)
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
auto entries{ trackList.get()->getEntries(Range {1, 1}) };
|
||||
ASSERT_EQ(entries.size(), 1);
|
||||
EXPECT_EQ(entries[0]->getTrack()->getId(), track2.getId());
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
#include "services/database/StarredTrack.hpp"
|
||||
#include "services/database/Track.hpp"
|
||||
#include "services/database/User.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
|
||||
#include "internal/InternalBackend.hpp"
|
||||
#include "listenbrainz/ListenBrainzBackend.hpp"
|
||||
@@ -44,15 +44,15 @@ namespace Feedback
|
||||
FeedbackService::FeedbackService(boost::asio::io_context& ioContext, Db& db)
|
||||
: _db{ db }
|
||||
{
|
||||
LMS_LOG(SCROBBLING, INFO) << "Starting service...";
|
||||
LMS_LOG(SCROBBLING, INFO, "Starting service...");
|
||||
_backends.emplace(Database::FeedbackBackend::Internal, std::make_unique<InternalBackend>(_db));
|
||||
_backends.emplace(Database::FeedbackBackend::ListenBrainz, std::make_unique<ListenBrainz::ListenBrainzBackend>(ioContext, _db));
|
||||
LMS_LOG(SCROBBLING, INFO) << "Service started!";
|
||||
LMS_LOG(SCROBBLING, INFO, "Service started!");
|
||||
}
|
||||
|
||||
FeedbackService::~FeedbackService()
|
||||
{
|
||||
LMS_LOG(SCROBBLING, INFO) << "Service stopped!";
|
||||
LMS_LOG(SCROBBLING, INFO, "Service stopped!");
|
||||
}
|
||||
|
||||
std::optional<Database::FeedbackBackend> FeedbackService::getUserFeedbackBackend(UserId userId)
|
||||
@@ -60,7 +60,7 @@ namespace Feedback
|
||||
std::optional<Database::FeedbackBackend> feedbackBackend;
|
||||
|
||||
Session& session{ _db.getTLSSession() };
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
if (const User::pointer user{ User::find(session, userId) })
|
||||
feedbackBackend = user->getFeedbackBackend();
|
||||
|
||||
@@ -101,7 +101,7 @@ namespace Feedback
|
||||
searchParams.setRange(params.range);
|
||||
|
||||
Session& session{ _db.getTLSSession() };
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
return Artist::findIds(session, searchParams);
|
||||
}
|
||||
@@ -139,7 +139,7 @@ namespace Feedback
|
||||
searchParams.setRange(params.range);
|
||||
|
||||
Session& session{ _db.getTLSSession() };
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
return Release::findIds(session, searchParams);
|
||||
}
|
||||
@@ -177,7 +177,7 @@ namespace Feedback
|
||||
searchParams.setRange(params.range);
|
||||
|
||||
Session& session{ _db.getTLSSession() };
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
return Track::findIds(session, searchParams);
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace Feedback
|
||||
typename StarredObjType::IdType starredObjId;
|
||||
{
|
||||
Session& session{ _db.getTLSSession() };
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
typename StarredObjType::pointer starredObj{ StarredObjType::find(session, objId, userId, *backend) };
|
||||
if (!starredObj)
|
||||
@@ -68,7 +68,7 @@ namespace Feedback
|
||||
typename StarredObjType::IdType starredObjId;
|
||||
{
|
||||
Session& session{ _db.getTLSSession() };
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
typename StarredObjType::pointer starredObj{ StarredObjType::find(session, objId, userId, *backend) };
|
||||
if (!starredObj)
|
||||
@@ -82,28 +82,20 @@ namespace Feedback
|
||||
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
||||
bool FeedbackService::isStarred(UserId userId, ObjIdType objId)
|
||||
{
|
||||
const auto backend{ getUserFeedbackBackend(userId) };
|
||||
if (!backend)
|
||||
return false;
|
||||
|
||||
Session& session{ _db.getTLSSession() };
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
typename StarredObjType::pointer starredObj{ StarredObjType::find(session, objId, userId, *backend) };
|
||||
typename StarredObjType::pointer starredObj{ StarredObjType::find(session, objId, userId) };
|
||||
return starredObj && (starredObj->getSyncState() != SyncState::PendingRemove);
|
||||
}
|
||||
|
||||
template <typename ObjType, typename ObjIdType, typename StarredObjType>
|
||||
Wt::WDateTime FeedbackService::getStarredDateTime(UserId userId, ObjIdType objId)
|
||||
{
|
||||
const auto backend{ getUserFeedbackBackend(userId) };
|
||||
if (!backend)
|
||||
return {};
|
||||
|
||||
Session& session{ _db.getTLSSession() };
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
typename StarredObjType::pointer starredObj{ StarredObjType::find(session, objId, userId, *backend) };
|
||||
typename StarredObjType::pointer starredObj{ StarredObjType::find(session, objId, userId) };
|
||||
if (starredObj && (starredObj->getSyncState() != SyncState::PendingRemove))
|
||||
return starredObj->getDateTime();
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace Feedback
|
||||
template <typename StarredObjType>
|
||||
void onStarred(Database::Session& session, typename StarredObjType::IdType id)
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
if (auto starredObj{ StarredObjType::find(session, id) })
|
||||
starredObj.modify()->setSyncState(Database::SyncState::Synchronized);
|
||||
@@ -41,7 +41,7 @@ namespace Feedback
|
||||
template <typename StarredObjType>
|
||||
void onUnstarred(Database::Session& session, typename StarredObjType::IdType id)
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
if (auto starredObj{ StarredObjType::find(session, id) })
|
||||
starredObj.remove();
|
||||
|
||||
@@ -57,7 +57,7 @@ namespace Feedback::ListenBrainz
|
||||
|
||||
const Wt::Json::Array& feedbacks = root.get("feedback");
|
||||
|
||||
LOG(DEBUG) << "Got " << feedbacks.size() << " feedbacks";
|
||||
LOG(DEBUG, "Got " << feedbacks.size() << " feedbacks");
|
||||
|
||||
if (feedbacks.empty())
|
||||
return res;
|
||||
@@ -72,17 +72,17 @@ namespace Feedback::ListenBrainz
|
||||
}
|
||||
catch (const Exception& e)
|
||||
{
|
||||
LOG(DEBUG) << "Cannot parse feedback: " << e.what() << ", skipping";
|
||||
LOG(DEBUG, "Cannot parse feedback: " << e.what() << ", skipping");
|
||||
}
|
||||
catch (const Wt::WException& e)
|
||||
{
|
||||
LOG(DEBUG) << "Cannot parse feedback: " << e.what() << ", skipping";
|
||||
LOG(DEBUG, "Cannot parse feedback: " << e.what() << ", skipping");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (const Wt::WException& error)
|
||||
{
|
||||
LOG(ERROR) << "Cannot parse 'feedback' result: " << error.what();
|
||||
LOG(ERROR, "Cannot parse 'feedback' result: " << error.what());
|
||||
}
|
||||
|
||||
return res;
|
||||
|
||||
@@ -53,7 +53,7 @@ namespace Feedback::ListenBrainz
|
||||
}
|
||||
catch (const Wt::WException& e)
|
||||
{
|
||||
LOG(ERROR) << "Cannot parse listen count response: " << e.what();
|
||||
LOG(ERROR, "Cannot parse listen count response: " << e.what());
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
@@ -66,7 +66,7 @@ namespace Feedback::ListenBrainz
|
||||
, _maxSyncFeedbackCount{ Service<IConfig>::get()->getULong("listenbrainz-max-sync-feedback-count", 1000) }
|
||||
, _syncFeedbacksPeriod{ Service<IConfig>::get()->getULong("listenbrainz-sync-feedbacks-period-hours", 1) }
|
||||
{
|
||||
LOG(INFO) << "Starting Feedbacks synchronizer, maxSyncFeedbackCount = " << _maxSyncFeedbackCount << ", _syncFeedbacksPeriod = " << _syncFeedbacksPeriod.count() << " hours";
|
||||
LOG(INFO, "Starting Feedbacks synchronizer, maxSyncFeedbackCount = " << _maxSyncFeedbackCount << ", _syncFeedbacksPeriod = " << _syncFeedbacksPeriod.count() << " hours");
|
||||
|
||||
scheduleSync(std::chrono::seconds{ 30 });
|
||||
}
|
||||
@@ -77,7 +77,7 @@ namespace Feedback::ListenBrainz
|
||||
{
|
||||
Database::Session& session{ _db.getTLSSession() };
|
||||
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
Database::StarredTrack::pointer starredTrack{ Database::StarredTrack::find(session, starredTrackId) };
|
||||
if (!starredTrack)
|
||||
@@ -95,7 +95,7 @@ namespace Feedback::ListenBrainz
|
||||
case FeedbackType::Erase:
|
||||
if (!recordingMBID)
|
||||
{
|
||||
LOG(DEBUG) << "Track has no recording MBID: erasing star";
|
||||
LOG(DEBUG, "Track has no recording MBID: erasing star");
|
||||
starredTrack.remove();
|
||||
}
|
||||
else
|
||||
@@ -112,7 +112,7 @@ namespace Feedback::ListenBrainz
|
||||
|
||||
if (!recordingMBID)
|
||||
{
|
||||
LOG(DEBUG) << "Track has no recording MBID: skipping";
|
||||
LOG(DEBUG, "Track has no recording MBID: skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ namespace Feedback::ListenBrainz
|
||||
}
|
||||
catch (Exception& e)
|
||||
{
|
||||
LOG(DEBUG) << "Cannot send feedback: " << e.what();
|
||||
LOG(DEBUG, "Cannot send feedback: " << e.what());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,12 +151,12 @@ namespace Feedback::ListenBrainz
|
||||
assert(_strand.running_in_this_thread());
|
||||
|
||||
Database::Session& session{ _db.getTLSSession() };
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
Database::StarredTrack::pointer starredTrack{ Database::StarredTrack::find(session, starredTrackId) };
|
||||
if (!starredTrack)
|
||||
{
|
||||
LOG(DEBUG) << "Starred track not found. deleted?";
|
||||
LOG(DEBUG, "Starred track not found. deleted?");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -166,23 +166,23 @@ namespace Feedback::ListenBrainz
|
||||
{
|
||||
case FeedbackType::Love:
|
||||
starredTrack.modify()->setSyncState(Database::SyncState::Synchronized);
|
||||
LOG(DEBUG) << "State set to synchronized";
|
||||
LOG(DEBUG, "State set to synchronized");
|
||||
|
||||
if (userContext.feedbackCount)
|
||||
{
|
||||
(*userContext.feedbackCount)++;
|
||||
LOG(DEBUG) << "Feedback count set to " << *userContext.feedbackCount << " for user '" << userContext.listenBrainzUserName << "'";
|
||||
LOG(DEBUG, "Feedback count set to " << *userContext.feedbackCount << " for user '" << userContext.listenBrainzUserName << "'");
|
||||
}
|
||||
break;
|
||||
|
||||
case FeedbackType::Erase:
|
||||
starredTrack.remove();
|
||||
LOG(DEBUG) << "Removed starred track";
|
||||
LOG(DEBUG, "Removed starred track");
|
||||
|
||||
if (userContext.feedbackCount && *userContext.feedbackCount > 0)
|
||||
{
|
||||
(*userContext.feedbackCount)--;
|
||||
LOG(DEBUG) << "Feedback count set to " << *userContext.feedbackCount << " for user '" << userContext.listenBrainzUserName << "'";
|
||||
LOG(DEBUG, "Feedback count set to " << *userContext.feedbackCount << " for user '" << userContext.listenBrainzUserName << "'");
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -202,7 +202,7 @@ namespace Feedback::ListenBrainz
|
||||
{
|
||||
Database::Session& session {_db.getTLSSession()};
|
||||
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
auto transaction {session.createReadTransaction()};
|
||||
|
||||
StarredTrack::FindParameters params;
|
||||
params.setFeedbackBackend(Database::FeedbackBackend::ListenBrainz, scrobblingState)
|
||||
@@ -211,7 +211,7 @@ namespace Feedback::ListenBrainz
|
||||
pendingFeedbacks = StarredTrack::find(session, params);
|
||||
}
|
||||
|
||||
LOG(DEBUG) << "Queing " << pendingFeedbacks.results.size() << " pending '" << (feedbackType == FeedbackType::Love ? "love" : "erase") << "' feedbacks";
|
||||
LOG(DEBUG, "Queing " << pendingFeedbacks.results.size() << " pending '" << (feedbackType == FeedbackType::Love ? "love" : "erase") << "' feedbacks");
|
||||
|
||||
for (const StarredTrackId starredTrackId : pendingFeedbacks.results)
|
||||
enqueFeedback(feedbackType, starredTrackId);
|
||||
@@ -247,13 +247,13 @@ namespace Feedback::ListenBrainz
|
||||
if (_syncFeedbacksPeriod.count() == 0 || _maxSyncFeedbackCount == 0)
|
||||
return;
|
||||
|
||||
LOG(DEBUG) << "Scheduled sync in " << fromNow.count() << " seconds...";
|
||||
LOG(DEBUG, "Scheduled sync in " << fromNow.count() << " seconds...");
|
||||
_syncTimer.expires_after(fromNow);
|
||||
_syncTimer.async_wait(boost::asio::bind_executor(_strand, [this](const boost::system::error_code& ec)
|
||||
{
|
||||
if (ec == boost::asio::error::operation_aborted)
|
||||
{
|
||||
LOG(DEBUG) << "getFeedbacks aborted";
|
||||
LOG(DEBUG, "getFeedbacks aborted");
|
||||
return;
|
||||
}
|
||||
else if (ec)
|
||||
@@ -267,7 +267,7 @@ namespace Feedback::ListenBrainz
|
||||
|
||||
void FeedbacksSynchronizer::startSync()
|
||||
{
|
||||
LOG(DEBUG) << "Starting sync!";
|
||||
LOG(DEBUG, "Starting sync!");
|
||||
|
||||
assert(!isSyncing());
|
||||
assert(_strand.running_in_this_thread());
|
||||
@@ -277,7 +277,7 @@ namespace Feedback::ListenBrainz
|
||||
Database::RangeResults<Database::UserId> userIds;
|
||||
{
|
||||
Database::Session& session{ _db.getTLSSession() };
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
userIds = Database::User::find(_db.getTLSSession(), Database::User::FindParameters{}.setFeedbackBackend(Database::FeedbackBackend::ListenBrainz));
|
||||
}
|
||||
|
||||
@@ -303,7 +303,7 @@ namespace Feedback::ListenBrainz
|
||||
{
|
||||
_strand.dispatch([this, &context]
|
||||
{
|
||||
LOG(INFO) << "Feedback sync done for user '" << context.listenBrainzUserName << "', fetched: " << context.fetchedFeedbackCount << ", matched: " << context.matchedFeedbackCount << ", imported: " << context.importedFeedbackCount;
|
||||
LOG(INFO, "Feedback sync done for user '" << context.listenBrainzUserName << "', fetched: " << context.fetchedFeedbackCount << ", matched: " << context.matchedFeedbackCount << ", imported: " << context.importedFeedbackCount);
|
||||
context.syncing = false;
|
||||
|
||||
if (!isSyncing())
|
||||
@@ -356,11 +356,11 @@ namespace Feedback::ListenBrainz
|
||||
std::string msgBodyCopy{ msgBody };
|
||||
_strand.dispatch([this, msgBodyCopy, &context]
|
||||
{
|
||||
LOG(DEBUG) << "Current feedback count = " << (context.feedbackCount ? *context.feedbackCount : 0) << " for user '" << context.listenBrainzUserName << "'";
|
||||
LOG(DEBUG, "Current feedback count = " << (context.feedbackCount ? *context.feedbackCount : 0) << " for user '" << context.listenBrainzUserName << "'");
|
||||
|
||||
const auto totalFeedbackCount = parseTotalFeedbackCount(msgBodyCopy);
|
||||
if (totalFeedbackCount)
|
||||
LOG(DEBUG) << "Feedback count for listenbrainz user '" << context.listenBrainzUserName << "' = " << *totalFeedbackCount;
|
||||
LOG(DEBUG, "Feedback count for listenbrainz user '" << context.listenBrainzUserName << "' = " << *totalFeedbackCount);
|
||||
|
||||
bool needSync{ totalFeedbackCount && (!context.feedbackCount || *context.feedbackCount != *totalFeedbackCount) };
|
||||
context.feedbackCount = totalFeedbackCount;
|
||||
@@ -416,7 +416,7 @@ namespace Feedback::ListenBrainz
|
||||
{
|
||||
const FeedbacksParser::Result parseResult{ FeedbacksParser::parse(msgBody) };
|
||||
|
||||
LOG(DEBUG) << "Parsed " << parseResult.feedbackCount << " feedbacks, found " << parseResult.feedbacks.size() << " usable entries";
|
||||
LOG(DEBUG, "Parsed " << parseResult.feedbackCount << " feedbacks, found " << parseResult.feedbacks.size() << " usable entries");
|
||||
context.fetchedFeedbackCount += parseResult.feedbackCount;
|
||||
|
||||
for (const Feedback& feedback : parseResult.feedbacks)
|
||||
@@ -437,23 +437,21 @@ namespace Feedback::ListenBrainz
|
||||
TrackId trackId;
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
const std::vector<Track::pointer> tracks{ Track::findByRecordingMBID(session, feedback.recordingMBID) };
|
||||
if (tracks.size() > 1)
|
||||
{
|
||||
LOG(DEBUG) << "Too many matches for feedback '" << feedback << "': duplicate recording MBIDs found";
|
||||
LOG(DEBUG, "Too many matches for feedback '" << feedback << "': duplicate recording MBIDs found");
|
||||
return;
|
||||
}
|
||||
else if (tracks.empty())
|
||||
{
|
||||
LOG(DEBUG) << "Cannot match feedback '" << feedback << "': no track found for this recording MBID";
|
||||
LOG(DEBUG, "Cannot match feedback '" << feedback << "': no track found for this recording MBID");
|
||||
return;
|
||||
}
|
||||
|
||||
trackId = tracks.front()->getId();
|
||||
|
||||
const StarredTrack::pointer starredTrack{ StarredTrack::find(session, trackId, context.userId, Database::FeedbackBackend::ListenBrainz) };
|
||||
needImport = !starredTrack;
|
||||
needImport = !StarredTrack::exists(session, trackId, context.userId, Database::FeedbackBackend::ListenBrainz);
|
||||
|
||||
// don't update starred date time
|
||||
// no need to update state if it was found as not synchronized
|
||||
@@ -463,9 +461,9 @@ namespace Feedback::ListenBrainz
|
||||
|
||||
if (needImport)
|
||||
{
|
||||
LOG(DEBUG) << "Importing feedback '" << feedback << "'";
|
||||
LOG(DEBUG, "Importing feedback '" << feedback << "'");
|
||||
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
const Track::pointer track{ Track::find(session, trackId) };
|
||||
if (!track)
|
||||
@@ -483,7 +481,7 @@ namespace Feedback::ListenBrainz
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG(DEBUG) << "No need to import feedback '" << feedback << "', already imported";
|
||||
LOG(DEBUG, "No need to import feedback '" << feedback << "', already imported");
|
||||
context.matchedFeedbackCount++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
#include "services/database/Track.hpp"
|
||||
#include "utils/IConfig.hpp"
|
||||
#include "utils/http/IClient.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
#include "utils/Service.hpp"
|
||||
#include "Utils.hpp"
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace Feedback::ListenBrainz
|
||||
template <typename StarredObjType>
|
||||
void onStarred(Database::Session& session, typename StarredObjType::IdType id)
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
if (auto starredObj{ StarredObjType::find(session, id) })
|
||||
{
|
||||
@@ -49,7 +49,7 @@ namespace Feedback::ListenBrainz
|
||||
template <typename StarredObjType>
|
||||
void onUnstarred(Database::Session& session, typename StarredObjType::IdType id)
|
||||
{
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
if (auto starredObj{ StarredObjType::find(session, id) })
|
||||
starredObj.remove();
|
||||
@@ -63,12 +63,12 @@ namespace Feedback::ListenBrainz
|
||||
, _client{ Http::createClient(_ioContext, _baseAPIUrl) }
|
||||
, _feedbacksSynchronizer{ _ioContext, db, *_client }
|
||||
{
|
||||
LOG(INFO) << "Starting ListenBrainz feedback backend... API endpoint = '" << _baseAPIUrl << "'";
|
||||
LOG(INFO, "Starting ListenBrainz feedback backend... API endpoint = '" << _baseAPIUrl << "'");
|
||||
}
|
||||
|
||||
ListenBrainzBackend::~ListenBrainzBackend()
|
||||
{
|
||||
LOG(INFO) << "Stopped ListenBrainz feedback backend!";
|
||||
LOG(INFO, "Stopped ListenBrainz feedback backend!");
|
||||
}
|
||||
|
||||
void ListenBrainzBackend::onStarred(Database::StarredArtistId starredArtistId)
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace Feedback::ListenBrainz::Utils
|
||||
{
|
||||
std::optional<UUID> getListenBrainzToken(Database::Session& session, Database::UserId userId)
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const Database::User::pointer user{ Database::User::find(session, userId) };
|
||||
if (!user)
|
||||
@@ -46,13 +46,13 @@ namespace Feedback::ListenBrainz::Utils
|
||||
Wt::Json::Object root;
|
||||
if (!Wt::Json::parse(std::string{ msgBody }, root, error))
|
||||
{
|
||||
LOG(ERROR) << "Cannot parse 'validate-token' result: " << error.what();
|
||||
LOG(ERROR, "Cannot parse 'validate-token' result: " << error.what());
|
||||
return listenBrainzUserName;
|
||||
}
|
||||
|
||||
if (!root.get("valid").orIfNull(false))
|
||||
{
|
||||
LOG(INFO) << "Invalid listenbrainz user";
|
||||
LOG(INFO, "Invalid listenbrainz user");
|
||||
return listenBrainzUserName;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,10 +20,10 @@
|
||||
#pragma once
|
||||
|
||||
#include "services/database/UserId.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
#include "utils/UUID.hpp"
|
||||
|
||||
#define LOG(sev) LMS_LOG(FEEDBACK, sev) << "[listenbrainz] "
|
||||
#define LOG(sev, message) LMS_LOG(FEEDBACK, sev, "[listenbrainz] " << message)
|
||||
|
||||
namespace Database
|
||||
{
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
#include "playlist-constraints/ConsecutiveArtists.hpp"
|
||||
#include "playlist-constraints/ConsecutiveReleases.hpp"
|
||||
#include "playlist-constraints/DuplicateTracks.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
|
||||
namespace Recommendation
|
||||
{
|
||||
@@ -48,7 +48,7 @@ namespace Recommendation
|
||||
|
||||
std::vector<TrackId> PlaylistGeneratorService::extendPlaylist(TrackListId tracklistId, std::size_t maxCount) const
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Requested to extend playlist by " << maxCount << " similar tracks";
|
||||
LMS_LOG(RECOMMENDATION, DEBUG, "Requested to extend playlist by " << maxCount << " similar tracks");
|
||||
|
||||
// supposed to be ordered from most similar to least similar
|
||||
std::vector<TrackId> similarTracks{ _recommendationService.findSimilarTracks(tracklistId, maxCount * 2) }; // ask for more tracks than we need as it will be easier to respect constraints
|
||||
@@ -100,7 +100,7 @@ namespace Recommendation
|
||||
TrackContainer tracks;
|
||||
|
||||
Session& dbSession{ _db.getTLSSession() };
|
||||
auto transaction{ dbSession.createSharedTransaction() };
|
||||
auto transaction{ dbSession.createReadTransaction() };
|
||||
|
||||
Track::FindParameters params;
|
||||
params.setTrackList(tracklistId);
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/ScanSettings.hpp"
|
||||
#include "utils/Exception.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
|
||||
namespace Recommendation
|
||||
{
|
||||
@@ -37,7 +37,7 @@ namespace Recommendation
|
||||
{
|
||||
Database::ScanSettings::SimilarityEngineType getSimilarityEngineType(Database::Session& session)
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
return Database::ScanSettings::get(session)->getSimilarityEngineType();
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ namespace Recommendation {
|
||||
return {};
|
||||
|
||||
Session& dbSession{ _db.getTLSSession() };
|
||||
auto transaction{ dbSession.createSharedTransaction() };
|
||||
auto transaction{ dbSession.createReadTransaction() };
|
||||
|
||||
const auto similarTrackIds{ Track::findSimilarTrackIds(dbSession, trackIds, Range {0, maxCount}) };
|
||||
return std::move(similarTrackIds.results);
|
||||
@@ -56,7 +56,7 @@ namespace Recommendation {
|
||||
|
||||
{
|
||||
Session& dbSession{ _db.getTLSSession() };
|
||||
auto transaction{ dbSession.createSharedTransaction() };
|
||||
auto transaction{ dbSession.createReadTransaction() };
|
||||
|
||||
const TrackList::pointer trackList{ TrackList::find(dbSession, tracklistId) };
|
||||
if (!trackList)
|
||||
@@ -78,7 +78,7 @@ namespace Recommendation {
|
||||
|
||||
{
|
||||
Session& dbSession{ _db.getTLSSession() };
|
||||
auto transaction{ dbSession.createSharedTransaction() };
|
||||
auto transaction{ dbSession.createReadTransaction() };
|
||||
|
||||
auto release{ Release::find(dbSession, releaseId) };
|
||||
if (!release)
|
||||
@@ -98,7 +98,7 @@ namespace Recommendation {
|
||||
return {};
|
||||
|
||||
Session& dbSession{ _db.getTLSSession() };
|
||||
auto transaction{ dbSession.createSharedTransaction() };
|
||||
auto transaction{ dbSession.createReadTransaction() };
|
||||
|
||||
auto artist{ Artist::find(dbSession, artistId) };
|
||||
if (!artist)
|
||||
|
||||
@@ -30,397 +30,384 @@
|
||||
#include "services/database/TrackFeatures.hpp"
|
||||
#include "services/database/TrackList.hpp"
|
||||
#include "som/DataNormalizer.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
#include "utils/Random.hpp"
|
||||
|
||||
|
||||
namespace Recommendation {
|
||||
|
||||
using namespace Database;
|
||||
|
||||
std::unique_ptr<IEngine> createFeaturesEngine(Db& db)
|
||||
namespace Recommendation
|
||||
{
|
||||
return std::make_unique<FeaturesEngine>(db);
|
||||
}
|
||||
|
||||
const FeatureSettingsMap&
|
||||
FeaturesEngine::getDefaultTrainFeatureSettings()
|
||||
{
|
||||
static const FeatureSettingsMap defaultTrainFeatureSettings
|
||||
{
|
||||
{ "lowlevel.spectral_energyband_high.mean", {1}},
|
||||
{ "lowlevel.spectral_rolloff.median", {1}},
|
||||
{ "lowlevel.spectral_contrast_valleys.var", {1}},
|
||||
{ "lowlevel.erbbands.mean", {1}},
|
||||
{ "lowlevel.gfcc.mean", {1}},
|
||||
};
|
||||
|
||||
return defaultTrainFeatureSettings;
|
||||
}
|
||||
|
||||
static
|
||||
std::optional<SOM::InputVector>
|
||||
convertFeatureValuesMapToInputVector(const FeatureValuesMap& featureValuesMap, std::size_t nbDimensions)
|
||||
{
|
||||
std::size_t i {};
|
||||
std::optional<SOM::InputVector> res {SOM::InputVector {nbDimensions}};
|
||||
for (const auto& [featureName, values] : featureValuesMap)
|
||||
{
|
||||
if (values.size() != getFeatureDef(featureName).nbDimensions)
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, WARNING) << "Dimension mismatch for feature '" << featureName << "'. Expected " << getFeatureDef(featureName).nbDimensions << ", got " << values.size();
|
||||
res.reset();
|
||||
break;
|
||||
}
|
||||
|
||||
for (double val : values)
|
||||
(*res)[i++] = val;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
static
|
||||
SOM::InputVector
|
||||
getInputVectorWeights(const FeatureSettingsMap& featureSettingsMap, std::size_t nbDimensions)
|
||||
{
|
||||
SOM::InputVector weights {nbDimensions};
|
||||
std::size_t index {};
|
||||
for (const auto& [featureName, featureSettings] : featureSettingsMap)
|
||||
{
|
||||
const std::size_t featureNbDimensions {getFeatureDef(featureName).nbDimensions};
|
||||
|
||||
for (std::size_t i {}; i < featureNbDimensions; ++i)
|
||||
weights[index++] = (1. / featureNbDimensions * featureSettings.weight);
|
||||
}
|
||||
|
||||
assert(index == nbDimensions);
|
||||
|
||||
return weights;
|
||||
}
|
||||
|
||||
void
|
||||
FeaturesEngine::loadFromTraining(const TrainSettings& trainSettings, const ProgressCallback& progressCallback)
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, INFO) << "Constructing features classifier...";
|
||||
|
||||
std::unordered_set<FeatureName> featureNames;
|
||||
std::transform(std::cbegin(trainSettings.featureSettingsMap), std::cend(trainSettings.featureSettingsMap), std::inserter(featureNames, std::begin(featureNames)),
|
||||
[](const auto& itFeatureSetting) { return itFeatureSetting.first; });
|
||||
|
||||
const std::size_t nbDimensions {std::accumulate(std::cbegin(featureNames), std::cend(featureNames), std::size_t {0},
|
||||
[](std::size_t sum, const FeatureName& featureName) { return sum + getFeatureDef(featureName).nbDimensions; })};
|
||||
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Features dimension = " << nbDimensions;
|
||||
|
||||
Session& session {_db.getTLSSession()};
|
||||
|
||||
RangeResults<TrackFeaturesId> trackFeaturesIds;
|
||||
{
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Getting Track features...";
|
||||
trackFeaturesIds = TrackFeatures::find(session);
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Getting Track features DONE (found " << trackFeaturesIds.results.size() << " track features)";
|
||||
}
|
||||
|
||||
std::vector<SOM::InputVector> samples;
|
||||
std::vector<TrackId> samplesTrackIds;
|
||||
|
||||
samples.reserve(trackFeaturesIds.results.size());
|
||||
samplesTrackIds.reserve(trackFeaturesIds.results.size());
|
||||
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Extracting features...";
|
||||
// TODO handle errors using exceptions
|
||||
for (const TrackFeaturesId trackFeaturesId : trackFeaturesIds.results)
|
||||
{
|
||||
if (_loadCancelled)
|
||||
return;
|
||||
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
TrackFeatures::pointer trackFeatures {TrackFeatures::find(session, trackFeaturesId)};
|
||||
if (!trackFeatures)
|
||||
continue;
|
||||
|
||||
FeatureValuesMap featureValuesMap {trackFeatures->getFeatureValuesMap(featureNames)};
|
||||
if (featureValuesMap.empty())
|
||||
continue;
|
||||
|
||||
std::optional<SOM::InputVector> inputVector {convertFeatureValuesMapToInputVector(featureValuesMap, nbDimensions)};
|
||||
if (!inputVector)
|
||||
continue;
|
||||
|
||||
samples.emplace_back(std::move(*inputVector));
|
||||
samplesTrackIds.emplace_back(trackFeatures->getTrack()->getId());
|
||||
}
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Extracting features DONE";
|
||||
|
||||
if (samples.empty())
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, INFO) << "Nothing to classify!";
|
||||
return;
|
||||
}
|
||||
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Normalizing data...";
|
||||
SOM::DataNormalizer dataNormalizer {nbDimensions};
|
||||
|
||||
dataNormalizer.computeNormalizationFactors(samples);
|
||||
for (auto& sample : samples)
|
||||
dataNormalizer.normalizeData(sample);
|
||||
using namespace Database;
|
||||
|
||||
std::unique_ptr<IEngine> createFeaturesEngine(Db& db)
|
||||
{
|
||||
return std::make_unique<FeaturesEngine>(db);
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
std::optional<SOM::InputVector> convertFeatureValuesMapToInputVector(const FeatureValuesMap& featureValuesMap, std::size_t nbDimensions)
|
||||
{
|
||||
std::size_t i{};
|
||||
std::optional<SOM::InputVector> res{ SOM::InputVector {nbDimensions} };
|
||||
for (const auto& [featureName, values] : featureValuesMap)
|
||||
{
|
||||
if (values.size() != getFeatureDef(featureName).nbDimensions)
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, WARNING, "Dimension mismatch for feature '" << featureName << "'. Expected " << getFeatureDef(featureName).nbDimensions << ", got " << values.size());
|
||||
res.reset();
|
||||
break;
|
||||
}
|
||||
|
||||
for (double val : values)
|
||||
(*res)[i++] = val;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
SOM::InputVector getInputVectorWeights(const FeatureSettingsMap& featureSettingsMap, std::size_t nbDimensions)
|
||||
{
|
||||
SOM::InputVector weights{ nbDimensions };
|
||||
std::size_t index{};
|
||||
for (const auto& [featureName, featureSettings] : featureSettingsMap)
|
||||
{
|
||||
const std::size_t featureNbDimensions{ getFeatureDef(featureName).nbDimensions };
|
||||
|
||||
for (std::size_t i{}; i < featureNbDimensions; ++i)
|
||||
weights[index++] = (1. / featureNbDimensions * featureSettings.weight);
|
||||
}
|
||||
|
||||
assert(index == nbDimensions);
|
||||
|
||||
return weights;
|
||||
}
|
||||
}
|
||||
|
||||
const FeatureSettingsMap& FeaturesEngine::getDefaultTrainFeatureSettings()
|
||||
{
|
||||
static const FeatureSettingsMap defaultTrainFeatureSettings
|
||||
{
|
||||
{ "lowlevel.spectral_energyband_high.mean", {1}},
|
||||
{ "lowlevel.spectral_rolloff.median", {1}},
|
||||
{ "lowlevel.spectral_contrast_valleys.var", {1}},
|
||||
{ "lowlevel.erbbands.mean", {1}},
|
||||
{ "lowlevel.gfcc.mean", {1}},
|
||||
};
|
||||
|
||||
return defaultTrainFeatureSettings;
|
||||
}
|
||||
|
||||
void FeaturesEngine::loadFromTraining(const TrainSettings& trainSettings, const ProgressCallback& progressCallback)
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, INFO, "Constructing features classifier...");
|
||||
|
||||
std::unordered_set<FeatureName> featureNames;
|
||||
std::transform(std::cbegin(trainSettings.featureSettingsMap), std::cend(trainSettings.featureSettingsMap), std::inserter(featureNames, std::begin(featureNames)),
|
||||
[](const auto& itFeatureSetting) { return itFeatureSetting.first; });
|
||||
|
||||
const std::size_t nbDimensions{ std::accumulate(std::cbegin(featureNames), std::cend(featureNames), std::size_t {0},
|
||||
[](std::size_t sum, const FeatureName& featureName) { return sum + getFeatureDef(featureName).nbDimensions; }) };
|
||||
|
||||
LMS_LOG(RECOMMENDATION, DEBUG, "Features dimension = " << nbDimensions);
|
||||
|
||||
Session & session{ _db.getTLSSession() };
|
||||
|
||||
RangeResults<TrackFeaturesId> trackFeaturesIds;
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
LMS_LOG(RECOMMENDATION, DEBUG, "Getting Track features...");
|
||||
trackFeaturesIds = TrackFeatures::find(session);
|
||||
LMS_LOG(RECOMMENDATION, DEBUG, "Getting Track features DONE (found " << trackFeaturesIds.results.size() << " track features)");
|
||||
}
|
||||
|
||||
std::vector<SOM::InputVector> samples;
|
||||
std::vector<TrackId> samplesTrackIds;
|
||||
|
||||
samples.reserve(trackFeaturesIds.results.size());
|
||||
samplesTrackIds.reserve(trackFeaturesIds.results.size());
|
||||
|
||||
LMS_LOG(RECOMMENDATION, DEBUG, "Extracting features...");
|
||||
// TODO handle errors using exceptions
|
||||
for (const TrackFeaturesId trackFeaturesId : trackFeaturesIds.results)
|
||||
{
|
||||
if (_loadCancelled)
|
||||
return;
|
||||
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
TrackFeatures::pointer trackFeatures{ TrackFeatures::find(session, trackFeaturesId) };
|
||||
if (!trackFeatures)
|
||||
continue;
|
||||
|
||||
FeatureValuesMap featureValuesMap{ trackFeatures->getFeatureValuesMap(featureNames) };
|
||||
if (featureValuesMap.empty())
|
||||
continue;
|
||||
|
||||
std::optional<SOM::InputVector> inputVector{ convertFeatureValuesMapToInputVector(featureValuesMap, nbDimensions) };
|
||||
if (!inputVector)
|
||||
continue;
|
||||
|
||||
samples.emplace_back(std::move(*inputVector));
|
||||
samplesTrackIds.emplace_back(trackFeatures->getTrack()->getId());
|
||||
}
|
||||
LMS_LOG(RECOMMENDATION, DEBUG, "Extracting features DONE");
|
||||
|
||||
if (samples.empty())
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, INFO, "Nothing to classify!");
|
||||
return;
|
||||
}
|
||||
|
||||
SOM::Coordinate size {static_cast<SOM::Coordinate>(std::sqrt(samples.size() / trainSettings.sampleCountPerNeuron))};
|
||||
if (size < 2)
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, WARNING) << "Very few tracks (" << samples.size() << ") are being used by the features engine, expect bad behaviors";
|
||||
size = 2;
|
||||
}
|
||||
LMS_LOG(RECOMMENDATION, INFO) << "Found " << samples.size() << " tracks, constructing a " << size << "*" << size << " network";
|
||||
LMS_LOG(RECOMMENDATION, DEBUG, "Normalizing data...");
|
||||
SOM::DataNormalizer dataNormalizer{ nbDimensions };
|
||||
|
||||
SOM::Network network {size, size, nbDimensions};
|
||||
|
||||
SOM::InputVector weights {getInputVectorWeights(trainSettings.featureSettingsMap, nbDimensions)};
|
||||
network.setDataWeights(weights);
|
||||
|
||||
auto somProgressCallback{[&](const SOM::Network::CurrentIteration& iter)
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Current pass = " << iter.idIteration << " / " << iter.iterationCount;
|
||||
progressCallback(Progress {iter.idIteration, iter.iterationCount});
|
||||
}};
|
||||
dataNormalizer.computeNormalizationFactors(samples);
|
||||
for (auto& sample : samples)
|
||||
dataNormalizer.normalizeData(sample);
|
||||
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Training network...";
|
||||
network.train(samples, trainSettings.iterationCount,
|
||||
progressCallback ? somProgressCallback : SOM::Network::ProgressCallback {},
|
||||
[this] { return _loadCancelled; });
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Training network DONE";
|
||||
SOM::Coordinate size{ static_cast<SOM::Coordinate>(std::sqrt(samples.size() / trainSettings.sampleCountPerNeuron)) };
|
||||
if (size < 2)
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, WARNING, "Very few tracks (" << samples.size() << ") are being used by the features engine, expect bad behaviors");
|
||||
size = 2;
|
||||
}
|
||||
LMS_LOG(RECOMMENDATION, INFO, "Found " << samples.size() << " tracks, constructing a " << size << "*" << size << " network");
|
||||
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Classifying tracks...";
|
||||
TrackPositions trackPositions;
|
||||
for (std::size_t i {}; i < samples.size(); ++i)
|
||||
{
|
||||
if (_loadCancelled)
|
||||
return;
|
||||
SOM::Network network{ size, size, nbDimensions };
|
||||
|
||||
const SOM::Position position {network.getClosestRefVectorPosition(samples[i])};
|
||||
SOM::InputVector weights{ getInputVectorWeights(trainSettings.featureSettingsMap, nbDimensions) };
|
||||
network.setDataWeights(weights);
|
||||
|
||||
trackPositions[samplesTrackIds[i]].push_back(position);
|
||||
}
|
||||
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Classifying tracks DONE";
|
||||
auto somProgressCallback{ [&](const SOM::Network::CurrentIteration& iter)
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, DEBUG, "Current pass = " << iter.idIteration << " / " << iter.iterationCount);
|
||||
progressCallback(Progress {iter.idIteration, iter.iterationCount});
|
||||
} };
|
||||
|
||||
LMS_LOG(RECOMMENDATION, DEBUG, "Training network...");
|
||||
network.train(samples, trainSettings.iterationCount,
|
||||
progressCallback ? somProgressCallback : SOM::Network::ProgressCallback{},
|
||||
[this] { return _loadCancelled; });
|
||||
LMS_LOG(RECOMMENDATION, DEBUG, "Training network DONE");
|
||||
|
||||
load(std::move(network), std::move(trackPositions));
|
||||
}
|
||||
LMS_LOG(RECOMMENDATION, DEBUG, "Classifying tracks...");
|
||||
TrackPositions trackPositions;
|
||||
for (std::size_t i{}; i < samples.size(); ++i)
|
||||
{
|
||||
if (_loadCancelled)
|
||||
return;
|
||||
|
||||
void
|
||||
FeaturesEngine::loadFromCache(FeaturesEngineCache&& cache)
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, INFO) << "Constructing features classifier from cache...";
|
||||
|
||||
load(std::move(cache._network), cache._trackPositions);
|
||||
}
|
||||
|
||||
TrackContainer
|
||||
FeaturesEngine::findSimilarTracksFromTrackList(TrackListId trackListId, std::size_t maxCount) const
|
||||
{
|
||||
const TrackContainer trackIds {[&]
|
||||
{
|
||||
TrackContainer res;
|
||||
|
||||
Session& session {_db.getTLSSession()};
|
||||
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
const TrackList::pointer trackList {TrackList::find(session, trackListId)};
|
||||
if (trackList)
|
||||
res = trackList->getTrackIds();
|
||||
|
||||
return res;
|
||||
}()};
|
||||
|
||||
return findSimilarTracks(trackIds, maxCount);
|
||||
}
|
||||
|
||||
TrackContainer
|
||||
FeaturesEngine::findSimilarTracks(const std::vector<TrackId>& tracksIds, std::size_t maxCount) const
|
||||
{
|
||||
auto similarTrackIds {getSimilarObjects(tracksIds, _trackMatrix, _trackPositions, maxCount)};
|
||||
|
||||
Session& session {_db.getTLSSession()};
|
||||
|
||||
{
|
||||
// Report only existing ids, as tracks may have been removed a long time ago (refreshing the SOM takes some time)
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
similarTrackIds.erase(std::remove_if(std::begin(similarTrackIds), std::end(similarTrackIds),
|
||||
[&](TrackId trackId)
|
||||
{
|
||||
return !Track::exists(session, trackId);
|
||||
}), std::end(similarTrackIds));
|
||||
}
|
||||
|
||||
return similarTrackIds;
|
||||
}
|
||||
|
||||
ReleaseContainer
|
||||
FeaturesEngine::getSimilarReleases(ReleaseId releaseId, std::size_t maxCount) const
|
||||
{
|
||||
auto similarReleaseIds {getSimilarObjects({releaseId}, _releaseMatrix, _releasePositions, maxCount)};
|
||||
|
||||
Session& session {_db.getTLSSession()};
|
||||
|
||||
if (!similarReleaseIds.empty())
|
||||
{
|
||||
// Report only existing ids
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
similarReleaseIds.erase(std::remove_if(std::begin(similarReleaseIds), std::end(similarReleaseIds),
|
||||
[&](ReleaseId releaseId)
|
||||
{
|
||||
return !Release::exists(session, releaseId);
|
||||
}), std::end(similarReleaseIds));
|
||||
}
|
||||
|
||||
return similarReleaseIds;
|
||||
}
|
||||
|
||||
ArtistContainer
|
||||
FeaturesEngine::getSimilarArtists(ArtistId artistId, EnumSet<TrackArtistLinkType> linkTypes, std::size_t maxCount) const
|
||||
{
|
||||
auto getSimilarArtistIdsForLinkType {[&] (TrackArtistLinkType linkType)
|
||||
{
|
||||
ArtistContainer similarArtistIds;
|
||||
|
||||
const auto itArtists {_artistMatrix.find(linkType)};
|
||||
if (itArtists == std::cend(_artistMatrix))
|
||||
{
|
||||
return similarArtistIds;
|
||||
}
|
||||
|
||||
return getSimilarObjects({artistId}, itArtists->second, _artistPositions, maxCount);
|
||||
}};
|
||||
|
||||
std::unordered_set<ArtistId> similarArtistIds;
|
||||
|
||||
for (TrackArtistLinkType linkType : linkTypes)
|
||||
{
|
||||
const auto similarArtistIdsForLinkType {getSimilarArtistIdsForLinkType(linkType)};
|
||||
similarArtistIds.insert(std::begin(similarArtistIdsForLinkType), std::end(similarArtistIdsForLinkType));
|
||||
}
|
||||
|
||||
ArtistContainer res(std::cbegin(similarArtistIds), std::cend(similarArtistIds));
|
||||
|
||||
Session& session {_db.getTLSSession()};
|
||||
{
|
||||
// Report only existing ids
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
res.erase(std::remove_if(std::begin(res), std::end(res),
|
||||
[&](ArtistId artistId)
|
||||
{
|
||||
return !Artist::exists(session, artistId);
|
||||
}), std::end(res));
|
||||
}
|
||||
|
||||
while (res.size() > maxCount)
|
||||
res.erase(Random::pickRandom(res));
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
FeaturesEngineCache
|
||||
FeaturesEngine::toCache() const
|
||||
{
|
||||
return FeaturesEngineCache {*_network, _trackPositions};
|
||||
}
|
||||
|
||||
void
|
||||
FeaturesEngine::load(bool forceReload, const ProgressCallback& progressCallback)
|
||||
{
|
||||
if (forceReload)
|
||||
{
|
||||
FeaturesEngineCache::invalidate();
|
||||
}
|
||||
else if (std::optional<FeaturesEngineCache> cache {FeaturesEngineCache::read()})
|
||||
{
|
||||
loadFromCache(std::move(*cache));
|
||||
return;
|
||||
}
|
||||
|
||||
TrainSettings trainSettings;
|
||||
trainSettings.featureSettingsMap = getDefaultTrainFeatureSettings();
|
||||
|
||||
loadFromTraining(trainSettings, progressCallback);
|
||||
if (!_loadCancelled && _network)
|
||||
toCache().write();
|
||||
}
|
||||
|
||||
void
|
||||
FeaturesEngine::requestCancelLoad()
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Requesting init cancellation";
|
||||
_loadCancelled = true;
|
||||
}
|
||||
|
||||
void
|
||||
FeaturesEngine::load(const SOM::Network& network, const TrackPositions& trackPositions)
|
||||
{
|
||||
using namespace Database;
|
||||
|
||||
_networkRefVectorsDistanceMedian = network.computeRefVectorsDistanceMedian();
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Median distance betweend ref vectors = " << _networkRefVectorsDistanceMedian;
|
||||
|
||||
const SOM::Coordinate width {network.getWidth()};
|
||||
const SOM::Coordinate height {network.getHeight()};
|
||||
|
||||
_releaseMatrix = ReleaseMatrix {width, height};
|
||||
_trackMatrix = TrackMatrix {width, height};
|
||||
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Constructing maps...";
|
||||
|
||||
Session& session {_db.getTLSSession()};
|
||||
|
||||
for (const auto& [trackId, positions] : trackPositions)
|
||||
{
|
||||
if (_loadCancelled)
|
||||
return;
|
||||
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
|
||||
const Track::pointer track {Track::find(session, trackId)};
|
||||
if (!track)
|
||||
continue;
|
||||
|
||||
for (const SOM::Position& position : positions)
|
||||
{
|
||||
Utils::push_back_if_not_present(_trackPositions[trackId], position);
|
||||
Utils::push_back_if_not_present(_trackMatrix[position], trackId);
|
||||
|
||||
if (Release::pointer release {track->getRelease()})
|
||||
{
|
||||
const ReleaseId releaseId {release->getId()};
|
||||
Utils::push_back_if_not_present(_releasePositions[releaseId], position);
|
||||
Utils::push_back_if_not_present(_releaseMatrix[position], releaseId);
|
||||
}
|
||||
for (const TrackArtistLink::pointer& artistLink : track->getArtistLinks())
|
||||
{
|
||||
const ArtistId artistId {artistLink->getArtist()->getId()};
|
||||
|
||||
Utils::push_back_if_not_present(_artistPositions[artistId], position);
|
||||
auto itArtists {_artistMatrix.find(artistLink->getType())};
|
||||
if (itArtists == std::cend(_artistMatrix))
|
||||
{
|
||||
[[maybe_unused]] auto [it, inserted] = _artistMatrix.try_emplace(artistLink->getType(), ArtistMatrix {width, height});
|
||||
assert(inserted);
|
||||
itArtists = it;
|
||||
}
|
||||
Utils::push_back_if_not_present(itArtists->second[position], artistId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_network = std::make_unique<SOM::Network>(network);
|
||||
|
||||
LMS_LOG(RECOMMENDATION, INFO) << "Classifier successfully loaded!";
|
||||
}
|
||||
const SOM::Position position{ network.getClosestRefVectorPosition(samples[i]) };
|
||||
|
||||
trackPositions[samplesTrackIds[i]].push_back(position);
|
||||
}
|
||||
|
||||
LMS_LOG(RECOMMENDATION, DEBUG, "Classifying tracks DONE");
|
||||
|
||||
load(std::move(network), std::move(trackPositions));
|
||||
}
|
||||
|
||||
void FeaturesEngine::loadFromCache(FeaturesEngineCache&& cache)
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, INFO, "Constructing features classifier from cache...");
|
||||
|
||||
load(std::move(cache._network), cache._trackPositions);
|
||||
}
|
||||
|
||||
TrackContainer FeaturesEngine::findSimilarTracksFromTrackList(TrackListId trackListId, std::size_t maxCount) const
|
||||
{
|
||||
const TrackContainer trackIds{ [&]
|
||||
{
|
||||
TrackContainer res;
|
||||
|
||||
Session& session {_db.getTLSSession()};
|
||||
|
||||
auto transaction {session.createReadTransaction()};
|
||||
|
||||
const TrackList::pointer trackList {TrackList::find(session, trackListId)};
|
||||
if (trackList)
|
||||
res = trackList->getTrackIds();
|
||||
|
||||
return res;
|
||||
}() };
|
||||
|
||||
return findSimilarTracks(trackIds, maxCount);
|
||||
}
|
||||
|
||||
TrackContainer FeaturesEngine::findSimilarTracks(const std::vector<TrackId>& tracksIds, std::size_t maxCount) const
|
||||
{
|
||||
auto similarTrackIds{ getSimilarObjects(tracksIds, _trackMatrix, _trackPositions, maxCount) };
|
||||
|
||||
Session& session{ _db.getTLSSession() };
|
||||
|
||||
{
|
||||
// Report only existing ids, as tracks may have been removed a long time ago (refreshing the SOM takes some time)
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
similarTrackIds.erase(std::remove_if(std::begin(similarTrackIds), std::end(similarTrackIds),
|
||||
[&](TrackId trackId)
|
||||
{
|
||||
return !Track::exists(session, trackId);
|
||||
}), std::end(similarTrackIds));
|
||||
}
|
||||
|
||||
return similarTrackIds;
|
||||
}
|
||||
|
||||
ReleaseContainer FeaturesEngine::getSimilarReleases(ReleaseId releaseId, std::size_t maxCount) const
|
||||
{
|
||||
auto similarReleaseIds{ getSimilarObjects({releaseId}, _releaseMatrix, _releasePositions, maxCount) };
|
||||
|
||||
Session& session{ _db.getTLSSession() };
|
||||
|
||||
if (!similarReleaseIds.empty())
|
||||
{
|
||||
// Report only existing ids
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
similarReleaseIds.erase(std::remove_if(std::begin(similarReleaseIds), std::end(similarReleaseIds),
|
||||
[&](ReleaseId releaseId)
|
||||
{
|
||||
return !Release::exists(session, releaseId);
|
||||
}), std::end(similarReleaseIds));
|
||||
}
|
||||
|
||||
return similarReleaseIds;
|
||||
}
|
||||
|
||||
ArtistContainer FeaturesEngine::getSimilarArtists(ArtistId artistId, EnumSet<TrackArtistLinkType> linkTypes, std::size_t maxCount) const
|
||||
{
|
||||
auto getSimilarArtistIdsForLinkType{ [&](TrackArtistLinkType linkType)
|
||||
{
|
||||
ArtistContainer similarArtistIds;
|
||||
|
||||
const auto itArtists {_artistMatrix.find(linkType)};
|
||||
if (itArtists == std::cend(_artistMatrix))
|
||||
{
|
||||
return similarArtistIds;
|
||||
}
|
||||
|
||||
return getSimilarObjects({artistId}, itArtists->second, _artistPositions, maxCount);
|
||||
} };
|
||||
|
||||
std::unordered_set<ArtistId> similarArtistIds;
|
||||
|
||||
for (TrackArtistLinkType linkType : linkTypes)
|
||||
{
|
||||
const auto similarArtistIdsForLinkType{ getSimilarArtistIdsForLinkType(linkType) };
|
||||
similarArtistIds.insert(std::begin(similarArtistIdsForLinkType), std::end(similarArtistIdsForLinkType));
|
||||
}
|
||||
|
||||
ArtistContainer res(std::cbegin(similarArtistIds), std::cend(similarArtistIds));
|
||||
|
||||
Session& session{ _db.getTLSSession() };
|
||||
{
|
||||
// Report only existing ids
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
res.erase(std::remove_if(std::begin(res), std::end(res),
|
||||
[&](ArtistId artistId)
|
||||
{
|
||||
return !Artist::exists(session, artistId);
|
||||
}), std::end(res));
|
||||
}
|
||||
|
||||
while (res.size() > maxCount)
|
||||
res.erase(Random::pickRandom(res));
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
FeaturesEngineCache FeaturesEngine::toCache() const
|
||||
{
|
||||
return FeaturesEngineCache{ *_network, _trackPositions };
|
||||
}
|
||||
|
||||
void FeaturesEngine::load(bool forceReload, const ProgressCallback& progressCallback)
|
||||
{
|
||||
if (forceReload)
|
||||
{
|
||||
FeaturesEngineCache::invalidate();
|
||||
}
|
||||
else if (std::optional<FeaturesEngineCache> cache{ FeaturesEngineCache::read() })
|
||||
{
|
||||
loadFromCache(std::move(*cache));
|
||||
return;
|
||||
}
|
||||
|
||||
TrainSettings trainSettings;
|
||||
trainSettings.featureSettingsMap = getDefaultTrainFeatureSettings();
|
||||
|
||||
loadFromTraining(trainSettings, progressCallback);
|
||||
if (!_loadCancelled && _network)
|
||||
toCache().write();
|
||||
}
|
||||
|
||||
void FeaturesEngine::requestCancelLoad()
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, DEBUG, "Requesting init cancellation");
|
||||
_loadCancelled = true;
|
||||
}
|
||||
|
||||
void FeaturesEngine::load(const SOM::Network& network, const TrackPositions& trackPositions)
|
||||
{
|
||||
using namespace Database;
|
||||
|
||||
_networkRefVectorsDistanceMedian = network.computeRefVectorsDistanceMedian();
|
||||
LMS_LOG(RECOMMENDATION, DEBUG, "Median distance betweend ref vectors = " << _networkRefVectorsDistanceMedian);
|
||||
|
||||
const SOM::Coordinate width{ network.getWidth() };
|
||||
const SOM::Coordinate height{ network.getHeight() };
|
||||
|
||||
_releaseMatrix = ReleaseMatrix{ width, height };
|
||||
_trackMatrix = TrackMatrix{ width, height };
|
||||
|
||||
LMS_LOG(RECOMMENDATION, DEBUG, "Constructing maps...");
|
||||
|
||||
Session & session{ _db.getTLSSession() };
|
||||
|
||||
for (const auto& [trackId, positions] : trackPositions)
|
||||
{
|
||||
if (_loadCancelled)
|
||||
return;
|
||||
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
const Track::pointer track{ Track::find(session, trackId) };
|
||||
if (!track)
|
||||
continue;
|
||||
|
||||
for (const SOM::Position& position : positions)
|
||||
{
|
||||
Utils::push_back_if_not_present(_trackPositions[trackId], position);
|
||||
Utils::push_back_if_not_present(_trackMatrix[position], trackId);
|
||||
|
||||
if (Release::pointer release{ track->getRelease() })
|
||||
{
|
||||
const ReleaseId releaseId{ release->getId() };
|
||||
Utils::push_back_if_not_present(_releasePositions[releaseId], position);
|
||||
Utils::push_back_if_not_present(_releaseMatrix[position], releaseId);
|
||||
}
|
||||
for (const TrackArtistLink::pointer& artistLink : track->getArtistLinks())
|
||||
{
|
||||
const ArtistId artistId{ artistLink->getArtist()->getId() };
|
||||
|
||||
Utils::push_back_if_not_present(_artistPositions[artistId], position);
|
||||
auto itArtists{ _artistMatrix.find(artistLink->getType()) };
|
||||
if (itArtists == std::cend(_artistMatrix))
|
||||
{
|
||||
[[maybe_unused]] auto [it, inserted] = _artistMatrix.try_emplace(artistLink->getType(), ArtistMatrix{ width, height });
|
||||
assert(inserted);
|
||||
itArtists = it;
|
||||
}
|
||||
Utils::push_back_if_not_present(itArtists->second[position], artistId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_network = std::make_unique<SOM::Network>(network);
|
||||
|
||||
LMS_LOG(RECOMMENDATION, INFO, "Classifier successfully loaded!");
|
||||
}
|
||||
|
||||
} // ns Recommendation
|
||||
|
||||
@@ -23,233 +23,226 @@
|
||||
#include <boost/property_tree/xml_parser.hpp>
|
||||
|
||||
#include "utils/IConfig.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
#include "utils/Service.hpp"
|
||||
|
||||
namespace Recommendation {
|
||||
|
||||
|
||||
static
|
||||
std::filesystem::path getCacheDirectory()
|
||||
namespace Recommendation
|
||||
{
|
||||
return Service<IConfig>::get()->getPath("working-dir") / "cache" / "features";
|
||||
}
|
||||
namespace
|
||||
{
|
||||
std::filesystem::path getCacheDirectory()
|
||||
{
|
||||
return Service<IConfig>::get()->getPath("working-dir") / "cache" / "features";
|
||||
}
|
||||
|
||||
static std::filesystem::path getCacheNetworkFilePath()
|
||||
{
|
||||
return getCacheDirectory() / "network";
|
||||
}
|
||||
std::filesystem::path getCacheNetworkFilePath()
|
||||
{
|
||||
return getCacheDirectory() / "network";
|
||||
}
|
||||
|
||||
static std::filesystem::path getCacheTrackPositionsFilePath()
|
||||
{
|
||||
return getCacheDirectory() / "track_positions";
|
||||
}
|
||||
std::filesystem::path getCacheTrackPositionsFilePath()
|
||||
{
|
||||
return getCacheDirectory() / "track_positions";
|
||||
}
|
||||
|
||||
static
|
||||
bool
|
||||
networkToCacheFile(const SOM::Network& network, std::filesystem::path path)
|
||||
{
|
||||
try
|
||||
{
|
||||
boost::property_tree::ptree root;
|
||||
bool networkToCacheFile(const SOM::Network& network, std::filesystem::path path)
|
||||
{
|
||||
try
|
||||
{
|
||||
boost::property_tree::ptree root;
|
||||
|
||||
root.put("width", network.getWidth());
|
||||
root.put("height", network.getHeight());
|
||||
root.put("dim_count", network.getInputDimCount());
|
||||
root.put("width", network.getWidth());
|
||||
root.put("height", network.getHeight());
|
||||
root.put("dim_count", network.getInputDimCount());
|
||||
|
||||
for (SOM::InputVector::value_type weight : network.getDataWeights())
|
||||
root.add("weights.weight", weight);
|
||||
for (SOM::InputVector::value_type weight : network.getDataWeights())
|
||||
root.add("weights.weight", weight);
|
||||
|
||||
for (SOM::Coordinate x = 0; x < network.getWidth(); ++x)
|
||||
{
|
||||
for (SOM::Coordinate y = 0; y < network.getWidth(); ++y)
|
||||
{
|
||||
const auto& refVector = network.getRefVector({x, y});
|
||||
for (SOM::Coordinate x = 0; x < network.getWidth(); ++x)
|
||||
{
|
||||
for (SOM::Coordinate y = 0; y < network.getWidth(); ++y)
|
||||
{
|
||||
const auto& refVector = network.getRefVector({ x, y });
|
||||
|
||||
boost::property_tree::ptree node;
|
||||
for (auto value : refVector)
|
||||
node.add("values.value", value);
|
||||
boost::property_tree::ptree node;
|
||||
for (const auto& value : refVector)
|
||||
node.add("values.value", value);
|
||||
|
||||
node.put("coord_x", x);
|
||||
node.put("coord_y", y);
|
||||
node.put("coord_x", x);
|
||||
node.put("coord_y", y);
|
||||
|
||||
root.add_child("ref_vectors.ref_vector", node);
|
||||
}
|
||||
}
|
||||
root.add_child("ref_vectors.ref_vector", node);
|
||||
}
|
||||
}
|
||||
|
||||
boost::property_tree::write_xml(path.string(), root);
|
||||
boost::property_tree::write_xml(path.string(), root);
|
||||
|
||||
LMS_LOG(RECOMMENDATION, DEBUG) << "Created network cache";
|
||||
return true;
|
||||
}
|
||||
catch (boost::property_tree::ptree_error& error)
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, ERROR) << "Cannot create network cache: " << error.what();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
LMS_LOG(RECOMMENDATION, DEBUG, "Created network cache");
|
||||
return true;
|
||||
}
|
||||
catch (boost::property_tree::ptree_error& error)
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, ERROR, "Cannot create network cache: " << error.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<SOM::Network>
|
||||
FeaturesEngineCache::createNetworkFromCacheFile(const std::filesystem::path& path)
|
||||
{
|
||||
if (!std::filesystem::exists(path))
|
||||
return std::nullopt;
|
||||
std::optional<SOM::Network> FeaturesEngineCache::createNetworkFromCacheFile(const std::filesystem::path& path)
|
||||
{
|
||||
if (!std::filesystem::exists(path))
|
||||
return std::nullopt;
|
||||
|
||||
try
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, INFO) << "Reading network from cache...";
|
||||
try
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, INFO, "Reading network from cache...");
|
||||
|
||||
boost::property_tree::ptree root;
|
||||
boost::property_tree::ptree root;
|
||||
|
||||
boost::property_tree::read_xml(path.string(), root);
|
||||
boost::property_tree::read_xml(path.string(), root);
|
||||
|
||||
SOM::Coordinate width {root.get<SOM::Coordinate>("width")};
|
||||
SOM::Coordinate height {root.get<SOM::Coordinate>("height")};
|
||||
std::size_t dimCount {root.get<std::size_t>("dim_count")};
|
||||
SOM::Coordinate width{ root.get<SOM::Coordinate>("width") };
|
||||
SOM::Coordinate height{ root.get<SOM::Coordinate>("height") };
|
||||
std::size_t dimCount{ root.get<std::size_t>("dim_count") };
|
||||
|
||||
SOM::Network res {width, height, dimCount};
|
||||
SOM::Network res{ width, height, dimCount };
|
||||
|
||||
{
|
||||
SOM::InputVector weights {dimCount};
|
||||
std::size_t i {};
|
||||
for (const auto& val : root.get_child("weights"))
|
||||
weights[i++] = val.second.get_value<double>();
|
||||
{
|
||||
SOM::InputVector weights{ dimCount };
|
||||
std::size_t i{};
|
||||
for (const auto& val : root.get_child("weights"))
|
||||
weights[i++] = val.second.get_value<double>();
|
||||
|
||||
res.setDataWeights(weights);
|
||||
}
|
||||
res.setDataWeights(weights);
|
||||
}
|
||||
|
||||
for (const auto& node : root.get_child("ref_vectors"))
|
||||
{
|
||||
SOM::Coordinate x {node.second.get<SOM::Coordinate>("coord_x")};
|
||||
SOM::Coordinate y {node.second.get<SOM::Coordinate>("coord_y")};
|
||||
for (const auto& node : root.get_child("ref_vectors"))
|
||||
{
|
||||
SOM::Coordinate x{ node.second.get<SOM::Coordinate>("coord_x") };
|
||||
SOM::Coordinate y{ node.second.get<SOM::Coordinate>("coord_y") };
|
||||
|
||||
SOM::InputVector refVector {dimCount};
|
||||
std::size_t i {};
|
||||
for (const auto& val : node.second.get_child("values"))
|
||||
refVector[i++] = val.second.get_value<SOM::InputVector::value_type>();
|
||||
SOM::InputVector refVector{ dimCount };
|
||||
std::size_t i{};
|
||||
for (const auto& val : node.second.get_child("values"))
|
||||
refVector[i++] = val.second.get_value<SOM::InputVector::value_type>();
|
||||
|
||||
res.setRefVector({x, y}, refVector);
|
||||
}
|
||||
res.setRefVector({ x, y }, refVector);
|
||||
}
|
||||
|
||||
LMS_LOG(RECOMMENDATION, INFO) << "Successfully read network from cache";
|
||||
LMS_LOG(RECOMMENDATION, INFO, "Successfully read network from cache");
|
||||
|
||||
return res;
|
||||
}
|
||||
catch (boost::property_tree::ptree_error& error)
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, ERROR) << "Cannot read network cache: " << error.what();
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
catch (boost::property_tree::ptree_error& error)
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, ERROR, "Cannot read network cache: " << error.what());
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
FeaturesEngineCache::objectPositionToCacheFile(const TrackPositions& trackPositions, const std::filesystem::path& path)
|
||||
{
|
||||
try
|
||||
{
|
||||
boost::property_tree::ptree root;
|
||||
bool FeaturesEngineCache::objectPositionToCacheFile(const TrackPositions& trackPositions, const std::filesystem::path& path)
|
||||
{
|
||||
try
|
||||
{
|
||||
boost::property_tree::ptree root;
|
||||
|
||||
for (const auto& [id, positions] : trackPositions)
|
||||
{
|
||||
boost::property_tree::ptree node;
|
||||
for (const auto& [id, positions] : trackPositions)
|
||||
{
|
||||
boost::property_tree::ptree node;
|
||||
|
||||
node.put("id", id.getValue());
|
||||
node.put("id", id.getValue());
|
||||
|
||||
for (const SOM::Position& position : positions)
|
||||
{
|
||||
boost::property_tree::ptree positionNode;
|
||||
positionNode.put("x", position.x);
|
||||
positionNode.put("y", position.y);
|
||||
for (const SOM::Position& position : positions)
|
||||
{
|
||||
boost::property_tree::ptree positionNode;
|
||||
positionNode.put("x", position.x);
|
||||
positionNode.put("y", position.y);
|
||||
|
||||
node.add_child("position.position", positionNode);
|
||||
}
|
||||
node.add_child("position.position", positionNode);
|
||||
}
|
||||
|
||||
root.add_child("objects.object", node);
|
||||
}
|
||||
root.add_child("objects.object", node);
|
||||
}
|
||||
|
||||
boost::property_tree::write_xml(path.string(), root);
|
||||
return true;
|
||||
}
|
||||
catch (boost::property_tree::ptree_error& error)
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, ERROR) << "Cannot cache object position: " << error.what();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
boost::property_tree::write_xml(path.string(), root);
|
||||
return true;
|
||||
}
|
||||
catch (boost::property_tree::ptree_error& error)
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, ERROR, "Cannot cache object position: " << error.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<FeaturesEngineCache::TrackPositions>
|
||||
FeaturesEngineCache::createObjectPositionsFromCacheFile(const std::filesystem::path& path)
|
||||
{
|
||||
try
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, INFO) << "Reading object position from cache...";
|
||||
std::optional<FeaturesEngineCache::TrackPositions> FeaturesEngineCache::createObjectPositionsFromCacheFile(const std::filesystem::path& path)
|
||||
{
|
||||
try
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, INFO, "Reading object position from cache...");
|
||||
|
||||
boost::property_tree::ptree root;
|
||||
boost::property_tree::ptree root;
|
||||
|
||||
boost::property_tree::read_xml(path.string(), root);
|
||||
boost::property_tree::read_xml(path.string(), root);
|
||||
|
||||
TrackPositions res;
|
||||
TrackPositions res;
|
||||
|
||||
for (const auto& object : root.get_child("objects"))
|
||||
{
|
||||
const Database::TrackId id {object.second.get<Database::IdType::ValueType>("id")};
|
||||
for (const auto& position : object.second.get_child("position"))
|
||||
{
|
||||
auto x = position.second.get<SOM::Coordinate>("x");
|
||||
auto y = position.second.get<SOM::Coordinate>("y");
|
||||
for (const auto& object : root.get_child("objects"))
|
||||
{
|
||||
const Database::TrackId id{ object.second.get<Database::IdType::ValueType>("id") };
|
||||
for (const auto& position : object.second.get_child("position"))
|
||||
{
|
||||
auto x = position.second.get<SOM::Coordinate>("x");
|
||||
auto y = position.second.get<SOM::Coordinate>("y");
|
||||
|
||||
res[id].push_back({x, y});
|
||||
}
|
||||
}
|
||||
res[id].push_back({ x, y });
|
||||
}
|
||||
}
|
||||
|
||||
LMS_LOG(RECOMMENDATION, INFO) << "Successfully read object position from cache";
|
||||
LMS_LOG(RECOMMENDATION, INFO, "Successfully read object position from cache");
|
||||
|
||||
return res;
|
||||
}
|
||||
catch (boost::property_tree::ptree_error& error)
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, ERROR) << "Cannot create object position from cache file: " << error.what();
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
catch (boost::property_tree::ptree_error& error)
|
||||
{
|
||||
LMS_LOG(RECOMMENDATION, ERROR, "Cannot create object position from cache file: " << error.what());
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
FeaturesEngineCache::invalidate()
|
||||
{
|
||||
std::filesystem::remove(getCacheNetworkFilePath());
|
||||
std::filesystem::remove(getCacheTrackPositionsFilePath());
|
||||
}
|
||||
void FeaturesEngineCache::invalidate()
|
||||
{
|
||||
std::filesystem::remove(getCacheNetworkFilePath());
|
||||
std::filesystem::remove(getCacheTrackPositionsFilePath());
|
||||
}
|
||||
|
||||
std::optional<FeaturesEngineCache>
|
||||
FeaturesEngineCache::read()
|
||||
{
|
||||
auto network{createNetworkFromCacheFile(getCacheNetworkFilePath())};
|
||||
if (!network)
|
||||
return std::nullopt;
|
||||
std::optional<FeaturesEngineCache> FeaturesEngineCache::read()
|
||||
{
|
||||
auto network{ createNetworkFromCacheFile(getCacheNetworkFilePath()) };
|
||||
if (!network)
|
||||
return std::nullopt;
|
||||
|
||||
auto trackPositions{createObjectPositionsFromCacheFile(getCacheTrackPositionsFilePath())};
|
||||
if (!trackPositions)
|
||||
return std::nullopt;
|
||||
auto trackPositions{ createObjectPositionsFromCacheFile(getCacheTrackPositionsFilePath()) };
|
||||
if (!trackPositions)
|
||||
return std::nullopt;
|
||||
|
||||
return FeaturesEngineCache {std::move(*network), std::move(*trackPositions)};
|
||||
}
|
||||
return FeaturesEngineCache{ std::move(*network), std::move(*trackPositions) };
|
||||
}
|
||||
|
||||
void
|
||||
FeaturesEngineCache::write() const
|
||||
{
|
||||
std::filesystem::create_directories(Service<IConfig>::get()->getPath("working-dir") / "cache" / "features");
|
||||
void FeaturesEngineCache::write() const
|
||||
{
|
||||
std::filesystem::create_directories(Service<IConfig>::get()->getPath("working-dir") / "cache" / "features");
|
||||
|
||||
if (!networkToCacheFile(_network, getCacheNetworkFilePath())
|
||||
|| !objectPositionToCacheFile(_trackPositions, getCacheTrackPositionsFilePath()))
|
||||
{
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
if (!networkToCacheFile(_network, getCacheNetworkFilePath())
|
||||
|| !objectPositionToCacheFile(_trackPositions, getCacheTrackPositionsFilePath()))
|
||||
{
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
FeaturesEngineCache::FeaturesEngineCache(SOM::Network network, TrackPositions trackPositions)
|
||||
: _network {std::move(network)},
|
||||
_trackPositions {std::move(trackPositions)}
|
||||
{
|
||||
}
|
||||
FeaturesEngineCache::FeaturesEngineCache(SOM::Network network, TrackPositions trackPositions)
|
||||
: _network{ std::move(network) },
|
||||
_trackPositions{ std::move(trackPositions) }
|
||||
{
|
||||
}
|
||||
|
||||
} // namespace Recommendation
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
#include "services/database/Release.hpp"
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/Track.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
|
||||
namespace Recommendation::PlaylistGeneratorConstraint
|
||||
{
|
||||
@@ -80,7 +80,7 @@ namespace Recommendation::PlaylistGeneratorConstraint
|
||||
ArtistContainer res;
|
||||
|
||||
Session& dbSession {_db.getTLSSession()};
|
||||
auto transaction {dbSession.createSharedTransaction()};
|
||||
auto transaction {dbSession.createReadTransaction()};
|
||||
|
||||
const Track::pointer track {Track::find(dbSession, trackId)};
|
||||
if (!track)
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
#include "services/database/Release.hpp"
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/Track.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
|
||||
namespace Recommendation::PlaylistGeneratorConstraint
|
||||
{
|
||||
@@ -61,7 +61,7 @@ namespace Recommendation::PlaylistGeneratorConstraint
|
||||
using namespace Database;
|
||||
|
||||
Session& dbSession {_db.getTLSSession()};
|
||||
auto transaction {dbSession.createSharedTransaction()};
|
||||
auto transaction {dbSession.createReadTransaction()};
|
||||
|
||||
const Track::pointer track {Track::find(dbSession, trackId)};
|
||||
if (!track)
|
||||
|
||||
@@ -22,34 +22,36 @@
|
||||
#include "services/database/Db.hpp"
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/Track.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
|
||||
namespace Scanner
|
||||
{
|
||||
void
|
||||
ScanStepCheckDuplicatedDbFiles::process(ScanContext& context)
|
||||
{
|
||||
using namespace Database;
|
||||
void ScanStepCheckDuplicatedDbFiles::process(ScanContext& context)
|
||||
{
|
||||
using namespace Database;
|
||||
|
||||
if (_abortScan)
|
||||
return;
|
||||
if (_abortScan)
|
||||
return;
|
||||
|
||||
Session& session {_db.getTLSSession()};
|
||||
auto transaction {session.createSharedTransaction()};
|
||||
auto transaction {session.createReadTransaction()};
|
||||
|
||||
const RangeResults<TrackId> tracks = Track::findIdsTrackMBIDDuplicates(session);
|
||||
for (const TrackId trackId : tracks.results)
|
||||
{
|
||||
const Track::pointer track {Track::find(session, trackId)};
|
||||
if (auto trackMBID {track->getTrackMBID()})
|
||||
{
|
||||
LMS_LOG(DBUPDATER, INFO) << "Found duplicated track MBID [" << trackMBID->getAsString() << "], file: " << track->getPath().string() << " - " << track->getName();
|
||||
context.stats.duplicates.emplace_back(ScanDuplicate {track->getId(), DuplicateReason::SameTrackMBID});
|
||||
context.currentStepStats.processedElems++;
|
||||
_progressCallback(context.currentStepStats);
|
||||
}
|
||||
}
|
||||
const RangeResults<TrackId> tracks = Track::findIdsTrackMBIDDuplicates(session);
|
||||
for (const TrackId trackId : tracks.results)
|
||||
{
|
||||
if (_abortScan)
|
||||
break;
|
||||
|
||||
LMS_LOG(DBUPDATER, DEBUG) << "Found " << context.currentStepStats.processedElems << " duplicated audio files";
|
||||
}
|
||||
const Track::pointer track{ Track::find(session, trackId) };
|
||||
if (auto trackMBID{ track->getTrackMBID() })
|
||||
{
|
||||
LMS_LOG(DBUPDATER, INFO, "Found duplicated track MBID [" << trackMBID->getAsString() << "], file: " << track->getPath().string() << " - " << track->getName());
|
||||
context.stats.duplicates.emplace_back(ScanDuplicate{ track->getId(), DuplicateReason::SameTrackMBID });
|
||||
context.currentStepStats.processedElems++;
|
||||
_progressCallback(context.currentStepStats);
|
||||
}
|
||||
}
|
||||
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Found " << context.currentStepStats.processedElems << " duplicated audio files");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
#include "services/database/Db.hpp"
|
||||
#include "services/database/Cluster.hpp"
|
||||
#include "services/database/Session.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
#include "utils/Path.hpp"
|
||||
|
||||
namespace Scanner
|
||||
@@ -36,7 +36,7 @@ namespace Scanner
|
||||
Session& dbSession{ _db.getTLSSession() };
|
||||
|
||||
const std::size_t clusterCount{ [&] {
|
||||
auto transaction{ dbSession.createSharedTransaction() };
|
||||
auto transaction{ dbSession.createReadTransaction() };
|
||||
return Cluster::getCount(dbSession);
|
||||
}() };
|
||||
|
||||
@@ -50,25 +50,28 @@ namespace Scanner
|
||||
params.setRange(range);
|
||||
|
||||
{
|
||||
auto transaction{ dbSession.createSharedTransaction() };
|
||||
auto transaction{ dbSession.createReadTransaction() };
|
||||
return std::move(Cluster::findIds(dbSession, params).results);
|
||||
}
|
||||
}() };
|
||||
|
||||
for (const ClusterId clusterId : clusterIds)
|
||||
{
|
||||
if (_abortScan)
|
||||
break;
|
||||
|
||||
std::size_t trackCount;
|
||||
std::size_t releaseCount;
|
||||
|
||||
{
|
||||
auto transaction{ dbSession.createSharedTransaction() };
|
||||
auto transaction{ dbSession.createReadTransaction() };
|
||||
|
||||
trackCount = Cluster::computeTrackCount(dbSession, clusterId);
|
||||
releaseCount = Cluster::computeReleaseCount(dbSession, clusterId);
|
||||
}
|
||||
|
||||
{
|
||||
auto transaction{ dbSession.createUniqueTransaction() };
|
||||
auto transaction{ dbSession.createWriteTransaction() };
|
||||
|
||||
auto cluster{ Cluster::find(dbSession, clusterId) };
|
||||
cluster.modify()->setTrackCount(trackCount);
|
||||
@@ -81,6 +84,6 @@ namespace Scanner
|
||||
return true;
|
||||
});
|
||||
|
||||
LMS_LOG(DBUPDATER, DEBUG) << "Recomputed stats for " << clusterCount << " clusters!";
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Recomputed stats for " << context.currentStepStats.processedElems << " clusters!");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,31 +18,30 @@
|
||||
*/
|
||||
|
||||
#include "ScanStepDiscoverFiles.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
#include "utils/Path.hpp"
|
||||
|
||||
namespace Scanner
|
||||
{
|
||||
void
|
||||
ScanStepDiscoverFiles::process(ScanContext& context)
|
||||
{
|
||||
context.stats.filesScanned = 0;
|
||||
PathUtils::exploreFilesRecursive(context.directory, [&](std::error_code ec, const std::filesystem::path& path)
|
||||
{
|
||||
if (_abortScan)
|
||||
return false;
|
||||
void ScanStepDiscoverFiles::process(ScanContext& context)
|
||||
{
|
||||
context.stats.filesScanned = 0;
|
||||
PathUtils::exploreFilesRecursive(context.directory, [&](std::error_code ec, const std::filesystem::path& path)
|
||||
{
|
||||
if (_abortScan)
|
||||
return false;
|
||||
|
||||
if (!ec && PathUtils::hasFileAnyExtension(path, _settings.supportedExtensions))
|
||||
{
|
||||
context.currentStepStats.processedElems++;
|
||||
_progressCallback(context.currentStepStats);
|
||||
}
|
||||
if (!ec && PathUtils::hasFileAnyExtension(path, _settings.supportedExtensions))
|
||||
{
|
||||
context.currentStepStats.processedElems++;
|
||||
_progressCallback(context.currentStepStats);
|
||||
}
|
||||
|
||||
return true;
|
||||
}, &excludeDirFileName);
|
||||
return true;
|
||||
}, &excludeDirFileName);
|
||||
|
||||
context.stats.filesScanned = context.currentStepStats.processedElems;
|
||||
context.stats.filesScanned = context.currentStepStats.processedElems;
|
||||
|
||||
LMS_LOG(DBUPDATER, DEBUG) << "Discovered " << context.stats.filesScanned << " files in '" << context.directory << "'";
|
||||
}
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Discovered " << context.stats.filesScanned << " files in '" << context.directory << "'");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,15 +25,56 @@
|
||||
#include "services/database/Release.hpp"
|
||||
#include "services/database/Session.hpp"
|
||||
#include "services/database/Track.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
#include "utils/Path.hpp"
|
||||
|
||||
namespace Scanner
|
||||
{
|
||||
using namespace Database;
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr std::size_t batchSize = 100;
|
||||
|
||||
template <typename T>
|
||||
void removeOrphanEntries(Session& session, bool& abortScan)
|
||||
{
|
||||
using IdType = typename T::IdType;
|
||||
|
||||
RangeResults<IdType> entries;
|
||||
while (!abortScan)
|
||||
{
|
||||
{
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
|
||||
entries = T::findOrphanIds(session, Range{ 0, batchSize });
|
||||
};
|
||||
|
||||
{
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
for (const IdType objectId : entries.results)
|
||||
{
|
||||
if (abortScan)
|
||||
break;
|
||||
|
||||
typename T::pointer entry{ T::find(session, objectId) };
|
||||
|
||||
entry.remove();
|
||||
}
|
||||
}
|
||||
|
||||
if (!entries.moreResults)
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ScanStepRemoveOrphanDbFiles::process(ScanContext& context)
|
||||
{
|
||||
removeOrphanTracks(context);
|
||||
removeOrphanClusters();
|
||||
removeOrphanClusterTypes();
|
||||
removeOrphanArtists();
|
||||
removeOrphanReleases();
|
||||
}
|
||||
@@ -45,17 +86,16 @@ namespace Scanner
|
||||
if (_abortScan)
|
||||
return;
|
||||
|
||||
static constexpr std::size_t batchSize{ 50 };
|
||||
Session& session{ _db.getTLSSession() };
|
||||
|
||||
LMS_LOG(DBUPDATER, DEBUG) << "Checking tracks to be removed...";
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Checking tracks to be removed...");
|
||||
std::size_t trackCount{};
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
trackCount = Track::getCount(session);
|
||||
}
|
||||
LMS_LOG(DBUPDATER, DEBUG) << trackCount << " tracks to be checked...";
|
||||
LMS_LOG(DBUPDATER, DEBUG, trackCount << " tracks to be checked...");
|
||||
|
||||
context.currentStepStats.totalElems = trackCount;
|
||||
|
||||
@@ -68,7 +108,7 @@ namespace Scanner
|
||||
tracksToRemove.clear();
|
||||
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createReadTransaction() };
|
||||
trackPaths = Track::findPaths(session, Range{ i, batchSize });
|
||||
}
|
||||
|
||||
@@ -85,7 +125,7 @@ namespace Scanner
|
||||
|
||||
if (!tracksToRemove.empty())
|
||||
{
|
||||
auto transaction{ session.createSharedTransaction() };
|
||||
auto transaction{ session.createWriteTransaction() };
|
||||
|
||||
for (const TrackId trackId : tracksToRemove)
|
||||
{
|
||||
@@ -104,62 +144,31 @@ namespace Scanner
|
||||
break;
|
||||
}
|
||||
|
||||
LMS_LOG(DBUPDATER, DEBUG) << trackCount << " tracks checked!";
|
||||
LMS_LOG(DBUPDATER, DEBUG, trackCount << " tracks checked!");
|
||||
}
|
||||
|
||||
void ScanStepRemoveOrphanDbFiles::removeOrphanClusters()
|
||||
{
|
||||
using namespace Database;
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Checking orphan clusters...");
|
||||
removeOrphanEntries<Database::Cluster>(_db.getTLSSession(), _abortScan);
|
||||
}
|
||||
|
||||
LMS_LOG(DBUPDATER, DEBUG) << "Checking orphan clusters...";
|
||||
Session& session{ _db.getTLSSession() };
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
|
||||
// Now process orphan Cluster (no track)
|
||||
auto clusterIds{ Cluster::findOrphans(session) };
|
||||
for (ClusterId clusterId : clusterIds.results)
|
||||
{
|
||||
Cluster::pointer cluster{ Cluster::find(session, clusterId) };
|
||||
LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan cluster '" << cluster->getName() << "'";
|
||||
cluster.remove();
|
||||
}
|
||||
void ScanStepRemoveOrphanDbFiles::removeOrphanClusterTypes()
|
||||
{
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Checking orphan cluster types...");
|
||||
removeOrphanEntries<Database::ClusterType>(_db.getTLSSession(), _abortScan);
|
||||
}
|
||||
|
||||
void ScanStepRemoveOrphanDbFiles::removeOrphanArtists()
|
||||
{
|
||||
using namespace Database;
|
||||
|
||||
LMS_LOG(DBUPDATER, DEBUG) << "Checking orphan artists...";
|
||||
|
||||
Session& session{ _db.getTLSSession() };
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
|
||||
auto artistIds{ Artist::findOrphanIds(session) };
|
||||
for (const ArtistId artistId : artistIds.results)
|
||||
{
|
||||
Artist::pointer artist{ Artist::find(session, artistId) };
|
||||
LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan artist '" << artist->getName() << "'";
|
||||
artist.remove();
|
||||
}
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Checking orphan artists...");
|
||||
removeOrphanEntries<Database::Artist>(_db.getTLSSession(), _abortScan);
|
||||
}
|
||||
|
||||
void ScanStepRemoveOrphanDbFiles::removeOrphanReleases()
|
||||
{
|
||||
using namespace Database;
|
||||
|
||||
LMS_LOG(DBUPDATER, DEBUG) << "Checking orphan releases...";
|
||||
|
||||
// TODO, by batch
|
||||
Session& session{ _db.getTLSSession() };
|
||||
auto transaction{ session.createUniqueTransaction() };
|
||||
|
||||
auto releases{ Release::findOrphanIds(session) };
|
||||
for (const ReleaseId releaseId : releases.results)
|
||||
{
|
||||
Release::pointer release{ Release::find(session, releaseId) };
|
||||
LMS_LOG(DBUPDATER, DEBUG) << "Removing orphan release '" << release->getName() << "'";
|
||||
release.remove();
|
||||
}
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Checking orphan releases...");
|
||||
removeOrphanEntries<Database::Release>(_db.getTLSSession(), _abortScan);
|
||||
}
|
||||
|
||||
bool ScanStepRemoveOrphanDbFiles::checkFile(const std::filesystem::path& p)
|
||||
@@ -168,22 +177,21 @@ namespace Scanner
|
||||
{
|
||||
// For each track, make sure the the file still exists
|
||||
// and still belongs to a media directory
|
||||
if (!std::filesystem::exists(p)
|
||||
|| !std::filesystem::is_regular_file(p))
|
||||
if (!std::filesystem::exists(p) || !std::filesystem::is_regular_file(p))
|
||||
{
|
||||
LMS_LOG(DBUPDATER, INFO) << "Removing '" << p.string() << "': missing";
|
||||
LMS_LOG(DBUPDATER, INFO, "Removing '" << p.string() << "': missing");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!PathUtils::isPathInRootPath(p, _settings.mediaDirectory, &excludeDirFileName))
|
||||
{
|
||||
LMS_LOG(DBUPDATER, INFO) << "Removing '" << p.string() << "': out of media directory";
|
||||
LMS_LOG(DBUPDATER, INFO, "Removing '" << p.string() << "': out of media directory");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!PathUtils::hasFileAnyExtension(p, _settings.supportedExtensions))
|
||||
{
|
||||
LMS_LOG(DBUPDATER, INFO) << "Removing '" << p.string() << "': file format no longer handled";
|
||||
LMS_LOG(DBUPDATER, INFO, "Removing '" << p.string() << "': file format no longer handled");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -191,7 +199,7 @@ namespace Scanner
|
||||
}
|
||||
catch (std::filesystem::filesystem_error& e)
|
||||
{
|
||||
LMS_LOG(DBUPDATER, ERROR) << "Caught exception while checking file '" << p.string() << "': " << e.what();
|
||||
LMS_LOG(DBUPDATER, ERROR, "Caught exception while checking file '" << p.string() << "': " << e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ namespace Scanner
|
||||
|
||||
void removeOrphanTracks(ScanContext& context);
|
||||
void removeOrphanClusters();
|
||||
void removeOrphanClusterTypes();
|
||||
void removeOrphanArtists();
|
||||
void removeOrphanReleases();
|
||||
bool checkFile(const std::filesystem::path& p);
|
||||
|
||||
@@ -30,264 +30,259 @@
|
||||
#include "services/database/TrackArtistLink.hpp"
|
||||
#include "utils/Exception.hpp"
|
||||
#include "utils/IConfig.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
#include "utils/Path.hpp"
|
||||
|
||||
using namespace Database;
|
||||
|
||||
namespace
|
||||
namespace Scanner
|
||||
{
|
||||
Artist::pointer
|
||||
createArtist(Session& session, const MetaData::Artist& artistInfo)
|
||||
namespace
|
||||
{
|
||||
Artist::pointer artist{ session.create<Artist>(artistInfo.name) };
|
||||
|
||||
if (artistInfo.mbid)
|
||||
artist.modify()->setMBID(*artistInfo.mbid);
|
||||
if (artistInfo.sortName)
|
||||
artist.modify()->setSortName(*artistInfo.sortName);
|
||||
|
||||
return artist;
|
||||
}
|
||||
|
||||
void
|
||||
updateArtistIfNeeded(Artist::pointer artist, const MetaData::Artist& artistInfo)
|
||||
{
|
||||
// Name may have been updated
|
||||
if (artist->getName() != artistInfo.name)
|
||||
Artist::pointer createArtist(Session& session, const MetaData::Artist& artistInfo)
|
||||
{
|
||||
artist.modify()->setName(artistInfo.name);
|
||||
}
|
||||
Artist::pointer artist{ session.create<Artist>(artistInfo.name) };
|
||||
|
||||
// Sortname may have been updated
|
||||
if (artistInfo.sortName && *artistInfo.sortName != artist->getSortName())
|
||||
{
|
||||
artist.modify()->setSortName(*artistInfo.sortName);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<Artist::pointer>
|
||||
getOrCreateArtists(Session& session, const std::vector<MetaData::Artist>& artistsInfo, bool allowFallbackOnMBIDEntries)
|
||||
{
|
||||
std::vector<Artist::pointer> artists;
|
||||
|
||||
for (const MetaData::Artist& artistInfo : artistsInfo)
|
||||
{
|
||||
Artist::pointer artist;
|
||||
|
||||
// First try to get by MBID
|
||||
if (artistInfo.mbid)
|
||||
{
|
||||
artist = Artist::find(session, *artistInfo.mbid);
|
||||
if (!artist)
|
||||
artist = createArtist(session, artistInfo);
|
||||
else
|
||||
updateArtistIfNeeded(artist, artistInfo);
|
||||
artist.modify()->setMBID(*artistInfo.mbid);
|
||||
if (artistInfo.sortName)
|
||||
artist.modify()->setSortName(*artistInfo.sortName);
|
||||
|
||||
artists.emplace_back(std::move(artist));
|
||||
continue;
|
||||
return artist;
|
||||
}
|
||||
|
||||
void updateArtistIfNeeded(Artist::pointer artist, const MetaData::Artist& artistInfo)
|
||||
{
|
||||
// Name may have been updated
|
||||
if (artist->getName() != artistInfo.name)
|
||||
{
|
||||
artist.modify()->setName(artistInfo.name);
|
||||
}
|
||||
|
||||
// Fall back on artist name (collisions may occur)
|
||||
if (!artistInfo.name.empty())
|
||||
// Sortname may have been updated
|
||||
if (artistInfo.sortName && *artistInfo.sortName != artist->getSortName())
|
||||
{
|
||||
for (const Artist::pointer& sameNamedArtist : Artist::find(session, artistInfo.name))
|
||||
{
|
||||
// Do not fallback on artist that is correctly tagged
|
||||
if (!allowFallbackOnMBIDEntries && sameNamedArtist->getMBID())
|
||||
continue;
|
||||
artist.modify()->setSortName(*artistInfo.sortName);
|
||||
}
|
||||
}
|
||||
|
||||
artist = sameNamedArtist;
|
||||
break;
|
||||
std::vector<Artist::pointer> getOrCreateArtists(Session& session, const std::vector<MetaData::Artist>& artistsInfo, bool allowFallbackOnMBIDEntries)
|
||||
{
|
||||
std::vector<Artist::pointer> artists;
|
||||
|
||||
for (const MetaData::Artist& artistInfo : artistsInfo)
|
||||
{
|
||||
Artist::pointer artist;
|
||||
|
||||
// First try to get by MBID
|
||||
if (artistInfo.mbid)
|
||||
{
|
||||
artist = Artist::find(session, *artistInfo.mbid);
|
||||
if (!artist)
|
||||
artist = createArtist(session, artistInfo);
|
||||
else
|
||||
updateArtistIfNeeded(artist, artistInfo);
|
||||
|
||||
artists.emplace_back(std::move(artist));
|
||||
continue;
|
||||
}
|
||||
|
||||
// No Artist found with the same name and without MBID -> creating
|
||||
if (!artist)
|
||||
artist = createArtist(session, artistInfo);
|
||||
else
|
||||
updateArtistIfNeeded(artist, artistInfo);
|
||||
// Fall back on artist name (collisions may occur)
|
||||
if (!artistInfo.name.empty())
|
||||
{
|
||||
for (const Artist::pointer& sameNamedArtist : Artist::find(session, artistInfo.name))
|
||||
{
|
||||
// Do not fallback on artist that is correctly tagged
|
||||
if (!allowFallbackOnMBIDEntries && sameNamedArtist->getMBID())
|
||||
continue;
|
||||
|
||||
artists.emplace_back(std::move(artist));
|
||||
continue;
|
||||
artist = sameNamedArtist;
|
||||
break;
|
||||
}
|
||||
|
||||
// No Artist found with the same name and without MBID -> creating
|
||||
if (!artist)
|
||||
artist = createArtist(session, artistInfo);
|
||||
else
|
||||
updateArtistIfNeeded(artist, artistInfo);
|
||||
|
||||
artists.emplace_back(std::move(artist));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return artists;
|
||||
}
|
||||
|
||||
return artists;
|
||||
}
|
||||
|
||||
ReleaseTypePrimary convertReleaseTypePrimary(MetaData::Release::PrimaryType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case MetaData::Release::PrimaryType::Album: return ReleaseTypePrimary::Album;
|
||||
case MetaData::Release::PrimaryType::Single: return ReleaseTypePrimary::Single;
|
||||
case MetaData::Release::PrimaryType::EP: return ReleaseTypePrimary::EP;
|
||||
case MetaData::Release::PrimaryType::Broadcast: return ReleaseTypePrimary::Broadcast;
|
||||
case MetaData::Release::PrimaryType::Other: return ReleaseTypePrimary::Other;
|
||||
}
|
||||
|
||||
return ReleaseTypePrimary::Other;
|
||||
}
|
||||
|
||||
EnumSet<ReleaseTypeSecondary> convertReleaseTypesSecondary(EnumSet<MetaData::Release::SecondaryType> types)
|
||||
{
|
||||
EnumSet<ReleaseTypeSecondary> res;
|
||||
|
||||
for (MetaData::Release::SecondaryType type : types)
|
||||
ReleaseTypePrimary convertReleaseTypePrimary(MetaData::Release::PrimaryType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case MetaData::Release::SecondaryType::Compilation:
|
||||
res.insert(ReleaseTypeSecondary::Compilation);
|
||||
break;
|
||||
case MetaData::Release::SecondaryType::Soundtrack:
|
||||
res.insert(ReleaseTypeSecondary::Soundtrack);
|
||||
break;
|
||||
case MetaData::Release::SecondaryType::Spokenword:
|
||||
res.insert(ReleaseTypeSecondary::Spokenword);
|
||||
break;
|
||||
case MetaData::Release::SecondaryType::Interview:
|
||||
res.insert(ReleaseTypeSecondary::Interview);
|
||||
break;
|
||||
case MetaData::Release::SecondaryType::Audiobook:
|
||||
res.insert(ReleaseTypeSecondary::Audiobook);
|
||||
break;
|
||||
case MetaData::Release::SecondaryType::AudioDrama:
|
||||
res.insert(ReleaseTypeSecondary::AudioDrama);
|
||||
break;
|
||||
case MetaData::Release::SecondaryType::Live:
|
||||
res.insert(ReleaseTypeSecondary::Live);
|
||||
break;
|
||||
case MetaData::Release::SecondaryType::Remix:
|
||||
res.insert(ReleaseTypeSecondary::Remix);
|
||||
break;
|
||||
case MetaData::Release::SecondaryType::DJMix:
|
||||
res.insert(ReleaseTypeSecondary::DJMix);
|
||||
break;
|
||||
case MetaData::Release::SecondaryType::Mixtape_Street:
|
||||
res.insert(ReleaseTypeSecondary::Mixtape_Street);
|
||||
break;
|
||||
case MetaData::Release::SecondaryType::Demo:
|
||||
res.insert(ReleaseTypeSecondary::Demo);
|
||||
break;
|
||||
case MetaData::Release::PrimaryType::Album: return ReleaseTypePrimary::Album;
|
||||
case MetaData::Release::PrimaryType::Single: return ReleaseTypePrimary::Single;
|
||||
case MetaData::Release::PrimaryType::EP: return ReleaseTypePrimary::EP;
|
||||
case MetaData::Release::PrimaryType::Broadcast: return ReleaseTypePrimary::Broadcast;
|
||||
case MetaData::Release::PrimaryType::Other: return ReleaseTypePrimary::Other;
|
||||
}
|
||||
|
||||
return ReleaseTypePrimary::Other;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
void
|
||||
updateReleaseIfNeeded(Release::pointer release, const MetaData::Release& releaseInfo)
|
||||
{
|
||||
if (release->getName() != releaseInfo.name)
|
||||
release.modify()->setName(releaseInfo.name);
|
||||
if (release->getTotalDisc() != releaseInfo.mediumCount)
|
||||
release.modify()->setTotalDisc(releaseInfo.mediumCount);
|
||||
if (releaseInfo.primaryType)
|
||||
EnumSet<ReleaseTypeSecondary> convertReleaseTypesSecondary(EnumSet<MetaData::Release::SecondaryType> types)
|
||||
{
|
||||
const ReleaseTypePrimary primaryType{ convertReleaseTypePrimary(*releaseInfo.primaryType) };
|
||||
if (release->getPrimaryType() != primaryType)
|
||||
release.modify()->setPrimaryType(primaryType);
|
||||
}
|
||||
const EnumSet<ReleaseTypeSecondary> secondaryTypes{ convertReleaseTypesSecondary(releaseInfo.secondaryTypes) };
|
||||
if (release->getSecondaryTypes() != secondaryTypes)
|
||||
release.modify()->setSecondaryTypes(secondaryTypes);
|
||||
if (release->getArtistDisplayName() != releaseInfo.artistDisplayName)
|
||||
release.modify()->setArtistDisplayName(releaseInfo.artistDisplayName);
|
||||
}
|
||||
EnumSet<ReleaseTypeSecondary> res;
|
||||
|
||||
Release::pointer
|
||||
getOrCreateRelease(Session& session, const MetaData::Release& releaseInfo)
|
||||
{
|
||||
Release::pointer release;
|
||||
|
||||
// First try to get by MBID
|
||||
if (releaseInfo.mbid)
|
||||
{
|
||||
release = Release::find(session, *releaseInfo.mbid);
|
||||
if (!release)
|
||||
release = session.create<Release>(releaseInfo.name, releaseInfo.mbid);
|
||||
|
||||
updateReleaseIfNeeded(release, releaseInfo);
|
||||
return release;
|
||||
}
|
||||
|
||||
// Fall back on release name (collisions may occur)
|
||||
if (!releaseInfo.name.empty())
|
||||
{
|
||||
for (const Release::pointer& sameNamedRelease : Release::find(session, releaseInfo.name))
|
||||
for (MetaData::Release::SecondaryType type : types)
|
||||
{
|
||||
// do not fallback on properly tagged releases
|
||||
if (sameNamedRelease->getMBID())
|
||||
continue;
|
||||
|
||||
release = sameNamedRelease;
|
||||
break;
|
||||
switch (type)
|
||||
{
|
||||
case MetaData::Release::SecondaryType::Compilation:
|
||||
res.insert(ReleaseTypeSecondary::Compilation);
|
||||
break;
|
||||
case MetaData::Release::SecondaryType::Soundtrack:
|
||||
res.insert(ReleaseTypeSecondary::Soundtrack);
|
||||
break;
|
||||
case MetaData::Release::SecondaryType::Spokenword:
|
||||
res.insert(ReleaseTypeSecondary::Spokenword);
|
||||
break;
|
||||
case MetaData::Release::SecondaryType::Interview:
|
||||
res.insert(ReleaseTypeSecondary::Interview);
|
||||
break;
|
||||
case MetaData::Release::SecondaryType::Audiobook:
|
||||
res.insert(ReleaseTypeSecondary::Audiobook);
|
||||
break;
|
||||
case MetaData::Release::SecondaryType::AudioDrama:
|
||||
res.insert(ReleaseTypeSecondary::AudioDrama);
|
||||
break;
|
||||
case MetaData::Release::SecondaryType::Live:
|
||||
res.insert(ReleaseTypeSecondary::Live);
|
||||
break;
|
||||
case MetaData::Release::SecondaryType::Remix:
|
||||
res.insert(ReleaseTypeSecondary::Remix);
|
||||
break;
|
||||
case MetaData::Release::SecondaryType::DJMix:
|
||||
res.insert(ReleaseTypeSecondary::DJMix);
|
||||
break;
|
||||
case MetaData::Release::SecondaryType::Mixtape_Street:
|
||||
res.insert(ReleaseTypeSecondary::Mixtape_Street);
|
||||
break;
|
||||
case MetaData::Release::SecondaryType::Demo:
|
||||
res.insert(ReleaseTypeSecondary::Demo);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// No release found with the same name and without MBID -> creating
|
||||
if (!release)
|
||||
release = session.create<Release>(releaseInfo.name);
|
||||
|
||||
updateReleaseIfNeeded(release, releaseInfo);
|
||||
return release;
|
||||
return res;
|
||||
}
|
||||
|
||||
return Release::pointer{};
|
||||
}
|
||||
|
||||
std::vector<Cluster::pointer>
|
||||
getOrCreateClusters(Session& session, const MetaData::Tags& tags)
|
||||
{
|
||||
std::vector<Cluster::pointer> clusters;
|
||||
|
||||
for (const auto& [tag, values] : tags)
|
||||
void updateReleaseIfNeeded(Release::pointer release, const MetaData::Release& releaseInfo)
|
||||
{
|
||||
auto clusterType = ClusterType::find(session, tag);
|
||||
if (!clusterType)
|
||||
continue;
|
||||
|
||||
for (auto clusterName : values)
|
||||
if (release->getName() != releaseInfo.name)
|
||||
release.modify()->setName(releaseInfo.name);
|
||||
if (release->getTotalDisc() != releaseInfo.mediumCount)
|
||||
release.modify()->setTotalDisc(releaseInfo.mediumCount);
|
||||
if (releaseInfo.primaryType)
|
||||
{
|
||||
auto cluster = clusterType->getCluster(clusterName);
|
||||
if (!cluster)
|
||||
cluster = session.create<Cluster>(clusterType, clusterName);
|
||||
|
||||
clusters.push_back(cluster);
|
||||
const ReleaseTypePrimary primaryType{ convertReleaseTypePrimary(*releaseInfo.primaryType) };
|
||||
if (release->getPrimaryType() != primaryType)
|
||||
release.modify()->setPrimaryType(primaryType);
|
||||
}
|
||||
const EnumSet<ReleaseTypeSecondary> secondaryTypes{ convertReleaseTypesSecondary(releaseInfo.secondaryTypes) };
|
||||
if (release->getSecondaryTypes() != secondaryTypes)
|
||||
release.modify()->setSecondaryTypes(secondaryTypes);
|
||||
if (release->getArtistDisplayName() != releaseInfo.artistDisplayName)
|
||||
release.modify()->setArtistDisplayName(releaseInfo.artistDisplayName);
|
||||
}
|
||||
|
||||
return clusters;
|
||||
}
|
||||
Release::pointer getOrCreateRelease(Session& session, const MetaData::Release& releaseInfo, const std::filesystem::path& expectedReleaseDirectory)
|
||||
{
|
||||
Release::pointer release;
|
||||
|
||||
MetaData::ParserReadStyle
|
||||
getParserReadStyle()
|
||||
{
|
||||
std::string_view readStyle{ Service<IConfig>::get()->getString("scanner-parser-read-style", "accurate") };
|
||||
// First try to get by MBID
|
||||
if (releaseInfo.mbid)
|
||||
{
|
||||
release = Release::find(session, *releaseInfo.mbid);
|
||||
if (!release)
|
||||
release = session.create<Release>(releaseInfo.name, releaseInfo.mbid);
|
||||
|
||||
if (readStyle == "fast")
|
||||
return MetaData::ParserReadStyle::Fast;
|
||||
else if (readStyle == "average")
|
||||
return MetaData::ParserReadStyle::Average;
|
||||
else if (readStyle == "accurate")
|
||||
return MetaData::ParserReadStyle::Accurate;
|
||||
updateReleaseIfNeeded(release, releaseInfo);
|
||||
return release;
|
||||
}
|
||||
|
||||
throw LmsException{ "Invalid value for 'scanner-parser-read-style'" };
|
||||
}
|
||||
} // namespace
|
||||
// Fall back on release name (collisions may occur), if and only if it is in the current directory
|
||||
if (!releaseInfo.name.empty())
|
||||
{
|
||||
for (const Release::pointer& sameNamedRelease : Release::find(session, releaseInfo.name, expectedReleaseDirectory))
|
||||
{
|
||||
// do not fallback on properly tagged releases
|
||||
if (sameNamedRelease->getMBID())
|
||||
continue;
|
||||
|
||||
release = sameNamedRelease;
|
||||
break;
|
||||
}
|
||||
|
||||
// No release found with the same name and without MBID -> creating
|
||||
if (!release)
|
||||
release = session.create<Release>(releaseInfo.name);
|
||||
|
||||
updateReleaseIfNeeded(release, releaseInfo);
|
||||
return release;
|
||||
}
|
||||
|
||||
return Release::pointer{};
|
||||
}
|
||||
|
||||
std::vector<Cluster::pointer> getOrCreateClusters(Session& session, const MetaData::Tags& tags)
|
||||
{
|
||||
std::vector<Cluster::pointer> clusters;
|
||||
|
||||
for (const auto& [tag, values] : tags)
|
||||
{
|
||||
auto clusterType = ClusterType::find(session, tag);
|
||||
if (!clusterType)
|
||||
clusterType = session.create<ClusterType>(tag);
|
||||
|
||||
for (const auto& clusterName : values)
|
||||
{
|
||||
auto cluster = clusterType->getCluster(clusterName);
|
||||
if (!cluster)
|
||||
cluster = session.create<Cluster>(clusterType, clusterName);
|
||||
|
||||
clusters.push_back(cluster);
|
||||
}
|
||||
}
|
||||
|
||||
return clusters;
|
||||
}
|
||||
|
||||
MetaData::ParserReadStyle getParserReadStyle()
|
||||
{
|
||||
std::string_view readStyle{ Service<IConfig>::get()->getString("scanner-parser-read-style", "average") };
|
||||
|
||||
if (readStyle == "fast")
|
||||
return MetaData::ParserReadStyle::Fast;
|
||||
else if (readStyle == "average")
|
||||
return MetaData::ParserReadStyle::Average;
|
||||
else if (readStyle == "accurate")
|
||||
return MetaData::ParserReadStyle::Accurate;
|
||||
|
||||
throw LmsException{ "Invalid value for 'scanner-parser-read-style'" };
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace Scanner
|
||||
{
|
||||
ScanStepScanFiles::ScanStepScanFiles(InitParams& initParams)
|
||||
: ScanStepBase{ initParams }
|
||||
, _metadataParser{ MetaData::createParser(MetaData::ParserType::TagLib, getParserReadStyle()) } // For now, always use TagLib
|
||||
{
|
||||
}
|
||||
|
||||
void
|
||||
ScanStepScanFiles::process(ScanContext& context)
|
||||
void ScanStepScanFiles::process(ScanContext& context)
|
||||
{
|
||||
_metadataParser->setClusterTypeNames(_settings.clusterTypeNames);
|
||||
std::vector<std::string> tagsToParse{ _tagsToParse };
|
||||
tagsToParse.insert(std::end(tagsToParse), std::cbegin(_settings.extraTags), std::cend(_settings.extraTags));
|
||||
|
||||
_metadataParser->setExtraTags(tagsToParse);
|
||||
|
||||
context.currentStepStats.totalElems = context.stats.filesScanned;
|
||||
|
||||
@@ -298,7 +293,7 @@ namespace Scanner
|
||||
|
||||
if (ec)
|
||||
{
|
||||
LMS_LOG(DBUPDATER, ERROR) << "Cannot process entry '" << path.string() << "': " << ec.message();
|
||||
LMS_LOG(DBUPDATER, ERROR, "Cannot process entry '" << path.string() << "': " << ec.message());
|
||||
context.stats.errors.emplace_back(ScanError{ path, ScanErrorType::CannotReadFile, ec.message() });
|
||||
}
|
||||
else if (PathUtils::hasFileAnyExtension(path, _settings.supportedExtensions))
|
||||
@@ -317,8 +312,7 @@ namespace Scanner
|
||||
}, &excludeDirFileName);
|
||||
}
|
||||
|
||||
void
|
||||
ScanStepScanFiles::scanAudioFile(const std::filesystem::path& file, ScanContext& context)
|
||||
void ScanStepScanFiles::scanAudioFile(const std::filesystem::path& file, ScanContext& context)
|
||||
{
|
||||
ScanStats& stats{ context.stats };
|
||||
Wt::WDateTime lastWriteTime;
|
||||
@@ -328,7 +322,7 @@ namespace Scanner
|
||||
}
|
||||
catch (LmsException& e)
|
||||
{
|
||||
LMS_LOG(DBUPDATER, ERROR) << e.what();
|
||||
LMS_LOG(DBUPDATER, ERROR, e.what());
|
||||
stats.skips++;
|
||||
return;
|
||||
}
|
||||
@@ -337,7 +331,7 @@ namespace Scanner
|
||||
{
|
||||
// Skip file if last write is the same
|
||||
Database::Session& dbSession{ _db.getTLSSession() };
|
||||
auto transaction{ _db.getTLSSession().createSharedTransaction() };
|
||||
auto transaction{ _db.getTLSSession().createReadTransaction() };
|
||||
|
||||
const Track::pointer track{ Track::findByPath(dbSession, file) };
|
||||
|
||||
@@ -359,7 +353,7 @@ namespace Scanner
|
||||
stats.scans++;
|
||||
|
||||
Database::Session& dbSession{ _db.getTLSSession() };
|
||||
auto uniqueTransaction{ dbSession.createUniqueTransaction() };
|
||||
auto transaction{ dbSession.createWriteTransaction() };
|
||||
|
||||
Track::pointer track{ Track::findByPath(dbSession, file) };
|
||||
|
||||
@@ -374,7 +368,7 @@ namespace Scanner
|
||||
std::error_code ec;
|
||||
if (!std::filesystem::exists(otherTrack->getPath(), ec))
|
||||
{
|
||||
LMS_LOG(DBUPDATER, DEBUG) << "Considering track '" << file.string() << "' moved from '" << otherTrack->getPath() << "'";
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Considering track '" << file.string() << "' moved from '" << otherTrack->getPath() << "'");
|
||||
track = otherTrack;
|
||||
track.modify()->setPath(file);
|
||||
}
|
||||
@@ -393,7 +387,7 @@ namespace Scanner
|
||||
if (!PathUtils::isPathInRootPath(file, _settings.mediaDirectory, &excludeDirFileName))
|
||||
continue;
|
||||
|
||||
LMS_LOG(DBUPDATER, DEBUG) << "Skipped '" << file.string() << "' (similar MBID in '" << otherTrack->getPath().string() << "')";
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Skipped '" << file.string() << "' (similar MBID in '" << otherTrack->getPath().string() << "')");
|
||||
// As this MBID already exists, just remove what we just scanned
|
||||
if (track)
|
||||
{
|
||||
@@ -408,7 +402,7 @@ namespace Scanner
|
||||
// We estimate this is an audio file if the duration is not null
|
||||
if (trackInfo->duration == std::chrono::milliseconds::zero())
|
||||
{
|
||||
LMS_LOG(DBUPDATER, DEBUG) << "Skipped '" << file.string() << "' (duration is 0)";
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Skipped '" << file.string() << "' (duration is 0)");
|
||||
|
||||
// If Track exists here, delete it!
|
||||
if (track)
|
||||
@@ -436,12 +430,12 @@ namespace Scanner
|
||||
if (!track)
|
||||
{
|
||||
track = dbSession.create<Track>(file);
|
||||
LMS_LOG(DBUPDATER, DEBUG) << "Adding '" << file.string() << "'";
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Adding '" << file.string() << "'");
|
||||
stats.additions++;
|
||||
}
|
||||
else
|
||||
{
|
||||
LMS_LOG(DBUPDATER, DEBUG) << "Updating '" << file.string() << "'";
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Updating '" << file.string() << "'");
|
||||
|
||||
stats.updates++;
|
||||
}
|
||||
@@ -488,7 +482,7 @@ namespace Scanner
|
||||
|
||||
track.modify()->setScanVersion(_settings.scanVersion);
|
||||
if (trackInfo->medium && trackInfo->medium->release)
|
||||
track.modify()->setRelease(getOrCreateRelease(dbSession, *trackInfo->medium->release));
|
||||
track.modify()->setRelease(getOrCreateRelease(dbSession, *trackInfo->medium->release, file.parent_path()));
|
||||
else
|
||||
track.modify()->setRelease({});
|
||||
track.modify()->setTotalTrack(trackInfo->medium ? trackInfo->medium->trackCount : std::nullopt);
|
||||
|
||||
@@ -20,24 +20,27 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "metadata/IParser.hpp"
|
||||
#include "ScanStepBase.hpp"
|
||||
|
||||
namespace Scanner
|
||||
{
|
||||
class ScanStepScanFiles : public ScanStepBase
|
||||
{
|
||||
public:
|
||||
ScanStepScanFiles(InitParams& initParams);
|
||||
class ScanStepScanFiles : public ScanStepBase
|
||||
{
|
||||
public:
|
||||
ScanStepScanFiles(InitParams& initParams);
|
||||
|
||||
private:
|
||||
ScanStep getStep() const override { return ScanStep::ScanningFiles; }
|
||||
std::string_view getStepName() const override { return "Scanning files"; }
|
||||
void process(ScanContext& context) override;
|
||||
private:
|
||||
ScanStep getStep() const override { return ScanStep::ScanningFiles; }
|
||||
std::string_view getStepName() const override { return "Scanning files"; }
|
||||
void process(ScanContext& context) override;
|
||||
|
||||
void scanAudioFile(const std::filesystem::path& file, ScanContext& context);
|
||||
void scanAudioFile(const std::filesystem::path& file, ScanContext& context);
|
||||
|
||||
std::unique_ptr<MetaData::IParser> _metadataParser;
|
||||
};
|
||||
std::unique_ptr<MetaData::IParser> _metadataParser;
|
||||
const std::vector<std::string> _tagsToParse{ "GENRE", "MOOD", "LANGUAGE", "ALBUMGROUPING" };
|
||||
};
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
#include "services/database/ScanSettings.hpp"
|
||||
#include "utils/Exception.hpp"
|
||||
#include "utils/IConfig.hpp"
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/ILogger.hpp"
|
||||
#include "utils/Path.hpp"
|
||||
#include "utils/Tuple.hpp"
|
||||
|
||||
@@ -82,9 +82,9 @@ namespace Scanner
|
||||
|
||||
ScannerService::~ScannerService()
|
||||
{
|
||||
LMS_LOG(DBUPDATER, INFO) << "Stopping service...";
|
||||
LMS_LOG(DBUPDATER, INFO, "Stopping service...");
|
||||
stop();
|
||||
LMS_LOG(DBUPDATER, INFO) << "Service stopped!";
|
||||
LMS_LOG(DBUPDATER, INFO, "Service stopped!");
|
||||
}
|
||||
|
||||
void ScannerService::start()
|
||||
@@ -113,15 +113,15 @@ namespace Scanner
|
||||
|
||||
void ScannerService::abortScan()
|
||||
{
|
||||
LMS_LOG(DBUPDATER, DEBUG) << "Aborting scan...";
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Aborting scan...");
|
||||
std::scoped_lock lock{ _controlMutex };
|
||||
|
||||
LMS_LOG(DBUPDATER, DEBUG) << "Waiting for the scan to abort...";
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Waiting for the scan to abort...");
|
||||
|
||||
_abortScan = true;
|
||||
_scheduleTimer.cancel();
|
||||
_ioService.stop();
|
||||
LMS_LOG(DBUPDATER, DEBUG) << "Scan abort done!";
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Scan abort done!");
|
||||
|
||||
_abortScan = false;
|
||||
_ioService.start();
|
||||
@@ -167,7 +167,7 @@ namespace Scanner
|
||||
|
||||
void ScannerService::scheduleNextScan()
|
||||
{
|
||||
LMS_LOG(DBUPDATER, DEBUG) << "Scheduling next scan";
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Scheduling next scan");
|
||||
|
||||
refreshScanSettings();
|
||||
|
||||
@@ -202,7 +202,7 @@ namespace Scanner
|
||||
break;
|
||||
|
||||
case ScanSettings::UpdatePeriod::Never:
|
||||
LMS_LOG(DBUPDATER, INFO) << "Auto scan disabled!";
|
||||
LMS_LOG(DBUPDATER, INFO, "Auto scan disabled!");
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -230,7 +230,7 @@ namespace Scanner
|
||||
|
||||
if (dateTime.isNull())
|
||||
{
|
||||
LMS_LOG(DBUPDATER, INFO) << "Scheduling next scan right now";
|
||||
LMS_LOG(DBUPDATER, INFO, "Scheduling next scan right now");
|
||||
_scheduleTimer.expires_from_now(std::chrono::seconds{ 0 });
|
||||
_scheduleTimer.async_wait(cb);
|
||||
}
|
||||
@@ -240,7 +240,7 @@ namespace Scanner
|
||||
std::time_t t{ std::chrono::system_clock::to_time_t(timePoint) };
|
||||
char ctimeStr[26];
|
||||
|
||||
LMS_LOG(DBUPDATER, INFO) << "Scheduling next scan at " << std::string(::ctime_r(&t, ctimeStr));
|
||||
LMS_LOG(DBUPDATER, INFO, "Scheduling next scan at " << std::string(::ctime_r(&t, ctimeStr)));
|
||||
_scheduleTimer.expires_at(timePoint);
|
||||
_scheduleTimer.async_wait(cb);
|
||||
}
|
||||
@@ -257,7 +257,7 @@ namespace Scanner
|
||||
}
|
||||
|
||||
|
||||
LMS_LOG(UI, INFO) << "New scan started!";
|
||||
LMS_LOG(UI, INFO, "New scan started!");
|
||||
|
||||
refreshScanSettings();
|
||||
|
||||
@@ -267,16 +267,16 @@ namespace Scanner
|
||||
|
||||
for (auto& scanStep : _scanSteps)
|
||||
{
|
||||
LMS_LOG(DBUPDATER, DEBUG) << "Starting scan step '" << scanStep->getStepName() << "'";
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Starting scan step '" << scanStep->getStepName() << "'");
|
||||
scanContext.currentStepStats = ScanStepStats{ Wt::WDateTime::currentDateTime(), scanStep->getStep() };
|
||||
|
||||
notifyInProgress(scanContext.currentStepStats);
|
||||
scanStep->process(scanContext);
|
||||
notifyInProgress(scanContext.currentStepStats);
|
||||
LMS_LOG(DBUPDATER, DEBUG) << "Completed scan step '" << scanStep->getStepName() << "'";
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Completed scan step '" << scanStep->getStepName() << "'");
|
||||
}
|
||||
|
||||
LMS_LOG(DBUPDATER, INFO) << "Scan " << (_abortScan ? "aborted" : "complete") << ". Changes = " << stats.nbChanges() << " (added = " << stats.additions << ", removed = " << stats.deletions << ", updated = " << stats.updates << "), Not changed = " << stats.skips << ", Scanned = " << stats.scans << " (errors = " << stats.errors.size() << "), features fetched = " << stats.featuresFetched << ", duplicates = " << stats.duplicates.size();
|
||||
LMS_LOG(DBUPDATER, INFO, "Scan " << (_abortScan ? "aborted" : "complete") << ". Changes = " << stats.nbChanges() << " (added = " << stats.additions << ", removed = " << stats.deletions << ", updated = " << stats.updates << "), Not changed = " << stats.skips << ", Scanned = " << stats.scans << " (errors = " << stats.errors.size() << "), features fetched = " << stats.featuresFetched << ", duplicates = " << stats.duplicates.size());
|
||||
|
||||
_dbSession.analyze();
|
||||
|
||||
@@ -290,14 +290,14 @@ namespace Scanner
|
||||
_currentScanStepStats.reset();
|
||||
}
|
||||
|
||||
LMS_LOG(DBUPDATER, DEBUG) << "Scan not aborted, scheduling next scan!";
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Scan not aborted, scheduling next scan!");
|
||||
scheduleNextScan();
|
||||
|
||||
_events.scanComplete.emit(stats);
|
||||
}
|
||||
else
|
||||
{
|
||||
LMS_LOG(DBUPDATER, DEBUG) << "Scan aborted, not scheduling next scan!";
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Scan aborted, not scheduling next scan!");
|
||||
|
||||
std::unique_lock lock{ _statusMutex };
|
||||
|
||||
@@ -312,9 +312,9 @@ namespace Scanner
|
||||
if (_settings == newSettings)
|
||||
return;
|
||||
|
||||
LMS_LOG(DBUPDATER, DEBUG) << "Scanner settings updated";
|
||||
LMS_LOG(DBUPDATER, DEBUG) << "skipDuplicateMBID = " << newSettings.skipDuplicateMBID;
|
||||
LMS_LOG(DBUPDATER, DEBUG) << "Using scan settings version " << newSettings.scanVersion;
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Scanner settings updated");
|
||||
LMS_LOG(DBUPDATER, DEBUG, "skipDuplicateMBID = " << newSettings.skipDuplicateMBID);
|
||||
LMS_LOG(DBUPDATER, DEBUG, "Using scan settings version " << newSettings.scanVersion);
|
||||
|
||||
_settings = std::move(newSettings);
|
||||
|
||||
@@ -345,7 +345,7 @@ namespace Scanner
|
||||
|
||||
newSettings.skipDuplicateMBID = Service<IConfig>::get()->getBool("scanner-skip-duplicate-mbid", false);
|
||||
{
|
||||
auto transaction{ _dbSession.createSharedTransaction() };
|
||||
auto transaction{ _dbSession.createReadTransaction() };
|
||||
|
||||
const ScanSettings::pointer scanSettings{ ScanSettings::get(_dbSession) };
|
||||
|
||||
@@ -361,14 +361,10 @@ namespace Scanner
|
||||
}
|
||||
newSettings.mediaDirectory = scanSettings->getMediaDirectory();
|
||||
|
||||
const auto clusterTypes = scanSettings->getClusterTypes();
|
||||
std::set<std::string> clusterTypeNames;
|
||||
|
||||
std::transform(std::cbegin(clusterTypes), std::cend(clusterTypes),
|
||||
std::inserter(clusterTypeNames, clusterTypeNames.begin()),
|
||||
[](ClusterType::pointer clusterType) { return clusterType->getName(); });
|
||||
|
||||
newSettings.clusterTypeNames = std::move(clusterTypeNames);
|
||||
{
|
||||
const auto& tags{ scanSettings->getExtraTagsToScan() };
|
||||
std::transform(std::cbegin(tags), std::cend(tags), std::back_inserter(newSettings.extraTags), [](std::string_view tag) { return std::string{ tag };});
|
||||
}
|
||||
}
|
||||
|
||||
return newSettings;
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <Wt/WDateTime.h>
|
||||
@@ -36,7 +35,7 @@ namespace Scanner
|
||||
std::vector<std::filesystem::path> supportedExtensions;
|
||||
std::filesystem::path mediaDirectory;
|
||||
bool skipDuplicateMBID {};
|
||||
std::set<std::string> clusterTypeNames;
|
||||
std::vector<std::string> extraTags;
|
||||
|
||||
bool operator==(const ScannerSettings& rhs) const
|
||||
{
|
||||
@@ -46,7 +45,7 @@ namespace Scanner
|
||||
&& supportedExtensions == rhs.supportedExtensions
|
||||
&& mediaDirectory == rhs.mediaDirectory
|
||||
&& skipDuplicateMBID == rhs.skipDuplicateMBID
|
||||
&& clusterTypeNames == rhs.clusterTypeNames;
|
||||
&& extraTags == rhs.extraTags;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ namespace Scanner
|
||||
ReloadingSimilarityEngine,
|
||||
ComputeClusterStats,
|
||||
};
|
||||
static inline constexpr unsigned ScanProgressStepCount{ 6 };
|
||||
static inline constexpr unsigned ScanProgressStepCount{ 7 };
|
||||
|
||||
// reduced scan stats
|
||||
struct ScanStepStats
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user