diff options
| -rw-r--r-- | catalogue.lua | 87 | ||||
| -rw-r--r-- | epg.lua | 13 | ||||
| -rw-r--r-- | osd.lua | 14 | ||||
| -rw-r--r-- | rt.lua | 349 | ||||
| -rw-r--r-- | state.lua | 14 |
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 @@ -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 @@ -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), @@ -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 @@ -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 |
