mirror of
https://github.com/yt-dlp/yt-dlp.git
synced 2024-11-28 02:01:25 +01:00
Compare commits
No commits in common. "b6951271ac014761c9c317b9cecd5e8e139cfa7c" and "85b33f5c163f60dbd089a6b9bc2ba1366d3ddf93" have entirely different histories.
b6951271ac
...
85b33f5c16
|
@ -280,7 +280,7 @@ While all the other dependencies are optional, `ffmpeg` and `ffprobe` are highly
|
||||||
|
|
||||||
* [**ffmpeg** and **ffprobe**](https://www.ffmpeg.org) - Required for [merging separate video and audio files](#format-selection) as well as for various [post-processing](#post-processing-options) tasks. License [depends on the build](https://www.ffmpeg.org/legal.html)
|
* [**ffmpeg** and **ffprobe**](https://www.ffmpeg.org) - Required for [merging separate video and audio files](#format-selection) as well as for various [post-processing](#post-processing-options) tasks. License [depends on the build](https://www.ffmpeg.org/legal.html)
|
||||||
|
|
||||||
There are bugs in ffmpeg that cause various issues when used alongside yt-dlp. Since ffmpeg is such an important dependency, we provide [custom builds](https://github.com/yt-dlp/FFmpeg-Builds#ffmpeg-static-auto-builds) with patches for some of these issues at [yt-dlp/FFmpeg-Builds](https://github.com/yt-dlp/FFmpeg-Builds). See [the readme](https://github.com/yt-dlp/FFmpeg-Builds#patches-applied) for details on the specific issues solved by these builds
|
There are bugs in ffmpeg that causes various issues when used alongside yt-dlp. Since ffmpeg is such an important dependency, we provide [custom builds](https://github.com/yt-dlp/FFmpeg-Builds#ffmpeg-static-auto-builds) with patches for some of these issues at [yt-dlp/FFmpeg-Builds](https://github.com/yt-dlp/FFmpeg-Builds). See [the readme](https://github.com/yt-dlp/FFmpeg-Builds#patches-applied) for details on the specific issues solved by these builds
|
||||||
|
|
||||||
**Important**: What you need is ffmpeg *binary*, **NOT** [the python package of the same name](https://pypi.org/project/ffmpeg)
|
**Important**: What you need is ffmpeg *binary*, **NOT** [the python package of the same name](https://pypi.org/project/ffmpeg)
|
||||||
|
|
||||||
|
|
|
@ -2340,58 +2340,6 @@ Line 1
|
||||||
self.assertEqual(traverse_obj(mobj, lambda k, _: k in (0, 'group')), ['0123', '3'],
|
self.assertEqual(traverse_obj(mobj, lambda k, _: k in (0, 'group')), ['0123', '3'],
|
||||||
msg='function on a `re.Match` should give group name as well')
|
msg='function on a `re.Match` should give group name as well')
|
||||||
|
|
||||||
# Test xml.etree.ElementTree.Element as input obj
|
|
||||||
etree = xml.etree.ElementTree.fromstring('''<?xml version="1.0"?>
|
|
||||||
<data>
|
|
||||||
<country name="Liechtenstein">
|
|
||||||
<rank>1</rank>
|
|
||||||
<year>2008</year>
|
|
||||||
<gdppc>141100</gdppc>
|
|
||||||
<neighbor name="Austria" direction="E"/>
|
|
||||||
<neighbor name="Switzerland" direction="W"/>
|
|
||||||
</country>
|
|
||||||
<country name="Singapore">
|
|
||||||
<rank>4</rank>
|
|
||||||
<year>2011</year>
|
|
||||||
<gdppc>59900</gdppc>
|
|
||||||
<neighbor name="Malaysia" direction="N"/>
|
|
||||||
</country>
|
|
||||||
<country name="Panama">
|
|
||||||
<rank>68</rank>
|
|
||||||
<year>2011</year>
|
|
||||||
<gdppc>13600</gdppc>
|
|
||||||
<neighbor name="Costa Rica" direction="W"/>
|
|
||||||
<neighbor name="Colombia" direction="E"/>
|
|
||||||
</country>
|
|
||||||
</data>''')
|
|
||||||
self.assertEqual(traverse_obj(etree, ''), etree,
|
|
||||||
msg='empty str key should return the element itself')
|
|
||||||
self.assertEqual(traverse_obj(etree, 'country'), list(etree),
|
|
||||||
msg='str key should lead all children with that tag name')
|
|
||||||
self.assertEqual(traverse_obj(etree, ...), list(etree),
|
|
||||||
msg='`...` as key should return all children')
|
|
||||||
self.assertEqual(traverse_obj(etree, lambda _, x: x[0].text == '4'), [etree[1]],
|
|
||||||
msg='function as key should get element as value')
|
|
||||||
self.assertEqual(traverse_obj(etree, lambda i, _: i == 1), [etree[1]],
|
|
||||||
msg='function as key should get index as key')
|
|
||||||
self.assertEqual(traverse_obj(etree, 0), etree[0],
|
|
||||||
msg='int key should return the nth child')
|
|
||||||
self.assertEqual(traverse_obj(etree, './/neighbor/@name'),
|
|
||||||
['Austria', 'Switzerland', 'Malaysia', 'Costa Rica', 'Colombia'],
|
|
||||||
msg='`@<attribute>` at end of path should give that attribute')
|
|
||||||
self.assertEqual(traverse_obj(etree, '//neighbor/@fail'), [None, None, None, None, None],
|
|
||||||
msg='`@<nonexistant>` at end of path should give `None`')
|
|
||||||
self.assertEqual(traverse_obj(etree, ('//neighbor/@', 2)), {'name': 'Malaysia', 'direction': 'N'},
|
|
||||||
msg='`@` should give the full attribute dict')
|
|
||||||
self.assertEqual(traverse_obj(etree, '//year/text()'), ['2008', '2011', '2011'],
|
|
||||||
msg='`text()` at end of path should give the inner text')
|
|
||||||
self.assertEqual(traverse_obj(etree, '//*[@direction]/@direction'), ['E', 'W', 'N', 'W', 'E'],
|
|
||||||
msg='full python xpath features should be supported')
|
|
||||||
self.assertEqual(traverse_obj(etree, (0, '@name')), 'Liechtenstein',
|
|
||||||
msg='special transformations should act on current element')
|
|
||||||
self.assertEqual(traverse_obj(etree, ('country', 0, ..., 'text()', {int_or_none})), [1, 2008, 141100],
|
|
||||||
msg='special transformations should act on current element')
|
|
||||||
|
|
||||||
def test_http_header_dict(self):
|
def test_http_header_dict(self):
|
||||||
headers = HTTPHeaderDict()
|
headers = HTTPHeaderDict()
|
||||||
headers['ytdl-test'] = b'0'
|
headers['ytdl-test'] = b'0'
|
||||||
|
|
|
@ -4,7 +4,6 @@ from functools import partial
|
||||||
from .common import InfoExtractor
|
from .common import InfoExtractor
|
||||||
from ..utils import (
|
from ..utils import (
|
||||||
OnDemandPagedList,
|
OnDemandPagedList,
|
||||||
bug_reports_message,
|
|
||||||
determine_ext,
|
determine_ext,
|
||||||
int_or_none,
|
int_or_none,
|
||||||
join_nonempty,
|
join_nonempty,
|
||||||
|
@ -234,7 +233,7 @@ class ARDBetaMediathekIE(InfoExtractor):
|
||||||
(?:(?:beta|www)\.)?ardmediathek\.de/
|
(?:(?:beta|www)\.)?ardmediathek\.de/
|
||||||
(?:[^/]+/)?
|
(?:[^/]+/)?
|
||||||
(?:player|live|video)/
|
(?:player|live|video)/
|
||||||
(?:[^?#]+/)?
|
(?:(?P<display_id>[^?#]+)/)?
|
||||||
(?P<id>[a-zA-Z0-9]+)
|
(?P<id>[a-zA-Z0-9]+)
|
||||||
/?(?:[?#]|$)'''
|
/?(?:[?#]|$)'''
|
||||||
_GEO_COUNTRIES = ['DE']
|
_GEO_COUNTRIES = ['DE']
|
||||||
|
@ -243,8 +242,8 @@ class ARDBetaMediathekIE(InfoExtractor):
|
||||||
'url': 'https://www.ardmediathek.de/video/filme-im-mdr/liebe-auf-vier-pfoten/mdr-fernsehen/Y3JpZDovL21kci5kZS9zZW5kdW5nLzI4MjA0MC80MjIwOTEtNDAyNTM0',
|
'url': 'https://www.ardmediathek.de/video/filme-im-mdr/liebe-auf-vier-pfoten/mdr-fernsehen/Y3JpZDovL21kci5kZS9zZW5kdW5nLzI4MjA0MC80MjIwOTEtNDAyNTM0',
|
||||||
'md5': 'b6e8ab03f2bcc6e1f9e6cef25fcc03c4',
|
'md5': 'b6e8ab03f2bcc6e1f9e6cef25fcc03c4',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'display_id': 'Y3JpZDovL21kci5kZS9zZW5kdW5nLzI4MjA0MC80MjIwOTEtNDAyNTM0',
|
'display_id': 'filme-im-mdr/liebe-auf-vier-pfoten/mdr-fernsehen',
|
||||||
'id': '12939099',
|
'id': 'Y3JpZDovL21kci5kZS9zZW5kdW5nLzI4MjA0MC80MjIwOTEtNDAyNTM0',
|
||||||
'title': 'Liebe auf vier Pfoten',
|
'title': 'Liebe auf vier Pfoten',
|
||||||
'description': r're:^Claudia Schmitt, Anwältin in Salzburg',
|
'description': r're:^Claudia Schmitt, Anwältin in Salzburg',
|
||||||
'duration': 5222,
|
'duration': 5222,
|
||||||
|
@ -256,7 +255,7 @@ class ARDBetaMediathekIE(InfoExtractor):
|
||||||
'series': 'Filme im MDR',
|
'series': 'Filme im MDR',
|
||||||
'age_limit': 0,
|
'age_limit': 0,
|
||||||
'channel': 'MDR',
|
'channel': 'MDR',
|
||||||
'_old_archive_ids': ['ardbetamediathek Y3JpZDovL21kci5kZS9zZW5kdW5nLzI4MjA0MC80MjIwOTEtNDAyNTM0'],
|
'_old_archive_ids': ['ardbetamediathek 12939099'],
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://www.ardmediathek.de/mdr/video/die-robuste-roswita/Y3JpZDovL21kci5kZS9iZWl0cmFnL2Ntcy84MWMxN2MzZC0wMjkxLTRmMzUtODk4ZS0wYzhlOWQxODE2NGI/',
|
'url': 'https://www.ardmediathek.de/mdr/video/die-robuste-roswita/Y3JpZDovL21kci5kZS9iZWl0cmFnL2Ntcy84MWMxN2MzZC0wMjkxLTRmMzUtODk4ZS0wYzhlOWQxODE2NGI/',
|
||||||
|
@ -277,37 +276,37 @@ class ARDBetaMediathekIE(InfoExtractor):
|
||||||
'url': 'https://www.ardmediathek.de/video/tagesschau-oder-tagesschau-20-00-uhr/das-erste/Y3JpZDovL2Rhc2Vyc3RlLmRlL3RhZ2Vzc2NoYXUvZmM4ZDUxMjgtOTE0ZC00Y2MzLTgzNzAtNDZkNGNiZWJkOTll',
|
'url': 'https://www.ardmediathek.de/video/tagesschau-oder-tagesschau-20-00-uhr/das-erste/Y3JpZDovL2Rhc2Vyc3RlLmRlL3RhZ2Vzc2NoYXUvZmM4ZDUxMjgtOTE0ZC00Y2MzLTgzNzAtNDZkNGNiZWJkOTll',
|
||||||
'md5': '1e73ded21cb79bac065117e80c81dc88',
|
'md5': '1e73ded21cb79bac065117e80c81dc88',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '10049223',
|
'id': 'Y3JpZDovL2Rhc2Vyc3RlLmRlL3RhZ2Vzc2NoYXUvZmM4ZDUxMjgtOTE0ZC00Y2MzLTgzNzAtNDZkNGNiZWJkOTll',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'title': 'tagesschau, 20:00 Uhr',
|
'title': 'tagesschau, 20:00 Uhr',
|
||||||
'timestamp': 1636398000,
|
'timestamp': 1636398000,
|
||||||
'description': 'md5:39578c7b96c9fe50afdf5674ad985e6b',
|
'description': 'md5:39578c7b96c9fe50afdf5674ad985e6b',
|
||||||
'upload_date': '20211108',
|
'upload_date': '20211108',
|
||||||
'display_id': 'Y3JpZDovL2Rhc2Vyc3RlLmRlL3RhZ2Vzc2NoYXUvZmM4ZDUxMjgtOTE0ZC00Y2MzLTgzNzAtNDZkNGNiZWJkOTll',
|
'display_id': 'tagesschau-oder-tagesschau-20-00-uhr/das-erste',
|
||||||
'duration': 915,
|
'duration': 915,
|
||||||
'episode': 'tagesschau, 20:00 Uhr',
|
'episode': 'tagesschau, 20:00 Uhr',
|
||||||
'series': 'tagesschau',
|
'series': 'tagesschau',
|
||||||
'thumbnail': 'https://api.ardmediathek.de/image-service/images/urn:ard:image:fbb21142783b0a49?w=960&ch=ee69108ae344f678',
|
'thumbnail': 'https://api.ardmediathek.de/image-service/images/urn:ard:image:fbb21142783b0a49?w=960&ch=ee69108ae344f678',
|
||||||
'channel': 'ARD-Aktuell',
|
'channel': 'ARD-Aktuell',
|
||||||
'_old_archive_ids': ['ardbetamediathek Y3JpZDovL2Rhc2Vyc3RlLmRlL3RhZ2Vzc2NoYXUvZmM4ZDUxMjgtOTE0ZC00Y2MzLTgzNzAtNDZkNGNiZWJkOTll'],
|
'_old_archive_ids': ['ardbetamediathek 10049223'],
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://www.ardmediathek.de/video/7-tage/7-tage-unter-harten-jungs/hr-fernsehen/N2I2YmM5MzgtNWFlOS00ZGFlLTg2NzMtYzNjM2JlNjk4MDg3',
|
'url': 'https://www.ardmediathek.de/video/7-tage/7-tage-unter-harten-jungs/hr-fernsehen/N2I2YmM5MzgtNWFlOS00ZGFlLTg2NzMtYzNjM2JlNjk4MDg3',
|
||||||
'md5': 'c428b9effff18ff624d4f903bda26315',
|
'md5': 'c428b9effff18ff624d4f903bda26315',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
'id': '94834686',
|
'id': 'N2I2YmM5MzgtNWFlOS00ZGFlLTg2NzMtYzNjM2JlNjk4MDg3',
|
||||||
'ext': 'mp4',
|
'ext': 'mp4',
|
||||||
'duration': 2700,
|
'duration': 2700,
|
||||||
'episode': '7 Tage ... unter harten Jungs',
|
'episode': '7 Tage ... unter harten Jungs',
|
||||||
'description': 'md5:0f215470dcd2b02f59f4bd10c963f072',
|
'description': 'md5:0f215470dcd2b02f59f4bd10c963f072',
|
||||||
'upload_date': '20231005',
|
'upload_date': '20231005',
|
||||||
'timestamp': 1696491171,
|
'timestamp': 1696491171,
|
||||||
'display_id': 'N2I2YmM5MzgtNWFlOS00ZGFlLTg2NzMtYzNjM2JlNjk4MDg3',
|
'display_id': '7-tage/7-tage-unter-harten-jungs/hr-fernsehen',
|
||||||
'series': '7 Tage ...',
|
'series': '7 Tage ...',
|
||||||
'channel': 'HR',
|
'channel': 'HR',
|
||||||
'thumbnail': 'https://api.ardmediathek.de/image-service/images/urn:ard:image:f6e6d5ffac41925c?w=960&ch=fa32ba69bc87989a',
|
'thumbnail': 'https://api.ardmediathek.de/image-service/images/urn:ard:image:f6e6d5ffac41925c?w=960&ch=fa32ba69bc87989a',
|
||||||
'title': '7 Tage ... unter harten Jungs',
|
'title': '7 Tage ... unter harten Jungs',
|
||||||
'_old_archive_ids': ['ardbetamediathek N2I2YmM5MzgtNWFlOS00ZGFlLTg2NzMtYzNjM2JlNjk4MDg3'],
|
'_old_archive_ids': ['ardbetamediathek 94834686'],
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://beta.ardmediathek.de/ard/video/Y3JpZDovL2Rhc2Vyc3RlLmRlL3RhdG9ydC9mYmM4NGM1NC0xNzU4LTRmZGYtYWFhZS0wYzcyZTIxNGEyMDE',
|
'url': 'https://beta.ardmediathek.de/ard/video/Y3JpZDovL2Rhc2Vyc3RlLmRlL3RhdG9ydC9mYmM4NGM1NC0xNzU4LTRmZGYtYWFhZS0wYzcyZTIxNGEyMDE',
|
||||||
|
@ -358,25 +357,14 @@ class ARDBetaMediathekIE(InfoExtractor):
|
||||||
}), get_all=False)
|
}), get_all=False)
|
||||||
|
|
||||||
def _real_extract(self, url):
|
def _real_extract(self, url):
|
||||||
display_id = self._match_id(url)
|
video_id, display_id = self._match_valid_url(url).group('id', 'display_id')
|
||||||
|
|
||||||
page_data = self._download_json(
|
page_data = self._download_json(
|
||||||
f'https://api.ardmediathek.de/page-gateway/pages/ard/item/{display_id}', display_id, query={
|
f'https://api.ardmediathek.de/page-gateway/pages/ard/item/{video_id}', video_id, query={
|
||||||
'embedded': 'false',
|
'embedded': 'false',
|
||||||
'mcV6': 'true',
|
'mcV6': 'true',
|
||||||
})
|
})
|
||||||
|
|
||||||
# For user convenience we use the old contentId instead of the longer crid
|
|
||||||
# Ref: https://github.com/yt-dlp/yt-dlp/issues/8731#issuecomment-1874398283
|
|
||||||
old_id = traverse_obj(page_data, ('tracking', 'atiCustomVars', 'contentId', {int}))
|
|
||||||
if old_id is not None:
|
|
||||||
video_id = str(old_id)
|
|
||||||
archive_ids = [make_archive_id(ARDBetaMediathekIE, display_id)]
|
|
||||||
else:
|
|
||||||
self.report_warning(f'Could not extract contentId{bug_reports_message()}')
|
|
||||||
video_id = display_id
|
|
||||||
archive_ids = None
|
|
||||||
|
|
||||||
player_data = traverse_obj(
|
player_data = traverse_obj(
|
||||||
page_data, ('widgets', lambda _, v: v['type'] in ('player_ondemand', 'player_live'), {dict}), get_all=False)
|
page_data, ('widgets', lambda _, v: v['type'] in ('player_ondemand', 'player_live'), {dict}), get_all=False)
|
||||||
is_live = player_data.get('type') == 'player_live'
|
is_live = player_data.get('type') == 'player_live'
|
||||||
|
@ -431,6 +419,8 @@ class ARDBetaMediathekIE(InfoExtractor):
|
||||||
})
|
})
|
||||||
|
|
||||||
age_limit = traverse_obj(page_data, ('fskRating', {lambda x: remove_start(x, 'FSK')}, {int_or_none}))
|
age_limit = traverse_obj(page_data, ('fskRating', {lambda x: remove_start(x, 'FSK')}, {int_or_none}))
|
||||||
|
old_id = traverse_obj(page_data, ('tracking', 'atiCustomVars', 'contentId'))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'id': video_id,
|
'id': video_id,
|
||||||
'display_id': display_id,
|
'display_id': display_id,
|
||||||
|
@ -448,7 +438,7 @@ class ARDBetaMediathekIE(InfoExtractor):
|
||||||
'channel': 'clipSourceName',
|
'channel': 'clipSourceName',
|
||||||
})),
|
})),
|
||||||
**self._extract_episode_info(page_data.get('title')),
|
**self._extract_episode_info(page_data.get('title')),
|
||||||
'_old_archive_ids': archive_ids,
|
'_old_archive_ids': [make_archive_id(ARDBetaMediathekIE, old_id)],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@ import contextlib
|
||||||
import inspect
|
import inspect
|
||||||
import itertools
|
import itertools
|
||||||
import re
|
import re
|
||||||
import xml.etree.ElementTree
|
|
||||||
|
|
||||||
from ._utils import (
|
from ._utils import (
|
||||||
IDENTITY,
|
IDENTITY,
|
||||||
|
@ -119,7 +118,7 @@ def traverse_obj(
|
||||||
branching = True
|
branching = True
|
||||||
if isinstance(obj, collections.abc.Mapping):
|
if isinstance(obj, collections.abc.Mapping):
|
||||||
result = obj.values()
|
result = obj.values()
|
||||||
elif is_iterable_like(obj) or isinstance(obj, xml.etree.ElementTree.Element):
|
elif is_iterable_like(obj):
|
||||||
result = obj
|
result = obj
|
||||||
elif isinstance(obj, re.Match):
|
elif isinstance(obj, re.Match):
|
||||||
result = obj.groups()
|
result = obj.groups()
|
||||||
|
@ -133,7 +132,7 @@ def traverse_obj(
|
||||||
branching = True
|
branching = True
|
||||||
if isinstance(obj, collections.abc.Mapping):
|
if isinstance(obj, collections.abc.Mapping):
|
||||||
iter_obj = obj.items()
|
iter_obj = obj.items()
|
||||||
elif is_iterable_like(obj) or isinstance(obj, xml.etree.ElementTree.Element):
|
elif is_iterable_like(obj):
|
||||||
iter_obj = enumerate(obj)
|
iter_obj = enumerate(obj)
|
||||||
elif isinstance(obj, re.Match):
|
elif isinstance(obj, re.Match):
|
||||||
iter_obj = itertools.chain(
|
iter_obj = itertools.chain(
|
||||||
|
@ -169,7 +168,7 @@ def traverse_obj(
|
||||||
result = next((v for k, v in obj.groupdict().items() if casefold(k) == key), None)
|
result = next((v for k, v in obj.groupdict().items() if casefold(k) == key), None)
|
||||||
|
|
||||||
elif isinstance(key, (int, slice)):
|
elif isinstance(key, (int, slice)):
|
||||||
if is_iterable_like(obj, (collections.abc.Sequence, xml.etree.ElementTree.Element)):
|
if is_iterable_like(obj, collections.abc.Sequence):
|
||||||
branching = isinstance(key, slice)
|
branching = isinstance(key, slice)
|
||||||
with contextlib.suppress(IndexError):
|
with contextlib.suppress(IndexError):
|
||||||
result = obj[key]
|
result = obj[key]
|
||||||
|
@ -177,34 +176,6 @@ def traverse_obj(
|
||||||
with contextlib.suppress(IndexError):
|
with contextlib.suppress(IndexError):
|
||||||
result = str(obj)[key]
|
result = str(obj)[key]
|
||||||
|
|
||||||
elif isinstance(obj, xml.etree.ElementTree.Element) and isinstance(key, str):
|
|
||||||
xpath, _, special = key.rpartition('/')
|
|
||||||
if not special.startswith('@') and special != 'text()':
|
|
||||||
xpath = key
|
|
||||||
special = None
|
|
||||||
|
|
||||||
# Allow abbreviations of relative paths, absolute paths error
|
|
||||||
if xpath.startswith('/'):
|
|
||||||
xpath = f'.{xpath}'
|
|
||||||
elif xpath and not xpath.startswith('./'):
|
|
||||||
xpath = f'./{xpath}'
|
|
||||||
|
|
||||||
def apply_specials(element):
|
|
||||||
if special is None:
|
|
||||||
return element
|
|
||||||
if special == '@':
|
|
||||||
return element.attrib
|
|
||||||
if special.startswith('@'):
|
|
||||||
return try_call(element.attrib.get, args=(special[1:],))
|
|
||||||
if special == 'text()':
|
|
||||||
return element.text
|
|
||||||
assert False, f'apply_specials is missing case for {special!r}'
|
|
||||||
|
|
||||||
if xpath:
|
|
||||||
result = list(map(apply_specials, obj.iterfind(xpath)))
|
|
||||||
else:
|
|
||||||
result = apply_specials(obj)
|
|
||||||
|
|
||||||
return branching, result if branching else (result,)
|
return branching, result if branching else (result,)
|
||||||
|
|
||||||
def lazy_last(iterable):
|
def lazy_last(iterable):
|
||||||
|
|
Loading…
Reference in New Issue
Block a user