summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Vazgenovich Shakaryan <dvshakaryan@gmail.com>2026-01-07 13:33:47 -0800
committerDavid Vazgenovich Shakaryan <dvshakaryan@gmail.com>2026-01-07 13:33:47 -0800
commitc442abc24ebbddaf8d9fab36375128e06547d65e (patch)
tree4f37b8ed27fd9d70091c6b925605bc70585d9114
parentd9efccd403dbb189e99f6c43fdad16940e695e30 (diff)
downloadmpv-iptv-menu-c442abc24ebbddaf8d9fab36375128e06547d65e.tar.gz
mpv-iptv-menu-c442abc24ebbddaf8d9fab36375128e06547d65e.tar.xz
move state to new module
-rw-r--r--main.lua177
-rw-r--r--osd.lua33
-rw-r--r--state.lua70
3 files changed, 156 insertions, 124 deletions
diff --git a/main.lua b/main.lua
index b09f96b..524d727 100644
--- a/main.lua
+++ b/main.lua
@@ -7,6 +7,7 @@ local _catalogue = require('catalogue')
local _downloader = require('downloader')
local _epg = require('epg')
local _osd = require('osd')
+local _state = require('state')
local _xc = require('xc')
local mp_utils = require('mp.utils')
@@ -14,6 +15,8 @@ local mp_utils = require('mp.utils')
local script_name = mp.get_script_name()
local script_dir = mp.get_script_directory()
+local state = _state.new()
+
local downloader = _downloader.new()
local xc = _xc.new({
server = mp.get_opt('iptv_menu.xc_server'),
@@ -40,19 +43,15 @@ xc = cacher.wrap(xc, {
local catalogue = _catalogue.new()
local epg = _epg.new()
local osd = _osd.new()
-local favourites
-local playing_id
-local depth = 0
-local menus = {}
local key_bindings = {}
local function update_osd()
- osd:redraw(menus, depth, favourites, playing_id)
+ osd:redraw(state)
end
local function osd_menu_lines()
- return osd:menu_lines(depth)
+ return osd:menu_lines(state)
end
-- FIXME leaving this here as a global for now since the image is downloaded
@@ -93,24 +92,16 @@ local function load_data()
epg:load_xc_data(xc:get_epg())
local t = util.read_json_file(config.favourites_file)
- favourites = t.favourites or {}
+ state.favourites = t.favourites or {}
end
local function save_favourites()
- util.write_json_file(config.favourites_file, {favourites = favourites})
-end
-
--- returns index or nil
-function favourited(id)
- for i, v in ipairs(favourites) do
- if v == id then
- return i
- end
- end
+ util.write_json_file(
+ config.favourites_file, {favourites = state.favourites})
end
local function set_cursor(pos, opts)
- local menu = menus[depth]
+ local menu = state:menu()
local lines = osd_menu_lines()
local pos = math.max(1, math.min(pos, #menu.options))
@@ -139,11 +130,11 @@ local function set_cursor(pos, opts)
end
local function cursor_up()
- set_cursor(menus[depth].cursor - 1)
+ set_cursor(state:menu().cursor - 1)
end
local function cursor_down()
- set_cursor(menus[depth].cursor + 1)
+ set_cursor(state:menu().cursor + 1)
end
local function cursor_start()
@@ -151,21 +142,21 @@ local function cursor_start()
end
local function cursor_end()
- set_cursor(#menus[depth].options)
+ set_cursor(#state:menu().options)
end
local function cursor_page_up()
set_cursor(
- menus[depth].cursor - osd_menu_lines(), {keep_offset = true})
+ state:menu().cursor - osd_menu_lines(), {keep_offset = true})
end
local function cursor_page_down()
set_cursor(
- menus[depth].cursor + osd_menu_lines(), {keep_offset = true})
+ state:menu().cursor + osd_menu_lines(), {keep_offset = true})
end
local function cursor_to_object(id)
- for i, v in ipairs(menus[depth].options) do
+ for i, v in ipairs(state:menu().options) do
if v.id == id then
set_cursor(i, {centre = true})
return
@@ -173,25 +164,8 @@ local function cursor_to_object(id)
end
end
--- inserts the given id into the favourites array before the next favourited
--- menu option, starting from the next cursor position, or the end if no such
--- option is found. this is meant for in-place favouriting from the favourites
--- menu.
-local function insert_favourite_before_next_in_menu(id)
- local menu = menus[depth]
- for i = menu.cursor+1, #menu.options do
- local ind = favourited(menu.options[i].id)
- if ind then
- table.insert(favourites, ind, id)
- return
- end
- end
-
- favourites[#favourites+1] = id
-end
-
local function move_option(pos, opts)
- local menu = menus[depth]
+ local menu = state:menu()
if menu.group_id ~= 'favourites' or menu.sorted then
return
end
@@ -207,10 +181,10 @@ local function move_option(pos, opts)
local opt = table.remove(menu.options, prev_cursor)
table.insert(menu.options, menu.cursor, opt)
- local ind = favourited(opt.id)
+ local ind = state:favourited(opt.id)
if ind then
- table.remove(favourites, ind)
- insert_favourite_before_next_in_menu(opt.id)
+ state:remove_favourite_at(ind)
+ state:insert_favourite_before_next_in_menu(opt.id)
save_favourites()
end
@@ -218,11 +192,11 @@ local function move_option(pos, opts)
end
local function move_option_up()
- move_option(menus[depth].cursor - 1)
+ move_option(state:menu().cursor - 1)
end
local function move_option_down()
- move_option(menus[depth].cursor + 1)
+ move_option(state:menu().cursor + 1)
end
local function move_option_start()
@@ -230,32 +204,17 @@ local function move_option_start()
end
local function move_option_end()
- move_option(#menus[depth].options)
+ move_option(#state:menu().options)
end
local function move_option_page_up()
move_option(
- menus[depth].cursor - osd_menu_lines(), {keep_offset = true})
+ state:menu().cursor - osd_menu_lines(), {keep_offset = true})
end
local function move_option_page_down()
move_option(
- menus[depth].cursor + osd_menu_lines(), {keep_offset = true})
-end
-
-local function push_menu(t)
- local menu = {
- options = {},
- cursor = 1,
- view_top = 1,
- }
-
- for k, v in pairs(t) do
- menu[k] = v
- end
-
- depth = depth + 1
- menus[depth] = menu
+ state:menu().cursor + osd_menu_lines(), {keep_offset = true})
end
local function sort_options(options)
@@ -265,7 +224,7 @@ local function sort_options(options)
if v.missing then
score = score - 4
end
- if favourited(v.id) then
+ if state:favourited(v.id) then
score = score + 2
end
if v.type == 'group' and v.group_type ~= 'series' then
@@ -310,14 +269,14 @@ local function group_count(group)
return count
elseif group.id == 'favourites' then
-- not recursive
- return #favourites
+ return #state.favourites
end
end
local function favourites_group_menu_options(group)
local options = {}
local time = os.time()
- for _, id in ipairs(favourites) do
+ for _, id in ipairs(state.favourites) do
local obj = catalogue:get(id)
if obj then
local path = {}
@@ -440,7 +399,7 @@ local function group_menu_options(group)
end
local function push_group_menu(group)
- push_menu({
+ state:push_menu({
options = group_menu_options(group),
title = group.name,
type = 'group',
@@ -477,7 +436,7 @@ end
-- is always respected, while still preserving the relative order of existing
-- options when possible.
local function refresh_favourites_menu()
- local menu = menus[depth]
+ local menu = state:menu()
local opt = menu.options[menu.cursor]
local sorted = menu.sorted
if sorted then
@@ -523,13 +482,13 @@ local function refresh_favourites_menu()
end
local function prev_menu()
- depth = depth - 1
+ state.depth = state.depth - 1
- if depth == 0 then
+ if state.depth == 0 then
-- reset main menu
- push_group_menu(catalogue:get(menus[1].group_id))
+ push_group_menu(catalogue:get(state.menus[1].group_id))
else
- if menus[depth].group_id == 'favourites' then
+ if state:menu().group_id == 'favourites' then
refresh_favourites_menu()
end
update_osd()
@@ -550,7 +509,7 @@ local function play_stream(stream)
end
local function select_option()
- local menu = menus[depth]
+ local menu = state:menu()
local opt = menu.options[menu.cursor]
if not opt or not opt.id then
return
@@ -564,19 +523,19 @@ local function select_option()
end
local function favourite_option()
- local menu = menus[depth]
+ local menu = state:menu()
local opt = menu.options[menu.cursor]
if not opt or not opt.id then
return
end
- local ind = favourited(opt.id)
+ local ind = state:favourited(opt.id)
if ind then
- table.remove(favourites, ind)
+ state:remove_favourite_at(ind)
elseif menu.group_id == 'favourites' then
- insert_favourite_before_next_in_menu(opt.id)
+ state:insert_favourite_before_next_in_menu(opt.id)
else
- favourites[#favourites+1] = opt.id
+ state:add_favourite(opt.id)
end
save_favourites()
@@ -584,17 +543,17 @@ local function favourite_option()
end
local function goto_option()
- local menu = menus[depth]
+ local menu = state:menu()
local opt = menu.options[menu.cursor]
if not opt then
return
end
if menu.group_id == 'favourites' then
- depth = 1
+ state.depth = 1
elseif menu.type == 'search' then
- depth = depth - 1
- if menus[depth].group_id == 'favourites' then
+ state.depth = state.depth - 1
+ if state:menu().group_id == 'favourites' then
refresh_favourites_menu()
end
end
@@ -610,11 +569,11 @@ local function goto_option()
end
local function goto_playing()
- if not playing_id then
+ if not state.playing_id then
return
end
- local obj = catalogue:get(playing_id)
+ local obj = catalogue:get(state.playing_id)
if not obj then
return
end
@@ -630,7 +589,7 @@ local function goto_playing()
return
end
- depth = 1
+ state.depth = 1
for i = #path, 1, -1 do
cursor_to_object(path[i].id)
push_group_menu(path[i])
@@ -654,7 +613,7 @@ local function open_epg_programme(prog)
end
end
- push_menu({
+ state:push_menu({
options = options,
title = 'Programme: ' .. prog.title,
type = 'epg',
@@ -688,7 +647,7 @@ local function open_option_epg(opt)
options[i] = prog
end
- push_menu({
+ state:push_menu({
options = options,
title = 'EPG: ' .. opt.name .. ' (' .. ch .. ')',
type = 'epg',
@@ -806,7 +765,7 @@ local function open_option_title_info(title, info)
m.img_url = img
end
- push_menu(m)
+ state:push_menu(m)
update_osd()
end
@@ -835,7 +794,7 @@ local function open_option_episode_info(opt)
end
local function open_option_info()
- local menu = menus[depth]
+ local menu = state:menu()
local opt = menu.options[menu.cursor]
if not opt then
return
@@ -857,7 +816,7 @@ local function open_option_info()
end
local function search_menu_options_build(options, t, path)
- local menu = menus[depth]
+ local menu = state:menu()
local path = path or {}
for _, v in ipairs(options) do
@@ -892,7 +851,7 @@ local function search_menu_options(options)
end
function update_search_matches()
- local menu = menus[depth]
+ local menu = state:menu()
if #menu.search_text == 0 then
menu.options = menu.search_options
@@ -936,7 +895,7 @@ local function search_input_char(event)
return
end
- local menu = menus[depth]
+ local menu = state:menu()
menu.search_text = menu.search_text:sub(1, menu.search_cursor - 1) ..
event.key_text .. menu.search_text:sub(menu.search_cursor)
menu.search_cursor = menu.search_cursor + #event.key_text
@@ -945,7 +904,7 @@ local function search_input_char(event)
end
local function search_input_bs()
- local menu = menus[depth]
+ local menu = state:menu()
if menu.search_cursor <= 1 then
return
end
@@ -959,7 +918,7 @@ local function search_input_bs()
end
local function search_input_del()
- local menu = menus[depth]
+ local menu = state:menu()
if menu.search_cursor > #menu.search_text then
return
end
@@ -972,7 +931,7 @@ local function search_input_del()
end
local function set_search_cursor(pos)
- local menu = menus[depth]
+ local menu = state:menu()
local pos = math.max(1, math.min(#menu.search_text + 1, pos))
if pos == menu.search_cursor then
return
@@ -983,13 +942,13 @@ local function set_search_cursor(pos)
end
local function search_cursor_left()
- local menu = menus[depth]
+ local menu = state:menu()
set_search_cursor(util.utf8_seek(
menu.search_text, menu.search_cursor, -1))
end
local function search_cursor_right()
- local menu = menus[depth]
+ local menu = state:menu()
set_search_cursor(util.utf8_seek(
menu.search_text, menu.search_cursor, 1))
end
@@ -999,14 +958,14 @@ local function search_cursor_start()
end
local function search_cursor_end()
- set_search_cursor(#menus[depth].search_text + 1)
+ set_search_cursor(#state:menu().search_text + 1)
end
local bind_search_keys
local bind_menu_keys
local function start_search()
- local menu = menus[depth]
+ local menu = state:menu()
local title = 'Searching: <text_with_cursor>' ..
' <colours.info>(<num_matches>/<num_total>)'
@@ -1022,7 +981,7 @@ local function start_search()
menu.cursor = 1
menu.view_top = 1
else
- push_menu({
+ state:push_menu({
title = title,
type = 'search',
search_active = true,
@@ -1038,7 +997,7 @@ local function start_search()
end
local function end_search()
- local menu = menus[depth]
+ local menu = state:menu()
menu.search_active = false
menu.title = 'Search results: <text>' ..
' <colours.info>(<num_matches>/<num_total>)'
@@ -1047,7 +1006,7 @@ local function end_search()
end
local function cancel_search()
- local menu = menus[depth]
+ local menu = state:menu()
-- cancelling resumed search restores previous state
if menu.prev_search_text then
@@ -1060,13 +1019,13 @@ local function cancel_search()
end
menu.search_active = false
- depth = depth - 1
+ state.depth = state.depth - 1
update_osd()
bind_menu_keys()
end
local function toggle_menu_sort()
- local menu = menus[depth]
+ local menu = state:menu()
if menu.type ~= 'group' and menu.type ~= 'search' then
return
end
@@ -1151,7 +1110,7 @@ local function set_key_bindings()
if osd:is_hidden() then
unbind_keys()
mp.command_native({'enable-section', 'default'})
- elseif menus[depth].search_active then
+ elseif state:menu().search_active then
bind_search_keys()
mp.command_native({'disable-section', 'default'})
else
@@ -1171,12 +1130,12 @@ mp.observe_property('osd-dimensions', 'native', function(_, val)
end)
mp.register_event('start-file', function()
- playing_id = mp.get_opt('iptv_menu.playing_id')
+ state.playing_id = mp.get_opt('iptv_menu.playing_id')
update_osd()
end)
mp.register_event('end-file', function()
- playing_id = nil
+ state.playing_id = nil
update_osd()
end)
diff --git a/osd.lua b/osd.lua
index 72d9cea..12ae826 100644
--- a/osd.lua
+++ b/osd.lua
@@ -57,10 +57,10 @@ function mt:resize(w, h)
self.scale = h / 720
end
-function mt:menu_lines(depth)
- if depth > 1 then
+function mt:menu_lines(state)
+ if state.depth > 1 then
-- leaves an extra line for padding between titles and options
- return self.lines - depth
+ return self.lines - state.depth
else
return self.lines
end
@@ -252,9 +252,10 @@ function mt:remove_image()
self.img = nil
end
-function mt:draw_scrollbar(menu, depth)
+function mt:draw_scrollbar(state)
+ local menu = state:menu()
local opts = #menu.options
- local lines = self:menu_lines(depth)
+ local lines = self:menu_lines(state)
if opts <= lines then
return
end
@@ -274,17 +275,17 @@ function mt:draw_scrollbar(menu, depth)
'{\\pos(2,' .. top + pos .. ')}' .. draw_rect(0, 0, w, hh))
end
-function mt:redraw(menus, depth, favourites, playing_id)
+function mt:redraw(state)
local out = {}
- if depth > 1 then
- for i = 2, depth do
- out[#out+1] = self:menu_title(menus[i])
+ if state.depth > 1 then
+ for i = 2, state.depth do
+ out[#out+1] = self:menu_title(state.menus[i])
end
out[#out+1] = ' ' -- space character for correct line height
end
- local menu = menus[depth]
+ local menu = state:menu()
local img
if menu.img_url then
@@ -292,13 +293,13 @@ function mt:redraw(menus, depth, favourites, playing_id)
end
for i = menu.view_top, math.min(
- menu.view_top + self:menu_lines(depth) - 1,
+ menu.view_top + self:menu_lines(state) - 1,
#menu.options) do
local opt = menu.options[i]
-- use real-time count for favourites
if opt.id == 'favourites' then
- opt.info = tostring(#favourites)
+ opt.info = tostring(#state.favourites)
end
local selected = i == menu.cursor and not menu.search_active
@@ -306,8 +307,10 @@ function mt:redraw(menus, depth, favourites, playing_id)
selected = selected,
empty = (opt.type == 'group' and not opt.lazy and
#opt.children == 0),
- playing = not not (opt.id and opt.id == playing_id),
- favourited = not not (opt.id and favourited(opt.id)),
+ playing = not not (
+ opt.id and opt.id == state.playing_id),
+ favourited = not not (
+ opt.id and state:favourited(opt.id)),
}
out[#out+1] = self:option_icons(opt, info) ..
self:option_text(opt, info) ..
@@ -325,7 +328,7 @@ function mt:redraw(menus, depth, favourites, playing_id)
self.bg.data =
'{\\pos(0,0)\\alpha&H' .. config.bg_alpha .. '&\\c&H&}' ..
draw_rect(0, 0, 7680, 720)
- local sb = self:draw_scrollbar(menu, depth)
+ local sb = self:draw_scrollbar(state)
if sb then
self.bg.data = self.bg.data .. '\n' .. sb
end
diff --git a/state.lua b/state.lua
new file mode 100644
index 0000000..472e4d2
--- /dev/null
+++ b/state.lua
@@ -0,0 +1,70 @@
+-- Copyright 2025 David Vazgenovich Shakaryan
+
+local state = {}
+local mt = {}
+mt.__index = mt
+
+function state.new()
+ return setmetatable({
+ menus = {},
+ depth = 0,
+
+ favourites = {},
+ playing_id = nil,
+ }, mt)
+end
+
+function mt:menu()
+ return self.menus[self.depth]
+end
+
+function mt:push_menu(t)
+ local menu = {
+ options = {},
+ cursor = 1,
+ view_top = 1,
+ }
+
+ for k, v in pairs(t) do
+ menu[k] = v
+ end
+
+ self.depth = self.depth + 1
+ self.menus[self.depth] = menu
+end
+
+-- returns index if found
+function mt:favourited(id)
+ for i, v in ipairs(self.favourites) do
+ if v == id then
+ return i
+ end
+ end
+end
+
+function mt:add_favourite(id)
+ self.favourites[#self.favourites+1] = id
+end
+
+function mt:remove_favourite_at(i)
+ table.remove(self.favourites, i)
+end
+
+-- inserts the given id into the favourites array before the next favourited
+-- menu option, starting from the next cursor position, or the end if no such
+-- option is found. this is meant for in-place favouriting from the favourites
+-- menu.
+function mt:insert_favourite_before_next_in_menu(id)
+ local menu = self:menu()
+ for i = menu.cursor+1, #menu.options do
+ local ind = self:favourited(menu.options[i].id)
+ if ind then
+ table.insert(self.favourites, ind, id)
+ return
+ end
+ end
+
+ self:add_favourite(id)
+end
+
+return state