pith-mp4 0.1.1__py3-none-win_amd64.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.
- pith_mp4/__init__.py +405 -0
- pith_mp4/pith_mp4.dll +0 -0
- pith_mp4-0.1.1.dist-info/METADATA +12 -0
- pith_mp4-0.1.1.dist-info/RECORD +6 -0
- pith_mp4-0.1.1.dist-info/WHEEL +5 -0
- pith_mp4-0.1.1.dist-info/top_level.txt +1 -0
pith_mp4/__init__.py
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2026 pith-hash
|
|
3
|
+
"""pith-mp4 SDK: ISO-BMFF demuxing through ctypes.
|
|
4
|
+
|
|
5
|
+
Demuxes a native ISO-BMFF file (ftyp/moov/trak/stbl: sample tables,
|
|
6
|
+
`stts`/`stsc`/`stsz`/`stco`/`co64`/`stss`, `stsd` sample entries with
|
|
7
|
+
`avcC`/`esds`) through the Rust cdylib, handing back the canonical
|
|
8
|
+
record walk the ``reference.json`` vectors are checked against: the
|
|
9
|
+
demuxed shape serialized deterministically, field by field
|
|
10
|
+
(big-endian), exactly as the crate's ``ffi`` module documents it.
|
|
11
|
+
|
|
12
|
+
The cdylib is located through the suite's discovery chain:
|
|
13
|
+
|
|
14
|
+
1. ``PITH_CDYLIB`` — an explicit cdylib *file* path;
|
|
15
|
+
2. ``PITH_CDYLIB_DIR`` — a *directory* scanned for the cdylib names
|
|
16
|
+
(the CD pipeline points this at ``target/release``);
|
|
17
|
+
3. the package directory itself (the built wheel ships the cdylib as
|
|
18
|
+
package data);
|
|
19
|
+
4. ``<repo root>/target/release`` — the repository working-tree layout,
|
|
20
|
+
so a source checkout runs against a local cargo build with no
|
|
21
|
+
configuration.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import ctypes
|
|
27
|
+
import os
|
|
28
|
+
from dataclasses import dataclass
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"Demuxed",
|
|
33
|
+
"Track",
|
|
34
|
+
"Sample",
|
|
35
|
+
"Entry",
|
|
36
|
+
"FfiError",
|
|
37
|
+
"LibraryNotFoundError",
|
|
38
|
+
"find_cdylib",
|
|
39
|
+
"demux_canonical",
|
|
40
|
+
"parse_canonical",
|
|
41
|
+
"STATUS_OK",
|
|
42
|
+
"STATUS_INVALID",
|
|
43
|
+
"STATUS_REJECTED",
|
|
44
|
+
"ERR_BAD_VALUE",
|
|
45
|
+
"ERR_INVALID_MAGIC",
|
|
46
|
+
"ERR_TOO_LARGE",
|
|
47
|
+
"ERR_TRUNCATED",
|
|
48
|
+
"ERR_UNSUPPORTED",
|
|
49
|
+
"ENTRY_VISUAL",
|
|
50
|
+
"ENTRY_AUDIO",
|
|
51
|
+
"ENTRY_OTHER",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
#: Status: success.
|
|
55
|
+
STATUS_OK = 0
|
|
56
|
+
#: Status: a caller argument is invalid (a null pointer).
|
|
57
|
+
STATUS_INVALID = -1
|
|
58
|
+
#: Status: the core demuxer refused the input (no ``ftyp``,
|
|
59
|
+
#: unrecognised brands, missing ``moov``, fragmented file, truncated).
|
|
60
|
+
STATUS_REJECTED = -2
|
|
61
|
+
|
|
62
|
+
#: Refusal kind ``"BadValue"`` (``Error::BadValue``).
|
|
63
|
+
ERR_BAD_VALUE = 1
|
|
64
|
+
#: Refusal kind ``"InvalidMagic"`` (``Error::InvalidMagic``).
|
|
65
|
+
ERR_INVALID_MAGIC = 2
|
|
66
|
+
#: Refusal kind ``"TooLarge"`` (``Error::TooLarge``).
|
|
67
|
+
ERR_TOO_LARGE = 3
|
|
68
|
+
#: Refusal kind ``"Truncated"`` (``Error::Truncated``).
|
|
69
|
+
ERR_TRUNCATED = 4
|
|
70
|
+
#: Refusal kind ``"Unsupported"`` (``Error::Unsupported``).
|
|
71
|
+
ERR_UNSUPPORTED = 5
|
|
72
|
+
|
|
73
|
+
#: ``stsd`` entry kind: a visual sample entry.
|
|
74
|
+
ENTRY_VISUAL = 0
|
|
75
|
+
#: ``stsd`` entry kind: an audio sample entry.
|
|
76
|
+
ENTRY_AUDIO = 1
|
|
77
|
+
#: ``stsd`` entry kind: neither visual nor audio.
|
|
78
|
+
ENTRY_OTHER = 2
|
|
79
|
+
|
|
80
|
+
#: Every cdylib file name cargo may drop into the build directory, per
|
|
81
|
+
#: platform (windows / linux / macOS).
|
|
82
|
+
CDYLIB_NAMES = ("pith_mp4.dll", "libpith_mp4.so", "libpith_mp4.dylib")
|
|
83
|
+
|
|
84
|
+
#: Refusal-kind names by code — the same stable names the ``errors``
|
|
85
|
+
#: of ``reference.json`` record as strings.
|
|
86
|
+
ERR_KIND_NAMES = {
|
|
87
|
+
ERR_BAD_VALUE: "BadValue",
|
|
88
|
+
ERR_INVALID_MAGIC: "InvalidMagic",
|
|
89
|
+
ERR_TOO_LARGE: "TooLarge",
|
|
90
|
+
ERR_TRUNCATED: "Truncated",
|
|
91
|
+
ERR_UNSUPPORTED: "Unsupported",
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
#: ``stsd`` entry-kind names by code — the variant names reference.json
|
|
95
|
+
#: records under ``stsd_entries[].kind``.
|
|
96
|
+
ENTRY_KIND_NAMES = {ENTRY_VISUAL: "Visual", ENTRY_AUDIO: "Audio", ENTRY_OTHER: "Other"}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class LibraryNotFoundError(OSError):
|
|
100
|
+
"""No cdylib was found through the discovery chain."""
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class FfiError(Exception):
|
|
104
|
+
"""A non-zero status code came back from the cdylib."""
|
|
105
|
+
|
|
106
|
+
def __init__(self, op: str, status: int, kind: int = 0) -> None:
|
|
107
|
+
detail = {
|
|
108
|
+
STATUS_INVALID: "invalid argument",
|
|
109
|
+
STATUS_REJECTED: "input rejected",
|
|
110
|
+
}.get(status, "unknown failure")
|
|
111
|
+
if kind:
|
|
112
|
+
detail = f"{detail} ({ERR_KIND_NAMES.get(kind, f'kind {kind}')})"
|
|
113
|
+
super().__init__(f"{op} failed: {detail} (status {status})")
|
|
114
|
+
#: The raw status code the FFI returned.
|
|
115
|
+
self.status = status
|
|
116
|
+
#: The refusal kind code (0 unless the status is
|
|
117
|
+
#: :data:`STATUS_REJECTED`; one of the ``ERR_*`` constants).
|
|
118
|
+
self.kind = kind
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def find_cdylib() -> Path:
|
|
122
|
+
"""Locates the cdylib through the suite's discovery chain."""
|
|
123
|
+
explicit = os.environ.get("PITH_CDYLIB")
|
|
124
|
+
if explicit:
|
|
125
|
+
p = Path(explicit)
|
|
126
|
+
if p.is_file():
|
|
127
|
+
return p
|
|
128
|
+
env_dir = os.environ.get("PITH_CDYLIB_DIR")
|
|
129
|
+
candidates: list[Path] = []
|
|
130
|
+
if env_dir:
|
|
131
|
+
env_dir_path = Path(env_dir)
|
|
132
|
+
candidates.append(env_dir_path)
|
|
133
|
+
if not env_dir_path.is_absolute():
|
|
134
|
+
# CD and local runs invoke tools from the repository root or
|
|
135
|
+
# from sdk/<lang>; resolve the env value against both.
|
|
136
|
+
candidates.append(Path.cwd() / env_dir_path)
|
|
137
|
+
candidates.append(Path(__file__).resolve().parents[3] / env_dir_path)
|
|
138
|
+
candidates.append(Path(__file__).resolve().parent) # packaged wheel
|
|
139
|
+
candidates.append(Path(__file__).resolve().parents[3] / "target" / "release")
|
|
140
|
+
for directory in candidates:
|
|
141
|
+
for name in CDYLIB_NAMES:
|
|
142
|
+
p = directory / name
|
|
143
|
+
if p.is_file():
|
|
144
|
+
return p
|
|
145
|
+
raise LibraryNotFoundError(
|
|
146
|
+
"no pith-mp4 cdylib found (searched PITH_CDYLIB, PITH_CDYLIB_DIR, "
|
|
147
|
+
"the package directory and <repo>/target/release); "
|
|
148
|
+
"run `cargo build --release` first"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
_lib: ctypes.CDLL | None = None
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _load() -> ctypes.CDLL:
|
|
156
|
+
global _lib
|
|
157
|
+
if _lib is None:
|
|
158
|
+
lib = ctypes.CDLL(str(find_cdylib()))
|
|
159
|
+
lib.pith_mp4_demux.argtypes = [
|
|
160
|
+
ctypes.c_void_p, # data
|
|
161
|
+
ctypes.c_size_t, # len
|
|
162
|
+
ctypes.POINTER(ctypes.c_void_p), # out buffer
|
|
163
|
+
ctypes.POINTER(ctypes.c_size_t), # out length
|
|
164
|
+
ctypes.POINTER(ctypes.c_int32), # out refusal kind
|
|
165
|
+
]
|
|
166
|
+
lib.pith_mp4_demux.restype = ctypes.c_int32
|
|
167
|
+
lib.pith_mp4_free.argtypes = [ctypes.c_void_p, ctypes.c_size_t]
|
|
168
|
+
lib.pith_mp4_free.restype = None
|
|
169
|
+
_lib = lib
|
|
170
|
+
return _lib
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def demux_canonical(data: bytes) -> bytes:
|
|
174
|
+
"""Demuxes a complete ISO-BMFF file into the canonical record walk
|
|
175
|
+
the SDK vectors are checked against.
|
|
176
|
+
|
|
177
|
+
Raises :class:`FfiError` with ``status == STATUS_REJECTED`` for any
|
|
178
|
+
refused input; ``err.kind`` then carries one of the ``ERR_*`` kind
|
|
179
|
+
codes the reference.json error vectors pin. The demuxer never
|
|
180
|
+
panics through this boundary.
|
|
181
|
+
"""
|
|
182
|
+
out = ctypes.c_void_p()
|
|
183
|
+
out_len = ctypes.c_size_t()
|
|
184
|
+
err = ctypes.c_int32()
|
|
185
|
+
status = _load().pith_mp4_demux(
|
|
186
|
+
data, len(data), ctypes.byref(out), ctypes.byref(out_len), ctypes.byref(err)
|
|
187
|
+
)
|
|
188
|
+
if status != STATUS_OK:
|
|
189
|
+
raise FfiError("pith_mp4_demux", status, err.value)
|
|
190
|
+
try:
|
|
191
|
+
return ctypes.string_at(out, out_len.value)
|
|
192
|
+
finally:
|
|
193
|
+
_load().pith_mp4_free(out, out_len.value)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@dataclass(frozen=True)
|
|
197
|
+
class Sample:
|
|
198
|
+
"""One sample record of the canonical walk."""
|
|
199
|
+
|
|
200
|
+
#: Absolute byte offset of the sample in the file.
|
|
201
|
+
offset: int
|
|
202
|
+
#: Sample size in bytes.
|
|
203
|
+
size: int
|
|
204
|
+
#: Decoding time in the track timescale.
|
|
205
|
+
decoding: int
|
|
206
|
+
#: Presentation time in the track timescale (may precede decoding
|
|
207
|
+
#: with edit lists / B-frames).
|
|
208
|
+
presentation: int
|
|
209
|
+
#: Sample duration in the track timescale.
|
|
210
|
+
duration: int
|
|
211
|
+
#: Whether the sample is a sync (keyframe) sample.
|
|
212
|
+
keyframe: bool
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@dataclass(frozen=True)
|
|
216
|
+
class Entry:
|
|
217
|
+
"""One ``stsd`` sample entry of the canonical walk."""
|
|
218
|
+
|
|
219
|
+
#: ``"Visual"``, ``"Audio"`` or ``"Other"``.
|
|
220
|
+
kind: str
|
|
221
|
+
#: The four-character coding (``avc1``, ``mp4a``, ...).
|
|
222
|
+
coding: str
|
|
223
|
+
#: Visual only: coded width.
|
|
224
|
+
width: int | None
|
|
225
|
+
#: Visual only: coded height.
|
|
226
|
+
height: int | None
|
|
227
|
+
#: Visual only: ``avcC`` payload, hex or ``None``.
|
|
228
|
+
avcc_hex: str | None
|
|
229
|
+
#: Audio only: channel count.
|
|
230
|
+
channels: int | None
|
|
231
|
+
#: Audio only: sample rate in Hz.
|
|
232
|
+
rate: int | None
|
|
233
|
+
#: ``esds`` payload, hex or ``None``.
|
|
234
|
+
esds_hex: str | None
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@dataclass(frozen=True)
|
|
238
|
+
class Track:
|
|
239
|
+
"""One demuxed track of the canonical walk."""
|
|
240
|
+
|
|
241
|
+
#: Track id from ``tkhd``.
|
|
242
|
+
id: int
|
|
243
|
+
#: Media timescale from ``mdhd``.
|
|
244
|
+
timescale: int
|
|
245
|
+
#: Track duration in the media timescale.
|
|
246
|
+
duration: int
|
|
247
|
+
#: ISO-639-2/T language code, or ``None``.
|
|
248
|
+
language: str | None
|
|
249
|
+
#: Handler type (``vide``, ``soun``, ...).
|
|
250
|
+
handler: str
|
|
251
|
+
#: Visual track width from ``tkhd`` (0 for non-visual).
|
|
252
|
+
width: int
|
|
253
|
+
#: Visual track height from ``tkhd`` (0 for non-visual).
|
|
254
|
+
height: int
|
|
255
|
+
#: All sample records, in sample order.
|
|
256
|
+
samples: list[Sample]
|
|
257
|
+
#: All ``stsd`` sample entries.
|
|
258
|
+
entries: list[Entry]
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@dataclass(frozen=True)
|
|
262
|
+
class Demuxed:
|
|
263
|
+
"""A demuxed ISO-BMFF file, re-expressed from the canonical walk."""
|
|
264
|
+
|
|
265
|
+
#: Major brand of the ``ftyp``.
|
|
266
|
+
major_brand: str
|
|
267
|
+
#: Compatible brands of the ``ftyp``.
|
|
268
|
+
compatible_brands: list[str]
|
|
269
|
+
#: Movie timescale from ``mvhd``.
|
|
270
|
+
timescale: int
|
|
271
|
+
#: Movie duration in the movie timescale.
|
|
272
|
+
duration: int
|
|
273
|
+
#: Demuxed tracks, in file order.
|
|
274
|
+
tracks: list[Track]
|
|
275
|
+
#: The canonical byte stream the walk is parsed from.
|
|
276
|
+
raw: bytes
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class _Reader:
|
|
280
|
+
"""Sequential big-endian reader over the canonical walk."""
|
|
281
|
+
|
|
282
|
+
def __init__(self, raw: bytes) -> None:
|
|
283
|
+
self.raw = raw
|
|
284
|
+
self.pos = 0
|
|
285
|
+
|
|
286
|
+
def take(self, n: int) -> bytes:
|
|
287
|
+
if self.pos + n > len(self.raw):
|
|
288
|
+
raise ValueError("canonical walk is truncated")
|
|
289
|
+
chunk = self.raw[self.pos : self.pos + n]
|
|
290
|
+
self.pos += n
|
|
291
|
+
return chunk
|
|
292
|
+
|
|
293
|
+
def u8(self) -> int:
|
|
294
|
+
return self.take(1)[0]
|
|
295
|
+
|
|
296
|
+
def u32(self) -> int:
|
|
297
|
+
return int.from_bytes(self.take(4), "big")
|
|
298
|
+
|
|
299
|
+
def u64(self) -> int:
|
|
300
|
+
return int.from_bytes(self.take(8), "big")
|
|
301
|
+
|
|
302
|
+
def i64(self) -> int:
|
|
303
|
+
return int.from_bytes(self.take(8), "big", signed=True)
|
|
304
|
+
|
|
305
|
+
def four(self) -> str:
|
|
306
|
+
return self.take(4).decode("latin-1")
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _parse_entry(r: _Reader) -> Entry:
|
|
310
|
+
kind_code = r.u8()
|
|
311
|
+
coding = r.four()
|
|
312
|
+
if kind_code == ENTRY_VISUAL:
|
|
313
|
+
width = r.u32()
|
|
314
|
+
height = r.u32()
|
|
315
|
+
avcc = r.take(r.u32())
|
|
316
|
+
esds = r.take(r.u32())
|
|
317
|
+
return Entry(
|
|
318
|
+
kind=ENTRY_KIND_NAMES[kind_code],
|
|
319
|
+
coding=coding,
|
|
320
|
+
width=width,
|
|
321
|
+
height=height,
|
|
322
|
+
avcc_hex=avcc.hex() if avcc else None,
|
|
323
|
+
channels=None,
|
|
324
|
+
rate=None,
|
|
325
|
+
esds_hex=esds.hex() if esds else None,
|
|
326
|
+
)
|
|
327
|
+
if kind_code == ENTRY_AUDIO:
|
|
328
|
+
channels = r.u32()
|
|
329
|
+
rate = r.u32()
|
|
330
|
+
esds = r.take(r.u32())
|
|
331
|
+
return Entry(
|
|
332
|
+
kind=ENTRY_KIND_NAMES[kind_code],
|
|
333
|
+
coding=coding,
|
|
334
|
+
width=None,
|
|
335
|
+
height=None,
|
|
336
|
+
avcc_hex=None,
|
|
337
|
+
channels=channels,
|
|
338
|
+
rate=rate,
|
|
339
|
+
esds_hex=esds.hex() if esds else None,
|
|
340
|
+
)
|
|
341
|
+
return Entry(
|
|
342
|
+
kind=ENTRY_KIND_NAMES[kind_code],
|
|
343
|
+
coding=coding,
|
|
344
|
+
width=None,
|
|
345
|
+
height=None,
|
|
346
|
+
avcc_hex=None,
|
|
347
|
+
channels=None,
|
|
348
|
+
rate=None,
|
|
349
|
+
esds_hex=None,
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def _parse_track(r: _Reader) -> Track:
|
|
354
|
+
track_id = r.u32()
|
|
355
|
+
timescale = r.u32()
|
|
356
|
+
duration = r.u64()
|
|
357
|
+
language = None
|
|
358
|
+
if r.u8() == 1:
|
|
359
|
+
language = r.take(3).decode("latin-1")
|
|
360
|
+
else:
|
|
361
|
+
r.take(3)
|
|
362
|
+
handler = r.four()
|
|
363
|
+
width = r.u32()
|
|
364
|
+
height = r.u32()
|
|
365
|
+
samples = [
|
|
366
|
+
Sample(
|
|
367
|
+
offset=r.u64(),
|
|
368
|
+
size=r.u32(),
|
|
369
|
+
decoding=r.u64(),
|
|
370
|
+
presentation=r.i64(),
|
|
371
|
+
duration=r.u32(),
|
|
372
|
+
keyframe=r.u8() == 1,
|
|
373
|
+
)
|
|
374
|
+
for _ in range(r.u32())
|
|
375
|
+
]
|
|
376
|
+
entries = [_parse_entry(r) for _ in range(r.u32())]
|
|
377
|
+
return Track(
|
|
378
|
+
id=track_id,
|
|
379
|
+
timescale=timescale,
|
|
380
|
+
duration=duration,
|
|
381
|
+
language=language,
|
|
382
|
+
handler=handler,
|
|
383
|
+
width=width,
|
|
384
|
+
height=height,
|
|
385
|
+
samples=samples,
|
|
386
|
+
entries=entries,
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def parse_canonical(raw: bytes) -> Demuxed:
|
|
391
|
+
"""Re-expresses the canonical record walk as a :class:`Demuxed`."""
|
|
392
|
+
r = _Reader(raw)
|
|
393
|
+
major_brand = r.four()
|
|
394
|
+
compatible_brands = [r.four() for _ in range(r.u32())]
|
|
395
|
+
timescale = r.u32()
|
|
396
|
+
duration = r.u64()
|
|
397
|
+
tracks = [_parse_track(r) for _ in range(r.u32())]
|
|
398
|
+
return Demuxed(
|
|
399
|
+
major_brand=major_brand,
|
|
400
|
+
compatible_brands=compatible_brands,
|
|
401
|
+
timescale=timescale,
|
|
402
|
+
duration=duration,
|
|
403
|
+
tracks=tracks,
|
|
404
|
+
raw=raw,
|
|
405
|
+
)
|
pith_mp4/pith_mp4.dll
ADDED
|
Binary file
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pith-mp4
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: ISO-BMFF demuxing SDK: ftyp/moov sample tables, stts/stsc/stsz/stco/co64/stss and stsd sample entries via the pith-mp4 Rust cdylib (ctypes, zero-dep)
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Repository, https://github.com/pith-hash/pith-mp4
|
|
7
|
+
Project-URL: Homepage, https://github.com/pith-hash/pith-mp4
|
|
8
|
+
Keywords: flac,audio,decoder,pcm,hashing
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Rust
|
|
11
|
+
Classifier: Topic :: Multimedia :: Sound/Audio
|
|
12
|
+
Requires-Python: >=3.9
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
pith_mp4/__init__.py,sha256=aWtXQssnFcF5nqM8sIcmGXdPNWSI2MI_lKzz1EffNBg,12259
|
|
2
|
+
pith_mp4/pith_mp4.dll,sha256=IBzPEfmmszBwZuPd-Nck4t1PKdj21M-NTadgQjlAy8k,160256
|
|
3
|
+
pith_mp4-0.1.1.dist-info/METADATA,sha256=M5ngM3tQxNyUmn5ISaxkERYHJZ9NlKuFX93Vz7rEYJQ,571
|
|
4
|
+
pith_mp4-0.1.1.dist-info/WHEEL,sha256=Zx98gwb_dQKckJK3HEOVKnxxD52EfU_PXDG_usMJ2ng,98
|
|
5
|
+
pith_mp4-0.1.1.dist-info/top_level.txt,sha256=Oxau1mlbt0pDRmdeLXRGiLgcpLG8BsNK3PG5gw0tmJg,9
|
|
6
|
+
pith_mp4-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pith_mp4
|