oxi 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.
oxi-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Domingo Ernesto Savoretti
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.
oxi-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,32 @@
1
+ Metadata-Version: 2.1
2
+ Name: oxi
3
+ Version: 0.1.0
4
+ Summary: A simple library to read and write dbase III files.
5
+ Home-page: https://github.com/sandy98/pybase3
6
+ Author: Domingo E. Savoretti
7
+ Author-email: esavoretti@gmail.com
8
+ License: MIT
9
+ Platform: UNKNOWN
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Requires-Python: >=3.9
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+
17
+ <p align="center">
18
+ <img src="static/img/oxi_1_rounded.jpg" alt="Oxi Logo" width="125px" />
19
+ </p>
20
+
21
+ <h1 align="center">Oxi</h1>
22
+
23
+ ASGI Full Stack:
24
+
25
+ * Web Server
26
+ * Web App
27
+
28
+ ...and yes, Oxi is a cutie faithful dog!
29
+
30
+ Coming soon...
31
+
32
+
oxi-0.1.0/README.md ADDED
@@ -0,0 +1,14 @@
1
+ <p align="center">
2
+ <img src="static/img/oxi_1_rounded.jpg" alt="Oxi Logo" width="125px" />
3
+ </p>
4
+
5
+ <h1 align="center">Oxi</h1>
6
+
7
+ ASGI Full Stack:
8
+
9
+ * Web Server
10
+ * Web App
11
+
12
+ ...and yes, Oxi is a cutie faithful dog!
13
+
14
+ Coming soon...
@@ -0,0 +1,19 @@
1
+ # -*- coding: utf-8 -*-
2
+ __version__ = "0.1.0"
3
+ __author__ = "Domingo E. Savoretti"
4
+ __email__ = "esavoretti@gmail.com"
5
+ __license__ = "MIT"
6
+ __url__ = "https://github.com/sandy98/pybase3"
7
+ __description__ = "A simple library to read and write dbase III files."
8
+ __copyright__ = "Copyright (c) 2025 Domingo E. Savoretti"
9
+ __oxi_port__ = 8086
10
+ __oxi_host__ = "0.0.0.0"
11
+
12
+ __all__ = [
13
+ "utils",
14
+ "mp4parser",
15
+ "template",
16
+ "websocket",
17
+ "app",
18
+ "server"
19
+ ]
@@ -0,0 +1,13 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ import asyncio
4
+
5
+ from . import __version__ as version
6
+
7
+ from .server import main
8
+
9
+ if __name__ == "__main__":
10
+ asyncio.run(main())
11
+
12
+
13
+
@@ -0,0 +1,24 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ # This script is used to manually activate a virtual environment by modifying
4
+ # the system path and environment variables to point to the virtual environment.
5
+
6
+ import os, sys
7
+ from pathlib import Path
8
+
9
+ oxi_env = False
10
+
11
+ try:
12
+ print("Activating virtualenv...")
13
+
14
+ # Path to the virtual environment
15
+ oxi_path = Path(os.path.abspath(__file__)).parent.parent
16
+
17
+ # Update sys.path to include oxi path
18
+ sys.path.insert(0, str(oxi_path))
19
+
20
+ oxi_env = True
21
+ print(f"Virtual environment activated for Oxi: {str(oxi_path)}.")
22
+ except Exception as e:
23
+ print("Virtual environment activation failed. Error: {0}".format(str(e)))
24
+
oxi-0.1.0/oxi/app.py ADDED
@@ -0,0 +1,8 @@
1
+ # -*- coding: utf-8 -*-
2
+ from . import __version__ as oxi_version
3
+
4
+ class OxiApp: ...
5
+
6
+
7
+
8
+
File without changes
@@ -0,0 +1,24 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ # This script is used to manually activate a virtual environment by modifying
4
+ # the system path and environment variables to point to the virtual environment.
5
+
6
+ import os, sys
7
+ from pathlib import Path
8
+
9
+ oxi_env = False
10
+
11
+ try:
12
+ print("Activating virtualenv...")
13
+
14
+ # Path to the virtual environment
15
+ oxi_path = Path(os.path.abspath(__file__)).parent.parent.parent
16
+
17
+ # Update sys.path to include oxi path
18
+ sys.path.insert(0, str(oxi_path))
19
+
20
+ oxi_env = True
21
+ print(f"Virtual environment activated for Oxi: {str(oxi_path)}.")
22
+ except Exception as e:
23
+ print("Virtual environment activation failed. Error: {0}".format(str(e)))
24
+
@@ -0,0 +1,464 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf_8 -*-
3
+
4
+ # test with: static/Media/Videos/Cele/Cele_patinando_cerca_de_Venecia.mp4
5
+
6
+ import os, mmap, io
7
+ from mimetypes import guess_type
8
+ import argparse #, copy
9
+ from . import __version__ as oxi_version
10
+
11
+ class Smmap:
12
+ def __init__(self, fileno, offset:int = 0, limit:int = 0):
13
+ self._mmap = mmap.mmap(fileno, 0, flags=mmap.MAP_PRIVATE)
14
+ self._offset = offset
15
+ self._limit = limit
16
+ self._mmap.seek(self._offset)
17
+
18
+ def __getitem__(self, index):
19
+ shifted_index = None
20
+ if isinstance(index, slice):
21
+ indexstart = 0 if not index.start else index.start
22
+ indexstop = len(self._mmap) if not index.stop else index.stop
23
+ indexstart = 0 if indexstart < 0 else indexstart
24
+ indexstart = self._limit if indexstart > self._limit else indexstart
25
+ indexstop = 0 if indexstop < 0 else indexstop
26
+ indexstop = self._limit if indexstop > self._limit else indexstop
27
+ shifted_index = slice(indexstart + self._offset, indexstop + self._offset, index.step)
28
+ else:
29
+ shifted_index = index
30
+ shifted_index = 0 if shifted_index < 0 else shifted_index
31
+ shifted_index = self._limit if shifted_index > self._limit else shifted_index
32
+ shifted_index += self._offset
33
+ return self._mmap.__getitem__(shifted_index)
34
+
35
+ def __setitem__(self, index, value):
36
+ shifted_index = None
37
+ if isinstance(index, slice):
38
+ indexstart = 0 if not index.start else index.start
39
+ indexstop = len(self._mmap) if not index.stop else index.stop
40
+ indexstart = 0 if indexstart < 0 else indexstart
41
+ indexstart = self._limit if indexstart > self._limit else indexstart
42
+ indexstop = 0 if indexstop < 0 else indexstop
43
+ indexstop = self._limit if indexstop > self._limit else indexstop
44
+ diff = indexstop - indexstart
45
+ if not isinstance(value, bytes) or len(value) > diff:
46
+ raise ValueError(f"value must be an instance of bytes with len <= to {diff}")
47
+ for i in range(diff):
48
+ self._mmap[indexstart + i + self._offset] = value[i]
49
+ else:
50
+ shifted_index = index
51
+ shifted_index = 0 if shifted_index < 0 else shifted_index
52
+ shifted_index = self._limit if shifted_index > self._limit else shifted_index
53
+ shifted_index += self._offset
54
+ self._mmap[shifted_index], = value
55
+
56
+ @property
57
+ def size(self):
58
+ return self._limit
59
+
60
+ @property
61
+ def offset(self):
62
+ return self._offset
63
+
64
+ @property
65
+ def mmap(self):
66
+ return self._mmap
67
+
68
+ def seek(self, num: int):
69
+ num = self._limit if num >= self._limit else num
70
+ self._mmap.seek(num + self._offset)
71
+
72
+ def tell(self):
73
+ return self._mmap.tell() - self._offset
74
+
75
+ def read(self, nbytes: int = -1):
76
+ if nbytes == -1 or nbytes >= (self._limit - self.tell()):
77
+ nbytes = self._limit - self.tell()
78
+ return self._mmap.read(nbytes)
79
+
80
+ def write(self, what: bytes = b''):
81
+ self._mmap.write(what)
82
+
83
+ def find(self, what: bytes):
84
+ found = self._mmap.find(what)
85
+ return found if found == -1 else found - self._offset
86
+
87
+ def rfind(self, what: bytes):
88
+ found = self._mmap.rfind(what)
89
+ return found if found == -1 else found - self._offset
90
+
91
+ def close(self):
92
+ self._mmap.close()
93
+
94
+ def __del__(self):
95
+ try:
96
+ if not self._mmap.closed:
97
+ self._mmap.close()
98
+ except:
99
+ pass
100
+
101
+
102
+ class Mp4:
103
+ """Stub for Atom"""
104
+
105
+ class Atom:
106
+ def __init__(self, offset: int, size: int, name: str, level: int = 0, ordinal: int = -1, container=None):
107
+ self.offset = offset
108
+ self.size = size
109
+ self.name = name
110
+ self.ordinal = ordinal
111
+ self.level = level
112
+ # self.contents = contents
113
+ self.container = container
114
+ self.children: list = []
115
+ self._mm = None
116
+
117
+ @property
118
+ def contents(self):
119
+ if self._mm:
120
+ return self._mm
121
+ if not self.container:
122
+ return None
123
+ fp = self.container.fp
124
+ if not fp:
125
+ return None
126
+ self._mm = Smmap(fp.fileno(), self.offset, self.size)
127
+ return self._mm
128
+
129
+ def setcontents(self, new_mm):
130
+ self._mm = new_mm
131
+
132
+ def __repr__(self):
133
+ # Atom ftyp @ 0 of size: 32, ends @ 32
134
+ args = ('\t' * self.level, self.name, f"{self.offset:,}", f"{self.size:,}", f"{(self.offset + self.size):,}")
135
+ return "{0}Atom {1} @ {2} of size: {3} , ends @ {4}".format(*args)
136
+
137
+
138
+ def __str__(self):
139
+ mainline = repr(self)
140
+ lenchildren = len(self.children) if self.children else 0
141
+ childrenlines = ''.join(map(str, self.children)) if lenchildren else ''
142
+ return "\n".join([mainline, childrenlines])
143
+
144
+ def load_contents(self, fp = None):
145
+ # Retained for backward compatibility
146
+ pass
147
+
148
+ def load_children(self):
149
+ container: Mp4 = self.container
150
+ ordinal = 0
151
+ offset = self.offset + 8
152
+ fp = mmap.mmap(container.fp.fileno(), container.filesize, flags=mmap.MAP_PRIVATE)
153
+ while offset < (self.size + self.offset):
154
+ child = container._get_atom(fp, offset, ordinal, self.level + 1)
155
+ if child.size == 0:
156
+ raise ValueError("Atom of size 0 not admitted out of 0 level.")
157
+ if child.name == '\x00\x00\x00\x00':
158
+ offset += child.size
159
+ continue
160
+ if child.name in ["trak", "mdia", "minf", "stbl"]:
161
+ # Known ancestor atom of stco or co64, search within it!
162
+ # for res in _find_atoms_ex(atom, datastream):
163
+ # yield res
164
+ child.load_children()
165
+ elif child.name in ["stco", "co64"]:
166
+ self.container.relocation_targets.append(child)
167
+ self.children.append(child)
168
+ ordinal += 1
169
+ offset += child.size
170
+
171
+ @property
172
+ def boundaries(self):
173
+ return [self.offset, self.offset + self.size]
174
+
175
+ @property
176
+ def named_boundaries(self):
177
+ return {self.name: self.boundaries}
178
+
179
+
180
+ class Mp4:
181
+ def __init__(self, filename):
182
+ if not os.path.exists(filename):
183
+ raise NameError(f"File {filename} doesn't exist.")
184
+ if guess_type(filename)[0] not in ['video/mp4', 'video/quicktime']:
185
+ raise TypeError(f"File {filename} is not an MP4 file.")
186
+ self.filename = filename
187
+ self.filesize = os.path.getsize(filename)
188
+ self.loop = None
189
+ self.atoms: list[Atom] = []
190
+ self.relocation_targets: list[Atom] = []
191
+ self.ftyp: Atom = None
192
+ self.moov: Atom = None
193
+ self.free: Atom = None
194
+ self.mdat: Atom = None
195
+ self._patched_moov: bytearray = None
196
+ self.fp = open(self.filename, 'rb')
197
+ self.atoms = self._collectatoms()
198
+
199
+ def __del__(self):
200
+ try:
201
+ if not self.fp.closed:
202
+ self.fp.close()
203
+ except:
204
+ pass
205
+
206
+ def __str__(self):
207
+ atomstrs = "\n".join(list(map(lambda a: " " + str(a), self.atoms)))
208
+ return f"""
209
+
210
+ {'_' * 80}
211
+
212
+ File: {self.filename}
213
+ Size: {self.filesize:,}
214
+ {'_' * 80}
215
+
216
+ {atomstrs}
217
+
218
+ {'_' * 80}
219
+ """
220
+
221
+ def __repr__(self):
222
+ return "\n".join(self.quicklist())
223
+
224
+ def _get_atom(self, fp: mmap.mmap, offset: int, ordinal: int=0, level:int=0):
225
+ size, name = [int.from_bytes(fp[offset:offset+4], byteorder='big'), fp[offset+4:offset+8].decode('latin-1')]
226
+ if size == 1:
227
+ size = int.from_bytes(fp[offset+8:offset+16], byteorder='big')
228
+ atom = Atom(offset=offset, size=size, name=name, level=level, ordinal = ordinal, container=self)
229
+ return atom
230
+
231
+ def _parse_mp4(self):
232
+ filesize = self.filesize
233
+ with mmap.mmap(self.fp.fileno(), filesize, flags=mmap.MAP_PRIVATE) as fp:
234
+ ordinal = 0
235
+ offset = 0
236
+ while offset < filesize:
237
+ atom = self._get_atom(fp, offset, ordinal, 0)
238
+ if atom.size == 0:
239
+ atom.size = self.filesize - offset
240
+ yield atom
241
+ return
242
+ ordinal += 1
243
+ offset += atom.size
244
+ if atom.name != '\x00\x00\x00\x00':
245
+ yield atom
246
+ else:
247
+ continue
248
+
249
+ def _collectatoms(self):
250
+ atoms = [self._classify_atom(atom) for atom in self._parse_mp4()]
251
+ # if self.moov:
252
+ # self.moov.load_children()
253
+ return atoms
254
+
255
+ def _is_relocatable(self):
256
+ return (not not self.moov) and (not self._is_compressed()) and (self.moov.ordinal > self.mdat.ordinal)
257
+
258
+ def _patch_moov(self):
259
+ if self._patched_moov is not None or not self.moov or self._is_compressed():
260
+ return
261
+ offset_shift = self.moov.size
262
+ self.moov.contents.seek(0)
263
+ self._patched_moov = mmap.mmap(-1, self.moov.size)
264
+ self._patched_moov.write(self.moov.contents.read())
265
+ for atom in self.relocation_targets:
266
+ os.sys.stderr.write(f'{atom.name} located at ' + str(atom.offset) + '\n')
267
+ os.sys.stderr.flush()
268
+ offset_pos_begin = atom.offset - self.moov.offset
269
+ offset_pos = offset_pos_begin
270
+ entry_count = int.from_bytes(self._patched_moov[offset_pos+12:offset_pos+16], byteorder='big')
271
+ print(f"Patching atom '{atom.name}' with {entry_count} entries.")
272
+ displacement = 4 if atom.name == 'stco' else 8 # in case atom.name == 'co64'
273
+ for i in range(entry_count):
274
+ offset_pos = offset_pos_begin + 16 + (i * displacement)
275
+ current_offset = int.from_bytes(self._patched_moov[offset_pos:offset_pos+4], byteorder='big')
276
+ new_offset = current_offset + offset_shift
277
+ # print(f"{i}: Transferring offset {current_offset} to {new_offset}")
278
+ self._patched_moov[offset_pos:offset_pos+4] = new_offset.to_bytes(displacement, byteorder='big')
279
+
280
+ def _classify_atom(self, atom: Atom):
281
+ # atom.container = self
282
+ # atom.load_contents(self.fp)
283
+ if atom.name == 'ftyp':
284
+ self.ftyp = atom
285
+ elif atom.name == 'moov':
286
+ atom.load_children()
287
+ self.moov = atom
288
+ elif atom.name == 'free':
289
+ self.free = atom
290
+ elif atom.name == 'mdat':
291
+ self.mdat = atom
292
+ elif not hasattr(self, atom.name):
293
+ setattr(self, atom.name, atom)
294
+ return atom
295
+
296
+ def _get_chunks(self, limit: int, stream = None, chunksize:int = None):
297
+ if stream is None:
298
+ stream = self.fp
299
+ chunk_size = chunksize or mmap.PAGESIZE
300
+ while chunk_size < (2 ** 15):
301
+ chunk_size *= 2
302
+ remaining = limit
303
+ while remaining:
304
+ chunk = stream.read(min(remaining, chunk_size))
305
+ if not chunk:
306
+ return
307
+ remaining -= len(chunk)
308
+ yield chunk
309
+
310
+ @property
311
+ def boundaries(self):
312
+ return list(map(lambda a: a.named_boundaries, self.atoms))
313
+
314
+ @property
315
+ def faststart(self):
316
+ if not self._is_relocatable():
317
+ return self.atoms
318
+ temp_ordinal = self.mdat.ordinal
319
+ self.mdat.ordinal = self.moov.ordinal
320
+ self.moov.ordinal = temp_ordinal
321
+ self.moov.setcontents(self.patched_moov)
322
+ self.atoms.sort(key=lambda atom: atom.ordinal)
323
+ for index, atm in enumerate(self.atoms):
324
+ atm.contents.seek(0)
325
+ if index == 0:
326
+ continue
327
+ atm.offset = self.atoms[index-1].offset + self.atoms[index-1].size
328
+ return self.atoms
329
+
330
+ @property
331
+ def faststart_boundaries(self):
332
+ return list(map(lambda a: a.named_boundaries, self.faststart))
333
+
334
+ @property
335
+ def patched_moov(self):
336
+ if not self._patched_moov:
337
+ self._patch_moov()
338
+ return self._patched_moov
339
+
340
+ def _get_moov_atom(self):
341
+ """
342
+ Deprecated. 'moov' is now an Mp4 attribute.
343
+ """
344
+ for atom in self.atoms:
345
+ if atom.name == 'moov':
346
+ return atom
347
+ return None
348
+
349
+ def _is_compressed(self):
350
+ for atom in self.moov.children:
351
+ if atom.name == 'cmov':
352
+ print(f"Warning!\nFile {self.filename} cannot be optimized for streaming because it's compressed.")
353
+ return True
354
+ return False
355
+
356
+ def stream(self, begin: int=0, end: int=None):
357
+ for atm in self.faststart:
358
+ atm.contents.seek(0)
359
+ if atm.name not in ['mdat', 'moov']:
360
+ yield atm.contents.read()
361
+ else:
362
+ chunkgen = self._get_chunks(limit=atm.size, stream=atm.contents)
363
+ for chunk in chunkgen:
364
+ try:
365
+ yield chunk
366
+ except:
367
+ chunkgen.close()
368
+ break
369
+ print(f"Mp4 '{atm.name}' stream sent succesfully ({atm.size:,} bytes).")
370
+
371
+ def stream_range(self, begin: int=0, end: int=0):
372
+ if not end:
373
+ end = self.filesize - 1
374
+ beginner, ender = {} , {}
375
+ for atom in self.faststart:
376
+ if begin in range(*atom.boundaries):
377
+ beginner['name'] = atom.name
378
+ beginner['ordinal'] = atom.ordinal
379
+ beginner['offset'] = begin - atom.offset
380
+ if end in range(*atom.boundaries):
381
+ ender['name'] = atom.name
382
+ ender['ordinal'] = atom.ordinal
383
+ ender['offset'] = end - atom.offset
384
+ if not ender:
385
+ atom = self.faststart[-1]
386
+ ender['name'] = atom.name
387
+ ender['ordinal'] = atom.ordinal
388
+ ender['offset'] = atom.size - 1
389
+ if not beginner:
390
+ atom = self.faststart[0]
391
+ ender['name'] = atom.name
392
+ ender['ordinal'] = atom.ordinal
393
+ ender['offset'] = 0
394
+ fragment = None
395
+ for atom in self.faststart:
396
+ if atom.ordinal < beginner.get('ordinal'):
397
+ continue
398
+ if atom.ordinal > ender.get('ordinal'):
399
+ return
400
+ if atom.ordinal == beginner.get('ordinal'):
401
+ frag_begin = beginner.get('offset')
402
+ if atom.ordinal == ender.get('ordinal'):
403
+ length = ender.get('offset') - frag_begin
404
+ # fragment = atom.contents[beginner.get('offset'):ender.get('offset')]
405
+ else:
406
+ length = atom.size - frag_begin
407
+ # fragment = atom.contents[beginner.get('offset'):]
408
+ elif atom.ordinal == ender.get('ordinal'):
409
+ frag_begin = 0
410
+ length = ender.get('offset')
411
+ # fragment = atom.contents[:ender.get('offset')]
412
+ else:
413
+ frag_begin = 0
414
+ length = atom.size
415
+ # fragment = atom.contents[:]
416
+ atom.contents.seek(frag_begin)
417
+ gen = self._get_chunks(length, atom.contents)
418
+ for chunk in gen:
419
+ yield chunk
420
+
421
+ def save(self, outputfile: str = 'out.mp4'):
422
+ with open(outputfile, 'wb') as fp:
423
+ for chunk in self.stream():
424
+ fp.write(chunk)
425
+
426
+ print(f"Mp4 file {self.filename} saved succesfully to {outputfile}.")
427
+
428
+ def quicklist(self):
429
+ fileline = f"File: {self.filename}"
430
+ sizeline= f"Size: {self.filesize:,}"
431
+ underline = "_" * max(len(fileline), len(sizeline))
432
+ return [underline, fileline, sizeline, underline, *list(map(repr, self.atoms))]
433
+
434
+ def main():
435
+ parser = argparse.ArgumentParser(description='Command line arguments for Mp4Parser', prog='mp4parser')
436
+ parser.add_argument('-V', '--version', action='version', version=f"{parser.prog} v {oxi_version}", help=f"Shows {parser.prog} version and exits.")
437
+ parser.add_argument('inputfile', type=str, help="Name of the file to be listed/processed.")
438
+ parser.add_argument('-o', '--outputfile', type=str, default='out.mp4', help="Name of the file you wish to save to. Defaults to 'out.mp4'")
439
+ group = parser.add_mutually_exclusive_group(required=False)
440
+ group.add_argument('-s', '--save', action="store_true", help="Use this option to save the input file to a new one.")
441
+ group.add_argument('-l', '--list', action="store_true", help="Shows a list of the main level atoms contained in the input file.")
442
+ group.add_argument('-L', '--full-list', action="store_true", help="Shows a detailed list of the atoms contained in the input file.")
443
+
444
+ args = parser.parse_args()
445
+ filename = args.inputfile
446
+
447
+ try:
448
+ mp4 = Mp4(filename)
449
+ except Exception as exc:
450
+ print(f"ERROR: {exc}")
451
+ raise SystemExit
452
+
453
+ if args.save:
454
+ mp4.save(args.outputfile)
455
+ elif args.list:
456
+ print('\n'.join(mp4.quicklist()))
457
+ elif args.full_list:
458
+ print(mp4)
459
+ else:
460
+ print('\n'.join(mp4.quicklist()))
461
+
462
+ if __name__ == '__main__':
463
+ main()
464
+