xmcam-py 0.1.0__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.
- xmcam_py-0.1.0/.github/workflows/ci.yml +74 -0
- xmcam_py-0.1.0/.gitignore +29 -0
- xmcam_py-0.1.0/LICENSE +21 -0
- xmcam_py-0.1.0/PKG-INFO +138 -0
- xmcam_py-0.1.0/README.md +102 -0
- xmcam_py-0.1.0/pyproject.toml +76 -0
- xmcam_py-0.1.0/release.sh +113 -0
- xmcam_py-0.1.0/src/xmcam/__init__.py +96 -0
- xmcam_py-0.1.0/src/xmcam/cli.py +393 -0
- xmcam_py-0.1.0/src/xmcam/config_backup.py +89 -0
- xmcam_py-0.1.0/src/xmcam/credentials.py +16 -0
- xmcam_py-0.1.0/src/xmcam/discovery.py +146 -0
- xmcam_py-0.1.0/src/xmcam/dvrip.py +313 -0
- xmcam_py-0.1.0/src/xmcam/exceptions.py +38 -0
- xmcam_py-0.1.0/src/xmcam/files.py +121 -0
- xmcam_py-0.1.0/src/xmcam/live.py +69 -0
- xmcam_py-0.1.0/src/xmcam/media.py +165 -0
- xmcam_py-0.1.0/src/xmcam/models.py +122 -0
- xmcam_py-0.1.0/src/xmcam/onvif.py +186 -0
- xmcam_py-0.1.0/src/xmcam/playback.py +111 -0
- xmcam_py-0.1.0/src/xmcam/ptz.py +87 -0
- xmcam_py-0.1.0/src/xmcam/timesync.py +75 -0
- xmcam_py-0.1.0/tests/conftest.py +88 -0
- xmcam_py-0.1.0/tests/test_config_backup.py +100 -0
- xmcam_py-0.1.0/tests/test_credentials.py +44 -0
- xmcam_py-0.1.0/tests/test_discovery.py +184 -0
- xmcam_py-0.1.0/tests/test_dvrip.py +86 -0
- xmcam_py-0.1.0/tests/test_files.py +90 -0
- xmcam_py-0.1.0/tests/test_live.py +38 -0
- xmcam_py-0.1.0/tests/test_media.py +115 -0
- xmcam_py-0.1.0/tests/test_media_codec.py +38 -0
- xmcam_py-0.1.0/tests/test_models.py +39 -0
- xmcam_py-0.1.0/tests/test_onvif.py +111 -0
- xmcam_py-0.1.0/tests/test_onvif_time.py +28 -0
- xmcam_py-0.1.0/tests/test_playback.py +65 -0
- xmcam_py-0.1.0/tests/test_ptz.py +107 -0
- xmcam_py-0.1.0/tests/test_timesync.py +103 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
name: Test, Build and Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- 'main'
|
|
7
|
+
tags:
|
|
8
|
+
- 'v*'
|
|
9
|
+
pull_request:
|
|
10
|
+
branches:
|
|
11
|
+
- 'main'
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
test:
|
|
15
|
+
name: Test and Lint
|
|
16
|
+
runs-on: ubuntu-latest
|
|
17
|
+
strategy:
|
|
18
|
+
fail-fast: false
|
|
19
|
+
matrix:
|
|
20
|
+
python-version: ["3.9", "3.11", "3.12"]
|
|
21
|
+
|
|
22
|
+
steps:
|
|
23
|
+
- name: Checkout repository
|
|
24
|
+
uses: actions/checkout@v4
|
|
25
|
+
|
|
26
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
27
|
+
uses: actions/setup-python@v5
|
|
28
|
+
with:
|
|
29
|
+
python-version: ${{ matrix.python-version }}
|
|
30
|
+
|
|
31
|
+
- name: Install dependencies
|
|
32
|
+
run: |
|
|
33
|
+
python -m pip install --upgrade pip
|
|
34
|
+
pip install -e ".[dev]"
|
|
35
|
+
|
|
36
|
+
- name: Lint with ruff
|
|
37
|
+
run: ruff check .
|
|
38
|
+
|
|
39
|
+
- name: Check formatting with Black
|
|
40
|
+
run: black --check .
|
|
41
|
+
|
|
42
|
+
- name: Type check with Mypy
|
|
43
|
+
run: mypy src
|
|
44
|
+
|
|
45
|
+
- name: Test with pytest
|
|
46
|
+
run: pytest
|
|
47
|
+
|
|
48
|
+
publish:
|
|
49
|
+
name: Build and Publish to PyPI
|
|
50
|
+
needs: test
|
|
51
|
+
runs-on: ubuntu-latest
|
|
52
|
+
if: startsWith(github.ref, 'refs/tags/v')
|
|
53
|
+
permissions:
|
|
54
|
+
id-token: write # OIDC authentication with PyPI (trusted publishing)
|
|
55
|
+
|
|
56
|
+
steps:
|
|
57
|
+
- name: Checkout repository
|
|
58
|
+
uses: actions/checkout@v4
|
|
59
|
+
with:
|
|
60
|
+
fetch-depth: 0
|
|
61
|
+
|
|
62
|
+
- name: Set up Python
|
|
63
|
+
uses: actions/setup-python@v5
|
|
64
|
+
with:
|
|
65
|
+
python-version: '3.x'
|
|
66
|
+
|
|
67
|
+
- name: Install build tools
|
|
68
|
+
run: python -m pip install --upgrade build
|
|
69
|
+
|
|
70
|
+
- name: Build package
|
|
71
|
+
run: python -m build
|
|
72
|
+
|
|
73
|
+
- name: Publish package to PyPI
|
|
74
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
.venv/
|
|
9
|
+
venv/
|
|
10
|
+
.pytest_cache/
|
|
11
|
+
.mypy_cache/
|
|
12
|
+
.ruff_cache/
|
|
13
|
+
|
|
14
|
+
# Downloaded recordings / caches (never commit footage or caches)
|
|
15
|
+
*.xmraw
|
|
16
|
+
*.h264
|
|
17
|
+
*.h265
|
|
18
|
+
*.mp4
|
|
19
|
+
*.jpg
|
|
20
|
+
nagrania/
|
|
21
|
+
recordings/
|
|
22
|
+
cache/
|
|
23
|
+
.xmcam-cache/
|
|
24
|
+
|
|
25
|
+
# Local credentials / scratch (never commit secrets)
|
|
26
|
+
*.secret
|
|
27
|
+
.env
|
|
28
|
+
secrets.*
|
|
29
|
+
_scratch/
|
xmcam_py-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 theundefined
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
xmcam_py-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: xmcam-py
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Local-first Python library and CLI for Xiongmai (XM) / iCSee / XMeye IP cameras: discovery, ONVIF, and recording-archive listing/download over DVRIP. No cloud, no internet.
|
|
5
|
+
Project-URL: Homepage, https://github.com/theundefined/xmcam-py
|
|
6
|
+
Project-URL: Repository, https://github.com/theundefined/xmcam-py
|
|
7
|
+
Project-URL: Issues, https://github.com/theundefined/xmcam-py/issues
|
|
8
|
+
Author-email: theundefined <undefine@aramin.net>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: camera,cctv,dvrip,icsee,ip-camera,nvr,onvif,rtsp,sofia,surveillance,xiongmai,xm,xmeye
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: System Administrators
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: POSIX
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Topic :: Multimedia :: Video
|
|
25
|
+
Classifier: Topic :: Security
|
|
26
|
+
Classifier: Topic :: System :: Networking
|
|
27
|
+
Requires-Python: >=3.9
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: black>=24.0.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: build>=1.0.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: mypy>=1.9.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
33
|
+
Requires-Dist: ruff>=0.3.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: twine>=5.0.0; extra == 'dev'
|
|
35
|
+
Description-Content-Type: text/markdown
|
|
36
|
+
|
|
37
|
+
# xmcam
|
|
38
|
+
|
|
39
|
+
**Local-first Python library and CLI for Xiongmai (XM) IP cameras** — the kind
|
|
40
|
+
sold under countless brands and driven by the **iCSee** / **XMeye** mobile apps.
|
|
41
|
+
Discover cameras on your LAN, read their capabilities over ONVIF, and **list and
|
|
42
|
+
download the recording archive** straight off the SD card/disk over the native
|
|
43
|
+
**DVRIP** protocol. Everything happens on the local network: **no cloud, no
|
|
44
|
+
internet, no vendor account.**
|
|
45
|
+
|
|
46
|
+
> Distribution name: `xmcam-py` · import name: `xmcam`
|
|
47
|
+
|
|
48
|
+
## Why
|
|
49
|
+
|
|
50
|
+
These cameras store footage locally but the official way to get it off the
|
|
51
|
+
device is a clunky mobile app or a Windows-only VMS. There was no maintained
|
|
52
|
+
Python tool for pulling the **archive** programmatically. `xmcam` fills that
|
|
53
|
+
gap with a clean, dependency-free API and a scriptable CLI.
|
|
54
|
+
|
|
55
|
+
## Features
|
|
56
|
+
|
|
57
|
+
- **Discovery** — find cameras via ONVIF WS-Discovery, with Xiongmai detection.
|
|
58
|
+
- **Credential recovery** — these cameras answer ONVIF **anonymously** and leak
|
|
59
|
+
the stream account in the RTSP URL; `xmcam` can recover the username/password
|
|
60
|
+
the way the app does (on hardware you own — see *Security & ethics*).
|
|
61
|
+
- **Recording archive** — list recordings in any time range and download them,
|
|
62
|
+
single files, arbitrary ranges or a whole day.
|
|
63
|
+
- **Transparent conversion** — the raw stream is Xiongmai's own container;
|
|
64
|
+
`xmcam` demuxes it to an elementary H.264/H.265 stream and (with `ffmpeg`)
|
|
65
|
+
remuxes to a playable **MP4** without re-encoding.
|
|
66
|
+
- **Live** — build RTSP URLs and grab JPEG snapshots.
|
|
67
|
+
- **PTZ** — pan/tilt/zoom over ONVIF where the hardware supports it.
|
|
68
|
+
- **Config backup** and **clock sync**.
|
|
69
|
+
- **Zero runtime dependencies** — pure standard library. `ffmpeg` is optional
|
|
70
|
+
and only needed for MP4 output.
|
|
71
|
+
|
|
72
|
+
## Install
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pip install xmcam-py
|
|
76
|
+
# optional, for MP4 output: install ffmpeg via your system package manager
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Quick start (CLI)
|
|
80
|
+
|
|
81
|
+
Credentials are read from `--user/--password` or the `XMCAM_USER` / `XMCAM_PASS`
|
|
82
|
+
environment variables (preferred — they stay out of your shell history).
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
# Find cameras on the LAN (pass your host interface IP for reliable multicast)
|
|
86
|
+
xmcam discover --iface 192.0.2.5
|
|
87
|
+
|
|
88
|
+
# Recover the camera account from anonymous ONVIF (hardware you own)
|
|
89
|
+
xmcam creds 192.0.2.10
|
|
90
|
+
|
|
91
|
+
# List today's recordings
|
|
92
|
+
xmcam list 192.0.2.10 --start "2026-11-10 00:00:00" --end "2026-11-10 23:59:59"
|
|
93
|
+
|
|
94
|
+
# Download a one-hour range to MP4
|
|
95
|
+
export XMCAM_USER=... XMCAM_PASS=...
|
|
96
|
+
xmcam pull 192.0.2.10 --start "2026-11-10 08:00:00" --end "2026-11-10 09:00:00" --out ./footage
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Quick start (library)
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from xmcam.dvrip import DVRIPClient
|
|
103
|
+
from xmcam.files import list_recordings
|
|
104
|
+
|
|
105
|
+
with DVRIPClient("192.0.2.10", user="rryp", password="...") as cam:
|
|
106
|
+
for rec in list_recordings(cam, "2026-11-10 08:00:00", "2026-11-10 09:00:00"):
|
|
107
|
+
print(rec.begin_time, rec.length_bytes, rec.filename)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Security & ethics
|
|
111
|
+
|
|
112
|
+
This library can recover camera credentials and pull footage over the LAN. That
|
|
113
|
+
is possible because the firmware answers ONVIF **without authentication** and
|
|
114
|
+
exposes the stream password in plaintext — a real weakness of these devices.
|
|
115
|
+
**Use `xmcam` only on cameras you own or are explicitly authorised to
|
|
116
|
+
administer.** It never contacts any cloud service and stores no credentials on
|
|
117
|
+
disk; supply them per-run via environment variables.
|
|
118
|
+
|
|
119
|
+
If you run these cameras: keep them off the internet (no P2P/cloud), put them on
|
|
120
|
+
an isolated VLAN, and firewall ports 8899/34567/554.
|
|
121
|
+
|
|
122
|
+
## Notes & limitations
|
|
123
|
+
|
|
124
|
+
- **Discovery is multicast-based.** Some firmwares (seen on XM535 / V5.01) do not
|
|
125
|
+
answer WS-Discovery at all, even though they serve ONVIF and DVRIP. Such
|
|
126
|
+
cameras won't appear in `discover` — address them by IP directly.
|
|
127
|
+
- **`stream-url` prints the password** inside the RTSP URL, because that is how
|
|
128
|
+
these cameras authenticate; the URL is only usable with it. Treat that output
|
|
129
|
+
as a secret (it may land in shell history/logs).
|
|
130
|
+
- **`info`**: several DVRIP info blocks (`SystemInfo`, `StorageInfo`, ...) are
|
|
131
|
+
restricted for the auto-recovered account and come back empty; the command
|
|
132
|
+
shows what is available plus the ONVIF device information.
|
|
133
|
+
- **Camera clock**: `synctime --show` reports whatever the camera believes the
|
|
134
|
+
time is — which is often UTC unless you configured a timezone on the device.
|
|
135
|
+
|
|
136
|
+
## License
|
|
137
|
+
|
|
138
|
+
MIT © theundefined
|
xmcam_py-0.1.0/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# xmcam
|
|
2
|
+
|
|
3
|
+
**Local-first Python library and CLI for Xiongmai (XM) IP cameras** — the kind
|
|
4
|
+
sold under countless brands and driven by the **iCSee** / **XMeye** mobile apps.
|
|
5
|
+
Discover cameras on your LAN, read their capabilities over ONVIF, and **list and
|
|
6
|
+
download the recording archive** straight off the SD card/disk over the native
|
|
7
|
+
**DVRIP** protocol. Everything happens on the local network: **no cloud, no
|
|
8
|
+
internet, no vendor account.**
|
|
9
|
+
|
|
10
|
+
> Distribution name: `xmcam-py` · import name: `xmcam`
|
|
11
|
+
|
|
12
|
+
## Why
|
|
13
|
+
|
|
14
|
+
These cameras store footage locally but the official way to get it off the
|
|
15
|
+
device is a clunky mobile app or a Windows-only VMS. There was no maintained
|
|
16
|
+
Python tool for pulling the **archive** programmatically. `xmcam` fills that
|
|
17
|
+
gap with a clean, dependency-free API and a scriptable CLI.
|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- **Discovery** — find cameras via ONVIF WS-Discovery, with Xiongmai detection.
|
|
22
|
+
- **Credential recovery** — these cameras answer ONVIF **anonymously** and leak
|
|
23
|
+
the stream account in the RTSP URL; `xmcam` can recover the username/password
|
|
24
|
+
the way the app does (on hardware you own — see *Security & ethics*).
|
|
25
|
+
- **Recording archive** — list recordings in any time range and download them,
|
|
26
|
+
single files, arbitrary ranges or a whole day.
|
|
27
|
+
- **Transparent conversion** — the raw stream is Xiongmai's own container;
|
|
28
|
+
`xmcam` demuxes it to an elementary H.264/H.265 stream and (with `ffmpeg`)
|
|
29
|
+
remuxes to a playable **MP4** without re-encoding.
|
|
30
|
+
- **Live** — build RTSP URLs and grab JPEG snapshots.
|
|
31
|
+
- **PTZ** — pan/tilt/zoom over ONVIF where the hardware supports it.
|
|
32
|
+
- **Config backup** and **clock sync**.
|
|
33
|
+
- **Zero runtime dependencies** — pure standard library. `ffmpeg` is optional
|
|
34
|
+
and only needed for MP4 output.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install xmcam-py
|
|
40
|
+
# optional, for MP4 output: install ffmpeg via your system package manager
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quick start (CLI)
|
|
44
|
+
|
|
45
|
+
Credentials are read from `--user/--password` or the `XMCAM_USER` / `XMCAM_PASS`
|
|
46
|
+
environment variables (preferred — they stay out of your shell history).
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# Find cameras on the LAN (pass your host interface IP for reliable multicast)
|
|
50
|
+
xmcam discover --iface 192.0.2.5
|
|
51
|
+
|
|
52
|
+
# Recover the camera account from anonymous ONVIF (hardware you own)
|
|
53
|
+
xmcam creds 192.0.2.10
|
|
54
|
+
|
|
55
|
+
# List today's recordings
|
|
56
|
+
xmcam list 192.0.2.10 --start "2026-11-10 00:00:00" --end "2026-11-10 23:59:59"
|
|
57
|
+
|
|
58
|
+
# Download a one-hour range to MP4
|
|
59
|
+
export XMCAM_USER=... XMCAM_PASS=...
|
|
60
|
+
xmcam pull 192.0.2.10 --start "2026-11-10 08:00:00" --end "2026-11-10 09:00:00" --out ./footage
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Quick start (library)
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from xmcam.dvrip import DVRIPClient
|
|
67
|
+
from xmcam.files import list_recordings
|
|
68
|
+
|
|
69
|
+
with DVRIPClient("192.0.2.10", user="rryp", password="...") as cam:
|
|
70
|
+
for rec in list_recordings(cam, "2026-11-10 08:00:00", "2026-11-10 09:00:00"):
|
|
71
|
+
print(rec.begin_time, rec.length_bytes, rec.filename)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Security & ethics
|
|
75
|
+
|
|
76
|
+
This library can recover camera credentials and pull footage over the LAN. That
|
|
77
|
+
is possible because the firmware answers ONVIF **without authentication** and
|
|
78
|
+
exposes the stream password in plaintext — a real weakness of these devices.
|
|
79
|
+
**Use `xmcam` only on cameras you own or are explicitly authorised to
|
|
80
|
+
administer.** It never contacts any cloud service and stores no credentials on
|
|
81
|
+
disk; supply them per-run via environment variables.
|
|
82
|
+
|
|
83
|
+
If you run these cameras: keep them off the internet (no P2P/cloud), put them on
|
|
84
|
+
an isolated VLAN, and firewall ports 8899/34567/554.
|
|
85
|
+
|
|
86
|
+
## Notes & limitations
|
|
87
|
+
|
|
88
|
+
- **Discovery is multicast-based.** Some firmwares (seen on XM535 / V5.01) do not
|
|
89
|
+
answer WS-Discovery at all, even though they serve ONVIF and DVRIP. Such
|
|
90
|
+
cameras won't appear in `discover` — address them by IP directly.
|
|
91
|
+
- **`stream-url` prints the password** inside the RTSP URL, because that is how
|
|
92
|
+
these cameras authenticate; the URL is only usable with it. Treat that output
|
|
93
|
+
as a secret (it may land in shell history/logs).
|
|
94
|
+
- **`info`**: several DVRIP info blocks (`SystemInfo`, `StorageInfo`, ...) are
|
|
95
|
+
restricted for the auto-recovered account and come back empty; the command
|
|
96
|
+
shows what is available plus the ONVIF device information.
|
|
97
|
+
- **Camera clock**: `synctime --show` reports whatever the camera believes the
|
|
98
|
+
time is — which is often UTC unless you configured a timezone on the device.
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT © theundefined
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "xmcam-py"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Local-first Python library and CLI for Xiongmai (XM) / iCSee / XMeye IP cameras: discovery, ONVIF, and recording-archive listing/download over DVRIP. No cloud, no internet."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "theundefined", email = "undefine@aramin.net" }]
|
|
14
|
+
keywords = [
|
|
15
|
+
"xiongmai", "xm", "icsee", "xmeye", "dvrip", "sofia", "onvif",
|
|
16
|
+
"ip-camera", "cctv", "nvr", "rtsp", "surveillance", "camera",
|
|
17
|
+
]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 4 - Beta",
|
|
20
|
+
"Environment :: Console",
|
|
21
|
+
"Intended Audience :: System Administrators",
|
|
22
|
+
"Intended Audience :: Developers",
|
|
23
|
+
"License :: OSI Approved :: MIT License",
|
|
24
|
+
"Operating System :: POSIX",
|
|
25
|
+
"Programming Language :: Python :: 3",
|
|
26
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
27
|
+
"Programming Language :: Python :: 3.9",
|
|
28
|
+
"Programming Language :: Python :: 3.10",
|
|
29
|
+
"Programming Language :: Python :: 3.11",
|
|
30
|
+
"Programming Language :: Python :: 3.12",
|
|
31
|
+
"Topic :: Multimedia :: Video",
|
|
32
|
+
"Topic :: System :: Networking",
|
|
33
|
+
"Topic :: Security",
|
|
34
|
+
]
|
|
35
|
+
dependencies = []
|
|
36
|
+
|
|
37
|
+
[project.optional-dependencies]
|
|
38
|
+
dev = [
|
|
39
|
+
"pytest>=7",
|
|
40
|
+
"ruff>=0.3.0",
|
|
41
|
+
"black>=24.0.0",
|
|
42
|
+
"mypy>=1.9.0",
|
|
43
|
+
"build>=1.0.0",
|
|
44
|
+
"twine>=5.0.0",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[project.urls]
|
|
48
|
+
Homepage = "https://github.com/theundefined/xmcam-py"
|
|
49
|
+
Repository = "https://github.com/theundefined/xmcam-py"
|
|
50
|
+
Issues = "https://github.com/theundefined/xmcam-py/issues"
|
|
51
|
+
|
|
52
|
+
[project.scripts]
|
|
53
|
+
xmcam = "xmcam.cli:main"
|
|
54
|
+
|
|
55
|
+
[tool.hatch.build.targets.wheel]
|
|
56
|
+
packages = ["src/xmcam"]
|
|
57
|
+
|
|
58
|
+
[tool.pytest.ini_options]
|
|
59
|
+
testpaths = ["tests"]
|
|
60
|
+
addopts = "-q"
|
|
61
|
+
|
|
62
|
+
[tool.black]
|
|
63
|
+
line-length = 120
|
|
64
|
+
target-version = ["py39"]
|
|
65
|
+
|
|
66
|
+
[tool.ruff]
|
|
67
|
+
line-length = 120
|
|
68
|
+
target-version = "py39"
|
|
69
|
+
|
|
70
|
+
[tool.ruff.lint]
|
|
71
|
+
# Classic default rule set (pyflakes + a few pycodestyle error checks), pinned
|
|
72
|
+
# so a ruff upgrade does not silently start failing CI on unrelated rules.
|
|
73
|
+
select = ["E4", "E7", "E9", "F"]
|
|
74
|
+
|
|
75
|
+
[tool.mypy]
|
|
76
|
+
ignore_missing_imports = true
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Exit immediately if a command exits with a non-zero status.
|
|
4
|
+
set -e
|
|
5
|
+
|
|
6
|
+
# Configuration
|
|
7
|
+
PROJECT_NAME="xmcam-py"
|
|
8
|
+
DEFAULT_INITIAL_VERSION="v0.1.0"
|
|
9
|
+
VERSION_BUMP_TYPE="patch" # Can be major, minor, patch
|
|
10
|
+
|
|
11
|
+
# Function to display usage
|
|
12
|
+
usage() {
|
|
13
|
+
echo "Usage: $0 [major|minor|patch|vX.Y.Z]"
|
|
14
|
+
echo " Automatically bumps the version, creates a git tag, and pushes it."
|
|
15
|
+
echo " If a version is specified (vX.Y.Z), it will use that exact version."
|
|
16
|
+
exit 1
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
# Parse arguments
|
|
20
|
+
if [ "$#" -eq 1 ]; then
|
|
21
|
+
if [[ "$1" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
22
|
+
NEW_VERSION_ARG="$1"
|
|
23
|
+
elif [[ "$1" =~ ^(major|minor|patch)$ ]]; then
|
|
24
|
+
VERSION_BUMP_TYPE="$1"
|
|
25
|
+
else
|
|
26
|
+
usage
|
|
27
|
+
fi
|
|
28
|
+
elif [ "$#" -ne 0 ]; then
|
|
29
|
+
usage
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
# Fetch all tags to ensure we have the latest
|
|
33
|
+
echo "Fetching latest tags..."
|
|
34
|
+
git fetch origin --tags
|
|
35
|
+
|
|
36
|
+
# Get the latest version tag
|
|
37
|
+
LAST_TAG=$(git describe --tags --abbrev=0 --match "v[0-9]*.[0-9]*.[0-9]*" 2>/dev/null || echo "$DEFAULT_INITIAL_VERSION")
|
|
38
|
+
|
|
39
|
+
# If no semantic version tags exist, start with the default initial version and assume 0.0.0 for first bump
|
|
40
|
+
if [ "$LAST_TAG" = "$DEFAULT_INITIAL_VERSION" ] && [ "$(git tag --list "$DEFAULT_INITIAL_VERSION")" = "" ]; then
|
|
41
|
+
echo "No semantic version tags found. Starting with $DEFAULT_INITIAL_VERSION."
|
|
42
|
+
CURRENT_VERSION_SEMVER="0.0.0" # Base for first bump
|
|
43
|
+
else
|
|
44
|
+
CURRENT_VERSION_SEMVER=$(echo "$LAST_TAG" | sed 's/^v//')
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
# Determine the new version
|
|
48
|
+
if [ -n "$NEW_VERSION_ARG" ]; then
|
|
49
|
+
NEW_VERSION=$(echo "$NEW_VERSION_ARG" | sed 's/^v//')
|
|
50
|
+
echo "Using specified version: v$NEW_VERSION"
|
|
51
|
+
else
|
|
52
|
+
# Split version into major, minor, patch
|
|
53
|
+
IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION_SEMVER"
|
|
54
|
+
|
|
55
|
+
case "$VERSION_BUMP_TYPE" in
|
|
56
|
+
major)
|
|
57
|
+
MAJOR=$((MAJOR + 1))
|
|
58
|
+
MINOR=0
|
|
59
|
+
PATCH=0
|
|
60
|
+
;;
|
|
61
|
+
minor)
|
|
62
|
+
MINOR=$((MINOR + 1))
|
|
63
|
+
PATCH=0
|
|
64
|
+
;;
|
|
65
|
+
patch)
|
|
66
|
+
PATCH=$((PATCH + 1))
|
|
67
|
+
;;
|
|
68
|
+
*)
|
|
69
|
+
echo "Invalid bump type: $VERSION_BUMP_TYPE. Must be major, minor, or patch."
|
|
70
|
+
usage
|
|
71
|
+
;;
|
|
72
|
+
esac
|
|
73
|
+
NEW_VERSION="${MAJOR}.${MINOR}.${PATCH}"
|
|
74
|
+
echo "Bumping $VERSION_BUMP_TYPE version: v$CURRENT_VERSION_SEMVER -> v$NEW_VERSION"
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
# Update version in pyproject.toml
|
|
78
|
+
sed -i "s/^version = \".*\"/version = \"$NEW_VERSION\"/" pyproject.toml
|
|
79
|
+
|
|
80
|
+
# Create commit for version bump only if there are changes
|
|
81
|
+
git add pyproject.toml
|
|
82
|
+
if ! git diff --cached --quiet; then
|
|
83
|
+
git commit -m "Bump version to v$NEW_VERSION"
|
|
84
|
+
echo "Committed version bump."
|
|
85
|
+
else
|
|
86
|
+
echo "Version in pyproject.toml is already up to date. Skipping commit."
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
# Create new tag if it doesn't exist
|
|
90
|
+
NEW_TAG="v$NEW_VERSION"
|
|
91
|
+
if git rev-parse "$NEW_TAG" >/dev/null 2>&1; then
|
|
92
|
+
echo "Tag $NEW_TAG already exists."
|
|
93
|
+
else
|
|
94
|
+
echo "Creating git tag: $NEW_TAG"
|
|
95
|
+
git tag "$NEW_TAG" -m "$PROJECT_NAME Release $NEW_VERSION"
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
# Push commit and tag
|
|
99
|
+
echo "Pushing changes and tag to origin..."
|
|
100
|
+
git push origin main || echo "Main already up to date or push failed (skipping)"
|
|
101
|
+
git push origin "$NEW_TAG"
|
|
102
|
+
|
|
103
|
+
# Get repository owner and name
|
|
104
|
+
REPO_URL=$(git config --get remote.origin.url)
|
|
105
|
+
REPO_OWNER=$(echo "$REPO_URL" | sed -E 's/.*github.com[:/]([^/]+)\/.*/\1/')
|
|
106
|
+
REPO_NAME=$(echo "$REPO_URL" | sed -E 's/.*github.com[:/][^/]+\/([^/\. ]+)(\.git)?/\1/')
|
|
107
|
+
REPO_PATH="$REPO_OWNER/$REPO_NAME"
|
|
108
|
+
|
|
109
|
+
# Construct Actions URL
|
|
110
|
+
ACTIONS_URL="https://github.com/$REPO_PATH/actions/workflows/ci.yml"
|
|
111
|
+
echo "Monitor action status here: $ACTIONS_URL"
|
|
112
|
+
|
|
113
|
+
echo "Release process initiated with tag $NEW_TAG. Check GitHub Actions for build status."
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""xmcam - local-first toolkit for Xiongmai (XM) / iCSee / XMeye IP cameras.
|
|
2
|
+
|
|
3
|
+
The package is organised into focused modules:
|
|
4
|
+
|
|
5
|
+
* :mod:`xmcam.discovery` - find cameras on the LAN (WS-Discovery)
|
|
6
|
+
* :mod:`xmcam.onvif` - anonymous ONVIF reads (device info, users, stream URIs)
|
|
7
|
+
* :mod:`xmcam.credentials` - recover the camera account from anonymous ONVIF
|
|
8
|
+
* :mod:`xmcam.dvrip` - low-level DVRIP ("Sofia") client
|
|
9
|
+
* :mod:`xmcam.files` - list the recording archive (OPFileQuery)
|
|
10
|
+
* :mod:`xmcam.playback` - download/stream recordings (OPPlayBack)
|
|
11
|
+
* :mod:`xmcam.media` - demux the XM container and remux to MP4
|
|
12
|
+
* :mod:`xmcam.live` - live RTSP URLs and JPEG snapshots
|
|
13
|
+
* :mod:`xmcam.ptz` - PTZ control (where supported)
|
|
14
|
+
* :mod:`xmcam.config_backup` - export/import camera configuration
|
|
15
|
+
* :mod:`xmcam.timesync` - read/set the camera clock
|
|
16
|
+
|
|
17
|
+
Everything runs on the local network - no cloud, no internet.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
__version__ = "0.1.0"
|
|
23
|
+
|
|
24
|
+
from .dvrip import DVRIP_PORT, DVRIPClient, OK_CODES, RET_MEANING, sofia_hash
|
|
25
|
+
from .exceptions import (
|
|
26
|
+
DownloadError,
|
|
27
|
+
LoginError,
|
|
28
|
+
OnvifError,
|
|
29
|
+
ProtocolError,
|
|
30
|
+
XMCamError,
|
|
31
|
+
XMConnectionError,
|
|
32
|
+
)
|
|
33
|
+
from .models import Camera, Credentials, DeviceInfo, Recording, StreamUri
|
|
34
|
+
|
|
35
|
+
# High-level modules and their most-used entry points.
|
|
36
|
+
from . import ( # noqa: E402
|
|
37
|
+
config_backup,
|
|
38
|
+
credentials,
|
|
39
|
+
discovery,
|
|
40
|
+
files,
|
|
41
|
+
live,
|
|
42
|
+
media,
|
|
43
|
+
onvif,
|
|
44
|
+
playback,
|
|
45
|
+
ptz,
|
|
46
|
+
timesync,
|
|
47
|
+
)
|
|
48
|
+
from .credentials import recover_credentials
|
|
49
|
+
from .discovery import discover
|
|
50
|
+
from .files import list_recordings, list_recordings_range
|
|
51
|
+
from .media import convert_raw, demux, have_ffmpeg
|
|
52
|
+
from .playback import download_recording, iter_recording
|
|
53
|
+
|
|
54
|
+
__all__ = [
|
|
55
|
+
"__version__",
|
|
56
|
+
# low-level
|
|
57
|
+
"DVRIPClient",
|
|
58
|
+
"DVRIP_PORT",
|
|
59
|
+
"OK_CODES",
|
|
60
|
+
"RET_MEANING",
|
|
61
|
+
"sofia_hash",
|
|
62
|
+
# models
|
|
63
|
+
"Camera",
|
|
64
|
+
"Credentials",
|
|
65
|
+
"DeviceInfo",
|
|
66
|
+
"Recording",
|
|
67
|
+
"StreamUri",
|
|
68
|
+
# exceptions
|
|
69
|
+
"XMCamError",
|
|
70
|
+
"XMConnectionError",
|
|
71
|
+
"ProtocolError",
|
|
72
|
+
"LoginError",
|
|
73
|
+
"DownloadError",
|
|
74
|
+
"OnvifError",
|
|
75
|
+
# high-level entry points
|
|
76
|
+
"discover",
|
|
77
|
+
"recover_credentials",
|
|
78
|
+
"list_recordings",
|
|
79
|
+
"list_recordings_range",
|
|
80
|
+
"download_recording",
|
|
81
|
+
"iter_recording",
|
|
82
|
+
"demux",
|
|
83
|
+
"convert_raw",
|
|
84
|
+
"have_ffmpeg",
|
|
85
|
+
# modules
|
|
86
|
+
"discovery",
|
|
87
|
+
"onvif",
|
|
88
|
+
"credentials",
|
|
89
|
+
"files",
|
|
90
|
+
"playback",
|
|
91
|
+
"media",
|
|
92
|
+
"live",
|
|
93
|
+
"ptz",
|
|
94
|
+
"config_backup",
|
|
95
|
+
"timesync",
|
|
96
|
+
]
|