DVR-Scan-Headless 1.8__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.
- dvr_scan/LICENSE +47 -0
- dvr_scan/__init__.py +55 -0
- dvr_scan/__main__.py +80 -0
- dvr_scan/app/__init__.py +10 -0
- dvr_scan/app/__main__.py +218 -0
- dvr_scan/app/about_window.py +137 -0
- dvr_scan/app/application.py +1700 -0
- dvr_scan/app/common.py +31 -0
- dvr_scan/app/region_editor.py +1416 -0
- dvr_scan/app/scan_window.py +258 -0
- dvr_scan/app/widgets.py +213 -0
- dvr_scan/cli.py +511 -0
- dvr_scan/config.py +607 -0
- dvr_scan/controller.py +215 -0
- dvr_scan/detector.py +124 -0
- dvr_scan/docs/404.html +1 -0
- dvr_scan/docs/assets/javascripts/bundle.88dd0f4e.min.js +15 -0
- dvr_scan/docs/assets/stylesheets/main.6f8fc17f.min.css +1 -0
- dvr_scan/docs/assets/stylesheets/palette.06af60db.min.css +1 -0
- dvr_scan/docs/changelog.html +1 -0
- dvr_scan/docs/docs.html +73 -0
- dvr_scan/docs/faq.html +13 -0
- dvr_scan/docs/guide.html +1 -0
- dvr_scan/docs/index.html +5 -0
- dvr_scan/docs/resources.html +30 -0
- dvr_scan/docs/style.min.css +1 -0
- dvr_scan/dvr-scan-logo.png +0 -0
- dvr_scan/dvr-scan.ico +0 -0
- dvr_scan/dvr-scan.png +0 -0
- dvr_scan/opencv_loader.py +37 -0
- dvr_scan/overlays.py +251 -0
- dvr_scan/platform.py +236 -0
- dvr_scan/region.py +65 -0
- dvr_scan/scanner.py +1303 -0
- dvr_scan/shared/__init__.py +287 -0
- dvr_scan/shared/cli.py +291 -0
- dvr_scan/shared/settings.py +72 -0
- dvr_scan/subtractor.py +163 -0
- dvr_scan/video_joiner.py +188 -0
- dvr_scan_headless-1.8.dist-info/METADATA +119 -0
- dvr_scan_headless-1.8.dist-info/RECORD +45 -0
- dvr_scan_headless-1.8.dist-info/WHEEL +5 -0
- dvr_scan_headless-1.8.dist-info/entry_points.txt +2 -0
- dvr_scan_headless-1.8.dist-info/licenses/LICENSE +47 -0
- dvr_scan_headless-1.8.dist-info/top_level.txt +1 -0
dvr_scan/LICENSE
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
-----------------------------------------------------------------------
|
|
2
|
+
DVR-Scan License (BSD 2-Clause)
|
|
3
|
+
< https://github.com/Breakthrough/DVR-Scan >
|
|
4
|
+
-----------------------------------------------------------------------
|
|
5
|
+
|
|
6
|
+
Copyright (C) 2016, Brandon Castellano.
|
|
7
|
+
All rights reserved.
|
|
8
|
+
|
|
9
|
+
Redistribution and use in source and binary forms, with or without
|
|
10
|
+
modification, are permitted provided that the following conditions
|
|
11
|
+
are met:
|
|
12
|
+
|
|
13
|
+
1. Redistributions of source code must retain the above copyright
|
|
14
|
+
notice, this list of conditions and the following disclaimer.
|
|
15
|
+
|
|
16
|
+
2. Redistributions in binary form must reproduce the above
|
|
17
|
+
copyright notice, this list of conditions and the following
|
|
18
|
+
disclaimer in the documentation and/or other materials
|
|
19
|
+
provided with the distribution.
|
|
20
|
+
|
|
21
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
22
|
+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
23
|
+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
24
|
+
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
25
|
+
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
26
|
+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
27
|
+
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
28
|
+
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
29
|
+
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
30
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
31
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
32
|
+
|
|
33
|
+
-----------------------------------------------------------------------
|
|
34
|
+
Ancillary Software Licenses
|
|
35
|
+
-----------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
DVR-Scan uses the following third-party software components:
|
|
38
|
+
|
|
39
|
+
> NumPy [Copyright (C) 2005-2022, Numpy Developers]
|
|
40
|
+
> OpenCV [Copyright (C) 2022, OpenCV Team]
|
|
41
|
+
> FFmpeg [Copyright (C) 2001, Fabrice Bellard]
|
|
42
|
+
> CUDA [Copyright (C) 2020, Nvidia Corporation]
|
|
43
|
+
|
|
44
|
+
Some binary distributions may redistribute these components. See the
|
|
45
|
+
included LICENSE-THIRDPARTY file for details. By downloading, copying,
|
|
46
|
+
or using this software, you agree to the terms of this license and all
|
|
47
|
+
third-party software licenses.
|
dvr_scan/__init__.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#
|
|
2
|
+
# DVR-Scan: Video Motion Event Detection & Extraction Tool
|
|
3
|
+
# --------------------------------------------------------------
|
|
4
|
+
# [ Site: https://www.dvr-scan.com/ ]
|
|
5
|
+
# [ Repo: https://github.com/Breakthrough/DVR-Scan ]
|
|
6
|
+
#
|
|
7
|
+
# Copyright (C) 2016 Brandon Castellano <http://www.bcastell.com>.
|
|
8
|
+
# DVR-Scan is licensed under the BSD 2-Clause License; see the included
|
|
9
|
+
# LICENSE file, or visit one of the above pages for details.
|
|
10
|
+
#
|
|
11
|
+
"""``dvr_scan`` Module
|
|
12
|
+
|
|
13
|
+
This is the main DVR-Scan module containing all application logic,
|
|
14
|
+
motion detection implementation, and command line processing. The
|
|
15
|
+
main modules under `dvr_scan` are organized as follows:
|
|
16
|
+
|
|
17
|
+
``cli``: command-line interface
|
|
18
|
+
|
|
19
|
+
``scanner``: scans a video for motion and extracts events
|
|
20
|
+
|
|
21
|
+
``detector``: motion detection algorithms
|
|
22
|
+
|
|
23
|
+
``overlays``: overlays which can be drawn when outputting events
|
|
24
|
+
|
|
25
|
+
There are also a few helper modules:
|
|
26
|
+
|
|
27
|
+
``video_joiner``: concatenates multiple input videos
|
|
28
|
+
|
|
29
|
+
``opencv_loader``: helper for resolving dynamic libraries used by OpenCV
|
|
30
|
+
|
|
31
|
+
``platform``: library/platform specific helpers
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
import pkgutil
|
|
35
|
+
import sys
|
|
36
|
+
|
|
37
|
+
# Used for module/distribution identification.
|
|
38
|
+
__version__ = "1.8"
|
|
39
|
+
|
|
40
|
+
# Handle loading OpenCV. This **MUST** be first before any other DVR-Scan or third-party
|
|
41
|
+
# packages are imported which might attempt to import the `cv2` module.
|
|
42
|
+
import dvr_scan.opencv_loader as _ # noqa: F401
|
|
43
|
+
from dvr_scan.platform import init_logger
|
|
44
|
+
|
|
45
|
+
# Initialize logger.
|
|
46
|
+
init_logger(show_stdout=True)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_license_info() -> str:
|
|
50
|
+
"""Get license/copyright information for the package or standalone executable."""
|
|
51
|
+
license_text = pkgutil.get_data(__name__, "LICENSE").decode()
|
|
52
|
+
# Include additional third-party license text if they were bundled into this release.
|
|
53
|
+
if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
|
|
54
|
+
license_text += pkgutil.get_data(__name__, "LICENSE-THIRDPARTY").decode()
|
|
55
|
+
return license_text
|
dvr_scan/__main__.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#
|
|
2
|
+
# DVR-Scan: Video Motion Event Detection & Extraction Tool
|
|
3
|
+
# --------------------------------------------------------------
|
|
4
|
+
# [ Site: https://www.dvr-scan.com/ ]
|
|
5
|
+
# [ Repo: https://github.com/Breakthrough/DVR-Scan ]
|
|
6
|
+
#
|
|
7
|
+
# Copyright (C) 2016 Brandon Castellano <http://www.bcastell.com>.
|
|
8
|
+
# DVR-Scan is licensed under the BSD 2-Clause License; see the included
|
|
9
|
+
# LICENSE file, or visit one of the above pages for details.
|
|
10
|
+
#
|
|
11
|
+
"""``dvr_scan.__main__`` Module
|
|
12
|
+
|
|
13
|
+
Provides entry point for DVR-Scan's command-line interface (CLI).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
import sys
|
|
18
|
+
from subprocess import CalledProcessError
|
|
19
|
+
|
|
20
|
+
from scenedetect import VideoOpenFailure
|
|
21
|
+
|
|
22
|
+
from dvr_scan.controller import parse_settings, run_dvr_scan
|
|
23
|
+
from dvr_scan.shared import logging_redirect_tqdm
|
|
24
|
+
|
|
25
|
+
EXIT_SUCCESS: int = 0
|
|
26
|
+
EXIT_ERROR: int = 1
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def main():
|
|
30
|
+
"""Main entry-point for DVR-Scan."""
|
|
31
|
+
settings = parse_settings()
|
|
32
|
+
if settings is None:
|
|
33
|
+
sys.exit(EXIT_ERROR)
|
|
34
|
+
logger = logging.getLogger("dvr_scan")
|
|
35
|
+
# TODO(1.7): The logging redirect does not respect the original log level, which is now set to
|
|
36
|
+
# DEBUG mode for rolling log files.
|
|
37
|
+
# We might have to just roll our own instead of relying on this one.
|
|
38
|
+
# TODO: Use Python __debug__ mode instead of hard-coding as config option.
|
|
39
|
+
debug_mode = settings.get("debug")
|
|
40
|
+
show_traceback = getattr(logging, settings.get("verbosity").upper()) == logging.DEBUG
|
|
41
|
+
with logging_redirect_tqdm(loggers=[logger]):
|
|
42
|
+
try:
|
|
43
|
+
run_dvr_scan(settings)
|
|
44
|
+
except ValueError as ex:
|
|
45
|
+
logger.critical("Setting Error: %s", str(ex), exc_info=show_traceback)
|
|
46
|
+
if debug_mode:
|
|
47
|
+
raise
|
|
48
|
+
except VideoOpenFailure as ex:
|
|
49
|
+
logger.critical("Failed to load input: %s", str(ex), exc_info=show_traceback)
|
|
50
|
+
if debug_mode:
|
|
51
|
+
raise
|
|
52
|
+
except KeyboardInterrupt:
|
|
53
|
+
logger.info("Stopping (interrupt received)...", exc_info=show_traceback)
|
|
54
|
+
if debug_mode:
|
|
55
|
+
raise
|
|
56
|
+
except CalledProcessError as ex:
|
|
57
|
+
logger.error(
|
|
58
|
+
"Failed to run command:\n %s\nCommand returned %d, output:\n\n%s",
|
|
59
|
+
" ".join(ex.cmd),
|
|
60
|
+
ex.returncode,
|
|
61
|
+
ex.output,
|
|
62
|
+
exc_info=show_traceback,
|
|
63
|
+
)
|
|
64
|
+
if debug_mode:
|
|
65
|
+
raise
|
|
66
|
+
except NotImplementedError as ex:
|
|
67
|
+
logger.critical("Error (Not Implemented): %s", str(ex), exc_info=show_traceback)
|
|
68
|
+
if debug_mode:
|
|
69
|
+
raise
|
|
70
|
+
except Exception as ex:
|
|
71
|
+
logger.critical("Critical Error: %s", str(ex), exc_info=True)
|
|
72
|
+
if debug_mode:
|
|
73
|
+
raise
|
|
74
|
+
else:
|
|
75
|
+
sys.exit(EXIT_SUCCESS)
|
|
76
|
+
sys.exit(EXIT_ERROR)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
if __name__ == "__main__":
|
|
80
|
+
main()
|
dvr_scan/app/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#
|
|
2
|
+
# DVR-Scan: Video Motion Event Detection & Extraction Tool
|
|
3
|
+
# --------------------------------------------------------------
|
|
4
|
+
# [ Site: https://www.dvr-scan.com/ ]
|
|
5
|
+
# [ Repo: https://github.com/Breakthrough/DVR-Scan ]
|
|
6
|
+
#
|
|
7
|
+
# Copyright (C) 2024 Brandon Castellano <http://www.bcastell.com>.
|
|
8
|
+
# DVR-Scan is licensed under the BSD 2-Clause License; see the included
|
|
9
|
+
# LICENSE file, or visit one of the above pages for details.
|
|
10
|
+
#
|
dvr_scan/app/__main__.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
#
|
|
2
|
+
# DVR-Scan: Video Motion Event Detection & Extraction Tool
|
|
3
|
+
# --------------------------------------------------------------
|
|
4
|
+
# [ Site: https://www.dvr-scan.com/ ]
|
|
5
|
+
# [ Repo: https://github.com/Breakthrough/DVR-Scan ]
|
|
6
|
+
#
|
|
7
|
+
# Copyright (C) 2024 Brandon Castellano <http://www.bcastell.com>.
|
|
8
|
+
# DVR-Scan is licensed under the BSD 2-Clause License; see the included
|
|
9
|
+
# LICENSE file, or visit one of the above pages for details.
|
|
10
|
+
#
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import logging
|
|
14
|
+
import sys
|
|
15
|
+
from subprocess import CalledProcessError
|
|
16
|
+
|
|
17
|
+
from scenedetect import VideoOpenFailure
|
|
18
|
+
|
|
19
|
+
import dvr_scan
|
|
20
|
+
from dvr_scan import get_license_info
|
|
21
|
+
from dvr_scan.app.application import Application
|
|
22
|
+
from dvr_scan.config import CHOICE_MAP, USER_CONFIG_FILE_PATH, ConfigLoadFailure, ConfigRegistry
|
|
23
|
+
from dvr_scan.shared import (
|
|
24
|
+
ScanSettings,
|
|
25
|
+
init_logging,
|
|
26
|
+
logfile_path,
|
|
27
|
+
logging_redirect_tqdm,
|
|
28
|
+
setup_logger,
|
|
29
|
+
)
|
|
30
|
+
from dvr_scan.shared.cli import VERSION_STRING, LicenseAction, VersionAction, string_type_check
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger("dvr_scan")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
EXIT_SUCCESS: int = 0
|
|
36
|
+
EXIT_ERROR: int = 1
|
|
37
|
+
|
|
38
|
+
LOGFILE_PATH = logfile_path(name_prefix="dvr-scan-app")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_cli_parser():
|
|
42
|
+
parser = argparse.ArgumentParser(
|
|
43
|
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
|
44
|
+
argument_default=argparse.SUPPRESS,
|
|
45
|
+
)
|
|
46
|
+
if hasattr(parser, "_optionals"):
|
|
47
|
+
parser._optionals.title = "arguments"
|
|
48
|
+
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
"-c",
|
|
51
|
+
"--config",
|
|
52
|
+
metavar="dvr-scan.cfg",
|
|
53
|
+
type=str,
|
|
54
|
+
help=(
|
|
55
|
+
"Config file to load scan settings from. If not set, tries to load one from %s"
|
|
56
|
+
% USER_CONFIG_FILE_PATH
|
|
57
|
+
),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
parser.add_argument(
|
|
61
|
+
"-V",
|
|
62
|
+
"--version",
|
|
63
|
+
action=VersionAction,
|
|
64
|
+
version=VERSION_STRING,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
parser.add_argument(
|
|
68
|
+
"-v",
|
|
69
|
+
"--verbosity",
|
|
70
|
+
metavar="type",
|
|
71
|
+
type=string_type_check(CHOICE_MAP["verbosity"], False, "type"),
|
|
72
|
+
help=(
|
|
73
|
+
"Verbosity type to use for log output. Must be one of: %s."
|
|
74
|
+
% (", ".join(CHOICE_MAP["verbosity"]),)
|
|
75
|
+
),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
parser.add_argument(
|
|
79
|
+
"--logfile",
|
|
80
|
+
metavar="file",
|
|
81
|
+
type=str,
|
|
82
|
+
help=(
|
|
83
|
+
"Appends application output to file. If file does not exist it will be created. "
|
|
84
|
+
f"Log path: {LOGFILE_PATH.parent}"
|
|
85
|
+
),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
parser.add_argument(
|
|
89
|
+
"-L",
|
|
90
|
+
"--license",
|
|
91
|
+
action=LicenseAction,
|
|
92
|
+
version=get_license_info(),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
parser.add_argument(
|
|
96
|
+
"--debug",
|
|
97
|
+
action="store_true",
|
|
98
|
+
help=argparse.SUPPRESS,
|
|
99
|
+
default=False,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
parser.add_argument(
|
|
103
|
+
"input",
|
|
104
|
+
nargs="*",
|
|
105
|
+
type=str,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return parser
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# TODO: There's a lot of duplicated code here between the CLI and GUI. See if we can combine some
|
|
112
|
+
# of the handling of config file loading and exceptions to be consistent between the two.
|
|
113
|
+
#
|
|
114
|
+
# It would also be nice if both commands took the same set of arguments. Can probably re-use the
|
|
115
|
+
# existing CLI parser.
|
|
116
|
+
def main():
|
|
117
|
+
"""Parse command line options and load config file settings."""
|
|
118
|
+
# We defer printing the debug log until we know where to put it.
|
|
119
|
+
init_log = []
|
|
120
|
+
failed_to_load_config = True
|
|
121
|
+
config = ConfigRegistry()
|
|
122
|
+
config_load_error = None
|
|
123
|
+
# Create debug log handler and try to load the user config file.
|
|
124
|
+
try:
|
|
125
|
+
user_config = ConfigRegistry()
|
|
126
|
+
user_config.load()
|
|
127
|
+
config = user_config
|
|
128
|
+
except ConfigLoadFailure as ex:
|
|
129
|
+
config_load_error = ex
|
|
130
|
+
# Parse CLI args, override config if an override was specified on the command line.
|
|
131
|
+
try:
|
|
132
|
+
args = get_cli_parser().parse_args()
|
|
133
|
+
init_logging(args, config)
|
|
134
|
+
init_log += [(logging.INFO, "DVR-Scan Application %s" % dvr_scan.__version__)]
|
|
135
|
+
if config_load_error and not hasattr(args, "config"):
|
|
136
|
+
raise config_load_error
|
|
137
|
+
if hasattr(args, "config"):
|
|
138
|
+
config_setting = ConfigRegistry()
|
|
139
|
+
config_setting.load(args.config)
|
|
140
|
+
init_logging(args, config_setting)
|
|
141
|
+
config = config_setting
|
|
142
|
+
init_log += config.consume_init_log()
|
|
143
|
+
if config.config_dict:
|
|
144
|
+
logger.debug("Loaded configuration:\n%s", str(config.config_dict))
|
|
145
|
+
logger.debug("Program arguments:\n%s", str(args))
|
|
146
|
+
settings = ScanSettings(args=args, config=config)
|
|
147
|
+
if settings.get("save-log"):
|
|
148
|
+
setup_logger(
|
|
149
|
+
logfile_path=LOGFILE_PATH,
|
|
150
|
+
max_log_files=settings.get("max-log-files"),
|
|
151
|
+
name_prefix="dvr-scan-app",
|
|
152
|
+
)
|
|
153
|
+
failed_to_load_config = False
|
|
154
|
+
except ConfigLoadFailure as ex:
|
|
155
|
+
init_log += ex.init_log
|
|
156
|
+
if ex.reason is not None:
|
|
157
|
+
init_log += [(logging.ERROR, "Error: %s" % str(ex.reason).replace("\t", " "))]
|
|
158
|
+
failed_to_load_config = True
|
|
159
|
+
config_load_error = ex
|
|
160
|
+
finally:
|
|
161
|
+
for log_level, log_str in init_log:
|
|
162
|
+
logger.log(log_level, log_str)
|
|
163
|
+
|
|
164
|
+
if failed_to_load_config:
|
|
165
|
+
logger.critical("Failed to load config file.")
|
|
166
|
+
logger.debug("Error loading config file:", exc_info=config_load_error)
|
|
167
|
+
# Intentionally suppress the exception in release mode since we've already logged the
|
|
168
|
+
# failure reason to the user above. We can now exit with an error code.
|
|
169
|
+
raise SystemExit(1)
|
|
170
|
+
|
|
171
|
+
show_traceback = getattr(logging, settings.get("verbosity").upper()) == logging.DEBUG
|
|
172
|
+
# TODO: Use Python __debug__ mode instead of hard-coding as config option.
|
|
173
|
+
debug_mode = settings.get("debug")
|
|
174
|
+
with logging_redirect_tqdm(loggers=[logger]):
|
|
175
|
+
try:
|
|
176
|
+
app = Application(
|
|
177
|
+
settings=settings, initial_videos=args.input if hasattr(args, "input") else []
|
|
178
|
+
)
|
|
179
|
+
app.run()
|
|
180
|
+
|
|
181
|
+
except ValueError as ex:
|
|
182
|
+
logger.critical("Setting Error: %s", str(ex), exc_info=show_traceback)
|
|
183
|
+
if debug_mode:
|
|
184
|
+
raise
|
|
185
|
+
except VideoOpenFailure as ex:
|
|
186
|
+
logger.critical("Failed to load input: %s", str(ex), exc_info=show_traceback)
|
|
187
|
+
if debug_mode:
|
|
188
|
+
raise
|
|
189
|
+
except KeyboardInterrupt:
|
|
190
|
+
# TODO: This doesn't always work when the GUI is running.
|
|
191
|
+
logger.info("Stopping (interrupt received)...", exc_info=show_traceback)
|
|
192
|
+
if debug_mode:
|
|
193
|
+
raise
|
|
194
|
+
except CalledProcessError as ex:
|
|
195
|
+
logger.error(
|
|
196
|
+
"Failed to run command:\n %s\nCommand returned %d, output:\n\n%s",
|
|
197
|
+
" ".join(ex.cmd),
|
|
198
|
+
ex.returncode,
|
|
199
|
+
ex.output,
|
|
200
|
+
exc_info=show_traceback,
|
|
201
|
+
)
|
|
202
|
+
if debug_mode:
|
|
203
|
+
raise
|
|
204
|
+
except NotImplementedError as ex:
|
|
205
|
+
logger.critical("Error (Not Implemented): %s", str(ex), exc_info=show_traceback)
|
|
206
|
+
if debug_mode:
|
|
207
|
+
raise
|
|
208
|
+
except Exception as ex:
|
|
209
|
+
logger.critical("Critical Error: %s", str(ex), exc_info=True)
|
|
210
|
+
if debug_mode:
|
|
211
|
+
raise
|
|
212
|
+
else:
|
|
213
|
+
sys.exit(EXIT_SUCCESS)
|
|
214
|
+
sys.exit(EXIT_ERROR)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
if __name__ == "__main__":
|
|
218
|
+
main()
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#
|
|
2
|
+
# DVR-Scan: Video Motion Event Detection & Extraction Tool
|
|
3
|
+
# --------------------------------------------------------------
|
|
4
|
+
# [ Site: https://www.dvr-scan.com/ ]
|
|
5
|
+
# [ Repo: https://github.com/Breakthrough/DVR-Scan ]
|
|
6
|
+
#
|
|
7
|
+
# Copyright (C) 2024 Brandon Castellano <http://www.bcastell.com>.
|
|
8
|
+
# DVR-Scan is licensed under the BSD 2-Clause License; see the included
|
|
9
|
+
# LICENSE file, or visit one of the above pages for details.
|
|
10
|
+
#
|
|
11
|
+
|
|
12
|
+
import importlib.resources as resources
|
|
13
|
+
import tkinter as tk
|
|
14
|
+
import tkinter.filedialog
|
|
15
|
+
import tkinter.messagebox
|
|
16
|
+
import tkinter.scrolledtext
|
|
17
|
+
import tkinter.ttk as ttk
|
|
18
|
+
import typing as ty
|
|
19
|
+
import webbrowser
|
|
20
|
+
|
|
21
|
+
import PIL
|
|
22
|
+
import PIL.Image
|
|
23
|
+
import PIL.ImageTk
|
|
24
|
+
|
|
25
|
+
import dvr_scan
|
|
26
|
+
from dvr_scan.platform import get_system_version_info
|
|
27
|
+
|
|
28
|
+
TITLE = "About DVR-Scan"
|
|
29
|
+
COPYRIGHT = (
|
|
30
|
+
f"DVR-Scan {dvr_scan.__version__}\n\nCopyright © Brandon Castellano.\nAll rights reserved."
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class AboutWindow:
|
|
35
|
+
def __init__(self):
|
|
36
|
+
self._version_info: ty.Optional[str] = None
|
|
37
|
+
self._about_image: PIL.Image = None
|
|
38
|
+
self._about_image_tk: PIL.ImageTk.PhotoImage = None
|
|
39
|
+
|
|
40
|
+
def show(self, root: tk.Tk):
|
|
41
|
+
window = tk.Toplevel(master=root)
|
|
42
|
+
window.withdraw()
|
|
43
|
+
window.title(TITLE)
|
|
44
|
+
window.resizable(True, True)
|
|
45
|
+
|
|
46
|
+
app_logo = PIL.Image.open(resources.open_binary(dvr_scan, "dvr-scan-logo.png"))
|
|
47
|
+
self._about_image = app_logo.crop((8, 8, app_logo.width - 132, app_logo.height - 8))
|
|
48
|
+
self._about_image_tk = PIL.ImageTk.PhotoImage(self._about_image)
|
|
49
|
+
canvas = tk.Canvas(window, width=self._about_image.width, height=self._about_image.height)
|
|
50
|
+
canvas.grid()
|
|
51
|
+
canvas.create_image(0, 0, anchor=tk.NW, image=self._about_image_tk)
|
|
52
|
+
|
|
53
|
+
ttk.Separator(window, orient=tk.HORIZONTAL).grid(row=1, sticky="ew", padx=16.0)
|
|
54
|
+
ttk.Label(
|
|
55
|
+
window,
|
|
56
|
+
text=COPYRIGHT,
|
|
57
|
+
).grid(row=2, sticky="nw", padx=24.0, pady=24.0)
|
|
58
|
+
|
|
59
|
+
# TODO: These should be buttons not labels.
|
|
60
|
+
website_link = ttk.Label(
|
|
61
|
+
window, text="www.dvr-scan.com", cursor="hand2", foreground="medium blue"
|
|
62
|
+
)
|
|
63
|
+
website_link.grid(row=2, sticky="ne", padx=24.0, pady=24.0)
|
|
64
|
+
website_link.bind("<Button-1>", lambda _: webbrowser.open_new_tab("www.dvr-scan.com"))
|
|
65
|
+
|
|
66
|
+
about_tabs = ttk.Notebook(window)
|
|
67
|
+
version_tab = ttk.Frame(about_tabs)
|
|
68
|
+
version_area = tkinter.scrolledtext.ScrolledText(
|
|
69
|
+
version_tab, wrap=tk.NONE, width=40, height=1
|
|
70
|
+
)
|
|
71
|
+
# TODO: See if we can add another button that will copy debug logs.
|
|
72
|
+
if not self._version_info:
|
|
73
|
+
self._version_info = get_system_version_info()
|
|
74
|
+
version_area.insert(tk.INSERT, self._version_info)
|
|
75
|
+
version_area.grid(sticky="nsew")
|
|
76
|
+
version_area.config(state="disabled")
|
|
77
|
+
version_tab.columnconfigure(0, weight=1)
|
|
78
|
+
version_tab.rowconfigure(0, weight=1)
|
|
79
|
+
tk.Button(
|
|
80
|
+
version_tab,
|
|
81
|
+
text="Copy to Clipboard",
|
|
82
|
+
command=lambda: root.clipboard_append(self._version_info),
|
|
83
|
+
).grid(row=1, column=0)
|
|
84
|
+
|
|
85
|
+
license_tab = ttk.Frame(about_tabs)
|
|
86
|
+
scrollbar = tk.Scrollbar(license_tab, orient=tk.HORIZONTAL)
|
|
87
|
+
license_area = tkinter.scrolledtext.ScrolledText(
|
|
88
|
+
license_tab, wrap=tk.NONE, width=40, xscrollcommand=scrollbar.set, height=1
|
|
89
|
+
)
|
|
90
|
+
license_area.insert(tk.INSERT, dvr_scan.get_license_info())
|
|
91
|
+
license_area.grid(sticky="nsew")
|
|
92
|
+
scrollbar.config(command=license_area.xview)
|
|
93
|
+
scrollbar.grid(row=1, sticky="swe")
|
|
94
|
+
license_area.config(state="disabled")
|
|
95
|
+
license_tab.columnconfigure(0, weight=1)
|
|
96
|
+
license_tab.rowconfigure(0, weight=1)
|
|
97
|
+
|
|
98
|
+
# TODO: Add tab that has some useful links like submitting bug report, etc
|
|
99
|
+
about_tabs.add(version_tab, text="Version Info")
|
|
100
|
+
about_tabs.add(license_tab, text="License Info")
|
|
101
|
+
|
|
102
|
+
about_tabs.grid(
|
|
103
|
+
row=0, column=1, rowspan=4, padx=(0.0, 16.0), pady=(16.0, 16.0), sticky="nsew"
|
|
104
|
+
)
|
|
105
|
+
window.update()
|
|
106
|
+
if self._about_image is not None:
|
|
107
|
+
window.columnconfigure(0, minsize=self._about_image.width)
|
|
108
|
+
window.rowconfigure(0, minsize=self._about_image.height)
|
|
109
|
+
else:
|
|
110
|
+
window.columnconfigure(0, minsize=200)
|
|
111
|
+
window.rowconfigure(0, minsize=100)
|
|
112
|
+
# minsize includes padding
|
|
113
|
+
window.columnconfigure(1, weight=1, minsize=100)
|
|
114
|
+
window.rowconfigure(3, weight=1)
|
|
115
|
+
|
|
116
|
+
window.minsize(width=window.winfo_reqwidth(), height=window.winfo_reqheight())
|
|
117
|
+
# can we query widget height?
|
|
118
|
+
|
|
119
|
+
root.grab_release()
|
|
120
|
+
|
|
121
|
+
window.transient(root)
|
|
122
|
+
window.focus()
|
|
123
|
+
window.grab_set()
|
|
124
|
+
|
|
125
|
+
def dismiss():
|
|
126
|
+
window.grab_release()
|
|
127
|
+
window.destroy()
|
|
128
|
+
root.grab_set()
|
|
129
|
+
root.focus()
|
|
130
|
+
|
|
131
|
+
window.protocol("WM_DELETE_WINDOW", dismiss)
|
|
132
|
+
window.attributes("-topmost", True)
|
|
133
|
+
window.bind("<Escape>", lambda _: window.destroy())
|
|
134
|
+
window.bind("<Destroy>", lambda _: dismiss())
|
|
135
|
+
|
|
136
|
+
window.deiconify()
|
|
137
|
+
window.wait_window()
|