commit dd25ddd888abb813c87d5eaaeedefe127848583d Author: ae Date: Tue Oct 29 18:40:05 2024 +0200 chore: commit history pruned diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..432538d --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 David Deprost + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..66a7676 --- /dev/null +++ b/README.md @@ -0,0 +1,77 @@ +# [Automatic subtitle downloading for MPV](https://github.com/davidde/mpv-autosub) + +- Cross-platform: **Windows, Mac and Linux** +- Multi-language support +- Subtitle provider login support +- **No hotkeys required**: opening a video will automatically trigger subtitles to download + (Only when the right subtitles are not yet present) + +## Dependencies + +This Lua script uses the [Python](https://www.python.org/downloads/) program +[subliminal](https://github.com/Diaoul/subliminal) to download subtitles. +Make sure you have both installed: + +```bash +pip install subliminal +``` + +## Setup + +1. Copy autosub.lua into: + + | OS | Path | + | ------------- | ---------------------------------------------------- | + | **Windows** | [Drive]:\Users\\[User]\AppData\Roaming\mpv\scripts\ | + | **Mac/Linux** | ~/.config/mpv/scripts/ | + + ```bash + mkdir ~/.config/mpv/scripts + cat > ~/.config/mpv/scripts/autosub.lua + [Paste script contents and CTRL+D] + ``` + +2. Specify the correct subliminal location for your system: + + - To determine the correct path, use: + + | OS | App | Command | + | ------------- | -------------- | ---------------- | + | **Windows** | Command Prompt | where subliminal | + | **Mac/Linux** | Terminal | which subliminal | + + - Copy the path(s) to the subliminal variable(s) at the start of the script (optionally define multiple paths for different operating systems) + ```lua + local subliminal_paths = { + ['windows'] = '', + ['linux'] = '', + ['darwin'] = '', + } + ``` + On Windows, the backslashes in the path need to be escaped, e.g.: + **C:\\\\Users\\\\Administrator\\\\AppData\\\\Local\\\\Programs\\\\Python\\\\Python37\\\\Scripts\\\\subliminal.exe** + +## Customization + +- Optionally change the subtitle languages / [ISO codes](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes). + Be sure to put your preferred language at the top of the list. + If necessary, you can manually trigger downloading your first choice language by pressing `b`, + or your second choice language by pressing `n`. +- Optionally specify the login credentials for your preferred subtitle provider(s), if you have one. +- If you do not care for the automatic downloading functionality, and only wish to use the hotkeys, + simply change the `auto` bool to `false`. +- For added convenience, you can specify the locations to exclude from auto-downloading subtitles, or alternatively, + the _only_ locations that _should_ auto-download subtitles. + +This script is under the [MIT License](./LICENSE-MIT), +so you are free to modify and adapt this script to your needs: +check out the [MPV Lua API](https://mpv.io/manual/stable/#lua-scripting) for more information. + +If you find yourself unable to find the correct subtitles for some niche movies/series, +you might be interested in the [submod](https://github.com/davidde/submod_rs) +command line tool I've written to manually correct subtitle timing. + +## Credits + +Inspired by [selsta's](https://gist.github.com/selsta/ce3fb37e775dbd15c698) and +[fullmetalsheep's](https://gist.github.com/fullmetalsheep/28c397b200a7348027d983f31a7eddfa) autosub scripts. diff --git a/autosub.lua b/autosub.lua new file mode 100644 index 0000000..d0aced3 --- /dev/null +++ b/autosub.lua @@ -0,0 +1,303 @@ +--============================================================================= +-->> SUBLIMINAL PATH: +--============================================================================= +-- This script uses Subliminal to download subtitles, +-- so make sure to specify your systems's Subliminal locations below: +local subliminal_paths = { + ['windows'] = '', + ['linux'] = '', + ['darwin'] = '', +} + +--============================================================================= +-->> SUBTITLE LANGUAGE: +--============================================================================= +-- Specify languages in this order: +-- { 'language name', 'ISO-639-1', 'ISO-639-2' } ! +-- (See: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) +local languages = { + -- If subtitles are found for the first language, + -- other languages will NOT be downloaded, + -- so put your preferred language first: + { 'English', 'en', 'eng' }, + -- { 'Swedish', 'sv', 'swe' }, + -- { 'Spanish', 'es', 'spa' }, + -- { 'French', 'fr', 'fre' }, + -- { 'German', 'de', 'ger' }, + -- { 'Italian', 'it', 'ita' }, + -- { 'Portuguese', 'pt', 'por' }, + -- { 'Polish', 'pl', 'pol' }, + -- { 'Russian', 'ru', 'rus' }, + -- { 'Chinese', 'zh', 'chi' }, + -- { 'Arabic', 'ar', 'ara' }, +} +--============================================================================= +-->> PROVIDER LOGINS: +--============================================================================= +-- These are completely optional and not required +-- for the functioning of the script! +-- If you use any of these services, simply uncomment it +-- and replace 'USERNAME' and 'PASSWORD' with your own: +local logins = { + -- { '--addic7ed', 'USERNAME', 'PASSWORD' }, + -- { '--legendastv', 'USERNAME', 'PASSWORD' }, + -- { '--opensubtitles', 'USERNAME', 'PASSWORD' }, + -- { '--subscenter', 'USERNAME', 'PASSWORD' }, +} +--============================================================================= +-->> ADDITIONAL OPTIONS: +--============================================================================= +local bools = { + auto = true, -- Automatically download subtitles, no hotkeys required + debug = false, -- Use `--debug` in subliminal command for debug output + force = true, -- Force download; will overwrite existing subtitle files + utf8 = true, -- Save all subtitle files as UTF-8 +} +local excludes = { + -- Movies with a path containing any of these strings/paths + -- will be excluded from auto-downloading subtitles. + -- Full paths are also allowed, e.g.: + -- '/home/david/Videos', + 'no-subs-dl', +} +local includes = { + -- If anything is defined here, only the movies with a path + -- containing any of these strings/paths will auto-download subtitles. + -- Full paths are also allowed, e.g.: + -- '/home/david/Videos', +} +--============================================================================= +local utils = require 'mp.utils' +local subliminal = nil + +-- Download function: download the best subtitles in most preferred language +function download_subs(language) + language = language or languages[1] + if #language == 0 then + log('No Language found\n') + return false + end + + log('Searching ' .. language[1] .. ' subtitles ...', 30) + + -- Build the `subliminal` command, starting with the executable: + local table = { args = { subliminal } } + local a = table.args + + for _, login in ipairs(logins) do + a[#a + 1] = login[1] + a[#a + 1] = login[2] + a[#a + 1] = login[3] + end + if bools.debug then + -- To see `--debug` output start MPV from the terminal! + a[#a + 1] = '--debug' + end + + a[#a + 1] = 'download' + if bools.force then + a[#a + 1] = '-f' + end + if bools.utf8 then + a[#a + 1] = '-e' + a[#a + 1] = 'utf-8' + end + + a[#a + 1] = '-l' + a[#a + 1] = language[2] + a[#a + 1] = '-d' + a[#a + 1] = directory + a[#a + 1] = filename --> Subliminal command ends with the movie filename. + + local result = utils.subprocess(table) + + if string.find(result.stdout, 'Downloaded 1 subtitle') then + -- When multiple external files are present, + -- always activate the most recently downloaded: + mp.set_property('slang', language[2]) + -- Subtitles are downloaded successfully, so rescan to activate them: + mp.commandv('rescan_external_files') + log(language[1] .. ' subtitles ready!') + return true + else + log('No ' .. language[1] .. ' subtitles found\n') + return false + end +end + +-- Manually download second language subs by pressing 'n': +function download_subs2() + download_subs(languages[2]) +end + +-- Control function: only download if necessary +function control_downloads() + -- Make MPV accept external subtitle files with language specifier: + mp.set_property('sub-auto', 'fuzzy') + -- Set subtitle language preference: + mp.set_property('slang', languages[1][2]) + mp.msg.warn('Reactivate external subtitle files:') + mp.commandv('rescan_external_files') + directory, filename = utils.split_path(mp.get_property('path')) + + if not autosub_allowed() then + return + end + + sub_tracks = {} + for _, track in ipairs(mp.get_property_native('track-list')) do + if track['type'] == 'sub' then + sub_tracks[#sub_tracks + 1] = track + end + end + if bools.debug then -- Log subtitle properties to terminal: + for _, track in ipairs(sub_tracks) do + mp.msg.warn('Subtitle track', track['id'], ':\n{') + for k, v in pairs(track) do + if type(v) == 'string' then v = '"' .. v .. '"' end + mp.msg.warn(' "' .. k .. '":', v) + end + mp.msg.warn('}\n') + end + end + + for _, language in ipairs(languages) do + if should_download_subs_in(language) then + if download_subs(language) then return end -- Download successful! + else + return + end -- No need to download! + end + log('No subtitles were found') +end + +-- Check if subtitles should be auto-downloaded: +function autosub_allowed() + local duration = tonumber(mp.get_property('duration')) + local active_format = mp.get_property('file-format') + + if not bools.auto then + mp.msg.warn('Automatic downloading disabled!') + return false + elseif duration < 900 then + mp.msg.warn('Video is less than 15 minutes\n' .. + '=> NOT auto-downloading subtitles') + return false + elseif directory:find('^http') then + mp.msg.warn('Automatic subtitle downloading is disabled for web streaming') + return false + elseif active_format:find('^cue') then + mp.msg.warn('Automatic subtitle downloading is disabled for cue files') + return false + else + local not_allowed = { 'aiff', 'ape', 'flac', 'mp3', 'ogg', 'wav', 'wv', 'tta' } + + for _, file_format in pairs(not_allowed) do + if file_format == active_format then + mp.msg.warn('Automatic subtitle downloading is disabled for audio files') + return false + end + end + + for _, exclude in pairs(excludes) do + local escaped_exclude = exclude:gsub('%W', '%%%0') + local excluded = directory:find(escaped_exclude) + + if excluded then + mp.msg.warn('This path is excluded from auto-downloading subs') + return false + end + end + + for i, include in ipairs(includes) do + local escaped_include = include:gsub('%W', '%%%0') + local included = directory:find(escaped_include) + + if included then + break + elseif i == #includes then + mp.msg.warn('This path is not included for auto-downloading subs') + return false + end + end + end + + return true +end + +-- Check if subtitles should be downloaded in this language: +function should_download_subs_in(language) + for i, track in ipairs(sub_tracks) do + local subtitles = track['external'] and + 'subtitle file' or 'embedded subtitles' + + if not track['lang'] and (track['external'] or not track['title']) + and i == #sub_tracks then + local status = track['selected'] and ' active' or ' present' + log('Unknown ' .. subtitles .. status) + mp.msg.warn('=> NOT downloading new subtitles') + return false -- Don't download if 'lang' key is absent + elseif track['lang'] == language[3] or track['lang'] == language[2] or + (track['title'] and track['title']:lower():find(language[3])) then + if not track['selected'] then + mp.set_property('sid', track['id']) + log('Enabled ' .. language[1] .. ' ' .. subtitles .. '!') + else + log(language[1] .. ' ' .. subtitles .. ' active') + end + mp.msg.warn('=> NOT downloading new subtitles') + return false -- The right subtitles are already present + end + end + mp.msg.warn('No ' .. language[1] .. ' subtitles were detected\n' .. + '=> Proceeding to download:') + return true +end + +-- Log function: log to both terminal and MPV OSD (On-Screen Display) +function log(string, secs) + secs = secs or 2.5 -- secs defaults to 2.5 when secs parameter is absent + mp.msg.warn(string) -- This logs to the terminal + mp.osd_message(string, secs) -- This logs to MPV screen +end + +-- Determine OS to set correct subliminal executable path +-- Source: https://gist.github.com/soulik/82e9d02a818ce12498d1 +function determine_os() + local raw_os_name = '' + + -- LuaJIT shortcut + if jit and jit.os and jit.arch then + raw_os_name = jit.os + else + local popen_status, popen_result = pcall(io.popen, '') + + if popen_status then + popen_result:close() + -- Unix based OS + raw_os_name = io.popen('uname -s', 'r'):read('*l') + else + -- Windows + local env_os = os.getenv('OS') + + if env_os then + raw_os_name = env_os + end + end + end + + return (raw_os_name):lower() +end + +local os_name = determine_os() + +if subliminal_paths[os_name] then + subliminal = subliminal_paths[os_name] +else + mp.msg.warn('Subliminal path not found for ' .. os_name) + mp.msg.warn('=> Subliminal path must be set manually in autosub.lua') +end + +mp.add_key_binding('b', 'download_subs', download_subs) +mp.add_key_binding('n', 'download_subs2', download_subs2) +mp.register_event('file-loaded', control_downloads)