#!/bin/sh # shellcheck disable=SC1091,SC1090 #fixed variables config_file="$HOME/.jellyfin-cli-config" VERSION="1.0.0" info() { #shellcheck disable=SC2059 printf "\033[2K\r\033[1;${2:-36}m${1}\033[0m" } success() { info "$1" "32" } ask() { info "$1" "33" } err() { info "$1\n" "31" } save_config() { if [ -f "$config_file" ] && grep -q "$1" "$config_file"; then sed -i "s|$1=.*|$1=$2|g" "$config_file" >/dev/null 2>&1 else #shellcheck disable=SC2059 printf "$1=$2\n" >>"$config_file" sed '/^$/d' "$config_file" >/dev/null 2>&1 fi [ -z "$3" ] && success "$1 Saved in config, to override config values use it as envs\n" } auth_quick_connect() { info "Generating Quick Connect Code.." DEVICE_ID="$(uuidgen)" custom_auth='Authorization: MediaBrowser Client="jellyfin-cli", Device="jellyfin-cli", DeviceId="'"$DEVICE_ID"'", Version="'"$VERSION"'"' eval "$(curl -s "$JF_URL/QuickConnect/Initiate" -X POST -H "$custom_auth" | sed -nE 's|.*"Secret":"([^"]*)","Code":"([^"]*)",.*|SECRET=\1;CODE=\2|p')" info "Your Quick Connect Code: " printf '%s\n' "$CODE" info "Waiting for you to authorized." while curl -s "$JF_URL/QuickConnect/Connect?Secret=$SECRET" -H "$custom_auth" | grep -q '"Authenticated":false'; do printf '.' sleep 2 done success "Authorized Successfully!!\n" # you authenticated YAY eval "$(curl -s "$JF_URL/Users/AuthenticateWithQuickConnect" -X POST -H "Content-Type: application/json" --data-raw '{"Secret":"'"$SECRET"'"}' -H "$custom_auth" | sed -nE 's|.*UserId":"([^"]*)".*"AccessToken":"([^"]*)",.*|JF_USER_ID=\1;JF_TOKEN=\2|p')" save_config "JF_USER_ID" "$JF_USER_ID" save_config "JF_TOKEN" "$JF_TOKEN" } check_config_auth() { info "Checking Config.." [ -f "$config_file" ] || return 1 for i in JF_USER_ID JF_TOKEN JF_URL; do if ! grep -qE "$i=(.+)" "$config_file"; then return 1 fi done info "Checking Auth..." . "$config_file" resp=$(curl -s "$JF_URL/Users/Me" -H 'Authorization: MediaBrowser Token="'"$JF_TOKEN"'"' | sed -nE 's|.*"Id":"([^"]*)".*|JF_USER_ID=\1|p') [ -z "$resp" ] && save_config "JF_TOKEN" "" "no_output" && return 1 return 0 } configure() { info "Welcome to Jellyfin CLI script, we will go through some configuration.\n" [ -f "$config_file" ] && . "$config_file" if ! [ -f "$config_file" ] || ! grep -qE "JF_URL=(.+)" "$config_file"; then ask "First, Where is Jellyfin hosted? : " read -r JF_URL save_config "JF_URL" "$JF_URL" fi if ! [ -f "$config_file" ] || ! grep -qE "JF_TOKEN=(.+)" "$config_file"; then auth_quick_connect fi if ! [ -f "$config_file" ] || ! grep -qE "JF_USER_ID=(.+)" "$config_file"; then eval "$(curl -s "$JF_URL/Users/Me" -H 'Authorization: MediaBrowser Token="'"$JF_TOKEN"'"' | sed -nE 's|.*"Id":"([^"]*)".*|JF_USER_ID=\1|p')" save_config "JF_USER_ID" "$JF_USER_ID" fi unset JF_USER_ID JF_TOKEN CODE SECRET JF_URL . "$config_file" } mpv_jellyfin() { [ -z "$1" ] && return 0 [ -z "$2" ] && return 0 ITEM_ID=$(printf '%s' "$1" | sed -E 's/(.{8})(.{4})(.{4})(.{4})(.{12})/\1-\2-\3-\4-\5/') success "Playing $2 on mpv" url="$JF_URL/Items/$1/Download?api_key=$JF_TOKEN" sub="$JF_URL/Videos/$ITEM_ID/$1/Subtitles/0/0/Stream.ass?api_key=$JF_TOKEN" ! curl -s "$sub" | grep -q "Error processing request" && sub_arg="--sub-file=$sub" if uname -o | grep -q 'ndroid'; then printf "Title %s\n" "$2" printf "URL: %s\n" "$url" [ -n "$sub_arg" ] && printf "Sub URL: %s\n" "$sub" return 0 fi #shellcheck disable=SC2086 nohup mpv --input-ipc-server="$socket" --start="$((playbackPositionTicks / 10000000))" --force-media-title="$2" "$url" $sub_arg >/dev/null 2>&1 & skipPositions=$(curl -s "$JF_URL/Episode/$ITEM_ID/IntroSkipperSegments" -H 'Authorization: MediaBrowser Token="'"$JF_TOKEN"'"' -H "Accept: application/json" | sed -nE 's|.*"Introduction".*"IntroStart":([^\.]*).*,"IntroEnd":([^\.]*).*"Credits".*"IntroStart":([^\.]*).*"IntroEnd":([^\.]*).*|op_start=\1;op_end=\2;ed_start=\3;ed_end=\4|p') track_progress } track_progress() { \cat <"$progress_track_file" #!/bin/sh skipPos(){ [ -z "\$3" ] && printf 1 && return 0 [ -z "\$4" ] && printf 1 && return 0 if [ "\$2" -ge "\$3" ] && [ "\$2" -lt "\$4" ]; then echo '{"command" :["seek" ,"'"\$4"'","absolute"]}' | socat - "$socket" >/dev/null echo '{"command": ["show-text", "'"\$5"'", 3000]}' | socat - "$socket" >/dev/null printf 1 return 0 fi printf 0 } positionTicks=$playbackPositionTicks $skipPositions introSkipped=0 outroSkipped=0 while sleep 5;do position=\$(echo '{"command" :["get_property","playback-time"]}' | socat - "$socket" 2>/dev/null | sed -nE 's_.*data":([^,]*).*_\1_p' | tr -d '.' | sed 's|$|0|g') [ -z "\$position" ] && break positionTicks=\$position positionSec=\$((position / 10000000)) [ "\$introSkipped" -eq 0 ] && introSkipped=\$(skipPos "\$introSkipped" "\$positionSec" "\$op_start" "\$op_end" "Intro Skipped") [ "\$outroSkipped" -eq 0 ] && outroSkipped=\$(skipPos "\$outroSkipped" "\$positionSec" "\$ed_start" "\$ed_end" "Outro Skipped") done [ -n "\$positionTicks" ] && curl -s "$JF_URL/Users/$JF_USER_ID/PlayingItems/$ITEM_ID?positionTicks=\$positionTicks" -X DELETE -H 'Authorization: MediaBrowser Token="'"$JF_TOKEN"'"' -H 'Content-Type: application/json' rm "$socket" rm "\$0" EOF chmod +x "$progress_track_file" setsid -f "$progress_track_file" } get_data() { curl -s "${JF_URL}/$1" -H 'Authorization: MediaBrowser Token="'"$JF_TOKEN"'"' -H "Accept: application/json" | sed 's|\[{|\n|g;s|},{|\n|g' | sed -nE 's|^"Name":"([^"]*)",.*,"Id":"([^"]*)".*"PlaybackPositionTicks":([^,]*),.*Primary":\{?"([^"]*)".*|\2\t\4\t\1\t\3|p' | menu "$2" } shows() { season=$(get_data "Shows/$id/Seasons?userId=$JF_USER_ID" "Select Season >") [ -z "$season" ] && exit 1 season_title=$(printf "%s" "$season" | cut -f3) season_id=$(printf "%s" "$season" | cut -f1) episode=$(get_data "Shows/$id/Episodes?seasonId=$season_id&userId=$JF_USER_ID" "Select Episode >") [ -z "$episode" ] && exit 1 episode_title=$(printf "%s" "$episode" | cut -f3) episode_id=$(printf "%s" "$episode" | cut -f1) title="$title $season_title ep: $episode_title" id=$episode_id playbackPositionTicks=$(printf '%s' "$episode" | cut -f4) } collection() { collection=$(get_data "UserViews?userId=$JF_USER_ID" "Select Collection >") [ -z "$collection" ] && exit 1 collection_id=$(printf '%s' "$collection" | cut -f1) collection_title=$(printf '%s' "$collection" | cut -f3 | sed 's|.$||g') recursive="false" [ "$collection_title" = "Movie" ] && recursive="true" data=$(get_data "Items?IncludeItemTypes=$collection_title&Recursive=$recursive&ParentId=$collection_id" "Select $collection_title >") [ -z "$data" ] && exit 1 id=$(printf "%s" "$data" | cut -f1) title=$(printf "%s" "$data" | cut -f3) playbackPositionTicks=$(printf '%s' "$data" | cut -f4) case "$collection_title" in Show) shows ;; Movie) ;; # the id and title already belongs to Movie. esac } resume() { resume=$(get_data "UserItems/Resume?enableImages=true" "Resume >") [ -z "$resume" ] && exit 1 id=$(printf "%s" "$resume" | cut -f1) title=$(printf "%s" "$resume" | cut -f3) playbackPositionTicks=$(printf '%s' "$resume" | cut -f4) } nextUp() { nextup=$(get_data "Shows/NextUp?enableTotalRecordCount=true&disableFirstEpisode=false&enableResumable=true" "Next UP >") [ -z "$nextup" ] && exit 1 id=$(printf "%s" "$nextup" | cut -f1) title=$(printf "%s" "$nextup" | cut -f3) playbackPositionTicks=$(printf '%s' "$nextup" | cut -f4) } menu() { fzf --prompt="$1" --layout=reverse --border -d'\t' --with-nth=3 --preview="img2sixel '$JF_URL/items/{1}/Images/Primary?fillHeight=450&quality=96'" --preview-window=right,70% } check_config_auth || configure info "" . "$config_file" socket="/tmp/${0##*/}-mpvsocket" progress_track_file="/tmp/${0##*/}-progress" what_to_watch=$(printf "My Media\nResume\nNext Up" | fzf --prompt="Select >" --layout=reverse --border) case "$what_to_watch" in 'My Media') collection ;; Resume) resume ;; 'Next Up') nextUp ;; esac mpv_jellyfin "$id" "$title"