Merge branch 'lyrics-fixes' for release v3.60.1
This commit is contained in:
@@ -34,59 +34,63 @@ namespace lms::metadata
|
|||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
std::string_view getSubmatchString(const std::csub_match& submatch)
|
// Parse a single line with a tag like [ar: Artist] and set the appropriate fields in the Lyrics object
|
||||||
|
bool parseTag(std::string_view line, Lyrics& lyrics)
|
||||||
{
|
{
|
||||||
assert(submatch.matched);
|
if (line.empty())
|
||||||
return std::string_view{ submatch.first, static_cast<std::string_view::size_type>(submatch.length()) };
|
return false;
|
||||||
}
|
|
||||||
|
|
||||||
// Parse a single line with ID tags like [ar: Artist] and set the appropriate fields in the Lyrics object
|
if (line.front() != '[' || line.back() != ']') // consider lines are trimmed
|
||||||
bool parseIDTag(std::string_view line, Lyrics& lyrics)
|
return false;
|
||||||
{
|
|
||||||
static const std::regex idTagRegex{ R"(^\[([a-zA-Z_]+):(.+?)\])" };
|
|
||||||
std::cmatch match;
|
|
||||||
|
|
||||||
if (std::regex_search(line.data(), line.data() + line.size(), match, idTagRegex))
|
const auto separator{ line.find(':') };
|
||||||
|
if (separator == std::string_view::npos)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const std::string_view tagType{ core::stringUtils::stringTrim(line.substr(1, separator - 1)) };
|
||||||
|
const std::string_view tagValue{ core::stringUtils::stringTrim(line.substr(separator + 1, line.size() - separator - 2)) };
|
||||||
|
|
||||||
|
if (tagType.empty())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// check for timestamps
|
||||||
|
if (std::any_of(tagType.begin(), tagType.end(), [](char c) { return std::isdigit(c); }))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (tagType == "ar")
|
||||||
{
|
{
|
||||||
std::string_view tagType{ getSubmatchString(match[1]) };
|
lyrics.displayArtist = tagValue;
|
||||||
std::string_view tagValue{ core::stringUtils::stringTrim(getSubmatchString(match[2])) };
|
|
||||||
|
|
||||||
if (tagType == "ar")
|
|
||||||
{
|
|
||||||
lyrics.displayArtist = tagValue;
|
|
||||||
}
|
|
||||||
else if (tagType == "al")
|
|
||||||
{
|
|
||||||
lyrics.displayAlbum = tagValue;
|
|
||||||
}
|
|
||||||
else if (tagType == "ti")
|
|
||||||
{
|
|
||||||
lyrics.displayTitle = tagValue;
|
|
||||||
}
|
|
||||||
else if (tagType == "la")
|
|
||||||
{
|
|
||||||
lyrics.language = tagValue;
|
|
||||||
}
|
|
||||||
else if (tagType == "offset")
|
|
||||||
{
|
|
||||||
if (const auto value{ core::stringUtils::readAs<int>(tagValue) })
|
|
||||||
lyrics.offset = std::chrono::milliseconds{ *value };
|
|
||||||
}
|
|
||||||
// not interrested by other tags like 'duration', 'id', etc.
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
else if (tagType == "al")
|
||||||
|
{
|
||||||
|
lyrics.displayAlbum = tagValue;
|
||||||
|
}
|
||||||
|
else if (tagType == "ti")
|
||||||
|
{
|
||||||
|
lyrics.displayTitle = tagValue;
|
||||||
|
}
|
||||||
|
else if (tagType == "la")
|
||||||
|
{
|
||||||
|
lyrics.language = tagValue;
|
||||||
|
}
|
||||||
|
else if (tagType == "offset")
|
||||||
|
{
|
||||||
|
if (const auto value{ core::stringUtils::readAs<int>(tagValue) })
|
||||||
|
lyrics.offset = std::chrono::milliseconds{ *value };
|
||||||
|
}
|
||||||
|
// not interrested by other tags like 'duration', 'id', etc.
|
||||||
|
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse timestamps from a line and return the associated times in milliseconds
|
// Parse timestamps from a line, update the associated times in milliseconds and return the remaining line
|
||||||
void extractTimestamps(std::string_view line, std::vector<std::chrono::milliseconds>& timestamps)
|
std::string_view extractTimestamps(std::string_view line, std::vector<std::chrono::milliseconds>& timestamps)
|
||||||
{
|
{
|
||||||
timestamps.clear();
|
timestamps.clear();
|
||||||
static const std::regex timeTagRegex{ R"(\[(?:(\d{1,2}):)?(\d{1,2}):(\d{1,2})(?:\.(\d{1,3}))?\])" };
|
static const std::regex timeTagRegex{ R"(\[(?:(\d{1,2}):)?(\d{1,2}):(\d{1,2})(?:\.(\d{1,3}))?\])" };
|
||||||
std::cregex_iterator regexIt(line.begin(), line.end(), timeTagRegex);
|
std::cregex_iterator regexIt(line.begin(), line.end(), timeTagRegex);
|
||||||
std::cregex_iterator regexEnd;
|
std::cregex_iterator regexEnd;
|
||||||
|
std::string_view::size_type offset{};
|
||||||
|
|
||||||
while (regexIt != regexEnd)
|
while (regexIt != regexEnd)
|
||||||
{
|
{
|
||||||
@@ -107,15 +111,12 @@ namespace lms::metadata
|
|||||||
currentTimestamp += std::chrono::milliseconds{ fractional };
|
currentTimestamp += std::chrono::milliseconds{ fractional };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
offset = match[0].second - line.data();
|
||||||
timestamps.push_back(currentTimestamp);
|
timestamps.push_back(currentTimestamp);
|
||||||
++regexIt;
|
++regexIt;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Extract the lyric text from a line, removing any timestamps
|
return line.substr(offset);
|
||||||
std::string_view extractLyricText(std::string_view line)
|
|
||||||
{
|
|
||||||
return line.substr(line.find_last_of(']') + 1);
|
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@@ -172,10 +173,10 @@ namespace lms::metadata
|
|||||||
if (currentState == State::None && trimmedLine.empty())
|
if (currentState == State::None && trimmedLine.empty())
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (parseIDTag(trimmedLine, lyrics))
|
if (parseTag(trimmedLine, lyrics))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
extractTimestamps(trimmedLine, timestamps);
|
const std::string_view lyricText{ extractTimestamps(trimmedLine, timestamps) };
|
||||||
|
|
||||||
// If there are timestamps, add as synchronized lyrics
|
// If there are timestamps, add as synchronized lyrics
|
||||||
if (!timestamps.empty())
|
if (!timestamps.empty())
|
||||||
@@ -186,7 +187,6 @@ namespace lms::metadata
|
|||||||
currentState = State::SynchronizedLyrics;
|
currentState = State::SynchronizedLyrics;
|
||||||
|
|
||||||
applyAccumulatedLyrics();
|
applyAccumulatedLyrics();
|
||||||
std::string_view lyricText{ extractLyricText(trimmedLine) };
|
|
||||||
for (std::chrono::milliseconds timestamp : timestamps)
|
for (std::chrono::milliseconds timestamp : timestamps)
|
||||||
lyrics.synchronizedLines.emplace(timestamp, lyricText);
|
lyrics.synchronizedLines.emplace(timestamp, lyricText);
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,34 @@ namespace lms::metadata::tests
|
|||||||
EXPECT_EQ(lyrics.synchronizedLines.find(9s + 160ms)->second, "I, I just woke up from a dream");
|
EXPECT_EQ(lyrics.synchronizedLines.find(9s + 160ms)->second, "I, I just woke up from a dream");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(Lyrics, tagsWithSpaces)
|
||||||
|
{
|
||||||
|
std::istringstream is{ R"([al: dqsxdkbu ]
|
||||||
|
[00:09.16]I, I just woke up from a dream)" };
|
||||||
|
|
||||||
|
const Lyrics lyrics{ parseLyrics(is) };
|
||||||
|
|
||||||
|
EXPECT_EQ(lyrics.unsynchronizedLines.size(), 0);
|
||||||
|
ASSERT_EQ(lyrics.synchronizedLines.size(), 1);
|
||||||
|
EXPECT_EQ(lyrics.displayAlbum, "dqsxdkbu");
|
||||||
|
ASSERT_TRUE(lyrics.synchronizedLines.contains(9s + 160ms));
|
||||||
|
EXPECT_EQ(lyrics.synchronizedLines.find(9s + 160ms)->second, "I, I just woke up from a dream");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(Lyrics, tagIDsWithSpaces)
|
||||||
|
{
|
||||||
|
std::istringstream is{ R"([ al : dqsxdkbu ]
|
||||||
|
[00:09.16]I, I just woke up from a dream)" };
|
||||||
|
|
||||||
|
const Lyrics lyrics{ parseLyrics(is) };
|
||||||
|
|
||||||
|
EXPECT_EQ(lyrics.unsynchronizedLines.size(), 0);
|
||||||
|
ASSERT_EQ(lyrics.synchronizedLines.size(), 1);
|
||||||
|
EXPECT_EQ(lyrics.displayAlbum, "dqsxdkbu");
|
||||||
|
ASSERT_TRUE(lyrics.synchronizedLines.contains(9s + 160ms));
|
||||||
|
EXPECT_EQ(lyrics.synchronizedLines.find(9s + 160ms)->second, "I, I just woke up from a dream");
|
||||||
|
}
|
||||||
|
|
||||||
TEST(Lyrics, tagAtTheEndOfLyrics)
|
TEST(Lyrics, tagAtTheEndOfLyrics)
|
||||||
{
|
{
|
||||||
std::istringstream is{ R"([00:03.30]Ooh, ooh
|
std::istringstream is{ R"([00:03.30]Ooh, ooh
|
||||||
@@ -163,6 +191,22 @@ Some unsynchronized lyrics
|
|||||||
EXPECT_EQ(lyrics.synchronizedLines.find(3s + 300ms)->second, "Ooh, ooh");
|
EXPECT_EQ(lyrics.synchronizedLines.find(3s + 300ms)->second, "Ooh, ooh");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(Lyrics, synchronized_withTimestampsDelimiters)
|
||||||
|
{
|
||||||
|
std::istringstream is{ R"([00:03.30]Ooh, ooh ] [])" };
|
||||||
|
|
||||||
|
const Lyrics lyrics{ parseLyrics(is) };
|
||||||
|
|
||||||
|
EXPECT_TRUE(lyrics.displayArtist.empty());
|
||||||
|
EXPECT_TRUE(lyrics.displayAlbum.empty());
|
||||||
|
EXPECT_TRUE(lyrics.displayTitle.empty());
|
||||||
|
EXPECT_EQ(lyrics.offset, std::chrono::milliseconds{ 0 });
|
||||||
|
EXPECT_EQ(lyrics.unsynchronizedLines.size(), 0);
|
||||||
|
ASSERT_EQ(lyrics.synchronizedLines.size(), 1);
|
||||||
|
ASSERT_TRUE(lyrics.synchronizedLines.contains(3s + 300ms));
|
||||||
|
EXPECT_EQ(lyrics.synchronizedLines.find(3s + 300ms)->second, "Ooh, ooh ] []");
|
||||||
|
}
|
||||||
|
|
||||||
TEST(Lyrics, synchronized_timestampFormats)
|
TEST(Lyrics, synchronized_timestampFormats)
|
||||||
{
|
{
|
||||||
std::istringstream is{ R"([00:03.30]First line
|
std::istringstream is{ R"([00:03.30]First line
|
||||||
|
|||||||
@@ -28,6 +28,14 @@ namespace lms::scanner
|
|||||||
{
|
{
|
||||||
context.stats.totalFileCount = 0;
|
context.stats.totalFileCount = 0;
|
||||||
|
|
||||||
|
std::vector<std::filesystem::path> supportedExtensions;
|
||||||
|
for (const auto& extension : _settings.supportedAudioFileExtensions)
|
||||||
|
supportedExtensions.emplace_back(extension);
|
||||||
|
for (const auto& extension : _settings.supportedImageFileExtensions)
|
||||||
|
supportedExtensions.emplace_back(extension);
|
||||||
|
for (const auto& extension : _settings.supportedLyricsFileExtensions)
|
||||||
|
supportedExtensions.emplace_back(extension);
|
||||||
|
|
||||||
for (const ScannerSettings::MediaLibraryInfo& mediaLibrary : _settings.mediaLibraries)
|
for (const ScannerSettings::MediaLibraryInfo& mediaLibrary : _settings.mediaLibraries)
|
||||||
{
|
{
|
||||||
std::size_t currentDirectoryProcessElemsCount{};
|
std::size_t currentDirectoryProcessElemsCount{};
|
||||||
@@ -36,7 +44,7 @@ namespace lms::scanner
|
|||||||
if (_abortScan)
|
if (_abortScan)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (!ec && (core::pathUtils::hasFileAnyExtension(path, _settings.supportedAudioFileExtensions) || core::pathUtils::hasFileAnyExtension(path, _settings.supportedImageFileExtensions)))
|
if (!ec && core::pathUtils::hasFileAnyExtension(path, supportedExtensions))
|
||||||
{
|
{
|
||||||
context.currentStepStats.processedElems++;
|
context.currentStepStats.processedElems++;
|
||||||
currentDirectoryProcessElemsCount++;
|
currentDirectoryProcessElemsCount++;
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ namespace lms::api::subsonic
|
|||||||
{ "/savePlayQueue", { handleNotImplemented } },
|
{ "/savePlayQueue", { handleNotImplemented } },
|
||||||
|
|
||||||
// Media library scanning
|
// Media library scanning
|
||||||
{ "/getScanStatus", { Scan::handleGetScanStatus, { db::UserType::ADMIN } } },
|
{ "/getScanStatus", { Scan::handleGetScanStatus } },
|
||||||
{ "/startScan", { Scan::handleStartScan, { db::UserType::ADMIN } } },
|
{ "/startScan", { Scan::handleStartScan, { db::UserType::ADMIN } } },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ namespace lms::api::subsonic
|
|||||||
if (lyrics->getOffset() != std::chrono::milliseconds{})
|
if (lyrics->getOffset() != std::chrono::milliseconds{})
|
||||||
lyricsNode.setAttribute("offset", lyrics->getOffset().count());
|
lyricsNode.setAttribute("offset", lyrics->getOffset().count());
|
||||||
|
|
||||||
lyricsNode.createEmptyArrayChild("lines");
|
lyricsNode.createEmptyArrayChild("line");
|
||||||
auto addLine{ [&](std::string&& line, std::optional<std::chrono::milliseconds> timestamp = std::nullopt) {
|
auto addLine{ [&](std::string&& line, std::optional<std::chrono::milliseconds> timestamp = std::nullopt) {
|
||||||
Response::Node lineNode;
|
Response::Node lineNode;
|
||||||
if (timestamp)
|
if (timestamp)
|
||||||
@@ -98,7 +98,7 @@ namespace lms::api::subsonic
|
|||||||
lineNode.setValue(std::move(line));
|
lineNode.setValue(std::move(line));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
lyricsNode.addArrayChild("lines", std::move(lineNode));
|
lyricsNode.addArrayChild("line", std::move(lineNode));
|
||||||
} };
|
} };
|
||||||
|
|
||||||
if (!lyrics->isSynchronized())
|
if (!lyrics->isSynchronized())
|
||||||
|
|||||||
Reference in New Issue
Block a user