WIP: Production release #6

Draft
fr wants to merge 28 commits from dev into master
Showing only changes of commit 8a832250a0 - Show all commits

View File

@ -0,0 +1,196 @@
+++
title = "Creating a Language-Specific Jellyfin Library"
date = 2024-12-14
description = "How to set up a Jellyfin library for language-specific content"
+++
Managing a multilingual media library can be a challenge, especially if you want to share specific language content with others. In my case, I have a large collection of movies and TV shows in both Czech and English. To help my parents enjoy only Czech-language content, I decided to create a dedicated Jellyfin library for it. Since Jellyfin doesn't natively support filtering libraries by language, I built a custom solution using Bash scripting and Docker.
## The Problem
My media library has the following folder structure for movies:
```
movies/
├── Movie name (Year of release)
│ ├── Movie name resolution.mkv
│ └── Movie name resolution.nfo
```
And for series:
```
series/
├── Series name
│ └── Season 1
│ ├── Series name - S01E01 - Pilot WEBRip-1080p.mkv
│ ├── Series name - S01E01 - Pilot WEBRip-1080p.cs.srt
│ └── Series name - S01E01 - Pilot WEBRip-1080p.en.srt
```
For movies, each film is stored in its own folder. For series, episodes are nested within season folders, under the parent series folder. Jellyfin requires each movie or series to reside in its own directory, which means symlinks must point to the parent folder rather than the media files themselves.
Additionally, I needed to differentiate between movies and series when creating symlinks. For movies, the parent directory is the movie folder, but for series, the symlink should point to the series' root folder (not the season folder). Here's how I solved it.
## Custom Script for Language Filtering
The following script processes the media directories to identify content with Czech audio tracks and creates symlinks to organize them into separate `cz_movies` and `cz_series` directories.
```bash
#!/bin/bash
# Define directories and language codes
SOURCE_DIR_MOVIES="/media/movies"
TARGET_DIR_MOVIES="/media/cz_movies"
SOURCE_DIR_SERIES="/media/series"
TARGET_DIR_SERIES="/media/cz_series"
LANGUAGES_TO_CHECK=("cze" "cz" "Czech" "czech" "česky" "cs" "ces")
FFPROBE="/usr/lib/jellyfin-ffmpeg/ffprobe"
# List of file extensions to skip
SKIP_EXTENSIONS=("nfo" "srt" "sh")
# Function to process a source directory
process_directory() {
local SOURCE_DIR="$1"
local TARGET_DIR="$2"
local IS_SERIES="$3"
# Loop through all files in SOURCE_DIR and subdirectories
find "$SOURCE_DIR" -type f | while read FILE; do
# Get the file extension
EXTENSION="${FILE##*.}"
# Check if the file extension is in the skip list
if [[ " ${SKIP_EXTENSIONS[@]} " =~ " $EXTENSION " ]]; then
# Skip the file if it's in the skip list
continue
fi
echo "Processing $FILE"
# Get the languages from the current file
LANGUAGES=($($FFPROBE -v error -show_entries stream=codec_type:stream_tags=language:stream=codec_name -of default=noprint_wrappers=1:nokey=1 "$FILE" | tr '\n' ' ' | grep -o 'audio [^ ]*' | awk '{print $2}'))
# Check if any of the languages in LANGUAGES_TO_CHECK exist in LANGUAGES
FOUND=false
for LANG_TO_CHECK in "${LANGUAGES_TO_CHECK[@]}"; do
if [[ " ${LANGUAGES[@]} " =~ " $LANG_TO_CHECK " ]]; then
FOUND=true
break
fi
done
if $FOUND; then
echo "Found a matching language (${LANGUAGES_TO_CHECK[@]}) in the list"
# Determine the symlink target based on whether this is a series or a movie
if [ "$IS_SERIES" = true ]; then
# For series, link the <Series name> directory
PARENT_DIR=$(dirname "$(dirname "$FILE")")
else
# For movies, link the <Movie name> directory
PARENT_DIR=$(dirname "$FILE")
fi
# Create a symlink to the parent directory in TARGET_DIR
ln -snf "$PARENT_DIR" "$TARGET_DIR/$(basename "$PARENT_DIR")"
echo "Symlink \"$PARENT_DIR\" \"$TARGET_DIR/$(basename "$PARENT_DIR")\""
echo "Symlink created for $PARENT_DIR"
else
echo "Skipping $FILE"
fi
done
}
# Process movies and series directories
process_directory "$SOURCE_DIR_MOVIES" "$TARGET_DIR_MOVIES" false
process_directory "$SOURCE_DIR_SERIES" "$TARGET_DIR_SERIES" true
```
## Automating the Script with Docker and Cron
To automate the script, I run it hourly using a minimal Docker container with cron installed. Instead of modifying the Jellyfin Docker image, I created a separate Alpine-based container to handle the job.
### Dockerfile for the Cron Container
```Dockerfile
FROM alpine:latest
RUN apk update && \
apk add --no-cache docker && \
rm -rf /var/cache/apk/*
CMD ["sh"]
```
### Docker-Compose Setup
Here's the updated `docker-compose.yml` that includes the Jellyfin service and the cron container:
```yaml
services:
jellyfin:
image: jellyfin/jellyfin:latest
container_name: jellyfin
# ... your jellyfin docker compose ...
volumes:
- ./czech-lib-cron.sh:/czech-lib-cron.sh:ro # the custom library script
- /mnt/media/torrent:/media:ro # your media folder
- /mnt/media/torrent/cz_movies:/media/cz_movies # the newely created language specific library folder
- /mnt/media/torrent/cz_series:/media/cz_series # the newely created language specific library folder
jellyfin-cron:
build:
context: ../docker-cron
dockerfile: Dockerfile
container_name: jellyfin-cron
# Run the custom library script every hour at 45 minutes past
command: >
/bin/sh -c "
echo '45 * * * * docker exec -u root jellyfin bash -c \"/czech-lib-cron.sh\"' > /etc/crontabs/root &&
crond -f -l 2"
volumes:
- /var/run/docker.sock:/var/run/docker.sock # Needed for `docker exec`
networks:
- default
```
## Bonus: Fix Missing Audio Language Metadata
Some files in my library lacked proper audio language metadata, causing them to be missed by the script. To fix this, I wrote another script to add the correct metadata:
```bash
#!/bin/bash
# Root directory (current working directory)
ROOT_DIR="$(pwd)"
# Function to process each video file
process_file() {
local input_file="$1"
local temp_file="${input_file%.avi}_temp.avi"
echo "Processing: $input_file"
# Add metadata to the audio track and overwrite the original file
ffmpeg -i "$input_file" -map 0 -c copy -metadata:s:a:0 language=cze "$temp_file"
if [[ $? -eq 0 ]]; then
mv "$temp_file" "$input_file"
echo "Successfully updated: $input_file"
else
echo "Error updating: $input_file"
rm -f "$temp_file" # Remove temporary file on failure
fi
}
# Export function for use in find's -exec
export -f process_file
# Find all .avi files and process them
find "$ROOT_DIR" -type f -name "*.avi" -exec bash -c 'process_file "$0"' {} \;
```
This script re-encodes files to include the missing language metadata. It defaults to `.avi` files, but you can adjust it as needed.
## Conclusion
With this setup, you can create language-specific libraries in Jellyfin. Make sure to add the /media/cz_movies and /media/cz_series folders as paths for new libraries in the Jellyfin dashboard settings. After adding these libraries, simply scan them to see your filtered content. Happy watching!