WIP replaygain support
This commit is contained in:
@@ -40,7 +40,7 @@
|
||||
|
||||
namespace Database {
|
||||
|
||||
#define LMS_DATABASE_VERSION 21
|
||||
#define LMS_DATABASE_VERSION 22
|
||||
|
||||
using Version = std::size_t;
|
||||
|
||||
@@ -246,6 +246,14 @@ CREATE TABLE "user_backup" (
|
||||
{
|
||||
_session.execute("DROP TABLE subsonic_settings");
|
||||
}
|
||||
else if (version == 21)
|
||||
{
|
||||
_session.execute("ALTER TABLE track ADD track_replay_gain REAL");
|
||||
_session.execute("ALTER TABLE track ADD release_replay_gain REAL");
|
||||
|
||||
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||
ScanSettings::get(*this).modify()->incScanVersion();
|
||||
}
|
||||
else
|
||||
{
|
||||
LMS_LOG(DB, ERROR) << "Database version " << version << " cannot be handled using migration";
|
||||
|
||||
@@ -38,6 +38,14 @@ _filePath( p.string() )
|
||||
{
|
||||
}
|
||||
|
||||
std::size_t
|
||||
Track::getCount(Session& session)
|
||||
{
|
||||
session.checkSharedLocked();
|
||||
|
||||
return session.getDboSession().query<int>("SELECT COUNT(*) FROM track");
|
||||
}
|
||||
|
||||
std::vector<Track::pointer>
|
||||
Track::getAll(Session& session, std::optional<std::size_t> limit)
|
||||
{
|
||||
@@ -107,13 +115,26 @@ Track::create(Session& session, const std::filesystem::path& p)
|
||||
return res;
|
||||
}
|
||||
|
||||
std::vector<std::filesystem::path>
|
||||
Track::getAllPaths(Session& session)
|
||||
std::vector<std::pair<IdType, std::filesystem::path>>
|
||||
Track::getAllPaths(Session& session, std::optional<std::size_t> offset, std::optional<std::size_t> size)
|
||||
{
|
||||
using QueryResultType = std::tuple<IdType, std::string>;
|
||||
session.checkSharedLocked();
|
||||
|
||||
Wt::Dbo::collection<std::string> res = session.getDboSession().query<std::string>("SELECT file_path FROM track");
|
||||
return std::vector<std::filesystem::path>(res.begin(), res.end());
|
||||
Wt::Dbo::collection<QueryResultType> queryRes = session.getDboSession().query<QueryResultType>("SELECT id,file_path FROM track")
|
||||
.limit(size ? static_cast<int>(*size) + 1 : -1)
|
||||
.offset(offset ? static_cast<int>(*offset) : -1);
|
||||
|
||||
std::vector<std::pair<IdType, std::filesystem::path>> result;
|
||||
result.reserve(queryRes.size());
|
||||
|
||||
std::transform(std::begin(queryRes), std::end(queryRes), std::back_inserter(result),
|
||||
[](const QueryResultType& queryResult)
|
||||
{
|
||||
return std::make_pair(std::get<0>(queryResult), std::get<1>(queryResult));
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<Track::pointer>
|
||||
|
||||
@@ -55,6 +55,7 @@ class Track : public Wt::Dbo::Dbo<Track>
|
||||
Track(const std::filesystem::path& p);
|
||||
|
||||
// Find utility functions
|
||||
static std::size_t getCount(Session& session);
|
||||
static pointer getByPath(Session& session, const std::filesystem::path& p);
|
||||
static pointer getById(Session& session, IdType id);
|
||||
static pointer getByMBID(Session& session, const UUID& MBID);
|
||||
@@ -74,7 +75,7 @@ class Track : public Wt::Dbo::Dbo<Track>
|
||||
static std::vector<pointer> getAll(Session& session, std::optional<std::size_t> limit = {});
|
||||
static std::vector<pointer> getAllRandom(Session& session, std::optional<std::size_t> limit = {});
|
||||
static std::vector<IdType> getAllIds(Session& session);
|
||||
static std::vector<std::filesystem::path> getAllPaths(Session& session);
|
||||
static std::vector<std::pair<IdType, std::filesystem::path>> getAllPaths(Session& session, std::optional<std::size_t> offset = std::nullopt, std::optional<std::size_t> size = std::nullopt);
|
||||
static std::vector<pointer> getMBIDDuplicates(Session& session);
|
||||
static std::vector<pointer> getLastAdded(Session& session, const Wt::WDateTime& after, std::optional<std::size_t> size = 1);
|
||||
static std::vector<pointer> getAllWithMBIDAndMissingFeatures(Session& session);
|
||||
@@ -100,6 +101,8 @@ class Track : public Wt::Dbo::Dbo<Track>
|
||||
void setMBID(const std::optional<UUID>& MBID) { _MBID = MBID ? MBID->getAsString() : ""; }
|
||||
void setCopyright(const std::string& copyright) { _copyright = std::string(copyright, 0, _maxCopyrightLength); }
|
||||
void setCopyrightURL(const std::string& copyrightURL) { _copyrightURL = std::string(copyrightURL, 0, _maxCopyrightURLLength); }
|
||||
void setTrackReplayGain(float replayGain) { _trackReplayGain = replayGain; }
|
||||
void setReleaseReplayGain(float replayGain) { _releaseReplayGain = replayGain; }
|
||||
void clearArtistLinks();
|
||||
void addArtistLink(const Wt::Dbo::ptr<TrackArtistLink>& artistLink);
|
||||
void setRelease(Wt::Dbo::ptr<Release> release) { _release = release; }
|
||||
@@ -122,6 +125,9 @@ class Track : public Wt::Dbo::Dbo<Track>
|
||||
std::optional<UUID> getMBID() const { return UUID::fromString(_MBID); }
|
||||
std::optional<std::string> getCopyright() const;
|
||||
std::optional<std::string> getCopyrightURL() const;
|
||||
std::optional<float> getTrackReplayGain() const { return _trackReplayGain; }
|
||||
std::optional<float> getReleaseReplayGain() const { return _releaseReplayGain; }
|
||||
|
||||
std::vector<Wt::Dbo::ptr<Artist>> getArtists(TrackArtistLink::Type type = TrackArtistLink::Type::Artist) const;
|
||||
std::vector<IdType> getArtistIds(TrackArtistLink::Type type = TrackArtistLink::Type::Artist) const;
|
||||
std::vector<Wt::Dbo::ptr<TrackArtistLink>> getArtistLinks() const;
|
||||
@@ -152,6 +158,8 @@ class Track : public Wt::Dbo::Dbo<Track>
|
||||
Wt::Dbo::field(a, _MBID, "mbid");
|
||||
Wt::Dbo::field(a, _copyright, "copyright");
|
||||
Wt::Dbo::field(a, _copyrightURL, "copyright_url");
|
||||
Wt::Dbo::field(a, _trackReplayGain, "track_replay_gain");
|
||||
Wt::Dbo::field(a, _releaseReplayGain, "release_replay_gain");
|
||||
Wt::Dbo::belongsTo(a, _release, "release", Wt::Dbo::OnDeleteCascade);
|
||||
Wt::Dbo::hasMany(a, _trackArtistLinks, Wt::Dbo::ManyToOne, "track");
|
||||
Wt::Dbo::hasMany(a, _clusters, Wt::Dbo::ManyToMany, "track_cluster", "", Wt::Dbo::OnDeleteCascade);
|
||||
@@ -184,6 +192,8 @@ class Track : public Wt::Dbo::Dbo<Track>
|
||||
std::string _MBID; // Musicbrainz Identifier
|
||||
std::string _copyright;
|
||||
std::string _copyrightURL;
|
||||
std::optional<float> _trackReplayGain;
|
||||
std::optional<float> _releaseReplayGain;
|
||||
|
||||
Wt::Dbo::ptr<Release> _release;
|
||||
Wt::Dbo::collection<Wt::Dbo::ptr<TrackArtistLink>> _trackArtistLinks;
|
||||
|
||||
@@ -19,13 +19,16 @@
|
||||
|
||||
#include "metadata/TagLibParser.hpp"
|
||||
|
||||
#include <taglib/apetag.h>
|
||||
#include <taglib/asffile.h>
|
||||
#include <taglib/id3v2tag.h>
|
||||
#include <taglib/fileref.h>
|
||||
#include <taglib/flacfile.h>
|
||||
#include <taglib/mpcfile.h>
|
||||
#include <taglib/mpegfile.h>
|
||||
#include <taglib/tag.h>
|
||||
#include <taglib/tpropertymap.h>
|
||||
#include <taglib/wavpackfile.h>
|
||||
|
||||
#include "utils/Logger.hpp"
|
||||
#include "utils/String.hpp"
|
||||
@@ -263,6 +266,10 @@ TagLibParser::processTag(Track& track, const std::string& tag, const TagLib::Str
|
||||
track.copyright = value;
|
||||
else if (tag == "COPYRIGHTURL")
|
||||
track.copyrightURL = value;
|
||||
else if (tag == "REPLAYGAIN_ALBUM_GAIN")
|
||||
track.albumReplayGain = StringUtils::readAs<float>(value);
|
||||
else if (tag == "REPLAYGAIN_TRACK_GAIN")
|
||||
track.trackReplayGain = StringUtils::readAs<float>(value);
|
||||
else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end())
|
||||
{
|
||||
std::set<std::string> clusterNames;
|
||||
@@ -311,6 +318,21 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
|
||||
|
||||
TagLib::PropertyMap properties {f.file()->properties()};
|
||||
|
||||
auto getAPETags = [&](const TagLib::APE::Tag* apeTag)
|
||||
{
|
||||
if (!apeTag)
|
||||
return;
|
||||
|
||||
for (const auto& [name, values] : apeTag->properties())
|
||||
{
|
||||
if (debug)
|
||||
std::cout << "APE property: '" << name << "'" << std::endl;
|
||||
|
||||
if (!properties.contains(name))
|
||||
properties.insert(name, values);
|
||||
}
|
||||
};
|
||||
|
||||
// Not that good embedded pictures handling
|
||||
|
||||
// WMA
|
||||
@@ -337,9 +359,10 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
|
||||
if (!stringAttributeList.isEmpty())
|
||||
{
|
||||
if (debug)
|
||||
std::cout << "Property: '" << name << "'" << std::endl;
|
||||
std::cout << "ASF property: '" << name << "'" << std::endl;
|
||||
|
||||
properties.insert(name, stringAttributeList);
|
||||
if (!properties.contains(name))
|
||||
properties.insert(name, stringAttributeList);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -352,6 +375,17 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
|
||||
if (!mp3File->ID3v2Tag()->frameListMap()["APIC"].isEmpty())
|
||||
track.hasCover = true;
|
||||
}
|
||||
|
||||
getAPETags(mp3File->APETag());
|
||||
}
|
||||
else if (TagLib::MPC::File* mpcFile {dynamic_cast<TagLib::MPC::File*>(f.file())})
|
||||
{
|
||||
getAPETags(mpcFile->APETag());
|
||||
}
|
||||
// WavPack
|
||||
else if (TagLib::WavPack::File* wavPackFile {dynamic_cast<TagLib::WavPack::File*>(f.file())})
|
||||
{
|
||||
getAPETags(wavPackFile->APETag());
|
||||
}
|
||||
// FLAC
|
||||
else if (TagLib::FLAC::File* flacFile {dynamic_cast<TagLib::FLAC::File*>(f.file())})
|
||||
@@ -360,7 +394,7 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
|
||||
track.hasCover = true;
|
||||
}
|
||||
|
||||
for(const auto& property : properties)
|
||||
for (const auto& property : properties)
|
||||
{
|
||||
const std::string tag {property.first.upper().to8Bit(true)};
|
||||
const TagLib::StringList& values {property.second};
|
||||
|
||||
@@ -75,6 +75,8 @@ namespace MetaData
|
||||
std::optional<UUID> acoustID;
|
||||
std::string copyright;
|
||||
std::string copyrightURL;
|
||||
std::optional<float> trackReplayGain;
|
||||
std::optional<float> albumReplayGain;
|
||||
};
|
||||
|
||||
class IParser
|
||||
|
||||
@@ -111,7 +111,6 @@ updateArtistIfNeeded(const Artist::pointer& artist, const MetaData::Artist& arti
|
||||
// Sortname may have been updated
|
||||
if (artistInfo.sortName && *artistInfo.sortName != artist->getSortName() )
|
||||
{
|
||||
LMS_LOG(DBUPDATER, INFO) << "Setting sort name = '" << *artistInfo.sortName << "'";
|
||||
artist.modify()->setSortName(*artistInfo.sortName);
|
||||
}
|
||||
}
|
||||
@@ -751,6 +750,10 @@ MediaScanner::scanAudioFile(const std::filesystem::path& file, bool forceScan, S
|
||||
track.modify()->setHasCover(trackInfo->hasCover);
|
||||
track.modify()->setCopyright(trackInfo->copyright);
|
||||
track.modify()->setCopyrightURL(trackInfo->copyrightURL);
|
||||
if (trackInfo->trackReplayGain)
|
||||
track.modify()->setTrackReplayGain(*trackInfo->trackReplayGain);
|
||||
if (trackInfo->albumReplayGain)
|
||||
track.modify()->setReleaseReplayGain(*trackInfo->albumReplayGain);
|
||||
}
|
||||
|
||||
void
|
||||
@@ -819,32 +822,61 @@ checkFile(const std::filesystem::path& p, const std::filesystem::path& mediaDire
|
||||
void
|
||||
MediaScanner::removeMissingTracks(ScanStats& stats)
|
||||
{
|
||||
std::vector<std::filesystem::path> trackPaths;
|
||||
static constexpr std::size_t batchSize {50};
|
||||
|
||||
LMS_LOG(DBUPDATER, DEBUG) << "Checking tracks to be removed...";
|
||||
std::size_t trackCount {};
|
||||
|
||||
{
|
||||
auto transaction {_dbSession.createSharedTransaction()};
|
||||
trackPaths = Track::getAllPaths(_dbSession);;
|
||||
trackCount = Track::getCount(_dbSession);
|
||||
}
|
||||
LMS_LOG(DBUPDATER, DEBUG) << trackCount << " tracks to be checked...";
|
||||
|
||||
LMS_LOG(DBUPDATER, DEBUG) << "Checking tracks...";
|
||||
for (const auto& trackPath : trackPaths)
|
||||
std::vector<std::pair<Database::IdType, std::filesystem::path>> trackPaths;
|
||||
std::vector<IdType> tracksToRemove;
|
||||
|
||||
for (std::size_t i {trackCount < batchSize ? 0 : trackCount - batchSize}; ; i -= (i > batchSize ? batchSize : i))
|
||||
{
|
||||
if (!_running)
|
||||
return;
|
||||
trackPaths.clear();
|
||||
tracksToRemove.clear();
|
||||
|
||||
if (!checkFile(trackPath, _mediaDirectory, _fileExtensions))
|
||||
{
|
||||
auto transaction {_dbSession.createSharedTransaction()};
|
||||
trackPaths = Track::getAllPaths(_dbSession, i, batchSize);
|
||||
}
|
||||
|
||||
for (const auto& [trackId, trackPath] : trackPaths)
|
||||
{
|
||||
if (!_running)
|
||||
return;
|
||||
|
||||
if (!checkFile(trackPath, _mediaDirectory, _fileExtensions))
|
||||
tracksToRemove.push_back(trackId);
|
||||
}
|
||||
|
||||
if (!tracksToRemove.empty())
|
||||
{
|
||||
auto transaction {_dbSession.createUniqueTransaction()};
|
||||
|
||||
Track::pointer track {Track::getByPath(_dbSession, trackPath)};
|
||||
if (track)
|
||||
for (const IdType trackId : tracksToRemove)
|
||||
{
|
||||
track.remove();
|
||||
stats.deletions++;
|
||||
Track::pointer track {Track::getById(_dbSession, trackId)};
|
||||
if (track)
|
||||
{
|
||||
track.remove();
|
||||
stats.deletions++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
notifyInProgressIfNeeded(stats);
|
||||
|
||||
if (i == 0)
|
||||
break;
|
||||
}
|
||||
|
||||
LMS_LOG(DBUPDATER, DEBUG) << trackCount << " tracks checked!";
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
Reference in New Issue
Block a user