@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.
- package/LICENSE +21 -0
- package/README.md +424 -0
- package/bin/che.py +869 -0
- package/bin/commands.py +872 -0
- package/bin/drives/eject-all-drives.py +203 -0
- package/bin/drives/mount-all-drives.py +140 -0
- package/bin/drives/ping-nas.py +242 -0
- package/bin/files/compress-folders.py +188 -0
- package/bin/files/delete-by-ext.py +108 -0
- package/bin/files/delete-empty-folders.py +136 -0
- package/bin/files/delete-smb-files.py +105 -0
- package/bin/files/files-under-size.py +132 -0
- package/bin/files/find-largest-files.py +102 -0
- package/bin/files/make-alpha-dir.py +63 -0
- package/bin/git/all-actions.py +545 -0
- package/bin/git/checkout-my-branches.py +147 -0
- package/bin/git/clean-stale-branches.py +161 -0
- package/bin/git/prune-worktrees.py +143 -0
- package/bin/git/sync-all-branches.py +368 -0
- package/bin/git/update-local-branches.py +195 -0
- package/bin/install.py +1515 -0
- package/bin/shellgen.py +782 -0
- package/bin/system/btop-launch.py +114 -0
- package/bin/system/update-brew.py +146 -0
- package/bin/tui.py +648 -0
- package/bin/utils.py +836 -0
- package/bin/video/delete-duplicate-videos.py +313 -0
- package/bin/video/detect-green-magenta-videos.py +268 -0
- package/bin/video/find-movie-by-year.py +84 -0
- package/bin/video/find-video-mkv-issues.py +242 -0
- package/bin/video/fix-codecs.py +209 -0
- package/bin/video/largest-tv-shows.py +175 -0
- package/bin/video/remove-metadata.py +126 -0
- package/bin/video/rename-video-file.py +329 -0
- package/bin/video/scan-videos-audio-language.py +110 -0
- package/bin/video/show-codecs.py +111 -0
- package/bin/video/validate-video-files.py +114 -0
- package/bin/video/video-list.py +120 -0
- package/package.json +54 -0
- package/shell/che.bash +382 -0
- package/shell/che.fish +282 -0
- package/shell/che.zsh +389 -0
- package/shell/completions/_che +146 -0
- package/shell/completions/che.bash +107 -0
- 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
|