mirror of
https://github.com/yt-dlp/yt-dlp.git
synced 2026-06-21 18:24:47 +00:00
Compare commits
No commits in common. "23c658b9cbe34a151f8f921ab1320bb5d4e40a4d" and "30302df22b7b431ce920e0f7298cd10be9989967" have entirely different histories.
23c658b9cb
...
30302df22b
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-14
|
runs-on: macos-13
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@ -261,8 +261,6 @@ 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,22 +62,16 @@ 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',
|
||||||
platform_name,
|
{'win32': '', 'darwin': 'macos'}.get(OS_NAME, OS_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,
|
||||||
extension,
|
OS_NAME == 'win32' and '.exe',
|
||||||
)))
|
)))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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 is not None and discontinuity_count != format_index:
|
if format_index 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 is not None and discontinuity_count != format_index:
|
if format_index and discontinuity_count != format_index:
|
||||||
continue
|
continue
|
||||||
if frag_index > 0:
|
if frag_index > 0:
|
||||||
self.report_error(
|
self.report_error(
|
||||||
|
|||||||
@ -571,6 +571,10 @@ 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 (
|
||||||
|
|||||||
215
yt_dlp/extractor/eagleplatform.py
Normal file
215
yt_dlp/extractor/eagleplatform.py
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
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,7 +9,6 @@ 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',
|
||||||
)
|
)
|
||||||
@ -26,18 +25,6 @@ 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,6 +1010,38 @@ 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,7 +3,6 @@ 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,6 +34,7 @@ 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=(',', ':'))
|
||||||
@ -63,8 +64,6 @@ 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,18 +139,7 @@ def _get_binary_name():
|
|||||||
|
|
||||||
|
|
||||||
def _get_system_deprecation():
|
def _get_system_deprecation():
|
||||||
MIN_SUPPORTED, MIN_RECOMMENDED = (3, 9), (3, 10)
|
MIN_SUPPORTED, MIN_RECOMMENDED = (3, 9), (3, 9)
|
||||||
|
|
||||||
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
|
||||||
@ -161,13 +150,6 @@ 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