├── .codecov.yml ├── .editorconfig ├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── 1_plugin_issue.yml │ ├── 1_plugin_request.yml │ ├── 2_bug_report.yml │ ├── 3_feature_request.yml │ └── config.yml ├── pull_request_template.md ├── release_template.md └── workflows │ ├── lint.yml │ ├── main.yml │ └── useragents.yml ├── .gitignore ├── AUTHORS ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── KNOWN_ISSUES.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── dev-requirements.txt ├── docs-requirements.txt ├── docs ├── Makefile ├── _applications.rst ├── _man.rst ├── _static │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── icon.svg │ ├── opengraph-image.png │ ├── site.webmanifest │ └── styles │ │ └── custom.css ├── _templates │ ├── base.html │ ├── page.html │ └── sidebar │ │ ├── brand.html │ │ └── github-buttons.html ├── api.rst ├── api_guide.rst ├── applications.rst ├── changelog.md ├── cli.rst ├── cli │ ├── config.rst │ ├── metadata.rst │ ├── plugin-sideloading.rst │ ├── plugins.rst │ ├── plugins │ │ ├── crunchyroll.rst │ │ ├── funimationnow.rst │ │ └── twitch.rst │ ├── protocols.rst │ ├── proxy.rst │ └── tutorial.rst ├── conf.py ├── deprecations.rst ├── developing.rst ├── docutils.conf ├── donate.rst ├── ext_argparse.py ├── ext_github.py ├── ext_html_template_vars.py ├── ext_plugins.py ├── ext_releaseref.py ├── index.rst ├── install.rst ├── issues.rst ├── players.rst ├── plugins.rst └── thirdparty.rst ├── examples └── opencv-face.py ├── icon.svg ├── netlify.toml ├── pyproject.toml ├── script ├── build-and-sign.sh ├── build-shell-completions.sh ├── deploy-docs.sh ├── deploy-pypi.sh ├── github-release.py ├── install-dependencies.sh └── update-user-agents.py ├── setup.cfg ├── setup.py ├── signing.key.gpg ├── src ├── streamlink │ ├── __init__.py │ ├── __main__.py │ ├── _version.py │ ├── api.py │ ├── buffers.py │ ├── cache.py │ ├── compat.py │ ├── exceptions.py │ ├── logger.py │ ├── options.py │ ├── packages │ │ ├── __init__.py │ │ └── requests_file.py │ ├── plugin │ │ ├── __init__.py │ │ ├── api │ │ │ ├── __init__.py │ │ │ ├── http_session.py │ │ │ ├── useragents.py │ │ │ ├── validate │ │ │ │ ├── __init__.py │ │ │ │ ├── _exception.py │ │ │ │ ├── _schemas.py │ │ │ │ ├── _validate.py │ │ │ │ └── _validators.py │ │ │ └── websocket.py │ │ └── plugin.py │ ├── plugins │ │ ├── __init__.py │ │ ├── abematv.py │ │ ├── adultswim.py │ │ ├── afreeca.py │ │ ├── albavision.py │ │ ├── aloula.py │ │ ├── app17.py │ │ ├── ard_live.py │ │ ├── ard_mediathek.py │ │ ├── artetv.py │ │ ├── atpchallenger.py │ │ ├── atresplayer.py │ │ ├── bbciplayer.py │ │ ├── bfmtv.py │ │ ├── bigo.py │ │ ├── bilibili.py │ │ ├── blazetv.py │ │ ├── bloomberg.py │ │ ├── booyah.py │ │ ├── brightcove.py │ │ ├── btv.py │ │ ├── cbsnews.py │ │ ├── cdnbg.py │ │ ├── ceskatelevize.py │ │ ├── cinergroup.py │ │ ├── clubbingtv.py │ │ ├── cmmedia.py │ │ ├── cnews.py │ │ ├── crunchyroll.py │ │ ├── dailymotion.py │ │ ├── dash.py │ │ ├── delfi.py │ │ ├── deutschewelle.py │ │ ├── dlive.py │ │ ├── dogan.py │ │ ├── dogus.py │ │ ├── drdk.py │ │ ├── earthcam.py │ │ ├── egame.py │ │ ├── euronews.py │ │ ├── facebook.py │ │ ├── filmon.py │ │ ├── foxtr.py │ │ ├── funimationnow.py │ │ ├── galatasaraytv.py │ │ ├── goltelevision.py │ │ ├── goodgame.py │ │ ├── googledrive.py │ │ ├── gulli.py │ │ ├── hiplayer.py │ │ ├── hls.py │ │ ├── http.py │ │ ├── htv.py │ │ ├── huajiao.py │ │ ├── huya.py │ │ ├── idf1.py │ │ ├── invintus.py │ │ ├── kugou.py │ │ ├── linelive.py │ │ ├── livestream.py │ │ ├── lnk.py │ │ ├── lrt.py │ │ ├── ltv_lsm_lv.py │ │ ├── mdstrm.py │ │ ├── mediaklikk.py │ │ ├── mediavitrina.py │ │ ├── mildom.py │ │ ├── mitele.py │ │ ├── mjunoon.py │ │ ├── mrtmk.py │ │ ├── n13tv.py │ │ ├── nbcnews.py │ │ ├── nhkworld.py │ │ ├── nicolive.py │ │ ├── nimotv.py │ │ ├── nos.py │ │ ├── nownews.py │ │ ├── nrk.py │ │ ├── ntv.py │ │ ├── okru.py │ │ ├── olympicchannel.py │ │ ├── oneplusone.py │ │ ├── onetv.py │ │ ├── openrectv.py │ │ ├── orf_tvthek.py │ │ ├── pandalive.py │ │ ├── picarto.py │ │ ├── piczel.py │ │ ├── pixiv.py │ │ ├── pluto.py │ │ ├── pluzz.py │ │ ├── qq.py │ │ ├── radiko.py │ │ ├── radionet.py │ │ ├── raiplay.py │ │ ├── reuters.py │ │ ├── rtbf.py │ │ ├── rtpa.py │ │ ├── rtpplay.py │ │ ├── rtve.py │ │ ├── rtvs.py │ │ ├── ruv.py │ │ ├── sbscokr.py │ │ ├── schoolism.py │ │ ├── showroom.py │ │ ├── sportal.py │ │ ├── sportschau.py │ │ ├── ssh101.py │ │ ├── stadium.py │ │ ├── steam.py │ │ ├── streamable.py │ │ ├── streann.py │ │ ├── stv.py │ │ ├── svtplay.py │ │ ├── swisstxt.py │ │ ├── telefe.py │ │ ├── tf1.py │ │ ├── trovo.py │ │ ├── turkuvaz.py │ │ ├── tv360.py │ │ ├── tv3cat.py │ │ ├── tv4play.py │ │ ├── tv5monde.py │ │ ├── tv8.py │ │ ├── tv999.py │ │ ├── tvibo.py │ │ ├── tviplayer.py │ │ ├── tvp.py │ │ ├── tvrby.py │ │ ├── tvrplus.py │ │ ├── tvtoya.py │ │ ├── twitcasting.py │ │ ├── twitch.py │ │ ├── useetv.py │ │ ├── ustreamtv.py │ │ ├── ustvnow.py │ │ ├── vidio.py │ │ ├── vimeo.py │ │ ├── vinhlongtv.py │ │ ├── vk.py │ │ ├── vlive.py │ │ ├── vtvgo.py │ │ ├── wasd.py │ │ ├── webtv.py │ │ ├── welt.py │ │ ├── wwenetwork.py │ │ ├── youtube.py │ │ ├── yupptv.py │ │ ├── zattoo.py │ │ ├── zdf_mediathek.py │ │ ├── zeenews.py │ │ ├── zengatv.py │ │ └── zhanqi.py │ ├── py.typed │ ├── session.py │ ├── stream │ │ ├── __init__.py │ │ ├── dash.py │ │ ├── dash_manifest.py │ │ ├── ffmpegmux.py │ │ ├── file.py │ │ ├── hls.py │ │ ├── hls_playlist.py │ │ ├── http.py │ │ ├── segmented.py │ │ ├── stream.py │ │ └── wrappers.py │ ├── user_input.py │ └── utils │ │ ├── __init__.py │ │ ├── args.py │ │ ├── cache.py │ │ ├── crypto.py │ │ ├── data.py │ │ ├── formatter.py │ │ ├── l10n.py │ │ ├── module.py │ │ ├── named_pipe.py │ │ ├── parse.py │ │ ├── times.py │ │ └── url.py └── streamlink_cli │ ├── __init__.py │ ├── __main__.py │ ├── argparser.py │ ├── compat.py │ ├── console.py │ ├── constants.py │ ├── main.py │ ├── output.py │ ├── py.typed │ └── utils │ ├── __init__.py │ ├── formatter.py │ ├── http_server.py │ ├── path.py │ ├── player.py │ ├── progress.py │ └── versioncheck.py └── tests ├── __init__.py ├── cli ├── __init__.py ├── output │ ├── __init__.py │ ├── test_output.py │ └── test_playeroutput.py ├── test_argparser.py ├── test_cmdline.py ├── test_cmdline_player_fifo.py ├── test_cmdline_title.py ├── test_console.py ├── test_main.py ├── test_main_formatter.py └── utils │ ├── __init__.py │ ├── test_formatter.py │ ├── test_path.py │ ├── test_progress.py │ └── test_versioncheck.py ├── mixins ├── __init__.py └── stream_hls.py ├── plugin ├── __init__.py ├── override │ └── testplugin.py ├── testplugin.py ├── testplugin_invalid.py └── testplugin_missing.py ├── plugins ├── __init__.py ├── conftest.py ├── test_abematv.py ├── test_adultswim.py ├── test_afreeca.py ├── test_albavision.py ├── test_aloula.py ├── test_app17.py ├── test_ard_live.py ├── test_ard_mediathek.py ├── test_artetv.py ├── test_atpchallenger.py ├── test_atresplayer.py ├── test_bbciplayer.py ├── test_bfmtv.py ├── test_bigo.py ├── test_bilibili.py ├── test_blazetv.py ├── test_bloomberg.py ├── test_booyah.py ├── test_brightcove.py ├── test_btv.py ├── test_cbsnews.py ├── test_cdnbg.py ├── test_ceskatelevize.py ├── test_cinergroup.py ├── test_clubbingtv.py ├── test_cmmedia.py ├── test_cnews.py ├── test_crunchyroll.py ├── test_dailymotion.py ├── test_dash.py ├── test_delfi.py ├── test_deutschewelle.py ├── test_dlive.py ├── test_dogan.py ├── test_dogus.py ├── test_drdk.py ├── test_earthcam.py ├── test_egame.py ├── test_euronews.py ├── test_facebook.py ├── test_filmon.py ├── test_foxtr.py ├── test_funimationnow.py ├── test_galatasaraytv.py ├── test_goltelevision.py ├── test_goodgame.py ├── test_googledrive.py ├── test_gulli.py ├── test_hiplayer.py ├── test_htv.py ├── test_huajiao.py ├── test_huya.py ├── test_idf1.py ├── test_invintus.py ├── test_kugou.py ├── test_linelive.py ├── test_livestream.py ├── test_lnk.py ├── test_lrt.py ├── test_ltv_lsm_lv.py ├── test_mdstrm.py ├── test_mediaklikk.py ├── test_mediavitrina.py ├── test_mildom.py ├── test_mitele.py ├── test_mjunoon.py ├── test_mrtmk.py ├── test_n13tv.py ├── test_nbcnews.py ├── test_nhkworld.py ├── test_nicolive.py ├── test_nimotv.py ├── test_nos.py ├── test_nownews.py ├── test_nrk.py ├── test_ntv.py ├── test_okru.py ├── test_olympicchannel.py ├── test_oneplusone.py ├── test_onetv.py ├── test_openrectv.py ├── test_orf_tvthek.py ├── test_pandalive.py ├── test_picarto.py ├── test_piczel.py ├── test_pixiv.py ├── test_pluto.py ├── test_pluzz.py ├── test_qq.py ├── test_radiko.py ├── test_radionet.py ├── test_raiplay.py ├── test_reuters.py ├── test_rtbf.py ├── test_rtpa.py ├── test_rtpplay.py ├── test_rtve.py ├── test_rtvs.py ├── test_ruv.py ├── test_sbscokr.py ├── test_schoolism.py ├── test_showroom.py ├── test_sportal.py ├── test_sportschau.py ├── test_ssh101.py ├── test_stadium.py ├── test_steam.py ├── test_stream.py ├── test_streamable.py ├── test_streann.py ├── test_stv.py ├── test_svtplay.py ├── test_swisstxt.py ├── test_telefe.py ├── test_tf1.py ├── test_trovo.py ├── test_turkuvaz.py ├── test_tv360.py ├── test_tv3cat.py ├── test_tv4play.py ├── test_tv5monde.py ├── test_tv8.py ├── test_tv999.py ├── test_tvibo.py ├── test_tviplayer.py ├── test_tvp.py ├── test_tvrby.py ├── test_tvrplus.py ├── test_tvtoya.py ├── test_twitcasting.py ├── test_twitch.py ├── test_useetv.py ├── test_ustreamtv.py ├── test_ustvnow.py ├── test_vidio.py ├── test_vimeo.py ├── test_vinhlongtv.py ├── test_vk.py ├── test_vlive.py ├── test_vtvgo.py ├── test_wasd.py ├── test_webtv.py ├── test_welt.py ├── test_wwenetwork.py ├── test_youtube.py ├── test_yupptv.py ├── test_zattoo.py ├── test_zdf_mediathek.py ├── test_zeenews.py ├── test_zengatv.py └── test_zhanqi.py ├── resources ├── __init__.py ├── cli │ └── config │ │ ├── custom │ │ ├── primary │ │ ├── primary.testplugin │ │ ├── secondary │ │ └── secondary.testplugin ├── dash │ ├── test_1.mpd │ ├── test_10.mpd │ ├── test_11_static.mpd │ ├── test_2.mpd │ ├── test_3.mpd │ ├── test_4.mpd │ ├── test_5.mpd │ ├── test_6_p1.mpd │ ├── test_6_p2.mpd │ ├── test_7.mpd │ ├── test_8.mpd │ └── test_9.mpd └── hls │ ├── test_1.m3u8 │ ├── test_2.m3u8 │ ├── test_date.m3u8 │ ├── test_master.m3u8 │ └── test_master_twitch_vod.m3u8 ├── stream ├── __init__.py ├── test_dash.py ├── test_dash_parser.py ├── test_ffmpegmux.py ├── test_file.py ├── test_hls.py ├── test_hls_filtered.py ├── test_hls_playlist.py ├── test_stream_json.py ├── test_stream_to_url.py └── test_stream_wrappers.py ├── test_api_http_session.py ├── test_api_validate.py ├── test_api_websocket.py ├── test_buffer.py ├── test_cache.py ├── test_logger.py ├── test_options.py ├── test_plugin.py ├── test_plugin_userinput.py ├── test_plugins.py ├── test_session.py ├── test_streamlink_api.py └── utils ├── __init__.py ├── test_args.py ├── test_cache.py ├── test_crypto.py ├── test_data.py ├── test_formatter.py ├── test_l10n.py ├── test_module.py ├── test_named_pipe.py ├── test_parse.py ├── test_times.py └── test_url.py /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | max_line_length = 128 10 | trim_trailing_whitespace = true 11 | 12 | [*.{cfg,svg,toml,yml}] 13 | indent_size = 2 14 | 15 | [*.{md,markdown}] 16 | trim_trailing_whitespace = false 17 | 18 | [docs/Makefile] 19 | indent_size = 8 20 | indent_style = tab 21 | 22 | [docs/{_templates/**.html,_static/**.css}] 23 | indent_size = 2 24 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | src/streamlink/_version.py export-subst 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | open_collective: streamlink 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: "Help" 4 | about: "The issue tracker is only meant for bug reports, feature/plugin requests and plugin issues. Please ask your questions on the discussions forum and look for already existing answers." 5 | url: https://github.com/streamlink/streamlink/discussions 6 | - name: "Documentation" 7 | about: "Please refer to Streamlink's documentation first before opening new issues or asking questions." 8 | url: https://streamlink.github.io/ 9 | - name: "Gitter channel" 10 | about: "Get in touch with other Streamlink users." 11 | url: https://gitter.im/streamlink/streamlink 12 | - name: "Matrix channel (Gitter bridge)" 13 | about: "Get in touch with other Streamlink users." 14 | url: https://matrix.to/#/#streamlink_streamlink:gitter.im 15 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 14 | -------------------------------------------------------------------------------- /.github/release_template.md: -------------------------------------------------------------------------------- 1 | {{ changelog }} 2 | 3 | ## 📦 Download and Installation 4 | 5 | Please see the [installation instructions](https://streamlink.github.io/install.html) for a list of available install methods and packages on the supported operating systems. 6 | 7 | **⚠️ PLEASE NOTE ⚠️** 8 | Streamlink's Windows installers have been moved to [streamlink/windows-builds](https://github.com/streamlink/windows-builds). 9 | 10 | ## ⚙️ Configuration and Usage 11 | 12 | Please see the [CLI documentation](https://streamlink.github.io/cli.html) for how to configure and use Streamlink. 13 | 14 | ## ❤️ Support 15 | 16 | If you think that Streamlink is useful and if you want to keep the project alive, then please consider supporting its maintainers by sending a small and optionally recurring tip via the [available options](https://streamlink.github.io/donate.html). 17 | Your support is very much appreciated, thank you! 18 | {%- if contributors %} 19 | 20 | ## 🙏 Contributors 21 | {% for contributor in contributors %} 22 | - {{ contributor.commits }}: @{{ contributor.name }} 23 | {%- endfor %} 24 | {%- endif %} 25 | {%- if gitshortlog %} 26 | 27 | ## 🗒️ Full changelog 28 | 29 | ```text 30 | {{ gitshortlog }} 31 | ``` 32 | {%- endif %} 33 | -------------------------------------------------------------------------------- /.github/workflows/useragents.yml: -------------------------------------------------------------------------------- 1 | name: Update user agents 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 1 * *' 6 | 7 | jobs: 8 | update-user-agents: 9 | if: github.repository == 'streamlink/streamlink' 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: actions/setup-python@v3 14 | with: 15 | python-version: "3.10" 16 | - run: python -m pip install requests 17 | - run: python ./script/update-user-agents.py 18 | env: 19 | WHATISMYBROWSER_API_KEY: ${{ secrets.WHATISMYBROWSER_API_KEY }} 20 | - uses: peter-evans/create-pull-request@10db75894f6d53fc01c3bb0995e95bd03e583a62 21 | with: 22 | token: ${{ secrets.STREAMLINKBOT_USERAGENTS_PR }} 23 | add-paths: | 24 | src/streamlink/plugin/api/useragents.py 25 | commit-message: "plugin.api: update useragents" 26 | committer: "streamlinkbot " 27 | author: "streamlinkbot " 28 | branch: "automated/plugin/api/useragents" 29 | branch-suffix: timestamp 30 | delete-branch: true 31 | title: "plugin.api: update useragents" 32 | body: "Automated pull request" 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS and editors 2 | .DS_Store 3 | ._* 4 | Thumbs.db 5 | Desktop.ini 6 | *.bak 7 | .cache 8 | .project 9 | .settings 10 | .tmproj 11 | nbproject 12 | *.sublime-project 13 | *.sublime-workspace 14 | .idea 15 | 16 | /build/ 17 | /completions/ 18 | /docs/_build/ 19 | /dist/ 20 | 21 | *.mo 22 | *.egg-info 23 | *.egg 24 | *.EGG 25 | *.EGG-INFO 26 | bin 27 | build-win32 28 | develop-eggs 29 | downloads 30 | eggs 31 | fake-eggs 32 | parts 33 | .installed.cfg 34 | .mr.developer.cfg 35 | .hg 36 | .bzr 37 | .svn 38 | *.pyc 39 | *.pyo 40 | *.tmp* 41 | *.swp 42 | include/ 43 | lib/ 44 | local/ 45 | share/ 46 | pip-selfcheck.json 47 | .pytest_cache/ 48 | .mypy_cache/ 49 | 50 | # ignore any key files 51 | *.key 52 | 53 | # coverage 54 | .coverage 55 | coverage.xml 56 | htmlcov 57 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Thank you to everyone who has contributed to streamlink / livestreamer. 2 | 3 | For a list of contributors, see 4 | https://github.com/streamlink/streamlink/graphs/contributors 5 | or use the command `git log --format='%aN' | sort -uf` 6 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | Minimalist Code of Conduct 2 | ==== 3 | 4 | We believe that an elaborate code of conduct invites a lot of fighting over intricate details, much like law, and we do not have the resources to build up the equivalent of a legal system. Therefore we prefer keeping our rules as short as possible and filling the gaps with the mortar of human interaction: empathy. 5 | 6 | All we ask of members and contributors of this project is this: 7 | 8 | - Please treat each other with respect and understanding. 9 | - Please respect our wish to not serve as a stage for disputes about fairness or personal differences. 10 | 11 | If you can agree to these conditions, your contributions are welcome. If you can not, please don't spoil it for the rest of us. 12 | -------------------------------------------------------------------------------- /KNOWN_ISSUES.md: -------------------------------------------------------------------------------- 1 | # Known issues 2 | 3 | For a detailed list of known issues, see here: 4 | https://streamlink.github.io/players.html#known-issues-and-workarounds 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-2016, Christopher Rosell 2 | Copyright (c) 2016-2022, Streamlink Team 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 18 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 21 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS 2 | include CHANGELOG.md 3 | include README.md 4 | include LICENSE* 5 | include *requirements.txt 6 | 7 | recursive-include completions * 8 | recursive-include docs * 9 | recursive-include examples * 10 | recursive-include tests * 11 | 12 | prune docs/_build 13 | include docs/_build/man/* 14 | 15 | prune */__pycache__ 16 | global-exclude *.pyc *~ *.bak *.swp *.pyo 17 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | pip>=9 2 | pytest 3 | pytest-cov 4 | coverage[toml] 5 | requests-mock 6 | freezegun>=1.0.0 7 | flake8 8 | flake8-import-order 9 | shtab 10 | versioningit >=2.0.0, <3 11 | 12 | mypy 13 | lxml-stubs 14 | types-freezegun 15 | types-requests 16 | types-urllib3 17 | -------------------------------------------------------------------------------- /docs-requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx >=4.0.0 2 | furo ==2022.06.04.1 3 | myst-parser >=0.18.0 4 | versioningit >=2.0.0, <3 5 | 6 | docutils-stubs 7 | -------------------------------------------------------------------------------- /docs/_applications.rst: -------------------------------------------------------------------------------- 1 | :orphan: 2 | 3 | 4 | .. raw:: html 5 | 6 | 14 | 15 | .. |Windows| raw:: html 16 | 17 | 18 | 19 | .. |MacOS| raw:: html 20 | 21 | 22 | 23 | .. |Linux| raw:: html 24 | 25 | 26 | -------------------------------------------------------------------------------- /docs/_man.rst: -------------------------------------------------------------------------------- 1 | :orphan: 2 | 3 | 4 | Streamlink 5 | ========== 6 | 7 | Synopsis 8 | -------- 9 | 10 | .. code-block:: console 11 | 12 | streamlink [OPTIONS] [STREAM] 13 | 14 | streamlink --loglevel debug youtu.be/VIDEO-ID best 15 | streamlink --player mpv --player-args '--no-border --no-keepaspect-window' twitch.tv/CHANNEL 1080p60 16 | streamlink --player-external-http --player-external-http-port 8888 URL STREAM 17 | streamlink --output /path/to/file --http-timeout 60 URL STREAM 18 | streamlink --stdout URL STREAM | ffmpeg -i pipe:0 ... 19 | streamlink --http-header 'Authorization=OAuth TOKEN' --http-header 'Referer=URL' URL STREAM 20 | streamlink --hls-live-edge 5 --stream-segment-threads 5 'hls://https://host/playlist.m3u8' best 21 | streamlink --twitch-low-latency -p mpv -a '--cache=yes --demuxer-max-bytes=750k' twitch.tv/CHANNEL best 22 | 23 | 24 | Options 25 | ======= 26 | 27 | .. argparse:: 28 | :module: streamlink_cli.main 29 | :attr: parser_helper 30 | -------------------------------------------------------------------------------- /docs/_static/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunghome/streamlink_optionalkey/63c387cb8d088a6a41cfd128348aa237329a0f5c/docs/_static/apple-touch-icon.png -------------------------------------------------------------------------------- /docs/_static/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunghome/streamlink_optionalkey/63c387cb8d088a6a41cfd128348aa237329a0f5c/docs/_static/favicon-16x16.png -------------------------------------------------------------------------------- /docs/_static/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunghome/streamlink_optionalkey/63c387cb8d088a6a41cfd128348aa237329a0f5c/docs/_static/favicon-32x32.png -------------------------------------------------------------------------------- /docs/_static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunghome/streamlink_optionalkey/63c387cb8d088a6a41cfd128348aa237329a0f5c/docs/_static/favicon.ico -------------------------------------------------------------------------------- /docs/_static/icon.svg: -------------------------------------------------------------------------------- 1 | ../../icon.svg -------------------------------------------------------------------------------- /docs/_static/opengraph-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunghome/streamlink_optionalkey/63c387cb8d088a6a41cfd128348aa237329a0f5c/docs/_static/opengraph-image.png -------------------------------------------------------------------------------- /docs/_static/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Streamlink documentation", 3 | "short_name": "Streamlink", 4 | "display": "standalone", 5 | "theme_color": "#121657", 6 | "background_color": "#ffffff", 7 | "icons": [ 8 | { 9 | "src": "/_static/icon.svg", 10 | "sizes": "1x1", 11 | "type": "image/svg" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /docs/_templates/base.html: -------------------------------------------------------------------------------- 1 | {% extends '!base.html' %} 2 | {%- block site_meta -%} 3 | {{ super() }} 4 | 5 | 6 | 7 | 8 | 9 | {% if not docstitle -%} 10 | 11 | {%- elif pagename == master_doc -%} 12 | 13 | {%- else -%} 14 | 15 | {%- endif %} 16 | 17 | 18 | 19 | 20 | {%- endblock -%} 21 | -------------------------------------------------------------------------------- /docs/_templates/page.html: -------------------------------------------------------------------------------- 1 | {% extends '!page.html' %} 2 | {% block content %} 3 | {% if release != version %} 4 |
5 |

You are reading the documentation for the in-development version of Streamlink.

6 |
7 | {% endif %} 8 | {{ super() }} 9 | {% endblock %} 10 | -------------------------------------------------------------------------------- /docs/_templates/sidebar/brand.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 15 | -------------------------------------------------------------------------------- /docs/_templates/sidebar/github-buttons.html: -------------------------------------------------------------------------------- 1 |
2 | Github 7 | Issues 10 | 11 |
12 | -------------------------------------------------------------------------------- /docs/applications.rst: -------------------------------------------------------------------------------- 1 | .. include:: _applications.rst 2 | 3 | 4 | Streamlink Applications 5 | ======================= 6 | 7 | 8 | Streamlink Twitch GUI 9 | --------------------- 10 | 11 | .. image:: https://user-images.githubusercontent.com/467294/151660267-97a8c0a3-62b3-4aa5-b960-c307da1b4fb2.png 12 | :alt: Streamlink Twitch GUI 13 | 14 | :Description: A multi platform Twitch.tv browser for Streamlink 15 | :Type: Graphical User Interface 16 | :OS: |Windows| |MacOS| |Linux| 17 | :Author: `Sebastian Meyer `_ 18 | :Website: https://streamlink.github.io/streamlink-twitch-gui 19 | :Source code: https://github.com/streamlink/streamlink-twitch-gui 20 | :Info: An NW.js based desktop web application. Browse Twitch.tv and watch multiple streams at once. 21 | Filter streams by language, receive desktop notifications when followed channels start 22 | streaming and access the Twitch chat by using customizable chat applications. 23 | -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- 1 | ../CHANGELOG.md -------------------------------------------------------------------------------- /docs/cli.rst: -------------------------------------------------------------------------------- 1 | Command-Line Interface 2 | ====================== 3 | 4 | .. toctree:: 5 | :maxdepth: 3 6 | :caption: Table of Contents 7 | 8 | Command-line usage 9 | Tutorial 10 | Configuration file 11 | Plugin sideloading 12 | Streaming protocols 13 | Proxy support 14 | Metadata 15 | Plugin specific usage 16 | 17 | 18 | Command-line usage 19 | ------------------ 20 | 21 | .. code-block:: console 22 | 23 | $ streamlink [OPTIONS] [STREAM] 24 | 25 | 26 | .. argparse:: 27 | :module: streamlink_cli.main 28 | :attr: parser_helper 29 | -------------------------------------------------------------------------------- /docs/cli/plugin-sideloading.rst: -------------------------------------------------------------------------------- 1 | Plugin sideloading 2 | ================== 3 | 4 | Streamlink will attempt to load standalone plugins from these directories: 5 | 6 | .. rst-class:: table-custom-layout table-custom-layout-platform-locations 7 | 8 | ================= ==================================================== 9 | Platform Location 10 | ================= ==================================================== 11 | Linux, BSD - ``${XDG_DATA_HOME:-${HOME}/.local/share}/streamlink/plugins`` 12 | 13 | Deprecated: 14 | 15 | - ``${XDG_CONFIG_HOME:-${HOME}/.config}/streamlink/plugins`` 16 | macOS - ``${HOME}/Library/Application Support/streamlink/plugins`` 17 | 18 | Deprecated: 19 | 20 | - ``${XDG_CONFIG_HOME:-${HOME}/.config}/streamlink/plugins`` 21 | Windows - ``%APPDATA%\streamlink\plugins`` 22 | ================= ==================================================== 23 | 24 | .. note:: 25 | 26 | If a plugin is added with the same name as a built-in plugin, then 27 | the added plugin will take precedence. This is useful if you want 28 | to upgrade plugins independently of the Streamlink version. 29 | 30 | .. warning:: 31 | 32 | If one of the sideloaded plugins fails to load, eg. due to a 33 | ``SyntaxError`` being raised by the parser, this exception will 34 | not get caught by Streamlink and the execution will stop, even if 35 | the input stream URL does not match the faulty plugin. 36 | -------------------------------------------------------------------------------- /docs/cli/plugins.rst: -------------------------------------------------------------------------------- 1 | Plugin specific usage 2 | ===================== 3 | 4 | 5 | .. toctree:: 6 | plugins/crunchyroll 7 | plugins/funimationnow 8 | plugins/twitch 9 | -------------------------------------------------------------------------------- /docs/cli/plugins/funimationnow.rst: -------------------------------------------------------------------------------- 1 | FunimationNow 2 | ============= 3 | 4 | Authentication 5 | -------------- 6 | 7 | Like Crunchyroll, the FunimationNow plugin requires authenticating with a premium account to access some 8 | content: :option:`--funimation-email`, :option:`--funimation-password`. In addition, this plugin requires 9 | the ``incap_ses`` cookie to be sent with each HTTP request (see issue #2088). This unique session cookie 10 | can be found in your browser and sent via the :option:`--http-cookie` option. 11 | 12 | .. code-block:: console 13 | 14 | $ streamlink --funimation-email='xxx' --funimation-password='xxx' --http-cookie 'incap_ses_xxx=xxxx=' https://funimation.com/shows/show/an-episode-link 15 | 16 | .. note:: 17 | 18 | There are multiple ways to retrieve the required cookie. For more 19 | information on browser cookies, please consult the following: 20 | 21 | - `What are cookies? `_ 22 | -------------------------------------------------------------------------------- /docs/cli/proxy.rst: -------------------------------------------------------------------------------- 1 | Proxy Support 2 | ------------- 3 | 4 | You can use the :option:`--http-proxy` option to change the proxy server 5 | that Streamlink will use for HTTP and HTTPS requests. :option:`--http-proxy` sets 6 | the proxy for all HTTP and HTTPS requests, including WebSocket connections. 7 | 8 | If separate proxies for each protocol are required, they can be set using 9 | environment variables - see the `Requests Proxies Documentation`_. 10 | 11 | Both HTTP and SOCKS proxies are supported, as well as authentication in each of them. 12 | 13 | .. note:: 14 | When using a SOCKS proxy, the ``socks4`` and ``socks5`` schemes mean that DNS lookups are done 15 | locally, rather than on the proxy server. To have the proxy server perform the DNS lookups, the 16 | ``socks4a`` and ``socks5h`` schemes should be used instead. 17 | 18 | .. code-block:: console 19 | 20 | $ streamlink --http-proxy "http://address:port" 21 | $ streamlink --http-proxy "https://address:port" 22 | $ streamlink --http-proxy "socks4a://address:port" 23 | $ streamlink --http-proxy "socks5h://address:port" 24 | 25 | .. _Requests Proxies Documentation: https://2.python-requests.org/en/master/user/advanced/#proxies 26 | -------------------------------------------------------------------------------- /docs/docutils.conf: -------------------------------------------------------------------------------- 1 | [restructuredtext parser] 2 | smart_quotes=no 3 | 4 | -------------------------------------------------------------------------------- /docs/ext_html_template_vars.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | 3 | from sphinx.addnodes import document 4 | from sphinx.application import Sphinx 5 | 6 | 7 | _CONFIG_VAR = "html_template_vars" 8 | 9 | 10 | def update_context( 11 | app: Sphinx, 12 | pagename: str, 13 | templatename: str, 14 | context: Dict[str, Any], 15 | doctree: document, 16 | ) -> None: 17 | for k, v in getattr(app.config, _CONFIG_VAR).items(): 18 | context[k] = v 19 | 20 | 21 | def setup(app: Sphinx) -> None: 22 | app.add_config_value(_CONFIG_VAR, {}, "") 23 | app.connect("html-page-context", update_context) 24 | -------------------------------------------------------------------------------- /docs/ext_releaseref.py: -------------------------------------------------------------------------------- 1 | """Creates links that allows substituting the current release 2 | within the title or target.""" 3 | 4 | import os.path 5 | 6 | from docutils import nodes 7 | from sphinx.util.nodes import split_explicit_title 8 | 9 | 10 | def releaseref_role(name, rawtext, text, lineno, inliner, options={}, content=[]): 11 | config = inliner.document.settings.env.config 12 | text = text.replace("|version|", config.version) 13 | text = text.replace("|release|", config.release) 14 | 15 | has_explicit_title, title, target = split_explicit_title(text) 16 | if not has_explicit_title: 17 | title = os.path.basename(target) 18 | 19 | node = nodes.reference(rawtext, title, refuri=target, **options) 20 | 21 | return [node], [] 22 | 23 | 24 | def setup(app): 25 | app.add_role("releaseref", releaseref_role) 26 | -------------------------------------------------------------------------------- /docs/plugins.rst: -------------------------------------------------------------------------------- 1 | Plugins 2 | ======= 3 | 4 | This is a list of the currently built-in plugins and what URLs and features 5 | they support. Streamlink's primary focus is live streams, so VOD support 6 | is limited. 7 | 8 | 9 | .. plugins:: 10 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "setuptools.build_meta" 3 | requires = [ 4 | "setuptools >=45", 5 | "wheel", 6 | "versioningit >=2.0.0, <3", 7 | ] 8 | 9 | 10 | # https://versioningit.readthedocs.io/en/stable/index.html 11 | [tool.versioningit] 12 | default-version = "0.0.0+unknown" 13 | 14 | [tool.versioningit.vcs] 15 | method = "git" 16 | 17 | [tool.versioningit.format] 18 | distance = "{base_version}+{distance}.{vcs}{rev}" 19 | dirty = "{base_version}+{distance}.{vcs}{rev}.dirty" 20 | distance-dirty = "{base_version}+{distance}.{vcs}{rev}.dirty" 21 | 22 | [tool.versioningit.next-version] 23 | method = "null" 24 | 25 | [tool.versioningit.onbuild] 26 | source-file = "src/streamlink/_version.py" 27 | build-file = "streamlink/_version.py" 28 | 29 | 30 | # https://coverage.readthedocs.io/en/latest/config.html 31 | [tool.coverage.run] 32 | branch = true 33 | source = [ 34 | "src", 35 | "tests", 36 | ] 37 | 38 | [tool.coverage.report] 39 | omit = [ 40 | "src/streamlink/packages/*", 41 | "src/streamlink_cli/packages/*", 42 | ] 43 | exclude_lines = [ 44 | "pragma: no cover", 45 | "def __repr__", 46 | "raise NotImplementedError", 47 | "if __name__ == \"__main__\":", 48 | ] 49 | 50 | 51 | # https://mypy.readthedocs.io/en/stable/config_file.html 52 | [tool.mypy] 53 | python_version = 3.7 54 | show_error_codes = true 55 | show_error_context = true 56 | show_column_numbers = true 57 | warn_no_return = false 58 | -------------------------------------------------------------------------------- /script/build-shell-completions.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | ROOT=$(git rev-parse --show-toplevel 2>/dev/null || realpath "$(dirname "$(readlink -f "${0}")")/..") 5 | 6 | DIST="${ROOT}/completions" 7 | PYTHON_DEPS=(streamlink_cli shtab) 8 | 9 | declare -A COMPLETIONS=( 10 | [bash]="streamlink" 11 | # ZSH requires the file to be prefixed with an underscore 12 | [zsh]="_streamlink" 13 | ) 14 | 15 | for dep in "${PYTHON_DEPS[@]}"; do 16 | python -c "import ${dep}" 2>&1 >/dev/null \ 17 | || { echo >&2 "${dep} could not be found in your python environment"; exit 1; } 18 | done 19 | 20 | for shell in "${!COMPLETIONS[@]}"; do 21 | mkdir -p "${DIST}/${shell}" 22 | dist="${DIST}/${shell}/${COMPLETIONS[${shell}]}" 23 | python -m shtab \ 24 | "--shell=${shell}" \ 25 | --error-unimportable \ 26 | streamlink_cli.main.parser_helper \ 27 | > "${dist}" 28 | echo "Completions for ${shell} written to ${dist}" 29 | done 30 | -------------------------------------------------------------------------------- /script/deploy-pypi.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | shopt -s nullglob 3 | set -e 4 | 5 | 6 | version=$(python setup.py --version) 7 | dist_dir=${STREAMLINK_DIST_DIR:-dist} 8 | 9 | 10 | if [[ "${1}" = "-n" ]] || [[ "${1}" = "--dry-run" ]]; then 11 | echo "deploy: dry-run (${version})" >&2 12 | for file in "${dist_dir}"/streamlink-"${version}"{.tar.gz,-*.whl}{,.asc}; do 13 | echo "${file}" >&2 14 | done 15 | 16 | else 17 | if ! python -m pip -q show twine; then 18 | echo "deploy: missing dependency 'twine'" >&2 19 | exit 1 20 | fi 21 | 22 | if [[ -z "${PYPI_USER}" ]] || [[ -z "${PYPI_PASSWORD}" ]]; then 23 | echo "deploy: missing PYPI_USER or PYPI_PASSWORD env var" >&2 24 | exit 1 25 | fi 26 | 27 | echo "deploy: Uploading files to PyPI (${version})" >&2 28 | twine upload \ 29 | --username "${PYPI_USER}" \ 30 | --password "${PYPI_PASSWORD}" \ 31 | "${dist_dir}"/streamlink-"${version}"{.tar.gz,-*.whl}{,.asc} 32 | fi 33 | -------------------------------------------------------------------------------- /script/install-dependencies.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | [[ "$CI" = true ]] || [[ -n "$GITHUB_ACTIONS" ]] || [[ -n "$VIRTUAL_ENV" ]] || exit 1 4 | 5 | set -ex 6 | 7 | python -m pip install --disable-pip-version-check --upgrade pip setuptools 8 | python -m pip install --upgrade -r dev-requirements.txt 9 | # https://github.com/streamlink/streamlink/issues/4021 10 | python -m pip install brotli 11 | python -m pip install -e . 12 | -------------------------------------------------------------------------------- /signing.key.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunghome/streamlink_optionalkey/63c387cb8d088a6a41cfd128348aa237329a0f5c/signing.key.gpg -------------------------------------------------------------------------------- /src/streamlink/__init__.py: -------------------------------------------------------------------------------- 1 | """Streamlink extracts streams from various services. 2 | 3 | The main compontent of Streamlink is a command-line utility that 4 | launches the streams in a video player. 5 | 6 | An API is also provided that allows direct access to stream data. 7 | 8 | Full documentation is available at https://streamlink.github.io. 9 | 10 | """ 11 | from streamlink._version import __version__ 12 | 13 | __title__ = "streamlink" 14 | __license__ = "Simplified BSD" 15 | __author__ = "Streamlink" 16 | __copyright__ = "Copyright 2022 Streamlink" 17 | __credits__ = ["https://github.com/streamlink/streamlink/blob/master/AUTHORS"] 18 | 19 | from streamlink.api import streams 20 | from streamlink.exceptions import (StreamlinkError, PluginError, NoStreamsError, 21 | NoPluginError, StreamError) 22 | from streamlink.session import Streamlink 23 | -------------------------------------------------------------------------------- /src/streamlink/__main__.py: -------------------------------------------------------------------------------- 1 | if __name__ == "__main__": 2 | from streamlink_cli.main import main 3 | main() 4 | -------------------------------------------------------------------------------- /src/streamlink/_version.py: -------------------------------------------------------------------------------- 1 | # Always get the current version in "editable" installs 2 | # `pip install -e .` / `python setup.py develop` 3 | def _get_version() -> str: 4 | from pathlib import Path 5 | from versioningit import get_version 6 | import streamlink 7 | 8 | return get_version( 9 | project_dir=Path(streamlink.__file__).parents[2] 10 | ) 11 | 12 | 13 | # The following _get_version() call will get replaced by versioningit with a static version string when building streamlink 14 | # `pip install .` / `pip wheel .` / `python setup.py build` / `python setup.py bdist_wheel` / etc. 15 | __version__ = _get_version() 16 | -------------------------------------------------------------------------------- /src/streamlink/api.py: -------------------------------------------------------------------------------- 1 | from streamlink.session import Streamlink 2 | 3 | 4 | def streams(url: str, **params): 5 | """ 6 | Initializes an empty Streamlink session, attempts to find a plugin and extracts streams from the URL if a plugin was found. 7 | 8 | :param url: a URL to match against loaded plugins 9 | :param params: Additional keyword arguments passed to :meth:`streamlink.Streamlink.streams` 10 | :raises NoPluginError: on plugin resolve failure 11 | :returns: A :class:`dict` of stream names and :class:`streamlink.stream.Stream` instances 12 | """ 13 | 14 | session = Streamlink() 15 | 16 | return session.streams(url, **params) 17 | -------------------------------------------------------------------------------- /src/streamlink/compat.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | 5 | is_darwin = sys.platform == "darwin" 6 | is_win32 = os.name == "nt" 7 | 8 | # win/nix compatible devnull 9 | try: 10 | from subprocess import DEVNULL 11 | 12 | def devnull(): 13 | return DEVNULL 14 | except ImportError: 15 | def devnull(): 16 | return open(os.path.devnull, 'w') 17 | 18 | 19 | __all__ = [ 20 | "is_darwin", 21 | "is_win32", 22 | "devnull", 23 | ] 24 | -------------------------------------------------------------------------------- /src/streamlink/exceptions.py: -------------------------------------------------------------------------------- 1 | class StreamlinkError(Exception): 2 | """Any error caused by Streamlink will be caught 3 | with this exception.""" 4 | 5 | 6 | class PluginError(StreamlinkError): 7 | """Plugin related error.""" 8 | 9 | 10 | class FatalPluginError(PluginError): 11 | """ 12 | Plugin related error that cannot be recovered from 13 | 14 | Plugin's should use this Exception when errors that can 15 | never be recovered from are encountered. For example, when 16 | a user's input is required an none can be given. 17 | """ 18 | 19 | 20 | class NoStreamsError(StreamlinkError): 21 | def __init__(self, url): 22 | self.url = url 23 | err = "No streams found on this URL: {0}".format(url) 24 | Exception.__init__(self, err) 25 | 26 | 27 | class NoPluginError(PluginError): 28 | """No relevant plugin has been loaded.""" 29 | 30 | 31 | class StreamError(StreamlinkError): 32 | """Stream related error.""" 33 | 34 | 35 | __all__ = ["StreamlinkError", "PluginError", "NoPluginError", 36 | "NoStreamsError", "StreamError"] 37 | -------------------------------------------------------------------------------- /src/streamlink/packages/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunghome/streamlink_optionalkey/63c387cb8d088a6a41cfd128348aa237329a0f5c/src/streamlink/packages/__init__.py -------------------------------------------------------------------------------- /src/streamlink/plugin/__init__.py: -------------------------------------------------------------------------------- 1 | from streamlink.exceptions import PluginError 2 | from streamlink.options import Argument as PluginArgument, Arguments as PluginArguments, Options as PluginOptions 3 | from streamlink.plugin.plugin import ( 4 | HIGH_PRIORITY, 5 | LOW_PRIORITY, 6 | NORMAL_PRIORITY, 7 | NO_PRIORITY, 8 | Plugin, 9 | pluginargument, 10 | pluginmatcher, 11 | ) 12 | 13 | __all__ = [ 14 | "HIGH_PRIORITY", "NORMAL_PRIORITY", "LOW_PRIORITY", "NO_PRIORITY", 15 | "Plugin", "PluginArguments", "PluginArgument", "PluginError", "PluginOptions", 16 | "pluginmatcher", "pluginargument", 17 | ] 18 | -------------------------------------------------------------------------------- /src/streamlink/plugin/api/__init__.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugin.api.http_session import HTTPSession 2 | 3 | __all__ = ["HTTPSession"] 4 | -------------------------------------------------------------------------------- /src/streamlink/plugin/api/useragents.py: -------------------------------------------------------------------------------- 1 | ANDROID = "Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.5195.58 Mobile Safari/537.36" 2 | CHROME = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36" 3 | CHROME_OS = "Mozilla/5.0 (X11; CrOS x86_64 14909.100.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.83 Safari/537.36" 4 | FIREFOX = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:104.0) Gecko/20100101 Firefox/104.0" 5 | IE_11 = "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko" 6 | IPHONE = "Mozilla/5.0 (iPhone; CPU iPhone OS 15_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6 Mobile/15E148 Safari/604.1" 7 | OPERA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36 OPR/90.0.4480.54" 8 | SAFARI = "Mozilla/5.0 (Macintosh; Intel Mac OS X 12_5_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.6 Safari/605.1.15" 9 | 10 | # Backwards compatibility 11 | EDGE = CHROME 12 | FIREFOX_MAC = FIREFOX 13 | IE_6 = IE_7 = IE_8 = IE_9 = IE_11 14 | IPHONE_6 = IPAD = IPHONE 15 | SAFARI_7 = SAFARI_8 = SAFARI 16 | WINDOWS_PHONE_8 = ANDROID 17 | -------------------------------------------------------------------------------- /src/streamlink/plugin/api/validate/__init__.py: -------------------------------------------------------------------------------- 1 | # noinspection PyPep8Naming,PyShadowingBuiltins 2 | from streamlink.plugin.api.validate._schemas import ( # noqa: I101, F401 3 | SchemaContainer, 4 | AllSchema as all, 5 | AnySchema as any, 6 | NoneOrAllSchema as none_or_all, 7 | ListSchema as list, 8 | RegexSchema as regex, 9 | TransformSchema as transform, 10 | OptionalSchema as optional, 11 | GetItemSchema as get, 12 | AttrSchema as attr, 13 | UnionSchema as union, 14 | UnionGetSchema as union_get, 15 | XmlElementSchema as xml_element, 16 | ) 17 | from streamlink.plugin.api.validate._validate import ( # noqa: F401 18 | Schema, 19 | validate, 20 | ) 21 | # noinspection PyShadowingBuiltins 22 | from streamlink.plugin.api.validate._validators import ( # noqa: I101, F401 23 | validator_length as length, 24 | validator_startswith as startswith, 25 | validator_endswith as endswith, 26 | validator_contains as contains, 27 | validator_url as url, 28 | validator_getattr as getattr, 29 | validator_hasattr as hasattr, 30 | validator_filter as filter, 31 | validator_map as map, 32 | validator_xml_find as xml_find, 33 | validator_xml_findall as xml_findall, 34 | validator_xml_findtext as xml_findtext, 35 | validator_xml_xpath as xml_xpath, 36 | validator_xml_xpath_string as xml_xpath_string, 37 | validator_parse_json as parse_json, 38 | validator_parse_html as parse_html, 39 | validator_parse_xml as parse_xml, 40 | validator_parse_qsd as parse_qsd, 41 | ) 42 | 43 | 44 | text = str 45 | -------------------------------------------------------------------------------- /src/streamlink/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | New plugins should use streamlink.plugin.Plugin instead 3 | of this module, but this is kept here for backwards 4 | compatibility. 5 | """ 6 | 7 | from streamlink.exceptions import NoPluginError, NoStreamsError, PluginError 8 | from streamlink.plugin.plugin import Plugin 9 | 10 | __all__ = ['Plugin', 'PluginError', 'NoStreamsError', 'NoPluginError'] 11 | -------------------------------------------------------------------------------- /src/streamlink/plugins/atpchallenger.py: -------------------------------------------------------------------------------- 1 | """ 2 | $description Tennis tournaments organized by the Association of Tennis Professionals. 3 | $url atptour.com/en/atp-challenger-tour/challenger-tv 4 | $type live, vod 5 | """ 6 | 7 | import re 8 | 9 | from streamlink.plugin import Plugin, pluginmatcher 10 | from streamlink.plugin.api import validate 11 | 12 | 13 | @pluginmatcher(re.compile( 14 | r"https?://(?:www\.)?atptour\.com/(?:en|es)/atp-challenger-tour/challenger-tv" 15 | )) 16 | class AtpChallengerTour(Plugin): 17 | def _get_streams(self): 18 | iframe_url = self.session.http.get(self.url, schema=validate.Schema( 19 | validate.parse_html(), 20 | validate.xml_xpath_string(".//iframe[starts-with(@id,'vimeoPlayer_')][@src][1]/@src"), 21 | validate.any(None, validate.url()), 22 | )) 23 | if iframe_url: 24 | return self.session.streams(iframe_url) 25 | 26 | 27 | __plugin__ = AtpChallengerTour 28 | -------------------------------------------------------------------------------- /src/streamlink/plugins/bigo.py: -------------------------------------------------------------------------------- 1 | """ 2 | $description Global live streaming platform for live video game broadcasts and individual live streams. 3 | $url live.bigo.tv 4 | $url bigoweb.co 5 | $type live 6 | """ 7 | 8 | import re 9 | 10 | from streamlink.plugin import Plugin, pluginmatcher 11 | from streamlink.plugin.api import useragents, validate 12 | from streamlink.stream.hls import HLSStream 13 | 14 | 15 | @pluginmatcher(re.compile( 16 | r"https?://(?:www\.)?bigo\.tv/([^/]+)$" 17 | )) 18 | class Bigo(Plugin): 19 | _api_url = "https://www.bigo.tv/OInterface/getVideoParam?bigoId={0}" 20 | 21 | _video_info_schema = validate.Schema({ 22 | "code": 0, 23 | "msg": "success", 24 | "data": { 25 | "videoSrc": validate.any(None, "", validate.url()) 26 | } 27 | }) 28 | 29 | def _get_streams(self): 30 | res = self.session.http.get( 31 | self._api_url.format(self.match.group(1)), 32 | allow_redirects=True, 33 | headers={"User-Agent": useragents.IPHONE_6} 34 | ) 35 | data = self.session.http.json(res, schema=self._video_info_schema) 36 | videourl = data["data"]["videoSrc"] 37 | if videourl: 38 | yield "live", HLSStream(self.session, videourl) 39 | 40 | 41 | __plugin__ = Bigo 42 | -------------------------------------------------------------------------------- /src/streamlink/plugins/cbsnews.py: -------------------------------------------------------------------------------- 1 | """ 2 | $description 24-hour live-streaming world news channel, based in the United States of America. 3 | $url cbsnews.com 4 | $type live 5 | """ 6 | 7 | import re 8 | 9 | from streamlink.plugin import Plugin, pluginmatcher 10 | from streamlink.plugin.api import validate 11 | from streamlink.stream.hls import HLSStream 12 | 13 | 14 | @pluginmatcher(re.compile( 15 | r"https?://(?:www\.)?cbsnews\.com/(?:\w+/)?live/?" 16 | )) 17 | class CBSNews(Plugin): 18 | def _get_streams(self): 19 | data = self.session.http.get(self.url, schema=validate.Schema( 20 | re.compile(r"CBSNEWS\.defaultPayload\s*=\s*(\{.*?})\s*\n"), 21 | validate.none_or_all( 22 | validate.get(1), 23 | validate.parse_json(), 24 | { 25 | "items": [{ 26 | "id": str, 27 | "canonicalTitle": str, 28 | "video": validate.url(), 29 | "format": "application/x-mpegURL", 30 | }], 31 | }, 32 | validate.get(("items", 0)), 33 | validate.union_get("id", "canonicalTitle", "video"), 34 | ), 35 | )) 36 | if not data: 37 | return 38 | 39 | self.id, self.title, hls_url = data 40 | 41 | return HLSStream.parse_variant_playlist(self.session, hls_url) 42 | 43 | 44 | __plugin__ = CBSNews 45 | -------------------------------------------------------------------------------- /src/streamlink/plugins/cinergroup.py: -------------------------------------------------------------------------------- 1 | """ 2 | $description Turkish live TV channels from Ciner Group, including Haberturk TV and Show TV. 3 | $url showtv.com.tr 4 | $url haberturk.com 5 | $url haberturk.tv 6 | $url showmax.com.tr 7 | $url showturk.com.tr 8 | $type live 9 | """ 10 | 11 | import re 12 | 13 | from streamlink.plugin import Plugin, pluginmatcher 14 | from streamlink.plugin.api import validate 15 | from streamlink.stream.hls import HLSStream 16 | 17 | 18 | @pluginmatcher(re.compile(r""" 19 | https?://(?:www\.)? 20 | (?: 21 | showtv\.com\.tr/canli-yayin(/showtv)? 22 | | 23 | haberturk\.(?:com|tv)(?:/tv)?/canliyayin 24 | | 25 | showmax\.com\.tr/canliyayin 26 | | 27 | showturk\.com\.tr/canli-yayin/showturk 28 | )/? 29 | """, re.VERBOSE)) 30 | class CinerGroup(Plugin): 31 | def _get_streams(self): 32 | stream_url = self.session.http.get(self.url, schema=validate.Schema( 33 | validate.parse_html(), 34 | validate.xml_xpath_string(".//div[@data-ht][1]/@data-ht"), 35 | validate.none_or_all( 36 | validate.parse_json(), 37 | { 38 | "ht_stream_m3u8": validate.url(), 39 | }, 40 | validate.get("ht_stream_m3u8"), 41 | ), 42 | )) 43 | if stream_url: 44 | return HLSStream.parse_variant_playlist(self.session, stream_url) 45 | 46 | 47 | __plugin__ = CinerGroup 48 | -------------------------------------------------------------------------------- /src/streamlink/plugins/dash.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import re 3 | 4 | from streamlink.plugin import Plugin, pluginmatcher 5 | from streamlink.plugin.plugin import LOW_PRIORITY, parse_params, stream_weight 6 | from streamlink.stream.dash import DASHStream 7 | from streamlink.utils.url import update_scheme 8 | 9 | log = logging.getLogger(__name__) 10 | 11 | 12 | @pluginmatcher(re.compile( 13 | r"dash://(?P\S+)(?:\s(?P.+))?" 14 | )) 15 | @pluginmatcher(priority=LOW_PRIORITY, pattern=re.compile( 16 | r"(?P\S+\.mpd(?:\?\S*)?)(?:\s(?P.+))?" 17 | )) 18 | class MPEGDASH(Plugin): 19 | @classmethod 20 | def stream_weight(cls, stream): 21 | match = re.match(r"^(?:(.*)\+)?(?:a(\d+)k)$", stream) 22 | if match and match.group(1) and match.group(2): 23 | weight, group = stream_weight(match.group(1)) 24 | weight += int(match.group(2)) 25 | return weight, group 26 | elif match and match.group(2): 27 | return stream_weight(f"{match.group(2)}k") 28 | else: 29 | return stream_weight(stream) 30 | 31 | def _get_streams(self): 32 | data = self.match.groupdict() 33 | url = update_scheme("https://", data.get("url"), force=False) 34 | params = parse_params(data.get("params")) 35 | log.debug(f"URL={url}; params={params}") 36 | 37 | return DASHStream.parse_manifest(self.session, url, **params) 38 | 39 | 40 | __plugin__ = MPEGDASH 41 | -------------------------------------------------------------------------------- /src/streamlink/plugins/foxtr.py: -------------------------------------------------------------------------------- 1 | """ 2 | $description Turkish live TV channel owned by Fox Network. 3 | $url fox.com.tr 4 | $type live 5 | """ 6 | 7 | import re 8 | 9 | from streamlink.plugin import Plugin, pluginmatcher 10 | from streamlink.plugin.api import validate 11 | from streamlink.stream.hls import HLSStream 12 | 13 | 14 | @pluginmatcher(re.compile( 15 | r"https?://(?:www\.)?fox(?:play)?\.com\.tr/" 16 | )) 17 | class FoxTR(Plugin): 18 | def _get_streams(self): 19 | re_streams = re.compile(r"""(['"])(?Phttps://\S+/foxtv\.m3u8\S+)\1""") 20 | res = self.session.http.get(self.url, schema=validate.Schema( 21 | validate.transform(re_streams.findall) 22 | )) 23 | for _, stream_url in res: 24 | return HLSStream.parse_variant_playlist(self.session, stream_url) 25 | 26 | 27 | __plugin__ = FoxTR 28 | -------------------------------------------------------------------------------- /src/streamlink/plugins/galatasaraytv.py: -------------------------------------------------------------------------------- 1 | """ 2 | $description Turkish live TV channel owned by Galatasaray TV. 3 | $url galatasaray.com 4 | $type live 5 | """ 6 | 7 | import logging 8 | import re 9 | 10 | from streamlink.plugin import Plugin, pluginmatcher 11 | from streamlink.stream.hls import HLSStream 12 | 13 | log = logging.getLogger(__name__) 14 | 15 | 16 | @pluginmatcher(re.compile( 17 | r"https?://(?:www\.)?galatasaray\.com" 18 | )) 19 | class GalatasarayTV(Plugin): 20 | playervars_re = re.compile(r"sources\s*:\s*\[\s*\{\s*type\s*:\s*\"(.*?)\",\s*src\s*:\s*\"(.*?)\"", re.DOTALL) 21 | 22 | title = "Galatasaray TV" 23 | 24 | def _get_streams(self): 25 | res = self.session.http.get(self.url) 26 | match = self.playervars_re.search(res.text) 27 | if match: 28 | stream_url = match.group(2) 29 | log.debug("URL={0}".format(stream_url)) 30 | return HLSStream.parse_variant_playlist(self.session, stream_url) 31 | 32 | 33 | __plugin__ = GalatasarayTV 34 | -------------------------------------------------------------------------------- /src/streamlink/plugins/goltelevision.py: -------------------------------------------------------------------------------- 1 | """ 2 | $description Spanish live TV sports channel owned by Gol Network. 3 | $url goltelevision.com 4 | $type live 5 | $region Spain 6 | """ 7 | 8 | import re 9 | 10 | from streamlink.plugin import Plugin, pluginmatcher 11 | from streamlink.plugin.api import validate 12 | from streamlink.stream.hls import HLSStream 13 | 14 | 15 | @pluginmatcher(re.compile( 16 | r"https?://(?:www\.)?goltelevision\.com/en-directo" 17 | )) 18 | class GOLTelevision(Plugin): 19 | def _get_streams(self): 20 | url = self.session.http.get( 21 | "https://play.goltelevision.com/api/stream/live", 22 | schema=validate.Schema( 23 | validate.parse_json(), 24 | {"manifest": validate.url()}, 25 | validate.get("manifest") 26 | ) 27 | ) 28 | return HLSStream.parse_variant_playlist(self.session, url) 29 | 30 | 31 | __plugin__ = GOLTelevision 32 | -------------------------------------------------------------------------------- /src/streamlink/plugins/googledrive.py: -------------------------------------------------------------------------------- 1 | """ 2 | $description Stream videos stored in Google Drive. 3 | $url docs.google.com 4 | $url drive.google.com 5 | $type vod 6 | """ 7 | 8 | import logging 9 | import re 10 | from urllib.parse import parse_qsl 11 | 12 | from streamlink.plugin import Plugin, pluginmatcher 13 | from streamlink.stream.http import HTTPStream 14 | 15 | log = logging.getLogger(__name__) 16 | 17 | 18 | @pluginmatcher(re.compile( 19 | r"https?://(?:drive|docs)\.google\.com/file/d/([^/]+)/?" 20 | )) 21 | class GoogleDocs(Plugin): 22 | api_url = "https://docs.google.com/get_video_info" 23 | 24 | def _get_streams(self): 25 | docid = self.match.group(1) 26 | log.debug("Google Docs ID: {0}".format(docid)) 27 | res = self.session.http.get(self.api_url, params=dict(docid=docid)) 28 | data = dict(parse_qsl(res.text)) 29 | 30 | if data["status"] == "ok": 31 | fmts = dict([s.split('/')[:2] for s in data["fmt_list"].split(",")]) 32 | streams = [s.split('|') for s in data["fmt_stream_map"].split(",")] 33 | for qcode, url in streams: 34 | _, h = fmts[qcode].split("x") 35 | yield "{0}p".format(h), HTTPStream(self.session, url) 36 | else: 37 | log.error("{0} (ID: {1})".format(data["reason"], docid)) 38 | 39 | 40 | __plugin__ = GoogleDocs 41 | -------------------------------------------------------------------------------- /src/streamlink/plugins/hls.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import re 3 | 4 | from streamlink.plugin import Plugin, pluginmatcher 5 | from streamlink.plugin.plugin import LOW_PRIORITY, parse_params 6 | from streamlink.stream.hls import HLSStream 7 | from streamlink.utils.url import update_scheme 8 | 9 | log = logging.getLogger(__name__) 10 | 11 | 12 | @pluginmatcher(re.compile( 13 | r"hls(?:variant)?://(?P\S+)(?:\s(?P.+))?" 14 | )) 15 | @pluginmatcher(priority=LOW_PRIORITY, pattern=re.compile( 16 | r"(?P\S+\.m3u8(?:\?\S*)?)(?:\s(?P.+))?" 17 | )) 18 | class HLSPlugin(Plugin): 19 | def _get_streams(self): 20 | data = self.match.groupdict() 21 | url = update_scheme("https://", data.get("url"), force=False) 22 | params = parse_params(data.get("params")) 23 | log.debug(f"URL={url}; params={params}") 24 | 25 | streams = HLSStream.parse_variant_playlist(self.session, url, **params) 26 | 27 | return streams or {"live": HLSStream(self.session, url, **params)} 28 | 29 | 30 | __plugin__ = HLSPlugin 31 | -------------------------------------------------------------------------------- /src/streamlink/plugins/http.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import re 3 | 4 | from streamlink.plugin import Plugin, pluginmatcher 5 | from streamlink.plugin.plugin import parse_params 6 | from streamlink.stream.http import HTTPStream 7 | from streamlink.utils.url import update_scheme 8 | 9 | log = logging.getLogger(__name__) 10 | 11 | 12 | @pluginmatcher(re.compile( 13 | r"httpstream://(?P\S+)(?:\s(?P.+))?" 14 | )) 15 | class HTTPStreamPlugin(Plugin): 16 | def _get_streams(self): 17 | data = self.match.groupdict() 18 | url = update_scheme("https://", data.get("url"), force=False) 19 | params = parse_params(data.get("params")) 20 | log.debug(f"URL={url}; params={params}") 21 | 22 | return {"live": HTTPStream(self.session, url, **params)} 23 | 24 | 25 | __plugin__ = HTTPStreamPlugin 26 | -------------------------------------------------------------------------------- /src/streamlink/plugins/lrt.py: -------------------------------------------------------------------------------- 1 | """ 2 | $description Live TV channels from LRT, a Lithuanian public, state-owned broadcaster. 3 | $url lrt.lt 4 | $type live 5 | """ 6 | 7 | import logging 8 | import re 9 | 10 | from streamlink.plugin import Plugin, pluginmatcher 11 | from streamlink.stream.hls import HLSStream 12 | 13 | log = logging.getLogger(__name__) 14 | 15 | 16 | @pluginmatcher(re.compile( 17 | r"https?://(?:www\.)?lrt\.lt/mediateka/tiesiogiai/" 18 | )) 19 | class LRT(Plugin): 20 | _video_id_re = re.compile(r"""var\svideo_id\s*=\s*["'](?P\w+)["']""") 21 | API_URL = "https://www.lrt.lt/servisai/stream_url/live/get_live_url.php?channel={0}" 22 | 23 | def _get_streams(self): 24 | page = self.session.http.get(self.url) 25 | m = self._video_id_re.search(page.text) 26 | if m: 27 | video_id = m.group("video_id") 28 | data = self.session.http.get(self.API_URL.format(video_id)).json() 29 | hls_url = data["response"]["data"]["content"] 30 | 31 | yield from HLSStream.parse_variant_playlist(self.session, hls_url).items() 32 | else: 33 | log.debug("No match for video_id regex") 34 | 35 | 36 | __plugin__ = LRT 37 | -------------------------------------------------------------------------------- /src/streamlink/plugins/nhkworld.py: -------------------------------------------------------------------------------- 1 | """ 2 | $description Current affairs and cultural channel owned by NHK, a Japanese public, state-owned broadcaster. 3 | $url nhk.or.jp/nhkworld 4 | $type live 5 | """ 6 | 7 | import re 8 | 9 | from streamlink.plugin import Plugin, pluginmatcher 10 | from streamlink.stream.hls import HLSStream 11 | 12 | API_URL = "http://{}.nhk.or.jp/nhkworld/app/tv/hlslive_web.json" 13 | 14 | 15 | @pluginmatcher(re.compile( 16 | r"https?://(?:(\w+)\.)?nhk\.or\.jp/nhkworld" 17 | )) 18 | class NHKWorld(Plugin): 19 | def _get_streams(self): 20 | # get the HLS json from the same sub domain as the main url, defaulting to www 21 | sdomain = self.match.group(1) or "www" 22 | res = self.session.http.get(API_URL.format(sdomain)) 23 | 24 | stream_url = self.session.http.json(res)['main']['wstrm'] 25 | return HLSStream.parse_variant_playlist(self.session, stream_url) 26 | 27 | 28 | __plugin__ = NHKWorld 29 | -------------------------------------------------------------------------------- /src/streamlink/plugins/ntv.py: -------------------------------------------------------------------------------- 1 | """ 2 | $description Russian live TV channel owned by Gazprom Media. 3 | $url ntv.ru 4 | $type live 5 | """ 6 | 7 | import re 8 | 9 | from streamlink.plugin import Plugin, pluginmatcher 10 | from streamlink.stream.hls import HLSStream 11 | 12 | 13 | @pluginmatcher(re.compile( 14 | r'https?://www\.ntv\.ru/air/' 15 | )) 16 | class NTV(Plugin): 17 | def _get_streams(self): 18 | body = self.session.http.get(self.url).text 19 | mrl = None 20 | match = re.search(r'var camHlsURL = \'(.*)\'', body) 21 | if match: 22 | mrl = f"http:{match.group(1)}" 23 | else: 24 | match = re.search(r'var hlsURL = \'(.*)\'', body) 25 | if match: 26 | mrl = match.group(1) 27 | if mrl: 28 | return HLSStream.parse_variant_playlist(self.session, mrl) 29 | 30 | 31 | __plugin__ = NTV 32 | -------------------------------------------------------------------------------- /src/streamlink/plugins/piczel.py: -------------------------------------------------------------------------------- 1 | """ 2 | $description Global live streaming platform for the creative community. 3 | $url piczel.tv 4 | $type live 5 | """ 6 | 7 | import logging 8 | import re 9 | 10 | from streamlink.plugin import Plugin, pluginmatcher 11 | from streamlink.plugin.api import validate 12 | from streamlink.stream.hls import HLSStream 13 | 14 | log = logging.getLogger(__name__) 15 | 16 | 17 | STREAMS_URL = "https://piczel.tv/api/streams?followedStreams=false&live_only=false&sfw=false" 18 | HLS_URL = "https://piczel.tv/hls/{0}/index.m3u8" 19 | 20 | _streams_schema = validate.Schema([ 21 | { 22 | "id": int, 23 | "live": bool, 24 | "slug": validate.text 25 | } 26 | ]) 27 | 28 | 29 | @pluginmatcher(re.compile( 30 | r"https?://piczel\.tv/watch/(\w+)" 31 | )) 32 | class Piczel(Plugin): 33 | def _get_streams(self): 34 | channel_name = self.match.group(1) 35 | 36 | res = self.session.http.get(STREAMS_URL) 37 | streams = self.session.http.json(res, schema=_streams_schema) 38 | 39 | for stream in streams: 40 | if stream["slug"] != channel_name: 41 | continue 42 | 43 | if not stream["live"]: 44 | return 45 | 46 | log.debug(f"HLS stream URL: {HLS_URL.format(stream['id'])}") 47 | 48 | return {"live": HLSStream(self.session, HLS_URL.format(stream["id"]))} 49 | 50 | 51 | __plugin__ = Piczel 52 | -------------------------------------------------------------------------------- /src/streamlink/plugins/rtpa.py: -------------------------------------------------------------------------------- 1 | """ 2 | $description Live TV channels and video on-demand service from RTPA, a Spanish public broadcaster. 3 | $url rtpa.es 4 | $type live, vod 5 | """ 6 | 7 | import re 8 | 9 | from streamlink.plugin import Plugin, pluginmatcher 10 | from streamlink.plugin.api import validate 11 | from streamlink.stream.hls import HLSStream 12 | 13 | 14 | @pluginmatcher(re.compile(r"https?://(?:www\.)?rtpa\.es")) 15 | class RTPA(Plugin): 16 | def _get_streams(self): 17 | hls_url, self.title = self.session.http.get(self.url, schema=validate.Schema( 18 | validate.parse_html(), 19 | validate.union(( 20 | validate.xml_xpath_string(".//video/source[@src][@type='application/x-mpegURL'][1]/@src"), 21 | validate.xml_xpath_string(".//head/title[1]/text()"), 22 | )) 23 | )) 24 | if not hls_url: 25 | return 26 | return HLSStream.parse_variant_playlist(self.session, hls_url, headers={"Referer": self.url}) 27 | 28 | 29 | __plugin__ = RTPA 30 | -------------------------------------------------------------------------------- /src/streamlink/plugins/sportal.py: -------------------------------------------------------------------------------- 1 | """ 2 | $description Sporting channel live stream owned by Sportal, a Bulgarian sports media website. 3 | $url sportal.bg 4 | $type live 5 | """ 6 | 7 | import logging 8 | import re 9 | 10 | from streamlink.plugin import Plugin, pluginmatcher 11 | from streamlink.stream.hls import HLSStream 12 | 13 | log = logging.getLogger(__name__) 14 | 15 | 16 | @pluginmatcher(re.compile( 17 | r'https?://(?:www\.)?sportal\.bg/sportal_live_tv\.php' 18 | )) 19 | class Sportal(Plugin): 20 | _hls_re = re.compile(r'''["'](?P[^"']+\.m3u8[^"']*?)["']''') 21 | 22 | def _get_streams(self): 23 | res = self.session.http.get(self.url) 24 | m = self._hls_re.search(res.text) 25 | if not m: 26 | return 27 | 28 | hls_url = m.group('url') 29 | log.debug('URL={0}'.format(hls_url)) 30 | log.warning('SSL certificate verification is disabled.') 31 | return HLSStream.parse_variant_playlist( 32 | self.session, hls_url, verify=False).items() 33 | 34 | 35 | __plugin__ = Sportal 36 | -------------------------------------------------------------------------------- /src/streamlink/plugins/ssh101.py: -------------------------------------------------------------------------------- 1 | """ 2 | $description Global live streaming platform. 3 | $url ssh101.com 4 | $type live 5 | """ 6 | 7 | import logging 8 | import re 9 | 10 | from streamlink.plugin import Plugin, pluginmatcher 11 | from streamlink.plugin.api import validate 12 | from streamlink.stream.hls import HLSStream 13 | 14 | log = logging.getLogger(__name__) 15 | 16 | 17 | @pluginmatcher(re.compile( 18 | r"https?://(?:www\.)?ssh101\.com/(?:(?:secure)?live/|detail\.php\?id=\w+)" 19 | )) 20 | class SSH101(Plugin): 21 | def _get_streams(self): 22 | hls_url = self.session.http.get(self.url, schema=validate.Schema( 23 | validate.parse_html(), 24 | validate.xml_xpath_string(".//source[contains(@src,'.m3u8')]/@src"), 25 | )) 26 | if not hls_url: 27 | return 28 | 29 | res = self.session.http.get(hls_url, acceptable_status=(200, 403, 404)) 30 | if res.status_code != 200 or len(res.text) <= 10: 31 | log.error("This stream is currently offline") 32 | return 33 | 34 | return {"live": HLSStream(self.session, hls_url)} 35 | 36 | 37 | __plugin__ = SSH101 38 | -------------------------------------------------------------------------------- /src/streamlink/plugins/streamable.py: -------------------------------------------------------------------------------- 1 | """ 2 | $description Global video hosting platform. 3 | $url streamable.com 4 | $type vod 5 | """ 6 | 7 | import re 8 | 9 | from streamlink.plugin import Plugin, pluginmatcher 10 | from streamlink.plugin.api import validate 11 | from streamlink.stream.http import HTTPStream 12 | from streamlink.utils.url import update_scheme 13 | 14 | 15 | @pluginmatcher(re.compile( 16 | r"https?://(?:www\.)?streamable\.com/(.+)" 17 | )) 18 | class Streamable(Plugin): 19 | def _get_streams(self): 20 | data = self.session.http.get(self.url, schema=validate.Schema( 21 | re.compile(r"var\s*videoObject\s*=\s*({.*?});"), 22 | validate.none_or_all( 23 | validate.get(1), 24 | validate.parse_json(), 25 | { 26 | "files": { 27 | str: { 28 | "url": validate.url(), 29 | "width": int, 30 | "height": int, 31 | "bitrate": int, 32 | }, 33 | }, 34 | }, 35 | ), 36 | )) 37 | 38 | for info in data["files"].values(): 39 | stream_url = update_scheme("https://", info["url"]) 40 | # pick the smaller of the two dimensions, for landscape v. portrait videos 41 | res = min(info["width"], info["height"]) 42 | yield f"{res}p", HTTPStream(self.session, stream_url) 43 | 44 | 45 | __plugin__ = Streamable 46 | -------------------------------------------------------------------------------- /src/streamlink/plugins/stv.py: -------------------------------------------------------------------------------- 1 | """ 2 | $description Live TV channels from STV, a Scottish free-to-air broadcaster. 3 | $url player.stv.tv 4 | $type live 5 | $region United Kingdom 6 | """ 7 | 8 | import logging 9 | import re 10 | 11 | from streamlink.plugin import Plugin, PluginError, pluginmatcher 12 | from streamlink.stream.hls import HLSStream 13 | 14 | log = logging.getLogger(__name__) 15 | 16 | 17 | @pluginmatcher(re.compile( 18 | r'https?://player\.stv\.tv/live' 19 | )) 20 | class STV(Plugin): 21 | API_URL = 'https://player.api.stv.tv/v1/streams/stv/' 22 | 23 | def get_title(self): 24 | if self.title is None: 25 | self._get_api_results() 26 | return self.title 27 | 28 | def _get_api_results(self): 29 | res = self.session.http.get(self.API_URL) 30 | data = self.session.http.json(res) 31 | 32 | if data['success'] is False: 33 | raise PluginError(data['reason']['message']) 34 | 35 | try: 36 | self.title = data['results']['now']['title'] 37 | except KeyError: 38 | self.title = 'STV' 39 | 40 | return data 41 | 42 | def _get_streams(self): 43 | hls_url = self._get_api_results()['results']['streamUrl'] 44 | return HLSStream.parse_variant_playlist(self.session, hls_url) 45 | 46 | 47 | __plugin__ = STV 48 | -------------------------------------------------------------------------------- /src/streamlink/plugins/tv360.py: -------------------------------------------------------------------------------- 1 | """ 2 | $description A privately owned Turkish live TV channel. 3 | $url tv360.com.tr 4 | $type live 5 | """ 6 | 7 | import re 8 | 9 | from streamlink.plugin import Plugin, pluginmatcher 10 | from streamlink.plugin.api import validate 11 | from streamlink.stream.hls import HLSStream 12 | 13 | 14 | @pluginmatcher(re.compile( 15 | r"https?://(?:www\.)?tv360\.com\.tr/canli-yayin" 16 | )) 17 | class TV360(Plugin): 18 | def _get_streams(self): 19 | hls_url = self.session.http.get(self.url, schema=validate.Schema( 20 | validate.parse_html(), 21 | validate.xml_xpath_string(".//video/source[@src][@type='application/x-mpegURL'][1]/@src"), 22 | validate.none_or_all(validate.url()), 23 | )) 24 | if hls_url: 25 | return HLSStream.parse_variant_playlist(self.session, hls_url) 26 | 27 | 28 | __plugin__ = TV360 29 | -------------------------------------------------------------------------------- /src/streamlink/plugins/tv8.py: -------------------------------------------------------------------------------- 1 | """ 2 | $description Turkish live TV channel owned by Acun Medya Group. 3 | $url tv8.com.tr 4 | $type live 5 | """ 6 | 7 | import logging 8 | import re 9 | 10 | from streamlink.plugin import Plugin, pluginmatcher 11 | from streamlink.plugin.api import validate 12 | from streamlink.stream.hls import HLSStream 13 | 14 | log = logging.getLogger(__name__) 15 | 16 | 17 | @pluginmatcher(re.compile( 18 | r"https?://www\.tv8\.com\.tr/canli-yayin" 19 | )) 20 | class TV8(Plugin): 21 | title = "TV8" 22 | 23 | def _get_streams(self): 24 | hls_url = self.session.http.get(self.url, schema=validate.Schema( 25 | re.compile(r"""file\s*:\s*(?P["'])(?Phttps?://.*?\.m3u8.*?)(?P=q)"""), 26 | validate.any(None, validate.get("hls_url")), 27 | )) 28 | if hls_url is not None: 29 | return HLSStream.parse_variant_playlist(self.session, hls_url) 30 | 31 | 32 | __plugin__ = TV8 33 | -------------------------------------------------------------------------------- /src/streamlink/plugins/tv999.py: -------------------------------------------------------------------------------- 1 | """ 2 | $description Turkish live TV channel owned by Detelina Media. 3 | $url tv999.bg 4 | $type live 5 | $region Bulgaria 6 | """ 7 | 8 | import logging 9 | import re 10 | 11 | from streamlink.plugin import Plugin, pluginmatcher 12 | from streamlink.plugin.api import validate 13 | from streamlink.stream.hls import HLSStream 14 | from streamlink.utils.url import update_scheme 15 | 16 | log = logging.getLogger(__name__) 17 | 18 | 19 | @pluginmatcher(re.compile( 20 | r"https?://(?:www\.)?tv999\.bg/live" 21 | )) 22 | class TV999(Plugin): 23 | title = "TV999" 24 | 25 | def _get_xpath_string(self, url, xpath): 26 | return self.session.http.get( 27 | url, 28 | schema=validate.Schema( 29 | validate.parse_html(), 30 | validate.xml_xpath_string(xpath), 31 | validate.any(None, validate.url()) 32 | ) 33 | ) 34 | 35 | def _get_streams(self): 36 | iframe_url = self._get_xpath_string(self.url, ".//iframe[@src]/@src") 37 | if not iframe_url: 38 | return 39 | hls_url = self._get_xpath_string(iframe_url, ".//source[contains(@src,'m3u8')]/@src") 40 | if not hls_url: 41 | return 42 | return {"live": HLSStream(self.session, update_scheme("http://", hls_url))} 43 | 44 | 45 | __plugin__ = TV999 46 | -------------------------------------------------------------------------------- /src/streamlink/plugins/tvibo.py: -------------------------------------------------------------------------------- 1 | """ 2 | $description Global live streaming and video hosting platform. 3 | $url player.tvibo.com 4 | $type live 5 | """ 6 | 7 | import logging 8 | import re 9 | 10 | from streamlink.plugin import Plugin, pluginmatcher 11 | from streamlink.stream.hls import HLSStream 12 | 13 | log = logging.getLogger(__name__) 14 | 15 | 16 | @pluginmatcher(re.compile( 17 | r"https?://player\.tvibo\.com/\w+/(?P\d+)" 18 | )) 19 | class Tvibo(Plugin): 20 | _api_url = "http://panel.tvibo.com/api/player/streamurl/{id}" 21 | 22 | def _get_streams(self): 23 | channel_id = self.match.group("id") 24 | 25 | api_response = self.session.http.get( 26 | self._api_url.format(id=channel_id), 27 | acceptable_status=(200, 404)) 28 | 29 | data = self.session.http.json(api_response) 30 | log.trace("{0!r}".format(data)) 31 | if data.get("st"): 32 | yield "source", HLSStream(self.session, data["st"]) 33 | elif data.get("error"): 34 | log.error(data["error"]["message"]) 35 | 36 | 37 | __plugin__ = Tvibo 38 | -------------------------------------------------------------------------------- /src/streamlink/plugins/tvrplus.py: -------------------------------------------------------------------------------- 1 | """ 2 | $description Live TV channels from TVR, a Romanian public, state-owned broadcaster. 3 | $url tvrplus.ro 4 | $type live 5 | $region Romania 6 | """ 7 | 8 | import logging 9 | import re 10 | 11 | from streamlink.plugin import Plugin, pluginmatcher 12 | from streamlink.plugin.api import validate 13 | from streamlink.stream.hls import HLSStream 14 | 15 | log = logging.getLogger(__name__) 16 | 17 | 18 | @pluginmatcher(re.compile( 19 | r"https?://(?:www\.)?tvrplus\.ro/live/" 20 | )) 21 | class TVRPlus(Plugin): 22 | hls_file_re = re.compile(r"""["'](?P[^"']+\.m3u8(?:[^"']+)?)["']""") 23 | 24 | stream_schema = validate.Schema( 25 | validate.all( 26 | validate.transform(hls_file_re.findall), 27 | validate.any(None, [validate.text]) 28 | ), 29 | ) 30 | 31 | def _get_streams(self): 32 | headers = {"Referer": self.url} 33 | stream_url = self.stream_schema.validate(self.session.http.get(self.url).text) 34 | if stream_url: 35 | stream_url = list(set(stream_url)) 36 | for url in stream_url: 37 | log.debug("URL={0}".format(url)) 38 | yield from HLSStream.parse_variant_playlist(self.session, url, headers=headers).items() 39 | 40 | 41 | __plugin__ = TVRPlus 42 | -------------------------------------------------------------------------------- /src/streamlink/plugins/tvtoya.py: -------------------------------------------------------------------------------- 1 | """ 2 | $description Polish live TV channel owned by Toya. 3 | $url tvtoya.pl 4 | $type live 5 | """ 6 | 7 | import re 8 | 9 | from streamlink.plugin import Plugin, PluginError, pluginmatcher 10 | from streamlink.plugin.api import validate 11 | from streamlink.stream.hls import HLSStream 12 | 13 | 14 | @pluginmatcher(re.compile( 15 | r"https?://(?:www\.)?tvtoya\.pl/player/live" 16 | )) 17 | class TVToya(Plugin): 18 | def _get_streams(self): 19 | try: 20 | hls = self.session.http.get(self.url, schema=validate.Schema( 21 | validate.parse_html(), 22 | validate.xml_xpath_string(".//script[@type='application/json'][@id='__NEXT_DATA__']/text()"), 23 | str, 24 | validate.parse_json(), 25 | { 26 | "props": { 27 | "pageProps": { 28 | "type": "live", 29 | "url": validate.all( 30 | str, 31 | validate.transform(lambda url: url.replace("https:////", "https://")), 32 | validate.url(path=validate.endswith(".m3u8")), 33 | ) 34 | } 35 | } 36 | }, 37 | validate.get(("props", "pageProps", "url")), 38 | )) 39 | except PluginError: 40 | return 41 | 42 | return HLSStream.parse_variant_playlist(self.session, hls) 43 | 44 | 45 | __plugin__ = TVToya 46 | -------------------------------------------------------------------------------- /src/streamlink/plugins/vinhlongtv.py: -------------------------------------------------------------------------------- 1 | """ 2 | $description Vietnamese live TV channels from THVL, including THVL1, THVL2, THVL3 and THVL4. 3 | $url thvli.vn 4 | $type live 5 | $region Vietnam 6 | """ 7 | 8 | import logging 9 | import re 10 | 11 | from streamlink.plugin import Plugin, pluginmatcher 12 | from streamlink.plugin.api import validate 13 | from streamlink.stream.hls import HLSStream 14 | 15 | log = logging.getLogger(__name__) 16 | 17 | 18 | @pluginmatcher(re.compile( 19 | r'https?://(?:www\.)?thvli\.vn/live/(?P[^/]+)' 20 | )) 21 | class VinhLongTV(Plugin): 22 | api_url = 'http://api.thvli.vn/backend/cm/detail/{0}/' 23 | 24 | _data_schema = validate.Schema( 25 | { 26 | 'link_play': validate.text, 27 | }, 28 | validate.get('link_play') 29 | ) 30 | 31 | def _get_streams(self): 32 | channel = self.match.group('channel') 33 | 34 | res = self.session.http.get(self.api_url.format(channel)) 35 | hls_url = self.session.http.json(res, schema=self._data_schema) 36 | log.debug('URL={0}'.format(hls_url)) 37 | 38 | streams = HLSStream.parse_variant_playlist(self.session, hls_url) 39 | if not streams: 40 | return {'live': HLSStream(self.session, hls_url)} 41 | else: 42 | return streams 43 | 44 | 45 | __plugin__ = VinhLongTV 46 | -------------------------------------------------------------------------------- /src/streamlink/plugins/welt.py: -------------------------------------------------------------------------------- 1 | """ 2 | $description German news and documentaries TV channel, owned by Axel Springer SE. 3 | $url welt.de 4 | $type live, vod 5 | $region Germany 6 | """ 7 | 8 | import re 9 | from urllib.parse import quote 10 | 11 | from streamlink.plugin import Plugin, pluginmatcher 12 | from streamlink.plugin.api import validate 13 | from streamlink.stream.hls import HLSStream 14 | 15 | 16 | @pluginmatcher(re.compile( 17 | r"https?://(\w+\.)?welt\.de/?" 18 | )) 19 | class Welt(Plugin): 20 | _url_vod = "https://www.welt.de/onward/video/play/{0}" 21 | _schema = validate.Schema( 22 | validate.parse_html(), 23 | validate.xml_findtext(".//script[@type='application/json'][@data-content='VideoPlayer.Config']"), 24 | validate.parse_json(), 25 | validate.get("sources"), 26 | validate.filter(lambda obj: obj["extension"] == "m3u8"), 27 | validate.get((0, "src")) 28 | ) 29 | 30 | def _get_streams(self): 31 | hls_url = self.session.http.get(self.url, schema=self._schema) 32 | 33 | if "mediathek" in self.url.lower(): 34 | url = self._url_vod.format(quote(hls_url, safe="")) 35 | hls_url = self.session.http.get(url, headers={"Referer": self.url}).url 36 | 37 | return HLSStream.parse_variant_playlist(self.session, hls_url, headers={"Referer": self.url}) 38 | 39 | 40 | __plugin__ = Welt 41 | -------------------------------------------------------------------------------- /src/streamlink/plugins/zeenews.py: -------------------------------------------------------------------------------- 1 | """ 2 | $description Indian Hindi-language news channel covering world & Indian news, business, entertainment and sport. 3 | $url zeenews.india.com 4 | $type live 5 | """ 6 | 7 | import logging 8 | import re 9 | 10 | from streamlink.plugin import Plugin, pluginmatcher 11 | from streamlink.stream.hls import HLSStream 12 | 13 | log = logging.getLogger(__name__) 14 | 15 | 16 | @pluginmatcher(re.compile( 17 | r'https?://zeenews\.india\.com/live-tv' 18 | )) 19 | class ZeeNews(Plugin): 20 | HLS_URL = 'https://z5ams.akamaized.net/zeenews/index.m3u8{0}' 21 | TOKEN_URL = 'https://useraction.zee5.com/token/live.php' 22 | 23 | title = 'Zee News' 24 | 25 | def _get_streams(self): 26 | res = self.session.http.get(self.TOKEN_URL) 27 | token = self.session.http.json(res)['video_token'] 28 | log.debug('video_token: {0}'.format(token)) 29 | yield from HLSStream.parse_variant_playlist(self.session, self.HLS_URL.format(token)).items() 30 | 31 | 32 | __plugin__ = ZeeNews 33 | -------------------------------------------------------------------------------- /src/streamlink/plugins/zengatv.py: -------------------------------------------------------------------------------- 1 | """ 2 | $description Indian live TV channels. OTT service from Zenga TV. 3 | $url zengatv.com 4 | $type live 5 | """ 6 | 7 | import logging 8 | import re 9 | 10 | from streamlink.plugin import Plugin, pluginmatcher 11 | from streamlink.stream.hls import HLSStream 12 | 13 | log = logging.getLogger(__name__) 14 | 15 | 16 | @pluginmatcher(re.compile( 17 | r"https?://(www\.)?zengatv\.com/\w+" 18 | )) 19 | class ZengaTV(Plugin): 20 | """Streamlink Plugin for livestreams on zengatv.com""" 21 | 22 | _id_re = re.compile(r"""id=(?P["'])dvrid(?P=q)\svalue=(?P=q)(?P[^"']+)(?P=q)""") 23 | _id_2_re = re.compile(r"""LivePlayer\(.+["'](?PD\d+)["']""") 24 | 25 | api_url = "http://www.zengatv.com/changeResulation/" 26 | 27 | def _get_streams(self): 28 | headers = {"Referer": self.url} 29 | 30 | res = self.session.http.get(self.url, headers=headers) 31 | for id_re in (self._id_re, self._id_2_re): 32 | m = id_re.search(res.text) 33 | if not m: 34 | continue 35 | break 36 | 37 | if not m: 38 | log.error("No video id found") 39 | return 40 | 41 | dvr_id = m.group("id") 42 | log.debug("Found video id: {0}".format(dvr_id)) 43 | data = {"feed": "hd", "dvrId": dvr_id} 44 | res = self.session.http.post(self.api_url, headers=headers, data=data) 45 | if res.status_code == 200: 46 | yield from HLSStream.parse_variant_playlist(self.session, res.text, headers=headers).items() 47 | 48 | 49 | __plugin__ = ZengaTV 50 | -------------------------------------------------------------------------------- /src/streamlink/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunghome/streamlink_optionalkey/63c387cb8d088a6a41cfd128348aa237329a0f5c/src/streamlink/py.typed -------------------------------------------------------------------------------- /src/streamlink/stream/__init__.py: -------------------------------------------------------------------------------- 1 | from streamlink.stream.dash import DASHStream 2 | from streamlink.stream.ffmpegmux import MuxedStream 3 | from streamlink.stream.hls import HLSStream, MuxedHLSStream 4 | from streamlink.stream.http import HTTPStream 5 | from streamlink.stream.stream import Stream, StreamIO 6 | from streamlink.stream.wrappers import StreamIOIterWrapper, StreamIOThreadWrapper, StreamIOWrapper 7 | -------------------------------------------------------------------------------- /src/streamlink/stream/file.py: -------------------------------------------------------------------------------- 1 | """ 2 | Stream wrapper around a file 3 | """ 4 | from streamlink.stream.stream import Stream 5 | 6 | 7 | class FileStream(Stream): 8 | __shortname__ = "file" 9 | 10 | def __init__(self, session, path=None, fileobj=None): 11 | super().__init__(session) 12 | self.path = path 13 | self.fileobj = fileobj 14 | if not self.path and not self.fileobj: 15 | raise ValueError("path or fileobj must be set") 16 | 17 | def __json__(self): 18 | json = super().__json__() 19 | 20 | if self.path: 21 | json["path"] = self.path 22 | 23 | return json 24 | 25 | def to_url(self): 26 | if self.path is None: 27 | return super().to_url() 28 | 29 | return self.path 30 | 31 | def open(self): 32 | return self.fileobj or open(self.path) 33 | -------------------------------------------------------------------------------- /src/streamlink/user_input.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | 4 | class UserInputRequester(abc.ABC): 5 | """ 6 | Base Class / Interface for requesting user input 7 | 8 | e.g. from the console 9 | """ 10 | @abc.abstractmethod 11 | def ask(self, prompt: str) -> str: 12 | """ 13 | Ask the user for a text input, the input is not sensitive 14 | and can be echoed to the user 15 | 16 | :param prompt: message to display when asking for the input 17 | :return: the value of the user input 18 | """ 19 | raise NotImplementedError 20 | 21 | @abc.abstractmethod 22 | def ask_password(self, prompt: str) -> str: 23 | """ 24 | Ask the user for a text input, the input _is_ sensitive 25 | and should be masked as the user gives the input 26 | 27 | :param prompt: message to display when asking for the input 28 | :return: the value of the user input 29 | """ 30 | raise NotImplementedError 31 | -------------------------------------------------------------------------------- /src/streamlink/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from streamlink.utils.cache import LRUCache 2 | from streamlink.utils.data import search_dict 3 | from streamlink.utils.module import load_module 4 | from streamlink.utils.named_pipe import NamedPipe 5 | from streamlink.utils.parse import parse_html, parse_json, parse_qsd, parse_xml 6 | from streamlink.utils.url import absolute_url, prepend_www, update_qsd, update_scheme, url_concat, url_equal 7 | 8 | 9 | __all__ = [ 10 | "LRUCache", 11 | "search_dict", 12 | "load_module", 13 | "NamedPipe", 14 | "parse_html", "parse_json", "parse_qsd", "parse_xml", 15 | "absolute_url", "prepend_www", "update_qsd", "update_scheme", "url_concat", "url_equal", 16 | ] 17 | -------------------------------------------------------------------------------- /src/streamlink/utils/cache.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | from typing import Generic, Optional, OrderedDict as TOrderedDict, TypeVar 3 | 4 | 5 | TCacheKey = TypeVar("TCacheKey") 6 | TCacheValue = TypeVar("TCacheValue") 7 | 8 | 9 | class LRUCache(Generic[TCacheKey, TCacheValue]): 10 | def __init__(self, num: int): 11 | self.cache: TOrderedDict[TCacheKey, TCacheValue] = OrderedDict() 12 | self.num = num 13 | 14 | def get(self, key: TCacheKey) -> Optional[TCacheValue]: 15 | if key not in self.cache: 16 | return None 17 | self.cache.move_to_end(key) 18 | return self.cache[key] 19 | 20 | def set(self, key: TCacheKey, value: TCacheValue) -> None: 21 | self.cache[key] = value 22 | self.cache.move_to_end(key) 23 | if len(self.cache) > self.num: 24 | self.cache.popitem(last=False) 25 | -------------------------------------------------------------------------------- /src/streamlink/utils/crypto.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | from Crypto.Cipher import AES 4 | 5 | 6 | def evp_bytestokey(password, salt, key_len, iv_len): 7 | """ 8 | Python implementation of OpenSSL's EVP_BytesToKey() 9 | :param password: or passphrase 10 | :param salt: 8 byte salt 11 | :param key_len: length of key in bytes 12 | :param iv_len: length of IV in bytes 13 | :return: (key, iv) 14 | """ 15 | d = d_i = b'' 16 | while len(d) < key_len + iv_len: 17 | d_i = hashlib.md5(d_i + password + salt).digest() 18 | d += d_i 19 | return d[:key_len], d[key_len:key_len + iv_len] 20 | 21 | 22 | def decrypt_openssl(data, passphrase, key_length=32): 23 | if data.startswith(b"Salted__"): 24 | salt = data[len(b"Salted__"):AES.block_size] 25 | key, iv = evp_bytestokey(passphrase, salt, key_length, AES.block_size) 26 | d = AES.new(key, AES.MODE_CBC, iv) 27 | out = d.decrypt(data[AES.block_size:]) 28 | return unpad_pkcs5(out) 29 | 30 | 31 | def unpad_pkcs5(padded): 32 | return padded[:-padded[-1]] 33 | -------------------------------------------------------------------------------- /src/streamlink/utils/data.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, List, Union 2 | 3 | 4 | def search_dict(data: Union[Dict, List], key: Any): 5 | """ 6 | Search for a key in a nested dict, or list of nested dicts, and return the values. 7 | 8 | :param data: dict/list to search 9 | :param key: key to find 10 | :return: matches for key 11 | """ 12 | if isinstance(data, dict): 13 | for dkey, value in data.items(): 14 | if dkey == key: 15 | yield value 16 | yield from search_dict(value, key) 17 | elif isinstance(data, list): 18 | for value in data: 19 | yield from search_dict(value, key) 20 | -------------------------------------------------------------------------------- /src/streamlink/utils/module.py: -------------------------------------------------------------------------------- 1 | from importlib.machinery import FileFinder, SOURCE_SUFFIXES, SourceFileLoader 2 | from importlib.util import module_from_spec 3 | 4 | 5 | _loader_details = [(SourceFileLoader, SOURCE_SUFFIXES)] 6 | 7 | 8 | def load_module(name, path=None): 9 | finder = FileFinder(path, *_loader_details) 10 | spec = finder.find_spec(name) 11 | if not spec or not spec.loader: 12 | raise ImportError(f"no module named {name}") 13 | mod = module_from_spec(spec) 14 | spec.loader.exec_module(mod) 15 | return mod 16 | -------------------------------------------------------------------------------- /src/streamlink_cli/__init__.py: -------------------------------------------------------------------------------- 1 | from signal import SIGINT, SIGTERM, signal 2 | from sys import exit 3 | 4 | 5 | def _exit(*_): 6 | # don't raise a KeyboardInterrupt until streamlink_cli has been fully initialized 7 | exit(128 | 2) 8 | 9 | 10 | # override default SIGINT handler (and set SIGTERM handler) as early as possible 11 | signal(SIGINT, _exit) 12 | signal(SIGTERM, _exit) 13 | -------------------------------------------------------------------------------- /src/streamlink_cli/__main__.py: -------------------------------------------------------------------------------- 1 | if __name__ == "__main__": 2 | from streamlink_cli.main import main 3 | main() 4 | -------------------------------------------------------------------------------- /src/streamlink_cli/compat.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from pathlib import Path 3 | from typing import BinaryIO, TYPE_CHECKING 4 | 5 | try: 6 | import importlib.metadata as importlib_metadata # type: ignore[import] # noqa: F401 7 | except ImportError: 8 | import importlib_metadata # type: ignore[import] # noqa: F401 9 | 10 | 11 | stdout: BinaryIO = sys.stdout.buffer 12 | 13 | 14 | if TYPE_CHECKING: # pragma: no cover 15 | _BasePath = Path 16 | else: 17 | _BasePath = type(Path()) 18 | 19 | 20 | class DeprecatedPath(_BasePath): 21 | pass 22 | 23 | 24 | __all__ = [ 25 | "importlib_metadata", 26 | "stdout", 27 | "DeprecatedPath", 28 | ] 29 | -------------------------------------------------------------------------------- /src/streamlink_cli/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunghome/streamlink_optionalkey/63c387cb8d088a6a41cfd128348aa237329a0f5c/src/streamlink_cli/py.typed -------------------------------------------------------------------------------- /src/streamlink_cli/utils/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | from datetime import datetime as _datetime 3 | 4 | from streamlink_cli.utils.formatter import Formatter 5 | from streamlink_cli.utils.http_server import HTTPServer 6 | from streamlink_cli.utils.player import find_default_player 7 | 8 | __all__ = [ 9 | "Formatter", "HTTPServer", "JSONEncoder", 10 | "datetime", 11 | "find_default_player", 12 | ] 13 | 14 | 15 | class JSONEncoder(json.JSONEncoder): 16 | def default(self, obj): 17 | if hasattr(obj, "__json__"): 18 | return obj.__json__() 19 | elif isinstance(obj, bytes): 20 | return obj.decode("utf8", "ignore") 21 | else: 22 | return json.JSONEncoder.default(self, obj) 23 | 24 | 25 | # noinspection PyPep8Naming 26 | class datetime(_datetime): 27 | def __str__(self): 28 | return self.strftime("%Y-%m-%d_%H-%M-%S") 29 | -------------------------------------------------------------------------------- /src/streamlink_cli/utils/formatter.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from typing import Optional 3 | 4 | from streamlink.utils.formatter import Formatter as _BaseFormatter 5 | from streamlink_cli.utils.path import replace_chars, replace_path 6 | 7 | 8 | class Formatter(_BaseFormatter): 9 | title = _BaseFormatter.format 10 | 11 | def path(self, pathlike: str, charmap: Optional[str] = None) -> Path: 12 | def mapper(s): 13 | return replace_chars(s, charmap) 14 | 15 | def format_part(part): 16 | return self._format(part, mapper, {}) 17 | 18 | return replace_path(pathlike, format_part) 19 | -------------------------------------------------------------------------------- /src/streamlink_cli/utils/path.py: -------------------------------------------------------------------------------- 1 | import re 2 | from pathlib import Path 3 | from typing import Callable, Optional, Union 4 | 5 | from streamlink.compat import is_win32 6 | 7 | 8 | REPLACEMENT = "_" 9 | SPECIAL_PATH_PARTS = (".", "..") 10 | 11 | _UNPRINTABLE = "".join(chr(c) for c in range(32)) 12 | _UNSUPPORTED_POSIX = "/" 13 | _UNSUPPORTED_WIN32 = "\x7f\"*/:<>?\\|" 14 | 15 | RE_CHARS_POSIX = re.compile(f"[{re.escape(_UNPRINTABLE + _UNSUPPORTED_POSIX)}]+") 16 | RE_CHARS_WIN32 = re.compile(f"[{re.escape(_UNPRINTABLE + _UNSUPPORTED_WIN32)}]+") 17 | if is_win32: 18 | RE_CHARS = RE_CHARS_WIN32 19 | else: 20 | RE_CHARS = RE_CHARS_POSIX 21 | 22 | 23 | def replace_chars(path: str, charmap: Optional[str] = None, replacement: str = REPLACEMENT) -> str: 24 | if charmap is None: 25 | pattern = RE_CHARS 26 | else: 27 | charmap = charmap.lower() 28 | if charmap in ("posix", "unix"): 29 | pattern = RE_CHARS_POSIX 30 | elif charmap in ("windows", "win32"): 31 | pattern = RE_CHARS_WIN32 32 | else: 33 | raise ValueError("Invalid charmap") 34 | 35 | return pattern.sub(replacement, path) 36 | 37 | 38 | def replace_path(pathlike: Union[str, Path], mapper: Callable[[str], str]) -> Path: 39 | def get_part(part): 40 | newpart = mapper(part) 41 | return REPLACEMENT if part != newpart and newpart in SPECIAL_PATH_PARTS else newpart 42 | 43 | return Path(*(get_part(part) for part in Path(pathlike).expanduser().parts)) 44 | -------------------------------------------------------------------------------- /src/streamlink_cli/utils/player.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | import sys 4 | 5 | 6 | def check_paths(exes, paths): 7 | for path in paths: 8 | for exe in exes: 9 | path = os.path.expanduser(os.path.join(path, exe)) 10 | if os.path.isfile(path): 11 | return path 12 | 13 | 14 | def find_default_player(): 15 | if "darwin" in sys.platform: 16 | paths = os.environ.get("PATH", "").split(":") 17 | paths += ["/Applications/VLC.app/Contents/MacOS/"] 18 | paths += ["~/Applications/VLC.app/Contents/MacOS/"] 19 | path = check_paths(("VLC", "vlc"), paths) 20 | elif "win32" in sys.platform: 21 | exename = "vlc.exe" 22 | paths = os.environ.get("PATH", "").split(";") 23 | path = check_paths((exename,), paths) 24 | 25 | if not path: 26 | subpath = "VideoLAN\\VLC\\" 27 | envvars = ("PROGRAMFILES", "PROGRAMFILES(X86)", "PROGRAMW6432") 28 | paths = filter(None, (os.environ.get(var) for var in envvars)) 29 | paths = (os.path.join(p, subpath) for p in paths) 30 | path = check_paths((exename,), paths) 31 | else: 32 | paths = os.environ.get("PATH", "").split(":") 33 | path = check_paths(("vlc",), paths) 34 | 35 | if path: 36 | # Quote command because it can contain space 37 | return subprocess.list2cmdline([path]) 38 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import signal 3 | 4 | import pytest 5 | 6 | # import streamlink_cli as early as possible to execute its default signal overrides 7 | # noinspection PyUnresolvedReferences 8 | import streamlink_cli # noqa: F401 9 | 10 | 11 | # immediately restore default signal handlers for the test runner 12 | signal.signal(signal.SIGINT, signal.default_int_handler) 13 | signal.signal(signal.SIGTERM, signal.default_int_handler) 14 | 15 | 16 | # make pytest rewrite assertions in dynamically parametrized plugin tests 17 | # https://docs.pytest.org/en/stable/how-to/writing_plugins.html#assertion-rewriting 18 | pytest.register_assert_rewrite("tests.plugins") 19 | 20 | 21 | windows_only = pytest.mark.skipif(os.name != "nt", reason="test only applicable on Windows") 22 | posix_only = pytest.mark.skipif(os.name != "posix", reason="test only applicable on a POSIX OS") 23 | 24 | 25 | __all__ = ["windows_only", "posix_only"] 26 | -------------------------------------------------------------------------------- /tests/cli/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunghome/streamlink_optionalkey/63c387cb8d088a6a41cfd128348aa237329a0f5c/tests/cli/__init__.py -------------------------------------------------------------------------------- /tests/cli/output/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunghome/streamlink_optionalkey/63c387cb8d088a6a41cfd128348aa237329a0f5c/tests/cli/output/__init__.py -------------------------------------------------------------------------------- /tests/cli/test_main_formatter.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock, patch 2 | 3 | import pytest 4 | 5 | from streamlink.plugin import Plugin 6 | from streamlink_cli.main import get_formatter 7 | from streamlink_cli.utils import datetime 8 | 9 | 10 | @pytest.fixture(scope="module") 11 | def plugin(): 12 | class FakePlugin(Plugin): 13 | __module__ = "FAKE" 14 | 15 | def _get_streams(self): # pragma: no cover 16 | pass 17 | 18 | plugin = FakePlugin(Mock(), "https://foo/bar") 19 | plugin.id = "ID" 20 | plugin.author = "AUTHOR" 21 | plugin.category = "CATEGORY" 22 | plugin.title = "TITLE" 23 | 24 | return plugin 25 | 26 | 27 | @pytest.mark.parametrize("formatterinput,expected", [ 28 | ("{url}", "https://foo/bar"), 29 | ("{plugin}", "FAKE"), 30 | ("{id}", "ID"), 31 | ("{author}", "AUTHOR"), 32 | ("{category}", "CATEGORY"), 33 | ("{game}", "CATEGORY"), 34 | ("{title}", "TITLE"), 35 | ("{time}", "2000-01-01_00-00-00"), 36 | ("{time:%Y}", "2000"), 37 | ]) 38 | # workaround for freezegun not being able to patch the subclassed datetime class in streamlink_cli.utils 39 | # which defines the default datetime->str conversion format (needed for path outputs) 40 | @patch("streamlink_cli.utils.datetime.now", Mock(return_value=datetime(2000, 1, 1, 0, 0, 0, 0))) 41 | @patch("streamlink_cli.main.args", Mock(url="https://foo/bar")) 42 | def test_get_formatter(plugin, formatterinput, expected): 43 | formatter = get_formatter(plugin) 44 | assert formatter.title(formatterinput) == expected 45 | -------------------------------------------------------------------------------- /tests/cli/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunghome/streamlink_optionalkey/63c387cb8d088a6a41cfd128348aa237329a0f5c/tests/cli/utils/__init__.py -------------------------------------------------------------------------------- /tests/mixins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunghome/streamlink_optionalkey/63c387cb8d088a6a41cfd128348aa237329a0f5c/tests/mixins/__init__.py -------------------------------------------------------------------------------- /tests/plugin/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunghome/streamlink_optionalkey/63c387cb8d088a6a41cfd128348aa237329a0f5c/tests/plugin/__init__.py -------------------------------------------------------------------------------- /tests/plugin/override/testplugin.py: -------------------------------------------------------------------------------- 1 | from tests.plugin.testplugin import __plugin__ as TestPlugin 2 | 3 | 4 | class TestPluginOverride(TestPlugin): 5 | pass 6 | 7 | 8 | __plugin__ = TestPluginOverride 9 | -------------------------------------------------------------------------------- /tests/plugin/testplugin_invalid.py: -------------------------------------------------------------------------------- 1 | class TestPluginInvalid: 2 | pass 3 | 4 | 5 | # does not inherit from streamlink.plugin.plugin.Plugin 6 | __plugin__ = TestPluginInvalid 7 | -------------------------------------------------------------------------------- /tests/plugin/testplugin_missing.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugin.plugin import Plugin 2 | 3 | 4 | class TestPluginMissing(Plugin): 5 | pass 6 | 7 | 8 | # does not export plugin via __plugin__ 9 | # __plugin__ = TestPluginMissing 10 | -------------------------------------------------------------------------------- /tests/plugins/conftest.py: -------------------------------------------------------------------------------- 1 | from tests.plugins import PluginCanHandleUrl, generic_negative_matches 2 | 3 | 4 | def pytest_collection_modifyitems(session, config, items): # pragma: no cover 5 | # remove empty parametrized tests 6 | session.items = list(filter(lambda item: not any( 7 | marker.name == "skip" and str(marker.kwargs.get("reason", "")).startswith("got empty parameter set") 8 | for marker in item.own_markers 9 | ), items)) 10 | 11 | 12 | def pytest_generate_tests(metafunc): # pragma: no cover 13 | if metafunc.cls is not None and issubclass(metafunc.cls, PluginCanHandleUrl): 14 | if metafunc.function.__name__ == "test_can_handle_url_positive": 15 | metafunc.parametrize("url", metafunc.cls.should_match + [url for url, groups in metafunc.cls.should_match_groups]) 16 | 17 | elif metafunc.function.__name__ == "test_can_handle_url_negative": 18 | metafunc.parametrize("url", metafunc.cls.should_not_match + generic_negative_matches) 19 | 20 | elif metafunc.function.__name__ == "test_capture_groups": 21 | metafunc.parametrize("url,groups", metafunc.cls.should_match_groups, ids=[ 22 | f"URL={url} GROUPS={groups}" 23 | for url, groups in metafunc.cls.should_match_groups 24 | ]) 25 | -------------------------------------------------------------------------------- /tests/plugins/test_abematv.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.abematv import AbemaTV 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlAbemaTV(PluginCanHandleUrl): 6 | __plugin__ = AbemaTV 7 | 8 | should_match = [ 9 | 'https://abema.tv/now-on-air/abema-news', 10 | 'https://abema.tv/video/episode/90-1053_s99_p12', 11 | 'https://abema.tv/channels/everybody-anime/slots/FJcUsdYjTk1rAb', 12 | 'https://abema.tv/now-on-air/abema-news?a=b&c=d', 13 | 'https://abema.tv/video/episode/90-1053_s99_p12?a=b&c=d', 14 | 'https://abema.tv/channels/abema-anime/slots/9rTULtcJFiFmM9?a=b' 15 | ] 16 | 17 | should_not_match = [ 18 | 'https://www.abema.tv/now-on-air/abema-news', 19 | 'https://www.abema.tv/now-on-air/', 20 | 'https://abema.tv/timetable', 21 | 'https://abema.tv/video', 22 | 'https://abema.tv/video/title/13-47', 23 | 'https://abema.tv/video/title/13-47?a=b' 24 | ] 25 | -------------------------------------------------------------------------------- /tests/plugins/test_adultswim.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.adultswim import AdultSwim 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlAdultSwim(PluginCanHandleUrl): 6 | __plugin__ = AdultSwim 7 | 8 | should_match = [ 9 | "http://www.adultswim.com/streams", 10 | "http://www.adultswim.com/streams/", 11 | "https://www.adultswim.com/streams/infomercials", 12 | "https://www.adultswim.com/streams/last-stream-on-the-left-channel/", 13 | "https://www.adultswim.com/videos/as-seen-on-adult-swim/wednesday-march-18th-2020", 14 | "https://www.adultswim.com/videos/fishcenter-live/wednesday-april-29th-2020/" 15 | ] 16 | -------------------------------------------------------------------------------- /tests/plugins/test_afreeca.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.afreeca import AfreecaTV 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlAfreecaTV(PluginCanHandleUrl): 6 | __plugin__ = AfreecaTV 7 | 8 | should_match = [ 9 | "http://play.afreecatv.com/exampleuser", 10 | "http://play.afreecatv.com/exampleuser/123123123", 11 | "https://play.afreecatv.com/exampleuser", 12 | ] 13 | 14 | should_not_match = [ 15 | "http://afreeca.com/exampleuser", 16 | "http://afreeca.com/exampleuser/123123123", 17 | "http://afreecatv.com/exampleuser", 18 | "http://afreecatv.com/exampleuser/123123123", 19 | "http://www.afreecatv.com.tw", 20 | "http://www.afreecatv.jp", 21 | ] 22 | -------------------------------------------------------------------------------- /tests/plugins/test_aloula.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.aloula import Aloula 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlAloula(PluginCanHandleUrl): 6 | __plugin__ = Aloula 7 | 8 | should_match_groups = [ 9 | ("https://www.aloula.sa/live/slug", {"live_slug": "slug"}), 10 | ("https://www.aloula.sa/en/live/slug", {"live_slug": "slug"}), 11 | ("https://www.aloula.sa/de/live/slug/abc", {"live_slug": "slug"}), 12 | ("https://www.aloula.sa/episode/123", {"vod_id": "123"}), 13 | ("https://www.aloula.sa/en/episode/123", {"vod_id": "123"}), 14 | ("https://www.aloula.sa/episode/123abc/456", {"vod_id": "123"}), 15 | ("https://www.aloula.sa/de/episode/123abc/456", {"vod_id": "123"}), 16 | ("https://www.aloula.sa/episode/123?continue=8", {"vod_id": "123"}), 17 | ("https://www.aloula.sa/xx/episode/123?continue=8", {"vod_id": "123"}), 18 | ] 19 | 20 | should_not_match = [ 21 | "https://www.aloula.sa/en/any", 22 | "https://www.aloula.sa/de/any/path", 23 | "https://www.aloula.sa/live/", 24 | "https://www.aloula.sa/abc/live/slug", 25 | "https://www.aloula.sa/en/live/", 26 | "https://www.aloula.sa/episode/", 27 | "https://www.aloula.sa/abc/episode/123", 28 | "https://www.aloula.sa/en/episode/", 29 | "https://www.aloula.sa/episode/abc", 30 | "https://www.aloula.sa/de/episode/abc", 31 | ] 32 | -------------------------------------------------------------------------------- /tests/plugins/test_app17.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.app17 import App17 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlApp17(PluginCanHandleUrl): 6 | __plugin__ = App17 7 | 8 | should_match = [ 9 | "https://17.live/en-US/live/123123" 10 | "https://17.live/en/live/123123", 11 | "https://17.live/ja/live/123123", 12 | ] 13 | -------------------------------------------------------------------------------- /tests/plugins/test_ard_live.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.ard_live import ARDLive 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlARDLive(PluginCanHandleUrl): 6 | __plugin__ = ARDLive 7 | 8 | should_match = [ 9 | 'https://daserste.de/live/index.html', 10 | 'https://www.daserste.de/live/index.html', 11 | ] 12 | 13 | should_not_match = [ 14 | 'http://mediathek.daserste.de/live', 15 | ] 16 | -------------------------------------------------------------------------------- /tests/plugins/test_atpchallenger.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.atpchallenger import AtpChallengerTour 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlAtpChallenger(PluginCanHandleUrl): 6 | __plugin__ = AtpChallengerTour 7 | 8 | should_match = [ 9 | "https://www.atptour.com/en/atp-challenger-tour/challenger-tv", 10 | "https://www.atptour.com/es/atp-challenger-tour/challenger-tv", 11 | "https://www.atptour.com/en/atp-challenger-tour/challenger-tv/challenger-tv-search-results/" 12 | + "2022-2785-ms005-zug-alexander-ritschard-vs-dominic-stricker/2022/2785/all", 13 | ] 14 | -------------------------------------------------------------------------------- /tests/plugins/test_atresplayer.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.atresplayer import AtresPlayer 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlAtresPlayer(PluginCanHandleUrl): 6 | __plugin__ = AtresPlayer 7 | 8 | should_match = [ 9 | 'http://www.atresplayer.com/directos/antena3/', 10 | 'http://www.atresplayer.com/directos/lasexta/', 11 | 'https://www.atresplayer.com/directos/antena3/', 12 | 'https://www.atresplayer.com/flooxer/programas/unas/temporada-1/dario-eme-hache-sindy-takanashi-entrevista_123/', 13 | ] 14 | -------------------------------------------------------------------------------- /tests/plugins/test_bbciplayer.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from streamlink.plugins.bbciplayer import BBCiPlayer 4 | from tests.plugins import PluginCanHandleUrl 5 | 6 | 7 | class TestPluginCanHandleUrlBBCiPlayer(PluginCanHandleUrl): 8 | __plugin__ = BBCiPlayer 9 | 10 | should_match = [ 11 | "http://www.bbc.co.uk/iplayer/episode/b00ymh67/madagascar-1-island-of-marvels", 12 | "http://www.bbc.co.uk/iplayer/live/bbcone", 13 | ] 14 | 15 | should_not_match = [ 16 | "http://www.bbc.co.uk/iplayer/", 17 | ] 18 | 19 | 20 | class TestPluginBBCiPlayer(unittest.TestCase): 21 | def test_vpid_hash(self): 22 | self.assertEqual( 23 | "71c345435589c6ddeea70d6f252e2a52281ecbf3", 24 | BBCiPlayer._hash_vpid("1234567890") 25 | ) 26 | -------------------------------------------------------------------------------- /tests/plugins/test_bfmtv.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.bfmtv import BFMTV 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlBFMTV(PluginCanHandleUrl): 6 | __plugin__ = BFMTV 7 | 8 | should_match = [ 9 | "https://www.bfmtv.com/mediaplayer/live-video/", 10 | "https://bfmbusiness.bfmtv.com/mediaplayer/live-video/", 11 | "https://www.bfmtv.com/mediaplayer/live-bfm-paris/", 12 | "https://rmc.bfmtv.com/mediaplayer/live-audio/", 13 | "https://rmcsport.bfmtv.com/mediaplayer/live-bfm-sport/", 14 | "https://rmcdecouverte.bfmtv.com/mediaplayer-direct/", 15 | "https://www.bfmtv.com/mediaplayer/replay/premiere-edition/", 16 | "https://bfmbusiness.bfmtv.com/mediaplayer/replay/good-morning-business/", 17 | "https://rmc.bfmtv.com/mediaplayer/replay/les-grandes-gueules/", 18 | "https://rmc.bfmtv.com/mediaplayer/replay/after-foot/", 19 | "https://www.01net.com/mediaplayer/replay/jtech/", 20 | "https://www.bfmtv.com/politique/macron-et-le-pen-talonnes-par-fillon-et-melenchon-a-l-approche" 21 | + "-du-premier-tour-1142070.html", 22 | "https://rmcdecouverte.bfmtv.com/mediaplayer-replay/?id=6714&title=TOP%20GEAR%20:PASSION%20VINTAGE" 23 | ] 24 | -------------------------------------------------------------------------------- /tests/plugins/test_bigo.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.bigo import Bigo 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlBigo(PluginCanHandleUrl): 6 | __plugin__ = Bigo 7 | 8 | should_match = [ 9 | "http://bigo.tv/00000000", 10 | "https://bigo.tv/00000000", 11 | "https://www.bigo.tv/00000000", 12 | "http://www.bigo.tv/00000000", 13 | "http://www.bigo.tv/fancy1234", 14 | "http://www.bigo.tv/abc.123", 15 | "http://www.bigo.tv/000000.00" 16 | ] 17 | 18 | should_not_match = [ 19 | # Old URLs don't work anymore 20 | "http://live.bigo.tv/00000000", 21 | "https://live.bigo.tv/00000000", 22 | "http://www.bigoweb.co/show/00000000", 23 | "https://www.bigoweb.co/show/00000000", 24 | "http://bigoweb.co/show/00000000", 25 | "https://bigoweb.co/show/00000000" 26 | 27 | # Wrong URL structure 28 | "https://www.bigo.tv/show/00000000", 29 | "http://www.bigo.tv/show/00000000", 30 | "http://bigo.tv/show/00000000", 31 | "https://bigo.tv/show/00000000" 32 | ] 33 | -------------------------------------------------------------------------------- /tests/plugins/test_bilibili.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.bilibili import Bilibili 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlBilibili(PluginCanHandleUrl): 6 | __plugin__ = Bilibili 7 | 8 | should_match = [ 9 | 'https://live.bilibili.com/123123123', 10 | ] 11 | -------------------------------------------------------------------------------- /tests/plugins/test_blazetv.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.blazetv import BlazeTV 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlBlazeTV(PluginCanHandleUrl): 6 | __plugin__ = BlazeTV 7 | 8 | should_match_groups = [ 9 | ("https://blaze.tv/live", {"is_live": "live"}), 10 | ("https://watch.blaze.tv/live/", {"is_live": "live"}), 11 | ("https://watch.blaze.tv/watch/replay/123456", {}), 12 | ] 13 | 14 | should_not_match = [ 15 | "https://blaze.tv/abc", 16 | "https://watch.blaze.tv/watch/replay/", 17 | "https://watch.blaze.tv/watch/replay/abc123", 18 | ] 19 | -------------------------------------------------------------------------------- /tests/plugins/test_bloomberg.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.bloomberg import Bloomberg 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlBloomberg(PluginCanHandleUrl): 6 | __plugin__ = Bloomberg 7 | 8 | should_match_groups = [ 9 | ("https://www.bloomberg.com/live", {"live": "live"}), 10 | ("https://www.bloomberg.com/live/", {"live": "live"}), 11 | ("https://www.bloomberg.com/live/europe", {"live": "live", "channel": "europe"}), 12 | ("https://www.bloomberg.com/live/europe/", {"live": "live", "channel": "europe"}), 13 | ("https://www.bloomberg.com/news/videos/2022-08-10/-bloomberg-surveillance-early-edition-full-08-10-22", {}), 14 | ] 15 | 16 | should_not_match = [ 17 | "https://www.bloomberg.com/politics/articles/2017-04-17/french-race-up-for-grabs-days-before-voters-cast-first-ballots", 18 | ] 19 | -------------------------------------------------------------------------------- /tests/plugins/test_booyah.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.booyah import Booyah 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlBooyah(PluginCanHandleUrl): 6 | __plugin__ = Booyah 7 | 8 | should_match = [ 9 | 'http://booyah.live/nancysp', 10 | 'https://booyah.live/nancysp', 11 | 'http://booyah.live/channels/21755518', 12 | 'https://booyah.live/channels/21755518', 13 | 'http://booyah.live/clips/13271208573492782667?source=2', 14 | 'https://booyah.live/clips/13271208573492782667?source=2', 15 | 'http://booyah.live/vods/13865237825203323136?source=2', 16 | 'https://booyah.live/vods/13865237825203323136?source=2', 17 | 'http://www.booyah.live/nancysp', 18 | 'https://www.booyah.live/nancysp', 19 | 'http://www.booyah.live/channels/21755518', 20 | 'https://www.booyah.live/channels/21755518', 21 | 'http://www.booyah.live/clips/13271208573492782667?source=2', 22 | 'https://www.booyah.live/clips/13271208573492782667?source=2', 23 | 'http://www.booyah.live/vods/13865237825203323136?source=2', 24 | 'https://www.booyah.live/vods/13865237825203323136?source=2', 25 | ] 26 | 27 | should_not_match = [ 28 | 'http://booyah.live/', 29 | 'https://booyah.live/', 30 | 'http://www.booyah.live/', 31 | 'https://www.booyah.live/', 32 | ] 33 | -------------------------------------------------------------------------------- /tests/plugins/test_brightcove.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.brightcove import Brightcove 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlBrightcove(PluginCanHandleUrl): 6 | __plugin__ = Brightcove 7 | 8 | should_match = [ 9 | "https://players.brightcove.net/123/default_default/index.html?videoId=456", 10 | "https://players.brightcove.net/456/default_default/index.html?videoId=789", 11 | ] 12 | -------------------------------------------------------------------------------- /tests/plugins/test_btv.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.btv import BTV 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlBTV(PluginCanHandleUrl): 6 | __plugin__ = BTV 7 | 8 | should_match = [ 9 | "http://btvplus.bg/live", 10 | "http://btvplus.bg/live/", 11 | "http://www.btvplus.bg/live/", 12 | ] 13 | -------------------------------------------------------------------------------- /tests/plugins/test_cbsnews.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.cbsnews import CBSNews 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlCBSNews(PluginCanHandleUrl): 6 | __plugin__ = CBSNews 7 | 8 | should_match = [ 9 | "https://cbsnews.com/live", 10 | "https://cbsnews.com/live/cbs-sports-hq", 11 | "https://cbsnews.com/sanfrancisco/live", 12 | "https://cbsnews.com/live/", 13 | "https://cbsnews.com/live/cbs-sports-hq/", 14 | "https://cbsnews.com/sanfrancisco/live/", 15 | "https://www.cbsnews.com/live/", 16 | "https://www.cbsnews.com/live/cbs-sports-hq/", 17 | "https://www.cbsnews.com/sanfrancisco/live/", 18 | "https://www.cbsnews.com/live/#x", 19 | "https://www.cbsnews.com/live/cbs-sports-hq/#x", 20 | "https://www.cbsnews.com/sanfrancisco/live/#x", 21 | ] 22 | 23 | should_not_match = [ 24 | "https://www.cbsnews.com/feature/election-2020/", 25 | "https://www.cbsnews.com/48-hours/", 26 | ] 27 | -------------------------------------------------------------------------------- /tests/plugins/test_cdnbg.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.cdnbg import CDNBG 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlCDNBG(PluginCanHandleUrl): 6 | __plugin__ = CDNBG 7 | 8 | should_match = [ 9 | 'http://bgonair.bg/tvonline', 10 | 'http://bgonair.bg/tvonline/', 11 | 'http://www.nova.bg/live', 12 | 'http://nova.bg/live', 13 | 'http://bnt.bg/live', 14 | 'http://bnt.bg/live/bnt1', 15 | 'http://bnt.bg/live/bnt2', 16 | 'http://bnt.bg/live/bnt3', 17 | 'http://bnt.bg/live/bnt4', 18 | 'http://tv.bnt.bg/bnt1', 19 | 'http://tv.bnt.bg/bnt2', 20 | 'http://tv.bnt.bg/bnt3', 21 | 'http://tv.bnt.bg/bnt4', 22 | 'http://mu-vi.tv/LiveStreams/pages/Live.aspx', 23 | 'http://live.bstv.bg/', 24 | 'https://www.bloombergtv.bg/video', 25 | 'https://i.cdn.bg/live/xfr3453g0d', 26 | 'https://armymedia.bg/%d0%bd%d0%b0-%d0%b6%d0%b8%d0%b2%d0%be/', 27 | ] 28 | 29 | should_not_match = [ 30 | 'https://www.tvevropa.com', 31 | 'http://www.kanal3.bg/live', 32 | 'http://inlife.bg/', 33 | 'http://videochanel.bstv.bg', 34 | 'http://video.bstv.bg/', 35 | 'http://bitelevision.com/live', 36 | 'http://mmtvmusic.com/live/', 37 | 'http://chernomore.bg/', 38 | ] 39 | -------------------------------------------------------------------------------- /tests/plugins/test_ceskatelevize.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.ceskatelevize import Ceskatelevize 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlCeskatelevize(PluginCanHandleUrl): 6 | __plugin__ = Ceskatelevize 7 | 8 | should_match = [ 9 | "https://www.ceskatelevize.cz/zive/ct1/", 10 | "https://www.ceskatelevize.cz/zive/ct2/", 11 | "https://www.ceskatelevize.cz/zive/ct24/", 12 | "https://www.ceskatelevize.cz/zive/ct26/", 13 | "https://www.ceskatelevize.cz/zive/ct27/", 14 | "https://www.ceskatelevize.cz/zive/ct28/", 15 | "https://www.ceskatelevize.cz/zive/ct31/", 16 | "https://www.ceskatelevize.cz/zive/ct32/", 17 | "https://www.ceskatelevize.cz/zive/decko/", 18 | "https://www.ceskatelevize.cz/zive/sport/", 19 | ] 20 | 21 | should_not_match = [ 22 | "http://decko.ceskatelevize.cz/zive/", 23 | "http://www.ceskatelevize.cz/art/zive/", 24 | "http://www.ceskatelevize.cz/ct1/zive/", 25 | "http://www.ceskatelevize.cz/ct2/zive/", 26 | "http://www.ceskatelevize.cz/ct24/", 27 | "http://www.ceskatelevize.cz/sport/zive-vysilani/", 28 | ] 29 | -------------------------------------------------------------------------------- /tests/plugins/test_cinergroup.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.cinergroup import CinerGroup 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlCinerGroup(PluginCanHandleUrl): 6 | __plugin__ = CinerGroup 7 | 8 | should_match = [ 9 | "https://showtv.com.tr/canli-yayin", 10 | "https://showtv.com.tr/canli-yayin/showtv", 11 | "https://haberturk.com/canliyayin", 12 | "https://haberturk.com/tv/canliyayin", 13 | "http://haberturk.tv/canliyayin", 14 | "http://showmax.com.tr/canliyayin", 15 | "https://showturk.com.tr/canli-yayin/showturk", 16 | ] 17 | -------------------------------------------------------------------------------- /tests/plugins/test_clubbingtv.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.clubbingtv import ClubbingTV 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlClubbingTV(PluginCanHandleUrl): 6 | __plugin__ = ClubbingTV 7 | 8 | should_match = [ 9 | "https://www.clubbingtv.com/live", 10 | "https://www.clubbingtv.com/video/play/3950/moonlight/", 11 | "https://www.clubbingtv.com/video/play/2897", 12 | "https://www.clubbingtv.com/tomer-s-pick/", 13 | ] 14 | -------------------------------------------------------------------------------- /tests/plugins/test_cmmedia.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.cmmedia import CMMedia 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlCMMedia(PluginCanHandleUrl): 6 | __plugin__ = CMMedia 7 | 8 | should_match = [ 9 | "http://cmmedia.es", 10 | "http://www.cmmedia.es", 11 | "http://cmmedia.es/any/path", 12 | "http://cmmedia.es/any/path?x", 13 | "http://www.cmmedia.es/any/path?x&y", 14 | "https://cmmedia.es", 15 | "https://www.cmmedia.es", 16 | "https://cmmedia.es/any/path", 17 | "https://cmmedia.es/any/path?x", 18 | "https://www.cmmedia.es/any/path?x&y", 19 | ] 20 | -------------------------------------------------------------------------------- /tests/plugins/test_cnews.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.cnews import CNEWS 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlCNEWS(PluginCanHandleUrl): 6 | __plugin__ = CNEWS 7 | 8 | should_match = [ 9 | "http://www.cnews.fr/le-direct", 10 | "http://www.cnews.fr/direct", 11 | "http://www.cnews.fr/emission/2018-06-12/meteo-du-12062018-784730", 12 | "http://www.cnews.fr/emission/2018-06-12/le-journal-des-faits-divers-du-12062018-784704" 13 | ] 14 | -------------------------------------------------------------------------------- /tests/plugins/test_dailymotion.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.dailymotion import DailyMotion 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlDailyMotion(PluginCanHandleUrl): 6 | __plugin__ = DailyMotion 7 | 8 | should_match = [ 9 | "https://www.dailymotion.com/video/xigbvx", 10 | "https://www.dailymotion.com/france24", 11 | "https://www.dailymotion.com/embed/video/xigbvx", 12 | ] 13 | 14 | should_not_match = [ 15 | "https://www.dailymotion.com/", 16 | ] 17 | -------------------------------------------------------------------------------- /tests/plugins/test_delfi.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.delfi import Delfi 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlDelfi(PluginCanHandleUrl): 6 | __plugin__ = Delfi 7 | 8 | should_match = [ 9 | # delfi.lt live (YouTube) 10 | "https://www.delfi.lt/video/tv/", 11 | # delfi.lt VOD 12 | "https://www.delfi.lt/video/laidos/dakaras/dakaras-2022-koki-tiksla-turi-pirmasis-lietuviskas-sunkvezimio-ekipazas.d" 13 | + "?id=89058633", 14 | 15 | # delfi.lv VOD 16 | "http://www.delfi.lv/delfi-tv-ar-jani-domburu/pilnie-raidijumi/delfi-tv-ar-jani-domburu-atbild" 17 | + "-veselibas-ministre-anda-caksa-pilna-intervija?id=49515013", 18 | # delfi.lv VOD (YouTube) 19 | "https://www.delfi.lv/news/national/politics/video-gads-ko-varetu-aizmirst-bet-nesanak-spilgtako-notikumu-atskats.d" 20 | + "?id=53912761", 21 | 22 | # delfi.ee live 23 | "https://sport.delfi.ee/artikkel/95517317/otse-delfi-tv-s-kalevcramo-voitis-tartu-ulikooli-vastu-avapoolaja-14" 24 | + "-punktiga", 25 | ] 26 | -------------------------------------------------------------------------------- /tests/plugins/test_deutschewelle.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.deutschewelle import DeutscheWelle 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlDeutscheWelle(PluginCanHandleUrl): 6 | __plugin__ = DeutscheWelle 7 | 8 | should_match = [ 9 | # Live: en/EN (no channel selection) 10 | "https://www.dw.com/en/live-tv/s-100825", 11 | 12 | # Live: de/DE (default channel) 13 | "https://www.dw.com/de/live-tv/s-100817", 14 | # Live: de/DE (selected channel) 15 | "https://www.dw.com/de/live-tv/s-100817?channel=5", 16 | # Live: de/EN (selected channel) 17 | "https://www.dw.com/de/live-tv/s-100817?channel=1", 18 | 19 | # Live: es/ES (default channel) 20 | "https://www.dw.com/es/tv-en-vivo/s-100837", 21 | # Live: es/ES (selected channel) 22 | "https://www.dw.com/es/tv-en-vivo/s-100837?channel=3", 23 | # Live: es/DE (selected channel) 24 | "https://www.dw.com/es/tv-en-vivo/s-100837?channel=5", 25 | 26 | # VOD 27 | "https://www.dw.com/en/top-stories-in-90-seconds/av-49496622", 28 | 29 | # Audio 30 | "https://www.dw.com/en/womens-euros-2022-the-end/av-62738085", 31 | ] 32 | -------------------------------------------------------------------------------- /tests/plugins/test_dlive.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.dlive import DLive 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlDLive(PluginCanHandleUrl): 6 | __plugin__ = DLive 7 | 8 | should_match = [ 9 | "https://dlive.tv/pewdiepie", 10 | "https://dlive.tv/p/pdp+K6DqqtYWR", 11 | ] 12 | 13 | should_not_match = [ 14 | "https://dlive.tv/", 15 | ] 16 | -------------------------------------------------------------------------------- /tests/plugins/test_dogus.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.dogus import Dogus 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlDogus(PluginCanHandleUrl): 6 | __plugin__ = Dogus 7 | 8 | should_match = [ 9 | "http://eurostartv.com.tr/canli-izle", 10 | "https://www.kralmuzik.com.tr/tv/kral-pop-tv", 11 | "https://www.kralmuzik.com.tr/tv/kral-tv", 12 | "https://www.ntv.com.tr/canli-yayin/ntv?youtube=true", 13 | "https://www.startv.com.tr/canli-yayin", 14 | ] 15 | -------------------------------------------------------------------------------- /tests/plugins/test_drdk.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.drdk import DRDK 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlDRDK(PluginCanHandleUrl): 6 | __plugin__ = DRDK 7 | 8 | should_match = [ 9 | 'https://www.dr.dk/drtv/kanal/dr1_20875', 10 | 'https://www.dr.dk/drtv/kanal/dr2_20876', 11 | 'https://www.dr.dk/drtv/kanal/dr-ramasjang_20892', 12 | ] 13 | 14 | should_not_match = [ 15 | 'https://www.dr.dk/tv/live/dr1', 16 | 'https://www.dr.dk/tv/live/dr2', 17 | 'https://www.dr.dk/tv/se/matador/matador-saeson-3/matador-15-24', 18 | ] 19 | -------------------------------------------------------------------------------- /tests/plugins/test_earthcam.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.earthcam import EarthCam 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlEarthCam(PluginCanHandleUrl): 6 | __plugin__ = EarthCam 7 | 8 | should_match = [ 9 | 'https://www.earthcam.com/usa/newyork/timessquare/?cam=tsstreet', 10 | 'https://www.earthcam.com/usa/newyork/timessquare/?cam=gts1', 11 | ] 12 | -------------------------------------------------------------------------------- /tests/plugins/test_egame.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.egame import Egame 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlEgame(PluginCanHandleUrl): 6 | __plugin__ = Egame 7 | 8 | should_match = [ 9 | 'https://egame.qq.com/497383565', 10 | ] 11 | 12 | should_not_match = [ 13 | 'https://egame.qq.com/', 14 | 'https://egame.qq.com/livelist?layoutid=lol', 15 | 'https://egame.qq.com/vod?videoId=123123123123123', 16 | 'https://egame.qq.com/aaabbbb' 17 | ] 18 | -------------------------------------------------------------------------------- /tests/plugins/test_euronews.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.euronews import Euronews 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlEuronews(PluginCanHandleUrl): 6 | __plugin__ = Euronews 7 | 8 | should_match = [ 9 | "http://www.euronews.com/live", 10 | "http://fr.euronews.com/live", 11 | "http://de.euronews.com/live", 12 | "http://it.euronews.com/live", 13 | "http://es.euronews.com/live", 14 | "http://pt.euronews.com/live", 15 | "http://ru.euronews.com/live", 16 | "http://ua.euronews.com/live", 17 | "http://tr.euronews.com/live", 18 | "http://gr.euronews.com/live", 19 | "http://hu.euronews.com/live", 20 | "http://fa.euronews.com/live", 21 | "http://arabic.euronews.com/live", 22 | "http://www.euronews.com/2017/05/10/peugeot-expects-more-opel-losses-this-year", 23 | "http://fr.euronews.com/2017/05/10/l-ag-de-psa-approuve-le-rachat-d-opel" 24 | ] 25 | -------------------------------------------------------------------------------- /tests/plugins/test_facebook.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.facebook import Facebook 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlFacebook(PluginCanHandleUrl): 6 | __plugin__ = Facebook 7 | 8 | should_match = [ 9 | "https://www.facebook.com/nos/videos/1725546430794241/", 10 | "https://www.facebook.com/nytfood/videos/1485091228202006/", 11 | "https://www.facebook.com/SporTurkTR/videos/798553173631138/", 12 | "https://www.facebook.com/119555411802156/posts/500665313691162/", 13 | "https://www.facebookwkhpilnemxj7asaniu7vnjjbiltxjqhye3mhbshg7kx5tfyd.onion/SporTurkTR/videos/798553173631138/", 14 | ] 15 | 16 | should_not_match = [ 17 | "https://www.facebook.com", 18 | ] 19 | -------------------------------------------------------------------------------- /tests/plugins/test_foxtr.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.foxtr import FoxTR 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlFoxTR(PluginCanHandleUrl): 6 | __plugin__ = FoxTR 7 | 8 | should_match = [ 9 | 'http://www.fox.com.tr/canli-yayin', 10 | ] 11 | -------------------------------------------------------------------------------- /tests/plugins/test_funimationnow.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import ANY, MagicMock, call 3 | 4 | from streamlink import Streamlink 5 | from streamlink.plugins.funimationnow import FunimationNow 6 | from tests.plugins import PluginCanHandleUrl 7 | 8 | 9 | class TestPluginCanHandleUrlFunimationNow(PluginCanHandleUrl): 10 | __plugin__ = FunimationNow 11 | 12 | should_match = [ 13 | "http://www.funimation.com/anything", 14 | "http://www.funimation.com/anything123", 15 | "http://www.funimationnow.uk/anything", 16 | "http://www.funimationnow.uk/anything123", 17 | ] 18 | 19 | 20 | class TestPluginFunimationNow(unittest.TestCase): 21 | def test_arguments(self): 22 | from streamlink_cli.main import setup_plugin_args 23 | session = Streamlink() 24 | parser = MagicMock() 25 | plugins = parser.add_argument_group("Plugin Options") 26 | group = parser.add_argument_group("FunimationNow", parent=plugins) 27 | 28 | session.plugins = { 29 | 'funimationnow': FunimationNow 30 | } 31 | 32 | setup_plugin_args(session, parser) 33 | self.assertSequenceEqual( 34 | group.add_argument.mock_calls, 35 | [ 36 | call('--funimation-email', help=ANY), 37 | call('--funimation-password', help=ANY), 38 | call('--funimation-language', choices=["en", "ja", "english", "japanese"], default="english", help=ANY) 39 | ] 40 | ) 41 | -------------------------------------------------------------------------------- /tests/plugins/test_galatasaraytv.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.galatasaraytv import GalatasarayTV 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlGalatasarayTV(PluginCanHandleUrl): 6 | __plugin__ = GalatasarayTV 7 | 8 | should_match = [ 9 | 'http://galatasaray.com/', 10 | 'https://galatasaray.com', 11 | 'https://galatasaray.com/', 12 | 'https://www.galatasaray.com/', 13 | ] 14 | -------------------------------------------------------------------------------- /tests/plugins/test_goltelevision.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.goltelevision import GOLTelevision 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlEuronews(PluginCanHandleUrl): 6 | __plugin__ = GOLTelevision 7 | 8 | should_match = [ 9 | "http://goltelevision.com/en-directo", 10 | "http://www.goltelevision.com/en-directo", 11 | "https://goltelevision.com/en-directo", 12 | "https://www.goltelevision.com/en-directo", 13 | ] 14 | 15 | should_not_match = [ 16 | "http://goltelevision.com/live", 17 | "http://www.goltelevision.com/live", 18 | "https://goltelevision.com/live", 19 | "https://www.goltelevision.com/live", 20 | ] 21 | -------------------------------------------------------------------------------- /tests/plugins/test_goodgame.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.goodgame import GoodGame 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlGoodGame(PluginCanHandleUrl): 6 | __plugin__ = GoodGame 7 | 8 | should_match = [ 9 | 'https://goodgame.ru/channel/ABC_ABC/#autoplay', 10 | 'https://goodgame.ru/channel/ABC123ABC/#autoplay', 11 | 'https://goodgame.ru/channel/ABC/#autoplay', 12 | 'https://goodgame.ru/channel/123ABC123/#autoplay', 13 | ] 14 | -------------------------------------------------------------------------------- /tests/plugins/test_googledrive.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.googledrive import GoogleDocs 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlGoogleDocs(PluginCanHandleUrl): 6 | __plugin__ = GoogleDocs 7 | 8 | should_match = [ 9 | 'https://drive.google.com/file/d/123123/preview?start=1', 10 | ] 11 | -------------------------------------------------------------------------------- /tests/plugins/test_gulli.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.gulli import Gulli 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlGulli(PluginCanHandleUrl): 6 | __plugin__ = Gulli 7 | 8 | should_match = [ 9 | "http://replay.gulli.fr/Direct", 10 | "http://replay.gulli.fr/dessins-animes/My-Little-Pony-les-amies-c-est-magique/VOD68328764799000", 11 | "https://replay.gulli.fr/emissions/In-Ze-Boite2/VOD68639028668000", 12 | "https://replay.gulli.fr/series/Power-Rangers-Dino-Super-Charge/VOD68612908435000" 13 | ] 14 | 15 | should_not_match = [ 16 | "http://replay.gulli.fr/", 17 | "http://replay.gulli.fr/dessins-animes", 18 | "http://replay.gulli.fr/emissions", 19 | "http://replay.gulli.fr/series", 20 | ] 21 | -------------------------------------------------------------------------------- /tests/plugins/test_hiplayer.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.hiplayer import HiPlayer 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlHiPlayer(PluginCanHandleUrl): 6 | __plugin__ = HiPlayer 7 | 8 | should_match = [ 9 | "https://www.alwasat.ly/any", 10 | "https://www.alwasat.ly/any/path", 11 | "https://www.cnbcarabia.com/any", 12 | "https://www.cnbcarabia.com/any/path", 13 | "https://www.media.gov.kw/any", 14 | "https://www.media.gov.kw/any/path", 15 | "https://rotana.net/any", 16 | "https://rotana.net/any/path", 17 | ] 18 | -------------------------------------------------------------------------------- /tests/plugins/test_htv.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.htv import HTV 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlHTV(PluginCanHandleUrl): 6 | __plugin__ = HTV 7 | 8 | should_match_groups = [ 9 | ("https://htv.com.vn/truc-tuyen", {}), 10 | ("https://htv.com.vn/truc-tuyen?channel=123", {"channel": "123"}), 11 | ("https://htv.com.vn/truc-tuyen?channel=123&foo", {"channel": "123"}), 12 | ("https://www.htv.com.vn/truc-tuyen", {}), 13 | ("https://www.htv.com.vn/truc-tuyen?channel=123", {"channel": "123"}), 14 | ("https://www.htv.com.vn/truc-tuyen?channel=123&foo", {"channel": "123"}), 15 | ] 16 | 17 | should_not_match = [ 18 | "https://htv.com.vn/", 19 | "https://htv.com.vn/any/path", 20 | "https://htv.com.vn/truc-tuyen?foo", 21 | "https://www.htv.com.vn/", 22 | "https://www.htv.com.vn/any/path", 23 | "https://www.htv.com.vn/truc-tuyen?foo", 24 | ] 25 | -------------------------------------------------------------------------------- /tests/plugins/test_huajiao.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.huajiao import Huajiao 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlHuajiao(PluginCanHandleUrl): 6 | __plugin__ = Huajiao 7 | 8 | should_match = [ 9 | "http://www.huajiao.com/l/123123123", 10 | "https://www.huajiao.com/l/123123123", 11 | ] 12 | -------------------------------------------------------------------------------- /tests/plugins/test_huya.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.huya import Huya 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlHuya(PluginCanHandleUrl): 6 | __plugin__ = Huya 7 | 8 | should_match = [ 9 | "http://www.huya.com/123123123", 10 | "http://www.huya.com/name", 11 | "https://www.huya.com/123123123", 12 | "https://www.huya.com/name", 13 | ] 14 | 15 | should_not_match = [ 16 | "http://www.huya.com", 17 | "https://www.huya.com", 18 | ] 19 | -------------------------------------------------------------------------------- /tests/plugins/test_idf1.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.idf1 import IDF1 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlIDF1(PluginCanHandleUrl): 6 | __plugin__ = IDF1 7 | 8 | should_match = [ 9 | "https://www.idf1.fr/live", 10 | "https://www.idf1.fr/videos/jlpp/best-of-2018-02-24-partie-2.html", 11 | "http://www.idf1.fr/videos/buzz-de-noel/partie-2.html", 12 | ] 13 | 14 | should_not_match = [ 15 | "https://www.idf1.fr/", 16 | "https://www.idf1.fr/videos", 17 | "https://www.idf1.fr/programmes/emissions/idf1-chez-vous.html", 18 | ] 19 | -------------------------------------------------------------------------------- /tests/plugins/test_invintus.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.invintus import InvintusMedia 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlInvintusMedia(PluginCanHandleUrl): 6 | __plugin__ = InvintusMedia 7 | 8 | should_match = [ 9 | 'https://player.invintus.com/?clientID=9375922947&eventID=2020031185', 10 | 'https://player.invintus.com/?clientID=9375922947&eventID=2020031184', 11 | 'https://player.invintus.com/?clientID=9375922947&eventID=2020031183', 12 | 'https://player.invintus.com/?clientID=9375922947&eventID=2020031182', 13 | 'https://player.invintus.com/?clientID=9375922947&eventID=2020031181' 14 | ] 15 | -------------------------------------------------------------------------------- /tests/plugins/test_kugou.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.kugou import Kugou 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlKugou(PluginCanHandleUrl): 6 | __plugin__ = Kugou 7 | 8 | should_match = [ 9 | 'https://fanxing.kugou.com/1062645?refer=605', 10 | 'https://fanxing.kugou.com/77997777?refer=605', 11 | 'https://fanxing.kugou.com/1047927?refer=605', 12 | 'https://fanxing.kugou.com/1048570?refer=605', 13 | 'https://fanxing.kugou.com/1062642?refer=605', 14 | 'https://fanxing.kugou.com/1071651', 15 | ] 16 | -------------------------------------------------------------------------------- /tests/plugins/test_linelive.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.linelive import LineLive 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlLineLive(PluginCanHandleUrl): 6 | __plugin__ = LineLive 7 | 8 | should_match = [ 9 | 'http://live.line.me/channels/123/broadcast/12345678', 10 | 'https://live.line.me/channels/123/broadcast/12345678', 11 | ] 12 | 13 | should_not_match = [ 14 | 'https://live.line.me/channels/123/upcoming/12345678', 15 | ] 16 | -------------------------------------------------------------------------------- /tests/plugins/test_lnk.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.lnk import LNK 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlLRT(PluginCanHandleUrl): 6 | __plugin__ = LNK 7 | 8 | should_match = [ 9 | "https://lnk.lt/tiesiogiai", 10 | "https://lnk.lt/tiesiogiai#lnk", 11 | "https://lnk.lt/tiesiogiai#btv", 12 | "https://lnk.lt/tiesiogiai#2tv", 13 | "https://lnk.lt/tiesiogiai#infotv", 14 | "https://lnk.lt/tiesiogiai#tv1", 15 | ] 16 | 17 | should_not_match = [ 18 | "https://lnk.lt/", 19 | "https://lnk.lt/vaikams", 20 | "https://lnk.lt/zinios/Visi/157471", 21 | ] 22 | -------------------------------------------------------------------------------- /tests/plugins/test_lrt.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.lrt import LRT 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlLRT(PluginCanHandleUrl): 6 | __plugin__ = LRT 7 | 8 | should_match = [ 9 | "https://www.lrt.lt/mediateka/tiesiogiai/lrt-opus", 10 | "https://www.lrt.lt/mediateka/tiesiogiai/lrt-klasika", 11 | "https://www.lrt.lt/mediateka/tiesiogiai/lrt-radijas", 12 | "https://www.lrt.lt/mediateka/tiesiogiai/lrt-lituanica", 13 | "https://www.lrt.lt/mediateka/tiesiogiai/lrt-plius", 14 | "https://www.lrt.lt/mediateka/tiesiogiai/lrt-televizija", 15 | ] 16 | 17 | should_not_match = [ 18 | "https://www.lrt.lt", 19 | "https://www.lrt.lt/mediateka/irasas/1013694276/savanoriai-tures-galimybe-pamatyti-popieziu-is-arciau", 20 | ] 21 | -------------------------------------------------------------------------------- /tests/plugins/test_ltv_lsm_lv.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.ltv_lsm_lv import LtvLsmLv 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlLtvLsmLv(PluginCanHandleUrl): 6 | __plugin__ = LtvLsmLv 7 | 8 | should_match = [ 9 | "https://ltv.lsm.lv/lv/tiesraide/example/", 10 | "https://ltv.lsm.lv/lv/tiesraide/example/", 11 | "https://ltv.lsm.lv/lv/tiesraide/example/live.123/", 12 | "https://ltv.lsm.lv/lv/tiesraide/example/live.123/", 13 | ] 14 | 15 | should_not_match = [ 16 | "https://ltv.lsm.lv", 17 | "http://ltv.lsm.lv", 18 | "https://ltv.lsm.lv/lv", 19 | "http://ltv.lsm.lv/lv", 20 | "https://ltv.lsm.lv/other-site/", 21 | "http://ltv.lsm.lv/other-site/", 22 | "https://ltv.lsm.lv/lv/other-site/", 23 | "http://ltv.lsm.lv/lv/other-site/", 24 | "https://ltv.lsm.lv/lv/tieshraide/example/", 25 | ] 26 | -------------------------------------------------------------------------------- /tests/plugins/test_mdstrm.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.mdstrm import MDStrm 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlMDStrm(PluginCanHandleUrl): 6 | __plugin__ = MDStrm 7 | 8 | should_match = [ 9 | "https://mdstrm.com/live-stream/57b4dbf5dbbfc8f16bb63ce1", 10 | "https://mdstrm.com/live-stream/5a7b1e63a8da282c34d65445", 11 | "https://mdstrm.com/live-stream/5ce7109c7398b977dc0744cd", 12 | "https://mdstrm.com/live-stream/60b578b060947317de7b57ac", 13 | "https://mdstrm.com/live-stream/61e1e088d04d7744686afc42", 14 | "https://saltillo.multimedios.com/video/monterrey-tv-en-vivo/v7567", 15 | "https://www.latina.pe/tvenvivo", 16 | ] 17 | -------------------------------------------------------------------------------- /tests/plugins/test_mediaklikk.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.mediaklikk import Mediaklikk 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlMediaklikk(PluginCanHandleUrl): 6 | __plugin__ = Mediaklikk 7 | 8 | should_match = [ 9 | 'https://www.mediaklikk.hu/duna-world-elo/', 10 | 'https://www.mediaklikk.hu/duna-world-radio-elo', 11 | 'https://www.mediaklikk.hu/m1-elo', 12 | 'https://www.mediaklikk.hu/m2-elo', 13 | 'https://mediaklikk.hu/video/hirado-2021-06-24-i-adas-6/', 14 | 'https://m4sport.hu/elo/', 15 | 'https://m4sport.hu/elo/?channelId=m4sport+', 16 | 'https://m4sport.hu/elo/?showchannel=mtv4plus', 17 | 'https://m4sport.hu/euro2020-video/goool2-13-resz', 18 | 'https://hirado.hu/videok/', 19 | 'https://hirado.hu/videok/nemzeti-sporthirado-2021-06-24-i-adas-2/', 20 | 'https://petofilive.hu/video/2021/06/23/the-anahit-klip-limited-edition/', 21 | ] 22 | -------------------------------------------------------------------------------- /tests/plugins/test_mildom.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.mildom import Mildom 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlMildom(PluginCanHandleUrl): 6 | __plugin__ = Mildom 7 | 8 | should_match = [ 9 | 'https://www.mildom.com/10707087', 10 | 'https://www.mildom.com/playback/10707087/10707087-c0p1d4d2lrnb79gc0kqg', 11 | ] 12 | 13 | should_not_match = [ 14 | 'https://support.mildom.com', 15 | 'https://www.mildom.com/ranking', 16 | ] 17 | -------------------------------------------------------------------------------- /tests/plugins/test_mitele.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.mitele import Mitele 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlMitele(PluginCanHandleUrl): 6 | __plugin__ = Mitele 7 | 8 | should_match = [ 9 | "http://www.mitele.es/directo/bemad", 10 | "http://www.mitele.es/directo/boing", 11 | "http://www.mitele.es/directo/cuatro", 12 | "http://www.mitele.es/directo/divinity", 13 | "http://www.mitele.es/directo/energy", 14 | "http://www.mitele.es/directo/fdf", 15 | "http://www.mitele.es/directo/telecinco", 16 | "https://www.mitele.es/directo/gh-duo-24h-senal-1", 17 | "https://www.mitele.es/directo/gh-duo-24h-senal-2", 18 | ] 19 | 20 | should_not_match = [ 21 | "http://www.mitele.es", 22 | ] 23 | -------------------------------------------------------------------------------- /tests/plugins/test_mjunoon.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.mjunoon import Mjunoon 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlMjunoon(PluginCanHandleUrl): 6 | __plugin__ = Mjunoon 7 | 8 | should_match = [ 9 | 'https://mjunoon.tv/news-live', 10 | 'http://mjunoon.tv/watch/some-long-vod-name23456', 11 | 'https://www.mjunoon.tv/other-live', 12 | 'https://www.mjunoon.tv/watch/something-else-2321', 13 | ] 14 | 15 | should_not_match = [ 16 | 'https://mjunoon.com', 17 | ] 18 | -------------------------------------------------------------------------------- /tests/plugins/test_mrtmk.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.mrtmk import MRTmk 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlMRTmk(PluginCanHandleUrl): 6 | __plugin__ = MRTmk 7 | 8 | should_match = [ 9 | 'http://play.mrt.com.mk/live/658323455489957', 10 | 'http://play.mrt.com.mk/live/47', 11 | 'http://play.mrt.com.mk/play/1581', 12 | ] 13 | 14 | should_not_match = [ 15 | 'http://play.mrt.com.mk/', 16 | 'http://play.mrt.com.mk/c/2', 17 | ] 18 | -------------------------------------------------------------------------------- /tests/plugins/test_n13tv.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.n13tv import N13TV 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlN13TV(PluginCanHandleUrl): 6 | __plugin__ = N13TV 7 | 8 | should_match = [ 9 | "http://13tv.co.il/live", 10 | "https://13tv.co.il/live", 11 | "http://www.13tv.co.il/live/", 12 | "https://www.13tv.co.il/live/", 13 | "http://13tv.co.il/item/entertainment/ambush/season-02/episodes/ffuk3-2026112/", 14 | "https://13tv.co.il/item/entertainment/ambush/season-02/episodes/ffuk3-2026112/", 15 | "http://www.13tv.co.il/item/entertainment/ambush/season-02/episodes/ffuk3-2026112/", 16 | "https://www.13tv.co.il/item/entertainment/ambush/season-02/episodes/ffuk3-2026112/" 17 | "http://13tv.co.il/item/entertainment/tzhok-mehamatzav/season-01/episodes/vkdoc-2023442/", 18 | "https://13tv.co.il/item/entertainment/tzhok-mehamatzav/season-01/episodes/vkdoc-2023442/", 19 | "http://www.13tv.co.il/item/entertainment/tzhok-mehamatzav/season-01/episodes/vkdoc-2023442/" 20 | "https://www.13tv.co.il/item/entertainment/tzhok-mehamatzav/season-01/episodes/vkdoc-2023442/" 21 | ] 22 | -------------------------------------------------------------------------------- /tests/plugins/test_nbcnews.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.nbcnews import NBCNews 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlNBCNews(PluginCanHandleUrl): 6 | __plugin__ = NBCNews 7 | 8 | should_match = [ 9 | 'https://www.nbcnews.com/now/', 10 | 'http://www.nbcnews.com/now/' 11 | ] 12 | 13 | should_not_match = [ 14 | 'https://www.nbcnews.com/', 15 | 'http://www.nbcnews.com/' 16 | ] 17 | -------------------------------------------------------------------------------- /tests/plugins/test_nhkworld.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.nhkworld import NHKWorld 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlNHKWorld(PluginCanHandleUrl): 6 | __plugin__ = NHKWorld 7 | 8 | should_match = [ 9 | 'https://www3.nhk.or.jp/nhkworld/en/live/', 10 | ] 11 | -------------------------------------------------------------------------------- /tests/plugins/test_nicolive.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.nicolive import NicoLive 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlNicoLive(PluginCanHandleUrl): 6 | __plugin__ = NicoLive 7 | 8 | should_match = [ 9 | 'https://live2.nicovideo.jp/watch/lv534562961', 10 | 'http://live2.nicovideo.jp/watch/lv534562961', 11 | 'https://live.nicovideo.jp/watch/lv534562961', 12 | 'https://live2.nicovideo.jp/watch/lv534562961?ref=rtrec&zroute=recent', 13 | 'https://live.nicovideo.jp/watch/co2467009?ref=community', 14 | 'https://live.nicovideo.jp/watch/co2619719', 15 | ] 16 | -------------------------------------------------------------------------------- /tests/plugins/test_nimotv.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.nimotv import NimoTV 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginNimoTV(PluginCanHandleUrl): 6 | __plugin__ = NimoTV 7 | 8 | should_match = [ 9 | 'http://www.nimo.tv/live/737614', 10 | 'https://www.nimo.tv/live/737614', 11 | 'http://www.nimo.tv/sanz', 12 | 'https://www.nimo.tv/sanz', 13 | 'https://m.nimo.tv/user', 14 | ] 15 | -------------------------------------------------------------------------------- /tests/plugins/test_nos.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.nos import NOS 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlNOS(PluginCanHandleUrl): 6 | __plugin__ = NOS 7 | 8 | should_match = [ 9 | 'https://nos.nl/livestream/2220100-wk-sprint-schaatsen-1-000-meter-mannen.html', 10 | 'https://nos.nl/collectie/13781/livestream/2385081-ek-voetbal-engeland-schotland', 11 | 'https://nos.nl/collectie/13781/livestream/2385461-ek-voetbal-voorbeschouwing-italie-wales-18-00-uur', 12 | 'https://nos.nl/collectie/13781/video/2385846-ek-in-2-21-gosens-show-tegen-portugal-en-weer-volle-bak-in-boedapest', 13 | 'https://nos.nl/video/2385779-dronebeelden-tonen-spoor-van-vernieling-bij-leersum', 14 | 'https://nos.nl/uitzendingen', 15 | 'https://nos.nl/uitzendingen/livestream/2385462', 16 | ] 17 | 18 | should_not_match = [ 19 | 'https://nos.nl/artikel/2385784-explosieve-situatie-leidde-tot-verwoeste-huizen-en-omgewaaide-bomen-leersum', 20 | ] 21 | -------------------------------------------------------------------------------- /tests/plugins/test_nownews.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.nownews import NowNews 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlNowNews(PluginCanHandleUrl): 6 | __plugin__ = NowNews 7 | 8 | should_match = [ 9 | "https://news.now.com/home/live", 10 | "http://news.now.com/home/live", 11 | "https://news.now.com/home/live331a", 12 | "http://news.now.com/home/live331a" 13 | ] 14 | 15 | should_not_match = [ 16 | "https://news.now.com/home/local", 17 | "http://media.now.com.hk/", 18 | ] 19 | -------------------------------------------------------------------------------- /tests/plugins/test_nrk.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.nrk import NRK 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlNRK(PluginCanHandleUrl): 6 | __plugin__ = NRK 7 | 8 | should_match = [ 9 | 'https://tv.nrk.no/direkte/nrk1', 10 | 'https://tv.nrk.no/direkte/nrk2', 11 | 'https://tv.nrk.no/direkte/nrk3', 12 | 'https://tv.nrk.no/direkte/nrksuper', 13 | 14 | 'https://tv.nrk.no/serie/nytt-paa-nytt/2020/MUHH43003020', 15 | 'https://tv.nrk.no/serie/kongelige-fotografer/sesong/1/episode/2/avspiller', 16 | 17 | 'https://tv.nrk.no/program/NNFA51102617', 18 | 19 | 'https://radio.nrk.no/direkte/p1', 20 | 'https://radio.nrk.no/direkte/p2', 21 | 22 | 'https://radio.nrk.no/podkast/oppdatert/l_5005d62a-7f4f-4581-85d6-2a7f4f2581f2', 23 | ] 24 | 25 | should_not_match = [ 26 | 'https://tv.nrk.no/', 27 | 'https://radio.nrk.no/', 28 | 'https://nrk.no/', 29 | ] 30 | -------------------------------------------------------------------------------- /tests/plugins/test_ntv.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.ntv import NTV 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlNTV(PluginCanHandleUrl): 6 | __plugin__ = NTV 7 | 8 | should_match = [ 9 | 'https://www.ntv.ru/air/', 10 | 'http://www.ntv.ru/air/' 11 | ] 12 | 13 | should_not_match = [ 14 | 'https://www.ntv.ru/', 15 | 'http://www.ntv.ru/' 16 | ] 17 | -------------------------------------------------------------------------------- /tests/plugins/test_okru.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.okru import OKru 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlOKru(PluginCanHandleUrl): 6 | __plugin__ = OKru 7 | 8 | should_match = [ 9 | "http://ok.ru/live/12345", 10 | "https://ok.ru/live/12345", 11 | "https://m.ok.ru/live/12345", 12 | "https://mobile.ok.ru/live/12345", 13 | "https://www.ok.ru/live/12345", 14 | "https://ok.ru/video/266205792931", 15 | ] 16 | -------------------------------------------------------------------------------- /tests/plugins/test_olympicchannel.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.olympicchannel import OlympicChannel 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlOlympicChannel(PluginCanHandleUrl): 6 | __plugin__ = OlympicChannel 7 | 8 | should_match = [ 9 | "https://www.olympicchannel.com/en/video/detail/stefanidi-husband-coach-krier-relationship/", 10 | "https://www.olympicchannel.com/en/live/", 11 | "https://www.olympicchannel.com/en/live/video/detail/olympic-ceremonies-channel/", 12 | "https://www.olympicchannel.com/de/video/detail/stefanidi-husband-coach-krier-relationship/", 13 | "https://www.olympicchannel.com/de/original-series/detail/body/body-season-season-1/episodes/" 14 | "treffen-sie-aaron-wheelz-fotheringham-den-paten-des-rollstuhl-extremsports/", 15 | "https://olympics.com/en/sport-events/2021-fiba-3x3-olympic-qualifier-graz/?" 16 | "slug=final-day-fiba-3x3-olympic-qualifier-graz", 17 | "https://olympics.com/en/video/spider-woman-shauna-coxsey-great-britain-climbing-interview", 18 | "https://olympics.com/en/original-series/episode/how-fun-fuels-this-para-taekwondo-world-champion-unleash-the-new", 19 | "https://olympics.com/tokyo-2020/en/news/videos/tokyo-2020-1-message", 20 | ] 21 | 22 | should_not_match = [ 23 | "https://www.olympicchannel.com/en/", 24 | "https://www.olympics.com/en/", 25 | "https://olympics.com/tokyo-2020/en/", 26 | ] 27 | -------------------------------------------------------------------------------- /tests/plugins/test_oneplusone.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.oneplusone import OnePlusOne 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlOnePlusOne(PluginCanHandleUrl): 6 | __plugin__ = OnePlusOne 7 | 8 | should_match = [ 9 | "https://1plus1.video/ru/tvguide/plusplus/online", 10 | "https://1plus1.video/tvguide/1plus1/online", 11 | "https://1plus1.video/tvguide/2plus2/online", 12 | "https://1plus1.video/tvguide/bigudi/online", 13 | "https://1plus1.video/tvguide/plusplus/online", 14 | "https://1plus1.video/tvguide/sport/online", 15 | "https://1plus1.video/tvguide/tet/online", 16 | "https://1plus1.video/tvguide/uniantv/online", 17 | ] 18 | 19 | should_not_match = [ 20 | "https://1plus1.video/", 21 | ] 22 | -------------------------------------------------------------------------------- /tests/plugins/test_onetv.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.onetv import OneTV 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlOneTV(PluginCanHandleUrl): 6 | __plugin__ = OneTV 7 | 8 | should_match = [ 9 | "https://www.1tv.ru/live", 10 | "http://www.1tv.ru/live", 11 | "https://static.1tv.ru/eump/embeds/1tv_live_orbit-plus-4.html?muted=no", 12 | "https://static.1tv.ru/eump/pages/1tv_live.html", 13 | "https://static.1tv.ru/eump/pages/1tv_live_orbit-plus-4.html", 14 | ] 15 | 16 | should_not_match = [ 17 | "http://www.1tv.ru/some-show/some-programme-2018-03-10", 18 | "https://www.ctc.ru/online", 19 | "http://www.ctc.ru/online", 20 | "https://www.chetv.ru/online", 21 | "http://www.chetv.ru/online", 22 | "https://www.ctclove.ru/online", 23 | "http://www.ctclove.ru/online", 24 | "https://www.domashny.ru/online", 25 | "http://www.domashny.ru/online", 26 | ] 27 | -------------------------------------------------------------------------------- /tests/plugins/test_openrectv.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.openrectv import OPENRECtv 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlOPENRECtv(PluginCanHandleUrl): 6 | __plugin__ = OPENRECtv 7 | 8 | should_match = [ 9 | 'https://www.openrec.tv/live/DXRLAPSGTpx', 10 | 'https://www.openrec.tv/movie/JsDw3rAV2Rj', 11 | ] 12 | 13 | should_not_match = [ 14 | 'https://www.openrec.tv/', 15 | ] 16 | -------------------------------------------------------------------------------- /tests/plugins/test_orf_tvthek.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.orf_tvthek import ORFTVThek 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlORFTVThek(PluginCanHandleUrl): 6 | __plugin__ = ORFTVThek 7 | 8 | should_match = [ 9 | "https://tvthek.orf.at/live/Christine-Lavant-Preis-2021/14139354", 10 | "https://tvthek.orf.at/profile/Aktuell-nach-eins/13887636/Aktuell-nach-eins/14107576", 11 | ] 12 | -------------------------------------------------------------------------------- /tests/plugins/test_pandalive.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.pandalive import Pandalive 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlPandalive(PluginCanHandleUrl): 6 | __plugin__ = Pandalive 7 | 8 | should_match = [ 9 | "http://pandalive.co.kr/", 10 | "http://pandalive.co.kr/any/path", 11 | "http://www.pandalive.co.kr/", 12 | "http://www.pandalive.co.kr/any/path", 13 | "https://pandalive.co.kr/", 14 | "https://pandalive.co.kr/any/path", 15 | "https://www.pandalive.co.kr/", 16 | "https://www.pandalive.co.kr/any/path", 17 | ] 18 | -------------------------------------------------------------------------------- /tests/plugins/test_picarto.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.picarto import Picarto 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlPicarto(PluginCanHandleUrl): 6 | __plugin__ = Picarto 7 | 8 | should_match = [ 9 | 'https://picarto.tv/example', 10 | 'https://www.picarto.tv/example', 11 | 'https://www.picarto.tv/example/videos/123456', 12 | 'https://www.picarto.tv/streampopout/example/public', 13 | 'https://www.picarto.tv/videopopout/123456', 14 | ] 15 | 16 | should_not_match = [ 17 | 'https://picarto.tv/', 18 | 'https://www.picarto.tv/example/', 19 | 'https://www.picarto.tv/example/videos/abc123', 20 | 'https://www.picarto.tv/streampopout/example/notpublic', 21 | 'https://www.picarto.tv/videopopout/abc123', 22 | ] 23 | -------------------------------------------------------------------------------- /tests/plugins/test_piczel.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.piczel import Piczel 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlPiczel(PluginCanHandleUrl): 6 | __plugin__ = Piczel 7 | 8 | should_match = [ 9 | 'https://piczel.tv/watch/example', 10 | ] 11 | -------------------------------------------------------------------------------- /tests/plugins/test_pixiv.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.pixiv import Pixiv 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlPixiv(PluginCanHandleUrl): 6 | __plugin__ = Pixiv 7 | 8 | should_match = [ 9 | 'https://sketch.pixiv.net/@exampleuser', 10 | 'https://sketch.pixiv.net/@exampleuser/lives/000000000000000000', 11 | ] 12 | 13 | should_not_match = [ 14 | 'https://sketch.pixiv.net', 15 | ] 16 | -------------------------------------------------------------------------------- /tests/plugins/test_pluzz.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.pluzz import Pluzz 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlPluzz(PluginCanHandleUrl): 6 | __plugin__ = Pluzz 7 | 8 | should_match = [ 9 | "https://www.france.tv/france-2/direct.html", 10 | "https://www.france.tv/france-3/direct.html", 11 | "https://www.france.tv/france-4/direct.html", 12 | "https://www.france.tv/france-5/direct.html", 13 | "https://www.france.tv/franceinfo/direct.html", 14 | "https://www.france.tv/france-2/journal-20h00/141003-edition-du-lundi-8-mai-2017.html", 15 | "https://france3-regions.francetvinfo.fr/bourgogne-franche-comte/tv/direct/franche-comte", 16 | "https://www.francetvinfo.fr/en-direct/tv.html", 17 | "https://www.francetvinfo.fr/meteo/orages/inondations-dans-le-gard-plus-de-deux-mois-de-pluie-en-quelques-heures-des" 18 | + "-degats-mais-pas-de-victime_4771265.html" 19 | ] 20 | -------------------------------------------------------------------------------- /tests/plugins/test_qq.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.qq import QQ 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlQQ(PluginCanHandleUrl): 6 | __plugin__ = QQ 7 | 8 | should_match = [ 9 | "http://live.qq.com/10003715", 10 | "http://live.qq.com/10007266", 11 | "http://live.qq.com/10039165", 12 | "http://m.live.qq.com/10003715", 13 | "http://m.live.qq.com/10007266", 14 | "http://m.live.qq.com/10039165" 15 | ] 16 | 17 | should_match_groups = [ 18 | ("http://live.qq.com/10003715", { 19 | "room_id": "10003715" 20 | }), 21 | ("http://m.live.qq.com/10039165", { 22 | "room_id": "10039165" 23 | }), 24 | ] 25 | 26 | should_not_match = [ 27 | "http://live.qq.com/", 28 | "http://qq.com/", 29 | "http://www.qq.com/" 30 | ] 31 | -------------------------------------------------------------------------------- /tests/plugins/test_radiko.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.radiko import Radiko 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlRadiko(PluginCanHandleUrl): 6 | __plugin__ = Radiko 7 | 8 | should_match = [ 9 | 'https://radiko.jp/#!/live/QRR', 10 | 'https://radiko.jp/#!/ts/YFM/20201206010000', 11 | 'http://radiko.jp/#!/live/QRR', 12 | 'http://radiko.jp/live/QRR', 13 | 'http://radiko.jp/#!/ts/QRR/20200308180000', 14 | 'http://radiko.jp/ts/QRR/20200308180000' 15 | ] 16 | -------------------------------------------------------------------------------- /tests/plugins/test_radionet.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.radionet import RadioNet 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlRadioNet(PluginCanHandleUrl): 6 | __plugin__ = RadioNet 7 | 8 | should_match = [ 9 | "http://radioparadise.radio.net/", 10 | "http://oe1.radio.at/", 11 | "http://deutschlandfunk.radio.de/", 12 | "http://rneradionacional.radio.es/", 13 | "http://franceinfo.radio.fr/", 14 | "https://drp1bagklog.radio.dk/", 15 | "http://rairadiouno.radio.it/", 16 | "http://program1jedynka.radio.pl/", 17 | "http://rtpantena1983fm.radio.pt/", 18 | "http://sverigesp1.radio.se/", 19 | ] 20 | 21 | should_not_match = [ 22 | "http://radio.net/", 23 | "http://radio.com/", 24 | ] 25 | -------------------------------------------------------------------------------- /tests/plugins/test_raiplay.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.raiplay import RaiPlay 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlRaiPlay(PluginCanHandleUrl): 6 | __plugin__ = RaiPlay 7 | 8 | should_match = [ 9 | "http://www.raiplay.it/dirette/rai1", 10 | "http://www.raiplay.it/dirette/rai2", 11 | "http://www.raiplay.it/dirette/rai3", 12 | "http://raiplay.it/dirette/rai3", 13 | "https://raiplay.it/dirette/rai3", 14 | "http://www.raiplay.it/dirette/rainews24", 15 | "https://www.raiplay.it/dirette/rainews24", 16 | ] 17 | -------------------------------------------------------------------------------- /tests/plugins/test_reuters.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.reuters import Reuters 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlReuters(PluginCanHandleUrl): 6 | __plugin__ = Reuters 7 | 8 | should_match = [ 9 | 'https://uk.reuters.com/video/watch/east-africa-battles-locust-invasion-idOVC2J9BHJ?chan=92jv7sln', 10 | 'https://www.reuters.com/livevideo?id=Pdeb', 11 | 'https://www.reuters.com/video/watch/baby-yoda-toy-makes-its-big-debut-idOVC1KAO9Z?chan=8adtq7aq', 12 | 'https://www.reuters.tv/l/PFJx/2019/04/19/way-of-the-cross-ritual-around-notre-dame-cathedral', 13 | 'https://www.reuters.tv/l/PFcO/2019/04/10/first-ever-black-hole-image-released-astrophysics-milestone', 14 | 'https://www.reuters.tv/p/WoRwM1a00y8', 15 | ] 16 | -------------------------------------------------------------------------------- /tests/plugins/test_rtbf.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.rtbf import RTBF 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlRTBF(PluginCanHandleUrl): 6 | __plugin__ = RTBF 7 | 8 | should_match = [ 9 | "https://www.rtbf.be/auvio/direct_doc-shot?lid=122046#/", 10 | "https://www.rtbf.be/auvio/emissions/detail_dans-la-toile?id=11493", 11 | "http://www.rtbfradioplayer.be/radio/liveradio/purefm", 12 | ] 13 | 14 | should_not_match = [ 15 | "http://www.rtbf.be/", 16 | "http://www.rtbf.be/auvio", 17 | "http://www.rtbfradioplayer.be/", 18 | ] 19 | -------------------------------------------------------------------------------- /tests/plugins/test_rtpa.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.rtpa import RTPA 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlRTPA(PluginCanHandleUrl): 6 | __plugin__ = RTPA 7 | 8 | should_match = [ 9 | "https://www.rtpa.es/tpa-television", 10 | "https://www.rtpa.es/video:Ciencia%20en%2060%20segundos_551644582052.html", 11 | ] 12 | -------------------------------------------------------------------------------- /tests/plugins/test_rtvs.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.rtvs import Rtvs 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlRtvs(PluginCanHandleUrl): 6 | __plugin__ = Rtvs 7 | 8 | should_match = [ 9 | 'http://www.rtvs.sk/televizia/live-1', 10 | 'http://www.rtvs.sk/televizia/live-2', 11 | 'http://www.rtvs.sk/televizia/live-o', 12 | 'https://www.rtvs.sk/televizia/live-3', 13 | 'https://www.rtvs.sk/televizia/live-rtvs', 14 | ] 15 | 16 | should_not_match = [ 17 | 'http://www.rtvs.sk/', 18 | ] 19 | -------------------------------------------------------------------------------- /tests/plugins/test_ruv.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.ruv import Ruv 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlRuv(PluginCanHandleUrl): 6 | __plugin__ = Ruv 7 | 8 | should_match = [ 9 | "http://ruv.is/ruv", 10 | "http://ruv.is/ruv", 11 | "http://ruv.is/ruv/", 12 | "https://ruv.is/ruv/", 13 | "http://www.ruv.is/ruv", 14 | "http://www.ruv.is/ruv/", 15 | "http://ruv.is/ruv2", 16 | "http://ruv.is/ras1", 17 | "http://ruv.is/ras2", 18 | "http://ruv.is/rondo", 19 | "http://www.ruv.is/spila/ruv/ol-2018-ishokki-karla/20180217", 20 | "http://www.ruv.is/spila/ruv/frettir/20180217" 21 | ] 22 | 23 | should_not_match = [ 24 | "http://ruv.is/ruvnew", 25 | ] 26 | -------------------------------------------------------------------------------- /tests/plugins/test_sbscokr.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.sbscokr import SBScokr 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlSBScokr(PluginCanHandleUrl): 6 | __plugin__ = SBScokr 7 | 8 | should_match = [ 9 | 'https://play.sbs.co.kr/onair/pc/index.html', 10 | 'http://play.sbs.co.kr/onair/pc/index.html', 11 | ] 12 | -------------------------------------------------------------------------------- /tests/plugins/test_showroom.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.showroom import Showroom 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlShowroom(PluginCanHandleUrl): 6 | __plugin__ = Showroom 7 | 8 | should_match = [ 9 | "https://www.showroom-live.com/48_NISHIMURA_NANAKO", 10 | "https://www.showroom-live.com/room/profile?room_id=61734", 11 | "http://showroom-live.com/48_YAMAGUCHI_MAHO", 12 | "https://www.showroom-live.com/4b9581094890", 13 | "https://www.showroom-live.com/157941217780", 14 | "https://www.showroom-live.com/madokacom" 15 | ] 16 | -------------------------------------------------------------------------------- /tests/plugins/test_sportal.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.sportal import Sportal 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlSportal(PluginCanHandleUrl): 6 | __plugin__ = Sportal 7 | 8 | should_match = [ 9 | "http://sportal.bg/sportal_live_tv.php?str=15", 10 | "http://www.sportal.bg/sportal_live_tv.php?", 11 | "http://www.sportal.bg/sportal_live_tv.php?str=15", 12 | ] 13 | -------------------------------------------------------------------------------- /tests/plugins/test_sportschau.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.sportschau import Sportschau 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlSportschau(PluginCanHandleUrl): 6 | __plugin__ = Sportschau 7 | 8 | should_match = [ 9 | 'http://www.sportschau.de/wintersport/videostream-livestream---wintersport-im-ersten-242.html', 10 | 'https://www.sportschau.de/weitere/allgemein/video-kite-surf-world-tour-100.html', 11 | ] 12 | -------------------------------------------------------------------------------- /tests/plugins/test_ssh101.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.ssh101 import SSH101 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlSSH101(PluginCanHandleUrl): 6 | __plugin__ = SSH101 7 | 8 | should_match = [ 9 | "http://ssh101.com/live/sarggg", 10 | "https://ssh101.com/detail.php?id=user", 11 | "https://www.ssh101.com/live/aigaiotvlive", 12 | "https://www.ssh101.com/securelive/index.php?id=aigaiotvlive", 13 | ] 14 | 15 | should_not_match = [ 16 | "https://ssh101.com/m3u8/dyn/aigaiotvlive/index.m3u8", 17 | ] 18 | -------------------------------------------------------------------------------- /tests/plugins/test_stadium.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.stadium import Stadium 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlStadium(PluginCanHandleUrl): 6 | __plugin__ = Stadium 7 | 8 | should_match = [ 9 | "http://www.watchstadium.com/live", 10 | "https://www.watchstadium.com/live", 11 | "https://watchstadium.com/live", 12 | "http://watchstadium.com/live", 13 | "https://watchstadium.com/sport/college-football/", 14 | ] 15 | -------------------------------------------------------------------------------- /tests/plugins/test_steam.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.steam import SteamBroadcastPlugin 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlSteamBroadcastPlugin(PluginCanHandleUrl): 6 | __plugin__ = SteamBroadcastPlugin 7 | 8 | should_match = [ 9 | 'https://steamcommunity.com/broadcast/watch/12432432', 10 | 'http://steamcommunity.com/broadcast/watch/342342', 11 | 'https://steam.tv/dota2', 12 | 'http://steam.tv/dota2', 13 | ] 14 | 15 | should_not_match = [ 16 | 'http://steamcommunity.com/broadcast', 17 | 'https://steamcommunity.com', 18 | ] 19 | -------------------------------------------------------------------------------- /tests/plugins/test_streamable.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.streamable import Streamable 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlStreamable(PluginCanHandleUrl): 6 | __plugin__ = Streamable 7 | 8 | should_match = [ 9 | 'https://streamable.com/example', 10 | ] 11 | -------------------------------------------------------------------------------- /tests/plugins/test_streann.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.streann import Streann 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlStreann(PluginCanHandleUrl): 6 | __plugin__ = Streann 7 | 8 | should_match = [ 9 | # ott.streann.com 10 | "https://ott.streann.com/streaming/player.html?U2FsdGVkX1/PAwXdSYuiw+o5BxSoG10K8ShiKMDOOUEuoYiQxiZlD0gg7y+Ij07/OaI9TWk+" 11 | + "MHp40Fx4jrOv304Z+PZwLqGJgs+b0xsnfZJMpx+UmYnjys1rzTZ8UeztNjDeYEElKdaHHGkv0HFcGBGWjyWOvQuCbjGyr4dtzLyaChewqR9lNCuil/HO" 12 | + "MiL/eYtEMEPVjMdeUFilb5GSVdIyeunr+JnI1tvdPC5ow3rx3NWjbqIHd13qWVSnaZSl/UZ0BDmWBf+Vr+3pPAd1Mg3y01mKaYZywOxRduBW2HZwoLQe" 13 | + "2Lok5Z/q4aJHO02ZESqEPLRKkEqMntuuqGfy1g==", 14 | "https://ott.streann.com/s-secure/player.html?U2FsdGVkX19Z8gO/ThCsK3I1DTIdVzIGRKiJP36DEx2K1n9HeXKV4nmJrKptTZRlTmM4KTxC/" 15 | + "Mi5k3kWEsC1pr9QWmQJzKAfGdROMWB6voarQ1UQqe8IMDiibG+lcNTggCIkAS8a+99Kbe/C1W++YEP+BCBC/8Ss2RYIhNyVdqjUtqvv4Exk6l1gJDWNH" 16 | + "c6b5P51020dUrkuJCgEJCbJBE/MYFuC5xlhmzf6kcN5GlBrTuwyHYBkkVi1nvjOm1QS0iQw36UgJx9JS3DDTf7BzlAimLV5M1rXS/ME3XpllejHV0aL3" 17 | + "sghCBzc4f4AAz1IoTsl4qEamWBxyfy2kdNJRQ==", 18 | # URL with iframe 19 | "https://centroecuador.ec/tv-radio/", 20 | "https://crc.cr/estaciones/crc-89-1/", 21 | "https://columnaestilos.com/", 22 | "https://crc.cr/estaciones/azul-999/", 23 | "https://evtv.online/noticias-de-venezuela/", 24 | "https://telecuracao.com/", 25 | ] 26 | 27 | should_not_match = [ 28 | "https://ott.streann.com", 29 | ] 30 | -------------------------------------------------------------------------------- /tests/plugins/test_stv.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.stv import STV 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlSTV(PluginCanHandleUrl): 6 | __plugin__ = STV 7 | 8 | should_match = [ 9 | 'https://player.stv.tv/live', 10 | 'http://player.stv.tv/live', 11 | ] 12 | -------------------------------------------------------------------------------- /tests/plugins/test_svtplay.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.svtplay import SVTPlay 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlSVTPlay(PluginCanHandleUrl): 6 | __plugin__ = SVTPlay 7 | 8 | should_match = [ 9 | 'https://www.svtplay.se/kanaler/svt1', 10 | 'https://www.svtplay.se/kanaler/svt2?start=auto', 11 | 'https://www.svtplay.se/kanaler/svtbarn', 12 | 'https://www.svtplay.se/kanaler/kunskapskanalen?start=auto', 13 | 'https://www.svtplay.se/kanaler/svt24?start=auto', 14 | 'https://www.svtplay.se/video/27659457/den-giftbla-floden?start=auto', 15 | 'https://www.svtplay.se/video/27794015/100-vaginor', 16 | 'https://www.svtplay.se/video/28065172/motet/motet-sasong-1-det-skamtar-du-inte-om', 17 | 'https://www.svtplay.se/dokument-inifran-att-radda-ett-barn', 18 | ] 19 | 20 | should_not_match = [ 21 | 'http://www.svtflow.se/video/2020285/avsnitt-6', 22 | 'https://www.svtflow.se/video/2020285/avsnitt-6', 23 | ] 24 | -------------------------------------------------------------------------------- /tests/plugins/test_swisstxt.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.swisstxt import Swisstxt 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlSwisstxt(PluginCanHandleUrl): 6 | __plugin__ = Swisstxt 7 | 8 | should_match = [ 9 | "http://www.srf.ch/sport/resultcenter/tennis?eventId=338052", 10 | "http://live.rsi.ch/tennis.html?eventId=338052", 11 | "http://live.rsi.ch/sport.html?eventId=12345" 12 | ] 13 | 14 | should_not_match = [ 15 | # regular srgssr sites 16 | "http://srf.ch/play/tv/live", 17 | "http://www.rsi.ch/play/tv/live#?tvLiveId=livestream_La1", 18 | "http://rsi.ch/play/tv/live?tvLiveId=livestream_La1", 19 | "http://www.rtr.ch/play/tv/live", 20 | "http://rtr.ch/play/tv/live", 21 | "http://rts.ch/play/tv/direct#?tvLiveId=3608506", 22 | "http://www.srf.ch/play/tv/live#?tvLiveId=c49c1d64-9f60-0001-1c36-43c288c01a10", 23 | "http://www.rts.ch/sport/direct/8328501-tennis-open-daustralie.html", 24 | "http://www.rts.ch/play/tv/tennis/video/tennis-open-daustralie?id=8328501", 25 | ] 26 | -------------------------------------------------------------------------------- /tests/plugins/test_telefe.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.telefe import Telefe 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlTelefe(PluginCanHandleUrl): 6 | __plugin__ = Telefe 7 | 8 | should_match = [ 9 | "https://mitelefe.com/vivo", 10 | "https://mitelefe.com/vivo/", 11 | ] 12 | 13 | should_not_match = [ 14 | "http://telefe.com/pone-a-francella/temporada-1/programa-01/", 15 | "http://telefe.com/los-simuladores/temporada-1/capitulo-01/", 16 | "http://telefe.com/dulce-amor/capitulos/capitulo-01/", 17 | "http://telefe.com/", 18 | "http://www.telefeinternacional.com.ar/", 19 | "http://marketing.telefe.com/", 20 | ] 21 | -------------------------------------------------------------------------------- /tests/plugins/test_tf1.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.tf1 import TF1 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlTF1(PluginCanHandleUrl): 6 | __plugin__ = TF1 7 | 8 | should_match = [ 9 | "http://tf1.fr/tf1/direct/", 10 | "http://tf1.fr/tfx/direct/", 11 | "http://tf1.fr/tf1-series-films/direct/", 12 | "http://lci.fr/direct", 13 | "http://www.lci.fr/direct", 14 | "http://tf1.fr/tmc/direct", 15 | "http://tf1.fr/lci/direct", 16 | ] 17 | 18 | should_not_match = [ 19 | "http://tf1.fr/direct", 20 | "http://www.tf1.fr/direct", 21 | ] 22 | -------------------------------------------------------------------------------- /tests/plugins/test_trovo.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.trovo import Trovo 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlTrovo(PluginCanHandleUrl): 6 | __plugin__ = Trovo 7 | 8 | should_match_groups = [ 9 | ("https://trovo.live/s/UserName", {"user": "UserName"}), 10 | ("https://trovo.live/s/UserName/abc", {"user": "UserName"}), 11 | ("https://trovo.live/s/UserName/123", {"user": "UserName"}), 12 | ("https://trovo.live/s/UserName/123?vid=vc-456&adtag=", {"user": "UserName", "video_id": "vc-456"}), 13 | ("https://trovo.live/s/UserName/123?vid=ltv-1_2_3&adtag=", {"user": "UserName", "video_id": "ltv-1_2_3"}), 14 | ("https://www.trovo.live/s/UserName", {"user": "UserName"}), 15 | ("https://www.trovo.live/s/UserName/abc", {"user": "UserName"}), 16 | ("https://www.trovo.live/s/UserName/123", {"user": "UserName"}), 17 | ("https://www.trovo.live/s/UserName/123?vid=vc-456&adtag=", {"user": "UserName", "video_id": "vc-456"}), 18 | ("https://www.trovo.live/s/UserName/123?vid=ltv-1_2_3&adtag=", {"user": "UserName", "video_id": "ltv-1_2_3"}), 19 | ] 20 | 21 | should_not_match = [ 22 | "https://trovo.live/", 23 | "https://www.trovo.live/", 24 | "https://www.trovo.live/s/", 25 | "https://www.trovo.live/other/", 26 | ] 27 | -------------------------------------------------------------------------------- /tests/plugins/test_turkuvaz.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.turkuvaz import Turkuvaz 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlTurkuvaz(PluginCanHandleUrl): 6 | __plugin__ = Turkuvaz 7 | 8 | should_match = [ 9 | 'http://www.atv.com.tr/a2tv/canli-yayin', 10 | 'https://www.atv.com.tr/a2tv/canli-yayin', 11 | 'https://www.atv.com.tr/webtv/canli-yayin', 12 | 'http://www.a2tv.com.tr/webtv/canli-yayin', 13 | 'http://www.ahaber.com.tr/video/canli-yayin', 14 | 'https://www.ahaber.com.tr/video/canli-yayin', 15 | 'https://www.ahaber.com.tr/webtv/canli-yayin', 16 | 'https://www.aspor.com.tr/webtv/canli-yayin', 17 | 'http://www.anews.com.tr/webtv/live-broadcast', 18 | 'http://www.atvavrupa.tv/webtv/canli-yayin', 19 | 'http://www.minikacocuk.com.tr/webtv/canli-yayin', 20 | 'http://www.minikago.com.tr/webtv/canli-yayin', 21 | 'https://www.sabah.com.tr/apara/canli-yayin', 22 | ] 23 | -------------------------------------------------------------------------------- /tests/plugins/test_tv360.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.tv360 import TV360 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlTV360(PluginCanHandleUrl): 6 | __plugin__ = TV360 7 | 8 | should_match = [ 9 | 'http://tv360.com.tr/canli-yayin', 10 | ] 11 | -------------------------------------------------------------------------------- /tests/plugins/test_tv3cat.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.tv3cat import TV3Cat 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlTV3Cat(PluginCanHandleUrl): 6 | __plugin__ = TV3Cat 7 | 8 | should_match = [ 9 | 'http://ccma.cat/tv3/directe/tv3/', 10 | 'http://ccma.cat/tv3/directe/324/', 11 | 'https://ccma.cat/tv3/directe/tv3/', 12 | 'https://ccma.cat/tv3/directe/324/', 13 | 'http://www.ccma.cat/tv3/directe/tv3/', 14 | 'http://www.ccma.cat/tv3/directe/324/', 15 | 'https://www.ccma.cat/tv3/directe/tv3/', 16 | 'https://www.ccma.cat/tv3/directe/324/', 17 | ] 18 | -------------------------------------------------------------------------------- /tests/plugins/test_tv4play.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.tv4play import TV4Play 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlTV4Play(PluginCanHandleUrl): 6 | __plugin__ = TV4Play 7 | 8 | should_match = [ 9 | 'https://www.tv4play.se/program/robinson/del-26-sasong-2021/13299862', 10 | 'https://www.tv4play.se/program/sverige-mot-norge/del-1-sasong-1/12490380', 11 | 'https://www.tv4play.se/program/nyheterna/live/10378590', 12 | 'https://www.fotbollskanalen.se/video/10395484/ghoddos-fullbordar-vandningen---ger-ofk-ledningen/', 13 | ] 14 | -------------------------------------------------------------------------------- /tests/plugins/test_tv5monde.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.tv5monde import TV5Monde 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlTV5Monde(PluginCanHandleUrl): 6 | __plugin__ = TV5Monde 7 | 8 | should_match = [ 9 | "http://live.tv5monde.com/fbs.html", 10 | "https://www.tv5monde.com/emissions/episode/version-francaise-vf-83", 11 | "https://revoir.tv5monde.com/toutes-les-videos/cinema/je-ne-reve-que-de-vous", 12 | "https://revoir.tv5monde.com/toutes-les-videos/documentaires/des-russes-blancs-des-russes-blancs", 13 | "https://information.tv5monde.com/video/la-diplomatie-francaise-est-elle-en-crise", 14 | "https://afrique.tv5monde.com/videos/exclusivites-web/les-tutos-de-magloire/season-1/episode-1", 15 | "https://www.tivi5mondeplus.com/conte-nous/episode-25", 16 | ] 17 | -------------------------------------------------------------------------------- /tests/plugins/test_tv8.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.tv8 import TV8 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlTV8(PluginCanHandleUrl): 6 | __plugin__ = TV8 7 | 8 | should_match = [ 9 | 'https://www.tv8.com.tr/canli-yayin', 10 | ] 11 | -------------------------------------------------------------------------------- /tests/plugins/test_tv999.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.tv999 import TV999 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlTV999(PluginCanHandleUrl): 6 | __plugin__ = TV999 7 | 8 | should_match = [ 9 | "http://tv999.bg/live.html", 10 | "http://www.tv999.bg/live.html", 11 | "https://tv999.bg/live", 12 | "https://tv999.bg/live.html", 13 | "https://www.tv999.bg/live.html", 14 | ] 15 | 16 | should_not_match = [ 17 | "http://tv999.bg/", 18 | ] 19 | -------------------------------------------------------------------------------- /tests/plugins/test_tvibo.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.tvibo import Tvibo 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlTvibo(PluginCanHandleUrl): 6 | __plugin__ = Tvibo 7 | 8 | should_match = [ 9 | 'http://player.tvibo.com/aztv/5929820', 10 | 'http://player.tvibo.com/aztv/6858270/', 11 | 'http://player.tvibo.com/aztv/3977238/', 12 | ] 13 | -------------------------------------------------------------------------------- /tests/plugins/test_tviplayer.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.tviplayer import TVIPlayer 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlTVIPlayer(PluginCanHandleUrl): 6 | __plugin__ = TVIPlayer 7 | 8 | should_match = [ 9 | "https://tviplayer.iol.pt/direto/TVI24", 10 | "https://tviplayer.iol.pt/direto/TVI_AFRICA", 11 | "https://tviplayer.iol.pt/programa/prisioneira/5c890ae00cf2f1892ed73779/episodio/t2e219", 12 | ] 13 | 14 | should_not_match = [ 15 | "https://tviplayer.iol.pt/programas/Exclusivos", 16 | ] 17 | -------------------------------------------------------------------------------- /tests/plugins/test_tvp.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.tvp import TVP 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlTVP(PluginCanHandleUrl): 6 | __plugin__ = TVP 7 | 8 | should_match = [ 9 | 'http://tvpstream.vod.tvp.pl/?channel_id=14327511', 10 | 'http://tvpstream.vod.tvp.pl/?channel_id=1455', 11 | ] 12 | 13 | should_not_match = [ 14 | 'http://tvp.pl/', 15 | 'http://vod.tvp.pl/', 16 | ] 17 | -------------------------------------------------------------------------------- /tests/plugins/test_tvrby.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import Mock 2 | 3 | import pytest 4 | 5 | from streamlink.plugins.tvrby import TVRBy 6 | from tests.plugins import PluginCanHandleUrl 7 | 8 | 9 | class TestPluginCanHandleUrlTVRBy(PluginCanHandleUrl): 10 | __plugin__ = TVRBy 11 | 12 | should_match = [ 13 | "http://www.tvr.by/televidenie/belarus-1/", 14 | "http://www.tvr.by/televidenie/belarus-1", 15 | "http://www.tvr.by/televidenie/belarus-24/", 16 | "http://www.tvr.by/televidenie/belarus-24", 17 | ] 18 | 19 | 20 | class TestPluginTVRBy: 21 | @pytest.mark.parametrize("url,expected", [ 22 | ( 23 | "http://www.tvr.by/televidenie/belarus-1/", 24 | "http://www.tvr.by/televidenie/belarus-1/", 25 | ), 26 | ( 27 | "http://www.tvr.by/televidenie/belarus-1", 28 | "http://www.tvr.by/televidenie/belarus-1/", 29 | ), 30 | ]) 31 | def test_url_fix(self, url, expected): 32 | assert TVRBy(Mock(), url).url == expected 33 | -------------------------------------------------------------------------------- /tests/plugins/test_tvrplus.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.tvrplus import TVRPlus 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlTVRPlus(PluginCanHandleUrl): 6 | __plugin__ = TVRPlus 7 | 8 | should_match = [ 9 | "http://tvrplus.ro/live/tvr-1", 10 | "http://www.tvrplus.ro/live/tvr-1", 11 | "http://www.tvrplus.ro/live/tvr-3", 12 | "http://www.tvrplus.ro/live/tvr-international", 13 | ] 14 | 15 | should_not_match = [ 16 | "http://www.tvrplus.ro/", 17 | ] 18 | -------------------------------------------------------------------------------- /tests/plugins/test_tvtoya.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.tvtoya import TVToya 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlTVRPlus(PluginCanHandleUrl): 6 | __plugin__ = TVToya 7 | 8 | should_match = [ 9 | "http://tvtoya.pl/player/live", 10 | "https://tvtoya.pl/player/live", 11 | ] 12 | 13 | should_not_match = [ 14 | "http://tvtoya.pl", 15 | "http://tvtoya.pl/", 16 | "http://tvtoya.pl/live", 17 | "https://tvtoya.pl", 18 | "https://tvtoya.pl/", 19 | "https://tvtoya.pl/live", 20 | ] 21 | -------------------------------------------------------------------------------- /tests/plugins/test_twitcasting.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.twitcasting import TwitCasting 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlTwitCasting(PluginCanHandleUrl): 6 | __plugin__ = TwitCasting 7 | 8 | should_match = [ 9 | 'https://twitcasting.tv/c:kk1992kkkk', 10 | 'https://twitcasting.tv/icchy8591/movie/566593738', 11 | ] 12 | -------------------------------------------------------------------------------- /tests/plugins/test_useetv.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.useetv import UseeTV 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlUseeTV(PluginCanHandleUrl): 6 | __plugin__ = UseeTV 7 | 8 | should_match = [ 9 | "http://useetv.com/any", 10 | "http://useetv.com/any/path", 11 | "http://www.useetv.com/any", 12 | "http://www.useetv.com/any/path", 13 | "https://useetv.com/any", 14 | "https://useetv.com/any/path", 15 | "https://www.useetv.com/any", 16 | "https://www.useetv.com/any/path", 17 | ] 18 | -------------------------------------------------------------------------------- /tests/plugins/test_ustvnow.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from streamlink.plugins.ustvnow import USTVNow 4 | from tests.plugins import PluginCanHandleUrl 5 | 6 | 7 | class TestPluginCanHandleUrlUSTVNow(PluginCanHandleUrl): 8 | __plugin__ = USTVNow 9 | 10 | should_match = [ 11 | "http://www.ustvnow.com/live/foo/-65" 12 | ] 13 | 14 | 15 | class TestPluginUSTVNow(unittest.TestCase): 16 | def test_encrypt_data(self): 17 | key = "80035ad42d7d-bb08-7a14-f726-78403b29" 18 | iv = "3157b5680927cc4a" 19 | 20 | self.assertEqual( 21 | b"uawIc5n+TnmsmR+aP2iEDKG/eMKji6EKzjI4mE+zMhlyCbHm7K4hz7IDJDWwM3aE+Ro4ydSsgJf4ZInnoW6gqvXvG0qB" 22 | + b"/J2WJeypTSt4W124zkJpvfoJJmGAvBg2t0HT", 23 | USTVNow.encrypt_data( 24 | b'{"login_id":"test@test.com","login_key":"testtest1234","login_mode":"1","manufacturer":"123"}', 25 | key, 26 | iv 27 | ) 28 | ) 29 | 30 | def test_decrypt_data(self): 31 | key = "80035ad42d7d-bb08-7a14-f726-78403b29" 32 | iv = "3157b5680927cc4a" 33 | 34 | self.assertEqual( 35 | b'{"status":false,"error":{"code":-2,"type":"","message":"Invalid credentials.","details":{}}}', 36 | USTVNow.decrypt_data( 37 | b"KcRLETVAmHlosM0OyUd5hdTQ6WhBRTe/YRAHiLJWrzf94OLkSueXTtQ9QZ1fjOLCbpX2qteEPUWVnzvvSgVDkQmRUttN" 38 | + b"/royoxW2aL0gYQSoH1NWoDV8sIgvS5vDiQ85", 39 | key, 40 | iv 41 | ) 42 | ) 43 | -------------------------------------------------------------------------------- /tests/plugins/test_vidio.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.vidio import Vidio 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlVidio(PluginCanHandleUrl): 6 | __plugin__ = Vidio 7 | 8 | should_match = [ 9 | 'https://www.vidio.com/live/204-sctv-tv-stream', 10 | 'https://www.vidio.com/live/5075-dw-tv-stream', 11 | 'https://www.vidio.com/watch/766861-5-rekor-fantastis-zidane-bersama-real-madrid', 12 | ] 13 | 14 | should_not_match = [ 15 | 'http://www.vidio.com', 16 | 'https://www.vidio.com', 17 | ] 18 | -------------------------------------------------------------------------------- /tests/plugins/test_vimeo.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.vimeo import Vimeo 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlVimeo(PluginCanHandleUrl): 6 | __plugin__ = Vimeo 7 | 8 | should_match = [ 9 | "https://vimeo.com/237163735", 10 | "https://vimeo.com/channels/music/176894130", 11 | "https://vimeo.com/album/3706071/video/148903960", 12 | "https://vimeo.com/ondemand/surveyopenspace/92630739", 13 | "https://vimeo.com/ondemand/100footsurfingdays", 14 | "https://player.vimeo.com/video/176894130", 15 | ] 16 | 17 | should_not_match = [ 18 | "https://www.vimeo.com/" 19 | ] 20 | -------------------------------------------------------------------------------- /tests/plugins/test_vinhlongtv.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.vinhlongtv import VinhLongTV 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlVinhLongTV(PluginCanHandleUrl): 6 | __plugin__ = VinhLongTV 7 | 8 | should_match = [ 9 | 'http://thvli.vn/live/thvl1-hd/aab94d1f-44e1-4992-8633-6d46da08db42', 10 | 'http://thvli.vn/live/thvl2-hd/bc60bddb-99ac-416e-be26-eb4d0852f5cc', 11 | 'http://thvli.vn/live/phat-thanh/c87174ba-7aeb-4cb4-af95-d59de715464c', 12 | ] 13 | -------------------------------------------------------------------------------- /tests/plugins/test_vlive.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.vlive import Vlive 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlVlive(PluginCanHandleUrl): 6 | __plugin__ = Vlive 7 | 8 | should_match = [ 9 | "https://www.vlive.tv/video/156824", 10 | "https://www.vlive.tv/post/0-19740901" 11 | ] 12 | 13 | should_not_match = [ 14 | "https://www.vlive.tv/events/2019vheartbeat?lang=en", 15 | ] 16 | -------------------------------------------------------------------------------- /tests/plugins/test_vtvgo.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.vtvgo import VTVgo 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlVTVgo(PluginCanHandleUrl): 6 | __plugin__ = VTVgo 7 | 8 | should_match = [ 9 | 'https://vtvgo.vn/xem-truc-tuyen-kenh-vtv1-1.html', 10 | 'https://vtvgo.vn/xem-truc-tuyen-kenh-vtv2-2.html', 11 | 'https://vtvgo.vn/xem-truc-tuyen-kenh-vtv3-3.html', 12 | 'https://vtvgo.vn/xem-truc-tuyen-kenh-vtv4-4.html', 13 | 'https://vtvgo.vn/xem-truc-tuyen-kenh-vtv5-5.html', 14 | 'https://vtvgo.vn/xem-truc-tuyen-kenh-vtv6-6.html', 15 | 'https://vtvgo.vn/xem-truc-tuyen-kenh-vtv7-27.html', 16 | 'https://vtvgo.vn/xem-truc-tuyen-kenh-vtv8-36.html', 17 | 'https://vtvgo.vn/xem-truc-tuyen-kenh-vtv9-39.html', 18 | 'https://vtvgo.vn/xem-truc-tuyen-kenh-vtv5-t%C3%A2y-nam-b%E1%BB%99-7.html', 19 | 'https://vtvgo.vn/xem-truc-tuyen-kenh-k%C3%AAnh-vtv5-t%C3%A2y-nguy%C3%AAn-163.html', 20 | ] 21 | 22 | should_not_match = [ 23 | # POST request will error with www. 24 | 'https://www.vtvgo.vn/xem-truc-tuyen-kenh-vtv1-1.html', 25 | ] 26 | -------------------------------------------------------------------------------- /tests/plugins/test_wasd.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.wasd import WASD 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlWasd(PluginCanHandleUrl): 6 | __plugin__ = WASD 7 | 8 | should_match = [ 9 | 'https://wasd.tv/channel', 10 | 'https://wasd.tv/channel/', 11 | ] 12 | 13 | should_not_match = [ 14 | 'https://wasd.tv/channel/12345', 15 | 'https://wasd.tv/channel/12345/videos/67890', 16 | 'https://wasd.tv/voodik/videos?record=123456', 17 | ] 18 | -------------------------------------------------------------------------------- /tests/plugins/test_webtv.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.webtv import WebTV 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlWebTV(PluginCanHandleUrl): 6 | __plugin__ = WebTV 7 | 8 | should_match = [ 9 | "http://planetmutfak.web.tv", 10 | "http://telex.web.tv", 11 | "http://nasamedia.web.tv", 12 | "http://genctv.web.tv", 13 | "http://etvmanisa.web.tv", 14 | "http://startv.web.tv", 15 | "http://akuntv.web.tv", 16 | "http://telebarnn.web.tv", 17 | "http://kanal48.web.tv", 18 | "http://digi24tv.web.tv", 19 | "http://french24.web.tv", 20 | ] 21 | -------------------------------------------------------------------------------- /tests/plugins/test_wwenetwork.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.wwenetwork import WWENetwork 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlWWENetwork(PluginCanHandleUrl): 6 | __plugin__ = WWENetwork 7 | 8 | should_match = [ 9 | 'https://watch.wwe.com/in-ring/3622', 10 | ] 11 | -------------------------------------------------------------------------------- /tests/plugins/test_yupptv.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.yupptv import YuppTV 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlYuppTV(PluginCanHandleUrl): 6 | __plugin__ = YuppTV 7 | 8 | should_match = [ 9 | 'https://www.yupptv.com/channels/etv-telugu/live', 10 | 'https://www.yupptv.com/channels/india-today-news/news/25326023/15-jun-2018', 11 | ] 12 | -------------------------------------------------------------------------------- /tests/plugins/test_zdf_mediathek.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.zdf_mediathek import ZDFMediathek 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlZDFMediathek(PluginCanHandleUrl): 6 | __plugin__ = ZDFMediathek 7 | 8 | should_match = [ 9 | 'http://www.zdf.de/live-tv', 10 | 'https://www.zdf.de/sender/zdf/zdf-live-beitrag-100.html', 11 | 'https://www.zdf.de/sender/zdfneo/zdfneo-live-beitrag-100.html', 12 | 'https://www.zdf.de/sender/3sat/3sat-live-beitrag-100.html', 13 | 'https://www.zdf.de/sender/phoenix/phoenix-live-beitrag-100.html', 14 | 'https://www.zdf.de/sender/arte/arte-livestream-100.html', 15 | 'https://www.zdf.de/sender/kika/kika-live-beitrag-100.html', 16 | 'https://www.zdf.de/dokumentation/zdfinfo-doku/zdfinfo-live-beitrag-100.html', 17 | 'https://www.zdf.de/comedy/heute-show/videos/diy-hazel-habeck-100.html', 18 | 'https://www.zdf.de/nachrichten/heute-sendungen/so-wird-das-wetter-102.html', 19 | 'https://www.3sat.de/wissen/nano', 20 | ] 21 | -------------------------------------------------------------------------------- /tests/plugins/test_zeenews.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.zeenews import ZeeNews 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlZeeNews(PluginCanHandleUrl): 6 | __plugin__ = ZeeNews 7 | 8 | should_match = [ 9 | 'https://zeenews.india.com/live-tv', 10 | 'https://zeenews.india.com/live-tv/embed', 11 | ] 12 | -------------------------------------------------------------------------------- /tests/plugins/test_zengatv.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.zengatv import ZengaTV 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlZengaTV(PluginCanHandleUrl): 6 | __plugin__ = ZengaTV 7 | 8 | should_match = [ 9 | "http://www.zengatv.com/indiatoday.html", 10 | "http://www.zengatv.com/live/87021a6d-411e-11e2-b4c6-7071bccc85ac.html", 11 | "http://zengatv.com/indiatoday.html", 12 | "http://zengatv.com/live/87021a6d-411e-11e2-b4c6-7071bccc85ac.html", 13 | ] 14 | 15 | should_not_match = [ 16 | "http://www.zengatv.com", 17 | "http://www.zengatv.com/" 18 | ] 19 | -------------------------------------------------------------------------------- /tests/plugins/test_zhanqi.py: -------------------------------------------------------------------------------- 1 | from streamlink.plugins.zhanqi import Zhanqitv 2 | from tests.plugins import PluginCanHandleUrl 3 | 4 | 5 | class TestPluginCanHandleUrlZhanqitv(PluginCanHandleUrl): 6 | __plugin__ = Zhanqitv 7 | 8 | should_match = [ 9 | 'https://www.zhanqi.tv/lpl', 10 | ] 11 | -------------------------------------------------------------------------------- /tests/resources/__init__.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | import os.path 3 | from contextlib import contextmanager 4 | from io import BytesIO 5 | 6 | from lxml.etree import iterparse 7 | 8 | 9 | __here__ = os.path.abspath(os.path.dirname(__file__)) 10 | 11 | 12 | def _parse_xml(data, strip_ns=False): 13 | data = bytes(data, "utf8") 14 | try: 15 | it = iterparse(BytesIO(data)) 16 | for _, el in it: 17 | if '}' in el.tag and strip_ns: # pragma: no branch 18 | # strip all namespaces 19 | el.tag = el.tag.split('}', 1)[1] 20 | return it.root 21 | except Exception as err: # pragma: no cover 22 | snippet = repr(data) 23 | if len(snippet) > 35: 24 | snippet = f"{snippet[:35]} ..." 25 | 26 | raise ValueError("Unable to parse XML: {0} ({1})".format(err, snippet)) 27 | 28 | 29 | @contextmanager 30 | def text(path, encoding="utf8"): 31 | with codecs.open(os.path.join(__here__, path), 'r', encoding=encoding) as resource_fh: 32 | yield resource_fh 33 | 34 | 35 | @contextmanager 36 | def xml(path, encoding="utf8"): 37 | with codecs.open(os.path.join(__here__, path), 'r', encoding=encoding) as resource_fh: 38 | yield _parse_xml(resource_fh.read(), strip_ns=True) 39 | -------------------------------------------------------------------------------- /tests/resources/cli/config/custom: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunghome/streamlink_optionalkey/63c387cb8d088a6a41cfd128348aa237329a0f5c/tests/resources/cli/config/custom -------------------------------------------------------------------------------- /tests/resources/cli/config/primary: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunghome/streamlink_optionalkey/63c387cb8d088a6a41cfd128348aa237329a0f5c/tests/resources/cli/config/primary -------------------------------------------------------------------------------- /tests/resources/cli/config/primary.testplugin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunghome/streamlink_optionalkey/63c387cb8d088a6a41cfd128348aa237329a0f5c/tests/resources/cli/config/primary.testplugin -------------------------------------------------------------------------------- /tests/resources/cli/config/secondary: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunghome/streamlink_optionalkey/63c387cb8d088a6a41cfd128348aa237329a0f5c/tests/resources/cli/config/secondary -------------------------------------------------------------------------------- /tests/resources/cli/config/secondary.testplugin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunghome/streamlink_optionalkey/63c387cb8d088a6a41cfd128348aa237329a0f5c/tests/resources/cli/config/secondary.testplugin -------------------------------------------------------------------------------- /tests/resources/dash/test_6_p1.mpd: -------------------------------------------------------------------------------- 1 | 2 | 14 | http://test.se/ 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/resources/dash/test_6_p2.mpd: -------------------------------------------------------------------------------- 1 | 2 | 14 | http://test.se/ 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/resources/hls/test_2.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-INDEPENDENT-SEGMENTS 3 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",LANGUAGE="en",NAME="English",AUTOSELECT=YES,DEFAULT=YES 4 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="English",LANGUAGE="en",AUTOSELECT=NO,URI="en.m3u8" 5 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="Spanish",LANGUAGE="es",AUTOSELECT=NO,URI="es.m3u8" 6 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="chunked",NAME="video",AUTOSELECT=YES,DEFAULT=YES 7 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=3982010,RESOLUTION=1920x1080,CODECS="avc1.4D4029,mp4a.40.2",VIDEO="chunked", AUDIO="aac" 8 | playlist.m3u8 -------------------------------------------------------------------------------- /tests/resources/hls/test_date.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | 3 | #EXT-X-TARGETDURATION:120 4 | 5 | #EXT-X-DATERANGE:ID="start-invalid",START-DATE="invalid" 6 | #EXT-X-DATERANGE:ID="start-no-frac",START-DATE="2000-01-01T00:00:00Z" 7 | #EXT-X-DATERANGE:ID="start-with-frac",START-DATE="2000-01-01T00:00:00.000Z" 8 | 9 | #EXT-X-DATERANGE:ID="with-class",CLASS="bar",START-DATE="2000-01-01T00:00:00.000Z" 10 | 11 | #EXT-X-DATERANGE:ID="duration",START-DATE="2000-01-01T00:00:00.000Z",DURATION=30.5 12 | #EXT-X-DATERANGE:ID="planned-duration",START-DATE="2000-01-01T00:00:00.000Z",PLANNED-DURATION=15 13 | #EXT-X-DATERANGE:ID="duration-precedence",START-DATE="2000-01-01T00:00:00.000Z",DURATION=30.5,PLANNED-DURATION=15 14 | #EXT-X-DATERANGE:ID="end",START-DATE="2000-01-01T00:00:00.000Z",END-DATE="2000-01-01T00:01:00.000Z" 15 | #EXT-X-DATERANGE:ID="end-precedence",START-DATE="2000-01-01T00:00:00.000Z",END-DATE="2000-01-01T00:01:00.000Z",DURATION=30.5 16 | 17 | #EXT-X-DATERANGE:X-CUSTOM="value" 18 | 19 | #EXT-X-PROGRAM-DATE-TIME:2000-01-01T00:00:00.000Z 20 | #EXTINF:15.000,live 21 | segment0-15.ts 22 | 23 | #EXT-X-PROGRAM-DATE-TIME:2000-01-01T00:00:15.000Z 24 | #EXTINF:15.500,live 25 | segment15-30.5.ts 26 | 27 | #EXT-X-PROGRAM-DATE-TIME:2000-01-01T00:00:30.500Z 28 | #EXTINF:29.500,live 29 | segment30.5-60.ts 30 | 31 | #EXT-X-PROGRAM-DATE-TIME:2000-01-01T00:01:00.000Z 32 | #EXTINF:60.000,live 33 | segment60-.ts 34 | -------------------------------------------------------------------------------- /tests/resources/hls/test_master.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="720p30",NAME="720p",AUTOSELECT=YES,DEFAULT=YES 3 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2299652,RESOLUTION=1280x720,CODECS="avc1.77.31,mp4a.40.2",VIDEO="720p30" 4 | 720p.m3u8 5 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="720p30",NAME="720p",AUTOSELECT=YES,DEFAULT=YES 6 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2299652,RESOLUTION=1280x720,CODECS="avc1.77.31,mp4a.40.2",VIDEO="720p30" 7 | 720p_alt.m3u8 8 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="480p30",NAME="480p",AUTOSELECT=YES,DEFAULT=YES 9 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1354652,RESOLUTION=852x480,CODECS="avc1.77.31,mp4a.40.2",VIDEO="480p30" 10 | 480p.m3u8 11 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="360p30",NAME="360p",AUTOSELECT=YES,DEFAULT=YES 12 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=630000,RESOLUTION=640x360,CODECS="avc1.77.31,mp4a.40.2",VIDEO="360p30" 13 | 360p.m3u8 14 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="160p30",NAME="160p",AUTOSELECT=YES,DEFAULT=YES 15 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=230000,RESOLUTION=284x160,CODECS="avc1.77.31,mp4a.40.2",VIDEO="160p30" 16 | 160p.m3u8 17 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="chunked",NAME="1080p (source)",AUTOSELECT=YES,DEFAULT=YES 18 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=3982010,RESOLUTION=1920x1080,CODECS="avc1.4D4029,mp4a.40.2",VIDEO="chunked" 19 | playlist.m3u8 20 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio_only",NAME="audio_only",AUTOSELECT=YES,DEFAULT=NO,LANGUAGE="en" 21 | #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=90145,CODECS="mp4a.40.2",VIDEO="audio_only" 22 | audio_only.m3u8 -------------------------------------------------------------------------------- /tests/resources/hls/test_master_twitch_vod.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="chunked",NAME="Source",AUTOSELECT=YES,DEFAULT=YES 3 | #EXT-X-STREAM-INF:BANDWIDTH=2830316,CODECS="avc1.64002A,mp4a.40.2",RESOLUTION=1920x1080,VIDEO="chunked" 4 | source.m3u8 5 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="720p60",NAME="720p60",AUTOSELECT=YES,DEFAULT=YES 6 | #EXT-X-STREAM-INF:BANDWIDTH=3070556,CODECS="avc1.4D401F,mp4a.40.2",RESOLUTION=1280x720,VIDEO="720p60" 7 | 720p60.m3u8 8 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="720p30",NAME="720p30",AUTOSELECT=YES,DEFAULT=YES 9 | #EXT-X-STREAM-INF:BANDWIDTH=2166929,CODECS="avc1.4D401F,mp4a.40.2",RESOLUTION=1280x720,VIDEO="720p30" 10 | 720p30.m3u8 11 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="480p30",NAME="480p30",AUTOSELECT=YES,DEFAULT=YES 12 | #EXT-X-STREAM-INF:BANDWIDTH=1417102,CODECS="avc1.4D401E,mp4a.40.2",RESOLUTION=852x480,VIDEO="480p30" 13 | 480p30.m3u8 14 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="audio_only",NAME="Audio Only",AUTOSELECT=NO,DEFAULT=NO 15 | #EXT-X-STREAM-INF:BANDWIDTH=216931,CODECS="mp4a.40.2",VIDEO="audio_only" 16 | audio_only.m3u8 17 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="360p30",NAME="360p30",AUTOSELECT=YES,DEFAULT=YES 18 | #EXT-X-STREAM-INF:BANDWIDTH=694948,CODECS="avc1.4D401E,mp4a.40.2",RESOLUTION=640x360,VIDEO="360p30" 19 | 360p30.m3u8 20 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="160p30",NAME="160p30",AUTOSELECT=YES,DEFAULT=YES 21 | #EXT-X-STREAM-INF:BANDWIDTH=285241,CODECS="avc1.4D400C,mp4a.40.2",RESOLUTION=284x160,VIDEO="160p30" 22 | 160p30.m3u8 23 | -------------------------------------------------------------------------------- /tests/stream/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunghome/streamlink_optionalkey/63c387cb8d088a6a41cfd128348aa237329a0f5c/tests/stream/__init__.py -------------------------------------------------------------------------------- /tests/stream/test_file.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import Mock, mock_open, patch 3 | 4 | from streamlink import Streamlink 5 | from streamlink.stream.file import FileStream 6 | 7 | 8 | class TestFileStream(unittest.TestCase): 9 | def setUp(self): 10 | self.session = Streamlink() 11 | 12 | def test_open_file_path(self): 13 | m = mock_open() 14 | s = FileStream(self.session, path="/test/path") 15 | with patch('streamlink.stream.file.open', m, create=True): 16 | s.open() 17 | m.assert_called_with("/test/path") 18 | 19 | def test_open_fileobj(self): 20 | fileobj = Mock() 21 | s = FileStream(self.session, fileobj=fileobj) 22 | self.assertEqual(fileobj, s.open()) 23 | -------------------------------------------------------------------------------- /tests/stream/test_stream_wrappers.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from streamlink.stream.wrappers import StreamIOIterWrapper 4 | 5 | 6 | class TestPluginStream(unittest.TestCase): 7 | def test_iter(self): 8 | def generator(): 9 | yield b"1" * 8192 10 | yield b"2" * 4096 11 | yield b"3" * 2048 12 | 13 | fd = StreamIOIterWrapper(generator()) 14 | self.assertEqual(fd.read(4096), b"1" * 4096) 15 | self.assertEqual(fd.read(2048), b"1" * 2048) 16 | self.assertEqual(fd.read(2048), b"1" * 2048) 17 | self.assertEqual(fd.read(1), b"2") 18 | self.assertEqual(fd.read(4095), b"2" * 4095) 19 | self.assertEqual(fd.read(1536), b"3" * 1536) 20 | self.assertEqual(fd.read(), b"3" * 512) 21 | -------------------------------------------------------------------------------- /tests/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sunghome/streamlink_optionalkey/63c387cb8d088a6a41cfd128348aa237329a0f5c/tests/utils/__init__.py -------------------------------------------------------------------------------- /tests/utils/test_cache.py: -------------------------------------------------------------------------------- 1 | from streamlink.utils.cache import LRUCache 2 | 3 | 4 | def test_lru_cache(): 5 | cache = LRUCache(num=3) 6 | assert cache.get("foo") is None, "Getter returns None for unknown items" 7 | 8 | cache.set("foo", "FOO") 9 | assert list(cache.cache.items()) == [("foo", "FOO")], "Setter adds new items" 10 | 11 | assert cache.get("foo") == "FOO", "Getter returns correct value of known items" 12 | 13 | cache.set("bar", "BAR") 14 | cache.set("baz", "BAZ") 15 | cache.set("qux", "QUX") 16 | assert list(cache.cache.items()) == [("bar", "BAR"), ("baz", "BAZ"), ("qux", "QUX")], "Setter respects max queue size" 17 | 18 | cache.get("bar") 19 | assert list(cache.cache.items()) == [("baz", "BAZ"), ("qux", "QUX"), ("bar", "BAR")], "Getter moves known items to the end" 20 | 21 | cache.get("unknown") 22 | assert list(cache.cache.items()) == [("baz", "BAZ"), ("qux", "QUX"), ("bar", "BAR")], "Getter keeps order on unknown items" 23 | 24 | cache.set("foo", "FOO") 25 | assert list(cache.cache.items()) == [("qux", "QUX"), ("bar", "BAR"), ("foo", "FOO")], "Setter moves new items to the end" 26 | 27 | cache.set("qux", "QUUX") 28 | assert list(cache.cache.items()) == [("bar", "BAR"), ("foo", "FOO"), ("qux", "QUUX")], "Setter moves known items to the end" 29 | -------------------------------------------------------------------------------- /tests/utils/test_crypto.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import unittest 3 | 4 | from streamlink.utils.crypto import decrypt_openssl, evp_bytestokey 5 | 6 | 7 | class TestUtil(unittest.TestCase): 8 | def test_evp_bytestokey(self): 9 | self.assertEqual((b']A@*\xbcK*v\xb9q\x9d\x91\x10\x17\xc5\x92', 10 | b'(\xb4n\xd3\xc1\x11\xe8Q\x02\x90\x9b\x1c\xfbP\xea\x0f'), 11 | evp_bytestokey(b"hello", b"", 16, 16)) 12 | 13 | def test_decrpyt(self): 14 | # data generated with: 15 | # echo "this is a test" | openssl enc -aes-256-cbc -pass pass:"streamlink" -base64 16 | data = base64.b64decode("U2FsdGVkX18nVyJ6Y+ksOASMSHKuRoQ9b4DKHuPbyQc=") 17 | self.assertEqual( 18 | b"this is a test\n", 19 | decrypt_openssl(data, b"streamlink") 20 | ) 21 | -------------------------------------------------------------------------------- /tests/utils/test_data.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from streamlink.utils.data import search_dict 4 | 5 | 6 | class TestUtilsData(unittest.TestCase): 7 | def test_search_dict(self): 8 | self.assertSequenceEqual( 9 | list(search_dict(["one", "two"], "one")), 10 | [] 11 | ) 12 | self.assertSequenceEqual( 13 | list(search_dict({"two": "test2"}, "one")), 14 | [] 15 | ) 16 | self.assertSequenceEqual( 17 | list(search_dict({"one": "test1", "two": "test2"}, "one")), 18 | ["test1"] 19 | ) 20 | self.assertSequenceEqual( 21 | list(search_dict({"one": {"inner": "test1"}, "two": "test2"}, "inner")), 22 | ["test1"] 23 | ) 24 | self.assertSequenceEqual( 25 | list(search_dict({"one": [{"inner": "test1"}], "two": "test2"}, "inner")), 26 | ["test1"] 27 | ) 28 | self.assertSequenceEqual( 29 | list(sorted(search_dict({"one": [{"inner": "test1"}], "two": {"inner": "test2"}}, "inner"))), 30 | list(sorted(["test1", "test2"])) 31 | ) 32 | -------------------------------------------------------------------------------- /tests/utils/test_module.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | import sys 3 | import unittest 4 | 5 | from streamlink.utils.module import load_module 6 | 7 | # used in the import test to verify that this module was imported 8 | __test_marker__ = "test_marker" 9 | 10 | 11 | class TestUtilsModule(unittest.TestCase): 12 | def test_load_module_non_existent(self): 13 | self.assertRaises(ImportError, load_module, "non_existent_module", os.path.dirname(__file__)) 14 | 15 | def test_load_module(self): 16 | self.assertEqual( 17 | sys.modules[__name__].__test_marker__, 18 | load_module(__name__.split(".")[-1], os.path.dirname(__file__)).__test_marker__ 19 | ) 20 | --------------------------------------------------------------------------------