diff options
| author | David Vazgenovich Shakaryan <dvshakaryan@gmail.com> | 2026-02-06 12:30:32 -0800 |
|---|---|---|
| committer | David Vazgenovich Shakaryan <dvshakaryan@gmail.com> | 2026-02-06 12:30:32 -0800 |
| commit | e1f7209131cafab74f0665599a13d668ebeb410c (patch) | |
| tree | 33d7d8c0beb9d91a5bd7ab5b51d4e1389ce97794 | |
| parent | ef41e8c9a54119628aa8cffb193e42782e69563f (diff) | |
| download | mpv-iptv-menu-e1f7209131cafab74f0665599a13d668ebeb410c.tar.gz mpv-iptv-menu-e1f7209131cafab74f0665599a13d668ebeb410c.tar.xz | |
implement global EPG search
| -rw-r--r-- | main.lua | 1 | ||||
| -rw-r--r-- | osd.lua | 40 | ||||
| -rw-r--r-- | rt.lua | 121 | ||||
| -rw-r--r-- | rx.lua | 59 | ||||
| -rw-r--r-- | state.lua | 36 |
5 files changed, 168 insertions, 89 deletions
@@ -129,6 +129,7 @@ input.set_persistent_mapping({ input.define_mapping('MENU', { ['BS'] = {rt.prev_menu}, ['/'] = {rt.start_search}, + ['E'] = {rt.start_epg_search}, ['Ctrl+s'] = {rt.toggle_menu_sort}, ['Ctrl+r'] = {rt.reload_option}, ['Ctrl+R'] = {rt.reload_data}, @@ -155,44 +155,14 @@ function mt:menu_lines(state) end function mt:menu_title(menu) - local str = asscape(menu.title) + local str, has_tags = menu:get_title() + str = asscape(str) local col = menu.search_active and colour.selected or colour.title - if menu.type == 'search' then - local start = #menu.search_text - #menu.search_term - local prefix = menu.search_text:sub(1, start) - local term = menu.search_text:sub(start + 1) - - if str:find('<text>', 1, true) then - str = str:gsub('<text>', function() - return colour.search_prefix .. - asscape(prefix) .. col .. asscape(term) - end) - end - - if str:find('<text_with_cursor>', 1, true) then - local idx = menu.search_cursor - if idx <= start then - prefix = asscape(prefix:sub(1, idx - 1)) .. - col .. cursor_glyph .. - colour.search_prefix .. - asscape(prefix:sub(idx)) - term = asscape(term) - else - idx = idx - start - prefix = asscape(prefix) - term = asscape(term:sub(1, idx - 1)) .. - cursor_glyph .. asscape(term:sub(idx)) - end - str = str:gsub('<text_with_cursor>', function() - return colour.search_prefix .. prefix .. col .. - term - end) - end - + if has_tags then + str = str:gsub('<cursor>', cursor_glyph) str = str:gsub('<colour%.(.-)>', colour) - str = str:gsub('<num_matches>', #menu.options) - str = str:gsub('<num_total>', #menu.search_options) + str = str:gsub('<reset_colour>', col) end if menu:is_sorted() then @@ -482,49 +482,6 @@ function rt.favourite_option() osd:dirty() end -function rt.goto_option() - local menu = state:menu() - local opt = menu.options[menu.cursor] - if not opt or not opt.path then - return - end - - if menu.group_id == 'favourites' then - state.depth = 1 - elseif menu.type == 'search' then - state.depth = state.depth - 1 - if state:menu().group_id == 'favourites' then - merge_refreshed_menu_options() - end - end - - for i = 1, #opt.path do - cursor_to_id(opt.path[i].id) - rt.push_group_menu(opt.path[i]) - end - cursor_to_id(opt.id) - - osd:dirty() -end - -function rt.goto_playing() - local id = state.playing_id - local obj = id and ctx.catalogue:get(id) - local path = obj and ctx.catalogue:path_to_root(obj) - if not path then - return - end - - state.depth = 1 - for i = #path, 1, -1 do - cursor_to_id(path[i].id) - rt.push_group_menu(path[i]) - end - cursor_to_id(id) - - osd:dirty() -end - local function open_option_programme_info(opt, img_url) local prog = opt.programme local options = rx.menu_options_programme_info(prog) @@ -621,6 +578,53 @@ function rt.open_option_info(opt) end end +function rt.goto_option() + local menu = state:menu() + local opt = menu.options[menu.cursor] + if not opt or not opt.path then + return + end + + if menu.group_id == 'favourites' then + state.depth = 1 + elseif menu.type == 'search' then + state.depth = state.depth - 1 + if state:menu().group_id == 'favourites' then + merge_refreshed_menu_options() + end + end + + for i = 1, #opt.path do + cursor_to_id(opt.path[i].id) + if i == #opt.path and opt.type == 'programme' then + open_option_channel_epg(opt.path[i]) + else + rt.push_group_menu(opt.path[i]) + end + end + cursor_to_id(opt.id) + + osd:dirty() +end + +function rt.goto_playing() + local id = state.playing_id + local obj = id and ctx.catalogue:get(id) + local path = obj and ctx.catalogue:path_to_root(obj) + if not path then + return + end + + state.depth = 1 + for i = #path, 1, -1 do + cursor_to_id(path[i].id) + rt.push_group_menu(path[i]) + end + cursor_to_id(id) + + osd:dirty() +end + local F_NAME = {name = true} local F_INFO = {info = true} local F_BOTH = {name = true, info = true} @@ -739,14 +743,12 @@ function rt.search_cursor_end() search_cursor_move(function(str) return #str + 1 end) end -function rt.start_search() +function rt.start_search(options, search_type, title) local menu = state:menu() - local title = 'Searching: <text_with_cursor>' .. - ' <colour.info>(<num_matches>/<num_total>)' - - if menu.type == 'search' then + if menu.type == 'search' and + (not search_type or + menu.search_type == search_type) then menu:save_checkpoint() - menu.title = title menu.search_active = true menu:set_search_cursor(#menu.search_text + 1) menu:set_cursor(1) @@ -754,8 +756,10 @@ function rt.start_search() state:push_menu({ title = title, type = 'search', - options = rx.menu_options_search(menu.options), + options = options or + rx.menu_options_search(menu.options), search_active = true, + search_type = search_type, }) end @@ -763,11 +767,20 @@ function rt.start_search() input.set_mapping('SEARCH') end -function rt.end_search() +function rt.start_epg_search() local menu = state:menu() - menu.search_active = false - menu.title = 'Search results: <text>' .. - ' <colour.info>(<num_matches>/<num_total>)' + local options = menu.search_type ~= 'epg' and + rx.menu_options_epg_search(menu.options) or nil + if options and not next(options) then + osd:flash_error('No EPG data found') + return + end + + rt.start_search(options, 'epg', 'EPG') +end + +function rt.end_search() + state:menu().search_active = false osd:dirty() input.set_mapping('MENU') end @@ -281,6 +281,7 @@ function rx.menu_options_channel_epg(src_id, ch) local time = os.time() for i, v in ipairs(progs) do local prog = { + id = ch .. ':' .. i, name = os.date('%a %d %b %H:%M', v.start) .. ' ' .. os.date('%H:%M', v.stop) .. ' ' .. v.title, type = 'programme', @@ -315,6 +316,64 @@ function rx.menu_options_programme_info(prog) return options end +local function menu_options_epg_search_build_progs(options, opt, path, time) + local ch = opt.epg_channel_id + local progs = ctx.src[opt.src_id].epg:channel_programmes(ch) + if not progs then + return + end + + for i, v in ipairs(progs) do + if time < v.stop then + options[#options+1] = setmetatable({ + id = ch .. ':' .. i, + name = os.date('%a %d %b %H:%M', v.start) .. + ' ' .. os.date('%H:%M', v.stop) .. + ' ' .. v.title, + type = 'programme', + programme = v, + path = path, + }, programme_mt) + end + end +end + +local function menu_options_epg_search_build(options, opts, path, seen, time) + for _, v in ipairs(opts) do + if v.id and not seen[v.id] then + seen[v.id] = true + path[#path+1] = v + if v.epg_channel_id then + menu_options_epg_search_build_progs( + options, v, util.copy_table(path), + time) + elseif v.type == 'group' and v.children then + menu_options_epg_search_build( + options, v.children, path, seen, time) + end + path[#path] = nil + end + end +end + +function rx.menu_options_epg_search(opts) + local options = {} + local time = os.time() + + menu_options_epg_search_build(options, opts, {}, {}, time) + table.sort(options, function(a, b) + if a.programme.start ~= b.programme.start then + return a.programme.start < b.programme.start + end + if a.programme.title ~= b.programme.title then + return a.programme.title < b.programme.title + end + return a.path[#a.path].name < b.path[#b.path].name + end) + + return options +end + local function menu_options_search_build(options, t, path) for _, v in ipairs(options) do v.path = path @@ -195,6 +195,42 @@ function menu_mt:set_options(options) self:set_cursor(1) end +function menu_mt:get_title() + if self.type == 'search' then + local str = self.search_active and 'Searching: ' or + 'Search results: ' + if self.title then + str = '(' .. self.title .. ') ' .. str + end + + local start = #self.search_text - #self.search_term + local prefix = self.search_text:sub(1, start) + local term = self.search_text:sub(start + 1) + if self.search_active then + local idx = self.search_cursor + if idx <= start then + prefix = prefix:sub(1, idx - 1) .. + '<reset_colour><cursor>' .. + '<colour.search_prefix>' .. + prefix:sub(idx) + else + idx = idx - start + term = term:sub(1, idx - 1) .. '<cursor>' .. + term:sub(idx) + end + end + str = str .. '<colour.search_prefix>' .. prefix .. + '<reset_colour>' .. term + + str = str .. ' <colour.info>(' .. #self.options .. '/' .. + #self.search_options .. ')' + + return str, true + end + + return self.title +end + function menu_mt:set_search_cursor(pos) local pos = math.max(1, math.min(#self.search_text + 1, pos)) if pos == self.search_cursor then |
