Added disc subtitles support. Fixes #56
This commit is contained in:
@@ -15,12 +15,13 @@ A [demo](http://lms.demo.poupon.io) instance is available, with the following li
|
|||||||
* User management
|
* User management
|
||||||
* Recommendation engine
|
* Recommendation engine
|
||||||
* Audio transcode for maximum interoperability and low bandwith requirements
|
* Audio transcode for maximum interoperability and low bandwith requirements
|
||||||
|
* ReplayGain support
|
||||||
* Persistent play queue across sessions
|
* Persistent play queue across sessions
|
||||||
* Compilation support
|
* Compilation support
|
||||||
* Multi-value tags: artists, genres, ...
|
* Multi-value tags: artists, genres, ...
|
||||||
* Custom tags (ex: _mood_, _genre_, _albummood_, _albumgrouping_, ...)
|
* Custom tags (ex: _mood_, _genre_, _albummood_, _albumgrouping_, ...)
|
||||||
* MusicBrainzID support to handle duplicated artist and release names
|
* MusicBrainzID support to handle duplicated artist and release names
|
||||||
* ReplayGain support
|
* Disc subtitles support
|
||||||
* _Systemd_ integration
|
* _Systemd_ integration
|
||||||
* Subsonic API, with the following additional features:
|
* Subsonic API, with the following additional features:
|
||||||
* Playlists
|
* Playlists
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
|
|
||||||
namespace Database {
|
namespace Database {
|
||||||
|
|
||||||
#define LMS_DATABASE_VERSION 22
|
#define LMS_DATABASE_VERSION 23
|
||||||
|
|
||||||
using Version = std::size_t;
|
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
|
// Just increment the scan version of the settings to make the next scheduled scan rescan everything
|
||||||
ScanSettings::get(*this).modify()->incScanVersion();
|
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
|
else
|
||||||
{
|
{
|
||||||
LMS_LOG(DB, ERROR) << "Database version " << version << " cannot be handled using migration";
|
LMS_LOG(DB, ERROR) << "Database version " << version << " cannot be handled using migration";
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ class Track : public Wt::Dbo::Dbo<Track>
|
|||||||
void setDiscNumber(int num) { _discNumber = num; }
|
void setDiscNumber(int num) { _discNumber = num; }
|
||||||
void setTotalTrack(std::optional<int> totalTrack) { totalTrack ? _totalTrack = *totalTrack : 0; }
|
void setTotalTrack(std::optional<int> totalTrack) { totalTrack ? _totalTrack = *totalTrack : 0; }
|
||||||
void setTotalDisc(std::optional<int> totalDisc) { totalDisc ? _totalDisc = *totalDisc : 0; }
|
void setTotalDisc(std::optional<int> 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 setName(const std::string& name) { _name = std::string(name, 0, _maxNameLength); }
|
||||||
void setDuration(std::chrono::milliseconds duration) { _duration = duration; }
|
void setDuration(std::chrono::milliseconds duration) { _duration = duration; }
|
||||||
void setLastWriteTime(Wt::WDateTime time) { _fileLastWrite = time; }
|
void setLastWriteTime(Wt::WDateTime time) { _fileLastWrite = time; }
|
||||||
@@ -113,6 +114,7 @@ class Track : public Wt::Dbo::Dbo<Track>
|
|||||||
std::optional<std::size_t> getTrackNumber() const;
|
std::optional<std::size_t> getTrackNumber() const;
|
||||||
std::optional<std::size_t> getTotalTrack() const;
|
std::optional<std::size_t> getTotalTrack() const;
|
||||||
std::optional<std::size_t> getDiscNumber() const;
|
std::optional<std::size_t> getDiscNumber() const;
|
||||||
|
const std::string& getDiscSubtitle() const { return _discSubtitle; }
|
||||||
std::optional<std::size_t> getTotalDisc() const;
|
std::optional<std::size_t> getTotalDisc() const;
|
||||||
std::string getName() const { return _name; }
|
std::string getName() const { return _name; }
|
||||||
std::filesystem::path getPath() const { return _filePath; }
|
std::filesystem::path getPath() const { return _filePath; }
|
||||||
@@ -145,6 +147,7 @@ class Track : public Wt::Dbo::Dbo<Track>
|
|||||||
Wt::Dbo::field(a, _scanVersion, "scan_version");
|
Wt::Dbo::field(a, _scanVersion, "scan_version");
|
||||||
Wt::Dbo::field(a, _trackNumber, "track_number");
|
Wt::Dbo::field(a, _trackNumber, "track_number");
|
||||||
Wt::Dbo::field(a, _discNumber, "disc_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, _totalTrack, "total_track");
|
||||||
Wt::Dbo::field(a, _totalDisc, "total_disc");
|
Wt::Dbo::field(a, _totalDisc, "total_disc");
|
||||||
Wt::Dbo::field(a, _name, "name");
|
Wt::Dbo::field(a, _name, "name");
|
||||||
@@ -177,6 +180,7 @@ class Track : public Wt::Dbo::Dbo<Track>
|
|||||||
int _scanVersion {};
|
int _scanVersion {};
|
||||||
int _trackNumber {};
|
int _trackNumber {};
|
||||||
int _discNumber {};
|
int _discNumber {};
|
||||||
|
std::string _discSubtitle;
|
||||||
int _totalTrack {};
|
int _totalTrack {};
|
||||||
int _totalDisc {};
|
int _totalDisc {};
|
||||||
std::string _name;
|
std::string _name;
|
||||||
|
|||||||
@@ -211,6 +211,12 @@ AvFormatParser::parse(const std::filesystem::path& p, bool debug)
|
|||||||
{
|
{
|
||||||
track.musicBrainzTrackID = UUID::fromString(value);
|
track.musicBrainzTrackID = UUID::fromString(value);
|
||||||
}
|
}
|
||||||
|
else if (tag == "TSST"
|
||||||
|
|| tag == "DISCSUBTITLE"
|
||||||
|
|| tag == "SETSUBTITLE")
|
||||||
|
{
|
||||||
|
track.discSubtitle = value;
|
||||||
|
}
|
||||||
else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end())
|
else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end())
|
||||||
{
|
{
|
||||||
std::vector<std::string> clusterNames {StringUtils::splitString(value, "/,;")};
|
std::vector<std::string> clusterNames {StringUtils::splitString(value, "/,;")};
|
||||||
|
|||||||
@@ -270,6 +270,8 @@ TagLibParser::processTag(Track& track, const std::string& tag, const TagLib::Str
|
|||||||
track.albumReplayGain = StringUtils::readAs<float>(value);
|
track.albumReplayGain = StringUtils::readAs<float>(value);
|
||||||
else if (tag == "REPLAYGAIN_TRACK_GAIN")
|
else if (tag == "REPLAYGAIN_TRACK_GAIN")
|
||||||
track.trackReplayGain = StringUtils::readAs<float>(value);
|
track.trackReplayGain = StringUtils::readAs<float>(value);
|
||||||
|
else if (tag == "DISCSUBTITLE" || tag == "SETSUBTITLE")
|
||||||
|
track.discSubtitle = value;
|
||||||
else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end())
|
else if (_clusterTypeNames.find(tag) != _clusterTypeNames.end())
|
||||||
{
|
{
|
||||||
std::set<std::string> clusterNames;
|
std::set<std::string> clusterNames;
|
||||||
@@ -372,8 +374,12 @@ TagLibParser::parse(const std::filesystem::path& p, bool debug)
|
|||||||
{
|
{
|
||||||
if (mp3File->ID3v2Tag())
|
if (mp3File->ID3v2Tag())
|
||||||
{
|
{
|
||||||
if (!mp3File->ID3v2Tag()->frameListMap()["APIC"].isEmpty())
|
const auto& frameListMap {mp3File->ID3v2Tag()->frameListMap()};
|
||||||
|
|
||||||
|
if (!frameListMap["APIC"].isEmpty())
|
||||||
track.hasCover = true;
|
track.hasCover = true;
|
||||||
|
if (!frameListMap["TSST"].isEmpty())
|
||||||
|
properties.insert("DISCSUBTITLE", frameListMap["TSST"].front()->toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
getAPETags(mp3File->APETag());
|
getAPETags(mp3File->APETag());
|
||||||
|
|||||||
@@ -58,25 +58,26 @@ namespace MetaData
|
|||||||
{
|
{
|
||||||
std::vector<Artist> artists;
|
std::vector<Artist> artists;
|
||||||
std::vector<Artist> albumArtists;
|
std::vector<Artist> albumArtists;
|
||||||
std::string title;
|
std::string title;
|
||||||
std::optional<UUID> musicBrainzTrackID;
|
std::optional<UUID> musicBrainzTrackID;
|
||||||
std::optional<UUID> musicBrainzRecordID;
|
std::optional<UUID> musicBrainzRecordID;
|
||||||
std::optional<Album> album;
|
std::optional<Album> album;
|
||||||
Clusters clusters;
|
Clusters clusters;
|
||||||
std::chrono::milliseconds duration {};
|
std::chrono::milliseconds duration;
|
||||||
std::optional<std::size_t> trackNumber;
|
std::optional<std::size_t> trackNumber;
|
||||||
std::optional<std::size_t> totalTrack;
|
std::optional<std::size_t> totalTrack;
|
||||||
std::optional<std::size_t> discNumber;
|
std::optional<std::size_t> discNumber;
|
||||||
std::optional<std::size_t> totalDisc;
|
std::optional<std::size_t> totalDisc;
|
||||||
std::optional<int> year;
|
std::optional<int> year;
|
||||||
std::optional<int> originalYear;
|
std::optional<int> originalYear;
|
||||||
bool hasCover {false};
|
bool hasCover {};
|
||||||
std::vector<AudioStream> audioStreams;
|
std::vector<AudioStream> audioStreams;
|
||||||
std::optional<UUID> acoustID;
|
std::optional<UUID> acoustID;
|
||||||
std::string copyright;
|
std::string copyright;
|
||||||
std::string copyrightURL;
|
std::string copyrightURL;
|
||||||
std::optional<float> trackReplayGain;
|
std::optional<float> trackReplayGain;
|
||||||
std::optional<float> albumReplayGain;
|
std::optional<float> albumReplayGain;
|
||||||
|
std::string discSubtitle;
|
||||||
};
|
};
|
||||||
|
|
||||||
class IParser
|
class IParser
|
||||||
|
|||||||
@@ -738,6 +738,8 @@ MediaScanner::scanAudioFile(const std::filesystem::path& file, bool forceScan, S
|
|||||||
track.modify()->setDiscNumber(trackInfo->discNumber ? *trackInfo->discNumber : 0);
|
track.modify()->setDiscNumber(trackInfo->discNumber ? *trackInfo->discNumber : 0);
|
||||||
track.modify()->setTotalTrack(trackInfo->totalTrack);
|
track.modify()->setTotalTrack(trackInfo->totalTrack);
|
||||||
track.modify()->setTotalDisc(trackInfo->totalDisc);
|
track.modify()->setTotalDisc(trackInfo->totalDisc);
|
||||||
|
if (!trackInfo->discSubtitle.empty())
|
||||||
|
track.modify()->setDiscSubtitle(trackInfo->discSubtitle);
|
||||||
track.modify()->setYear(trackInfo->year ? *trackInfo->year : 0);
|
track.modify()->setYear(trackInfo->year ? *trackInfo->year : 0);
|
||||||
track.modify()->setOriginalYear(trackInfo->originalYear ? *trackInfo->originalYear : 0);
|
track.modify()->setOriginalYear(trackInfo->originalYear ? *trackInfo->originalYear : 0);
|
||||||
|
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ Release::refreshView()
|
|||||||
|
|
||||||
// Expect to be call in asc order
|
// Expect to be call in asc order
|
||||||
std::map<std::size_t, Wt::WContainerWidget*> trackContainers;
|
std::map<std::size_t, Wt::WContainerWidget*> 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);
|
auto it = trackContainers.find(discNumber);
|
||||||
@@ -178,9 +178,13 @@ Release::refreshView()
|
|||||||
}
|
}
|
||||||
|
|
||||||
Wt::WTemplate* disc {rootContainer->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Release.template.disc-entry"))};
|
Wt::WTemplate* disc {rootContainer->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Release.template.disc-entry"))};
|
||||||
disc->bindNew<Wt::WText>("disc-title", Wt::WString::tr("Lms.Explore.Release.disc").arg(discNumber));
|
|
||||||
disc->addFunction("tr", &Wt::WTemplate::Functions::tr);
|
disc->addFunction("tr", &Wt::WTemplate::Functions::tr);
|
||||||
|
|
||||||
|
if (discSubtitle.empty())
|
||||||
|
disc->bindNew<Wt::WText>("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<Wt::WContainerWidget>("tracks")};
|
Wt::WContainerWidget* tracksContainer {disc->bindNew<Wt::WContainerWidget>("tracks")};
|
||||||
trackContainers[discNumber] = tracksContainer;
|
trackContainers[discNumber] = tracksContainer;
|
||||||
|
|
||||||
@@ -198,7 +202,7 @@ Release::refreshView()
|
|||||||
|
|
||||||
Wt::WContainerWidget* container {rootContainer};
|
Wt::WContainerWidget* container {rootContainer};
|
||||||
if (isReleaseMultiDisc && discNumber)
|
if (isReleaseMultiDisc && discNumber)
|
||||||
container = getOrAddDiscContainer(*discNumber);
|
container = getOrAddDiscContainer(*discNumber, track->getDiscSubtitle());
|
||||||
|
|
||||||
Wt::WTemplate* entry {container->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Release.template.entry"))};
|
Wt::WTemplate* entry {container->addNew<Wt::WTemplate>(Wt::WString::tr("Lms.Explore.Release.template.entry"))};
|
||||||
|
|
||||||
|
|||||||
@@ -110,6 +110,9 @@ void parse(MetaData::IParser& parser, const std::filesystem::path& file)
|
|||||||
if (track->discNumber)
|
if (track->discNumber)
|
||||||
std::cout << "Disc: " << *track->discNumber << std::endl;
|
std::cout << "Disc: " << *track->discNumber << std::endl;
|
||||||
|
|
||||||
|
if (!track->discSubtitle.empty())
|
||||||
|
std::cout << "Disc Subtitle: " << track->discSubtitle << std::endl;
|
||||||
|
|
||||||
if (track->totalDisc)
|
if (track->totalDisc)
|
||||||
std::cout << "TotalDisc: " << *track->totalDisc << std::endl;
|
std::cout << "TotalDisc: " << *track->totalDisc << std::endl;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user