summaryrefslogtreecommitdiff
path: root/rt.lua
diff options
context:
space:
mode:
authorDavid Vazgenovich Shakaryan <dvshakaryan@gmail.com>2026-01-29 21:48:19 -0800
committerDavid Vazgenovich Shakaryan <dvshakaryan@gmail.com>2026-01-29 21:48:19 -0800
commit28068fb57dc1e0aff22f6de3c79ee3f9d8ffa65d (patch)
tree16d25a670867ba56a53ea07afacd780b07275d8c /rt.lua
parentd59d49480e2981345fe173f741d6b0ad4a2d2320 (diff)
downloadmpv-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.lua453
1 files changed, 47 insertions, 406 deletions
diff --git a/rt.lua b/rt.lua
index 77e6ff7..1cdf777 100644
--- a/rt.lua
+++ b/rt.lua
@@ -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