diff options
| -rw-r--r-- | libmat2/video.py | 95 | ||||
| -rwxr-xr-x | mat2 | 2 | ||||
| -rw-r--r-- | tests/data/dirty.mp4 | bin | 0 -> 383631 bytes | |||
| -rw-r--r-- | tests/test_libmat2.py | 23 |
4 files changed, 101 insertions, 19 deletions
diff --git a/libmat2/video.py b/libmat2/video.py index 85b5b2e..a5029c0 100644 --- a/libmat2/video.py +++ b/libmat2/video.py | |||
| @@ -2,10 +2,37 @@ import os | |||
| 2 | import subprocess | 2 | import subprocess |
| 3 | import logging | 3 | import logging |
| 4 | 4 | ||
| 5 | from typing import Dict, Union | ||
| 6 | |||
| 5 | from . import exiftool | 7 | from . import exiftool |
| 6 | 8 | ||
| 7 | 9 | ||
| 8 | class AVIParser(exiftool.ExiftoolParser): | 10 | class AbstractFFmpegParser(exiftool.ExiftoolParser): |
| 11 | """ Abstract parser for all FFmpeg-based ones, mainly for video. """ | ||
| 12 | def remove_all(self) -> bool: | ||
| 13 | cmd = [_get_ffmpeg_path(), | ||
| 14 | '-i', self.filename, # input file | ||
| 15 | '-y', # overwrite existing output file | ||
| 16 | '-map', '0', # copy everything all streams from input to output | ||
| 17 | '-codec', 'copy', # don't decode anything, just copy (speed!) | ||
| 18 | '-loglevel', 'panic', # Don't show log | ||
| 19 | '-hide_banner', # hide the banner | ||
| 20 | '-map_metadata', '-1', # remove supperficial metadata | ||
| 21 | '-map_chapters', '-1', # remove chapters | ||
| 22 | '-disposition', '0', # Remove dispositions (check ffmpeg's manpage) | ||
| 23 | '-fflags', '+bitexact', # don't add any metadata | ||
| 24 | '-flags:v', '+bitexact', # don't add any metadata | ||
| 25 | '-flags:a', '+bitexact', # don't add any metadata | ||
| 26 | self.output_filename] | ||
| 27 | try: | ||
| 28 | subprocess.check_call(cmd) | ||
| 29 | except subprocess.CalledProcessError as e: | ||
| 30 | logging.error("Something went wrong during the processing of %s: %s", self.filename, e) | ||
| 31 | return False | ||
| 32 | return True | ||
| 33 | |||
| 34 | |||
| 35 | class AVIParser(AbstractFFmpegParser): | ||
| 9 | mimetypes = {'video/x-msvideo', } | 36 | mimetypes = {'video/x-msvideo', } |
| 10 | meta_whitelist = {'SourceFile', 'ExifToolVersion', 'FileName', 'Directory', | 37 | meta_whitelist = {'SourceFile', 'ExifToolVersion', 'FileName', 'Directory', |
| 11 | 'FileSize', 'FileModifyDate', 'FileAccessDate', | 38 | 'FileSize', 'FileModifyDate', 'FileAccessDate', |
| @@ -24,25 +51,55 @@ class AVIParser(exiftool.ExiftoolParser): | |||
| 24 | 'SampleRate', 'AvgBytesPerSec', 'BitsPerSample', | 51 | 'SampleRate', 'AvgBytesPerSec', 'BitsPerSample', |
| 25 | 'Duration', 'ImageSize', 'Megapixels'} | 52 | 'Duration', 'ImageSize', 'Megapixels'} |
| 26 | 53 | ||
| 54 | class MP4Parser(AbstractFFmpegParser): | ||
| 55 | mimetypes = {'video/mp4', } | ||
| 56 | meta_whitelist = {'AudioFormat', 'AvgBitrate', 'Balance', 'TrackDuration', | ||
| 57 | 'XResolution', 'YResolution', 'ExifToolVersion', | ||
| 58 | 'FileAccessDate', 'FileInodeChangeDate', 'FileModifyDate', | ||
| 59 | 'FileName', 'FilePermissions', 'MIMEType', 'FileType', | ||
| 60 | 'FileTypeExtension', 'Directory', 'ImageWidth', | ||
| 61 | 'ImageSize', 'ImageHeight', 'FileSize', 'SourceFile', | ||
| 62 | 'BitDepth', 'Duration', 'AudioChannels', | ||
| 63 | 'AudioBitsPerSample', 'AudioSampleRate', 'Megapixels', | ||
| 64 | 'MovieDataSize', 'VideoFrameRate', 'MediaTimeScale', | ||
| 65 | 'SourceImageHeight', 'SourceImageWidth', | ||
| 66 | 'MatrixStructure', 'MediaDuration'} | ||
| 67 | meta_key_value_whitelist = { # some metadata are mandatory :/ | ||
| 68 | 'CreateDate': '0000:00:00 00:00:00', | ||
| 69 | 'CurrentTime': '0 s', | ||
| 70 | 'MediaCreateDate': '0000:00:00 00:00:00', | ||
| 71 | 'MediaLanguageCode': 'und', | ||
| 72 | 'MediaModifyDate': '0000:00:00 00:00:00', | ||
| 73 | 'ModifyDate': '0000:00:00 00:00:00', | ||
| 74 | 'OpColor': '0 0 0', | ||
| 75 | 'PosterTime': '0 s', | ||
| 76 | 'PreferredRate': '1', | ||
| 77 | 'PreferredVolume': '100.00%', | ||
| 78 | 'PreviewDuration': '0 s', | ||
| 79 | 'PreviewTime': '0 s', | ||
| 80 | 'SelectionDuration': '0 s', | ||
| 81 | 'SelectionTime': '0 s', | ||
| 82 | 'TrackCreateDate': '0000:00:00 00:00:00', | ||
| 83 | 'TrackModifyDate': '0000:00:00 00:00:00', | ||
| 84 | 'TrackVolume': '0.00%', | ||
| 85 | } | ||
| 86 | |||
| 27 | def remove_all(self) -> bool: | 87 | def remove_all(self) -> bool: |
| 28 | cmd = [_get_ffmpeg_path(), | 88 | logging.warning('The format of "%s" (video/mp4) has some mandatory ' |
| 29 | '-i', self.filename, # input file | 89 | 'metadata fields; mat2 filled them with standard data.', |
| 30 | '-y', # overwrite existing output file | 90 | self.filename) |
| 31 | '-loglevel', 'panic', # Don't show log | 91 | return super().remove_all() |
| 32 | '-hide_banner', # hide the banner | 92 | |
| 33 | '-codec', 'copy', # don't decode anything, just copy (speed!) | 93 | def get_meta(self) -> Dict[str, Union[str, dict]]: |
| 34 | '-map_metadata', '-1', # remove supperficial metadata | 94 | meta = super().get_meta() |
| 35 | '-map_chapters', '-1', # remove chapters | 95 | |
| 36 | '-fflags', '+bitexact', # don't add any metadata | 96 | ret = dict() # type: Dict[str, Union[str, dict]] |
| 37 | '-flags:v', '+bitexact', # don't add any metadata | 97 | for key, value in meta.items(): |
| 38 | '-flags:a', '+bitexact', # don't add any metadata | 98 | if key in self.meta_key_value_whitelist.keys(): |
| 39 | self.output_filename] | 99 | if value == self.meta_key_value_whitelist[key]: |
| 40 | try: | 100 | continue |
| 41 | subprocess.check_call(cmd) | 101 | ret[key] = value |
| 42 | except subprocess.CalledProcessError as e: | 102 | return ret |
| 43 | logging.error("Something went wrong during the processing of %s: %s", self.filename, e) | ||
| 44 | return False | ||
| 45 | return True | ||
| 46 | 103 | ||
| 47 | 104 | ||
| 48 | def _get_ffmpeg_path() -> str: # pragma: no cover | 105 | def _get_ffmpeg_path() -> str: # pragma: no cover |
| @@ -20,6 +20,8 @@ __version__ = '0.5.0' | |||
| 20 | assert Tuple | 20 | assert Tuple |
| 21 | assert Union | 21 | assert Union |
| 22 | 22 | ||
| 23 | logging.basicConfig(format='%(levelname)s: %(message)s', level=logging.WARNING) | ||
| 24 | |||
| 23 | 25 | ||
| 24 | def __check_file(filename: str, mode: int=os.R_OK) -> bool: | 26 | def __check_file(filename: str, mode: int=os.R_OK) -> bool: |
| 25 | if not os.path.exists(filename): | 27 | if not os.path.exists(filename): |
diff --git a/tests/data/dirty.mp4 b/tests/data/dirty.mp4 new file mode 100644 index 0000000..1fc4788 --- /dev/null +++ b/tests/data/dirty.mp4 | |||
| Binary files differ | |||
diff --git a/tests/test_libmat2.py b/tests/test_libmat2.py index 1602480..e3072a8 100644 --- a/tests/test_libmat2.py +++ b/tests/test_libmat2.py | |||
| @@ -521,3 +521,26 @@ class TestCleaning(unittest.TestCase): | |||
| 521 | os.remove('./tests/data/dirty.cleaned.zip') | 521 | os.remove('./tests/data/dirty.cleaned.zip') |
| 522 | os.remove('./tests/data/dirty.cleaned.cleaned.zip') | 522 | os.remove('./tests/data/dirty.cleaned.cleaned.zip') |
| 523 | 523 | ||
| 524 | |||
| 525 | def test_mp4(self): | ||
| 526 | try: | ||
| 527 | video._get_ffmpeg_path() | ||
| 528 | except RuntimeError: | ||
| 529 | raise unittest.SkipTest | ||
| 530 | |||
| 531 | shutil.copy('./tests/data/dirty.mp4', './tests/data/clean.mp4') | ||
| 532 | p = video.MP4Parser('./tests/data/clean.mp4') | ||
| 533 | |||
| 534 | meta = p.get_meta() | ||
| 535 | self.assertEqual(meta['Encoder'], 'HandBrake 0.9.4 2009112300') | ||
| 536 | |||
| 537 | ret = p.remove_all() | ||
| 538 | self.assertTrue(ret) | ||
| 539 | |||
| 540 | p = video.MP4Parser('./tests/data/clean.cleaned.mp4') | ||
| 541 | self.assertNotIn('Encoder', p.get_meta()) | ||
| 542 | self.assertTrue(p.remove_all()) | ||
| 543 | |||
| 544 | os.remove('./tests/data/clean.mp4') | ||
| 545 | os.remove('./tests/data/clean.cleaned.mp4') | ||
| 546 | os.remove('./tests/data/clean.cleaned.cleaned.mp4') | ||
