pypetkitapi 1.10.1__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/litter_container.py +1 -1
- pypetkitapi/media.py +80 -22
- {pypetkitapi-1.10.1.dist-info → pypetkitapi-1.10.3.dist-info}/METADATA +1 -1
- {pypetkitapi-1.10.1.dist-info → pypetkitapi-1.10.3.dist-info}/RECORD +8 -8
- {pypetkitapi-1.10.1.dist-info → pypetkitapi-1.10.3.dist-info}/LICENSE +0 -0
- {pypetkitapi-1.10.1.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/litter_container.py
CHANGED
@@ -451,7 +451,7 @@ class Litter(BaseModel):
|
|
451
451
|
service_status: int | None = Field(None, alias="serviceStatus")
|
452
452
|
total_time: int | None = Field(None, alias="totalTime")
|
453
453
|
with_k3: int | None = Field(None, alias="withK3")
|
454
|
-
|
454
|
+
user: UserDevice | None = None
|
455
455
|
device_records: list[LitterRecord] | None = None
|
456
456
|
device_stats: LitterStats | None = None
|
457
457
|
device_pet_graph_out: list[PetOutGraph] | None = None
|
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,8 +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.
|
105
|
-
|
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
|
+
"""
|
106
111
|
self.media_table.clear()
|
107
112
|
|
108
113
|
today_str = datetime.now().strftime("%Y%m%d")
|
@@ -114,7 +119,7 @@ class MediaManager:
|
|
114
119
|
video_path = record_path / "video"
|
115
120
|
|
116
121
|
# Regex pattern to match valid filenames
|
117
|
-
valid_pattern = re.compile(rf"^{device_id}_\d+\.(jpg|avi)$")
|
122
|
+
valid_pattern = re.compile(rf"^(?:\d+_)?{device_id}_\d+\.(jpg|avi)$")
|
118
123
|
|
119
124
|
# Populate the media table with event_id from filenames
|
120
125
|
for subdir in [snapshot_path, video_path]:
|
@@ -130,7 +135,7 @@ class MediaManager:
|
|
130
135
|
if entry.is_file() and valid_pattern.match(entry.name):
|
131
136
|
_LOGGER.debug("Entries found: %s", entry.name)
|
132
137
|
event_id = Path(entry.name).stem
|
133
|
-
timestamp =
|
138
|
+
timestamp = self._extract_timestamp(str(entry.name))
|
134
139
|
media_type_str = Path(entry.name).suffix.lstrip(".")
|
135
140
|
try:
|
136
141
|
media_type = MediaType(media_type_str)
|
@@ -148,13 +153,29 @@ class MediaManager:
|
|
148
153
|
)
|
149
154
|
)
|
150
155
|
|
156
|
+
@staticmethod
|
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)
|
163
|
+
if match:
|
164
|
+
return int(match.group(1))
|
165
|
+
return 0
|
166
|
+
|
151
167
|
async def prepare_missing_files(
|
152
168
|
self,
|
153
169
|
media_cloud_list: list[MediaCloud],
|
154
170
|
dl_type: list[MediaType] | None = None,
|
155
171
|
event_type: list[RecordType] | None = None,
|
156
172
|
) -> list[MediaCloud]:
|
157
|
-
"""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
|
+
"""
|
158
179
|
missing_media = []
|
159
180
|
existing_event_ids = {media_file.event_id for media_file in self.media_table}
|
160
181
|
|
@@ -199,7 +220,10 @@ class MediaManager:
|
|
199
220
|
return missing_media
|
200
221
|
|
201
222
|
def _process_feeder(self, feeder: Feeder) -> list[MediaCloud]:
|
202
|
-
"""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
|
+
"""
|
203
227
|
media_files: list[MediaCloud] = []
|
204
228
|
records = feeder.device_records
|
205
229
|
|
@@ -219,7 +243,12 @@ class MediaManager:
|
|
219
243
|
def _process_feeder_record(
|
220
244
|
self, record, record_type: RecordType, device_obj: Feeder
|
221
245
|
) -> list[MediaCloud]:
|
222
|
-
"""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
|
+
"""
|
223
252
|
media_files: list[MediaCloud] = []
|
224
253
|
user_id = device_obj.user.id if device_obj.user else None
|
225
254
|
feeder_id = device_obj.device_nfo.device_id if device_obj.device_nfo else None
|
@@ -277,7 +306,10 @@ class MediaManager:
|
|
277
306
|
return media_files
|
278
307
|
|
279
308
|
def _process_litter(self, litter: Litter) -> list[MediaCloud]:
|
280
|
-
"""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
|
+
"""
|
281
313
|
media_files: list[MediaCloud] = []
|
282
314
|
records = litter.device_records
|
283
315
|
litter_id = litter.device_nfo.device_id if litter.device_nfo else None
|
@@ -339,7 +371,13 @@ class MediaManager:
|
|
339
371
|
def construct_video_url(
|
340
372
|
device_type: str | None, media_url: str | None, user_id: int, cp_sub: int | None
|
341
373
|
) -> str | None:
|
342
|
-
"""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
|
+
"""
|
343
381
|
if not media_url or not user_id or cp_sub != 1:
|
344
382
|
return None
|
345
383
|
params = parse_qs(urlparse(media_url).query)
|
@@ -348,7 +386,10 @@ class MediaManager:
|
|
348
386
|
|
349
387
|
@staticmethod
|
350
388
|
def _get_timestamp(item) -> int:
|
351
|
-
"""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
|
+
"""
|
352
393
|
timestamp = (
|
353
394
|
item.timestamp
|
354
395
|
or item.completed_at
|
@@ -375,7 +416,10 @@ class DownloadDecryptMedia:
|
|
375
416
|
self.client = client
|
376
417
|
|
377
418
|
async def get_fpath(self, file_name: str) -> Path:
|
378
|
-
"""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
|
+
"""
|
379
423
|
subdir = ""
|
380
424
|
if file_name.endswith(".jpg"):
|
381
425
|
subdir = "snapshot"
|
@@ -386,7 +430,10 @@ class DownloadDecryptMedia:
|
|
386
430
|
async def download_file(
|
387
431
|
self, file_data: MediaCloud, file_type: MediaType | None
|
388
432
|
) -> None:
|
389
|
-
"""Get image and video file
|
433
|
+
"""Get image and video file
|
434
|
+
:param file_data: MediaCloud object
|
435
|
+
:param file_type: MediaType object
|
436
|
+
"""
|
390
437
|
_LOGGER.debug("Downloading media file %s", file_data.event_id)
|
391
438
|
self.file_data = file_data
|
392
439
|
|
@@ -453,7 +500,12 @@ class DownloadDecryptMedia:
|
|
453
500
|
return await self.client.extract_segments_m3u8(str(media_api))
|
454
501
|
|
455
502
|
async def _get_file(self, url: str, aes_key: str, full_filename: str) -> bool:
|
456
|
-
"""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
|
+
"""
|
457
509
|
|
458
510
|
full_file_path = await self.get_fpath(full_filename)
|
459
511
|
if full_file_path.exists():
|
@@ -481,7 +533,11 @@ class DownloadDecryptMedia:
|
|
481
533
|
return False
|
482
534
|
|
483
535
|
async def _save_file(self, content: bytes, filename: str) -> Path:
|
484
|
-
"""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
|
+
"""
|
485
541
|
file_path = await self.get_fpath(filename)
|
486
542
|
try:
|
487
543
|
# Ensure the directory exists
|
@@ -528,11 +584,11 @@ class DownloadDecryptMedia:
|
|
528
584
|
Path(file_path).unlink()
|
529
585
|
return decrypted_data
|
530
586
|
|
531
|
-
async def _concat_segments(self, ts_files: list[Path], output_file):
|
532
|
-
"""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.
|
533
589
|
|
534
|
-
:param ts_files: List of absolute paths of .
|
535
|
-
: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")
|
536
592
|
"""
|
537
593
|
full_output_file = await self.get_fpath(output_file)
|
538
594
|
if full_output_file.exists():
|
@@ -580,7 +636,9 @@ class DownloadDecryptMedia:
|
|
580
636
|
_LOGGER.error("OS error during concatenation: %s", e)
|
581
637
|
|
582
638
|
async def _delete_segments(self, ts_files: list[Path]) -> None:
|
583
|
-
"""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
|
+
"""
|
584
642
|
for file in ts_files:
|
585
643
|
if file.exists():
|
586
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
|
-
pypetkitapi/litter_container.py,sha256=
|
10
|
-
pypetkitapi/media.py,sha256=
|
9
|
+
pypetkitapi/litter_container.py,sha256=KWvHNAOJ6hDSeJ_55tqtzY9GxHtd9gAntPkbnVbdb-I,19275
|
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
|