Compare commits

..

5 Commits

Author SHA1 Message Date
Elan Ruusamäe
66a0127d45
[ie/duoplay] Add extractor (#8542)
Authored by: glensc
2023-11-16 22:46:29 +00:00
Raphaël Droz
3f90813f06
[ie/altcensored] Add extractor (#8291)
Authored by: drzraf
2023-11-16 22:24:12 +00:00
Ha Tien Loi
64de1a4c25
[ie/zingmp3] Add support for radio and podcasts (#7189)
Authored by: hatienl0i261299
2023-11-16 22:08:00 +00:00
sepro
f96ab86cd8
[ie/drtv] Set default ext for m3u8 formats (#8590)
Closes #8589
Authored by: seproDev
2023-11-16 20:46:13 +00:00
bashonly
f4b95acafc
Remove Python 3.7 support (#8361)
Closes #7803
Authored by: bashonly
2023-11-16 18:39:00 +00:00
17 changed files with 553 additions and 120 deletions

View File

@ -377,8 +377,8 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-python@v4 - uses: actions/setup-python@v4
with: # 3.7 is used for Vista support. See https://github.com/yt-dlp/yt-dlp/issues/390 with:
python-version: "3.7" python-version: "3.8"
architecture: "x86" architecture: "x86"
- name: Install Requirements - name: Install Requirements
run: | run: |
@ -436,7 +436,16 @@ jobs:
run: | run: |
cat >> _update_spec << EOF cat >> _update_spec << EOF
# This file is used for regulating self-update # This file is used for regulating self-update
lock 2022.08.18.36 .+ Python 3.6 lock 2022.08.18.36 .+ Python 3\.6
lock 2023.11.16 (?!win_x86_exe).+ Python 3\.7
lock 2023.11.16 win_x86_exe .+ Windows-(?:Vista|2008Server)
lockV2 yt-dlp/yt-dlp 2022.08.18.36 .+ Python 3\.6
lockV2 yt-dlp/yt-dlp 2023.11.16 (?!win_x86_exe).+ Python 3\.7
lockV2 yt-dlp/yt-dlp 2023.11.16 win_x86_exe .+ Windows-(?:Vista|2008Server)
lockV2 yt-dlp/yt-dlp-nightly-builds 2023.11.15.232826 (?!win_x86_exe).+ Python 3\.7
lockV2 yt-dlp/yt-dlp-nightly-builds 2023.11.15.232826 win_x86_exe .+ Windows-(?:Vista|2008Server)
lockV2 yt-dlp/yt-dlp-master-builds 2023.11.15.232812 (?!win_x86_exe).+ Python 3\.7
lockV2 yt-dlp/yt-dlp-master-builds 2023.11.15.232812 win_x86_exe .+ Windows-(?:Vista|2008Server)
EOF EOF
- name: Sign checksum files - name: Sign checksum files

View File

@ -13,12 +13,12 @@ jobs:
matrix: matrix:
os: [ubuntu-latest] os: [ubuntu-latest]
# CPython 3.11 is in quick-test # CPython 3.11 is in quick-test
python-version: ['3.8', '3.9', '3.10', '3.12', pypy-3.7, pypy-3.8, pypy-3.10] python-version: ['3.8', '3.9', '3.10', '3.12', pypy-3.8, pypy-3.10]
run-tests-ext: [sh] run-tests-ext: [sh]
include: include:
# atleast one of each CPython/PyPy tests must be in windows # atleast one of each CPython/PyPy tests must be in windows
- os: windows-latest - os: windows-latest
python-version: '3.7' python-version: '3.8'
run-tests-ext: bat run-tests-ext: bat
- os: windows-latest - os: windows-latest
python-version: '3.12' python-version: '3.12'
@ -32,7 +32,7 @@ jobs:
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install dependencies - name: Install test requirements
run: pip install pytest -r requirements.txt run: pip install pytest -r requirements.txt
- name: Run tests - name: Run tests
continue-on-error: False continue-on-error: False

View File

@ -15,7 +15,7 @@ jobs:
with: with:
python-version: 3.9 python-version: 3.9
- name: Install test requirements - name: Install test requirements
run: pip install pytest run: pip install pytest -r requirements.txt
- name: Run tests - name: Run tests
continue-on-error: true continue-on-error: true
run: ./devscripts/run_tests.sh download run: ./devscripts/run_tests.sh download
@ -28,7 +28,7 @@ jobs:
fail-fast: true fail-fast: true
matrix: matrix:
os: [ubuntu-latest] os: [ubuntu-latest]
python-version: ['3.7', '3.10', '3.12', pypy-3.7, pypy-3.8, pypy-3.10] python-version: ['3.10', '3.11', '3.12', pypy-3.8, pypy-3.10]
run-tests-ext: [sh] run-tests-ext: [sh]
include: include:
# atleast one of each CPython/PyPy tests must be in windows # atleast one of each CPython/PyPy tests must be in windows
@ -44,8 +44,8 @@ jobs:
uses: actions/setup-python@v4 uses: actions/setup-python@v4
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install pytest - name: Install test requirements
run: pip install pytest run: pip install pytest -r requirements.txt
- name: Run tests - name: Run tests
continue-on-error: true continue-on-error: true
run: ./devscripts/run_tests.${{ matrix.run-tests-ext }} download run: ./devscripts/run_tests.${{ matrix.run-tests-ext }} download

View File

@ -15,7 +15,7 @@ jobs:
with: with:
python-version: '3.11' python-version: '3.11'
- name: Install test requirements - name: Install test requirements
run: pip install pytest pycryptodomex run: pip install pytest -r requirements.txt
- name: Run tests - name: Run tests
run: | run: |
python3 -m yt_dlp -v || true python3 -m yt_dlp -v || true

View File

@ -222,7 +222,7 @@ After you have ensured this site is distributing its content legally, you can fo
$ flake8 yt_dlp/extractor/yourextractor.py $ flake8 yt_dlp/extractor/yourextractor.py
1. Make sure your code works under all [Python](https://www.python.org/) versions supported by yt-dlp, namely CPython and PyPy for Python 3.7 and above. Backward compatibility is not required for even older versions of Python. 1. Make sure your code works under all [Python](https://www.python.org/) versions supported by yt-dlp, namely CPython and PyPy for Python 3.8 and above. Backward compatibility is not required for even older versions of Python.
1. When the tests pass, [add](https://git-scm.com/docs/git-add) the new files, [commit](https://git-scm.com/docs/git-commit) them and [push](https://git-scm.com/docs/git-push) the result, like this: 1. When the tests pass, [add](https://git-scm.com/docs/git-add) the new files, [commit](https://git-scm.com/docs/git-commit) them and [push](https://git-scm.com/docs/git-push) the result, like this:
$ git add yt_dlp/extractor/_extractors.py $ git add yt_dlp/extractor/_extractors.py

View File

@ -131,7 +131,7 @@ Features marked with a **\*** have been back-ported to youtube-dl
Some of yt-dlp's default options are different from that of youtube-dl and youtube-dlc: Some of yt-dlp's default options are different from that of youtube-dl and youtube-dlc:
* yt-dlp supports only [Python 3.7+](## "Windows 7"), and *may* remove support for more versions as they [become EOL](https://devguide.python.org/versions/#python-release-cycle); while [youtube-dl still supports Python 2.6+ and 3.2+](https://github.com/ytdl-org/youtube-dl/issues/30568#issue-1118238743) * yt-dlp supports only [Python 3.8+](## "Windows 7"), and *may* remove support for more versions as they [become EOL](https://devguide.python.org/versions/#python-release-cycle); while [youtube-dl still supports Python 2.6+ and 3.2+](https://github.com/ytdl-org/youtube-dl/issues/30568#issue-1118238743)
* The options `--auto-number` (`-A`), `--title` (`-t`) and `--literal` (`-l`), no longer work. See [removed options](#Removed) for details * The options `--auto-number` (`-A`), `--title` (`-t`) and `--literal` (`-l`), no longer work. See [removed options](#Removed) for details
* `avconv` is not supported as an alternative to `ffmpeg` * `avconv` is not supported as an alternative to `ffmpeg`
* yt-dlp stores config files in slightly different locations to youtube-dl. See [CONFIGURATION](#configuration) for a list of correct locations * yt-dlp stores config files in slightly different locations to youtube-dl. See [CONFIGURATION](#configuration) for a list of correct locations
@ -266,7 +266,7 @@ gpg --verify SHA2-512SUMS.sig SHA2-512SUMS
**Note**: The manpages, shell completion (autocomplete) files etc. are available inside the [source tarball](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.tar.gz) **Note**: The manpages, shell completion (autocomplete) files etc. are available inside the [source tarball](https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.tar.gz)
## DEPENDENCIES ## DEPENDENCIES
Python versions 3.7+ (CPython and PyPy) are supported. Other versions and implementations may or may not work correctly. Python versions 3.8+ (CPython and PyPy) are supported. Other versions and implementations may or may not work correctly.
<!-- Python 3.5+ uses VC++14 and it is already embedded in the binary created <!-- Python 3.5+ uses VC++14 and it is already embedded in the binary created
<!x-- https://www.microsoft.com/en-us/download/details.aspx?id=26999 --x> <!x-- https://www.microsoft.com/en-us/download/details.aspx?id=26999 --x>
@ -334,7 +334,7 @@ On some systems, you may need to use `py` or `python` instead of `python3`.
**Important**: Running `pyinstaller` directly **without** using `pyinst.py` is **not** officially supported. This may or may not work correctly. **Important**: Running `pyinstaller` directly **without** using `pyinst.py` is **not** officially supported. This may or may not work correctly.
### Platform-independent Binary (UNIX) ### Platform-independent Binary (UNIX)
You will need the build tools `python` (3.7+), `zip`, `make` (GNU), `pandoc`\* and `pytest`\*. You will need the build tools `python` (3.8+), `zip`, `make` (GNU), `pandoc`\* and `pytest`\*.
After installing these, simply run `make`. After installing these, simply run `make`.

View File

@ -26,7 +26,7 @@ markers =
[tox:tox] [tox:tox]
skipsdist = true skipsdist = true
envlist = py{36,37,38,39,310,311},pypy{36,37,38,39} envlist = py{38,39,310,311,312},pypy{38,39,310}
skip_missing_interpreters = true skip_missing_interpreters = true
[testenv] # tox [testenv] # tox
@ -39,7 +39,7 @@ setenv =
[isort] [isort]
py_version = 37 py_version = 38
multi_line_output = VERTICAL_HANGING_INDENT multi_line_output = VERTICAL_HANGING_INDENT
line_length = 80 line_length = 80
reverse_relative = true reverse_relative = true

View File

@ -152,7 +152,7 @@ def main():
url='https://github.com/yt-dlp/yt-dlp', url='https://github.com/yt-dlp/yt-dlp',
packages=packages(), packages=packages(),
install_requires=REQUIREMENTS, install_requires=REQUIREMENTS,
python_requires='>=3.7', python_requires='>=3.8',
project_urls={ project_urls={
'Documentation': 'https://github.com/yt-dlp/yt-dlp#readme', 'Documentation': 'https://github.com/yt-dlp/yt-dlp#readme',
'Source': 'https://github.com/yt-dlp/yt-dlp', 'Source': 'https://github.com/yt-dlp/yt-dlp',
@ -164,11 +164,11 @@ def main():
'Development Status :: 5 - Production/Stable', 'Development Status :: 5 - Production/Stable',
'Environment :: Console', 'Environment :: Console',
'Programming Language :: Python', 'Programming Language :: Python',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11', 'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Programming Language :: Python :: Implementation', 'Programming Language :: Python :: Implementation',
'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: CPython',
'Programming Language :: Python :: Implementation :: PyPy', 'Programming Language :: Python :: Implementation :: PyPy',

View File

@ -68,25 +68,34 @@ TEST_API_DATA = {
}, },
} }
TEST_LOCKFILE_V1 = '''# This file is used for regulating self-update TEST_LOCKFILE_COMMENT = '# This file is used for regulating self-update'
lock 2022.08.18.36 .+ Python 3.6
lock 2023.11.13 .+ Python 3.7 TEST_LOCKFILE_V1 = r'''%s
lock 2022.08.18.36 .+ Python 3\.6
lock 2023.11.16 (?!win_x86_exe).+ Python 3\.7
lock 2023.11.16 win_x86_exe .+ Windows-(?:Vista|2008Server)
''' % TEST_LOCKFILE_COMMENT
TEST_LOCKFILE_V2_TMPL = r'''%s
lockV2 yt-dlp/yt-dlp 2022.08.18.36 .+ Python 3\.6
lockV2 yt-dlp/yt-dlp 2023.11.16 (?!win_x86_exe).+ Python 3\.7
lockV2 yt-dlp/yt-dlp 2023.11.16 win_x86_exe .+ Windows-(?:Vista|2008Server)
lockV2 yt-dlp/yt-dlp-nightly-builds 2023.11.15.232826 (?!win_x86_exe).+ Python 3\.7
lockV2 yt-dlp/yt-dlp-nightly-builds 2023.11.15.232826 win_x86_exe .+ Windows-(?:Vista|2008Server)
lockV2 yt-dlp/yt-dlp-master-builds 2023.11.15.232812 (?!win_x86_exe).+ Python 3\.7
lockV2 yt-dlp/yt-dlp-master-builds 2023.11.15.232812 win_x86_exe .+ Windows-(?:Vista|2008Server)
''' '''
TEST_LOCKFILE_V2 = '''# This file is used for regulating self-update TEST_LOCKFILE_V2 = TEST_LOCKFILE_V2_TMPL % TEST_LOCKFILE_COMMENT
lockV2 yt-dlp/yt-dlp 2022.08.18.36 .+ Python 3.6
lockV2 yt-dlp/yt-dlp 2023.11.13 .+ Python 3.7
'''
TEST_LOCKFILE_V1_V2 = '''# This file is used for regulating self-update TEST_LOCKFILE_ACTUAL = TEST_LOCKFILE_V2_TMPL % TEST_LOCKFILE_V1.rstrip('\n')
lock 2022.08.18.36 .+ Python 3.6
lock 2023.11.13 .+ Python 3.7 TEST_LOCKFILE_FORK = r'''%s# Test if a fork blocks updates to non-numeric tags
lockV2 yt-dlp/yt-dlp 2022.08.18.36 .+ Python 3.6
lockV2 yt-dlp/yt-dlp 2023.11.13 .+ Python 3.7
lockV2 fork/yt-dlp pr0000 .+ Python 3.6 lockV2 fork/yt-dlp pr0000 .+ Python 3.6
lockV2 fork/yt-dlp pr1234 .+ Python 3.7 lockV2 fork/yt-dlp pr1234 (?!win_x86_exe).+ Python 3\.7
lockV2 fork/yt-dlp pr1234 win_x86_exe .+ Windows-(?:Vista|2008Server)
lockV2 fork/yt-dlp pr9999 .+ Python 3.11 lockV2 fork/yt-dlp pr9999 .+ Python 3.11
''' ''' % TEST_LOCKFILE_ACTUAL
class FakeUpdater(Updater): class FakeUpdater(Updater):
@ -97,7 +106,7 @@ class FakeUpdater(Updater):
_origin = 'yt-dlp/yt-dlp' _origin = 'yt-dlp/yt-dlp'
def _download_update_spec(self, *args, **kwargs): def _download_update_spec(self, *args, **kwargs):
return TEST_LOCKFILE_V1_V2 return TEST_LOCKFILE_ACTUAL
def _call_api(self, tag): def _call_api(self, tag):
tag = f'tags/{tag}' if tag != 'latest' else tag tag = f'tags/{tag}' if tag != 'latest' else tag
@ -112,7 +121,7 @@ class TestUpdate(unittest.TestCase):
def test_update_spec(self): def test_update_spec(self):
ydl = FakeYDL() ydl = FakeYDL()
updater = FakeUpdater(ydl, 'stable@latest') updater = FakeUpdater(ydl, 'stable')
def test(lockfile, identifier, input_tag, expect_tag, exact=False, repo='yt-dlp/yt-dlp'): def test(lockfile, identifier, input_tag, expect_tag, exact=False, repo='yt-dlp/yt-dlp'):
updater._identifier = identifier updater._identifier = identifier
@ -124,35 +133,46 @@ class TestUpdate(unittest.TestCase):
f'{identifier!r} requesting {repo}@{input_tag} (exact={exact}) ' f'{identifier!r} requesting {repo}@{input_tag} (exact={exact}) '
f'returned {result!r} instead of {expect_tag!r}') f'returned {result!r} instead of {expect_tag!r}')
test(TEST_LOCKFILE_V1, 'zip Python 3.11.0', '2023.11.13', '2023.11.13') for lockfile in (TEST_LOCKFILE_V1, TEST_LOCKFILE_V2, TEST_LOCKFILE_ACTUAL, TEST_LOCKFILE_FORK):
test(TEST_LOCKFILE_V1, 'zip stable Python 3.11.0', '2023.11.13', '2023.11.13', exact=True) # Normal operation
test(TEST_LOCKFILE_V1, 'zip Python 3.6.0', '2023.11.13', '2022.08.18.36') test(lockfile, 'zip Python 3.12.0', '2023.12.31', '2023.12.31')
test(TEST_LOCKFILE_V1, 'zip stable Python 3.6.0', '2023.11.13', None, exact=True) test(lockfile, 'zip stable Python 3.12.0', '2023.12.31', '2023.12.31', exact=True)
test(TEST_LOCKFILE_V1, 'zip Python 3.7.0', '2023.11.13', '2023.11.13') # Python 3.6 --update should update only to its lock
test(TEST_LOCKFILE_V1, 'zip stable Python 3.7.1', '2023.11.13', '2023.11.13') test(lockfile, 'zip Python 3.6.0', '2023.11.16', '2022.08.18.36')
test(TEST_LOCKFILE_V1, 'zip Python 3.7.1', '2023.12.31', '2023.11.13') # --update-to an exact version later than the lock should return None
test(TEST_LOCKFILE_V1, 'zip stable Python 3.7.1', '2023.12.31', '2023.11.13') test(lockfile, 'zip stable Python 3.6.0', '2023.11.16', None, exact=True)
# Python 3.7 should be able to update to its lock
test(lockfile, 'zip Python 3.7.0', '2023.11.16', '2023.11.16')
test(lockfile, 'zip stable Python 3.7.1', '2023.11.16', '2023.11.16', exact=True)
# Non-win_x86_exe builds on py3.7 must be locked
test(lockfile, 'zip Python 3.7.1', '2023.12.31', '2023.11.16')
test(lockfile, 'zip stable Python 3.7.1', '2023.12.31', None, exact=True)
test( # Windows Vista w/ win_x86_exe must be locked
lockfile, 'win_x86_exe stable Python 3.7.9 (CPython x86 32bit) - Windows-Vista-6.0.6003-SP2',
'2023.12.31', '2023.11.16')
test( # Windows 2008Server w/ win_x86_exe must be locked
lockfile, 'win_x86_exe Python 3.7.9 (CPython x86 32bit) - Windows-2008Server',
'2023.12.31', None, exact=True)
test( # Windows 7 w/ win_x86_exe py3.7 build should be able to update beyond lock
lockfile, 'win_x86_exe stable Python 3.7.9 (CPython x86 32bit) - Windows-7-6.1.7601-SP1',
'2023.12.31', '2023.12.31')
test( # Windows 8.1 w/ '2008Server' in platform string should be able to update beyond lock
lockfile, 'win_x86_exe Python 3.7.9 (CPython x86 32bit) - Windows-post2008Server-6.2.9200',
'2023.12.31', '2023.12.31', exact=True)
test(TEST_LOCKFILE_V2, 'zip Python 3.11.1', '2023.11.13', '2023.11.13') # Forks can block updates to non-numeric tags rather than lock
test(TEST_LOCKFILE_V2, 'zip stable Python 3.11.1', '2023.12.31', '2023.12.31') test(TEST_LOCKFILE_FORK, 'zip Python 3.6.3', 'pr0000', None, repo='fork/yt-dlp')
test(TEST_LOCKFILE_V2, 'zip Python 3.6.1', '2023.11.13', '2022.08.18.36') test(TEST_LOCKFILE_FORK, 'zip stable Python 3.7.4', 'pr0000', 'pr0000', repo='fork/yt-dlp')
test(TEST_LOCKFILE_V2, 'zip stable Python 3.7.2', '2023.11.13', '2023.11.13') test(TEST_LOCKFILE_FORK, 'zip stable Python 3.7.4', 'pr1234', None, repo='fork/yt-dlp')
test(TEST_LOCKFILE_V2, 'zip Python 3.7.2', '2023.12.31', '2023.11.13') test(TEST_LOCKFILE_FORK, 'zip Python 3.8.1', 'pr1234', 'pr1234', repo='fork/yt-dlp', exact=True)
test(
test(TEST_LOCKFILE_V1_V2, 'zip Python 3.11.2', '2023.11.13', '2023.11.13') TEST_LOCKFILE_FORK, 'win_x86_exe stable Python 3.7.9 (CPython x86 32bit) - Windows-Vista-6.0.6003-SP2',
test(TEST_LOCKFILE_V1_V2, 'zip stable Python 3.11.2', '2023.12.31', '2023.12.31') 'pr1234', None, repo='fork/yt-dlp')
test(TEST_LOCKFILE_V1_V2, 'zip Python 3.6.2', '2023.11.13', '2022.08.18.36') test(
test(TEST_LOCKFILE_V1_V2, 'zip stable Python 3.7.3', '2023.11.13', '2023.11.13') TEST_LOCKFILE_FORK, 'win_x86_exe stable Python 3.7.9 (CPython x86 32bit) - Windows-7-6.1.7601-SP1',
test(TEST_LOCKFILE_V1_V2, 'zip Python 3.7.3', '2023.12.31', '2023.11.13') '2023.12.31', '2023.12.31', repo='fork/yt-dlp')
test(TEST_LOCKFILE_V1_V2, 'zip Python 3.6.3', 'pr0000', None, repo='fork/yt-dlp') test(TEST_LOCKFILE_FORK, 'zip Python 3.11.2', 'pr9999', None, repo='fork/yt-dlp', exact=True)
test(TEST_LOCKFILE_V1_V2, 'zip stable Python 3.7.4', 'pr0000', 'pr0000', repo='fork/yt-dlp') test(TEST_LOCKFILE_FORK, 'zip stable Python 3.12.0', 'pr9999', 'pr9999', repo='fork/yt-dlp')
test(TEST_LOCKFILE_V1_V2, 'zip Python 3.6.4', 'pr0000', None, repo='fork/yt-dlp')
test(TEST_LOCKFILE_V1_V2, 'zip Python 3.7.4', 'pr1234', None, repo='fork/yt-dlp')
test(TEST_LOCKFILE_V1_V2, 'zip stable Python 3.8.1', 'pr1234', 'pr1234', repo='fork/yt-dlp')
test(TEST_LOCKFILE_V1_V2, 'zip Python 3.7.5', 'pr1234', None, repo='fork/yt-dlp')
test(TEST_LOCKFILE_V1_V2, 'zip Python 3.11.3', 'pr9999', None, repo='fork/yt-dlp')
test(TEST_LOCKFILE_V1_V2, 'zip stable Python 3.12.0', 'pr9999', 'pr9999', repo='fork/yt-dlp')
test(TEST_LOCKFILE_V1_V2, 'zip Python 3.11.4', 'pr9999', None, repo='fork/yt-dlp')
def test_query_update(self): def test_query_update(self):
ydl = FakeYDL() ydl = FakeYDL()

View File

@ -1,8 +1,8 @@
try: import sys
import contextvars # noqa: F401
except Exception: if sys.version_info < (3, 8):
raise Exception( raise ImportError(
f'You are using an unsupported version of Python. Only Python versions 3.7 and above are supported by yt-dlp') # noqa: F541 f'You are using an unsupported version of Python. Only Python versions 3.8 and above are supported by yt-dlp') # noqa: F541
__license__ = 'Public Domain' __license__ = 'Public Domain'
@ -12,7 +12,6 @@ import itertools
import optparse import optparse
import os import os
import re import re
import sys
import traceback import traceback
from .compat import compat_shlex_quote from .compat import compat_shlex_quote

View File

@ -10,17 +10,3 @@ try:
cache # >= 3.9 cache # >= 3.9
except NameError: except NameError:
cache = lru_cache(maxsize=None) cache = lru_cache(maxsize=None)
try:
cached_property # >= 3.8
except NameError:
class cached_property:
def __init__(self, func):
update_wrapper(self, func)
self.func = func
def __get__(self, instance, _):
if instance is None:
return self
setattr(instance, self.func.__name__, self.func(instance))
return getattr(instance, self.func.__name__)

View File

@ -82,6 +82,10 @@ from .airtv import AirTVIE
from .aitube import AitubeKZVideoIE from .aitube import AitubeKZVideoIE
from .aljazeera import AlJazeeraIE from .aljazeera import AlJazeeraIE
from .alphaporno import AlphaPornoIE from .alphaporno import AlphaPornoIE
from .altcensored import (
AltCensoredIE,
AltCensoredChannelIE,
)
from .amara import AmaraIE from .amara import AmaraIE
from .alura import ( from .alura import (
AluraIE, AluraIE,
@ -541,6 +545,7 @@ from .dropout import (
DropoutSeasonIE, DropoutSeasonIE,
DropoutIE DropoutIE
) )
from .duoplay import DuoplayIE
from .dw import ( from .dw import (
DWIE, DWIE,
DWArticleIE, DWArticleIE,
@ -2588,6 +2593,9 @@ from .zingmp3 import (
ZingMp3ChartMusicVideoIE, ZingMp3ChartMusicVideoIE,
ZingMp3UserIE, ZingMp3UserIE,
ZingMp3HubIE, ZingMp3HubIE,
ZingMp3LiveRadioIE,
ZingMp3PodcastEpisodeIE,
ZingMp3PodcastIE,
) )
from .zoom import ZoomIE from .zoom import ZoomIE
from .zype import ZypeIE from .zype import ZypeIE

View File

@ -0,0 +1,96 @@
import re
from .archiveorg import ArchiveOrgIE
from .common import InfoExtractor
from ..utils import (
InAdvancePagedList,
int_or_none,
orderedSet,
str_to_int,
urljoin,
)
class AltCensoredIE(InfoExtractor):
IE_NAME = 'altcensored'
_VALID_URL = r'https?://(?:www\.)?altcensored\.com/(?:watch\?v=|embed/)(?P<id>[^/?#]+)'
_TESTS = [{
'url': 'https://www.altcensored.com/watch?v=k0srjLSkga8',
'info_dict': {
'id': 'youtube-k0srjLSkga8',
'ext': 'webm',
'title': "QUELLES SONT LES CONSÉQUENCES DE L'HYPERSEXUALISATION DE LA SOCIÉTÉ ?",
'display_id': 'k0srjLSkga8.webm',
'release_date': '20180403',
'creator': 'Virginie Vota',
'release_year': 2018,
'upload_date': '20230318',
'uploader': 'admin@altcensored.com',
'description': 'md5:0b38a8fc04103579d5c1db10a247dc30',
'timestamp': 1679161343,
'track': 'k0srjLSkga8',
'duration': 926.09,
'thumbnail': 'https://archive.org/download/youtube-k0srjLSkga8/youtube-k0srjLSkga8.thumbs/k0srjLSkga8_000925.jpg',
'view_count': int,
'categories': ['News & Politics'],
}
}]
def _real_extract(self, url):
video_id = self._match_id(url)
webpage = self._download_webpage(url, video_id)
return {
'_type': 'url_transparent',
'url': f'https://archive.org/details/youtube-{video_id}',
'ie_key': ArchiveOrgIE.ie_key(),
'view_count': str_to_int(self._html_search_regex(
r'YouTube Views:(?:\s|&nbsp;)*([\d,]+)', webpage, 'view count', default=None)),
'categories': self._html_search_regex(
r'<a href="/category/\d+">\s*\n?\s*([^<]+)</a>',
webpage, 'category', default='').split() or None,
}
class AltCensoredChannelIE(InfoExtractor):
IE_NAME = 'altcensored:channel'
_VALID_URL = r'https?://(?:www\.)?altcensored\.com/channel/(?!page|table)(?P<id>[^/?#]+)'
_PAGE_SIZE = 24
_TESTS = [{
'url': 'https://www.altcensored.com/channel/UCFPTO55xxHqFqkzRZHu4kcw',
'info_dict': {
'title': 'Virginie Vota',
'id': 'UCFPTO55xxHqFqkzRZHu4kcw',
},
'playlist_count': 91
}, {
'url': 'https://altcensored.com/channel/UC9CcJ96HKMWn0LZlcxlpFTw',
'info_dict': {
'title': 'yukikaze775',
'id': 'UC9CcJ96HKMWn0LZlcxlpFTw',
},
'playlist_count': 4
}]
def _real_extract(self, url):
channel_id = self._match_id(url)
webpage = self._download_webpage(
url, channel_id, 'Download channel webpage', 'Unable to get channel webpage')
title = self._html_search_meta('altcen_title', webpage, 'title', fatal=False)
page_count = int_or_none(self._html_search_regex(
r'<a[^>]+href="/channel/\w+/page/(\d+)">(?:\1)</a>',
webpage, 'page count', default='1'))
def page_func(page_num):
page_num += 1
webpage = self._download_webpage(
f'https://altcensored.com/channel/{channel_id}/page/{page_num}',
channel_id, note=f'Downloading page {page_num}')
items = re.findall(r'<a[^>]+href="(/watch\?v=[^"]+)', webpage)
return [self.url_result(urljoin('https://www.altcensored.com', path), AltCensoredIE)
for path in orderedSet(items)]
return self.playlist_result(
InAdvancePagedList(page_func, page_count, self._PAGE_SIZE),
playlist_id=channel_id, playlist_title=title)

View File

@ -209,7 +209,7 @@ class DRTVIE(InfoExtractor):
elif access_service == 'StandardVideo': elif access_service == 'StandardVideo':
preference = 1 preference = 1
fmts, subs = self._extract_m3u8_formats_and_subtitles( fmts, subs = self._extract_m3u8_formats_and_subtitles(
stream.get('url'), video_id, preference=preference, m3u8_id=format_id, fatal=False) stream.get('url'), video_id, ext='mp4', preference=preference, m3u8_id=format_id, fatal=False)
formats.extend(fmts) formats.extend(fmts)
api_subtitles = traverse_obj(stream, ('subtitles', lambda _, v: url_or_none(v['link']), {dict})) api_subtitles = traverse_obj(stream, ('subtitles', lambda _, v: url_or_none(v['link']), {dict}))

119
yt_dlp/extractor/duoplay.py Normal file
View File

@ -0,0 +1,119 @@
from .common import InfoExtractor
from ..utils import (
ExtractorError,
extract_attributes,
get_element_text_and_html_by_tag,
int_or_none,
join_nonempty,
str_or_none,
try_call,
unified_timestamp,
)
from ..utils.traversal import traverse_obj
class DuoplayIE(InfoExtractor):
_VALID_URL = r'https://duoplay\.ee/(?P<id>\d+)/[\w-]+/?(?:\?(?:[^#]+&)?ep=(?P<ep>\d+))?'
_TESTS = [{
'note': 'Siberi võmm S02E12',
'url': 'https://duoplay.ee/4312/siberi-vomm?ep=24',
'md5': '1ff59d535310ac9c5cf5f287d8f91b2d',
'info_dict': {
'id': '4312_24',
'ext': 'mp4',
'title': 'Operatsioon "Öö"',
'thumbnail': r're:https://.+\.jpg(?:\?c=\d+)?$',
'description': 'md5:8ef98f38569d6b8b78f3d350ccc6ade8',
'upload_date': '20170523',
'timestamp': 1495567800,
'series': 'Siberi võmm',
'series_id': '4312',
'season': 'Season 2',
'season_number': 2,
'episode': 'Operatsioon "Öö"',
'episode_number': 12,
'episode_id': 24,
},
}, {
'note': 'Empty title',
'url': 'https://duoplay.ee/17/uhikarotid?ep=14',
'md5': '6aca68be71112314738dd17cced7f8bf',
'info_dict': {
'id': '17_14',
'ext': 'mp4',
'title': 'Ühikarotid',
'thumbnail': r're:https://.+\.jpg(?:\?c=\d+)?$',
'description': 'md5:4719b418e058c209def41d48b601276e',
'upload_date': '20100916',
'timestamp': 1284661800,
'series': 'Ühikarotid',
'series_id': '17',
'season': 'Season 2',
'season_number': 2,
'episode_id': 14,
'release_year': 2010,
},
}, {
'note': 'Movie',
'url': 'https://duoplay.ee/4325/naljamangud',
'md5': '2b0bcac4159a08b1844c2bfde06b1199',
'info_dict': {
'id': '4325',
'ext': 'mp4',
'title': 'Näljamängud',
'thumbnail': r're:https://.+\.jpg(?:\?c=\d+)?$',
'description': 'md5:fb35f5eb2ff46cdb82e4d5fbe7b49a13',
'cast': ['Jennifer Lawrence', 'Josh Hutcherson', 'Liam Hemsworth'],
'upload_date': '20231109',
'timestamp': 1699552800,
'release_year': 2012,
},
}, {
'note': 'Movie without expiry',
'url': 'https://duoplay.ee/5501/pilvede-all.-neljas-ode',
'md5': '7abf63d773a49ef7c39f2c127842b8fd',
'info_dict': {
'id': '5501',
'ext': 'mp4',
'title': 'Pilvede all. Neljas õde',
'thumbnail': r're:https://.+\.jpg(?:\?c=\d+)?$',
'description': 'md5:d86a70f8f31e82c369d4d4f4c79b1279',
'cast': 'count:9',
'upload_date': '20221214',
'timestamp': 1671054000,
'release_year': 2018,
},
}]
def _real_extract(self, url):
telecast_id, episode = self._match_valid_url(url).group('id', 'ep')
video_id = join_nonempty(telecast_id, episode, delim='_')
webpage = self._download_webpage(url, video_id)
video_player = try_call(lambda: extract_attributes(
get_element_text_and_html_by_tag('video-player', webpage)[1]))
if not video_player or not video_player.get('manifest-url'):
raise ExtractorError('No video found', expected=True)
episode_attr = self._parse_json(video_player.get(':episode') or '', video_id, fatal=False) or {}
return {
'id': video_id,
'formats': self._extract_m3u8_formats(video_player['manifest-url'], video_id, 'mp4'),
**traverse_obj(episode_attr, {
'title': 'title',
'description': 'synopsis',
'thumbnail': ('images', 'original'),
'timestamp': ('airtime', {lambda x: unified_timestamp(x + ' +0200')}),
'cast': ('cast', {lambda x: x.split(', ')}),
'release_year': ('year', {int_or_none}),
}),
**(traverse_obj(episode_attr, {
'title': (None, ('subtitle', ('episode_nr', {lambda x: f'Episode {x}' if x else None}))),
'series': 'title',
'series_id': ('telecast_id', {str_or_none}),
'season_number': ('season_id', {int_or_none}),
'episode': 'subtitle',
'episode_number': ('episode_nr', {int_or_none}),
'episode_id': ('episode_id', {int_or_none}),
}, get_all=False) if episode_attr.get('category') != 'movies' else {}),
}

View File

@ -5,7 +5,15 @@ import json
import urllib.parse import urllib.parse
from .common import InfoExtractor from .common import InfoExtractor
from ..utils import int_or_none, traverse_obj, try_call, urljoin from ..utils import (
ExtractorError,
int_or_none,
join_nonempty,
try_call,
urljoin,
url_or_none
)
from ..utils.traversal import traverse_obj
class ZingMp3BaseIE(InfoExtractor): class ZingMp3BaseIE(InfoExtractor):
@ -20,9 +28,17 @@ class ZingMp3BaseIE(InfoExtractor):
'video-clip': '/api/v2/page/get/video', 'video-clip': '/api/v2/page/get/video',
'lyric': '/api/v2/lyric/get/lyric', 'lyric': '/api/v2/lyric/get/lyric',
'song-streaming': '/api/v2/song/get/streaming', 'song-streaming': '/api/v2/song/get/streaming',
'liveradio': '/api/v2/livestream/get/info',
'eps': '/api/v2/page/get/podcast-episode',
'episode-streaming': '/api/v2/podcast/episode/get/streaming',
# Playlist # Playlist
'playlist': '/api/v2/page/get/playlist', 'playlist': '/api/v2/page/get/playlist',
'album': '/api/v2/page/get/playlist', 'album': '/api/v2/page/get/playlist',
'pgr': '/api/v2/page/get/podcast-program',
'pgr-list': '/api/v2/podcast/episode/get/list',
'cgr': '/api/v2/page/get/podcast-category',
'cgr-list': '/api/v2/podcast/program/get/list-by-cate',
'cgrs': '/api/v2/page/get/podcast-categories',
# Chart # Chart
'zing-chart': '/api/v2/page/get/chart-home', 'zing-chart': '/api/v2/page/get/chart-home',
'zing-chart-tuan': '/api/v2/page/get/week-chart', 'zing-chart-tuan': '/api/v2/page/get/week-chart',
@ -33,6 +49,10 @@ class ZingMp3BaseIE(InfoExtractor):
'user-list-song': '/api/v2/song/get/list', 'user-list-song': '/api/v2/song/get/list',
'user-list-video': '/api/v2/video/get/list', 'user-list-video': '/api/v2/video/get/list',
'hub': '/api/v2/page/get/hub-detail', 'hub': '/api/v2/page/get/hub-detail',
'new-release': '/api/v2/chart/get/new-release',
'top100': '/api/v2/page/get/top-100',
'podcast-new': '/api/v2/podcast/program/get/list-by-type',
'top-podcast': '/api/v2/podcast/program/get/top-episode',
} }
def _api_url(self, url_type, params): def _api_url(self, url_type, params):
@ -78,7 +98,7 @@ class ZingMp3BaseIE(InfoExtractor):
class ZingMp3IE(ZingMp3BaseIE): class ZingMp3IE(ZingMp3BaseIE):
_VALID_URL = ZingMp3BaseIE._VALID_URL_TMPL % 'bai-hat|video-clip|embed' _VALID_URL = ZingMp3BaseIE._VALID_URL_TMPL % 'bai-hat|video-clip|embed|eps'
IE_NAME = 'zingmp3' IE_NAME = 'zingmp3'
IE_DESC = 'zingmp3.vn' IE_DESC = 'zingmp3.vn'
_TESTS = [{ _TESTS = [{
@ -102,7 +122,7 @@ class ZingMp3IE(ZingMp3BaseIE):
}, },
}, { }, {
'url': 'https://zingmp3.vn/video-clip/Suong-Hoa-Dua-Loi-K-ICM-RYO/ZO8ZF7C7.html', 'url': 'https://zingmp3.vn/video-clip/Suong-Hoa-Dua-Loi-K-ICM-RYO/ZO8ZF7C7.html',
'md5': '3c2081e79471a2f4a3edd90b70b185ea', 'md5': '92c6e7a019f06b4682a6c35ae5785fab',
'info_dict': { 'info_dict': {
'id': 'ZO8ZF7C7', 'id': 'ZO8ZF7C7',
'title': 'Sương Hoa Đưa Lối', 'title': 'Sương Hoa Đưa Lối',
@ -128,6 +148,20 @@ class ZingMp3IE(ZingMp3BaseIE):
'album': 'Người Yêu Tôi Lạnh Lùng Sắt Đá (Single)', 'album': 'Người Yêu Tôi Lạnh Lùng Sắt Đá (Single)',
'album_artist': 'Mr. Siro', 'album_artist': 'Mr. Siro',
}, },
}, {
'url': 'https://zingmp3.vn/eps/Cham-x-Ban-Noi-Goi-La-Nha/ZZD9ACWI.html',
'md5': 'd52f9f63e2631e004e4f15188eedcf80',
'info_dict': {
'id': 'ZZD9ACWI',
'title': 'Chạm x Bạn - Nơi Gọi Là Nhà',
'ext': 'mp3',
'duration': 3716,
'thumbnail': r're:^https?://.+\.jpg',
'track': 'Chạm x Bạn - Nơi Gọi Là Nhà',
'artist': 'On Air',
'album': 'Top Podcast',
'album_artist': 'On Air',
},
}, { }, {
'url': 'https://zingmp3.vn/embed/song/ZWZEI76B?start=false', 'url': 'https://zingmp3.vn/embed/song/ZWZEI76B?start=false',
'only_matching': True, 'only_matching': True,
@ -147,6 +181,8 @@ class ZingMp3IE(ZingMp3BaseIE):
'http://api.mp3.zing.vn/api/mobile/video/getvideoinfo', item_id, 'http://api.mp3.zing.vn/api/mobile/video/getvideoinfo', item_id,
query={'requestdata': json.dumps({'id': item_id})}, query={'requestdata': json.dumps({'id': item_id})},
note='Downloading mp4 JSON metadata').get('source') note='Downloading mp4 JSON metadata').get('source')
elif url_type == 'eps':
source = self._call_api('episode-streaming', {'id': item_id})
else: else:
source = self._call_api('song-streaming', {'id': item_id}) source = self._call_api('song-streaming', {'id': item_id})
@ -189,9 +225,10 @@ class ZingMp3IE(ZingMp3BaseIE):
'thumbnail': traverse_obj(item, 'thumbnail', 'thumbnailM'), 'thumbnail': traverse_obj(item, 'thumbnail', 'thumbnailM'),
'duration': int_or_none(item.get('duration')), 'duration': int_or_none(item.get('duration')),
'track': traverse_obj(item, 'title', 'alias'), 'track': traverse_obj(item, 'title', 'alias'),
'artist': traverse_obj(item, 'artistsNames', 'artists_names'), 'artist': traverse_obj(item, 'artistsNames', 'artists_names', ('artists', 0, 'name')),
'album': traverse_obj(item, ('album', ('name', 'title')), get_all=False), 'album': traverse_obj(item, ('album', ('name', 'title')), ('genres', 0, 'name'), get_all=False),
'album_artist': traverse_obj(item, ('album', ('artistsNames', 'artists_names')), get_all=False), 'album_artist': traverse_obj(item, ('album', ('artistsNames', 'artists_names')),
('artists', 0, 'name'), get_all=False),
'formats': formats, 'formats': formats,
'subtitles': {'origin': [{'url': lyric}]} if lyric else None, 'subtitles': {'origin': [{'url': lyric}]} if lyric else None,
} }
@ -200,12 +237,12 @@ class ZingMp3IE(ZingMp3BaseIE):
class ZingMp3AlbumIE(ZingMp3BaseIE): class ZingMp3AlbumIE(ZingMp3BaseIE):
_VALID_URL = ZingMp3BaseIE._VALID_URL_TMPL % 'album|playlist' _VALID_URL = ZingMp3BaseIE._VALID_URL_TMPL % 'album|playlist'
_TESTS = [{ _TESTS = [{
'url': 'http://mp3.zing.vn/album/Lau-Dai-Tinh-Ai-Bang-Kieu-Minh-Tuyet/ZWZBWDAF.html', 'url': 'https://zingmp3.vn/album/Ca-Phe-Quan-Quen-Hoang-Dung-My-Anh-Da-LAB-Thinh-Suy/ZOC7WUZC.html',
'info_dict': { 'info_dict': {
'id': 'ZWZBWDAF', 'id': 'ZOC7WUZC',
'title': 'Lâu Đài Tình Ái', 'title': 'Cà Phê Quán Quen',
}, },
'playlist_mincount': 9, 'playlist_mincount': 10,
}, { }, {
'url': 'https://zingmp3.vn/album/Nhung-Bai-Hat-Hay-Nhat-Cua-Mr-Siro-Mr-Siro/ZWZAEZZD.html', 'url': 'https://zingmp3.vn/album/Nhung-Bai-Hat-Hay-Nhat-Cua-Mr-Siro-Mr-Siro/ZWZAEZZD.html',
'info_dict': { 'info_dict': {
@ -231,7 +268,7 @@ class ZingMp3AlbumIE(ZingMp3BaseIE):
class ZingMp3ChartHomeIE(ZingMp3BaseIE): class ZingMp3ChartHomeIE(ZingMp3BaseIE):
_VALID_URL = r'https?://(?:mp3\.zing|zingmp3)\.vn/(?P<id>(?:zing-chart|moi-phat-hanh))/?(?:[#?]|$)' _VALID_URL = r'https?://(?:mp3\.zing|zingmp3)\.vn/(?P<id>(?:zing-chart|moi-phat-hanh|top100|podcast-discover))/?(?:[#?]|$)'
_TESTS = [{ _TESTS = [{
'url': 'https://zingmp3.vn/zing-chart', 'url': 'https://zingmp3.vn/zing-chart',
'info_dict': { 'info_dict': {
@ -244,13 +281,34 @@ class ZingMp3ChartHomeIE(ZingMp3BaseIE):
'id': 'moi-phat-hanh', 'id': 'moi-phat-hanh',
}, },
'playlist_mincount': 100, 'playlist_mincount': 100,
}, {
'url': 'https://zingmp3.vn/top100',
'info_dict': {
'id': 'top100',
},
'playlist_mincount': 50,
}, {
'url': 'https://zingmp3.vn/podcast-discover',
'info_dict': {
'id': 'podcast-discover',
},
'playlist_mincount': 4,
}] }]
IE_NAME = 'zingmp3:chart-home' IE_NAME = 'zingmp3:chart-home'
def _real_extract(self, url): def _real_extract(self, url):
url_type = self._match_id(url) url_type = self._match_id(url)
data = self._call_api(url_type, {'id': url_type}) params = {'id': url_type}
items = traverse_obj(data, ('RTChart', 'items') if url_type == 'zing-chart' else 'items') if url_type == 'podcast-discover':
params['type'] = 'discover'
data = self._call_api(url_type, params)
items = []
if url_type == 'top100':
items.extend(traverse_obj(data, (..., 'items', ..., {dict})))
elif url_type == 'zing-chart':
items.extend(traverse_obj(data, ('RTChart', 'items', ..., {dict})))
else:
items.extend(traverse_obj(data, ('items', ..., {dict})))
return self.playlist_result(self._parse_items(items), url_type) return self.playlist_result(self._parse_items(items), url_type)
@ -334,7 +392,7 @@ class ZingMp3ChartMusicVideoIE(ZingMp3BaseIE):
class ZingMp3UserIE(ZingMp3BaseIE): class ZingMp3UserIE(ZingMp3BaseIE):
_VALID_URL = r'https?://(?:mp3\.zing|zingmp3)\.vn/(?P<user>[^/]+)/(?P<type>bai-hat|single|album|video)/?(?:[?#]|$)' _VALID_URL = r'https?://(?:mp3\.zing|zingmp3)\.vn/(?P<user>[^/]+)/(?P<type>bai-hat|single|album|video|song)/?(?:[?#]|$)'
IE_NAME = 'zingmp3:user' IE_NAME = 'zingmp3:user'
_TESTS = [{ _TESTS = [{
'url': 'https://zingmp3.vn/Mr-Siro/bai-hat', 'url': 'https://zingmp3.vn/Mr-Siro/bai-hat',
@ -368,6 +426,18 @@ class ZingMp3UserIE(ZingMp3BaseIE):
'description': 'md5:5bdcf45e955dc1b8d7f518f322ffef36', 'description': 'md5:5bdcf45e955dc1b8d7f518f322ffef36',
}, },
'playlist_mincount': 15, 'playlist_mincount': 15,
}, {
'url': 'https://zingmp3.vn/new-release/song',
'info_dict': {
'id': 'new-release-song',
},
'playlist_mincount': 50,
}, {
'url': 'https://zingmp3.vn/new-release/album',
'info_dict': {
'id': 'new-release-album',
},
'playlist_mincount': 20,
}] }]
def _fetch_page(self, user_id, url_type, page): def _fetch_page(self, user_id, url_type, page):
@ -380,20 +450,28 @@ class ZingMp3UserIE(ZingMp3BaseIE):
}) })
def _real_extract(self, url): def _real_extract(self, url):
user_alias, url_type = self._match_valid_url(url).group('user', 'type') alias, url_type = self._match_valid_url(url).group('user', 'type')
if not url_type: if not url_type:
url_type = 'bai-hat' url_type = 'bai-hat'
user_info = self._call_api('info-artist', {}, user_alias, query={'alias': user_alias}) user_info = self._call_api('info-artist', {}, alias, query={'alias': alias})
# Handle for new-release
if alias == 'new-release' and url_type in ('song', 'album'):
_id = f'{alias}-{url_type}'
return self.playlist_result(self._parse_items(
self._call_api('new-release', params={'type': url_type}, display_id=_id)), _id)
else:
# Handle for user/artist
if url_type in ('bai-hat', 'video'): if url_type in ('bai-hat', 'video'):
entries = self._paged_list(user_info['id'], url_type) entries = self._paged_list(user_info['id'], url_type)
else: else:
section_id = 'aAlbum' if url_type == 'album' else 'aSingle'
entries = self._parse_items(traverse_obj(user_info, ( entries = self._parse_items(traverse_obj(user_info, (
'sections', 'sections', lambda _, v: v['sectionId'] == section_id, 'items', ...)))
lambda _, v: v['sectionId'] == 'aAlbum' if url_type == 'album' else v['sectionId'] == 'aSingle',
'items', ...)))
return self.playlist_result( return self.playlist_result(
entries, user_info['id'], f'{user_info.get("name")} - {url_type}', user_info.get('biography')) entries, user_info['id'], join_nonempty(user_info.get('name'), url_type, delim=' - '),
user_info.get('biography'))
class ZingMp3HubIE(ZingMp3BaseIE): class ZingMp3HubIE(ZingMp3BaseIE):
@ -403,7 +481,7 @@ class ZingMp3HubIE(ZingMp3BaseIE):
'url': 'https://zingmp3.vn/hub/Nhac-Moi/IWZ9Z0CA.html', 'url': 'https://zingmp3.vn/hub/Nhac-Moi/IWZ9Z0CA.html',
'info_dict': { 'info_dict': {
'id': 'IWZ9Z0CA', 'id': 'IWZ9Z0CA',
'title': 'Nhạc Mới', 'title': 'BXH Nhạc Mới',
'description': 'md5:1cc31b68a6f746427b07b2756c22a558', 'description': 'md5:1cc31b68a6f746427b07b2756c22a558',
}, },
'playlist_mincount': 20, 'playlist_mincount': 20,
@ -424,3 +502,129 @@ class ZingMp3HubIE(ZingMp3BaseIE):
'sections', lambda _, v: v['sectionId'] == 'hub', 'items', ...))) 'sections', lambda _, v: v['sectionId'] == 'hub', 'items', ...)))
return self.playlist_result( return self.playlist_result(
entries, song_id, hub_detail.get('title'), hub_detail.get('description')) entries, song_id, hub_detail.get('title'), hub_detail.get('description'))
class ZingMp3LiveRadioIE(ZingMp3BaseIE):
IE_NAME = 'zingmp3:liveradio'
_VALID_URL = r'https?://(?:mp3\.zing|zingmp3)\.vn/(?P<type>(?:liveradio))/(?P<id>\w+)(?:\.html|\?)'
_TESTS = [{
'url': 'https://zingmp3.vn/liveradio/IWZ979UB.html',
'info_dict': {
'id': 'IWZ979UB',
'title': r're:^V\-POP',
'description': 'md5:aa857f8a91dc9ce69e862a809e4bdc10',
'protocol': 'm3u8_native',
'ext': 'mp4',
'view_count': int,
'thumbnail': r're:^https?://.*\.jpg',
'like_count': int,
'live_status': 'is_live',
},
'params': {
'skip_download': True,
},
}, {
'url': 'https://zingmp3.vn/liveradio/IWZ97CWB.html',
'info_dict': {
'id': 'IWZ97CWB',
'title': r're:^Live\s247',
'description': 'md5:d41d8cd98f00b204e9800998ecf8427e',
'protocol': 'm3u8_native',
'ext': 'm4a',
'view_count': int,
'thumbnail': r're:^https?://.*\.jpg',
'like_count': int,
'live_status': 'is_live',
},
'params': {
'skip_download': True,
},
}]
def _real_extract(self, url):
url_type, live_radio_id = self._match_valid_url(url).group('type', 'id')
info = self._call_api(url_type, {'id': live_radio_id})
manifest_url = info.get('streaming')
if not manifest_url:
raise ExtractorError('This radio is offline.', expected=True)
fmts, subtitles = self._extract_m3u8_formats_and_subtitles(manifest_url, live_radio_id, fatal=False)
return {
'id': live_radio_id,
'is_live': True,
'formats': fmts,
'subtitles': subtitles,
**traverse_obj(info, {
'title': 'title',
'thumbnail': (('thumbnail', 'thumbnailM', 'thumbnailV', 'thumbnailH'), {url_or_none}),
'view_count': ('activeUsers', {int_or_none}),
'like_count': ('totalReaction', {int_or_none}),
'description': 'description',
}, get_all=False),
}
class ZingMp3PodcastEpisodeIE(ZingMp3BaseIE):
IE_NAME = 'zingmp3:podcast-episode'
_VALID_URL = ZingMp3BaseIE._VALID_URL_TMPL % 'pgr|cgr'
_TESTS = [{
'url': 'https://zingmp3.vn/pgr/Nhac-Moi-Moi-Ngay/68Z9W66B.html',
'info_dict': {
'id': '68Z9W66B',
'title': 'Nhạc Mới Mỗi Ngày',
'description': 'md5:2875dfa951f8e5356742f1610cf20691'
},
'playlist_mincount': 20,
}, {
'url': 'https://zingmp3.vn/cgr/Am-nhac/IWZ980AO.html',
'info_dict': {
'id': 'IWZ980AO',
'title': 'Âm nhạc'
},
'playlist_mincount': 2,
}]
def _fetch_page(self, eps_id, url_type, page):
return self._call_api(url_type, {
'id': eps_id,
'page': page,
'count': self._PER_PAGE
})
def _real_extract(self, url):
podcast_id, url_type = self._match_valid_url(url).group('id', 'type')
podcast_info = self._call_api(url_type, {'id': podcast_id})
entries = self._paged_list(podcast_id, 'pgr-list' if url_type == 'pgr' else 'cgr-list')
return self.playlist_result(
entries, podcast_id, podcast_info.get('title'), podcast_info.get('description'))
class ZingMp3PodcastIE(ZingMp3BaseIE):
IE_NAME = 'zingmp3:podcast'
_VALID_URL = r'https?://(?:mp3\.zing|zingmp3)\.vn/(?P<id>(?:cgr|top-podcast|podcast-new))/?(?:[#?]|$)'
_TESTS = [{
'url': 'https://zingmp3.vn/cgr',
'info_dict': {
'id': 'cgr',
},
'playlist_mincount': 5,
}, {
'url': 'https://zingmp3.vn/top-podcast',
'info_dict': {
'id': 'top-podcast',
},
'playlist_mincount': 7,
}, {
'url': 'https://zingmp3.vn/podcast-new',
'info_dict': {
'id': 'podcast-new',
},
'playlist_mincount': 4,
}]
def _real_extract(self, url):
url_type = self._match_id(url)
params = {'id': url_type}
if url_type == 'podcast-new':
params['type'] = 'new'
items = self._call_api('cgrs' if url_type == 'cgr' else url_type, params)['items']
return self.playlist_result(self._parse_items(items), url_type)

View File

@ -131,7 +131,7 @@ def _get_binary_name():
def _get_system_deprecation(): def _get_system_deprecation():
MIN_SUPPORTED, MIN_RECOMMENDED = (3, 7), (3, 8) MIN_SUPPORTED, MIN_RECOMMENDED = (3, 8), (3, 8)
if sys.version_info > MIN_RECOMMENDED: if sys.version_info > MIN_RECOMMENDED:
return None return None
@ -140,15 +140,7 @@ def _get_system_deprecation():
if sys.version_info < MIN_SUPPORTED: if sys.version_info < MIN_SUPPORTED:
msg = f'Python version {major}.{minor} is no longer supported' msg = f'Python version {major}.{minor} is no longer supported'
else: else:
msg = f'Support for Python version {major}.{minor} has been deprecated. ' msg = (f'Support for Python version {major}.{minor} has been deprecated. '
# Temporary until `win_x86_exe` uses 3.8, which will deprecate Vista and Server 2008
if detect_variant() == 'win_x86_exe':
platform_name = platform.platform()
if any(platform_name.startswith(f'Windows-{name}') for name in ('Vista', '2008Server')):
msg = 'Support for Windows Vista/Server 2008 has been deprecated. '
else:
return None
msg += ('See https://github.com/yt-dlp/yt-dlp/issues/7803 for details.'
'\nYou may stop receiving updates on this version at any time') '\nYou may stop receiving updates on this version at any time')
major, minor = MIN_RECOMMENDED major, minor = MIN_RECOMMENDED