VideoTubeEditor 1.0.1__py3-none-any.whl

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.
VideoTubeEditor/cli.py ADDED
@@ -0,0 +1,720 @@
1
+ #!/usr/bin/env python
2
+ # =============================================================================
3
+ # VideoTubeEditor CLI — text menu and typed verbs from the join and speed domains.
4
+ # Specialized from VideoJoin. Speed steps live beside the join steps.
5
+ # Law keys: requirement-python-cli-interface, requirement-python-cli-logging,
6
+ # requirement-python-tui, requirement-python-gui, requirement-python-oop,
7
+ # requirement-python-about, requirement-domain-videoeditor,
8
+ # requirement-python-error-handling, requirement-python-coding-style,
9
+ # requirement-runtime-prerequisites, requirement-video-ffmpeg-pipeline
10
+ # CIAO-Lite: Caution • Intentional • Anti-fragile • Over-protect
11
+ # def main writes ChronicleLogger(...). This module does not construct it
12
+ # at import time and does not re-export it.
13
+ # =============================================================================
14
+ from __future__ import print_function, unicode_literals
15
+
16
+ import argparse
17
+ import os
18
+ import sys
19
+ from pathlib import Path
20
+
21
+ from .about_page import AboutPage
22
+ from .check_system import CheckSystem
23
+ from .edit_walk import EditWalk
24
+ from .encoder import Encoder
25
+ from .file_stage import FileStage
26
+ from .gui import Gui
27
+ from .host_setup import HostSetup
28
+ from .join import Join
29
+ from .media_info import MediaInfo
30
+ from .run_output import RunOutput
31
+ from .self_management import SelfManage
32
+ from .tui import Tui
33
+
34
+
35
+ class Cli:
36
+ """Parser, dispatch, verbs, and the tty gate. One class in this file.
37
+
38
+ Identity, verb lists, and suffix sets are attributes of this class.
39
+ Collaborators arrive through the constructor. def main stays beside this class.
40
+ """
41
+
42
+ from . import __version__ as VERSION
43
+
44
+ APP_NAME = "VideoTubeEditor"
45
+ CONSOLE_NAME = "video-tube-editor"
46
+ PRODUCT_VERBS = (
47
+ "help",
48
+ "version",
49
+ "about",
50
+ "hello",
51
+ "tui",
52
+ "join",
53
+ "list-videos",
54
+ "edit",
55
+ "list-mp4",
56
+ "self-install",
57
+ "version-check",
58
+ "self-update",
59
+ "self-uninstall",
60
+ "setup",
61
+ )
62
+ LIFECYCLE_VERBS = (
63
+ "version",
64
+ "self-install",
65
+ "version-check",
66
+ "self-update",
67
+ "self-uninstall",
68
+ )
69
+ VIDEO_SUFFIXES = {".mp4", ".mov", ".mkv", ".avi", ".m4v"}
70
+ OUTPUT_SUFFIXES = (".mp4", ".mkv", ".mov")
71
+ AUTHOR_NAME = "Wilgat Wong"
72
+ AUTHOR_EMAIL = "wilgat.wong@gmail.com"
73
+ HOMEPAGE = "https://github.com/cloudgen/VideoTubeEditor"
74
+ LAST_UPDATE = "2026-10-12"
75
+ DOWNLOAD_URL = ""
76
+ BASIC_USAGE = "video-tube-editor join"
77
+ RATIO_MIN = 20.0
78
+ RATIO_MAX = 200.0
79
+
80
+ def __init__(self, logger=None):
81
+ self.logger = logger
82
+ if logger is not None:
83
+ logger.log_message("instantiated", component="Cli")
84
+ self.app_name = Cli.APP_NAME
85
+ self.version = Cli.VERSION
86
+ self.output = RunOutput(Cli.APP_NAME, Cli.VERSION, logger=logger)
87
+ self.join = Join(
88
+ logger=logger,
89
+ video_suffixes=Cli.VIDEO_SUFFIXES,
90
+ output_suffixes=Cli.OUTPUT_SUFFIXES,
91
+ )
92
+ self.stage = FileStage(logger=logger)
93
+ self.media = MediaInfo(self.output, logger=logger)
94
+ self.encoder = Encoder(
95
+ self.output,
96
+ self.media,
97
+ self.stage,
98
+ Cli.RATIO_MIN,
99
+ Cli.RATIO_MAX,
100
+ logger=logger,
101
+ )
102
+ self.edit = EditWalk(
103
+ self.output,
104
+ self.encoder,
105
+ self.media,
106
+ Cli.RATIO_MIN,
107
+ Cli.RATIO_MAX,
108
+ logger=logger,
109
+ )
110
+ self.about = AboutPage(
111
+ CheckSystem(
112
+ logger=logger,
113
+ app_name=Cli.APP_NAME,
114
+ version=Cli.VERSION,
115
+ console_name=Cli.CONSOLE_NAME,
116
+ ),
117
+ Cli.APP_NAME,
118
+ Cli.VERSION,
119
+ Cli.AUTHOR_NAME,
120
+ Cli.LAST_UPDATE,
121
+ Cli.HOMEPAGE,
122
+ Cli.DOWNLOAD_URL,
123
+ Cli.BASIC_USAGE,
124
+ Cli.CONSOLE_NAME,
125
+ logger=logger,
126
+ )
127
+ self.self_manage = SelfManage(Cli.APP_NAME, Cli.VERSION, logger=logger)
128
+ self.host_setup = HostSetup(logger=logger)
129
+ self.tui = Tui(self, logger=logger)
130
+ self.gui = Gui(logger=logger)
131
+
132
+ @staticmethod
133
+ def stdin_is_tty():
134
+ """General Purpose: Whether stdin can take interactive prompts."""
135
+ try:
136
+ return sys.stdin.isatty()
137
+ except Exception:
138
+ return False
139
+
140
+ @staticmethod
141
+ def stdout_is_tty():
142
+ """General Purpose: Whether the text screen can be drawn on this stdout."""
143
+ try:
144
+ return sys.stdout.isatty()
145
+ except Exception:
146
+ return False
147
+
148
+ @staticmethod
149
+ def opens_text_screen(argv):
150
+ """
151
+ General Purpose: Whether this argv draws the text screen.
152
+
153
+ The real parser has not run yet. This walk only decides is_quiet.
154
+ Help, version, hello, the pip verbs, and --json do not draw the screen.
155
+ The tui verb does, on a terminal. Empty argv, join, list-videos, about,
156
+ and list-mp4 do, on a terminal. edit does, unless --file, --start, and
157
+ --end are already present. --verbose alone still opens a screen.
158
+ A bare start with DISPLAY set is the window; this still returns True
159
+ so the logger stays quiet.
160
+ """
161
+ if argv is None:
162
+ argv = sys.argv[1:]
163
+ argv = list(argv)
164
+ if "--json" in argv or "--help" in argv or "-h" in argv or "--version" in argv:
165
+ return False
166
+ if not Cli.stdout_is_tty():
167
+ return False
168
+ verb = None
169
+ saw_force = False
170
+ has_file = False
171
+ has_start = False
172
+ has_end = False
173
+ has_folder = False
174
+ has_percent = False
175
+ has_boomerang = False
176
+ has_repeat = False
177
+ index = 0
178
+ while index < len(argv):
179
+ token = argv[index]
180
+ if token == "--force":
181
+ saw_force = True
182
+ elif token == "--verbose":
183
+ pass
184
+ elif token == "--file" or token.startswith("--file="):
185
+ has_file = True
186
+ if token == "--file":
187
+ index += 1
188
+ elif token == "--start" or token.startswith("--start="):
189
+ has_start = True
190
+ if token == "--start":
191
+ index += 1
192
+ elif token == "--end" or token.startswith("--end="):
193
+ has_end = True
194
+ if token == "--end":
195
+ index += 1
196
+ elif token == "--folder" or token.startswith("--folder="):
197
+ has_folder = True
198
+ if token == "--folder":
199
+ index += 1
200
+ elif token == "--percent" or token.startswith("--percent="):
201
+ has_percent = True
202
+ if token == "--percent":
203
+ index += 1
204
+ elif token == "--boomerang":
205
+ has_boomerang = True
206
+ elif token == "--repeat":
207
+ has_repeat = True
208
+ elif token.startswith("-"):
209
+ return False
210
+ elif verb is None:
211
+ verb = token
212
+ else:
213
+ return False
214
+ index += 1
215
+ if saw_force and verb != "self-uninstall":
216
+ return False
217
+ if verb in ("help", "hello", "setup") or verb in Cli.LIFECYCLE_VERBS:
218
+ return False
219
+ if verb == "tui":
220
+ return True
221
+ if verb == "edit":
222
+ return not (has_file and has_start and has_end)
223
+ if verb in ("join", "list-videos", "about", "list-mp4"):
224
+ return True
225
+ if verb is not None:
226
+ return False
227
+ if (
228
+ has_file or has_start or has_end or has_folder
229
+ or has_percent or has_boomerang or has_repeat
230
+ ):
231
+ return False
232
+ return True
233
+
234
+ @staticmethod
235
+ def opens_window(argv):
236
+ """
237
+ General Purpose: Bare interactive start on X11 opens the window.
238
+
239
+ DISPLAY is set for a local X session, XWayland, or ssh -X.
240
+ A verb, a job flag, --json, or a closed stdin stays off this path.
241
+ requirement-python-gui. On Windows the host contract is
242
+ requirement-python-windows. An empty DISPLAY does not remove this window.
243
+ """
244
+ if not os.environ.get("DISPLAY", "").strip():
245
+ return False
246
+ if not Cli.stdin_is_tty() or not Cli.stdout_is_tty():
247
+ return False
248
+ if argv is None:
249
+ argv = sys.argv[1:]
250
+ argv = list(argv)
251
+ if "--json" in argv or "--help" in argv or "-h" in argv or "--version" in argv:
252
+ return False
253
+ verb = None
254
+ has_job = False
255
+ index = 0
256
+ while index < len(argv):
257
+ token = argv[index]
258
+ if token in ("--force", "--verbose"):
259
+ pass
260
+ elif token in ("--boomerang", "--repeat"):
261
+ has_job = True
262
+ elif token in ("--file", "--start", "--end", "--folder", "--percent") or any(
263
+ token.startswith(prefix)
264
+ for prefix in ("--file=", "--start=", "--end=", "--folder=", "--percent=")
265
+ ):
266
+ has_job = True
267
+ if "=" not in token:
268
+ index += 1
269
+ elif token.startswith("-"):
270
+ return False
271
+ elif verb is None:
272
+ verb = token
273
+ else:
274
+ return False
275
+ index += 1
276
+ return verb is None and not has_job
277
+
278
+ def report_error(self, message, nxt):
279
+ """User-visible failure plus the same fact on the logger when one exists."""
280
+ if self.logger is not None:
281
+ self.logger.log_message(
282
+ "{0} Next: {1}".format(message, nxt),
283
+ level="ERROR",
284
+ component="main",
285
+ )
286
+ if self.output is not None:
287
+ self.output._remember(message, nxt)
288
+ print("ERROR: {0}".format(message), file=sys.stderr)
289
+ print(" Next: {0}".format(nxt), file=sys.stderr)
290
+ return 1
291
+
292
+ def build_parser(self):
293
+ """
294
+ General Purpose: One optional product verb, plus the edit job flags.
295
+ help is also --help. --force belongs only to self-uninstall.
296
+ Join stays on the current folder. Edit may name a file and a cut.
297
+ """
298
+ name = Cli.APP_NAME
299
+ dist = self.self_manage.DISTRIBUTION
300
+ parser = argparse.ArgumentParser(
301
+ prog=Cli.CONSOLE_NAME,
302
+ formatter_class=argparse.RawDescriptionHelpFormatter,
303
+ description=(
304
+ "{0} — join two videos, or cut, speed, and optionally boomerang or repeat an MP4.\n"
305
+ "With no arguments on a terminal and DISPLAY set, opens the window.\n"
306
+ "With no arguments on a terminal and no DISPLAY, opens the text menu.\n"
307
+ "With no arguments and no terminal, prints this help and stops.\n"
308
+ "tui opens the text menu on a terminal, including when DISPLAY is set.\n"
309
+ "Product verbs: help, version, about, hello, tui, join, list-videos,\n"
310
+ "edit, list-mp4, self-install, version-check, self-update,\n"
311
+ "self-uninstall, setup.\n"
312
+ "help prints this usage. version prints the installed version.\n"
313
+ "about opens the about page on a terminal. hello does not draw the menu.\n"
314
+ "join asks for two videos in this folder and an output name.\n"
315
+ "list-videos lists eligible videos and does not join.\n"
316
+ "edit asks for a folder, then a file, when those are not already named.\n"
317
+ "list-mp4 lists MP4 files and does not encode.\n"
318
+ "video-join and video-speed call this same program.\n"
319
+ "version-check runs: python -m pip index versions {1}\n"
320
+ "self-update runs: python -m pip install --upgrade {1}\n"
321
+ "self-install runs: python -m pip install {1}\n"
322
+ "self-uninstall runs: python -m pip uninstall -y {1}\n"
323
+ "and needs --force. setup installs the declared pip libraries.\n"
324
+ "On Windows, setup also tries curses and ffmpeg.\n"
325
+ "Empty arguments do not install or update.\n"
326
+ "Join formats: .mp4 .mov .mkv .avi .m4v.\n"
327
+ "Edit input: MP4. For a script, pass edit --file, --start, and --end.\n"
328
+ "--json prints one object and does not open the text menu.\n"
329
+ "This product does not cut, speed, or boomerang inside join."
330
+ .format(name, dist)
331
+ ),
332
+ )
333
+ parser.add_argument(
334
+ "verb",
335
+ nargs="?",
336
+ default=None,
337
+ metavar="verb",
338
+ help=(
339
+ "Product verb: help, version, about, hello, tui, join, list-videos, "
340
+ "edit, list-mp4, self-install, version-check, self-update, "
341
+ "self-uninstall, or setup"
342
+ ),
343
+ )
344
+ parser.add_argument(
345
+ "--version",
346
+ action="version",
347
+ version="{0} {1}".format(Cli.APP_NAME, Cli.VERSION),
348
+ )
349
+ parser.add_argument("--file", help="MP4 path for a non-interactive edit")
350
+ parser.add_argument(
351
+ "--folder",
352
+ help="Folder of MP4s for list-mp4 or an edit that still needs a file",
353
+ )
354
+ parser.add_argument(
355
+ "--start",
356
+ type=float,
357
+ help="Cut start seconds (with --file and --end)",
358
+ )
359
+ parser.add_argument(
360
+ "--end",
361
+ type=float,
362
+ help="Cut end seconds (with --file and --start)",
363
+ )
364
+ parser.add_argument(
365
+ "--percent",
366
+ type=float,
367
+ default=None,
368
+ help="New length percent of the cut (default 100; allowed 20–200)",
369
+ )
370
+ parser.add_argument(
371
+ "--boomerang",
372
+ action="store_true",
373
+ help="Append the reverse after the forward clip. Not with --repeat",
374
+ )
375
+ parser.add_argument(
376
+ "--repeat",
377
+ action="store_true",
378
+ help="Append the forward clip again so it plays twice. Not with --boomerang",
379
+ )
380
+ parser.add_argument(
381
+ "--json",
382
+ action="store_true",
383
+ help="Quiet stdout and print one JSON object. Does not open the text menu.",
384
+ )
385
+ parser.add_argument(
386
+ "--verbose",
387
+ action="store_true",
388
+ help="Accepted. A text screen and --json stay quiet.",
389
+ )
390
+ parser.add_argument(
391
+ "--force",
392
+ action="store_true",
393
+ help="Confirm self-uninstall. Required on the command line.",
394
+ )
395
+ return parser
396
+
397
+ def _page(self, text):
398
+ stream = sys.stderr if self.output.json_mode else sys.stdout
399
+ print(text, file=stream)
400
+ return 0
401
+
402
+ def _verb_hello(self):
403
+ """Hello from this package, then the help next step. Does not draw the menu."""
404
+ stream = sys.stderr if self.output.json_mode else sys.stdout
405
+ print(self.tui.hello_text(), file=stream)
406
+ print("Next: {0} help".format(Cli.CONSOLE_NAME), file=stream)
407
+ return 0
408
+
409
+ def _unknown_verb(self, token):
410
+ names = ", ".join(Cli.PRODUCT_VERBS)
411
+ return self.report_error(
412
+ "Unknown verb '{0}'.".format(token),
413
+ "{0} help — verbs: {1}".format(Cli.CONSOLE_NAME, names),
414
+ )
415
+
416
+ def _print_video_list(self):
417
+ """Eligible names on the console. Does not join and does not draw the screen."""
418
+ stream = sys.stderr if self.output.json_mode else sys.stdout
419
+ print(self.tui._video_listing(), file=stream)
420
+ return 0
421
+
422
+ def _print_mp4_list(self, folder):
423
+ """Numbered MP4 names. Does not encode."""
424
+ folder_path = Path(folder)
425
+ if not folder_path.is_dir():
426
+ return self.report_error(
427
+ "Not a directory: {0}".format(folder),
428
+ "{0} list-mp4 --folder .".format(Cli.CONSOLE_NAME),
429
+ )
430
+ files = self.media.get_mp4_files(folder_path)
431
+ if not files:
432
+ return self.report_error(
433
+ "No MP4 files found!",
434
+ "{0} list-mp4 --folder .".format(Cli.CONSOLE_NAME),
435
+ )
436
+ lines = ["{0:2d}. {1}".format(i, item.name) for i, item in enumerate(files, 1)]
437
+ stream = sys.stderr if self.output.json_mode else sys.stdout
438
+ for line in lines:
439
+ print(line, file=stream)
440
+ return 0
441
+
442
+ def _job_ready(self, args):
443
+ return args.file is not None and args.start is not None and args.end is not None
444
+
445
+ def _tail_from_args(self, args):
446
+ """One tail. The caller rejects --boomerang together with --repeat."""
447
+ if args.repeat:
448
+ return Encoder.TAIL_REPEAT
449
+ if args.boomerang:
450
+ return Encoder.TAIL_BOOMERANG
451
+ return Encoder.TAIL_NONE
452
+
453
+ def _reject_both_tails(self, args):
454
+ """Fail closed before encode when both tail flags are set."""
455
+ if args.boomerang and args.repeat:
456
+ return self.report_error(
457
+ "Choose one tail: --boomerang or --repeat.",
458
+ "{0} edit --file clip.mp4 --start 0 --end 5 --repeat".format(
459
+ Cli.CONSOLE_NAME
460
+ ),
461
+ )
462
+ return None
463
+
464
+ def _run_batch(self, args):
465
+ percent = 100.0 if args.percent is None else args.percent
466
+ return self.encoder.batch_session(
467
+ args.file, args.start, args.end, percent, self._tail_from_args(args)
468
+ )
469
+
470
+ def _verb_edit(self, args):
471
+ """One edit job, or the edit questions when a target is missing."""
472
+ if self._job_ready(args):
473
+ return self._run_batch(args)
474
+ if not self.stdin_is_tty():
475
+ return self.report_error(
476
+ "Non-interactive job needs --file, --start, and --end.",
477
+ "{0} edit --file clip.mp4 --start 0 --end 5".format(Cli.CONSOLE_NAME),
478
+ )
479
+ if self.output.json_mode:
480
+ return self.edit._edit_json(args)
481
+ if self.logger is not None:
482
+ self.logger.quiet(True)
483
+ return self.tui.open_direct("edit", args)
484
+
485
+ def _verb_list_mp4(self, args):
486
+ """List MP4 files and stop. Does not encode."""
487
+ if args.folder is None and args.file is not None:
488
+ args.folder = str(Path(args.file).parent)
489
+ if not self.stdin_is_tty():
490
+ return self._print_mp4_list("." if args.folder is None else args.folder)
491
+ if self.output.json_mode:
492
+ if args.folder is None:
493
+ self.output.json_walk = True
494
+ raw = self.edit._prompt_line("Folder (Enter = current):")
495
+ if raw is None:
496
+ return 0
497
+ args.folder = raw.strip() or "."
498
+ return self._print_mp4_list(args.folder)
499
+ if self.logger is not None:
500
+ self.logger.quiet(True)
501
+ return self.tui.open_direct("list-mp4", args)
502
+
503
+ def _has_selectors(self, args):
504
+ return (
505
+ args.file is not None
506
+ or args.start is not None
507
+ or args.end is not None
508
+ or args.folder is not None
509
+ )
510
+
511
+ def _dispatch(self, args):
512
+ """One product verb, one edit job, the front board, or a fail-closed stop."""
513
+ verb = args.verb
514
+ json_mode = self.output.json_mode
515
+ if args.force and verb != "self-uninstall":
516
+ return self.report_error(
517
+ "--force is only for self-uninstall.",
518
+ "{0} self-uninstall --force".format(Cli.CONSOLE_NAME),
519
+ )
520
+ rejected = self._reject_both_tails(args)
521
+ if rejected is not None:
522
+ return rejected
523
+ if verb == "about":
524
+ if json_mode or not self.stdout_is_tty():
525
+ return self._page(self.tui.about_text())
526
+ result = self.tui.about_on_screen()
527
+ if isinstance(result, int):
528
+ return result
529
+ return self._page(result)
530
+ if verb == "hello":
531
+ return self._verb_hello()
532
+ if verb == "tui":
533
+ if json_mode or not self.stdin_is_tty() or not self.stdout_is_tty():
534
+ return self.report_error(
535
+ "No terminal for the text menu.",
536
+ "{0} tui".format(Cli.CONSOLE_NAME),
537
+ )
538
+ if self.logger is not None:
539
+ self.logger.quiet(True)
540
+ action = self.tui.open_text_menu()
541
+ if action == "missing":
542
+ return 1
543
+ return 0
544
+ if verb == "version":
545
+ return self._page(self.self_manage.local_version())
546
+ if verb == "self-uninstall":
547
+ if not args.force:
548
+ return self.report_error(
549
+ "self-uninstall removes this package with pip.",
550
+ "{0} self-uninstall --force".format(Cli.CONSOLE_NAME),
551
+ )
552
+ return self.self_manage.emit(verb, json_mode=json_mode)
553
+ if verb in ("version-check", "self-update", "self-install"):
554
+ return self.self_manage.emit(verb, json_mode=json_mode)
555
+ if verb == "setup":
556
+ return self.host_setup.run(
557
+ window_lines=self.about.window_library_lines()
558
+ )
559
+ if verb == "join":
560
+ if json_mode or not self.stdout_is_tty():
561
+ return self.report_error(
562
+ "No terminal for join. Use a terminal.",
563
+ "{0} join".format(Cli.CONSOLE_NAME),
564
+ )
565
+ return self.tui.open_direct("join")
566
+ if verb == "list-videos":
567
+ if json_mode or not self.stdout_is_tty():
568
+ return self._print_video_list()
569
+ result = self.tui.list_on_screen()
570
+ if isinstance(result, int):
571
+ return result
572
+ return self._page(result)
573
+ if verb == "edit":
574
+ return self._verb_edit(args)
575
+ if verb == "list-mp4":
576
+ return self._verb_list_mp4(args)
577
+ if verb is not None:
578
+ return self._unknown_verb(verb)
579
+
580
+ selectors = self._has_selectors(args)
581
+ if (args.percent is not None or args.boomerang or args.repeat) and not selectors:
582
+ return self.report_error(
583
+ "--percent, --boomerang, and --repeat need --file, --start, and --end.",
584
+ "{0} edit --file clip.mp4 --start 0 --end 5".format(Cli.CONSOLE_NAME),
585
+ )
586
+ if selectors:
587
+ if args.folder is not None and args.file is None:
588
+ folder_path = Path(args.folder)
589
+ if not folder_path.is_dir():
590
+ return self.report_error(
591
+ "Not a directory: {0}".format(args.folder),
592
+ "{0} list-mp4 --folder .".format(Cli.CONSOLE_NAME),
593
+ )
594
+ files = self.media.get_mp4_files(folder_path)
595
+ if not files:
596
+ return self.report_error(
597
+ "No MP4 files found!",
598
+ "{0} list-mp4 --folder .".format(Cli.CONSOLE_NAME),
599
+ )
600
+ return self.report_error(
601
+ "--folder listed {0} MP4 file(s); also pass --file, --start, and --end.".format(
602
+ len(files)
603
+ ),
604
+ "{0} edit --file clip.mp4 --start 0 --end 5".format(Cli.CONSOLE_NAME),
605
+ )
606
+ if not self._job_ready(args):
607
+ return self.report_error(
608
+ "Non-interactive job needs --file, --start, and --end.",
609
+ "{0} edit --file clip.mp4 --start 0 --end 5".format(Cli.CONSOLE_NAME),
610
+ )
611
+ return self._run_batch(args)
612
+ if not self.stdout_is_tty():
613
+ self.build_parser().print_help()
614
+ return 0
615
+ if self.logger is not None:
616
+ self.logger.quiet(True)
617
+ if Cli.opens_window([]):
618
+ code = self.gui.open(self)
619
+ if code != 0:
620
+ return self.report_error(
621
+ self.gui.failure or "The window did not open.",
622
+ "{0} tui".format(Cli.CONSOLE_NAME),
623
+ )
624
+ return 0
625
+ action = self.tui.open_text_menu()
626
+ if action == "missing":
627
+ return 1
628
+ return 0
629
+
630
+ def run(self, argv=None):
631
+ """One job. def main already wrote ChronicleLogger(...) and passed it in."""
632
+ self.output._json_reset()
633
+ if argv is None:
634
+ argv = sys.argv[1:]
635
+ argv = list(argv)
636
+ parser = self.build_parser()
637
+ try:
638
+ args = parser.parse_args(argv)
639
+ except SystemExit as exc:
640
+ code = exc.code
641
+ if code is None or code == 0:
642
+ return 0
643
+ return int(code)
644
+ if args.json:
645
+ self.output.json_mode = True
646
+ bare_json_help = (
647
+ args.json
648
+ and args.verb is None
649
+ and not self._has_selectors(args)
650
+ and args.percent is None
651
+ and not args.boomerang
652
+ and not args.repeat
653
+ )
654
+ if args.verb == "help" or bare_json_help:
655
+ if args.json:
656
+ parser.print_help(file=sys.stderr)
657
+ code = self.output._emit_json("noninteractive", 0)
658
+ self.output.json_mode = False
659
+ return code
660
+ parser.print_help()
661
+ return 0
662
+ code = self._dispatch(args)
663
+ if not args.json:
664
+ return code
665
+ mode = "interactive" if self.output.json_walk else "noninteractive"
666
+ code = self.output._emit_json(mode, code)
667
+ self.output.json_mode = False
668
+ return code
669
+
670
+
671
+ def main(argv=None, basedir="", logdir=""):
672
+ """This function instantiates ChronicleLogger. The statement below is the construct."""
673
+ if argv is None:
674
+ argv = sys.argv[1:]
675
+ argv = list(argv)
676
+ from VideoTubeEditor import __version__
677
+
678
+ HostSetup.ensure_geteuid()
679
+ try:
680
+ from ChronicleLogger import ChronicleLogger
681
+ except ImportError:
682
+ sys.stderr.write(
683
+ 'ChronicleLogger is required. Next step: python -m pip install "ChronicleLogger>=1.3.1"\n'
684
+ )
685
+ return 1
686
+
687
+ screen = Cli.opens_text_screen(argv) or Cli.opens_window(argv)
688
+ quiet = screen or ("--json" in argv)
689
+ logger = ChronicleLogger(
690
+ logname="VideoTubeEditor",
691
+ is_quiet=quiet,
692
+ basedir=basedir,
693
+ logdir=logdir,
694
+ )
695
+ appname = logger.logName()
696
+ resolved_base = logger.baseDir()
697
+ logger.logDir()
698
+
699
+ if logger.isDebug():
700
+ logger.log_message(
701
+ "{0} v{1} ({2})".format(appname, __version__, __file__),
702
+ component="main",
703
+ )
704
+ logger.log_message(
705
+ "Using {0}".format(ChronicleLogger.class_version()),
706
+ component="main",
707
+ )
708
+ logger.log_message(
709
+ "Base {0}".format(resolved_base),
710
+ level="DEBUG",
711
+ component="main",
712
+ )
713
+ logger.log_message("debug mode", component="main")
714
+
715
+ app = Cli(logger)
716
+ return app.run(argv)
717
+
718
+
719
+ if __name__ == "__main__":
720
+ sys.exit(main())