Compare commits

..

No commits in common. "code-documentation" and "master" have entirely different histories.

46 changed files with 81 additions and 1188 deletions

149
atom
View File

@ -1,21 +1,5 @@
#!/usr/bin/env 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.
# Directories for various required data. Set by the configure script and the
# Makefile.
# Also save $IFS just in case.
declare -r \
DOCDIR=%DOCDIR% \
LIBDIR=%LIBDIR% \
@ -29,7 +13,7 @@ declare -r \
## Define exit codes
source "$SHAREDIR"/errorcodes
# Config structures.
# config structures
declare -A \
destinationenabled \
destinationascii \
@ -54,13 +38,10 @@ declare -A \
exit $EBASHVERS
}
# Locales break parsingg.
LC_ALL=C
# Enable extended globbing for some filename manipulations.
shopt -s extglob
# Array of ID3v1 genres, number to name mapping.
source "$SHAREDIR"/id3genres
for function in "$LIBDIR"/*/*
@ -68,7 +49,6 @@ do
source "$function"
done
# Migrate old config to XDG where required
if ! [[ -f "${XDG_CONFIG_HOME:-$HOME/.config}/AtOM/atom.cfg" ]] \
&& [[ -f "$HOME/.atom/atom.cfg" ]]
then
@ -148,9 +128,6 @@ do
done
askconf() {
# Prompt user to interactively create a config if it doesn't exist and
# we're not in cron mode.
# Called recursively until a valid answer is given.
if (( cron ))
then
echo 'Non-interactive, not running setup. Please run atom -S.' >&2
@ -175,7 +152,6 @@ askconf() {
esac
}
# Read config if it exists, call askconf otherwise.
if [ ! -f "$cffile" ]
then
if [ ! -d "${cffile%/*}" ]
@ -187,17 +163,14 @@ then
fi
getConfig
# Uf user wants to changeg config, run setup.
(( forcesetup )) && setup
# Deactivate `!` history expansion, just in case.
set +H
# Apply CLI overrides
[ -n "$cliload" ] && maxload=$cliload
[ -n "$cliltimer" ] && loadinterval=$cliltimer
# Print config if requested or in debug mode. Exit if only dumping config.
(( debug || cfgdump )) && printConfig
(( cfgdump )) && exit
@ -205,11 +178,6 @@ set +H
sanityCheck
openDatabase
# create missing destination directories and DB entries, get destination IDs
# for later use
createDestinations
# Apply destinations "enabled" status in DB with respect to config.
for destination in "${destinations[@]}"
do
if (( ${destinationenabled["$destination"]} ))
@ -220,20 +188,14 @@ do
fi
done
# get source files. Update DB.
createDestinations
getFiles
# Scan mime-types (for new/changed files). Update DB
updateMimes
# Remove source files that are gone from DB. (`last_seen` column).
# FOREIGN KEY `source_file_id` on table `destination_files` gets set to NULL
# by `ON DELETE` parameter. We can use that to find destination files that need
# to be removed.
removeObsoleteFiles
# remove destination files for which the source file is gone. (`source_file_id`
# NUUL -- `rm` them if they exist, remove the DB entry in any case)
(( cron )) || echo -n 'Gathering files for cleaning...'
echo '
SELECT COUNT(id)
@ -241,7 +203,6 @@ echo '
WHERE source_file_id is NULL;' >&3
read -u4 -r -d $'\0' removecount
# Gather in 500 files batches to avoid pipe overflow.
until (( ${#removefile[@]} == removecount ))
do
echo '
@ -273,10 +234,6 @@ done
unset deleted
unset removed
echo 'BEGIN TRANSACTION;' >&3
# Remove the files if they exist. Unconditionnally remove from DB.
# Run in transactrion to speed up the process, COMMIT every 1000th file in case
# process gets killed.
for id in ${!removefile[@]}
do
filename=${removefile[id]}
@ -304,14 +261,8 @@ echo -n "${deleted+$deleted files deleted${removed:+, }}${removed:+$removed remo
(( deleted || removed )) && echo
unset removecount deleted removed removefile
# Update tags for new/changed files and updated tag parsers. Update DB.
# Uses `tags.last_changge` vs `source_files.last_change`
# + `tags.tagreader` vs tagreader versions declared in running version's
# source code.
updateTags
# Reset timestamps for files in destinations that were requested to be fully
# rebuilt.
for forcedest in "${forceall[@]}"
do
if forcedestid=$(Select destinations id <<<"name = $forcedest")
@ -326,18 +277,6 @@ do
fi
done
# Create TEMPORARY (in-memory) tables for tasks. Up to 60 arguments per task
# (including command).
#
# `requires` designates the id of a tasks that must be completed before this
# one can be run.
# `required_by` is a counter of tasks that require this one. Used for temp
# files cleanup.
# `status` is 0 for pending tasks, 2 for failed tasks, 4 for completed tasks
# depended upon (temp file should be left intact).
#
# TRIGGER `fail_depends` sets status of all tasks that depend on a failed task
# to 2
echo '
CREATE TEMPORARY TABLE tasks(
id INTEGER PRIMARY KEY,
@ -431,7 +370,6 @@ echo '
END;
' >&3
# Get number of files to process. Apply `maxbatch` limit if specified.
echo '
SELECT COUNT(source_files.id)
FROM source_files
@ -455,8 +393,6 @@ then
(( togo = filecount - maxbatch ))
filecount=$maxbatch
fi
# Get files to process. Apply `maxbatch` limit if specified.
echo '
SELECT
source_files.id,
@ -503,26 +439,16 @@ echo ';
read -u4 -r -d $'\0' line
while ! [[ $line = AtOM:NoMoreFiles ]]
do
# Append `::AtOM:SQL:Sep::` at the end of the line to make sure we can
# parse empty fields.
decodefiles+=("$line::AtOM:SQL:Sep::")
read -u4 -r -d $'\0' line
done
(( cron )) || echo -n $'Creating tasks...\033[K'
# Spawn perl coprocess for unicode to ascii conversion if needed.
(( textunidecodeneeded )) && ascii
# Generate tasks for each file. Tasks that depend on other tasks (e.g. encoding
# depends on decoding) get the ID of the task they depend on in `requires`
# column. This is used to make sure that encoding doesn't start before decoding
# and other transforms have completed.
echo 'BEGIN TRANSACTION;' >&3
for line in "${decodefiles[@]}"
do
# Parsing SQL output is fun. We use `::AtOM:SQL:Sep::` as separator
# between fields at the end of the line to make sure we can parse empty
# fields.
fileid=${line%%::AtOM:SQL:Sep::*}
rest=${line#*::AtOM:SQL:Sep::}
filename=${rest%%::AtOM:SQL:Sep::*}
@ -567,24 +493,14 @@ do
rest=${rest#*::AtOM:SQL:Sep::}
year=${rest%%::AtOM:SQL:Sep::*}
unset rest
# Skip destinations with formats for which tools are missing.
case ${destinationformat["$destination"]} in
vorbis) (( disableoggenc )) && continue ;;
opus) (( disableopusenc )) && continue ;;
mp3) (( disablelame )) && continue ;;
esac
# Create decoding task depending on mimetype.
decodeFile
# Build target directory path from source file path OR from rename
# pattern if set.
getDestDir
# Same for filename.
getDestFile
# Set copied to 1 for files with extension in `copy_extension`.
for copy_ext in "${destinationcopyext[@]}"
do
if [[ $filename =~ '.*\.'"$copy_ext"'$' ]]
@ -595,17 +511,10 @@ do
done
if (( copied ))
then
# Copy file as-is to destination.
copyFiles_matching
else
# Call suitable function to create encoding task depending on
# destination format.
# encodeFile::mp3
# encodeFile::opus
# encodeFile::vorbis
encodeFile::${destinationformat[$destination]}
fi
# Cleanup variables. Avoids leaking data between iterations.
unset \
album \
albumartist \
@ -648,13 +557,6 @@ echo 'COMMIT;' >&3
# remove perl unicode to ascii coprocess
(( textunidecodeneeded )) && eval exec "${toascii[1]}>&-"
# Main loop. Run up to `concurrency` tasks in parallel, depending on system
# load. Spawn new tasks as old ones complete. Update progress info and commit
# DB every minute.
#
# Start with concurrency = maxload / 2, which seems like a good starting point
# on most systems. If result is 0, force to 1 to make sure we get going. If
# `-f <workers>` option was used, use that value instead and don't change it.
concurrency=$(( maxload / 2 ))
(( concurrency )) || concurrency=1
active=0
@ -675,13 +577,6 @@ do
fi
read humanload garbage < /proc/loadavg
load=${humanload%.*}
# If `-f <workers>` option was used, keep concurrency fixed to that
# value. Otherwise, adjust concurrency according to load and `maxload`
# value. If load is above `maxload`, reduce concurrency by 1 (down to 0
# if `allow_zero_running` is set). If load is below `maxload`, increase
# concurrency by 1 (only if all slots are populated).
# Don't update concurrency more often than every `load-interval` seconds
# to reduce histeresis.
if (( fixed_workers ))
then
concurrency="$fixed_workers"
@ -701,30 +596,9 @@ do
fi
fi
fi
# check if workers have finished.
# If a worker finished with non-zero exit code, mark the task as failed
# in DB. TRIGGER `fail_depends` will fail all tasks that depend on it.
checkworkers
# If task failed, set status to 2, remove temp files if any and
# increment `failed` counter. Don't delete task from DB to keep track
# of failed tasks for final report.
# If task was successful, update destination_files.last_change to
# source_files.last_change, update old_filename to previous
# (destination file) filename, store rename_pattern, fat32compat and
# ascii settings.
# Set status to 4 for tasks that were depended upon by other tasks to
# avoid cleaning up temp files before all tasks depending on them have
# completed, delete task otherwise.
cleaner
# Look for pending tasks that can be started (required tasks's status
# is 4 or reqquires is NULL) and start them.
# Trigger a dump of the tasks table if it's inconsistent (no running
# tasks, no ready tasks, but pending tasks exist) and exit with error
# $ETASKLEFT.
master
# "Fancy" progress info. Only calculate if at least one task has
# succeeded to avoid division by zero.
if (( ran - failed ))
then
currenttime=$timestamp
@ -763,7 +637,6 @@ do
fmtworkers='W:%i/%i'
fmtprogress="T:%${#taskcount}i/%i (F:%i) %3i%%"
fmttime='%2id %2ih%02im%02is (A:%4.1fs/task)'
# Abuse timeformatting to get ETA.
eta="ETA:$(
printf "%(%c)T" "$(( currenttime + secsremaining ))"
)"
@ -782,7 +655,6 @@ do
${minutes:-0} \
${seconds:-0} \
${avgdsec:-0}.${avgdmsec:-0}
# If 0 concurrency is allowed, show paused status when concurrency is 0
if ! (( concurrency )) && ! (( cron ))
then
if (( active ))
@ -796,7 +668,6 @@ done
echo 'COMMIT;' >&3
unset count
# Final report. Calculate elapsed time and format it in human readable way.
endtime=$EPOCHSECONDS
(( elapsedseconds = endtime - starttime ))
@ -831,9 +702,6 @@ endtime=$EPOCHSECONDS
(( cron )) || echo -en "\033[K"
(( ran )) && echo
# If some tasks failed, print them. Don't print failed tasks that did not run
# to avoid confusing tasks marked as failed because they depended on ones that
# failed.
if (( failed ))
then
echo $'\nFailed tasks:\n'
@ -920,8 +788,6 @@ then
done
fi
# Check if there are files that need to be renamed because their rename pattern
# changed.
for destination in "${!destinationpath[@]}"
do
echo '
@ -981,8 +847,6 @@ do
'vorbis') extension=ogg ;;
esac
(( cron )) || echo -en "$destination: rename pattern changed, renaming files...\033[K"
# Spawn perl coprocess for unicode to ascii conversion if
# needed.
(( textunidecodeneeded )) && ascii
echo 'BEGIN TRANSACTION;' >&3
for line in "${renamefiles[@]}"
@ -1058,12 +922,8 @@ do
unset count changedcount renamefiles
done
# Copy files of mime-types matching `copy_mime-type`
copyFiles_action
# Remove files obsoleted by `renamme_pattern`, `ascii` or `fat32compat` changes.
# Based on `destination_files.old_filename` field, populated upon task
# completion.
echo '
SELECT destination_files.id,
destination_files.filename,
@ -1110,9 +970,6 @@ echo 'COMMIT;' >&3
(( cron )) || echo -en "\033[K"
(( count )) && echo
# Remove empty directories in destinations.
# We blindly duplicate the source tree in the destination, so we may end up
# with empty directories.
(( debug )) && echo "Purging empty directories..."
for path in "${destinationpath[@]}"
do

7
configure vendored
View File

@ -1,28 +1,25 @@
#!/usr/bin/env bash
# Default install prefix if --prefix= is not provided
# defaults
default_prefix=/usr/local
#default_bindir=$default_prefix/bin
#default_libdir=$default_prefix/lib
#default_sharedir=$default_prefix/share
# Parse command-line arguments (only --prefix=VALUE is supported)
while (( $# ))
do
case "$1" in
--prefix=*) prefix="${1#*=}" # Strip the "--prefix=" portion to get the value
--prefix=*) prefix="${1#*=}"
;;
esac
shift
done
# Derive install directories from prefix (or default_prefix if unset)
bindir="${prefix:-$default_prefix}"/bin
libdir="${prefix:-$default_prefix}"/lib/AtOM
sharedir="${prefix:-$default_prefix}"/share/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
bindir = "$bindir"
libdir = "$libdir"

View File

@ -1,47 +1,27 @@
#!/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() {
# Read the config file line by line; 'key' gets the first word, 'value'
# the rest
while read key value
do
case $key in
'#'*)
#comment - skip comment lines
#comment
;;
'')
#empty line - skip blank lines
#empty line
;;
'[general]')
# Switch parsing context to the [general] section
context=General
;;
'[source]')
# Switch parsing context to the [source] section
context=Source
;;
\[*\])
# Any other [section] header is a destination name
context=Destination
destination="${key#[}"
destination="${destination%]}"
destinations+=("${destination%]}") # Append to list of destinations
destinations+=("${destination%]}")
;;
*)
# Dispatch key=value to the handler for the current section context
getConfig$context
;;
esac

View File

@ -1,50 +1,30 @@
#!/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() {
case "$key" in
'enabled')
# 1 = process this destination, 0 = skip it
destinationenabled["$destination"]="$value"
;;
'path')
# Strip trailing slash for consistency
destinationpath["$destination"]="${value%/}"
;;
'format')
case "$value" in
'mp3')
destinationformat["$destination"]=mp3
# Flag that lame must be present
lameneeded=1
# MP3 can't handle more than 2 channels
# MP3 can't handfle more than 2 channels
[[ -z ${destinationchannels["$destination"]} ]] \
&& destinationchannels["$destination"]=2
;;
'opus')
destinationformat["$destination"]=opus
# Flag that opusenc must be present
opusencneeded=1
;;
'vorbis')
destinationformat["$destination"]=vorbis
# Flag that oggenc must be present
oggencneeded=1
;;
'copy')
# Files are copied/hardlinked as-is
destinationformat["$destination"]=copy
;;
*)
@ -54,7 +34,6 @@ getConfigDestination() {
esac
;;
'quality')
# Vorbis-only: oggenc quality scale (integer, typically 0-10)
expr='^[0-9]*$'
if ! [[ $value =~ $expr ]]
then
@ -67,32 +46,27 @@ getConfigDestination() {
destinationquality["$destination"]="$value"
;;
*)
echo "Invalid parameter '$key' for" \
"format" \
"'${destinationformat["$destination"]}'" >&2
echo "Invalid parameter \"$key\" for format \"${destinationformat["$destination"]}\"" >&2
exit $EFMTINVPARM
;;
esac
;;
'normalize')
# Whether to normalize audio volume (via sox --norm)
case $value in
'true'|'on'|'yes'|'1')
'true'|'on'|'yes')
destinationnormalize["$destination"]=1
;;
'false'|'off'|'no'|'0')
'false'|'off'|'no')
destinationnormalize["$destination"]=0
;;
*)
echo "normalize takes values:" \
"'yes' ,'true' ,'on', '1', 'no'," \
"'false','off', '0'"
echo "normalize takes values:" \
"'yes' ,'true' ,'on', 'no', 'false',"\
"'off'"
;;
esac
;;
'bitrate')
# Opus/MP3: target bitrate in kbps
# integer only; Bash doesn't support floats
expr='^[0-9]*$'
if ! [[ $value =~ $expr ]]
then
@ -114,7 +88,6 @@ getConfigDestination() {
esac
;;
'loss')
# Opus Forward Error Correction: expected packet loss percentage (0-100)
expr='^[0-9]*$'
if ! [[ $value =~ $expr ]]
then
@ -127,15 +100,12 @@ getConfigDestination() {
destinationloss["$destination"]="$value"
;;
*)
echo "Invalid parameter '$key' for" \
"format" \
"'${destinationformat["$destination"]}'" >&2
echo "$Invalid parameter \"$key\" for format \"${destinationformat["$destination"]}\"" >&2
exit $EFMTINVPARM
;;
esac
;;
'channels')
# Up/downmix to this many channels if needed
expr='^[0-9]*$'
if ! [[ $value =~ $expr ]]
then
@ -146,7 +116,6 @@ getConfigDestination() {
destinationchannels["$destination"]=$value
;;
'frequency')
# Resample to this sample rate in Hz (e.g. 44100)
expr='^[0-9]*$'
if ! [[ $value =~ $expr ]]
then
@ -157,24 +126,21 @@ getConfigDestination() {
destinationfrequency["$destination"]=$value
;;
'noresample')
# MP3-only: prevent lame from auto-downsampling at low
# bitrates
case $value in
'true'|'on'|'yes'|'1')
'true'|'on'|'yes')
destinationnoresample["$destination"]=1
;;
'false'|'off'|'no'|'0')
'false'|'off'|'no')
destinationnoresample["$destination"]=0
;;
*)
echo "noresample takes values:" \
"'yes' ,'true' ,'on', '1', 'no',"\
"'false','off', '0'"
"'yes' ,'true' ,'on', 'no', 'false',"\
"'off'"
;;
esac
;;
'rename')
# File rename pattern using %{tag} tokens
case "$value" in
*/*)
destinationrenamepath["$destination"]="${value%/*}"
@ -183,62 +149,46 @@ getConfigDestination() {
destinationrename["$destination"]="${value##*/}"
;;
'fat32compat')
# Strip FAT32-illegal characters (? \ < > : * | ")
# trim spaces/dots
case $value in
'true'|'on'|'yes'|'1')
'true'|'on'|'yes')
destinationfat32compat["$destination"]=1
;;
'false'|'off'|'no'|'0')
'false'|'off'|'no')
destinationfat32compat["$destination"]=0
;;
*)
echo "fat32compat takes values:" \
"'yes' ,'true' ,'on', '1', 'no',"\
"'false','off', '0'"
echo "fat32compat takes values:" \
"'yes' ,'true' ,'on', 'no', 'false',"\
"'off'"
;;
esac
;;
'ascii-only')
# Transliterate Unicode filenames to ASCII using
#Requires Perl Text::Unidecode
case $value in
'true'|'on'|'yes'|'1')
'true'|'on'|'yes')
destinationascii["$destination"]=1
# Signal that the perl coprocess will
# be needed
textunidecodeneeded=1
;;
'false'|'off'|'no'|'0')
'false'|'off'|'no')
destinationascii["$destination"]=0
;;
*)
echo "ascii-only takes values:" \
"'yes' ,'true' ,'on', '1', 'no',"\
"'false','off', '0'"
"'yes' ,'true' ,'on', 'no', 'false',"\
"'off'"
;;
esac
;;
'skip_mime-type')
# Accumulate pipe-separated list of mime patterns to
# exclude entirely
destinationskipmime[$destination]="${destinationskipmime[$destination]:+${destinationskipmime[$destination]}|}$value"
;;
'copy_mime-type')
# Accumulate pipe-separated list of mime patterns to
# copy verbatim (action=2)
destinationcopymime[$destination]="${destinationcopymime[$destination]:+${destinationcopymime[$destination]}|}$value"
;;
'copy_extension')
# Accumulate pipe-separated list of file extensions to
# copy verbatim
destinationcopyext[$destination]="${destinationcopyext[$destination]:+${destinationcopyext[$destination]}|}$value"
;;
'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]*$'
if ! [[ $value =~ $expr ]]
then

View File

@ -1,23 +1,7 @@
#!/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() {
case $key in
'max-load')
# Target system 1-minute load average
# concurrency is adjusted to stay near this
expr='^[0-9]*$'
if [[ $value =~ $expr ]]
then
@ -29,7 +13,6 @@ getConfigGeneral() {
unset expr
;;
'load-interval')
# How often (seconds) to adjust worker count
expr='^[0-9]*$'
if [[ $value =~ $expr ]]
then
@ -65,7 +48,6 @@ getConfigGeneral() {
fi
;;
2)
# Best-effort class; niceness 0 (highest) to 7 (lowest)
if [ -n "$niceness" ] \
&& (( niceness >= 0 && niceness <= 7 ))
then
@ -77,11 +59,10 @@ getConfigGeneral() {
fi
;;
3)
# Idle class: only gets I/O when no other process needs it
ionice="ionice -c3 "
;;
*)
echo "Invalid ionice class $value"\
echo "Invalid ionice parameters $value"\
>&2
exit $EIONICE
;;
@ -89,23 +70,15 @@ getConfigGeneral() {
fi
;;
'temporary-directory')
# Directory for
# * SQLite FIFOs
# * intermediate WAV files
# * debug logs
tempdir="$value"
;;
'database')
# Path to the SQLite database file
database="$value"
;;
'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"
;;
debug)
# Allow config file to raise debug level (but not lower it)
(( value > debug )) && debug=$value
;;
esac

View File

@ -1,27 +1,10 @@
#!/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() {
case "$key" in
'path')
# Root directory of music collection to transcode from
sourcepath="$value"
;;
'skip')
# Directory pattern to exclude from scanning
# Multiple 'skip' entries accumulate into this array
skippeddirectories+=( "$value" )
;;
esac

View File

@ -1,22 +1,6 @@
#!/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() {
{
# Build a pipe-delimited table
# 'column -t -s|' will transform into columns
echo "General|Config file|$cffile"
[ -n "$ionice" ] && echo "|IO Nice|$ionice"
cat <<-EOF
@ -28,19 +12,15 @@ printConfig() {
Source|Path|$sourcepath
EOF
# Print skipped directories
# use a continuation prefix after the first
for prune_expression in "${skippeddirectories[@]}"
do
(( printed )) \
(( printed )) \
&& echo -n ' | |' \
|| echo -n ' |Skipped directories|'
echo "$prune_expression"
printed=1
done
unset printed
# Loop over destinations and print their settings
# use the destination name as a key into the associative arrays
for destination in ${destinations[@]}
do
cat <<-EOF
@ -50,7 +30,6 @@ printConfig() {
|Format|${destinationformat["$destination"]}
|Quality|${destinationquality["$destination"]}
EOF
# Show format-specific fields
if [[ ${destinationformat["$destination"]} == opus ]]
then
echo " |Expected loss|${destinationloss["$destination"]}"
@ -68,7 +47,6 @@ printConfig() {
|Path Change|${destinationrenamepath["$destination"]}
|File Rename|${destinationrename["$destination"]}
EOF
# Display pipe-separated mime lists: one entry per row
[ -n "${destinationskipmime["$destination"]}" ] \
&& echo " |Skipped mime-types|${destinationskipmime["$destination"]//\|/
| |}"
@ -79,5 +57,5 @@ printConfig() {
&& echo " |Copied extensions|${destinationcopyext["$destination"]//\|/
| |}"
done
}|column -t -s'|' # Format as aligned columns using '|' as delimiter
}|column -t -s'|'
}

View File

@ -1,22 +1,5 @@
#!/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() {
cat <<-EOCfg
[general]
@ -64,7 +47,6 @@ path $sourcepath
# * skip <directory>: String. Files in <directory> will be ignored. Note that
# <directory> can be any expression accepted by find.
EOCfg
# Emit one skip line per skipped directory
for dir in "${skippeddirectories[@]}"
do
echo $'skip\t\t\t'"$dir"
@ -73,7 +55,6 @@ path $sourcepath
EOCfg
# Emit one section per configured destination
for destination in "${destinations[@]}"
do
cat <<-EOCfg
@ -209,8 +190,6 @@ bitrate ${destinationquality["$destination"]}
# be included in that destination. For more than one mime-type, use multiple
# times, as needed. The '*' character is a wildcard.
EOCfg
# Emit one skip_mime-type line per MIME pattern (pipe-separated
# in the array)
destinationskipmime["$destination"]="${destinationskipmime["$destination"]}|"
while [[ ${destinationskipmime["$destination"]} =~ \| ]]
do
@ -224,7 +203,6 @@ bitrate ${destinationquality["$destination"]}
# covers and other images to the destination. In fact, AtOM will try to use
# hard links instead of copies.
EOCfg
# Emit one copy_mime-type line per MIME pattern
destinationcopymime["$destination"]="${destinationcopymime["$destination"]}|"
while [[ ${destinationcopymime["$destination"]} =~ \| ]]
do
@ -235,7 +213,6 @@ bitrate ${destinationquality["$destination"]}
# * copy_extension <extension>: Copy files whose name and with ".<extension>"
EOCfg
# Emit one copy_extension line per extension pattern
destinationcopyext["$destination"]="${destinationcopyext["$destination"]}|"
while [[ ${destinationcopyext["$destination"]} =~ \| ]]
do

View File

@ -1,25 +1,6 @@
#!/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() {
(( 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 '
SELECT
source_files.filename,
@ -41,8 +22,6 @@ copyFiles_action() {
AND mime_type_actions.action = 2;
SELECT "AtOM:NoMoreFiles";' >&3
# Results are NUL-delimited rows; sentinel value signals end of result
# set.
read -u4 -r -d $'\0' line
while ! [[ $line = AtOM:NoMoreFiles ]]
do
@ -50,14 +29,9 @@ copyFiles_action() {
read -u4 -r -d $'\0' line
done
# Wrap all DB updates in a single transaction for performance.
echo 'BEGIN TRANSACTION;' >&3
for copyfile in "${copyfiles[@]}"
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::*}
sourcedir=${sourcefilename%/*}
rest="${copyfile#*::AtOM:SQL:Sep::}::AtOM:SQL:Sep::"
@ -71,33 +45,20 @@ copyFiles_action() {
rest=${rest#*::AtOM:SQL:Sep::}
(( count++ ))
(( 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"]}" ]
then
destdir="$(guessPath)"
guessstatus=$?
case $guessstatus in
1)
# guessPath found no transcoded
# sibling; skip this file entirely.
continue
;;
2)
# Transcoded siblings exist but are not
# yet up to date; defer this copy until
# the next run so the directory is
# stable.
(( postponed++ ))
continue
;;
esac
else
# No rename pattern: mirror the source directory
# structure under the destination root, sanitizing each
# path component for the target FS.
destdir="${destinationpath["$destination"]}/"
if [[ $sourcefilename =~ / ]]
then
@ -118,9 +79,6 @@ copyFiles_action() {
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 \
"$sourcepath/$sourcefilename" \
"$destdir" \
@ -133,9 +91,6 @@ copyFiles_action() {
"$sourcepath/$sourcefilename" \
"$destdir"
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::}
Update destination_files \
filename \

View File

@ -1,39 +1,12 @@
#!/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() {
# 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]}" ] \
|| (( ${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]}" ] \
|| (( ${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]} )) \
|| (
[ -n "${destinationmaxbps[$destination]}" ] \

View File

@ -1,29 +1,6 @@
#!/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() {
# 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( (
SELECT destination_files.last_change
FROM destination_files
@ -45,17 +22,10 @@ guessPath() {
),"0.0");
'>&3
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 ))
then
# No transcoded sibling found at all
# caller should postpone this copy.
return 2
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( (
SELECT destination_files.filename
FROM destination_files
@ -79,12 +49,8 @@ guessPath() {
read -u4 -r -d $'\0' filename
if [[ $filename != AtOM:NotFound ]]
then
# Strip the filename component to return only the directory
# portion.
echo "${filename%/*}"
else
# Sibling record exists but has no usable filename — skip this
# file.
return 1
fi
}

View File

@ -1,25 +1,6 @@
#!/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() {
# 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##*.}"
# Try hardlink first (no extra disk space); fall back to a full data
# copy if the source and destination are on different filesystems.
if \
cp -al \
"$sourcepath/$filename" \
@ -29,11 +10,6 @@ copyFiles_matching() {
"$sourcepath/$filename" \
"${destinationpath[$destination]}/$destdir/$destfile.$extension"
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 \
"UPDATE destination_files" \
"SET filename=" \

View File

@ -1,18 +1,4 @@
#!/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.
Delete() {
#Delete table < where_key where_operator where_value
# [where_key where_operator where_value
@ -24,16 +10,13 @@ Delete() {
value \
where_statement \
results
# Build WHERE clause from stdin: one "key op value" triple per line
while read key operator value
do
(( ${#where_statement} )) && where_statement+=( "AND" )
if [[ $value == NULL ]]
then
# NULL comparisons require IS NULL, not = "NULL"
where_statement+=( "$key is NULL" )
else
# Double embedded quotes to safely escape string values
where_statement+=( "$key $operator "'"'"${value//\"/\"\"}"'"' )
fi
done

View File

@ -1,32 +1,14 @@
#!/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.
Insert() {
#Insert table [no_id] < key value
# [key value
# […]]
#
# If no_id is set to a non-zero value, the function will not return the
# auto-assigned row ID of the inserted row. 
local \
table="$1" \
no_id="${2:-0}" \
insert_keys \
insert_values \
results
# Build column list and value list from stdin key-value pairs
while read key value
do
(( ${#insert_keys} )) && insert_keys+=","
@ -34,24 +16,16 @@ Insert() {
(( ${#insert_values} )) && insert_values+=","
case $value in
'::AtOM:FT::'*)
# Force-text prefix: strip the marker and quote
# as string (prevents numeric-looking values
# from being stored as int / float)
value="${value//::AtOM:FT::/}"
insert_values+='"'"${value//\"/\"\"}"'"'
;;
'NULL')
# Insert SQL NULL (not the string "NULL")
insert_values+="NULL"
;;
+([0-9])?(.+([0-9])))
# Pure integer or decimal: insert unquoted for
# numeric storage
insert_values+=$value
;;
*)
# General string: restore encoded newlines,
# then quote
value=${value//::AtOM:NewLine:SQL:Inline::/$'\n'}
insert_values+='"'"${value//\"/\"\"}"'"'
;;
@ -61,7 +35,6 @@ Insert() {
"( $insert_keys )" \
"VALUES" \
"( $insert_values );" >&3
# Unless no_id is set, return the auto-assigned row ID
(( no_id )) || {
echo 'SELECT LAST_INSERT_ROWID();' >&3
read -u4 -r -d $'\0' results

View File

@ -1,23 +1,6 @@
#!/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.
InsertIfUnset() {
#InsertIfUnset table [no_id] < key value \n key value
#
# If no_id is set to a non-zero value, the function will not return the
# auto-assigned row ID of the inserted row. 
local \
table="$1" \
no_id="${2:-0}" \
@ -27,32 +10,27 @@ InsertIfUnset() {
results \
value \
values
# Read all key-value pairs from stdin into parallel arrays
while read key value
do
keys+=( "$key" )
values+=( "$value" )
done
# Choose which column to return: first key column if no_id, else 'id'
if (( no_id ))
then
column="${keys[0]}"
else
column='id'
fi
# Check if a matching row already exists
if ! results=$(
Select "$table" "$column" < <(
for key in ${!keys[@]}
do
# Strip ::AtOM:FT:: for WHERE comparison
echo "${keys[$key]}" = \
"${values[$key]//::AtOM:FT::}"
done
)
)
then
# Row not found: insert it and return the new id
results=$(
Insert "$table" < <(
for key in ${!keys[@]}

View File

@ -1,18 +1,4 @@
#!/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.
InsertOrUpdate() {
#InsertOrUpdate table set_key set_value [set_key set_value […]] < where_key where_value
# [where_key where_value
@ -29,8 +15,6 @@ InsertOrUpdate() {
what \
results
shift
# Parse positional args as alternating key/value pairs for the SET
# clause
what=key
for argument
do
@ -45,13 +29,11 @@ InsertOrUpdate() {
;;
esac
done
# Read WHERE conditions from stdin
while read key value
do
keys+=( "$key" )
values+=( "$value" )
done
# Check if a matching row exists using the WHERE keys
if results=$(
Select "$table" ${keys[0]} < <(
for key in ${!keys[@]}
@ -61,7 +43,6 @@ InsertOrUpdate() {
)
)
then
# Row exists: update it with the SET values
Update "$table" "$@" < <(
for key in ${!keys[@]}
do
@ -69,8 +50,6 @@ InsertOrUpdate() {
done
)
else
# Row not found: insert combining SET columns and WHERE-match
# columns
results=$(
Insert "$table" < <(
for key in ${!set_keys[@]}

View File

@ -1,18 +1,4 @@
#!/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.
Select() {
#Select table [col1 [col2 [..]]] < WHERE_key WHERE_operator WHERE_value
# [WHERE_key WHERE_operator WHERE_value
@ -25,28 +11,23 @@ Select() {
results \
where_statement
shift
# Build column list
for col
do
(( ${#columns} )) && columns+=','
columns+="$col"
done
# Build WHERE clause from stdin triplets
while read key operator value
do
(( ${#where_statement} )) && where_statement+=( "AND" )
# Restore encoded newlines before embedding in SQL
value=${value//::AtOM:NewLine:SQL:Inline::/$'\n'}
where_statement+=( "$key $operator "'"'"${value//\"/\"\"}"'"' )
done
# Use IFNULL so SQLite always produces output.
echo "SELECT IFNULL(" \
"(SELECT $columns FROM $table" \
"WHERE ${where_statement[@]})" \
",'SQL::Select:not found'" \
");" >&3
read -u 4 -r -d $'\0' results
# Return exit code 1 if the sentinel value indicates no row was found
if ! [[ $results == "SQL::Select:not found" ]]
then
echo "$results"

View File

@ -1,18 +1,4 @@
#!/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.
Update() {
#Update table set_key set_value [set_key set_value […]] < where_key where_operator where_value
# [where_key where_operator where_value
@ -30,22 +16,17 @@ Update() {
where_statement \
results
shift
# Parse positional args as alternating key/value for the SET clause
what=key
for argument
do
case $what in
key)
# Backtick-quote column name to handle reserved
# words
set_statement="${set_statement:+${set_statement},}\`$argument\`"
what=value
;;
value)
case $argument in
'::AtOM:FT::'*)
# Force-text: strip prefix and
# quote as string
argument="${argument//::AtOM:FT::/}"
set_statement+=" = "'"'"${argument//\"/\"\"}"'"'
;;
@ -53,7 +34,6 @@ Update() {
set_statement+=" = NULL"
;;
+([0-9])?(.+([0-9])))
# Numeric value: store unquoted
set_statement+=" = $argument"
;;
*)
@ -64,7 +44,6 @@ Update() {
;;
esac
done
# Build WHERE clause from stdin
while read key operator value
do
(( ${#where_statement} )) && where_statement+=( "AND" )
@ -73,7 +52,6 @@ Update() {
where_statement+=( "$key is NULL" )
;;
+([0-9.]))
# Numeric: compare without quotes
where_statement+=( "$key $operator $value" )
;;
*)

View File

@ -1,53 +1,26 @@
#!/usr/bin/env 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.
# Current schema version this AtOM binary understands
currentdbversion=8
checkDatabaseVersion() {
local dbversion
# Try to read the stored version from the 'atom' metadata table
if dbversion=$(Select atom version <<<"\"1\" = 1")
then
if (( dbversion == currentdbversion ))
then
return 0 # Already up to date
return 0
elif (( dbversion < currentdbversion ))
then
# Run sequential upgrade functions until we reach
# `$currentdbversion`
until (( dbversion == currentdbversion ))
do
# Dynamically calls e.g. upgradedatabase_3_4
upgradedatabase_${dbversion}_$((dbversion+1))
# After each upgrade, re-read the version from
# the database to ensure it was updated
# correctly
dbversion=$(Select atom version <<<"\"1\" = 1")
done
else
# DB was created by a newer AtOM; we can't run and
# ensure consistency
echo "Database schema version $dbversion is" \
"higher thanthat of this version of" \
"AtOM ($currentdbversion). Bailing out." >&2
exit $EDBVERSION
fi
else
# No version row found: this is a database from very early
# drafts
# This is stupid but nobody is running with DB schema v0 anyway
Insert atom 1 <<<"version $currentdbversion"
fi
}

View File

@ -1,31 +1,11 @@
#!/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.
closeDatabase() {
# Run VACUUM to reclaim space and defragment the database before closing
echo 'vacuum;' >&3
# Tell sqlite3 to exit cleanly
echo .quit >&3
(( debug )) && echo -n "Waiting for SQLite to terminate... "
# Close the debug tee fd if it was opened (debug level > 2)
(( debug > 2 )) && exec 5>&-
# Wait for the sqlite3 background process to fully exit
wait $db_pid
(( debug )) && echo OK
# Close the write end of the SQLite input FIFO (FD 3)
exec 3>&-
# Close the read end of the SQLite output FIFO (FD 4)
exec 4<&-
}

View File

@ -1,35 +1,11 @@
#!/usr/bin/env 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.
openDatabase() {
local \
populate_db
# If the DB file doesn't exist yet, mark it for schema population
[[ -f "$database" ]] || populate_db=1
# Create named FIFOs for bidirectional communication with sqlite3
rm -f "$tempdir"/sqlite.{in,out}
mkfifo "$tempdir"/sqlite.{in,out}
# Start sqlite3 in background:
# - '-newline ::AtOM:SQL:EOL::' makes each row end with our custom
# marker. Allows storing newlines.
# - pipe through sed to convert the custom EOL to NUL bytes
# (for 'read -d $'\0'')
# - sed also removes the trailing newline that follows each NUL
stdbuf -o0 sqlite3 -bail \
-newline $'::AtOM:SQL:EOL::\n' \
"$database" \
@ -37,42 +13,21 @@ openDatabase() {
| stdbuf -o0 \
sed 's/::AtOM:SQL:EOL::/\x0/g;s/\(\x0\)\xA/\1/g' \
> "$tempdir/sqlite.out" &
# Store the PID of the background sqlite3 process so we can wait for it
# to exit
db_pid=$!
# Open FD 3 as the write end (send SQL commands to sqlite3)
exec 3> "$tempdir"/sqlite.in
# Open FD 4 as the read end (receive query results from sqlite3)
exec 4< "$tempdir"/sqlite.out
# FIFOs can be deleted immediately after opening; the fds keep them
# alive
rm "$tempdir"/sqlite.{in,out}
# At debug level > 2, tee all SQL to a debug log file (FD 5 = original FD 3)
if (( debug > 2 ))
then
exec 5>&3
exec 3> >(tee -a "$tempdir/debug.log" >&5)
fi
# If new database, populate schema from the SQL schema file
(( populate_db )) && cat $schema >&3
# Configure sqlite3 output separator to match what we parse with ::AtOM:SQL:Sep::
echo '.separator ::AtOM:SQL:Sep::' >&3
# Enforce referential integrity
echo 'PRAGMA foreign_keys = ON;' >&3
# Allow trigger chains
echo 'PRAGMA recursive_triggers = ON;' >&3
# Keep temp tables in memory
echo 'PRAGMA temp_store = 2;' >&3
# We don't handle concurrent writes, lock the database for exclusive
# access to prevent corruption
echo 'PRAGMA locking_mode = EXCLUSIVE;' >&3
# Drain the initial empty result sqlite3 sends on startup
read -u4 -r -d $'\0'
unset REPLY
checkDatabaseVersion

View File

@ -1,18 +1,5 @@
#!/usr/bin/env 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.
upgradedatabase_1_2() {
local data \
datas \
@ -22,11 +9,8 @@ upgradedatabase_1_2() {
fat32
echo "Upgrading database to version 2... (backup is $database.bak_v1)"
cp "$database" "$database.bak_v1"
# Add new columns to hold the fat32compat and ascii settings separately
echo 'ALTER TABLE destination_files ADD COLUMN fat32compat INTEGER;' >&3
echo 'ALTER TABLE destination_files ADD COLUMN ascii INTEGER;' >&3
# Read all existing destination_files rows to migrate rename_pattern format
# Old format embedded fat32compat after a colon: "pattern:fat32value"
echo '
SELECT id,
rename_pattern
@ -40,17 +24,14 @@ SELECT "AtOM::NoMoreData";' >&3
datas+=( "$data" )
read -u4 -r -d $'\0' data
done
# Ensure consistency by performing all updates in a single transaction
echo 'BEGIN TRANSACTION;' >&3
for data in "${datas[@]}"
do
id="${data%%::AtOM:SQL:Sep::*}"
rename_pattern="${data#*::AtOM:SQL:Sep::}"
# Split "pattern:fat32" on the colon separator
IFS=':'
read pattern fat32 <<<"$rename_pattern"
IFS="$oldIFS"
# ASCII-only didn't exist in v1; default to off
Update destination_files \
rename_pattern "$pattern" \
fat32compat "$fat32" \

View File

@ -1,18 +1,5 @@
#!/usr/bin/env 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.
upgradedatabase_2_3() {
local data \
datas \
@ -20,9 +7,7 @@ upgradedatabase_2_3() {
destination
echo "Upgrading database to version 3... (backup is $database.bak_v2)"
cp "$database" "$database.bak_v2"
# Add 'enabled' flag to destinations so individual destinations can be disabled
echo 'ALTER TABLE destinations ADD COLUMN enabled INTEGER DEFAULT 1;' >&3
# Enable all existing destinations (preserve old behaviour where all were active)
Update destinations enabled 1 <<< "1 = 1"
Update atom version 3 <<<"1 = 1"

View File

@ -1,22 +1,8 @@
#!/usr/bin/env 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.
upgradedatabase_3_4() {
echo "Upgrading database to version 4... (backup is $database.bak_v3)"
cp "$database" "$database.bak_v3"
# Add releasecountry tag storage (MusicBrainz Album Release Country)
echo 'ALTER TABLE tags ADD COLUMN releasecountry TEXT;' >&3
Update atom version 4 <<<"1 = 1"

View File

@ -1,23 +1,8 @@
#!/usr/bin/env 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.
upgradedatabase_4_5() {
echo "Upgrading database to version 5... (backup is $database.bak_v4)"
cp "$database" "$database.bak_v4"
# Drop and recreate the trigger so it now also watches releasecountry
# (added in v4 but not yet included in the trigger's watched columns)
echo 'DROP TRIGGER force_destination_update_on_tag_update;' >&3
echo '
CREATE TRIGGER IF NOT EXISTS force_destination_update_on_tag_update
@ -39,8 +24,6 @@ upgradedatabase_4_5() {
depth
ON tags
BEGIN
-- Reset destination timestamp so the file gets
-- re-encoded on next run
UPDATE destination_files SET last_change=0
WHERE source_file_id=old.source_file;
END;

View File

@ -1,25 +1,11 @@
#!/usr/bin/env 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.
upgradedatabase_5_6() {
echo "Upgrading database to version 6... (backup is $database.bak_v5)"
cp "$database" "$database.bak_v5"
echo '
ALTER TABLE tags ADD COLUMN replaygain_alb TEXT;
ALTER TABLE tags ADD COLUMN replaygain_trk TEXT;
-- Recreate trigger to also watch the new ReplayGain columns
DROP TRIGGER force_destination_update_on_tag_update;
CREATE TRIGGER IF NOT EXISTS force_destination_update_on_tag_update
AFTER UPDATE OF

View File

@ -1,25 +1,8 @@
#!/usr/bin/env 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.
upgradedatabase_6_7() {
echo "Upgrading database to version 7... (backup is $database.bak_v6)"
cp "$database" "$database.bak_v6"
# In v6 and earlier, destination_files.filename stored absolute paths.
# From v7 onwards, filenames are stored relative to the destination
# root.
# Strip the destination path prefix from all stored filenames.
for destination in "${destinations[@]}"
do
echo "UPDATE destination_files SET filename = REPLACE(filename,'${destinationpath[$destination]}/','') WHERE filename LIKE '${destinationpath[$destination]}/%';" >&3

View File

@ -1,25 +1,9 @@
#!/usr/bin/env 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.
upgradedatabase_7_8() {
echo "Upgrading database to version 8... (backup is $database.bak_v7)"
cp "$database" "$database.bak_v7"
# This migration only contains a user notice; no schema changes needed.
# The bug fixed in v8 was that old destination files were not being
# deleted on disk correctly; users must run 'cleandestinations -r' to clean up.
echo 'Deletion of old files was failing. Users of previous versions (YOU!) are strongly advised to run cleandestinations with the "-r" flag.'
read -p "Press Enter to continue..."
Update atom version 8 <<<"1 = 1"
cp "$database" "$database.bak_v7"
echo 'Deletion of old files was failing. Users of previous versions (YOU!) are strongly advised to run cleandestinations with the "-r" flag.'
read -p "Press Enter to continue..."
Update atom version 8 <<<"1 = 1"
}

View File

@ -1,34 +1,12 @@
#!/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.
# soxtaskid persists across destinations so encode tasks share a single sox task
declare soxtaskid
decodeFile() {
# copy destinations bypass decoding entirely
if [[ ${destinationformat["$destination"]} == copy ]]
then
copied=1
else
# Dispatch to the appropriate decoder based on the source
# MIME type.
# sox_needed is set when normalization, resampling, or channel
# up/downmixing is required. Used to determine whether to
# insert an intermediate sox processing task.
case "$mimetype" in
'video/'*)
# Extract audio if ffmpeg is available
(( disablevideo )) && continue
extractAudio
if (( ${destinationnormalize["$destination"]}))\
@ -44,8 +22,6 @@ decodeFile() {
fi
;;
'audio/mpeg')
# Copy MP3 as-is if format and quality match
# otherwise decode with sox
if [[ ${destinationformat[$destination]} = mp3 ]] \
&& checkCopy
then
@ -54,9 +30,7 @@ decodeFile() {
decodeSox
fi
;;
'application/ogg opus'|'audio/ogg opus')
# Copy Opus as-is if format and quality match
# otherwise decode with opusdec
'application/ogg opus')
if [[ ${destinationformat[$destination]} = opus ]] \
&& checkCopy
then
@ -77,9 +51,28 @@ decodeFile() {
fi
fi
;;
'application/ogg'*|'audio/ogg'*)
# Ogg Vorbis: copy if format/quality match
# otherwise decode with sox
'audio/ogg opus')
if [[ ${destinationformat[$destination]} = opus ]] \
&& checkCopy
then
copied=1
else
(( disableopusdec )) && continue
decodeOpusdec
if (( ${destinationnormalize["$destination"]}))\
|| (
[ -n "${destinationfrequency["$destination"]}" ]\
&& (( ${rate:-0} != ${destinationfrequency["$destination"]}))\
) || (
[ -n "${destinationchannels["$destination"]}" ]\
&& (( ${channels:-0} != ${destinationchannels["$destination"]} ))
)
then
sox_needed=1
fi
fi
;;
'application/ogg'*)
if [[ ${destinationformat[$destination]} = vorbis ]] \
&& checkCopy
then
@ -88,14 +81,22 @@ decodeFile() {
decodeSox
fi
;;
'audio/x-flac'|'audio/flac')
# FLAC: always decode via sox
# copy for FLAC makes little sense
'audio/ogg'*)
if [[ ${destinationformat[$destination]} = vorbis ]] \
&& checkCopy
then
copied=1
else
decodeSox
fi
;;
'audio/x-flac')
decodeSox
;;
'audio/flac')
decodeSox
;;
*)
# Unknown MIME type: probe with file to detect
# Musepack
extendedtype=$(file -b "$sourcepath/$filename")
case "$extendedtype" in
*'Musepack '*)
@ -114,9 +115,6 @@ decodeFile() {
fi
;;
*)
# Truly unknown format: try
# ffmpeg if available,
# otherwise fall back to sox
if (( disablevideo ))
then
decodeSox
@ -140,10 +138,6 @@ decodeFile() {
esac
if ! (( copied ))
then
# Insert a decode task if one doesn't already exist for
# this source file
# keyed by $tmpfile so multiple destinations share a
# single decode task
if ! decodetaskid=$(
Select tasks id <<<"key = $tmpfile"
)
@ -166,18 +160,11 @@ decodeFile() {
fi
if (( sox_needed ))
then
# Insert a sox post-processing task chained
# after the decode task
decodeSox "$tempdir/$tmpfile.wav"
if ! soxtaskid=$(
Select tasks id <<<"key = $tmpfile"
)
then
# Increment the decode task's
# required_by counter so cleaner()
# waits for all dependent tasks to
# finish before cleaning up the
# intermediate file
parent_required=$(
Select tasks required_by \
<<<"id = $decodetaskid"

View File

@ -1,26 +1,6 @@
#!/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.
decodeMpcdec() {
# Set the tmpfile base name for this specific decode task
# source file id + decoder name
tmpfile="${fileid}mpcdec"
# Build mpcdec command: decode Musepack to WAV in tempdir
# ${ionice} prepends the ionice invocation string if configured
commandline=(${ionice}mpcdec)
# Encode any literal newlines in the filename as the SQL-safe inline
# marker
commandline+=("$sourcepath/${filename//$'\n'/::AtOM:NewLine:SQL:Inline::}" "$tempdir/$tmpfile.wav")
}

View File

@ -1,26 +1,6 @@
#!/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.
decodeOpusdec() {
# Set the tmpfile base name for this specific decode task
# source file id + decoder name
tmpfile="${fileid}opusdec"
# Build opusdec command: decode Opus to WAV in tempdir
# ${ionice} prepends the ionice invocation string if configured
commandline=(${ionice}opusdec)
# Encode any literal newlines in the filename as the SQL-safe inline
# marker
commandline+=("$sourcepath/${filename//$'\n'/::AtOM:NewLine:SQL:Inline::}" "$tempdir/$tmpfile.wav")
}

View File

@ -1,67 +1,36 @@
#!/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.
decodeSox() {
# Build a SoX decode command with optional processing (normalize,
# resample, up/downmix)
commandline=(${ionice}sox --single-threaded --temp "$tempdir")
soxoptions_in=''
soxoptions_out=''
# Add --norm if output normalization is requested
if (( ${destinationnormalize["$destination"]} ))
then
commandline+=(--norm)
soxoptions_in+=' --norm'
fi
# $1 can be set to pass an already decoded file.
# Use the the original file when unset
if [ -n "$1" ]
then
commandline+=("$1")
else
commandline+=("$sourcepath/${filename//$'\n'/::AtOM:NewLine:SQL:Inline::}")
fi
# Add resampling if requested
if [ -n "${destinationfrequency["$destination"]}" ] \
&& (( ${rate:-0} != ${destinationfrequency["$destination"]} ))
then
commandline+=(-r ${destinationfrequency["$destination"]})
soxoptions_out+=" -r ${destinationfrequency["$destination"]}"
fi
# Add channel up/downmixing if requested
if [ -n "${destinationchannels["$destination"]}" ] \
&& (( ${channels:-0} != ${destinationchannels["$destination"]} ))
then
commandline+=(-c ${destinationchannels["$destination"]})
soxoptions_out+=" -c ${destinationchannels["$destination"]}"
fi
# Downsample to 16-bit if source resolution exceeds 16 bits
if (( ${depth:-0} > 16 ))
then
commandline+=(-b 16)
soxoptions_out+=" -b 16"
fi
# Encode all sox options into the tmpfile name so different processing
# chains get unique WAV files
# Avoids conflicts with different samplerate/norm/channel counts
tmpfile="$fileid${soxoptions_in// /}${soxoptions_out// /}"
commandline+=("$tempdir/$tmpfile.wav")
}

View File

@ -1,22 +1,7 @@
#!/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.
createDestinations() {
for destination in ${destinations[@]}
do
# Create destination directory if it doesn't exist yet
if ! [ -d "${destinationpath["$destination"]}" ]
then
if ! mkdir -p "${destinationpath["$destination"]}"
@ -25,8 +10,6 @@ createDestinations() {
exit $EINVDEST
fi
fi
# Ensure the destination has a DB record; store its numeric ID
# for later use
destinationid["$destination"]=$(
InsertIfUnset destinations <<<"name $destination ${destinationenabled[\"$destination\"]}"
)

View File

@ -1,32 +1,12 @@
#!/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.
updateMimes() {
# Reset all mime_actions to action=1 (transcode) as the default
Update mime_actions action 1 <<<"action != 1"
# For each destination's skip patterns, set action=0 (exclude from
# processing)
# Multiple patterns are pipe-separated; split by setting IFS='|'
for destination in ${!destinationskipmime[@]}
do
IFS='|'
for mime_type in ${destinationskipmime["$destination"]}
do
IFS="$oldIFS"
# Convert config wildcard '*' to SQL wildcard '%'
Update mime_type_actions action 0 >/dev/null < <(
cat <<-EOWhere
destination_id = ${destinationid["$destination"]}
@ -35,17 +15,12 @@ updateMimes() {
)
done
done
# For each destination's copy-mime patterns, set action=2 (copy
# verbatim)
# Multiple patterns are pipe-separated; split by setting IFS='|'
for destination in ${!destinationcopymime[@]}
do
IFS='|'
for mime_type in ${destinationcopymime["$destination"]}
do
IFS="$oldIFS"
# Convert config wildcard '*' to SQL wildcard '%'
Update mime_type_actions action 2 >/dev/null < <(
cat <<-EOWhere
destination_id = ${destinationid["$destination"]}

View File

@ -1,30 +1,13 @@
#!/usr/bin/env 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.
encodeFile::mp3() {
# Build lame ABR encode command with all available metadata
lameopts=(${ionice}lame --quiet --noreplaygain)
lameopts+=(-v --abr ${destinationquality[$destination]})
# Embed ID3 tags for each available metadata field
[ -n "$album" ] && lameopts+=(--tl "$album" )
[ -n "$artist" ] && lameopts+=(--ta "$artist")
[ -n "$genre" ] && lameopts+=(--tg "$genre")
[ -n "$title" ] && lameopts+=(--tt "$title")
[ -n "$track" ] && lameopts+=(--tn "$track")
[ -n "$year" ] && lameopts+=(--ty "$year")
# Extended tags using ID3v2 frames (TXXX for custom/non-standard fields)
[ -n "$albumartist" ] && lameopts+=(--tv TPE2="$albumartist")
[ -n "$composer" ] && lameopts+=(--tv TCOM="$composer")
[ -n "$performer" ] && lameopts+=(--tv TOPE="$performer")
@ -35,9 +18,6 @@ encodeFile::mp3() {
[ -n "$replaygain_trk" ] \
&& lameopts+=(--tv "TXXX=REPLAYGAIN_TRACK_GAIN=$replaygain_trk")
[ -n "$disc" ] && lameopts+=(--tv TPOS="$disc")
# Handle noresample: force lame to use a specific sample rate to prevent
# lame's own automatic downsampling at low bitrates
if (( ${destinationnoresample[$destination]:-0} == 1 ))
then
# If 'rate' is not one of these value, it cannot be encoded to
@ -45,7 +25,6 @@ encodeFile::mp3() {
# rate to use.
if [ -n "${destinationfrequency["$destination"]}" ]
then
# Target frequency was explicitly set; use that
case ${destinationfrequency["$destination"]} in
48000) lameopts+=(--resample 48) ;;
44100) lameopts+=(--resample 44.1) ;;
@ -59,10 +38,8 @@ encodeFile::mp3() {
esac
elif (( rate > 48000 ))
then
# Source rate exceeds MP3 maximum; cap at 48kHz
lameopts+=(--resample 48)
else
# Use the source file's own sample rate
case $rate in
48000) lameopts+=(--resample 48) ;;
44100) lameopts+=(--resample 44.1) ;;
@ -76,11 +53,7 @@ encodeFile::mp3() {
esac
fi
fi
# Append input WAV and output MP3 paths to complete the command
lameopts+=("$tempdir/$tmpfile.wav" "${destinationpath[$destination]}/$destdir/$destfile.mp3")
# Insert the encode task into the DB, linked to the decode (or sox) task
# Depend on sox task if it exists, else decode task
encodetaskid=$(
Insert tasks <<-EOInsert
key ${fileid}lame$destination
@ -90,9 +63,6 @@ encodeFile::mp3() {
$(
for key in ${!lameopts[@]}
do
# Escape special characters that could
# interfere with bash glob/brace
# expansion
cleanedopts="${lameopts[key]//\&/\\\&}"
cleanedopts="${cleanedopts//\[/\\[}"
cleanedopts="${cleanedopts//\]/\\]}"
@ -108,8 +78,6 @@ encodeFile::mp3() {
ascii ${destinationascii["$destination"]}
EOInsert
)
# Increment parent task's required_by counter so it won't clean up
# until all children finish
parent_required=$(
Select tasks required_by \
<<<"id = ${soxtaskid:-$decodetaskid}"
@ -117,5 +85,5 @@ encodeFile::mp3() {
Update tasks required_by $((++parent_required)) \
<<<"id = ${soxtaskid:-$decodetaskid}"
progressSpin
soxtaskid='' # Clear sox task ID so next destination starts fresh
soxtaskid=''
}

View File

@ -1,27 +1,9 @@
#!/usr/bin/env 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.
encodeFile::opus() {
# Build opusenc VBR encode command
opusencopts=(${ionice}opusenc --quiet)
opusencopts+=(--bitrate ${destinationquality[$destination]})
# Add Forward Error Correction if a packet loss percentage was
# configured
[ -n "${destinationloss["$destination"]}" ] \
&& opusencopts+=(--expect-loss "${destinationloss["$destination"]}")
# Embed Ogg comment tags
[ -n "$albumartist" ] && opusencopts+=(--comment "ALBUMARTIST=$albumartist")
[ -n "$album" ] && opusencopts+=(--comment "ALBUM=$album")
[ -n "$artist" ] && opusencopts+=(--artist "$artist")
@ -38,8 +20,6 @@ encodeFile::opus() {
&& opusencopts+=(--comment) \
&& opusencopts+=("REPLAYGAIN_TRACK_GAIN=$replaygain_trk")
[ -n "$title" ] && opusencopts+=(--title "$title")
# Split "track/total" format: opusenc uses separate TRACKNUMBER and
# TRACKTOTAL fields
[ -n "$track" ] && opusencopts+=(--comment "TRACKNUMBER=${track%/*}")
[ -n "${track#*/}" ] && opusencopts+=(--comment "TRACKTOTAL=${track#*/}")
[ -n "$year" ] && opusencopts+=(--comment "DATE=$year")
@ -51,8 +31,6 @@ encodeFile::opus() {
fileid $destfileid
filename $destdir/$destfile.opus
$(
# Escape special characters that could
# interfere with bash glob/brace expansion
for key in ${!opusencopts[@]}
do
cleanedopts="${opusencopts[key]//\&/\\\&}"
@ -70,8 +48,6 @@ encodeFile::opus() {
ascii ${destinationascii["$destination"]}
EOInsert
)
# Increment parent task's required_by counter so it won't clean up
# until all children finish
parent_required=$(
Select tasks required_by \
<<<"id = ${soxtaskid:-$decodetaskid}"

View File

@ -1,23 +1,6 @@
#!/usr/bin/env 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.
encodeFile::vorbis() {
# Build oggenc quality-based encode command
# (-Q = quiet, -q = quality level)
oggencopts=(${ionice}oggenc -Q -q ${destinationquality[$destination]})
# Embed Ogg comment tags using oggenc's -c "FIELD=value" syntax
[ -n "$albumartist" ] && oggencopts+=(-c "ALBUMARTIST=$albumartist")
[ -n "$album" ] && oggencopts+=(-l "$album")
[ -n "$artist" ] && oggencopts+=(-a "$artist")
@ -34,7 +17,6 @@ encodeFile::vorbis() {
[ -n "$title" ] && oggencopts+=(-t "$title")
[ -n "$track" ] && oggencopts+=(-N "$track")
[ -n "$year" ] && oggencopts+=(-d "$year")
# -o output must come before input for oggenc
oggencopts+=(-o "${destinationpath[$destination]}/$destdir/$destfile.ogg" "$tempdir/$tmpfile.wav")
encodetaskid=$(
Insert tasks <<-EOInsert
@ -43,8 +25,6 @@ encodeFile::vorbis() {
fileid $destfileid
filename $destdir/$destfile.ogg
$(
# Escape special characters that could
# interfere with bash glob/brace expansion
for key in ${!oggencopts[@]}
do
cleanedopts="${oggencopts[key]//\&/\\\&}"
@ -62,8 +42,6 @@ encodeFile::vorbis() {
ascii ${destinationascii["$destination"]}
EOInsert
)
# Increment parent task's required_by counter so it won't clean up
# until all children finish
parent_required=$(
Select tasks required_by \
<<<"id = ${soxtaskid:-$decodetaskid}"

View File

@ -1,22 +1,5 @@
#!/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.
removeObsoleteFiles() {
# Delete source_files records that were not seen in the latest scan.
# The DB's ON DELETE CASCADE will remove tags
# ON DELETE SET NULL will let us take care of destination_files later.
Delete source_files <<-EOWhere
last_seen < $scantime
EOWhere

View File

@ -1,18 +1,4 @@
#!/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.
sanitizeFile() {
shopt -s extglob
string="$1"
@ -20,7 +6,7 @@ sanitizeFile() {
string="${string//\// }"
if (( ${destinationfat32compat[$destination]} ))
then
# FAT32 forbids these characters in filenames
# Filenames can't contain:
string=${string//\?/ }
string=${string//\\/ }
string=${string//</ }

View File

@ -1,18 +1,5 @@
#!/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.
setupDestination() {
cat <<-EODesc

View File

@ -1,18 +1,5 @@
#!/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.
setupDestinations() {
cat <<-EODesc

View File

@ -1,18 +1,5 @@
#!/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.
setupGeneral() {
cat <<-EODesc
@ -27,14 +14,11 @@ setupGeneral() {
1 minute load average between <load> and <load>+1 by adjusting
concurrency. Initial concurrency will be set to half of that value.
EODesc
# Check input is an integer
# Reused across multiple prompts in this function
expr='^[0-9]*$'
comeagain() {
read \
-e \
-p'Target load: (integer) ' \
# Pre-fill with the existing value when re-running setup
${maxload+-i"$maxload"} \
value
if [ -n "$value" ] && [[ $value =~ $expr ]]
@ -42,7 +26,6 @@ setupGeneral() {
maxload="$value"
else
echo "Invalid max-load value: $value" >&2
# Recurse until we get a valid value
comeagain
fi
}
@ -81,7 +64,6 @@ setupGeneral() {
read \
-e \
-p'Ionice: <1-3> [0-7] ' \
# Default to class 3 (idle) when no prior value exists
-i"${class:-3} ${niceness}" \
class niceness
case $class in
@ -105,7 +87,6 @@ setupGeneral() {
fi
;;
2)
# Best-effort class; niceness 0-7 is mandatory
if [ -n "$niceness" ] \
&& (( niceness >= 0 && niceness <= 7 ))
then
@ -117,7 +98,6 @@ setupGeneral() {
fi
;;
3)
# Idle class; no niceness level is accepted
ionice="ionice -c3 "
;;
*)
@ -135,7 +115,6 @@ setupGeneral() {
sqlite) and temporary WAVE files will be created. Note that debug logs
(if enabled) will go there too.
EODesc
# Tab-completion (-e) works here because readline is active
read \
-e \
-i"${tempdir:-$HOME/.atom/tmp}" \

View File

@ -1,18 +1,5 @@
#!/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.
setupRegen() {
(( regen )) && return 0
echo "Parameter $1 for destination $destination changed."

View File

@ -1,18 +1,5 @@
#!/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.
setup() {
cat <<-EOStartConf
You will now be asked (hopefully) simple questions to help you configure AtOM's
@ -20,17 +7,11 @@ behavior.
Completion is available for prompts asking for a paths or filenames.
EOStartConf
# Collect all configuration sections in order
setupGeneral
setupSource
setupDestinations
# Clear the regex used by validation loops inside setup sub-functions
unset expr
# Write the newly gathered config to a temp file so the original is
# preserved until the user confirms
writeConfig >"$cffile".tmp
# Unset all config variables so getConfig can repopulate them cleanly
# from the temp file; avoids stale values leaking into the review
unset \
sourcepath \
skippeddirectories \
@ -56,8 +37,6 @@ Completion is available for prompts asking for a paths or filenames.
destinationnoresample \
destinationrenamepath \
destinationskipmime
# Re-declare per-destination variables as associative arrays so
# getConfig can populate them with [destinationname]=value entries
declare -A \
destinationchannels \
destinationfat32compat \
@ -75,8 +54,6 @@ Completion is available for prompts asking for a paths or filenames.
destinationnoresample \
destinationrenamepath \
destinationskipmime
# Point getConfig at the temp file so the review reflects exactly what
# would be written, not the old on-disk config
oldcffile="$cffile"
cffile="$cffile".tmp
getConfig
@ -84,7 +61,6 @@ Completion is available for prompts asking for a paths or filenames.
echo $'Please review your new configuration:\n'
printConfig
}| less -F -e
# Restore the original config path before deciding whether to commit
cffile="$oldcffile"
read -p'Write config file? [y/N] ' do_write
case $do_write in

View File

@ -1,18 +1,5 @@
#!/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.
setupSource() {
cat <<-EODesc
@ -31,7 +18,6 @@ setupSource() {
-i"${sourcepath:-/var/lib/mpd/music}" \
-p'Music collection (<TAB> for completion): ' \
sourcepath
# Reject paths that don't exist; AtOM doesn't create the source
if ! [ -d "$sourcepath" ]
then
echo "$sourcepath does not exist or is not a" \
@ -48,17 +34,12 @@ setupSource() {
This prompt will loop until an empty string is encountered.
EODesc
# Change into the source directory so readline tab-completion resolves
# relative paths correctly while the user types skip entries
cd "$sourcepath"
# Remember how many skip entries already exist so we know when to stop
# re-presenting existing entries vs. treating blanks as "done"
count=${#skippeddirectories[@]}
for (( i=0 ; 1 ; i++ ))
do
read \
-e \
# Pre-fill with the existing entry at this index, if any
${skippeddirectories[i]+-i"${skippeddirectories[i]}"}\
-p'Skip: ' \
value
@ -67,14 +48,11 @@ setupSource() {
skippeddirectories[i]="$value"
elif (( i < count ))
then
# Blank input on a previously set entry removes it
unset skippeddirectories[i]
else
# Blank input beyond the old list signals end of input
break
fi
done
unset count
# Return to the prior directory; output suppressed to keep the UI clean
cd - >/dev/null
}

View File

@ -6,6 +6,7 @@ CREATE TABLE IF NOT EXISTS source_files (
id INTEGER PRIMARY KEY,
filename TEXT UNIQUE NOT NULL,
size INTEGER NOT NULL,
hash TEXT,
mime_type INTEGER,
last_change FLOAT NOT NULL DEFAULT (strftime('%s','now')),
last_seen INTEGER NOT NULL DEFAULT (strftime('%s','now')),