local mp = require('mp') local msg = require('mp.msg') local utils = require('mp.utils') local jf = require("utils.jellyfin") -- require mpv scroll list library package.path = mp.command_native({"expand-path", "~~/script-modules/?.lua;"}) ..package.path local lok, list = pcall(require, "scroll-list") if not lok then msg.info("Install: https://github.com/CogentRedTester/mpv-scroll-list") return 1 end -- make an instance of the list local original_open = list.open function list:open() original_open(self) end local settings = { url = '', username = '', password = '', header = { prefix = "jellyfin/", separator = "/", contents = { { "watched", " ", "favorite", " ", "runtime" }, { "stars", " ", "rating", " ", "release" } } }, client = { name = 'Jellyfin Lua Client', device = 'Mpv', version = '1.1' } } --- table containing all info about the runtime ---@type table local rt = { authenticated = false, -- are we connected to the server user = {}, library = jf.library(), -- current library title = jf.library(), -- current movie or show season = jf.library(), -- current season of show video = jf.video(), -- current video (movie/episode) nextup = jf.video(), -- next video -- our menu to render menu = { items = {} -- contains table of strings to display (with ass styling) } } --- draw list to screen ---@param items table list of elements to draw to the screen local function redraw_list(items) -- clear out the render list list.list = {} -- style each line for i = 1, #items do local item = {} item.style = "" if items[i].UserData then -- if the user has started watching it make it blue if items[i].UserData.PlayedPercentage then if items[i].UserData.PlayedPercentage > 0 then item.style = [[{\c&Hdca400&}]]..item.style end -- if the user has finished watching it make it green elseif items[i].UserData.Played then item.style = [[{\c&H33ff66&}]]..item.style end end item.style = [[{\c&Hffffff&}]]..item.style item.ass = rt.menu.items[i].Name list:insert(item) end list:update() end function list:format_header_string() ---@type string local header -- shorten the selected item local sel = rt.menu.items[list.selected] -- setup topheading if not rt.library.id then header = settings.header.prefix elseif not rt.title.id then header = settings.header.prefix..rt.library.name..settings.header.separator elseif not rt.season.id then header = settings.header.prefix..rt.library.name..settings.header.separator ..rt.title.name else header = settings.header.prefix..rt.library.name..settings.header.separator ..rt.title.name..settings.header.separator..rt.season.name end -- generate x subheadings based on user config for i, v in pairs(settings.header.contents) do -- if no selected item exit if not rt.menu.items or not sel then goto finish end -- if this isn't the first line or the last line add a newline symbol and -- reset colors if i == 1 or i <= #settings.header.contents then header = header..[[\N\h{\q2\fs20\c&Hffffff&}]] end for _, c in pairs(v) do if not sel or not sel.UserData then goto continue end if type(c) == "function" then header = header..c(rt.menu.items, list.selected) elseif c == "watched" then if sel.UserData.PlayedPercentage then if sel.UserData.PlayedPercentage > 0 then header = header..[[{\c&Hdca400&}]].. math.floor(sel.UserData.PlayedPercentage)..[[%{\c&Hffffff&}]] end elseif sel.UserData.Played then header = header..[[{\c&H33ff66&}✔️{\c&Hffffff&}]] elseif sel.UserData.UnplayedItemCount then header = header..[[{\c&Hdca400&}]]..sel.UserData.UnplayedItemCount.. [[{\c&Hffffff&}]] else header = header..[[{\c&H444444&}✔️{\c&Hffffff&}]] end elseif c == "favorite" then if sel.UserData.IsFavorite then header = header..[[{\c&H0000ff&}♥️{\c&Hffffff&}]] else header = header..[[{\c&H444444&}♥️{\c&Hffffff&}]] end elseif c == "runtime" then if sel.RunTimeTicks then ---@type number local hour local min = math.floor(sel.RunTimeTicks / 600000000) if min >= 60 then hour = math.floor(min / 60) min = min - math.floor(min / 60) * 60 header = header..hour.."h "..min.."m" else header = header..min.."m" end end elseif c == "stars" then if sel.CommunityRating then local stars = math.floor(sel.CommunityRating * 10 + 0.5) / 10 header = header..[[{\c&H00ffff}★]]..stars..[[{\c&Hffffff&}]] end elseif c == "rating" then if sel.OfficialRating then header = header..[[{\c&Hffffff}[]]..sel.OfficialRating.. [[]{\c&Hffffff&}]] end elseif c == "release" then if sel.ProductionYear then header = header..sel.ProductionYear end elseif c == "entries" then header = header..#rt.menu.items else header = header..c end end ::continue:: end ::finish:: return header end --- change position in jellyfin local function enter() local sel = rt.menu.items[list.selected] if not sel then return end if sel.IsFolder == false then msg.trace("selected a video") rt.video.id = sel.Id -- start playing the movie jf.play() else if not rt.library.id then msg.trace("selected a library") rt.library.id = sel.Id rt.library.name = sel.Name rt.library.pos = list.selected elseif not rt.title.id then msg.trace("selected a movie") rt.title.id = sel.Id rt.title.name = sel.Name rt.title.pos = list.selected elseif not rt.season.id then msg.trace("selected a season") rt.season.id = sel.Id rt.season.name = sel.Name rt.season.pos = list.selected end list.selected = 1 end end --- helper to addkeys to the list view ---@param keys string keybind ---@param name string action name ---@param fn function callback ---@param opts? table options local function addkey(keys, name, fn, opts) opts = opts or { repeatable = true } local i = 1 for key in keys:gmatch("%S+") do table.insert(list.keybinds, { key, name..i, fn, opts }) i = i + 1 end end --- wrapper to safely open the list local function openlist() -- setup jellyfin api on first open if type(jf.setup) ~= "boolean" then jf.setup(list, rt, settings) -- mark jellyfin api as setup by replacing function with a boolean jf.setup = true end -- authenticate with the server based on users settings rt.authenticated = jf.authenticate() if not rt.authenticated then return end -- refresh the list rt.menu.items = jf.getpos() redraw_list(rt.menu.items) -- open the list list:open() end -- wrapper to safely close the list local function closelist() rt.authenticated = false list:close() end -- register script message for users so they may define keybinds mp.register_script_message("jnj-bind-open", function(key) mp.add_key_binding(key, 'JNJ-BIND-OPEN', openlist) end) mp.register_script_message("jnj-bind-close", function(key) addkey(key, 'close', closelist) end) mp.register_script_message("jnj-bind-toggle", function(key) mp.add_key_binding(key, 'JNJ-BIND-TOGGLE', function() if list.hidden then openlist() else closelist() end end) end) mp.register_script_message("jnj-bind-enter", function(key) addkey(key, 'enter', function() enter() local items = jf.getpos() if not items then return end rt.menu.items = items redraw_list(rt.menu.items) end) end) mp.register_script_message("jnj-bind-leave", function(key) addkey(key, 'leave', function() if not rt.library.id then -- do nothing we're already in the root elseif not rt.title.id then rt.library.id = nil list.selected = rt.library.pos elseif not rt.season.id then rt.title.id = nil list.selected = rt.title.pos else rt.season.id = nil list.selected = rt.season.pos end local items = jf.getpos() if not items then return end rt.menu.items = items redraw_list(rt.menu.items) end) end) mp.register_script_message("jnj-bind-down", function(key) addkey(key, 'down', function() list:scroll_down() end) end) mp.register_script_message("jnj-bind-up", function(key) addkey(key, 'up', function() list:scroll_up() end) end) mp.register_script_message("jnj-bind-pagedown", function(key) addkey(key, 'pageup', function() list:move_pagedown() end) end) mp.register_script_message("jnj-bind-pageup", function(key) addkey(key, 'pagedown', function() list:move_pageup() end) end) mp.register_script_message("jnj-bind-top", function(key) addkey(key, 'top', function() list:move_begin() end) end) mp.register_script_message("jnj-bind-bottom", function(key) addkey(key, 'bottom', function() list:move_end() end) end) mp.register_script_message("jnj-bind-toggle-played", function(key) addkey(key, 'bottom', function() if rt.menu.items[list.selected] and rt.menu.items[list.selected].Id and not rt.menu.items[list.selected].isFolder then jf.setplayed(not rt.menu.items[list.selected].UserData.Played) redraw_list(rt.menu.items) end end) end) mp.register_script_message("jnj-bind-toggle-favorite", function(key) addkey(key, 'bottom', function() if rt.menu.items[list.selected] and rt.menu.items[list.selected].Id and not rt.menu.items[list.selected].isFolder then jf.setfavorite(not rt.menu.items[list.selected].UserData.IsFavorite) redraw_list(rt.menu.items) end end) end) -- add script message for users to provide their settings via json mp.register_script_message("jnj-set-settings", function(json) local user_settings = utils.parse_json(json) for i, v in pairs(user_settings) do settings[i] = v end end)