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