summaryrefslogtreecommitdiff
path: root/rt.lua
diff options
context:
space:
mode:
authorDavid Vazgenovich Shakaryan <dvshakaryan@gmail.com>2026-01-27 01:12:16 -0800
committerDavid Vazgenovich Shakaryan <dvshakaryan@gmail.com>2026-01-27 01:16:58 -0800
commit82526665364389d92e0f2c33eba04678275863bb (patch)
tree547f3a797b861e85c79bde7baf87fe5145c0396d /rt.lua
parent1d2c82bfb4dcfd71045f2948bb320a94013971a5 (diff)
downloadmpv-iptv-menu-82526665364389d92e0f2c33eba04678275863bb.tar.gz
mpv-iptv-menu-82526665364389d92e0f2c33eba04678275863bb.tar.xz
on-demand calculation and update of option info
Using metatables to calculate info strings on render, we can avoid precomputing it for all options when generating the menu, making certain menus open much faster. This also allows us to update dynamic info, e.g. the currently programme, while the menu is open.
Diffstat (limited to 'rt.lua')
-rw-r--r--rt.lua349
1 files changed, 211 insertions, 138 deletions
diff --git a/rt.lua b/rt.lua
index 3cc2aa8..67d18a5 100644
--- a/rt.lua
+++ b/rt.lua
@@ -29,7 +29,148 @@ 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
+end
+
+local function catalogue_add_section(sect, cats, elems)
+ ctx.catalogue:add({
+ section = sect.id,
+ type = 'group',
+ group_type = 'category',
+ id = sect.id .. ':category:0',
+ parent_id = 'root',
+ name = sect.name,
+ })
+
+ -- currently, this will not correctly handle subcategories which come
+ -- before their parent category
+ for _, v in ipairs(cats) do
+ ctx.catalogue:add({
+ section = sect.id,
+ type = 'group',
+ group_type = 'category',
+ id = sect.id .. ':category:' .. v.category_id,
+ parent_id = sect.id .. ':category:' .. v.parent_id,
+ name = util.strip(v.category_name),
+ })
+ end
+
+ for _, v in ipairs(elems) do
+ local vv = {
+ section = sect.id,
+ parent_id = sect.id .. ':category:' .. v.category_id,
+ name = util.strip(v.name),
+ }
+
+ if sect.type == 'series' then
+ vv.type = 'group'
+ vv.group_type = 'series'
+ vv.id = sect.id .. ':series:' .. v.series_id
+ vv.series_id = v.series_id
+ vv.img_url = util.strip_ne(v.cover)
+ vv.count = 1
+ vv.hide_count = true
+ vv.children_f = series_children
+ else
+ vv.type = 'stream'
+ vv.id = sect.id .. ':stream:' .. v.stream_id
+ vv.stream_type = v.stream_type
+ vv.stream_id = v.stream_id
+ vv.img_url = util.strip_ne(v.stream_icon)
+ vv.epg_channel_id = util.strip_ne(v.epg_channel_id)
+ end
+
+ ctx.catalogue:add(vv)
+ end
+end
+
function rt.load_data(force)
+ ctx.catalogue:add({
+ type = 'group',
+ id = 'favourites',
+ parent_id = 'root',
+ name = 'Favourites',
+ count_f = function()
+ return #state.favourites
+ end,
+ children_f = function()
+ local options = {}
+ for i, id in ipairs(state.favourites) do
+ -- missing favourites are displayed so that
+ -- they can be removed
+ options[i] = ctx.catalogue:get(id) or
+ {
+ id = id,
+ name = id,
+ missing = true,
+ }
+ end
+ return options
+ end,
+ })
+
local arr = {
{id = 'live', name = 'Live TV', type = 'live'},
{id = 'movie', name = 'Movies', type = 'vod'},
@@ -56,15 +197,15 @@ function rt.load_data(force)
end
end,
}
- for _, v in ipairs(arr) do
- sect_str = base_str .. ' » ' .. v.name
- v.categories = ctx.xc:with_opts(
- 'get_' .. v.type .. '_categories', call_opts)
- v.elements = ctx.xc:with_opts(
- v.type == 'series' and 'get_series' or
- ('get_' .. v.type .. '_streams'),
+ for _, sect in ipairs(arr) do
+ sect_str = base_str .. ' » ' .. sect.name
+ local cats = ctx.xc:with_opts(
+ 'get_' .. sect.type .. '_categories', call_opts)
+ local elems = ctx.xc:with_opts(
+ sect.type == 'series' and 'get_series' or
+ ('get_' .. sect.type .. '_streams'),
call_opts)
- ctx.catalogue:load_xc_section(v)
+ catalogue_add_section(sect, cats, elems)
end
osd:set_status('Loading EPG...')
@@ -257,140 +398,65 @@ local function add_programme(opt, time)
end
end
-local function group_count(group)
- if group.children and not group.lazy then
- local count = 0
- for _, v in ipairs(group.children) do
- if v.type == 'stream' or v.group_type == 'series' then
- count = count + 1
- elseif v.type == 'group' then
- local c = group_count(v)
- if c then
- count = count + c
- end
- end
- end
- return count
- elseif group.id == 'favourites' then
- -- not recursive
- return #state.favourites
- end
-end
+local menu_option_mt_count = 0
+local menu_option_mt = {
+ __index = function(t, k)
+ local v = t._v
-local function favourites_group_menu_options(group)
- local options = {}
- local time = os.time()
- for _, id in ipairs(state.favourites) do
- local obj = ctx.catalogue:get(id)
- if obj then
- obj = util.copy_table(obj)
- add_programme(obj, time)
- local c = group_count(obj)
- if c then
- obj.info = tostring(c)
- end
- local path = ctx.catalogue:path_from_root(obj)
- if path then
- obj.path = path
- end
- options[#options+1] = obj
- else
- -- display missing favourites so that they can be
- -- removed
- options[#options+1] = {
- id = id,
- name = id,
- missing = true,
- }
- end
- end
- return options
-end
+ 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 ''
+ rawset(t, 'info', ret)
+ return ret
+ elseif v.epg_channel_id then
+ if t._info_exp and
+ osd.redraw_time <
+ t._info_exp then
+ return t._info
+ end
-local function series_group_menu_options(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 prog = ctx.epg:scheduled_programme(
+ v.epg_channel_id,
+ osd.redraw_time)
+ local ret = prog and prog.title or ''
+ local exp = prog and prog.stop
+
+ if not prog then
+ prog = ctx.epg:next_programme(
+ v.epg_channel_id,
+ osd.redraw_time)
+ exp = prog and prog.start
+
+ if not prog then
+ rawset(t, 'info', ret)
+ return ret
+ end
+ 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
+ rawset(t, '_info_exp', exp)
+ rawset(t, '_info', ret)
+ return ret
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
-end
+ return v[k]
+ end,
+}
local function group_menu_options(group)
- if group.id == 'favourites' then
- return favourites_group_menu_options(group)
- end
-
- if group.group_type == 'series' then
- return series_group_menu_options(group)
- end
-
local options = {}
- local time = os.time()
- for i, v in ipairs(group.children) do
- v = util.copy_table(v)
- add_programme(v, time)
- local c = group_count(v)
- if c then
- v.info = tostring(c)
+ 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] = v
+
+ options[i] = t
end
return options
end
@@ -784,12 +850,9 @@ function rt.open_option_info(opt)
end
local function search_menu_options_build(options, t, path)
- local menu = state:menu()
- local path = path or {}
-
for _, v in ipairs(options) do
- local v = util.copy_table(v)
v.path = path
+
if v.type == 'group' and v.group_type ~= 'series' then
t.categories[#t.categories+1] = v
else
@@ -797,7 +860,7 @@ local function search_menu_options_build(options, t, path)
end
-- contents of lazy-loaded groups should not be searchable
- if v.type == 'group' and not v.lazy then
+ if v.type == 'group' and v.children then
local path = util.copy_table(path)
path[#path+1] = v
search_menu_options_build(
@@ -808,7 +871,17 @@ end
local function search_menu_options(options)
local t = {categories = {}, elements = {}}
- search_menu_options_build(options, t)
+
+ 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