Automatisch YouTube tijdstippen genereren voor hoofdstukken

Wanneer je video’s publiceert op YouTube, zeker als ze wat langer zijn, kan het handig zijn om te zorgen dat er hoofdstukken vermeld worden; een soort mijlpalen in de video.

YouTube kan deze automatisch voor je maken, maar net als met de automatische ondertiteling is het dan natuurlijk maar de vraag of het echt klopt. Je kunt ze ook met de hand opgeven, onder andere door een standaard format te volgen.

Zet je in je omschrijving tekst als dit:

  • 00:10 Hoofdstuk 1
  • 02:48 Hoofdstuk 2
  • 04:21 Hoofdstuk 3

Dan komen er automatisch hoofdstuk-markeringen op 00:10, 02:48, en 04:21.

Dergelijke hoofdstukken gewoon ondersteund in veel video-formaten, maar die pakt YouTube dan weer niet automatisch op.

Zonde, want als ik bijvoorbeeld markeringen zet in ScreenFlow moet ik ze vervolgens dan weer met de hand overnemen naar YouTube.

Dus, hoe lossen we dat op?

Wat hebben we nodig?

De makkelijkste manier is gewoon wat shell scripting. Dit kun je goed combineren met iets als Hazel om je video-map in de gaten te houden, of in een Automator-actie zodat je gewoon een bestand op het icoon kunt slepen.

We hebben twee dingen nodig: ffprobe (onderdeel van ffmpeg) en jq. Beide zijn te installeren met Homebrew.

Vervolgens maken we een simpel script dat een bestandsnaam als eerste argument accepteert, met ffprobe de hoofdstuk-informatie uitleest, en dan in het juiste formaat de hoofdstukken laat zien.

Je zou dit zelfs nog met de YouTube API kunnen combineren om de hoofdstukken automatisch weg te schrijven naar de omschrijving van een video.

Het script

In het script op zich zijn er een paar dingen waar we rekening mee moeten houden:

  1. Zijn de relevante tools (jq en ffprobe geïnstalleerd?)
  2. Welke date binary hebben we? GNU date (standaard op bijvoorbeeld Linux) en BSD date (standaard op onder andere macOS) hebben andere parameters nodig
  3. Heeft de video’s wel hoofdstukken?
  4. Als er geen hoofdstuk gezet is op seconde 0, zul je standaard een “Chapter 0” krijgen. Die willen we er waarschijnlijk uit filteren.

Met dat allemaal in gedachten is hier het script, met de nodige toelichting:

# Als er geen hoofdstukken in de video data zitten stoppen we sowieso.
# Afhankelijk van je voorkeuren kun je hier een andere exit code dan 0
# gebruiken, zodat het script "faalt" 
# Dat hangt er vooral vanaf of je er van uit gaat dat een video hoofdstukken
# *moet* hebben
EXIT_CODE_NO_CHAPTERS=0

EXIT_CODE_SUCCES=0
EXIT_CODE_NO_FILE_GIVEN=1
EXIT_CODE_JQ_MISSING=2
EXIT_CODE_FFPROBE_MISSING=3

# Als er geen bestand is opgegeven kunnen we gelijk stoppen Laten we er dan van
# uit gaan dat de gebruiker niet *weet* dat ze een bestand moeten opgeven, 
# en de help-instructies printen
if [[ -z "$1" ]]; then
    echo "Usage: $0 <video_file>"
    exit $EXIT_CODE_NO_FILE_GIVEN
fi

# check of ffprobe en jq aanwezig zijn
if ! command -v ffprobe &> /dev/null
then
    echo "ffprobe is missing"
    exit $EXIT_CODE_FFPROBE_MISSING
fi

if ! command -v jq &> /dev/null
then
    echo "jq is missing"
    exit $EXIT_CODE_JQ_MISSING
fi

# We moeten weten of we GNU date of BSD date gebruiken
# BSD date ondersteunt --version niet, dus daarmee testen we
#
# wel gaan we standaard uit van GNU date
date_cmd="date"
date_format='@%s'
if ! date --version &>/dev/null; then
    date_cmd="date -u -r"
    date_format=''
fi

# Hoofdstuk-gegevens uit de video halen, en een error als het niet lukt
chapter_data=$(ffprobe -v quiet -print_format json -show_chapters "$1")
if [[ -z "$chapter_data" ]]; then
    echo "Error extracting chapter data."
    exit $EXIT_CODE_FFPROBE_MISSING
fi

# Als er geen chapter data is hoeven we niet verder
if echo "$chapter_data" | jq -e '.chapters | length == 0' >/dev/null; then
    echo "No chapters found in the video file."
    exit $EXIT_CODE_NO_CHAPTERS
fi

# we krijgen output zoals dit van jq:
#
# Chapter 001
# 0.000000
#
# maar voor YouTube willen we:
#
# 00:00:00 Chapter 001 
echo "$chapter_data" | jq -r '.chapters[] | (.tags.title, .start_time)' | \
{
  # we beginnen zonder titel, zodat het script geforceerd wordt de eerste
  # regel als een titel te beschouwen
  title=""
  start_time=0
  while IFS= read -r line; do
    # als er geen titel is, is deze regel dat
    if [[ -z "$title" ]]; then
      title="$line"

    # als er wel een titel is, is deze regel een tijd die we moeten verwerken
    else
      start_time=$(echo "$line" | jq -r 'tonumber | floor')

      # de automatische "Chapter 001" op seconde 0 hebben we niet nodig, 
      # en slaan we over 
      # we resetten wel de title-variable, zodat het voor de volgende iteratie
      # weer klopt
      if [[ "$title" == "Chapter 001" && "$start_time" == "0" ]]; then
        title=""
        continue
      fi
      formatted_time=$($date_cmd "$start_time" +'%H:%M:%S' $date_format)
      #omdat we nu een titel en een tijd verwerkt hebben, kunnen we de output naar
      #
      echo "$formatted_time $title"
      # reset de titel, zodat we op de volgende iteratie *geen* titel hebben 
      # zo weet het script wat het moet doen met de volgende regel
      title=""
    fi
  done
}