initial commit
This commit is contained in:
41
utils/curl.lua
Normal file
41
utils/curl.lua
Normal file
@ -0,0 +1,41 @@
|
||||
local mp = require('mp')
|
||||
local utils = require('mp.utils')
|
||||
local msg = require('mp.msg')
|
||||
|
||||
local M = {}
|
||||
|
||||
--- run a curl request
|
||||
---@param cmd table list of curl args
|
||||
---@param method? string
|
||||
---@return table? json, number? httpcode
|
||||
function M.request(cmd, method)
|
||||
if type(cmd) == "string" then
|
||||
cmd = { cmd }
|
||||
end
|
||||
|
||||
table.insert(cmd, 1, '%{http_code}')
|
||||
table.insert(cmd, 1, "-w")
|
||||
if method then
|
||||
table.insert(cmd, 1, method)
|
||||
table.insert(cmd, 1, "-X")
|
||||
end
|
||||
table.insert(cmd, 1, "curl")
|
||||
|
||||
local r = mp.command_native({
|
||||
name = "subprocess",
|
||||
capture_stdout = true,
|
||||
capture_stderr = true,
|
||||
playback_only = false,
|
||||
args = cmd
|
||||
})
|
||||
|
||||
local json, err, code = utils.parse_json(r.stdout, true)
|
||||
if not json then
|
||||
msg.error(string.format("failed to parse json: %s", err))
|
||||
return
|
||||
end
|
||||
|
||||
return json, tonumber(code)
|
||||
end
|
||||
|
||||
return M
|
497
utils/jellyfin.lua
Normal file
497
utils/jellyfin.lua
Normal file
@ -0,0 +1,497 @@
|
||||
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
|
Reference in New Issue
Block a user