| jellyfin.lua - mpv-jellyfin - MPV script for adding an interface for Jellyfin. | |
| git clone git://jay.scot/mpv-jellyfin | |
| Log | |
| Files | |
| Refs | |
| README | |
| 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 t… | |
| 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&}… | |
| 80 else | |
| 81 overlay.data = overlay.data.."{\\fs16}"..index..… | |
| 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.… | |
| 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.… | |
| 121 }, function(success, result, error) show_image(success, … | |
| 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.RunTimeT… | |
| 133 local rating = "" | |
| 134 if item.CommunityRating then rating = " "..item.CommunityRatin… | |
| 135 local hidden = "" | |
| 136 local watched = "" | |
| 137 if item.UserData.Played == false then | |
| 138 if options.hide_spoilers ~= "off" then hidden = "{\\bord… | |
| 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..t… | |
| 147 local tagline = line_break(item.Taglines[1], "{\\a7}{\\fs20}", 3… | |
| 148 meta_overlay.data = meta_overlay.data..tagline.."\n" | |
| 149 local description = line_break(item.Overview, "{\\a7}{\\fs16}"..… | |
| 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=".… | |
| 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.."/str… | |
| 182 mp.set_property("force-media-title", items[selection[layer]].Nam… | |
| 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 … | |
| 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 e… | |
| 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"… | |
| 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, { repeata… | |
| 243 mp.add_forced_key_binding("RIGHT", "jright", key_right) | |
| 244 mp.add_forced_key_binding("DOWN", "jdown", key_down, { r… | |
| 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/"..use… | |
| 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 |