feat(translate): save translated subtitles next to the video
This commit is contained in:
1 parent
b34a01a92f
commit
5d00262c19
13 files changed
+712
-65
No files matched your search
@@ -24,3 +24,8 @@ ecm_add_test(trackselectiontest.cpp
|
||||
TEST_NAME trackselectiontest
|
||||
LINK_LIBRARIES qtpotato_core Qt6::Test
|
||||
)
|
||||
|
||||
ecm_add_test(translationstoretest.cpp
|
||||
TEST_NAME translationstoretest
|
||||
LINK_LIBRARIES qtpotato_core Qt6::Test
|
||||
)
|
||||
@@ -67,6 +67,19 @@ private Q_SLOTS:
|
||||
QCOMPARE(TrackSelection::pickSubtitle(tracks, {u"de"_s}, {u"en"_s}, 1, true), 1);
|
||||
}
|
||||
|
||||
// A saved translation is turned on when the file opens, over mpv's choice and the
|
||||
// fallback, but not over a subtitle in a preferred language.
|
||||
void picksSavedTranslation()
|
||||
{
|
||||
const QList<Track> tracks{sub(1, u"rus"_s), sub(2, u"rus"_s, u"subrip"_s, false, true), sub(3, u"eng"_s), sub(4, u"vi"_s)};
|
||||
QCOMPARE(TrackSelection::pickSubtitle(tracks, {}, {u"en"_s}, 0, true, 4), 4);
|
||||
QCOMPARE(TrackSelection::pickSubtitle(tracks, {}, {u"en"_s}, 2, true, 4), 4);
|
||||
QCOMPARE(TrackSelection::pickSubtitle(tracks, {u"en"_s}, {u"en"_s}, 0, true, 4), 3);
|
||||
// Automatic subtitles turned off: keep mpv's choice.
|
||||
QCOMPARE(TrackSelection::pickSubtitle(tracks, {}, {u"en"_s}, 0, false, 4), 0);
|
||||
QCOMPARE(TrackSelection::pickSubtitle(tracks, {}, {u"en"_s}, 0, true, 0), 3);
|
||||
}
|
||||
|
||||
void picksAudioByPreference()
|
||||
{
|
||||
const QList<Track> tracks{sub(1, u"en"_s, u"aac"_s), sub(2, u"jpn"_s, u"aac"_s)};
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
#include "translationstore.h"
|
||||
|
||||
#include <QFile>
|
||||
#include <QTemporaryDir>
|
||||
#include <QTest>
|
||||
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
namespace
|
||||
{
|
||||
void writeText(const QString &path, const QByteArray &data)
|
||||
{
|
||||
QFile file(path);
|
||||
QVERIFY(file.open(QIODevice::WriteOnly));
|
||||
file.write(data);
|
||||
}
|
||||
|
||||
QByteArray readText(const QString &path)
|
||||
{
|
||||
QFile file(path);
|
||||
return file.open(QIODevice::ReadOnly) ? file.readAll() : QByteArray();
|
||||
}
|
||||
}
|
||||
|
||||
class TranslationStoreTest : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private:
|
||||
QTemporaryDir m_dir;
|
||||
QString m_videos;
|
||||
QString m_cache;
|
||||
QString m_video;
|
||||
QUrl m_videoUrl;
|
||||
|
||||
// The translation as TranslationService leaves it in the cache before publishing.
|
||||
QString cached(const QString &key, const QByteArray &data)
|
||||
{
|
||||
const QString path = TranslationStore::cachePath(m_cache, m_videoUrl, key, u"vi"_s);
|
||||
writeText(path, data);
|
||||
return path;
|
||||
}
|
||||
|
||||
private Q_SLOTS:
|
||||
void init()
|
||||
{
|
||||
QVERIFY(m_dir.isValid());
|
||||
QDir root(m_dir.path());
|
||||
root.removeRecursively();
|
||||
QVERIFY(root.mkpath(u"videos"_s));
|
||||
QVERIFY(root.mkpath(u"cache"_s));
|
||||
m_videos = m_dir.filePath(u"videos"_s);
|
||||
m_cache = m_dir.filePath(u"cache"_s);
|
||||
m_video = m_videos + u"/Show.S01E01.mkv"_s;
|
||||
writeText(m_video, "video");
|
||||
m_videoUrl = QUrl::fromLocalFile(m_video);
|
||||
}
|
||||
|
||||
void namesFiles()
|
||||
{
|
||||
QCOMPARE(TranslationStore::languageSuffix(u"Vietnamese"_s), u"vi"_s);
|
||||
QCOMPARE(TranslationStore::languageSuffix(u"vietnamese"_s), u"vi"_s);
|
||||
QCOMPARE(TranslationStore::languageSuffix(u"Klingon!"_s), u"klingon"_s);
|
||||
QCOMPARE(TranslationStore::sidecarPaths(u"/films/Show.S01E01.mkv"_s, u"vi"_s),
|
||||
QStringList({u"/films/Show.S01E01.vi.srt"_s, u"/films/Show.S01E01.qtpotato.vi.srt"_s}));
|
||||
QCOMPARE(TranslationStore::cachePath(u"/cache"_s, QUrl::fromLocalFile(u"/films/Show.S01E01.mkv"_s), u"abc"_s, u"vi"_s),
|
||||
u"/cache/Show.S01E01.abc.vi.srt"_s);
|
||||
QCOMPARE(TranslationStore::cachePath(u"/cache"_s, QUrl(u"https://example.com/live.m3u8"_s), u"abc"_s, u"vi"_s),
|
||||
u"/cache/subtitles.abc.vi.srt"_s);
|
||||
QCOMPARE(TranslationStore::videoKey(QUrl(u"https://example.com/live.m3u8"_s)), u"https://example.com/live.m3u8"_s);
|
||||
}
|
||||
|
||||
void writesNextToVideo()
|
||||
{
|
||||
TranslationStore::Registry registry;
|
||||
const QString cache = cached(u"k1"_s, "one");
|
||||
const auto result = TranslationStore::publish(registry, m_videoUrl, u"vi"_s, u"k1"_s, "one", cache);
|
||||
|
||||
const QString sidecar = m_videos + u"/Show.S01E01.vi.srt"_s;
|
||||
QCOMPARE(result.path, sidecar);
|
||||
QVERIFY(result.nextToVideo);
|
||||
QCOMPARE(readText(sidecar), "one");
|
||||
QVERIFY(TranslationStore::isOwned(registry, sidecar));
|
||||
QCOMPARE(TranslationStore::savedTranslation(registry, m_videoUrl, u"vi"_s), sidecar);
|
||||
QCOMPARE(TranslationStore::savedTranslation(registry, m_videoUrl, u"ja"_s), QString());
|
||||
QCOMPARE(registry.find(TranslationStore::videoKey(m_videoUrl), u"vi"_s)->configKey, u"k1"_s);
|
||||
}
|
||||
|
||||
// Different settings give a new translation, which replaces QtPotato's own file.
|
||||
void replacesOwnFileWithNewSettings()
|
||||
{
|
||||
TranslationStore::Registry registry;
|
||||
TranslationStore::publish(registry, m_videoUrl, u"vi"_s, u"k1"_s, "one", cached(u"k1"_s, "one"));
|
||||
const auto result = TranslationStore::publish(registry, m_videoUrl, u"vi"_s, u"k2"_s, "two", cached(u"k2"_s, "two"));
|
||||
|
||||
QCOMPARE(result.path, m_videos + u"/Show.S01E01.vi.srt"_s);
|
||||
QCOMPARE(readText(result.path), "two");
|
||||
QCOMPARE(registry.records.size(), 1);
|
||||
QCOMPARE(registry.records.constFirst().configKey, u"k2"_s);
|
||||
QVERIFY(!QFile::exists(m_videos + u"/Show.S01E01.qtpotato.vi.srt"_s));
|
||||
}
|
||||
|
||||
void keepsUserSubtitle()
|
||||
{
|
||||
const QString userFile = m_videos + u"/Show.S01E01.vi.srt"_s;
|
||||
writeText(userFile, "mine");
|
||||
TranslationStore::Registry registry;
|
||||
QVERIFY(!TranslationStore::isOwned(registry, userFile));
|
||||
|
||||
const auto result = TranslationStore::publish(registry, m_videoUrl, u"vi"_s, u"k1"_s, "one", cached(u"k1"_s, "one"));
|
||||
QCOMPARE(result.path, m_videos + u"/Show.S01E01.qtpotato.vi.srt"_s);
|
||||
QCOMPARE(readText(userFile), "mine");
|
||||
QCOMPARE(readText(result.path), "one");
|
||||
}
|
||||
|
||||
// A file QtPotato wrote and the user then edited counts as the user's.
|
||||
void keepsEditedTranslation()
|
||||
{
|
||||
TranslationStore::Registry registry;
|
||||
const auto first = TranslationStore::publish(registry, m_videoUrl, u"vi"_s, u"k1"_s, "one", cached(u"k1"_s, "one"));
|
||||
writeText(first.path, "one, fixed by hand");
|
||||
QVERIFY(!TranslationStore::isOwned(registry, first.path));
|
||||
|
||||
const auto second = TranslationStore::publish(registry, m_videoUrl, u"vi"_s, u"k2"_s, "two", cached(u"k2"_s, "two"));
|
||||
QCOMPARE(second.path, m_videos + u"/Show.S01E01.qtpotato.vi.srt"_s);
|
||||
QCOMPARE(readText(first.path), "one, fixed by hand");
|
||||
}
|
||||
|
||||
// Once on ".qtpotato.vi.srt", stay there instead of leaving a second copy behind.
|
||||
void staysOnPreviousFile()
|
||||
{
|
||||
const QString userFile = m_videos + u"/Show.S01E01.vi.srt"_s;
|
||||
writeText(userFile, "mine");
|
||||
TranslationStore::Registry registry;
|
||||
const auto first = TranslationStore::publish(registry, m_videoUrl, u"vi"_s, u"k1"_s, "one", cached(u"k1"_s, "one"));
|
||||
QFile::remove(userFile);
|
||||
|
||||
const auto second = TranslationStore::publish(registry, m_videoUrl, u"vi"_s, u"k2"_s, "two", cached(u"k2"_s, "two"));
|
||||
QCOMPARE(second.path, first.path);
|
||||
QCOMPARE(readText(second.path), "two");
|
||||
QVERIFY(!QFile::exists(userFile));
|
||||
}
|
||||
|
||||
void fallsBackToCacheWhenFolderIsReadOnly()
|
||||
{
|
||||
QFile::setPermissions(m_videos, QFileDevice::ReadOwner | QFileDevice::ExeOwner);
|
||||
auto restore = qScopeGuard([this] {
|
||||
QFile::setPermissions(m_videos, QFileDevice::ReadOwner | QFileDevice::WriteOwner | QFileDevice::ExeOwner);
|
||||
});
|
||||
if (QFileInfo(m_videos).isWritable()) {
|
||||
QSKIP("Permissions are not enforced here (running as root?)");
|
||||
}
|
||||
|
||||
TranslationStore::Registry registry;
|
||||
const QString cache = cached(u"k1"_s, "one");
|
||||
const auto result = TranslationStore::publish(registry, m_videoUrl, u"vi"_s, u"k1"_s, "one", cache);
|
||||
QCOMPARE(result.path, cache);
|
||||
QVERIFY(!result.nextToVideo);
|
||||
QCOMPARE(TranslationStore::savedTranslation(registry, m_videoUrl, u"vi"_s), cache);
|
||||
}
|
||||
|
||||
void fallsBackToCacheForStreams()
|
||||
{
|
||||
const QUrl stream(u"https://example.com/live.m3u8"_s);
|
||||
const QString cache = TranslationStore::cachePath(m_cache, stream, u"k1"_s, u"vi"_s);
|
||||
writeText(cache, "one");
|
||||
TranslationStore::Registry registry;
|
||||
const auto result = TranslationStore::publish(registry, stream, u"vi"_s, u"k1"_s, "one", cache);
|
||||
QCOMPARE(result.path, cache);
|
||||
QCOMPARE(TranslationStore::savedTranslation(registry, stream, u"vi"_s), cache);
|
||||
}
|
||||
|
||||
void glossaryRewriteKeepsOwnership()
|
||||
{
|
||||
TranslationStore::Registry registry;
|
||||
const auto result = TranslationStore::publish(registry, m_videoUrl, u"vi"_s, u"k1"_s, "one", cached(u"k1"_s, "one"));
|
||||
writeText(result.path, "one, new glossary");
|
||||
TranslationStore::recordRewrite(registry, result.path, "one, new glossary");
|
||||
QVERIFY(TranslationStore::isOwned(registry, result.path));
|
||||
}
|
||||
|
||||
void savesAndLoadsRegistry()
|
||||
{
|
||||
const QString file = m_dir.filePath(u"data/translations.json"_s);
|
||||
TranslationStore::Registry registry;
|
||||
const auto result = TranslationStore::publish(registry, m_videoUrl, u"vi"_s, u"k1"_s, "one", cached(u"k1"_s, "one"));
|
||||
// A record whose file is gone is not worth keeping.
|
||||
registry.put({u"/gone.mkv"_s, u"vi"_s, u"/gone.vi.srt"_s, u"k0"_s, u"00"_s});
|
||||
QVERIFY(TranslationStore::save(file, registry));
|
||||
|
||||
const TranslationStore::Registry loaded = TranslationStore::load(file);
|
||||
QCOMPARE(loaded.records.size(), 1);
|
||||
QVERIFY(TranslationStore::isOwned(loaded, result.path));
|
||||
QCOMPARE(TranslationStore::savedTranslation(loaded, m_videoUrl, u"vi"_s), result.path);
|
||||
QVERIFY(TranslationStore::load(m_dir.filePath(u"missing.json"_s)).records.isEmpty());
|
||||
}
|
||||
|
||||
// The registry is no longer right about a file that disappeared.
|
||||
void forgetsDeletedTranslation()
|
||||
{
|
||||
TranslationStore::Registry registry;
|
||||
const auto result = TranslationStore::publish(registry, m_videoUrl, u"vi"_s, u"k1"_s, "one", cached(u"k1"_s, "one"));
|
||||
QFile::remove(result.path);
|
||||
QCOMPARE(TranslationStore::savedTranslation(registry, m_videoUrl, u"vi"_s), QString());
|
||||
}
|
||||
};
|
||||
|
||||
QTEST_GUILESS_MAIN(TranslationStoreTest)
|
||||
#include "translationstoretest.moc"
|
||||
Reference in new issue
Block a user