Compare commits

...

5 Commits

Author SHA1 Message Date
Adam Malcontenti-Wilson
851c4975f9
Merge dc179db221331a6e63c7e1e7bd569fc07b0841ec into 3eb8d22ddb8982ca4fb56bb7a8d6517538bf14c6 2025-04-01 10:48:36 +02:00
dirkf
3eb8d22ddb
[JSInterp] Temporary fix for #33102 2025-03-31 04:21:09 +01:00
dirkf
4e714f9df1 [Misc] Correct [_]IE_DESC/NAME in a few IEs
* thx seproDev, yt-dlp/yt-dlp/pull/12694/commits/ae69e3c
* also add documenting comment in `InfoExtractor`
2025-03-26 12:47:19 +00:00
dirkf
c1ea7f5a24 [ITV] Mark ITVX not working
* update old shim
* correct [_]IE_DESC
2025-03-26 12:17:49 +00:00
Adam Malcontenti-Wilson
dc179db221 Add concat-playlist option 2021-02-13 20:09:31 +11:00
12 changed files with 76 additions and 29 deletions

View File

@ -122,6 +122,7 @@ from .postprocessor import (
FFmpegFixupM4aPP,
FFmpegFixupStretchedPP,
FFmpegMergerPP,
FFmpegConcatPP,
FFmpegPostProcessor,
get_postprocessor,
)
@ -1206,6 +1207,20 @@ class YoutubeDL(object):
entry_result = self.__process_iterable_entry(entry, download, extra)
# TODO: skip failed (empty) entries?
playlist_results.append(entry_result)
if self.params.get('concat_playlist', False):
concat_pp = FFmpegConcatPP(self)
ie_result['__postprocessors'] = [concat_pp]
ie_result['__files_to_concat'] = list(map(lambda e: e['_filename'], entries))
filename_wo_ext = self.prepare_filename(ie_result)
# Ensure filename always has a correct extension for successful merge
ie_result['ext'] = self.params.get('merge_output_format') or entries[0]['ext']
ie_result['_filename'] = filename = '%s.%s' % (filename_wo_ext, ie_result['ext'])
self.post_process(filename, ie_result)
ie_result['entries'] = playlist_results
self.to_screen('[download] Finished downloading playlist: %s' % playlist)
return ie_result
@ -1858,6 +1873,7 @@ class YoutubeDL(object):
new_info = dict(info_dict)
new_info.update(format)
self.process_info(new_info)
format.update(new_info)
# We update the info dict with the best quality format (backwards compatibility)
info_dict.update(formats_to_download[-1])
return info_dict

View File

@ -411,6 +411,7 @@ def _real_main(argv=None):
'youtube_include_dash_manifest': opts.youtube_include_dash_manifest,
'encoding': opts.encoding,
'extract_flat': opts.extract_flat,
'concat_playlist': opts.concat_playlist,
'mark_watched': opts.mark_watched,
'merge_output_format': opts.merge_output_format,
'postprocessors': postprocessors,

View File

@ -32,7 +32,7 @@ class BokeCCBaseIE(InfoExtractor):
class BokeCCIE(BokeCCBaseIE):
_IE_DESC = 'CC视频'
IE_DESC = 'CC视频'
_VALID_URL = r'https?://union\.bokecc\.com/playvideo\.bo\?(?P<query>.*)'
_TESTS = [{

View File

@ -9,7 +9,7 @@ from ..utils import (
class CloudyIE(InfoExtractor):
_IE_DESC = 'cloudy.ec'
IE_DESC = 'cloudy.ec'
_VALID_URL = r'https?://(?:www\.)?cloudy\.ec/(?:v/|embed\.php\?.*?\bid=)(?P<id>[A-Za-z0-9]+)'
_TESTS = [{
'url': 'https://www.cloudy.ec/v/af511e2527aac',

View File

@ -422,6 +422,8 @@ class InfoExtractor(object):
_GEO_COUNTRIES = None
_GEO_IP_BLOCKS = None
_WORKING = True
# supply this in public subclasses: used in supported sites list, etc
# IE_DESC = 'short description of IE'
def __init__(self, downloader=None):
"""Constructor. Receives an optional downloader."""

View File

@ -35,15 +35,6 @@ from ..utils import (
class ITVBaseIE(InfoExtractor):
def _search_nextjs_data(self, webpage, video_id, **kw):
transform_source = kw.pop('transform_source', None)
fatal = kw.pop('fatal', True)
return self._parse_json(
self._search_regex(
r'''<script\b[^>]+\bid=('|")__NEXT_DATA__\1[^>]*>(?P<js>[^<]+)</script>''',
webpage, 'next.js data', group='js', fatal=fatal, **kw),
video_id, transform_source=transform_source, fatal=fatal)
def __handle_request_webpage_error(self, err, video_id=None, errnote=None, fatal=True):
if errnote is False:
return False
@ -109,7 +100,9 @@ class ITVBaseIE(InfoExtractor):
class ITVIE(ITVBaseIE):
_VALID_URL = r'https?://(?:www\.)?itv\.com/(?:(?P<w>watch)|hub)/[^/]+/(?(w)[\w-]+/)(?P<id>\w+)'
_IE_DESC = 'ITVX'
IE_DESC = 'ITVX'
_WORKING = False
_TESTS = [{
'note': 'Hub URLs redirect to ITVX',
'url': 'https://www.itv.com/hub/liar/2a4547a0012',
@ -270,7 +263,7 @@ class ITVIE(ITVBaseIE):
'ext': determine_ext(href, 'vtt'),
})
next_data = self._search_nextjs_data(webpage, video_id, fatal=False, default='{}')
next_data = self._search_nextjs_data(webpage, video_id, fatal=False, default={})
video_data.update(traverse_obj(next_data, ('props', 'pageProps', ('title', 'episode')), expected_type=dict)[0] or {})
title = traverse_obj(video_data, 'headerTitle', 'episodeTitle')
info = self._og_extract(webpage, require_title=not title)
@ -323,7 +316,7 @@ class ITVIE(ITVBaseIE):
class ITVBTCCIE(ITVBaseIE):
_VALID_URL = r'https?://(?:www\.)?itv\.com/(?!(?:watch|hub)/)(?:[^/]+/)+(?P<id>[^/?#&]+)'
_IE_DESC = 'ITV articles: News, British Touring Car Championship'
IE_DESC = 'ITV articles: News, British Touring Car Championship'
_TESTS = [{
'note': 'British Touring Car Championship',
'url': 'https://www.itv.com/btcc/articles/btcc-2018-all-the-action-from-brands-hatch',

View File

@ -47,7 +47,7 @@ class SenateISVPIE(InfoExtractor):
['vetaff', '76462', 'http://vetaff-f.akamaihd.net'],
['arch', '', 'http://ussenate-f.akamaihd.net/']
]
_IE_NAME = 'senate.gov'
IE_NAME = 'senate.gov'
_VALID_URL = r'https?://(?:www\.)?senate\.gov/isvp/?\?(?P<qs>.+)'
_TESTS = [{
'url': 'http://www.senate.gov/isvp/?comm=judiciary&type=live&stt=&filename=judiciary031715&auto_play=false&wmode=transparent&poster=http%3A%2F%2Fwww.judiciary.senate.gov%2Fthemes%2Fjudiciary%2Fimages%2Fvideo-poster-flash-fit.png',

View File

@ -686,6 +686,8 @@ class JSInterpreter(object):
raise self.Exception('Cannot get index {idx!r:.100}'.format(**locals()), expr=repr(obj), cause=e)
def _dump(self, obj, namespace):
if obj is JS_Undefined:
return 'undefined'
try:
return json.dumps(obj)
except TypeError:

View File

@ -183,6 +183,11 @@ def parseOpts(overrideArguments=None):
action='store_const', dest='extract_flat', const='in_playlist',
default=False,
help='Do not extract the videos of a playlist, only list them.')
general.add_option(
'--concat-playlist',
action='store_true', dest='concat_playlist', default=False,
help='Concatenate all videos in a playlist into a single video file. Useful for services which split up full '
'episodes into multiple segments.')
general.add_option(
'--mark-watched',
action='store_true', dest='mark_watched', default=False,

View File

@ -9,6 +9,7 @@ from .ffmpeg import (
FFmpegFixupM3u8PP,
FFmpegFixupM4aPP,
FFmpegMergerPP,
FFmpegConcatPP,
FFmpegMetadataPP,
FFmpegVideoConvertorPP,
FFmpegSubtitlesConvertorPP,
@ -31,6 +32,7 @@ __all__ = [
'FFmpegFixupM4aPP',
'FFmpegFixupStretchedPP',
'FFmpegMergerPP',
'FFmpegConcatPP',
'FFmpegMetadataPP',
'FFmpegPostProcessor',
'FFmpegSubtitlesConvertorPP',

View File

@ -84,7 +84,7 @@ class EmbedThumbnailPP(FFmpegPostProcessor):
self._downloader.to_screen('[ffmpeg] Adding thumbnail to "%s"' % filename)
self.run_ffmpeg_multiple_files([filename, thumbnail_filename], temp_filename, options)
self.run_ffmpeg_multiple_files([filename, thumbnail_filename], temp_filename, options, [])
if not self._already_have_thumbnail:
os.remove(encodeFilename(thumbnail_filename))

View File

@ -5,7 +5,6 @@ import subprocess
import time
import re
from .common import AudioConversionError, PostProcessor
from ..compat import compat_open as open
@ -24,7 +23,6 @@ from ..utils import (
replace_extension,
)
EXT_TO_OUT_FORMATS = {
'aac': 'adts',
'flac': 'flac',
@ -196,7 +194,7 @@ class FFmpegPostProcessor(PostProcessor):
return mobj.group(1)
return None
def run_ffmpeg_multiple_files(self, input_paths, out_path, opts):
def run_ffmpeg_multiple_files(self, input_paths, out_path, opts, file_opts):
self.check_version()
oldest_mtime = min(
@ -207,6 +205,7 @@ class FFmpegPostProcessor(PostProcessor):
files_cmd = []
for path in input_paths:
files_cmd.extend([
*file_opts,
encodeArgument('-i'),
encodeFilename(self._ffmpeg_filename_argument(path), True)
])
@ -231,8 +230,8 @@ class FFmpegPostProcessor(PostProcessor):
raise FFmpegPostProcessorError(msg)
self.try_utime(out_path, oldest_mtime, oldest_mtime)
def run_ffmpeg(self, path, out_path, opts):
self.run_ffmpeg_multiple_files([path], out_path, opts)
def run_ffmpeg(self, path, out_path, opts, file_opts):
self.run_ffmpeg_multiple_files([path], out_path, opts, file_opts)
def _ffmpeg_filename_argument(self, fn):
# Always use 'file:' because the filename may contain ':' (ffmpeg
@ -270,7 +269,8 @@ class FFmpegExtractAudioPP(FFmpegPostProcessor):
raise PostProcessingError('WARNING: unable to obtain file audio codec with ffprobe')
more_opts = []
if self._preferredcodec == 'best' or self._preferredcodec == filecodec or (self._preferredcodec == 'm4a' and filecodec == 'aac'):
if self._preferredcodec == 'best' or self._preferredcodec == filecodec or (
self._preferredcodec == 'm4a' and filecodec == 'aac'):
if filecodec == 'aac' and self._preferredcodec in ['m4a', 'best']:
# Lossless, but in another container
acodec = 'copy'
@ -315,7 +315,8 @@ class FFmpegExtractAudioPP(FFmpegPostProcessor):
extension = 'wav'
more_opts += ['-f', 'wav']
prefix, sep, ext = path.rpartition('.') # not os.path.splitext, since the latter does not work on unicode in all setups
prefix, sep, ext = path.rpartition(
'.') # not os.path.splitext, since the latter does not work on unicode in all setups
new_path = prefix + sep + extension
information['filepath'] = new_path
@ -353,14 +354,16 @@ class FFmpegVideoConvertorPP(FFmpegPostProcessor):
def run(self, information):
path = information['filepath']
if information['ext'] == self._preferedformat:
self._downloader.to_screen('[ffmpeg] Not converting video file %s - already is in target format %s' % (path, self._preferedformat))
self._downloader.to_screen(
'[ffmpeg] Not converting video file %s - already is in target format %s' % (path, self._preferedformat))
return [], information
options = []
if self._preferedformat == 'avi':
options.extend(['-c:v', 'libxvid', '-vtag', 'XVID'])
prefix, sep, ext = path.rpartition('.')
outpath = prefix + sep + self._preferedformat
self._downloader.to_screen('[' + 'ffmpeg' + '] Converting video from %s to %s, Destination: ' % (information['ext'], self._preferedformat) + outpath)
self._downloader.to_screen('[' + 'ffmpeg' + '] Converting video from %s to %s, Destination: ' % (
information['ext'], self._preferedformat) + outpath)
self.run_ffmpeg(path, outpath, options)
information['filepath'] = outpath
information['format'] = self._preferedformat
@ -419,7 +422,7 @@ class FFmpegEmbedSubtitlePP(FFmpegPostProcessor):
temp_filename = prepend_extension(filename, 'temp')
self._downloader.to_screen('[ffmpeg] Embedding subtitles in \'%s\'' % filename)
self.run_ffmpeg_multiple_files(input_files, temp_filename, opts)
self.run_ffmpeg_multiple_files(input_files, temp_filename, opts, [])
os.remove(encodeFilename(filename))
os.rename(encodeFilename(temp_filename), encodeFilename(filename))
@ -502,7 +505,7 @@ class FFmpegMetadataPP(FFmpegPostProcessor):
options.extend(['-map_metadata', '1'])
self._downloader.to_screen('[ffmpeg] Adding metadata to \'%s\'' % filename)
self.run_ffmpeg_multiple_files(in_filenames, temp_filename, options)
self.run_ffmpeg_multiple_files(in_filenames, temp_filename, options, [])
if chapters:
os.remove(metadata_filename)
os.remove(encodeFilename(filename))
@ -516,7 +519,7 @@ class FFmpegMergerPP(FFmpegPostProcessor):
temp_filename = prepend_extension(filename, 'temp')
args = ['-c', 'copy', '-map', '0:v:0', '-map', '1:a:0']
self._downloader.to_screen('[ffmpeg] Merging formats into "%s"' % filename)
self.run_ffmpeg_multiple_files(info['__files_to_merge'], temp_filename, args)
self.run_ffmpeg_multiple_files(info['__files_to_merge'], temp_filename, args, [])
os.rename(encodeFilename(temp_filename), encodeFilename(filename))
return info['__files_to_merge'], info
@ -538,6 +541,29 @@ class FFmpegMergerPP(FFmpegPostProcessor):
return True
class FFmpegConcatPP(FFmpegPostProcessor):
def run(self, info):
filename = info['filepath']
list_filename = prepend_extension(filename, 'list')
temp_filename = prepend_extension(filename, 'temp')
with open(list_filename, 'wt') as f:
f.write('ffconcat version 1.0\n')
for file in info['__files_to_concat']:
f.write("file '%s'\n" % self._ffmpeg_filename_argument(file))
file_opts = ['-f', 'concat', '-safe', '0']
opts = ['-c', 'copy']
self._downloader.to_screen('[ffmpeg] Concatenating files into "%s"' % filename)
self.run_ffmpeg(list_filename, temp_filename, opts, file_opts)
os.rename(encodeFilename(temp_filename), encodeFilename(filename))
return info['__files_to_concat'], info
def can_merge(self):
return True
class FFmpegFixupStretchedPP(FFmpegPostProcessor):
def run(self, info):
stretched_ratio = info.get('stretched_ratio')
@ -583,7 +609,7 @@ class FFmpegFixupM3u8PP(FFmpegPostProcessor):
options = ['-c', 'copy', '-f', 'mp4', '-bsf:a', 'aac_adtstoasc']
self._downloader.to_screen('[ffmpeg] Fixing malformed AAC bitstream in "%s"' % filename)
self.run_ffmpeg(filename, temp_filename, options)
self.run_ffmpeg(filename, temp_filename, options, [])
os.remove(encodeFilename(filename))
os.rename(encodeFilename(temp_filename), encodeFilename(filename))