diff --git a/youtube_dl/YoutubeDL.py b/youtube_dl/YoutubeDL.py index 61675d8ec8..4cc3ec2fb4 100755 --- a/youtube_dl/YoutubeDL.py +++ b/youtube_dl/YoutubeDL.py @@ -70,6 +70,7 @@ from .extractor import get_info_extractor, gen_extractors from .downloader import get_suitable_downloader from .downloader.rtmp import rtmpdump_version from .postprocessor import ( + FFmpegFixupStretchedPP, FFmpegMergerPP, FFmpegPostProcessor, get_postprocessor, @@ -204,6 +205,12 @@ class YoutubeDL(object): Progress hooks are guaranteed to be called at least once (with status "finished") if the download is successful. merge_output_format: Extension to use when merging formats. + fixup: Automatically correct known faults of the file. + One of: + - "never": do nothing + - "warn": only emit a warning + - "detect_or_warn": check whether we can do anything + about it, warn otherwise The following parameters are not used by YoutubeDL itself, they are used by @@ -924,6 +931,7 @@ class YoutubeDL(object): 'fps': formats_info[0].get('fps'), 'vcodec': formats_info[0].get('vcodec'), 'vbr': formats_info[0].get('vbr'), + 'stretched_ratio': formats_info[0].get('stretched_ratio'), 'acodec': formats_info[1].get('acodec'), 'abr': formats_info[1].get('abr'), 'ext': output_ext, @@ -1154,6 +1162,27 @@ class YoutubeDL(object): return if success: + # Fixup content + stretched_ratio = info_dict.get('stretched_ratio') + if stretched_ratio is not None and stretched_ratio != 1: + fixup_policy = self.params.get('fixup') + if fixup_policy is None: + fixup_policy = 'detect_or_warn' + if fixup_policy == 'warn': + self.report_warning('%s: Non-uniform pixel ratio (%s)' % ( + info_dict['id'], stretched_ratio)) + elif fixup_policy == 'detect_or_warn': + stretched_pp = FFmpegFixupStretchedPP(self) + if stretched_pp.available: + info_dict.setdefault('__postprocessors', []) + info_dict['__postprocessors'].append(stretched_pp) + else: + self.report_warning( + '%s: Non-uniform pixel ratio (%s). Install ffmpeg or avconv to fix this automatically.' % ( + info_dict['id'], stretched_ratio)) + else: + assert fixup_policy == 'ignore' + try: self.post_process(filename, info_dict) except (PostProcessingError) as err: diff --git a/youtube_dl/__init__.py b/youtube_dl/__init__.py index 8e7b744666..659a92a3b9 100644 --- a/youtube_dl/__init__.py +++ b/youtube_dl/__init__.py @@ -326,6 +326,7 @@ def _real_main(argv=None): 'extract_flat': opts.extract_flat, 'merge_output_format': opts.merge_output_format, 'postprocessors': postprocessors, + 'fixup': opts.fixup, } with YoutubeDL(ydl_opts) as ydl: diff --git a/youtube_dl/extractor/common.py b/youtube_dl/extractor/common.py index cd155a0901..363e2000c1 100644 --- a/youtube_dl/extractor/common.py +++ b/youtube_dl/extractor/common.py @@ -114,6 +114,9 @@ class InfoExtractor(object): to add to the request. * http_post_data Additional data to send with a POST request. + * stretched_ratio If given and not 1, indicates that the + video's pixels are not square. + width : height ratio as float. url: Final video URL. ext: Video filename extension. format: The video format, defaults to ext (used for --get-format) diff --git a/youtube_dl/extractor/youtube.py b/youtube_dl/extractor/youtube.py index bc18276d6c..c7611a3a09 100644 --- a/youtube_dl/extractor/youtube.py +++ b/youtube_dl/extractor/youtube.py @@ -465,6 +465,20 @@ class YoutubeIE(YoutubeBaseInfoExtractor, SubtitlesInfoExtractor): 'skip_download': 'requires avconv', } }, + # Non-square pixels + { + 'url': 'https://www.youtube.com/watch?v=_b-2C3KPAM0', + 'info_dict': { + 'id': '_b-2C3KPAM0', + 'ext': 'mp4', + 'stretched_ratio': 16 / 9., + 'upload_date': '20110310', + 'uploader_id': 'AllenMeow', + 'description': 'made by Wacom from Korea | 字幕&加油添醋 by TY\'s Allen | 感謝heylisa00cavey1001同學熱情提供梗及翻譯', + 'uploader': '孫艾倫', + 'title': '[A-made] 變態妍字幕版 太妍 我就是這樣的人', + }, + } ] def __init__(self, *args, **kwargs): @@ -1051,6 +1065,16 @@ class YoutubeIE(YoutubeBaseInfoExtractor, SubtitlesInfoExtractor): f['preference'] = f.get('preference', 0) - 10000 formats.extend(dash_formats) + # Check for malformed aspect ratio + stretched_m = re.search( + r'[0-9]+):(?P[0-9]+)">', + video_webpage) + if stretched_m: + ratio = float(stretched_m.group('w')) / float(stretched_m.group('h')) + for f in formats: + if f.get('vcodec') != 'none': + f['stretched_ratio'] = ratio + self._sort_formats(formats) return { diff --git a/youtube_dl/options.py b/youtube_dl/options.py index 14006178d7..e5602bb3a3 100644 --- a/youtube_dl/options.py +++ b/youtube_dl/options.py @@ -631,6 +631,13 @@ def parseOpts(overrideArguments=None): '--xattrs', action='store_true', dest='xattrs', default=False, help='write metadata to the video file\'s xattrs (using dublin core and xdg standards)') + postproc.add_option( + '--fixup', + metavar='POLICY', dest='fixup', default='detect_or_warn', + help='(experimental) Automatically correct known faults of the file. ' + 'One of never (do nothing), warn (only emit a warning), ' + 'detect_or_warn(check whether we can do anything about it, warn ' + 'otherwise') postproc.add_option( '--prefer-avconv', action='store_false', dest='prefer_ffmpeg', diff --git a/youtube_dl/postprocessor/__init__.py b/youtube_dl/postprocessor/__init__.py index 7f505b58e2..f8507951ce 100644 --- a/youtube_dl/postprocessor/__init__.py +++ b/youtube_dl/postprocessor/__init__.py @@ -6,6 +6,7 @@ from .ffmpeg import ( FFmpegAudioFixPP, FFmpegEmbedSubtitlePP, FFmpegExtractAudioPP, + FFmpegFixupStretchedPP, FFmpegMergerPP, FFmpegMetadataPP, FFmpegVideoConvertorPP, @@ -24,6 +25,7 @@ __all__ = [ 'FFmpegAudioFixPP', 'FFmpegEmbedSubtitlePP', 'FFmpegExtractAudioPP', + 'FFmpegFixupStretchedPP', 'FFmpegMergerPP', 'FFmpegMetadataPP', 'FFmpegPostProcessor', diff --git a/youtube_dl/postprocessor/ffmpeg.py b/youtube_dl/postprocessor/ffmpeg.py index d1b342c7a6..6e9194fa6a 100644 --- a/youtube_dl/postprocessor/ffmpeg.py +++ b/youtube_dl/postprocessor/ffmpeg.py @@ -50,6 +50,10 @@ class FFmpegPostProcessor(PostProcessor): programs = ['avprobe', 'avconv', 'ffmpeg', 'ffprobe'] return dict((p, get_exe_version(p, args=['-version'])) for p in programs) + @property + def available(self): + return self._executable is not None + @property def _executable(self): if self._downloader.params.get('prefer_ffmpeg', False): @@ -540,3 +544,22 @@ class FFmpegAudioFixPP(FFmpegPostProcessor): os.rename(encodeFilename(temp_filename), encodeFilename(filename)) return True, info + + +class FFmpegFixupStretchedPP(FFmpegPostProcessor): + def run(self, info): + stretched_ratio = info.get('stretched_ratio') + if stretched_ratio is None or stretched_ratio == 1: + return + + filename = info['filepath'] + temp_filename = prepend_extension(filename, 'temp') + + options = ['-c', 'copy', '-aspect', '%f' % stretched_ratio] + self._downloader.to_screen('[ffmpeg] Fixing aspect ratio in "%s"' % filename) + self.run_ffmpeg(filename, temp_filename, options) + + os.remove(encodeFilename(filename)) + os.rename(encodeFilename(temp_filename), encodeFilename(filename)) + + return True, info