diff --git a/README.md b/README.md index e9600508..2d1b24a0 100644 --- a/README.md +++ b/README.md @@ -15,12 +15,13 @@ A [demo](http://lms.demo.poupon.io) instance is available, with the following li * User management * Recommendation engine * Audio transcode for maximum interoperability and low bandwith requirements +* ReplayGain support * Persistent play queue across sessions * Compilation support * Multi-value tags: artists, genres, ... * Custom tags (ex: _mood_, _genre_, _albummood_, _albumgrouping_, ...) * MusicBrainzID support to handle duplicated artist and release names -* ReplayGain support +* Disc subtitles support * _Systemd_ integration * Subsonic API, with the following additional features: * Playlists diff --git a/src/libs/database/impl/Session.cpp b/src/libs/database/impl/Session.cpp index 39319ff2..7cb92cbe 100644 --- a/src/libs/database/impl/Session.cpp +++ b/src/libs/database/impl/Session.cpp @@ -40,7 +40,7 @@ namespace Database { -#define LMS_DATABASE_VERSION 22 +#define LMS_DATABASE_VERSION 23 using Version = std::size_t; @@ -254,6 +254,13 @@ CREATE TABLE "user_backup" ( // Just increment the scan version of the settings to make the next scheduled scan rescan everything ScanSettings::get(*this).modify()->incScanVersion(); } + else if (version == 22) + { + _session.execute("ALTER TABLE track ADD disc_subtitle TEXT NOT NULL DEFAULT ''"); + + // Just increment the scan version of the settings to make the next scheduled scan rescan everything + ScanSettings::get(*this).modify()->incScanVersion(); + } else { LMS_LOG(DB, ERROR) << "Database version " << version << " cannot be handled using migration"; diff --git a/src/libs/database/include/database/Track.hpp b/src/libs/database/include/database/Track.hpp index 53fabb71..52a5113a 100644 --- a/src/libs/database/include/database/Track.hpp +++ b/src/libs/database/include/database/Track.hpp @@ -91,6 +91,7 @@ class Track : public Wt::Dbo::Dbo void setDiscNumber(int num) { _discNumber = num; } void setTotalTrack(std::optional totalTrack) { totalTrack ? _totalTrack = *totalTrack : 0; } void setTotalDisc(std::optional totalDisc) { totalDisc ? _totalDisc = *totalDisc : 0; } + void setDiscSubtitle(const std::string& name) { _discSubtitle = name; } void setName(const std::string& name) { _name = std::string(name, 0, _maxNameLength); } void setDuration(std::chrono::milliseconds duration) { _duration = duration; } void setLastWriteTime(Wt::WDateTime time) { _fileLastWrite = time; } @@ -113,6 +114,7 @@ class Track : public Wt::Dbo::Dbo std::optional getTrackNumber() const; std::optional getTotalTrack() const; std::optional getDiscNumber() const; + const std::string& getDiscSubtitle() const { return _discSubtitle; } std::optional getTotalDisc() const; std::string getName() const { return _name; } std::filesystem::path getPath() const { return _filePath; } @@ -145,6 +147,7 @@ class Track : public Wt::Dbo::Dbo Wt::Dbo::field(a, _scanVersion, "scan_version"); Wt::Dbo::field(a, _trackNumber, "track_number"); Wt::Dbo::field(a, _discNumber, "disc_number"); + Wt::Dbo::field(a, _discSubtitle, "disc_subtitle"); Wt::Dbo::field(a, _totalTrack, "total_track"); Wt::Dbo::field(a, _totalDisc, "total_disc"); Wt::Dbo::field(a, _name, "name"); @@ -177,6 +180,7 @@ class Track : public Wt::Dbo::Dbo int _scanVersion {}; int _trackNumber {}; int _discNumber {}; + std::string _discSubtitle; int _totalTrack {}; int _totalDisc {}; std::string _name; diff --git a/src/libs/metadata/impl/AvFormatParser.cpp b/src/libs/metadata/impl/AvFormatParser.cpp index ae2d9495..3da2e720 100644 --- a/src/libs/metadata/impl/AvFormatParser.cpp +++ b/src/libs/metadata/impl/AvFormatParser.cpp @@ -211,6 +211,12 @@ AvFormatParser::parse(const std::filesystem::path& p, bool debug) { track.musicBrainzTrackID = UUID::fromString(value); } + else if (tag == "TSST" + || tag == "DISCSUBTITLE" + || tag == "SETSUBTITLE") + { + track.discSubtitle = value; + } else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end()) { std::vector clusterNames {StringUtils::splitString(value, "/,;")}; diff --git a/src/libs/metadata/impl/TagLibParser.cpp b/src/libs/metadata/impl/TagLibParser.cpp index d6a9f3c5..5fa1bfea 100644 --- a/src/libs/metadata/impl/TagLibParser.cpp +++ b/src/libs/metadata/impl/TagLibParser.cpp @@ -270,6 +270,8 @@ TagLibParser::processTag(Track& track, const std::string& tag, const TagLib::Str track.albumReplayGain = StringUtils::readAs(value); else if (tag == "REPLAYGAIN_TRACK_GAIN") track.trackReplayGain = StringUtils::readAs(value); + else if (tag == "DISCSUBTITLE" || tag == "SETSUBTITLE") + track.discSubtitle = value; else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end()) { std::set clusterNames; @@ -372,8 +374,12 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug) { if (mp3File->ID3v2Tag()) { - if (!mp3File->ID3v2Tag()->frameListMap()["APIC"].isEmpty()) + const auto& frameListMap {mp3File->ID3v2Tag()->frameListMap()}; + + if (!frameListMap["APIC"].isEmpty()) track.hasCover = true; + if (!frameListMap["TSST"].isEmpty()) + properties.insert("DISCSUBTITLE", frameListMap["TSST"].front()->toString()); } getAPETags(mp3File->APETag()); diff --git a/src/libs/metadata/include/metadata/IParser.hpp b/src/libs/metadata/include/metadata/IParser.hpp index 68b72b4d..624af218 100644 --- a/src/libs/metadata/include/metadata/IParser.hpp +++ b/src/libs/metadata/include/metadata/IParser.hpp @@ -58,25 +58,26 @@ namespace MetaData { std::vector artists; std::vector albumArtists; - std::string title; + std::string title; std::optional musicBrainzTrackID; std::optional musicBrainzRecordID; - std::optional album; - Clusters clusters; - std::chrono::milliseconds duration {}; + std::optional album; + Clusters clusters; + std::chrono::milliseconds duration; std::optional trackNumber; std::optional totalTrack; std::optional discNumber; std::optional totalDisc; std::optional year; std::optional originalYear; - bool hasCover {false}; + bool hasCover {}; std::vector audioStreams; std::optional acoustID; - std::string copyright; - std::string copyrightURL; - std::optional trackReplayGain; - std::optional albumReplayGain; + std::string copyright; + std::string copyrightURL; + std::optional trackReplayGain; + std::optional albumReplayGain; + std::string discSubtitle; }; class IParser diff --git a/src/libs/scanner/impl/MediaScanner.cpp b/src/libs/scanner/impl/MediaScanner.cpp index c9ead16d..36a3e659 100644 --- a/src/libs/scanner/impl/MediaScanner.cpp +++ b/src/libs/scanner/impl/MediaScanner.cpp @@ -738,6 +738,8 @@ MediaScanner::scanAudioFile(const std::filesystem::path& file, bool forceScan, S track.modify()->setDiscNumber(trackInfo->discNumber ? *trackInfo->discNumber : 0); track.modify()->setTotalTrack(trackInfo->totalTrack); track.modify()->setTotalDisc(trackInfo->totalDisc); + if (!trackInfo->discSubtitle.empty()) + track.modify()->setDiscSubtitle(trackInfo->discSubtitle); track.modify()->setYear(trackInfo->year ? *trackInfo->year : 0); track.modify()->setOriginalYear(trackInfo->originalYear ? *trackInfo->originalYear : 0); diff --git a/src/lms/ui/explore/ReleaseView.cpp b/src/lms/ui/explore/ReleaseView.cpp index 6dc7d81e..44504b07 100644 --- a/src/lms/ui/explore/ReleaseView.cpp +++ b/src/lms/ui/explore/ReleaseView.cpp @@ -169,7 +169,7 @@ Release::refreshView() // Expect to be call in asc order std::map trackContainers; - auto getOrAddDiscContainer = [&](std::size_t discNumber) -> Wt::WContainerWidget* + auto getOrAddDiscContainer = [&](std::size_t discNumber, const std::string& discSubtitle) -> Wt::WContainerWidget* { { auto it = trackContainers.find(discNumber); @@ -178,9 +178,13 @@ Release::refreshView() } Wt::WTemplate* disc {rootContainer->addNew(Wt::WString::tr("Lms.Explore.Release.template.disc-entry"))}; - disc->bindNew("disc-title", Wt::WString::tr("Lms.Explore.Release.disc").arg(discNumber)); disc->addFunction("tr", &Wt::WTemplate::Functions::tr); + if (discSubtitle.empty()) + disc->bindNew("disc-title", Wt::WString::tr("Lms.Explore.Release.disc").arg(discNumber)); + else + disc->bindString("disc-title", Wt::WString::fromUTF8(discSubtitle), Wt::TextFormat::Plain); + Wt::WContainerWidget* tracksContainer {disc->bindNew("tracks")}; trackContainers[discNumber] = tracksContainer; @@ -198,7 +202,7 @@ Release::refreshView() Wt::WContainerWidget* container {rootContainer}; if (isReleaseMultiDisc && discNumber) - container = getOrAddDiscContainer(*discNumber); + container = getOrAddDiscContainer(*discNumber, track->getDiscSubtitle()); Wt::WTemplate* entry {container->addNew(Wt::WString::tr("Lms.Explore.Release.template.entry"))}; diff --git a/src/tools/metadata/LmsMetadata.cpp b/src/tools/metadata/LmsMetadata.cpp index 3d299bc3..24071070 100644 --- a/src/tools/metadata/LmsMetadata.cpp +++ b/src/tools/metadata/LmsMetadata.cpp @@ -110,6 +110,9 @@ void parse(MetaData::IParser& parser, const std::filesystem::path& file) if (track->discNumber) std::cout << "Disc: " << *track->discNumber << std::endl; + if (!track->discSubtitle.empty()) + std::cout << "Disc Subtitle: " << track->discSubtitle << std::endl; + if (track->totalDisc) std::cout << "TotalDisc: " << *track->totalDisc << std::endl;