224 lines
8.4 KiB
Lua
224 lines
8.4 KiB
Lua
-- Install [Torrserver](https://github.com/YouROK/TorrServer)
|
|
-- then add "script-opts-append=mpv_torrserver-server=http://[TorrServer ip]:[port]" to mpv.conf
|
|
local utils = require 'mp.utils'
|
|
|
|
local opts = {
|
|
server = "http://localhost:8090",
|
|
torrserver_init = false,
|
|
torrserver_path = "TorrServer",
|
|
search_for_external_tracks = true
|
|
}
|
|
|
|
(require 'mp.options').read_options(opts)
|
|
local luacurl_available, cURL = pcall(require, 'cURL')
|
|
|
|
local is_windows = package.config:sub(1, 1) == "\\" -- detect path separator, windows uses backslashes
|
|
|
|
local function find_executable(name)
|
|
local os_path = os.getenv("PATH") or ""
|
|
local fallback_path = utils.join_path("/usr/bin", name)
|
|
local exec_path
|
|
for path in os_path:gmatch("[^:]+") do
|
|
exec_path = utils.join_path(path, name)
|
|
local meta, meta_error = utils.file_info(exec_path)
|
|
if meta and meta.is_file then
|
|
return exec_path
|
|
end
|
|
end
|
|
if not is_windows then return fallback_path end
|
|
return name -- fallback to just the name, hoping it's in PATH
|
|
end
|
|
|
|
local function init()
|
|
local exec_path = find_executable(opts.torrserver_path)
|
|
local windows_args = { 'powershell', '-NoProfile', '-Command', exec_path }
|
|
local unix_args = { '/bin/bash', '-c', exec_path }
|
|
local args = is_windows and windows_args or unix_args
|
|
local res = mp.command_native_async({ name = "subprocess", capture_stdout = true, playback_only = false, args = args })
|
|
if res.status == 0 then
|
|
mp.msg.error("TorrServer failed to start: ")
|
|
end
|
|
end
|
|
|
|
local char_to_hex = function(c)
|
|
return string.format("%%%02X", string.byte(c))
|
|
end
|
|
|
|
local function urlencode(url)
|
|
if url == nil then
|
|
return
|
|
end
|
|
url = url:gsub("\n", "\r\n")
|
|
url = url:gsub("([^%w ])", char_to_hex)
|
|
url = url:gsub(" ", "+")
|
|
return url
|
|
end
|
|
|
|
local function get_magnet_info(url)
|
|
local info_url = opts.server .. "/stream?stat&link=" .. urlencode(url)
|
|
local res
|
|
if not (luacurl_available) then
|
|
-- if Lua-cURL is not available on this system
|
|
local curl_cmd = {
|
|
"curl",
|
|
"-L",
|
|
"--silent",
|
|
"--max-time", "10",
|
|
info_url
|
|
}
|
|
local cmd = mp.command_native {
|
|
name = "subprocess",
|
|
capture_stdout = true,
|
|
playback_only = false,
|
|
args = curl_cmd
|
|
}
|
|
res = cmd.stdout
|
|
else
|
|
-- otherwise use Lua-cURL (binding to libcurl)
|
|
local buf = {}
|
|
local c = cURL.easy_init()
|
|
c:setopt_followlocation(1)
|
|
c:setopt_url(info_url)
|
|
c:setopt_writefunction(function(chunk)
|
|
table.insert(buf, chunk);
|
|
return true;
|
|
end)
|
|
c:perform()
|
|
res = table.concat(buf)
|
|
end
|
|
if res and res ~= "" then
|
|
return (require 'mp.utils').parse_json(res)
|
|
else
|
|
return nil, "no info response (timeout?)"
|
|
end
|
|
end
|
|
|
|
local function edlencode(url)
|
|
return "%" .. string.len(url) .. "%" .. url
|
|
end
|
|
|
|
local function guess_type_by_extension(ext)
|
|
if ext == "mkv" or ext == "mp4" or ext == "avi" or ext == "wmv" or ext == "vob" or ext == "m2ts" or ext == "ogm" then
|
|
return "video"
|
|
end
|
|
if ext == "mka" or ext == "mp3" or ext == "aac" or ext == "flac" or ext == "ogg" or ext == "wma" or ext == "mpg"
|
|
or ext == "wav" or ext == "wv" or ext == "opus" or ext == "ac3" then
|
|
return "audio"
|
|
end
|
|
if ext == "ass" or ext == "srt" or ext == "vtt" then
|
|
return "sub"
|
|
end
|
|
return "other";
|
|
end
|
|
|
|
local function string_replace(str, match, replace)
|
|
local s, e = string.find(str, match, 1, true)
|
|
if s == nil or e == nil then
|
|
return str
|
|
end
|
|
return string.sub(str, 1, s - 1) .. replace .. string.sub(str, e + 1)
|
|
end
|
|
|
|
-- https://github.com/mpv-player/mpv/blob/master/DOCS/edl-mpv.rst
|
|
local function generate_m3u(magnet_uri, files)
|
|
for _, fileinfo in ipairs(files) do
|
|
-- strip top directory
|
|
if fileinfo.path:find("/", 1, true) then
|
|
fileinfo.fullpath = string.sub(fileinfo.path, fileinfo.path:find("/", 1, true) + 1)
|
|
else
|
|
fileinfo.fullpath = fileinfo.path
|
|
end
|
|
fileinfo.path = {}
|
|
for w in fileinfo.fullpath:gmatch("([^/]+)") do table.insert(fileinfo.path, w) end
|
|
local ext = string.match(fileinfo.path[#fileinfo.path], "%.(%w+)$")
|
|
fileinfo.type = guess_type_by_extension(ext)
|
|
end
|
|
table.sort(files, function(a, b)
|
|
-- make top-level files appear first in the playlist
|
|
if (#a.path == 1 or #b.path == 1) and #a.path ~= #b.path then
|
|
return #a.path < #b.path
|
|
end
|
|
-- make videos first
|
|
if (a.type == "video" or b.type == "video") and a.type ~= b.type then
|
|
return a.type == "video"
|
|
end
|
|
-- otherwise sort by path
|
|
return a.fullpath < b.fullpath
|
|
end);
|
|
|
|
local infohash = magnet_uri:match("^magnet:%?xt=urn:bt[im]h:(%w+)") or urlencode(magnet_uri)
|
|
|
|
local playlist = { '#EXTM3U' }
|
|
|
|
for _, fileinfo in ipairs(files) do
|
|
if fileinfo.processed ~= true then
|
|
table.insert(playlist, '#EXTINF:0,' .. fileinfo.fullpath)
|
|
local basename = string.match(fileinfo.path[#fileinfo.path], '^(.+)%.%w+$')
|
|
|
|
local url = opts.server .. "/stream/" .. urlencode(fileinfo.fullpath) .."?play&index=" .. fileinfo.id .. "&link=" .. infohash
|
|
local hdr = { "!new_stream", "!no_clip",
|
|
--"!track_meta,title=" .. edlencode(basename),
|
|
edlencode(url)
|
|
}
|
|
local edl = "edl://" .. table.concat(hdr, ";") .. ";"
|
|
local external_tracks = 0
|
|
|
|
fileinfo.processed = true
|
|
if opts.search_for_external_tracks and basename ~= nil and fileinfo.type == "video" then
|
|
mp.msg.info("!" .. basename)
|
|
|
|
for _, fileinfo2 in ipairs(files) do
|
|
if #fileinfo2.path > 0 and
|
|
fileinfo2.type ~= "other" and
|
|
fileinfo2.processed ~= true and
|
|
string.find(fileinfo2.path[#fileinfo2.path], basename, 1, true) ~= nil
|
|
then
|
|
mp.msg.info("->" .. fileinfo2.fullpath)
|
|
local title = string_replace(fileinfo2.fullpath, basename, "%")
|
|
local url = opts.server .. "/stream/" .. urlencode(fileinfo2.fullpath).."?play&index=" .. fileinfo2.id .. "&link=" .. infohash
|
|
local hdr = { "!new_stream", "!no_clip", "!no_chapters",
|
|
"!delay_open,media_type=" .. fileinfo2.type,
|
|
"!track_meta,title=" .. edlencode(title),
|
|
edlencode(url)
|
|
}
|
|
edl = edl .. table.concat(hdr, ";") .. ";"
|
|
fileinfo2.processed = true
|
|
external_tracks = external_tracks + 1
|
|
end
|
|
end
|
|
end
|
|
if external_tracks == 0 then -- dont use edl
|
|
table.insert(playlist, url)
|
|
else
|
|
table.insert(playlist, edl)
|
|
end
|
|
end
|
|
end
|
|
return table.concat(playlist, '\n')
|
|
end
|
|
|
|
mp.add_hook("on_load", 5, function()
|
|
local url = mp.get_property("stream-open-filename")
|
|
if url:find("^magnet:") == 1 or (url:find("^https?://") == 1 and url:find("%.torrent$") ~= nil) then
|
|
mp.set_property_bool("file-local-options/ytdl", false)
|
|
if opts.torrserver_init then init() end
|
|
local magnet_info, err = get_magnet_info(url)
|
|
if type(magnet_info) == "table" then
|
|
if magnet_info.file_stats then
|
|
-- torrent has multiple files. open as playlist
|
|
mp.set_property("stream-open-filename", "memory://" .. generate_m3u(url, magnet_info.file_stats))
|
|
return
|
|
end
|
|
-- if not a playlist and has a name
|
|
if magnet_info.name then
|
|
mp.set_property("stream-open-filename", "memory://#EXTM3U\n" ..
|
|
"#EXTINF:0," .. magnet_info.name .. "\n" ..
|
|
opts.server .. "/stream?play&index=1&link=" .. urlencode(url))
|
|
return
|
|
end
|
|
else
|
|
mp.msg.warn("error: " .. err)
|
|
end
|
|
mp.set_property("stream-open-filename", opts.server .. "/stream?m3u&link=" .. urlencode(url))
|
|
end
|
|
end) |