2019-10-13 01:13:49 +02:00
|
|
|
import subprocess
|
2019-05-16 00:31:40 +02:00
|
|
|
import functools
|
2020-02-10 03:31:07 +01:00
|
|
|
import shutil
|
2018-10-22 16:45:30 +02:00
|
|
|
import logging
|
2018-10-18 19:19:56 +02:00
|
|
|
|
2023-01-28 16:57:20 +01:00
|
|
|
from typing import Union, Dict
|
2018-10-28 15:41:04 +01:00
|
|
|
|
2018-10-18 19:19:56 +02:00
|
|
|
from . import exiftool
|
2019-10-13 01:13:49 +02:00
|
|
|
from . import bubblewrap
|
2018-10-18 19:19:56 +02:00
|
|
|
|
|
|
|
|
2018-10-28 15:41:04 +01:00
|
|
|
class AbstractFFmpegParser(exiftool.ExiftoolParser):
|
|
|
|
""" Abstract parser for all FFmpeg-based ones, mainly for video. """
|
2019-02-02 18:44:02 +01:00
|
|
|
# Some fileformats have mandatory metadata fields
|
2023-05-03 22:28:02 +02:00
|
|
|
meta_key_value_allowlist: Dict[str, Union[str, int]] = dict()
|
2019-02-02 18:44:02 +01:00
|
|
|
|
2018-10-28 15:41:04 +01:00
|
|
|
def remove_all(self) -> bool:
|
2019-02-20 00:45:27 +01:00
|
|
|
if self.meta_key_value_allowlist:
|
2019-02-10 21:46:13 +01:00
|
|
|
logging.warning('The format of "%s" (%s) has some mandatory '
|
2019-02-02 18:44:02 +01:00
|
|
|
'metadata fields; mat2 filled them with standard '
|
2019-02-10 21:46:13 +01:00
|
|
|
'data.', self.filename, ', '.join(self.mimetypes))
|
2018-10-28 15:41:04 +01:00
|
|
|
cmd = [_get_ffmpeg_path(),
|
|
|
|
'-i', self.filename, # input file
|
|
|
|
'-y', # overwrite existing output file
|
|
|
|
'-map', '0', # copy everything all streams from input to output
|
|
|
|
'-codec', 'copy', # don't decode anything, just copy (speed!)
|
|
|
|
'-loglevel', 'panic', # Don't show log
|
|
|
|
'-hide_banner', # hide the banner
|
|
|
|
'-map_metadata', '-1', # remove supperficial metadata
|
|
|
|
'-map_chapters', '-1', # remove chapters
|
|
|
|
'-disposition', '0', # Remove dispositions (check ffmpeg's manpage)
|
|
|
|
'-fflags', '+bitexact', # don't add any metadata
|
|
|
|
'-flags:v', '+bitexact', # don't add any metadata
|
|
|
|
'-flags:a', '+bitexact', # don't add any metadata
|
|
|
|
self.output_filename]
|
|
|
|
try:
|
2019-10-13 01:13:49 +02:00
|
|
|
if self.sandbox:
|
|
|
|
bubblewrap.run(cmd, check=True,
|
|
|
|
input_filename=self.filename,
|
|
|
|
output_filename=self.output_filename)
|
|
|
|
else:
|
|
|
|
subprocess.run(cmd, check=True)
|
2018-10-28 15:41:04 +01:00
|
|
|
except subprocess.CalledProcessError as e:
|
|
|
|
logging.error("Something went wrong during the processing of %s: %s", self.filename, e)
|
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
2023-01-28 16:57:20 +01:00
|
|
|
def get_meta(self) -> Dict[str, Union[str, Dict]]:
|
2019-02-02 18:44:02 +01:00
|
|
|
meta = super().get_meta()
|
|
|
|
|
2023-05-03 22:28:02 +02:00
|
|
|
ret: Dict[str, Union[str, Dict]] = dict()
|
2019-02-02 18:44:02 +01:00
|
|
|
for key, value in meta.items():
|
2021-11-21 11:02:22 +01:00
|
|
|
if key in self.meta_key_value_allowlist:
|
2019-02-20 00:45:27 +01:00
|
|
|
if value == self.meta_key_value_allowlist[key]:
|
2019-02-02 18:44:02 +01:00
|
|
|
continue
|
|
|
|
ret[key] = value
|
|
|
|
return ret
|
|
|
|
|
|
|
|
|
|
|
|
class WMVParser(AbstractFFmpegParser):
|
|
|
|
mimetypes = {'video/x-ms-wmv', }
|
2019-02-20 00:45:27 +01:00
|
|
|
meta_allowlist = {'AudioChannels', 'AudioCodecID', 'AudioCodecName',
|
2019-02-02 18:44:02 +01:00
|
|
|
'ErrorCorrectionType', 'AudioSampleRate', 'DataPackets',
|
|
|
|
'Directory', 'Duration', 'ExifToolVersion',
|
|
|
|
'FileAccessDate', 'FileInodeChangeDate', 'FileLength',
|
|
|
|
'FileModifyDate', 'FileName', 'FilePermissions',
|
|
|
|
'FileSize', 'FileType', 'FileTypeExtension',
|
|
|
|
'FrameCount', 'FrameRate', 'ImageHeight', 'ImageSize',
|
|
|
|
'ImageWidth', 'MIMEType', 'MaxBitrate', 'MaxPacketSize',
|
|
|
|
'Megapixels', 'MinPacketSize', 'Preroll', 'SendDuration',
|
|
|
|
'SourceFile', 'StreamNumber', 'VideoCodecName', }
|
2019-02-20 00:45:27 +01:00
|
|
|
meta_key_value_allowlist = { # some metadata are mandatory :/
|
2019-02-02 18:44:02 +01:00
|
|
|
'AudioCodecDescription': '',
|
|
|
|
'CreationDate': '0000:00:00 00:00:00Z',
|
|
|
|
'FileID': '00000000-0000-0000-0000-000000000000',
|
|
|
|
'Flags': 2, # FIXME: What is this? Why 2?
|
|
|
|
'ModifyDate': '0000:00:00 00:00:00',
|
|
|
|
'TimeOffset': '0 s',
|
|
|
|
'VideoCodecDescription': '',
|
|
|
|
'StreamType': 'Audio',
|
|
|
|
}
|
|
|
|
|
2018-10-28 15:41:04 +01:00
|
|
|
|
|
|
|
class AVIParser(AbstractFFmpegParser):
|
2018-10-18 19:19:56 +02:00
|
|
|
mimetypes = {'video/x-msvideo', }
|
2019-02-20 00:45:27 +01:00
|
|
|
meta_allowlist = {'SourceFile', 'ExifToolVersion', 'FileName', 'Directory',
|
2018-10-18 19:19:56 +02:00
|
|
|
'FileSize', 'FileModifyDate', 'FileAccessDate',
|
|
|
|
'FileInodeChangeDate', 'FilePermissions', 'FileType',
|
|
|
|
'FileTypeExtension', 'MIMEType', 'FrameRate', 'MaxDataRate',
|
|
|
|
'FrameCount', 'StreamCount', 'StreamType', 'VideoCodec',
|
|
|
|
'VideoFrameRate', 'VideoFrameCount', 'Quality',
|
|
|
|
'SampleSize', 'BMPVersion', 'ImageWidth', 'ImageHeight',
|
|
|
|
'Planes', 'BitDepth', 'Compression', 'ImageLength',
|
2022-10-09 21:04:19 +02:00
|
|
|
'PixelsPerMeterX', 'PixelsPerMeterY',
|
|
|
|
'NumImportantColors', 'NumColors',
|
2018-10-18 19:19:56 +02:00
|
|
|
'RedMask', 'GreenMask', 'BlueMask', 'AlphaMask',
|
|
|
|
'ColorSpace', 'AudioCodec', 'AudioCodecRate',
|
2022-10-09 21:04:19 +02:00
|
|
|
'AudioSampleCount',
|
2018-10-18 19:19:56 +02:00
|
|
|
'AudioSampleRate', 'Encoding', 'NumChannels',
|
|
|
|
'SampleRate', 'AvgBytesPerSec', 'BitsPerSample',
|
|
|
|
'Duration', 'ImageSize', 'Megapixels'}
|
|
|
|
|
2019-02-02 18:44:02 +01:00
|
|
|
|
2018-10-28 15:41:04 +01:00
|
|
|
class MP4Parser(AbstractFFmpegParser):
|
|
|
|
mimetypes = {'video/mp4', }
|
2019-02-20 00:45:27 +01:00
|
|
|
meta_allowlist = {'AudioFormat', 'AvgBitrate', 'Balance', 'TrackDuration',
|
2018-10-28 15:41:04 +01:00
|
|
|
'XResolution', 'YResolution', 'ExifToolVersion',
|
|
|
|
'FileAccessDate', 'FileInodeChangeDate', 'FileModifyDate',
|
|
|
|
'FileName', 'FilePermissions', 'MIMEType', 'FileType',
|
|
|
|
'FileTypeExtension', 'Directory', 'ImageWidth',
|
|
|
|
'ImageSize', 'ImageHeight', 'FileSize', 'SourceFile',
|
|
|
|
'BitDepth', 'Duration', 'AudioChannels',
|
|
|
|
'AudioBitsPerSample', 'AudioSampleRate', 'Megapixels',
|
|
|
|
'MovieDataSize', 'VideoFrameRate', 'MediaTimeScale',
|
|
|
|
'SourceImageHeight', 'SourceImageWidth',
|
|
|
|
'MatrixStructure', 'MediaDuration'}
|
2019-02-20 00:45:27 +01:00
|
|
|
meta_key_value_allowlist = { # some metadata are mandatory :/
|
2018-10-28 15:41:04 +01:00
|
|
|
'CreateDate': '0000:00:00 00:00:00',
|
|
|
|
'CurrentTime': '0 s',
|
|
|
|
'MediaCreateDate': '0000:00:00 00:00:00',
|
|
|
|
'MediaLanguageCode': 'und',
|
|
|
|
'MediaModifyDate': '0000:00:00 00:00:00',
|
|
|
|
'ModifyDate': '0000:00:00 00:00:00',
|
|
|
|
'OpColor': '0 0 0',
|
|
|
|
'PosterTime': '0 s',
|
|
|
|
'PreferredRate': '1',
|
|
|
|
'PreferredVolume': '100.00%',
|
|
|
|
'PreviewDuration': '0 s',
|
|
|
|
'PreviewTime': '0 s',
|
|
|
|
'SelectionDuration': '0 s',
|
|
|
|
'SelectionTime': '0 s',
|
|
|
|
'TrackCreateDate': '0000:00:00 00:00:00',
|
|
|
|
'TrackModifyDate': '0000:00:00 00:00:00',
|
|
|
|
'TrackVolume': '0.00%',
|
|
|
|
}
|
|
|
|
|
2018-10-18 19:19:56 +02:00
|
|
|
|
2023-01-31 20:42:39 +01:00
|
|
|
@functools.lru_cache(maxsize=None)
|
2018-10-18 19:19:56 +02:00
|
|
|
def _get_ffmpeg_path() -> str: # pragma: no cover
|
2020-02-10 03:31:07 +01:00
|
|
|
which_path = shutil.which('ffmpeg')
|
|
|
|
if which_path:
|
|
|
|
return which_path
|
2018-10-18 19:19:56 +02:00
|
|
|
|
|
|
|
raise RuntimeError("Unable to find ffmpeg")
|