I have discovered part of my issue being that filebot will see Argument[0] as the current directory, which causes an Invalid Usage error when the current directory is '/'.
Code: Select all
root@filebot-movie-watcher:/# /opt/bin/filebot-watcher
[2024-11-27 08:22:27] [INFO] ********************************************************************************
[2024-11-27 08:22:27] [INFO] Starting FileBot automation script
[2024-11-27 08:22:30] [INFO] ********************************************************************************
[2024-11-27 08:22:30] [INFO] Watching [/media/watch] for new files...
[2024-11-27 08:22:30] [INFO] Processing video file: /media/watch/8 Mile (2002) (tmdb-65)/8 Mile (2002) [ID=65.1080p.HEVC.x265.SDR .10bit.Opus.6ch.R.3.2 Mbps].mkv
[2024-11-27 08:22:35] [INFO] Executing FileBot command for: /media/watch/8 Mile (2002) (tmdb-65)/
[2024-11-27 08:22:35] [ERROR] FileBot processing failed for: /media/watch/8 Mile (2002) (tmdb-65)/
[2024-11-27 08:22:35] [ERROR] Filebot Command: filebot @/tmp/filebot.args
[2024-11-27 08:22:35] [ERROR] /opt/bin/filebot-watcher: line 216: filebot @/tmp/filebot.args: No such file or directory
[2024-11-27 08:22:35] [ERROR] Filebot Args file [/tmp/filebot.args]:
[2024-11-27 08:22:35] [ERROR]
-script
fn:amc
/media/watch/8 Mile (2002) (tmdb-65)/
--action
test
--output
/media/Movies/
--conflict
auto
-non-strict
--log-file
/data/filebot/logs/amc.2024-11-27.log
--log
all
-no-xattr
--def
unsorted=y
music=n
artwork=y
movieFormat={hd}/{~plex.id % {" {imdb-"+imdbid+"}"} % {" {edition-"+edition+"}" } }{ fn.match(/\\[[A-Z]*\\]/) }{ fn.match(/\\[[A-Za-z]*[ -][a-zA-Z0-9]*\\]/) }{" [" + s3d + "]"}{" [" + hdr + "]"}[{aco}{channels}][{vc}]{"-" + group}
subtitles=en
plex=plex:<REDACTED>
reportError=y
clean=y
movieDB=TheMovieDB
storeReport=/data/filebot/reports
discord=https://discord.com/api/webhooks/<REDACTED>
root@filebot-movie-watcher:/# filebot @/tmp/filebot.args
Run script [fn:amc] at [Wed Nov 27 08:22:40 CST 2024]
Parameter: unsorted = y
Parameter: music = n
Parameter: artwork = y
Parameter: movieFormat = {hd}/{~plex.id % {" {imdb-"+imdbid+"}"} % {" {edition-"+edition+"}" } }{ fn.match(/\\[[A-Z]*\\]/) }{ fn.match(/\\[[A-Za-z]*[ -][a-zA-Z0-9]*\\]/) }{" [" + s3d + "]"}{" [" + hdr + "]"}[{aco}{channels}][{vc}]{"-" + group}
Parameter: subtitles = en
Parameter: plex = *****
Parameter: reportError = y
Parameter: clean = y
Parameter: movieDB = TheMovieDB
Parameter: storeReport = /data/filebot/reports
Parameter: discord = *****
Argument[0]: /
Argument[1]: /media/watch/8 Mile (2002) (tmdb-65)
[TEST] --def artwork is incompatible with --action TEST and has been disabled
[TEST] --def clean is incompatible with --action TEST and has been disabled
[TEST] --def unsorted is incompatible with --action TEST and has been disabled
Invalid usage: output folder [/media/Movies] must not be the same as or be inside of input folder [/, /media/watch/8 Mile (2002) (tmdb-65)]
Abort (×_×)
Code: Select all
Argument[0]: /
Argument[1]: /media/watch/8 Mile (2002) (tmdb-65)
Code: Select all
Invalid usage: output folder [/media/Movies] must not be the same as or be inside of input folder [/, /media/watch/8 Mile (2002) (tmdb-65)]
Code: Select all
root@filebot-movie-watcher:/# filebot -script fn:sysenv
# Local Time #
Wed Nov 27 08:27:36 CST 2024
# Process Tree #
/usr/bin/bash
└─ /usr/bin/dash
└─ /usr/lib/jvm/java-21-openjdk-amd64/bin/java
# Environment Variables #
AMC_ACTION: test
AMC_ARGS: -no-xattr
AMC_ARTWORK: y
AMC_CONFLICT: auto
AMC_DEFS: subtitles=en plex=plex:<REDACTED> reportError=y clean=y movieDB=TheMovieDB storeReport=/data/filebot/reports discord=https://discord.com/api/webhooks/<REDACTED>
AMC_FORMAT_MOVIE: {hd}/{~plex.id % {" {imdb-"+imdbid+"}"} % {" {edition-"+edition+"}" } }{ fn.match(/\\[[A-Z]*\\]/) }{ fn.match(/\\[[A-Za-z]*[ -][a-zA-Z0-9]*\\]/) }{" [" + s3d + "]"}{" [" + hdr + "]"}[{aco}{channels}][{vc}]{"-" + group}
AMC_MUSIC: n
AMC_OUT_DIR: /media/Movies/
AMC_UNSORTED: y
FILEBOT_VERSION: 5.1.6
HOME: /data
HOSTNAME: filebot-movie-watcher
LANG: C.UTF-8
PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
PGID: 568
PGROUP: filebot
PUID: 568
PUSER: filebot
PWD: /
SETTLE_DOWN_CHECK: 5 seconds ago
SETTLE_DOWN_TIME: 300
SHLVL: 1
TERM: xterm
TZ: America/Chicago
UMASK: 002
_: /usr/bin/filebot
# Java System Properties #
application.cache: /data/cache
application.deployment: docker
application.dir: /data
awt.useSystemAAFontSettings: on
file.encoding: UTF-8
file.separator: /
grape.root: /data/grape
http.agent: FileBot/5.1.6
java.class.path: /usr/share/filebot/jar/filebot.jar
java.class.version: 65.0
java.home: /usr/lib/jvm/java-21-openjdk-amd64
java.io.tmpdir: /tmp
java.library.path: /usr/lib/x86_64-linux-gnu/jni
java.net.useSystemProxies: false
java.runtime.name: OpenJDK Runtime Environment
java.runtime.version: 21.0.5+11-Ubuntu-1ubuntu124.10
java.specification.name: Java Platform API Specification
java.specification.vendor: Oracle Corporation
java.specification.version: 21
java.vendor: Ubuntu
java.vendor.url: https://ubuntu.com/
java.vendor.url.bug: https://bugs.launchpad.net/ubuntu/+source/openjdk-21
java.version: 21.0.5
java.version.date: 2024-10-15
java.vm.compressedOopsMode: Zero based
java.vm.info: mixed mode, sharing
java.vm.name: OpenJDK 64-Bit Server VM
java.vm.specification.name: Java Virtual Machine Specification
java.vm.specification.vendor: Oracle Corporation
java.vm.specification.version: 21
java.vm.vendor: Ubuntu
java.vm.version: 21.0.5+11-Ubuntu-1ubuntu124.10
jdk.debug: release
jdk.logger.packages: net.filebot.Logging
jdk.module.path: /usr/share/openjfx/lib
jna.boot.library.name: jnidispatch.system
jna.boot.library.path: /usr/lib/x86_64-linux-gnu/jni
jna.library.path: /usr/lib/x86_64-linux-gnu/jni
jna.noclasspath: true
jna.nosys: false
jna.nounpack: true
line.separator:
native.encoding: UTF-8
net.filebot.WebServices.OpenSubtitles.v1: true
net.filebot.archive.extractor: ShellExecutables
net.filebot.gio.GVFS: /gvfs
org.apache.commons.logging.Log: org.apache.commons.logging.impl.NoOpLog
os.arch: amd64
os.name: Linux
os.version: 6.6.44-production+truenas
path.separator: :
prism.order: sw
stderr.encoding: UTF-8
stdout.encoding: UTF-8
sun.arch.data.model: 64
sun.boot.library.path: /usr/lib/jvm/java-21-openjdk-amd64/lib
sun.cpu.endian: little
sun.io.unicode.encoding: UnicodeLittle
sun.java.command: /usr/share/filebot/jar/filebot.jar -script fn:sysenv
sun.java.launcher: SUN_STANDARD
sun.jnu.encoding: UTF-8
sun.management.compiler: HotSpot 64-Bit Tiered Compilers
sun.net.client.defaultConnectTimeout: 10000
sun.net.client.defaultReadTimeout: 60000
unixfs: false
useCreationDate: false
useExtendedFileAttributes: true
useGVFS: true
user.dir: /
user.home: /data
user.language: en
user.name: root
user.timezone: America/Chicago
# Arguments #
args[0] = -script
args[1] = fn:sysenv
Done ヾ(@⌒ー⌒@)ノ
Code: Select all
#!/bin/bash
# Improved FileBot Automation Script
# Strict mode for better error handling
set -euo pipefail
# Default configuration with sensible defaults
declare -A CONFIG=(
[AMC_MAX_WAIT_TIME]=300
[AMC_BACKOFF_MULTIPLIER]=1
[AMC_ACTION]=duplicate
[AMC_CONFLICT]=auto
[AMC_WATCH_DIR]=/media/watch
[AMC_OUT_DIR]=/media/filebot
[AMC_UNSORTED]=y
[AMC_MUSIC]=y
[AMC_ARTWORK]=y
[AMC_LOG_DIR]=/data/filebot/logs
[AMC_LOG_FILE_PREFIX]=amc
[AMC_LOG_LEVEL]=all
[AMC_LOG_MAX_AGE]=7
[AMC_EXCLUDE_LIST]=.excludes
[AMC_FILEBOT_ARGS_FILE]=/tmp/filebot.args
)
get_log_file() {
today=$(date +%Y-%m-%d)
echo "${CONFIG[AMC_LOG_DIR]}/${CONFIG[AMC_LOG_FILE_PREFIX]}.${today}.log"
}
# Logging function with timestamp and severity levels
log() {
local severity="${1}"
local message="${2}"
local log_file
log_file="$(get_log_file)"
if [ "${severity}" == "TRACE" ]; then
# Log TRACE messages to the log file only.
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [${severity}] ${message}" >> "${log_file}"
else
# Log to file and STDERR so that function log output does not get captured in commands.
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [${severity}] ${message}" | tee -a "${log_file}" >&2
fi
}
# Error handling function
error_exit() {
log "ERROR" "${1}"
exit 1
}
# Validate required directories
validate_directories() {
[[ -d "${CONFIG[AMC_WATCH_DIR]}" ]] || error_exit "Watch directory does not exist: ${CONFIG[AMC_WATCH_DIR]}"
[[ -d "${CONFIG[AMC_OUT_DIR]}" ]] || error_exit "Output directory does not exist: ${CONFIG[AMC_OUT_DIR]}"
[[ -d "${CONFIG[AMC_OUT_DIR]}/Unsorted" ]] || error_exit "Unsorted output directory does not exist: ${CONFIG[AMC_OUT_DIR]}/Unsorted"
}
# Build FileBot command dynamically
build_filebot_command() {
local process_path="$1"
shift
local command=(
"filebot"
#"-script" "fn:sysenv"
"-script" "fn:amc" "'${process_path}'"
"--action" "${CONFIG[AMC_ACTION]}"
"--output" "${CONFIG[AMC_OUT_DIR]}"
"--conflict" "${CONFIG[AMC_CONFLICT]}"
"-non-strict"
"--log-file" "'$(get_log_file)'"
"--log" "${CONFIG[AMC_LOG_LEVEL]}"
)
# Optional Arguments
[[ -n "${AMC_ARGS:-}" ]] && command+=("${AMC_ARGS}")
## --def options below
command+=("--def")
[[ -n "${CONFIG[AMC_UNSORTED]:-}" ]] && command+=("unsorted=${CONFIG[AMC_UNSORTED]}")
[[ -n "${CONFIG[AMC_MUSIC]:-}" ]] && command+=("music=${CONFIG[AMC_MUSIC]}")
[[ -n "${CONFIG[AMC_ARTWORK]:-}" ]] && command+=("artwork=${CONFIG[AMC_ARTWORK]}")
# Optional format overrides
[[ -n "${AMC_FORMAT_MOVIE:-}" ]] && command+=("movieFormat='${AMC_FORMAT_MOVIE}'")
[[ -n "${AMC_FORMAT_SERIES:-}" ]] && command+=("seriesFormat='${AMC_FORMAT_SERIES}'")
[[ -n "${AMC_FORMAT_ANIME:-}" ]] && command+=("animeFormat='${AMC_FORMAT_ANIME}'")
[[ -n "${AMC_FORMAT_MUSIC:-}" ]] && command+=("musicFormat='${AMC_FORMAT_MUSIC}'")
[[ -n "${AMC_FORMAT_UNSORTED:-}" ]] && command+=("unsortedFormat='${AMC_FORMAT_UNSORTED}'")
# Additional optional parameters
[[ -n "${AMC_DEFS:-}" ]] && command+=("${AMC_DEFS}")
[[ -n "${CONFIG[AMC_EXCLUDE_LIST]:-}" && -f "${CONFIG[AMC_EXCLUDE_LIST]}" ]] && command+=("excludeList='${CONFIG[AMC_EXCLUDE_LIST]}'")
## Add remaining args and processing path
# command+=("'${process_path}'")
echo "${command[@]}"
}
# Build FileBot File dynamically
build_filebot_file() {
local process_path="$1"
shift
echo > "${CONFIG[AMC_FILEBOT_ARGS_FILE]}"
{
echo "-script";
echo "fn:amc";
echo "${process_path}";
echo "--action";
echo "${CONFIG[AMC_ACTION]}";
echo "--output";
echo "${CONFIG[AMC_OUT_DIR]}";
echo "--conflict";
echo "${CONFIG[AMC_CONFLICT]}";
echo "-non-strict";
echo "--log-file";
get_log_file;
echo "--log";
echo "${CONFIG[AMC_LOG_LEVEL]}";
echo "-no-xattr";
echo "--def";
[[ -n "${CONFIG[AMC_UNSORTED]:-}" ]] && echo "unsorted=${CONFIG[AMC_UNSORTED]}";
[[ -n "${CONFIG[AMC_MUSIC]:-}" ]] && echo "music=${CONFIG[AMC_MUSIC]}";
[[ -n "${CONFIG[AMC_ARTWORK]:-}" ]] && echo "artwork=${CONFIG[AMC_ARTWORK]}";
[[ -n "${CONFIG[AMC_EXCLUDE_LIST]:-}" && -f "${CONFIG[AMC_EXCLUDE_LIST]}" ]] && echo "excludeList='${CONFIG[AMC_EXCLUDE_LIST]}'";
# Optional format overrides
[[ -n "${AMC_FORMAT_MOVIE:-}" ]] && echo "movieFormat=${AMC_FORMAT_MOVIE}";
[[ -n "${AMC_FORMAT_SERIES:-}" ]] && echo "seriesFormat=${AMC_FORMAT_SERIES}";
[[ -n "${AMC_FORMAT_ANIME:-}" ]] && echo "animeFormat=${AMC_FORMAT_ANIME}";
[[ -n "${AMC_FORMAT_MUSIC:-}" ]] && echo "musicFormat=${AMC_FORMAT_MUSIC}";
[[ -n "${AMC_FORMAT_UNSORTED:-}" ]] && echo "unsortedFormat=${AMC_FORMAT_UNSORTED}";
# Additional optional parameters
# [[ -n "${AMC_DEFS:-}" ]] && echo "${AMC_DEFS}";
} >> "${CONFIG[AMC_FILEBOT_ARGS_FILE]}"
# Use eval and an array to handle quoted strings correctly
local items
eval "items=(${AMC_DEFS})"
for item in "${items[@]}"; do
echo "${item}" >> "${CONFIG[AMC_FILEBOT_ARGS_FILE]}"
done;
command=("filebot" "@${CONFIG[AMC_FILEBOT_ARGS_FILE]}")
echo "${command[@]}"
}
# Exponential backoff with jitter
exponential_backoff() {
local attempt="${1}"
local multiplier="${2}"
local max_sleep="${3}"
local jitter=$((RANDOM % 10))
local sleep_time=$((attempt * attempt * multiplier + jitter))
[[ "${sleep_time}" -gt "${max_sleep}" ]] && sleep_time="${max_sleep}"
log "INFO" "Backing off for ${sleep_time} seconds"
sleep "${sleep_time}"
}
# Check if a file is stable and ready for processing
is_file_stable() {
local file="${1}"
local max_wait=60 # Maximum wait time for file stability
local check_interval=5
local last_size
last_size=$(stat -c %s "${file}")
for ((i = 0; i < max_wait; i += check_interval)); do
sleep "${check_interval}"
local current_size
current_size=$(stat -c %s "${file}")
[[ "${current_size}" -eq "${last_size}" ]] && return 0 # File is stable
last_size="${current_size}"
done
return 1 # File is still changing after max wait time
}
# Process video files
process_video_file() {
local file="${1}"
local mime="${2}"
if ! echo "${mime}" | grep -qE ': video/'; then
log "DEBUG" "Skipping non-video file: ${file}"
return 0
fi
log "INFO" "Processing video file: ${file}"
is_file_stable "${file}" || {
log "WARN" "File is unstable, skipping: ${file}"
return 0
}
# Determine file path for processing
local process_path
process_path="$(dirname "${file}")/"
[[ "${process_path%/}" == "${CONFIG[AMC_WATCH_DIR]%/}" ]] && process_path="${file}"
log "INFO" "Executing FileBot command for: ${process_path}"
local filebot_cmd
mapfile -t filebot_cmd < <(build_filebot_file "${process_path}")
# Capture the output of filebot_cmd
if ! output=$("${filebot_cmd[@]}" 2>&1); then
# Log the error output if the command fails
log "ERROR" "FileBot processing failed for: ${process_path}"
log "ERROR" "Filebot Command: ${filebot_cmd[*]}"
log "ERROR" "${output}"
if [[ -f "${CONFIG[AMC_FILEBOT_ARGS_FILE]}" ]]; then
log "ERROR" "Filebot Args file [${CONFIG[AMC_FILEBOT_ARGS_FILE]}]:"
log "ERROR" "$(cat "${CONFIG[AMC_FILEBOT_ARGS_FILE]}")"
fi
return 1
else
# Log the success output if the command succeeds
log "INFO" "FileBot processing succeeded."
log "INFO" "${output}"
fi
# Clean up empty directories
if [[ -d "${process_path}" ]]; then
log "INFO" "Cleaning up directory: ${process_path}"
find "${process_path}" -type d -empty -delete
fi
}
# Recursive directory cleanup
cleanup_directories() {
local directory="${1}"
local max_depth="${2:-5}" # Prevent excessive recursion
[[ "${max_depth}" -le 0 ]] && return 1
log "DEBUG" "Cleaning directory: ${directory}"
# Remove empty directories
find "${directory}" -mindepth 1 -type d -empty -not -path '.recycle' -delete
# Recursively clean subdirectories
find "${directory}" -mindepth 1 -maxdepth 1 -type d | while read -r subdir; do
[[ "${subdir}" == "${directory}" ]] && continue
cleanup_directories "${subdir}" $((max_depth - 1))
done
}
# Main processing loop
main() {
local log_file
log_file="$(get_log_file)"
log "INFO" "********************************************************************************"
log "INFO" "Starting FileBot automation script"
validate_directories
# Capture the output of filebot_cmd
if ! output=$(filebot -script fn:sysenv --log-file "${log_file}" 2>&1); then
# Log the error output if the command fails
log "ERROR" "Failed to check filebot Environment."
log "ERROR" "${output}"
return 1
else
# Log the success output if the command succeeds
log "TRACE" "FileBot Environment."
log "TRACE" "${output}"
fi
local runs=0
while true; do
log "INFO" "********************************************************************************"
# Delete old log files
find "$(dirname "${log_file}")" \
-maxdepth 1 \
-name "${CONFIG[AMC_LOG_FILE_PREFIX]}*.log" \
-mtime "+${CONFIG[AMC_LOG_MAX_AGE]}" \
-type f \
-delete \
-exec echo "Deleting old log: [{}]" \;
runs=$((runs+1))
log "INFO" "Watching [${CONFIG[AMC_WATCH_DIR]}] for new files..."
# Process video files
find "${CONFIG[AMC_WATCH_DIR]}" -type f \
-not -path '.recycle' \
-not -iname '.plexmatch' \
| while read -r file; do
mime=$(file --mime-type "${file}")
process_video_file "${file}" "${mime}"
done
# Remove Plex match files
find "${CONFIG[AMC_WATCH_DIR]}" -type f -iname '.plexmatch' -delete
# Cleanup directories
cleanup_directories "${CONFIG[AMC_WATCH_DIR]}"
# Intelligent backoff
exponential_backoff "${runs}" "${CONFIG[AMC_BACKOFF_MULTIPLIER]}" "${CONFIG[AMC_MAX_WAIT_TIME]}"
log "INFO" "********************************************************************************"
done
}
# Allow configuration via environment variables
for key in "${!CONFIG[@]}"; do
CONFIG[${key}]="${!key:-${CONFIG[${key}]}}"
done
if [ "${AMC_ACTION}" = "rsyncmove" ]; then
# shellcheck disable=SC2016
CONFIG[AMC_ACTION]="'{ from, to -> rsync -a --no-owner --no-group --no-perms --remove-source-files --progress \"\$from\" \"\$to\" }'"
fi
# Execute main function
main
Code: Select all
if [ "${AMC_ACTION}" = "rsyncmove" ]; then
# shellcheck disable=SC2016
CONFIG[AMC_ACTION]="'{ from, to -> rsync -a --no-owner --no-group --no-perms --remove-source-files --progress \"\$from\" \"\$to\" }'"
fi