#include "translationstore.h" #include #include #include #include #include #include #include #include #include #include using namespace Qt::Literals::StringLiterals; namespace { constexpr int RegistryVersion = 1; QString normalized(const QString &path) { return path.isEmpty() ? QString() : QFileInfo(path).absoluteFilePath(); } std::optional readFile(const QString &path) { QFile file(path); if (!file.open(QIODevice::ReadOnly)) { return std::nullopt; } return file.readAll(); } bool writeFile(const QString &path, const QByteArray &data) { // No direct-write fallback: a folder that can't take a temporary file is treated // as read-only, and the translation stays in the cache. QSaveFile file(path); return file.open(QIODevice::WriteOnly) && file.write(data) == data.size() && file.commit(); } } namespace TranslationStore { QString languageSuffix(const QString &language) { for (int i = QLocale::C + 1; i <= QLocale::LastLanguage; ++i) { const auto candidate = static_cast(i); if (QLocale::languageToString(candidate).compare(language, Qt::CaseInsensitive) == 0) { const QString code = QLocale::languageToCode(candidate, QLocale::ISO639Part1); return code.isEmpty() ? QLocale::languageToCode(candidate) : code; } } QString suffix = language.toLower(); suffix.removeIf([](QChar c) { return !c.isLetterOrNumber() && c != u'-'; }); return suffix.isEmpty() ? u"translated"_s : suffix; } QString videoKey(const QUrl &videoUrl) { return videoUrl.isLocalFile() ? normalized(videoUrl.toLocalFile()) : videoUrl.toString(); } QString cachePath(const QString &cacheDir, const QUrl &videoUrl, const QString &configKey, const QString &suffix) { const QString baseName = videoUrl.isLocalFile() ? QFileInfo(videoUrl.toLocalFile()).completeBaseName() : u"subtitles"_s; return u"%1/%2.%3.%4.srt"_s.arg(cacheDir, baseName, configKey, suffix); } QStringList sidecarPaths(const QString &videoPath, const QString &suffix) { const QFileInfo info(videoPath); const QString stem = info.absolutePath() + u'/' + info.completeBaseName(); return {u"%1.%2.srt"_s.arg(stem, suffix), u"%1.qtpotato.%2.srt"_s.arg(stem, suffix)}; } QByteArray contentHash(const QByteArray &data) { return QCryptographicHash::hash(data, QCryptographicHash::Sha256).toHex(); } std::optional Registry::find(const QString &video, const QString &language) const { for (const Record &record : records) { if (record.video == video && record.language == language) { return record; } } return std::nullopt; } std::optional Registry::findPath(const QString &path) const { const QString wanted = normalized(path); for (const Record &record : records) { if (record.path == wanted) { return record; } } return std::nullopt; } void Registry::put(const Record &record) { Record stored = record; stored.path = normalized(record.path); for (Record &existing : records) { if (existing.video == stored.video && existing.language == stored.language) { existing = stored; return; } } records.append(stored); } Registry load(const QString &file) { Registry registry; const auto data = readFile(file); if (!data) { return registry; } const QJsonObject root = QJsonDocument::fromJson(*data).object(); for (const QJsonValue &value : root.value(u"translations"_s).toArray()) { const QJsonObject object = value.toObject(); Record record{object.value(u"video"_s).toString(), object.value(u"language"_s).toString(), object.value(u"path"_s).toString(), object.value(u"configKey"_s).toString(), object.value(u"contentHash"_s).toString()}; if (!record.video.isEmpty() && !record.language.isEmpty() && !record.path.isEmpty()) { registry.put(record); } } return registry; } bool save(const QString &file, const Registry ®istry, QString *error) { QJsonArray translations; for (const Record &record : registry.records) { if (!QFileInfo::exists(record.path)) { continue; } translations.append(QJsonObject{{u"video"_s, record.video}, {u"language"_s, record.language}, {u"path"_s, record.path}, {u"configKey"_s, record.configKey}, {u"contentHash"_s, record.contentHash}}); } const QJsonObject root{{u"version"_s, RegistryVersion}, {u"translations"_s, translations}}; QDir().mkpath(QFileInfo(file).absolutePath()); QSaveFile output(file); if (!output.open(QIODevice::WriteOnly) || output.write(QJsonDocument(root).toJson()) < 0 || !output.commit()) { if (error) { *error = output.errorString(); } return false; } return true; } QString registryFile() { return QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + u"/translations.json"_s; } bool isOwned(const Registry ®istry, const QString &path) { const auto record = registry.findPath(path); if (!record) { return false; } const auto data = readFile(path); return data && contentHash(*data) == record->contentHash.toLatin1(); } PublishResult publish(Registry ®istry, const QUrl &videoUrl, const QString &suffix, const QString &configKey, const QByteArray &data, const QString &cacheFile) { const QString video = videoKey(videoUrl); const QByteArray hash = contentHash(data); PublishResult result{normalized(cacheFile), false}; if (videoUrl.isLocalFile()) { QStringList candidates = sidecarPaths(videoUrl.toLocalFile(), suffix); // Keep using the file written last time, so a translation does not move to // ".vi.srt" and leave its ".qtpotato.vi.srt" copy behind. if (const auto previous = registry.find(video, suffix); previous && candidates.contains(previous->path) && isOwned(registry, previous->path)) { candidates.removeAll(previous->path); candidates.prepend(previous->path); } for (const QString &path : std::as_const(candidates)) { if (QFileInfo::exists(path) && !isOwned(registry, path)) { continue; } const auto current = readFile(path); if ((current && contentHash(*current) == hash) || writeFile(path, data)) { result = {path, true}; } // Both names are in the same folder; when one can't be written, neither can. break; } } registry.put({video, suffix, result.path, configKey, QString::fromLatin1(hash)}); return result; } void recordRewrite(Registry ®istry, const QString &path, const QByteArray &data) { if (auto record = registry.findPath(path)) { record->contentHash = QString::fromLatin1(contentHash(data)); registry.put(*record); } } QString savedTranslation(const Registry ®istry, const QUrl &videoUrl, const QString &suffix) { const auto record = registry.find(videoKey(videoUrl), suffix); return record && QFileInfo::exists(record->path) ? record->path : QString(); } }