././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643136177.0728405 streamlink-3.1.1/0000755000175100001710000000000000000000000013225 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/AUTHORS0000644000175100001710000000033500000000000014276 0ustar00runnerdockerThank you to everyone who has contributed to streamlink / livestreamer. For a list of contributors, see https://github.com/streamlink/streamlink/graphs/contributors or use the command `git log --format='%aN' | sort -uf` ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/CHANGELOG.md0000644000175100001710000051757300000000000015060 0ustar00runnerdocker# Changelog ## streamlink 3.1.1 (2022-01-25) Patch release: - Fixed: broken `streamlink.exe`/`streamlinkw.exe` executables in Windows installer ([#4308](https://github.com/streamlink/streamlink/pull/4308)) ```text Mozi <29089388+pzhlkj6612@users.noreply.github.com> (1): cli: tell users the stream could be saved or piped back-to (1): plugins.twitcasting: Fix error messages bastimeyer (1): installer: set pynsist to 2.7 and distlib to 0.3.3 ``` ## streamlink 3.1.0 (2022-01-22) Release highlights: - Changed: file overwrite prompt to wait for user input before opening streams ([#4252](https://github.com/streamlink/streamlink/pull/4252)) - Fixed: log messages appearing in `--json` output ([#4258](https://github.com/streamlink/streamlink/pull/4258)) - Fixed: keep-alive TCP connections when filtering out HLS segments ([#4229](https://github.com/streamlink/streamlink/pull/4229)) - Fixed: sort order of DASH streams with the same video resolution ([#4220](https://github.com/streamlink/streamlink/pull/4220)) - Fixed: HLS segment byterange offsets ([#4301](https://github.com/streamlink/streamlink/pull/4301), [#4302](https://github.com/streamlink/streamlink/pull/4302)) - Fixed: YouTube /live URLs ([#4222](https://github.com/streamlink/streamlink/pull/4222)) - Fixed: UStream websocket address ([#4238](https://github.com/streamlink/streamlink/pull/4238)) - Fixed: Pluto desync issues by filtering out bumper segments ([#4255](https://github.com/streamlink/streamlink/pull/4255)) - Fixed: various plugin issues - please see the changelog down below - Removed plugins: abweb ([#4270](https://github.com/streamlink/streamlink/pull/4270)), latina ([#4269](https://github.com/streamlink/streamlink/pull/4269)), live_russia_tv ([#4263](https://github.com/streamlink/streamlink/pull/4263)), liveme ([#4264](https://github.com/streamlink/streamlink/pull/4264)) ```text Christian Kündig (1): plugins.yupptv: override encoding, set Origin header (#4261) Ian Cameron <1661072+mkbloke@users.noreply.github.com> (4): plugins.pluto: rewrite/fix plugins.albavision: fix/update plugins.albavision: update plugin_matrix.rst plugins.pluto: add filtering of bumper segments PleasantMachine9 <65126927+PleasantMachine9@users.noreply.github.com> (1): stream.hls: read and discard filtered sequences properly back-to (8): stream.dash: sort video duplicated resolutions by bandwidth plugins.onetv: added support for channel with different timezone +4 plugins.ceskatelevize: Fix Livestreams plugins.mediavitrina: better support for different channel names plugins.live_russia_tv: removed outdated plugin plugins.liveme: removed plugins.abweb: removed plugins.dogus: update and cleanup bastimeyer (21): plugins.youtube: fix metadata on /live URLs plugins.ustreamtv: fix websocket address plugins.steam: refactor plugin plugins.stadium: rewrite cli: create file output before opening the stream logger: change NONE loglevel to sys.maxsize cli.console: ignore msg() calls if json=True tests: fix named pipe being created in CLI tests plugins.vtvgo: remove itertags plugins.vk: rewrite and remove itertags plugins.latina: remove plugin plugins.streann: remove itertags plugins.nos: remove itertags tests: rewrite plugins_meta tests 2022 plugins.foxtr: fix regex plugins.delfi: rewrite plugin plugins.twitch: fix pluginmatcher regex docs: fix linux package infos stream.hls: fix byterange parser stream.hls: refactor segment byterange calculation zappepappe (1): plugins.svtplay: fix live channel URL matching (#4219) ``` ## streamlink 3.0.3 (2021-11-27) Patch release: - Fixed: broken output of the `--help` CLI argument ([#4213](https://github.com/streamlink/streamlink/pull/4213)) - Fixed: parsing of invalid HTML5 documents ([#4210](https://github.com/streamlink/streamlink/pull/4210)) Please see the [changelog of 3.0.0](https://streamlink.github.io/changelog.html#streamlink-3-0-0-2021-11-17), as it contains breaking changes that may require user interaction. ```text bastimeyer (3): utils.parse: parse invalid XHTML5 documents cli: prioritize --help and fix its output plugins.youtube: add category metadata ``` ## streamlink 3.0.2 (2021-11-25) Patch release: - Added: support for the `id` plugin metadata property ([#4203](https://github.com/streamlink/streamlink/pull/4203)) - Updated: Twitch access token request parameter regarding embedded ads ([#4194](https://github.com/streamlink/streamlink/pull/4194)) - Fixed: early `SIGINT`/`SIGTERM` signal handling ([#4190](https://github.com/streamlink/streamlink/pull/4190)) - Fixed: broken character set decoding when parsing HTML documents ([#4201](https://github.com/streamlink/streamlink/pull/4201)) - Fixed: missing home directory expansion (tilde character) in file output paths ([#4204](https://github.com/streamlink/streamlink/pull/4204)) - New plugin: tviplayer ([#4199](https://github.com/streamlink/streamlink/pull/4199)) ```text back-to (1): plugins.tviplayer: new plugin bastimeyer (14): cli: override default signal handlers chore: add GH gist link to issue templates plugins.twitch: set playerType back to embed plugins.twitch: add type annotations plugins.twitch: avg duration for prefetch segments plugins.ard_mediathek: rewrite plugin utils.parse: fix encoding in parse_html plugins.ard_mediathek: fix plugin cli: expand user in file output paths cli.output: remove MPV title variable escape logic plugin: add 'id' metadata property plugins.youtube: add 'id' metadata plugins.twitch: add 'id' metadata docs: add dedicated metadata variables section kyldery (1): plugins.crunchyroll: add metadata attributes (#4185) ``` ## streamlink 3.0.1 (2021-11-17) Patch release: - Fixed: broken pycountry import in Windows installer's Python environment ([#4180](https://github.com/streamlink/streamlink/pull/4180)) ```text bastimeyer (1): installer: rewrite wheels config, fix pycountry ``` ## streamlink 3.0.0 (2021-11-17) Breaking changes: - BREAKING: dropped support for RTMP, HDS and AkamaiHD streams ([#4169](https://github.com/streamlink/streamlink/pull/4169), [#4168](https://github.com/streamlink/streamlink/pull/4168)) - removed the `rtmp://`, `hds://` and `akamaihd://` protocol plugins - removed all Flash related code - upgraded all plugins using these old streaming protocols - dropped RTMPDump dependency - BREAKING: removed the following CLI arguments (and respective session options): ([#4169](https://github.com/streamlink/streamlink/pull/4169), [#4168](https://github.com/streamlink/streamlink/pull/4168)) - `--rtmp-rtmpdump`, `--rtmpdump`, `--rtmp-proxy`, `--rtmp-timeout` Users of Streamlink's Windows installer will need to update their [config file](https://streamlink.github.io/cli.html#configuration-file). - `--subprocess-cmdline`, `--subprocess-errorlog`, `--subprocess-errorlog-path` - `--hds-live-edge`, `--hds-segment-attempts`, `--hds-segment-threads`, `--hds-segment-timeout`, `--hds-timeout` - BREAKING: switched from HTTP to HTTPS for all kinds of scheme-less input URLs. If a site or http-proxy doesn't support HTTPS, then HTTP needs to be set explicitly. ([#4068](https://github.com/streamlink/streamlink/pull/4068), [#4053](https://github.com/streamlink/streamlink/pull/4053)) - BREAKING/API: changed `Session.resolve_url()` and `Session.resolve_url_no_redirect()` to return a tuple of a plugin class and the resolved URL instead of an initialized plugin class instance. This fixes the availability of plugin options in a plugin's constructor. ([#4163](https://github.com/streamlink/streamlink/pull/4163)) - BREAKING/requirements: dropped alternative dependency `pycrypto` and removed the `STREAMLINK_USE_PYCRYPTO` env var switch ([#4174](https://github.com/streamlink/streamlink/pull/4174)) - BREAKING/requirements: switched from `iso-639`+`iso3166` to `pycountry` and removed the `STREAMLINK_USE_PYCOUNTRY` env var switch ([#4175](https://github.com/streamlink/streamlink/pull/4175)) - BREAKING/setup: disabled unsupported Python versions, disabled the deprecated `test` setuptools command, removed the `NO_DEPS` env var, and switched to declarative package data via `setup.cfg` ([#4079](https://github.com/streamlink/streamlink/pull/4079), [#4107](https://github.com/streamlink/streamlink/pull/4107), [#4115](https://github.com/streamlink/streamlink/pull/4115), [#4113](https://github.com/streamlink/streamlink/pull/4113)) Release highlights: - Deprecated: `--https-proxy` in favor of a single `--http-proxy` CLI argument (and respective session option). Both now set the same proxy for all HTTPS/HTTP requests and websocket connections. [`--https-proxy` will be removed in a future release.](https://streamlink.github.io/deprecations.html#streamlink-3-0-0) ([#4120](https://github.com/streamlink/streamlink/pull/4120)) - Added: official support for Python 3.10 ([#4144](https://github.com/streamlink/streamlink/pull/4144)) - Added: `--twitch-api-header` for only setting Twitch.tv API requests headers (for authentication, etc.) as an alternative to `--http-header` ([#4156](https://github.com/streamlink/streamlink/pull/4156)) - Added: BASH and ZSH completions to sdist tarball and wheels. ([#4048](https://github.com/streamlink/streamlink/pull/4048), [#4178](https://github.com/streamlink/streamlink/pull/4178)) - Added: support for creating parent directories via metadata variables in file output paths ([#4085](https://github.com/streamlink/streamlink/pull/4085)) - Added: new WebsocketClient implementation ([#4153](https://github.com/streamlink/streamlink/pull/4153)) - Updated: plugins using websocket connections - nicolive, ustreamtv, twitcasting ([#4155](https://github.com/streamlink/streamlink/pull/4155), [#4164](https://github.com/streamlink/streamlink/pull/4164), [#4154](https://github.com/streamlink/streamlink/pull/4154)) - Updated: circumvention for YouTube's age verification ([#4058](https://github.com/streamlink/streamlink/pull/4058)) - Updated: and fixed lots of other plugins, see the detailed changelog below - Reverted: HLS segment downloads always being streamed, and added back `--hls-segment-stream-data` to prevent connection issues ([#4159](https://github.com/streamlink/streamlink/pull/4159)) - Fixed: URL percent-encoding for sites which require the lowercase format ([#4003](https://github.com/streamlink/streamlink/pull/4003)) - Fixed: XML parsing issues ([#4075](https://github.com/streamlink/streamlink/pull/4075)) - Fixed: broken `method` parameter when using the `httpstream://` protocol plugin ([#4171](https://github.com/streamlink/streamlink/pull/4171)) - Fixed: test failures when the `brotli` package is installed ([#4022](https://github.com/streamlink/streamlink/pull/4022)) - Requirements: bumped `lxml` to `>4.6.4,<5.0` and `websocket-client` to `>=1.2.1,<2.0` ([#4143](https://github.com/streamlink/streamlink/pull/4143), [#4153](https://github.com/streamlink/streamlink/pull/4153)) - Windows installer: upgraded Python to `3.9.8` and FFmpeg to `n4.4.1` ([#4176](https://github.com/streamlink/streamlink/pull/4176), [#4124](https://github.com/streamlink/streamlink/pull/4124)) - Documentation: upgraded to first stable version of the Furo theme ([#4000](https://github.com/streamlink/streamlink/pull/4000)) - New plugins: pandalive ([#4064](https://github.com/streamlink/streamlink/pull/4064)) - Removed plugins: tga ([#4129](https://github.com/streamlink/streamlink/pull/4129)), viasat ([#4087](https://github.com/streamlink/streamlink/pull/4087)), viutv ([#4018](https://github.com/streamlink/streamlink/pull/4018)), webcast_india_gov ([#4024](https://github.com/streamlink/streamlink/pull/4024)) ```text Ian Cameron <1661072+mkbloke@users.noreply.github.com> (4): plugins.bbciplayer: remove HDSStream, upgrade scheme (#4041) plugins.pandalive: new plugin plugins.facebook: update onion address plugins.picarto: update URL regex and logic MinePlayersPE (1): plugins.youtube: better API age-gate bypassing (#4058) back-to (14): ci: temporary windows python 3.10 fix for missing `lxml 4.6.3` wheel stream.hls: Fix error msg for 'Unable to decrypt cipher ...' plugins.viutv: removed plugins.webcast_india_gov: removed plugins.oneplusone: cleanup and add auto session reload (#4049) plugins.showroom: cleanup (#4065) plugins.tv999: use parse_html plugins.ssh101: use parse_html plugins.app17: remove RTMPStream, cleanup plugins.viasat: removed plugins.twitch: add device-id headers (#4086) plugin.api: update useragents plugins.twitch: new plugin command --twitch-api-header plugins.goltelevision: fix api url and update plugin url bastimeyer (70): docs: fix CLI argument example in manpage docs: bump furo docs req to 2021.09.08 http_session: override urllib3 percent-encoding installer: upgrade python from 3.9.6 to 3.9.7 tests: fix typo in pytest skipif marker tests: fix deprecated module imports on py310 plugins.ardlive: rewrite plugin utils: replace LazyFormatter with new Formatter utils: move all URL methods to utils.url tests: fix Accept-Encoding headers in stream_json plugins.pluzz: rewrite plugin plugins: clean up imports of parse_* utils utils: split into submodules and fix imports plugins.artetv: rewrite plugin using v2 API plugins.bloomberg: rewrite plugin stream: clean up imports tests: move tests/streams to tests/stream plugins.earthcam: rewrite plugin, remove rtmp build: include bash and zsh completions in wheels plugins.picarto: fix HLS URL hostname utils.url: make update_scheme always update target plugins: fix update_scheme calls plugins.bfmtv: rewrite plugin using XPath plugins.youtube: replace itertags with XPath tests: fix partial coverage in can_handle_url session: don't override https-proxy scheme session: move from http to https as default scheme plugins.brightcove: rewrite plugin ci.github: add regular py310 test runners utils.parse: fix ignore_ns in parse_xml script: fix update-removed-plugins bash script plugins.tv5monde: remove plugin plugins.tv5monde: re-implement plugin setup: show error on older python versions cli: refactor FileOutput and Formatter plugin.api: remove StreamMapper plugins.okru: rewrite plugin, drop RTMP ci.github: switch to codecov-action@v2 setup: disable test command docs: fix Solus package link plugins.twitch: remove device-id headers installer: remove unneeded 3rd party license texts setup: switch to declarative package metadata setup: remove NO_DEPS env var plugin: trim metadata strings plugins.brightcove: add more HLS source types installer: bump ffmpeg to n4.4.1 plugins.tga: remove plugin vendor: bump lxml to >4.6.4,<5.0 setup: add Python 3.10 to classifiers list ci.github: check for unicode bidi control chars installer: bump lxml to 4.6.4 logger: fix warning import and trace export plugin.api: implement WebsocketClient plugins.twitcasting: re-implement websocket client plugins.nicolive: re-implement plugin revert: stream.hls: remove hls-segment-stream-data option plugin.api.websocket: add reconnect method plugins.ustreamtv: re-implement plugin session.resolve_url: return plugin class + URL cli.main: add plugin type annotations plugins.twitch: refactor api-headers streams: remove HDS/AkamaiHD and flashmedia pkg stream: remove RTMP and RTMPDump dependency plugins.rtmp: add to removed plugins list stream.http: fix custom method argument setup: drop pycrypto support setup: drop iso-639/iso3166, default to pycountry installer: upgrade python from 3.9.7 to 3.9.8 setup: include shell completions in sdist beardypig (2): cli: deprecate the --https-proxy option as well as the Session options plugins.ltv_lsm_lv: update the plugin for the new page layout nnrm <91910832+nnrm@users.noreply.github.com> (1): plugins.nicolive: add support for community urls vinyl-umbrella <61788251+vinyl-umbrella@users.noreply.github.com> (1): plugins.openrectv: be able to get subscription video (#4130) ``` ## streamlink 2.4.0 (2021-09-07) Release highlights: - Deprecated: stream-type specific stream transport options in favor of generic options ([#3893](https://github.com/streamlink/streamlink/pull/3893)) - use `--stream-segment-attempts` instead of `--{dash,hds,hls}-segment-attempts` - use `--stream-segment-threads` instead of `--{dash,hds,hls}-segment-threads` - use `--stream-segment-timeout` instead of `--{dash,hds,hls}-segment-timeout` - use `--stream-timeout` instead of `--{dash,hds,hls,rtmp,http-stream}-timeout` See the documentation's [deprecations page](https://streamlink.github.io/latest/deprecations.html#streamlink-2-4-0) for more information. - Deprecated: `--hls-segment-stream-data` option and made it always stream segment data ([#3894](https://github.com/streamlink/streamlink/pull/3894)) - Updated: Python version of the Windows installer from 3.8 to 3.9 and dropped support for Windows 7 due to Python incompatibilities ([#3918](https://github.com/streamlink/streamlink/pull/3918)) See the documentation's [install page](https://streamlink.github.io/install.html) for alternative installation methods on Windows 7. - Updated: FFmpeg in the Windows Installer from 4.2 (Zeranoe) to 4.4 ([streamlink/FFmpeg-Builds](https://github.com/streamlink/FFmpeg-Builds)) ([#3981](https://github.com/streamlink/streamlink/pull/3981)) - Added: `{author}`, `{category}`/`{game}`, `{title}` and `{url}` variables to `--output`, `--record` and `--record-and-play` ([#3962](https://github.com/streamlink/streamlink/pull/3962)) - Added: `{time}`/`{time:custom-format}` variable to `--title`, `--output`, `--record` and `--record-and-play` ([#3993](https://github.com/streamlink/streamlink/pull/3993)) - Added: `--fs-safe-rules` for changing character replacement rules in file outputs ([#3962](https://github.com/streamlink/streamlink/pull/3962)) - Added: plugin metadata to `--json` stream data output ([#3987](https://github.com/streamlink/streamlink/pull/3987)) - Fixed: named pipes not being cleaned up by FFMPEGMuxer ([#3992](https://github.com/streamlink/streamlink/pull/3992)) - Fixed: KeyError on invalid variables in `--player-args` ([#3988](https://github.com/streamlink/streamlink/pull/3988)) - Fixed: tests failing in certain cases when run in different order ([#3920](https://github.com/streamlink/streamlink/pull/3920)) - Fixed: initial HLS playlist parsing issues ([#3903](https://github.com/streamlink/streamlink/pull/3903), [#3910](https://github.com/streamlink/streamlink/pull/3910)) - Fixed: various plugin issues. Please see the changelog down below. - Dependencies: added `lxml>=4.6.3` ([#3952](https://github.com/streamlink/streamlink/pull/3952)) - Dependencies: switched back to `requests>=2.26.0` on Windows ([#3930](https://github.com/streamlink/streamlink/pull/3930)) - Removed plugins: animeworld ([#3951](https://github.com/streamlink/streamlink/pull/3951)), gardenersworld ([#3966](https://github.com/streamlink/streamlink/pull/3966)), huomao ([#3932](https://github.com/streamlink/streamlink/pull/3932)) ```text Grabien <60237587+Grabien@users.noreply.github.com> (1): plugins.nbcnews: fix stream URL extraction (#3909) Ian Cameron <1661072+mkbloke@users.noreply.github.com> (2): plugins.huomao: plugin removal plugins.pluto: fix URL match for 2 letter language codes Leonardo Nascimento (1): plugins.booyah: add support for source stream (#3969) back-to (9): stream.hls: handle exception StreamError in Thread-HLSStreamWorker - iter_segments plugins.raiplay: use 'res.encoding = "UTF-8"' plugins.rtve: update for /play/ URLs plugins.zattoo: fix HLS stream, added more debug details tests.mixins.stream_hls: increase TIMEOUT_AWAIT_WRITE timeout, use --durations 10 for pytest setup: update requests version >=2.26.0 and makeinstaller.sh plugins.abematv: skip invalid ad segments plugins.animelab: removed cli.argparser: Fixed ValueError for streamlink --help bastimeyer (39): session: deprecate options for spec. stream types stream.hls: remove hls-segment-stream-data option docs: reorganize stream transport options stream.hls: except more errors raised by requests tests.hls: fix playlist reload time tests stream.hls: close stream on initial parsing error installer: upgrade to python 3.9 tests: fix Plugin.bind(session) calls plugin: fix cookie related error messages docs: update python-requests version comment plugins.twitch: replace remaining kraken API calls plugins.twitch: refactor TwitchAPI class methods plugins.euronews: add API fallback requests plugins.sportschau: fix audio streams vendor: add lxml dependency plugins.deutschewelle: rewrite plugin plugins.gardenersworld: remove plugin cli: player title and file output metadata vars plugin.api.validate: switch to lxml.etree plugin.api.validate: add args+kwargs to transform plugin.api.validate: add parse_{json,html,xml,qsd} plugin: metadata attributes plugins: fix utils imports plugins.welt: rewrite and simplify using XPath plugins.deutschewelle: validate.parse_html plugins.reuters: rewrite and fix using XPath plugins.euronews: rewrite and fix using XPath installer: move assets config to local JSON file installer: switch to streamlink/FFmpeg-Builds cli.main: f-strings cli.main: annotate types of global vars cli.main: check args.json instead of console.json cli.console: refactor ConsoleOutput cli: include plugin metadata in --json output cli.output: fix unknown vars in --player-args / -a stream.ffmpegmux: always clean up named pipes cli.utils.formatter: rewrite Formatter cli.utils.formatter: implement format_spec cli: add {time:format} var to --output / --title gustaf (1): plugins.svtplay: fix plugin video id steven7851 (1): plugins.app17: fix API_URL and URL match (#3989) ``` ## streamlink 2.3.0 (2021-07-26) Release highlights: - Implemented: new plugin URL matching API ([#3814](https://github.com/streamlink/streamlink/issues/3814), [#3821](https://github.com/streamlink/streamlink/pull/3821)) Third-party plugins which use the old API will still be resolved, but those plugins will have to upgrade in the future. See the documentation's [deprecations page](https://streamlink.github.io/latest/deprecations.html#streamlink-2-3-0) for more information. - Implemented: HLS media initialization section (fragmented MPEG-4 streams) ([#3828](https://github.com/streamlink/streamlink/pull/3828)) - Upgraded: `requests` to `>=2.26.0,<3` and set it to `==2.25.1` on Windows ([#3864](https://github.com/streamlink/streamlink/pull/3864), [#3880](https://github.com/streamlink/streamlink/pull/3880)) - Fixed: YouTube channel URLs, premiering live streams, added API fallback ([#3847](https://github.com/streamlink/streamlink/pull/3847), [#3873](https://github.com/streamlink/streamlink/pull/3873), [#3809](https://github.com/streamlink/streamlink/pull/3809)) - Removed plugins: canalplus ([#3841](https://github.com/streamlink/streamlink/pull/3841)), dommune ([#3818](https://github.com/streamlink/streamlink/pull/3818)), liveedu ([#3845](https://github.com/streamlink/streamlink/pull/3845)), periscope ([#3813](https://github.com/streamlink/streamlink/pull/3813)), powerapp ([#3816](https://github.com/streamlink/streamlink/pull/3816)), rtlxl ([#3842](https://github.com/streamlink/streamlink/pull/3842)), streamingvideoprovider ([#3843](https://github.com/streamlink/streamlink/pull/3843)), teleclubzoom ([#3817](https://github.com/streamlink/streamlink/pull/3817)), tigerdile ([#3819](https://github.com/streamlink/streamlink/pull/3819)) ```text Hakkin Lain (1): stream.hls: set fallback playlist reload time to 6 seconds (#3887) back-to (16): plugins.youtube: added API fallback plugins.rtvs: fixed livestream plugins.nos: Fixed Livestream and VOD plugins.vlive: fixed livestream (#3820) plugins.Tigerdile: removed plugins.Dommune: removed plugins.PowerApp: removed plugins.TeleclubZoom: removed (#3817) plugins.cdnbg: Fix regex and referer issues plugins.rtlxl: removed plugins.CanalPlus: removed plugins.liveedu: removed plugins.Streamingvideoprovider: removed plugin.api: update useragents plugins.youtube: detect Livestreams with 'isLive' plugins.nimotv: use 'mStreamPkg' bastimeyer (30): plugins.youtube: translate embed_live URLs plugins.periscope: remove plugin plugins.mediaklikk: rewrite plugin stream.hls: add type hints and refactor stream.hls: implement media initialization section plugin: new matchers API plugins: update protocol plugins plugins: update basic plugins plugins: update plugins with URL capture groups plugins: update plugins with spec. can_handle_url plugins: update plugins with multiple URL matchers plugins: update plugins with URL translations session: resolve deprecated plugins plugins.zdf_mediathek: refactor plugin, drop HDS docs: add deprecations page plugins.tv8: remove API, find HLS via simple regex plugins.youtube: find videoId on channel pages chore: replace issue templates with forms chore: fix issue forms checklist tests: remove mock from dev dependencies vendor: set requests to >=2.26.0,<3 tests: temporarily skip broken tests on win32 tests: fix unnecessary hostname lookup in cli_main docs: fix headline anchors on deprecations page vendor: downgrade requests to 2.25.1 on Windows tests: refactor TestMixinStreamHLS streams.segmented: refactor worker and writer streams.segmented: refactor reader streams.hls: refactor worker streams.hls: fix playlist_reload_time gustaf (1): plugins.tv4play: fix plugin URL regex vinyl-umbrella <61788251+vinyl-umbrella@users.noreply.github.com> (1): plugins.openrectv: update HLS URLs (#3850) ``` ## streamlink 2.2.0 (2021-06-19) Release highlights: - Changed: default config file path on macOS and Windows ([#3766](https://github.com/streamlink/streamlink/pull/3766)) - macOS: `${HOME}/Library/Application Support/streamlink/config` - Windows: `%APPDATA%\streamlink\config` - Changed: default custom plugins directory path on macOS and Linux/BSD ([#3766](https://github.com/streamlink/streamlink/pull/3766)) - macOS: `${HOME}/Library/Application Support/streamlink/plugins` - Linux/BSD: `${XDG_DATA_HOME:-${HOME}/.local/share}/streamlink/plugins` - Deprecated: old config file paths and old custom plugins directory paths ([#3784](https://github.com/streamlink/streamlink/pull/3784)) - Windows: - `%APPDATA%\streamlink\streamlinkrc` - macOS: - `${XDG_CONFIG_HOME:-${HOME}/.config}/streamlink/config` - `${XDG_CONFIG_HOME:-${HOME}/.config}/streamlink/plugins` - `${HOME}/.streamlinkrc` - Linux/BSD: - `${XDG_CONFIG_HOME:-${HOME}/.config}/streamlink/plugins` - `${HOME}/.streamlinkrc` Support for these old paths will be dropped in the future. See the [CLI documentation](https://streamlink.github.io/cli.html) for all the details regarding these changes. - Implemented: `--logfile` CLI argument ([#3753](https://github.com/streamlink/streamlink/pull/3753)) - Fixed: Youtube 404 errors by dropping private API calls (plugin rewrite) ([#3797](https://github.com/streamlink/streamlink/pull/3797)) - Fixed: Twitch clips ([#3762](https://github.com/streamlink/streamlink/pull/3762), [#3775](https://github.com/streamlink/streamlink/pull/3775)) and hosted channel redirection ([#3776](https://github.com/streamlink/streamlink/pull/3776)) - Fixed: Olympicchannel plugin ([#3760](https://github.com/streamlink/streamlink/pull/3760)) - Fixed: various Zattoo plugin issues ([#3773](https://github.com/streamlink/streamlink/pull/3773), [#3780](https://github.com/streamlink/streamlink/pull/3780)) - Fixed: HTTP responses with truncated body and mismatching content-length header ([#3768](https://github.com/streamlink/streamlink/pull/3768)) - Fixed: scheme-less URLs with address:port for `--http-proxy`, etc. ([#3765](https://github.com/streamlink/streamlink/pull/3765)) - Fixed: rendered man page path on Sphinx 4 ([#3750](https://github.com/streamlink/streamlink/pull/3750)) - Added plugins: mildom.com ([#3584](https://github.com/streamlink/streamlink/pull/3584)), booyah.live ([#3585](https://github.com/streamlink/streamlink/pull/3585)), mediavitrina.ru ([#3743](https://github.com/streamlink/streamlink/pull/3743)) - Removed plugins: ine.com ([#3781](https://github.com/streamlink/streamlink/pull/3781)), playtv.fr ([#3798](https://github.com/streamlink/streamlink/pull/3798)) ```text Billy2011 (2): plugins.mediaklikk: add m4sport.hu (#3757) plugins.olympicchannel: fix / rewrite DESK-coder (1): plugins.zattoo: changes to hello_v3 and new token.js (#3773) FaceHiddenInsideTheDark (1): plugins.funimationnow: fix subtitle language (#3752) Ian Cameron <1661072+mkbloke@users.noreply.github.com> (2): plugins.bfmtv: fix/find Brightcove video data in JS (#3662) plugins.booyah: new plugin back-to (7): plugins.tf1: fixed api_url plugins.onetv: cleanup plugins.mediavitrina: new plugin plugin.api: update useragents, remove EDGE plugins.ine: removed plugins.zattoo: cleanup, fix other domains plugins.playtv: removed - SEC_ERROR_EXPIRED_CERTIFICATE (#3798) bastimeyer (27): plugins.rtpplay: fix obfuscated HLS URL parsing utils.url: add encoding options to update_qsd docs: set man_make_section_directory to false tests.hls: test headers on segment+key requests cli.argparser: fix description text utils.url: fix update_scheme with implicit schemes plugins.twitch: add access token to clips tests: refactor TestCLIMainLogging cli: implement --logfile plugins.twitch: fix clips URL regex plugin.api.http_session: refactor HTTPSession plugin.api.http_session: enforce_content_length stream.hls: replace custom PKCS#7 unpad function plugin.api.validate: add nested lookups to get() plugin.api.validate: implement union_get() plugins.twitch: query hosted channels on GQL plugins.twitch: tidy up API calls cli: refactor CONFIG_FILES and PLUGIN_DIRS cli: add XDG_DATA_HOME as first plugins dir cli: rename config file on Windows to "config" cli: use correct config and plugins dir on macOS cli: deprecate old config files and plugin dirs cli: fix order of config file deprecation log msgs plugins.youtube: clean up a bit plugins.youtube: update URL regex, translate URLs plugins.youtube: replace private API calls plugins.youtube: unescape consent form values shirokumacode <79662880+shirokumacode@users.noreply.github.com> (1): plugins.mildom: new plugin for mildom.com (#3584) ``` ## streamlink 2.1.2 (2021-05-20) Patch release: - Fixed: youtube 404 errors ([#3732](https://github.com/streamlink/streamlink/pull/3732)), consent dialog ([#3672](https://github.com/streamlink/streamlink/pull/3672)) and added short URLs ([#3677](https://github.com/streamlink/streamlink/pull/3677)) - Fixed: picarto plugin ([#3661](https://github.com/streamlink/streamlink/pull/3661)) - Fixed: euronews plugin ([#3698](https://github.com/streamlink/streamlink/pull/3698)) - Fixed: bbciplayer plugin ([#3725](https://github.com/streamlink/streamlink/pull/3725)) - Fixed: missing removed-plugins-file in `setup.py build` ([#3653](https://github.com/streamlink/streamlink/pull/3653)) - Changed: HLS streams to use rounded bandwidth names ([#3721](https://github.com/streamlink/streamlink/pull/3721)) - Removed: plugin for hitbox.tv / smashcast.tv ([#3686](https://github.com/streamlink/streamlink/pull/3686)), tvplayer.com ([#3673](https://github.com/streamlink/streamlink/pull/3673)) ```text Alexis Murzeau (1): build: include .removed file in build Ian Cameron <1661072+mkbloke@users.noreply.github.com> (3): plugins.tvplayer: plugin removal plugins.picarto: rewrite/fix (#3661) plugins.bbciplayer: fix/update state_re regex Kagamia (1): plugins.nicolive: fix proxy arguments (#3710) Yavuz Kömeçoğlu (1): plugins.youtube: add html5=1 parameter (#3732) back-to (3): plugins.youtube: fix consent dialog (#3672) plugins.mitele: use '_{bitrate}' and remove duplicates stream.hls_playlist: round BANDWIDTH and parse as int (#3721) bastimeyer (7): plugins.youtube: add short video URLs plugins.hitbox: remove plugin chore: remove square brackets from issue titles plugins.euronews: rewrite and fix live streams utils.named_pipe: rewrite named pipes docs: fix winget package link ci.github: add python 3.10-dev to test runners bururaku (1): plugins.abematv: Fixed download problem again. (#3658) ``` ## streamlink 2.1.1 (2021-03-25) Patch release: - Fixed: test failure due to missing removed plugins file in sdist tarball ([#3644](https://github.com/streamlink/streamlink/pull/3644)). ```text Sebastian Meyer (1): build: don't build sdist/bdist quietly (#3645) bastimeyer (1): build: include removed plugins file in sdist ``` ## streamlink 2.1.0 (2021-03-22) Release highlights: - Added: `--interface`, `-4` / `--ipv4` and `-6` / `--ipv6` ([#3483](https://github.com/streamlink/streamlink/pull/3483)) - Added: `--niconico-purge-credentials` ([#3434](https://github.com/streamlink/streamlink/pull/3434)) - Added: `--twitcasting-password` ([#3505](https://github.com/streamlink/streamlink/pull/3505)) - Added: Linux AppImages ([#3611](https://github.com/streamlink/streamlink/pull/3611)) - Added: pre-built man page to bdist wheels and sdist tarballs ([#3459](https://github.com/streamlink/streamlink/pull/3459), [#3510](https://github.com/streamlink/streamlink/pull/3510)) - Added: plugin for ahaber.com.tr and atv.com.tr ([#3484](https://github.com/streamlink/streamlink/pull/3484)), nimo.tv ([#3508](https://github.com/streamlink/streamlink/pull/3508)) - Fixed: `--player-http` / `--player-continuous-http` HTTP server being bound to all interfaces ([#3450](https://github.com/streamlink/streamlink/pull/3450)) - Fixed: handling of languages without alpha_2 code when using pycountry ([#3518](https://github.com/streamlink/streamlink/pull/3518)) - Fixed: memory leak when calling `streamlink.streams()` ([#3486](https://github.com/streamlink/streamlink/pull/3486)) - Fixed: race condition in HLS related tests ([#3454](https://github.com/streamlink/streamlink/pull/3454)) - Fixed: `--player-fifo` issues on Windows with VLC or MPV ([#3619](https://github.com/streamlink/streamlink/pull/3619)) - Fixed: various plugins issues (see detailed changelog down below) - Removed: Windows portable (RosadinTV) ([#3535](https://github.com/streamlink/streamlink/pull/3535)) - Removed: plugin for micous.com ([#3457](https://github.com/streamlink/streamlink/pull/3457)), ntvspor.net ([#3485](https://github.com/streamlink/streamlink/pull/3485)), btsports ([#3636](https://github.com/streamlink/streamlink/pull/3636)) - Dependencies: set `websocket-client` to `>=0.58.0` ([#3634](https://github.com/streamlink/streamlink/pull/3634)) ```text Alexis Murzeau (1): docs: update Debian stable install instructions Billy2011 (1): plugins.stadium: adaptions for new player api (#3506) Ian Cameron <1661072+mkbloke@users.noreply.github.com> (7): plugins.mico: plugin removal plugins.dogus: remove channel and update test plugins.turkuvaz: add channels and URL tests plugins.tvtoya: fix playlist regex plugins.nimotv: new plugin plugins.tvtoya: minor fixes plugins.mjunoon: rewrite/fix Jefffrey <22608443+Jefffrey@users.noreply.github.com> (1): plugins.Nicolive: login before getting wss api url Miguel Valadas (1): plugins.rtpplay: add schema and fix HLS URL (#3627) Vladimir Stavrinov <9163352+vstavrinov@users.noreply.github.com> (1): plugins.oneplusone: fix iframe url pattern (#3503) alnj (1): plugins.twitcasting: add support for private/password-protected streams (#3505) back-to (11): cli.main: use *_args, **_kwargs for create_http_server (#3450) plugins.nicolive: added --niconico-purge-credentials docs: remove outdated gst-player example plugins.facebook: Add 'Log into Facebook' error message. plugins.afreeca: use 'gs_cdn_pc_web' and 'common' stream.dash: Fix static playlist - refresh_wait - Pipe copy aborted - Read timeout plugin.api: update useragents (#3637) plugins.zattoo: use 'dash' as default stream setup.py: require websocket-client>=0.58.0 plugins.nicolive: fixed websocket-client plugins.btsports: remove plugin bastimeyer (36): tools: force LF line endings via .gitattributes docs: add minimalist code of conduct stream.hls: open reader from class attribute tests.hls: await all filtered-HLS writer calls plugins.twitch: fix access_token on invalid inputs ci: add netlify docs preview deploy config docs: add thank-you section to index page build: include man page in wheels docs: bump furo docs req to 2020.12.28.beta23 2021 http_session: remove HTTPAdapterWithReadTimeout docs: improve install-via-pip section docs: fix description of `--ffmpeg-fout` build: include man page in sdist tarballs utils/l10n: fix langs without alpha_2 in pycountry plugins.bloomberg: fix and refactor plugin utils: remove custom memoize decorator docs: remove CLI tutorial from man page session: implement --interface, --ipv4 and --ipv6 docs: remove RosadinTV Windows portable version ci.github: increase git fetch depth of tests tests: fix test code coverage ci.codecov: 100% tests target, add patch status docs: clean up package maintainers list plugins.vtvgo: ignore duplicate params ci.codecov: disable GH status check annotations chore: reorder and improve issue templates plugins: fix invalid plugin class names tests.plugins: parametrize can_handle_url tests plugins: fix and update removed plugins list docs: add appimages section to install docs ci.netlify: build docs when CHANGELOG.md changes docs: add pip to packages lists cli.output: fix named pipe player input on Windows cli: debug-log arguments set by the user cli: refactor log_current_versions and add tests bururaku (1): plugins.abematv: Update abematv.py (#3617) fenopa <62562166+fenopa@users.noreply.github.com> (1): installer: upgrade to python 3.8.7 losuler (1): docs: update URL to Fedora repo onde2rock (1): plugins.bfmtv : fix rmcstory and rmcdecouverte (#3471) vinyl-umbrella <61788251+vinyl-umbrella@users.noreply.github.com> (1): plugins.openrectv: update/fix (#3583) ``` ## streamlink 2.0.0 (2020-12-22) Release highlights: - BREAKING: dropped support for Python 2 and Python 3.5 ([#3232](https://github.com/streamlink/streamlink/pull/3232), [#3269](https://github.com/streamlink/streamlink/pull/3269)) - BREAKING: updated the Python version of the Windows installer to 3.8 ([#3330](https://github.com/streamlink/streamlink/pull/3330)) Users of Windows 7 will need their system to be fully upgraded. - BREAKING: removed all deprecated CLI arguments ([#3277](https://github.com/streamlink/streamlink/pull/3277), [#3349](https://github.com/streamlink/streamlink/pull/3349)) - `--http-cookies`, `--http-headers`, `--http-query-params` - `--no-version-check` - `--rtmpdump-proxy` - `--cmdline`, `-c` - `--errorlog`, `-e` - `--errorlog-path` - `--btv-username`, `--btv-password` - `--crunchyroll-locale` - `--pixiv-username`, `--pixiv-password` - `--twitch-oauth-authenticate`, `--twitch-oauth-token`, `--twitch-cookie` - `--ustvnow-station-code` - `--youtube-api-key` - BREAKING: replaced various subtitle muxing CLI arguments with `--mux-subtitles` ([#3324](https://github.com/streamlink/streamlink/pull/3324)) - `--funimationnow-mux-subtitles` - `--pluzz-mux-subtitles` - `--rtve-mux-subtitles` - `--svtplay-mux-subtitles` - `--vimeo-mux-subtitles` - BREAKING: sideloading faulty plugins will now raise an `Exception` ([#3366](https://github.com/streamlink/streamlink/pull/3366)) - BREAKING: changed trace logging timestamp format ([#3273](https://github.com/streamlink/streamlink/pull/3273)) - BREAKING/API: removed deprecated `Session` compat options ([#3349](https://github.com/streamlink/streamlink/pull/3349)) - BREAKING/API: removed deprecated custom `Logger` and `LogRecord` ([#3273](https://github.com/streamlink/streamlink/pull/3273)) - BREAKING/API: removed deprecated parameters from `HLSStream.parse_variant_playlist` ([#3347](https://github.com/streamlink/streamlink/pull/3347)) - BREAKING/API: removed `plugin.api.support_plugin` ([#3398](https://github.com/streamlink/streamlink/pull/3398)) - Added: new plugin for pluto.tv ([#3363](https://github.com/streamlink/streamlink/pull/3363)) - Added: support for HLS master playlist URLs to `--stream-url` / `--json` ([#3300](https://github.com/streamlink/streamlink/pull/3300)) - Added: `--ffmpeg-fout` for changing the output format of muxed streams ([#2892](https://github.com/streamlink/streamlink/pull/2892)) - Added: `--ffmpeg-copyts` and `--ffmpeg-start-at-zero` ([#3404](https://github.com/streamlink/streamlink/pull/3404), [#3413](https://github.com/streamlink/streamlink/pull/3413)) - Added: `--streann-url` for iframe referencing ([#3356](https://github.com/streamlink/streamlink/pull/3356)) - Added: `--niconico-timeshift-offset` ([#3425](https://github.com/streamlink/streamlink/pull/3425)) - Fixed: duplicate stream names in DASH inputs ([#3410](https://github.com/streamlink/streamlink/pull/3410)) - Fixed: youtube live playback ([#3268](https://github.com/streamlink/streamlink/pull/3268), [#3372](https://github.com/streamlink/streamlink/pull/3372), [#3428](https://github.com/streamlink/streamlink/pull/3428)) - Fixed: `--twitch-disable-reruns` ([#3375](https://github.com/streamlink/streamlink/pull/3375)) - Fixed: various plugins issues (see detailed changelog down below) - Changed: `{filename}` variable in `--player-args` / `-a` to `{playerinput}` and made both optional ([#3313](https://github.com/streamlink/streamlink/pull/3313)) - Changed: and fixed `streamlinkrc` config file in the Windows installer ([#3350](https://github.com/streamlink/streamlink/pull/3350)) - Changed: MPV's automated `--title` argument to `--force-media-title` ([#3405](https://github.com/streamlink/streamlink/pull/3405)) - Changed: HTML documentation theme to [furo](https://github.com/pradyunsg/furo) ([#3335](https://github.com/streamlink/streamlink/pull/3335)) - Removed: plugins for `skai`, `kingkong`, `ellobo`, `trt`/`trtspor`, `tamago`, `streamme`, `metube`, `cubetv`, `willax` ```text Billy2011 (2): plugins.youtube: fix live playback (#3268) stream.ffmpegmux: add --ffmpeg-copyts option (#3404) Forrest Alvarez (1): Update author email to shared email Hunter Peavey (1): docs: update wtwitch in thirdparty list (#3286) Ian Cameron <1661072+mkbloke@users.noreply.github.com> (9): plugins.skai: plugin removal plugins.kingkong: plugin removal plugins.cnews: fix video ID search, add schema plugins.ellobo: plugin removal plugins.nbcnews: fix video ID search, add schemas plugins.bfmtv: fix ID & embed re, use Dailymotion plugins.filmon: mitigate for non-JSON data response plugins.schoolism: fix and test for colon in title (#3421) plugins.dogan: fix/update Jon Bergli Heier (1): plugins.nrk: fix/rewrite plugin (#3318) Mark Ignacio (1): plugins.NicoLive: add --niconico-timeshift-offset option (#3425) Martin Buck (1): plugins.zdf_mediathek: also support 3sat mediathek Sean Greenslade (1): plugins.picarto: explicitly detect and fail on private streams (#3278) Sebastian Meyer (2): chore: drop support for Python 3.5 (#3269) ci.github: run lint step before test step (#3294) Seonjae Hyeon (1): plugins.vlive: fix URL regex and plugin (#3315) azizLIGHT (1): docs: fix mpv property-list link in --title description (#3342) back-to (26): plugins.facebook: remove User-Agent (#3272) plugins.trt/trtspor: remove plugins plugin.api.useragents: update User-Agent plugins: remove FIREFOX User-Agent imports plugins.abweb: fixed login issues plugins.huya: use FLV stream with multiple mirrors plugin.api.useragents: update User-Agent's plugins.tamago: removed dead plugin plugins.streamme: removed dead plugin plugins.metube: removed dead plugin plugins.cubetv: removed dead plugin cli.utils: remove named_pipe.py file, use streamlink.utils import plugins.willax: removed plugin, they use streann plugins.streann: allow different source URLs plugins.pixiv: set headers for stream data, fixed login issue plugins.pluto: new plugin for https://pluto.tv/ (#3363) plugins.twitch: fix ads plugins.twitch: fix --twitch-disable-reruns plugins.youtube: quickfix for "/live" URL plugins.pluto: ignore invalid channels stream.dash: allow '_alt' streams with the same resolution (#3410) plugins.afreeca: update '_get_channel_info' with 'bno', plugin cleanup (#3408) plugins.plugin: use the same cls.logger 'plugins' stream.ffmpegmux: disable -start_at_zero for -copyts as default (#3413) plugin.api.useragents: update User-Agent plugins.youtube: Fix 'ytInitialData' for channel pages bastimeyer (71): chore: drop support for Python 2 chore: remove is_py{2,3} compat checks chore: remove compat imports of builtins chore: remove streamlink.utils.encoding chore: remove simple aliased compat imports chore: remove compat imports of removed py2 deps chore: remove compat import of html module chore: remove compat imports of urllib and queue chore: remove remaining inspect compat import chore: remove unneeded __future__ imports chore: remove file encoding header comments chore: remove compat imports from tests logger: replace self.logger calls in plugins logger: format all log messages directly logger: remove deprecated compat logger logger: refactor StringFormatter chore: remove old LIVESTREAMER_VERSION constant chore: remove deprecated CLI arguments flake8: add import-order linting config plugins.twitch: player_type access token parameter ci.github: install latest version of pynsist chore: implicit py3 super() calls chore: remove u-strings ci.github: set ubuntu to 20.04 and python to 3.9 cli: optional player-args input variable cli: add support for stream manifest URL output installer: upgrade to Python 3.9.0 installer: switch back to latest pynsist release installer: downgrade to python 3.8 docs: add note about supported Windows versions docs: add autosectionlabel Sphinx extension docs: fix most http links plugin: implement global plugin arguments plugins: turn mux-subtitles into a global argument plugins.twitch: remove player_type parameter plugins.twitch: move access_token request to GQL chore: remove HLS variant playlist compat params chore: remove old rtmpdump/subprocess CLI args installer: fix + rewrite streamlinkrc config file stream.ffmpegmux: only close FFMPEGMuxer once chore: add dev version checkbox to issue templates chore: inherit from object implicitly chore: set literals and dict comprehensions chore: use yield from where possible chore: replace old errors classes with OSError chore: drop python six compat stuff chore: fix deprecated logging.Logger.warn calls docs: fix CLI page docs: split CLI args in HTML output into rows session: replace usage of deprecated imp module docs: add warning to plugin sideloading section refactor: test_session, move testplugin files plugin.api: remove support_plugin tests: fix test_cmdline{,_title} chore: add issue template config with more links docs: switch theme to furo, bump sphinx to >=3.0 docs: remove custom sphinx_rtd_theme_violet tools: update editorconfig for docs theme files docs: add index page to toctree docs: add custom stylesheet and customize sidebar docs: change/fix fonts, brand colors and spacings docs: add version warning message docs: fix applications and donate pages cli: move plugin args into their own args group docs: fix scrollbar issues in both sidebars docs: add favicons and PWA manifest cli.output: replace MPV player title parameter stream.hls: merge hls_filtered with hls cli: move --stream-url to different args group cache: catch OverflowError in set() docs: fix link in readme beardypig (6): tests: fix log tests when run on a system with a non-UTC timezone chore: use new py3 yield from syntax chore: sort imports, fix a dependency cycle and use absolute imports tests: validate all plugins' global arguments plugins.mitele: update plugin to support new website APIs (#3338) stream.ffmpegmux: Add support for specifying output file format and audio sync option (#2892) enilfodne (1): plugins.cdnbg: simplify and fix iframes without schema smallbomb (1): plugins: fix radiko.py url (#3394) ``` ## streamlink 1.7.0 (2020-10-18) Release highlights: - Added: new plugins for micous.com, tv999.bg and cbsnews.com - Added: new embedded ad detection for Twitch streams ([#3213](https://github.com/streamlink/streamlink/pull/3213)) - Fixed: a few broken plugins and minor plugin issues (see changelog down below) - Fixed: arguments in config files were read too late before taking effect ([#3255](https://github.com/streamlink/streamlink/pull/3255)) - Fixed: Arte plugin returning too many streams and overriding primary ones ([#3228](https://github.com/streamlink/streamlink/pull/3228)) - Fixed: Twitch plugin error when stream metadata API response is empty ([#3223](https://github.com/streamlink/streamlink/pull/3223)) - Fixed: Zattoo login issues ([#3202](https://github.com/streamlink/streamlink/pull/3202)) - Changed: plugin request and submission guidelines ([#3244](https://github.com/streamlink/streamlink/pull/3244)) - Changed: refactored and cleaned up Twitch plugin ([#3227](https://github.com/streamlink/streamlink/pull/3227)) - Removed: `platform=_` stream token request parameter from Twitch plugin (again) ([#3220](https://github.com/streamlink/streamlink/pull/3220)) - Removed: plugins for itvplayer, aljazeeraen, srgssr and dingittv ```text Alexis Murzeau (1): docs: use recommonmark as an extension Billy2011 (3): plugins.zattoo: use hello api v2 for zattoo.com (#3202) plugins.dlive: rewrite plugin (#3239) utils.l10n: use DEFAULT_LANGUAGE_CODE if locale lookup fails (#3055) Forrest (1): plugins.itvplayer: remove due to DRM (#2934) Ian Cameron <1661072+mkbloke@users.noreply.github.com> (8): plugins.mico: new plugin for http://www.micous.com/ (#3188) plugins.cdnbg: update url_re, plugin test, plugin matrix (#3205) plugins.tv999: new plugin for http://tv999.bg/live.html (#3199) plugins.aljazeeraen: plugin removal (#3207) plugins.srgssr: plugin removal plugins.tv3cat: update URL match, test and plugin matrix chore: update issue templates (#3250) docs: add plugin addition/removal infos (#3249) Sebastian Meyer (2): Improve coverage reports on codecov (#3200) plugins.twitch: remove platform access token param (#3220) back-to (4): plugin.api.useragents: update User-Agent plugins.livestream: remove AkamaiHDStream, use only secure HLSStream (#3243) plugins.dingittv: removed, website is unmaintained plugins: mark some plugins as broken (#3262) bastimeyer (21): ci.coverage: increase threshold of tests status tests: add stream_hls mixin for testing HLSStreams stream.hls_filtered: refactor tests, use mixin plugins.twitch: refactor tests, use mixin stream.hls: refactor reload time tests, use mixin stream.hls: separate variant playlist tests stream.hls: separate default and encrypted tests stream.hls_playlist: implement EXT-X-DATERANGE tag plugins.twitch: filter ads by EXT-X-DATERANGE tag plugins.twitch: fix metadata API response handling ci: add python 3.9 test runners tests: fix early writer close in stream_hls mixin stream.segmented: gracefully shut down thread pool plugins.twitch: remove video-type distinction plugins.twitch: refactor Twitch API related code plugins.twitch: refactor _get_hls_streams plugins.twitch: remove stream weights and clean up docs: fix working tree check in deploy script docs: update plugin guidelines docs: add developing menu with basic setup steps docs: add generic pull request template beardypig (3): plugins.cbsnews: support for live streams from CBS News (#3251) plugins.artetv: only pick the first variant of the stream (#3228) cli: make config based args available during early setup (#3255) ``` ## streamlink 1.6.0 (2020-09-22) Release highlights: - Fixed: lots of broken plugins and minor plugin issues (see changelog down below) - Fixed: embedded ads on Twitch with an ads workaround, removing pre-roll and mid-stream ads ([#3173](https://github.com/streamlink/streamlink/pull/3173)) - Fixed: read timeout error when filtering out HLS segments ([#3187](https://github.com/streamlink/streamlink/pull/3187)) - Fixed: twitch plugin logging incorrect low-latency status when pre-roll ads exist ([#3169](https://github.com/streamlink/streamlink/pull/3169)) - Fixed: crunchyroll auth logic ([#3150](https://github.com/streamlink/streamlink/pull/3150)) - Added: the `--hls-playlist-reload-time` parameter for customizing HLS playlist reload times ([#2925](https://github.com/streamlink/streamlink/pull/2925)) - Added: `python -m streamlink` invocation style support ([#3174](https://github.com/streamlink/streamlink/pull/3174)) - Added: plugin for mrt.com.mk ([#3097](https://github.com/streamlink/streamlink/pull/3097)) - Changed: yupptv plugin and replaced email+pass with id+token authentication ([#3116](https://github.com/streamlink/streamlink/pull/3116)) - Removed: plugins for vaughnlive, pandatv, douyutv, cybergame, europaplus and startv ```text Ian Cameron <1661072+mkbloke@users.noreply.github.com> (11): docs: update turkuvaz plugin matrix entry (#3114) docs: Add reuters.com for reuters plugin entry in plugin matrix (#3124) Fix formatting for reuters plugin entry plugins.huomao: fix/rewrite (#3126) plugins.drdk: fix livestreams (#3115) plugins.tvplayer: update regex and tests for /uk/ URLs plugins.tv360: fix HLS URL regex and plugin (#3185) plugins: fix unescaped literal dots in url_re entries (#3192) plugins.svtplay: rewrite/fix (#3155) plugins.yupptv: fix/minor rewrite (#3116) plugins.ine: fix unescaped literal dots in js_re (#3196) Il Harper (2): Add OBS-Streamlink into thirdparty.rst Apply suggestions from code review PleasantMachine9 <65126927+PleasantMachine9@users.noreply.github.com> (1): support `python -m` cli invocation Sebastian Meyer (4): plugins.bloomberg: fix regex module anchor (#3131) plugins.sportschau: rewrite and fix plugin (#3142) plugins.raiplay: rewrite and fix plugin (#3147) plugins.twitch: refactor worker, parser and tests (#3169) Tr4sK (1): plugins.mrtmk: new plugin for http://play.mrt.com.mk/ (#3097) Yahya <5457202+anakaiti@users.noreply.github.com> (1): docs: update reference to minimum VLC version back-to (9): plugins.vaughnlive: removed plugins.pandatv: removed plugins.douyutv: removed plugins.tv8: fix plugin with new api plugins.cybergame: removed plugins.europaplus: remove plugin plugins.vk: remove '\' from data plugins.nicolive: fix quality plugins.wasd: fixed plugin (#3139) bastimeyer (8): stream.hls: customizable playlist reload times plugins.twitch: platform=_ in access_token request docs: fix NixOS link docs: replace easy_install macOS entry with pip docs: add comment regarding pip/pip3 differences stream.hls_filtered: implement FilteredHLSStream plugins.twitch: use FilteredHLS{Writer,Reader} stream.hls_filtered: fix tests beardypig (1): plugins.crunchyroll: update auth logic derFogel (1): plugins.zattoo: fix quantum tv streaming (#3108) hymer-up <34783904+hymer-up@users.noreply.github.com> (2): plugins.startv: remove plugin (#3163) plugins.dogus: add startv URL (#3161) ``` ## streamlink 1.5.0 (2020-07-07) A minor release with fixes for `pycountry==20.7.3` ([#3057](https://github.com/streamlink/streamlink/pull/3057)) and a few plugin additions and removals. And of course the usual plugin fixes and upgrades, which you can see in the git shortlog down below. Thank you to everyone involved! Support for Python2 has not been dropped yet (contrary to the comment in the last changelog), but will be in the near future. ```text Alexis Murzeau (1): docs: update debian install instructions Billy2011 (8): plugins.nbcsports: fix embed_url_re (#2980) plugins.olympicchannel: fix/rewrite (#2981) plugins.foxtr: fix playervars_re (#3013) plugins.huya: fix _hls_re (#3007) plugins.ceskatelevize: add new api for some links (#2991) plugins.beattv: remove plugin (#3053) plugins.ard_live: fix / rewrite (#3052) plugins.ard_mediathek: fix / update (#3049) Code <60588434+superusercode@users.noreply.github.com> (1): Streamlink was added to Windows Package Manager Ian Cameron <1661072+mkbloke@users.noreply.github.com> (6): plugins.tvplayer: Add missing platform key in the GET for stream_url (#2989) plugins.btv: remove login and fix API URL (#3019) plugins.n13tv: new plugin - replaces plugins.reshet (#3034) plugins.reshet: plugin removal (#3000) plugins.tvnbg: plugin removal (#3056) plugins.adultswim: fix/rewrite (#2952) Sebastian Meyer (3): ci: no test/documentation jobs on scheduled run (#3012) cli.main: fix msecs format in logging output (#3025) utils.l10n: fix pycountry language lookup (#3057) Vladimir Stavrinov <9163352+vstavrinov@users.noreply.github.com> (1): plugins.nbcnews: new plugin for http://nbcnews.com/now (#2927) back-to (11): plugins.showroom: use normal HLSStreams docs: remove unimportant note / file plugins.viasat: remove play.nova.bg domain actions: fixed incorrect versions and use names for codecov (#2932) plugins.filmon: use /tv/ url and raise PluginError for invalid channels flake8: E741 ambiguous variable name plugins.youtube: Fix isLive and signatureCipher (#3026) plugins.facebook: use meta og:video:url and added basic title support (#3024) plugins.picarto: fixed vod url detection ci: fix pycountry issue temporarily with a fixed version plugin.api.useragents: update User-Agent bastimeyer (3): docs/install: fix Windows package manager plugins.mixer: remove plugin ci: run scheduled tests, ignore coverage report beardypig (1): plugins.cdnbg: update plugin to support new sites, and remove old sites (#2912) lanroth (1): plugins.radionet: fix plugin so it works with new page format (#3018) resloved (1): fixed typo steven7851 (1): plugins.app17: update API (#2969) tnira (1): Plugin.nicolive:resolve API format change (#3061) unavailable <51099894+EnterGin@users.noreply.github.com> (1): plugins.twitch: fix call_subdomain (#2958) wiresp33d <66558220+wiresp33d@users.noreply.github.com> (2): plugins.bigo: use API for video URL (#3016) plugins.nicolive: resolve new api format (#3039) ``` ## streamlink 1.4.1 (2020-04-24) No code changes. [See the full `1.4.0` changelog here.](https://github.com/streamlink/streamlink/releases/tag/1.4.0) ```text beardypig (1): build: include correct signing key: 0xE3DB9E282E390FA0 ``` ## streamlink 1.4.0 (2020-04-22) This will be the last release with support for Python 2, as it has finally reached its EOL at the beginning of this year. Streamlink 1.4.0 comes with lots of plugin fixes/improvements, as well as some new features and plugins, and also a few plugin removals. Notable changes: - New: low latency streaming on Twitch via `--twitch-low-latency` ([#2513](https://github.com/streamlink/streamlink/pull/2513)) - New: output HLS segment data immediately via `--hls-segment-stream-data` ([#2513](https://github.com/streamlink/streamlink/pull/2513)) - New: always show download progress via `--force-progress` ([#2438](https://github.com/streamlink/streamlink/pull/2438)) - New: URL template support for `--hls-segment-key-uri` ([#2821](https://github.com/streamlink/streamlink/pull/2821)) - Removed: Twitch auth logic, `--twitch-oauth-token`, `--twitch-oauth-authenticate`, `--twitch-cookie` ([#2846](https://github.com/streamlink/streamlink/pull/2846)) - Fixed: Youtube plugin ([#2858](https://github.com/streamlink/streamlink/pull/2858)) - Fixed: Crunchyroll plugin ([#2788](https://github.com/streamlink/streamlink/pull/2788)) - Fixed: Pixiv plugin ([#2840](https://github.com/streamlink/streamlink/pull/2840)) - Fixed: TVplayer plugin ([#2802](https://github.com/streamlink/streamlink/pull/2802)) - Fixed: Zattoo plugin ([#2887](https://github.com/streamlink/streamlink/pull/2887)) - Changed: set Firefox User-Agent HTTP header by default ([#2795](https://github.com/streamlink/streamlink/pull/2795)) - Changed: upgraded bundled FFmpeg to `4.2.2` in Windows installer ([#2916](https://github.com/streamlink/streamlink/pull/2916)) ```text Adam Baxter (1): stream.hls_playlist: Add extra logging for invalid #EXTM3U line (#2479) Alexis Murzeau (1): docs: fix duplicate object description of streamlink in api docs Colas Broux (2): plugins.youtube: Fix for new Youtube VOD API (#2858) Updating README Applying changes from 1402fb0 to the README Closes #2880 Finn (1): plugins.invintus: Add support for Invintus Media live streams and VOD (#2845) Ian Cameron <1661072+mkbloke@users.noreply.github.com> (3): Fix tvplayer plugin and tests (#2802) plugins.piczel: Added HLS, Removed RTMP (#2815) plugins.reuters: fix (#2811) Mohamed El Morabity (1): plugins.tf1: use new API to retrieve DASH streams (#2759) Riolu <16816842+iucario@users.noreply.github.com> (1): plugins.radiko: Add support for radiko.jp (#2826) Uinden <25625733+Uinden@users.noreply.github.com> (1): plugins.wasd: new plugin for WASD.TV (#2641) YYY (1): plugins.nicolive: new plugin for Niconico Live (#2651) Yavuz Kömeçoğlu (1): plugins.galatasaraytv: Add support for GALATASARAY SK TV (#2760) Zhenyu Hu (1): plugins.kugou: Add Kugou Fanxing live plugin (#2794) back-to (17): plugin.api: use Firefox as default User-Agent instead of python-requests plugins.filmon: retry for 50X errors cli: New command --force-progress (#2438) travis-ci: don't run doctr on pull requests plugins.bilibili: ignore unavailable URLs (#2818) plugins.mlgtv: remove plugin they use DRM for Livestreams (#2829) plugins.twitch: Fixed clips (#2843) plugins.showroom: Fix HLS missing segments plugins.kanal7: Removed Plugin they use static URLs plugins.rotana: new plugin for rotana.net (#2838) plugins.pixiv: removed not working login process via username (#2840) plugins.abema: support for Abema overseas version plugins.younow: remove plugin plugin.api.useragents: update User-Agent plugins.zattoo: fix app token and new recording URL plugins.zeenews: new plugin for https://zeenews.india.com/live-tv AUTHORS: removed unused script and removed outdated list (#2889) bastimeyer (58): plugins.twitch: fix rerun validation schema flake8: E303 flake8: E111 flake8: E117 flake8: E121 flake8: E122 flake8: E126, E127, E128 flake8: E203, E226, E231, E241, E261 flake8: E265 flake8: E302, E305 flake8: E402 flake8: E712 flake8: W291, W292, W293, W391 flake8: F401, F403 flake8: F405 flake8: F811 flake8: F841 flake8: W504 flake8: E741 flake8: E501 flake8: F601 flake8: E722 flake8: F821 flake8: F812 flake8: add flake8 to TravisCI cleanup: remove unnecessary unittest.main() calls cleanup: remove unnecessary python shebangs PEP263: use consistent utf-8 coding header comment tools: add .editorconfig stream.hls: add hls-segment-stream-data parameter plugins.twitch: low latency plugins.twitch: disable LL when filtering ads plugins.twitch: print info msg if stream is not LL plugins.bloomberg: fix vods and live streams plugins.twitch: remove cookie auth plugins.twitch: remove oauth token login docs: fix multiple options on the same line ci.github: implement main workflow ci.github: add release config and rewrite scripts ci.github: add scheduled nightly builds ci.github: deploy documentation ci: show extra test summary info ci: remove old CI configs ci.github: fix codecov uploads cleanup: change build badge and link in README.md cleanup: remove TravisCI from deploy scripts ci: remove macOS test runners codecov: wait before notifying docs: rewrite windows nightly builds section docs: rewrite pip/source install section docs: fix and rewrite index page docs: reformat donation page ci.github: fix continue on error installer: rewrite / clean up makeinstaller script installer: download ffmpeg+rtmpdump assets installer: delete locally included binary files plugins.twitch: rewrite disable ads logic Release 1.4.0 beardypig (10): update release signing key update docs deployment key plugins.tv360: updated URL and HLS stream extraction method util: fix some encoding issue when setting unicode/utf8 titles in py2 cli.output: make sure the player arguments are properly encoded utils: update_qsd to leave blank values unchanged (#2869) plugins.eurocom: remove eurocom plugin plugins.tv1channel: remove tv1channel plugin actions: no need to use a secret for the PyPI username add python 2.7 deprecation warning danieljpiazza (1): Update Crunchyroll access token. Fixes streamlink/streamlink issue #2785. malvinas2 (3): plugins.latina: new plugin for https://www.latina.pe/tvenvivo (#2793) plugins.albavision: Added support for ATV and ATVMas (#2801) plugins.rtve: Added support for clan tve, children's channel of RTVE (#2875) steven7851 (1): plugins.app17: fix for new layout (#2833) tarkah (1): stream.hls: add templating for hls-segment-key-uri option (#2821) ``` ## streamlink 1.3.1 (2020-01-27) A small patch release that addresses the removal of [MPV's legacy option syntax](https://mpv.io/manual/master/#legacy-option-syntax), also with fixes of several plugins, the addition of the `--twitch-disable-reruns` parameter and dropped support for Python 3.4. ```text Hunter Peavey (4): Add wtwitch to list of thirdparty programs Try adding an image Move image position Make requested changes Vladimir Stavrinov <9163352+vstavrinov@users.noreply.github.com> (1): plugins.nhkworld: the site migrates from xml to json stream data back-to (6): docs/tests: remove python 3.4, use 3.8 and nightly for travis-ci plugins.bilibili: fix Livestreams with status 1 (set Referer) plugins.youtube: Remove itag 303 plugins.ustream: Added support for video.ibm.com plugins.bbciplayer: Fixed login params plugins.bbciplayer: remove test_extract_nonce bastimeyer (5): plugins.twitch: use python logging module plugins.twitch: fix rerun detection cli.output: fix mpv player parameter format 2020 docs: fix MPV parameters on common issues page skulblakka (1): Allow to disable twitch reruns (#2722) ``` ## streamlink 1.3.0 (2019-11-22) A new release with plugin updates and fixes, including Twitch.tv (see [#2680](https://github.com/streamlink/streamlink/issues/2680)), which had to be delayed due to back and forth API changes. The Twitch.tv workarounds mentioned in [#2680](https://github.com/streamlink/streamlink/issues/2680) don't have to be applied anymore, but authenticating via `--twitch-oauth-token` has been disabled, regardless of the origin of the OAuth token (via `--twitch-oauth-authenticate` or the Twitch website). In order to not introduce breaking changes, both parameters have been kept in this release and the user name will still be logged when using an OAuth token, but receiving item drops or accessing restricted streams is not possible anymore. Plugins for the following sites have also been added: - albavision - news.now.com - twitcasting.tv - viu.tv - vlive.tv - willax.tv ```text Alexis Murzeau (1): plugins.pixiv: fix doc typo thats -> that's Mohamed El Morabity (1): plugins.idf1: HTTPS support Mohamed El Morabity (1): plugins.playtv: Fix for new stream data API (#2388) Ozan Karaali (1): plugins.foxtr: Extended support Ozan Karaali (1): plugins.cinergroup: #2390 fix (#2629) Troogle (1): plugins.bilibili: fix resolution issue Werner Robitza (1): remove direct installation instructions, link to docs back-to (6): setup.cfg: added flake8 settings plugins.vk: use html_unescape for HLS streams plugins.willax: new plugin for http://willax.tv/en-vivo/ plugins.zattoo: _url_re update for some new urls plugin.api.useragents: update CHROME and FIREFOX User-Agent stream.hls: Fix UnicodeDecodeError for log msg and name_prefix for stream_name bastimeyer (3): ci/travis: install pynsist 2.4 plugins.twitch: fix API issue - 410 gone error docs.cli: fix and reword the tutorial section beardypig (10): plugins.bbciplayer: update API URL to use https plugins.nownews: added support for the HK news site news.now.com plugins.tv8: update regex for the stream url plugins.bbciplayer: fix issue with nonce extraction plugins.bbciplayer: extract master brand/channel id from the state json plugins.itvplayer: Use flash streams for ITV1/ITV4 plugins.viutv: support for the viu.tv live stream plugins.albavision: support for some albavision live streams plugins.bloomberg: fix issue where the incorrect playlist could be used stream.streamprocess: check that a process is usable before using it derrod (1): plugins.vlive: Add support for V LIVE live streams printempw (1): plugins.twitcasting: new plugin for TwitCasting.tv ssaqua (1): plugins.linelive: update to support VOD/archived streams ``` ## streamlink 1.2.0 (2019-08-18) Here are the changes for this month's release - Multiple plugin fixes - Fixed single hyphen params at the beginning of --player-args (#2333) - `--http-proxy` will set the default value of `--https-proxy` to same as `--http-proxy`. (#2536) - DASH Streams will handle headers correctly (#2545) - the timestamp for FFMPEGMuxer streams will start with zero (#2559) ```text Davi Guimarães (1): plugins.cubetv: base url changes (#2430) Forrest (1): Add a sponsor button (#2478) Jiting (1): plugin.youtube: bug fix for YouTube live streams check Juan Ramirez (2): Invalid use of console.logger in CLI Too many arguments for logging format string Mohamed El Morabity (9): plugins.vimeo: new plugin for Vimeo streams plugins.vimeo: add subtitle support for vimeo plugin plugins.vimeo: fix alphabetical order in plugin matrix Use class parameter instead of class name in class method [plugins.bfmtv] Fix player regex [plugins.idf1] Update for new website layout plugins.gulli: enable HTTPS support plugins.gulli: fix live stream fetching plugins.tvrplus: fix for new website layout Mohamed El Morabity (1): plugins.clubbingtv: new plugin for Clubbing TV website (#2569) Viktor Kálmán (1): plugins.mediaklikk: update broken plugin (#2401) Vladimir Stavrinov (2): plugins.live_russia_tv: adjust to site changes (#2523) plugins.oneplusone: fix site changes (#2425) YuuichiMizuoka <32476209+YuuichiMizuoka@users.noreply.github.com> (1): add login posibility for pixiv using sessionid and devicetoken aqxa1 (1): Handle keyboard interrupts in can_handle_url checks (#2318) back-to (12): cli.argparser: Fix single hyphen params at the beginning of --player-args plugins.reuters: New Plugin plugins: Removed rte and tvcatchup utils.__init__: remove cElementTree, it's just an alias for ElementTree plugins.teamliquid: New domain, fix stream_weight plugins.vimeo: Fixed DASH Livestreams plugin.api.useragents: update CHROME and FIREFOX User-Agent ffmpegmux: use -start_at_zero with -copyts plugins.youtube: fixed reason msg, updated _url_re plugins.TV1Channel: Fixed new livestream iframe plugins.npo: removed due to DRM plugins.lrt: fixed livestreams bastimeyer (1): plugins.welt: fix plugin beardypig (13): plugins.bbciplayer: small change to where the VPID is extracted from (#2376) plugins.goodgame: fix for debug logging error plugins.cdnbg: fix for bstv url plugins.ustvnow: updated to handle new auth, and site design plugin.schoolism: bug fix for videos with subtitles (#2524) stream.dash: use the stream args in the writer and worker session: default https-proxy to the same as http-proxy, can be overridden plugins.beattv: partial fix for the be-at.tv streams tests: test the behaviour of setting http-proxy and https-proxy plugins.twitch: support for different clips URL plugins.wwenetwork: support for new site plugins.ustreamtv: add support for proxying WebSocket connections plugins.wwenetwork: update for small page/api change skulblakka (1): plugins.DLive: New Plugin for dlive.tv (#2419) ssaqua (1): plugins.linelive: new plugin for LINE LIVE (live.line.me) (#2574) ``` ## streamlink 1.1.1 (2019-04-02) This is just a small patch release which fixes a build/deploy issue with the new special wheels for Windows on PyPI. (#2392) [Please see the full changelog of the `1.1.0` release!](https://github.com/streamlink/streamlink/releases/tag/1.1.0) ```text Forrest (1): build: remove cygwin from wheels for Windows (#2393) ``` ## streamlink 1.1.0 (2019-03-31) These are the highlights of Streamlink's first minor release after the 1.0.0 milestone: - several plugin fixes, improvements and new plugin implementations - addition of the `--twitch-disable-ads` parameter for filtering out advertisement segments from Twitch.tv streams (#2372) - DASH stream improvements (#2285) - documentation enhancements (#2292, #2293) - addition of the `{url}` player title variable (#2232) - default player title config for PotPlayer (#2224) - new `streamlinkw` executable on Windows (wheels + installer) (#2326) - Github release assets simplification (#2360) ```text Brian Callahan (1): Add OpenBSD to the installation docs Peter Rowlands (변기호) (2): streams.dash: Support manifest strings in addition to manifest urls (#2285) plugins.facebook: Support manifest strings and tahoe player urls (#2286) Roman Kornev (2): cli.main: Add {url} argument to window --title (#2232) cli.output: Add window title for PotPlayer (#2224) Sebastian Meyer (1): Build additional "streamlinkw" launcher on Windows (#2326) Steve Oswald <30654895+SteveOswald@users.noreply.github.com> (1): plugins.zattoo: Added support for www.1und1.tv (#2274) Vladimir Stavrinov (2): plugins.ntv: new Plugin for ntv.ru (#2351) plugins.live_russia_tv: fix iframe format differences (#2375) back-to (13): plugins.atresplayer: Fixed HLSStream plugins.streamme: Fixed source quality, added title and author plugins.atresplayer: update for new api schema plugins.mitele: plugin update plugins.ustreamtv: handle stream names better, allow '_alt' streams (#2267) plugins.rtve: Fixed content_id search (#2300) plugins.streamme: Fixed null error for 'origin' tests: detect unsupported versions for itertags plugins.pluzz: Fixed Video ID and logging update plugins.pluzz: Fixed regex, they use quotes now. plugins.cdnbg: New domain videochanel.bstv.bg plugins.tf1: Fixed python2.7 ascii error plugins.okru: Fixed Plugin (#2374) plugins.rtpplay: fix _m3u8_re and plugin cleanup bastimeyer (13): docs/install: git+makepkg instead of AUR helper docs/install: rewrite source code and pip section docs/install: shell code blocks, remove prompts docs/install: simplify pip user/system table docs/install: rewrite virtual env section docs/install: move Windows and macOS to the top Add force_verify=true to Twitch OAuth URL plugins.twitch: platform=_ in access_token request TravisCI: don't publish wheels on Github releases stream.hls: refactor M3U8Parser stream.hls: refactor HLSStream{,Worker} plugins.twitch: implement disable-ads parameter Release 1.1.0 beardypig (2): plugins.dogus: support for YouTube embedded streams plugins.bbciplayer: do not try to authenticate when not required lon (1): plugins.crunchyroll: Allow CR's multilingual URLs to be handled (#2304) ``` ## streamlink 1.0.0 (2019-01-30) The celebratory release of Streamlink 1.0.0! *A lot* of hard work has gone into getting Streamlink to where it is. Not only is Streamlink used across multiple applications and platforms, but companies as well. Streamlink started from the inaugural [fork of Livestreamer](https://github.com/chrippa/livestreamer/issues/1427) on September 17th, 2016. Since then, We've hit multiple milestones: - Over 886 PRs - Hit 3,000 commits in Streamlink - Obtaining our first sponsors as well as backers of the project - The creation of our own logo (https://github.com/streamlink/streamlink/issues/1123) Thanks to everyone who has contributed to Streamlink (and our backers)! Without you, we wouldn't be where we are today. **Without further ado, here are the changes in release 1.0.0:** - We have a new icon / logo for Streamlink! (https://github.com/streamlink/streamlink/pull/2165) - Updated dependencies (https://github.com/streamlink/streamlink/pull/2230) - A *ton* of plugin updates. Have a look at [this search query](https://github.com/streamlink/streamlink/pulls?utf8=%E2%9C%93&q=is%3Apr+is%3Aclosed+plugins.+) for all the recent updates. - You can now provide a custom key URI to override HLS streams (https://github.com/streamlink/streamlink/pull/2139). For example: `--hls-segment-key-uri ` - User agents for API communication have been updated (https://github.com/streamlink/streamlink/pull/2194) - Special synonyms have been added to sort "best" and "worst" streams (https://github.com/streamlink/streamlink/pull/2127). For example: `streamlink --stream-sorting-excludes '>=480p' URL best,best-unfiltered` - Process output will no longer show if tty is unavailable (https://github.com/streamlink/streamlink/pull/2090) - We've removed BountySource in favour of our OpenCollective page. If you have any features you'd like to request, please open up an issue with the request and possibly consider backing us! - Improved terminal progress display for wide characters (https://github.com/streamlink/streamlink/pull/2032) - Fixed a bug with dynamic playlists on playback (https://github.com/streamlink/streamlink/pull/2096) - Fixed makeinstaller.sh (https://github.com/streamlink/streamlink/pull/2098) - Old Livestreamer deprecations and API references were removed (https://github.com/streamlink/streamlink/pull/1987) - Dependencies have been updated for Python (https://github.com/streamlink/streamlink/pull/1975) - Newer and more common User-Agents are now used (https://github.com/streamlink/streamlink/pull/1974) - DASH stream bitrates now round-up to the nearest 10, 100, 1000, etc. (https://github.com/streamlink/streamlink/pull/1995) - Updated documentation on issue templates (https://github.com/streamlink/streamlink/pull/1996) - URL have been added for better processing of HTML tags (https://github.com/streamlink/streamlink/pull/1675) - Fixed sort and prog issue (https://github.com/streamlink/streamlink/pull/1964) - Reformatted issue templates (https://github.com/streamlink/streamlink/pull/1966) - Fixed crashing bug with player-continuous-http option (https://github.com/streamlink/streamlink/pull/2234) - Make sure all dev dependencies (https://github.com/streamlink/streamlink/pull/2235) - -r parameter has been replaced for --rtmp-rtmpdump (https://github.com/streamlink/streamlink/pull/2152) **Breaking changes:** - A large number of unmaintained or NSFW plugins have been removed. You can find the PR that implemented that change here: https://github.com/streamlink/streamlink/pull/2003 . See our [CONTRIBUTING.md](https://github.com/streamlink/streamlink/blob/130489c6f5ad15488cd4ff7a25c74bf070f163ec/CONTRIBUTING.md) documentation for plugin policy. ```text Billy2011 (3): streamlink.plugins: replace global http session by self.session.http (#1925) stream.hls_playlist: fix some regex pattern & code optimization (#1918) plugins.filmon: fix NoPluginError, if channel is not an ID (#2005) David Bell (1): Update ine.py (#2171) Forrest (2): Remove bountysource from donation page, update flattr Add a note about specifying the full player path Forrest (2): Feature/plugin request policy update (#1838) Add icon, modify installer, update docs (#2165) Hubcapp (1): Window Titles = Stream Titles + Other Attributes (#1576) Jani Ollikainen (1): Added support for viafree.fi Lukas (1): plugins.zdf_mediathek: use ptmd-template api path (#2233) Maxwell Cody (1): Add ability to specify custom key URI override for HLS streams (#2139) Mohamed El Morabity (1): plugins.pluzz: fixes and francetvinfo.fr support (#2119) Nick Gal (1): Update steam plugin to work with steam.tv urls Petar Kukolj (5): plugins.cubetv: support for live streams on cubetv.sg plugins.ok_live: Changed URL regex to support VoDs plugins.bilibili: Fix plugin after API change plugins.twitch: Add support for {title}, {author} and {category} plugins.skai: Fix plugin after site update Roman (2): [FIX] Debug log arguments string [FIX] Debug log arguments cross-platform Roman Kornev (1): [FIX] Message duplicate Sebastian Meyer (1): Reword+reformat issue templates for consistency (#1966) Stefan de Konink (1): Update the documentation with comments for playing YouTube Live Streams (#2156) Søren Fuglede Jørgensen (1): Update and reactivate plugin for dr.dk Twilight0 (1): plugins.ssh101: Fixed plugin (#1916) Vincent Rozenberg (1): Update npo.py Vinny (1): docs: Added documentation for the Funimation plugin (#2091) Visatouch Deeying (1): Fix crash on missing output.record when using player-continous-http back-to (48): plugins.EuropaPlusTV: Fix for "No connection adapters were found" utils.args: Moved streamlink_cli utils.args into streamlink package tests.plugins: Test that each plugin has a test file (#1885) plugins.zattoo: session update and allow muxed hls / dash streams (#1902) plugins.tv4play: Fix for updated website debug: Added Session Language code as a debug message tests: run Python 3.7 tests on AppVeyor and Travis-CI (#1928) plugins.rtve: Fixed AttributeError 'ZTNRClient' has no 'session' plugins.twitch: Fixed AttributeError and Flake8 plugins.filmon: Fixed AttributeError plugins.crunchyroll: Fixed AttributeError and Flake8 tests.localization: use en_CA instead of en_US for test_equivalent plugins.younow: Fix for session error and plugin cleanup plugins.media_ccc_de: removed plugin plugins.pixiv: use API for the stream URL and cache login cookies plugins.youtube: Added support for {author} and {title} (#1944) docs-CLI: Fix for sort and prog issue plugins.ceskatelevize: Fix for https issues plugins.mjunoon: use a User-Agent api.useragents: use newer and more common User-Agent's script.makeinstaller: use a more recent version of Python and Pycryptodome Removed old Livestreamer deprecations and API references plugins.sportal: Removed old RTMP streams, use HLS plugins.twitch: new video URL regex Removed old or unwanted Streamlink Plugins tests: use unittest instead of pytest for itertags error (#1999) utils.parse_xml: allow a tab for ignore_ns plugins.afreeca: ignore new preloading urls plugins.filmon: use the same cdn for a playlist url (#2074) plugins.teleclubzoom: New plugin for teleclubzoom.ch plugins.oldlivestream: remove plugin, service not available anymore (#2081) travis: increase git clone depth stream.dash_manifest: Fixed bug for dynamic playlists when parent.id is None plugins.ustreamtv: use Desktop instead of Mobile streams (#2082) plugins.youtube: Added support for embedded livestream urls versioneer: always use 7 characters plugins.stv: New Plugin for https://player.stv.tv/live plugins.sbscokr: New Plugin for http://play.sbs.co.kr/onair/pc/index.html plugins.vtvgo: New plugin for https://vtvgo.vn/ travis-ci: Fixed Python 3.8 error's plugins.cdnbg: Update for extra channels (#2186) api.useragents: update User-Agent list plugins.afreeca: use Referer for every connection (#2204) plugins.trt: Added support for https url, fixed logger error plugins.turkuvaz: Added support for channel 'apara' plugins.tvrby: Fixed broken plugin. plugins.youtube: replace "gaming" with "www" subdomain plugins.kanal7: Fixed iframe_re/stream_re, added new domain bastimeyer (6): Fix bug report template Move/rename issue templates Update generic issue template plugins.euronews: fix live stream API URL scheme Fix installer by moving additional files feature: {best,worst}-unfiltered stream synonyms beardypig (23): plugins.live_russia_tv: fix for live streams, and support for VOD plugin args: if args are suppressed, ignore them plugin.tvtoya: refactor, add tests, plugin docs, etc. stream.dash: fix bug where timeline_segments.t can be None stream.hls: only include audio/video streams in MuxedHLSStreams stream.hls: if the primary video stream has audio then include it plugins.facebook: support for videos in posts plugins.steam: api requests now require a session id test for rounding dash stream bitrate stream.dash: make the bitrate name for videos more human friendly plugins.schoolism: add support for assignment feedback videos plugins.senategov: support for hearing streams on senate.gov plugins.stadium: support for live stream on watchstadium.com plugins.funimationnow: workaround for #1899, see #2088 cli: make the HH part of HH:MM:SS options optional plugins.bbciplayer: change in the mediator info in the page layout plugins.btsports: fix for change in login flow plugins.filmon: fix for live tv URLs that start with channel plugin.powerapp: support for "tvs" URLs setup: update requests to latest version and set urllib3 to match CI: make sure all the dev dependencies are up to date plugins.tf1: update to support new DASH streams plugins.tf1: re-add support for HLS with the new API beardypig (7): Test coverage increase (#1646) Handle unicode log message in Python 2 (#1886) Update method for finding YouTube videoIds (#1888) stream.dash: prefer audio streams based on the user's locale (#1927) plugins.openrectv: update to match site changes and title support (#1968) URL builder utils (#1675) cli: disable progress output for -o when no tty is available (#2090) fozzy (1): update regex to support new pattern fozzy (1): plugins.egame: new plugin for egame.qq.com (#2070) jackyzy823 (2): Plugin Request: new plugin for Abema.tv (#1949) Improve terminal progress display for wide characters (#2032) mp107 (1): plugins.ltvlmslv: new Plugin for Latvian live TV channels on ltv.lsm.lv (#1986) qkolj (5): plugins.tamago: support for live streams on player.tamago.live (#2108) plugins.huomao: Fix plugin after website changes (#2134) plugins.metube: Add support for live streams and VoDs on www.metube.id (#2112) plugins.tvibo: Add support for livestreams on player.tvibo.com (#2130) Fix recording added in #920 (#2152) remek30 (1): plugins.toya: support for tvtoya.pl skulblakka (1): [picarto.tv] Fix regarding changed URL (#1935) yoya3312 <40212627+yoya3312@users.noreply.github.com> (1): plugins.youtube: use new "hlsManifestUrl" for Livestreams (#2238) ``` ## streamlink 0.14.2 (2018-06-28) Just a few small fixes in this release. - Fixed Twitch OAuth request flow (https://github.com/streamlink/streamlink/pull/1856) - Fix the tv3cat and vk plugins (https://github.com/streamlink/streamlink/pull/1851, https://github.com/streamlink/streamlink/pull/1874) - VOD supported added to atresplayer plugin (https://github.com/streamlink/streamlink/pull/1852, https://github.com/streamlink/streamlink/pull/1853) - Removed tv8cati and nineanime plugins (https://github.com/streamlink/streamlink/pull/1860, https://github.com/streamlink/streamlink/pull/1863) - Added mjunoon.tv plugin (https://github.com/streamlink/streamlink/pull/1857) ```text NyanKiyoshi (1): Fix 404 error on oauth authorize url back-to (1): plugins.vk: _url_re update, allow embedded content, plugin cleanup (#1874) beardypig (10): plugins.t3cat: update validation rule, refactor plugin a little bit plugins.atresplayer: update to support VOD streams stream.dash: support for SegmentList streams with ranged segments plugins.mjunoon: support for live and vod streams on mjunoon.tv release: fix release notes manual install url plugins.tv8cat: plugin removed - the live broadcast is no longer available plugins.nineanime: no longer supported release: set the date of the release for UTC time plugin: support stream weights returned by DASHStream.parse_manifest ``` ## streamlink 0.14.0 (2018-06-26) Here are the changes to this months release! - Multiple plugin fixes - Bug fixes for DASH streams (https://github.com/streamlink/streamlink/pull/1846) - Updated API call for api.utils hours_minutes_seconds (https://github.com/streamlink/streamlink/pull/1804) - Updated documentation (https://github.com/streamlink/streamlink/pull/1826) - Dict structures fix (https://github.com/streamlink/streamlink/pull/1792) - Reformated help menu (https://github.com/streamlink/streamlink/pull/1754) - Logger fix (https://github.com/streamlink/streamlink/pull/1773) ```text Alexis Murzeau (3): sdist: include tests resources (#1785) tests: freezegun: use object instead of lambda (#1787) rtlxl: use raw string to fix escape sequences (#1786) BZHDeveloper <39899575+BZHDeveloper@users.noreply.github.com> (1): plugins.cnews : separate CNEWS data from CanalPlus plugin. (#1782) MasterofJOKers (1): plugins.sportschau: Fix "No schema supplied" error Mohamed El Morabity (2): plugins.pluzz: support for DASH streams plugins.pluzz: fix HDS streams Mohamed El Morabity (1): [plugins.rtbf] Fix radio streams + DASH support (#1771) Sebastian Meyer (1): Move docs version selection to sidebar (#1802) Twilight0 (2): Convert literal comprehensive dicts to dict contructs Add arguments to __init__ - super to work on Python 2.7.X (#1796) back-to (9): plugins: marked or removed broken plugins plugins.earthcam: Fixed hls_url - No schema supplied. plugins.rte: allow https plugins.VinhLongTV: New plugin for livestreams of thvli.vn docs.thirdparty: Added LiveProxy plugins.tlctr: New Plugin for tlctv.com.tr/canli-izle plugins.bigo: Fix for new channelnames and plugin cleanup (#1797) docs: removed some notes, updated some urls utils.times: hours_minutes_seconds update, twitch automate time offset beardypig (15): help: reformat all the help text so that it is <80 chars plugins.bbciplayer: fix bug is DASH for VOD sdist and wheel release fixes (#1758) plugin.youtube: find video id in page, fall back to API (#1746) logging: rename logger for main back to 'cli' plugins.vaughnlive: support for the HTML flv player plugins.yupptv: support for yupptv with login support plugins.nos: update for new page layout for live and VOD plugins.lrt: add support for Lithuanian National Television plugins.delfi: support for delfi.lt web portal plugins.vrtbe: update to new page layout/API plugins.itvplayer: update to new HTML5 API plugins.atresplayer: support new layout/API stream.dash: use the ID and mime-type to identify a representation plugins.mitele: sometimes ogn is null, html5 pdata endpoint works better beardypig (5): plugins.crunchyroll: refactoring and updated API calls (#1820) Suppress/fix deprecated warnings for Python 3 (#1833) API for plugins to request input from the user (#1827) Steam Broadcast Plugin (#1717) USTV Now (#1840) fozzy (1): fix bug caused by indentation and add support for url pattern like 'xingxiu.panda.tv' ``` ## streamlink 0.13.0 (2018-06-06) Massive release this month! Here are the changes: - Initial MPEG DASH support has been added! (https://github.com/streamlink/streamlink/pull/1637) Many thanks to @beardypig - As always, a *ton* of plugin updates - Updates to our documentation (https://github.com/streamlink/streamlink/pull/1673) - Updates to our logging (https://github.com/streamlink/streamlink/pull/1752) as well as log --quiet options (https://github.com/streamlink/streamlink/pull/1744) (https://github.com/streamlink/streamlink/pull/1720) - Our release script has been updated (https://github.com/streamlink/streamlink/pull/1711) - Support for livestreams when using the `--hls-duration` option (https://github.com/streamlink/streamlink/pull/1710) - Allow streamlink to exit faster when using Ctrl+C (https://github.com/streamlink/streamlink/pull/1658) - Added an OpenCV Face Detection example (https://github.com/streamlink/streamlink/pull/1689) ```text BZHDeveloper (1): plugins.bfmtv : Update regular expression (#1703) Billy (1): plugins.ok_live: fix extraction Billy2011 (2): stream.dash: fix stuttering streams & maybe high CPU load (#1718) stream.akamaihd: fix some log.debug... issues (#1729) Hsiao-Ting Yu (1): Add plugin for www.kingkong.com.tw (#1666) LoneFox78 (1): plugins.tvcatchup: support for https URLs back-to (3): plugins.chaturbate: only open a stream if the url is not empty stream.hls_playlist: removed int check from PROGRAM-ID (#1707) docs: PotPlayer Stdin Pipe beardypig (30): plugins.bbciplayer: enable HD for some channels and speed up start-up plugins.goodgame: update for change in page layout tests: test to ensure each plugin listed in the plugin matrix exists plugins.goodgame: fix bug with streamkey extraction plugins.onetv: add support for 1tv.ru and a few other related sites plugins.rtve: fix for m3u8 stream url formatting example: added an opencv face detection example plugins.europaplus: support for the europaplustv stream plugins.goltelevision: support for the live stream plugins.tvcatchup: add URL tests plugin.ustvnow: plugin to support ustvnow.com hls: support for live streams when using --hls-duration release: update to release script win-installer: add missing isodate module plugins.onetv: fix issues with ctc channels and add DASH support plugins.dailymotion: fix error logging for stream errors logging: set the log level once the plugin config files have been loaded logging: fixed issue with logging from plugins using logging module plugins.onetv: fixed tests plugins.reshet: support for reshet.tv live and VOD streams dash: fix for manifest reload - should be more reliable tests: coverage on src instead of the modules plugins.crunchyroll: switch method of obtaining session id plugins.facebook: remove debugging code plugins: store cookies between sessions (#1724) plugins.facebook: sd_src|hd_src can contain non-dash streams plugins.funimationnow: login support and bug fixes (#1721) logging: do not log when using quiet options (--json, --quiet, etc) dash: fix --json for dash streams and allow custom headers (#1748) logging: when using the trace level, log the timestamp beardypig (21): plugins.filmon: more robust channel_id extraction build: use versioneer to set the build number (#1413) plugins.btsports: add plugin bt sports Allow streamlink to exit faster when using Ctrl-C plugins.tf1: add lci url to the main tf1 domain (#1660) plugins.ine: support for updated site layout docs: add a note about socks4 and socks5 vs socks4a and socks5h (#1655) plugins.gardenersworld: updated page layout plugins.vidio: update to support new layout plugins.btsports: add missing plugin matrix entry and tests plugins.vidio: py2 vs. py3 unicode fix tests: test to ensure that all plugins are listed in the plugin matrix build: use _ instead of + in the Windows installer exe filename Plugin Arguments API (#1638) Change log as markdown refactor (#1667) Add the README file to the Python package (#1665) build: build and sign sdist in travis using an RSA sign-only key (#1701) logging: refactor to use python logging module (#1690) MPEG DASH Support (initial) (#1637) plugins.bbciplayer: add dash support plugins.facebook: support for DASH streams (#1727) hoilc (1): fix checking live status jshir (1): Fix bug 1730, vaughnlive port change yhel (1): Feature/france.tv sport (#1700) ``` ## streamlink 0.12.1 (2018-05-07) Streamlink 0.12.1 Small release to fix a pip / Windows.exe generation bug! ```text Charlie Drage (1): 0.12.0 Release ``` ## streamlink 0.12.0 (2018-05-07) Streamlink 0.12.0 Thanks for all the contributors to this month's release! New updates: - A *ton* of plugin updates (like always! see below for a list of updates) - Ignoring a bunch of useless files when developing (https://github.com/streamlink/streamlink/pull/1570) - A new option to limit the number of fetch retries (https://github.com/streamlink/streamlink/pull/1375) - YouTube has been updated to not use MuxedStream for livestreams (https://github.com/streamlink/streamlink/pull/1556) - Bug fix with ffmpegmux (https://github.com/streamlink/streamlink/pull/1502) - Removed dead plugins and deprecated options (https://github.com/streamlink/streamlink/pull/1546) ```text Alexis Murzeau (2): Avoid use of non-ASCII in dogan plugin Fix test_plugins.py encoding errors in containerized environment (#1582) BZHDeveloper (1): [TF1] Fix plugin (Fixes #1579) (#1606) Charlie Drage (4): Add OpenCollective message to release script Manually update CHANGELOG.rst Remove livestream.patch Update release script Igor Piddubnyi (3): Plugin implementation for live.russia.tv Fix review coments Correctly exit on error James Prance (1): Small tweaks to fix ITV player. Fixes #1622 (#1623) Mattias Amnefelt (1): stream.hls: change --hls-audio-select to take a list and wildcard (#1591) Mohamed El Morabity (1): Add support for international Play TV website Mohamed El Morabity (1): Add support for RTBF Mohamed El Morabity (1): [dailymotion] Fix for new stream data API (#1543) Sean Greenslade (1): Added retry-max option to limit the number of fetch retries. back-to (9): [ffmpegmux] Fixed bug of an invisible terminal [TVRPlus] Fix for hls_re and use headers for HLSStream [streann] Fixed broken plugin Removed some dead plugins and some Deprecated options [youtube] Don't use MuxedStream for livestreams [pixiv] New plugin for sketch.pixiv.net (#1550) [TVP] New Plugin for Telewizja Polska S.A. [build] Fixed AppVeyor build pip10 error (#1605) [ABweb] New plugin for BIS Livestreams of french AB Groupe (#1595) bastimeyer (2): plugins.welt: add plugin Add OS + editor file/directory names to .gitignore beardypig (7): plugins.rtve: add an option to parse_xml to try to fix invalid character entities plugins.vaughnlive: Updated server map plugins.brittv: fixed script layout change build/deploy: do not deploy streamlink-latest, and remove old nightlies (#1624) plugins.brittv: fix issue with stream url extraction, from 7018fc8 (#1625) plugins.raiplay: add user-agent header to stream redirect request plugins.dogan: update for page layout change fozzy (1): update plugin for longzhu.com to support new url pattern steven7851 (1): [app17] Fix HLS URL (#1600) ``` ## streamlink 0.11.0 (2018-03-08) Streamlink 0.11.0! Here's what's new: - Fixed documentation (https://github.com/streamlink/streamlink/pull/1467 and https://github.com/streamlink/streamlink/pull/1468) - Current versions of the OS, Python, Streamlink and Requests are now shown with -l debug (https://github.com/streamlink/streamlink/pull/1374) - ok.ru/live plugin added (https://github.com/streamlink/streamlink/pull/1451) - New option --hls-segment-ignore-names (https://github.com/streamlink/streamlink/pull/1432) - AfreecaTV plugin updates (https://github.com/streamlink/streamlink/pull/1390) - Added support for zattoo recordings (https://github.com/streamlink/streamlink/pull/1480) - Bigo plugin updates (https://github.com/streamlink/streamlink/pull/1474) - Neulion plugin removed due to DMCA notice (https://github.com/streamlink/streamlink/pull/1497) - And many more updates to numerous other plugins! ```text Alexis Murzeau (3): Remove Debian directory docs/install: use sudo for Ubuntu and Solus docs/install: add Debian instructions (#1455) Anton Tykhyy (1): Add ok.ru/live plugin BZHDeveloper (1): [TF1] Fix plugin (#1457) Bruno Ribeiro (1): added cd streamlink Drew J. Sonne (1): [bbciplayer] Fix authentication failures (#1411) Hannes Pétur Eggertsson (1): Ruv plugin updated. Fixes #643. (#1486) Mohamed El Morabity (1): Add support for IDF1 back-to (10): [cli-debug] Show current installed versions with -l debug [hls] New option --hls-segment-ignore-names [cli-debug] Renamed method and small template update [afreeca] Plugin update. - Login for +19 streams --afreeca-username --afreeca-password - Removed 15 sec countdown - Added some error messages - Removed old Global AfreecaTV plugin - Added url tests [zattoo] Added support for zattoo recordings [tests] Fixed metaclass on python 3 [periscope] Fix for variant HLS streams [facebook] mark as broken, they use dash now. Removed furstream: dead website and file was wrong formated UTF8-BOM [codecov] use pytest and upload all data bastimeyer (2): docs: fix table layout on the install page [neulion] Remove plugin. See #1493 beardypig (2): plugins.kanal7: fix for new streaming iframe plugins.foxtr: update regex to match new site layout leshka (1): [goodgame] Fixed url regexp for handling miscellaneous symbols in username. schrobby (1): update from github comments sqrt2 (1): [orf_tvthek] Work around broken HTTP connection persistence (#1420) unnutricious (1): [bigo] update video regex to match current website (#1412) ``` ## streamlink 0.10.0 (2018-01-23) Streamlink 0.10.0! There's been a lot of activity since our November release. Changes: - Multiple plugin updates (too many to list, see below for the plugin changes!) - HLS seeking support (https://github.com/streamlink/streamlink/pull/1303) - Changes to the Windows binary (docs: https://github.com/streamlink/streamlink/pull/1408 minor changes to install directory: https://github.com/streamlink/streamlink/pull/1407) ```text Alexis Murzeau (3): docs: remove flattr-badge.png image Fix various typos in comments and documentation Implement PKCS#7 padding decoding with AES-128 HLS BZHDeveloper (1): [canalplus] Update plugin according to website changes (#1378) Mohamed El Morabity (1): [pluzz] Fix video ID regex for France 3 Régions streams RosadinTV (1): Welcome 2018 (#1410) Sean Greenslade (4): Reworked picarto.tv plugin to deal with website changes. (#1359) Tweaked tigerdile URL regex to allow missing trailing slash. Added tigerdile HLS support and proper API poll for offline streams. Added basic URL tests for tigerdile. back-to (5): [zdf] apiToken update [camsoda] Fixed broken plugin [mixer] moved beam.py to mixer.py file requires two commits, for a proper commit history [mixer] replaced beam.pro with mixer.com [docs] Removed MPlayer2 - Domain expired - Not maintained anymore back-to (13): [BTV] Fixed login return message [qq] New Plugin for live.qq.com [mlgtv] Fixed broken Plugin streamlink/streamlink#1362 [viasat] Added support for urls without a stream_id - removed dead domains from _url_re - added a error message for geo blocking - new regex for stream_id from image url - Removed old embed plugin - try to find an iframe if no stream_id was found. - added tests [streann] Added headers for post request [Dailymotion] Fixed livestream id from channelpage [neulion] renamed ufctv.py to neulion.py [neulion] Updated the ufctv plugin to make it useable for other domains [youtube] added Audio m4a itag 256 and 258 [hls] Don't try to skip a stream if the offset is 0, don't raise KeyError if the m3u8 file is empty this allows the file to reload. [zengatv] New Plugin for zengatv.com [mitele] Update for different api response - fallback if not hls_url was found, just the suffix. - added url tests [youtube] New params for get_video_info (#1423) bastimeyer (2): nsis: restore old install dir, keep multiuser docs: rewrite Windows binaries install section beardypig (12): plugins.vaughnlive: try to guess the stream ID from the channel name plugins.vaughnlive: updated rtmp server map Update server map stream.hls: add options to skip some time at the start/end of VOD streams stream.hls: add option to restart live stream, if possible stream.hls: remove the end offset and replace with duration hls: add absolute start offset and duration options to the HLStream API duratio bug Fix bug with hls start offset = 0 EOL Python 3.3 plugins.kanal7: update to stream player URL config plugins.huya: fix stream URL scheme prefix fozzy (1): fix plugin for bilibili to adapt the new API hicrop <35128217+hicrop@users.noreply.github.com> (1): PEP8 (#1427) steven7851 (1): [Douyutv] fix API xela722 (1): Add plugin for olympicchannel.com (#1353) ``` ## streamlink 0.9.0 (2017-11-14) Streamlink 0.9.0 has been released! This release is mostly code refactoring as well as module inclusion. Features: - Updates to multiple plugins (electrecetv, tvplayer, Teve2, cnnturk, kanald) - SOCKS module being included in the Streamlink installer (PySocks) Many thanks to those who've contributed in this release! ```text Alexis Murzeau (2): docs: add new line before codeblock to fix them Fix sphinx warning on Directive class Charlie Drage (1): Update the release script Emrah Er (1): plugins.canlitv: fix URLs (#1281) Jake Robertson (3): exit with code 130 after a KeyboardInterrupt refactor error code determination unify sys.exit() calls RosadinTV (5): Update eltrecetv.py Update eltrecetv.py Update plugin_matrix.rst Add webcast_india_gov.py Add test_webcast_india_gov.py back-to (3): [zattoo] It won't work with None in Python 3.6, set always a default date instead of None. [liveme] API update (#1298) Ignore WinError 10053 / WSAECONNABORTED beardypig (10): plugins.tvplayer: extract the channel id when logged in as a subscriber installer: include the socks proxy modules plugins.kanal7: update for page layout change and referrer check plugins.turkuvaz: fix some turkuvaz sites and add support for anews plugins.cinergroup: support for different showtv url plugins.dogus/startv: fix dogus sites plugins.dogan: fix for teve2 and cnnturk plugins.dogan: fix for kanald plugins.tvcatchup: HLS source extraction update setup: fix PySocks module dependency ficofabrid <31028711+ficofabrid@users.noreply.github.com> (1): Add a single newline at the end of the file. (#1235) fozzy (1): fix huya.com plugin steven7851 (1): plugins.pandatv: fix APIv3 (#1286) wlerin (1): plugin.showroom: update to new api (#1311) ``` ## streamlink 0.8.1 (2017-09-12) 0.8.1 of Streamlink! 97 commits have occurred since the last release, including a large majority of plugin changes. Here's the outline of what's new: - Multiple plugin fixes (twitch, vaughlive, hitbox, etc.) - Donations! We've gone ahead and joined the Open Collective at https://opencollective.com/streamlink - Multiple doc updates - Support for SOCKS proxies - Code refactoring Many thanks to those who've contributed in this release! ```text Benedikt Gollatz (1): Fix player URL extraction in bloomberg plugin Forrest (1): Update donation docs to note open collective (#1105) Journey (2): Update Arconaitv to new url fix arconai test plugin Pascal Romahn (1): The site always contains the text "does not exist". This should resolve issue https://github.com/streamlink/streamlink/issues/1193 RosadinTV (2): Update Windows portable version documentation Fix documentation font-size Sad Paladin (1): plugins.vk: add support for vk.com vod/livestreams Xavier Damman (1): Added backers and sponsors on the README back-to (5): [zattoo] New plugin for zattoo.com / tvonline.ewe.de / nettv.netcologne.com (#1039) [vidio] Fixed Plugin, new Regex for HLS URL [arconai] Fixed plugin for new website [npo] Update for new website layout, Added HTTPStream support [liveme] url regex update bastimeyer (3): docs: add a third party applications list docs: add an official streamlink applications list Restructure README.md beardypig (17): plugins.brittv: support for live streams on brittv.co.uk plugins.hitbox: fix bug when checking for hosted channels plugins.tvplayer: small update to channel id extraction plugins.vaughnlive: support for the new vaughnlive website layout plugins.vaughnlive: work around for a ssl websocket issue plugins.vaughnlive: drop HLS stream support for vaughnlive plugins.twitch: enable certificate verification for twitch api Resolve InsecurePlatformWarnings for older Python2.7 versions cli: remove the deprecation warnings for some of the http options plugins.vaughnlive: set a user agent for the initial page request plugins.adultswim: fix for some live streams plugins: separated the built-in plugins in to separate plugins cli: support for SOCKS proxies plugins.bbciplayer: fix for page formatting changes and login plugins.cdnbg: support for updated layout and extra channels plugins: add priority ordering to plugins plugins.bbciplayer: support for older VOD streams fozzy (10): remove unused code fix douyutv plugin by using new API update douyutv.py to support multiple rates by steven7851 update HLS Stream name to 'live' update weights for streams fix stream name update stream name, middle and middle2 are of different quality Add support for skai.gr add eol remove unused importing jgilf (2): Update ufctv.py Update ufctv.py sdfwv (1): [bongacams] replace RTMP with HLS Fixed streamlink/streamlink#1074 steven7851 (8): plugins.douyutv: update post data plugins.app17: fix HLS url plugins.app17: RTMPStream is no longer used plugins.app17: return RTMPStream back plugins.douyutv: use douyu open API plugins.app17: new layout plugins.app17: use https plugins.app17: fix wansu cdn url supergonkas (1): Add support for RTP Play (#1051) unnutricious (2): bigo: add support for hls streams bigo: improve plugin url regex ``` ## streamlink 0.7.0 (2017-06-30) 0.7.0 of Streamlink! Since our May release, we've incorporated quite a few changes! Outlined are the major features in this month's release: - Stream types will now be sorted accordingly in terms of quality - TeamLiquid.net Plugin added - Numerous plugin & bug fixes - Updated HomeBrew package - Improved CLI documentation Many thanks to those who've contributed in this release! If you think that this application is helpful, please consider supporting the maintainers by [donating](https://streamlink.github.io/donate.html). ```text Alex Shafer (1): Return sorted list of streams. (#731) Alexandre Hitchcox (1): Allow live channel links without '/c/' prefix Alexis Murzeau (1): docs: fix typo: specifiying, neverthless CatKasha (1): Add MPC-HC x64 in streamlinkrc Forrest (1): Add a few more examples to the player option (#896) Jacob Malmberg (3): Here's the plugin I wrote for teamliquid.net (w/ some help from https://github.com/back-to) Tests for teamliquid plugin Now with RE! Mohamed El Morabity (9): Update for live API changes Add unit tests for Euronews plugin Drop pcyourfreetv plugin Add support for regional France 3 streams Add support for TV5Monde PEP8 Add support for VOD/audio streams Add support for radio.net Ignore unreliable stream status returned by radio.net Sebastian Meyer (1): Homebrew package (#929) back-to (2): [dailymotion] fix for broken .f4m file that is a .m3u8 file (only livestreams) [arte] vod api url update & add new/missing languages bastimeyer (2): docs: fix parameters being linked in code blocks Improve CLI documentation beardypig (1): plugins.hitbox: add support for smashcast.tv beardypig (21): plugins.bbciplayer: update to reflect slight site layout change plugins.bbciplayer: add option to login to a bbc account http_server: handle socket closed exception for Python 2.7 docs: update Sphinx config to fix the rendering of -- docs: pin sphinx to 1.6.+ so that no future changes affect the docs plugins.tvplayer: fix bug with some channels not loading plugins.hitbox: fix new VOD urls, and add support for hosted streams plugins.tvplayer: fix bug with some channels when not authenticated setup: exclude requests version 2.16 through 2.17.1 win32: fix missing modules when using windows installer bbciplayer: fix for api changes to iplayer tvplayer: updated to match change token parameter name plugins.looch: support for live and vod streams on looch.tv plugins.webtv: decrypt the stream URL when applicable plugins.dogan: small api change for teve2.com.tr plugins.kanal7: fix for nested iframes win32: update the dependencies for the windows installer plugins.canlitv: simplified and fixed the m3u8 regex plugins.picarto: support for VOD plugins.ine: update to extract the relocated jwplayer config plugin.ufctv: support for free and premium vod/live streams cirrus (3): Create arconia.py Rename arconia.py to arconai.py Create plugin_matrix.rst steven7851 (4): plugins.app17: fix hls url and support UID page little change plugins.app17: change ROOM_URL [douyu] temporary fix by revert to previously commit (#1015) whizzoo (2): Restore support for RTL XL plugin.rtlxl: Remove spaces from line 14 yhel (1): Don't return an error when the stream is offline yhel (1): Add capability of extracting current sport.francetv stream ``` ## streamlink 0.6.0 (2017-05-11) Another release of Streamlink! We've updated more plugins, improved documentation, and moved out nightly builds to Bintray (S3 was costing *wayyyy* too much). Again, many thanks for those who've contributed! Thank you very much! ```text Daniel Draper (1): Will exit with exit code 1 if stream cannot be opened. (#785) Forrest Alvarez (3): Update readme so users are aware using Streamlink bypasses ads Forgot a ) Make notice more agnostic Mohamed El Morabity (18): Disable HDS streams which are no more available Add support for pc-yourfreetv.com Add support for BFMTV Add support for Cam4 Disable HDS streams for live videos Add support for Bloomberg Add support for Bloomberg Radio live stream Add support for cnews.fr Fix unit tests for canalplus plugin Add authentication token to http queries Add rte.ie/player support Add support for HLS streams Update for new page layout Update for new new page layout Fix for new layout Pluzz platform replaced by new france.tv website Update documentation Always use token generator for streams from france.tv Mohamed El Morabity (1): plugins.brightcove: support for HLS stream URLs with query strings + RTMPE stream URLs (#790) RosadinTV (5): Update plugin_matrix.rst Add telefe.py Add test_plugin_telefe.py Update telefe.py Add support for ElTreceTV (VOD & Live) (#816) Sebastian Meyer (1): Improve contribution guidelines (#772) back-to (9): [chaturbate] New API for HLS url [chaturbate] Fixed python 3.5 bug and added regex tests [VRTbe] new plugin for vrt.be/vrtnu [oldlivestream] New regex for cdn subdomains and embeded streams [tv1channel.org] New Plugin for embeded streams on tv1channel.org [cyro] New plugin for embeded streams from cyro.se [Facebook] Added unittests [ArteTV] new regex, removed rtmp and better result for available streams [NRK.NO] fixed regex for _api_baseurl_re beardypig (15): travis: use pytest to run the tests for coverage Revert "stream.hds: ensure the live edge does not go past the latest fragment" plugins.azubutv: plugin removed plugins.ustreamtv: log timeout errors and adjust retries for polling appveyor: update config to fix builds on Python 3.3 plugin.tvplayer: update to support new site layout plugin.tvplayer: update tests to match new plugin plugins.tvplayer: allow https stream URLs plugins.tvnbg: add support for live streams on tvn.bg plugins.apac: add ustream apac wrapper Deploy nightly builds to Bintray instead of S3 plugins.streann: support for ott.streann.com utils.crypto: fix openssl_decrypt for py27 build: update the bintray release notes for nightlies plugins.streamable: support for videos on streamable.com beardypig (20): plugins.ustreamtv: support for the new ustream.tv API plugins.ustreamtv: add suppot for redirectLocked embedded streams plugins.livecodingtv: renamed to livedu, and updated for new site plugins.ustreamtv: continue to poll the ustream API when streaming plugins.ustreamtv: rename the plugin class back to UStreamTV docs: remove references to python-librtmp plugins.ustream: add some comments plugins.ustreamtv: support for password protected streams plugins.nbc: support vod from nbc.com plugins.nbcsports: add support for nbcsports.com via theplatform stream.hds: ensure the live edge does not go past the latest fragment Dailymotion feature video and backup stream fallback (#773) plugin.gardenersworld: support for VOD on gardenersworld.com plugins.twitch: support for pop-out player URLS and fixed clips tests: cmdline tests can fail if there are some config options set plugins.ustreamtv: fix moduleInfo retry loop cli: add --url option that can be used in config files to set a URL cli: clarification of the --url option cli: add wildcard to --stream-types option plugins.rtve: stop IOError bubbling up on 404 errors wlerin (2): Send Referer and UserAgent headers Fix method decorator zp@users.noreply.github.com (1): New plugin for Facebook 360p streams https://gist.github.com/zp/c461761565dba764c90548758ee5ae9f ``` ## streamlink 0.5.0 (2017-04-04) Streamlink 0.5.0! Lot's of contributions since the last release. As always, lot's of updating to plugins! One of the new features is the addition of Google Drive / Google Docs, you can now stream videos stored on Google Docs. We've also gone ahead and removed dead plugins (sites which have gone down) as well as added pycrypto as a dependency for future plugins. Again, many thanks for those who have contributed! Thank you very much! ```text CallMeJuf (2): Aliez plugin now accepts any TLD (#696) New Periscope URL #748 Daniel Draper (2): More robust url regex for bigo plugin. More robust url regex for bigo plugin, added unittest Josip Ponjavic (4): fix vaugnlive info_url Update archlinux installation instructions and maintainer info setup: choose pycrypto as a dependency using an environment variable Add info about pycrypto and pycountry variables to install doc Mohamed El Morabity (1): plugins.pluzz: fix SWF player URL search to bring back HDS stream support (#679) back-to (5): plugins.camsoda Added support for camsoda.com plugins.canlitv - Added new plugin canlitv Removed dead plugins (#702) plugins.camsoda - Added tests and small update for the plugin plugins.garena - Added new plugin garena beardypig (11): plugins.bbciplayer: add support for BBC iPlayer live and VOD plugins.vaughnlive: updated player version and info URL plugins.vaughnlive: search for player version, etc in the swf file plugins.beam: add support for VOD and HLS streams for live (#694) plugins.bbciplayer: add support for HLS streams utils.l10n: use default locale if the system returns an invalid locale plugins.dailymotion: play the featured video from channel pages plugins.rtve: support for avi/mov VOD streams plugins.googledocs: plugin to support playing videos stored on google docs plugins.googledocs: updated the url regex and added a status check plugins.googledrive: add googledrive support steven7851 (3): plugins.17media: Add support for HTTP stream plugins.17media: fix rtmp stream plugins.douyutv: support vod (#706) ``` ## streamlink 0.4.0 (2017-03-09) 0.4.0 of Streamlink! 114 commits since the last release and *a lot* has changed. In general, we've added some localization as well as an assortment of new plugins. We've also introduced a change for Streamlink to *not* check for new updates each time Streamlink starts. We found this feature annoying as well as delaying the initial start of the stream. This feature can be re-enabled by the command line. The major features of this release are: - New plugins added - Ongoing support to current plugins via bug fixes - Ensure retries to HLS streams - Disable update check Many thanks to all contributors who have contributed in this release! ```text 406NotAcceptable <406NotAcceptable@somewhere> (2): plugins.afreecatv: API changes plugins.connectcast: API changes BackTo (1): plugins.zdf_mediathek Added missing headers for http.get (#653) Charlie Drage (7): Updating the release script. 0.3.1 Release Update release script again to include sdist Fix underlining issue Fix the CHANGELOG.rst 0.3.2 Release Update underscores title release script (#563) Forrest (3): Update license and debian copyright (#515) Add a donation page (#578) Fix up the donate docs (#672) Forrest Alvarez (1): Update license and debian copyright John Smith (1): plugins.bongacams: a few small changes (#429) Mohamed El Morabity (1): Check whether videos are DRM-protected Add log messages when no stream is available Mohamed El Morabity (3): Add support for replay.gulli.fr (#468) plugins.pluzz: add support for ludo.fr and zouzous.fr (#536) Add subtitle support for pluzz plugins (#646) Scott Buettner (1): Fix Crunchyroll string.format in Python 2.6 (#539) Sven (1): Adding Huomao plugin with possibility for different stream qualities. Sven Anderzén (1): Huomao plugin tests (#566) back-to (2): [earthcam] Added HLS, Fixed live RTMP and changes some stuff plugins.ard_mediathek added mediathek.daserste.de support beardypig (74): plugins.schoolism: add support for schoolism.com plugins.earthcam: added support for live and archive cam streams stream.hls_playlist: invalid durations in EXTINF lines are ignored plugins.livecoding: update to support the new domain: liveedu.tv plugins.srgssr: fix playlist reload auth issue Play twitch VOD stream from the beginning even if is still being recorded cli: wait for process to exit, not exit with non-0 error code Fix bug in customized Windows install add a general locale setting which can be used by plugins stream.hls: support external audio tracks plugins.turkuvaz: add referer to the secure token request localization: search for language codes in part2t+part2b+part3 localization: invalid language/country codes are always inequivalent stream.hls: only support external audio tracks if ffmpeg is available installer: include the missing pkg_resources package Rewritten StreamProcess class (#441) plugins.dogus: fix for ntv streams not being found plugins.dogus: add support for eurostartv live stream plugins.twitch: update public API calls to use v5 API (#484) plugins.filmon: support for new site layout (#508) Support for Ceskatelevize streams (#520) Ensure retries with HLS Streams (#522) utils.l10n: add Country/Language classes, use pycountry is the iso modules are not available plugins.crunchyroll: added option to set the session id to a specific value CI: add pycountry for testing plugins.openrectv: add source quality for openrectv utils.l10n: default to en_US when an invalid locale is set fix some python2.6 issues allow failure for python2.6 in travis and update minimum supported python version to 2.7, as well as adding an annoying deprecation warning stream.hls: pick a better default stream language stream.hls: Retry HTTP requests to get the key for HLS streams plugins.openrectv: fixed broken vod support appveyor: use the build.cmd script to install streamlink, so that the sdk can be used if required stream.hls: last chance fallback audio stream: make Stream responsible for generating the stream_url utils.l10n: fix bug in iso3166 country lookup tests: speed up the cmdline tests Remove deprecation warning for invalid escape sequences tests: merged the Localization tests back in to one module plugins.foxtr: adjusted regex for slight site layout change plugins.ard_mediathek: update to support site change stream.hds: warn about streams being protected by DRM plugins.tvrplus: add support for tvrplus.ro live streams plugins.tvrby: support for live streams of Belarus national TV plugins.ovvatv: add support for ovva.tv live streams cli.utils.http_server: avoid "Address already in use" with --player-external-http setup: choose pycountry as a dependency using an environment variable plugins.ovvatv: fix b64decoding bug plugin.mitele: use the default plugin cache plugins.seetv: add support for seetv.tv live streams cli.utils.http_server: ignore errors with socket.shutdown plugins.daisuki: add support for VOD streams from daisuki.net (#609) plugins.daisuki: fix for truncated subtitles cli: disable automatic version checking by default plugins.rtve: update rtve plugin to support VOD (#628) plugins.rtve: return all the available qualities plugins.funimationnow: support for US and UK funimation|now streams (#629) cli: --no-version-check always disables the version check plugins.tvplayer: support for authenticated streams docs: updated the docs for built-in stream parameters utils.l10n: fix for some locales without an official name in pycountry plugins.wwenetwork: support for WWE Network streams plugins.trt: make the url test case insensitive and fix py3 bug plugins.tvplayer: automatically set postcode when required plugins.ard_live: updated to new site layout plugins.vidio: fix for regex, if the url is the english version plugins.animelab: added support for AnimeLab.com VOD plugin.npo: rewrite of plugin to use the new API (#642) plugins.goodgame: support for http URLs docs.donate: drop name headers to subsection level stream.hls: format string name input for parse_variant_playlist plugins.wwenetwork: use the resolution and bitrate in the stream name docs: make the nightly installer link more obvious stream.hls: option to select a specific, non-standard audio channel fozzy (4): update douyutv plugin, use new API update to support different quality fix typo and indent correct typo fozzy (3): Add support for Huya.com in issue #425 (#465) Fix issue #426 on plugins/tga.py (#456) fix douyutv issue #637 (#666) intact (1): Add Rtvs.sk Plugin steven7851 (4): plugins.douyutv: fix room id regex (#514) plugins.pandatv: use Pandatv API v3 (#410) Add plugin for 17app.co (#502) plugins.zhanqi: use new api (#498) wlerin (1): plugins.showroom: add support for showroom-live.com live streams (#633) ``` ## streamlink 0.3.2 (2017-02-10) 0.3.2 release of Streamlink! A minor bug release of 0.3.2 to fix a few issues with stream providers. Thanks to all whom have contributed to this (tiny) release! ```text Charlie Drage (3): Update release script again to include sdist Fix underlining issue Fix the CHANGELOG.rst Sven (1): Adding Huomao plugin with possibility for different stream qualities. beardypig (7): Ensure retries with HLS Streams (#522) utils.l10n: add Country/Language classes, use pycountry is the iso modules are not available plugins.crunchyroll: added option to set the session id to a specific value CI: add pycountry for testing plugins.openrectv: add source quality for openrectv utils.l10n: default to en_US when an invalid locale is set stream.hls: pick a better default stream language intact (1): Add Rtvs.sk Plugin ``` ## streamlink 0.3.1 (2017-02-03) 0.3.1 release of Streamlink A *minor* release, we update our source code upload to *not* include the ffmpeg.exe binary as well as update a multitude of plugins. Thanks again for all the contributions as well as updates! ```text Charlie Drage (1): Updating the release script. Forrest (1): Update license and debian copyright (#515) Forrest Alvarez (1): Update license and debian copyright John Smith (1): plugins.bongacams: a few small changes (#429) Mohamed El Morabity (1): Check whether videos are DRM-protected Add log messages when no stream is available Mohamed El Morabity (1): Add support for replay.gulli.fr (#468) beardypig (20): plugins.schoolism: add support for schoolism.com stream.hls_playlist: invalid durations in EXTINF lines are ignored plugins.livecoding: update to support the new domain: liveedu.tv plugins.srgssr: fix playlist reload auth issue Play twitch VOD stream from the beginning even if is still being recorded cli: wait for process to exit, not exit with non-0 error code Fix bug in customized Windows install add a general locale setting which can be used by plugins stream.hls: support external audio tracks plugins.turkuvaz: add referer to the secure token request localization: search for language codes in part2t+part2b+part3 localization: invalid language/country codes are always inequivalent stream.hls: only support external audio tracks if ffmpeg is available installer: include the missing pkg_resources package Rewritten StreamProcess class (#441) plugins.dogus: fix for ntv streams not being found plugins.dogus: add support for eurostartv live stream plugins.twitch: update public API calls to use v5 API (#484) plugins.filmon: support for new site layout (#508) Support for Ceskatelevize streams (#520) fozzy (1): Add support for Huya.com in issue #425 (#465) steven7851 (1): plugins.douyutv: fix room id regex (#514) ``` ## streamlink 0.3.0 (2017-01-24) Release 0.3.0 of Streamlink! A lot of updates to each plugin (thank you @beardypig !), automated Windows releases, PEP8 formatting throughout Streamlink are some of the few updates to this release as we near a stable 1.0.0 release. Main features are: - Lot's of maintaining / updates to plugins - General bug and doc fixes - Major improvements to development (github issue templates, automatically created releases) ```text Agustín Carrasco (1): Links on crunchy's rss no longer contain the show name in the url (#379) Brainzyy (1): Add basic tests for stream.me plugin (#391) Javier Cantero (2): plugins/twitch: use version v3 of the API plugins/twitch: use kraken URL John Smith (3): Added support for bongacams.com streams (#329) streamlink_cli.main: close stream_fd on exit (#427) streamlink_cli.utils.progress: write new line at finish (#442) Max Riegler (1): plugins.chaturbate: new regex (#457) Michiel Sikma (1): Update PLAYER_VERSION, as old one does not return data. Add ability to use streams with /embed/video in the URL, from embedded players. (#311) Mohamed El Morabity (6): Add support for pluzz.francetv.fr (#343) Fix ArteTV plugin (#385) Add support for Canal+ TV group channels (#416) Update installation instructions for Fedora (#443) Add support for Play TV (#439) Use token generator for HLS streams, as for HDS ones (#466) RosadinTV (1): --can-handle-url-no-redirect parameter added (#333) Stefan Hanreich (1): added chocolatey to the documentation (#380) bastimeyer (3): Automatically create Github releases Set changelog in automated github releases Add a github issue template beardypig (55): plugins.tvcatchup: site layout changed, updated the stream regex to accommodate the change (#338) plugins.streamlive: streamlive.to have added some extra protection to their streams which currently prevents us from capturing them (#339) cli: add command line option to specific logging path for subprocess errorlog plugins.trtspor: added support for trtspor.com (#349) plugins.kanal7: fixed page change in kanal7 live stream (#348) plugins.picarto: Remove the unreliable rtmp stream (#353) packaging: removed the built in backports infavour of including them as dependencies when required (#355) Boost the test coverage a bit (#362) plugins: all regex string should be raw (#361) ci: build and test on Python 3.6 (+3.7 on travis, with allowed failure) (#360) packages.flashmedia: fix bug in AMFMessage (#359) tests: use mock from unittest when available otherwise fallback to mock (#358) stream.hls: try to retry stream segments (#357) tests: add codecov config file (#363) plugins.picarto: updated plugin to use tech_switch divs to find the stream parameters plugins.mitele: support for live streams on mitele.es docs: add a note about python-devel needing to be installed in some cases docs/release: generate the changelog as rst instead of md plugins.adultswim: support https urls use iso 8601 date format for the changelog plugins.tf1: added plugin to support tf1.fr and lci.fr plugins.raiplay: added plugin to support raiplay.it plugins.vaughnlive: updated player version and info URL (#383) plugins.tv8cat: added support for tv8.cat live stream (#390) Fix TF1.fr plugin (#389) plugins.stream: fix a default scheme handling for urls Add support for some Bulgarian live streams (#392) rtmp: fix bug in redirect for rtmp streams plugins.sportal: added support for the live stream on sportal.bg plugins.bnt: update the user agent string for the http requests plugins.ssh101: update to support new site layout Optionally use FFMPEG to mux separate video and audio streams (#224) Support for 4K videos in YouTube (#225) windows-installer: add the version info to the installer file include CHANGELOG.rst instead of .md in the egg stream.hls: output duplicate streams for HLS when multiple streams of the same quality are available stream.ffmpegmux: fix support for avconv, avconv will be used if ffmpeg is not found Adultswin VOD support (#406) Move streamlink_cli.utils.named_pipe in to streamlink.utils plugins.rtve: update plugin to support new streaming method stream.hds: omit HDS streams that are protected by DRM Adultswin VOD fix for live show replays (#418) plugins.rtve: add support for legacy stream URLs installer: remove the streamlink bin dir from %PATH% before installing plugins.twitch: only check hosted channels when playing a live stream docs: tweaks to docs and docs build process Fix iframe detection for BTN/cdn.bg streams (#437) fix some regex that give deprecation warnings in python 3.6 plugins.adultswim: correct behaviour for archived streams plugins.nineanime: add scheme to grabber api url if not present session: add an option to disable Diffie Hellman key exchange plugins.srgssr: added support for srg ssr sites: srf, rts and rsi plugins.srgssr: fixed bug in api URL and fixed akamai urls with authparams cli: try to terminate the player process before killing it (if terminate takes too long) plugins.swisstxt: add support for the SRG SSR sites sports sections fozzy (1): Add plugin for huajiao.com and zhanqi.tv (#334) sqrt2 (1): Fix swf_url in livestream.com plugin (#428) stepshal (1): Remove trailing. stepshal (2): Add blank line after class or function definition (#408) PEP8 (#414) ``` ## streamlink 0.2.0 (2016-12-16) Release 0.2.0 of Streamlink! We've done numerous changes to plugins as well as fixed quite a few which were originally failing. Among these changes are updated docs as well as general UI/UX cleaning with console output. The main features are: - Additional plugins added - Plugin fixes - Cleaned up console output - Additional documentation (contribution, installation instructions) Again, thank you everyone whom contributed to this release! :D ```text Beardypig (6): Turkish Streams Part III (#292) coverage: include streamlink_cli in the coverage, but exclude the vendored packages (#302) Windows command line parsing fix (#300) plugins.atresplayer: add support for live streams on atresplayer.com (#303) Turkish Streams IV (#305) Support for local files (#304) Charlie Drage (2): Spelling error in release script Fix issue with building installer Fishscene (3): Updated homepage Updated README.md Fixed type in README.md. Forrest (3): Modify the browser redirect (#191) Update client ID (#241) Update requests version after bug fix (#239) Josip Ponjavic (1): Add NixOS install instructions Simon Bernier St-Pierre (1): add contributing guidelines bastimeyer (1): Add metadata to Windows installer beardypig (25): plugins.nhkworld: update the plugin to use the new HLS streams plugins.picarto: updated the plugin to use the new javascript and support HLS streams add pycryptodome==3.4.3 to the setup.py dependencies plugins.nineanime: added a plugin to support 9anime.to plugins.nineanime: update the plugin matrix in the docs plugins.atv: add support for the live stream on atv.com.tr include omxplayer in the list of players in the documentation update the player docs with findings from @Junior1544 and @stevekmcc plugins.bigo: support for bigo.tv docs: move pycryptodome to the list of automatically installed libraries in the docs plugins.dingittv: add support for dingit.tv plugins.crunchyroll: support ultra quality for subscribers update URL for docs to point to the github.io page stream.hls: stream the HLS segments out to the player as they are downloaded, decrypting on the fly installer: install the required MS VC++ runtime files beside the python installation (see takluyver/pynsist/pull/87) plugins.bigo: FlashVars regex updated due to site change add some license notices for the bundled libraries plugins.youtube: support additional live urls add support for a few Turkish live streams plugins.foxtr: add support for turkish fox live streams plugins.kralmuzik: basic support for the HLS stream only stream.hds: added option to force akamai authentication plugins.startv: refactored in to a base class, to be used in other plugins that use the same hosting as StarTV plugins.kralmuzik: refactored to use StarTVBase plugins.ntv: added NTV support plugins.atv: add support for a2tv which is very similar to atv plugins.dogan: support for teve2, kanald, dreamtv, and ccnturk via the same plugin plugins.trt: added support for the live channels on trt.net.tr che (1): plugins.twitch: support for clips added ioblank (1): Use ConsoleOutput for run-as-root warning mmetak (3): Update install instruction (#257) Add links for windows portable version. (#299) Add package maintainers to docs. (#301) thatlinuxfur (1): Added tigerdile.com support. (#221) ``` ## streamlink 0.1.0 (2016-11-21) A major update to Streamlink. With this release, we include a Windows binary as well as numerous plugin changes and fixes. The main features are: - Windows binary (and generation!) thanks to the fabulous work by @beardypig - Multiple plugin fixes - Remove unneeded run-as-root (no more warning you when you run as root, we trust that you know what you're doing) - Fix stream quality naming issue ```text Beardypig (13): fix stream quality naming issue with py2 vs. py3, fixing #89 (#96) updated connectcast plugin to support the new rtmp streams; fixes #93 (#95) Fix for erroneous escape coding the livecoding plugin. Fixes #106 (#121) TVPlayer.com: fix for 400 error, correctly set the platform parameter (#123) Added a method to automatically determine the encoding when parsing JSON, if no encoding is provided. (#122) when retry-streams and twitch-disable-hosting arguments are used the stream is retried until a non-hosted stream is found (#125) plugins.goodgame: Update for API change (#130) plugins.adultswim: added a new adultswim.com plugin (#139) plugins.goodgame: restored DDOS protection cookie support (#136) plugins.younow: update API url (#135) plugins.euronew: update to support the new site (#141) plugins.webtv: added a new plugin to support web.tv (#144) plugins.connectcast: fix regex issue with python 3 (#152) Brainzyy (1): Add piczel.tv plugin (courtesy of @intact) (#114) Charlie Drage (1): Update release scripts Erk- (1): Changed the twitch plugin to use https instead of http as discussed in #103 (#104) Forrest (2): Modify the changelog link (#107) Update cli to note a few windows issues (#108) Simon Bernier St-Pierre (1): change icon Simon Bernier St-Pierre (1): finish the installer (#98) Stefan (1): Debian packaging base (#80) Stefan (1): remove run-as-root option, reworded warning #85 (#109) Weslly (1): Fixed afreecatv.com url matching (#90) bastimeyer (2): Improve NSIS installer script Remove shortcut from previous releases on Windows beardypig (8): plugins.cybergame: update to support changes to the live streams on the cybergame.tv website Use pycryptodome inplace of pyCrypto Automated build of the Windows NSIS installer support for relative paths for rtmpdump makeinstaller: install the streamlinkrc file in to the users %APPDATA% directory remove references to livestreamer in the win32 config template stream.rtmpdump: fixed the rtmpdump path issue, introduced in 6bf7fd7 pin requests to <2.12.0 to avoid the strict IDNA2008 validation ethanhlc (1): fixed instance of livestreamer (#99) intact (1): plugins.livestream: Support old player urls mmetak (2): fix vaughnlive.tv info_url (#88) fix vaughnlive.tv info_url (yet again...) (#143) skulblakka (1): Overworked Plugin for ZDF Mediathek (#154) sqrt2 (1): Fix ORF TVthek plugin (#113) tam1m (1): Fix zdf_mediathek TypeError (#156) ``` ## streamlink 0.0.2 (2016-10-12) The second ever release of Streamlink! In this release we've not only set the stepping stone for the further development of Streamlink (documentation site updated, CI builds working) but we're already fixing bugs and implementing features past the initial fork of livestreamer. The main features of this release are: - New windows build available and generated via pyinstaller - Multiple provider bug fixes (twitch, picarto, itvplayer, crunchyroll, periscope, douyutv) - Updated and reformed documentation which also includes our site https://streamlink.github.io As always, below is a `git shortlog` of all changes from the previous release of Streamlink (0.0.1) to now (0.0.2). ```text Brainzyy (1): add stream.me to the docs Charlie Drage (9): Add script to generate authors list / update authors Add release script Get setup.py ready for a release. Revert "Latest fix to plugin from livestreamer" 0.0.1 Release Update the README with installation notes Update copyright author Update plugin description on README It's now 2016 Forrest (1): Add a coverage file (#54) Forrest Alvarez (4): Modify release for streamlink Remove faraday from travis run Remove tox Add the code coverage badge Latent Logic (1): Picarto plugin: multistream workaround (fixes #50) Maschmi (1): added travis build status badge fixes #74 (#76) Randy Taylor (1): Fix typo in issues docs and improve wording (#61) Simon Bernier St-Pierre (8): add script to build & copy the docs move makedocs.sh to script/ Automated docs updates via travis-ci prevent the build from hanging fix automated commit message add streamboat to the docs disable docs on pull requests twitch.tv: add option to disable hosting Simon Bernier St-Pierre (2): Don't delete everything if docs build fail (#62) Create install script for pynsist (#27) beardypig (3): TVPlayer plugin supports the latest version of the website crunchyroll: decide if to parse the stream links as HLS variant playlist or plain old HLS stream (fixes #70) itvplayer: updated the productionId extraction method boda2004 (1): fixed periscope live streaming and allowed url re (#79) ethanhlc (1): fixed instances of chrippa/streamlink to streamlink/streamlink scottbernstein (1): Latest fix to plugin from livestreamer steven7851 (1): Update plugin.douyutv ``` ## streamlink 0.0.1 (2016-09-23) The first release of Streamlink! This is the first release from the initial fork of Livestreamer. We aim to have a concise, fast review process and progress in terms of development and future releases. Below is a `git shortlog` of all commits since the last change within Livestream (hash ab80dbd6560f6f9835865b2fc9f9c6015aee5658). This will serve as a base-point as we continue development of "Streamlink". New releases will include a list of changes as we add new features / code refactors to the existing code-base. ```text Agustin Carrasco (2): plugins.crunchyroll: added support for locale selection plugins.crunchyroll: use locale parameter on the header's user-agent as well Alan Love (3): added support for livecoding.tv removed printing updated plugin matrix Alexander (1): channel info url change in afreeca plugin Andreas Streichardt (1): Add Sportschau Anton (2): goodgame ddos validation add stream_id with words Benedikt Gollatz (1): Add support for ORF TVthek livestreams and VOD segments Benoit Dien (1): Meerkat plugin Brainzyy (1): fix azubu.tv plugin Charlie Drage (9): Update the README Fix travis Rename instances of "livestreamer" to "streamlink" Fix travis Add script to generate authors list / update authors Get setup.py ready for a release. Add release script Revert "Latest fix to plugin from livestreamer" 0.0.0 Release Charmander <~@charmander.me> (1): plugins.picarto: Update for API and URL change Chris-Werner Reimer (1): fix vaughnlive plugin #897 Christopher Rosell (7): plugins.twitch: Handle subdomains with dash in them, e.g. en-gb. cli: Close output on exit. Show a brief usage when no option is specified. cli: Fix typo. travis: Use new artifacts tool. docs: Fix readthedocs build. travis: Build installer exe aswell. Daniel Meißner (2): plugin: added media_ccc_de api and protocol changes docs/plugin_matrix: removed needless characters Dominik Sokal (1): plugins.afreeca: fix stream Ed Holohan (1): Quick hack to handle Picarto changes Emil Stahl (1): Add support for viafree.dk Erik G (7): Added plugin for Dplay. Added plugin for Dplay and removed sbsdiscovery plugin. Add HLS support, adjust API schema, no SSL verify Add pvswf parameter to HDS stream parser Fix Video ID matching, add .no & .dk support, add error handling Match new URL, add HDS support, handle incorrect geolocation Add API support Fat Deer (1): Update pandatv.py Forrest Alvarez (3): Add some python releases Add coveralls to after_success Remove artifacts Guillaume Depardon (1): Now catching socket errors on send Javier Cantero (1): Add new parameter to Twitch usher URL Jeremy Symon (2): Sort list of streams by quality Avoid sorting streams twice Jon Bergli Heier (2): plugins.nrk: Updated for webpage changes. plugins.nrk: Fixed _id_re regex not matching series URLs. Kari Hänninen (7): Use client ID for twitch.tv API calls Revert "update INFO_URL for VaughnLive" Remove spurious print statement that made the plugin incompatible with python 3. livecoding.tv: fix breakage ("TypeError: cannot use a string pattern on a bytes-like object") sportschau: Fix breakage ("TypeError: a bytes-like object is required, not 'str'"). Also remove debug output. Update the plugin matrix Bump version to 1.14.0-rc1 Marcus Soll (2): Added plugin for blip.tv VOD Updated blip.tv plugin Mateusz Starzak (1): Update periscope.py Michael Copland (1): Fixed weighting of Twitch stream names Michael Hoang (1): Add OPENREC.tv plugin and chmod 2 files Michiel (1): Support for Tour de France stream Paul LaMendola (2): Maybe fixed ustream validation failure. More strict test for weird stream. Pavlos Touboulidis (2): Add antenna.gr plugin Update plugin matrix for antenna Robin Schroer (1): azubutv: set video_player to None if stream is offline Seth Creech (1): Added logic to support host mode Simon Bernier St-Pierre (5): update the streamup.com plugin support virtualenv update references to livestreamer add stream.me plugin add streamboat plugin Summon528 (1): add support to afreecatv.com.tw Swirt (2): Picarto plugin: update RTMPStream-settings Picarto plugin: update RTMPStream-settings Tang (1): New provider: live.bilibili.com Warnar Boekkooi (1): NPO token fix WeinerRinkler (2): First version Error fixed when streamer offline or invalid blxd (5): fixed tvcatchup.com plugin, the website layout changed and the method to find the stream URLs needed to be updated. tvcatchup now returns a variant playlist tvplayer.com only works with a browser user agent not all channels return hlsvariant playlists add user agent header to the tvcatchup plugin chvrn (4): added expressen plugin added expressen plugin update() => assign with subscript added entry for expressen e00E (1): Fix Twitch plugin not working because bandwith was parsed as an int when it is really a float fat deer (1): Add Panda.tv Plugin. fcicq (1): add afreecatv.jp support hannespetur (8): plugin for Ruv - the Icelandic national television - was added removed print statements and started to use quality key as audio if the url extensions is mp3 the plugin added to the plugin matrix removed unused import alphabetical order is hard removed redundant assignments of best/worst quality HLS support added for the Ruv plugin Ruv plugin: returning generators instead of a dict int3l (1): Refactoring and update for the VOD support intact (21): plugins.artetv: Update json regex Updated douyutv.com plugin Added plugin for streamup.com plugins.streamupcom: Check live status plugins.streamupcom: Update for API change plugins.streamupcom: Update for API change plugins.dailymotion: Add HLS streams support plugins.npo: Fix Python 3 compatibility plugins.livestream: Prefer standard SWF players plugins.tga: Support more streams plugins.tga: Fix offline streams plugins.vaughnlive: Fix INFO_URL Added plugin for vidio.com plugins.vaughnlive: Update for API change plugins.vaughnlive: Fix app for some ingest servers plugins.vaughnlive: Remove debug print plugins.vaughnlive: Lowercase channel name plugins.vaughnlive: Update for API change plugins.vaughnlive: Update for API change plugins.livestream: Tolerate missing swf player URL plugins.livestream: Fix player URL jkieberk (1): Change Fedora Package Manager from Yum to Dnf kviktor (2): plugins: mediaklikk.hu stream and video support update mediaklikk plugin livescope (1): Add VOD/replay support for periscope.tv liz1rgin (2): Fix goodgame find Streame Update goodgame.py maop (1): Add Beam.pro plugin. mindhalt (1): Update redirect URI after successful twitch auth neutric (1): Update issues.rst nitpicker (2): I doesn't sign the term of services, so I doesnt violate! update INFO_URL for VaughnLive oyvindln (1): Allow https urls for nrk.no. ph0o (1): Create servustv.py pulviscriptor (1): GoodGame URL parse fix scottbernstein (1): Latest fix to plugin from livestreamer steven7851 (16): plugins.douyutv: Use new api. update douyu fix cdn.. fix for Python 3.x.. use mobile api for reducing code fix for non number channel add middle and low quality fix quality fix room id regex make did by UUID module fix channel on event more retries for redirection remove useless lib try to support event page use https protocol Update plugin.douyutv trocknet (1): plugins.afreeca: Fix HLS stream. whizzoo (2): Add RTLXL plugin Add RTLXL plugin wolftankk (3): get azubu live status from api use new api get stream info fix video_player error ``` ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/LICENSE0000644000175100001710000000250700000000000014236 0ustar00runnerdockerCopyright (c) 2011-2016, Christopher Rosell Copyright (c) 2016-2022, Streamlink Team All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/MANIFEST.in0000644000175100001710000000064000000000000014763 0ustar00runnerdockerinclude AUTHORS include CHANGELOG.md include README.md include LICENSE* include *requirements.txt include src/streamlink/_version.py include src/streamlink/plugins/.removed include versioneer.py recursive-include completions * recursive-include docs * recursive-include examples * recursive-include tests * prune docs/_build include docs/_build/man/* prune */__pycache__ global-exclude *.pyc *~ *.bak *.swp *.pyo ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643136177.0728405 streamlink-3.1.1/PKG-INFO0000644000175100001710000001661600000000000014334 0ustar00runnerdockerMetadata-Version: 2.1 Name: streamlink Version: 3.1.1 Summary: Streamlink is a command-line utility that extracts streams from various services and pipes them into a video player of choice. Home-page: https://github.com/streamlink/streamlink Author: Streamlink Author-email: streamlink@protonmail.com License: Simplified BSD Project-URL: Documentation, https://streamlink.github.io/ Project-URL: Tracker, https://github.com/streamlink/streamlink/issues Project-URL: Source, https://github.com/streamlink/streamlink Project-URL: Funding, https://opencollective.com/streamlink Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: License :: OSI Approved :: BSD License Classifier: Environment :: Console Classifier: Intended Audience :: End Users/Desktop Classifier: Operating System :: POSIX Classifier: Operating System :: MacOS Classifier: Operating System :: Microsoft :: Windows Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Multimedia :: Sound/Audio Classifier: Topic :: Multimedia :: Video Classifier: Topic :: Utilities Requires-Python: <4,>=3.6 Description-Content-Type: text/markdown License-File: LICENSE # [Streamlink][streamlink-website] [![Github build status][workflow-status-badge]][workflow-status] [![codecov.io][codecov-coverage-badge]][codecov-coverage] [![Backers on Open Collective][opencollective-backers-badge]](#backers) [![Sponsors on Open Collective][opencollective-sponsors-badge]](#sponsors) Streamlink is a CLI utility which pipes video streams from various services into a video player, such as VLC. The main purpose of streamlink is to avoid resource-heavy and unoptimized websites, while still allowing the user to enjoy various streamed content. Streamlink is a fork of the [Livestreamer][livestreamer] project. Please note that by using this application you're bypassing ads run by sites such as Twitch.tv. Please consider donating or paying for subscription services when they are available for the content you consume and enjoy. # [Installation][streamlink-installation] Please refer to our documentation for different ways to install Streamlink: - [Windows][streamlink-installation-windows] - [macOS][streamlink-installation-macos] - [Linux and BSD][streamlink-installation-linux-and-bsd] - [PyPI package and source code][streamlink-installation-pypi-source] # Features Streamlink is built upon a plugin system which allows support for new services to be easily added. Most of the big streaming services are supported, such as: - [Twitch](https://www.twitch.tv) - [YouTube](https://www.youtube.com) - [Livestream](https://livestream.com) - [Dailymotion](https://www.dailymotion.com) ... and many more. A full list of plugins currently included can be found on the [plugin page][streamlink-plugins]. # Quickstart After installing, simply use: ``` streamlink STREAMURL best ``` The default behavior of Streamlink is to play back streams in the VLC player. For more in-depth usage and install instructions, please refer to the [detailed documentation][streamlink-documentation]. # Contributing All contributions are welcome. Feel free to open a new thread on the issue tracker or submit a new pull request. Please read [CONTRIBUTING.md][contributing] first. Thanks! [![Contributors][opencollective-contributors]][contributors] ## Backers Thank you to all our backers! \[[Become a backer][opencollective-backer]\] [![Backers on Open Collective][opencollective-backers-image]][opencollective-backers] ## Sponsors Support this project by becoming a sponsor. Your logo will show up here with a link to your website. \[[Become a sponsor][opencollective-sponsor]\] [![Open Collective Streamlink Sponsor](https://opencollective.com/streamlink/sponsor/0/avatar.svg)](https://opencollective.com/streamlink/sponsor/0/website) [![Open Collective Streamlink Sponsor](https://opencollective.com/streamlink/sponsor/1/avatar.svg)](https://opencollective.com/streamlink/sponsor/1/website) [![Open Collective Streamlink Sponsor](https://opencollective.com/streamlink/sponsor/2/avatar.svg)](https://opencollective.com/streamlink/sponsor/2/website) [![Open Collective Streamlink Sponsor](https://opencollective.com/streamlink/sponsor/3/avatar.svg)](https://opencollective.com/streamlink/sponsor/3/website) [![Open Collective Streamlink Sponsor](https://opencollective.com/streamlink/sponsor/4/avatar.svg)](https://opencollective.com/streamlink/sponsor/4/website) [![Open Collective Streamlink Sponsor](https://opencollective.com/streamlink/sponsor/5/avatar.svg)](https://opencollective.com/streamlink/sponsor/5/website) [![Open Collective Streamlink Sponsor](https://opencollective.com/streamlink/sponsor/6/avatar.svg)](https://opencollective.com/streamlink/sponsor/6/website) [![Open Collective Streamlink Sponsor](https://opencollective.com/streamlink/sponsor/7/avatar.svg)](https://opencollective.com/streamlink/sponsor/7/website) [![Open Collective Streamlink Sponsor](https://opencollective.com/streamlink/sponsor/8/avatar.svg)](https://opencollective.com/streamlink/sponsor/8/website) [![Open Collective Streamlink Sponsor](https://opencollective.com/streamlink/sponsor/9/avatar.svg)](https://opencollective.com/streamlink/sponsor/9/website) [streamlink-website]: https://streamlink.github.io [streamlink-plugins]: https://streamlink.github.io/plugin_matrix.html [streamlink-documentation]: https://streamlink.github.io/cli.html [streamlink-installation]: https://streamlink.github.io/install.html [streamlink-installation-windows]: https://streamlink.github.io/install.html#windows [streamlink-installation-macos]: https://streamlink.github.io/install.html#macos [streamlink-installation-linux-and-bsd]: https://streamlink.github.io/install.html#linux-and-bsd [streamlink-installation-pypi-source]: https://streamlink.github.io/install.html#pypi-package-and-source-code [livestreamer]: https://github.com/chrippa/livestreamer [contributing]: https://github.com/streamlink/streamlink/blob/master/CONTRIBUTING.md [changelog]: https://github.com/streamlink/streamlink/blob/master/CHANGELOG.rst [contributors]: https://github.com/streamlink/streamlink/graphs/contributors [workflow-status]: https://github.com/streamlink/streamlink/actions?query=event%3Apush [workflow-status-badge]: https://github.com/streamlink/streamlink/workflows/Test,%20build%20and%20deploy/badge.svg?branch=master&event=push [codecov-coverage]: https://codecov.io/github/streamlink/streamlink?branch=master [codecov-coverage-badge]: https://codecov.io/github/streamlink/streamlink/coverage.svg?branch=master [opencollective-contributors]: https://opencollective.com/streamlink/contributors.svg?width=890 [opencollective-backer]: https://opencollective.com/streamlink#backer [opencollective-backers]: https://opencollective.com/streamlink#backers [opencollective-backers-image]: https://opencollective.com/streamlink/backers.svg?width=890 [opencollective-sponsor]: https://opencollective.com/streamlink#sponsor [opencollective-backers-badge]: https://opencollective.com/streamlink/backers/badge.svg [opencollective-sponsors-badge]: https://opencollective.com/streamlink/sponsors/badge.svg ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/README.md0000644000175100001710000001363400000000000014513 0ustar00runnerdocker# [Streamlink][streamlink-website] [![Github build status][workflow-status-badge]][workflow-status] [![codecov.io][codecov-coverage-badge]][codecov-coverage] [![Backers on Open Collective][opencollective-backers-badge]](#backers) [![Sponsors on Open Collective][opencollective-sponsors-badge]](#sponsors) Streamlink is a CLI utility which pipes video streams from various services into a video player, such as VLC. The main purpose of streamlink is to avoid resource-heavy and unoptimized websites, while still allowing the user to enjoy various streamed content. Streamlink is a fork of the [Livestreamer][livestreamer] project. Please note that by using this application you're bypassing ads run by sites such as Twitch.tv. Please consider donating or paying for subscription services when they are available for the content you consume and enjoy. # [Installation][streamlink-installation] Please refer to our documentation for different ways to install Streamlink: - [Windows][streamlink-installation-windows] - [macOS][streamlink-installation-macos] - [Linux and BSD][streamlink-installation-linux-and-bsd] - [PyPI package and source code][streamlink-installation-pypi-source] # Features Streamlink is built upon a plugin system which allows support for new services to be easily added. Most of the big streaming services are supported, such as: - [Twitch](https://www.twitch.tv) - [YouTube](https://www.youtube.com) - [Livestream](https://livestream.com) - [Dailymotion](https://www.dailymotion.com) ... and many more. A full list of plugins currently included can be found on the [plugin page][streamlink-plugins]. # Quickstart After installing, simply use: ``` streamlink STREAMURL best ``` The default behavior of Streamlink is to play back streams in the VLC player. For more in-depth usage and install instructions, please refer to the [detailed documentation][streamlink-documentation]. # Contributing All contributions are welcome. Feel free to open a new thread on the issue tracker or submit a new pull request. Please read [CONTRIBUTING.md][contributing] first. Thanks! [![Contributors][opencollective-contributors]][contributors] ## Backers Thank you to all our backers! \[[Become a backer][opencollective-backer]\] [![Backers on Open Collective][opencollective-backers-image]][opencollective-backers] ## Sponsors Support this project by becoming a sponsor. Your logo will show up here with a link to your website. \[[Become a sponsor][opencollective-sponsor]\] [![Open Collective Streamlink Sponsor](https://opencollective.com/streamlink/sponsor/0/avatar.svg)](https://opencollective.com/streamlink/sponsor/0/website) [![Open Collective Streamlink Sponsor](https://opencollective.com/streamlink/sponsor/1/avatar.svg)](https://opencollective.com/streamlink/sponsor/1/website) [![Open Collective Streamlink Sponsor](https://opencollective.com/streamlink/sponsor/2/avatar.svg)](https://opencollective.com/streamlink/sponsor/2/website) [![Open Collective Streamlink Sponsor](https://opencollective.com/streamlink/sponsor/3/avatar.svg)](https://opencollective.com/streamlink/sponsor/3/website) [![Open Collective Streamlink Sponsor](https://opencollective.com/streamlink/sponsor/4/avatar.svg)](https://opencollective.com/streamlink/sponsor/4/website) [![Open Collective Streamlink Sponsor](https://opencollective.com/streamlink/sponsor/5/avatar.svg)](https://opencollective.com/streamlink/sponsor/5/website) [![Open Collective Streamlink Sponsor](https://opencollective.com/streamlink/sponsor/6/avatar.svg)](https://opencollective.com/streamlink/sponsor/6/website) [![Open Collective Streamlink Sponsor](https://opencollective.com/streamlink/sponsor/7/avatar.svg)](https://opencollective.com/streamlink/sponsor/7/website) [![Open Collective Streamlink Sponsor](https://opencollective.com/streamlink/sponsor/8/avatar.svg)](https://opencollective.com/streamlink/sponsor/8/website) [![Open Collective Streamlink Sponsor](https://opencollective.com/streamlink/sponsor/9/avatar.svg)](https://opencollective.com/streamlink/sponsor/9/website) [streamlink-website]: https://streamlink.github.io [streamlink-plugins]: https://streamlink.github.io/plugin_matrix.html [streamlink-documentation]: https://streamlink.github.io/cli.html [streamlink-installation]: https://streamlink.github.io/install.html [streamlink-installation-windows]: https://streamlink.github.io/install.html#windows [streamlink-installation-macos]: https://streamlink.github.io/install.html#macos [streamlink-installation-linux-and-bsd]: https://streamlink.github.io/install.html#linux-and-bsd [streamlink-installation-pypi-source]: https://streamlink.github.io/install.html#pypi-package-and-source-code [livestreamer]: https://github.com/chrippa/livestreamer [contributing]: https://github.com/streamlink/streamlink/blob/master/CONTRIBUTING.md [changelog]: https://github.com/streamlink/streamlink/blob/master/CHANGELOG.rst [contributors]: https://github.com/streamlink/streamlink/graphs/contributors [workflow-status]: https://github.com/streamlink/streamlink/actions?query=event%3Apush [workflow-status-badge]: https://github.com/streamlink/streamlink/workflows/Test,%20build%20and%20deploy/badge.svg?branch=master&event=push [codecov-coverage]: https://codecov.io/github/streamlink/streamlink?branch=master [codecov-coverage-badge]: https://codecov.io/github/streamlink/streamlink/coverage.svg?branch=master [opencollective-contributors]: https://opencollective.com/streamlink/contributors.svg?width=890 [opencollective-backer]: https://opencollective.com/streamlink#backer [opencollective-backers]: https://opencollective.com/streamlink#backers [opencollective-backers-image]: https://opencollective.com/streamlink/backers.svg?width=890 [opencollective-sponsor]: https://opencollective.com/streamlink#sponsor [opencollective-backers-badge]: https://opencollective.com/streamlink/backers/badge.svg [opencollective-sponsors-badge]: https://opencollective.com/streamlink/sponsors/badge.svg ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643136177.0168402 streamlink-3.1.1/completions/0000755000175100001710000000000000000000000015561 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643136177.0208402 streamlink-3.1.1/completions/bash/0000755000175100001710000000000000000000000016476 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136169.0 streamlink-3.1.1/completions/bash/streamlink0000644000175100001710000002137000000000000020575 0ustar00runnerdocker# AUTOMATCALLY GENERATED by `shtab` _shtab_streamlink_cli_option_strings=('-h' '--help' '-V' '--version' '--plugins' '--plugin-dirs' '--can-handle-url' '--can-handle-url-no-redirect' '--config' '-l' '--loglevel' '--logfile' '-Q' '--quiet' '-j' '--json' '--auto-version-check' '--version-check' '--locale' '--interface' '-4' '--ipv4' '-6' '--ipv6' '-p' '--player' '-a' '--player-args' '-v' '--verbose-player' '-n' '--player-fifo' '--fifo' '--player-http' '--player-continuous-http' '--player-external-http' '--player-external-http-port' '--player-passthrough' '--player-no-close' '-t' '--title' '-o' '--output' '-f' '--force' '--force-progress' '-O' '--stdout' '-r' '--record' '-R' '--record-and-pipe' '--fs-safe-rules' '--url' '--default-stream' '--stream-url' '--retry-streams' '--retry-max' '--retry-open' '--stream-types' '--stream-priority' '--stream-sorting-excludes' '--ringbuffer-size' '--stream-segment-attempts' '--stream-segment-threads' '--stream-segment-timeout' '--stream-timeout' '--mux-subtitles' '--hls-live-edge' '--hls-segment-stream-data' '--hls-playlist-reload-attempts' '--hls-playlist-reload-time' '--hls-segment-ignore-names' '--hls-segment-key-uri' '--hls-audio-select' '--hls-start-offset' '--hls-duration' '--hls-live-restart' '--ffmpeg-ffmpeg' '--ffmpeg-verbose' '--ffmpeg-verbose-path' '--ffmpeg-fout' '--ffmpeg-video-transcode' '--ffmpeg-audio-transcode' '--ffmpeg-copyts' '--ffmpeg-start-at-zero' '--http-proxy' '--http-cookie' '--http-header' '--http-query-param' '--http-ignore-env' '--http-no-ssl-verify' '--http-disable-dh' '--http-ssl-cert' '--http-ssl-cert-crt-key' '--http-timeout' '--afreeca-username' '--afreeca-password' '--afreeca-purge-credentials' '--bbciplayer-username' '--bbciplayer-password' '--bbciplayer-hd' '--clubbingtv-username' '--clubbingtv-password' '--crunchyroll-username' '--crunchyroll-password' '--crunchyroll-purge-credentials' '--crunchyroll-session-id' '--funimation-email' '--funimation-password' '--funimation-language' '--niconico-email' '--niconico-password' '--niconico-user-session' '--niconico-purge-credentials' '--niconico-timeshift-offset' '--openrectv-email' '--openrectv-password' '--pixiv-sessionid' '--pixiv-devicetoken' '--pixiv-purge-credentials' '--pixiv-performer' '--sbscokr-id' '--schoolism-email' '--schoolism-password' '--schoolism-part' '--steam-email' '--steam-password' '--streann-url' '--twitcasting-password' '--twitch-disable-hosting' '--twitch-disable-ads' '--twitch-disable-reruns' '--twitch-low-latency' '--twitch-api-header' '--ustream-password' '--ustvnow-username' '--ustvnow-password' '--wwenetwork-email' '--wwenetwork-password' '--yupptv-boxid' '--yupptv-yuppflixtoken' '--yupptv-purge-credentials' '--zattoo-email' '--zattoo-password' '--zattoo-purge-credentials' '--zattoo-stream-types') _shtab_streamlink_cli__l_choices='none critical error warning info debug trace' _shtab_streamlink_cli___loglevel_choices='none critical error warning info debug trace' _shtab_streamlink_cli___fs_safe_rules_choices='POSIX Windows' _shtab_streamlink_cli___funimation_language_choices='en ja english japanese' _shtab_streamlink_cli__h_nargs=0 _shtab_streamlink_cli___help_nargs=0 _shtab_streamlink_cli__V_nargs=0 _shtab_streamlink_cli___version_nargs=0 _shtab_streamlink_cli___plugins_nargs=0 _shtab_streamlink_cli__Q_nargs=0 _shtab_streamlink_cli___quiet_nargs=0 _shtab_streamlink_cli__j_nargs=0 _shtab_streamlink_cli___json_nargs=0 _shtab_streamlink_cli___version_check_nargs=0 _shtab_streamlink_cli__4_nargs=0 _shtab_streamlink_cli___ipv4_nargs=0 _shtab_streamlink_cli__6_nargs=0 _shtab_streamlink_cli___ipv6_nargs=0 _shtab_streamlink_cli__v_nargs=0 _shtab_streamlink_cli___verbose_player_nargs=0 _shtab_streamlink_cli__n_nargs=0 _shtab_streamlink_cli___player_fifo_nargs=0 _shtab_streamlink_cli___fifo_nargs=0 _shtab_streamlink_cli___player_http_nargs=0 _shtab_streamlink_cli___player_continuous_http_nargs=0 _shtab_streamlink_cli___player_external_http_nargs=0 _shtab_streamlink_cli___player_no_close_nargs=0 _shtab_streamlink_cli__f_nargs=0 _shtab_streamlink_cli___force_nargs=0 _shtab_streamlink_cli___force_progress_nargs=0 _shtab_streamlink_cli__O_nargs=0 _shtab_streamlink_cli___stdout_nargs=0 _shtab_streamlink_cli___stream_url_nargs=0 _shtab_streamlink_cli___mux_subtitles_nargs=0 _shtab_streamlink_cli___hls_segment_stream_data_nargs=0 _shtab_streamlink_cli___hls_live_restart_nargs=0 _shtab_streamlink_cli___ffmpeg_verbose_nargs=0 _shtab_streamlink_cli___ffmpeg_copyts_nargs=0 _shtab_streamlink_cli___ffmpeg_start_at_zero_nargs=0 _shtab_streamlink_cli___http_ignore_env_nargs=0 _shtab_streamlink_cli___http_no_ssl_verify_nargs=0 _shtab_streamlink_cli___http_disable_dh_nargs=0 _shtab_streamlink_cli___http_ssl_cert_crt_key_nargs=2 _shtab_streamlink_cli___afreeca_purge_credentials_nargs=0 _shtab_streamlink_cli___bbciplayer_hd_nargs=0 _shtab_streamlink_cli___crunchyroll_password_nargs=? _shtab_streamlink_cli___crunchyroll_purge_credentials_nargs=0 _shtab_streamlink_cli___niconico_purge_credentials_nargs=0 _shtab_streamlink_cli___pixiv_purge_credentials_nargs=0 _shtab_streamlink_cli___twitch_disable_hosting_nargs=0 _shtab_streamlink_cli___twitch_disable_ads_nargs=0 _shtab_streamlink_cli___twitch_disable_reruns_nargs=0 _shtab_streamlink_cli___twitch_low_latency_nargs=0 _shtab_streamlink_cli___yupptv_purge_credentials_nargs=0 _shtab_streamlink_cli___zattoo_purge_credentials_nargs=0 # $1=COMP_WORDS[1] _shtab_compgen_files() { compgen -f -- $1 # files } # $1=COMP_WORDS[1] _shtab_compgen_dirs() { compgen -d -- $1 # recurse into subdirs } # $1=COMP_WORDS[1] _shtab_replace_nonword() { echo "${1//[^[:word:]]/_}" } # set default values (called for the initial parser & any subparsers) _set_parser_defaults() { local subparsers_var="${prefix}_subparsers[@]" sub_parsers=${!subparsers_var} local current_option_strings_var="${prefix}_option_strings[@]" current_option_strings=${!current_option_strings_var} completed_positional_actions=0 _set_new_action "pos_${completed_positional_actions}" true } # $1=action identifier # $2=positional action (bool) # set all identifiers for an action's parameters _set_new_action() { current_action="${prefix}_$(_shtab_replace_nonword $1)" local current_action_compgen_var=${current_action}_COMPGEN current_action_compgen="${!current_action_compgen_var}" local current_action_choices_var="${current_action}_choices" current_action_choices="${!current_action_choices_var}" local current_action_nargs_var="${current_action}_nargs" if [ -n "${!current_action_nargs_var}" ]; then current_action_nargs="${!current_action_nargs_var}" else current_action_nargs=1 fi current_action_args_start_index=$(( $word_index + 1 )) current_action_is_positional=$2 } # Notes: # `COMPREPLY`: what will be rendered after completion is triggered # `completing_word`: currently typed word to generate completions for # `${!var}`: evaluates the content of `var` and expand its content as a variable # hello="world" # x="hello" # ${!x} -> ${hello} -> "world" _shtab_streamlink_cli() { local completing_word="${COMP_WORDS[COMP_CWORD]}" COMPREPLY=() prefix=_shtab_streamlink_cli word_index=0 _set_parser_defaults word_index=1 # determine what arguments are appropriate for the current state # of the arg parser while [ $word_index -ne $COMP_CWORD ]; do local this_word="${COMP_WORDS[$word_index]}" if [[ -n $sub_parsers && " ${sub_parsers[@]} " =~ " ${this_word} " ]]; then # valid subcommand: add it to the prefix & reset the current action prefix="${prefix}_$(_shtab_replace_nonword $this_word)" _set_parser_defaults fi if [[ " ${current_option_strings[@]} " =~ " ${this_word} " ]]; then # a new action should be acquired (due to recognised option string or # no more input expected from current action); # the next positional action can fill in here _set_new_action $this_word false fi if [[ "$current_action_nargs" != "*" ]] && \ [[ "$current_action_nargs" != "+" ]] && \ [[ "$current_action_nargs" != *"..." ]] && \ (( $word_index + 1 - $current_action_args_start_index >= \ $current_action_nargs )); then $current_action_is_positional && let "completed_positional_actions += 1" _set_new_action "pos_${completed_positional_actions}" true fi let "word_index+=1" done # Generate the completions if [[ "${completing_word}" == -* ]]; then # optional argument started: use option strings COMPREPLY=( $(compgen -W "${current_option_strings[*]}" -- "${completing_word}") ) else # use choices & compgen COMPREPLY=( $(compgen -W "${current_action_choices}" -- "${completing_word}"; \ [ -n "${current_action_compgen}" ] \ && "${current_action_compgen}" "${completing_word}") ) fi return 0 } complete -o filenames -F _shtab_streamlink_cli streamlink ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643136177.0208402 streamlink-3.1.1/completions/zsh/0000755000175100001710000000000000000000000016365 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136169.0 streamlink-3.1.1/completions/zsh/_streamlink0000644000175100001710000007622400000000000020633 0ustar00runnerdocker#compdef streamlink # AUTOMATCALLY GENERATED by `shtab` _shtab_streamlink_cli_options_=( {-h,--help}"[ Show this help message and exit. ]" "(- :)"{-V,--version}"[ Show version number and exit. ]" "--plugins[ Print a list of all currently installed plugins. ]" "--plugin-dirs[ Attempts to load plugins from these directories. Multiple directories can be used by separating them with a comma. ]:plugin_dirs:" "--can-handle-url[ Check if Streamlink has a plugin that can handle the specified URL. Returns status code 1 for false and 0 for true. Useful for external scripting. ]:can_handle_url:" "--can-handle-url-no-redirect[ Same as --can-handle-url but without following redirects when looking up the URL. ]:can_handle_url_no_redirect:" "*--config[ Load options from this config file. Can be repeated to load multiple files, in which case the options are merged on top of each other where the last config has highest priority. ]:config:" {-l,--loglevel}"[ Set the log message threshold. Valid levels are\: none, error, warning, info, debug, trace ]:loglevel:(none critical error warning info debug trace)" "--logfile[ Append log output to FILE instead of writing to stdout\/stderr. User prompts and download progress won\'t be written to FILE. A value of \`\`-\`\` will set the file name to an ISO8601-like string and will choose the following default log directories. Windows\: \%\%TEMP\%\%\\streamlink\\logs macOS\: \$\{HOME\}\/Library\/Logs\/streamlink Linux\/BSD\: \$\{XDG_STATE_HOME\:-\$\{HOME\}\/.local\/state\}\/streamlink\/logs ]:logfile:" {-Q,--quiet}"[ Hide all log output. Alias for \"--loglevel none\". ]" {-j,--json}"[ Output JSON representations instead of the normal text output. Useful for external scripting. ]" "--auto-version-check[ Enable or disable the automatic check for a new version of Streamlink. Default is \"no\". ]:auto_version_check:" "--version-check[ Runs a version check and exits. ]" "--locale[ The preferred locale setting, for selecting the preferred subtitle and audio language. The locale is formatted as \[language_code\]_\[country_code\], eg. en_US or es_ES. Default is system locale. ]:locale:" "--interface[ Set the network interface. ]:interface:" {-4,--ipv4}"[ Resolve address names to IPv4 only. This option overrides \:option\:\`-6\`. ]" {-6,--ipv6}"[ Resolve address names to IPv6 only. This option overrides \:option\:\`-4\`. ]" {-p,--player}"[ Player to feed stream data to. By default, VLC will be used if it can be found in its default location. This is a shell-like syntax to support using a specific player\: \%(prog)s --player\=vlc \ \[stream\] Absolute or relative paths can also be passed via this option in the event the player\'s executable can not be resolved\: \%(prog)s --player\=\/path\/to\/vlc \ \[stream\] \%(prog)s --player\=.\/vlc-player\/vlc \ \[stream\] To use a player that is located in a path with spaces you must quote the parameter or its value\: \%(prog)s \"--player\=\/path\/with spaces\/vlc\" \ \[stream\] \%(prog)s --player \"C\:\\path\\with spaces\\mpc-hc64.exe\" \ \[stream\] Options may also be passed to the player. For example\: \%(prog)s --player \"vlc --file-caching\=5000\" \ \[stream\] As an alternative to this, see the --player-args parameter, which does not log any custom player arguments. ]:player:" {-a,--player-args}"[ This option allows you to customize the default arguments which are put together with the value of --player to create a command to execute. It\'s usually enough to only use --player instead of this unless you need to add arguments after the player\'s input argument or if you don\'t want any of the player arguments to be logged. The value can contain formatting variables surrounded by curly braces, \{ and \}. If you need to include a brace character, it can be escaped by doubling, e.g. \{\{ and \}\}. Formatting variables available\: \{playerinput\} This is the input that the player will use. For standard input (stdin), it is \`\`-\`\`, but it can also be a URL, depending on the options used. \{filename\} The old fallback variable name with the same functionality. Example\: \%(prog)s -p vlc -a \"--play-and-exit \{playerinput\}\" \ \[stream\] Note\: When neither of the variables are found, \`\`\{playerinput\}\`\` will be appended to the whole parameter value, to ensure that the player always receives an input argument. ]:player_args:" {-v,--verbose-player}"[ Allow the player to display its console output. ]" {-n,--player-fifo,--fifo}"[ Make the player read the stream through a named pipe instead of the stdin pipe. ]" "--player-http[ Make the player read the stream through HTTP instead of the stdin pipe. ]" "--player-continuous-http[ Make the player read the stream through HTTP, but unlike --player-http it will continuously try to open the stream if the player requests it. This makes it possible to handle stream disconnects if your player is capable of reconnecting to a HTTP stream. This is usually done by setting your player to a \"repeat mode\". ]" "--player-external-http[ Serve stream data through HTTP without running any player. This is useful to allow external devices like smartphones or streaming boxes to watch streams they wouldn\'t be able to otherwise. Behavior will be similar to the continuous HTTP option, but no player program will be started, and the server will listen on all available connections instead of just in the local (loopback) interface. The URLs that can be used to access the stream will be printed to the console, and the server can be interrupted using CTRL-C. ]" "--player-external-http-port[ A fixed port to use for the external HTTP server if that mode is enabled. Omit or set to 0 to use a random high ( \>1024) port. ]:player_external_http_port:" "--player-passthrough[ A comma-delimited list of stream types to pass to the player as a URL to let it handle the transport of the stream instead. Stream types that can be converted into a playable URL are\: - hls - http Make sure your player can handle the stream type when using this. ]:player_passthrough:" "--player-no-close[ By default Streamlink will close the player when the stream ends. This is to avoid \"dead\" GUI players lingering after a stream ends. It does however have the side-effect of sometimes closing a player before it has played back all of its cached data. This option will instead let the player decide when to exit. ]" {-t,--title}"[ Change the title of the video player\'s window. Please see the \"Metadata variables\" section of Streamlink\'s CLI documentation for all available metadata variables. This option is only supported for the following players\: mpv, potplayer, vlc VLC specific information\: VLC does support special formatting variables on its own\: https\:\/\/wiki.videolan.org\/Documentation\:Format_String\/ These variables are accessible in the --title option by adding a backslash in front of the dollar sign which VLC uses as its formatting character. For example, to put the current date in your VLC window title, the string \"\\\\\$A\" could be inserted inside the --title string. Example\: \%(prog)s -p mpv --title \"\{author\} - \{category\} - \{title\}\" \ \[STREAM\] ]:title:" {-o,--output}"[ Write stream data to FILENAME instead of playing it. You will be prompted if the file already exists. Please see the \"Metadata variables\" section of Streamlink\'s CLI documentation for all available metadata variables. Unsupported characters in substituted variables will be replaced with an underscore. Example\: \%(prog)s --output \"\~\/recordings\/\{author\}\/\{category\}\/\{id\}-\{time\:\%\%Y\%\%m\%\%d\%\%H\%\%M\%\%S\}.ts\" \ \[STREAM\] ]:output:" {-f,--force}"[ When using -o or -r, always write to file even if it already exists. ]" "--force-progress[ When using -o or -r, show the download progress bar even if there is no terminal. ]" {-O,--stdout}"[ Write stream data to stdout instead of playing it. ]" {-r,--record}"[ Open the stream in the player, while at the same time writing it to FILENAME. You will be prompted if the file already exists. Please see the \"Metadata variables\" section of Streamlink\'s CLI documentation for all available metadata variables. Unsupported characters in substituted variables will be replaced with an underscore. Example\: \%(prog)s --record \"\~\/recordings\/\{author\}\/\{category\}\/\{id\}-\{time\:\%\%Y\%\%m\%\%d\%\%H\%\%M\%\%S\}.ts\" \ \[STREAM\] ]:record:" {-R,--record-and-pipe}"[ Write stream data to stdout, while at the same time writing it to FILENAME. You will be prompted if the file already exists. Please see the \"Metadata variables\" section of Streamlink\'s CLI documentation for all available metadata variables. Unsupported characters in substituted variables will be replaced with an underscore. Example\: \%(prog)s --record-and-pipe \"\~\/recordings\/\{author\}\/\{category\}\/\{id\}-\{time\:\%\%Y\%\%m\%\%d\%\%H\%\%M\%\%S\}.ts\" \ \[STREAM\] ]:record_and_pipe:" "--fs-safe-rules[ The rules used to make formatting variables filesystem-safe are chosen automatically according to the type of system in use. This overrides the automatic detection. Intended for use when Streamlink is running on a UNIX-like OS but writing to Windows filesystems such as NTFS\; USB devices using VFAT or exFAT\; CIFS shares that are enforcing Windows filename limitations, etc. These characters are replaced with an underscore for the rules in use\: POSIX \: \\x00-\\x1F \/ Windows\: \\x00-\\x1F \\x7F \" \* \/ \: \< \> \? \\ \| ]:fs_safe_rules:(POSIX Windows)" "--url[ A URL to attempt to extract streams from. Usually, the protocol of http(s) URLs can be omitted (https\:\/\/), depending on the implementation of the plugin being used. This is an alternative to setting the URL using a positional argument and can be useful if set in a config file. ]:url_param:" "--default-stream[ Stream to play. Use \`\`best\`\` or \`\`worst\`\` for selecting the highest or lowest available quality. Fallback streams can be specified by using a comma-separated list\: \"720p,480p,best\" This is an alternative to setting the stream using a positional argument and can be useful if set in a config file. ]:default_stream:" "--stream-url[ If possible, translate the resolved stream to a URL and print it. ]" "--retry-streams[ Retry fetching the list of available streams until streams are found while waiting DELAY second(s) between each attempt. If unset, only one attempt will be made to fetch the list of streams available. The number of fetch retry attempts can be capped with --retry-max. ]:retry_streams:" "--retry-max[ When using --retry-streams, stop retrying the fetch after COUNT retry attempt(s). Fetch will retry infinitely if COUNT is zero or unset. If --retry-max is set without setting --retry-streams, the delay between retries will default to 1 second. ]:retry_max:" "--retry-open[ After a successful fetch, try ATTEMPTS time(s) to open the stream until giving up. Default is 1. ]:retry_open:" {--stream-types,--stream-priority}"[ A comma-delimited list of stream types to allow. The order will be used to separate streams when there are multiple streams with the same name but different stream types. Any stream type not listed will be omitted from the available streams list. A \`\`\*\`\` can be used as a wildcard to match any other type of stream, eg. muxed-stream. Default is \"hls,http,\*\". ]:stream_types:" "--stream-sorting-excludes[ Fine tune the \`\`best\`\` and \`\`worst\`\` stream name synonyms by excluding unwanted streams. If all of the available streams get excluded, \`\`best\`\` and \`\`worst\`\` will become inaccessible and new special stream synonyms \`\`best-unfiltered\`\` and \`\`worst-unfiltered\`\` can be used as a fallback selection method. Uses a filter expression in the format\: \[operator\]\ Valid operators are \`\`\>\`\`, \`\`\>\=\`\`, \`\`\<\`\` and \`\`\<\=\`\`. If no operator is specified then equality is tested. For example this will exclude streams ranked higher than \"480p\"\: \"\>480p\" Multiple filters can be used by separating each expression with a comma. For example this will exclude streams from two quality types\: \"\>480p,\>medium\" ]:stream_sorting_excludes:" "--ringbuffer-size[ The maximum size of the ringbuffer. Mega- or kilobytes can be specified via the M or K suffix respectively. The ringbuffer is used as a temporary storage between the stream and the player. This allows Streamlink to download the stream faster than the player which reads the data from the ringbuffer. The smaller the size of the ringbuffer, the higher the chance of the player buffering if the download speed decreases, and the higher the size, the more data can be use as a storage to recover from volatile download speeds. Most players have their own additional cache and will read the ringbuffer\'s content as soon as data is available. If the player stops reading data while playback is paused, Streamlink will continue to download the stream in the background as long as the ringbuffer doesn\'t get full. Default is \"16M\". Note\: A smaller size is recommended on lower end systems (such as Raspberry Pi) when playing stream types that require some extra processing to avoid unnecessary background processing. ]:ringbuffer_size:" "--stream-segment-attempts[ How many attempts should be done to download each segment before giving up. This applies to all different kinds of segmented stream types, such as DASH, HLS, etc. Default is 3. ]:stream_segment_attempts:" "--stream-segment-threads[ The size of the thread pool used to download segments. Minimum value is 1 and maximum is 10. This applies to all different kinds of segmented stream types, such as DASH, HLS, etc. Default is 1. ]:stream_segment_threads:" "--stream-segment-timeout[ Segment connect and read timeout. This applies to all different kinds of segmented stream types, such as DASH, HLS, etc. Default is 10.0. ]:stream_segment_timeout:" "--stream-timeout[ Timeout for reading data from streams. This applies to all different kinds of stream types, such as DASH, HLS, HTTP, etc. Default is 60.0. ]:stream_timeout:" "--mux-subtitles[ Automatically mux available subtitles into the output stream. Needs to be supported by the used plugin. ]" "--hls-live-edge[ Number of segments from the live stream\'s current live position to begin streaming. The size or length of each segment is determined by the streaming provider. Lower values will decrease the latency, but will also increase the chance of buffering, as there is less time for Streamlink to download segments and write their data to the output buffer. The number of parallel segment downloads can be set with --stream-segment-threads and the HLS playlist reload time to fetch and queue new segments can be overridden with --hls-playlist-reload-time. Default is 3. Note\: During live playback, the caching\/buffering settings of the used player will add additional latency. To adjust this, please refer to the player\'s own documentation for the required configuration. Player parameters can be set via --player-args. ]:hls_live_edge:" "--hls-segment-stream-data[ Immediately write segment data into output buffer while downloading. ]" "--hls-playlist-reload-attempts[ How many attempts should be done to reload the HLS playlist before giving up. Default is 3. ]:hls_playlist_reload_attempts:" "--hls-playlist-reload-time[ Set a custom HLS playlist reload time value, either in seconds or by using one of the following keywords\: segment\: The duration of the last segment in the current playlist live-edge\: The sum of segment durations of the live edge value minus one default\: The playlist\'s target duration metadata Default is default. ]:hls_playlist_reload_time:" "--hls-segment-ignore-names[ A comma-delimited list of segment names that will get filtered out. Example\: --hls-segment-ignore-names 000,001,002 This will ignore every segment that ends with 000.ts, 001.ts and 002.ts Default is None. ]:hls_segment_ignore_names:" "--hls-segment-key-uri[ Override the segment encryption key URIs for encrypted streams. The value can be templated using the following variables, which will be replaced with their respective part from the source segment URI\: \{url\} \{scheme\} \{netloc\} \{path\} \{query\} Examples\: --hls-segment-key-uri \"https\:\/\/example.com\/hls\/encryption_key\" --hls-segment-key-uri \"\{scheme\}\:\/\/1.2.3.4\{path\}\{query\}\" --hls-segment-key-uri \"\{scheme\}\:\/\/\{netloc\}\/custom\/path\/to\/key\" Default is None. ]:hls_segment_key_uri:" "--hls-audio-select[ Selects a specific audio source or sources, by language code or name, when multiple audio sources are available. Can be \* to download all audio sources. Examples\: --hls-audio-select \"English,German\" --hls-audio-select \"en,de\" --hls-audio-select \"\*\" Note\: This is only useful in special circumstances where the regular locale option fails, such as when multiple sources of the same language exists. ]:hls_audio_select:" "--hls-start-offset[ Amount of time to skip from the beginning of the stream. For live streams, this is a negative offset from the end of the stream (rewind). Default is 00\:00\:00. ]:hls_start_offset:" "--hls-duration[ Limit the playback duration, useful for watching segments of a stream. The actual duration may be slightly longer, as it is rounded to the nearest HLS segment. Default is unlimited. ]:hls_duration:" "--hls-live-restart[ Skip to the beginning of a live stream, or as far back as possible. ]" "--ffmpeg-ffmpeg[ FFMPEG is used to access or mux separate video and audio streams. You can specify the location of the ffmpeg executable if it is not in your PATH. Example\: \"\/usr\/local\/bin\/ffmpeg\" ]:ffmpeg_ffmpeg:" "--ffmpeg-verbose[ Write the console output from ffmpeg to the console. ]" "--ffmpeg-verbose-path[ Path to write the output from the ffmpeg console. ]:ffmpeg_verbose_path:" "--ffmpeg-fout[ When muxing streams, set the output format to OUTFORMAT. Default is \"matroska\". Example\: \"mpegts\" ]:ffmpeg_fout:" "--ffmpeg-video-transcode[ When muxing streams, transcode the video to CODEC. Default is \"copy\". Example\: \"h264\" ]:ffmpeg_video_transcode:" "--ffmpeg-audio-transcode[ When muxing streams, transcode the audio to CODEC. Default is \"copy\". Example\: \"aac\" ]:ffmpeg_audio_transcode:" "--ffmpeg-copyts[ Forces the -copyts ffmpeg option and does not remove the initial start time offset value. ]" "--ffmpeg-start-at-zero[ Enable the -start_at_zero ffmpeg option when using copyts. ]" "--http-proxy[ A HTTP proxy to use for all HTTP and HTTPS requests, including WebSocket connections. Example\: \"http\:\/\/hostname\:port\/\" ]:http_proxy:" "*--http-cookie[ A cookie to add to each HTTP request. Can be repeated to add multiple cookies. ]:http_cookie:" "*--http-header[ A header to add to each HTTP request. Can be repeated to add multiple headers. ]:http_header:" "*--http-query-param[ A query parameter to add to each HTTP request. Can be repeated to add multiple query parameters. ]:http_query_param:" "--http-ignore-env[ Ignore HTTP settings set in the environment such as environment variables (HTTP_PROXY, etc) or \~\/.netrc authentication. ]" "--http-no-ssl-verify[ Don\'t attempt to verify SSL certificates. Usually a bad idea, only use this if you know what you\'re doing. ]" "--http-disable-dh[ Disable Diffie Hellman key exchange Usually a bad idea, only use this if you know what you\'re doing. ]" "--http-ssl-cert[ SSL certificate to use. Expects a .pem file. ]:http_ssl_cert:" "--http-ssl-cert-crt-key[ SSL certificate to use. Expects a .crt and a .key file. ]:http_ssl_cert_crt_key:" "--http-timeout[ General timeout used by all HTTP requests except the ones covered by other options. Default is 20.0. ]:http_timeout:" "--afreeca-username[The username used to register with afreecatv.com.]:afreeca_username:" "--afreeca-password[A afreecatv.com account password to use with --afreeca-username.]:afreeca_password:" "--afreeca-purge-credentials[ Purge cached AfreecaTV credentials to initiate a new session and reauthenticate. ]" "--bbciplayer-username[The username used to register with bbc.co.uk.]:bbciplayer_username:" "--bbciplayer-password[A bbc.co.uk account password to use with --bbciplayer-username.]:bbciplayer_password:" "--bbciplayer-hd[ Prefer HD streams over local SD streams, some live programmes may not be broadcast in HD. ]" "--clubbingtv-username[The username used to register with Clubbing TV.]:clubbingtv_username:" "--clubbingtv-password[A Clubbing TV account password to use with --clubbingtv-username.]:clubbingtv_password:" "--crunchyroll-username[A Crunchyroll username to allow access to restricted streams.]:crunchyroll_username:" "--crunchyroll-password[ A Crunchyroll password for use with --crunchyroll-username. If left blank you will be prompted. ]:crunchyroll_password:" "--crunchyroll-purge-credentials[ Purge cached Crunchyroll credentials to initiate a new session and reauthenticate. ]" "--crunchyroll-session-id[ Set a specific session ID for crunchyroll, can be used to bypass region restrictions. If using an authenticated session ID, it is recommended that the authentication parameters be omitted as the session ID is account specific. Note\: The session ID will be overwritten if authentication is used and the session ID does not match the account. ]:crunchyroll_session_id:" "--funimation-email[Email address for your Funimation account.]:funimation_email:" "--funimation-password[Password for your Funimation account.]:funimation_password:" "--funimation-language[ The audio language to use for the stream\; japanese or english. Default is \"english\". ]:funimation_language:(en ja english japanese)" "--niconico-email[The email or phone number associated with your Niconico account]:niconico_email:" "--niconico-password[The password of your Niconico account]:niconico_password:" "--niconico-user-session[Value of the user-session token (can be used in case you do not want to put your password here)]:niconico_user_session:" "--niconico-purge-credentials[Purge cached Niconico credentials to initiate a new session and reauthenticate.]" "--niconico-timeshift-offset[Amount of time to skip from the beginning of a stream. Default is 00\:00\:00.]:niconico_timeshift_offset:" "--openrectv-email[ The email associated with your openrectv account, required to access any openrectv stream. ]:openrectv_email:" "--openrectv-password[ An openrectv account password to use with --openrectv-email. ]:openrectv_password:" "--pixiv-sessionid[ The pixiv.net sessionid that\'s used in pixivs PHPSESSID cookie. can be used instead of the username\/password login process. ]:pixiv_sessionid:" "--pixiv-devicetoken[ The pixiv.net device token that\'s used in pixivs device_token cookie. can be used instead of the username\/password login process. ]:pixiv_devicetoken:" "--pixiv-purge-credentials[ Purge cached Pixiv credentials to initiate a new session and reauthenticate. ]" "--pixiv-performer[ Select a co-host stream instead of the owner stream. ]:pixiv_performer:" "--sbscokr-id[ Channel ID to play. Example\: \%(prog)s http\:\/\/play.sbs.co.kr\/onair\/pc\/index.html best --sbscokr-id S01 ]:sbscokr_id:" "--schoolism-email[ The email associated with your Schoolism account, required to access any Schoolism stream. ]:schoolism_email:" "--schoolism-password[A Schoolism account password to use with --schoolism-email.]:schoolism_password:" "--schoolism-part[ Play part number PART of the lesson, or assignment feedback video. Defaults is 1. ]:schoolism_part:" "--steam-email[ A Steam account email address to access friends\/private streams ]:steam_email:" "--steam-password[ A Steam account password to use with --steam-email. ]:steam_password:" "--streann-url[ Source URL where the iframe is located, only required for direct URLs of \`ott.streann.com\` ]:streann_url:" "--twitcasting-password[Password for private Twitcasting streams.]:twitcasting_password:" "--twitch-disable-hosting[ Do not open the stream if the target channel is hosting another channel. ]" "--twitch-disable-ads[ Skip embedded advertisement segments at the beginning or during a stream. Will cause these segments to be missing from the stream. ]" "--twitch-disable-reruns[ Do not open the stream if the target channel is currently broadcasting a rerun. ]" "--twitch-low-latency[ Enables low latency streaming by prefetching HLS segments. Sets --hls-segment-stream-data to true and --hls-live-edge to 2, if it is higher. Reducing --hls-live-edge to 1 will result in the lowest latency possible, but will most likely cause buffering. In order to achieve true low latency streaming during playback, the player\'s caching\/buffering settings will need to be adjusted and reduced to a value as low as possible, but still high enough to not cause any buffering. This depends on the stream\'s bitrate and the quality of the connection to Twitch\'s servers. Please refer to the player\'s own documentation for the required configuration. Player parameters can be set via --player-args. Note\: Low latency streams have to be enabled by the broadcasters on Twitch themselves. Regular streams can cause buffering issues with this option enabled due to the reduced --hls-live-edge value. ]" "*--twitch-api-header[ A header to add to each Twitch API HTTP request. Can be repeated to add multiple headers. ]:twitch_api_header:" "--ustream-password[A password to access password protected UStream.tv channels.]:ustream_password:" "--ustvnow-username[Your USTV Now account username]:ustvnow_username:" "--ustvnow-password[Your USTV Now account password]:ustvnow_password:" "--wwenetwork-email[ The email associated with your WWE Network account, required to access any WWE Network stream. ]:wwenetwork_email:" "--wwenetwork-password[ A WWE Network account password to use with --wwenetwork-email. ]:wwenetwork_password:" "--yupptv-boxid[ The yupptv.com boxid that\'s used in the BoxId cookie. Can be used instead of the username\/password login process. ]:yupptv_boxid:" "--yupptv-yuppflixtoken[ The yupptv.com yuppflixtoken that\'s used in the YuppflixToken cookie. Can be used instead of the username\/password login process. ]:yupptv_yuppflixtoken:" "--yupptv-purge-credentials[ Purge cached YuppTV credentials to initiate a new session and reauthenticate. ]" "--zattoo-email[ The email associated with your zattoo account, required to access any zattoo stream. ]:zattoo_email:" "--zattoo-password[ A zattoo account password to use with --zattoo-email. ]:zattoo_password:" "--zattoo-purge-credentials[ Purge cached zattoo credentials to initiate a new session and reauthenticate. ]" "--zattoo-stream-types[ A comma-delimited list of stream types which should be used, the following types are allowed\: - dash - hls7 Default is \"dash\". ]:zattoo_stream_types:" ) _shtab_streamlink_cli_commands_() { local _commands=( ) _describe 'streamlink commands' _commands } typeset -A opt_args local context state line curcontext="$curcontext" _arguments \ $_shtab_streamlink_cli_options_ \ ":A URL to attempt to extract streams from.:" \ ":Stream to play.:" \ ': :_shtab_streamlink_cli_commands_' \ '*::args:->args' case $words[1] in esac ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/dev-requirements.txt0000644000175100001710000000014200000000000017262 0ustar00runnerdockerpip>=9 pytest pytest-cov coverage requests-mock freezegun>=1.0.0 flake8 flake8-import-order shtab ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643136177.0248404 streamlink-3.1.1/docs/0000755000175100001710000000000000000000000014155 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/docs/Makefile0000644000175100001710000001271700000000000015625 0ustar00runnerdocker# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = -W SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/streamlink.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/streamlink.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/streamlink" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/streamlink" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/docs/_applications.rst0000644000175100001710000000060700000000000017537 0ustar00runnerdocker:orphan: .. raw:: html .. |Windows| raw:: html .. |MacOS| raw:: html .. |Linux| raw:: html ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643136177.0208402 streamlink-3.1.1/docs/_build/0000755000175100001710000000000000000000000015413 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643136177.0248404 streamlink-3.1.1/docs/_build/man/0000755000175100001710000000000000000000000016166 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136172.0 streamlink-3.1.1/docs/_build/man/streamlink.10000644000175100001710000010445600000000000020433 0ustar00runnerdocker.\" Man page generated from reStructuredText. . . .nr rst2man-indent-level 0 . .de1 rstReportMargin \\$1 \\n[an-margin] level \\n[rst2man-indent-level] level margin: \\n[rst2man-indent\\n[rst2man-indent-level]] - \\n[rst2man-indent0] \\n[rst2man-indent1] \\n[rst2man-indent2] .. .de1 INDENT .\" .rstReportMargin pre: . RS \\$1 . nr rst2man-indent\\n[rst2man-indent-level] \\n[an-margin] . nr rst2man-indent-level +1 .\" .rstReportMargin post: .. .de UNINDENT . RE .\" indent \\n[an-margin] .\" old: \\n[rst2man-indent\\n[rst2man-indent-level]] .nr rst2man-indent-level -1 .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. .TH "STREAMLINK" "1" "Jan 25, 2022" "3.1.1" "Streamlink" .SH NAME streamlink \- extracts streams from various services and pipes them into a video player of choice .SH SYNOPSIS .INDENT 0.0 .INDENT 3.5 .sp .nf .ft C streamlink [OPTIONS] [STREAM] streamlink \-\-loglevel debug youtu.be/VIDEO\-ID best streamlink \-\-player mpv \-\-player\-args \(aq\-\-no\-border \-\-no\-keepaspect\-window\(aq twitch.tv/CHANNEL 1080p60 streamlink \-\-player\-external\-http \-\-player\-external\-http\-port 8888 URL STREAM streamlink \-\-output /path/to/file \-\-http\-timeout 60 URL STREAM streamlink \-\-stdout URL STREAM | ffmpeg \-i pipe:0 ... streamlink \-\-http\-header \(aqAuthorization=OAuth TOKEN\(aq \-\-http\-header \(aqReferer=URL\(aq URL STREAM streamlink \-\-hls\-live\-edge 5 \-\-stream\-segment\-threads 5 \(aqhls://https://host/playlist.m3u8\(aq best streamlink \-\-twitch\-low\-latency \-p mpv \-a \(aq\-\-cache=yes \-\-demuxer\-max\-bytes=750k\(aq twitch.tv/CHANNEL best .ft P .fi .UNINDENT .UNINDENT .SH POSITIONAL ARGUMENTS .INDENT 0.0 .TP .B URL A URL to attempt to extract streams from. .sp Usually, the protocol of http(s) URLs can be omitted ("\fI\%https://\fP"), depending on the implementation of the plugin being used. .sp Alternatively, the URL can also be specified by using the \fB\-\-url\fP option. .UNINDENT .INDENT 0.0 .TP .B STREAM Stream to play. .sp Use \fBbest\fP or \fBworst\fP for selecting the highest or lowest available quality. .sp Fallback streams can be specified by using a comma\-separated list: .INDENT 7.0 .INDENT 3.5 .sp .nf .ft C "720p,480p,best" .ft P .fi .UNINDENT .UNINDENT .sp If no stream is specified and \fB\-\-default\-stream\fP is not used, then a list of available streams will be printed. .UNINDENT .SH GENERAL OPTIONS .INDENT 0.0 .TP .B \-h .TP .B \-\-help Show this help message and exit. .UNINDENT .INDENT 0.0 .TP .B \-V .TP .B \-\-version Show version number and exit. .UNINDENT .INDENT 0.0 .TP .B \-\-plugins Print a list of all currently installed plugins. .UNINDENT .INDENT 0.0 .TP .B \-\-plugin\-dirs DIRECTORY Attempts to load plugins from these directories. .sp Multiple directories can be used by separating them with a comma. .UNINDENT .INDENT 0.0 .TP .B \-\-can\-handle\-url URL Check if Streamlink has a plugin that can handle the specified URL. .sp Returns status code 1 for false and 0 for true. .sp Useful for external scripting. .UNINDENT .INDENT 0.0 .TP .B \-\-can\-handle\-url\-no\-redirect URL Same as \fB\-\-can\-handle\-url\fP but without following redirects when looking up the URL. .UNINDENT .INDENT 0.0 .TP .B \-\-config FILENAME Load options from this config file. .sp Can be repeated to load multiple files, in which case the options are merged on top of each other where the last config has highest priority. .UNINDENT .INDENT 0.0 .TP .B \-l LEVEL .TP .B \-\-loglevel LEVEL Set the log message threshold. .sp Valid levels are: none, error, warning, info, debug, trace .UNINDENT .INDENT 0.0 .TP .B \-\-logfile FILE Append log output to FILE instead of writing to stdout/stderr. .sp User prompts and download progress won\(aqt be written to FILE. .sp A value of \fB\-\fP will set the file name to an ISO8601\-like string and will choose the following default log directories. .sp Windows: .INDENT 7.0 .INDENT 3.5 .sp .nf .ft C %TEMP%\estreamlink\elogs .ft P .fi .UNINDENT .UNINDENT .sp macOS: .INDENT 7.0 .INDENT 3.5 .sp .nf .ft C ${HOME}/Library/Logs/streamlink .ft P .fi .UNINDENT .UNINDENT .sp Linux/BSD: .INDENT 7.0 .INDENT 3.5 .sp .nf .ft C ${XDG_STATE_HOME:\-${HOME}/.local/state}/streamlink/logs .ft P .fi .UNINDENT .UNINDENT .UNINDENT .INDENT 0.0 .TP .B \-Q .TP .B \-\-quiet Hide all log output. .sp Alias for "\-\-loglevel none". .UNINDENT .INDENT 0.0 .TP .B \-j .TP .B \-\-json Output JSON representations instead of the normal text output. .sp Useful for external scripting. .UNINDENT .INDENT 0.0 .TP .B \-\-auto\-version\-check {yes,true,1,on,no,false,0,off} Enable or disable the automatic check for a new version of Streamlink. .sp Default is: \fB"no"\fP\&. .UNINDENT .INDENT 0.0 .TP .B \-\-version\-check Runs a version check and exits. .UNINDENT .INDENT 0.0 .TP .B \-\-locale LOCALE The preferred locale setting, for selecting the preferred subtitle and audio language. .sp The locale is formatted as [language_code]_[country_code], eg. en_US or es_ES. .sp Default is: \fBsystem locale\fP\&. .UNINDENT .INDENT 0.0 .TP .B \-\-interface INTERFACE Set the network interface. .UNINDENT .INDENT 0.0 .TP .B \-4 .TP .B \-\-ipv4 Resolve address names to IPv4 only. This option overrides \fB\-6\fP\&. .UNINDENT .INDENT 0.0 .TP .B \-6 .TP .B \-\-ipv6 Resolve address names to IPv6 only. This option overrides \fB\-4\fP\&. .UNINDENT .SH PLAYER OPTIONS .INDENT 0.0 .TP .B \-p COMMAND .TP .B \-\-player COMMAND Player to feed stream data to. By default, VLC will be used if it can be found in its default location. .sp This is a shell\-like syntax to support using a specific player: .INDENT 7.0 .INDENT 3.5 .sp .nf .ft C streamlink \-\-player=vlc [stream] .ft P .fi .UNINDENT .UNINDENT .sp Absolute or relative paths can also be passed via this option in the event the player\(aqs executable can not be resolved: .INDENT 7.0 .INDENT 3.5 .sp .nf .ft C streamlink \-\-player=/path/to/vlc [stream] streamlink \-\-player=./vlc\-player/vlc [stream] .ft P .fi .UNINDENT .UNINDENT .sp To use a player that is located in a path with spaces you must quote the parameter or its value: .INDENT 7.0 .INDENT 3.5 .sp .nf .ft C streamlink "\-\-player=/path/with spaces/vlc" [stream] streamlink \-\-player "C:\epath\ewith spaces\empc\-hc64.exe" [stream] .ft P .fi .UNINDENT .UNINDENT .sp Options may also be passed to the player. For example: .INDENT 7.0 .INDENT 3.5 .sp .nf .ft C streamlink \-\-player "vlc \-\-file\-caching=5000" [stream] .ft P .fi .UNINDENT .UNINDENT .sp As an alternative to this, see the \fB\-\-player\-args\fP parameter, which does not log any custom player arguments. .UNINDENT .INDENT 0.0 .TP .B \-a ARGUMENTS .TP .B \-\-player\-args ARGUMENTS This option allows you to customize the default arguments which are put together with the value of \fB\-\-player\fP to create a command to execute. .sp It\(aqs usually enough to only use \fB\-\-player\fP instead of this unless you need to add arguments after the player\(aqs input argument or if you don\(aqt want any of the player arguments to be logged. .sp The value can contain formatting variables surrounded by curly braces, { and }. If you need to include a brace character, it can be escaped by doubling, e.g. {{ and }}. .sp Formatting variables available: .INDENT 7.0 .TP .B {playerinput} This is the input that the player will use. For standard input (stdin), it is \fB\-\fP, but it can also be a URL, depending on the options used. .TP .B {filename} The old fallback variable name with the same functionality. .UNINDENT .sp Example: .INDENT 7.0 .INDENT 3.5 .sp .nf .ft C streamlink \-p vlc \-a "\-\-play\-and\-exit {playerinput}" [stream] .ft P .fi .UNINDENT .UNINDENT .sp \fBNOTE:\fP .INDENT 7.0 .INDENT 3.5 When neither of the variables are found, \fB{playerinput}\fP will be appended to the whole parameter value, to ensure that the player always receives an input argument. .UNINDENT .UNINDENT .UNINDENT .INDENT 0.0 .TP .B \-v .TP .B \-\-verbose\-player Allow the player to display its console output. .UNINDENT .INDENT 0.0 .TP .B \-n .TP .B \-\-player\-fifo .TP .B \-\-fifo Make the player read the stream through a named pipe instead of the stdin pipe. .UNINDENT .INDENT 0.0 .TP .B \-\-player\-http Make the player read the stream through HTTP instead of the stdin pipe. .UNINDENT .INDENT 0.0 .TP .B \-\-player\-continuous\-http Make the player read the stream through HTTP, but unlike \fB\-\-player\-http\fP it will continuously try to open the stream if the player requests it. .sp This makes it possible to handle stream disconnects if your player is capable of reconnecting to a HTTP stream. This is usually done by setting your player to a "repeat mode". .UNINDENT .INDENT 0.0 .TP .B \-\-player\-external\-http Serve stream data through HTTP without running any player. This is useful to allow external devices like smartphones or streaming boxes to watch streams they wouldn\(aqt be able to otherwise. .sp Behavior will be similar to the continuous HTTP option, but no player program will be started, and the server will listen on all available connections instead of just in the local (loopback) interface. .sp The URLs that can be used to access the stream will be printed to the console, and the server can be interrupted using CTRL\-C. .UNINDENT .INDENT 0.0 .TP .B \-\-player\-external\-http\-port PORT A fixed port to use for the external HTTP server if that mode is enabled. Omit or set to 0 to use a random high ( >1024) port. .UNINDENT .INDENT 0.0 .TP .B \-\-player\-passthrough TYPES A comma\-delimited list of stream types to pass to the player as a URL to let it handle the transport of the stream instead. .sp Stream types that can be converted into a playable URL are: .INDENT 7.0 .IP \(bu 2 hls .IP \(bu 2 http .UNINDENT .sp Make sure your player can handle the stream type when using this. .UNINDENT .INDENT 0.0 .TP .B \-\-player\-no\-close By default Streamlink will close the player when the stream ends. This is to avoid "dead" GUI players lingering after a stream ends. .sp It does however have the side\-effect of sometimes closing a player before it has played back all of its cached data. .sp This option will instead let the player decide when to exit. .UNINDENT .INDENT 0.0 .TP .B \-t TITLE .TP .B \-\-title TITLE Change the title of the video player\(aqs window. .sp Please see the "Metadata variables" section of Streamlink\(aqs CLI documentation for all available metadata variables. .sp This option is only supported for the following players: mpv, potplayer, vlc .INDENT 7.0 .TP .B VLC specific information: VLC does support special formatting variables on its own: \fI\%https://wiki.videolan.org/Documentation:Format_String/\fP .sp These variables are accessible in the \-\-title option by adding a backslash in front of the dollar sign which VLC uses as its formatting character. .sp For example, to put the current date in your VLC window title, the string "\e$A" could be inserted inside the \-\-title string. .UNINDENT .sp Example: .INDENT 7.0 .INDENT 3.5 .sp .nf .ft C streamlink \-p mpv \-\-title "{author} \- {category} \- {title}" [STREAM] .ft P .fi .UNINDENT .UNINDENT .UNINDENT .SH FILE OUTPUT OPTIONS .INDENT 0.0 .TP .B \-o FILENAME .TP .B \-\-output FILENAME Write stream data to FILENAME instead of playing it. .sp You will be prompted if the file already exists. .sp Please see the "Metadata variables" section of Streamlink\(aqs CLI documentation for all available metadata variables. .sp Unsupported characters in substituted variables will be replaced with an underscore. .sp Example: .INDENT 7.0 .INDENT 3.5 .sp .nf .ft C streamlink \-\-output "~/recordings/{author}/{category}/{id}\-{time:%Y%m%d%H%M%S}.ts" [STREAM] .ft P .fi .UNINDENT .UNINDENT .UNINDENT .INDENT 0.0 .TP .B \-f .TP .B \-\-force When using \-o or \-r, always write to file even if it already exists. .UNINDENT .INDENT 0.0 .TP .B \-\-force\-progress When using \-o or \-r, show the download progress bar even if there is no terminal. .UNINDENT .INDENT 0.0 .TP .B \-O .TP .B \-\-stdout Write stream data to stdout instead of playing it. .UNINDENT .INDENT 0.0 .TP .B \-r FILENAME .TP .B \-\-record FILENAME Open the stream in the player, while at the same time writing it to FILENAME. .sp You will be prompted if the file already exists. .sp Please see the "Metadata variables" section of Streamlink\(aqs CLI documentation for all available metadata variables. .sp Unsupported characters in substituted variables will be replaced with an underscore. .sp Example: .INDENT 7.0 .INDENT 3.5 .sp .nf .ft C streamlink \-\-record "~/recordings/{author}/{category}/{id}\-{time:%Y%m%d%H%M%S}.ts" [STREAM] .ft P .fi .UNINDENT .UNINDENT .UNINDENT .INDENT 0.0 .TP .B \-R FILENAME .TP .B \-\-record\-and\-pipe FILENAME Write stream data to stdout, while at the same time writing it to FILENAME. .sp You will be prompted if the file already exists. .sp Please see the "Metadata variables" section of Streamlink\(aqs CLI documentation for all available metadata variables. .sp Unsupported characters in substituted variables will be replaced with an underscore. .sp Example: .INDENT 7.0 .INDENT 3.5 .sp .nf .ft C streamlink \-\-record\-and\-pipe "~/recordings/{author}/{category}/{id}\-{time:%Y%m%d%H%M%S}.ts" [STREAM] .ft P .fi .UNINDENT .UNINDENT .UNINDENT .INDENT 0.0 .TP .B \-\-fs\-safe\-rules The rules used to make formatting variables filesystem\-safe are chosen automatically according to the type of system in use. This overrides the automatic detection. .sp Intended for use when Streamlink is running on a UNIX\-like OS but writing to Windows filesystems such as NTFS; USB devices using VFAT or exFAT; CIFS shares that are enforcing Windows filename limitations, etc. .sp These characters are replaced with an underscore for the rules in use: .INDENT 7.0 .INDENT 3.5 .sp .nf .ft C POSIX : \ex00\-\ex1F / Windows: \ex00\-\ex1F \ex7F " * / : < > ? \e | .ft P .fi .UNINDENT .UNINDENT .UNINDENT .SH STREAM OPTIONS .INDENT 0.0 .TP .B \-\-url URL A URL to attempt to extract streams from. .sp Usually, the protocol of http(s) URLs can be omitted (\fI\%https://\fP), depending on the implementation of the plugin being used. .sp This is an alternative to setting the URL using a positional argument and can be useful if set in a config file. .UNINDENT .INDENT 0.0 .TP .B \-\-default\-stream STREAM Stream to play. .sp Use \fBbest\fP or \fBworst\fP for selecting the highest or lowest available quality. .sp Fallback streams can be specified by using a comma\-separated list: .INDENT 7.0 .INDENT 3.5 .sp .nf .ft C "720p,480p,best" .ft P .fi .UNINDENT .UNINDENT .sp This is an alternative to setting the stream using a positional argument and can be useful if set in a config file. .UNINDENT .INDENT 0.0 .TP .B \-\-stream\-url If possible, translate the resolved stream to a URL and print it. .UNINDENT .INDENT 0.0 .TP .B \-\-retry\-streams DELAY Retry fetching the list of available streams until streams are found while waiting DELAY second(s) between each attempt. If unset, only one attempt will be made to fetch the list of streams available. .sp The number of fetch retry attempts can be capped with \fB\-\-retry\-max\fP\&. .UNINDENT .INDENT 0.0 .TP .B \-\-retry\-max COUNT When using \fB\-\-retry\-streams\fP, stop retrying the fetch after COUNT retry attempt(s). Fetch will retry infinitely if COUNT is zero or unset. .sp If \fB\-\-retry\-max\fP is set without setting \fB\-\-retry\-streams\fP, the delay between retries will default to 1 second. .UNINDENT .INDENT 0.0 .TP .B \-\-retry\-open ATTEMPTS After a successful fetch, try ATTEMPTS time(s) to open the stream until giving up. .sp Default is: \fB1\fP\&. .UNINDENT .INDENT 0.0 .TP .B \-\-stream\-types TYPES .TP .B \-\-stream\-priority TYPES A comma\-delimited list of stream types to allow. .sp The order will be used to separate streams when there are multiple streams with the same name but different stream types. Any stream type not listed will be omitted from the available streams list. A \fB*\fP can be used as a wildcard to match any other type of stream, eg. muxed\-stream. .sp Default is: \fB"hls,http,*"\fP\&. .UNINDENT .INDENT 0.0 .TP .B \-\-stream\-sorting\-excludes STREAMS Fine tune the \fBbest\fP and \fBworst\fP stream name synonyms by excluding unwanted streams. .sp If all of the available streams get excluded, \fBbest\fP and \fBworst\fP will become inaccessible and new special stream synonyms \fBbest\-unfiltered\fP and \fBworst\-unfiltered\fP can be used as a fallback selection method. .sp Uses a filter expression in the format: .INDENT 7.0 .INDENT 3.5 .sp .nf .ft C [operator] .ft P .fi .UNINDENT .UNINDENT .sp Valid operators are \fB>\fP, \fB>=\fP, \fB<\fP and \fB<=\fP\&. If no operator is specified then equality is tested. .sp For example this will exclude streams ranked higher than "480p": .INDENT 7.0 .INDENT 3.5 .sp .nf .ft C ">480p" .ft P .fi .UNINDENT .UNINDENT .sp Multiple filters can be used by separating each expression with a comma. .sp For example this will exclude streams from two quality types: .INDENT 7.0 .INDENT 3.5 .sp .nf .ft C ">480p,>medium" .ft P .fi .UNINDENT .UNINDENT .UNINDENT .SH STREAM TRANSPORT OPTIONS .INDENT 0.0 .TP .B \-\-ringbuffer\-size SIZE The maximum size of the ringbuffer. Mega\- or kilobytes can be specified via the M or K suffix respectively. .sp The ringbuffer is used as a temporary storage between the stream and the player. This allows Streamlink to download the stream faster than the player which reads the data from the ringbuffer. .sp The smaller the size of the ringbuffer, the higher the chance of the player buffering if the download speed decreases, and the higher the size, the more data can be use as a storage to recover from volatile download speeds. .sp Most players have their own additional cache and will read the ringbuffer\(aqs content as soon as data is available. If the player stops reading data while playback is paused, Streamlink will continue to download the stream in the background as long as the ringbuffer doesn\(aqt get full. .sp Default is: \fB"16M"\fP\&. .sp \fBNOTE:\fP .INDENT 7.0 .INDENT 3.5 A smaller size is recommended on lower end systems (such as Raspberry Pi) when playing stream types that require some extra processing to avoid unnecessary background processing. .UNINDENT .UNINDENT .UNINDENT .INDENT 0.0 .TP .B \-\-stream\-segment\-attempts ATTEMPTS How many attempts should be done to download each segment before giving up. .sp This applies to all different kinds of segmented stream types, such as DASH, HLS, etc. .sp Default is: \fB3\fP\&. .UNINDENT .INDENT 0.0 .TP .B \-\-stream\-segment\-threads THREADS The size of the thread pool used to download segments. Minimum value is 1 and maximum is 10. .sp This applies to all different kinds of segmented stream types, such as DASH, HLS, etc. .sp Default is: \fB1\fP\&. .UNINDENT .INDENT 0.0 .TP .B \-\-stream\-segment\-timeout TIMEOUT Segment connect and read timeout. .sp This applies to all different kinds of segmented stream types, such as DASH, HLS, etc. .sp Default is: \fB10.0\fP\&. .UNINDENT .INDENT 0.0 .TP .B \-\-stream\-timeout TIMEOUT Timeout for reading data from streams. .sp This applies to all different kinds of stream types, such as DASH, HLS, HTTP, etc. .sp Default is: \fB60.0\fP\&. .UNINDENT .INDENT 0.0 .TP .B \-\-mux\-subtitles Automatically mux available subtitles into the output stream. .sp Needs to be supported by the used plugin. .sp \fBSupported plugins:\fP funimationnow, rtve, svtplay, vimeo .UNINDENT .SS HLS options .INDENT 0.0 .TP .B \-\-hls\-live\-edge SEGMENTS Number of segments from the live stream\(aqs current live position to begin streaming. The size or length of each segment is determined by the streaming provider. .sp Lower values will decrease the latency, but will also increase the chance of buffering, as there is less time for Streamlink to download segments and write their data to the output buffer. The number of parallel segment downloads can be set with \fB\-\-stream\-segment\-threads\fP and the HLS playlist reload time to fetch and queue new segments can be overridden with \fB\-\-hls\-playlist\-reload\-time\fP\&. .sp Default is: \fB3\fP\&. .sp \fBNOTE:\fP .INDENT 7.0 .INDENT 3.5 During live playback, the caching/buffering settings of the used player will add additional latency. To adjust this, please refer to the player\(aqs own documentation for the required configuration. Player parameters can be set via \fB\-\-player\-args\fP\&. .UNINDENT .UNINDENT .UNINDENT .INDENT 0.0 .TP .B \-\-hls\-segment\-stream\-data Immediately write segment data into output buffer while downloading. .UNINDENT .INDENT 0.0 .TP .B \-\-hls\-playlist\-reload\-attempts ATTEMPTS How many attempts should be done to reload the HLS playlist before giving up. .sp Default is: \fB3\fP\&. .UNINDENT .INDENT 0.0 .TP .B \-\-hls\-playlist\-reload\-time TIME Set a custom HLS playlist reload time value, either in seconds or by using one of the following keywords: .INDENT 7.0 .INDENT 3.5 .sp .nf .ft C segment: The duration of the last segment in the current playlist live\-edge: The sum of segment durations of the live edge value minus one default: The playlist\(aqs target duration metadata .ft P .fi .UNINDENT .UNINDENT .sp Default is: \fBdefault\fP\&. .UNINDENT .INDENT 0.0 .TP .B \-\-hls\-segment\-ignore\-names NAMES A comma\-delimited list of segment names that will get filtered out. .sp Example: \-\-hls\-segment\-ignore\-names 000,001,002 .sp This will ignore every segment that ends with 000.ts, 001.ts and 002.ts .sp Default is: \fBNone\fP\&. .UNINDENT .INDENT 0.0 .TP .B \-\-hls\-segment\-key\-uri URI Override the segment encryption key URIs for encrypted streams. .sp The value can be templated using the following variables, which will be replaced with their respective part from the source segment URI: .INDENT 7.0 .INDENT 3.5 .sp .nf .ft C {url} {scheme} {netloc} {path} {query} .ft P .fi .UNINDENT .UNINDENT .sp Examples: .INDENT 7.0 .INDENT 3.5 .sp .nf .ft C \-\-hls\-segment\-key\-uri "https://example.com/hls/encryption_key" \-\-hls\-segment\-key\-uri "{scheme}://1.2.3.4{path}{query}" \-\-hls\-segment\-key\-uri "{scheme}://{netloc}/custom/path/to/key" .ft P .fi .UNINDENT .UNINDENT .sp Default is: \fBNone\fP\&. .UNINDENT .INDENT 0.0 .TP .B \-\-hls\-audio\-select CODE Selects a specific audio source or sources, by language code or name, when multiple audio sources are available. Can be * to download all audio sources. .sp Examples: .INDENT 7.0 .INDENT 3.5 .sp .nf .ft C \-\-hls\-audio\-select "English,German" \-\-hls\-audio\-select "en,de" \-\-hls\-audio\-select "*" .ft P .fi .UNINDENT .UNINDENT .sp \fBNOTE:\fP .INDENT 7.0 .INDENT 3.5 This is only useful in special circumstances where the regular locale option fails, such as when multiple sources of the same language exists. .UNINDENT .UNINDENT .UNINDENT .INDENT 0.0 .TP .B \-\-hls\-start\-offset [HH:]MM:SS Amount of time to skip from the beginning of the stream. For live streams, this is a negative offset from the end of the stream (rewind). .sp Default is: \fB00:00:00\fP\&. .UNINDENT .INDENT 0.0 .TP .B \-\-hls\-duration [HH:]MM:SS Limit the playback duration, useful for watching segments of a stream. The actual duration may be slightly longer, as it is rounded to the nearest HLS segment. .sp Default is: \fBunlimited\fP\&. .UNINDENT .INDENT 0.0 .TP .B \-\-hls\-live\-restart Skip to the beginning of a live stream, or as far back as possible. .UNINDENT .SS FFmpeg options .INDENT 0.0 .TP .B \-\-ffmpeg\-ffmpeg FILENAME FFMPEG is used to access or mux separate video and audio streams. You can specify the location of the ffmpeg executable if it is not in your PATH. .sp Example: "/usr/local/bin/ffmpeg" .UNINDENT .INDENT 0.0 .TP .B \-\-ffmpeg\-verbose Write the console output from ffmpeg to the console. .UNINDENT .INDENT 0.0 .TP .B \-\-ffmpeg\-verbose\-path PATH Path to write the output from the ffmpeg console. .UNINDENT .INDENT 0.0 .TP .B \-\-ffmpeg\-fout OUTFORMAT When muxing streams, set the output format to OUTFORMAT. .sp Default is: \fB"matroska"\fP\&. .sp Example: "mpegts" .UNINDENT .INDENT 0.0 .TP .B \-\-ffmpeg\-video\-transcode CODEC When muxing streams, transcode the video to CODEC. .sp Default is: \fB"copy"\fP\&. .sp Example: "h264" .UNINDENT .INDENT 0.0 .TP .B \-\-ffmpeg\-audio\-transcode CODEC When muxing streams, transcode the audio to CODEC. .sp Default is: \fB"copy"\fP\&. .sp Example: "aac" .UNINDENT .INDENT 0.0 .TP .B \-\-ffmpeg\-copyts Forces the \-copyts ffmpeg option and does not remove the initial start time offset value. .UNINDENT .INDENT 0.0 .TP .B \-\-ffmpeg\-start\-at\-zero Enable the \-start_at_zero ffmpeg option when using copyts. .UNINDENT .SH HTTP OPTIONS .INDENT 0.0 .TP .B \-\-http\-proxy HTTP_PROXY A HTTP proxy to use for all HTTP and HTTPS requests, including WebSocket connections. .sp Example: "\fI\%http://hostname:port/\fP" .UNINDENT .INDENT 0.0 .TP .B \-\-http\-cookie KEY=VALUE A cookie to add to each HTTP request. .sp Can be repeated to add multiple cookies. .UNINDENT .INDENT 0.0 .TP .B \-\-http\-header KEY=VALUE A header to add to each HTTP request. .sp Can be repeated to add multiple headers. .UNINDENT .INDENT 0.0 .TP .B \-\-http\-query\-param KEY=VALUE A query parameter to add to each HTTP request. .sp Can be repeated to add multiple query parameters. .UNINDENT .INDENT 0.0 .TP .B \-\-http\-ignore\-env Ignore HTTP settings set in the environment such as environment variables (HTTP_PROXY, etc) or ~/.netrc authentication. .UNINDENT .INDENT 0.0 .TP .B \-\-http\-no\-ssl\-verify Don\(aqt attempt to verify SSL certificates. .sp Usually a bad idea, only use this if you know what you\(aqre doing. .UNINDENT .INDENT 0.0 .TP .B \-\-http\-disable\-dh Disable Diffie Hellman key exchange .sp Usually a bad idea, only use this if you know what you\(aqre doing. .UNINDENT .INDENT 0.0 .TP .B \-\-http\-ssl\-cert FILENAME SSL certificate to use. .sp Expects a .pem file. .UNINDENT .INDENT 0.0 .TP .B \-\-http\-ssl\-cert\-crt\-key CRT_FILENAME KEY_FILENAME SSL certificate to use. .sp Expects a .crt and a .key file. .UNINDENT .INDENT 0.0 .TP .B \-\-http\-timeout TIMEOUT General timeout used by all HTTP requests except the ones covered by other options. .sp Default is: \fB20.0\fP\&. .UNINDENT .SH PLUGIN OPTIONS .SS Afreeca .INDENT 0.0 .TP .B \-\-afreeca\-username USERNAME The username used to register with afreecatv.com. .UNINDENT .INDENT 0.0 .TP .B \-\-afreeca\-password PASSWORD A afreecatv.com account password to use with \fB\-\-afreeca\-username\fP\&. .UNINDENT .INDENT 0.0 .TP .B \-\-afreeca\-purge\-credentials Purge cached AfreecaTV credentials to initiate a new session and reauthenticate. .UNINDENT .SS Bbciplayer .INDENT 0.0 .TP .B \-\-bbciplayer\-username USERNAME The username used to register with bbc.co.uk. .UNINDENT .INDENT 0.0 .TP .B \-\-bbciplayer\-password PASSWORD A bbc.co.uk account password to use with \fB\-\-bbciplayer\-username\fP\&. .UNINDENT .INDENT 0.0 .TP .B \-\-bbciplayer\-hd Prefer HD streams over local SD streams, some live programmes may not be broadcast in HD. .UNINDENT .SS Clubbingtv .INDENT 0.0 .TP .B \-\-clubbingtv\-username The username used to register with Clubbing TV. .UNINDENT .INDENT 0.0 .TP .B \-\-clubbingtv\-password A Clubbing TV account password to use with \fB\-\-clubbingtv\-username\fP\&. .UNINDENT .SS Crunchyroll .INDENT 0.0 .TP .B \-\-crunchyroll\-username USERNAME A Crunchyroll username to allow access to restricted streams. .UNINDENT .INDENT 0.0 .TP .B \-\-crunchyroll\-password [PASSWORD] A Crunchyroll password for use with \fB\-\-crunchyroll\-username\fP\&. .sp If left blank you will be prompted. .UNINDENT .INDENT 0.0 .TP .B \-\-crunchyroll\-purge\-credentials Purge cached Crunchyroll credentials to initiate a new session and reauthenticate. .UNINDENT .INDENT 0.0 .TP .B \-\-crunchyroll\-session\-id SESSION_ID Set a specific session ID for crunchyroll, can be used to bypass region restrictions. If using an authenticated session ID, it is recommended that the authentication parameters be omitted as the session ID is account specific. .sp \fBNOTE:\fP .INDENT 7.0 .INDENT 3.5 The session ID will be overwritten if authentication is used and the session ID does not match the account. .UNINDENT .UNINDENT .UNINDENT .SS Funimationnow .INDENT 0.0 .TP .B \-\-funimation\-email Email address for your Funimation account. .UNINDENT .INDENT 0.0 .TP .B \-\-funimation\-password Password for your Funimation account. .UNINDENT .INDENT 0.0 .TP .B \-\-funimation\-language The audio language to use for the stream; japanese or english. .sp Default is: \fB"english"\fP\&. .UNINDENT .SS Nicolive .INDENT 0.0 .TP .B \-\-niconico\-email EMAIL The email or phone number associated with your Niconico account .UNINDENT .INDENT 0.0 .TP .B \-\-niconico\-password PASSWORD The password of your Niconico account .UNINDENT .INDENT 0.0 .TP .B \-\-niconico\-user\-session VALUE Value of the user\-session token (can be used in case you do not want to put your password here) .UNINDENT .INDENT 0.0 .TP .B \-\-niconico\-purge\-credentials Purge cached Niconico credentials to initiate a new session and reauthenticate. .UNINDENT .INDENT 0.0 .TP .B \-\-niconico\-timeshift\-offset [HH:]MM:SS Amount of time to skip from the beginning of a stream. Default is 00:00:00. .UNINDENT .SS Openrectv .INDENT 0.0 .TP .B \-\-openrectv\-email EMAIL The email associated with your openrectv account, required to access any openrectv stream. .UNINDENT .INDENT 0.0 .TP .B \-\-openrectv\-password PASSWORD An openrectv account password to use with \fB\-\-openrectv\-email\fP\&. .UNINDENT .SS Pixiv .INDENT 0.0 .TP .B \-\-pixiv\-sessionid SESSIONID The pixiv.net sessionid that\(aqs used in pixivs PHPSESSID cookie. can be used instead of the username/password login process. .UNINDENT .INDENT 0.0 .TP .B \-\-pixiv\-devicetoken DEVICETOKEN The pixiv.net device token that\(aqs used in pixivs device_token cookie. can be used instead of the username/password login process. .UNINDENT .INDENT 0.0 .TP .B \-\-pixiv\-purge\-credentials Purge cached Pixiv credentials to initiate a new session and reauthenticate. .UNINDENT .INDENT 0.0 .TP .B \-\-pixiv\-performer USER Select a co\-host stream instead of the owner stream. .UNINDENT .SS Sbscokr .INDENT 0.0 .TP .B \-\-sbscokr\-id CHANNELID Channel ID to play. .sp Example: .INDENT 7.0 .INDENT 3.5 .sp .nf .ft C streamlink http://play.sbs.co.kr/onair/pc/index.html best \-\-sbscokr\-id S01 .ft P .fi .UNINDENT .UNINDENT .UNINDENT .SS Schoolism .INDENT 0.0 .TP .B \-\-schoolism\-email EMAIL The email associated with your Schoolism account, required to access any Schoolism stream. .UNINDENT .INDENT 0.0 .TP .B \-\-schoolism\-password PASSWORD A Schoolism account password to use with \fB\-\-schoolism\-email\fP\&. .UNINDENT .INDENT 0.0 .TP .B \-\-schoolism\-part PART Play part number PART of the lesson, or assignment feedback video. .sp Defaults is 1. .UNINDENT .SS Steam .INDENT 0.0 .TP .B \-\-steam\-email EMAIL A Steam account email address to access friends/private streams .UNINDENT .INDENT 0.0 .TP .B \-\-steam\-password PASSWORD A Steam account password to use with \fB\-\-steam\-email\fP\&. .UNINDENT .SS Streann .INDENT 0.0 .TP .B \-\-streann\-url URL Source URL where the iframe is located, only required for direct URLs of \fIott.streann.com\fP .UNINDENT .SS Twitcasting .INDENT 0.0 .TP .B \-\-twitcasting\-password PASSWORD Password for private Twitcasting streams. .UNINDENT .SS Twitch .INDENT 0.0 .TP .B \-\-twitch\-disable\-hosting Do not open the stream if the target channel is hosting another channel. .UNINDENT .INDENT 0.0 .TP .B \-\-twitch\-disable\-ads Skip embedded advertisement segments at the beginning or during a stream. Will cause these segments to be missing from the stream. .UNINDENT .INDENT 0.0 .TP .B \-\-twitch\-disable\-reruns Do not open the stream if the target channel is currently broadcasting a rerun. .UNINDENT .INDENT 0.0 .TP .B \-\-twitch\-low\-latency Enables low latency streaming by prefetching HLS segments. Sets \fB\-\-hls\-segment\-stream\-data\fP to true and \fB\-\-hls\-live\-edge\fP to 2, if it is higher. Reducing \fB\-\-hls\-live\-edge\fP to 1 will result in the lowest latency possible, but will most likely cause buffering. .sp In order to achieve true low latency streaming during playback, the player\(aqs caching/buffering settings will need to be adjusted and reduced to a value as low as possible, but still high enough to not cause any buffering. This depends on the stream\(aqs bitrate and the quality of the connection to Twitch\(aqs servers. Please refer to the player\(aqs own documentation for the required configuration. Player parameters can be set via \fB\-\-player\-args\fP\&. .sp \fBNOTE:\fP .INDENT 7.0 .INDENT 3.5 Low latency streams have to be enabled by the broadcasters on Twitch themselves. Regular streams can cause buffering issues with this option enabled due to the reduced \fB\-\-hls\-live\-edge\fP value. .UNINDENT .UNINDENT .UNINDENT .INDENT 0.0 .TP .B \-\-twitch\-api\-header KEY=VALUE A header to add to each Twitch API HTTP request. .sp Can be repeated to add multiple headers. .UNINDENT .SS Ustreamtv .INDENT 0.0 .TP .B \-\-ustream\-password PASSWORD A password to access password protected UStream.tv channels. .UNINDENT .SS Ustvnow .INDENT 0.0 .TP .B \-\-ustvnow\-username USERNAME Your USTV Now account username .UNINDENT .INDENT 0.0 .TP .B \-\-ustvnow\-password PASSWORD Your USTV Now account password .UNINDENT .SS Wwenetwork .INDENT 0.0 .TP .B \-\-wwenetwork\-email EMAIL The email associated with your WWE Network account, required to access any WWE Network stream. .UNINDENT .INDENT 0.0 .TP .B \-\-wwenetwork\-password PASSWORD A WWE Network account password to use with \fB\-\-wwenetwork\-email\fP\&. .UNINDENT .SS Yupptv .INDENT 0.0 .TP .B \-\-yupptv\-boxid BOXID The yupptv.com boxid that\(aqs used in the BoxId cookie. Can be used instead of the username/password login process. .UNINDENT .INDENT 0.0 .TP .B \-\-yupptv\-yuppflixtoken YUPPFLIXTOKEN The yupptv.com yuppflixtoken that\(aqs used in the YuppflixToken cookie. Can be used instead of the username/password login process. .UNINDENT .INDENT 0.0 .TP .B \-\-yupptv\-purge\-credentials Purge cached YuppTV credentials to initiate a new session and reauthenticate. .UNINDENT .SS Zattoo .INDENT 0.0 .TP .B \-\-zattoo\-email EMAIL The email associated with your zattoo account, required to access any zattoo stream. .UNINDENT .INDENT 0.0 .TP .B \-\-zattoo\-password PASSWORD A zattoo account password to use with \fB\-\-zattoo\-email\fP\&. .UNINDENT .INDENT 0.0 .TP .B \-\-zattoo\-purge\-credentials Purge cached zattoo credentials to initiate a new session and reauthenticate. .UNINDENT .INDENT 0.0 .TP .B \-\-zattoo\-stream\-types TYPES A comma\-delimited list of stream types which should be used, the following types are allowed: .INDENT 7.0 .IP \(bu 2 dash .IP \(bu 2 hls7 .UNINDENT .sp Default is: \fB"dash"\fP\&. .UNINDENT .SH AUTHOR Streamlink Contributors .SH COPYRIGHT 2022, Streamlink .\" Generated by docutils manpage writer. . ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/docs/_man.rst0000644000175100001710000000155000000000000015622 0ustar00runnerdocker:orphan: Streamlink ========== Synopsis -------- .. code-block:: console streamlink [OPTIONS] [STREAM] streamlink --loglevel debug youtu.be/VIDEO-ID best streamlink --player mpv --player-args '--no-border --no-keepaspect-window' twitch.tv/CHANNEL 1080p60 streamlink --player-external-http --player-external-http-port 8888 URL STREAM streamlink --output /path/to/file --http-timeout 60 URL STREAM streamlink --stdout URL STREAM | ffmpeg -i pipe:0 ... streamlink --http-header 'Authorization=OAuth TOKEN' --http-header 'Referer=URL' URL STREAM streamlink --hls-live-edge 5 --stream-segment-threads 5 'hls://https://host/playlist.m3u8' best streamlink --twitch-low-latency -p mpv -a '--cache=yes --demuxer-max-bytes=750k' twitch.tv/CHANNEL best Options ======= .. argparse:: :module: streamlink_cli.main :attr: parser_helper ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643136177.0248404 streamlink-3.1.1/docs/_static/0000755000175100001710000000000000000000000015603 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/docs/_static/apple-touch-icon.png0000644000175100001710000002747400000000000021476 0ustar00runnerdockerPNG  IHDR=2tEXtSoftwareAdobe ImageReadyqe<.IDATx} \GyWIsj4:,Fe6k'!MrJh6aarge!K [e]#h.ͥ94ڪz#[ƚy3=3~dI}(-o[-hoy+K!{TFζ0{~ѷ{w}ס^zu 0f?Tz:CJ '& UnaqNP1Ê(Q1(TK9~ك-nyN m/0ČM]G0LdHїžnT^ba@6c6,`&*Ζf}w7=@Q7O)0b{t!aUɗ[D<@{^vzz},@FÓyK AtD~;<@{Nyxא>&Pxt* P=J*#:lrhsLW`;P #g0(UZ &v0:]j_s#PHp޹ 揌;_盥;/5:UebfY cv\:(}'_yd;Q:sr]#"X8П!ÿdi0f\w9~WDoc&#|v;G! Y мR Fu ח`D ?v=z:HfwLYC&FơYaPWYi8Gů%FLNҰO_p=@A@y%ek"L'mxk?Jϟi| Yߋ* eivTLr_)8^Zt0K}L] *.x2sxB 8I:v@Tŀ lf]@%?>7zCYgzNZ'[#!5Tyv9 qy _s`o, z?y8ߠB6O]0O_ED4#:_ί7:˒0 etDY񱖇wt}:e잲!R~ |MO6WWq46`_Kx Ok&N73<@[zm) ^I Wv0`5/QY hG.h&oKΨzEɧS6cYbN:t5%gEIukE}S%Uu@}~Ls\E!Et2&&Cn' 4zP;.Vcf1*j,ls4dz|_9z9,rH w[/uR+WVO^20T'j\hfuS=Э&J>x]JZ?ؔ0*&ynzS[! Fiv8kkJ r8W%7:QCRvM^;tAP%u#ĩƸr+xݖDR ğ8.}'M? Z5 貏:wNa zM]sI{S% P= ~3S,ZN)7[HÃontxdW`[E vak._ `ha5Pt;BH@S>!A Enuib\uǓ>?NnN/Dh,S"Oh; ?h5O<`#Ϣ\+U6@k:Z[ HO..~^Yݢ/]a:~R4.zA{Mj:!QǓ#بG/HC$5"*%hbT <ot9w|7mt3. 9Onjt.Vy:e,z6FG62~D0m,|PLJ߸ gG0»m^w;D)P;@. -BdDlPDcf'1t0 n̾:EoSRaGo*UPֹGXP 8fdFïCl?/G $L,O hƓs?X/L'@V.x$ȓS!"@Ư71ĺ_stП97vQh`, P sn{-o oFDOG<06 @$dB%FH;:X/MQƃj.D['Aa8ni_G ݀l'ȎQՇKUz>l0B:u|FΜÛ ]tU.545%FVѲje *to,l{@GSR[, ØGm"eMDUz)\sׇ֚;Ɩhbҟ"/{` ME`e hu" 7= t9JFkc4HRX#:+wy2l];vfW"$+?Y?2q{_>QGtobRbnxYH%YKbL `GG)q2d Q$!k P :s wU2.Y7?ԏ㻓&lf~OoŷUCȁE3"&j") |QWzEیތt$(=5`ÂeQ9Y`V.wC^za8f3 jMhl'8xn~Tyt$~k22h  Bh>מ,Դx›[ /Z,Tf[s!H (4W2&ۣFNaɅkmN\T2Fe5rk~#p(Q k^#;E*N}x Fe8*^IQR1Q B4 .Cx0:`"xrn'`RĔkE5*6bDzNؑM #fY(.J5 h"2|\a|RoM=ꬳA42\ѿ)"hnԮ݀g QP_:NxzvrW&"3'~] vʌNr,(Ɂmav; @1*ؒ aŞhקG3+ݠj@ymϢq8в&' ϦkrU\jJDLvU;\h,t* N}SVv@pyWHG 41WTż󝧪Abì=M-m1;s- :;fi|ױȼS$RY J̥+ z6 ff81eSb$$9}BڜE*h/%*(vڬ6 |NH ag.R?Xp{Iw&Ehy >q_P:쌬%m΂ 1$-JkZ:1in}K$2ڋ7 j G6-N@Kkp=/JC| ߲ς__f 0y]J?1in%Gu e~H6DN^w@d7ΓU_+c ēKX8ԃ;H<Ս#6n|$Ko< `XjIe'Fc HSc*"9\ʚ<,, _[4pN W]\ťZ_)橇G9d~""*DQ:#ݷv _>Q=w2~g^sx--WVy'D>~> YJ Dg_eh_xI2\&;k԰( [ y T[JFCp/ 3ΥOIZ qeR;8.q̸3YnZr9Yٌ^k4!8^ɨʹ',1CfG |[8Kx4f僾sLGx5 KcyUZ4з *1M)A=/t ~IzC}]h)_:YJF}Y`/ȽmQmH^X()]5C7fiEr#^L1t0 'XgӘ{i+:];~M#Bb`iOW{cn;+qÆP$磎b4FZcΊQRm;!N>9=jg3ȗ.8<9#d3^4r5)C)Umeú %f3@.,c N%Dɢ'4hlS ]o2s nrʃB{o 6bz Qt`-bBQ> Jfq 0-h5iQg`~/[&F:UrOK 3_rh4*,M$|-מ x'&" [5` tTp]kA\Oο6> ACphɡ >x"g$t#|u h;3K>R <1^<%Mcl̚5w)Vbp}f<|N];:sEC {FfhE-UfJ$xZ3σvvÂx$ %hǔE[T"- ʅ33.MnWTN)9G^&?]ZCp+d價sTCvggBv7>*|IN=(>kO[Jıuڸ6z+=qY[@Xpmn ym-ˎhOHn m1kH)0|qǓHNmN:Wq8fUc̤_qZO2d c a&ElZK}s0l^MWw¸2O\?c\pƫәw"hˑ - HQ6/y.}c]i%c'Y f'RCl N5hyS%v:x4vYȭ'ST.Z>[Fr|<\ ×tA/z;DO %M7\^f;$=4jɬvPKOͺĬM+ziN\.opnOEo,eΡgDE9l&vZ֫H2>;}d9Z5h2ߍ q室>ޕ[%n\•2kDHLp:/BCxt)'JzJy6)ecmkv̾~ ē%_JZ7Z!d, -Cc"6CsEO>x&fkģ<6ҿl-NC=w"7yiclx@/(3.i"ٱմ,l}m;@d"zҋӋ16g!){k"lg㞹Ƭ3yQo 99y͸qJwWpR~7 bNe< +&n;Z؆n" DR3x|0a8r\EbiM]1MH|cԓOQOwb>9ǃ6g\0-Rq1:u]"˥3Z)D_r:Ŗj #֦:!+p j3Nw ch105&fSTƙ[K\]JK qO^CVǑ܁Խ>~̌xdk zGk]-z[46%C .JoSם%XB`&欵;B|<)suUGO՝ن+{Py~BpB/, JFf;s8b,0EbN,BB&W3C/EyHK"H e|tb *djtEwu XE1U4A[bh|8f,4Q .s ر Uq wȡЫc¬1PZ^Z[2> xEy(ErLy Hc|T1w]skQ+g&@\aWaYgco#J1'f惾 >۝}ȍ0Py_"x94bYcSUl6^G]Y!bYBHoQm1gI1#z phQc 7 ANBj¯g|Uh?xnNbbV`mFGϕPJ-'_?͋XAK'J "d3&Ajp8;sԠZ0f485bڑ&#K]g*~P2b{L 7k-|E'Lw=VrvzOJ&uxoFwXe}!\k؈_a@*@fwvƓDOy: S>2<0SWU} jwZЌ;=wк4X7Vr~0kvgȓc ŀBHU㟽k&5z;|9 بZ QēlG/Tw(p29S0,T|;Z~)Kzϴ o)QWZPӀzyq-q95;:_^K _ykA{hc&Ĭn7)(֚kUF@.~' 8g6۩Y_NrFg媚߷L?8v0nc5\L%Ƒeb<0~}:K\v!`o ',@Mԥ>g(ͧ@jN+xbd60yb$+5]\DQ[LuOKXӀv"S&miSU ٣ׯO/c-`NOL(gcܾnp,7~xIqxvP8C+ ”`pR.F/tD=SFɗC'Nƫ¨$-#>mPK{}z%PZN2SzQ˵cDPkF/e8S ȡu oՑq*?c{"UVf v8Eb$S'[;+p5:͑C+}C/]:]̐@4˫䰬9uDgs4]#Y*瀆~vݗ\loв\VBh)SU.]3pՇ99E`nfYt|GJz^`U~tĜqA}7b,J5nI{K;E%!0%QHϤsc{,0AZ 0xоUSQtIH1 Ӟ P- |i(.CjZf }6I ^ f'Vt}|y (2|> |S4џj<t}=?4m;E7ؔim)o\k>4+_67oSn`*K&/7agcW R6m*O?Gl(w햱,@"'_nH+ fn0*vzX9xp  T&MIeƯyX_kgF1. J D.@f&ѡ"]=}hCOf#9$wׯԇ<6? ś DE ȓ9 Y³kt낇3LMmr #ū6$x}fs^/#np.S_WS {Nº!TcYӌHi%YH&" ltPx&* "ƑpnQ15$8_N Vz_MZIӤ A hgYLdf#ٮ<-RDBQNJ݅Un Н#?D~§EF]FW:+(.)DsxΏ*:s{|/TJ&#B,Շ84Jy8,u/X%@[#]0UL> ޽n*=@/#Kg & DQ'U(>jdHGQC?zoZo72/^ک{f(?L= 4i2$S~X"no ptMQȮ)sY`KԨ}殮 BZ 3#}Ϭ}Nsw'K0#*|\#?T<@{&) @9,@Cdv%_ WzzO>Y?m }!t~$Qk?1[gDyeS :f/ B(\CuyRUơYoD7?#V-oy<@{[__Rw2QIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/docs/_static/favicon-16x16.png0000644000175100001710000000125400000000000020523 0ustar00runnerdockerPNG  IHDRatEXtSoftwareAdobe ImageReadyqe<NIDATxڄS]HSa~e.#lm-5[RlD^Ttm^vZ]4+pwA7^v$GJʵяR2xwNgg-z{}i62z~$}ֵ匍JQZ<&M䛁w B#."nK|rR&7-VTϙ>Cַ5D̕MY zӬVj~qa?yDrNQj|]=Mw%EjfekA9$ Rcˁ:f0{5L1/|dwTZuٌ\bf I%!:| /%t1{YOCo0׏>U`V)zI'} Mri؞M|=+I{f O#JVJJ?9#z2V~4Ӓ a~E't11_(SNL,S.Jr%m ,UIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/docs/_static/favicon-32x32.png0000644000175100001710000000274700000000000020527 0ustar00runnerdockerPNG  IHDR szztEXtSoftwareAdobe ImageReadyqe<IDATxڴW[SG=33  T<ŊySMY!ш U R%TD QTV xA@Q$^{zg墠tUNNw| ˲4Җ>Sqi\ai祐ѴVqMldjHM+:aY#/Bh]  b &)eHqVEL,ϲ9Gq)ͫ6K`H"x020<)lo3yVxeC:yV3bHAfUk H˄(zsfecuDhc(cb_@ m9 f=vpJvkJ`NrPזB-dERS,S^zz#fEq݉A]W d NF={0PJ`z"+%JEi XNx tblFȼzRLާL/ BO1~S>;[>)PKa.dO5~?m@G_T,qӂمq47-g֛  |@YgӵUEL3qq.&XC>q  5 *#+w*8vBdFq%v3"+0#Cqer@@`SK|U ~ڲ|XebSvt-{*j[iu}:|Zhb- jAEEOg<}G2T?^&@eo$"njzmrwKL A A A A A A L Iqxmqk(^" ZeEH A A A A A W" nqWcJh/ \LA A A A A A kqZrR A A {>bX^' A A A kqW[XR \% js8DoPL B kqW^QoEr6ha* P V]IkqWWWm*YdBB A M sYmqWWWWWWJ]h/ GgXmsl)WWWWW\Fg^k(j(o`rwNaWWWWWWaMszq zkB ZWWZB lu r/dx4 z6 dt.sPrJ( @ q u^sruYu ;sɯgm3r6itýv6sun~@F A A H Cpuvj trNN A A A A A A P Prt} v]t\\% A A A A A A A A A A ]& \sv[r :tȯhn3B A A A A A A A A A A A A B m2gtȽw:v tpEMA A A A A A A A A A A A A A A A F }@mtwrsTc!VB h/ R]Sj1 B A A A A A A A A A A A A N Mrs (s(qXWROsssss^]& A A A A A A A A A A A A A iv6s(qXW? ssYCXssr`( A A A A A A A A A A A A iv 6s(qXWhsNA A A b) `p\% A A A A A A A A A A A A it 7s(qX^ss^' A A A A B G `( LWNe- A A A A A A A A it 7s(qX[qsh/ A A A A A GsssssZY# A A A A A A it7s(qXW\scR A A A p6ss`H\ssrKK A A A A it 7s(qXWn+qsoAF A }?sQD \% D g. dssll2A A Gjt 7s(qXWWu2 jssfl2C ]& D IsHA D z=nsre, A Njt 7s(qXWWW]Jqss^G[ss|?A A A N _sbB Ujt 7s(qXWWWWWh%XsssssQC A A A A ^' ssU Wjt7s(qXWWWWWWWq. PZRv2 H B A A A A S ssa"Wjy7s(qXWWWWWWWWWWWe#nbe- A A A EsnZWjy7s(qXWWWWWWWWWWWi'qssZAUssHWWjv8s(qXWWWWWWWWWWWWj(\sssssZ[WWjv8wtVe#WWWWWWWWWWWWWv2 U`X}9 XWc!Ssz,sspH]WWWWWWWWWWWWWWWW\Fotx$r:sȳj~9 XWWWWWWWWWWWWX}9 it˺u ?t\s`o, WWWWWWWWWWp- `tu^t tsSc!WWWWWWe#Ust}t v spF\WW]Ipuvv 6tIJi}9 = ktu 0u Ws߼tֺrNq @(0` ̙f3s*uvs(*w rs۽sssټwkU wGu˻qdw:~?frv»x@t vAttfCU B B X" Fittw:@t utpNV D A A A A E X" Qqsu s u `to_e, B A A A A A A A A C i0 `ot޺u Yƀ u Wts_u9K A A A A A A A A A A A A L w;`rtu U z 2utk|>N C A A A A A A A A A A A A A A C N }?ksur 1v u~tmVY" A A A A A A A A A A A A A A A A A A A A X! Umsu~vt uotϺpYl2 E A A A A A A A A A A A A A A A A A A A A A A E i0 Upsнtpo u Ltνsi}8 [D A A A A A A A A A A A A A A A A A A A A A A A A A J k1 estϽt QUmsskM`WNB K a) DOKt8T F A A A A A A A A A A A A A A A A A A A I Ehrt̀ v sqs0 YWVKg. YpssssjI[% A A A A A A A A A A A A A A A A A A A C ]& itqv spYWWUp3 isssssssriCI A A A A A A A A A A A A A A A A A A A btqv spYWWc"cssqhaclsssrQH A A A A A A A A A A A A A A A A A A bsqv spYWWFsspJV D J g. QmssdP A A A A A A A A A A A A A A A A A A bsqv spYWXhss\J A A A A H g. arJG A A A A A A A A A A A A A A A A A A bsqv spYWb rssv:A A A A A A A D O F O e, ~@Dx<]& J A A A A A A A A A A A A bsqv spYWe#sssp5A A A A A A A A H y<assssnVk2 H A A A A A A A A A A bsqv spYW[ossLA A A A A A A B MosssssssmUW! A A A A A A A A A bsqv spYWWVsske- C A A A A B |?sssogdiqssspBP B A A A A A A csqv spYWWs0 mssfAQ A A A H bssgt8R I Z$ Dgsssr^u9I A A A A Fdsqv spYWW[Mrssqan3D A C Ssmg- B X" p4J D T MpsssoVP A A A Mdsqv spYWWW`SnsssqSX" F F o4_' C ]& gs\H A C g. `pssrQI A C Sdsqv spYWWWW[}9 ]qsssjG]& I N l2assfR A A A M y<csspp5A GUdtqv spYWWWWWXa@ nsssqjdfnsssMD A A A A C ]& gssbB LWetƀv spYWWWWWWWWe#SlsssssssqZH A A A A A A A }?ssrT QWetƀv spYWWWWWWWWW[v2 UmsssshEN A A A A A A A A ^' sssf+ UWetƀv spYWWWWWWWWWWW]l)@ JGz7 d#QL D A A A A A A A d+ sssf( WWety v spYWWWWWWWWWWWWWWWWWYCpdk1 J A A A A F Pssp_WWety v spYWWWWWWWWWWWWWWWWW`^ssoUk1 K C Q BnssSWWWety v spYWWWWWWWWWWWWWWWWW[Lrsssmc_fpssjk(WWWety v spYWWWWWWWWWWWWWWWWWW\B hqsssssssn@ YWWWety v sqv2 ZWWWWWWWWWWWWWWWWWWWi&Hhssssq`~: [WWYq. kty rslPb WWWWWWWWWWWWWWWWWWWZe#; OTLw3 b XWW`Ljsu UtQtϽsl= `XWWWWWWWWWWWWWWWWWWWWWWWWX_~: ittսv ]*t rpsлq[|8 \WWWWWWWWWWWWWWWWWWWWWW[{7 Zqtվvu{vs~snZm*XWWWWWWWWWWWWWWWWWWXm)Yntw wfr1tsmF c!YWWWWWWWWWWWWWWYc!Fmstv4q tVtsb? `WWWWWWWWWWWWa A csuw V yv[tpaw3 XWWWWWWWWY{7 cpt޼w Xy y ttqRi'ZWWWWZk)Vrttxu=ushHh&XXl)Kkttv 8t vAtļrfA Firtw :q UUw gt׾sstϻtZy &vw rw̙3UU././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/docs/_static/icon.svg0000644000175100001710000004424100000000000017261 0ustar00runnerdocker image/svg+xml ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/docs/_static/site.webmanifest0000644000175100001710000000046700000000000021004 0ustar00runnerdocker{ "name": "Streamlink documentation", "short_name": "Streamlink", "display": "standalone", "theme_color": "#121657", "background_color": "#ffffff", "icons": [ { "src": "/_static/icon.svg", "sizes": "1x1", "type": "image/svg" } ] } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643136177.0248404 streamlink-3.1.1/docs/_static/styles/0000755000175100001710000000000000000000000017126 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/docs/_static/styles/custom.css0000644000175100001710000001315600000000000021160 0ustar00runnerdocker/* Furo CSS variables https://github.com/pradyunsg/furo/blob/main/src/furo/assets/styles/variables/_index.scss https://github.com/pradyunsg/furo/blob/main/src/furo/theme/partials/_head_css_variables.html */ body, body[data-theme="auto"], body[data-theme="light"], body[data-theme="dark"] { /* adjust font-stack by adding "SF Pro Display" and "SF Mono" */ --font-stack: -apple-system, BlinkMacSystemFont, SF Pro Display, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji; --font-stack--monospace: SFMono-Regular, SF Mono, Menlo, Consolas, Monaco, Liberation Mono, Lucida Console, monospace; /* Streamlink colors */ --color-brand-primary: rgb(18, 22, 87); /* dark icon color (shadow icon color: rgb(13, 16, 65)) */ --color-brand-content: rgb(0, 115, 189); /* bright icon color */ /* misc */ --sidebar-width: 15rem; --sidebar-scrollbar-width: .5rem; --sidebar-item-spacing-vertical: .4rem; --admonition-font-size: var(--font-size--small); --admonition-title-font-size: var(--font-size--normal); --custom-thumb-color: var(--color-foreground-border); --custom-track-color: transparent; } body[data-theme="dark"] { --color-brand-primary: rgb(0, 115, 189); /* bright icon color */ --color-brand-content: rgb(0, 115, 189); /* bright icon color */ } @media (prefers-color-scheme: dark) { body[data-theme="auto"] { --color-brand-primary: rgb(0, 115, 189); /* bright icon color */ --color-brand-content: rgb(0, 115, 189); /* bright icon color */ } } /* Generic style overrides */ html { /* unset @media (min-width: $full-width + $sidebar-width) query which sets font-size to 110% */ font-size: 100% !important; } code.literal { font-size: var(--font-size--small); } strong.command { padding: .1em .2em; border-radius: .2em; background: var(--color-background-secondary); color: var(--color-api-name); font: normal var(--font-size--small) var(--font-stack--monospace); } /* Sidebar/Menubar and related */ .toc-scroll:not(:hover) { scrollbar-color: transparent !important; } .toc-scroll:not(:hover)::-webkit-scrollbar-thumb { background-color: transparent !important; } .sidebar-brand, .sidebar-versions, .sidebar-search, .github-buttons { box-sizing: border-box; width: calc(var(--sidebar-width) - 2 * var(--sidebar-scrollbar-width)) !important; margin-left: var(--sidebar-scrollbar-width) !important; margin-right: 0 !important; } .toc-title, .toc-tree > ul { padding-right: .5em; } .sidebar-logo-container { margin: 0; } .sidebar-logo { max-width: 62.5%; } .sidebar-brand { color: var(--color-sidebar-brand-text); } .sidebar-brand-text { font-size: 1.75rem; color: unset; } .sidebar-brand-oneliner { margin: 0; font-size: var(--font-size--small--2); color: unset; } .sidebar-versions { margin: .5rem 0; } .sidebar-versions a { color: inherit; } .sidebar-versions-current { font-family: var(--font-stack--monospace); font-size: var(--font-size--small--2); } .sidebar-versions-others { display: flex; margin: .5rem 0 0; font-size: var(--font-size--small); } .sidebar-versions-others dd { width: 50%; margin: 0; } .sidebar-versions-others dd:first-of-type { padding-right: .5rem; text-align: right; } .sidebar-versions-others dd:last-of-type { padding-left: .5rem; text-align: left; } .sidebar-versions-others .version-current { font-weight: bold; } .sidebar-search-container { border-top: 1px solid var(--color-sidebar-search-border); border-bottom: 1px solid var(--color-sidebar-search-border); } .sidebar-search { border: 0; padding-left: calc( var(--sidebar-item-spacing-horizontal) + var(--sidebar-search-input-spacing-horizontal) + var(--sidebar-search-icon-size) - var(--sidebar-scrollbar-width) ); } .github-buttons { margin: 1.5rem 0 1rem; } .toc-tree li.scroll-current > .reference { position: relative; font-weight: normal; } .toc-tree li.scroll-current > .reference::before { content: "\2023"; display: inline-block; position: absolute; left: -1em; width: 1em; color: var(--color-toc-item-text); font: normal normal 400 1em var(--font-stack); text-rendering: auto; -webkit-font-smoothing: antialiased; opacity: .75; } /* Components */ .admonition p.admonition-title { font-weight: bold; } .admonition.version-warning { padding-bottom: 0; } .admonition.version-warning > .admonition-title { margin-bottom: 0; font-weight: normal; } table.table-custom-layout { width: 100%; table-layout: fixed; } table.table-custom-layout colgroup { display: none; } table.table-custom-layout colgroup col { width: auto; } table.table-custom-layout th { text-align: left; } table.table-custom-layout th:first-of-type { width: 14rem; } table.table-custom-layout tbody tr td { overflow: unset; white-space: unset; } table.table-custom-layout tbody tr td:first-of-type { vertical-align: top; } table.table-custom-layout.table-custom-layout-platform-locations th:first-of-type { width: 7rem; } table.table-custom-layout.table-custom-layout-platform-locations tbody>tr>td:last-of-type { overflow-x: auto; } table.table-custom-layout.table-custom-layout-platform-locations ul { margin: 0; padding: 0; list-style: none; } table.table-custom-layout.table-custom-layout-platform-locations code { white-space: pre; } .option .sig-name, .option .sig-prename { font-family: var(--font-stack--monospace); } /* Content */ .github-avatar { float: left; width: 150px; height: 150px; margin: 0 1rem 1rem 0; } .github-avatar + .container > ul > li { list-style: none; } /* Utils */ .clearfix:before, .clearfix:after { display: table; content: ""; } .clearfix:after { clear: both; } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643136177.0248404 streamlink-3.1.1/docs/_templates/0000755000175100001710000000000000000000000016312 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/docs/_templates/base.html0000644000175100001710000000070500000000000020114 0ustar00runnerdocker{% extends '!base.html' %} {%- block site_meta -%} {{ super() }} {%- endblock -%} ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/docs/_templates/page.html0000644000175100001710000000046200000000000020116 0ustar00runnerdocker{% extends '!page.html' %} {% block content %} {% if release != version %}

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

{% endif %} {{ super() }} {% endblock %} ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643136177.0248404 streamlink-3.1.1/docs/_templates/sidebar/0000755000175100001710000000000000000000000017723 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/docs/_templates/sidebar/brand.html0000644000175100001710000000154500000000000021704 0ustar00runnerdocker ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/docs/_templates/sidebar/github-buttons.html0000644000175100001710000000100700000000000023565 0ustar00runnerdocker ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/docs/api.rst0000644000175100001710000000213400000000000015460 0ustar00runnerdockerAPI Reference ============= .. module:: streamlink This ia reference of all the available API methods in Streamlink. Streamlink ------------ .. autofunction:: streams Session ------- .. autoclass:: Streamlink :members: Plugins ------- .. module:: streamlink.plugin .. autoclass:: Plugin :members: .. module:: streamlink.options .. autoclass:: Arguments :members: .. autoclass:: Argument :members: .. automethod:: __init__ Streams ------- All streams inherit from the :class:`Stream` class. .. module:: streamlink.stream .. autoclass:: Stream :members: Stream subclasses ^^^^^^^^^^^^^^^^^ You are able to inspect the parameters used by each stream, different properties are available depending on stream type. .. autoclass:: HLSStream :members: .. autoclass:: HTTPStream :members: .. autoclass:: DASHStream :members: Exceptions ---------- Streamlink has three types of exceptions: .. autoexception:: streamlink.StreamlinkError .. autoexception:: streamlink.PluginError .. autoexception:: streamlink.NoPluginError .. autoexception:: streamlink.StreamError ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/docs/api_guide.rst0000644000175100001710000000553100000000000016641 0ustar00runnerdockerAPI Guide ========= This API is what powers the :ref:`cli ` but is also available to developers that wish to make use of the data Streamlink can retrieve in their own application. Extracting streams ------------------ The simplest use of the Streamlink API looks like this: .. code-block:: python >>> import streamlink >>> streams = streamlink.streams("https://twitch.tv/day9tv") This simply attempts to find a plugin and use it to extract streams from the URL. This works great in simple cases but if you want more fine tuning you need to use a `session object`_ instead. The returned value is a dict containing :class:`Stream ` objects: .. code-block:: python >>> streams {'best': , 'high': , 'low': , 'medium': , 'mobile': , 'source': , 'worst': } If no plugin for the URL is found, a :exc:`NoPluginError` will be raised. If an error occurs while fetching streams, a :exc:`PluginError` will be raised. Opening streams to read data ---------------------------- Now that you have extracted some streams we might want to read some data from one of them. When you call `open()` on a stream, a file-like object will be returned, which you can call `.read(size)` and `.close()` on. .. code-block:: python >>> stream = streams["source"] >>> fd = stream.open() >>> data = fd.read(1024) >>> fd.close() If an error occurs while opening a stream, a :exc:`StreamError` will be raised. Inspecting streams ------------------ It's also possible to inspect streams internal parameters, go to :ref:`Stream subclasses ` to see what attributes are available for inspection for each stream type. For example this is a :class:`HLSStream ` object which contains a `url` attribute. .. code-block:: python >>> stream.url 'https://video38.ams01.hls.twitch.tv/hls11/ ...' Session object -------------- The session allows you to set various options and is more efficient when extracting streams more than once. You start by creating a :class:`Streamlink` object: .. code-block:: python >>> from streamlink import Streamlink >>> session = Streamlink() You can then extract streams like this: .. code-block:: python >>> streams = session.streams("https://twitch.tv/day9tv") or set options like this: .. code-block:: python >>> session.set_option("stream-timeout", 30) See :func:`Streamlink.set_option` to see which options are available. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/docs/applications.rst0000644000175100001710000000160600000000000017400 0ustar00runnerdocker.. include:: _applications.rst Streamlink Applications ======================= Streamlink Twitch GUI --------------------- .. image:: https://user-images.githubusercontent.com/467294/28097570-3415020e-66b1-11e7-928d-4b9da35daf13.jpg :alt: Streamlink Twitch GUI :Description: A multi platform Twitch.tv browser for Streamlink :Type: Graphical User Interface :OS: |Windows| |MacOS| |Linux| :Author: `Sebastian Meyer `_ :Website: https://streamlink.github.io/streamlink-twitch-gui :Source code: https://github.com/streamlink/streamlink-twitch-gui :Info: A NW.js based desktop web application, formerly known as *Livestreamer Twitch GUI*. Browse Twitch.tv and watch multiple streams at once. Filter streams by language, receive desktop notifications when followed channels start streaming and access the Twitch chat by using customizable chat applications. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/docs/changelog.md0000644000175100001710000051757300000000000016450 0ustar00runnerdocker# Changelog ## streamlink 3.1.1 (2022-01-25) Patch release: - Fixed: broken `streamlink.exe`/`streamlinkw.exe` executables in Windows installer ([#4308](https://github.com/streamlink/streamlink/pull/4308)) ```text Mozi <29089388+pzhlkj6612@users.noreply.github.com> (1): cli: tell users the stream could be saved or piped back-to (1): plugins.twitcasting: Fix error messages bastimeyer (1): installer: set pynsist to 2.7 and distlib to 0.3.3 ``` ## streamlink 3.1.0 (2022-01-22) Release highlights: - Changed: file overwrite prompt to wait for user input before opening streams ([#4252](https://github.com/streamlink/streamlink/pull/4252)) - Fixed: log messages appearing in `--json` output ([#4258](https://github.com/streamlink/streamlink/pull/4258)) - Fixed: keep-alive TCP connections when filtering out HLS segments ([#4229](https://github.com/streamlink/streamlink/pull/4229)) - Fixed: sort order of DASH streams with the same video resolution ([#4220](https://github.com/streamlink/streamlink/pull/4220)) - Fixed: HLS segment byterange offsets ([#4301](https://github.com/streamlink/streamlink/pull/4301), [#4302](https://github.com/streamlink/streamlink/pull/4302)) - Fixed: YouTube /live URLs ([#4222](https://github.com/streamlink/streamlink/pull/4222)) - Fixed: UStream websocket address ([#4238](https://github.com/streamlink/streamlink/pull/4238)) - Fixed: Pluto desync issues by filtering out bumper segments ([#4255](https://github.com/streamlink/streamlink/pull/4255)) - Fixed: various plugin issues - please see the changelog down below - Removed plugins: abweb ([#4270](https://github.com/streamlink/streamlink/pull/4270)), latina ([#4269](https://github.com/streamlink/streamlink/pull/4269)), live_russia_tv ([#4263](https://github.com/streamlink/streamlink/pull/4263)), liveme ([#4264](https://github.com/streamlink/streamlink/pull/4264)) ```text Christian Kündig (1): plugins.yupptv: override encoding, set Origin header (#4261) Ian Cameron <1661072+mkbloke@users.noreply.github.com> (4): plugins.pluto: rewrite/fix plugins.albavision: fix/update plugins.albavision: update plugin_matrix.rst plugins.pluto: add filtering of bumper segments PleasantMachine9 <65126927+PleasantMachine9@users.noreply.github.com> (1): stream.hls: read and discard filtered sequences properly back-to (8): stream.dash: sort video duplicated resolutions by bandwidth plugins.onetv: added support for channel with different timezone +4 plugins.ceskatelevize: Fix Livestreams plugins.mediavitrina: better support for different channel names plugins.live_russia_tv: removed outdated plugin plugins.liveme: removed plugins.abweb: removed plugins.dogus: update and cleanup bastimeyer (21): plugins.youtube: fix metadata on /live URLs plugins.ustreamtv: fix websocket address plugins.steam: refactor plugin plugins.stadium: rewrite cli: create file output before opening the stream logger: change NONE loglevel to sys.maxsize cli.console: ignore msg() calls if json=True tests: fix named pipe being created in CLI tests plugins.vtvgo: remove itertags plugins.vk: rewrite and remove itertags plugins.latina: remove plugin plugins.streann: remove itertags plugins.nos: remove itertags tests: rewrite plugins_meta tests 2022 plugins.foxtr: fix regex plugins.delfi: rewrite plugin plugins.twitch: fix pluginmatcher regex docs: fix linux package infos stream.hls: fix byterange parser stream.hls: refactor segment byterange calculation zappepappe (1): plugins.svtplay: fix live channel URL matching (#4219) ``` ## streamlink 3.0.3 (2021-11-27) Patch release: - Fixed: broken output of the `--help` CLI argument ([#4213](https://github.com/streamlink/streamlink/pull/4213)) - Fixed: parsing of invalid HTML5 documents ([#4210](https://github.com/streamlink/streamlink/pull/4210)) Please see the [changelog of 3.0.0](https://streamlink.github.io/changelog.html#streamlink-3-0-0-2021-11-17), as it contains breaking changes that may require user interaction. ```text bastimeyer (3): utils.parse: parse invalid XHTML5 documents cli: prioritize --help and fix its output plugins.youtube: add category metadata ``` ## streamlink 3.0.2 (2021-11-25) Patch release: - Added: support for the `id` plugin metadata property ([#4203](https://github.com/streamlink/streamlink/pull/4203)) - Updated: Twitch access token request parameter regarding embedded ads ([#4194](https://github.com/streamlink/streamlink/pull/4194)) - Fixed: early `SIGINT`/`SIGTERM` signal handling ([#4190](https://github.com/streamlink/streamlink/pull/4190)) - Fixed: broken character set decoding when parsing HTML documents ([#4201](https://github.com/streamlink/streamlink/pull/4201)) - Fixed: missing home directory expansion (tilde character) in file output paths ([#4204](https://github.com/streamlink/streamlink/pull/4204)) - New plugin: tviplayer ([#4199](https://github.com/streamlink/streamlink/pull/4199)) ```text back-to (1): plugins.tviplayer: new plugin bastimeyer (14): cli: override default signal handlers chore: add GH gist link to issue templates plugins.twitch: set playerType back to embed plugins.twitch: add type annotations plugins.twitch: avg duration for prefetch segments plugins.ard_mediathek: rewrite plugin utils.parse: fix encoding in parse_html plugins.ard_mediathek: fix plugin cli: expand user in file output paths cli.output: remove MPV title variable escape logic plugin: add 'id' metadata property plugins.youtube: add 'id' metadata plugins.twitch: add 'id' metadata docs: add dedicated metadata variables section kyldery (1): plugins.crunchyroll: add metadata attributes (#4185) ``` ## streamlink 3.0.1 (2021-11-17) Patch release: - Fixed: broken pycountry import in Windows installer's Python environment ([#4180](https://github.com/streamlink/streamlink/pull/4180)) ```text bastimeyer (1): installer: rewrite wheels config, fix pycountry ``` ## streamlink 3.0.0 (2021-11-17) Breaking changes: - BREAKING: dropped support for RTMP, HDS and AkamaiHD streams ([#4169](https://github.com/streamlink/streamlink/pull/4169), [#4168](https://github.com/streamlink/streamlink/pull/4168)) - removed the `rtmp://`, `hds://` and `akamaihd://` protocol plugins - removed all Flash related code - upgraded all plugins using these old streaming protocols - dropped RTMPDump dependency - BREAKING: removed the following CLI arguments (and respective session options): ([#4169](https://github.com/streamlink/streamlink/pull/4169), [#4168](https://github.com/streamlink/streamlink/pull/4168)) - `--rtmp-rtmpdump`, `--rtmpdump`, `--rtmp-proxy`, `--rtmp-timeout` Users of Streamlink's Windows installer will need to update their [config file](https://streamlink.github.io/cli.html#configuration-file). - `--subprocess-cmdline`, `--subprocess-errorlog`, `--subprocess-errorlog-path` - `--hds-live-edge`, `--hds-segment-attempts`, `--hds-segment-threads`, `--hds-segment-timeout`, `--hds-timeout` - BREAKING: switched from HTTP to HTTPS for all kinds of scheme-less input URLs. If a site or http-proxy doesn't support HTTPS, then HTTP needs to be set explicitly. ([#4068](https://github.com/streamlink/streamlink/pull/4068), [#4053](https://github.com/streamlink/streamlink/pull/4053)) - BREAKING/API: changed `Session.resolve_url()` and `Session.resolve_url_no_redirect()` to return a tuple of a plugin class and the resolved URL instead of an initialized plugin class instance. This fixes the availability of plugin options in a plugin's constructor. ([#4163](https://github.com/streamlink/streamlink/pull/4163)) - BREAKING/requirements: dropped alternative dependency `pycrypto` and removed the `STREAMLINK_USE_PYCRYPTO` env var switch ([#4174](https://github.com/streamlink/streamlink/pull/4174)) - BREAKING/requirements: switched from `iso-639`+`iso3166` to `pycountry` and removed the `STREAMLINK_USE_PYCOUNTRY` env var switch ([#4175](https://github.com/streamlink/streamlink/pull/4175)) - BREAKING/setup: disabled unsupported Python versions, disabled the deprecated `test` setuptools command, removed the `NO_DEPS` env var, and switched to declarative package data via `setup.cfg` ([#4079](https://github.com/streamlink/streamlink/pull/4079), [#4107](https://github.com/streamlink/streamlink/pull/4107), [#4115](https://github.com/streamlink/streamlink/pull/4115), [#4113](https://github.com/streamlink/streamlink/pull/4113)) Release highlights: - Deprecated: `--https-proxy` in favor of a single `--http-proxy` CLI argument (and respective session option). Both now set the same proxy for all HTTPS/HTTP requests and websocket connections. [`--https-proxy` will be removed in a future release.](https://streamlink.github.io/deprecations.html#streamlink-3-0-0) ([#4120](https://github.com/streamlink/streamlink/pull/4120)) - Added: official support for Python 3.10 ([#4144](https://github.com/streamlink/streamlink/pull/4144)) - Added: `--twitch-api-header` for only setting Twitch.tv API requests headers (for authentication, etc.) as an alternative to `--http-header` ([#4156](https://github.com/streamlink/streamlink/pull/4156)) - Added: BASH and ZSH completions to sdist tarball and wheels. ([#4048](https://github.com/streamlink/streamlink/pull/4048), [#4178](https://github.com/streamlink/streamlink/pull/4178)) - Added: support for creating parent directories via metadata variables in file output paths ([#4085](https://github.com/streamlink/streamlink/pull/4085)) - Added: new WebsocketClient implementation ([#4153](https://github.com/streamlink/streamlink/pull/4153)) - Updated: plugins using websocket connections - nicolive, ustreamtv, twitcasting ([#4155](https://github.com/streamlink/streamlink/pull/4155), [#4164](https://github.com/streamlink/streamlink/pull/4164), [#4154](https://github.com/streamlink/streamlink/pull/4154)) - Updated: circumvention for YouTube's age verification ([#4058](https://github.com/streamlink/streamlink/pull/4058)) - Updated: and fixed lots of other plugins, see the detailed changelog below - Reverted: HLS segment downloads always being streamed, and added back `--hls-segment-stream-data` to prevent connection issues ([#4159](https://github.com/streamlink/streamlink/pull/4159)) - Fixed: URL percent-encoding for sites which require the lowercase format ([#4003](https://github.com/streamlink/streamlink/pull/4003)) - Fixed: XML parsing issues ([#4075](https://github.com/streamlink/streamlink/pull/4075)) - Fixed: broken `method` parameter when using the `httpstream://` protocol plugin ([#4171](https://github.com/streamlink/streamlink/pull/4171)) - Fixed: test failures when the `brotli` package is installed ([#4022](https://github.com/streamlink/streamlink/pull/4022)) - Requirements: bumped `lxml` to `>4.6.4,<5.0` and `websocket-client` to `>=1.2.1,<2.0` ([#4143](https://github.com/streamlink/streamlink/pull/4143), [#4153](https://github.com/streamlink/streamlink/pull/4153)) - Windows installer: upgraded Python to `3.9.8` and FFmpeg to `n4.4.1` ([#4176](https://github.com/streamlink/streamlink/pull/4176), [#4124](https://github.com/streamlink/streamlink/pull/4124)) - Documentation: upgraded to first stable version of the Furo theme ([#4000](https://github.com/streamlink/streamlink/pull/4000)) - New plugins: pandalive ([#4064](https://github.com/streamlink/streamlink/pull/4064)) - Removed plugins: tga ([#4129](https://github.com/streamlink/streamlink/pull/4129)), viasat ([#4087](https://github.com/streamlink/streamlink/pull/4087)), viutv ([#4018](https://github.com/streamlink/streamlink/pull/4018)), webcast_india_gov ([#4024](https://github.com/streamlink/streamlink/pull/4024)) ```text Ian Cameron <1661072+mkbloke@users.noreply.github.com> (4): plugins.bbciplayer: remove HDSStream, upgrade scheme (#4041) plugins.pandalive: new plugin plugins.facebook: update onion address plugins.picarto: update URL regex and logic MinePlayersPE (1): plugins.youtube: better API age-gate bypassing (#4058) back-to (14): ci: temporary windows python 3.10 fix for missing `lxml 4.6.3` wheel stream.hls: Fix error msg for 'Unable to decrypt cipher ...' plugins.viutv: removed plugins.webcast_india_gov: removed plugins.oneplusone: cleanup and add auto session reload (#4049) plugins.showroom: cleanup (#4065) plugins.tv999: use parse_html plugins.ssh101: use parse_html plugins.app17: remove RTMPStream, cleanup plugins.viasat: removed plugins.twitch: add device-id headers (#4086) plugin.api: update useragents plugins.twitch: new plugin command --twitch-api-header plugins.goltelevision: fix api url and update plugin url bastimeyer (70): docs: fix CLI argument example in manpage docs: bump furo docs req to 2021.09.08 http_session: override urllib3 percent-encoding installer: upgrade python from 3.9.6 to 3.9.7 tests: fix typo in pytest skipif marker tests: fix deprecated module imports on py310 plugins.ardlive: rewrite plugin utils: replace LazyFormatter with new Formatter utils: move all URL methods to utils.url tests: fix Accept-Encoding headers in stream_json plugins.pluzz: rewrite plugin plugins: clean up imports of parse_* utils utils: split into submodules and fix imports plugins.artetv: rewrite plugin using v2 API plugins.bloomberg: rewrite plugin stream: clean up imports tests: move tests/streams to tests/stream plugins.earthcam: rewrite plugin, remove rtmp build: include bash and zsh completions in wheels plugins.picarto: fix HLS URL hostname utils.url: make update_scheme always update target plugins: fix update_scheme calls plugins.bfmtv: rewrite plugin using XPath plugins.youtube: replace itertags with XPath tests: fix partial coverage in can_handle_url session: don't override https-proxy scheme session: move from http to https as default scheme plugins.brightcove: rewrite plugin ci.github: add regular py310 test runners utils.parse: fix ignore_ns in parse_xml script: fix update-removed-plugins bash script plugins.tv5monde: remove plugin plugins.tv5monde: re-implement plugin setup: show error on older python versions cli: refactor FileOutput and Formatter plugin.api: remove StreamMapper plugins.okru: rewrite plugin, drop RTMP ci.github: switch to codecov-action@v2 setup: disable test command docs: fix Solus package link plugins.twitch: remove device-id headers installer: remove unneeded 3rd party license texts setup: switch to declarative package metadata setup: remove NO_DEPS env var plugin: trim metadata strings plugins.brightcove: add more HLS source types installer: bump ffmpeg to n4.4.1 plugins.tga: remove plugin vendor: bump lxml to >4.6.4,<5.0 setup: add Python 3.10 to classifiers list ci.github: check for unicode bidi control chars installer: bump lxml to 4.6.4 logger: fix warning import and trace export plugin.api: implement WebsocketClient plugins.twitcasting: re-implement websocket client plugins.nicolive: re-implement plugin revert: stream.hls: remove hls-segment-stream-data option plugin.api.websocket: add reconnect method plugins.ustreamtv: re-implement plugin session.resolve_url: return plugin class + URL cli.main: add plugin type annotations plugins.twitch: refactor api-headers streams: remove HDS/AkamaiHD and flashmedia pkg stream: remove RTMP and RTMPDump dependency plugins.rtmp: add to removed plugins list stream.http: fix custom method argument setup: drop pycrypto support setup: drop iso-639/iso3166, default to pycountry installer: upgrade python from 3.9.7 to 3.9.8 setup: include shell completions in sdist beardypig (2): cli: deprecate the --https-proxy option as well as the Session options plugins.ltv_lsm_lv: update the plugin for the new page layout nnrm <91910832+nnrm@users.noreply.github.com> (1): plugins.nicolive: add support for community urls vinyl-umbrella <61788251+vinyl-umbrella@users.noreply.github.com> (1): plugins.openrectv: be able to get subscription video (#4130) ``` ## streamlink 2.4.0 (2021-09-07) Release highlights: - Deprecated: stream-type specific stream transport options in favor of generic options ([#3893](https://github.com/streamlink/streamlink/pull/3893)) - use `--stream-segment-attempts` instead of `--{dash,hds,hls}-segment-attempts` - use `--stream-segment-threads` instead of `--{dash,hds,hls}-segment-threads` - use `--stream-segment-timeout` instead of `--{dash,hds,hls}-segment-timeout` - use `--stream-timeout` instead of `--{dash,hds,hls,rtmp,http-stream}-timeout` See the documentation's [deprecations page](https://streamlink.github.io/latest/deprecations.html#streamlink-2-4-0) for more information. - Deprecated: `--hls-segment-stream-data` option and made it always stream segment data ([#3894](https://github.com/streamlink/streamlink/pull/3894)) - Updated: Python version of the Windows installer from 3.8 to 3.9 and dropped support for Windows 7 due to Python incompatibilities ([#3918](https://github.com/streamlink/streamlink/pull/3918)) See the documentation's [install page](https://streamlink.github.io/install.html) for alternative installation methods on Windows 7. - Updated: FFmpeg in the Windows Installer from 4.2 (Zeranoe) to 4.4 ([streamlink/FFmpeg-Builds](https://github.com/streamlink/FFmpeg-Builds)) ([#3981](https://github.com/streamlink/streamlink/pull/3981)) - Added: `{author}`, `{category}`/`{game}`, `{title}` and `{url}` variables to `--output`, `--record` and `--record-and-play` ([#3962](https://github.com/streamlink/streamlink/pull/3962)) - Added: `{time}`/`{time:custom-format}` variable to `--title`, `--output`, `--record` and `--record-and-play` ([#3993](https://github.com/streamlink/streamlink/pull/3993)) - Added: `--fs-safe-rules` for changing character replacement rules in file outputs ([#3962](https://github.com/streamlink/streamlink/pull/3962)) - Added: plugin metadata to `--json` stream data output ([#3987](https://github.com/streamlink/streamlink/pull/3987)) - Fixed: named pipes not being cleaned up by FFMPEGMuxer ([#3992](https://github.com/streamlink/streamlink/pull/3992)) - Fixed: KeyError on invalid variables in `--player-args` ([#3988](https://github.com/streamlink/streamlink/pull/3988)) - Fixed: tests failing in certain cases when run in different order ([#3920](https://github.com/streamlink/streamlink/pull/3920)) - Fixed: initial HLS playlist parsing issues ([#3903](https://github.com/streamlink/streamlink/pull/3903), [#3910](https://github.com/streamlink/streamlink/pull/3910)) - Fixed: various plugin issues. Please see the changelog down below. - Dependencies: added `lxml>=4.6.3` ([#3952](https://github.com/streamlink/streamlink/pull/3952)) - Dependencies: switched back to `requests>=2.26.0` on Windows ([#3930](https://github.com/streamlink/streamlink/pull/3930)) - Removed plugins: animeworld ([#3951](https://github.com/streamlink/streamlink/pull/3951)), gardenersworld ([#3966](https://github.com/streamlink/streamlink/pull/3966)), huomao ([#3932](https://github.com/streamlink/streamlink/pull/3932)) ```text Grabien <60237587+Grabien@users.noreply.github.com> (1): plugins.nbcnews: fix stream URL extraction (#3909) Ian Cameron <1661072+mkbloke@users.noreply.github.com> (2): plugins.huomao: plugin removal plugins.pluto: fix URL match for 2 letter language codes Leonardo Nascimento (1): plugins.booyah: add support for source stream (#3969) back-to (9): stream.hls: handle exception StreamError in Thread-HLSStreamWorker - iter_segments plugins.raiplay: use 'res.encoding = "UTF-8"' plugins.rtve: update for /play/ URLs plugins.zattoo: fix HLS stream, added more debug details tests.mixins.stream_hls: increase TIMEOUT_AWAIT_WRITE timeout, use --durations 10 for pytest setup: update requests version >=2.26.0 and makeinstaller.sh plugins.abematv: skip invalid ad segments plugins.animelab: removed cli.argparser: Fixed ValueError for streamlink --help bastimeyer (39): session: deprecate options for spec. stream types stream.hls: remove hls-segment-stream-data option docs: reorganize stream transport options stream.hls: except more errors raised by requests tests.hls: fix playlist reload time tests stream.hls: close stream on initial parsing error installer: upgrade to python 3.9 tests: fix Plugin.bind(session) calls plugin: fix cookie related error messages docs: update python-requests version comment plugins.twitch: replace remaining kraken API calls plugins.twitch: refactor TwitchAPI class methods plugins.euronews: add API fallback requests plugins.sportschau: fix audio streams vendor: add lxml dependency plugins.deutschewelle: rewrite plugin plugins.gardenersworld: remove plugin cli: player title and file output metadata vars plugin.api.validate: switch to lxml.etree plugin.api.validate: add args+kwargs to transform plugin.api.validate: add parse_{json,html,xml,qsd} plugin: metadata attributes plugins: fix utils imports plugins.welt: rewrite and simplify using XPath plugins.deutschewelle: validate.parse_html plugins.reuters: rewrite and fix using XPath plugins.euronews: rewrite and fix using XPath installer: move assets config to local JSON file installer: switch to streamlink/FFmpeg-Builds cli.main: f-strings cli.main: annotate types of global vars cli.main: check args.json instead of console.json cli.console: refactor ConsoleOutput cli: include plugin metadata in --json output cli.output: fix unknown vars in --player-args / -a stream.ffmpegmux: always clean up named pipes cli.utils.formatter: rewrite Formatter cli.utils.formatter: implement format_spec cli: add {time:format} var to --output / --title gustaf (1): plugins.svtplay: fix plugin video id steven7851 (1): plugins.app17: fix API_URL and URL match (#3989) ``` ## streamlink 2.3.0 (2021-07-26) Release highlights: - Implemented: new plugin URL matching API ([#3814](https://github.com/streamlink/streamlink/issues/3814), [#3821](https://github.com/streamlink/streamlink/pull/3821)) Third-party plugins which use the old API will still be resolved, but those plugins will have to upgrade in the future. See the documentation's [deprecations page](https://streamlink.github.io/latest/deprecations.html#streamlink-2-3-0) for more information. - Implemented: HLS media initialization section (fragmented MPEG-4 streams) ([#3828](https://github.com/streamlink/streamlink/pull/3828)) - Upgraded: `requests` to `>=2.26.0,<3` and set it to `==2.25.1` on Windows ([#3864](https://github.com/streamlink/streamlink/pull/3864), [#3880](https://github.com/streamlink/streamlink/pull/3880)) - Fixed: YouTube channel URLs, premiering live streams, added API fallback ([#3847](https://github.com/streamlink/streamlink/pull/3847), [#3873](https://github.com/streamlink/streamlink/pull/3873), [#3809](https://github.com/streamlink/streamlink/pull/3809)) - Removed plugins: canalplus ([#3841](https://github.com/streamlink/streamlink/pull/3841)), dommune ([#3818](https://github.com/streamlink/streamlink/pull/3818)), liveedu ([#3845](https://github.com/streamlink/streamlink/pull/3845)), periscope ([#3813](https://github.com/streamlink/streamlink/pull/3813)), powerapp ([#3816](https://github.com/streamlink/streamlink/pull/3816)), rtlxl ([#3842](https://github.com/streamlink/streamlink/pull/3842)), streamingvideoprovider ([#3843](https://github.com/streamlink/streamlink/pull/3843)), teleclubzoom ([#3817](https://github.com/streamlink/streamlink/pull/3817)), tigerdile ([#3819](https://github.com/streamlink/streamlink/pull/3819)) ```text Hakkin Lain (1): stream.hls: set fallback playlist reload time to 6 seconds (#3887) back-to (16): plugins.youtube: added API fallback plugins.rtvs: fixed livestream plugins.nos: Fixed Livestream and VOD plugins.vlive: fixed livestream (#3820) plugins.Tigerdile: removed plugins.Dommune: removed plugins.PowerApp: removed plugins.TeleclubZoom: removed (#3817) plugins.cdnbg: Fix regex and referer issues plugins.rtlxl: removed plugins.CanalPlus: removed plugins.liveedu: removed plugins.Streamingvideoprovider: removed plugin.api: update useragents plugins.youtube: detect Livestreams with 'isLive' plugins.nimotv: use 'mStreamPkg' bastimeyer (30): plugins.youtube: translate embed_live URLs plugins.periscope: remove plugin plugins.mediaklikk: rewrite plugin stream.hls: add type hints and refactor stream.hls: implement media initialization section plugin: new matchers API plugins: update protocol plugins plugins: update basic plugins plugins: update plugins with URL capture groups plugins: update plugins with spec. can_handle_url plugins: update plugins with multiple URL matchers plugins: update plugins with URL translations session: resolve deprecated plugins plugins.zdf_mediathek: refactor plugin, drop HDS docs: add deprecations page plugins.tv8: remove API, find HLS via simple regex plugins.youtube: find videoId on channel pages chore: replace issue templates with forms chore: fix issue forms checklist tests: remove mock from dev dependencies vendor: set requests to >=2.26.0,<3 tests: temporarily skip broken tests on win32 tests: fix unnecessary hostname lookup in cli_main docs: fix headline anchors on deprecations page vendor: downgrade requests to 2.25.1 on Windows tests: refactor TestMixinStreamHLS streams.segmented: refactor worker and writer streams.segmented: refactor reader streams.hls: refactor worker streams.hls: fix playlist_reload_time gustaf (1): plugins.tv4play: fix plugin URL regex vinyl-umbrella <61788251+vinyl-umbrella@users.noreply.github.com> (1): plugins.openrectv: update HLS URLs (#3850) ``` ## streamlink 2.2.0 (2021-06-19) Release highlights: - Changed: default config file path on macOS and Windows ([#3766](https://github.com/streamlink/streamlink/pull/3766)) - macOS: `${HOME}/Library/Application Support/streamlink/config` - Windows: `%APPDATA%\streamlink\config` - Changed: default custom plugins directory path on macOS and Linux/BSD ([#3766](https://github.com/streamlink/streamlink/pull/3766)) - macOS: `${HOME}/Library/Application Support/streamlink/plugins` - Linux/BSD: `${XDG_DATA_HOME:-${HOME}/.local/share}/streamlink/plugins` - Deprecated: old config file paths and old custom plugins directory paths ([#3784](https://github.com/streamlink/streamlink/pull/3784)) - Windows: - `%APPDATA%\streamlink\streamlinkrc` - macOS: - `${XDG_CONFIG_HOME:-${HOME}/.config}/streamlink/config` - `${XDG_CONFIG_HOME:-${HOME}/.config}/streamlink/plugins` - `${HOME}/.streamlinkrc` - Linux/BSD: - `${XDG_CONFIG_HOME:-${HOME}/.config}/streamlink/plugins` - `${HOME}/.streamlinkrc` Support for these old paths will be dropped in the future. See the [CLI documentation](https://streamlink.github.io/cli.html) for all the details regarding these changes. - Implemented: `--logfile` CLI argument ([#3753](https://github.com/streamlink/streamlink/pull/3753)) - Fixed: Youtube 404 errors by dropping private API calls (plugin rewrite) ([#3797](https://github.com/streamlink/streamlink/pull/3797)) - Fixed: Twitch clips ([#3762](https://github.com/streamlink/streamlink/pull/3762), [#3775](https://github.com/streamlink/streamlink/pull/3775)) and hosted channel redirection ([#3776](https://github.com/streamlink/streamlink/pull/3776)) - Fixed: Olympicchannel plugin ([#3760](https://github.com/streamlink/streamlink/pull/3760)) - Fixed: various Zattoo plugin issues ([#3773](https://github.com/streamlink/streamlink/pull/3773), [#3780](https://github.com/streamlink/streamlink/pull/3780)) - Fixed: HTTP responses with truncated body and mismatching content-length header ([#3768](https://github.com/streamlink/streamlink/pull/3768)) - Fixed: scheme-less URLs with address:port for `--http-proxy`, etc. ([#3765](https://github.com/streamlink/streamlink/pull/3765)) - Fixed: rendered man page path on Sphinx 4 ([#3750](https://github.com/streamlink/streamlink/pull/3750)) - Added plugins: mildom.com ([#3584](https://github.com/streamlink/streamlink/pull/3584)), booyah.live ([#3585](https://github.com/streamlink/streamlink/pull/3585)), mediavitrina.ru ([#3743](https://github.com/streamlink/streamlink/pull/3743)) - Removed plugins: ine.com ([#3781](https://github.com/streamlink/streamlink/pull/3781)), playtv.fr ([#3798](https://github.com/streamlink/streamlink/pull/3798)) ```text Billy2011 (2): plugins.mediaklikk: add m4sport.hu (#3757) plugins.olympicchannel: fix / rewrite DESK-coder (1): plugins.zattoo: changes to hello_v3 and new token.js (#3773) FaceHiddenInsideTheDark (1): plugins.funimationnow: fix subtitle language (#3752) Ian Cameron <1661072+mkbloke@users.noreply.github.com> (2): plugins.bfmtv: fix/find Brightcove video data in JS (#3662) plugins.booyah: new plugin back-to (7): plugins.tf1: fixed api_url plugins.onetv: cleanup plugins.mediavitrina: new plugin plugin.api: update useragents, remove EDGE plugins.ine: removed plugins.zattoo: cleanup, fix other domains plugins.playtv: removed - SEC_ERROR_EXPIRED_CERTIFICATE (#3798) bastimeyer (27): plugins.rtpplay: fix obfuscated HLS URL parsing utils.url: add encoding options to update_qsd docs: set man_make_section_directory to false tests.hls: test headers on segment+key requests cli.argparser: fix description text utils.url: fix update_scheme with implicit schemes plugins.twitch: add access token to clips tests: refactor TestCLIMainLogging cli: implement --logfile plugins.twitch: fix clips URL regex plugin.api.http_session: refactor HTTPSession plugin.api.http_session: enforce_content_length stream.hls: replace custom PKCS#7 unpad function plugin.api.validate: add nested lookups to get() plugin.api.validate: implement union_get() plugins.twitch: query hosted channels on GQL plugins.twitch: tidy up API calls cli: refactor CONFIG_FILES and PLUGIN_DIRS cli: add XDG_DATA_HOME as first plugins dir cli: rename config file on Windows to "config" cli: use correct config and plugins dir on macOS cli: deprecate old config files and plugin dirs cli: fix order of config file deprecation log msgs plugins.youtube: clean up a bit plugins.youtube: update URL regex, translate URLs plugins.youtube: replace private API calls plugins.youtube: unescape consent form values shirokumacode <79662880+shirokumacode@users.noreply.github.com> (1): plugins.mildom: new plugin for mildom.com (#3584) ``` ## streamlink 2.1.2 (2021-05-20) Patch release: - Fixed: youtube 404 errors ([#3732](https://github.com/streamlink/streamlink/pull/3732)), consent dialog ([#3672](https://github.com/streamlink/streamlink/pull/3672)) and added short URLs ([#3677](https://github.com/streamlink/streamlink/pull/3677)) - Fixed: picarto plugin ([#3661](https://github.com/streamlink/streamlink/pull/3661)) - Fixed: euronews plugin ([#3698](https://github.com/streamlink/streamlink/pull/3698)) - Fixed: bbciplayer plugin ([#3725](https://github.com/streamlink/streamlink/pull/3725)) - Fixed: missing removed-plugins-file in `setup.py build` ([#3653](https://github.com/streamlink/streamlink/pull/3653)) - Changed: HLS streams to use rounded bandwidth names ([#3721](https://github.com/streamlink/streamlink/pull/3721)) - Removed: plugin for hitbox.tv / smashcast.tv ([#3686](https://github.com/streamlink/streamlink/pull/3686)), tvplayer.com ([#3673](https://github.com/streamlink/streamlink/pull/3673)) ```text Alexis Murzeau (1): build: include .removed file in build Ian Cameron <1661072+mkbloke@users.noreply.github.com> (3): plugins.tvplayer: plugin removal plugins.picarto: rewrite/fix (#3661) plugins.bbciplayer: fix/update state_re regex Kagamia (1): plugins.nicolive: fix proxy arguments (#3710) Yavuz Kömeçoğlu (1): plugins.youtube: add html5=1 parameter (#3732) back-to (3): plugins.youtube: fix consent dialog (#3672) plugins.mitele: use '_{bitrate}' and remove duplicates stream.hls_playlist: round BANDWIDTH and parse as int (#3721) bastimeyer (7): plugins.youtube: add short video URLs plugins.hitbox: remove plugin chore: remove square brackets from issue titles plugins.euronews: rewrite and fix live streams utils.named_pipe: rewrite named pipes docs: fix winget package link ci.github: add python 3.10-dev to test runners bururaku (1): plugins.abematv: Fixed download problem again. (#3658) ``` ## streamlink 2.1.1 (2021-03-25) Patch release: - Fixed: test failure due to missing removed plugins file in sdist tarball ([#3644](https://github.com/streamlink/streamlink/pull/3644)). ```text Sebastian Meyer (1): build: don't build sdist/bdist quietly (#3645) bastimeyer (1): build: include removed plugins file in sdist ``` ## streamlink 2.1.0 (2021-03-22) Release highlights: - Added: `--interface`, `-4` / `--ipv4` and `-6` / `--ipv6` ([#3483](https://github.com/streamlink/streamlink/pull/3483)) - Added: `--niconico-purge-credentials` ([#3434](https://github.com/streamlink/streamlink/pull/3434)) - Added: `--twitcasting-password` ([#3505](https://github.com/streamlink/streamlink/pull/3505)) - Added: Linux AppImages ([#3611](https://github.com/streamlink/streamlink/pull/3611)) - Added: pre-built man page to bdist wheels and sdist tarballs ([#3459](https://github.com/streamlink/streamlink/pull/3459), [#3510](https://github.com/streamlink/streamlink/pull/3510)) - Added: plugin for ahaber.com.tr and atv.com.tr ([#3484](https://github.com/streamlink/streamlink/pull/3484)), nimo.tv ([#3508](https://github.com/streamlink/streamlink/pull/3508)) - Fixed: `--player-http` / `--player-continuous-http` HTTP server being bound to all interfaces ([#3450](https://github.com/streamlink/streamlink/pull/3450)) - Fixed: handling of languages without alpha_2 code when using pycountry ([#3518](https://github.com/streamlink/streamlink/pull/3518)) - Fixed: memory leak when calling `streamlink.streams()` ([#3486](https://github.com/streamlink/streamlink/pull/3486)) - Fixed: race condition in HLS related tests ([#3454](https://github.com/streamlink/streamlink/pull/3454)) - Fixed: `--player-fifo` issues on Windows with VLC or MPV ([#3619](https://github.com/streamlink/streamlink/pull/3619)) - Fixed: various plugins issues (see detailed changelog down below) - Removed: Windows portable (RosadinTV) ([#3535](https://github.com/streamlink/streamlink/pull/3535)) - Removed: plugin for micous.com ([#3457](https://github.com/streamlink/streamlink/pull/3457)), ntvspor.net ([#3485](https://github.com/streamlink/streamlink/pull/3485)), btsports ([#3636](https://github.com/streamlink/streamlink/pull/3636)) - Dependencies: set `websocket-client` to `>=0.58.0` ([#3634](https://github.com/streamlink/streamlink/pull/3634)) ```text Alexis Murzeau (1): docs: update Debian stable install instructions Billy2011 (1): plugins.stadium: adaptions for new player api (#3506) Ian Cameron <1661072+mkbloke@users.noreply.github.com> (7): plugins.mico: plugin removal plugins.dogus: remove channel and update test plugins.turkuvaz: add channels and URL tests plugins.tvtoya: fix playlist regex plugins.nimotv: new plugin plugins.tvtoya: minor fixes plugins.mjunoon: rewrite/fix Jefffrey <22608443+Jefffrey@users.noreply.github.com> (1): plugins.Nicolive: login before getting wss api url Miguel Valadas (1): plugins.rtpplay: add schema and fix HLS URL (#3627) Vladimir Stavrinov <9163352+vstavrinov@users.noreply.github.com> (1): plugins.oneplusone: fix iframe url pattern (#3503) alnj (1): plugins.twitcasting: add support for private/password-protected streams (#3505) back-to (11): cli.main: use *_args, **_kwargs for create_http_server (#3450) plugins.nicolive: added --niconico-purge-credentials docs: remove outdated gst-player example plugins.facebook: Add 'Log into Facebook' error message. plugins.afreeca: use 'gs_cdn_pc_web' and 'common' stream.dash: Fix static playlist - refresh_wait - Pipe copy aborted - Read timeout plugin.api: update useragents (#3637) plugins.zattoo: use 'dash' as default stream setup.py: require websocket-client>=0.58.0 plugins.nicolive: fixed websocket-client plugins.btsports: remove plugin bastimeyer (36): tools: force LF line endings via .gitattributes docs: add minimalist code of conduct stream.hls: open reader from class attribute tests.hls: await all filtered-HLS writer calls plugins.twitch: fix access_token on invalid inputs ci: add netlify docs preview deploy config docs: add thank-you section to index page build: include man page in wheels docs: bump furo docs req to 2020.12.28.beta23 2021 http_session: remove HTTPAdapterWithReadTimeout docs: improve install-via-pip section docs: fix description of `--ffmpeg-fout` build: include man page in sdist tarballs utils/l10n: fix langs without alpha_2 in pycountry plugins.bloomberg: fix and refactor plugin utils: remove custom memoize decorator docs: remove CLI tutorial from man page session: implement --interface, --ipv4 and --ipv6 docs: remove RosadinTV Windows portable version ci.github: increase git fetch depth of tests tests: fix test code coverage ci.codecov: 100% tests target, add patch status docs: clean up package maintainers list plugins.vtvgo: ignore duplicate params ci.codecov: disable GH status check annotations chore: reorder and improve issue templates plugins: fix invalid plugin class names tests.plugins: parametrize can_handle_url tests plugins: fix and update removed plugins list docs: add appimages section to install docs ci.netlify: build docs when CHANGELOG.md changes docs: add pip to packages lists cli.output: fix named pipe player input on Windows cli: debug-log arguments set by the user cli: refactor log_current_versions and add tests bururaku (1): plugins.abematv: Update abematv.py (#3617) fenopa <62562166+fenopa@users.noreply.github.com> (1): installer: upgrade to python 3.8.7 losuler (1): docs: update URL to Fedora repo onde2rock (1): plugins.bfmtv : fix rmcstory and rmcdecouverte (#3471) vinyl-umbrella <61788251+vinyl-umbrella@users.noreply.github.com> (1): plugins.openrectv: update/fix (#3583) ``` ## streamlink 2.0.0 (2020-12-22) Release highlights: - BREAKING: dropped support for Python 2 and Python 3.5 ([#3232](https://github.com/streamlink/streamlink/pull/3232), [#3269](https://github.com/streamlink/streamlink/pull/3269)) - BREAKING: updated the Python version of the Windows installer to 3.8 ([#3330](https://github.com/streamlink/streamlink/pull/3330)) Users of Windows 7 will need their system to be fully upgraded. - BREAKING: removed all deprecated CLI arguments ([#3277](https://github.com/streamlink/streamlink/pull/3277), [#3349](https://github.com/streamlink/streamlink/pull/3349)) - `--http-cookies`, `--http-headers`, `--http-query-params` - `--no-version-check` - `--rtmpdump-proxy` - `--cmdline`, `-c` - `--errorlog`, `-e` - `--errorlog-path` - `--btv-username`, `--btv-password` - `--crunchyroll-locale` - `--pixiv-username`, `--pixiv-password` - `--twitch-oauth-authenticate`, `--twitch-oauth-token`, `--twitch-cookie` - `--ustvnow-station-code` - `--youtube-api-key` - BREAKING: replaced various subtitle muxing CLI arguments with `--mux-subtitles` ([#3324](https://github.com/streamlink/streamlink/pull/3324)) - `--funimationnow-mux-subtitles` - `--pluzz-mux-subtitles` - `--rtve-mux-subtitles` - `--svtplay-mux-subtitles` - `--vimeo-mux-subtitles` - BREAKING: sideloading faulty plugins will now raise an `Exception` ([#3366](https://github.com/streamlink/streamlink/pull/3366)) - BREAKING: changed trace logging timestamp format ([#3273](https://github.com/streamlink/streamlink/pull/3273)) - BREAKING/API: removed deprecated `Session` compat options ([#3349](https://github.com/streamlink/streamlink/pull/3349)) - BREAKING/API: removed deprecated custom `Logger` and `LogRecord` ([#3273](https://github.com/streamlink/streamlink/pull/3273)) - BREAKING/API: removed deprecated parameters from `HLSStream.parse_variant_playlist` ([#3347](https://github.com/streamlink/streamlink/pull/3347)) - BREAKING/API: removed `plugin.api.support_plugin` ([#3398](https://github.com/streamlink/streamlink/pull/3398)) - Added: new plugin for pluto.tv ([#3363](https://github.com/streamlink/streamlink/pull/3363)) - Added: support for HLS master playlist URLs to `--stream-url` / `--json` ([#3300](https://github.com/streamlink/streamlink/pull/3300)) - Added: `--ffmpeg-fout` for changing the output format of muxed streams ([#2892](https://github.com/streamlink/streamlink/pull/2892)) - Added: `--ffmpeg-copyts` and `--ffmpeg-start-at-zero` ([#3404](https://github.com/streamlink/streamlink/pull/3404), [#3413](https://github.com/streamlink/streamlink/pull/3413)) - Added: `--streann-url` for iframe referencing ([#3356](https://github.com/streamlink/streamlink/pull/3356)) - Added: `--niconico-timeshift-offset` ([#3425](https://github.com/streamlink/streamlink/pull/3425)) - Fixed: duplicate stream names in DASH inputs ([#3410](https://github.com/streamlink/streamlink/pull/3410)) - Fixed: youtube live playback ([#3268](https://github.com/streamlink/streamlink/pull/3268), [#3372](https://github.com/streamlink/streamlink/pull/3372), [#3428](https://github.com/streamlink/streamlink/pull/3428)) - Fixed: `--twitch-disable-reruns` ([#3375](https://github.com/streamlink/streamlink/pull/3375)) - Fixed: various plugins issues (see detailed changelog down below) - Changed: `{filename}` variable in `--player-args` / `-a` to `{playerinput}` and made both optional ([#3313](https://github.com/streamlink/streamlink/pull/3313)) - Changed: and fixed `streamlinkrc` config file in the Windows installer ([#3350](https://github.com/streamlink/streamlink/pull/3350)) - Changed: MPV's automated `--title` argument to `--force-media-title` ([#3405](https://github.com/streamlink/streamlink/pull/3405)) - Changed: HTML documentation theme to [furo](https://github.com/pradyunsg/furo) ([#3335](https://github.com/streamlink/streamlink/pull/3335)) - Removed: plugins for `skai`, `kingkong`, `ellobo`, `trt`/`trtspor`, `tamago`, `streamme`, `metube`, `cubetv`, `willax` ```text Billy2011 (2): plugins.youtube: fix live playback (#3268) stream.ffmpegmux: add --ffmpeg-copyts option (#3404) Forrest Alvarez (1): Update author email to shared email Hunter Peavey (1): docs: update wtwitch in thirdparty list (#3286) Ian Cameron <1661072+mkbloke@users.noreply.github.com> (9): plugins.skai: plugin removal plugins.kingkong: plugin removal plugins.cnews: fix video ID search, add schema plugins.ellobo: plugin removal plugins.nbcnews: fix video ID search, add schemas plugins.bfmtv: fix ID & embed re, use Dailymotion plugins.filmon: mitigate for non-JSON data response plugins.schoolism: fix and test for colon in title (#3421) plugins.dogan: fix/update Jon Bergli Heier (1): plugins.nrk: fix/rewrite plugin (#3318) Mark Ignacio (1): plugins.NicoLive: add --niconico-timeshift-offset option (#3425) Martin Buck (1): plugins.zdf_mediathek: also support 3sat mediathek Sean Greenslade (1): plugins.picarto: explicitly detect and fail on private streams (#3278) Sebastian Meyer (2): chore: drop support for Python 3.5 (#3269) ci.github: run lint step before test step (#3294) Seonjae Hyeon (1): plugins.vlive: fix URL regex and plugin (#3315) azizLIGHT (1): docs: fix mpv property-list link in --title description (#3342) back-to (26): plugins.facebook: remove User-Agent (#3272) plugins.trt/trtspor: remove plugins plugin.api.useragents: update User-Agent plugins: remove FIREFOX User-Agent imports plugins.abweb: fixed login issues plugins.huya: use FLV stream with multiple mirrors plugin.api.useragents: update User-Agent's plugins.tamago: removed dead plugin plugins.streamme: removed dead plugin plugins.metube: removed dead plugin plugins.cubetv: removed dead plugin cli.utils: remove named_pipe.py file, use streamlink.utils import plugins.willax: removed plugin, they use streann plugins.streann: allow different source URLs plugins.pixiv: set headers for stream data, fixed login issue plugins.pluto: new plugin for https://pluto.tv/ (#3363) plugins.twitch: fix ads plugins.twitch: fix --twitch-disable-reruns plugins.youtube: quickfix for "/live" URL plugins.pluto: ignore invalid channels stream.dash: allow '_alt' streams with the same resolution (#3410) plugins.afreeca: update '_get_channel_info' with 'bno', plugin cleanup (#3408) plugins.plugin: use the same cls.logger 'plugins' stream.ffmpegmux: disable -start_at_zero for -copyts as default (#3413) plugin.api.useragents: update User-Agent plugins.youtube: Fix 'ytInitialData' for channel pages bastimeyer (71): chore: drop support for Python 2 chore: remove is_py{2,3} compat checks chore: remove compat imports of builtins chore: remove streamlink.utils.encoding chore: remove simple aliased compat imports chore: remove compat imports of removed py2 deps chore: remove compat import of html module chore: remove compat imports of urllib and queue chore: remove remaining inspect compat import chore: remove unneeded __future__ imports chore: remove file encoding header comments chore: remove compat imports from tests logger: replace self.logger calls in plugins logger: format all log messages directly logger: remove deprecated compat logger logger: refactor StringFormatter chore: remove old LIVESTREAMER_VERSION constant chore: remove deprecated CLI arguments flake8: add import-order linting config plugins.twitch: player_type access token parameter ci.github: install latest version of pynsist chore: implicit py3 super() calls chore: remove u-strings ci.github: set ubuntu to 20.04 and python to 3.9 cli: optional player-args input variable cli: add support for stream manifest URL output installer: upgrade to Python 3.9.0 installer: switch back to latest pynsist release installer: downgrade to python 3.8 docs: add note about supported Windows versions docs: add autosectionlabel Sphinx extension docs: fix most http links plugin: implement global plugin arguments plugins: turn mux-subtitles into a global argument plugins.twitch: remove player_type parameter plugins.twitch: move access_token request to GQL chore: remove HLS variant playlist compat params chore: remove old rtmpdump/subprocess CLI args installer: fix + rewrite streamlinkrc config file stream.ffmpegmux: only close FFMPEGMuxer once chore: add dev version checkbox to issue templates chore: inherit from object implicitly chore: set literals and dict comprehensions chore: use yield from where possible chore: replace old errors classes with OSError chore: drop python six compat stuff chore: fix deprecated logging.Logger.warn calls docs: fix CLI page docs: split CLI args in HTML output into rows session: replace usage of deprecated imp module docs: add warning to plugin sideloading section refactor: test_session, move testplugin files plugin.api: remove support_plugin tests: fix test_cmdline{,_title} chore: add issue template config with more links docs: switch theme to furo, bump sphinx to >=3.0 docs: remove custom sphinx_rtd_theme_violet tools: update editorconfig for docs theme files docs: add index page to toctree docs: add custom stylesheet and customize sidebar docs: change/fix fonts, brand colors and spacings docs: add version warning message docs: fix applications and donate pages cli: move plugin args into their own args group docs: fix scrollbar issues in both sidebars docs: add favicons and PWA manifest cli.output: replace MPV player title parameter stream.hls: merge hls_filtered with hls cli: move --stream-url to different args group cache: catch OverflowError in set() docs: fix link in readme beardypig (6): tests: fix log tests when run on a system with a non-UTC timezone chore: use new py3 yield from syntax chore: sort imports, fix a dependency cycle and use absolute imports tests: validate all plugins' global arguments plugins.mitele: update plugin to support new website APIs (#3338) stream.ffmpegmux: Add support for specifying output file format and audio sync option (#2892) enilfodne (1): plugins.cdnbg: simplify and fix iframes without schema smallbomb (1): plugins: fix radiko.py url (#3394) ``` ## streamlink 1.7.0 (2020-10-18) Release highlights: - Added: new plugins for micous.com, tv999.bg and cbsnews.com - Added: new embedded ad detection for Twitch streams ([#3213](https://github.com/streamlink/streamlink/pull/3213)) - Fixed: a few broken plugins and minor plugin issues (see changelog down below) - Fixed: arguments in config files were read too late before taking effect ([#3255](https://github.com/streamlink/streamlink/pull/3255)) - Fixed: Arte plugin returning too many streams and overriding primary ones ([#3228](https://github.com/streamlink/streamlink/pull/3228)) - Fixed: Twitch plugin error when stream metadata API response is empty ([#3223](https://github.com/streamlink/streamlink/pull/3223)) - Fixed: Zattoo login issues ([#3202](https://github.com/streamlink/streamlink/pull/3202)) - Changed: plugin request and submission guidelines ([#3244](https://github.com/streamlink/streamlink/pull/3244)) - Changed: refactored and cleaned up Twitch plugin ([#3227](https://github.com/streamlink/streamlink/pull/3227)) - Removed: `platform=_` stream token request parameter from Twitch plugin (again) ([#3220](https://github.com/streamlink/streamlink/pull/3220)) - Removed: plugins for itvplayer, aljazeeraen, srgssr and dingittv ```text Alexis Murzeau (1): docs: use recommonmark as an extension Billy2011 (3): plugins.zattoo: use hello api v2 for zattoo.com (#3202) plugins.dlive: rewrite plugin (#3239) utils.l10n: use DEFAULT_LANGUAGE_CODE if locale lookup fails (#3055) Forrest (1): plugins.itvplayer: remove due to DRM (#2934) Ian Cameron <1661072+mkbloke@users.noreply.github.com> (8): plugins.mico: new plugin for http://www.micous.com/ (#3188) plugins.cdnbg: update url_re, plugin test, plugin matrix (#3205) plugins.tv999: new plugin for http://tv999.bg/live.html (#3199) plugins.aljazeeraen: plugin removal (#3207) plugins.srgssr: plugin removal plugins.tv3cat: update URL match, test and plugin matrix chore: update issue templates (#3250) docs: add plugin addition/removal infos (#3249) Sebastian Meyer (2): Improve coverage reports on codecov (#3200) plugins.twitch: remove platform access token param (#3220) back-to (4): plugin.api.useragents: update User-Agent plugins.livestream: remove AkamaiHDStream, use only secure HLSStream (#3243) plugins.dingittv: removed, website is unmaintained plugins: mark some plugins as broken (#3262) bastimeyer (21): ci.coverage: increase threshold of tests status tests: add stream_hls mixin for testing HLSStreams stream.hls_filtered: refactor tests, use mixin plugins.twitch: refactor tests, use mixin stream.hls: refactor reload time tests, use mixin stream.hls: separate variant playlist tests stream.hls: separate default and encrypted tests stream.hls_playlist: implement EXT-X-DATERANGE tag plugins.twitch: filter ads by EXT-X-DATERANGE tag plugins.twitch: fix metadata API response handling ci: add python 3.9 test runners tests: fix early writer close in stream_hls mixin stream.segmented: gracefully shut down thread pool plugins.twitch: remove video-type distinction plugins.twitch: refactor Twitch API related code plugins.twitch: refactor _get_hls_streams plugins.twitch: remove stream weights and clean up docs: fix working tree check in deploy script docs: update plugin guidelines docs: add developing menu with basic setup steps docs: add generic pull request template beardypig (3): plugins.cbsnews: support for live streams from CBS News (#3251) plugins.artetv: only pick the first variant of the stream (#3228) cli: make config based args available during early setup (#3255) ``` ## streamlink 1.6.0 (2020-09-22) Release highlights: - Fixed: lots of broken plugins and minor plugin issues (see changelog down below) - Fixed: embedded ads on Twitch with an ads workaround, removing pre-roll and mid-stream ads ([#3173](https://github.com/streamlink/streamlink/pull/3173)) - Fixed: read timeout error when filtering out HLS segments ([#3187](https://github.com/streamlink/streamlink/pull/3187)) - Fixed: twitch plugin logging incorrect low-latency status when pre-roll ads exist ([#3169](https://github.com/streamlink/streamlink/pull/3169)) - Fixed: crunchyroll auth logic ([#3150](https://github.com/streamlink/streamlink/pull/3150)) - Added: the `--hls-playlist-reload-time` parameter for customizing HLS playlist reload times ([#2925](https://github.com/streamlink/streamlink/pull/2925)) - Added: `python -m streamlink` invocation style support ([#3174](https://github.com/streamlink/streamlink/pull/3174)) - Added: plugin for mrt.com.mk ([#3097](https://github.com/streamlink/streamlink/pull/3097)) - Changed: yupptv plugin and replaced email+pass with id+token authentication ([#3116](https://github.com/streamlink/streamlink/pull/3116)) - Removed: plugins for vaughnlive, pandatv, douyutv, cybergame, europaplus and startv ```text Ian Cameron <1661072+mkbloke@users.noreply.github.com> (11): docs: update turkuvaz plugin matrix entry (#3114) docs: Add reuters.com for reuters plugin entry in plugin matrix (#3124) Fix formatting for reuters plugin entry plugins.huomao: fix/rewrite (#3126) plugins.drdk: fix livestreams (#3115) plugins.tvplayer: update regex and tests for /uk/ URLs plugins.tv360: fix HLS URL regex and plugin (#3185) plugins: fix unescaped literal dots in url_re entries (#3192) plugins.svtplay: rewrite/fix (#3155) plugins.yupptv: fix/minor rewrite (#3116) plugins.ine: fix unescaped literal dots in js_re (#3196) Il Harper (2): Add OBS-Streamlink into thirdparty.rst Apply suggestions from code review PleasantMachine9 <65126927+PleasantMachine9@users.noreply.github.com> (1): support `python -m` cli invocation Sebastian Meyer (4): plugins.bloomberg: fix regex module anchor (#3131) plugins.sportschau: rewrite and fix plugin (#3142) plugins.raiplay: rewrite and fix plugin (#3147) plugins.twitch: refactor worker, parser and tests (#3169) Tr4sK (1): plugins.mrtmk: new plugin for http://play.mrt.com.mk/ (#3097) Yahya <5457202+anakaiti@users.noreply.github.com> (1): docs: update reference to minimum VLC version back-to (9): plugins.vaughnlive: removed plugins.pandatv: removed plugins.douyutv: removed plugins.tv8: fix plugin with new api plugins.cybergame: removed plugins.europaplus: remove plugin plugins.vk: remove '\' from data plugins.nicolive: fix quality plugins.wasd: fixed plugin (#3139) bastimeyer (8): stream.hls: customizable playlist reload times plugins.twitch: platform=_ in access_token request docs: fix NixOS link docs: replace easy_install macOS entry with pip docs: add comment regarding pip/pip3 differences stream.hls_filtered: implement FilteredHLSStream plugins.twitch: use FilteredHLS{Writer,Reader} stream.hls_filtered: fix tests beardypig (1): plugins.crunchyroll: update auth logic derFogel (1): plugins.zattoo: fix quantum tv streaming (#3108) hymer-up <34783904+hymer-up@users.noreply.github.com> (2): plugins.startv: remove plugin (#3163) plugins.dogus: add startv URL (#3161) ``` ## streamlink 1.5.0 (2020-07-07) A minor release with fixes for `pycountry==20.7.3` ([#3057](https://github.com/streamlink/streamlink/pull/3057)) and a few plugin additions and removals. And of course the usual plugin fixes and upgrades, which you can see in the git shortlog down below. Thank you to everyone involved! Support for Python2 has not been dropped yet (contrary to the comment in the last changelog), but will be in the near future. ```text Alexis Murzeau (1): docs: update debian install instructions Billy2011 (8): plugins.nbcsports: fix embed_url_re (#2980) plugins.olympicchannel: fix/rewrite (#2981) plugins.foxtr: fix playervars_re (#3013) plugins.huya: fix _hls_re (#3007) plugins.ceskatelevize: add new api for some links (#2991) plugins.beattv: remove plugin (#3053) plugins.ard_live: fix / rewrite (#3052) plugins.ard_mediathek: fix / update (#3049) Code <60588434+superusercode@users.noreply.github.com> (1): Streamlink was added to Windows Package Manager Ian Cameron <1661072+mkbloke@users.noreply.github.com> (6): plugins.tvplayer: Add missing platform key in the GET for stream_url (#2989) plugins.btv: remove login and fix API URL (#3019) plugins.n13tv: new plugin - replaces plugins.reshet (#3034) plugins.reshet: plugin removal (#3000) plugins.tvnbg: plugin removal (#3056) plugins.adultswim: fix/rewrite (#2952) Sebastian Meyer (3): ci: no test/documentation jobs on scheduled run (#3012) cli.main: fix msecs format in logging output (#3025) utils.l10n: fix pycountry language lookup (#3057) Vladimir Stavrinov <9163352+vstavrinov@users.noreply.github.com> (1): plugins.nbcnews: new plugin for http://nbcnews.com/now (#2927) back-to (11): plugins.showroom: use normal HLSStreams docs: remove unimportant note / file plugins.viasat: remove play.nova.bg domain actions: fixed incorrect versions and use names for codecov (#2932) plugins.filmon: use /tv/ url and raise PluginError for invalid channels flake8: E741 ambiguous variable name plugins.youtube: Fix isLive and signatureCipher (#3026) plugins.facebook: use meta og:video:url and added basic title support (#3024) plugins.picarto: fixed vod url detection ci: fix pycountry issue temporarily with a fixed version plugin.api.useragents: update User-Agent bastimeyer (3): docs/install: fix Windows package manager plugins.mixer: remove plugin ci: run scheduled tests, ignore coverage report beardypig (1): plugins.cdnbg: update plugin to support new sites, and remove old sites (#2912) lanroth (1): plugins.radionet: fix plugin so it works with new page format (#3018) resloved (1): fixed typo steven7851 (1): plugins.app17: update API (#2969) tnira (1): Plugin.nicolive:resolve API format change (#3061) unavailable <51099894+EnterGin@users.noreply.github.com> (1): plugins.twitch: fix call_subdomain (#2958) wiresp33d <66558220+wiresp33d@users.noreply.github.com> (2): plugins.bigo: use API for video URL (#3016) plugins.nicolive: resolve new api format (#3039) ``` ## streamlink 1.4.1 (2020-04-24) No code changes. [See the full `1.4.0` changelog here.](https://github.com/streamlink/streamlink/releases/tag/1.4.0) ```text beardypig (1): build: include correct signing key: 0xE3DB9E282E390FA0 ``` ## streamlink 1.4.0 (2020-04-22) This will be the last release with support for Python 2, as it has finally reached its EOL at the beginning of this year. Streamlink 1.4.0 comes with lots of plugin fixes/improvements, as well as some new features and plugins, and also a few plugin removals. Notable changes: - New: low latency streaming on Twitch via `--twitch-low-latency` ([#2513](https://github.com/streamlink/streamlink/pull/2513)) - New: output HLS segment data immediately via `--hls-segment-stream-data` ([#2513](https://github.com/streamlink/streamlink/pull/2513)) - New: always show download progress via `--force-progress` ([#2438](https://github.com/streamlink/streamlink/pull/2438)) - New: URL template support for `--hls-segment-key-uri` ([#2821](https://github.com/streamlink/streamlink/pull/2821)) - Removed: Twitch auth logic, `--twitch-oauth-token`, `--twitch-oauth-authenticate`, `--twitch-cookie` ([#2846](https://github.com/streamlink/streamlink/pull/2846)) - Fixed: Youtube plugin ([#2858](https://github.com/streamlink/streamlink/pull/2858)) - Fixed: Crunchyroll plugin ([#2788](https://github.com/streamlink/streamlink/pull/2788)) - Fixed: Pixiv plugin ([#2840](https://github.com/streamlink/streamlink/pull/2840)) - Fixed: TVplayer plugin ([#2802](https://github.com/streamlink/streamlink/pull/2802)) - Fixed: Zattoo plugin ([#2887](https://github.com/streamlink/streamlink/pull/2887)) - Changed: set Firefox User-Agent HTTP header by default ([#2795](https://github.com/streamlink/streamlink/pull/2795)) - Changed: upgraded bundled FFmpeg to `4.2.2` in Windows installer ([#2916](https://github.com/streamlink/streamlink/pull/2916)) ```text Adam Baxter (1): stream.hls_playlist: Add extra logging for invalid #EXTM3U line (#2479) Alexis Murzeau (1): docs: fix duplicate object description of streamlink in api docs Colas Broux (2): plugins.youtube: Fix for new Youtube VOD API (#2858) Updating README Applying changes from 1402fb0 to the README Closes #2880 Finn (1): plugins.invintus: Add support for Invintus Media live streams and VOD (#2845) Ian Cameron <1661072+mkbloke@users.noreply.github.com> (3): Fix tvplayer plugin and tests (#2802) plugins.piczel: Added HLS, Removed RTMP (#2815) plugins.reuters: fix (#2811) Mohamed El Morabity (1): plugins.tf1: use new API to retrieve DASH streams (#2759) Riolu <16816842+iucario@users.noreply.github.com> (1): plugins.radiko: Add support for radiko.jp (#2826) Uinden <25625733+Uinden@users.noreply.github.com> (1): plugins.wasd: new plugin for WASD.TV (#2641) YYY (1): plugins.nicolive: new plugin for Niconico Live (#2651) Yavuz Kömeçoğlu (1): plugins.galatasaraytv: Add support for GALATASARAY SK TV (#2760) Zhenyu Hu (1): plugins.kugou: Add Kugou Fanxing live plugin (#2794) back-to (17): plugin.api: use Firefox as default User-Agent instead of python-requests plugins.filmon: retry for 50X errors cli: New command --force-progress (#2438) travis-ci: don't run doctr on pull requests plugins.bilibili: ignore unavailable URLs (#2818) plugins.mlgtv: remove plugin they use DRM for Livestreams (#2829) plugins.twitch: Fixed clips (#2843) plugins.showroom: Fix HLS missing segments plugins.kanal7: Removed Plugin they use static URLs plugins.rotana: new plugin for rotana.net (#2838) plugins.pixiv: removed not working login process via username (#2840) plugins.abema: support for Abema overseas version plugins.younow: remove plugin plugin.api.useragents: update User-Agent plugins.zattoo: fix app token and new recording URL plugins.zeenews: new plugin for https://zeenews.india.com/live-tv AUTHORS: removed unused script and removed outdated list (#2889) bastimeyer (58): plugins.twitch: fix rerun validation schema flake8: E303 flake8: E111 flake8: E117 flake8: E121 flake8: E122 flake8: E126, E127, E128 flake8: E203, E226, E231, E241, E261 flake8: E265 flake8: E302, E305 flake8: E402 flake8: E712 flake8: W291, W292, W293, W391 flake8: F401, F403 flake8: F405 flake8: F811 flake8: F841 flake8: W504 flake8: E741 flake8: E501 flake8: F601 flake8: E722 flake8: F821 flake8: F812 flake8: add flake8 to TravisCI cleanup: remove unnecessary unittest.main() calls cleanup: remove unnecessary python shebangs PEP263: use consistent utf-8 coding header comment tools: add .editorconfig stream.hls: add hls-segment-stream-data parameter plugins.twitch: low latency plugins.twitch: disable LL when filtering ads plugins.twitch: print info msg if stream is not LL plugins.bloomberg: fix vods and live streams plugins.twitch: remove cookie auth plugins.twitch: remove oauth token login docs: fix multiple options on the same line ci.github: implement main workflow ci.github: add release config and rewrite scripts ci.github: add scheduled nightly builds ci.github: deploy documentation ci: show extra test summary info ci: remove old CI configs ci.github: fix codecov uploads cleanup: change build badge and link in README.md cleanup: remove TravisCI from deploy scripts ci: remove macOS test runners codecov: wait before notifying docs: rewrite windows nightly builds section docs: rewrite pip/source install section docs: fix and rewrite index page docs: reformat donation page ci.github: fix continue on error installer: rewrite / clean up makeinstaller script installer: download ffmpeg+rtmpdump assets installer: delete locally included binary files plugins.twitch: rewrite disable ads logic Release 1.4.0 beardypig (10): update release signing key update docs deployment key plugins.tv360: updated URL and HLS stream extraction method util: fix some encoding issue when setting unicode/utf8 titles in py2 cli.output: make sure the player arguments are properly encoded utils: update_qsd to leave blank values unchanged (#2869) plugins.eurocom: remove eurocom plugin plugins.tv1channel: remove tv1channel plugin actions: no need to use a secret for the PyPI username add python 2.7 deprecation warning danieljpiazza (1): Update Crunchyroll access token. Fixes streamlink/streamlink issue #2785. malvinas2 (3): plugins.latina: new plugin for https://www.latina.pe/tvenvivo (#2793) plugins.albavision: Added support for ATV and ATVMas (#2801) plugins.rtve: Added support for clan tve, children's channel of RTVE (#2875) steven7851 (1): plugins.app17: fix for new layout (#2833) tarkah (1): stream.hls: add templating for hls-segment-key-uri option (#2821) ``` ## streamlink 1.3.1 (2020-01-27) A small patch release that addresses the removal of [MPV's legacy option syntax](https://mpv.io/manual/master/#legacy-option-syntax), also with fixes of several plugins, the addition of the `--twitch-disable-reruns` parameter and dropped support for Python 3.4. ```text Hunter Peavey (4): Add wtwitch to list of thirdparty programs Try adding an image Move image position Make requested changes Vladimir Stavrinov <9163352+vstavrinov@users.noreply.github.com> (1): plugins.nhkworld: the site migrates from xml to json stream data back-to (6): docs/tests: remove python 3.4, use 3.8 and nightly for travis-ci plugins.bilibili: fix Livestreams with status 1 (set Referer) plugins.youtube: Remove itag 303 plugins.ustream: Added support for video.ibm.com plugins.bbciplayer: Fixed login params plugins.bbciplayer: remove test_extract_nonce bastimeyer (5): plugins.twitch: use python logging module plugins.twitch: fix rerun detection cli.output: fix mpv player parameter format 2020 docs: fix MPV parameters on common issues page skulblakka (1): Allow to disable twitch reruns (#2722) ``` ## streamlink 1.3.0 (2019-11-22) A new release with plugin updates and fixes, including Twitch.tv (see [#2680](https://github.com/streamlink/streamlink/issues/2680)), which had to be delayed due to back and forth API changes. The Twitch.tv workarounds mentioned in [#2680](https://github.com/streamlink/streamlink/issues/2680) don't have to be applied anymore, but authenticating via `--twitch-oauth-token` has been disabled, regardless of the origin of the OAuth token (via `--twitch-oauth-authenticate` or the Twitch website). In order to not introduce breaking changes, both parameters have been kept in this release and the user name will still be logged when using an OAuth token, but receiving item drops or accessing restricted streams is not possible anymore. Plugins for the following sites have also been added: - albavision - news.now.com - twitcasting.tv - viu.tv - vlive.tv - willax.tv ```text Alexis Murzeau (1): plugins.pixiv: fix doc typo thats -> that's Mohamed El Morabity (1): plugins.idf1: HTTPS support Mohamed El Morabity (1): plugins.playtv: Fix for new stream data API (#2388) Ozan Karaali (1): plugins.foxtr: Extended support Ozan Karaali (1): plugins.cinergroup: #2390 fix (#2629) Troogle (1): plugins.bilibili: fix resolution issue Werner Robitza (1): remove direct installation instructions, link to docs back-to (6): setup.cfg: added flake8 settings plugins.vk: use html_unescape for HLS streams plugins.willax: new plugin for http://willax.tv/en-vivo/ plugins.zattoo: _url_re update for some new urls plugin.api.useragents: update CHROME and FIREFOX User-Agent stream.hls: Fix UnicodeDecodeError for log msg and name_prefix for stream_name bastimeyer (3): ci/travis: install pynsist 2.4 plugins.twitch: fix API issue - 410 gone error docs.cli: fix and reword the tutorial section beardypig (10): plugins.bbciplayer: update API URL to use https plugins.nownews: added support for the HK news site news.now.com plugins.tv8: update regex for the stream url plugins.bbciplayer: fix issue with nonce extraction plugins.bbciplayer: extract master brand/channel id from the state json plugins.itvplayer: Use flash streams for ITV1/ITV4 plugins.viutv: support for the viu.tv live stream plugins.albavision: support for some albavision live streams plugins.bloomberg: fix issue where the incorrect playlist could be used stream.streamprocess: check that a process is usable before using it derrod (1): plugins.vlive: Add support for V LIVE live streams printempw (1): plugins.twitcasting: new plugin for TwitCasting.tv ssaqua (1): plugins.linelive: update to support VOD/archived streams ``` ## streamlink 1.2.0 (2019-08-18) Here are the changes for this month's release - Multiple plugin fixes - Fixed single hyphen params at the beginning of --player-args (#2333) - `--http-proxy` will set the default value of `--https-proxy` to same as `--http-proxy`. (#2536) - DASH Streams will handle headers correctly (#2545) - the timestamp for FFMPEGMuxer streams will start with zero (#2559) ```text Davi Guimarães (1): plugins.cubetv: base url changes (#2430) Forrest (1): Add a sponsor button (#2478) Jiting (1): plugin.youtube: bug fix for YouTube live streams check Juan Ramirez (2): Invalid use of console.logger in CLI Too many arguments for logging format string Mohamed El Morabity (9): plugins.vimeo: new plugin for Vimeo streams plugins.vimeo: add subtitle support for vimeo plugin plugins.vimeo: fix alphabetical order in plugin matrix Use class parameter instead of class name in class method [plugins.bfmtv] Fix player regex [plugins.idf1] Update for new website layout plugins.gulli: enable HTTPS support plugins.gulli: fix live stream fetching plugins.tvrplus: fix for new website layout Mohamed El Morabity (1): plugins.clubbingtv: new plugin for Clubbing TV website (#2569) Viktor Kálmán (1): plugins.mediaklikk: update broken plugin (#2401) Vladimir Stavrinov (2): plugins.live_russia_tv: adjust to site changes (#2523) plugins.oneplusone: fix site changes (#2425) YuuichiMizuoka <32476209+YuuichiMizuoka@users.noreply.github.com> (1): add login posibility for pixiv using sessionid and devicetoken aqxa1 (1): Handle keyboard interrupts in can_handle_url checks (#2318) back-to (12): cli.argparser: Fix single hyphen params at the beginning of --player-args plugins.reuters: New Plugin plugins: Removed rte and tvcatchup utils.__init__: remove cElementTree, it's just an alias for ElementTree plugins.teamliquid: New domain, fix stream_weight plugins.vimeo: Fixed DASH Livestreams plugin.api.useragents: update CHROME and FIREFOX User-Agent ffmpegmux: use -start_at_zero with -copyts plugins.youtube: fixed reason msg, updated _url_re plugins.TV1Channel: Fixed new livestream iframe plugins.npo: removed due to DRM plugins.lrt: fixed livestreams bastimeyer (1): plugins.welt: fix plugin beardypig (13): plugins.bbciplayer: small change to where the VPID is extracted from (#2376) plugins.goodgame: fix for debug logging error plugins.cdnbg: fix for bstv url plugins.ustvnow: updated to handle new auth, and site design plugin.schoolism: bug fix for videos with subtitles (#2524) stream.dash: use the stream args in the writer and worker session: default https-proxy to the same as http-proxy, can be overridden plugins.beattv: partial fix for the be-at.tv streams tests: test the behaviour of setting http-proxy and https-proxy plugins.twitch: support for different clips URL plugins.wwenetwork: support for new site plugins.ustreamtv: add support for proxying WebSocket connections plugins.wwenetwork: update for small page/api change skulblakka (1): plugins.DLive: New Plugin for dlive.tv (#2419) ssaqua (1): plugins.linelive: new plugin for LINE LIVE (live.line.me) (#2574) ``` ## streamlink 1.1.1 (2019-04-02) This is just a small patch release which fixes a build/deploy issue with the new special wheels for Windows on PyPI. (#2392) [Please see the full changelog of the `1.1.0` release!](https://github.com/streamlink/streamlink/releases/tag/1.1.0) ```text Forrest (1): build: remove cygwin from wheels for Windows (#2393) ``` ## streamlink 1.1.0 (2019-03-31) These are the highlights of Streamlink's first minor release after the 1.0.0 milestone: - several plugin fixes, improvements and new plugin implementations - addition of the `--twitch-disable-ads` parameter for filtering out advertisement segments from Twitch.tv streams (#2372) - DASH stream improvements (#2285) - documentation enhancements (#2292, #2293) - addition of the `{url}` player title variable (#2232) - default player title config for PotPlayer (#2224) - new `streamlinkw` executable on Windows (wheels + installer) (#2326) - Github release assets simplification (#2360) ```text Brian Callahan (1): Add OpenBSD to the installation docs Peter Rowlands (변기호) (2): streams.dash: Support manifest strings in addition to manifest urls (#2285) plugins.facebook: Support manifest strings and tahoe player urls (#2286) Roman Kornev (2): cli.main: Add {url} argument to window --title (#2232) cli.output: Add window title for PotPlayer (#2224) Sebastian Meyer (1): Build additional "streamlinkw" launcher on Windows (#2326) Steve Oswald <30654895+SteveOswald@users.noreply.github.com> (1): plugins.zattoo: Added support for www.1und1.tv (#2274) Vladimir Stavrinov (2): plugins.ntv: new Plugin for ntv.ru (#2351) plugins.live_russia_tv: fix iframe format differences (#2375) back-to (13): plugins.atresplayer: Fixed HLSStream plugins.streamme: Fixed source quality, added title and author plugins.atresplayer: update for new api schema plugins.mitele: plugin update plugins.ustreamtv: handle stream names better, allow '_alt' streams (#2267) plugins.rtve: Fixed content_id search (#2300) plugins.streamme: Fixed null error for 'origin' tests: detect unsupported versions for itertags plugins.pluzz: Fixed Video ID and logging update plugins.pluzz: Fixed regex, they use quotes now. plugins.cdnbg: New domain videochanel.bstv.bg plugins.tf1: Fixed python2.7 ascii error plugins.okru: Fixed Plugin (#2374) plugins.rtpplay: fix _m3u8_re and plugin cleanup bastimeyer (13): docs/install: git+makepkg instead of AUR helper docs/install: rewrite source code and pip section docs/install: shell code blocks, remove prompts docs/install: simplify pip user/system table docs/install: rewrite virtual env section docs/install: move Windows and macOS to the top Add force_verify=true to Twitch OAuth URL plugins.twitch: platform=_ in access_token request TravisCI: don't publish wheels on Github releases stream.hls: refactor M3U8Parser stream.hls: refactor HLSStream{,Worker} plugins.twitch: implement disable-ads parameter Release 1.1.0 beardypig (2): plugins.dogus: support for YouTube embedded streams plugins.bbciplayer: do not try to authenticate when not required lon (1): plugins.crunchyroll: Allow CR's multilingual URLs to be handled (#2304) ``` ## streamlink 1.0.0 (2019-01-30) The celebratory release of Streamlink 1.0.0! *A lot* of hard work has gone into getting Streamlink to where it is. Not only is Streamlink used across multiple applications and platforms, but companies as well. Streamlink started from the inaugural [fork of Livestreamer](https://github.com/chrippa/livestreamer/issues/1427) on September 17th, 2016. Since then, We've hit multiple milestones: - Over 886 PRs - Hit 3,000 commits in Streamlink - Obtaining our first sponsors as well as backers of the project - The creation of our own logo (https://github.com/streamlink/streamlink/issues/1123) Thanks to everyone who has contributed to Streamlink (and our backers)! Without you, we wouldn't be where we are today. **Without further ado, here are the changes in release 1.0.0:** - We have a new icon / logo for Streamlink! (https://github.com/streamlink/streamlink/pull/2165) - Updated dependencies (https://github.com/streamlink/streamlink/pull/2230) - A *ton* of plugin updates. Have a look at [this search query](https://github.com/streamlink/streamlink/pulls?utf8=%E2%9C%93&q=is%3Apr+is%3Aclosed+plugins.+) for all the recent updates. - You can now provide a custom key URI to override HLS streams (https://github.com/streamlink/streamlink/pull/2139). For example: `--hls-segment-key-uri ` - User agents for API communication have been updated (https://github.com/streamlink/streamlink/pull/2194) - Special synonyms have been added to sort "best" and "worst" streams (https://github.com/streamlink/streamlink/pull/2127). For example: `streamlink --stream-sorting-excludes '>=480p' URL best,best-unfiltered` - Process output will no longer show if tty is unavailable (https://github.com/streamlink/streamlink/pull/2090) - We've removed BountySource in favour of our OpenCollective page. If you have any features you'd like to request, please open up an issue with the request and possibly consider backing us! - Improved terminal progress display for wide characters (https://github.com/streamlink/streamlink/pull/2032) - Fixed a bug with dynamic playlists on playback (https://github.com/streamlink/streamlink/pull/2096) - Fixed makeinstaller.sh (https://github.com/streamlink/streamlink/pull/2098) - Old Livestreamer deprecations and API references were removed (https://github.com/streamlink/streamlink/pull/1987) - Dependencies have been updated for Python (https://github.com/streamlink/streamlink/pull/1975) - Newer and more common User-Agents are now used (https://github.com/streamlink/streamlink/pull/1974) - DASH stream bitrates now round-up to the nearest 10, 100, 1000, etc. (https://github.com/streamlink/streamlink/pull/1995) - Updated documentation on issue templates (https://github.com/streamlink/streamlink/pull/1996) - URL have been added for better processing of HTML tags (https://github.com/streamlink/streamlink/pull/1675) - Fixed sort and prog issue (https://github.com/streamlink/streamlink/pull/1964) - Reformatted issue templates (https://github.com/streamlink/streamlink/pull/1966) - Fixed crashing bug with player-continuous-http option (https://github.com/streamlink/streamlink/pull/2234) - Make sure all dev dependencies (https://github.com/streamlink/streamlink/pull/2235) - -r parameter has been replaced for --rtmp-rtmpdump (https://github.com/streamlink/streamlink/pull/2152) **Breaking changes:** - A large number of unmaintained or NSFW plugins have been removed. You can find the PR that implemented that change here: https://github.com/streamlink/streamlink/pull/2003 . See our [CONTRIBUTING.md](https://github.com/streamlink/streamlink/blob/130489c6f5ad15488cd4ff7a25c74bf070f163ec/CONTRIBUTING.md) documentation for plugin policy. ```text Billy2011 (3): streamlink.plugins: replace global http session by self.session.http (#1925) stream.hls_playlist: fix some regex pattern & code optimization (#1918) plugins.filmon: fix NoPluginError, if channel is not an ID (#2005) David Bell (1): Update ine.py (#2171) Forrest (2): Remove bountysource from donation page, update flattr Add a note about specifying the full player path Forrest (2): Feature/plugin request policy update (#1838) Add icon, modify installer, update docs (#2165) Hubcapp (1): Window Titles = Stream Titles + Other Attributes (#1576) Jani Ollikainen (1): Added support for viafree.fi Lukas (1): plugins.zdf_mediathek: use ptmd-template api path (#2233) Maxwell Cody (1): Add ability to specify custom key URI override for HLS streams (#2139) Mohamed El Morabity (1): plugins.pluzz: fixes and francetvinfo.fr support (#2119) Nick Gal (1): Update steam plugin to work with steam.tv urls Petar Kukolj (5): plugins.cubetv: support for live streams on cubetv.sg plugins.ok_live: Changed URL regex to support VoDs plugins.bilibili: Fix plugin after API change plugins.twitch: Add support for {title}, {author} and {category} plugins.skai: Fix plugin after site update Roman (2): [FIX] Debug log arguments string [FIX] Debug log arguments cross-platform Roman Kornev (1): [FIX] Message duplicate Sebastian Meyer (1): Reword+reformat issue templates for consistency (#1966) Stefan de Konink (1): Update the documentation with comments for playing YouTube Live Streams (#2156) Søren Fuglede Jørgensen (1): Update and reactivate plugin for dr.dk Twilight0 (1): plugins.ssh101: Fixed plugin (#1916) Vincent Rozenberg (1): Update npo.py Vinny (1): docs: Added documentation for the Funimation plugin (#2091) Visatouch Deeying (1): Fix crash on missing output.record when using player-continous-http back-to (48): plugins.EuropaPlusTV: Fix for "No connection adapters were found" utils.args: Moved streamlink_cli utils.args into streamlink package tests.plugins: Test that each plugin has a test file (#1885) plugins.zattoo: session update and allow muxed hls / dash streams (#1902) plugins.tv4play: Fix for updated website debug: Added Session Language code as a debug message tests: run Python 3.7 tests on AppVeyor and Travis-CI (#1928) plugins.rtve: Fixed AttributeError 'ZTNRClient' has no 'session' plugins.twitch: Fixed AttributeError and Flake8 plugins.filmon: Fixed AttributeError plugins.crunchyroll: Fixed AttributeError and Flake8 tests.localization: use en_CA instead of en_US for test_equivalent plugins.younow: Fix for session error and plugin cleanup plugins.media_ccc_de: removed plugin plugins.pixiv: use API for the stream URL and cache login cookies plugins.youtube: Added support for {author} and {title} (#1944) docs-CLI: Fix for sort and prog issue plugins.ceskatelevize: Fix for https issues plugins.mjunoon: use a User-Agent api.useragents: use newer and more common User-Agent's script.makeinstaller: use a more recent version of Python and Pycryptodome Removed old Livestreamer deprecations and API references plugins.sportal: Removed old RTMP streams, use HLS plugins.twitch: new video URL regex Removed old or unwanted Streamlink Plugins tests: use unittest instead of pytest for itertags error (#1999) utils.parse_xml: allow a tab for ignore_ns plugins.afreeca: ignore new preloading urls plugins.filmon: use the same cdn for a playlist url (#2074) plugins.teleclubzoom: New plugin for teleclubzoom.ch plugins.oldlivestream: remove plugin, service not available anymore (#2081) travis: increase git clone depth stream.dash_manifest: Fixed bug for dynamic playlists when parent.id is None plugins.ustreamtv: use Desktop instead of Mobile streams (#2082) plugins.youtube: Added support for embedded livestream urls versioneer: always use 7 characters plugins.stv: New Plugin for https://player.stv.tv/live plugins.sbscokr: New Plugin for http://play.sbs.co.kr/onair/pc/index.html plugins.vtvgo: New plugin for https://vtvgo.vn/ travis-ci: Fixed Python 3.8 error's plugins.cdnbg: Update for extra channels (#2186) api.useragents: update User-Agent list plugins.afreeca: use Referer for every connection (#2204) plugins.trt: Added support for https url, fixed logger error plugins.turkuvaz: Added support for channel 'apara' plugins.tvrby: Fixed broken plugin. plugins.youtube: replace "gaming" with "www" subdomain plugins.kanal7: Fixed iframe_re/stream_re, added new domain bastimeyer (6): Fix bug report template Move/rename issue templates Update generic issue template plugins.euronews: fix live stream API URL scheme Fix installer by moving additional files feature: {best,worst}-unfiltered stream synonyms beardypig (23): plugins.live_russia_tv: fix for live streams, and support for VOD plugin args: if args are suppressed, ignore them plugin.tvtoya: refactor, add tests, plugin docs, etc. stream.dash: fix bug where timeline_segments.t can be None stream.hls: only include audio/video streams in MuxedHLSStreams stream.hls: if the primary video stream has audio then include it plugins.facebook: support for videos in posts plugins.steam: api requests now require a session id test for rounding dash stream bitrate stream.dash: make the bitrate name for videos more human friendly plugins.schoolism: add support for assignment feedback videos plugins.senategov: support for hearing streams on senate.gov plugins.stadium: support for live stream on watchstadium.com plugins.funimationnow: workaround for #1899, see #2088 cli: make the HH part of HH:MM:SS options optional plugins.bbciplayer: change in the mediator info in the page layout plugins.btsports: fix for change in login flow plugins.filmon: fix for live tv URLs that start with channel plugin.powerapp: support for "tvs" URLs setup: update requests to latest version and set urllib3 to match CI: make sure all the dev dependencies are up to date plugins.tf1: update to support new DASH streams plugins.tf1: re-add support for HLS with the new API beardypig (7): Test coverage increase (#1646) Handle unicode log message in Python 2 (#1886) Update method for finding YouTube videoIds (#1888) stream.dash: prefer audio streams based on the user's locale (#1927) plugins.openrectv: update to match site changes and title support (#1968) URL builder utils (#1675) cli: disable progress output for -o when no tty is available (#2090) fozzy (1): update regex to support new pattern fozzy (1): plugins.egame: new plugin for egame.qq.com (#2070) jackyzy823 (2): Plugin Request: new plugin for Abema.tv (#1949) Improve terminal progress display for wide characters (#2032) mp107 (1): plugins.ltvlmslv: new Plugin for Latvian live TV channels on ltv.lsm.lv (#1986) qkolj (5): plugins.tamago: support for live streams on player.tamago.live (#2108) plugins.huomao: Fix plugin after website changes (#2134) plugins.metube: Add support for live streams and VoDs on www.metube.id (#2112) plugins.tvibo: Add support for livestreams on player.tvibo.com (#2130) Fix recording added in #920 (#2152) remek30 (1): plugins.toya: support for tvtoya.pl skulblakka (1): [picarto.tv] Fix regarding changed URL (#1935) yoya3312 <40212627+yoya3312@users.noreply.github.com> (1): plugins.youtube: use new "hlsManifestUrl" for Livestreams (#2238) ``` ## streamlink 0.14.2 (2018-06-28) Just a few small fixes in this release. - Fixed Twitch OAuth request flow (https://github.com/streamlink/streamlink/pull/1856) - Fix the tv3cat and vk plugins (https://github.com/streamlink/streamlink/pull/1851, https://github.com/streamlink/streamlink/pull/1874) - VOD supported added to atresplayer plugin (https://github.com/streamlink/streamlink/pull/1852, https://github.com/streamlink/streamlink/pull/1853) - Removed tv8cati and nineanime plugins (https://github.com/streamlink/streamlink/pull/1860, https://github.com/streamlink/streamlink/pull/1863) - Added mjunoon.tv plugin (https://github.com/streamlink/streamlink/pull/1857) ```text NyanKiyoshi (1): Fix 404 error on oauth authorize url back-to (1): plugins.vk: _url_re update, allow embedded content, plugin cleanup (#1874) beardypig (10): plugins.t3cat: update validation rule, refactor plugin a little bit plugins.atresplayer: update to support VOD streams stream.dash: support for SegmentList streams with ranged segments plugins.mjunoon: support for live and vod streams on mjunoon.tv release: fix release notes manual install url plugins.tv8cat: plugin removed - the live broadcast is no longer available plugins.nineanime: no longer supported release: set the date of the release for UTC time plugin: support stream weights returned by DASHStream.parse_manifest ``` ## streamlink 0.14.0 (2018-06-26) Here are the changes to this months release! - Multiple plugin fixes - Bug fixes for DASH streams (https://github.com/streamlink/streamlink/pull/1846) - Updated API call for api.utils hours_minutes_seconds (https://github.com/streamlink/streamlink/pull/1804) - Updated documentation (https://github.com/streamlink/streamlink/pull/1826) - Dict structures fix (https://github.com/streamlink/streamlink/pull/1792) - Reformated help menu (https://github.com/streamlink/streamlink/pull/1754) - Logger fix (https://github.com/streamlink/streamlink/pull/1773) ```text Alexis Murzeau (3): sdist: include tests resources (#1785) tests: freezegun: use object instead of lambda (#1787) rtlxl: use raw string to fix escape sequences (#1786) BZHDeveloper <39899575+BZHDeveloper@users.noreply.github.com> (1): plugins.cnews : separate CNEWS data from CanalPlus plugin. (#1782) MasterofJOKers (1): plugins.sportschau: Fix "No schema supplied" error Mohamed El Morabity (2): plugins.pluzz: support for DASH streams plugins.pluzz: fix HDS streams Mohamed El Morabity (1): [plugins.rtbf] Fix radio streams + DASH support (#1771) Sebastian Meyer (1): Move docs version selection to sidebar (#1802) Twilight0 (2): Convert literal comprehensive dicts to dict contructs Add arguments to __init__ - super to work on Python 2.7.X (#1796) back-to (9): plugins: marked or removed broken plugins plugins.earthcam: Fixed hls_url - No schema supplied. plugins.rte: allow https plugins.VinhLongTV: New plugin for livestreams of thvli.vn docs.thirdparty: Added LiveProxy plugins.tlctr: New Plugin for tlctv.com.tr/canli-izle plugins.bigo: Fix for new channelnames and plugin cleanup (#1797) docs: removed some notes, updated some urls utils.times: hours_minutes_seconds update, twitch automate time offset beardypig (15): help: reformat all the help text so that it is <80 chars plugins.bbciplayer: fix bug is DASH for VOD sdist and wheel release fixes (#1758) plugin.youtube: find video id in page, fall back to API (#1746) logging: rename logger for main back to 'cli' plugins.vaughnlive: support for the HTML flv player plugins.yupptv: support for yupptv with login support plugins.nos: update for new page layout for live and VOD plugins.lrt: add support for Lithuanian National Television plugins.delfi: support for delfi.lt web portal plugins.vrtbe: update to new page layout/API plugins.itvplayer: update to new HTML5 API plugins.atresplayer: support new layout/API stream.dash: use the ID and mime-type to identify a representation plugins.mitele: sometimes ogn is null, html5 pdata endpoint works better beardypig (5): plugins.crunchyroll: refactoring and updated API calls (#1820) Suppress/fix deprecated warnings for Python 3 (#1833) API for plugins to request input from the user (#1827) Steam Broadcast Plugin (#1717) USTV Now (#1840) fozzy (1): fix bug caused by indentation and add support for url pattern like 'xingxiu.panda.tv' ``` ## streamlink 0.13.0 (2018-06-06) Massive release this month! Here are the changes: - Initial MPEG DASH support has been added! (https://github.com/streamlink/streamlink/pull/1637) Many thanks to @beardypig - As always, a *ton* of plugin updates - Updates to our documentation (https://github.com/streamlink/streamlink/pull/1673) - Updates to our logging (https://github.com/streamlink/streamlink/pull/1752) as well as log --quiet options (https://github.com/streamlink/streamlink/pull/1744) (https://github.com/streamlink/streamlink/pull/1720) - Our release script has been updated (https://github.com/streamlink/streamlink/pull/1711) - Support for livestreams when using the `--hls-duration` option (https://github.com/streamlink/streamlink/pull/1710) - Allow streamlink to exit faster when using Ctrl+C (https://github.com/streamlink/streamlink/pull/1658) - Added an OpenCV Face Detection example (https://github.com/streamlink/streamlink/pull/1689) ```text BZHDeveloper (1): plugins.bfmtv : Update regular expression (#1703) Billy (1): plugins.ok_live: fix extraction Billy2011 (2): stream.dash: fix stuttering streams & maybe high CPU load (#1718) stream.akamaihd: fix some log.debug... issues (#1729) Hsiao-Ting Yu (1): Add plugin for www.kingkong.com.tw (#1666) LoneFox78 (1): plugins.tvcatchup: support for https URLs back-to (3): plugins.chaturbate: only open a stream if the url is not empty stream.hls_playlist: removed int check from PROGRAM-ID (#1707) docs: PotPlayer Stdin Pipe beardypig (30): plugins.bbciplayer: enable HD for some channels and speed up start-up plugins.goodgame: update for change in page layout tests: test to ensure each plugin listed in the plugin matrix exists plugins.goodgame: fix bug with streamkey extraction plugins.onetv: add support for 1tv.ru and a few other related sites plugins.rtve: fix for m3u8 stream url formatting example: added an opencv face detection example plugins.europaplus: support for the europaplustv stream plugins.goltelevision: support for the live stream plugins.tvcatchup: add URL tests plugin.ustvnow: plugin to support ustvnow.com hls: support for live streams when using --hls-duration release: update to release script win-installer: add missing isodate module plugins.onetv: fix issues with ctc channels and add DASH support plugins.dailymotion: fix error logging for stream errors logging: set the log level once the plugin config files have been loaded logging: fixed issue with logging from plugins using logging module plugins.onetv: fixed tests plugins.reshet: support for reshet.tv live and VOD streams dash: fix for manifest reload - should be more reliable tests: coverage on src instead of the modules plugins.crunchyroll: switch method of obtaining session id plugins.facebook: remove debugging code plugins: store cookies between sessions (#1724) plugins.facebook: sd_src|hd_src can contain non-dash streams plugins.funimationnow: login support and bug fixes (#1721) logging: do not log when using quiet options (--json, --quiet, etc) dash: fix --json for dash streams and allow custom headers (#1748) logging: when using the trace level, log the timestamp beardypig (21): plugins.filmon: more robust channel_id extraction build: use versioneer to set the build number (#1413) plugins.btsports: add plugin bt sports Allow streamlink to exit faster when using Ctrl-C plugins.tf1: add lci url to the main tf1 domain (#1660) plugins.ine: support for updated site layout docs: add a note about socks4 and socks5 vs socks4a and socks5h (#1655) plugins.gardenersworld: updated page layout plugins.vidio: update to support new layout plugins.btsports: add missing plugin matrix entry and tests plugins.vidio: py2 vs. py3 unicode fix tests: test to ensure that all plugins are listed in the plugin matrix build: use _ instead of + in the Windows installer exe filename Plugin Arguments API (#1638) Change log as markdown refactor (#1667) Add the README file to the Python package (#1665) build: build and sign sdist in travis using an RSA sign-only key (#1701) logging: refactor to use python logging module (#1690) MPEG DASH Support (initial) (#1637) plugins.bbciplayer: add dash support plugins.facebook: support for DASH streams (#1727) hoilc (1): fix checking live status jshir (1): Fix bug 1730, vaughnlive port change yhel (1): Feature/france.tv sport (#1700) ``` ## streamlink 0.12.1 (2018-05-07) Streamlink 0.12.1 Small release to fix a pip / Windows.exe generation bug! ```text Charlie Drage (1): 0.12.0 Release ``` ## streamlink 0.12.0 (2018-05-07) Streamlink 0.12.0 Thanks for all the contributors to this month's release! New updates: - A *ton* of plugin updates (like always! see below for a list of updates) - Ignoring a bunch of useless files when developing (https://github.com/streamlink/streamlink/pull/1570) - A new option to limit the number of fetch retries (https://github.com/streamlink/streamlink/pull/1375) - YouTube has been updated to not use MuxedStream for livestreams (https://github.com/streamlink/streamlink/pull/1556) - Bug fix with ffmpegmux (https://github.com/streamlink/streamlink/pull/1502) - Removed dead plugins and deprecated options (https://github.com/streamlink/streamlink/pull/1546) ```text Alexis Murzeau (2): Avoid use of non-ASCII in dogan plugin Fix test_plugins.py encoding errors in containerized environment (#1582) BZHDeveloper (1): [TF1] Fix plugin (Fixes #1579) (#1606) Charlie Drage (4): Add OpenCollective message to release script Manually update CHANGELOG.rst Remove livestream.patch Update release script Igor Piddubnyi (3): Plugin implementation for live.russia.tv Fix review coments Correctly exit on error James Prance (1): Small tweaks to fix ITV player. Fixes #1622 (#1623) Mattias Amnefelt (1): stream.hls: change --hls-audio-select to take a list and wildcard (#1591) Mohamed El Morabity (1): Add support for international Play TV website Mohamed El Morabity (1): Add support for RTBF Mohamed El Morabity (1): [dailymotion] Fix for new stream data API (#1543) Sean Greenslade (1): Added retry-max option to limit the number of fetch retries. back-to (9): [ffmpegmux] Fixed bug of an invisible terminal [TVRPlus] Fix for hls_re and use headers for HLSStream [streann] Fixed broken plugin Removed some dead plugins and some Deprecated options [youtube] Don't use MuxedStream for livestreams [pixiv] New plugin for sketch.pixiv.net (#1550) [TVP] New Plugin for Telewizja Polska S.A. [build] Fixed AppVeyor build pip10 error (#1605) [ABweb] New plugin for BIS Livestreams of french AB Groupe (#1595) bastimeyer (2): plugins.welt: add plugin Add OS + editor file/directory names to .gitignore beardypig (7): plugins.rtve: add an option to parse_xml to try to fix invalid character entities plugins.vaughnlive: Updated server map plugins.brittv: fixed script layout change build/deploy: do not deploy streamlink-latest, and remove old nightlies (#1624) plugins.brittv: fix issue with stream url extraction, from 7018fc8 (#1625) plugins.raiplay: add user-agent header to stream redirect request plugins.dogan: update for page layout change fozzy (1): update plugin for longzhu.com to support new url pattern steven7851 (1): [app17] Fix HLS URL (#1600) ``` ## streamlink 0.11.0 (2018-03-08) Streamlink 0.11.0! Here's what's new: - Fixed documentation (https://github.com/streamlink/streamlink/pull/1467 and https://github.com/streamlink/streamlink/pull/1468) - Current versions of the OS, Python, Streamlink and Requests are now shown with -l debug (https://github.com/streamlink/streamlink/pull/1374) - ok.ru/live plugin added (https://github.com/streamlink/streamlink/pull/1451) - New option --hls-segment-ignore-names (https://github.com/streamlink/streamlink/pull/1432) - AfreecaTV plugin updates (https://github.com/streamlink/streamlink/pull/1390) - Added support for zattoo recordings (https://github.com/streamlink/streamlink/pull/1480) - Bigo plugin updates (https://github.com/streamlink/streamlink/pull/1474) - Neulion plugin removed due to DMCA notice (https://github.com/streamlink/streamlink/pull/1497) - And many more updates to numerous other plugins! ```text Alexis Murzeau (3): Remove Debian directory docs/install: use sudo for Ubuntu and Solus docs/install: add Debian instructions (#1455) Anton Tykhyy (1): Add ok.ru/live plugin BZHDeveloper (1): [TF1] Fix plugin (#1457) Bruno Ribeiro (1): added cd streamlink Drew J. Sonne (1): [bbciplayer] Fix authentication failures (#1411) Hannes Pétur Eggertsson (1): Ruv plugin updated. Fixes #643. (#1486) Mohamed El Morabity (1): Add support for IDF1 back-to (10): [cli-debug] Show current installed versions with -l debug [hls] New option --hls-segment-ignore-names [cli-debug] Renamed method and small template update [afreeca] Plugin update. - Login for +19 streams --afreeca-username --afreeca-password - Removed 15 sec countdown - Added some error messages - Removed old Global AfreecaTV plugin - Added url tests [zattoo] Added support for zattoo recordings [tests] Fixed metaclass on python 3 [periscope] Fix for variant HLS streams [facebook] mark as broken, they use dash now. Removed furstream: dead website and file was wrong formated UTF8-BOM [codecov] use pytest and upload all data bastimeyer (2): docs: fix table layout on the install page [neulion] Remove plugin. See #1493 beardypig (2): plugins.kanal7: fix for new streaming iframe plugins.foxtr: update regex to match new site layout leshka (1): [goodgame] Fixed url regexp for handling miscellaneous symbols in username. schrobby (1): update from github comments sqrt2 (1): [orf_tvthek] Work around broken HTTP connection persistence (#1420) unnutricious (1): [bigo] update video regex to match current website (#1412) ``` ## streamlink 0.10.0 (2018-01-23) Streamlink 0.10.0! There's been a lot of activity since our November release. Changes: - Multiple plugin updates (too many to list, see below for the plugin changes!) - HLS seeking support (https://github.com/streamlink/streamlink/pull/1303) - Changes to the Windows binary (docs: https://github.com/streamlink/streamlink/pull/1408 minor changes to install directory: https://github.com/streamlink/streamlink/pull/1407) ```text Alexis Murzeau (3): docs: remove flattr-badge.png image Fix various typos in comments and documentation Implement PKCS#7 padding decoding with AES-128 HLS BZHDeveloper (1): [canalplus] Update plugin according to website changes (#1378) Mohamed El Morabity (1): [pluzz] Fix video ID regex for France 3 Régions streams RosadinTV (1): Welcome 2018 (#1410) Sean Greenslade (4): Reworked picarto.tv plugin to deal with website changes. (#1359) Tweaked tigerdile URL regex to allow missing trailing slash. Added tigerdile HLS support and proper API poll for offline streams. Added basic URL tests for tigerdile. back-to (5): [zdf] apiToken update [camsoda] Fixed broken plugin [mixer] moved beam.py to mixer.py file requires two commits, for a proper commit history [mixer] replaced beam.pro with mixer.com [docs] Removed MPlayer2 - Domain expired - Not maintained anymore back-to (13): [BTV] Fixed login return message [qq] New Plugin for live.qq.com [mlgtv] Fixed broken Plugin streamlink/streamlink#1362 [viasat] Added support for urls without a stream_id - removed dead domains from _url_re - added a error message for geo blocking - new regex for stream_id from image url - Removed old embed plugin - try to find an iframe if no stream_id was found. - added tests [streann] Added headers for post request [Dailymotion] Fixed livestream id from channelpage [neulion] renamed ufctv.py to neulion.py [neulion] Updated the ufctv plugin to make it useable for other domains [youtube] added Audio m4a itag 256 and 258 [hls] Don't try to skip a stream if the offset is 0, don't raise KeyError if the m3u8 file is empty this allows the file to reload. [zengatv] New Plugin for zengatv.com [mitele] Update for different api response - fallback if not hls_url was found, just the suffix. - added url tests [youtube] New params for get_video_info (#1423) bastimeyer (2): nsis: restore old install dir, keep multiuser docs: rewrite Windows binaries install section beardypig (12): plugins.vaughnlive: try to guess the stream ID from the channel name plugins.vaughnlive: updated rtmp server map Update server map stream.hls: add options to skip some time at the start/end of VOD streams stream.hls: add option to restart live stream, if possible stream.hls: remove the end offset and replace with duration hls: add absolute start offset and duration options to the HLStream API duratio bug Fix bug with hls start offset = 0 EOL Python 3.3 plugins.kanal7: update to stream player URL config plugins.huya: fix stream URL scheme prefix fozzy (1): fix plugin for bilibili to adapt the new API hicrop <35128217+hicrop@users.noreply.github.com> (1): PEP8 (#1427) steven7851 (1): [Douyutv] fix API xela722 (1): Add plugin for olympicchannel.com (#1353) ``` ## streamlink 0.9.0 (2017-11-14) Streamlink 0.9.0 has been released! This release is mostly code refactoring as well as module inclusion. Features: - Updates to multiple plugins (electrecetv, tvplayer, Teve2, cnnturk, kanald) - SOCKS module being included in the Streamlink installer (PySocks) Many thanks to those who've contributed in this release! ```text Alexis Murzeau (2): docs: add new line before codeblock to fix them Fix sphinx warning on Directive class Charlie Drage (1): Update the release script Emrah Er (1): plugins.canlitv: fix URLs (#1281) Jake Robertson (3): exit with code 130 after a KeyboardInterrupt refactor error code determination unify sys.exit() calls RosadinTV (5): Update eltrecetv.py Update eltrecetv.py Update plugin_matrix.rst Add webcast_india_gov.py Add test_webcast_india_gov.py back-to (3): [zattoo] It won't work with None in Python 3.6, set always a default date instead of None. [liveme] API update (#1298) Ignore WinError 10053 / WSAECONNABORTED beardypig (10): plugins.tvplayer: extract the channel id when logged in as a subscriber installer: include the socks proxy modules plugins.kanal7: update for page layout change and referrer check plugins.turkuvaz: fix some turkuvaz sites and add support for anews plugins.cinergroup: support for different showtv url plugins.dogus/startv: fix dogus sites plugins.dogan: fix for teve2 and cnnturk plugins.dogan: fix for kanald plugins.tvcatchup: HLS source extraction update setup: fix PySocks module dependency ficofabrid <31028711+ficofabrid@users.noreply.github.com> (1): Add a single newline at the end of the file. (#1235) fozzy (1): fix huya.com plugin steven7851 (1): plugins.pandatv: fix APIv3 (#1286) wlerin (1): plugin.showroom: update to new api (#1311) ``` ## streamlink 0.8.1 (2017-09-12) 0.8.1 of Streamlink! 97 commits have occurred since the last release, including a large majority of plugin changes. Here's the outline of what's new: - Multiple plugin fixes (twitch, vaughlive, hitbox, etc.) - Donations! We've gone ahead and joined the Open Collective at https://opencollective.com/streamlink - Multiple doc updates - Support for SOCKS proxies - Code refactoring Many thanks to those who've contributed in this release! ```text Benedikt Gollatz (1): Fix player URL extraction in bloomberg plugin Forrest (1): Update donation docs to note open collective (#1105) Journey (2): Update Arconaitv to new url fix arconai test plugin Pascal Romahn (1): The site always contains the text "does not exist". This should resolve issue https://github.com/streamlink/streamlink/issues/1193 RosadinTV (2): Update Windows portable version documentation Fix documentation font-size Sad Paladin (1): plugins.vk: add support for vk.com vod/livestreams Xavier Damman (1): Added backers and sponsors on the README back-to (5): [zattoo] New plugin for zattoo.com / tvonline.ewe.de / nettv.netcologne.com (#1039) [vidio] Fixed Plugin, new Regex for HLS URL [arconai] Fixed plugin for new website [npo] Update for new website layout, Added HTTPStream support [liveme] url regex update bastimeyer (3): docs: add a third party applications list docs: add an official streamlink applications list Restructure README.md beardypig (17): plugins.brittv: support for live streams on brittv.co.uk plugins.hitbox: fix bug when checking for hosted channels plugins.tvplayer: small update to channel id extraction plugins.vaughnlive: support for the new vaughnlive website layout plugins.vaughnlive: work around for a ssl websocket issue plugins.vaughnlive: drop HLS stream support for vaughnlive plugins.twitch: enable certificate verification for twitch api Resolve InsecurePlatformWarnings for older Python2.7 versions cli: remove the deprecation warnings for some of the http options plugins.vaughnlive: set a user agent for the initial page request plugins.adultswim: fix for some live streams plugins: separated the built-in plugins in to separate plugins cli: support for SOCKS proxies plugins.bbciplayer: fix for page formatting changes and login plugins.cdnbg: support for updated layout and extra channels plugins: add priority ordering to plugins plugins.bbciplayer: support for older VOD streams fozzy (10): remove unused code fix douyutv plugin by using new API update douyutv.py to support multiple rates by steven7851 update HLS Stream name to 'live' update weights for streams fix stream name update stream name, middle and middle2 are of different quality Add support for skai.gr add eol remove unused importing jgilf (2): Update ufctv.py Update ufctv.py sdfwv (1): [bongacams] replace RTMP with HLS Fixed streamlink/streamlink#1074 steven7851 (8): plugins.douyutv: update post data plugins.app17: fix HLS url plugins.app17: RTMPStream is no longer used plugins.app17: return RTMPStream back plugins.douyutv: use douyu open API plugins.app17: new layout plugins.app17: use https plugins.app17: fix wansu cdn url supergonkas (1): Add support for RTP Play (#1051) unnutricious (2): bigo: add support for hls streams bigo: improve plugin url regex ``` ## streamlink 0.7.0 (2017-06-30) 0.7.0 of Streamlink! Since our May release, we've incorporated quite a few changes! Outlined are the major features in this month's release: - Stream types will now be sorted accordingly in terms of quality - TeamLiquid.net Plugin added - Numerous plugin & bug fixes - Updated HomeBrew package - Improved CLI documentation Many thanks to those who've contributed in this release! If you think that this application is helpful, please consider supporting the maintainers by [donating](https://streamlink.github.io/donate.html). ```text Alex Shafer (1): Return sorted list of streams. (#731) Alexandre Hitchcox (1): Allow live channel links without '/c/' prefix Alexis Murzeau (1): docs: fix typo: specifiying, neverthless CatKasha (1): Add MPC-HC x64 in streamlinkrc Forrest (1): Add a few more examples to the player option (#896) Jacob Malmberg (3): Here's the plugin I wrote for teamliquid.net (w/ some help from https://github.com/back-to) Tests for teamliquid plugin Now with RE! Mohamed El Morabity (9): Update for live API changes Add unit tests for Euronews plugin Drop pcyourfreetv plugin Add support for regional France 3 streams Add support for TV5Monde PEP8 Add support for VOD/audio streams Add support for radio.net Ignore unreliable stream status returned by radio.net Sebastian Meyer (1): Homebrew package (#929) back-to (2): [dailymotion] fix for broken .f4m file that is a .m3u8 file (only livestreams) [arte] vod api url update & add new/missing languages bastimeyer (2): docs: fix parameters being linked in code blocks Improve CLI documentation beardypig (1): plugins.hitbox: add support for smashcast.tv beardypig (21): plugins.bbciplayer: update to reflect slight site layout change plugins.bbciplayer: add option to login to a bbc account http_server: handle socket closed exception for Python 2.7 docs: update Sphinx config to fix the rendering of -- docs: pin sphinx to 1.6.+ so that no future changes affect the docs plugins.tvplayer: fix bug with some channels not loading plugins.hitbox: fix new VOD urls, and add support for hosted streams plugins.tvplayer: fix bug with some channels when not authenticated setup: exclude requests version 2.16 through 2.17.1 win32: fix missing modules when using windows installer bbciplayer: fix for api changes to iplayer tvplayer: updated to match change token parameter name plugins.looch: support for live and vod streams on looch.tv plugins.webtv: decrypt the stream URL when applicable plugins.dogan: small api change for teve2.com.tr plugins.kanal7: fix for nested iframes win32: update the dependencies for the windows installer plugins.canlitv: simplified and fixed the m3u8 regex plugins.picarto: support for VOD plugins.ine: update to extract the relocated jwplayer config plugin.ufctv: support for free and premium vod/live streams cirrus (3): Create arconia.py Rename arconia.py to arconai.py Create plugin_matrix.rst steven7851 (4): plugins.app17: fix hls url and support UID page little change plugins.app17: change ROOM_URL [douyu] temporary fix by revert to previously commit (#1015) whizzoo (2): Restore support for RTL XL plugin.rtlxl: Remove spaces from line 14 yhel (1): Don't return an error when the stream is offline yhel (1): Add capability of extracting current sport.francetv stream ``` ## streamlink 0.6.0 (2017-05-11) Another release of Streamlink! We've updated more plugins, improved documentation, and moved out nightly builds to Bintray (S3 was costing *wayyyy* too much). Again, many thanks for those who've contributed! Thank you very much! ```text Daniel Draper (1): Will exit with exit code 1 if stream cannot be opened. (#785) Forrest Alvarez (3): Update readme so users are aware using Streamlink bypasses ads Forgot a ) Make notice more agnostic Mohamed El Morabity (18): Disable HDS streams which are no more available Add support for pc-yourfreetv.com Add support for BFMTV Add support for Cam4 Disable HDS streams for live videos Add support for Bloomberg Add support for Bloomberg Radio live stream Add support for cnews.fr Fix unit tests for canalplus plugin Add authentication token to http queries Add rte.ie/player support Add support for HLS streams Update for new page layout Update for new new page layout Fix for new layout Pluzz platform replaced by new france.tv website Update documentation Always use token generator for streams from france.tv Mohamed El Morabity (1): plugins.brightcove: support for HLS stream URLs with query strings + RTMPE stream URLs (#790) RosadinTV (5): Update plugin_matrix.rst Add telefe.py Add test_plugin_telefe.py Update telefe.py Add support for ElTreceTV (VOD & Live) (#816) Sebastian Meyer (1): Improve contribution guidelines (#772) back-to (9): [chaturbate] New API for HLS url [chaturbate] Fixed python 3.5 bug and added regex tests [VRTbe] new plugin for vrt.be/vrtnu [oldlivestream] New regex for cdn subdomains and embeded streams [tv1channel.org] New Plugin for embeded streams on tv1channel.org [cyro] New plugin for embeded streams from cyro.se [Facebook] Added unittests [ArteTV] new regex, removed rtmp and better result for available streams [NRK.NO] fixed regex for _api_baseurl_re beardypig (15): travis: use pytest to run the tests for coverage Revert "stream.hds: ensure the live edge does not go past the latest fragment" plugins.azubutv: plugin removed plugins.ustreamtv: log timeout errors and adjust retries for polling appveyor: update config to fix builds on Python 3.3 plugin.tvplayer: update to support new site layout plugin.tvplayer: update tests to match new plugin plugins.tvplayer: allow https stream URLs plugins.tvnbg: add support for live streams on tvn.bg plugins.apac: add ustream apac wrapper Deploy nightly builds to Bintray instead of S3 plugins.streann: support for ott.streann.com utils.crypto: fix openssl_decrypt for py27 build: update the bintray release notes for nightlies plugins.streamable: support for videos on streamable.com beardypig (20): plugins.ustreamtv: support for the new ustream.tv API plugins.ustreamtv: add suppot for redirectLocked embedded streams plugins.livecodingtv: renamed to livedu, and updated for new site plugins.ustreamtv: continue to poll the ustream API when streaming plugins.ustreamtv: rename the plugin class back to UStreamTV docs: remove references to python-librtmp plugins.ustream: add some comments plugins.ustreamtv: support for password protected streams plugins.nbc: support vod from nbc.com plugins.nbcsports: add support for nbcsports.com via theplatform stream.hds: ensure the live edge does not go past the latest fragment Dailymotion feature video and backup stream fallback (#773) plugin.gardenersworld: support for VOD on gardenersworld.com plugins.twitch: support for pop-out player URLS and fixed clips tests: cmdline tests can fail if there are some config options set plugins.ustreamtv: fix moduleInfo retry loop cli: add --url option that can be used in config files to set a URL cli: clarification of the --url option cli: add wildcard to --stream-types option plugins.rtve: stop IOError bubbling up on 404 errors wlerin (2): Send Referer and UserAgent headers Fix method decorator zp@users.noreply.github.com (1): New plugin for Facebook 360p streams https://gist.github.com/zp/c461761565dba764c90548758ee5ae9f ``` ## streamlink 0.5.0 (2017-04-04) Streamlink 0.5.0! Lot's of contributions since the last release. As always, lot's of updating to plugins! One of the new features is the addition of Google Drive / Google Docs, you can now stream videos stored on Google Docs. We've also gone ahead and removed dead plugins (sites which have gone down) as well as added pycrypto as a dependency for future plugins. Again, many thanks for those who have contributed! Thank you very much! ```text CallMeJuf (2): Aliez plugin now accepts any TLD (#696) New Periscope URL #748 Daniel Draper (2): More robust url regex for bigo plugin. More robust url regex for bigo plugin, added unittest Josip Ponjavic (4): fix vaugnlive info_url Update archlinux installation instructions and maintainer info setup: choose pycrypto as a dependency using an environment variable Add info about pycrypto and pycountry variables to install doc Mohamed El Morabity (1): plugins.pluzz: fix SWF player URL search to bring back HDS stream support (#679) back-to (5): plugins.camsoda Added support for camsoda.com plugins.canlitv - Added new plugin canlitv Removed dead plugins (#702) plugins.camsoda - Added tests and small update for the plugin plugins.garena - Added new plugin garena beardypig (11): plugins.bbciplayer: add support for BBC iPlayer live and VOD plugins.vaughnlive: updated player version and info URL plugins.vaughnlive: search for player version, etc in the swf file plugins.beam: add support for VOD and HLS streams for live (#694) plugins.bbciplayer: add support for HLS streams utils.l10n: use default locale if the system returns an invalid locale plugins.dailymotion: play the featured video from channel pages plugins.rtve: support for avi/mov VOD streams plugins.googledocs: plugin to support playing videos stored on google docs plugins.googledocs: updated the url regex and added a status check plugins.googledrive: add googledrive support steven7851 (3): plugins.17media: Add support for HTTP stream plugins.17media: fix rtmp stream plugins.douyutv: support vod (#706) ``` ## streamlink 0.4.0 (2017-03-09) 0.4.0 of Streamlink! 114 commits since the last release and *a lot* has changed. In general, we've added some localization as well as an assortment of new plugins. We've also introduced a change for Streamlink to *not* check for new updates each time Streamlink starts. We found this feature annoying as well as delaying the initial start of the stream. This feature can be re-enabled by the command line. The major features of this release are: - New plugins added - Ongoing support to current plugins via bug fixes - Ensure retries to HLS streams - Disable update check Many thanks to all contributors who have contributed in this release! ```text 406NotAcceptable <406NotAcceptable@somewhere> (2): plugins.afreecatv: API changes plugins.connectcast: API changes BackTo (1): plugins.zdf_mediathek Added missing headers for http.get (#653) Charlie Drage (7): Updating the release script. 0.3.1 Release Update release script again to include sdist Fix underlining issue Fix the CHANGELOG.rst 0.3.2 Release Update underscores title release script (#563) Forrest (3): Update license and debian copyright (#515) Add a donation page (#578) Fix up the donate docs (#672) Forrest Alvarez (1): Update license and debian copyright John Smith (1): plugins.bongacams: a few small changes (#429) Mohamed El Morabity (1): Check whether videos are DRM-protected Add log messages when no stream is available Mohamed El Morabity (3): Add support for replay.gulli.fr (#468) plugins.pluzz: add support for ludo.fr and zouzous.fr (#536) Add subtitle support for pluzz plugins (#646) Scott Buettner (1): Fix Crunchyroll string.format in Python 2.6 (#539) Sven (1): Adding Huomao plugin with possibility for different stream qualities. Sven Anderzén (1): Huomao plugin tests (#566) back-to (2): [earthcam] Added HLS, Fixed live RTMP and changes some stuff plugins.ard_mediathek added mediathek.daserste.de support beardypig (74): plugins.schoolism: add support for schoolism.com plugins.earthcam: added support for live and archive cam streams stream.hls_playlist: invalid durations in EXTINF lines are ignored plugins.livecoding: update to support the new domain: liveedu.tv plugins.srgssr: fix playlist reload auth issue Play twitch VOD stream from the beginning even if is still being recorded cli: wait for process to exit, not exit with non-0 error code Fix bug in customized Windows install add a general locale setting which can be used by plugins stream.hls: support external audio tracks plugins.turkuvaz: add referer to the secure token request localization: search for language codes in part2t+part2b+part3 localization: invalid language/country codes are always inequivalent stream.hls: only support external audio tracks if ffmpeg is available installer: include the missing pkg_resources package Rewritten StreamProcess class (#441) plugins.dogus: fix for ntv streams not being found plugins.dogus: add support for eurostartv live stream plugins.twitch: update public API calls to use v5 API (#484) plugins.filmon: support for new site layout (#508) Support for Ceskatelevize streams (#520) Ensure retries with HLS Streams (#522) utils.l10n: add Country/Language classes, use pycountry is the iso modules are not available plugins.crunchyroll: added option to set the session id to a specific value CI: add pycountry for testing plugins.openrectv: add source quality for openrectv utils.l10n: default to en_US when an invalid locale is set fix some python2.6 issues allow failure for python2.6 in travis and update minimum supported python version to 2.7, as well as adding an annoying deprecation warning stream.hls: pick a better default stream language stream.hls: Retry HTTP requests to get the key for HLS streams plugins.openrectv: fixed broken vod support appveyor: use the build.cmd script to install streamlink, so that the sdk can be used if required stream.hls: last chance fallback audio stream: make Stream responsible for generating the stream_url utils.l10n: fix bug in iso3166 country lookup tests: speed up the cmdline tests Remove deprecation warning for invalid escape sequences tests: merged the Localization tests back in to one module plugins.foxtr: adjusted regex for slight site layout change plugins.ard_mediathek: update to support site change stream.hds: warn about streams being protected by DRM plugins.tvrplus: add support for tvrplus.ro live streams plugins.tvrby: support for live streams of Belarus national TV plugins.ovvatv: add support for ovva.tv live streams cli.utils.http_server: avoid "Address already in use" with --player-external-http setup: choose pycountry as a dependency using an environment variable plugins.ovvatv: fix b64decoding bug plugin.mitele: use the default plugin cache plugins.seetv: add support for seetv.tv live streams cli.utils.http_server: ignore errors with socket.shutdown plugins.daisuki: add support for VOD streams from daisuki.net (#609) plugins.daisuki: fix for truncated subtitles cli: disable automatic version checking by default plugins.rtve: update rtve plugin to support VOD (#628) plugins.rtve: return all the available qualities plugins.funimationnow: support for US and UK funimation|now streams (#629) cli: --no-version-check always disables the version check plugins.tvplayer: support for authenticated streams docs: updated the docs for built-in stream parameters utils.l10n: fix for some locales without an official name in pycountry plugins.wwenetwork: support for WWE Network streams plugins.trt: make the url test case insensitive and fix py3 bug plugins.tvplayer: automatically set postcode when required plugins.ard_live: updated to new site layout plugins.vidio: fix for regex, if the url is the english version plugins.animelab: added support for AnimeLab.com VOD plugin.npo: rewrite of plugin to use the new API (#642) plugins.goodgame: support for http URLs docs.donate: drop name headers to subsection level stream.hls: format string name input for parse_variant_playlist plugins.wwenetwork: use the resolution and bitrate in the stream name docs: make the nightly installer link more obvious stream.hls: option to select a specific, non-standard audio channel fozzy (4): update douyutv plugin, use new API update to support different quality fix typo and indent correct typo fozzy (3): Add support for Huya.com in issue #425 (#465) Fix issue #426 on plugins/tga.py (#456) fix douyutv issue #637 (#666) intact (1): Add Rtvs.sk Plugin steven7851 (4): plugins.douyutv: fix room id regex (#514) plugins.pandatv: use Pandatv API v3 (#410) Add plugin for 17app.co (#502) plugins.zhanqi: use new api (#498) wlerin (1): plugins.showroom: add support for showroom-live.com live streams (#633) ``` ## streamlink 0.3.2 (2017-02-10) 0.3.2 release of Streamlink! A minor bug release of 0.3.2 to fix a few issues with stream providers. Thanks to all whom have contributed to this (tiny) release! ```text Charlie Drage (3): Update release script again to include sdist Fix underlining issue Fix the CHANGELOG.rst Sven (1): Adding Huomao plugin with possibility for different stream qualities. beardypig (7): Ensure retries with HLS Streams (#522) utils.l10n: add Country/Language classes, use pycountry is the iso modules are not available plugins.crunchyroll: added option to set the session id to a specific value CI: add pycountry for testing plugins.openrectv: add source quality for openrectv utils.l10n: default to en_US when an invalid locale is set stream.hls: pick a better default stream language intact (1): Add Rtvs.sk Plugin ``` ## streamlink 0.3.1 (2017-02-03) 0.3.1 release of Streamlink A *minor* release, we update our source code upload to *not* include the ffmpeg.exe binary as well as update a multitude of plugins. Thanks again for all the contributions as well as updates! ```text Charlie Drage (1): Updating the release script. Forrest (1): Update license and debian copyright (#515) Forrest Alvarez (1): Update license and debian copyright John Smith (1): plugins.bongacams: a few small changes (#429) Mohamed El Morabity (1): Check whether videos are DRM-protected Add log messages when no stream is available Mohamed El Morabity (1): Add support for replay.gulli.fr (#468) beardypig (20): plugins.schoolism: add support for schoolism.com stream.hls_playlist: invalid durations in EXTINF lines are ignored plugins.livecoding: update to support the new domain: liveedu.tv plugins.srgssr: fix playlist reload auth issue Play twitch VOD stream from the beginning even if is still being recorded cli: wait for process to exit, not exit with non-0 error code Fix bug in customized Windows install add a general locale setting which can be used by plugins stream.hls: support external audio tracks plugins.turkuvaz: add referer to the secure token request localization: search for language codes in part2t+part2b+part3 localization: invalid language/country codes are always inequivalent stream.hls: only support external audio tracks if ffmpeg is available installer: include the missing pkg_resources package Rewritten StreamProcess class (#441) plugins.dogus: fix for ntv streams not being found plugins.dogus: add support for eurostartv live stream plugins.twitch: update public API calls to use v5 API (#484) plugins.filmon: support for new site layout (#508) Support for Ceskatelevize streams (#520) fozzy (1): Add support for Huya.com in issue #425 (#465) steven7851 (1): plugins.douyutv: fix room id regex (#514) ``` ## streamlink 0.3.0 (2017-01-24) Release 0.3.0 of Streamlink! A lot of updates to each plugin (thank you @beardypig !), automated Windows releases, PEP8 formatting throughout Streamlink are some of the few updates to this release as we near a stable 1.0.0 release. Main features are: - Lot's of maintaining / updates to plugins - General bug and doc fixes - Major improvements to development (github issue templates, automatically created releases) ```text Agustín Carrasco (1): Links on crunchy's rss no longer contain the show name in the url (#379) Brainzyy (1): Add basic tests for stream.me plugin (#391) Javier Cantero (2): plugins/twitch: use version v3 of the API plugins/twitch: use kraken URL John Smith (3): Added support for bongacams.com streams (#329) streamlink_cli.main: close stream_fd on exit (#427) streamlink_cli.utils.progress: write new line at finish (#442) Max Riegler (1): plugins.chaturbate: new regex (#457) Michiel Sikma (1): Update PLAYER_VERSION, as old one does not return data. Add ability to use streams with /embed/video in the URL, from embedded players. (#311) Mohamed El Morabity (6): Add support for pluzz.francetv.fr (#343) Fix ArteTV plugin (#385) Add support for Canal+ TV group channels (#416) Update installation instructions for Fedora (#443) Add support for Play TV (#439) Use token generator for HLS streams, as for HDS ones (#466) RosadinTV (1): --can-handle-url-no-redirect parameter added (#333) Stefan Hanreich (1): added chocolatey to the documentation (#380) bastimeyer (3): Automatically create Github releases Set changelog in automated github releases Add a github issue template beardypig (55): plugins.tvcatchup: site layout changed, updated the stream regex to accommodate the change (#338) plugins.streamlive: streamlive.to have added some extra protection to their streams which currently prevents us from capturing them (#339) cli: add command line option to specific logging path for subprocess errorlog plugins.trtspor: added support for trtspor.com (#349) plugins.kanal7: fixed page change in kanal7 live stream (#348) plugins.picarto: Remove the unreliable rtmp stream (#353) packaging: removed the built in backports infavour of including them as dependencies when required (#355) Boost the test coverage a bit (#362) plugins: all regex string should be raw (#361) ci: build and test on Python 3.6 (+3.7 on travis, with allowed failure) (#360) packages.flashmedia: fix bug in AMFMessage (#359) tests: use mock from unittest when available otherwise fallback to mock (#358) stream.hls: try to retry stream segments (#357) tests: add codecov config file (#363) plugins.picarto: updated plugin to use tech_switch divs to find the stream parameters plugins.mitele: support for live streams on mitele.es docs: add a note about python-devel needing to be installed in some cases docs/release: generate the changelog as rst instead of md plugins.adultswim: support https urls use iso 8601 date format for the changelog plugins.tf1: added plugin to support tf1.fr and lci.fr plugins.raiplay: added plugin to support raiplay.it plugins.vaughnlive: updated player version and info URL (#383) plugins.tv8cat: added support for tv8.cat live stream (#390) Fix TF1.fr plugin (#389) plugins.stream: fix a default scheme handling for urls Add support for some Bulgarian live streams (#392) rtmp: fix bug in redirect for rtmp streams plugins.sportal: added support for the live stream on sportal.bg plugins.bnt: update the user agent string for the http requests plugins.ssh101: update to support new site layout Optionally use FFMPEG to mux separate video and audio streams (#224) Support for 4K videos in YouTube (#225) windows-installer: add the version info to the installer file include CHANGELOG.rst instead of .md in the egg stream.hls: output duplicate streams for HLS when multiple streams of the same quality are available stream.ffmpegmux: fix support for avconv, avconv will be used if ffmpeg is not found Adultswin VOD support (#406) Move streamlink_cli.utils.named_pipe in to streamlink.utils plugins.rtve: update plugin to support new streaming method stream.hds: omit HDS streams that are protected by DRM Adultswin VOD fix for live show replays (#418) plugins.rtve: add support for legacy stream URLs installer: remove the streamlink bin dir from %PATH% before installing plugins.twitch: only check hosted channels when playing a live stream docs: tweaks to docs and docs build process Fix iframe detection for BTN/cdn.bg streams (#437) fix some regex that give deprecation warnings in python 3.6 plugins.adultswim: correct behaviour for archived streams plugins.nineanime: add scheme to grabber api url if not present session: add an option to disable Diffie Hellman key exchange plugins.srgssr: added support for srg ssr sites: srf, rts and rsi plugins.srgssr: fixed bug in api URL and fixed akamai urls with authparams cli: try to terminate the player process before killing it (if terminate takes too long) plugins.swisstxt: add support for the SRG SSR sites sports sections fozzy (1): Add plugin for huajiao.com and zhanqi.tv (#334) sqrt2 (1): Fix swf_url in livestream.com plugin (#428) stepshal (1): Remove trailing. stepshal (2): Add blank line after class or function definition (#408) PEP8 (#414) ``` ## streamlink 0.2.0 (2016-12-16) Release 0.2.0 of Streamlink! We've done numerous changes to plugins as well as fixed quite a few which were originally failing. Among these changes are updated docs as well as general UI/UX cleaning with console output. The main features are: - Additional plugins added - Plugin fixes - Cleaned up console output - Additional documentation (contribution, installation instructions) Again, thank you everyone whom contributed to this release! :D ```text Beardypig (6): Turkish Streams Part III (#292) coverage: include streamlink_cli in the coverage, but exclude the vendored packages (#302) Windows command line parsing fix (#300) plugins.atresplayer: add support for live streams on atresplayer.com (#303) Turkish Streams IV (#305) Support for local files (#304) Charlie Drage (2): Spelling error in release script Fix issue with building installer Fishscene (3): Updated homepage Updated README.md Fixed type in README.md. Forrest (3): Modify the browser redirect (#191) Update client ID (#241) Update requests version after bug fix (#239) Josip Ponjavic (1): Add NixOS install instructions Simon Bernier St-Pierre (1): add contributing guidelines bastimeyer (1): Add metadata to Windows installer beardypig (25): plugins.nhkworld: update the plugin to use the new HLS streams plugins.picarto: updated the plugin to use the new javascript and support HLS streams add pycryptodome==3.4.3 to the setup.py dependencies plugins.nineanime: added a plugin to support 9anime.to plugins.nineanime: update the plugin matrix in the docs plugins.atv: add support for the live stream on atv.com.tr include omxplayer in the list of players in the documentation update the player docs with findings from @Junior1544 and @stevekmcc plugins.bigo: support for bigo.tv docs: move pycryptodome to the list of automatically installed libraries in the docs plugins.dingittv: add support for dingit.tv plugins.crunchyroll: support ultra quality for subscribers update URL for docs to point to the github.io page stream.hls: stream the HLS segments out to the player as they are downloaded, decrypting on the fly installer: install the required MS VC++ runtime files beside the python installation (see takluyver/pynsist/pull/87) plugins.bigo: FlashVars regex updated due to site change add some license notices for the bundled libraries plugins.youtube: support additional live urls add support for a few Turkish live streams plugins.foxtr: add support for turkish fox live streams plugins.kralmuzik: basic support for the HLS stream only stream.hds: added option to force akamai authentication plugins.startv: refactored in to a base class, to be used in other plugins that use the same hosting as StarTV plugins.kralmuzik: refactored to use StarTVBase plugins.ntv: added NTV support plugins.atv: add support for a2tv which is very similar to atv plugins.dogan: support for teve2, kanald, dreamtv, and ccnturk via the same plugin plugins.trt: added support for the live channels on trt.net.tr che (1): plugins.twitch: support for clips added ioblank (1): Use ConsoleOutput for run-as-root warning mmetak (3): Update install instruction (#257) Add links for windows portable version. (#299) Add package maintainers to docs. (#301) thatlinuxfur (1): Added tigerdile.com support. (#221) ``` ## streamlink 0.1.0 (2016-11-21) A major update to Streamlink. With this release, we include a Windows binary as well as numerous plugin changes and fixes. The main features are: - Windows binary (and generation!) thanks to the fabulous work by @beardypig - Multiple plugin fixes - Remove unneeded run-as-root (no more warning you when you run as root, we trust that you know what you're doing) - Fix stream quality naming issue ```text Beardypig (13): fix stream quality naming issue with py2 vs. py3, fixing #89 (#96) updated connectcast plugin to support the new rtmp streams; fixes #93 (#95) Fix for erroneous escape coding the livecoding plugin. Fixes #106 (#121) TVPlayer.com: fix for 400 error, correctly set the platform parameter (#123) Added a method to automatically determine the encoding when parsing JSON, if no encoding is provided. (#122) when retry-streams and twitch-disable-hosting arguments are used the stream is retried until a non-hosted stream is found (#125) plugins.goodgame: Update for API change (#130) plugins.adultswim: added a new adultswim.com plugin (#139) plugins.goodgame: restored DDOS protection cookie support (#136) plugins.younow: update API url (#135) plugins.euronew: update to support the new site (#141) plugins.webtv: added a new plugin to support web.tv (#144) plugins.connectcast: fix regex issue with python 3 (#152) Brainzyy (1): Add piczel.tv plugin (courtesy of @intact) (#114) Charlie Drage (1): Update release scripts Erk- (1): Changed the twitch plugin to use https instead of http as discussed in #103 (#104) Forrest (2): Modify the changelog link (#107) Update cli to note a few windows issues (#108) Simon Bernier St-Pierre (1): change icon Simon Bernier St-Pierre (1): finish the installer (#98) Stefan (1): Debian packaging base (#80) Stefan (1): remove run-as-root option, reworded warning #85 (#109) Weslly (1): Fixed afreecatv.com url matching (#90) bastimeyer (2): Improve NSIS installer script Remove shortcut from previous releases on Windows beardypig (8): plugins.cybergame: update to support changes to the live streams on the cybergame.tv website Use pycryptodome inplace of pyCrypto Automated build of the Windows NSIS installer support for relative paths for rtmpdump makeinstaller: install the streamlinkrc file in to the users %APPDATA% directory remove references to livestreamer in the win32 config template stream.rtmpdump: fixed the rtmpdump path issue, introduced in 6bf7fd7 pin requests to <2.12.0 to avoid the strict IDNA2008 validation ethanhlc (1): fixed instance of livestreamer (#99) intact (1): plugins.livestream: Support old player urls mmetak (2): fix vaughnlive.tv info_url (#88) fix vaughnlive.tv info_url (yet again...) (#143) skulblakka (1): Overworked Plugin for ZDF Mediathek (#154) sqrt2 (1): Fix ORF TVthek plugin (#113) tam1m (1): Fix zdf_mediathek TypeError (#156) ``` ## streamlink 0.0.2 (2016-10-12) The second ever release of Streamlink! In this release we've not only set the stepping stone for the further development of Streamlink (documentation site updated, CI builds working) but we're already fixing bugs and implementing features past the initial fork of livestreamer. The main features of this release are: - New windows build available and generated via pyinstaller - Multiple provider bug fixes (twitch, picarto, itvplayer, crunchyroll, periscope, douyutv) - Updated and reformed documentation which also includes our site https://streamlink.github.io As always, below is a `git shortlog` of all changes from the previous release of Streamlink (0.0.1) to now (0.0.2). ```text Brainzyy (1): add stream.me to the docs Charlie Drage (9): Add script to generate authors list / update authors Add release script Get setup.py ready for a release. Revert "Latest fix to plugin from livestreamer" 0.0.1 Release Update the README with installation notes Update copyright author Update plugin description on README It's now 2016 Forrest (1): Add a coverage file (#54) Forrest Alvarez (4): Modify release for streamlink Remove faraday from travis run Remove tox Add the code coverage badge Latent Logic (1): Picarto plugin: multistream workaround (fixes #50) Maschmi (1): added travis build status badge fixes #74 (#76) Randy Taylor (1): Fix typo in issues docs and improve wording (#61) Simon Bernier St-Pierre (8): add script to build & copy the docs move makedocs.sh to script/ Automated docs updates via travis-ci prevent the build from hanging fix automated commit message add streamboat to the docs disable docs on pull requests twitch.tv: add option to disable hosting Simon Bernier St-Pierre (2): Don't delete everything if docs build fail (#62) Create install script for pynsist (#27) beardypig (3): TVPlayer plugin supports the latest version of the website crunchyroll: decide if to parse the stream links as HLS variant playlist or plain old HLS stream (fixes #70) itvplayer: updated the productionId extraction method boda2004 (1): fixed periscope live streaming and allowed url re (#79) ethanhlc (1): fixed instances of chrippa/streamlink to streamlink/streamlink scottbernstein (1): Latest fix to plugin from livestreamer steven7851 (1): Update plugin.douyutv ``` ## streamlink 0.0.1 (2016-09-23) The first release of Streamlink! This is the first release from the initial fork of Livestreamer. We aim to have a concise, fast review process and progress in terms of development and future releases. Below is a `git shortlog` of all commits since the last change within Livestream (hash ab80dbd6560f6f9835865b2fc9f9c6015aee5658). This will serve as a base-point as we continue development of "Streamlink". New releases will include a list of changes as we add new features / code refactors to the existing code-base. ```text Agustin Carrasco (2): plugins.crunchyroll: added support for locale selection plugins.crunchyroll: use locale parameter on the header's user-agent as well Alan Love (3): added support for livecoding.tv removed printing updated plugin matrix Alexander (1): channel info url change in afreeca plugin Andreas Streichardt (1): Add Sportschau Anton (2): goodgame ddos validation add stream_id with words Benedikt Gollatz (1): Add support for ORF TVthek livestreams and VOD segments Benoit Dien (1): Meerkat plugin Brainzyy (1): fix azubu.tv plugin Charlie Drage (9): Update the README Fix travis Rename instances of "livestreamer" to "streamlink" Fix travis Add script to generate authors list / update authors Get setup.py ready for a release. Add release script Revert "Latest fix to plugin from livestreamer" 0.0.0 Release Charmander <~@charmander.me> (1): plugins.picarto: Update for API and URL change Chris-Werner Reimer (1): fix vaughnlive plugin #897 Christopher Rosell (7): plugins.twitch: Handle subdomains with dash in them, e.g. en-gb. cli: Close output on exit. Show a brief usage when no option is specified. cli: Fix typo. travis: Use new artifacts tool. docs: Fix readthedocs build. travis: Build installer exe aswell. Daniel Meißner (2): plugin: added media_ccc_de api and protocol changes docs/plugin_matrix: removed needless characters Dominik Sokal (1): plugins.afreeca: fix stream Ed Holohan (1): Quick hack to handle Picarto changes Emil Stahl (1): Add support for viafree.dk Erik G (7): Added plugin for Dplay. Added plugin for Dplay and removed sbsdiscovery plugin. Add HLS support, adjust API schema, no SSL verify Add pvswf parameter to HDS stream parser Fix Video ID matching, add .no & .dk support, add error handling Match new URL, add HDS support, handle incorrect geolocation Add API support Fat Deer (1): Update pandatv.py Forrest Alvarez (3): Add some python releases Add coveralls to after_success Remove artifacts Guillaume Depardon (1): Now catching socket errors on send Javier Cantero (1): Add new parameter to Twitch usher URL Jeremy Symon (2): Sort list of streams by quality Avoid sorting streams twice Jon Bergli Heier (2): plugins.nrk: Updated for webpage changes. plugins.nrk: Fixed _id_re regex not matching series URLs. Kari Hänninen (7): Use client ID for twitch.tv API calls Revert "update INFO_URL for VaughnLive" Remove spurious print statement that made the plugin incompatible with python 3. livecoding.tv: fix breakage ("TypeError: cannot use a string pattern on a bytes-like object") sportschau: Fix breakage ("TypeError: a bytes-like object is required, not 'str'"). Also remove debug output. Update the plugin matrix Bump version to 1.14.0-rc1 Marcus Soll (2): Added plugin for blip.tv VOD Updated blip.tv plugin Mateusz Starzak (1): Update periscope.py Michael Copland (1): Fixed weighting of Twitch stream names Michael Hoang (1): Add OPENREC.tv plugin and chmod 2 files Michiel (1): Support for Tour de France stream Paul LaMendola (2): Maybe fixed ustream validation failure. More strict test for weird stream. Pavlos Touboulidis (2): Add antenna.gr plugin Update plugin matrix for antenna Robin Schroer (1): azubutv: set video_player to None if stream is offline Seth Creech (1): Added logic to support host mode Simon Bernier St-Pierre (5): update the streamup.com plugin support virtualenv update references to livestreamer add stream.me plugin add streamboat plugin Summon528 (1): add support to afreecatv.com.tw Swirt (2): Picarto plugin: update RTMPStream-settings Picarto plugin: update RTMPStream-settings Tang (1): New provider: live.bilibili.com Warnar Boekkooi (1): NPO token fix WeinerRinkler (2): First version Error fixed when streamer offline or invalid blxd (5): fixed tvcatchup.com plugin, the website layout changed and the method to find the stream URLs needed to be updated. tvcatchup now returns a variant playlist tvplayer.com only works with a browser user agent not all channels return hlsvariant playlists add user agent header to the tvcatchup plugin chvrn (4): added expressen plugin added expressen plugin update() => assign with subscript added entry for expressen e00E (1): Fix Twitch plugin not working because bandwith was parsed as an int when it is really a float fat deer (1): Add Panda.tv Plugin. fcicq (1): add afreecatv.jp support hannespetur (8): plugin for Ruv - the Icelandic national television - was added removed print statements and started to use quality key as audio if the url extensions is mp3 the plugin added to the plugin matrix removed unused import alphabetical order is hard removed redundant assignments of best/worst quality HLS support added for the Ruv plugin Ruv plugin: returning generators instead of a dict int3l (1): Refactoring and update for the VOD support intact (21): plugins.artetv: Update json regex Updated douyutv.com plugin Added plugin for streamup.com plugins.streamupcom: Check live status plugins.streamupcom: Update for API change plugins.streamupcom: Update for API change plugins.dailymotion: Add HLS streams support plugins.npo: Fix Python 3 compatibility plugins.livestream: Prefer standard SWF players plugins.tga: Support more streams plugins.tga: Fix offline streams plugins.vaughnlive: Fix INFO_URL Added plugin for vidio.com plugins.vaughnlive: Update for API change plugins.vaughnlive: Fix app for some ingest servers plugins.vaughnlive: Remove debug print plugins.vaughnlive: Lowercase channel name plugins.vaughnlive: Update for API change plugins.vaughnlive: Update for API change plugins.livestream: Tolerate missing swf player URL plugins.livestream: Fix player URL jkieberk (1): Change Fedora Package Manager from Yum to Dnf kviktor (2): plugins: mediaklikk.hu stream and video support update mediaklikk plugin livescope (1): Add VOD/replay support for periscope.tv liz1rgin (2): Fix goodgame find Streame Update goodgame.py maop (1): Add Beam.pro plugin. mindhalt (1): Update redirect URI after successful twitch auth neutric (1): Update issues.rst nitpicker (2): I doesn't sign the term of services, so I doesnt violate! update INFO_URL for VaughnLive oyvindln (1): Allow https urls for nrk.no. ph0o (1): Create servustv.py pulviscriptor (1): GoodGame URL parse fix scottbernstein (1): Latest fix to plugin from livestreamer steven7851 (16): plugins.douyutv: Use new api. update douyu fix cdn.. fix for Python 3.x.. use mobile api for reducing code fix for non number channel add middle and low quality fix quality fix room id regex make did by UUID module fix channel on event more retries for redirection remove useless lib try to support event page use https protocol Update plugin.douyutv trocknet (1): plugins.afreeca: Fix HLS stream. whizzoo (2): Add RTLXL plugin Add RTLXL plugin wolftankk (3): get azubu live status from api use new api get stream info fix video_player error ``` ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/docs/cli.rst0000644000175100001710000004155700000000000015472 0ustar00runnerdockerCommand-Line Interface ====================== Tutorial -------- Streamlink is a command-line application, which means that the commands described here should be typed into a terminal. On Windows, you have to open either the `Command Prompt`_, `PowerShell`_ or `Windows Terminal`_, on macOS open the `Terminal `_ app, and if you're on Linux or BSD you probably already know the drill. The way Streamlink works is that it's only a means to extract and transport the streams, and the playback is done by an external video player. Streamlink works best with `VLC`_ or `mpv`_, which are also cross-platform, but other players may be compatible too, see the :ref:`Players ` page for a complete overview. Now to get into actually using Streamlink, let's say you want to watch the stream located on twitch.tv/day9tv, you start off by telling Streamlink where to attempt to extract streams from. This is done by giving the URL to the command :command:`streamlink` as the first argument: .. code-block:: console $ streamlink twitch.tv/day9tv [cli][info] Found matching plugin twitch for URL twitch.tv/day9tv Available streams: audio, high, low, medium, mobile (worst), source (best) .. note:: You don't need to include the protocol when dealing with HTTP(s) URLs, e.g. just ``twitch.tv/day9tv`` is enough and quicker to type. This command will tell Streamlink to attempt to extract streams from the URL specified, and if it's successful, print out a list of available streams to choose from. In some cases (`Supported streaming protocols`_) local files are supported using the ``file://`` protocol, for example a local HLS playlist can be played. Relative file paths and absolute paths are supported. All path separators are ``/``, even on Windows. .. code-block:: console $ streamlink hls://file://C:/hls/playlist.m3u8 [cli][info] Found matching plugin stream for URL hls://file://C:/hls/playlist.m3u8 Available streams: 180p (worst), 272p, 408p, 554p, 818p, 1744p (best) To select a stream and start playback, simply add the stream name as a second argument to the :command:`streamlink` command: .. sourcecode:: console $ streamlink twitch.tv/day9tv 1080p60 [cli][info] Found matching plugin twitch for URL twitch.tv/day9tv [cli][info] Opening stream: 1080p60 (hls) [cli][info] Starting player: vlc The stream you chose should now be playing in the player. It's a common use case to just want to start the highest quality stream and not be bothered with what it's named. To do this, just specify ``best`` as the stream name and Streamlink will attempt to rank the streams and open the one of highest quality. You can also specify ``worst`` to get the lowest quality. Now that you have a basic grasp of how Streamlink works, you may want to look into customizing it to your own needs, such as: - Creating a :ref:`configuration file ` of options you want to use - Setting up your player to :ref:`cache some data ` before playing the stream to help avoiding buffering issues .. _Command Prompt: https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/windows-commands .. _PowerShell: https://docs.microsoft.com/en-us/powershell/ .. _Windows Terminal: https://docs.microsoft.com/en-us/windows/terminal/get-started .. _macOS Terminal: https://support.apple.com/guide/terminal/welcome/mac .. _VLC: https://videolan.org/ .. _mpv: https://mpv.io/ Configuration file ------------------ Writing the command-line options every time is inconvenient, that's why Streamlink is capable of reading options from a configuration file instead. Streamlink will look for config files in different locations depending on your platform: .. rst-class:: table-custom-layout table-custom-layout-platform-locations ================= ==================================================== Platform Location ================= ==================================================== Linux, BSD - ``${XDG_CONFIG_HOME:-${HOME}/.config}/streamlink/config`` Deprecated: - ``${HOME}/.streamlinkrc`` macOS - ``${HOME}/Library/Application Support/streamlink/config`` Deprecated: - ``${XDG_CONFIG_HOME:-${HOME}/.config}/streamlink/config`` - ``${HOME}/.streamlinkrc`` Windows - ``%APPDATA%\streamlink\config`` Deprecated: - ``%APPDATA%\streamlink\streamlinkrc`` ================= ==================================================== You can also specify the location yourself using the :option:`--config` option. .. warning:: On Windows, there is a default config created by the installer, but on any other platform you must create the file yourself. Syntax ^^^^^^ The config file is a simple text file and should contain one :ref:`command-line option ` (omitting the dashes) per line in the format:: option=value or for an option without value:: option .. note:: Any quotes used will be part of the value, so only use them when the value needs them, e.g. when specifying a player with a path which contains spaces. Example ^^^^^^^ .. code-block:: bash # Player options player=mpv --cache 2048 player-no-close .. note:: Full player paths are supported via configuration file options such as ``player="C:\mpv-x86_64\mpv"`` Plugin specific configuration file ---------------------------------- You may want to use specific options for some plugins only. This can be accomplished by placing those settings inside a plugin specific config file. Options inside these config files will override the main config file when a URL matching the plugin is used. Streamlink expects this config to be named like the main config but with ``.`` attached to the end. Examples ^^^^^^^^ .. rst-class:: table-custom-layout table-custom-layout-platform-locations ================= ==================================================== Platform Location ================= ==================================================== Linux, BSD - ``${XDG_CONFIG_HOME:-${HOME}/.config}/streamlink/config.pluginname`` Deprecated: - ``${HOME}/.streamlinkrc.pluginname`` macOS - ``${HOME}/Library/Application Support/streamlink/config.pluginname`` Deprecated: - ``${XDG_CONFIG_HOME:-${HOME}/.config}/streamlink/config.pluginname`` - ``${HOME}/.streamlinkrc.pluginname`` Windows - ``%APPDATA%\streamlink\config.pluginname`` Deprecated: - ``%APPDATA%\streamlink\streamlinkrc.pluginname`` ================= ==================================================== Have a look at the :ref:`list of plugins `, or check the :option:`--plugins` option to see the name of each built-in plugin. Sideloading plugins ------------------- Streamlink will attempt to load standalone plugins from these directories: .. rst-class:: table-custom-layout table-custom-layout-platform-locations ================= ==================================================== Platform Location ================= ==================================================== Linux, BSD - ``${XDG_DATA_HOME:-${HOME}/.local/share}/streamlink/plugins`` Deprecated: - ``${XDG_CONFIG_HOME:-${HOME}/.config}/streamlink/plugins`` macOS - ``${HOME}/Library/Application Support/streamlink/plugins`` Deprecated: - ``${XDG_CONFIG_HOME:-${HOME}/.config}/streamlink/plugins`` Windows - ``%APPDATA%\streamlink\plugins`` ================= ==================================================== .. note:: If a plugin is added with the same name as a built-in plugin, then the added plugin will take precedence. This is useful if you want to upgrade plugins independently of the Streamlink version. .. warning:: If one of the sideloaded plugins fails to load, eg. due to a ``SyntaxError`` being raised by the parser, this exception will not get caught by Streamlink and the execution will stop, even if the input stream URL does not match the faulty plugin. Plugin specific usage --------------------- Authenticating with Crunchyroll ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Crunchyroll requires authenticating with a premium account to access some of their content. To do so, the plugin provides a couple of options to input your information, :option:`--crunchyroll-username` and :option:`--crunchyroll-password`. You can login like this: .. sourcecode:: console $ streamlink --crunchyroll-username=xxxx --crunchyroll-password=xxx https://crunchyroll.com/a-crunchyroll-episode-link .. note:: If you omit the password, streamlink will ask for it. Once logged in, the plugin makes sure to save the session credentials to avoid asking your username and password again. Nevertheless, these credentials are valid for a limited amount of time, so it might be a good idea to save your username and password in your :ref:`configuration file ` anyway. .. warning:: The API this plugin uses isn't supposed to be available on desktop computers. The plugin tries to blend in as a valid device using custom headers and following the API's usual flow (e.g. reusing credentials), but this does not assure that your account will be safe from being spotted for unusual behavior. HTTP proxy with Crunchyroll ^^^^^^^^^^^^^^^^^^^^^^^^^^^ To be able to stream region locked content, you can use Streamlink's proxy options, which are described in the :ref:`Proxy Support ` section. When doing this, it's possible that access to the stream will still be denied; this can happen because the session and credentials used by the plugin were obtained while being logged from your own region, and the server still assumes you're in that region. For cases like this, the plugin provides the :option:`--crunchyroll-purge-credentials` option, which removes your saved session and credentials and tries to log in again using your username and password. Authenticating with FunimationNow ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Like Crunchyroll, the FunimationNow plugin requires authenticating with a premium account to access some content: :option:`--funimation-email`, :option:`--funimation-password`. In addition, this plugin requires the ``incap_ses`` cookie to be sent with each HTTP request (see issue #2088). This unique session cookie can be found in your browser and sent via the :option:`--http-cookie` option. .. sourcecode:: console $ streamlink --funimation-email='xxx' --funimation-password='xxx' --http-cookie 'incap_ses_xxx=xxxx=' https://funimation.com/shows/show/an-episode-link .. note:: There are multiple ways to retrieve the required cookie. For more information on browser cookies, please consult the following: - `What are cookies? `_ Playing built-in streaming protocols directly --------------------------------------------- There are many types of streaming protocols used by services today and Streamlink supports most of them. It's possible to tell Streamlink to access a streaming protocol directly instead of relying on a plugin to extract the streams from a URL for you. A streaming protocol can be accessed directly by specifying it in the ``protocol://URL`` format with an optional list of parameters, like so: .. code-block:: console $ streamlink "protocol://https://streamingserver/path key1=value1 key2=value2" Depending on the input URL, the explicit protocol scheme may be omitted. The following example shows HLS streams (``.m3u8``) and DASH streams (``.mdp``): .. code-block:: console $ streamlink "https://streamingserver/playlist.m3u8" $ streamlink "https://streamingserver/manifest.mpd" When passing parameters to the built-in streaming protocols, the values will either be treated as plain strings or they will be interpreted as Python literals: .. code-block:: console $ streamlink "httpstream://https://streamingserver/path method=POST params={'abc':123} json=['foo','bar','baz']" .. code-block:: python method="POST" params={"key": 123} json=["foo", "bar", "baz"] The parameters from the example above are used to make an HTTP ``POST`` request with ``abc=123`` added to the query string and ``["foo", "bar", "baz"]`` used as the content of the HTTP request's body (the serialized JSON data). Some parameters allow you to configure the behavior of the streaming protocol implementation directly: .. code-block:: console $ streamlink "hls://https://streamingserver/path start_offset=123 duration=321 force_restart=True" Supported streaming protocols ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ============================== ================================================= Name Prefix ============================== ================================================= Apple HTTP Live Streaming hls:// [1]_ MPEG-DASH [2]_ dash:// Progressive HTTP, HTTPS, etc httpstream:// [1]_ ============================== ================================================= .. [1] supports local files using the file:// protocol .. [2] Dynamic Adaptive Streaming over HTTP Proxy Support ------------- You can use the :option:`--http-proxy` option to change the proxy server that Streamlink will use for HTTP and HTTPS requests. :option:`--http-proxy` sets the proxy for all HTTP and HTTPS requests, including WebSocket connections. If separate proxies for each protocol are required, they can be set using environment variables - see `Requests Proxies Documentation`_ Both HTTP and SOCKS proxies are supported, as well as authentication in each of them. .. note:: When using a SOCKS proxy, the ``socks4`` and ``socks5`` schemes mean that DNS lookups are done locally, rather than on the proxy server. To have the proxy server perform the DNS lookups, the ``socks4a`` and ``socks5h`` schemes should be used instead. .. code-block:: console $ streamlink --http-proxy "http://address:port" $ streamlink --http-proxy "https://address:port" $ streamlink --http-proxy "socks4a://address:port" $ streamlink --http-proxy "socks5h://address:port" .. _Requests Proxies Documentation: https://2.python-requests.org/en/master/user/advanced/#proxies Metadata variables ------------------ Streamlink supports a number of metadata variables that can be used in the following CLI arguments: - :option:`--title` - :option:`--output` - :option:`--record` - :option:`--record-and-pipe` Metadata variables are surrounded by curly braces and can be escaped by doubling the curly brace characters, eg. ``{variable}`` and ``{{not-a-variable}}``. The availability of each variable depends on the used plugin and whether that plugin supports this kind of metadata. If a variable is unsupported or not available, then its substitution will either be a short placeholder text (:option:`--title`) or an empty string (:option:`--output`, :option:`--record`, :option:`--record-and-pipe`). The :option:`--json` argument always lists the standard plugin metadata: ``id``, ``author``, ``category`` and ``title``. .. rst-class:: table-custom-layout table-custom-layout-platform-locations ============================== ================================================= Variable Description ============================== ================================================= ``id`` The unique ID of the stream, eg. an internal numeric ID or randomized string. ``title`` The stream's title, usually a short descriptive text. ``author`` The stream's author, eg. a channel or broadcaster name. ``category`` The stream's category, eg. the name of a game being played, a music genre, etc. ``game`` Alias for ``category``. ``url`` The resolved URL of the stream. ``time`` The current timestamp. Can optionally be formatted via ``{time:format}``. The format parameter string is passed to Python's `datetime.strftime()`_ method, so all the usual time directives are available. The default format is ``%Y-%m-%d_%H-%M-%S``. ============================== ================================================= Examples: .. code-block:: console $ streamlink --title "{author} - {category} - {title}" [STREAM] $ streamlink --output "~/recordings/{author}/{category}/{id}-{time:%Y%m%d%H%M%S}.ts" [STREAM] .. _datetime.strftime(): https://docs.python.org/3/library/datetime.html#strftime-and-strptime-format-codes Command-line usage ------------------ .. code-block:: console $ streamlink [OPTIONS] [STREAM] .. argparse:: :module: streamlink_cli.main :attr: parser_helper ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/docs/conf.py0000644000175100001710000001454600000000000015466 0ustar00runnerdocker#!/usr/bin/env python3 import os import sys from streamlink import __version__ as streamlink_version # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath('.')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. needs_sphinx = '3.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.autosectionlabel', 'ext_argparse', 'ext_github', 'ext_releaseref', 'recommonmark' ] autosectionlabel_prefix_document = True # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = 'Streamlink' copyright = '2022, Streamlink' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = streamlink_version.split('+')[0] # The full version, including alpha/beta/rc tags. release = streamlink_version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build', '_applications.rst'] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. #pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] github_project = 'streamlink/streamlink' # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'furo' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. html_logo = "../icon.svg" # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] html_css_files = [ 'styles/custom.css', 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/fontawesome.min.css', 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/solid.min.css', 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/css/brands.min.css', ] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # Custom sidebar templates, maps document names to template names. html_sidebars = { '**': [ 'sidebar/scroll-start.html', 'sidebar/brand.html', 'sidebar/search.html', 'sidebar/navigation.html', 'sidebar/github-buttons.html', 'sidebar/scroll-end.html', ] } # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. html_domain_indices = False # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. html_show_sourcelink = False # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'streamlinkdoc' # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('_man', 'streamlink', 'extracts streams from various services and pipes them into a video player of choice', ['Streamlink Contributors'], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # If true, make a section directory on build man page. # Always set this to false to fix inconsistencies between recent sphinx releases man_make_section_directory = False ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/docs/deprecations.rst0000644000175100001710000001514200000000000017372 0ustar00runnerdockerDeprecations ============ streamlink 3.0.0 ---------------- Removal of separate https-proxy option ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ :ref:`HTTPS proxy CLI option ` and the respective :ref:`Session options ` have been deprecated in favor of a single :option:`--http-proxy` that sets the proxy for all HTTP and HTTPS requests, including WebSocket connections. streamlink 2.4.0 ---------------- Stream-type related CLI arguments ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ :ref:`Stream-type related CLI arguments ` and the respective :ref:`Session options ` have been deprecated in favor of existing generic arguments/options, to avoid redundancy and potential confusion. - use :option:`--stream-segment-attempts` instead of ``--{dash,hds,hls}-segment-attempts`` - use :option:`--stream-segment-threads` instead of ``--{dash,hds,hls}-segment-threads`` - use :option:`--stream-segment-timeout` instead of ``--{dash,hds,hls}-segment-timeout`` - use :option:`--stream-timeout` instead of ``--{dash,hds,hls,rtmp,http-stream}-timeout`` streamlink 2.3.0 ---------------- Plugin.can_handle_url() and Plugin.priority() ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ A new plugin URL matching API was introduced in 2.3.0 which will help Streamlink with static code analysis and an improved plugin loading mechanism in the future. Plugins now define their matching URLs and priorities declaratively. The old ``can_handle_url`` and ``priority`` classmethods have therefore been deprecated and will be removed in the future. When side-loading plugins which don't implement the new ``@pluginmatcher`` but implement the old classmethods, a deprecation message will be written to the info log output for the first plugin that gets resolved this way. **Deprecated plugin URL matching** .. code-block:: python import re from streamlink.plugin import Plugin from streamlink.plugin.plugin import HIGH_PRIORITY, NORMAL_PRIORITY class MyPlugin(Plugin): _re_url_one = re.compile( r"https?://pattern-(?Pone)" ) _re_url_two = re.compile(r""" https?://pattern-(?Ptwo) """, re.VERBOSE) @classmethod def can_handle_url(cls, url: str) -> bool: return cls._re_url_one.match(url) is not None \ or cls._re_url_two.match(url) is not None @classmethod def priority(cls, url: str) -> int: if cls._re_url_two.match(url) is not None: return HIGH_PRIORITY else: return NORMAL_PRIORITY def _get_streams(self): match_one = self._re_url_one.match(self.url) match_two = self._re_url_two.match(self.url) match = match_one or match_two param = match.group("param") if match_one: yield ... elif match_two: yield ... __plugin__ = MyPlugin **Migration** .. code-block:: python import re from streamlink.plugin import HIGH_PRIORITY, Plugin, pluginmatcher @pluginmatcher(re.compile( r"https?://pattern-(?Pone)" )) @pluginmatcher(priority=HIGH_PRIORITY, pattern=re.compile(r""" https?://pattern-(?Ptwo) """, re.VERBOSE)) class MyPlugin(Plugin): def _get_streams(self): param = self.match.group("param") if self.matches[0]: yield ... elif self.matches[1]: yield ... __plugin__ = MyPlugin .. note:: Plugins which have more sophisticated logic in their ``can_handle_url()`` classmethod need to be rewritten with multiple ``@pluginmatcher`` decorators and/or an improved ``_get_streams()`` method which returns ``None`` or raises a ``NoStreamsError`` when there are no streams to be found on that particular URL. streamlink 2.2.0 ---------------- Config file paths ^^^^^^^^^^^^^^^^^ Streamlink's default config file paths got updated and corrected on Linux/BSD, macOS and Windows. Old and deprecated paths will be dropped in the future. Only the first existing config file will be loaded. If a config file gets loaded from a deprecated path, a deprecation message will be written to the info log output. To resolve this, move the config file(s) to the correct location or copy the contents of the old file(s) to the new one(s). .. note:: Please note that this also affects all plugin config files, as they use the same path as the primary config file but with ``.pluginname`` appended to the file name, eg. ``config.twitch``. .. warning:: **On Windows**, when installing Streamlink via the Windows installer, a default config file gets created automatically due to technical reasons (bundled ffmpeg and rtmpdump dependencies). This means that the Windows installer will create a config file with the new name when upgrading from an earlier version to Streamlink 2.2.0+, and the old config file won't be loaded as a result of this. This is unfortunately a soft breaking change, as the Windows installer is not supposed to touch user config data and the users are required to update this by themselves. **Deprecated paths** .. rst-class:: table-custom-layout table-custom-layout-platform-locations ========= ======== Platform Location ========= ======== Linux/BSD - ``${HOME}/.streamlinkrc`` macOS - ``${XDG_CONFIG_HOME:-${HOME}/.config}/streamlink/config`` - ``${HOME}/.streamlinkrc`` Windows - ``%APPDATA%\streamlink\streamlinkrc`` ========= ======== **Migration** .. rst-class:: table-custom-layout table-custom-layout-platform-locations ========= ======== Platform Location ========= ======== Linux/BSD ``${XDG_CONFIG_HOME:-${HOME}/.config}/streamlink/config`` macOS ``${HOME}/Library/Application Support/streamlink/config`` Windows ``%APPDATA%\streamlink\config`` ========= ======== Custom plugins sideloading paths ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Streamlink's default custom plugins directory path got updated and corrected on Linux/BSD and macOS. Old and deprecated paths will be dropped in the future. **Deprecated paths** .. rst-class:: table-custom-layout table-custom-layout-platform-locations ========= ======== Platform Location ========= ======== Linux/BSD ``${XDG_CONFIG_HOME:-${HOME}/.config}/streamlink/plugins`` macOS ``${XDG_CONFIG_HOME:-${HOME}/.config}/streamlink/plugins`` ========= ======== **Migration** .. rst-class:: table-custom-layout table-custom-layout-platform-locations ========= ======== Platform Location ========= ======== Linux/BSD ``${XDG_DATA_HOME:-${HOME}/.local/share}/streamlink/plugins`` macOS ``${HOME}/Library/Application Support/streamlink/plugins`` ========= ======== ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/docs/developing.rst0000644000175100001710000001104400000000000017043 0ustar00runnerdockerDeveloping ========== Setup ----- Setting up the repository ^^^^^^^^^^^^^^^^^^^^^^^^^ In order to start working on Streamlink, you must first install the latest stable version of ``git``, optionally fork the repository on Github onto your account if you want to submit changes in a pull request, and then locally clone the repository. .. code-block:: bash mkdir streamlink cd streamlink git clone --origin=upstream 'https://github.com/streamlink/streamlink.git' . git remote add fork 'git@github.com:/streamlink.git' git remote -v git fetch --all When submitting a pull request, commit and push your changes onto a different branch. .. code-block:: bash git checkout master git pull upstream master git checkout -b new/feature/or/bugfix/branch git add ./foo git commit git push fork new/feature/or/bugfix/branch Setting up a new environment ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ While working on any kind of python-based project, it is usually best to do this in a virtual environment which is isolated from the Python environment of the host system. This ensures that development can be done in a clean space which is free of version conflicts and other unrelated packages. First, make sure that you have the latest stable versions of Python and pip installed. .. code-block:: bash python --version pip --version As the second preparation step, install the latest version of ``virtualenv``, either via your system's package manager or pip, and create a new virtual environment. These environments can be created separately for different Python versions. Please refer to the `virtualenv documentation`_ for all available parameters. There are also several wrappers around virtualenv available. .. code-block:: bash pip install --upgrade --user virtualenv virtualenv --version # replace ~/venvs/streamlink with your path of choice and give it a proper name virtualenv --download --verbose ~/venvs/streamlink Now activate the virtual environment by sourcing the activation shell script. .. code-block:: bash source ~/venvs/streamlink/bin/activate # non-POSIX shells have their own activation script, eg. FISH source ~/venvs/streamlink/bin/activate.fish # on Windows, activation scripts are located in the Scripts/ subdirectory instead of bin/ source ~/venvs/streamlink/Scripts/activate .. _virtualenv documentation: https://virtualenv.pypa.io/en/latest/ Installing Streamlink ^^^^^^^^^^^^^^^^^^^^^ After activating the new virtual environment, Streamlink's build dependencies and Streamlink itself need to be installed. Regular development dependencies and documentation related dependencies are listed in the text files shown below and need to be installed separately. .. code-block:: bash # install additional dependencies pip install -r dev-requirements.txt pip install -r docs-requirements.txt # install Streamlink from source # check setup.py for optional dependencies and install those manually if you need to pip install -e . # validate that Streamlink is working which streamlink streamlink --version Validating changes ------------------ Before submitting a pull request, run tests, perform code linting and build the documentation on your system first, to see if your changes contain any mistakes or errors. This will be done automatically for each pull request on each change, but performing these checks locally avoids unnecessary build failures. .. code-block:: bash # run automated tests python -m pytest -ra # or just run a subset of all tests python -m pytest -ra path/to/test-file.py::TestClassName::test_method_name ... # check code for linting errors flake8 # build the documentation make --directory=docs clean html $BROWSER ./docs/_build/html/index.html Plugins ------- Adding plugins ^^^^^^^^^^^^^^ 1. Implement the plugin in ``src/streamlink/plugins/``, similar to already existing plugins. Check the git log for recently added or modified plugins to help you get an overview of what's needed to properly implement a plugin. A complete guide is currently not available. 2. Add at least tests for the URL regex matching in ``tests/plugins/``. Once again, check other plugin tests from the git log. Removing plugins ^^^^^^^^^^^^^^^^ 1. Remove the plugin file in ``src/streamlink/plugins/`` and the test file in ``tests/plugins/`` 2. Remove the plugin entry from the documentation in ``docs/plugin_matrix.rst`` 3. Run ``script/update-removed-plugins.sh`` once to update ``src/streamlink/plugins/.removed`` ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/docs/docutils.conf0000644000175100001710000000005300000000000016650 0ustar00runnerdocker[restructuredtext parser] smart_quotes=no ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/docs/donate.rst0000644000175100001710000000406700000000000016170 0ustar00runnerdockerDonating -------- Thank you for considering a donation to the Streamlink team! Streamlink is an organization composed of a geographically diverse group of individuals. Donations/tips can be sent in a variety of ways. If you would like to donate directly to a specific user, please review their details in the team section below. If you would prefer to donate to the team as a whole, please use our `Open Collective `_. Note that donations are not tax deductible, as Streamlink does not operate as a charitable organization. Your donation to the team or a team member may be used in any way and does not come with expectations of work to be provided or as payment for future work. .. raw:: html

Individual team member donations

Bastimeyer ^^^^^^^^^^ .. container:: clearfix .. image:: https://github.com/bastimeyer.png?size=150 :class: github-avatar .. container:: - `Github `__ - `Github Sponsors `__ - `Paypal `__ - `Bitcoin `__ :code:`1EZg8eBz4RdPb8pEzYD9JEzr9Fyitzj8j8` Beardypig ^^^^^^^^^ .. container:: clearfix .. image:: https://github.com/beardypig.png?size=150 :class: github-avatar .. container:: - `Github `__ - `Bitcoin `__ :code:`1Ey3KZ9SwTj6QyASE6vgJVebUiJsW1HuRh` Gravyboat ^^^^^^^^^ .. container:: clearfix .. image:: https://github.com/gravyboat.png?size=150 :class: github-avatar .. container:: - `Github `__ - `Bitcoin `__ :code:`1PpdFh9LkTsjtG2AAcrWqk6RiFrC2iTCxj` - `Flattr `__ - `Gratipay `__ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/docs/ext_argparse.py0000644000175100001710000001233700000000000017221 0ustar00runnerdocker"""Convert a argparse parser to option directives. Inspired by sphinxcontrib.autoprogram but with a few differences: - Contains some simple pre-processing on the help messages to make the Sphinx version a bit prettier. """ import argparse import re from textwrap import dedent from docutils import nodes from docutils.parsers.rst import Directive from docutils.parsers.rst.directives import unchanged from docutils.statemachine import ViewList from sphinx.util.nodes import nested_parse_with_titles _block_re = re.compile(r":\n{2}\s{2}") _default_re = re.compile(r"Default is (.+)\.\n") _note_re = re.compile(r"Note: (.*)(?:\n\n|\n*$)", re.DOTALL) _option_line_re = re.compile(r"^(?!\s{2}|Example: )(.+)$", re.MULTILINE) _option_re = re.compile(r"(?:^|(?<=\s))(--\w[\w-]*\w)\b") _prog_re = re.compile(r"%\(prog\)s") _percent_re = re.compile(r"%%") def get_parser(module_name, attr): module = __import__(module_name, globals(), locals(), [attr]) parser = getattr(module, attr) return parser if not(callable(parser)) else parser.__call__() def indent(value, length=4): space = " " * length return "\n".join(space + line for line in value.splitlines()) class ArgparseDirective(Directive): has_content = True option_spec = { "module": unchanged, "attr": unchanged, } _headlines = ["^", "~"] def process_help(self, help): # Dedent the help to make sure we are always dealing with # non-indented text. help = dedent(help) # Replace option references with links. # Do this before indenting blocks and notes. help = _option_line_re.sub( lambda m: ( _option_re.sub( lambda m2: ( ":option:`{0}`".format(m2.group(1)) if m2.group(1) in self._available_options else m2.group(0) ), m.group(1) ) ), help ) # Create simple blocks. help = _block_re.sub("::\n\n ", help) # Boldify the default value. help = _default_re.sub(r"Default is: **\1**.\n", help) # Create note directives from "Note: " paragraphs. help = _note_re.sub( lambda m: ".. note::\n\n" + indent(m.group(1)) + "\n\n", help ) # workaround to replace %(prog)s with streamlink help = _prog_re.sub("streamlink", help) # fix escaped chars for percent-formatted argparse help strings help = _percent_re.sub("%", help) return indent(help) def generate_group_rst(self, group): for action in group._group_actions: # don't document suppressed parameters if action.help == argparse.SUPPRESS: continue metavar = action.metavar if isinstance(metavar, tuple): metavar = " ".join(metavar) options = [] # parameter(s) with metavar if action.option_strings and metavar: for arg in action.option_strings: # optional parameter value if action.nargs == "?": metavar = f"[{metavar}]" options.append(f"{arg} {metavar}") # positional parameter elif metavar: options.append(metavar) # parameter(s) without metavar else: options += action.option_strings directive = ".. option:: " options = f"\n{' ' * len(directive)}".join(options) yield f"{directive}{options}" yield "" for line in self.process_help(action.help).split("\n"): yield line yield "" if hasattr(action, "plugins") and len(action.plugins) > 0: yield f" **Supported plugins:** {', '.join(action.plugins)}" yield "" def generate_parser_rst(self, parser, depth=0): if depth >= len(self._headlines): return for group in parser._action_groups: # Exclude empty groups if not group._group_actions and not group._action_groups: continue title = group.title yield "" yield title yield self._headlines[depth] * len(title) yield from self.generate_group_rst(group) if group._action_groups: yield "" yield from self.generate_parser_rst(group, depth + 1) def run(self): module = self.options.get("module") attr = self.options.get("attr") parser = get_parser(module, attr) self._available_options = [] for action in parser._actions: # positional parameters have an empty option_strings list self._available_options += action.option_strings or [action.dest] node = nodes.section() node.document = self.state.document result = ViewList() for line in self.generate_parser_rst(parser): result.append(line, "argparse") nested_parse_with_titles(self.state, result, node) return node.children def setup(app): app.add_directive("argparse", ArgparseDirective) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/docs/ext_github.py0000644000175100001710000000401300000000000016667 0ustar00runnerdocker"""Creates Github links from @user and #issue text. Bascially a much simplified version of sphinxcontrib.issuetracker with support for @user. """ import re from docutils import nodes from docutils.transforms import Transform GITHUB_ISSUE_URL = "https://github.com/{0}/issues/{1}" GITHUB_USER_URL = "https://github.com/{1}" class GithubReferences(Transform): default_priority = 999 def apply(self): config = self.document.settings.env.config issue_re = re.compile(config.github_issue_pattern) mention_re = re.compile(config.github_mention_pattern) self._replace_pattern(issue_re, GITHUB_ISSUE_URL) self._replace_pattern(mention_re, GITHUB_USER_URL) def _replace_pattern(self, pattern, url_format): project = self.document.settings.env.config.github_project for node in self.document.traverse(nodes.Text): parent = node.parent if isinstance(parent, (nodes.literal, nodes.FixedTextElement)): continue text = str(node) new_nodes = [] last_ref_end = 0 for match in pattern.finditer(text): head = text[last_ref_end:match.start()] if head: new_nodes.append(nodes.Text(head)) last_ref_end = match.end() ref = url_format.format(project, match.group(1)) link = nodes.reference( match.group(0), match.group(0), refuri=ref ) new_nodes.append(link) if not new_nodes: continue tail = text[last_ref_end:] if tail: new_nodes.append(nodes.Text(tail)) parent.replace(node, new_nodes) def setup(app): app.add_config_value("github_project", None, "env") app.add_config_value("github_issue_pattern", r"#(\d+)", "env") app.add_config_value("github_mention_pattern", r"@(\w+)", "env") app.add_transform(GithubReferences) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/docs/ext_releaseref.py0000644000175100001710000000133000000000000017521 0ustar00runnerdocker"""Creates links that allows substituting the current release within the title or target.""" import os.path from docutils import nodes from sphinx.util.nodes import split_explicit_title def releaseref_role(name, rawtext, text, lineno, inliner, options={}, content=[]): config = inliner.document.settings.env.config text = text.replace("|version|", config.version) text = text.replace("|release|", config.release) has_explicit_title, title, target = split_explicit_title(text) if not has_explicit_title: title = os.path.basename(target) node = nodes.reference(rawtext, title, refuri=target, **options) return [node], [] def setup(app): app.add_role("releaseref", releaseref_role) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/docs/index.rst0000644000175100001710000000475100000000000016025 0ustar00runnerdockerStreamlink ========== Overview -------- Streamlink is a :ref:`command-line utility ` which pipes video streams from various services into a video player, such as `VLC`_. The main purpose of Streamlink is to avoid resource-heavy and unoptimized websites, while still allowing the user to enjoy various streamed content. There is also an :ref:`API ` available for developers who want access to the stream data. This project was forked from Livestreamer, which is no longer maintained. Latest release (|version|) https://github.com/streamlink/streamlink/releases/latest GitHub https://github.com/streamlink/streamlink Issue tracker https://github.com/streamlink/streamlink/issues PyPI https://pypi.org/project/streamlink/ Free software Simplified BSD license Features -------- Streamlink is built upon a plugin system which allows support for new services to be easily added. Most of the big streaming services are supported, such as: - `Twitch.tv `_ - `YouTube.com `_ - `Livestream.com `_ - `Dailymotion.com `_ ... and many more. A full list of plugins currently included can be found on the :ref:`Plugins ` page. Quickstart ---------- The default behavior of Streamlink is to play back streams in the `VLC `_ player. .. sourcecode:: console $ streamlink twitch.tv/day9tv best [cli][info] Found matching plugin twitch for URL twitch.tv/day9tv [cli][info] Available streams: audio_only, 160p (worst), 360p, 480p, 720p, 720p60, 1080p60 (best) [cli][info] Opening stream: 1080p60 (hls) [cli][info] Starting player: vlc For more in-depth usage and install instructions, please refer to the `User guide`_. User guide ---------- Streamlink is made up of two parts, a :ref:`cli ` and a library :ref:`API `. See their respective sections for more information on how to use them. Thank you --------- - `Github `_, for hosting the git repo, docs, release assets and providing CI tools - `Netlify `_, for hosting docs preview builds Table of contents ----------------- .. toctree:: :maxdepth: 2 Overview install cli plugin_matrix players issues deprecations developing api_guide api changelog donate applications thirdparty ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/docs/install.rst0000644000175100001710000004356600000000000016373 0ustar00runnerdocker.. |br| raw:: html
Installation ============ Windows ------- .. rst-class:: table-custom-layout ==================================== =========================================== Method Installing ==================================== =========================================== Installers See the `Windows binaries`_ section below Portable See the `Windows portable version`_ section below Python pip See the `PyPI package and source code`_ section below `Chocolatey`_ .. code-block:: bat choco install streamlink `Installing Chocolatey packages`_ `Windows Package Manager`_ .. code-block:: bat winget install streamlink `Installing Winget packages`_ ==================================== =========================================== .. _Chocolatey: https://chocolatey.org/packages/streamlink .. _Windows Package Manager: https://github.com/microsoft/winget-pkgs/tree/master/manifests/s/Streamlink/Streamlink .. _Installing Chocolatey packages: https://chocolatey.org .. _Installing Winget packages: https://docs.microsoft.com/en-us/windows/package-manager/ macOS ----- .. rst-class:: table-custom-layout ==================================== =========================================== Method Installing ==================================== =========================================== Python pip See the `PyPI package and source code`_ section below `Homebrew`_ .. code-block:: bash brew install streamlink `Installing Homebrew packages`_ ==================================== =========================================== .. _Homebrew: https://github.com/Homebrew/homebrew-core/blob/master/Formula/streamlink.rb .. _Installing Homebrew packages: https://brew.sh Linux and BSD ------------- .. rst-class:: table-custom-layout ==================================== =========================================== Distribution Installing ==================================== =========================================== AppImage See the `AppImages`_ section below Python pip See the `PyPI package and source code`_ section below `Arch Linux`_ .. code-block:: bash sudo pacman -S streamlink `Arch Linux (aur, git)`_ .. code-block:: bash git clone https://aur.archlinux.org/streamlink-git.git cd streamlink-git makepkg -si `Installing AUR packages`_ `Debian (sid, testing)`_ .. code-block:: bash sudo apt update sudo apt install streamlink `Debian (stable)`_ .. code-block:: bash # If you don't have Debian backports already (see link below): echo "deb http://deb.debian.org/debian buster-backports main" | sudo tee "/etc/apt/sources.list.d/streamlink.list" sudo apt update sudo apt -t buster-backports install streamlink `Installing Debian backported packages`_ `Fedora`_ .. code-block:: bash sudo dnf install streamlink `Gentoo Linux`_ .. code-block:: bash sudo emerge net-misc/streamlink `NetBSD (pkgsrc)`_ .. code-block:: bash cd /usr/pkgsrc/multimedia/streamlink sudo make install clean `NixOS`_ .. code-block:: bash nix-env -iA nixos.streamlink `NixOS channel`_ `OpenBSD`_ .. code-block:: bash doas pkg_add streamlink `Solus`_ .. code-block:: bash sudo eopkg install streamlink `Ubuntu`_ .. code-block:: bash sudo add-apt-repository ppa:nilarimogard/webupd8 sudo apt update sudo apt install streamlink `Void`_ .. code-block:: bash sudo xbps-install streamlink ==================================== =========================================== Please see the `PyPI package and source code`_ or `AppImages`_ sections down below if a package is not available for your distro or platform, or if it's out of date. .. _Arch Linux: https://www.archlinux.org/packages/community/any/streamlink/ .. _Arch Linux (aur, git): https://aur.archlinux.org/packages/streamlink-git/ .. _Debian (sid, testing): https://packages.debian.org/unstable/streamlink .. _Debian (stable): https://packages.debian.org/unstable/streamlink .. _Fedora: https://src.fedoraproject.org/rpms/python-streamlink .. _Gentoo Linux: https://packages.gentoo.org/package/net-misc/streamlink .. _NetBSD (pkgsrc): https://pkgsrc.se/multimedia/streamlink .. _NixOS: https://github.com/NixOS/nixpkgs/tree/master/pkgs/applications/video/streamlink .. _OpenBSD: https://openports.se/multimedia/streamlink .. _Solus: https://dev.getsol.us/source/streamlink/ .. _Ubuntu: https://launchpad.net/~nilarimogard/+archive/ubuntu/webupd8/+packages?field.name_filter=streamlink&field.status_filter=published&field.series_filter= .. _Void: https://github.com/void-linux/void-packages/tree/master/srcpkgs/streamlink .. _Installing AUR packages: https://wiki.archlinux.org/index.php/Arch_User_Repository#Installing_packages .. _Installing Debian backported packages: https://wiki.debian.org/Backports#Using_the_command_line .. _NixOS channel: https://search.nixos.org/packages?show=streamlink&query=streamlink Package maintainers ------------------- .. rst-class:: table-custom-layout ==================================== =========================================== Distribution/Platform Maintainer ==================================== =========================================== Arch Giancarlo Razzolini Arch (aur, git) Josip Ponjavic Chocolatey Scott Walters Debian Alexis Murzeau Fedora Mohamed El Morabity Gentoo soredake NetBSD Maya Rashish NixOS Tuomas Tynkkynen OpenBSD Brian Callahan Solus Joey Riches Ubuntu Alin Andrei Void Michal Vasilek Windows binaries beardypig Windows port. version beardypig ==================================== =========================================== PyPI package and source code ---------------------------- If a package is not available on your platform, or if it's out of date, Streamlink can be installed via `pip`_, the Python package manager. Before running :command:`pip`, make sure that it's the Python 3 version of `pip`_ (to check, run :command:`pip --version`). On some systems, this isn't the case by default and an alternative, like :command:`pip3` for example, needs to be run instead. .. note:: On some Linux distributions, the Python headers package needs to be installed before installing Streamlink (``python-devel`` on RedHat, Fedora, etc.). Ensure that you are using an up-to-date version of `pip`_. At least version **6** is required. .. warning:: On Linux, when not using a virtual environment, it is recommended to **install custom python packages like this only for the current user** (see the ``--user`` parameter below), since system-wide packages can cause conflicts with the system's regular package manager. Those user-packages will be installed into ``~/.local`` instead of ``/usr`` and entry-scripts for running the programs can be found in ``~/.local/bin``, eg. ``~/.local/bin/streamlink``. In order for the command line shell to be able to find these executables, the user's ``PATH`` environment variable needs to be extended. This can be done by adding ``export PATH="${HOME}/.local/bin:${PATH}"`` to ``~/.profile`` or ``~/.bashrc``. .. rst-class:: table-custom-layout ==================================== =========================================== Version Installing ==================================== =========================================== `Latest release`_ .. code-block:: bash pip install --user --upgrade streamlink `Master branch`_ .. code-block:: bash pip install --user --upgrade git+https://github.com/streamlink/streamlink.git `Specific tag/branch or commit`_ .. code-block:: bash pip install --user --upgrade git+https://github.com/USERNAME/streamlink.git@BRANCH-OR-COMMIT ==================================== =========================================== .. _pip: https://pip.pypa.io/en/stable/ .. _Latest release: https://pypi.python.org/pypi/streamlink .. _Master branch: https://github.com/streamlink/streamlink/commits/master .. _Specific tag/branch or commit: https://pip.pypa.io/en/stable/reference/pip_install/#git Virtual environment ^^^^^^^^^^^^^^^^^^^ Another method of installing Streamlink in a non-system-wide way is using `virtualenv`_, which creates a user owned Python environment instead. .. code-block:: bash # Create a new environment virtualenv ~/myenv # Activate the environment source ~/myenv/bin/activate # Install Streamlink in the environment pip install --upgrade streamlink # Use Streamlink in the environment streamlink ... # Deactivate the environment deactivate # Use Streamlink without activating the environment ~/myenv/bin/streamlink ... .. note:: This may also be required on some macOS versions that seem to have weird permission issues. .. _virtualenv: https://virtualenv.readthedocs.io/en/latest/ Dependencies ^^^^^^^^^^^^ To install Streamlink from source you will need these dependencies. .. rst-class:: table-custom-layout ==================================== =========================================== Name Notes ==================================== =========================================== `Python`_ At least version **3.6**. `python-setuptools`_ At least version **42.0.0**. **Automatically installed by the setup script** -------------------------------------------------------------------------------- `isodate`_ Used for parsing ISO8601 strings `lxml`_ Used for processing HTML and XML data `pycountry`_ Used for localization settings, provides country and language data `pycryptodome`_ Used for decrypting encrypted streams `PySocks`_ Used for SOCKS Proxies `requests`_ Used for making any kind of HTTP/HTTPS request `websocket-client`_ Used for making websocket connections **Optional** -------------------------------------------------------------------------------- `ffmpeg`_ Required for `muxing`_ multiple video/audio/subtitle streams into a single output stream. - DASH streams with video and audio content always have to get remuxed. - HLS streams optionally need to get remuxed depending on the stream selection. ==================================== =========================================== .. _Python: https://www.python.org/ .. _python-setuptools: https://setuptools.pypa.io/en/latest/ .. _isodate: https://pypi.org/project/isodate/ .. _lxml: https://lxml.de/ .. _pycountry: https://pypi.org/project/pycountry/ .. _pycryptodome: https://pycryptodome.readthedocs.io/en/latest/ .. _PySocks: https://github.com/Anorov/PySocks .. _requests: https://docs.python-requests.org/en/master/ .. _websocket-client: https://pypi.org/project/websocket-client/ .. _ffmpeg: https://www.ffmpeg.org/ .. _muxing: https://en.wikipedia.org/wiki/Multiplexing#Video_processing Windows binaries ---------------- .. rst-class:: table-custom-layout ==================================== ==================================== Release Notes ==================================== ==================================== `Stable release`_ Download the installer from the `GitHub releases page`_. `Development build`_ For testing purposes only! Built each day at midnight (UTC). |br| Download the zipped installer from the `build artifacts`_ section of one of the recent scheduled builds. Build artifacts are stored by Github for 90 days. |br| See the `commit log`_ for a list of changes since the last stable release. ==================================== ==================================== .. warning:: **The Streamlink installer for Windows is currently based on Python 3.9.** |br| Versions of Windows prior to 10 are **not** supported. Be aware that the packages for `Chocolatey`_ and the `Windows Package Manager`_ are just wrappers around the stable installer and thus depend on Windows 10+ as well. Alternatively, :ref:`Streamlink can be installed via python-pip ` in a :ref:`compatible Python environment `. .. _Stable release: .. _GitHub releases page: https://github.com/streamlink/streamlink/releases/latest .. _Development build: .. _build artifacts: https://github.com/streamlink/streamlink/actions?query=event%3Aschedule+is%3Asuccess+branch%3Amaster .. _commit log: https://github.com/streamlink/streamlink/commits/master These installers contain: - A compiled version of Streamlink that **does not require an existing Python installation** - `ffmpeg`_ for muxing streams and perform the following tasks: - Add Streamlink to the system's list of installed applications. |br| An uninstaller will automatically be created during installation. - Add Streamlink's installation directory to the system's ``PATH`` environment variable. |br| This allows the user to run the ``streamlink`` command globally from the command prompt or powershell without specifying its directory. To build the installer on your own, ``NSIS`` and ``pynsist`` need to be installed. Windows portable version ^^^^^^^^^^^^^^^^^^^^^^^^ .. rst-class:: table-custom-layout ==================================== =========================================== Maintainer Links ==================================== =========================================== Beardypig `Latest precompiled stable release`__ |br| `Latest builder`__ |br| `More info`__ ==================================== =========================================== __ https://github.com/beardypig/streamlink-portable/releases/latest __ https://github.com/beardypig/streamlink-portable/archive/master.zip __ https://github.com/beardypig/streamlink-portable AppImages --------- Download & Setup ^^^^^^^^^^^^^^^^ First, download the latest `Streamlink AppImage`_ which matches your system's architecture from the `Streamlink AppImage releases page`_. Then simply set the executable flag and run the app. .. code-block:: bash # Set the executable flag. Note that all AppImage release file names include # the release version, Python version, platform name and CPU architecture chmod +x streamlink-2.0.0-1-cp39-cp39-manylinux2014_x86_64.AppImage # Run the Streamlink AppImage with any parameter supported by Streamlink ./streamlink-2.0.0-1-cp39-cp39-manylinux2014_x86_64.AppImage --version What are AppImages? ^^^^^^^^^^^^^^^^^^^ AppImages are portable apps for Linux which are independent of the distro and package management. Note: Check out `AppImageLauncher`_, which automates the setup and system integration of AppImages. AppImageLauncher may also be available via your distro's package management. Additional information, like for example how to inspect the AppImage contents or how to extract the contents if `FUSE`_ is not available on your system, can be found in the `AppImage documentation`_. .. _Streamlink AppImage: https://github.com/streamlink/streamlink-appimage .. _Streamlink AppImage releases page: https://github.com/streamlink/streamlink-appimage/releases .. _AppImageLauncher: https://github.com/TheAssassin/AppImageLauncher .. _FUSE: https://docs.appimage.org/user-guide/troubleshooting/fuse.html .. _AppImage documentation: https://docs.appimage.org/user-guide/run-appimages.html ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/docs/issues.rst0000644000175100001710000000316000000000000016222 0ustar00runnerdockerCommon issues ============= Streams are buffering/lagging ----------------------------- Enable caching in your player ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ By default most players do not cache the data they receive from Streamlink. Caching can reduce the amount of buffering you run into because the player will have some breathing room between receiving the data and playing it. ============= ============================== ====================================== Player Parameter Note ============= ============================== ====================================== MPC-HC -- Currently no way of configuring the cache MPlayer ``-cache `` Between 1024 and 8192 is recommended mpv ``--cache=yes Between 1024 and 8192 is recommended --demuxer-max-bytes=`` VLC ``--file-caching Between 1000 and 10000 is recommended --network-caching `` ============= ============================== ====================================== Use the :option:`--player-args` or :option:`--player` option to pass these options to your player. Multi-threaded streaming ^^^^^^^^^^^^^^^^^^^^^^^^ On segmented streaming protocols (such as HLS and DASH) it's possible to use multiple threads for downloading multiple segments at the same time to potentially increase the throughput. This can be done via Streamlink's :option:`--stream-segment-threads` argument. .. note:: Using 2 or 3 threads should be enough to see an impact on live streams, any more will likely not show much effect. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/docs/players.rst0000644000175100001710000001132700000000000016372 0ustar00runnerdockerPlayers ======= Transport modes --------------- There are three different modes of transporting the stream to the player. ====================== ========================================================= Name Description ====================== ========================================================= Standard input pipe This is the default behaviour when there are no other options specified. Named pipe (FIFO) Use the :option:`--player-fifo` option to enable. HTTP Use the :option:`--player-http` or :option:`--player-continuous-http` options to enable. ====================== ========================================================= Player compatibility -------------------- This is a list of video players and their compatibility with the transport modes. ===================================================== ========== ========== ==== Name Stdin Pipe Named Pipe HTTP ===================================================== ========== ========== ==== `Daum Pot Player`_ Yes No Yes [1]_ `MPC-HC`_ Yes [2]_ No Yes [1]_ `MPlayer`_ Yes Yes Yes `mpv`_ Yes Yes Yes `OMXPlayer`_ No Yes Yes [4]_ `QuickTime`_ No No No `VLC media player`_ Yes [3]_ Yes Yes ===================================================== ========== ========== ==== .. [1] :option:`--player-continuous-http` must be used. Using HTTP with players that rely on Windows' codecs to access HTTP streams may have a long startup time since Windows tend to do multiple HTTP requests and Streamlink will attempt to open the stream for each request. .. [2] Stdin requires MPC-HC 1.7 or newer. .. [3] Some versions of VLC might be unable to use the stdin pipe and prints the error message VLC is unable to open the MRL 'fd://0' Use one of the other transport methods instead to work around this. .. [4] :option:`--player-continuous-http` has been reported to work for HLS streams when also using the timeout option for omxplayer (see `When using OMXPlayer the stream stops unexpectedly`_.) Other stream types may not work as expected, it is recommended that :option:`--player-fifo` be used. .. _Daum Pot Player: https://potplayer.daum.net .. _MPC-HC: https://mpc-hc.org/ .. _MPlayer: https://mplayerhq.hu .. _mpv: https://mpv.io .. _OMXPlayer: https://www.raspberrypi.org/documentation/raspbian/applications/omxplayer.md .. _QuickTime: https://apple.com/quicktime .. _VLC media player: https://videolan.org Known issues and workarounds ---------------------------- MPC-HC reports "File not found" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Upgrading to version 1.7 or newer will solve this issue since reading data from standard input is not supported in version 1.6.x of MPC-HC. MPC-HC only plays sound on Twitch streams ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Twitch sometimes returns badly muxed streams which may confuse players. The following workaround was contributed by MPC-HC developer @kasper93: *To fix this problem go to options -> internal filters -> open splitter settings and increase "Stream Analysis Duration" this will let ffmpeg to properly detect all streams.* Using :option:`--player-passthrough hls <--player-passthrough>` has also been reported to work. MPlayer tries to play Twitch streams at the wrong FPS ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This is a bug in MPlayer, using the MPlayer fork `mpv`_ instead is recommended. Youtube Live does not work with VLC ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ VLC versions below 3 cannot play Youtube Live streams. Please update your player. You can also try using a different player. Youtube Live does not work with Mplayer ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Some versions of Mplayer cannot play Youtube Live streams. And errors like: .. code-block:: console Cannot seek backward in linear streams! Seek failed Switching to a recent fork such as mpv resolves the issue. When using OMXPlayer the stream stops unexpectedly ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ When reading from a fifo pipe OMXPlayer will quit when there is no data, to fix this you can supply the timeout option to OMXPlayer using :option:`--player "omxplayer --timeout 20" <--player>`. For live streams it might be beneficial to also add the omxplayer parameter ``--live``. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/docs/plugin_matrix.rst0000644000175100001710000003436200000000000017601 0ustar00runnerdockerPlugins ======= This is a list of the currently built in plugins and what URLs and features they support. Streamlink's primary focus is live streams, so VOD support is limited. ======================= ==================== ===== ===== =========================== Name URL(s) Live VOD Notes ======================= ==================== ===== ===== =========================== abematv abema.tv Yes Yes Streams are geo-restricted to Japan. adultswim adultswim.com Yes Yes Streams may be geo-restricted, some VOD streams are protected by DRM. afreeca play.afreecatv.com Yes No albavision - antena7.com.do Yes No Some streams are geo-restricted. - atv.pe - c9n.com.py - canal10.com.ni - canal12.com.sv - chapintv.com - elnueve.com.ar - redbolivis... [4]_ - repretel.com - rts.com.ec - snt.com.py - tvc.com.ec - vtv.com.hn app17 17app.co Yes -- ard_live daserste.de Yes Yes Streams may be geo-restricted to Germany. ard_mediathek - ardmediathek.de Yes Yes Streams may be geo-restricted to Germany. - mediathek... [5]_ artetv arte.tv Yes Yes atresplayer atresplayer.com Yes No Streams are geo-restricted to Spain. bbciplayer bbc.co.uk/iplayer Yes Yes Streams may be geo-restricted to the United Kingdom. bfmtv - bfmtv.com Yes Yes - 01net.com bigo - live.bigo.tv Yes -- - bigoweb.co bilibili live.bilibili.com Yes ? bloomberg bloomberg.com Yes Yes booyah booyah.live Yes Yes brightcove players.brig... [6]_ Yes Yes btv btvplus.bg Yes No Streams are geo-restricted to Bulgaria. cbsnews cbsnews.com Yes No cdnbg - armymedia.bg Yes No Streams may be geo-restricted to Bulgaria - bgonair.bg - bloombergtv.bg - bnt.bg - live.bstv.bg - i.cdn.bg - nova.bg - mu-vi.tv ceskatelevize ceskatelevize.cz Yes No Streams may be geo-restricted to Czechia. cinergroup - showtv.com.tr Yes No - haberturk.com - showmax.com.tr - showturk.com.tr - bloomberght.com clubbingtv clubbingtv.com Yes Yes Requires a login. cnews cnews.fr Yes Yes crunchyroll crunchyroll.com -- Yes dailymotion dailymotion.com Yes Yes delfi - delfi.lt -- Yes - delfi.ee - delfi.lv deutschewelle dw.com Yes Yes dlive dlive.tv Yes Yes dogan - cnnturk.com Yes Yes - dreamturk.com.tr - dreamtv.com.tr - kanald.com.tr - teve2.com.tr dogus - eurostartv.com.tr Yes No - kralmuzik.com.tr - ntv.com.tr - startv.com.tr drdk dr.dk Yes No Streams may be geo-restricted to Denmark. earthcam earthcam.com Yes Yes Only works for the cams hosted on EarthCam. egame egame.qq.com Yes No eltrecetv eltrecetv.com.ar Yes Yes Streams may be geo-restricted to Argentina. euronews euronews.com Yes No facebook facebook.com Yes Yes filmon filmon.com Yes Yes Only SD quality streams. foxtr fox.com.tr Yes No funimationnow - funimation.com -- Yes :ref:`Requires session cookies ` - funimationnow.uk galatasaraytv galatasaray.com Yes No garena garena.live Yes -- goltelevision goltelevision.com Yes No Streams may be geo-restricted to Spain. goodgame goodgame.ru Yes No Only HLS streams are available. googledrive - docs.google.com -- Yes - drive.google.com gulli replay.gulli.fr Yes Yes Streams may be geo-restricted to France. huajiao huajiao.com Yes No huya huya.com Yes No idf1 idf1.fr Yes Yes invintus player.invintus.com Yes Yes kugou fanxing.kugou.com Yes -- linelive live.line.me Yes Yes livestream livestream.com Yes -- lrt lrt.lt Yes No ltv_lsm_lv ltv.lsm.lv Yes No Streams may be geo-restricted to Latvia. mediaklikk - mediaklikk.hu Yes No Streams may be geo-restricted to Hungary. - m4sport.hu mediavitrina mediavitrina.ru Yes No Streams may be geo-restricted to Russia. mildom mildom.com Yes Yes mitele mitele.es Yes No Streams may be geo-restricted to Spain. mjunoon mjunoon.tv Yes Yes Streams may be geo-restricted to Pakistan. mrtmk play.mrt.com.mk Yes Yes Streams may be geo-restricted to North Macedonia. n13tv 13tv.co.il Yes Yes Streams may be geo-restricted to Israel. nbc nbc.com No Yes Streams are geo-restricted to USA. Authentication is not supported. nbcnews nbcnews.com Yes No nbcsports nbcsports.com No Yes Streams maybe be geo-restricted to USA. Authentication is not supported. nhkworld nhk.or.jp/nhkworld Yes No nicolive live.nicovideo.jp Yes Yes Timeshift is supported. Some content may require login. nimotv nimo.tv Yes No nos nos.nl Yes Yes Streams may be geo-restricted to Netherlands. nownews news.now.com Yes No nrk - tv.nrk.no Yes Yes Streams may be geo-restricted to Norway. - radio.nrk.no ntv ntv.ru Yes No okru ok.ru Yes Yes olympicchannel - olympicchannel.com Yes Yes Only non-premium content is available. - olympics.com oneplusone 1plus1.video Yes No onetv 1tv.ru Yes No Streams may be geo-restricted to Russia. openrectv openrec.tv Yes Yes orf_tvthek tvthek.orf.at Yes Yes pandalive pandalive.co.kr Yes No picarto picarto.tv Yes Yes piczel piczel.tv Yes No pixiv sketch.pixiv.net Yes -- pluto pluto.tv Yes Yes pluzz - france.tv Yes Yes Streams may be geo-restricted to France, Andorra and Monaco. - francetvinfo.fr qq live.qq.com Yes No radiko radiko.jp Yes Yes Streams are geo-restricted to Japan. radionet - radio.net Yes -- - radio.at - radio.de - radio.dk - radio.es - radio.fr - radio.it - radio.pl - radio.pt - radio.se raiplay raiplay.it Yes No Most streams are geo-restricted to Italy. reuters - reuters.com Yes Yes - reuters.tv rotana rotana.net Yes -- Streams are geo-restricted to Saudi Arabia. rtbf - rtbf.be/auvio Yes Yes Streams may be geo-restricted to Belgium or Europe. - rtbfradioplayer.be rtpplay rtp.pt/play Yes Yes Streams may be geo-restricted to Portugal. rtve rtve.es Yes Yes Streams may be geo-restricted to Spain. rtvs rtvs.sk Yes No Streams may be geo-restricted to Slovakia. ruv ruv.is Yes Yes Streams may be geo-restricted to Iceland. sbscokr play.sbs.co.kr Yes No Streams may be geo-restricted to South Korea. schoolism schoolism.com -- Yes Requires a login and a subscription. senategov senate.gov -- Yes Supports hearing streams. showroom showroom-live.com Yes No sportal sportal.bg Yes No sportschau sportschau.de Yes No ssh101 ssh101.com Yes No stadium watchstadium.com Yes Yes steam steamcommunity.com Yes No Some streams will require a Steam account. streamable streamable.com - Yes streann ott.streann.com Yes Yes stv player.stv.tv Yes No Streams are geo-restricted to Great Britain. svtplay - svtplay.se Yes Yes Streams may be geo-restricted to Sweden. - oppetarkiv.se swisstxt - srf.ch Yes No Streams are geo-restricted to Switzerland. - rsi.ch teamliquid - teamliquid.net Yes -- - tl.net telefe telefe.com No Yes Streams are geo-restricted to Argentina. tf1 - tf1.fr Yes No Streams may be geo-restricted to France. - lci.fr theplatform player.thepl... [7]_ No Yes tlctr tlctv.com.tr Yes No turkuvaz - atv.com.tr Yes No Streams may be geo-restricted. - a2tv.com.tr - ahaber.com.tr - anews.com.tr - aspor.com.tr - atvavrupa.tv - minikacocuk.com.tr - minikago.com.tr - sabah.com.tr tv3cat ccma.cat Yes Yes Streams may be geo-restricted to Spain. tv4play - tv4play.se Yes Yes Streams may be geo-restricted to Sweden. Only non-premium streams currently supported. - fotbollskanalen.se tv5monde - tv5monde.com Yes Yes Streams may be geo-restricted to France, Belgium or Switzerland - tivi5mondeplus.com tv8 tv8.com.tr Yes No tv360 tv360.com.tr Yes No tv999 tv999.bg Yes -- Streams are geo-restricted to Bulgaria tvibo player.tvibo.com Yes -- tviplayer tviplayer.iol.pt Yes Yes tvp tvpstream.vod.tvp.pl Yes No Streams may be geo-restricted to Poland. tvrby tvr.by Yes No Streams may be geo-restricted to Belarus. tvrplus tvrplus.ro Yes No Streams may be geo-restricted to Romania. tvtoya tvtoya.pl Yes -- twitcasting twitcasting.tv Yes No twitch twitch.tv Yes Yes ustreamtv - ustream.tv Yes Yes - video.ibm.com ustvnow ustvnow.com Yes -- All streams require an account, some streams require a subscription. vidio vidio.com Yes Yes vimeo vimeo.com Yes Yes Password-protected videos are not supported. vinhlongtv thvli.vn Yes No Streams are geo-restricted to Vietnam vk vk.com Yes Yes vlive vlive.tv Yes No Embedded Naver VODs are not supported. vrtbe vrt.be/vrtnu Yes Yes vtvgo vtvgo.vn Yes No wasd wasd.tv Yes No webtv web.tv Yes -- welt welt.de Yes Yes Streams may be geo-restricted to Germany. wwenetwork network.wwe.com Yes Yes Requires an account to access any content. youtube - youtube.com Yes Yes Protected videos are not supported. - youtu.be yupptv yupptv.com Yes Yes Some streams require an account and subscription. zattoo zattoo.com Yes Yes Other sub-providers are also available. zdf_mediathek zdf.de Yes Yes Streams may be geo-restricted to Germany. zeenews zeenews.india.com Yes No zengatv zengatv.com Yes No zhanqi zhanqi.tv Yes No ======================= ==================== ===== ===== =========================== .. [4] redbolivision.tv.bo .. [5] mediathek.daserste.de .. [6] players.brightcove.net .. [7] player.theplatform.com ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/docs/thirdparty.rst0000644000175100001710000000655200000000000017111 0ustar00runnerdocker.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! If you're a developer and want to add your project/application to this list, please 1. adhere to the same structure and format of the entries of the applications.rst file and this one 2. add your new entry to the bottom of the list 3. at least provide the required fields (in the same order) - "Description" (a brief text describing your application) - "Type" (eg. Graphical User Interface, CLI wrapper, etc.) - "OS" (please use the available substitutions) - "Author" (if possible, include a link to the creator's Github/Gitlab profile, etc. or a contact email address) - "Website" 4. use an image - in the jpeg or png format - with a static and reliable !!!https!!! URL (use Github or an image hoster like Imgur, etc.) - with a reasonable size and aspect ratio - with a decent compression quality - that is not too large (at most 1 MiB allowed, the smaller the better) - that is neutral and only shows your application 5. optionally add more fields like a URL to the source code repository, a larger info or features text, etc. Please be aware that the Streamlink team may edit and remove your entry at any time. Thank you! :) !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. include:: _applications.rst .. |br| raw:: html
Third Party Applications ======================== .. content list start LiveProxy --------- :Description: Local proxy server between Streamlink and an URL :OS: |Windows| |MacOS| |Linux| :Author: `back-to `_ :Website: https://liveproxy.github.io :Source code: https://github.com/back-to/liveproxy :Info: LiveProxy allows Streamlink to be easy accessible from **m3u** playlists, |br| it is also available for **Kodi Leia** and **Enigma2** devices. |br| A detailed guide can be found on the website. Wtwitch ------- .. image:: https://user-images.githubusercontent.com/25400030/97115072-6951e600-16ec-11eb-88f7-51939c0d0bc1.jpg :align: center :alt: Wtwitch screenshot of checking subscriptions :Description: Browse Twitch streams; subscribe to streamers locally without an account :Type: TUI (terminal user interface) :OS: |Linux| |MacOS| :Author: `Hunter Peavey `_ :Website: https://git.sr.ht/~krathalan/wtwitch :Info: wtwitch is a Bash program that lets you browse the top games on Twitch, the |br| top streamers for a given game, and lets you check the status of streamers you |br| subscribe to. Open streams easily with settings that save your preferred quality |br| and player. A man page comes with the AUR package or can be compiled manually. OBS-Streamlink -------------- :Description: OBS source plugin for embedding streams using Streamlink :Type: OBS Plugin :OS: |Windows| :Author: `DD Center `_ :Website: https://github.com/dd-center/obs-streamlink :Info: OBS-Streamlink is a plugin for OBS (Open Broadcaster Software) which allows embedding streams directly as a scene source. This is useful for broadcasters of multi-platform live streams and for live commentators. .. content list end Help us extend this list by sending us a pull request on Github. Thanks! ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/docs-requirements.txt0000644000175100001710000000006100000000000017434 0ustar00runnerdockersphinx>=3.0 furo==2021.09.08 recommonmark>=0.5.0 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643136177.0248404 streamlink-3.1.1/examples/0000755000175100001710000000000000000000000015043 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/examples/opencv-face.py0000755000175100001710000000425500000000000017614 0ustar00runnerdocker#!/usr/bin/env python3 import logging import sys import streamlink import os.path try: import cv2 except ImportError: sys.stderr.write("This example requires opencv-python is installed") raise log = logging.getLogger(__name__) GREEN = (0, 255, 0) def stream_to_url(url, quality='best'): streams = streamlink.streams(url) if streams: return streams[quality].to_url() else: raise ValueError("No steams were available") def detect_faces(cascade, frame, scale_factor=1.1, min_neighbors=5): frame_copy = frame.copy() frame_gray = cv2.cvtColor(frame_copy, cv2.COLOR_BGR2GRAY) faces = cascade.detectMultiScale(frame_gray, scaleFactor=scale_factor, minNeighbors=min_neighbors) for (x, y, w, h) in faces: cv2.rectangle(frame_copy, (x, y), (x + w, y + h), GREEN, 1) return frame_copy def main(url, quality='best', fps=30.0): face_cascade = cv2.CascadeClassifier(os.path.join(cv2.haarcascades, 'haarcascade_frontalface_default.xml')) stream_url = stream_to_url(url, quality) log.info("Loading stream {0}".format(stream_url)) cap = cv2.VideoCapture(stream_url) frame_time = int((1.0 / fps) * 1000.0) while True: try: ret, frame = cap.read() if ret: frame_f = detect_faces(face_cascade, frame, scale_factor=1.2) cv2.imshow('frame', frame_f) if cv2.waitKey(frame_time) & 0xFF == ord('q'): break else: break except KeyboardInterrupt: break cv2.destroyAllWindows() cap.release() if __name__ == "__main__": import argparse logging.basicConfig(level=logging.INFO) parser = argparse.ArgumentParser(description="Face detection on streams via Streamlink") parser.add_argument("url", help="Stream to play") parser.add_argument("--stream-quality", help="Requested stream quality [default=best]", default="best", dest="quality") parser.add_argument("--fps", help="Play back FPS for opencv [default=30]", default=30.0, type=float) opts = parser.parse_args() main(opts.url, opts.quality, opts.fps) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643136177.0728405 streamlink-3.1.1/setup.cfg0000644000175100001710000000505500000000000015053 0ustar00runnerdocker[metadata] name = streamlink author = Streamlink author_email = streamlink@protonmail.com description = Streamlink is a command-line utility that extracts streams from various services and pipes them into a video player of choice. long_description = file: README.md long_description_content_type = text/markdown url = https://github.com/streamlink/streamlink project_urls = Documentation = https://streamlink.github.io/ Tracker = https://github.com/streamlink/streamlink/issues Source = https://github.com/streamlink/streamlink Funding = https://opencollective.com/streamlink license = Simplified BSD license_files = LICENSE classifiers = Development Status :: 5 - Production/Stable License :: OSI Approved :: BSD License Environment :: Console Intended Audience :: End Users/Desktop Operating System :: POSIX Operating System :: MacOS Operating System :: Microsoft :: Windows Programming Language :: Python :: 3 Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Topic :: Internet :: WWW/HTTP Topic :: Multimedia :: Sound/Audio Topic :: Multimedia :: Video Topic :: Utilities [options] python_requires = >=3.6, <4 package_dir = =src packages = find: install_requires = isodate lxml >=4.6.4,<5.0 pycountry pycryptodome >=3.4.3,<4 PySocks !=1.5.7,>=1.5.6 requests >=2.26.0,<3.0 websocket-client >=1.2.1,<2.0 [options.packages.find] where = src [options.package_data] streamlink.plugins = .removed [versioneer] VCS = git style = pep440 versionfile_source = src/streamlink/_version.py versionfile_build = streamlink/_version.py tag_prefix = parentdir_prefix = streamlink- [flake8] ignore = W503, exclude = __pycache__/, .git/, build/, dist/, docs/, examples/, env/, script/ venv/, versioneer.py, win32/, per-file-ignores = src/streamlink/__init__.py:E402,F401,I100,I101,I201,I202,I666, src/streamlink/packages/*:I100,I101,I201,I202,I666, src/streamlink/packages/**/*:I100,I101,I201,I202,I666, src/streamlink/plugin/api/useragents.py:E501, src/streamlink/plugins/__init__.py:F401, src/streamlink/stream/__init__.py:F401, src/streamlink_cli/utils/named_pipe.py:F401, tests/mock.py:F401,F403, max-line-length = 128 show-source = True statistics = True builtins = basestring, file, raw_input, unicode, xrange, import-order-style = pycharm application-import-names = streamlink, streamlink_cli, tests, versioneer, [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/setup.py0000644000175100001710000000532600000000000014745 0ustar00runnerdocker#!/usr/bin/env python from os import path from sys import argv, exit, version_info from textwrap import dedent from setuptools import setup import versioneer def format_msg(text, *args, **kwargs): return dedent(text).strip(" \n").format(*args, **kwargs) CURRENT_PYTHON = version_info[:2] REQUIRED_PYTHON = (3, 6) # This check and everything above must remain compatible with older Python versions if CURRENT_PYTHON < REQUIRED_PYTHON: exit(format_msg(""" ======================================================== Unsupported Python version ======================================================== This version of Streamlink requires at least Python {}.{}, but you're trying to install it on Python {}.{}. This may be because you are using a version of pip that doesn't understand the python_requires classifier. Make sure you have pip >= 9.0 and setuptools >= 24.2 """, *(REQUIRED_PYTHON + CURRENT_PYTHON))) # Explicitly disable running tests via setuptools if "test" in argv: exit(format_msg(""" Running `python setup.py test` has been deprecated since setuptools 41.5.0. Streamlink requires pytest for collecting and running tests, via one of these commands: `pytest` or `python -m pytest` (see the pytest docs for more infos about this) """)) def is_wheel_for_windows(): if "bdist_wheel" in argv: names = ["win32", "win-amd64", "cygwin"] length = len(argv) for pos in range(argv.index("bdist_wheel") + 1, length): if argv[pos] == "--plat-name" and pos + 1 < length: return argv[pos + 1] in names elif argv[pos][:12] == "--plat-name=": return argv[pos][12:] in names return False entry_points = { "console_scripts": ["streamlink=streamlink_cli.main:main"] } if is_wheel_for_windows(): entry_points["gui_scripts"] = ["streamlinkw=streamlink_cli.main:main"] # optional data files data_files = [ # shell completions # requires pre-built completion files via shtab (dev-requirements.txt) # `./script/build-shell-completions.sh` ("share/bash-completion/completions", ["completions/bash/streamlink"]), ("share/zsh/site-functions", ["completions/zsh/_streamlink"]), # man page # requires pre-built man page file via sphinx (docs-requirements.txt) # `make --directory=docs clean man` ("share/man/man1", ["docs/_build/man/streamlink.1"]) ] data_files = [ (destdir, [file for file in srcfiles if path.exists(file)]) for destdir, srcfiles in data_files ] setup( version=versioneer.get_version(), cmdclass=versioneer.get_cmdclass(), entry_points=entry_points, data_files=data_files, ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643136177.0208402 streamlink-3.1.1/src/0000755000175100001710000000000000000000000014014 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643136177.0728405 streamlink-3.1.1/src/streamlink/0000755000175100001710000000000000000000000016165 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/__init__.py0000644000175100001710000000147400000000000020304 0ustar00runnerdocker"""Streamlink extracts streams from various services. The main compontent of Streamlink is a command-line utility that launches the streams in a video player. An API is also provided that allows direct access to stream data. Full documentation is available at https://streamlink.github.io. """ from streamlink._version import get_versions __version__ = get_versions()['version'] del get_versions __title__ = "streamlink" __license__ = "Simplified BSD" __author__ = "Streamlink" __copyright__ = "Copyright 2022 Streamlink" __credits__ = ["https://github.com/streamlink/streamlink/blob/master/AUTHORS"] from streamlink.api import streams from streamlink.exceptions import (StreamlinkError, PluginError, NoStreamsError, NoPluginError, StreamError) from streamlink.session import Streamlink ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/__main__.py0000644000175100001710000000011700000000000020256 0ustar00runnerdockerif __name__ == "__main__": from streamlink_cli.main import main main() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643136177.0728405 streamlink-3.1.1/src/streamlink/_version.py0000644000175100001710000000076100000000000020367 0ustar00runnerdocker # This file was generated by 'versioneer.py' (0.18) from # revision-control system data, or from the parent directory name of an # unpacked source archive. Distribution tarballs contain a pre-generated copy # of this file. import json version_json = ''' { "date": "2022-01-25T09:25:00-0800", "dirty": false, "error": null, "full-revisionid": "711418045debdc37fb9ea6f42fc737872b8eb342", "version": "3.1.1" } ''' # END VERSION_JSON def get_versions(): return json.loads(version_json) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/api.py0000644000175100001710000000050700000000000017312 0ustar00runnerdockerfrom streamlink.session import Streamlink def streams(url, **params): """Attempts to find a plugin and extract streams from the *url*. *params* are passed to :func:`Plugin.streams`. Raises :exc:`NoPluginError` if no plugin is found. """ session = Streamlink() return session.streams(url, **params) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/buffers.py0000644000175100001710000000741100000000000020176 0ustar00runnerdockerfrom collections import deque from io import BytesIO from threading import Event, Lock class Chunk(BytesIO): """A single chunk, part of the buffer.""" def __init__(self, buf): self.length = len(buf) BytesIO.__init__(self, buf) @property def empty(self): return self.tell() == self.length class Buffer: """Simple buffer for use in single-threaded consumer/filler. Stores chunks in a deque to avoid inefficient reallocating of large buffers. """ def __init__(self): self.chunks = deque() self.current_chunk = None self.closed = False self.length = 0 def _iterate_chunks(self, size): bytes_left = size while bytes_left: try: current_chunk = self.current_chunk or Chunk(self.chunks.popleft()) except IndexError: break data = current_chunk.read(bytes_left) bytes_left -= len(data) if current_chunk.empty: self.current_chunk = None else: self.current_chunk = current_chunk yield data def write(self, data): if not self.closed: data = bytes(data) # Copy so that original buffer may be reused self.chunks.append(data) self.length += len(data) def read(self, size=-1): if size < 0 or size > self.length: size = self.length if not size: return b"" data = b"".join(self._iterate_chunks(size)) self.length -= len(data) return data def close(self): self.closed = True class RingBuffer(Buffer): """Circular buffer for use in multi-threaded consumer/filler.""" def __init__(self, size=8192 * 4): Buffer.__init__(self) self.buffer_size = size self.buffer_lock = Lock() self.event_free = Event() self.event_free.set() self.event_used = Event() def _check_events(self): if self.length > 0: self.event_used.set() else: self.event_used.clear() if self.is_full: self.event_free.clear() else: self.event_free.set() def _read(self, size=-1): with self.buffer_lock: data = Buffer.read(self, size) self._check_events() return data def read(self, size=-1, block=True, timeout=None): if block and not self.closed: if not self.event_used.wait(timeout) and self.length == 0: raise OSError("Read timeout") return self._read(size) def write(self, data): if self.closed: return data_left = len(data) data_total = len(data) while data_left > 0: self.event_free.wait() if self.closed: return with self.buffer_lock: write_len = min(self.free, data_left) written = data_total - data_left Buffer.write(self, data[written:written + write_len]) data_left -= write_len self._check_events() def resize(self, size): with self.buffer_lock: self.buffer_size = size self._check_events() def wait_free(self, timeout=None): self.event_free.wait(timeout) def wait_used(self, timeout=None): self.event_used.wait(timeout) def close(self): Buffer.close(self) # Make sure we don't let a .write() and .read() block forever self.event_free.set() self.event_used.set() @property def free(self): return max(self.buffer_size - self.length, 0) @property def is_full(self): return self.free == 0 __all__ = ["Buffer", "RingBuffer"] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/cache.py0000644000175100001710000000563300000000000017611 0ustar00runnerdockerimport json import os import shutil import tempfile from time import mktime, time from streamlink.compat import is_win32 if is_win32: xdg_cache = os.environ.get("APPDATA", os.path.expanduser("~")) else: xdg_cache = os.environ.get("XDG_CACHE_HOME", os.path.expanduser("~/.cache")) cache_dir = os.path.join(xdg_cache, "streamlink") class Cache: """Caches Python values as JSON and prunes expired entries.""" def __init__(self, filename, key_prefix=""): self.key_prefix = key_prefix self.filename = os.path.join(cache_dir, filename) self._cache = {} def _load(self): if os.path.exists(self.filename): try: with open(self.filename, "r") as fd: self._cache = json.load(fd) except Exception: self._cache = {} else: self._cache = {} def _prune(self): now = time() pruned = [] for key, value in self._cache.items(): expires = value.get("expires", now) if expires <= now: pruned.append(key) for key in pruned: self._cache.pop(key, None) return len(pruned) > 0 def _save(self): fd, tempname = tempfile.mkstemp() fd = os.fdopen(fd, "w") json.dump(self._cache, fd, indent=2, separators=(",", ": ")) fd.close() # Silently ignore errors try: if not os.path.exists(os.path.dirname(self.filename)): os.makedirs(os.path.dirname(self.filename)) shutil.move(tempname, self.filename) except OSError: os.remove(tempname) def set(self, key, value, expires=60 * 60 * 24 * 7, expires_at=None): self._load() self._prune() if self.key_prefix: key = "{0}:{1}".format(self.key_prefix, key) if expires_at is None: expires += time() else: try: expires = mktime(expires_at.timetuple()) except OverflowError: expires = 0 self._cache[key] = dict(value=value, expires=expires) self._save() def get(self, key, default=None): self._load() if self._prune(): self._save() if self.key_prefix: key = "{0}:{1}".format(self.key_prefix, key) if key in self._cache and "value" in self._cache[key]: return self._cache[key]["value"] else: return default def get_all(self): ret = {} self._load() if self._prune(): self._save() for key, value in self._cache.items(): if self.key_prefix: prefix = self.key_prefix + ":" else: prefix = "" if key.startswith(prefix): okey = key[len(prefix):] ret[okey] = value["value"] return ret __all__ = ["Cache"] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/compat.py0000644000175100001710000000041400000000000020021 0ustar00runnerdockerimport os is_win32 = os.name == "nt" # win/nix compatible devnull try: from subprocess import DEVNULL def devnull(): return DEVNULL except ImportError: def devnull(): return open(os.path.devnull, 'w') __all__ = ["is_win32", "devnull"] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/exceptions.py0000644000175100001710000000164000000000000020721 0ustar00runnerdockerclass StreamlinkError(Exception): """Any error caused by Streamlink will be caught with this exception.""" class PluginError(StreamlinkError): """Plugin related error.""" class FatalPluginError(PluginError): """ Plugin related error that cannot be recovered from Plugin's should use this Exception when errors that can never be recovered from are encountered. For example, when a user's input is required an none can be given. """ class NoStreamsError(StreamlinkError): def __init__(self, url): self.url = url err = "No streams found on this URL: {0}".format(url) Exception.__init__(self, err) class NoPluginError(PluginError): """No relevant plugin has been loaded.""" class StreamError(StreamlinkError): """Stream related error.""" __all__ = ["StreamlinkError", "PluginError", "NoPluginError", "NoStreamsError", "StreamError"] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/logger.py0000644000175100001710000000743000000000000020022 0ustar00runnerdockerimport logging import sys from datetime import datetime from logging import CRITICAL, DEBUG, ERROR, INFO, WARNING from pathlib import Path from threading import Lock from typing import IO, List, Optional, Union FORMAT_STYLE = "{" FORMAT_BASE = "[{name}][{levelname}] {message}" FORMAT_DATE = "%H:%M:%S" REMOVE_BASE = ["streamlink", "streamlink_cli"] # Make NONE ("none") the highest possible level that suppresses all log messages: # `logging.NOTSET` (equal to 0) can't be used as the "none" level because of `logging.Logger.getEffectiveLevel()`, which # loops through the logger instance's ancestor chain and checks whether the instance's level is NOTSET. If it is NOTSET, # then it continues with the parent logger, which means that if the level of `streamlink.logger.root` was set to "none" and # its value NOTSET, then it would continue with `logging.root` whose default level is `logging.WARNING` (equal to 30). NONE = sys.maxsize # Add "trace" to Streamlink's log levels TRACE = 5 # Define Streamlink's log levels (and register both lowercase and uppercase names) _levelToNames = { NONE: "none", CRITICAL: "critical", ERROR: "error", WARNING: "warning", INFO: "info", DEBUG: "debug", TRACE: "trace", } for _level, _name in _levelToNames.items(): logging.addLevelName(_level, _name.upper()) logging.addLevelName(_level, _name) _config_lock = Lock() class StreamlinkLogger(logging.getLoggerClass()): def trace(self, message, *args, **kws): if self.isEnabledFor(TRACE): self._log(TRACE, message, args, **kws) class StringFormatter(logging.Formatter): def __init__(self, fmt, datefmt=None, style="%", remove_base=None): if style not in ("{", "%"): raise ValueError("Only {} and % formatting styles are supported") super().__init__(fmt, datefmt=datefmt, style=style) self.style = style self.fmt = fmt self.remove_base = remove_base or [] self._usesTime = (style == "%" and "%(asctime)" in fmt) or (style == "{" and "{asctime}" in fmt) def usesTime(self): return self._usesTime def formatTime(self, record, datefmt=None): tdt = datetime.fromtimestamp(record.created) return tdt.strftime(datefmt or self.default_time_format) def formatMessage(self, record): if self.style == "{": return self.fmt.format(**record.__dict__) else: return self.fmt % record.__dict__ def format(self, record): for rbase in self.remove_base: record.name = record.name.replace(rbase + ".", "") record.levelname = record.levelname.lower() return super().format(record) # noinspection PyShadowingBuiltins,PyPep8Naming def basicConfig( filename: Optional[Union[str, Path]] = None, filemode: str = "a", stream: Optional[IO] = None, level: Optional[str] = None, format: str = FORMAT_BASE, style: str = FORMAT_STYLE, datefmt: str = FORMAT_DATE, remove_base: Optional[List[str]] = None ) -> Union[logging.FileHandler, logging.StreamHandler]: with _config_lock: if filename is not None: handler = logging.FileHandler(filename, filemode) else: handler = logging.StreamHandler(stream) formatter = StringFormatter( format, datefmt, style=style, remove_base=remove_base or REMOVE_BASE ) handler.setFormatter(formatter) root.addHandler(handler) if level is not None: root.setLevel(level) return handler logging.setLoggerClass(StreamlinkLogger) root = logging.getLogger("streamlink") root.setLevel(WARNING) levels = list(_levelToNames.values()) __all__ = [ "NONE", "TRACE", "StreamlinkLogger", "basicConfig", "root", "levels", ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/options.py0000644000175100001710000001253600000000000020241 0ustar00runnerdockerfrom collections import OrderedDict def _normalise_option_name(name): return name.replace('-', '_') def _normalise_argument_name(name): return name.replace('_', '-').strip("-") class Options: """ For storing options to be used by plugins, with default values. Note: Option names are normalised by replacing "-" with "_", this means that the keys ``example-one`` and ``example_one`` are equivalent. """ def __init__(self, defaults=None): if not defaults: defaults = {} self.defaults = self._normalise_dict(defaults) self.options = self.defaults.copy() @classmethod def _normalise_dict(cls, src): dest = {} for key, value in src.items(): dest[_normalise_option_name(key)] = value return dest def set(self, key, value): self.options[_normalise_option_name(key)] = value def get(self, key): key = _normalise_option_name(key) if key in self.options: return self.options[key] def update(self, options): for key, value in options.items(): self.set(key, value) class Argument: """ :class:`Argument` accepts most of the same parameters as :func:`ArgumentParser.add_argument`, except requires is a special case as in this case it is only enforced if the plugin is in use. In addition the name parameter is the name relative to the plugin eg. username, password, etc. """ def __init__(self, name, required=False, requires=None, prompt=None, sensitive=False, argument_name=None, dest=None, is_global=False, **options): """ :param name: name of the argument, without -- or plugin name prefixes, eg. ``"password"``, ``"mux-subtitles"``, etc. :param required (bool): if the argument is required for the plugin :param requires: list of the arguments which this argument requires, eg ``["password"]`` :param prompt: if the argument is required and not given, this prompt will show at run time :param sensitive (bool): if the argument is sensitive (passwords, etc) and should be masked in logs and if prompted use askpass :param argument_name: :param option_name: :param options: arguments passed to :func:`ArgumentParser.add_argument`, excluding requires, and dest """ self.required = required self.name = name self.options = options self._argument_name = argument_name # override the cli argument name self._dest = dest # override for the plugin option name self.requires = requires and (list(requires) if isinstance(requires, (list, tuple)) else [requires]) or [] self.prompt = prompt self.sensitive = sensitive self._default = options.get("default") self.is_global = is_global def _name(self, plugin): return self._argument_name or _normalise_argument_name("{0}-{1}".format(plugin, self.name)) def argument_name(self, plugin): return f"--{self.name if self.is_global else self._name(plugin)}" def namespace_dest(self, plugin): return _normalise_option_name(self._name(plugin)) @property def dest(self): return self._dest or _normalise_option_name(self.name) @property def default(self): # read-only return self._default class Arguments: """ Provides a wrapper around a list of :class:`Argument`. For example .. code-block:: python class PluginExample(Plugin): arguments = PluginArguments( PluginArgument("username", help="The username for your account.", metavar="EMAIL", requires=["password"]), // requires the password too PluginArgument("password", sensitive=True, // should be masked in logs, etc. help="The password for your account.", metavar="PASSWORD") ) This will add the ``--plugin-username`` and ``--plugin-password`` arguments to the CLI (assuming the plugin module is ``plugin``). """ def __init__(self, *args): self.arguments = OrderedDict((arg.name, arg) for arg in args) def __iter__(self): return iter(self.arguments.values()) def get(self, name): return self.arguments.get(name) def requires(self, name): """ Find all the arguments required by name :param name: name of the argument the find the dependencies :return: list of dependant arguments """ results = {name} argument = self.get(name) for reqname in argument.requires: required = self.get(reqname) if not required: raise KeyError("{0} is not a valid argument for this plugin".format(reqname)) if required.name in results: raise RuntimeError("cycle detected in plugin argument config") results.add(required.name) yield required for r in self.requires(required.name): if r.name in results: raise RuntimeError("cycle detected in plugin argument config") results.add(r.name) yield r __all__ = ["Options", "Arguments", "Argument"] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643136177.0288403 streamlink-3.1.1/src/streamlink/packages/0000755000175100001710000000000000000000000017743 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/packages/__init__.py0000644000175100001710000000000000000000000022042 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/packages/requests_file.py0000644000175100001710000001406700000000000023177 0ustar00runnerdocker""" Copyright 2015 Red Hat, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. """ from io import BytesIO import sys from requests.adapters import BaseAdapter from requests.compat import urlparse, unquote, urljoin from requests import Response, codes import errno import os import os.path import stat import locale import io from streamlink.compat import is_win32 class FileAdapter(BaseAdapter): def send(self, request, **kwargs): """ Wraps a file, described in request, in a Response object. :param request: The PreparedRequest` being "sent". :returns: a Response object containing the file """ # Check that the method makes sense. Only support GET if request.method not in ("GET", "HEAD"): raise ValueError("Invalid request method %s" % request.method) # Parse the URL url_parts = urlparse(request.url) # Make the Windows URLs slightly nicer if is_win32 and url_parts.netloc.endswith(":"): url_parts = url_parts._replace(path="/" + url_parts.netloc + url_parts.path, netloc='') # Reject URLs with a hostname component if url_parts.netloc and url_parts.netloc not in ("localhost", ".", "..", "-"): raise ValueError("file: URLs with hostname components are not permitted") # If the path is relative update it to be absolute if url_parts.netloc in (".", ".."): pwd = os.path.abspath(url_parts.netloc).replace(os.sep, "/") + "/" if is_win32: # prefix the path with a / in Windows pwd = "/" + pwd url_parts = url_parts._replace(path=urljoin(pwd, url_parts.path.lstrip("/"))) resp = Response() resp.url = request.url # Open the file, translate certain errors into HTTP responses # Use urllib's unquote to translate percent escapes into whatever # they actually need to be try: # If the netloc is - then read from stdin if url_parts.netloc == "-": resp.raw = sys.stdin.buffer # make a fake response URL, the current directory resp.url = "file://" + os.path.abspath(".").replace(os.sep, "/") + "/" else: # Split the path on / (the URL directory separator) and decode any # % escapes in the parts path_parts = [unquote(p) for p in url_parts.path.split('/')] # Strip out the leading empty parts created from the leading /'s while path_parts and not path_parts[0]: path_parts.pop(0) # If os.sep is in any of the parts, someone fed us some shenanigans. # Treat is like a missing file. if any(os.sep in p for p in path_parts): raise IOError(errno.ENOENT, os.strerror(errno.ENOENT)) # Look for a drive component. If one is present, store it separately # so that a directory separator can correctly be added to the real # path, and remove any empty path parts between the drive and the path. # Assume that a part ending with : or | (legacy) is a drive. if path_parts and (path_parts[0].endswith('|') or path_parts[0].endswith(':')): path_drive = path_parts.pop(0) if path_drive.endswith('|'): path_drive = path_drive[:-1] + ':' while path_parts and not path_parts[0]: path_parts.pop(0) else: path_drive = '' # Try to put the path back together # Join the drive back in, and stick os.sep in front of the path to # make it absolute. path = path_drive + os.sep + os.path.join(*path_parts) # Check if the drive assumptions above were correct. If path_drive # is set, and os.path.splitdrive does not return a drive, it wasn't # reall a drive. Put the path together again treating path_drive # as a normal path component. if path_drive and not os.path.splitdrive(path): path = os.sep + os.path.join(path_drive, *path_parts) # Use io.open since we need to add a release_conn method, and # methods can't be added to file objects in python 2. resp.raw = io.open(path, "rb") resp.raw.release_conn = resp.raw.close except IOError as e: if e.errno == errno.EACCES: resp.status_code = codes.forbidden elif e.errno == errno.ENOENT: resp.status_code = codes.not_found else: resp.status_code = codes.bad_request # Wrap the error message in a file-like object # The error message will be localized, try to convert the string # representation of the exception into a byte stream resp_str = str(e).encode(locale.getpreferredencoding(False)) resp.raw = BytesIO(resp_str) resp.headers['Content-Length'] = len(resp_str) # Add release_conn to the BytesIO object resp.raw.release_conn = resp.raw.close else: resp.status_code = codes.ok # If it's a regular file, set the Content-Length resp_stat = os.fstat(resp.raw.fileno()) if stat.S_ISREG(resp_stat.st_mode): resp.headers['Content-Length'] = resp_stat.st_size return resp def close(self): pass ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643136177.0288403 streamlink-3.1.1/src/streamlink/plugin/0000755000175100001710000000000000000000000017463 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugin/__init__.py0000644000175100001710000000072400000000000021577 0ustar00runnerdockerfrom streamlink.exceptions import PluginError from streamlink.options import Argument as PluginArgument, Arguments as PluginArguments, Options as PluginOptions from streamlink.plugin.plugin import HIGH_PRIORITY, LOW_PRIORITY, NORMAL_PRIORITY, NO_PRIORITY, Plugin, pluginmatcher __all__ = [ "HIGH_PRIORITY", "NORMAL_PRIORITY", "LOW_PRIORITY", "NO_PRIORITY", "Plugin", "PluginArguments", "PluginArgument", "PluginError", "PluginOptions", "pluginmatcher", ] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643136177.0288403 streamlink-3.1.1/src/streamlink/plugin/api/0000755000175100001710000000000000000000000020234 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugin/api/__init__.py0000644000175100001710000000012600000000000022344 0ustar00runnerdockerfrom streamlink.plugin.api.http_session import HTTPSession __all__ = ["HTTPSession"] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugin/api/http_session.py0000644000175100001710000002075000000000000023334 0ustar00runnerdockerimport time from typing import Any, Callable, List, Pattern, Tuple import requests.adapters import urllib3 from requests import Session from streamlink.exceptions import PluginError from streamlink.packages.requests_file import FileAdapter from streamlink.plugin.api import useragents from streamlink.utils.parse import parse_json, parse_xml urllib3_version = tuple(map(int, urllib3.__version__.split(".")[:3])) try: # We tell urllib3 to disable warnings about unverified HTTPS requests, # because in some plugins we have to do unverified requests intentionally. urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) except AttributeError: pass class _HTTPResponse(urllib3.response.HTTPResponse): def __init__(self, *args, **kwargs): # Always enforce content length validation! # This fixes a bug in requests which doesn't raise errors on HTTP responses where # the "Content-Length" header doesn't match the response's body length. # https://github.com/psf/requests/issues/4956#issuecomment-573325001 # # Summary: # This bug is related to urllib3.response.HTTPResponse.stream() which calls urllib3.response.HTTPResponse.read() as # a wrapper for http.client.HTTPResponse.read(amt=...), where no http.client.IncompleteRead exception gets raised # due to "backwards compatiblity" of an old bug if a specific amount is attempted to be read on an incomplete response. # # urllib3.response.HTTPResponse.read() however has an additional check implemented via the enforce_content_length # parameter, but it doesn't check by default and requests doesn't set the parameter for enabling it either. # # Fix this by overriding urllib3.response.HTTPResponse's constructor and always setting enforce_content_length to True, # as there is no way to make requests set this parameter on its own. kwargs.update({"enforce_content_length": True}) super().__init__(*args, **kwargs) # override all urllib3.response.HTTPResponse references in requests.adapters.HTTPAdapter.send urllib3.connectionpool.HTTPConnectionPool.ResponseCls = _HTTPResponse requests.adapters.HTTPResponse = _HTTPResponse # Never convert percent-encoded characters to uppercase in urllib3>=1.25.4. # This is required for sites which compare request URLs byte for byte and return different responses depending on that. # Older versions of urllib3 are not compatible with this override and will always convert to uppercase characters. # # https://datatracker.ietf.org/doc/html/rfc3986#section-2.1 # > The uppercase hexadecimal digits 'A' through 'F' are equivalent to # > the lowercase digits 'a' through 'f', respectively. If two URIs # > differ only in the case of hexadecimal digits used in percent-encoded # > octets, they are equivalent. For consistency, URI producers and # > normalizers should use uppercase hexadecimal digits for all percent- # > encodings. if urllib3_version >= (1, 25, 4): class Urllib3UtilUrlPercentReOverride: _re_percent_encoding: Pattern = urllib3.util.url.PERCENT_RE @classmethod def _num_percent_encodings(cls, string) -> int: return len(cls._re_percent_encoding.findall(string)) # urllib3>=1.25.8 # https://github.com/urllib3/urllib3/blame/1.25.8/src/urllib3/util/url.py#L219-L227 @classmethod def subn(cls, repl: Callable, string: str) -> Tuple[str, int]: return string, cls._num_percent_encodings(string) # urllib3>=1.25.4,<1.25.8 # https://github.com/urllib3/urllib3/blame/1.25.4/src/urllib3/util/url.py#L218-L228 @classmethod def findall(cls, string: str) -> List[Any]: class _List(list): def __len__(self) -> int: return cls._num_percent_encodings(string) return _List() urllib3.util.url.PERCENT_RE = Urllib3UtilUrlPercentReOverride def _parse_keyvalue_list(val): for keyvalue in val.split(";"): try: key, value = keyvalue.split("=", 1) yield key.strip(), value.strip() except ValueError: continue class HTTPSession(Session): def __init__(self): super().__init__() self.headers['User-Agent'] = useragents.FIREFOX self.timeout = 20.0 self.mount('file://', FileAdapter()) @classmethod def determine_json_encoding(cls, sample): """ Determine which Unicode encoding the JSON text sample is encoded with RFC4627 (http://www.ietf.org/rfc/rfc4627.txt) suggests that the encoding of JSON text can be determined by checking the pattern of NULL bytes in first 4 octets of the text. :param sample: a sample of at least 4 bytes of the JSON text :return: the most likely encoding of the JSON text """ nulls_at = [i for i, j in enumerate(bytearray(sample[:4])) if j == 0] if nulls_at == [0, 1, 2]: return "UTF-32BE" elif nulls_at == [0, 2]: return "UTF-16BE" elif nulls_at == [1, 2, 3]: return "UTF-32LE" elif nulls_at == [1, 3]: return "UTF-16LE" else: return "UTF-8" @classmethod def json(cls, res, *args, **kwargs): """Parses JSON from a response.""" # if an encoding is already set then use the provided encoding if res.encoding is None: res.encoding = cls.determine_json_encoding(res.content[:4]) return parse_json(res.text, *args, **kwargs) @classmethod def xml(cls, res, *args, **kwargs): """Parses XML from a response.""" return parse_xml(res.text, *args, **kwargs) def parse_cookies(self, cookies, **kwargs): """Parses a semi-colon delimited list of cookies. Example: foo=bar;baz=qux """ for name, value in _parse_keyvalue_list(cookies): self.cookies.set(name, value, **kwargs) def parse_headers(self, headers): """Parses a semi-colon delimited list of headers. Example: foo=bar;baz=qux """ for name, value in _parse_keyvalue_list(headers): self.headers[name] = value def parse_query_params(self, cookies, **kwargs): """Parses a semi-colon delimited list of query parameters. Example: foo=bar;baz=qux """ for name, value in _parse_keyvalue_list(cookies): self.params[name] = value def resolve_url(self, url): """Resolves any redirects and returns the final URL.""" return self.get(url, stream=True).url def request(self, method, url, *args, **kwargs): acceptable_status = kwargs.pop("acceptable_status", []) exception = kwargs.pop("exception", PluginError) headers = kwargs.pop("headers", {}) params = kwargs.pop("params", {}) proxies = kwargs.pop("proxies", self.proxies) raise_for_status = kwargs.pop("raise_for_status", True) schema = kwargs.pop("schema", None) session = kwargs.pop("session", None) timeout = kwargs.pop("timeout", self.timeout) total_retries = kwargs.pop("retries", 0) retry_backoff = kwargs.pop("retry_backoff", 0.3) retry_max_backoff = kwargs.pop("retry_max_backoff", 10.0) retries = 0 if session: headers.update(session.headers) params.update(session.params) while True: try: res = super().request( method, url, headers=headers, params=params, timeout=timeout, proxies=proxies, *args, **kwargs ) if raise_for_status and res.status_code not in acceptable_status: res.raise_for_status() break except KeyboardInterrupt: raise except Exception as rerr: if retries >= total_retries: err = exception(f"Unable to open URL: {url} ({rerr})") err.err = rerr raise err retries += 1 # back off retrying, but only to a maximum sleep time delay = min(retry_max_backoff, retry_backoff * (2 ** (retries - 1))) time.sleep(delay) if schema: res = schema.validate(res.text, name="response text", exception=PluginError) return res __all__ = ["HTTPSession"] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugin/api/useragents.py0000644000175100001710000000217600000000000022774 0ustar00runnerdockerANDROID = "Mozilla/5.0 (Linux; Android 11) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.120 Mobile Safari/537.36" CHROME = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.7113.93 Safari/537.36" CHROME_OS = "Mozilla/5.0 (X11; CrOS x86_64 13904.77.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.147 Safari/537.36" FIREFOX = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:94.0) Gecko/20100101 Firefox/94.0" IE_11 = "Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; rv:11.0) like Gecko" IPHONE = "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1" OPERA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 OPR/77.0.4054.203" SAFARI = "Mozilla/5.0 (Macintosh; Intel Mac OS X 11_4) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15" # Backwards compatibility EDGE = CHROME FIREFOX_MAC = FIREFOX IE_6 = IE_7 = IE_8 = IE_9 = IE_11 IPHONE_6 = IPAD = IPHONE SAFARI_7 = SAFARI_8 = SAFARI WINDOWS_PHONE_8 = ANDROID ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugin/api/utils.py0000644000175100001710000000216300000000000021750 0ustar00runnerdocker"""Useful wrappers and other tools.""" import re from collections import namedtuple from streamlink.utils.parse import parse_json, parse_qsd as parse_query, parse_xml __all__ = ["parse_json", "parse_xml", "parse_query"] tag_re = re.compile(r'''(?=<(?P[a-zA-Z]+)(?P.*?)(?P/)?>(?:(?P.*?))?)''', re.MULTILINE | re.DOTALL) attr_re = re.compile(r'''\s*(?P[\w-]+)\s*(?:=\s*(?P["']?)(?P.*?)(?P=quote)\s*)?''') Tag = namedtuple("Tag", "tag attributes text") def itertags(html, tag): """ Brute force regex based HTML tag parser. This is a rough-and-ready searcher to find HTML tags when standards compliance is not required. Will find tags that are commented out, or inside script tag etc. :param html: HTML page :param tag: tag name to find :return: generator with Tags """ for match in tag_re.finditer(html): if match.group("tag") == tag: attrs = {a.group("key").lower(): a.group("value") for a in attr_re.finditer(match.group("attr"))} yield Tag(match.group("tag"), attrs, match.group("inner")) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugin/api/validate.py0000644000175100001710000003353400000000000022407 0ustar00runnerdocker"""This module provides an API to validate and to some extent manipulate data structures, such as JSON and XML parsing results. Example usage: >>> validate(int, 5) 5 >>> validate({text: int}, {'foo': '1'}) ValueError: Type of '1' should be 'int' but is 'str' >>> validate({'foo': transform(int)}, {'foo': '1'}) {'foo': 1} """ from copy import copy as copy_obj from functools import singledispatch from typing import Any, Tuple, Union from urllib.parse import urlparse from lxml.etree import Element, iselement from streamlink.exceptions import PluginError from streamlink.utils.parse import ( parse_html as _parse_html, parse_json as _parse_json, parse_qsd as _parse_qsd, parse_xml as _parse_xml ) __all__ = [ "any", "all", "filter", "get", "getattr", "hasattr", "length", "optional", "transform", "text", "union", "union_get", "url", "startswith", "endswith", "contains", "xml_element", "xml_find", "xml_findall", "xml_findtext", "xml_xpath", "xml_xpath_string", "parse_json", "parse_html", "parse_xml", "parse_qsd", "validate", "Schema", "SchemaContainer" ] text = str # References to original functions that we override in this module _all = all _getattr = getattr _hasattr = hasattr _filter = filter _map = map _re_match_attr = ("group", "groups", "groupdict", "re") def _is_re_match(value): return _all(_hasattr(value, a) for a in _re_match_attr) class any(tuple): """At least one of the schemas must be valid.""" def __new__(cls, *args): return super().__new__(cls, args) class all(tuple): """All schemas must be valid.""" def __new__(cls, *args): return super().__new__(cls, args) class SchemaContainer: def __init__(self, schema): self.schema = schema class transform: """Applies function to value to transform it.""" def __init__(self, func, *args, **kwargs): self.func = func self.args = args self.kwargs = kwargs class optional: """An optional key used in a dict or union-dict.""" def __init__(self, key): self.key = key class union(SchemaContainer): """Extracts multiple validations based on the same value.""" class attr(SchemaContainer): """Validates an object's attributes.""" class union_get: def __init__(self, *keys, seq=tuple): self.keys = keys self.seq = seq class xml_element: """A XML element.""" def __init__(self, tag=None, text=None, attrib=None): self.tag = tag self.text = text self.attrib = attrib # ---- def length(length): """Checks value for minimum length using len().""" def min_len(value): if not len(value) >= length: raise ValueError( "Minimum length is {0} but value is {1}".format(length, len(value)) ) return True return min_len def startswith(string): """Checks if the string value starts with another string.""" def starts_with(value): validate(text, value) if not value.startswith(string): raise ValueError("'{0}' does not start with '{1}'".format(value, string)) return True return starts_with def endswith(string): """Checks if the string value ends with another string.""" def ends_with(value): validate(text, value) if not value.endswith(string): raise ValueError("'{0}' does not end with '{1}'".format(value, string)) return True return ends_with def contains(string): """Checks if the string value contains another string.""" def contains_str(value): validate(text, value) if string not in value: raise ValueError("'{0}' does not contain '{1}'".format(value, string)) return True return contains_str def get(item: Union[Any, Tuple[Any]], default: Any = None, strict: bool = False): """Get item from value (value[item]). Unless strict is set to True, item can be a tuple of items for recursive lookups. If the item is not found in the last object of a recursive lookup, return the default. Handles XML elements, regex matches and anything that has __getitem__. """ if type(item) is not tuple or strict: item = (item,) def getter(value): idx = 0 try: for key in item: if iselement(value): value = value.attrib[key] # Use .group() if this is a regex match object elif _is_re_match(value): value = value.group(key) else: value = value[key] idx += 1 return value except (KeyError, IndexError): # only return default value on last item in nested lookup if idx < len(item) - 1: raise ValueError(f"Object \"{value}\" does not have item \"{key}\"") return default except (TypeError, AttributeError) as err: raise ValueError(err) return transform(getter) def getattr(attr, default=None): """Get a named attribute from an object. When a default argument is given, it is returned when the attribute doesn't exist. """ def getter(value): return _getattr(value, attr, default) return transform(getter) def hasattr(attr): """Verifies that the object has an attribute with the given name.""" def has_attr(value): return _hasattr(value, attr) return has_attr def filter(func): """Filters out unwanted items using the specified function. Supports both dicts and sequences, key/value pairs are expanded when applied to a dict. """ def expand_kv(kv): return func(*kv) def filter_values(value): cls = type(value) if isinstance(value, dict): return cls(_filter(expand_kv, value.items())) else: return cls(_filter(func, value)) return transform(filter_values) def map(func): """Apply function to each value inside the sequence or dict. Supports both dicts and sequences, key/value pairs are expanded when applied to a dict. """ def expand_kv(kv): return func(*kv) def map_values(value): cls = type(value) if isinstance(value, dict): return cls(_map(expand_kv, value.items())) else: return cls(_map(func, value)) return transform(map_values) def url(**attributes): """Parses an URL and validates its attributes.""" def check_url(value): validate(text, value) parsed = urlparse(value) if not parsed.netloc: raise ValueError("'{0}' is not a valid URL".format(value)) for name, schema in attributes.items(): if not _hasattr(parsed, name): raise ValueError("Invalid URL attribute '{0}'".format(name)) try: validate(schema, _getattr(parsed, name)) except ValueError as err: raise ValueError( "Unable to validate URL attribute '{0}': {1}".format( name, err ) ) return True # Convert "http" to be either any("http", "https") for convenience if attributes.get("scheme") == "http": attributes["scheme"] = any("http", "https") return check_url def xml_find(xpath): """Find a XML element via xpath.""" def xpath_find(value): validate(iselement, value) value = value.find(xpath) if value is None: raise ValueError(f"XPath '{xpath}' did not return an element") return validate(iselement, value) return transform(xpath_find) def xml_findall(xpath): """Find a list of XML elements via xpath.""" def xpath_findall(value): validate(iselement, value) return value.findall(xpath) return transform(xpath_findall) def xml_findtext(xpath): """Find a XML element via xpath and extract its text.""" return all( xml_find(xpath), getattr("text"), ) def xml_xpath(xpath): def transform_xpath(value): validate(iselement, value) return value.xpath(xpath) or None return transform(transform_xpath) def xml_xpath_string(xpath): return xml_xpath(f"string({xpath})") def parse_json(*args, **kwargs): return transform(_parse_json, *args, **kwargs, exception=ValueError, schema=None) def parse_html(*args, **kwargs): return transform(_parse_html, *args, **kwargs, exception=ValueError, schema=None) def parse_xml(*args, **kwargs): return transform(_parse_xml, *args, **kwargs, exception=ValueError, schema=None) def parse_qsd(*args, **kwargs): return transform(_parse_qsd, *args, **kwargs, exception=ValueError, schema=None) # ---- @singledispatch def validate(schema, value): if callable(schema): if schema(value): return value else: raise ValueError("{0}({1!r}) is not true".format(schema.__name__, value)) if schema == value: return value else: raise ValueError("{0!r} does not equal {1!r}".format(value, schema)) @validate.register(any) def validate_any(schema, value): errors = [] for subschema in schema: try: return validate(subschema, value) except ValueError as err: errors.append(err) else: err = " or ".join(_map(str, errors)) raise ValueError(err) @validate.register(all) def validate_all(schemas, value): for schema in schemas: value = validate(schema, value) return value @validate.register(transform) def validate_transform(schema: transform, value): validate(callable, schema.func) return schema.func(value, *schema.args, **schema.kwargs) @validate.register(list) @validate.register(tuple) @validate.register(set) @validate.register(frozenset) def validate_sequence(schema, value): validate(type(schema), value) return type(schema)(validate(any(*schema), v) for v in value) @validate.register(dict) def validate_dict(schema, value): validate(type(schema), value) new = type(schema)() for key, subschema in schema.items(): if isinstance(key, optional): if key.key not in value: continue key = key.key if type(key) in (type, transform, any, all, union): for subkey, subvalue in value.items(): new[validate(key, subkey)] = validate(subschema, subvalue) break else: if key not in value: raise ValueError("Key '{0}' not found in {1!r}".format(key, value)) try: new[key] = validate(subschema, value[key]) except ValueError as err: raise ValueError("Unable to validate key '{0}': {1}".format(key, err)) return new @validate.register(type) def validate_type(schema, value): if isinstance(value, schema): return value else: raise ValueError( "Type of {0!r} should be '{1}' but is '{2}'".format( value, schema.__name__, type(value).__name__ ) ) @validate.register(xml_element) def validate_xml_element(schema, value): validate(iselement, value) _tag = value.tag _attrib = value.attrib _text = value.text if schema.attrib is not None: try: _attrib = validate(schema.attrib, dict(value.attrib)) except ValueError as err: raise ValueError(f"Unable to validate XML attributes: {err}") if schema.tag is not None: try: _tag = validate(schema.tag, value.tag) except ValueError as err: raise ValueError(f"Unable to validate XML tag: {err}") if schema.text is not None: try: _text = validate(schema.text, value.text) except ValueError as err: raise ValueError(f"Unable to validate XML text: {err}") new = Element(_tag, _attrib) new.text = _text for child in value: new.append(child) return new @validate.register(attr) def validate_attr(schema, value): new = copy_obj(value) for attr, schema in schema.schema.items(): if not _hasattr(value, attr): raise ValueError("Attribute '{0}' not found on object '{1}'".format( attr, value )) setattr(new, attr, validate(schema, _getattr(value, attr))) return new @validate.register(union_get) def validate_union_from(schema, value): return schema.seq(validate(get(k), value) for k in schema.keys) @singledispatch def validate_union(schema, value): raise ValueError("Invalid union type: {0}".format(type(schema).__name__)) @validate_union.register(dict) def validate_union_dict(schema, value): new = type(schema)() for key, schema in schema.items(): optional_ = isinstance(key, optional) if optional_: key = key.key try: new[key] = validate(schema, value) except ValueError as err: if optional_: continue raise ValueError("Unable to validate union '{0}': {1}".format(key, err)) return new @validate_union.register(list) @validate_union.register(tuple) @validate_union.register(set) @validate_union.register(frozenset) def validate_union_sequence(schemas, value): return type(schemas)(validate(schema, value) for schema in schemas) @validate.register(union) def validate_unions(schema, value): return validate_union(schema.schema, value) class Schema: """Wraps a validator schema into a object.""" def __init__(self, *schemas): self.schema = all(*schemas) def validate(self, value, name="result", exception=PluginError): try: return validate(self.schema, value) except ValueError as err: raise exception("Unable to validate {0}: {1}".format(name, err)) @validate.register(Schema) def validate_schema(schema, value): return schema.validate(value, exception=ValueError) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugin/api/websocket.py0000644000175100001710000001327500000000000022604 0ustar00runnerdockerimport json import logging from threading import RLock, Thread from typing import Any, Dict, List, Optional, Tuple, Union from urllib.parse import unquote_plus, urlparse from websocket import ABNF, STATUS_NORMAL, WebSocketApp, enableTrace from streamlink.logger import TRACE, root as rootlogger from streamlink.session import Streamlink log = logging.getLogger(__name__) class WebsocketClient(Thread): _id: int = 0 ws: WebSocketApp def __init__( self, session: Streamlink, url: str, subprotocols: Optional[List[str]] = None, header: Optional[Union[List, Dict]] = None, cookie: Optional[str] = None, sockopt: Optional[Tuple] = None, sslopt: Optional[Dict] = None, host: Optional[str] = None, origin: Optional[str] = None, suppress_origin: bool = False, ping_interval: Union[int, float] = 0, ping_timeout: Optional[Union[int, float]] = None, ping_payload: str = "" ): if rootlogger.level <= TRACE: enableTrace(True, log) if not header: header = [] if not any(True for h in header if h.startswith("User-Agent: ")): header.append(f"User-Agent: {session.http.headers['User-Agent']}") proxy_options = {} http_proxy: Optional[str] = session.get_option("http-proxy") if http_proxy: p = urlparse(http_proxy) proxy_options["proxy_type"] = p.scheme proxy_options["http_proxy_host"] = p.hostname if p.port: # pragma: no branch proxy_options["http_proxy_port"] = p.port if p.username: # pragma: no branch proxy_options["http_proxy_auth"] = unquote_plus(p.username), unquote_plus(p.password or "") self._reconnect = False self._reconnect_lock = RLock() self.session = session self._ws_init(url, subprotocols, header, cookie) self._ws_rundata = dict( sockopt=sockopt, sslopt=sslopt, host=host, origin=origin, suppress_origin=suppress_origin, ping_interval=ping_interval, ping_timeout=ping_timeout, ping_payload=ping_payload, **proxy_options ) self._id += 1 super().__init__( name=f"Thread-{self.__class__.__name__}-{self._id}", daemon=True ) def _ws_init(self, url, subprotocols, header, cookie): self.ws = WebSocketApp( url=url, subprotocols=subprotocols, header=header, cookie=cookie, on_open=self.on_open, on_error=self.on_error, on_close=self.on_close, on_ping=self.on_ping, on_pong=self.on_pong, on_message=self.on_message, on_cont_message=self.on_cont_message, on_data=self.on_data ) def run(self) -> None: while True: log.debug(f"Connecting to: {self.ws.url}") self.ws.run_forever(**self._ws_rundata) # check if closed via a reconnect() call with self._reconnect_lock: if not self._reconnect: return self._reconnect = False # ---- def reconnect( self, url: str = None, subprotocols: Optional[List[str]] = None, header: Optional[Union[List, Dict]] = None, cookie: Optional[str] = None, closeopts: Optional[Dict] = None ) -> None: with self._reconnect_lock: # ws connection is not active (anymore) if not self.ws.keep_running: return log.debug("Reconnecting...") self._reconnect = True self.ws.close(**(closeopts or {})) self._ws_init( url=self.ws.url if url is None else url, subprotocols=self.ws.subprotocols if subprotocols is None else subprotocols, header=self.ws.header if header is None else header, cookie=self.ws.cookie if cookie is None else cookie ) def close(self, status: int = STATUS_NORMAL, reason: Union[str, bytes] = "", timeout: int = 3) -> None: self.ws.close(status=status, reason=bytes(reason, encoding="utf-8"), timeout=timeout) if self.is_alive(): # pragma: no branch self.join() def send(self, data: Union[str, bytes], opcode: int = ABNF.OPCODE_TEXT) -> None: return self.ws.send(data, opcode) def send_json(self, data: Any) -> None: return self.send(json.dumps(data, indent=None, separators=(",", ":"))) # ---- # noinspection PyMethodMayBeStatic def on_open(self, wsapp: WebSocketApp) -> None: log.debug(f"Connected: {wsapp.url}") # pragma: no cover # noinspection PyMethodMayBeStatic # noinspection PyUnusedLocal def on_error(self, wsapp: WebSocketApp, error: Exception) -> None: log.error(error) # pragma: no cover # noinspection PyMethodMayBeStatic # noinspection PyUnusedLocal def on_close(self, wsapp: WebSocketApp, status: int, message: str) -> None: log.debug(f"Closed: {wsapp.url}") # pragma: no cover def on_ping(self, wsapp: WebSocketApp, data: str) -> None: pass # pragma: no cover def on_pong(self, wsapp: WebSocketApp, data: str) -> None: pass # pragma: no cover def on_message(self, wsapp: WebSocketApp, data: str) -> None: pass # pragma: no cover def on_cont_message(self, wsapp: WebSocketApp, data: str, cont: Any) -> None: pass # pragma: no cover def on_data(self, wsapp: WebSocketApp, data: str, data_type: int, cont: Any) -> None: pass # pragma: no cover ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugin/plugin.py0000644000175100001710000004543000000000000021341 0ustar00runnerdockerimport ast import logging import operator import re import time from collections import OrderedDict from functools import partial from typing import Any, Callable, ClassVar, Dict, List, Match, NamedTuple, Optional, Pattern, Sequence, Type import requests.cookies from streamlink.cache import Cache from streamlink.exceptions import FatalPluginError, NoStreamsError, PluginError from streamlink.options import Arguments, Options log = logging.getLogger(__name__) # FIXME: This is a crude attempt at making a bitrate's # weight end up similar to the weight of a resolution. # Someone who knows math, please fix. BIT_RATE_WEIGHT_RATIO = 2.8 ALT_WEIGHT_MOD = 0.01 QUALITY_WEIGTHS_EXTRA = { "other": { "live": 1080, }, "tv": { "hd": 1080, "sd": 576, }, "quality": { "ehq": 720, "hq": 576, "sq": 360, }, } FILTER_OPERATORS = { "<": operator.lt, "<=": operator.le, ">": operator.gt, ">=": operator.ge, } PARAMS_REGEX = r"(\w+)=({.+?}|\[.+?\]|\(.+?\)|'(?:[^'\\]|\\')*'|\"(?:[^\"\\]|\\\")*\"|\S+)" HIGH_PRIORITY = 30 NORMAL_PRIORITY = 20 LOW_PRIORITY = 10 NO_PRIORITY = 0 def stream_weight(stream): for group, weights in QUALITY_WEIGTHS_EXTRA.items(): if stream in weights: return weights[stream], group match = re.match(r"^(\d+)(k|p)?(\d+)?(\+)?(?:[a_](\d+)k)?(?:_(alt)(\d)?)?$", stream) if match: weight = 0 if match.group(6): if match.group(7): weight -= ALT_WEIGHT_MOD * int(match.group(7)) else: weight -= ALT_WEIGHT_MOD name_type = match.group(2) if name_type == "k": # bit rate bitrate = int(match.group(1)) weight += bitrate / BIT_RATE_WEIGHT_RATIO return weight, "bitrate" elif name_type == "p": # resolution weight += int(match.group(1)) if match.group(3): # fps eg. 60p or 50p weight += int(match.group(3)) if match.group(4) == "+": weight += 1 if match.group(5): # bit rate classifier for resolution weight += int(match.group(5)) / BIT_RATE_WEIGHT_RATIO return weight, "pixels" return 0, "none" def iterate_streams(streams): for name, stream in streams: if isinstance(stream, list): for sub_stream in stream: yield (name, sub_stream) else: yield (name, stream) def stream_type_priority(stream_types, stream): stream_type = type(stream[1]).shortname() try: prio = stream_types.index(stream_type) except ValueError: try: prio = stream_types.index("*") except ValueError: prio = 99 return prio def stream_sorting_filter(expr, stream_weight): match = re.match(r"(?P<=|>=|<|>)?(?P[\w+]+)", expr) if not match: raise PluginError("Invalid filter expression: {0}".format(expr)) op, value = match.group("op", "value") op = FILTER_OPERATORS.get(op, operator.eq) filter_weight, filter_group = stream_weight(value) def func(quality): weight, group = stream_weight(quality) if group == filter_group: return not op(weight, filter_weight) return True return func def parse_params(params: Optional[str] = None) -> Dict[str, Any]: rval = {} if not params: return rval matches = re.findall(PARAMS_REGEX, params) for key, value in matches: try: value = ast.literal_eval(value) except Exception: pass rval[key] = value return rval class UserInputRequester: """ Base Class / Interface for requesting user input eg. From the console """ def ask(self, prompt): """ Ask the user for a text input, the input is not sensitive and can be echoed to the user :param prompt: message to display when asking for the input :return: the value the user input """ raise NotImplementedError def ask_password(self, prompt): """ Ask the user for a text input, the input _is_ sensitive and should be masked as the user gives the input :param prompt: message to display when asking for the input :return: the value the user input """ raise NotImplementedError class Matcher(NamedTuple): pattern: Pattern priority: int class Plugin: """A plugin can retrieve stream information from the URL specified. :param url: URL that the plugin will operate on """ # the list of plugin matchers (URL pattern + priority) # use the streamlink.plugin.pluginmatcher decorator for initializing this list matchers: ClassVar[List[Matcher]] = None # a tuple of `re.Match` results of all defined matchers matches: Sequence[Optional[Match]] # a reference to the compiled `re.Pattern` of the first matching matcher matcher: Pattern # a reference to the `re.Match` result of the first matching matcher match: Match # plugin metadata attributes id: Optional[str] = None author: Optional[str] = None category: Optional[str] = None title: Optional[str] = None cache = None logger = None module = "unknown" options = Options() arguments = Arguments() session = None _url: str = None _user_input_requester = None @classmethod def bind(cls, session, module, user_input_requester=None): cls.cache = Cache(filename="plugin-cache.json", key_prefix=module) cls.logger = logging.getLogger("streamlink.plugins." + module) cls.module = module cls.session = session if user_input_requester is not None: if isinstance(user_input_requester, UserInputRequester): cls._user_input_requester = user_input_requester else: raise RuntimeError("user-input-requester must be an instance of UserInputRequester") @property def url(self) -> str: return self._url @url.setter def url(self, value: str): self._url = value matches = [(pattern, pattern.match(value)) for pattern, priority in self.matchers or []] self.matches = tuple(m for p, m in matches) self.matcher, self.match = next(((p, m) for p, m in matches if m is not None), (None, None)) def __init__(self, url: str) -> None: self.url = url try: self.load_cookies() except RuntimeError: pass # unbound cannot load @classmethod def set_option(cls, key, value): cls.options.set(key, value) @classmethod def get_option(cls, key): return cls.options.get(key) @classmethod def get_argument(cls, key): return cls.arguments.get(key) @classmethod def stream_weight(cls, stream): return stream_weight(stream) @classmethod def default_stream_types(cls, streams): stream_types = ["hls", "http"] for name, stream in iterate_streams(streams): stream_type = type(stream).shortname() if stream_type not in stream_types: stream_types.append(stream_type) return stream_types @classmethod def broken(cls, issue=None): def func(*args, **kwargs): msg = ( "This plugin has been marked as broken. This is likely due to " "changes to the service preventing a working implementation. " ) if issue: msg += "More info: https://github.com/streamlink/streamlink/issues/{0}".format(issue) raise PluginError(msg) def decorator(*args, **kwargs): return func return decorator def streams(self, stream_types=None, sorting_excludes=None): """Attempts to extract available streams. Returns a :class:`dict` containing the streams, where the key is the name of the stream, most commonly the quality and the value is a :class:`Stream` object. The result can contain the synonyms **best** and **worst** which points to the streams which are likely to be of highest and lowest quality respectively. If multiple streams with the same name are found, the order of streams specified in *stream_types* will determine which stream gets to keep the name while the rest will be renamed to "_". The synonyms can be fine tuned with the *sorting_excludes* parameter. This can be either of these types: - A list of filter expressions in the format *[operator]*. For example the filter ">480p" will exclude streams ranked higher than "480p" from the list used in the synonyms ranking. Valid operators are >, >=, < and <=. If no operator is specified then equality will be tested. - A function that is passed to filter() with a list of stream names as input. :param stream_types: A list of stream types to return. :param sorting_excludes: Specify which streams to exclude from the best/worst synonyms. """ try: ostreams = self._get_streams() if isinstance(ostreams, dict): ostreams = ostreams.items() # Flatten the iterator to a list so we can reuse it. if ostreams: ostreams = list(ostreams) except NoStreamsError: return {} except (OSError, ValueError) as err: raise PluginError(err) if not ostreams: return {} if stream_types is None: stream_types = self.default_stream_types(ostreams) # Add streams depending on stream type and priorities sorted_streams = sorted(iterate_streams(ostreams), key=partial(stream_type_priority, stream_types)) streams = {} for name, stream in sorted_streams: stream_type = type(stream).shortname() # Use * as wildcard to match other stream types if "*" not in stream_types and stream_type not in stream_types: continue # drop _alt from any stream names if name.endswith("_alt"): name = name[:-len("_alt")] existing = streams.get(name) if existing: existing_stream_type = type(existing).shortname() if existing_stream_type != stream_type: name = "{0}_{1}".format(name, stream_type) if name in streams: name = "{0}_alt".format(name) num_alts = len(list(filter(lambda n: n.startswith(name), streams.keys()))) # We shouldn't need more than 2 alt streams if num_alts >= 2: continue elif num_alts > 0: name = "{0}{1}".format(name, num_alts + 1) # Validate stream name and discard the stream if it's bad. match = re.match("([A-z0-9_+]+)", name) if match: name = match.group(1) else: self.logger.debug(f"The stream '{name}' has been ignored since it is badly named.") continue # Force lowercase name and replace space with underscore. streams[name.lower()] = stream # Create the best/worst synonyms def stream_weight_only(s): return (self.stream_weight(s)[0] or (len(streams) == 1 and 1)) stream_names = filter(stream_weight_only, streams.keys()) sorted_streams = sorted(stream_names, key=stream_weight_only) unfiltered_sorted_streams = sorted_streams if isinstance(sorting_excludes, list): for expr in sorting_excludes: filter_func = stream_sorting_filter(expr, self.stream_weight) sorted_streams = list(filter(filter_func, sorted_streams)) elif callable(sorting_excludes): sorted_streams = list(filter(sorting_excludes, sorted_streams)) final_sorted_streams = OrderedDict() for stream_name in sorted(streams, key=stream_weight_only): final_sorted_streams[stream_name] = streams[stream_name] if len(sorted_streams) > 0: best = sorted_streams[-1] worst = sorted_streams[0] final_sorted_streams["worst"] = streams[worst] final_sorted_streams["best"] = streams[best] elif len(unfiltered_sorted_streams) > 0: best = unfiltered_sorted_streams[-1] worst = unfiltered_sorted_streams[0] final_sorted_streams["worst-unfiltered"] = streams[worst] final_sorted_streams["best-unfiltered"] = streams[best] return final_sorted_streams def _get_streams(self): raise NotImplementedError def get_metadata(self) -> Dict[str, Optional[str]]: return dict( id=self.get_id(), author=self.get_author(), category=self.get_category(), title=self.get_title() ) def get_id(self) -> Optional[str]: return None if self.id is None else str(self.id).strip() def get_title(self) -> Optional[str]: return None if self.title is None else str(self.title).strip() def get_author(self) -> Optional[str]: return None if self.author is None else str(self.author).strip() def get_category(self) -> Optional[str]: return None if self.category is None else str(self.category).strip() def save_cookies(self, cookie_filter=None, default_expires=60 * 60 * 24 * 7): """ Store the cookies from ``http`` in the plugin cache until they expire. The cookies can be filtered by supplying a filter method. eg. ``lambda c: "auth" in c.name``. If no expiry date is given in the cookie then the ``default_expires`` value will be used. :param cookie_filter: a function to filter the cookies :type cookie_filter: function :param default_expires: time (in seconds) until cookies with no expiry will expire :type default_expires: int :return: list of the saved cookie names """ if not self.session or not self.cache: raise RuntimeError("Cannot cache cookies in unbound plugin") cookie_filter = cookie_filter or (lambda c: True) saved = [] for cookie in filter(cookie_filter, self.session.http.cookies): cookie_dict = {} for attr in ("version", "name", "value", "port", "domain", "path", "secure", "expires", "discard", "comment", "comment_url", "rfc2109"): cookie_dict[attr] = getattr(cookie, attr, None) cookie_dict["rest"] = getattr(cookie, "rest", getattr(cookie, "_rest", None)) expires = default_expires if cookie_dict['expires']: expires = int(cookie_dict['expires'] - time.time()) key = "__cookie:{0}:{1}:{2}:{3}".format(cookie.name, cookie.domain, cookie.port_specified and cookie.port or "80", cookie.path_specified and cookie.path or "*") self.cache.set(key, cookie_dict, expires) saved.append(cookie.name) if saved: self.logger.debug("Saved cookies: {0}".format(", ".join(saved))) return saved def load_cookies(self): """ Load any stored cookies for the plugin that have not expired. :return: list of the restored cookie names """ if not self.session or not self.cache: raise RuntimeError("Cannot load cached cookies in unbound plugin") restored = [] for key, value in self.cache.get_all().items(): if key.startswith("__cookie"): cookie = requests.cookies.create_cookie(**value) self.session.http.cookies.set_cookie(cookie) restored.append(cookie.name) if restored: self.logger.debug("Restored cookies: {0}".format(", ".join(restored))) return restored def clear_cookies(self, cookie_filter=None): """ Removes all of the saved cookies for this Plugin. To filter the cookies that are deleted specify the ``cookie_filter`` argument (see :func:`save_cookies`). :param cookie_filter: a function to filter the cookies :type cookie_filter: function :return: list of the removed cookie names """ if not self.session or not self.cache: raise RuntimeError("Cannot clear cached cookies in unbound plugin") cookie_filter = cookie_filter or (lambda c: True) removed = [] for key, value in sorted(self.cache.get_all().items(), key=operator.itemgetter(0), reverse=True): if key.startswith("__cookie"): cookie = requests.cookies.create_cookie(**value) if cookie_filter(cookie): del self.session.http.cookies[cookie.name] self.cache.set(key, None, 0) removed.append(key) return removed def input_ask(self, prompt): if self._user_input_requester: try: return self._user_input_requester.ask(prompt) except OSError as e: raise FatalPluginError("User input error: {0}".format(e)) except NotImplementedError: # ignore this and raise a FatalPluginError pass raise FatalPluginError("This plugin requires user input, however it is not supported on this platform") def input_ask_password(self, prompt): if self._user_input_requester: try: return self._user_input_requester.ask_password(prompt) except OSError as e: raise FatalPluginError("User input error: {0}".format(e)) except NotImplementedError: # ignore this and raise a FatalPluginError pass raise FatalPluginError("This plugin requires user input, however it is not supported on this platform") def pluginmatcher(pattern: Pattern, priority: int = NORMAL_PRIORITY) -> Callable[[Type[Plugin]], Type[Plugin]]: matcher = Matcher(pattern, priority) def decorator(cls: Type[Plugin]) -> Type[Plugin]: if not issubclass(cls, Plugin): raise TypeError(f"{repr(cls)} is not a Plugin") if cls.matchers is None: cls.matchers = [] cls.matchers.insert(0, matcher) return cls return decorator __all__ = [ "HIGH_PRIORITY", "NORMAL_PRIORITY", "LOW_PRIORITY", "NO_PRIORITY", "Plugin", "Matcher", "pluginmatcher", ] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643136177.0448403 streamlink-3.1.1/src/streamlink/plugins/0000755000175100001710000000000000000000000017646 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/.removed0000644000175100001710000000230300000000000021306 0ustar00runnerdocker# Removed plugin files # https://github.com/streamlink/streamlink/pull/2003 # https://github.com/streamlink/streamlink/issues/1223 abweb afreecatv aftonbladet akamaihd aliez alieztv aljazeeraen animelab antenna apac arconai arconia atv azubutv bambuser beam beattv bliptv bnt bongacams brittv btsports cam4 camsoda canalplus canlitv chaturbate common_swf connectcast cubetv cybergame cyro daisuki dingittv disney_de dmcloud dmcloud_embed dommune douyutv douyutv_blackbox dplay ellobo eurocom europaplus expressen filmon_us furstream gaminglive gardenersworld gomexp googledocs hds hitbox huomao ine itvplayer kanal7 kingkong kralmuzik latina letontv live_russia_tv livecodingtv liveedu liveme livestation looch media_ccc_de meerkat metube mico mips mixer mlgtv neulion nineanime npo ok_live oldlivestream ovvatv pandatv pcyourfreetv periscope playtv powerapp reshet rte rtlxl rtmp seemeplay seetv servustv showtv skai speedrunslive srgssr startv stream streamboat streamingvideoprovider streamlive streamme streamupcom tamago teleclubzoom tga tigerdile toya trt trtspor tv1channel tv8cat tvcatchup tvnbg tvplayer ufctv vaughnlive veetle vgtv viagame viasat viasat_embed viutv wattv webcast_india_gov weeb willax younow ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/__init__.py0000644000175100001710000000052100000000000021755 0ustar00runnerdocker""" New plugins should use streamlink.plugin.Plugin instead of this module, but this is kept here for backwards compatibility. """ from streamlink.exceptions import NoPluginError, NoStreamsError, PluginError from streamlink.plugin.plugin import Plugin __all__ = ['Plugin', 'PluginError', 'NoStreamsError', 'NoPluginError'] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/abematv.py0000644000175100001710000002306400000000000021644 0ustar00runnerdockerimport hashlib import hmac import logging import re import struct import time import uuid from base64 import urlsafe_b64encode from binascii import unhexlify from Crypto.Cipher import AES from requests import Response from requests.adapters import BaseAdapter from streamlink.exceptions import NoStreamsError from streamlink.plugin import Plugin, pluginmatcher from streamlink.plugin.api import useragents, validate from streamlink.stream.hls import HLSStream, HLSStreamReader, HLSStreamWriter from streamlink.utils.url import update_qsd log = logging.getLogger(__name__) class AbemaTVHLSStreamWriter(HLSStreamWriter): def should_filter_sequence(self, sequence): return "/tsad/" in sequence.segment.uri or super().should_filter_sequence(sequence) class AbemaTVHLSStreamReader(HLSStreamReader): __writer__ = AbemaTVHLSStreamWriter class AbemaTVHLSStream(HLSStream): __reader__ = AbemaTVHLSStreamReader class AbemaTVLicenseAdapter(BaseAdapter): ''' Handling abematv-license:// protocol to get real video key_data. ''' STRTABLE = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" HKEY = b"3AF0298C219469522A313570E8583005A642E73EDD58E3EA2FB7339D3DF1597E" _MEDIATOKEN_API = "https://api.abema.io/v1/media/token" _LICENSE_API = "https://license.abema.io/abematv-hls" _MEDIATOKEN_SCHEMA = validate.Schema({"token": validate.text}) _LICENSE_SCHEMA = validate.Schema({"k": validate.text, "cid": validate.text}) def __init__(self, session, deviceid, usertoken): self._session = session self.deviceid = deviceid self.usertoken = usertoken super().__init__() def _get_videokey_from_ticket(self, ticket): params = { "osName": "android", "osVersion": "6.0.1", "osLang": "ja_JP", "osTimezone": "Asia/Tokyo", "appId": "tv.abema", "appVersion": "3.27.1" } auth_header = {"Authorization": "Bearer " + self.usertoken} res = self._session.http.get(self._MEDIATOKEN_API, params=params, headers=auth_header) jsonres = self._session.http.json(res, schema=self._MEDIATOKEN_SCHEMA) mediatoken = jsonres['token'] res = self._session.http.post(self._LICENSE_API, params={"t": mediatoken}, json={"kv": "a", "lt": ticket}) jsonres = self._session.http.json(res, schema=self._LICENSE_SCHEMA) cid = jsonres['cid'] k = jsonres['k'] res = sum([self.STRTABLE.find(k[i]) * (58 ** (len(k) - 1 - i)) for i in range(len(k))]) encvideokey = struct.pack('>QQ', res >> 64, res & 0xffffffffffffffff) # HKEY: # RC4KEY = unhexlify('DB98A8E7CECA3424D975280F90BD03EE') # RC4DATA = unhexlify(b'D4B718BBBA9CFB7D0192A58F9E2D146A' # b'FC5DB29E4352DE05FC4CF2C1005804BB') # rc4 = ARC4.new(RC4KEY) # HKEY = rc4.decrypt(RC4DATA) h = hmac.new(unhexlify(self.HKEY), (cid + self.deviceid).encode("utf-8"), digestmod=hashlib.sha256) enckey = h.digest() aes = AES.new(enckey, AES.MODE_ECB) rawvideokey = aes.decrypt(encvideokey) return rawvideokey def send(self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None): resp = Response() resp.status_code = 200 ticket = re.findall(r"abematv-license://(.*)", request.url)[0] resp._content = self._get_videokey_from_ticket(ticket) return resp def close(self): return @pluginmatcher(re.compile(r""" https?://abema\.tv/( now-on-air/(?P[^?]+) | video/episode/(?P[^?]+) | channels/.+?/slots/(?P[^?]+) ) """, re.VERBOSE)) class AbemaTV(Plugin): _CHANNEL = "https://api.abema.io/v1/channels" _USER_API = "https://api.abema.io/v1/users" _PRGM_API = "https://api.abema.io/v1/video/programs/{0}" _SLOTS_API = "https://api.abema.io/v1/media/slots/{0}" _PRGM3U8 = "https://vod-abematv.akamaized.net/program/{0}/playlist.m3u8" _SLOTM3U8 = "https://vod-abematv.akamaized.net/slot/{0}/playlist.m3u8" SECRETKEY = (b"v+Gjs=25Aw5erR!J8ZuvRrCx*rGswhB&qdHd_SYerEWdU&a?3DzN9B" b"Rbp5KwY4hEmcj5#fykMjJ=AuWz5GSMY-d@H7DMEh3M@9n2G552Us$$" b"k9cD=3TxwWe86!x#Zyhe") _USER_SCHEMA = validate.Schema({"profile": {"userId": validate.text}, "token": validate.text}) _CHANNEL_SCHEMA = validate.Schema({"channels": [{"id": validate.text, "name": validate.text, "playback": {validate.optional("dash"): validate.text, "hls": validate.text}}]}) _PRGM_SCHEMA = validate.Schema({"terms": [{validate.optional("onDemandType"): int}]}) _SLOT_SCHEMA = validate.Schema({"slot": {"flags": {validate.optional("timeshiftFree"): bool}}}) def __init__(self, url): super().__init__(url) self.session.http.headers.update({'User-Agent': useragents.CHROME}) def _generate_applicationkeysecret(self, deviceid): deviceid = deviceid.encode("utf-8") # for python3 # plus 1 hour and drop minute and secs # for python3 : floor division ts_1hour = (int(time.time()) + 60 * 60) // 3600 * 3600 time_struct = time.gmtime(ts_1hour) ts_1hour_str = str(ts_1hour).encode("utf-8") h = hmac.new(self.SECRETKEY, digestmod=hashlib.sha256) h.update(self.SECRETKEY) tmp = h.digest() for i in range(time_struct.tm_mon): h = hmac.new(self.SECRETKEY, digestmod=hashlib.sha256) h.update(tmp) tmp = h.digest() h = hmac.new(self.SECRETKEY, digestmod=hashlib.sha256) h.update(urlsafe_b64encode(tmp).rstrip(b"=") + deviceid) tmp = h.digest() for i in range(time_struct.tm_mday % 5): h = hmac.new(self.SECRETKEY, digestmod=hashlib.sha256) h.update(tmp) tmp = h.digest() h = hmac.new(self.SECRETKEY, digestmod=hashlib.sha256) h.update(urlsafe_b64encode(tmp).rstrip(b"=") + ts_1hour_str) tmp = h.digest() for i in range(time_struct.tm_hour % 5): # utc hour h = hmac.new(self.SECRETKEY, digestmod=hashlib.sha256) h.update(tmp) tmp = h.digest() return urlsafe_b64encode(tmp).rstrip(b"=").decode("utf-8") def _is_playable(self, vtype, vid): auth_header = {"Authorization": "Bearer " + self.usertoken} if vtype == "episode": res = self.session.http.get(self._PRGM_API.format(vid), headers=auth_header) jsonres = self.session.http.json(res, schema=self._PRGM_SCHEMA) playable = False for item in jsonres["terms"]: if item.get("onDemandType", False) == 3: playable = True return playable elif vtype == "slots": res = self.session.http.get(self._SLOTS_API.format(vid), headers=auth_header) jsonres = self.session.http.json(res, schema=self._SLOT_SCHEMA) return jsonres["slot"]["flags"].get("timeshiftFree", False) is True def _get_streams(self): deviceid = str(uuid.uuid4()) appkeysecret = self._generate_applicationkeysecret(deviceid) json_data = {"deviceId": deviceid, "applicationKeySecret": appkeysecret} res = self.session.http.post(self._USER_API, json=json_data) jsonres = self.session.http.json(res, schema=self._USER_SCHEMA) self.usertoken = jsonres['token'] # for authorzation matchresult = self.match if matchresult.group("onair"): onair = matchresult.group("onair") if onair == "news-global": self._CHANNEL = update_qsd(self._CHANNEL, {"division": "1"}) res = self.session.http.get(self._CHANNEL) jsonres = self.session.http.json(res, schema=self._CHANNEL_SCHEMA) channels = jsonres["channels"] for channel in channels: if onair == channel["id"]: break else: raise NoStreamsError(self.url) playlisturl = channel["playback"]["hls"] elif matchresult.group("episode"): episode = matchresult.group("episode") if not self._is_playable("episode", episode): log.error("Premium stream is not playable") return {} playlisturl = self._PRGM3U8.format(episode) elif matchresult.group("slots"): slots = matchresult.group("slots") if not self._is_playable("slots", slots): log.error("Premium stream is not playable") return {} playlisturl = self._SLOTM3U8.format(slots) log.debug("URL={0}".format(playlisturl)) # hook abematv private protocol self.session.http.mount("abematv-license://", AbemaTVLicenseAdapter(self.session, deviceid, self.usertoken)) return AbemaTVHLSStream.parse_variant_playlist(self.session, playlisturl) __plugin__ = AbemaTV ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/adultswim.py0000644000175100001710000001441600000000000022237 0ustar00runnerdockerimport logging import re from urllib.parse import urlparse, urlunparse from streamlink.plugin import Plugin, PluginError, pluginmatcher from streamlink.plugin.api import validate from streamlink.stream.hls import HLSStream from streamlink.utils.parse import parse_json log = logging.getLogger(__name__) @pluginmatcher(re.compile(r""" https?://(?:www\.)?adultswim\.com /(streams|videos) (?:/([^/]+))? (?:/([^/]+))? """, re.VERBOSE)) class AdultSwim(Plugin): token_url = 'https://token.ngtv.io/token/token_spe' video_data_url = 'https://www.adultswim.com/api/shows/v1/media/{0}/desktop' app_id_js_url_re = re.compile( r'''''' ) truncate_url_re = re.compile(r'''(.*)/\w+/?''') _api_schema = validate.Schema({ 'media': { 'desktop': { validate.text: { 'url': validate.url() } } }}, validate.get('media'), validate.get('desktop'), validate.filter(lambda k, v: k in ['unprotected', 'bulkaes']) ) _stream_data_schema = validate.Schema({ 'props': {'__REDUX_STATE__': {'streams': [{ 'id': validate.text, 'stream': validate.text, }]}}}, validate.get('props'), validate.get('__REDUX_STATE__'), validate.get('streams'), ) _token_schema = validate.Schema( validate.any( {'auth': {'token': validate.text}}, {'auth': {'error': {'message': validate.text}}}, ), validate.get('auth'), ) _video_data_schema = validate.Schema({ 'props': {'pageProps': {'__APOLLO_STATE__': { validate.text: { validate.optional('id'): validate.text, validate.optional('slug'): validate.text, } }}}}, validate.get('props'), validate.get('pageProps'), validate.get('__APOLLO_STATE__'), validate.filter(lambda k, v: k.startswith('Video:')), ) def _get_stream_data(self, id): res = self.session.http.get(self.url) m = self.json_data_re.search(res.text) if m and m.group(1): streams = parse_json(m.group(1), schema=self._stream_data_schema) else: raise PluginError("Failed to get json_data") for stream in streams: if 'id' in stream: if id == stream['id'] and 'stream' in stream: return stream['stream'] def _get_video_data(self, slug): m = self.truncate_url_re.search(self.url) if m and m.group(1): log.debug("Truncated URL={0}".format(m.group(1))) else: raise PluginError("Failed to truncate URL") res = self.session.http.get(m.group(1)) m = self.json_data_re.search(res.text) if m and m.group(1): videos = parse_json(m.group(1), schema=self._video_data_schema) else: raise PluginError("Failed to get json_data") for video in videos: if 'slug' in videos[video]: if slug == videos[video]['slug'] and 'id' in videos[video]: return videos[video]['id'] def _get_token(self, path): res = self.session.http.get(self.url) m = self.app_id_js_url_re.search(res.text) app_id_js_url = m and m.group(1) if not app_id_js_url: raise PluginError("Could not determine app_id_js_url") log.debug("app_id_js_url={0}".format(app_id_js_url)) res = self.session.http.get(app_id_js_url) m = self.app_id_re.search(res.text) app_id = m and m.group(1) if not app_id: raise PluginError("Could not determine app_id") log.debug("app_id={0}".format(app_id)) res = self.session.http.get(self.token_url, params=dict( format='json', appId=app_id, path=path, )) token_data = self.session.http.json(res, schema=self._token_schema) if 'error' in token_data: raise PluginError(token_data['error']['message']) return token_data['token'] def _get_streams(self): url_type, show_name, episode_name = self.match.groups() if url_type == 'streams' and not show_name: url_type = 'live-stream' elif not show_name: raise PluginError("Missing show_name for url_type: {0}".format( url_type, )) log.debug("URL type={0}".format(url_type)) if url_type == 'live-stream': video_id = self._get_stream_data(url_type) elif url_type == 'streams': video_id = self._get_stream_data(show_name) elif url_type == 'videos': if show_name is None or episode_name is None: raise PluginError( "Missing show_name or episode_name for url_type: {0}".format( url_type, ) ) video_id = self._get_video_data(episode_name) else: raise PluginError("Unrecognised url_type: {0}".format(url_type)) if video_id is None: raise PluginError("Could not find video_id") log.debug("Video ID={0}".format(video_id)) res = self.session.http.get(self.video_data_url.format(video_id)) url_data = self.session.http.json(res, schema=self._api_schema) if 'unprotected' in url_data: url = url_data['unprotected']['url'] elif 'bulkaes' in url_data: url_parsed = urlparse(url_data['bulkaes']['url']) token = self._get_token(url_parsed.path) url = urlunparse(( url_parsed.scheme, url_parsed.netloc, url_parsed.path, url_parsed.params, "{0}={1}".format('hdnts', token), url_parsed.fragment, )) else: raise PluginError("Could not find a usable URL in url_data") log.debug("URL={0}".format(url)) return HLSStream.parse_variant_playlist(self.session, url) __plugin__ = AdultSwim ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/afreeca.py0000644000175100001710000001614600000000000021616 0ustar00runnerdockerimport logging import re from streamlink.plugin import Plugin, PluginArgument, PluginArguments, pluginmatcher from streamlink.plugin.api import validate from streamlink.stream.hls import HLSStream from streamlink.stream.hls import HLSStreamReader, HLSStreamWriter log = logging.getLogger(__name__) class AfreecaHLSStreamWriter(HLSStreamWriter): def should_filter_sequence(self, sequence): return "preloading" in sequence.segment.uri or super().should_filter_sequence(sequence) class AfreecaHLSStreamReader(HLSStreamReader): __writer__ = AfreecaHLSStreamWriter class AfreecaHLSStream(HLSStream): __reader__ = AfreecaHLSStreamReader @pluginmatcher(re.compile( r"https?://play\.afreecatv\.com/(?P\w+)(?:/(?P:\d+))?" )) class AfreecaTV(Plugin): _re_bno = re.compile(r"var nBroadNo = (?P\d+);") CHANNEL_API_URL = "http://live.afreecatv.com/afreeca/player_live_api.php" CHANNEL_RESULT_OK = 1 QUALITYS = ["original", "hd", "sd"] QUALITY_WEIGHTS = { "original": 1080, "hd": 720, "sd": 480, } _schema_channel = validate.Schema( { "CHANNEL": { "RESULT": validate.transform(int), validate.optional("BPWD"): str, validate.optional("BNO"): str, validate.optional("RMD"): str, validate.optional("AID"): str, validate.optional("CDN"): str, } }, validate.get("CHANNEL") ) _schema_stream = validate.Schema( { validate.optional("view_url"): validate.url( scheme=validate.any("rtmp", "http") ), "stream_status": str, } ) arguments = PluginArguments( PluginArgument( "username", sensitive=True, requires=["password"], metavar="USERNAME", help="The username used to register with afreecatv.com." ), PluginArgument( "password", sensitive=True, metavar="PASSWORD", help="A afreecatv.com account password to use with --afreeca-username." ), PluginArgument( "purge-credentials", action="store_true", help=""" Purge cached AfreecaTV credentials to initiate a new session and reauthenticate. """), ) def __init__(self, url): super().__init__(url) self._authed = ( self.session.http.cookies.get("PdboxBbs") and self.session.http.cookies.get("PdboxSaveTicket") and self.session.http.cookies.get("PdboxTicket") and self.session.http.cookies.get("PdboxUser") and self.session.http.cookies.get("RDB") ) @classmethod def stream_weight(cls, key): weight = cls.QUALITY_WEIGHTS.get(key) if weight: return weight, "afreeca" return Plugin.stream_weight(key) def _get_channel_info(self, broadcast, username): data = { "bid": username, "bno": broadcast, "from_api": "0", "mode": "landing", "player_type": "html5", "pwd": "", "stream_type": "common", "type": "live", } res = self.session.http.post(self.CHANNEL_API_URL, data=data) return self.session.http.json(res, schema=self._schema_channel) def _get_hls_key(self, broadcast, username, quality): data = { "bid": username, "bno": broadcast, "from_api": "0", "mode": "landing", "player_type": "html5", "pwd": "", "quality": quality, "stream_type": "common", "type": "aid", } res = self.session.http.post(self.CHANNEL_API_URL, data=data) return self.session.http.json(res, schema=self._schema_channel) def _get_stream_info(self, broadcast, quality, rmd): params = { "return_type": "gs_cdn_pc_web", "broad_key": f"{broadcast}-common-{quality}-hls", } res = self.session.http.get(f"{rmd}/broad_stream_assign.html", params=params) return self.session.http.json(res, schema=self._schema_stream) def _get_hls_stream(self, broadcast, username, quality, rmd): keyjson = self._get_hls_key(broadcast, username, quality) if keyjson["RESULT"] != self.CHANNEL_RESULT_OK: return key = keyjson["AID"] info = self._get_stream_info(broadcast, quality, rmd) if "view_url" in info: return AfreecaHLSStream(self.session, info["view_url"], params={"aid": key}) def _login(self, username, password): data = { "szWork": "login", "szType": "json", "szUid": username, "szPassword": password, "isSaveId": "true", "isSavePw": "false", "isSaveJoin": "false", "isLoginRetain": "Y", } res = self.session.http.post("https://login.afreecatv.com/app/LoginAction.php", data=data) data = self.session.http.json(res) log.trace(f"{data!r}") if data["RESULT"] == self.CHANNEL_RESULT_OK: self.save_cookies() return True else: return False def _get_streams(self): login_username = self.get_option("username") login_password = self.get_option("password") self.session.http.headers.update({"Referer": self.url, "Origin": "http://play.afreecatv.com"}) if self.options.get("purge_credentials"): self.clear_cookies() self._authed = False log.info("All credentials were successfully removed") if self._authed: log.debug("Attempting to authenticate using cached cookies") elif login_username and login_password: log.debug("Attempting to login using username and password") if self._login(login_username, login_password): log.info("Login was successful") else: log.error("Failed to login") m = self.match.groupdict() username = m["username"] bno = m["bno"] if bno is None: res = self.session.http.get(self.url) m = self._re_bno.search(res.text) if not m: log.error("Could not find broadcast number.") return bno = m.group("bno") channel = self._get_channel_info(bno, username) log.trace(f"{channel!r}") if channel.get("BPWD") == "Y": log.error("Stream is Password-Protected") return elif channel.get("RESULT") == -6: log.error("Login required") return elif channel.get("RESULT") != self.CHANNEL_RESULT_OK: return (broadcast, rmd) = (channel["BNO"], channel["RMD"]) if not (broadcast and rmd): return for qkey in self.QUALITYS: hls_stream = self._get_hls_stream(broadcast, username, qkey, rmd) if hls_stream: yield qkey, hls_stream __plugin__ = AfreecaTV ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/albavision.py0000644000175100001710000001277000000000000022356 0ustar00runnerdockerimport logging import re import time from streamlink.plugin import Plugin, pluginmatcher from streamlink.plugin.api import validate from streamlink.stream.hls import HLSStream from streamlink.utils.url import update_qsd log = logging.getLogger(__name__) @pluginmatcher(re.compile(r""" https?://(?:www\.)? ( antena7\.com\.do | atv\.pe | c9n\.com\.py | canal10\.com\.ni | canal12\.com\.sv | chapintv\.com | elnueve\.com\.ar | redbolivision\.tv\.bo | repretel\.com | rts\.com\.ec | snt\.com\.py | tvc\.com\.ec | vtv\.com\.hn ) / (?: (?: en-?vivo(?:-atv(?:mas)?|-canal-?\d{1,2})? ) | upptv ) (?:/|\#)?$ """, re.VERBOSE)) class Albavision(Plugin): def __init__(self, url): super().__init__(url) self._page = None @property def page(self): if self._page is None: self._page = self.session.http.get(self.url, schema=validate.Schema( validate.parse_html(), )) return self._page def _is_token_based_site(self): schema = validate.Schema( validate.xml_xpath_string(".//script[contains(text(), 'jQuery.get')]/text()"), ) is_token_based_site = validate.validate(schema, self.page) is not None log.debug(f"is_token_based_site={is_token_based_site}") return is_token_based_site def _get_live_url(self): live_url_re = re.compile(r"""LIVE_URL\s*=\s*['"]([^'"]+)['"]""") schema = validate.Schema( validate.xml_xpath_string(".//script[contains(text(), 'LIVE_URL')]/text()"), validate.any(None, validate.all( validate.transform(live_url_re.search), validate.any(None, validate.all( validate.get(1), validate.url(), )), )), ) live_url = validate.validate(schema, self.page) log.debug(f"live_url={live_url}") return live_url def _get_token_req_url(self): token_req_host_re = re.compile(r"""jQuery\.get\s*\(['"]([^'"]+)['"]""") schema = validate.Schema( validate.xml_xpath_string(".//script[contains(text(), 'LIVE_URL')]/text()"), validate.any(None, validate.all( validate.transform(token_req_host_re.search), validate.any(None, validate.all( validate.get(1), validate.url(), )), )), ) token_req_host = validate.validate(schema, self.page) log.debug(f"token_req_host={token_req_host}") token_req_str_re = re.compile(r"""Math\.floor\(Date\.now\(\)\s*/\s*3600000\),\s*['"]([^'"]+)['"]""") schema = validate.Schema( validate.xml_xpath_string(".//script[contains(text(), 'LIVE_URL')]/text()"), validate.any(None, validate.all( validate.transform(token_req_str_re.search), validate.any(None, validate.all( validate.get(1), str, )), )), ) token_req_str = validate.validate(schema, self.page) log.debug(f"token_req_str={token_req_str}") if not token_req_str: return date = int(time.time() // 3600) token_req_token = self.transform_token(token_req_str, date) or self.transform_token(token_req_str, date - 1) if token_req_host and token_req_token: return update_qsd(token_req_host, {"rsk": token_req_token}) def _get_token(self): token_req_url = self._get_token_req_url() if not token_req_url: return res = self.session.http.get(token_req_url, schema=validate.Schema( validate.parse_json(), { "success": bool, validate.optional("error"): int, validate.optional("token"): str, }, )) if not res["success"]: if res["error"]: log.error(f"Token request failed with error: {res['error']}") else: log.error("Token request failed") return if not res["token"]: log.error("Token not found in response") return token = res["token"] log.debug(f"token={token}") return token @staticmethod def transform_token(token_in, date): token_out = list(token_in) offset = len(token_in) for i in range(offset - 1, -1, -1): p = (i * date) % offset # swap chars at p and i token_out[i], token_out[p] = token_out[p], token_out[i] token_out = ''.join(token_out) if token_out.endswith("OK"): return token_out[:-2] else: log.error(f"Invalid site token: {token_in} => {token_out}") def _get_streams(self): live_url = self._get_live_url() if not live_url: log.info("This stream may be off-air or not available in your country") return if self._is_token_based_site(): token = self._get_token() if not token: return return HLSStream.parse_variant_playlist(self.session, update_qsd(live_url, {"iut": token})) else: return HLSStream.parse_variant_playlist(self.session, live_url) __plugin__ = Albavision ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/app17.py0000644000175100001710000000364000000000000021153 0ustar00runnerdockerimport logging import re from streamlink.plugin import Plugin, pluginmatcher from streamlink.plugin.api import validate from streamlink.stream.hls import HLSStream from streamlink.stream.http import HTTPStream log = logging.getLogger(__name__) @pluginmatcher(re.compile( r"https?://17\.live/.+/live/(?P[^/&?]+)" )) class App17(Plugin): def _get_streams(self): channel = self.match.group("channel") self.session.http.headers.update({"Referer": self.url}) data = self.session.http.post( f"https://wap-api.17app.co/api/v1/lives/{channel}/viewers/alive", data={"liveStreamID": channel}, schema=validate.Schema( validate.parse_json(), validate.any( {"rtmpUrls": [{ validate.optional("provider"): validate.any(int, None), "url": validate.url(path=validate.endswith(".flv")), }]}, {"errorCode": int, "errorMessage": str}, ), ), acceptable_status=(200, 403, 404, 420)) log.trace(f"{data!r}") if data.get("errorCode"): log.error(f"{data['errorCode']} - {data['errorMessage'].replace('Something wrong: ', '')}") return flv_url = data["rtmpUrls"][0]["url"] yield "live", HTTPStream(self.session, flv_url) if "wansu-" in flv_url: hls_url = flv_url.replace(".flv", "/playlist.m3u8") else: hls_url = flv_url.replace("live-hdl", "live-hls").replace(".flv", ".m3u8") s = HLSStream.parse_variant_playlist(self.session, hls_url) if not s: yield "live", HLSStream(self.session, hls_url) else: if len(s) == 1: for _n, _s in s.items(): yield "live", _s else: yield from s.items() __plugin__ = App17 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/ard_live.py0000644000175100001710000000441400000000000022010 0ustar00runnerdockerimport logging import re from urllib.parse import urljoin from streamlink.plugin import Plugin, PluginError, pluginmatcher from streamlink.plugin.api import validate from streamlink.stream.hls import HLSStream from streamlink.stream.http import HTTPStream log = logging.getLogger(__name__) @pluginmatcher(re.compile( r"https?://((www|live)\.)?daserste\.de/" )) class ARDLive(Plugin): _QUALITY_MAP = { 4: "1080p", 3: "720p", 2: "540p", 1: "270p", 0: "180p" } def _get_streams(self): try: data_url = self.session.http.get(self.url, schema=validate.Schema( validate.parse_html(), validate.xml_find(".//*[@data-ctrl-player]"), validate.get("data-ctrl-player"), validate.transform(lambda s: s.replace("'", "\"")), validate.parse_json(), {"url": str}, validate.get("url") )) except PluginError: return data_url = urljoin(self.url, data_url) log.debug(f"Player URL: '{data_url}'") self.title, media = self.session.http.get(data_url, schema=validate.Schema( validate.parse_json(name="MEDIAINFO"), {"mc": { validate.optional("_title"): str, "_mediaArray": [validate.all( { "_mediaStreamArray": [validate.all( { "_quality": validate.any(str, int), "_stream": [validate.url()], }, validate.union_get("_quality", ("_stream", 0)) )] }, validate.get("_mediaStreamArray"), validate.transform(dict) )] }}, validate.get("mc"), validate.union_get("_title", ("_mediaArray", 0)) )) if media.get("auto"): yield from HLSStream.parse_variant_playlist(self.session, media.get("auto")).items() else: for quality, stream in media.items(): yield self._QUALITY_MAP.get(quality, quality), HTTPStream(self.session, stream) __plugin__ = ARDLive ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/ard_mediathek.py0000644000175100001710000000746400000000000023014 0ustar00runnerdockerimport logging import re from streamlink.plugin import Plugin, pluginmatcher from streamlink.plugin.api import validate from streamlink.stream.hls import HLSStream from streamlink.stream.http import HTTPStream log = logging.getLogger(__name__) @pluginmatcher(re.compile( r"https?://(?:(\w+\.)?ardmediathek\.de/|mediathek\.daserste\.de/)" )) class ARDMediathek(Plugin): _QUALITY_MAP = { 4: "1080p", 3: "720p", 2: "540p", 1: "360p", 0: "270p" } def _get_streams(self): data_json = self.session.http.get(self.url, schema=validate.Schema( validate.parse_html(), validate.xml_findtext(".//script[@id='fetchedContextValue'][@type='application/json']"), validate.any(None, validate.all( validate.parse_json(), {str: dict}, validate.transform(lambda obj: list(obj.items())), validate.filter(lambda item: item[0].startswith("https://api.ardmediathek.de/page-gateway/pages/")), validate.any(validate.get((0, 1)), []) )) )) if not data_json: return schema_data = validate.Schema({ "id": str, "widgets": validate.all( [dict], validate.filter(lambda item: item.get("mediaCollection")), validate.get(0), validate.any(None, validate.all( { "geoblocked": bool, "publicationService": { "name": str, }, "show": validate.any(None, validate.all( {"title": str}, validate.get("title") )), "title": str, "mediaCollection": { "embedded": { "_mediaArray": [validate.all( { "_mediaStreamArray": [validate.all( { "_quality": validate.any(str, int), "_stream": validate.url(), }, validate.union_get("_quality", "_stream") )] }, validate.get("_mediaStreamArray"), validate.transform(dict) )] } }, }, validate.union_get( "geoblocked", ("mediaCollection", "embedded", "_mediaArray", 0), ("publicationService", "name"), "title", "show", ) )) ) }) data = schema_data.validate(data_json) log.debug(f"Found media id: {data['id']}") if not data["widgets"]: log.info("The content is unavailable") return geoblocked, media, self.author, self.title, show = data["widgets"] if geoblocked: log.info("The content is not available in your region") return if show: self.title = f"{show}: {self.title}" if media.get("auto"): yield from HLSStream.parse_variant_playlist(self.session, media.get("auto")).items() else: for quality, stream in media.items(): yield self._QUALITY_MAP.get(quality, quality), HTTPStream(self.session, stream) __plugin__ = ARDMediathek ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/artetv.py0000644000175100001710000000423600000000000021532 0ustar00runnerdockerimport logging import re from operator import itemgetter from streamlink.plugin import Plugin, pluginmatcher from streamlink.plugin.api import validate from streamlink.stream.hls import HLSStream log = logging.getLogger(__name__) @pluginmatcher(re.compile(r""" https?://(?:\w+\.)?arte\.tv/(?:guide/)? (?P[a-z]{2})/ (?: (?:videos/)?(?P(?!RC-|videos)[^/]+?)/.+ | (?:direct|live) ) """, re.VERBOSE)) class ArteTV(Plugin): API_URL = "https://api.arte.tv/api/player/v2/config/{0}/{1}" API_TOKEN = "MzYyZDYyYmM1Y2Q3ZWRlZWFjMmIyZjZjNTRiMGY4MzY4NzBhOWQ5YjE4MGQ1NGFiODJmOTFlZDQwN2FkOTZjMQ" def _get_streams(self): language = self.match.group("language") video_id = self.match.group("video_id") json_url = self.API_URL.format(language, video_id or "LIVE") headers = { "Authorization": f"Bearer {self.API_TOKEN}" } streams, metadata = self.session.http.get(json_url, headers=headers, schema=validate.Schema( validate.parse_json(), {"data": {"attributes": { "streams": validate.any( [], [ validate.all( { "url": validate.url(), "slot": int, "protocol": validate.any("HLS", "HLS_NG"), }, validate.union_get("slot", "protocol", "url") ) ] ), "metadata": { "title": str, "subtitle": validate.any(None, str) } }}}, validate.get(("data", "attributes")), validate.union_get("streams", "metadata") )) if not streams: return self.title = f"{metadata['title']} - {metadata['subtitle']}" if metadata["subtitle"] else metadata["title"] for slot, protocol, url in sorted(streams, key=itemgetter(0)): return HLSStream.parse_variant_playlist(self.session, url) __plugin__ = ArteTV ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/atresplayer.py0000644000175100001710000000504500000000000022557 0ustar00runnerdockerimport logging import re from streamlink.plugin import Plugin, pluginmatcher from streamlink.plugin.api import validate from streamlink.stream.dash import DASHStream from streamlink.stream.hls import HLSStream from streamlink.utils.data import search_dict from streamlink.utils.url import update_scheme log = logging.getLogger(__name__) @pluginmatcher(re.compile( r"https?://(?:www\.)?atresplayer\.com/" )) class AtresPlayer(Plugin): state_re = re.compile(r"""window.__PRELOADED_STATE__\s*=\s*({.*?});""", re.DOTALL) channel_id_schema = validate.Schema( validate.transform(state_re.search), validate.any( None, validate.all( validate.get(1), validate.parse_json(), validate.transform(search_dict, key="href"), ) ) ) player_api_schema = validate.Schema( validate.any( None, validate.all( validate.parse_json(), validate.transform(search_dict, key="urlVideo"), ) ) ) stream_schema = validate.Schema( validate.parse_json(), {"sources": [ validate.all({ "src": validate.url(), validate.optional("type"): validate.text }) ]}, validate.get("sources")) def __init__(self, url): # must be HTTPS super().__init__(update_scheme("https://", url)) def _get_streams(self): api_urls = self.session.http.get(self.url, schema=self.channel_id_schema) _api_url = list(api_urls)[0] log.debug("API URL: {0}".format(_api_url)) player_api_url = self.session.http.get(_api_url, schema=self.player_api_schema) for api_url in player_api_url: log.debug("Player API URL: {0}".format(api_url)) for source in self.session.http.get(api_url, schema=self.stream_schema): log.debug("Stream source: {0} ({1})".format(source['src'], source.get("type", "n/a"))) if "type" not in source or source["type"] == "application/vnd.apple.mpegurl": streams = HLSStream.parse_variant_playlist(self.session, source["src"]) if not streams: yield "live", HLSStream(self.session, source["src"]) else: yield from streams.items() elif source["type"] == "application/dash+xml": yield from DASHStream.parse_manifest(self.session, source["src"]).items() __plugin__ = AtresPlayer ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/bbciplayer.py0000644000175100001710000002023500000000000022336 0ustar00runnerdockerimport base64 import logging import re from collections import defaultdict from hashlib import sha1 from urllib.parse import urlparse, urlunparse from streamlink.plugin import Plugin, PluginArgument, PluginArguments, PluginError, pluginmatcher from streamlink.plugin.api import validate from streamlink.stream.dash import DASHStream from streamlink.stream.hls import HLSStream from streamlink.utils.parse import parse_json log = logging.getLogger(__name__) @pluginmatcher(re.compile(r""" https?://(?:www\.)?bbc\.co\.uk/iplayer/ ( episode/(?P\w+) | live/(?P\w+) ) """, re.VERBOSE)) class BBCiPlayer(Plugin): """ Allows streaming of live channels from bbc.co.uk/iplayer/live/* and of iPlayer programmes from bbc.co.uk/iplayer/episode/* """ mediator_re = re.compile( r'window\.__IPLAYER_REDUX_STATE__\s*=\s*({.*?});', re.DOTALL) state_re = re.compile(r'window.__IPLAYER_REDUX_STATE__\s*=\s*({.*?});') account_locals_re = re.compile(r'window.bbcAccount.locals\s*=\s*({.*?});') hash = base64.b64decode(b"N2RmZjc2NzFkMGM2OTdmZWRiMWQ5MDVkOWExMjE3MTk5MzhiOTJiZg==") api_url = "https://open.live.bbc.co.uk/mediaselector/6/select/version/2.0/mediaset/" \ "{platform}/vpid/{vpid}/format/json/atk/{vpid_hash}/asn/1/" platforms = ("pc", "iptv-all") session_url = "https://session.bbc.com/session" auth_url = "https://account.bbc.com/signin" mediator_schema = validate.Schema( { "versions": [{"id": validate.text}] }, validate.get("versions"), validate.get(0), validate.get("id") ) mediaselector_schema = validate.Schema( validate.parse_json(), {"media": [ {"connection": validate.all([{ validate.optional("href"): validate.url(), validate.optional("transferFormat"): validate.text }], validate.filter(lambda c: c.get("href"))), "kind": validate.text} ]}, validate.get("media"), validate.filter(lambda x: x["kind"] == "video") ) arguments = PluginArguments( PluginArgument( "username", requires=["password"], metavar="USERNAME", help="The username used to register with bbc.co.uk." ), PluginArgument( "password", sensitive=True, metavar="PASSWORD", help="A bbc.co.uk account password to use with --bbciplayer-username.", prompt="Enter bbc.co.uk account password" ), PluginArgument( "hd", action="store_true", help=""" Prefer HD streams over local SD streams, some live programmes may not be broadcast in HD. """ ), ) def __init__(self, url): super().__init__(url) self.url = urlunparse(urlparse(self.url)._replace(scheme="https")) @classmethod def _hash_vpid(cls, vpid): return sha1(cls.hash + str(vpid).encode("utf8")).hexdigest() def find_vpid(self, url, res=None): """ Find the Video Packet ID in the HTML for the provided URL :param url: URL to download, if res is not provided. :param res: Provide a cached version of the HTTP response to search :type url: string :type res: requests.Response :return: Video Packet ID for a Programme in iPlayer :rtype: string """ log.debug(f"Looking for vpid on {url}") # Use pre-fetched page if available res = res or self.session.http.get(url) m = self.mediator_re.search(res.text) vpid = m and parse_json(m.group(1), schema=self.mediator_schema) return vpid def find_tvip(self, url, master=False): log.debug("Looking for {0} tvip on {1}".format("master" if master else "", url)) res = self.session.http.get(url) m = self.state_re.search(res.text) data = m and parse_json(m.group(1)) if data: channel = data.get("channel") if master: return channel.get("masterBrand") return channel.get("id") def mediaselector(self, vpid): urls = defaultdict(set) for platform in self.platforms: url = self.api_url.format(vpid=vpid, vpid_hash=self._hash_vpid(vpid), platform=platform) log.debug(f"Info API request: {url}") medias = self.session.http.get(url, schema=self.mediaselector_schema) for media in medias: for connection in media["connection"]: urls[connection.get("transferFormat")].add(connection["href"]) for stream_type, urls in urls.items(): log.debug(f"{len(urls)} {stream_type} streams") for url in list(urls): try: if stream_type == "hls": yield from HLSStream.parse_variant_playlist(self.session, url).items() if stream_type == "dash": yield from DASHStream.parse_manifest(self.session, url).items() log.debug(f" OK: {url}") except Exception: log.debug(f" FAIL: {url}") def login(self, ptrt_url): """ Create session using BBC ID. See https://www.bbc.co.uk/usingthebbc/account/ :param ptrt_url: The snapback URL to redirect to after successful authentication :type ptrt_url: string :return: Whether authentication was successful :rtype: bool """ def auth_check(res): return ptrt_url in ([h.url for h in res.history] + [res.url]) # make the session request to get the correct cookies session_res = self.session.http.get( self.session_url, params=dict(ptrt=ptrt_url) ) if auth_check(session_res): log.debug("Already authenticated, skipping authentication") return True res = self.session.http.post( self.auth_url, params=urlparse(session_res.url).query, data=dict( jsEnabled=True, username=self.get_option("username"), password=self.get_option('password'), attempts=0 ), headers={"Referer": self.url}) return auth_check(res) def _get_streams(self): if not self.get_option("username"): log.error( "BBC iPlayer requires an account you must login using " "--bbciplayer-username and --bbciplayer-password") return log.info( "A TV License is required to watch BBC iPlayer streams, see the BBC website for more " "information: https://www.bbc.co.uk/iplayer/help/tvlicence") if not self.login(self.url): log.error( "Could not authenticate, check your username and password") return episode_id = self.match.group("episode_id") channel_name = self.match.group("channel_name") if episode_id: log.debug(f"Loading streams for episode: {episode_id}") vpid = self.find_vpid(self.url) if vpid: log.debug(f"Found VPID: {vpid}") yield from self.mediaselector(vpid) else: log.error(f"Could not find VPID for episode {episode_id}") elif channel_name: log.debug(f"Loading stream for live channel: {channel_name}") if self.get_option("hd"): tvip = self.find_tvip(self.url, master=True) + "_hd" if tvip: log.debug(f"Trying HD stream {tvip}...") try: yield from self.mediaselector(tvip) except PluginError: log.error("Failed to get HD streams, falling back to SD") else: return tvip = self.find_tvip(self.url) if tvip: log.debug(f"Found TVIP: {tvip}") yield from self.mediaselector(tvip) __plugin__ = BBCiPlayer ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/bfmtv.py0000644000175100001710000000750200000000000021342 0ustar00runnerdockerimport logging import re from urllib.parse import urljoin from streamlink.plugin import Plugin, PluginError, pluginmatcher from streamlink.plugin.api import validate from streamlink.plugins.brightcove import BrightcovePlayer from streamlink.stream.http import HTTPStream log = logging.getLogger(__name__) @pluginmatcher(re.compile( r"https?://(?:[\w-]+\.)+(?:bfmtv|01net)\.com" )) class BFMTV(Plugin): def _brightcove(self, account_id, video_id): log.debug(f"Account ID: {account_id}") log.debug(f"Video ID: {video_id}") player = BrightcovePlayer(self.session, account_id) return dict(player.get_streams(video_id)) def _streams_brightcove(self, root): schema_brightcove = validate.Schema(validate.any( validate.all( validate.xml_find(".//*[@accountid][@videoid]"), validate.union_get("accountid", "videoid") ), validate.all( validate.xml_find(".//*[@data-account][@data-video-id]"), validate.union_get("data-account", "data-video-id") ) )) try: account_id, video_id = schema_brightcove.validate(root) except PluginError: return return self._brightcove(account_id, video_id) def _streams_brightcove_js(self, root): re_js_src = re.compile(r"^[\w/]+/main\.\w+\.js$") re_js_brightcove_video = re.compile( r'i\?\([A-Z]="[^"]+",y="(?P[0-9]+).*"data-account"\s*:\s*"(?P[0-9]+)', ) schema_brightcove_js = validate.Schema( validate.xml_findall(r".//script[@src]"), validate.filter(lambda elem: re_js_src.search(elem.attrib.get("src"))), validate.get(0), str, validate.transform(lambda src: urljoin(self.url, src)) ) schema_brightcove_js2 = validate.Schema( validate.transform(re_js_brightcove_video.search), validate.union_get("account_id", "video_id") ) try: js_url = schema_brightcove_js.validate(root) log.debug(f"JS URL: {js_url}") account_id, video_id = self.session.http.get(js_url, schema=schema_brightcove_js2) except (PluginError, TypeError): return return self._brightcove(account_id, video_id) def _streams_dailymotion(self, root): schema_dailymotion = validate.Schema( validate.xml_xpath_string(".//iframe[contains(@src,'dailymotion.com/')][1]/@src"), str, validate.transform(lambda src: src.split("/")[-1]) ) try: video_id = schema_dailymotion.validate(root) except PluginError: return log.debug(f"Found dailymotion video ID: {video_id}") return self.session.streams(f"https://www.dailymotion.com/embed/video/{video_id}") def _streams_audio(self, root): schema_audio = validate.Schema(validate.any( validate.all( validate.xml_xpath_string(".//audio/source[contains(@src,'.mp3')][1]/@src"), str ), validate.all( validate.xml_xpath_string(".//div[contains(@class,'audio-player')][@data-media-url][1]/@data-media-url"), str ) )) try: audio_url = schema_audio.validate(root) except PluginError: return return {"audio": HTTPStream(self.session, audio_url)} def _get_streams(self): root = self.session.http.get(self.url, schema=validate.Schema( validate.parse_html() )) return ( self._streams_brightcove(root) or self._streams_dailymotion(root) or self._streams_brightcove_js(root) or self._streams_audio(root) ) __plugin__ = BFMTV ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/bigo.py0000644000175100001710000000167700000000000021153 0ustar00runnerdockerimport re from streamlink.plugin import Plugin, pluginmatcher from streamlink.plugin.api import useragents, validate from streamlink.stream.hls import HLSStream @pluginmatcher(re.compile( r"https?://(?:www\.)?bigo\.tv/([^/]+)$" )) class Bigo(Plugin): _api_url = "https://www.bigo.tv/OInterface/getVideoParam?bigoId={0}" _video_info_schema = validate.Schema({ "code": 0, "msg": "success", "data": { "videoSrc": validate.any(None, "", validate.url()) } }) def _get_streams(self): res = self.session.http.get( self._api_url.format(self.match.group(1)), allow_redirects=True, headers={"User-Agent": useragents.IPHONE_6} ) data = self.session.http.json(res, schema=self._video_info_schema) videourl = data["data"]["videoSrc"] if videourl: yield "live", HLSStream(self.session, videourl) __plugin__ = Bigo ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/bilibili.py0000644000175100001710000000514200000000000022001 0ustar00runnerdockerimport logging import re from urllib.parse import urlparse from streamlink.plugin import Plugin, pluginmatcher from streamlink.plugin.api import validate from streamlink.stream.http import HTTPStream log = logging.getLogger(__name__) API_URL = "https://api.live.bilibili.com/room/v1/Room/playUrl" ROOM_API = "https://api.live.bilibili.com/room/v1/Room/room_init?id={}" SHOW_STATUS_OFFLINE = 0 SHOW_STATUS_ONLINE = 1 SHOW_STATUS_ROUND = 2 STREAM_WEIGHTS = { "source": 1080 } _room_id_schema = validate.Schema( { "data": validate.any(None, { "room_id": int, "live_status": int }) }, validate.get("data") ) _room_stream_list_schema = validate.Schema( { "data": validate.any(None, { "durl": [{"url": validate.url()}] }) }, validate.get("data") ) @pluginmatcher(re.compile( r"https?://live\.bilibili\.com/(?P[^/]+)" )) class Bilibili(Plugin): @classmethod def stream_weight(cls, stream): if stream in STREAM_WEIGHTS: return STREAM_WEIGHTS[stream], "Bilibili" return Plugin.stream_weight(stream) def _get_streams(self): self.session.http.headers.update({'Referer': self.url}) channel = self.match.group("channel") res_room_id = self.session.http.get(ROOM_API.format(channel)) room_id_json = self.session.http.json(res_room_id, schema=_room_id_schema) room_id = room_id_json['room_id'] if room_id_json['live_status'] != SHOW_STATUS_ONLINE: return params = { 'cid': room_id, 'quality': '4', 'platform': 'web', } res = self.session.http.get(API_URL, params=params) room = self.session.http.json(res, schema=_room_stream_list_schema) if not room: return for stream_list in room["durl"]: name = "source" url = stream_list["url"] # check if the URL is available log.trace('URL={0}'.format(url)) r = self.session.http.get(url, retries=0, timeout=3, stream=True, acceptable_status=(200, 403, 404, 405)) p = urlparse(url) if r.status_code != 200: log.error('Netloc: {0} with error {1}'.format(p.netloc, r.status_code)) continue log.debug('Netloc: {0}'.format(p.netloc)) stream = HTTPStream(self.session, url) yield name, stream __plugin__ = Bilibili ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/bloomberg.py0000644000175100001710000001104300000000000022167 0ustar00runnerdockerimport logging import re from streamlink.plugin import Plugin, PluginError, pluginmatcher from streamlink.plugin.api import validate from streamlink.stream.hls import HLSStream log = logging.getLogger(__name__) @pluginmatcher(re.compile(r""" https?://(?:www\.)?bloomberg\.com/ (?: news/videos/[^/]+/[^/]+ | live/(?P.+)/? ) """, re.VERBOSE)) class Bloomberg(Plugin): LIVE_API_URL = "https://cdn.gotraffic.net/projector/latest/assets/config/config.min.json?v=1" VOD_API_URL = "https://www.bloomberg.com/api/embed?id={0}" _re_mp4_bitrate = re.compile(r".*_(?P[0-9]+)\.mp4") def _get_live_streams(self, data, channel): schema_live_ids = validate.Schema( {"live": {"channels": {"byChannelId": { channel: validate.all( {"liveId": str}, validate.get("liveId") ) }}}}, validate.get(("live", "channels", "byChannelId", channel)), ) try: live_id = schema_live_ids.validate(data) except PluginError: log.error(f"Could not find liveId for channel '{channel}'") return log.debug(f"Found liveId: {live_id}") return self.session.http.get(self.LIVE_API_URL, schema=validate.Schema( validate.parse_json(), {"livestreams": { live_id: { validate.optional("cdns"): validate.all( [{"streams": [{ "url": validate.url() }]}], validate.transform(lambda x: [urls["url"] for y in x for urls in y["streams"]]) ) } }}, validate.get(("livestreams", live_id, "cdns")) )) def _get_vod_streams(self, data): schema_vod_list = validate.Schema( validate.any( validate.all( {"video": {"videoStory": dict}}, validate.get(("video", "videoStory")) ), validate.all( {"quicktakeVideo": {"videoStory": dict}}, validate.get(("quicktakeVideo", "videoStory")) ) ), {"video": { "bmmrId": str }}, validate.get(("video", "bmmrId")) ) schema_url = validate.all( {"url": validate.url()}, validate.get("url") ) try: video_id = schema_vod_list.validate(data) except PluginError: log.error("Could not find videoId") return log.debug(f"Found videoId: {video_id}") vod_url = self.VOD_API_URL.format(video_id) secureStreams, streams, self.title = self.session.http.get(vod_url, schema=validate.Schema( validate.parse_json(), { validate.optional("secureStreams"): [schema_url], validate.optional("streams"): [schema_url], "title": str }, validate.union_get("secureStreams", "streams", "title") )) return secureStreams or streams def _get_streams(self): self.session.http.headers.update({ "authority": "www.bloomberg.com", "upgrade-insecure-requests": "1", "dnt": "1", "accept": ";".join([ "text/html,application/xhtml+xml,application/xml", "q=0.9,image/webp,image/apng,*/*", "q=0.8,application/signed-exchange", "v=b3" ]) }) try: data = self.session.http.get(self.url, schema=validate.Schema( validate.parse_html(), validate.xml_xpath_string(".//script[contains(text(),'window.__PRELOADED_STATE__')][1]/text()"), str, validate.transform(re.compile(r"^\s*window\.__PRELOADED_STATE__\s*=\s*({.+})\s*;?\s*$", re.DOTALL).search), validate.get(1), validate.parse_json() )) except PluginError: log.error("Could not find JSON data. Invalid URL or bot protection...") return channel = self.match.group("channel") if channel: streams = self._get_live_streams(data, channel) else: streams = self._get_vod_streams(data) if streams: # just return the first stream return HLSStream.parse_variant_playlist(self.session, streams[0]) __plugin__ = Bloomberg ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/booyah.py0000644000175100001710000001127700000000000021511 0ustar00runnerdockerimport logging import re import sys from urllib.parse import urljoin from streamlink.plugin import Plugin, pluginmatcher from streamlink.plugin.api import validate from streamlink.stream.hls import HLSStream from streamlink.stream.http import HTTPStream log = logging.getLogger(__name__) @pluginmatcher(re.compile( r"https?://(?:www\.)?booyah\.live/(?:(?Pchannels|clips|vods)/)?(?P[^?]+)" )) class Booyah(Plugin): auth_api_url = 'https://booyah.live/api/v3/auths/sessions' vod_api_url = 'https://booyah.live/api/v3/playbacks/{0}' live_api_url = 'https://booyah.live/api/v3/channels/{0}' streams_api_url = 'https://booyah.live/api/v3/channels/{0}/streams' auth_schema = validate.Schema({ 'expiry_time': int, 'uid': int, }) vod_schema = validate.Schema({ 'user': { 'nickname': str, }, 'playback': { 'name': str, 'endpoint_list': [{ 'stream_url': validate.url(), 'resolution': validate.all( int, validate.transform(lambda x: f'{x}p'), ), }], }, }) live_schema = validate.Schema({ 'user': { 'nickname': str, }, 'channel': { 'channel_id': int, 'name': str, 'is_streaming': bool, validate.optional('hostee'): { 'channel_id': int, 'nickname': str, }, }, }) @classmethod def stream_weight(cls, stream): if stream == "source": return sys.maxsize, "source" return super().stream_weight(stream) def do_auth(self): res = self.session.http.post(self.auth_api_url) self.session.http.json(res, self.auth_schema) def get_vod(self, id): res = self.session.http.get(self.vod_api_url.format(id)) user_data = self.session.http.json(res, schema=self.vod_schema) self.author = user_data['user']['nickname'] self.category = 'VOD' self.title = user_data['playback']['name'] for stream in user_data['playback']['endpoint_list']: if stream['stream_url'].endswith('.mp4'): yield stream['resolution'], HTTPStream( self.session, stream['stream_url'], ) else: yield stream['resolution'], HLSStream( self.session, stream['stream_url'], ) def get_live(self, id): res = self.session.http.get(self.live_api_url.format(id)) user_data = self.session.http.json(res, schema=self.live_schema) if user_data['channel']['is_streaming']: self.category = 'Live' stream_id = user_data['channel']['channel_id'] elif 'hostee' in user_data['channel']: self.category = f'Hosted by {user_data["channel"]["hostee"]["nickname"]}' stream_id = user_data['channel']['hostee']['channel_id'] else: log.info('User is offline') return self.author = user_data['user']['nickname'] self.title = user_data['channel']['name'] res = self.session.http.get(self.streams_api_url.format(stream_id)) streams = self.session.http.json(res, schema=validate.Schema({ "default_mirror": str, "mirror_list": [{ "name": str, "url_domain": validate.url(), }], "source_stream_url_path": str, "stream_addr_list": [{ "resolution": str, "url_path": str, }], })) mirror = ( next(filter(lambda item: item["name"] == streams["default_mirror"], streams["mirror_list"]), None) or next(iter(streams["mirror_list"]), None) ) if not mirror: return auto = next(filter(lambda item: item["resolution"] == "Auto", streams["stream_addr_list"]), None) if auto: yield from HLSStream.parse_variant_playlist(self.session, urljoin(mirror["url_domain"], auto["url_path"])).items() if streams["source_stream_url_path"]: yield "source", HLSStream(self.session, urljoin(mirror["url_domain"], streams["source_stream_url_path"])) def _get_streams(self): self.do_auth() url_data = self.match.groupdict() log.debug(f'ID={url_data["id"]}') if not url_data['type'] or url_data['type'] == 'channels': log.debug('Type=Live') return self.get_live(url_data['id']) else: log.debug('Type=VOD') return self.get_vod(url_data['id']) __plugin__ = Booyah ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/brightcove.py0000644000175100001710000000703700000000000022363 0ustar00runnerdockerimport logging import re from urllib.parse import urlparse from streamlink.plugin import Plugin, PluginError, pluginmatcher from streamlink.plugin.api import validate from streamlink.stream.hls import HLSStream from streamlink.stream.http import HTTPStream from streamlink.utils.parse import parse_qsd log = logging.getLogger(__name__) class BrightcovePlayer: URL_PLAYER = "https://players.brightcove.net/{account_id}/{player_id}/index.html?videoId={video_id}" URL_API = "https://edge.api.brightcove.com/playback/v1/accounts/{account_id}/videos/{video_id}" def __init__(self, session, account_id, player_id="default_default"): self.session = session self.account_id = account_id self.player_id = player_id self.title = None log.debug(f"Creating player for account {account_id} (player_id={player_id})") def get_streams(self, video_id): log.debug(f"Finding streams for video: {video_id}") player_url = self.URL_PLAYER.format( account_id=self.account_id, player_id=self.player_id, video_id=video_id ) policy_key = self.session.http.get( player_url, params={"videoId": video_id}, schema=validate.Schema( validate.transform(re.compile(r"""policyKey\s*:\s*(?P['"])(?P[\w-]+)(?P=q)""").search), validate.any(None, validate.get("key")) ) ) if not policy_key: raise PluginError("Could not find Brightcove policy key") log.debug(f"Found policy key: {policy_key}") self.session.http.headers.update({"Referer": player_url}) sources, self.title = self.session.http.get( self.URL_API.format(account_id=self.account_id, video_id=video_id), headers={"Accept": f"application/json;pk={policy_key}"}, schema=validate.Schema( validate.parse_json(), { "sources": [{ "src": validate.url(), validate.optional("type"): str, validate.optional("container"): str, validate.optional("height"): int, validate.optional("avg_bitrate"): int, }], validate.optional("name"): str, }, validate.union_get("sources", "name") ) ) for source in sources: if source.get("type") in ("application/vnd.apple.mpegurl", "application/x-mpegURL"): yield from HLSStream.parse_variant_playlist(self.session, source.get("src")).items() elif source.get("container") == "MP4": # determine quality name if source.get("height"): q = f"{source.get('height')}p" elif source.get("avg_bitrate"): q = f"{source.get('avg_bitrate') // 1000}k" else: q = "live" yield q, HTTPStream(self.session, source.get("src")) @pluginmatcher(re.compile( r"https?://players\.brightcove\.net/(?P[^/]+)/(?P[^/]+)/index\.html" )) class Brightcove(Plugin): def _get_streams(self): video_id = parse_qsd(urlparse(self.url).query).get("videoId") player = BrightcovePlayer(self.session, self.match.group("account_id"), self.match.group("player_id")) streams = dict(player.get_streams(video_id)) self.title = player.title return streams __plugin__ = Brightcove ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/btv.py0000644000175100001710000000277000000000000021021 0ustar00runnerdockerimport logging import re from streamlink.plugin import Plugin, pluginmatcher from streamlink.plugin.api import validate from streamlink.stream.hls import HLSStream from streamlink.utils.parse import parse_json log = logging.getLogger(__name__) @pluginmatcher(re.compile( r"https?://(?:www\.)?btvplus\.bg/live/?" )) class BTV(Plugin): api_url = "https://btvplus.bg/lbin/v3/btvplus/player_config.php" media_id_re = re.compile(r"media_id=(\d+)") src_re = re.compile(r"src: \"(http.*?)\"") api_schema = validate.Schema( validate.all( {"status": "ok", "config": validate.text}, validate.get("config"), validate.all( validate.transform(src_re.search), validate.any( None, validate.get(1), validate.url() ) ) ) ) def get_hls_url(self, media_id): res = self.session.http.get(self.api_url, params=dict(media_id=media_id)) return parse_json(res.text, schema=self.api_schema) def _get_streams(self): res = self.session.http.get(self.url) media_match = self.media_id_re.search(res.text) media_id = media_match and media_match.group(1) if media_id: log.debug(f"Found media id: {media_id}") stream_url = self.get_hls_url(media_id) if stream_url: return HLSStream.parse_variant_playlist(self.session, stream_url) __plugin__ = BTV ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/cbsnews.py0000644000175100001710000000176000000000000021670 0ustar00runnerdockerimport re from streamlink.plugin import Plugin, pluginmatcher from streamlink.plugin.api import validate from streamlink.stream.hls import HLSStream @pluginmatcher(re.compile( r"https?://www\.cbsnews\.com/live/" )) class CBSNews(Plugin): _re_default_payload = re.compile(r"CBSNEWS.defaultPayload = (\{.*)") _schema_items = validate.Schema( validate.transform(_re_default_payload.search), validate.any(None, validate.all( validate.get(1), validate.parse_json(), {"items": [validate.all({ "video": validate.url(), "format": "application/x-mpegURL" }, validate.get("video"))]}, validate.get("items") )) ) def _get_streams(self): items = self.session.http.get(self.url, schema=self._schema_items) if items: for hls_url in items: yield from HLSStream.parse_variant_playlist(self.session, hls_url).items() __plugin__ = CBSNews ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/cdnbg.py0000644000175100001710000000620600000000000021301 0ustar00runnerdockerimport logging import re from html import unescape as html_unescape from urllib.parse import urlparse from streamlink.plugin import Plugin, pluginmatcher from streamlink.plugin.api import validate from streamlink.plugin.api.utils import itertags from streamlink.stream.hls import HLSStream from streamlink.utils.url import update_scheme log = logging.getLogger(__name__) @pluginmatcher(re.compile(r""" https?://(?:www\.)?(?: armymedia\.bg| bgonair\.bg/tvonline| bloombergtv\.bg/video| (?:tv\.)?bnt\.bg/\w+(?:/\w+)?| live\.bstv\.bg| i\.cdn\.bg/live/| nova\.bg/live| mu-vi\.tv/LiveStreams/pages/Live\.aspx )/? """, re.VERBOSE)) class CDNBG(Plugin): _re_frame = re.compile(r"'src',\s*'(https?://i\.cdn\.bg/live/\w+)'\);") sdata_re = re.compile(r"sdata\.src.*?=.*?(?P[\"'])(?Phttp.*?)(?P=q)") hls_file_re = re.compile(r"(src|file): (?P[\"'])(?P(https?:)?//.+?m3u8.*?)(?P=q)") hls_src_re = re.compile(r"video src=(?Phttp[^ ]+m3u8[^ ]*)") _re_source_src = re.compile(r"source src=\"(?P[^\"]+m3u8[^\"]*)\"") _re_geoblocked = re.compile(r"(?P[^\"]+geoblock[^\"]+)") stream_schema = validate.Schema( validate.any( validate.all(validate.transform(sdata_re.search), validate.get("url")), validate.all(validate.transform(hls_file_re.search), validate.get("url")), validate.all(validate.transform(hls_src_re.search), validate.get("url")), validate.all(validate.transform(_re_source_src.search), validate.get("url")), # GEOBLOCKED validate.all(validate.transform(_re_geoblocked.search), validate.get("url")), ) ) def _get_streams(self): if "cdn.bg" in urlparse(self.url).netloc: iframe_url = self.url h = self.session.get_option("http-headers") if h and h.get("Referer"): _referer = h.get("Referer") else: log.error("Missing Referer for iframe URL, use --http-header \"Referer=URL\" ") return else: _referer = self.url res = self.session.http.get(self.url) m = self._re_frame.search(res.text) if m: iframe_url = m.group(1) else: for iframe in itertags(res.text, "iframe"): iframe_url = iframe.attributes.get("src") if iframe_url and "cdn.bg" in iframe_url: iframe_url = update_scheme("https://", html_unescape(iframe_url), force=False) break else: return log.debug(f"Found iframe: {iframe_url}") res = self.session.http.get(iframe_url, headers={"Referer": _referer}) stream_url = self.stream_schema.validate(res.text) if "geoblock" in stream_url: log.error("Geo-restricted content") return return HLSStream.parse_variant_playlist( self.session, update_scheme(iframe_url, stream_url), headers={"Referer": "https://i.cdn.bg/"}, ) __plugin__ = CDNBG ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/ceskatelevize.py0000644000175100001710000001021200000000000023052 0ustar00runnerdockerimport logging import re from streamlink.plugin import Plugin, PluginError, pluginmatcher from streamlink.plugin.api import validate from streamlink.stream.dash import DASHStream log = logging.getLogger(__name__) @pluginmatcher(re.compile( r"https://(?:www\.)?ceskatelevize\.cz/zive/\w+" )) class Ceskatelevize(Plugin): _re_playlist_info = re.compile(r"{\"type\":\"([a-z]+)\",\"id\":\"([0-9]+)\"") def _get_streams(self): self.session.http.headers.update({"Referer": self.url}) schema_data = validate.Schema( validate.parse_html(), validate.xml_xpath_string(".//script[@id='__NEXT_DATA__'][text()]/text()"), str, validate.parse_json(), { "props": { "pageProps": { "data": { "liveBroadcast": { # "id": str, "current": validate.any(None, { "channel": str, "channelName": str, "legacyEncoder": str, }), "next": validate.any(None, { "channel": str, "channelName": str, "legacyEncoder": str, }) } } } } }, validate.get(("props", "pageProps", "data", "liveBroadcast")), validate.union_get("current", "next"), ) try: data_current, data_next = self.session.http.get( self.url, schema=schema_data) except PluginError: return log.debug(f"current={data_current!r}") log.debug(f"next={data_next!r}") data = data_current or data_next video_id = data["legacyEncoder"] self.title = data["channelName"] _hash = self.session.http.get( "https://www.ceskatelevize.cz/v-api/iframe-hash/", schema=validate.Schema(str)) res = self.session.http.get( "https://www.ceskatelevize.cz/ivysilani/embed/iFramePlayer.php", params={ "hash": _hash, "origin": "iVysilani", "autoStart": "true", "videoID": video_id, }, ) m = self._re_playlist_info.search(res.text) if not m: return _type, _id = m.groups() data = self.session.http.post( "https://www.ceskatelevize.cz/ivysilani/ajax/get-client-playlist/", data={ "playlist[0][type]": _type, "playlist[0][id]": _id, "requestUrl": "/ivysilani/embed/iFramePlayer.php", "requestSource": "iVysilani", "type": "html", "canPlayDRM": "false", }, headers={ "x-addr": "127.0.0.1", }, schema=validate.Schema( validate.parse_json(), { validate.optional("streamingProtocol"): str, "url": validate.any( validate.url(), "Error", "error_region" ) } ), ) if data["url"] in ["Error", "error_region"]: log.error("This stream is not available") return url = self.session.http.get( data["url"], schema=validate.Schema( validate.parse_json(), { "playlist": [{ validate.optional("type"): str, "streamUrls": { "main": validate.url(), } }] }, validate.get(("playlist", 0, "streamUrls", "main")) ) ) return DASHStream.parse_manifest(self.session, url) __plugin__ = Ceskatelevize ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/cinergroup.py0000644000175100001710000000267600000000000022410 0ustar00runnerdockerimport json import re from urllib.parse import unquote from streamlink.plugin import Plugin, pluginmatcher from streamlink.plugin.api import validate from streamlink.stream.hls import HLSStream @pluginmatcher(re.compile(r""" https?://(?:www\.)? (?: showtv\.com\.tr/canli-yayin(/showtv)?| haberturk\.com/canliyayin| haberturk\.com/tv/canliyayin| showmax\.com\.tr/canliyayin| showturk\.com\.tr/canli-yayin/showturk| bloomberght\.com/tv| haberturk\.tv/canliyayin )/? """, re.VERBOSE)) class CinerGroup(Plugin): stream_re = re.compile(r"""div .*? data-ht=(?P["'])(?P.*?)(?P=quote)""", re.DOTALL) stream_data_schema = validate.Schema( validate.transform(stream_re.search), validate.any( None, validate.all( validate.get("data"), validate.transform(unquote), validate.transform(lambda x: x.replace(""", '"')), validate.transform(json.loads), { "ht_stream_m3u8": validate.url() }, validate.get("ht_stream_m3u8") ) ) ) def _get_streams(self): res = self.session.http.get(self.url) stream_url = self.stream_data_schema.validate(res.text) if stream_url: return HLSStream.parse_variant_playlist(self.session, stream_url) __plugin__ = CinerGroup ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/clubbingtv.py0000644000175100001710000000474000000000000022364 0ustar00runnerdockerimport logging import re from streamlink.plugin import Plugin, PluginArgument, PluginArguments, pluginmatcher from streamlink.stream.hls import HLSStream log = logging.getLogger(__name__) @pluginmatcher(re.compile( r"https?://(www\.)?clubbingtv\.com/" )) class ClubbingTV(Plugin): _login_url = "https://www.clubbingtv.com/user/login" _live_re = re.compile( r'playerInstance\.setup\({\s*"file"\s*:\s*"(?P.+?)"', re.DOTALL, ) _vod_re = re.compile(r'', re.DOTALL ) _video_stream_data_re = re.compile( r' self.iso8601_to_epoch(stream_data['endDate']): log.error('Stream has expired') def _get_streams(self): if self.match.group('video_id'): return self._get_video_streams() return self._get_radio_streams() __plugin__ = RTBF ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/rtpplay.py0000644000175100001710000000366200000000000021722 0ustar00runnerdockerimport re from base64 import b64decode from urllib.parse import unquote from streamlink.plugin import Plugin, pluginmatcher from streamlink.plugin.api import useragents, validate from streamlink.stream.hls import HLSStream @pluginmatcher(re.compile( r"https?://www\.rtp\.pt/play/" )) class RTPPlay(Plugin): _m3u8_re = re.compile(r""" hls\s*:\s*(?: (["'])(?P[^"']*)\1 | decodeURIComponent\s*\((?P\[.*?])\.join\( | atob\s*\(\s*decodeURIComponent\s*\((?P\[.*?])\.join\( ) """, re.VERBOSE) _schema_hls = validate.Schema( validate.transform(lambda text: next(reversed(list(RTPPlay._m3u8_re.finditer(text))), None)), validate.any( None, validate.all( validate.get("string"), str, validate.any( validate.length(0), validate.url() ) ), validate.all( validate.get("obfuscated"), str, validate.parse_json(), validate.transform(lambda arr: unquote("".join(arr))), validate.url() ), validate.all( validate.get("obfuscated_b64"), str, validate.parse_json(), validate.transform(lambda arr: unquote("".join(arr))), validate.transform(lambda b64: b64decode(b64).decode("utf-8")), validate.url() ) ) ) def _get_streams(self): self.session.http.headers.update({"User-Agent": useragents.CHROME, "Referer": self.url}) hls_url = self.session.http.get(self.url, schema=self._schema_hls) if hls_url: return HLSStream.parse_variant_playlist(self.session, hls_url) __plugin__ = RTPPlay ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/rtve.py0000644000175100001710000001363400000000000021207 0ustar00runnerdockerimport base64 import logging import re from urllib.parse import urlparse from Crypto.Cipher import Blowfish from streamlink.plugin import Plugin, PluginArgument, PluginArguments, pluginmatcher from streamlink.plugin.api import validate from streamlink.stream.ffmpegmux import MuxedStream from streamlink.stream.hls import HLSStream from streamlink.stream.http import HTTPStream log = logging.getLogger(__name__) class ZTNRClient: base_url = "https://ztnr.rtve.es/ztnr/res/" block_size = 16 def __init__(self, key, session): self.cipher = Blowfish.new(key, Blowfish.MODE_ECB) self.session = session @classmethod def pad(cls, data): n = cls.block_size - len(data) % cls.block_size return data + bytes(chr(cls.block_size - len(data) % cls.block_size), "utf8") * n @staticmethod def unpad(data): return data[0:-data[-1]] def encrypt(self, data): return base64.b64encode(self.cipher.encrypt(self.pad(bytes(data, "utf-8"))), altchars=b"-_").decode("ascii") def decrypt(self, data): return self.unpad(self.cipher.decrypt(base64.b64decode(data, altchars=b"-_"))) def request(self, data, *args, **kwargs): res = self.session.http.get(self.base_url + self.encrypt(data), *args, **kwargs) return self.decrypt(res.content) def get_cdn_list(self, vid, manager="apedemak", vtype="video", lang="es", schema=None): data = self.request("{id}_{manager}_{type}_{lang}".format(id=vid, manager=manager, type=vtype, lang=lang)) if schema: return schema.validate(data) else: return data @pluginmatcher(re.compile( r"https?://(?:www\.)?rtve\.es/play/videos/.+" )) class Rtve(Plugin): _re_idAsset = re.compile(r"\"idAsset\":\"(\d+)\"") secret_key = base64.b64decode("eWVMJmRhRDM=") cdn_schema = validate.Schema( validate.parse_xml(invalid_char_entities=True), validate.xml_findall(".//preset"), [ validate.union({ "quality": validate.all(validate.getattr("attrib"), validate.get("type")), "urls": validate.all( validate.xml_findall(".//url"), [validate.getattr("text")] ) }) ] ) subtitles_api = "https://www.rtve.es/api/videos/{id}/subtitulos.json" subtitles_schema = validate.Schema({ "page": { "items": [{ "src": validate.url(), "lang": validate.text }] } }, validate.get("page"), validate.get("items")) video_api = "https://www.rtve.es/api/videos/{id}.json" video_schema = validate.Schema({ "page": { "items": [{ "qualities": [{ "preset": validate.text, "height": int }] }] } }, validate.get("page"), validate.get("items"), validate.get(0)) arguments = PluginArguments( PluginArgument("mux-subtitles", is_global=True) ) def __init__(self, url): super().__init__(url) self.zclient = ZTNRClient(self.secret_key, self.session) def _get_subtitles(self, content_id): res = self.session.http.get(self.subtitles_api.format(id=content_id)) return self.session.http.json(res, schema=self.subtitles_schema) def _get_quality_map(self, content_id): res = self.session.http.get(self.video_api.format(id=content_id)) data = self.session.http.json(res, schema=self.video_schema) qmap = {} for item in data["qualities"]: qname = {"MED": "Media", "HIGH": "Alta", "ORIGINAL": "Original"}.get(item["preset"], item["preset"]) qmap[qname] = f"{item['height']}p" return qmap def _get_streams(self): res = self.session.http.get(self.url) m = self._re_idAsset.search(res.text) if m: content_id = m.group(1) log.debug(f"Found content with id: {content_id}") stream_data = self.zclient.get_cdn_list(content_id, schema=self.cdn_schema) quality_map = None streams = [] for stream in stream_data: # only use one stream _one_m3u8 = False _one_mp4 = False for url in stream["urls"]: p_url = urlparse(url) if p_url.path.endswith(".m3u8"): if _one_m3u8: continue try: streams.extend(HLSStream.parse_variant_playlist(self.session, url).items()) _one_m3u8 = True except OSError as err: log.error(str(err)) elif p_url.path.endswith(".mp4"): if _one_mp4: continue if quality_map is None: # only make the request when it is necessary quality_map = self._get_quality_map(content_id) # rename the HTTP sources to match the HLS sources quality = quality_map.get(stream["quality"], stream["quality"]) streams.append((quality, HTTPStream(self.session, url))) _one_mp4 = True subtitles = None if self.get_option("mux_subtitles"): subtitles = self._get_subtitles(content_id) if subtitles: substreams = {} for i, subtitle in enumerate(subtitles): substreams[subtitle["lang"]] = HTTPStream(self.session, subtitle["src"]) for q, s in streams: yield q, MuxedStream(self.session, s, subtitles=substreams) else: for s in streams: yield s __plugin__ = Rtve ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/rtvs.py0000644000175100001710000000243100000000000021216 0ustar00runnerdockerimport re from streamlink.plugin import Plugin, pluginmatcher from streamlink.plugin.api import validate from streamlink.stream.hls import HLSStream from streamlink.utils.parse import parse_json @pluginmatcher(re.compile( r"https?://www\.rtvs\.sk/televizia/live-[\w-]+" )) class Rtvs(Plugin): _re_channel_id = re.compile(r"'stream':\s*'live-(\d+)'") def _get_streams(self): res = self.session.http.get(self.url) m = self._re_channel_id.search(res.text) if not m: return res = self.session.http.get( "https://www.rtvs.sk/json/live5f.json", params={ "c": m.group(1), "b": "mozilla", "p": "win", "f": "0", "d": "1", } ) videos = parse_json(res.text, schema=validate.Schema({ "clip": { "sources": [{ "src": validate.url(), "type": str, }], }}, validate.get(("clip", "sources")), validate.filter(lambda n: n["type"] == "application/x-mpegurl"), )) for video in videos: yield from HLSStream.parse_variant_playlist(self.session, video["src"]).items() __plugin__ = Rtvs ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/ruv.py0000644000175100001710000000705100000000000021037 0ustar00runnerdocker"""Plugin for RUV, the Icelandic national television.""" import re from streamlink.plugin import Plugin, pluginmatcher from streamlink.stream.hls import HLSStream # URL to the RUV LIVE API RUV_LIVE_API = """http://www.ruv.is/sites/all/themes/at_ruv/scripts/\ ruv-stream.php?channel={0}&format=json""" _single_re = re.compile(r"""(?Phttp://[0-9a-zA-Z\-\.]*/ (lokad|opid) / ([0-9]+/[0-9][0-9]/[0-9][0-9]/)? ([A-Z0-9\$_]+\.mp4\.m3u8) ) """, re.VERBOSE) _multi_re = re.compile(r"""(?Phttp://[0-9a-zA-Z\-\.]*/ (lokad|opid) /) manifest.m3u8\?tlm=hls&streams= (?P[0-9a-zA-Z\/\.\,:]+) """, re.VERBOSE) @pluginmatcher(re.compile(r""" https?://(?:www\.)?ruv\.is/ (?Pruv|ruv2|ruv-2|ras1|ras2|rondo) /?$ """, re.VERBOSE)) @pluginmatcher(re.compile(r""" https?://(?:www\.)?ruv\.is/spila/ (?Pruv|ruv2|ruv-2|ruv-aukaras) /[a-zA-Z0-9_-]+ /[0-9]+ /? """, re.VERBOSE)) class Ruv(Plugin): def __init__(self, url): super().__init__(url) self.live = self.matches[0] is not None if self.live: # Remove dashes self.stream_id = self.match.group("stream_id").replace("-", "") # Rondo is identified as ras3 if self.stream_id == "rondo": self.stream_id = "ras3" def _get_live_streams(self): # Get JSON API res = self.session.http.get(RUV_LIVE_API.format(self.stream_id)) # Parse the JSON API json_res = self.session.http.json(res) for url in json_res["result"]: if url.startswith("rtmp:"): continue # Get available streams streams = HLSStream.parse_variant_playlist(self.session, url) yield from streams.items() def _get_sarpurinn_streams(self): # Get HTML page res = self.session.http.get(self.url).text lines = "\n".join([line for line in res.split("\n") if "video.src" in line]) multi_stream_match = _multi_re.search(lines) if multi_stream_match and multi_stream_match.group("streams"): base_url = multi_stream_match.group("base_url") streams = multi_stream_match.group("streams").split(",") for stream in streams: if stream.count(":") != 1: continue [token, quality] = stream.split(":") quality = int(quality) key = "" if quality <= 500: key = "240p" elif quality <= 800: key = "360p" elif quality <= 1200: key = "480p" elif quality <= 2400: key = "720p" else: key = "1080p" yield key, HLSStream( self.session, base_url + token ) else: single_stream_match = _single_re.search(lines) if single_stream_match: url = single_stream_match.group("url") yield "576p", HLSStream(self.session, url) def _get_streams(self): if self.live: return self._get_live_streams() else: return self._get_sarpurinn_streams() __plugin__ = Ruv ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/sbscokr.py0000644000175100001710000000653000000000000021672 0ustar00runnerdockerimport logging import random import re from streamlink.plugin import Plugin, PluginArgument, PluginArguments, pluginmatcher from streamlink.plugin.api import validate from streamlink.stream.hls import HLSStream log = logging.getLogger(__name__) @pluginmatcher(re.compile( r'https?://play\.sbs\.co\.kr/onair/pc/index\.html' )) class SBScokr(Plugin): api_channel = 'http://apis.sbs.co.kr/play-api/1.0/onair/channel/{0}' api_channels = 'http://static.apis.sbs.co.kr/play-api/1.0/onair/channels' _channels_schema = validate.Schema({ 'list': [{ 'channelname': validate.all( validate.text, ), 'channelid': validate.text, validate.optional('type'): validate.text, }]}, validate.get('list'), ) _channel_schema = validate.Schema( { 'onair': { 'info': { 'onair_yn': validate.text, 'overseas_yn': validate.text, 'overseas_text': validate.text, }, 'source': { 'mediasourcelist': validate.any([{ validate.optional('default'): validate.text, 'mediaurl': validate.text, }], []) }, } }, validate.get('onair'), ) arguments = PluginArguments( PluginArgument( 'id', metavar='CHANNELID', type=str.upper, help=''' Channel ID to play. Example: %(prog)s http://play.sbs.co.kr/onair/pc/index.html best --sbscokr-id S01 ''' ) ) def _get_streams(self): user_channel_id = self.get_option('id') res = self.session.http.get(self.api_channels) res = self.session.http.json(res, schema=self._channels_schema) channels = {} for channel in sorted(res, key=lambda x: x['channelid']): if channel.get('type') in ('TV', 'Radio'): channels[channel['channelid']] = channel['channelname'] log.info('Available IDs: {0}'.format(', '.join( '{0} ({1})'.format(key, value) for key, value in channels.items()))) if not user_channel_id: log.error('No channel selected, use --sbscokr-id CHANNELID') return elif user_channel_id and user_channel_id not in channels.keys(): log.error('Channel ID "{0}" is not available.'.format(user_channel_id)) return params = { 'v_type': '2', 'platform': 'pcweb', 'protocol': 'hls', 'jwt-token': '', 'rnd': random.randint(50, 300) } res = self.session.http.get(self.api_channel.format(user_channel_id), params=params) res = self.session.http.json(res, schema=self._channel_schema) for media in res['source']['mediasourcelist']: if media['mediaurl']: yield from HLSStream.parse_variant_playlist(self.session, media['mediaurl']).items() else: if res['info']['onair_yn'] != 'Y': log.error('This channel is currently unavailable') elif res['info']['overseas_yn'] != 'Y': log.error(res['info']['overseas_text']) __plugin__ = SBScokr ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/schoolism.py0000644000175100001710000001254300000000000022225 0ustar00runnerdockerimport logging import re from functools import partial from streamlink.plugin import Plugin, PluginArgument, PluginArguments, pluginmatcher from streamlink.plugin.api import useragents, validate from streamlink.stream.hls import HLSStream from streamlink.stream.http import HTTPStream log = logging.getLogger(__name__) @pluginmatcher(re.compile( r"https?://(?:www\.)?schoolism\.com/(viewAssignment|watchLesson)\.php" )) class Schoolism(Plugin): login_url = "https://www.schoolism.com/index.php" key_time_url = "https://www.schoolism.com/video-html/key-time.php" playlist_re = re.compile(r"var allVideos\s*=\s*(\[.*\]);", re.DOTALL) js_to_json = partial(re.compile(r'(?!<")(\w+):(?!/)').sub, r'"\1":') fix_brackets = partial(re.compile(r',\s*\}').sub, r'}') fix_colon_in_title = partial(re.compile(r'"title":""(.*?)":(.*?)"').sub, r'"title":"\1:\2"') playlist_schema = validate.Schema( validate.transform(playlist_re.search), validate.any( None, validate.all( validate.get(1), validate.transform(js_to_json), validate.transform(fix_brackets), # remove invalid , validate.transform(fix_colon_in_title), validate.parse_json(), [{ "sources": validate.all([{ validate.optional("playlistTitle"): validate.text, "title": validate.text, "src": validate.text, "type": validate.text, }], # only include HLS streams # validate.filter(lambda s: s["type"] == "application/x-mpegurl") ) }] ) ) ) arguments = PluginArguments( PluginArgument( "email", required=True, requires=["password"], metavar="EMAIL", help=""" The email associated with your Schoolism account, required to access any Schoolism stream. """ ), PluginArgument( "password", sensitive=True, metavar="PASSWORD", help="A Schoolism account password to use with --schoolism-email." ), PluginArgument( "part", type=int, default=1, metavar="PART", help=""" Play part number PART of the lesson, or assignment feedback video. Defaults is 1. """ ) ) def login(self, email, password): """ Login to the schoolism account and return the users account :param email: (str) email for account :param password: (str) password for account :return: (str) users email """ if self.options.get("email") and self.options.get("password"): res = self.session.http.post(self.login_url, data={"email": email, "password": password, "redirect": None, "submit": "Login"}) if res.cookies.get("password") and res.cookies.get("email"): return res.cookies.get("email") else: log.error("Failed to login to Schoolism, incorrect email/password combination") else: log.error("An email and password are required to access Schoolism streams") def _get_streams(self): user = self.login(self.options.get("email"), self.options.get("password")) if user: log.debug(f"Logged in to Schoolism as {user}") res = self.session.http.get(self.url, headers={"User-Agent": useragents.SAFARI_8}) lesson_playlist = self.playlist_schema.validate(res.text) part = self.options.get("part") video_type = "Lesson" if "lesson" in self.match.group(1).lower() else "Assignment Feedback" log.info(f"Attempting to play {video_type} Part {part}") found = False # make request to key-time api, to get key specific headers _ = self.session.http.get(self.key_time_url, headers={"User-Agent": useragents.SAFARI_8}) for i, video in enumerate(lesson_playlist, 1): if video["sources"] and i == part: found = True for source in video["sources"]: if source['type'] == "video/mp4": yield "live", HTTPStream(self.session, source["src"], headers={"User-Agent": useragents.SAFARI_8, "Referer": self.url}) elif source['type'] == "application/x-mpegurl": yield from HLSStream.parse_variant_playlist( self.session, source["src"], headers={ "User-Agent": useragents.SAFARI_8, "Referer": self.url } ).items() if not found: log.error(f"Could not find {video_type} Part {part}") __plugin__ = Schoolism ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/senategov.py0000644000175100001710000001050400000000000022213 0ustar00runnerdockerimport logging import re from urllib.parse import parse_qsl, urlparse from streamlink.plugin import Plugin, pluginmatcher from streamlink.plugin.api import useragents from streamlink.plugin.api.utils import itertags from streamlink.stream.hls import HLSStream log = logging.getLogger(__name__) @pluginmatcher(re.compile( r"https?://(?:[\w-]+\.)*senate\.gov/(isvp)?" )) class SenateGov(Plugin): streaminfo_re = re.compile(r"""var\s+streamInfo\s+=\s+new\s+Array\s*\(\s*(\[.*\])\);""") stt_re = re.compile(r"""^(?:(?P\d+):)?(?P\d+):(?P\d+)$""") url_lookup = { "ag": ["76440", "https://ag-f.akamaihd.net"], "aging": ["76442", "https://aging-f.akamaihd.net"], "approps": ["76441", "https://approps-f.akamaihd.net"], "armed": ["76445", "https://armed-f.akamaihd.net"], "banking": ["76446", "https://banking-f.akamaihd.net"], "budget": ["76447", "https://budget-f.akamaihd.net"], "cecc": ["76486", "https://srs-f.akamaihd.net"], "commerce": ["80177", "https://commerce1-f.akamaihd.net"], "csce": ["75229", "https://srs-f.akamaihd.net"], "dpc": ["76590", "https://dpc-f.akamaihd.net"], "energy": ["76448", "https://energy-f.akamaihd.net"], "epw": ["76478", "https://epw-f.akamaihd.net"], "ethics": ["76449", "https://ethics-f.akamaihd.net"], "finance": ["76450", "https://finance-f.akamaihd.net"], "foreign": ["76451", "https://foreign-f.akamaihd.net"], "govtaff": ["76453", "https://govtaff-f.akamaihd.net"], "help": ["76452", "https://help-f.akamaihd.net"], "indian": ["76455", "https://indian-f.akamaihd.net"], "intel": ["76456", "https://intel-f.akamaihd.net"], "intlnarc": ["76457", "https://intlnarc-f.akamaihd.net"], "jccic": ["85180", "https://jccic-f.akamaihd.net"], "jec": ["76458", "https://jec-f.akamaihd.net"], "judiciary": ["76459", "https://judiciary-f.akamaihd.net"], "rpc": ["76591", "https://rpc-f.akamaihd.net"], "rules": ["76460", "https://rules-f.akamaihd.net"], "saa": ["76489", "https://srs-f.akamaihd.net"], "smbiz": ["76461", "https://smbiz-f.akamaihd.net"], "srs": ["75229", "https://srs-f.akamaihd.net"], "uscc": ["76487", "https://srs-f.akamaihd.net"], "vetaff": ["76462", "https://vetaff-f.akamaihd.net"], } hls_url = "{base}/i/{filename}_1@{number}/master.m3u8?" hlsarch_url = "https://ussenate-f.akamaihd.net/i/{filename}.mp4/master.m3u8" def _isvp_to_m3u8(self, url): qs = dict(parse_qsl(urlparse(url).query)) if "comm" not in qs: log.error("Missing `comm` value") if "filename" not in qs: log.error("Missing `filename` value") d = self.url_lookup.get(qs['comm']) if d: snumber, baseurl = d stream_url = self.hls_url.format(filename=qs['filename'], number=snumber, base=baseurl) else: stream_url = self.hlsarch_url.format(filename=qs['filename']) return stream_url, self.parse_stt(qs.get('stt', 0)) def _get_streams(self): self.session.http.headers.update({ "User-Agent": useragents.CHROME, }) if not self.match.group(1): log.debug("Searching for ISVP URL") isvp_url = self._get_isvp_url() else: isvp_url = self.url if not isvp_url: log.error("Could not find the ISVP URL") return else: log.debug("ISVP URL: {0}".format(isvp_url)) stream_url, start_offset = self._isvp_to_m3u8(isvp_url) log.debug("Start offset is: {0}s".format(start_offset)) return HLSStream.parse_variant_playlist(self.session, stream_url, start_offset=start_offset) def _get_isvp_url(self): res = self.session.http.get(self.url) for iframe in itertags(res.text, 'iframe'): m = self.url_re.match(iframe.attributes.get('src')) return m and m.group(1) is not None and iframe.attributes.get('src') @classmethod def parse_stt(cls, param): m = cls.stt_re.match(param) if not m: return 0 return ( int(m.group('hours') or 0) * 3600 + int(m.group('minutes')) * 60 + int(m.group('seconds')) ) __plugin__ = SenateGov ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/showroom.py0000644000175100001710000000437200000000000022103 0ustar00runnerdockerimport logging import re from streamlink.plugin import Plugin, pluginmatcher from streamlink.plugin.api import validate from streamlink.stream.hls import HLSStream, HLSStreamReader, HLSStreamWorker log = logging.getLogger(__name__) class ShowroomHLSStreamWorker(HLSStreamWorker): def _playlist_reload_time(self, playlist, sequences): return 1.5 class ShowroomHLSStreamReader(HLSStreamReader): __worker__ = ShowroomHLSStreamWorker class ShowroomHLSStream(HLSStream): __reader__ = ShowroomHLSStreamReader @pluginmatcher(re.compile( r"https?://(?:\w+\.)?showroom-live\.com/" )) class Showroom(Plugin): def _get_streams(self): data = self.session.http.get( self.url, schema=validate.Schema( validate.parse_html(), validate.xml_xpath_string(".//script[@id='js-live-data'][@data-json]/@data-json"), validate.any(None, validate.all( validate.parse_json(), {"is_live": int, "room_id": int, validate.optional("room"): {"content_region_permission": int, "is_free": int}}, )) ) ) if not data: # URL without livestream return log.debug(f"{data!r}") if data["is_live"] != 1: log.info("This stream is currently offline") return url = self.session.http.get( "https://www.showroom-live.com/api/live/streaming_url", params={"room_id": data["room_id"], "abr_available": 1}, schema=validate.Schema( validate.parse_json(), {"streaming_url_list": [{ "type": str, "url": validate.url(), }]}, validate.get("streaming_url_list"), validate.filter(lambda p: p["type"] == "hls_all"), validate.get((0, "url")) ), ) res = self.session.http.get(url, acceptable_status=(200, 403, 404)) if res.headers["Content-Type"] != "application/x-mpegURL": log.error("This stream is restricted") return return ShowroomHLSStream.parse_variant_playlist(self.session, url) __plugin__ = Showroom ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/sportal.py0000644000175100001710000000140000000000000021677 0ustar00runnerdockerimport logging import re from streamlink.plugin import Plugin, pluginmatcher from streamlink.stream.hls import HLSStream log = logging.getLogger(__name__) @pluginmatcher(re.compile( r'https?://(?:www\.)?sportal\.bg/sportal_live_tv\.php' )) class Sportal(Plugin): _hls_re = re.compile(r'''["'](?P[^"']+\.m3u8[^"']*?)["']''') def _get_streams(self): res = self.session.http.get(self.url) m = self._hls_re.search(res.text) if not m: return hls_url = m.group('url') log.debug('URL={0}'.format(hls_url)) log.warning('SSL certificate verification is disabled.') return HLSStream.parse_variant_playlist( self.session, hls_url, verify=False).items() __plugin__ = Sportal ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/sportschau.py0000644000175100001710000000332000000000000022411 0ustar00runnerdockerimport logging import re from streamlink.plugin import Plugin, pluginmatcher from streamlink.plugin.api import validate from streamlink.stream.hls import HLSStream from streamlink.stream.http import HTTPStream from streamlink.utils.url import update_scheme log = logging.getLogger(__name__) @pluginmatcher(re.compile( r"https?://(?:\w+\.)*sportschau\.de/" )) class Sportschau(Plugin): _re_player = re.compile(r"https?:(//deviceids-medp.wdr.de/ondemand/\S+\.js)") _re_json = re.compile(r"\$mediaObject.jsonpHelper.storeAndPlay\(({.+})\);?") def _get_streams(self): player_js = self.session.http.get(self.url, schema=validate.Schema( validate.transform(self._re_player.search), validate.any(None, validate.Schema( validate.get(1), validate.transform(lambda url: update_scheme("https:", url)) )) )) if not player_js: return log.debug(f"Found player js {player_js}") data = self.session.http.get(player_js, schema=validate.Schema( validate.transform(self._re_json.match), validate.get(1), validate.parse_json(), validate.get("mediaResource"), validate.get("dflt"), { validate.optional("audioURL"): validate.url(), validate.optional("videoURL"): validate.url() } )) if data.get("videoURL"): yield from HLSStream.parse_variant_playlist(self.session, update_scheme("https:", data.get("videoURL"))).items() if data.get("audioURL"): yield "audio", HTTPStream(self.session, update_scheme("https:", data.get("audioURL"))) __plugin__ = Sportschau ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/ssh101.py0000644000175100001710000000160200000000000021236 0ustar00runnerdockerimport logging import re from streamlink.plugin import Plugin, pluginmatcher from streamlink.plugin.api import validate from streamlink.stream import HLSStream log = logging.getLogger(__name__) @pluginmatcher(re.compile( r"https?://(?:www\.)?ssh101\.com/(?:(?:secure)?live/|detail\.php\?id=\w+)" )) class SSH101(Plugin): def _get_streams(self): hls_url = self.session.http.get(self.url, schema=validate.Schema( validate.parse_html(), validate.xml_xpath_string(".//source[contains(@src,'.m3u8')]/@src"), )) if not hls_url: return res = self.session.http.get(hls_url, acceptable_status=(200, 403, 404)) if res.status_code != 200 or len(res.text) <= 10: log.error("This stream is currently offline") return return {"live": HLSStream(self.session, hls_url)} __plugin__ = SSH101 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/stadium.py0000644000175100001710000000446600000000000021700 0ustar00runnerdockerimport logging import re from streamlink.plugin import Plugin, PluginError, pluginmatcher from streamlink.plugin.api import validate from streamlink.stream.hls import HLSStream from streamlink.utils.url import update_qsd log = logging.getLogger(__name__) @pluginmatcher(re.compile( r"https?://(?:www\.)?watchstadium\.com/" )) class Stadium(Plugin): _policy_key_re = re.compile(r"""options:\s*{.+policyKey:\s*"([^"]+)""", re.DOTALL) _API_URL = "https://edge.api.brightcove.com/playback/v1/accounts/{data_account}/videos/{data_video_id}" _PLAYER_URL = "https://players.brightcove.net/{data_account}/{data_player}_default/index.min.js" def _get_streams(self): try: data = self.session.http.get(self.url, schema=validate.Schema( validate.parse_html(), validate.xml_find(".//video[@id='brightcove_video_player']"), validate.union_get("data-video-id", "data-account", "data-ad-config-id", "data-player") )) except PluginError: return data_video_id, data_account, data_ad_config_id, data_player = data url = self._PLAYER_URL.format(data_account=data_account, data_player=data_player) policy_key = self.session.http.get(url, schema=validate.Schema( validate.transform(self._policy_key_re.search), validate.any(None, validate.get(1)) )) if not policy_key: return url = self._API_URL.format(data_account=data_account, data_video_id=data_video_id) if data_ad_config_id is not None: url = update_qsd(url, dict(ad_config_id=data_ad_config_id)) streams = self.session.http.get( url, headers={"Accept": f"application/json;pk={policy_key}"}, schema=validate.Schema( validate.parse_json(), { "sources": [{ validate.optional("type"): str, "src": validate.url(), }], }, validate.get("sources"), validate.filter(lambda source: source.get("type") == "application/x-mpegURL") ) ) for stream in streams: return HLSStream.parse_variant_playlist(self.session, stream["src"]) __plugin__ = Stadium ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/steam.py0000644000175100001710000002126300000000000021335 0ustar00runnerdockerimport base64 import logging import re import time from Crypto.Cipher import PKCS1_v1_5 from Crypto.PublicKey import RSA from streamlink.exceptions import FatalPluginError from streamlink.plugin import Plugin, PluginArgument, PluginArguments, pluginmatcher from streamlink.plugin.api import validate from streamlink.stream.dash import DASHStream log = logging.getLogger(__name__) class SteamLoginFailed(Exception): pass @pluginmatcher(re.compile( r"https?://steamcommunity\.com/broadcast/watch/(\d+)" )) @pluginmatcher(re.compile( r"https?://steam\.tv/(\w+)" )) class SteamBroadcastPlugin(Plugin): _watch_broadcast_url = "https://steamcommunity.com/broadcast/watch/{steamid}" _get_broadcast_url = "https://steamcommunity.com/broadcast/getbroadcastmpd/" _get_rsa_key_url = "https://steamcommunity.com/login/getrsakey/" _dologin_url = "https://steamcommunity.com/login/dologin/" _captcha_url = "https://steamcommunity.com/public/captcha.php?gid={}" arguments = PluginArguments( PluginArgument( "email", metavar="EMAIL", requires=["password"], help=""" A Steam account email address to access friends/private streams """ ), PluginArgument( "password", metavar="PASSWORD", sensitive=True, help=""" A Steam account password to use with --steam-email. """ )) def encrypt_password(self, email, password): """ Get the RSA key for the user and encrypt the users password :param email: steam account :param password: password for account :return: encrypted password """ rsadata = self.session.http.get( self._get_rsa_key_url, params=dict( username=email, donotcache=str(int(time.time() * 1000)) ), schema=validate.Schema( validate.parse_json(), { "publickey_exp": validate.all(str, validate.transform(lambda x: int(x, 16))), "publickey_mod": validate.all(str, validate.transform(lambda x: int(x, 16))), "success": True, "timestamp": str, "token_gid": str } ) ) rsa = RSA.construct((rsadata["publickey_mod"], rsadata["publickey_exp"])) cipher = PKCS1_v1_5.new(rsa) return base64.b64encode(cipher.encrypt(password.encode("utf8"))), rsadata["timestamp"] def dologin(self, email, password, emailauth="", emailsteamid="", captchagid="-1", captcha_text="", twofactorcode=""): epassword, rsatimestamp = self.encrypt_password(email, password) login_data = { "username": email, "password": epassword, "emailauth": emailauth, "loginfriendlyname": "Streamlink", "captchagid": captchagid, "captcha_text": captcha_text, "emailsteamid": emailsteamid, "rsatimestamp": rsatimestamp, "remember_login": True, "donotcache": self.donotcache, "twofactorcode": twofactorcode } resp = self.session.http.post( self._dologin_url, data=login_data, schema=validate.Schema( validate.parse_json(), { "success": bool, "requires_twofactor": bool, validate.optional("message"): str, validate.optional("emailauth_needed"): bool, validate.optional("emaildomain"): str, validate.optional("emailsteamid"): str, validate.optional("login_complete"): bool, validate.optional("captcha_needed"): bool, validate.optional("captcha_gid"): validate.any(str, int) } ) ) if resp.get("login_complete"): return True if not resp["success"]: if resp.get("captcha_needed"): # special case for captcha captchagid = resp["captcha_gid"] captchaurl = self._captcha_url.format(captchagid) log.error(f"Captcha result required, open this URL to see the captcha: {captchaurl}") try: captcha_text = self.input_ask("Captcha text") except FatalPluginError: captcha_text = None if not captcha_text: return False else: # If the user must enter the code that was emailed to them if resp.get("emailauth_needed"): if emailauth: raise SteamLoginFailed("Email auth key error") try: emailauth = self.input_ask("Email auth code required") except FatalPluginError: emailauth = None if not emailauth: return False # If the user must enter a two factor auth code if resp.get("requires_twofactor"): try: twofactorcode = self.input_ask("Two factor auth code required") except FatalPluginError: twofactorcode = None if not twofactorcode: return False if resp.get("message"): raise SteamLoginFailed(resp["message"]) return self.dologin( email, password, emailauth=emailauth, emailsteamid=resp.get("emailsteamid", ""), captcha_text=captcha_text, captchagid=captchagid, twofactorcode=twofactorcode ) log.error("Something went wrong while logging in to Steam") return False def _get_broadcast_stream(self, steamid, viewertoken=0, sessionid=None): log.debug(f"Getting broadcast stream: sessionid={sessionid}") return self.session.http.get( self._get_broadcast_url, params=dict( broadcastid=0, steamid=steamid, viewertoken=viewertoken, sessionid=sessionid ), schema=validate.Schema( validate.parse_json(), { "success": validate.any("ready", "unavailable", "waiting", "waiting_to_start", "waiting_for_start"), "retry": int, "broadcastid": validate.any(str, int), validate.optional("url"): validate.url(), validate.optional("viewertoken"): str } ) ) def _find_steamid(self, url): return self.session.http.get(url, schema=validate.Schema( validate.parse_html(), validate.xml_xpath_string(".//div[@id='webui_config']/@data-broadcast"), validate.any(None, validate.all( str, validate.parse_json(), {"steamid": str}, validate.get("steamid") )) )) def _get_streams(self): self.session.http.headers["User-Agent"] = f"streamlink/{self.session.version}" email = self.get_option("email") if email: log.info(f"Attempting to login to Steam as {email}") if self.dologin(email, self.get_option("password")): log.info(f"Logged in as {email}") self.save_cookies(lambda c: "steamMachineAuth" in c.name) if self.matches[1] is None: steamid = self.match.group(1) else: steamid = self._find_steamid(self.url) if not steamid: return self.url = self._watch_broadcast_url.format(steamid=steamid) res = self.session.http.get(self.url) # get the page to set some cookies sessionid = res.cookies.get("sessionid") streamdata = None while streamdata is None or streamdata["success"] in ("waiting", "waiting_for_start"): streamdata = self._get_broadcast_stream(steamid, sessionid=sessionid) if streamdata["success"] == "ready": return DASHStream.parse_manifest(self.session, streamdata["url"]) if streamdata["success"] == "unavailable": log.error("This stream is currently unavailable") return r = streamdata["retry"] / 1000.0 log.info(f"Waiting for stream, will retry again in {r:.1f} seconds...") time.sleep(r) __plugin__ = SteamBroadcastPlugin ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/streamable.py0000644000175100001710000000262400000000000022343 0ustar00runnerdockerimport re from streamlink.plugin import Plugin, pluginmatcher from streamlink.plugin.api import validate from streamlink.stream.http import HTTPStream from streamlink.utils.url import update_scheme @pluginmatcher(re.compile( r"https?://(?:www\.)?streamable\.com/(.+)" )) class Streamable(Plugin): meta_re = re.compile(r'''var\s*videoObject\s*=\s*({.*});''') config_schema = validate.Schema( validate.transform(meta_re.search), validate.any(None, validate.all( validate.get(1), validate.parse_json(), { "files": {validate.text: {"url": validate.url(), "width": int, "height": int, "bitrate": int}} }) ) ) def _get_streams(self): data = self.session.http.get(self.url, schema=self.config_schema) for info in data["files"].values(): stream_url = update_scheme("https://", info["url"]) # pick the smaller of the two dimensions, for landscape v. portrait videos res = min(info["width"], info["height"]) yield "{0}p".format(res), HTTPStream(self.session, stream_url) __plugin__ = Streamable ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/streann.py0000644000175100001710000001304600000000000021676 0ustar00runnerdockerimport base64 import logging import random import re import time from urllib.parse import urlparse from streamlink.plugin import Plugin, PluginArgument, PluginArguments, pluginmatcher from streamlink.plugin.api import validate from streamlink.stream.hls import HLSStream from streamlink.utils.crypto import decrypt_openssl from streamlink.utils.parse import parse_qsd log = logging.getLogger(__name__) @pluginmatcher(re.compile( r"https?://ott\.streann\.com/s(?:treaming|-secure)/player\.html" )) @pluginmatcher(re.compile(r""" https?://(?:www\.)?(?: centroecuador\.ec | columnaestilos\.com | crc\.cr/estaciones/ | evtv\.online/noticias-de-venezuela/ | telecuracao\.com ) """, re.VERBOSE)) class Streann(Plugin): arguments = PluginArguments( PluginArgument( "url", type=str, metavar="URL", help=""" Source URL where the iframe is located, only required for direct URLs of `ott.streann.com` """ ) ) base_url = "https://ott.streann.com" get_time_url = base_url + "/web/services/public/get-server-time" token_url = base_url + "/loadbalancer/services/web-players/{playerId}/token/{type}/{dataId}/{deviceId}" stream_url = base_url + "/loadbalancer/services/web-players/{type}s-reseller-secure/{dataId}/{playerId}" \ "/{token}/{resellerId}/playlist.m3u8?date={time}&device-type=web&device-name=web" \ "&device-os=web&device-id={deviceId}" passphrase_re = re.compile(r'''CryptoJS\.AES\.decrypt\(.*?,\s*(['"])(?P(?:(?!\1).)*)\1\s*?\);''') _device_id = None _domain = None @property def device_id(self): """ Randomly generated deviceId. :return: """ if self._device_id is None: self._device_id = "".join( random.choice("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") for _ in range(50)) return self._device_id @property def time(self): res = self.session.http.get(self.get_time_url) data = self.session.http.json(res) return str(data.get("serverTime", int(time.time() * 1000))) def passphrase(self): log.debug("passphrase") res = self.session.http.get(self.url) passphrase_m = self.passphrase_re.search(res.text) return passphrase_m and passphrase_m.group("passphrase").encode("utf8") def get_token(self, **config): log.debug("get_token") pdata = dict(arg1=base64.b64encode(self._domain.encode("utf8")), arg2=base64.b64encode(self.time.encode("utf8"))) headers = { "Referer": self.url, "X-Requested-With": "XMLHttpRequest", "Content-Type": "application/x-www-form-urlencoded" } res = self.session.http.post( self.token_url.format(deviceId=self.device_id, **config), data=pdata, headers=headers ) if res.status_code == 204: log.error(f"self._domain might be invalid - {self._domain}") return data = self.session.http.json(res, schema=validate.Schema({ "token": str, validate.optional("name"): str, validate.optional("webPlayer"): { validate.optional("id"): str, validate.optional("name"): str, validate.optional("type"): str, validate.optional("allowedDomains"): [str], }, })) log.trace(f"{data!r}") self.title = data.get("name") return data["token"] def _get_streams(self): if not self.matches[0]: self._domain = urlparse(self.url).netloc iframes = self.session.http.get(self.url, schema=validate.Schema( validate.parse_html(), validate.xml_findall(".//iframe[@src]"), validate.filter(lambda elem: urlparse(elem.attrib.get("src")).netloc == "ott.streann.com") )) if not iframes: log.error("Could not find 'ott.streann.com' iframe") return self.url = iframes[0].attrib.get("src") if not self._domain and self.get_option("url"): self._domain = urlparse(self.get_option("url")).netloc if self._domain is None: log.error("Missing source URL, use --streann-url") return self.session.http.headers.update({"Referer": self.url}) # Get the query string encrypted_data = urlparse(self.url).query data = base64.b64decode(encrypted_data) # and decrypt it passphrase = self.passphrase() if passphrase: log.debug("Found passphrase") params = decrypt_openssl(data, passphrase) config = parse_qsd(params.decode("utf8")) log.trace(f"config: {config!r}") token = self.get_token(**config) if not token: return hls_url = self.stream_url.format(time=self.time, deviceId=self.device_id, token=token, **config) log.debug("URL={0}".format(hls_url)) return HLSStream.parse_variant_playlist(self.session, hls_url, acceptable_status=(200, 403, 404, 500)) __plugin__ = Streann ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/stv.py0000644000175100001710000000173500000000000021042 0ustar00runnerdockerimport logging import re from streamlink.plugin import Plugin, PluginError, pluginmatcher from streamlink.stream.hls import HLSStream log = logging.getLogger(__name__) @pluginmatcher(re.compile( r'https?://player\.stv\.tv/live' )) class STV(Plugin): API_URL = 'https://player.api.stv.tv/v1/streams/stv/' def get_title(self): if self.title is None: self._get_api_results() return self.title def _get_api_results(self): res = self.session.http.get(self.API_URL) data = self.session.http.json(res) if data['success'] is False: raise PluginError(data['reason']['message']) try: self.title = data['results']['now']['title'] except KeyError: self.title = 'STV' return data def _get_streams(self): hls_url = self._get_api_results()['results']['streamUrl'] return HLSStream.parse_variant_playlist(self.session, hls_url) __plugin__ = STV ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/svtplay.py0000644000175100001710000000747400000000000021736 0ustar00runnerdockerimport logging import re from urllib.parse import parse_qsl, urlparse from streamlink.plugin import Plugin, PluginArgument, PluginArguments, pluginmatcher from streamlink.plugin.api import validate from streamlink.stream.dash import DASHStream from streamlink.stream.ffmpegmux import MuxedStream from streamlink.stream.http import HTTPStream log = logging.getLogger(__name__) @pluginmatcher(re.compile( r'https?://(?:www\.)?(?:svtplay|oppetarkiv)\.se(/(kanaler/)?.*)' )) class SVTPlay(Plugin): api_url = 'https://api.svt.se/videoplayer-api/video/{0}' latest_episode_url_re = re.compile(r''' data-rt="top-area-play-button"\s+href="(?P[^"]+)" ''', re.VERBOSE) live_id_re = re.compile(r'.*/(?P[^?]+)') _video_schema = validate.Schema({ validate.optional('programTitle'): validate.text, validate.optional('episodeTitle'): validate.text, 'videoReferences': [{ 'url': validate.url(), 'format': validate.text, }], validate.optional('subtitleReferences'): [{ 'url': validate.url(), 'format': validate.text, }], }) arguments = PluginArguments( PluginArgument("mux-subtitles", is_global=True) ) def _set_metadata(self, data, category): if 'programTitle' in data: self.author = data['programTitle'] self.category = category if 'episodeTitle' in data: self.title = data['episodeTitle'] def _get_live(self, path): match = self.live_id_re.search(path) if match is None: return live_id = "ch-{0}".format(match.group('live_id')) log.debug("Live ID={0}".format(live_id)) res = self.session.http.get(self.api_url.format(live_id)) api_data = self.session.http.json(res, schema=self._video_schema) self._set_metadata(api_data, 'Live') for playlist in api_data['videoReferences']: if playlist['format'] == 'dashhbbtv': yield from DASHStream.parse_manifest(self.session, playlist['url']).items() def _get_vod(self): vod_id = self._get_vod_id(self.url) if vod_id is None: res = self.session.http.get(self.url) match = self.latest_episode_url_re.search(res.text) if match is None: return vod_id = self._get_vod_id(match.group("url")) if vod_id is None: return log.debug("VOD ID={0}".format(vod_id)) res = self.session.http.get(self.api_url.format(vod_id)) api_data = self.session.http.json(res, schema=self._video_schema) self._set_metadata(api_data, 'VOD') substreams = {} if 'subtitleReferences' in api_data: for subtitle in api_data['subtitleReferences']: if subtitle['format'] == 'webvtt': log.debug("Subtitle={0}".format(subtitle['url'])) substreams[subtitle['format']] = HTTPStream( self.session, subtitle['url'], ) for manifest in api_data['videoReferences']: if manifest['format'] == 'dashhbbtv': for q, s in DASHStream.parse_manifest(self.session, manifest['url']).items(): if self.get_option('mux_subtitles') and substreams: yield q, MuxedStream(self.session, s, subtitles=substreams) else: yield q, s def _get_vod_id(self, url): qs = dict(parse_qsl(urlparse(url).query)) return qs.get("id") def _get_streams(self): path, live = self.match.groups() log.debug("Path={0}".format(path)) if live: return self._get_live(path) else: return self._get_vod() __plugin__ = SVTPlay ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/swisstxt.py0000644000175100001710000000250200000000000022127 0ustar00runnerdockerimport logging import re from urllib.parse import parse_qsl, urlparse, urlunparse from streamlink.plugin import Plugin, pluginmatcher from streamlink.stream.hls import HLSStream log = logging.getLogger(__name__) @pluginmatcher(re.compile(r""" https?://(?: live\.(rsi)\.ch/| (?:www\.)?(srf)\.ch/sport/resultcenter ) """, re.VERBOSE)) class Swisstxt(Plugin): api_url = "http://event.api.swisstxt.ch/v1/stream/{site}/byEventItemIdAndType/{id}/HLS" def get_stream_url(self, event_id): site = self.match.group(1) or self.match.group(2) api_url = self.api_url.format(id=event_id, site=site.upper()) log.debug("Calling API: {0}".format(api_url)) stream_url = self.session.http.get(api_url).text.strip("\"'") parsed = urlparse(stream_url) query = dict(parse_qsl(parsed.query)) return urlunparse(parsed._replace(query="")), query def _get_streams(self): event_id = dict(parse_qsl(urlparse(self.url).query.lower())).get("eventid") if event_id is None: return stream_url, params = self.get_stream_url(event_id) return HLSStream.parse_variant_playlist(self.session, stream_url, params=params) __plugin__ = Swisstxt ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/teamliquid.py0000644000175100001710000000206400000000000022360 0ustar00runnerdockerimport logging import re from urllib.parse import urlparse from streamlink.plugin import Plugin, pluginmatcher from streamlink.plugins.afreeca import AfreecaTV from streamlink.plugins.twitch import Twitch log = logging.getLogger(__name__) @pluginmatcher(re.compile( r"https?://(?:www\.)?(?:tl|teamliquid)\.net/video/streams/" )) class Teamliquid(Plugin): def _get_streams(self): res = self.session.http.get(self.url) stream_address_re = re.compile(r'''href\s*=\s*"([^"]+)"\s*>\s*View on''') stream_url_match = stream_address_re.search(res.text) if stream_url_match: stream_url = stream_url_match.group(1) log.info("Attempting to play streams from {0}".format(stream_url)) p = urlparse(stream_url) if p.netloc.endswith("afreecatv.com"): self.stream_weight = AfreecaTV.stream_weight elif p.netloc.endswith("twitch.tv"): self.stream_weight = Twitch.stream_weight return self.session.streams(stream_url) __plugin__ = Teamliquid ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/telefe.py0000644000175100001710000000363600000000000021474 0ustar00runnerdockerimport logging import re from streamlink.plugin import Plugin, pluginmatcher from streamlink.plugin.api import useragents from streamlink.stream.hls import HLSStream from streamlink.stream.http import HTTPStream from streamlink.utils.parse import parse_json log = logging.getLogger(__name__) @pluginmatcher(re.compile( r'https?://telefe\.com/.+' )) class Telefe(Plugin): def _get_streams(self): res = self.session.http.get(self.url, headers={'User-Agent': useragents.CHROME}) video_search = res.text video_search = video_search[video_search.index('{"top":{"view":"PlayerContainer","model":{'):] video_search = video_search[: video_search.index('}]}}') + 4] + "}" video_url_found_hls = "" video_url_found_http = "" json_video_search = parse_json(video_search) json_video_search_sources = json_video_search["top"]["model"]["videos"][0]["sources"] log.debug('Video ID found: {0}'.format(json_video_search["top"]["model"]["id"])) for current_video_source in json_video_search_sources: if "HLS" in current_video_source["type"]: video_url_found_hls = "http://telefe.com" + current_video_source["url"] log.debug("HLS content available") if "HTTP" in current_video_source["type"]: video_url_found_http = "http://telefe.com" + current_video_source["url"] log.debug("HTTP content available") self.session.http.headers = { 'Referer': self.url, 'User-Agent': useragents.CHROME, 'X-Requested-With': 'ShockwaveFlash/25.0.0.148' } if video_url_found_hls: hls_streams = HLSStream.parse_variant_playlist(self.session, video_url_found_hls) yield from hls_streams.items() if video_url_found_http: yield "http", HTTPStream(self.session, video_url_found_http) __plugin__ = Telefe ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/tf1.py0000644000175100001710000000421100000000000020710 0ustar00runnerdockerimport logging import re from streamlink.plugin import Plugin, PluginError, pluginmatcher from streamlink.plugin.api import useragents from streamlink.stream.dash import DASHStream from streamlink.stream.hls import HLSStream log = logging.getLogger(__name__) @pluginmatcher(re.compile( r"https?://(?:www\.)?(?:tf1\.fr/([\w-]+)/direct|(lci)\.fr/direct)/?" )) class TF1(Plugin): api_url = "https://mediainfo.tf1.fr/mediainfocombo/{}?context=MYTF1&pver=4001000" def api_call(self, channel, useragent=useragents.CHROME): url = self.api_url.format("L_" + channel.upper()) req = self.session.http.get(url, headers={"User-Agent": useragent}) return self.session.http.json(req) def get_stream_urls(self, channel): for useragent in [useragents.CHROME, useragents.IPHONE_6]: data = self.api_call(channel, useragent) if 'delivery' not in data or 'url' not in data['delivery']: continue log.debug("Got {format} stream {url}".format(**data['delivery'])) yield data['delivery']['format'], data['delivery']['url'] def _get_streams(self): m = self.match if m: channel = m.group(1) or m.group(2) log.debug("Found channel {0}".format(channel)) for sformat, url in self.get_stream_urls(channel): try: if sformat == "dash": yield from DASHStream.parse_manifest( self.session, url, headers={"User-Agent": useragents.CHROME} ).items() if sformat == "hls": yield from HLSStream.parse_variant_playlist( self.session, url, headers={"User-Agent": useragents.IPHONE}, ).items() except PluginError as e: log.error("Could not open {0} stream".format(sformat)) log.debug("Failed with error: {0}".format(e)) __plugin__ = TF1 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/theplatform.py0000644000175100001710000000234100000000000022545 0ustar00runnerdockerimport logging import re from streamlink.plugin import Plugin, pluginmatcher from streamlink.stream.hls import HLSStream log = logging.getLogger(__name__) @pluginmatcher(re.compile( r"https?://player\.theplatform\.com/p/" )) class ThePlatform(Plugin): release_re = re.compile(r'''tp:releaseUrl\s*=\s*"(.*?)"''') video_src_re = re.compile(r'''video.*?src="(.*?)"''') def _get_streams(self): res = self.session.http.get(self.url) m = self.release_re.search(res.text) release_url = m and m.group(1) if release_url: api_url = release_url + "&formats=m3u,mpeg4" res = self.session.http.get(api_url, allow_redirects=False, raise_for_status=False) if res.status_code == 302: stream_url = res.headers.get("Location") return HLSStream.parse_variant_playlist(self.session, stream_url, headers={ "Referer": self.url }) else: error = self.session.http.json(res) log.error("{0}: {1}".format( error.get("title", "Error"), error.get("description", "An unknown error occurred") )) __plugin__ = ThePlatform ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/tlctr.py0000644000175100001710000000152400000000000021352 0ustar00runnerdockerimport logging import re from streamlink.plugin import Plugin, pluginmatcher from streamlink.stream.hls import HLSStream log = logging.getLogger(__name__) @pluginmatcher(re.compile( r'https?://(?:www\.)?tlctv\.com\.tr/canli-izle' )) class TLCtr(Plugin): _hls_re = re.compile( r'''["'](?Phttps?://[^/]+/live/hls/[^"']+)["']''') def _get_streams(self): res = self.session.http.get(self.url) m = self._hls_re.search(res.text) if not m: log.error('No playlist found.') return hls_url = m.group('url') log.debug('URL={0}'.format(hls_url)) streams = HLSStream.parse_variant_playlist(self.session, hls_url) if not streams: return {'live': HLSStream(self.session, hls_url)} else: return streams __plugin__ = TLCtr ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/turkuvaz.py0000644000175100001710000000375400000000000022124 0ustar00runnerdockerimport logging import re from streamlink.plugin import Plugin, pluginmatcher from streamlink.plugin.api import useragents, validate from streamlink.stream.hls import HLSStream log = logging.getLogger(__name__) @pluginmatcher(re.compile(r"""https?://(?:www\.)? (?: (?: (atvavrupa)\.tv | (atv|a2tv|ahaber|aspor|minikago|minikacocuk|anews)\.com\.tr )/webtv/(?:live-broadcast|canli-yayin) | (ahaber)\.com\.tr/video/canli-yayin | atv\.com\.tr/(a2tv)/canli-yayin | sabah\.com\.tr/(apara)/canli-yayin ) """, re.VERBOSE)) class Turkuvaz(Plugin): _hls_url = "https://trkvz-live.ercdn.net/{channel}/{channel}.m3u8" _token_url = "https://securevideotoken.tmgrup.com.tr/webtv/secure" _token_schema = validate.Schema(validate.all( { "Success": True, "Url": validate.url(), }, validate.get("Url")) ) def _get_streams(self): url_m = self.match domain = url_m.group(1) or url_m.group(2) or url_m.group(3) or url_m.group(4) or url_m.group(5) # remap the domain to channel channel = {"atv": "atvhd", "ahaber": "ahaberhd", "apara": "aparahd", "aspor": "asporhd", "anews": "anewshd", "minikacocuk": "minikagococuk"}.get(domain, domain) hls_url = self._hls_url.format(channel=channel) # get the secure HLS URL res = self.session.http.get(self._token_url, params="url={0}".format(hls_url), headers={"Referer": self.url, "User-Agent": useragents.CHROME}) secure_hls_url = self.session.http.json(res, schema=self._token_schema) log.debug("Found HLS URL: {0}".format(secure_hls_url)) return HLSStream.parse_variant_playlist(self.session, secure_hls_url) __plugin__ = Turkuvaz ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/tv360.py0000644000175100001710000000125500000000000021105 0ustar00runnerdockerimport re from streamlink.plugin import Plugin, pluginmatcher from streamlink.plugin.api import validate from streamlink.stream.hls import HLSStream @pluginmatcher(re.compile( r"https?://(?:www\.)?tv360\.com\.tr/canli-yayin" )) class TV360(Plugin): hls_re = re.compile(r'''src="(http.*m3u8)"''') hls_schema = validate.Schema( validate.transform(hls_re.search), validate.any(None, validate.all(validate.get(1), validate.url())) ) def _get_streams(self): hls_url = self.session.http.get(self.url, schema=self.hls_schema) if hls_url: return HLSStream.parse_variant_playlist(self.session, hls_url) __plugin__ = TV360 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/tv3cat.py0000644000175100001710000000272400000000000021431 0ustar00runnerdockerimport logging import re from streamlink.plugin import Plugin, PluginError, pluginmatcher from streamlink.plugin.api import validate from streamlink.stream.hls import HLSStream log = logging.getLogger(__name__) @pluginmatcher(re.compile( r"https?://(?:www\.)?ccma\.cat/tv3/directe/(.+?)/" )) class TV3Cat(Plugin): _stream_info_url = "http://dinamics.ccma.cat/pvideo/media.jsp" \ "?media=video&version=0s&idint={ident}&profile=pc&desplacament=0" _media_schema = validate.Schema({ "geo": validate.text, "url": validate.url(scheme=validate.any("http", "https")) }) _channel_schema = validate.Schema({ "media": validate.any([_media_schema], _media_schema)}, validate.get("media"), # If there is only one item, it's not a list ... silly validate.transform(lambda x: x if isinstance(x, list) else [x]) ) def _get_streams(self): if self.match: ident = self.match.group(1) data_url = self._stream_info_url.format(ident=ident) stream_infos = self.session.http.json(self.session.http.get(data_url), schema=self._channel_schema) for stream in stream_infos: try: return HLSStream.parse_variant_playlist(self.session, stream['url'], name_fmt="{pixels}_{bitrate}") except PluginError: log.debug("Failed to get streams for: {0}".format(stream['geo'])) __plugin__ = TV3Cat ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/tv4play.py0000644000175100001710000000453200000000000021627 0ustar00runnerdockerimport logging import re from urllib.parse import urljoin from streamlink.plugin import Plugin, PluginError, pluginmatcher from streamlink.plugin.api import validate from streamlink.stream.hls import HLSStream log = logging.getLogger(__name__) @pluginmatcher(re.compile(r""" https?://(?:www\.)? (?: tv4play\.se/program/[^?/]+/[^?/]+ | fotbollskanalen\.se/video ) /(?P\d+) """, re.VERBOSE)) class TV4Play(Plugin): video_id = None api_url = "https://playback-api.b17g.net" api_assets = urljoin(api_url, "/asset/{0}") _meta_schema = validate.Schema( { "metadata": { "title": validate.text }, "mediaUri": validate.text } ) @property def get_video_id(self): if self.video_id is None: self.video_id = self.match.group("video_id") log.debug("Found video ID: {0}".format(self.video_id)) return self.video_id def get_metadata(self): params = { "device": "browser", "protocol": "hls", "service": "tv4", } try: res = self.session.http.get( self.api_assets.format(self.get_video_id), params=params ) except Exception as e: if "404 Client Error" in str(e): raise PluginError("This Video is not available") raise e log.debug("Found metadata") metadata = self.session.http.json(res, schema=self._meta_schema) self.title = metadata["metadata"]["title"] return metadata def get_title(self): if self.title is None: self.get_metadata() return self.title def _get_streams(self): metadata = self.get_metadata() try: res = self.session.http.get(urljoin(self.api_url, metadata["mediaUri"])) except Exception as e: if "401 Client Error" in str(e): raise PluginError("This Video is not available in your country") raise e log.debug("Found stream data") data = self.session.http.json(res) hls_url = data["playbackItem"]["manifestUrl"] log.debug("URL={0}".format(hls_url)) yield from HLSStream.parse_variant_playlist(self.session, hls_url).items() __plugin__ = TV4Play ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/tv5monde.py0000644000175100001710000000473000000000000021765 0ustar00runnerdockerimport re from urllib.parse import urlparse from streamlink.plugin import Plugin, PluginError, pluginmatcher from streamlink.plugin.api import validate from streamlink.stream.hls import HLSStream from streamlink.stream.http import HTTPStream from streamlink.utils.url import update_scheme @pluginmatcher(re.compile(r""" https?://(?:[\w-]+\.)*(?:tv5monde|tivi5mondeplus)\.com/ """, re.VERBOSE)) class TV5Monde(Plugin): def _get_hls(self, root): schema_live = validate.Schema( validate.xml_xpath_string(".//*[contains(@data-broadcast,'m3u8')]/@data-broadcast"), str, validate.parse_json(), validate.any( validate.all({"files": list}, validate.get("files")), list ), [{ "url": validate.url(path=validate.endswith(".m3u8")) }], validate.get((0, "url")), validate.transform(lambda content_url: update_scheme("https://", content_url)) ) try: live = schema_live.validate(root) except PluginError: return return HLSStream.parse_variant_playlist(self.session, live) def _get_vod(self, root): schema_vod = validate.Schema( validate.xml_xpath_string(".//script[@type='application/ld+json'][contains(text(),'VideoObject')][1]/text()"), str, validate.transform(lambda jsonlike: re.sub(r"[\r\n]+", "", jsonlike)), validate.parse_json(), validate.any( validate.all( {"@graph": [dict]}, validate.get("@graph"), validate.filter(lambda obj: obj["@type"] == "VideoObject"), validate.get(0) ), dict ), {"contentUrl": validate.url()}, validate.get("contentUrl"), validate.transform(lambda content_url: update_scheme("https://", content_url)) ) try: vod = schema_vod.validate(root) except PluginError: return if urlparse(vod).path.endswith(".m3u8"): return HLSStream.parse_variant_playlist(self.session, vod) return {"vod": HTTPStream(self.session, vod)} def _get_streams(self): root = self.session.http.get(self.url, schema=validate.Schema( validate.parse_html() )) return self._get_hls(root) or self._get_vod(root) __plugin__ = TV5Monde ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/tv8.py0000644000175100001710000000136000000000000020741 0ustar00runnerdockerimport logging import re from streamlink.plugin import Plugin, pluginmatcher from streamlink.plugin.api import validate from streamlink.stream.hls import HLSStream log = logging.getLogger(__name__) @pluginmatcher(re.compile( r'https?://www\.tv8\.com\.tr/canli-yayin' )) class TV8(Plugin): _re_hls = re.compile(r"""file\s*:\s*(["'])(?Phttps?://.*?\.m3u8.*?)\1""") title = "TV8" def _get_streams(self): hls_url = self.session.http.get(self.url, schema=validate.Schema( validate.transform(self._re_hls.search), validate.any(None, validate.get("hls_url")) )) if hls_url is not None: return HLSStream.parse_variant_playlist(self.session, hls_url) __plugin__ = TV8 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/tv999.py0000644000175100001710000000204100000000000021121 0ustar00runnerdockerimport logging import re from streamlink.plugin import Plugin, pluginmatcher from streamlink.plugin.api import validate from streamlink.stream.hls import HLSStream from streamlink.utils.url import update_scheme log = logging.getLogger(__name__) @pluginmatcher(re.compile( r"https?://(?:www\.)?tv999\.bg/live" )) class TV999(Plugin): title = "TV999" def _get_xpath_string(self, url, xpath): return self.session.http.get( url, schema=validate.Schema( validate.parse_html(), validate.xml_xpath_string(xpath), validate.any(None, validate.url()) ) ) def _get_streams(self): iframe_url = self._get_xpath_string(self.url, ".//iframe[@src]/@src") if not iframe_url: return hls_url = self._get_xpath_string(iframe_url, ".//source[contains(@src,'m3u8')]/@src") if not hls_url: return return {"live": HLSStream(self.session, update_scheme("http://", hls_url))} __plugin__ = TV999 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/tvibo.py0000644000175100001710000000146700000000000021353 0ustar00runnerdockerimport logging import re from streamlink.plugin import Plugin, pluginmatcher from streamlink.stream.hls import HLSStream log = logging.getLogger(__name__) @pluginmatcher(re.compile( r"https?://player\.tvibo\.com/\w+/(?P\d+)" )) class Tvibo(Plugin): _api_url = "http://panel.tvibo.com/api/player/streamurl/{id}" def _get_streams(self): channel_id = self.match.group("id") api_response = self.session.http.get( self._api_url.format(id=channel_id), acceptable_status=(200, 404)) data = self.session.http.json(api_response) log.trace("{0!r}".format(data)) if data.get("st"): yield "source", HLSStream(self.session, data["st"]) elif data.get("error"): log.error(data["error"]["message"]) __plugin__ = Tvibo ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/tviplayer.py0000644000175100001710000000463200000000000022244 0ustar00runnerdockerimport logging import re from streamlink.plugin import Plugin, pluginmatcher from streamlink.plugin.api import validate from streamlink.stream.hls import HLSStream from streamlink.utils.url import update_qsd log = logging.getLogger(__name__) @pluginmatcher(re.compile( r"https://tviplayer\.iol\.pt/(?:direto|programa)/", )) class TVIPlayer(Plugin): _re_jsonData = re.compile(r"jsonData\s*=\s*(?P{.+?})\s*;", re.DOTALL) def _get_streams(self): self.session.http.headers.update({"Referer": "https://tviplayer.iol.pt/"}) data = self.session.http.get( self.url, schema=validate.Schema( validate.parse_html(), validate.xml_xpath_string(".//script[contains(text(),'.m3u8')]/text()"), str, validate.transform(self._re_jsonData.search), validate.any(None, validate.all( validate.get("json"), validate.parse_json(), { "id": str, "liveType": str, "videoType": str, "videoUrl": validate.url(path=validate.endswith(".m3u8")), validate.optional("channel"): str, } )) ) ) if not data: return log.debug(f"{data!r}") if data["liveType"].upper() == "DIRETO" and data["videoType"].upper() == "LIVE": geo_path = "live" else: geo_path = "vod" data_geo = self.session.http.get( f"https://services.iol.pt/direitos/rights/{geo_path}?id={data['id']}", acceptable_status=(200, 403), schema=validate.Schema( validate.parse_json(), { "code": str, "error": validate.any(None, str), "detail": str, } ) ) log.debug(f"{data_geo!r}") if data_geo["detail"] != "ok": log.error(f"{data_geo['detail']}") return wmsAuthSign = self.session.http.get( "https://services.iol.pt/matrix?userId=", schema=validate.Schema(str) ) hls_url = update_qsd(data["videoUrl"], {"wmsAuthSign": wmsAuthSign}) return HLSStream.parse_variant_playlist(self.session, hls_url) __plugin__ = TVIPlayer ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/tvp.py0000644000175100001710000000312700000000000021034 0ustar00runnerdockerimport logging import re from streamlink.plugin import Plugin, PluginError, pluginmatcher from streamlink.stream.hls import HLSStream from streamlink.stream.http import HTTPStream log = logging.getLogger(__name__) @pluginmatcher(re.compile( r'https?://tvpstream\.vod\.tvp\.pl' )) class TVP(Plugin): player_url = 'https://www.tvp.pl/sess/tvplayer.php?object_id={0}&autoplay=true' _stream_re = re.compile(r'''src:["'](?P[^"']+\.(?:m3u8|mp4))["']''') _video_id_re = re.compile(r'''class=["']tvp_player["'][^>]+data-video-id=["'](?P\d+)["']''') def get_embed_url(self): res = self.session.http.get(self.url) m = self._video_id_re.search(res.text) if not m: raise PluginError('Unable to find a video id') video_id = m.group('video_id') log.debug('Found video id: {0}'.format(video_id)) p_url = self.player_url.format(video_id) return p_url def _get_streams(self): embed_url = self.get_embed_url() res = self.session.http.get(embed_url) m = self._stream_re.findall(res.text) if not m: raise PluginError('Unable to find a stream url') streams = [] for url in m: log.debug('URL={0}'.format(url)) if url.endswith('.m3u8'): for s in HLSStream.parse_variant_playlist(self.session, url, name_fmt='{pixels}_{bitrate}').items(): streams.append(s) elif url.endswith('.mp4'): streams.append(('vod', HTTPStream(self.session, url))) return streams __plugin__ = TVP ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/tvrby.py0000644000175100001710000000276000000000000021373 0ustar00runnerdockerimport logging import re from streamlink.plugin import Plugin, pluginmatcher from streamlink.plugin.api import validate from streamlink.stream.hls import HLSStream log = logging.getLogger(__name__) @pluginmatcher(re.compile( r"https?://(?:www\.)?tvr\.by/televidenie/belarus" )) class TVRBy(Plugin): file_re = re.compile(r"""(?Phttps://stream\.hoster\.by[^"',]+\.m3u8[^"',]*)""") player_re = re.compile(r"""["'](?P[^"']+tvr\.by/plugines/online-tv-main\.php[^"']+)["']""") stream_schema = validate.Schema( validate.all( validate.transform(file_re.finditer), validate.transform(list), [validate.get("url")], # remove duplicates validate.transform(set), validate.transform(list), ), ) def __init__(self, url): # ensure the URL ends with a / if not url.endswith("/"): url += "/" super().__init__(url) def _get_streams(self): res = self.session.http.get(self.url) m = self.player_re.search(res.text) if not m: return player_url = m.group("url") res = self.session.http.get(player_url) stream_urls = self.stream_schema.validate(res.text) log.debug("Found {0} stream URL{1}".format(len(stream_urls), "" if len(stream_urls) == 1 else "s")) for stream_url in stream_urls: yield from HLSStream.parse_variant_playlist(self.session, stream_url).items() __plugin__ = TVRBy ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/tvrplus.py0000644000175100001710000000174000000000000021741 0ustar00runnerdockerimport logging import re from streamlink.plugin import Plugin, pluginmatcher from streamlink.plugin.api import validate from streamlink.stream.hls import HLSStream log = logging.getLogger(__name__) @pluginmatcher(re.compile( r"https?://(?:www\.)?tvrplus\.ro/live/" )) class TVRPlus(Plugin): hls_file_re = re.compile(r"""["'](?P[^"']+\.m3u8(?:[^"']+)?)["']""") stream_schema = validate.Schema( validate.all( validate.transform(hls_file_re.findall), validate.any(None, [validate.text]) ), ) def _get_streams(self): headers = {"Referer": self.url} stream_url = self.stream_schema.validate(self.session.http.get(self.url).text) if stream_url: stream_url = list(set(stream_url)) for url in stream_url: log.debug("URL={0}".format(url)) yield from HLSStream.parse_variant_playlist(self.session, url, headers=headers).items() __plugin__ = TVRPlus ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/tvtoya.py0000644000175100001710000000132200000000000021544 0ustar00runnerdockerimport logging import re from streamlink.plugin import Plugin, pluginmatcher from streamlink.stream.hls import HLSStream log = logging.getLogger(__name__) @pluginmatcher(re.compile( r"https?://(?:www\.)?tvtoya\.pl/live" )) class TVToya(Plugin): _playlist_re = re.compile(r'') def _get_streams(self): self.session.set_option('hls-live-edge', 10) res = self.session.http.get(self.url) playlist_m = self._playlist_re.search(res.text) if playlist_m: return HLSStream.parse_variant_playlist(self.session, playlist_m.group(1)) else: log.debug("Could not find stream data") __plugin__ = TVToya ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/twitcasting.py0000644000175100001710000001113000000000000022554 0ustar00runnerdockerimport hashlib import logging import re from streamlink.buffers import RingBuffer from streamlink.plugin import Plugin, PluginArgument, PluginArguments, PluginError, pluginmatcher from streamlink.plugin.api import validate from streamlink.plugin.api.websocket import WebsocketClient from streamlink.stream.stream import Stream from streamlink.stream.stream import StreamIO from streamlink.utils.url import update_qsd log = logging.getLogger(__name__) @pluginmatcher(re.compile( r"https?://twitcasting\.tv/(?P[^/]+)" )) class TwitCasting(Plugin): arguments = PluginArguments( PluginArgument( "password", sensitive=True, metavar="PASSWORD", help="Password for private Twitcasting streams." ) ) _STREAM_INFO_URL = "https://twitcasting.tv/streamserver.php?target={channel}&mode=client" _STREAM_REAL_URL = "{proto}://{host}/ws.app/stream/{movie_id}/fmp4/bd/1/1500?mode={mode}" _STREAM_INFO_SCHEMA = validate.Schema({ validate.optional("movie"): { "id": int, "live": bool }, validate.optional("fmp4"): { "host": str, "proto": str, "source": bool, "mobilesource": bool } }) def __init__(self, url): super().__init__(url) self.channel = self.match.group("channel") def _get_streams(self): stream_info = self._get_stream_info() log.debug(f"Live stream info: {stream_info}") if not stream_info.get("movie") or not stream_info["movie"]["live"]: raise PluginError("The live stream is offline") if not stream_info.get("fmp4"): raise PluginError("Login required") # Keys are already validated by schema above proto = stream_info["fmp4"]["proto"] host = stream_info["fmp4"]["host"] movie_id = stream_info["movie"]["id"] if stream_info["fmp4"]["source"]: mode = "main" # High quality elif stream_info["fmp4"]["mobilesource"]: mode = "mobilesource" # Medium quality else: mode = "base" # Low quality if (proto == '') or (host == '') or (not movie_id): raise PluginError(f"No stream available for user {self.channel}") real_stream_url = self._STREAM_REAL_URL.format(proto=proto, host=host, movie_id=movie_id, mode=mode) password = self.options.get("password") if password is not None: password_hash = hashlib.md5(password.encode()).hexdigest() real_stream_url = update_qsd(real_stream_url, {"word": password_hash}) log.debug(f"Real stream url: {real_stream_url}") return {mode: TwitCastingStream(session=self.session, url=real_stream_url)} def _get_stream_info(self): url = self._STREAM_INFO_URL.format(channel=self.channel) res = self.session.http.get(url) return self.session.http.json(res, schema=self._STREAM_INFO_SCHEMA) class TwitCastingWsClient(WebsocketClient): def __init__(self, buffer: RingBuffer, *args, **kwargs): self.buffer = buffer super().__init__(*args, **kwargs) def on_close(self, *args, **kwargs): super().on_close(*args, **kwargs) self.buffer.close() def on_message(self, wsapp, data: str) -> None: try: self.buffer.write(data) except Exception as err: log.error(err) self.close() class TwitCastingReader(StreamIO): def __init__(self, stream: "TwitCastingStream", timeout=None): super().__init__() self.session = stream.session self.stream = stream self.timeout = timeout or self.session.options.get("stream-timeout") buffer_size = self.session.get_option("ringbuffer-size") self.buffer = RingBuffer(buffer_size) self.wsclient = TwitCastingWsClient( self.buffer, stream.session, stream.url, origin="https://twitcasting.tv/" ) def open(self): self.wsclient.start() def close(self): self.wsclient.close() self.buffer.close() def read(self, size): return self.buffer.read( size, block=self.wsclient.is_alive(), timeout=self.timeout ) class TwitCastingStream(Stream): def __init__(self, session, url): super().__init__(session) self.url = url def __repr__(self): return f"" def open(self): reader = TwitCastingReader(self) reader.open() return reader __plugin__ = TwitCasting ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/twitch.py0000644000175100001710000006010200000000000021521 0ustar00runnerdockerimport json import logging import re from datetime import datetime from random import random from typing import List, NamedTuple, Optional from urllib.parse import urlparse import requests from streamlink.exceptions import NoStreamsError, PluginError from streamlink.plugin import Plugin, PluginArgument, PluginArguments, pluginmatcher from streamlink.plugin.api import validate from streamlink.stream.hls import HLSStream, HLSStreamReader, HLSStreamWorker, HLSStreamWriter from streamlink.stream.hls_playlist import ByteRange, ExtInf, Key, M3U8, M3U8Parser, Map, load as load_hls_playlist from streamlink.stream.http import HTTPStream from streamlink.utils.args import keyvalue from streamlink.utils.parse import parse_json, parse_qsd from streamlink.utils.times import hours_minutes_seconds from streamlink.utils.url import update_qsd log = logging.getLogger(__name__) LOW_LATENCY_MAX_LIVE_EDGE = 2 class TwitchSegment(NamedTuple): uri: str duration: float title: Optional[str] key: Optional[Key] discontinuity: bool byterange: Optional[ByteRange] date: Optional[datetime] map: Optional[Map] ad: bool prefetch: bool # generic namedtuples are unsupported, so just subclass class TwitchSequence(NamedTuple): num: int segment: TwitchSegment class TwitchM3U8(M3U8): segments: List[TwitchSegment] def __init__(self): super().__init__() self.dateranges_ads = [] class TwitchM3U8Parser(M3U8Parser): m3u8: TwitchM3U8 def parse_tag_ext_x_twitch_prefetch(self, value): segments = self.m3u8.segments if not segments: # pragma: no cover return last = segments[-1] # Use the average duration of all regular segments for the duration of prefetch segments. # This is better than using the duration of the last segment when regular segment durations vary a lot. # In low latency mode, the playlist reload time is the duration of the last segment. duration = last.duration if last.prefetch else sum(segment.duration for segment in segments) / float(len(segments)) segments.append(last._replace( uri=self.uri(value), duration=duration, prefetch=True )) def parse_tag_ext_x_daterange(self, value): super().parse_tag_ext_x_daterange(value) daterange = self.m3u8.dateranges[-1] is_ad = ( daterange.classname == "twitch-stitched-ad" or str(daterange.id or "").startswith("stitched-ad-") or any(attr_key.startswith("X-TV-TWITCH-AD-") for attr_key in daterange.x.keys()) ) if is_ad: self.m3u8.dateranges_ads.append(daterange) def get_segment(self, uri: str) -> TwitchSegment: extinf: ExtInf = self.state.pop("extinf", None) or ExtInf(0, None) date = self.state.pop("date", None) ad = any(self.m3u8.is_date_in_daterange(date, daterange) for daterange in self.m3u8.dateranges_ads) return TwitchSegment( uri=uri, duration=extinf.duration, title=extinf.title, key=self.state.get("key"), discontinuity=self.state.pop("discontinuity", False), byterange=self.state.pop("byterange", None), date=date, map=self.state.get("map"), ad=ad, prefetch=False ) class TwitchHLSStreamWorker(HLSStreamWorker): def __init__(self, reader, *args, **kwargs): self.had_content = False super().__init__(reader, *args, **kwargs) def _reload_playlist(self, *args): return load_hls_playlist(*args, parser=TwitchM3U8Parser, m3u8=TwitchM3U8) def _playlist_reload_time(self, playlist: TwitchM3U8, sequences: List[TwitchSequence]): if self.stream.low_latency and sequences: return sequences[-1].segment.duration return super()._playlist_reload_time(playlist, sequences) def process_sequences(self, playlist: TwitchM3U8, sequences: List[TwitchSequence]): # ignore prefetch segments if not LL streaming if not self.stream.low_latency: sequences = [seq for seq in sequences if not seq.segment.prefetch] # check for sequences with real content if not self.had_content: self.had_content = next((True for seq in sequences if not seq.segment.ad), False) # When filtering ads, to check whether it's a LL stream, we need to wait for the real content to show up, # since playlists with only ad segments don't contain prefetch segments if ( self.stream.low_latency and self.had_content and not next((True for seq in sequences if seq.segment.prefetch), False) ): log.info("This is not a low latency stream") # show pre-roll ads message only on the first playlist containing ads if self.stream.disable_ads and self.playlist_sequence == -1 and not self.had_content: log.info("Waiting for pre-roll ads to finish, be patient") return super().process_sequences(playlist, sequences) class TwitchHLSStreamWriter(HLSStreamWriter): def should_filter_sequence(self, sequence: TwitchSequence): return self.stream.disable_ads and sequence.segment.ad class TwitchHLSStreamReader(HLSStreamReader): __worker__ = TwitchHLSStreamWorker __writer__ = TwitchHLSStreamWriter def __init__(self, stream): if stream.disable_ads: log.info("Will skip ad segments") if stream.low_latency: live_edge = max(1, min(LOW_LATENCY_MAX_LIVE_EDGE, stream.session.options.get("hls-live-edge"))) stream.session.options.set("hls-live-edge", live_edge) stream.session.options.set("hls-segment-stream-data", True) log.info(f"Low latency streaming (HLS live edge: {live_edge})") super().__init__(stream) class TwitchHLSStream(HLSStream): __reader__ = TwitchHLSStreamReader def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.disable_ads = self.session.get_plugin_option("twitch", "disable-ads") self.low_latency = self.session.get_plugin_option("twitch", "low-latency") class UsherService: def __init__(self, session): self.session = session def _create_url(self, endpoint, **extra_params): url = f"https://usher.ttvnw.net{endpoint}" params = { "player": "twitchweb", "p": int(random() * 999999), "type": "any", "allow_source": "true", "allow_audio_only": "true", "allow_spectre": "false", } params.update(extra_params) req = requests.Request("GET", url, params=params) req = self.session.http.prepare_request(req) return req.url def channel(self, channel, **extra_params): try: extra_params_debug = validate.Schema( validate.get("token"), validate.parse_json(), { "adblock": bool, "geoblock_reason": str, "hide_ads": bool, "server_ads": bool, "show_ads": bool, } ).validate(extra_params) log.debug(f"{extra_params_debug!r}") except PluginError: pass return self._create_url(f"/api/channel/hls/{channel}.m3u8", **extra_params) def video(self, video_id, **extra_params): return self._create_url(f"/vod/{video_id}", **extra_params) class TwitchAPI: def __init__(self, session): self.session = session self.headers = { "Client-ID": "kimne78kx3ncx6brgo4mv6wki5h1ko", } self.headers.update(**{k: v for k, v in session.get_plugin_option("twitch", "api-header") or []}) def call(self, data, schema=None): res = self.session.http.post( "https://gql.twitch.tv/gql", data=json.dumps(data), headers=self.headers ) return self.session.http.json(res, schema=schema) @staticmethod def _gql_persisted_query(operationname, sha256hash, **variables): return { "operationName": operationname, "extensions": { "persistedQuery": { "version": 1, "sha256Hash": sha256hash } }, "variables": dict(**variables) } @staticmethod def parse_token(tokenstr): return parse_json(tokenstr, schema=validate.Schema( {"chansub": {"restricted_bitrates": validate.all( [str], validate.filter(lambda n: not re.match(r"(.+_)?archives|live|chunked", n)) )}}, validate.get(("chansub", "restricted_bitrates")) )) # GraphQL API calls def metadata_video(self, video_id): query = self._gql_persisted_query( "VideoMetadata", "cb3b1eb2f2d2b2f65b8389ba446ec521d76c3aa44f5424a1b1d235fe21eb4806", channelLogin="", # parameter can be empty videoID=video_id ) return self.call(query, schema=validate.Schema( {"data": {"video": { "id": str, "owner": { "displayName": str }, "title": str, "game": { "displayName": str } }}}, validate.get(("data", "video")), validate.union_get( "id", ("owner", "displayName"), ("game", "displayName"), "title" ) )) def metadata_channel(self, channel): queries = [ self._gql_persisted_query( "ChannelShell", "c3ea5a669ec074a58df5c11ce3c27093fa38534c94286dc14b68a25d5adcbf55", login=channel, lcpVideosEnabled=False ), self._gql_persisted_query( "StreamMetadata", "059c4653b788f5bdb2f5a2d2a24b0ddc3831a15079001a3d927556a96fb0517f", channelLogin=channel ) ] return self.call(queries, schema=validate.Schema( [ validate.all( {"data": {"userOrError": { "displayName": str }}} ), validate.all( {"data": {"user": { "lastBroadcast": { "title": str }, "stream": { "id": str, "game": { "name": str } } }}} ) ], validate.union_get( (1, "data", "user", "stream", "id"), (0, "data", "userOrError", "displayName"), (1, "data", "user", "stream", "game", "name"), (1, "data", "user", "lastBroadcast", "title") ) )) def metadata_clips(self, clipname): queries = [ self._gql_persisted_query( "ClipsView", "4480c1dcc2494a17bb6ef64b94a5213a956afb8a45fe314c66b0d04079a93a8f", slug=clipname ), self._gql_persisted_query( "ClipsTitle", "f6cca7f2fdfbfc2cecea0c88452500dae569191e58a265f97711f8f2a838f5b4", slug=clipname ) ] return self.call(queries, schema=validate.Schema( [ validate.all( {"data": {"clip": { "id": str, "broadcaster": {"displayName": str}, "game": {"name": str} }}}, validate.get(("data", "clip")) ), validate.all( {"data": {"clip": {"title": str}}}, validate.get(("data", "clip")) ) ], validate.union_get( (0, "id"), (0, "broadcaster", "displayName"), (0, "game", "name"), (1, "title") ) )) def access_token(self, is_live, channel_or_vod): query = self._gql_persisted_query( "PlaybackAccessToken", "0828119ded1c13477966434e15800ff57ddacf13ba1911c129dc2200705b0712", isLive=is_live, login=channel_or_vod if is_live else "", isVod=not is_live, vodID=channel_or_vod if not is_live else "", playerType="embed" ) subschema = validate.any(None, validate.all( { "value": str, "signature": str }, validate.union_get("signature", "value") )) return self.call(query, schema=validate.Schema( {"data": validate.any( validate.all( {"streamPlaybackAccessToken": subschema}, validate.get("streamPlaybackAccessToken") ), validate.all( {"videoPlaybackAccessToken": subschema}, validate.get("videoPlaybackAccessToken") ) )}, validate.get("data") )) def clips(self, clipname): query = self._gql_persisted_query( "VideoAccessToken_Clip", "36b89d2507fce29e5ca551df756d27c1cfe079e2609642b4390aa4c35796eb11", slug=clipname ) return self.call(query, schema=validate.Schema( {"data": {"clip": { "playbackAccessToken": { "signature": str, "value": str }, "videoQualities": [validate.all( { "frameRate": validate.transform(int), "quality": str, "sourceURL": validate.url() }, validate.transform(lambda q: ( f"{q['quality']}p{q['frameRate']}", q["sourceURL"] )) )] }}}, validate.get(("data", "clip")), validate.union_get( ("playbackAccessToken", "signature"), ("playbackAccessToken", "value"), "videoQualities" ) )) def stream_metadata(self, channel): query = self._gql_persisted_query( "StreamMetadata", "1c719a40e481453e5c48d9bb585d971b8b372f8ebb105b17076722264dfa5b3e", channelLogin=channel ) return self.call(query, schema=validate.Schema( {"data": {"user": {"stream": {"type": str}}}}, validate.get(("data", "user", "stream")) )) def hosted_channel(self, channel): query = self._gql_persisted_query( "UseHosting", "427f55a3daca510f726c02695a898ef3a0de4355b39af328848876052ea6b337", channelLogin=channel ) return self.call(query, schema=validate.Schema( {"data": {"user": { "hosting": { "login": str, "displayName": str } }}}, validate.get(("data", "user", "hosting")), validate.union_get("login", "displayName") )) @pluginmatcher(re.compile(r""" https?://(?:(?P[\w-]+)\.)?twitch\.tv/ (?: videos/(?P\d+) | (?P[^/?]+) (?: /video/(?P\d+) | /clip/(?P[^/?]+) )? ) """, re.VERBOSE)) class Twitch(Plugin): arguments = PluginArguments( PluginArgument( "disable-hosting", action="store_true", help=""" Do not open the stream if the target channel is hosting another channel. """ ), PluginArgument( "disable-ads", action="store_true", help=""" Skip embedded advertisement segments at the beginning or during a stream. Will cause these segments to be missing from the stream. """ ), PluginArgument( "disable-reruns", action="store_true", help=""" Do not open the stream if the target channel is currently broadcasting a rerun. """ ), PluginArgument( "low-latency", action="store_true", help=f""" Enables low latency streaming by prefetching HLS segments. Sets --hls-segment-stream-data to true and --hls-live-edge to {LOW_LATENCY_MAX_LIVE_EDGE}, if it is higher. Reducing --hls-live-edge to 1 will result in the lowest latency possible, but will most likely cause buffering. In order to achieve true low latency streaming during playback, the player's caching/buffering settings will need to be adjusted and reduced to a value as low as possible, but still high enough to not cause any buffering. This depends on the stream's bitrate and the quality of the connection to Twitch's servers. Please refer to the player's own documentation for the required configuration. Player parameters can be set via --player-args. Note: Low latency streams have to be enabled by the broadcasters on Twitch themselves. Regular streams can cause buffering issues with this option enabled due to the reduced --hls-live-edge value. """ ), PluginArgument( "api-header", metavar="KEY=VALUE", type=keyvalue, action="append", help=""" A header to add to each Twitch API HTTP request. Can be repeated to add multiple headers. """ ) ) def __init__(self, url): super().__init__(url) match = self.match.groupdict() parsed = urlparse(url) self.params = parse_qsd(parsed.query) self.subdomain = match.get("subdomain") self.video_id = None self.channel = None self.clip_name = None self._checked_metadata = False if self.subdomain == "player": # pop-out player if self.params.get("video"): self.video_id = self.params["video"] self.channel = self.params.get("channel") elif self.subdomain == "clips": # clip share URL self.clip_name = match.get("channel") else: self.channel = match.get("channel") and match.get("channel").lower() self.video_id = match.get("video_id") or match.get("videos_id") self.clip_name = match.get("clip_name") self.api = TwitchAPI(session=self.session) self.usher = UsherService(session=self.session) def method_factory(parent_method): def inner(): if not self._checked_metadata: self._checked_metadata = True self._get_metadata() return parent_method() return inner parent = super() for metadata in "id", "author", "category", "title": method = f"get_{metadata}" setattr(self, method, method_factory(getattr(parent, method))) def _get_metadata(self): try: if self.video_id: data = self.api.metadata_video(self.video_id) elif self.clip_name: data = self.api.metadata_clips(self.clip_name) elif self.channel: data = self.api.metadata_channel(self.channel) else: # pragma: no cover return self.id, self.author, self.category, self.title = data except (PluginError, TypeError): pass def _access_token(self, is_live, channel_or_vod): try: sig, token = self.api.access_token(is_live, channel_or_vod) except (PluginError, TypeError): raise NoStreamsError(self.url) try: restricted_bitrates = self.api.parse_token(token) except PluginError: restricted_bitrates = [] return sig, token, restricted_bitrates def _switch_to_hosted_channel(self): disabled = self.options.get("disable_hosting") hosted_chain = [self.channel] while True: try: login, display_name = self.api.hosted_channel(self.channel) except PluginError: return False log.info(f"{self.channel} is hosting {login}") if disabled: log.info("hosting was disabled by command line option") return True if login in hosted_chain: loop = " -> ".join(hosted_chain + [login]) log.error(f"A loop of hosted channels has been detected, cannot find a playable stream. ({loop})") return True hosted_chain.append(login) log.info(f"switching to {login}") self.channel = login self.author = display_name def _check_for_rerun(self): if not self.options.get("disable_reruns"): return False try: stream = self.api.stream_metadata(self.channel) if stream["type"] != "live": log.info("Reruns were disabled by command line option") return True except (PluginError, TypeError): pass return False def _get_hls_streams_live(self): if self._switch_to_hosted_channel(): return if self._check_for_rerun(): return # only get the token once the channel has been resolved log.debug(f"Getting live HLS streams for {self.channel}") self.session.http.headers.update({ "referer": "https://player.twitch.tv", "origin": "https://player.twitch.tv", }) sig, token, restricted_bitrates = self._access_token(True, self.channel) url = self.usher.channel(self.channel, sig=sig, token=token, fast_bread=True) return self._get_hls_streams(url, restricted_bitrates) def _get_hls_streams_video(self): log.debug(f"Getting HLS streams for video ID {self.video_id}") sig, token, restricted_bitrates = self._access_token(False, self.video_id) url = self.usher.video(self.video_id, nauthsig=sig, nauth=token) # If the stream is a VOD that is still being recorded, the stream should start at the beginning of the recording return self._get_hls_streams(url, restricted_bitrates, force_restart=True) def _get_hls_streams(self, url, restricted_bitrates, **extra_params): time_offset = self.params.get("t", 0) if time_offset: try: time_offset = hours_minutes_seconds(time_offset) except ValueError: time_offset = 0 try: streams = TwitchHLSStream.parse_variant_playlist(self.session, url, start_offset=time_offset, **extra_params) except OSError as err: err = str(err) if "404 Client Error" in err or "Failed to parse playlist" in err: return else: raise PluginError(err) for name in restricted_bitrates: if name not in streams: log.warning(f"The quality '{name}' is not available since it requires a subscription.") return streams def _get_clips(self): try: sig, token, streams = self.api.clips(self.clip_name) except (PluginError, TypeError): return for quality, stream in streams: yield quality, HTTPStream(self.session, update_qsd(stream, {"sig": sig, "token": token})) def _get_streams(self): if self.video_id: return self._get_hls_streams_video() elif self.clip_name: return self._get_clips() elif self.channel: return self._get_hls_streams_live() __plugin__ = Twitch ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/ustreamtv.py0000644000175100001710000004413500000000000022261 0ustar00runnerdockerimport logging import re from collections import deque from datetime import datetime, timedelta from random import randint from threading import Event, RLock from typing import Any, Callable, Deque, Dict, List, NamedTuple, Union from urllib.parse import urljoin, urlunparse from requests import Response from streamlink.exceptions import PluginError, StreamError from streamlink.plugin import Plugin, PluginArgument, PluginArguments, pluginmatcher from streamlink.plugin.api import useragents, validate from streamlink.plugin.api.websocket import WebsocketClient from streamlink.stream.ffmpegmux import MuxedStream from streamlink.stream.segmented import SegmentedStreamReader, SegmentedStreamWorker, SegmentedStreamWriter from streamlink.stream.stream import Stream from streamlink.utils.parse import parse_json log = logging.getLogger(__name__) # TODO: use dataclasses for stream formats after dropping py36 to be able to subclass class StreamFormatVideo(NamedTuple): contentType: str sourceStreamVersion: int initUrl: str segmentUrl: str bitrate: int height: int class StreamFormatAudio(NamedTuple): contentType: str sourceStreamVersion: int initUrl: str segmentUrl: str bitrate: int language: str = "" class Segment(NamedTuple): num: int duration: int available_at: datetime hash: str path: str # the segment URLs depend on the CDN and the chosen stream format and its segment template string def url(self, base: str, template: str) -> str: return urljoin( base, f"{self.path}/{template.replace('%', str(self.num), 1).replace('%', self.hash, 1)}" ) class UStreamTVWsClient(WebsocketClient): API_URL = "wss://r{0}-1-{1}-{2}-ws-{3}.ums.services.video.ibm.com/1/ustream" APP_ID = 3 APP_VERSION = 2 STREAM_OPENED_TIMEOUT = 6 _schema_cmd = validate.Schema({ "cmd": str, "args": [{str: object}], }) _schema_stream_formats = validate.Schema({ "streams": [validate.any( validate.all( { "contentType": "video/mp4", "sourceStreamVersion": int, "initUrl": str, "segmentUrl": str, "bitrate": int, "height": int, }, validate.transform(lambda obj: StreamFormatVideo(**obj)) ), validate.all( { "contentType": "audio/mp4", "sourceStreamVersion": int, "initUrl": str, "segmentUrl": str, "bitrate": int, validate.optional("language"): str, }, validate.transform(lambda obj: StreamFormatAudio(**obj)) ), object )] }) _schema_stream_segments = validate.Schema({ "chunkId": int, "chunkTime": int, "contentAccess": validate.all( { "accessList": [{ "data": { "path": str } }] }, validate.get(("accessList", 0, "data", "path")) ), "hashes": {validate.transform(int): str} }) stream_cdn: str = None stream_formats_video: List[StreamFormatVideo] = None stream_formats_audio: List[StreamFormatAudio] = None stream_initial_id: int = None def __init__( self, session, media_id, application, referrer=None, cluster="live", password=None, app_id=APP_ID, app_version=APP_VERSION ): self.opened = Event() self.ready = Event() self.stream_error = None # a list of deques subscribed by worker threads which independently need to read segments self.stream_segments_subscribers: List[Deque[Segment]] = [] self.stream_segments_initial: Deque[Segment] = deque() self.stream_segments_lock = RLock() self.media_id = media_id self.application = application self.referrer = referrer self.cluster = cluster self.password = password self.app_id = app_id self.app_version = app_version super().__init__(session, self._get_url(), origin="https://www.ustream.tv") def _get_url(self): return self.API_URL.format(randint(0, 0xffffff), self.media_id, self.application, self.cluster) def _set_error(self, error: Any): self.stream_error = error self.ready.set() def _set_ready(self): if not self.ready.is_set() and self.stream_cdn and self.stream_initial_id is not None: self.ready.set() if self.opened.wait(self.STREAM_OPENED_TIMEOUT): log.debug("Stream opened, keeping websocket connection alive") else: log.info("Closing websocket connection") self.ws.close() def segments_subscribe(self) -> Deque[Segment]: with self.stream_segments_lock: # copy the initial segments deque (segments arrive early) new_deque = self.stream_segments_initial.copy() self.stream_segments_subscribers.append(new_deque) return new_deque def _segments_append(self, segment: Segment): # if there are no subscribers yet, add segment(s) to the initial deque if not self.stream_segments_subscribers: self.stream_segments_initial.append(segment) else: for subscriber_deque in self.stream_segments_subscribers: subscriber_deque.append(segment) def on_open(self, wsapp): args = { "type": "viewer", "appId": self.app_id, "appVersion": self.app_version, "rsid": f"{randint(0, 10_000_000_000):x}:{randint(0, 10_000_000_000):x}", "rpin": f"_rpin.{randint(0, 1_000_000_000_000_000)}", "referrer": self.referrer, "clusterHost": "r%rnd%-1-%mediaId%-%mediaType%-%protocolPrefix%-%cluster%.ums.ustream.tv", "media": self.media_id, "application": self.application } if self.password: args["password"] = self.password self.send_json({ "cmd": "connect", "args": [args] }) def on_message(self, wsapp, data: str): try: parsed = parse_json(data, schema=self._schema_cmd) except PluginError: log.error(f"Could not parse message: {data[:50]}") return cmd: str = parsed["cmd"] args: List[Dict] = parsed["args"] log.trace(f"Received '{cmd}' command") log.trace(f"{args!r}") handlers = self._MESSAGE_HANDLERS.get(cmd) if handlers is not None: for arg in args: for name, handler in handlers.items(): argdata = arg.get(name) if argdata is not None: log.debug(f"Processing '{cmd}' - '{name}'") handler(self, argdata) # noinspection PyMethodMayBeStatic def _handle_warning(self, data: Dict): log.warning(f"{data['code']}: {str(data['message'])[:50]}") # noinspection PyUnusedLocal def _handle_reject_nonexistent(self, *args): self._set_error("This channel does not exist") # noinspection PyUnusedLocal def _handle_reject_geo_lock(self, *args): self._set_error("This content is not available in your area") def _handle_reject_cluster(self, arg: Dict): self.cluster = arg["name"] log.info(f"Switching cluster to: {self.cluster}") self.reconnect(url=self._get_url()) def _handle_reject_referrer_lock(self, arg: Dict): self.referrer = arg["redirectUrl"] log.info(f"Updating referrer to: {self.referrer}") self.reconnect(url=self._get_url()) def _handle_module_info_cdn_config(self, data: Dict): self.stream_cdn = urlunparse(( data["protocol"], data["data"][0]["data"][0]["sites"][0]["host"], data["data"][0]["data"][0]["sites"][0]["path"], "", "", "" )) self._set_ready() def _handle_module_info_stream(self, data: Dict): if data.get("contentAvailable") is False: return self._set_error("This stream is currently offline") mp4_segmented = data.get("streamFormats", {}).get("mp4/segmented") if not mp4_segmented: return # parse the stream formats once if self.stream_initial_id is None: try: formats = self._schema_stream_formats.validate(mp4_segmented) formats = formats["streams"] except PluginError as err: return self._set_error(err) self.stream_formats_video = list(filter(lambda f: type(f) is StreamFormatVideo, formats)) self.stream_formats_audio = list(filter(lambda f: type(f) is StreamFormatAudio, formats)) # parse segment duration and hashes, and queue new segments try: segmentdata: Dict = self._schema_stream_segments.validate(mp4_segmented) except PluginError: log.error("Failed parsing hashes") return current_id: int = segmentdata["chunkId"] duration: int = segmentdata["chunkTime"] path: str = segmentdata["contentAccess"] hashes: Dict[int, str] = segmentdata["hashes"] sorted_ids = sorted(hashes.keys()) count = len(sorted_ids) if count == 0: return # initial segment ID (needed by the workers to filter queued segments) if self.stream_initial_id is None: self.stream_initial_id = current_id current_time = datetime.now() # lock the stream segments deques for the worker threads with self.stream_segments_lock: # interpolate and extrapolate segments from the provided id->hash data diff = 10 - sorted_ids[0] % 10 # if there's only one id->hash item, extrapolate until the next decimal for idx, segment_id in enumerate(sorted_ids): idx_next = idx + 1 if idx_next < count: # calculate the difference between IDs and use that to interpolate segment IDs # the last id->hash item will use the previous diff to extrapolate segment IDs diff = sorted_ids[idx_next] - segment_id for num in range(segment_id, segment_id + diff): self._segments_append(Segment( num=num, duration=duration, available_at=current_time + timedelta(seconds=(num - current_id - 1) * duration / 1000), hash=hashes[segment_id], path=path )) self._set_ready() # ---- _MESSAGE_HANDLERS: Dict[str, Dict[str, Callable[["UStreamTVWsClient", Any], None]]] = { "warning": { "code": _handle_warning, }, "reject": { "cluster": _handle_reject_cluster, "referrerLock": _handle_reject_referrer_lock, "nonexistent": _handle_reject_nonexistent, "geoLock": _handle_reject_geo_lock, }, "moduleInfo": { "cdnConfig": _handle_module_info_cdn_config, "stream": _handle_module_info_stream, } } class UStreamTVStreamWriter(SegmentedStreamWriter): stream: "UStreamTVStream" reader: "UStreamTVStreamReader" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._has_init = False def put(self, segment): if self.closed: # pragma: no cover return if segment is None: self.queue(None, None) else: if not self._has_init: self._has_init = True self.queue(segment, self.executor.submit(self.fetch, segment, True)) self.queue(segment, self.executor.submit(self.fetch, segment, False)) # noinspection PyMethodOverriding def fetch(self, segment: Segment, is_init: bool): if self.closed: # pragma: no cover return now = datetime.now() if segment.available_at > now: time_to_wait = (segment.available_at - now).total_seconds() log.debug(f"Waiting for {self.stream.kind} segment: {segment.num} ({time_to_wait:.01f}s)") if not self.reader.worker.wait(time_to_wait): return try: return self.session.http.get( segment.url( self.stream.wsclient.stream_cdn, self.stream.stream_format.initUrl if is_init else self.stream.stream_format.segmentUrl ), timeout=self.timeout, retries=self.retries, exception=StreamError ) except StreamError as err: log.error(f"Failed to fetch {self.stream.kind} segment {segment.num}: {err}") def write(self, segment: Segment, res: Response, *data): if self.closed: # pragma: no cover return try: for chunk in res.iter_content(8192): self.reader.buffer.write(chunk) log.debug(f"Download of {self.stream.kind} segment {segment.num} complete") except OSError as err: log.error(f"Failed to read {self.stream.kind} segment {segment.num}: {err}") class UStreamTVStreamWorker(SegmentedStreamWorker): stream: "UStreamTVStream" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.wsclient = self.stream.wsclient self.segment_id = self.wsclient.stream_initial_id self.queue = self.wsclient.segments_subscribe() def iter_segments(self): duration = 5000 while not self.closed: try: with self.wsclient.stream_segments_lock: segment = self.queue.popleft() duration = segment.duration except IndexError: # wait for new segments to be queued (half the last segment's duration in seconds) if self.wait(duration / 1000 / 2): continue if self.closed: return if segment.num < self.segment_id: continue log.debug(f"Adding {self.stream.kind} segment {segment.num} to queue") yield segment self.segment_id = segment.num + 1 class UStreamTVStreamReader(SegmentedStreamReader): __worker__ = UStreamTVStreamWorker __writer__ = UStreamTVStreamWriter stream: "UStreamTVStream" def open(self): self.stream.wsclient.opened.set() super().open() def close(self): super().close() self.stream.wsclient.close() class UStreamTVStream(Stream): __shortname__ = "ustreamtv" def __init__( self, session, kind: str, wsclient: UStreamTVWsClient, stream_format: Union[StreamFormatVideo, StreamFormatAudio] ): super().__init__(session) self.kind = kind self.wsclient = wsclient self.stream_format = stream_format def open(self): reader = UStreamTVStreamReader(self) reader.open() return reader @pluginmatcher(re.compile(r""" https?://(?:(www\.)?ustream\.tv|video\.ibm\.com) (?: (/embed/|/channel/id/)(?P\d+) )? (?: (/embed)?/recorded/(?P\d+) )? """, re.VERBOSE)) class UStreamTV(Plugin): arguments = PluginArguments( PluginArgument( "password", argument_name="ustream-password", sensitive=True, metavar="PASSWORD", help="A password to access password protected UStream.tv channels." ) ) STREAM_READY_TIMEOUT = 15 def _get_media_app(self): video_id = self.match.group("video_id") if video_id: return video_id, "recorded" channel_id = self.match.group("channel_id") if not channel_id: channel_id = self.session.http.get( self.url, headers={"User-Agent": useragents.CHROME}, schema=validate.Schema( validate.parse_html(), validate.xml_xpath_string(".//meta[@name='ustream:channel_id'][@content][1]/@content") ) ) return channel_id, "channel" def _get_streams(self): if not MuxedStream.is_usable(self.session): return media_id, application = self._get_media_app() if not media_id: return wsclient = UStreamTVWsClient( self.session, media_id, application, referrer=self.url, cluster="live", password=self.get_option("password") ) log.debug( f"Connecting to UStream API:" f" media_id={media_id}," f" application={application}," f" referrer={self.url}," f" cluster=live" ) wsclient.start() log.debug(f"Waiting for stream data (for at most {self.STREAM_READY_TIMEOUT} seconds)...") if ( not wsclient.ready.wait(self.STREAM_READY_TIMEOUT) or not wsclient.is_alive() or wsclient.stream_error ): log.error(wsclient.stream_error or "Waiting for stream data timed out.") wsclient.close() return if not wsclient.stream_formats_audio: for video in wsclient.stream_formats_video: yield f"{video.height}p", UStreamTVStream(self.session, "video", wsclient, video) else: for video in wsclient.stream_formats_video: for audio in wsclient.stream_formats_audio: yield f"{video.height}p+a{audio.bitrate}k", MuxedStream( self.session, UStreamTVStream(self.session, "video", wsclient, video), UStreamTVStream(self.session, "audio", wsclient, audio) ) __plugin__ = UStreamTV ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/ustvnow.py0000644000175100001710000001362100000000000021750 0ustar00runnerdockerimport base64 import json import logging import re from urllib.parse import urljoin, urlparse from uuid import uuid4 from Crypto.Cipher import AES from Crypto.Hash import SHA256 from Crypto.Util.Padding import pad, unpad from streamlink.plugin import Plugin, PluginArgument, PluginArguments, PluginError, pluginmatcher from streamlink.stream.hls import HLSStream log = logging.getLogger(__name__) @pluginmatcher(re.compile( r"https?://(?:www\.)?ustvnow\.com/live/(?P\w+)/-(?P\d+)" )) class USTVNow(Plugin): _main_js_re = re.compile(r"""src=['"](main\..*\.js)['"]""") _enc_key_re = re.compile(r'(?PAES_(?:Key|IV))\s*:\s*"(?P[^"]+)"') TENANT_CODE = "ustvnow" _api_url = "https://teleupapi.revlet.net/service/api/v1/" _token_url = _api_url + "get/token" _signin_url = "https://www.ustvnow.com/signin" arguments = PluginArguments( PluginArgument( "username", metavar="USERNAME", required=True, help="Your USTV Now account username" ), PluginArgument( "password", sensitive=True, metavar="PASSWORD", required=True, help="Your USTV Now account password", prompt="Enter USTV Now account password" ) ) def __init__(self, url): super().__init__(url) self._encryption_config = {} self._token = None @classmethod def encrypt_data(cls, data, key, iv): rkey = "".join(reversed(key)).encode('utf8') riv = "".join(reversed(iv)).encode('utf8') fkey = SHA256.new(rkey).hexdigest()[:32].encode("utf8") cipher = AES.new(fkey, AES.MODE_CBC, riv) encrypted = cipher.encrypt(pad(data, 16, 'pkcs7')) return base64.b64encode(encrypted) @classmethod def decrypt_data(cls, data, key, iv): rkey = "".join(reversed(key)).encode('utf8') riv = "".join(reversed(iv)).encode('utf8') fkey = SHA256.new(rkey).hexdigest()[:32].encode("utf8") cipher = AES.new(fkey, AES.MODE_CBC, riv) decrypted = cipher.decrypt(base64.b64decode(data)) if decrypted: return unpad(decrypted, 16, 'pkcs7') else: return decrypted def _get_encryption_config(self, url): # find the path to the main.js # load the main.js and extract the config if not self._encryption_config: res = self.session.http.get(url) m = self._main_js_re.search(res.text) main_js_path = m and m.group(1) if main_js_path: res = self.session.http.get(urljoin(url, main_js_path)) self._encryption_config = dict(self._enc_key_re.findall(res.text)) return self._encryption_config.get("AES_Key"), self._encryption_config.get("AES_IV") @property def box_id(self): if not self.cache.get("box_id"): self.cache.set("box_id", str(uuid4())) return self.cache.get("box_id") def get_token(self): """ Get the token for USTVNow :return: a valid token """ if not self._token: log.debug("Getting new session token") res = self.session.http.get(self._token_url, params={ "tenant_code": self.TENANT_CODE, "box_id": self.box_id, "product": self.TENANT_CODE, "device_id": 5, "display_lang_code": "ENG", "device_sub_type": "", "timezone": "UTC" }) data = res.json() if data['status']: self._token = data['response']['sessionId'] log.debug("New token: {}".format(self._token)) else: log.error("Token acquisition failed: {details} ({detail})".format(**data['error'])) raise PluginError("could not obtain token") return self._token def api_request(self, path, data, metadata=None): key, iv = self._get_encryption_config(self._signin_url) post_data = { "data": self.encrypt_data(json.dumps(data).encode('utf8'), key, iv).decode("utf8"), "metadata": self.encrypt_data(json.dumps(metadata).encode('utf8'), key, iv).decode("utf8") } headers = {"box-id": self.box_id, "session-id": self.get_token(), "tenant-code": self.TENANT_CODE, "content-type": "application/json"} res = self.session.http.post(self._api_url + path, data=json.dumps(post_data), headers=headers).json() data = {k: v and json.loads(self.decrypt_data(v, key, iv)) for k, v in res.items()} return data def login(self, username, password): log.debug("Trying to login...") resp = self.api_request( "send", { "login_id": username, "login_key": password, "login_mode": "1", "manufacturer": "123" }, {"request": "signin"} ) return resp['data']['status'] def _get_streams(self): """ Finds the streams from ustvnow.com. """ if self.login(self.get_option("username"), self.get_option("password")): path = urlparse(self.url).path.strip("/") resp = self.api_request("send", {"path": path}, {"request": "page/stream"}) if resp['data']['status']: for stream in resp['data']['response']['streams']: if stream['keys']['licenseKey']: log.warning("Stream possibly protected by DRM") yield from HLSStream.parse_variant_playlist(self.session, stream['url']).items() else: log.error("Could not find any streams: {code}: {message}".format(**resp['data']['error'])) else: log.error("Failed to login, check username and password") __plugin__ = USTVNow ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/vidio.py0000644000175100001710000000447000000000000021337 0ustar00runnerdocker""" Plugin for vidio.com - https://www.vidio.com/live/5075-dw-tv-stream - https://www.vidio.com/watch/766861-5-rekor-fantastis-zidane-bersama-real-madrid """ import logging import re from streamlink.plugin import Plugin, pluginmatcher from streamlink.plugin.api import useragents, validate from streamlink.stream.hls import HLSStream log = logging.getLogger(__name__) @pluginmatcher(re.compile( r"https?://(?:www\.)?vidio\.com/(?:en/)?(?Plive|watch)/(?P\d+)-(?P[^/?#&]+)" )) class Vidio(Plugin): _playlist_re = re.compile(r'''hls-url=["'](?P[^"']+)["']''') _data_id_re = re.compile(r'''meta\s+data-id=["'](?P[^"']+)["']''') csrf_tokens_url = "https://www.vidio.com/csrf_tokens" tokens_url = "https://www.vidio.com/live/{id}/tokens" token_schema = validate.Schema(validate.parse_json(), {"token": validate.text}, validate.get("token")) def get_csrf_tokens(self): return self.session.http.get( self.csrf_tokens_url, schema=self.token_schema ) def get_url_tokens(self, stream_id): log.debug("Getting stream tokens") csrf_token = self.get_csrf_tokens() return self.session.http.post( self.tokens_url.format(id=stream_id), files={"authenticity_token": (None, csrf_token)}, headers={ "User-Agent": useragents.CHROME, "Referer": self.url }, schema=self.token_schema ) def _get_streams(self): res = self.session.http.get(self.url) plmatch = self._playlist_re.search(res.text) idmatch = self._data_id_re.search(res.text) hls_url = plmatch and plmatch.group("url") stream_id = idmatch and idmatch.group("id") tokens = self.get_url_tokens(stream_id) if hls_url: log.debug("HLS URL: {0}".format(hls_url)) log.debug("Tokens: {0}".format(tokens)) return HLSStream.parse_variant_playlist(self.session, hls_url + "?" + tokens, headers={"User-Agent": useragents.CHROME, "Referer": self.url}) __plugin__ = Vidio ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/vimeo.py0000644000175100001710000001023300000000000021336 0ustar00runnerdockerimport logging import re from html import unescape as html_unescape from urllib.parse import urlparse from streamlink.plugin import Plugin, PluginArgument, PluginArguments, pluginmatcher from streamlink.plugin.api import validate from streamlink.stream.dash import DASHStream from streamlink.stream.ffmpegmux import MuxedStream from streamlink.stream.hls import HLSStream from streamlink.stream.http import HTTPStream log = logging.getLogger(__name__) @pluginmatcher(re.compile( r"https?://(player\.vimeo\.com/video/\d+|(www\.)?vimeo\.com/.+)" )) class Vimeo(Plugin): _config_url_re = re.compile(r'(?:"config_url"|\bdata-config-url)\s*[:=]\s*(".+?")') _config_re = re.compile(r"var\s+config\s*=\s*({.+?})\s*;") _config_url_schema = validate.Schema( validate.transform(_config_url_re.search), validate.any( None, validate.Schema( validate.get(1), validate.parse_json(), validate.transform(html_unescape), validate.url(), ), ), ) _config_schema = validate.Schema( validate.parse_json(), { "request": { "files": { validate.optional("dash"): {"cdns": {validate.text: {"url": validate.url()}}}, validate.optional("hls"): {"cdns": {validate.text: {"url": validate.url()}}}, validate.optional("progressive"): validate.all( [{"url": validate.url(), "quality": validate.text}] ), }, validate.optional("text_tracks"): validate.all( [{"url": validate.text, "lang": validate.text}] ), } }, ) _player_schema = validate.Schema( validate.transform(_config_re.search), validate.any(None, validate.Schema(validate.get(1), _config_schema)), ) arguments = PluginArguments( PluginArgument("mux-subtitles", is_global=True) ) def _get_streams(self): if "player.vimeo.com" in self.url: data = self.session.http.get(self.url, schema=self._player_schema) else: api_url = self.session.http.get(self.url, schema=self._config_url_schema) if not api_url: return data = self.session.http.get(api_url, schema=self._config_schema) videos = data["request"]["files"] streams = [] for stream_type in ("hls", "dash"): if stream_type not in videos: continue for _, video_data in videos[stream_type]["cdns"].items(): log.trace("{0!r}".format(video_data)) url = video_data.get("url") if stream_type == "hls": for stream in HLSStream.parse_variant_playlist(self.session, url).items(): streams.append(stream) elif stream_type == "dash": p = urlparse(url) if p.path.endswith("dash.mpd"): # LIVE url = self.session.http.get(url).json()["url"] elif p.path.endswith("master.json"): # VOD url = url.replace("master.json", "master.mpd") else: log.error("Unsupported DASH path: {0}".format(p.path)) continue for stream in DASHStream.parse_manifest(self.session, url).items(): streams.append(stream) for stream in videos.get("progressive", []): streams.append((stream["quality"], HTTPStream(self.session, stream["url"]))) if self.get_option("mux_subtitles") and data["request"].get("text_tracks"): substreams = { s["lang"]: HTTPStream(self.session, "https://vimeo.com" + s["url"]) for s in data["request"]["text_tracks"] } for quality, stream in streams: yield quality, MuxedStream(self.session, stream, subtitles=substreams) else: for stream in streams: yield stream __plugin__ = Vimeo ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/vinhlongtv.py0000644000175100001710000000175200000000000022423 0ustar00runnerdockerimport logging import re from streamlink.plugin import Plugin, pluginmatcher from streamlink.plugin.api import validate from streamlink.stream.hls import HLSStream log = logging.getLogger(__name__) @pluginmatcher(re.compile( r'https?://(?:www\.)?thvli\.vn/live/(?P[^/]+)' )) class VinhLongTV(Plugin): api_url = 'http://api.thvli.vn/backend/cm/detail/{0}/' _data_schema = validate.Schema( { 'link_play': validate.text, }, validate.get('link_play') ) def _get_streams(self): channel = self.match.group('channel') res = self.session.http.get(self.api_url.format(channel)) hls_url = self.session.http.json(res, schema=self._data_schema) log.debug('URL={0}'.format(hls_url)) streams = HLSStream.parse_variant_playlist(self.session, hls_url) if not streams: return {'live': HLSStream(self.session, hls_url)} else: return streams __plugin__ = VinhLongTV ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/src/streamlink/plugins/vk.py0000644000175100001710000000634100000000000020644 0ustar00runnerdockerimport logging import re from urllib.parse import parse_qsl, unquote, urlparse from streamlink.exceptions import NoStreamsError from streamlink.plugin import Plugin, PluginError, pluginmatcher from streamlink.plugin.api import validate from streamlink.stream.dash import DASHStream from streamlink.stream.hls import HLSStream log = logging.getLogger(__name__) @pluginmatcher(re.compile( r"https?://(?:\w+\.)?vk\.com/videos?(?:\?z=video)?(?P-?\d+_\d+)" )) @pluginmatcher(re.compile( r"https?://(\w+\.)?vk\.com/.+" )) class VK(Plugin): API_URL = "https://vk.com/al_video.php" def _has_video_id(self): return any(m for m in self.matches[:-1]) def follow_vk_redirect(self): if self._has_video_id(): return try: parsed_url = urlparse(self.url) true_path = next(unquote(v).split("/")[0] for k, v in parse_qsl(parsed_url.query) if k == "z" and len(v) > 0) self.url = f"{parsed_url.scheme}://{parsed_url.netloc}/{true_path}" if self._has_video_id(): return except StopIteration: pass try: self.url = self.session.http.get(self.url, schema=validate.Schema( validate.parse_html(), validate.xml_xpath_string(".//head/meta[@property='og:url'][@content]/@content"), str )) except PluginError: pass if self._has_video_id(): return raise NoStreamsError(self.url) def _get_streams(self): self.follow_vk_redirect() video_id = self.match.group("video_id") if not video_id: return log.debug(f"video ID: {video_id}") try: data = self.session.http.post( self.API_URL, params={ "act": "show_inline", "al": "1", "video": video_id, }, schema=validate.Schema( validate.transform(lambda text: re.sub(r"^\s* dash/ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/resources/dash/test_6_p1.mpd0000644000175100001710000000237000000000000021630 0ustar00runnerdocker http://test.se/ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/resources/dash/test_6_p2.mpd0000644000175100001710000000237000000000000021631 0ustar00runnerdocker http://test.se/ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/resources/dash/test_7.mpd0000644000175100001710000000371400000000000021234 0ustar00runnerdocker test/dash.smil ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/resources/dash/test_8.mpd0000644000175100001710000000401600000000000021231 0ustar00runnerdocker ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/resources/dash/test_9.mpd0000644000175100001710000000506400000000000021236 0ustar00runnerdocker ltv9 https://595ac1e01d44e.streamlock.net/ltv9/ltv9/manifest_TOKEN.mpd ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643136177.0688405 streamlink-3.1.1/tests/resources/hls/0000755000175100001710000000000000000000000017167 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/resources/hls/test_1.m3u80000644000175100001710000000441400000000000021107 0ustar00runnerdocker#EXTM3U #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="stereo",LANGUAGE="en",NAME="English",DEFAULT=YES,AUTOSELECT=YES,URI="audio/stereo/en/128kbit.m3u8" #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="stereo",LANGUAGE="dubbing",NAME="Dubbing",DEFAULT=NO,AUTOSELECT=YES,URI="audio/stereo/none/128kbit.m3u8" #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="surround",LANGUAGE="en",NAME="English",DEFAULT=YES,AUTOSELECT=YES,URI="audio/surround/en/320kbit.m3u8" #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="surround",LANGUAGE="dubbing",NAME="Dubbing",DEFAULT=NO,AUTOSELECT=YES,URI="audio/stereo/none/128kbit.m3u8" #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Deutsch",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="de",URI="subtitles_de.m3u8" #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="English",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="en",URI="subtitles_en.m3u8" #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Espanol",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="es",URI="subtitles_es.m3u8" #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Français",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="fr",URI="subtitles_fr.m3u8" #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=258157,CODECS="avc1.4d400d,mp4a.40.2",AUDIO="stereo",RESOLUTION=422x180,SUBTITLES="subs" video/250kbit.m3u8 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=520929,CODECS="avc1.4d4015,mp4a.40.2",AUDIO="stereo",RESOLUTION=638x272,SUBTITLES="subs" video/500kbit.m3u8 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=831270,CODECS="avc1.4d4015,mp4a.40.2",AUDIO="stereo",RESOLUTION=638x272,SUBTITLES="subs" video/800kbit.m3u8 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1144430,CODECS="avc1.4d401f,mp4a.40.2",AUDIO="surround",RESOLUTION=958x408,SUBTITLES="subs" video/1100kbit.m3u8 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1558322,CODECS="avc1.4d401f,mp4a.40.2",AUDIO="surround",RESOLUTION=1277x554,SUBTITLES="subs" video/1500kbit.m3u8 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=4149264,CODECS="avc1.4d4028,mp4a.40.2",AUDIO="surround",RESOLUTION=1921x818,SUBTITLES="subs" video/4000kbit.m3u8 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=6214307,CODECS="avc1.4d4028,mp4a.40.2",AUDIO="surround",RESOLUTION=1921x818,SUBTITLES="subs" video/6000kbit.m3u8 #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=10285391,CODECS="avc1.4d4033,mp4a.40.2",AUDIO="surround",RESOLUTION=4096x1744,SUBTITLES="subs" video/10000kbit.m3u8 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/resources/hls/test_2.m3u80000644000175100001710000000104500000000000021105 0ustar00runnerdocker#EXTM3U #EXT-X-INDEPENDENT-SEGMENTS #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",LANGUAGE="en",NAME="English",AUTOSELECT=YES,DEFAULT=YES #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="English",LANGUAGE="en",AUTOSELECT=NO,URI="en.m3u8" #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="Spanish",LANGUAGE="es",AUTOSELECT=NO,URI="es.m3u8" #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="chunked",NAME="video",AUTOSELECT=YES,DEFAULT=YES #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=3982010,RESOLUTION=1920x1080,CODECS="avc1.4D4029,mp4a.40.2",VIDEO="chunked", AUDIO="aac" playlist.m3u8././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/resources/hls/test_date.m3u80000644000175100001710000000231300000000000021660 0ustar00runnerdocker#EXTM3U #EXT-X-TARGETDURATION:120 #EXT-X-DATERANGE:ID="start-invalid",START-DATE="invalid" #EXT-X-DATERANGE:ID="start-no-frac",START-DATE="2000-01-01T00:00:00Z" #EXT-X-DATERANGE:ID="start-with-frac",START-DATE="2000-01-01T00:00:00.000Z" #EXT-X-DATERANGE:ID="with-class",CLASS="bar",START-DATE="2000-01-01T00:00:00.000Z" #EXT-X-DATERANGE:ID="duration",START-DATE="2000-01-01T00:00:00.000Z",DURATION=30.5 #EXT-X-DATERANGE:ID="planned-duration",START-DATE="2000-01-01T00:00:00.000Z",PLANNED-DURATION=15 #EXT-X-DATERANGE:ID="duration-precedence",START-DATE="2000-01-01T00:00:00.000Z",DURATION=30.5,PLANNED-DURATION=15 #EXT-X-DATERANGE:ID="end",START-DATE="2000-01-01T00:00:00.000Z",END-DATE="2000-01-01T00:01:00.000Z" #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 #EXT-X-DATERANGE:X-CUSTOM="value" #EXT-X-PROGRAM-DATE-TIME:2000-01-01T00:00:00.000Z #EXTINF:15.000,live segment0-15.ts #EXT-X-PROGRAM-DATE-TIME:2000-01-01T00:00:15.000Z #EXTINF:15.500,live segment15-30.5.ts #EXT-X-PROGRAM-DATE-TIME:2000-01-01T00:00:30.500Z #EXTINF:29.500,live segment30.5-60.ts #EXT-X-PROGRAM-DATE-TIME:2000-01-01T00:01:00.000Z #EXTINF:60.000,live segment60-.ts ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/resources/hls/test_master.m3u80000644000175100001710000000266300000000000022246 0ustar00runnerdocker#EXTM3U #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="720p30",NAME="720p",AUTOSELECT=YES,DEFAULT=YES #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2299652,RESOLUTION=1280x720,CODECS="avc1.77.31,mp4a.40.2",VIDEO="720p30" 720p.m3u8 #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="720p30",NAME="720p",AUTOSELECT=YES,DEFAULT=YES #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2299652,RESOLUTION=1280x720,CODECS="avc1.77.31,mp4a.40.2",VIDEO="720p30" 720p_alt.m3u8 #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="480p30",NAME="480p",AUTOSELECT=YES,DEFAULT=YES #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1354652,RESOLUTION=852x480,CODECS="avc1.77.31,mp4a.40.2",VIDEO="480p30" 480p.m3u8 #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="360p30",NAME="360p",AUTOSELECT=YES,DEFAULT=YES #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=630000,RESOLUTION=640x360,CODECS="avc1.77.31,mp4a.40.2",VIDEO="360p30" 360p.m3u8 #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="160p30",NAME="160p",AUTOSELECT=YES,DEFAULT=YES #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=230000,RESOLUTION=284x160,CODECS="avc1.77.31,mp4a.40.2",VIDEO="160p30" 160p.m3u8 #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="chunked",NAME="1080p (source)",AUTOSELECT=YES,DEFAULT=YES #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=3982010,RESOLUTION=1920x1080,CODECS="avc1.4D4029,mp4a.40.2",VIDEO="chunked" playlist.m3u8 #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio_only",NAME="audio_only",AUTOSELECT=YES,DEFAULT=NO,LANGUAGE="en" #EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=90145,CODECS="mp4a.40.2",VIDEO="audio_only" audio_only.m3u8././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643136177.0688405 streamlink-3.1.1/tests/stream/0000755000175100001710000000000000000000000015662 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/stream/__init__.py0000644000175100001710000000000000000000000017761 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/stream/test_dash.py0000644000175100001710000004462200000000000020222 0ustar00runnerdockerimport unittest from unittest.mock import ANY, MagicMock, Mock, call, patch from streamlink import PluginError from streamlink.stream.dash import DASHStream, DASHStreamWorker from streamlink.stream.dash_manifest import MPD from tests.resources import text, xml class TestDASHStream(unittest.TestCase): def setUp(self): self.session = MagicMock() self.test_url = "http://test.bar/foo.mpd" self.session.http.get.return_value = Mock(url=self.test_url) @patch('streamlink.stream.dash.MPD') def test_parse_manifest_video_only(self, mpdClass): mpdClass.return_value = Mock(periods=[ Mock(adaptationSets=[ Mock(contentProtection=None, representations=[ Mock(id=1, mimeType="video/mp4", height=720), Mock(id=2, mimeType="video/mp4", height=1080) ]) ]) ]) streams = DASHStream.parse_manifest(self.session, self.test_url) mpdClass.assert_called_with(ANY, base_url="http://test.bar", url="http://test.bar/foo.mpd") self.assertSequenceEqual( sorted(list(streams.keys())), sorted(["720p", "1080p"]) ) @patch('streamlink.stream.dash.MPD') def test_parse_manifest_audio_only(self, mpdClass): mpdClass.return_value = Mock(periods=[ Mock(adaptationSets=[ Mock(contentProtection=None, representations=[ Mock(id=1, mimeType="audio/mp4", bandwidth=128.0, lang='en'), Mock(id=2, mimeType="audio/mp4", bandwidth=256.0, lang='en') ]) ]) ]) streams = DASHStream.parse_manifest(self.session, self.test_url) mpdClass.assert_called_with(ANY, base_url="http://test.bar", url="http://test.bar/foo.mpd") self.assertSequenceEqual( sorted(list(streams.keys())), sorted(["a128k", "a256k"]) ) @patch('streamlink.stream.dash.MPD') def test_parse_manifest_audio_single(self, mpdClass): mpdClass.return_value = Mock(periods=[ Mock(adaptationSets=[ Mock(contentProtection=None, representations=[ Mock(id=1, mimeType="video/mp4", height=720), Mock(id=2, mimeType="video/mp4", height=1080), Mock(id=3, mimeType="audio/aac", bandwidth=128.0, lang='en') ]) ]) ]) streams = DASHStream.parse_manifest(self.session, self.test_url) mpdClass.assert_called_with(ANY, base_url="http://test.bar", url="http://test.bar/foo.mpd") self.assertSequenceEqual( sorted(list(streams.keys())), sorted(["720p", "1080p"]) ) @patch('streamlink.stream.dash.MPD') def test_parse_manifest_audio_multi(self, mpdClass): mpdClass.return_value = Mock(periods=[ Mock(adaptationSets=[ Mock(contentProtection=None, representations=[ Mock(id=1, mimeType="video/mp4", height=720), Mock(id=2, mimeType="video/mp4", height=1080), Mock(id=3, mimeType="audio/aac", bandwidth=128.0, lang='en'), Mock(id=4, mimeType="audio/aac", bandwidth=256.0, lang='en') ]) ]) ]) streams = DASHStream.parse_manifest(self.session, self.test_url) mpdClass.assert_called_with(ANY, base_url="http://test.bar", url="http://test.bar/foo.mpd") self.assertSequenceEqual( sorted(list(streams.keys())), sorted(["720p+a128k", "1080p+a128k", "720p+a256k", "1080p+a256k"]) ) @patch('streamlink.stream.dash.MPD') def test_parse_manifest_audio_multi_lang(self, mpdClass): mpdClass.return_value = Mock(periods=[ Mock(adaptationSets=[ Mock(contentProtection=None, representations=[ Mock(id=1, mimeType="video/mp4", height=720), Mock(id=2, mimeType="video/mp4", height=1080), Mock(id=3, mimeType="audio/aac", bandwidth=128.0, lang='en'), Mock(id=4, mimeType="audio/aac", bandwidth=128.0, lang='es') ]) ]) ]) streams = DASHStream.parse_manifest(self.session, self.test_url) mpdClass.assert_called_with(ANY, base_url="http://test.bar", url="http://test.bar/foo.mpd") self.assertSequenceEqual( sorted(list(streams.keys())), sorted(["720p", "1080p"]) ) self.assertEqual(streams["720p"].audio_representation.lang, "en") self.assertEqual(streams["1080p"].audio_representation.lang, "en") @patch('streamlink.stream.dash.MPD') def test_parse_manifest_audio_multi_lang_alpha3(self, mpdClass): mpdClass.return_value = Mock(periods=[ Mock(adaptationSets=[ Mock(contentProtection=None, representations=[ Mock(id=1, mimeType="video/mp4", height=720), Mock(id=2, mimeType="video/mp4", height=1080), Mock(id=3, mimeType="audio/aac", bandwidth=128.0, lang='eng'), Mock(id=4, mimeType="audio/aac", bandwidth=128.0, lang='spa') ]) ]) ]) streams = DASHStream.parse_manifest(self.session, self.test_url) mpdClass.assert_called_with(ANY, base_url="http://test.bar", url="http://test.bar/foo.mpd") self.assertSequenceEqual( sorted(list(streams.keys())), sorted(["720p", "1080p"]) ) self.assertEqual(streams["720p"].audio_representation.lang, "eng") self.assertEqual(streams["1080p"].audio_representation.lang, "eng") @patch('streamlink.stream.dash.MPD') def test_parse_manifest_audio_invalid_lang(self, mpdClass): mpdClass.return_value = Mock(periods=[ Mock(adaptationSets=[ Mock(contentProtection=None, representations=[ Mock(id=1, mimeType="video/mp4", height=720), Mock(id=2, mimeType="video/mp4", height=1080), Mock(id=3, mimeType="audio/aac", bandwidth=128.0, lang='en_no_voice'), ]) ]) ]) streams = DASHStream.parse_manifest(self.session, self.test_url) mpdClass.assert_called_with(ANY, base_url="http://test.bar", url="http://test.bar/foo.mpd") self.assertSequenceEqual( sorted(list(streams.keys())), sorted(["720p", "1080p"]) ) self.assertEqual(streams["720p"].audio_representation.lang, "en_no_voice") self.assertEqual(streams["1080p"].audio_representation.lang, "en_no_voice") @patch('streamlink.stream.dash.MPD') def test_parse_manifest_audio_multi_lang_locale(self, mpdClass): self.session.localization.language.alpha2 = "es" self.session.localization.explicit = True mpdClass.return_value = Mock(periods=[ Mock(adaptationSets=[ Mock(contentProtection=None, representations=[ Mock(id=1, mimeType="video/mp4", height=720), Mock(id=2, mimeType="video/mp4", height=1080), Mock(id=3, mimeType="audio/aac", bandwidth=128.0, lang='en'), Mock(id=4, mimeType="audio/aac", bandwidth=128.0, lang='es') ]) ]) ]) streams = DASHStream.parse_manifest(self.session, self.test_url) mpdClass.assert_called_with(ANY, base_url="http://test.bar", url="http://test.bar/foo.mpd") self.assertSequenceEqual( sorted(list(streams.keys())), sorted(["720p", "1080p"]) ) self.assertEqual(streams["720p"].audio_representation.lang, "es") self.assertEqual(streams["1080p"].audio_representation.lang, "es") @patch('streamlink.stream.dash.MPD') def test_parse_manifest_drm(self, mpdClass): mpdClass.return_value = Mock(periods=[Mock(adaptationSets=[Mock(contentProtection="DRM")])]) self.assertRaises(PluginError, DASHStream.parse_manifest, self.session, self.test_url) mpdClass.assert_called_with(ANY, base_url="http://test.bar", url="http://test.bar/foo.mpd") def test_parse_manifest_string(self): with text("dash/test_9.mpd") as mpd_txt: test_manifest = mpd_txt.read() streams = DASHStream.parse_manifest(self.session, test_manifest) self.assertSequenceEqual(list(streams.keys()), ['2500k']) @patch('streamlink.stream.dash.DASHStreamReader') @patch('streamlink.stream.dash.FFMPEGMuxer') def test_stream_open_video_only(self, muxer, reader): stream = DASHStream(self.session, Mock(), Mock(id=1, mimeType="video/mp4")) open_reader = reader.return_value = Mock() stream.open() reader.assert_called_with(stream, 1, "video/mp4") open_reader.open.assert_called_with() muxer.assert_not_called() @patch('streamlink.stream.dash.DASHStreamReader') @patch('streamlink.stream.dash.FFMPEGMuxer') def test_stream_open_video_audio(self, muxer, reader): stream = DASHStream(self.session, Mock(), Mock(id=1, mimeType="video/mp4"), Mock(id=2, mimeType="audio/mp3", lang='en')) open_reader = reader.return_value = Mock() stream.open() self.assertSequenceEqual(reader.mock_calls, [call(stream, 1, "video/mp4"), call().open(), call(stream, 2, "audio/mp3"), call().open()]) self.assertSequenceEqual(muxer.mock_calls, [call(self.session, open_reader, open_reader, copyts=True), call().open()]) @patch('streamlink.stream.dash.MPD') def test_segments_number_time(self, mpdClass): with xml("dash/test_9.mpd") as mpd_xml: mpdClass.return_value = MPD(mpd_xml, base_url="http://test.bar", url="http://test.bar/foo.mpd") streams = DASHStream.parse_manifest(self.session, self.test_url) mpdClass.assert_called_with(ANY, base_url="http://test.bar", url="http://test.bar/foo.mpd") self.assertSequenceEqual(list(streams.keys()), ['2500k']) @patch('streamlink.stream.dash.MPD') def test_parse_manifest_with_duplicated_resolutions(self, mpdClass): """ Verify the fix for https://github.com/streamlink/streamlink/issues/3365 """ mpdClass.return_value = Mock(periods=[ Mock(adaptationSets=[ Mock(contentProtection=None, representations=[ Mock(id=1, mimeType="video/mp4", height=1080, bandwidth=128.0), Mock(id=2, mimeType="video/mp4", height=1080, bandwidth=64.0), Mock(id=3, mimeType="video/mp4", height=1080, bandwidth=32.0), Mock(id=4, mimeType="video/mp4", height=720), ]) ]) ]) streams = DASHStream.parse_manifest(self.session, self.test_url) mpdClass.assert_called_with(ANY, base_url="http://test.bar", url="http://test.bar/foo.mpd") self.assertSequenceEqual( sorted(list(streams.keys())), sorted(["720p", "1080p", "1080p_alt", "1080p_alt2"]) ) @patch('streamlink.stream.dash.MPD') def test_parse_manifest_with_duplicated_resolutions_sorted_bandwidth(self, mpdClass): """ Verify the fix for https://github.com/streamlink/streamlink/issues/4217 """ mpdClass.return_value = Mock(periods=[ Mock(adaptationSets=[ Mock(contentProtection=None, representations=[ Mock(id=1, mimeType="video/mp4", height=1080, bandwidth=64.0), Mock(id=2, mimeType="video/mp4", height=1080, bandwidth=128.0), Mock(id=3, mimeType="video/mp4", height=1080, bandwidth=32.0), ]) ]) ]) streams = DASHStream.parse_manifest(self.session, self.test_url) mpdClass.assert_called_with(ANY, base_url="http://test.bar", url="http://test.bar/foo.mpd") self.assertEqual(streams["1080p"].video_representation.bandwidth, 128.0) self.assertEqual(streams["1080p_alt"].video_representation.bandwidth, 64.0) self.assertEqual(streams["1080p_alt2"].video_representation.bandwidth, 32.0) class TestDASHStreamWorker(unittest.TestCase): @patch("streamlink.stream.dash_manifest.time.sleep") @patch('streamlink.stream.dash.MPD') def test_dynamic_reload(self, mpdClass, sleep): reader = MagicMock() worker = DASHStreamWorker(reader) reader.representation_id = 1 reader.mime_type = "video/mp4" representation = Mock(id=1, mimeType="video/mp4", height=720) segments = [Mock(url="init_segment"), Mock(url="first_segment"), Mock(url="second_segment")] representation.segments.return_value = [segments[0]] mpdClass.return_value = worker.mpd = Mock(dynamic=True, publishTime=1, periods=[ Mock(adaptationSets=[ Mock(contentProtection=None, representations=[ representation ]) ]) ]) worker.mpd.type = "dynamic" worker.mpd.minimumUpdatePeriod.total_seconds.return_value = 0 worker.mpd.periods[0].duration.total_seconds.return_value = 0 segment_iter = worker.iter_segments() representation.segments.return_value = segments[:1] self.assertEqual(next(segment_iter), segments[0]) representation.segments.assert_called_with(init=True) representation.segments.return_value = segments[1:] self.assertSequenceEqual([next(segment_iter), next(segment_iter)], segments[1:]) representation.segments.assert_called_with(init=False) @patch("streamlink.stream.dash_manifest.time.sleep") def test_static(self, sleep): reader = MagicMock() worker = DASHStreamWorker(reader) reader.representation_id = 1 reader.mime_type = "video/mp4" representation = Mock(id=1, mimeType="video/mp4", height=720) segments = [Mock(url="init_segment"), Mock(url="first_segment"), Mock(url="second_segment")] representation.segments.return_value = [segments[0]] worker.mpd = Mock(dynamic=False, publishTime=1, periods=[ Mock(adaptationSets=[ Mock(contentProtection=None, representations=[ representation ]) ]) ]) worker.mpd.type = "static" worker.mpd.minimumUpdatePeriod.total_seconds.return_value = 0 worker.mpd.periods[0].duration.total_seconds.return_value = 0 representation.segments.return_value = segments self.assertSequenceEqual(list(worker.iter_segments()), segments) representation.segments.assert_called_with(init=True) @patch("streamlink.stream.dash_manifest.time.time") @patch("streamlink.stream.dash_manifest.time.sleep") def test_static_refresh_wait(self, sleep, time): """ Verify the fix for https://github.com/streamlink/streamlink/issues/2873 """ time.return_value = 1 reader = MagicMock() worker = DASHStreamWorker(reader) reader.representation_id = 1 reader.mime_type = "video/mp4" representation = Mock(id=1, mimeType="video/mp4", height=720) segments = [Mock(url="init_segment"), Mock(url="first_segment"), Mock(url="second_segment")] representation.segments.return_value = [segments[0]] worker.mpd = Mock(dynamic=False, publishTime=1, periods=[ Mock(adaptationSets=[ Mock(contentProtection=None, representations=[ representation ]) ]) ]) worker.mpd.type = "static" for duration in (0, 204.32): worker.mpd.minimumUpdatePeriod.total_seconds.return_value = 0 worker.mpd.periods[0].duration.total_seconds.return_value = duration representation.segments.return_value = segments self.assertSequenceEqual(list(worker.iter_segments()), segments) representation.segments.assert_called_with(init=True) sleep.assert_called_with(5) @patch("streamlink.stream.dash_manifest.time.sleep") def test_duplicate_rep_id(self, sleep): representation_vid = Mock(id=1, mimeType="video/mp4", height=720) representation_aud = Mock(id=1, mimeType="audio/aac", lang='en') mpd = Mock(dynamic=False, publishTime=1, periods=[ Mock(adaptationSets=[ Mock(contentProtection=None, representations=[ representation_vid ]), Mock(contentProtection=None, representations=[ representation_aud ]) ]) ]) self.assertEqual(representation_vid, DASHStreamWorker.get_representation(mpd, 1, "video/mp4")) self.assertEqual(representation_aud, DASHStreamWorker.get_representation(mpd, 1, "audio/aac")) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/stream/test_dash_parser.py0000644000175100001710000003224700000000000021576 0ustar00runnerdockerimport datetime import itertools import unittest from operator import attrgetter from unittest.mock import Mock from freezegun import freeze_time from freezegun.api import FakeDatetime from streamlink.stream.dash_manifest import MPD, MPDParsers, MPDParsingError, Representation, utc from tests.resources import xml class TestMPDParsers(unittest.TestCase): def test_utc(self): self.assertIn(utc.tzname(None), ("UTC", "UTC+00:00")) # depends on the implementation self.assertIn(utc.dst(None), (None, datetime.timedelta(0))) # depends on the implementation self.assertEqual(utc.utcoffset(None), datetime.timedelta(0)) def test_bool_str(self): self.assertEqual(MPDParsers.bool_str("true"), True) self.assertEqual(MPDParsers.bool_str("TRUE"), True) self.assertEqual(MPDParsers.bool_str("True"), True) self.assertEqual(MPDParsers.bool_str("0"), False) self.assertEqual(MPDParsers.bool_str("False"), False) self.assertEqual(MPDParsers.bool_str("false"), False) self.assertEqual(MPDParsers.bool_str("FALSE"), False) def test_type(self): self.assertEqual(MPDParsers.type("dynamic"), "dynamic") self.assertEqual(MPDParsers.type("static"), "static") with self.assertRaises(MPDParsingError): MPDParsers.type("other") def test_duration(self): self.assertEqual(MPDParsers.duration("PT1S"), datetime.timedelta(0, 1)) def test_datetime(self): self.assertEqual(MPDParsers.datetime("2018-01-01T00:00:00Z"), datetime.datetime(2018, 1, 1, 0, 0, 0, tzinfo=utc)) def test_segment_template(self): self.assertEqual(MPDParsers.segment_template("$Time$-$Number$-$Other$")(Time=1, Number=2, Other=3), "1-2-3") self.assertEqual(MPDParsers.segment_template("$Number%05d$")(Number=123), "00123") self.assertEqual(MPDParsers.segment_template("$Time%0.02f$")(Time=100.234), "100.23") def test_frame_rate(self): self.assertAlmostEqual(MPDParsers.frame_rate("1/25"), 1 / 25.0) self.assertAlmostEqual(MPDParsers.frame_rate("0.2"), 0.2) def test_timedelta(self): self.assertEqual(MPDParsers.timedelta(1)(100), datetime.timedelta(0, 100.0)) self.assertEqual(MPDParsers.timedelta(10)(100), datetime.timedelta(0, 10.0)) def test_range(self): self.assertEqual(MPDParsers.range("100-"), (100, None)) self.assertEqual(MPDParsers.range("100-199"), (100, 100)) self.assertRaises(MPDParsingError, MPDParsers.range, "100") class TestMPDParser(unittest.TestCase): maxDiff = None def test_segments_number_time(self): with xml("dash/test_1.mpd") as mpd_xml: mpd = MPD(mpd_xml, base_url="http://test.se/", url="http://test.se/manifest.mpd") segments = mpd.periods[0].adaptationSets[0].representations[0].segments() init_segment = next(segments) self.assertEqual(init_segment.url, "http://test.se/tracks-v3/init-1526842800.g_m4v") video_segments = list(map(attrgetter("url"), (itertools.islice(segments, 5)))) # suggested delay is 11 seconds, each segment is 5 seconds long - so there should be 3 self.assertSequenceEqual(video_segments, ['http://test.se/tracks-v3/dvr-1526842800-698.g_m4v?t=3403000', 'http://test.se/tracks-v3/dvr-1526842800-699.g_m4v?t=3408000', 'http://test.se/tracks-v3/dvr-1526842800-700.g_m4v?t=3413000']) def test_segments_static_number(self): with xml("dash/test_2.mpd") as mpd_xml: mpd = MPD(mpd_xml, base_url="http://test.se/", url="http://test.se/manifest.mpd") segments = mpd.periods[0].adaptationSets[3].representations[0].segments() init_segment = next(segments) self.assertEqual(init_segment.url, "http://test.se/video/250kbit/init.mp4") video_segments = list(map(attrgetter("url"), (itertools.islice(segments, 100000)))) self.assertEqual(len(video_segments), 444) self.assertSequenceEqual(video_segments[:5], ['http://test.se/video/250kbit/segment_1.m4s', 'http://test.se/video/250kbit/segment_2.m4s', 'http://test.se/video/250kbit/segment_3.m4s', 'http://test.se/video/250kbit/segment_4.m4s', 'http://test.se/video/250kbit/segment_5.m4s']) def test_segments_dynamic_time(self): with xml("dash/test_3.mpd") as mpd_xml: mpd = MPD(mpd_xml, base_url="http://test.se/", url="http://test.se/manifest.mpd") segments = mpd.periods[0].adaptationSets[0].representations[0].segments() init_segment = next(segments) self.assertEqual(init_segment.url, "http://test.se/video-2800000-0.mp4?z32=") video_segments = list(map(attrgetter("url"), (itertools.islice(segments, 3)))) # default suggested delay is 3 seconds, each segment is 4 seconds long - so there should be 1 segment self.assertSequenceEqual(video_segments, ['http://test.se/video-time=1525450872000-2800000-0.m4s?z32=']) def test_segments_dynamic_number(self): with freeze_time(FakeDatetime(2018, 5, 22, 13, 37, 0, tzinfo=utc)): with xml("dash/test_4.mpd") as mpd_xml: mpd = MPD(mpd_xml, base_url="http://test.se/", url="http://test.se/manifest.mpd") segments = mpd.periods[0].adaptationSets[0].representations[0].segments() init_segment = next(segments) self.assertEqual(init_segment.url, "http://test.se/hd-5-init.mp4") video_segments = [] for _ in range(3): seg = next(segments) video_segments.append((seg.url, seg.available_at)) self.assertSequenceEqual(video_segments, [('http://test.se/hd-5_000311235.mp4', datetime.datetime(2018, 5, 22, 13, 37, 0, tzinfo=utc)), ('http://test.se/hd-5_000311236.mp4', datetime.datetime(2018, 5, 22, 13, 37, 5, tzinfo=utc)), ('http://test.se/hd-5_000311237.mp4', datetime.datetime(2018, 5, 22, 13, 37, 10, tzinfo=utc)) ]) def test_segments_static_no_publish_time(self): with xml("dash/test_5.mpd") as mpd_xml: mpd = MPD(mpd_xml, base_url="http://test.se/", url="http://test.se/manifest.mpd") segments = mpd.periods[0].adaptationSets[1].representations[0].segments() init_segment = next(segments) self.assertEqual(init_segment.url, "http://test.se/dash/150633-video_eng=194000.dash") video_segments = [x.url for x in itertools.islice(segments, 3)] self.assertSequenceEqual(video_segments, ['http://test.se/dash/150633-video_eng=194000-0.dash', 'http://test.se/dash/150633-video_eng=194000-2000.dash', 'http://test.se/dash/150633-video_eng=194000-4000.dash', ]) def test_segments_list(self): with xml("dash/test_7.mpd") as mpd_xml: mpd = MPD(mpd_xml, base_url="http://test.se/", url="http://test.se/manifest.mpd") segments = mpd.periods[0].adaptationSets[0].representations[0].segments() init_segment = next(segments) self.assertEqual(init_segment.url, "http://test.se/chunk_ctvideo_ridp0va0br4332748_cinit_mpd.m4s") video_segments = [x.url for x in itertools.islice(segments, 3)] self.assertSequenceEqual(video_segments, ['http://test.se/chunk_ctvideo_ridp0va0br4332748_cn1_mpd.m4s', 'http://test.se/chunk_ctvideo_ridp0va0br4332748_cn2_mpd.m4s', 'http://test.se/chunk_ctvideo_ridp0va0br4332748_cn3_mpd.m4s', ]) def test_segments_dynamic_timeline_continue(self): with xml("dash/test_6_p1.mpd") as mpd_xml_p1: with xml("dash/test_6_p2.mpd") as mpd_xml_p2: mpd_p1 = MPD(mpd_xml_p1, base_url="http://test.se/", url="http://test.se/manifest.mpd") segments_p1 = mpd_p1.periods[0].adaptationSets[0].representations[0].segments() init_segment = next(segments_p1) self.assertEqual(init_segment.url, "http://test.se/video/init.mp4") video_segments_p1 = [x.url for x in itertools.islice(segments_p1, 100)] self.assertSequenceEqual(video_segments_p1, ['http://test.se/video/1006000.mp4', 'http://test.se/video/1007000.mp4', 'http://test.se/video/1008000.mp4', 'http://test.se/video/1009000.mp4', 'http://test.se/video/1010000.mp4']) # Continue in the next manifest mpd_p2 = MPD(mpd_xml_p2, base_url=mpd_p1.base_url, url=mpd_p1.url, timelines=mpd_p1.timelines) segments_p2 = mpd_p2.periods[0].adaptationSets[0].representations[0].segments(init=False) video_segments_p2 = [x.url for x in itertools.islice(segments_p2, 100)] self.assertSequenceEqual(video_segments_p2, ['http://test.se/video/1011000.mp4', 'http://test.se/video/1012000.mp4', 'http://test.se/video/1013000.mp4', 'http://test.se/video/1014000.mp4', 'http://test.se/video/1015000.mp4']) def test_tsegment_t_is_none_1895(self): """ Verify the fix for https://github.com/streamlink/streamlink/issues/1895 """ with xml("dash/test_8.mpd") as mpd_xml: mpd = MPD(mpd_xml, base_url="http://test.se/", url="http://test.se/manifest.mpd") segments = mpd.periods[0].adaptationSets[0].representations[0].segments() init_segment = next(segments) self.assertEqual(init_segment.url, "http://test.se/video-2799000-0.mp4?z32=CENSORED_SESSION") video_segments = [x.url for x in itertools.islice(segments, 3)] self.assertSequenceEqual(video_segments, ['http://test.se/video-time=0-2799000-0.m4s?z32=CENSORED_SESSION', 'http://test.se/video-time=4000-2799000-0.m4s?z32=CENSORED_SESSION', 'http://test.se/video-time=8000-2799000-0.m4s?z32=CENSORED_SESSION', ]) def test_bitrate_rounded(self): def mock_rep(bandwidth): node = Mock( tag="Representation", attrib={ "id": "test", "bandwidth": bandwidth, "mimeType": "video/mp4" } ) node.findall.return_value = [] return Representation(node) self.assertEqual(mock_rep(1.2 * 1000.0).bandwidth_rounded, 1.2) self.assertEqual(mock_rep(45.6 * 1000.0).bandwidth_rounded, 46.0) self.assertEqual(mock_rep(134.0 * 1000.0).bandwidth_rounded, 130.0) self.assertEqual(mock_rep(1324.0 * 1000.0).bandwidth_rounded, 1300.0) def test_duplicated_resolutions(self): """ Verify the fix for https://github.com/streamlink/streamlink/issues/3365 """ with xml("dash/test_10.mpd") as mpd_xml: mpd = MPD(mpd_xml, base_url="http://test.se/", url="http://test.se/manifest.mpd") representations_0 = mpd.periods[0].adaptationSets[0].representations[0] self.assertEqual(representations_0.height, 804) self.assertEqual(representations_0.bandwidth, 10000.0) representations_1 = mpd.periods[0].adaptationSets[0].representations[1] self.assertEqual(representations_1.height, 804) self.assertEqual(representations_1.bandwidth, 8000.0) def test_segments_static_periods_duration(self): """ Verify the fix for https://github.com/streamlink/streamlink/issues/2873 """ with xml("dash/test_11_static.mpd") as mpd_xml: mpd = MPD(mpd_xml, base_url="http://test.se/", url="http://test.se/manifest.mpd") duration = mpd.periods[0].duration.total_seconds() self.assertEqual(duration, 204.32) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/stream/test_ffmpegmux.py0000644000175100001710000003472300000000000021302 0ustar00runnerdockerfrom unittest.mock import ANY, patch import pytest from streamlink.stream.ffmpegmux import FFMPEGMuxer @pytest.fixture def session(): from streamlink import Streamlink return Streamlink() def test_ffmpeg_command(session): with patch('streamlink.stream.ffmpegmux.which', return_value="ffmpeg"): assert FFMPEGMuxer.command(session) == "ffmpeg" def test_ffmpeg_open(session): with patch('streamlink.stream.ffmpegmux.which', return_value="ffmpeg"): f = FFMPEGMuxer(session, format="mpegts") with patch('subprocess.Popen') as popen: f.open() popen.assert_called_with(['ffmpeg', '-nostats', '-y', '-c:v', FFMPEGMuxer.DEFAULT_VIDEO_CODEC, '-c:a', FFMPEGMuxer.DEFAULT_AUDIO_CODEC, '-f', 'mpegts', 'pipe:1'], stderr=ANY, stdout=ANY, stdin=ANY) def test_ffmpeg_open_default(session): with patch('streamlink.stream.ffmpegmux.which', return_value="ffmpeg"): f = FFMPEGMuxer(session) with patch('subprocess.Popen') as popen: f.open() popen.assert_called_with(['ffmpeg', '-nostats', '-y', '-c:v', FFMPEGMuxer.DEFAULT_VIDEO_CODEC, '-c:a', FFMPEGMuxer.DEFAULT_AUDIO_CODEC, '-f', FFMPEGMuxer.DEFAULT_OUTPUT_FORMAT, 'pipe:1'], stderr=ANY, stdout=ANY, stdin=ANY) def test_ffmpeg_open_format(session): with patch('streamlink.stream.ffmpegmux.which', return_value="ffmpeg"): session.options.set("ffmpeg-fout", "avi") f = FFMPEGMuxer(session, format="mpegts") with patch('subprocess.Popen') as popen: f.open() popen.assert_called_with(['ffmpeg', '-nostats', '-y', '-c:v', FFMPEGMuxer.DEFAULT_VIDEO_CODEC, '-c:a', FFMPEGMuxer.DEFAULT_AUDIO_CODEC, '-f', 'avi', 'pipe:1'], stderr=ANY, stdout=ANY, stdin=ANY) def test_ffmpeg_open_copyts(session): with patch('streamlink.stream.ffmpegmux.which', return_value="ffmpeg"): f = FFMPEGMuxer(session, copyts=True) with patch('subprocess.Popen') as popen: f.open() popen.assert_called_with(['ffmpeg', '-nostats', '-y', '-c:v', FFMPEGMuxer.DEFAULT_VIDEO_CODEC, '-c:a', FFMPEGMuxer.DEFAULT_AUDIO_CODEC, '-copyts', '-f', 'matroska', 'pipe:1'], stderr=ANY, stdout=ANY, stdin=ANY) def test_ffmpeg_open_copyts_user_override(session): with patch('streamlink.stream.ffmpegmux.which', return_value="ffmpeg"): session.options.set("ffmpeg-copyts", True) f = FFMPEGMuxer(session, copyts=False) with patch('subprocess.Popen') as popen: f.open() popen.assert_called_with(['ffmpeg', '-nostats', '-y', '-c:v', FFMPEGMuxer.DEFAULT_VIDEO_CODEC, '-c:a', FFMPEGMuxer.DEFAULT_AUDIO_CODEC, '-copyts', '-f', 'matroska', 'pipe:1'], stderr=ANY, stdout=ANY, stdin=ANY) def test_ffmpeg_open_copyts_disable_session_start_at_zero(session): with patch('streamlink.stream.ffmpegmux.which', return_value="ffmpeg"): session.options.set("ffmpeg-start-at-zero", False) f = FFMPEGMuxer(session, copyts=True) with patch('subprocess.Popen') as popen: f.open() popen.assert_called_with(['ffmpeg', '-nostats', '-y', '-c:v', FFMPEGMuxer.DEFAULT_VIDEO_CODEC, '-c:a', FFMPEGMuxer.DEFAULT_AUDIO_CODEC, '-copyts', '-f', 'matroska', 'pipe:1'], stderr=ANY, stdout=ANY, stdin=ANY) def test_ffmpeg_open_copyts_disable_session_start_at_zero_user_override(session): with patch('streamlink.stream.ffmpegmux.which', return_value="ffmpeg"): session.options.set("ffmpeg-copyts", True) session.options.set("ffmpeg-start-at-zero", False) f = FFMPEGMuxer(session, copyts=False) with patch('subprocess.Popen') as popen: f.open() popen.assert_called_with(['ffmpeg', '-nostats', '-y', '-c:v', FFMPEGMuxer.DEFAULT_VIDEO_CODEC, '-c:a', FFMPEGMuxer.DEFAULT_AUDIO_CODEC, '-copyts', '-f', 'matroska', 'pipe:1'], stderr=ANY, stdout=ANY, stdin=ANY) def test_ffmpeg_open_copyts_enable_session_start_at_zero(session): with patch('streamlink.stream.ffmpegmux.which', return_value="ffmpeg"): session.options.set("ffmpeg-start-at-zero", True) f = FFMPEGMuxer(session, copyts=True) with patch('subprocess.Popen') as popen: f.open() popen.assert_called_with(['ffmpeg', '-nostats', '-y', '-c:v', FFMPEGMuxer.DEFAULT_VIDEO_CODEC, '-c:a', FFMPEGMuxer.DEFAULT_AUDIO_CODEC, '-copyts', '-start_at_zero', '-f', 'matroska', 'pipe:1'], stderr=ANY, stdout=ANY, stdin=ANY) def test_ffmpeg_open_copyts_enable_session_start_at_zero_user_override(session): with patch('streamlink.stream.ffmpegmux.which', return_value="ffmpeg"): session.options.set("ffmpeg-copyts", True) session.options.set("ffmpeg-start-at-zero", True) f = FFMPEGMuxer(session, copyts=False) with patch('subprocess.Popen') as popen: f.open() popen.assert_called_with(['ffmpeg', '-nostats', '-y', '-c:v', FFMPEGMuxer.DEFAULT_VIDEO_CODEC, '-c:a', FFMPEGMuxer.DEFAULT_AUDIO_CODEC, '-copyts', '-start_at_zero', '-f', 'matroska', 'pipe:1'], stderr=ANY, stdout=ANY, stdin=ANY) def test_ffmpeg_open_copyts_disable_start_at_zero(session): with patch('streamlink.stream.ffmpegmux.which', return_value="ffmpeg"): f = FFMPEGMuxer(session, copyts=True, start_at_zero=False) with patch('subprocess.Popen') as popen: f.open() popen.assert_called_with(['ffmpeg', '-nostats', '-y', '-c:v', FFMPEGMuxer.DEFAULT_VIDEO_CODEC, '-c:a', FFMPEGMuxer.DEFAULT_AUDIO_CODEC, '-copyts', '-f', 'matroska', 'pipe:1'], stderr=ANY, stdout=ANY, stdin=ANY) def test_ffmpeg_open_copyts_disable_start_at_zero_user_override(session): with patch('streamlink.stream.ffmpegmux.which', return_value="ffmpeg"): session.options.set("ffmpeg-copyts", True) f = FFMPEGMuxer(session, copyts=False, start_at_zero=False) with patch('subprocess.Popen') as popen: f.open() popen.assert_called_with(['ffmpeg', '-nostats', '-y', '-c:v', FFMPEGMuxer.DEFAULT_VIDEO_CODEC, '-c:a', FFMPEGMuxer.DEFAULT_AUDIO_CODEC, '-copyts', '-f', 'matroska', 'pipe:1'], stderr=ANY, stdout=ANY, stdin=ANY) def test_ffmpeg_open_copyts_enable_start_at_zero(session): with patch('streamlink.stream.ffmpegmux.which', return_value="ffmpeg"): f = FFMPEGMuxer(session, copyts=True, start_at_zero=True) with patch('subprocess.Popen') as popen: f.open() popen.assert_called_with(['ffmpeg', '-nostats', '-y', '-c:v', FFMPEGMuxer.DEFAULT_VIDEO_CODEC, '-c:a', FFMPEGMuxer.DEFAULT_AUDIO_CODEC, '-copyts', '-start_at_zero', '-f', 'matroska', 'pipe:1'], stderr=ANY, stdout=ANY, stdin=ANY) def test_ffmpeg_open_copyts_enable_start_at_zero_user_override(session): with patch('streamlink.stream.ffmpegmux.which', return_value="ffmpeg"): session.options.set("ffmpeg-copyts", True) f = FFMPEGMuxer(session, copyts=False, start_at_zero=True) with patch('subprocess.Popen') as popen: f.open() popen.assert_called_with(['ffmpeg', '-nostats', '-y', '-c:v', FFMPEGMuxer.DEFAULT_VIDEO_CODEC, '-c:a', FFMPEGMuxer.DEFAULT_AUDIO_CODEC, '-copyts', '-start_at_zero', '-f', 'matroska', 'pipe:1'], stderr=ANY, stdout=ANY, stdin=ANY) def test_ffmpeg_open_vcodec(session): with patch('streamlink.stream.ffmpegmux.which', return_value="ffmpeg"): f = FFMPEGMuxer(session, vcodec="avc") with patch('subprocess.Popen') as popen: f.open() popen.assert_called_with(['ffmpeg', '-nostats', '-y', '-c:v', 'avc', '-c:a', FFMPEGMuxer.DEFAULT_AUDIO_CODEC, '-f', 'matroska', 'pipe:1'], stderr=ANY, stdout=ANY, stdin=ANY) def test_ffmpeg_open_vcodec_user_override(session): with patch('streamlink.stream.ffmpegmux.which', return_value="ffmpeg"): session.options.set("ffmpeg-video-transcode", "divx") f = FFMPEGMuxer(session, vcodec="avc") with patch('subprocess.Popen') as popen: f.open() popen.assert_called_with(['ffmpeg', '-nostats', '-y', '-c:v', 'divx', '-c:a', FFMPEGMuxer.DEFAULT_AUDIO_CODEC, '-f', 'matroska', 'pipe:1'], stderr=ANY, stdout=ANY, stdin=ANY) def test_ffmpeg_open_acodec(session): with patch('streamlink.stream.ffmpegmux.which', return_value="ffmpeg"): f = FFMPEGMuxer(session, acodec="mp3") with patch('subprocess.Popen') as popen: f.open() popen.assert_called_with(['ffmpeg', '-nostats', '-y', '-c:v', FFMPEGMuxer.DEFAULT_VIDEO_CODEC, '-c:a', 'mp3', '-f', 'matroska', 'pipe:1'], stderr=ANY, stdout=ANY, stdin=ANY) def test_ffmpeg_open_acodec_user_override(session): with patch('streamlink.stream.ffmpegmux.which', return_value="ffmpeg"): session.options.set("ffmpeg-audio-transcode", "ogg") f = FFMPEGMuxer(session, acodec="mp3") with patch('subprocess.Popen') as popen: f.open() popen.assert_called_with(['ffmpeg', '-nostats', '-y', '-c:v', FFMPEGMuxer.DEFAULT_VIDEO_CODEC, '-c:a', 'ogg', '-f', 'matroska', 'pipe:1'], stderr=ANY, stdout=ANY, stdin=ANY) def test_ffmpeg_open_vcodec_acodec(session): with patch('streamlink.stream.ffmpegmux.which', return_value="ffmpeg"): f = FFMPEGMuxer(session, acodec="mp3", vcodec="avc") with patch('subprocess.Popen') as popen: f.open() popen.assert_called_with(['ffmpeg', '-nostats', '-y', '-c:v', 'avc', '-c:a', 'mp3', '-f', 'matroska', 'pipe:1'], stderr=ANY, stdout=ANY, stdin=ANY) def test_ffmpeg_open_vcodec_acodec_user_override(session): with patch('streamlink.stream.ffmpegmux.which', return_value="ffmpeg"): session.options.set("ffmpeg-video-transcode", "divx") session.options.set("ffmpeg-audio-transcode", "ogg") f = FFMPEGMuxer(session, acodec="mp3", vcodec="avc") with patch('subprocess.Popen') as popen: f.open() popen.assert_called_with(['ffmpeg', '-nostats', '-y', '-c:v', 'divx', '-c:a', 'ogg', '-f', 'matroska', 'pipe:1'], stderr=ANY, stdout=ANY, stdin=ANY) def test_ffmpeg_open_maps(session): with patch('streamlink.stream.ffmpegmux.which', return_value="ffmpeg"): f = FFMPEGMuxer(session, maps=["test", "test2"]) with patch('subprocess.Popen') as popen: f.open() popen.assert_called_with(['ffmpeg', '-nostats', '-y', '-c:v', FFMPEGMuxer.DEFAULT_VIDEO_CODEC, '-c:a', FFMPEGMuxer.DEFAULT_AUDIO_CODEC, '-map', 'test', '-map', 'test2', '-f', 'matroska', 'pipe:1'], stderr=ANY, stdout=ANY, stdin=ANY) def test_ffmpeg_open_metadata_stream_audio(session): with patch('streamlink.stream.ffmpegmux.which', return_value="ffmpeg"): f = FFMPEGMuxer(session, metadata={"s:a:0": ["language=eng"]}) with patch('subprocess.Popen') as popen: f.open() popen.assert_called_with(['ffmpeg', '-nostats', '-y', '-c:v', FFMPEGMuxer.DEFAULT_VIDEO_CODEC, '-c:a', FFMPEGMuxer.DEFAULT_AUDIO_CODEC, '-metadata:s:a:0', 'language=eng', '-f', 'matroska', 'pipe:1'], stderr=ANY, stdout=ANY, stdin=ANY) def test_ffmpeg_open_metadata_title(session): with patch('streamlink.stream.ffmpegmux.which', return_value="ffmpeg"): f = FFMPEGMuxer(session, metadata={None: ["title=test"]}) with patch('subprocess.Popen') as popen: f.open() popen.assert_called_with(['ffmpeg', '-nostats', '-y', '-c:v', FFMPEGMuxer.DEFAULT_VIDEO_CODEC, '-c:a', FFMPEGMuxer.DEFAULT_AUDIO_CODEC, '-metadata', 'title=test', '-f', 'matroska', 'pipe:1'], stderr=ANY, stdout=ANY, stdin=ANY) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/stream/test_hls.py0000644000175100001710000006516000000000000020071 0ustar00runnerdockerimport os import typing import unittest from threading import Event from unittest.mock import Mock, call, patch import pytest import requests_mock from Crypto.Cipher import AES from Crypto.Util.Padding import pad from streamlink.session import Streamlink from streamlink.stream.hls import HLSStream, HLSStreamReader from tests.mixins.stream_hls import EventedHLSStreamWriter, Playlist, Segment, Tag, TestMixinStreamHLS from tests.resources import text class EncryptedBase: def __init__(self, num, key, iv, *args, padding=b"", append=b"", **kwargs): super().__init__(num, *args, **kwargs) aesCipher = AES.new(key, AES.MODE_CBC, iv) padded = self.content + padding if padding else pad(self.content, AES.block_size, style="pkcs7") self.content_plain = self.content self.content = aesCipher.encrypt(padded) + append class TagMap(Tag): def __init__(self, num, namespace, attrs=None): self.path = f"map{num}" self.content = f"[map{num}]".encode("ascii") super().__init__("EXT-X-MAP", { "URI": self.val_quoted_string(self.url(namespace)), **(attrs or {}) }) class TagMapEnc(EncryptedBase, TagMap): pass class TagKey(Tag): path = "encryption.key" def __init__(self, method="NONE", uri=None, iv=None, keyformat=None, keyformatversions=None): attrs = {"METHOD": method} if uri is not False: # pragma: no branch attrs.update({"URI": lambda tag, namespace: tag.val_quoted_string(tag.url(namespace))}) if iv is not None: # pragma: no branch attrs.update({"IV": self.val_hex(iv)}) if keyformat is not None: # pragma: no branch attrs.update({"KEYFORMAT": self.val_quoted_string(keyformat)}) if keyformatversions is not None: # pragma: no branch attrs.update({"KEYFORMATVERSIONS": self.val_quoted_string(keyformatversions)}) super().__init__("EXT-X-KEY", attrs) self.uri = uri def url(self, namespace): return self.uri.format(namespace=namespace) if self.uri else super().url(namespace) class SegmentEnc(EncryptedBase, Segment): pass class TestHLSStreamRepr(unittest.TestCase): def test_repr(self): session = Streamlink() stream = HLSStream(session, "https://foo.bar/playlist.m3u8") self.assertEqual(repr(stream), "") stream = HLSStream(session, "https://foo.bar/playlist.m3u8", "https://foo.bar/master.m3u8") self.assertEqual(repr(stream), "") class TestHLSVariantPlaylist(unittest.TestCase): @classmethod def get_master_playlist(cls, playlist): with text(playlist) as pl: return pl.read() def subject(self, playlist, options=None): with requests_mock.Mocker() as mock: url = "http://mocked/{0}/master.m3u8".format(self.id()) content = self.get_master_playlist(playlist) mock.get(url, text=content) session = Streamlink(options) return HLSStream.parse_variant_playlist(session, url) def test_variant_playlist(self): streams = self.subject("hls/test_master.m3u8") self.assertEqual( list(streams.keys()), ["720p", "720p_alt", "480p", "360p", "160p", "1080p (source)", "90k"], "Finds all streams in master playlist" ) self.assertTrue( all([isinstance(stream, HLSStream) for stream in streams.values()]), "Returns HLSStream instances" ) class EventedHLSReader(HLSStreamReader): __writer__ = EventedHLSStreamWriter class EventedHLSStream(HLSStream): __reader__ = EventedHLSReader @patch("streamlink.stream.hls.HLSStreamWorker.wait", Mock(return_value=True)) class TestHLSStream(TestMixinStreamHLS, unittest.TestCase): def get_session(self, options=None, *args, **kwargs): session = super().get_session(options) session.set_option("hls-live-edge", 3) return session def test_offset_and_duration(self): thread, segments = self.subject([ Playlist(1234, [Segment(0), Segment(1, duration=0.5), Segment(2, duration=0.5), Segment(3)], end=True) ], streamoptions={"start_offset": 1, "duration": 1}) data = self.await_read(read_all=True) self.assertEqual(data, self.content(segments, cond=lambda s: 0 < s.num < 3), "Respects the offset and duration") self.assertTrue(all(self.called(s) for s in segments.values() if 0 < s.num < 3), "Downloads second and third segment") self.assertFalse(any(self.called(s) for s in segments.values() if 0 > s.num > 3), "Skips other segments") def test_map(self): discontinuity = Tag("EXT-X-DISCONTINUITY") map1 = TagMap(1, self.id()) map2 = TagMap(2, self.id()) self.mock("GET", self.url(map1), content=map1.content) self.mock("GET", self.url(map2), content=map2.content) thread, segments = self.subject([ Playlist(0, [map1, Segment(0), Segment(1), Segment(2), Segment(3)]), Playlist(4, [map1, Segment(4), map2, Segment(5), Segment(6), discontinuity, Segment(7)], end=True) ]) data = self.await_read(read_all=True, timeout=None) self.assertEqual(data, self.content([ map1, segments[1], map1, segments[2], map1, segments[3], map1, segments[4], map2, segments[5], map2, segments[6], segments[7] ])) self.assertTrue(self.called(map1, once=True), "Downloads first map only once") self.assertTrue(self.called(map2, once=True), "Downloads second map only once") @patch("streamlink.stream.hls.HLSStreamWorker.wait", Mock(return_value=True)) class TestHLSStreamByterange(TestMixinStreamHLS, unittest.TestCase): __stream__ = EventedHLSStream # The dummy segments in the error tests are required because the writer's run loop would otherwise continue forever # due to the segment's future result being None (no requests result), and we can't await the end of the stream # without waiting for the stream's timeout error. The dummy segments ensure that we can call await_write for these # successful segments, so we can close the stream afterwards and safely make the test assertions. # The EventedHLSStreamWriter could also implement await_fetch, but this is unnecessarily more complex than it already is. @patch("streamlink.stream.hls.log") def test_unknown_offset(self, mock_log: Mock): thread, _ = self.subject([ Playlist(0, [ Tag("EXT-X-BYTERANGE", "3"), Segment(0), Segment(1) ], end=True) ]) self.await_write(2 - 1) self.thread.close() self.assertEqual(mock_log.error.call_args_list, [ call("Failed to fetch segment 0: Missing BYTERANGE offset") ]) self.assertFalse(self.called(Segment(0))) @patch("streamlink.stream.hls.log") def test_unknown_offset_map(self, mock_log: Mock): map1 = TagMap(1, self.id(), {"BYTERANGE": "\"1234\""}) self.mock("GET", self.url(map1), content=map1.content) thread, _ = self.subject([ Playlist(0, [ Segment(0), map1, Segment(1) ], end=True) ]) self.await_write(3 - 1) self.thread.close() self.assertEqual(mock_log.error.call_args_list, [ call("Failed to fetch map for segment 1: Missing BYTERANGE offset") ]) self.assertFalse(self.called(map1)) @patch("streamlink.stream.hls.log") def test_invalid_offset_reference(self, mock_log: Mock): thread, _ = self.subject([ Playlist(0, [ Tag("EXT-X-BYTERANGE", "3@0"), Segment(0), Segment(1), Tag("EXT-X-BYTERANGE", "5"), Segment(2), Segment(3) ], end=True) ]) self.await_write(4 - 1) self.thread.close() self.assertEqual(mock_log.error.call_args_list, [ call("Failed to fetch segment 2: Missing BYTERANGE offset") ]) self.assertEqual(self.mocks[self.url(Segment(0))].last_request._request.headers["Range"], "bytes=0-2") self.assertFalse(self.called(Segment(2))) def test_offsets(self): map1 = TagMap(1, self.id(), {"BYTERANGE": "\"1234@0\""}) map2 = TagMap(2, self.id(), {"BYTERANGE": "\"42@1337\""}) self.mock("GET", self.url(map1), content=map1.content) self.mock("GET", self.url(map2), content=map2.content) s1, s2, s3, s4, s5 = Segment(0), Segment(1), Segment(2), Segment(3), Segment(4) self.subject([ Playlist(0, [ map1, Tag("EXT-X-BYTERANGE", "5@3"), s1, Tag("EXT-X-BYTERANGE", "7"), s2, map2, Tag("EXT-X-BYTERANGE", "11"), s3, Tag("EXT-X-BYTERANGE", "17@13"), s4, Tag("EXT-X-BYTERANGE", "19"), s5, ], end=True) ]) self.await_write(5 * 2) self.await_read(read_all=True) self.assertEqual(self.mocks[self.url(map1)].last_request._request.headers["Range"], "bytes=0-1233") self.assertEqual(self.mocks[self.url(map2)].last_request._request.headers["Range"], "bytes=1337-1378") self.assertEqual(self.mocks[self.url(s1)].last_request._request.headers["Range"], "bytes=3-7") self.assertEqual(self.mocks[self.url(s2)].last_request._request.headers["Range"], "bytes=8-14") self.assertEqual(self.mocks[self.url(s3)].last_request._request.headers["Range"], "bytes=15-25") self.assertEqual(self.mocks[self.url(s4)].last_request._request.headers["Range"], "bytes=13-29") self.assertEqual(self.mocks[self.url(s5)].last_request._request.headers["Range"], "bytes=30-48") @patch("streamlink.stream.hls.HLSStreamWorker.wait", Mock(return_value=True)) class TestHLSStreamEncrypted(TestMixinStreamHLS, unittest.TestCase): __stream__ = EventedHLSStream def get_session(self, options=None, *args, **kwargs): session = super().get_session(options) session.set_option("hls-live-edge", 3) session.set_option("http-headers", {"X-FOO": "BAR"}) return session def gen_key(self, aes_key=None, aes_iv=None, method="AES-128", uri=None, keyformat="identity", keyformatversions=1): aes_key = aes_key or os.urandom(16) aes_iv = aes_iv or os.urandom(16) key = TagKey(method=method, uri=uri, iv=aes_iv, keyformat=keyformat, keyformatversions=keyformatversions) self.mock("GET", key.url(self.id()), content=aes_key) return aes_key, aes_iv, key def test_hls_encrypted_aes128(self): aesKey, aesIv, key = self.gen_key() # noinspection PyTypeChecker thread, segments = self.subject([ Playlist(0, [key] + [SegmentEnc(num, aesKey, aesIv) for num in range(0, 4)]), Playlist(4, [key] + [SegmentEnc(num, aesKey, aesIv) for num in range(4, 8)], end=True) ]) self.await_write(3 + 4) data = self.await_read(read_all=True) expected = self.content(segments, prop="content_plain", cond=lambda s: s.num >= 1) self.assertEqual(data, expected, "Decrypts the AES-128 identity stream") self.assertTrue(self.called(key, once=True), "Downloads encryption key only once") self.assertEqual(self.get_mock(key).last_request._request.headers.get("X-FOO"), "BAR") self.assertFalse(any(self.called(s) for s in segments.values() if s.num < 1), "Skips first segment") self.assertTrue(all(self.called(s) for s in segments.values() if s.num >= 1), "Downloads all remaining segments") self.assertEqual(self.get_mock(segments[1]).last_request._request.headers.get("X-FOO"), "BAR") def test_hls_encrypted_aes128_with_map(self): aesKey, aesIv, key = self.gen_key() map1 = TagMapEnc(1, namespace=self.id(), key=aesKey, iv=aesIv) map2 = TagMapEnc(2, namespace=self.id(), key=aesKey, iv=aesIv) self.mock("GET", self.url(map1), content=map1.content) self.mock("GET", self.url(map2), content=map2.content) # noinspection PyTypeChecker thread, segments = self.subject([ Playlist(0, [key, map1] + [SegmentEnc(num, aesKey, aesIv) for num in range(0, 2)]), Playlist(2, [key, map2] + [SegmentEnc(num, aesKey, aesIv) for num in range(2, 4)], end=True) ]) self.await_write(2 * 2 + 2 * 2) data = self.await_read(read_all=True) self.assertEqual(data, self.content([ map1, segments[0], map1, segments[1], map2, segments[2], map2, segments[3] ], prop="content_plain")) def test_hls_encrypted_aes128_key_uri_override(self): aesKey, aesIv, key = self.gen_key(uri="http://real-mocked/{namespace}/encryption.key?foo=bar") aesKeyInvalid = bytes([ord(aesKey[i:i + 1]) ^ 0xFF for i in range(16)]) _, __, key_invalid = self.gen_key(aesKeyInvalid, aesIv, uri="http://mocked/{namespace}/encryption.key?foo=bar") # noinspection PyTypeChecker thread, segments = self.subject([ Playlist(0, [key_invalid] + [SegmentEnc(num, aesKey, aesIv) for num in range(0, 4)]), Playlist(4, [key_invalid] + [SegmentEnc(num, aesKey, aesIv) for num in range(4, 8)], end=True) ], options={"hls-segment-key-uri": "{scheme}://real-{netloc}{path}?{query}"}) self.await_write(3 + 4) data = self.await_read(read_all=True) expected = self.content(segments, prop="content_plain", cond=lambda s: s.num >= 1) self.assertEqual(data, expected, "Decrypts stream from custom key") self.assertFalse(self.called(key_invalid), "Skips encryption key") self.assertTrue(self.called(key, once=True), "Downloads custom encryption key") self.assertEqual(self.get_mock(key).last_request._request.headers.get("X-FOO"), "BAR") @patch("streamlink.stream.hls.log") def test_hls_encrypted_aes128_incorrect_block_length(self, mock_log): aesKey, aesIv, key = self.gen_key() # noinspection PyTypeChecker thread, segments = self.subject([ Playlist(0, [key] + [ SegmentEnc(0, aesKey, aesIv, append=b"?" * 1), SegmentEnc(1, aesKey, aesIv, append=b"?" * (AES.block_size - 1)) ], end=True) ]) self.await_write(2) data = self.await_read(read_all=True) expected = self.content(segments, prop="content_plain") self.assertEqual(data, expected, "Removes garbage data from segments") self.assertIn(call("Cutting off 1 bytes of garbage before decrypting"), mock_log.debug.mock_calls) self.assertIn(call("Cutting off 15 bytes of garbage before decrypting"), mock_log.debug.mock_calls) def test_hls_encrypted_aes128_incorrect_padding_length(self): aesKey, aesIv, key = self.gen_key() padding = b"\x00" * (AES.block_size - len(b"[0]")) self.subject([ Playlist(0, [key, SegmentEnc(0, aesKey, aesIv, padding=padding)], end=True) ]) # close read thread early self.thread.close() with self.assertRaises(ValueError) as cm: self.await_write() self.assertEqual(str(cm.exception), "Padding is incorrect.", "Crypto.Util.Padding.unpad exception") def test_hls_encrypted_aes128_incorrect_padding_content(self): aesKey, aesIv, key = self.gen_key() padding = (b"\x00" * (AES.block_size - len(b"[0]") - 1)) + bytes([AES.block_size]) self.subject([ Playlist(0, [key, SegmentEnc(0, aesKey, aesIv, padding=padding)], end=True) ]) # close read thread early self.thread.close() with self.assertRaises(ValueError) as cm: self.await_write() self.assertEqual(str(cm.exception), "PKCS#7 padding is incorrect.", "Crypto.Util.Padding.unpad exception") @patch("streamlink.stream.hls.HLSStreamWorker.wait", Mock(return_value=True)) class TestHlsPlaylistReloadTime(TestMixinStreamHLS, unittest.TestCase): segments = [ Segment(0, duration=11), Segment(1, duration=7), Segment(2, duration=5), Segment(3, duration=3) ] def get_session(self, options=None, reload_time=None, *args, **kwargs): return super().get_session(dict(options or {}, **{ "hls-live-edge": 3, "hls-playlist-reload-time": reload_time })) def subject(self, *args, **kwargs): thread, segments = super().subject(start=False, *args, **kwargs) # mock the worker thread's _playlist_reload_time method, so that the main thread can wait on its call playlist_reload_time_called = Event() orig_playlist_reload_time = thread.reader.worker._playlist_reload_time def mocked_playlist_reload_time(*args, **kwargs): playlist_reload_time_called.set() return orig_playlist_reload_time(*args, **kwargs) # immediately kill the writer thread as we don't need it and don't want to wait for its queue polling to end def mocked_futures_get(): return None, None with patch.object(thread.reader.worker, "_playlist_reload_time", side_effect=mocked_playlist_reload_time), \ patch.object(thread.reader.writer, "_futures_get", side_effect=mocked_futures_get): self.start() if not playlist_reload_time_called.wait(timeout=5): # pragma: no cover raise RuntimeError("Missing _playlist_reload_time() call") # wait for the worker thread to terminate, so that deterministic assertions can be done about the reload time thread.reader.worker.join() return thread.reader.worker.playlist_reload_time def test_hls_playlist_reload_time_default(self): time = self.subject([Playlist(0, self.segments, end=True, targetduration=4)], reload_time="default") self.assertEqual(time, 4, "default sets the reload time to the playlist's target duration") def test_hls_playlist_reload_time_segment(self): time = self.subject([Playlist(0, self.segments, end=True, targetduration=4)], reload_time="segment") self.assertEqual(time, 3, "segment sets the reload time to the playlist's last segment") def test_hls_playlist_reload_time_segment_no_segments(self): time = self.subject([Playlist(0, [], end=True, targetduration=4)], reload_time="segment") self.assertEqual(time, 4, "segment sets the reload time to the targetduration if no segments are available") def test_hls_playlist_reload_time_segment_no_segments_no_targetduration(self): time = self.subject([Playlist(0, [], end=True, targetduration=0)], reload_time="segment") self.assertEqual(time, 6, "sets reload time to 6 seconds when no segments and no targetduration are available") def test_hls_playlist_reload_time_live_edge(self): time = self.subject([Playlist(0, self.segments, end=True, targetduration=4)], reload_time="live-edge") self.assertEqual(time, 8, "live-edge sets the reload time to the sum of the number of segments of the live-edge") def test_hls_playlist_reload_time_live_edge_no_segments(self): time = self.subject([Playlist(0, [], end=True, targetduration=4)], reload_time="live-edge") self.assertEqual(time, 4, "live-edge sets the reload time to the targetduration if no segments are available") def test_hls_playlist_reload_time_live_edge_no_segments_no_targetduration(self): time = self.subject([Playlist(0, [], end=True, targetduration=0)], reload_time="live-edge") self.assertEqual(time, 6, "sets reload time to 6 seconds when no segments and no targetduration are available") def test_hls_playlist_reload_time_number(self): time = self.subject([Playlist(0, self.segments, end=True, targetduration=4)], reload_time="2") self.assertEqual(time, 2, "number values override the reload time") def test_hls_playlist_reload_time_number_invalid(self): time = self.subject([Playlist(0, self.segments, end=True, targetduration=4)], reload_time="0") self.assertEqual(time, 4, "invalid number values set the reload time to the playlist's targetduration") def test_hls_playlist_reload_time_no_target_duration(self): time = self.subject([Playlist(0, self.segments, end=True, targetduration=0)], reload_time="default") self.assertEqual(time, 8, "uses the live-edge sum if the playlist is missing the targetduration data") def test_hls_playlist_reload_time_no_data(self): time = self.subject([Playlist(0, [], end=True, targetduration=0)], reload_time="default") self.assertEqual(time, 6, "sets reload time to 6 seconds when no data is available") @patch("streamlink.stream.hls.log") @patch("streamlink.stream.hls.HLSStreamWorker.wait", Mock(return_value=True)) class TestHlsPlaylistParseErrors(TestMixinStreamHLS, unittest.TestCase): __stream__ = EventedHLSStream class FakePlaylist(typing.NamedTuple): is_master: bool = False iframes_only: bool = False class InvalidPlaylist(Playlist): def build(self, *args, **kwargs): return "invalid" def test_generic(self, mock_log): self.subject([self.InvalidPlaylist()]) self.assertEqual(self.await_read(read_all=True), b"") self.await_close() self.assertTrue(self.thread.reader.buffer.closed, "Closes the stream on initial playlist parsing error") self.assertEqual(mock_log.debug.mock_calls, [call("Reloading playlist")]) self.assertEqual(mock_log.error.mock_calls, [call("Missing #EXTM3U header")]) def test_reload(self, mock_log): thread, segments = self.subject([ Playlist(1, [Segment(0)]), self.InvalidPlaylist(), self.InvalidPlaylist(), Playlist(2, [Segment(2)], end=True) ]) self.await_write(2) data = self.await_read(read_all=True) self.assertEqual(data, self.content(segments)) self.close() self.await_close() self.assertEqual(mock_log.warning.mock_calls, [ call("Failed to reload playlist: Missing #EXTM3U header"), call("Failed to reload playlist: Missing #EXTM3U header") ]) @patch("streamlink.stream.hls.HLSStreamWorker._reload_playlist", Mock(return_value=FakePlaylist(is_master=True))) def test_is_master(self, mock_log): self.subject([Playlist()]) self.assertEqual(self.await_read(read_all=True), b"") self.await_close() self.assertTrue(self.thread.reader.buffer.closed, "Closes the stream on initial playlist parsing error") self.assertEqual(mock_log.debug.mock_calls, [call("Reloading playlist")]) self.assertEqual(mock_log.error.mock_calls, [ call(f"Attempted to play a variant playlist, use 'hls://{self.stream.url}' instead") ]) @patch("streamlink.stream.hls.HLSStreamWorker._reload_playlist", Mock(return_value=FakePlaylist(iframes_only=True))) def test_iframes_only(self, mock_log): self.subject([Playlist()]) self.assertEqual(self.await_read(read_all=True), b"") self.await_close() self.assertTrue(self.thread.reader.buffer.closed, "Closes the stream on initial playlist parsing error") self.assertEqual(mock_log.debug.mock_calls, [call("Reloading playlist")]) self.assertEqual(mock_log.error.mock_calls, [call("Streams containing I-frames only are not playable")]) @patch('streamlink.stream.hls.FFMPEGMuxer.is_usable', Mock(return_value=True)) class TestHlsExtAudio(unittest.TestCase): @property def playlist(self): with text("hls/test_2.m3u8") as pl: return pl.read() def run_streamlink(self, playlist, audio_select=None): streamlink = Streamlink() if audio_select: streamlink.set_option("hls-audio-select", audio_select) master_stream = HLSStream.parse_variant_playlist(streamlink, playlist) return master_stream def test_hls_ext_audio_not_selected(self): master_url = "http://mocked/path/master.m3u8" with requests_mock.Mocker() as mock: mock.get(master_url, text=self.playlist) master_stream = self.run_streamlink(master_url)['video'] with pytest.raises(AttributeError): master_stream.substreams assert master_stream.url == 'http://mocked/path/playlist.m3u8' def test_hls_ext_audio_en(self): """ m3u8 with ext audio but no options should not download additional streams :return: """ master_url = "http://mocked/path/master.m3u8" expected = ['http://mocked/path/playlist.m3u8', 'http://mocked/path/en.m3u8'] with requests_mock.Mocker() as mock: mock.get(master_url, text=self.playlist) master_stream = self.run_streamlink(master_url, 'en') substreams = master_stream['video'].substreams result = [x.url for x in substreams] # Check result self.assertEqual(result, expected) def test_hls_ext_audio_es(self): """ m3u8 with ext audio but no options should not download additional streams :return: """ master_url = "http://mocked/path/master.m3u8" expected = ['http://mocked/path/playlist.m3u8', 'http://mocked/path/es.m3u8'] with requests_mock.Mocker() as mock: mock.get(master_url, text=self.playlist) master_stream = self.run_streamlink(master_url, 'es') substreams = master_stream['video'].substreams result = [x.url for x in substreams] # Check result self.assertEqual(result, expected) def test_hls_ext_audio_all(self): """ m3u8 with ext audio but no options should not download additional streams :return: """ master_url = "http://mocked/path/master.m3u8" expected = ['http://mocked/path/playlist.m3u8', 'http://mocked/path/en.m3u8', 'http://mocked/path/es.m3u8'] with requests_mock.Mocker() as mock: mock.get(master_url, text=self.playlist) master_stream = self.run_streamlink(master_url, 'en,es') substreams = master_stream['video'].substreams result = [x.url for x in substreams] # Check result self.assertEqual(result, expected) def test_hls_ext_audio_wildcard(self): master_url = "http://mocked/path/master.m3u8" expected = ['http://mocked/path/playlist.m3u8', 'http://mocked/path/en.m3u8', 'http://mocked/path/es.m3u8'] with requests_mock.Mocker() as mock: mock.get(master_url, text=self.playlist) master_stream = self.run_streamlink(master_url, '*') substreams = master_stream['video'].substreams result = [x.url for x in substreams] # Check result self.assertEqual(result, expected) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/stream/test_hls_filtered.py0000644000175100001710000001634200000000000021745 0ustar00runnerdockerimport unittest from threading import Event from unittest.mock import MagicMock, call, patch from streamlink.stream.hls import HLSStream, HLSStreamReader from tests.mixins.stream_hls import EventedHLSStreamWriter, Playlist, Segment, TestMixinStreamHLS FILTERED = "filtered" class SegmentFiltered(Segment): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.title = FILTERED class _TestSubjectHLSReader(HLSStreamReader): __writer__ = EventedHLSStreamWriter class _TestSubjectHLSStream(HLSStream): __reader__ = _TestSubjectHLSReader @patch("streamlink.stream.hls.HLSStreamWorker.wait", MagicMock(return_value=True)) class TestFilteredHLSStream(TestMixinStreamHLS, unittest.TestCase): __stream__ = _TestSubjectHLSStream @classmethod def filter_sequence(cls, sequence): return sequence.segment.title == FILTERED def get_session(self, options=None, *args, **kwargs): session = super().get_session(options) session.set_option("hls-live-edge", 2) session.set_option("hls-timeout", 0) session.set_option("stream-timeout", 0) return session def subject(self, *args, **kwargs): thread, segments = super().subject(*args, **kwargs) return thread, thread.reader, thread.reader.writer, segments # don't patch should_filter_sequence here (it always returns False) def test_not_filtered(self): thread, reader, writer, segments = self.subject([ Playlist(0, [SegmentFiltered(0), SegmentFiltered(1)], end=True) ]) self.await_write(2) data = self.await_read() self.assertEqual(data, self.content(segments), "Does not filter by default") @patch("streamlink.stream.hls.HLSStreamWriter.should_filter_sequence", new=filter_sequence) @patch("streamlink.stream.hls.log") def test_filtered_logging(self, mock_log): thread, reader, writer, segments = self.subject([ Playlist(0, [SegmentFiltered(0), SegmentFiltered(1)]), Playlist(2, [Segment(2), Segment(3)]), Playlist(4, [SegmentFiltered(4), SegmentFiltered(5)]), Playlist(6, [Segment(6), Segment(7)], end=True) ]) data = b"" self.assertTrue(reader.filter_event.is_set(), "Doesn't let the reader wait if not filtering") for i in range(2): self.await_write(2) self.assertEqual(len(mock_log.info.mock_calls), i * 2 + 1) self.assertEqual(mock_log.info.mock_calls[i * 2 + 0], call("Filtering out segments and pausing stream output")) self.assertFalse(reader.filter_event.is_set(), "Lets the reader wait if filtering") self.await_write(2) self.assertEqual(len(mock_log.info.mock_calls), i * 2 + 2) self.assertEqual(mock_log.info.mock_calls[i * 2 + 1], call("Resuming stream output")) self.assertTrue(reader.filter_event.is_set(), "Doesn't let the reader wait if not filtering") data += self.await_read() self.assertEqual( data, self.content(segments, cond=lambda s: s.num % 4 > 1), "Correctly filters out segments" ) self.assertTrue(all([self.called(s) for s in segments.values()]), "Downloads all segments") @patch("streamlink.stream.hls.HLSStreamWriter.should_filter_sequence", new=filter_sequence) def test_filtered_timeout(self): thread, reader, writer, segments = self.subject([ Playlist(0, [Segment(0), Segment(1)], end=True) ]) self.await_write() data = self.await_read() self.assertEqual(data, segments[0].content, "Has read the first segment") # simulate a timeout by having an empty buffer # timeout value is set to 0 with self.assertRaises(IOError) as cm: self.await_read() self.assertEqual(str(cm.exception), "Read timeout", "Raises a timeout error when no data is available to read") @patch("streamlink.stream.hls.HLSStreamWriter.should_filter_sequence", new=filter_sequence) def test_filtered_no_timeout(self): thread, reader, writer, segments = self.subject([ Playlist(0, [SegmentFiltered(0), SegmentFiltered(1)]), Playlist(2, [Segment(2), Segment(3)], end=True) ]) self.assertTrue(reader.filter_event.is_set(), "Doesn't let the reader wait if not filtering") self.await_write(2) self.assertFalse(reader.filter_event.is_set(), "Lets the reader wait if filtering") # make reader read (no data available yet) thread.read_wait.set() # once data becomes available, the reader continues reading self.await_write() self.assertTrue(reader.filter_event.is_set(), "Reader is not waiting anymore") thread.read_done.wait() thread.read_done.clear() self.assertFalse(thread.error, "Doesn't time out when filtering") self.assertEqual(b"".join(thread.data), segments[2].content, "Reads next available buffer data") self.await_write() data = self.await_read() self.assertEqual(data, self.content(segments, cond=lambda s: s.num >= 2)) @patch("streamlink.stream.hls.HLSStreamWriter.should_filter_sequence", new=filter_sequence) def test_filtered_closed(self): thread, reader, writer, segments = self.subject(start=False, playlists=[ Playlist(0, [SegmentFiltered(0), SegmentFiltered(1)], end=True) ]) # mock the reader thread's filter_event.wait method, so that the main thread can wait on its call filter_event_wait_called = Event() orig_wait = reader.filter_event.wait def mocked_wait(*args, **kwargs): filter_event_wait_called.set() return orig_wait(*args, **kwargs) with patch.object(reader.filter_event, "wait", side_effect=mocked_wait): self.start() # write first filtered segment and trigger the filter_event's lock self.assertTrue(reader.filter_event.is_set(), "Doesn't let the reader wait if not filtering") self.await_write() self.assertFalse(reader.filter_event.is_set(), "Lets the reader wait if filtering") # make reader read (no data available yet) thread.read_wait.set() # before calling reader.close(), wait until reader thread's filter_event.wait was called if not filter_event_wait_called.wait(timeout=5): # pragma: no cover raise RuntimeError("Missing filter_event.wait() call") # close stream while reader is waiting for filtering to end thread.reader.close() thread.read_done.wait() thread.read_done.clear() self.assertEqual(thread.data, [b""], "Stops reading on stream close") self.assertFalse(thread.error, "Is not a read timeout on stream close") def test_hls_segment_ignore_names(self): thread, reader, writer, segments = self.subject([ Playlist(0, [Segment(0), Segment(1), Segment(2), Segment(3)], end=True) ], {"hls-segment-ignore-names": [ ".*", "segment0", "segment2", ]}) self.await_write(4) self.assertEqual(self.await_read(), self.content(segments, cond=lambda s: s.num % 2 > 0)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/stream/test_hls_playlist.py0000644000175100001710000002204300000000000022003 0ustar00runnerdockerimport unittest from datetime import datetime, timedelta # noinspection PyPackageRequirements from isodate import tzinfo from streamlink.stream.hls_playlist import DateRange, Media, Resolution, Segment, StreamInfo, load from tests.resources import text class TestHLSPlaylist(unittest.TestCase): def test_load(self): with text("hls/test_1.m3u8") as m3u8_fh: playlist = load(m3u8_fh.read(), "http://test.se/") self.assertEqual( playlist.media, [ Media(uri='http://test.se/audio/stereo/en/128kbit.m3u8', type='AUDIO', group_id='stereo', language='en', name='English', default=True, autoselect=True, forced=False, characteristics=None), Media(uri='http://test.se/audio/stereo/none/128kbit.m3u8', type='AUDIO', group_id='stereo', language='dubbing', name='Dubbing', default=False, autoselect=True, forced=False, characteristics=None), Media(uri='http://test.se/audio/surround/en/320kbit.m3u8', type='AUDIO', group_id='surround', language='en', name='English', default=True, autoselect=True, forced=False, characteristics=None), Media(uri='http://test.se/audio/stereo/none/128kbit.m3u8', type='AUDIO', group_id='surround', language='dubbing', name='Dubbing', default=False, autoselect=True, forced=False, characteristics=None), Media(uri='http://test.se/subtitles_de.m3u8', type='SUBTITLES', group_id='subs', language='de', name='Deutsch', default=False, autoselect=True, forced=False, characteristics=None), Media(uri='http://test.se/subtitles_en.m3u8', type='SUBTITLES', group_id='subs', language='en', name='English', default=True, autoselect=True, forced=False, characteristics=None), Media(uri='http://test.se/subtitles_es.m3u8', type='SUBTITLES', group_id='subs', language='es', name='Espanol', default=False, autoselect=True, forced=False, characteristics=None), Media(uri='http://test.se/subtitles_fr.m3u8', type='SUBTITLES', group_id='subs', language='fr', name='Français', default=False, autoselect=True, forced=False, characteristics=None) ] ) self.assertEqual( [p.stream_info for p in playlist.playlists], [ StreamInfo(bandwidth=260000, program_id='1', codecs=['avc1.4d400d', 'mp4a.40.2'], resolution=Resolution(width=422, height=180), audio='stereo', video=None, subtitles='subs'), StreamInfo(bandwidth=520000, program_id='1', codecs=['avc1.4d4015', 'mp4a.40.2'], resolution=Resolution(width=638, height=272), audio='stereo', video=None, subtitles='subs'), StreamInfo(bandwidth=830000, program_id='1', codecs=['avc1.4d4015', 'mp4a.40.2'], resolution=Resolution(width=638, height=272), audio='stereo', video=None, subtitles='subs'), StreamInfo(bandwidth=1100000, program_id='1', codecs=['avc1.4d401f', 'mp4a.40.2'], resolution=Resolution(width=958, height=408), audio='surround', video=None, subtitles='subs'), StreamInfo(bandwidth=1600000, program_id='1', codecs=['avc1.4d401f', 'mp4a.40.2'], resolution=Resolution(width=1277, height=554), audio='surround', video=None, subtitles='subs'), StreamInfo(bandwidth=4100000, program_id='1', codecs=['avc1.4d4028', 'mp4a.40.2'], resolution=Resolution(width=1921, height=818), audio='surround', video=None, subtitles='subs'), StreamInfo(bandwidth=6200000, program_id='1', codecs=['avc1.4d4028', 'mp4a.40.2'], resolution=Resolution(width=1921, height=818), audio='surround', video=None, subtitles='subs'), StreamInfo(bandwidth=10000000, program_id='1', codecs=['avc1.4d4033', 'mp4a.40.2'], resolution=Resolution(width=4096, height=1744), audio='surround', video=None, subtitles='subs') ] ) def test_parse_date(self): with text("hls/test_date.m3u8") as m3u8_fh: playlist = load(m3u8_fh.read(), "http://test.se/") start_date = datetime(year=2000, month=1, day=1, hour=0, minute=0, second=0, microsecond=0, tzinfo=tzinfo.UTC) end_date = datetime(year=2000, month=1, day=1, hour=0, minute=1, second=0, microsecond=0, tzinfo=tzinfo.UTC) delta_15 = timedelta(seconds=15) delta_30 = timedelta(seconds=30, milliseconds=500) delta_60 = timedelta(seconds=60) self.assertEqual(playlist.target_duration, 120) self.assertEqual( [daterange for daterange in playlist.dateranges], [ DateRange(id="start-invalid", start_date=None, classname=None, end_date=None, duration=None, planned_duration=None, end_on_next=False, x={}), DateRange(id="start-no-frac", start_date=start_date, classname=None, end_date=None, duration=None, planned_duration=None, end_on_next=False, x={}), DateRange(id="start-with-frac", start_date=start_date, classname=None, end_date=None, duration=None, planned_duration=None, end_on_next=False, x={}), DateRange(id="with-class", start_date=start_date, classname="bar", end_date=None, duration=None, planned_duration=None, end_on_next=False, x={}), DateRange(id="duration", start_date=start_date, duration=delta_30, classname=None, end_date=None, planned_duration=None, end_on_next=False, x={}), DateRange(id="planned-duration", start_date=start_date, planned_duration=delta_15, classname=None, end_date=None, duration=None, end_on_next=False, x={}), DateRange(id="duration-precedence", start_date=start_date, duration=delta_30, planned_duration=delta_15, classname=None, end_date=None, end_on_next=False, x={}), DateRange(id="end", start_date=start_date, end_date=end_date, classname=None, duration=None, planned_duration=None, end_on_next=False, x={}), DateRange(id="end-precedence", start_date=start_date, end_date=end_date, duration=delta_30, classname=None, planned_duration=None, end_on_next=False, x={}), DateRange(x={"X-CUSTOM": "value"}, id=None, start_date=None, end_date=None, duration=None, classname=None, planned_duration=None, end_on_next=False) ] ) self.assertEqual( [segment for segment in playlist.segments], [ Segment(uri="http://test.se/segment0-15.ts", duration=15.0, title="live", date=start_date, key=None, discontinuity=False, byterange=None, map=None), Segment(uri="http://test.se/segment15-30.5.ts", duration=15.5, title="live", date=start_date + delta_15, key=None, discontinuity=False, byterange=None, map=None), Segment(uri="http://test.se/segment30.5-60.ts", duration=29.5, title="live", date=start_date + delta_30, key=None, discontinuity=False, byterange=None, map=None), Segment(uri="http://test.se/segment60-.ts", duration=60.0, title="live", date=start_date + delta_60, key=None, discontinuity=False, byterange=None, map=None) ] ) self.assertEqual( [playlist.is_date_in_daterange(playlist.segments[0].date, daterange) for daterange in playlist.dateranges], [None, True, True, True, True, True, True, True, True, None] ) self.assertEqual( [playlist.is_date_in_daterange(playlist.segments[1].date, daterange) for daterange in playlist.dateranges], [None, True, True, True, True, False, True, True, True, None] ) self.assertEqual( [playlist.is_date_in_daterange(playlist.segments[2].date, daterange) for daterange in playlist.dateranges], [None, True, True, True, False, False, False, True, True, None] ) self.assertEqual( [playlist.is_date_in_daterange(playlist.segments[3].date, daterange) for daterange in playlist.dateranges], [None, True, True, True, False, False, False, False, False, None] ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/stream/test_stream_to_url.py0000644000175100001710000000400200000000000022146 0ustar00runnerdockerimport unittest from unittest.mock import PropertyMock, patch from streamlink import Streamlink from streamlink.plugins.filmon import FilmOnHLS from streamlink.stream.hls import HLSStream from streamlink.stream.http import HTTPStream from streamlink.stream.stream import Stream from streamlink_cli.utils import stream_to_url class TestStreamToURL(unittest.TestCase): def setUp(self): self.session = Streamlink() def test_base_stream(self): stream = Stream(self.session) self.assertEqual(None, stream_to_url(stream)) self.assertRaises(TypeError, stream.to_url) def test_http_stream(self): expected = "http://test.se/stream" stream = HTTPStream(self.session, expected, invalid_arg="invalid") self.assertEqual(expected, stream_to_url(stream)) self.assertEqual(expected, stream.to_url()) def test_hls_stream(self): expected = "http://test.se/stream.m3u8" stream = HLSStream(self.session, expected) self.assertEqual(expected, stream_to_url(stream)) self.assertEqual(expected, stream.to_url()) @patch("time.time") @patch("streamlink.plugins.filmon.FilmOnHLS.url", new_callable=PropertyMock) def test_filmon_stream(self, url, time): stream = FilmOnHLS(self.session, channel="test") url.return_value = "http://filmon.test.se/test.m3u8" stream.watch_timeout = 10 time.return_value = 1 expected = "http://filmon.test.se/test.m3u8" self.assertEqual(expected, stream_to_url(stream)) self.assertEqual(expected, stream.to_url()) @patch("time.time") @patch("streamlink.plugins.filmon.FilmOnHLS.url", new_callable=PropertyMock) def test_filmon_expired_stream(self, url, time): stream = FilmOnHLS(self.session, channel="test") url.return_value = "http://filmon.test.se/test.m3u8" stream.watch_timeout = 0 time.return_value = 1 self.assertEqual(None, stream_to_url(stream)) self.assertRaises(TypeError, stream.to_url) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/stream/test_stream_wrappers.py0000644000175100001710000000123000000000000022505 0ustar00runnerdockerimport unittest from streamlink.stream.wrappers import StreamIOIterWrapper class TestPluginStream(unittest.TestCase): def test_iter(self): def generator(): yield b"1" * 8192 yield b"2" * 4096 yield b"3" * 2048 fd = StreamIOIterWrapper(generator()) self.assertEqual(fd.read(4096), b"1" * 4096) self.assertEqual(fd.read(2048), b"1" * 2048) self.assertEqual(fd.read(2048), b"1" * 2048) self.assertEqual(fd.read(1), b"2") self.assertEqual(fd.read(4095), b"2" * 4095) self.assertEqual(fd.read(1536), b"3" * 1536) self.assertEqual(fd.read(), b"3" * 512) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/test_api_http_session.py0000644000175100001710000001063300000000000021356 0ustar00runnerdockerimport unittest from unittest.mock import PropertyMock, call, patch import pytest import requests from streamlink.exceptions import PluginError from streamlink.plugin.api.http_session import HTTPSession, urllib3_version from streamlink.plugin.api.useragents import FIREFOX @pytest.mark.skipif(urllib3_version < (1, 25, 4), reason="test only applicable on urllib3 >=1.25.4") class TestUrllib3Overrides: @pytest.fixture(scope="class") def httpsession(self) -> HTTPSession: return HTTPSession() @pytest.mark.parametrize("url,expected,assertion", [ ("https://foo/bar%3F?baz%21", "https://foo/bar%3F?baz%21", "Keeps encoded reserved characters"), ("https://foo/%62%61%72?%62%61%7A", "https://foo/bar?baz", "Decodes encoded unreserved characters"), ("https://foo/bär?bäz", "https://foo/b%C3%A4r?b%C3%A4z", "Encodes other characters"), ("https://foo/b%c3%a4r?b%c3%a4z", "https://foo/b%c3%a4r?b%c3%a4z", "Keeps percent-encodings with lowercase characters"), ("https://foo/b%C3%A4r?b%C3%A4z", "https://foo/b%C3%A4r?b%C3%A4z", "Keeps percent-encodings with uppercase characters"), ("https://foo/%?%", "https://foo/%25?%25", "Empty percent-encodings without valid encodings"), ("https://foo/%0?%0", "https://foo/%250?%250", "Incomplete percent-encodings without valid encodings"), ("https://foo/%zz?%zz", "https://foo/%25zz?%25zz", "Invalid percent-encodings without valid encodings"), ("https://foo/%3F%?%3F%", "https://foo/%253F%25?%253F%25", "Empty percent-encodings with valid encodings"), ("https://foo/%3F%0?%3F%0", "https://foo/%253F%250?%253F%250", "Incomplete percent-encodings with valid encodings"), ("https://foo/%3F%zz?%3F%zz", "https://foo/%253F%25zz?%253F%25zz", "Invalid percent-encodings with valid encodings"), ]) def test_encode_invalid_chars(self, httpsession: HTTPSession, url: str, expected: str, assertion: str): req = requests.Request(method="GET", url=url) prep = httpsession.prepare_request(req) assert prep.url == expected, assertion class TestPluginAPIHTTPSession(unittest.TestCase): def test_session_init(self): session = HTTPSession() self.assertEqual(session.headers.get("User-Agent"), FIREFOX) self.assertEqual(session.timeout, 20.0) self.assertIn("file://", session.adapters.keys()) @patch("streamlink.plugin.api.http_session.time.sleep") @patch("streamlink.plugin.api.http_session.Session.request", side_effect=requests.Timeout) def test_read_timeout(self, mock_request, mock_sleep): session = HTTPSession() with self.assertRaises(PluginError) as cm: session.get("http://localhost/", timeout=123, retries=3, retry_backoff=2, retry_max_backoff=5) self.assertTrue(str(cm.exception).startswith("Unable to open URL: http://localhost/")) self.assertEqual(mock_request.mock_calls, [ call("GET", "http://localhost/", headers={}, params={}, timeout=123, proxies={}, allow_redirects=True), call("GET", "http://localhost/", headers={}, params={}, timeout=123, proxies={}, allow_redirects=True), call("GET", "http://localhost/", headers={}, params={}, timeout=123, proxies={}, allow_redirects=True), call("GET", "http://localhost/", headers={}, params={}, timeout=123, proxies={}, allow_redirects=True), ]) self.assertEqual(mock_sleep.mock_calls, [ call(2), call(4), call(5) ]) def test_json_encoding(self): json_str = "{\"test\": \"Α and Ω\"}" # encode the json string with each encoding and assert that the correct one is detected for encoding in ["UTF-32BE", "UTF-32LE", "UTF-16BE", "UTF-16LE", "UTF-8"]: with patch('requests.Response.content', new_callable=PropertyMock) as mock_content: mock_content.return_value = json_str.encode(encoding) res = requests.Response() self.assertEqual(HTTPSession.json(res), {"test": "\u0391 and \u03a9"}) def test_json_encoding_override(self): json_text = "{\"test\": \"Α and Ω\"}".encode("cp949") with patch('requests.Response.content', new_callable=PropertyMock) as mock_content: mock_content.return_value = json_text res = requests.Response() res.encoding = "cp949" self.assertEqual(HTTPSession.json(res), {"test": "\u0391 and \u03a9"}) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/test_api_validate.py0000644000175100001710000002613600000000000020432 0ustar00runnerdockerimport re import unittest from lxml.etree import Element from streamlink.plugin.api.validate import ( all, any, attr, endswith, filter, get, getattr, hasattr, length, map, optional, parse_html, parse_json, parse_qsd, parse_xml, startswith, text, transform, union, union_get, url, validate, xml_element, xml_find, xml_findall, xml_findtext, xml_xpath, xml_xpath_string, ) class TestPluginAPIValidate(unittest.TestCase): def test_basic(self): assert validate(1, 1) == 1 assert validate(int, 1) == 1 assert validate(text, "abc") == "abc" assert validate(text, "日本語") == "日本語" assert validate(list, ["a", 1]) == ["a", 1] assert validate(dict, {"a": 1}) == {"a": 1} assert validate(lambda n: 0 < n < 5, 3) == 3 def test_all(self): assert validate(all(int, lambda n: 0 < n < 5), 3) == 3 assert validate(all(transform(int), lambda n: 0 < n < 5), 3.33) == 3 def test_any(self): assert validate(any(int, dict), 5) == 5 assert validate(any(int, dict), {}) == {} assert validate(any(int), 4) == 4 def test_transform(self): assert validate(transform(int), "1") == 1 assert validate(transform(str), 1) == "1" assert validate( transform( lambda value, *args, **kwargs: f"{value}{args}{kwargs}", *("b", "c"), **dict(d="d", e="e") ), "a" ) == "a('b', 'c'){'d': 'd', 'e': 'e'}" def no_args(): pass # pragma: no cover self.assertRaises(TypeError, validate, transform(no_args), "some value") def test_union(self): assert validate(union((get("foo"), get("bar"))), {"foo": "alpha", "bar": "beta"}) == ("alpha", "beta") def test_union_get(self): assert validate(union_get("foo", "bar"), {"foo": "alpha", "bar": "beta"}) == ("alpha", "beta") assert validate(union_get("foo", "bar", seq=list), {"foo": "alpha", "bar": "beta"}) == ["alpha", "beta"] assert validate(union_get(("foo", "bar"), ("baz", "qux")), {"foo": {"bar": "alpha"}, "baz": {"qux": "beta"}}) == ("alpha", "beta") def test_list(self): assert validate([1, 0], [1, 0, 1, 1]) == [1, 0, 1, 1] assert validate([1, 0], []) == [] assert validate(all([0, 1], lambda l: len(l) > 2), [0, 1, 0]) == [0, 1, 0] def test_list_tuple_set_frozenset(self): assert validate([int], [1, 2]) assert validate({int}, {1, 2}) == {1, 2} assert validate(tuple([int]), tuple([1, 2])) == tuple([1, 2]) def test_dict(self): assert validate({"key": 5}, {"key": 5}) == {"key": 5} assert validate({"key": int}, {"key": 5}) == {"key": 5} assert validate({"n": int, "f": float}, {"n": 5, "f": 3.14}) == {"n": 5, "f": 3.14} def test_dict_keys(self): assert validate({text: int}, {"a": 1, "b": 2}) == {"a": 1, "b": 2} assert validate({transform(text): transform(int)}, {1: 3.14, 3.14: 1}) == {"1": 3, "3.14": 1} def test_nested_dict_keys(self): assert validate({text: {text: int}}, {"a": {"b": 1, "c": 2}}) == {"a": {"b": 1, "c": 2}} def test_dict_optional_keys(self): assert validate({"a": 1, optional("b"): 2}, {"a": 1}) == {"a": 1} assert validate({"a": 1, optional("b"): 2}, {"a": 1, "b": 2}) == {"a": 1, "b": 2} def test_filter(self): assert validate(filter(lambda i: i > 5), [10, 5, 4, 6, 7]) == [10, 6, 7] def test_map(self): assert validate(map(lambda v: v[0]), [(1, 2), (3, 4)]) == [1, 3] def test_map_dict(self): assert validate(map(lambda k, v: (v, k)), {"foo": "bar"}) == {"bar": "foo"} def test_get(self): assert validate(get("key"), {"key": "value"}) == "value" assert validate(get("key"), re.match(r"(?P.+)", "value")) == "value" assert validate(get("invalidkey"), {"key": "value"}) is None assert validate(get("invalidkey", "default"), {"key": "value"}) == "default" assert validate(get(3, "default"), [0, 1, 2]) == "default" assert validate(get("attr"), Element("foo", {"attr": "value"})) == "value" with self.assertRaisesRegex(ValueError, "'NoneType' object is not subscriptable"): validate(get("key"), None) data = {"one": {"two": {"three": "value1"}}, ("one", "two", "three"): "value2"} assert validate(get(("one", "two", "three")), data) == "value1", "Recursive lookup" assert validate(get(("one", "two", "three"), strict=True), data) == "value2", "Strict tuple-key lookup" assert validate(get(("one", "two", "invalidkey")), data) is None, "Default value is None" assert validate(get(("one", "two", "invalidkey"), "default"), data) == "default", "Custom default value" with self.assertRaisesRegex(ValueError, "Object \"{'two': {'three': 'value1'}}\" does not have item \"invalidkey\""): validate(get(("one", "invalidkey", "three")), data) with self.assertRaisesRegex(ValueError, "'NoneType' object is not subscriptable"): validate(all(get("one"), get("invalidkey"), get("three")), data) def test_get_re(self): m = re.match(r"(\d+)p", "720p") assert validate(get(1), m) == "720" def test_getattr(self): el = Element("foo") assert validate(getattr("tag"), el) == "foo" assert validate(getattr("invalid", "default"), el) == "default" def test_hasattr(self): el = Element("foo") assert validate(hasattr("tag"), el) == el def test_length(self): assert validate(length(1), [1, 2, 3]) == [1, 2, 3] def invalid_length(): validate(length(2), [1]) self.assertRaises(ValueError, invalid_length) def test_xml_element(self): el = Element("tag") el.set("key", "value") el.text = "test" childA = Element("childA") childB = Element("childB") el.append(childA) el.append(childB) upper = transform(str.upper) newelem: Element = validate(xml_element(tag=upper, text=upper, attrib={upper: upper}), el) assert newelem.tag == "TAG" assert newelem.text == "TEST" assert newelem.attrib == {"KEY": "VALUE"} assert list(newelem.iterchildren()) == [childA, childB] with self.assertRaises(ValueError) as cm: validate(xml_element(tag="invalid"), el) assert str(cm.exception).startswith("Unable to validate XML tag: ") with self.assertRaises(ValueError) as cm: validate(xml_element(text="invalid"), el) assert str(cm.exception).startswith("Unable to validate XML text: ") with self.assertRaises(ValueError) as cm: validate(xml_element(attrib={"key": "invalid"}), el) assert str(cm.exception).startswith("Unable to validate XML attributes: ") def test_xml_find(self): el = Element("parent") el.append(Element("foo")) el.append(Element("bar")) assert validate(xml_find("bar"), el).tag == "bar" with self.assertRaises(ValueError) as cm: validate(xml_find("baz"), el) assert str(cm.exception) == "XPath 'baz' did not return an element" def test_xml_findtext(self): el = Element("foo") el.text = "bar" assert validate(xml_findtext("."), el) == "bar" def test_xml_findall(self): el = Element("parent") children = [Element("child") for i in range(10)] for child in children: el.append(child) assert validate(xml_findall("child"), el) == children def test_xml_xpath(self): root = Element("root") foo = Element("foo") bar = Element("bar") baz = Element("baz") root.append(foo) root.append(bar) foo.append(baz) assert validate(xml_xpath("./descendant-or-self::node()"), root) == [root, foo, baz, bar], "Returns correct node set" assert validate(xml_xpath("./child::qux"), root) is None, "Returns None when node set is empty" assert validate(xml_xpath("name(.)"), root) == "root", "Returns function values instead of node sets" self.assertRaises(ValueError, validate, xml_xpath("."), "not an Element") def test_xml_xpath_string(self): root = Element("root") foo = Element("foo") bar = Element("bar") root.set("attr", "") foo.set("attr", "FOO") bar.set("attr", "BAR") root.append(foo) root.append(bar) assert validate(xml_xpath_string("./baz"), root) is None, "Returns None if nothing was found" assert validate(xml_xpath_string("./@attr"), root) is None, "Returns None if string is empty" assert validate(xml_xpath_string("./foo/@attr"), root) == "FOO", "Returns the attr value of foo" assert validate(xml_xpath_string("./bar/@attr"), root) == "BAR", "Returns the attr value of bar" assert validate(xml_xpath_string("count(./*)"), root) == "2", "Wraps arbitrary functions" assert validate(xml_xpath_string("./*/@attr"), root) == "FOO", "Returns the first item of a set of nodes" def test_attr(self): el = Element("foo") el.text = "bar" assert validate(attr({"text": text}), el).text == "bar" def test_url(self): url_ = "https://google.se/path" assert validate(url(), url_) assert validate(url(scheme="http"), url_) assert validate(url(path="/path"), url_) def test_startswith(self): assert validate(startswith("abc"), "abcedf") def test_endswith(self): assert validate(endswith("åäö"), "xyzåäö") def test_parse_json(self): assert validate(parse_json(), '{"a": ["b", true, false, null, 1, 2.3]}') == {"a": ["b", True, False, None, 1, 2.3]} with self.assertRaises(ValueError) as cm: validate(parse_json(), "invalid") assert str(cm.exception) == "Unable to parse JSON: Expecting value: line 1 column 1 (char 0) ('invalid')" def test_parse_html(self): assert validate(parse_html(), '"perfectly"valid
HTML').tag == "html" with self.assertRaises(ValueError) as cm: validate(parse_html(), None) assert str(cm.exception) == "Unable to parse HTML: can only parse strings (None)" def test_parse_xml(self): assert validate(parse_xml(), '').tag == "root" with self.assertRaises(ValueError) as cm: validate(parse_xml(), None) assert str(cm.exception) == "Unable to parse XML: can only parse strings (None)" def test_parse_qsd(self): assert validate(parse_qsd(), 'foo=bar&foo=baz') == {"foo": "baz"} with self.assertRaises(ValueError) as cm: validate(parse_qsd(), 123) assert str(cm.exception) == "Unable to parse query string: 'int' object has no attribute 'decode' (123)" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/test_api_websocket.py0000644000175100001710000001576300000000000020633 0ustar00runnerdockerimport unittest from threading import Event from unittest.mock import Mock, call, patch from websocket import ABNF, STATUS_NORMAL from streamlink.logger import DEBUG, TRACE from streamlink.plugin.api.websocket import WebsocketClient from streamlink.session import Streamlink class TestWebsocketClient(unittest.TestCase): def setUp(self): self.session = Streamlink() def tearDown(self): self.session = None @patch("streamlink.plugin.api.websocket.enableTrace") def test_log(self, mock_enable_trace: Mock): with patch("streamlink.plugin.api.websocket.rootlogger", Mock(level=DEBUG)): WebsocketClient(self.session, "wss://localhost:0") self.assertFalse(mock_enable_trace.called) with patch("streamlink.plugin.api.websocket.rootlogger", Mock(level=TRACE)): WebsocketClient(self.session, "wss://localhost:0") self.assertTrue(mock_enable_trace.called) def test_user_agent(self): client = WebsocketClient(self.session, "wss://localhost:0") self.assertEqual(client.ws.header, [ f"User-Agent: {self.session.http.headers['User-Agent']}" ]) client = WebsocketClient(self.session, "wss://localhost:0", header=["User-Agent: foo"]) self.assertEqual(client.ws.header, [ "User-Agent: foo" ]) def test_args_and_proxy(self): self.session.set_option("http-proxy", "https://username:password@hostname:1234") client = WebsocketClient( self.session, "wss://localhost:0", subprotocols=["sub1", "sub2"], cookie="cookie", sockopt=("sockopt1", "sockopt2"), sslopt={"ssloptkey": "ssloptval"}, host="customhost", origin="customorigin", suppress_origin=True, ping_interval=30, ping_timeout=4, ping_payload="ping" ) self.assertEqual(client.ws.url, "wss://localhost:0") self.assertEqual(client.ws.subprotocols, ["sub1", "sub2"]) self.assertEqual(client.ws.cookie, "cookie") with patch.object(client.ws, "run_forever") as mock_ws_run_forever: client.start() client.join(1) self.assertFalse(client.is_alive()) self.assertEqual(mock_ws_run_forever.call_args_list, [ call( sockopt=("sockopt1", "sockopt2"), sslopt={"ssloptkey": "ssloptval"}, host="customhost", origin="customorigin", suppress_origin=True, ping_interval=30, ping_timeout=4, ping_payload="ping", proxy_type="https", http_proxy_host="hostname", http_proxy_port=1234, http_proxy_auth=("username", "password") ) ]) def test_handlers(self): client = WebsocketClient(self.session, "wss://localhost:0") self.assertEqual(client.ws.on_open, client.on_open) self.assertEqual(client.ws.on_error, client.on_error) self.assertEqual(client.ws.on_close, client.on_close) self.assertEqual(client.ws.on_ping, client.on_ping) self.assertEqual(client.ws.on_pong, client.on_pong) self.assertEqual(client.ws.on_message, client.on_message) self.assertEqual(client.ws.on_cont_message, client.on_cont_message) self.assertEqual(client.ws.on_data, client.on_data) def test_send(self): client = WebsocketClient(self.session, "wss://localhost:0") with patch.object(client, "ws") as mock_ws: client.send("foo") client.send(b"foo", ABNF.OPCODE_BINARY) client.send_json({"foo": "bar", "baz": "qux"}) self.assertEqual(mock_ws.send.call_args_list, [ call("foo", ABNF.OPCODE_TEXT), call(b"foo", ABNF.OPCODE_BINARY), call("{\"foo\":\"bar\",\"baz\":\"qux\"}", ABNF.OPCODE_TEXT), ]) def test_close(self): class WebsocketClientSubclass(WebsocketClient): running = Event() status = False def run(self): self.status = self.running.wait(4) client = WebsocketClientSubclass(self.session, "wss://localhost:0") with patch.object(client.ws, "close") as mock_ws_close: mock_ws_close.side_effect = lambda *_, **__: client.running.set() client.start() client.close(reason="foo") self.assertFalse(client.is_alive()) self.assertTrue(client.status) self.assertEqual(mock_ws_close.call_args_list, [ call( status=STATUS_NORMAL, reason=b"foo", timeout=3 ) ]) @patch("streamlink.plugin.api.websocket.WebSocketApp") def test_reconnect_disconnected(self, mock_wsapp: Mock): client = WebsocketClient(self.session, "wss://localhost:0") event_run_forever_entered = Event() # noinspection PyUnusedLocal def mock_run_forever(**data): client.ws.keep_running = False event_run_forever_entered.set() client.ws.keep_running = True client.ws.run_forever.side_effect = mock_run_forever client.start() self.assertTrue(event_run_forever_entered.wait(1), "Enters run_forever loop on ws client thread") self.assertEqual(mock_wsapp.call_count, 1) client.reconnect() self.assertEqual(mock_wsapp.call_count, 1, "Doesn't reconnect if disconnected") client.join() @patch("streamlink.plugin.api.websocket.WebSocketApp") def test_reconnect_once(self, mock_wsapp: Mock): client = WebsocketClient(self.session, "wss://localhost:0") run_forever_entered = Event() run_forever_ended = Event() # noinspection PyUnusedLocal def mock_run_forever(**data): run_forever_entered.set() run_forever_ended.wait(1) run_forever_ended.clear() client.ws.keep_running = True client.ws.run_forever.side_effect = mock_run_forever client.start() self.assertEqual(client.ws.close.call_count, 0) self.assertEqual(mock_wsapp.call_count, 1, "Creates initial connection") self.assertFalse(client._reconnect, "Has not set the _reconnect state") self.assertTrue(run_forever_entered.wait(1), "Enters run_forever loop on client thread") run_forever_entered.clear() client.reconnect() self.assertEqual(client.ws.close.call_count, 1) self.assertEqual(mock_wsapp.call_count, 2, "Creates new connection") self.assertTrue(client._reconnect, "Has set the _reconnect state") run_forever_ended.set() self.assertTrue(run_forever_entered.wait(1), "Enters run_forever loop on client thread again") self.assertFalse(client._reconnect, "Has reset the _reconnect state") run_forever_ended.set() client.join(1) self.assertFalse(client.is_alive()) self.assertEqual(mock_wsapp.call_count, 2, "Connection has ended regularly") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/test_buffer.py0000644000175100001710000000706600000000000017262 0ustar00runnerdockerimport unittest from streamlink.buffers import Buffer, RingBuffer class TestBuffer(unittest.TestCase): def setUp(self): self.buffer = Buffer() def test_write(self): self.buffer.write(b"1" * 8192) self.buffer.write(b"2" * 4096) self.assertEqual(self.buffer.length, 8192 + 4096) def test_read(self): self.buffer.write(b"1" * 8192) self.buffer.write(b"2" * 4096) self.assertEqual(self.buffer.length, 8192 + 4096) self.assertEqual(self.buffer.read(4096), b"1" * 4096) self.assertEqual(self.buffer.read(4096), b"1" * 4096) self.assertEqual(self.buffer.read(), b"2" * 4096) self.assertEqual(self.buffer.read(4096), b"") self.assertEqual(self.buffer.read(), b"") self.assertEqual(self.buffer.length, 0) def test_readwrite(self): self.buffer.write(b"1" * 8192) self.assertEqual(self.buffer.length, 8192) self.assertEqual(self.buffer.read(4096), b"1" * 4096) self.assertEqual(self.buffer.length, 4096) self.buffer.write(b"2" * 4096) self.assertEqual(self.buffer.length, 8192) self.assertEqual(self.buffer.read(1), b"1") self.assertEqual(self.buffer.read(4095), b"1" * 4095) self.assertEqual(self.buffer.read(8192), b"2" * 4096) self.assertEqual(self.buffer.read(8192), b"") self.assertEqual(self.buffer.read(), b"") self.assertEqual(self.buffer.length, 0) def test_close(self): self.buffer.write(b"1" * 8192) self.assertEqual(self.buffer.length, 8192) self.buffer.close() self.buffer.write(b"2" * 8192) self.assertEqual(self.buffer.length, 8192) def test_reuse_input(self): """Objects should be reusable after write()""" original = b"original" tests = [bytearray(original), memoryview(bytearray(original))] for data in tests: self.buffer.write(data) data[:] = b"reused!!" self.assertEqual(self.buffer.read(), original) def test_read_empty(self): self.assertRaises( StopIteration, lambda: next(self.buffer._iterate_chunks(10))) class TestRingBuffer(unittest.TestCase): BUFFER_SIZE = 8192 * 4 def setUp(self): self.buffer = RingBuffer(size=self.BUFFER_SIZE) def test_write(self): self.buffer.write(b"1" * 8192) self.buffer.write(b"2" * 4096) self.assertEqual(self.buffer.length, 8192 + 4096) def test_read(self): self.buffer.write(b"1" * 8192) self.buffer.write(b"2" * 4096) self.assertEqual(self.buffer.length, 8192 + 4096) self.assertEqual(self.buffer.read(4096), b"1" * 4096) self.assertEqual(self.buffer.read(4096), b"1" * 4096) self.assertEqual(self.buffer.read(), b"2" * 4096) self.assertEqual(self.buffer.length, 0) def test_read_timeout(self): self.assertRaises( IOError, self.buffer.read, timeout=0.1) def test_write_after_close(self): self.buffer.close() self.buffer.write(b"1" * 8192) self.assertEqual(self.buffer.length, 0) self.assertTrue(self.buffer.closed) def test_resize(self): self.assertEqual(self.buffer.buffer_size, self.BUFFER_SIZE) self.buffer.resize(self.BUFFER_SIZE * 2) self.assertEqual(self.buffer.buffer_size, self.BUFFER_SIZE * 2) def test_free(self): self.assertEqual(self.buffer.free, self.BUFFER_SIZE) self.buffer.write(b'1' * 100) self.assertEqual(self.buffer.free, self.BUFFER_SIZE - 100) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/test_cache.py0000644000175100001710000000754600000000000017057 0ustar00runnerdockerimport datetime import os.path import tempfile import unittest from shutil import rmtree from unittest.mock import patch import streamlink.cache class TestCache(unittest.TestCase): def setUp(self): self.tmp_dir = tempfile.mkdtemp("streamlink-test") streamlink.cache.cache_dir = self.tmp_dir self.cache = streamlink.cache.Cache("cache.json") def tearDown(self): rmtree(self.tmp_dir) def test_get_no_file(self): self.assertEqual(self.cache.get("missing-value"), None) self.assertEqual(self.cache.get("missing-value", default="default"), "default") def test_put_get(self): self.cache.set("value", 1) self.assertEqual(self.cache.get("value"), 1) def test_put_get_prefix(self): self.cache.key_prefix = "test" self.cache.set("value", 1) self.assertEqual(self.cache.get("value"), 1) def test_key_prefix(self): self.cache.key_prefix = "test" self.cache.set("value", 1) self.assertTrue("test:value" in self.cache._cache) self.assertEqual(1, self.cache._cache["test:value"]["value"]) @patch('os.path.exists', return_value=True) def test_load_fail(self, exists_mock): patch('streamlink.cache.open', side_effect=IOError) self.cache._load() self.assertEqual({}, self.cache._cache) def test_expired(self): self.cache.set("value", 10, expires=-20) self.assertEqual(None, self.cache.get("value")) def test_expired_at_before(self): self.cache.set("value", 10, expires_at=datetime.datetime.now() - datetime.timedelta(seconds=20)) self.assertEqual(None, self.cache.get("value")) def test_expired_at_after(self): self.cache.set("value", 10, expires_at=datetime.datetime.now() + datetime.timedelta(seconds=20)) self.assertEqual(10, self.cache.get("value")) @patch("streamlink.cache.mktime", side_effect=OverflowError) def test_expired_at_raise_overflowerror(self, mock): self.cache.set("value", 10, expires_at=datetime.datetime.now()) self.assertEqual(None, self.cache.get("value")) def test_not_expired(self): self.cache.set("value", 10, expires=20) self.assertEqual(10, self.cache.get("value")) def test_create_directory(self): try: streamlink.cache.cache_dir = os.path.join(tempfile.gettempdir(), "streamlink-test") cache = streamlink.cache.Cache("cache.json") self.assertFalse(os.path.exists(cache.filename)) cache.set("value", 10) self.assertTrue(os.path.exists(cache.filename)) finally: rmtree(streamlink.cache.cache_dir, ignore_errors=True) @patch('os.makedirs', side_effect=OSError) def test_create_directory_fail(self, makedirs): try: streamlink.cache.cache_dir = os.path.join(tempfile.gettempdir(), "streamlink-test") cache = streamlink.cache.Cache("cache.json") self.assertFalse(os.path.exists(cache.filename)) cache.set("value", 10) self.assertFalse(os.path.exists(cache.filename)) finally: rmtree(streamlink.cache.cache_dir, ignore_errors=True) def test_get_all(self): self.cache.set("test1", 1) self.cache.set("test2", 2) self.assertDictEqual( {"test1": 1, "test2": 2}, self.cache.get_all()) def test_get_all_prefix(self): self.cache.set("test1", 1) self.cache.set("test2", 2) self.cache.key_prefix = "test" self.cache.set("test3", 3) self.cache.set("test4", 4) self.assertDictEqual( {"test3": 3, "test4": 4}, self.cache.get_all()) def test_get_all_prune(self): self.cache.set("test1", 1) self.cache.set("test2", 2, -1) self.assertDictEqual( {"test1": 1}, self.cache.get_all()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/test_cli_main.py0000644000175100001710000011034100000000000017553 0ustar00runnerdockerimport datetime import logging import os import sys import unittest from pathlib import Path, PosixPath, WindowsPath from textwrap import dedent from unittest.mock import Mock, call, patch import freezegun import streamlink_cli.main import tests.resources from streamlink.exceptions import StreamError from streamlink.session import Streamlink from streamlink.stream.stream import Stream from streamlink_cli.compat import DeprecatedPath, is_win32, stdout from streamlink_cli.main import ( Formatter, NoPluginError, check_file_output, create_output, format_valid_streams, handle_stream, handle_url, output_stream, resolve_stream_name, setup_config_args ) from streamlink_cli.output import FileOutput, PlayerOutput from tests.plugin.testplugin import TestPlugin as _TestPlugin class FakePlugin(_TestPlugin): module = "fake" arguments = [] _streams = {} def streams(self, *args, **kwargs): return self._streams def _get_streams(self): # pragma: no cover pass class TestCLIMain(unittest.TestCase): def test_resolve_stream_name(self): a = Mock() b = Mock() c = Mock() d = Mock() e = Mock() streams = { "160p": a, "360p": b, "480p": c, "720p": d, "1080p": e, "worst": b, "best": d, "worst-unfiltered": a, "best-unfiltered": e } self.assertEqual(resolve_stream_name(streams, "unknown"), "unknown") self.assertEqual(resolve_stream_name(streams, "160p"), "160p") self.assertEqual(resolve_stream_name(streams, "360p"), "360p") self.assertEqual(resolve_stream_name(streams, "480p"), "480p") self.assertEqual(resolve_stream_name(streams, "720p"), "720p") self.assertEqual(resolve_stream_name(streams, "1080p"), "1080p") self.assertEqual(resolve_stream_name(streams, "worst"), "360p") self.assertEqual(resolve_stream_name(streams, "best"), "720p") self.assertEqual(resolve_stream_name(streams, "worst-unfiltered"), "160p") self.assertEqual(resolve_stream_name(streams, "best-unfiltered"), "1080p") def test_format_valid_streams(self): a = Mock() b = Mock() c = Mock() streams = { "audio": a, "720p": b, "1080p": c, "worst": b, "best": c } self.assertEqual( format_valid_streams(_TestPlugin, streams), ", ".join([ "audio", "720p (worst)", "1080p (best)" ]) ) streams = { "audio": a, "720p": b, "1080p": c, "worst-unfiltered": b, "best-unfiltered": c } self.assertEqual( format_valid_streams(_TestPlugin, streams), ", ".join([ "audio", "720p (worst-unfiltered)", "1080p (best-unfiltered)" ]) ) class TestCLIMainJsonAndStreamUrl(unittest.TestCase): @patch("streamlink_cli.main.args", json=True, stream_url=True, subprocess_cmdline=False) @patch("streamlink_cli.main.console") def test_handle_stream_with_json_and_stream_url(self, console, args): stream = Mock() streams = dict(best=stream) plugin = FakePlugin("") plugin._streams = streams handle_stream(plugin, streams, "best") self.assertEqual(console.msg.mock_calls, []) self.assertEqual(console.msg_json.mock_calls, [call( stream, metadata=dict( id="test-id-1234-5678", author="Tѥst Āuƭhǿr", category=None, title="Test Title" ) )]) self.assertEqual(console.error.mock_calls, []) console.msg_json.mock_calls.clear() args.json = False handle_stream(plugin, streams, "best") self.assertEqual(console.msg.mock_calls, [call(stream.to_url())]) self.assertEqual(console.msg_json.mock_calls, []) self.assertEqual(console.error.mock_calls, []) console.msg.mock_calls.clear() stream.to_url.side_effect = TypeError() handle_stream(plugin, streams, "best") self.assertEqual(console.msg.mock_calls, []) self.assertEqual(console.msg_json.mock_calls, []) self.assertEqual(console.exit.mock_calls, [call("The stream specified cannot be translated to a URL")]) @patch("streamlink_cli.main.args", json=True, stream_url=True, stream=[], default_stream=[], retry_max=0, retry_streams=0) @patch("streamlink_cli.main.console") def test_handle_url_with_json_and_stream_url(self, console, args): stream = Mock() streams = dict(worst=Mock(), best=stream) class _FakePlugin(FakePlugin): _streams = streams with patch("streamlink_cli.main.streamlink", resolve_url=Mock(return_value=(_FakePlugin, ""))): handle_url() self.assertEqual(console.msg.mock_calls, []) self.assertEqual(console.msg_json.mock_calls, [call( plugin="fake", metadata=dict( id="test-id-1234-5678", author="Tѥst Āuƭhǿr", category=None, title="Test Title" ), streams=streams )]) self.assertEqual(console.error.mock_calls, []) console.msg_json.mock_calls.clear() args.json = False handle_url() self.assertEqual(console.msg.mock_calls, [call(stream.to_manifest_url())]) self.assertEqual(console.msg_json.mock_calls, []) self.assertEqual(console.error.mock_calls, []) console.msg.mock_calls.clear() stream.to_manifest_url.side_effect = TypeError() handle_url() self.assertEqual(console.msg.mock_calls, []) self.assertEqual(console.msg_json.mock_calls, []) self.assertEqual(console.exit.mock_calls, [call("The stream specified cannot be translated to a URL")]) console.exit.mock_calls.clear() class TestCLIMainCheckFileOutput(unittest.TestCase): @staticmethod def mock_path(path, is_file=True): return Mock( spec=Path(path), is_file=Mock(return_value=is_file), __str__=Mock(return_value=path) ) def test_check_file_output(self): path = self.mock_path("foo", is_file=False) output = check_file_output(path, False) self.assertIsInstance(output, FileOutput) self.assertIs(output.filename, path) def test_check_file_output_exists_force(self): path = self.mock_path("foo", is_file=True) output = check_file_output(path, True) self.assertIsInstance(output, FileOutput) self.assertIs(output.filename, path) @patch("streamlink_cli.main.console") @patch("streamlink_cli.main.sys") def test_check_file_output_exists_ask_yes(self, mock_sys: Mock, mock_console: Mock): mock_sys.stdin.isatty.return_value = True mock_console.ask = Mock(return_value="y") path = self.mock_path("foo", is_file=True) output = check_file_output(path, False) self.assertEqual(mock_console.ask.call_args_list, [call("File foo already exists! Overwrite it? [y/N] ")]) self.assertIsInstance(output, FileOutput) self.assertIs(output.filename, path) @patch("streamlink_cli.main.console") @patch("streamlink_cli.main.sys") def test_check_file_output_exists_ask_no(self, mock_sys: Mock, mock_console: Mock): mock_sys.stdin.isatty.return_value = True mock_sys.exit.side_effect = SystemExit mock_console.ask = Mock(return_value="N") path = self.mock_path("foo", is_file=True) with self.assertRaises(SystemExit): check_file_output(path, False) self.assertEqual(mock_console.ask.call_args_list, [call("File foo already exists! Overwrite it? [y/N] ")]) @patch("streamlink_cli.main.console") @patch("streamlink_cli.main.sys") def test_check_file_output_exists_notty(self, mock_sys: Mock, mock_console: Mock): mock_sys.stdin.isatty.return_value = False mock_sys.exit.side_effect = SystemExit path = self.mock_path("foo", is_file=True) with self.assertRaises(SystemExit): check_file_output(path, False) self.assertEqual(mock_console.ask.call_args_list, []) class TestCLIMainCreateOutput(unittest.TestCase): @patch("streamlink_cli.main.args") @patch("streamlink_cli.main.console", Mock()) @patch("streamlink_cli.main.DEFAULT_STREAM_METADATA", {"title": "bar"}) def test_create_output_no_file_output_options(self, args: Mock): formatter = Formatter({ "author": lambda: "foo" }) args.output = None args.stdout = None args.record = None args.record_and_pipe = None args.player_fifo = False args.title = None args.url = "URL" args.player = "mpv" args.player_args = "" output = create_output(formatter) self.assertIsInstance(output, PlayerOutput) self.assertEqual(output.title, "URL") args.title = "{author} - {title}" output = create_output(formatter) self.assertIsInstance(output, PlayerOutput) self.assertEqual(output.title, "foo - bar") @patch("streamlink_cli.main.args") @patch("streamlink_cli.main.check_file_output") def test_create_output_file_output(self, mock_check_file_output: Mock, args: Mock): formatter = Formatter({}) mock_check_file_output.side_effect = lambda path, force: FileOutput(path) args.output = "foo" args.stdout = None args.record = None args.record_and_pipe = None args.force = False args.fs_safe_rules = None output = create_output(formatter) self.assertEqual(mock_check_file_output.call_args_list, [call(Path("foo"), False)]) self.assertIsInstance(output, FileOutput) self.assertEqual(output.filename, Path("foo")) self.assertIsNone(output.fd) self.assertIsNone(output.record) @patch("streamlink_cli.main.args") def test_create_output_stdout(self, args: Mock): formatter = Formatter({}) args.output = None args.stdout = True args.record = None args.record_and_pipe = None output = create_output(formatter) self.assertIsInstance(output, FileOutput) self.assertIsNone(output.filename) self.assertIs(output.fd, stdout) self.assertIsNone(output.record) args.output = "-" args.stdout = False output = create_output(formatter) self.assertIsInstance(output, FileOutput) self.assertIsNone(output.filename) self.assertIs(output.fd, stdout) self.assertIsNone(output.record) @patch("streamlink_cli.main.args") @patch("streamlink_cli.main.check_file_output") def test_create_output_record_and_pipe(self, mock_check_file_output: Mock, args: Mock): formatter = Formatter({}) mock_check_file_output.side_effect = lambda path, force: FileOutput(path) args.output = None args.stdout = None args.record_and_pipe = "foo" args.force = False args.fs_safe_rules = None output = create_output(formatter) self.assertEqual(mock_check_file_output.call_args_list, [call(Path("foo"), False)]) self.assertIsInstance(output, FileOutput) self.assertIsNone(output.filename) self.assertIs(output.fd, stdout) self.assertIsInstance(output.record, FileOutput) self.assertEqual(output.record.filename, Path("foo")) self.assertIsNone(output.record.fd) self.assertIsNone(output.record.record) @patch("streamlink_cli.main.args") @patch("streamlink_cli.main.check_file_output") @patch("streamlink_cli.main.DEFAULT_STREAM_METADATA", {"title": "bar"}) def test_create_output_record(self, mock_check_file_output: Mock, args: Mock): formatter = Formatter({ "author": lambda: "foo" }) mock_check_file_output.side_effect = lambda path, force: FileOutput(path) args.output = None args.stdout = None args.record = "foo" args.record_and_pipe = None args.force = False args.fs_safe_rules = None args.title = None args.url = "URL" args.player = "mpv" args.player_args = "" args.player_fifo = None args.player_http = None output = create_output(formatter) self.assertIsInstance(output, PlayerOutput) self.assertEqual(output.title, "URL") self.assertIsInstance(output.record, FileOutput) self.assertEqual(output.record.filename, Path("foo")) self.assertIsNone(output.record.fd) self.assertIsNone(output.record.record) args.title = "{author} - {title}" output = create_output(formatter) self.assertIsInstance(output, PlayerOutput) self.assertEqual(output.title, "foo - bar") self.assertIsInstance(output.record, FileOutput) self.assertEqual(output.record.filename, Path("foo")) self.assertIsNone(output.record.fd) self.assertIsNone(output.record.record) @patch("streamlink_cli.main.args") @patch("streamlink_cli.main.console") def test_create_output_record_and_other_file_output(self, console: Mock, args: Mock): formatter = Formatter({}) args.output = None args.stdout = True args.record_and_pipe = True create_output(formatter) console.exit.assert_called_with("Cannot use record options with other file output options.") @patch("streamlink_cli.main.args") @patch("streamlink_cli.main.console") def test_create_output_no_default_player(self, console: Mock, args: Mock): formatter = Formatter({}) args.output = None args.stdout = False args.record_and_pipe = False args.player = None console.exit.side_effect = SystemExit with self.assertRaises(SystemExit): create_output(formatter) self.assertRegex( console.exit.call_args_list[0][0][0], r"^The default player \(\w+\) does not seem to be installed\." ) class TestCLIMainHandleStream(unittest.TestCase): @patch("streamlink_cli.main.output_stream") @patch("streamlink_cli.main.args") def test_handle_stream_output_stream(self, args: Mock, mock_output_stream: Mock): """ Test that the formatter does define the correct variables """ args.json = False args.subprocess_cmdline = False args.stream_url = False args.output = False args.stdout = False args.url = "URL" args.player_passthrough = [] args.player_external_http = False args.player_continuous_http = False mock_output_stream.return_value = True plugin = _TestPlugin("") plugin.author = "AUTHOR" plugin.category = "CATEGORY" plugin.title = "TITLE" stream = Stream(session=Mock()) streams = {"best": stream} handle_stream(plugin, streams, "best") self.assertEqual(mock_output_stream.call_count, 1) paramStream, paramFormatter = mock_output_stream.call_args[0] self.assertIs(paramStream, stream) self.assertIsInstance(paramFormatter, Formatter) self.assertEqual( paramFormatter.title("{url} - {author} - {category}/{game} - {title}"), "URL - AUTHOR - CATEGORY/CATEGORY - TITLE" ) class TestCLIMainOutputStream(unittest.TestCase): @patch("streamlink_cli.main.args", Mock(retry_open=2)) @patch("streamlink_cli.main.log") @patch("streamlink_cli.main.console") def test_stream_failure_no_output_open(self, mock_console: Mock, mock_log: Mock): output = Mock() stream = Mock( __str__=lambda _: "fake-stream", open=Mock(side_effect=StreamError("failure")) ) formatter = Formatter({}) with patch("streamlink_cli.main.output", Mock()), \ patch("streamlink_cli.main.create_output", return_value=output): output_stream(stream, formatter) self.assertEqual(mock_log.error.call_args_list, [ call("Try 1/2: Could not open stream fake-stream (Could not open stream: failure)"), call("Try 2/2: Could not open stream fake-stream (Could not open stream: failure)"), ]) self.assertEqual(mock_console.exit.call_args_list, [ call("Could not open stream fake-stream, tried 2 times, exiting") ]) self.assertFalse(output.open.called, "Does not open the output on stream error") @patch("streamlink_cli.main.log") class TestCLIMainSetupConfigArgs(unittest.TestCase): configdir = Path(tests.resources.__path__[0], "cli", "config") parser = Mock() @classmethod def subject(cls, config_files, **args): def resolve_url(name): if name == "noplugin": raise NoPluginError() return Mock(module="testplugin"), name session = Mock() session.resolve_url.side_effect = resolve_url args.setdefault("url", "testplugin") with patch("streamlink_cli.main.setup_args") as mock_setup_args, \ patch("streamlink_cli.main.args", **args), \ patch("streamlink_cli.main.streamlink", session), \ patch("streamlink_cli.main.CONFIG_FILES", config_files): setup_config_args(cls.parser) return mock_setup_args def test_no_plugin(self, mock_log): mock_setup_args = self.subject( [self.configdir / "primary", DeprecatedPath(self.configdir / "secondary")], config=None, url="noplugin" ) expected = [self.configdir / "primary"] mock_setup_args.assert_called_once_with(self.parser, expected, ignore_unknown=False) self.assertEqual(mock_log.info.mock_calls, []) def test_default_primary(self, mock_log): mock_setup_args = self.subject( [self.configdir / "primary", DeprecatedPath(self.configdir / "secondary")], config=None ) expected = [self.configdir / "primary", self.configdir / "primary.testplugin"] mock_setup_args.assert_called_once_with(self.parser, expected, ignore_unknown=False) self.assertEqual(mock_log.info.mock_calls, []) def test_default_secondary_deprecated(self, mock_log): mock_setup_args = self.subject( [self.configdir / "non-existent", DeprecatedPath(self.configdir / "secondary")], config=None ) expected = [self.configdir / "secondary", self.configdir / "secondary.testplugin"] mock_setup_args.assert_called_once_with(self.parser, expected, ignore_unknown=False) self.assertEqual(mock_log.info.mock_calls, [ call(f"Loaded config from deprecated path, see CLI docs for how to migrate: {expected[0]}"), call(f"Loaded plugin config from deprecated path, see CLI docs for how to migrate: {expected[1]}") ]) def test_custom_with_primary_plugin(self, mock_log): mock_setup_args = self.subject( [self.configdir / "primary", DeprecatedPath(self.configdir / "secondary")], config=[str(self.configdir / "custom")] ) expected = [self.configdir / "custom", self.configdir / "primary.testplugin"] mock_setup_args.assert_called_once_with(self.parser, expected, ignore_unknown=False) self.assertEqual(mock_log.info.mock_calls, []) def test_custom_with_deprecated_plugin(self, mock_log): mock_setup_args = self.subject( [self.configdir / "non-existent", DeprecatedPath(self.configdir / "secondary")], config=[str(self.configdir / "custom")] ) expected = [self.configdir / "custom", DeprecatedPath(self.configdir / "secondary.testplugin")] mock_setup_args.assert_called_once_with(self.parser, expected, ignore_unknown=False) self.assertEqual(mock_log.info.mock_calls, [ call(f"Loaded plugin config from deprecated path, see CLI docs for how to migrate: {expected[1]}") ]) def test_custom_multiple(self, mock_log): mock_setup_args = self.subject( [self.configdir / "primary", DeprecatedPath(self.configdir / "secondary")], config=[str(self.configdir / "non-existent"), str(self.configdir / "primary"), str(self.configdir / "secondary")] ) expected = [self.configdir / "secondary", self.configdir / "primary", self.configdir / "primary.testplugin"] mock_setup_args.assert_called_once_with(self.parser, expected, ignore_unknown=False) self.assertEqual(mock_log.info.mock_calls, []) class _TestCLIMainLogging(unittest.TestCase): @classmethod def subject(cls, argv, **kwargs): session = Streamlink() session.load_plugins(os.path.join(os.path.dirname(__file__), "plugin")) # stop test execution at the setup_signals() call, as we're not interested in what comes afterwards class StopTest(Exception): pass with patch("streamlink_cli.main.streamlink", session), \ patch("streamlink_cli.main.setup_signals", side_effect=StopTest), \ patch("streamlink_cli.main.CONFIG_FILES", []), \ patch("streamlink_cli.main.setup_streamlink"), \ patch("streamlink_cli.main.setup_plugins"), \ patch("streamlink_cli.main.setup_http_session"), \ patch("streamlink.session.Streamlink.load_builtin_plugins"), \ patch("sys.argv") as mock_argv: mock_argv.__getitem__.side_effect = lambda x: argv[x] try: streamlink_cli.main.main() except StopTest: pass def tearDown(self): streamlink_cli.main.logger.root.handlers.clear() # python >=3.7.2: https://bugs.python.org/issue35046 _write_call_log_cli_info = ( [call("[cli][info] foo\n")] if sys.version_info >= (3, 7, 2) else [call("[cli][info] foo"), call("\n")] ) _write_call_console_msg = [call("bar\n")] _write_call_console_msg_error = [call("error: bar\n")] _write_call_console_msg_json = [call("{\n \"error\": \"bar\"\n}\n")] _write_calls = _write_call_log_cli_info + _write_call_console_msg def write_file_and_assert(self, mock_mkdir: Mock, mock_write: Mock, mock_stdout: Mock): streamlink_cli.main.log.info("foo") streamlink_cli.main.console.msg("bar") self.assertEqual(mock_mkdir.mock_calls, [call(parents=True, exist_ok=True)]) self.assertEqual(mock_write.mock_calls, self._write_calls) self.assertFalse(mock_stdout.write.called) class TestCLIMainLoggingStreams(_TestCLIMainLogging): # python >=3.7.2: https://bugs.python.org/issue35046 _write_call_log_testcli_err = ( [call("[test_cli_main][error] baz\n")] if sys.version_info >= (3, 7, 2) else [call("[test_cli_main][error] baz"), call("\n")] ) def subject(self, argv, stream=None): super().subject(argv) childlogger = logging.getLogger("streamlink.test_cli_main") with self.assertRaises(SystemExit): streamlink_cli.main.log.info("foo") childlogger.error("baz") streamlink_cli.main.console.exit("bar") self.assertIs(streamlink_cli.main.log.parent.handlers[0].stream, stream) self.assertIs(childlogger.parent.handlers[0].stream, stream) self.assertIs(streamlink_cli.main.console.output, stream) @patch("sys.stderr") @patch("sys.stdout") def test_no_pipe_no_json(self, mock_stdout: Mock, mock_stderr: Mock): self.subject(["streamlink"], mock_stdout) self.assertEqual(mock_stdout.write.mock_calls, self._write_call_log_cli_info + self._write_call_log_testcli_err + self._write_call_console_msg_error) self.assertEqual(mock_stderr.write.mock_calls, []) @patch("sys.stderr") @patch("sys.stdout") def test_no_pipe_json(self, mock_stdout: Mock, mock_stderr: Mock): self.subject(["streamlink", "--json"], mock_stdout) self.assertEqual(mock_stdout.write.mock_calls, self._write_call_console_msg_json) self.assertEqual(mock_stderr.write.mock_calls, []) @patch("sys.stderr") @patch("sys.stdout") def test_pipe_no_json(self, mock_stdout: Mock, mock_stderr: Mock): self.subject(["streamlink", "--stdout"], mock_stderr) self.assertEqual(mock_stdout.write.mock_calls, []) self.assertEqual(mock_stderr.write.mock_calls, self._write_call_log_cli_info + self._write_call_log_testcli_err + self._write_call_console_msg_error) @patch("sys.stderr") @patch("sys.stdout") def test_pipe_json(self, mock_stdout: Mock, mock_stderr: Mock): self.subject(["streamlink", "--stdout", "--json"], mock_stderr) self.assertEqual(mock_stdout.write.mock_calls, []) self.assertEqual(mock_stderr.write.mock_calls, self._write_call_console_msg_json) class TestCLIMainLoggingInfos(_TestCLIMainLogging): @unittest.skipIf(is_win32, "test only applicable on a POSIX OS") @patch("streamlink_cli.main.log") @patch("streamlink_cli.main.os.geteuid", Mock(return_value=0)) def test_log_root_warning(self, mock_log): self.subject(["streamlink"]) self.assertEqual(mock_log.info.mock_calls, [call("streamlink is running as root! Be careful!")]) @patch("streamlink_cli.main.log") @patch("streamlink_cli.main.streamlink_version", "streamlink") @patch("streamlink_cli.main.requests.__version__", "requests") @patch("streamlink_cli.main.socks_version", "socks") @patch("streamlink_cli.main.websocket_version", "websocket") @patch("platform.python_version", Mock(return_value="python")) def test_log_current_versions(self, mock_log): self.subject(["streamlink", "--loglevel", "info"]) self.assertEqual(mock_log.debug.mock_calls, [], "Doesn't log anything if not debug logging") with patch("sys.platform", "linux"), \ patch("platform.platform", Mock(return_value="linux")): self.subject(["streamlink", "--loglevel", "debug"]) self.assertEqual( mock_log.debug.mock_calls[:4], [ call("OS: linux"), call("Python: python"), call("Streamlink: streamlink"), call("Requests(requests), Socks(socks), Websocket(websocket)") ] ) mock_log.debug.reset_mock() with patch("sys.platform", "darwin"), \ patch("platform.mac_ver", Mock(return_value=["0.0.0"])): self.subject(["streamlink", "--loglevel", "debug"]) self.assertEqual( mock_log.debug.mock_calls[:4], [ call("OS: macOS 0.0.0"), call("Python: python"), call("Streamlink: streamlink"), call("Requests(requests), Socks(socks), Websocket(websocket)") ] ) mock_log.debug.reset_mock() with patch("sys.platform", "win32"), \ patch("platform.system", Mock(return_value="Windows")), \ patch("platform.release", Mock(return_value="0.0.0")): self.subject(["streamlink", "--loglevel", "debug"]) self.assertEqual( mock_log.debug.mock_calls[:4], [ call("OS: Windows 0.0.0"), call("Python: python"), call("Streamlink: streamlink"), call("Requests(requests), Socks(socks), Websocket(websocket)") ] ) mock_log.debug.reset_mock() @patch("streamlink_cli.main.log") def test_log_current_arguments(self, mock_log): self.subject([ "streamlink", "--loglevel", "info" ]) self.assertEqual(mock_log.debug.mock_calls, [], "Doesn't log anything if not debug logging") self.subject([ "streamlink", "--loglevel", "debug", "-p", "custom", "--testplugin-bool", "--testplugin-password=secret", "test.se/channel", "best,worst" ]) self.assertEqual( mock_log.debug.mock_calls[-7:], [ call("Arguments:"), call(" url=test.se/channel"), call(" stream=['best', 'worst']"), call(" --loglevel=debug"), call(" --player=custom"), call(" --testplugin-bool=True"), call(" --testplugin-password=********") ] ) class TestCLIMainLoggingLogfile(_TestCLIMainLogging): @patch("sys.stdout") @patch("builtins.open") def test_logfile_no_logfile(self, mock_open, mock_stdout): self.subject(["streamlink"]) streamlink_cli.main.log.info("foo") streamlink_cli.main.console.msg("bar") self.assertEqual(streamlink_cli.main.console.output, sys.stdout) self.assertFalse(mock_open.called) self.assertEqual(mock_stdout.write.mock_calls, self._write_calls) @patch("sys.stdout") @patch("builtins.open") def test_logfile_loglevel_none(self, mock_open, mock_stdout): self.subject(["streamlink", "--loglevel", "none", "--logfile", "foo"]) streamlink_cli.main.log.info("foo") streamlink_cli.main.console.msg("bar") self.assertEqual(streamlink_cli.main.console.output, sys.stdout) self.assertFalse(mock_open.called) self.assertEqual(mock_stdout.write.mock_calls, [call("bar\n")]) @patch("sys.stdout") @patch("builtins.open") @patch("pathlib.Path.mkdir", Mock()) def test_logfile_path_relative(self, mock_open, mock_stdout): path = Path("foo").resolve() self.subject(["streamlink", "--logfile", "foo"]) self.write_file_and_assert( mock_mkdir=path.mkdir, mock_write=mock_open(str(path), "a").write, mock_stdout=mock_stdout ) @unittest.skipIf(is_win32, "test only applicable on a POSIX OS") class TestCLIMainLoggingLogfilePosix(_TestCLIMainLogging): @patch("sys.stdout") @patch("builtins.open") @patch("pathlib.Path.mkdir", Mock()) def test_logfile_path_absolute(self, mock_open, mock_stdout): self.subject(["streamlink", "--logfile", "/foo/bar"]) self.write_file_and_assert( mock_mkdir=PosixPath("/foo").mkdir, mock_write=mock_open("/foo/bar", "a").write, mock_stdout=mock_stdout ) @patch("sys.stdout") @patch("builtins.open") @patch("pathlib.Path.mkdir", Mock()) def test_logfile_path_expanduser(self, mock_open, mock_stdout): with patch.dict(os.environ, {"HOME": "/foo"}): self.subject(["streamlink", "--logfile", "~/bar"]) self.write_file_and_assert( mock_mkdir=PosixPath("/foo").mkdir, mock_write=mock_open("/foo/bar", "a").write, mock_stdout=mock_stdout ) @patch("sys.stdout") @patch("builtins.open") @patch("pathlib.Path.mkdir", Mock()) @freezegun.freeze_time(datetime.datetime(2000, 1, 2, 3, 4, 5)) def test_logfile_path_auto(self, mock_open, mock_stdout): with patch("streamlink_cli.constants.LOG_DIR", PosixPath("/foo")): self.subject(["streamlink", "--logfile", "-"]) self.write_file_and_assert( mock_mkdir=PosixPath("/foo").mkdir, mock_write=mock_open("/foo/2000-01-02_03-04-05.log", "a").write, mock_stdout=mock_stdout ) @unittest.skipIf(not is_win32, "test only applicable on Windows") class TestCLIMainLoggingLogfileWindows(_TestCLIMainLogging): @patch("sys.stdout") @patch("builtins.open") @patch("pathlib.Path.mkdir", Mock()) def test_logfile_path_absolute(self, mock_open, mock_stdout): self.subject(["streamlink", "--logfile", "C:\\foo\\bar"]) self.write_file_and_assert( mock_mkdir=WindowsPath("C:\\foo").mkdir, mock_write=mock_open("C:\\foo\\bar", "a").write, mock_stdout=mock_stdout ) @patch("sys.stdout") @patch("builtins.open") @patch("pathlib.Path.mkdir", Mock()) def test_logfile_path_expanduser(self, mock_open, mock_stdout): with patch.dict(os.environ, {"USERPROFILE": "C:\\foo"}): self.subject(["streamlink", "--logfile", "~\\bar"]) self.write_file_and_assert( mock_mkdir=WindowsPath("C:\\foo").mkdir, mock_write=mock_open("C:\\foo\\bar", "a").write, mock_stdout=mock_stdout ) @patch("sys.stdout") @patch("builtins.open") @patch("pathlib.Path.mkdir", Mock()) @freezegun.freeze_time(datetime.datetime(2000, 1, 2, 3, 4, 5)) def test_logfile_path_auto(self, mock_open, mock_stdout): with patch("streamlink_cli.constants.LOG_DIR", WindowsPath("C:\\foo")): self.subject(["streamlink", "--logfile", "-"]) self.write_file_and_assert( mock_mkdir=WindowsPath("C:\\foo").mkdir, mock_write=mock_open("C:\\foo\\2000-01-02_03-04-05.log", "a").write, mock_stdout=mock_stdout ) class TestCLIMainPrint(unittest.TestCase): def subject(self): with patch.object(Streamlink, "load_builtin_plugins"), \ patch.object(Streamlink, "resolve_url") as mock_resolve_url, \ patch.object(Streamlink, "resolve_url_no_redirect") as mock_resolve_url_no_redirect: session = Streamlink() session.load_plugins(os.path.join(os.path.dirname(__file__), "plugin")) with patch("streamlink_cli.main.streamlink", session), \ patch("streamlink_cli.main.CONFIG_FILES", []), \ patch("streamlink_cli.main.setup_streamlink"), \ patch("streamlink_cli.main.setup_plugins"), \ patch("streamlink_cli.main.setup_http_session"), \ patch("streamlink_cli.main.setup_signals"), \ patch("streamlink_cli.main.setup_options") as mock_setup_options: with self.assertRaises(SystemExit) as cm: streamlink_cli.main.main() self.assertEqual(cm.exception.code, 0) mock_resolve_url.assert_not_called() mock_resolve_url_no_redirect.assert_not_called() mock_setup_options.assert_not_called() @staticmethod def get_stdout(mock_stdout): return "".join([call_arg[0][0] for call_arg in mock_stdout.write.call_args_list]) @patch("sys.stdout") @patch("sys.argv", ["streamlink"]) def test_print_usage(self, mock_stdout): self.subject() self.assertEqual( self.get_stdout(mock_stdout), "usage: streamlink [OPTIONS] [STREAM]\n\n" + "Use -h/--help to see the available options or read the manual at https://streamlink.github.io\n" ) @patch("sys.stdout") @patch("sys.argv", ["streamlink", "--help"]) def test_print_help(self, mock_stdout): self.subject() output = self.get_stdout(mock_stdout) self.assertIn( "usage: streamlink [OPTIONS] [STREAM]", output ) self.assertIn( dedent(""" Streamlink is a command-line utility that extracts streams from various services and pipes them into a video player of choice. """), output ) self.assertIn( dedent(""" For more in-depth documentation see: https://streamlink.github.io Please report broken plugins or bugs to the issue tracker on Github: https://github.com/streamlink/streamlink/issues """), output ) @patch("sys.stdout") @patch("sys.argv", ["streamlink", "--plugins"]) def test_print_plugins(self, mock_stdout): self.subject() self.assertEqual(self.get_stdout(mock_stdout), "Loaded plugins: testplugin\n") @patch("sys.stdout") @patch("sys.argv", ["streamlink", "--plugins", "--json"]) def test_print_plugins_json(self, mock_stdout): self.subject() self.assertEqual(self.get_stdout(mock_stdout), """[\n "testplugin"\n]\n""") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/test_cli_playerout.py0000644000175100001710000000647500000000000020667 0ustar00runnerdockerfrom unittest.mock import ANY, Mock, patch from streamlink_cli.output import PlayerOutput from tests import posix_only, windows_only UNICODE_TITLE = "기타치는소율 with UL섬 " @posix_only @patch("streamlink_cli.output.sleep", Mock()) @patch("subprocess.Popen") def test_output_mpv_unicode_title_posix(popen): po = PlayerOutput("mpv", title=UNICODE_TITLE) popen().poll.side_effect = lambda: None po.open() popen.assert_called_with(["mpv", f"--force-media-title={UNICODE_TITLE}", "-"], bufsize=ANY, stderr=ANY, stdout=ANY, stdin=ANY) @posix_only @patch("streamlink_cli.output.sleep", Mock()) @patch("subprocess.Popen") def test_output_vlc_unicode_title_posix(popen): po = PlayerOutput("vlc", title=UNICODE_TITLE) popen().poll.side_effect = lambda: None po.open() popen.assert_called_with(["vlc", "--input-title-format", UNICODE_TITLE, "-"], bufsize=ANY, stderr=ANY, stdout=ANY, stdin=ANY) @windows_only @patch("streamlink_cli.output.sleep", Mock()) @patch("subprocess.Popen") def test_output_mpv_unicode_title_windows_py3(popen): po = PlayerOutput("mpv.exe", title=UNICODE_TITLE) popen().poll.side_effect = lambda: None po.open() popen.assert_called_with(f"mpv.exe \"--force-media-title={UNICODE_TITLE}\" -", bufsize=ANY, stderr=ANY, stdout=ANY, stdin=ANY) @windows_only @patch("streamlink_cli.output.sleep", Mock()) @patch("subprocess.Popen") def test_output_vlc_unicode_title_windows_py3(popen): po = PlayerOutput("vlc.exe", title=UNICODE_TITLE) popen().poll.side_effect = lambda: None po.open() popen.assert_called_with(f"vlc.exe --input-title-format \"{UNICODE_TITLE}\" -", bufsize=ANY, stderr=ANY, stdout=ANY, stdin=ANY) @posix_only def test_output_args_posix(): po_none = PlayerOutput("foo") assert po_none._create_arguments() == ["foo", "-"] po_implicit = PlayerOutput("foo", args="--bar") assert po_implicit._create_arguments() == ["foo", "--bar", "-"] po_explicit = PlayerOutput("foo", args="--bar {playerinput}") assert po_explicit._create_arguments() == ["foo", "--bar", "-"] po_fallback = PlayerOutput("foo", args="--bar {filename}") assert po_fallback._create_arguments() == ["foo", "--bar", "-"] po_fallback = PlayerOutput("foo", args="--bar {playerinput} {filename}") assert po_fallback._create_arguments() == ["foo", "--bar", "-", "-"] po_fallback = PlayerOutput("foo", args="--bar {qux}") assert po_fallback._create_arguments() == ["foo", "--bar", "{qux}", "-"] @windows_only def test_output_args_windows(): po_none = PlayerOutput("foo") assert po_none._create_arguments() == "foo -" po_implicit = PlayerOutput("foo", args="--bar") assert po_implicit._create_arguments() == "foo --bar -" po_explicit = PlayerOutput("foo", args="--bar {playerinput}") assert po_explicit._create_arguments() == "foo --bar -" po_fallback = PlayerOutput("foo", args="--bar {filename}") assert po_fallback._create_arguments() == "foo --bar -" po_fallback = PlayerOutput("foo", args="--bar {playerinput} {filename}") assert po_fallback._create_arguments() == "foo --bar - -" po_fallback = PlayerOutput("foo", args="--bar {qux}") assert po_fallback._create_arguments() == "foo --bar {qux} -" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/test_cli_util_progress.py0000644000175100001710000000160700000000000021534 0ustar00runnerdockerimport unittest from streamlink_cli.utils.progress import get_cut_prefix, terminal_width class TestCliUtilProgess(unittest.TestCase): def test_terminal_width(self): self.assertEqual(10, terminal_width("ABCDEFGHIJ")) self.assertEqual(30, terminal_width("A你好世界こんにちは안녕하세요B")) self.assertEqual(30, terminal_width("·「」『』【】-=!@#¥%……&×()")) pass def test_get_cut_prefix(self): self.assertEqual("녕하세요CD", get_cut_prefix("你好世界こんにちは안녕하세요CD", 10)) self.assertEqual("하세요CD", get_cut_prefix("你好世界こんにちは안녕하세요CD", 9)) self.assertEqual("こんにちは안녕하세요CD", get_cut_prefix("你好世界こんにちは안녕하세요CD", 23)) pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/test_cli_utils_formatter.py0000644000175100001710000001127100000000000022054 0ustar00runnerdockerimport unittest from datetime import datetime from os.path import sep from pathlib import Path from unittest.mock import Mock, call, patch from freezegun import freeze_time from streamlink_cli.utils.formatter import Formatter @freeze_time(datetime(2000, 1, 2, 3, 4, 5, 6, None)) class TestFormatter(unittest.TestCase): class Obj: def __str__(self): return "obj" def setUp(self): self.prop = Mock(return_value="prop") self.obj = self.Obj() self.formatter = Formatter( { "prop": self.prop, "obj": lambda: self.obj, "time": datetime.now, "empty": lambda: "", "none": lambda: None }, { "time": lambda dt, fmt: dt.strftime(fmt) } ) def test_unknown(self): self.assertEqual(self.formatter.title("{}"), "{}") self.assertEqual(self.formatter.title("some {unknown} variable"), "some {unknown} variable") self.assertEqual(self.formatter.title("some {unknown} variable", {"unknown": "known"}), "some known variable") self.assertEqual(self.formatter.cache, dict()) def test_title(self): self.assertEqual(self.formatter.title("text '{prop}' '{empty}' '{none}'"), "text 'prop' '' ''") self.assertEqual(self.formatter.cache, dict(prop="prop", empty="", none=None)) self.assertEqual(self.prop.call_count, 1) self.assertEqual(self.formatter.title("text '{prop}' '{obj}' '{empty}' '{none}'"), "text 'prop' 'obj' '' ''") self.assertEqual(self.formatter.cache, dict(prop="prop", obj=self.obj, empty="", none=None)) self.assertEqual(self.prop.call_count, 1) defaults = dict(prop="PROP", obj="OBJ", empty="EMPTY", none="NONE") self.assertEqual(self.formatter.title("'{prop}' '{obj}' '{empty}' '{none}'", defaults), "'prop' 'obj' '' 'NONE'") self.assertEqual(self.formatter.cache, dict(prop="prop", obj=self.obj, empty="", none=None)) self.assertEqual(self.prop.call_count, 1) @patch("streamlink_cli.utils.formatter.replace_chars") def test_path(self, mock_replace_chars: Mock): mock_replace_chars.side_effect = lambda s, *_: s.upper() self.assertEqual( self.formatter.path("text '{prop}' '{empty}' '{none}'"), Path("text 'PROP' '' ''") ) self.assertEqual(self.formatter.cache, dict(prop="prop", empty="", none=None)) self.assertEqual(self.prop.call_count, 1) self.assertEqual(mock_replace_chars.call_args_list, [ call("prop", None), call("", None), call("", None) ]) mock_replace_chars.reset_mock() self.assertEqual( self.formatter.path("text '{prop}' '{obj}' '{empty}' '{none}'", "foo"), Path("text 'PROP' 'OBJ' '' ''") ) self.assertEqual(self.formatter.cache, dict(prop="prop", obj=self.obj, empty="", none=None)) self.assertEqual(self.prop.call_count, 1) self.assertEqual(mock_replace_chars.call_args_list, [ call("prop", "foo"), call("obj", "foo"), call("", "foo"), call("", "foo") ]) def test_path_substitute(self): self.formatter.mapping.update(**{ "current": lambda: ".", "parent": lambda: "..", "dots": lambda: "...", "separator": lambda: sep, }) self.assertEqual( self.formatter.path(f"{{current}}{sep}{{parent}}{sep}{{dots}}{sep}{{separator}}{sep}foo{sep}.{sep}..{sep}bar"), Path("_", "_", "...", "_", "foo", ".", "..", "bar"), "Formats the path's parts separately and ignores current and parent directories in substitutions only" ) def test_format_spec(self): self.assertEqual(self.formatter.title("{time}"), "2000-01-02 03:04:05.000006") self.assertEqual(self.formatter.cache, dict(time=datetime(2000, 1, 2, 3, 4, 5, 6, None))) self.assertEqual(self.formatter.title("{time:%Y}"), "2000") self.assertEqual(self.formatter.title("{time:%Y-%m-%d}"), "2000-01-02") self.assertEqual(self.formatter.title("{time:%H:%M:%S}"), "03:04:05") with patch("datetime.datetime.strftime", side_effect=ValueError): self.assertEqual(self.formatter.title("{time:foo:bar}"), "{time:foo:bar}") self.assertEqual(self.formatter.cache, dict(time=datetime(2000, 1, 2, 3, 4, 5, 6, None))) self.assertEqual(self.formatter.title("{prop:foo}"), "prop") self.assertEqual(self.formatter.title("{none:foo}"), "") self.assertEqual(self.formatter.title("{unknown:format}"), "{unknown:format}") self.assertEqual(self.formatter.title("{unknown:format}", {"unknown": "known"}), "known") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/test_cli_utils_path.py0000644000175100001710000000560000000000000021004 0ustar00runnerdockerimport os from pathlib import Path from unittest.mock import patch import pytest from streamlink_cli.utils.path import replace_chars, replace_path from tests import posix_only, windows_only @pytest.mark.parametrize("char", [i for i in range(32)]) def test_replace_chars_unprintable(char: int): assert replace_chars(f"foo{chr(char)}{chr(char)}bar") == "foo_bar", "Replaces unprintable characters" @posix_only @pytest.mark.parametrize("char", "/".split()) def test_replace_chars_posix(char: str): assert replace_chars(f"foo{char}{char}bar") == "foo_bar", "Replaces multiple unsupported characters in a row" @windows_only @pytest.mark.parametrize("char", "\x7f\"*/:<>?\\|".split()) def test_replace_chars_windows(char: str): assert replace_chars(f"foo{char}{char}bar") == "foo_bar", "Replaces multiple unsupported characters in a row" @posix_only def test_replace_chars_posix_all(): assert replace_chars("".join(chr(i) for i in range(32)) + "/") == "_" @windows_only def test_replace_chars_windows_all(): assert replace_chars("".join(chr(i) for i in range(32)) + "\x7f\"*/:<>?\\|") == "_" @posix_only def test_replace_chars_posix_override(): all_chars = "".join(chr(i) for i in range(32)) + "\x7f\"*:/<>?\\|" assert replace_chars(all_chars) == "_\x7f\"*:_<>?\\|" assert replace_chars(all_chars, "posix") == "_\x7f\"*:_<>?\\|" assert replace_chars(all_chars, "unix") == "_\x7f\"*:_<>?\\|" assert replace_chars(all_chars, "windows") == "_" assert replace_chars(all_chars, "win32") == "_" @windows_only def test_replace_chars_windows_override(): all_chars = "".join(chr(i) for i in range(32)) + "\x7f\"*:/<>?\\|" assert replace_chars(all_chars) == "_" assert replace_chars(all_chars, "posix") == "_\x7f\"*:_<>?\\|" assert replace_chars(all_chars, "unix") == "_\x7f\"*:_<>?\\|" assert replace_chars(all_chars, "windows") == "_" assert replace_chars(all_chars, "win32") == "_" def test_replace_chars_replacement(): assert replace_chars("\x00", None, "+") == "+" def test_replace_path(): def mapper(s): return dict(foo=".", bar="..").get(s, s) path = Path("foo", ".", "bar", "..", "baz") expected = Path("_", ".", "_", "..", "baz") assert replace_path(path, mapper) == expected, "Only replaces mapped parts which are in the special parts tuple" @posix_only def test_replace_path_expanduser_posix(): with patch.object(os, "environ", {"HOME": "/home/foo"}): assert replace_path("~/bar", lambda s: s) == Path("/home/foo/bar") assert replace_path("foo/bar", lambda s: dict(foo="~").get(s, s)) == Path("~/bar") @windows_only def test_replace_path_expanduser_windows(): with patch.object(os, "environ", {"USERPROFILE": "C:\\Users\\foo"}): assert replace_path("~\\bar", lambda s: s) == Path("C:\\Users\\foo\\bar") assert replace_path("foo\\bar", lambda s: dict(foo="~").get(s, s)) == Path("~\\bar") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/test_cmdline.py0000644000175100001710000001372100000000000017417 0ustar00runnerdockerimport os.path import unittest from unittest.mock import ANY, Mock, patch import streamlink_cli.main from streamlink import Streamlink from streamlink_cli.compat import is_win32 class CommandLineTestCase(unittest.TestCase): """ Test that when invoked for the command line arguments are parsed as expected """ @patch('streamlink_cli.main.CONFIG_FILES', []) @patch('streamlink_cli.main.setup_streamlink') @patch('streamlink_cli.output.sleep') @patch('streamlink_cli.output.subprocess.call') @patch('streamlink_cli.output.subprocess.Popen') @patch('sys.argv') def _test_args(self, args, commandline, mock_argv, mock_popen, mock_call, mock_sleep, mock_setup_streamlink, passthrough=False, exit_code=0): mock_argv.__getitem__.side_effect = lambda x: args[x] def side_effect(results): def fn(*args): result = results.pop(0) return result return fn mock_popen.return_value = Mock(poll=Mock(side_effect=side_effect([None, 0]))) session = Streamlink() session.load_plugins(os.path.join(os.path.dirname(__file__), "plugin")) actual_exit_code = 0 with patch('streamlink_cli.main.streamlink', session): try: streamlink_cli.main.main() except SystemExit as exc: actual_exit_code = exc.code self.assertEqual(exit_code, actual_exit_code) mock_setup_streamlink.assert_called_with() if not passthrough: mock_popen.assert_called_with(commandline, stderr=ANY, stdout=ANY, bufsize=ANY, stdin=ANY) else: mock_call.assert_called_with(commandline, stderr=ANY, stdout=ANY) @unittest.skipIf(is_win32, "test only applicable in a POSIX OS") class TestCommandLinePOSIX(CommandLineTestCase): """ Commandline tests under POSIX-like operating systems """ def test_open_regular_path_player(self): self._test_args(["streamlink", "-p", "/usr/bin/player", "http://test.se", "test"], ["/usr/bin/player", "-"]) def test_open_space_path_player(self): self._test_args(["streamlink", "-p", "\"/Applications/Video Player/player\"", "http://test.se", "test"], ["/Applications/Video Player/player", "-"]) # escaped self._test_args(["streamlink", "-p", "/Applications/Video\\ Player/player", "http://test.se", "test"], ["/Applications/Video Player/player", "-"]) def test_open_player_extra_args_in_player(self): self._test_args(["streamlink", "-p", "/usr/bin/player", "-a", '''--input-title-format "Poker \\"Stars\\"" {filename}''', "http://test.se", "test"], ["/usr/bin/player", "--input-title-format", 'Poker "Stars"', "-"]) def test_open_player_extra_args_in_player_pass_through(self): self._test_args(["streamlink", "--player-passthrough", "hls", "-p", "/usr/bin/player", "-a", '''--input-title-format "Poker \\"Stars\\"" {filename}''', "test.se", "hls"], ["/usr/bin/player", "--input-title-format", 'Poker "Stars"', "http://test.se/playlist.m3u8"], passthrough=True) def test_single_hyphen_extra_player_args_971(self): """single hyphen params at the beginning of --player-args - https://github.com/streamlink/streamlink/issues/971 """ self._test_args(["streamlink", "-p", "/usr/bin/player", "-a", "-v {filename}", "http://test.se", "test"], ["/usr/bin/player", "-v", "-"]) @unittest.skipIf(not is_win32, "test only applicable on Windows") class TestCommandLineWindows(CommandLineTestCase): """ Commandline tests for Windows """ def test_open_space_path_player(self): self._test_args(["streamlink", "-p", "c:\\Program Files\\Player\\player.exe", "http://test.se", "test"], "c:\\Program Files\\Player\\player.exe -") def test_open_space_quote_path_player(self): self._test_args(["streamlink", "-p", "\"c:\\Program Files\\Player\\player.exe\"", "http://test.se", "test"], "\"c:\\Program Files\\Player\\player.exe\" -") def test_open_player_args_with_quote_in_player(self): self._test_args(["streamlink", "-p", '''c:\\Program Files\\Player\\player.exe --input-title-format "Poker \\"Stars\\""''', "http://test.se", "test"], '''c:\\Program Files\\Player\\player.exe --input-title-format "Poker \\"Stars\\"" -''') def test_open_player_extra_args_in_player(self): self._test_args(["streamlink", "-p", "c:\\Program Files\\Player\\player.exe", "-a", '''--input-title-format "Poker \\"Stars\\"" {filename}''', "http://test.se", "test"], '''c:\\Program Files\\Player\\player.exe --input-title-format "Poker \\"Stars\\"" -''') def test_open_player_extra_args_in_player_pass_through(self): self._test_args(["streamlink", "--player-passthrough", "hls", "-p", "c:\\Program Files\\Player\\player.exe", "-a", '''--input-title-format "Poker \\"Stars\\"" {filename}''', "test.se", "hls"], '''c:\\Program Files\\Player\\player.exe''' + ''' --input-title-format "Poker \\"Stars\\"" \"http://test.se/playlist.m3u8\"''', passthrough=True) def test_single_hyphen_extra_player_args_971(self): """single hyphen params at the beginning of --player-args - https://github.com/streamlink/streamlink/issues/971 """ self._test_args(["streamlink", "-p", "c:\\Program Files\\Player\\player.exe", "-a", "-v {filename}", "http://test.se", "test"], "c:\\Program Files\\Player\\player.exe -v -") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/test_cmdline_player_fifo.py0000644000175100001710000000334400000000000021776 0ustar00runnerdockerimport unittest from unittest.mock import Mock, patch from streamlink.compat import is_win32 from tests.test_cmdline import CommandLineTestCase @unittest.skipIf(is_win32, "test only applicable in a POSIX OS") @patch("streamlink_cli.main.NamedPipe", Mock(return_value=Mock(path="/tmp/streamlinkpipe"))) class TestCommandLineWithPlayerFifoPosix(CommandLineTestCase): def test_player_fifo_default(self): self._test_args( ["streamlink", "--player-fifo", "-p", "any-player", "http://test.se", "test"], ["any-player", "/tmp/streamlinkpipe"] ) @unittest.skipIf(not is_win32, "test only applicable on Windows") @patch("streamlink_cli.main.NamedPipe", Mock(return_value=Mock(path="\\\\.\\pipe\\streamlinkpipe"))) class TestCommandLineWithPlayerFifoWindows(CommandLineTestCase): def test_player_fifo_default(self): self._test_args( ["streamlink", "--player-fifo", "-p", "any-player.exe", "http://test.se", "test"], "any-player.exe \\\\.\\pipe\\streamlinkpipe" ) def test_player_fifo_vlc(self): self._test_args( ["streamlink", "--player-fifo", "-p", "C:\\Program Files\\VideoLAN\\vlc.exe", "http://test.se", "test"], "C:\\Program Files\\VideoLAN\\vlc.exe --input-title-format http://test.se stream://\\\\\\.\\pipe\\streamlinkpipe" ) def test_player_fifo_mpv(self): self._test_args( ["streamlink", "--player-fifo", "-p", "C:\\Program Files\\mpv\\mpv.exe", "http://test.se", "test"], "C:\\Program Files\\mpv\\mpv.exe --force-media-title=http://test.se file://\\\\.\\pipe\\streamlinkpipe" ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/test_cmdline_title.py0000644000175100001710000001007300000000000020615 0ustar00runnerdockerimport unittest from streamlink.compat import is_win32 from tests.test_cmdline import CommandLineTestCase @unittest.skipIf(is_win32, "test only applicable in a POSIX OS") class TestCommandLineWithTitlePOSIX(CommandLineTestCase): def test_open_player_with_title_vlc(self): self._test_args(["streamlink", "-p", "/usr/bin/vlc", "--title", "{title} - {author} - {category}", "http://test.se", "test"], ["/usr/bin/vlc", "--input-title-format", "Test Title - Tѥst Āuƭhǿr - No Category", "-"]) def test_open_player_with_default_title_vlc(self): self._test_args(["streamlink", "-p", "/usr/bin/vlc", "http://test.se", "test"], ["/usr/bin/vlc", "--input-title-format", 'http://test.se', "-"]) def test_open_player_with_default_title_vlc_args(self): self._test_args(["streamlink", "-p", "\"/Applications/VLC/vlc\" --other-option", "http://test.se", "test"], ["/Applications/VLC/vlc", "--other-option", "--input-title-format", 'http://test.se', "-"]) def test_open_player_with_title_mpv(self): self._test_args(["streamlink", "-p", "/usr/bin/mpv", "--title", "{title}", "http://test.se", "test"], ["/usr/bin/mpv", "--force-media-title=Test Title", "-"]) def test_unicode_title_2444(self): self._test_args(["streamlink", "-p", "mpv", "-t", "★ ★ ★", "http://test.se", "test"], ["mpv", "--force-media-title=★ ★ ★", "-"]) @unittest.skipIf(not is_win32, "test only applicable on Windows") class TestCommandLineWithTitleWindows(CommandLineTestCase): def test_open_player_with_title_vlc(self): self._test_args( ["streamlink", "-p", "c:\\Program Files\\VideoLAN\\vlc.exe", "--title", "{title} - {author} - {category}", "http://test.se", "test"], "c:\\Program Files\\VideoLAN\\vlc.exe --input-title-format \"Test Title - Tѥst Āuƭhǿr - No Category\" -" ) def test_open_player_with_default_title_vlc(self): self._test_args( ["streamlink", "-p", "c:\\Program Files\\VideoLAN\\vlc.exe", "http://test.se", "test"], "c:\\Program Files\\VideoLAN\\vlc.exe --input-title-format http://test.se -" ) def test_open_player_with_default_arg_vlc(self): self._test_args( ["streamlink", "-p", "c:\\Program Files\\VideoLAN\\vlc.exe --argh", "http://test.se", "test"], "c:\\Program Files\\VideoLAN\\vlc.exe --argh --input-title-format http://test.se -" ) # PotPlayer def test_open_player_with_title_pot(self): self._test_args( ["streamlink", "-p", "\"c:\\Program Files\\DAUM\\PotPlayer\\PotPlayerMini64.exe\"", "--title", "{title}", "http://test.se/stream", "hls", "--player-passthrough", "hls"], "\"c:\\Program Files\\DAUM\\PotPlayer\\PotPlayerMini64.exe\" \"http://test.se/playlist.m3u8\\Test Title\"", passthrough=True ) def test_open_player_with_unicode_author_pot_py3(self): self._test_args( ["streamlink", "-p", "\"c:\\Program Files\\DAUM\\PotPlayer\\PotPlayerMini64.exe\"", "--title", "{author}", "http://test.se/stream", "hls", "--player-passthrough", "hls"], "\"c:\\Program Files\\DAUM\\PotPlayer\\PotPlayerMini64.exe\" " + "\"http://test.se/playlist.m3u8\\Tѥst Āuƭhǿr\"", passthrough=True ) def test_open_player_with_default_title_pot(self): self._test_args( ["streamlink", "-p", "\"c:\\Program Files\\DAUM\\PotPlayer\\PotPlayerMini64.exe\"", "http://test.se/stream", "hls", "--player-passthrough", "hls"], "\"c:\\Program Files\\DAUM\\PotPlayer\\PotPlayerMini64.exe\" " + "\"http://test.se/playlist.m3u8\\http://test.se/stream\"", passthrough=True ) def test_unicode_title_2444_py3(self): self._test_args(["streamlink", "-p", "mpv", "-t", "★ ★ ★", "http://test.se", "test"], "mpv \"--force-media-title=★ ★ ★\" -") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/test_console.py0000644000175100001710000001210700000000000017443 0ustar00runnerdockerimport unittest from io import StringIO from unittest.mock import Mock, patch from streamlink_cli.console import ConsoleOutput class TestConsoleOutput(unittest.TestCase): def test_msg(self): output = StringIO() console = ConsoleOutput(output) console.msg("foo") console.msg_json({"test": 1}) self.assertEqual("foo\n", output.getvalue()) def test_msg_json(self): output = StringIO() console = ConsoleOutput(output, json=True) console.msg("foo") console.msg_json({"test": 1}) self.assertEqual('{\n "test": 1\n}\n', output.getvalue()) def test_msg_json_object(self): output = StringIO() console = ConsoleOutput(output, json=True) console.msg_json(Mock(__json__=Mock(return_value={"test": 1}))) self.assertEqual('{\n "test": 1\n}\n', output.getvalue()) def test_msg_json_list(self): output = StringIO() console = ConsoleOutput(output, json=True) test_list = ["foo", "bar"] console.msg_json(test_list) self.assertEqual('[\n "foo",\n "bar"\n]\n', output.getvalue()) def test_msg_json_merge_object(self): output = StringIO() console = ConsoleOutput(output, json=True) test_obj1 = {"test": 1, "foo": "foo"} test_obj2 = Mock(__json__=Mock(return_value={"test": 2})) console.msg_json(test_obj1, test_obj2, ["qux"], foo="bar", baz="qux") self.assertEqual( '{\n' ' "test": 2,\n' ' "foo": "bar",\n' ' "baz": "qux"\n' '}\n', output.getvalue() ) self.assertEqual([("test", 1), ("foo", "foo")], list(test_obj1.items())) def test_msg_json_merge_list(self): output = StringIO() console = ConsoleOutput(output, json=True) test_list1 = ["foo", "bar"] test_list2 = Mock(__json__=Mock(return_value={"foo": "bar"})) console.msg_json(test_list1, ["baz"], test_list2, {"foo": "bar"}, foo="bar", baz="qux") self.assertEqual( '[\n' ' "foo",\n' ' "bar",\n' ' "baz",\n' ' {\n "foo": "bar"\n },\n' ' {\n "foo": "bar"\n },\n' ' {\n "foo": "bar",\n "baz": "qux"\n }\n' ']\n', output.getvalue() ) self.assertEqual(["foo", "bar"], test_list1) @patch("streamlink_cli.console.sys.exit") def test_msg_json_error(self, mock_exit): output = StringIO() console = ConsoleOutput(output, json=True) console.msg_json({"error": "bad"}) self.assertEqual('{\n "error": "bad"\n}\n', output.getvalue()) mock_exit.assert_called_with(1) @patch("streamlink_cli.console.sys.exit") def test_exit(self, mock_exit: Mock): output = StringIO() console = ConsoleOutput(output) console.exit("error") self.assertEqual("error: error\n", output.getvalue()) mock_exit.assert_called_with(1) @patch("streamlink_cli.console.sys.exit") def test_exit_json(self, mock_exit: Mock): output = StringIO() console = ConsoleOutput(output, json=True) console.exit("error") self.assertEqual('{\n "error": "error"\n}\n', output.getvalue()) mock_exit.assert_called_with(1) @patch("streamlink_cli.console.input", Mock(return_value="hello")) @patch("streamlink_cli.console.sys.stdin.isatty", Mock(return_value=True)) def test_ask(self): output = StringIO() console = ConsoleOutput(output) self.assertEqual("hello", console.ask("test: ")) self.assertEqual("test: ", output.getvalue()) @patch("streamlink_cli.console.input") @patch("streamlink_cli.console.sys.stdin.isatty", Mock(return_value=False)) def test_ask_no_tty(self, mock_input: Mock): output = StringIO() console = ConsoleOutput(output) self.assertIsNone(console.ask("test: ")) self.assertEqual("", output.getvalue()) mock_input.assert_not_called() @patch("streamlink_cli.console.input", Mock(side_effect=ValueError)) @patch("streamlink_cli.console.sys.stdin.isatty", Mock(return_value=True)) def test_ask_input_exception(self): output = StringIO() console = ConsoleOutput(output) self.assertIsNone(console.ask("test: ")) self.assertEqual("test: ", output.getvalue()) @patch("streamlink_cli.console.getpass") @patch("streamlink_cli.console.sys.stdin.isatty", Mock(return_value=True)) def test_askpass(self, mock_getpass: Mock): def getpass(prompt, stream): stream.write(prompt) return "hello" output = StringIO() console = ConsoleOutput(output) mock_getpass.side_effect = getpass self.assertEqual("hello", console.askpass("test: ")) self.assertEqual("test: ", output.getvalue()) @patch("streamlink_cli.console.sys.stdin.isatty", Mock(return_value=False)) def test_askpass_no_tty(self): output = StringIO() console = ConsoleOutput(output) self.assertIsNone(console.askpass("test: ")) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/test_log.py0000644000175100001710000001124100000000000016560 0ustar00runnerdockerimport logging import unittest from datetime import datetime from io import StringIO import freezegun from streamlink import logger class TestLogging(unittest.TestCase): @classmethod def _new_logger(cls, format="[{name}][{levelname}] {message}", style="{", **params): output = StringIO() logger.basicConfig(stream=output, format=format, style=style, **params) return logging.getLogger("streamlink.test"), output def test_level_names(self): self.assertEqual(logger.levels, [ "none", "critical", "error", "warning", "info", "debug", "trace" ]) self.assertEqual(logging.getLevelName(logger.NONE), "none") self.assertEqual(logging.getLevelName(logger.CRITICAL), "critical") self.assertEqual(logging.getLevelName(logger.ERROR), "error") self.assertEqual(logging.getLevelName(logger.WARNING), "warning") self.assertEqual(logging.getLevelName(logger.INFO), "info") self.assertEqual(logging.getLevelName(logger.DEBUG), "debug") self.assertEqual(logging.getLevelName(logger.TRACE), "trace") self.assertEqual(logging.getLevelName("none"), logger.NONE) self.assertEqual(logging.getLevelName("critical"), logger.CRITICAL) self.assertEqual(logging.getLevelName("error"), logger.ERROR) self.assertEqual(logging.getLevelName("warning"), logger.WARNING) self.assertEqual(logging.getLevelName("info"), logger.INFO) self.assertEqual(logging.getLevelName("debug"), logger.DEBUG) self.assertEqual(logging.getLevelName("trace"), logger.TRACE) self.assertEqual(logging.getLevelName("NONE"), logger.NONE) self.assertEqual(logging.getLevelName("CRITICAL"), logger.CRITICAL) self.assertEqual(logging.getLevelName("ERROR"), logger.ERROR) self.assertEqual(logging.getLevelName("WARNING"), logger.WARNING) self.assertEqual(logging.getLevelName("INFO"), logger.INFO) self.assertEqual(logging.getLevelName("DEBUG"), logger.DEBUG) self.assertEqual(logging.getLevelName("TRACE"), logger.TRACE) def test_level(self): log, output = self._new_logger() logger.root.setLevel("info") log.debug("test") self.assertEqual(output.tell(), 0) logger.root.setLevel("debug") log.debug("test") self.assertNotEqual(output.tell(), 0) def test_level_none(self): log, output = self._new_logger() logger.root.setLevel("none") log.critical("test") log.error("test") log.warning("test") log.info("test") log.debug("test") log.trace("test") self.assertEqual(output.tell(), 0) def test_output(self): log, output = self._new_logger() logger.root.setLevel("debug") log.debug("test") self.assertEqual(output.getvalue(), "[test][debug] test\n") def test_trace_output(self): log, output = self._new_logger() logger.root.setLevel("trace") log.trace("test") self.assertEqual(output.getvalue(), "[test][trace] test\n") def test_trace_no_output(self): log, output = self._new_logger() logger.root.setLevel("debug") log.trace("test") self.assertEqual(output.getvalue(), "") def test_debug_out_at_trace(self): log, output = self._new_logger() logger.root.setLevel("trace") log.debug("test") self.assertEqual(output.getvalue(), "[test][debug] test\n") def test_style_percent(self): log, output = self._new_logger(style="%", format="[%(name)s][%(levelname)s] %(message)s") logger.root.setLevel("info") log.info("test") self.assertEqual(output.getvalue(), "[test][info] test\n") def test_style_invalid(self): with self.assertRaises(ValueError) as cm: self._new_logger(style="invalid") self.assertEqual(str(cm.exception), "Only {} and % formatting styles are supported") def test_datefmt_default(self): with freezegun.freeze_time(datetime(2000, 1, 2, 3, 4, 5, 123456), tz_offset=0): log, output = self._new_logger(format="[{asctime}][{name}][{levelname}] {message}") logger.root.setLevel("info") log.info("test") self.assertEqual(output.getvalue(), "[03:04:05][test][info] test\n") def test_datefmt_custom(self): with freezegun.freeze_time(datetime(2000, 1, 2, 3, 4, 5, 123456), tz_offset=0): log, output = self._new_logger(format="[{asctime}][{name}][{levelname}] {message}", datefmt="%H:%M:%S.%f") logger.root.setLevel("info") log.info("test") self.assertEqual(output.getvalue(), "[03:04:05.123456][test][info] test\n") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/test_meta.py0000644000175100001710000000123200000000000016724 0ustar00runnerdockerimport unittest import warnings from tests import catch_warnings class TestMeta(unittest.TestCase): """ Meta tests, to test the tests or test utils """ def test_catch_warnings(self): @catch_warnings() def _assert_false(): assert False self.assertRaises(AssertionError, _assert_false) def test_catch_warnings_record(self): @catch_warnings(record=True) def _includes_warnings(w): def _inner(): warnings.warn("a warning") _inner() self.assertEqual(1, len(w)) return True self.assertEqual(True, _includes_warnings()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/test_options.py0000644000175100001710000001670100000000000017500 0ustar00runnerdockerimport argparse import unittest from unittest.mock import Mock, patch from streamlink.options import Argument, Arguments, Options from streamlink_cli.main import setup_plugin_args, setup_plugin_options class TestOptions(unittest.TestCase): def setUp(self): self.options = Options({ "a_default": "default", "another-default": "default2" }) def test_options(self): self.assertEqual(self.options.get("a_default"), "default") self.assertEqual(self.options.get("non_existing"), None) self.options.set("a_option", "option") self.assertEqual(self.options.get("a_option"), "option") def test_options_update(self): self.assertEqual(self.options.get("a_default"), "default") self.assertEqual(self.options.get("non_existing"), None) self.options.update({"a_option": "option"}) self.assertEqual(self.options.get("a_option"), "option") def test_options_name_normalised(self): self.assertEqual(self.options.get("a_default"), "default") self.assertEqual(self.options.get("a-default"), "default") self.assertEqual(self.options.get("another-default"), "default2") self.assertEqual(self.options.get("another_default"), "default2") class TestArgument(unittest.TestCase): def test_name(self): self.assertEqual(Argument("test-arg").argument_name("plugin"), "--plugin-test-arg") self.assertEqual(Argument("test-arg").namespace_dest("plugin"), "plugin_test_arg") self.assertEqual(Argument("test-arg").dest, "test_arg") def test_name_plugin(self): self.assertEqual(Argument("test-arg").argument_name("test_plugin"), "--test-plugin-test-arg") self.assertEqual(Argument("test-arg").namespace_dest("test_plugin"), "test_plugin_test_arg") self.assertEqual(Argument("test-arg").dest, "test_arg") def test_name_override(self): self.assertEqual(Argument("test", argument_name="override-name").argument_name("plugin"), "--override-name") self.assertEqual(Argument("test", argument_name="override-name").namespace_dest("plugin"), "override_name") self.assertEqual(Argument("test", argument_name="override-name").dest, "test") class TestArguments(unittest.TestCase): def test_getter(self): test1 = Argument("test1") test2 = Argument("test2") args = Arguments(test1, test2) self.assertEqual(args.get("test1"), test1) self.assertEqual(args.get("test2"), test2) self.assertEqual(args.get("test3"), None) def test_iter(self): test1 = Argument("test1") test2 = Argument("test2") args = Arguments(test1, test2) i_args = iter(args) self.assertEqual(next(i_args), test1) self.assertEqual(next(i_args), test2) def test_requires(self): test1 = Argument("test1", requires="test2") test2 = Argument("test2", requires="test3") test3 = Argument("test3") args = Arguments(test1, test2, test3) self.assertEqual(list(args.requires("test1")), [test2, test3]) def test_requires_invalid(self): test1 = Argument("test1", requires="test2") args = Arguments(test1) self.assertRaises(KeyError, lambda: list(args.requires("test1"))) def test_requires_cycle(self): test1 = Argument("test1", requires="test2") test2 = Argument("test2", requires="test1") args = Arguments(test1, test2) self.assertRaises(RuntimeError, lambda: list(args.requires("test1"))) def test_requires_cycle_deep(self): test1 = Argument("test1", requires="test-2") test2 = Argument("test-2", requires="test3") test3 = Argument("test3", requires="test1") args = Arguments(test1, test2, test3) self.assertRaises(RuntimeError, lambda: list(args.requires("test1"))) def test_requires_cycle_self(self): test1 = Argument("test1", requires="test1") args = Arguments(test1) self.assertRaises(RuntimeError, lambda: list(args.requires("test1"))) class TestSetupOptions(unittest.TestCase): def test_setup_plugin_args(self): session = Mock() plugin = Mock() parser = argparse.ArgumentParser(add_help=False) parser.add_argument("--global-arg1", default=123) parser.add_argument("--global-arg2", default=456) session.plugins = {"mock": plugin} plugin.arguments = Arguments( Argument("global-arg1", is_global=True), Argument("test1", default="default1"), Argument("test2", default="default2"), Argument("test3") ) setup_plugin_args(session, parser) group_plugins = next((grp for grp in parser._action_groups if grp.title == "Plugin options"), None) # pragma: no branch self.assertIsNotNone(group_plugins, "Adds the 'Plugin options' arguments group") group_plugin = next((grp for grp in group_plugins._action_groups if grp.title == "Mock"), None) # pragma: no branch self.assertIsNotNone(group_plugin, "Adds the 'Mock' arguments group to the 'Plugin options' group") self.assertEqual( [item for action in group_plugin._group_actions for item in action.option_strings], ["--mock-test1", "--mock-test2", "--mock-test3"], "Only adds plugin arguments and ignores global argument references" ) self.assertEqual( [item for action in parser._actions for item in action.option_strings], ["--global-arg1", "--global-arg2", "--mock-test1", "--mock-test2", "--mock-test3"], "Parser has all arguments registered" ) self.assertEqual(plugin.options.get("global-arg1"), 123) self.assertEqual(plugin.options.get("global-arg2"), None) self.assertEqual(plugin.options.get("test1"), "default1") self.assertEqual(plugin.options.get("test2"), "default2") self.assertEqual(plugin.options.get("test3"), None) def test_setup_plugin_options(self): session = Mock() plugin = Mock(module="plugin") parser = argparse.ArgumentParser() parser.add_argument("--foo-foo", default=123) session.plugins = {"plugin": plugin} session.set_plugin_option = lambda name, key, value: session.plugins[name].options.update({key: value}) plugin.arguments = Arguments( Argument("foo-foo", is_global=True), Argument("bar-bar", default=456), Argument("baz-baz", default=789, help=argparse.SUPPRESS) ) with patch("streamlink_cli.main.args") as args: args.foo_foo = 321 args.plugin_bar_bar = 654 args.plugin_baz_baz = 987 # this wouldn't be set by the parser if the argument is suppressed setup_plugin_args(session, parser) self.assertEqual(plugin.options.get("foo_foo"), 123, "Sets the global-argument's default value") self.assertEqual(plugin.options.get("bar_bar"), 456, "Sets the plugin-argument's default value") self.assertEqual(plugin.options.get("baz_baz"), 789, "Sets the suppressed plugin-argument's default value") setup_plugin_options(session, plugin) self.assertEqual(plugin.options.get("foo_foo"), 321, "Sets the provided global-argument value") self.assertEqual(plugin.options.get("bar_bar"), 654, "Sets the provided plugin-argument value") self.assertEqual(plugin.options.get("baz_baz"), 789, "Doesn't set values of suppressed plugin-arguments") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/test_output.py0000644000175100001710000001354300000000000017346 0ustar00runnerdockerimport ntpath import os import posixpath import unittest from pathlib import Path from unittest.mock import Mock, call, patch from streamlink_cli.output import FileOutput, PlayerOutput from tests import posix_only, windows_only @patch("streamlink_cli.output.stdout") class TestFileOutput(unittest.TestCase): @staticmethod def subject(filename, fd): fo_record = FileOutput(fd=fd) fo_main = FileOutput(filename=filename, record=fo_record) return fo_main, fo_record def test_init(self, mock_stdout: Mock): mock_path = Mock(spec=Path("foo", "bar")) fo_main, fo_record = self.subject(mock_path, mock_stdout) self.assertEqual(fo_main.opened, False) self.assertIs(fo_main.filename, mock_path) self.assertIs(fo_main.fd, None) self.assertIs(fo_main.record, fo_record) self.assertEqual(fo_main.record.opened, False) self.assertIs(fo_main.record.filename, None) self.assertIs(fo_main.record.fd, mock_stdout) self.assertIs(fo_main.record.record, None) def test_early_close(self, mock_stdout: Mock): mock_path = Mock(spec=Path("foo", "bar")) fo_main, fo_record = self.subject(mock_path, mock_stdout) fo_main.close() # doesn't raise def test_early_write(self, mock_stdout: Mock): mock_path = Mock(spec=Path("foo", "bar")) fo_main, fo_record = self.subject(mock_path, mock_stdout) with self.assertRaises(OSError) as cm: fo_main.write(b"foo") self.assertEqual(str(cm.exception), "Output is not opened") def _test_open(self, mock_open: Mock, mock_stdout: Mock): mock_path = Mock(spec=Path("foo", "bar")) mock_fd = mock_open(mock_path, "wb") fo_main, fo_record = self.subject(mock_path, mock_stdout) fo_main.open() self.assertEqual(fo_main.opened, True) self.assertEqual(fo_main.record.opened, True) self.assertEqual(mock_path.parent.mkdir.call_args_list, [call(parents=True, exist_ok=True)]) self.assertIs(fo_main.fd, mock_fd) fo_main.write(b"foo") self.assertEqual(mock_fd.write.call_args_list, [call(b"foo")]) self.assertEqual(mock_stdout.write.call_args_list, [call(b"foo")]) fo_main.close() self.assertEqual(mock_fd.close.call_args_list, [call()]) self.assertEqual(mock_stdout.close.call_args_list, []) self.assertEqual(fo_main.opened, False) self.assertEqual(fo_main.record.opened, False) return mock_path @posix_only @patch("builtins.open") def test_open_posix(self, mock_open: Mock, mock_stdout: Mock): self._test_open(mock_open, mock_stdout) @windows_only @patch("streamlink_cli.output.msvcrt") @patch("builtins.open") def test_open_windows(self, mock_open: Mock, mock_msvcrt: Mock, mock_stdout: Mock): mock_path = self._test_open(mock_open, mock_stdout) self.assertEqual(mock_msvcrt.setmode.call_args_list, [ call(mock_stdout.fileno(), os.O_BINARY), call(mock_open(mock_path, "wb").fileno(), os.O_BINARY), ]) class TestPlayerOutput(unittest.TestCase): def test_supported_player_generic(self): self.assertEqual("vlc", PlayerOutput.supported_player("vlc")) self.assertEqual("mpv", PlayerOutput.supported_player("mpv")) self.assertEqual("potplayer", PlayerOutput.supported_player("potplayermini.exe")) @patch("streamlink_cli.output.os.path.basename", new=ntpath.basename) def test_supported_player_win32(self): self.assertEqual("mpv", PlayerOutput.supported_player("C:\\MPV\\mpv.exe")) self.assertEqual("vlc", PlayerOutput.supported_player("C:\\VLC\\vlc.exe")) self.assertEqual("potplayer", PlayerOutput.supported_player("C:\\PotPlayer\\PotPlayerMini64.exe")) @patch("streamlink_cli.output.os.path.basename", new=posixpath.basename) def test_supported_player_posix(self): self.assertEqual("mpv", PlayerOutput.supported_player("/usr/bin/mpv")) self.assertEqual("vlc", PlayerOutput.supported_player("/usr/bin/vlc")) @patch("streamlink_cli.output.os.path.basename", new=ntpath.basename) def test_supported_player_args_win32(self): self.assertEqual("mpv", PlayerOutput.supported_player("C:\\MPV\\mpv.exe --argh")) self.assertEqual("vlc", PlayerOutput.supported_player("C:\\VLC\\vlc.exe --argh")) self.assertEqual("potplayer", PlayerOutput.supported_player("C:\\PotPlayer\\PotPlayerMini64.exe --argh")) @patch("streamlink_cli.output.os.path.basename", new=posixpath.basename) def test_supported_player_args_posix(self): self.assertEqual("mpv", PlayerOutput.supported_player("/usr/bin/mpv --argh")) self.assertEqual("vlc", PlayerOutput.supported_player("/usr/bin/vlc --argh")) @patch("streamlink_cli.output.os.path.basename", new=posixpath.basename) def test_supported_player_negative_posix(self): self.assertEqual(None, PlayerOutput.supported_player("/usr/bin/xmpvideo")) self.assertEqual(None, PlayerOutput.supported_player("/usr/bin/echo")) @patch("streamlink_cli.output.os.path.basename", new=ntpath.basename) def test_supported_player_negative_win32(self): self.assertEqual(None, PlayerOutput.supported_player("C:\\mpc\\mpc-hd.exe")) self.assertEqual(None, PlayerOutput.supported_player("C:\\mplayer\\not-vlc.exe")) self.assertEqual(None, PlayerOutput.supported_player("C:\\NotPlayer\\NotPlayerMini64.exe")) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/test_plugin.py0000644000175100001710000002114600000000000017302 0ustar00runnerdockerimport datetime import re import time import unittest from unittest.mock import Mock, call, patch import freezegun import pytest import requests.cookies from streamlink.plugin import HIGH_PRIORITY, NORMAL_PRIORITY, Plugin, pluginmatcher from streamlink.plugin.plugin import Matcher class FakePlugin(Plugin): def _get_streams(self): pass # pragma: no cover class TestPlugin(unittest.TestCase): def _create_cookie_dict(self, name, value, expires): return {'version': 0, 'name': name, 'value': value, 'port': None, 'domain': "test.se", 'path': "/", 'secure': False, 'expires': expires, 'discard': True, 'comment': None, 'comment_url': None, 'rest': {"HttpOnly": None}, 'rfc2109': False} def _cookie_to_dict(self, cookie): r = {} for name in ("version", "name", "value", "port", "domain", "path", "secure", "expires", "discard", "comment", "comment_url"): r[name] = getattr(cookie, name, None) r["rest"] = getattr(cookie, "rest", getattr(cookie, "_rest", None)) return r def tearDown(self): Plugin.session = None Plugin.cache = None Plugin.module = None Plugin.logger = None def test_cookie_store_save(self): session = Mock() session.http.cookies = [ requests.cookies.create_cookie("test-name", "test-value", domain="test.se") ] Plugin.bind(session, 'tests.test_plugin') Plugin.cache = Mock() Plugin.cache.get_all.return_value = {} plugin = Plugin("http://test.se") plugin.save_cookies(default_expires=3600) Plugin.cache.set.assert_called_with("__cookie:test-name:test.se:80:/", self._create_cookie_dict("test-name", "test-value", None), 3600) def test_cookie_store_save_expires(self): with freezegun.freeze_time(datetime.datetime(2018, 1, 1)): session = Mock() session.http.cookies = [ requests.cookies.create_cookie("test-name", "test-value", domain="test.se", expires=time.time() + 3600, rest={'HttpOnly': None}) ] Plugin.bind(session, 'tests.test_plugin') Plugin.cache = Mock() Plugin.cache.get_all.return_value = {} plugin = Plugin("http://test.se") plugin.save_cookies(default_expires=60) Plugin.cache.set.assert_called_with("__cookie:test-name:test.se:80:/", self._create_cookie_dict("test-name", "test-value", 1514768400), 3600) def test_cookie_store_load(self): session = Mock() session.http.cookies = requests.cookies.RequestsCookieJar() Plugin.bind(session, 'tests.test_plugin') Plugin.cache = Mock() Plugin.cache.get_all.return_value = { "__cookie:test-name:test.se:80:/": self._create_cookie_dict("test-name", "test-value", None) } Plugin("http://test.se") self.assertSequenceEqual( list(map(self._cookie_to_dict, session.http.cookies)), [self._cookie_to_dict(requests.cookies.create_cookie("test-name", "test-value", domain="test.se"))] ) def test_cookie_store_clear(self): session = Mock() session.http.cookies = requests.cookies.RequestsCookieJar() Plugin.bind(session, 'tests.test_plugin') Plugin.cache = Mock() Plugin.cache.get_all.return_value = { "__cookie:test-name:test.se:80:/": self._create_cookie_dict("test-name", "test-value", None), "__cookie:test-name2:test.se:80:/": self._create_cookie_dict("test-name2", "test-value2", None) } plugin = Plugin("http://test.se") # non-empty cookiejar self.assertTrue(len(session.http.cookies.get_dict()) > 0) plugin.clear_cookies() self.assertSequenceEqual( Plugin.cache.set.mock_calls, [call("__cookie:test-name:test.se:80:/", None, 0), call("__cookie:test-name2:test.se:80:/", None, 0)]) self.assertSequenceEqual(session.http.cookies, []) def test_cookie_store_clear_filter(self): session = Mock() session.http.cookies = requests.cookies.RequestsCookieJar() Plugin.bind(session, 'tests.test_plugin') Plugin.cache = Mock() Plugin.cache.get_all.return_value = { "__cookie:test-name:test.se:80:/": self._create_cookie_dict("test-name", "test-value", None), "__cookie:test-name2:test.se:80:/": self._create_cookie_dict("test-name2", "test-value2", None) } plugin = Plugin("http://test.se") # non-empty cookiejar self.assertTrue(len(session.http.cookies.get_dict()) > 0) plugin.clear_cookies(lambda c: c.name.endswith("2")) self.assertSequenceEqual( Plugin.cache.set.mock_calls, [call("__cookie:test-name2:test.se:80:/", None, 0)]) self.assertSequenceEqual( list(map(self._cookie_to_dict, session.http.cookies)), [self._cookie_to_dict(requests.cookies.create_cookie("test-name", "test-value", domain="test.se"))] ) def test_cookie_load_unbound(self): plugin = Plugin("http://test.se") with self.assertRaises(RuntimeError) as cm: plugin.load_cookies() self.assertEqual(str(cm.exception), "Cannot load cached cookies in unbound plugin") def test_cookie_save_unbound(self): plugin = Plugin("http://test.se") with self.assertRaises(RuntimeError) as cm: plugin.save_cookies() self.assertEqual(str(cm.exception), "Cannot cache cookies in unbound plugin") def test_cookie_clear_unbound(self): plugin = Plugin("http://test.se") with self.assertRaises(RuntimeError) as cm: plugin.clear_cookies() self.assertEqual(str(cm.exception), "Cannot clear cached cookies in unbound plugin") class TestPluginMatcher(unittest.TestCase): @patch("builtins.repr", Mock(return_value="Foo")) def test_decorator(self): with self.assertRaises(TypeError) as cm: @pluginmatcher(re.compile("")) class Foo: pass self.assertEqual(str(cm.exception), "Foo is not a Plugin") @pluginmatcher(re.compile("foo", re.VERBOSE)) @pluginmatcher(re.compile("bar"), priority=HIGH_PRIORITY) class Bar(FakePlugin): pass self.assertEqual(Bar.matchers, [ Matcher(re.compile("foo", re.VERBOSE), NORMAL_PRIORITY), Matcher(re.compile("bar"), HIGH_PRIORITY) ]) def test_url_setter(self): @pluginmatcher(re.compile("http://(foo)")) @pluginmatcher(re.compile("http://(bar)")) @pluginmatcher(re.compile("http://(baz)")) class MyPlugin(FakePlugin): pass MyPlugin.bind(Mock(), "tests.test_plugin") plugin = MyPlugin("http://foo") self.assertEqual(plugin.url, "http://foo") self.assertEqual([m is not None for m in plugin.matches], [True, False, False]) self.assertEqual(plugin.matcher, plugin.matchers[0].pattern) self.assertEqual(plugin.match.group(1), "foo") plugin.url = "http://bar" self.assertEqual(plugin.url, "http://bar") self.assertEqual([m is not None for m in plugin.matches], [False, True, False]) self.assertEqual(plugin.matcher, plugin.matchers[1].pattern) self.assertEqual(plugin.match.group(1), "bar") plugin.url = "http://baz" self.assertEqual(plugin.url, "http://baz") self.assertEqual([m is not None for m in plugin.matches], [False, False, True]) self.assertEqual(plugin.matcher, plugin.matchers[2].pattern) self.assertEqual(plugin.match.group(1), "baz") plugin.url = "http://qux" self.assertEqual(plugin.url, "http://qux") self.assertEqual([m is not None for m in plugin.matches], [False, False, False]) self.assertEqual(plugin.matcher, None) self.assertEqual(plugin.match, None) @pytest.mark.parametrize("attr", ["id", "author", "category", "title"]) def test_plugin_metadata(attr): plugin = FakePlugin("https://foo.bar/") getter = getattr(plugin, f"get_{attr}") assert callable(getter) assert getattr(plugin, attr) is None assert getter() is None setattr(plugin, attr, " foo bar ") assert getter() == "foo bar" class Foo: def __str__(self): return " baz qux " setattr(plugin, attr, Foo()) assert getter() == "baz qux" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/test_plugin_utils.py0000644000175100001710000000732500000000000020525 0ustar00runnerdockerimport sys import unittest from streamlink.plugin.api.utils import itertags def unsupported_versions_1979(): """Unsupported python versions for itertags 3.7.0 - 3.7.2 and 3.8.0a1 - https://github.com/streamlink/streamlink/issues/1979 - https://bugs.python.org/issue34294 """ v = sys.version_info return (v.major == 3) and ( # 3.7.0 - 3.7.2 (v.minor == 7 and v.micro <= 2) # 3.8.0a1 or (v.minor == 8 and v.micro == 0 and v.releaselevel == 'alpha' and v.serial <= 1) ) class TestPluginUtil(unittest.TestCase): test_html = """ Title

bar

""" # noqa: W291 def test_itertags_single_text(self): title = list(itertags(self.test_html, "title")) self.assertTrue(len(title), 1) self.assertEqual(title[0].tag, "title") self.assertEqual(title[0].text, "Title") self.assertEqual(title[0].attributes, {}) def test_itertags_attrs_text(self): script = list(itertags(self.test_html, "script")) self.assertTrue(len(script), 2) self.assertEqual(script[0].tag, "script") self.assertEqual(script[0].text, "") self.assertEqual(script[0].attributes, {"src": "https://test.se/test.js"}) self.assertEqual(script[1].tag, "script") self.assertEqual(script[1].text.strip(), """Tester.ready(function () {\nalert("Hello, world!"); });""") self.assertEqual(script[1].attributes, {}) @unittest.skipIf(unsupported_versions_1979(), "python3.7 issue, see bpo-34294") def test_itertags_multi_attrs(self): metas = list(itertags(self.test_html, "meta")) self.assertTrue(len(metas), 3) self.assertTrue(all(meta.tag == "meta" for meta in metas)) self.assertEqual(metas[0].text, None) self.assertEqual(metas[1].text, None) self.assertEqual(metas[2].text, None) self.assertEqual(metas[0].attributes, {"property": "og:type", "content": "website"}) self.assertEqual(metas[1].attributes, {"property": "og:url", "content": "http://test.se/"}) self.assertEqual(metas[2].attributes, {"property": "og:site_name", "content": "Test"}) def test_multi_line_a(self): anchor = list(itertags(self.test_html, "a")) self.assertTrue(len(anchor), 1) self.assertEqual(anchor[0].tag, "a") self.assertEqual(anchor[0].text, "bar") self.assertEqual(anchor[0].attributes, {"href": "http://test.se/foo"}) @unittest.skipIf(unsupported_versions_1979(), "python3.7 issue, see bpo-34294") def test_no_end_tag(self): links = list(itertags(self.test_html, "link")) self.assertTrue(len(links), 1) self.assertEqual(links[0].tag, "link") self.assertEqual(links[0].text, None) self.assertEqual(links[0].attributes, {"rel": "stylesheet", "type": "text/css", "href": "https://test.se/test.css"}) def test_tag_inner_tag(self): links = list(itertags(self.test_html, "p")) self.assertTrue(len(links), 1) self.assertEqual(links[0].tag, "p") self.assertEqual(links[0].text.strip(), 'bar') self.assertEqual(links[0].attributes, {}) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/test_plugins.py0000644000175100001710000000374600000000000017473 0ustar00runnerdockerimport os.path import pkgutil import unittest import streamlink.plugins from streamlink.plugin.plugin import Matcher, Plugin from streamlink.utils.module import load_module class PluginTestMeta(type): def __new__(mcs, name, bases, dict): plugin_path = os.path.dirname(streamlink.plugins.__file__) def gentest(plugin): def load_plugin_test(self): assert hasattr(plugin, "__plugin__"), "It exports __plugin__" pluginclass = plugin.__plugin__ assert issubclass(plugin.__plugin__, Plugin), "__plugin__ is an instance of the Plugin class" classname = pluginclass.__name__ assert classname == classname[0].upper() + classname[1:], "__plugin__ class name starts with uppercase letter" assert "_" not in classname, "__plugin__ class name does not contain underscores" assert isinstance(pluginclass.matchers, list) and len(pluginclass.matchers) > 0, "Has at least one matcher" assert all(isinstance(matcher, Matcher) for matcher in pluginclass.matchers), "Only has valid matchers" assert not hasattr(pluginclass, "can_handle_url"), "Does not implement deprecated can_handle_url(url)" assert not hasattr(pluginclass, "priority"), "Does not implement deprecated priority(url)" assert callable(pluginclass._get_streams), "Implements _get_streams()" return load_plugin_test pname: str for finder, pname, ispkg in pkgutil.iter_modules([plugin_path]): if pname.startswith("common_"): continue plugin_module = load_module(f"streamlink.plugins.{pname}", plugin_path) dict[f"test_{pname}_load"] = gentest(plugin_module) return type.__new__(mcs, name, bases, dict) class TestPlugins(unittest.TestCase, metaclass=PluginTestMeta): """ Test that each plugin can be loaded and does not fail when calling can_handle_url. """ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/test_plugins_input.py0000644000175100001710000000520000000000000020675 0ustar00runnerdockerimport os.path import unittest from contextlib import contextmanager from unittest.mock import MagicMock, patch from streamlink import PluginError, Streamlink from streamlink.plugin.plugin import UserInputRequester from streamlink_cli.console import ConsoleUserInputRequester from tests.plugin.testplugin import TestPlugin as _TestPlugin class TestPluginUserInput(unittest.TestCase): def setUp(self): self.session = Streamlink() @contextmanager def _mock_console_input(self, isatty=True): with patch('streamlink_cli.console.sys.stdin.isatty', return_value=isatty): mock_console = MagicMock() mock_console.ask.return_value = "username" mock_console.askpass.return_value = "password" yield ConsoleUserInputRequester(mock_console) def test_user_input_bad_class(self): p = _TestPlugin("http://example.com/stream") self.assertRaises(RuntimeError, p.bind, self.session, 'test_plugin', object()) def test_user_input_not_implemented(self): p = _TestPlugin("http://example.com/stream") p.bind(self.session, 'test_plugin', UserInputRequester()) self.assertRaises(PluginError, p.input_ask, 'test') self.assertRaises(PluginError, p.input_ask_password, 'test') def test_user_input_console(self): p = _TestPlugin("http://example.com/stream") with self._mock_console_input() as console_input: p.bind(self.session, 'test_plugin', console_input) self.assertEqual("username", p.input_ask("username")) self.assertEqual("password", p.input_ask_password("password")) console_input.console.ask.assert_called_with("username: ") console_input.console.askpass.assert_called_with("password: ") def test_user_input_console_no_tty(self): p = _TestPlugin("http://example.com/stream") with self._mock_console_input(isatty=False) as console_input: p.bind(self.session, 'test_plugin', console_input) self.assertRaises(PluginError, p.input_ask, "username") self.assertRaises(PluginError, p.input_ask_password, "password") def test_set_via_session(self): with self._mock_console_input() as console_input: session = Streamlink({"user-input-requester": console_input}) session.load_plugins(os.path.join(os.path.dirname(__file__), "plugin")) pluginclass, resolved_url = session.resolve_url("http://test.se/channel") p = pluginclass(resolved_url) self.assertEqual("username", p.input_ask("username")) self.assertEqual("password", p.input_ask_password("password")) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/test_plugins_meta.py0000644000175100001710000000622400000000000020473 0ustar00runnerdockerimport os.path import re import unittest from glob import glob from streamlink import Streamlink, plugins as streamlinkplugins from streamlink_cli.argparser import build_parser class TestPluginMeta(unittest.TestCase): """ Test that each plugin has an entry in the plugin matrix and a test file """ longMessage = False protocol_tests = ["http", "hls", "dash", "stream"] @classmethod def setUpClass(cls): cls.session = Streamlink() docs_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "../docs")) plugins_dir = streamlinkplugins.__path__[0] with open(os.path.join(docs_dir, "plugin_matrix.rst")) as plfh: parts = re.split(r"\n[= ]+\n", plfh.read()) cls.plugins_in_docs = list(re.findall(r"^([\w_]+)\s", parts[3], re.MULTILINE)) with open(os.path.join(plugins_dir, ".removed")) as rmfh: cls.plugins_removed = [pname for pname in rmfh.read().split("\n") if pname and not pname.startswith("#")] tests_plugins_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "plugins")) tests_plugin_files = glob(os.path.join(tests_plugins_dir, "test_*.py")) cls.plugins = cls.session.plugins.keys() cls.plugin_tests = [re.sub(r"^test_(.+)\.py$", r"\1", os.path.basename(file)) for file in tests_plugin_files] cls.plugins_no_protocols = [pname for pname in cls.plugins if pname not in cls.protocol_tests] cls.plugin_tests_no_protocols = [pname for pname in cls.plugin_tests if pname not in cls.protocol_tests] def test_plugin_has_docs_matrix(self): for pname in self.plugins_no_protocols: self.assertIn(pname, self.plugins_in_docs, f"{pname} is not in plugin matrix") def test_docs_matrix_has_plugin(self): for pname in self.plugins_in_docs: self.assertIn(pname, self.plugins_no_protocols, f"{pname} plugin does not exist") def test_plugin_has_tests(self): for pname in self.plugins_no_protocols: self.assertIn(pname, self.plugin_tests, f"{pname} has no tests") def test_unknown_plugin_has_tests(self): for pname in self.plugin_tests_no_protocols: self.assertIn(pname, self.plugins_no_protocols, f"{pname} is not a plugin but has tests") def test_plugin_not_in_removed_list(self): for pname in self.plugins: self.assertNotIn(pname, self.plugins_removed, f"{pname} is in removed plugins list") def test_removed_list_is_sorted(self): plugins_removed_sorted = self.plugins_removed.copy() plugins_removed_sorted.sort() self.assertEqual(self.plugins_removed, plugins_removed_sorted, "Removed plugins list is not sorted alphabetically") def test_plugin_has_valid_global_args(self): parser = build_parser() global_arg_dests = [action.dest for action in parser._actions] for pname, plugin in self.session.plugins.items(): for parg in plugin.arguments: if not parg.is_global: # pragma: no cover continue self.assertIn(parg.dest, global_arg_dests, f"{parg.name} from plugins.{pname} is not a valid global argument") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/test_session.py0000644000175100001710000004460300000000000017472 0ustar00runnerdockerimport os import re import unittest from socket import AF_INET, AF_INET6 from unittest.mock import Mock, call, patch import requests_mock from requests.packages.urllib3.util.connection import allowed_gai_family from streamlink import NoPluginError, Streamlink from streamlink.plugin import HIGH_PRIORITY, LOW_PRIORITY, NORMAL_PRIORITY, NO_PRIORITY, Plugin, pluginmatcher from streamlink.stream.hls import HLSStream from streamlink.stream.http import HTTPStream class EmptyPlugin(Plugin): def _get_streams(self): pass # pragma: no cover class TestSession(unittest.TestCase): mocker: requests_mock.Mocker plugin_path = os.path.join(os.path.dirname(__file__), "plugin") def setUp(self): self.mocker = requests_mock.Mocker() self.mocker.register_uri(requests_mock.ANY, requests_mock.ANY, text="") self.mocker.start() def tearDown(self): self.mocker.stop() Streamlink.resolve_url.cache_clear() def subject(self, load_plugins=True): session = Streamlink() if load_plugins: session.load_plugins(self.plugin_path) return session @staticmethod def _resolve_url(method, *args, **kwargs) -> Plugin: pluginclass, resolved_url = method(*args, **kwargs) return pluginclass(resolved_url) def resolve_url(self, session: Streamlink, url: str, *args, **kwargs) -> Plugin: return self._resolve_url(session.resolve_url, url, *args, **kwargs) def resolve_url_no_redirect(self, session: Streamlink, url: str, *args, **kwargs) -> Plugin: return self._resolve_url(session.resolve_url_no_redirect, url, *args, **kwargs) # ---- def test_load_plugins(self): session = self.subject() plugins = session.get_plugins() self.assertIn("testplugin", plugins) self.assertNotIn("testplugin_missing", plugins) self.assertNotIn("testplugin_invalid", plugins) def test_load_plugins_builtin(self): session = self.subject() plugins = session.get_plugins() self.assertIn("twitch", plugins) self.assertEqual(plugins["twitch"].__module__, "streamlink.plugins.twitch") @patch("streamlink.session.log") def test_load_plugins_override(self, mock_log): session = self.subject() plugins = session.get_plugins() file = os.path.join(os.path.dirname(__file__), "plugin", "testplugin_override.py") self.assertIn("testplugin", plugins) self.assertNotIn("testplugin_override", plugins) self.assertEqual(plugins["testplugin"].__name__, "TestPluginOverride") self.assertEqual(plugins["testplugin"].__module__, "streamlink.plugins.testplugin_override") self.assertEqual(mock_log.debug.mock_calls, [call(f"Plugin testplugin is being overridden by {file}")]) @patch("streamlink.session.load_module") @patch("streamlink.session.log") def test_load_plugins_importerror(self, mock_log, mock_load_module): mock_load_module.side_effect = ImportError() session = self.subject() plugins = session.get_plugins() self.assertGreater(len(mock_log.exception.mock_calls), 0) self.assertEqual(len(plugins.keys()), 0) @patch("streamlink.session.load_module") @patch("streamlink.session.log") def test_load_plugins_syntaxerror(self, mock_log, mock_load_module): mock_load_module.side_effect = SyntaxError() with self.assertRaises(SyntaxError): self.subject() def test_resolve_url(self): session = self.subject() plugins = session.get_plugins() pluginclass, resolved_url = session.resolve_url("http://test.se/channel") self.assertTrue(issubclass(pluginclass, Plugin)) self.assertIs(pluginclass, plugins["testplugin"]) self.assertEqual(resolved_url, "http://test.se/channel") self.assertTrue(hasattr(session.resolve_url, "cache_info"), "resolve_url has a lookup cache") def test_resolve_url__noplugin(self): session = self.subject() self.mocker.get("http://invalid2", status_code=301, headers={"Location": "http://invalid3"}) self.assertRaises(NoPluginError, session.resolve_url, "http://invalid1") self.assertRaises(NoPluginError, session.resolve_url, "http://invalid2") def test_resolve_url__redirected(self): session = self.subject() plugins = session.get_plugins() self.mocker.head("http://redirect1", status_code=501) self.mocker.get("http://redirect1", status_code=301, headers={"Location": "http://redirect2"}) self.mocker.head("http://redirect2", status_code=301, headers={"Location": "http://test.se/channel"}) pluginclass, resolved_url = session.resolve_url("http://redirect1") self.assertTrue(issubclass(pluginclass, Plugin)) self.assertIs(pluginclass, plugins["testplugin"]) self.assertEqual(resolved_url, "http://test.se/channel") def test_resolve_url_no_redirect(self): session = self.subject() plugins = session.get_plugins() pluginclass, resolved_url = session.resolve_url_no_redirect("http://test.se/channel") self.assertTrue(issubclass(pluginclass, Plugin)) self.assertIs(pluginclass, plugins["testplugin"]) self.assertEqual(resolved_url, "http://test.se/channel") def test_resolve_url_no_redirect__noplugin(self): session = self.subject() self.assertRaises(NoPluginError, session.resolve_url_no_redirect, "http://invalid") def test_resolve_url_scheme(self): @pluginmatcher(re.compile("http://insecure")) class PluginHttp(EmptyPlugin): pass @pluginmatcher(re.compile("https://secure")) class PluginHttps(EmptyPlugin): pass session = self.subject(load_plugins=False) session.plugins = { "insecure": PluginHttp, "secure": PluginHttps, } self.assertRaises(NoPluginError, self.resolve_url, session, "insecure") self.assertIsInstance(self.resolve_url(session, "http://insecure"), PluginHttp) self.assertRaises(NoPluginError, self.resolve_url, session, "https://insecure") self.assertIsInstance(self.resolve_url(session, "secure"), PluginHttps) self.assertRaises(NoPluginError, self.resolve_url, session, "http://secure") self.assertIsInstance(self.resolve_url(session, "https://secure"), PluginHttps) def test_resolve_url_priority(self): @pluginmatcher(priority=HIGH_PRIORITY, pattern=re.compile( "https://(high|normal|low|no)$" )) class HighPriority(EmptyPlugin): pass @pluginmatcher(priority=NORMAL_PRIORITY, pattern=re.compile( "https://(normal|low|no)$" )) class NormalPriority(EmptyPlugin): pass @pluginmatcher(priority=LOW_PRIORITY, pattern=re.compile( "https://(low|no)$" )) class LowPriority(EmptyPlugin): pass @pluginmatcher(priority=NO_PRIORITY, pattern=re.compile( "https://(no)$" )) class NoPriority(EmptyPlugin): pass session = self.subject(load_plugins=False) session.plugins = { "high": HighPriority, "normal": NormalPriority, "low": LowPriority, "no": NoPriority, } no = self.resolve_url_no_redirect(session, "no") low = self.resolve_url_no_redirect(session, "low") normal = self.resolve_url_no_redirect(session, "normal") high = self.resolve_url_no_redirect(session, "high") self.assertIsInstance(no, HighPriority) self.assertIsInstance(low, HighPriority) self.assertIsInstance(normal, HighPriority) self.assertIsInstance(high, HighPriority) session.resolve_url.cache_clear() session.plugins = { "no": NoPriority, } with self.assertRaises(NoPluginError): self.resolve_url_no_redirect(session, "no") @patch("streamlink.session.log") def test_resolve_deprecated(self, mock_log: Mock): @pluginmatcher(priority=LOW_PRIORITY, pattern=re.compile( "https://low" )) class LowPriority(EmptyPlugin): pass class DeprecatedNormalPriority(EmptyPlugin): # noinspection PyUnusedLocal @classmethod def can_handle_url(cls, url): return True class DeprecatedHighPriority(DeprecatedNormalPriority): # noinspection PyUnusedLocal @classmethod def priority(cls, url): return HIGH_PRIORITY session = self.subject(load_plugins=False) session.plugins = { "empty": EmptyPlugin, "low": LowPriority, "dep-normal-one": DeprecatedNormalPriority, "dep-normal-two": DeprecatedNormalPriority, "dep-high": DeprecatedHighPriority, } self.assertIsInstance(self.resolve_url_no_redirect(session, "low"), DeprecatedHighPriority) self.assertEqual(mock_log.info.mock_calls, [ call("Resolved plugin dep-normal-one with deprecated can_handle_url API"), call("Resolved plugin dep-high with deprecated can_handle_url API") ]) def test_options(self): session = self.subject() session.set_option("test_option", "option") self.assertEqual(session.get_option("test_option"), "option") self.assertEqual(session.get_option("non_existing"), None) self.assertEqual(session.get_plugin_option("testplugin", "a_option"), "default") session.set_plugin_option("testplugin", "another_option", "test") self.assertEqual(session.get_plugin_option("testplugin", "another_option"), "test") self.assertEqual(session.get_plugin_option("non_existing", "non_existing"), None) self.assertEqual(session.get_plugin_option("testplugin", "non_existing"), None) def test_plugin(self): session = self.subject() plugin = self.resolve_url(session, "http://test.se/channel") streams = plugin.streams() self.assertTrue("best" in streams) self.assertTrue("worst" in streams) self.assertTrue(streams["best"] is streams["1080p"]) self.assertTrue(streams["worst"] is streams["350k"]) self.assertTrue(isinstance(streams["http"], HTTPStream)) self.assertTrue(isinstance(streams["hls"], HLSStream)) def test_plugin_stream_types(self): session = self.subject() plugin = self.resolve_url(session, "http://test.se/channel") streams = plugin.streams(stream_types=["http", "hls"]) self.assertTrue(isinstance(streams["480p"], HTTPStream)) self.assertTrue(isinstance(streams["480p_hls"], HLSStream)) streams = plugin.streams(stream_types=["hls", "http"]) self.assertTrue(isinstance(streams["480p"], HLSStream)) self.assertTrue(isinstance(streams["480p_http"], HTTPStream)) def test_plugin_stream_sorting_excludes(self): session = self.subject() plugin = self.resolve_url(session, "http://test.se/channel") streams = plugin.streams(sorting_excludes=[]) self.assertTrue("best" in streams) self.assertTrue("worst" in streams) self.assertFalse("best-unfiltered" in streams) self.assertFalse("worst-unfiltered" in streams) self.assertTrue(streams["worst"] is streams["350k"]) self.assertTrue(streams["best"] is streams["1080p"]) streams = plugin.streams(sorting_excludes=["1080p", "3000k"]) self.assertTrue("best" in streams) self.assertTrue("worst" in streams) self.assertFalse("best-unfiltered" in streams) self.assertFalse("worst-unfiltered" in streams) self.assertTrue(streams["worst"] is streams["350k"]) self.assertTrue(streams["best"] is streams["1500k"]) streams = plugin.streams(sorting_excludes=[">=1080p", ">1500k"]) self.assertTrue(streams["best"] is streams["1500k"]) streams = plugin.streams(sorting_excludes=lambda q: not q.endswith("p")) self.assertTrue(streams["best"] is streams["3000k"]) streams = plugin.streams(sorting_excludes=lambda q: False) self.assertFalse("best" in streams) self.assertFalse("worst" in streams) self.assertTrue("best-unfiltered" in streams) self.assertTrue("worst-unfiltered" in streams) self.assertTrue(streams["worst-unfiltered"] is streams["350k"]) self.assertTrue(streams["best-unfiltered"] is streams["1080p"]) plugin = self.resolve_url(session, "http://test.se/UnsortableStreamNames") streams = plugin.streams() self.assertFalse("best" in streams) self.assertFalse("worst" in streams) self.assertFalse("best-unfiltered" in streams) self.assertFalse("worst-unfiltered" in streams) self.assertTrue("vod" in streams) self.assertTrue("vod_alt" in streams) self.assertTrue("vod_alt2" in streams) def test_set_and_get_locale(self): session = Streamlink() session.set_option("locale", "en_US") self.assertEqual(session.localization.country.alpha2, "US") self.assertEqual(session.localization.language.alpha2, "en") self.assertEqual(session.localization.language_code, "en_US") @patch("streamlink.session.HTTPSession") def test_interface(self, mock_httpsession): adapter_http = Mock(poolmanager=Mock(connection_pool_kw={})) adapter_https = Mock(poolmanager=Mock(connection_pool_kw={})) adapter_foo = Mock(poolmanager=Mock(connection_pool_kw={})) mock_httpsession.return_value = Mock(adapters={ "http://": adapter_http, "https://": adapter_https, "foo://": adapter_foo }) session = self.subject(load_plugins=False) self.assertEqual(session.get_option("interface"), None) session.set_option("interface", "my-interface") self.assertEqual(adapter_http.poolmanager.connection_pool_kw, {"source_address": ("my-interface", 0)}) self.assertEqual(adapter_https.poolmanager.connection_pool_kw, {"source_address": ("my-interface", 0)}) self.assertEqual(adapter_foo.poolmanager.connection_pool_kw, {}) self.assertEqual(session.get_option("interface"), "my-interface") session.set_option("interface", None) self.assertEqual(adapter_http.poolmanager.connection_pool_kw, {}) self.assertEqual(adapter_https.poolmanager.connection_pool_kw, {}) self.assertEqual(adapter_foo.poolmanager.connection_pool_kw, {}) self.assertEqual(session.get_option("interface"), None) @patch("streamlink.session.urllib3_connection", allowed_gai_family=allowed_gai_family) def test_ipv4_ipv6(self, mock_urllib3_connection): session = self.subject(load_plugins=False) self.assertEqual(session.get_option("ipv4"), False) self.assertEqual(session.get_option("ipv6"), False) self.assertEqual(mock_urllib3_connection.allowed_gai_family, allowed_gai_family) session.set_option("ipv4", True) self.assertEqual(session.get_option("ipv4"), True) self.assertEqual(session.get_option("ipv6"), False) self.assertNotEqual(mock_urllib3_connection.allowed_gai_family, allowed_gai_family) self.assertEqual(mock_urllib3_connection.allowed_gai_family(), AF_INET) session.set_option("ipv4", False) self.assertEqual(session.get_option("ipv4"), False) self.assertEqual(session.get_option("ipv6"), False) self.assertEqual(mock_urllib3_connection.allowed_gai_family, allowed_gai_family) session.set_option("ipv6", True) self.assertEqual(session.get_option("ipv4"), False) self.assertEqual(session.get_option("ipv6"), True) self.assertNotEqual(mock_urllib3_connection.allowed_gai_family, allowed_gai_family) self.assertEqual(mock_urllib3_connection.allowed_gai_family(), AF_INET6) session.set_option("ipv6", False) self.assertEqual(session.get_option("ipv4"), False) self.assertEqual(session.get_option("ipv6"), False) self.assertEqual(mock_urllib3_connection.allowed_gai_family, allowed_gai_family) session.set_option("ipv4", True) session.set_option("ipv6", False) self.assertEqual(session.get_option("ipv4"), True) self.assertEqual(session.get_option("ipv6"), False) self.assertEqual(mock_urllib3_connection.allowed_gai_family, allowed_gai_family) def test_https_proxy_default(self): session = self.subject(load_plugins=False) session.set_option("http-proxy", "http://testproxy.com") self.assertEqual("http://testproxy.com", session.http.proxies['http']) self.assertEqual("http://testproxy.com", session.http.proxies['https']) def test_https_proxy_set_first(self): session = self.subject(load_plugins=False) session.set_option("https-proxy", "https://testhttpsproxy.com") session.set_option("http-proxy", "http://testproxy.com") self.assertEqual("http://testproxy.com", session.http.proxies['http']) self.assertEqual("http://testproxy.com", session.http.proxies['https']) def test_https_proxy_default_override(self): session = self.subject(load_plugins=False) session.set_option("http-proxy", "http://testproxy.com") session.set_option("https-proxy", "https://testhttpsproxy.com") self.assertEqual("https://testhttpsproxy.com", session.http.proxies['http']) self.assertEqual("https://testhttpsproxy.com", session.http.proxies['https']) def test_https_proxy_set_only(self): session = self.subject(load_plugins=False) session.set_option("https-proxy", "https://testhttpsproxy.com") self.assertEqual("https://testhttpsproxy.com", session.http.proxies['http']) self.assertEqual("https://testhttpsproxy.com", session.http.proxies['https']) def test_http_proxy_socks(self): session = self.subject(load_plugins=False) session.set_option("http-proxy", "socks5://localhost:1234") self.assertEqual("socks5://localhost:1234", session.http.proxies["http"]) self.assertEqual("socks5://localhost:1234", session.http.proxies["https"]) def test_https_proxy_socks(self): session = self.subject(load_plugins=False) session.set_option("https-proxy", "socks5://localhost:1234") self.assertEqual("socks5://localhost:1234", session.http.proxies["http"]) self.assertEqual("socks5://localhost:1234", session.http.proxies["https"]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/test_stream_file.py0000644000175100001710000000121600000000000020272 0ustar00runnerdockerimport unittest from unittest.mock import Mock, mock_open, patch from streamlink import Streamlink from streamlink.stream.file import FileStream class TestFileStream(unittest.TestCase): def setUp(self): self.session = Streamlink() def test_open_file_path(self): m = mock_open() s = FileStream(self.session, path="/test/path") with patch('streamlink.stream.file.open', m, create=True): s.open() m.assert_called_with("/test/path") def test_open_fileobj(self): fileobj = Mock() s = FileStream(self.session, fileobj=fileobj) self.assertEqual(fileobj, s.open()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/test_stream_json.py0000644000175100001710000000423000000000000020323 0ustar00runnerdockerimport unittest # noinspection PyUnresolvedReferences from requests.utils import DEFAULT_ACCEPT_ENCODING from streamlink import Streamlink from streamlink.stream.hls import HLSStream from streamlink.stream.http import HTTPStream from streamlink.stream.stream import Stream class TestStreamToJSON(unittest.TestCase): def setUp(self): self.session = Streamlink() def test_base_stream(self): stream = Stream(self.session) self.assertEqual( {"type": "stream"}, stream.__json__() ) def test_http_stream(self): url = "http://test.se/stream" stream = HTTPStream(self.session, url, headers={"User-Agent": "Test"}) self.assertEqual( {"type": "http", "url": url, "method": "GET", "body": None, "headers": { "User-Agent": "Test", "Accept": "*/*", "Accept-Encoding": DEFAULT_ACCEPT_ENCODING, "Connection": "keep-alive", }}, stream.__json__() ) def test_hls_stream(self): url = "http://test.se/stream.m3u8" master = "http://test.se/master.m3u8" stream = HLSStream(self.session, url, headers={"User-Agent": "Test"}) self.assertEqual( { "type": "hls", "url": url, "headers": { "User-Agent": "Test", "Accept": "*/*", "Accept-Encoding": DEFAULT_ACCEPT_ENCODING, "Connection": "keep-alive", } }, stream.__json__() ) stream = HLSStream(self.session, url, master, headers={"User-Agent": "Test"}) self.assertEqual( { "type": "hls", "url": url, "headers": { "User-Agent": "Test", "Accept": "*/*", "Accept-Encoding": DEFAULT_ACCEPT_ENCODING, "Connection": "keep-alive", }, "master": master }, stream.__json__() ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/test_streamlink_api.py0000644000175100001710000000304500000000000021004 0ustar00runnerdockerimport os.path import unittest from unittest.mock import patch from streamlink import Streamlink from streamlink.api import streams PluginPath = os.path.join(os.path.dirname(__file__), "plugin") def get_session(): s = Streamlink() s.load_plugins(PluginPath) return s class TestStreamlinkAPI(unittest.TestCase): @patch('streamlink.api.Streamlink', side_effect=get_session) def test_find_test_plugin(self, session): self.assertIn("hls", streams("test.se")) @patch('streamlink.api.Streamlink', side_effect=get_session) def test_no_streams_exception(self, session): self.assertEqual({}, streams("test.se/NoStreamsError")) @patch('streamlink.api.Streamlink', side_effect=get_session) def test_no_streams(self, session): self.assertEqual({}, streams("test.se/empty")) @patch('streamlink.api.Streamlink', side_effect=get_session) def test_stream_type_filter(self, session): stream_types = ["hls"] available_streams = streams("test.se", stream_types=stream_types) self.assertIn("hls", available_streams) self.assertNotIn("test", available_streams) self.assertNotIn("http", available_streams) @patch('streamlink.api.Streamlink', side_effect=get_session) def test_stream_type_wildcard(self, session): stream_types = ["hls", "*"] available_streams = streams("test.se", stream_types=stream_types) self.assertIn("hls", available_streams) self.assertIn("test", available_streams) self.assertIn("http", available_streams) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1643136177.0728405 streamlink-3.1.1/tests/utils/0000755000175100001710000000000000000000000015527 5ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/utils/__init__.py0000644000175100001710000000000000000000000017626 0ustar00runnerdocker././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/utils/test_args.py0000644000175100001710000000766000000000000020105 0ustar00runnerdockerimport unittest from argparse import ArgumentTypeError from streamlink.utils.args import ( boolean, comma_list, comma_list_filter, filesize, keyvalue, num ) class TestUtilsArgs(unittest.TestCase): def test_boolean_true(self): self.assertEqual(boolean('1'), True) self.assertEqual(boolean('on'), True) self.assertEqual(boolean('true'), True) self.assertEqual(boolean('yes'), True) self.assertEqual(boolean('Yes'), True) def test_boolean_false(self): self.assertEqual(boolean('0'), False) self.assertEqual(boolean('false'), False) self.assertEqual(boolean('no'), False) self.assertEqual(boolean('No'), False) self.assertEqual(boolean('off'), False) def test_boolean_error(self): with self.assertRaises(ArgumentTypeError): boolean('yesno') with self.assertRaises(ArgumentTypeError): boolean('FOO') with self.assertRaises(ArgumentTypeError): boolean('2') def test_comma_list(self): # (values, result) test_data = [ ('foo.bar,example.com', ['foo.bar', 'example.com']), ('/var/run/foo,/var/run/bar', ['/var/run/foo', '/var/run/bar']), ('foo bar,24', ['foo bar', '24']), ('hls', ['hls']), ] for _v, _r in test_data: self.assertEqual(comma_list(_v), _r) def test_comma_list_filter(self): # (acceptable, values, result) test_data = [ (['foo', 'bar', 'com'], 'foo,bar,example.com', ['foo', 'bar']), (['/var/run/foo', 'FO'], '/var/run/foo,/var/run/bar', ['/var/run/foo']), (['hls', 'hls5', 'dash'], 'hls,hls5', ['hls', 'hls5']), (['EU', 'RU'], 'DE,FR,RU,US', ['RU']), ] for _a, _v, _r in test_data: func = comma_list_filter(_a) self.assertEqual(func(_v), _r) def test_filesize(self): self.assertEqual(filesize('2000'), 2000) self.assertEqual(filesize('11KB'), 1024 * 11) self.assertEqual(filesize('12MB'), 1024 * 1024 * 12) self.assertEqual(filesize('1KB'), 1024) self.assertEqual(filesize('1MB'), 1024 * 1024) self.assertEqual(filesize('2KB'), 1024 * 2) def test_filesize_error(self): with self.assertRaises(ValueError): filesize('FOO') with self.assertRaises(ValueError): filesize('0.00000') def test_keyvalue(self): # (value, result) test_data = [ ('X-Forwarded-For=127.0.0.1', ('X-Forwarded-For', '127.0.0.1')), ('Referer=https://foo.bar', ('Referer', 'https://foo.bar')), ( 'User-Agent=Mozilla/5.0 (X11; Linux x86_64; rv:60.0)' ' Gecko/20100101 Firefox/60.0', ('User-Agent', 'Mozilla/5.0 (X11; Linux x86_64; rv:60.0) ' 'Gecko/20100101 Firefox/60.0') ), ('domain=example.com', ('domain', 'example.com')), ] for _v, _r in test_data: self.assertEqual(keyvalue(_v), _r) def test_keyvalue_error(self): with self.assertRaises(ValueError): keyvalue('127.0.0.1') def test_num(self): # (value, func, result) test_data = [ ('33', num(int, 5, 120), 33), ('234', num(int, min=10), 234), ('50.222', num(float, 10, 120), 50.222), ] for _v, _f, _r in test_data: self.assertEqual(_f(_v), _r) def test_num_error(self): with self.assertRaises(ArgumentTypeError): func = num(int, 5, 10) func('3') with self.assertRaises(ArgumentTypeError): func = num(int, max=11) func('12') with self.assertRaises(ArgumentTypeError): func = num(int, min=15) func('8') with self.assertRaises(ArgumentTypeError): func = num(float, 10, 20) func('40.222') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/utils/test_crypto.py0000644000175100001710000000134500000000000020463 0ustar00runnerdockerimport base64 import unittest from streamlink.utils.crypto import decrypt_openssl, evp_bytestokey class TestUtil(unittest.TestCase): def test_evp_bytestokey(self): self.assertEqual((b']A@*\xbcK*v\xb9q\x9d\x91\x10\x17\xc5\x92', b'(\xb4n\xd3\xc1\x11\xe8Q\x02\x90\x9b\x1c\xfbP\xea\x0f'), evp_bytestokey(b"hello", b"", 16, 16)) def test_decrpyt(self): # data generated with: # echo "this is a test" | openssl enc -aes-256-cbc -pass pass:"streamlink" -base64 data = base64.b64decode("U2FsdGVkX18nVyJ6Y+ksOASMSHKuRoQ9b4DKHuPbyQc=") self.assertEqual( b"this is a test\n", decrypt_openssl(data, b"streamlink") ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/utils/test_data.py0000644000175100001710000000175200000000000020056 0ustar00runnerdockerimport unittest from streamlink.utils.data import search_dict class TestUtilsData(unittest.TestCase): def test_search_dict(self): self.assertSequenceEqual( list(search_dict(["one", "two"], "one")), [] ) self.assertSequenceEqual( list(search_dict({"two": "test2"}, "one")), [] ) self.assertSequenceEqual( list(search_dict({"one": "test1", "two": "test2"}, "one")), ["test1"] ) self.assertSequenceEqual( list(search_dict({"one": {"inner": "test1"}, "two": "test2"}, "inner")), ["test1"] ) self.assertSequenceEqual( list(search_dict({"one": [{"inner": "test1"}], "two": "test2"}, "inner")), ["test1"] ) self.assertSequenceEqual( list(sorted(search_dict({"one": [{"inner": "test1"}], "two": {"inner": "test2"}}, "inner"))), list(sorted(["test1", "test2"])) ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/utils/test_l10n.py0000644000175100001710000001154100000000000017714 0ustar00runnerdockerimport unittest from unittest.mock import patch import streamlink.utils.l10n as l10n class TestLocalization(unittest.TestCase): def test_language_code_us(self): locale = l10n.Localization("en_US") self.assertEqual("en_US", locale.language_code) def test_language_code_kr(self): locale = l10n.Localization("ko_KR") self.assertEqual("ko_KR", locale.language_code) def test_bad_language_code(self): self.assertRaises(LookupError, l10n.Localization, "enUS") def test_equivalent(self): locale = l10n.Localization("en_CA") self.assertTrue(locale.equivalent(language="eng")) self.assertTrue(locale.equivalent(language="en")) self.assertTrue(locale.equivalent(language="en", country="CA")) self.assertTrue(locale.equivalent(language="en", country="CAN")) self.assertTrue(locale.equivalent(language="en", country="Canada")) def test_equivalent_remap(self): locale = l10n.Localization("fr_FR") self.assertTrue(locale.equivalent(language="fra")) self.assertTrue(locale.equivalent(language="fre")) def test_not_equivalent(self): locale = l10n.Localization("es_ES") self.assertFalse(locale.equivalent(language="eng")) self.assertFalse(locale.equivalent(language="en")) self.assertFalse(locale.equivalent(language="en", country="US")) self.assertFalse(locale.equivalent(language="en", country="Canada")) self.assertFalse(locale.equivalent(language="en", country="ES")) self.assertFalse(locale.equivalent(language="en", country="Spain")) @patch("locale.getdefaultlocale") def test_default(self, getdefaultlocale): getdefaultlocale.return_value = (None, None) locale = l10n.Localization() self.assertEqual("en_US", locale.language_code) self.assertTrue(locale.equivalent(language="en", country="US")) @patch("locale.getdefaultlocale") def test_default_invalid(self, getdefaultlocale): getdefaultlocale.return_value = ("en_150", None) locale = l10n.Localization() self.assertEqual("en_US", locale.language_code) self.assertTrue(locale.equivalent(language="en", country="US")) def test_get_country(self): self.assertEqual("US", l10n.Localization.get_country("USA").alpha2) self.assertEqual("GB", l10n.Localization.get_country("GB").alpha2) self.assertEqual("Canada", l10n.Localization.get_country("Canada").name) def test_get_country_miss(self): self.assertRaises(LookupError, l10n.Localization.get_country, "XE") self.assertRaises(LookupError, l10n.Localization.get_country, "XEX") self.assertRaises(LookupError, l10n.Localization.get_country, "Nowhere") def test_get_language(self): self.assertEqual("eng", l10n.Localization.get_language("en").alpha3) self.assertEqual("fre", l10n.Localization.get_language("fra").bibliographic) self.assertEqual("fra", l10n.Localization.get_language("fre").alpha3) self.assertEqual("gre", l10n.Localization.get_language("gre").bibliographic) def test_get_language_miss(self): self.assertRaises(LookupError, l10n.Localization.get_language, "00") self.assertRaises(LookupError, l10n.Localization.get_language, "000") self.assertRaises(LookupError, l10n.Localization.get_language, "0000") def test_country_compare(self): a = l10n.Country("AA", "AAA", "001", "Test") b = l10n.Country("AA", "AAA", "001", "Test") self.assertEqual(a, b) def test_language_compare(self): a = l10n.Language("AA", "AAA", "Test") b = l10n.Language("AA", None, "Test") self.assertEqual(a, b) a = l10n.Language("BB", "BBB", "Test") b = l10n.Language("AA", None, "Test") self.assertNotEqual(a, b) # issue #3517: language lookups without alpha2 but with alpha3 codes should not raise def test_language_a3_no_a2(self): a = l10n.Localization.get_language("des") self.assertEqual(a.alpha2, "") self.assertEqual(a.alpha3, "des") self.assertEqual(a.name, "Desano") self.assertEqual(a.bibliographic, "") # issue #3057: generic "en" lookups via pycountry yield the "En" language, but not "English" def test_language_en(self): english_a = l10n.Localization.get_language("en") english_b = l10n.Localization.get_language("eng") english_c = l10n.Localization.get_language("English") for lang in [english_a, english_b, english_c]: self.assertEqual(lang.alpha2, "en") self.assertEqual(lang.alpha3, "eng") self.assertEqual(lang.name, "English") self.assertEqual(lang.bibliographic, "") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/utils/test_module.py0000644000175100001710000000112100000000000020420 0ustar00runnerdockerimport os.path import sys import unittest from streamlink.utils.module import load_module # used in the import test to verify that this module was imported __test_marker__ = "test_marker" class TestUtilsModule(unittest.TestCase): def test_load_module_non_existent(self): self.assertRaises(ImportError, load_module, "non_existent_module", os.path.dirname(__file__)) def test_load_module(self): self.assertEqual( sys.modules[__name__].__test_marker__, load_module(__name__.split(".")[-1], os.path.dirname(__file__)).__test_marker__ ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/utils/test_named_pipe.py0000644000175100001710000001320300000000000021240 0ustar00runnerdockerimport threading import unittest from unittest.mock import Mock, call, patch from streamlink.compat import is_win32 from streamlink.utils.named_pipe import NamedPipe, NamedPipePosix, NamedPipeWindows if is_win32: from ctypes import windll, create_string_buffer, c_ulong, byref GENERIC_READ = 0x80000000 OPEN_EXISTING = 3 class ReadNamedPipeThread(threading.Thread): def __init__(self, pipe: NamedPipe): super().__init__(daemon=True) self.path = str(pipe.path) self.error = None self.data = b"" self.done = threading.Event() def run(self): try: self.read() except OSError as err: # pragma: no cover self.error = err self.done.set() def read(self): raise NotImplementedError class ReadNamedPipeThreadPosix(ReadNamedPipeThread): def read(self): with open(self.path, "rb") as file: while True: data = file.read(-1) if len(data) == 0: break self.data += data class ReadNamedPipeThreadWindows(ReadNamedPipeThread): def read(self): handle = windll.kernel32.CreateFileW(self.path, GENERIC_READ, 0, None, OPEN_EXISTING, 0, None) try: while True: data = create_string_buffer(NamedPipeWindows.bufsize) read = c_ulong(0) if not windll.kernel32.ReadFile(handle, data, NamedPipeWindows.bufsize, byref(read), None): # pragma: no cover raise OSError(f"Failed reading pipe: {windll.kernel32.GetLastError()}") self.data += data.value if read.value != len(data.value): break finally: windll.kernel32.CloseHandle(handle) class TestNamedPipe(unittest.TestCase): @patch("streamlink.utils.named_pipe._id", 0) @patch("streamlink.utils.named_pipe.os.getpid", Mock(return_value=12345)) @patch("streamlink.utils.named_pipe.random.randint", Mock(return_value=67890)) @patch("streamlink.utils.named_pipe.NamedPipe._create", Mock(return_value=None)) @patch("streamlink.utils.named_pipe.log") def test_name(self, mock_log): NamedPipe() NamedPipe() self.assertEqual(mock_log.info.mock_calls, [ call("Creating pipe streamlinkpipe-12345-1-67890"), call("Creating pipe streamlinkpipe-12345-2-67890") ]) @unittest.skipIf(is_win32, "test only applicable on a POSIX OS") class TestNamedPipePosix(unittest.TestCase): def test_export(self): self.assertEqual(NamedPipe, NamedPipePosix) @patch("streamlink.utils.named_pipe.os.mkfifo") def test_create(self, mock_mkfifo): mock_mkfifo.side_effect = OSError() with self.assertRaises(OSError): NamedPipePosix() self.assertEqual(mock_mkfifo.call_args[0][1:], (0o660,)) def test_close_before_open(self): pipe = NamedPipePosix() self.assertTrue(pipe.path.is_fifo()) pipe.close() self.assertFalse(pipe.path.is_fifo()) # closing twice doesn't raise pipe.close() def test_write_before_open(self): pipe = NamedPipePosix() self.assertTrue(pipe.path.is_fifo()) with self.assertRaises(Exception): pipe.write(b"foo") pipe.close() def test_named_pipe(self): pipe = NamedPipePosix() self.assertTrue(pipe.path.is_fifo()) reader = ReadNamedPipeThreadPosix(pipe) reader.start() pipe.open() self.assertEqual(pipe.write(b"foo"), 3) self.assertEqual(pipe.write(b"bar"), 3) pipe.close() self.assertFalse(pipe.path.is_fifo()) reader.done.wait(4000) self.assertEqual(reader.error, None) self.assertEqual(reader.data, b"foobar") self.assertFalse(reader.is_alive()) @unittest.skipIf(not is_win32, "test only applicable on Windows") class TestNamedPipeWindows(unittest.TestCase): def test_export(self): self.assertEqual(NamedPipe, NamedPipeWindows) @patch("streamlink.utils.named_pipe.windll.kernel32") def test_create(self, mock_kernel32): mock_kernel32.CreateNamedPipeW.return_value = NamedPipeWindows.INVALID_HANDLE_VALUE mock_kernel32.GetLastError.return_value = 12345 with self.assertRaises(OSError) as cm: NamedPipeWindows() self.assertEqual(str(cm.exception), "Named pipe error code 0x00003039") self.assertEqual(mock_kernel32.CreateNamedPipeW.call_args[0][1:], ( 0x00000002, 0x00000000, 255, 8192, 8192, 0, None )) def test_close_before_open(self): pipe = NamedPipeWindows() handle = windll.kernel32.CreateFileW(str(pipe.path), GENERIC_READ, 0, None, OPEN_EXISTING, 0, None) self.assertNotEqual(handle, NamedPipeWindows.INVALID_HANDLE_VALUE) windll.kernel32.CloseHandle(handle) pipe.close() handle = windll.kernel32.CreateFileW(str(pipe.path), GENERIC_READ, 0, None, OPEN_EXISTING, 0, None) self.assertEqual(handle, NamedPipeWindows.INVALID_HANDLE_VALUE) # closing twice doesn't raise pipe.close() def test_named_pipe(self): pipe = NamedPipeWindows() reader = ReadNamedPipeThreadWindows(pipe) reader.start() pipe.open() self.assertEqual(pipe.write(b"foo"), 3) self.assertEqual(pipe.write(b"bar"), 3) self.assertEqual(pipe.write(b"\0"), 1) reader.done.wait(4000) self.assertEqual(reader.error, None) self.assertEqual(reader.data, b"foobar") self.assertFalse(reader.is_alive()) pipe.close() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/utils/test_parse.py0000644000175100001710000001143400000000000020255 0ustar00runnerdockerimport unittest from lxml.etree import Element from streamlink.exceptions import PluginError from streamlink.plugin.api import validate from streamlink.plugin.api.validate import xml_element from streamlink.utils.parse import parse_html, parse_json, parse_qsd, parse_xml class TestUtilsParse(unittest.TestCase): def test_parse_json(self): self.assertEqual({}, parse_json("{}")) self.assertEqual({"test": 1}, parse_json("""{"test": 1}""")) self.assertEqual({"test": 1}, parse_json("""{"test": 1}""", schema=validate.Schema({"test": 1}))) self.assertRaises(PluginError, parse_json, """{"test: 1}""") self.assertRaises(IOError, parse_json, """{"test: 1}""", exception=IOError) self.assertRaises(PluginError, parse_json, """{"test: 1}""" * 10) def test_parse_xml(self): expected = Element("test", {"foo": "bar"}) actual = parse_xml("""""", ignore_ns=True) self.assertEqual(expected.tag, actual.tag) self.assertEqual(expected.attrib, actual.attrib) def test_parse_xml_ns_ignore(self): expected = Element("test", {"foo": "bar"}) actual = parse_xml("""""", ignore_ns=True) self.assertEqual(expected.tag, actual.tag) self.assertEqual(expected.attrib, actual.attrib) actual = parse_xml("""""", ignore_ns=True) self.assertEqual(expected.tag, actual.tag) self.assertEqual(expected.attrib, actual.attrib) actual = parse_xml("""""", ignore_ns=True) self.assertEqual(expected.tag, actual.tag) self.assertEqual(expected.attrib, actual.attrib) def test_parse_xml_ns(self): expected = Element("{foo:bar}test", {"foo": "bar"}) actual = parse_xml("""""") self.assertEqual(expected.tag, actual.tag) self.assertEqual(expected.attrib, actual.attrib) def test_parse_xml_fail(self): self.assertRaises(PluginError, parse_xml, "1" * 1000) self.assertRaises(IOError, parse_xml, "1" * 1000, exception=IOError) def test_parse_xml_validate(self): expected = Element("test", {"foo": "bar"}) actual = parse_xml( """""", schema=validate.Schema(xml_element(tag="test", attrib={"foo": str})) ) self.assertEqual(expected.tag, actual.tag) self.assertEqual(expected.attrib, actual.attrib) def test_parse_xml_entities_fail(self): self.assertRaises(PluginError, parse_xml, """""") def test_parse_xml_entities(self): expected = Element("test", {"foo": "bar &"}) actual = parse_xml( """""", schema=validate.Schema(xml_element(tag="test", attrib={"foo": str})), invalid_char_entities=True ) self.assertEqual(expected.tag, actual.tag) self.assertEqual(expected.attrib, actual.attrib) def test_parse_xml_encoding(self): tree = parse_xml("""ä""") self.assertEqual(tree.xpath(".//text()"), ["ä"]) tree = parse_xml("""ä""") self.assertEqual(tree.xpath(".//text()"), ["ä"]) tree = parse_xml(b"""\xC3\xA4""") self.assertEqual(tree.xpath(".//text()"), ["ä"]) tree = parse_xml(b"""\xC3\xA4""") self.assertEqual(tree.xpath(".//text()"), ["ä"]) def test_parse_html_encoding(self): tree = parse_html("""ä""") self.assertEqual(tree.xpath(".//body/text()"), ["ä"]) tree = parse_html("""ä""") self.assertEqual(tree.xpath(".//body/text()"), ["ä"]) tree = parse_html(b"""\xC3\xA4""") self.assertEqual(tree.xpath(".//body/text()"), ["ä"]) tree = parse_html(b"""\xC3\xA4""") self.assertEqual(tree.xpath(".//body/text()"), ["ä"]) def test_parse_html_xhtml5(self): tree = parse_html("""ä?>""") self.assertEqual(tree.xpath(".//body/text()"), ["ä?>"]) tree = parse_html(b"""\xC3\xA4?>""") self.assertEqual(tree.xpath(".//body/text()"), ["ä?>"]) def test_parse_qsd(self): self.assertEqual( {"test": "1", "foo": "bar"}, parse_qsd("test=1&foo=bar", schema=validate.Schema({"test": str, "foo": "bar"})) ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/utils/test_times.py0000644000175100001710000000361000000000000020261 0ustar00runnerdockerimport unittest from streamlink.utils.times import hours_minutes_seconds, seconds_to_hhmmss class TestUtilsTimes(unittest.TestCase): def test_hours_minutes_seconds(self): self.assertEqual(hours_minutes_seconds("00:01:30"), 90) self.assertEqual(hours_minutes_seconds("01:20:15"), 4815) self.assertEqual(hours_minutes_seconds("26:00:00"), 93600) self.assertEqual(hours_minutes_seconds("07"), 7) self.assertEqual(hours_minutes_seconds("444"), 444) self.assertEqual(hours_minutes_seconds("8888"), 8888) self.assertEqual(hours_minutes_seconds("01h"), 3600) self.assertEqual(hours_minutes_seconds("01h22m33s"), 4953) self.assertEqual(hours_minutes_seconds("01H22M37S"), 4957) self.assertEqual(hours_minutes_seconds("01h30s"), 3630) self.assertEqual(hours_minutes_seconds("1m33s"), 93) self.assertEqual(hours_minutes_seconds("55s"), 55) self.assertEqual(hours_minutes_seconds("-00:01:40"), 100) self.assertEqual(hours_minutes_seconds("-00h02m30s"), 150) self.assertEqual(hours_minutes_seconds("02:04"), 124) self.assertEqual(hours_minutes_seconds("1:10"), 70) self.assertEqual(hours_minutes_seconds("10:00"), 600) with self.assertRaises(ValueError): hours_minutes_seconds("FOO") with self.assertRaises(ValueError): hours_minutes_seconds("BAR") with self.assertRaises(ValueError): hours_minutes_seconds("11:ERR:00") def test_seconds_to_hhmmss(self): self.assertEqual(seconds_to_hhmmss(0), "00:00:00") self.assertEqual(seconds_to_hhmmss(1), "00:00:01") self.assertEqual(seconds_to_hhmmss(60), "00:01:00") self.assertEqual(seconds_to_hhmmss(3600), "01:00:00") self.assertEqual(seconds_to_hhmmss(13997), "03:53:17") self.assertEqual(seconds_to_hhmmss(13997.4), "03:53:17.4") ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/tests/utils/test_url.py0000644000175100001710000001303700000000000017746 0ustar00runnerdockerfrom collections import OrderedDict from urllib.parse import quote import pytest from streamlink.utils.url import absolute_url, prepend_www, update_qsd, update_scheme, url_concat, url_equal @pytest.mark.parametrize("baseurl,url,expected", [ ("http://test.se", "/test", "http://test.se/test"), ("http://test.se", "http/test.se/test", "http://test.se/http/test.se/test"), ("http://test.se", "http://test2.se/test", "http://test2.se/test"), ]) def test_absolute_url(baseurl, url, expected): assert expected == absolute_url(baseurl, url) @pytest.mark.parametrize("url,expected", [ ("http://test.se/test", "http://www.test.se/test"), ("http://www.test.se", "http://www.test.se"), ]) def test_prepend_www(url, expected): assert expected == prepend_www(url) @pytest.mark.parametrize("assertion,args,expected", [ ("current scheme overrides target scheme (https)", ("https://other.com/bar", "http://example.com/foo"), "https://example.com/foo"), ("current scheme overrides target scheme (http)", ("http://other.com/bar", "https://example.com/foo"), "http://example.com/foo"), ("current scheme does not override target scheme if force is False (https)", ("http://other.com/bar", "https://example.com/foo", False), "https://example.com/foo"), ("current scheme does not override target scheme if force is False (http)", ("https://other.com/bar", "http://example.com/foo", False), "http://example.com/foo"), ("current scheme gets applied to scheme-less target", ("https://other.com/bar", "//example.com/foo"), "https://example.com/foo"), ("current scheme gets applied to scheme-less target, even if force is False", ("https://other.com/bar", "//example.com/foo", False), "https://example.com/foo"), ("current scheme gets added to target string", ("https://other.com/bar", "example.com/foo"), "https://example.com/foo"), ("current scheme gets added to target string, even if force is False", ("https://other.com/bar", "example.com/foo", False), "https://example.com/foo"), ("implicit scheme with IPv4+port", ("http://", "127.0.0.1:1234/foo"), "http://127.0.0.1:1234/foo"), ("implicit scheme with hostname+port", ("http://", "foo.bar:1234/foo"), "http://foo.bar:1234/foo"), ("correctly parses all kinds of schemes", ("foo.1+2-bar://baz", "FOO.1+2-BAR://qux"), "foo.1+2-bar://qux"), ]) def test_update_scheme(assertion, args, expected): assert update_scheme(*args) == expected, assertion def test_url_equal(): assert url_equal("http://test.com/test", "http://test.com/test") assert not url_equal("http://test.com/test", "http://test.com/test2") assert url_equal("http://test.com/test", "http://test.com/test2", ignore_path=True) assert url_equal("http://test.com/test", "https://test.com/test", ignore_scheme=True) assert not url_equal("http://test.com/test", "https://test.com/test") assert url_equal("http://test.com/test", "http://test.com/test#hello", ignore_fragment=True) assert url_equal("http://test.com/test", "http://test2.com/test", ignore_netloc=True) assert not url_equal("http://test.com/test", "http://test2.com/test1", ignore_netloc=True) def test_url_concat(): assert url_concat("http://test.se", "one", "two", "three") == "http://test.se/one/two/three" assert url_concat("http://test.se", "/one", "/two", "/three") == "http://test.se/one/two/three" assert url_concat("http://test.se/one", "../two", "three") == "http://test.se/two/three" assert url_concat("http://test.se/one", "../two", "../three") == "http://test.se/three" def test_update_qsd(): assert update_qsd("http://test.se?one=1&two=3", {"two": 2}) == "http://test.se?one=1&two=2" assert update_qsd("http://test.se?one=1&two=3", remove=["two"]) == "http://test.se?one=1" assert update_qsd("http://test.se?one=1&two=3", {"one": None}, remove="*") == "http://test.se?one=1" assert update_qsd("http://test.se", OrderedDict([("one", ""), ("two", "")])) == "http://test.se?one=&two=", \ "should add empty params" assert update_qsd("http://test.se?one=", {"one": None}) == "http://test.se?one=", "should leave empty params unchanged" assert update_qsd("http://test.se?one=", keep_blank_values=False) == "http://test.se", "should strip blank params" assert update_qsd("http://test.se?one=&two=", {"one": None}, keep_blank_values=False) == "http://test.se?one=", \ "should leave one" assert update_qsd("http://test.se?&two=", {"one": ''}, keep_blank_values=False) == "http://test.se?one=", \ "should set one blank" assert update_qsd("http://test.se?one=", {"two": 2}) == "http://test.se?one=&two=2" assert update_qsd("http://test.se?foo=%3F", {"bar": "!"}) == "http://test.se?foo=%3F&bar=%21", \ "urlencode - encoded URL" assert update_qsd("http://test.se?foo=?", {"bar": "!"}) == "http://test.se?foo=%3F&bar=%21", \ "urlencode - fix URL" assert update_qsd("http://test.se?foo=?", {"bar": "!"}, quote_via=lambda s, *_: s) == "http://test.se?foo=?&bar=!", \ "urlencode - dummy quote method" assert update_qsd("http://test.se", {"foo": "/ "}) == "http://test.se?foo=%2F+", \ "urlencode - default quote_plus" assert update_qsd("http://test.se", {"foo": "/ "}, safe="/", quote_via=quote) == "http://test.se?foo=/%20", \ "urlencode - regular quote with reserved slash" assert update_qsd("http://test.se", {"foo": "/ "}, safe="", quote_via=quote) == "http://test.se?foo=%2F%20", \ "urlencode - regular quote without reserved slash" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1643136141.0 streamlink-3.1.1/versioneer.py0000644000175100001710000020602100000000000015761 0ustar00runnerdocker # Version: 0.18 """The Versioneer - like a rocketeer, but for versions. The Versioneer ============== * like a rocketeer, but for versions! * https://github.com/warner/python-versioneer * Brian Warner * License: Public Domain * Compatible With: python2.6, 2.7, 3.2, 3.3, 3.4, 3.5, 3.6, and pypy * [![Latest Version] (https://pypip.in/version/versioneer/badge.svg?style=flat) ](https://pypi.python.org/pypi/versioneer/) * [![Build Status] (https://travis-ci.org/warner/python-versioneer.png?branch=master) ](https://travis-ci.org/warner/python-versioneer) This is a tool for managing a recorded version number in distutils-based python projects. The goal is to remove the tedious and error-prone "update the embedded version string" step from your release process. Making a new release should be as easy as recording a new tag in your version-control system, and maybe making new tarballs. ## Quick Install * `pip install versioneer` to somewhere to your $PATH * add a `[versioneer]` section to your setup.cfg (see below) * run `versioneer install` in your source tree, commit the results ## Version Identifiers Source trees come from a variety of places: * a version-control system checkout (mostly used by developers) * a nightly tarball, produced by build automation * a snapshot tarball, produced by a web-based VCS browser, like github's "tarball from tag" feature * a release tarball, produced by "setup.py sdist", distributed through PyPI Within each source tree, the version identifier (either a string or a number, this tool is format-agnostic) can come from a variety of places: * ask the VCS tool itself, e.g. "git describe" (for checkouts), which knows about recent "tags" and an absolute revision-id * the name of the directory into which the tarball was unpacked * an expanded VCS keyword ($Id$, etc) * a `_version.py` created by some earlier build step For released software, the version identifier is closely related to a VCS tag. Some projects use tag names that include more than just the version string (e.g. "myproject-1.2" instead of just "1.2"), in which case the tool needs to strip the tag prefix to extract the version identifier. For unreleased software (between tags), the version identifier should provide enough information to help developers recreate the same tree, while also giving them an idea of roughly how old the tree is (after version 1.2, before version 1.3). Many VCS systems can report a description that captures this, for example `git describe --tags --dirty --always` reports things like "0.7-1-g574ab98-dirty" to indicate that the checkout is one revision past the 0.7 tag, has a unique revision id of "574ab98", and is "dirty" (it has uncommitted changes. The version identifier is used for multiple purposes: * to allow the module to self-identify its version: `myproject.__version__` * to choose a name and prefix for a 'setup.py sdist' tarball ## Theory of Operation Versioneer works by adding a special `_version.py` file into your source tree, where your `__init__.py` can import it. This `_version.py` knows how to dynamically ask the VCS tool for version information at import time. `_version.py` also contains `$Revision$` markers, and the installation process marks `_version.py` to have this marker rewritten with a tag name during the `git archive` command. As a result, generated tarballs will contain enough information to get the proper version. To allow `setup.py` to compute a version too, a `versioneer.py` is added to the top level of your source tree, next to `setup.py` and the `setup.cfg` that configures it. This overrides several distutils/setuptools commands to compute the version when invoked, and changes `setup.py build` and `setup.py sdist` to replace `_version.py` with a small static file that contains just the generated version data. ## Installation See [INSTALL.md](./INSTALL.md) for detailed installation instructions. ## Version-String Flavors Code which uses Versioneer can learn about its version string at runtime by importing `_version` from your main `__init__.py` file and running the `get_versions()` function. From the "outside" (e.g. in `setup.py`), you can import the top-level `versioneer.py` and run `get_versions()`. Both functions return a dictionary with different flavors of version information: * `['version']`: A condensed version string, rendered using the selected style. This is the most commonly used value for the project's version string. The default "pep440" style yields strings like `0.11`, `0.11+2.g1076c97`, or `0.11+2.g1076c97.dirty`. See the "Styles" section below for alternative styles. * `['full-revisionid']`: detailed revision identifier. For Git, this is the full SHA1 commit id, e.g. "1076c978a8d3cfc70f408fe5974aa6c092c949ac". * `['date']`: Date and time of the latest `HEAD` commit. For Git, it is the commit date in ISO 8601 format. This will be None if the date is not available. * `['dirty']`: a boolean, True if the tree has uncommitted changes. Note that this is only accurate if run in a VCS checkout, otherwise it is likely to be False or None * `['error']`: if the version string could not be computed, this will be set to a string describing the problem, otherwise it will be None. It may be useful to throw an exception in setup.py if this is set, to avoid e.g. creating tarballs with a version string of "unknown". Some variants are more useful than others. Including `full-revisionid` in a bug report should allow developers to reconstruct the exact code being tested (or indicate the presence of local changes that should be shared with the developers). `version` is suitable for display in an "about" box or a CLI `--version` output: it can be easily compared against release notes and lists of bugs fixed in various releases. The installer adds the following text to your `__init__.py` to place a basic version in `YOURPROJECT.__version__`: from ._version import get_versions __version__ = get_versions()['version'] del get_versions ## Styles The setup.cfg `style=` configuration controls how the VCS information is rendered into a version string. The default style, "pep440", produces a PEP440-compliant string, equal to the un-prefixed tag name for actual releases, and containing an additional "local version" section with more detail for in-between builds. For Git, this is TAG[+DISTANCE.gHEX[.dirty]] , using information from `git describe --tags --dirty --always`. For example "0.11+2.g1076c97.dirty" indicates that the tree is like the "1076c97" commit but has uncommitted changes (".dirty"), and that this commit is two revisions ("+2") beyond the "0.11" tag. For released software (exactly equal to a known tag), the identifier will only contain the stripped tag, e.g. "0.11". Other styles are available. See [details.md](details.md) in the Versioneer source tree for descriptions. ## Debugging Versioneer tries to avoid fatal errors: if something goes wrong, it will tend to return a version of "0+unknown". To investigate the problem, run `setup.py version`, which will run the version-lookup code in a verbose mode, and will display the full contents of `get_versions()` (including the `error` string, which may help identify what went wrong). ## Known Limitations Some situations are known to cause problems for Versioneer. This details the most significant ones. More can be found on Github [issues page](https://github.com/warner/python-versioneer/issues). ### Subprojects Versioneer has limited support for source trees in which `setup.py` is not in the root directory (e.g. `setup.py` and `.git/` are *not* siblings). The are two common reasons why `setup.py` might not be in the root: * Source trees which contain multiple subprojects, such as [Buildbot](https://github.com/buildbot/buildbot), which contains both "master" and "slave" subprojects, each with their own `setup.py`, `setup.cfg`, and `tox.ini`. Projects like these produce multiple PyPI distributions (and upload multiple independently-installable tarballs). * Source trees whose main purpose is to contain a C library, but which also provide bindings to Python (and perhaps other langauges) in subdirectories. Versioneer will look for `.git` in parent directories, and most operations should get the right version string. However `pip` and `setuptools` have bugs and implementation details which frequently cause `pip install .` from a subproject directory to fail to find a correct version string (so it usually defaults to `0+unknown`). `pip install --editable .` should work correctly. `setup.py install` might work too. Pip-8.1.1 is known to have this problem, but hopefully it will get fixed in some later version. [Bug #38](https://github.com/warner/python-versioneer/issues/38) is tracking this issue. The discussion in [PR #61](https://github.com/warner/python-versioneer/pull/61) describes the issue from the Versioneer side in more detail. [pip PR#3176](https://github.com/pypa/pip/pull/3176) and [pip PR#3615](https://github.com/pypa/pip/pull/3615) contain work to improve pip to let Versioneer work correctly. Versioneer-0.16 and earlier only looked for a `.git` directory next to the `setup.cfg`, so subprojects were completely unsupported with those releases. ### Editable installs with setuptools <= 18.5 `setup.py develop` and `pip install --editable .` allow you to install a project into a virtualenv once, then continue editing the source code (and test) without re-installing after every change. "Entry-point scripts" (`setup(entry_points={"console_scripts": ..})`) are a convenient way to specify executable scripts that should be installed along with the python package. These both work as expected when using modern setuptools. When using setuptools-18.5 or earlier, however, certain operations will cause `pkg_resources.DistributionNotFound` errors when running the entrypoint script, which must be resolved by re-installing the package. This happens when the install happens with one version, then the egg_info data is regenerated while a different version is checked out. Many setup.py commands cause egg_info to be rebuilt (including `sdist`, `wheel`, and installing into a different virtualenv), so this can be surprising. [Bug #83](https://github.com/warner/python-versioneer/issues/83) describes this one, but upgrading to a newer version of setuptools should probably resolve it. ### Unicode version strings While Versioneer works (and is continually tested) with both Python 2 and Python 3, it is not entirely consistent with bytes-vs-unicode distinctions. Newer releases probably generate unicode version strings on py2. It's not clear that this is wrong, but it may be surprising for applications when then write these strings to a network connection or include them in bytes-oriented APIs like cryptographic checksums. [Bug #71](https://github.com/warner/python-versioneer/issues/71) investigates this question. ## Updating Versioneer To upgrade your project to a new release of Versioneer, do the following: * install the new Versioneer (`pip install -U versioneer` or equivalent) * edit `setup.cfg`, if necessary, to include any new configuration settings indicated by the release notes. See [UPGRADING](./UPGRADING.md) for details. * re-run `versioneer install` in your source tree, to replace `SRC/_version.py` * commit any changed files ## Future Directions This tool is designed to make it easily extended to other version-control systems: all VCS-specific components are in separate directories like src/git/ . The top-level `versioneer.py` script is assembled from these components by running make-versioneer.py . In the future, make-versioneer.py will take a VCS name as an argument, and will construct a version of `versioneer.py` that is specific to the given VCS. It might also take the configuration arguments that are currently provided manually during installation by editing setup.py . Alternatively, it might go the other direction and include code from all supported VCS systems, reducing the number of intermediate scripts. ## License To make Versioneer easier to embed, all its code is dedicated to the public domain. The `_version.py` that it creates is also in the public domain. Specifically, both are released under the Creative Commons "Public Domain Dedication" license (CC0-1.0), as described in https://creativecommons.org/publicdomain/zero/1.0/ . """ from __future__ import print_function try: import configparser except ImportError: import ConfigParser as configparser import errno import json import os import re import subprocess import sys class VersioneerConfig: """Container for Versioneer configuration parameters.""" def get_root(): """Get the project root directory. We require that all commands are run from the project root, i.e. the directory that contains setup.py, setup.cfg, and versioneer.py . """ root = os.path.realpath(os.path.abspath(os.getcwd())) setup_py = os.path.join(root, "setup.py") versioneer_py = os.path.join(root, "versioneer.py") if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): # allow 'python path/to/setup.py COMMAND' root = os.path.dirname(os.path.realpath(os.path.abspath(sys.argv[0]))) setup_py = os.path.join(root, "setup.py") versioneer_py = os.path.join(root, "versioneer.py") if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): err = ("Versioneer was unable to run the project root directory. " "Versioneer requires setup.py to be executed from " "its immediate directory (like 'python setup.py COMMAND'), " "or in a way that lets it use sys.argv[0] to find the root " "(like 'python path/to/setup.py COMMAND').") raise VersioneerBadRootError(err) try: # Certain runtime workflows (setup.py install/develop in a setuptools # tree) execute all dependencies in a single python process, so # "versioneer" may be imported multiple times, and python's shared # module-import table will cache the first one. So we can't use # os.path.dirname(__file__), as that will find whichever # versioneer.py was first imported, even in later projects. me = os.path.realpath(os.path.abspath(__file__)) me_dir = os.path.normcase(os.path.splitext(me)[0]) vsr_dir = os.path.normcase(os.path.splitext(versioneer_py)[0]) if me_dir != vsr_dir: print("Warning: build in %s is using versioneer.py from %s" % (os.path.dirname(me), versioneer_py)) except NameError: pass return root def get_config_from_root(root): """Read the project setup.cfg file to determine Versioneer config.""" # This might raise EnvironmentError (if setup.cfg is missing), or # configparser.NoSectionError (if it lacks a [versioneer] section), or # configparser.NoOptionError (if it lacks "VCS="). See the docstring at # the top of versioneer.py for instructions on writing your setup.cfg . setup_cfg = os.path.join(root, "setup.cfg") parser = configparser.SafeConfigParser() with open(setup_cfg, "r") as f: parser.readfp(f) VCS = parser.get("versioneer", "VCS") # mandatory def get(parser, name): if parser.has_option("versioneer", name): return parser.get("versioneer", name) return None cfg = VersioneerConfig() cfg.VCS = VCS cfg.style = get(parser, "style") or "" cfg.versionfile_source = get(parser, "versionfile_source") cfg.versionfile_build = get(parser, "versionfile_build") cfg.tag_prefix = get(parser, "tag_prefix") if cfg.tag_prefix in ("''", '""'): cfg.tag_prefix = "" cfg.parentdir_prefix = get(parser, "parentdir_prefix") cfg.verbose = get(parser, "verbose") return cfg class NotThisMethod(Exception): """Exception raised if a method is not valid for the current scenario.""" # these dictionaries contain VCS-specific tools LONG_VERSION_PY = {} HANDLERS = {} def register_vcs_handler(vcs, method): # decorator """Decorator to mark a method as the handler for a particular VCS.""" def decorate(f): """Store f in HANDLERS[vcs][method].""" if vcs not in HANDLERS: HANDLERS[vcs] = {} HANDLERS[vcs][method] = f return f return decorate def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env=None): """Call the given command(s).""" assert isinstance(commands, list) p = None for c in commands: try: dispcmd = str([c] + args) # remember shell=False, so use git.cmd on windows, not just git p = subprocess.Popen([c] + args, cwd=cwd, env=env, stdout=subprocess.PIPE, stderr=(subprocess.PIPE if hide_stderr else None)) break except EnvironmentError: e = sys.exc_info()[1] if e.errno == errno.ENOENT: continue if verbose: print("unable to run %s" % dispcmd) print(e) return None, None else: if verbose: print("unable to find command, tried %s" % (commands,)) return None, None stdout = p.communicate()[0].strip() if sys.version_info[0] >= 3: stdout = stdout.decode() if p.returncode != 0: if verbose: print("unable to run %s (error)" % dispcmd) print("stdout was %s" % stdout) return None, p.returncode return stdout, p.returncode LONG_VERSION_PY['git'] = ''' # This file helps to compute a version number in source trees obtained from # git-archive tarball (such as those provided by githubs download-from-tag # feature). Distribution tarballs (built by setup.py sdist) and build # directories (produced by setup.py build) will contain a much shorter file # that just contains the computed version number. # This file is released into the public domain. Generated by # versioneer-0.18 (https://github.com/warner/python-versioneer) """Git implementation of _version.py.""" import errno import os import re import subprocess import sys def get_keywords(): """Get the keywords needed to look up the version information.""" # these strings will be replaced by git during git-archive. # setup.py/versioneer.py will grep for the variable names, so they must # each be defined on a line of their own. _version.py will just call # get_keywords(). git_refnames = "%(DOLLAR)sFormat:%%d%(DOLLAR)s" git_full = "%(DOLLAR)sFormat:%%H%(DOLLAR)s" git_date = "%(DOLLAR)sFormat:%%ci%(DOLLAR)s" keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} return keywords class VersioneerConfig: """Container for Versioneer configuration parameters.""" def get_config(): """Create, populate and return the VersioneerConfig() object.""" # these strings are filled in when 'setup.py versioneer' creates # _version.py cfg = VersioneerConfig() cfg.VCS = "git" cfg.style = "%(STYLE)s" cfg.tag_prefix = "%(TAG_PREFIX)s" cfg.parentdir_prefix = "%(PARENTDIR_PREFIX)s" cfg.versionfile_source = "%(VERSIONFILE_SOURCE)s" cfg.verbose = False return cfg class NotThisMethod(Exception): """Exception raised if a method is not valid for the current scenario.""" LONG_VERSION_PY = {} HANDLERS = {} def register_vcs_handler(vcs, method): # decorator """Decorator to mark a method as the handler for a particular VCS.""" def decorate(f): """Store f in HANDLERS[vcs][method].""" if vcs not in HANDLERS: HANDLERS[vcs] = {} HANDLERS[vcs][method] = f return f return decorate def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env=None): """Call the given command(s).""" assert isinstance(commands, list) p = None for c in commands: try: dispcmd = str([c] + args) # remember shell=False, so use git.cmd on windows, not just git p = subprocess.Popen([c] + args, cwd=cwd, env=env, stdout=subprocess.PIPE, stderr=(subprocess.PIPE if hide_stderr else None)) break except EnvironmentError: e = sys.exc_info()[1] if e.errno == errno.ENOENT: continue if verbose: print("unable to run %%s" %% dispcmd) print(e) return None, None else: if verbose: print("unable to find command, tried %%s" %% (commands,)) return None, None stdout = p.communicate()[0].strip() if sys.version_info[0] >= 3: stdout = stdout.decode() if p.returncode != 0: if verbose: print("unable to run %%s (error)" %% dispcmd) print("stdout was %%s" %% stdout) return None, p.returncode return stdout, p.returncode def versions_from_parentdir(parentdir_prefix, root, verbose): """Try to determine the version from the parent directory name. Source tarballs conventionally unpack into a directory that includes both the project name and a version string. We will also support searching up two directory levels for an appropriately named parent directory """ rootdirs = [] for i in range(3): dirname = os.path.basename(root) if dirname.startswith(parentdir_prefix): return {"version": dirname[len(parentdir_prefix):], "full-revisionid": None, "dirty": False, "error": None, "date": None} else: rootdirs.append(root) root = os.path.dirname(root) # up a level if verbose: print("Tried directories %%s but none started with prefix %%s" %% (str(rootdirs), parentdir_prefix)) raise NotThisMethod("rootdir doesn't start with parentdir_prefix") @register_vcs_handler("git", "get_keywords") def git_get_keywords(versionfile_abs): """Extract version information from the given file.""" # the code embedded in _version.py can just fetch the value of these # keywords. When used from setup.py, we don't want to import _version.py, # so we do it with a regexp instead. This function is not used from # _version.py. keywords = {} try: f = open(versionfile_abs, "r") for line in f.readlines(): if line.strip().startswith("git_refnames ="): mo = re.search(r'=\s*"(.*)"', line) if mo: keywords["refnames"] = mo.group(1) if line.strip().startswith("git_full ="): mo = re.search(r'=\s*"(.*)"', line) if mo: keywords["full"] = mo.group(1) if line.strip().startswith("git_date ="): mo = re.search(r'=\s*"(.*)"', line) if mo: keywords["date"] = mo.group(1) f.close() except EnvironmentError: pass return keywords @register_vcs_handler("git", "keywords") def git_versions_from_keywords(keywords, tag_prefix, verbose): """Get version information from git keywords.""" if not keywords: raise NotThisMethod("no keywords at all, weird") date = keywords.get("date") if date is not None: # git-2.2.0 added "%%cI", which expands to an ISO-8601 -compliant # datestamp. However we prefer "%%ci" (which expands to an "ISO-8601 # -like" string, which we must then edit to make compliant), because # it's been around since git-1.5.3, and it's too difficult to # discover which version we're using, or to work around using an # older one. date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) refnames = keywords["refnames"].strip() if refnames.startswith("$Format"): if verbose: print("keywords are unexpanded, not using") raise NotThisMethod("unexpanded keywords, not a git-archive tarball") refs = set([r.strip() for r in refnames.strip("()").split(",")]) # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of # just "foo-1.0". If we see a "tag: " prefix, prefer those. TAG = "tag: " tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) if not tags: # Either we're using git < 1.8.3, or there really are no tags. We use # a heuristic: assume all version tags have a digit. The old git %%d # expansion behaves like git log --decorate=short and strips out the # refs/heads/ and refs/tags/ prefixes that would let us distinguish # between branches and tags. By ignoring refnames without digits, we # filter out many common branch names like "release" and # "stabilization", as well as "HEAD" and "master". tags = set([r for r in refs if re.search(r'\d', r)]) if verbose: print("discarding '%%s', no digits" %% ",".join(refs - tags)) if verbose: print("likely tags: %%s" %% ",".join(sorted(tags))) for ref in sorted(tags): # sorting will prefer e.g. "2.0" over "2.0rc1" if ref.startswith(tag_prefix): r = ref[len(tag_prefix):] if verbose: print("picking %%s" %% r) return {"version": r, "full-revisionid": keywords["full"].strip(), "dirty": False, "error": None, "date": date} # no suitable tags, so version is "0+unknown", but full hex is still there if verbose: print("no suitable tags, using unknown + full revision id") return {"version": "0+unknown", "full-revisionid": keywords["full"].strip(), "dirty": False, "error": "no suitable tags", "date": None} @register_vcs_handler("git", "pieces_from_vcs") def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): """Get version from 'git describe' in the root of the source tree. This only gets called if the git-archive 'subst' keywords were *not* expanded, and _version.py hasn't already been rewritten with a short version string, meaning we're inside a checked out source tree. """ GITS = ["git"] if sys.platform == "win32": GITS = ["git.cmd", "git.exe"] out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, hide_stderr=True) if rc != 0: if verbose: print("Directory %%s not under git control" %% root) raise NotThisMethod("'git rev-parse --git-dir' returned error") # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] # if there isn't one, this yields HEX[-dirty] (no NUM) describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", "--always", "--long", "--match", "%%s*" %% tag_prefix], cwd=root) # --long was added in git-1.5.5 if describe_out is None: raise NotThisMethod("'git describe' failed") describe_out = describe_out.strip() full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) if full_out is None: raise NotThisMethod("'git rev-parse' failed") full_out = full_out.strip() pieces = {} pieces["long"] = full_out pieces["short"] = full_out[:7] # maybe improved later pieces["error"] = None # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] # TAG might have hyphens. git_describe = describe_out # look for -dirty suffix dirty = git_describe.endswith("-dirty") pieces["dirty"] = dirty if dirty: git_describe = git_describe[:git_describe.rindex("-dirty")] # now we have TAG-NUM-gHEX or HEX if "-" in git_describe: # TAG-NUM-gHEX mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) if not mo: # unparseable. Maybe git-describe is misbehaving? pieces["error"] = ("unable to parse git-describe output: '%%s'" %% describe_out) return pieces # tag full_tag = mo.group(1) if not full_tag.startswith(tag_prefix): if verbose: fmt = "tag '%%s' doesn't start with prefix '%%s'" print(fmt %% (full_tag, tag_prefix)) pieces["error"] = ("tag '%%s' doesn't start with prefix '%%s'" %% (full_tag, tag_prefix)) return pieces pieces["closest-tag"] = full_tag[len(tag_prefix):] # distance: number of commits since tag pieces["distance"] = int(mo.group(2)) # commit: short hex revision ID pieces["short"] = mo.group(3) else: # HEX: no tags pieces["closest-tag"] = None count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], cwd=root) pieces["distance"] = int(count_out) # total number of commits # commit date: see ISO-8601 comment in git_versions_from_keywords() date = run_command(GITS, ["show", "-s", "--format=%%ci", "HEAD"], cwd=root)[0].strip() pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) return pieces def plus_or_dot(pieces): """Return a + if we don't already have one, else return a .""" if "+" in pieces.get("closest-tag", ""): return "." return "+" def render_pep440(pieces): """Build up version string, with post-release "local version identifier". Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty Exceptions: 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"] or pieces["dirty"]: rendered += plus_or_dot(pieces) rendered += "%%d.g%%s" %% (pieces["distance"], pieces["short"]) if pieces["dirty"]: rendered += ".dirty" else: # exception #1 rendered = "0+untagged.%%d.g%%s" %% (pieces["distance"], pieces["short"]) if pieces["dirty"]: rendered += ".dirty" return rendered def render_pep440_pre(pieces): """TAG[.post.devDISTANCE] -- No -dirty. Exceptions: 1: no tags. 0.post.devDISTANCE """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"]: rendered += ".post.dev%%d" %% pieces["distance"] else: # exception #1 rendered = "0.post.dev%%d" %% pieces["distance"] return rendered def render_pep440_post(pieces): """TAG[.postDISTANCE[.dev0]+gHEX] . The ".dev0" means dirty. Note that .dev0 sorts backwards (a dirty tree will appear "older" than the corresponding clean one), but you shouldn't be releasing software with -dirty anyways. Exceptions: 1: no tags. 0.postDISTANCE[.dev0] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"] or pieces["dirty"]: rendered += ".post%%d" %% pieces["distance"] if pieces["dirty"]: rendered += ".dev0" rendered += plus_or_dot(pieces) rendered += "g%%s" %% pieces["short"] else: # exception #1 rendered = "0.post%%d" %% pieces["distance"] if pieces["dirty"]: rendered += ".dev0" rendered += "+g%%s" %% pieces["short"] return rendered def render_pep440_old(pieces): """TAG[.postDISTANCE[.dev0]] . The ".dev0" means dirty. Eexceptions: 1: no tags. 0.postDISTANCE[.dev0] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"] or pieces["dirty"]: rendered += ".post%%d" %% pieces["distance"] if pieces["dirty"]: rendered += ".dev0" else: # exception #1 rendered = "0.post%%d" %% pieces["distance"] if pieces["dirty"]: rendered += ".dev0" return rendered def render_git_describe(pieces): """TAG[-DISTANCE-gHEX][-dirty]. Like 'git describe --tags --dirty --always'. Exceptions: 1: no tags. HEX[-dirty] (note: no 'g' prefix) """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"]: rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"]) else: # exception #1 rendered = pieces["short"] if pieces["dirty"]: rendered += "-dirty" return rendered def render_git_describe_long(pieces): """TAG-DISTANCE-gHEX[-dirty]. Like 'git describe --tags --dirty --always -long'. The distance/hash is unconditional. Exceptions: 1: no tags. HEX[-dirty] (note: no 'g' prefix) """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"]) else: # exception #1 rendered = pieces["short"] if pieces["dirty"]: rendered += "-dirty" return rendered def render(pieces, style): """Render the given version pieces into the requested style.""" if pieces["error"]: return {"version": "unknown", "full-revisionid": pieces.get("long"), "dirty": None, "error": pieces["error"], "date": None} if not style or style == "default": style = "pep440" # the default if style == "pep440": rendered = render_pep440(pieces) elif style == "pep440-pre": rendered = render_pep440_pre(pieces) elif style == "pep440-post": rendered = render_pep440_post(pieces) elif style == "pep440-old": rendered = render_pep440_old(pieces) elif style == "git-describe": rendered = render_git_describe(pieces) elif style == "git-describe-long": rendered = render_git_describe_long(pieces) else: raise ValueError("unknown style '%%s'" %% style) return {"version": rendered, "full-revisionid": pieces["long"], "dirty": pieces["dirty"], "error": None, "date": pieces.get("date")} def get_versions(): """Get version information or return default if unable to do so.""" # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have # __file__, we can work backwards from there to the root. Some # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which # case we can only use expanded keywords. cfg = get_config() verbose = cfg.verbose try: return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, verbose) except NotThisMethod: pass try: root = os.path.realpath(__file__) # versionfile_source is the relative path from the top of the source # tree (where the .git directory might live) to this file. Invert # this to find the root from __file__. for i in cfg.versionfile_source.split('/'): root = os.path.dirname(root) except NameError: return {"version": "0+unknown", "full-revisionid": None, "dirty": None, "error": "unable to find root of source tree", "date": None} try: pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) return render(pieces, cfg.style) except NotThisMethod: pass try: if cfg.parentdir_prefix: return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) except NotThisMethod: pass return {"version": "0+unknown", "full-revisionid": None, "dirty": None, "error": "unable to compute version", "date": None} ''' @register_vcs_handler("git", "get_keywords") def git_get_keywords(versionfile_abs): """Extract version information from the given file.""" # the code embedded in _version.py can just fetch the value of these # keywords. When used from setup.py, we don't want to import _version.py, # so we do it with a regexp instead. This function is not used from # _version.py. keywords = {} try: f = open(versionfile_abs, "r") for line in f.readlines(): if line.strip().startswith("git_refnames ="): mo = re.search(r'=\s*"(.*)"', line) if mo: keywords["refnames"] = mo.group(1) if line.strip().startswith("git_full ="): mo = re.search(r'=\s*"(.*)"', line) if mo: keywords["full"] = mo.group(1) if line.strip().startswith("git_date ="): mo = re.search(r'=\s*"(.*)"', line) if mo: keywords["date"] = mo.group(1) f.close() except EnvironmentError: pass return keywords @register_vcs_handler("git", "keywords") def git_versions_from_keywords(keywords, tag_prefix, verbose): """Get version information from git keywords.""" if not keywords: raise NotThisMethod("no keywords at all, weird") date = keywords.get("date") if date is not None: # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 # -like" string, which we must then edit to make compliant), because # it's been around since git-1.5.3, and it's too difficult to # discover which version we're using, or to work around using an # older one. date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) refnames = keywords["refnames"].strip() if refnames.startswith("$Format"): if verbose: print("keywords are unexpanded, not using") raise NotThisMethod("unexpanded keywords, not a git-archive tarball") refs = set([r.strip() for r in refnames.strip("()").split(",")]) # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of # just "foo-1.0". If we see a "tag: " prefix, prefer those. TAG = "tag: " tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) if not tags: # Either we're using git < 1.8.3, or there really are no tags. We use # a heuristic: assume all version tags have a digit. The old git %d # expansion behaves like git log --decorate=short and strips out the # refs/heads/ and refs/tags/ prefixes that would let us distinguish # between branches and tags. By ignoring refnames without digits, we # filter out many common branch names like "release" and # "stabilization", as well as "HEAD" and "master". tags = set([r for r in refs if re.search(r'\d', r)]) if verbose: print("discarding '%s', no digits" % ",".join(refs - tags)) if verbose: print("likely tags: %s" % ",".join(sorted(tags))) for ref in sorted(tags): # sorting will prefer e.g. "2.0" over "2.0rc1" if ref.startswith(tag_prefix): r = ref[len(tag_prefix):] if verbose: print("picking %s" % r) return {"version": r, "full-revisionid": keywords["full"].strip(), "dirty": False, "error": None, "date": date} # no suitable tags, so version is "0+unknown", but full hex is still there if verbose: print("no suitable tags, using unknown + full revision id") return {"version": "0+unknown", "full-revisionid": keywords["full"].strip(), "dirty": False, "error": "no suitable tags", "date": None} @register_vcs_handler("git", "pieces_from_vcs") def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): """Get version from 'git describe' in the root of the source tree. This only gets called if the git-archive 'subst' keywords were *not* expanded, and _version.py hasn't already been rewritten with a short version string, meaning we're inside a checked out source tree. """ GITS = ["git"] if sys.platform == "win32": GITS = ["git.cmd", "git.exe"] out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, hide_stderr=True) if rc != 0: if verbose: print("Directory %s not under git control" % root) raise NotThisMethod("'git rev-parse --git-dir' returned error") # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] # if there isn't one, this yields HEX[-dirty] (no NUM) describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", "--always", "--long", "--abbrev=7", "--match", "%s*" % tag_prefix], cwd=root) # --long was added in git-1.5.5 if describe_out is None: raise NotThisMethod("'git describe' failed") describe_out = describe_out.strip() full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) if full_out is None: raise NotThisMethod("'git rev-parse' failed") full_out = full_out.strip() pieces = {} pieces["long"] = full_out pieces["short"] = full_out[:7] # maybe improved later pieces["error"] = None # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] # TAG might have hyphens. git_describe = describe_out # look for -dirty suffix dirty = git_describe.endswith("-dirty") pieces["dirty"] = dirty if dirty: git_describe = git_describe[:git_describe.rindex("-dirty")] # now we have TAG-NUM-gHEX or HEX if "-" in git_describe: # TAG-NUM-gHEX mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) if not mo: # unparseable. Maybe git-describe is misbehaving? pieces["error"] = ("unable to parse git-describe output: '%s'" % describe_out) return pieces # tag full_tag = mo.group(1) if not full_tag.startswith(tag_prefix): if verbose: fmt = "tag '%s' doesn't start with prefix '%s'" print(fmt % (full_tag, tag_prefix)) pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" % (full_tag, tag_prefix)) return pieces pieces["closest-tag"] = full_tag[len(tag_prefix):] # distance: number of commits since tag pieces["distance"] = int(mo.group(2)) # commit: short hex revision ID pieces["short"] = mo.group(3) else: # HEX: no tags pieces["closest-tag"] = None count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], cwd=root) pieces["distance"] = int(count_out) # total number of commits # commit date: see ISO-8601 comment in git_versions_from_keywords() date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[0].strip() pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) return pieces def do_vcs_install(manifest_in, versionfile_source, ipy): """Git-specific installation logic for Versioneer. For Git, this means creating/changing .gitattributes to mark _version.py for export-subst keyword substitution. """ GITS = ["git"] if sys.platform == "win32": GITS = ["git.cmd", "git.exe"] files = [manifest_in, versionfile_source] if ipy: files.append(ipy) try: me = __file__ if me.endswith(".pyc") or me.endswith(".pyo"): me = os.path.splitext(me)[0] + ".py" versioneer_file = os.path.relpath(me) except NameError: versioneer_file = "versioneer.py" files.append(versioneer_file) present = False try: f = open(".gitattributes", "r") for line in f.readlines(): if line.strip().startswith(versionfile_source): if "export-subst" in line.strip().split()[1:]: present = True f.close() except EnvironmentError: pass if not present: f = open(".gitattributes", "a+") f.write("%s export-subst\n" % versionfile_source) f.close() files.append(".gitattributes") run_command(GITS, ["add", "--"] + files) def versions_from_parentdir(parentdir_prefix, root, verbose): """Try to determine the version from the parent directory name. Source tarballs conventionally unpack into a directory that includes both the project name and a version string. We will also support searching up two directory levels for an appropriately named parent directory """ rootdirs = [] for i in range(3): dirname = os.path.basename(root) if dirname.startswith(parentdir_prefix): return {"version": dirname[len(parentdir_prefix):], "full-revisionid": None, "dirty": False, "error": None, "date": None} else: rootdirs.append(root) root = os.path.dirname(root) # up a level if verbose: print("Tried directories %s but none started with prefix %s" % (str(rootdirs), parentdir_prefix)) raise NotThisMethod("rootdir doesn't start with parentdir_prefix") SHORT_VERSION_PY = """ # This file was generated by 'versioneer.py' (0.18) from # revision-control system data, or from the parent directory name of an # unpacked source archive. Distribution tarballs contain a pre-generated copy # of this file. import json version_json = ''' %s ''' # END VERSION_JSON def get_versions(): return json.loads(version_json) """ def versions_from_file(filename): """Try to determine the version from _version.py if present.""" try: with open(filename) as f: contents = f.read() except EnvironmentError: raise NotThisMethod("unable to read _version.py") mo = re.search(r"version_json = '''\n(.*)''' # END VERSION_JSON", contents, re.M | re.S) if not mo: mo = re.search(r"version_json = '''\r\n(.*)''' # END VERSION_JSON", contents, re.M | re.S) if not mo: raise NotThisMethod("no version_json in _version.py") return json.loads(mo.group(1)) def write_to_version_file(filename, versions): """Write the given version number to the given _version.py file.""" os.unlink(filename) contents = json.dumps(versions, sort_keys=True, indent=1, separators=(",", ": ")) with open(filename, "w") as f: f.write(SHORT_VERSION_PY % contents) print("set %s to '%s'" % (filename, versions["version"])) def plus_or_dot(pieces): """Return a + if we don't already have one, else return a .""" if "+" in pieces.get("closest-tag", ""): return "." return "+" def render_pep440(pieces): """Build up version string, with post-release "local version identifier". Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty Exceptions: 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"] or pieces["dirty"]: rendered += plus_or_dot(pieces) rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) if pieces["dirty"]: rendered += ".dirty" else: # exception #1 rendered = "0+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) if pieces["dirty"]: rendered += ".dirty" return rendered def render_pep440_pre(pieces): """TAG[.post.devDISTANCE] -- No -dirty. Exceptions: 1: no tags. 0.post.devDISTANCE """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"]: rendered += ".post.dev%d" % pieces["distance"] else: # exception #1 rendered = "0.post.dev%d" % pieces["distance"] return rendered def render_pep440_post(pieces): """TAG[.postDISTANCE[.dev0]+gHEX] . The ".dev0" means dirty. Note that .dev0 sorts backwards (a dirty tree will appear "older" than the corresponding clean one), but you shouldn't be releasing software with -dirty anyways. Exceptions: 1: no tags. 0.postDISTANCE[.dev0] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"] or pieces["dirty"]: rendered += ".post%d" % pieces["distance"] if pieces["dirty"]: rendered += ".dev0" rendered += plus_or_dot(pieces) rendered += "g%s" % pieces["short"] else: # exception #1 rendered = "0.post%d" % pieces["distance"] if pieces["dirty"]: rendered += ".dev0" rendered += "+g%s" % pieces["short"] return rendered def render_pep440_old(pieces): """TAG[.postDISTANCE[.dev0]] . The ".dev0" means dirty. Eexceptions: 1: no tags. 0.postDISTANCE[.dev0] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"] or pieces["dirty"]: rendered += ".post%d" % pieces["distance"] if pieces["dirty"]: rendered += ".dev0" else: # exception #1 rendered = "0.post%d" % pieces["distance"] if pieces["dirty"]: rendered += ".dev0" return rendered def render_git_describe(pieces): """TAG[-DISTANCE-gHEX][-dirty]. Like 'git describe --tags --dirty --always'. Exceptions: 1: no tags. HEX[-dirty] (note: no 'g' prefix) """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] if pieces["distance"]: rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) else: # exception #1 rendered = pieces["short"] if pieces["dirty"]: rendered += "-dirty" return rendered def render_git_describe_long(pieces): """TAG-DISTANCE-gHEX[-dirty]. Like 'git describe --tags --dirty --always -long'. The distance/hash is unconditional. Exceptions: 1: no tags. HEX[-dirty] (note: no 'g' prefix) """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) else: # exception #1 rendered = pieces["short"] if pieces["dirty"]: rendered += "-dirty" return rendered def render(pieces, style): """Render the given version pieces into the requested style.""" if pieces["error"]: return {"version": "unknown", "full-revisionid": pieces.get("long"), "dirty": None, "error": pieces["error"], "date": None} if not style or style == "default": style = "pep440" # the default if style == "pep440": rendered = render_pep440(pieces) elif style == "pep440-pre": rendered = render_pep440_pre(pieces) elif style == "pep440-post": rendered = render_pep440_post(pieces) elif style == "pep440-old": rendered = render_pep440_old(pieces) elif style == "git-describe": rendered = render_git_describe(pieces) elif style == "git-describe-long": rendered = render_git_describe_long(pieces) else: raise ValueError("unknown style '%s'" % style) return {"version": rendered, "full-revisionid": pieces["long"], "dirty": pieces["dirty"], "error": None, "date": pieces.get("date")} class VersioneerBadRootError(Exception): """The project root directory is unknown or missing key files.""" def get_versions(verbose=False): """Get the project version from whatever source is available. Returns dict with two keys: 'version' and 'full'. """ if "versioneer" in sys.modules: # see the discussion in cmdclass.py:get_cmdclass() del sys.modules["versioneer"] root = get_root() cfg = get_config_from_root(root) assert cfg.VCS is not None, "please set [versioneer]VCS= in setup.cfg" handlers = HANDLERS.get(cfg.VCS) assert handlers, "unrecognized VCS '%s'" % cfg.VCS verbose = verbose or cfg.verbose assert cfg.versionfile_source is not None, \ "please set versioneer.versionfile_source" assert cfg.tag_prefix is not None, "please set versioneer.tag_prefix" versionfile_abs = os.path.join(root, cfg.versionfile_source) # extract version from first of: _version.py, VCS command (e.g. 'git # describe'), parentdir. This is meant to work for developers using a # source checkout, for users of a tarball created by 'setup.py sdist', # and for users of a tarball/zipball created by 'git archive' or github's # download-from-tag feature or the equivalent in other VCSes. get_keywords_f = handlers.get("get_keywords") from_keywords_f = handlers.get("keywords") if get_keywords_f and from_keywords_f: try: keywords = get_keywords_f(versionfile_abs) ver = from_keywords_f(keywords, cfg.tag_prefix, verbose) if verbose: print("got version from expanded keyword %s" % ver) return ver except NotThisMethod: pass try: ver = versions_from_file(versionfile_abs) if verbose: print("got version from file %s %s" % (versionfile_abs, ver)) return ver except NotThisMethod: pass from_vcs_f = handlers.get("pieces_from_vcs") if from_vcs_f: try: pieces = from_vcs_f(cfg.tag_prefix, root, verbose) ver = render(pieces, cfg.style) if verbose: print("got version from VCS %s" % ver) return ver except NotThisMethod: pass try: if cfg.parentdir_prefix: ver = versions_from_parentdir(cfg.parentdir_prefix, root, verbose) if verbose: print("got version from parentdir %s" % ver) return ver except NotThisMethod: pass if verbose: print("unable to compute version") return {"version": "0+unknown", "full-revisionid": None, "dirty": None, "error": "unable to compute version", "date": None} def get_version(): """Get the short version string for this project.""" return get_versions()["version"] def get_cmdclass(): """Get the custom setuptools/distutils subclasses used by Versioneer.""" if "versioneer" in sys.modules: del sys.modules["versioneer"] # this fixes the "python setup.py develop" case (also 'install' and # 'easy_install .'), in which subdependencies of the main project are # built (using setup.py bdist_egg) in the same python process. Assume # a main project A and a dependency B, which use different versions # of Versioneer. A's setup.py imports A's Versioneer, leaving it in # sys.modules by the time B's setup.py is executed, causing B to run # with the wrong versioneer. Setuptools wraps the sub-dep builds in a # sandbox that restores sys.modules to it's pre-build state, so the # parent is protected against the child's "import versioneer". By # removing ourselves from sys.modules here, before the child build # happens, we protect the child from the parent's versioneer too. # Also see https://github.com/warner/python-versioneer/issues/52 cmds = {} # we add "version" to both distutils and setuptools from distutils.core import Command class cmd_version(Command): description = "report generated version string" user_options = [] boolean_options = [] def initialize_options(self): pass def finalize_options(self): pass def run(self): vers = get_versions(verbose=True) print("Version: %s" % vers["version"]) print(" full-revisionid: %s" % vers.get("full-revisionid")) print(" dirty: %s" % vers.get("dirty")) print(" date: %s" % vers.get("date")) if vers["error"]: print(" error: %s" % vers["error"]) cmds["version"] = cmd_version # we override "build_py" in both distutils and setuptools # # most invocation pathways end up running build_py: # distutils/build -> build_py # distutils/install -> distutils/build ->.. # setuptools/bdist_wheel -> distutils/install ->.. # setuptools/bdist_egg -> distutils/install_lib -> build_py # setuptools/install -> bdist_egg ->.. # setuptools/develop -> ? # pip install: # copies source tree to a tempdir before running egg_info/etc # if .git isn't copied too, 'git describe' will fail # then does setup.py bdist_wheel, or sometimes setup.py install # setup.py egg_info -> ? # we override different "build_py" commands for both environments if "setuptools" in sys.modules: from setuptools.command.build_py import build_py as _build_py else: from distutils.command.build_py import build_py as _build_py class cmd_build_py(_build_py): def run(self): root = get_root() cfg = get_config_from_root(root) versions = get_versions() _build_py.run(self) # now locate _version.py in the new build/ directory and replace # it with an updated value if cfg.versionfile_build: target_versionfile = os.path.join(self.build_lib, cfg.versionfile_build) print("UPDATING %s" % target_versionfile) write_to_version_file(target_versionfile, versions) cmds["build_py"] = cmd_build_py if "cx_Freeze" in sys.modules: # cx_freeze enabled? from cx_Freeze.dist import build_exe as _build_exe # nczeczulin reports that py2exe won't like the pep440-style string # as FILEVERSION, but it can be used for PRODUCTVERSION, e.g. # setup(console=[{ # "version": versioneer.get_version().split("+", 1)[0], # FILEVERSION # "product_version": versioneer.get_version(), # ... class cmd_build_exe(_build_exe): def run(self): root = get_root() cfg = get_config_from_root(root) versions = get_versions() target_versionfile = cfg.versionfile_source print("UPDATING %s" % target_versionfile) write_to_version_file(target_versionfile, versions) _build_exe.run(self) os.unlink(target_versionfile) with open(cfg.versionfile_source, "w") as f: LONG = LONG_VERSION_PY[cfg.VCS] f.write(LONG % {"DOLLAR": "$", "STYLE": cfg.style, "TAG_PREFIX": cfg.tag_prefix, "PARENTDIR_PREFIX": cfg.parentdir_prefix, "VERSIONFILE_SOURCE": cfg.versionfile_source, }) cmds["build_exe"] = cmd_build_exe del cmds["build_py"] if 'py2exe' in sys.modules: # py2exe enabled? try: from py2exe.distutils_buildexe import py2exe as _py2exe # py3 except ImportError: from py2exe.build_exe import py2exe as _py2exe # py2 class cmd_py2exe(_py2exe): def run(self): root = get_root() cfg = get_config_from_root(root) versions = get_versions() target_versionfile = cfg.versionfile_source print("UPDATING %s" % target_versionfile) write_to_version_file(target_versionfile, versions) _py2exe.run(self) os.unlink(target_versionfile) with open(cfg.versionfile_source, "w") as f: LONG = LONG_VERSION_PY[cfg.VCS] f.write(LONG % {"DOLLAR": "$", "STYLE": cfg.style, "TAG_PREFIX": cfg.tag_prefix, "PARENTDIR_PREFIX": cfg.parentdir_prefix, "VERSIONFILE_SOURCE": cfg.versionfile_source, }) cmds["py2exe"] = cmd_py2exe # we override different "sdist" commands for both environments if "setuptools" in sys.modules: from setuptools.command.sdist import sdist as _sdist else: from distutils.command.sdist import sdist as _sdist class cmd_sdist(_sdist): def run(self): versions = get_versions() self._versioneer_generated_versions = versions # unless we update this, the command will keep using the old # version self.distribution.metadata.version = versions["version"] return _sdist.run(self) def make_release_tree(self, base_dir, files): root = get_root() cfg = get_config_from_root(root) _sdist.make_release_tree(self, base_dir, files) # now locate _version.py in the new base_dir directory # (remembering that it may be a hardlink) and replace it with an # updated value target_versionfile = os.path.join(base_dir, cfg.versionfile_source) print("UPDATING %s" % target_versionfile) write_to_version_file(target_versionfile, self._versioneer_generated_versions) cmds["sdist"] = cmd_sdist return cmds CONFIG_ERROR = """ setup.cfg is missing the necessary Versioneer configuration. You need a section like: [versioneer] VCS = git style = pep440 versionfile_source = src/myproject/_version.py versionfile_build = myproject/_version.py tag_prefix = parentdir_prefix = myproject- You will also need to edit your setup.py to use the results: import versioneer setup(version=versioneer.get_version(), cmdclass=versioneer.get_cmdclass(), ...) Please read the docstring in ./versioneer.py for configuration instructions, edit setup.cfg, and re-run the installer or 'python versioneer.py setup'. """ SAMPLE_CONFIG = """ # See the docstring in versioneer.py for instructions. Note that you must # re-run 'versioneer.py setup' after changing this section, and commit the # resulting files. [versioneer] #VCS = git #style = pep440 #versionfile_source = #versionfile_build = #tag_prefix = #parentdir_prefix = """ INIT_PY_SNIPPET = """ from ._version import get_versions __version__ = get_versions()['version'] del get_versions """ def do_setup(): """Main VCS-independent setup function for installing Versioneer.""" root = get_root() try: cfg = get_config_from_root(root) except (EnvironmentError, configparser.NoSectionError, configparser.NoOptionError) as e: if isinstance(e, (EnvironmentError, configparser.NoSectionError)): print("Adding sample versioneer config to setup.cfg", file=sys.stderr) with open(os.path.join(root, "setup.cfg"), "a") as f: f.write(SAMPLE_CONFIG) print(CONFIG_ERROR, file=sys.stderr) return 1 print(" creating %s" % cfg.versionfile_source) with open(cfg.versionfile_source, "w") as f: LONG = LONG_VERSION_PY[cfg.VCS] f.write(LONG % {"DOLLAR": "$", "STYLE": cfg.style, "TAG_PREFIX": cfg.tag_prefix, "PARENTDIR_PREFIX": cfg.parentdir_prefix, "VERSIONFILE_SOURCE": cfg.versionfile_source, }) ipy = os.path.join(os.path.dirname(cfg.versionfile_source), "__init__.py") if os.path.exists(ipy): try: with open(ipy, "r") as f: old = f.read() except EnvironmentError: old = "" if INIT_PY_SNIPPET not in old: print(" appending to %s" % ipy) with open(ipy, "a") as f: f.write(INIT_PY_SNIPPET) else: print(" %s unmodified" % ipy) else: print(" %s doesn't exist, ok" % ipy) ipy = None # Make sure both the top-level "versioneer.py" and versionfile_source # (PKG/_version.py, used by runtime code) are in MANIFEST.in, so # they'll be copied into source distributions. Pip won't be able to # install the package without this. manifest_in = os.path.join(root, "MANIFEST.in") simple_includes = set() try: with open(manifest_in, "r") as f: for line in f: if line.startswith("include "): for include in line.split()[1:]: simple_includes.add(include) except EnvironmentError: pass # That doesn't cover everything MANIFEST.in can do # (http://docs.python.org/2/distutils/sourcedist.html#commands), so # it might give some false negatives. Appending redundant 'include' # lines is safe, though. if "versioneer.py" not in simple_includes: print(" appending 'versioneer.py' to MANIFEST.in") with open(manifest_in, "a") as f: f.write("include versioneer.py\n") else: print(" 'versioneer.py' already in MANIFEST.in") if cfg.versionfile_source not in simple_includes: print(" appending versionfile_source ('%s') to MANIFEST.in" % cfg.versionfile_source) with open(manifest_in, "a") as f: f.write("include %s\n" % cfg.versionfile_source) else: print(" versionfile_source already in MANIFEST.in") # Make VCS-specific changes. For git, this means creating/changing # .gitattributes to mark _version.py for export-subst keyword # substitution. do_vcs_install(manifest_in, cfg.versionfile_source, ipy) return 0 def scan_setup_py(): """Validate the contents of setup.py against Versioneer's expectations.""" found = set() setters = False errors = 0 with open("setup.py", "r") as f: for line in f.readlines(): if "import versioneer" in line: found.add("import") if "versioneer.get_cmdclass()" in line: found.add("cmdclass") if "versioneer.get_version()" in line: found.add("get_version") if "versioneer.VCS" in line: setters = True if "versioneer.versionfile_source" in line: setters = True if len(found) != 3: print("") print("Your setup.py appears to be missing some important items") print("(but I might be wrong). Please make sure it has something") print("roughly like the following:") print("") print(" import versioneer") print(" setup( version=versioneer.get_version(),") print(" cmdclass=versioneer.get_cmdclass(), ...)") print("") errors += 1 if setters: print("You should remove lines like 'versioneer.VCS = ' and") print("'versioneer.versionfile_source = ' . This configuration") print("now lives in setup.cfg, and should be removed from setup.py") print("") errors += 1 return errors if __name__ == "__main__": cmd = sys.argv[1] if cmd == "setup": errors = do_setup() errors += scan_setup_py() if errors: sys.exit(1)