AtOM/toys/createindex
2025-11-09 06:56:37 +01:00

837 lines
17 KiB
Bash
Executable File

#!/usr/bin/env bash
declare -r \
DOCDIR=%DOCDIR% \
LIBDIR=%LIBDIR% \
SHAREDIR=%SHAREDIR%
declare -r \
exampleconf=$DOCDIR/example.cfg \
schema=$SHAREDIR/schema.sql \
\
oldIFS="$IFS"
## Define exit codes
source "$SHAREDIR"/errorcodes
# config structures
declare -A \
destinationchannels \
destinationfat32compat \
destinationcopymime \
destinationformat \
destinationfrequency \
destinationid \
destinationloss \
destinationmaxbps \
destinationnormalize \
destinationpath \
destinationquality \
destinationrename \
destinationnoresample \
destinationrenamepath \
destinationskipmime \
|| {
echo "Check your Bash version. You need >= 4.0" >&2
exit $EBASHVERS
}
LC_ALL=C
shopt -s extglob
source "$SHAREDIR"/id3genres
for function in "$LIBDIR"/*/*
do
source "$function"
done
if ! [[ -f "${XDG_CONFIG_HOME:-$HOME/.config}/AtOM/atom.cfg" ]] \
&& [[ -f "$HOME/.atom/atom.cfg" ]]
then
echo "Configuration found in legacy location $HOME/.atom/atom.cfg."\
"Migrating configuration and data to XDG standard"
xdgMigrate
fi
cffile="${XDG_CONFIG_HOME:-$HOME/.config}/AtOM/atom.cfg"
args="$@"
while [ -n "$1" ]
do
opt="$1"
shift
case $opt in
'-A') show+=(albumartists) ;;
'-l') show+=(albums) ;;
'-a') show+=(artists) ;;
'-b') show+=(bitrates) ;;
'-c') show+=(composers) ;;
'-C') show+=(channelss) ;;
'-d') show+=(discs) ;;
'-f') show+=(path) ;;
'-g') show+=(genres) ;;
'-m') show+=(oldtimestamp) ;;
'-M') show+=(types) ;;
'-N') show+=(tracktotals) ;;
'-p') show+=(performers) ;;
'-r') show+=(releasecountries) ;;
'-s') show+=(rates) ;;
'-S') show+=(size)
length[count]=5 ;;
'-B') show+=(depths) ;;
'-t') show+=(titles) ;;
'-y') show+=(years) ;;
'-#') show+=(count) ;;
'--stats')
stats=1
continue ;;
'-T') timeformat="$1"
shift
continue ;;
'-o') output="$1"
shift
continue ;;
'-u') update=1
continue ;;
'-D') (( debug++ ))
continue ;;
[0-9]*) length[count-1]=$opt
continue ;;
esac
(( count++ ))
done
[ -z "$output" ] && {
cat <<-EOHelp
No output specified!
-f Path
-# File count
-S Size
-b Average bitrate
-C Channels
-s Sample rate
-B Bits per sample
-m Mofification time
-M Format
-A Album artist
-l Album
-a Artist
-c Composer
-d Disc
-g Genre
-p Performer
-r Release Country
-N Track total
-t Title
-y Year
--stats
-T <format>: date-time format (see 'man date' for possible values)
-o <file> : output file (path relative to Source)
-u : update database first
-D : debug
EOHelp
exit 1
}
getConfig
sanityCheck
openDatabase
columns="${show[@]//*/-}"
printPath() {
for key in ${!pathparts[@]}
do
if [[ ${pathparts[key]} == ${oldpathparts[key]} ]]
then
echo -n " ${pathparts[key]//?/ }"
else
echo -n "${pathparts[key]}/"
fi
done
}
printline() {
local print
for index in ${!show[@]}
do
info="${show[index]}"
locallength="${length[index]:=50}"
path="$olddir"
case $info in
'bitrates')
info=$(
printf %${locallength}d \
$((bitrates/count))
)
(( info )) || unset info
;;
'oldtimestamp')
info=$(printDate ${!info})
;;
'count')
info=$(printf %${locallength}d $count)
;;
'path')
while [[ $path =~ / ]]
do
pathparts+=("${path%%/*}")
path=${path#*/}
done
pathparts+=("$path")
info=$(printPath)
unset oldpathparts
for key in ${!pathparts[@]}
do
oldpathparts[key]=${pathparts[key]}
done
unset pathparts
;;
'size')
if (( size > 1073741823 ))
then
info=$(( (size * 1000) / 1073741824 ))
int=$(( info / 1000 ))
if (( ${#int} > 2 ))
then
info=$(printf %4sG $int)
else
info=$(
printf %2s.%.1sG\
$int \
${info#int}
)
fi
elif (( size > 1048575 ))
then
info=$(( (size * 1000) / 1048576 ))
int=$(( info / 1000 ))
if (( ${#int} > 2 ))
then
info=$(printf %4sM $int)
else
info=$(
printf %2s.%.1sM\
$int \
${info#int}
)
fi
suffix=M
elif (( size > 1023 ))
then
info=$(( (size * 1000) / 1024 ))
int=$(( info / 1000 ))
if (( ${#int} > 2 ))
then
info=$(printf %4sk $int)
else
info=$(
printf %2s.%.1sk\
$int \
${info#int}
)
fi
else
info=$size
info=$(printf %4s $size)
fi
;;
*)
info="${!info}"
;;
esac
printtmp="${info:0:$locallength}"
if [ -z "$printtmp" ]
then
until (( ${#printtmp} == locallength/2))
do
printtmp+=' '
done
printtmp+='-'
fi
until (( ${#printtmp} == locallength ))
do
printtmp+=' '
done
print+=(${print+|} "$printtmp")
done
echo "${print[@]}"
}
if (( update ))
then
getFiles
updateMimes
updateTags
fi
echo 'SELECT IFNULL(
(SELECT last_seen FROM source_files ORDER BY last_seen DESC LIMIT 1),
0);' >&3
read -u4 lastupdate
if ! [[ "$output" == - ]]
then
exec > "$output"
fi
cat <<-EOBrag
# Generated by AtOM's createindex toy.
# https://framagit.org/atom/AtOM/
# (C) 2012-2025 Vincent Riquer (GPL-3)
#
# $0 $args
#
# Last database update: $(printf "%(%x %X)T" "$lastupdate")
EOBrag
printDate() {
printf "%("${timeformat:-%x %X}")T" "$1"
}
for index in ${!show[@]}
do
info="${show[index]}"
locallength="${length[index]:=50}"
case $info in
albumartists) info='Album artist' ;;
albums) info='Album' ;;
artists) info='Artist' ;;
bitrates) info='Bitrate' ;;
depths) info='Bit depth' ;;
channelss) info='Channels' ;;
composers) info='Composer' ;;
count) info='#' ;;
discs) info='Disc' ;;
path) info='Directory name' ;;
genres) info='Genre' ;;
oldtimestamp) info='Last modified' ;;
types) info='Format' ;;
performers) info='Performer' ;;
releasecountries) info='Country' ;;
rates) info='Sample rate' ;;
titles) info='Title' ;;
tracktotals) info='Track total' ;;
years) info='Date' ;;
size) info='Size' ;;
esac
printtmp="${info:0:$locallength}"
until (( ${#printtmp} == locallength ))
do
printtmp+=' '
done
print+=(${print+|} "$printtmp")
done
echo "${print[@]}"
echo "${print[@]//[^|]/=}"
unset print
echo '
SELECT
source_files.filename,
tags.bitrate,
tags.channels,
tags.rate,
tags.depth,
source_files.last_change,
mime_types.mime_text,
tags.albumartist,
tags.album,
tags.artist,
tags.composer,
tags.disc,
tags.genre,
tags.performer,
tags.releasecountry,
tags.title,
tags.track,
tags.year,
source_files.size
FROM mime_types
INNER JOIN source_files
ON source_files.mime_type=mime_types.id
INNER JOIN tags
ON source_files.id=tags.source_file
WHERE
NOT mime_types.mime_text LIKE "text/%"
AND NOT mime_types.mime_text LIKE "image/%"
AND NOT mime_types.mime_text LIKE "application/pdf"
AND last_seen = '$lastupdate'
ORDER BY source_files.filename
COLLATE NOCASE;
SELECT "AtOM:NoMoreFiles";' >&3
read -u4 line
until [[ $line == AtOM:NoMoreFiles ]]
do
files+=("$line")
read -u4 line
done
for line in "${files[@]}"
do
filename="${line%%::AtOM:SQL:Sep::*}"
dir="${filename%/*}"
rest="${line#*::AtOM:SQL:Sep::}::AtOM:SQL:Sep::"
bitrate="${rest%%::AtOM:SQL:Sep::*}"
rest="${rest#*::AtOM:SQL:Sep::}"
channels="${rest%%::AtOM:SQL:Sep::*}"
rest="${rest#*::AtOM:SQL:Sep::}"
rate="${rest%%::AtOM:SQL:Sep::*}"
rest="${rest#*::AtOM:SQL:Sep::}"
depth="${rest%%::AtOM:SQL:Sep::*}"
rest="${rest#*::AtOM:SQL:Sep::}"
timestamp="${rest%%::AtOM:SQL:Sep::*}"
timestamp="${timestamp%%.*}"
rest="${rest#*::AtOM:SQL:Sep::}"
mimetype="${rest%%::AtOM:SQL:Sep::*}"
rest="${rest#*::AtOM:SQL:Sep::}"
albumartist="${rest%%::AtOM:SQL:Sep::*}"
rest="${rest#*::AtOM:SQL:Sep::}"
album="${rest%%::AtOM:SQL:Sep::*}"
rest="${rest#*::AtOM:SQL:Sep::}"
artist="${rest%%::AtOM:SQL:Sep::*}"
rest="${rest#*::AtOM:SQL:Sep::}"
composer="${rest%%::AtOM:SQL:Sep::*}"
rest="${rest#*::AtOM:SQL:Sep::}"
disc="${rest%%::AtOM:SQL:Sep::*}"
rest="${rest#*::AtOM:SQL:Sep::}"
genre="${rest%%::AtOM:SQL:Sep::*}"
rest="${rest#*::AtOM:SQL:Sep::}"
performer="${rest%%::AtOM:SQL:Sep::*}"
rest="${rest#*::AtOM:SQL:Sep::}"
releasecountry="${rest%%::AtOM:SQL:Sep::*}"
rest="${rest#*::AtOM:SQL:Sep::}"
title="${rest%%::AtOM:SQL:Sep::*}"
rest="${rest#*::AtOM:SQL:Sep::}"
track="${rest%%::AtOM:SQL:Sep::*}"
[[ $track =~ / ]] && tracktotal=${track#*/}
rest="${rest#*::AtOM:SQL:Sep::}"
year="${rest%%::AtOM:SQL:Sep::*}"
rest="${rest#*::AtOM:SQL:Sep::}"
filesize="${rest%%::AtOM:SQL:Sep::*}"
rest="${rest#*::AtOM:SQL:Sep::}"
case $mimetype in
application/ogg\ opus) type=Opus ;;
audio/ogg\ opus) type=Opus ;;
application/ogg\ vorbis) type=Vorbis ;;
audio/ogg\ vorbis) type=Vorbis ;;
audio/mp4) type=MPEG4\ Audio;;
audio/mpeg) type=MPEG\ Audio;;
audio/x-flac) type=FLAC ;;
audio/flac) type=FLAC ;;
video/mpeg) type=MPEG\ Video;;
video/webm) type=WebM ;;
video/x-flv) type=Flash\ Video;;
video/x-ms-asf) type=Windows\ Media;;
video/x-msvideo) type=AVI\ Video ;;
audio/*) type=Other\ Audio;;
video/*) type=Other\ Video;;
*) type=Other ;;
esac
if [[ $dir == $olddir ]]
then
(( size += filesize ))
(( count++ ))
(( $bitrate )) && (( bitrates+=bitrate ))
((
oldtimestamp = (
timestamp > oldtimestamp
? timestamp
: oldtimestamp
)
))
expr1='(^|,)'
expr2='(,|$)'
if ! [[ $channelss =~ $expr1"$channels"$expr2 ]]
then
if [[ -n "$channels" ]]
then
:
elif [ -n "$channelss" ] \
&& (( channels < ${channelss%%,*} ))
then
channelss="$channels,$channelss"
else
channelss+="${channelss+,}$channels"
fi
fi
if ! [[ $rates =~ $expr1"$rate"$expr2 ]]
then
if [[ -n "$rate" ]]
then
:
elif [ -n "$rates" ] \
&& (( rate < ${rates%%,*} ))
then
rates="$rate,$rates"
else
rates+="${rates+,}$rate"
fi
fi
if [ -n "$depth" ] && ! [[ $depths =~ $expr1"$depth"$expr2 ]]
then
if [[ -n "$depth" ]]
then
:
elif [ -n "$depths" ] \
&& (( depth < ${depths%%,*} ))
then
depths="$depth,$depths"
else
depths+="${depths+,}$depth"
fi
fi
if ! [[ $types =~ $expr1"$type"$expr2 ]]
then
[ -z "$types" ] \
&& unset types
[ -n "$type" ] \
&& types+="${types+,}$type"
fi
if ! [[ $albumartists =~ $expr1"$albumartist"$expr2 ]]
then
[ -z "$albumartists" ] \
&& unset albumartists
[ -n "$albumartist" ] \
&& albumartists+="${albumartists+,}$albumartist"
fi
if ! [[ $albums =~ $expr1"$album"$expr2 ]]
then
[ -z "$albums" ] \
&& unset albums
[ -n "$album" ] \
&& albums+="${albums+,}$album"
fi
if ! [[ $artists =~ $expr1"$artist"$expr2 ]]
then
[ -z "$artists" ] \
&& unset artists
[ -n "$artist" ] \
&& artists+="${artists+,}$artist"
fi
if ! [[ $composers =~ $expr1"$composer"$expr2 ]]
then
[ -z "$composers" ] \
&& unset composers
[ -n "$composer" ] \
&& composers+="${composers+,}$composer"
fi
if ! [[ $discs =~ $expr1"$disc"$expr2 ]]
then
[ -z "$discs" ] \
&& unset discs
[ -n "$disc" ] \
&& discs+="${discs+,}$disc"
fi
if ! [[ $genres =~ $expr1"$genre"$expr2 ]]
then
[ -z "$genres" ] \
&& unset genres
[ -n "$genre" ] \
&& genres+="${genres+,}$genre"
fi
if ! [[ $performers =~ $expr1"$performer"$expr2 ]]
then
[ -z "$performers" ] \
&& unset performers
[ -n "$performer" ] \
&& performers+="${performers+,}$performer"
fi
if ! [[ $releasecountries =~ $expr1"$releasecountry"$expr2 ]]
then
[ -z "$releasecountries" ] \
&& unset releasecountries
[ -n "$releasecountry" ] \
&& releasecountries+="${releasecountries+,}$releasecountry"
fi
if ! [[ $titles =~ $expr1"$title"$expr2 ]]
then
[ -z "$titles" ] \
&& unset titles
[ -n "$title" ] \
&& titles+="${titles+,}$title"
fi
if ! [[ $tracktotals =~ $expr1"$tracktotal"$expr2 ]]
then
[ -z "$tracktotals" ] \
&& unset tracktotals
[ -n "$tracktotal" ] \
&& tracktotals+="${tracktotals+,}$tracktotal"
fi
if ! [[ $years =~ $expr1"$year"$expr2 ]]
then
[ -z "$years" ] \
&& unset years
[ -n "$year" ] \
&& years+="${years+,}$year"
fi
else
if [ -n "$olddir" ]
then
printline
fi
unset bitrates depths rates
channelss="$channels"
(( rate )) && rates="$rate"
(( depth )) && depths="$depth"
types="$type"
albumartists="$albumartist"
albums="$album"
artists="$artist"
composers="$composer"
discs="$disc"
genres="$genre"
performers="$performer"
releasecountries="$releasecountry"
titles="$title"
tracktotals="$tracktotal"
years="$year"
size="$filesize"
count=1
(( bitrate )) && (( bitrates=bitrate ))
oldmimetype=$mimetype
oldrate=$rate
oldtimestamp=$timestamp
fi
unset tracktotal
olddir="$dir"
done
printline
(( stats )) || exit 0
unset types counts counpcts sizes sizepcts
echo '
Top 5 genres:'
maxgenrelen=5
maxcountlen=5
unset counts genres
echo '
SELECT genre,
COUNT(*)
FROM tags
WHERE genre NOT NULL
GROUP BY genre
ORDER BY COUNT(*) DESC
LIMIT 5;
SELECT "AtOM:NoMoreFiles";' >&3
read -u4 line
until [[ $line == AtOM:NoMoreFiles ]]
do
genre="${line%%::AtOM:SQL:Sep::*}"
rest="${line#*::AtOM:SQL:Sep::}::AtOM:SQL:Sep::"
count="${rest%%::AtOM:SQL:Sep::*}"
counts+=( "$count" )
genres+=( "$genre" )
maxcountlen=$(( ${#count} > maxcountlen ? ${#count} : maxcountlen ))
maxgenrelen=$(( ${#genre} > maxgenrelen ? ${#genre} : maxgenrelen ))
read -u4 line
done
head=$(
printf "| # | %'${maxcountlen}s | %-${maxgenrelen}s |" \
Count Genre
)
sep=${head//[^|]/-}
sep=${sep//\|/+}
echo "$sep"
echo "$head"
echo "$sep"
for id in ${!genres[@]}
do
printf "| %i | %'${maxcountlen}i | %-${maxgenrelen}s |\n" \
$(( id + 1 )) \
"${counts[id]}" \
"${genres[id]}"
echo "$sep"
done
unset line genre count rest genres counts maxgenrelen maxcountlen
echo '
Top 10 artists:'
maxartistlen=6
maxcountlen=5
unset counts artists
echo '
SELECT artist,
COUNT(*)
FROM tags
WHERE artist NOT NULL
GROUP BY artist
ORDER BY COUNT(*) DESC
LIMIT 10;
SELECT "AtOM:NoMoreFiles";' >&3
read -u4 line
until [[ $line == AtOM:NoMoreFiles ]]
do
artist="${line%%::AtOM:SQL:Sep::*}"
rest="${line#*::AtOM:SQL:Sep::}::AtOM:SQL:Sep::"
count="${rest%%::AtOM:SQL:Sep::*}"
counts+=( "$count" )
artists+=( "$artist" )
maxcountlen=$(( ${#count} > maxcountlen ? ${#count} : maxcountlen ))
maxartistlen=$(( ${#artist} > maxartistlen ? ${#artist} : maxartistlen ))
read -u4 line
done
head=$(
printf "| # | %'${maxcoutlen}s | %-${maxartistlen}s |" \
Count Artist
)
sep=${head//[^|]/-}
sep=${sep//\|/+}
echo "$sep"
echo "$head"
echo "$sep"
for id in ${!artists[@]}
do
printf "| %2i | %'${maxcountlen}i | %-${maxartistlen}s |\n" \
$(( id + 1 )) \
"${counts[id]}" \
"${artists[id]}"
echo "$sep"
done
unset line artist count rest artists counts maxartistlen maxcountlen
echo '
File formats:'
echo '
SELECT COUNT(*),SUM(size)
FROM source_files
INNER JOIN mime_types
ON source_files.mime_type=mime_types.id;' >&3
read -u4 line
totalcount="${line%%::AtOM:SQL:Sep::*}"
maxcountlen=$(printf "%'i" $totalcount)
maxcountlen=${#maxcountlen}
rest="${line#*::AtOM:SQL:Sep::}::AtOM:SQL:Sep::"
totalsize="${line%%::AtOM:SQL:Sep::*}"
for format in \
audio/flac \
audio/ogg\ vorbis \
audio/ogg\ opus \
audio/mpeg \
audio/mp4 \
\
video/x-ms-asf \
video/webm \
video/mpeg \
video/x-flv \
video/x-msvideo \
\
'image/%' \
'text/%' \
'%'
do
echo '
SELECT COUNT(*),SUM(size)
FROM source_files
INNER JOIN mime_types
ON source_files.mime_type=mime_types.id
WHERE mime_text LIKE "'"$format"'";' >&3
read -u4 line
count="${line%%::AtOM:SQL:Sep::*}"
rest="${line#*::AtOM:SQL:Sep::}::AtOM:SQL:Sep::"
size="${rest%%::AtOM:SQL:Sep::*}"
(( count )) || continue
case $format in
audio/ogg\ opus) type=Opus ;;
audio/ogg\ vorbis) type=Vorbis ;;
audio/mp4) type=MPEG4\ Audio ;;
audio/mpeg) type=MPEG\ Audio ;;
audio/flac) type=FLAC ;;
video/mpeg) type=MPEG\ Video ;;
video/webm) type=WebM ;;
video/x-flv) type=Flash\ Video ;;
video/x-ms-asf) type=Windows\ Media ;;
video/x-msvideo) type=AVI\ Video ;;
audio/%) type=Total\ Audio ;;
video/%) type=Total\ Video ;;
image/%) type=Images ;;
text/%) type=Texts ;;
%) type=Total ;;
esac
counts+=( "$count" )
types+=( "$type" )
maxtypelen=$(( ${#type} > maxtypelen ? ${#type} : maxtypelen ))
if (( size > 1073741823 ))
then
size=$(( (size * 1000) / 1073741824 ))
int=$(( size / 1000 ))
if (( ${#int} > 2 ))
then
sizes+=( "$(printf %4sG $int)" )
else
sizes+=( "$(
printf %2s.%.1sG\
$int \
${size#int}
)" )
fi
elif (( size > 1048575 ))
then
size=$(( (size * 1000) / 1048576 ))
int=$(( size / 1000 ))
if (( ${#int} > 2 ))
then
sizes+=( "$(printf %4sM $int)" )
else
sizes+=( "$(
printf %2s.%.1sM\
$int \
${size#int}
)" )
fi
suffix=M
elif (( size > 1023 ))
then
size=$(( (size * 1000) / 1024 ))
int=$(( size / 1000 ))
if (( ${#int} > 2 ))
then
sizes+=( "$(printf %4sk $int)" )
else
sizes+=( "$(
printf %2s.%.1sk\
$int \
${size#int}
)" )
fi
else
size=$size
sizes+=( "$(printf "%4s " $size)" )
fi
done
head=$(
printf "| %-${maxtypelen}s | %'${maxcountlen}s | %% | Size |"\
Format Count
)
sep=${head//[^|]/-}
sep=${sep//\|/+}
echo "$sep"
echo "$head"
echo "$sep"
for id in ${!types[@]}
do
printf "| %-${maxtypelen}s | %'${maxcountlen}i | %3i%% | %5s |\n"\
"${types[id]}" \
"${counts[id]}" \
$(( ${counts[id]} * 100 / totalcount )) \
"${sizes[id]}"
echo "$sep"
done