]> jfr.im git - yt-dlp.git/blame - yt_dlp/postprocessor/embedthumbnail.py
Fix number of digits in `%(playlist_index)s`
[yt-dlp.git] / yt_dlp / postprocessor / embedthumbnail.py
CommitLineData
dcdb292f 1# coding: utf-8
ddbed364 2from __future__ import unicode_literals
3
ddbed364 4import os
5import subprocess
06167fbb 6import struct
7import re
8import base64
9
10try:
11 import mutagen
06ff212d 12 has_mutagen = True
06167fbb 13except ImportError:
06ff212d 14 has_mutagen = False
ddbed364 15
8fa43c73 16from .ffmpeg import (
17 FFmpegPostProcessor,
18 FFmpegThumbnailsConvertorPP,
19)
ddbed364 20from ..utils import (
21 check_executable,
2cc6d135 22 encodeArgument,
ddbed364 23 encodeFilename,
06167fbb 24 error_to_compat_str,
ddbed364 25 PostProcessingError,
26 prepend_extension,
06167fbb 27 process_communicate_or_kill,
f5b1bca9 28 shell_quote,
ddbed364 29)
30
31
32class EmbedThumbnailPPError(PostProcessingError):
33 pass
34
35
31fd9c76 36class EmbedThumbnailPP(FFmpegPostProcessor):
1b77b347 37
8e595397 38 def __init__(self, downloader=None, already_have_thumbnail=False):
8fa43c73 39 FFmpegPostProcessor.__init__(self, downloader)
8e595397
YCH
40 self._already_have_thumbnail = already_have_thumbnail
41
ddbed364 42 def run(self, info):
43 filename = info['filepath']
44 temp_filename = prepend_extension(filename, 'temp')
ddbed364 45
8e595397 46 if not info.get('thumbnails'):
1b77b347 47 self.to_screen('There aren\'t any thumbnails to embed')
b5cbe3d6 48 return [], info
ddbed364 49
8fa43c73 50 thumbnail_filename = info['thumbnails'][-1]['filepath']
c33a8639 51 if not os.path.exists(encodeFilename(thumbnail_filename)):
f446cc66 52 self.report_warning('Skipping embedding the thumbnail because the file is missing.')
c33a8639
YCH
53 return [], info
54
bff857a8 55 # Correct extension for WebP file with wrong extension (see #25687, #25717)
8fa43c73 56 convertor = FFmpegThumbnailsConvertorPP(self._downloader)
57 convertor.fixup_webp(info, -1)
58
59 original_thumbnail = thumbnail_filename = info['thumbnails'][-1]['filepath']
bff857a8
S
60
61 # Convert unsupported thumbnail formats to JPEG (see #25687, #25717)
8fa43c73 62 _, thumbnail_ext = os.path.splitext(thumbnail_filename)
63 if thumbnail_ext not in ('jpg', 'png'):
64 thumbnail_filename = convertor.convert_thumbnail(thumbnail_filename, 'jpg')
06167fbb 65 thumbnail_ext = 'jpg'
777d5a45 66
ca879745 67 mtime = os.stat(encodeFilename(filename)).st_mtime
68
67002a5a 69 success = True
8e2915d7 70 if info['ext'] == 'mp3':
92995e62 71 options = [
c76eb41b 72 '-c', 'copy', '-map', '0:0', '-map', '1:0', '-id3v2_version', '3',
e51f368c 73 '-metadata:s:v', 'title="Album cover"', '-metadata:s:v', 'comment="Cover (front)"']
ddbed364 74
1b77b347 75 self.to_screen('Adding thumbnail to "%s"' % filename)
bb8ca1d1 76 self.run_ffmpeg_multiple_files([filename, thumbnail_filename], temp_filename, options)
ddbed364 77
06167fbb 78 elif info['ext'] in ['mkv', 'mka']:
79 options = ['-c', 'copy', '-map', '0', '-dn']
ddbed364 80
06167fbb 81 mimetype = 'image/%s' % ('png' if thumbnail_ext == 'png' else 'jpeg')
82 old_stream, new_stream = self.get_stream_number(
83 filename, ('tags', 'mimetype'), mimetype)
84 if old_stream is not None:
85 options.extend(['-map', '-0:%d' % old_stream])
86 new_stream -= 1
87 options.extend([
88 '-attach', thumbnail_filename,
89 '-metadata:s:%d' % new_stream, 'mimetype=%s' % mimetype,
90 '-metadata:s:%d' % new_stream, 'filename=cover.%s' % thumbnail_ext])
ddbed364 91
1b77b347 92 self.to_screen('Adding thumbnail to "%s"' % filename)
06167fbb 93 self.run_ffmpeg(filename, temp_filename, options)
94
95 elif info['ext'] in ['m4a', 'mp4', 'mov']:
96 try:
97 options = ['-c', 'copy', '-map', '0', '-dn', '-map', '1']
98
99 old_stream, new_stream = self.get_stream_number(
100 filename, ('disposition', 'attached_pic'), 1)
101 if old_stream is not None:
102 options.extend(['-map', '-0:%d' % old_stream])
103 new_stream -= 1
104 options.extend(['-disposition:%s' % new_stream, 'attached_pic'])
105
106 self.to_screen('Adding thumbnail to "%s"' % filename)
107 self.run_ffmpeg_multiple_files([filename, thumbnail_filename], temp_filename, options)
108
109 except PostProcessingError as err:
110 self.report_warning('unable to embed using ffprobe & ffmpeg; %s' % error_to_compat_str(err))
bc2ca1bb 111 atomicparsley = next((
112 x for x in ['AtomicParsley', 'atomicparsley']
113 if check_executable(x, ['-v'])), None)
114 if atomicparsley is None:
beb4b92a 115 raise EmbedThumbnailPPError('AtomicParsley was not found. Please install')
06167fbb 116
bc2ca1bb 117 cmd = [encodeFilename(atomicparsley, True),
06167fbb 118 encodeFilename(filename, True),
119 encodeArgument('--artwork'),
120 encodeFilename(thumbnail_filename, True),
121 encodeArgument('-o'),
122 encodeFilename(temp_filename, True)]
e92caff5 123 cmd += [encodeArgument(o) for o in self._configuration_args('AtomicParsley')]
06167fbb 124
125 self.to_screen('Adding thumbnail to "%s"' % filename)
126 self.write_debug('AtomicParsley command line: %s' % shell_quote(cmd))
127 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
128 stdout, stderr = process_communicate_or_kill(p)
129 if p.returncode != 0:
130 msg = stderr.decode('utf-8', 'replace').strip()
131 raise EmbedThumbnailPPError(msg)
132 # for formats that don't support thumbnails (like 3gp) AtomicParsley
133 # won't create to the temporary file
134 if b'No changes' in stdout:
135 self.report_warning('The file format doesn\'t support embedding a thumbnail')
136 success = False
137
138 elif info['ext'] in ['ogg', 'opus']:
06ff212d 139 if not has_mutagen:
e38df8f9 140 raise EmbedThumbnailPPError('module mutagen was not found. Please install using `python -m pip install mutagen`')
e3b771a8 141 self.to_screen('Adding thumbnail to "%s"' % filename)
142
06167fbb 143 size_regex = r',\s*(?P<w>\d+)x(?P<h>\d+)\s*[,\[]'
ece8a2a1 144 size_result = self.run_ffmpeg(thumbnail_filename, thumbnail_filename, ['-hide_banner'])
06167fbb 145 mobj = re.search(size_regex, size_result)
146 width, height = int(mobj.group('w')), int(mobj.group('h'))
147 mimetype = ('image/%s' % ('png' if thumbnail_ext == 'png' else 'jpeg')).encode('ascii')
148
149 # https://xiph.org/flac/format.html#metadata_block_picture
150 data = bytearray()
151 data += struct.pack('>II', 3, len(mimetype))
152 data += mimetype
153 data += struct.pack('>IIIIII', 0, width, height, 8, 0, os.stat(thumbnail_filename).st_size) # 32 if png else 24
154
155 fin = open(thumbnail_filename, "rb")
156 data += fin.read()
157 fin.close()
158
159 temp_filename = filename
160 f = mutagen.File(temp_filename)
161 f.tags['METADATA_BLOCK_PICTURE'] = base64.b64encode(data).decode('ascii')
162 f.save()
67002a5a 163
ddbed364 164 else:
06167fbb 165 raise EmbedThumbnailPPError('Supported filetypes for thumbnail embedding are: mp3, mkv/mka, ogg/opus, m4a/mp4/mov')
ddbed364 166
06167fbb 167 if success and temp_filename != filename:
67002a5a 168 os.remove(encodeFilename(filename))
169 os.rename(encodeFilename(temp_filename), encodeFilename(filename))
2e339f59 170
ca879745 171 self.try_utime(filename, mtime, mtime)
172
2e339f59 173 files_to_delete = [thumbnail_filename]
de6000d9 174 if self._already_have_thumbnail:
2e339f59 175 if original_thumbnail == thumbnail_filename:
176 files_to_delete = []
0e004051 177 elif original_thumbnail != thumbnail_filename:
178 files_to_delete.append(original_thumbnail)
67002a5a 179 return files_to_delete, info