jellyfin.lua - mpv-jellyfin - MPV script for adding an interface for Jellyfin.
 (HTM) git clone git://jay.scot/mpv-jellyfin
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) README
 (DIR) LICENSE
       ---
       jellyfin.lua (7977B)
       ---
            1 local opt = require 'mp.options'
            2 local utils = require 'mp.utils'
            3 local msg = require 'mp.msg'
            4 
            5 local options = {
            6         url = "",
            7         username = "",
            8         password = "",
            9         image_path = "",
           10         hide_spoilers = "on",
           11         show_by_default = ""
           12 }
           13 opt.read_options(options, mp.get_script_name())
           14 
           15 local overlay = mp.create_osd_overlay("ass-events")
           16 local meta_overlay = mp.create_osd_overlay("ass-events")
           17 local shown = false
           18 local user_id = ""
           19 local api_key = ""
           20 
           21 local parent_id = {"", "", "", ""}
           22 local selection = {1, 1, 1, 1}
           23 local list_start = {1, 1, 1, 1}
           24 local layer = 1
           25 
           26 local items = {}
           27 local ow, oh, op = 0, 0, 0
           28 local video_id = ""
           29 local async = nil
           30 
           31 local toggle_overlay -- function
           32 
           33 local function send_request(method, url)
           34         if #api_key > 0 then
           35                 local request = mp.command_native({
           36                         name = "subprocess",
           37                         capture_stdout = true,
           38                         capture_stderr = true,
           39                         playback_only = false,
           40                         args = {"curl", "-X", method, url}
           41                 })
           42                 return utils.parse_json(request.stdout)
           43         end
           44         return nil
           45 end
           46 
           47 local function line_break(str, flags, space)
           48         if str == nil then return "" end
           49         local text = flags
           50         local n = 0
           51         for i = 1, #str do
           52                 local c = str:sub(i, i)
           53                 if (c == ' ' and i-n > space) or c == '\n' then
           54                         text = text..str:sub(n, i-1).."\n"..flags
           55                         n = i+1
           56                 end
           57         end
           58         text = text..str:sub(n, -1)
           59         return text
           60 end
           61 
           62 local function update_list()
           63         overlay.data = ""
           64         local magic_num = 29 -- const
           65         if selection[layer] - list_start[layer] > magic_num then
           66                 list_start[layer] = selection[layer] - magic_num
           67         elseif selection[layer] - list_start[layer] < 0 then
           68                 list_start[layer] = selection[layer]
           69         end
           70         for i=list_start[layer],list_start[layer]+magic_num do
           71                 if i > #items then break end
           72                 local index = ""
           73                 if items[i].IndexNumber and items[i].IsFolder == false then
           74                         index = items[i].IndexNumber..". "
           75                 else
           76                         -- nothing
           77                 end
           78                 if i == selection[layer] then
           79                         overlay.data = overlay.data.."{\\fs16}{\\c&HFF&}"..index..items[i].Name.."\n"
           80                 else
           81                         overlay.data = overlay.data.."{\\fs16}"..index..items[i].Name.."\n"
           82                 end
           83         end
           84         overlay:update()
           85 end
           86 
           87 local scale = 2 -- const
           88 
           89 local function show_image(success, result, error, userdata)
           90         if success == true and shown == true then
           91                 mp.command_native({
           92                         name = "overlay-add",
           93                         id = 0,
           94                         x = math.floor(ow/2.5),
           95                         y = 10,
           96                         file = userdata[3],
           97                         offset = 0,
           98                         fmt = "bgra",
           99                         w = userdata[1],
          100                         h = userdata[2],
          101                         stride = userdata[1]*4,
          102                         dw = userdata[1]*scale,
          103                         dh = userdata[2]*scale
          104                 })
          105         end
          106 end
          107 
          108 local function update_image(item)
          109         local width = math.floor(ow/(3*scale))
          110         local height = 0
          111         local filepath = ""
          112         mp.commandv("overlay-remove", "0")
          113         if item.ImageTags.Primary ~= nil then
          114                 height = math.floor(width/item.PrimaryImageAspectRatio)
          115                 filepath = options.image_path.."/"..item.Id.."_"..width.."_"..height..".bgra"
          116                 if async ~= nil then mp.abort_async_command(async) end
          117                 async = mp.command_native_async({
          118                         name = "subprocess",
          119                         playback_only = false,
          120                         args = { "mpv", options.url.."/Items/"..item.Id.."/Images/Primary?api_key="..api_key.."&width="..width.."&height="..height, "--no-config", "--msg-level=all=no", "--vf=lavfi=[format=bgra]", "--of=rawvideo", "--o="..filepath }
          121                 }, function(success, result, error) show_image(success, result, error, {width, height, filepath}) end)
          122         end
          123 end
          124 
          125 local function update_metadata(item)
          126         meta_overlay.data = ""
          127         local name = line_break(item.Name, "{\\a7}{\\fs24}", 30)
          128         meta_overlay.data = meta_overlay.data..name.."\n"
          129         local year = ""
          130         if item.ProductionYear then year = item.ProductionYear end
          131         local time = ""
          132         if item.RunTimeTicks then time = "   "..math.floor(item.RunTimeTicks/600000000).."m" end
          133         local rating = ""
          134         if item.CommunityRating then rating = "   "..item.CommunityRating end
          135         local hidden = ""
          136         local watched = ""
          137         if item.UserData.Played == false then
          138                 if options.hide_spoilers ~= "off" then hidden = "{\\bord0}{\\1a&HFF&}" end
          139         else
          140                 watched = "   Watched"
          141         end
          142         local favourite = ""
          143         if item.UserData.IsFavorite == true then
          144                 favourite = "   Favorite"
          145         end
          146         meta_overlay.data = meta_overlay.data.."{\\a7}{\\fs16}"..year..time..rating..watched..favourite.."\n\n"
          147         local tagline = line_break(item.Taglines[1], "{\\a7}{\\fs20}", 35)
          148         meta_overlay.data = meta_overlay.data..tagline.."\n"
          149         local description = line_break(item.Overview, "{\\a7}{\\fs16}"..hidden, 45)
          150         meta_overlay.data = meta_overlay.data..description
          151         meta_overlay:update()
          152 end
          153 
          154 local function update_data()
          155         update_list()
          156         local item = items[selection[layer]]
          157         update_image(item)
          158         update_metadata(item)
          159 end
          160 
          161 local function update_overlay()
          162         overlay.data = "{\\fs16}Loading..."
          163         overlay:update()
          164         local url = options.url.."/Items?api_key="..api_key.."&userID="..user_id.."&parentId="..parent_id[layer].."&enableImageTypes=Primary&imageTypeLimit=1&fields=PrimaryImageAspectRatio,Taglines,Overview"
          165         if layer == 2 then
          166                 url = url.."&sortBy=SortName"
          167         else
          168                 -- nothing
          169         end
          170         items = send_request("GET", url).Items
          171         ow, oh, op = mp.get_osd_size()
          172         update_data()
          173 end
          174 
          175 local function width_change(name, data)
          176         if shown then update_overlay() end
          177 end
          178 
          179 local function play_video()
          180         toggle_overlay()
          181         mp.commandv("loadfile", options.url.."/Videos/"..video_id.."/stream?static=true&api_key="..api_key)
          182         mp.set_property("force-media-title", items[selection[layer]].Name)
          183 end
          184 
          185 local function key_up()
          186         if #items > 1 then
          187                 selection[layer] = selection[layer] - 1
          188                 if selection[layer] == 0 then selection[layer] = #items end
          189                 update_data()
          190         end
          191 end
          192 
          193 local function key_right()
          194         if items[selection[layer]].IsFolder == false then
          195                 video_id = items[selection[layer]].Id
          196                 play_video()
          197         else
          198                 layer = layer + 1 -- shouldn't get too big
          199                 parent_id[layer] = items[selection[layer-1]].Id
          200                 selection[layer] = 1
          201                 update_overlay()
          202         end
          203 end
          204 
          205 local function key_down()
          206         if #items > 1 then
          207                 selection[layer] = selection[layer] + 1
          208                 if selection[layer] > #items then selection[layer] = 1 end
          209                 update_data()
          210         end
          211 end
          212 
          213 local function key_left()
          214         if layer == 1 then return end
          215         layer = layer - 1
          216         update_overlay()
          217 end
          218 
          219 local function connect()
          220         local request = mp.command_native({
          221                 name = "subprocess",
          222                 capture_stdout = true,
          223                 capture_stderr = true,
          224                 playback_only = false,
          225                 args = {"curl", options.url.."/Users/AuthenticateByName", "-H", "accept: application/json", "-H", "content-type: application/json", "-H", "x-emby-authorization: MediaBrowser Client=\"Custom Client\", Device=\"Custom Device\", DeviceId=\"1\", Version=\"0.0.1\"", "-d", "{\"username\":\""..options.username.."\",\"Pw\":\""..options.password.."\"}"}
          226         })
          227         local result = utils.parse_json(request.stdout)
          228         user_id = result.User.Id
          229         api_key = result.AccessToken
          230 end
          231 
          232 toggle_overlay = function()
          233         if shown then
          234                 mp.remove_key_binding("jup")
          235                 mp.remove_key_binding("jright")
          236                 mp.remove_key_binding("jdown")
          237                 mp.remove_key_binding("jleft")
          238                 mp.commandv("overlay-remove", "0")
          239                 overlay:remove()
          240                 meta_overlay:remove()
          241         else
          242                 mp.add_forced_key_binding("UP", "jup", key_up, { repeatable = true })
          243                 mp.add_forced_key_binding("RIGHT", "jright", key_right)
          244                 mp.add_forced_key_binding("DOWN", "jdown", key_down, { repeatable = true })
          245                 mp.add_forced_key_binding("LEFT", "jleft", key_left)
          246                 if #api_key <= 0 then connect() end
          247                 if #items == 0 then
          248                         update_overlay()
          249                 else
          250                         update_data()
          251                 end
          252         end
          253         shown = not shown
          254 end
          255 
          256 local function check_percent()
          257         local pos = mp.get_property_number("percent-pos")
          258         if pos then
          259                 if pos > 95 and #video_id > 0 then
          260                         send_request("POST", options.url.."/Users/"..user_id.."/PlayedItems/"..video_id.."?api_key="..api_key)
          261                         items[selection[layer]].UserData.Played = true
          262                         video_id = ""
          263                 end
          264         end
          265 end
          266 
          267 local function unpause()
          268         mp.set_property_bool("pause", false)
          269 end
          270 
          271 os.execute("mkdir -p "..options.image_path)
          272 mp.add_periodic_timer(1, check_percent)
          273 mp.add_key_binding("Ctrl+j", "jf", toggle_overlay)
          274 mp.observe_property("osd-width", "number", width_change)
          275 mp.register_event("end-file", unpause)
          276 if options.show_by_default == "on" then toggle_overlay() end