diff --git a/content/homelab/yt-dlp-tv-show/index.md b/content/homelab/yt-dlp-tv-show/index.md deleted file mode 100644 index 9e6ba3d..0000000 --- a/content/homelab/yt-dlp-tv-show/index.md +++ /dev/null @@ -1,188 +0,0 @@ -+++ -title = "yt-dlp Downloads as TV Shows" -date = 2025-12-10 -description = "" -+++ - -> **TL;DR:** -> With a Nix module, I customize yt-dlp to download YouTube/Bilibili videos and arrange the files that can be recognize by Jellyfin/Emby as "TV shows". - -I like to archive high-quality videos from YouTube and Bilibili. Not that I really fear they might be taken down. For videos I like to watch, I've never encountered that situation. The main reason is for videos that I would like to re-watch quite frequently, not having to dig them from the original website is more convenient. - -You probably know for downloading videos from mainstream video platforms, there is this handy command line tool called [yt-dlp](https://github.com/yt-dlp/yt-dlp). Even if you haven't heard of it, essentially most of the GUI video download programs are using yt-dlp under the hood. It is quite reliable, as long as you always have the latest version. - -By default yt-dlp downloads videos from a given channel/playlist as plain video files. I have a home server, I like to watch videos on TV, and I use a stack of private media servers (Plex/Jellyfin/Emby, see [this post](@/homelab/nixos-home-server/index.md)) that are the primary services I use on the TV. Intuitively I would like to integrate the videos downloaded by yt-dlp to those servers. - -To begin, while I quite like using Plex for "real" movies/TV shows/music streaming, it is not suitable for this scenario. Jellyfin/Emby is the way to go. For those two servers to properly recognize a collection of videos as a TV show, you just need two things. - -**First, file naming and arrangement.** As noted in the [official guide](https://jellyfin.org/docs/general/server/media/shows/), a TV show should be organized into the following structure. - -``` -Series Name A -├── Season 1 -│ ├── S01E01.mkv -│ ├── S01E02.mkv -│ └── S01E03.mkv -└── Season 2 - ├── S02E01.mkv - ├── S02E02.mkv - └── S02E03.mkv -``` - -**Second, optional `.nfo` files.** There can be a `.nfo` file for each show, season, and episode recording corresponding information. - -The first requirement is quite easy to implement using yt-dlp's built-in template functionality. I designed the template so that the upload year will be the season number, and upload month+day will be the episode number. - -``` -output_template="$DOWNLOAD_DIR/$platform_name/%(uploader|Unknown)s/Season %(upload_date>%Y|0000)s/S%(upload_date>%Y|0000)sE%(upload_date>%m%d|0000)s - %(title)s.%(ext)s" -``` - -For example, below is the file structure you will get from a channel download. - -``` -Kitboga -├── Season 2020 -│   ├── S2020E1021 - This Must Be Really Embarrassing, Jay Anderson :).mp4 -│   └── S2020E1025 - Scammer Helps Me Prank Other Scammers.mp4 -└── Season 2021 -    ├── S2021E0513 - When Scammers See $2,100,000 In Your Bank.mp4 -    └── S2021E0515 - This Clueless Scammer Went Crazy After 4 Hours.mp4 -``` - -For the second requirement, yt-dlp has a built-in video metadata extracting functionality, but it writes them into JSON files. We can define logic to parse those JSON files and write the corresponding `.nfo` files: - -```zsh -_generate_jellyfin_nfo() { - local filepath="$1" - [[ -z "$filepath" ]] && return 1 - - local dir=$(dirname "$filepath") - local basename=$(basename "$filepath") - local name_noext="${basename%.*}" - local season_dir="$dir" - local series_dir=$(dirname "$season_dir") - local json_file="$dir/$name_noext.info.json" - - [[ ! -f "$json_file" ]] && return 1 - - local title=$(jq -r '.title // "Unknown"' "$json_file") - local description=$(jq -r '.description // ""' "$json_file" | head -c 2000) - local upload_date=$(jq -r '.upload_date // ""' "$json_file") - local uploader=$(jq -r '.uploader // "Unknown"' "$json_file") - - local season_num="" - local episode_num="" - local aired_date="" - if [[ ${#upload_date} -eq 8 ]]; then - season_num="${upload_date:0:4}" - episode_num="${upload_date:4:4}" - aired_date="${upload_date:0:4}-${upload_date:4:2}-${upload_date:6:2}" - fi - - # XML escape special characters - description=$(echo "$description" | sed 's/&/\&/g; s//\>/g') - title=$(echo "$title" | sed 's/&/\&/g; s//\>/g') - uploader=$(echo "$uploader" | sed 's/&/\&/g; s//\>/g') - - local nfo_file="$dir/$name_noext.nfo" - cat > "$nfo_file" << EPISODENFO - - - $title - $season_num - $episode_num - ${aired_date:-} - $description - -EPISODENFO - - if [[ ! -f "$series_dir/tvshow.nfo" ]]; then - cat > "$series_dir/tvshow.nfo" << TVSHOWNFO - - - $uploader - Videos from $uploader - -TVSHOWNFO - fi - - if [[ ! -f "$season_dir/season.nfo" ]] && [[ -n "$season_num" ]]; then - cat > "$season_dir/season.nfo" << SEASONNFO - - - Season $season_num - $season_num - -SEASONNFO - fi - - # Handle thumbnail: rename and copy as posters - local thumb_file="" - for ext in.webp webp.webp; do - if [[ -f "$dir/$name_noext.$ext" ]]; then - thumb_file="$dir/$name_noext.$ext" - break - fi - done - - if [[ -n "$thumb_file" ]]; then - local thumb_ext="${thumb_file##*.}" - mv "$thumb_file" "$dir/$name_noext-thumb.$thumb_ext" 2>/dev/null - - if [[ ! -f "$series_dir/poster.webp" ]] && [[ ! -f "$series_dir/poster.webp" ]] && [[ ! -f "$series_dir/poster.webp" ]]; then - cp "$dir/$name_noext-thumb.$thumb_ext" "$series_dir/poster.$thumb_ext" - fi - - if [[ ! -f "$season_dir/poster.webp" ]] && [[ ! -f "$season_dir/poster.webp" ]] && [[ ! -f "$season_dir/poster.webp" ]]; then - cp "$dir/$name_noext-thumb.$thumb_ext" "$season_dir/poster.$thumb_ext" - fi - fi -} -``` - -I also include the thumbnail extraction logic in the implementation. The end result is, we will have a `.nfo` file and a thumbnail image coupled with each episode, season, and show. - -``` -PMM_LORD -├── poster.webp -├── Season 2025 -│   ├── poster.webp -│   ├── S2025E1031 - 【PGN】狐狸鸣泣之时——寂静岭f.mp4 -│   ├── S2025E1031 - 【PGN】狐狸鸣泣之时——寂静岭f.nfo -│   ├── S2025E1031 - 【PGN】狐狸鸣泣之时——寂静岭f-thumb.webp -│   ├── S2025E1127 - 【PGN】卡洛斯传奇略人区——宝可梦传说ZA.mp4 -│   ├── S2025E1127 - 【PGN】卡洛斯传奇略人区——宝可梦传说ZA.nfo -│   ├── S2025E1127 - 【PGN】卡洛斯传奇略人区——宝可梦传说ZA-thumb.webp -│   └── season.nfo -└── tvshow.nfo - -``` - -And the episode `.nfo` file will record the title, upload date, and video description as the "plot". - -```xml - - - 【PGN】卡洛斯传奇略人区——宝可梦传说ZA - 2025 - 1127 - 2025-11-27 - 臭外地的来密阿雷要饭了 -谁还有多余队长! -太灵了!三连的朋友都吃上了可颂咖喱! - -``` - -Finally, in Jellyfin/Emby, we can set up a "TV show" type library, but uncheck all the metadata fetching sources so that the information will only be provided by the `.nfo` files. - -![Jellyfin library settings](jellyfin-settings.webp) - -And it works! - -![Jellyfin result](jellyfin-result.webp) - -And of course it also works nicely with third-party clients like Infuse on my mobile devices/TV. - -![Infuse client](infuse-client.webp) - -I packaged all my custom implementation of yt-dlp into a Nix module, which you can take a look in the [this link](https://github.com/Logan-Lin/nix-config/blob/master/modules/yt-dlp.nix) if interested. diff --git a/content/homelab/yt-dlp-tv-show/infuse-client.png b/content/homelab/yt-dlp-tv-show/infuse-client.png deleted file mode 100644 index 43fb64f..0000000 Binary files a/content/homelab/yt-dlp-tv-show/infuse-client.png and /dev/null differ diff --git a/content/homelab/yt-dlp-tv-show/infuse-client.webp b/content/homelab/yt-dlp-tv-show/infuse-client.webp deleted file mode 100644 index 84ccfb9..0000000 Binary files a/content/homelab/yt-dlp-tv-show/infuse-client.webp and /dev/null differ diff --git a/content/homelab/yt-dlp-tv-show/jellyfin-result.png b/content/homelab/yt-dlp-tv-show/jellyfin-result.png deleted file mode 100644 index 738cebe..0000000 Binary files a/content/homelab/yt-dlp-tv-show/jellyfin-result.png and /dev/null differ diff --git a/content/homelab/yt-dlp-tv-show/jellyfin-result.webp b/content/homelab/yt-dlp-tv-show/jellyfin-result.webp deleted file mode 100644 index 5854e6b..0000000 Binary files a/content/homelab/yt-dlp-tv-show/jellyfin-result.webp and /dev/null differ diff --git a/content/homelab/yt-dlp-tv-show/jellyfin-settings.png b/content/homelab/yt-dlp-tv-show/jellyfin-settings.png deleted file mode 100644 index 1d009c5..0000000 Binary files a/content/homelab/yt-dlp-tv-show/jellyfin-settings.png and /dev/null differ diff --git a/content/homelab/yt-dlp-tv-show/jellyfin-settings.webp b/content/homelab/yt-dlp-tv-show/jellyfin-settings.webp deleted file mode 100644 index 02a2aab..0000000 Binary files a/content/homelab/yt-dlp-tv-show/jellyfin-settings.webp and /dev/null differ