move files
move scripts
This commit is contained in:
1 parent
5cdbf51c1e
commit
408566b6ea
21 files changed
+53
No files matched your search
@@ -1,29 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ -z "$1" ]; then
|
||||
echo "Usage: $0 <Series> [additional ffmpeg_handling.py args...]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
series="$1"
|
||||
shift
|
||||
|
||||
for d in */; do
|
||||
[ -d "$d" ] || continue
|
||||
name="${d%/}"
|
||||
|
||||
# extract leading number (int or float)
|
||||
if [[ $name =~ ([0-9]+([.,][0-9]+)?) ]]; then
|
||||
num="${BASH_REMATCH[1]}"
|
||||
else
|
||||
continue
|
||||
fi
|
||||
echo $d $num
|
||||
/home/lucas/scripts/.venv/bin/python \
|
||||
/home/lucas/scripts/ffmpeg_handling.py \
|
||||
ms "$name" \
|
||||
--override \
|
||||
-k grouping \
|
||||
-v "$series #$num" \
|
||||
"$@"
|
||||
done
|
||||
@@ -1,64 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -Eeuo pipefail
|
||||
escape_kid3() {
|
||||
printf '%s' "$1" | sed "s/'/'\\\\''/g"
|
||||
}
|
||||
DRY_RUN=false
|
||||
|
||||
if [[ "$1" == "--dry-run" ]]; then
|
||||
DRY_RUN=true
|
||||
shift
|
||||
fi
|
||||
|
||||
if [[ $# -ne 1 ]]; then
|
||||
echo "Usage: $0 [--dry-run] file.txt"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
list="$1"
|
||||
|
||||
if [[ ! -f "$list" ]]; then
|
||||
echo "ERROR: File list not found: $list"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
while IFS= read -r file; do
|
||||
|
||||
[[ -z "$file" ]] && continue
|
||||
|
||||
if [[ ! -e "$file" ]]; then
|
||||
echo "ERROR: Missing file: $file"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -d "$file" ]]; then
|
||||
echo "WARNING: Is a directory, skipping: $file"
|
||||
continue
|
||||
fi
|
||||
|
||||
bookdir="$(dirname "$file")"
|
||||
booktitle="$(basename "$bookdir")"
|
||||
author="$(basename "$(dirname "$bookdir")")"
|
||||
|
||||
echo
|
||||
echo "File: $file"
|
||||
echo "Artist: $author"
|
||||
echo "Album: $booktitle"
|
||||
|
||||
if $DRY_RUN; then
|
||||
echo "DRY RUN: no changes made"
|
||||
continue
|
||||
fi
|
||||
|
||||
artist=$(escape_kid3 "$author")
|
||||
album=$(escape_kid3 "$booktitle")
|
||||
|
||||
kid3-cli "$file" \
|
||||
-c "tag 1" \
|
||||
-c "set artist '$artist'" \
|
||||
-c "set album '$album'" \
|
||||
-c "tag 2" \
|
||||
-c "set artist '$artist'" \
|
||||
-c "set album '$album'" \
|
||||
-c save
|
||||
done < "$list"
|
||||
@@ -1,40 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
BASE="${1:-.}"
|
||||
|
||||
for dir in "$BASE"/*/; do
|
||||
[ -d "$dir" ] || continue
|
||||
|
||||
title=$(basename "$dir")
|
||||
|
||||
# Skip directories starting with a number
|
||||
[[ "$title" =~ ^[0-9] ]] && continue
|
||||
|
||||
# Author = parent directory of book directory
|
||||
author=$(basename "$(dirname "$dir")")
|
||||
|
||||
# Fix author name formatting
|
||||
author="${author//JRR/J. R. R.}"
|
||||
|
||||
for pdf in "$dir"/*.pdf; do
|
||||
[ -f "$pdf" ] || continue
|
||||
|
||||
opf="$dir/metadata.opf"
|
||||
|
||||
cat > "$opf" <<EOF
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<package xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:opf="http://www.idpf.org/2007/opf"
|
||||
version="2.0">
|
||||
<metadata>
|
||||
<dc:title>$title</dc:title>
|
||||
<dc:creator opf:role="aut">$author</dc:creator>
|
||||
<dc:language>en</dc:language>
|
||||
<dc:format>application/pdf</dc:format>
|
||||
</metadata>
|
||||
</package>
|
||||
EOF
|
||||
|
||||
echo "Created: $opf"
|
||||
done
|
||||
done
|
||||
@@ -1,203 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from difflib import SequenceMatcher
|
||||
|
||||
AUDIO_EXTENSIONS = {
|
||||
".mp3",
|
||||
".m4b",
|
||||
".m4a",
|
||||
}
|
||||
|
||||
RED = "\033[31m"
|
||||
GREEN = "\033[32m"
|
||||
RESET = "\033[0m"
|
||||
USE_KID3 = True
|
||||
def normalize(text: str) -> str:
|
||||
return re.sub(r"[^a-z0-9]", "", text.lower())
|
||||
|
||||
|
||||
def get_tag(path, tag):
|
||||
if USE_KID3:
|
||||
result = subprocess.run(
|
||||
["kid3-cli", "-c", f"get {tag}", str(path)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
return result.stdout.strip()
|
||||
|
||||
# id3v2 frame mapping
|
||||
frames = {
|
||||
"album": "TALB",
|
||||
"artist": "TPE1",
|
||||
"title": "TIT2",
|
||||
"year": "TYER",
|
||||
"grouping": "TIT1", # Content group description
|
||||
"work": "TIT1", # No separate WORK frame in id3v2
|
||||
}
|
||||
|
||||
frame = frames[tag]
|
||||
|
||||
result = subprocess.run(
|
||||
["id3v2", "-R", str(path)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
|
||||
for line in result.stdout.splitlines():
|
||||
if line.startswith(frame):
|
||||
return line.split(":", 1)[1].strip()
|
||||
|
||||
return ""
|
||||
|
||||
def tag_file(path: Path, rank: int, year: str, dry_run: bool, replace_pattern: str=r"Top 100 SciFi Books" )
|
||||
work = get_tag(path, "work")
|
||||
grouping = get_tag(path, "grouping")
|
||||
existing_year = get_tag(path, "year")
|
||||
|
||||
if work:
|
||||
tag = "work"
|
||||
existing = work
|
||||
elif grouping:
|
||||
tag = "grouping"
|
||||
existing = grouping
|
||||
else:
|
||||
tag = "grouping"
|
||||
existing = ""
|
||||
pattern = f"{replace_pattern} #\d+(?:\.\d+)?"
|
||||
|
||||
if existing:
|
||||
if re.search(pattern, existing):
|
||||
new_group = re.sub(
|
||||
pattern,
|
||||
f"{replace} #{rank};",
|
||||
existing,
|
||||
)
|
||||
else:
|
||||
new_group = f"{existing}; {replace} #{rank};"
|
||||
else:
|
||||
new_group = f"{replace} #{rank};"
|
||||
|
||||
new_group = re.sub(r";+", ";", new_group)
|
||||
# Skip if nothing changes
|
||||
if existing == new_group and existing_year == str(year):
|
||||
print("Skipping (already tagged)")
|
||||
return "skip"
|
||||
print(f"Would tag {path.name}: {new_group} {year}")
|
||||
|
||||
if dry_run:
|
||||
return
|
||||
|
||||
subprocess.run(
|
||||
[
|
||||
"kid3-cli",
|
||||
"-c", f'set {tag} "{new_group}"',
|
||||
"-c", f"set year {year}",
|
||||
str(path),
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
parser.add_argument("library")
|
||||
parser.add_argument("booklist")
|
||||
parser.add_argument("--dry-run", action="store_true")
|
||||
parser.add_argument("--find-missing", action="store_true")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
library = Path(args.library)
|
||||
replace_pattern=Path(args.booklist).stem
|
||||
found = set()
|
||||
|
||||
with open(args.booklist, encoding="utf-8") as f:
|
||||
books = [
|
||||
line.strip()
|
||||
for line in f
|
||||
if line.strip() and not line.startswith("#")
|
||||
]
|
||||
|
||||
author_dirs = {
|
||||
normalize(p.name): p
|
||||
for p in library.iterdir()
|
||||
if p.is_dir()
|
||||
}
|
||||
rank_list=[]
|
||||
# print(books)
|
||||
for line in books:
|
||||
line = line.strip()
|
||||
|
||||
if not line:
|
||||
continue # skip empty lines
|
||||
|
||||
parts = line.split(" - ")
|
||||
|
||||
if len(parts) == 4:
|
||||
rank, title, author, year = parts
|
||||
elif len(parts) == 3:
|
||||
rank, title, author = parts
|
||||
year = None
|
||||
else:
|
||||
raise ValueError(f"Invalid line: {line}")
|
||||
author_key = normalize(author)
|
||||
title_key = normalize(title)
|
||||
rank_list.append(rank)
|
||||
author_dir = author_dirs.get(author_key)
|
||||
|
||||
if author_dir is None:
|
||||
print(f"{RED}AUTHOR{RESET} {author} ")
|
||||
continue
|
||||
|
||||
book_dir = None
|
||||
best_score = 0.0
|
||||
|
||||
for subdir in author_dir.rglob("*"):
|
||||
if not subdir.is_dir():
|
||||
continue
|
||||
score = SequenceMatcher(
|
||||
None,
|
||||
title_key,
|
||||
normalize(subdir.name),
|
||||
).ratio()
|
||||
|
||||
if score > best_score:
|
||||
best_score = score
|
||||
book_dir = subdir
|
||||
|
||||
if best_score < 0.6:
|
||||
book_dir = None
|
||||
print(f"{RED}TITLE{RESET} {title}, Score {best_score} ")
|
||||
for subdir in author_dir.rglob("*"):
|
||||
if not subdir.is_dir():
|
||||
continue
|
||||
else:
|
||||
print(subdir)
|
||||
|
||||
continue
|
||||
|
||||
found.add(rank)
|
||||
|
||||
print(f"{GREEN}FOUND{RESET} #{rank:3} {title} in {book_dir}")
|
||||
|
||||
if args.find_missing:
|
||||
continue
|
||||
|
||||
for file in book_dir.iterdir():
|
||||
if file.suffix.lower() not in AUDIO_EXTENSIONS:
|
||||
continue
|
||||
|
||||
ret=tag_file(file, rank, year, args.dry_run, replace_pattern)
|
||||
if ret == "skip":
|
||||
break
|
||||
#print("next")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
DRY_RUN=false
|
||||
DELETE=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--dry-run)
|
||||
DRY_RUN=true
|
||||
shift
|
||||
;;
|
||||
--delete)
|
||||
DELETE=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ $# -ne 1 ]]; then
|
||||
echo "Usage: $0 [--dry-run] [--delete] file.txt"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
list="$1"
|
||||
|
||||
while IFS= read -r file; do
|
||||
|
||||
[[ -z "$file" ]] && continue
|
||||
|
||||
if [[ ! -e "$file" ]]; then
|
||||
echo "ERROR: Missing file: $file"
|
||||
exit 1
|
||||
fi
|
||||
if [[ -d "$file" ]]; then
|
||||
echo "WARNING: Is a directory, skipping: $file"
|
||||
continue
|
||||
fi
|
||||
# Get comment from ID3v2
|
||||
comment=$(kid3-cli "$file" \
|
||||
-c "tag 2" \
|
||||
-c "get comment" 2>/dev/null)
|
||||
|
||||
[[ -z "$comment" ]] && continue
|
||||
|
||||
echo
|
||||
echo "File: $file"
|
||||
echo "Comment: $comment"
|
||||
|
||||
composer=""
|
||||
|
||||
# Match: read by Name; text
|
||||
if [[ "$comment" =~ ^[Rr]ead[[:space:]]+[Bb]y[[:space:]]+([^;]+)\; ]]; then
|
||||
composer="${BASH_REMATCH[1]}"
|
||||
composer="$(echo "$composer" | sed 's/[[:space:]]*$//')"
|
||||
|
||||
echo "Found narrator: $composer"
|
||||
|
||||
if $DRY_RUN; then
|
||||
echo "DRY RUN: set composer '$composer', delete comment"
|
||||
else
|
||||
kid3-cli "$file" \
|
||||
-c "tag 2" \
|
||||
-c "set composer '$composer'" \
|
||||
-c "set comment ''" \
|
||||
-c save
|
||||
fi
|
||||
|
||||
elif [[ ${#comment} -lt 40 ]]; then
|
||||
|
||||
echo "Short comment (${#comment} chars): $comment"
|
||||
|
||||
if $DELETE; then
|
||||
if $DRY_RUN; then
|
||||
echo "DRY RUN: delete comment"
|
||||
else
|
||||
kid3-cli "$file" \
|
||||
-c "tag 2" \
|
||||
-c "set comment ''" \
|
||||
-c save
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
done < "$list"
|
||||
@@ -1,101 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
LIBRARY="$1"
|
||||
MODE="${2:-}"
|
||||
REPORT="${3:-/home/lucas/audiobook_genre_cleanup.txt}"
|
||||
|
||||
if [[ -z "$LIBRARY" ]]; then
|
||||
echo "Usage: $0 <library_path> [--delete] [report_file]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mapfile -t FILES < <(
|
||||
find "$LIBRARY" -type f \( \
|
||||
-iname "*.mp3" -o \
|
||||
-iname "*.m4b" -o \
|
||||
-iname "*.m4a" \
|
||||
\)
|
||||
)
|
||||
|
||||
TOTAL=${#FILES[@]}
|
||||
PROCESSED=0
|
||||
MATCHES=0
|
||||
MODIFIED=0
|
||||
|
||||
{
|
||||
echo "Audiobook Genre Cleanup Report"
|
||||
echo "Generated: $(date)"
|
||||
echo "Library: $LIBRARY"
|
||||
echo "Total files: $TOTAL"
|
||||
echo
|
||||
} > "$REPORT"
|
||||
|
||||
for file in "${FILES[@]}"; do
|
||||
((PROCESSED++))
|
||||
|
||||
if (( PROCESSED % 100 == 0 )); then
|
||||
echo "Progress: $PROCESSED/$TOTAL files processed"
|
||||
fi
|
||||
genre=$(kid3-cli -c "get genre" "$file" 2>/dev/null)
|
||||
|
||||
[[ -z "$genre" ]] && continue
|
||||
cleaned=$(printf '%s\n' "$genre" | sed -E '
|
||||
s/[Aa]udio[[:space:]]*[Bb]ooks?//g
|
||||
s/\b(Hörbücher?|Hoerbücher?|Hoerbuecher?|Hörspiele?|Hoerspiele?)\b//Ig
|
||||
|
||||
# Normalize genre names
|
||||
s/\b(Sci[ -]?Fi|Science Fiction|SF)\b/SciFi/Ig
|
||||
s/\bnonfiction\b/NonFiction/Ig
|
||||
|
||||
# Normalize separators (&, :, / -> ;)
|
||||
s/\b(Sci[ -]?Fi|Science Fiction|SF)\b/Science Fiction/Ig
|
||||
s/[[:space:]]*[,&:\/][[:space:]]*/;/g
|
||||
|
||||
# Normalize commas
|
||||
s/[[:space:]]*;[[:space:]]*/; /g
|
||||
|
||||
# Trim leading/trailing punctuation and whitespace
|
||||
s/^[[:space:],;:-]+//
|
||||
s/[[:space:],;-]+$//
|
||||
')
|
||||
# s/.*//;
|
||||
|
||||
if [[ "$genre" != "$cleaned" ]]; then
|
||||
((MATCHES++))
|
||||
|
||||
status="MATCH"
|
||||
|
||||
if [[ "$MODE" == "--delete" ]]; then
|
||||
kid3-cli \
|
||||
-c "tag 2" \
|
||||
-c "set genre \"$cleaned\"" \
|
||||
-c save \
|
||||
"$file"
|
||||
((MODIFIED++))
|
||||
status="UPDATED"
|
||||
fi
|
||||
|
||||
printf '[%d/%d] [%s] %s | OLD="%s" | NEW="%s"\n' \
|
||||
"$PROCESSED" "$TOTAL" "$status" "$file" "$genre" "$cleaned"
|
||||
|
||||
printf '[%d/%d] [%s] %s | OLD="%s" | NEW="%s"\n' \
|
||||
"$PROCESSED" "$TOTAL" "$status" "$file" "$genre" "$cleaned" >> "$REPORT"
|
||||
fi
|
||||
|
||||
done
|
||||
|
||||
{
|
||||
echo
|
||||
echo "========== SUMMARY =========="
|
||||
echo "Files scanned : $TOTAL"
|
||||
echo "Matches found : $MATCHES"
|
||||
echo "Files updated : $MODIFIED"
|
||||
} >> "$REPORT"
|
||||
|
||||
echo
|
||||
echo "========== SUMMARY =========="
|
||||
echo "Files scanned : $TOTAL"
|
||||
echo "Matches found : $MATCHES"
|
||||
echo "Files updated : $MODIFIED"
|
||||
echo "Report written to: $REPORT"
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
shopt -s globstar nullglob
|
||||
|
||||
root="${1:-.}"
|
||||
|
||||
for file in "$root"/**/*; do
|
||||
[[ -f "$file" ]] || continue
|
||||
|
||||
case "${file##*.}" in
|
||||
epub|EPUB)
|
||||
continue
|
||||
;;
|
||||
nfo|NFO)
|
||||
grouping="$(grep -i 'Series Name:' "$file" 2>/dev/null | sed 's/^[Ss]eries:[[:space:]]*//')"
|
||||
;;
|
||||
m4b|M4B|m4a|M4A|mp3|MP3|aac|AAC|flac|FLAC)
|
||||
grouping="$(kid3-cli -c "get grouping" "$file" 2>/dev/null)"
|
||||
;;
|
||||
*)
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ ! "$grouping" =~ Publication ]]; then
|
||||
echo Grouping: $grouping
|
||||
echo "$file"
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
root="${1:-.}"
|
||||
mode="${2:-}"
|
||||
THRESHOLD=40
|
||||
find "$root" \( -iname '*.mp3' -o -iname '*.m4a' -o -iname '*.m4b' \) -print0 |
|
||||
while IFS= read -r -d '' f; do
|
||||
|
||||
comment=$(ffprobe -v error \
|
||||
-show_entries format_tags=comment \
|
||||
-of default=nw=1:nk=1 \
|
||||
-- "$f")
|
||||
|
||||
description=$(ffprobe -v error \
|
||||
-show_entries format_tags=description \
|
||||
-of default=nw=1:nk=1 \
|
||||
-- "$f")
|
||||
|
||||
# trim comment
|
||||
comment="${comment#"${comment%%[![:space:]]*}"}"
|
||||
comment="${comment%"${comment##*[![:space:]]}"}"
|
||||
|
||||
# ---- DELETE MODE ----
|
||||
if [[ "$mode" == "del" && -n "$comment" && ${#comment} -lt $THRESHOLD ]]; then
|
||||
tmp=$(mktemp -p "$(dirname -- "$f")" ".fftmp.XXXXXX.${f##*.}") || continue
|
||||
|
||||
echo "Removing short comment: $f"
|
||||
echo " comment: $comment"
|
||||
|
||||
ffmpeg -nostdin -v error -y \
|
||||
-i "$f" \
|
||||
-map 0 \
|
||||
-map_metadata 0 \
|
||||
-c copy \
|
||||
-metadata comment= \
|
||||
"$tmp" &&
|
||||
mv -f -- "$tmp" "$f" || {
|
||||
echo "FAILED: $f" >&2
|
||||
rm -f -- "$tmp"
|
||||
}
|
||||
|
||||
continue
|
||||
fi
|
||||
|
||||
# ---- SCAN MODE ----
|
||||
# only show short non-empty tags (<20 chars)
|
||||
for tag in comment description; do
|
||||
value="${!tag}"
|
||||
|
||||
value="${value#"${value%%[![:space:]]*}"}"
|
||||
value="${value%"${value##*[![:space:]]}"}"
|
||||
|
||||
if [[ -n "$value" && ${#value} -lt $THRESHOLD ]]; then
|
||||
printf "%s\n %s: %s\n\n" "$f" "$tag" "$value"
|
||||
fi
|
||||
done
|
||||
|
||||
done
|
||||
@@ -1,48 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
root="${1:-.}"
|
||||
find "$root" \( -iname '*.mp3' -o -iname '*.m4a' -o -iname '*.m4b' \) -print0 |
|
||||
while IFS= read -r -d '' f; do
|
||||
|
||||
comment=$(ffprobe -v error \
|
||||
-show_entries format_tags=comment \
|
||||
-of default=nw=1:nk=1 \
|
||||
-- "$f")
|
||||
|
||||
[[ $comment =~ ^[Rr]ead[[:space:]]+[Bb]y[[:space:]]+(.+) ]] || continue
|
||||
|
||||
composer="${BASH_REMATCH[1]}"
|
||||
echo "Updating: $f"
|
||||
echo " Composer: $composer"
|
||||
|
||||
dir=$(dirname -- "$f")
|
||||
base=$(basename -- "$f")
|
||||
ext="${base##*.}"
|
||||
|
||||
case "${ext,,}" in
|
||||
mp3) format=mp3 ;;
|
||||
m4a|m4b) format=ipod ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
|
||||
tmp=$(mktemp -p "$dir" ".${base}.XXXXXX") || {
|
||||
echo "Couldn't create temporary file in $dir" >&2
|
||||
continue
|
||||
}
|
||||
|
||||
if ffmpeg -nostdin -v error -y \
|
||||
-i "$f" \
|
||||
-map 0 \
|
||||
-map_metadata 0 \
|
||||
-c copy \
|
||||
-metadata composer="$composer" \
|
||||
-metadata comment= \
|
||||
-f "$format" \
|
||||
"$tmp"
|
||||
then
|
||||
mv -f -- "$tmp" "$f"
|
||||
else
|
||||
echo "FAILED: $f" >&2
|
||||
rm -f -- "$tmp"
|
||||
fi
|
||||
done
|
||||
@@ -1,137 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
RED=$'\e[31m'
|
||||
GREEN=$'\e[32m'
|
||||
RESET=$'\e[0m'
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage:
|
||||
$(basename "$0") <library-root> <book-list> [--dry-run|--find-missing]
|
||||
|
||||
Arguments:
|
||||
library-root Root containing author/*/book/
|
||||
book-list Text file containing:
|
||||
rank|title|author|year
|
||||
|
||||
Options:
|
||||
--dry-run Show what would be tagged without writing tags
|
||||
--find-missing List books from the ranking that were not found
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ $# -ge 2 ]] || usage
|
||||
|
||||
ROOT="$1"
|
||||
BOOKLIST="$2"
|
||||
MODE="write"
|
||||
|
||||
case "${3:-}" in
|
||||
--dry-run)
|
||||
MODE="dryrun"
|
||||
;;
|
||||
--find-missing)
|
||||
MODE="missing"
|
||||
;;
|
||||
"")
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
|
||||
[[ -d "$ROOT" ]] || {
|
||||
echo "Library not found: $ROOT" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ -f "$BOOKLIST" ]] || {
|
||||
echo "Book list not found: $BOOKLIST" >&2
|
||||
exit 1
|
||||
}
|
||||
echo "Using MODE $MODE"
|
||||
echo "Libary: $ROOT"
|
||||
echo "File: $BOOKLIST"
|
||||
|
||||
normalize() {
|
||||
echo "$1" |
|
||||
tr '[:upper:]' '[:lower:]' |
|
||||
sed -E "s/[^a-z0-9]+//g"
|
||||
}
|
||||
|
||||
declare -A FOUND
|
||||
|
||||
while IFS='|' read -r RANK TITLE AUTHOR YEAR; do
|
||||
|
||||
# Skip blank lines and comments
|
||||
[[ -z "$RANK" ]] && continue
|
||||
[[ "$RANK" =~ ^# ]] && continue
|
||||
|
||||
NTITLE=$(normalize "$TITLE")
|
||||
NAUTHOR=$(normalize "$AUTHOR")
|
||||
|
||||
MATCH=""
|
||||
|
||||
AUTHOR_PATH=$(find "$ROOT" -type d -iname "*$AUTHOR*" | head -n1)
|
||||
|
||||
[[ -z "$AUTHOR_PATH" ]] && continue
|
||||
|
||||
BOOK_PATH="$AUTHOR_PATH/book"
|
||||
[[ -d "$BOOK_PATH" ]] || continue
|
||||
|
||||
while IFS= read -r FILE; do
|
||||
FILE_NAME="${FILE##*/}"
|
||||
FILE_BASE="${FILE_NAME%.*}"
|
||||
|
||||
echo "Checking: $FILE_BASE"
|
||||
|
||||
if [[ "$(normalize "$FILE_BASE")" == "$NTITLE" ]]; then
|
||||
MATCH="$FILE"
|
||||
break
|
||||
fi
|
||||
done < <(find "$BOOK_PATH" -maxdepth 1 -type f)
|
||||
if [[ -n "$MATCH" ]]; then
|
||||
FOUND["$RANK"]=1
|
||||
|
||||
GROUP="Sci Fi Top 100 Books #$RANK"
|
||||
|
||||
case "$MODE" in
|
||||
dryrun)
|
||||
echo "Would tag:"
|
||||
echo " File : $MATCH"
|
||||
echo " Album: $GROUP"
|
||||
echo " Year : $YEAR"
|
||||
echo
|
||||
;;
|
||||
|
||||
write)
|
||||
echo "Tagging: $MATCH"
|
||||
|
||||
id3v2 \
|
||||
--TALB "$GROUP" \
|
||||
--TYER "$YEAR" \
|
||||
"$MATCH"
|
||||
;;
|
||||
|
||||
missing)
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
done < "$BOOKLIST"
|
||||
|
||||
if [[ "$MODE" == "missing" ]]; then
|
||||
while IFS='|' read -r RANK TITLE AUTHOR YEAR; do
|
||||
[[ -z "$RANK" ]] && continue
|
||||
[[ "$RANK" =~ ^# ]] && continue
|
||||
|
||||
if [[ -n "${FOUND[$RANK]:-}" ]]; then
|
||||
printf "${GREEN}FOUND${RESET} #%3d | %s | %s | %s\n" \
|
||||
"$RANK" "$TITLE" "$AUTHOR" "$YEAR"
|
||||
else
|
||||
printf "${RED}MISSING${RESET} #%3d | %s | %s | %s\n" \
|
||||
"$RANK" "$TITLE" "$AUTHOR" "$YEAR"
|
||||
fi
|
||||
done < "$BOOKLIST"
|
||||
fi
|
||||
Reference in new issue
Block a user