Compare commits

..

No commits in common. "0f13154d6feba831cb67619ba1bb7daf595587b3" and "ee119f07a48111babf185f2b064ea47bf6c71fb1" have entirely different histories.

33 changed files with 47 additions and 720 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,53 +1,26 @@
#!/usr/bin/env bash #!/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 currentdbversion=8
checkDatabaseVersion() { checkDatabaseVersion() {
local dbversion local dbversion
# Try to read the stored version from the 'atom' metadata table
if dbversion=$(Select atom version <<<"\"1\" = 1") if dbversion=$(Select atom version <<<"\"1\" = 1")
then then
if (( dbversion == currentdbversion )) if (( dbversion == currentdbversion ))
then then
return 0 # Already up to date return 0
elif (( dbversion < currentdbversion )) elif (( dbversion < currentdbversion ))
then then
# Run sequential upgrade functions until we reach
# `$currentdbversion`
until (( dbversion == currentdbversion )) until (( dbversion == currentdbversion ))
do do
# Dynamically calls e.g. upgradedatabase_3_4
upgradedatabase_${dbversion}_$((dbversion+1)) 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") dbversion=$(Select atom version <<<"\"1\" = 1")
done done
else else
# DB was created by a newer AtOM; we can't run and
# ensure consistency
echo "Database schema version $dbversion is" \ echo "Database schema version $dbversion is" \
"higher thanthat of this version of" \ "higher thanthat of this version of" \
"AtOM ($currentdbversion). Bailing out." >&2 "AtOM ($currentdbversion). Bailing out." >&2
exit $EDBVERSION exit $EDBVERSION
fi fi
else 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" Insert atom 1 <<<"version $currentdbversion"
fi fi
} }

View File

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

View File

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

View File

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

View File

@ -1,18 +1,5 @@
#!/usr/bin/env bash #!/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() { upgradedatabase_2_3() {
local data \ local data \
datas \ datas \
@ -20,9 +7,7 @@ upgradedatabase_2_3() {
destination destination
echo "Upgrading database to version 3... (backup is $database.bak_v2)" echo "Upgrading database to version 3... (backup is $database.bak_v2)"
cp "$database" "$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 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 destinations enabled 1 <<< "1 = 1"
Update atom version 3 <<<"1 = 1" Update atom version 3 <<<"1 = 1"

View File

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

View File

@ -1,23 +1,8 @@
#!/usr/bin/env bash #!/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() { upgradedatabase_4_5() {
echo "Upgrading database to version 5... (backup is $database.bak_v4)" echo "Upgrading database to version 5... (backup is $database.bak_v4)"
cp "$database" "$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 'DROP TRIGGER force_destination_update_on_tag_update;' >&3
echo ' echo '
CREATE TRIGGER IF NOT EXISTS force_destination_update_on_tag_update CREATE TRIGGER IF NOT EXISTS force_destination_update_on_tag_update
@ -39,8 +24,6 @@ upgradedatabase_4_5() {
depth depth
ON tags ON tags
BEGIN BEGIN
-- Reset destination timestamp so the file gets
-- re-encoded on next run
UPDATE destination_files SET last_change=0 UPDATE destination_files SET last_change=0
WHERE source_file_id=old.source_file; WHERE source_file_id=old.source_file;
END; END;

View File

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

View File

@ -1,25 +1,8 @@
#!/usr/bin/env bash #!/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() { upgradedatabase_6_7() {
echo "Upgrading database to version 7... (backup is $database.bak_v6)" echo "Upgrading database to version 7... (backup is $database.bak_v6)"
cp "$database" "$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[@]}" for destination in "${destinations[@]}"
do do
echo "UPDATE destination_files SET filename = REPLACE(filename,'${destinationpath[$destination]}/','') WHERE filename LIKE '${destinationpath[$destination]}/%';" >&3 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 #!/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() { upgradedatabase_7_8() {
echo "Upgrading database to version 8... (backup is $database.bak_v7)" echo "Upgrading database to version 8... (backup is $database.bak_v7)"
cp "$database" "$database.bak_v7" cp "$database" "$database.bak_v7"
# This migration only contains a user notice; no schema changes needed. echo 'Deletion of old files was failing. Users of previous versions (YOU!) are strongly advised to run cleandestinations with the "-r" flag.'
# The bug fixed in v8 was that old destination files were not being read -p "Press Enter to continue..."
# deleted on disk correctly; users must run 'cleandestinations -r' to clean up. Update atom version 8 <<<"1 = 1"
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 #!/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 declare soxtaskid
decodeFile() { decodeFile() {
# copy destinations bypass decoding entirely
if [[ ${destinationformat["$destination"]} == copy ]] if [[ ${destinationformat["$destination"]} == copy ]]
then then
copied=1 copied=1
else 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 case "$mimetype" in
'video/'*) 'video/'*)
# Extract audio if ffmpeg is available
(( disablevideo )) && continue (( disablevideo )) && continue
extractAudio extractAudio
if (( ${destinationnormalize["$destination"]}))\ if (( ${destinationnormalize["$destination"]}))\
@ -44,8 +22,6 @@ decodeFile() {
fi fi
;; ;;
'audio/mpeg') 'audio/mpeg')
# Copy MP3 as-is if format and quality match
# otherwise decode with sox
if [[ ${destinationformat[$destination]} = mp3 ]] \ if [[ ${destinationformat[$destination]} = mp3 ]] \
&& checkCopy && checkCopy
then then
@ -54,9 +30,7 @@ decodeFile() {
decodeSox decodeSox
fi fi
;; ;;
'application/ogg opus'|'audio/ogg opus') 'application/ogg opus')
# Copy Opus as-is if format and quality match
# otherwise decode with opusdec
if [[ ${destinationformat[$destination]} = opus ]] \ if [[ ${destinationformat[$destination]} = opus ]] \
&& checkCopy && checkCopy
then then
@ -77,9 +51,28 @@ decodeFile() {
fi fi
fi fi
;; ;;
'application/ogg'*|'audio/ogg'*) 'audio/ogg opus')
# Ogg Vorbis: copy if format/quality match if [[ ${destinationformat[$destination]} = opus ]] \
# otherwise decode with sox && 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 ]] \ if [[ ${destinationformat[$destination]} = vorbis ]] \
&& checkCopy && checkCopy
then then
@ -88,14 +81,22 @@ decodeFile() {
decodeSox decodeSox
fi fi
;; ;;
'audio/x-flac'|'audio/flac') 'audio/ogg'*)
# FLAC: always decode via sox if [[ ${destinationformat[$destination]} = vorbis ]] \
# copy for FLAC makes little sense && checkCopy
then
copied=1
else
decodeSox
fi
;;
'audio/x-flac')
decodeSox
;;
'audio/flac')
decodeSox decodeSox
;; ;;
*) *)
# Unknown MIME type: probe with file to detect
# Musepack
extendedtype=$(file -b "$sourcepath/$filename") extendedtype=$(file -b "$sourcepath/$filename")
case "$extendedtype" in case "$extendedtype" in
*'Musepack '*) *'Musepack '*)
@ -114,9 +115,6 @@ decodeFile() {
fi fi
;; ;;
*) *)
# Truly unknown format: try
# ffmpeg if available,
# otherwise fall back to sox
if (( disablevideo )) if (( disablevideo ))
then then
decodeSox decodeSox
@ -140,10 +138,6 @@ decodeFile() {
esac esac
if ! (( copied )) if ! (( copied ))
then 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=$( if ! decodetaskid=$(
Select tasks id <<<"key = $tmpfile" Select tasks id <<<"key = $tmpfile"
) )
@ -166,18 +160,11 @@ decodeFile() {
fi fi
if (( sox_needed )) if (( sox_needed ))
then then
# Insert a sox post-processing task chained
# after the decode task
decodeSox "$tempdir/$tmpfile.wav" decodeSox "$tempdir/$tmpfile.wav"
if ! soxtaskid=$( if ! soxtaskid=$(
Select tasks id <<<"key = $tmpfile" Select tasks id <<<"key = $tmpfile"
) )
then 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=$( parent_required=$(
Select tasks required_by \ Select tasks required_by \
<<<"id = $decodetaskid" <<<"id = $decodetaskid"

View File

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

View File

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

View File

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

View File

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

View File

@ -1,32 +1,12 @@
#!/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.
updateMimes() { updateMimes() {
# Reset all mime_actions to action=1 (transcode) as the default
Update mime_actions action 1 <<<"action != 1" 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[@]} for destination in ${!destinationskipmime[@]}
do do
IFS='|' IFS='|'
for mime_type in ${destinationskipmime["$destination"]} for mime_type in ${destinationskipmime["$destination"]}
do do
IFS="$oldIFS" IFS="$oldIFS"
# Convert config wildcard '*' to SQL wildcard '%'
Update mime_type_actions action 0 >/dev/null < <( Update mime_type_actions action 0 >/dev/null < <(
cat <<-EOWhere cat <<-EOWhere
destination_id = ${destinationid["$destination"]} destination_id = ${destinationid["$destination"]}
@ -35,17 +15,12 @@ updateMimes() {
) )
done done
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[@]} for destination in ${!destinationcopymime[@]}
do do
IFS='|' IFS='|'
for mime_type in ${destinationcopymime["$destination"]} for mime_type in ${destinationcopymime["$destination"]}
do do
IFS="$oldIFS" IFS="$oldIFS"
# Convert config wildcard '*' to SQL wildcard '%'
Update mime_type_actions action 2 >/dev/null < <( Update mime_type_actions action 2 >/dev/null < <(
cat <<-EOWhere cat <<-EOWhere
destination_id = ${destinationid["$destination"]} destination_id = ${destinationid["$destination"]}

View File

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

View File

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

View File

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

View File

@ -1,22 +1,5 @@
#!/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.
removeObsoleteFiles() { 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 Delete source_files <<-EOWhere
last_seen < $scantime last_seen < $scantime
EOWhere EOWhere

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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