diff options
Diffstat (limited to '')
-rw-r--r-- | README.md | 66 | ||||
-rw-r--r-- | main.lua | 402 | ||||
-rw-r--r-- | utils/curl.lua | 41 | ||||
-rw-r--r-- | utils/jellyfin.lua | 497 |
4 files changed, 1006 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..fda11ce --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +# JnJ - Jellies and Jams +A jellyfin client built into mpv +## Configuring +Every option in JnJ may be configured via a script-message. Example: +```lua +local mp = require("mp") +local utils = require("mp.utils") + +-- action keybind +mp.commandv("script-message", "jnj-bind-toggle", "Ctrl+j") +mp.commandv("script-message", "jnj-bind-enter", "l") +mp.commandv("script-message", "jnj-bind-leave", "h") +mp.commandv("script-message", "jnj-bind-down", "j") +mp.commandv("script-message", "jnj-bind-up", "k") +mp.commandv("script-message", "jnj-bind-toggle-played", "m") +mp.commandv("script-message", "jnj-bind-toggle-favorite", "s") + +-- send user login info (this should be done through a script message to ensure +-- other scripts cannot leach your password from the script-message +mp.commandv("script-message-to", "jellies_and_jams" "jnj-set-settings", + utils.format_json({ + url = 'http://localhost:8096', + username = 'username', + password = 'password' + })) +``` + +### Available actions +- jnj-set-settings +- jnj-bind-toggle +- jnj-bind-open +- jnj-bind-close +- jnj-bind-leave +- jnj-bind-down +- jnj-bind-up +- jnj-bind-toggle-played +- jnj-bind-toggle-favorite +- jnj-bind-top +- jnj-bind-bottom +- jnj-bind-pagedown +- jnj-bind-pageup + +`jnj-bind-*` takes a keybind as their only argument + +`jnj-set-settings` takes a json formatted string of the following structure: +```json +{ + "url":"", + "username":"", + "password":"", + "header":{ + "prefix": "jellyfin/", + "separator":"/", + "contents":[ + ["watched", " ","favorite"," ","runtime"], + ["stars"," ","rating"," ","release"] + ] + }, + "client":{ + "name":"Jellyfin Lua Client", + "device":"Mpv", + "version":"1.1" + } +} +``` +All filled in values are provided by default, but may be changed by the user. diff --git a/main.lua b/main.lua new file mode 100644 index 0000000..b12807f --- /dev/null +++ b/main.lua @@ -0,0 +1,402 @@ +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) diff --git a/utils/curl.lua b/utils/curl.lua new file mode 100644 index 0000000..532d0bd --- /dev/null +++ b/utils/curl.lua @@ -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 diff --git a/utils/jellyfin.lua b/utils/jellyfin.lua new file mode 100644 index 0000000..1cb30e5 --- /dev/null +++ b/utils/jellyfin.lua @@ -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 |