From fd35c3fbaaab7b6b1563af2b01bac35cb6837f99 Mon Sep 17 00:00:00 2001 From: shin Date: Thu, 26 Dec 2019 12:57:48 +0530 Subject: [PATCH 1/8] MP4/M4A files use 'mutagen' for thumbnail embedding Previously, AtomicParsley was used to embed thumbnails into moov.udta.meta.ilst atom which contains iTunes style metadata for audio files. Using AtomicParsley for only embedding thumbnails was quite infiuriating.Mutagen can manipulate audio metadata and the module itself is quite lightweight and doesn't require any deps. I (shin-ts) favoured to have a package as addon instead of an executable, because this method is quite convinient in my opinion. --- youtube_dl/postprocessor/embedthumbnail.py | 82 +++++++++++----------- 1 file changed, 42 insertions(+), 40 deletions(-) diff --git a/youtube_dl/postprocessor/embedthumbnail.py b/youtube_dl/postprocessor/embedthumbnail.py index 56be914b8..405ea2dbd 100644 --- a/youtube_dl/postprocessor/embedthumbnail.py +++ b/youtube_dl/postprocessor/embedthumbnail.py @@ -5,6 +5,10 @@ from __future__ import unicode_literals import os import subprocess +import imghdr +from mutagen.id3 import PictureType, ID3, APIC +from mutagen.mp4 import MP4, MP4Cover + from .ffmpeg import FFmpegPostProcessor from ..utils import ( @@ -28,7 +32,6 @@ class EmbedThumbnailPP(FFmpegPostProcessor): def run(self, info): filename = info['filepath'] - temp_filename = prepend_extension(filename, 'temp') if not info.get('thumbnails'): self._downloader.to_screen('[embedthumbnail] There aren\'t any thumbnails to embed') @@ -42,52 +45,51 @@ class EmbedThumbnailPP(FFmpegPostProcessor): return [], info if info['ext'] == 'mp3': - options = [ - '-c', 'copy', '-map', '0', '-map', '1', - '-metadata:s:v', 'title="Album cover"', '-metadata:s:v', 'comment="Cover (Front)"'] - - self._downloader.to_screen('[ffmpeg] Adding thumbnail to "%s"' % filename) - - self.run_ffmpeg_multiple_files([filename, thumbnail_filename], temp_filename, options) - + try: + meta = ID3(filename) + except: + raise EmbedThumbnailPPError("MP3 file doesn't have a existing ID3v2 tag.") + + # Update older tags (eg. ID3v1) to a newer version, + # which supports embedded-thumbnails (e.g ID3v2.3). + # NOTE: ID3v2.4 might not be supported by programs. + meta.update_to_v23() + + # Appends a Cover-front thumbnail, it's the most common + # type of thumbnail distributed with. + meta.add(APIC( + data= open(thumbnail_filename, 'rb').read(), + mime= 'image/'+imghdr.what(thumbnail_filename), + type= PictureType.COVER_FRONT + )) + + meta.save() # Save the changes to file, does in-place replacement. + self._downloader.to_screen('[mutagen.id3] Merged Thumbnail into "%s"' % filename) + if not self._already_have_thumbnail: - os.remove(encodeFilename(thumbnail_filename)) - os.remove(encodeFilename(filename)) - os.rename(encodeFilename(temp_filename), encodeFilename(filename)) + os.remove(encodeFilename(thumbnail_filename)) elif info['ext'] in ['m4a', 'mp4']: - if not check_executable('AtomicParsley', ['-v']): - raise EmbedThumbnailPPError('AtomicParsley was not found. Please install.') + try: + meta = MP4(filename) + except: + raise EmbedThumbnailPPError("MPEG-4 file's atomic structure for embedding isn't correct!") - cmd = [encodeFilename('AtomicParsley', True), - encodeFilename(filename, True), - encodeArgument('--artwork'), - encodeFilename(thumbnail_filename, True), - encodeArgument('-o'), - encodeFilename(temp_filename, True)] + # NOTE: the 'covr' atom is a non-standard MPEG-4 atom, + # Apple iTunes 'M4A' files include the 'moov.udta.meta.ilst' atom. + meta.tags['covr'] = [MP4Cover( + data= open(thumbnail_filename, 'rb').read(), + imageformat= MP4Cover.FORMAT_JPEG if \ + imghdr.what(thumbnail_filename) == 'jpeg' \ + else MP4Cover.FORMAT_PNG + )] - self._downloader.to_screen('[atomicparsley] Adding thumbnail to "%s"' % filename) - - if self._downloader.params.get('verbose', False): - self._downloader.to_screen('[debug] AtomicParsley command line: %s' % shell_quote(cmd)) - - p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) - stdout, stderr = p.communicate() - - if p.returncode != 0: - msg = stderr.decode('utf-8', 'replace').strip() - raise EmbedThumbnailPPError(msg) + meta.save() + self._downloader.to_screen('[mutagen.mp4] Merged thumbnail to "%s"' % filename) if not self._already_have_thumbnail: os.remove(encodeFilename(thumbnail_filename)) - # for formats that don't support thumbnails (like 3gp) AtomicParsley - # won't create to the temporary file - if b'No changes' in stdout: - self._downloader.report_warning('The file format doesn\'t support embedding a thumbnail') - else: - os.remove(encodeFilename(filename)) - os.rename(encodeFilename(temp_filename), encodeFilename(filename)) else: - raise EmbedThumbnailPPError('Only mp3 and m4a/mp4 are supported for thumbnail embedding for now.') + raise EmbedThumbnailPPError('Only mp3, m4a/mp4 are supported for thumbnail embedding for now.') - return [], info + return [], info \ No newline at end of file From e82a75d7e7b2417a3c5dff9c2cd2c77c14c57bfe Mon Sep 17 00:00:00 2001 From: shin Date: Thu, 26 Dec 2019 13:00:51 +0530 Subject: [PATCH 2/8] Throw exceptions if 'mutagen' cannot be found --- youtube_dl/postprocessor/embedthumbnail.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/youtube_dl/postprocessor/embedthumbnail.py b/youtube_dl/postprocessor/embedthumbnail.py index 405ea2dbd..de6a645e2 100644 --- a/youtube_dl/postprocessor/embedthumbnail.py +++ b/youtube_dl/postprocessor/embedthumbnail.py @@ -5,9 +5,12 @@ from __future__ import unicode_literals import os import subprocess -import imghdr -from mutagen.id3 import PictureType, ID3, APIC -from mutagen.mp4 import MP4, MP4Cover +try: + import imghdr + from mutagen.id3 import PictureType, ID3, APIC + from mutagen.mp4 import MP4, MP4Cover +except ImportError: + raise Exception('[embedthumbnail] Mutagen isn\'t found as a dependency to embed thumbnails!') from .ffmpeg import FFmpegPostProcessor From 6fb38337d623bbe737b8ba64b119a7d732bb3bcc Mon Sep 17 00:00:00 2001 From: shin Date: Thu, 26 Dec 2019 13:18:13 +0530 Subject: [PATCH 3/8] Followed the code-convention of 'youtube-dl' --- youtube_dl/postprocessor/embedthumbnail.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/youtube_dl/postprocessor/embedthumbnail.py b/youtube_dl/postprocessor/embedthumbnail.py index de6a645e2..1c2f1150a 100644 --- a/youtube_dl/postprocessor/embedthumbnail.py +++ b/youtube_dl/postprocessor/embedthumbnail.py @@ -63,8 +63,7 @@ class EmbedThumbnailPP(FFmpegPostProcessor): meta.add(APIC( data= open(thumbnail_filename, 'rb').read(), mime= 'image/'+imghdr.what(thumbnail_filename), - type= PictureType.COVER_FRONT - )) + type= PictureType.COVER_FRONT)) meta.save() # Save the changes to file, does in-place replacement. self._downloader.to_screen('[mutagen.id3] Merged Thumbnail into "%s"' % filename) @@ -84,8 +83,7 @@ class EmbedThumbnailPP(FFmpegPostProcessor): data= open(thumbnail_filename, 'rb').read(), imageformat= MP4Cover.FORMAT_JPEG if \ imghdr.what(thumbnail_filename) == 'jpeg' \ - else MP4Cover.FORMAT_PNG - )] + else MP4Cover.FORMAT_PNG)] meta.save() self._downloader.to_screen('[mutagen.mp4] Merged thumbnail to "%s"' % filename) From edcaf554b001dd7d3fae78ba5a90b3bac614eb9b Mon Sep 17 00:00:00 2001 From: shin Date: Thu, 26 Dec 2019 13:43:50 +0530 Subject: [PATCH 4/8] Refactored the according according to 'flake8' --- youtube_dl/postprocessor/embedthumbnail.py | 50 +++++++++------------- 1 file changed, 20 insertions(+), 30 deletions(-) diff --git a/youtube_dl/postprocessor/embedthumbnail.py b/youtube_dl/postprocessor/embedthumbnail.py index 1c2f1150a..ecd875dbb 100644 --- a/youtube_dl/postprocessor/embedthumbnail.py +++ b/youtube_dl/postprocessor/embedthumbnail.py @@ -3,24 +3,19 @@ from __future__ import unicode_literals import os -import subprocess try: import imghdr - from mutagen.id3 import PictureType, ID3, APIC - from mutagen.mp4 import MP4, MP4Cover + from mutagen.id3 import PictureType, ID3, APIC, ID3NoHeaderError + from mutagen.mp4 import MP4, MP4Cover, MP4MetadataError except ImportError: raise Exception('[embedthumbnail] Mutagen isn\'t found as a dependency to embed thumbnails!') from .ffmpeg import FFmpegPostProcessor from ..utils import ( - check_executable, - encodeArgument, encodeFilename, - PostProcessingError, - prepend_extension, - shell_quote + PostProcessingError ) @@ -49,41 +44,36 @@ class EmbedThumbnailPP(FFmpegPostProcessor): if info['ext'] == 'mp3': try: - meta = ID3(filename) - except: - raise EmbedThumbnailPPError("MP3 file doesn't have a existing ID3v2 tag.") - - # Update older tags (eg. ID3v1) to a newer version, - # which supports embedded-thumbnails (e.g ID3v2.3). - # NOTE: ID3v2.4 might not be supported by programs. - meta.update_to_v23() - + meta = ID3(filename) + except ID3NoHeaderError: + raise EmbedThumbnailPPError("MP3 file doesn't have a existing ID3v2 tag.") + # Appends a Cover-front thumbnail, it's the most common # type of thumbnail distributed with. meta.add(APIC( - data= open(thumbnail_filename, 'rb').read(), - mime= 'image/'+imghdr.what(thumbnail_filename), - type= PictureType.COVER_FRONT)) - - meta.save() # Save the changes to file, does in-place replacement. + data=open(thumbnail_filename, 'rb').read(), + mime='image/' + imghdr.what(thumbnail_filename), + type=PictureType.COVER_FRONT)) + + meta.save() # Save the changes to file, does in-place replacement. self._downloader.to_screen('[mutagen.id3] Merged Thumbnail into "%s"' % filename) - + if not self._already_have_thumbnail: - os.remove(encodeFilename(thumbnail_filename)) + os.remove(encodeFilename(thumbnail_filename)) elif info['ext'] in ['m4a', 'mp4']: try: meta = MP4(filename) - except: + except MP4MetadataError: raise EmbedThumbnailPPError("MPEG-4 file's atomic structure for embedding isn't correct!") # NOTE: the 'covr' atom is a non-standard MPEG-4 atom, # Apple iTunes 'M4A' files include the 'moov.udta.meta.ilst' atom. meta.tags['covr'] = [MP4Cover( - data= open(thumbnail_filename, 'rb').read(), - imageformat= MP4Cover.FORMAT_JPEG if \ - imghdr.what(thumbnail_filename) == 'jpeg' \ - else MP4Cover.FORMAT_PNG)] + data=open(thumbnail_filename, 'rb').read(), + imageformat=MP4Cover.FORMAT_JPEG if + imghdr.what(thumbnail_filename) == 'jpeg' + else MP4Cover.FORMAT_PNG)] meta.save() self._downloader.to_screen('[mutagen.mp4] Merged thumbnail to "%s"' % filename) @@ -93,4 +83,4 @@ class EmbedThumbnailPP(FFmpegPostProcessor): else: raise EmbedThumbnailPPError('Only mp3, m4a/mp4 are supported for thumbnail embedding for now.') - return [], info \ No newline at end of file + return [], info From e40d4761687ff0396363ca85bbd22c3dc2056d69 Mon Sep 17 00:00:00 2001 From: shin Date: Thu, 26 Dec 2019 14:09:15 +0530 Subject: [PATCH 5/8] Use FFmpeg for embedding thumbnails into MP3 Embedding thumbnails inside ID3v2 tags is done by FFmpeg quite nicely, there's no need to depend on the external dependency. Also, FFmpeg already used for many post-processing and conversion work. --- youtube_dl/postprocessor/embedthumbnail.py | 24 +++++++++------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/youtube_dl/postprocessor/embedthumbnail.py b/youtube_dl/postprocessor/embedthumbnail.py index ecd875dbb..699eeca0c 100644 --- a/youtube_dl/postprocessor/embedthumbnail.py +++ b/youtube_dl/postprocessor/embedthumbnail.py @@ -6,14 +6,14 @@ import os try: import imghdr - from mutagen.id3 import PictureType, ID3, APIC, ID3NoHeaderError from mutagen.mp4 import MP4, MP4Cover, MP4MetadataError except ImportError: - raise Exception('[embedthumbnail] Mutagen isn\'t found as a dependency to embed thumbnails!') + raise Exception('[embedthumbnail] Mutagen isn\'t found, install from PyPI.') from .ffmpeg import FFmpegPostProcessor from ..utils import ( + prepend_extension, encodeFilename, PostProcessingError ) @@ -30,6 +30,7 @@ class EmbedThumbnailPP(FFmpegPostProcessor): def run(self, info): filename = info['filepath'] + temp_filename = prepend_extension(filename, 'temp') if not info.get('thumbnails'): self._downloader.to_screen('[embedthumbnail] There aren\'t any thumbnails to embed') @@ -43,23 +44,18 @@ class EmbedThumbnailPP(FFmpegPostProcessor): return [], info if info['ext'] == 'mp3': - try: - meta = ID3(filename) - except ID3NoHeaderError: - raise EmbedThumbnailPPError("MP3 file doesn't have a existing ID3v2 tag.") + options = [ + '-c', 'copy', '-map', '0', '-map', '1', + '-metadata:s:v', 'title="Album cover"', '-metadata:s:v', 'comment="Cover (Front)"'] - # Appends a Cover-front thumbnail, it's the most common - # type of thumbnail distributed with. - meta.add(APIC( - data=open(thumbnail_filename, 'rb').read(), - mime='image/' + imghdr.what(thumbnail_filename), - type=PictureType.COVER_FRONT)) + self._downloader.to_screen('[ffmpeg] Adding thumbnail to "%s"' % filename) - meta.save() # Save the changes to file, does in-place replacement. - self._downloader.to_screen('[mutagen.id3] Merged Thumbnail into "%s"' % filename) + self.run_ffmpeg_multiple_files([filename, thumbnail_filename], temp_filename, options) if not self._already_have_thumbnail: os.remove(encodeFilename(thumbnail_filename)) + os.remove(encodeFilename(filename)) + os.rename(encodeFilename(temp_filename), encodeFilename(filename)) elif info['ext'] in ['m4a', 'mp4']: try: From 95560183884ce3e71b1653ebfdaaca50398de63f Mon Sep 17 00:00:00 2001 From: shin Date: Thu, 26 Dec 2019 14:20:17 +0530 Subject: [PATCH 6/8] Handle ImportError as a warning --- youtube_dl/postprocessor/embedthumbnail.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/youtube_dl/postprocessor/embedthumbnail.py b/youtube_dl/postprocessor/embedthumbnail.py index 699eeca0c..3523f4992 100644 --- a/youtube_dl/postprocessor/embedthumbnail.py +++ b/youtube_dl/postprocessor/embedthumbnail.py @@ -8,7 +8,7 @@ try: import imghdr from mutagen.mp4 import MP4, MP4Cover, MP4MetadataError except ImportError: - raise Exception('[embedthumbnail] Mutagen isn\'t found, install from PyPI.') + raise Warning('[embedthumbnail] Mutagen isn\'t found for M4A thumbnail embedding, install from PyPI.') from .ffmpeg import FFmpegPostProcessor From 17f82ec733b2699e67fbaba696a1e65b322aaf29 Mon Sep 17 00:00:00 2001 From: shin Date: Thu, 26 Dec 2019 14:26:53 +0530 Subject: [PATCH 7/8] Mutagen is a required dependency. --- setup.py | 3 +++ youtube_dl/postprocessor/embedthumbnail.py | 10 +++------- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/setup.py b/setup.py index af68b485e..a29b93b5a 100644 --- a/setup.py +++ b/setup.py @@ -114,6 +114,9 @@ setup( 'youtube_dl', 'youtube_dl.extractor', 'youtube_dl.downloader', 'youtube_dl.postprocessor'], + install_requires=[ + 'mutagen' + ], # Provokes warning on most systems (why?!) # test_suite = 'nose.collector', diff --git a/youtube_dl/postprocessor/embedthumbnail.py b/youtube_dl/postprocessor/embedthumbnail.py index 3523f4992..74b3e2f5f 100644 --- a/youtube_dl/postprocessor/embedthumbnail.py +++ b/youtube_dl/postprocessor/embedthumbnail.py @@ -3,15 +3,11 @@ from __future__ import unicode_literals import os - -try: - import imghdr - from mutagen.mp4 import MP4, MP4Cover, MP4MetadataError -except ImportError: - raise Warning('[embedthumbnail] Mutagen isn\'t found for M4A thumbnail embedding, install from PyPI.') - from .ffmpeg import FFmpegPostProcessor +import imghdr +from mutagen.mp4 import MP4, MP4Cover, MP4MetadataError + from ..utils import ( prepend_extension, encodeFilename, From 0fab63b7766d0d7926d011366eec99084ebc2ba1 Mon Sep 17 00:00:00 2001 From: shin Date: Thu, 26 Dec 2019 14:30:49 +0530 Subject: [PATCH 8/8] Tests won't yell that mutagen is missing --- youtube_dl/postprocessor/embedthumbnail.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/youtube_dl/postprocessor/embedthumbnail.py b/youtube_dl/postprocessor/embedthumbnail.py index 74b3e2f5f..5d782a41a 100644 --- a/youtube_dl/postprocessor/embedthumbnail.py +++ b/youtube_dl/postprocessor/embedthumbnail.py @@ -5,8 +5,11 @@ from __future__ import unicode_literals import os from .ffmpeg import FFmpegPostProcessor -import imghdr -from mutagen.mp4 import MP4, MP4Cover, MP4MetadataError +try: + import imghdr + from mutagen.mp4 import MP4, MP4Cover, MP4MetadataError +except ImportError: + print("[embedthumbnail] MP4 thumbnail embedding cannot be done, mutagen is missing.") from ..utils import ( prepend_extension,