commit 6be8201511f2c43f4f886bc37b5c6aac757f90b9 Author: Urban Date: Thu Oct 9 10:55:52 2025 +0200 first commit diff --git a/._wunf_upd_prefix.sh b/._wunf_upd_prefix.sh new file mode 100755 index 0000000..eea6093 Binary files /dev/null and b/._wunf_upd_prefix.sh differ diff --git a/checkmp4.sh b/checkmp4.sh new file mode 100755 index 0000000..9a361e9 --- /dev/null +++ b/checkmp4.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# +# checkmp4.sh +# +# Verifies all .mp4 files in the given directory ie checks if they are broken or not. +# Prints "OK" or "Not OK" for each file. +# Exit code is 0 if all files are OK, 1 if any are broken. + +dir="$1" + +if [ -z "$dir" ]; then + echo "Usage: $0 " + exit 2 +fi + +if [ ! -d "$dir" ]; then + echo "Not a directory: $dir" + exit 2 +fi + +bad=0 + +# loop through mp4 files (non-recursive) +# use `find "$dir" -type f -iname "*.mp4"` if you want recursive +shopt -s nullglob +for file in "$dir"/*.mp4 "$dir"/*.MP4; do + [ -e "$file" ] || continue + + # --- Test 1: metadata sanity check --- + ffprobe -v error -select_streams v:0 -show_entries stream=width,height,duration \ + -of csv=p=0 "$file" > /dev/null 2>&1 + probe_status=$? + + # --- Test 2: deep read/decode --- + ffmpeg -v error -xerror -i "$file" -f null - -nostats > /dev/null 2>&1 + decode_status=$? + + if [ $probe_status -eq 0 ] && [ $decode_status -eq 0 ]; then + echo "OK $file" + else + echo "Not OK $file" + bad=1 + fi +done +shopt -u nullglob + +exit $bad diff --git a/clean_nfo.sh b/clean_nfo.sh new file mode 100755 index 0000000..d4de97a --- /dev/null +++ b/clean_nfo.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +set -euo pipefail + +DRYRUN=false +DIR="." + +# Argumenthantering +for arg in "$@"; do + case "$arg" in + --dry-run) DRYRUN=true ;; + *) DIR="$arg" ;; + esac +done + +# Kontroll att katalogen finns +if [ ! -d "$DIR" ]; then + echo "Fel: katalogen '$DIR' finns inte" + exit 1 +fi + +echo "Söker efter .nfo-filer i: $DIR" +$DRYRUN && echo ">>> DRY-RUN: inga filer tas bort <<<" + +# Gå igenom alla .nfo-filer +find "$DIR" -type f -iname '*.nfo' -print0 | +while IFS= read -r -d '' nfo; do + base="${nfo%.*}" + + # Matcha motsvarande .mp4 (case-insensitive) + shopt -s nullglob nocaseglob + mp4s=( "$base".mp4 ) + shopt -u nocaseglob + + if (( ${#mp4s[@]} == 0 )); then + if $DRYRUN; then + echo "[dry-run] would delete: $nfo" + else + echo "Deleting: $nfo" + rm -v -- "$nfo" + fi + fi +done diff --git a/compare_advanced.sh b/compare_advanced.sh new file mode 100755 index 0000000..40bb654 --- /dev/null +++ b/compare_advanced.sh @@ -0,0 +1,127 @@ +#!/usr/bin/env bash +# compare_advanced.sh file1.mp4 file2.mp4 +set -euo pipefail + +f1="${1:-}"; f2="${2:-}" +if [[ -z "$f1" || -z "$f2" ]]; then + echo "Usage: $0 " >&2 + exit 2 +fi +for f in "$f1" "$f2"; do + [[ -f "$f" ]] || { echo "Not found: $f" >&2; exit 2; } +done + +have_signature_filter() { + ffmpeg -hide_banner -filters 2>/dev/null | grep -qE ' V[.*] signature ' +} + +# Pretty print quick summary (reuse from simple approach) +summary() { + local f="$1" + local size dur v w h fps + size=$(stat -c%s "$f" 2>/dev/null || stat -f%z "$f") + dur=$(ffprobe -v error -show_entries format=duration -of default=nw=1:nk=1 "$f" || echo "") + v=$(ffprobe -v error -select_streams v:0 -show_entries stream=codec_name,width,height,r_frame_rate -of csv=p=0 "$f" | head -n1) + IFS=',' read -r vcodec w h rfr <<<"$v" + if [[ "$rfr" == */* ]]; then + fps=$(awk -v r="$rfr" 'BEGIN{split(r,a,"/"); if(a[2]==0) print 0; else printf "%.3f", a[1]/a[2]}') + else + fps=$(awk -v r="$rfr" 'BEGIN{printf "%.3f", r+0}') + fi + printf " Size: %s bytes | Duration: %.3fs | Video: %s, %sx%s @ %.3f fps\n" "$size" "${dur:-0}" "$vcodec" "$w" "$h" "$fps" +} + +if have_signature_filter; then + # Use ffmpeg's signature filter (robust against re-encode/resize) + tmpdir=$(mktemp -d) + trap 'rm -rf "$tmpdir"' EXIT + + sig1="$tmpdir/1.sig" + sig2="$tmpdir/2.sig" + log="$tmpdir/compare.log" + + echo "Computing video fingerprints (signature filter)..." + ffmpeg -v error -i "$f1" -vf "signature=format=xml:filename=$sig1" -f null - "$log"; then + : + elif ffmpeg -v info -i "$sig2" -i "$sig1" -filter_complex "signature=compare=1" -f null - 2>>"$log"; then + : + fi + + # Extract a rough similarity ratio (fallback to match count if ratio not printed) + ratio=$(grep -Eo 'similarity[^0-9]*([0-9]+(\.[0-9]+)?)' "$log" | tail -n1 | awk '{print $NF}' || true) + matches=$(grep -Eo 'matches[^0-9]*([0-9]+)' "$log" | awk '{print $NF}' | tail -n1 || true) + + echo + echo "Quick summaries:" + echo "File 1: $f1"; summary "$f1" + echo "File 2: $f2"; summary "$f2" + echo + + if [[ -n "${ratio:-}" ]]; then + echo "Similarity (signature): ${ratio}" + verdict=$(awk -v r="$ratio" 'BEGIN{ if(r+0 >= 0.80) print "Very likely same content"; else if(r+0 >= 0.60) print "Possibly same with edits"; else print "Likely different"; }') + elif [[ -n "${matches:-}" ]]; then + echo "Matching frames (signature): ${matches}" + verdict=$(awk -v m="$matches" 'BEGIN{ if(m+0 >= 50) print "Very likely same content"; else if(m+0 >= 10) print "Possibly same"; else print "Likely different"; }') + else + echo "Could not parse a similarity score from FFmpeg output; check log at: $log" + verdict="Inconclusive (check logs)" + fi + + echo "Verdict: $verdict" + exit 0 +fi + +# ---- Fallback heuristic (no signature filter) ---- +# Sample 1 frame every 10s, normalize to small grayscale, hash each frame, compare overlap. + +echo "Signature filter not available; using frame-sampling heuristic..." +tmpdir=$(mktemp -d) +trap 'rm -rf "$tmpdir"' EXIT + +extract_hashes() { + local f="$1" base="$2" + # One frame every 10s, small grayscale to be robust to resizes/re-encodes + ffmpeg -v error -i "$f" -vf "fps=1/10,scale=160:90,format=gray" -f image2pipe -vcodec png - \ + | sha256sum | awk '{print $1}' > "$tmpdir/${base}.hashes" + # Note: sha256sum on the whole pipe will hash the full stream; we want per-frame hashes. + # If system sha256sum collapses the stream to one hash, fall back to numbered files: + if [[ ! -s "$tmpdir/${base}.hashes" || $(wc -l < "$tmpdir/${base}.hashes") -le 1 ]]; then + rm -f "$tmpdir/${base}"_*.png + ffmpeg -v error -i "$f" -vf "fps=1/10,scale=160:90,format=gray" "$tmpdir/${base}_%05d.png" + ( cd "$tmpdir" && for p in ${base}_*.png; do sha256sum "$p" | awk '{print $1}'; done ) > "$tmpdir/${base}.hashes" + rm -f "$tmpdir/${base}"_*.png + fi +} + +extract_hashes "$f1" A +extract_hashes "$f2" B + +A="$tmpdir/A.hashes"; B="$tmpdir/B.hashes" +countA=$(wc -l < "$A"); countB=$(wc -l < "$B") +common=$(comm -12 <(sort "$A") <(sort "$B") | wc -l) +jaccard=$(awk -v c="$common" -v a="$countA" -v b="$countB" 'BEGIN{u=a+b-c; if(u==0) print 0; else printf "%.3f", c/u}') + +echo +echo "Quick summaries:" +echo "File 1: $f1"; summary "$f1" +echo "File 2: $f2"; summary "$f2" +echo +echo "Frame-sampling similarity:" +echo " Frames hashed: file1=$countA, file2=$countB" +echo " Common hashes: $common" +echo " Jaccard index: $jaccard" + +verdict=$(awk -v j="$jaccard" 'BEGIN{ + if(j+0 >= 0.80) print "Very likely same content"; + else if(j+0 >= 0.50) print "Possibly same content (edits/recodes)"; + else print "Likely different"; +}') +echo "Verdict: $verdict" diff --git a/compare_simple.sh b/compare_simple.sh new file mode 100755 index 0000000..e009e8f --- /dev/null +++ b/compare_simple.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# compare_simple.sh file1.mp4 file2.mp4 +set -euo pipefail + +f1="${1:-}"; f2="${2:-}" +if [[ -z "$f1" || -z "$f2" ]]; then + echo "Usage: $0 " >&2 + exit 2 +fi +for f in "$f1" "$f2"; do + [[ -f "$f" ]] || { echo "Not found: $f" >&2; exit 2; } +done + +probe() { + local f="$1" + # format props + local size dur br + size=$(stat -c%s "$f" 2>/dev/null || stat -f%z "$f") + dur=$(ffprobe -v error -show_entries format=duration -of default=nw=1:nk=1 "$f" || echo "") + br=$(ffprobe -v error -show_entries format=bit_rate -of default=nw=1:nk=1 "$f" || echo "") + + # first video stream + local vcodec w h fps + vcodec=$(ffprobe -v error -select_streams v:0 -show_entries stream=codec_name -of default=nw=1:nk=1 "$f" || echo "") + w=$(ffprobe -v error -select_streams v:0 -show_entries stream=width -of default=nw=1:nk=1 "$f" || echo "") + h=$(ffprobe -v error -select_streams v:0 -show_entries stream=height -of default=nw=1:nk=1 "$f" || echo "") + # r_frame_rate like 24000/1001 → convert to float + local rfr + rfr=$(ffprobe -v error -select_streams v:0 -show_entries stream=r_frame_rate -of default=nw=1:nk=1 "$f" || echo "") + if [[ "$rfr" == */* ]]; then + fps=$(awk -v r="$rfr" 'BEGIN{split(r,a,"/"); if(a[2]==0) print 0; else printf "%.3f", a[1]/a[2]}') + else + fps=$(awk -v r="$rfr" 'BEGIN{printf "%.3f", r+0}') + fi + + # audio summary (codec:channels@rate, semicolon-separated) + local ainfo + ainfo=$(ffprobe -v error -select_streams a -show_entries stream=codec_name,channels,sample_rate -of csv=p=0 "$f" \ + | awk -F, '{printf "%s:%s@%s", $1, $2, $3; if (NR!=0) printf "\n"}' \ + | paste -sd';' - || true) + + echo "$size|$dur|$br|$vcodec|${w}x${h}|$fps|$ainfo" +} + +read -r s1 d1 br1 vc1 res1 fps1 a1 <<<"$(probe "$f1" | tr '|' ' ')" +read -r s2 d2 br2 vc2 res2 fps2 a2 <<<"$(probe "$f2" | tr '|' ' ')" + +# helpers +absdiff() { awk -v a="$1" -v b="$2" 'BEGIN{d=a-b; if(d<0)d=-d; print d}'; } +pctdiff() { awk -v a="$1" -v b="$2" 'BEGIN{if(a==0&&b==0){print 0;exit} m=(a+b)/2; if(m==0){print 100;exit} d=a-b; if(d<0)d=-d; printf "%.2f", (d/m)*100 }'; } + +dur_diff=$(absdiff "${d1:-0}" "${d2:-0}") +fps_diff=$(absdiff "${fps1:-0}" "${fps2:-0}") +size_pct=$(pctdiff "${s1:-0}" "${s2:-0}") + +printf "File 1: %s\n" "$f1" +printf " Size: %s bytes\n Duration: %.3fs\n Video: %s, %s, %.3f fps\n" "$s1" "${d1:-0}" "${vc1:-?}" "${res1:-?}" "${fps1:-0}" +printf " Audio: %s\n" "${a1:-none}" +printf "\n" +printf "File 2: %s\n" "$f2" +printf " Size: %s bytes\n Duration: %.3fs\n Video: %s, %s, %.3f fps\n" "$s2" "${d2:-0}" "${vc2:-?}" "${res2:-?}" "${fps2:-0}" +printf " Audio: %s\n" "${a2:-none}" +printf "\n" + +echo "Differences:" +printf " Size delta: %s%%\n" "$size_pct" +printf " Duration delta: %.3fs\n" "$dur_diff" +printf " FPS delta: %.3f\n" "$fps_diff" +printf " Resolution match: %s\n" "$([[ "$res1" == "$res2" ]] && echo yes || echo no)" +printf " Video codec match: %s\n" "$([[ "$vc1" == "$vc2" ]] && echo yes || echo no)" +printf " Audio tracks match: %s\n" "$([[ "$a1" == "$a2" ]] && echo yes || echo no)" +echo + +# verdict heuristics +likely="Different" +# allow small tolerance on duration/fps +dur_ok=$(awk -v d="$dur_diff" 'BEGIN{print (d<=1.0)?"1":"0"}') +fps_ok=$(awk -v d="$fps_diff" 'BEGIN{print (d<=0.5)?"1":"0"}') + +if [[ "$res1" == "$res2" && "$dur_ok" == "1" && "$fps_ok" == "1" ]]; then + likely="Likely same content (possibly different encode)" +fi +if [[ "$vc1" == "$vc2" && "$a1" == "$a2" && "$size_pct" == "0.00" ]]; then + likely="Identical encode/container" +fi + +echo "Verdict: $likely" diff --git a/compare_snapshot.sh b/compare_snapshot.sh new file mode 100755 index 0000000..f234bbe --- /dev/null +++ b/compare_snapshot.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash +# compare_snapshot.sh (robust v2) +# Compare two MP4s by grabbing a snapshot at a given time and measuring SSIM/PSNR. +# +# Usage: +# ./compare_snapshot.sh [time_seconds] [scale] [--verbose] +# Examples: +# ./compare_snapshot.sh A.mp4 B.mp4 +# ./compare_snapshot.sh A.mp4 B.mp4 12 320:-1 --verbose +# +# Exit codes: 0 = ran ok (doesn't imply "very close"), 2 = usage error, 3 = ffmpeg failure + +set -u # keep -e/-o pipefail off to avoid silent exits from grep/awk mismatches + +f1="${1:-}"; f2="${2:-}" +t="${3:-12}" +scale="${4:-320:-1}" +verbose=0 +[[ "${5:-}" == "--verbose" ]] && verbose=1 + +if [[ -z "${f1}" || -z "${f2}" ]]; then + echo "Usage: $0 [time_seconds] [scale] [--verbose]" >&2 + exit 2 +fi +for f in "$f1" "$f2"; do + [[ -f "$f" ]] || { echo "Not found: $f" >&2; exit 2; } +done + +tmpdir="$(mktemp -d)" +trap 'rm -rf "$tmpdir"' EXIT + +shot1="$tmpdir/shot1.png" +shot2="$tmpdir/shot2.png" +log="$tmpdir/metrics.log" + +# Helper for verbose prints +vprint() { [[ $verbose -eq 1 ]] && echo "[DBG]" "$@" >&2; } + +# 1) Extract frames (accurate seek: -ss after -i) +# Use -y to avoid “file exists” issues, and format to a stable pixel format. +cmd1=(ffmpeg -hide_banner -v error -y -i "$f1" -ss "$t" -frames:v 1 -vf "scale=$scale,format=yuv420p" "$shot1") +cmd2=(ffmpeg -hide_banner -v error -y -i "$f2" -ss "$t" -frames:v 1 -vf "scale=$scale,format=yuv420p" "$shot2") +vprint "${cmd1[@]}" +"${cmd1[@]}" || { echo "Failed to extract snapshot from $f1 at ${t}s." >&2; exit 3; } +vprint "${cmd2[@]}" +"${cmd2[@]}" || { echo "Failed to extract snapshot from $f2 at ${t}s." >&2; exit 3; } + +# Sanity check: files exist and non-empty +if [[ ! -s "$shot1" || ! -s "$shot2" ]]; then + echo "Snapshot extraction produced empty files. Try a different time or scale." >&2 + [[ $verbose -eq 1 ]] && ls -l "$tmpdir" >&2 + exit 3 +fi + +# 2) Compare with SSIM (+ PSNR). Different ffmpeg builds print slightly different lines. +# We send stderr to $log and parse multiple possible formats. +cmp_cmd=(ffmpeg -hide_banner -v info -i "$shot1" -i "$shot2" -lavfi "ssim;[0:v][1:v]psnr" -f null -) +vprint "${cmp_cmd[@]}" +"${cmp_cmd[@]}" > /dev/null 2> "$log" || true + +# Try to parse SSIM "All:" first, fallback to Y channel, then average/mean lines. +ssim_all="$(grep -Eo 'All:[0-9]+(\.[0-9]+)?' "$log" | head -n1 | cut -d: -f2 || true)" +[[ -z "$ssim_all" ]] && ssim_all="$(grep -Eo 'SSIM [^ ]* All:[0-9]+(\.[0-9]+)?' "$log" | awk -F'All:' '{print $2}' | head -n1 || true)" +[[ -z "$ssim_all" ]] && ssim_all="$(grep -Eo 'SSIM Y:[0-9]+(\.[0-9]+)?' "$log" | head -n1 | cut -d: -f2 || true)" + +# Parse PSNR average if present (may appear as "average:xx.xx" or "avg:xx.xx"). +psnr_all="$(grep -Eo 'average:[0-9]+(\.[0-9]+)?' "$log" | head -n1 | cut -d: -f2 || true)" +[[ -z "$psnr_all" ]] && psnr_all="$(grep -Eo 'avg:[0-9]+(\.[0-9]+)?' "$log" | head -n1 | cut -d: -f2 || true)" + +# 3) Print summaries of the source files (optional but handy) +summarize() { + local f="$1" + local dur vinfo fps w h vcodec rfr + dur=$(ffprobe -v error -show_entries format=duration -of default=nw=1:nk=1 "$f" 2>/dev/null || echo "") + vinfo=$(ffprobe -v error -select_streams v:0 -show_entries stream=codec_name,width,height,r_frame_rate -of csv=p=0 "$f" 2>/dev/null | head -n1) + IFS=',' read -r vcodec w h rfr <<<"$vinfo" + if [[ "$rfr" == */* ]]; then + fps=$(awk -v r="$rfr" 'BEGIN{split(r,a,"/"); if(a[2]==0) print 0; else printf "%.3f", a[1]/a[2]}') + else + fps=$(awk -v r="$rfr" 'BEGIN{printf "%.3f", r+0}') + fi + printf " Duration: %.3fs | Video: %s, %sx%s @ %s fps\n" "${dur:-0}" "${vcodec:-?}" "${w:-?}" "${h:-?}" "${fps:-?}" +} + +echo "Snapshot comparison at t=${t}s (scaled to ${scale}):" +echo "File 1: $f1"; summarize "$f1" +echo "File 2: $f2"; summarize "$f2" +echo + +# 4) Handle cases where parsing failed +if [[ -z "$ssim_all" && -z "$psnr_all" ]]; then + echo "Could not parse SSIM/PSNR from ffmpeg output." + [[ $verbose -eq 1 ]] && { echo "--- ffmpeg log ---"; cat "$log"; echo "-------------------"; } + echo "Tips: try a different time (e.g. 5 or 30), or a different scale (e.g. 640:-1)." + exit 0 +fi + +# 5) Report metrics and give a verdict +printf "Metrics:\n" +[[ -n "$ssim_all" ]] && printf " SSIM (All): %s (1.00 = identical)\n" "$ssim_all" +[[ -n "$psnr_all" ]] && printf " PSNR avg: %s dB\n" "$psnr_all" +echo + +# Verdict from SSIM (fallback to PSNR if SSIM missing) +verdict="Different" +if [[ -n "$ssim_all" ]]; then + verdict=$(awk -v s="$ssim_all" 'BEGIN{ + if (s+0 >= 0.97) print "Very close (near-identical)"; + else if (s+0 >= 0.90) print "Close"; + else print "Different"; + }') +elif [[ -n "$psnr_all" ]]; then + verdict=$(awk -v p="$psnr_all" 'BEGIN{ + if (p+0 >= 40) print "Very close (near-identical)"; + else if (p+0 >= 30) print "Close"; + else print "Different"; + }') +fi +echo "Verdict: $verdict" + +# Optional: show where log is if verbose +[[ $verbose -eq 1 ]] && { echo "(Log at: $log)"; } diff --git a/count_words.sh b/count_words.sh new file mode 100755 index 0000000..cc5b9d6 --- /dev/null +++ b/count_words.sh @@ -0,0 +1,183 @@ +#!/usr/bin/env bash +set -uo pipefail +# count_words.sh — Räknar underscore-separerade ord i filnamn +# Version: 1.3.0 + +VERSION="1.3.0" + +# Usage: +# ./count_words.sh [katalog] [--to-file] [--ext "mp4,avi,mkv"] [--version] +# +# Exempel: +# ./count_words.sh . +# ./count_words.sh /path --to-file +# ./count_words.sh /path --ext "mp4,avi" --to-file +# ./count_words.sh --version + +DIR="." +TO_FILE=false +OUTFILE="reserved__words.txt" +EXTS=("mp4") # default + +print_help() { + cat <&2 + print_help + exit 1 + ;; + esac +done + +# Bygg lookup för tillåtna ändelser (lowercase) +declare -A ALLOWED_EXT=() +for e in "${EXTS[@]}"; do + [[ -n "$e" ]] || continue + ALLOWED_EXT["${e,,}"]=1 +done + +declare -A counts=() + +shopt -s nullglob +for f in "$DIR"/*; do + [[ -f "$f" ]] || continue + name=$(basename "$f") + + # Filtrera på tillåtna ändelser + ext="${name##*.}" + if [[ "$name" == "$ext" ]]; then + continue # ingen ändelse + fi + ext_lc="${ext,,}" + [[ ${ALLOWED_EXT[$ext_lc]+_} ]] || continue + + # Ta bort ändelsen + base="${name%.*}" + + # TA BORT WUNF-PREFIX: [WUNF] eller [WUNF_ddd] var som helst i namnet + # (behåll allt annat) +base_noprefix=$(sed -E 's/\[WUNF(_[0-9]{1,3})?\]//g' <<< "$base") +# base_noprefix=$(sed -E 's/\[WUNF(_[0-9]{3})?\]//g' <<< "$base") + + # Splitta på '_' och räkna (case-insensitive) + oldIFS=$IFS + IFS='_' + read -r -a parts <<< "$base_noprefix" + IFS=$oldIFS + + for word in "${parts[@]}"; do + # Trimma whitespace + w="$word" + w="${w#"${w%%[![:space:]]*}"}" # trim left + w="${w%"${w##*[![:space:]]}"}" # trim right + [[ -n "$w" ]] || continue + + # Normalisera till lowercase + w_lc="${w,,}" + + # Initiera och öka + : "${counts[$w_lc]:=0}" + (( counts[$w_lc]++ )) + done +done + +# Samla ord med förekomst > 2 +results=() +for w in "${!counts[@]}"; do + if (( counts[$w] > 2 )); then + results+=("${counts[$w]} $w") + fi +done + +# Ingen träff? +if [[ ${#results[@]} -eq 0 ]]; then + if $TO_FILE; then + if [[ -f "$OUTFILE" ]]; then + echo "Inga nya ord (>2) hittades. Filen lämnades oförändrad. (v$VERSION)" + else + : > "$OUTFILE" + echo "Inga ord (>2) hittades. Tom fil skapad: $OUTFILE (v$VERSION)" + fi + else + echo "Inga ord förekom mer än två gånger. (v$VERSION)" + fi + exit 0 +fi + +# Sortera: primärt på antal (fallande), sekundärt på ord (stigande) +sorted_lines=$(printf "%s\n" "${results[@]}" | sort -nrk1,1 -k2,2) + +if $TO_FILE; then + # Nya ord (lowercase redan), en per rad + new_words=$(printf "%s\n" "$sorted_lines" | awk '{print $2}') + + # Läs befintliga ord från OUTFILE (om den finns) och bygg union utan dubbletter + declare -A uniq=() + if [[ -f "$OUTFILE" ]]; then + # Tolka både kommatecken och whitespace som separerare + while IFS= read -r tok; do + w="$tok" + # trim + w="${w#"${w%%[![:space:]]*}"}" + w="${w%"${w##*[![:space:]]}"}" + [[ -n "$w" ]] || continue + uniq["${w,,}"]=1 + done < <(tr -s ',[:space:]' '\n' < "$OUTFILE") + fi + + # Lägg till nya ord + while IFS= read -r nw; do + [[ -n "$nw" ]] || continue + uniq["$nw"]=1 + done <<< "$new_words" + + # Bygg kommaseparerad sträng (alfabetiskt för determinism) + all_words=$(printf "%s\n" "${!uniq[@]}" | sort -u | paste -sd, -) + + # Skriv tillbaka hela unionen (utan dubbletter) + echo "$all_words" > "$OUTFILE" + echo "Resultat uppdaterat i $OUTFILE (v$VERSION)" +else + echo "Ord som förekommit mer än två gånger (sorterat högst->lägst) — v$VERSION:" + while read -r count word; do + echo "$word ($count ggr)" + done <<< "$sorted_lines" +fi diff --git a/count_words.sh.save b/count_words.sh.save new file mode 100755 index 0000000..a8ef521 --- /dev/null +++ b/count_words.sh.save @@ -0,0 +1,182 @@ +#!/usr/bin/env bash +set -uo pipefail +# count_words.sh — Räknar underscore-separerade ord i filnamn +# Version: 1.3.0 + +VERSION="1.3.1" + +# Usage: +# ./count_words.sh [katalog] [--to-file] [--ext "mp4,avi,mkv"] [--version] +# +# Exempel: +# ./count_words.sh . +# ./count_words.sh /path --to-file +# ./count_words.sh /path --ext "mp4,avi" --to-file +# ./count_words.sh --version + +DIR="." +TO_FILE=false +OUTFILE="reserved__words.txt" +EXTS=("mp4") # default + +print_help() { + cat <&2 + print_help + exit 1 + ;; + esac +done + +# Bygg lookup för tillåtna ändelser (lowercase) +declare -A ALLOWED_EXT=() +for e in "${EXTS[@]}"; do + [[ -n "$e" ]] || continue + ALLOWED_EXT["${e,,}"]=1 +done + +declare -A counts=() + +shopt -s nullglob +for f in "$DIR"/*; do + [[ -f "$f" ]] || continue + name=$(basename "$f") + + # Filtrera på tillåtna ändelser + ext="${name##*.}" + if [[ "$name" == "$ext" ]]; then + continue # ingen ändelse + fi + ext_lc="${ext,,}" + [[ ${ALLOWED_EXT[$ext_lc]+_} ]] || continue + + # Ta bort ändelsen + base="${name%.*}" + + # TA BORT WUNF-PREFIX: [WUNF] eller [WUNF_ddd] var som helst i namnet + # (behåll allt annat) + base_noprefix=$(sed -E 's/\[WUNF(_[0-9]{3})?\]//g' <<< "$base") + + # Splitta på '_' och räkna (case-insensitive) + oldIFS=$IFS + IFS='_' + read -r -a parts <<< "$base_noprefix" + IFS=$oldIFS + + for word in "${parts[@]}"; do + # Trimma whitespace + w="$word" + w="${w#"${w%%[![:space:]]*}"}" # trim left + w="${w%"${w##*[![:space:]]}"}" # trim right + [[ -n "$w" ]] || continue + + # Normalisera till lowercase + w_lc="${w,,}" + + # Initiera och öka + : "${counts[$w_lc]:=0}" + (( counts[$w_lc]++ )) + done +done + +# Samla ord med förekomst > 2 +results=() +for w in "${!counts[@]}"; do + if (( counts[$w] > 2 )); then + results+=("${counts[$w]} $w") + fi +done + +# Ingen träff? +if [[ ${#results[@]} -eq 0 ]]; then + if $TO_FILE; then + if [[ -f "$OUTFILE" ]]; then + echo "Inga nya ord (>2) hittades. Filen lämnades oförändrad. (v$VERSION)" + else + : > "$OUTFILE" + echo "Inga ord (>2) hittades. Tom fil skapad: $OUTFILE (v$VERSION)" + fi + else + echo "Inga ord förekom mer än två gånger. (v$VERSION)" + fi + exit 0 +fi + +# Sortera: primärt på antal (fallande), sekundärt på ord (stigande) +sorted_lines=$(printf "%s\n" "${results[@]}" | sort -nrk1,1 -k2,2) + +if $TO_FILE; then + # Nya ord (lowercase redan), en per rad + new_words=$(printf "%s\n" "$sorted_lines" | awk '{print $2}') + + # Läs befintliga ord från OUTFILE (om den finns) och bygg union utan dubbletter + declare -A uniq=() + if [[ -f "$OUTFILE" ]]; then + # Tolka både kommatecken och whitespace som separerare + while IFS= read -r tok; do + w="$tok" + # trim + w="${w#"${w%%[![:space:]]*}"}" + w="${w%"${w##*[![:space:]]}"}" + [[ -n "$w" ]] || continue + uniq["${w,,}"]=1 + done < <(tr -s ',[:space:]' '\n' < "$OUTFILE") + fi + + # Lägg till nya ord + while IFS= read -r nw; do + [[ -n "$nw" ]] || continue + uniq["$nw"]=1 + done <<< "$new_words" + + # Bygg kommaseparerad sträng (alfabetiskt för determinism) + all_words=$(printf "%s\n" "${!uniq[@]}" | sort -u | paste -sd, -) + + # Skriv tillbaka hela unionen (utan dubbletter) + echo "$all_words" > "$OUTFILE" + echo "Resultat uppdaterat i $OUTFILE (v$VERSION)" +else + echo "Ord som förekommit mer än två gånger (sorterat högst->lägst) — v$VERSION:" + while read -r count word; do + echo "$word ($count ggr)" + done <<< "$sorted_lines" +fi diff --git a/detect_wunf.sh b/detect_wunf.sh new file mode 100755 index 0000000..2d42789 --- /dev/null +++ b/detect_wunf.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +# detect_pink_intro_hsv.sh [duration_s] [fps] [hue_min] [hue_max] [sat_min] [val_min] [min_ratio] +# +# Detect a magenta/pink intro by averaging frames to 1x1 and classifying in HSV. +# +# Defaults: +# duration_s = 1.0 +# fps = 6 +# hue_min = 285 # degrees, magenta band start +# hue_max = 330 # degrees, magenta band end +# sat_min = 0.35 # require some saturation +# val_min = 0.35 # avoid very dark frames +# min_ratio = 0.6 # % of sampled frames that must be pink-ish +# +# Exit codes: 0 detected, 1 not detected, 2 usage error + +set -euo pipefail + +file="${1:-}" +duration="${2:-1.0}" +sample_fps="${3:-6}" +HMIN="${4:-285}" +HMAX="${5:-330}" +SMIN="${6:-0.35}" +VMIN="${7:-0.35}" +MIN_RATIO="${8:-0.6}" + +if [[ -z "$file" ]]; then + echo "Usage: $0 [duration_s] [fps] [hue_min] [hue_max] [sat_min] [val_min] [min_ratio]" >&2 + exit 2 +fi +[[ -f "$file" ]] || { echo "File not found: $file" >&2; exit 2; } + +# Stream 1x1 RGB for sampled frames; each frame = 3 bytes. Parse with od+awk. +summary="$( + LC_ALL=C ffmpeg -hide_banner -v error \ + -i "$file" -t "$duration" \ + -vf "fps=${sample_fps},scale=1:1:flags=area,format=rgb24" \ + -f rawvideo - 2>/dev/null \ + | od -An -tu1 -w3 \ + | awk -v HMIN="$HMIN" -v HMAX="$HMAX" -v SMIN="$SMIN" -v VMIN="$VMIN" ' + function max(a,b){return a>b?a:b} + function min(a,b){return aM) M=g; if(b>M) M=b; + m=r; if(g=HMIN && H<=HMAX && S>=SMIN && V>=VMIN) pink++; + # save last metrics to show a couple of examples (optional) + if (total<=10) printf "DBG frame%02d: RGB=(%3d,%3d,%3d) HSV=(%6.1f°, %4.2f, %4.2f)\n", total,R,G,B,H,S,V > "/dev/stderr" + } + END { + if (total==0) { printf "TOTAL=0 PINK=0 RATIO=0\n"; exit; } + printf "TOTAL=%d PINK=%d RATIO=%.3f\n", total, pink, pink/total; + }' +)" + +if ! printf "%s" "$summary" | grep -q 'TOTAL='; then + echo "No frames parsed (clip too short or pipeline issue)." + exit 1 +fi + +eval "$summary" # sets TOTAL,PINK,RATIO +echo "File: $file" +echo "Analyzed: first ${duration}s @ ${sample_fps} fps → frames: ${TOTAL}" +echo "Pink-ish frames (hue in ${HMIN}-${HMAX}°, S>=${SMIN}, V>=${VMIN}): ${PINK}" +echo "Ratio: ${RATIO} (threshold: ${MIN_RATIO})" + +awk -v r="$RATIO" -v thr="$MIN_RATIO" 'BEGIN{ exit !(r+0 >= thr+0) }' \ + && { echo "DETECTED: Pink intro present"; exit 0; } +echo "Not detected"; exit 1 + diff --git a/mvi.sh b/mvi.sh new file mode 100755 index 0000000..91cd597 --- /dev/null +++ b/mvi.sh @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'EOF' +Användning: + mvi KÄLLA MÅL + mvi KÄLLA1 KÄLLA2 ... KATALOG + +Flyttar som 'mv', men om en målfil redan finns så läggs ett index in +före filändelsen, t.ex. "fil.txt" -> "fil.1.txt", "fil.2.txt", ... + +Exempel: + mvi bild.jpg bilder/ # bilder/bild.jpg eller bilder/bild.1.jpg + mvi .env prod/ # prod/.env eller prod/.env.1 + mvi projekt/ arkiv/ # arkiv/projekt eller arkiv/projekt.1 +EOF +} + +# Ta reda på om sista argumentet är en katalog (målet när flera källor) +is_dir() { + local p=$1 + [[ -d "$p" ]] +} + +# Skapa ett indexerat fil-/katalognamn om target redan finns. +# Regler: +# - index (".N") injiceras före sista filändelsen +# - för dolda filer utan ändelse (t.ex. ".env"): ".env.1" +# - för flera punkter (t.ex. "fil.tar.gz"): "fil.tar.1.gz" +indexed_path() { + local target="$1" + local dir base name ext n candidate + + dir=$(dirname -- "$target") + name=$(basename -- "$target") + + # Dela upp name i bas + ev. ext (sista punkten räknas som extension-separator) + if [[ "$name" = .* && "$name" != *.* ]]; then + # Dold fil utan extension (".env") + base="$name" + ext="" + elif [[ "$name" = *.* ]]; then + base="${name%.*}" + ext=".${name##*.}" + else + base="$name" + ext="" + fi + + n=1 + while :; do + candidate="${dir}/${base}.${n}${ext}" + [[ ! -e "$candidate" ]] && { printf '%s\n' "$candidate"; return 0; } + n=$((n+1)) + done +} + +# Flytta en enskild källa till ett givet mål (fil eller katalog) +move_one() { + local src="$1" + local dst="$2" + local target + + if [[ -d "$dst" ]]; then + target="${dst%/}/$(basename -- "$src")" + else + target="$dst" + fi + + if [[ -e "$target" ]]; then + target=$(indexed_path "$target") + fi + + # Skapa målkatalog om den inte finns (som mv gör med -t dir om den finns) + mkdir -p -- "$(dirname -- "$target")" + + mv -- "$src" "$target" +} + +# --- Huvudprogram --- +if [[ $# -lt 2 ]]; then + usage + exit 1 +fi + +# Sista arg är destination +dest="${@: -1}" +sources=("${@:1:$(($#-1))}") + +# Om flera källor: dest måste vara katalog +if [[ ${#sources[@]} -gt 1 ]]; then + if ! is_dir "$dest"; then + echo "Fel: Flera källor kräver att destinationen är en katalog." >&2 + exit 2 + fi +fi + +# Iterera över alla källor +for src in "${sources[@]}"; do + if [[ ! -e "$src" ]]; then + echo "Varning: källan finns inte: $src" >&2 + continue + fi + move_one "$src" "$dest" +done diff --git a/rename_and_filter.sh b/rename_and_filter.sh new file mode 100755 index 0000000..be1d3e6 --- /dev/null +++ b/rename_and_filter.sh @@ -0,0 +1,266 @@ +#!/usr/bin/env bash +# ============================================================================= +# rename_and_filter.sh +# +# Version: 1.3.1 +# Last updated: 2025-10-04 +# +# Summary +# ------- +# Given a directory, a comma-separated file of reserved words, and an optional +# file extension filter: +# 1) Removes any filename tokens (separated by "_") that match reserved words +# (case-insensitive, exact token matches). +# - EXTRA: If a reserved word appears as the last token (e.g., "..._good"), +# it is removed even if it's only delimited on the left side. +# 2) Replaces one or more spaces with a single underscore "_". +# 3) Collapses multiple "_" into a single "_", and trims leading/trailing "_". +# +# Notes: +# - No files are deleted; only renaming occurs. +# - Processes files only in the given directory (non-recursive). +# - Collision handling: if target name exists, suffix "_N" before the extension. +# - Reserved words match only against the basename tokens (extension excluded). +# +# Extension filter: +# - Default: mp4 +# - "*" : all files +# - "jpg" : only .jpg (with or without leading dot accepted, e.g., "jpg" or ".jpg") +# +# Logging: +# RENAME: "old_name.ext" => "new_name.ext" +# NOCHANGE (dry-run): "file.ext" (nothing to do) +# +# Usage +# ----- +# ./rename_and_filter.sh [--dry-run] [extension] +# +# Examples +# -------- +# # Dry-run on mp4 files (default): +# ./rename_and_filter.sh --dry-run ./videos ./reserved_words.csv +# +# # All files: +# ./rename_and_filter.sh ./media ./reserved_words.csv "*" +# +# # Only jpg: +# ./rename_and_filter.sh ./pictures ./reserved_words.csv jpg +# +# Exit codes +# ---------- +# 0 Success +# 1 Invalid arguments / missing files +# 2 Requires bash >= 4 +# ============================================================================= + +set -euo pipefail + +# Requires bash 4 for associative arrays +if [ "${BASH_VERSINFO:-0}" -lt 4 ]; then + echo "This script requires bash >= 4." >&2 + exit 2 +fi + +DRY_RUN=0 + +print_help() { + sed -n '2,160p' "$0" | sed 's/^# \{0,1\}//' +} + +log() { + printf '[%s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" +} + +rename_file() { + local src="$1" + local dst="$2" + + # Guard: identical path, nothing to do + if [[ "$src" == "$dst" ]]; then + if (( DRY_RUN )); then + log "NOCHANGE (dry-run): \"$(basename "$src")\" (nothing to do)" + fi + return 0 + fi + + local base ext candidate n + base="${dst##*/}" + ext="" + if [[ "$base" == *.* ]]; then + ext=".${base##*.}" + base="${base%.*}" + fi + candidate="$dst" + n=1 + # Avoid collisions (unless it's the same file, already handled above) + while [[ -e "$candidate" && "$src" != "$candidate" ]]; do + candidate="$(dirname "$dst")/${base}_$n$ext" + ((n++)) + done + + if (( DRY_RUN )); then + log "RENAME: \"$(basename "$src")\" => \"$(basename "$candidate")\"" + else + mv -v -- "$src" "$candidate" >/dev/null + log "RENAME: \"$(basename "$src")\" => \"$(basename "$candidate")\"" + fi +} + +# --- Argument parsing --- +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + print_help + exit 0 +fi + +if [[ "${1:-}" == "-n" || "${1:-}" == "--dry-run" ]]; then + DRY_RUN=1 + shift +fi + +if [[ $# -lt 2 || $# -gt 3 ]]; then + echo "Error: Provide [extension]. Use --help for info." >&2 + exit 1 +fi + +DIR="$1" +WORD_FILE="$2" +EXT="${3:-mp4}" + +# Normalize EXT: strip leading dot if present +if [[ "$EXT" == .* ]]; then + EXT="${EXT#.}" +fi + +if [[ ! -d "$DIR" ]]; then + echo "Error: Directory does not exist: $DIR" >&2 + exit 1 +fi +if [[ ! -f "$WORD_FILE" ]]; then + echo "Error: Reserved words file does not exist: $WORD_FILE" >&2 + exit 1 +fi + +# --- Load reserved words into an associative set --- +declare -A RESERVED=() +# Allow comma and newlines as separators +# shellcheck disable=SC2002 +mapfile -t _tokens < <(cat "$WORD_FILE" | tr ',\r\n' '\n') +for raw in "${_tokens[@]}"; do + # trim whitespace + w="${raw#"${raw%%[![:space:]]*}"}" # ltrim + w="${w%"${w##*[![:space:]]}"}" # rtrim + [[ -z "$w" ]] && continue + RESERVED["${w,,}"]=1 +done + +log "Processing directory: $DIR (dry-run=$DRY_RUN, extension=${EXT})" +log "Reserved words loaded: ${#RESERVED[@]}" + +# --- Normalize (rules 2 & 3) --- +normalize_name() { + local name="$1" + # Replace one-or-more spaces with single underscore + name="$(printf '%s' "$name" | sed -E 's/[[:space:]]+/_/g')" + # Collapse multiple underscores + name="$(printf '%s' "$name" | sed -E 's/_+/_/g')" + # Trim leading/trailing underscores from the basename (keep extension) + local base ext + base="$name" + ext="" + if [[ "$base" == *.* ]]; then + ext=".${base##*.}" + base="${base%.*}" + fi + base="${base##_}" + base="${base%%_}" + printf '%s%s' "$base" "$ext" +} + +# --- Remove reserved tokens from basename (plus end-token rule) --- +strip_reserved() { + local name="$1" + local base ext token lw + local -a parts new_parts=() + + base="$(basename "$name")" + ext="" + if [[ "$base" == *.* ]]; then + ext=".${base##*.}" + base="${base%.*}" + fi + + # Pre-normalize for tokenization + local norm_base + norm_base="$(printf '%s' "$base" | sed -E 's/[[:space:]]+/_/g' | sed -E 's/_+/_/g')" + + IFS='_' read -r -a parts <<< "$norm_base" + for token in "${parts[@]}"; do + [[ -z "$token" ]] && continue + lw="${token,,}" + if [[ -n "${RESERVED[$lw]:-}" ]]; then + continue + fi + new_parts+=("$token") + done + + local new_base + if (( ${#new_parts[@]} == 0 )); then + new_base="" + else + new_base="$(IFS=_; echo "${new_parts[*]}")" + fi + + # Extra end-token rule (defensive): if last token is reserved, drop it + if [[ -n "$new_base" ]]; then + local last last_lc + last="${new_base##*_}" + last_lc="${last,,}" + if [[ -n "${RESERVED[$last_lc]:-}" ]]; then + if [[ "$new_base" == *_* ]]; then + new_base="${new_base%_*}" + else + new_base="" + fi + fi + fi + + # Fallback if everything disappeared + if [[ -z "$new_base" ]]; then + new_base="untitled" + fi + + printf '%s%s' "$new_base" "$ext" +} + +# --- File pattern based on EXT --- +shopt -s nullglob +pattern="*" +if [[ "$EXT" != "*" ]]; then + pattern="*.$EXT" +fi + +# --- Main loop --- +for path in "$DIR"/$pattern; do + [[ -f "$path" ]] || continue + + orig_basename="$(basename "$path")" + + # 1) remove reserved tokens from basename + stripped="$(strip_reserved "$orig_basename")" + + # 2 & 3) normalize (spaces -> "_", collapse "_", trim) + new_name="$(normalize_name "$stripped")" + + # NEW: If nothing changed, do not attempt to rename at all + if [[ "$new_name" == "$orig_basename" ]]; then + if (( DRY_RUN )); then + log "NOCHANGE (dry-run): \"${orig_basename}\" (nothing to do)" + fi + continue + fi + + target="$(dirname "$path")/$new_name" + rename_file "$path" "$target" +done + +log "Done." diff --git a/rename_space.sh b/rename_space.sh new file mode 100755 index 0000000..067de7f --- /dev/null +++ b/rename_space.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +# Standardvärden +DIR="." +DRY_RUN=false + +# Läs argument +for arg in "$@"; do + case "$arg" in + --dry-run) + DRY_RUN=true + ;; + *) + DIR="$arg" + ;; + esac +done + +# Gå igenom alla filer i katalogen rekursivt +find "$DIR" -depth -type f | while read -r file; do + dir=$(dirname "$file") + base=$(basename "$file") + + # Byt ut mellanslag mot underscore + newbase=$(echo "$base" | tr ' ' '_') + + # Byt ut flera underscores mot en enda + newbase=$(echo "$newbase" | sed 's/_\+/_/g') + + # Om namnet har ändrats + if [ "$base" != "$newbase" ]; then + newpath="$dir/$newbase" + if [ "$DRY_RUN" = true ]; then + echo "[Dry-run] $file -> $newpath" + else + echo "Renaming: $file -> $newpath" + mv -i "$file" "$newpath" + fi + fi +done diff --git a/run_wunf.sh b/run_wunf.sh new file mode 100755 index 0000000..b62a4d9 --- /dev/null +++ b/run_wunf.sh @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Hitta scriptets egen katalog +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Standardvärden +DIR="." # <-- katalogen man står i +PREFIX="[WUNF]_" +CSV="wunf_scenes.csv" +EXT="mp4" +LOG_DIR="${HOME}/log" +LOG_FILE="${LOG_DIR}/run_wunf.log" +SCAN_SCRIPT="${SCRIPT_DIR}/scan_and_prefix_pink.sh" +UPDATE_SCRIPT="${SCRIPT_DIR}/wunf_upd_prefix.sh" + +# Skapa loggmapp om den inte finns +mkdir -p "$LOG_DIR" + +# Starta loggning: både stdout och stderr till loggfil + terminal +exec > >(tee -a "$LOG_FILE") 2>&1 + +echo "============================================================" +echo " Körning startad: $(date)" +echo " Script: $0" +echo " Loggfil: $LOG_FILE" +echo "============================================================" + +usage() { + cat < Katalog att skanna (default: .) + -p, --prefix Prefix att sätta (default: [WUNF]_) + -c, --csv CSV-fil att använda (default: wunf_scenes.csv) + -e, --ext Filändelse att matcha (default: mp4) + -h, --help Visa denna hjälp + +Exempel: + $(basename "$0") --dir . --prefix "[WUNF]_" +EOF +} + +# --- Parse args --- +while [[ $# -gt 0 ]]; do + case "$1" in + -d|--dir) DIR="${2:-}"; shift 2 ;; + -p|--prefix) PREFIX="${2:-}"; shift 2 ;; + -c|--csv) CSV="${2:-}"; shift 2 ;; + -e|--ext) EXT="${2:-}"; shift 2 ;; + -h|--help) usage; exit 0 ;; + *) echo "Okänd flagga: $1"; usage; exit 1 ;; + esac +done + +# --- Kontroller --- +[[ -d "$DIR" ]] || { echo "Fel: katalogen finns ej: $DIR" >&2; exit 1; } +[[ -x "$SCAN_SCRIPT" ]] || { echo "Fel: saknar körbart $SCAN_SCRIPT" >&2; exit 1; } +[[ -x "$UPDATE_SCRIPT" ]] || { echo "Fel: saknar körbart $UPDATE_SCRIPT" >&2; exit 1; } + +# --- Cleanup-hantering --- +LIST_FILE="$(mktemp -t wunf_filenames.XXXXXX.txt)" +cleanup() { + rm -f "$LIST_FILE" + echo "Tillfällig fil raderad: $LIST_FILE" + echo "Körning avslutad: $(date)" + echo "============================================================" +} +trap cleanup EXIT INT TERM + +echo "==> Steg 1: Scannar och sätter prefix (${PREFIX}) i ${DIR}" +"$SCAN_SCRIPT" "$DIR" --prefix "$PREFIX" + +echo "==> Steg 2: Skapar fil-lista för filer med prefix (${PREFIX}) och .${EXT}" +escaped_prefix_for_glob="$(printf '%s' "${PREFIX}" | sed -e 's/\[/\\[/g' -e 's/]/\\]/g')" +pattern="${escaped_prefix_for_glob}*.${EXT}" + +pushd "$DIR" > /dev/null +# shellcheck disable=SC2086 +ls -1d --quoting-style=literal $pattern > "$LIST_FILE" 2>/dev/null || true +popd > /dev/null + +if [[ ! -s "$LIST_FILE" ]]; then + echo "Inga filer matchade '${PREFIX}*.${EXT}' i ${DIR}." >&2 + exit 2 +fi + +echo "==> Hittade $(wc -l < "$LIST_FILE" | tr -d ' ') filer. Kör uppdateringsscriptet…" +"$UPDATE_SCRIPT" --list "$LIST_FILE" "$CSV" + +echo "==> Klart!" diff --git a/sanitycheck.sh b/sanitycheck.sh new file mode 100755 index 0000000..4c9afa8 --- /dev/null +++ b/sanitycheck.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# +# sanitycheck.sh +# +# Returns: "OK" if file looks good, "Not OK" otherwise. + +file="$1" + +if [ -z "$file" ]; then + echo "Usage: $0 " + exit 2 +fi + +if [ ! -f "$file" ]; then + echo "Not OK (file not found)" + exit 1 +fi + +# --- Test 1: metadata sanity check --- +ffprobe -v error -select_streams v:0 -show_entries stream=width,height,duration \ + -of csv=p=0 "$file" > /dev/null 2>&1 +probe_status=$? + +# --- Test 2: deep read/decode --- +ffmpeg -v error -xerror -i "$file" -f null - -nostats > /dev/null 2>&1 +decode_status=$? + +if [ $probe_status -eq 0 ] && [ $decode_status -eq 0 ]; then + echo "OK" + exit 0 +else + echo "Not OK" + exit 1 +fi diff --git a/scan_and_prefix_pink.md b/scan_and_prefix_pink.md new file mode 100644 index 0000000..9629e94 --- /dev/null +++ b/scan_and_prefix_pink.md @@ -0,0 +1,30 @@ +Examples + +Basic (non-recursive): + +chmod +x scan_and_prefix_pink.sh +./scan_and_prefix_pink.sh /path/to/videos + + +Recursive + dry-run + custom prefix: + +./scan_and_prefix_pink.sh /path/to/videos --recursive --dry-run --prefix INTRO_ + + +Stricter detection (wider window, brighter requirement, higher ratio): + +./scan_and_prefix_pink.sh /path/to/videos \ + --duration 1.2 --fps 6 --hue-min 280 --hue-max 335 \ + --sat-min 0.40 --val-min 0.45 --min-ratio 0.7 + +Notes + +Collision-safe: if the target name exists, the script appends .1, .2, … to the new filename. + +Idempotent: already-prefixed files are skipped. + +Output: one line per file with a clear status: + +RENAMED, NOT_MATCH, SKIPPED, or ERROR (with a short reason). + +You can tweak the HSV thresholds if your intro varies (e.g., broader hue band 270–340 or lower sat-min for paler pink). diff --git a/scan_and_prefix_pink.sh b/scan_and_prefix_pink.sh new file mode 100755 index 0000000..8440673 --- /dev/null +++ b/scan_and_prefix_pink.sh @@ -0,0 +1,152 @@ +#!/usr/bin/env bash +# scan_and_prefix_pink.sh +# +# Scan a directory for MP4 files, detect a pink/magenta intro, +# and prefix matching files (unless already prefixed). +# +# Options: +# --dry-run, --recursive/-r, --prefix PREFIX, etc. (same as before) + +set -euo pipefail + +# ---- Defaults ---- +DRY_RUN=0 +RECURSIVE=0 +PREFIX="PINK_" +DURATION="1.0" +FPS="6" +HUE_MIN="285" +HUE_MAX="330" +SAT_MIN="0.35" +VAL_MIN="0.35" +MIN_RATIO="0.6" + +# ---- Parse args ---- +if [[ $# -lt 1 ]]; then + echo "Usage: $0 [--dry-run] [--recursive|-r] [--prefix PREFIX] ..." >&2 + exit 2 +fi + +DIR="$1"; shift || true +[[ -d "$DIR" ]] || { echo "Not a directory: $DIR" >&2; exit 2; } + +while [[ $# -gt 0 ]]; do + case "$1" in + --dry-run) DRY_RUN=1; shift ;; + --recursive|-r) RECURSIVE=1; shift ;; + --prefix) PREFIX="${2:-}"; shift 2 ;; + --duration) DURATION="${2:-}"; shift 2 ;; + --fps) FPS="${2:-}"; shift 2 ;; + --hue-min) HUE_MIN="${2:-}"; shift 2 ;; + --hue-max) HUE_MAX="${2:-}"; shift 2 ;; + --sat-min) SAT_MIN="${2:-}"; shift 2 ;; + --val-min) VAL_MIN="${2:-}"; shift 2 ;; + --min-ratio) MIN_RATIO="${2:-}"; shift 2 ;; + --help|-h) + grep -E '^# ' "$0" | sed 's/^# //'; exit 0 ;; + *) echo "Unknown option: $1" >&2; exit 2 ;; + esac +done + +# ---- Helpers ---- +print_result() { # $1=status $2=file $3=detail + printf "%-10s %s%s\n" "$1" "$2" "${3:+ ($3)}" +} + +safe_rename() { # $1=path $2=prefix + local path="$1" pre="$2" + local dir base target + dir=$(dirname -- "$path") + base=$(basename -- "$path") + target="${dir}/${pre}${base}" + + # Avoid collisions + if [[ -e "$target" ]]; then + local n=1 + while [[ -e "${target}.${n}" ]]; do n=$((n+1)); done + target="${target}.${n}" + fi + + if [[ $DRY_RUN -eq 1 ]]; then + print_result "RENAMED" "$path" "dry-run → $(basename -- "$target")" + else + if mv -- "$path" "$target"; then + print_result "RENAMED" "$path" "→ $(basename -- "$target")" + else + print_result "ERROR" "$path" "rename failed" + fi + fi +} + +detect_pink() { # $1=file -> echoes "total pink ratio" + local file="$1" + LC_ALL=C ffmpeg -hide_banner -v error \ + -i "$file" -t "$DURATION" \ + -vf "fps=${FPS},scale=1:1:flags=area,format=rgb24" \ + -f rawvideo - 2>/dev/null \ + | od -An -tu1 -w3 \ + | awk -v HMIN="$HUE_MIN" -v HMAX="$HUE_MAX" -v SMIN="$SAT_MIN" -v VMIN="$VAL_MIN" ' + function fmod(a,b){ return a - int(a/b)*b } + function rgb2hsv(R,G,B, r,g,b,M,m,d,H,S,V) { + r=R/255.0; g=G/255.0; b=B/255.0; + M=r; if(g>M) M=g; if(b>M) M=b; + m=r; if(g=HMIN && H<=HMAX && S>=SMIN && V>=VMIN) pink++; + } + END { + if (total==0) { print "0 0 0"; exit; } + printf "%d %d %.3f\n", total, pink, pink/total; + }' +} + +is_mp4() { case "$1" in *.mp4|*.MP4) return 0;; *) return 1;; esac; } + +# ---- File list ---- +if [[ $RECURSIVE -eq 1 ]]; then + mapfile -t FILES < <(find "$DIR" -type f \( -iname '*.mp4' \)) +else + mapfile -t FILES < <(ls "$DIR"/*.mp4 "$DIR"/*.MP4 2>/dev/null || true) +fi + +# ---- Process ---- +if [[ ${#FILES[@]} -eq 0 ]]; then + echo "No MP4 files found in: $DIR" + exit 0 +fi + +for f in "${FILES[@]}"; do + [[ -f "$f" ]] || { print_result "SKIPPED" "$f" "not a file"; continue; } + is_mp4 "$f" || { print_result "SKIPPED" "$f" "not mp4"; continue; } + + base=$(basename -- "$f") + if [[ "$base" == "$PREFIX"* ]]; then + print_result "SKIPPED" "$f" "already prefixed" + continue + fi + + read -r total pink ratio < <(detect_pink "$f") + if [[ "$total" -eq 0 ]]; then + print_result "ERROR" "$f" "no frames parsed" + continue + fi + + awk -v r="$ratio" -v thr="$MIN_RATIO" 'BEGIN{exit !(r>=thr)}' && { + safe_rename "$f" "$PREFIX" + continue + } + + print_result "NOT_MATCH" "$f" "ratio=${ratio} (<${MIN_RATIO})" +done diff --git a/scrapWUNFIdx.sh b/scrapWUNFIdx.sh new file mode 100755 index 0000000..ddf52cc --- /dev/null +++ b/scrapWUNFIdx.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -euo pipefail + +OUTFILE="${1:-wunf_scenes.csv}" + +# Töm (eller skapa) utfilen +: > "$OUTFILE" + +for page in $(seq 1 18); do + echo "Hämtar sida $page..." >&2 + curl -sL "https://www.wakeupnfuck.com/scene?page=${page}" | + perl -0777 -ne ' + # Läs hela sidan som en sträng (-0777) och matcha varje informationsblock + while (m{ + \s* + .*?

\s*([^<]+)\s*

\s* + .*?\s*([^<]+)\s*

\s* + .*?\s*([^<]+)\s*

+ }gxis) { + my ($code, $name, $dur) = ($1, $2, $3); + for ($code, $name, $dur) { s/^\s+|\s+$//g } # trim + # Skriv i önskat format: Namn;Kod;Tid + print "$name;$code;$dur\n"; + } + ' >> "$OUTFILE" +done + +echo "Klart. Skrev $(wc -l < "$OUTFILE") rader till $OUTFILE" >&2 diff --git a/thumbnail.sh b/thumbnail.sh new file mode 100644 index 0000000..11336b1 --- /dev/null +++ b/thumbnail.sh @@ -0,0 +1 @@ +ffmpeg -i WoodManCastingX_17_08_25_Ninel_Mojado_XXX_1080p__hdporn__ghost__dailyvids__0dayporn__internallink__V.mp4 -ss 00:00:07.000 -frames:v 1 out.png diff --git a/videocmp_select.sh b/videocmp_select.sh new file mode 100755 index 0000000..65d70be --- /dev/null +++ b/videocmp_select.sh @@ -0,0 +1,346 @@ +#!/usr/bin/env bash +# videocmp_select.sh +# +# Modes: +# 1) Pair mode: compare A and B +# ./videocmp_select.sh A.mp4 B.mp4 [options] +# +# 2) Directory scan mode: group by prefix before first "WoodmanCastingX" (case-insensitive) +# ./videocmp_select.sh --scan-dir [DIR] [--recursive] [options] +# (DIR defaults to "." if omitted) +# +# Pipeline: +# 1) Validate files (ffprobe fields + ffmpeg deep decode) +# 2) Confirm same movie via snapshot SSIM @ --snapshot-time (default 12s) +# 3) Optional: run external compare impl (simple/advanced) for logging +# 4) Pick preferred: prefer --prefer-height (default 720), then longer duration, then larger file +# 5) Act on loser: --action print|move|delete (with --dry-run) +# +# Common options: +# --snapshot-time SEC (default: 12) +# --snapshot-scale WxH (default: 320:-1) +# --snapshot-ssim THRESH (default: 0.97) +# --impl simple|advanced (default: simple) # logs only +# --impl-simple PATH (default: ./compare_simple.sh) +# --impl-advanced PATH (default: ./compare_advanced.sh) +# --impl-optional (default) warn if impl missing +# --impl-required error if chosen impl missing +# --prefer-height N (default: 720) +# --duration-eps SEC (default: 0.0) +# --action print|move|delete (default: print) +# --trash-dir PATH (default: $HOME/.video_trash) +# --dry-run +# --verbose +# +# Directory-scan options: +# --scan-dir [DIR] Enable directory mode (DIR optional; default ".") +# --recursive, -r Recurse into subfolders +# --delimiter WORD (default: WoodmanCastingX) case-insensitive +# +# Exit codes: +# 0 success | 1 differ/broken | 2 usage | 3 missing dependency +set -u + +# -------- defaults -------- +SNAP_T="12" +SNAP_SCALE="320:-1" +SNAP_SSIM="0.97" + +IMPL="simple" +IMPL_SIMPLE="./compare_simple.sh" +IMPL_ADV="./compare_advanced.sh" +IMPL_REQUIRED=0 + +PREF_HEIGHT=720 +DURATION_EPS=0.0 + +ACTION="print" +TRASH_DIR="${HOME}/.video_trash" +DRY=0 +VERBOSE=0 + +SCAN_DIR="" +RECURSIVE=0 +DELIM="WoodmanCastingX" + +# -------- helpers -------- +need() { command -v "$1" >/dev/null 2>&1 || { echo "Missing dependency: $1" >&2; exit 3; }; } +v() { [[ $VERBOSE -eq 1 ]] && echo "[LOG]" "$@" >&2; } +die() { echo "[ERR]" "$@" >&2; exit 1; } + +need ffmpeg; need ffprobe; need awk; need grep; need stat; need sed; need tr; need find + +# -------- array-based option parser -------- +ARGS=("$@") +REM_ARR=() +i=0 +while (( i < ${#ARGS[@]} )); do + arg="${ARGS[i]}" + case "$arg" in + --snapshot-time) SNAP_T="${ARGS[i+1]:-}"; i=$((i+2));; + --snapshot-scale) SNAP_SCALE="${ARGS[i+1]:-}"; i=$((i+2));; + --snapshot-ssim) SNAP_SSIM="${ARGS[i+1]:-}"; i=$((i+2));; + --impl) IMPL="${ARGS[i+1]:-}"; i=$((i+2));; + --impl-simple) IMPL_SIMPLE="${ARGS[i+1]:-}"; i=$((i+2));; + --impl-advanced) IMPL_ADV="${ARGS[i+1]:-}"; i=$((i+2));; + --impl-optional) IMPL_REQUIRED=0; i=$((i+1));; + --impl-required) IMPL_REQUIRED=1; i=$((i+1));; + --prefer-height) PREF_HEIGHT="${ARGS[i+1]:-}"; i=$((i+2));; + --duration-eps) DURATION_EPS="${ARGS[i+1]:-}"; i=$((i+2));; + --action) ACTION="${ARGS[i+1]:-}"; i=$((i+2));; + --trash-dir) TRASH_DIR="${ARGS[i+1]:-}"; i=$((i+2));; + --dry-run) DRY=1; i=$((i+1));; + --verbose) VERBOSE=1; i=$((i+1));; + --scan-dir) + # optional arg: use next token unless it looks like another option + next="${ARGS[i+1]:-}" + if [[ -n "$next" && "$next" != --* ]]; then + SCAN_DIR="$next"; i=$((i+2)) + else + SCAN_DIR="."; i=$((i+1)) + fi + ;; + --recursive|-r) RECURSIVE=1; i=$((i+1));; + --delimiter) DELIM="${ARGS[i+1]:-}"; i=$((i+2));; + --help|-h) + grep -E '^# ' "$0" | sed 's/^# //' + exit 0 ;; + *) + # leave positional for pair mode + REM_ARR+=("$arg"); i=$((i+1));; + esac +done + +v "[DBG] Options: SCAN_DIR='${SCAN_DIR:-}' RECURSIVE=$RECURSIVE DELIM='$DELIM' PREF_HEIGHT=$PREF_HEIGHT SNAP_T=$SNAP_T" + +# -------- core functions -------- +probe_meta() { # file -> "w h dur codec size" + local f="$1" size w h dur vcodec + size=$(stat -c%s "$f" 2>/dev/null || stat -f%z "$f") + w=$(ffprobe -v error -select_streams v:0 -show_entries stream=width -of default=nw=1:nk=1 "$f" || echo 0) + h=$(ffprobe -v error -select_streams v:0 -show_entries stream=height -of default=nw=1:nk=1 "$f" || echo 0) + dur=$(ffprobe -v error -show_entries format=duration -of default=nw=1:nk=1 "$f" || echo 0) + vcodec=$(ffprobe -v error -select_streams v:0 -show_entries stream=codec_name -of default=nw=1:nk=1 "$f" || echo "?") + echo "$w $h $dur $vcodec $size" +} + +check_ok() { # file -> 0 ok / 1 bad + local f="$1" w h dur + w=$(ffprobe -v error -select_streams v:0 -show_entries stream=width -of default=nw=1:nk=1 "$f" 2>/dev/null || echo "") + h=$(ffprobe -v error -select_streams v:0 -show_entries stream=height -of default=nw=1:nk=1 "$f" 2>/dev/null || echo "") + dur=$(ffprobe -v error -show_entries format=duration -of default=nw=1:nk=1 "$f" 2>/dev/null || echo "") + if [[ -z "$w" || -z "$h" || "$w" = "N/A" || "$h" = "N/A" || "$w" -eq 0 || "$h" -eq 0 ]]; then + echo "BROKEN(ffprobe: no valid video stream): $f" >&2; return 1; fi + if [[ -n "$dur" && "$dur" != "N/A" ]]; then + awk -v d="$dur" 'BEGIN{exit !(d+0>0)}' || { echo "BROKEN(ffprobe: nonpositive duration): $f" >&2; return 1; } + fi + if ! ffmpeg -v error -xerror -i "$f" -f null - -nostats >/dev/null 2>&1; then + echo "BROKEN(ffmpeg decode): $f" >&2; return 1; fi + return 0 +} + +snapshot_compare_ssim() { # f1 f2 time scale -> "ssim" (empty if fail) + local f1="$1" f2="$2" t="$3" sc="$4" + local tmpd s1 s2 log ssim + tmpd="$(mktemp -d)"; s1="$tmpd/1.png"; s2="$tmpd/2.png"; log="$tmpd/cmp.log" + ffmpeg -hide_banner -v error -y -i "$f1" -ss "$t" -frames:v 1 -vf "scale=$sc,format=yuv420p" "$s1" || true + ffmpeg -hide_banner -v error -y -i "$f2" -ss "$t" -frames:v 1 -vf "scale=$sc,format=yuv420p" "$s2" || true + if [[ ! -s "$s1" || ! -s "$s2" ]]; then rm -rf "$tmpd"; echo ""; return 0; fi + ffmpeg -hide_banner -v info -i "$s1" -i "$s2" -lavfi "ssim" -f null - > /dev/null 2> "$log" || true + ssim="$(grep -Eo 'All:[0-9]+(\.[0-9]+)?' "$log" | head -n1 | cut -d: -f2)" + [[ -z "$ssim" ]] && ssim="$(grep -Eo 'SSIM [^ ]* All:[0-9]+(\.[0-9]+)?' "$log" | awk -F'All:' '{print $2}' | head -n1)" + [[ -z "$ssim" ]] && ssim="$(grep -Eo 'SSIM Y:[0-9]+(\.[0-9]+)?' "$log" | head -n1 | cut -d: -f2)" + rm -rf "$tmpd"; echo "$ssim" +} + +run_impl() { # impl, A, B + local which="$1" f1="$2" f2="$3" path="" + [[ "$which" == "advanced" ]] && path="$IMPL_ADV" || path="$IMPL_SIMPLE" + if [[ ! -x "$path" ]]; then + if [[ $IMPL_REQUIRED -eq 1 ]]; then + die "Requested --impl=$which but script not found/executable at $path" + else + echo "[impl:$which] not found ($path) — skipping" >&2; return 0 + fi + fi + echo "[impl:$which] $path \"$f1\" \"$f2\"" >&2 + "$path" "$f1" "$f2" 2>&1 | sed -n '1,12p' >&2 +} + +score_file() { # file -> "tier720 dur size" + local f="$1" + read -r W H DUR VC SIZE <<<"$(probe_meta "$f")" + local tier=1; [[ "$H" -eq "$PREF_HEIGHT" ]] && tier=0 + echo "$tier $DUR $SIZE" +} + +pick_winner() { # A B -> "KEEP|DROP|why" + local a="$1" b="$2" + read -r aTier aDur aSize <<<"$(score_file "$a")" + read -r bTier bDur bSize <<<"$(score_file "$b")" + v "Quality scores: A[tier=$aTier dur=$aDur size=$aSize] B[tier=$bTier dur=$bDur size=$bSize]" + if (( aTier < bTier )); then echo "$a|$b|prefer ${PREF_HEIGHT}p (A)"; return 0; fi + if (( bTier < aTier )); then echo "$b|$a|prefer ${PREF_HEIGHT}p (B)"; return 0; fi + awk -v A="$aDur" -v B="$bDur" -v eps="$DURATION_EPS" 'BEGIN{ + if ((A-B) > eps) print "A"; else if ((B-A) > eps) print "B"; else print "TIE"; + }' | { + read who + if [[ "$who" == "A" ]]; then echo "$a|$b|longer duration (A)"; return 0; fi + if [[ "$who" == "B" ]]; then echo "$b|$a|longer duration (B)"; return 0; fi + if (( aSize > bSize )); then echo "$a|$b|larger file size (A)"; else + if (( bSize > aSize )); then echo "$b|$a|larger file size (B)"; else + echo "$a|$b|tie-break (keep A)"; fi; fi + } +} + +act_on_loser() { # loser keep + local loser="$1" keeper="$2" + case "$ACTION" in + print) + echo "[ACTION] Keep: $keeper" + echo "[ACTION] Drop: $loser" + ;; + move) + mkdir -p -- "$TRASH_DIR" + if [[ $DRY -eq 1 ]]; then + echo "[ACTION] dry-run: mv \"$loser\" \"$TRASH_DIR/\"" + else + mv -- "$loser" "$TRASH_DIR/" && echo "[ACTION] moved to trash: $loser -> $TRASH_DIR/" + fi + echo "[ACTION] kept: $keeper" + ;; + delete) + if [[ $DRY -eq 1 ]]; then + echo "[ACTION] dry-run: rm \"$loser\"" + else + rm -- "$loser" && echo "[ACTION] deleted: $loser" + fi + echo "[ACTION] kept: $keeper" + ;; + *) echo "[WARN] Unknown --action='$ACTION' → printing only."; echo "[ACTION] Keep: $keeper ; Drop: $loser" ;; + esac +} + +same_movie_or_skip() { # A B -> 0 if same (SSIM>=thr), else 1 + local a="$1" b="$2" ssim + echo "== Snapshot compare @${SNAP_T}s: ==" >&2 + ssim="$(snapshot_compare_ssim "$a" "$b" "$SNAP_T" "$SNAP_SCALE")" + if [[ -z "$ssim" ]]; then + echo "[WARN] Could not compute SSIM for: $a vs $b" >&2; return 1; fi + printf "[INFO] SSIM(All) %s vs %s → %s\n" "$(basename -- "$a")" "$(basename -- "$b")" "$ssim" >&2 + awk -v s="$ssim" -v thr="$SNAP_SSIM" 'BEGIN{exit !(s+0 >= thr+0)}' +} + +# ----- pair comparison driver ----- +compare_pair() { # A B + local A="$1" B="$2" + echo "== Step 1: Validating files =="; okA=0; okB=0 + check_ok "$A" && okA=1; check_ok "$B" && okB=1 + if (( okA==0 || okB==0 )); then echo "[FAIL] One or both files broken. A_ok=$okA B_ok=$okB" >&2; exit 1; fi + echo "[OK] Both files decoded cleanly." + + echo; echo "== Step 2: Snapshot compare =="; + if ! same_movie_or_skip "$A" "$B"; then + echo "[FAIL] Files are not the same movie (SSIM < ${SNAP_SSIM})." >&2; exit 1 + fi + echo "[OK] Same movie." + + echo; echo "== Step 3: External compare ($IMPL) =="; run_impl "$IMPL" "$A" "$B" + + echo; echo "== Step 4: Quality selection (prefer ${PREF_HEIGHT}p) =="; + read -r keep drop why <<<"$(pick_winner "$A" "$B" | tr '|' ' ')" + echo "[DECISION] Keep: $keep"; echo "[DECISION] Drop: $drop"; echo "[REASON] $why" + + echo; echo "== Step 5: Action =="; act_on_loser "$drop" "$keep" + + echo; echo "== Summary =="; echo "Kept: $keep"; echo "Dropped: $drop" + [[ "$ACTION" == "move" ]] && echo "(Moved loser to: $TRASH_DIR)" + [[ "$ACTION" == "delete" ]] && echo "(Loser was deleted)" + [[ $DRY -eq 1 ]] && echo "(Dry-run only; no changes made)" +} + +# ----- directory scan helpers/drivers ----- +scan_and_group() { + if [[ $RECURSIVE -eq 1 ]]; then + mapfile -t FILES < <(find "$SCAN_DIR" -type f \( -iname '*.mp4' \)) + else + mapfile -t FILES < <(ls "$SCAN_DIR"/*.mp4 "$SCAN_DIR"/*.MP4 2>/dev/null || true) + fi +} + +extract_key() { # filename -> group key before first DELIM (CI), original case + local name="$1" low delimlow key + low="$(echo -n "$name" | tr '[:upper:]' '[:lower:]')" + delimlow="$(echo -n "$DELIM" | tr '[:upper:]' '[:lower:]')" + [[ "$low" == *"$delimlow"* ]] || { echo ""; return 0; } + key="${low%%"$delimlow"*}" + local cutlen=${#key} + echo "${name:0:$cutlen}" +} + +process_group() { # files... + local files=("$@") n=${#files[@]} + if (( n < 2 )); then v "Group <2 files → skip"; return 0; fi + echo; echo "=== Group (${n} files): prefix before '${DELIM}' ===" + echo "Files:"; for f in "${files[@]}"; do echo " - $f"; done + + local best="${files[0]}" + if ! check_ok "$best"; then echo "[WARN] Skipping broken file: $best"; return 0; fi + + for ((i=1;i> Directory scan mode on: $SCAN_DIR (recursive=$RECURSIVE, delimiter='$DELIM')" + scan_and_group + if [[ ${#FILES[@]} -eq 0 ]]; then echo "No MP4 files found."; exit 0; fi + + declare -A groups + for f in "${FILES[@]}"; do + base="$(basename -- "$f")" + key="$(extract_key "$base")" + [[ -z "$key" ]] && { v "No delimiter in: $base → skip grouping"; continue; } + groups["$key"]+=$'\n'"$f" + done + + if [[ ${#groups[@]} -eq 0 ]]; then echo "No files with delimiter '$DELIM' found."; exit 0; fi + + for k in "${!groups[@]}"; do + IFS=$'\n' read -r -d '' -a grpfiles < <(printf "%s" "${groups[$k]}" | sed '/^$/d' && printf '\0') + process_group "${grpfiles[@]}" + done + + echo; echo ">> Directory scan complete." +} + +# -------- dispatch -------- +if [[ -n "${SCAN_DIR:-}" ]]; then + dir_mode + exit 0 +fi + +# Pair mode +if [[ ${#REM_ARR[@]} -lt 2 ]]; then + echo "Usage (pair): $0 A.mp4 B.mp4 [options]" >&2 + echo " or (scan): $0 --scan-dir [DIR] [--recursive] [options]" >&2 + exit 2 +fi +A="${REM_ARR[0]}"; B="${REM_ARR[1]}" +[[ -f "$A" ]] || die "File not found: $A" +[[ -f "$B" ]] || die "File not found: $B" +compare_pair "$A" "$B" diff --git a/wunf_upd_prefix.sh b/wunf_upd_prefix.sh new file mode 100755 index 0000000..db6ff12 --- /dev/null +++ b/wunf_upd_prefix.sh @@ -0,0 +1,155 @@ +#!/usr/bin/env bash +set -euo pipefail + +CSV_DEFAULT="wunf_scenes.csv" + +usage() { + echo "Användning:" + echo " $0 [csv-fil]" + echo " $0 --all [csv-fil]" + echo " $0 --list [csv-fil] # en väg per rad" + echo " ls -1 --quoting-style=literal \[WUNF\]_*.mp4 > filenames.txt && ./script/wunf_upd_prefix.sh --list filenames.txt wunf_scenes.csv && rm filenames.txt" + exit 1 +} + +rename_file() { + local FILE="$1" + local CSV="$2" + + local base dir rest first second remainder name code code_us newbase newpath + + if [[ ! -f "$FILE" ]]; then + echo "Fel: Filen finns inte: $FILE" >&2 + return 1 + fi + + base="$(basename -- "$FILE")" + dir="$(dirname -- "$FILE")" + + # Kontrollera prefix + if [[ "$base" != "[WUNF]"* ]]; then + echo "Hoppar över (ej [WUNF]-prefix): $base" >&2 + return 0 + fi + + # Plocka ut allt efter prefixet [WUNF]_ + rest="${base#"[WUNF]"}" + rest="${rest#_}" + + IFS='_' read -r first second remainder <<< "$rest" + + if [[ -z "${first:-}" || -z "${second:-}" ]]; then + echo "Fel: Kunde inte extrahera två namn-delar i: $base" >&2 + return 1 + fi + + # Namn = två första token, med '_' -> ' ' + name="${first//_/ } ${second//_/ }" + name="$(printf '%s' "$name" | sed -E 's/^[[:space:]]+|[[:space:]]+$//g')" + + # slå upp i CSV (format Namn;Kod;Tid) + code="$( + awk -F';' -v target="$name" ' + BEGIN{IGNORECASE=1} + { + n=$1; gsub(/^[ \t]+|[ \t]+$/, "", n); + if (n==target) { + c=$2; gsub(/^[ \t]+|[ \t]+$/, "", c); + print c; exit + } + }' "$CSV" + )" + + if [[ -z "$code" ]]; then + echo "Ingen kod för \"$name\" hittades i $CSV (fil: $base)" >&2 + return 1 + fi + + code_us="${code// /_}" + + newbase="$(printf '%s' "$base" | sed -E "s/^\[WUNF\]/[$code_us]/")" + newpath="$dir/$newbase" + + if [[ "$base" == "$newbase" ]]; then + echo "Hoppar över (redan rätt namn): $base" >&2 + return 0 + fi + + if [[ -e "$newpath" ]]; then + echo "Fel: Målfilen finns redan: $newpath" >&2 + return 1 + fi + + mv -- "$dir/$base" "$newpath" + echo "Bytt namn:" + echo " Från: $base" + echo " Till: $newbase" +} + +### Huvudprogram +if [[ $# -lt 1 ]]; then + usage +fi + +mode="$1" + +case "$mode" in + --all) + if [[ $# -lt 2 ]]; then usage; fi + DIR="$2" + CSV="${3:-$CSV_DEFAULT}" + + if [[ ! -d "$DIR" ]]; then + echo "Fel: Katalog finns inte: $DIR" >&2 + exit 1 + fi + if [[ ! -f "$CSV" ]]; then + echo "Fel: CSV-filen finns inte: $CSV" >&2 + exit 1 + fi + + shopt -s nullglob + for f in "$DIR"/\[WUNF\]*.mp4; do + rename_file "$f" "$CSV" || true + done + ;; + + --list) + if [[ $# -lt 2 ]]; then usage; fi + LIST="$2" + CSV="${3:-$CSV_DEFAULT}" + + if [[ ! -f "$LIST" ]]; then + echo "Fel: Textfil finns inte: $LIST" >&2 + exit 1 + fi + if [[ ! -f "$CSV" ]]; then + echo "Fel: CSV-filen finns inte: $CSV" >&2 + exit 1 + fi + + # Läs en rad i taget; ignorera tomma rader och kommentarer + # Hanterar vägar med mellanslag korrekt + while IFS= read -r line || [[ -n "$line" ]]; do + # Trim CR (om filen är CRLF) + line="${line%$'\r'}" + # Trim whitespace runt + trimmed="$(printf '%s' "$line" | sed -E 's/^[[:space:]]+|[[:space:]]+$//g')" + # Hoppa över tomma rader och kommentarer + [[ -z "$trimmed" || "$trimmed" =~ ^# ]] && continue + rename_file "$trimmed" "$CSV" || true + done < "$LIST" + ;; + + *) + FILE="$mode" + CSV="${2:-$CSV_DEFAULT}" + + if [[ ! -f "$CSV" ]]; then + echo "Fel: CSV-filen finns inte: $CSV" >&2 + exit 1 + fi + + rename_file "$FILE" "$CSV" + ;; +esac