metadata scripts
This commit is contained in:
1 parent
2044545136
commit
83e6d7ee22
3 files changed
+382
No files matched your search
@@ -0,0 +1,205 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
from difflib import SequenceMatcher
|
||||||
|
from turtledemo.lindenmayer import replace
|
||||||
|
|
||||||
|
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):
|
||||||
|
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 = r"Top 100 SciFi Books #\d+"
|
||||||
|
# pattern = r"Enderverse"
|
||||||
|
replace = r"Top 100 SciFi Books"
|
||||||
|
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)
|
||||||
|
|
||||||
|
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, author, title, year = parts
|
||||||
|
elif len(parts) == 3:
|
||||||
|
rank, author, title = 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} #{int(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, int(rank), year, args.dry_run)
|
||||||
|
if ret == "skip":
|
||||||
|
break
|
||||||
|
# print("next")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
#!/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
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
#!/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
|
||||||
Reference in new issue
Block a user