fix(mpv): auto-select subtitle and audio tracks on file open (#12)

Closes #7

mpv only turns on a subtitle that is flagged default or matches `slang`, so releases like VisionQuest (Russian dub by default, ru/ru/en subtitles, none flagged) open with subtitles off, which also disabled **Translate subtitle**.

- New `TrackSelection` helpers in `qtpotato_core` with unit tests.
- On file load: audio follows *Preferred languages* (Playback settings); subtitle follows *Preferred languages* (Subtitles settings), else keeps mpv's choice, else (new *Turn on a subtitle when none matches*, on by default) picks a full text subtitle in the system language, then English, then the audio language, then the default/first track.
- Translate is enabled whenever the file has a text subtitle; with none or a bitmap subtitle shown it translates the best text track (English first).

Verified in the real app on VisionQuest S01E01: subtitle 3 (en) is selected on open; with audio set to `en` and auto-select off, audio switches to track 7, subtitle stays off and Translate stays enabled with the English source.

Reviewed-on: #12
Co-authored-by: Tri Vo Khac <git@trivk.dev>
This commit was merged in pull request #12.
This commit is contained in:
tri authored and tri committed 2026-11-13 21:15:22 +07:00
1 parent 32c686e0e3
commit b34a01a92f
13 files changed
+437 -1

No files matched your search

+66
View File
@@ -1,11 +1,13 @@
#include "mpvitem.h"
#include "mpvuserconfig.h"
#include "trackselection.h"
#include <MpvController>
#include <QDir>
#include <QFile>
#include <QLocale>
#include <QSaveFile>
#include <QStandardPaths>
@@ -25,6 +27,33 @@ QVariant trackIdValue(int id)
{
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)
@@ -64,6 +93,7 @@ MpvItem::MpvItem(QQuickItem *parent)
this,
[this] {
setFileLoaded(true);
selectInitialTracks();
},
Qt::QueuedConnection);
connect(
@@ -338,6 +368,37 @@ void MpvItem::showTranslatedSubtitleFile(const QUrl &url, const QString &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)
{
setSubtitleId(id);
@@ -484,6 +545,8 @@ void MpvItem::updateTracks(const QVariantList &trackList)
{QStringLiteral("title"), track.value(QStringLiteral("title")).toString()},
{QStringLiteral("lang"), track.value(QStringLiteral("lang")).toString()},
{QStringLiteral("codec"), track.value(QStringLiteral("codec")).toString()},
{QStringLiteral("forced"), track.value(QStringLiteral("forced")).toBool()},
{QStringLiteral("default"), track.value(QStringLiteral("default")).toBool()},
});
continue;
}
@@ -505,6 +568,9 @@ void MpvItem::updateTracks(const QVariantList &trackList)
{QStringLiteral("title"), track.value(QStringLiteral("title")).toString()},
{QStringLiteral("lang"), track.value(QStringLiteral("lang")).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("externalFilename"), track.value(QStringLiteral("external-filename")).toString()},
{QStringLiteral("ffIndex"), track.value(QStringLiteral("ff-index"), -1).toInt()},