Knowledge Base

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

My DVD-ripping solution

This is a work in progress, but its current status is as follows.

Ripping the DVDs

To rip my DVD collection, use my handbrake wrapper script. My ripping machine uses two disc drives. Run the script once per drive, and save all output to a temporary file.

{ INPUT=/dev/sr0 handbrake.sh ; INPUT=/dev/sr1 handbrake.sh ; } > ~/handbrake1

Inspect the script for correctness. Mine looks like:

HandBrakeCLI --markers --format mkv --min-duration 125 --input /dev/sr0 --output /mnt/public/Video/temp/TNGS2D3_V1.mkv --encoder x264 --rate 30 --native-language eng --title 1 --audio 1,2, --subtitle 1,2,scan --native-language eng --mixdown 6ch --aencoder ffaac
HandBrakeCLI --markers --format mkv --min-duration 125 --input /dev/sr0 --output /mnt/public/Video/temp/TNGS2D3_V2.mkv --encoder x264 --rate 30 --native-language eng --title 2 --audio 1,2, --subtitle 1,2,scan --native-language eng --mixdown 6ch --aencoder ffaac

The handbrake.sh script can be found at the bottom of this post and is explained further there. Run the temp file's commands. I like to do this in a GNU screen session.

time sh -x ~/handbrake1

Adding TV show metadata

Prepare a csv with s, sep, ep, airdate, title, and filename columns.

have,s,ep,sep,title,airdate,filename
1,1,1,01-e02,Encounter at Farpoint,1987-09-28,s01e01-e02 - Encounter at Farpoint
1,1,3,3,The Naked Now,1987-10-05,s01e03 - The Naked Now
1,1,4,4,Code of Honor,1987-10-12,s01e04 - Code of Honor
1,1,5,5,The Last Outpost,1987-10-19,s01e05 - The Last Outpost
1,1,6,6,Where No One Has Gone Before,1987-10-26,s01e06 - Where No One Has Gone Before

This filename output is supposed to be what Jellyfin wants for TV shows. I used the information from Wikipedia and some LibreOffice Calc functions to make this csv file, and some hand-munging for the double-long episodes.

="s" & TEXT(A2,"\00") & "e" & TEXT(C2,"00") & " - " & REGEX($D2,"[,!']","")

I'm a bit obsessive about removing commas, exclamation marks, question marks, and quote marks from my filenames. Inspect the saved files and rename each one to the filename string from the csv that corresponds to the correct episode. To add the mkv metadata including airdate, and next and previous episodes, run the next script on the directory. This does not currently recurse into subdirectories.

time tv-mkv-helper.py --inputcsv "/mnt/public/Video/TV/Star Trek The Next Generation (1987)/STTNG.csv" -d /mnt/public/Video/temp/

Alternatives

Automatic Ripping Machine is just too fully-featured for me.

References

  1. Jellyfin TV show naming scheme

Scripts

handbrake.sh

My handbrake.sh wrapper reads the disc contents to learn the disc title and the number of video tracks longer than 125 seconds.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#!/bin/sh
# startdate: 2021-03-10 11:36
# Alternatives:
#   handbrake preset: /mnt/public/Support/Programs/DVDs/1920x1080.json

test -z "${INPUT}" && INPUT=/dev/sr0 ; export INPUT
test -z "${OUTPUT}" && OUTPUT=/mnt/public/Video/temp ; export OUTPUT

raw="$( HandBrakeCLI --markers --format mkv --min-duration 125 --scan --input "${INPUT}" --output "${OUTPUT}"  --encoder x264 --rate 30 --native-language eng --title 0 2>&1 )"
disctitle="$( echo "${raw}" | sed -n -r -e '/DVD Title:/{s/.*DVD Title: //;p}' )"
array="$( echo "${raw}" | grep -E '^\s*\+' | awk 'BEGIN{x=0} /title [0-9]/{gsub(":","",$NF);x++;title[x]=$NF} /kbps/{audio[x]=audio[x]""$2} !/subtitle tracks:/ && !/\+ [0-9]+,/ {us=0} /subtitle tracks:/{us=1} us == 1 && $2 ~ /[0-9]+,/{subtitle[x]=subtitle[x]""$2} END {for(i in title) {{print title[i],audio[i],subtitle[i]}}}' )"
commands="$( echo "${array}" | awk -v "disctitle=${disctitle}" -v "input=${INPUT}" -v "output=${OUTPUT}" '{a=$1;b=$2;c=$3;print "HandBrakeCLI --markers --format mkv --min-duration 125 --input "input" --output "output"/"disctitle"_V"a".mkv --encoder x264 --rate 30 --native-language eng --title "a" --audio "b" --subtitle "c"scan --native-language eng --mixdown 6ch --aencoder ffaac"}' )" 
echo "${commands}"

My hard-coded values include outputting to mkv, and x264 video encoding, and 30 bps, with English language. I also tell it to use the specific audio encoder ffaac which was the first one I tried when trying to get my 6-channel mixdown (that's 5.1 surround sound) option to work. I'm not a media bitrate snob; just trying to preserve some semblance of fancier file value than I can even consume, should I ever upgrade my viewing equipment in the next 40 years.

tv-mkv-helper.py

  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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
#!/usr/bin/env python3
# Startdate: 2021-08-07
# Purpose: given INCSV="/mnt/public/Video/TV/Star Trek The Next Generation 1987)/STTNG.csv"
#    with columns s, ep, sep, title, airdate, filename,
#    update files in provided directory, whose name matches the filename column, with the mkv properties
# Usage:
#    time tv-mkv-helper.py --inputcsv "/mnt/public/Video/TV/Star Trek The Next Generation (1987)/STTNG.csv" -d /mnt/public/Video/temp/
# References:
#    /mnt/public/Support/Programs/DVDs/handbrake-internal.sh
# Dependencies:
#    STTNG.csv sorted ascending, with columns s, sep, title, airdate, filename
#    mkvpropedit

import csv, os, subprocess

###############################################################
def load_file_to_array(inputcsv):
   episode_array = []
   with open(inputcsv,"r") as o:
      csv_reader = csv.reader(o)
      headers = next(csv_reader)
      error_string = ""

      # sanity check for the columns we want
      if "s" not in headers:
         error_string = "Inputcsv needs column named 's' for season number."
      #elif "ep" not in headers:
      #   error_string = "Inputcsv needs column named 'ep' for episode number."
      elif "sep" not in headers:
         error_string = "Inputcsv needs column named 'sep' for season episode number."
      elif "airdate" not in headers:
         error_string = "Inputcsv needs column named 'airdate'."
      elif "filename" not in headers:
         error_string = "Inputcsv needs column named 'filename'."
      if error_string != "":
         print(error_string)
         return -1

      x = 0
      for row in csv_reader:
         x = x + 1
         ep = {key: value for key, value in zip(headers,row)}
         episode_array.append(ep)
   return episode_array

############################################################
def fix_directory_contents(directory, inputcsv):
   os.chdir(directory)
   episodes = load_file_to_array(inputcsv)
   if episodes == -1:
      print("Aborting.")
      return -1
   # simple one-level down method. This might be insufficient someday.
   for filename in os.listdir(directory):
      if filename.endswith(".mkv"):
         print("----------------------")
         print(f"Checking file {filename}")
         filename_sans_ext, _ = os.path.splitext(filename)
         # loop through episodes to find this file
         # need enumerate, per https://stackoverflow.com/questions/1011938/loop-that-also-accesses-previous-and-next-values/1011962#1011962
         l = len(episodes)
         for index, ep in enumerate(episodes):
            previous = next_ = None
            if ep['filename'] == filename_sans_ext:
               if index > 0:
                  previous = episodes[index-1]['filename']
               if index < ( l - 1 ):
                  next_ = episodes[index+1]['filename']
               fix_file(
                  filename=filename,
                  title=ep['filename'],
                  season=ep['s'],
                  seasonep=ep['sep'],
                  next_name=next_,
                  prev_name=previous,
                  airdate=ep['airdate']
               )

############################################
def fix_file(filename,title,season,seasonep,airdate,next_name, prev_name):
   print(f"Please fix {filename} to have")
   print(f"title: {title}")
   print(f"season: {season}")
   print(f"seasonep: {seasonep}")
   print(f"airdate: {airdate}")
   print(f"next_name: {next_name}")
   print(f"prev_name: {prev_name}")
   if next_name is not None and not next_name.endswith(".mkv"):
      next_name = next_name + ".mkv"
   if prev_name is not None and not prev_name.endswith(".mkv"):
      prev_name = prev_name + ".mkv"
   date_string = str(airdate) + "T12:00:00Z" # just set it to noon UTC, so the EST/EDT time will still be the same day.
   #cmd_string = f"mkvpropedit --set 'title={title}' --set 'date={date_string}' --set 'segment-filename={filename}.mkv' --set 'prev-filename={prev_name}' --set 'next-filename={next_name}'"
   cmd_array = ["mkvpropedit","--set",f'title={title}',"--set",f'date={date_string}',"--set",f'segment-filename={filename}',"--set",f'prev-filename={prev_name}',"--set",f'next-filename={next_name}',filename]
   result = subprocess.run(cmd_array)
   print(result)
   aux_cmd_array=["mkvpropedit",filename,"--delete"]
   if next_name is None or next_name == "":
      aux_cmd_array.append("next-filename")
   if prev_name is None or prev_name == "":
      aux_cmd_array.append("prev-filename")
   # if we added anything to the auxiliary command array, then run it
   if aux_cmd_array[-1] != "--delete":
      result = subprocess.run(aux_cmd_array)
      print(result)

# argparse stuff
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("-i","--inputcsv",required=True,help="Episode CSV, with columns s, sep, title, filename, airdate")
parser.add_argument("-d","--directory",required=True,help="Path to directory where files should be given metadata")
args = parser.parse_args()

inputcsv = args.inputcsv
directory = args.directory

if __name__ == "__main__":
   print(f"running with inputcsv=\"{inputcsv}\", directory={directory}")
   fix_directory_contents(directory,inputcsv)

In a departure from my normal style where I put all the functions in a library and call it, I wanted a self-contained script with the argument parsing. It was quick and dirty. And all I'm doing is adding the 3 or 4 mkv attributes to the mkv files whose filename matches an entry in the csv. I do all this using a custom csv, because I'm too lazy to learn how to use some fancy TheTVDB api or whatever. I'm obsessive enough to be fine with manually scraping metadata from Wikipedia tv show episode lists.

Comments