11-- AniLINK M3U8 Player for MPV
2+ -- Version: 2.0.0
23-- Auto-adds subtitles from M3U8 playlists with referrer/origin support
34
45local 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
812local 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
4770end
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" )
60126end
61127
62128local 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
67140end
68141
69142mp .register_event (' start-file' , handle_m3u8 )
70- mp .register_event (' file-loaded' , add_subtitles )
143+ mp .register_event (' file-loaded' , add_subtitles_parallel )
71144mp .msg .info (' AniLINK M3U8 plugin loaded' )
0 commit comments