pypetkitapi 1.10.2__py3-none-any.whl → 1.10.3__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.
- pypetkitapi/__init__.py +1 -1
- pypetkitapi/client.py +2 -1
- pypetkitapi/media.py +74 -22
- {pypetkitapi-1.10.2.dist-info → pypetkitapi-1.10.3.dist-info}/METADATA +1 -1
- {pypetkitapi-1.10.2.dist-info → pypetkitapi-1.10.3.dist-info}/RECORD +7 -7
- {pypetkitapi-1.10.2.dist-info → pypetkitapi-1.10.3.dist-info}/LICENSE +0 -0
- {pypetkitapi-1.10.2.dist-info → pypetkitapi-1.10.3.dist-info}/WHEEL +0 -0
pypetkitapi/__init__.py
CHANGED
pypetkitapi/client.py
CHANGED
@@ -108,8 +108,9 @@ class PetKitClient:
|
|
108
108
|
_LOGGER.debug("Getting API server list")
|
109
109
|
self.req.base_url = PetkitDomain.PASSPORT_PETKIT
|
110
110
|
|
111
|
-
if self.region.lower() == "china":
|
111
|
+
if self.region.lower() == "china" or self.region.lower() == "cn":
|
112
112
|
self.req.base_url = PetkitDomain.CHINA_SRV
|
113
|
+
_LOGGER.debug("Using specific China server: %s", PetkitDomain.CHINA_SRV)
|
113
114
|
return
|
114
115
|
|
115
116
|
response = await self.req.request(
|
pypetkitapi/media.py
CHANGED
@@ -31,8 +31,8 @@ _LOGGER = logging.getLogger(__name__)
|
|
31
31
|
|
32
32
|
@dataclass
|
33
33
|
class MediaCloud:
|
34
|
-
"""Dataclass
|
35
|
-
Represents a media file from
|
34
|
+
"""Dataclass MediaCloud.
|
35
|
+
Represents a media file from Petkit API.
|
36
36
|
"""
|
37
37
|
|
38
38
|
event_id: str
|
@@ -68,7 +68,10 @@ class MediaManager:
|
|
68
68
|
async def get_all_media_files(
|
69
69
|
self, devices: list[Feeder | Litter]
|
70
70
|
) -> list[MediaCloud]:
|
71
|
-
"""Get all media files from all devices and return a list of
|
71
|
+
"""Get all media files from all devices and return a list of MediaCloud.
|
72
|
+
:param devices: List of devices
|
73
|
+
:return: List of MediaCloud objects
|
74
|
+
"""
|
72
75
|
media_files: list[MediaCloud] = []
|
73
76
|
_LOGGER.debug("Processing media files for %s devices", len(devices))
|
74
77
|
|
@@ -101,7 +104,10 @@ class MediaManager:
|
|
101
104
|
async def get_all_media_files_disk(
|
102
105
|
self, storage_path: Path, device_id: int
|
103
106
|
) -> None:
|
104
|
-
"""Construct the media file table for disk storage.
|
107
|
+
"""Construct the media file table for disk storage.
|
108
|
+
:param storage_path: Path to the storage directory
|
109
|
+
:param device_id: Device ID
|
110
|
+
"""
|
105
111
|
self.media_table.clear()
|
106
112
|
|
107
113
|
today_str = datetime.now().strftime("%Y%m%d")
|
@@ -129,7 +135,7 @@ class MediaManager:
|
|
129
135
|
if entry.is_file() and valid_pattern.match(entry.name):
|
130
136
|
_LOGGER.debug("Entries found: %s", entry.name)
|
131
137
|
event_id = Path(entry.name).stem
|
132
|
-
timestamp = self.
|
138
|
+
timestamp = self._extract_timestamp(str(entry.name))
|
133
139
|
media_type_str = Path(entry.name).suffix.lstrip(".")
|
134
140
|
try:
|
135
141
|
media_type = MediaType(media_type_str)
|
@@ -148,8 +154,12 @@ class MediaManager:
|
|
148
154
|
)
|
149
155
|
|
150
156
|
@staticmethod
|
151
|
-
def
|
152
|
-
|
157
|
+
def _extract_timestamp(file_name: str) -> int:
|
158
|
+
"""Extract timestamp from a filename.
|
159
|
+
:param file_name: Filename
|
160
|
+
:return: Timestamp
|
161
|
+
"""
|
162
|
+
match = re.search(r"_(\d+)\.[a-zA-Z0-9]+$", file_name)
|
153
163
|
if match:
|
154
164
|
return int(match.group(1))
|
155
165
|
return 0
|
@@ -160,7 +170,12 @@ class MediaManager:
|
|
160
170
|
dl_type: list[MediaType] | None = None,
|
161
171
|
event_type: list[RecordType] | None = None,
|
162
172
|
) -> list[MediaCloud]:
|
163
|
-
"""Compare MediaCloud objects with MediaFile objects and return a list of missing MediaCloud objects.
|
173
|
+
"""Compare MediaCloud objects with MediaFile objects and return a list of missing MediaCloud objects.
|
174
|
+
:param media_cloud_list: List of MediaCloud objects
|
175
|
+
:param dl_type: List of media types to download
|
176
|
+
:param event_type: List of event types to filter
|
177
|
+
:return: List of missing MediaCloud objects
|
178
|
+
"""
|
164
179
|
missing_media = []
|
165
180
|
existing_event_ids = {media_file.event_id for media_file in self.media_table}
|
166
181
|
|
@@ -205,7 +220,10 @@ class MediaManager:
|
|
205
220
|
return missing_media
|
206
221
|
|
207
222
|
def _process_feeder(self, feeder: Feeder) -> list[MediaCloud]:
|
208
|
-
"""Process media files for a Feeder device.
|
223
|
+
"""Process media files for a Feeder device.
|
224
|
+
:param feeder: Feeder device object
|
225
|
+
:return: List of MediaCloud objects for the device
|
226
|
+
"""
|
209
227
|
media_files: list[MediaCloud] = []
|
210
228
|
records = feeder.device_records
|
211
229
|
|
@@ -225,7 +243,12 @@ class MediaManager:
|
|
225
243
|
def _process_feeder_record(
|
226
244
|
self, record, record_type: RecordType, device_obj: Feeder
|
227
245
|
) -> list[MediaCloud]:
|
228
|
-
"""Process individual feeder records.
|
246
|
+
"""Process individual feeder records.
|
247
|
+
:param record: Record object
|
248
|
+
:param record_type: Record type
|
249
|
+
:param device_obj: Feeder device object
|
250
|
+
:return: List of MediaCloud objects for the record
|
251
|
+
"""
|
229
252
|
media_files: list[MediaCloud] = []
|
230
253
|
user_id = device_obj.user.id if device_obj.user else None
|
231
254
|
feeder_id = device_obj.device_nfo.device_id if device_obj.device_nfo else None
|
@@ -283,7 +306,10 @@ class MediaManager:
|
|
283
306
|
return media_files
|
284
307
|
|
285
308
|
def _process_litter(self, litter: Litter) -> list[MediaCloud]:
|
286
|
-
"""Process media files for a Litter device.
|
309
|
+
"""Process media files for a Litter device.
|
310
|
+
:param litter: Litter device object
|
311
|
+
:return: List of MediaCloud objects for the device
|
312
|
+
"""
|
287
313
|
media_files: list[MediaCloud] = []
|
288
314
|
records = litter.device_records
|
289
315
|
litter_id = litter.device_nfo.device_id if litter.device_nfo else None
|
@@ -345,7 +371,13 @@ class MediaManager:
|
|
345
371
|
def construct_video_url(
|
346
372
|
device_type: str | None, media_url: str | None, user_id: int, cp_sub: int | None
|
347
373
|
) -> str | None:
|
348
|
-
"""Construct the video URL.
|
374
|
+
"""Construct the video URL.
|
375
|
+
:param device_type: Device type
|
376
|
+
:param media_url: Media URL
|
377
|
+
:param user_id: User ID
|
378
|
+
:param cp_sub: Cpsub value
|
379
|
+
:return: Constructed video URL
|
380
|
+
"""
|
349
381
|
if not media_url or not user_id or cp_sub != 1:
|
350
382
|
return None
|
351
383
|
params = parse_qs(urlparse(media_url).query)
|
@@ -354,7 +386,10 @@ class MediaManager:
|
|
354
386
|
|
355
387
|
@staticmethod
|
356
388
|
def _get_timestamp(item) -> int:
|
357
|
-
"""Extract timestamp from a record item and raise an exception if it is None.
|
389
|
+
"""Extract timestamp from a record item and raise an exception if it is None.
|
390
|
+
:param item: Record item
|
391
|
+
:return: Timestamp
|
392
|
+
"""
|
358
393
|
timestamp = (
|
359
394
|
item.timestamp
|
360
395
|
or item.completed_at
|
@@ -381,7 +416,10 @@ class DownloadDecryptMedia:
|
|
381
416
|
self.client = client
|
382
417
|
|
383
418
|
async def get_fpath(self, file_name: str) -> Path:
|
384
|
-
"""Return the full path of the file.
|
419
|
+
"""Return the full path of the file.
|
420
|
+
:param file_name: Name of the file.
|
421
|
+
:return: Full path of the file.
|
422
|
+
"""
|
385
423
|
subdir = ""
|
386
424
|
if file_name.endswith(".jpg"):
|
387
425
|
subdir = "snapshot"
|
@@ -392,7 +430,10 @@ class DownloadDecryptMedia:
|
|
392
430
|
async def download_file(
|
393
431
|
self, file_data: MediaCloud, file_type: MediaType | None
|
394
432
|
) -> None:
|
395
|
-
"""Get image and video file
|
433
|
+
"""Get image and video file
|
434
|
+
:param file_data: MediaCloud object
|
435
|
+
:param file_type: MediaType object
|
436
|
+
"""
|
396
437
|
_LOGGER.debug("Downloading media file %s", file_data.event_id)
|
397
438
|
self.file_data = file_data
|
398
439
|
|
@@ -459,7 +500,12 @@ class DownloadDecryptMedia:
|
|
459
500
|
return await self.client.extract_segments_m3u8(str(media_api))
|
460
501
|
|
461
502
|
async def _get_file(self, url: str, aes_key: str, full_filename: str) -> bool:
|
462
|
-
"""Download a file from a URL and decrypt it.
|
503
|
+
"""Download a file from a URL and decrypt it.
|
504
|
+
:param url: URL of the file to download.
|
505
|
+
:param aes_key: AES key used for decryption.
|
506
|
+
:param full_filename: Name of the file to save.
|
507
|
+
:return: True if the file was downloaded successfully, False otherwise.
|
508
|
+
"""
|
463
509
|
|
464
510
|
full_file_path = await self.get_fpath(full_filename)
|
465
511
|
if full_file_path.exists():
|
@@ -487,7 +533,11 @@ class DownloadDecryptMedia:
|
|
487
533
|
return False
|
488
534
|
|
489
535
|
async def _save_file(self, content: bytes, filename: str) -> Path:
|
490
|
-
"""Save content to a file asynchronously and return the file path.
|
536
|
+
"""Save content to a file asynchronously and return the file path.
|
537
|
+
:param content: Bytes data to save.
|
538
|
+
:param filename: Name of the file to save.
|
539
|
+
:return: Path of the saved file.
|
540
|
+
"""
|
491
541
|
file_path = await self.get_fpath(filename)
|
492
542
|
try:
|
493
543
|
# Ensure the directory exists
|
@@ -534,11 +584,11 @@ class DownloadDecryptMedia:
|
|
534
584
|
Path(file_path).unlink()
|
535
585
|
return decrypted_data
|
536
586
|
|
537
|
-
async def _concat_segments(self, ts_files: list[Path], output_file):
|
538
|
-
"""Concatenate a list of .
|
587
|
+
async def _concat_segments(self, ts_files: list[Path], output_file) -> None:
|
588
|
+
"""Concatenate a list of .avi segments into a single output file without using a temporary file.
|
539
589
|
|
540
|
-
:param ts_files: List of absolute paths of .
|
541
|
-
:param output_file: Path of the output file (e.g., "output.
|
590
|
+
:param ts_files: List of absolute paths of .avi files
|
591
|
+
:param output_file: Path of the output file (e.g., "output.avi")
|
542
592
|
"""
|
543
593
|
full_output_file = await self.get_fpath(output_file)
|
544
594
|
if full_output_file.exists():
|
@@ -586,7 +636,9 @@ class DownloadDecryptMedia:
|
|
586
636
|
_LOGGER.error("OS error during concatenation: %s", e)
|
587
637
|
|
588
638
|
async def _delete_segments(self, ts_files: list[Path]) -> None:
|
589
|
-
"""Delete all segment files after concatenation.
|
639
|
+
"""Delete all segment files after concatenation.
|
640
|
+
:param ts_files: List of absolute paths of .avi files
|
641
|
+
"""
|
590
642
|
for file in ts_files:
|
591
643
|
if file.exists():
|
592
644
|
try:
|
@@ -1,19 +1,19 @@
|
|
1
|
-
pypetkitapi/__init__.py,sha256=
|
1
|
+
pypetkitapi/__init__.py,sha256=qP06yQAfRHrJ7yiYlQgNDJD5cGUggj29WsKLhsR_P8M,2107
|
2
2
|
pypetkitapi/bluetooth.py,sha256=u_xGp701WnrroTOt_KuIVUCZ3kRQ7BJeoMR8b9RpJ54,7176
|
3
|
-
pypetkitapi/client.py,sha256=
|
3
|
+
pypetkitapi/client.py,sha256=mbND1lzu1DQ2hHxcQnyk9-8ig1JJ5lAo8S-3lGKmYXs,27037
|
4
4
|
pypetkitapi/command.py,sha256=cMCUutZCQo9Ddvjl_FYR5UjU_CqFz1iyetMznYwjpzM,7500
|
5
5
|
pypetkitapi/const.py,sha256=US5QihmBYvlm8hIHX0PORPUnMmDW3nmLzwLWTepkkGg,4609
|
6
6
|
pypetkitapi/containers.py,sha256=F_uyDBD0a5QD4s_ArjYiKTAAg1XHYBvmV_lEnO9RQ-U,4786
|
7
7
|
pypetkitapi/exceptions.py,sha256=4BXUyYXLfZjNxdnOGJPjyE9ASIl7JmQphjws87jvHtE,1631
|
8
8
|
pypetkitapi/feeder_container.py,sha256=PhidWd5WpsZqtdKZy60PzE67YXgQfApjm8CqvMCHK3U,14743
|
9
9
|
pypetkitapi/litter_container.py,sha256=KWvHNAOJ6hDSeJ_55tqtzY9GxHtd9gAntPkbnVbdb-I,19275
|
10
|
-
pypetkitapi/media.py,sha256=
|
10
|
+
pypetkitapi/media.py,sha256=OrtEN9LKe3Xy4AapvTUCj4LZ09ZAltGpRYEmD_xQwcA,24220
|
11
11
|
pypetkitapi/purifier_container.py,sha256=ssyIxhNben5XJ4KlQTXTrtULg2ji6DqHqjzOq08d1-I,2491
|
12
12
|
pypetkitapi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
13
13
|
pypetkitapi/schedule_container.py,sha256=OjLAY6FY-g14JNJJnYMNFV5ZtdkjUzNBit1VUiiZKnQ,2053
|
14
14
|
pypetkitapi/utils.py,sha256=z7325kcJQUburnF28HSXrJMvY_gY9007K73Zwxp-4DQ,743
|
15
15
|
pypetkitapi/water_fountain_container.py,sha256=5J0b-fDZYcFLNX2El7fifv8H6JMhBCt-ttxSow1ozRQ,6787
|
16
|
-
pypetkitapi-1.10.
|
17
|
-
pypetkitapi-1.10.
|
18
|
-
pypetkitapi-1.10.
|
19
|
-
pypetkitapi-1.10.
|
16
|
+
pypetkitapi-1.10.3.dist-info/LICENSE,sha256=u5jNkZEn6YMrtN4Kr5rU3TcBJ5-eAt0qMx4JDsbsnzM,1074
|
17
|
+
pypetkitapi-1.10.3.dist-info/METADATA,sha256=OyJ5ainwvLr8ZSkZVM6Dt3cfLlqR1SRE2kXgiq5rBUs,6256
|
18
|
+
pypetkitapi-1.10.3.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
|
19
|
+
pypetkitapi-1.10.3.dist-info/RECORD,,
|
File without changes
|
File without changes
|