diff options
| author | David Vazgenovich Shakaryan <dvshakaryan@gmail.com> | 2026-02-03 22:42:26 -0800 |
|---|---|---|
| committer | David Vazgenovich Shakaryan <dvshakaryan@gmail.com> | 2026-02-03 22:42:26 -0800 |
| commit | 554e833b1a39ede9aef234449fc8992435f10751 (patch) | |
| tree | 3b5e7cf0077c6b758eafd76a97bfc81f5ba15145 | |
| parent | 6cc548c27470b81114412f3ecd74380894eba72e (diff) | |
| download | mpv-iptv-menu-554e833b1a39ede9aef234449fc8992435f10751.tar.gz mpv-iptv-menu-554e833b1a39ede9aef234449fc8992435f10751.tar.xz | |
support multiple sources
| -rw-r--r-- | catalogue.lua | 13 | ||||
| -rw-r--r-- | config.lua | 22 | ||||
| -rw-r--r-- | main.lua | 66 | ||||
| -rw-r--r-- | rt.lua | 115 | ||||
| -rw-r--r-- | rx.lua | 16 | ||||
| -rw-r--r-- | xc.lua | 8 |
6 files changed, 145 insertions, 95 deletions
diff --git a/catalogue.lua b/catalogue.lua index 7f83e82..425dc17 100644 --- a/catalogue.lua +++ b/catalogue.lua @@ -22,19 +22,20 @@ function mt:get(id) return self.data[id] end -function mt:_ensure_catchall(section_id) - local id = section_id .. ':category:catchall' +function mt:_ensure_catchall(src_id, section) + local id = src_id .. ':' .. section .. ':cat:catchall' local entry = self.data[id] if entry then return entry end return self:add({ - section = section_id, + src_id = src_id, + section = section, type = 'group', - group_type = 'category', + group_type = 'cat', id = id, - parent_id = section_id .. ':category:0', + parent_id = src_id .. ':' .. section .. ':cat:0', -- non-ascii symbol to sort near end name = '∗CATCHALL∗', }) @@ -56,7 +57,7 @@ function mt:add(entry) -- dump any entries referencing nonexistent categories into a single -- catchall category if not parent then - parent = self:_ensure_catchall(entry.section) + parent = self:_ensure_catchall(entry.src_id, entry.section) entry.parent_id = parent.id end @@ -81,22 +81,40 @@ local f = io.open( mp.command_native( {'expand-path', '~~/script-opts/' .. script_name .. '.conf'}), 'r') +config.src_order = {} +local conf_srcs = {} if f then for line in f:lines() do if #line > 0 then local k = line:match('^([^=]*)=') opts[k] = opts[k] or '' + + local src = k:match('^src%.([^.]*)%.') + if src and not conf_srcs[src] then + config.src_order[#config.src_order+1] = src + conf_srcs[src] = true + end end end f:close() end for k, v in pairs(mp.get_property_native('script-opts')) do if k:find(script_name .. '-', 1, true) == 1 then - local pk = k:sub(#script_name + 2) - opts[pk] = opts[pk] or '' + local k = k:sub(#script_name + 2) + opts[k] = opts[k] or '' end end mp_options.read_options(opts) util.unflatten_table(opts, config) +local cli_srcs = {} +for src in pairs(config.src or {}) do + if not conf_srcs[src] then + cli_srcs[#cli_srcs+1] = src + end +end +table.sort(cli_srcs) +for _, src in ipairs(cli_srcs) do + config.src_order[#config.src_order+1] = src +end return config @@ -16,32 +16,7 @@ local _xc = require('xc') local mp_utils = require('mp.utils') local state = _state.new() - local downloader = _downloader.new({limit = 5}) -local xc = _xc.new({ - server = config.xc_server, - user = config.xc_user, - pass = config.xc_pass, -}) -xc = cacher.wrap(xc, { - directory = config.cache_dir, - prefix = (xc.server:gsub('%W', '_')), - time = 24*60*60, - functions = { - get_live_categories = true, - get_live_streams = true, - get_vod_categories = true, - get_vod_streams = true, - get_vod_info = true, - get_series_categories = true, - get_series = true, - get_series_info = true, - get_epg = true, - }, -}) - -local catalogue = _catalogue.new() -local epg = _epg.new() local osd local function dl_img(url, path, cb) @@ -71,10 +46,43 @@ osd = _osd.new({ }) local ctx = { - catalogue = catalogue, - epg = epg, - xc = xc, + catalogue = _catalogue.new(), + src = {}, + src_order = {}, } +for _, v in ipairs(config.src_order) do + local src = config.src[v] + local xc = _xc.new({ + url = src.url, + user = src.user, + pass = src.pass, + }) + xc = cacher.wrap(xc, { + directory = config.cache_dir, + prefix = (xc.url:gsub('%W', '_')), + time = 24*60*60, + functions = { + get_live_categories = true, + get_live_streams = true, + get_vod_categories = true, + get_vod_streams = true, + get_vod_info = true, + get_series_categories = true, + get_series = true, + get_series_info = true, + get_epg = true, + }, + }) + + ctx.src[v] = { + id = v, + name = src.name, + xc = xc, + epg = _epg.new(), + } + ctx.src_order[#ctx.src_order+1] = v +end + rt.init(state, osd, ctx) rx.init(state, osd, ctx) @@ -216,6 +224,6 @@ input.activate(true) mp.add_timeout(0, function() rt.load_data() state.depth = 0 - rt.push_group_menu(catalogue:get('root')) + rt.push_group_menu(ctx.catalogue:get('root')) osd:redraw(state) end) @@ -4,6 +4,7 @@ local config = require('config') local input = require('input') local rx = require('rx') local util = require('util') +local _catalogue = require('catalogue') local rt = {} @@ -33,16 +34,18 @@ end local function series_children(series) return rx.series_children( series, - ctx.xc:with_opts('get_series_info', series.series_id, + ctx.src[series.src_id].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) +local function catalogue_add_section(src_id, sect, cats, elems) ctx.catalogue:add({ + src_id = src_id, section = sect.id, type = 'group', - group_type = 'category', - id = sect.id .. ':category:0', + group_type = 'cat', + id = src_id .. ':' .. sect.id .. ':cat:0', parent_id = 'root', name = sect.name, }) @@ -51,26 +54,32 @@ local function catalogue_add_section(sect, cats, elems) -- before their parent category for _, v in ipairs(cats) do ctx.catalogue:add({ + src_id = src_id, section = sect.id, type = 'group', - group_type = 'category', - id = sect.id .. ':category:' .. v.category_id, - parent_id = sect.id .. ':category:' .. v.parent_id, + group_type = 'cat', + id = src_id .. ':' .. sect.id .. ':cat:' .. + v.category_id, + parent_id = src_id .. ':' .. sect.id .. ':cat:' .. + v.parent_id, name = util.strip(v.category_name), }) end for _, v in ipairs(elems) do local vv = { + src_id = src_id, section = sect.id, - parent_id = sect.id .. ':category:' .. v.category_id, + parent_id = src_id .. ':' .. sect.id .. ':cat:' .. + 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.id = src_id .. ':' .. sect.id .. ':series:' .. + v.series_id vv.series_id = v.series_id vv.img_url = util.strip_ne(v.cover) vv.count = 1 @@ -78,7 +87,8 @@ local function catalogue_add_section(sect, cats, elems) vv.children_f = series_children else vv.type = 'stream' - vv.id = sect.id .. ':stream:' .. v.stream_id + vv.id = src_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) @@ -89,35 +99,12 @@ local function catalogue_add_section(sect, cats, elems) 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, - }) - +function rt.load_data_src(src, force) + local pre = src.name and src.name .. ' | ' or '' local arr = { - {id = 'live', name = 'Live TV', type = 'live'}, - {id = 'movie', name = 'Movies', type = 'vod'}, - {id = 'series', name = 'Series', type = 'series'}, + {id = 'live', name = pre .. 'Channels', type = 'live'}, + {id = 'movie', name = pre .. 'Movies', type = 'vod'}, + {id = 'series', name = pre .. 'Series', type = 'series'}, } local base_str = 'Loading catalogue' @@ -142,20 +129,50 @@ function rt.load_data(force) } for _, sect in ipairs(arr) do sect_str = base_str .. ' » ' .. sect.name - local cats = ctx.xc:with_opts( + local cats = src.xc:with_opts( 'get_' .. sect.type .. '_categories', call_opts) - local elems = ctx.xc:with_opts( + local elems = src.xc:with_opts( sect.type == 'series' and 'get_series' or ('get_' .. sect.type .. '_streams'), call_opts) - catalogue_add_section(sect, cats, elems) + catalogue_add_section(src.id, sect, cats, elems) end osd:set_status('Loading EPG...') osd:redraw(state) - ctx.epg:load_xc_data( - ctx.xc:with_opts('get_epg', {force = not not force})) + src.epg:load_xc_data( + src.xc:with_opts('get_epg', {force = not not force})) osd:set_status() +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, + }) + + for _, v in ipairs(ctx.src_order) do + rt.load_data_src(ctx.src[v], force) + end local t = util.read_json_file(config.favourites_file) state.favourites = t.favourites or {} @@ -352,7 +369,8 @@ end local function play_stream(stream) local url = stream.stream_url or - ctx.xc:stream_url(stream.stream_type, stream.stream_id) + ctx.src[stream.src_id].xc:stream_url( + stream.stream_type, stream.stream_id) if not url then return end @@ -459,7 +477,7 @@ end local function open_option_channel_epg(opt) local ch = opt.epg_channel_id:lower() - local options, idx = rx.menu_options_channel_epg(ch) + local options, idx = rx.menu_options_channel_epg(opt.src_id, ch) if not options then return end @@ -490,13 +508,15 @@ local function open_option_entry_info(title, info) end local function open_option_movie_info(opt) - local info = ctx.xc:with_opts('get_vod_info', opt.stream_id, + local info = ctx.src[opt.src_id].xc:with_opts( + 'get_vod_info', opt.stream_id, cache_miss_status_msg('Loading movie 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, + local info = ctx.src[opt.src_id].xc:with_opts( + 'get_series_info', opt.series_id, cache_miss_status_msg('Loading series info...')) open_option_entry_info('Series Info: ' .. opt.name, info) end @@ -661,7 +681,6 @@ function rt.cancel_search() return end - menu.search_active = false state.depth = state.depth - 1 osd:dirty() input.set_mapping('MENU') @@ -30,7 +30,8 @@ function rx.series_children(series, info) name = util.strip(episode.title), type = 'stream', stream_type = 'series', - id = series.section .. ':stream:' .. + id = series.src_id .. ':' .. + series.section .. ':stream:' .. episode.id, stream_id = episode.id, img_url = util.strip_ne( @@ -57,7 +58,9 @@ function rx.series_children(series, info) local t = { type = 'group', group_type = 'season', - id = series.id .. ':season:' .. season.id, + id = series.src_id .. ':' .. series.section .. + ':season:' .. series.series_id .. '-' .. + season.id, children = episodes, name = util.strip(season.name), info = count, @@ -95,14 +98,15 @@ local entry_mt = { return t._info end - local prog = ctx.epg:scheduled_programme( + local epg = ctx.src[v.src_id].epg + local prog = 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( + prog = epg:next_programme( v.epg_channel_id, time) exp = prog and prog.start end @@ -266,8 +270,8 @@ local programme_mt = { end, } -function rx.menu_options_channel_epg(ch) - local progs = ctx.epg:channel_programmes(ch) +function rx.menu_options_channel_epg(src_id, ch) + local progs = ctx.src[src_id].epg:channel_programmes(ch) if not progs then return end @@ -7,7 +7,7 @@ local mt = {} mt.__index = mt function xc.new(t) - assert(t.server) + assert(t.url) assert(t.user) assert(t.pass) @@ -16,7 +16,7 @@ end function mt:get(path, params) local url = - self.server .. path .. + self.url .. path .. '?username=' .. self.user .. '&password=' .. self.pass for k, v in pairs(params or {}) do @@ -110,12 +110,12 @@ end function mt:stream_url(stream_type, stream_id) if stream_type == 'series' then - return self.server .. '/series/' .. + return self.url .. '/series/' .. self.user .. '/' .. self.pass .. '/' .. stream_id .. '.vod' else - return self.server .. '/' .. + return self.url .. '/' .. self.user .. '/' .. self.pass .. '/' .. stream_id |
