This script automates audio and subtitle selection in MPV.
MPV will automatically load your preferred languages for both audio and subtitles. The player will also distinguish between full dialogue and signs/songs based on your audio selection, even updating subtitles instantly when you switch audio tracks.
track-selector.lua in your mpv scripts folder.
C:\Program Files\mpv\portable_config\scripts)
preferred_lang_codes,
preferred_lang_Forced_Subs_Keywords and
preferred_lang_Normal_Subs_Keywords
sections inside the User Configuration.
es-419 for Latin American Spanish, then es and spa).
preferred_lang_codes) : {"it", "ita"} → {"es-419", "es", "spa"}
preferred_lang_Forced_Subs_Keywords): {"cartell", "forzat"} → {"whatever", "keywords"}
preferred_lang_Normal_Subs_Keywords): {"dialog", "complet"} → {"whatever", "keywords"}
-- ############ USER CONFIGURATION ############
-- Set your preferred language codes here. All script logic will adapt to this.
-- The first code is the primary choice for keyword lookups (e.g., "it" for Italian).
-- To prioritize a specific region, put the regional code first followed by general
-- fallbacks (e.g., {"es-419", "es", "spa"} for Latin American Spanish).
local preferred_lang_codes = {"it", "ita"}
-- Set to true to prioritize preferred language audio ONLY if it is tagged as a "default" track.
-- Set to false to always prioritize preferred language audio if it exists in the file.
local prioritize_preferred_lang_audio_only_if_default = true -- default = true
-- Set audio priorities.
-- This list is used as a fallback only if:
-- 1. The file does not contain any audio tracks matching "preferred_lang_codes".
-- 2. "prioritize_preferred_lang_audio_only_if_default" is true and the preferred
-- language tracks are not tagged as "default".
local audio_priority = {
{"ja", "jpn", "jp"}, -- Japanese
{"ko", "kor"}, -- Korean
preferred_lang_codes, -- Your preferred language
{"en", "eng"} -- English
}
-- Set keywords for the preferred language (Forced Subs)
local preferred_lang_Forced_Subs_Keywords = {"cartell", "sign", "forced", "forzat"}
-- Set keywords for the preferred language (Normal Subs)
local preferred_lang_Normal_Subs_Keywords = {"dialog", "complet", "full"}
-- Set keywords for English (Forced Subs)
local English_Forced_Subs_Keywords = {"sign", "forced"}
-- Set keywords for English (Normal Subs)
local English_Normal_Subs_Keywords = {"honorific", "dialog", "full", "complet"}
-- Set to true to prioritize "enm" (Middle English / Honorifics) subtitles over "en","eng" (English) subtitles.
-- Set to false to treat "enm" just like any other English subtitle.
local prioritize_honorifics = true -- default = true
-- If no Forced subs are picked through the rules, fall back to the first forced sub available
local fallback_to_any_forced_subs = false -- default = false
-- ######### USER CONFIGURATION - END #########
--[[
-- =================================================================================================
-- SCRIPT LOGIC - DETAILED EXPLANATION
-- =================================================================================================
--
-- This script automates the selection of audio and subtitle tracks in mpv based on a
-- user-defined hierarchy of preferences. Its goal is to provide a "set it and forget it"
-- experience by intelligently choosing the best tracks for any given media file.
--
--
-- ### Core Concepts ###
--
-- 1. Event-Driven: The script doesn't run from top to bottom once. Instead, it waits for
-- specific events to happen in the player. The main events are:
-- * `file-loaded`: When you open a new video, this triggers the entire logic flow.
-- * `aid` changes: When the audio track is changed (either by the script or manually
-- by you), the subtitle selection logic runs again to find the best match for the
-- new audio language.
-- * `sid` changes: A small helper watches for manual subtitle selections to ensure they
-- are always visible.
--
-- 2. Language Matching: The script supports both base language codes (like "it", "en") and
-- region-specific codes (like "it-IT", "en-US"). It does this by checking if a track's
-- language code either matches exactly OR starts with the base code followed by a dash.
--
-- 3. Keyword-Based Selection: Beyond language codes, the script can inspect the "title" of
-- a track for specific keywords (e.g., "Forced", "Signs", "Full", "Dialog") to make
-- more granular decisions. This is crucial for distinguishing between different types
-- of subtitles or to find wrongly tagged tracks.
--
--
-- ### Execution Flow ###
--
-- When a video is loaded (`file-loaded` event):
--
-- #### STEP 1: AUDIO SELECTION (`set_preferred_audio` function)
--
-- The script first tries to select the best audio track. The logic depends on the
-- `prioritize_preferred_lang_audio_only_if_default` setting:
--
-- * IF `true` (Default behavior):
-- 1. It looks for an audio track that is BOTH in your preferred language AND has the
-- "default" flag set in the media container. This is the ideal scenario.
-- 2. If not found, it falls back to finding ANY track with the "default" flag, in case
-- the language code is missing or incorrect.
--
-- * IF `false`:
-- 1. It simply finds the very FIRST audio track that matches your preferred language,
-- ignoring whether it's the "default" track or not.
--
-- * AUDIO FALLBACK:
-- If neither of the above methods selects a track, the script will loop through the
-- `audio_priority` list (`ja` -> `ko` -> your preferred lang -> `en`) and pick the first
-- one it finds.
--
--
-- #### STEP 2: SUBTITLE SELECTION (`update_subtitle` function)
--
-- After the audio track is set, this function runs to find the perfect subtitle match. This
-- is the most complex part of the script.
--
-- * A. Get Context:
-- - It first determines the language of the CURRENTLY PLAYING audio track. This language
-- is the primary context for all subtitle decisions.
-- - If no audio is playing, subtitles are turned off, and the function stops.
--
-- * B. "Strict Mode" vs. "Normal Mode":
-- - The script has an internal concept of a "strict mode". This mode is activated if the
-- audio is in a language you are presumed to understand (your preferred language or English).
-- - In "strict mode", the script assumes you ONLY want "Forced" subtitles (for signs,
-- on-screen text, or non-English dialogue). It will NOT look for full dialogue subtitles.
-- - If the audio is in any other language (e.g., Japanese), "strict mode" is off, and the
-- script assumes you need full dialogue subtitles to understand the content.
--
-- * C. Subtitle Search Logic:
-- The script follows a long, cascading set of rules to find a subtitle track (`sid`).
-- As soon as a rule finds a match, the search stops.
--
-- -- IF AUDIO IS PREFERRED LANGUAGE or ENGLISH ("Strict Mode"): --
-- The goal is to find FORCED subtitles ONLY.
--
-- 1. Preferred Language Forced Subs:
-- - Seeks PREFERRED language subs with the "forced" flag AND matching keywords (e.g., "cartell").
-- - Then, PREFERRED language subs with just the "forced" flag.
-- - Then, PREFERRED language subs with just the keywords (in case the flag is missing).
--
-- 2. English Forced Subs (prioritizing Honorifics if enabled):
-- - It repeats the same 3-step search (flag+keyword, flag only, keyword only) for
-- `enm` (Honorifics), and then for `en`/`eng` (standard English).
--
-- 3. Final Forced Fallback:
-- - If still nothing is found, it will look for ANY subtitle track with the "forced"
-- flag, in the order defined by `final_fallback_sub_priority`.
--
--
-- -- IF AUDIO IS OTHER LANGUAGE (e.g., Japanese) ("Normal Mode"): --
-- The goal is to find FULL DIALOGUE subtitles.
--
-- 1. Preferred Language Full Subs:
-- - Seeks PREFERRED language subs with normal keywords (e.g., "dialog", "full").
-- - Then, ANY PREFERRED language sub if the keyword search fails.
--
-- 2. English Full Subs (prioritizing Honorifics if enabled):
-- - It repeats the same 2-step search (keyword, then any) for `enm` (Honorifics),
-- and then for `en`/`eng` (standard English).
--
-- 3. Final Full Sub Fallback:
-- - If still nothing is found, it picks the first available subtitle from the
-- `final_fallback_sub_priority` list.
--
-- * D. Final Action:
-- - If a matching subtitle track (`sid`) was found, it is selected and subtitles are made visible.
-- - If no suitable track was found after all the checks, subtitles are turned off.
--
-- =================================================================================================
--]]
-- ######### SCRIPT LOGIC (No need to edit below) #########
-- Derive primary code from the user setting for keyword map lookups
local preferred_lang_primary_code = preferred_lang_codes[1]
local audio_priority_keywords_map = {
[preferred_lang_primary_code] = {preferred_lang_codes[2]} -- e.g., ["it"] = {"ita"}
}
-- Build subtitle priorities dynamically from user settings
local sub_priority_PREFERRED_LANG = { preferred_lang_codes }
local sub_priority_ENM = {
{"enm"} -- Middle English (honorifics)
}
local sub_priority_STANDARD_ENG = {
{"en", "eng"} -- English
}
-- Build the final fallback list dynamically
local final_fallback_sub_priority = { preferred_lang_codes }
if prioritize_honorifics then
-- If true, search for 'enm' first, then 'en'/'eng' as a fallback.
table.insert(final_fallback_sub_priority, {"enm"})
table.insert(final_fallback_sub_priority, {"en", "eng"})
else
-- If false, search for all English variants together.
table.insert(final_fallback_sub_priority, {"en", "eng", "enm"})
end
local forced_subs_keywords_map = {
[preferred_lang_primary_code] = preferred_lang_Forced_Subs_Keywords,
[preferred_lang_codes[2]] = preferred_lang_Forced_Subs_Keywords,
["enm"] = English_Forced_Subs_Keywords,
["en"] = English_Forced_Subs_Keywords,
["eng"] = English_Forced_Subs_Keywords
}
local normal_subs_keywords_map = {
[preferred_lang_primary_code] = preferred_lang_Normal_Subs_Keywords,
[preferred_lang_codes[2]] = preferred_lang_Normal_Subs_Keywords,
["enm"] = English_Normal_Subs_Keywords,
["en"] = English_Normal_Subs_Keywords,
["eng"] = English_Normal_Subs_Keywords
}
-- Helper function to check for base language code match (e.g., "it-it" matches "it")
local function lang_matches(track_lang, target_lang)
-- Direct match (e.g., "it" == "it")
if track_lang == target_lang then
return true
end
-- Region-specific match (e.g., "it-it" starts with "it-")
if track_lang:sub(1, #target_lang + 1) == target_lang .. "-" then
return true
end
return false
end
-- Helper function to check if a language is in a list of codes
local function is_lang_in_list(lang_to_check, lang_list)
for _, lang in ipairs(lang_list) do
if lang_matches(lang_to_check, lang) then
return true
end
end
return false
end
local function track_has_keywords(track, keywords)
if not keywords then return false end
local title = (track.title or ""):lower()
local filename = track.external and (track.external_filename or ""):lower() or ""
for _, kw in ipairs(keywords) do
if title:find(kw:lower(), 1, true) or filename:find(kw:lower(), 1, true) then
return true
end
end
return false
end
-- THIS FUNCTION HAS BEEN CORRECTED TO BE STRICTER
local function find_matching_track(tracks, track_type, lang_list, require_forced, keywords)
-- Split tracks into non-forced and forced groups
local non_forced_tracks = {}
local forced_tracks = {}
for _, track in ipairs(tracks) do
if track.type == track_type then
if track.forced then
table.insert(forced_tracks, track)
else
table.insert(non_forced_tracks, track)
end
end
end
-- This logic is now stricter. It will only search the category specified by require_forced.
local tracks_to_search = require_forced and forced_tracks or non_forced_tracks
for _, track in ipairs(tracks_to_search) do
local track_lang = (track.lang or ""):lower()
for _, lang_group in ipairs(lang_list) do
for _, lang in ipairs(lang_group) do
if lang == "*" or lang_matches(track_lang, lang) then
if not keywords or track_has_keywords(track, keywords) then
return track.id
end
end
end
end
end
-- If nothing is found, it will implicitly return nil.
end
local function set_preferred_audio()
local tracks = mp.get_property_native("track-list", {})
local aid_to_set = nil
if prioritize_preferred_lang_audio_only_if_default then
-- TRUE CASE: Requires the preferred language track to be 'default'.
-- Check for PREFERRED_LANG default audio first by ISO code.
for _, track in ipairs(tracks) do
if track.type == "audio" then
local lang = (track.lang or ""):lower()
if is_lang_in_list(lang, preferred_lang_codes) and track.default then
aid_to_set = track.id
break
end
end
end
-- If no ISO-code match, look for a default audio track whose label/title contains any keyword.
if not aid_to_set then
for _, track in ipairs(tracks) do
if track.type == "audio" and track.default then
local text = ((track.title or "") .. " " .. (track.label or "")):lower()
for lang_code, kw_list in pairs(audio_priority_keywords_map) do
for _, kw in ipairs(kw_list) do
if text:find(kw:lower(), 1, true) then
aid_to_set = track.id
break
end
end
if aid_to_set then break end
end
if aid_to_set then break end
end
end
end
else
-- FALSE CASE: Prioritizes preferred language audio even if it's not default.
for _, track in ipairs(tracks) do
if track.type == "audio" then
local lang = (track.lang or ""):lower()
if is_lang_in_list(lang, preferred_lang_codes) then
aid_to_set = track.id
break -- Found the first preferred language track, that's what we want.
end
end
end
end
-- Final step: Apply the chosen track or fall back to the general audio_priority list.
if aid_to_set then
mp.set_property("aid", aid_to_set)
else
local fallback_aid = find_matching_track(tracks, "audio", audio_priority, false)
if fallback_aid then
mp.set_property("aid", fallback_aid)
end
end
end
local function update_subtitle()
local tracks = mp.get_property_native("track-list", {})
local current_aid = mp.get_property_number("aid")
local audio_lang = nil
local strict_mode = false
-- if no audio track is selected, turn off subs and exit
if not current_aid or current_aid == 0 then
mp.set_property("sid", "no")
mp.set_property("sub-visibility", "no")
return
end
-- Get current audio language
if current_aid then
for _, track in ipairs(tracks) do
if track.id == current_aid and track.type == "audio" then
audio_lang = (track.lang or ""):lower()
-- If lang is empty, try detecting it in title/label using our keyword map
if audio_lang == "" then
local text = ((track.title or "") .. " " .. (track.label or "")):lower()
for lang_code, kw_list in pairs(audio_priority_keywords_map) do
for _, kw in ipairs(kw_list) do
if text:find(kw, 1, true) then
audio_lang = lang_code -- use the language key from the map
break
end
end
if audio_lang ~= "" then break end
end
end
break
end
end
end
local sid = nil
-- 1. Selecting Forced Subs for PREFERRED LANGUAGE Audio
if is_lang_in_list(audio_lang, preferred_lang_codes) then
strict_mode = true
-- Preferred Language
sid = find_matching_track(tracks, "sub", sub_priority_PREFERRED_LANG, true, forced_subs_keywords_map[preferred_lang_primary_code]) -- 1. Flag + Keyword
if not sid then sid = find_matching_track(tracks, "sub", sub_priority_PREFERRED_LANG, true) end -- 2. Flag Only
if not sid then sid = find_matching_track(tracks, "sub", sub_priority_PREFERRED_LANG, false, forced_subs_keywords_map[preferred_lang_primary_code]) end -- 3. Keyword Only (out of the non-Forced subs)
-- Middle English (if enabled)
if prioritize_honorifics then
if not sid then sid = find_matching_track(tracks, "sub", sub_priority_ENM, true, forced_subs_keywords_map.enm) end -- 1. Flag + Keyword
if not sid then sid = find_matching_track(tracks, "sub", sub_priority_ENM, true) end -- 2. Flag Only
if not sid then sid = find_matching_track(tracks, "sub", sub_priority_ENM, false, forced_subs_keywords_map.enm) end -- 3. Keyword Only (out of the non-Forced subs)
end
-- Standard English
if not sid then sid = find_matching_track(tracks, "sub", sub_priority_STANDARD_ENG, true, forced_subs_keywords_map.en) end -- 1. Flag + Keyword
if not sid then sid = find_matching_track(tracks, "sub", sub_priority_STANDARD_ENG, true) end -- 2. Flag Only
if not sid then sid = find_matching_track(tracks, "sub", sub_priority_STANDARD_ENG, false, forced_subs_keywords_map.en) end -- 3. Keyword Only (out of the non-Forced subs)
-- Fallback to final_fallback_sub_priority order
if not sid then
sid = find_matching_track(tracks, "sub", final_fallback_sub_priority, true)
end
-- Fallback to any Forced subs (if enabled)
if not sid and fallback_to_any_forced_subs then
sid = find_matching_track(tracks, "sub", {{"*"}}, true)
end
end
-- 2. Selecting Forced Subs for English Audio (Forced ENG Only, prioritizing 'enm' if enabled)
if not sid and audio_lang and (audio_lang == "en" or audio_lang == "eng") then
strict_mode = true
-- Middle English (if enabled)
if prioritize_honorifics then
sid = find_matching_track(tracks, "sub", sub_priority_ENM, true, forced_subs_keywords_map.enm) -- 1. Flag + Keyword
if not sid then sid = find_matching_track(tracks, "sub", sub_priority_ENM, true) end -- 2. Flag Only
if not sid then sid = find_matching_track(tracks, "sub", sub_priority_ENM, false, forced_subs_keywords_map.enm) end -- 3. Keyword Only (out of the non-Forced subs)
end
-- Standard English
if not sid then sid = find_matching_track(tracks, "sub", sub_priority_STANDARD_ENG, true, forced_subs_keywords_map.en) end -- 1. Flag + Keyword
if not sid then sid = find_matching_track(tracks, "sub", sub_priority_STANDARD_ENG, true) end -- 2. Flag Only
if not sid then sid = find_matching_track(tracks, "sub", sub_priority_STANDARD_ENG, false, forced_subs_keywords_map.en) end -- 3. Keyword Only (out of the non-Forced subs)
-- Fallback to final_fallback_sub_priority order
if not sid then
sid = find_matching_track(tracks, "sub", final_fallback_sub_priority, true)
end
end
-- 3. Selecting Non-Forced Subs (non-strict mode)
if not sid and not strict_mode then
-- 1. Preferred Language (Flag + Keyword)
sid = find_matching_track(tracks, "sub", sub_priority_PREFERRED_LANG, false, normal_subs_keywords_map[preferred_lang_primary_code])
-- 2. Preferred Language (Flag Only)
if not sid then
sid = find_matching_track(tracks, "sub", sub_priority_PREFERRED_LANG, false)
end
-- 3. & 4. Middle English ('enm') with and without keywords (only if enabled)
if prioritize_honorifics then
-- 3. Middle English ('enm') (Flag + Keyword) (if enabled)
if not sid then
sid = find_matching_track(tracks, "sub", sub_priority_ENM, false, normal_subs_keywords_map.enm)
end
-- 4. Any Middle English ('enm') (Flag Only) (if enabled)
if not sid then
sid = find_matching_track(tracks, "sub", sub_priority_ENM, false)
end
end
-- 5. Standard English ('en'/'eng') (Flag + Keyword)
if not sid then
sid = find_matching_track(tracks, "sub", sub_priority_STANDARD_ENG, false, normal_subs_keywords_map.en)
end
-- 6. Any Standard English ('en'/'eng') (Flag Only)
if not sid then
sid = find_matching_track(tracks, "sub", sub_priority_STANDARD_ENG, false)
end
-- 7. Final fallback to final_fallback_sub_priority order (Flag Only)
if not sid then
sid = find_matching_track(tracks, "sub", final_fallback_sub_priority, false)
end
end
if sid then
mp.set_property("sid", sid)
mp.set_property("sub-visibility", "yes")
else
mp.set_property("sid", "no")
mp.set_property("sub-visibility", "no")
end
end
-- This function fixes the manual override problem
local function on_manual_sub_change(_, new_sid)
-- If a subtitle track is manually selected (not 'no'), ensure visibility is on.
if new_sid and new_sid > 0 then
mp.set_property("sub-visibility", "yes")
end
end
-- Event handlers
mp.register_event("file-loaded", function()
set_preferred_audio()
update_subtitle()
end)
mp.observe_property("aid", "number", update_subtitle)
-- This observer listens for manual subtitle changes and ensures they become visible.
mp.observe_property("sid", "number", on_manual_sub_change)