├── README.md ├── trackselect.conf └── trackselect.lua /README.md: -------------------------------------------------------------------------------- 1 | # trackselect 2 | Automatically select your preferred tracks based on title, because --slang isn't smart enough. 3 | 4 | ## Installation 5 | Place trackselect.lua in your mpv `scripts` folder. 6 | 7 | ## Usage 8 | Play a file with english audio and/or a "Signs/Songs" track set as default. 9 | Default settings will select non-dub audio and subtitle tracks (intended for anime). -------------------------------------------------------------------------------- /trackselect.conf: -------------------------------------------------------------------------------- 1 | # Options are slash-separated lists of words and languages 2 | # These are the default settings, intended for anime 3 | 4 | preferred_audio_lang=jpn/japanese 5 | preferred_audio_channels= 6 | excluded_audio_words= 7 | expected_audio_words= 8 | 9 | preferred_video_lang= 10 | excluded_video_words= 11 | expected_video_words= 12 | 13 | preferred_sub_lang=eng 14 | excluded_sub_words=sign 15 | expected_sub_words= 16 | 17 | # Settings for selecting dubs 18 | 19 | #preferred_audio_lang=eng 20 | #preferred_audio_channels= 21 | #excluded_audio_words= 22 | #expected_audio_words= 23 | # 24 | #preferred_sub_lang=eng 25 | #excluded_sub_words= 26 | #expected_sub_words=sign 27 | -------------------------------------------------------------------------------- /trackselect.lua: -------------------------------------------------------------------------------- 1 | -- trackselect.lua 2 | -- 3 | -- Because --slang isn't smart enough. 4 | -- 5 | -- This script tries to select non-dub 6 | -- audio and subtitle tracks. 7 | -- Idea from https://github.com/siikamiika/scripts/blob/master/mpv%20scripts/dualaudiofix.lua 8 | 9 | local defaults = { 10 | audio = { 11 | selected = nil, 12 | best = {}, 13 | lang_score = nil, 14 | channels_score = -math.huge, 15 | preferred = "jpn/japanese", 16 | excluded = "", 17 | expected = "", 18 | id = "" 19 | }, 20 | video = { 21 | selected = nil, 22 | best = {}, 23 | lang_score = nil, 24 | preferred = "", 25 | excluded = "", 26 | expected = "", 27 | id = "" 28 | }, 29 | sub = { 30 | selected = nil, 31 | best = {}, 32 | lang_score = nil, 33 | preferred = "eng", 34 | excluded = "sign", 35 | expected = "", 36 | id = "" 37 | } 38 | } 39 | 40 | local options = { 41 | enabled = true, 42 | 43 | -- Do track selection synchronously, plays nicer with other scripts 44 | hook = true, 45 | 46 | -- Mimic mpv's track list fingerprint to preserve user-selected tracks across files 47 | fingerprint = true, 48 | 49 | -- Override user's explicit track selection 50 | force = false, 51 | 52 | -- Try to re-select the last track if mpv cannot do it e.g. when fingerprint changes 53 | smart_keep = false 54 | } 55 | 56 | for _type, track in pairs(defaults) do 57 | options["preferred_" .. _type .. "_lang"] = track.preferred 58 | options["excluded_" .. _type .. "_words"] = track.excluded 59 | options["expected_" .. _type .. "_words"] = track.expected 60 | end 61 | 62 | options["preferred_audio_channels"] = "" 63 | 64 | local tracks = {} 65 | local last = {} 66 | local fingerprint = "" 67 | 68 | mp.options = require "mp.options" 69 | 70 | function contains(track, words, attr) 71 | if not track[attr] then return false end 72 | local i = 0 73 | if track.external then 74 | i = 1 75 | end 76 | for word in string.gmatch(words:lower(), "([^/]+)") do 77 | i = i - 1 78 | if string.match(tostring(track[attr] or ""):lower(), word) then 79 | return i 80 | end 81 | end 82 | return false 83 | end 84 | 85 | function preferred(track, words, attr, title) 86 | local score = contains(track, words, attr) 87 | if not score then 88 | if tracks[track.type][title] == nil then 89 | tracks[track.type][title] = -math.huge 90 | return true 91 | end 92 | return false 93 | end 94 | if tracks[track.type][title] == nil or score > tracks[track.type][title] then 95 | tracks[track.type][title] = score 96 | return true 97 | end 98 | return false 99 | end 100 | 101 | function preferred_or_equals(track, words, attr, title) 102 | local score = contains(track, words, attr) 103 | if not score then 104 | if tracks[track.type][title] == nil or tracks[track.type][title] == -math.huge then 105 | return true 106 | end 107 | return false 108 | end 109 | if tracks[track.type][title] == nil or score >= tracks[track.type][title] then 110 | return true 111 | end 112 | return false 113 | end 114 | 115 | function copy(obj) 116 | if type(obj) ~= "table" then return obj end 117 | local res = {} 118 | for k, v in pairs(obj) do res[k] = copy(v) end 119 | return res 120 | end 121 | 122 | function track_layout_hash(tracklist) 123 | local t = {} 124 | for _, track in ipairs(tracklist) do 125 | t[#t+1] = string.format("%s-%d-%s-%s-%s-%s", track.type, track.id, tostring(track.default), tostring(track.external), track.lang or "", track.external and "" or (track.title or "")) 126 | end 127 | return table.concat(t, "\n") 128 | end 129 | 130 | function trackselect() 131 | mp.options.read_options(options, "trackselect") 132 | if not options.enabled then return end 133 | tracks = copy(defaults) 134 | local filename = mp.get_property("filename/no-ext") 135 | local tracklist = mp.get_property_native("track-list") 136 | local tracklist_changed = false 137 | local found_last = {} 138 | if options.fingerprint then 139 | local new_fingerprint = track_layout_hash(tracklist) 140 | if new_fingerprint == fingerprint then 141 | return 142 | end 143 | fingerprint = new_fingerprint 144 | tracklist_changed = true 145 | end 146 | for _, track in ipairs(tracklist) do 147 | if options.smart_keep and last[track.type] ~= nil and last[track.type].lang == track.lang and last[track.type].codec == track.codec and last[track.type].external == track.external and last[track.type].title == track.title then 148 | tracks[track.type].best = track 149 | options["preferred_" .. track.type .. "_lang"] = "" 150 | options["excluded_" .. track.type .. "_words"] = "" 151 | options["expected_" .. track.type .. "_words"] = "" 152 | options["preferred_" .. track.type .. "_channels"] = "" 153 | found_last[track.type] = true 154 | elseif not options.force and (tracklist_changed or not options.fingerprint) then 155 | if tracks[track.type].id == "" then 156 | tracks[track.type].id = mp.get_property(track.type:sub(1, 1) .. "id", "auto") 157 | end 158 | if tracks[track.type].id ~= "auto" then 159 | options["preferred_" .. track.type .. "_lang"] = "" 160 | options["excluded_" .. track.type .. "_words"] = "" 161 | options["expected_" .. track.type .. "_words"] = "" 162 | options["preferred_" .. track.type .. "_channels"] = "" 163 | end 164 | end 165 | if options["preferred_" .. track.type .. "_lang"] ~= "" or options["excluded_" .. track.type .. "_words"] ~= "" or options["expected_" .. track.type .. "_words"] ~= "" or (options["preferred_" .. track.type .. "_channels"] or "") ~= "" then 166 | if track.selected then 167 | tracks[track.type].selected = track.id 168 | if options.smart_keep then 169 | last[track.type] = track 170 | end 171 | end 172 | if track.external then 173 | track.title = string.gsub(string.gsub(track.title, "%W", "%%%1"), filename, "") 174 | end 175 | if next(tracks[track.type].best) == nil or not (tracks[track.type].best.external and tracks[track.type].best.lang ~= nil and not track.external) then 176 | if options["excluded_" .. track.type .. "_words"] == "" or not contains(track, options["excluded_" .. track.type .. "_words"], "title") then 177 | if options["expected_" .. track.type .. "_words"] == "" or contains(track, options["expected_" .. track.type .. "_words"], "title") then 178 | local pass = true 179 | local channels = false 180 | local lang = false 181 | if (options["preferred_" .. track.type .. "_channels"] or "") ~= "" and preferred_or_equals(track, options["preferred_" .. track.type .. "_lang"], "lang", "lang_score") then 182 | channels = preferred(track, options["preferred_" .. track.type .. "_channels"], "demux-channel-count", "channels_score") 183 | pass = channels 184 | end 185 | if options["preferred_" .. track.type .. "_lang"] ~= "" then 186 | lang = preferred(track, options["preferred_" .. track.type .. "_lang"], "lang", "lang_score") 187 | end 188 | if (options["preferred_" .. track.type .. "_lang"] == "" and pass) or channels or lang or (track.external and track.lang == nil and (not tracks[track.type].best.external or tracks[track.type].best.lang == nil)) then 189 | tracks[track.type].best = track 190 | end 191 | end 192 | end 193 | end 194 | end 195 | end 196 | for _type, track in pairs(tracks) do 197 | if next(track.best) ~= nil and track.best.id ~= track.selected then 198 | mp.set_property(_type:sub(1, 1) .. "id", track.best.id) 199 | if options.smart_keep and found_last[track.best.type] then 200 | last[track.best.type] = track.best 201 | end 202 | end 203 | end 204 | end 205 | 206 | function selected_tracks() 207 | local tracklist = mp.get_property_native("track-list") 208 | last = {} 209 | for _, track in ipairs(tracklist) do 210 | if track.selected then 211 | last[track.type] = track 212 | end 213 | end 214 | end 215 | 216 | if options.hook then 217 | mp.add_hook("on_preloaded", 50, trackselect) 218 | else 219 | mp.register_event("file-loaded", trackselect) 220 | end 221 | 222 | if options.smart_keep then 223 | mp.register_event("track-switched", selected_tracks) 224 | end --------------------------------------------------------------------------------