VideoSpeed 1.0.6__tar.gz → 1.0.9__tar.gz

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 (44) hide show
  1. {videospeed-1.0.6/src/VideoSpeed.egg-info → videospeed-1.0.9}/PKG-INFO +25 -11
  2. {videospeed-1.0.6 → videospeed-1.0.9}/README.md +24 -10
  3. {videospeed-1.0.6 → videospeed-1.0.9}/pyproject.toml +1 -1
  4. {videospeed-1.0.6 → videospeed-1.0.9}/src/VideoSpeed/__init__.py +1 -1
  5. videospeed-1.0.9/src/VideoSpeed/about_page.py +201 -0
  6. videospeed-1.0.9/src/VideoSpeed/check_system.py +200 -0
  7. videospeed-1.0.9/src/VideoSpeed/cli.py +557 -0
  8. videospeed-1.0.9/src/VideoSpeed/edit_walk.py +428 -0
  9. videospeed-1.0.9/src/VideoSpeed/encoder.py +324 -0
  10. videospeed-1.0.9/src/VideoSpeed/file_stage.py +73 -0
  11. videospeed-1.0.9/src/VideoSpeed/media_info.py +67 -0
  12. videospeed-1.0.9/src/VideoSpeed/menu_model.py +211 -0
  13. videospeed-1.0.9/src/VideoSpeed/menu_painter.py +269 -0
  14. videospeed-1.0.9/src/VideoSpeed/menu_session.py +75 -0
  15. videospeed-1.0.9/src/VideoSpeed/run_output.py +110 -0
  16. videospeed-1.0.9/src/VideoSpeed/tui.py +387 -0
  17. {videospeed-1.0.6 → videospeed-1.0.9/src/VideoSpeed.egg-info}/PKG-INFO +25 -11
  18. {videospeed-1.0.6 → videospeed-1.0.9}/src/VideoSpeed.egg-info/SOURCES.txt +12 -1
  19. {videospeed-1.0.6 → videospeed-1.0.9}/tests/test_about.py +90 -32
  20. videospeed-1.0.9/tests/test_cli.py +452 -0
  21. {videospeed-1.0.6 → videospeed-1.0.9}/tests/test_docs.py +18 -2
  22. videospeed-1.0.9/tests/test_errors.py +49 -0
  23. videospeed-1.0.9/tests/test_fs.py +173 -0
  24. {videospeed-1.0.6 → videospeed-1.0.9}/tests/test_json.py +11 -9
  25. videospeed-1.0.9/tests/test_logging.py +286 -0
  26. {videospeed-1.0.6 → videospeed-1.0.9}/tests/test_prereq.py +11 -2
  27. videospeed-1.0.9/tests/test_tui.py +678 -0
  28. videospeed-1.0.6/src/VideoSpeed/cli.py +0 -1569
  29. videospeed-1.0.6/src/VideoSpeed/menu.py +0 -482
  30. videospeed-1.0.6/tests/test_cli.py +0 -158
  31. videospeed-1.0.6/tests/test_errors.py +0 -37
  32. videospeed-1.0.6/tests/test_fs.py +0 -68
  33. videospeed-1.0.6/tests/test_tui.py +0 -349
  34. {videospeed-1.0.6 → videospeed-1.0.9}/LICENSE.md +0 -0
  35. {videospeed-1.0.6 → videospeed-1.0.9}/setup.cfg +0 -0
  36. {videospeed-1.0.6 → videospeed-1.0.9}/src/VideoSpeed/__main__.py +0 -0
  37. {videospeed-1.0.6 → videospeed-1.0.9}/src/VideoSpeed/cli.bootstrap-old.py +0 -0
  38. {videospeed-1.0.6 → videospeed-1.0.9}/src/VideoSpeed.egg-info/dependency_links.txt +0 -0
  39. {videospeed-1.0.6 → videospeed-1.0.9}/src/VideoSpeed.egg-info/entry_points.txt +0 -0
  40. {videospeed-1.0.6 → videospeed-1.0.9}/src/VideoSpeed.egg-info/requires.txt +0 -0
  41. {videospeed-1.0.6 → videospeed-1.0.9}/src/VideoSpeed.egg-info/top_level.txt +0 -0
  42. {videospeed-1.0.6 → videospeed-1.0.9}/tests/test_build.py +0 -0
  43. {videospeed-1.0.6 → videospeed-1.0.9}/tests/test_dependencies.py +0 -0
  44. {videospeed-1.0.6 → videospeed-1.0.9}/tests/test_package.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: VideoSpeed
3
- Version: 1.0.6
3
+ Version: 1.0.9
4
4
  Summary: A command-line video editing tool built in Python for cutting, speeding up, and applying a boomerang effect to MP4 videos.
5
5
  Author-email: Wilgat Wong <wilgat.wong@gmail.com>
6
6
  License: MIT
@@ -17,20 +17,20 @@ Dynamic: license-file
17
17
 
18
18
  # VideoSpeed - Cut, speed, and boomerang MP4 clips from the CLI
19
19
 
20
- ![Version](https://img.shields.io/badge/Version-1.0.6-blue?style=flat-square)
20
+ ![Version](https://img.shields.io/badge/Version-1.0.9-blue?style=flat-square)
21
21
  ![License](https://img.shields.io/badge/License-MIT-green?style=flat-square)
22
22
  [![CIAO](https://img.shields.io/badge/Philosophy-CIAO%20(Caution%20%E2%80%A2%20Intentional%20%E2%80%A2%20Anti--fragile%20%E2%80%A2%20Over--engineered)-purple.svg)](https://github.com/cloudgen/ciao)
23
23
  [![Stars](https://img.shields.io/github/stars/Wilgat/VideoSpeed?style=flat-square)](https://github.com/Wilgat/VideoSpeed)
24
24
  [![Python](https://img.shields.io/badge/Python-3.11%2B-blue?style=flat-square)]()
25
25
  [![PyPI](https://img.shields.io/pypi/v/VideoSpeed?style=flat-square)](https://pypi.org/project/VideoSpeed/)
26
26
 
27
- VideoSpeed cuts a time range from an MP4, changes that clip’s length (percent), and can append a reverse pass (boomerang). On a terminal, no arguments opens a text menu (edit, about, Exit). Choosing edit walks through the prompts. Scripts pass `--file` / `--start` / `--end`. Encoding uses **FFmpeg**; duration probing uses **OpenCV**. The menu screen uses the default text-menu style: aligned number, verb, and note, and a rounded box along the bottom.
27
+ VideoSpeed cuts a time range from an MP4, changes that clip’s length (percent), and can append a reverse pass (boomerang). On a terminal, no arguments opens a text menu (edit, hello, about, Exit). Choosing edit walks through the prompts. Scripts pass `--file` / `--start` / `--end`. Encoding uses **FFmpeg**; duration probing uses **OpenCV**. The menu screen uses the default text-menu style: aligned number, verb, and note, and a rounded box along the bottom.
28
28
 
29
29
  | You | The other role | Not this |
30
30
  |-----|----------------|----------|
31
31
  | Editor at a terminal or a script | FFmpeg on `PATH` (does the encode) | A website, installer, or root/sudo tool |
32
32
 
33
- Package version SSOT: `src/VideoSpeed/__init__.py` (`MAJOR_VERSION`, `MINOR_VERSION`, `PATCH_VERSION` → **1.0.6**). `pyproject.toml` copies that string. Console entrypoint: `video-speed`.
33
+ Package version SSOT: `src/VideoSpeed/__init__.py` (`MAJOR_VERSION`, `MINOR_VERSION`, `PATCH_VERSION` → **1.0.9**). `pyproject.toml` copies that string. Console entrypoint: `video-speed`.
34
34
 
35
35
  ## Features
36
36
 
@@ -39,9 +39,10 @@ Package version SSOT: `src/VideoSpeed/__init__.py` (`MAJOR_VERSION`, `MINOR_VERS
39
39
  - **Length/speed** change (**20–200%**) with A/V tempo kept in sync
40
40
  - Optional **boomerang** (forward then reverse)
41
41
  - **USB-safe** intermediate files (staged next to the output) and final publish via `shutil.move`
42
- - **Text menu** on a terminal (`edit`, `about`, `Exit`): columns line up, and a three-row rounded input box sits on the bottom with a status line under it
43
- - **`--help`** / **`--version`**; no arguments in a terminal opens that menu, and **edit** starts the interactive editor
44
- - Non-interactive job: `--file`, `--start`, `--end`, optional `--percent` / `--boomerang`. Any of `--file`, `--start`, `--end`, or `--folder` skips the menu
42
+ - **Text menu** on a terminal (`edit`, `hello`, `about`, `Exit`): columns line up, and a three-row rounded input box sits on the bottom with a status line under it
43
+ - **Product verbs** `help`, `about`, `hello`, `edit`, and `list-mp4`. `help` prints the same usage as `--help`. `about` and `hello` print a page and do not ask for a folder or a file. `edit` on a terminal asks for the folder, then the file. `list-mp4` lists MP4 files and does not encode. `Exit` stays on the menu. `./build.sh` verbs such as `setup` are not `video-speed` verbs
44
+ - **`--help`** / **`--version`**; no arguments in a terminal opens that menu
45
+ - Non-interactive job: `--file`, `--start`, `--end`, optional `--percent` / `--boomerang`, with or without `edit`. Any of those selectors with no product verb skips the menu
45
46
  - Fail-closed when **FFmpeg** is missing from `PATH`, when prompts are needed without a terminal, or when `--percent` or `--boomerang` is passed without a job
46
47
 
47
48
  ## Quick Installation
@@ -92,9 +93,9 @@ The console script is installed on the pyenv 3.14 prefix. `./setup.sh` changes t
92
93
 
93
94
  ```bash
94
95
  # after: python -m build (or ./build.sh build)
95
- pip install dist/VideoSpeed-1.0.6-py3-none-any.whl
96
+ pip install dist/VideoSpeed-1.0.9-py3-none-any.whl
96
97
  # or
97
- pip install dist/VideoSpeed-1.0.6.tar.gz
98
+ pip install dist/VideoSpeed-1.0.9.tar.gz
98
99
  ```
99
100
 
100
101
  ### Verify
@@ -115,7 +116,20 @@ video-speed
115
116
  python -m VideoSpeed
116
117
  ```
117
118
 
118
- The screen shows a numbered menu and a rounded input box along the bottom, as wide as the terminal, with one line to type on and a status line under the box. **edit** continues the prompts. **about** shows the product. **Exit** leaves.
119
+ The screen shows a numbered menu and a rounded input box along the bottom, as wide as the terminal, with one line to type on and a status line under the box. **edit** continues the prompts. **hello** (7) shows `Hello.`. **about** shows the product. **Exit** leaves.
120
+
121
+ The same actions are verbs. `edit` starts at the folder question. `list-mp4` asks for the folder when it is omitted, prints the numbered list, and stops.
122
+
123
+ ```bash
124
+ video-speed help
125
+ video-speed about
126
+ video-speed hello
127
+ video-speed edit
128
+ video-speed edit --folder ./clips
129
+ video-speed edit --file clip.mp4 --start 1 --end 5 --percent 100
130
+ video-speed list-mp4
131
+ video-speed list-mp4 --folder ./clips
132
+ ```
119
133
 
120
134
  1. Folder containing MP4s (Enter = current directory)
121
135
  2. Choose a video by number
@@ -186,4 +200,4 @@ MIT License — see [`LICENSE.md`](LICENSE.md).
186
200
 
187
201
  ## Last Update
188
202
 
189
- 2026-10-01 — **1.0.6**: text menu (edit, about, Exit), `--json`, job flags, and `./build.sh` maintainer verbs.
203
+ 2026-10-01 — **1.0.9**: a non-TUI, non-JSON run shows `debug mode` when `DEBUG` is already on, and each class logs `instantiated` on the one ChronicleLogger. `def main` stays in `src/VideoSpeed/cli.py`.
@@ -1,19 +1,19 @@
1
1
  # VideoSpeed - Cut, speed, and boomerang MP4 clips from the CLI
2
2
 
3
- ![Version](https://img.shields.io/badge/Version-1.0.6-blue?style=flat-square)
3
+ ![Version](https://img.shields.io/badge/Version-1.0.9-blue?style=flat-square)
4
4
  ![License](https://img.shields.io/badge/License-MIT-green?style=flat-square)
5
5
  [![CIAO](https://img.shields.io/badge/Philosophy-CIAO%20(Caution%20%E2%80%A2%20Intentional%20%E2%80%A2%20Anti--fragile%20%E2%80%A2%20Over--engineered)-purple.svg)](https://github.com/cloudgen/ciao)
6
6
  [![Stars](https://img.shields.io/github/stars/Wilgat/VideoSpeed?style=flat-square)](https://github.com/Wilgat/VideoSpeed)
7
7
  [![Python](https://img.shields.io/badge/Python-3.11%2B-blue?style=flat-square)]()
8
8
  [![PyPI](https://img.shields.io/pypi/v/VideoSpeed?style=flat-square)](https://pypi.org/project/VideoSpeed/)
9
9
 
10
- VideoSpeed cuts a time range from an MP4, changes that clip’s length (percent), and can append a reverse pass (boomerang). On a terminal, no arguments opens a text menu (edit, about, Exit). Choosing edit walks through the prompts. Scripts pass `--file` / `--start` / `--end`. Encoding uses **FFmpeg**; duration probing uses **OpenCV**. The menu screen uses the default text-menu style: aligned number, verb, and note, and a rounded box along the bottom.
10
+ VideoSpeed cuts a time range from an MP4, changes that clip’s length (percent), and can append a reverse pass (boomerang). On a terminal, no arguments opens a text menu (edit, hello, about, Exit). Choosing edit walks through the prompts. Scripts pass `--file` / `--start` / `--end`. Encoding uses **FFmpeg**; duration probing uses **OpenCV**. The menu screen uses the default text-menu style: aligned number, verb, and note, and a rounded box along the bottom.
11
11
 
12
12
  | You | The other role | Not this |
13
13
  |-----|----------------|----------|
14
14
  | Editor at a terminal or a script | FFmpeg on `PATH` (does the encode) | A website, installer, or root/sudo tool |
15
15
 
16
- Package version SSOT: `src/VideoSpeed/__init__.py` (`MAJOR_VERSION`, `MINOR_VERSION`, `PATCH_VERSION` → **1.0.6**). `pyproject.toml` copies that string. Console entrypoint: `video-speed`.
16
+ Package version SSOT: `src/VideoSpeed/__init__.py` (`MAJOR_VERSION`, `MINOR_VERSION`, `PATCH_VERSION` → **1.0.9**). `pyproject.toml` copies that string. Console entrypoint: `video-speed`.
17
17
 
18
18
  ## Features
19
19
 
@@ -22,9 +22,10 @@ Package version SSOT: `src/VideoSpeed/__init__.py` (`MAJOR_VERSION`, `MINOR_VERS
22
22
  - **Length/speed** change (**20–200%**) with A/V tempo kept in sync
23
23
  - Optional **boomerang** (forward then reverse)
24
24
  - **USB-safe** intermediate files (staged next to the output) and final publish via `shutil.move`
25
- - **Text menu** on a terminal (`edit`, `about`, `Exit`): columns line up, and a three-row rounded input box sits on the bottom with a status line under it
26
- - **`--help`** / **`--version`**; no arguments in a terminal opens that menu, and **edit** starts the interactive editor
27
- - Non-interactive job: `--file`, `--start`, `--end`, optional `--percent` / `--boomerang`. Any of `--file`, `--start`, `--end`, or `--folder` skips the menu
25
+ - **Text menu** on a terminal (`edit`, `hello`, `about`, `Exit`): columns line up, and a three-row rounded input box sits on the bottom with a status line under it
26
+ - **Product verbs** `help`, `about`, `hello`, `edit`, and `list-mp4`. `help` prints the same usage as `--help`. `about` and `hello` print a page and do not ask for a folder or a file. `edit` on a terminal asks for the folder, then the file. `list-mp4` lists MP4 files and does not encode. `Exit` stays on the menu. `./build.sh` verbs such as `setup` are not `video-speed` verbs
27
+ - **`--help`** / **`--version`**; no arguments in a terminal opens that menu
28
+ - Non-interactive job: `--file`, `--start`, `--end`, optional `--percent` / `--boomerang`, with or without `edit`. Any of those selectors with no product verb skips the menu
28
29
  - Fail-closed when **FFmpeg** is missing from `PATH`, when prompts are needed without a terminal, or when `--percent` or `--boomerang` is passed without a job
29
30
 
30
31
  ## Quick Installation
@@ -75,9 +76,9 @@ The console script is installed on the pyenv 3.14 prefix. `./setup.sh` changes t
75
76
 
76
77
  ```bash
77
78
  # after: python -m build (or ./build.sh build)
78
- pip install dist/VideoSpeed-1.0.6-py3-none-any.whl
79
+ pip install dist/VideoSpeed-1.0.9-py3-none-any.whl
79
80
  # or
80
- pip install dist/VideoSpeed-1.0.6.tar.gz
81
+ pip install dist/VideoSpeed-1.0.9.tar.gz
81
82
  ```
82
83
 
83
84
  ### Verify
@@ -98,7 +99,20 @@ video-speed
98
99
  python -m VideoSpeed
99
100
  ```
100
101
 
101
- The screen shows a numbered menu and a rounded input box along the bottom, as wide as the terminal, with one line to type on and a status line under the box. **edit** continues the prompts. **about** shows the product. **Exit** leaves.
102
+ The screen shows a numbered menu and a rounded input box along the bottom, as wide as the terminal, with one line to type on and a status line under the box. **edit** continues the prompts. **hello** (7) shows `Hello.`. **about** shows the product. **Exit** leaves.
103
+
104
+ The same actions are verbs. `edit` starts at the folder question. `list-mp4` asks for the folder when it is omitted, prints the numbered list, and stops.
105
+
106
+ ```bash
107
+ video-speed help
108
+ video-speed about
109
+ video-speed hello
110
+ video-speed edit
111
+ video-speed edit --folder ./clips
112
+ video-speed edit --file clip.mp4 --start 1 --end 5 --percent 100
113
+ video-speed list-mp4
114
+ video-speed list-mp4 --folder ./clips
115
+ ```
102
116
 
103
117
  1. Folder containing MP4s (Enter = current directory)
104
118
  2. Choose a video by number
@@ -169,4 +183,4 @@ MIT License — see [`LICENSE.md`](LICENSE.md).
169
183
 
170
184
  ## Last Update
171
185
 
172
- 2026-10-01 — **1.0.6**: text menu (edit, about, Exit), `--json`, job flags, and `./build.sh` maintainer verbs.
186
+ 2026-10-01 — **1.0.9**: a non-TUI, non-JSON run shows `debug mode` when `DEBUG` is already on, and each class logs `instantiated` on the one ChronicleLogger. `def main` stays in `src/VideoSpeed/cli.py`.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "VideoSpeed"
7
- version = "1.0.6"
7
+ version = "1.0.9"
8
8
  description = "A command-line video editing tool built in Python for cutting, speeding up, and applying a boomerang effect to MP4 videos."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,6 +1,6 @@
1
1
  # Version SSOT (requirement-python-version). Do not import the CLI from here.
2
2
  MAJOR_VERSION = 1
3
3
  MINOR_VERSION = 0
4
- PATCH_VERSION = 6
4
+ PATCH_VERSION = 9
5
5
  __version__ = "{0}.{1}.{2}".format(MAJOR_VERSION, MINOR_VERSION, PATCH_VERSION)
6
6
  __all__ = ["MAJOR_VERSION", "MINOR_VERSION", "PATCH_VERSION", "__version__"]
@@ -0,0 +1,201 @@
1
+ # =============================================================================
2
+ # About page composer.
3
+ # requirement-python-oop — class AboutPage. Line text stays on requirement-python-about.
4
+ # =============================================================================
5
+ from __future__ import print_function, unicode_literals
6
+
7
+ import os
8
+
9
+ from .check_system import CheckSystem
10
+ from .run_output import log_instantiated
11
+
12
+
13
+ class AboutPage:
14
+ """Identity lines, install sentence, and the star box. One class, one module.
15
+
16
+ The host check is CheckSystem. This class calls that object.
17
+ """
18
+
19
+ def __init__(
20
+ self,
21
+ check,
22
+ app_name,
23
+ version,
24
+ major,
25
+ minor,
26
+ patch,
27
+ author,
28
+ updated,
29
+ homepage,
30
+ download_url,
31
+ usage,
32
+ console_name,
33
+ logger=None,
34
+ ):
35
+ self.logger = logger
36
+ log_instantiated(logger, "AboutPage")
37
+ self.check = check if check is not None else CheckSystem(logger=logger)
38
+ self.app_name = app_name
39
+ self.version = version
40
+ self.major = major
41
+ self.minor = minor
42
+ self.patch = patch
43
+ self.author = author
44
+ self.updated = updated
45
+ self.homepage = homepage
46
+ self.download_url = download_url
47
+ self.usage = usage
48
+ self.console_name = console_name
49
+
50
+ def _is_source_checkout(self, path):
51
+ """True when path is this package inside a source tree that has pyproject.toml."""
52
+ marker = "{}src{}VideoSpeed{}".format(os.sep, os.sep, os.sep)
53
+ if marker not in path:
54
+ return False
55
+ cursor = os.path.dirname(path)
56
+ for _ in range(8):
57
+ if os.path.isfile(os.path.join(cursor, "pyproject.toml")):
58
+ return True
59
+ parent = os.path.dirname(cursor)
60
+ if parent == cursor:
61
+ return False
62
+ cursor = parent
63
+ return False
64
+
65
+ def install_kind(self, path):
66
+ """
67
+ General Purpose: global, local, or uninstalled for an about location.
68
+ A source checkout is uninstalled. A copy under the home directory is local.
69
+ """
70
+ path = os.path.realpath(path)
71
+ parts = path.split(os.sep)
72
+ packaged = "site-packages" in parts or "dist-packages" in parts
73
+ if not packaged and self._is_source_checkout(path):
74
+ return "uninstalled"
75
+ home = os.path.realpath(os.path.expanduser("~"))
76
+ in_home = path == home or path.startswith(home + os.sep)
77
+ base = os.path.basename(path)
78
+ if in_home and (packaged or base in (self.console_name, self.app_name)):
79
+ return "local"
80
+ if path.startswith("/usr/") or path.startswith("/opt/") or path.startswith("/bin/"):
81
+ return "global"
82
+ if packaged and not in_home:
83
+ return "global"
84
+ return "uninstalled"
85
+
86
+ def install_sentence(self, kind):
87
+ """General Purpose: The location sentence inside the about box."""
88
+ if kind == "global":
89
+ return "You are using the GLOBAL INSTALLED version, location:"
90
+ if kind == "local":
91
+ return "You are using the LOCAL INSTALLED version, location:"
92
+ return "You are using an UNINSTALLED version, location:"
93
+
94
+ def about_box_lines(
95
+ self,
96
+ usage=None,
97
+ location=None,
98
+ kind=None,
99
+ homepage=None,
100
+ download_url=None,
101
+ author=None,
102
+ updated=None,
103
+ ):
104
+ """
105
+ General Purpose: Star-bordered about box.
106
+ An empty download URL omits the install line.
107
+ """
108
+ if usage is None:
109
+ usage = self.usage
110
+ if location is None:
111
+ location = self.check.self_location()
112
+ if kind is None:
113
+ kind = self.install_kind(location)
114
+ if homepage is None:
115
+ homepage = self.homepage
116
+ if download_url is None:
117
+ download_url = self.download_url
118
+ if author is None:
119
+ author = self.author
120
+ if updated is None:
121
+ updated = self.updated
122
+ app = self.install_sentence(kind)
123
+ python_exe = self.check.python_executable_name() or "python3"
124
+ msg1 = "{} ({}.{}.{}) by {} on {}".format(
125
+ self.app_name, self.major, self.minor, self.patch, author, updated
126
+ )
127
+ if download_url == "":
128
+ msg = [
129
+ msg1,
130
+ "",
131
+ app,
132
+ " {}".format(location),
133
+ "",
134
+ "Basic Usage:",
135
+ " {}".format(usage),
136
+ "",
137
+ "Please visit our homepage: ",
138
+ ' "{}"'.format(homepage),
139
+ "",
140
+ "",
141
+ "",
142
+ "",
143
+ ]
144
+ else:
145
+ msg = [
146
+ msg1,
147
+ "",
148
+ app,
149
+ " {}".format(location),
150
+ "",
151
+ "Basic Usage:",
152
+ " {}".format(usage),
153
+ "",
154
+ "Please visit our homepage: ",
155
+ ' "{}"'.format(homepage),
156
+ "",
157
+ "Installation command:",
158
+ " curl -fsSL {} | {}".format(download_url, python_exe),
159
+ "",
160
+ ]
161
+ if download_url == "":
162
+ if homepage == "":
163
+ max_line = len(msg) - 3
164
+ else:
165
+ max_line = len(msg) - 2
166
+ else:
167
+ max_line = len(msg) - 1
168
+ if max_line < 1:
169
+ max_line = len(msg)
170
+ max_len = 0
171
+ for index in range(0, max_line):
172
+ if len(msg[index]) > max_len:
173
+ max_len = len(msg[index])
174
+ boxed = []
175
+ for index in range(0, max_line):
176
+ boxed.append(msg[index] + (" " * (max_len - len(msg[index]))))
177
+ star = "*" * (max_len + 4)
178
+ blank = " " * max_len
179
+ lines = [star, "* {} *".format(blank)]
180
+ for row in boxed:
181
+ lines.append("* {} *".format(row))
182
+ lines.append("* {} *".format(blank))
183
+ lines.append(star)
184
+ return lines
185
+
186
+ def framework_about(self):
187
+ """
188
+ General Purpose: About text for the menu result page.
189
+ requirement-python-about: identity, host check, and the install box.
190
+ """
191
+ lines = [
192
+ "{} {}".format(self.app_name, self.version),
193
+ "Domain: Cut → speed/length → optional boomerang for MP4",
194
+ "Runtime tools: FFmpeg (encode), OpenCV (duration probe)",
195
+ "Entry points: video-speed, python -m VideoSpeed",
196
+ "",
197
+ ]
198
+ lines.extend(self.check.check_system_lines())
199
+ lines.append("")
200
+ lines.extend(self.about_box_lines())
201
+ return "\n".join(lines)
@@ -0,0 +1,200 @@
1
+ # =============================================================================
2
+ # Host check for the about page.
3
+ # requirement-python-oop — class CheckSystem.
4
+ # requirement-python-about — line text stays with the about composer.
5
+ # =============================================================================
6
+ from __future__ import print_function, unicode_literals
7
+
8
+ import datetime
9
+ import os
10
+ import platform
11
+ import shutil
12
+ import sys
13
+ import sysconfig
14
+
15
+ from .run_output import log_instantiated
16
+
17
+
18
+ class CheckSystem:
19
+ """Reads for the [CHECK SYSTEM] block. One class, one module.
20
+
21
+ The about composer calls this object. It does not own the star box.
22
+ """
23
+
24
+ def __init__(self, logger=None):
25
+ self.logger = logger
26
+ log_instantiated(logger, "CheckSystem")
27
+
28
+ def parse_sys_version(self, version_text, python_version):
29
+ """
30
+ General Purpose: Python display and C library string from sys.version.
31
+ The C library line is the compiler token, such as GCC 13.3.0.
32
+ """
33
+ gcc = version_text
34
+ if "\n" in gcc:
35
+ gcc = gcc.split("\n", 1)[1]
36
+ elif "[" in gcc and "]" in gcc:
37
+ gcc = gcc.split("[", 1)[1].split("]", 1)[0]
38
+ if gcc == "GCC":
39
+ gcc = "[GCC]"
40
+ if " (Red Hat" in gcc:
41
+ gcc = gcc.split(" (Red Hat", 1)[0] + "]"
42
+ probe = gcc if "[PyPy " in gcc else version_text
43
+ if "[PyPy " in probe and "with" in probe:
44
+ extra = probe.split("with", 1)[0].split("[", 1)[1].strip()
45
+ python_version = "{} ({})".format(python_version, extra)
46
+ gcc = "[" + probe.split("with ", 1)[1]
47
+ shown = gcc.strip()
48
+ if shown.startswith("[") and shown.endswith("]"):
49
+ shown = shown[1:-1].strip()
50
+ return python_version, shown
51
+
52
+ def arch_label(self, version_text, machine=None):
53
+ """General Purpose: amd64 / arm64 / x86 style name for this host."""
54
+ if "AMD64" in version_text:
55
+ return "amd64"
56
+ if "AMD32" in version_text:
57
+ return "x86"
58
+ if machine is None:
59
+ machine = platform.machine()
60
+ mapped = {
61
+ "x86_64": "amd64",
62
+ "amd64": "amd64",
63
+ "aarch64": "arm64",
64
+ "arm64": "arm64",
65
+ "i386": "x86",
66
+ "i686": "x86",
67
+ "x86": "x86",
68
+ }
69
+ return mapped.get(machine.lower(), machine)
70
+
71
+ def libc_label(self, version_text, shell, detected=None):
72
+ """General Purpose: libc token used in the binary type, such as glibc."""
73
+ if detected is None:
74
+ try:
75
+ detected = platform.libc_ver()[0] or ""
76
+ except Exception:
77
+ detected = "unknown"
78
+ name = detected
79
+ if ("AMD64" in version_text or "AMD32" in version_text) and "MSC" in version_text:
80
+ name = "msc"
81
+ if "clang" in version_text.lower():
82
+ name = "clang"
83
+ if name == "" and shell == "/bin/ash":
84
+ name = "muslc"
85
+ return name
86
+
87
+ def binary_type(self, arch, libc):
88
+ """General Purpose: arch-libc token, such as amd64-glibc."""
89
+ if arch == "":
90
+ return ""
91
+ if libc == "":
92
+ return "{}-".format(arch)
93
+ return "{}-{}".format(arch, libc)
94
+
95
+ def current_user(self):
96
+ """General Purpose: Login name for the about system check."""
97
+ user = os.environ.get("USER") or os.environ.get("USERNAME") or ""
98
+ if user:
99
+ return user
100
+ try:
101
+ import getpass
102
+ return getpass.getuser()
103
+ except Exception:
104
+ return ""
105
+
106
+ def shell_text(self):
107
+ """General Purpose: The shell path from the environment."""
108
+ return os.environ.get("SHELL") or ""
109
+
110
+ def python_executable_name(self):
111
+ """General Purpose: Basename of the running Python executable."""
112
+ exe = sys.executable or ""
113
+ if "/" in exe:
114
+ return exe.rsplit("/", 1)[-1]
115
+ if "\\" in exe:
116
+ return exe.rsplit("\\", 1)[-1]
117
+ return exe
118
+
119
+ def command_location(self, name):
120
+ """General Purpose: PATH location of a tool, or an empty string."""
121
+ found = shutil.which(name)
122
+ return found or ""
123
+
124
+ def os_text(self):
125
+ """General Purpose: Pretty operating-system name, such as Ubuntu 24.04 LTS."""
126
+ try:
127
+ info = platform.freedesktop_os_release()
128
+ except Exception:
129
+ info = {}
130
+ pretty = info.get("PRETTY_NAME") or ""
131
+ if pretty:
132
+ return pretty
133
+ return platform.platform()
134
+
135
+ def inside_docker(self, marker="/.dockerenv"):
136
+ """General Purpose: True when the docker marker file is present."""
137
+ return os.path.exists(marker)
138
+
139
+ def cpython_soabi(self):
140
+ """General Purpose: CPython ABI string shown as the Cython string."""
141
+ value = sysconfig.get_config_var("SOABI") or ""
142
+ return value
143
+
144
+ def self_location(self):
145
+ """
146
+ General Purpose: Path of the running program.
147
+ A console-script argv wins. Otherwise the CLI module file.
148
+ The about page names that program file, so this read stays on it.
149
+ """
150
+ from .cli import APP_NAME, CONSOLE_NAME
151
+ from . import cli as cli_mod
152
+
153
+ here = os.path.realpath(cli_mod.__file__)
154
+ argv0 = sys.argv[0] if sys.argv else ""
155
+ if argv0 and os.path.isfile(argv0):
156
+ argv_real = os.path.realpath(argv0)
157
+ base = os.path.basename(argv_real)
158
+ if base in (CONSOLE_NAME, APP_NAME):
159
+ return argv_real
160
+ same_dir = os.path.dirname(argv_real) == os.path.dirname(here)
161
+ if same_dir and base in ("cli.py", "__main__.py"):
162
+ return argv_real
163
+ return here
164
+
165
+ def check_system_lines(self, now=None):
166
+ """
167
+ General Purpose: Host check block for the about page.
168
+ The stamp is local time with microseconds.
169
+ """
170
+ from .cli import APP_NAME, _PKG_VERSION
171
+
172
+ when = now if now is not None else datetime.datetime.now()
173
+ stamp = when.strftime("%Y-%m-%d %H:%M:%S.%f")
174
+ version_text = sys.version
175
+ py_text, c_library = self.parse_sys_version(
176
+ version_text, platform.python_version()
177
+ )
178
+ arch = self.arch_label(version_text)
179
+ shell = self.shell_text()
180
+ libc = self.libc_label(version_text, shell)
181
+ rows = [
182
+ "{} {}(v{}) [CHECK SYSTEM]:".format(stamp, APP_NAME, _PKG_VERSION),
183
+ " Now checking your operation system!",
184
+ " Python: {}".format(py_text),
185
+ " C Library: {}".format(c_library),
186
+ " Operation System: {}".format(self.os_text()),
187
+ " Architecture: {}".format(arch),
188
+ " Current User: {}".format(self.current_user()),
189
+ " Shell: {}".format(shell),
190
+ " Python Executable: {}".format(self.python_executable_name()),
191
+ " python2 location: {}".format(self.command_location("python2")),
192
+ " python3 location: {}".format(self.command_location("python3")),
193
+ " conda location: {}".format(self.command_location("conda")),
194
+ " pyenv location: {}".format(self.command_location("pyenv")),
195
+ " Inside docker container: {}".format(self.inside_docker()),
196
+ " Cython String: {}".format(self.cpython_soabi()),
197
+ " Binary Type: {}".format(self.binary_type(arch, libc)),
198
+ " Location: {}".format(self.self_location()),
199
+ ]
200
+ return rows