@codebend3r/che 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +424 -0
  3. package/bin/che.py +869 -0
  4. package/bin/commands.py +872 -0
  5. package/bin/drives/eject-all-drives.py +203 -0
  6. package/bin/drives/mount-all-drives.py +140 -0
  7. package/bin/drives/ping-nas.py +242 -0
  8. package/bin/files/compress-folders.py +188 -0
  9. package/bin/files/delete-by-ext.py +108 -0
  10. package/bin/files/delete-empty-folders.py +136 -0
  11. package/bin/files/delete-smb-files.py +105 -0
  12. package/bin/files/files-under-size.py +132 -0
  13. package/bin/files/find-largest-files.py +102 -0
  14. package/bin/files/make-alpha-dir.py +63 -0
  15. package/bin/git/all-actions.py +545 -0
  16. package/bin/git/checkout-my-branches.py +147 -0
  17. package/bin/git/clean-stale-branches.py +161 -0
  18. package/bin/git/prune-worktrees.py +143 -0
  19. package/bin/git/sync-all-branches.py +368 -0
  20. package/bin/git/update-local-branches.py +195 -0
  21. package/bin/install.py +1515 -0
  22. package/bin/shellgen.py +782 -0
  23. package/bin/system/btop-launch.py +114 -0
  24. package/bin/system/update-brew.py +146 -0
  25. package/bin/tui.py +648 -0
  26. package/bin/utils.py +836 -0
  27. package/bin/video/delete-duplicate-videos.py +313 -0
  28. package/bin/video/detect-green-magenta-videos.py +268 -0
  29. package/bin/video/find-movie-by-year.py +84 -0
  30. package/bin/video/find-video-mkv-issues.py +242 -0
  31. package/bin/video/fix-codecs.py +209 -0
  32. package/bin/video/largest-tv-shows.py +175 -0
  33. package/bin/video/remove-metadata.py +126 -0
  34. package/bin/video/rename-video-file.py +329 -0
  35. package/bin/video/scan-videos-audio-language.py +110 -0
  36. package/bin/video/show-codecs.py +111 -0
  37. package/bin/video/validate-video-files.py +114 -0
  38. package/bin/video/video-list.py +120 -0
  39. package/package.json +54 -0
  40. package/shell/che.bash +382 -0
  41. package/shell/che.fish +282 -0
  42. package/shell/che.zsh +389 -0
  43. package/shell/completions/_che +146 -0
  44. package/shell/completions/che.bash +107 -0
  45. package/shell/completions/che.fish +371 -0
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env python3
2
+ """🩺 Check .mp4/.mkv files are playable by decoding a single frame with mpv.
3
+
4
+ Usage:
5
+ validate-video-files.py --path=/path/to/media [--verbose]
6
+
7
+ Options:
8
+ --path=DIR Directory to scan (required)
9
+ --verbose Print each file as it is checked
10
+ --help Show help
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import subprocess
16
+ import sys
17
+ from pathlib import Path
18
+
19
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
20
+
21
+ from utils import (
22
+ add_bool_flag,
23
+ build_parser,
24
+ info,
25
+ iter_files,
26
+ require_binary,
27
+ run,
28
+ run_cli,
29
+ scan_root,
30
+ success,
31
+ warning,
32
+ )
33
+
34
+ __version__ = "3.0.0"
35
+
36
+ VIDEO_EXTENSIONS = ("mp4", "mkv")
37
+
38
+
39
+ def main(argv: list[str] | None = None) -> int:
40
+ parser = build_parser(
41
+ prog="validate-video-files",
42
+ description="🩺 Check .mp4/.mkv files are playable by decoding one frame with mpv.",
43
+ )
44
+ parser.add_argument("--path", default="", metavar="DIR", help="Directory to scan (required)")
45
+ add_bool_flag(parser, "--verbose", help="Print each file as it is checked")
46
+ info(f"Running command in {Path.cwd()}")
47
+ args = parser.parse_args(argv)
48
+
49
+ if not args.path:
50
+ warning(f"❌ Error: '{args.path}' is not a valid directory")
51
+ return 1
52
+
53
+ root = scan_root(args.path)
54
+
55
+ require_binary("mpv", hint="Install it using Homebrew:\n brew install mpv")
56
+
57
+ checked = 0
58
+ bad: list[Path] = []
59
+
60
+ for path in iter_files(root, extensions=VIDEO_EXTENSIONS):
61
+ # The shell version gated this on `[[ -z "$VERBOSE" ]]`, but VERBOSE is
62
+ # the literal string "false" when unset, which is never empty - so the
63
+ # line never printed and --verbose did nothing. Wired up properly here.
64
+ if args.verbose:
65
+ info(f"Checking: {path}")
66
+
67
+ checked += 1
68
+ try:
69
+ completed = run(
70
+ [
71
+ "mpv",
72
+ # --no-config so a stale ~/.config/mpv/mpv.conf cannot emit
73
+ # warnings that would be misread as playback failures.
74
+ "--no-config",
75
+ "--no-audio",
76
+ "--vo=null",
77
+ "--really-quiet",
78
+ "--frames=1",
79
+ str(path),
80
+ ],
81
+ check=False,
82
+ capture=True,
83
+ timeout=30,
84
+ )
85
+ except subprocess.TimeoutExpired:
86
+ # The shell version had no timeout, so an mpv wedged on a stalled
87
+ # SMB read blocked the scan forever.
88
+ warning(f"❌ Timed out reading: {path}")
89
+ bad.append(path)
90
+ continue
91
+
92
+ # DELIBERATE DIVERGENCE: the shell version ran mpv as a simple command
93
+ # under `set -e`, so the FIRST unplayable file killed the whole scan
94
+ # before it could even print which file it was. This records the
95
+ # failure and carries on. The verdict is the exit code alone; stderr
96
+ # noise is reported but is not by itself a failure, since --really-quiet
97
+ # does not suppress every warning mpv can emit.
98
+ if completed.returncode != 0:
99
+ warning(f"❌ Unplayable or error in: {path}")
100
+ bad.append(path)
101
+ elif (completed.stderr or "").strip():
102
+ warning(f"⚠️ Played, but mpv complained about: {path}")
103
+
104
+ print()
105
+ if bad:
106
+ warning(f"❌ {len(bad)} of {checked} file(s) failed playback.")
107
+ return 1
108
+
109
+ success(f"✅ All {checked} file(s) played back cleanly.")
110
+ return 0
111
+
112
+
113
+ if __name__ == "__main__":
114
+ run_cli(main)
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env python3
2
+ """🎬 List .mp4/.mkv files under a path with human-readable sizes.
3
+
4
+ Usage:
5
+ video-list.py --path=/dir [--recursive] [--sort=alpha|fileSizeAsc|fileSizeDesc]
6
+ [--with-folder]
7
+
8
+ Options:
9
+ --path=DIR Directory to scan (required)
10
+ --recursive Recurse into subdirectories (default: top level only)
11
+ --sort=METHOD alpha (default), fileSizeAsc, or fileSizeDesc
12
+ --with-folder Prefix each name with its containing folder
13
+ --help Show help
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import locale
19
+ import sys
20
+ from pathlib import Path
21
+
22
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
23
+
24
+ from utils import (
25
+ CYAN,
26
+ GREEN,
27
+ NC,
28
+ YELLOW,
29
+ add_bool_flag,
30
+ build_parser,
31
+ format_bytes,
32
+ info,
33
+ iter_files,
34
+ log,
35
+ note,
36
+ run_cli,
37
+ scan_root,
38
+ warning,
39
+ )
40
+
41
+ __version__ = "3.0.0"
42
+
43
+ VIDEO_EXTENSIONS = ("mp4", "mkv")
44
+ SORT_METHODS = ("alpha", "fileSizeAsc", "fileSizeDesc")
45
+
46
+
47
+ def main(argv: list[str] | None = None) -> int:
48
+ parser = build_parser(
49
+ prog="video-list",
50
+ description="🎬 List .mp4/.mkv files under a path with human-readable sizes.",
51
+ )
52
+ parser.add_argument("--path", default="", metavar="DIR", help="Directory to scan (required)")
53
+ add_bool_flag(parser, "--recursive", help="Recurse into subdirectories")
54
+ add_bool_flag(parser, "--with-folder", dest="with_folder", help="Prefix with folder name")
55
+ parser.add_argument(
56
+ "--sort",
57
+ default="alpha",
58
+ choices=SORT_METHODS,
59
+ metavar="METHOD",
60
+ help="alpha (default), fileSizeAsc, or fileSizeDesc",
61
+ )
62
+ info(f"Running command in {Path.cwd()}")
63
+ args = parser.parse_args(argv)
64
+
65
+ note(f"Scanning: {args.path}")
66
+ note(f"Recursive: {str(args.recursive).lower()}")
67
+ note(f"Sort method: {args.sort}")
68
+ note(f"With folder: {str(args.with_folder).lower()}")
69
+ note("----------------------------------------------------")
70
+
71
+ if not args.path:
72
+ warning("❌ --path is required")
73
+ return 1
74
+
75
+ # Replaces the shell version's `find` error on a missing path with a
76
+ # styled message; the exit code is unchanged.
77
+ root = scan_root(args.path)
78
+
79
+ entries: list[tuple[int, Path]] = []
80
+ for path in iter_files(
81
+ root,
82
+ extensions=VIDEO_EXTENSIONS,
83
+ recursive=args.recursive,
84
+ skip_hidden=True,
85
+ ):
86
+ try:
87
+ entries.append((path.stat().st_size, path))
88
+ except OSError:
89
+ continue
90
+
91
+ if args.sort == "fileSizeAsc":
92
+ entries.sort(key=lambda item: (item[0], str(item[1])))
93
+ elif args.sort == "fileSizeDesc":
94
+ # `sort -nr` reverses its last-resort full-line tiebreak too, so equal
95
+ # sizes come out in DESCENDING path order. Reverse both key fields.
96
+ entries.sort(key=lambda item: (item[0], str(item[1])), reverse=True)
97
+ else:
98
+ # `sort -k2` collates under LC_COLLATE, which is case-insensitive at
99
+ # the primary level. strxfrm reproduces that; a plain codepoint sort
100
+ # would put every capitalised name first.
101
+ locale.setlocale(locale.LC_COLLATE, "")
102
+ entries.sort(key=lambda item: locale.strxfrm(str(item[1])))
103
+
104
+ for size, path in entries:
105
+ size_human = format_bytes(size)
106
+ if args.with_folder:
107
+ # `basename $(dirname ./x.mkv)` is ".", but pathlib collapses the
108
+ # "." away and leaves parent.name empty - which would render as a
109
+ # bare leading slash and read like an absolute path.
110
+ folder = path.parent.name or path.parent.as_posix()
111
+ display = f"{CYAN}{folder}{NC}/{GREEN}{path.name}{NC} {YELLOW}[{size_human}]{NC}"
112
+ else:
113
+ display = f"{GREEN}{path.name}{NC} {YELLOW}[{size_human}]{NC}"
114
+ log(display)
115
+
116
+ return 0
117
+
118
+
119
+ if __name__ == "__main__":
120
+ run_cli(main)
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@codebend3r/che",
3
+ "version": "1.0.0",
4
+ "description": "Helper scripts shared across machines, called by name from your shell and browsable through one menu",
5
+ "keywords": [
6
+ "cli",
7
+ "shell",
8
+ "zsh",
9
+ "bash",
10
+ "fish",
11
+ "python",
12
+ "scripts",
13
+ "menu",
14
+ "macos"
15
+ ],
16
+ "homepage": "https://github.com/codebend3r/che#readme",
17
+ "bugs": "https://github.com/codebend3r/che/issues",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/codebend3r/che.git"
21
+ },
22
+ "license": "MIT",
23
+ "author": "CJ Rivas (https://github.com/codebend3r)",
24
+ "bin": {
25
+ "che": "bin/che.py"
26
+ },
27
+ "files": [
28
+ "bin/",
29
+ "shell/",
30
+ "!bin/version-bump.py",
31
+ "!**/__pycache__/",
32
+ "!**/*.pyc"
33
+ ],
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "scripts": {
38
+ "test": "uv run --no-project --with-requirements requirements-dev.txt -- pytest",
39
+ "ci": "run-s lint format syntax executable shebang generated test",
40
+ "lint": "uv run --no-project --with-requirements requirements-dev.txt -- ruff check bin test",
41
+ "format": "uv run --no-project --with-requirements requirements-dev.txt -- ruff format --check bin test",
42
+ "syntax": "find bin test -type f -name '*.py' -print0 | xargs -0 -n1 python3 -m py_compile",
43
+ "executable": "find bin -type f -name '*.py' ! -perm -u+x -print0 | xargs -0 awk 'FNR==1 && /^#!/{print \"Has a shebang but is not executable: \" FILENAME; ec=1} END{exit ec+0}'",
44
+ "shebang": "find bin -type f -name '*.py' -perm -u+x -print0 | xargs -0 awk 'FNR==1 && !/^#!/{print \"Executable but has no shebang: \" FILENAME; ec=1} END{exit ec+0}'",
45
+ "generate": "python3 bin/install.py --generate",
46
+ "generated": "python3 bin/install.py --check",
47
+ "version": "bun run generate && git add shell .zshrc README.md",
48
+ "prepublishOnly": "bun run ci"
49
+ },
50
+ "packageManager": "bun@1.3.14",
51
+ "devDependencies": {
52
+ "npm-run-all2": "9.0.1"
53
+ }
54
+ }
package/shell/che.bash ADDED
@@ -0,0 +1,382 @@
1
+ # che 1.0.0 - generated file, do not edit.
2
+ #
3
+ # Generated from bin/commands.py by `che install`.
4
+ # To change a wrapper, edit that manifest and run `che install` again.
5
+ #
6
+ # Source this file (or let the `che install` block in your rc file do it):
7
+ #
8
+ # . ~/Developer/git/che/shell/che.bash
9
+ #
10
+ # Every wrapper is a shell function, so `type <name>` shows what it runs and
11
+ # `che doctor` can tell you when something it needs is missing.
12
+
13
+ # CHE_HOME is normally exported by the installed rc block. Fall back to this
14
+ # file's own location so a bare `source` of it also works.
15
+ if [ -z "${CHE_HOME:-}" ]; then
16
+ CHE_HOME="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
17
+ fi
18
+
19
+ # Exported, not just set: every wrapper expands "$CHE_BIN", and a shell that
20
+ # replays the *functions* without re-reading this file - a `typeset -f` dump, an
21
+ # environment snapshot, any tool that rebuilds a shell from its exported
22
+ # environment - keeps the functions and drops a plain variable, leaving the
23
+ # wrappers to run "/che.py". CHE_HOME comes in exported by the rc block; this
24
+ # keeps everything derived from it exported too.
25
+ export CHE_BIN="${CHE_HOME}/bin"
26
+
27
+ # Any python3 on PATH by default. `che install` records the interpreter it
28
+ # verified (3.12+) in the rc block, which wins over this.
29
+ : "${CHE_PYTHON:=python3}"
30
+ export CHE_PYTHON
31
+
32
+ # Legacy name from the hand-maintained wrappers, kept so an older snippet in
33
+ # ~/.zshrc that still refers to it keeps resolving.
34
+ export SHELL_FUNCTIONS_BIN="${CHE_BIN}"
35
+
36
+ # Play the completion sound for a wrapper, then return the status it was given.
37
+ #
38
+ # playsound-N is defined outside this repo. Every wrapper used to call it
39
+ # unconditionally, which printed "command not found" on a machine that never
40
+ # had it; here a missing playsound is simply skipped. Set CHE_SOUNDS=0 to
41
+ # silence them all.
42
+ che_notify() {
43
+ if [ "${CHE_SOUNDS:-1}" = "1" ] && command -v "playsound-$1" >/dev/null 2>&1; then
44
+ "playsound-$1" >/dev/null 2>&1
45
+ fi
46
+ return "${2:-0}"
47
+ }
48
+
49
+ # The dispatcher. `che` with no arguments opens the interactive menu.
50
+ che() {
51
+ if [ "$#" -eq 0 ]; then
52
+ "$CHE_PYTHON" "$CHE_BIN/che.py"
53
+ else
54
+ "$CHE_PYTHON" "$CHE_BIN/che.py" "$@"
55
+ che_notify 7 $?
56
+ fi
57
+ }
58
+
59
+ # Same trap as zsh: with expand_aliases on (every interactive bash), an alias
60
+ # sharing a name with a wrapper below turns its definition into a syntax error
61
+ # and the rest of this file never loads. Stash, define, restore - so the user's
62
+ # alias keeps winning at the prompt and `\name` reaches the wrapper.
63
+ _che_stashed_aliases=()
64
+
65
+ _che_stash_aliases() {
66
+ local name
67
+ for name in "$@"; do
68
+ if alias "$name" >/dev/null 2>&1; then
69
+ _che_stashed_aliases+=("$(alias "$name")")
70
+ unalias "$name"
71
+ fi
72
+ done
73
+ }
74
+
75
+ _che_restore_aliases() {
76
+ local entry
77
+ for entry in "${_che_stashed_aliases[@]}"; do
78
+ eval "$entry"
79
+ done
80
+ _che_stashed_aliases=()
81
+ }
82
+
83
+ _che_stash_aliases che all-actions all-actions-watch checkout-my-branches \
84
+ clean-stale-branches clean-stale-branches-dr prune-worktrees \
85
+ prune-worktrees-dr sync-all-branches sync-all-branches-dr \
86
+ update-from-origin update-local-branches show-codecs fix-codecs \
87
+ fix-codecs-dr find-video-mkv-issues validate-video-files \
88
+ scan-videos-audio-language remove-metadata rename-video-file \
89
+ delete-duplicate-videos delete-duplicate-videos-dr video-list \
90
+ detect-green-magenta-videos find-movie-by-year largest-tv-shows \
91
+ delete-by-ext delete-by-ext-dr delete-empty-folders \
92
+ delete-empty-folders-dr delete-smb-files delete-smb-files-dr \
93
+ files-under-size files-under-size-dr find-largest-files make-alpha-dir \
94
+ compress-folders list-permission mount-all-drives eject-all-drives \
95
+ eject-all-drives-dr ping-nas update-brew update-brew-dr btop
96
+
97
+ # --------------------------------------------------------------------------
98
+ # Git - Branch hygiene, worktrees and GitHub Actions
99
+ # --------------------------------------------------------------------------
100
+
101
+ # GitHub Actions status for every open PR you authored
102
+ all-actions() {
103
+ "$CHE_PYTHON" "$CHE_BIN/git/all-actions.py" "$@"
104
+ }
105
+
106
+ # Same as all-actions, refreshed on an interval
107
+ all-actions-watch() {
108
+ "$CHE_PYTHON" "$CHE_BIN/git/all-actions.py" --watch "$@"
109
+ }
110
+
111
+ # Check out recent remote branches you authored that aren't local yet
112
+ checkout-my-branches() {
113
+ "$CHE_PYTHON" "$CHE_BIN/git/checkout-my-branches.py" "$@"
114
+ che_notify 7 $?
115
+ }
116
+
117
+ # Delete local branches whose upstream is gone
118
+ clean-stale-branches() {
119
+ DRY_RUN=false "$CHE_PYTHON" "$CHE_BIN/git/clean-stale-branches.py" "$@"
120
+ che_notify 4 $?
121
+ }
122
+
123
+ # Delete local branches whose upstream is gone (preview only)
124
+ clean-stale-branches-dr() {
125
+ DRY_RUN=true "$CHE_PYTHON" "$CHE_BIN/git/clean-stale-branches.py" "$@"
126
+ che_notify 7 $?
127
+ }
128
+
129
+ # Remove every linked worktree, then prune stale admin records
130
+ prune-worktrees() {
131
+ DRY_RUN=false "$CHE_PYTHON" "$CHE_BIN/git/prune-worktrees.py" "$@"
132
+ che_notify 4 $?
133
+ }
134
+
135
+ # Remove every linked worktree, then prune stale admin records (preview only)
136
+ prune-worktrees-dr() {
137
+ DRY_RUN=true "$CHE_PYTHON" "$CHE_BIN/git/prune-worktrees.py" "$@"
138
+ che_notify 7 $?
139
+ }
140
+
141
+ # Push every worktree, collapse the pushed ones, then tidy branches
142
+ sync-all-branches() {
143
+ DRY_RUN=false "$CHE_PYTHON" "$CHE_BIN/git/sync-all-branches.py" "$@"
144
+ che_notify 7 $?
145
+ }
146
+
147
+ # Push every worktree, collapse the pushed ones, then tidy branches (preview only)
148
+ sync-all-branches-dr() {
149
+ DRY_RUN=true "$CHE_PYTHON" "$CHE_BIN/git/sync-all-branches.py" "$@"
150
+ che_notify 7 $?
151
+ }
152
+
153
+ # Same as sync-all-branches but never pushes: fetch, collapse, rebase
154
+ update-from-origin() {
155
+ DRY_RUN=false "$CHE_PYTHON" "$CHE_BIN/git/sync-all-branches.py" --no-push "$@"
156
+ che_notify 7 $?
157
+ }
158
+
159
+ # Rebase every local branch that has an upstream onto origin
160
+ update-local-branches() {
161
+ "$CHE_PYTHON" "$CHE_BIN/git/update-local-branches.py" "$@"
162
+ che_notify 5 $?
163
+ }
164
+
165
+ # --------------------------------------------------------------------------
166
+ # Video - Codecs, renaming, dedupe and metadata
167
+ # --------------------------------------------------------------------------
168
+
169
+ # Report media files outside the Direct Play codec/container set
170
+ show-codecs() {
171
+ "$CHE_PYTHON" "$CHE_BIN/video/show-codecs.py" "$@"
172
+ che_notify 6 $?
173
+ }
174
+
175
+ # Re-encode media to h265/aac mp4 so it Direct Plays
176
+ fix-codecs() {
177
+ DRY_RUN=false "$CHE_PYTHON" "$CHE_BIN/video/fix-codecs.py" "$@"
178
+ che_notify 7 $?
179
+ }
180
+
181
+ # Re-encode media to h265/aac mp4 so it Direct Plays (preview only)
182
+ fix-codecs-dr() {
183
+ DRY_RUN=true "$CHE_PYTHON" "$CHE_BIN/video/fix-codecs.py" "$@"
184
+ che_notify 7 $?
185
+ }
186
+
187
+ # Scan MKV files and estimate Plex direct-play compatibility
188
+ find-video-mkv-issues() {
189
+ "$CHE_PYTHON" "$CHE_BIN/video/find-video-mkv-issues.py" "$@"
190
+ che_notify 4 $?
191
+ }
192
+
193
+ # Check .mp4/.mkv files decode by playing one frame with mpv
194
+ validate-video-files() {
195
+ "$CHE_PYTHON" "$CHE_BIN/video/validate-video-files.py" "$@"
196
+ che_notify 2 $?
197
+ }
198
+
199
+ # Print the audio language tags of every video under a path
200
+ scan-videos-audio-language() {
201
+ "$CHE_PYTHON" "$CHE_BIN/video/scan-videos-audio-language.py" "$@"
202
+ che_notify 3 $?
203
+ }
204
+
205
+ # Strip all metadata from video files recursively
206
+ remove-metadata() {
207
+ "$CHE_PYTHON" "$CHE_BIN/video/remove-metadata.py" "$@"
208
+ che_notify 4 $?
209
+ }
210
+
211
+ # Title-case video filenames (and optionally their folders)
212
+ rename-video-file() {
213
+ "$CHE_PYTHON" "$CHE_BIN/video/rename-video-file.py" "$@"
214
+ che_notify 5 $?
215
+ }
216
+
217
+ # Delete duplicate MKV/MP4 files under a root directory
218
+ delete-duplicate-videos() {
219
+ DRY_RUN=false "$CHE_PYTHON" "$CHE_BIN/video/delete-duplicate-videos.py" "$@"
220
+ che_notify 6 $?
221
+ }
222
+
223
+ # Delete duplicate MKV/MP4 files under a root directory (preview only)
224
+ delete-duplicate-videos-dr() {
225
+ DRY_RUN=true "$CHE_PYTHON" "$CHE_BIN/video/delete-duplicate-videos.py" "$@"
226
+ che_notify 6 $?
227
+ }
228
+
229
+ # List .mp4/.mkv files under a path with human-readable sizes
230
+ video-list() {
231
+ "$CHE_PYTHON" "$CHE_BIN/video/video-list.py" "$@"
232
+ che_notify 5 $?
233
+ }
234
+
235
+ # Detect videos with the green/magenta chroma artifact
236
+ detect-green-magenta-videos() {
237
+ "$CHE_PYTHON" "$CHE_BIN/video/detect-green-magenta-videos.py" "$@"
238
+ che_notify 3 $?
239
+ }
240
+
241
+ # Find movie folders whose name ends with "(YYYY)"
242
+ find-movie-by-year() {
243
+ "$CHE_PYTHON" "$CHE_BIN/video/find-movie-by-year.py" "$@"
244
+ che_notify 2 $?
245
+ }
246
+
247
+ # Rank TV show folders in a library by total size on disk
248
+ largest-tv-shows() {
249
+ "$CHE_PYTHON" "$CHE_BIN/video/largest-tv-shows.py" "$@"
250
+ che_notify 2 $?
251
+ }
252
+
253
+ # --------------------------------------------------------------------------
254
+ # Files - Delete, compress and inspect by size or extension
255
+ # --------------------------------------------------------------------------
256
+
257
+ # Delete files under a path matching a set of extensions
258
+ delete-by-ext() {
259
+ DRY_RUN=false "$CHE_PYTHON" "$CHE_BIN/files/delete-by-ext.py" "$@"
260
+ che_notify 6 $?
261
+ }
262
+
263
+ # Delete files under a path matching a set of extensions (preview only)
264
+ delete-by-ext-dr() {
265
+ DRY_RUN=true "$CHE_PYTHON" "$CHE_BIN/files/delete-by-ext.py" "$@"
266
+ che_notify 7 $?
267
+ }
268
+
269
+ # Delete truly-empty directories under a path, cascading upwards
270
+ delete-empty-folders() {
271
+ DRY_RUN=false "$CHE_PYTHON" "$CHE_BIN/files/delete-empty-folders.py" "$@"
272
+ che_notify 6 $?
273
+ }
274
+
275
+ # Delete truly-empty directories under a path, cascading upwards (preview only)
276
+ delete-empty-folders-dr() {
277
+ DRY_RUN=true "$CHE_PYTHON" "$CHE_BIN/files/delete-empty-folders.py" "$@"
278
+ che_notify 7 $?
279
+ }
280
+
281
+ # Delete .smbdelete* files left behind by an SMB share
282
+ delete-smb-files() {
283
+ DRY_RUN=false "$CHE_PYTHON" "$CHE_BIN/files/delete-smb-files.py" "$@"
284
+ che_notify 6 $?
285
+ }
286
+
287
+ # Delete .smbdelete* files left behind by an SMB share (preview only)
288
+ delete-smb-files-dr() {
289
+ DRY_RUN=true "$CHE_PYTHON" "$CHE_BIN/files/delete-smb-files.py" "$@"
290
+ che_notify 7 $?
291
+ }
292
+
293
+ # Find video files at or under a size threshold
294
+ files-under-size() {
295
+ DRY_RUN=false "$CHE_PYTHON" "$CHE_BIN/files/files-under-size.py" "$@"
296
+ che_notify 4 $?
297
+ }
298
+
299
+ # Find video files at or under a size threshold (preview only)
300
+ files-under-size-dr() {
301
+ DRY_RUN=true "$CHE_PYTHON" "$CHE_BIN/files/files-under-size.py" "$@"
302
+ che_notify 7 $?
303
+ }
304
+
305
+ # List the largest files under a path, biggest first
306
+ find-largest-files() {
307
+ "$CHE_PYTHON" "$CHE_BIN/files/find-largest-files.py" "$@"
308
+ che_notify 2 $?
309
+ }
310
+
311
+ # Create '#' and A-Z bucket folders under a parent directory
312
+ make-alpha-dir() {
313
+ "$CHE_PYTHON" "$CHE_BIN/files/make-alpha-dir.py" "$@"
314
+ che_notify 4 $?
315
+ }
316
+
317
+ # Zip every immediate subfolder of a path at max compression
318
+ compress-folders() {
319
+ "$CHE_PYTHON" "$CHE_BIN/files/compress-folders.py" "$@"
320
+ che_notify 7 $?
321
+ }
322
+
323
+ # Show ownership and mode of a volume under /Volumes
324
+ list-permission() {
325
+ command ls -ld "/Volumes/$1"
326
+ che_notify 7 $?
327
+ }
328
+
329
+ # --------------------------------------------------------------------------
330
+ # Drives - Mount, eject and keep-alive for the NAS volumes
331
+ # --------------------------------------------------------------------------
332
+
333
+ # Mount all NAS drives over SMB via AppleScript
334
+ mount-all-drives() {
335
+ "$CHE_PYTHON" "$CHE_BIN/drives/mount-all-drives.py" "$@"
336
+ che_notify 7 $?
337
+ }
338
+
339
+ # Eject all NAS volumes from /Volumes
340
+ eject-all-drives() {
341
+ "$CHE_PYTHON" "$CHE_BIN/drives/eject-all-drives.py" "$@"
342
+ che_notify 7 $?
343
+ }
344
+
345
+ # Eject all NAS volumes from /Volumes (preview only)
346
+ eject-all-drives-dr() {
347
+ "$CHE_PYTHON" "$CHE_BIN/drives/eject-all-drives.py" --dry-run "$@"
348
+ che_notify 7 $?
349
+ }
350
+
351
+ # Keep-alive pinger that remounts a NAS drive that dropped off
352
+ ping-nas() {
353
+ "$CHE_PYTHON" "$CHE_BIN/drives/ping-nas.py" "$@"
354
+ }
355
+
356
+ # --------------------------------------------------------------------------
357
+ # System - Homebrew, monitoring and the machine itself
358
+ # --------------------------------------------------------------------------
359
+
360
+ # Update Homebrew, upgrade formulae and casks, then clean up
361
+ update-brew() {
362
+ "$CHE_PYTHON" "$CHE_BIN/system/update-brew.py" "$@"
363
+ che_notify 7 $?
364
+ }
365
+
366
+ # Update Homebrew, upgrade formulae and casks, then clean up (preview only)
367
+ update-brew-dr() {
368
+ "$CHE_PYTHON" "$CHE_BIN/system/update-brew.py" --dry-run "$@"
369
+ che_notify 7 $?
370
+ }
371
+
372
+ # Launch btop with a gruvbox theme matching the macOS appearance
373
+ btop() {
374
+ "$CHE_PYTHON" "$CHE_BIN/system/btop-launch.py" "$@"
375
+ }
376
+
377
+ _che_restore_aliases
378
+
379
+ # Completions, when this is bash and the completion builtin is available.
380
+ if [ -n "${BASH_VERSION:-}" ] && [ -r "${CHE_HOME}/shell/completions/che.bash" ]; then
381
+ . "${CHE_HOME}/shell/completions/che.bash"
382
+ fi