Opens a larger view. Escape closes it.

home-server

sort.sh

#!/bin/sh
DOWNLOADS=/downloads
INGEST=/ingest
SENT=/downloads/.cwa-sent
AUDIOBOOKS=$DOWNLOADS/audiobooks
ABS=http://172.21.0.1:13378
ABS_USER=CHANGE_ME
ABS_PASS="CHANGE_ME"
AUDIO_LIB=d93406c2-ae51-45a3-978f-a06ff63f8c2f

mkdir -p "$AUDIOBOOKS" "$SENT"
rm -f /tmp/moved_ebooks /tmp/moved_audio

# Fresh ABS token on every run — avoids stale JWT from file
TOKEN=$(wget -qO- --header="Content-Type: application/json" \
  --post-data="{\"username\":\"$ABS_USER\",\"password\":\"$ABS_PASS\"}" \
  "$ABS/login" 2>/dev/null | python3 -c "import json,sys; print(json.load(sys.stdin)['user']['accessToken'])" 2>/dev/null)
[ -z "$TOKEN" ] && echo "[sort] Warning: could not get ABS token, metadata step will be skipped"

# Process EPUBs and PDFs — quote path exclusions to prevent glob expansion.
# All prose ebooks go to CWA's ingest folder; CWA auto-converts/organizes and
# then REMOVES the file from ingest once done (it's a watch-and-consume
# folder, not persistent storage). kobodl also has no way to skip
# re-downloading a title on every 300s cycle, and re-writes over our symlink
# each time -- so "does it already exist in ingest" is never a reliable
# skip-check (ingest is usually empty because CWA just ate it). Track what's
# already been sent in $SENT instead, which nothing else touches.
find "$DOWNLOADS" \( -name '*.epub' -o -name '*.pdf' \) \
  ! -path "${AUDIOBOOKS}/*" ! -path "${SENT}/*" \
  ! -type l | while IFS= read -r f; do
    fname=$(basename "$f")
    if [ ! -f "$SENT/$fname" ]; then
      cp "$f" "$INGEST/$fname" && echo "[ebook] -> ingest: $fname"
      cp "$f" "$SENT/$fname"
      echo "$fname" >> /tmp/moved_ebooks
    fi
    # Symlink back so kobodl's re-download lands somewhere harmless-ish;
    # this is belt-and-suspenders since the real skip-check is $SENT now.
    ln -sfn "$SENT/$fname" "$f" 2>/dev/null
  done

# Process audio — use while loop (not xargs) to handle spaces in filenames
find "$DOWNLOADS" \( -name '*.mp3' -o -name '*.m4b' -o -name '*.m4a' \) \
  ! -path "${AUDIOBOOKS}/*" \
  ! -type l | while IFS= read -r af; do
    dirname "$af"
  done | sort -u | while IFS= read -r audiodir; do
    [ -L "$audiodir" ] && continue
    title=$(basename "$audiodir")
    author=$(basename "$(dirname "$audiodir")")
    dest="$AUDIOBOOKS/$author/$title"
    [ "$audiodir" = "$dest" ] && continue
    mkdir -p "$AUDIOBOOKS/$author"
    mv "$audiodir" "$dest" && echo "[audiobook] $author/$title"
    echo "$author/$title" >> /tmp/moved_audio
    mkdir -p "$(dirname "$audiodir")"
    ln -sfn "$dest" "$audiodir"
  done

# Companion: find any epub for the same title in $SENT (the persistent
# ebook store) -- NOT $INGEST, which CWA empties out after processing.
for audiobook_dir in "$AUDIOBOOKS"/*/*; do
  [ -d "$audiobook_dir" ] || continue
  title=$(basename "$audiobook_dir")
  ebook_file=$(find "$SENT" -maxdepth 1 -iname "*${title}*.epub" 2>/dev/null | head -1)
  [ -z "$ebook_file" ] && continue
  ebook_fname=$(basename "$ebook_file")
  if [ ! -f "$audiobook_dir/$ebook_fname" ]; then
    cp "$ebook_file" "$audiobook_dir/"
    echo "[companion] $title"
    echo "$title" >> /tmp/moved_audio
  fi
done

HAS_EBOOKS=0; HAS_AUDIO=0
[ -s /tmp/moved_ebooks ] && HAS_EBOOKS=1
[ -s /tmp/moved_audio ] && HAS_AUDIO=1
[ "$HAS_EBOOKS$HAS_AUDIO" = "00" ] && exit 0

[ -z "$TOKEN" ] && echo "[sort] No ABS token, skipping ABS scan" && exit 0

abs_post() { wget -qO- --header="Authorization: Bearer $TOKEN" --header="Content-Type: application/json" --post-data="$2" "$ABS$1" 2>/dev/null; }
abs_get()  { wget -qO- --header="Authorization: Bearer $TOKEN" "$ABS$1" 2>/dev/null; }

[ "$HAS_AUDIO" = "1" ] && echo "[sort] Scanning audiobooks..." && abs_post "/api/libraries/$AUDIO_LIB/scan" "{}" > /dev/null

echo "[sort] Waiting 20s for ABS to index..."
sleep 20

fetch_metadata() {
  LIB=$1 PROV=$2
  abs_get "/api/libraries/$LIB/items?limit=500" | python3 -c "
import sys,json
try:
    r=json.load(sys.stdin)
    for item in r.get('results',[]):
        m=item.get('media',{}).get('metadata',{})
        if not item.get('coverPath') or not m.get('description'):
            t=m.get('title','?')
            a=m.get('authorName',m.get('authors','?'))
            print(item['id']+'|'+t+'|'+str(a))
except: pass
" 2>/dev/null | while IFS='|' read -r ID TITLE AUTHOR; do
    [ -z "$ID" ] && continue
    echo "Matching: $TITLE"
    abs_post "/api/items/$ID/match" "{\"provider\":\"$PROV\",\"updateDetails\":true}" > /dev/null
    sleep 3
  done
}

[ "$HAS_AUDIO" = "1" ] && fetch_metadata "$AUDIO_LIB" audible

rm -f /tmp/moved_ebooks /tmp/moved_audio
echo "[sort] Done."