first commit
This commit is contained in:
BIN
._wunf_upd_prefix.sh
Executable file
BIN
._wunf_upd_prefix.sh
Executable file
Binary file not shown.
47
checkmp4.sh
Executable file
47
checkmp4.sh
Executable file
@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# checkmp4.sh <directory>
|
||||
#
|
||||
# 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 <directory>"
|
||||
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
|
||||
42
clean_nfo.sh
Executable file
42
clean_nfo.sh
Executable file
@ -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
|
||||
127
compare_advanced.sh
Executable file
127
compare_advanced.sh
Executable file
@ -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 <file1> <file2>" >&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 - </dev/null
|
||||
ffmpeg -v error -i "$f2" -vf "signature=format=xml:filename=$sig2" -f null - </dev/null
|
||||
|
||||
echo "Comparing fingerprints..."
|
||||
# The signature filter can compare two signature files. Different FFmpeg builds print slightly different lines.
|
||||
# We run a compare pass and parse a similarity metric from stderr.
|
||||
# Try two plausible invocations; accept the first that succeeds.
|
||||
if ffmpeg -v info -i "$sig1" -i "$sig2" -filter_complex "signature=compare=1" -f null - 2>"$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"
|
||||
87
compare_simple.sh
Executable file
87
compare_simple.sh
Executable file
@ -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 <file1> <file2>" >&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"
|
||||
122
compare_snapshot.sh
Executable file
122
compare_snapshot.sh
Executable file
@ -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 <file1.mp4> <file2.mp4> [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 <file1.mp4> <file2.mp4> [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)"; }
|
||||
183
count_words.sh
Executable file
183
count_words.sh
Executable file
@ -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 <<EOF
|
||||
count_words.sh v$VERSION
|
||||
Räknar ord (underscore-separerade) i filnamn, case-insensitive.
|
||||
Ignorerar filändelser; filtrerar på angivna ändelser (default: mp4).
|
||||
Ignorerar prefix som [WUNF] eller [WUNF_001] i filnamn.
|
||||
|
||||
Usage:
|
||||
$0 [dir] [--to-file] [--ext "mp4,avi,mkv"] [--version]
|
||||
|
||||
Flaggor:
|
||||
--to-file Skriv endast orden (kommaseparerat) till $OUTFILE (append utan dubbletter)
|
||||
--ext "a,b,c" Tillåtna filändelser (kommaseparerad lista), t.ex. "mp4,avi"
|
||||
--version Skriv ut version och avsluta
|
||||
-h, --help Visa denna hjälp
|
||||
EOF
|
||||
}
|
||||
|
||||
# Om första argumentet är en katalog, använd den
|
||||
if [[ $# -gt 0 && -d "${1:-}" ]]; then
|
||||
DIR="$1"
|
||||
shift
|
||||
fi
|
||||
|
||||
# Arg-parsning
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--to-file)
|
||||
TO_FILE=true; shift ;;
|
||||
--ext)
|
||||
shift
|
||||
IFS=',' read -r -a EXTS <<< "${1:-}"
|
||||
shift || true
|
||||
;;
|
||||
--version)
|
||||
echo "$VERSION"
|
||||
exit 0
|
||||
;;
|
||||
-h|--help)
|
||||
print_help; exit 0 ;;
|
||||
*)
|
||||
echo "Okänt argument: $1" >&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
|
||||
182
count_words.sh.save
Executable file
182
count_words.sh.save
Executable file
@ -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 <<EOF
|
||||
count_words.sh v$VERSION
|
||||
Räknar ord (underscore-separerade) i filnamn, case-insensitive.
|
||||
Ignorerar filändelser; filtrerar på angivna ändelser (default: mp4).
|
||||
Ignorerar prefix som [WUNF] eller [WUNF_001] i filnamn.
|
||||
|
||||
Usage:
|
||||
$0 [dir] [--to-file] [--ext "mp4,avi,mkv"] [--version]
|
||||
|
||||
Flaggor:
|
||||
--to-file Skriv endast orden (kommaseparerat) till $OUTFILE (append utan dubbletter)
|
||||
--ext "a,b,c" Tillåtna filändelser (kommaseparerad lista), t.ex. "mp4,avi"
|
||||
--version Skriv ut version och avsluta
|
||||
-h, --help Visa denna hjälp
|
||||
EOF
|
||||
}
|
||||
|
||||
# Om första argumentet är en katalog, använd den
|
||||
if [[ $# -gt 0 && -d "${1:-}" ]]; then
|
||||
DIR="$1"
|
||||
shift
|
||||
fi
|
||||
|
||||
# Arg-parsning
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--to-file)
|
||||
TO_FILE=true; shift ;;
|
||||
--ext)
|
||||
shift
|
||||
IFS=',' read -r -a EXTS <<< "${1:-}"
|
||||
shift || true
|
||||
;;
|
||||
--version)
|
||||
echo "$VERSION"
|
||||
exit 0
|
||||
;;
|
||||
-h|--help)
|
||||
print_help; exit 0 ;;
|
||||
*)
|
||||
echo "Okänt argument: $1" >&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
|
||||
90
detect_wunf.sh
Executable file
90
detect_wunf.sh
Executable file
@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env bash
|
||||
# detect_pink_intro_hsv.sh <file.mp4> [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 <file.mp4> [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 a<b?a:b}
|
||||
# Convert RGB(0..255) to HSV; return H in degrees [0,360), S,V in [0,1]
|
||||
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<m) m=g; if(b<m) m=b;
|
||||
d=M-m; V=M;
|
||||
if (M==0) { S=0 } else { S=(d==0?0:d/M) }
|
||||
if (d==0) { H=0 }
|
||||
else if (M==r) { H = 60.0 * fmod(((g-b)/d), 6) }
|
||||
else if (M==g) { H = 60.0 * (((b-r)/d) + 2) }
|
||||
else { H = 60.0 * (((r-g)/d) + 4) }
|
||||
if (H < 0) H += 360.0
|
||||
hsv[1]=H; hsv[2]=S; hsv[3]=V
|
||||
}
|
||||
function fmod(a,b){ return a - int(a/b)*b }
|
||||
BEGIN{ total=0; pink=0; }
|
||||
NF==3 {
|
||||
R=$1; G=$2; B=$3;
|
||||
total++;
|
||||
rgb2hsv(R,G,B);
|
||||
H=hsv[1]; S=hsv[2]; V=hsv[3];
|
||||
# classify: hue in magenta band AND sufficiently saturated/bright
|
||||
if (H>=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
|
||||
|
||||
106
mvi.sh
Executable file
106
mvi.sh
Executable file
@ -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
|
||||
266
rename_and_filter.sh
Executable file
266
rename_and_filter.sh
Executable file
@ -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] <directory> <reserved_words.csv> [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 <directory> <reserved_words.csv> [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."
|
||||
40
rename_space.sh
Executable file
40
rename_space.sh
Executable file
@ -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
|
||||
94
run_wunf.sh
Executable file
94
run_wunf.sh
Executable file
@ -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 <<EOF
|
||||
Usage: $(basename "$0") [options]
|
||||
|
||||
Kör scan + prefix och uppdatera därefter prefix i CSV-listan.
|
||||
|
||||
Options:
|
||||
-d, --dir <path> Katalog att skanna (default: .)
|
||||
-p, --prefix <str> Prefix att sätta (default: [WUNF]_)
|
||||
-c, --csv <file> CSV-fil att använda (default: wunf_scenes.csv)
|
||||
-e, --ext <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!"
|
||||
34
sanitycheck.sh
Executable file
34
sanitycheck.sh
Executable file
@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# sanitycheck.sh <file.mp4>
|
||||
#
|
||||
# Returns: "OK" if file looks good, "Not OK" otherwise.
|
||||
|
||||
file="$1"
|
||||
|
||||
if [ -z "$file" ]; then
|
||||
echo "Usage: $0 <file.mp4>"
|
||||
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
|
||||
30
scan_and_prefix_pink.md
Normal file
30
scan_and_prefix_pink.md
Normal file
@ -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).
|
||||
152
scan_and_prefix_pink.sh
Executable file
152
scan_and_prefix_pink.sh
Executable file
@ -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 <directory> [--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<m) m=g; if(b<m) m=b;
|
||||
d=M-m; V=M;
|
||||
if (M==0) S=0; else S=(d==0?0:d/M);
|
||||
if (d==0) H=0;
|
||||
else if (M==r) H = 60.0 * fmod(((g-b)/d), 6);
|
||||
else if (M==g) H = 60.0 * (((b-r)/d) + 2);
|
||||
else H = 60.0 * (((r-g)/d) + 4);
|
||||
if (H < 0) H += 360.0;
|
||||
hsv[1]=H; hsv[2]=S; hsv[3]=V;
|
||||
}
|
||||
BEGIN{ total=0; pink=0; }
|
||||
NF==3 {
|
||||
R=$1; G=$2; B=$3; total++;
|
||||
rgb2hsv(R,G,B); H=hsv[1]; S=hsv[2]; V=hsv[3];
|
||||
if (H>=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
|
||||
28
scrapWUNFIdx.sh
Executable file
28
scrapWUNFIdx.sh
Executable file
@ -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{
|
||||
<div\ class="informations">\s*
|
||||
.*?<h3>\s*([^<]+)\s*</h3>\s*
|
||||
.*?<p\ class="sub">\s*([^<]+)\s*</p>\s*
|
||||
.*?<p\ class="timer">\s*([^<]+)\s*</p>
|
||||
}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
|
||||
1
thumbnail.sh
Normal file
1
thumbnail.sh
Normal file
@ -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
|
||||
346
videocmp_select.sh
Executable file
346
videocmp_select.sh
Executable file
@ -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<n;i++)); do
|
||||
local cand="${files[i]}"
|
||||
if ! check_ok "$cand"; then echo "[WARN] Skipping broken file: $cand"; continue; fi
|
||||
echo; echo "--- Compare: $(basename -- "$best") vs $(basename -- "$cand") ---"
|
||||
if ! same_movie_or_skip "$best" "$cand"; then
|
||||
echo "[SKIP] Snapshot says NOT same movie; leaving both in place."; continue; fi
|
||||
run_impl "$IMPL" "$best" "$cand"
|
||||
read -r keep drop why <<<"$(pick_winner "$best" "$cand" | tr '|' ' ')"
|
||||
echo "[DECISION] Keep: $keep"; echo "[DECISION] Drop: $drop"; echo "[REASON] $why"
|
||||
act_on_loser "$drop" "$keep"
|
||||
best="$keep"
|
||||
done
|
||||
|
||||
echo; echo "=== Group result: kept $(basename -- "$best") ==="
|
||||
}
|
||||
|
||||
dir_mode() {
|
||||
[[ -n "${SCAN_DIR:-}" ]] || SCAN_DIR="."
|
||||
[[ -d "$SCAN_DIR" ]] || die "Not a directory: $SCAN_DIR"
|
||||
echo ">> 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"
|
||||
155
wunf_upd_prefix.sh
Executable file
155
wunf_upd_prefix.sh
Executable file
@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
CSV_DEFAULT="wunf_scenes.csv"
|
||||
|
||||
usage() {
|
||||
echo "Användning:"
|
||||
echo " $0 <filnamn> [csv-fil]"
|
||||
echo " $0 --all <katalog> [csv-fil]"
|
||||
echo " $0 --list <textfil> [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
|
||||
Reference in New Issue
Block a user