summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--catalogue.lua87
-rw-r--r--epg.lua13
-rw-r--r--osd.lua14
-rw-r--r--rt.lua349
-rw-r--r--state.lua14
5 files changed, 267 insertions, 210 deletions
diff --git a/catalogue.lua b/catalogue.lua
index cf9d897..7f83e82 100644
--- a/catalogue.lua
+++ b/catalogue.lua
@@ -14,13 +14,6 @@ function catalogue.new()
id = 'root',
name = '/',
})
- t:add({
- type = 'group',
- id = 'favourites',
- parent_id = 'root',
- name = 'Favourites',
- lazy = true, -- prevent infinite recursion on search
- })
return t
end
@@ -50,7 +43,7 @@ end
function mt:add(entry)
self.data[entry.id] = entry
- if entry.type == 'group' then
+ if entry.type == 'group' and not entry.children_f then
entry.children = {}
end
@@ -76,57 +69,6 @@ function mt:add(entry)
return entry
end
-function mt:load_xc_section(section)
- self:add({
- section = section.id,
- type = 'group',
- group_type = 'category',
- id = section.id .. ':category:0',
- parent_id = 'root',
- name = section.name,
- })
-
- -- currently, this will not correctly handle subcategories which come
- -- before their parent category
- for _, v in ipairs(section.categories) do
- self:add({
- section = section.id,
- type = 'group',
- group_type = 'category',
- id = section.id .. ':category:' .. v.category_id,
- parent_id = section.id .. ':category:' .. v.parent_id,
- name = util.strip(v.category_name),
- })
- end
-
- for _, v in ipairs(section.elements) do
- local vv = {
- section = section.id,
- parent_id = section.id .. ':category:' ..
- v.category_id,
- name = util.strip(v.name),
- }
-
- if section.type == 'series' then
- vv.type = 'group'
- vv.group_type = 'series'
- vv.id = section.id .. ':series:' .. v.series_id
- vv.series_id = v.series_id
- vv.img_url = util.strip_ne(v.cover)
- vv.lazy = true -- avoid API calls on search
- else
- vv.type = 'stream'
- vv.id = section.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
-
- self:add(vv)
- end
-end
-
function mt:path_to_root(entry)
local path = {}
@@ -153,4 +95,31 @@ function mt:path_from_root(entry)
return path
end
+function mt:group_count(group)
+ if group.count then
+ return group.count
+ end
+
+ if group.count_f then
+ return group.count_f(group)
+ end
+
+ local count = 0
+ -- only children, not children_f, is considered here. dynamically
+ -- loaded groups will have a count of 0 unless count or count_f is
+ -- specified.
+ for _, v in ipairs(group.children or {}) do
+ if v.type == 'group' then
+ count = count + (v.count or mt:group_count(v) or 0)
+ else
+ count = count + 1
+ end
+ end
+ return count
+end
+
+function mt:group_children(group)
+ return group.children_f and group.children_f(group) or group.children
+end
+
return catalogue
diff --git a/epg.lua b/epg.lua
index c7af266..a3d84f2 100644
--- a/epg.lua
+++ b/epg.lua
@@ -66,4 +66,17 @@ function mt:scheduled_programme(ch, time)
end
end
+function mt:next_programme(ch, time)
+ local progs = self.channels[ch]
+ if not progs then
+ return
+ end
+
+ for _, v in ipairs(progs) do
+ if v.start > time then
+ return v
+ end
+ end
+end
+
return epg
diff --git a/osd.lua b/osd.lua
index 58c6903..7075a73 100644
--- a/osd.lua
+++ b/osd.lua
@@ -235,8 +235,9 @@ function mt:option_text(opt, info)
str = col .. '[' .. str .. ']'
end
- if opt.info and #opt.info > 0 then
- str = str .. colour.info .. ' (' .. asscape(opt.info) .. ')'
+ local opt_info = opt.info
+ if opt_info and #opt_info > 0 then
+ str = str .. colour.info .. ' (' .. asscape(opt_info) .. ')'
end
return str
@@ -542,6 +543,8 @@ function mt:render()
end
function mt:redraw(state)
+ self.redraw_time = os.time()
+
local out_titles = {}
local out_options = {}
@@ -574,15 +577,10 @@ function mt:redraw(state)
#menu.options) do
local opt = menu.options[i]
- -- use real-time count for favourites
- if opt.id == 'favourites' then
- opt.info = tostring(#state.favourites)
- end
-
local selected = i == menu.cursor and not menu.search_active
local info = {
selected = selected,
- empty = (opt.type == 'group' and not opt.lazy and
+ empty = (opt.type == 'group' and opt.children and
#opt.children == 0),
playing = not not (
opt.id and opt.id == state.playing_id),
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
diff --git a/state.lua b/state.lua
index da26a9a..a88fe41 100644
--- a/state.lua
+++ b/state.lua
@@ -167,7 +167,7 @@ function menu_mt:update_search_matches()
local options = {}
for _, v in ipairs(self.search_options) do
- local matches = {}
+ local matches
local name = v.name
if not case_sensitive then
@@ -180,13 +180,17 @@ function menu_mt:update_search_matches()
if not i then
break
end
+ matches = matches or {}
matches[#matches+1] = {start = i, stop = j}
end
- if #matches > 0 then
- local t = util.copy_table(v)
- t.matches = matches
- options[#options+1] = t
+ if matches then
+ -- search options may contain dynamic data that is
+ -- updated on redraw. using a proxy table instead of
+ -- copying prevents potential updates on every change
+ -- of search text.
+ options[#options+1] = setmetatable(
+ {matches = matches}, {__index = v})
end
end