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.
- {videospeed-1.0.6/src/VideoSpeed.egg-info → videospeed-1.0.9}/PKG-INFO +25 -11
- {videospeed-1.0.6 → videospeed-1.0.9}/README.md +24 -10
- {videospeed-1.0.6 → videospeed-1.0.9}/pyproject.toml +1 -1
- {videospeed-1.0.6 → videospeed-1.0.9}/src/VideoSpeed/__init__.py +1 -1
- videospeed-1.0.9/src/VideoSpeed/about_page.py +201 -0
- videospeed-1.0.9/src/VideoSpeed/check_system.py +200 -0
- videospeed-1.0.9/src/VideoSpeed/cli.py +557 -0
- videospeed-1.0.9/src/VideoSpeed/edit_walk.py +428 -0
- videospeed-1.0.9/src/VideoSpeed/encoder.py +324 -0
- videospeed-1.0.9/src/VideoSpeed/file_stage.py +73 -0
- videospeed-1.0.9/src/VideoSpeed/media_info.py +67 -0
- videospeed-1.0.9/src/VideoSpeed/menu_model.py +211 -0
- videospeed-1.0.9/src/VideoSpeed/menu_painter.py +269 -0
- videospeed-1.0.9/src/VideoSpeed/menu_session.py +75 -0
- videospeed-1.0.9/src/VideoSpeed/run_output.py +110 -0
- videospeed-1.0.9/src/VideoSpeed/tui.py +387 -0
- {videospeed-1.0.6 → videospeed-1.0.9/src/VideoSpeed.egg-info}/PKG-INFO +25 -11
- {videospeed-1.0.6 → videospeed-1.0.9}/src/VideoSpeed.egg-info/SOURCES.txt +12 -1
- {videospeed-1.0.6 → videospeed-1.0.9}/tests/test_about.py +90 -32
- videospeed-1.0.9/tests/test_cli.py +452 -0
- {videospeed-1.0.6 → videospeed-1.0.9}/tests/test_docs.py +18 -2
- videospeed-1.0.9/tests/test_errors.py +49 -0
- videospeed-1.0.9/tests/test_fs.py +173 -0
- {videospeed-1.0.6 → videospeed-1.0.9}/tests/test_json.py +11 -9
- videospeed-1.0.9/tests/test_logging.py +286 -0
- {videospeed-1.0.6 → videospeed-1.0.9}/tests/test_prereq.py +11 -2
- videospeed-1.0.9/tests/test_tui.py +678 -0
- videospeed-1.0.6/src/VideoSpeed/cli.py +0 -1569
- videospeed-1.0.6/src/VideoSpeed/menu.py +0 -482
- videospeed-1.0.6/tests/test_cli.py +0 -158
- videospeed-1.0.6/tests/test_errors.py +0 -37
- videospeed-1.0.6/tests/test_fs.py +0 -68
- videospeed-1.0.6/tests/test_tui.py +0 -349
- {videospeed-1.0.6 → videospeed-1.0.9}/LICENSE.md +0 -0
- {videospeed-1.0.6 → videospeed-1.0.9}/setup.cfg +0 -0
- {videospeed-1.0.6 → videospeed-1.0.9}/src/VideoSpeed/__main__.py +0 -0
- {videospeed-1.0.6 → videospeed-1.0.9}/src/VideoSpeed/cli.bootstrap-old.py +0 -0
- {videospeed-1.0.6 → videospeed-1.0.9}/src/VideoSpeed.egg-info/dependency_links.txt +0 -0
- {videospeed-1.0.6 → videospeed-1.0.9}/src/VideoSpeed.egg-info/entry_points.txt +0 -0
- {videospeed-1.0.6 → videospeed-1.0.9}/src/VideoSpeed.egg-info/requires.txt +0 -0
- {videospeed-1.0.6 → videospeed-1.0.9}/src/VideoSpeed.egg-info/top_level.txt +0 -0
- {videospeed-1.0.6 → videospeed-1.0.9}/tests/test_build.py +0 -0
- {videospeed-1.0.6 → videospeed-1.0.9}/tests/test_dependencies.py +0 -0
- {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.
|
|
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
|
-

|
|
21
21
|

|
|
22
22
|
[-purple.svg)](https://github.com/cloudgen/ciao)
|
|
23
23
|
[](https://github.com/Wilgat/VideoSpeed)
|
|
24
24
|
[]()
|
|
25
25
|
[](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.
|
|
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
|
-
-
|
|
44
|
-
-
|
|
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.
|
|
96
|
+
pip install dist/VideoSpeed-1.0.9-py3-none-any.whl
|
|
96
97
|
# or
|
|
97
|
-
pip install dist/VideoSpeed-1.0.
|
|
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.
|
|
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
|
-

|
|
4
4
|

|
|
5
5
|
[-purple.svg)](https://github.com/cloudgen/ciao)
|
|
6
6
|
[](https://github.com/Wilgat/VideoSpeed)
|
|
7
7
|
[]()
|
|
8
8
|
[](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.
|
|
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
|
-
-
|
|
27
|
-
-
|
|
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.
|
|
79
|
+
pip install dist/VideoSpeed-1.0.9-py3-none-any.whl
|
|
79
80
|
# or
|
|
80
|
-
pip install dist/VideoSpeed-1.0.
|
|
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.
|
|
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.
|
|
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 =
|
|
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
|