├── README.md └── osdb.lua /README.md: -------------------------------------------------------------------------------- 1 | # OSDb-mpv 2 | 3 | OpenSubtitles automatic downloader script for [MPV](http://mpv.io/). Relies on LuaSocket, lua-xmlrpc and lua-zlib. 4 | 5 | # Prerequisites 6 | 7 | Obviously, you need to install MPV first. 8 | 9 | *lua-xmlrpc* and its dependencies are available on LuaRocks: 10 | 11 | luarocks install luaxmlrpc lua-zlib 12 | 13 | MPV is using Lua 5.2 by default, so it's recommended to use LuaRocks for the same version of Lua. 14 | 15 | Alternatively, one can use distribution Lua packages (confirmed working on Ubuntu 16.04 and derivatives): 16 | 17 | sudo apt-get install lua-socket lua-xmlrpc lua-zlib 18 | # Currently, symlink is required to be able to use xmlrpc in Lua 5.2 19 | sudo ln -s /usr/share/lua/5.1/xmlrpc /usr/share/lua/5.2/xmlrpc 20 | 21 | # Installation 22 | 23 | Just drop *osdb.lua* and *osdb-rpc.lua* into **~/.mpv/scripts** (or **~/.config/mpv/scripts**). 24 | 25 | # Configuration 26 | 27 | At the moment, this plugin has the following options: 28 | 29 | user=foo 30 | password=bar 31 | 32 | Optional credentials to use with OpenSubtitles API - your login and password that you use on opensubtitles.org. 33 | 34 | autoLoadSubtitles=[yes|no] 35 | 36 | Automatically load subtitles when a file is loaded. Default is 'no'. 37 | 38 | numSubtitles=10 39 | 40 | Number of matching subtitles to query from OpenSubtitles. Default is 10. Maximum allowed is 500. 41 | 42 | language='eng' 43 | 44 | Subtitle languages to search for. Default is 'eng'. Can be multiple values, comma-separated. 45 | 46 | autoFlagSubtitles=[yes|no] 47 | 48 | Flag subtitles automatically when switching to the next subtitle suggestion. Default is 'no'. 49 | 50 | You can either add those to MPV configuration file, for example: 51 | 52 | script-opts=osdb-autoLoadSubtitles=yes,osdb-numSubtitles=100 53 | 54 | Or create a separate lua-settings/osdb.conf file with following contents: 55 | 56 | autoLoadSubtitles=yes 57 | numSubtitles=100 58 | user=foo 59 | password=bar 60 | 61 | # Usage 62 | 63 | If *autoLoadSubtitles* is enabled, subtitles will be found automatically when a file is loaded. 64 | 65 | Otherwise, press **Ctrl+F** to search for subtitles. 66 | 67 | To cycle through different subtitles found on OSDb, press **Ctrl+F** again. 68 | 69 | To flag a subtitle, if it has invalid timings and/or designed for another release of the same movie, press **Ctrl+R**. 70 | 71 | -------------------------------------------------------------------------------- /osdb.lua: -------------------------------------------------------------------------------- 1 | if mp == nil then 2 | print('Must be run inside MPV') 3 | end 4 | 5 | local rpc = require 'xmlrpc.http' 6 | local os = require 'os' 7 | local io = require 'io' 8 | local http = require 'socket.http' 9 | local zlib = require 'zlib' 10 | local ltn12 = require 'ltn12' 11 | 12 | local msg = require 'mp.msg' 13 | local utils = require 'mp.utils' 14 | 15 | require 'mp.options' 16 | -- Read options from {mpv_config_dir}/lua-settings/osdb.conf 17 | local options = { 18 | tempFolder = '/tmp', 19 | autoLoadSubtitles = false, 20 | numSubtitles = 10, 21 | language = 'eng', 22 | autoFlagSubtitles = false, 23 | useHashSearch = true, 24 | useFilenameSearch = true, 25 | osdDelayLong = 30, 26 | osdDelayShort = -1, 27 | user = '', 28 | password = '' 29 | } 30 | read_options(options, 'osdb') 31 | 32 | 33 | -- This is for performing RPC calls to OpenSubtitles 34 | local osdb = {} 35 | 36 | osdb.API = 'http://api.opensubtitles.org/xml-rpc' 37 | osdb.USERAGENT = 'osdb-mpv v1' 38 | 39 | function osdb.rpc(...) 40 | local args = {...} 41 | 42 | for i, item in pairs(args) do 43 | args[i] = osdb.xml_escape(item) 44 | end 45 | 46 | local ok, res = rpc.call(osdb.API, table.unpack(args)) 47 | 48 | if not ok then 49 | error('Request failed.') 50 | elseif res.status ~= '200 OK' then 51 | error('Request failed: ' .. res.status) 52 | end 53 | 54 | return res 55 | end 56 | 57 | function osdb.login(user, password) 58 | local res = osdb.rpc('LogIn', user, password, 'en', osdb.USERAGENT) 59 | osdb.token = res.token 60 | end 61 | 62 | function osdb.logout() 63 | assert(osdb.token) 64 | osdb.rpc('LogOut', osdb.token) 65 | end 66 | 67 | function osdb.query(search_query, nsubtitles) 68 | assert(osdb.token) 69 | local limit = {limit = nsubtitles} 70 | 71 | local res = osdb.rpc('SearchSubtitles', osdb.token, search_query, limit) 72 | if res.data == false then 73 | error('No subtitles found in OSDb') 74 | end 75 | return res.data 76 | end 77 | 78 | function osdb.report(subdata) 79 | assert(osdb.token) 80 | assert(subdata) 81 | osdb.rpc('ReportWrongMovieHash', osdb.token, subdata.IDSubMovieFile) 82 | end 83 | 84 | function osdb.xml_escape(val) 85 | if type(val) == 'string' then 86 | return val:gsub('%&', '&'):gsub('%<', '<'):gsub('%>', '>') 87 | elseif type(val) == 'table' then 88 | local conv = {} 89 | for k, v in pairs(val) do 90 | conv[k] = osdb.xml_escape(v) 91 | end 92 | return conv 93 | else 94 | return val 95 | end 96 | end 97 | 98 | -- Movie hash function for OSDB, courtesy of 99 | -- http://trac.opensubtitles.org/projects/opensubtitles/wiki/HashSourceCodes 100 | function movieHash(fileName) 101 | local fil = io.open(fileName, "rb") 102 | if fil == nil then 103 | error("Can't open file") 104 | end 105 | local lo,hi=0,0 106 | for i=1,8192 do 107 | local a,b,c,d = fil:read(4):byte(1,4) 108 | lo = lo + a + b*256 + c*65536 + d*16777216 109 | a,b,c,d = fil:read(4):byte(1,4) 110 | hi = hi + a + b*256 + c*65536 + d*16777216 111 | while lo>=4294967296 do 112 | lo = lo-4294967296 113 | hi = hi+1 114 | end 115 | while hi>=4294967296 do 116 | hi = hi-4294967296 117 | end 118 | end 119 | local size = fil:seek("end", -65536) + 65536 120 | for i=1,8192 do 121 | local a,b,c,d = fil:read(4):byte(1,4) 122 | lo = lo + a + b*256 + c*65536 + d*16777216 123 | a,b,c,d = fil:read(4):byte(1,4) 124 | hi = hi + a + b*256 + c*65536 + d*16777216 125 | while lo>=4294967296 do 126 | lo = lo-4294967296 127 | hi = hi+1 128 | end 129 | while hi>=4294967296 do 130 | hi = hi-4294967296 131 | end 132 | end 133 | lo = lo + size 134 | while lo>=4294967296 do 135 | lo = lo-4294967296 136 | hi = hi+1 137 | end 138 | while hi>=4294967296 do 139 | hi = hi-4294967296 140 | end 141 | fil:close() 142 | return string.format("%08x%08x", hi,lo), size 143 | end 144 | 145 | function download(subtitle) 146 | assert(subtitle.SubDownloadLink and subtitle.SubFileName) 147 | 148 | local inflate = zlib.inflate() 149 | local decompress = function(chunk) 150 | if chunk ~= '' and chunk ~= nil then 151 | return inflate(chunk) 152 | else 153 | return chunk 154 | end 155 | end 156 | 157 | local subfile = string.format(options.tempFolder..'/%s', subtitle.SubFileName) 158 | http.request { 159 | url = subtitle.SubDownloadLink, 160 | sink = ltn12.sink.chain( 161 | decompress, 162 | ltn12.sink.file(io.open(subfile, 'wb')) 163 | ) 164 | } 165 | return subfile 166 | end 167 | 168 | -- Subtitle list cache 169 | local subtitles = {} 170 | 171 | function subtitles.set(self, list) 172 | self.count = #list 173 | self.list = list 174 | self.current = nil 175 | self.idx = nil 176 | 177 | for _, sub in pairs(list) do 178 | sub.download = download 179 | end 180 | end 181 | 182 | function subtitles.next(self) 183 | self.idx = next(self.list, self.idx) 184 | self.current = self.list[self.idx] 185 | end 186 | 187 | function fetch_list() 188 | local srcfile = mp.get_property('path') 189 | assert(srcfile ~= nil) 190 | mp.osd_message("Searching for subtitles...", options.osdDelayLong) 191 | local searchQuery = {} 192 | if options.useHashSearch then 193 | local ok, mhash, fsize = pcall(movieHash, srcfile) 194 | if ok then 195 | table.insert(searchQuery, 196 | { 197 | moviehash = mhash, 198 | moviebytesize = fsize, 199 | sublanguageid = options.language 200 | }) 201 | else 202 | msg.warn("Movie hash couldn't be computed") 203 | end 204 | end 205 | if options.useFilenameSearch then 206 | local _, basename = utils.split_path(srcfile) 207 | table.insert(searchQuery, 208 | { 209 | query = basename, 210 | sublanguageid = options.language 211 | }) 212 | end 213 | osdb.login(options.user, options.password) 214 | subtitles:set(osdb.query(searchQuery, options.numSubtitles)) 215 | 216 | if subtitles.count == 0 then 217 | mp.osd_message("No subtitles found", options.osdDelayShort) 218 | end 219 | osdb.logout() 220 | end 221 | 222 | function rotate_subtitles() 223 | if subtitles.count == 0 then 224 | -- Refresh the subtitle list 225 | fetch_list() 226 | end 227 | 228 | -- Remove previous subtitle track, if possible 229 | if subtitles.current ~= nil and subtitles.current._sid ~= nil then 230 | mp.commandv('sub_remove', subtitles.current._sid) 231 | if options.autoFlagSubtitles then 232 | flag_subtitle() 233 | end 234 | end 235 | 236 | -- Move to the next subtitle 237 | subtitles:next() 238 | 239 | -- If at the end of the list (or no subtitles found), don't do anything 240 | if subtitles.current == nil then 241 | return 242 | end 243 | 244 | -- Load current subtitle 245 | mp.osd_message(string.format( 246 | "[%d/%d] Downloading subtitle…\n%s", 247 | subtitles.idx, subtitles.count, subtitles.current.SubFileName 248 | ), options.osdDelayLong) 249 | local filename = subtitles.current:download() 250 | mp.commandv('sub_add', filename) 251 | mp.osd_message(string.format( 252 | "[%d/%d] Using subtitle (matched by %s)\n%s", 253 | subtitles.idx, subtitles.count, 254 | subtitles.current.MatchedBy, subtitles.current.SubFileName 255 | ), options.osdDelayShort) 256 | -- Remember which track it is 257 | subtitles.current._sid = mp.get_property('sid') 258 | end 259 | 260 | function flag_subtitle() 261 | if subtitles.current ~= nil and subtitles.current.MatchedBy == 'moviehash' then 262 | osdb.login(options.user, options.password) 263 | mp.osd_message("Subtitle suggestion reported as incorrect", 264 | options.osdDelayShort) 265 | osdb.report(subtitles.current) 266 | osdb.logout() 267 | end 268 | end 269 | 270 | function catch(callback, ...) 271 | xpcall( 272 | callback, 273 | function(err) 274 | msg.warn(debug.traceback()) 275 | msg.fatal(err) 276 | mp.osd_message("Error: " .. err, options.osdDelayShort) 277 | end, 278 | ... 279 | ) 280 | end 281 | 282 | mp.add_key_binding('Ctrl+r', 'osdb_report', function() catch(flag_subtitle) end) 283 | mp.add_key_binding('Ctrl+f', 'osdb_find_subtitles', function() catch(rotate_subtitles) end) 284 | mp.register_event('file-loaded', function (event) 285 | -- Reset the cache 286 | subtitles:set({}) 287 | if options.autoLoadSubtitles then 288 | catch(rotate_subtitles) 289 | end 290 | end) 291 | 292 | --------------------------------------------------------------------------------