smart-media-manager 0.5.43a4__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.
- smart_media_manager/__init__.py +15 -0
- smart_media_manager/cli.py +4941 -0
- smart_media_manager/format_compatibility.json +628 -0
- smart_media_manager/format_registry.json +11874 -0
- smart_media_manager/format_registry.py +491 -0
- smart_media_manager/format_rules.py +677 -0
- smart_media_manager/metadata_registry.json +1113 -0
- smart_media_manager/metadata_registry.py +229 -0
- smart_media_manager/uuid_generator.py +140 -0
- smart_media_manager-0.5.43a4.dist-info/METADATA +340 -0
- smart_media_manager-0.5.43a4.dist-info/RECORD +15 -0
- smart_media_manager-0.5.43a4.dist-info/WHEEL +5 -0
- smart_media_manager-0.5.43a4.dist-info/entry_points.txt +2 -0
- smart_media_manager-0.5.43a4.dist-info/licenses/LICENSE +21 -0
- smart_media_manager-0.5.43a4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,677 @@
|
|
|
1
|
+
"""Machine-readable Apple Photos format rules for SMART_MEDIA_MANAGER.
|
|
2
|
+
|
|
3
|
+
This module is generated from APPLE_PHOTOS_FORMAT_RULES.md. Each rule describes:
|
|
4
|
+
|
|
5
|
+
* rule_id: stable identifier (e.g., "R-IMG-001").
|
|
6
|
+
* category: high-level grouping (image, raw, video, vector).
|
|
7
|
+
* action: deterministic handler (import, convert, rewrap, skip, etc.).
|
|
8
|
+
* identifiers: canonical values emitted by detection tiers.
|
|
9
|
+
* conditions: optional constraints (e.g., animation, size thresholds).
|
|
10
|
+
|
|
11
|
+
Helper utilities provide lookup by extension/identifiers so the CLI can
|
|
12
|
+
automatically choose the correct processing path.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from typing import Iterable, Optional
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class FormatRule:
|
|
23
|
+
rule_id: str
|
|
24
|
+
category: str
|
|
25
|
+
action: str
|
|
26
|
+
extensions: tuple[str, ...]
|
|
27
|
+
libmagic: tuple[str, ...]
|
|
28
|
+
puremagic: tuple[str, ...]
|
|
29
|
+
pyfsig: tuple[str, ...]
|
|
30
|
+
binwalk: tuple[str, ...]
|
|
31
|
+
rawpy: tuple[str, ...]
|
|
32
|
+
ffprobe: tuple[str, ...]
|
|
33
|
+
conditions: dict[str, object]
|
|
34
|
+
notes: str
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _rule(
|
|
38
|
+
*,
|
|
39
|
+
rule_id: str,
|
|
40
|
+
category: str,
|
|
41
|
+
action: str,
|
|
42
|
+
extensions: Iterable[str],
|
|
43
|
+
libmagic: Iterable[str] = (),
|
|
44
|
+
puremagic: Iterable[str] = (),
|
|
45
|
+
pyfsig: Iterable[str] = (),
|
|
46
|
+
binwalk: Iterable[str] = (),
|
|
47
|
+
rawpy: Iterable[str] = (),
|
|
48
|
+
ffprobe: Iterable[str] = (),
|
|
49
|
+
conditions: Optional[dict[str, object]] = None,
|
|
50
|
+
notes: str = "",
|
|
51
|
+
) -> FormatRule:
|
|
52
|
+
return FormatRule(
|
|
53
|
+
rule_id=rule_id,
|
|
54
|
+
category=category,
|
|
55
|
+
action=action,
|
|
56
|
+
extensions=tuple(sorted({ext.lower() for ext in extensions})),
|
|
57
|
+
libmagic=tuple(sorted({ident.lower() for ident in libmagic})),
|
|
58
|
+
puremagic=tuple(sorted({ident.lower() for ident in puremagic})),
|
|
59
|
+
pyfsig=tuple(sorted({ident.lower() for ident in pyfsig})),
|
|
60
|
+
binwalk=tuple(sorted({ident.lower() for ident in binwalk})),
|
|
61
|
+
rawpy=tuple(sorted({ident.lower() for ident in rawpy})),
|
|
62
|
+
ffprobe=tuple(sorted({ident.lower() for ident in ffprobe})),
|
|
63
|
+
conditions=conditions or {},
|
|
64
|
+
notes=notes,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
FORMAT_RULES: tuple[FormatRule, ...] = (
|
|
69
|
+
_rule(
|
|
70
|
+
rule_id="R-IMG-001",
|
|
71
|
+
category="image",
|
|
72
|
+
action="import",
|
|
73
|
+
extensions=[".jpg", ".jpeg"],
|
|
74
|
+
libmagic=["image/jpeg", "jpeg image data"],
|
|
75
|
+
puremagic=["jpeg", "image/jpeg"],
|
|
76
|
+
pyfsig=["jpeg image file"],
|
|
77
|
+
binwalk=["jpeg image data"],
|
|
78
|
+
notes="Standard JPEG",
|
|
79
|
+
),
|
|
80
|
+
_rule(
|
|
81
|
+
rule_id="R-IMG-002",
|
|
82
|
+
category="image",
|
|
83
|
+
action="import",
|
|
84
|
+
extensions=[".png"],
|
|
85
|
+
libmagic=["image/png"],
|
|
86
|
+
puremagic=["png", "image/png"],
|
|
87
|
+
pyfsig=["png image"],
|
|
88
|
+
binwalk=["png image"],
|
|
89
|
+
notes="Portable Network Graphics",
|
|
90
|
+
),
|
|
91
|
+
_rule(
|
|
92
|
+
rule_id="R-IMG-003",
|
|
93
|
+
category="image",
|
|
94
|
+
action="import",
|
|
95
|
+
extensions=[".heic", ".heif"],
|
|
96
|
+
libmagic=["image/heic", "iso media, heif"],
|
|
97
|
+
puremagic=["heic", "image/heic"],
|
|
98
|
+
pyfsig=["iso base media (heic)"],
|
|
99
|
+
binwalk=["heif"],
|
|
100
|
+
notes="HEIF/HEIC",
|
|
101
|
+
),
|
|
102
|
+
_rule(
|
|
103
|
+
rule_id="R-IMG-004",
|
|
104
|
+
category="image",
|
|
105
|
+
action="import",
|
|
106
|
+
extensions=[".gif"],
|
|
107
|
+
libmagic=["image/gif"],
|
|
108
|
+
puremagic=["gif", "image/gif"],
|
|
109
|
+
pyfsig=["gif image"],
|
|
110
|
+
binwalk=["gif image data"],
|
|
111
|
+
conditions={"animated": False},
|
|
112
|
+
notes="Static GIF",
|
|
113
|
+
),
|
|
114
|
+
_rule(
|
|
115
|
+
rule_id="R-IMG-005",
|
|
116
|
+
category="image",
|
|
117
|
+
action="import",
|
|
118
|
+
extensions=[".gif"],
|
|
119
|
+
libmagic=["image/gif"],
|
|
120
|
+
puremagic=["gif", "image/gif"],
|
|
121
|
+
pyfsig=["gif image"],
|
|
122
|
+
binwalk=["gif image data"],
|
|
123
|
+
conditions={"animated": True, "max_size_mb": 100},
|
|
124
|
+
notes="Animated GIF under Apple size limit",
|
|
125
|
+
),
|
|
126
|
+
_rule(
|
|
127
|
+
rule_id="R-IMG-006",
|
|
128
|
+
category="image",
|
|
129
|
+
action="convert_animation_to_hevc_mp4",
|
|
130
|
+
extensions=[".gif"],
|
|
131
|
+
libmagic=["image/gif"],
|
|
132
|
+
puremagic=["gif", "image/gif"],
|
|
133
|
+
pyfsig=["gif image"],
|
|
134
|
+
binwalk=["gif image data"],
|
|
135
|
+
conditions={"animated": True, "min_size_mb": 100},
|
|
136
|
+
notes="Animated GIF above Photos limit",
|
|
137
|
+
),
|
|
138
|
+
_rule(
|
|
139
|
+
rule_id="R-IMG-007",
|
|
140
|
+
category="image",
|
|
141
|
+
action="import",
|
|
142
|
+
extensions=[".tif", ".tiff"],
|
|
143
|
+
libmagic=["image/tiff"],
|
|
144
|
+
puremagic=["tiff", "image/tiff"],
|
|
145
|
+
pyfsig=["tiff image"],
|
|
146
|
+
binwalk=["tiff image data"],
|
|
147
|
+
notes="Tagged Image File Format",
|
|
148
|
+
),
|
|
149
|
+
_rule(
|
|
150
|
+
rule_id="R-IMG-008",
|
|
151
|
+
category="image",
|
|
152
|
+
action="import",
|
|
153
|
+
extensions=[".psd"],
|
|
154
|
+
libmagic=["application/photoshop"],
|
|
155
|
+
puremagic=["psd", "image/vnd.adobe.photoshop"],
|
|
156
|
+
pyfsig=["adobe photoshop image"],
|
|
157
|
+
binwalk=["photoshop image data"],
|
|
158
|
+
conditions={"psd_color_mode": "rgb"},
|
|
159
|
+
notes="Adobe Photoshop (RGB)",
|
|
160
|
+
),
|
|
161
|
+
_rule(
|
|
162
|
+
rule_id="R-IMG-009",
|
|
163
|
+
category="image",
|
|
164
|
+
action="convert_to_png",
|
|
165
|
+
extensions=[".psd"],
|
|
166
|
+
libmagic=["application/photoshop"],
|
|
167
|
+
puremagic=["psd", "image/vnd.adobe.photoshop"],
|
|
168
|
+
pyfsig=["adobe photoshop image"],
|
|
169
|
+
binwalk=["photoshop image data"],
|
|
170
|
+
conditions={"psd_color_mode": "non-rgb"},
|
|
171
|
+
notes="PSD CMYK/multichannel",
|
|
172
|
+
),
|
|
173
|
+
_rule(
|
|
174
|
+
rule_id="R-IMG-010",
|
|
175
|
+
category="image",
|
|
176
|
+
action="convert_to_png",
|
|
177
|
+
extensions=[".webp"],
|
|
178
|
+
libmagic=["image/webp"],
|
|
179
|
+
puremagic=["webp", "image/webp"],
|
|
180
|
+
pyfsig=["google webp image"],
|
|
181
|
+
binwalk=["webp"],
|
|
182
|
+
notes="WebP still",
|
|
183
|
+
),
|
|
184
|
+
_rule(
|
|
185
|
+
rule_id="R-IMG-011",
|
|
186
|
+
category="image",
|
|
187
|
+
action="convert_animation_to_hevc_mp4",
|
|
188
|
+
extensions=[".webp"],
|
|
189
|
+
libmagic=["image/webp"],
|
|
190
|
+
puremagic=["webp", "image/webp"],
|
|
191
|
+
pyfsig=["google webp image"],
|
|
192
|
+
binwalk=["webp"],
|
|
193
|
+
conditions={"animated": True},
|
|
194
|
+
notes="Animated WebP",
|
|
195
|
+
),
|
|
196
|
+
_rule(
|
|
197
|
+
rule_id="R-IMG-012",
|
|
198
|
+
category="image",
|
|
199
|
+
action="convert_to_png",
|
|
200
|
+
extensions=[".avif"],
|
|
201
|
+
libmagic=["image/avif"],
|
|
202
|
+
puremagic=["avif", "image/avif"],
|
|
203
|
+
pyfsig=["avif image"],
|
|
204
|
+
binwalk=["avif"],
|
|
205
|
+
notes="AVIF still",
|
|
206
|
+
),
|
|
207
|
+
_rule(
|
|
208
|
+
rule_id="R-IMG-013",
|
|
209
|
+
category="image",
|
|
210
|
+
action="convert_to_heic_lossless",
|
|
211
|
+
extensions=[".jxl"],
|
|
212
|
+
libmagic=["image/jxl"],
|
|
213
|
+
puremagic=["jxl", "image/jxl"],
|
|
214
|
+
pyfsig=["jpeg xl image"],
|
|
215
|
+
binwalk=["jpeg xl"],
|
|
216
|
+
notes="JPEG XL",
|
|
217
|
+
),
|
|
218
|
+
_rule(
|
|
219
|
+
rule_id="R-IMG-014",
|
|
220
|
+
category="image",
|
|
221
|
+
action="convert_animation_to_hevc_mp4",
|
|
222
|
+
extensions=[".png"],
|
|
223
|
+
libmagic=["image/png"],
|
|
224
|
+
puremagic=["png", "image/png"],
|
|
225
|
+
pyfsig=["png image"],
|
|
226
|
+
binwalk=["png image"],
|
|
227
|
+
conditions={"animated": True},
|
|
228
|
+
notes="Animated PNG (APNG)",
|
|
229
|
+
),
|
|
230
|
+
_rule(
|
|
231
|
+
rule_id="R-IMG-015",
|
|
232
|
+
category="image",
|
|
233
|
+
action="import",
|
|
234
|
+
extensions=[".bmp"],
|
|
235
|
+
libmagic=["image/bmp"],
|
|
236
|
+
puremagic=["bmp", "image/bmp"],
|
|
237
|
+
pyfsig=["bmp image"],
|
|
238
|
+
binwalk=["pc bitmap"],
|
|
239
|
+
notes="Bitmap",
|
|
240
|
+
),
|
|
241
|
+
_rule(
|
|
242
|
+
rule_id="R-IMG-016",
|
|
243
|
+
category="vector",
|
|
244
|
+
action="skip_vector",
|
|
245
|
+
extensions=[
|
|
246
|
+
".svg",
|
|
247
|
+
".ai",
|
|
248
|
+
".eps",
|
|
249
|
+
".ps",
|
|
250
|
+
".pdf",
|
|
251
|
+
".wmf",
|
|
252
|
+
".emf",
|
|
253
|
+
".drw",
|
|
254
|
+
".tex",
|
|
255
|
+
],
|
|
256
|
+
libmagic=["image/svg+xml", "application/postscript", "application/pdf"],
|
|
257
|
+
puremagic=["svg", "postscript", "pdf"],
|
|
258
|
+
pyfsig=["postscript document", "pdf document", "svg document"],
|
|
259
|
+
binwalk=["pdf document", "postscript"],
|
|
260
|
+
notes="Vector formats unsupported by Photos",
|
|
261
|
+
),
|
|
262
|
+
# RAW rules
|
|
263
|
+
_rule(
|
|
264
|
+
rule_id="R-RAW-001",
|
|
265
|
+
category="raw",
|
|
266
|
+
action="import",
|
|
267
|
+
extensions=[".crw", ".cr2", ".cr3", ".crm", ".crx"],
|
|
268
|
+
libmagic=["image/x-canon-cr2", "application/octet-stream"],
|
|
269
|
+
puremagic=["cr2"],
|
|
270
|
+
pyfsig=["canon cr2 raw image"],
|
|
271
|
+
binwalk=["canon raw"],
|
|
272
|
+
rawpy=["canon"],
|
|
273
|
+
notes="Canon RAW",
|
|
274
|
+
),
|
|
275
|
+
_rule(
|
|
276
|
+
rule_id="R-RAW-002",
|
|
277
|
+
category="raw",
|
|
278
|
+
action="import",
|
|
279
|
+
extensions=[".nef", ".nrw"],
|
|
280
|
+
libmagic=["image/x-nikon-nef"],
|
|
281
|
+
puremagic=["nef"],
|
|
282
|
+
pyfsig=["nikon nef raw image"],
|
|
283
|
+
binwalk=["nikon raw"],
|
|
284
|
+
rawpy=["nikon"],
|
|
285
|
+
notes="Nikon RAW",
|
|
286
|
+
),
|
|
287
|
+
_rule(
|
|
288
|
+
rule_id="R-RAW-003",
|
|
289
|
+
category="raw",
|
|
290
|
+
action="import",
|
|
291
|
+
extensions=[".arw", ".srf", ".sr2"],
|
|
292
|
+
libmagic=["image/x-sony-arw"],
|
|
293
|
+
puremagic=["arw"],
|
|
294
|
+
pyfsig=["sony arw raw image"],
|
|
295
|
+
binwalk=["sony raw"],
|
|
296
|
+
rawpy=["sony"],
|
|
297
|
+
notes="Sony RAW",
|
|
298
|
+
),
|
|
299
|
+
_rule(
|
|
300
|
+
rule_id="R-RAW-004",
|
|
301
|
+
category="raw",
|
|
302
|
+
action="import",
|
|
303
|
+
extensions=[".raf"],
|
|
304
|
+
libmagic=["image/x-fuji-raf"],
|
|
305
|
+
puremagic=["raf"],
|
|
306
|
+
pyfsig=["fujifilm raf raw image"],
|
|
307
|
+
binwalk=["fujifilm raw"],
|
|
308
|
+
rawpy=["fujifilm"],
|
|
309
|
+
notes="Fujifilm RAW",
|
|
310
|
+
),
|
|
311
|
+
_rule(
|
|
312
|
+
rule_id="R-RAW-005",
|
|
313
|
+
category="raw",
|
|
314
|
+
action="import",
|
|
315
|
+
extensions=[".orf"],
|
|
316
|
+
libmagic=["image/x-olympus-orf"],
|
|
317
|
+
puremagic=["orf"],
|
|
318
|
+
pyfsig=["olympus orf raw image"],
|
|
319
|
+
binwalk=["olympus raw"],
|
|
320
|
+
rawpy=["olympus"],
|
|
321
|
+
notes="Olympus RAW",
|
|
322
|
+
),
|
|
323
|
+
_rule(
|
|
324
|
+
rule_id="R-RAW-006",
|
|
325
|
+
category="raw",
|
|
326
|
+
action="import",
|
|
327
|
+
extensions=[".rw2", ".raw"],
|
|
328
|
+
libmagic=["image/x-panasonic-rw2"],
|
|
329
|
+
puremagic=["rw2"],
|
|
330
|
+
pyfsig=["panasonic rw2 raw image"],
|
|
331
|
+
binwalk=["panasonic raw"],
|
|
332
|
+
rawpy=["panasonic"],
|
|
333
|
+
notes="Panasonic RAW",
|
|
334
|
+
),
|
|
335
|
+
_rule(
|
|
336
|
+
rule_id="R-RAW-007",
|
|
337
|
+
category="raw",
|
|
338
|
+
action="import",
|
|
339
|
+
extensions=[".pef", ".dng"],
|
|
340
|
+
libmagic=["image/x-pentax-pef", "image/x-adobe-dng"],
|
|
341
|
+
puremagic=["pef", "dng"],
|
|
342
|
+
pyfsig=["pentax pef raw image", "adobe dng"],
|
|
343
|
+
binwalk=["pentax raw", "dng image"],
|
|
344
|
+
rawpy=["pentax", "ricoh", "adobe"],
|
|
345
|
+
notes="Pentax/Adobe DNG",
|
|
346
|
+
),
|
|
347
|
+
_rule(
|
|
348
|
+
rule_id="R-RAW-008",
|
|
349
|
+
category="raw",
|
|
350
|
+
action="import",
|
|
351
|
+
extensions=[".3fr", ".fff", ".iiq", ".cap"],
|
|
352
|
+
libmagic=["image/x-hasselblad-3fr"],
|
|
353
|
+
puremagic=["3fr", "iiq"],
|
|
354
|
+
pyfsig=["hasselblad raw", "phaseone raw"],
|
|
355
|
+
binwalk=["hasselblad raw"],
|
|
356
|
+
rawpy=["hasselblad", "phase one"],
|
|
357
|
+
notes="Medium-format RAW",
|
|
358
|
+
),
|
|
359
|
+
_rule(
|
|
360
|
+
rule_id="R-RAW-009",
|
|
361
|
+
category="raw",
|
|
362
|
+
action="import",
|
|
363
|
+
extensions=[".x3f"],
|
|
364
|
+
libmagic=["image/x-sigma-x3f"],
|
|
365
|
+
puremagic=["x3f"],
|
|
366
|
+
pyfsig=["sigma x3f raw"],
|
|
367
|
+
binwalk=["sigma raw"],
|
|
368
|
+
rawpy=["sigma"],
|
|
369
|
+
notes="Sigma RAW",
|
|
370
|
+
),
|
|
371
|
+
_rule(
|
|
372
|
+
rule_id="R-RAW-010",
|
|
373
|
+
category="raw",
|
|
374
|
+
action="import",
|
|
375
|
+
extensions=[".gpr"],
|
|
376
|
+
libmagic=["image/x-gopro-gpr"],
|
|
377
|
+
puremagic=["gpr"],
|
|
378
|
+
pyfsig=["gopro gpr raw"],
|
|
379
|
+
binwalk=["gopro raw"],
|
|
380
|
+
rawpy=["gopro"],
|
|
381
|
+
notes="GoPro RAW",
|
|
382
|
+
),
|
|
383
|
+
_rule(
|
|
384
|
+
rule_id="R-RAW-011",
|
|
385
|
+
category="raw",
|
|
386
|
+
action="import",
|
|
387
|
+
extensions=[".dng"],
|
|
388
|
+
libmagic=["image/x-adobe-dng"],
|
|
389
|
+
puremagic=["dng"],
|
|
390
|
+
pyfsig=["adobe dng"],
|
|
391
|
+
binwalk=["dng image"],
|
|
392
|
+
rawpy=["dji"],
|
|
393
|
+
notes="DJI DNG",
|
|
394
|
+
),
|
|
395
|
+
_rule(
|
|
396
|
+
rule_id="R-RAW-012",
|
|
397
|
+
category="raw",
|
|
398
|
+
action="skip_raw_unsupported",
|
|
399
|
+
extensions=[".raw", ".unknown"],
|
|
400
|
+
libmagic=["application/octet-stream"],
|
|
401
|
+
puremagic=["None"],
|
|
402
|
+
pyfsig=["Unknown file type"],
|
|
403
|
+
binwalk=["unknown"],
|
|
404
|
+
notes="Unknown RAW",
|
|
405
|
+
),
|
|
406
|
+
# Video rules
|
|
407
|
+
_rule(
|
|
408
|
+
rule_id="R-VID-001a",
|
|
409
|
+
category="video",
|
|
410
|
+
action="rewrap_to_mp4",
|
|
411
|
+
extensions=[".m4v"],
|
|
412
|
+
libmagic=["video/mp4", "video/x-m4v", "iso media, mp4 base media"],
|
|
413
|
+
puremagic=["m4v", "video/x-m4v"],
|
|
414
|
+
pyfsig=["iso base media"],
|
|
415
|
+
binwalk=["mpeg-4 part 14"],
|
|
416
|
+
ffprobe=[
|
|
417
|
+
"video:h264",
|
|
418
|
+
"audio:aac",
|
|
419
|
+
"audio:ac3",
|
|
420
|
+
"audio:eac3",
|
|
421
|
+
"audio:alac",
|
|
422
|
+
"audio:pcm",
|
|
423
|
+
],
|
|
424
|
+
notes="M4V with compatible codecs - remux to MP4",
|
|
425
|
+
),
|
|
426
|
+
_rule(
|
|
427
|
+
rule_id="R-VID-001",
|
|
428
|
+
category="video",
|
|
429
|
+
action="import",
|
|
430
|
+
extensions=[".mp4", ".mov", ".qt"],
|
|
431
|
+
libmagic=["video/mp4", "iso media, mp4 base media"],
|
|
432
|
+
puremagic=["mp4", "video/mp4"],
|
|
433
|
+
pyfsig=["iso base media"],
|
|
434
|
+
binwalk=["mpeg-4 part 14"],
|
|
435
|
+
ffprobe=[
|
|
436
|
+
"video:h264",
|
|
437
|
+
"audio:aac",
|
|
438
|
+
"audio:ac3",
|
|
439
|
+
"audio:eac3",
|
|
440
|
+
"audio:alac",
|
|
441
|
+
"audio:pcm",
|
|
442
|
+
],
|
|
443
|
+
notes="H.264 + AAC/AC-3/E-AC-3/ALAC/PCM",
|
|
444
|
+
),
|
|
445
|
+
_rule(
|
|
446
|
+
rule_id="R-VID-002",
|
|
447
|
+
category="video",
|
|
448
|
+
action="import",
|
|
449
|
+
extensions=[".mp4", ".mov", ".hevc", ".qt"],
|
|
450
|
+
libmagic=["video/h265", "iso media, mp4 base media"],
|
|
451
|
+
puremagic=["hevc", "video/h265"],
|
|
452
|
+
pyfsig=["iso base media"],
|
|
453
|
+
binwalk=["hevc"],
|
|
454
|
+
ffprobe=["video:hevc", "audio:aac", "audio:ac3", "audio:eac3", "audio:alac"],
|
|
455
|
+
notes="HEVC + AAC/AC-3/E-AC-3/ALAC",
|
|
456
|
+
),
|
|
457
|
+
_rule(
|
|
458
|
+
rule_id="R-VID-003",
|
|
459
|
+
category="video",
|
|
460
|
+
action="import",
|
|
461
|
+
extensions=[".mp4", ".mov", ".qt"],
|
|
462
|
+
libmagic=["video/mp4"],
|
|
463
|
+
puremagic=["mp4", "video/mp4"],
|
|
464
|
+
pyfsig=["iso base media"],
|
|
465
|
+
binwalk=["dolby vision"],
|
|
466
|
+
ffprobe=["video:hevc", "dolby_vision", "audio:eac3"],
|
|
467
|
+
notes="Dolby Vision + Atmos",
|
|
468
|
+
),
|
|
469
|
+
_rule(
|
|
470
|
+
rule_id="R-VID-004",
|
|
471
|
+
category="video",
|
|
472
|
+
action="transcode_video_to_lossless_hevc",
|
|
473
|
+
extensions=[".mp4", ".mov", ".qt"],
|
|
474
|
+
libmagic=["video/mp4", "iso media, mp4 base media"],
|
|
475
|
+
puremagic=["mp4", "video/mp4"],
|
|
476
|
+
pyfsig=["iso base media"],
|
|
477
|
+
binwalk=["mpeg-4 part 14"],
|
|
478
|
+
ffprobe=["video:vp9", "video:av1", "video:mpeg2video"],
|
|
479
|
+
notes="Unsupported video codec inside MP4",
|
|
480
|
+
),
|
|
481
|
+
_rule(
|
|
482
|
+
rule_id="R-VID-005",
|
|
483
|
+
category="video",
|
|
484
|
+
action="transcode_audio_to_aac_or_eac3",
|
|
485
|
+
extensions=[".mp4", ".mov", ".qt"],
|
|
486
|
+
libmagic=["video/mp4"],
|
|
487
|
+
puremagic=["mp4", "video/mp4"],
|
|
488
|
+
pyfsig=["iso base media"],
|
|
489
|
+
binwalk=["mpeg-4 part 14"],
|
|
490
|
+
ffprobe=["audio:opus", "audio:dts", "audio:truehd"],
|
|
491
|
+
notes="Unsupported audio codec inside MP4/MOV",
|
|
492
|
+
),
|
|
493
|
+
_rule(
|
|
494
|
+
rule_id="R-VID-006",
|
|
495
|
+
category="video",
|
|
496
|
+
action="rewrap_to_mp4",
|
|
497
|
+
extensions=[".mkv"],
|
|
498
|
+
libmagic=["video/x-matroska"],
|
|
499
|
+
puremagic=["mkv", "video/x-matroska"],
|
|
500
|
+
pyfsig=["matroska data"],
|
|
501
|
+
binwalk=["matroska"],
|
|
502
|
+
ffprobe=["video:h264", "video:hevc"],
|
|
503
|
+
notes="Matroska container with compatible codecs",
|
|
504
|
+
),
|
|
505
|
+
_rule(
|
|
506
|
+
rule_id="R-VID-007",
|
|
507
|
+
category="video",
|
|
508
|
+
action="transcode_to_hevc_mp4",
|
|
509
|
+
extensions=[".mkv", ".webm"],
|
|
510
|
+
libmagic=["video/x-matroska", "video/webm"],
|
|
511
|
+
puremagic=["webm", "video/webm"],
|
|
512
|
+
pyfsig=["matroska data", "webm"],
|
|
513
|
+
binwalk=["webm"],
|
|
514
|
+
ffprobe=["video:vp9", "audio:opus"],
|
|
515
|
+
notes="VP9/Opus containers",
|
|
516
|
+
),
|
|
517
|
+
_rule(
|
|
518
|
+
rule_id="R-VID-008",
|
|
519
|
+
category="video",
|
|
520
|
+
action="transcode_to_hevc_mp4",
|
|
521
|
+
extensions=[".avi"],
|
|
522
|
+
libmagic=["video/x-msvideo"],
|
|
523
|
+
puremagic=["avi", "video/x-msvideo"],
|
|
524
|
+
pyfsig=["riff avi"],
|
|
525
|
+
binwalk=["avi"],
|
|
526
|
+
notes="AVI container",
|
|
527
|
+
),
|
|
528
|
+
_rule(
|
|
529
|
+
rule_id="R-VID-009",
|
|
530
|
+
category="video",
|
|
531
|
+
action="transcode_to_hevc_mp4",
|
|
532
|
+
extensions=[".wmv"],
|
|
533
|
+
libmagic=["video/x-ms-wmv"],
|
|
534
|
+
puremagic=["wmv", "video/x-ms-wmv"],
|
|
535
|
+
pyfsig=["asf/wmv"],
|
|
536
|
+
binwalk=["microsoft asf"],
|
|
537
|
+
notes="Windows Media",
|
|
538
|
+
),
|
|
539
|
+
_rule(
|
|
540
|
+
rule_id="R-VID-010",
|
|
541
|
+
category="video",
|
|
542
|
+
action="transcode_to_hevc_mp4",
|
|
543
|
+
extensions=[".flv"],
|
|
544
|
+
libmagic=["video/x-flv"],
|
|
545
|
+
puremagic=["flv", "video/x-flv"],
|
|
546
|
+
pyfsig=["flash video"],
|
|
547
|
+
binwalk=["flv"],
|
|
548
|
+
notes="Flash Video",
|
|
549
|
+
),
|
|
550
|
+
_rule(
|
|
551
|
+
rule_id="R-VID-011",
|
|
552
|
+
category="video",
|
|
553
|
+
action="rewrap_or_transcode_to_mp4",
|
|
554
|
+
extensions=[".3gp", ".3g2"],
|
|
555
|
+
libmagic=["video/3gpp", "video/3gpp2"],
|
|
556
|
+
puremagic=["3gp", "3g2"],
|
|
557
|
+
pyfsig=["3gpp multimedia"],
|
|
558
|
+
binwalk=["3gp"],
|
|
559
|
+
notes="3GPP container",
|
|
560
|
+
),
|
|
561
|
+
_rule(
|
|
562
|
+
rule_id="R-VID-012",
|
|
563
|
+
category="video",
|
|
564
|
+
action="skip_unknown_video",
|
|
565
|
+
extensions=[".unknown"],
|
|
566
|
+
notes="Unhandled/legacy video",
|
|
567
|
+
),
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
EXTENSION_INDEX: dict[str, list[FormatRule]] = {}
|
|
572
|
+
for rule in FORMAT_RULES:
|
|
573
|
+
for ext in rule.extensions:
|
|
574
|
+
EXTENSION_INDEX.setdefault(ext, []).append(rule)
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def find_rules_by_extension(extension: Optional[str]) -> list[FormatRule]:
|
|
578
|
+
if not extension:
|
|
579
|
+
return []
|
|
580
|
+
ext = extension.lower()
|
|
581
|
+
if not ext.startswith("."):
|
|
582
|
+
ext = f".{ext}"
|
|
583
|
+
return EXTENSION_INDEX.get(ext, [])
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def match_rule(
|
|
587
|
+
*,
|
|
588
|
+
extension: Optional[str] = None,
|
|
589
|
+
libmagic: Optional[Iterable[str] | str] = None,
|
|
590
|
+
puremagic: Optional[Iterable[str] | str] = None,
|
|
591
|
+
pyfsig: Optional[Iterable[str] | str] = None,
|
|
592
|
+
binwalk: Optional[Iterable[str] | str] = None,
|
|
593
|
+
rawpy: Optional[Iterable[str] | str] = None,
|
|
594
|
+
ffprobe_streams: Optional[Iterable[str]] = None,
|
|
595
|
+
animated: Optional[bool] = None,
|
|
596
|
+
size_bytes: Optional[int] = None,
|
|
597
|
+
psd_color_mode: Optional[str] = None,
|
|
598
|
+
) -> Optional[FormatRule]:
|
|
599
|
+
"""Return the first rule that matches the supplied identifiers."""
|
|
600
|
+
|
|
601
|
+
candidates = list(FORMAT_RULES)
|
|
602
|
+
if extension:
|
|
603
|
+
candidates = find_rules_by_extension(extension) or candidates
|
|
604
|
+
|
|
605
|
+
def normalise_iter(value: Optional[Iterable[str] | str]) -> set[str]:
|
|
606
|
+
if value is None:
|
|
607
|
+
return set()
|
|
608
|
+
if isinstance(value, str):
|
|
609
|
+
value_lower = value.lower()
|
|
610
|
+
str_result = {value_lower}
|
|
611
|
+
if value_lower.startswith(".") and "/" not in value_lower:
|
|
612
|
+
stripped = value_lower.lstrip(".")
|
|
613
|
+
if stripped:
|
|
614
|
+
str_result.add(stripped)
|
|
615
|
+
return str_result
|
|
616
|
+
iter_result: set[str] = set()
|
|
617
|
+
for entry in value:
|
|
618
|
+
if entry:
|
|
619
|
+
lowered = entry.lower()
|
|
620
|
+
iter_result.add(lowered)
|
|
621
|
+
if lowered.startswith(".") and "/" not in lowered:
|
|
622
|
+
stripped = lowered.lstrip(".")
|
|
623
|
+
if stripped:
|
|
624
|
+
iter_result.add(stripped)
|
|
625
|
+
return iter_result
|
|
626
|
+
|
|
627
|
+
libmagic_set = normalise_iter(libmagic)
|
|
628
|
+
puremagic_set = normalise_iter(puremagic)
|
|
629
|
+
pyfsig_set = normalise_iter(pyfsig)
|
|
630
|
+
binwalk_set = normalise_iter(binwalk)
|
|
631
|
+
rawpy_set = normalise_iter(rawpy)
|
|
632
|
+
ffprobe_set = normalise_iter(ffprobe_streams)
|
|
633
|
+
|
|
634
|
+
for rule in candidates:
|
|
635
|
+
if rule.libmagic and libmagic_set and not libmagic_set & set(rule.libmagic):
|
|
636
|
+
continue
|
|
637
|
+
if rule.puremagic and puremagic_set and not puremagic_set & set(rule.puremagic):
|
|
638
|
+
continue
|
|
639
|
+
if rule.pyfsig and pyfsig_set and not pyfsig_set & set(rule.pyfsig):
|
|
640
|
+
pass
|
|
641
|
+
if rule.binwalk and binwalk_set and not binwalk_set & set(rule.binwalk):
|
|
642
|
+
continue
|
|
643
|
+
if rule.rawpy and rawpy_set and not rawpy_set & set(rule.rawpy):
|
|
644
|
+
continue
|
|
645
|
+
if rule.ffprobe and ffprobe_set and not ffprobe_set & set(rule.ffprobe):
|
|
646
|
+
continue
|
|
647
|
+
|
|
648
|
+
conditions = rule.conditions
|
|
649
|
+
if "animated" in conditions and animated is not None:
|
|
650
|
+
if bool(conditions["animated"]) != animated:
|
|
651
|
+
continue
|
|
652
|
+
if "max_size_mb" in conditions and size_bytes is not None:
|
|
653
|
+
max_size_mb = conditions["max_size_mb"]
|
|
654
|
+
if isinstance(max_size_mb, (int, float)) and size_bytes / (1024 * 1024) > float(max_size_mb):
|
|
655
|
+
continue
|
|
656
|
+
if "min_size_mb" in conditions and size_bytes is not None:
|
|
657
|
+
min_size_mb = conditions["min_size_mb"]
|
|
658
|
+
if isinstance(min_size_mb, (int, float)) and size_bytes / (1024 * 1024) < float(min_size_mb):
|
|
659
|
+
continue
|
|
660
|
+
if "psd_color_mode" in conditions and psd_color_mode is not None:
|
|
661
|
+
required_mode = conditions["psd_color_mode"]
|
|
662
|
+
if isinstance(required_mode, str) and required_mode == "rgb" and psd_color_mode.lower() != "rgb":
|
|
663
|
+
continue
|
|
664
|
+
if isinstance(required_mode, str) and required_mode == "non-rgb" and psd_color_mode.lower() == "rgb":
|
|
665
|
+
continue
|
|
666
|
+
|
|
667
|
+
return rule
|
|
668
|
+
|
|
669
|
+
return None
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
__all__ = [
|
|
673
|
+
"FormatRule",
|
|
674
|
+
"FORMAT_RULES",
|
|
675
|
+
"find_rules_by_extension",
|
|
676
|
+
"match_rule",
|
|
677
|
+
]
|