local utils = require('mp.utils') local script_dir = mp.get_script_directory() local stream_prefix = mp.get_opt('iptv_menu.stream_prefix') -- font size is in units of osd height, which is scaled to 720 local font_size = 20 local osd = mp.create_osd_overlay('ass-events') local osd_lines = math.floor((720 / font_size) + 0.5) - 1 local osd_padding = math.floor((720 - (osd_lines * font_size)) / 2) local key_bindings = {} local categories = {} local streams = {} local depth = 0 local menus = {} local menu_top = {} local menu_pos = {} local function load_json_file(path) local f = io.open(script_dir .. '/' .. path, 'r') local data = f:read('*all') f:close() return utils.parse_json(data) end local function load_data() categories = load_json_file('categories.json') for _, v in ipairs(categories) do v.type = 'category' end streams = load_json_file('streams.json') for _, v in ipairs(streams) do v.type = 'stream' end end local function osd_menu_lines() if depth > 1 then return osd_lines - depth else return osd_lines end end local function update_osd() local out = {} if depth > 1 then for i = 1, depth - 1 do out[#out+1] = '{\\c&H999999&} ยป [' .. menus[i][menu_pos[i]].category_name .. ']{\\c}' end out[#out+1] = ' ' -- space character for correct line height end for i = menu_top[depth], math.min( menu_top[depth] + osd_menu_lines() - 1, #menus[depth]) do local opt = menus[depth][i] local str if opt.type == 'category' then str = '[' .. opt.category_name .. ']' else str = opt.name end if i == menu_pos[depth] then str = '{\\c&HFF00&}* ' .. str .. '{\\c}' elseif opt.type == 'category' then str = '{\\c&H99DDFF&}' .. str .. '{\\c}' end out[#out+1] = str end -- \q2 disables line wrapping osd.data = '{\\q2}{\\fs' .. font_size .. '}{\\pos(' .. osd_padding .. ',' .. osd_padding .. '}' .. table.concat(out, '\\N') osd:update() end local function advance_cursor(n, opts) pos = math.max(1, math.min(menu_pos[depth] + n, #menus[depth])) top = menu_top[depth] if opts and opts.advance_page then top = top + n end -- move page to keep selected option visible if pos < top then top = pos elseif pos > top + osd_menu_lines() - 1 then top = pos - osd_menu_lines() + 1 end top = math.max(1, math.min(top, #menus[depth] - osd_menu_lines() + 1)) menu_pos[depth] = pos menu_top[depth] = top update_osd() end local function next_option() advance_cursor(1) end local function prev_option() advance_cursor(-1) end local function next_page() advance_cursor(osd_menu_lines(), {advance_page=true}) end local function prev_page() advance_cursor(-osd_menu_lines(), {advance_page=true}) end local function first_option() advance_cursor(-math.huge) end local function last_option() advance_cursor(math.huge) end local function add_category_menu(category_id) local menu = {} for _, v in ipairs(categories) do if tostring(v.parent_id) == category_id then menu[#menu+1] = v end end for _, v in ipairs(streams) do if v.category_id == category_id then menu[#menu+1] = v end end depth = depth + 1 menus[depth] = menu menu_top[depth] = 1 menu_pos[depth] = 1 update_osd() end local function play_stream(stream_id) local url = stream_prefix .. stream_id mp.commandv('loadfile', url) end local function select_option() local opt = menus[depth][menu_pos[depth]] if opt.type == 'category' then add_category_menu(opt.category_id) else play_stream(opt.stream_id) end end local function prev_menu() if depth > 1 then depth = depth - 1 update_osd() end end local function bind_key(key, name, func, opts) key_bindings[#key_bindings+1] = name mp.add_forced_key_binding(key, name, func, opts) end local function unbind_keys() for _, name in ipairs(key_bindings) do mp.remove_key_binding(name) end key_bindings = {} end local function bind_keys() local repeatable = {repeatable=true} bind_key('DOWN', 'next-option', next_option, repeatable) bind_key('UP', 'prev-option', prev_option, repeatable) bind_key('PGDWN', 'next-page', next_page, repeatable) bind_key('PGUP', 'prev-page', prev_page, repeatable) bind_key('HOME', 'first-option', first_option) bind_key('END', 'last-option', last_option) bind_key('ENTER', 'select-option', select_option) bind_key('BS', 'prev-menu', prev_menu) end local function toggle_menu() osd.hidden = not osd.hidden osd:update() if osd.hidden then unbind_keys() else bind_keys() end end mp.add_forced_key_binding('TAB', 'toggle-menu', toggle_menu) bind_keys() load_data() add_category_menu('0') update_osd()