Updated readme + sanitize subtitle file names before matching, ref #699
This commit is contained in:
@@ -90,6 +90,9 @@ If neither exists, it will then search for a file named `artist` (or another nam
|
|||||||
2. Scan for the image: the directory is scanned starting from this common path, moving upwards if needed, until the artist image file is found.
|
2. Scan for the image: the directory is scanned starting from this common path, moving upwards if needed, until the artist image file is found.
|
||||||
3. Fallback search: if no image is found, _LMS_ will then search within each individual album folder.
|
3. Fallback search: if no image is found, _LMS_ will then search within each individual album folder.
|
||||||
|
|
||||||
|
## Disc image lookup
|
||||||
|
_LMS_ automatically associates images with each disc in your collection. Name the image file after the disc's subtitle or another identifier configured in `lms.conf` (see the `medium-image-file-names` setting), and place it in the same directory as the disc's tracks. If no suitable image is found, LMS will also look for embedded images within the tracks of the disc.
|
||||||
|
|
||||||
## Playlist support
|
## Playlist support
|
||||||
_LMS_ supports playlist files in `m3u` and `m3u8` formats. These playlists are synced during the scan process and are available as public shared playlists.
|
_LMS_ supports playlist files in `m3u` and `m3u8` formats. These playlists are synced during the scan process and are available as public shared playlists.
|
||||||
|
|
||||||
|
|||||||
@@ -86,4 +86,24 @@ namespace lms::core::pathUtils
|
|||||||
|
|
||||||
return longestCommonPath;
|
return longestCommonPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string sanitizeFileStem(const std::string_view fileStem)
|
||||||
|
{
|
||||||
|
// Keep UTF8-encoded characters, but skip illegal ASCII characters
|
||||||
|
constexpr std::array<unsigned char, 9> illegalChars{ '/', '\\', ':', '*', '?', '"', '<', '>', '|' };
|
||||||
|
static_assert(std::all_of(std::begin(illegalChars), std::end(illegalChars), [](unsigned char c) { return c < 128; }), "Illegal characters must be ASCII");
|
||||||
|
|
||||||
|
std::string sanitized;
|
||||||
|
sanitized.reserve(fileStem.size());
|
||||||
|
|
||||||
|
for (const char c : fileStem)
|
||||||
|
{
|
||||||
|
if (std::any_of(std::begin(illegalChars), std::end(illegalChars), [c](char illegalChar) { return c == illegalChar; }))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
sanitized.push_back(c);
|
||||||
|
}
|
||||||
|
|
||||||
|
return sanitized;
|
||||||
|
}
|
||||||
} // namespace lms::core::pathUtils
|
} // namespace lms::core::pathUtils
|
||||||
|
|||||||
@@ -21,6 +21,8 @@
|
|||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <span>
|
#include <span>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
#include <Wt/WDateTime.h>
|
#include <Wt/WDateTime.h>
|
||||||
|
|
||||||
@@ -54,4 +56,7 @@ namespace lms::core::pathUtils
|
|||||||
|
|
||||||
return longestCommonPath;
|
return longestCommonPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A method that sanitize a file stem, removing any illegal chars
|
||||||
|
std::string sanitizeFileStem(std::string_view fileStem);
|
||||||
} // namespace lms::core::pathUtils
|
} // namespace lms::core::pathUtils
|
||||||
|
|||||||
@@ -103,4 +103,34 @@ namespace lms::core::pathUtils::tests
|
|||||||
EXPECT_EQ(core::pathUtils::isPathInRootPath(test.path, test.rootPath), test.expectedResult) << "Failed: path = " << test.path << ", rootPath = " << test.rootPath;
|
EXPECT_EQ(core::pathUtils::isPathInRootPath(test.path, test.rootPath), test.expectedResult) << "Failed: path = " << test.path << ", rootPath = " << test.rootPath;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(Path, sanitizeFileStem)
|
||||||
|
{
|
||||||
|
struct TestCase
|
||||||
|
{
|
||||||
|
std::string input;
|
||||||
|
std::string_view expectedOutput;
|
||||||
|
};
|
||||||
|
|
||||||
|
TestCase tests[]{
|
||||||
|
{ "", "" }, // empty input
|
||||||
|
{ "valid_file_name", "valid_file_name" },
|
||||||
|
{ "invalid:file*name?", "invalidfilename" },
|
||||||
|
{ "another|invalid<name>", "anotherinvalidname" },
|
||||||
|
{ "/leading/slash", "leadingslash" },
|
||||||
|
{ "\\backslash\\file", "backslashfile" },
|
||||||
|
{ "file_with_äöüß", "file_with_äöüß" }, // keep German umlauts
|
||||||
|
{ "file_with_éèêë", "file_with_éèêë" }, // keep French accents
|
||||||
|
{ "héllo 漢字", "héllo 漢字" }, // keep UTF8 characters
|
||||||
|
{ "file_with_üñîçødë", "file_with_üñîçødë" }, // keep special characters
|
||||||
|
{ "file_with_!@#$%^&*()_+", "file_with_!@#$%^&()_+" }, // remove special characters
|
||||||
|
{ "file_with_", "file_with_" }, // handle double dots
|
||||||
|
{ "file.with.extension", "file.with.extension" }, // keep extensions
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const TestCase& test : tests)
|
||||||
|
{
|
||||||
|
EXPECT_EQ(core::pathUtils::sanitizeFileStem(test.input), test.expectedOutput) << "Failed: input = " << test.input;
|
||||||
|
}
|
||||||
|
}
|
||||||
} // namespace lms::core::pathUtils::tests
|
} // namespace lms::core::pathUtils::tests
|
||||||
@@ -73,7 +73,7 @@ namespace lms::scanner
|
|||||||
{
|
{
|
||||||
db::Image::FindParameters params;
|
db::Image::FindParameters params;
|
||||||
params.setDirectory(directory->getId());
|
params.setDirectory(directory->getId());
|
||||||
params.setFileStem(fileStem);
|
params.setFileStem(fileStem); // no need to sanitize here, user is responsible for providing sanitized file stems in conf file
|
||||||
|
|
||||||
db::Image::find(session, params, [&](const db::Image::pointer foundImg) {
|
db::Image::find(session, params, [&](const db::Image::pointer foundImg) {
|
||||||
if (!image)
|
if (!image)
|
||||||
@@ -135,13 +135,11 @@ namespace lms::scanner
|
|||||||
{
|
{
|
||||||
// Expect layout like this:
|
// Expect layout like this:
|
||||||
// ReleaseArtist/Release/Tracks'
|
// ReleaseArtist/Release/Tracks'
|
||||||
// /artist.jpg
|
// /someUserConfiguredArtistFile.jpg
|
||||||
// /someOtherUserConfiguredArtistFile.jpg
|
|
||||||
//
|
//
|
||||||
// Or:
|
// Or:
|
||||||
// ReleaseArtist/SomeGrouping/Release/Tracks'
|
// ReleaseArtist/SomeGrouping/Release/Tracks'
|
||||||
// /artist.jpg
|
// /someUserConfiguredArtistFile.jpg
|
||||||
// /someOtherUserConfiguredArtistFile.jpg
|
|
||||||
//
|
//
|
||||||
std::filesystem::path directoryToInspect{ core::pathUtils::getLongestCommonPath(std::cbegin(releasePaths), std::cend(releasePaths)) };
|
std::filesystem::path directoryToInspect{ core::pathUtils::getLongestCommonPath(std::cbegin(releasePaths), std::cend(releasePaths)) };
|
||||||
while (true)
|
while (true)
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
#include "core/IConfig.hpp"
|
#include "core/IConfig.hpp"
|
||||||
#include "core/IJob.hpp"
|
#include "core/IJob.hpp"
|
||||||
#include "core/ILogger.hpp"
|
#include "core/ILogger.hpp"
|
||||||
|
#include "core/Path.hpp"
|
||||||
#include "database/IDb.hpp"
|
#include "database/IDb.hpp"
|
||||||
#include "database/Session.hpp"
|
#include "database/Session.hpp"
|
||||||
#include "database/Types.hpp"
|
#include "database/Types.hpp"
|
||||||
@@ -105,8 +106,11 @@ namespace lms::scanner
|
|||||||
if (image)
|
if (image)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (const std::string_view mediumName{ medium->getName() }; !mediumName.empty())
|
if (const std::string mediumName{ core::pathUtils::sanitizeFileStem(medium->getName()) }; !mediumName.empty())
|
||||||
image = findImageInDirectory(session, directory, std::span{ &mediumName, 1 });
|
{
|
||||||
|
std::string_view mediumNameView{ mediumName };
|
||||||
|
image = findImageInDirectory(session, directory, std::span{ &mediumNameView, 1 });
|
||||||
|
}
|
||||||
|
|
||||||
if (!image)
|
if (!image)
|
||||||
image = findImageInDirectory(session, directory, searchParams.mediumFileNames);
|
image = findImageInDirectory(session, directory, searchParams.mediumFileNames);
|
||||||
|
|||||||
Reference in New Issue
Block a user