]>
Commit | Line | Data |
---|---|---|
95131b21 | 1 | import base64 |
ddbed364 | 2 | import os |
06167fbb | 3 | import re |
f8271158 | 4 | import subprocess |
06167fbb | 5 | |
8326b00a | 6 | from .common import PostProcessor |
f8271158 | 7 | from .ffmpeg import FFmpegPostProcessor, FFmpegThumbnailsConvertorPP |
5792c950 | 8 | from ..compat import imghdr |
9b8ee23b | 9 | from ..dependencies import mutagen |
ddbed364 | 10 | from ..utils import ( |
f8271158 | 11 | Popen, |
12 | PostProcessingError, | |
ddbed364 | 13 | check_executable, |
2cc6d135 | 14 | encodeArgument, |
ddbed364 | 15 | encodeFilename, |
06167fbb | 16 | error_to_compat_str, |
ddbed364 | 17 | prepend_extension, |
f5b1bca9 | 18 | shell_quote, |
ddbed364 | 19 | ) |
20 | ||
9b8ee23b | 21 | if mutagen: |
22 | from mutagen.flac import FLAC, Picture | |
23 | from mutagen.mp4 import MP4, MP4Cover | |
24 | from mutagen.oggopus import OggOpus | |
25 | from mutagen.oggvorbis import OggVorbis | |
26 | ||
ddbed364 | 27 | |
28 | class EmbedThumbnailPPError(PostProcessingError): | |
29 | pass | |
30 | ||
31 | ||
31fd9c76 | 32 | class EmbedThumbnailPP(FFmpegPostProcessor): |
1b77b347 | 33 | |
8e595397 | 34 | def __init__(self, downloader=None, already_have_thumbnail=False): |
8fa43c73 | 35 | FFmpegPostProcessor.__init__(self, downloader) |
8e595397 YCH |
36 | self._already_have_thumbnail = already_have_thumbnail |
37 | ||
95131b21 | 38 | def _get_thumbnail_resolution(self, filename, thumbnail_dict): |
39 | def guess(): | |
40 | width, height = thumbnail_dict.get('width'), thumbnail_dict.get('height') | |
41 | if width and height: | |
42 | return width, height | |
43 | ||
44 | try: | |
45 | size_regex = r',\s*(?P<w>\d+)x(?P<h>\d+)\s*[,\[]' | |
00034c14 | 46 | size_result = self.run_ffmpeg(filename, None, ['-hide_banner'], expected_retcodes=(1,)) |
95131b21 | 47 | mobj = re.search(size_regex, size_result) |
48 | if mobj is None: | |
49 | return guess() | |
50 | except PostProcessingError as err: | |
51 | self.report_warning('unable to find the thumbnail resolution; %s' % error_to_compat_str(err)) | |
52 | return guess() | |
53 | return int(mobj.group('w')), int(mobj.group('h')) | |
54 | ||
acdecdfa | 55 | def _report_run(self, exe, filename): |
86e5f3ed | 56 | self.to_screen(f'{exe}: Adding thumbnail to "{filename}"') |
acdecdfa | 57 | |
8326b00a | 58 | @PostProcessor._restrict_to(images=False) |
ddbed364 | 59 | def run(self, info): |
60 | filename = info['filepath'] | |
61 | temp_filename = prepend_extension(filename, 'temp') | |
ddbed364 | 62 | |
8e595397 | 63 | if not info.get('thumbnails'): |
1b77b347 | 64 | self.to_screen('There aren\'t any thumbnails to embed') |
b5cbe3d6 | 65 | return [], info |
ddbed364 | 66 | |
337e0c62 | 67 | idx = next((-i for i, t in enumerate(info['thumbnails'][::-1], 1) if t.get('filepath')), None) |
885cc0b7 | 68 | if idx is None: |
69 | self.to_screen('There are no thumbnails on disk') | |
70 | return [], info | |
71 | thumbnail_filename = info['thumbnails'][idx]['filepath'] | |
c33a8639 | 72 | if not os.path.exists(encodeFilename(thumbnail_filename)): |
f446cc66 | 73 | self.report_warning('Skipping embedding the thumbnail because the file is missing.') |
c33a8639 YCH |
74 | return [], info |
75 | ||
bff857a8 | 76 | # Correct extension for WebP file with wrong extension (see #25687, #25717) |
8fa43c73 | 77 | convertor = FFmpegThumbnailsConvertorPP(self._downloader) |
885cc0b7 | 78 | convertor.fixup_webp(info, idx) |
8fa43c73 | 79 | |
885cc0b7 | 80 | original_thumbnail = thumbnail_filename = info['thumbnails'][idx]['filepath'] |
bff857a8 | 81 | |
7774db5b ES |
82 | # Convert unsupported thumbnail formats (see #25687, #25717) |
83 | # PNG is preferred since JPEG is lossy | |
1d485a1a | 84 | thumbnail_ext = os.path.splitext(thumbnail_filename)[1][1:] |
7774db5b | 85 | if info['ext'] not in ('mkv', 'mka') and thumbnail_ext not in ('jpg', 'jpeg', 'png'): |
a927acb1 | 86 | thumbnail_filename = convertor.convert_thumbnail(thumbnail_filename, 'png') |
87 | thumbnail_ext = 'png' | |
777d5a45 | 88 | |
ca879745 | 89 | mtime = os.stat(encodeFilename(filename)).st_mtime |
90 | ||
67002a5a | 91 | success = True |
8e2915d7 | 92 | if info['ext'] == 'mp3': |
92995e62 | 93 | options = [ |
f8942946 | 94 | '-c', 'copy', '-map', '0:0', '-map', '1:0', '-write_id3v1', '1', '-id3v2_version', '3', |
e02e6d86 | 95 | '-metadata:s:v', 'title="Album cover"', '-metadata:s:v', 'comment=Cover (front)'] |
ddbed364 | 96 | |
acdecdfa | 97 | self._report_run('ffmpeg', filename) |
bb8ca1d1 | 98 | self.run_ffmpeg_multiple_files([filename, thumbnail_filename], temp_filename, options) |
ddbed364 | 99 | |
06167fbb | 100 | elif info['ext'] in ['mkv', 'mka']: |
397235c5 | 101 | options = list(self.stream_copy_opts()) |
ddbed364 | 102 | |
1d485a1a | 103 | mimetype = f'image/{thumbnail_ext.replace("jpg", "jpeg")}' |
06167fbb | 104 | old_stream, new_stream = self.get_stream_number( |
105 | filename, ('tags', 'mimetype'), mimetype) | |
106 | if old_stream is not None: | |
107 | options.extend(['-map', '-0:%d' % old_stream]) | |
108 | new_stream -= 1 | |
109 | options.extend([ | |
0f0875ed | 110 | '-attach', self._ffmpeg_filename_argument(thumbnail_filename), |
06167fbb | 111 | '-metadata:s:%d' % new_stream, 'mimetype=%s' % mimetype, |
112 | '-metadata:s:%d' % new_stream, 'filename=cover.%s' % thumbnail_ext]) | |
ddbed364 | 113 | |
acdecdfa | 114 | self._report_run('ffmpeg', filename) |
06167fbb | 115 | self.run_ffmpeg(filename, temp_filename, options) |
116 | ||
117 | elif info['ext'] in ['m4a', 'mp4', 'mov']: | |
e858a9d6 | 118 | prefer_atomicparsley = 'embed-thumbnail-atomicparsley' in self.get_param('compat_opts', []) |
acdecdfa | 119 | # Method 1: Use mutagen |
9b8ee23b | 120 | if not mutagen or prefer_atomicparsley: |
acdecdfa | 121 | success = False |
122 | else: | |
123 | try: | |
124 | self._report_run('mutagen', filename) | |
125 | meta = MP4(filename) | |
126 | # NOTE: the 'covr' atom is a non-standard MPEG-4 atom, | |
127 | # Apple iTunes 'M4A' files include the 'moov.udta.meta.ilst' atom. | |
f17c7022 | 128 | f = {'jpeg': MP4Cover.FORMAT_JPEG, 'png': MP4Cover.FORMAT_PNG}[imghdr.what(thumbnail_filename)] |
acdecdfa | 129 | with open(thumbnail_filename, 'rb') as thumbfile: |
130 | thumb_data = thumbfile.read() | |
131 | meta.tags['covr'] = [MP4Cover(data=thumb_data, imageformat=f)] | |
132 | meta.save() | |
133 | temp_filename = filename | |
134 | except Exception as err: | |
135 | self.report_warning('unable to embed using mutagen; %s' % error_to_compat_str(err)) | |
136 | success = False | |
06167fbb | 137 | |
77cee0f1 | 138 | # Method 2: Use AtomicParsley |
139 | if not success: | |
140 | success = True | |
141 | atomicparsley = next(( | |
b5e9a641 | 142 | # libatomicparsley.so : See https://github.com/xibr/ytdlp-lazy/issues/1 |
143 | x for x in ['AtomicParsley', 'atomicparsley', 'libatomicparsley.so'] | |
77cee0f1 | 144 | if check_executable(x, ['-v'])), None) |
145 | if atomicparsley is None: | |
146 | self.to_screen('Neither mutagen nor AtomicParsley was found. Falling back to ffmpeg') | |
147 | success = False | |
148 | else: | |
149 | if not prefer_atomicparsley: | |
150 | self.to_screen('mutagen was not found. Falling back to AtomicParsley') | |
151 | cmd = [encodeFilename(atomicparsley, True), | |
152 | encodeFilename(filename, True), | |
153 | encodeArgument('--artwork'), | |
154 | encodeFilename(thumbnail_filename, True), | |
155 | encodeArgument('-o'), | |
156 | encodeFilename(temp_filename, True)] | |
157 | cmd += [encodeArgument(o) for o in self._configuration_args('AtomicParsley')] | |
158 | ||
159 | self._report_run('atomicparsley', filename) | |
160 | self.write_debug('AtomicParsley command line: %s' % shell_quote(cmd)) | |
f0c9fb96 | 161 | stdout, stderr, returncode = Popen.run(cmd, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
162 | if returncode: | |
163 | self.report_warning(f'Unable to embed thumbnails using AtomicParsley; {stderr.strip()}') | |
77cee0f1 | 164 | # for formats that don't support thumbnails (like 3gp) AtomicParsley |
165 | # won't create to the temporary file | |
f0c9fb96 | 166 | if 'No changes' in stdout: |
77cee0f1 | 167 | self.report_warning('The file format doesn\'t support embedding a thumbnail') |
168 | success = False | |
169 | ||
170 | # Method 3: Use ffmpeg+ffprobe | |
171 | # Thumbnails attached using this method doesn't show up as cover in some cases | |
172 | # See https://github.com/yt-dlp/yt-dlp/issues/2125, https://github.com/yt-dlp/yt-dlp/issues/411 | |
173 | if not success: | |
acdecdfa | 174 | success = True |
175 | try: | |
397235c5 | 176 | options = [*self.stream_copy_opts(), '-map', '1'] |
acdecdfa | 177 | |
178 | old_stream, new_stream = self.get_stream_number( | |
179 | filename, ('disposition', 'attached_pic'), 1) | |
180 | if old_stream is not None: | |
181 | options.extend(['-map', '-0:%d' % old_stream]) | |
182 | new_stream -= 1 | |
183 | options.extend(['-disposition:%s' % new_stream, 'attached_pic']) | |
184 | ||
185 | self._report_run('ffmpeg', filename) | |
186 | self.run_ffmpeg_multiple_files([filename, thumbnail_filename], temp_filename, options) | |
187 | except PostProcessingError as err: | |
06167fbb | 188 | success = False |
77cee0f1 | 189 | raise EmbedThumbnailPPError(f'Unable to embed using ffprobe & ffmpeg; {err}') |
06167fbb | 190 | |
95131b21 | 191 | elif info['ext'] in ['ogg', 'opus', 'flac']: |
9b8ee23b | 192 | if not mutagen: |
e38df8f9 | 193 | raise EmbedThumbnailPPError('module mutagen was not found. Please install using `python -m pip install mutagen`') |
06167fbb | 194 | |
acdecdfa | 195 | self._report_run('mutagen', filename) |
95131b21 | 196 | f = {'opus': OggOpus, 'flac': FLAC, 'ogg': OggVorbis}[info['ext']](filename) |
197 | ||
198 | pic = Picture() | |
199 | pic.mime = 'image/%s' % imghdr.what(thumbnail_filename) | |
200 | with open(thumbnail_filename, 'rb') as thumbfile: | |
201 | pic.data = thumbfile.read() | |
202 | pic.type = 3 # front cover | |
885cc0b7 | 203 | res = self._get_thumbnail_resolution(thumbnail_filename, info['thumbnails'][idx]) |
95131b21 | 204 | if res is not None: |
205 | pic.width, pic.height = res | |
206 | ||
207 | if info['ext'] == 'flac': | |
208 | f.add_picture(pic) | |
209 | else: | |
210 | # https://wiki.xiph.org/VorbisComment#METADATA_BLOCK_PICTURE | |
211 | f['METADATA_BLOCK_PICTURE'] = base64.b64encode(pic.write()).decode('ascii') | |
06167fbb | 212 | f.save() |
acdecdfa | 213 | temp_filename = filename |
67002a5a | 214 | |
ddbed364 | 215 | else: |
95131b21 | 216 | raise EmbedThumbnailPPError('Supported filetypes for thumbnail embedding are: mp3, mkv/mka, ogg/opus/flac, m4a/mp4/mov') |
ddbed364 | 217 | |
06167fbb | 218 | if success and temp_filename != filename: |
d75201a8 | 219 | os.replace(temp_filename, filename) |
2e339f59 | 220 | |
ca879745 | 221 | self.try_utime(filename, mtime, mtime) |
43d7f5a5 | 222 | converted = original_thumbnail != thumbnail_filename |
223 | self._delete_downloaded_files( | |
224 | thumbnail_filename if converted or not self._already_have_thumbnail else None, | |
225 | original_thumbnail if converted and not self._already_have_thumbnail else None, | |
226 | info=info) | |
227 | return [], info |