New post: Creating a Language-Specific Jellyfin Library
This commit is contained in:
parent
225f2af48b
commit
8a832250a0
196
content/posts/jellyfin-language-specific-library.md
Normal file
196
content/posts/jellyfin-language-specific-library.md
Normal 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!
|
||||
|
Loading…
Reference in New Issue
Block a user