diff options
| author | David Vazgenovich Shakaryan <dvshakaryan@gmail.com> | 2026-01-29 21:48:19 -0800 |
|---|---|---|
| committer | David Vazgenovich Shakaryan <dvshakaryan@gmail.com> | 2026-01-29 21:48:19 -0800 |
| commit | 28068fb57dc1e0aff22f6de3c79ee3f9d8ffa65d (patch) | |
| tree | 16d25a670867ba56a53ea07afacd780b07275d8c /rt.lua | |
| parent | d59d49480e2981345fe173f741d6b0ad4a2d2320 (diff) | |
| download | mpv-iptv-menu-28068fb57dc1e0aff22f6de3c79ee3f9d8ffa65d.tar.gz mpv-iptv-menu-28068fb57dc1e0aff22f6de3c79ee3f9d8ffa65d.tar.xz | |
move application-aware utility functions to separate module
Diffstat (limited to 'rt.lua')
| -rw-r--r-- | rt.lua | 453 |
1 files changed, 47 insertions, 406 deletions
@@ -2,6 +2,7 @@ local config = require('config') local input = require('input') +local rx = require('rx') local util = require('util') local rt = {} @@ -30,68 +31,10 @@ local function cache_miss_status_msg(str) end local function series_children(series) - local info = ctx.xc:with_opts('get_series_info', series.series_id, - cache_miss_status_msg('Loading series info...')) - if not info or not info.seasons then - return {} - end - - local seasons = {} - for _, season in pairs(info.seasons) do - local episodes = {} - local season_num = tostring(season.season_number) - if info.episodes and info.episodes[season_num] then - for i, episode in pairs(info.episodes[season_num]) do - local epinfo = episode.info or {} - local t = { - name = util.strip(episode.title), - type = 'stream', - stream_type = 'series', - id = series.section .. ':stream:' .. - episode.id, - stream_id = episode.id, - img_url = util.strip_ne( - epinfo.movie_image), - } - t.info_data = { - name = t.name, - cover = t.img_url, - description = epinfo.plot, - releasedate = epinfo.releasedate, - duration = epinfo.duration, - video = epinfo.video, - audio = epinfo.audio, - } - episodes[#episodes+1] = t - end - end - - local count = tostring(#episodes) - local tmp = util.strip_ne(season.episode_count) - if tmp then - count = count .. '/' .. tmp - end - local t = { - type = 'group', - group_type = 'season', - id = series.id .. ':season:' .. season.id, - children = episodes, - name = util.strip(season.name), - info = count, - img_url = util.strip_ne(season.cover_big) or - util.strip_ne(season.cover), - } - t.info_data = { - name = t.name, - cover = t.img_url, - description = season.overview, - releasedate = season.air_date, - num_episodes = count, - } - seasons[#seasons+1] = t - end - - return seasons + return rx.series_children( + series, + ctx.xc:with_opts('get_series_info', series.series_id, + cache_miss_status_msg('Loading series info...'))) end local function catalogue_add_section(sect, cats, elems) @@ -278,7 +221,7 @@ function rt.cursor_wheel_page_down() {keep_offset = true}) end -local function cursor_to_object(id) +local function cursor_to_id(id) for i, v in ipairs(state:menu().options) do if v.id == id then rt.set_cursor(i, {centre = true}) @@ -361,93 +304,9 @@ function rt.move_option_wheel_page_down() {keep_offset = true}) end -local function sort_options(options) - local scores = {} - for _, v in ipairs(options) do - local score = 0 - if v.missing then - score = score - 4 - end - if state:favourited(v.id) then - score = score + 2 - end - if v.type == 'group' and v.group_type ~= 'series' then - score = score + 1 - end - scores[v] = score - end - - table.sort(options, function(a, b) - local sa = scores[a] - local sb = scores[b] - if sa ~= sb then - return sa > sb - else - return a.name < b.name - end - end) -end - -local menu_option_mt = { - __index = function(t, k) - local v = t._v - - if k == 'info' then - if v.type == 'group' and not v.hide_count then - local c = ctx.catalogue:group_count(v) - local ret = c and tostring(c) or '' - if not v.count_f then - rawset(t, 'info', ret) - end - return ret - elseif v.epg_channel_id then - local time = osd.redraw_time - if t._exp and time < t._exp then - return t._info - end - - local prog = ctx.epg:scheduled_programme( - v.epg_channel_id, time) - local ret = prog and prog.title or '' - local exp = prog and prog.stop - rawset(t, 'programme', prog) - - if not prog then - prog = ctx.epg:next_programme( - v.epg_channel_id, time) - exp = prog and prog.start - end - - rawset(t, exp and '_info' or 'info', ret) - rawset(t, '_exp', exp) - return ret - end - end - - return v[k] - end, -} - -local function group_menu_options(group) - local options = {} - for i, v in ipairs(ctx.catalogue:group_children(group)) do - local t = setmetatable({_v = v}, menu_option_mt) - - if group.id == 'favourites' then - local path = ctx.catalogue:path_from_root(v) - if path then - t.path = path - end - end - - options[i] = t - end - return options -end - function rt.push_group_menu(group) state:push_menu({ - options = group_menu_options(group), + options = rx.menu_options_group(group), title = group.name, type = 'group', group_id = group.id, @@ -455,9 +314,7 @@ function rt.push_group_menu(group) end -- refresh options when navigating up the stack to a previous favourites menu. --- existing menu options are never removed, even if unfavourited. the new order --- is always respected, while still preserving the relative order of existing --- options when possible. +-- existing menu options are never removed, even if unfavourited. local function refresh_favourites_menu() local menu = state:menu() local opt = menu.options[menu.cursor] @@ -466,41 +323,16 @@ local function refresh_favourites_menu() menu:set_sort(false) end - local options = group_menu_options(ctx.catalogue:get(menu.group_id)) - local pos = {} - for i, v in ipairs(options) do - pos[v.id] = i - end - - local res = {} - local seen = {} - local function append(v) - if not seen[v.id] then - res[#res+1] = v - seen[v.id] = true - end - end - - local ind = 1 - for _, v in ipairs(menu.options) do - if pos[v.id] then - while ind <= pos[v.id] do - append(options[ind]) - ind = ind + 1 - end - end - append(v) - end - for i = ind, #options do - append(options[i]) - end - menu.options = res + menu.options = util.stable_kmerge( + menu.options, + rx.menu_options_group(ctx.catalogue:get(menu.group_id)), + 'id') if sorted then - menu:set_sort(true, sort_options) + menu:set_sort(true, rx.sort_menu_options) end if opt then - cursor_to_object(opt.id) + cursor_to_id(opt.id) end end @@ -584,10 +416,10 @@ function rt.goto_option() end for i = 1, #opt.path do - cursor_to_object(opt.path[i].id) + cursor_to_id(opt.path[i].id) rt.push_group_menu(opt.path[i]) end - cursor_to_object(opt.id) + cursor_to_id(opt.id) osd:dirty() end @@ -602,231 +434,82 @@ function rt.goto_playing() state.depth = 1 for i = #path, 1, -1 do - cursor_to_object(path[i].id) + cursor_to_id(path[i].id) rt.push_group_menu(path[i]) end - cursor_to_object(id) + cursor_to_id(id) osd:dirty() end -local function open_epg_programme(prog, img_url) - local options = { - {name = 'Title: ' .. prog.title}, - {name = 'Start: ' .. os.date('%a %d %b %H:%M', prog.start)}, - {name = 'Stop: ' .. os.date('%a %d %b %H:%M', prog.stop)}, - } - - if prog.desc then - options[#options+1] = {name = ''} - for _, v in ipairs(util.wrap(prog.desc, 80)) do - options[#options+1] = {name = v} - end +local function open_option_programme_info(opt, img_url) + local prog = opt.programme + local options = rx.menu_options_programme_info(prog) + if not options then + return end - local menu = state:push_menu({ + state:push_menu({ options = options, - title = 'Programme: ' .. prog.title, - type = 'epg', + title = 'Programme Info: ' .. prog.title, + type = 'info', + img_url = img_url, }) - if img_url then - menu.img_url = img_url - end osd:dirty() end -local epg_programme_mt = { - __index = function(t, k) - if k == 'active' then - local time = osd.redraw_time - if t._exp and time < t._exp then - return t._active - end - - local v = t.programme - local ret = time >= v.start and time < v.stop - local exp = time < v.start and v.start or - ret and v.stop or nil - rawset(t, exp and '_active' or 'active', ret) - rawset(t, '_exp', exp) - return ret - end - end, -} - -local function open_option_epg(opt) +local function open_option_channel_epg(opt) local ch = opt.epg_channel_id:lower() - local progs = ctx.epg:channel_programmes(ch) - if not progs then + local options, idx = rx.menu_options_channel_epg(ch) + if not options then return end - local options = {} - local idx - local time = os.time() - for i, v in ipairs(progs) do - local prog = { - name = os.date('%a %d %b %H:%M', v.start) .. ' ' .. - os.date('%H:%M', v.stop) .. ' ' .. v.title, - type = 'programme', - info = v.desc, - programme = v, - } - - if not idx and time < v.stop then - idx = i - end - - options[i] = setmetatable(prog, epg_programme_mt) - end - idx = idx or #options - - local menu = state:push_menu({ + state:push_menu({ options = options, title = 'EPG: ' .. opt.name .. ' (' .. ch .. ')', type = 'epg', + img_url = opt.img_url, }) - if opt.img_url then - menu.img_url = opt.img_url - end rt.set_cursor(idx, {centre = true}) osd:dirty() end -local function add_info_field(dst, k, v, fmt) - local str = util.strip_ne(v) - if not str then - return - end - if fmt then - str = string.format(fmt, str) - end - if k then - str = k .. ': ' .. str - end - - -- continuation lines are 4 chars shorter and indented with 2 em spaces - for i, v in ipairs(util.wrap(str, 80, 76)) do - if i > 1 then - v = '\xe2\x80\x83\xe2\x80\x83' .. v - end - dst[#dst+1] = {name = v} - end -end - -local function add_info_set(options, set) - if #set > 0 then - options[#options+1] = {name = ''} - for _, v in ipairs(set) do - options[#options+1] = v - end - end -end - -local function open_option_title_info(title, info) - if not info or not info.info then +local function open_option_entry_info(title, info) + local options, img = rx.menu_options_entry_info(info) + if not options then return end - local info = info.info - - local options = {} - add_info_field(options, nil, info.name) - add_info_field(options, 'Directed by', info.director) - add_info_field(options, 'Starring', info.cast) - - local desc = util.strip_ne(info.description) or - util.strip_ne(info.plot) - if desc then - options[#options+1] = {name = ''} - for _, v in ipairs(util.wrap(desc, 80)) do - options[#options+1] = {name = v} - end - end - local set = {} - add_info_field(set, 'Genre', info.genre) - local date = util.strip_ne(info.releasedate) or - util.strip_ne(info.releaseDate) - if date then - local y, m, d = date:match('(%d+)-(%d+)-(%d+)') - if y then - local dt = {year = y, month = m, day = d} - add_info_field(set, 'Release date', - os.date('%d %B %Y', os.time(dt))) - end - end - add_info_field(set, 'Episode count', info.num_episodes) - add_info_field(set, 'Running time', info.duration) - add_info_set(options, set) - - local set = {} - if info.video then - local w = util.strip_ne(info.video.width) - local h = util.strip_ne(info.video.height) - if w and h then - local res = w .. 'x' .. h - local ar = util.strip_ne( - info.video.display_aspect_ratio) - if ar then - res = res .. ' (' .. ar .. ')' - end - add_info_field(set, 'Resolution', res) - end - add_info_field(set, 'Video codec', info.video.codec_long_name) - end - if info.audio then - add_info_field(set, 'Audio codec', info.audio.codec_long_name) - end - if info.bitrate ~= 0 then - add_info_field(set, 'Bitrate', info.bitrate, '%s Kbps') - end - add_info_set(options, set) - - local ytid = util.strip_ne(info.youtube_trailer) - if ytid then - local url = 'https://youtu.be/' .. ytid - add_info_set(options, {{ - name = 'Trailer: ' .. url, - type = 'stream', - id = 'youtube:' .. ytid, - stream_url = url, - }}) - end - - local m = { + state:push_menu({ options = options, title = title, type = 'info', - } - local img = util.strip_ne(info.cover_big) or util.strip_ne(info.cover) - if img then - m.img_url = img - end - - state:push_menu(m) + img_url = img, + }) osd:dirty() end local function open_option_movie_info(opt) local info = ctx.xc:with_opts('get_vod_info', opt.stream_id, cache_miss_status_msg('Loading movie info...')) - open_option_title_info('Movie Info: ' .. opt.name, info) + open_option_entry_info('Movie Info: ' .. opt.name, info) end local function open_option_series_info(opt) local info = ctx.xc:with_opts('get_series_info', opt.series_id, cache_miss_status_msg('Loading series info...')) - open_option_title_info('Series Info: ' .. opt.name, info) + open_option_entry_info('Series Info: ' .. opt.name, info) end local function open_option_season_info(opt) - open_option_title_info( + open_option_entry_info( 'Season Info: ' .. opt.name, {info = opt.info_data}) end local function open_option_episode_info(opt) - open_option_title_info( + open_option_entry_info( 'Episode Info: ' .. opt.name, {info = opt.info_data}) end @@ -838,10 +521,10 @@ function rt.open_option_info(opt) return end - if menu.type == 'epg' and opt.programme then - open_epg_programme(opt.programme, menu.img_url) + if opt.programme then + open_option_programme_info(opt, menu.img_url) elseif opt.epg_channel_id then - open_option_epg(opt) + open_option_channel_epg(opt) elseif opt.group_type == 'series' then open_option_series_info(opt) elseif opt.group_type == 'season' then @@ -853,48 +536,6 @@ function rt.open_option_info(opt) end end -local function search_menu_options_build(options, t, path) - for _, v in ipairs(options) do - v.path = path - - if v.type == 'group' and v.group_type ~= 'series' then - t.categories[#t.categories+1] = v - else - t.elements[#t.elements+1] = v - end - - -- contents of lazy-loaded groups should not be searchable - if v.type == 'group' and v.children then - local path = util.copy_table(path) - path[#path+1] = v - search_menu_options_build( - group_menu_options(v), t, path) - end - end -end - -local function search_menu_options(options) - local t = {categories = {}, elements = {}} - - local opts = {} - for i, v in ipairs(options) do - -- menu options may contain dynamic data that is updated on - -- redraw. using a proxy table instead of copying allows a - -- single update to target both the source and search menus. - opts[i] = setmetatable({}, {__index = v}) - end - - -- empty table is needed to shadow existing path from proxied table - search_menu_options_build(opts, t, {}) - - -- display categories first - local ret = t.categories - for _, v in ipairs(t.elements) do - ret[#ret+1] = v - end - return ret -end - function rt.search_input_char(ev) if ev.event ~= 'down' and ev.event ~= 'repeat' then return @@ -981,7 +622,7 @@ function rt.start_search() state:push_menu({ title = title, type = 'search', - options = search_menu_options(menu.options), + options = rx.menu_options_search(menu.options), search_active = true, }) end @@ -1023,7 +664,7 @@ function rt.toggle_menu_sort() return end - menu:set_sort(not menu.sorted, sort_options) + menu:set_sort(not menu.sorted, rx.sort_menu_options) osd:dirty() end |
