commit 0d4c8cc29811769d8139769c8275246761eb4aea Author: Jerry Aldrich Date: Wed Aug 2 12:13:54 2023 +0100 First commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..30b1d0d --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +jellyfin/* diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8fce603 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +data/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a6b3ecc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +FROM python:3.9.17-slim + +# https://pypi.org/project/yt-dlp/ +ARG YT_DLP_VERSION=2023.6.22 + +RUN groupadd -g 1337 python && \ + useradd -r -u 1337 -g python --home-dir /srv python + +RUN chown -R python:python /srv + +RUN mkdir /export && chown python:python /export + +COPY entrypoint.sh /srv/entrypoint.sh +RUN chown python:python /srv/entrypoint.sh +RUN chmod +x /srv/entrypoint.sh + +USER python + +WORKDIR /srv/youtube-dlp + +RUN pip install --user --upgrade yt-dlp==${YT_DLP_VERSION} + +ENTRYPOINT ["/srv/entrypoint.sh", "/export/batch.txt"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..241e84b --- /dev/null +++ b/README.md @@ -0,0 +1,89 @@ +# Pirate YouTube + +> WARNING: This repo only supports Linux at this time + +Do you have an ever growing list of "Watch Later" videos on YouTube? + +Do you also have a significant amount of time coming up where you won't have +access to the Internet? Like, say, a flight to Taiwan? + +This repo has a solution! + +Simply: + - Create a `batch.txt` file with URls to YouTube videos + - Run `docker compose up` + - View your videos at [http://localhost:8080](http://localhost:8080) + - ...PROFIT!!! + +Okay, it's not *quite* that simple. See [Usage](#Usage) for actual steps. + +## Usage + +Clone this repo: + - Run the following: + ``` + git clone git@git.jerryaldrichiii.com:jerry/pirate-youtube.git + ``` + - Create data directory (With `sudo` or root) + ``` + sudo sh init-data-dir.sh + ``` + - Populate 'batch.txt': + ``` + echo "https://www.youtube.com/watch?v=o-YBDTqX_ZU" | sudo tee -a "data/batch.txt" + ``` + > You can also use a text editor, as long as you use sudo/root, or run: + > `chown -R 1337 data/batch.txt` + +### Download YouTube Vidoes + +Start `yt-dlp` container: + +``` +sudo docker compose up ytdlp +``` + +### Start/Configure Jellyfin + +Start Jellyfin: + +``` +sudo docker compose up jellyfin +``` + +Configure Jellyfin: + - Connect to [http://localhost:8080](http://localhost:8080) + - Next -> Add User -> Add Media Library + - Content type -> Mixed Movies and Shows + - Display name: Pirate YouTube + - Click `+` on Folders + - Add /media/pirate-youtube + - Click `Ok` + - Click `Ok` (again) + - Click `Next`, `Next`, `Next` (Your choice on "Remote Access") + - Click `Finish` + - "Sign In" + - Click hamburger menu in top-left (three horizontal bars) + - Administration -> Dashboard -> Scan All Libraries + - Click the "House" icon + - Profit!!! + +## FAQ + +### Why not youtube-dl? + +At the time of this writing, the last release of youtube-dl was 2023/12/17 +which was non-functional with current YouTube URLs + +### How do I add more videos? + +- Add URLs to `data/batch.txt` (make sure you use `sudo` or root) +- Run `docker compose up ytdlp` in an another terminal +- In Jellyfin ([http://localhost:8080](http://localhost:8080)): + - Login as admin, then goto the "Dashboard" and "Scan All Libraries" + +### How do I remove videos? + +- Remove relevant commented URL in `data/batch.txt` (NOTE: use `sudo` or root) +- In Jellyfin ([http://localhost:8080](http://localhost:8080)): + - Login as admin, goto "My Media", then "Pirate YouTube", and "Delete Media" diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..43ed207 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,21 @@ +version: '3.5' +services: + ytdlp: + build: . + container_name: yt-dlp + volumes: + - ./data/jellyfin/media/pirate-youtube:/export/jellyfin + - ./data/batch.txt:/export/batch.txt + - ./entrypoint.sh:/srv/entrypoint.sh + environment: + - LOG_LEVEL=DEBUG + jellyfin: + image: jellyfin/jellyfin + container_name: jellyfin + user: 1337:1337 + volumes: + - ./data/jellyfin/config:/config + - ./data/jellyfin/cache:/cache + - ./data/jellyfin/media:/media + ports: + - 8080:8096 diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 0000000..1c9b296 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,168 @@ +#!/usr/bin/env bash + +# Exit 1 if any command errors +set -e + +# $1 here is the first argument to this script +BATCH_FILE="$1" + +DOWNLOAD_DIR="/export/jellyfin" +YTDLP_LOG_FILE="/export/ytdlp.log" + +# 'cat << 'EOF"' is used here to allow for newlines in the command +YTDLP_COMMAND=$(cat << EOF + /srv/.local/bin/yt-dlp + -P $DOWNLOAD_DIR + --sub-format srt + --write-subs + --write-thumbnail +EOF +) + +printUsage() { + cat << EOF +Usage: $(basename "$0") +EOF +} + +# log() takes a log level and log line and uses the LOG_LEVEL env var to +# control how/when it is logged +# To enable debug logging set LOG_LEVEL=debug as an env var +log() { + # '$1' is the first argument passed to this function + passed_level="$1" + + # '$2' is the first argument passed to this function + log_line="$2" + + # This is passed from the env as 'LOG_LEVEL=' + env_level="$LOG_LEVEL" + + # Convert env_level to int + case $env_level in + "DEBUG") + min_level_int=0;; + "INFO") + min_level_int=1;; + "ERROR") + min_level_int=2;; + *) + min_level_int=1;; + esac + + # Convert passed_level to int + case $passed_level in + "DEBUG") + log_level_int=0;; + "INFO") + log_level_int=1;; + "ERROR") + log_level_int=1;; + *) + log_level_int=99;; + esac + + if [ $log_level_int -ge $min_level_int ]; then + echo "[$passed_level] $log_line" + fi +} + +validate_args() { + # If no argument specified, exit 1 + if [ -z "$BATCH_FILE" ]; then + printUsage + exit 1 + fi + + # Exit 1 if passed batch file is not a file (or doesn't exist) + if [ ! -f "$BATCH_FILE" ]; then + echo "Error: Batch file '$BATCH_FILE' not found" + printUsage + exit 1 + fi + + # Exit 0 if passed file contains no urls + if ! grep -q "^.*http.*" "$BATCH_FILE"; then + echo "File '$BATCH_FILE' contains no URLs" + exit 1 + fi + + if [[ ! -O "$BATCH_FILE" ]]; then + echo "File '$BATCH_FILE' is not owned by user ID '${UID}'" + echo "Run 'chown ${UID} $BATCH_FILE' before mounting" + exit 1 + fi + + if [[ ! -O "$DOWNLOAD_DIR" ]]; then + echo "Directory '$DOWNLOAD_DIR' is not owned by user ID '${UID}'" + echo "Run 'chown -R ${UID} $DOWNLOAD_DIR' before mounting" + exit 1 + fi +} + +process_batchfile() { + # Using `read` parse each line of the file as a '$line' while setting IFS='' + # to prevent lines containing things such as `\n` from being parsed as a + # literal newline + while IFS='' read -r line; do + # Ignore any commented lines RegEx explanation: https://regexr.com/7hs52 + regex_match_leading_comment='^\s*[#].*$' + if [[ "$line" =~ $regex_match_leading_comment ]]; then + # RegEx explanation: https://regexr.com/7i073 + line_without_comment=$(echo "$line" | sed 's/^\s*[#]\s*\(.*\)$/\1/') + log "INFO" "Skipping: $line_without_comment" + continue + fi + + log "INFO" "Downloading: $line" + + # Temporarily allow commands to fail without exiting + set +e + + # Run the command with $line as an argument + # Quotes are used here as a measure to protect against "\n" and the like + # '>' is used to overwrite the log file each call so only the last commands + # output is shown + $YTDLP_COMMAND "$line" > $YTDLP_LOG_FILE 2>&1 + + # Save the exit code of the last command + exit_code="$?" + + # Re-enable exiting 1 on command failure + set -e + + if [ "$exit_code" -ne "0" ]; then + log "ERROR" "Command failed: '$YTDLP_COMMAND'" + cat $YTDLP_LOG_FILE + continue + fi + + log "INFO" "Finished downloading '$line'" + + # By here we can assume the file was downloaded successfully, thus we can + # comment out the line in the file + log "DEBUG" "Commenting out '$line' in '$BATCH_FILE'" + + # Prepend a '#" on the current line in the file + # '|' is used because URLs contain '/' which is the usual sed delimiter + # 'cp' to a buffer file is used because sed cannot modify the mounted + # file's inode when using Docker ('-c' is not available in Ubuntu's sed) + new_file="/export/$(basename "$BATCH_FILE").new" + cp "$BATCH_FILE" "$new_file" + sed -i "s|\($line\)|# \1|" "$new_file" + cp "$new_file" "$BATCH_FILE" + + log "DEBUG" "Finished commenting '$line' from '$BATCH_FILE'" + done <<< "$(cat "$BATCH_FILE")" +} + +main() { + validate_args + + log "INFO" "Processing $BATCH_FILE" + process_batchfile + log "INFO" "Finished processing $BATCH_FILE" +} + +# Run main() +main diff --git a/init-data-dir.sh b/init-data-dir.sh new file mode 100755 index 0000000..3ad4254 --- /dev/null +++ b/init-data-dir.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +# Exit 1 if program not run as root +# We need to run 'chown' which requires root` +if [ "$EUID" -ne 0 ]; then + echo "Please run with sudo or as root" + exit 1 +fi + +# Create base dir +mkdir data + +# Create Jellyfin directories +mkdir data/jellyfin +mkdir data/jellyfin/cache +mkdir data/jellyfin/config +mkdir data/jellyfin/media +mkdir data/jellyfin/media/pirate-youtube + +# Create batch file +touch data/batch.txt + +sudo chown -R 1337:1337 data