fix(mpv): auto-select subtitle and audio tracks on file open
This commit is contained in:
1 parent
32c686e0e3
commit
23cf7e31a9
13 files changed
+437
-1
No files matched your search
@@ -19,3 +19,8 @@ ecm_add_test(playlistmodeltest.cpp ../src/playlist/playlistmodel.cpp
|
|||||||
LINK_LIBRARIES Qt6::Qml Qt6::Test
|
LINK_LIBRARIES Qt6::Qml Qt6::Test
|
||||||
)
|
)
|
||||||
target_include_directories(playlistmodeltest PRIVATE ../src/playlist)
|
target_include_directories(playlistmodeltest PRIVATE ../src/playlist)
|
||||||
|
|
||||||
|
ecm_add_test(trackselectiontest.cpp
|
||||||
|
TEST_NAME trackselectiontest
|
||||||
|
LINK_LIBRARIES qtpotato_core Qt6::Test
|
||||||
|
)
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
#include "trackselection.h"
|
||||||
|
|
||||||
|
#include <QTest>
|
||||||
|
|
||||||
|
using namespace Qt::Literals::StringLiterals;
|
||||||
|
using TrackSelection::Track;
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
Track sub(int id, const QString &lang, const QString &codec = u"subrip"_s, bool forced = false, bool isDefault = false)
|
||||||
|
{
|
||||||
|
return {.id = id, .lang = lang, .codec = codec, .forced = forced, .isDefault = isDefault};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TrackSelectionTest : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
private Q_SLOTS:
|
||||||
|
void matchesLanguageTags()
|
||||||
|
{
|
||||||
|
QVERIFY(TrackSelection::sameLanguage(u"en"_s, u"eng"_s));
|
||||||
|
QVERIFY(TrackSelection::sameLanguage(u"en-US"_s, u"English"_s));
|
||||||
|
QVERIFY(TrackSelection::sameLanguage(u"ger"_s, u"de"_s));
|
||||||
|
QVERIFY(TrackSelection::sameLanguage(u"vie"_s, u"vietnamese"_s));
|
||||||
|
QVERIFY(!TrackSelection::sameLanguage(u"en"_s, u"ru"_s));
|
||||||
|
QVERIFY(!TrackSelection::sameLanguage(u""_s, u""_s));
|
||||||
|
QVERIFY(!TrackSelection::sameLanguage(u"en"_s, u""_s));
|
||||||
|
}
|
||||||
|
|
||||||
|
void detectsBitmapSubtitles()
|
||||||
|
{
|
||||||
|
QVERIFY(TrackSelection::isBitmapSubtitle(u"hdmv_pgs_subtitle"_s));
|
||||||
|
QVERIFY(TrackSelection::isBitmapSubtitle(u"dvd_subtitle"_s));
|
||||||
|
QVERIFY(!TrackSelection::isBitmapSubtitle(u"subrip"_s));
|
||||||
|
QVERIFY(!TrackSelection::isBitmapSubtitle(u"ass"_s));
|
||||||
|
QVERIFY(!TrackSelection::isBitmapSubtitle(u""_s));
|
||||||
|
}
|
||||||
|
|
||||||
|
// VisionQuest release: Russian dub by default, two Russian subtitles before the
|
||||||
|
// English one, no default subtitle, so mpv turns none on.
|
||||||
|
void fallsBackInLanguageOrder()
|
||||||
|
{
|
||||||
|
const QList<Track> tracks{sub(1, u"rus"_s), sub(2, u"rus"_s), sub(3, u"eng"_s)};
|
||||||
|
QCOMPARE(TrackSelection::pickSubtitle(tracks, {}, {u"vi"_s, u"en"_s, u"rus"_s}, 0, true), 3);
|
||||||
|
QCOMPARE(TrackSelection::pickSubtitle(tracks, {}, {u"vi"_s, u"en"_s, u"rus"_s}, 0, false), 0);
|
||||||
|
QCOMPARE(TrackSelection::pickSubtitle(tracks, {}, {u"ru"_s, u"en"_s}, 0, true), 1);
|
||||||
|
QCOMPARE(TrackSelection::pickSubtitle(tracks, {}, {u"ja"_s}, 0, true), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void fallbackPrefersDefaultFullTextTracks()
|
||||||
|
{
|
||||||
|
const QList<Track> tracks{sub(1, u"en"_s, u"subrip"_s, true), sub(2, u"en"_s, u"hdmv_pgs_subtitle"_s), sub(3, u"en"_s), sub(4, u"fr"_s, u"subrip"_s, false, true)};
|
||||||
|
QCOMPARE(TrackSelection::pickSubtitle(tracks, {}, {u"en"_s}, 0, true), 3);
|
||||||
|
QCOMPARE(TrackSelection::pickSubtitle(tracks, {}, {u"ja"_s}, 0, true), 4);
|
||||||
|
// Only forced subtitles: leave subtitles off.
|
||||||
|
QCOMPARE(TrackSelection::pickSubtitle({sub(1, u"en"_s, u"subrip"_s, true)}, {}, {u"en"_s}, 0, true), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void preferredLanguagesWinOverMpvChoice()
|
||||||
|
{
|
||||||
|
const QList<Track> tracks{sub(1, u"ru"_s), sub(2, u"vi"_s), sub(3, u"en"_s)};
|
||||||
|
QCOMPARE(TrackSelection::pickSubtitle(tracks, {u"vi"_s, u"en"_s}, {u"en"_s}, 1, true), 2);
|
||||||
|
QCOMPARE(TrackSelection::pickSubtitle(tracks, {u"de"_s, u"English"_s}, {u"en"_s}, 1, true), 3);
|
||||||
|
// No preferred language present: keep mpv's choice.
|
||||||
|
QCOMPARE(TrackSelection::pickSubtitle(tracks, {u"de"_s}, {u"en"_s}, 1, true), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void picksAudioByPreference()
|
||||||
|
{
|
||||||
|
const QList<Track> tracks{sub(1, u"en"_s, u"aac"_s), sub(2, u"jpn"_s, u"aac"_s)};
|
||||||
|
QCOMPARE(TrackSelection::pickAudio(tracks, {u"ja"_s}, 1), 2);
|
||||||
|
QCOMPARE(TrackSelection::pickAudio(tracks, {u"de"_s}, 1), 1);
|
||||||
|
QCOMPARE(TrackSelection::pickAudio(tracks, {}, 1), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void picksTextTranslationSource()
|
||||||
|
{
|
||||||
|
const QList<Track> tracks{sub(1, u"en"_s, u"hdmv_pgs_subtitle"_s), sub(2, u"ru"_s), sub(3, u"en"_s)};
|
||||||
|
QCOMPARE(TrackSelection::pickTranslationSource(tracks, {u"en"_s}, 2), 2);
|
||||||
|
QCOMPARE(TrackSelection::pickTranslationSource(tracks, {u"en"_s}, 1), 3);
|
||||||
|
QCOMPARE(TrackSelection::pickTranslationSource(tracks, {u"en"_s}, 0), 3);
|
||||||
|
QCOMPARE(TrackSelection::pickTranslationSource(tracks, {u"ru"_s}, 0), 2);
|
||||||
|
QCOMPARE(TrackSelection::pickTranslationSource({sub(1, u"en"_s, u"dvd_subtitle"_s)}, {u"en"_s}, 0), 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
QTEST_GUILESS_MAIN(TrackSelectionTest)
|
||||||
|
#include "trackselectiontest.moc"
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
add_library(qtpotato_core STATIC
|
add_library(qtpotato_core STATIC
|
||||||
mpv/mpvuserconfig.cpp
|
mpv/mpvuserconfig.cpp
|
||||||
mpv/mpvuserconfig.h
|
mpv/mpvuserconfig.h
|
||||||
|
mpv/trackselection.cpp
|
||||||
|
mpv/trackselection.h
|
||||||
subtitles/subtitleparser.cpp
|
subtitles/subtitleparser.cpp
|
||||||
subtitles/subtitleparser.h
|
subtitles/subtitleparser.h
|
||||||
subtitles/subtitleextractor.cpp
|
subtitles/subtitleextractor.cpp
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
#include "mpvitem.h"
|
#include "mpvitem.h"
|
||||||
|
|
||||||
#include "mpvuserconfig.h"
|
#include "mpvuserconfig.h"
|
||||||
|
#include "trackselection.h"
|
||||||
|
|
||||||
#include <MpvController>
|
#include <MpvController>
|
||||||
|
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
|
#include <QLocale>
|
||||||
#include <QSaveFile>
|
#include <QSaveFile>
|
||||||
#include <QStandardPaths>
|
#include <QStandardPaths>
|
||||||
|
|
||||||
@@ -25,6 +27,33 @@ QVariant trackIdValue(int id)
|
|||||||
{
|
{
|
||||||
return id > 0 ? QVariant(id) : QVariant(QStringLiteral("no"));
|
return id > 0 ? QVariant(id) : QVariant(QStringLiteral("no"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QList<TrackSelection::Track> selectionTracks(const QVariantList &tracks)
|
||||||
|
{
|
||||||
|
QList<TrackSelection::Track> result;
|
||||||
|
for (const QVariant &entry : tracks) {
|
||||||
|
const QVariantMap track = entry.toMap();
|
||||||
|
result.append({
|
||||||
|
.id = track.value(QStringLiteral("id")).toInt(),
|
||||||
|
.lang = track.value(QStringLiteral("lang")).toString(),
|
||||||
|
.codec = track.value(QStringLiteral("codec")).toString(),
|
||||||
|
.forced = track.value(QStringLiteral("forced")).toBool(),
|
||||||
|
.isDefault = track.value(QStringLiteral("default")).toBool(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString trackLanguage(const QVariantList &tracks, int id)
|
||||||
|
{
|
||||||
|
for (const QVariant &entry : tracks) {
|
||||||
|
const QVariantMap track = entry.toMap();
|
||||||
|
if (track.value(QStringLiteral("id")).toInt() == id) {
|
||||||
|
return track.value(QStringLiteral("lang")).toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MpvItem::MpvItem(QQuickItem *parent)
|
MpvItem::MpvItem(QQuickItem *parent)
|
||||||
@@ -64,6 +93,7 @@ MpvItem::MpvItem(QQuickItem *parent)
|
|||||||
this,
|
this,
|
||||||
[this] {
|
[this] {
|
||||||
setFileLoaded(true);
|
setFileLoaded(true);
|
||||||
|
selectInitialTracks();
|
||||||
},
|
},
|
||||||
Qt::QueuedConnection);
|
Qt::QueuedConnection);
|
||||||
connect(
|
connect(
|
||||||
@@ -338,6 +368,37 @@ void MpvItem::showTranslatedSubtitleFile(const QUrl &url, const QString &title)
|
|||||||
commandAsync(QStringList{QStringLiteral("sub-add"), path, QStringLiteral("auto"), title});
|
commandAsync(QStringList{QStringLiteral("sub-add"), path, QStringLiteral("auto"), title});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int MpvItem::translationSourceId() const
|
||||||
|
{
|
||||||
|
// English first: it is the usual original, and dubbed releases often default to
|
||||||
|
// subtitles translated from it.
|
||||||
|
return TrackSelection::pickTranslationSource(selectionTracks(m_subtitleTracks),
|
||||||
|
{QStringLiteral("en"), trackLanguage(m_audioTracks, m_audioId)},
|
||||||
|
m_subtitleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MpvItem::selectInitialTracks()
|
||||||
|
{
|
||||||
|
// The observed track-list may still describe the previous file; read it directly.
|
||||||
|
updateTracks(getProperty(QStringLiteral("track-list")).toList());
|
||||||
|
|
||||||
|
const int audioId = TrackSelection::pickAudio(selectionTracks(m_audioTracks), m_preferredAudioLanguages, m_audioId);
|
||||||
|
if (audioId != m_audioId) {
|
||||||
|
setAudioId(audioId);
|
||||||
|
}
|
||||||
|
// Subtitles are for reading: the user's own language, then English, before the
|
||||||
|
// audio language. Dubbed releases often default to a dub with matching subtitles.
|
||||||
|
const QStringList fallbackLanguages{QLocale::languageToCode(QLocale::system().language()), QStringLiteral("en"), trackLanguage(m_audioTracks, audioId)};
|
||||||
|
const int subtitleId = TrackSelection::pickSubtitle(selectionTracks(m_subtitleTracks),
|
||||||
|
m_preferredSubtitleLanguages,
|
||||||
|
fallbackLanguages,
|
||||||
|
m_subtitleId,
|
||||||
|
m_autoSelectSubtitle);
|
||||||
|
if (subtitleId != m_subtitleId) {
|
||||||
|
setSubtitleId(subtitleId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void MpvItem::showOnlySubtitle(int id)
|
void MpvItem::showOnlySubtitle(int id)
|
||||||
{
|
{
|
||||||
setSubtitleId(id);
|
setSubtitleId(id);
|
||||||
@@ -484,6 +545,8 @@ void MpvItem::updateTracks(const QVariantList &trackList)
|
|||||||
{QStringLiteral("title"), track.value(QStringLiteral("title")).toString()},
|
{QStringLiteral("title"), track.value(QStringLiteral("title")).toString()},
|
||||||
{QStringLiteral("lang"), track.value(QStringLiteral("lang")).toString()},
|
{QStringLiteral("lang"), track.value(QStringLiteral("lang")).toString()},
|
||||||
{QStringLiteral("codec"), track.value(QStringLiteral("codec")).toString()},
|
{QStringLiteral("codec"), track.value(QStringLiteral("codec")).toString()},
|
||||||
|
{QStringLiteral("forced"), track.value(QStringLiteral("forced")).toBool()},
|
||||||
|
{QStringLiteral("default"), track.value(QStringLiteral("default")).toBool()},
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -505,6 +568,9 @@ void MpvItem::updateTracks(const QVariantList &trackList)
|
|||||||
{QStringLiteral("title"), track.value(QStringLiteral("title")).toString()},
|
{QStringLiteral("title"), track.value(QStringLiteral("title")).toString()},
|
||||||
{QStringLiteral("lang"), track.value(QStringLiteral("lang")).toString()},
|
{QStringLiteral("lang"), track.value(QStringLiteral("lang")).toString()},
|
||||||
{QStringLiteral("codec"), track.value(QStringLiteral("codec")).toString()},
|
{QStringLiteral("codec"), track.value(QStringLiteral("codec")).toString()},
|
||||||
|
{QStringLiteral("bitmap"), TrackSelection::isBitmapSubtitle(track.value(QStringLiteral("codec")).toString())},
|
||||||
|
{QStringLiteral("forced"), track.value(QStringLiteral("forced")).toBool()},
|
||||||
|
{QStringLiteral("default"), track.value(QStringLiteral("default")).toBool()},
|
||||||
{QStringLiteral("external"), track.value(QStringLiteral("external")).toBool()},
|
{QStringLiteral("external"), track.value(QStringLiteral("external")).toBool()},
|
||||||
{QStringLiteral("externalFilename"), track.value(QStringLiteral("external-filename")).toString()},
|
{QStringLiteral("externalFilename"), track.value(QStringLiteral("external-filename")).toString()},
|
||||||
{QStringLiteral("ffIndex"), track.value(QStringLiteral("ff-index"), -1).toInt()},
|
{QStringLiteral("ffIndex"), track.value(QStringLiteral("ff-index"), -1).toInt()},
|
||||||
|
|||||||
@@ -51,6 +51,13 @@ class MpvItem : public MpvAbstractItem
|
|||||||
Q_PROPERTY(QStringList loadedUserConfigFiles READ loadedUserConfigFiles NOTIFY userConfigApplied)
|
Q_PROPERTY(QStringList loadedUserConfigFiles READ loadedUserConfigFiles NOTIFY userConfigApplied)
|
||||||
Q_PROPERTY(QStringList skippedUserOptions READ skippedUserOptions NOTIFY userConfigApplied)
|
Q_PROPERTY(QStringList skippedUserOptions READ skippedUserOptions NOTIFY userConfigApplied)
|
||||||
|
|
||||||
|
// Tracks turned on when a file opens; see TrackSelection. Languages are ISO 639 codes
|
||||||
|
// or English names, in order of preference.
|
||||||
|
Q_PROPERTY(QStringList preferredAudioLanguages MEMBER m_preferredAudioLanguages NOTIFY trackPreferencesChanged)
|
||||||
|
Q_PROPERTY(QStringList preferredSubtitleLanguages MEMBER m_preferredSubtitleLanguages NOTIFY trackPreferencesChanged)
|
||||||
|
// Turn on a subtitle when mpv would open the file with none.
|
||||||
|
Q_PROPERTY(bool autoSelectSubtitle MEMBER m_autoSelectSubtitle NOTIFY trackPreferencesChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit MpvItem(QQuickItem *parent = nullptr);
|
explicit MpvItem(QQuickItem *parent = nullptr);
|
||||||
~MpvItem() override = default;
|
~MpvItem() override = default;
|
||||||
@@ -106,6 +113,9 @@ public:
|
|||||||
// Makes \a url the only subtitle shown, adding it first (named \a title) unless it is
|
// Makes \a url the only subtitle shown, adding it first (named \a title) unless it is
|
||||||
// already loaded.
|
// already loaded.
|
||||||
Q_INVOKABLE void showTranslatedSubtitleFile(const QUrl &url, const QString &title);
|
Q_INVOKABLE void showTranslatedSubtitleFile(const QUrl &url, const QString &title);
|
||||||
|
// Id of the subtitle to translate: the shown one if it is text, else the best text
|
||||||
|
// subtitle. 0 when the file has no text subtitle.
|
||||||
|
Q_INVOKABLE int translationSourceId() const;
|
||||||
|
|
||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void currentUrlChanged();
|
void currentUrlChanged();
|
||||||
@@ -130,6 +140,7 @@ Q_SIGNALS:
|
|||||||
void loadUserConfigChanged();
|
void loadUserConfigChanged();
|
||||||
// mpv.conf was queued for loading; options set after this take precedence over it.
|
// mpv.conf was queued for loading; options set after this take precedence over it.
|
||||||
void userConfigApplied();
|
void userConfigApplied();
|
||||||
|
void trackPreferencesChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void onPropertyChanged(const QString &property, const QVariant &value);
|
void onPropertyChanged(const QString &property, const QVariant &value);
|
||||||
@@ -137,6 +148,7 @@ private:
|
|||||||
void updateTracks(const QVariantList &trackList);
|
void updateTracks(const QVariantList &trackList);
|
||||||
void applyUserConfig();
|
void applyUserConfig();
|
||||||
void showOnlySubtitle(int id);
|
void showOnlySubtitle(int id);
|
||||||
|
void selectInitialTracks();
|
||||||
|
|
||||||
bool m_ready = false;
|
bool m_ready = false;
|
||||||
QUrl m_pendingUrl;
|
QUrl m_pendingUrl;
|
||||||
@@ -165,4 +177,7 @@ private:
|
|||||||
bool m_loadUserConfig = false;
|
bool m_loadUserConfig = false;
|
||||||
QStringList m_loadedUserConfigFiles;
|
QStringList m_loadedUserConfigFiles;
|
||||||
QStringList m_skippedUserOptions;
|
QStringList m_skippedUserOptions;
|
||||||
|
QStringList m_preferredAudioLanguages;
|
||||||
|
QStringList m_preferredSubtitleLanguages;
|
||||||
|
bool m_autoSelectSubtitle = true;
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
#include "trackselection.h"
|
||||||
|
|
||||||
|
#include <QLocale>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#include <libavcodec/avcodec.h>
|
||||||
|
}
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
using TrackSelection::Track;
|
||||||
|
|
||||||
|
QLocale::Language languageOf(QString tag)
|
||||||
|
{
|
||||||
|
tag = tag.trimmed();
|
||||||
|
if (const auto separator = tag.indexOf(QRegularExpression(QStringLiteral("[-_]"))); separator > 0) {
|
||||||
|
tag.truncate(separator);
|
||||||
|
}
|
||||||
|
if (tag.isEmpty()) {
|
||||||
|
return QLocale::AnyLanguage;
|
||||||
|
}
|
||||||
|
if (const auto language = QLocale::codeToLanguage(tag, QLocale::AnyLanguageCode); language != QLocale::AnyLanguage) {
|
||||||
|
return language;
|
||||||
|
}
|
||||||
|
for (int i = QLocale::C + 1; i <= QLocale::LastLanguage; ++i) {
|
||||||
|
const auto candidate = static_cast<QLocale::Language>(i);
|
||||||
|
if (QLocale::languageToString(candidate).compare(tag, Qt::CaseInsensitive) == 0) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return QLocale::AnyLanguage;
|
||||||
|
}
|
||||||
|
|
||||||
|
// First full (not forced) subtitle matching \a matches, a text one before a bitmap one.
|
||||||
|
int firstFull(const QList<Track> &tracks, const std::function<bool(const Track &)> &matches)
|
||||||
|
{
|
||||||
|
for (const bool textOnly : {true, false}) {
|
||||||
|
for (const Track &track : tracks) {
|
||||||
|
if (!track.forced && matches(track) && !(textOnly && TrackSelection::isBitmapSubtitle(track.codec))) {
|
||||||
|
return track.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int fallbackSubtitle(const QList<Track> &tracks, const QStringList &languages)
|
||||||
|
{
|
||||||
|
for (const QString &language : languages) {
|
||||||
|
if (const int id = firstFull(tracks, [&](const Track &track) {
|
||||||
|
return TrackSelection::sameLanguage(track.lang, language);
|
||||||
|
})) {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (const int id = firstFull(tracks, [](const Track &track) {
|
||||||
|
return track.isDefault;
|
||||||
|
})) {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
return firstFull(tracks, [](const Track &) {
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace TrackSelection
|
||||||
|
{
|
||||||
|
bool isBitmapSubtitle(const QString &codec)
|
||||||
|
{
|
||||||
|
const AVCodecDescriptor *descriptor = avcodec_descriptor_get_by_name(codec.toLatin1().constData());
|
||||||
|
return descriptor && (descriptor->props & AV_CODEC_PROP_BITMAP_SUB);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool sameLanguage(const QString &a, const QString &b)
|
||||||
|
{
|
||||||
|
const QLocale::Language first = languageOf(a);
|
||||||
|
const QLocale::Language second = languageOf(b);
|
||||||
|
if (first != QLocale::AnyLanguage || second != QLocale::AnyLanguage) {
|
||||||
|
return first == second;
|
||||||
|
}
|
||||||
|
// Tags QLocale doesn't know ("und", "mis", made-up ones) only match themselves.
|
||||||
|
return !a.trimmed().isEmpty() && a.trimmed().compare(b.trimmed(), Qt::CaseInsensitive) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int pickAudio(const QList<Track> &tracks, const QStringList &preferredLanguages, int currentId)
|
||||||
|
{
|
||||||
|
for (const QString &language : preferredLanguages) {
|
||||||
|
for (const Track &track : tracks) {
|
||||||
|
if (sameLanguage(track.lang, language)) {
|
||||||
|
return track.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return currentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
int pickSubtitle(const QList<Track> &tracks, const QStringList &preferredLanguages, const QStringList &fallbackLanguages, int currentId, bool fallback)
|
||||||
|
{
|
||||||
|
for (const QString &language : preferredLanguages) {
|
||||||
|
if (const int id = firstFull(tracks, [&](const Track &track) {
|
||||||
|
return sameLanguage(track.lang, language);
|
||||||
|
})) {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currentId > 0 || !fallback) {
|
||||||
|
return currentId;
|
||||||
|
}
|
||||||
|
return fallbackSubtitle(tracks, fallbackLanguages);
|
||||||
|
}
|
||||||
|
|
||||||
|
int pickTranslationSource(const QList<Track> &tracks, const QStringList &sourceLanguages, int currentId)
|
||||||
|
{
|
||||||
|
QList<Track> text;
|
||||||
|
for (const Track &track : tracks) {
|
||||||
|
if (isBitmapSubtitle(track.codec)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (track.id == currentId) {
|
||||||
|
return currentId;
|
||||||
|
}
|
||||||
|
text.append(track);
|
||||||
|
}
|
||||||
|
if (const int id = fallbackSubtitle(text, sourceLanguages)) {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
// Only forced text subtitles left; still better than nothing to translate.
|
||||||
|
return text.isEmpty() ? 0 : text.constFirst().id;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QList>
|
||||||
|
#include <QString>
|
||||||
|
#include <QStringList>
|
||||||
|
|
||||||
|
// Which audio and subtitle tracks to turn on when a file opens. mpv only turns on a
|
||||||
|
// subtitle that is flagged default or matches --slang, so many files open with none.
|
||||||
|
namespace TrackSelection
|
||||||
|
{
|
||||||
|
struct Track {
|
||||||
|
int id = 0;
|
||||||
|
QString lang;
|
||||||
|
QString codec;
|
||||||
|
bool forced = false;
|
||||||
|
bool isDefault = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// True for picture-based subtitles (PGS, VobSub, DVB), which can't be translated.
|
||||||
|
// \a codec is the FFmpeg codec name that mpv reports.
|
||||||
|
bool isBitmapSubtitle(const QString &codec);
|
||||||
|
|
||||||
|
// Compares language tags written as ISO 639-1/2 codes ("en", "eng"), with a region
|
||||||
|
// ("en-US") or as English names ("English").
|
||||||
|
bool sameLanguage(const QString &a, const QString &b);
|
||||||
|
|
||||||
|
// The first audio track in a \a preferredLanguages language, else \a currentId.
|
||||||
|
int pickAudio(const QList<Track> &tracks, const QStringList &preferredLanguages, int currentId);
|
||||||
|
|
||||||
|
// The subtitle to turn on when the file opens. A track in one of \a preferredLanguages
|
||||||
|
// wins over mpv's own choice \a currentId; without one, \a currentId is kept. When mpv
|
||||||
|
// chose nothing and \a fallback is set, picks a full (not forced) subtitle in the first
|
||||||
|
// of \a fallbackLanguages that has one, then one flagged default, then the first one.
|
||||||
|
// Text subtitles win over bitmap ones in the same step. Returns 0 for no subtitle.
|
||||||
|
int pickSubtitle(const QList<Track> &tracks, const QStringList &preferredLanguages, const QStringList &fallbackLanguages, int currentId, bool fallback);
|
||||||
|
|
||||||
|
// The subtitle to translate: \a currentId when it is a text subtitle, else a full text
|
||||||
|
// subtitle in the first of \a sourceLanguages that has one, then one flagged default,
|
||||||
|
// then the first one. 0 when there is no text subtitle.
|
||||||
|
int pickTranslationSource(const QList<Track> &tracks, const QStringList &sourceLanguages, int currentId);
|
||||||
|
}
|
||||||
+1
-1
@@ -389,7 +389,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
property Action translateSubtitle: Action {
|
property Action translateSubtitle: Action {
|
||||||
enabled: root.shortcutsEnabled && root.hasFile && root.mpv.subtitleId > 0 && !root.translator.busy
|
enabled: root.shortcutsEnabled && root.hasFile && root.mpv.subtitleTracks.some(track => !track.bitmap) && !root.translator.busy
|
||||||
text: KI18n.i18nc("@action", "Translate Subtitle into %1", QtPotatoSettings.targetLanguage)
|
text: KI18n.i18nc("@action", "Translate Subtitle into %1", QtPotatoSettings.targetLanguage)
|
||||||
icon.name: "translate"
|
icon.name: "translate"
|
||||||
shortcut: "Ctrl+T"
|
shortcut: "Ctrl+T"
|
||||||
|
|||||||
@@ -51,6 +51,11 @@ ApplicationWindow {
|
|||||||
if (track && translatedSubtitleUrls.includes(track.externalFilename) && lastTranslationTrack) {
|
if (track && translatedSubtitleUrls.includes(track.externalFilename) && lastTranslationTrack) {
|
||||||
track = video.subtitleTracks.find(track => track.id === lastTranslationTrack.id)
|
track = video.subtitleTracks.find(track => track.id === lastTranslationTrack.id)
|
||||||
}
|
}
|
||||||
|
// No subtitle shown, or a picture-based one: translate the best text subtitle.
|
||||||
|
if (!track || track.bitmap) {
|
||||||
|
const sourceId = video.translationSourceId()
|
||||||
|
track = video.subtitleTracks.find(track => track.id === sourceId)
|
||||||
|
}
|
||||||
if (track) {
|
if (track) {
|
||||||
lastTranslationTrack = track
|
lastTranslationTrack = track
|
||||||
lastTranslationVideo = video.currentUrl
|
lastTranslationVideo = video.currentUrl
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ MpvItem {
|
|||||||
Kirigami.Theme.inherit: false
|
Kirigami.Theme.inherit: false
|
||||||
|
|
||||||
loadUserConfig: QtPotatoSettings.loadMpvConfig
|
loadUserConfig: QtPotatoSettings.loadMpvConfig
|
||||||
|
preferredAudioLanguages: QtPotatoSettings.preferredAudioLanguages
|
||||||
|
preferredSubtitleLanguages: QtPotatoSettings.preferredSubtitleLanguages
|
||||||
|
autoSelectSubtitle: QtPotatoSettings.autoSelectSubtitle
|
||||||
|
|
||||||
// Set once the saved volume is applied; mpv reports its default volume before that.
|
// Set once the saved volume is applied; mpv reports its default volume before that.
|
||||||
property bool volumeRestored: false
|
property bool volumeRestored: false
|
||||||
|
|||||||
@@ -72,5 +72,25 @@ SettingsBasePage {
|
|||||||
QtPotatoSettings.save()
|
QtPotatoSettings.save()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
Kirigami.FormData.label: KI18n.i18nc("@label:textbox", "Preferred languages")
|
||||||
|
Kirigami.FormData.buddyFor: audioLanguagesField
|
||||||
|
|
||||||
|
TextField {
|
||||||
|
id: audioLanguagesField
|
||||||
|
text: QtPotatoSettings.preferredAudioLanguages.join(", ")
|
||||||
|
placeholderText: KI18n.i18nc("@info:placeholder language codes", "e.g. ja, en")
|
||||||
|
onEditingFinished: {
|
||||||
|
QtPotatoSettings.preferredAudioLanguages = text.split(/[,;\s]+/).filter(Boolean)
|
||||||
|
QtPotatoSettings.save()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolTipButton {
|
||||||
|
toolTipText: KI18n.i18nc("@info:tooltip", "Audio track turned on when a file opens, first match wins. Use language codes or English names, separated by commas. Leave empty to keep the file's default track.")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,6 +13,49 @@ SettingsBasePage {
|
|||||||
|
|
||||||
Kirigami.FormLayout {
|
Kirigami.FormLayout {
|
||||||
|
|
||||||
|
SettingsHeader {
|
||||||
|
text: KI18n.i18nc("@title", "Track Selection")
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
spacing: Kirigami.Units.smallSpacing
|
||||||
|
Kirigami.FormData.label: KI18n.i18nc("@label:textbox", "Preferred languages")
|
||||||
|
Kirigami.FormData.buddyFor: subtitleLanguagesField
|
||||||
|
|
||||||
|
TextField {
|
||||||
|
id: subtitleLanguagesField
|
||||||
|
text: QtPotatoSettings.preferredSubtitleLanguages.join(", ")
|
||||||
|
placeholderText: KI18n.i18nc("@info:placeholder language codes", "e.g. vi, en")
|
||||||
|
onEditingFinished: {
|
||||||
|
QtPotatoSettings.preferredSubtitleLanguages = text.split(/[,;\s]+/).filter(Boolean)
|
||||||
|
QtPotatoSettings.save()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolTipButton {
|
||||||
|
toolTipText: KI18n.i18nc("@info:tooltip", "Subtitle turned on when a file opens, first match wins. Use language codes or English names, separated by commas. Subtitle files next to the video count too.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CheckBox {
|
||||||
|
text: KI18n.i18nc("@option:check", "Turn on a subtitle when none matches")
|
||||||
|
checked: QtPotatoSettings.autoSelectSubtitle
|
||||||
|
onToggled: {
|
||||||
|
QtPotatoSettings.autoSelectSubtitle = checked
|
||||||
|
QtPotatoSettings.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolTip.text: KI18n.i18nc("@info:tooltip", "Many files have no default subtitle, so they open with subtitles off. Picks one in the system language instead, then English, then the audio language.")
|
||||||
|
ToolTip.visible: hovered
|
||||||
|
ToolTip.delay: Kirigami.Units.toolTipDelay
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsHeader {
|
||||||
|
text: KI18n.i18nc("@title", "Appearance")
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
spacing: Kirigami.Units.smallSpacing
|
spacing: Kirigami.Units.smallSpacing
|
||||||
Kirigami.FormData.label: KI18n.i18nc("@label:spinbox", "Font size")
|
Kirigami.FormData.label: KI18n.i18nc("@label:spinbox", "Font size")
|
||||||
|
|||||||
@@ -64,6 +64,18 @@
|
|||||||
<min>1</min>
|
<min>1</min>
|
||||||
<max>600</max>
|
<max>600</max>
|
||||||
</entry>
|
</entry>
|
||||||
|
<entry name="PreferredAudioLanguages" type="StringList">
|
||||||
|
<label>Audio languages to turn on when a file opens, in order of preference (ISO 639 codes or English names); empty keeps mpv's choice</label>
|
||||||
|
<default></default>
|
||||||
|
</entry>
|
||||||
|
<entry name="PreferredSubtitleLanguages" type="StringList">
|
||||||
|
<label>Subtitle languages to turn on when a file opens, in order of preference (ISO 639 codes or English names)</label>
|
||||||
|
<default></default>
|
||||||
|
</entry>
|
||||||
|
<entry name="AutoSelectSubtitle" type="Bool">
|
||||||
|
<label>Turn on a subtitle when a file would open with none, preferring the audio language</label>
|
||||||
|
<default>true</default>
|
||||||
|
</entry>
|
||||||
</group>
|
</group>
|
||||||
<group name="Subtitles">
|
<group name="Subtitles">
|
||||||
<entry name="SubtitleFontSize" type="Int">
|
<entry name="SubtitleFontSize" type="Int">
|
||||||
|
|||||||
Reference in new issue
Block a user