mirror of
https://github.com/yt-dlp/yt-dlp.git
synced 2026-05-02 13:35:54 +00:00
Compare commits
7 Commits
30302df22b
...
23c658b9cb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23c658b9cb | ||
|
|
cc5a5caac5 | ||
|
|
66aa21dc5a | ||
|
|
57186f958f | ||
|
|
daa1859be1 | ||
|
|
e8c2bf798b | ||
|
|
1fe83b0111 |
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@ -242,7 +242,7 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
actions: write # For cleaning up cache
|
actions: write # For cleaning up cache
|
||||||
runs-on: macos-13
|
runs-on: macos-14
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@ -261,6 +261,8 @@ jobs:
|
|||||||
- name: Install Requirements
|
- name: Install Requirements
|
||||||
run: |
|
run: |
|
||||||
brew install coreutils
|
brew install coreutils
|
||||||
|
# We need to use system Python in order to roll our own universal2 curl_cffi wheel
|
||||||
|
brew uninstall --ignore-dependencies python3
|
||||||
python3 -m venv ~/yt-dlp-build-venv
|
python3 -m venv ~/yt-dlp-build-venv
|
||||||
source ~/yt-dlp-build-venv/bin/activate
|
source ~/yt-dlp-build-venv/bin/activate
|
||||||
python3 devscripts/install_deps.py -o --include build
|
python3 devscripts/install_deps.py -o --include build
|
||||||
|
|||||||
@ -62,16 +62,22 @@ def parse_options():
|
|||||||
|
|
||||||
def exe(onedir):
|
def exe(onedir):
|
||||||
"""@returns (name, path)"""
|
"""@returns (name, path)"""
|
||||||
|
platform_name, machine, extension = {
|
||||||
|
'win32': (None, MACHINE, '.exe'),
|
||||||
|
'darwin': ('macos', None, None),
|
||||||
|
}.get(OS_NAME, (OS_NAME, MACHINE, None))
|
||||||
|
|
||||||
name = '_'.join(filter(None, (
|
name = '_'.join(filter(None, (
|
||||||
'yt-dlp',
|
'yt-dlp',
|
||||||
{'win32': '', 'darwin': 'macos'}.get(OS_NAME, OS_NAME),
|
platform_name,
|
||||||
MACHINE,
|
machine,
|
||||||
)))
|
)))
|
||||||
|
|
||||||
return name, ''.join(filter(None, (
|
return name, ''.join(filter(None, (
|
||||||
'dist/',
|
'dist/',
|
||||||
onedir and f'{name}/',
|
onedir and f'{name}/',
|
||||||
name,
|
name,
|
||||||
OS_NAME == 'win32' and '.exe',
|
extension,
|
||||||
)))
|
)))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -205,7 +205,7 @@ class HlsFD(FragmentFD):
|
|||||||
line = line.strip()
|
line = line.strip()
|
||||||
if line:
|
if line:
|
||||||
if not line.startswith('#'):
|
if not line.startswith('#'):
|
||||||
if format_index and discontinuity_count != format_index:
|
if format_index is not None and discontinuity_count != format_index:
|
||||||
continue
|
continue
|
||||||
if ad_frag_next:
|
if ad_frag_next:
|
||||||
continue
|
continue
|
||||||
@ -231,7 +231,7 @@ class HlsFD(FragmentFD):
|
|||||||
byte_range = {}
|
byte_range = {}
|
||||||
|
|
||||||
elif line.startswith('#EXT-X-MAP'):
|
elif line.startswith('#EXT-X-MAP'):
|
||||||
if format_index and discontinuity_count != format_index:
|
if format_index is not None and discontinuity_count != format_index:
|
||||||
continue
|
continue
|
||||||
if frag_index > 0:
|
if frag_index > 0:
|
||||||
self.report_error(
|
self.report_error(
|
||||||
|
|||||||
@ -571,10 +571,6 @@ from .dw import (
|
|||||||
DWIE,
|
DWIE,
|
||||||
DWArticleIE,
|
DWArticleIE,
|
||||||
)
|
)
|
||||||
from .eagleplatform import (
|
|
||||||
ClipYouEmbedIE,
|
|
||||||
EaglePlatformIE,
|
|
||||||
)
|
|
||||||
from .ebaumsworld import EbaumsWorldIE
|
from .ebaumsworld import EbaumsWorldIE
|
||||||
from .ebay import EbayIE
|
from .ebay import EbayIE
|
||||||
from .egghead import (
|
from .egghead import (
|
||||||
|
|||||||
@ -1,215 +0,0 @@
|
|||||||
import functools
|
|
||||||
import re
|
|
||||||
|
|
||||||
from .common import InfoExtractor
|
|
||||||
from ..networking.exceptions import HTTPError
|
|
||||||
from ..utils import (
|
|
||||||
ExtractorError,
|
|
||||||
int_or_none,
|
|
||||||
smuggle_url,
|
|
||||||
unsmuggle_url,
|
|
||||||
url_or_none,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class EaglePlatformIE(InfoExtractor):
|
|
||||||
_VALID_URL = r'''(?x)
|
|
||||||
(?:
|
|
||||||
eagleplatform:(?P<custom_host>[^/]+):|
|
|
||||||
https?://(?P<host>.+?\.media\.eagleplatform\.com)/index/player\?.*\brecord_id=
|
|
||||||
)
|
|
||||||
(?P<id>\d+)
|
|
||||||
'''
|
|
||||||
_EMBED_REGEX = [r'<iframe[^>]+src=(["\'])(?P<url>(?:https?:)?//.+?\.media\.eagleplatform\.com/index/player\?.+?)\1']
|
|
||||||
_TESTS = [{
|
|
||||||
# http://lenta.ru/news/2015/03/06/navalny/
|
|
||||||
'url': 'http://lentaru.media.eagleplatform.com/index/player?player=new&record_id=227304&player_template_id=5201',
|
|
||||||
# Not checking MD5 as sometimes the direct HTTP link results in 404 and HLS is used
|
|
||||||
'info_dict': {
|
|
||||||
'id': '227304',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Навальный вышел на свободу',
|
|
||||||
'description': 'md5:d97861ac9ae77377f3f20eaf9d04b4f5',
|
|
||||||
'thumbnail': r're:^https?://.*\.jpg$',
|
|
||||||
'duration': 87,
|
|
||||||
'view_count': int,
|
|
||||||
'age_limit': 0,
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
# http://muz-tv.ru/play/7129/
|
|
||||||
# http://media.clipyou.ru/index/player?record_id=12820&width=730&height=415&autoplay=true
|
|
||||||
'url': 'eagleplatform:media.clipyou.ru:12820',
|
|
||||||
'md5': '358597369cf8ba56675c1df15e7af624',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '12820',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': "'O Sole Mio",
|
|
||||||
'thumbnail': r're:^https?://.*\.jpg$',
|
|
||||||
'duration': 216,
|
|
||||||
'view_count': int,
|
|
||||||
},
|
|
||||||
'skip': 'Georestricted',
|
|
||||||
}, {
|
|
||||||
# referrer protected video (https://tvrain.ru/lite/teleshow/kak_vse_nachinalos/namin-418921/)
|
|
||||||
'url': 'eagleplatform:tvrainru.media.eagleplatform.com:582306',
|
|
||||||
'only_matching': True,
|
|
||||||
}]
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _extract_embed_urls(cls, url, webpage):
|
|
||||||
add_referer = functools.partial(smuggle_url, data={'referrer': url})
|
|
||||||
|
|
||||||
res = tuple(super()._extract_embed_urls(url, webpage))
|
|
||||||
if res:
|
|
||||||
return map(add_referer, res)
|
|
||||||
|
|
||||||
PLAYER_JS_RE = r'''
|
|
||||||
<script[^>]+
|
|
||||||
src=(?P<qjs>["\'])(?:https?:)?//(?P<host>(?:(?!(?P=qjs)).)+\.media\.eagleplatform\.com)/player/player\.js(?P=qjs)
|
|
||||||
.+?
|
|
||||||
'''
|
|
||||||
# "Basic usage" embedding (see http://dultonmedia.github.io/eplayer/)
|
|
||||||
mobj = re.search(
|
|
||||||
rf'''(?xs)
|
|
||||||
{PLAYER_JS_RE}
|
|
||||||
<div[^>]+
|
|
||||||
class=(?P<qclass>["\'])eagleplayer(?P=qclass)[^>]+
|
|
||||||
data-id=["\'](?P<id>\d+)
|
|
||||||
''', webpage)
|
|
||||||
if mobj is not None:
|
|
||||||
return [add_referer('eagleplatform:{host}:{id}'.format(**mobj.groupdict()))]
|
|
||||||
# Generalization of "Javascript code usage", "Combined usage" and
|
|
||||||
# "Usage without attaching to DOM" embeddings (see
|
|
||||||
# http://dultonmedia.github.io/eplayer/)
|
|
||||||
mobj = re.search(
|
|
||||||
r'''(?xs)
|
|
||||||
%s
|
|
||||||
<script>
|
|
||||||
.+?
|
|
||||||
new\s+EaglePlayer\(
|
|
||||||
(?:[^,]+\s*,\s*)?
|
|
||||||
{
|
|
||||||
.+?
|
|
||||||
\bid\s*:\s*["\']?(?P<id>\d+)
|
|
||||||
.+?
|
|
||||||
}
|
|
||||||
\s*\)
|
|
||||||
.+?
|
|
||||||
</script>
|
|
||||||
''' % PLAYER_JS_RE, webpage) # noqa: UP031
|
|
||||||
if mobj is not None:
|
|
||||||
return [add_referer('eagleplatform:{host}:{id}'.format(**mobj.groupdict()))]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _handle_error(response):
|
|
||||||
status = int_or_none(response.get('status', 200))
|
|
||||||
if status != 200:
|
|
||||||
raise ExtractorError(' '.join(response['errors']), expected=True)
|
|
||||||
|
|
||||||
def _download_json(self, url_or_request, video_id, *args, **kwargs):
|
|
||||||
try:
|
|
||||||
response = super()._download_json(
|
|
||||||
url_or_request, video_id, *args, **kwargs)
|
|
||||||
except ExtractorError as ee:
|
|
||||||
if isinstance(ee.cause, HTTPError):
|
|
||||||
response = self._parse_json(ee.cause.response.read().decode('utf-8'), video_id)
|
|
||||||
self._handle_error(response)
|
|
||||||
raise
|
|
||||||
return response
|
|
||||||
|
|
||||||
def _get_video_url(self, url_or_request, video_id, note='Downloading JSON metadata'):
|
|
||||||
return self._download_json(url_or_request, video_id, note)['data'][0]
|
|
||||||
|
|
||||||
def _real_extract(self, url):
|
|
||||||
url, smuggled_data = unsmuggle_url(url, {})
|
|
||||||
|
|
||||||
mobj = self._match_valid_url(url)
|
|
||||||
host, video_id = mobj.group('custom_host') or mobj.group('host'), mobj.group('id')
|
|
||||||
|
|
||||||
headers = {}
|
|
||||||
query = {
|
|
||||||
'id': video_id,
|
|
||||||
}
|
|
||||||
|
|
||||||
referrer = smuggled_data.get('referrer')
|
|
||||||
if referrer:
|
|
||||||
headers['Referer'] = referrer
|
|
||||||
query['referrer'] = referrer
|
|
||||||
|
|
||||||
player_data = self._download_json(
|
|
||||||
f'http://{host}/api/player_data', video_id,
|
|
||||||
headers=headers, query=query)
|
|
||||||
|
|
||||||
media = player_data['data']['playlist']['viewports'][0]['medialist'][0]
|
|
||||||
|
|
||||||
title = media['title']
|
|
||||||
description = media.get('description')
|
|
||||||
thumbnail = self._proto_relative_url(media.get('snapshot'), 'http:')
|
|
||||||
duration = int_or_none(media.get('duration'))
|
|
||||||
view_count = int_or_none(media.get('views'))
|
|
||||||
|
|
||||||
age_restriction = media.get('age_restriction')
|
|
||||||
age_limit = None
|
|
||||||
if age_restriction:
|
|
||||||
age_limit = 0 if age_restriction == 'allow_all' else 18
|
|
||||||
|
|
||||||
secure_m3u8 = self._proto_relative_url(media['sources']['secure_m3u8']['auto'], 'http:')
|
|
||||||
|
|
||||||
formats = []
|
|
||||||
|
|
||||||
m3u8_url = self._get_video_url(secure_m3u8, video_id, 'Downloading m3u8 JSON')
|
|
||||||
m3u8_formats = self._extract_m3u8_formats(
|
|
||||||
m3u8_url, video_id, 'mp4', entry_protocol='m3u8_native',
|
|
||||||
m3u8_id='hls', fatal=False)
|
|
||||||
formats.extend(m3u8_formats)
|
|
||||||
|
|
||||||
m3u8_formats_dict = {}
|
|
||||||
for f in m3u8_formats:
|
|
||||||
if f.get('height') is not None:
|
|
||||||
m3u8_formats_dict[f['height']] = f
|
|
||||||
|
|
||||||
mp4_data = self._download_json(
|
|
||||||
# Secure mp4 URL is constructed according to Player.prototype.mp4 from
|
|
||||||
# http://lentaru.media.eagleplatform.com/player/player.js
|
|
||||||
re.sub(r'm3u8|hlsvod|hls|f4m', 'mp4s', secure_m3u8),
|
|
||||||
video_id, 'Downloading mp4 JSON', fatal=False)
|
|
||||||
if mp4_data:
|
|
||||||
for format_id, format_url in mp4_data.get('data', {}).items():
|
|
||||||
if not url_or_none(format_url):
|
|
||||||
continue
|
|
||||||
height = int_or_none(format_id)
|
|
||||||
if height is not None and m3u8_formats_dict.get(height):
|
|
||||||
f = m3u8_formats_dict[height].copy()
|
|
||||||
f.update({
|
|
||||||
'format_id': f['format_id'].replace('hls', 'http'),
|
|
||||||
'protocol': 'http',
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
f = {
|
|
||||||
'format_id': f'http-{format_id}',
|
|
||||||
'height': int_or_none(format_id),
|
|
||||||
}
|
|
||||||
f['url'] = format_url
|
|
||||||
formats.append(f)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'id': video_id,
|
|
||||||
'title': title,
|
|
||||||
'description': description,
|
|
||||||
'thumbnail': thumbnail,
|
|
||||||
'duration': duration,
|
|
||||||
'view_count': view_count,
|
|
||||||
'age_limit': age_limit,
|
|
||||||
'formats': formats,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class ClipYouEmbedIE(InfoExtractor):
|
|
||||||
_VALID_URL = False
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _extract_embed_urls(cls, url, webpage):
|
|
||||||
mobj = re.search(
|
|
||||||
r'<iframe[^>]+src="https?://(?P<host>media\.clipyou\.ru)/index/player\?.*\brecord_id=(?P<id>\d+).*"', webpage)
|
|
||||||
if mobj is not None:
|
|
||||||
yield smuggle_url('eagleplatform:{host}:{id}'.format(**mobj.groupdict()), {'referrer': url})
|
|
||||||
@ -9,6 +9,7 @@ from ..utils.traversal import traverse_obj
|
|||||||
class FaulioLiveIE(InfoExtractor):
|
class FaulioLiveIE(InfoExtractor):
|
||||||
_DOMAINS = (
|
_DOMAINS = (
|
||||||
'aloula.sba.sa',
|
'aloula.sba.sa',
|
||||||
|
'bahry.com',
|
||||||
'maraya.sba.net.ae',
|
'maraya.sba.net.ae',
|
||||||
'sat7plus.org',
|
'sat7plus.org',
|
||||||
)
|
)
|
||||||
@ -25,6 +26,18 @@ class FaulioLiveIE(InfoExtractor):
|
|||||||
'params': {
|
'params': {
|
||||||
'skip_download': 'Livestream',
|
'skip_download': 'Livestream',
|
||||||
},
|
},
|
||||||
|
}, {
|
||||||
|
'url': 'https://bahry.com/live/1',
|
||||||
|
'info_dict': {
|
||||||
|
'id': 'bahry.faulio.com_1',
|
||||||
|
'title': str,
|
||||||
|
'description': str,
|
||||||
|
'ext': 'mp4',
|
||||||
|
'live_status': 'is_live',
|
||||||
|
},
|
||||||
|
'params': {
|
||||||
|
'skip_download': 'Livestream',
|
||||||
|
},
|
||||||
}, {
|
}, {
|
||||||
'url': 'https://maraya.sba.net.ae/live/1',
|
'url': 'https://maraya.sba.net.ae/live/1',
|
||||||
'info_dict': {
|
'info_dict': {
|
||||||
|
|||||||
@ -1010,38 +1010,6 @@ class GenericIE(InfoExtractor):
|
|||||||
},
|
},
|
||||||
'add_ie': ['Kaltura'],
|
'add_ie': ['Kaltura'],
|
||||||
},
|
},
|
||||||
# referrer protected EaglePlatform embed
|
|
||||||
{
|
|
||||||
'url': 'https://tvrain.ru/lite/teleshow/kak_vse_nachinalos/namin-418921/',
|
|
||||||
'info_dict': {
|
|
||||||
'id': '582306',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': 'Стас Намин: «Мы нарушили девственность Кремля»',
|
|
||||||
'thumbnail': r're:^https?://.*\.jpg$',
|
|
||||||
'duration': 3382,
|
|
||||||
'view_count': int,
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
'skip_download': True,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
# ClipYou (EaglePlatform) embed (custom URL)
|
|
||||||
{
|
|
||||||
'url': 'http://muz-tv.ru/play/7129/',
|
|
||||||
# Not checking MD5 as sometimes the direct HTTP link results in 404 and HLS is used
|
|
||||||
'info_dict': {
|
|
||||||
'id': '12820',
|
|
||||||
'ext': 'mp4',
|
|
||||||
'title': "'O Sole Mio",
|
|
||||||
'thumbnail': r're:^https?://.*\.jpg$',
|
|
||||||
'duration': 216,
|
|
||||||
'view_count': int,
|
|
||||||
},
|
|
||||||
'params': {
|
|
||||||
'skip_download': True,
|
|
||||||
},
|
|
||||||
'skip': 'This video is unavailable.',
|
|
||||||
},
|
|
||||||
# Pladform embed
|
# Pladform embed
|
||||||
{
|
{
|
||||||
'url': 'http://muz-tv.ru/kinozal/view/7400/',
|
'url': 'http://muz-tv.ru/kinozal/view/7400/',
|
||||||
|
|||||||
@ -3,6 +3,7 @@ from ..utils import int_or_none
|
|||||||
|
|
||||||
|
|
||||||
class LiveJournalIE(InfoExtractor):
|
class LiveJournalIE(InfoExtractor):
|
||||||
|
_WORKING = False
|
||||||
_VALID_URL = r'https?://(?:[^.]+\.)?livejournal\.com/video/album/\d+.+?\bid=(?P<id>\d+)'
|
_VALID_URL = r'https?://(?:[^.]+\.)?livejournal\.com/video/album/\d+.+?\bid=(?P<id>\d+)'
|
||||||
_TEST = {
|
_TEST = {
|
||||||
'url': 'https://andrei-bt.livejournal.com/video/album/407/?mode=view&id=51272',
|
'url': 'https://andrei-bt.livejournal.com/video/album/407/?mode=view&id=51272',
|
||||||
|
|||||||
@ -34,7 +34,6 @@ class NetEaseMusicBaseIE(InfoExtractor):
|
|||||||
'sky', # SVIP tier; 沉浸环绕声 (Surround Audio); flac
|
'sky', # SVIP tier; 沉浸环绕声 (Surround Audio); flac
|
||||||
)
|
)
|
||||||
_API_BASE = 'http://music.163.com/api/'
|
_API_BASE = 'http://music.163.com/api/'
|
||||||
_GEO_BYPASS = False
|
|
||||||
|
|
||||||
def _create_eapi_cipher(self, api_path, query_body, cookies):
|
def _create_eapi_cipher(self, api_path, query_body, cookies):
|
||||||
request_text = json.dumps({**query_body, 'header': cookies}, separators=(',', ':'))
|
request_text = json.dumps({**query_body, 'header': cookies}, separators=(',', ':'))
|
||||||
@ -64,6 +63,8 @@ class NetEaseMusicBaseIE(InfoExtractor):
|
|||||||
'MUSIC_U': ('MUSIC_U', {lambda i: i.value}),
|
'MUSIC_U': ('MUSIC_U', {lambda i: i.value}),
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
if self._x_forwarded_for_ip:
|
||||||
|
headers.setdefault('X-Real-IP', self._x_forwarded_for_ip)
|
||||||
return self._download_json(
|
return self._download_json(
|
||||||
urljoin('https://interface3.music.163.com/', f'/eapi{path}'), video_id,
|
urljoin('https://interface3.music.163.com/', f'/eapi{path}'), video_id,
|
||||||
data=self._create_eapi_cipher(f'/api{path}', query_body, cookies), headers={
|
data=self._create_eapi_cipher(f'/api{path}', query_body, cookies), headers={
|
||||||
|
|||||||
@ -139,7 +139,18 @@ def _get_binary_name():
|
|||||||
|
|
||||||
|
|
||||||
def _get_system_deprecation():
|
def _get_system_deprecation():
|
||||||
MIN_SUPPORTED, MIN_RECOMMENDED = (3, 9), (3, 9)
|
MIN_SUPPORTED, MIN_RECOMMENDED = (3, 9), (3, 10)
|
||||||
|
|
||||||
|
EXE_MSG_TMPL = ('Support for {} has been deprecated. '
|
||||||
|
'See https://github.com/yt-dlp/yt-dlp/{} for details.\n{}')
|
||||||
|
STOP_MSG = 'You may stop receiving updates on this version at any time!'
|
||||||
|
variant = detect_variant()
|
||||||
|
|
||||||
|
# Temporary until macos_legacy executable builds are discontinued
|
||||||
|
if variant == 'darwin_legacy_exe':
|
||||||
|
return EXE_MSG_TMPL.format(
|
||||||
|
f'{variant} (the PyInstaller-bundled executable for macOS versions older than 10.15)',
|
||||||
|
'issues/13856', STOP_MSG)
|
||||||
|
|
||||||
if sys.version_info > MIN_RECOMMENDED:
|
if sys.version_info > MIN_RECOMMENDED:
|
||||||
return None
|
return None
|
||||||
@ -150,6 +161,13 @@ def _get_system_deprecation():
|
|||||||
if sys.version_info < MIN_SUPPORTED:
|
if sys.version_info < MIN_SUPPORTED:
|
||||||
return f'Python version {major}.{minor} is no longer supported! {PYTHON_MSG}'
|
return f'Python version {major}.{minor} is no longer supported! {PYTHON_MSG}'
|
||||||
|
|
||||||
|
# Temporary until aarch64/armv7l build flow is bumped to Ubuntu 22.04 and Python 3.10
|
||||||
|
if variant in ('linux_aarch64_exe', 'linux_armv7l_exe'):
|
||||||
|
libc_ver = version_tuple(os.confstr('CS_GNU_LIBC_VERSION').partition(' ')[2])
|
||||||
|
if libc_ver < (2, 35):
|
||||||
|
return EXE_MSG_TMPL.format('system glibc version < 2.35', 'issues/13858', STOP_MSG)
|
||||||
|
return None
|
||||||
|
|
||||||
return f'Support for Python version {major}.{minor} has been deprecated. {PYTHON_MSG}'
|
return f'Support for Python version {major}.{minor} has been deprecated. {PYTHON_MSG}'
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user