Compare commits

...

2 Commits

Author SHA1 Message Date
Vincent Riquer
ee119f07a4 Comment lib/copy/* (#LLM-assisted - Claude Code) 2026-03-13 03:54:31 +01:00
Vincent Riquer
c99825912f Comment config handling (#LLM-assisted - Claude Code) 2026-03-13 02:42:40 +01:00
11 changed files with 322 additions and 30 deletions

7
configure vendored
View File

@ -1,25 +1,28 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# defaults # Default install prefix if --prefix= is not provided
default_prefix=/usr/local default_prefix=/usr/local
#default_bindir=$default_prefix/bin #default_bindir=$default_prefix/bin
#default_libdir=$default_prefix/lib #default_libdir=$default_prefix/lib
#default_sharedir=$default_prefix/share #default_sharedir=$default_prefix/share
# Parse command-line arguments (only --prefix=VALUE is supported)
while (( $# )) while (( $# ))
do do
case "$1" in case "$1" in
--prefix=*) prefix="${1#*=}" --prefix=*) prefix="${1#*=}" # Strip the "--prefix=" portion to get the value
;; ;;
esac esac
shift shift
done done
# Derive install directories from prefix (or default_prefix if unset)
bindir="${prefix:-$default_prefix}"/bin bindir="${prefix:-$default_prefix}"/bin
libdir="${prefix:-$default_prefix}"/lib/AtOM libdir="${prefix:-$default_prefix}"/lib/AtOM
sharedir="${prefix:-$default_prefix}"/share/AtOM sharedir="${prefix:-$default_prefix}"/share/AtOM
docdir="${prefix:-$default_prefix}"/share/doc/AtOM docdir="${prefix:-$default_prefix}"/share/doc/AtOM
# Write Makefile.in so the Makefile can substitute real paths at build time
cat > Makefile.in <<-EOMakefile.in cat > Makefile.in <<-EOMakefile.in
bindir = "$bindir" bindir = "$bindir"
libdir = "$libdir" libdir = "$libdir"

View File

@ -1,27 +1,47 @@
#!/bin/bash #!/bin/bash
# Copyright © 2012-2026 ScriptFanix
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# A copy of the GNU General Public License v3 is includded in the LICENSE file
# at the root of the project.
getConfig() { getConfig() {
# Read the config file line by line; 'key' gets the first word, 'value'
# the rest
while read key value while read key value
do do
case $key in case $key in
'#'*) '#'*)
#comment #comment - skip comment lines
;; ;;
'') '')
#empty line #empty line - skip blank lines
;; ;;
'[general]') '[general]')
# Switch parsing context to the [general] section
context=General context=General
;; ;;
'[source]') '[source]')
# Switch parsing context to the [source] section
context=Source context=Source
;; ;;
\[*\]) \[*\])
# Any other [section] header is a destination name
context=Destination context=Destination
destination="${key#[}" destination="${key#[}"
destination="${destination%]}" destination="${destination%]}"
destinations+=("${destination%]}") destinations+=("${destination%]}") # Append to list of destinations
;; ;;
*) *)
# Dispatch key=value to the handler for the current section context
getConfig$context getConfig$context
;; ;;
esac esac

View File

@ -1,30 +1,50 @@
#!/bin/bash #!/bin/bash
# Copyright © 2012-2026 ScriptFanix
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# A copy of the GNU General Public License v3 is includded in the LICENSE file
# at the root of the project.
getConfigDestination() { getConfigDestination() {
case "$key" in case "$key" in
'enabled') 'enabled')
# 1 = process this destination, 0 = skip it
destinationenabled["$destination"]="$value" destinationenabled["$destination"]="$value"
;; ;;
'path') 'path')
# Strip trailing slash for consistency
destinationpath["$destination"]="${value%/}" destinationpath["$destination"]="${value%/}"
;; ;;
'format') 'format')
case "$value" in case "$value" in
'mp3') 'mp3')
destinationformat["$destination"]=mp3 destinationformat["$destination"]=mp3
# Flag that lame must be present
lameneeded=1 lameneeded=1
# MP3 can't handfle more than 2 channels # MP3 can't handle more than 2 channels
[[ -z ${destinationchannels["$destination"]} ]] \ [[ -z ${destinationchannels["$destination"]} ]] \
&& destinationchannels["$destination"]=2 && destinationchannels["$destination"]=2
;; ;;
'opus') 'opus')
destinationformat["$destination"]=opus destinationformat["$destination"]=opus
# Flag that opusenc must be present
opusencneeded=1 opusencneeded=1
;; ;;
'vorbis') 'vorbis')
destinationformat["$destination"]=vorbis destinationformat["$destination"]=vorbis
# Flag that oggenc must be present
oggencneeded=1 oggencneeded=1
;; ;;
'copy') 'copy')
# Files are copied/hardlinked as-is
destinationformat["$destination"]=copy destinationformat["$destination"]=copy
;; ;;
*) *)
@ -34,6 +54,7 @@ getConfigDestination() {
esac esac
;; ;;
'quality') 'quality')
# Vorbis-only: oggenc quality scale (integer, typically 0-10)
expr='^[0-9]*$' expr='^[0-9]*$'
if ! [[ $value =~ $expr ]] if ! [[ $value =~ $expr ]]
then then
@ -46,27 +67,32 @@ getConfigDestination() {
destinationquality["$destination"]="$value" destinationquality["$destination"]="$value"
;; ;;
*) *)
echo "Invalid parameter \"$key\" for format \"${destinationformat["$destination"]}\"" >&2 echo "Invalid parameter '$key' for" \
"format" \
"'${destinationformat["$destination"]}'" >&2
exit $EFMTINVPARM exit $EFMTINVPARM
;; ;;
esac esac
;; ;;
'normalize') 'normalize')
# Whether to normalize audio volume (via sox --norm)
case $value in case $value in
'true'|'on'|'yes') 'true'|'on'|'yes'|'1')
destinationnormalize["$destination"]=1 destinationnormalize["$destination"]=1
;; ;;
'false'|'off'|'no') 'false'|'off'|'no'|'0')
destinationnormalize["$destination"]=0 destinationnormalize["$destination"]=0
;; ;;
*) *)
echo "normalize takes values:" \ echo "normalize takes values:" \
"'yes' ,'true' ,'on', 'no', 'false',"\ "'yes' ,'true' ,'on', '1', 'no'," \
"'off'" "'false','off', '0'"
;; ;;
esac esac
;; ;;
'bitrate') 'bitrate')
# Opus/MP3: target bitrate in kbps
# integer only; Bash doesn't support floats
expr='^[0-9]*$' expr='^[0-9]*$'
if ! [[ $value =~ $expr ]] if ! [[ $value =~ $expr ]]
then then
@ -88,6 +114,7 @@ getConfigDestination() {
esac esac
;; ;;
'loss') 'loss')
# Opus Forward Error Correction: expected packet loss percentage (0-100)
expr='^[0-9]*$' expr='^[0-9]*$'
if ! [[ $value =~ $expr ]] if ! [[ $value =~ $expr ]]
then then
@ -100,12 +127,15 @@ getConfigDestination() {
destinationloss["$destination"]="$value" destinationloss["$destination"]="$value"
;; ;;
*) *)
echo "$Invalid parameter \"$key\" for format \"${destinationformat["$destination"]}\"" >&2 echo "Invalid parameter '$key' for" \
"format" \
"'${destinationformat["$destination"]}'" >&2
exit $EFMTINVPARM exit $EFMTINVPARM
;; ;;
esac esac
;; ;;
'channels') 'channels')
# Up/downmix to this many channels if needed
expr='^[0-9]*$' expr='^[0-9]*$'
if ! [[ $value =~ $expr ]] if ! [[ $value =~ $expr ]]
then then
@ -116,6 +146,7 @@ getConfigDestination() {
destinationchannels["$destination"]=$value destinationchannels["$destination"]=$value
;; ;;
'frequency') 'frequency')
# Resample to this sample rate in Hz (e.g. 44100)
expr='^[0-9]*$' expr='^[0-9]*$'
if ! [[ $value =~ $expr ]] if ! [[ $value =~ $expr ]]
then then
@ -126,21 +157,24 @@ getConfigDestination() {
destinationfrequency["$destination"]=$value destinationfrequency["$destination"]=$value
;; ;;
'noresample') 'noresample')
# MP3-only: prevent lame from auto-downsampling at low
# bitrates
case $value in case $value in
'true'|'on'|'yes') 'true'|'on'|'yes'|'1')
destinationnoresample["$destination"]=1 destinationnoresample["$destination"]=1
;; ;;
'false'|'off'|'no') 'false'|'off'|'no'|'0')
destinationnoresample["$destination"]=0 destinationnoresample["$destination"]=0
;; ;;
*) *)
echo "noresample takes values:" \ echo "noresample takes values:" \
"'yes' ,'true' ,'on', 'no', 'false',"\ "'yes' ,'true' ,'on', '1', 'no',"\
"'off'" "'false','off', '0'"
;; ;;
esac esac
;; ;;
'rename') 'rename')
# File rename pattern using %{tag} tokens
case "$value" in case "$value" in
*/*) */*)
destinationrenamepath["$destination"]="${value%/*}" destinationrenamepath["$destination"]="${value%/*}"
@ -149,46 +183,62 @@ getConfigDestination() {
destinationrename["$destination"]="${value##*/}" destinationrename["$destination"]="${value##*/}"
;; ;;
'fat32compat') 'fat32compat')
# Strip FAT32-illegal characters (? \ < > : * | ")
# trim spaces/dots
case $value in case $value in
'true'|'on'|'yes') 'true'|'on'|'yes'|'1')
destinationfat32compat["$destination"]=1 destinationfat32compat["$destination"]=1
;; ;;
'false'|'off'|'no') 'false'|'off'|'no'|'0')
destinationfat32compat["$destination"]=0 destinationfat32compat["$destination"]=0
;; ;;
*) *)
echo "fat32compat takes values:" \ echo "fat32compat takes values:" \
"'yes' ,'true' ,'on', 'no', 'false',"\ "'yes' ,'true' ,'on', '1', 'no',"\
"'off'" "'false','off', '0'"
;; ;;
esac esac
;; ;;
'ascii-only') 'ascii-only')
# Transliterate Unicode filenames to ASCII using
#Requires Perl Text::Unidecode
case $value in case $value in
'true'|'on'|'yes') 'true'|'on'|'yes'|'1')
destinationascii["$destination"]=1 destinationascii["$destination"]=1
# Signal that the perl coprocess will
# be needed
textunidecodeneeded=1 textunidecodeneeded=1
;; ;;
'false'|'off'|'no') 'false'|'off'|'no'|'0')
destinationascii["$destination"]=0 destinationascii["$destination"]=0
;; ;;
*) *)
echo "ascii-only takes values:" \ echo "ascii-only takes values:" \
"'yes' ,'true' ,'on', 'no', 'false',"\ "'yes' ,'true' ,'on', '1', 'no',"\
"'off'" "'false','off', '0'"
;; ;;
esac esac
;; ;;
'skip_mime-type') 'skip_mime-type')
# Accumulate pipe-separated list of mime patterns to
# exclude entirely
destinationskipmime[$destination]="${destinationskipmime[$destination]:+${destinationskipmime[$destination]}|}$value" destinationskipmime[$destination]="${destinationskipmime[$destination]:+${destinationskipmime[$destination]}|}$value"
;; ;;
'copy_mime-type') 'copy_mime-type')
# Accumulate pipe-separated list of mime patterns to
# copy verbatim (action=2)
destinationcopymime[$destination]="${destinationcopymime[$destination]:+${destinationcopymime[$destination]}|}$value" destinationcopymime[$destination]="${destinationcopymime[$destination]:+${destinationcopymime[$destination]}|}$value"
;; ;;
'copy_extension') 'copy_extension')
# Accumulate pipe-separated list of file extensions to
# copy verbatim
destinationcopyext[$destination]="${destinationcopyext[$destination]:+${destinationcopyext[$destination]}|}$value" destinationcopyext[$destination]="${destinationcopyext[$destination]:+${destinationcopyext[$destination]}|}$value"
;; ;;
'higher-than') 'higher-than')
# Only re-encode source files with bitrate ABOVE this
# threshold (kbps)
# Files at or below this bitrate will be
# hardlinked/copied instead
expr='^[0-9]*$' expr='^[0-9]*$'
if ! [[ $value =~ $expr ]] if ! [[ $value =~ $expr ]]
then then

View File

@ -1,7 +1,23 @@
#!/bin/bash #!/bin/bash
# Copyright © 2012-2026 ScriptFanix
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# A copy of the GNU General Public License v3 is includded in the LICENSE file
# at the root of the project.
getConfigGeneral() { getConfigGeneral() {
case $key in case $key in
'max-load') 'max-load')
# Target system 1-minute load average
# concurrency is adjusted to stay near this
expr='^[0-9]*$' expr='^[0-9]*$'
if [[ $value =~ $expr ]] if [[ $value =~ $expr ]]
then then
@ -13,6 +29,7 @@ getConfigGeneral() {
unset expr unset expr
;; ;;
'load-interval') 'load-interval')
# How often (seconds) to adjust worker count
expr='^[0-9]*$' expr='^[0-9]*$'
if [[ $value =~ $expr ]] if [[ $value =~ $expr ]]
then then
@ -48,6 +65,7 @@ getConfigGeneral() {
fi fi
;; ;;
2) 2)
# Best-effort class; niceness 0 (highest) to 7 (lowest)
if [ -n "$niceness" ] \ if [ -n "$niceness" ] \
&& (( niceness >= 0 && niceness <= 7 )) && (( niceness >= 0 && niceness <= 7 ))
then then
@ -59,10 +77,11 @@ getConfigGeneral() {
fi fi
;; ;;
3) 3)
# Idle class: only gets I/O when no other process needs it
ionice="ionice -c3 " ionice="ionice -c3 "
;; ;;
*) *)
echo "Invalid ionice parameters $value"\ echo "Invalid ionice class $value"\
>&2 >&2
exit $EIONICE exit $EIONICE
;; ;;
@ -70,15 +89,23 @@ getConfigGeneral() {
fi fi
;; ;;
'temporary-directory') 'temporary-directory')
# Directory for
# * SQLite FIFOs
# * intermediate WAV files
# * debug logs
tempdir="$value" tempdir="$value"
;; ;;
'database') 'database')
# Path to the SQLite database file
database="$value" database="$value"
;; ;;
'skip-timestamp-microsec') 'skip-timestamp-microsec')
# If non-zero, ignore sub-second precision in file timestamps
# Useful on filesystems that don't preserve microseconds
skip_us_timestamp="$value" skip_us_timestamp="$value"
;; ;;
debug) debug)
# Allow config file to raise debug level (but not lower it)
(( value > debug )) && debug=$value (( value > debug )) && debug=$value
;; ;;
esac esac

View File

@ -1,10 +1,27 @@
#!/bin/bash #!/bin/bash
# Copyright © 2012-2026 ScriptFanix
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# A copy of the GNU General Public License v3 is includded in the LICENSE file
# at the root of the project.
getConfigSource() { getConfigSource() {
case "$key" in case "$key" in
'path') 'path')
# Root directory of music collection to transcode from
sourcepath="$value" sourcepath="$value"
;; ;;
'skip') 'skip')
# Directory pattern to exclude from scanning
# Multiple 'skip' entries accumulate into this array
skippeddirectories+=( "$value" ) skippeddirectories+=( "$value" )
;; ;;
esac esac

View File

@ -1,6 +1,22 @@
#!/bin/bash #!/bin/bash
# Copyright © 2012-2026 ScriptFanix
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# A copy of the GNU General Public License v3 is includded in the LICENSE file
# at the root of the project.
printConfig() { printConfig() {
{ {
# Build a pipe-delimited table
# 'column -t -s|' will transform into columns
echo "General|Config file|$cffile" echo "General|Config file|$cffile"
[ -n "$ionice" ] && echo "|IO Nice|$ionice" [ -n "$ionice" ] && echo "|IO Nice|$ionice"
cat <<-EOF cat <<-EOF
@ -12,6 +28,8 @@ printConfig() {
Source|Path|$sourcepath Source|Path|$sourcepath
EOF EOF
# Print skipped directories
# use a continuation prefix after the first
for prune_expression in "${skippeddirectories[@]}" for prune_expression in "${skippeddirectories[@]}"
do do
(( printed )) \ (( printed )) \
@ -21,6 +39,8 @@ printConfig() {
printed=1 printed=1
done done
unset printed unset printed
# Loop over destinations and print their settings
# use the destination name as a key into the associative arrays
for destination in ${destinations[@]} for destination in ${destinations[@]}
do do
cat <<-EOF cat <<-EOF
@ -30,6 +50,7 @@ printConfig() {
|Format|${destinationformat["$destination"]} |Format|${destinationformat["$destination"]}
|Quality|${destinationquality["$destination"]} |Quality|${destinationquality["$destination"]}
EOF EOF
# Show format-specific fields
if [[ ${destinationformat["$destination"]} == opus ]] if [[ ${destinationformat["$destination"]} == opus ]]
then then
echo " |Expected loss|${destinationloss["$destination"]}" echo " |Expected loss|${destinationloss["$destination"]}"
@ -47,6 +68,7 @@ printConfig() {
|Path Change|${destinationrenamepath["$destination"]} |Path Change|${destinationrenamepath["$destination"]}
|File Rename|${destinationrename["$destination"]} |File Rename|${destinationrename["$destination"]}
EOF EOF
# Display pipe-separated mime lists: one entry per row
[ -n "${destinationskipmime["$destination"]}" ] \ [ -n "${destinationskipmime["$destination"]}" ] \
&& echo " |Skipped mime-types|${destinationskipmime["$destination"]//\|/ && echo " |Skipped mime-types|${destinationskipmime["$destination"]//\|/
| |}" | |}"
@ -57,5 +79,5 @@ printConfig() {
&& echo " |Copied extensions|${destinationcopyext["$destination"]//\|/ && echo " |Copied extensions|${destinationcopyext["$destination"]//\|/
| |}" | |}"
done done
}|column -t -s'|' }|column -t -s'|' # Format as aligned columns using '|' as delimiter
} }

View File

@ -1,5 +1,22 @@
#!/bin/bash #!/bin/bash
# Copyright © 2012-2026 ScriptFanix
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# A copy of the GNU General Public License v3 is includded in the LICENSE file
# at the root of the project.
# writeConfig() generates a new atom.cfg from the current in-memory settings.
# Output is sent to stdout so callers can redirect it to any config file path.
# Each setting is annotated with a description.
writeConfig() { writeConfig() {
cat <<-EOCfg cat <<-EOCfg
[general] [general]
@ -47,6 +64,7 @@ path $sourcepath
# * skip <directory>: String. Files in <directory> will be ignored. Note that # * skip <directory>: String. Files in <directory> will be ignored. Note that
# <directory> can be any expression accepted by find. # <directory> can be any expression accepted by find.
EOCfg EOCfg
# Emit one skip line per skipped directory
for dir in "${skippeddirectories[@]}" for dir in "${skippeddirectories[@]}"
do do
echo $'skip\t\t\t'"$dir" echo $'skip\t\t\t'"$dir"
@ -55,6 +73,7 @@ path $sourcepath
EOCfg EOCfg
# Emit one section per configured destination
for destination in "${destinations[@]}" for destination in "${destinations[@]}"
do do
cat <<-EOCfg cat <<-EOCfg
@ -190,6 +209,8 @@ bitrate ${destinationquality["$destination"]}
# be included in that destination. For more than one mime-type, use multiple # be included in that destination. For more than one mime-type, use multiple
# times, as needed. The '*' character is a wildcard. # times, as needed. The '*' character is a wildcard.
EOCfg EOCfg
# Emit one skip_mime-type line per MIME pattern (pipe-separated
# in the array)
destinationskipmime["$destination"]="${destinationskipmime["$destination"]}|" destinationskipmime["$destination"]="${destinationskipmime["$destination"]}|"
while [[ ${destinationskipmime["$destination"]} =~ \| ]] while [[ ${destinationskipmime["$destination"]} =~ \| ]]
do do
@ -203,6 +224,7 @@ bitrate ${destinationquality["$destination"]}
# covers and other images to the destination. In fact, AtOM will try to use # covers and other images to the destination. In fact, AtOM will try to use
# hard links instead of copies. # hard links instead of copies.
EOCfg EOCfg
# Emit one copy_mime-type line per MIME pattern
destinationcopymime["$destination"]="${destinationcopymime["$destination"]}|" destinationcopymime["$destination"]="${destinationcopymime["$destination"]}|"
while [[ ${destinationcopymime["$destination"]} =~ \| ]] while [[ ${destinationcopymime["$destination"]} =~ \| ]]
do do
@ -213,6 +235,7 @@ bitrate ${destinationquality["$destination"]}
# * copy_extension <extension>: Copy files whose name and with ".<extension>" # * copy_extension <extension>: Copy files whose name and with ".<extension>"
EOCfg EOCfg
# Emit one copy_extension line per extension pattern
destinationcopyext["$destination"]="${destinationcopyext["$destination"]}|" destinationcopyext["$destination"]="${destinationcopyext["$destination"]}|"
while [[ ${destinationcopyext["$destination"]} =~ \| ]] while [[ ${destinationcopyext["$destination"]} =~ \| ]]
do do

View File

@ -1,6 +1,25 @@
#!/bin/bash #!/bin/bash
# Copyright © 2012-2026 ScriptFanix
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# A copy of the GNU General Public License v3 is includded in the LICENSE file
# at the root of the project.
copyFiles_action() { copyFiles_action() {
(( cron )) || echo -n $'Copying files...\033[K' (( cron )) || echo -n $'Copying files...\033[K'
# Query all destination_files whose last_change doesn't match the
# source, restricted to mime_type_actions with action=2 (direct copy,
# not transcode).
# The join on mime_type_actions ensures we only copy files destined for
# a destination that has explicitly mapped this MIME type to action=2.
echo ' echo '
SELECT SELECT
source_files.filename, source_files.filename,
@ -22,6 +41,8 @@ copyFiles_action() {
AND mime_type_actions.action = 2; AND mime_type_actions.action = 2;
SELECT "AtOM:NoMoreFiles";' >&3 SELECT "AtOM:NoMoreFiles";' >&3
# Results are NUL-delimited rows; sentinel value signals end of result
# set.
read -u4 -r -d $'\0' line read -u4 -r -d $'\0' line
while ! [[ $line = AtOM:NoMoreFiles ]] while ! [[ $line = AtOM:NoMoreFiles ]]
do do
@ -29,9 +50,14 @@ copyFiles_action() {
read -u4 -r -d $'\0' line read -u4 -r -d $'\0' line
done done
# Wrap all DB updates in a single transaction for performance.
echo 'BEGIN TRANSACTION;' >&3 echo 'BEGIN TRANSACTION;' >&3
for copyfile in "${copyfiles[@]}" for copyfile in "${copyfiles[@]}"
do do
# Each row is a single string with columns joined by
# ::AtOM:SQL:Sep::.
# Strip prefix to extract each field in order, advancing $rest
# each time.
sourcefilename=${copyfile%%::AtOM:SQL:Sep::*} sourcefilename=${copyfile%%::AtOM:SQL:Sep::*}
sourcedir=${sourcefilename%/*} sourcedir=${sourcefilename%/*}
rest="${copyfile#*::AtOM:SQL:Sep::}::AtOM:SQL:Sep::" rest="${copyfile#*::AtOM:SQL:Sep::}::AtOM:SQL:Sep::"
@ -45,20 +71,33 @@ copyFiles_action() {
rest=${rest#*::AtOM:SQL:Sep::} rest=${rest#*::AtOM:SQL:Sep::}
(( count++ )) (( count++ ))
(( cron )) || printf '\b\b\b\b%3i%%' $(( (count * 100) / ${#copyfiles[@]} )) (( cron )) || printf '\b\b\b\b%3i%%' $(( (count * 100) / ${#copyfiles[@]} ))
# If this destination uses a rename/path pattern, delegate path
# resolution to guessPath(), which looks up the
# already-transcoded sibling to find the correct output
# directory.
if [ -n "${destinationrenamepath["$destination"]}" ] if [ -n "${destinationrenamepath["$destination"]}" ]
then then
destdir="$(guessPath)" destdir="$(guessPath)"
guessstatus=$? guessstatus=$?
case $guessstatus in case $guessstatus in
1) 1)
# guessPath found no transcoded
# sibling; skip this file entirely.
continue continue
;; ;;
2) 2)
# Transcoded siblings exist but are not
# yet up to date; defer this copy until
# the next run so the directory is
# stable.
(( postponed++ )) (( postponed++ ))
continue continue
;; ;;
esac esac
else else
# No rename pattern: mirror the source directory
# structure under the destination root, sanitizing each
# path component for the target FS.
destdir="${destinationpath["$destination"]}/" destdir="${destinationpath["$destination"]}/"
if [[ $sourcefilename =~ / ]] if [[ $sourcefilename =~ / ]]
then then
@ -79,6 +118,9 @@ copyFiles_action() {
fi fi
fi fi
fi fi
# Try the cheapest copy methods first: reflink (CoW, same
# filesystem), then hardlink (csame filesystem), falling back
# to a full data copy.
if cp -a --reflink=always \ if cp -a --reflink=always \
"$sourcepath/$sourcefilename" \ "$sourcepath/$sourcefilename" \
"$destdir" \ "$destdir" \
@ -91,6 +133,9 @@ copyFiles_action() {
"$sourcepath/$sourcefilename" \ "$sourcepath/$sourcefilename" \
"$destdir" "$destdir"
then then
# Newlines in filenames are stored as a safe inline
# placeholder so the value can be embedded in SQL
# without breaking row parsing.
destfilename=${sourcefilename//$'\n'/::AtOM:NewLine:SQL:Inline::} destfilename=${sourcefilename//$'\n'/::AtOM:NewLine:SQL:Inline::}
Update destination_files \ Update destination_files \
filename \ filename \

View File

@ -1,12 +1,39 @@
#!/bin/bash #!/bin/bash
# Copyright © 2012-2026 ScriptFanix
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# A copy of the GNU General Public License v3 is includded in the LICENSE file
# at the root of the project.
checkCopy() { checkCopy() {
# Returns true only when the source file is compatible enough with the
# destination profile that it can be copied instead of re-encoded.
# All three conditions must hold simultaneously.
( (
# If the destination doesn't restrict sample rate, any rate is
# acceptable. Otherwise the source rate must match exactly.
[ -z "${destinationfrequency[$destination]}" ] \ [ -z "${destinationfrequency[$destination]}" ] \
|| (( ${rate:-0} == ${destinationfrequency[$destination]} )) || (( ${rate:-0} == ${destinationfrequency[$destination]} ))
) && ( ) && (
# If the destination doesn't restrict channel count, any number
# is acceptable. Otherwise the source channel count must match
# exactly.
[ -z "${destinationchannels[$destination]}" ] \ [ -z "${destinationchannels[$destination]}" ] \
|| (( ${channels:-0} == ${destinationchannels[$destination]} )) || (( ${channels:-0} == ${destinationchannels[$destination]} ))
) && ( ) && (
# Bitrate check: accept if source exactly matches the target
# quality setting, OR if a maximum bps ceiling is configured
# and the source bitrate is at or below it.
# Default of 1000 kbps when bitrate is unknown forces a
# re-encode
(( ${bitrate:-1000} == ${destinationquality[$destination]} )) \ (( ${bitrate:-1000} == ${destinationquality[$destination]} )) \
|| ( || (
[ -n "${destinationmaxbps[$destination]}" ] \ [ -n "${destinationmaxbps[$destination]}" ] \

View File

@ -1,6 +1,29 @@
#!/bin/bash #!/bin/bash
# Copyright © 2012-2026 ScriptFanix
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# A copy of the GNU General Public License v3 is includded in the LICENSE file
# at the root of the project.
guessPath() { guessPath() {
# For copy files (action=2) with a rename pattern, we don't know the
# output directory ourselves: it was determined when the audio siblings
# were transcoded (action=1). We infer it by finding a transcoded
# sibling in the same source directory and reading back its destination
# path.
#
# First query: check whether any transcoded sibling is up to date.
# The LIKE pattern matches files directly inside $sourcedir only
# the NOT LIKE excludes deeper subdirectorie to avoid crossing
# boundaries.
echo 'SELECT IFNULL( ( echo 'SELECT IFNULL( (
SELECT destination_files.last_change SELECT destination_files.last_change
FROM destination_files FROM destination_files
@ -22,10 +45,17 @@ guessPath() {
),"0.0"); ),"0.0");
'>&3 '>&3
read -u4 -r -d $'\0' timestamp read -u4 -r -d $'\0' timestamp
# IFNULL returns "0.0" when no transcoded sibling exists yet; strip the
# decimal and treat as zero to detect the no-sibling case.
if (( ${timestamp/./} == 0 )) if (( ${timestamp/./} == 0 ))
then then
# No transcoded sibling found at all
# caller should postpone this copy.
return 2 return 2
fi fi
# Second query: retrieve the actual destination filename of the most
# recently updated transcoded sibling so we can derive its parent
# directory.
echo 'SELECT IFNULL( ( echo 'SELECT IFNULL( (
SELECT destination_files.filename SELECT destination_files.filename
FROM destination_files FROM destination_files
@ -49,8 +79,12 @@ guessPath() {
read -u4 -r -d $'\0' filename read -u4 -r -d $'\0' filename
if [[ $filename != AtOM:NotFound ]] if [[ $filename != AtOM:NotFound ]]
then then
# Strip the filename component to return only the directory
# portion.
echo "${filename%/*}" echo "${filename%/*}"
else else
# Sibling record exists but has no usable filename — skip this
# file.
return 1 return 1
fi fi
} }

View File

@ -1,6 +1,25 @@
#!/bin/bash #!/bin/bash
# Copyright © 2012-2026 ScriptFanix
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# A copy of the GNU General Public License v3 is includded in the LICENSE file
# at the root of the project.
copyFiles_matching() { copyFiles_matching() {
# Preserve the original file extension so the copy keeps its type
# (e.g. .jpg, .png, .cue) regardless of any rename pattern applied to
# the base name.
local extension="${filename##*.}" local extension="${filename##*.}"
# Try hardlink first (no extra disk space); fall back to a full data
# copy if the source and destination are on different filesystems.
if \ if \
cp -al \ cp -al \
"$sourcepath/$filename" \ "$sourcepath/$filename" \
@ -10,6 +29,11 @@ copyFiles_matching() {
"$sourcepath/$filename" \ "$sourcepath/$filename" \
"${destinationpath[$destination]}/$destdir/$destfile.$extension" "${destinationpath[$destination]}/$destdir/$destfile.$extension"
then then
# Record the new destination path and copy the source
# last_change timestamp via a subquery so the DB reflects when
# the source was last modified.
# old_filename captures the previous path so stale files can be
# cleaned up later.
echo \ echo \
"UPDATE destination_files" \ "UPDATE destination_files" \
"SET filename=" \ "SET filename=" \