Knowledge Base

Preserving for the future: Shell scripts, AoC, and more

jellyfin show manager

I'm back with an overengineered solution for myself. I have a complete show, but I want to have Jellyfin show only up to 2 new episodes at a time. I'll set a cron job to check and add new episodes from the real location as necessary.

So, the overcomplicated script:

files/2024/listings/show-manager.sh (Source)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
#!/bin/sh
# File: show-manager.sh
# Location: /mnt/public/Support/Programs/jellyfin/scripts/
# Author: bgstack15
# Startdate: 2024-02-18-1 17:24
# SPDX-License-Identifier: GPL-3.0-only
# Title: Slowly add new episodes of a completed show to jellyfin
# Purpose: Check jellyfin for if the last season of a given show is greater than (x-2)/x episodes, and if so, then add that many new episodes.
# History:
# Usage:
#    . ./jellystack-autocomplete.bash ; manage_show stsnw
# Improve:
#    hook in to jellyfin deeper, by checking file paths of the episodes where watched=True, to know which episodes to skip trying to add
#    learn how to completely clear the "watched" information about the show in jellyfin.
# Documentation:
#    Assumptions:
#       1. media files are sorted in "Season 01/" type directories.
#       2. Media files are named "*s01e07 *" style which will be sorted alphabetically in episode number order.
#       3. watched episodes are in exact order!
#       4. input files are all mkv (this is to simplify filtering out non-episode files like images; it could be made more complex as needed)
# Dependencies:
#    jellystack_lib with watched_episodes_for_show()

# flow:
# get episodes-watched for last season of show (value is, e.g., "8/10"


# depends on content being in "Season 01/" directories.
get_episodes_watched_count() {
   # input env vars: library, show, DEBUG
   # output: prints watched= and total= numbers
   # usage: $( library="TV" show="Star Trek: Strange New Worlds" get_episodes_watched_count )
   _pyverbose=False
   test -n "${DEBUG}" && _pyverbose=True
   cd /mnt/public/Support/Programs/jellyfin/scripts
   . ~/.config/jellystack.viewing.user
   {
      python3 <<-EOF
import jellystack_lib
a = jellystack_lib.get_authenticated_client(url="${server}",username="${username}",password="${password}")
b = jellystack_lib.watched_episodes_for_show(a,"${library}","${show}","sum",${_pyverbose})
c = b.split("/")
print(f"watched={c[0]}")
print(f"total={c[1]}")
EOF
   }
}

symlink_file() {
   # input vars: infile SOURCE_DIR LINK_DIR LINK_PREFIX VERBOSE APPLY
   # reference: printf '%s\n' *s01e0* | while read line ; do ln -sf "../../../off.movies/Star Trek - Strange New Worlds (2021/Season 01/${line}" "/mnt/public/Video/TV/Star Trek - Strange New Worlds (2021/Season 01/" ; done
   # We now calculate season number based on file name
   #season_num_str="$( \printf '%02d' "${season_num}" )"
   base="$( basename "${infile}" )"
   season_num_str="$( \printf '%02d' "$( echo "${base}" | grep -oE '\<s[0-9]+e[0-9]+' | awk -F'e' '{print $1}' | tr -dc '0-9' )" )"
   test -n "${VERBOSE}" && echo ln -s "${LINK_PREFIX}/Season ${season_num_str}/${base}" "${LINK_DIR}/Season ${season_num_str}/"
   test -n "${APPLY}" && ln -s "${LINK_PREFIX}/Season ${season_num_str}/${base}" "${LINK_DIR}/Season ${season_num_str}/"
}

list_sorted_episode_files() {
   # input env vars: SOURCE_DIR
   # if changing from %P, probably do a cd in a sub-shell.
   find "${SOURCE_DIR}" -name '*mkv' -iregex '.*\<s[0-9]+e[0-9]+\>.*' -printf '%P\n' | sort -n
}

# Logic flow:
# diff=total-watched
# if diff < 2,
#    find the smallest-number episode files and add them in.
manage_show() {
   # input vars: MAX_NEW_EPISODES SOURCE_DIR VERBOSE LIBRARY SHOW
   # You may also run: manage_show "TV/Star Trek: Strange New Worlds"
   conf="${1}"
   test -f "/mnt/public/Support/Programs/jellyfin/scripts/input/${conf}.conf" && . "/mnt/public/Support/Programs/jellyfin/scripts/input/${conf}.conf"
   # Not necessary with the conf file method
   #test -z "${SHOW}" && test -z "${LIBRARY}" && {
   #   LIBRARY="$( echo "${1}" | awk -F'/' '{print $1}' )"
   #   SHOW="$( echo "${1}" | awk -F'/' '{print $2}' )"
   #}
   test -z "${MAX_NEW_EPISODES}" && MAX_NEW_EPISODES=2
   # this next function sets vars: watched, total
   eval $( library="${LIBRARY}" show="${SHOW}" get_episodes_watched_count )
   # hardcode these to test max_new_episodes
   #watched=6 total=8
   diff=$((total-watched))
   test -n "${VERBOSE}" && echo "Got watched=${watched} total=${total} diff=${diff}" 1>&2
   if test ${diff:-0} -lt ${MAX_NEW_EPISODES} ;
   then
      need=$((MAX_NEW_EPISODES-diff))
      echo "Must prepare next ${need} episodes." 1>&2
      w1=$((watched+1))
      end=$((watched+need))
      list_sorted_episode_files | sed -n -r -e "${w1},${end}p" | while read infile ;
      do
         # VERBOSE and APPLY are used in this function:
         infile="${infile}" symlink_file
      done
   fi
}

# Before the config file redesign, use one of these:
# These will still get "/Season 01" prepended to the file basename:
# SOURCE_DIR="/mnt/public/Video/off.movies/Star Trek - Strange New Worlds (2021)" LINK_DIR="/mnt/public/Video/TV/Star Trek - Strange New Worlds (2021)" LINK_PREFIX="../../../off.movies/Star Trek - Strange New Worlds (2021)" LIBRARY="TV" SHOW="Star Trek - Strange New Worlds (2021)" DIR="${SHOW}" manage_show

So I'm pretty sure this uses yet a new password file, this time as a shell snippet with export statements of the interesting variables, ~/.config/jellystack.viewing.user. (I don't use the admin user for actually watching, what am I, ridiculous?!)

export password="KEEPASS" username="jellyfin" server="http://vm4:8096"

And then with this autocomplete:

files/2024/listings/jellystack-autocomplete.bash (Source)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#!/bin/bash
# trimmed for blog
_show_manager_confs() {
   # reference: vm4:/etc/bash_completion.d/docker-nfs-check
   local cur prev words cword;
   _init_completion || return
   _tmpfile1="$( mktemp )"
   # populate list
   find /mnt/public/Support/Programs/jellyfin/scripts/input -mindepth 1 -maxdepth 1 ! -type d -name '*.conf' -printf '%P\n' | sed -r -e 's/\.conf$//;' > "${_tmpfile1}"
   COMPREPLY=($( compgen -W "$( cat ${_tmpfile1} )" -- "$cur" | sed -r -e "/^${prev}/d;" ))
   command rm -rf "${_tmpfile1:-NOTHINGTODEL}" 1>/dev/null 2>&1
   return 0
} &&
complete -F _show_manager_confs manage-show
. /mnt/public/Support/Programs/jellyfin/scripts/show-manager.sh
alias manage-show="manage_show"

And any number of files in that input/ directory:

files/2024/listings/strangenewworlds.conf.example (Source)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Where all the episode mkv files are
SOURCE_DIR="/mnt/public/Video/off.movies/Star Trek - Strange New Worlds (2021)"
# Where they should be placed for Jellyfin to pick them up
LINK_DIR="/mnt/public/Video/TV/Star Trek - Strange New Worlds (2021)"
# Symlink prefix, so probably the relative path from LINK_DIR to SOURCE_DIR
LINK_PREFIX="../../../off.movies/Star Trek - Strange New Worlds (2021)"
# Jellyfin library name
LIBRARY="TV"
# Jellyfin show name
SHOW="Star Trek: Strange New Worlds"
# Show directory name. Not necessarily the same as SHOW.
DIR="Star Trek - Strange New Worlds (2021)"

So if I've watched 6/7 episodes, and MAX_NEW_EPISODES=2, then it should symlink in one new episode.

This process counts incorrectly sometimes (because I originally had all the episode files available to it and the counting got messed up), so I just keep increasing MAX_NEW_EPISODES until it puts the correct number of new episodes in the path for Jellyfin.

So I just run:

. jellystack-autocomplete.bash
VERBOSE=1 MAX_NEW_EPISODES=3 manage-show <tab>

And list the show confs, and pick one, and then run it. Once it looks good, I re-run with APPLY=1.

Comments