From bce2a5606e34e4d720af3d3fe99d5c188d6930f9 Mon Sep 17 00:00:00 2001 From: Serega007 Date: Fri, 6 Feb 2026 20:50:48 +0300 Subject: [PATCH 01/16] Rewrite select menu to mp.utils.input.select API --- torrserver_loader.lua | 165 +++++++++++------------------------------- 1 file changed, 41 insertions(+), 124 deletions(-) diff --git a/torrserver_loader.lua b/torrserver_loader.lua index b03a847..7ac3efb 100644 --- a/torrserver_loader.lua +++ b/torrserver_loader.lua @@ -2,8 +2,8 @@ -- -- Requires **curl** to be installed in your OS. -local mp = require "mp" local utils = require "mp.utils" +local input = require "mp.input" local options = { TORRSERVER_SCHEME = "http", @@ -15,16 +15,10 @@ require "mp.options".read_options(options, "torrserver-loader") local TORRSERVER = options.TORRSERVER_SCHEME .. "://" .. options.TORRSERVER_HOST .. ":" .. options.TORRSERVER_PORT local torrents = {} -local menu = {} -local cursor_pos = 1 -local State = { HIDDEN = 0, TORRENTS = 1, FILES = 2, } -local state = State.HIDDEN local torrent_index = 1 -local VISIBLE_LINES = 12 -local offset = 0 local opened_btih -local back +local show_torrents -- https://github.com/YouROK/TorrServer/blob/master/server/utils/filetypes.go#L10 local VIDEO_EXTS = { @@ -141,49 +135,6 @@ local function curl(url, data) return response end -local function torr_osd() - local osd_title - if state == State.HIDDEN then - mp.osd_message("") - return - elseif state == State.TORRENTS then - osd_title = "torrent list" - elseif state == State.FILES then - osd_title = "torrent content" - end - - local text = "TorrServer - " .. osd_title .. "\n\n" - - local start = offset + 1 - local finish = math.min(offset + VISIBLE_LINES, #menu) - - for i = start, finish do - text = text - .. ((i == cursor_pos) and "▶ " or " ") - .. menu[i] .. "\n" - end - - if #menu > VISIBLE_LINES then - text = text .. string.format("\n[%d/%d]", cursor_pos, #menu) - end - - mp.osd_message(text, 60) -end - -local function remove_menu_keys() - mp.remove_key_binding("torr_up") - mp.remove_key_binding("torr_down") - mp.remove_key_binding("torr_enter") - mp.remove_key_binding("torr_back") - mp.remove_key_binding("torr_close") -end - -local function close_menu() - state = State.HIDDEN - remove_menu_keys() - mp.osd_message("") -end - -- external name local function replace(s, needle) local i, j = s:find(needle, 1, true) @@ -323,26 +274,7 @@ local function generate_m3u_edl(torrent) torrent.playlist = table.concat(playlist, '\n') end -local function show_torrent_files(torrent) - menu = {} - cursor_pos = 1 - offset = 0 - state = State.FILES - - generate_m3u_edl(torrent) - - for _, fileinfo in ipairs(torrent.file_stats) do - if fileinfo.main_file then - table.insert(menu, fileinfo.filename) - end - end - - torr_osd() -end - local function play_from_playlist(torrent, index) - close_menu() - mp.osd_message("Opening " .. (torrent.name or torrent.title) .. "...") opened_btih = torrent.hash @@ -350,73 +282,58 @@ local function play_from_playlist(torrent, index) mp.set_property_number("playlist-pos", index - 1) end -local function enter() - if state == State.TORRENTS then - if not torrents[cursor_pos].file_stats then - local torrent = curl(TORRSERVER .. "/stream?link=" .. torrents[cursor_pos].hash .. "&stat") - if not torrent then return end - torrents[cursor_pos] = torrent - end - torrent_index = cursor_pos - show_torrent_files(torrents[cursor_pos]) - elseif state == State.FILES then - play_from_playlist(torrents[torrent_index], cursor_pos) +local function show_torrent_files(torrent) + if not torrents[torrent_index].file_stats then + torrent = curl(TORRSERVER .. "/stream?link=" .. torrents[torrent_index].hash .. "&stat") + if not torrent then return end + torrents[torrent_index] = torrent end -end - -local function add_menu_keys() - mp.add_forced_key_binding("UP", "torr_up", function() - if cursor_pos > 1 then - cursor_pos = cursor_pos - 1 - - if cursor_pos <= offset then - offset = math.max(0, cursor_pos - 1) - end - - torr_osd() - end - end, { repeatable = true }) - mp.add_forced_key_binding("DOWN", "torr_down", function() - if cursor_pos < #menu then - cursor_pos = cursor_pos + 1 - - if cursor_pos > offset + VISIBLE_LINES then - offset = cursor_pos - VISIBLE_LINES - end + generate_m3u_edl(torrent) - torr_osd() + local items = {} + for i, entry in ipairs(torrent.file_stats) do + if entry.main_file then + items[i] = entry.filename end - end, { repeatable = true }) + end - mp.add_forced_key_binding("ENTER", "torr_enter", enter) - mp.add_forced_key_binding("BS", "torr_back", back) - mp.add_forced_key_binding("ESC", "torr_close", close_menu) + local selected = false + input.select({ + prompt = "Select an entry of torrent:", + items = items, + + submit = function (index) + selected = true + play_from_playlist(torrents[torrent_index], index) + end, + closed = function () + if selected then return end + + show_torrents(torrent_index) + end, + }) end -local function show_torrents() +show_torrents = function (default_item) torrents = curl(TORRSERVER .. "/torrents", '{"action":"list"}') if not torrents then return end - menu = {} - cursor_pos = 1 - offset = 0 - state = State.TORRENTS - add_menu_keys() - - for _, t in ipairs(torrents) do - table.insert(menu, t.name or t.title) + local items = {} + for i, entry in ipairs(torrents) do + items[i] = entry.name or entry.title end - torr_osd() -end + input.select({ + prompt = "Select a torrent:", + items = items, + default_item = default_item, -back = function() - if state == State.FILES then - show_torrents() - else - close_menu() - end + submit = function (index) + torrent_index = index + show_torrent_files(torrents[index]) + end, + }) end mp.add_key_binding("Ctrl+t", "torr_open", show_torrents) From 90a26626c8b7efd4ff1ac61ba4b9dee6350c5e11 Mon Sep 17 00:00:00 2001 From: Serega007 Date: Fri, 6 Feb 2026 21:34:25 +0300 Subject: [PATCH 02/16] Added display of viewed --- torrserver_loader.lua | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/torrserver_loader.lua b/torrserver_loader.lua index 7ac3efb..5a22522 100644 --- a/torrserver_loader.lua +++ b/torrserver_loader.lua @@ -289,12 +289,26 @@ local function show_torrent_files(torrent) torrents[torrent_index] = torrent end + local viewed_list = curl(TORRSERVER .. "/viewed", '{"action":"list","hash":"' .. torrent.hash ..'"}') + if not viewed_list then viewed_list = {} end + generate_m3u_edl(torrent) local items = {} + local last_viewed for i, entry in ipairs(torrent.file_stats) do if entry.main_file then - items[i] = entry.filename + for _, entry2 in ipairs(viewed_list) do + if entry2.file_index == entry.id then + last_viewed = i + break + end + end + if i == last_viewed then + items[i] = "> " .. entry.filename + else + items[i] = " " ..entry.filename + end end end @@ -302,6 +316,7 @@ local function show_torrent_files(torrent) input.select({ prompt = "Select an entry of torrent:", items = items, + default_item = last_viewed, submit = function (index) selected = true From 4bf8e0ad793ab8361e217bdce631d0ce694b3f05 Mon Sep 17 00:00:00 2001 From: Serega007 Date: Sat, 7 Feb 2026 17:24:04 +0300 Subject: [PATCH 03/16] Refactoring --- script-opts/torrserver-loader.conf | 9 +- torrserver_loader.lua | 129 ++++++++++++++--------------- 2 files changed, 67 insertions(+), 71 deletions(-) diff --git a/script-opts/torrserver-loader.conf b/script-opts/torrserver-loader.conf index 30d32f3..24183f3 100644 --- a/script-opts/torrserver-loader.conf +++ b/script-opts/torrserver-loader.conf @@ -1,3 +1,6 @@ -TORRSERVER_SCHEME=http -TORRSERVER_HOST=127.0.0.1 -TORRSERVER_PORT=8090 +# http or https +scheme=http + +host=127.0.0.1 + +port=8090 \ No newline at end of file diff --git a/torrserver_loader.lua b/torrserver_loader.lua index 5a22522..62fb137 100644 --- a/torrserver_loader.lua +++ b/torrserver_loader.lua @@ -6,17 +6,16 @@ local utils = require "mp.utils" local input = require "mp.input" local options = { - TORRSERVER_SCHEME = "http", - TORRSERVER_HOST = "127.0.0.1", - TORRSERVER_PORT = "8090", + scheme = "http", + host = "127.0.0.1", + port = "8090" } require "mp.options".read_options(options, "torrserver-loader") -local TORRSERVER = options.TORRSERVER_SCHEME .. "://" .. options.TORRSERVER_HOST .. ":" .. options.TORRSERVER_PORT +local TORRSERVER = options.scheme .. "://" .. options.host .. ":" .. options.port -local torrents = {} -local torrent_index = 1 -local opened_btih +local torrent +local file_index = 1 local show_torrents @@ -201,7 +200,7 @@ local function extend_with_extra_fileinfo(fileinfo) end -- https://github.com/mpv-player/mpv/blob/master/DOCS/edl-mpv.rst -local function generate_m3u_edl(torrent) +local function generate_m3u_edl(torr) -- Portions of this code are derived from https://github.com/dyphire/mpv-scripts/blob/main/mpv-torrserver.lua#L123 -- Copyright (c) <2022> dyphire -- Licensed under the MIT License https://github.com/dyphire/mpv-scripts/blob/main/LICENSE @@ -209,13 +208,13 @@ local function generate_m3u_edl(torrent) local playlist = { "#EXTM3U", "# Generated by TorrServer-Loader" } local count = 0 - for _, fileinfo in ipairs(torrent.file_stats) do + for _, fileinfo in ipairs(torr.file_stats) do if not fileinfo.filename then extend_with_extra_fileinfo(fileinfo) end if not fileinfo.processed and VIDEO_EXTS[fileinfo.ext] then table.insert(playlist, '#EXTINF:0,' .. fileinfo.name) - local url = TORRSERVER .. "/stream?link=" .. torrent.hash .. "&index=" .. fileinfo.id .. "&play" + local url = TORRSERVER .. "/stream?link=" .. torr.hash .. "&index=" .. fileinfo.id .. "&play" local hdr = { "!new_stream", "!no_clip", --"!track_meta,title=" .. edlencode(basename), edlencode(url) @@ -225,21 +224,26 @@ local function generate_m3u_edl(torrent) fileinfo.processed = true fileinfo.main_file = true + if not fileinfo.ext_files then fileinfo.ext_files = {} end count = count + 1 --mp.msg.info("Attached main file: " .. fileinfo.name) - for _, fileinfo2 in ipairs(torrent.file_stats) do + for i, fileinfo2 in ipairs(torr.file_stats) do if not fileinfo2.filename then extend_with_extra_fileinfo(fileinfo2) end if not fileinfo2.processed and not VIDEO_EXTS[fileinfo2.ext] and string.find(fileinfo2.name, fileinfo.name, 1, true) then --mp.msg.info("Attached external track: " .. fileinfo2.name) - local title = format_external_filename(fileinfo.name, fileinfo2.path, torrent.name) or + fileinfo2.title = format_external_filename(fileinfo.name, fileinfo2.path, torr.name) or ("Unknown name (Index " .. fileinfo2.id .. ")") + table.insert(fileinfo.ext_files, i) + if not fileinfo2.type then + fileinfo2.type = AUDIO_EXTS[fileinfo2.ext] and "audio" or "sub" + end -- TODO: check if we can assign those values to upper url and hdr vars - local url_ext = TORRSERVER .. "/stream?link=" .. torrent.hash .. "&index=" .. fileinfo2.id .. "&play" + local url_ext = TORRSERVER .. "/stream?link=" .. torr.hash .. "&index=" .. fileinfo2.id .. "&play" local hdr_ext = { "!new_stream", "!no_clip", "!no_chapters", - "!delay_open,media_type=" .. (AUDIO_EXTS[fileinfo2.ext] and "audio" or "sub"), - "!track_meta,title=" .. edlencode(title .. " [external]"), + "!delay_open,media_type=" .. fileinfo2.type, + "!track_meta,title=" .. edlencode(fileinfo2.title .. " [external]"), edlencode(url_ext) } edl = edl .. table.concat(hdr_ext, ";") .. ";" @@ -248,55 +252,56 @@ local function generate_m3u_edl(torrent) external_tracks = external_tracks + 1 end end + fileinfo.count_ext_tracks = external_tracks if external_tracks == 0 then -- dont use edl table.insert(playlist, url) else - fileinfo.count_ext_tracks = external_tracks table.insert(playlist, edl) end end end -- if this playlist is audio only - if #torrent.file_stats > count then - for _, fileinfo in ipairs(torrent.file_stats) do + if #torr.file_stats > count then + for _, fileinfo in ipairs(torr.file_stats) do if not fileinfo.processed and AUDIO_EXTS[fileinfo.ext] then fileinfo.processed = true fileinfo.main_file = true table.insert(playlist, '#EXTINF:0,' .. fileinfo.name) - local url = TORRSERVER .. "/stream?link=" .. torrent.hash .. "&index=" .. fileinfo.id .. "&play" + local url = TORRSERVER .. "/stream?link=" .. torr.hash .. "&index=" .. fileinfo.id .. "&play" table.insert(playlist, url) end end end - torrent.playlist = table.concat(playlist, '\n') + torr.playlist = table.concat(playlist, '\n') end -local function play_from_playlist(torrent, index) +local function play_from_playlist(torrent_menu, index) + torrent = torrent_menu + file_index = index + mp.osd_message("Opening " .. (torrent.name or torrent.title) .. "...") - opened_btih = torrent.hash mp.commandv("loadlist", "memory://" .. torrent.playlist) - mp.set_property_number("playlist-pos", index - 1) + mp.set_property_number("playlist-pos-1", index) end -local function show_torrent_files(torrent) - if not torrents[torrent_index].file_stats then - torrent = curl(TORRSERVER .. "/stream?link=" .. torrents[torrent_index].hash .. "&stat") - if not torrent then return end - torrents[torrent_index] = torrent +local function show_torrent_files(torrent_menu, torrent_index) + if not torrent_menu.file_stats then + torrent_menu = curl(TORRSERVER .. "/stream?link=" .. torrent_menu.hash .. "&stat") + if not torrent_menu then return end end - local viewed_list = curl(TORRSERVER .. "/viewed", '{"action":"list","hash":"' .. torrent.hash ..'"}') + local viewed_list = curl(TORRSERVER .. "/viewed", '{"action":"list","hash":"' .. torrent_menu.hash ..'"}') if not viewed_list then viewed_list = {} end - generate_m3u_edl(torrent) + generate_m3u_edl(torrent_menu) local items = {} local last_viewed - for i, entry in ipairs(torrent.file_stats) do + for i, entry in ipairs(torrent_menu.file_stats) do if entry.main_file then for _, entry2 in ipairs(viewed_list) do if entry2.file_index == entry.id then @@ -320,7 +325,7 @@ local function show_torrent_files(torrent) submit = function (index) selected = true - play_from_playlist(torrents[torrent_index], index) + play_from_playlist(torrent_menu, index) end, closed = function () if selected then return end @@ -331,7 +336,7 @@ local function show_torrent_files(torrent) end show_torrents = function (default_item) - torrents = curl(TORRSERVER .. "/torrents", '{"action":"list"}') + local torrents = curl(TORRSERVER .. "/torrents", '{"action":"list"}') if not torrents then return end local items = {} @@ -345,8 +350,7 @@ show_torrents = function (default_item) default_item = default_item, submit = function (index) - torrent_index = index - show_torrent_files(torrents[index]) + show_torrent_files(torrents[index], index) end, }) end @@ -358,16 +362,19 @@ local LOCAL_HOSTS = { "127.0.0.1", "[::1]", "localhost" } local torrserver_is_localhost = false for _, host in ipairs(LOCAL_HOSTS) do - if options.TORRSERVER_HOST == host then + if options.host == host then torrserver_is_localhost = true break end end local function is_torrserver(path) + if not path then + path = mp.get_property("path", "") + end if torrserver_is_localhost then for _, host in ipairs(LOCAL_HOSTS) do - if path:find(options.TORRSERVER_SCHEME .. "://" .. host .. ":" .. options.TORRSERVER_PORT, 1, true) then + if path:find(options.scheme .. "://" .. host .. ":" .. options.port, 1, true) then return true end end @@ -381,67 +388,55 @@ end local function load_external_assets() local path = mp.get_property("path", "") if not is_torrserver(path) then + torrent = nil return end + + file_index = mp.get_property_number("playlist-pos-1", 1) + if mp.get_property("playlist-path", ""):find("# Generated by TorrServer-Loader", 1, true) then return end local btih = path:match("%link=(" .. string.rep(".", 40) .. ")") if not btih then + torrent = nil mp.osd_message("Invalid BTIH extracted from path!", 7) return end - if opened_btih and opened_btih == btih and not mp.get_property("media-title", ""):find(btih, 1, true) then - return - end - local torrent, torr_i - for i, t in ipairs(torrents) do - if t.hash == btih then - torrent = t - torr_i = i - break - end + if torrent and torrent.hash ~= btih then + torrent = nil end + if not torrent or not torrent.file_stats then torrent = curl(TORRSERVER .. "/stream?link=" .. btih .. "&stat") if not torrent then return end - if torr_i then - torrents[torr_i] = torrent - else - table.insert(torrents, torrent) - end end - local memory_link = torrent.playlist - if not memory_link then + if not torrent.playlist then generate_m3u_edl(torrent) - memory_link = torrent.playlist end - --mp.msg.info(memory_link) -- finding pos of playlist - local count = 0 - local play_index = 1 + local play_index = 0 local found_pos = false - local file_index = tonumber(path:match("index=(%d+)")) + local f_index = tonumber(path:match("index=(%d+)")) for _, fileinfo in ipairs(torrent.file_stats) do if fileinfo.main_file then - if fileinfo.id == file_index then - play_index = count + play_index = play_index + 1 + if fileinfo.id == f_index then found_pos = true break end - count = count + 1 end end if not found_pos then + play_index = 1 mp.msg.warn("Couldn't find playlist position") end - opened_btih = btih - mp.commandv("loadlist", "memory://" .. memory_link) - mp.set_property_number("playlist-pos", play_index) + mp.commandv("loadlist", "memory://" .. torrent.playlist) + mp.set_property_number("playlist-pos-1", play_index) end mp.add_hook("on_load", 5, load_external_assets) @@ -449,9 +444,7 @@ mp.add_hook("on_load", 5, load_external_assets) -- By default, edl inserts a chapter with a link in the name, we fix this by deleting it. -- https://github.com/mpv-player/mpv/blob/master/DOCS/edl-mpv.rst#implicit-chapters local function fix_edl_chapters() - if not is_torrserver(mp.get_property("path", "")) then - return - end + if not torrent then return end local chapters = mp.get_property_native("chapter-list") if not chapters then return end From 1712bb38ec932d809702a8f1df0ca016ccf05358 Mon Sep 17 00:00:00 2001 From: Serega007 Date: Sat, 7 Feb 2026 17:39:59 +0300 Subject: [PATCH 04/16] Added display of status of loading --- torrserver_loader.lua | 88 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 84 insertions(+), 4 deletions(-) diff --git a/torrserver_loader.lua b/torrserver_loader.lua index 62fb137..16790c3 100644 --- a/torrserver_loader.lua +++ b/torrserver_loader.lua @@ -92,8 +92,10 @@ local AUDIO_EXTS = { mka = true } -local function curl(url, data) - mp.osd_message("Requesting TorrServer's API...", 99) +local function curl(url, data, dont_show_osd) + if not dont_show_osd then + mp.osd_message("Requesting TorrServer's API...", 99) + end local args if data then args = { @@ -130,10 +132,78 @@ local function curl(url, data) response = utils.parse_json(response) - mp.osd_message("") + if not dont_show_osd then + mp.osd_message("") + end + return response end +local function is_buffering(cache_time) + if not cache_time then + cache_time = mp.get_property_number("demuxer-cache-duration", 0) + end + if cache_time < 1 then + return true + end + return false +end + +local function humanize_speed(speed) + if not speed or speed == 0 then return "0 bps" end + + local bps = speed * 8 + local units = {"bps", "kbps", "Mbps", "Gbps", "Tbps"} + local i = math.floor(math.log(bps) / math.log(1000)) + if i > #units then i = #units end + + local value = bps / (1000 ^ i) + return string.format("%.2f %s", value, units[i+1]) +end + +local function bytes_to_gb(bytes) + return string.format("%.2f", bytes / (1024^3)) +end + +local function show_torrent_load_info(torr, file_ind) + local download_speed = humanize_speed(torr.download_speed or 0) + + local preloaded_gb = bytes_to_gb(torr.loaded_size or 0) + local total_gb = bytes_to_gb(torr.capacity or 0) + + local peers_info = string.format("%d / %d · %d", torr.active_peers or 0, torr.total_peers or 0, torr.connected_seeders or 0) + + local message = string.format( + "Caching...\n\nTorrent load info:\nSpeed: %s\nCache: %s / %s GB\nPeers·Seeders: %s", + download_speed, preloaded_gb, total_gb, peers_info + ) + mp.osd_message(message) +end + +local update_timer +local function timer_torrent_load_info() + if not torrent or not is_buffering() then + if update_timer then + update_timer:kill() + update_timer = nil + end + return + elseif not update_timer then + update_timer = mp.add_periodic_timer(0.5, timer_torrent_load_info) + end + local cache = curl(TORRSERVER .. "/cache", '{"action":"get","hash":"' .. torrent.hash .. '"}', true) + if not cache then return end + torrent.capacity = cache.Capacity + if cache.Torrent then + torrent.download_speed = cache.Torrent.download_speed + torrent.loaded_size = cache.Torrent.loaded_size + torrent.active_peers = cache.Torrent.active_peers + torrent.total_peers = cache.Torrent.total_peers + torrent.connected_seeders = cache.Torrent.connected_seeders + end + show_torrent_load_info(torrent, file_index) +end + -- external name local function replace(s, needle) local i, j = s:find(needle, 1, true) @@ -282,7 +352,7 @@ local function play_from_playlist(torrent_menu, index) torrent = torrent_menu file_index = index - mp.osd_message("Opening " .. (torrent.name or torrent.title) .. "...") + show_torrent_load_info(torrent, file_index) mp.commandv("loadlist", "memory://" .. torrent.playlist) mp.set_property_number("playlist-pos-1", index) @@ -395,6 +465,7 @@ local function load_external_assets() file_index = mp.get_property_number("playlist-pos-1", 1) if mp.get_property("playlist-path", ""):find("# Generated by TorrServer-Loader", 1, true) then + timer_torrent_load_info() return end local btih = path:match("%link=(" .. string.rep(".", 40) .. ")") @@ -464,3 +535,12 @@ local function fix_edl_chapters() end mp.add_hook("on_preloaded", 5, fix_edl_chapters) + +mp.observe_property("demuxer-cache-duration", "number", function(_, value) + if not value then return end + if update_timer or not torrent or not is_buffering(value) then + return + end + + timer_torrent_load_info() +end) From 930b61277fd781ae516af7faf0d05078421a98f2 Mon Sep 17 00:00:00 2001 From: Serega007 Date: Sat, 7 Feb 2026 17:41:25 +0300 Subject: [PATCH 05/16] Added the option not to use edl added the ability to return the old method of connecting external files via audio-add and sub-add. --- torrserver_loader.lua | 86 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 72 insertions(+), 14 deletions(-) diff --git a/torrserver_loader.lua b/torrserver_loader.lua index 16790c3..5162f1b 100644 --- a/torrserver_loader.lua +++ b/torrserver_loader.lua @@ -8,7 +8,8 @@ local input = require "mp.input" local options = { scheme = "http", host = "127.0.0.1", - port = "8090" + port = "8090", + use_edl = true } require "mp.options".read_options(options, "torrserver-loader") @@ -16,6 +17,7 @@ local TORRSERVER = options.scheme .. "://" .. options.host .. ":" .. options.por local torrent local file_index = 1 +local loadings = {} local show_torrents @@ -145,6 +147,8 @@ local function is_buffering(cache_time) end if cache_time < 1 then return true + elseif not options.use_edl and next(loadings) then + return true end return false end @@ -173,11 +177,23 @@ local function show_torrent_load_info(torr, file_ind) local peers_info = string.format("%d / %d · %d", torr.active_peers or 0, torr.total_peers or 0, torr.connected_seeders or 0) + local ext_files + local filename = torr.file_stats[file_ind] + local timeout + if options.use_edl then + ext_files = "Connected " .. filename.count_ext_tracks .. " external files" + else + ext_files = string.format("Connected %d / %d external files, Errors: %d", filename.loaded_ext_files or 0, filename.count_ext_tracks, filename.error_ext_files or 0) + if next(loadings) then + timeout = 99 + end + end + local message = string.format( - "Caching...\n\nTorrent load info:\nSpeed: %s\nCache: %s / %s GB\nPeers·Seeders: %s", - download_speed, preloaded_gb, total_gb, peers_info + "Caching...\n\nTorrent load info:\nSpeed: %s\nCache: %s / %s GB\nPeers·Seeders: %s\n\n%s", + download_speed, preloaded_gb, total_gb, peers_info, ext_files ) - mp.osd_message(message) + mp.osd_message(message, timeout) end local update_timer @@ -308,15 +324,17 @@ local function generate_m3u_edl(torr) if not fileinfo2.type then fileinfo2.type = AUDIO_EXTS[fileinfo2.ext] and "audio" or "sub" end - -- TODO: check if we can assign those values to upper url and hdr vars - local url_ext = TORRSERVER .. "/stream?link=" .. torr.hash .. "&index=" .. fileinfo2.id .. "&play" - local hdr_ext = { - "!new_stream", "!no_clip", "!no_chapters", - "!delay_open,media_type=" .. fileinfo2.type, - "!track_meta,title=" .. edlencode(fileinfo2.title .. " [external]"), - edlencode(url_ext) - } - edl = edl .. table.concat(hdr_ext, ";") .. ";" + if options.use_edl then + -- TODO: check if we can assign those values to upper url and hdr vars + local url_ext = TORRSERVER .. "/stream?link=" .. torr.hash .. "&index=" .. fileinfo2.id .. "&play" + local hdr_ext = { + "!new_stream", "!no_clip", "!no_chapters", + "!delay_open,media_type=" .. fileinfo2.type, + "!track_meta,title=" .. edlencode(fileinfo2.title .. " [external]"), + edlencode(url_ext) + } + edl = edl .. table.concat(hdr_ext, ";") .. ";" + end fileinfo2.processed = true count = count + 1 external_tracks = external_tracks + 1 @@ -324,7 +342,7 @@ local function generate_m3u_edl(torr) end fileinfo.count_ext_tracks = external_tracks - if external_tracks == 0 then -- dont use edl + if not options.use_edl or external_tracks == 0 then -- dont use edl table.insert(playlist, url) else table.insert(playlist, edl) @@ -455,7 +473,46 @@ local function is_torrserver(path) end +local function connect_external_assets() + if not torrent then return end + + if options.use_edl then return end + + local main_fileinfo = torrent.file_stats[file_index] + main_fileinfo.loaded_ext_files = 0 + main_fileinfo.error_ext_files = 0 + for _, i_ext in ipairs(main_fileinfo.ext_files) do + local ext_fileinfo = torrent.file_stats[i_ext] + local url_ext = TORRSERVER .. "/stream?link=" .. torrent.hash .. "&index=" .. ext_fileinfo.id .. "&play" + local cmd = ext_fileinfo.type == 'audio' and "audio-add" or "sub-add" + + local request_id + request_id = mp.command_native_async({ cmd, url_ext, "auto", ext_fileinfo.title }, function(success) + loadings[request_id] = nil + + if success then + main_fileinfo.loaded_ext_files = main_fileinfo.loaded_ext_files + 1 + else + main_fileinfo.error_ext_files = main_fileinfo.error_ext_files + 1 + end + + show_torrent_load_info(torrent, file_index) + end) + + loadings[request_id] = true + end +end + +local function abort_loadings() + for request_id in pairs(loadings) do + mp.abort_async_command(request_id) + end + loadings = {} +end + local function load_external_assets() + abort_loadings() + local path = mp.get_property("path", "") if not is_torrserver(path) then torrent = nil @@ -465,6 +522,7 @@ local function load_external_assets() file_index = mp.get_property_number("playlist-pos-1", 1) if mp.get_property("playlist-path", ""):find("# Generated by TorrServer-Loader", 1, true) then + connect_external_assets() timer_torrent_load_info() return end From 425ef939f699aeebe0471180a3538cc6cf870a90 Mon Sep 17 00:00:00 2001 From: Serega007 Date: Sat, 7 Feb 2026 18:02:05 +0300 Subject: [PATCH 06/16] Added display of current state loading --- torrserver_loader.lua | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/torrserver_loader.lua b/torrserver_loader.lua index 5162f1b..743eaf2 100644 --- a/torrserver_loader.lua +++ b/torrserver_loader.lua @@ -170,6 +170,14 @@ local function bytes_to_gb(bytes) end local function show_torrent_load_info(torr, file_ind) + local dcd = mp.get_property_number("demuxer-cache-duration", nil) + local current_state_loading = '' + if not dcd then + current_state_loading = 'Opening' + elseif dcd < 1.0 then + current_state_loading = 'Caching' + end + local download_speed = humanize_speed(torr.download_speed or 0) local preloaded_gb = bytes_to_gb(torr.loaded_size or 0) @@ -186,12 +194,17 @@ local function show_torrent_load_info(torr, file_ind) ext_files = string.format("Connected %d / %d external files, Errors: %d", filename.loaded_ext_files or 0, filename.count_ext_tracks, filename.error_ext_files or 0) if next(loadings) then timeout = 99 + if #current_state_loading > 0 then + current_state_loading = current_state_loading .. ' / Connecting external files' + else + current_state_loading = 'Connecting external files' + end end end local message = string.format( - "Caching...\n\nTorrent load info:\nSpeed: %s\nCache: %s / %s GB\nPeers·Seeders: %s\n\n%s", - download_speed, preloaded_gb, total_gb, peers_info, ext_files + "Loading %s...\nCurrent state: %s\n\nTorrent load info:\nSpeed: %s\nCache: %s / %s GB\nPeers·Seeders: %s\n\n%s", + filename.filename, current_state_loading, download_speed, preloaded_gb, total_gb, peers_info, ext_files ) mp.osd_message(message, timeout) end From 40d72a5b02e046d0178b068f8939e32e07e59d06 Mon Sep 17 00:00:00 2001 From: Serega007 Date: Sat, 7 Feb 2026 18:20:37 +0300 Subject: [PATCH 07/16] forgot "use_edl" option in conf file --- script-opts/torrserver-loader.conf | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/script-opts/torrserver-loader.conf b/script-opts/torrserver-loader.conf index 24183f3..2b9d567 100644 --- a/script-opts/torrserver-loader.conf +++ b/script-opts/torrserver-loader.conf @@ -3,4 +3,11 @@ scheme=http host=127.0.0.1 -port=8090 \ No newline at end of file +port=8090 + +# method of connect external files +# if edl is enabled, external files are connected quickly, +# but without pre-caching the meta data of external files, the bitrate and language will not be visible. +# if edl is disabled, external files will connect slowly +# and the meta data of external files will be preloaded. +use_edl=yes \ No newline at end of file From 29b99a6606536cca68c1054d0c8fa1f0ecc52311 Mon Sep 17 00:00:00 2001 From: pursvir Date: Sun, 8 Feb 2026 18:16:26 +0300 Subject: [PATCH 08/16] Add timeout before input.select to prevent race condition --- torrserver_loader.lua | 46 +++++++++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/torrserver_loader.lua b/torrserver_loader.lua index 743eaf2..b8c4fc4 100644 --- a/torrserver_loader.lua +++ b/torrserver_loader.lua @@ -157,16 +157,16 @@ local function humanize_speed(speed) if not speed or speed == 0 then return "0 bps" end local bps = speed * 8 - local units = {"bps", "kbps", "Mbps", "Gbps", "Tbps"} + local units = { "bps", "kbps", "Mbps", "Gbps", "Tbps" } local i = math.floor(math.log(bps) / math.log(1000)) if i > #units then i = #units end local value = bps / (1000 ^ i) - return string.format("%.2f %s", value, units[i+1]) + return string.format("%.2f %s", value, units[i + 1]) end local function bytes_to_gb(bytes) - return string.format("%.2f", bytes / (1024^3)) + return string.format("%.2f", bytes / (1024 ^ 3)) end local function show_torrent_load_info(torr, file_ind) @@ -183,7 +183,8 @@ local function show_torrent_load_info(torr, file_ind) local preloaded_gb = bytes_to_gb(torr.loaded_size or 0) local total_gb = bytes_to_gb(torr.capacity or 0) - local peers_info = string.format("%d / %d · %d", torr.active_peers or 0, torr.total_peers or 0, torr.connected_seeders or 0) + local peers_info = string.format("%d / %d · %d", torr.active_peers or 0, torr.total_peers or 0, + torr.connected_seeders or 0) local ext_files local filename = torr.file_stats[file_ind] @@ -191,7 +192,8 @@ local function show_torrent_load_info(torr, file_ind) if options.use_edl then ext_files = "Connected " .. filename.count_ext_tracks .. " external files" else - ext_files = string.format("Connected %d / %d external files, Errors: %d", filename.loaded_ext_files or 0, filename.count_ext_tracks, filename.error_ext_files or 0) + ext_files = string.format("Connected %d / %d external files, Errors: %d", filename.loaded_ext_files or 0, + filename.count_ext_tracks, filename.error_ext_files or 0) if next(loadings) then timeout = 99 if #current_state_loading > 0 then @@ -203,8 +205,8 @@ local function show_torrent_load_info(torr, file_ind) end local message = string.format( - "Loading %s...\nCurrent state: %s\n\nTorrent load info:\nSpeed: %s\nCache: %s / %s GB\nPeers·Seeders: %s\n\n%s", - filename.filename, current_state_loading, download_speed, preloaded_gb, total_gb, peers_info, ext_files + "Loading %s...\nCurrent state: %s\n\nTorrent load info:\nSpeed: %s\nCache: %s / %s GB\nPeers·Seeders: %s\n\n%s", + filename.filename, current_state_loading, download_speed, preloaded_gb, total_gb, peers_info, ext_files ) mp.osd_message(message, timeout) end @@ -332,14 +334,15 @@ local function generate_m3u_edl(torr) if not fileinfo2.processed and not VIDEO_EXTS[fileinfo2.ext] and string.find(fileinfo2.name, fileinfo.name, 1, true) then --mp.msg.info("Attached external track: " .. fileinfo2.name) fileinfo2.title = format_external_filename(fileinfo.name, fileinfo2.path, torr.name) or - ("Unknown name (Index " .. fileinfo2.id .. ")") + ("Unknown name (Index " .. fileinfo2.id .. ")") table.insert(fileinfo.ext_files, i) if not fileinfo2.type then fileinfo2.type = AUDIO_EXTS[fileinfo2.ext] and "audio" or "sub" end if options.use_edl then -- TODO: check if we can assign those values to upper url and hdr vars - local url_ext = TORRSERVER .. "/stream?link=" .. torr.hash .. "&index=" .. fileinfo2.id .. "&play" + local url_ext = TORRSERVER .. + "/stream?link=" .. torr.hash .. "&index=" .. fileinfo2.id .. "&play" local hdr_ext = { "!new_stream", "!no_clip", "!no_chapters", "!delay_open,media_type=" .. fileinfo2.type, @@ -389,13 +392,22 @@ local function play_from_playlist(torrent_menu, index) mp.set_property_number("playlist-pos-1", index) end +-- temporary solution to the problem +-- https://github.com/mpv-player/mpv/pull/17256 +-- TODO: remove this after a stable release of MPV with a fix appears +local function input_select(args) + mp.add_timeout(0.1, function() + input.select(args) + end) +end + local function show_torrent_files(torrent_menu, torrent_index) if not torrent_menu.file_stats then torrent_menu = curl(TORRSERVER .. "/stream?link=" .. torrent_menu.hash .. "&stat") if not torrent_menu then return end end - local viewed_list = curl(TORRSERVER .. "/viewed", '{"action":"list","hash":"' .. torrent_menu.hash ..'"}') + local viewed_list = curl(TORRSERVER .. "/viewed", '{"action":"list","hash":"' .. torrent_menu.hash .. '"}') if not viewed_list then viewed_list = {} end generate_m3u_edl(torrent_menu) @@ -413,22 +425,22 @@ local function show_torrent_files(torrent_menu, torrent_index) if i == last_viewed then items[i] = "> " .. entry.filename else - items[i] = " " ..entry.filename + items[i] = " " .. entry.filename end end end local selected = false - input.select({ + input_select({ prompt = "Select an entry of torrent:", items = items, default_item = last_viewed, - submit = function (index) + submit = function(index) selected = true play_from_playlist(torrent_menu, index) end, - closed = function () + closed = function() if selected then return end show_torrents(torrent_index) @@ -436,7 +448,7 @@ local function show_torrent_files(torrent_menu, torrent_index) }) end -show_torrents = function (default_item) +show_torrents = function(default_item) local torrents = curl(TORRSERVER .. "/torrents", '{"action":"list"}') if not torrents then return end @@ -445,12 +457,12 @@ show_torrents = function (default_item) items[i] = entry.name or entry.title end - input.select({ + input_select({ prompt = "Select a torrent:", items = items, default_item = default_item, - submit = function (index) + submit = function(index) show_torrent_files(torrents[index], index) end, }) From acc1d4b886373d5c2b499169aa1a392e0a9d63fe Mon Sep 17 00:00:00 2001 From: Serega007 Date: Mon, 9 Feb 2026 01:56:48 +0300 Subject: [PATCH 09/16] fix error if file index is not in order --- torrserver_loader.lua | 70 +++++++++++++++++++++---------------------- 1 file changed, 34 insertions(+), 36 deletions(-) diff --git a/torrserver_loader.lua b/torrserver_loader.lua index b8c4fc4..a7a711d 100644 --- a/torrserver_loader.lua +++ b/torrserver_loader.lua @@ -16,7 +16,7 @@ require "mp.options".read_options(options, "torrserver-loader") local TORRSERVER = options.scheme .. "://" .. options.host .. ":" .. options.port local torrent -local file_index = 1 +local playing_pos = 1 local loadings = {} local show_torrents @@ -169,7 +169,7 @@ local function bytes_to_gb(bytes) return string.format("%.2f", bytes / (1024 ^ 3)) end -local function show_torrent_load_info(torr, file_ind) +local function show_torrent_load_info(torr, p_pos) local dcd = mp.get_property_number("demuxer-cache-duration", nil) local current_state_loading = '' if not dcd then @@ -187,7 +187,7 @@ local function show_torrent_load_info(torr, file_ind) torr.connected_seeders or 0) local ext_files - local filename = torr.file_stats[file_ind] + local filename = torr.file_stats[torr.main_files[p_pos]] local timeout if options.use_edl then ext_files = "Connected " .. filename.count_ext_tracks .. " external files" @@ -232,7 +232,7 @@ local function timer_torrent_load_info() torrent.total_peers = cache.Torrent.total_peers torrent.connected_seeders = cache.Torrent.connected_seeders end - show_torrent_load_info(torrent, file_index) + show_torrent_load_info(torrent, playing_pos) end -- external name @@ -308,8 +308,9 @@ local function generate_m3u_edl(torr) local playlist = { "#EXTM3U", "# Generated by TorrServer-Loader" } local count = 0 + torr.main_files = {} - for _, fileinfo in ipairs(torr.file_stats) do + for i, fileinfo in ipairs(torr.file_stats) do if not fileinfo.filename then extend_with_extra_fileinfo(fileinfo) end if not fileinfo.processed and VIDEO_EXTS[fileinfo.ext] then @@ -324,18 +325,18 @@ local function generate_m3u_edl(torr) local external_tracks = 0 fileinfo.processed = true - fileinfo.main_file = true + table.insert(torr.main_files, i) if not fileinfo.ext_files then fileinfo.ext_files = {} end count = count + 1 --mp.msg.info("Attached main file: " .. fileinfo.name) - for i, fileinfo2 in ipairs(torr.file_stats) do + for i2, fileinfo2 in ipairs(torr.file_stats) do if not fileinfo2.filename then extend_with_extra_fileinfo(fileinfo2) end if not fileinfo2.processed and not VIDEO_EXTS[fileinfo2.ext] and string.find(fileinfo2.name, fileinfo.name, 1, true) then --mp.msg.info("Attached external track: " .. fileinfo2.name) fileinfo2.title = format_external_filename(fileinfo.name, fileinfo2.path, torr.name) or ("Unknown name (Index " .. fileinfo2.id .. ")") - table.insert(fileinfo.ext_files, i) + table.insert(fileinfo.ext_files, i2) if not fileinfo2.type then fileinfo2.type = AUDIO_EXTS[fileinfo2.ext] and "audio" or "sub" end @@ -368,10 +369,10 @@ local function generate_m3u_edl(torr) -- if this playlist is audio only if #torr.file_stats > count then - for _, fileinfo in ipairs(torr.file_stats) do + for i, fileinfo in ipairs(torr.file_stats) do if not fileinfo.processed and AUDIO_EXTS[fileinfo.ext] then fileinfo.processed = true - fileinfo.main_file = true + table.insert(torr.main_files, i) table.insert(playlist, '#EXTINF:0,' .. fileinfo.name) local url = TORRSERVER .. "/stream?link=" .. torr.hash .. "&index=" .. fileinfo.id .. "&play" table.insert(playlist, url) @@ -384,9 +385,9 @@ end local function play_from_playlist(torrent_menu, index) torrent = torrent_menu - file_index = index + playing_pos = index - show_torrent_load_info(torrent, file_index) + show_torrent_load_info(torrent, playing_pos) mp.commandv("loadlist", "memory://" .. torrent.playlist) mp.set_property_number("playlist-pos-1", index) @@ -414,20 +415,19 @@ local function show_torrent_files(torrent_menu, torrent_index) local items = {} local last_viewed - for i, entry in ipairs(torrent_menu.file_stats) do - if entry.main_file then - for _, entry2 in ipairs(viewed_list) do - if entry2.file_index == entry.id then - last_viewed = i - break - end - end - if i == last_viewed then - items[i] = "> " .. entry.filename - else - items[i] = " " .. entry.filename + for i, entry in ipairs(torrent_menu.main_files) do + local filename = torrent_menu.file_stats[entry] + for _, viewed in ipairs(viewed_list) do + if viewed.file_index == filename.id then + last_viewed = i + break end end + if i == last_viewed then + table.insert(items, "> " .. filename.filename) + else + table.insert(items, " " .. filename.filename) + end end local selected = false @@ -503,7 +503,7 @@ local function connect_external_assets() if options.use_edl then return end - local main_fileinfo = torrent.file_stats[file_index] + local main_fileinfo = torrent.file_stats[torrent.main_files[playing_pos]] main_fileinfo.loaded_ext_files = 0 main_fileinfo.error_ext_files = 0 for _, i_ext in ipairs(main_fileinfo.ext_files) do @@ -521,7 +521,7 @@ local function connect_external_assets() main_fileinfo.error_ext_files = main_fileinfo.error_ext_files + 1 end - show_torrent_load_info(torrent, file_index) + show_torrent_load_info(torrent, playing_pos) end) loadings[request_id] = true @@ -544,7 +544,7 @@ local function load_external_assets() return end - file_index = mp.get_property_number("playlist-pos-1", 1) + playing_pos = mp.get_property_number("playlist-pos-1", 1) if mp.get_property("playlist-path", ""):find("# Generated by TorrServer-Loader", 1, true) then connect_external_assets() @@ -572,20 +572,18 @@ local function load_external_assets() end -- finding pos of playlist - local play_index = 0 + local play_index = 1 local found_pos = false local f_index = tonumber(path:match("index=(%d+)")) - for _, fileinfo in ipairs(torrent.file_stats) do - if fileinfo.main_file then - play_index = play_index + 1 - if fileinfo.id == f_index then - found_pos = true - break - end + for i, entry in ipairs(torrent.main_files) do + local fileinfo = torrent.file_stats[entry] + if fileinfo.id == f_index then + found_pos = true + play_index = i + break end end if not found_pos then - play_index = 1 mp.msg.warn("Couldn't find playlist position") end From b77e354cda8f6b028711604f62b1fb4fa9fed558 Mon Sep 17 00:00:00 2001 From: pursvir Date: Mon, 9 Feb 2026 11:34:08 +0300 Subject: [PATCH 10/16] Variable naming clarifications, use 'x' as a 'watched' marker, observe cache only if watching content from TorrServer --- torrserver_loader.lua | 132 +++++++++++++++++++++--------------------- 1 file changed, 66 insertions(+), 66 deletions(-) diff --git a/torrserver_loader.lua b/torrserver_loader.lua index a7a711d..c816e42 100644 --- a/torrserver_loader.lua +++ b/torrserver_loader.lua @@ -145,12 +145,7 @@ local function is_buffering(cache_time) if not cache_time then cache_time = mp.get_property_number("demuxer-cache-duration", 0) end - if cache_time < 1 then - return true - elseif not options.use_edl and next(loadings) then - return true - end - return false + return cache_time < 1 or (not options.use_edl and next(loadings)) end local function humanize_speed(speed) @@ -165,7 +160,7 @@ local function humanize_speed(speed) return string.format("%.2f %s", value, units[i + 1]) end -local function bytes_to_gb(bytes) +local function convert_bytes_to_gb(bytes) return string.format("%.2f", bytes / (1024 ^ 3)) end @@ -180,8 +175,8 @@ local function show_torrent_load_info(torr, p_pos) local download_speed = humanize_speed(torr.download_speed or 0) - local preloaded_gb = bytes_to_gb(torr.loaded_size or 0) - local total_gb = bytes_to_gb(torr.capacity or 0) + local preloaded_gb = convert_bytes_to_gb(torr.loaded_size or 0) + local total_gb = convert_bytes_to_gb(torr.capacity or 0) local peers_info = string.format("%d / %d · %d", torr.active_peers or 0, torr.total_peers or 0, torr.connected_seeders or 0) @@ -190,10 +185,10 @@ local function show_torrent_load_info(torr, p_pos) local filename = torr.file_stats[torr.main_files[p_pos]] local timeout if options.use_edl then - ext_files = "Connected " .. filename.count_ext_tracks .. " external files" + ext_files = "Connected " .. filename.ext_tracks_count .. " external files" else ext_files = string.format("Connected %d / %d external files, Errors: %d", filename.loaded_ext_files or 0, - filename.count_ext_tracks, filename.error_ext_files or 0) + filename.ext_tracks_count, filename.error_ext_files or 0) if next(loadings) then timeout = 99 if #current_state_loading > 0 then @@ -212,7 +207,8 @@ local function show_torrent_load_info(torr, p_pos) end local update_timer -local function timer_torrent_load_info() +local TIMER_TIMEOUT = 0.5 +local function init_torrent_loading_timer() if not torrent or not is_buffering() then if update_timer then update_timer:kill() @@ -220,7 +216,7 @@ local function timer_torrent_load_info() end return elseif not update_timer then - update_timer = mp.add_periodic_timer(0.5, timer_torrent_load_info) + update_timer = mp.add_periodic_timer(TIMER_TIMEOUT, init_torrent_loading_timer) end local cache = curl(TORRSERVER .. "/cache", '{"action":"get","hash":"' .. torrent.hash .. '"}', true) if not cache then return end @@ -255,9 +251,9 @@ local function remove_duplicate_words(str) local seen = {} local result = {} - -- a function for clearing words from quotation marks/brackets and reducing them to lowercase + -- A function for clearing words from quotation marks/brackets and reducing them to lowercase local function clean_word(word) - -- we remove all [], (), "" and spaces inside, and reduce them to lowercase + -- We remove all [], (), "" and spaces inside, and reduce them to lowercase word = word:gsub("[%[%]()\"]", ""):gsub("%s+", "") return word:lower() end @@ -266,7 +262,7 @@ local function remove_duplicate_words(str) local cleaned = clean_word(word) if cleaned and not seen[cleaned] then seen[cleaned] = true - table.insert(result, word) -- inserting the original word with quotes/brackets + table.insert(result, word) -- Inserting the original word with quotes/brackets end end @@ -274,13 +270,13 @@ local function remove_duplicate_words(str) end local function format_external_filename(basename, filename_path, torrent_name) - -- removing the file extension + -- Removing the file extension local name = filename_path:match("^(.*)%.%w+$") - -- removing the base file name + -- Removing the base file name name = replace(name, basename) - -- deleting the torrent name (usually the name of the root folder) + -- Deleting the torrent name (usually the name of the root folder) name = replace(name, torrent_name) - -- removing the dots "." and slashes "/" + -- Removing the dots "." and slashes "/" name = name:gsub("[./]", " ") name = remove_extra_spaces(name) name = remove_duplicate_words(name) @@ -301,7 +297,7 @@ local function extend_with_extra_fileinfo(fileinfo) end -- https://github.com/mpv-player/mpv/blob/master/DOCS/edl-mpv.rst -local function generate_m3u_edl(torr) +local function generate_m3u(torr) -- Portions of this code are derived from https://github.com/dyphire/mpv-scripts/blob/main/mpv-torrserver.lua#L123 -- Copyright (c) <2022> dyphire -- Licensed under the MIT License https://github.com/dyphire/mpv-scripts/blob/main/LICENSE @@ -310,15 +306,17 @@ local function generate_m3u_edl(torr) local count = 0 torr.main_files = {} + local url + local hdr + for i, fileinfo in ipairs(torr.file_stats) do if not fileinfo.filename then extend_with_extra_fileinfo(fileinfo) end if not fileinfo.processed and VIDEO_EXTS[fileinfo.ext] then table.insert(playlist, '#EXTINF:0,' .. fileinfo.name) - local url = TORRSERVER .. "/stream?link=" .. torr.hash .. "&index=" .. fileinfo.id .. "&play" - local hdr = { "!new_stream", "!no_clip", - --"!track_meta,title=" .. edlencode(basename), + url = TORRSERVER .. "/stream?link=" .. torr.hash .. "&index=" .. fileinfo.id .. "&play" + hdr = { "!new_stream", "!no_clip", edlencode(url) } local edl = "edl://" .. table.concat(hdr, ";") .. ";" @@ -329,37 +327,38 @@ local function generate_m3u_edl(torr) if not fileinfo.ext_files then fileinfo.ext_files = {} end count = count + 1 --mp.msg.info("Attached main file: " .. fileinfo.name) - for i2, fileinfo2 in ipairs(torr.file_stats) do - if not fileinfo2.filename then extend_with_extra_fileinfo(fileinfo2) end + for j, extra_fileinfo in ipairs(torr.file_stats) do + if not extra_fileinfo.filename then extend_with_extra_fileinfo(extra_fileinfo) end - if not fileinfo2.processed and not VIDEO_EXTS[fileinfo2.ext] and string.find(fileinfo2.name, fileinfo.name, 1, true) then + if not extra_fileinfo.processed + and not VIDEO_EXTS[extra_fileinfo.ext] + and string.find(extra_fileinfo.name, fileinfo.name, 1, true) then --mp.msg.info("Attached external track: " .. fileinfo2.name) - fileinfo2.title = format_external_filename(fileinfo.name, fileinfo2.path, torr.name) or - ("Unknown name (Index " .. fileinfo2.id .. ")") - table.insert(fileinfo.ext_files, i2) - if not fileinfo2.type then - fileinfo2.type = AUDIO_EXTS[fileinfo2.ext] and "audio" or "sub" + extra_fileinfo.title = format_external_filename(fileinfo.name, extra_fileinfo.path, torr.name) or + ("Unknown name (Index " .. extra_fileinfo.id .. ")") + table.insert(fileinfo.ext_files, j) + if not extra_fileinfo.type then + extra_fileinfo.type = AUDIO_EXTS[extra_fileinfo.ext] and "audio" or "sub" end if options.use_edl then - -- TODO: check if we can assign those values to upper url and hdr vars - local url_ext = TORRSERVER .. - "/stream?link=" .. torr.hash .. "&index=" .. fileinfo2.id .. "&play" - local hdr_ext = { + url = TORRSERVER .. + "/stream?link=" .. torr.hash .. "&index=" .. extra_fileinfo.id .. "&play" + hdr = { "!new_stream", "!no_clip", "!no_chapters", - "!delay_open,media_type=" .. fileinfo2.type, - "!track_meta,title=" .. edlencode(fileinfo2.title .. " [external]"), - edlencode(url_ext) + "!delay_open,media_type=" .. extra_fileinfo.type, + "!track_meta,title=" .. edlencode(extra_fileinfo.title .. " [external]"), + edlencode(url) } - edl = edl .. table.concat(hdr_ext, ";") .. ";" + edl = edl .. table.concat(hdr, ";") .. ";" end - fileinfo2.processed = true + extra_fileinfo.processed = true count = count + 1 external_tracks = external_tracks + 1 end end - fileinfo.count_ext_tracks = external_tracks + fileinfo.ext_tracks_count = external_tracks - if not options.use_edl or external_tracks == 0 then -- dont use edl + if not options.use_edl or external_tracks == 0 then table.insert(playlist, url) else table.insert(playlist, edl) @@ -367,14 +366,14 @@ local function generate_m3u_edl(torr) end end - -- if this playlist is audio only + -- If this playlist is audio only if #torr.file_stats > count then for i, fileinfo in ipairs(torr.file_stats) do if not fileinfo.processed and AUDIO_EXTS[fileinfo.ext] then fileinfo.processed = true table.insert(torr.main_files, i) table.insert(playlist, '#EXTINF:0,' .. fileinfo.name) - local url = TORRSERVER .. "/stream?link=" .. torr.hash .. "&index=" .. fileinfo.id .. "&play" + url = TORRSERVER .. "/stream?link=" .. torr.hash .. "&index=" .. fileinfo.id .. "&play" table.insert(playlist, url) end end @@ -393,9 +392,8 @@ local function play_from_playlist(torrent_menu, index) mp.set_property_number("playlist-pos-1", index) end --- temporary solution to the problem --- https://github.com/mpv-player/mpv/pull/17256 --- TODO: remove this after a stable release of MPV with a fix appears +-- A temporary solution to the problem described here: https://github.com/mpv-player/mpv/pull/17256 +-- TODO: remove this after a stable release of MPV where it'll be fixed local function input_select(args) mp.add_timeout(0.1, function() input.select(args) @@ -411,7 +409,7 @@ local function show_torrent_files(torrent_menu, torrent_index) local viewed_list = curl(TORRSERVER .. "/viewed", '{"action":"list","hash":"' .. torrent_menu.hash .. '"}') if not viewed_list then viewed_list = {} end - generate_m3u_edl(torrent_menu) + generate_m3u(torrent_menu) local items = {} local last_viewed @@ -424,7 +422,7 @@ local function show_torrent_files(torrent_menu, torrent_index) end end if i == last_viewed then - table.insert(items, "> " .. filename.filename) + table.insert(items, "x " .. filename.filename) else table.insert(items, " " .. filename.filename) end @@ -482,9 +480,6 @@ for _, host in ipairs(LOCAL_HOSTS) do end local function is_torrserver(path) - if not path then - path = mp.get_property("path", "") - end if torrserver_is_localhost then for _, host in ipairs(LOCAL_HOSTS) do if path:find(options.scheme .. "://" .. host .. ":" .. options.port, 1, true) then @@ -535,11 +530,13 @@ local function abort_loadings() loadings = {} end +local is_torrserver_path = false + local function load_external_assets() abort_loadings() local path = mp.get_property("path", "") - if not is_torrserver(path) then + if not is_torrserver_path then torrent = nil return end @@ -548,7 +545,7 @@ local function load_external_assets() if mp.get_property("playlist-path", ""):find("# Generated by TorrServer-Loader", 1, true) then connect_external_assets() - timer_torrent_load_info() + init_torrent_loading_timer() return end local btih = path:match("%link=(" .. string.rep(".", 40) .. ")") @@ -568,7 +565,7 @@ local function load_external_assets() end if not torrent.playlist then - generate_m3u_edl(torrent) + generate_m3u(torrent) end -- finding pos of playlist @@ -591,7 +588,19 @@ local function load_external_assets() mp.set_property_number("playlist-pos-1", play_index) end -mp.add_hook("on_load", 5, load_external_assets) +mp.add_hook("on_load", 5, function() + local path = mp.get_property("path", "") + is_torrserver_path = is_torrserver(path) + if is_torrserver_path then + mp.observe_property("demuxer-cache-duration", "number", function(_, value) + if not value or update_timer or not torrent or not is_buffering(value) then + return + end + init_torrent_loading_timer() + end) + end + load_external_assets() +end) -- By default, edl inserts a chapter with a link in the name, we fix this by deleting it. -- https://github.com/mpv-player/mpv/blob/master/DOCS/edl-mpv.rst#implicit-chapters @@ -616,12 +625,3 @@ local function fix_edl_chapters() end mp.add_hook("on_preloaded", 5, fix_edl_chapters) - -mp.observe_property("demuxer-cache-duration", "number", function(_, value) - if not value then return end - if update_timer or not torrent or not is_buffering(value) then - return - end - - timer_torrent_load_info() -end) From 5f83ab014253f5aa467982ff90b870ea4c8c467d Mon Sep 17 00:00:00 2001 From: pursvir Date: Mon, 9 Feb 2026 18:15:26 +0300 Subject: [PATCH 11/16] fix: prevent creating multiple demuxer cache observers --- torrserver_loader.lua | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/torrserver_loader.lua b/torrserver_loader.lua index c816e42..ff50409 100644 --- a/torrserver_loader.lua +++ b/torrserver_loader.lua @@ -306,17 +306,14 @@ local function generate_m3u(torr) local count = 0 torr.main_files = {} - local url - local hdr - for i, fileinfo in ipairs(torr.file_stats) do if not fileinfo.filename then extend_with_extra_fileinfo(fileinfo) end if not fileinfo.processed and VIDEO_EXTS[fileinfo.ext] then table.insert(playlist, '#EXTINF:0,' .. fileinfo.name) - url = TORRSERVER .. "/stream?link=" .. torr.hash .. "&index=" .. fileinfo.id .. "&play" - hdr = { "!new_stream", "!no_clip", + local url = TORRSERVER .. "/stream?link=" .. torr.hash .. "&index=" .. fileinfo.id .. "&play" + local hdr = { "!new_stream", "!no_clip", edlencode(url) } local edl = "edl://" .. table.concat(hdr, ";") .. ";" @@ -588,16 +585,19 @@ local function load_external_assets() mp.set_property_number("playlist-pos-1", play_index) end +local function observe_demuxer_cache(_, value) + if not value or update_timer or not torrent or not is_buffering(value) then + return + end + init_torrent_loading_timer() +end + mp.add_hook("on_load", 5, function() local path = mp.get_property("path", "") is_torrserver_path = is_torrserver(path) if is_torrserver_path then - mp.observe_property("demuxer-cache-duration", "number", function(_, value) - if not value or update_timer or not torrent or not is_buffering(value) then - return - end - init_torrent_loading_timer() - end) + mp.unobserve_property(observe_demuxer_cache) + mp.observe_property("demuxer-cache-duration", "number", observe_demuxer_cache) end load_external_assets() end) From ca6a5e0cd3642fe54208c5147652c68be0fa55a6 Mon Sep 17 00:00:00 2001 From: pursvir Date: Mon, 9 Feb 2026 22:51:05 +0300 Subject: [PATCH 12/16] always unobserve demux cache prop on_load, deduplicate code --- torrserver_loader.lua | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/torrserver_loader.lua b/torrserver_loader.lua index ff50409..d2c5ae4 100644 --- a/torrserver_loader.lua +++ b/torrserver_loader.lua @@ -370,7 +370,7 @@ local function generate_m3u(torr) fileinfo.processed = true table.insert(torr.main_files, i) table.insert(playlist, '#EXTINF:0,' .. fileinfo.name) - url = TORRSERVER .. "/stream?link=" .. torr.hash .. "&index=" .. fileinfo.id .. "&play" + local url = TORRSERVER .. "/stream?link=" .. torr.hash .. "&index=" .. fileinfo.id .. "&play" table.insert(playlist, url) end end @@ -527,16 +527,8 @@ local function abort_loadings() loadings = {} end -local is_torrserver_path = false - local function load_external_assets() - abort_loadings() - local path = mp.get_property("path", "") - if not is_torrserver_path then - torrent = nil - return - end playing_pos = mp.get_property_number("playlist-pos-1", 1) @@ -593,12 +585,15 @@ local function observe_demuxer_cache(_, value) end mp.add_hook("on_load", 5, function() + mp.unobserve_property(observe_demuxer_cache) + local path = mp.get_property("path", "") - is_torrserver_path = is_torrserver(path) + local is_torrserver_path = is_torrserver(path) if is_torrserver_path then - mp.unobserve_property(observe_demuxer_cache) mp.observe_property("demuxer-cache-duration", "number", observe_demuxer_cache) end + + abort_loadings() load_external_assets() end) From fa6abbf06f18357862a32bfb6cc10a7561623d46 Mon Sep 17 00:00:00 2001 From: pursvir Date: Mon, 9 Feb 2026 22:53:27 +0300 Subject: [PATCH 13/16] reduce input.select timeout --- torrserver_loader.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/torrserver_loader.lua b/torrserver_loader.lua index d2c5ae4..4f5b4e8 100644 --- a/torrserver_loader.lua +++ b/torrserver_loader.lua @@ -392,7 +392,7 @@ end -- A temporary solution to the problem described here: https://github.com/mpv-player/mpv/pull/17256 -- TODO: remove this after a stable release of MPV where it'll be fixed local function input_select(args) - mp.add_timeout(0.1, function() + mp.add_timeout(0.01, function() input.select(args) end) end From 0b58b4ffa8a85e360d0a0cfe90d098ddaa283128 Mon Sep 17 00:00:00 2001 From: pursvir Date: Mon, 9 Feb 2026 22:56:29 +0300 Subject: [PATCH 14/16] prevent (again) loading assets if not viewing TorrServer content --- torrserver_loader.lua | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/torrserver_loader.lua b/torrserver_loader.lua index 4f5b4e8..eb14237 100644 --- a/torrserver_loader.lua +++ b/torrserver_loader.lua @@ -527,7 +527,12 @@ local function abort_loadings() loadings = {} end +local is_torrserver_path = false + local function load_external_assets() + if not is_torrserver_path then + return + end local path = mp.get_property("path", "") playing_pos = mp.get_property_number("playlist-pos-1", 1) @@ -588,7 +593,7 @@ mp.add_hook("on_load", 5, function() mp.unobserve_property(observe_demuxer_cache) local path = mp.get_property("path", "") - local is_torrserver_path = is_torrserver(path) + is_torrserver_path = is_torrserver(path) if is_torrserver_path then mp.observe_property("demuxer-cache-duration", "number", observe_demuxer_cache) end From dbd5ecc63ba3d64fa114783680a21030a449c82c Mon Sep 17 00:00:00 2001 From: pursvir Date: Tue, 10 Feb 2026 00:10:40 +0300 Subject: [PATCH 15/16] remove unneeded is_torrserver_path var --- torrserver_loader.lua | 33 +++++++++++++-------------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/torrserver_loader.lua b/torrserver_loader.lua index eb14237..40c795a 100644 --- a/torrserver_loader.lua +++ b/torrserver_loader.lua @@ -520,21 +520,29 @@ local function connect_external_assets() end end +local function observe_demuxer_cache(_, value) + if not value or update_timer or not torrent or not is_buffering(value) then + return + end + init_torrent_loading_timer() +end + local function abort_loadings() + mp.unobserve_property(observe_demuxer_cache) + for request_id in pairs(loadings) do mp.abort_async_command(request_id) end loadings = {} end -local is_torrserver_path = false - local function load_external_assets() - if not is_torrserver_path then + local path = mp.get_property("path", "") + if is_torrserver(path) then + mp.observe_property("demuxer-cache-duration", "number", observe_demuxer_cache) + else return end - local path = mp.get_property("path", "") - playing_pos = mp.get_property_number("playlist-pos-1", 1) if mp.get_property("playlist-path", ""):find("# Generated by TorrServer-Loader", 1, true) then @@ -582,22 +590,7 @@ local function load_external_assets() mp.set_property_number("playlist-pos-1", play_index) end -local function observe_demuxer_cache(_, value) - if not value or update_timer or not torrent or not is_buffering(value) then - return - end - init_torrent_loading_timer() -end - mp.add_hook("on_load", 5, function() - mp.unobserve_property(observe_demuxer_cache) - - local path = mp.get_property("path", "") - is_torrserver_path = is_torrserver(path) - if is_torrserver_path then - mp.observe_property("demuxer-cache-duration", "number", observe_demuxer_cache) - end - abort_loadings() load_external_assets() end) From d12e7ddc1fcc9ff7eded36bb0e7c77434d5fecca Mon Sep 17 00:00:00 2001 From: Serega007 Date: Tue, 10 Feb 2026 00:45:01 +0300 Subject: [PATCH 16/16] reset torrent var is not torrent and small changes with observe_property & is_torrserver --- torrserver_loader.lua | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/torrserver_loader.lua b/torrserver_loader.lua index 40c795a..df6c55c 100644 --- a/torrserver_loader.lua +++ b/torrserver_loader.lua @@ -538,11 +538,12 @@ end local function load_external_assets() local path = mp.get_property("path", "") - if is_torrserver(path) then - mp.observe_property("demuxer-cache-duration", "number", observe_demuxer_cache) - else + if not is_torrserver(path) then + torrent = nil return end + + mp.observe_property("demuxer-cache-duration", "number", observe_demuxer_cache) playing_pos = mp.get_property_number("playlist-pos-1", 1) if mp.get_property("playlist-path", ""):find("# Generated by TorrServer-Loader", 1, true) then