498 lines
14 KiB
Lua
498 lines
14 KiB
Lua
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
|