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.

Split mkv file on chapters

To split a large mkv file into a file for each chapter, run the output of this command.

files/2024/listings/split-on-chapters.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
#!/bin/sh
# File: split-on-chapters.sh
# Location: /mnt/public/Support/Programs/DVDs
# Author: bgstack15
# Startdate: 2024-02-17-7 14:22
# Title: Split mkv on chapters
# Purpose: oneliner for splitting mkv file into separate files per chapter
# History:
#    2024-02-22 moved -ss and -to before -i
# Usage:
# Reference:
#    man ffmpeg
#    slightly improved over https://superuser.com/questions/795373/split-mkv-based-on-chapters
#    https://superuser.com/questions/795373/split-mkv-based-on-chapters/1830690#1830690
#    https://stackoverflow.com/questions/14005110/how-to-split-a-video-using-ffmpeg-so-that-each-chunk-starts-with-a-key-frame/33188399#33188399
# Improve:
# Dependencies:
# Documentation:
#    This is insufficient:
#       HandBrakeCLI -c 2 -i twoeps.mkv -o 2.mkv # this does not get subtitles, and reencodes which takes a while
#   WARNING! Jellyfin+chromecast might malfunction on a video split in this manner, within the first few seconds of playback, because of the keyframe/chapter mismatch.

split_by_chapter() {
   for word in "${@}" ;
   do
      ffmpeg -i "${word}" 2>&1 | sed -n -r -e "/start.*end.*[0-9]*/{s/.*#[0-9]*:([0-9]*).* ([0-9]*\.[0-9]*).*( [0-9]*\.[0-9]*)/ffmpeg -ss \2 -to\3 -i ${word} -acodec copy -vcodec copy -scodec copy ${word%%.mkv}-chapter\1.mkv \;/g;p;}"
   done
}

split_by_chapter "${@}"

So:

$( ./split-on-chapters.sh twoepisodes.mkv )

Splitting a video file can get weird with -codec copy and -ss. If you stream at the beginning of a file split in this manner, it might never start displaying the video. There might be some way to split "correctly" by dealing with the key frames, but I spent maybe 30 minutes and couldn't figure it out. I'll just live with the consequences, of waiting a few seconds before chromecasting a split video file from Jellyfin on an android device.

References

Man pages

  1. man ffmpeg

Web links

  1. slightly improved over video - Split MKV based on Chapters - Super User
  2. video - Split MKV based on Chapters - Super User#1830690
  3. How to split a video using FFMPEG so that each chunk starts with a key frame? - Stack Overflow

controlling mouse sensitivity primarily for 640x480 Dosbos-X windows

When you want to run an old Windows 9x-era software title in dosbox-x, and it the program has a very small maximum resolution, you might want to lower the resolution of the virtual desktop, and then maximize the window of the emulator.

And if you want to do this, your effective mouse sensitivity goes through the roof. You can try lowering it in the virtual environment, but that has limited effect.

So, you can adjust it in your X11 environment. I don't use a desktop environment, and xfce4-settings-manager can view but not make changes to settings (I guess it needs an xfce4 daemon running for that somewhere). So I modify the xinput properties directly. First of all, here's the script.

files/2024/listings/mouse-sensitivity.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
#!/bin/sh
# File: mouse-sensitivity.sh
# Location: /usr/local/bin
# Author: bgstack15
# Startdate: 2024-02-14-4 14:04
# SPDX-License-Identifier: GPL-3.0-only
# Title: Simple control for mouse sensitivity
# Purpose: provide easy options to slow down my mouse
# History:
# Usage: ./mouse-sensitivity.sh slow
# Reference:
#    https://unix.stackexchange.com/questions/90572/how-can-i-set-mouse-sensitivity-not-just-mouse-acceleration
#    https://shallowsky.com/blog/linux/setting-mouse-speed.html
#    experimentation for my mouse
# Improve:
# Dependencies:
#    x11, notify-send | zenity
# Documentation:

# Functions
warn() {
   #zenity --icon-name mouse --warning --text "${1}"
   notify-send --icon mouse --expire-time=2000 "Mouse sensitivity" "${1}"
}

# Load config
test -f "${XDG_CONFIG_HOME:-~/.config}/mouse-sensitivity.conf" && . "${XDG_CONFIG_HOME:-~/.config}/mouse-sensitivity.conf"
test -z "${DEVICE_NAME}" && DEVICE_NAME="Logitech Wireless Receiver Mouse"
# Load environment/runtime settings
if test -z "${SPEED}" ;
then
   if test -n "${1}" ;
   then
      SPEED="${1}"
   else
      warn "Need SPEED or \$1 of number between -1.0 and 1.0, or slow, normal, fast."
      exit 1
   fi
fi

# determine xinput id
xid="$( xinput list | sed -n -r -e "/${DEVICE_NAME}/{s/.*id=([0-9]+).*/\1/;p;}" )"

# Validate SPEED
if echo "${SPEED}" | grep -qE '^[0-9\.\-]+$' ;
then
   # a decimal number, so running with just that number
   :
elif echo "${SPEED}" | grep -qE '^(slow|normal|fast)' ;
then
   case "${SPEED}" in
      slow) SPEED=-1 ;;
      normal) SPEED=0 ;;
      fast) SPEED=1 ;;
   esac
else
   warn "Need speed of number between -1.0 and 1.0, or slow, normal, fast."
   exit 1
fi

# Run command
printf '%s\n' "Setting device ${xid} speed ${SPEED}" 1>&2
xinput set-prop "${xid}" "libinput Accel Speed" "${SPEED}"

You can configure it with a file ~/.config/mouse-sensitivity.conf for the exact device name to look for. I hardcoded the property to modify, but I actually have no idea if that is different for different mice. I'll probably improve this over time.

So now the mouse doesn't zip around quite as ridiculously as before in the tiny, zoomed-in retro environment, so it's easier to click on things!

enable paste on website with javascript

I'm directly quoting/ripping off https://stackoverflow.com/questions/32784487/script-to-enable-paste-on-website/68414487#68414487 here. Because it's incredibly useful.

I found this simple script did work [for enabling "paste" on website]:

document.addEventListener('paste', function(e) {
  e.stopImmediatePropagation();
  return true;
}, true);

Which was found here: https://www.howtogeek.com/251807/how-to-enable-pasting-text-on-sites-that-block-it/

Edit: May need to stop propagation for keydown as well for some websites.

document.addEventListener('keydown', function(e) {
  e.stopImmediatePropagation();
  return true;
}, true);

Paste that into the console of devtools (F12) and carry on with business.

python3 urllib3: ignore certificates

In python3 urrlib3, when you set up a pool manager, you can tell it to use your ca cert bundle, or even just ignore certs.

import urrlib3
webpool = urllib3.PoolManager(cert_reqs="CERT_NONE")
webpool = urllib3.PoolManager(cert_reqs="CERT_REQUIRED",ca_certs="/path/to/cacerts.pem")

You can change the settings later too (but I haven't actually tested this yet!):

webpool.connection_pool_kw["cert_reqs"] = "CERT_NONE"

If you use certifi:

import certifi, urllib3
webpool = urllib3.PoolManager(ca_certs=certifi.where())

And also to suppress the warnings (HERE BE DRAGONS):

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

Use at my own risk.

My FreeFileSync package is rebased on the Debian one

Overview

I have been maintaining a package of FreeFileSync for a long time, first as an rpm and currently only a dpkg (source on cgit and possibly gitlab).

I was even asked to participate in maintaing FreeFileSync in Debian by the maintainer! He uses my work, although less and less as he has started writing better patches than I maintain.

I have now rebased my dpkg on his work. He obviously understand debianized build and package processes better than I do, and also he's better at solving the C++ problems that show up.

Notes on patches

So in the whole list of patches (whew, Debian tends to have a lot, doesn't it?), I reviewed what Bastif has and what I wanted too. He implements them in a different order than I do.

  1. pkg-config.patch: I implement in makefile-improvements.patch
  2. wx-config-version.patch: also in makefile-improvements.patch
  3. ffs_devuan.patch is a smaller, lesser version of my main patch that allows me to compile on Devuan. It did show me to add CPPFLAGS and clean up the LDFLAGS.
  4. remove_upstream_build_optimization.patch is very small and I implement in my ffs_devuan.patch.
  5. ffs_devuan_gtk3.patch really should just get folded into the ffs_devuan.patch. It just switches gtk2 to 3, like mine
  6. zlib-dep.patch: also in makefile-improvements.patch.
  7. reproducible-build.patch: I skip this one. I'm sure that's important for Debian, but I'm lazy and don't care. It just fixes a timestamp to be some consistent (static?) value. This sort of thing is why I wasn't willing to sign up for all this work. This seems like a good idea, but takes mental overhead and repo space and .3 seconds for quilt to apply/unapply every time so I just don't care. I'm glad people like Bastif do this for the rest of us though!
  8. ffs_dpkg_vendor_specific_about.patch: This turns into a variable my previously hard-coded "for Devuan" string I would put in the app. I implement this now too, in my ffs_devuan.patch, and stick my vendor name in the Makefile and not just a debian/rules file. I test compiling with make directly, and not just debuild, so I don't want to depend only on the debuild.
  9. ffs_no_check_updates.patch: I didn't actually even read this one. I'm the upstream for ffs_no_check_updates.patch anyways.
  10. ffs_sftp.patch: same as mine, or close enough.
  11. ffs_traditional_view.patch: same as mine. I'm the upstream author of this one too.
  12. ffs_icon_loader.patch: Woops, he wrote this one better than mine. I should replace mine.
  13. skip-missing-Animal.dat.patch: I implement the bare minimum amount of this in ffs_devuan.patch. His is more thorough but mine is minimal and is good enough for me.
  14. fix-gtk3-kde-hang-and-dialog-size.patch: I think I implement this in revert_buggy_gtk3_change_in_12.1.patch, but I also don't use KDE so cannot test.
  15. libssh2_relax_dep.patch: This one is so far superior to mine; he handles it based on different version numbers of the library. I threw mine away and use his now.
  16. Disable_wxWidgets_uncaught_exception_handling.patch: I implement something similar enough in disable_wxuse_Exceptions.patch. I'm willing to guess Bastif's patch is better, but mine works too.
  17. deactivate_google_drive_button.patch: I don't implement this. That means that I don't deactivate the button, but I don't have a google key and secret to plug in, so technically my package doesn't work with Google Drive either. I probably should use this patch, but again, I'm lazy.
  18. libcurl_improve_supported_error_codes.patch: This was so far superior to my old version that I threw mine away and just use this now.

And leave it to Debian to have 18 patches for a program like this!

stackrpms-diff: a complex example

To install my printer shared from my one server, just run this shell script.

files/2024/listings/devuan-printer.sh (Source)

#!/bin/sh
# File: devuan-printer.sh
# Location: /mnt/public/Support/Platform/printer/
# Author: bgstack15
# SPDX-License-Identifier: GPL-3.0-only
# Startdate: 2024-02-08-5 08:15
# Title: Install my printer on Devuan
# Purpose: oneliner install printer
# History:
# Usage:
# Reference:
#    https://www.cups.org/doc/admin.html
#    https://serverfault.com/questions/1011996/cups-how-to-list-all-detected-printers-from-command-line-linux
#    http://localhost:631/
#    https://bgstack15.ddns.net/blog/posts/2022/11/17/use-remote-printer-driver-for-cups-shared-printer-in-cups-client/"
#    man lpadmin
# Improve:
# Dependencies:
#    run on Devuan. Sudo access.
# Documentation:
#    if you need printing with `lpr -P ml1865w /path/to/file`, then install cups-bsd
# install cups
if ! dpkg -l | awk '$2=="cups" && $1~/^i./' 1>/dev/null ;
then
   sudo apt-get update
   sudo apt-get install --no-install-recommends -y cups
fi
ensure_printer() {
   _name="${1}"
   _url="${2}"
   _location="${3}"
   status="$( sudo lpstat -v )"
   if ! echo "${status}" | grep -qiE "${_url}"  ;
   then
      sudo lpadmin -p "${_name}" -E -v "${_url}" -L "${_location}"
   fi
}
# printer list
ensure_printer "ml1865" "ipp://dns2.ipa.internal.com:631/printers/ml1865" "computer room"
# display status
sudo lpstat -v

This reduces my need to install system-config-printer just to get my one printer set up.

Cups is a beautiful project, isn't it?

X11 wine window not visible: Jazz Jackrabbit 2

One of my applications, Jazz Jackrabbit 2, had a problem where the window would not be visible, but the game is running.

After testing with using a wine virtual desktop environment (failed) and a different wine context (worked), I realized the problem lies between wine and the game. I learned that by deleting the registry key the game uses to store its window state (full screen or windowed) was somehow invalid or incorrect.

HKLM\Software\Epic MegaGames\Jazz Jackrabbit 2 Special Edition

And anywhere else this Software path would be in the registry, such as HKCU. And then run the game, and you can view the game.

And a link to the JJ2 community: Jazz Jackrabbit Online - Jazz Jackrabbit News, Information and Downloads on Jazz Jackrabbit 1, Jazz Jackrabbit 2, and Jazz Jackrabbit Advance - Jazz2Online

Alternate research

I had done a lot of searching for x11 make window visible. How you would do that is with xdotool:

$ xwininfo -root -children | grep -i mktrayicon
     0x1c00001 "mktrayicon": ("mktrayicon" "Mktrayicon")  10x10+10+10  +10+10

And then you can plug in the id force it to be visible.

$ xdotool windowmap 0x1c00001

But even after doing that with JJ2 did not make the game contents display.

Rescan Jellyfin library from cli

I use Jellyfin (see the tags link below), and when I add new media, sometimes I want to see it right away. In order to do that, you need to tell the library to re-scan, or you can have a library configured to watch for filesystem changes. (I assume it uses inotify or something.) I use the "watch" option, but it doesn't seem to work [fast enough?] for me. I use nfs so that might have something to do with it.

So, you can log in to the web/mobile app with an account that can tell the library to scan now for new/changed files, but that takes quite a few clicks. So I spent way more time to write a oneliner that does all that for me, and it even has bash tab autocompletion! Check out the whole project at my cgit/jellystack, but here are some highlights:

Dot-source the autocomplete-rescan.bash script:

test -f /path/to/project/autocomplete-rescan.bash && . /path/to/project/autocomplete-rescan.bash

And after setting the ~/.jellyfin.password (with contents of password) and ~/.config/jellystack (shell script that exports username, password, and server) dependency files, you can use:

rescan-library <TAB>

stackrpms-diff: a complex example

I maintain packages for a number of projects. Some of them are redressings of existing packages by other netizens. My best example is waterfox-g (upstream by hawkeye116477).

I like to maintain a patch that shows the differences between the upstream and my work. This is for transparency, as well as to show the differences so I can recreate those differences for the next version of the package. I don't version-control my debian/ directory in the proper way. I've been chided for this before, but I understand this janky way and you are welcome to re-do my work the correct way!

For a package like waterfox-g where I drop the -kpe ending because I don't use KDE and company (just not my style; they seem nicer than gnomes in general though), some of the filenames change too, which messes up a diff. So here is my complex stackrpms-diff.sh (original in my scm). files/2024/listings/stackrpms-diff-waterfox-g.sh (Source)

#!/bin/sh
# Startdate: 2024-01-25-5 15:11
# Reference: scite/stackrpms-diff.sh
# Usage: stackrpms/waterfox-g/stackrpms-diff.sh | vi -
# Purpose: handle the renamed files because of dropping the -kpe suffix
left="/usr/src/waterfox/waterfox-deb-rpm-arch-AppImage/waterfox-g-kpe"
right="stackrpms/waterfox-g/debian"
cd ~/dev
# do diff color=always if this is going to a terminal
_diff_color="never"
\test -c /proc/$$/fd/1 && _diff_color="always"
{ test -n "${DIFF_COLOR}" || test -n "${DIFF_COLORS}" ; } && { _diff_color="always" ; }
diff -x '.*.swp' \
   --exclude-from="${right}/../diff-excludes" \
   --color="${_diff_color}" \
   -aur "${left}" "${right}"
for word in dirs dsc install links lintian-overrides manpages postinst postrm preinst prerm links ;
do
   _result="$( diff --color="${_diff_color}" -aur "${left}/waterfox-g-kpe.${word}" "${right}/waterfox-g.${word}" )"
   test -n "${_result}" && {
      echo diff -aur "${left}/waterfox-g-kpe.${word}" "${right}/waterfox-g.${word}"
      echo "${_result}"
   }
done

Random notes on this cool script:

I have a separate file, diff-excludes that lists all the old and new filenames to exclude from the first diff, one per line. I didn't want to just have a dozen -x FILENAME entries because that clutters the otherwise useful output of the long command, with the full command line before each diff entry.

I probably need to come up with a programmatic way to generate this from line 17, but I haven't done that yet.

Line 11 escapes the test so it uses the executable test and not any possible built-in. I know it forks a new process and all that, but I'd rather not depend on a bash or other shell built-in.

I do my own stdout evaluation to determine color. Half the time, I'm running the command in a terminal, and the other half the time I'm sending it to a file to then view with $EDITOR (guess which one). So I wrote all this extra logic to handle adding or excluding the color.

If anybody has a better method for diffing two directories that experiences file renames (with possible content changes too; that's why I'm running the diff!), please let me know.