local mp = require('mp') local utils = require('mp.utils') local msg = require('mp.msg') local curl = require("utils.curl") local M = {} --- refrence to runtime table ---@type table local rt = {} --- refrence to settings table ---@type table local settings = {} --- refrence to list table ---@type table local list = {} --- user id + apikey ---@type string local user_api --- function which returns a library table (a struct) ---@return table library function M.library() return { --- library id ---@type string|nil id = nil, --- name of library ---@type string name = "", --- position in library ---@type number pos = 1 } end ---table of generic actions on http return codes ---@type table local http = { [401] = function() msg.trace("return code 401 encoutered retrying authentication") rt.authenticated = M.authenticate() if not rt.authenticated then msg.trace("authentication failed, good luck") end end, [403] = function() msg.trace("return code 403 encoutered retrying authentication") rt.authenticated = M.authenticate() if not rt.authenticated then msg.trace("authentication failed, good luck") end end } --- function which returns a video table (a struct) ---@return table video function M.video() return { --- id of video ---@type string|nil id = nil, --- progress through video in seconds ---@type number time = 0 } end --- return jellyfin ticks ---@param s number seconds ---@return number ticks function M.seconds2ticks(s) return s * 10000000 end --- setup jellyfin api ---@param _rt table runtime ---@param _settings table settings function M.setup(_list, _rt, _settings) list = _list rt = _rt settings = _settings msg.trace("initialized the jellyfin api") end --- attempt to authenticate the user with their specified server ---@return boolean authenticated true if successfully authenticated function M.authenticate() local result, code = curl.request({ settings.url.."/Users/AuthenticateByName", "-H", "accept: application/json", "-H", "content-type: application/json", "-H", 'x-emby-authorization: MediaBrowser Client="' ..settings.client.name..'", Device="'..settings.client.device ..'", DeviceId="1", Version="'..settings.client.version..'"', "-d", '{"username":"'..settings.username..'","Pw":"'..settings.password ..'"}', }) -- failed to get anything if not result and not code then msg.error(string.format( "failed to connect to jellyfin server at: %s, no response", settings.url)) return false elseif not result and code then msg.error(string.format( "failed to connect to jellyfin server at: %s, error code %s", settings.url, code)) return false end msg.trace(string.format("successfully connected to %s", settings.url)) -- load the userinfo table into the runtime table rt.user = result if not rt.user then msg.error(string.format("something weird happened: %s", utils.to_string(result))) -- something has gone wrong, this could mean that we have not authenticated correctly return false end -- populate user api string user_api = "?api_key="..rt.user.AccessToken.."&userID="..rt.user.User.Id return true end --- get the current position in the library ---@return table ret new list to display function M.getpos() ---@type table local ret, libraries, items, seasons, episodes ---@type number? local code -- If you're not in a library you're on the homepage if not rt.library.id then libraries, code = curl.request(settings.url.."/Items"..user_api, "GET") ret = libraries.Items -- If you're not viewing a movie or show you're viewing a library elseif not rt.title.id then items, code = curl.request(settings.url.."/Items"..user_api.."&parentId=".. rt.library.id.."&sortBy=SortName", "GET") ret = items.Items -- if you're not viewing a season of a show you're viewing a list of seasons elseif not rt.season.id then seasons, code = curl.request(settings.url.."/Items"..user_api.. "&parentId="..rt.title.id, "GET") ret = seasons.Items -- you're viewing a list of episodes in a season else episodes, code = curl.request(settings.url.."/Items"..user_api.. "&parentId="..rt.season.id, "GET") ret = episodes.Items end if code == 200 then msg.trace("successfully retrieved information at position") elseif code == 401 then msg.error("Unauthorizied to get information at position") elseif code == 403 then msg.error("Forbidden from getting information at position") end return ret end --- play file based on rt.video.id function M.play() msg.trace(string.format("playing movie with id %s", rt.video.id)) local ok, err = mp.commandv("loadfile", settings.url.."/Videos/"..rt.video.id .."/stream"..user_api.."&static=true") if not ok then return end mp.set_property("force-media-title", rt.menu.items[list.selected].Name) -- we started playing this file local _, code curl.request(settings.url.."/PlayingItems/"..rt.video.id..user_api.. "&canSeek=true", "POST") if code == 204 then msg.trace("play start recorded on server") elseif code == 401 then msg.error("Unauthorizied to report playback start") elseif code == 403 then msg.error("Forbidden from get reporting playback start") end -- try some generic actions that may help. wrapped in pcall so nothing stupid -- can happen (fingers crossed) pcall(http[code]) -- close ui after playback starts list:close() --- callback when the file has been loaded local function file_loaded() if rt.menu.items[list.selected].UserData.PlayedPercentage then mp.commandv("seek", rt.menu.items[list.selected].UserData.PlayedPercentage, "absolute-percent") end M.reportplayback() -- unregister event mp.unregister_event(file_loaded) end -- when the file has loaded seek to last positon if there is one mp.register_event("file-loaded", file_loaded) end --- set current item's played status ---@param bool boolean played function M.setplayed(bool) ---@type number? local code ---@type string? local ret -- if no item selected leave if not rt.menu.items[list.selected].Id then return end if bool == true then -- if the video isn't already played set it played if rt.menu.items[list.selected].UserData.Played == false then ret, code = curl.request(settings.url.."/UserPlayedItems/" ..rt.menu.items[list.selected].Id..user_api, "POST") end elseif bool == false then -- if the video isn't unplayed set it to unplayed if rt.menu.items[list.selected].UserData.Played == true then ret, code = curl.request(settings.url.."/UserPlayedItems/" ..rt.menu.items[list.selected].Id..user_api, "DELETE") end end -- check if the server has recieved the request and update accordingly if not code then msg.error(string.format("couldn't mark item '%s' as played", rt.menu.items[list.selected].Name)) return elseif code == 200 then msg.trace("new user data recieved updating info") rt.menu.items[list.selected].UserData = ret elseif code == 401 then msg.error("unauthorizied to get user data") elseif code == 403 then msg.error("forbidden from getting user data") elseif code == 404 then msg.error(string.format("item not found on %s", rt.menu.items[list.selected].Name)) end -- try some generic actions that may help. wrapped in pcall so nothing -- stupid can happen (fingers crossed) pcall(http[code]) end --- set current items favorite status ---@param bool boolean favorite function M.setfavorite(bool) ---@type number? local code ---@type string? local ret -- if no item selected leave if not rt.menu.items[list.selected].Id then return end if bool == true then -- if the video isn't already favorite set it as a favorite if rt.menu.items[list.selected].UserData.IsFavorite == false then ret, code = curl.request(settings.url.."/UserFavoriteItems/" ..rt.menu.items[list.selected].Id..user_api, "POST") end elseif bool == false then -- if the video isn't a favorite unfavorite it if rt.menu.items[list.selected].UserData.IsFavorite == true then ret, code = curl.request(settings.url.."/UserFavoriteItems/" ..rt.menu.items[list.selected].Id..user_api, "DELETE") end end -- check if the server has recieved the request and update accordingly if not code then msg.error(string.format("couldn't mark item '%s' as favorite", rt.menu.items[list.selected].Name)) return elseif code == 200 then msg.trace("new user data recieved updating info") rt.menu.items[list.selected].UserData = ret elseif code == 401 then msg.error("unauthorizied to get user data") elseif code == 403 then msg.error("forbidden from getting user data") end -- try some generic actions that may help. wrapped in pcall so nothing -- stupid can happen (fingers crossed) pcall(http[code]) end --- start playback reporting function M.reportplayback() --- table of events used to keep the server up to date with what's happening --- on the client ---@type table local A = {} --- last percent that we reported to the server ---@type number local last_reported_percent --- listen to an event and run a callback on change ---@param property string mpv native property to listen to ---@param callback function function callback local function listen(property, callback) A[#A + 1] = { property = property, callback = callback } end local function elapsedticks() local timepos = mp.get_property_native("time-pos") if not timepos then return nil end -- convert the elapsed seconds to ticks (not sure what ticks are I just -- know that 1 seconds is equal to 10,000,000 ticks) return M.seconds2ticks(math.floor(timepos)) end local function paused() if mp.get_property_native("pause") then return "true" else return "false" end end listen("percent-pos", function(name) local value = mp.get_property_native(name) if not value then return end local percent = math.floor(value) if percent ~= last_reported_percent then local ticks = elapsedticks() if not ticks then msg.trace("failed to report playback time to the server: unable to get time-pos") return end -- send off percent position to the server local _, code = curl.request(settings.url.."/Users/"..rt.user.User.Id.. "/PlayingItems/"..rt.video.id.."/Progress/?api_key=".. rt.user.AccessToken.."&positionTicks="..ticks.."&isPaused="..paused(), "POST") if code == 204 then msg.trace("playback percent recorded on server") elseif code == 401 then msg.error("Unauthorizied to report playback percent") elseif code == 403 then msg.error("Forbidden from get reporting playback percent") end -- try some generic actions that may help. wrapped in pcall so nothing -- stupid can happen (fingers crossed) pcall(http[code]) -- set our last reported percent to the current percent last_reported_percent = percent msg.trace(string.format("reported %s%% as last known position to the server", last_reported_percent)) end end) listen("pause", function(name) ---@type number? local code local value = paused() local ticks = elapsedticks() if not ticks then msg.trace("reported pause may be inaccurate: failed to get time-pos") -- fallback to just sending a pause without the current position if we're -- unable to get the current position _, code = curl.request(settings.url.."/Users/"..rt.user.User.Id.. "/PlayingItems/"..rt.video.id.."/Progress/?api_key=".. rt.user.AccessToken.."&isPaused="..value, "POST") goto error end -- send pause with the current position _, code = curl.request(settings.url.."/Users/"..rt.user.User.Id.. "/PlayingItems/"..rt.video.id.."/Progress/?api_key=".. rt.user.AccessToken.."&isPaused="..value.."&positionTicks="..ticks, "POST") ::error:: if code == 204 then msg.trace("pause recorded on server") elseif code == 401 then msg.error("Unauthorizied to report pause") elseif code == 403 then msg.error("Forbidden from get reporting pause") end -- try some generic actions that may help. wrapped in pcall so nothing -- stupid can happen (fingers crossed) pcall(http[code]) end) local function endfile(info) ---@type number? local code local ticks = elapsedticks() if not ticks then -- fallback to just telling the server we stopped watching without the -- current position if we're unable to get the current position _, code = curl.request(settings.url.."/Users/"..rt.user.User.Id.. "/PlayingItems/"..rt.video.id.."?api_key="..rt.user.AccessToken, "DELETE") msg.trace("reported stopping time may be inaccurate: failed to get time-pos") else _, code = curl.request(settings.url.."/Users/"..rt.user.User.Id.. "/PlayingItems/"..rt.video.id.."?api_key="..rt.user.AccessToken.. "&positionTicks="..ticks, "DELETE") msg.trace("reported stopping time successfully") end if code == 204 then msg.trace("play end recorded on server") elseif code == 401 then msg.error("Unauthorizied to report play end") elseif code == 403 then msg.error("Forbidden from get reporting play end") end -- try some generic actions that may help. wrapped in pcall so nothing -- stupid can happen (fingers crossed) pcall(http[code]) msg.trace(string.format("ending reason: %s", info.reason)) -- unregister all events for the video mp.unregister_event(endfile) for _, val in pairs(A) do mp.unobserve_property(val.callback) end end -- register end file event mp.register_event("end-file", endfile) -- register all listeners for _, val in pairs(A) do mp.observe_property(val.property, "native", val.callback) end end return M