Skip to content

Commit c1b20c3

Browse files
committed
refactor(anilink-m3u8): improve robustness and reliability of m3u8 parsing, header management and parallel subtitle loading
1 parent dc2ceac commit c1b20c3

1 file changed

Lines changed: 107 additions & 34 deletions

File tree

AniLINK/anilink-m3u8.lua

Lines changed: 107 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,144 @@
11
-- AniLINK M3U8 Player for MPV
2+
-- Version: 2.0.0
23
-- Auto-adds subtitles from M3U8 playlists with referrer/origin support
34

45
local mp = require 'mp'
6+
local utils = require 'mp.utils'
57

6-
local m3u8_data = {}
8+
local episodes = {}
9+
local current_episode_idx = 0
10+
local last_set_referrer = nil
711

812
local function parse_m3u8(path)
913
mp.msg.info("Parsing M3U8:", path)
1014
local file
11-
-- handle network file
1215
if path:match('^https?://') then
1316
local curl_cmd = string.format('curl -L --silent "%s"', path)
1417
file = io.popen(curl_cmd, 'r')
1518
else
1619
file = io.open(path, 'r')
1720
end
18-
if not file then return {} end
19-
20-
local entries = {}
21-
local referrer, origin, current_subs = nil, nil, {}
21+
if not file then return nil end
2222

23+
local lines = {}
2324
for line in file:lines() do
25+
table.insert(lines, line)
26+
end
27+
file:close()
28+
29+
-- Parse entire playlist structure first
30+
local episodes = {}
31+
local current_referrer, current_origin = nil, nil
32+
local pending_subs = {}
33+
local pending_audio = {}
34+
35+
for i, line in ipairs(lines) do
2436
if line:match('^#EXTVLCOPT:http%-referrer=(.+)') then
25-
referrer = line:match('=(.+)')
26-
origin = referrer:match('^(https?://[^/]+)'):gsub('/$', '') -- TODO: implement origin extraction in AniLINK userscript
27-
elseif line:match('^#EXT%-X%-MEDIA:TYPE=SUBTITLES') then
28-
-- Parse subtitle entry
37+
current_referrer = line:match('=(.+)')
38+
current_origin = current_referrer:match('^(https?://[^/]+)') or current_referrer
39+
elseif line:match('^#EXT%-X%-MEDIA:') then
40+
local media_type = line:match('TYPE=(%w+)')
41+
local group_id = line:match('GROUP%-ID="([^"]+)"')
2942
local name = line:match('NAME="([^"]+)"')
3043
local uri = line:match('URI="([^"]+)"')
31-
if name and uri then table.insert(current_subs, {name = name, uri = uri}) end
32-
elseif line:match('^https?://') then
33-
-- Map new media entry for url to collected subtitles
34-
entries[line] = current_subs
35-
current_subs = {}
44+
local is_default = line:match('DEFAULT=YES') ~= nil
45+
46+
if media_type == 'SUBTITLES' and name and uri then
47+
table.insert(pending_subs, {name = name, uri = uri, default = is_default, group = group_id})
48+
elseif media_type == 'AUDIO' and name and uri then
49+
table.insert(pending_audio, {name = name, uri = uri, default = is_default, group = group_id})
50+
end
51+
elseif line:match('^#EXTINF:') then
52+
local title = line:match(',(.+)') or 'Episode'
53+
local next_line = lines[i + 1]
54+
if next_line and next_line:match('^https?://') then
55+
table.insert(episodes, {
56+
title = title,
57+
url = next_line,
58+
subtitles = pending_subs,
59+
audio = pending_audio,
60+
referrer = current_referrer,
61+
origin = current_origin
62+
})
63+
pending_subs = {}
64+
pending_audio = {}
65+
end
3666
end
3767
end
3868

39-
-- Set HTTP referrer if provided
40-
if referrer then
41-
mp.set_property('http-header-fields', 'Referer:' .. referrer .. ',Origin:' .. origin)
42-
mp.msg.info("Set http-header-fields as Referrer:" .. referrer .. ",Origin:" .. origin)
43-
end
44-
45-
file:close()
46-
return entries
69+
return episodes
4770
end
4871

49-
local function add_subtitles()
50-
local url = mp.get_property('path')
51-
local subs = m3u8_data[url]
52-
if not subs then return end
72+
local function add_subtitles_parallel()
73+
local current_url = mp.get_property('path')
74+
if not current_url then return end
75+
76+
-- Find current episode
77+
local episode = nil
78+
for idx, ep in ipairs(episodes) do
79+
if ep.url == current_url then
80+
episode = ep
81+
current_episode_idx = idx
82+
break
83+
end
84+
end
5385

54-
for _, sub in ipairs(subs) do
55-
mp.commandv('sub-add', sub.uri, 'cached', sub.name)
86+
if not episode then
87+
mp.msg.verbose("Episode not found in playlist")
88+
return
5689
end
5790

58-
mp.commandv('set', 'sub', 'auto')
59-
mp.msg.info('Added', #subs, 'subtitle tracks')
91+
-- Update referrer if different from last set
92+
if episode.referrer and episode.referrer ~= last_set_referrer then
93+
mp.set_property('http-header-fields', 'Referer:' .. episode.referrer .. ',Origin:' .. episode.origin)
94+
mp.msg.info("Updated headers - Referrer:" .. episode.referrer .. ", Origin:" .. episode.origin)
95+
last_set_referrer = episode.referrer
96+
end
97+
98+
if not episode.subtitles or #episode.subtitles == 0 then
99+
mp.msg.verbose("No subtitles for current episode")
100+
return
101+
end
102+
103+
mp.msg.info("Adding", #episode.subtitles, "subtitle tracks for:", episode.title)
104+
105+
-- Add all subtitles in parallel (non-blocking) with completion tracking
106+
local total = #episode.subtitles
107+
local completed = 0
108+
109+
for _, sub in ipairs(episode.subtitles) do
110+
mp.command_native_async({
111+
name = 'sub-add',
112+
url = sub.uri,
113+
flags = 'cached',
114+
title = sub.name
115+
}, function(success, result, error)
116+
completed = completed + 1
117+
if completed == total then
118+
-- All subtitles loaded, now auto-select
119+
mp.commandv('set', 'sub', 'auto')
120+
mp.msg.info("Successfully loaded", total, "subtitles")
121+
end
122+
end)
123+
end
124+
125+
mp.msg.info("Queued", total, "subtitles for parallel loading")
60126
end
61127

62128
local function handle_m3u8()
63129
local path = mp.get_property('path')
64-
if path and path:match('%.m3u8$') and not path:match('^https?://') or path:match('/paste.rs/') then
65-
m3u8_data = parse_m3u8(path)
130+
if not path then return end
131+
132+
-- Only parse local m3u8 files or paste.rs URLs (paste.rs is the workaround used by AniLINK for playing entire playlists)
133+
if path:match('%.m3u8$') and (not path:match('^https?://') or path:match('paste%.rs/')) then
134+
local parsed = parse_m3u8(path)
135+
if parsed then
136+
episodes = parsed
137+
mp.msg.info("Parsed playlist with", #episodes, "episodes")
138+
end
66139
end
67140
end
68141

69142
mp.register_event('start-file', handle_m3u8)
70-
mp.register_event('file-loaded', add_subtitles)
143+
mp.register_event('file-loaded', add_subtitles_parallel)
71144
mp.msg.info('AniLINK M3U8 plugin loaded')

0 commit comments

Comments
 (0)