pax_global_header00006660000000000000000000000064146516513240014521gustar00rootroot0000000000000052 comment=757be3c748f4a7b3afa9dce3ed6c96e94bac435a svtplay-dl-4.97.1/000077500000000000000000000000001465165132400137225ustar00rootroot00000000000000svtplay-dl-4.97.1/.coveragerc000066400000000000000000000001421465165132400160400ustar00rootroot00000000000000[run] branch = true include = lib/svtplay_dl/* omit = lib/svtplay_dl/__version__.py */tests/* svtplay-dl-4.97.1/.gitattributes000066400000000000000000000000531465165132400166130ustar00rootroot00000000000000lib/svtplay_dl/__version__.py export-subst svtplay-dl-4.97.1/.github/000077500000000000000000000000001465165132400152625ustar00rootroot00000000000000svtplay-dl-4.97.1/.github/ISSUE_TEMPLATE.md000066400000000000000000000010441465165132400177660ustar00rootroot00000000000000 ### svtplay-dl versions: Run `svtplay-dl --version` ### Operating system and Python version: Name and version of the operating system and python version (run `python --version`) ### What is the issue: Always include the URL you want to download and all switches you are using. You should also add `--verbose` because it makes it much easier for use to find the issue :) svtplay-dl --verbose https://www.example.com svtplay-dl-4.97.1/.github/workflows/000077500000000000000000000000001465165132400173175ustar00rootroot00000000000000svtplay-dl-4.97.1/.github/workflows/tests.yaml000066400000000000000000000131411465165132400213450ustar00rootroot00000000000000name: CI on: [push, pull_request] jobs: tests: name: ${{ matrix.name }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: include: - {name: '3.12', python: '3.12', os: ubuntu-latest, architecture: 'x64', cibuild: "no"} - {name: '3.11', python: '3.11', os: ubuntu-latest, architecture: 'x64', cibuild: "no"} - {name: '3.10', python: '3.10', os: ubuntu-latest, architecture: 'x64', cibuild: "no"} - {name: '3.9', python: '3.9', os: ubuntu-latest, architecture: 'x64', cibuild: "yes"} - {name: '3.8', python: '3.8', os: ubuntu-latest, architecture: 'x64', cibuild: "no"} - {name: Windows, python: '3.8', os: windows-latest, architecture: 'x64', arch-cx: 'win-amd64', cx_name: 'amd64', cibuild: "yes"} - {name: WindowsX86, python: '3.8', os: windows-latest, architecture: 'x86', arch-cx: 'win32', cx_name: 'win32', cibuild: "yes"} steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} architecture: ${{ matrix.architecture }} cache: 'pip' - name: update pip run: | pip install -U wheel pip install -U setuptools python -m pip install -U pip - name: install deps run: | pip install -r requirements.txt pip install -r requirements-dev.txt - name: cache pre-commit uses: actions/cache@v4 with: path: ~/.cache/pre-commit key: per-commit|${{ runner.os }}-${{ matrix.python }}-${{ hashFiles('.pre-commit-config.yaml') }} restore-keys: per-commit|${{ runner.os }}-${{ matrix.python }}- if: matrix.os == 'ubuntu-latest' - name: pre-commit run: pre-commit run --all-files --show-diff-on-failure if: matrix.os == 'ubuntu-latest' - name: pytest run: pytest -v --cov binaries-make: name: "binaries make" runs-on: "ubuntu-latest" strategy: fail-fast: false steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: actions/setup-python@v5 with: python-version: '3.9' cache: 'pip' - name: update pip run: | pip install -U setuptools python -m pip install -U pip - name: install deps run: | pip install -r requirements.txt pip install -r requirements-dev.txt # Build .zip fil for *nix - run: make - run: ./svtplay-dl --version - name: cibuild run: python scripts/cibuild.py env: CIBUILD: "yes" BUILD_DOCKER: "no" AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} OS: "ubuntu-latest" binaries-exe: name: "binaries exe ${{ matrix.architecture }}" runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: include: - {name: Windows, python: '3.8', os: windows-latest, architecture: 'x64', arch-cx: 'win-amd64', cx_name: 'amd64', cibuild: "yes"} - {name: WindowsX86, python: '3.8', os: windows-latest, architecture: 'x86', arch-cx: 'win32', cx_name: 'win32', cibuild: "yes"} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} architecture: ${{ matrix.architecture }} cache: 'pip' - name: update pip run: | pip install -U wheel pip install -U setuptools python -m pip install -U pip - name: install deps run: | pip install -r requirements.txt pip install -r requirements-dev.txt - name: set version run: python setversion.py - name: build .exe run: python setup.py build_exe - name: run the .exe file run: build\\exe.${{ matrix.arch-cx }}-${{ matrix.python }}\\svtplay-dl.exe --version - run: | mkdir svtplay-dl xcopy /s build\\exe.${{ matrix.arch-cx }}-${{ matrix.python }} svtplay-dl - run: 7z a -tzip svtplay-dl-${{ matrix.cx_name }}.zip svtplay-dl - name: cibuild run: python scripts/cibuild.py env: CIBUILD: "yes" BUILD_DOCKER: "no" AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} OS: "windows-latest" binaries-pypi: name: "binaries pypi" runs-on: "ubuntu-latest" strategy: fail-fast: false steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: actions/setup-python@v5 with: python-version: 3.9 cache: 'pip' - name: update pip run: | pip install -U setuptools python -m pip install -U pip - name: install deps run: | pip install -r requirements.txt pip install -r requirements-dev.txt - name: python pkg run: python setup.py sdist bdist_wheel - name: cibuild run: python scripts/cibuild.py env: CIBUILD: "yes" BUILD_DOCKER: "yes" TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }} TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} OS: "ubuntu-latest" svtplay-dl-4.97.1/.gitignore000066400000000000000000000055271465165132400157230ustar00rootroot00000000000000cover/ svtplay-dl svtplay-dl.1 svtplay-dl.1.gz github.com/* # Windows thumbnail cache files Thumbs.db ehthumbs.db ehthumbs_vista.db # Dump file *.stackdump # Folder config file [Dd]esktop.ini # Recycle Bin used on file shares $RECYCLE.BIN/ # Windows Installer files *.cab *.msi *.msix *.msm *.msp # Windows shortcuts *.lnk # General .DS_Store .AppleDouble .LSOverride # Thumbnails ._* # Files that might appear in the root of a volume .DocumentRevisions-V100 .fseventsd .Spotlight-V100 .TemporaryItems .Trashes .VolumeIcon.icns .com.apple.timemachine.donotpresent # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ # Swap [._]*.s[a-v][a-z] [._]*.sw[a-p] [._]s[a-v][a-z] [._]sw[a-p] # Session Session.vim # Temporary .netrwhist *~ # Auto-generated tag files tags # Persistent undo [._]*.un~ # User-specific stuff .idea/**/workspace.xml .idea/**/tasks.xml .idea/**/dictionaries .idea/**/shelf # Sensitive or high-churn files .idea/**/dataSources/ .idea/**/dataSources.ids .idea/**/dataSources.local.xml .idea/**/sqlDataSources.xml .idea/**/dynamic.xml .idea/**/uiDesigner.xml .idea/**/dbnavigator.xml # Gradle .idea/**/gradle.xml .idea/**/libraries # Mongo Explorer plugin .idea/**/mongoSettings.xml # File-based project format *.iws # IntelliJ out/ # mpeltonen/sbt-idea plugin .idea_modules/ # JIRA plugin atlassian-ide-plugin.xml # Cursive Clojure plugin .idea/replstate.xml # Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties fabric.properties # Editor-based Rest Client .idea/httpRequests # media *.ts *.mp4 *.srt svtplay-dl-4.97.1/.pre-commit-config.yaml000066400000000000000000000015571465165132400202130ustar00rootroot00000000000000# See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-yaml - id: check-added-large-files - repo: https://github.com/ambv/black rev: 24.3.0 hooks: - id: black language_version: python3 - repo: https://github.com/pycqa/flake8 rev: 7.0.0 hooks: - id: flake8 - repo: https://github.com/asottile/pyupgrade rev: v3.15.2 hooks: - id: pyupgrade args: [--py38-plus] - repo: https://github.com/asottile/reorder_python_imports rev: v3.12.0 hooks: - id: reorder-python-imports - repo: https://github.com/asottile/add-trailing-comma rev: v3.1.0 hooks: - id: add-trailing-comma svtplay-dl-4.97.1/LICENSE000066400000000000000000000021121465165132400147230ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2011-2015 Johan Andersson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. svtplay-dl-4.97.1/MANIFEST.in000066400000000000000000000001361465165132400154600ustar00rootroot00000000000000include README.md include LICENSE include versioneer.py include lib/svtplay_dl/__version__.py svtplay-dl-4.97.1/Makefile000066400000000000000000000030051465165132400153600ustar00rootroot00000000000000all: svtplay-dl .PHONY: test cover doctest pylint svtplay-dl \ release clean_releasedir $(RELEASE_DIR) # These variables describe the latest release: VERSION = 1.9.11 LATEST_RELEASE = $(VERSION) # Compress the manual if MAN_GZIP is set to y, ifeq ($(MAN_GZIP),y) MANFILE_EXT = .gz endif MANFILE = svtplay-dl.1$(MANFILE_EXT) # As pod2man is a perl tool, we have to jump through some hoops # to remove references to perl.. :-) POD2MAN ?= pod2man --section 1 --utf8 \ --center "svtplay-dl manual" \ --release "svtplay-dl $(VERSION)" \ --date "$(LATEST_RELEASE_DATE)" PREFIX ?= /usr/local BINDIR = $(PREFIX)/bin PYTHON ?= /usr/bin/env python3 export PYTHONPATH=lib # If you don't have a python3 environment (e.g. mock for py3 and # nosetests3), you can remove the -3 flag. TEST_OPTS ?= -2 -3 svtplay-dl: $(PYFILES) $(MAKE) -C lib mv -f lib/svtplay-dl . svtplay-dl.1: svtplay-dl.pod rm -f $@ $(POD2MAN) $< $@ svtplay-dl.1.gz: svtplay-dl.1 rm -f $@ gzip -9 svtplay-dl.1 test: sh scripts/run-tests.sh $(TEST_OPTS) install: svtplay-dl install -d $(DESTDIR)$(BINDIR) install -m 755 svtplay-dl $(DESTDIR)$(BINDIR) cover: sh scripts/run-tests.sh -C pylint: $(MAKE) -C lib pylint doctest: svtplay-dl sh scripts/diff_man_help.sh release: git tag -m "New version $(NEW_RELEASE)" \ -m "$$(git log --oneline $$(git describe --tags --abbrev=0 HEAD^)..HEAD^)" \ $(NEW_RELEASE) clean: $(MAKE) -C lib clean rm -f svtplay-dl rm -f $(MANFILE) rm -rf .tox svtplay-dl-4.97.1/README.md000066400000000000000000000104271465165132400152050ustar00rootroot00000000000000# svtplay-dl [![Build Status Actions](https://img.shields.io/github/actions/workflow/status/spaam/svtplay-dl/tests.yaml?label=Build%20status%20action)](https://github.com/spaam/svtplay-dl/actions) [![Discord](https://img.shields.io/static/v1?label=chat&message=discord&color=green&style=flat&logo=discord)](https://discord.gg/S2YK6Jtb3P) ## Installation ### MacOS If you have [Homebrew](https://brew.sh/) on your machine you can install by running: ``` brew install svtplay-dl ``` You will need to run `brew install ffmpeg` afterwards, if you don't already have one of these packages. ### Debian and Ubuntu svtplay-dl is available in Debian strech and later and on Ubuntu 16.04 and later, which means you can install it straight away using apt. The version in their repo is often old and thus we **strongly** recommend using our own apt repo, which always include the latest version. The svtplay-dl repo for Debian / Ubuntu can be found at [apt.svtplay-dl.se](https://apt.svtplay-dl.se/). ##### Add the release PGP keys: ``` curl -s https://svtplay-dl.se/release-key.txt | sudo apt-key add - ``` ##### Add the "release" channel to your APT sources: ``` echo "deb https://apt.svtplay-dl.se/ svtplay-dl release" | sudo tee /etc/apt/sources.list.d/svtplay-dl.list ``` ##### Update and install svtplay-dl: ``` sudo apt-get update sudo apt-get install svtplay-dl ``` ### Solus svtplay-dl is avaliable in the [Solus](https://getsol.us.com/) repository and can be installed by simply running: ``` sudo eopkg it svtplay-dl ``` ### Windows You can download the Windows binaries from [svtplay-dl.se](https://svtplay-dl.se/) If you want to build your own Windows binaries: 1. Install [cx_freeze](https://anthony-tuininga.github.io/cx_Freeze/) 3. Follow the steps listed under [From source](#from-source) 4. cd path\to\svtplay-dl && mkdir build 5. `pip install -e .` 6. `python setversion.py` # this will change the version string to a more useful one 7. `python %PYTHON%\\Scripts\\cxfreeze --include-modules=cffi,queue,idna.idnadata --target-dir=build bin/svtplay-dl` 8. Find binary in build folder. you need `svtplay-dl.exe` and `pythonXX.dll` from that folder to run `svtplay-dl.exe` ### Other systems with python ``` pip3 install svtplay-dl ``` ### Any UNIX (Linux, BSD, macOS, etc.) ##### Download with curl ``` sudo curl -L https://svtplay-dl.se/download/latest/svtplay-dl -o /usr/local/bin/svtplay-dl ``` ##### Make it executable ``` sudo chmod a+rx /usr/local/bin/svtplay-dl ``` ### From source If packaging isn’t available for your operating system, or you want to use a non-released version, you’ll want to install from source. Use git to download the sources: ``` git clone https://github.com/spaam/svtplay-dl ``` svtplay-dl requires the following additional tools and libraries. They are usually available from your distribution’s package repositories. If you don’t have them, some features will not be working. - [Python](https://www.python.org) 3.6 or higher - [cryptography](https://cryptography.io/en/latest) to download encrypted HLS streams - [PyYaml](https://github.com/yaml/pyyaml) for configure file - [Requests](https://2.python-requests.org) - [PySocks](https://github.com/Anorov/PySocks) to enable proxy support - [ffmpeg](https://ffmpeg.org) for postprocessing and/or for DASH streams ([ffmpeg](https://ffmpeg.zeranoe.com) for Windows) ##### To install it, run: ``` sudo python3 setup.py install ``` ## After install ``` svtplay-dl [options] URL ``` If you encounter any bugs or problems, don’t hesitate to open an issue [on github](https://github.com/spaam/svtplay-dl/issues). Or why not join the ``#svtplay-dl`` IRC channel on Freenode? ## Supported services This script works for: - aftonbladet.se - bambuser.com - comedycentral.se - di.se - dn.se - dplay.se - dr.dk - efn.se - expressen.se - hbo.com - kanal9play.se - nickelodeon.nl - nickelodeon.no - nickelodeon.se - nrk.no - oppetarkiv.se - pluto.tv (former viafree.se, viafree.dk, viafree.no) - ruv.is - svd.se - sverigesradio.se - svtplay.se - tv3play.ee - tv3play.lt - tv3play.lv - tv4.se - tv4play.se - twitch.tv - ur.se - urplay.se - vg.no - viagame.com ## License This project is licensed under [The MIT License (MIT)](LICENSE) Homepage: [svtplay-dl.se](https://svtplay-dl.se/) svtplay-dl-4.97.1/bin/000077500000000000000000000000001465165132400144725ustar00rootroot00000000000000svtplay-dl-4.97.1/bin/svtplay-dl000077500000000000000000000002641465165132400165210ustar00rootroot00000000000000#!/usr/bin/env python3 # ex:ts=4:sw=4:sts=4:et # -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- import svtplay_dl if __name__ == "__main__": svtplay_dl.main() svtplay-dl-4.97.1/dockerfile/000077500000000000000000000000001465165132400160315ustar00rootroot00000000000000svtplay-dl-4.97.1/dockerfile/Dockerfile000066400000000000000000000006141465165132400200240ustar00rootroot00000000000000# using edge to get ffmpeg-4.x FROM alpine:edge LABEL maintainer="j@i19.se" COPY dist/*.whl . RUN set -xe \ && apk add --no-cache \ ca-certificates \ python3 \ py3-pip \ py3-cryptography \ ffmpeg \ && python3 -m pip install --break-system-packages *.whl \ && rm -f *.whl WORKDIR /data ENTRYPOINT ["python3", "/usr/bin/svtplay-dl"] CMD ["--help"] svtplay-dl-4.97.1/docs/000077500000000000000000000000001465165132400146525ustar00rootroot00000000000000svtplay-dl-4.97.1/docs/README.docker.md000066400000000000000000000007411465165132400174010ustar00rootroot00000000000000# svtplay-dl container version of the script. # usage ```sh docker run -it --rm -u $(id -u):$(id -g) -v "$(pwd):/data" spaam/svtplay-dl ``` or create an alias: ##### bash (~/.bashrc) ``` alias svtplay-dl='docker run -it --rm -u $(id -u):$(id -g) -v "$(pwd):/data" spaam/svtplay-dl' ``` ##### zsh (~/.zshrc) ``` alias svtplay-dl='docker run -it --rm -u $(id -u):$(id -g) -v "$(pwd):/data" spaam/svtplay-dl' ``` # build example ```sh docker build -t svtplay-dl . ``` svtplay-dl-4.97.1/lib/000077500000000000000000000000001465165132400144705ustar00rootroot00000000000000svtplay-dl-4.97.1/lib/Makefile000066400000000000000000000026111465165132400161300ustar00rootroot00000000000000all: svtplay-dl clean: find . -name '*.pyc' -exec rm {} \; rm -f svtplay-dl pylint: pylint $(PYLINT_OPTS) svtplay_dl export PACKAGES = svtplay_dl \ svtplay_dl.fetcher \ svtplay_dl.utils \ svtplay_dl.service \ svtplay_dl.subtitle \ svtplay_dl.postprocess export PYFILES = $(sort $(addsuffix /*.py,$(subst .,/,$(PACKAGES)))) PYTHON ?= /usr/bin/env python3 VERSION = $(shell git describe --tags --dirty --always 2>/dev/null || echo $(LATEST_RELEASE)-unknown) svtplay-dl: $(PYFILES) @# "Verify that there's no .build already" ! [ -d .build ] || { \ echo "ERROR: build already in progress? (or remove $(CURDIR)/.build/)"; \ exit 1; \ }; \ mkdir -p .build @# Stage the files in .build for postprocessing for py in $(PYFILES); do \ install -d ".build/$${py%/*}"; \ install $$py .build/$$py; \ done @# Add git version info to __version__, seen in --version sed -i -e 's/^__version__ = \(.*\)$$/__version__ = "$(VERSION)"/' \ .build/svtplay_dl/__init__.py @# reset timestamps, to avoid non-determinism in zip file find .build/ -exec touch -m -t 198001010000 {} \; (cd .build && zip -X --quiet svtplay-dl $(PYFILES)) (cd .build && zip -X --quiet --junk-paths svtplay-dl svtplay_dl/__main__.py) echo '#!$(PYTHON)' > svtplay-dl cat .build/svtplay-dl.zip >> svtplay-dl rm -rf .build chmod a+x svtplay-dl svtplay-dl-4.97.1/lib/svtplay_dl/000077500000000000000000000000001465165132400166515ustar00rootroot00000000000000svtplay-dl-4.97.1/lib/svtplay_dl/__init__.py000066400000000000000000000043221465165132400207630ustar00rootroot00000000000000# ex:ts=4:sw=4:sts=4:et # -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- import logging import sys import yaml from svtplay_dl.service.cmore import Cmore from svtplay_dl.utils.getmedia import get_media from svtplay_dl.utils.getmedia import get_multiple_media from svtplay_dl.utils.parser import parser from svtplay_dl.utils.parser import parsertoconfig from svtplay_dl.utils.parser import setup_defaults from .__version__ import get_versions __version__ = get_versions()["version"] del get_versions log = logging.getLogger("svtplay_dl") def setup_log(silent, verbose=False): logging.addLevelName(25, "INFO") fmt = "%(levelname)s: %(message)s" if silent: stream = sys.stderr level = 25 elif verbose: stream = sys.stderr level = logging.DEBUG fmt = "%(levelname)s [%(created)s] %(pathname)s/%(funcName)s: %(message)s" else: stream = sys.stdout level = logging.INFO logging.basicConfig(level=level, format=fmt) hdlr = logging.StreamHandler(stream) log.addHandler(hdlr) def main(): """Main program""" parse, options = parser(__version__) if options.flexibleq and not options.quality: logging.error("flexible-quality requires a quality") if options.only_audio and options.only_video: logging.error("Only use one of them, not both at the same time") sys.exit(2) if len(options.urls) == 0: parse.print_help() sys.exit(0) urls = options.urls config = parsertoconfig(setup_defaults(), options) if len(urls) < 1: parse.error("Incorrect number of arguments") setup_log(config.get("silent"), config.get("verbose")) if options.cmoreoperatorlist: config = parsertoconfig(setup_defaults(), options) c = Cmore(config, urls) c.operatorlist() sys.exit(0) try: if len(urls) == 1: get_media(urls[0], config, __version__) else: get_multiple_media(urls, config) except KeyboardInterrupt: print("") except (yaml.YAMLError, yaml.MarkedYAMLError) as e: logging.error("Your settings file(s) contain invalid YAML syntax! Please fix and restart!, %s", str(e)) sys.exit(2) svtplay-dl-4.97.1/lib/svtplay_dl/__main__.py000066400000000000000000000005751465165132400207520ustar00rootroot00000000000000#!/usr/bin/env python # ex:ts=4:sw=4:sts=4:et # -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- import sys if __package__ is None and not hasattr(sys, "frozen"): # direct call of __main__.py import os.path sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import svtplay_dl if __name__ == "__main__": svtplay_dl.main() svtplay-dl-4.97.1/lib/svtplay_dl/__version__.py000066400000000000000000000550741465165132400215170ustar00rootroot00000000000000# This file helps to compute a version number in source trees obtained from # git-archive tarball (such as those provided by githubs download-from-tag # feature). Distribution tarballs (built by setup.py sdist) and build # directories (produced by setup.py build) will contain a much shorter file # that just contains the computed version number. # This file is released into the public domain. # Generated by versioneer-0.28 # https://github.com/python-versioneer/python-versioneer """Git implementation of _version.py.""" import errno import functools import os import re import subprocess import sys from typing import Callable from typing import Dict def get_keywords(): """Get the keywords needed to look up the version information.""" # these strings will be replaced by git during git-archive. # setup.py/versioneer.py will grep for the variable names, so they must # each be defined on a line of their own. _version.py will just call # get_keywords(). git_refnames = " (HEAD -> master, tag: 4.97.1)" git_full = "757be3c748f4a7b3afa9dce3ed6c96e94bac435a" git_date = "2024-07-29 10:29:08 +0200" keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} return keywords class VersioneerConfig: """Container for Versioneer configuration parameters.""" def get_config(): """Create, populate and return the VersioneerConfig() object.""" # these strings are filled in when 'setup.py versioneer' creates # _version.py cfg = VersioneerConfig() cfg.VCS = "git" cfg.style = "pep440" cfg.tag_prefix = "" cfg.parentdir_prefix = "None" cfg.versionfile_source = "lib/svtplay_dl/__version__.py" cfg.verbose = False return cfg class NotThisMethod(Exception): """Exception raised if a method is not valid for the current scenario.""" LONG_VERSION_PY: Dict[str, str] = {} HANDLERS: Dict[str, Dict[str, Callable]] = {} def register_vcs_handler(vcs, method): # decorator """Create decorator to mark a method as the handler of a VCS.""" def decorate(f): """Store f in HANDLERS[vcs][method].""" if vcs not in HANDLERS: HANDLERS[vcs] = {} HANDLERS[vcs][method] = f return f return decorate def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env=None): """Call the given command(s).""" assert isinstance(commands, list) process = None popen_kwargs = {} if sys.platform == "win32": # This hides the console window if pythonw.exe is used startupinfo = subprocess.STARTUPINFO() startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW popen_kwargs["startupinfo"] = startupinfo for command in commands: try: dispcmd = str([command] + args) # remember shell=False, so use git.cmd on windows, not just git process = subprocess.Popen( [command] + args, cwd=cwd, env=env, stdout=subprocess.PIPE, stderr=(subprocess.PIPE if hide_stderr else None), **popen_kwargs, ) break except OSError: e = sys.exc_info()[1] if e.errno == errno.ENOENT: continue if verbose: print("unable to run %s" % dispcmd) print(e) return None, None else: if verbose: print(f"unable to find command, tried {commands}") return None, None stdout = process.communicate()[0].strip().decode() if process.returncode != 0: if verbose: print("unable to run %s (error)" % dispcmd) print("stdout was %s" % stdout) return None, process.returncode return stdout, process.returncode def versions_from_parentdir(parentdir_prefix, root, verbose): """Try to determine the version from the parent directory name. Source tarballs conventionally unpack into a directory that includes both the project name and a version string. We will also support searching up two directory levels for an appropriately named parent directory """ rootdirs = [] for _ in range(3): dirname = os.path.basename(root) if dirname.startswith(parentdir_prefix): return {"version": dirname[len(parentdir_prefix) :], "full-revisionid": None, "dirty": False, "error": None, "date": None} rootdirs.append(root) root = os.path.dirname(root) # up a level if verbose: print(f"Tried directories {str(rootdirs)} but none started with prefix {parentdir_prefix}") raise NotThisMethod("rootdir doesn't start with parentdir_prefix") @register_vcs_handler("git", "get_keywords") def git_get_keywords(versionfile_abs): """Extract version information from the given file.""" # the code embedded in _version.py can just fetch the value of these # keywords. When used from setup.py, we don't want to import _version.py, # so we do it with a regexp instead. This function is not used from # _version.py. keywords = {} try: with open(versionfile_abs) as fobj: for line in fobj: if line.strip().startswith("git_refnames ="): mo = re.search(r'=\s*"(.*)"', line) if mo: keywords["refnames"] = mo.group(1) if line.strip().startswith("git_full ="): mo = re.search(r'=\s*"(.*)"', line) if mo: keywords["full"] = mo.group(1) if line.strip().startswith("git_date ="): mo = re.search(r'=\s*"(.*)"', line) if mo: keywords["date"] = mo.group(1) except OSError: pass return keywords @register_vcs_handler("git", "keywords") def git_versions_from_keywords(keywords, tag_prefix, verbose): """Get version information from git keywords.""" if "refnames" not in keywords: raise NotThisMethod("Short version file found") date = keywords.get("date") if date is not None: # Use only the last line. Previous lines may contain GPG signature # information. date = date.splitlines()[-1] # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 # -like" string, which we must then edit to make compliant), because # it's been around since git-1.5.3, and it's too difficult to # discover which version we're using, or to work around using an # older one. date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) refnames = keywords["refnames"].strip() if refnames.startswith("$Format"): if verbose: print("keywords are unexpanded, not using") raise NotThisMethod("unexpanded keywords, not a git-archive tarball") refs = {r.strip() for r in refnames.strip("()").split(",")} # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of # just "foo-1.0". If we see a "tag: " prefix, prefer those. TAG = "tag: " tags = {r[len(TAG) :] for r in refs if r.startswith(TAG)} if not tags: # Either we're using git < 1.8.3, or there really are no tags. We use # a heuristic: assume all version tags have a digit. The old git %d # expansion behaves like git log --decorate=short and strips out the # refs/heads/ and refs/tags/ prefixes that would let us distinguish # between branches and tags. By ignoring refnames without digits, we # filter out many common branch names like "release" and # "stabilization", as well as "HEAD" and "master". tags = {r for r in refs if re.search(r"\d", r)} if verbose: print("discarding '%s', no digits" % ",".join(refs - tags)) if verbose: print("likely tags: %s" % ",".join(sorted(tags))) for ref in sorted(tags): # sorting will prefer e.g. "2.0" over "2.0rc1" if ref.startswith(tag_prefix): r = ref[len(tag_prefix) :] # Filter out refs that exactly match prefix or that don't start # with a number once the prefix is stripped (mostly a concern # when prefix is '') if not re.match(r"\d", r): continue if verbose: print("picking %s" % r) return {"version": r, "full-revisionid": keywords["full"].strip(), "dirty": False, "error": None, "date": date} # no suitable tags, so version is "0+unknown", but full hex is still there if verbose: print("no suitable tags, using unknown + full revision id") return {"version": "0+unknown", "full-revisionid": keywords["full"].strip(), "dirty": False, "error": "no suitable tags", "date": None} @register_vcs_handler("git", "pieces_from_vcs") def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command): """Get version from 'git describe' in the root of the source tree. This only gets called if the git-archive 'subst' keywords were *not* expanded, and _version.py hasn't already been rewritten with a short version string, meaning we're inside a checked out source tree. """ GITS = ["git"] if sys.platform == "win32": GITS = ["git.cmd", "git.exe"] # GIT_DIR can interfere with correct operation of Versioneer. # It may be intended to be passed to the Versioneer-versioned project, # but that should not change where we get our version from. env = os.environ.copy() env.pop("GIT_DIR", None) runner = functools.partial(runner, env=env) _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, hide_stderr=not verbose) if rc != 0: if verbose: print("Directory %s not under git control" % root) raise NotThisMethod("'git rev-parse --git-dir' returned error") # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] # if there isn't one, this yields HEX[-dirty] (no NUM) describe_out, rc = runner(GITS, ["describe", "--tags", "--dirty", "--always", "--long", "--match", f"{tag_prefix}[[:digit:]]*"], cwd=root) # --long was added in git-1.5.5 if describe_out is None: raise NotThisMethod("'git describe' failed") describe_out = describe_out.strip() full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root) if full_out is None: raise NotThisMethod("'git rev-parse' failed") full_out = full_out.strip() pieces = {} pieces["long"] = full_out pieces["short"] = full_out[:7] # maybe improved later pieces["error"] = None branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], cwd=root) # --abbrev-ref was added in git-1.6.3 if rc != 0 or branch_name is None: raise NotThisMethod("'git rev-parse --abbrev-ref' returned error") branch_name = branch_name.strip() if branch_name == "HEAD": # If we aren't exactly on a branch, pick a branch which represents # the current commit. If all else fails, we are on a branchless # commit. branches, rc = runner(GITS, ["branch", "--contains"], cwd=root) # --contains was added in git-1.5.4 if rc != 0 or branches is None: raise NotThisMethod("'git branch --contains' returned error") branches = branches.split("\n") # Remove the first line if we're running detached if "(" in branches[0]: branches.pop(0) # Strip off the leading "* " from the list of branches. branches = [branch[2:] for branch in branches] if "master" in branches: branch_name = "master" elif not branches: branch_name = None else: # Pick the first branch that is returned. Good or bad. branch_name = branches[0] pieces["branch"] = branch_name # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] # TAG might have hyphens. git_describe = describe_out # look for -dirty suffix dirty = git_describe.endswith("-dirty") pieces["dirty"] = dirty if dirty: git_describe = git_describe[: git_describe.rindex("-dirty")] # now we have TAG-NUM-gHEX or HEX if "-" in git_describe: # TAG-NUM-gHEX mo = re.search(r"^(.+)-(\d+)-g([0-9a-f]+)$", git_describe) if not mo: # unparsable. Maybe git-describe is misbehaving? pieces["error"] = "unable to parse git-describe output: '%s'" % describe_out return pieces # tag full_tag = mo.group(1) if not full_tag.startswith(tag_prefix): if verbose: fmt = "tag '%s' doesn't start with prefix '%s'" print(fmt % (full_tag, tag_prefix)) pieces["error"] = f"tag '{full_tag}' doesn't start with prefix '{tag_prefix}'" return pieces pieces["closest-tag"] = full_tag[len(tag_prefix) :] # distance: number of commits since tag pieces["distance"] = int(mo.group(2)) # commit: short hex revision ID pieces["short"] = mo.group(3) else: # HEX: no tags pieces["closest-tag"] = None out, rc = runner(GITS, ["rev-list", "HEAD", "--left-right"], cwd=root) pieces["distance"] = len(out.split()) # total number of commits # commit date: see ISO-8601 comment in git_versions_from_keywords() date = runner(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[0].strip() # Use only the last line. Previous lines may contain GPG signature # information. date = date.splitlines()[-1] pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) return pieces def plus_or_dot(pieces): """Return a + if we don't already have one, else return a .""" if "+" in pieces.get("closest-tag", ""): return "." return "+" def render_pep440(pieces): """Build up version string, with post-release "local version identifier". Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty Exceptions: 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"] or pieces["dirty"]: rendered += plus_or_dot(pieces) rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) if pieces["dirty"]: rendered += ".dirty" else: # exception #1 rendered = "0+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) if pieces["dirty"]: rendered += ".dirty" return rendered def render_pep440_branch(pieces): """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] . The ".dev0" means not master branch. Note that .dev0 sorts backwards (a feature branch will appear "older" than the master branch). Exceptions: 1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"] or pieces["dirty"]: if pieces["branch"] != "master": rendered += ".dev0" rendered += plus_or_dot(pieces) rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) if pieces["dirty"]: rendered += ".dirty" else: # exception #1 rendered = "0" if pieces["branch"] != "master": rendered += ".dev0" rendered += "+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) if pieces["dirty"]: rendered += ".dirty" return rendered def pep440_split_post(ver): """Split pep440 version string at the post-release segment. Returns the release segments before the post-release and the post-release version number (or -1 if no post-release segment is present). """ vc = str.split(ver, ".post") return vc[0], int(vc[1] or 0) if len(vc) == 2 else None def render_pep440_pre(pieces): """TAG[.postN.devDISTANCE] -- No -dirty. Exceptions: 1: no tags. 0.post0.devDISTANCE """ if pieces["closest-tag"]: if pieces["distance"]: # update the post release segment tag_version, post_version = pep440_split_post(pieces["closest-tag"]) rendered = tag_version if post_version is not None: rendered += ".post%d.dev%d" % (post_version + 1, pieces["distance"]) else: rendered += ".post0.dev%d" % (pieces["distance"]) else: # no commits, use the tag as the version rendered = pieces["closest-tag"] else: # exception #1 rendered = "0.post0.dev%d" % pieces["distance"] return rendered def render_pep440_post(pieces): """TAG[.postDISTANCE[.dev0]+gHEX] . The ".dev0" means dirty. Note that .dev0 sorts backwards (a dirty tree will appear "older" than the corresponding clean one), but you shouldn't be releasing software with -dirty anyways. Exceptions: 1: no tags. 0.postDISTANCE[.dev0] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"] or pieces["dirty"]: rendered += ".post%d" % pieces["distance"] if pieces["dirty"]: rendered += ".dev0" rendered += plus_or_dot(pieces) rendered += "g%s" % pieces["short"] else: # exception #1 rendered = "0.post%d" % pieces["distance"] if pieces["dirty"]: rendered += ".dev0" rendered += "+g%s" % pieces["short"] return rendered def render_pep440_post_branch(pieces): """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] . The ".dev0" means not master branch. Exceptions: 1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"] or pieces["dirty"]: rendered += ".post%d" % pieces["distance"] if pieces["branch"] != "master": rendered += ".dev0" rendered += plus_or_dot(pieces) rendered += "g%s" % pieces["short"] if pieces["dirty"]: rendered += ".dirty" else: # exception #1 rendered = "0.post%d" % pieces["distance"] if pieces["branch"] != "master": rendered += ".dev0" rendered += "+g%s" % pieces["short"] if pieces["dirty"]: rendered += ".dirty" return rendered def render_pep440_old(pieces): """TAG[.postDISTANCE[.dev0]] . The ".dev0" means dirty. Exceptions: 1: no tags. 0.postDISTANCE[.dev0] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"] or pieces["dirty"]: rendered += ".post%d" % pieces["distance"] if pieces["dirty"]: rendered += ".dev0" else: # exception #1 rendered = "0.post%d" % pieces["distance"] if pieces["dirty"]: rendered += ".dev0" return rendered def render_git_describe(pieces): """TAG[-DISTANCE-gHEX][-dirty]. Like 'git describe --tags --dirty --always'. Exceptions: 1: no tags. HEX[-dirty] (note: no 'g' prefix) """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"]: rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) else: # exception #1 rendered = pieces["short"] if pieces["dirty"]: rendered += "-dirty" return rendered def render_git_describe_long(pieces): """TAG-DISTANCE-gHEX[-dirty]. Like 'git describe --tags --dirty --always -long'. The distance/hash is unconditional. Exceptions: 1: no tags. HEX[-dirty] (note: no 'g' prefix) """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) else: # exception #1 rendered = pieces["short"] if pieces["dirty"]: rendered += "-dirty" return rendered def render(pieces, style): """Render the given version pieces into the requested style.""" if pieces["error"]: return {"version": "unknown", "full-revisionid": pieces.get("long"), "dirty": None, "error": pieces["error"], "date": None} if not style or style == "default": style = "pep440" # the default if style == "pep440": rendered = render_pep440(pieces) elif style == "pep440-branch": rendered = render_pep440_branch(pieces) elif style == "pep440-pre": rendered = render_pep440_pre(pieces) elif style == "pep440-post": rendered = render_pep440_post(pieces) elif style == "pep440-post-branch": rendered = render_pep440_post_branch(pieces) elif style == "pep440-old": rendered = render_pep440_old(pieces) elif style == "git-describe": rendered = render_git_describe(pieces) elif style == "git-describe-long": rendered = render_git_describe_long(pieces) else: raise ValueError("unknown style '%s'" % style) return {"version": rendered, "full-revisionid": pieces["long"], "dirty": pieces["dirty"], "error": None, "date": pieces.get("date")} def get_versions(): """Get version information or return default if unable to do so.""" # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have # __file__, we can work backwards from there to the root. Some # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which # case we can only use expanded keywords. cfg = get_config() verbose = cfg.verbose try: return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, verbose) except NotThisMethod: pass try: root = os.path.realpath(__file__) # versionfile_source is the relative path from the top of the source # tree (where the .git directory might live) to this file. Invert # this to find the root from __file__. for _ in cfg.versionfile_source.split("/"): root = os.path.dirname(root) except NameError: return {"version": "0+unknown", "full-revisionid": None, "dirty": None, "error": "unable to find root of source tree", "date": None} try: pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) return render(pieces, cfg.style) except NotThisMethod: pass try: if cfg.parentdir_prefix: return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) except NotThisMethod: pass return {"version": "0+unknown", "full-revisionid": None, "dirty": None, "error": "unable to compute version", "date": None} svtplay-dl-4.97.1/lib/svtplay_dl/error.py000066400000000000000000000017261465165132400203620ustar00rootroot00000000000000# ex:ts=4:sw=4:sts=4:et # -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- class UIException(Exception): pass class ServiceError(Exception): pass class NoRequestedProtocols(UIException): """ This excpetion is thrown when the service provides streams, but not using any accepted protocol (as decided by options.stream_prio). """ def __init__(self, requested, found): """ The constructor takes two mandatory parameters, requested and found. Both should be lists. requested is the protocols we want and found is the protocols that can be used to access the stream. """ self.requested = requested self.found = found super().__init__(f"None of the provided protocols ({self.found}) are in the current list of accepted protocols ({self.requested})") def __repr__(self): return f"NoRequestedProtocols(requested={self.requested}, found={self.found})" svtplay-dl-4.97.1/lib/svtplay_dl/fetcher/000077500000000000000000000000001465165132400202715ustar00rootroot00000000000000svtplay-dl-4.97.1/lib/svtplay_dl/fetcher/__init__.py000066400000000000000000000024131465165132400224020ustar00rootroot00000000000000from svtplay_dl.utils.http import HTTP class VideoRetriever: def __init__(self, config, url, bitrate, output, **kwargs): self.config = config self.url = url self.bitrate = int(bitrate) if bitrate else 0 self.kwargs = kwargs self.http = HTTP(config) self.finished = False self.audio = kwargs.pop("audio", None) self.files = kwargs.pop("files", None) self.keycookie = kwargs.pop("keycookie", None) self.authorization = kwargs.pop("authorization", None) self.output = output self.segments = kwargs.pop("segments", None) self.output_extention = None channels = kwargs.pop("channels", None) codec = kwargs.pop("codec", "h264") self.resolution = kwargs.pop("resolution", "") self.language = kwargs.pop("language", "") self.audio_role = kwargs.pop("role", "main") self.video_role = kwargs.pop("video_role", "main") self.format = f"{codec}-{channels}" if channels else codec def __repr__(self): return f"" @property def name(self): pass svtplay-dl-4.97.1/lib/svtplay_dl/fetcher/dash.py000066400000000000000000000403451465165132400215700ustar00rootroot00000000000000# ex:ts=4:sw=4:sts=4:et # -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- import copy import math import os import re import time import xml.etree.ElementTree as ET from datetime import datetime from urllib.parse import urljoin from svtplay_dl.error import ServiceError from svtplay_dl.error import UIException from svtplay_dl.fetcher import VideoRetriever from svtplay_dl.subtitle import subtitle_probe from svtplay_dl.utils.output import ETA from svtplay_dl.utils.output import formatname from svtplay_dl.utils.output import progress_stream from svtplay_dl.utils.output import progressbar class DASHException(UIException): def __init__(self, url, message): self.url = url super().__init__(message) class LiveDASHException(DASHException): def __init__(self, url): super().__init__(url, "This is a live DASH stream, and they are not supported.") class DASHattibutes: def __init__(self): self.default = {} def set(self, key, value): self.default[key] = value def get(self, key): if key in self.default: return self.default[key] return 0 def templateelemt(attributes, element, filename, idnumber): files = [] init = element.attrib["initialization"] media = element.attrib["media"] if "startNumber" in element.attrib: start = int(element.attrib["startNumber"]) else: start = 1 if "timescale" in element.attrib: attributes.set("timescale", float(element.attrib["timescale"])) else: attributes.set("timescale", 1) if "duration" in element.attrib: attributes.set("duration", float(element.attrib["duration"])) segments = [] timeline = element.findall("{urn:mpeg:dash:schema:mpd:2011}SegmentTimeline/{urn:mpeg:dash:schema:mpd:2011}S") if timeline: t = -1 for s in timeline: duration = int(s.attrib["d"]) repeat = int(s.attrib["r"]) if "r" in s.attrib else 0 segmenttime = int(s.attrib["t"]) if "t" in s.attrib else 0 if t < 0: t = segmenttime count = repeat + 1 end = start + len(segments) + count number = start + len(segments) while number < end: segments.append({"number": number, "duration": math.ceil(duration / attributes.get("timescale")), "time": t}) t += duration number += 1 else: if attributes.get("type") == "static": end = math.ceil(attributes.get("mediaPresentationDuration") / (attributes.get("duration") / attributes.get("timescale"))) else: # Saw this on dynamic live content start = 0 now = time.time() periodStartWC = time.mktime(attributes.get("availabilityStartTime").timetuple()) + start periodEndWC = now + attributes.get("minimumUpdatePeriod") periodDuration = periodEndWC - periodStartWC segmentCount = math.ceil(periodDuration * attributes.get("timescale") / attributes.get("duration")) availableStart = math.floor( (now - periodStartWC - attributes.get("timeShiftBufferDepth")) * attributes.get("timescale") / attributes.get("duration"), ) availableEnd = math.floor((now - periodStartWC) * attributes.get("timescale") / attributes.get("duration")) start = max(0, availableStart) end = min(segmentCount, availableEnd) for number in range(start, end): segments.append({"number": number, "duration": int(attributes.get("duration") / attributes.get("timescale"))}) name = media.replace("$RepresentationID$", idnumber).replace("$Bandwidth$", attributes.get("bandwidth")) if init[:4] == "http": files.append(init.replace("$RepresentationID$", idnumber).replace("$Bandwidth$", attributes.get("bandwidth"))) else: files.append(urljoin(filename, init.replace("$RepresentationID$", idnumber).replace("$Bandwidth$", attributes.get("bandwidth")))) for segment in segments: if "$Time$" in media: new = name.replace("$Time$", str(segment["time"])) if "$Number" in name: if re.search(r"\$Number(\%\d+)d\$", name): vname = name.replace("$Number", "").replace("$", "") new = vname % segment["number"] else: new = name.replace("$Number$", str(segment["number"])) if new[:4] == "http": files.append(new) else: files.append(urljoin(filename, new)) return files def adaptionset(attributes, elements, url, baseurl=None): streams = [] dirname = os.path.dirname(url) + "/" if baseurl: dirname = urljoin(dirname, baseurl) for element in elements: role = "main" template = element.find("{urn:mpeg:dash:schema:mpd:2011}SegmentTemplate") represtation = element.findall(".//{urn:mpeg:dash:schema:mpd:2011}Representation") role_elemets = element.findall(".//{urn:mpeg:dash:schema:mpd:2011}Role") accessibility = element.findall(".//{urn:mpeg:dash:schema:mpd:2011}Accessibility[@schemeIdUri='urn:se:svt:accessibility']") codecs = None if "codecs" in element.attrib: codecs = element.attrib["codecs"] lang = "" if "lang" in element.attrib: lang = element.attrib["lang"] if role_elemets: role = role_elemets[0].attrib["value"] if accessibility: role = f'{role}-{accessibility[0].get("value")}' resolution = "" if "maxWidth" in element.attrib and "maxHeight" in element.attrib: resolution = f'{element.attrib["maxWidth"]}x{element.attrib["maxHeight"]}' for i in represtation: files = [] segments = False filename = dirname mimetype = None attributes.set("bandwidth", i.attrib["bandwidth"]) bitrate = int(i.attrib["bandwidth"]) / 1000 if "mimeType" in element.attrib: mimetype = element.attrib["mimeType"] idnumber = i.attrib["id"] channels = None codec = None if codecs is None and "codecs" in i.attrib: codecs = i.attrib["codecs"] if codecs and codecs[:3] == "avc": codec = "h264" elif codecs and codecs[:3] == "hvc": codec = "hevc" elif codecs and codecs[:3] == "dvh": codec = "dvhevc" else: codec = codecs if not resolution and "maxWidth" in i.attrib and "maxHeight" in i.attrib: resolution = f'{i.attrib["maxWidth"]}x{i.attrib["maxHeight"]}' elif not resolution and "width" in i.attrib and "height" in i.attrib: resolution = f'{i.attrib["width"]}x{i.attrib["height"]}' if i.find("{urn:mpeg:dash:schema:mpd:2011}AudioChannelConfiguration") is not None: chan = i.find("{urn:mpeg:dash:schema:mpd:2011}AudioChannelConfiguration").attrib["value"] if chan == "6": channels = "51" else: channels = None if i.find("{urn:mpeg:dash:schema:mpd:2011}BaseURL") is not None: filename = urljoin(filename, i.find("{urn:mpeg:dash:schema:mpd:2011}BaseURL").text) if i.find("{urn:mpeg:dash:schema:mpd:2011}SegmentBase") is not None: segments = True files.append(filename) if template is not None: segments = True files = templateelemt(attributes, template, filename, idnumber) elif i.find("{urn:mpeg:dash:schema:mpd:2011}SegmentTemplate") is not None: segments = True files = templateelemt(attributes, i.find("{urn:mpeg:dash:schema:mpd:2011}SegmentTemplate"), filename, idnumber) if mimetype == "text/vtt": files.append(filename) if files: streams.append( { "bitrate": bitrate, "segments": segments, "files": files, "codecs": codec, "channels": channels, "lang": lang, "mimetype": mimetype, "resolution": resolution, "role": role, }, ) resolution = "" return streams def dashparse(config, res, url, output, **kwargs): if not res: return if res.status_code >= 400: yield ServiceError(f"Can't read DASH playlist. {res.status_code}") if len(res.text) < 1: yield ServiceError(f"Can't read DASH playlist. {res.status_code}, size: {len(res.text)}") yield from _dashparse(config, res.text, url, output, cookies=res.cookies, **kwargs) def _dashparse(config, text, url, output, cookies, **kwargs): baseurl = None loutput = copy.copy(output) loutput["ext"] = "mp4" attributes = DASHattibutes() subtitles = [] text = re.sub("&(?!amp;)", "&", text) xml = ET.XML(text) if xml.find("./{urn:mpeg:dash:schema:mpd:2011}BaseURL") is not None: baseurl = xml.find("./{urn:mpeg:dash:schema:mpd:2011}BaseURL").text if "availabilityStartTime" in xml.attrib: attributes.set("availabilityStartTime", parse_dates(xml.attrib["availabilityStartTime"])) attributes.set("publishTime", parse_dates(xml.attrib["publishTime"])) if "mediaPresentationDuration" in xml.attrib: attributes.set("mediaPresentationDuration", parse_duration(xml.attrib["mediaPresentationDuration"])) if "timeShiftBufferDepth" in xml.attrib: attributes.set("timeShiftBufferDepth", parse_duration(xml.attrib["timeShiftBufferDepth"])) if "minimumUpdatePeriod" in xml.attrib: attributes.set("minimumUpdatePeriod", parse_duration(xml.attrib["minimumUpdatePeriod"])) attributes.set("type", xml.attrib["type"]) temp = xml.findall('.//{urn:mpeg:dash:schema:mpd:2011}AdaptationSet[@mimeType="audio/mp4"]') if len(temp) == 0: temp = xml.findall('.//{urn:mpeg:dash:schema:mpd:2011}AdaptationSet[@contentType="audio"]') audiofiles = adaptionset(attributes, temp, url, baseurl) temp = xml.findall('.//{urn:mpeg:dash:schema:mpd:2011}AdaptationSet[@mimeType="video/mp4"]') if len(temp) == 0: temp = xml.findall('.//{urn:mpeg:dash:schema:mpd:2011}AdaptationSet[@contentType="video"]') videofiles = adaptionset(attributes, temp, url, baseurl) temp = xml.findall('.//{urn:mpeg:dash:schema:mpd:2011}AdaptationSet[@contentType="text"]') if len(temp) == 0: temp = xml.findall('.//{urn:mpeg:dash:schema:mpd:2011}AdaptationSet[@mimeType="application/mp4"]') subtitles = adaptionset(attributes, temp, url, baseurl) if not audiofiles or not videofiles: yield ServiceError("Found no Audiofiles or Videofiles to download.") return if "channels" in kwargs: kwargs.pop("channels") if "codec" in kwargs: kwargs.pop("codec") for video in videofiles: for audio in audiofiles: bitrate = video["bitrate"] + audio["bitrate"] yield DASH( copy.copy(config), url, bitrate, cookies=cookies, audio=audio["files"], files=video["files"], output=loutput, segments=video["segments"], codec=video["codecs"], channels=audio["channels"], resolution=video["resolution"], language=audio["lang"], role=audio["role"], video_role=video["role"], **kwargs, ) for sub in subtitles: continue if len(subtitles) > 1: if sub["role"] and sub["role"] != "main" and sub["role"] != "subtitle": sub["lang"] = f'{sub["lang"]}-{sub["role"]}' yield from subtitle_probe(copy.copy(config), url, subfix=sub["lang"], output=copy.copy(loutput), files=sub["files"], **kwargs) def parse_duration(duration): match = re.search(r"P(?:(\d*)Y)?(?:(\d*)M)?(?:(\d*)D)?(?:T(?:(\d*)H)?(?:(\d*)M)?(?:([\d.]*)S)?)?", duration) if not match: return 0 year = int(match.group(1)) * 365 * 24 * 60 * 60 if match.group(1) else 0 month = int(match.group(2)) * 30 * 24 * 60 * 60 if match.group(2) else 0 day = int(match.group(3)) * 24 * 60 * 60 if match.group(3) else 0 hour = int(match.group(4)) * 60 * 60 if match.group(4) else 0 minute = int(match.group(5)) * 60 if match.group(5) else 0 second = float(match.group(6)) if match.group(6) else 0 return year + month + day + hour + minute + second def parse_dates(date_str): match = re.search(r"(.*:.*)\.(\d{5,9})Z", date_str) if match: date_str = f"{match.group(1)}.{int(int(match.group(2)) / 1000)}Z" # Need to translate nanoseconds to milliseconds date_patterns = ["%Y-%m-%dT%H:%M:%S.%fZ", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M:%SZ"] dt = None for pattern in date_patterns: try: dt = datetime.strptime(date_str, pattern) break except Exception: pass if not dt: raise ValueError(f"Can't parse date format: {date_str}") return dt class DASH(VideoRetriever): @property def name(self): return "dash" def download(self): self.output_extention = "mp4" if self.config.get("live") and not self.config.get("force"): raise LiveDASHException(self.url) if self.segments: if self.audio and not self.config.get("only_video"): self._download2(self.audio, audio=True) if not self.config.get("only_audio"): self._download2(self.files) else: if self.audio and not self.config.get("only_video"): self._download_url(self.audio, audio=True) if not self.config.get("only_audio"): self._download_url(self.url) def _download2(self, files, audio=False): cookies = self.kwargs["cookies"] if audio: self.output["ext"] = "m4a" else: self.output["ext"] = "mp4" filename = formatname(self.output, self.config) file_d = open(filename, "wb") eta = ETA(len(files)) n = 1 for i in files: if not self.config.get("silent"): eta.increment() progressbar(len(files), n, "".join(["ETA: ", str(eta)])) n += 1 data = self.http.request("get", i, cookies=cookies) if data.status_code == 404: break data = data.content file_d.write(data) file_d.close() if not self.config.get("silent"): progress_stream.write("\n") self.finished = True def _download_url(self, url, audio=False, total_size=None): cookies = self.kwargs["cookies"] data = self.http.request("get", url, cookies=cookies, headers={"Range": "bytes=0-8192"}) if not total_size: try: total_size = data.headers["Content-Range"] total_size = total_size[total_size.find("/") + 1 :] total_size = int(total_size) except KeyError: raise KeyError("Can't get the total size.") bytes_so_far = 8192 if audio: self.output["ext"] = "m4a" else: self.output["ext"] = "mp4" filename = formatname(self.output, self.config) file_d = open(filename, "wb") file_d.write(data.content) eta = ETA(total_size) while bytes_so_far < total_size: if not self.config.get("silent"): eta.update(bytes_so_far) progressbar(total_size, bytes_so_far, "".join(["ETA: ", str(eta)])) old = bytes_so_far + 1 bytes_so_far = total_size bytes_range = f"bytes={old}-{bytes_so_far}" data = self.http.request("get", url, cookies=cookies, headers={"Range": bytes_range}) file_d.write(data.content) file_d.close() progressbar(bytes_so_far, total_size, "ETA: complete") # progress_stream.write('\n') self.finished = True svtplay-dl-4.97.1/lib/svtplay_dl/fetcher/hls.py000066400000000000000000000357451465165132400214470ustar00rootroot00000000000000# ex:ts=4:sw=4:sts=4:et # -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- import binascii import copy import os import struct import time from datetime import datetime from datetime import timedelta from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.ciphers import algorithms from cryptography.hazmat.primitives.ciphers import Cipher from cryptography.hazmat.primitives.ciphers import modes from svtplay_dl.error import ServiceError from svtplay_dl.error import UIException from svtplay_dl.fetcher import VideoRetriever from svtplay_dl.fetcher.m3u8 import M3U8 from svtplay_dl.subtitle import subtitle_probe from svtplay_dl.utils.fetcher import filter_files from svtplay_dl.utils.http import get_full_url from svtplay_dl.utils.output import ETA from svtplay_dl.utils.output import formatname from svtplay_dl.utils.output import progress_stream from svtplay_dl.utils.output import progressbar class HLSException(UIException): def __init__(self, url, message): self.url = url super().__init__(message) class LiveHLSException(HLSException): def __init__(self, url): super().__init__(url, "This is a live HLS stream, and they are not supported.") def hlsparse(config, res, url, output, **kwargs): if not res: return if res.status_code > 400: yield ServiceError(f"Can't read HLS playlist. {res.status_code}") return yield from _hlsparse(config, res.text, url, output, cookies=res.cookies, **kwargs) def _hlsparse(config, text, url, output, **kwargs): m3u8 = M3U8(text) keycookie = kwargs.pop("keycookie", None) cookies = kwargs.pop("cookies", None) authorization = kwargs.pop("authorization", None) loutput = copy.copy(output) loutput["ext"] = "ts" channels = kwargs.pop("channels", None) codec = kwargs.pop("codec", "h264") media = {} subtitles = {} videos = {} segments = None if m3u8.master_playlist: video_group = None for i in m3u8.master_playlist: audio_url = None vcodec = None chans = None audio_group = None language = "" resolution = "" if i["TAG"] == "EXT-X-MEDIA": if i["TYPE"] and i["TYPE"] != "SUBTITLES": uri = None if i["GROUP-ID"] not in media: media[i["GROUP-ID"]] = [] if i["TYPE"] == "VIDEO": video_group = i["GROUP-ID"] if "URI" in i: if segments is None: segments = True uri = i["URI"] else: segments = False if "CHANNELS" in i: if i["CHANNELS"] == "6": chans = "51" if "LANGUAGE" in i: language = i["LANGUAGE"] if "AUTOSELECT" in i and i["AUTOSELECT"].upper() == "YES" and "DEFAULT" in i and i["DEFAULT"].upper() == "YES": role = "main" else: role = "alternate" if "CHARACTERISTICS" in i: role = f'{role}-{i["CHARACTERISTICS"].replace("se.svt.accessibility.", "")}' media[i["GROUP-ID"]].append([uri, chans, language, role]) if i["TYPE"] == "SUBTITLES": if "URI" in i: caption = None if i["GROUP-ID"] not in subtitles: subtitles[i["GROUP-ID"]] = [] if "LANGUAGE" in i: lang = i["LANGUAGE"] else: lang = "und" if "CHARACTERISTICS" in i: caption = True item = [i["URI"], lang, caption] if item not in subtitles[i["GROUP-ID"]]: subtitles[i["GROUP-ID"]].append(item) continue elif i["TAG"] == "EXT-X-STREAM-INF": if "AVERAGE-BANDWIDTH" in i: bit_rate = float(i["AVERAGE-BANDWIDTH"]) / 1000 else: bit_rate = float(i["BANDWIDTH"]) / 1000 if "RESOLUTION" in i: resolution = i["RESOLUTION"] if "CODECS" in i: if i["CODECS"][:3] == "hvc": vcodec = "hevc" if i["CODECS"][:3] == "avc": vcodec = "h264" if i["CODECS"][:3] == "dvh": vcodec = "dvhevc" if "AUDIO" in i: audio_group = i["AUDIO"] urls = get_full_url(i["URI"], url) videos[bit_rate] = [urls, resolution, vcodec, audio_group, video_group] else: continue # Needs to be changed to utilise other tags. for bit_rate in list(videos.keys()): urls, resolution, vcodec, audio_group, video_group = videos[bit_rate] if audio_group and media: if video_group and video_group in media: for vgroup in media[video_group]: vurl = urls video_role = vgroup[3] if vgroup[0]: vurl = vgroup[0] for group in media[audio_group]: audio_url = get_full_url(group[0], url) chans = group[1] if audio_url else channels codec = vcodec if vcodec else codec yield HLS( copy.copy(config), vurl, bit_rate, cookies=cookies, keycookie=keycookie, authorization=authorization, audio=audio_url, role=group[3], video_role=video_role, output=loutput, segments=bool(segments), channels=chans, codec=codec, resolution=resolution, language=group[2], **kwargs, ) else: vurl = urls video_role = "main" for group in media[audio_group]: if group[0]: audio_url = get_full_url(group[0], url) chans = group[1] if audio_url else channels codec = vcodec if vcodec else codec yield HLS( copy.copy(config), vurl, bit_rate, cookies=cookies, keycookie=keycookie, authorization=authorization, audio=audio_url, video_role=video_role, output=loutput, segments=bool(segments), channels=chans, codec=codec, resolution=resolution, language=group[2], role=group[3], **kwargs, ) else: chans = channels codec = vcodec if vcodec else codec yield HLS( copy.copy(config), urls, bit_rate, cookies=cookies, keycookie=keycookie, authorization=authorization, audio=audio_url, output=loutput, segments=bool(segments), channels=chans, codec=codec, resolution=resolution, **kwargs, ) if subtitles: for sub in list(subtitles.keys()): for n in subtitles[sub]: subfix = n[1] if len(subtitles[sub]) > 1: if subfix: subfix = f"{n[1]}-caption" yield from subtitle_probe( copy.copy(config), get_full_url(n[0], url), output=copy.copy(output), subfix=subfix, cookies=cookies, **kwargs, ) elif m3u8.media_segment: config.set("segments", False) yield HLS( copy.copy(config), url, 0, cookies=cookies, keycookie=keycookie, authorization=authorization, output=loutput, segments=False, ) else: yield ServiceError("Can't find HLS playlist in m3u8 file.") class HLS(VideoRetriever): @property def name(self): return "hls" def download(self): self.output_extention = "ts" if self.segments: if self.audio and not self.config.get("only_video"): # self._download(self.audio, file_name=(copy.copy(self.output), "audio.ts")) self._download(self.audio, True) if not self.audio or not self.config.get("only_audio"): self._download(self.url) else: # Ignore audio self.audio = None self._download(self.url) def _download(self, url, audio=False): cookies = self.kwargs.get("cookies", None) start_time = time.time() m3u8 = M3U8(self.http.request("get", url, cookies=cookies).text) key = None def random_iv(): return os.urandom(16) if audio: self.output["ext"] = "audio.ts" else: self.output["ext"] = "ts" filename = formatname(self.output, self.config) file_d = open(filename, "wb") hls_time_stamp = self.kwargs.pop("hls_time_stamp", False) if self.kwargs.get("filter", False): m3u8 = filter_files(m3u8) decryptor = None size_media = len(m3u8.media_segment) eta = ETA(size_media) total_duration = 0 duration = 0 max_duration = 0 key = None key_iv = None for index, i in enumerate(m3u8.media_segment): if "EXTINF" in i and "duration" in i["EXTINF"]: duration = i["EXTINF"]["duration"] max_duration = max(max_duration, duration) total_duration += duration item = get_full_url(i["URI"], url) if not self.config.get("silent"): if self.config.get("live"): progressbar(size_media, index + 1, "".join(["DU: ", str(timedelta(seconds=int(total_duration)))])) else: eta.increment() progressbar(size_media, index + 1, "".join(["ETA: ", str(eta)])) headers = {} if "EXT-X-BYTERANGE" in i: headers["Range"] = f'bytes={i["EXT-X-BYTERANGE"]["o"]}-{i["EXT-X-BYTERANGE"]["o"] + i["EXT-X-BYTERANGE"]["n"] - 1}' resb = self.http.request("get", item, cookies=cookies, headers=headers) if resb.status_code == 404: break data = resb.content if m3u8.encrypted: headers = {} if self.keycookie: keycookies = self.keycookie else: keycookies = cookies if self.authorization: headers["authorization"] = self.authorization # Update key/decryptor if "EXT-X-KEY" in i: keyurl = get_full_url(i["EXT-X-KEY"]["URI"], url) if keyurl and keyurl[:4] == "skd:": raise HLSException(keyurl, "Can't decrypt beacuse of DRM") key = self.http.request("get", keyurl, cookies=keycookies, headers=headers).content key_iv = binascii.unhexlify(i["EXT-X-KEY"]["IV"][2:].zfill(32)) if "IV" in i["EXT-X-KEY"] else None if key: iv = key_iv if key_iv else struct.pack(">8xq", index) backend = default_backend() cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=backend) decryptor = cipher.decryptor() # In some cases the playlist say its encrypted but the files is not. # This happen on svtplay 5.1ch stream where it started with ID3.. # Adding the other ones is header for mpeg-ts files. third byte is 10 or 11.. if data[:3] != b"ID3" and data[:3] != b"\x47\x40\x11" and data[:3] != b"\x47\x40\x10" and data[4:12] != b"ftyp": if decryptor: data = _unpad(decryptor.update(data)) else: if key: raise ValueError("No decryptor found for encrypted hls steam.") file_d.write(data) if self.config.get("capture_time") > 0 and total_duration >= self.config.get("capture_time") * 60: break if (size_media == (index + 1)) and self.config.get("live"): sleep_int = (start_time + max_duration * 2) - time.time() if sleep_int > 0: time.sleep(sleep_int) size_media_old = size_media while size_media_old == size_media: start_time = time.time() if hls_time_stamp: end_time_stamp = (datetime.utcnow() - timedelta(minutes=1, seconds=max_duration * 2)).replace(microsecond=0) start_time_stamp = end_time_stamp - timedelta(minutes=1) base_url = url.split(".m3u8")[0] url = f"{base_url}.m3u8?in={start_time_stamp.isoformat()}&out={end_time_stamp.isoformat()}?" new_m3u8 = M3U8(self.http.request("get", url, cookies=cookies).text) for n_m3u in new_m3u8.media_segment: if not any(d["URI"] == n_m3u["URI"] for d in m3u8.media_segment): m3u8.media_segment.append(n_m3u) size_media = len(m3u8.media_segment) if size_media_old == size_media: time.sleep(max_duration) file_d.close() if not self.config.get("silent"): progress_stream.write("\n") self.finished = True def _unpad(data): return data[: -data[-1]] svtplay-dl-4.97.1/lib/svtplay_dl/fetcher/http.py000066400000000000000000000024471465165132400216310ustar00rootroot00000000000000# ex:ts=4:sw=4:sts=4:et # -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- import os from svtplay_dl.fetcher import VideoRetriever from svtplay_dl.utils.output import ETA from svtplay_dl.utils.output import formatname from svtplay_dl.utils.output import progressbar class HTTP(VideoRetriever): @property def name(self): return "http" def download(self): """Get the stream from HTTP""" _, ext = os.path.splitext(self.url) if ext == ".mp3": self.output["ext"] = "mp3" else: self.output["ext"] = "mp4" # this might be wrong.. data = self.http.request("get", self.url, stream=True) try: total_size = data.headers["content-length"] except KeyError: total_size = 0 total_size = int(total_size) bytes_so_far = 0 filename = formatname(self.output, self.config) file_d = open(filename, "wb") eta = ETA(total_size) for i in data.iter_content(8192): bytes_so_far += len(i) file_d.write(i) if not self.config.get("silent"): eta.update(bytes_so_far) progressbar(total_size, bytes_so_far, "".join(["ETA: ", str(eta)])) file_d.close() self.finished = True svtplay-dl-4.97.1/lib/svtplay_dl/fetcher/m3u8.py000066400000000000000000000216571465165132400214520ustar00rootroot00000000000000import re class M3U8: # Created for hls version <=7 # https://tools.ietf.org/html/rfc8216 MEDIA_SEGMENT_TAGS = ("EXTINF", "EXT-X-BYTERANGE", "EXT-X-DISCONTINUITY", "EXT-X-KEY", "EXT-X-MAP", "EXT-X-PROGRAM-DATE-TIME", "EXT-X-DATERANGE") MEDIA_PLAYLIST_TAGS = ( "EXT-X-TARGETDURATION", "EXT-X-MEDIA-SEQUENCE", "EXT-X-DISCONTINUITY-SEQUENCE", "EXT-X-ENDLIST", "EXT-X-PLAYLIST-TYPE", "EXT-X-I-FRAMES-ONLY", ) MASTER_PLAYLIST_TAGS = ("EXT-X-MEDIA", "EXT-X-STREAM-INF", "EXT-X-I-FRAME-STREAM-INF", "EXT-X-SESSION-DATA", "EXT-X-SESSION-KEY") MEDIA_OR_MASTER_PLAYLIST_TAGS = ("EXT-X-INDEPENDENT-SEGMENTS", "EXT-X-START") TAG_TYPES = {"MEDIA_SEGMENT": 0, "MEDIA_PLAYLIST": 1, "MASTER_PLAYLIST": 2} def __init__(self, data): self.version = None self.media_segment = [] self.media_playlist = {} self.master_playlist = [] self.encrypted = False self.independent_segments = False self.parse_m3u(data) def __str__(self): return ( f"Version: {self.version}\nMedia Segment: {self.media_segment}\n" f"Media Playlist: {self.media_playlist}\nMaster Playlist: {self.master_playlist}\n" f"Encrypted: {self.encrypted}\tIndependent_segments: {self.independent_segments}" ) def parse_m3u(self, data): if not data.startswith("#EXTM3U"): raise ValueError("Does not appear to be an 'EXTM3U' file.") data = data.replace("\r\n", "\n") lines = data.split("\n")[1:] last_tag_type = None tag_type = None media_segment_info = {} for index, l in enumerate(lines): if not l: continue elif l.startswith("#EXT"): info = {} tag, attr = _get_tag_attribute(l) if tag == "EXT-X-VERSION": self.version = int(attr) # 4.3.2. Media Segment Tags elif tag in M3U8.MEDIA_SEGMENT_TAGS: tag_type = M3U8.TAG_TYPES["MEDIA_SEGMENT"] # 4.3.2.1. EXTINF if tag == "EXTINF": if "," in attr: dur, title = attr.split(",", 1) else: dur = attr title = None info["duration"] = float(dur) info["title"] = title # 4.3.2.2. EXT-X-BYTERANGE elif tag == "EXT-X-BYTERANGE": if "@" in attr: n, o = attr.split("@", 1) info["n"], info["o"] = (int(n), int(o)) else: info["n"] = int(attr) info["o"] = 0 # 4.3.2.3. EXT-X-DISCONTINUITY elif tag == "EXT-X-DISCONTINUITY": pass # 4.3.2.4. EXT-X-KEY elif tag == "EXT-X-KEY": self.encrypted = True info = _get_tuple_attribute(attr) if "URI" not in info: self.encrypted = False # 4.3.2.5. EXT-X-MAP elif tag == "EXT-X-MAP": info = _get_tuple_attribute(attr) if "BYTERANGE" in info: if "@" in info["BYTERANGE"]: n, o = info["BYTERANGE"].split("@", 1) info["EXT-X-BYTERANGE"] = {} info["EXT-X-BYTERANGE"]["n"], info["EXT-X-BYTERANGE"]["o"] = (int(n), int(o)) else: info["EXT-X-BYTERANGE"] = {} info["EXT-X-BYTERANGE"]["n"] = int(attr) info["EXT-X-BYTERANGE"]["o"] = 0 if "BYTERANGE" not in info: info["EXTINF"] = {} info["EXTINF"]["duration"] = 0 self.media_segment.insert(0, info) # 4.3.2.6. EXT-X-PROGRAM-DATE-TIME" elif tag == "EXT-X-PROGRAM-DATE-TIME": info = attr # 4.3.2.7. EXT-X-DATERANGE elif tag == "EXT-X-DATERANGE": info = _get_tuple_attribute(attr) media_segment_info[tag] = info # 4.3.3. Media Playlist Tags elif tag in M3U8.MEDIA_PLAYLIST_TAGS: tag_type = M3U8.TAG_TYPES["MEDIA_PLAYLIST"] # 4.3.3.1. EXT-X-TARGETDURATION if tag == "EXT-X-TARGETDURATION": info = int(attr) # 4.3.3.2. EXT-X-MEDIA-SEQUENCE elif tag == "EXT-X-MEDIA-SEQUENCE": info = int(attr) # 4.3.3.3. EXT-X-DISCONTINUITY-SEQUENCE elif tag == "EXT-X-DISCONTINUITY-SEQUENCE": info = int(attr) # 4.3.3.4. EXT-X-ENDLIST elif tag == "EXT-X-ENDLIST": break # 4.3.3.5. EXT-X-PLAYLIST-TYPE elif tag == "EXT-X-PLAYLIST-TYPE": info = attr # 4.3.3.6. EXT-X-I-FRAMES-ONLY elif tag == "EXT-X-I-FRAMES-ONLY": pass self.media_playlist[tag] = info # 4.3.4. Master Playlist Tags elif tag in M3U8.MASTER_PLAYLIST_TAGS: tag_type = M3U8.TAG_TYPES["MASTER_PLAYLIST"] # 4.3.4.1. EXT-X-MEDIA if tag == "EXT-X-MEDIA": info = _get_tuple_attribute(attr) # 4.3.4.2. EXT-X-STREAM-INF elif tag == "EXT-X-STREAM-INF": info = _get_tuple_attribute(attr) if "BANDWIDTH" not in info: raise ValueError("Can't find 'BANDWIDTH' in 'EXT-X-STREAM-INF'") info["URI"] = lines[index + 1] # 4.3.4.3. EXT-X-I-FRAME-STREAM-INF elif tag == "EXT-X-I-FRAME-STREAM-INF": info = _get_tuple_attribute(attr) # 4.3.4.4. EXT-X-SESSION-DATA elif tag == "EXT-X-SESSION-DATA": info = _get_tuple_attribute(attr) # 4.3.4.5. EXT-X-SESSION-KEY elif tag == "EXT-X-SESSION-KEY": self.encrypted = True info = _get_tuple_attribute(attr) info["TAG"] = tag self.master_playlist.append(info) # 4.3.5. Media or Master Playlist Tags elif tag in M3U8.MEDIA_OR_MASTER_PLAYLIST_TAGS: tag_type = M3U8.TAG_TYPES["MEDIA_PLAYLIST"] # 4.3.5.1. EXT-X-INDEPENDENT-SEGMENTS if tag == "EXT-X-INDEPENDENT-SEGMENTS": self.independent_segments = True # 4.3.5.2. EXT-X-START elif tag == "EXT-X-START": info = _get_tuple_attribute(attr) self.media_playlist[tag] = info # Unused tags else: pass # This is a comment elif l.startswith("#"): pass # This must be a url/uri else: tag_type = None if last_tag_type is M3U8.TAG_TYPES["MEDIA_SEGMENT"]: media_segment_info["URI"] = l self.media_segment.append(media_segment_info) media_segment_info = {} last_tag_type = tag_type if self.media_segment and self.master_playlist: raise ValueError("This 'M3U8' file contains data for both 'Media Segment' and 'Master Playlist'. This is not allowed.") def _get_tag_attribute(line): line = line[1:] try: search_line = re.search(r"^([A-Z\-]*):(.*)", line) return search_line.group(1), search_line.group(2) except Exception: return line, None def _get_tuple_attribute(attribute): attr_tuple = {} for art_l in re.split(""",(?=(?:[^'"]|'[^']*'|"[^"]*")*$)""", attribute): if art_l: name, value = art_l.split("=", 1) name = name.strip() # Checks for attribute name if not re.match(r"^[A-Z0-9\-]*$", name): raise ValueError("Not a valid attribute name.") # Remove extra quotes of string if value.startswith('"') and value.endswith('"'): value = value[1:-1] attr_tuple[name] = value return attr_tuple svtplay-dl-4.97.1/lib/svtplay_dl/log.py000066400000000000000000000002671465165132400200110ustar00rootroot00000000000000# ex:ts=4:sw=4:sts=4:et # -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- import logging import sys log = logging.getLogger("svtplay_dl") progress_stream = sys.stderr svtplay-dl-4.97.1/lib/svtplay_dl/postprocess/000077500000000000000000000000001465165132400212355ustar00rootroot00000000000000svtplay-dl-4.97.1/lib/svtplay_dl/postprocess/__init__.py000066400000000000000000000223771465165132400233610ustar00rootroot00000000000000import logging import os import pathlib import platform import re import sys from random import sample from shutil import which from requests import codes from requests import post from requests import Timeout from svtplay_dl.utils.http import FIREFOX_UA from svtplay_dl.utils.output import formatname from svtplay_dl.utils.proc import run_program from svtplay_dl.utils.stream import subtitle_filter class postprocess: def __init__(self, stream, config, subfixes=None): self.stream = stream self.config = config self.subfixes = [x.subfix for x in subtitle_filter(subfixes)] self.detect = None for i in ["ffmpeg", "avconv"]: self.detect = which(i) if self.detect: break if self.detect is None and platform.system() == "Windows": path = pathlib.Path(sys.executable).parent / "ffmpeg.exe" if os.path.isfile(path): self.detect = path def merge(self): if self.detect is None: logging.error("Cant detect ffmpeg or avconv. Cant mux files without it.") return if self.stream.finished is False: return orig_filename = formatname(self.stream.output, self.config) ext = orig_filename.suffix new_name = orig_filename.with_suffix(f".{self.config.get('output_format')}") if ext == ".ts": if self.stream.audio: if not str(orig_filename).endswith(".audio.ts"): audio_filename = orig_filename.with_suffix(".audio.ts") else: audio_filename = orig_filename else: audio_filename = orig_filename.with_suffix(".m4a") cmd = [self.detect] if (self.config.get("only_video") or not self.config.get("only_audio")) or (not self.stream.audio and self.config.get("only_audio")): cmd += ["-i", str(orig_filename)] if self.stream.audio: cmd += ["-i", str(audio_filename)] _, _, stderr = run_program(cmd, False) # return 1 is good here. streams = _streams(stderr) videotrack, audiotrack = _checktracks(streams) if self.config.get("merge_subtitle"): logging.info("Merge audio, video and subtitle into %s", new_name.name) else: logging.info(f"Merge audio and video into {str(new_name.name).replace('.audio', '')}") tempfile = orig_filename.with_suffix(".temp") arguments = [] if self.config.get("only_audio"): arguments += ["-vn"] if self.config.get("only_video"): arguments += ["-an"] arguments += ["-c:v", "copy", "-c:a", "copy", "-f", "matroska" if self.config.get("output_format") == "mkv" else "mp4"] if ext == ".ts": if audiotrack and "aac" in _getcodec(streams, audiotrack): arguments += ["-bsf:a", "aac_adtstoasc"] if videotrack and "dvh1" in _getcodec(streams, videotrack): if self.config.get("output_format") == "mkv": logging.warning("HDR and mkv is not supported.") arguments += ["-strict", "unofficial"] cmd = [self.detect] if self.config.get("only_video") or (not self.config.get("only_audio") or (not self.stream.audio and self.config.get("only_audio"))): cmd += ["-i", str(orig_filename)] if self.stream.audio: cmd += ["-i", str(audio_filename)] if videotrack: arguments += ["-map", f"{videotrack}"] if audiotrack: arguments += ["-map", f"{audiotrack}"] if self.config.get("merge_subtitle"): langs = _sublanguage(self.stream, self.config, self.subfixes) tracks = [x for x in [videotrack, audiotrack] if x] subs_nr = 0 sub_start = 0 # find what sub track to start with. when a/v is in one file it start with 1 # if seperate it will start with 2 for i in tracks: if int(i[0]) >= sub_start: sub_start += 1 for stream_num, language in enumerate(langs, start=sub_start): arguments += [ "-map", f"{str(stream_num)}:0", "-c:s:" + str(subs_nr), "mov_text" if self.config.get("output_format") == "mp4" else "copy", "-metadata:s:s:" + str(subs_nr), "language=" + language, ] subs_nr += 1 if self.subfixes and self.config.get("get_all_subtitles"): for subfix in self.subfixes: subfile = orig_filename.parent / (orig_filename.stem + "." + subfix + ".srt") cmd += ["-i", str(subfile)] else: subfile = orig_filename.with_suffix(".srt") cmd += ["-i", str(subfile)] arguments += ["-y", str(tempfile)] cmd += arguments returncode, stdout, stderr = run_program(cmd) if returncode != 0: return if self.config.get("keep_original") is True: logging.info("Merging done, keeping original files.") os.rename(tempfile, orig_filename) return logging.info("Merging done, removing old files.") if self.config.get("only_video") or (not self.config.get("only_audio") or (not self.stream.audio and self.config.get("only_audio"))): os.remove(orig_filename) if (self.stream.audio and self.config.get("only_audio")) or (self.stream.audio and not self.config.get("only_video")): os.remove(audio_filename) # This if statement is for use cases where both -S and -M are specified to not only merge the subtitle but also store it separately. if self.config.get("merge_subtitle") and not self.config.get("subtitle"): if self.subfixes and len(self.subfixes) >= 2 and self.config.get("get_all_subtitles"): for subfix in self.subfixes: subfile = orig_filename.parent / (orig_filename.stem + "." + subfix + ".srt") os.remove(subfile) else: os.remove(subfile) os.rename(tempfile, str(new_name).replace(".audio", "")) def _streams(output): return re.findall(r"Stream \#(\d:\d)([\[\(][^:\[]+[\]\)])?([\(\)\w]+)?: (Video|Audio): (.*)", output) def _getcodec(streams, number): for stream in streams: if stream[0] == number: return stream[4] return None def _checktracks(streams): videotrack = None audiotrack = None for stream in streams: if stream[3] == "Video": videotrack = stream[0] if stream[3] == "Audio": if stream[4] == "mp3, 0 channels": continue audiotrack = stream[0] return videotrack, audiotrack def _sublanguage(stream, config, subfixes): # parse() function partly borrowed from a guy on github. /thanks! # https://github.com/riobard/srt.py/blob/master/srt.py def parse(self): def parse_block(block): lines = block.strip("-").split("\n") txt = "\r\n".join(lines[2:]) return txt if platform.system() == "Windows": fd = open(self, encoding="utf8") else: fd = open(self) return list(map(parse_block, fd.read().strip().replace("\r", "").split("\n\n"))) def query(self): _ = parse(self) random_sentences = " ".join(sample(_, len(_) if len(_) < 8 else 8)).replace("\r\n", "") url = "https://svtplay-dl.se/langdetect/" bits = "64" if sys.maxsize > 2**32 else "32" headers = {"User-Agent": f"{FIREFOX_UA} {platform.machine()} {platform.platform()} {bits}"} try: r = post(url, json={"query": random_sentences}, headers=headers, timeout=30) if r.status_code == codes.ok: try: response = r.json() return response["language"] except TypeError: return "und" else: logging.error("Server error appeared. Setting language as undetermined.") return "und" except Timeout: logging.error("30 seconds server timeout reached. Setting language as undetermined.") return "und" langs = [] exceptions = {"lulesamiska": "smj", "meankieli": "fit", "jiddisch": "yid"} logging.info("Determining the language of the subtitle(s).") if config.get("get_all_subtitles"): for subfix in subfixes: if [exceptions[key] for key in exceptions.keys() if re.match(key, subfix.strip("-"))]: if "oversattning" in subfix.strip("-"): subfix = subfix.strip("-").split(".")[0] else: subfix = subfix.strip("-") langs += [exceptions[subfix]] continue sfile = formatname(stream.output, config) subfile = sfile.parent / (sfile.stem + "." + subfix + ".srt") langs += [query(subfile)] else: subfile = formatname(stream.output, config).with_suffix(".srt") langs += [query(subfile)] if len(langs) >= 2: logging.info("Language codes: %s", ", ".join(langs)) else: logging.info("Language code: %s", langs[0]) return langs svtplay-dl-4.97.1/lib/svtplay_dl/service/000077500000000000000000000000001465165132400203115ustar00rootroot00000000000000svtplay-dl-4.97.1/lib/svtplay_dl/service/__init__.py000066400000000000000000000165251465165132400224330ustar00rootroot00000000000000# ex:ts=4:sw=4:sts=4:et # -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- import logging import os import re from urllib.parse import urlparse from svtplay_dl.utils.http import download_thumbnails from svtplay_dl.utils.http import HTTP from svtplay_dl.utils.parser import merge from svtplay_dl.utils.parser import readconfig from svtplay_dl.utils.parser import setup_defaults class Service: supported_domains = [] supported_domains_re = [] def __init__(self, config, _url, http=None): self._url = _url self._urldata = None self._error = False self.subtitle = None self.cookies = {} self.auto_name = None self.output = { "title": None, "season": None, "episode": None, "episodename": None, "id": None, "service": self.__class__.__name__.lower(), "tvshow": None, "title_nice": None, "showdescription": None, "episodedescription": None, "showthumbnailurl": None, "episodethumbnailurl": None, "publishing_datetime": None, "language": None, "ext": None, } # Config if config.get("configfile") and os.path.isfile(config.get("configfile")): self.config = merge( readconfig(setup_defaults(), config.get("configfile"), service=self.__class__.__name__.lower()).get_variable(), config.get_variable(), ) else: self.config = config if not http: self.http = HTTP(self.config) else: self.http = http logging.debug("service: %s", self.__class__.__name__.lower()) @property def url(self): return self._url def get_urldata(self): if self._urldata is None: self._urldata = self.http.request("get", self.url).text return self._urldata @classmethod def handles(cls, url): urlp = urlparse(url) # Apply supported_domains_re regexp to the netloc. This # is meant for 'dynamic' domains, e.g. containing country # information etc. for domain_re in [re.compile(x) for x in cls.supported_domains_re]: if domain_re.match(urlp.netloc): return True if urlp.netloc in cls.supported_domains: return True # For every listed domain, try with www.subdomain as well. if urlp.netloc in ["www." + x for x in cls.supported_domains]: return True return False def get_subtitle(self, options): pass # the options parameter is unused, but is part of the # interface, so we don't want to remove it. Thus, the # pylint ignore. def find_all_episodes(self, options): # pylint: disable-msg=unused-argument logging.warning("--all-episodes not implemented for this service") return [self.url] def opengraph_get(html, prop): """ Extract specified OpenGraph property from html. >>> opengraph_get('>> opengraph_get('>> opengraph_get(']*property="og:' + prop + '" content="([^"]*)"', html) if match is None: match = re.search(']*content="([^"]*)" property="og:' + prop + '"', html) if match is None: return None return match.group(1) class OpenGraphThumbMixin: """ Mix this into the service class to grab thumbnail from OpenGraph properties. """ def get_thumbnail(self, options): url = opengraph_get(self.get_urldata(), "image") if url is None: return download_thumbnails(self.output, options, [(False, url)]) class MetadataThumbMixin: """ Mix this into the service class to grab thumbnail from extracted metadata. """ def get_thumbnail(self, options): urls = [] if self.output["showthumbnailurl"] is not None: urls.append((True, self.output["showthumbnailurl"])) if self.output["episodethumbnailurl"] is not None: urls.append((False, self.output["episodethumbnailurl"])) if urls: download_thumbnails(self.output, options, urls) class Generic(Service): """Videos embed in sites""" def get(self, sites): data = self.http.request("get", self.url).text return self._match(data, sites) def _match(self, data, sites): match = re.search(r"src=(\"|\')(http://www.svt.se/wd[^\'\"]+)(\"|\')", data) stream = None if match: url = match.group(2) for i in sites: if i.handles(url): url = url.replace("&", "&").replace("&", "&") return url, i(self.config, url) matchlist = [ r"src=\"(https://player.vimeo.com/video/[0-9]+)\" ", r'src="(http://tv.aftonbladet[^"]*)"', r'a href="(http://tv.aftonbladet[^"]*)" class="abVi', r"iframe src='(http://www.svtplay[^']*)'", 'src="(http://mm-resource-service.herokuapp.com[^"]*)"', r'src="([^.]+\.solidtango.com[^"+]+)"', 's.src="(https://csp-ssl.picsearch.com[^"]+|http://csp.picsearch.com/rest[^"]+)', ] for i in matchlist: match = re.search(i, data) if match: url = match.group(1) for n in sites: if n.handles(match.group(1)): return match.group(1), n(self.config, url) match = re.search(r"tv4play.se/iframe/video/(\d+)?", data) if match: url = f"http://www.tv4play.se/?video_id={match.group(1)}" for i in sites: if i.handles(url): return url, i(self.config, url) match = re.search("(lemonwhale|lwcdn.com)", data) if match: url = "http://lemonwhale.com" for i in sites: if i.handles(url): return self.url, i(self.config, self.url) match = re.search("cdn.screen9.com", data) if match: url = "https://cdn.screen9.com" for i in sites: if i.handles(url): return self.url, i(self.config, self.url) match = re.search('iframe src="(//csp.screen9.com[^"]+)"', data) if match: url = f"https:{match.group(1)}" for i in sites: if i.handles(url): return self.url, i(self.config, self.url) match = re.search('source src="([^"]+)" type="application/x-mpegURL"', data) if match: for i in sites: if i.__name__ == "Raw": return self.url, i(self.config, match.group(1)) return self.url, stream def service_handler(sites, options, url): handler = None for i in sites: if i.handles(url): handler = i(options, url) break return handler svtplay-dl-4.97.1/lib/svtplay_dl/service/aftonbladet.py000066400000000000000000000043571465165132400231570ustar00rootroot00000000000000# ex:ts=4:sw=4:sts=4:et # -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- import json import re from svtplay_dl.error import ServiceError from svtplay_dl.fetcher.hls import hlsparse from svtplay_dl.service import Service from svtplay_dl.utils.text import decode_html_entities class Aftonbladettv(Service): supported_domains = ["svd.se", "tv.aftonbladet.se"] def get(self): data = self.get_urldata() match = re.search('data-player-config="([^"]+)"', data) if not match: match = re.search('data-svpPlayer-video="([^"]+)"', data) if not match: match = re.search("window.ASSET = ({.*})", data) if not match: yield ServiceError("Can't find video info") return data = json.loads(decode_html_entities(match.group(1))) yield from hlsparse(self.config, self.http.request("get", data["streamUrls"]["hls"]), data["streamUrls"]["hls"], output=self.output) class Aftonbladet(Service): supported_domains = ["aftonbladet.se"] def get(self): data = self.get_urldata() match = re.search("window.FLUX_STATE = ({.*})", data) if not match: yield ServiceError("Can't find video info") return try: janson = json.loads(match.group(1)) except json.decoder.JSONDecodeError: yield ServiceError(f"Can't decode api request: {match.group(1)}") return videos = self._get_video(janson) yield from videos def _get_video(self, janson): collections = janson["collections"] for n in list(collections.keys()): contents = collections[n]["contents"]["items"] for i in list(contents.keys()): if "type" in contents[i] and contents[i]["type"] == "video": streams = hlsparse( self.config, self.http.request("get", contents[i]["videoAsset"]["streamUrls"]["hls"]), contents[i]["videoAsset"]["streamUrls"]["hls"], output=self.output, ) for key in list(streams.keys()): yield streams[key] svtplay-dl-4.97.1/lib/svtplay_dl/service/angelstudios.py000066400000000000000000000105441465165132400233700ustar00rootroot00000000000000import json import re from svtplay_dl.error import ServiceError from svtplay_dl.fetcher.hls import hlsparse from svtplay_dl.service import Service class Angelstudios(Service): supported_domains = ["watch.angelstudios.com"] def get(self): data = self.get_urldata() match = re.search(r'contentUrl": "([^"]+)"', data) if not match: yield ServiceError("Can't find the video") return hls_playlist = match.group(1) match = re.search(r"", data) if not match: match = re.search('source src="([^"]+)"', data) if not match: yield ServiceError("Cant find info for this video") return res = self.http.request("get", match.group(1)) if res.status_code > 400: yield ServiceError("Can't play this because the video is geoblocked or not available.") else: yield from hlsparse(self.config, res, match.group(1), output=self.output) return janson = json.loads(match.group(1)) page = janson["cache"]["page"][list(janson["cache"]["page"].keys())[0]] resolution = None vid = None if page["key"] != "Watch": yield ServiceError("Wrong url, need to be video url") return if "item" in page["entries"][0]: offers = page["entries"][0]["item"]["offers"] elif "item" in page: offers = page["item"]["offers"] self.output["id"] = page["entries"][0]["item"]["id"] if "season" in page["entries"][0]["item"]: self.output["title"] = page["entries"][0]["item"]["season"]["title"] self.output["season"] = page["entries"][0]["item"]["season"]["seasonNumber"] self.output["episode"] = page["entries"][0]["item"]["episodeNumber"] self.output["episodename"] = page["entries"][0]["item"]["contextualTitle"] elif "title" in page["entries"][0]["item"]: self.output["title"] = page["entries"][0]["item"]["title"] offerlist = [] for i in offers: if i["deliveryType"] == "Stream": offerlist.append([i["scopes"][0], i["resolution"]]) deviceid = uuid.uuid1() res = self.http.request( "post", "https://isl.dr-massive.com/api/authorization/anonymous-sso?device=web_browser&ff=idp%2Cldp&lang=da", json={"deviceId": str(deviceid), "scopes": ["Catalog"], "optout": True}, ) token = res.json()[0]["value"] if len(offerlist) == 0: yield ServiceError("Can't find any videos") return for i in offerlist: vid, resolution = i url = ( f"https://isl.dr-massive.com/api/account/items/{vid}/videos?delivery=stream&device=web_browser&" f"ff=idp%2Cldp&lang=da&resolution={resolution}&sub=Anonymous" ) res = self.http.request("get", url, headers={"authorization": f"Bearer {token}"}) for video in res.json(): if video["accessService"] == "StandardVideo" and video["format"] == "video/hls": res = self.http.request("get", video["url"]) if res.status_code > 400: yield ServiceError("Can't play this because the video is geoblocked or not available.") else: yield from hlsparse(self.config, res, video["url"], output=self.output) if len(video["subtitles"]) > 0: yield from subtitle_probe(copy.copy(self.config), video["subtitles"][0]["link"], output=self.output) def find_all_episodes(self, config): episodes = [] seasons = [] data = self.get_urldata() match = re.search("__data = ([^<]+)", data) if not match: if "bonanza" in self.url: parse = urlparse(self.url) match = re.search(r"(\/bonanza\/serie\/[0-9]+\/[\-\w]+)", parse.path) if match: match = re.findall(rf"a href=\"({match.group(1)}\/\d+[^\"]+)\"", data) if not match: logging.error("Can't find video info.") for url in match: episodes.append(f"https://www.dr.dk{url}") else: logging.error("Can't find video info.") return episodes else: logging.error("Can't find video info.") return episodes janson = json.loads(match.group(1)) page = janson["cache"]["page"][list(janson["cache"]["page"].keys())[0]] if "show" in page["item"] and "seasons" in page["item"]["show"]: for i in page["item"]["show"]["seasons"]["items"]: seasons.append(f'https://www.dr.dk/drtv{i["path"]}') if seasons: for season in seasons: data = self.http.get(season).text match = re.search("__data = ([^<]+)", data) janson = json.loads(match.group(1)) page = janson["cache"]["page"][list(janson["cache"]["page"].keys())[0]] episodes.extend(self._get_episodes(page)) else: episodes.extend(self._get_episodes(page)) if config.get("all_last") != -1: episodes = episodes[: config.get("all_last")] else: episodes.reverse() return episodes def _get_episodes(self, page): episodes = [] if "episodes" in page["item"]: entries = page["item"]["episodes"]["items"] for i in entries: episodes.append(f'https://www.dr.dk/drtv{i["watchPath"]}') return episodes svtplay-dl-4.97.1/lib/svtplay_dl/service/efn.py000066400000000000000000000011001465165132400214230ustar00rootroot00000000000000import re from svtplay_dl.error import ServiceError from svtplay_dl.fetcher.hls import hlsparse from svtplay_dl.service import OpenGraphThumbMixin from svtplay_dl.service import Service class Efn(Service, OpenGraphThumbMixin): supported_domains_re = ["www.efn.se"] def get(self): match = re.search('data-hls="([^"]+)"', self.get_urldata()) if not match: yield ServiceError("Cant find video info") return yield from hlsparse(self.config, self.http.request("get", match.group(1)), match.group(1), output=self.output) svtplay-dl-4.97.1/lib/svtplay_dl/service/eurosport.py000066400000000000000000000115721465165132400227330ustar00rootroot00000000000000import json import re from urllib.parse import quote from urllib.parse import urlparse from svtplay_dl.error import ServiceError from svtplay_dl.fetcher.hls import hlsparse from svtplay_dl.service import Service class Eurosport(Service): supported_domains_re = [r"^([^.]+\.)*eurosportplayer.com"] def get(self): parse = urlparse(self.url) match = re.search("window.server_path = ({.*});", self.get_urldata()) if not match: yield ServiceError("Cant find api key") return janson = json.loads(match.group(1)) clientapikey = janson["sdk"]["clientApiKey"] header = {"authorization": f"Bearer {clientapikey}"} res = self.http.post( "https://eu.edge.bamgrid.com/devices", headers=header, json={"deviceFamily": "browser", "applicationRuntime": "firefox", "deviceProfile": "macosx", "attributes": {}}, ) res = self.http.post( "https://eu.edge.bamgrid.com/token", headers=header, data={ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", "latitude": 0, "longitude": 0, "platform": "browser", "subject_token": res.json()["assertion"], "subject_token_type": "urn:bamtech:params:oauth:token-type:device", }, ) header = {"authorization": f"Bearer {res.json()['access_token']}"} res = self.http.post( "https://eu.edge.bamgrid.com/idp/login", headers=header, json={"email": self.config.get("username"), "password": self.config.get("password")}, ) if res.status_code > 400: yield ServiceError("Wrong username or password") return grant = "https://eu.edge.bamgrid.com/accounts/grant" res = self.http.post(grant, headers=header, json={"id_token": res.json()["id_token"]}) header = {"authorization": f"Bearer {clientapikey}"} res = self.http.post( "https://eu.edge.bamgrid.com/token", headers=header, data={ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", "latitude": 0, "longitude": 0, "platform": "browser", "subject_token": res.json()["assertion"], "subject_token_type": "urn:bamtech:params:oauth:token-type:account", }, ) access_token = res.json()["access_token"] query = {"preferredLanguages": ["en"], "mediaRights": ["GeoMediaRight"], "uiLang": "en", "include_images": True} if parse.path[:11] == "/en/channel": pagetype = "channel" match = re.search("/([^/]+)$", parse.path) if not match: yield ServiceError("Cant find channel") return (vid,) = match.groups() query["pageType"] = pagetype query["channelCallsign"] = vid query["channelCallsigns"] = vid query["onAir"] = True self.config.set("live", True) # lets override to true url = ( "https://search-api.svcs.eurosportplayer.com/svc/search/v2/graphql/persisted/" f"query/eurosport/web/Airings/onAir?variables={quote(json.dumps(query))}" ) res = self.http.get(url, headers={"authorization": access_token}) vid2 = res.json()["data"]["Airings"][0]["channel"]["id"] url = f"https://global-api.svcs.eurosportplayer.com/channels/{vid2}/scenarios/browser" res = self.http.get(url, headers={"authorization": access_token, "Accept": "application/vnd.media-service+json; version=1"}) hls_url = res.json()["stream"]["slide"] else: pagetype = "event" match = re.search("/([^/]+)/([^/]+)$", parse.path) if not match: yield ServiceError("Cant fint event id") return query["title"], query["contentId"] = match.groups() query["pageType"] = pagetype url = f"https://search-api.svcs.eurosportplayer.com/svc/search/v2/graphql/persisted/query/eurosport/Airings?variables={quote(json.dumps(query))}" res = self.http.get(url, headers={"authorization": access_token}) programid = res.json()["data"]["Airings"][0]["programId"] mediaid = res.json()["data"]["Airings"][0]["mediaId"] url = f"https://global-api.svcs.eurosportplayer.com/programs/{programid}/media/{mediaid}/scenarios/browser" res = self.http.get(url, headers={"authorization": access_token, "Accept": "application/vnd.media-service+json; version=1"}) hls_url = res.json()["stream"]["complete"] yield from hlsparse(self.config, self.http.request("get", hls_url), hls_url, authorization=access_token, output=self.output) svtplay-dl-4.97.1/lib/svtplay_dl/service/expressen.py000066400000000000000000000015101465165132400226740ustar00rootroot00000000000000# ex:ts=4:sw=4:sts=4:et # -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- import json import re from svtplay_dl.error import ServiceError from svtplay_dl.fetcher.hls import hlsparse from svtplay_dl.service import Service from svtplay_dl.utils.text import decode_html_entities class Expressen(Service): supported_domains = ["expressen.se"] def get(self): data = self.get_urldata() match = re.search('data-article-data="([^"]+)"', data) if not match: yield ServiceError("Cant find video file info") return data = decode_html_entities(match.group(1)) janson = json.loads(data) self.config.set("live", janson["isLive"]) yield from hlsparse(self.config, self.http.request("get", janson["stream"]), janson["stream"], output=self.output) svtplay-dl-4.97.1/lib/svtplay_dl/service/facebook.py000066400000000000000000000026141465165132400224370ustar00rootroot00000000000000import copy import json import re from urllib.parse import unquote_plus from svtplay_dl.error import ServiceError from svtplay_dl.fetcher.http import HTTP from svtplay_dl.service import OpenGraphThumbMixin from svtplay_dl.service import Service class Facebook(Service, OpenGraphThumbMixin): supported_domains_re = ["www.facebook.com"] def get(self): data = self.get_urldata() match = re.search('params","([^"]+)"', data) if not match: yield ServiceError("Cant find params info. video need to be public.") return data2 = json.loads(f'["{match.group(1)}"]') data2 = json.loads(unquote_plus(data2[0])) if "sd_src_no_ratelimit" in data2["video_data"]["progressive"][0]: yield HTTP(copy.copy(self.config), data2["video_data"]["progressive"][0]["sd_src_no_ratelimit"], "240", output=self.output) else: yield HTTP(copy.copy(self.config), data2["video_data"]["progressive"][0]["sd_src"], "240") if "hd_src_no_ratelimit" in data2["video_data"]["progressive"][0]: yield HTTP(copy.copy(self.config), data2["video_data"]["progressive"][0]["hd_src_no_ratelimit"], "720", output=self.output) else: if data2["video_data"]["progressive"][0]["hd_src"]: yield HTTP(copy.copy(self.config), data2["video_data"]["progressive"][0]["hd_src"], "720", output=self.output) svtplay-dl-4.97.1/lib/svtplay_dl/service/filmarkivet.py000066400000000000000000000012211465165132400231740ustar00rootroot00000000000000# ex:ts=4:sw=4:sts=4:et # -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- import copy import re from svtplay_dl.error import ServiceError from svtplay_dl.fetcher.http import HTTP from svtplay_dl.service import OpenGraphThumbMixin from svtplay_dl.service import Service class Filmarkivet(Service, OpenGraphThumbMixin): supported_domains = ["filmarkivet.se"] def get(self): match = re.search(r'[^/]file: "(http[^"]+)', self.get_urldata()) if not match: yield ServiceError("Can't find the video file") return yield HTTP(copy.copy(self.config), match.group(1), 480, output=self.output) svtplay-dl-4.97.1/lib/svtplay_dl/service/flowonline.py000066400000000000000000000023721465165132400230430ustar00rootroot00000000000000# ex:ts=4:sw=4:sts=4:et # -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- import copy import re from urllib.parse import urlparse from svtplay_dl.error import ServiceError from svtplay_dl.fetcher.hls import hlsparse from svtplay_dl.service import OpenGraphThumbMixin from svtplay_dl.service import Service from svtplay_dl.subtitle import subtitle_probe class Flowonline(Service, OpenGraphThumbMixin): supported_domains_re = [r"^([a-z]{1,4}\.|www\.)?flowonline\.tv$"] def get(self): match = re.search('iframe src="(/embed/[^"]+)"', self.get_urldata()) if not match: yield ServiceError("Cant find video") return parse = urlparse(self.url) url = f"{parse.scheme}://{parse.netloc}{match.group(1)}" data = self.http.get(url) match = re.search('src="([^"]+vtt)"', data.text) if match: yield from subtitle_probe(copy.copy(self.config), match.group(1)) match = re.search('source src="([^"]+)" type="application/x-mpegURL"', data.text) if not match: yield ServiceError("Cant find video file") return yield from hlsparse(self.config, self.http.request("get", match.group(1)), match.group(1), output=self.output) svtplay-dl-4.97.1/lib/svtplay_dl/service/koket.py000066400000000000000000000036661465165132400220130ustar00rootroot00000000000000# ex:ts=4:sw=4:sts=4:et # -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- import json import re from urllib.parse import urlparse from svtplay_dl.error import ServiceError from svtplay_dl.fetcher.hls import hlsparse from svtplay_dl.service import OpenGraphThumbMixin from svtplay_dl.service import Service class Koket(Service, OpenGraphThumbMixin): supported_domains = ["koket.se"] def get(self): urlp = urlparse(self.url) if urlp.path.startswith("/kurser"): res = self.http.post( "https://www.koket.se/konto/authentication/login", json={"username": self.config.get("username"), "password": self.config.get("password")}, ) if "errorMessage" in res.json(): yield ServiceError("Wrong username or password") return data = self.http.get(self.url) match = re.search(r'({"@.*})', data) if not match: yield ServiceError("Can't find video info") return janson = json.loads(f"[{match.group(1)}]") for i in janson: if "video" in i: self.output["title"] = i["video"]["name"] break match = re.search(r"dataLayer = (\[.*\]);<", data) if not match: yield ServiceError("Can't find video id") return janson = json.loads(match.group(1)) self.output["id"] = janson[0]["video"] url = f"https://playback-api.b17g.net/media/{self.output['id']}?service=tv4&device=browser&protocol=hls%2Cdash&drm=widevine" videoDataRes = self.http.get(url) if videoDataRes.json()["playbackItem"]["type"] == "hls": yield from hlsparse( self.config, self.http.get(videoDataRes.json()["playbackItem"]["manifestUrl"]), videoDataRes.json()["playbackItem"]["manifestUrl"], output=self.output, ) svtplay-dl-4.97.1/lib/svtplay_dl/service/lemonwhale.py000066400000000000000000000036601465165132400230230ustar00rootroot00000000000000# ex:ts=4:sw=4:sts=4:et # -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- import json import re from svtplay_dl.error import ServiceError from svtplay_dl.fetcher.hls import hlsparse from svtplay_dl.service import Service from svtplay_dl.utils.text import decode_html_entities class Lemonwhale(Service): # lemonwhale.com is just bogus for generic supported_domains = ["vk.se", "lemonwhale.com"] def get(self): vid = self.get_vid() if not vid: yield ServiceError("Can't find video id") return url = f"http://ljsp.lwcdn.com/web/public/item.json?type=video&{decode_html_entities(vid)}" data = self.http.request("get", url).text jdata = json.loads(data) if "videos" in jdata: yield from self.get_video(jdata) url = f"http://ljsp.lwcdn.com/web/public/video.json?id={decode_html_entities(vid)}&delivery=hls" data = self.http.request("get", url).text jdata = json.loads(data) if "videos" in jdata: yield from self.get_video(jdata) def get_vid(self): match = re.search(r'video url-([^"]+)', self.get_urldata()) if match: return match.group(1) match = re.search(r"__INITIAL_STATE__ = ({.*})", self.get_urldata()) if match: janson = json.loads(match.group(1)) vid = janson["content"]["current"]["data"]["templateData"]["pageData"]["video"]["id"] return vid match = re.search(r'embed.jsp\?([^"]+)"', self.get_urldata()) if match: return match.group(1) return None def get_video(self, janson): videos = janson["videos"][0]["media"]["streams"] for i in videos: if i["name"] == "auto": hls = f"{janson['videos'][0]['media']['base']}{i['url']}" yield from hlsparse(self.config, self.http.request("get", hls), hls, output=self.output) svtplay-dl-4.97.1/lib/svtplay_dl/service/mtvnn.py000066400000000000000000000140501465165132400220250ustar00rootroot00000000000000import json import logging import re import xml.etree.ElementTree as ET from urllib.parse import urlparse from svtplay_dl.error import ServiceError from svtplay_dl.fetcher.hls import hlsparse from svtplay_dl.service import OpenGraphThumbMixin from svtplay_dl.service import Service # This is _very_ similar to mtvservices.. class Mtvnn(Service, OpenGraphThumbMixin): supported_domains = ["nickelodeon.se", "nickelodeon.nl", "nickelodeon.no", "www.comedycentral.se", "nickelodeon.dk"] def get(self): data = self.get_urldata() parse = urlparse(self.url) if parse.netloc.endswith("se"): match = re.search(r'
', data) if not match: yield ServiceError("Can't find video info") return match_id = re.search(r'data-id="([0-9a-fA-F|\-]+)" ', match.group(1)) if not match_id: yield ServiceError("Can't find video info") return wanted_id = match_id.group(1) url_service = ( f"http://feeds.mtvnservices.com/od/feed/intl-mrss-player-feed?mgid=mgid:arc:episode:nick.intl:{wanted_id}" "&arcEp=nickelodeon.se&imageEp=nickelodeon.se&stage=staging&accountOverride=intl.mtvi.com&ep=a9cc543c" ) service_asset = self.http.request("get", url_service) match_guid = re.search('(.*)', service_asset.text) if not match_guid: yield ServiceError("Can't find video info") return hls_url = ( f"https://mediautilssvcs-a.akamaihd.net/services/MediaGenerator/{match_guid.group(1)}?arcStage=staging&accountOverride=intl.mtvi.com&" "billingSection=intl&ep=a9cc543c&acceptMethods=hls" ) hls_asset = self.http.request("get", hls_url) xml = ET.XML(hls_asset.text) if ( xml.find("./video") is not None and xml.find("./video").find("item") is not None and xml.find("./video").find("item").find("rendition") is not None and xml.find("./video").find("item").find("rendition").find("src") is not None ): hls_url = xml.find("./video").find("item").find("rendition").find("src").text stream = hlsparse(self.config, self.http.request("get", hls_url), hls_url, output=self.output) for key in list(stream.keys()): yield stream[key] return match = re.search(r'data-mrss=[\'"](http://gakusei-cluster.mtvnn.com/v2/mrss.xml[^\'"]+)[\'"]', data) if not match: yield ServiceError("Can't find id for the video") return mrssxmlurl = match.group(1) data = self.http.request("get", mrssxmlurl).content xml = ET.XML(data) title = xml.find("channel").find("item").find("title").text self.output["title"] = title match = re.search("gon.viacom_config=([^;]+);", self.get_urldata()) if match: countrycode = json.loads(match.group(1))["country_code"].replace("_", "/") match = re.search("mtvnn.com:([^&]+)", mrssxmlurl) if match: urlpart = match.group(1).replace("-", "/").replace("playlist", "playlists") # it use playlists dunno from where it gets it hlsapi = f"http://api.mtvnn.com/v2/{countrycode}/{urlpart}.json?video_format=m3u8&callback=&" data = self.http.request("get", hlsapi).text dataj = json.loads(data) for i in dataj["local_playlist_videos"]: yield from hlsparse(self.config, self.http.request("get", i["url"]), i["url"], output=self.output) def find_all_episodes(self, config): episodes = [] match = re.search(r"data-franchise='([^']+)'", self.get_urldata()) if match is None: logging.error("Couldn't program id") return episodes programid = match.group(1) match = re.findall(r"
  • 0: alt = self.http.get(query["alt"][0]) if alt: yield from hlsparse(self.config, self.http.request("get", alt.request.url), alt.request.url, output=self.output) if i["format"] == "dash264" or i["format"] == "dashhbbtv": yield from dashparse(self.config, self.http.request("get", i["url"]), i["url"], output=self.output) if "alt" in query and len(query["alt"]) > 0: alt = self.http.get(query["alt"][0]) if alt: yield from dashparse(self.config, self.http.request("get", alt.request.url), alt.request.url, output=self.output) def find_video_id(self): match = re.search('data-video-id="([^"]+)"', self.get_urldata()) if match: return match.group(1) return None def find_all_episodes(self, config): episodes = [] page = 1 data = self.get_urldata() match = re.search(r'"/etikett/titel/([^"/]+)', data) if match is None: match = re.search(r'"http://www.oppetarkiv.se/etikett/titel/([^/]+)/', self.url) if match is None: logging.error("Couldn't find title") return episodes program = match.group(1) n = 0 if config.get("all_last") > 0: sort = "tid_fallande" else: sort = "tid_stigande" while True: url = f"http://www.oppetarkiv.se/etikett/titel/{program}/?sida={page}&sort={sort}&embed=true" data = self.http.request("get", url) if data.status_code == 404: break data = data.text regex = re.compile(r'href="(/video/[^"]+)"') for match in regex.finditer(data): if n == self.config.get("all_last"): break episodes.append(f"http://www.oppetarkiv.se{match.group(1)}") n += 1 page += 1 return episodes def outputfilename(self, data): vid = hashlib.sha256(data["programVersionId"].encode("utf-8")).hexdigest()[:7] self.output["id"] = vid datatitle = re.search('data-title="([^"]+)"', self.get_urldata()) if not datatitle: return datat = decode_html_entities(datatitle.group(1)) self.output["title"] = self.name(datat) self.seasoninfo(datat) def seasoninfo(self, data): match = re.search(r"S.song (\d+) - Avsnitt (\d+)", data) if match: self.output["season"] = int(match.group(1)) self.output["episode"] = int(match.group(2)) else: match = re.search(r"Avsnitt (\d+)", data) if match: self.output["episode"] = int(match.group(1)) def name(self, data): if data.find(" - S.song") > 0: title = data[: data.find(" - S.song")] else: if data.find(" - Avsnitt") > 0: title = data[: data.find(" - Avsnitt")] else: title = data return title svtplay-dl-4.97.1/lib/svtplay_dl/service/picsearch.py000066400000000000000000000024521465165132400226270ustar00rootroot00000000000000# ex:ts=4:sw=4:sts=4:et # -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- import json import re from svtplay_dl.error import ServiceError from svtplay_dl.fetcher.hls import hlsparse from svtplay_dl.service import OpenGraphThumbMixin from svtplay_dl.service import Service class Picsearch(Service, OpenGraphThumbMixin): supported_domains = ["dn.se", "mobil.dn.se", "di.se", "csp.picsearch.com", "csp.screen9.com", "cdn.screen9.com"] backupapi = None def get(self): mediaid = self.get_mediaid() if not mediaid: yield ServiceError("Cant find media id") return mediaid = mediaid.group(1) jsondata = self.http.request("get", f"https://api.screen9.com/player/config/{mediaid}").text jsondata = json.loads(jsondata) for i in jsondata["src"]: if "application/x-mpegURL" in i["type"]: yield from hlsparse( self.config, self.http.request("get", i["src"]), i["src"], output=self.output, ) def get_mediaid(self): match = re.search(r'media-id="([^"]+)"', self.get_urldata()) if not match: match = re.search(r'"mediaid": "([^"]+)"', self.get_urldata()) return match svtplay-dl-4.97.1/lib/svtplay_dl/service/plutotv.py000066400000000000000000000120421465165132400223770ustar00rootroot00000000000000import datetime import logging import re import uuid from urllib.parse import urlparse from svtplay_dl.error import ServiceError from svtplay_dl.fetcher.hls import hlsparse from svtplay_dl.service import OpenGraphThumbMixin from svtplay_dl.service import Service from svtplay_dl.subtitle import subtitle class Plutotv(Service, OpenGraphThumbMixin): supported_domains = ["pluto.tv"] urlreg = r"/on-demand/(movies|series)/([^/]+)(/season/\d+/episode/([^/]+))?" urlreg2 = r"/on-demand/(movies|series)/([^/]+)(/episode/([^/]+))?" def get(self): self.data = self.get_urldata() parse = urlparse(self.url) urlmatch = re.search(self.urlreg, parse.path) if not urlmatch: yield ServiceError("Can't find what video it is or live is not supported") return self.slug = urlmatch.group(2) episodename = urlmatch.group(4) if episodename is None: urlmatch = re.search(self.urlreg2, parse.path) if not urlmatch: yield ServiceError("Can't find what video it is or live is not supported") return self.slug = urlmatch.group(2) episodename = urlmatch.group(4) self._janson() HLSplaylist = None for vod in self.janson["VOD"]: self.output["title"] = vod["name"] if "seasons" in vod: for season in vod["seasons"]: if "episodes" in season: for episode in season["episodes"]: if episode["_id"] == episodename: self.output["season"] = season["number"] self.output["episodename"] = episode["name"] for stich in episode["stitched"]["paths"]: if stich["type"] == "hls": HLSplaylist = f"{self.mediaserver}{stich['path']}?{self.stitcherParams}" if self.http.request("get", HLSplaylist).status_code < 400: break if "stitched" in vod and "paths" in vod["stitched"]: for stich in vod["stitched"]["paths"]: if stich["type"] == "hls": HLSplaylist = f"{self.mediaserver}{stich['path']}?{self.stitcherParams}" if self.http.request("get", HLSplaylist).status_code < 400: break if not HLSplaylist: yield ServiceError("Can't find video info") return playlists = hlsparse( self.config, self.http.request("get", HLSplaylist, headers={"Authorization": f"Bearer {self.sessionToken}"}), HLSplaylist, self.output, filter=True, ) for playlist in playlists: if self.config.get("subtitle") and isinstance(playlist, subtitle): logging.warning("Subtitles are no longer supported for pluto.tv") continue yield playlist def find_all_episodes(self, options): episodes = [] self.data = self.get_urldata() parse = urlparse(self.url) urlmatch = re.search(self.urlreg, parse.path) if urlmatch is None: logging.error("Can't find what video it is or live is not supported") return episodes if urlmatch.group(1) != "series": return episodes self.slug = urlmatch.group(2) self._janson() match = re.search(r"^/([^\/]+)/", parse.path) language = match.group(1) for vod in self.janson["VOD"]: if "seasons" in vod: for season in vod["seasons"]: seasonnr = season["number"] if "episodes" in season: for episode in season["episodes"]: episodes.append(f"https://pluto.tv/{language}/on-demand/series/{self.slug}/season/{seasonnr}/episode/{episode['_id']}") return episodes def _janson(self) -> None: self.appversion = re.search('appVersion" content="([^"]+)"', self.data) self.query = { "appName": "web", "appVersion": self.appversion.group(1) if self.appversion else "na", "deviceVersion": "119.0.0", "deviceModel": "web", "deviceMake": "firefox", "deviceType": "web", "clientID": uuid.uuid1(), "clientModelNumber": "1.0.0", "seriesIDs": self.slug, "serverSideAds": "false", "constraints": "", "drmCapabilities": "widevine%3AL3", "clientTime": datetime.datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ"), } res = self.http.request("get", "https://boot.pluto.tv/v4/start", params=self.query) self.janson = res.json() self.mediaserver = self.janson["servers"]["stitcher"] self.stitcherParams = self.janson["stitcherParams"] self.sessionToken = self.janson["sessionToken"] svtplay-dl-4.97.1/lib/svtplay_dl/service/pokemon.py000066400000000000000000000030121465165132400223270ustar00rootroot00000000000000import re from urllib.parse import urlparse from svtplay_dl.error import ServiceError from svtplay_dl.fetcher.hls import hlsparse from svtplay_dl.service import OpenGraphThumbMixin from svtplay_dl.service import Service class Pokemon(Service, OpenGraphThumbMixin): supported_domains = ["watch.pokemon.com"] def get(self): parse = urlparse(self.url) if parse.fragment == "": yield ServiceError("need the whole url") return match = re.search(r"id=([a-f0-9]+)\&", parse.fragment) if not match: yield ServiceError("Cant find the ID in the url") return match2 = re.search(r'region: "(\w+)"', self.get_urldata()) if not match2: yield ServiceError("Can't find region data") return res = self.http.get(f"https://www.pokemon.com/api/pokemontv/v2/channels/{match2.group(1)}/") janson = res.json() stream = None for i in janson: for n in i["media"]: if n["id"] == match.group(1): stream = n break if stream is None: yield ServiceError("Can't find video") return self.output["title"] = "pokemon" self.output["season"] = stream["season"] self.output["episode"] = stream["episode"] self.output["episodename"] = stream["title"] yield from hlsparse(self.config, self.http.request("get", stream["stream_url"]), stream["stream_url"], output=self.output) svtplay-dl-4.97.1/lib/svtplay_dl/service/radioplay.py000066400000000000000000000013411465165132400226460ustar00rootroot00000000000000# ex:ts=4:sw=4:sts=4:et # -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- import copy import json import re from svtplay_dl.error import ServiceError from svtplay_dl.fetcher.http import HTTP from svtplay_dl.service import Service class Radioplay(Service): supported_domains = ["radioplay.se"] def get(self): data = self.get_urldata() match = re.search(r"RP.vcdData = ({.*});", data) if match: data = json.loads(match.group(1)) for i in list(data["station"]["streams"].keys()): yield HTTP(copy.copy(self.config), data["station"]["streams"][i], i) else: yield ServiceError("Can't find stream info") return svtplay-dl-4.97.1/lib/svtplay_dl/service/raw.py000066400000000000000000000011361465165132400214550ustar00rootroot00000000000000import os import re from svtplay_dl.fetcher.dash import dashparse from svtplay_dl.fetcher.hls import hlsparse from svtplay_dl.service import Service class Raw(Service): def get(self): filename = os.path.basename(self.url[: self.url.rfind("/")]) self.output["title"] = filename if re.search(".m3u8", self.url): yield from hlsparse(self.config, self.http.request("get", self.url), self.url, output=self.output) if re.search(".mpd", self.url): yield from dashparse(self.config, self.http.request("get", self.url), self.url, output=self.output) svtplay-dl-4.97.1/lib/svtplay_dl/service/regeringen.py000066400000000000000000000023241465165132400230110ustar00rootroot00000000000000import html import re from svtplay_dl.error import ServiceError from svtplay_dl.fetcher.hls import hlsparse from svtplay_dl.service import Service class Regeringen(Service): supported_domains_re = ["regeringen.se", "www.regeringen.se"] def get(self): res = self.http.get(self.url) html_data = res.text match = re.search(r"(.*?) -", html_data) if match: self.output["title"] = html.unescape(match.group(1)) match = re.search(r"//video.qbrick.com/api/v1/(.*?)'", html_data) if match: result = match.group(1) else: yield ServiceError("Cant find the video.") data_url = f"https://video.qbrick.com/api/v1/{result}" res = self.http.get(data_url) data = res.json() resources = data["asset"]["resources"] index_resources = [resource for resource in resources if resource["type"] == "index"] links = index_resources[0]["renditions"][0]["links"] hls_url = [link for link in links if "x-mpegURL" in link["mimeType"]][0]["href"] if hls_url.find(".m3u8") > 0: yield from hlsparse(self.config, self.http.request("get", hls_url), hls_url, output=self.output) ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������svtplay-dl-4.97.1/lib/svtplay_dl/service/riksdagen.py�����������������������������������������������0000664�0000000�0000000�00000001337�14651651324�0022636�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������import json import re from svtplay_dl.error import ServiceError from svtplay_dl.fetcher.hls import hlsparse from svtplay_dl.service import OpenGraphThumbMixin from svtplay_dl.service import Service class Riksdagen(Service, OpenGraphThumbMixin): supported_domains_re = ["riksdagen.se", "www.riksdagen.se"] def get(self): match = re.search(r"application\/json\">({.+})<\/script>", self.get_urldata()) if not match: yield ServiceError("Cant find the video.") return janson = json.loads(match.group(1)) url = janson["props"]["pageProps"]["contentApiData"]["video"]["url"] yield from hlsparse(self.config, self.http.request("get", url), url, output=self.output) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������svtplay-dl-4.97.1/lib/svtplay_dl/service/ruv.py�����������������������������������������������������0000664�0000000�0000000�00000003156�14651651324�0021504�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# ex:ts=4:sw=4:sts=4:et # -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- import copy import json import re from svtplay_dl.error import ServiceError from svtplay_dl.fetcher.hls import HLS from svtplay_dl.fetcher.hls import hlsparse from svtplay_dl.fetcher.http import HTTP from svtplay_dl.service import Service class Ruv(Service): supported_domains = ["ruv.is"] def get(self): data = self.get_urldata() match = re.search(r'"([^"]+geo.php)"', data) if match: data = self.http.request("get", match.group(1)).content match = re.search(r"punktur=\(([^ ]+)\)", data) if match: janson = json.loads(match.group(1)) self.config.get("live", checklive(janson["result"][1])) yield from hlsparse(self.config, self.http.request("get", janson["result"][1]), janson["result"][1], output=self.output) else: yield ServiceError("Can't find json info") else: match = re.search(r'<source [^ ]*[ ]*src="([^"]+)" ', self.get_urldata()) if not match: yield ServiceError(f"Can't find video info for: {self.url}") return if match.group(1).endswith("mp4"): yield HTTP(copy.copy(self.config), match.group(1), 800, output=self.output) else: m3u8_url = match.group(1) self.config.set("live", checklive(m3u8_url)) yield HLS(copy.copy(self.config), m3u8_url, 800, output=self.output) def checklive(url): return True if re.search("live", url) else False ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������svtplay-dl-4.97.1/lib/svtplay_dl/service/services.py������������������������������������������������0000664�0000000�0000000�00000005303�14651651324�0022507�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������from svtplay_dl.service.aftonbladet import Aftonbladet from svtplay_dl.service.aftonbladet import Aftonbladettv from svtplay_dl.service.angelstudios import Angelstudios from svtplay_dl.service.barnkanalen import Barnkanalen from svtplay_dl.service.bigbrother import Bigbrother from svtplay_dl.service.cmore import Cmore from svtplay_dl.service.disney import Disney from svtplay_dl.service.dplay import Discoveryplus from svtplay_dl.service.dr import Dr from svtplay_dl.service.efn import Efn from svtplay_dl.service.eurosport import Eurosport from svtplay_dl.service.expressen import Expressen from svtplay_dl.service.facebook import Facebook from svtplay_dl.service.filmarkivet import Filmarkivet from svtplay_dl.service.flowonline import Flowonline from svtplay_dl.service.koket import Koket from svtplay_dl.service.lemonwhale import Lemonwhale from svtplay_dl.service.mtvnn import Mtvnn from svtplay_dl.service.mtvservices import Mtvservices from svtplay_dl.service.nhl import NHL from svtplay_dl.service.nrk import Nrk from svtplay_dl.service.oppetarkiv import OppetArkiv from svtplay_dl.service.picsearch import Picsearch from svtplay_dl.service.plutotv import Plutotv from svtplay_dl.service.pokemon import Pokemon from svtplay_dl.service.radioplay import Radioplay from svtplay_dl.service.raw import Raw from svtplay_dl.service.regeringen import Regeringen from svtplay_dl.service.riksdagen import Riksdagen from svtplay_dl.service.ruv import Ruv from svtplay_dl.service.solidtango import Solidtango from svtplay_dl.service.sportlib import Sportlib from svtplay_dl.service.sr import Sr from svtplay_dl.service.svt import Svt from svtplay_dl.service.svtplay import Svtplay from svtplay_dl.service.tv4play import Tv4 from svtplay_dl.service.tv4play import Tv4play from svtplay_dl.service.twitch import Twitch from svtplay_dl.service.urplay import Urplay from svtplay_dl.service.vasaloppet import Vasaloppet from svtplay_dl.service.vg import Vg from svtplay_dl.service.viaplay import Viafree from svtplay_dl.service.viasatsport import Viasatsport from svtplay_dl.service.vimeo import Vimeo from svtplay_dl.service.youplay import Youplay sites = [ Aftonbladet, Aftonbladettv, Angelstudios, Barnkanalen, Bigbrother, Cmore, Disney, Discoveryplus, Dr, Efn, Eurosport, Expressen, Facebook, Filmarkivet, Flowonline, Koket, Twitch, Lemonwhale, Mtvservices, Mtvnn, NHL, Nrk, Picsearch, Plutotv, Pokemon, Ruv, Radioplay, Solidtango, Sportlib, Sr, Svt, Svtplay, OppetArkiv, Tv4, Tv4play, Urplay, Vasaloppet, Viafree, Viasatsport, Vimeo, Vg, Youplay, Regeringen, Riksdagen, Raw, ] �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������svtplay-dl-4.97.1/lib/svtplay_dl/service/solidtango.py����������������������������������������������0000664�0000000�0000000�00000010363�14651651324�0023031�0����������������������������������������������������������������������������������������������������ustar�00root����������������������������root����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# ex:ts=4:sw=4:sts=4:et # -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- import re import xml.etree.ElementTree as ET from urllib.parse import urlparse from svtplay_dl.error import ServiceError from svtplay_dl.fetcher.dash import dashparse from svtplay_dl.fetcher.hls import hlsparse from svtplay_dl.service import Service class Solidtango(Service): supported_domains_re = [r"^([^.]+\.)*solidtango.com"] supported_domains = ["mm-resource-service.herokuapp.com", "solidtango.com", "solidsport.com"] def get(self): data = self.get_urldata() parse = urlparse(self.url) if "solidsport" in parse.netloc: if self.config.get("username") and self.config.get("password"): self.http.request("get", "https://solidsport.com/login") pdata = { "username": self.config.get("username"), "password": self.config.get("password"), } res = self.http.request("post", "https://solidsport.com/api/play_v1/session/auth", json=pdata) if res.status_code > 400: yield ServiceError("Wrong username or password") return auth_token = res.json()["token"] slug = parse.path[parse.path.rfind("/") + 1 :] company = re.search(r"/([^\/]+)/", parse.path).group(1) url = f"https://solidsport.com/api/play_v1/media_object/watch?company={company}&" if "/watch/" in self.url: url += f"media_object_slug={slug}" elif "/games/" in self.url: url += f"game_ident={slug}" res = self.http.request("get", url, headers={"Authorization": f"Bearer {auth_token}"}) videoid = res.json()["id"] self.output["title"] = res.json()["title"] self.output["id"] = res.json()["id"] url = f"https://solidsport.com/api/play_v1/media_object/{videoid}/request_stream_urls?admin_access=false&company={company}" res = self.http.request("get", url, headers={"Authorization": f"Bearer {auth_token}"}) if "dash" in res.json()["urls"]: yield from dashparse(self.config, self.http.request("get", res.json()["urls"]["dash"]), res.json()["urls"]["dash"], self.output) if "hls" in res.json()["urls"]: yield from hlsparse(self.config, self.http.request("get", res.json()["urls"]["hls"]), res.json()["urls"]["hls"], self.output) return match = re.search('src="(http://mm-resource-service.herokuapp.com[^"]*)"', data) if match: data = self.http.request("get", match.group(1)).text match = re.search('src="(https://[^"]+solidtango[^"]+)" ', data) if match: data = self.http.request("get", match.group(1)).text match = re.search(r"<title>(http[^<]+)", data) if match: data = self.http.request("get", match.group(1)).text match = re.search("is_livestream: true", data) if match: self.config.set("live", True) match = re.search("isLivestream: true", data) if match: self.config.set("live", True) match = re.search('html5_source: "([^"]+)"', data) match2 = re.search('hlsURI: "([^"]+)"', data) if match: yield from hlsparse(self.config, self.http.request("get", match.group(1)), match.group(1), output=self.output) elif match2: yield from hlsparse(self.config, self.http.request("get", match2.group(1)), match2.group(1), output=self.output) else: parse = urlparse(self.url) url2 = f"https://{parse.netloc}/api/v1/play/{parse.path[parse.path.rfind('/') + 1 :]}.xml" data = self.http.request("get", url2) if data.status_code != 200: yield ServiceError("Can't find video info. if there is a video on the page. its a bug.") return xmldoc = data.text xml = ET.XML(xmldoc) elements = xml.findall(".//manifest") yield from hlsparse(self.config, self.http.request("get", elements[0].text), elements[0].text, output=self.output) svtplay-dl-4.97.1/lib/svtplay_dl/service/sportlib.py000066400000000000000000000055021465165132400225230ustar00rootroot00000000000000# ex:ts=4:sw=4:sts=4:et # -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- import re from urllib.parse import urljoin from urllib.parse import urlparse from svtplay_dl.error import ServiceError from svtplay_dl.fetcher.hls import hlsparse from svtplay_dl.service import OpenGraphThumbMixin from svtplay_dl.service import Service class Sportlib(Service, OpenGraphThumbMixin): supported_domains = ["sportlib.se"] def get(self): data = self.http.get("https://www.sportlib.se/sportlib/login").text match = re.search('src="(/app[^"]+)">', data) if not match: yield ServiceError("Can't find url for login info") return url = urljoin("https://www.sportlib.se", match.group(1)) data = self.http.get(url).text match = re.search('CLIENT_SECRET:"([^"]+)"', data) if not match: yield ServiceError("Cant fint login info") return cs = match.group(1) match = re.search('CLIENT_ID:"([^"]+)"', data) if not match: yield ServiceError("Cant fint login info") return res = self.http.get("https://core.oz.com/channels?slug=sportlib&org=www.sportlib.se") janson = res.json() sid = janson["data"][0]["id"] res = self.http.post( f"https://core.oz.com/oauth2/token?channelId={sid}", data={ "client_id": match.group(1), "client_secret": cs, "grant_type": "password", "username": self.config.get("username"), "password": self.config.get("password"), }, ) if res.status_code > 200: yield ServiceError("Wrong username / password?") return janson = res.json() parse = urlparse(self.url) match = re.search("video/([-a-fA-F0-9]+)", parse.path) if not match: yield ServiceError("Cant find video id") return url = f"https://core.oz.com/channels/{sid}/videos/{match.group(1)}?include=collection,streamUrl" res = self.http.get( url, headers={"content-type": "application/json", "authorization": f"{janson['token_type'].title()} {janson['access_token']}"}, ) janson = res.json() self.output["title"] = janson["data"]["title"] # get cookie postjson = {"name": janson["data"]["streamUrl"]["cookieName"], "value": janson["data"]["streamUrl"]["token"]} res = self.http.post("https://playlist.oz.com/cookie", json=postjson) cookies = res.cookies yield from hlsparse( self.config, self.http.request("get", janson["data"]["streamUrl"]["cdnUrl"]), janson["data"]["streamUrl"]["cdnUrl"], keycookie=cookies, output=self.output, ) svtplay-dl-4.97.1/lib/svtplay_dl/service/sr.py000066400000000000000000000023671465165132400213170ustar00rootroot00000000000000# ex:ts=4:sw=4:sts=4:et # -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- import copy import re from svtplay_dl.error import ServiceError from svtplay_dl.fetcher.http import HTTP from svtplay_dl.service import OpenGraphThumbMixin from svtplay_dl.service import Service class Sr(Service, OpenGraphThumbMixin): supported_domains = ["sverigesradio.se"] def get(self): data = self.get_urldata() match = re.search(r'data-audio-id="(\d+)"', data) match2 = re.search(r'data-publication-id="(\w+)"', data) if match and match2: aid = match.group(1) pubid = match2.group(1) else: yield ServiceError("Can't find audio info") return for what in ["episode", "secondary"]: language = "" apiurl = f"https://sverigesradio.se/playerajax/audio?id={aid}&type={what}&publicationid={pubid}&quality=high" resp = self.http.request("get", apiurl) if resp.status_code > 400: continue playerinfo = resp.json() if what == "secondary": language = "musik" yield HTTP(copy.copy(self.config), playerinfo["audioUrl"], 128, output=self.output, language=language) svtplay-dl-4.97.1/lib/svtplay_dl/service/svt.py000066400000000000000000000033761465165132400215100ustar00rootroot00000000000000import codecs import copy import json import re from svtplay_dl.error import ServiceError from svtplay_dl.service.svtplay import Svtplay from svtplay_dl.subtitle import subtitle_probe class Svt(Svtplay): supported_domains = ["svt.se", "www.svt.se"] def get(self): vid = None data = self.get_urldata() match = re.search("urqlState = (.*);", data) if not match: match = re.search(r"stateData = JSON.parse\(\"(.*)\"\)\<\/script", data) if not match: yield ServiceError("Cant find video info.") return janson = json.loads(codecs.escape_decode(match.group(1))[0].decode("utf-8")) if janson["recipe"]["content"]["data"]["videoClips"]: vid = janson["recipe"]["content"]["data"]["videoClips"][0]["id"] else: vid = janson["recipe"]["content"]["data"]["videoEpisodes"][0]["id"] res = self.http.get(f"https://api.svt.se/videoplayer-api/video/{vid}") else: janson = json.loads(match.group(1)) for key in list(janson.keys()): janson2 = json.loads(janson[key]["data"]) if "page" in janson2 and "topMedia" in janson2["page"]: vid = janson2["page"]["topMedia"]["svtId"] if not vid: yield ServiceError("Can't find any videos") return res = self.http.get(f"https://api.svt.se/video/{vid}") janson = res.json() if "subtitleReferences" in janson: for i in janson["subtitleReferences"]: if "url" in i: yield from subtitle_probe(copy.copy(self.config), i["url"], output=self.output) yield from self._get_video(janson) svtplay-dl-4.97.1/lib/svtplay_dl/service/svtplay.py000066400000000000000000000432031465165132400223670ustar00rootroot00000000000000# ex:ts=4:sw=4:sts=4:et # -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- import copy import datetime import hashlib import json import logging import re import time from urllib.parse import parse_qs from urllib.parse import quote_plus from urllib.parse import urljoin from urllib.parse import urlparse from svtplay_dl.error import ServiceError from svtplay_dl.fetcher.dash import dashparse from svtplay_dl.fetcher.hls import hlsparse from svtplay_dl.service import MetadataThumbMixin from svtplay_dl.service import Service from svtplay_dl.subtitle import subtitle_probe from svtplay_dl.utils.text import filenamify URL_VIDEO_API = "https://api.svt.se/video/" LIVE_CHANNELS = { "svtbarn": "ch-barnkanalen", "svt1": "ch-svt1", "svt2": "ch-svt2", "svt24": "ch-svt24", "kunskapskanalen": "ch-kunskapskanalen", } class Svtplay(Service, MetadataThumbMixin): supported_domains = ["svtplay.se", "svt.se", "beta.svtplay.se", "svtflow.se"] info_search_expr = r"