diff --git a/Dockerfile b/Dockerfile index e9aa369..827334f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -71,6 +71,13 @@ RUN \ adwaita-qt \ font-croscore +# Install Python and Apprise for optional notifications +RUN \ + add-pkg \ + python3 \ + py3-pip && \ + pip3 install --break-system-packages --no-cache-dir apprise + # Generate and install favicons. RUN \ APP_ICON_URL=https://raw.githubusercontent.com/jlesage/docker-templates/master/jlesage/images/makemkv-icon.png && \ @@ -104,7 +111,9 @@ ENV \ AUTO_DISC_RIPPER_BD_MODE=mkv \ AUTO_DISC_RIPPER_DVD_MODE=mkv \ AUTO_DISC_RIPPER_FORCE_UNIQUE_OUTPUT_DIR=0 \ - AUTO_DISC_RIPPER_NO_GUI_PROGRESS=0 + AUTO_DISC_RIPPER_NO_GUI_PROGRESS=0 \ + ENABLE_DOCKER_LOGGING=0 \ + NOTIFY_START=1 # Define mountable directories. VOLUME ["/storage"] diff --git a/README.md b/README.md index 98123c4..8f0c563 100644 --- a/README.md +++ b/README.md @@ -160,6 +160,8 @@ the `-e` parameter in the format `=`. |`AUTO_DISC_RIPPER_DVD_MODE`| Rip mode of DVD discs. `mkv` is the default mode, where a set of MKV files are produced. When set to `backup`, a copy of the (decrypted) file system of the disc is instead created as an ISO file. | `mkv` | |`AUTO_DISC_RIPPER_FORCE_UNIQUE_OUTPUT_DIR`| When set to `0`, files are written to `/output/DISC_LABEL/`, where `DISC_LABEL` is the label/name of the disc. If this directory exists, then files are written to `/output/DISC_LABEL-XXXXXX`, where `XXXXXX` are random readable characters. When set to `1`, the `/output/DISC_LABEL-XXXXXX` pattern is always used. | `0` | |`AUTO_DISC_RIPPER_NO_GUI_PROGRESS`| When set to `1`, progress of discs ripped by the automatic disc ripper is not shown in the MakeMKV GUI. | `0` | +|`ENABLE_DOCKER_LOGGING`| When set to `1`, streams all MakeMKV output to Docker stdout for better container monitoring. Automatically enabled if notification support is configured. | `0` | +|`NOTIFY_START`| When set to `1` and notifications are configured, sends a notification when a rip starts. | `1` | #### Deployment Considerations @@ -787,6 +789,30 @@ configuring environment variables. > recommended to increase the interval for checking new discs using > `AUTO_DISC_RIPPER_INTERVAL`, to reduce performance impact. +## Notifications + +The container supports optional notifications for disc ripping events via [Apprise](https://github.com/caronc/apprise), which provides support for 80+ notification services including Discord, Slack, Telegram, Email, Pushover, and many more. + +To enable notifications: + +1. Create an `apprise.yml` configuration file in your `/config` directory +2. Add your notification service URLs according to the [Apprise documentation](https://github.com/caronc/apprise/wiki) + +Example `apprise.yml`: +```yaml +urls: + - discord://webhook_id/webhook_token + - mailto://username:password@gmail.com + - slack://TokenA/TokenB/TokenC +``` + +Once configured, you'll receive notifications for: +- **Rip Started** - When a disc ripping begins (can be disabled with `NOTIFY_START=0`) +- **Rip Complete** - When a disc is successfully ripped +- **Rip Failed** - When a disc ripping fails or partially fails + +Note that notifications require debug logging to be enabled, which happens automatically when an `apprise.yml` file is present. + ## Hooks Custom actions can be performed at various disc-ripping stages using hooks. diff --git a/apprise.yml.example b/apprise.yml.example new file mode 100644 index 0000000..0c2eb00 --- /dev/null +++ b/apprise.yml.example @@ -0,0 +1,10 @@ +# Apprise Configuration for MakeMKV Enhanced +# +# Copy this file to your config directory as apprise.yml +# +# Documentation: https://github.com/caronc/apprise/wiki +# Supported services: https://github.com/caronc/apprise#supported-notifications +# +# Example for Discord: +urls: + - discord://WEBHOOK_ID/WEBHOOK_TOKEN \ No newline at end of file diff --git a/rootfs/etc/cont-init.d/57-enhanced-logging.sh b/rootfs/etc/cont-init.d/57-enhanced-logging.sh new file mode 100755 index 0000000..7c3c8a7 --- /dev/null +++ b/rootfs/etc/cont-init.d/57-enhanced-logging.sh @@ -0,0 +1,37 @@ +#!/bin/sh + +set -e # Exit immediately if a command exits with a non-zero status. +set -u # Treat unset variables as an error. + +# Check if Apprise notifications are configured +APPRISE_CONFIGURED=false +if [ -f /config/apprise.yml ]; then + echo "Apprise configuration detected - notifications will be sent." + APPRISE_CONFIGURED=true +fi + +# Enable enhanced logging if requested OR if Apprise is configured +# (notifications require debug logging to work) +if is-bool-val-true "${ENABLE_DOCKER_LOGGING:-0}" || is-bool-val-true "$APPRISE_CONFIGURED"; then + if is-bool-val-true "$APPRISE_CONFIGURED"; then + echo "Enabling debug logging (required for notifications)..." + else + echo "Enabling enhanced Docker logging..." + fi + + # Ensure debug logging is enabled + if [ -f /config/settings.conf ]; then + # Ensure app_ShowDebug is set to "1" + if grep -q "^[ \t]*app_ShowDebug[ \t]*=" /config/settings.conf; then + sed -i 's|^[ \t]*app_ShowDebug[ \t]*=.*|app_ShowDebug = "1"|' /config/settings.conf + else + echo 'app_ShowDebug = "1"' >> /config/settings.conf + fi + + echo "Debug logging enabled." + fi +else + echo "Enhanced Docker logging disabled (ENABLE_DOCKER_LOGGING=0)." +fi + +# vim:ft=sh:ts=4:sw=4:et:sts=4 \ No newline at end of file diff --git a/rootfs/startapp.sh b/rootfs/startapp.sh index 5c1833d..dd9412b 100755 --- a/rootfs/startapp.sh +++ b/rootfs/startapp.sh @@ -22,6 +22,11 @@ if is-bool-val-true "${CONTAINER_DEBUG:-0}"; then DEBUG_ARGS="debug /config/log/makemkv/debug.txt" fi +# Start the log watcher in the background if enhanced logging is enabled +if is-bool-val-true "${ENABLE_DOCKER_LOGGING:-0}" || [ -f "/config/apprise.yml" ]; then + /usr/bin/makemkv-log-watcher.sh & +fi + cd /storage exec /opt/makemkv/bin/makemkv $DEBUG_ARGS -std diff --git a/rootfs/usr/bin/makemkv-log-watcher.sh b/rootfs/usr/bin/makemkv-log-watcher.sh new file mode 100755 index 0000000..2d78582 --- /dev/null +++ b/rootfs/usr/bin/makemkv-log-watcher.sh @@ -0,0 +1,18 @@ +#!/bin/sh +# Watch for MakeMKV log file and tail it to stdout when it appears + +LOG_FILE="/config/MakeMKV_log.txt" +MONITOR_SCRIPT="/usr/bin/makemkv-monitor.sh" + +echo "[log-watcher] Starting MakeMKV log watcher..." + +# Wait for log file to exist +while [ ! -f "$LOG_FILE" ]; do + sleep 2 +done + +echo "[log-watcher] Found MakeMKV log at $LOG_FILE, starting tail..." + +# Tail the log file starting from end (don't process old entries) +# -n0 means start with 0 lines from existing file, only show new additions +tail -n0 -f "$LOG_FILE" 2>/dev/null | $MONITOR_SCRIPT \ No newline at end of file diff --git a/rootfs/usr/bin/makemkv-monitor.sh b/rootfs/usr/bin/makemkv-monitor.sh new file mode 100755 index 0000000..cdc46ea --- /dev/null +++ b/rootfs/usr/bin/makemkv-monitor.sh @@ -0,0 +1,144 @@ +#!/bin/sh +# MakeMKV output monitor with optional notifications + +# Check if apprise config exists +if [ ! -f "/config/apprise.yml" ]; then + # No config = just pass through the output for docker logs + while IFS= read -r line; do + echo "$line" + done + exit 0 +fi + +# Function to send notification via Python script +send_notification() { + local type="$1" + local title="$2" + local details="$3" + + # Call Python notification handler + python3 /usr/bin/makemkv-notify.py "$type" "$title" "$details" 2>/dev/null +} + +# State tracking +CURRENT_TITLE="" +CURRENT_FILE="" +MOVIE_TITLE="" +ERRORS_COUNT=0 +ERROR_TYPE="" +START_TIME="" + +# Process MakeMKV output line by line +while IFS= read -r line; do + # Echo to stdout so tee can still capture it + echo "$line" + + # Detect disc scanning/opening (currently not used for notifications) + # Keeping this in case we want to track disc name for other purposes + if echo "$line" | grep -q "Opening DVD\|Opening Blu-ray\|Scanning title"; then + DISC_NAME=$(echo "$line" | grep -oE '"[^"]+"' | head -1 | tr -d '"' || echo "Unknown") + # Don't set START_TIME here - wait for actual rip start + fi + + # Detect actual rip starting - "Saving X titles into directory" + if echo "$line" | grep -q "Saving.*titles into directory"; then + START_TIME=$(date +%s) + TITLE_COUNT=$(echo "$line" | grep -oE "Saving [0-9]+" | grep -oE "[0-9]+") + OUTPUT_DIR=$(echo "$line" | grep -oE "directory.*" | sed 's/directory //' | sed 's|file://||') + # Extract just the movie title from the path (last component) + MOVIE_TITLE=$(echo "$OUTPUT_DIR" | sed 's|.*/||') + + # Only send start notification if enabled + if is-bool-val-true "${NOTIFY_START:-1}"; then + send_notification "start" "$MOVIE_TITLE" "šŸ“ \`$OUTPUT_DIR\`\\nšŸ“€ Saving \`$TITLE_COUNT\` title(s)" + fi + fi + + # Detect title being processed + if echo "$line" | grep -q "Processing.*playlist\|Saving.*titles"; then + CURRENT_TITLE=$(echo "$line" | grep -oE 'playlist [0-9]+|title [0-9]+' | head -1 || echo "") + [ ! -z "$CURRENT_TITLE" ] && send_notification "progress" "Processing" "šŸ“ Working on $CURRENT_TITLE" + fi + + # Detect output file creation + if echo "$line" | grep -q "Saving to.*\.mkv"; then + CURRENT_FILE=$(echo "$line" | grep -oE '[^/]+\.mkv' | head -1 || echo "") + send_notification "progress" "Saving" "šŸ’¾ Creating file: \`$CURRENT_FILE\`" + fi + + # Detect completion - "Copy complete" is the definitive marker + if echo "$line" | grep -q "Copy complete"; then + # Extract saved and failed counts + SAVED_COUNT=$(echo "$line" | grep -oE "[0-9]+ titles saved" | grep -oE "^[0-9]+" || echo "0") + FAILED_COUNT=$(echo "$line" | grep -oE "[0-9]+ failed" | grep -oE "^[0-9]+" || echo "0") + + END_TIME=$(date +%s) + if [ ! -z "$START_TIME" ]; then + DURATION=$((END_TIME - START_TIME)) + DURATION_HOURS=$((DURATION / 3600)) + DURATION_MINS=$(((DURATION % 3600) / 60)) + DURATION_SECS=$((DURATION % 60)) + + # Format as HH:MM:SS or MM:SS if less than an hour + if [ $DURATION_HOURS -gt 0 ]; then + TIME_STR=$(printf "%02d:%02d:%02d" $DURATION_HOURS $DURATION_MINS $DURATION_SECS) + else + TIME_STR=$(printf "%02d:%02d" $DURATION_MINS $DURATION_SECS) + fi + else + TIME_STR="Unknown" + fi + + # Determine notification type based on counts + if [ "$SAVED_COUNT" = "0" ] && [ "$FAILED_COUNT" != "0" ]; then + # Complete failure + ERROR_DETAIL="āœ… Saved: \`0\` titles\\n" + ERROR_DETAIL="${ERROR_DETAIL}āŒ Failed: \`$FAILED_COUNT\` titles\\n" + [ "$ERRORS_COUNT" -gt 0 ] && [ ! -z "$ERROR_TYPE" ] && ERROR_DETAIL="${ERROR_DETAIL}āš ļø \`$ERRORS_COUNT\` $ERROR_TYPE errors\\n" + ERROR_DETAIL="${ERROR_DETAIL}ā±ļø Duration: \`$TIME_STR\`" + + send_notification "error" "${MOVIE_TITLE:-Unknown Title}" "$ERROR_DETAIL" + + elif [ "$SAVED_COUNT" != "0" ] && [ "$FAILED_COUNT" != "0" ]; then + # Partial success + PARTIAL_INFO="āœ… Saved: \`$SAVED_COUNT\` titles\\n" + PARTIAL_INFO="${PARTIAL_INFO}āŒ Failed: \`$FAILED_COUNT\` titles\\n" + [ "$ERRORS_COUNT" -gt 0 ] && [ ! -z "$ERROR_TYPE" ] && PARTIAL_INFO="${PARTIAL_INFO}āš ļø \`$ERRORS_COUNT\` $ERROR_TYPE errors\\n" + PARTIAL_INFO="${PARTIAL_INFO}ā±ļø Duration: \`$TIME_STR\`" + + send_notification "error" "${MOVIE_TITLE:-Unknown Title}" "$PARTIAL_INFO" + + elif [ "$SAVED_COUNT" != "0" ]; then + # Complete success + FILE_INFO="āœ… Saved: \`$SAVED_COUNT\` titles\\n" + FILE_INFO="${FILE_INFO}ā±ļø Duration: \`$TIME_STR\`" + + send_notification "success" "${MOVIE_TITLE:-Unknown Title}" "$FILE_INFO" + fi + + # Reset state + CURRENT_TITLE="" + CURRENT_FILE="" + MOVIE_TITLE="" + START_TIME="" + ERRORS_COUNT=0 + ERROR_TYPE="" + fi + + # Track specific error types + if echo "$line" | grep -q "Failed to save title"; then + LAST_ERROR_MSG=$(echo "$line" | cut -c1-200) + fi + + if echo "$line" | grep -q "Encountered.*errors of type"; then + ERROR_TYPE=$(echo "$line" | grep -oE "of type '[^']+'" | cut -d"'" -f2) + ERROR_COUNT=$(echo "$line" | grep -oE "Encountered [0-9]+" | grep -oE "[0-9]+") + ERRORS_COUNT=$ERROR_COUNT + ERROR_SUMMARY="$ERROR_COUNT errors of type '$ERROR_TYPE'" + fi + + # Track hash check failures silently (they're common with dirty discs) + if echo "$line" | grep -q "Hash check failed"; then + ERRORS_COUNT=$((ERRORS_COUNT + 1)) + fi +done \ No newline at end of file diff --git a/rootfs/usr/bin/makemkv-notify.py b/rootfs/usr/bin/makemkv-notify.py new file mode 100755 index 0000000..1856d55 --- /dev/null +++ b/rootfs/usr/bin/makemkv-notify.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python3 +""" +Notification handler for MakeMKV using Apprise +Configure via /config/apprise.yml +""" + +import sys +import os +from apprise import Apprise, AppriseConfig + +def send_notification(notification_type, title, details): + """Send notification via Apprise config""" + + # Create Apprise instance + apobj = Apprise() + + # Load config file (required for notifications) + config_file = '/config/apprise.yml' + if not os.path.exists(config_file): + # No config = no notifications, just exit silently + return + + config = AppriseConfig() + config.add(config_file) + apobj.add(config) + + # If no services configured, exit silently + if len(apobj) == 0: + return + + # Format message based on type + if notification_type == 'start': + header = 'Rip Started' + elif notification_type == 'success': + header = 'Rip Complete' + elif notification_type == 'error': + header = 'Rip Failed' + elif notification_type == 'partial': + header = 'Rip Partially Completed' + else: + header = 'MakeMKV Update' + + # Build cleaner message body + message_parts = [] + + # Always use movie icon with title + if title: + message_parts.append(f"šŸŽ¬ **{title}**") + + # Process details - replace literal \n with actual newlines + if details: + details = details.replace('\\n', '\n') + message_parts.append(details) + + body = '\n'.join(message_parts) if message_parts else header + + # Send notification with clean title (no icon) + apobj.notify( + body=body, + title=header, + body_format='markdown' + ) + +if __name__ == '__main__': + # Read arguments from command line + if len(sys.argv) < 4: + print("Usage: notify.py <details>", file=sys.stderr) + sys.exit(1) + + notification_type = sys.argv[1] + title = sys.argv[2] + details = sys.argv[3] + + send_notification(notification_type, title, details) \ No newline at end of file