Merge pull request #4 from EmperorPenguin18/testing - mpv-jellyfin - MPV script for adding an interface for Jellyfin.
 (DIR) Log
 (DIR) Files
 (DIR) Refs
 (DIR) README
 (DIR) LICENSE
       ---
 (DIR) commit 9220ad8195ee3e8f844bc1fb5f205c96c31f3450
 (DIR) parent 7aef1c8f0dfbf7c68456d03bde28d8f972c20617
 (HTM) Author: Sebastien MacDougall-Landry <60635017+EmperorPenguin18@users.noreply.github.com>
       Date:   Thu, 23 May 2024 21:38:51 -0400
       
       Merge pull request #4 from EmperorPenguin18/testing
       
       Merge changes from testing
       Diffstat:
         A PKGBUILD                            |      18 ++++++++++++++++++
         M README.md                           |       2 --
         M scripts/jellyfin.lua                |     193 +++++++++++++++++--------------
       
       3 files changed, 124 insertions(+), 89 deletions(-)
       ---
 (DIR) diff --git a/PKGBUILD b/PKGBUILD
       @@ -0,0 +1,18 @@
       +# Maintainer: Sebastien MacDougall-Landry
       +
       +pkgname=mpv-jellyfin
       +pkgver=1.1
       +pkgrel=1
       +pkgdesc='mpv plugin that turns it into a Jellyfin client'
       +url='https://github.com/EmperorPenguin18/mpv-jellyfin/'
       +source=("$pkgname-$pkgver.tar.gz::https://github.com/EmperorPenguin18/mpv-jellyfin/archive/refs/tags/$pkgver.tar.gz")
       +arch=('any')
       +license=('Unlicense')
       +depends=('mpv' 'curl')
       +sha256sums=('')
       +
       +package () {
       +  cd "$srcdir/$pkgname-$pkgver"
       +  install -Dm644 scripts/jellyfin.lua -t "$pkgdir/etc/mpv/scripts"
       +  install -Dm644 script-opts/jellyfin.conf -t "$pkgdir/etc/mpv/script-opts"
       +}
 (DIR) diff --git a/README.md b/README.md
       @@ -29,6 +29,4 @@ When you activate a video in the menu, it will begin to play that file.
        
        In general this is a very minimal script and isn't designed to be a full Jellyfin client. Changing settings or metadata has to be done from a real Jellyfin client.
        
       -Currently navigation is very slow because metadata is pulled in sequentially. You may want to create a playlist with stuff you want to watch to save yourself the time.
       -
        Thumbnails will accumulate if the selected image path isn't tmpfs. In addition thumbnails are raw bgra, which means they are less space efficient than the source images from the Jellyfin server.
 (DIR) diff --git a/scripts/jellyfin.lua b/scripts/jellyfin.lua
       @@ -16,18 +16,18 @@ local connected = false
        local shown = false
        local user_id = ""
        local api_key = ""
       -local library_id = nil
       -local library_selection = 1
       -local title_id = nil
       -local title_selection = 1
       -local season_id = nil
       -local season_selection = 1
       -local video_id = ""
       -local selection = 1
       +
       +local parent_id = {"", "", ""}
       +local selection = {1, 1, 1}
       +local layer = 1
       +
        local items = {}
        local ow, oh, op = 0, 0, 0
       +local video_id = ""
       +local async = nil
       +local list_start = 1
        
       -local toggle_overlay
       +local toggle_overlay -- function
        
        local function send_request(method, url)
                if connected then
       @@ -58,82 +58,115 @@ local function line_break(str, flags, space)
                return text
        end
        
       -local function update_data()
       +local function update_list()
                overlay.data = ""
       -        for _, item in ipairs(items) do
       -                if _ > selection - (53 / op) then
       -                        if _ < selection + (20 * op) then
       -                                if _ == selection then
       -                                        overlay.data = overlay.data.."{\\fs16}{\\c&HFF&}"..item.Name.."\n"
       -                                else
       -                                        overlay.data = overlay.data.."{\\fs16}"..item.Name.."\n"
       -                                end
       -                        end
       +        local magic_num = 29 -- const
       +        if selection[layer] - list_start > magic_num then
       +                list_start = selection[layer] - magic_num
       +        elseif selection[layer] - list_start < 0 then
       +                list_start = selection[layer]
       +        end
       +        for i=list_start,list_start+magic_num do
       +                if i > #items then break end
       +                local index = ""
       +                if items[i].IndexNumber and items[i].IsFolder == false then
       +                        index = items[i].IndexNumber..". "
       +                else
       +                        -- nothing
       +                end
       +                if i == selection[layer] then
       +                        overlay.data = overlay.data.."{\\fs16}{\\c&HFF&}"..index..items[i].Name.."\n"
       +                else
       +                        overlay.data = overlay.data.."{\\fs16}"..index..items[i].Name.."\n"
                        end
                end
                overlay:update()
       -        local id = items[selection].Id
       -        local width = math.floor(ow/3)
       +end
       +
       +local scale = 2 -- const
       +
       +local function show_image(success, result, error, userdata)
       +        if success == true then
       +                mp.command_native({
       +                        name = "overlay-add",
       +                        id = 0,
       +                        x = math.floor(ow/2.5),
       +                        y = 10,
       +                        file = userdata[3],
       +                        offset = 0,
       +                        fmt = "bgra",
       +                        w = userdata[1],
       +                        h = userdata[2],
       +                        stride = userdata[1]*4,
       +                        dw = userdata[1]*scale,
       +                        dh = userdata[2]*scale
       +                })
       +        end
       +end
       +
       +local function update_image(item)
       +        local width = math.floor(ow/(3*scale))
                local height = 0
                local filepath = ""
       -        for _, image in ipairs(send_request("GET", options.url.."/Items/"..id.."/Images?api_key="..api_key)) do
       -                if image.ImageType == "Primary" then
       -                        height = math.floor( width*(image.Height / image.Width) )
       -                        filepath = options.image_path.."/"..id.."_"..width.."_"..height..".bgra"
       -                        mp.command_native({
       -                                name = "subprocess",
       -                                playback_only = false,
       -                                args = { "mpv", options.url.."/Items/"..id.."/Images/Primary?api_key="..api_key, "--no-config", "--msg-level=all=no", "--vf=lavfi=[scale="..width..":"..height..",format=bgra]", "--of=rawvideo", "--o="..filepath }
       -                        })
       -                        break
       -                end
       -        end
                mp.commandv("overlay-remove", "0")
       -        mp.commandv("overlay-add", "0", tostring(math.floor(ow/2.5)), tostring(10), filepath, "0", "bgra", tostring(width), tostring(height), tostring(width*4))
       +        if item.ImageTags.Primary ~= nil then
       +                height = math.floor(width/item.PrimaryImageAspectRatio)
       +                filepath = options.image_path.."/"..item.Id.."_"..width.."_"..height..".bgra"
       +                if async ~= nil then mp.abort_async_command(async) end
       +                async = mp.command_native_async({
       +                        name = "subprocess",
       +                        playback_only = false,
       +                        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 }
       +                }, function(success, result, error) show_image(success, result, error, {width, height, filepath}) end)
       +        end
       +end
       +
       +local function update_metadata(item)
                meta_overlay.data = ""
       -        local metadata = send_request("GET", options.url.."/Users/"..user_id.."/Items/"..id.."?api_key="..api_key)
       -        local name = line_break(metadata.Name, "{\\a7}{\\fs24}", 30)
       +        local name = line_break(item.Name, "{\\a7}{\\fs24}", 30)
                meta_overlay.data = meta_overlay.data..name.."\n"
                local year = ""
       -        if metadata.ProductionYear then year = metadata.ProductionYear end
       +        if item.ProductionYear then year = item.ProductionYear end
                local time = ""
       -        if metadata.RunTimeTicks then time = "   "..math.floor(metadata.RunTimeTicks/600000000).."m" end
       +        if item.RunTimeTicks then time = "   "..math.floor(item.RunTimeTicks/600000000).."m" end
                local rating = ""
       -        if metadata.CommunityRating then rating = "   "..metadata.CommunityRating end
       +        if item.CommunityRating then rating = "   "..item.CommunityRating end
                local hidden = ""
                local watched = ""
       -        if metadata.UserData.Played == false then
       +        if item.UserData.Played == false then
                        if options.hide_spoilers ~= "off" then hidden = "{\\bord0}{\\1a&HFF&}" end
                else
                        watched = "   Watched"
                end
                local favourite = ""
       -        if metadata.UserData.IsFavorite == true then
       +        if item.UserData.IsFavorite == true then
                        favourite = "   Favorite"
                end
                meta_overlay.data = meta_overlay.data.."{\\a7}{\\fs16}"..year..time..rating..watched..favourite.."\n\n"
       -        local tagline = line_break(metadata.Taglines[1], "{\\a7}{\\fs20}", 35)
       +        local tagline = line_break(item.Taglines[1], "{\\a7}{\\fs20}", 35)
                meta_overlay.data = meta_overlay.data..tagline.."\n"
       -        local description = line_break(metadata.Overview, "{\\a7}{\\fs16}"..hidden, 45)
       +        local description = line_break(item.Overview, "{\\a7}{\\fs16}"..hidden, 45)
                meta_overlay.data = meta_overlay.data..description
                meta_overlay:update()
        end
        
       +local function update_data()
       +        update_list()
       +        local item = items[selection[layer]]
       +        update_image(item)
       +        update_metadata(item)
       +end
       +
        local function update_overlay()
                overlay.data = "{\\fs16}Loading..."
                overlay:update()
       -        local result
       -        if not library_id then
       -                result = send_request("GET", options.url.."/Items?api_key="..api_key.."&userID="..user_id)
       -        elseif not title_id then
       -                result = send_request("GET", options.url.."/Items?api_key="..api_key.."&userID="..user_id.."&parentId="..library_id.."&sortBy=SortName")
       -        elseif not season_id then
       -                result = send_request("GET", options.url.."/Items?api_key="..api_key.."&userID="..user_id.."&parentId="..title_id)
       +        local url = options.url.."/Items?api_key="..api_key.."&userID="..user_id.."&parentId="..parent_id[layer].."&enableImageTypes=Primary&imageTypeLimit=1&fields=PrimaryImageAspectRatio,Taglines,Overview"
       +        if layer == 2 then
       +                url = url.."&sortBy=SortName"
                else
       -                result = send_request("GET", options.url.."/Items?api_key="..api_key.."&userID="..user_id.."&parentId="..season_id)
       +                -- nothing
                end
       -        items = result.Items
       -        heights = {}
       +        items = send_request("GET", url).Items
                ow, oh, op = mp.get_osd_size()
                update_data()
        end
       @@ -145,54 +178,40 @@ end
        local function play_video()
                toggle_overlay()
                mp.commandv("loadfile", options.url.."/Videos/"..video_id.."/stream?static=true&api_key="..api_key)
       -        mp.set_property("force-media-title", items[selection].Name)
       +        mp.set_property("force-media-title", items[selection[layer]].Name)
        end
        
        local function key_up()
       -        selection = selection - 1
       -        if selection == 0 then selection = #items end
       -        update_data()
       +        if #items > 1 then
       +                selection[layer] = selection[layer] - 1
       +                if selection[layer] == 0 then selection[layer] = #items end
       +                update_data()
       +        end
        end
        
        local function key_right()
       -        if items[selection].IsFolder == false then
       -                video_id = items[selection].Id
       +        if items[selection[layer]].IsFolder == false then
       +                video_id = items[selection[layer]].Id
                        play_video()
                else
       -                if not library_id then
       -                        library_id = items[selection].Id
       -                        library_selection = selection
       -                elseif not title_id then
       -                        title_id = items[selection].Id
       -                        title_selection = selection
       -                elseif not season_id then
       -                        season_id = items[selection].Id
       -                        season_selection = selection
       -                end
       -                selection = 1
       +                layer = layer + 1 -- shouldn't get too big
       +                parent_id[layer] = items[selection[layer-1]].Id
       +                selection[layer] = 1
                        update_overlay()
                end
        end
        
        local function key_down()
       -        selection = selection + 1
       -        if selection > #items then selection = 1 end
       -        update_data()
       +        if #items > 1 then
       +                selection[layer] = selection[layer] + 1
       +                if selection[layer] > #items then selection[layer] = 1 end
       +                update_data()
       +        end
        end
        
        local function key_left()
       -        if not library_id then
       -                return
       -        elseif not title_id then
       -                library_id = nil
       -                selection = library_selection
       -        elseif not season_id then
       -                title_id = nil
       -                selection = title_selection
       -        else
       -                season_id = nil
       -                selection = season_selection
       -        end
       +        if layer == 1 then return end
       +        layer = layer - 1
                update_overlay()
        end
        
       @@ -237,7 +256,7 @@ end
        local function check_percent()
                local pos = mp.get_property_number("percent-pos")
                if pos then
       -                if pos > 95 then
       +                if pos > 95 and #video_id > 0 then
                                send_request("POST", options.url.."/Users/"..user_id.."/PlayedItems/"..video_id.."?api_key="..api_key)
                                video_id = ""
                        end
       @@ -252,4 +271,4 @@ os.execute("mkdir -p "..options.image_path)
        mp.add_periodic_timer(1, check_percent)
        mp.add_key_binding("Ctrl+j", "jf", toggle_overlay)
        mp.observe_property("osd-width", "number", width_change)
       -mp.register_event("file-loaded", unpause)
       +mp.register_event("end-file", unpause)