Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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 && \
Expand Down Expand Up @@ -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"]
Expand Down
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,8 @@ the `-e` parameter in the format `<VARIABLE_NAME>=<VALUE>`.
|`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

Expand Down Expand Up @@ -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.
Expand Down
10 changes: 10 additions & 0 deletions apprise.yml.example
Original file line number Diff line number Diff line change
@@ -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
37 changes: 37 additions & 0 deletions rootfs/etc/cont-init.d/57-enhanced-logging.sh
Original file line number Diff line number Diff line change
@@ -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
5 changes: 5 additions & 0 deletions rootfs/startapp.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
18 changes: 18 additions & 0 deletions rootfs/usr/bin/makemkv-log-watcher.sh
Original file line number Diff line number Diff line change
@@ -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
144 changes: 144 additions & 0 deletions rootfs/usr/bin/makemkv-monitor.sh
Original file line number Diff line number Diff line change
@@ -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
74 changes: 74 additions & 0 deletions rootfs/usr/bin/makemkv-notify.py
Original file line number Diff line number Diff line change
@@ -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 <type> <title> <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)