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 /rx.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 'rx.lua')
| -rw-r--r-- | rx.lua | 374 |
1 files changed, 374 insertions, 0 deletions
@@ -0,0 +1,374 @@ +-- Copyright 2025 David Vazgenovich Shakaryan + +local util = require('util') + +local rx = {} + +local state +local osd +local ctx + +function rx.init(_state, _osd, _ctx) + state = _state + osd = _osd + ctx = _ctx +end + +function rx.series_children(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 entry_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, 'active_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, +} + +function rx.menu_options_group(group) + local options = {} + for i, v in ipairs(ctx.catalogue:group_children(group)) do + local t = setmetatable({_v = v}, entry_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 + +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 + +function rx.menu_options_entry_info(info) + if not info or not info.info 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 img = util.strip_ne(info.cover_big) or util.strip_ne(info.cover) + return options, img +end + +local 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, +} + +function rx.menu_options_channel_epg(ch) + local progs = ctx.epg:channel_programmes(ch) + if not progs 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, programme_mt) + end + + return options, idx or #options +end + +function rx.menu_options_programme_info(prog) + 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 + end + + return options +end + +local function menu_options_search_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 + menu_options_search_build( + rx.menu_options_group(v), t, path) + end + end +end + +function rx.menu_options_search(opts) + local t = {categories = {}, elements = {}} + + local options = {} + for i, v in ipairs(opts) 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. + options[i] = setmetatable({}, {__index = v}) + end + + -- empty table is needed to shadow existing path from proxied table + menu_options_search_build(options, t, {}) + + -- display categories first + options = t.categories + for _, v in ipairs(t.elements) do + options[#options+1] = v + end + return options +end + +function rx.sort_menu_options(opts) + local scores = {} + for _, v in ipairs(opts) 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(opts, 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 + +return rx |
