summaryrefslogtreecommitdiff
path: root/ar/.local/bin/rmev
blob: 954b06d3b0ba91ded08eb7d8c2db216107a025fe (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
#!/usr/bin/env bash
# rmev - clean up videos that mpv integrity-check flagged as corrupt.
#
# The corrupt list comes from integrity_cache.tsv (current status). A file is
# only eligible for deletion when its on-disk mtime/size exactly match what the
# cache recorded. (If the file changed after the scan it is skipped as
# "changed" and a re-scan is suggested.)
#
# Default behaviour:
#   * Show the target list and ask for y/N confirmation before deleting.
#   * Use trash-put (trash) if available, otherwise fall back to rm.
#   * After deletion, remove the entries from integrity_cache.tsv and corrupted.log.

set -euo pipefail

CONFIG_DIR="${MPV_CONFIG_DIR:-$HOME/.config/mpv}"
CACHE="$CONFIG_DIR/integrity_cache.tsv"
LOG="$CONFIG_DIR/corrupted.log"

DRY=0
FORCE=0
MODE=auto # MODE: auto|trash|rm

usage() {
  cat <<'EOF'
Usage: rmev [options]

Delete video files that mpv integrity-check flagged as corrupt.

Options:
  -n, --dry-run   Show the targets without deleting anything
  -f, --force     Delete without the confirmation prompt
  -t, --trash     Move to trash (requires trash-put)
  -r, --rm        Permanently delete (rm) instead of trashing
  -h, --help      Show this help

Default: trash if trash-put exists, otherwise rm. Confirms the list first.
Deleted files are also removed from integrity_cache.tsv and corrupted.log.
EOF
}

while [ $# -gt 0 ]; do
  case "$1" in
  -n | --dry-run) DRY=1 ;;
  -f | --force) FORCE=1 ;;
  -t | --trash) MODE=trash ;;
  -r | --rm) MODE=rm ;;
  -h | --help)
    usage
    exit 0
    ;;
  *)
    printf 'Unknown option: %s\n\n' "$1" >&2
    usage >&2
    exit 2
    ;;
  esac
  shift
done

if [ ! -f "$CACHE" ]; then
  printf 'Cache file not found: %s\n' "$CACHE" >&2
  exit 1
fi

# Decide deletion method
if [ "$MODE" = auto ]; then
  if command -v trash-put >/dev/null 2>&1; then MODE=trash; else MODE=rm; fi
fi
if [ "$MODE" = trash ] && ! command -v trash-put >/dev/null 2>&1; then
  printf 'trash-put not found. Use --rm to delete permanently, or install trash-cli.\n' >&2
  exit 1
fi

# Extract entries whose last cached status is corrupt (mtime, size, path).
# The cache is append-only, so a path may appear on several lines; last wins.
mapfile -t entries < <(
  awk -F'\t' '
        NF>=5 {
            p=$5; for (i=6; i<=NF; i++) p = p "\t" $i
            mt[p]=$1; sz[p]=$2; st[p]=$3
        }
        END { for (p in st) if (st[p]=="corrupt") printf "%s\t%s\t%s\n", mt[p], sz[p], p }
    ' "$CACHE"
)

if [ "${#entries[@]}" -eq 0 ]; then
  printf 'No files flagged as corrupt.\n'
  exit 0
fi

# Select real targets: must exist and match the cached mtime/size.
targets=()
skipped=()
for line in "${entries[@]}"; do
  mt=${line%%$'\t'*}
  rest=${line#*$'\t'}
  sz=${rest%%$'\t'*}
  path=${rest#*$'\t'}

  if [ ! -e "$path" ]; then
    skipped+=("missing  | $path")
    continue
  fi
  cur_mt=$(stat -c %Y -- "$path" 2>/dev/null || echo -1)
  cur_sz=$(stat -c %s -- "$path" 2>/dev/null || echo -1)
  if [ "$cur_mt" != "$mt" ] || [ "$cur_sz" != "$sz" ]; then
    skipped+=("changed  | $path  (re-scan needed)")
    continue
  fi
  targets+=("$path")
done

printf 'Corrupt files (%d):\n' "${#targets[@]}"
for p in "${targets[@]}"; do printf '  %s\n' "$p"; done

if [ "${#skipped[@]}" -gt 0 ]; then
  printf '\nSkipped (%d):\n' "${#skipped[@]}"
  for s in "${skipped[@]}"; do printf '  %s\n' "$s"; done
fi

if [ "${#targets[@]}" -eq 0 ]; then
  exit 0
fi

if [ "$DRY" -eq 1 ]; then
  printf '\n(dry-run) Nothing deleted.\n'
  exit 0
fi

method_label=$([ "$MODE" = trash ] && echo 'move to trash' || echo 'permanently delete')
if [ "$FORCE" -ne 1 ]; then
  printf '\n%s %d file(s)? [y/N] ' "$method_label" "${#targets[@]}"
  read -r ans
  case "$ans" in
  y | Y | yes | YES) ;;
  *)
    printf 'Cancelled.\n'
    exit 0
    ;;
  esac
fi

deleted=()
for p in "${targets[@]}"; do
  if [ "$MODE" = trash ]; then
    if trash-put -- "$p"; then deleted+=("$p"); else printf 'Failed: %s\n' "$p" >&2; fi
  else
    if rm -f -- "$p"; then deleted+=("$p"); else printf 'Failed: %s\n' "$p" >&2; fi
  fi
done

past_label=$([ "$MODE" = trash ] && echo 'moved to trash' || echo 'deleted')
printf '%d file(s) %s.\n' "${#deleted[@]}" "$past_label"

# Remove deleted entries from the cache and log
if [ "${#deleted[@]}" -gt 0 ]; then
  tmpset=$(mktemp)
  printf '%s\n' "${deleted[@]}" >"$tmpset"

  prune() { # $1: file, $2: field number where the path starts
    local f="$1" start="$2" tmp
    [ -f "$f" ] || return 0
    tmp=$(mktemp)
    awk -F'\t' -v start="$start" '
            NR==FNR { del[$0]=1; next }
            {
                p=$start; for (i=start+1; i<=NF; i++) p = p "\t" $i
                if (!(p in del)) print
            }
        ' "$tmpset" "$f" >"$tmp"
    mv "$tmp" "$f"
  }

  prune "$CACHE" 5 # integrity_cache.tsv: mtime size status errors PATH
  prune "$LOG" 2   # corrupted.log: timestamp PATH
  rm -f "$tmpset"
fi