livestreamer-1.12.2/ 0000755 0001750 0001750 00000000000 12521217500 015370 5 ustar chrippa chrippa 0000000 0000000 livestreamer-1.12.2/setup.cfg 0000644 0001750 0001750 00000000122 12521217500 017204 0 ustar chrippa chrippa 0000000 0000000 [wheel]
universal = 1
[egg_info]
tag_svn_revision = 0
tag_date = 0
tag_build =
livestreamer-1.12.2/PKG-INFO 0000644 0001750 0001750 00000001575 12521217500 016475 0 ustar chrippa chrippa 0000000 0000000 Metadata-Version: 1.1
Name: livestreamer
Version: 1.12.2
Summary: Livestreamer is command-line utility that extracts streams from various services and pipes them into a video player of choice.
Home-page: http://livestreamer.io/
Author: Christopher Rosell
Author-email: chrippa@tanuki.se
License: Simplified BSD
Description: UNKNOWN
Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Console
Classifier: Operating System :: POSIX
Classifier: Operating System :: Microsoft :: Windows
Classifier: Programming Language :: Python :: 2.6
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3.3
Classifier: Programming Language :: Python :: 3.4
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Multimedia :: Sound/Audio
Classifier: Topic :: Multimedia :: Video
Classifier: Topic :: Utilities
livestreamer-1.12.2/setup.py 0000644 0001750 0001750 00000004771 12521217321 017114 0 ustar chrippa chrippa 0000000 0000000 #!/usr/bin/env python
from os import environ
from os.path import abspath, dirname, join
from setuptools import setup
from sys import version_info, path as sys_path
deps = []
packages = [
"livestreamer",
"livestreamer.stream",
"livestreamer.plugin",
"livestreamer.plugin.api",
"livestreamer.plugins",
"livestreamer.packages",
"livestreamer.packages.flashmedia",
"livestreamer_cli",
"livestreamer_cli.packages",
"livestreamer_cli.utils"
]
if version_info[0] == 2:
# Require backport of concurrent.futures on Python 2
deps.append("futures")
# Require backport of argparse on Python 2.6
if version_info[1] == 6:
deps.append("argparse")
# Require singledispatch on Python <3.4
if version_info[0] == 2 or (version_info[0] == 3 and version_info[1] < 4):
deps.append("singledispatch")
# requests 2.0 does not work correctly on Python <2.6.3
if (version_info[0] == 2 and version_info[1] == 6 and version_info[2] < 3):
deps.append("requests>=1.0,<2.0")
else:
deps.append("requests>=1.0,<3.0")
# When we build an egg for the Win32 bootstrap we don't want dependency
# information built into it.
if environ.get("NO_DEPS"):
deps = []
srcdir = join(dirname(abspath(__file__)), "src/")
sys_path.insert(0, srcdir)
setup(name="livestreamer",
version="1.12.2",
description="Livestreamer is command-line utility that extracts streams "
"from various services and pipes them into a video player of "
"choice.",
url="http://livestreamer.io/",
author="Christopher Rosell",
author_email="chrippa@tanuki.se",
license="Simplified BSD",
packages=packages,
package_dir={ "": "src" },
entry_points={
"console_scripts": ["livestreamer=livestreamer_cli.main:main"]
},
install_requires=deps,
test_suite="tests",
classifiers=["Development Status :: 5 - Production/Stable",
"Environment :: Console",
"Operating System :: POSIX",
"Operating System :: Microsoft :: Windows",
"Programming Language :: Python :: 2.6",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3.3",
"Programming Language :: Python :: 3.4",
"Topic :: Internet :: WWW/HTTP",
"Topic :: Multimedia :: Sound/Audio",
"Topic :: Multimedia :: Video",
"Topic :: Utilities"]
)
livestreamer-1.12.2/requirements-docs.txt 0000644 0001750 0001750 00000000007 12521217321 021600 0 ustar chrippa chrippa 0000000 0000000 sphinx
livestreamer-1.12.2/README.rst 0000644 0001750 0001750 00000005073 12521217321 017065 0 ustar chrippa chrippa 0000000 0000000 Livestreamer
============
.. image:: http://img.shields.io/pypi/v/livestreamer.svg?style=flat-square
:target: https://pypi.python.org/pypi/livestreamer
.. image:: http://img.shields.io/pypi/dm/livestreamer.svg?style=flat-square
:target: https://pypi.python.org/pypi/livestreamer
.. image:: http://img.shields.io/travis/chrippa/livestreamer.svg?style=flat-square
:target: http://travis-ci.org/chrippa/livestreamer
Overview
--------
Livestreamer is a `command-line utility`_ that pipes video streams
from various services into a video player, such as `VLC `_.
The main purpose of Livestreamer is to allow the user to avoid buggy and CPU
heavy flash plugins but still be able to enjoy various streamed content.
There is also an `API`_ available for developers who want access
to the video stream data.
- Documentation: http://docs.livestreamer.io/
- Issue tracker: https://github.com/chrippa/livestreamer/issues
- PyPI: https://pypi.python.org/pypi/livestreamer
- Discussions: https://groups.google.com/forum/#!forum/livestreamer
- IRC: #livestreamer @ Freenode
- Free software: Simplified BSD license
.. _command-line utility: http://docs.livestreamer.io/cli.html
.. _API: http://docs.livestreamer.io/api_guide.html
Features
--------
Livestreamer is built upon a plugin system which allows support for new services
to be easily added. Currently most of the big streaming services are supported,
such as:
- `Dailymotion `_
- `Livestream `_
- `Twitch `_
- `UStream `_
- `YouTube Live `_
... and many more. A full list of plugins currently included can be found
on the `Plugins`_ page.
.. _Plugins: http://docs.livestreamer.io/plugin_matrix.html
Quickstart
-----------
The default behaviour of Livestreamer is to playback a stream in the default
player (`VLC `_).
.. sourcecode:: console
# pip install livestreamer
$ livestreamer twitch.tv/day9tv best
[cli][info] Found matching plugin twitch for URL twitch.tv/day9tv
[cli][info] Opening stream: source
[cli][info] Starting player: vlc
For more in-depth usage and install instructions see the `User guide`_.
.. _User guide: http://docs.livestreamer.io/index.html#user-guide
Related software
----------------
Feel free to add any Livestreamer related things to
the `wiki `_.
Contributing
------------
If you wish to report a bug or contribute code, please take a look
at `CONTRIBUTING.rst `_ first.
livestreamer-1.12.2/MANIFEST.in 0000644 0001750 0001750 00000000324 12521217321 017126 0 ustar chrippa chrippa 0000000 0000000 include AUTHORS
include CHANGELOG.rst
include CONTRIBUTING.rst
include README.rst
include LICENSE*
include requirements-docs.txt
recursive-include docs *
recursive-include examples *
recursive-include tests *py
livestreamer-1.12.2/LICENSE.pbs 0000644 0001750 0001750 00000002052 12521217321 017160 0 ustar chrippa chrippa 0000000 0000000 Copyright (C) 2011-2012 by Andrew Moffat
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
livestreamer-1.12.2/LICENSE.flashmedia 0000644 0001750 0001750 00000002436 12521217321 020477 0 ustar chrippa chrippa 0000000 0000000 Copyright (c) 2011-2013, Christopher Rosell
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.
livestreamer-1.12.2/LICENSE 0000644 0001750 0001750 00000002436 12521217321 016403 0 ustar chrippa chrippa 0000000 0000000 Copyright (c) 2011-2015, Christopher Rosell
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.
livestreamer-1.12.2/CONTRIBUTING.rst 0000644 0001750 0001750 00000006566 12521217321 020047 0 ustar chrippa chrippa 0000000 0000000 ============
Contributing
============
Contributions are welcome, and they are greatly appreciated! Every
little bit helps, and credit will always be given.
You can contribute in many ways:
Types of Contributions
----------------------
Report Bugs
~~~~~~~~~~~
Report bugs at https://github.com/chrippa/livestreamer/issues.
If you are reporting a bug, please include:
* Your operating system name and version.
* Any details about your local setup that might be helpful in troubleshooting.
* Detailed steps to reproduce the bug.
Fix Bugs
~~~~~~~~
Look through the GitHub issues for bugs. Anything tagged with "bug"
is open to whoever wants to implement it.
Implement Features
~~~~~~~~~~~~~~~~~~
Look through the GitHub issues for features. Anything tagged with "feature"
is open to whoever wants to implement it.
Adding Plugins
~~~~~~~~~~~~~~
Livestreamer can always use more plugins. Look through the GitHub issues
if you are looking for something to implement.
There is no plugin documentation at the moment, but look at the existing
plugins to get an idea of how it works.
Write Documentation
~~~~~~~~~~~~~~~~~~~
Livestreamer could always use more documentation, whether as part of the
official Livestreamer docs, in docstrings, or even on the web in blog posts,
articles, and such.
Submit Feedback
~~~~~~~~~~~~~~~
The best way to send feedback is to file an issue at https://github.com/chrippa/livestreamer/issues.
If you are proposing a feature:
* Explain in detail how it would work.
* Keep the scope as narrow as possible, to make it easier to implement.
* Remember that this is a volunteer-driven project, and that contributions
are welcome :)
Get Started!
------------
Ready to contribute? Here's how to set up `livestreamer` for local development.
1. Fork the `livestreamer` repo on GitHub.
2. Clone your fork locally::
$ git clone git@github.com:your_name_here/livestreamer.git
3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development::
$ mkvirtualenv livestreamer
$ cd livestreamer/
$ python setup.py develop
4. Create a branch for local development::
$ git checkout -b name-of-your-bugfix-or-feature
Now you can make your changes locally.
5. When you're done making changes, check that your changes pass the
tests, including testing other Python versions with tox::
$ python setup.py test
$ tox
To get tox, just pip install it into your virtualenv.
6. Commit your changes and push your branch to GitHub::
$ git add .
$ git commit -m "Your detailed description of your changes."
$ git push origin name-of-your-bugfix-or-feature
7. Submit a pull request through the GitHub website.
Pull Request Guidelines
-----------------------
Before you submit a pull request, check that it meets these guidelines:
1. The pull request should include tests if it's a core feature.
2. If the pull request adds functionality, the docs should be updated.
3. When creating a pull request, make sure it's on the correct branch.
These branches are currently used:
- master: Only critical fixes that needs to be released ASAP.
- develop: Everything else.
4. The pull request should work for Python 2.6, 2.7, and 3.3, and for PyPy. Check
https://travis-ci.org/chrippa/livestreamer/pull_requests
and make sure that the tests pass for all supported Python versions.
livestreamer-1.12.2/CHANGELOG.rst 0000644 0001750 0001750 00000064415 12521217321 017424 0 ustar chrippa chrippa 0000000 0000000 Version 1.12.2 (2015-05-02)
---------------------------
Bug fixes:
- hds: Don't modify request params when handling PVSWF. (#842)
- hls: Handle unpadded encryption IV's.
- Fixed regression in redirect resolver. (#816)
Plugins:
- Added plugin for media.ccc.de (media_ccc_de), patch by @meise.
- Added plugin for Kanal 5/9/11 (sbsdiscovery), patch by @tboss. (#815)
- Added plugin for Periscope (periscope).
- Added plugin for SSH101 (ssh101), patch by @Razier-23. (#869)
- artetv: Updated for service changes.
- crunchyroll: Updated for service changes. (#864, #865)
- hitbox: Fixed VOD support. (#856)
- livestream: Updated for service changes.
- viasat: Added support for juicyplay.se.
- viasat: Fixed missing streams. (#822)
- youtube: Added support for /channel URLs. (#825)
Version 1.12.1 (2015-03-22)
---------------------------
Bug fixes:
- Don't crash when failing to look up listening ports. (#790)
Plugins:
- Added plugin for ITV Player, patch by @blxd. (#776)
- Added plugin for tv3.cat, patch by @blxd. (#784)
- Added plugin for TV Catchup, patch by @blxd. (#775)
- connectcast: Fixed crash, patch by @mammothb. (#779)
- dailymotion: Added support for HDS VODs. (#731)
- gaminglive: Added support for VODs, patches by @kasper93 and @chhe. (#789, #808)
- picarto: Updated for service changes, patch by @FireDart. (#803)
- tv4play: Work around bad SSL implementation on Python 2. (#785)
- twitch: Use correct OAuth scopes, patch by @josephglanville. (#778)
- ustreamtv: Updated for service changes, patch by @trUSTssc. (#799)
- viasat: Fixed missing streams. (#750)
- viasat: Added play.tv3.lt to supported URLs. (#773)
Streams:
- hds: Fixed issue with query parameters when building fragment URLs. (#786)
Version 1.12.0 (2015-03-01)
---------------------------
Bug fixes:
- Made HTTP modes more strict to avoid issues with `mpv --yt-dl`.
- Fixed :option:`--http-cookie` option crash.
CLI:
- Added :option:`--can-handle-url` option, useful for scripting.
- Added :option:`--version-check` option to force a version check.
- Added a passive HTTP server mode (:option:`--player-external-http`), patch by @danielkza. (#699)
Plugins:
- Added plugin for Disney Channel Germany, patch by @boekkooi. (#698)
- Added plugin for NOS (Nederlandse Omroep Stichting), patch by @boekkooi. (#697)
- Added plugin for tga.plu.cn, patch by @wolftankk. (#669)
- Added plugin for Wat.tv, patch by @boekkooi. (#701)
- Added plugin for afreeca.tv. (The old afreecatv plugin has been renamed to afreeca)
- chaturbate: Added support for subdomain URLs, patch by @GameWalker. (#676)
- connectcast: Updated for service changes, patch by @darvelo. (#722)
- dailymotion: Added support for games.dailymotion.com, patch by @daslicious. (#684)
- dommune: Fixed Youtube redirect URL.
- gaminglive: Updated for service changes, patch by @chhe. (#721)
- mlgtv: Updated for service changes, patch by @daslicious. (#686)
- hitbox: Updated for services changes. (#648)
- streamlive: Updated for service changes, patch by @daslicious. (#667)
- ustreamtv: Updated for service changes. (#707)
- youtube: Now handles more URL types.
Version 1.11.1 (2014-12-12)
---------------------------
Plugins:
- twitch: Updated for API changes. (#633)
Version 1.11.0 (2014-12-10)
---------------------------
Bugfixes:
- cli: Only apply the backslash magic on player paths on Windows.
CLI:
- Added :option:`--http-cookie` option.
- Added :option:`--http-header` option.
- Added :option:`--http-query-param` option.
- Deprecated the :option:`--http-cookies` option.
- Deprecated the :option:`--http-headers` option.
- Deprecated the :option:`--http-query-params` option.
- Changed the continuous HTTP mode to always fetch streams.
Should fix segmented streams repeating at the end for most
services.
Plugins:
- Added plugin for NPO, patch by @monkeyphysics. (#599)
- afreecatv: Updated for service changes. (#568)
- beattv: Updated validation schema to include float offsets, patch by @suhailpatel. (#555)
- douyutv: Added support for transcodes.
- gaminglive: Fixed quality names, patch by @chhe. (#545)
- goodgame: Updated for service changes, patch by @JaxxC. (#554)
- oldlivestream: Check that streams don't return 404. (#560)
- ilive: Updated for service changes and renamed to streamlive. (#563)
- livestation: Updated for service changes. (#581)
- twitch: Added support for the new video streams.
- vaughnlive: Updated for service changes. (#611)
- veetle: Fixed shortcut URLs, patch by @monkeyphysics. (#601)
- viasat/viagame: Updated for service changes (#564, #566, #617)
Plugin API:
- Added a class to simplify mapping data to stream objects.
Version 1.10.2 (2014-09-05)
---------------------------
Plugins:
- Added plugin for Arte.tv (artetv). (#457)
- Added plugin for RTVE.es (rtve), patch by @jaimeMF. (#509)
- Added plugin for Seemeplay.ru (seemeplay). (#510)
- euronews: Updated for service changes.
- filmon: Updated for service changes. (#514)
- gaminglive: Updated for service changes, patch by @chhe. (#524)
- twitch: Now handles videos with chunks that are missing URLs.
- vaughnlive: Added support for breakers.tv, instagib.tv and vapers.tv. (#521)
- youtube: Added support for audio-only streams. (#522)
Version 1.10.1 (2014-08-22)
---------------------------
Bug fixes:
- Fixed strange read error caused by double buffering in FLV playlists.
Plugins:
- Added plugin for Vaughn Live (vaughnlive). (#478)
Version 1.10.0 (2014-08-18)
---------------------------
Bug fixes:
- The HDS options added in 1.8.0 where never actually applied when
used via the CLI, oops.
- Fixed default player paths not expanding ~, patch by @medina. (#484)
CLI:
- Added :option:`--hds-segment-threads` option.
- Added :option:`--hls-segment-threads` option.
- Added :option:`--stream-segment-attempts` option.
- Added :option:`--stream-segment-threads` option.
- Added :option:`--stream-segment-timeout` option.
- Added :option:`--stream-timeout` option.
- Deprecated the :option:`--jtv-cookie` option.
- Deprecated the :option:`--jtv-password` option.
- Significantly improved the status line printed while writing a
stream to a file. (#462)
Plugins:
- Added plugin for goodgame.ru (goodgame), patch by @eltiren. (#466)
- Added plugin for gaminglive.tv (gaminglive), patch by @chhe. (#468)
- Added plugin for douyutv.com (douyutv), patch by @nixxquality. (#469)
- Added plugin for NHK World (nhkworld).
- Added plugin for Let On TV (letontv), patch by @cheah. (#500)
- Removed plugin: justintv.
- afreecatv: Updated for service changes. (#488)
- hitbox: Added support for HLS videos.
- twitch: Fixed some Twitch broadcasts being unplayable. (#490)
- ustreamtv: Fixed regression that caused channels using RTMP streams to fail.
Streams:
- akamaihd: Now supports background buffering.
- http: Now supports background buffering.
API:
- Added new session option: ``hds-segment-threads``.
- Added new session option: ``hls-segment-threads``.
- Added new session option: ``stream-segment-attempts``.
- Added new session option: ``stream-segment-threads``.
- Added new session option: ``stream-segment-timeout``.
- Added new session option: ``stream-timeout``.
Version 1.9.0 (2014-07-22)
--------------------------
General:
- **Dropped support for Python 3.2.** This is due to missing features
which are necessary for this projects progression.
- `singledispatch `_ is now a
dependency on Python <3.4.
Bug fixes:
- Handle bad input data better in parse_json/xml. (#440)
- Handle bad input data in config files. (#432)
- Fixed regression causing rtmpdump proxies to have no effect.
CLI:
- Improved :option:`--help` significantly, more readable and more content.
- Added :option:`--config` option.
- Added :option:`--stream-url` option. (#281)
- Added support for K and M suffixes to the :option:`--ringbuffer-size` option.
- Added support for loading config files based on plugin.
- Added ~/Applications to the search path for VLC on Mac OS X, patch by @maxnordlund. (#454)
- Deprecated :option:`--best-stream-default` and added :option:`--default-stream`
as a more flexible replacement. (#381)
- Will now only warn about newer versions available every 6 hours.
Plugins:
- Many plugins have been refactored to use the validation API and better coding standards.
- Added plugin for Aftonbladet (aftonbladet).
- Added plugin for ARD Live (ard_live), patch by @MasterofJOKers. (#419)
- Added plugin for ARD Mediathek (ard_mediathek), patch by @yeeeargh. (#421)
- Added plugin for Connect Cast (connectcast). (#423)
- Added plugin for Danmarks Radio (drdk).
- Added plugin for DOMMUNE (dommune).
- Added plugin for TV4 Play (tv4play).
- Added plugin for VGTV (vgtv), patch by @jantore. (#435)
- Removed plugin: cast3d
- Removed plugin: freedocast
- Removed plugin: hashd
- Removed plugin: ongamenet
- afreecatv: Updated for service changes. (#412, #413)
- dailymotion: Added support for source streams, patch by @kasper93. (#428)
- euronews: Added support for videos.
- nrk: Added support for radio.nrk.no, patch by @jantore. (#433)
- picarto: Updated for service changes. (#431)
- twitch: Added support for audio only streams, patch by @CommanderRoot. (#411)
- viasat: Added support for HDS streams.
- viasat: Added support for viagame.com.
API:
- Added :func:`Livestreamer.streams` method.
- Added :func:`livestreamer.streams` function.
- Renamed :func:`Plugin.get_streams` to :func:`Plugin.streams`.
Plugin API:
- Added a validation API to make validating data easier and safer.
Version 1.8.2 (2014-05-30)
--------------------------
Bug fixes:
- Fixed regression in loading config from non-ascii paths on Python 2.
Plugins:
- azubutv: Update for service changes, patch by Gapato. (#399)
- dailymotion: Added support for VODs, patch by Gapato. (#402)
- hitbox: Fixed a issue where the correct streaming server was not used.
Streams:
- hls: Handle playlists that redirect. (#405)
Version 1.8.1 (2014-05-18)
--------------------------
General:
- Added a wheel package to PyPi for speedier installation via pip.
Bug fixes:
- hls: Handle encrypted segments that are invalid length (not multiple by 16). (#365)
Plugins:
- Added plugin for Furstream, patch by Pascal Romahn. (#360)
- Added plugin for Viasat's play sites (tv6play.se, etc). (#378)
- Added plugin for ZDFmediathek, patch by Pascal Romahn. (#360)
- azubutv: Updated for service changes. (#373)
- crunchyroll: Correctly handle unicode errors, patch by Agustin Carrasco. (#387, #388)
- filmon: Updated for service changes, patch by Athanasios Oikonomou. (#375)
- hitbox: Updated for service changes.
- ilive: Updated for service changes, patch by Athanasios Oikonomou. (#376)
- svtplay: Added support for SVT Flow.
- twitch: Now uses the beta API on beta.twitch.tv URLs. (#391)
- ustream: Correctly handle UHS streams containing only video or audio.
Version 1.8.0 (2014-04-21)
--------------------------
CLI:
- Added option: ``--no-version-check``
- Added HTTP options: ``--http-cookies``,
``--http-headers``,
``--http-query-params``,
``--http-ignore-env``,
``--http-no-ssl-verify``,
``--http-ssl-cert``,
``--http-ssl-cert-crt-key`` and
``--http-timeout``
- Added HTTP stream option: ``--http-stream-timeout``
- Added HDS stream options: ``--hds-segment-attempts``,
``--hds-segment-timeout``
``--hds-timeout``
- Added HLS stream options: ``--hls-live-edge``,
``--hls-segment-attempts``,
``--hls-segment-timeout`` and
``--hls-timeout``
- Added RTMP stream option: ``--rtmp-timeout``
- Added plugin options: ``--livestation-email`` and ``--livestation-password``
- Added stream options: ``--retry-streams``,
``--retry-open`` and
``--best-stream-default``
- Deprecated option: ``--hds-fragment-buffer``
Plugins:
- Added plugin for Bambuser, patch by Athanasios Oikonomou. (#327)
- Added plugin for Be-at.tv, patch by Athanasios Oikonomou. (#342)
- Added plugin for Chaturbate, patch by papplampe. (#337)
- Added plugin for Cybergame.tv, patch by Athanasios Oikonomou. (#324)
- Added plugin for Picarto, patch by papplampe. (#352)
- Added plugin for SpeedRunsLive, patch by Stefan Breunig. (#335)
- Removed plugins for dead services: Owncast.me and YYCast.
- azubutv: Added support for beta.azubu.tv.
- crunchyroll: Added workaround for SSL verification issue.
- dailymotion: Added support for HDS streams. (#348)
- gomexp: Fixed encoding issue on Python 2.
- livestation: Added support for logging in, patch by Sunaga Takahiro. (#344)
- mlgtv: Removed the ``mobile_`` prefix from the HLS streams.
- twitch: Added workaround for SSL verification issue. (#255)
- ustreamtv: Improved UHS stream stability.
- ustreamtv: Added support for RTMP VODs.
- youtube: Updated for service changes.
- youtube: Added support for embed URLs, patch by Athanasios Oikonomou.
- youtube: Now only picks up live streams from channel pages.
General:
- Now attempts to resolve URL redirects such as URL shorterners.
Bug fixes:
- Added workaround for HTTP streams not applying read timeout on some requests versions.
API:
- Added new options: ``hds-segment-attempts``,
``hds-segment-timeout``,
``hds-timeout``,
``hls-live-edge``,
``hls-segment-attempts``,
``hls-segment-timeout``,
``hls-timeout``,
``http-proxy``,
``https-proxy``,
``http-cookies``,
``http-headers``,
``http-query-params``,
``http-trust-env``,
``http-ssl-verify``,
``http-ssl-cert``,
``http-timeout``,
``http-stream-timeout`` and
``rtmp-timeout``
- Renamed option ``errorlog`` to ``subprocess-errorlog``.
- Renamed option ``rtmpdump-proxy`` to ``rtmp-proxy``.
- Renamed option ``rtmpdump`` to ``rtmp-rtmpdump``.
Version 1.7.5 (2014-03-07)
--------------------------
Plugins:
- filmon: Added VOD support, patch by Athanasios Oikonomou.
- ilive: Added support for HLS streams, patch by Athanasios Oikonomou.
- mlgtv: Updated for service changes.
- veetle: Now handles shortened URLs, patch by Athanasios Oikonomou.
- youtube: Updated for service changes.
Bug fixes:
- Fixed gzip not getting decoded in streams.
Other:
- Added scripts to automatically create Windows builds via Travis CI.
Builds are available here: http://livestreamer-builds.s3.amazonaws.com/builds.html
Version 1.7.4 (2014-02-28)
--------------------------
Plugins:
- Added plugin for MLG.tv. (#275)
- Added plugin for DMCloud, patch by Athanasios Oikonomou. (#297)
- Added plugin for NRK TV, patch by Jon Bergli Heier. (#309)
- Added plugin for GOMeXP.com.
- Removed GOMTV.net plugin as the service no longer exists.
- mips: Fixed issue with case sensitive playpath. (#306)
- ilive: Added missing app parameter. (#293)
- ustreamtv: Added support for password protected streams via ``--ustream-password``.
- youtube: Now handles youtu.be shortcuts, patch by Andy Mikhailenko. (#288)
- youtube: Use first available stream found on channel pages, patch by "unintended". (#291)
Streams:
- hds: Fixed segmented streams logic, patch by Moritz Blanke.
Bug fixes:
- Fixed buffer overwriting issue when passing a memoryview, patch by Martin Panter. (#295)
- Avoid a ResourceWarning when using ``--player-continuous-http``, patch by Martin Panter. (#296)
Version 1.7.3 (2014-01-31)
--------------------------
Plugins:
- Added plugin for hitbox.tv, patch by t0mm0. (#248)
- Added plugin for Crunchyroll, patch by Agustín Carrasco. (#262)
- twitch: Added support for hours in ?t=... on VODs.
- twitch: Added support for ?t=... on VOD highlights.
Streams:
- hls: Now allows retries on failed segment fetch.
Bug fixes:
- cli: Don't pass our proxy settings to the player. (#260)
- hds: Now uses global height as stream name if needed when parsing manifests.
- hls: Always use first stream for each quality in variant playlists. (#256)
- hls: Now returns correct exception on playlist parser errors.
- hls: Now remembers cookies set by variant playlist response. (#258)
Version 1.7.2 (2013-12-17)
--------------------------
CLI:
- The ``--twitch-legacy-names`` option is now deprecated.
- Added ``--twitch-oauth-authenticate`` and ``--twitch-oauth-token`` options.
Plugins:
- filmon: Added quality weights. (#239)
- filmon_us: Added support for VODs, patch by John Peterson. (#237)
- twitch: Updated for service changes. No more RTMP streams, only HLS.
- twitch: Removed mobile streams since they are the same as the new desktop streams.
- twitch: Removed the legacy names option.
- twitch: Added support for OAuth2 authentication.
- twitch: Added support for the t=00m0s parameter in VOD URLs.
Bug fixes:
- Always wait for the player process to exit, patch by Martin Panter. (#234)
- Fixed potential deadlocking when using named pipe, patch by Martin Panter. (#236)
- Fixed issue with spaces in default player path, patch by John Peterson. (#237)
Version 1.7.1 (2013-12-07)
--------------------------
Plugins:
- Added FilmOn Social TV plugin by John Peterson. (#225)
- twitch: Support mobile_source quality, patch by Andrew Bashore.
Streams:
- hds: Will now use video height as stream names if available.
- hds: Removed the use of movie identifier in the fragment URLs.
- hds: Added support for player verification, patch by Martin Panter. (#222)
Bug fixes:
- Fixed various Python warnings, patch by Martin Panter. (#221)
- cli: Fixed back-slash issue in ``--player-args``. (#218)
- hds: Fixed some streams complaining about the hardcoded hdcore parameter.
- hls: Fixed live streams that keep all previous segments in the playlists. (#224)
- setup.py now forces requests 1.x on Python <2.6.3. (#219)
Version 1.7.0 (2013-11-07)
--------------------------
CLI:
- Added a ``--player-no-close`` option.
- Added options to use HTTP proxies with ``--http-proxy`` and ``--https-proxy``.
- It's now possible to specify multiple streams as a comma-separated
list. If a stream is not available the next one in the list will be tried.
- Now only resolves synonyms once when using ``--player-continuous-http``.
- Removed the ``-u`` shortcut for ``--plugins``. This is a response to someone
spreading the misinformation that ``-url`` is a sane parameter to use.
It's technically valid, but due to the ``-u`` shortcut it would be
interpreted by Python's argparse as ``--plugins --rtmpdump l`` which
would cause livestreamer to look for a non-existing rtmpdump executable,
thus disabling any RTMP streams. (#193)
Plugins:
- Added Afreeca.tv plugin.
- dailymotion: Fixed incorrect RTMP parameters. (#201)
- filmon: Updated after service changes. Patch by Athanasios Oikonomou. (#205)
- ilive: Updated after service changes. (#200)
- livestream: Added support for HLS streams.
- livestream: Updated after service changes. (#195)
- mips: Updated after service changes. (#200)
- svtplay: Fixed some broken HDS streams. (#200)
- twitch: Updated to use the new HLS API.
- weeb: Updated after service changes. Patch by Athanasios Oikonomou. (#207)
- youtube: Now handles 3D streams properly. (#202)
Streams:
- hds: Added support for global bootstraps.
- hls: Rewrote the playlist parser from scratch to be more solid and correct
in accordance to the latest M3U8 spec.
- hls: Now supports playlists using EXT-X-BYTERANGE.
- hls: Now supports playlists using multiple EXT-X-KEY tags.
- hls: Now accepts extra requests parameters to be used when doing
HTTP requests.
Bug fixes:
- Fixed bytes-serialization when using ``--json``.
Version 1.6.1 (2013-10-07)
--------------------------
Bug fixes:
- CLI: Fixed broken ``--player-http`` and ``--player-continuous-http`` on Windows.
- CLI: Fixed un-quoted player paths containing backslashes being broken.
Version 1.6.0 (2013-09-29)
--------------------------
General:
- All stream names are now forced to lowercase to avoid issues with
services renaming streams. (#179)
- Updated requests compatibility to 2.0. (#183)
Plugins:
- Added plugin for Hashd.tv by kasper93. (#184)
- Azubu.tv: Updated after service changes. (#170)
- ILive.to: Updated after service changes. (#182)
- Twitch/Justin.tv: Refactored and split into separate plugins.
- Added support for archived streams (VOD). (#70)
- Added a option to force legacy stream names (720p, 1080p+, etc).
- Added a option to access password protected streams.
- UStream.tv: Refactored plugin and added support for their RTMP API and
special streaming technology (UHS). (#144)
CLI:
- Added some more player options: ``--player-args``, ``--player-http``,
``--player-continuous-http`` and ``--player-passthrough``. (#131)
- Expanded ``--stream-sorting-excludes`` to support more advanced
filtering. (#159)
- Now notifies the user if a new version of Livestreamer is available.
- Now allows case-insensitive stream name lookup.
API:
- Added a new exception (``LivestreamerError``) that all other exceptions
inherit from.
- The ``sorting_excludes`` parameter in ``Plugin.get_streams``
now supports more advanced filtering. (#159)
Bug fixes:
- Fixed HTTPStream with headers breaking ``--json`` on Python 3.
Version 1.5.2 (2013-08-27)
--------------------------
Plugins:
- Twitch/Justin.tv: Fix stream names.
Version 1.5.1 (2013-08-13)
--------------------------
Plugins:
- Added plugin for Filmon.
- Twitch/Justin.tv: Safer cookie and SWF URL handling.
- Youtube: Enable VOD support.
Bug fixes:
- Fixed potential crash when invalid UTF-8 is passed as arguments
to subprocesses.
Version 1.5.0 (2013-07-18)
--------------------------
CLI:
- Handle SIGTERM as SIGINT.
- Improved default player (VLC) detection.
- --stream-priority renamed to --stream-types and now excludes
any stream types not specified.
- Added --stream-sorting-excludes which excludes streams
from the internal sorting used by best/worst synonyms.
- Now returns exit code 1 on errors.
API:
- plugin.get_streams(): Renamed priority parameter to stream_types
and changed behaviour slightly.
- plugin.get_streams(): Added the parameter sorting_excludes.
Plugins:
- Added plugin for Aliez.tv.
- Added plugin for Weeb.tv.
- Added plugin for Veetle.
- Added plugin for Euronews.
- Dailymotion: Updated for JSON result changes.
- Livestream: Added SWF verification.
- Stream: Added httpstream://.
- Stream: Now evaluates parameters as Python values.
- Twitch/Justin.tv: Fixed HLS stream names.
- Youtube Live: Improved stream names.
Version 1.4.5 (2013-05-11)
--------------------------
Plugins:
- Twitch/Justin.tv: Fixed mobile transcode request never happening.
- GOMTV.net: Fixed issue causing disabled streams to be picked up.
- Azubu.tv: Updated for HTML change.
Streams:
- HLS: Fixed potential crash when getting a invalid playlist.
Version 1.4.4 (2013-05-03)
--------------------------
Plugins:
- Twitch/Justin.tv: Fixed possible crash on Python 3.
- Ilive.to: HTML parsing fixes by Sam Edwards.
Version 1.4.3 (2013-05-01)
--------------------------
CLI:
- Major refactoring of the code base.
- Now respects the XDG Base Directory Specification.
Will attempt to load config and plugins from the following paths:
- $XDG_CONFIG_HOME/livestreamer/config
- $XDG_CONFIG_HOME/livestreamer/plugins/
- The option --quiet-player is now deprecated since
it is now the default behaviour. A new option --verbose-player
was added to show the player's console output.
- The option --cmdline now prints arguments in quotes.
- Print error message if the player fails to start.
Plugins:
- Added a cache plugins can use to store data
that does not need to be generated on every run.
- Added Azubu.tv plugin.
- Added owncast.me plugin by Athanasios Oikonomou.
- Youtube: Updated for HTML changes.
- GOMTV.net:
- Fixed incorrect cookie names
- Stream names are now more consistent
- Added support for Limelight streams
- Twitch/Justin.tv:
- Fixed SWF verification issues
- The HLS streams available are now higher quality
Streams:
- Minor improvements and fixes to HDS.
Bug fixes:
- Properly fixed named pipe support on Windows.
Version 1.4.2 (2013-03-01)
--------------------------
CLI:
- Attempt to find VLC locations on OS X and Windows.
- Added --stream-priority parameter.
- Added --json parameter which makes livestreamer output JSON,
useful for scripting in other languages.
- Handle player exit cleaner by using SIGPIPE.
Plugins:
- UStream: Now falls back on alternative CDNs when neccessary and added
support for embed URLs.
- Added ilive.to plugin by Athanasios Oikonomou.
- Added cast3d.tv plugin by Athanasios Oikonomou.
- streamingvideoprovider.co.uk: Added support for RTMP streams.
- GOMTV.net: Major refactoring and also added support Adobe HDS streams.
- SVTPlay: Added support for Adobe HDS streams.
- Twitch/Justin.tv: Some minor tweaks and fixes.
- Ongamenet: Update to URL and HTML changes.
- Livestream.com: Update for HTML changes.
Streams:
- Minor improvements and fixes to HLS.
- Added support for Adobe HDS streams.
General:
- Removed cache parameter from default player, since they do not work
on older versions of VLC.
- Added meta-stream "worst".
- Removed sh dependancy and embeded pbs instead.
Bug fixes:
- Fix named pipes on Windows x64.
API:
- Added optional priority argument to Plugin.get_streams.
- Improved docstrings.
Version 1.4.1 (2012-12-20)
--------------------------
CLI:
- Added --ringbuffer-size option.
Plugins:
- Fixed problem with UStream plugin and latest RTMPDump.
- Added freedocast.com plugin by Athanasios Oikonomou.
- Added livestation.com plugin by Athanasios Oikonomou.
- Added mips.tv plugin by Athanasios Oikonomou.
- Added streamingvideoprovider.co.uk plugin by Athanasios Oikonomou.
- Added stream plugin that handles URLs such as hls://, rtmp://, etc.
- Added yycast.com plugin by Athanasios Oikonomou.
Streams:
- Refactored the HLS stream support.
General:
- Bumped requests version requirement to 1.0.
- Bumped sh version requirement to 1.07.
Version 1.4 (2012-11-23)
------------------------
CLI:
- Added --rtmpdump-proxy option.
- Added --plugin-dirs option.
- Now automatically attempts to use secondary stream CDNs when primary fails.
Plugins:
- Added Dailymotion plugin by Gaspard Jankowiak.
- Added livestream.com plugin.
- Added VOD support to GOMTV plugin.
- Twitch plugin now finds HLS streams.
- own3D.tv plugin now finds more CDNs.
- Fixed bugs in Youtube and GOMTV plugin.
- Refactored UStream plugin.
Streams:
- Added support for AkamaiHD HTTP streams.
General:
- Added unit tests, still fairly small coverage though.
- Added travis-ci integration.
- Now using python-sh on *nix since python-pbs is deprecated.
livestreamer-1.12.2/tests/ 0000755 0001750 0001750 00000000000 12521217500 016532 5 ustar chrippa chrippa 0000000 0000000 livestreamer-1.12.2/tests/test_stream_wrappers.py 0000644 0001750 0001750 00000001302 12521217321 023356 0 ustar chrippa chrippa 0000000 0000000 import unittest
from livestreamer.stream 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)
if __name__ == "__main__":
unittest.main()
livestreamer-1.12.2/tests/test_session.py 0000644 0001750 0001750 00000007435 12521217321 021640 0 ustar chrippa chrippa 0000000 0000000 import os
import unittest
from livestreamer import Livestreamer, PluginError, NoPluginError
from livestreamer.plugins import Plugin
from livestreamer.stream import *
class TestSession(unittest.TestCase):
PluginPath = os.path.join(os.path.dirname(__file__), "plugins")
def setUp(self):
self.session = Livestreamer()
self.session.load_plugins(self.PluginPath)
def test_exceptions(self):
try:
self.session.resolve_url("invalid url")
self.assertTrue(False)
except NoPluginError:
self.assertTrue(True)
def test_load_plugins(self):
plugins = self.session.get_plugins()
self.assertTrue(plugins["testplugin"])
def test_builtin_plugins(self):
plugins = self.session.get_plugins()
self.assertTrue("twitch" in plugins)
def test_resolve_url(self):
plugins = self.session.get_plugins()
channel = self.session.resolve_url("http://test.se/channel")
self.assertTrue(isinstance(channel, Plugin))
self.assertTrue(isinstance(channel, plugins["testplugin"]))
def test_options(self):
self.session.set_option("test_option", "option")
self.assertEqual(self.session.get_option("test_option"), "option")
self.assertEqual(self.session.get_option("non_existing"), None)
self.assertEqual(self.session.get_plugin_option("testplugin", "a_option"), "default")
self.session.set_plugin_option("testplugin", "another_option", "test")
self.assertEqual(self.session.get_plugin_option("testplugin", "another_option"), "test")
self.assertEqual(self.session.get_plugin_option("non_existing", "non_existing"), None)
self.assertEqual(self.session.get_plugin_option("testplugin", "non_existing"), None)
def test_plugin(self):
channel = self.session.resolve_url("http://test.se/channel")
streams = channel.get_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["rtmp"], RTMPStream))
self.assertTrue(isinstance(streams["http"], HTTPStream))
self.assertTrue(isinstance(streams["hls"], HLSStream))
self.assertTrue(isinstance(streams["akamaihd"], AkamaiHDStream))
def test_plugin_stream_types(self):
channel = self.session.resolve_url("http://test.se/channel")
streams = channel.get_streams(stream_types=["http", "rtmp"])
self.assertTrue(isinstance(streams["480p"], HTTPStream))
self.assertTrue(isinstance(streams["480p_rtmp"], RTMPStream))
streams = channel.get_streams(stream_types=["rtmp", "http"])
self.assertTrue(isinstance(streams["480p"], RTMPStream))
self.assertTrue(isinstance(streams["480p_http"], HTTPStream))
def test_plugin_stream_sorted_excludes(self):
channel = self.session.resolve_url("http://test.se/channel")
streams = channel.get_streams(sorting_excludes=["1080p", "3000k"])
self.assertTrue("best" in streams)
self.assertTrue("worst" in streams)
self.assertTrue(streams["best"] is streams["1500k"])
streams = channel.get_streams(sorting_excludes=[">=1080p", ">1500k"])
self.assertTrue(streams["best"] is streams["1500k"])
streams = channel.get_streams(sorting_excludes=lambda q: not q.endswith("p"))
self.assertTrue(streams["best"] is streams["3000k"])
def test_plugin_support(self):
channel = self.session.resolve_url("http://test.se/channel")
streams = channel.get_streams()
self.assertTrue("support" in streams)
self.assertTrue(isinstance(streams["support"], HTTPStream))
if __name__ == "__main__":
unittest.main()
livestreamer-1.12.2/tests/test_plugin_stream.py 0000644 0001750 0001750 00000006367 12521217321 023031 0 ustar chrippa chrippa 0000000 0000000 import os
import unittest
from livestreamer import Livestreamer, PluginError, NoPluginError
from livestreamer.plugins import Plugin
from livestreamer.stream import *
class TestPluginStream(unittest.TestCase):
def setUp(self):
self.session = Livestreamer()
def assertDictHas(self, a, b):
for key, value in a.items():
self.assertEqual(b[key], value)
def _test_akamaihd(self, surl, url):
channel = self.session.resolve_url(surl)
streams = channel.get_streams()
self.assertTrue("live" in streams)
stream = streams["live"]
self.assertTrue(isinstance(stream, AkamaiHDStream))
self.assertEqual(stream.url, url)
def _test_hls(self, surl, url):
channel = self.session.resolve_url(surl)
streams = channel.get_streams()
self.assertTrue("live" in streams)
stream = streams["live"]
self.assertTrue(isinstance(stream, HLSStream))
self.assertEqual(stream.url, url)
def _test_rtmp(self, surl, url, params):
channel = self.session.resolve_url(surl)
streams = channel.get_streams()
self.assertTrue("live" in streams)
stream = streams["live"]
self.assertTrue(isinstance(stream, RTMPStream))
self.assertEqual(stream.params["rtmp"], url)
self.assertDictHas(params, stream.params)
def _test_http(self, surl, url, params):
channel = self.session.resolve_url(surl)
streams = channel.get_streams()
self.assertTrue("live" in streams)
stream = streams["live"]
self.assertTrue(isinstance(stream, HTTPStream))
self.assertEqual(stream.url, url)
self.assertDictHas(params, stream.args)
def test_plugin(self):
self._test_rtmp("rtmp://hostname.se/stream",
"rtmp://hostname.se/stream", dict())
self._test_rtmp("rtmp://hostname.se/stream live=1 num=47",
"rtmp://hostname.se/stream", dict(live=True, num=47))
self._test_rtmp("rtmp://hostname.se/stream live=1 qarg='a \\'string' noq=test",
"rtmp://hostname.se/stream", dict(live=True, qarg='a \'string', noq="test"))
self._test_hls("hls://https://hostname.se/playlist.m3u8",
"https://hostname.se/playlist.m3u8")
self._test_hls("hls://hostname.se/playlist.m3u8",
"http://hostname.se/playlist.m3u8")
self._test_akamaihd("akamaihd://http://hostname.se/stream",
"http://hostname.se/stream")
self._test_akamaihd("akamaihd://hostname.se/stream",
"http://hostname.se/stream")
self._test_http("httpstream://http://hostname.se/auth.php auth=('test','test2')",
"http://hostname.se/auth.php", dict(auth=("test", "test2")))
self._test_http("httpstream://hostname.se/auth.php auth=('test','test2')",
"http://hostname.se/auth.php", dict(auth=("test", "test2")))
self._test_http("httpstream://https://hostname.se/auth.php verify=False params={'key': 'a value'}",
"https://hostname.se/auth.php?key=a+value", dict(verify=False, params=dict(key='a value')))
if __name__ == "__main__":
unittest.main()
livestreamer-1.12.2/tests/test_plugin_api_validate.py 0000644 0001750 0001750 00000012236 12521217321 024150 0 ustar chrippa chrippa 0000000 0000000 # coding: utf8
import re
import unittest
from xml.etree.ElementTree import Element
from livestreamer.plugin.api.validate import (
validate, all, any, optional, transform, text, filter, map, hasattr,
get, getattr, length, xml_element, xml_find, xml_findtext, xml_findall,
union, attr, url, startswith, endswith
)
class TestPluginAPIValidate(unittest.TestCase):
def test_basic(self):
assert validate(1, 1) == 1
assert validate(int, 1) == 1
assert validate(transform(int), "1") == 1
assert validate(text, "abc") == "abc"
assert validate(text, u"日本語") == u"日本語"
assert validate(transform(text), 1) == "1"
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_union(self):
assert validate(union((get("foo"), get("bar"))),
{"foo": "alpha", "bar": "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(set([int]), set([1, 2])) == set([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("invalidkey", "default"), {"key": "value"}) == "default"
def test_get_re(self):
m = re.match("(\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", attrib={"key": "value"})
el.text = "test"
assert validate(xml_element("tag"), el).tag == "tag"
assert validate(xml_element(text="test"), el).text == "test"
assert validate(xml_element(attrib={"key": text}), el).attrib == {"key": "value"}
def test_xml_find(self):
el = Element("parent")
el.append(Element("foo"))
el.append(Element("bar"))
assert validate(xml_find("bar"), el).tag == "bar"
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_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(u"åäö"), u"xyzåäö")
if __name__ == "__main__":
unittest.main()
livestreamer-1.12.2/tests/test_plugin_api_http_session.py 0000644 0001750 0001750 00000001021 12521217321 025067 0 ustar chrippa chrippa 0000000 0000000 import unittest
from livestreamer.exceptions import PluginError
from livestreamer.plugin.api.http_session import HTTPSession
class TestPluginAPIHTTPSession(unittest.TestCase):
def test_read_timeout(self):
session = HTTPSession()
def stream_data():
res = session.get("http://httpbin.org/delay/6",
timeout=3, stream=True)
next(res.iter_content(8192))
self.assertRaises(PluginError, stream_data)
if __name__ == "__main__":
unittest.main()
livestreamer-1.12.2/tests/test_options.py 0000644 0001750 0001750 00000001010 12521217321 021627 0 ustar chrippa chrippa 0000000 0000000 import unittest
from livestreamer.options import Options
class TestOptions(unittest.TestCase):
def setUp(self):
self.options = Options({
"a_default": "default"
})
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")
if __name__ == "__main__":
unittest.main()
livestreamer-1.12.2/tests/test_log.py 0000644 0001750 0001750 00000001723 12521217321 020730 0 ustar chrippa chrippa 0000000 0000000 import unittest
from livestreamer.logger import Logger
from livestreamer.compat import is_py2
# Docs says StringIO is suppose to take non-unicode strings
# but it doesn't, so let's use BytesIO instead there...
if is_py2:
from io import BytesIO as StringIO
else:
from io import StringIO
class TestSession(unittest.TestCase):
def setUp(self):
self.output = StringIO()
self.manager = Logger()
self.manager.set_output(self.output)
self.logger = self.manager.new_module("test")
def test_level(self):
self.logger.debug("test")
self.assertEqual(self.output.tell(), 0)
self.manager.set_level("debug")
self.logger.debug("test")
self.assertNotEqual(self.output.tell(), 0)
def test_output(self):
self.manager.set_level("debug")
self.logger.debug("test")
self.assertEqual(self.output.getvalue(), "[test][debug] test\n")
if __name__ == "__main__":
unittest.main()
livestreamer-1.12.2/tests/test_buffer.py 0000644 0001750 0001750 00000004505 12521217321 021421 0 ustar chrippa chrippa 0000000 0000000 import unittest
from livestreamer.buffers import Buffer
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)]
try:
m = memoryview(bytearray(original))
except NameError: # Python 2.6 does not have "memoryview"
pass
else:
# Python 2.7 doesn't do bytes(memoryview) properly
if bytes(m) == original:
tests.append(m)
for data in tests:
self.buffer.write(data)
data[:] = b"reused!!"
self.assertEqual(self.buffer.read(), original)
if __name__ == "__main__":
unittest.main()
livestreamer-1.12.2/tests/__init__.py 0000644 0001750 0001750 00000000000 12521217321 020632 0 ustar chrippa chrippa 0000000 0000000 livestreamer-1.12.2/tests/plugins/ 0000755 0001750 0001750 00000000000 12521217500 020213 5 ustar chrippa chrippa 0000000 0000000 livestreamer-1.12.2/tests/plugins/testplugin_support.py 0000644 0001750 0001750 00000000215 12521217321 024556 0 ustar chrippa chrippa 0000000 0000000 from livestreamer.stream import HTTPStream
def get_streams(session):
return dict(support=HTTPStream(session, "http://test.se/support"))
livestreamer-1.12.2/tests/plugins/testplugin.py 0000644 0001750 0001750 00000003013 12521217321 022761 0 ustar chrippa chrippa 0000000 0000000 from livestreamer.plugins import Plugin
from livestreamer.options import Options
from livestreamer.stream import *
from livestreamer.plugin.api.support_plugin import testplugin_support
class TestPlugin(Plugin):
options = Options({
"a_option": "default"
})
@classmethod
def can_handle_url(self, url):
return "test.se" in url
def _get_streams(self):
streams = {}
streams["rtmp"] = RTMPStream(self.session, dict(rtmp="rtmp://test.se"))
streams["hls"] = HLSStream(self.session, "http://test.se/playlist.m3u8")
streams["http"] = HTTPStream(self.session, "http://test.se/stream")
streams["akamaihd"] = AkamaiHDStream(self.session, "http://test.se/stream")
streams["240p"] = HTTPStream(self.session, "http://test.se/stream")
streams["360p"] = HTTPStream(self.session, "http://test.se/stream")
streams["1080p"] = HTTPStream(self.session, "http://test.se/stream")
streams["350k"] = HTTPStream(self.session, "http://test.se/stream")
streams["800k"] = HTTPStream(self.session, "http://test.se/stream")
streams["1500k"] = HTTPStream(self.session, "http://test.se/stream")
streams["3000k"] = HTTPStream(self.session, "http://test.se/stream")
streams["480p"] = [HTTPStream(self.session, "http://test.se/stream"),
RTMPStream(self.session, dict(rtmp="rtmp://test.se"))]
streams.update(testplugin_support.get_streams(self.session))
return streams
__plugin__ = TestPlugin
livestreamer-1.12.2/tests/plugins/__init__.py 0000644 0001750 0001750 00000000000 12521217321 022313 0 ustar chrippa chrippa 0000000 0000000 livestreamer-1.12.2/src/ 0000755 0001750 0001750 00000000000 12521217500 016157 5 ustar chrippa chrippa 0000000 0000000 livestreamer-1.12.2/src/livestreamer_cli/ 0000755 0001750 0001750 00000000000 12521217500 021510 5 ustar chrippa chrippa 0000000 0000000 livestreamer-1.12.2/src/livestreamer_cli/output.py 0000644 0001750 0001750 00000011054 12521217321 023424 0 ustar chrippa chrippa 0000000 0000000 import os
import shlex
import subprocess
import sys
from time import sleep
from .compat import is_win32, stdout
from .constants import DEFAULT_PLAYER_ARGUMENTS
from .utils import ignored
if is_win32:
import msvcrt
class Output(object):
def __init__(self):
self.opened = False
def open(self):
self._open()
self.opened = True
def close(self):
if self.opened:
self._close()
self.opened = False
def write(self, data):
if not self.opened:
raise IOError("Output is not opened")
return self._write(data)
def _open(self):
pass
def _close(self):
pass
def _write(self, data):
pass
class FileOutput(Output):
def __init__(self, filename=None, fd=None):
self.filename = filename
self.fd = fd
def _open(self):
if self.filename:
self.fd = open(self.filename, "wb")
if is_win32:
msvcrt.setmode(self.fd.fileno(), os.O_BINARY)
def _close(self):
if self.fd is not stdout:
self.fd.close()
def _write(self, data):
self.fd.write(data)
class PlayerOutput(Output):
def __init__(self, cmd, args=DEFAULT_PLAYER_ARGUMENTS,
filename=None, quiet=True, kill=True,
call=False, http=False, namedpipe=None):
self.cmd = cmd
self.args = args
self.kill = kill
self.call = call
self.quiet = quiet
self.filename = filename
self.namedpipe = namedpipe
self.http = http
if self.namedpipe or self.filename or self.http:
self.stdin = sys.stdin
else:
self.stdin = subprocess.PIPE
if self.quiet:
self.stdout = open(os.devnull, "w")
self.stderr = open(os.devnull, "w")
else:
self.stdout = sys.stdout
self.stderr = sys.stderr
@property
def running(self):
sleep(0.5)
self.player.poll()
return self.player.returncode is None
def _create_arguments(self):
if self.namedpipe:
filename = self.namedpipe.path
elif self.filename:
filename = self.filename
elif self.http:
filename = self.http.url
else:
filename = "-"
args = self.args.format(filename=filename)
cmd = self.cmd
if is_win32:
# We want to keep the backslashes on Windows as forcing the user to
# escape backslashes for paths would be inconvenient.
cmd = cmd.replace("\\", "\\\\")
args = args.replace("\\", "\\\\")
return shlex.split(cmd) + shlex.split(args)
def _open(self):
try:
if self.call and self.filename:
self._open_call()
else:
self._open_subprocess()
finally:
if self.quiet:
# Output streams no longer needed in parent process
self.stdout.close()
self.stderr.close()
def _open_call(self):
subprocess.call(self._create_arguments(),
stdout=self.stdout,
stderr=self.stderr)
def _open_subprocess(self):
# Force bufsize=0 on all Python versions to avoid writing the
# unflushed buffer when closing a broken input pipe
self.player = subprocess.Popen(self._create_arguments(),
stdin=self.stdin, bufsize=0,
stdout=self.stdout,
stderr=self.stderr)
# Wait 0.5 seconds to see if program exited prematurely
if not self.running:
raise OSError("Process exited prematurely")
if self.namedpipe:
self.namedpipe.open("wb")
elif self.http:
self.http.open()
def _close(self):
# Close input to the player first to signal the end of the
# stream and allow the player to terminate of its own accord
if self.namedpipe:
self.namedpipe.close()
elif self.http:
self.http.close()
elif not self.filename:
self.player.stdin.close()
if self.kill:
with ignored(Exception):
self.player.kill()
self.player.wait()
def _write(self, data):
if self.namedpipe:
self.namedpipe.write(data)
elif self.http:
self.http.write(data)
else:
self.player.stdin.write(data)
__all__ = ["PlayerOutput", "FileOutput"]
livestreamer-1.12.2/src/livestreamer_cli/main.py 0000644 0001750 0001750 00000072571 12521217321 023023 0 ustar chrippa chrippa 0000000 0000000 import errno
import os
import requests
import sys
import signal
import webbrowser
from contextlib import closing
from distutils.version import StrictVersion
from functools import partial
from itertools import chain
from time import sleep
from livestreamer import (Livestreamer, StreamError, PluginError,
NoPluginError)
from livestreamer.cache import Cache
from livestreamer.stream import StreamProcess
from .argparser import parser
from .compat import stdout, is_win32
from .console import ConsoleOutput
from .constants import CONFIG_FILES, PLUGINS_DIR, STREAM_SYNONYMS
from .output import FileOutput, PlayerOutput
from .utils import NamedPipe, HTTPServer, ignored, progress, stream_to_url
ACCEPTABLE_ERRNO = (errno.EPIPE, errno.EINVAL, errno.ECONNRESET)
QUIET_OPTIONS = ("json", "stream_url", "subprocess_cmdline", "quiet")
args = console = livestreamer = plugin = stream_fd = None
def check_file_output(filename, force):
"""Checks if file already exists and ask the user if it should
be overwritten if it does."""
console.logger.debug("Checking file output")
if os.path.isfile(filename) and not force:
answer = console.ask("File {0} already exists! Overwrite it? [y/N] ",
filename)
if answer.lower() != "y":
sys.exit()
return FileOutput(filename)
def create_output():
"""Decides where to write the stream.
Depending on arguments it can be one of these:
- The stdout pipe
- A subprocess' stdin pipe
- A named pipe that the subprocess reads from
- A regular file
"""
if args.output:
if args.output == "-":
out = FileOutput(fd=stdout)
else:
out = check_file_output(args.output, args.force)
elif args.stdout:
out = FileOutput(fd=stdout)
else:
http = namedpipe = None
if not args.player:
console.exit("The default player (VLC) does not seem to be "
"installed. You must specify the path to a player "
"executable with --player.")
if args.player_fifo:
pipename = "livestreamerpipe-{0}".format(os.getpid())
console.logger.info("Creating pipe {0}", pipename)
try:
namedpipe = NamedPipe(pipename)
except IOError as err:
console.exit("Failed to create pipe: {0}", err)
elif args.player_http:
http = create_http_server()
console.logger.info("Starting player: {0}", args.player)
out = PlayerOutput(args.player, args=args.player_args,
quiet=not args.verbose_player,
kill=not args.player_no_close,
namedpipe=namedpipe, http=http)
return out
def create_http_server(host=None, port=0):
"""Creates a HTTP server listening on a given host and port.
If host is empty, listen on all available interfaces, and if port is 0,
listen on a random high port.
"""
try:
http = HTTPServer()
http.bind(host=host, port=port)
except OSError as err:
console.exit("Failed to create HTTP server: {0}", err)
return http
def iter_http_requests(server, player):
"""Repeatedly accept HTTP connections on a server.
Forever if the serving externally, or while a player is running if it is not
empty.
"""
while not player or player.running:
try:
yield server.open(timeout=2.5)
except OSError:
continue
def output_stream_http(plugin, initial_streams, external=False, port=0):
"""Continuously output the stream over HTTP."""
if not external:
if not args.player:
console.exit("The default player (VLC) does not seem to be "
"installed. You must specify the path to a player "
"executable with --player.")
server = create_http_server()
player = PlayerOutput(args.player, args=args.player_args,
filename=server.url,
quiet=not args.verbose_player)
try:
console.logger.info("Starting player: {0}", args.player)
if player:
player.open()
except OSError as err:
console.exit("Failed to start player: {0} ({1})",
args.player, err)
else:
server = create_http_server(host=None, port=port)
player = None
console.logger.info("Starting server, access with one of:")
for url in server.urls:
console.logger.info(" " + url)
for req in iter_http_requests(server, player):
user_agent = req.headers.get("User-Agent") or "unknown player"
console.logger.info("Got HTTP request from {0}".format(user_agent))
stream_fd = prebuffer = None
while not stream_fd and (not player or player.running):
try:
streams = initial_streams or fetch_streams(plugin)
initial_streams = None
for stream_name in (resolve_stream_name(streams, s) for s in args.stream):
if stream_name in streams:
stream = streams[stream_name]
break
else:
console.logger.info("Stream not available, will re-fetch "
"streams in 10 sec")
sleep(10)
continue
except PluginError as err:
console.logger.error(u"Unable to fetch new streams: {0}", err)
continue
try:
console.logger.info("Opening stream: {0} ({1})", stream_name,
type(stream).shortname())
stream_fd, prebuffer = open_stream(stream)
except StreamError as err:
console.logger.error("{0}", err)
if stream_fd and prebuffer:
console.logger.debug("Writing stream to player")
read_stream(stream_fd, server, prebuffer)
server.close(True)
player.close()
server.close()
def output_stream_passthrough(stream):
"""Prepares a filename to be passed to the player."""
filename = '"{0}"'.format(stream_to_url(stream))
out = PlayerOutput(args.player, args=args.player_args,
filename=filename, call=True,
quiet=not args.verbose_player)
try:
console.logger.info("Starting player: {0}", args.player)
out.open()
except OSError as err:
console.exit("Failed to start player: {0} ({1})", args.player, err)
return False
return True
def open_stream(stream):
"""Opens a stream and reads 8192 bytes from it.
This is useful to check if a stream actually has data
before opening the output.
"""
global stream_fd
# Attempts to open the stream
try:
stream_fd = stream.open()
except StreamError as err:
raise StreamError("Could not open stream: {0}".format(err))
# Read 8192 bytes before proceeding to check for errors.
# This is to avoid opening the output unnecessarily.
try:
console.logger.debug("Pre-buffering 8192 bytes")
prebuffer = stream_fd.read(8192)
except IOError as err:
raise StreamError("Failed to read data from stream: {0}".format(err))
if not prebuffer:
raise StreamError("No data returned from stream")
return stream_fd, prebuffer
def output_stream(stream):
"""Open stream, create output and finally write the stream to output."""
for i in range(args.retry_open):
try:
stream_fd, prebuffer = open_stream(stream)
break
except StreamError as err:
console.logger.error("{0}", err)
else:
return
output = create_output()
try:
output.open()
except (IOError, OSError) as err:
if isinstance(output, PlayerOutput):
console.exit("Failed to start player: {0} ({1})",
args.player, err)
else:
console.exit("Failed to open output: {0} ({1})",
args.output, err)
with closing(output):
console.logger.debug("Writing stream to output")
read_stream(stream_fd, output, prebuffer)
return True
def read_stream(stream, output, prebuffer, chunk_size=8192):
"""Reads data from stream and then writes it to the output."""
is_player = isinstance(output, PlayerOutput)
is_http = isinstance(output, HTTPServer)
is_fifo = is_player and output.namedpipe
show_progress = isinstance(output, FileOutput) and output.fd is not stdout
stream_iterator = chain(
[prebuffer],
iter(partial(stream.read, chunk_size), b"")
)
if show_progress:
stream_iterator = progress(stream_iterator,
prefix=os.path.basename(args.output))
try:
for data in stream_iterator:
# We need to check if the player process still exists when
# using named pipes on Windows since the named pipe is not
# automatically closed by the player.
if is_win32 and is_fifo:
output.player.poll()
if output.player.returncode is not None:
console.logger.info("Player closed")
break
try:
output.write(data)
except IOError as err:
if is_player and err.errno in ACCEPTABLE_ERRNO:
console.logger.info("Player closed")
elif is_http and err.errno in ACCEPTABLE_ERRNO:
console.logger.info("HTTP connection closed")
else:
console.logger.error("Error when writing to output: {0}", err)
break
except IOError as err:
console.logger.error("Error when reading from stream: {0}", err)
stream.close()
console.logger.info("Stream ended")
def handle_stream(plugin, streams, stream_name):
"""Decides what to do with the selected stream.
Depending on arguments it can be one of these:
- Output internal command-line
- Output JSON represenation
- Continuously output the stream over HTTP
- Output stream data to selected output
"""
stream_name = resolve_stream_name(streams, stream_name)
stream = streams[stream_name]
# Print internal command-line if this stream
# uses a subprocess.
if args.subprocess_cmdline:
if isinstance(stream, StreamProcess):
try:
cmdline = stream.cmdline()
except StreamError as err:
console.exit("{0}", err)
console.msg("{0}", cmdline)
else:
console.exit("The stream specified cannot be translated to a command")
# Print JSON representation of the stream
elif console.json:
console.msg_json(stream)
elif args.stream_url:
url = stream_to_url(stream)
if url:
console.msg("{0}", url)
else:
console.exit("The stream specified cannot be translated to a URL")
# Output the stream
else:
# Find any streams with a '_alt' suffix and attempt
# to use these in case the main stream is not usable.
alt_streams = list(filter(lambda k: stream_name + "_alt" in k,
sorted(streams.keys())))
file_output = args.output or args.stdout
for stream_name in [stream_name] + alt_streams:
stream = streams[stream_name]
stream_type = type(stream).shortname()
if stream_type in args.player_passthrough and not file_output:
console.logger.info("Opening stream: {0} ({1})", stream_name,
stream_type)
success = output_stream_passthrough(stream)
elif args.player_external_http:
return output_stream_http(plugin, streams, external=True,
port=args.player_external_http_port)
elif args.player_continuous_http and not file_output:
return output_stream_http(plugin, streams)
else:
console.logger.info("Opening stream: {0} ({1})", stream_name,
stream_type)
success = output_stream(stream)
if success:
break
def fetch_streams(plugin):
"""Fetches streams using correct parameters."""
return plugin.get_streams(stream_types=args.stream_types,
sorting_excludes=args.stream_sorting_excludes)
def fetch_streams_infinite(plugin, interval):
"""Attempts to fetch streams until some are returned."""
try:
streams = fetch_streams(plugin)
except PluginError as err:
console.logger.error(u"{0}", err)
streams = None
if not streams:
console.logger.info("Waiting for streams, retrying every {0} "
"second(s)", args.retry_streams)
while not streams:
sleep(args.retry_streams)
try:
streams = fetch_streams(plugin)
except PluginError as err:
console.logger.error(u"{0}", err)
return streams
def resolve_stream_name(streams, stream_name):
"""Returns the real stream name of a synonym."""
if stream_name in STREAM_SYNONYMS and stream_name in streams:
for name, stream in streams.items():
if stream is streams[stream_name] and name not in STREAM_SYNONYMS:
return name
return stream_name
def format_valid_streams(streams):
"""Formats a dict of streams.
Filters out synonyms and displays them next to
the stream they point to.
"""
delimiter = ", "
validstreams = []
for name, stream in sorted(streams.items()):
if name in STREAM_SYNONYMS:
continue
synonymfilter = lambda n: stream is streams[n] and n is not name
synonyms = list(filter(synonymfilter, streams.keys()))
if len(synonyms) > 0:
joined = delimiter.join(synonyms)
name = "{0} ({1})".format(name, joined)
validstreams.append(name)
return delimiter.join(validstreams)
def handle_url():
"""The URL handler.
Attempts to resolve the URL to a plugin and then attempts
to fetch a list of available streams.
Proceeds to handle stream if user specified a valid one,
otherwise output list of valid streams.
"""
try:
plugin = livestreamer.resolve_url(args.url)
console.logger.info("Found matching plugin {0} for URL {1}",
plugin.module, args.url)
if args.retry_streams:
streams = fetch_streams_infinite(plugin, args.retry_streams)
else:
streams = fetch_streams(plugin)
except NoPluginError:
console.exit("No plugin can handle URL: {0}", args.url)
except PluginError as err:
console.exit(u"{0}", err)
if not streams:
console.exit("No streams found on this URL: {0}", args.url)
if args.best_stream_default:
args.default_stream = ["best"]
if args.default_stream and not args.stream and not args.json:
args.stream = args.default_stream
if args.stream:
validstreams = format_valid_streams(streams)
for stream_name in args.stream:
if stream_name in streams:
console.logger.info("Available streams: {0}", validstreams)
handle_stream(plugin, streams, stream_name)
return
err = ("The specified stream(s) '{0}' could not be "
"found".format(", ".join(args.stream)))
if console.json:
console.msg_json(dict(streams=streams, plugin=plugin.module,
error=err))
else:
console.exit("{0}.\n Available streams: {1}",
err, validstreams)
else:
if console.json:
console.msg_json(dict(streams=streams, plugin=plugin.module))
else:
validstreams = format_valid_streams(streams)
console.msg("Available streams: {0}", validstreams)
def print_plugins():
"""Outputs a list of all plugins Livestreamer has loaded."""
pluginlist = list(livestreamer.get_plugins().keys())
pluginlist_formatted = ", ".join(sorted(pluginlist))
if console.json:
console.msg_json(pluginlist)
else:
console.msg("Loaded plugins: {0}", pluginlist_formatted)
def authenticate_twitch_oauth():
"""Opens a web browser to allow the user to grant Livestreamer
access to their Twitch account."""
client_id = "ewvlchtxgqq88ru9gmfp1gmyt6h2b93"
redirect_uri = "http://livestreamer.tanuki.se/en/develop/twitch_oauth.html"
url = ("https://api.twitch.tv/kraken/oauth2/authorize/"
"?response_type=token&client_id={0}&redirect_uri="
"{1}&scope=user_read+user_subscriptions").format(client_id, redirect_uri)
console.msg("Attempting to open a browser to let you authenticate "
"Livestreamer with Twitch")
try:
if not webbrowser.open_new_tab(url):
raise webbrowser.Error
except webbrowser.Error:
console.exit("Unable to open a web browser, try accessing this URL "
"manually instead:\n{0}".format(url))
def load_plugins(dirs):
"""Attempts to load plugins from a list of directories."""
dirs = [os.path.expanduser(d) for d in dirs]
for directory in dirs:
if os.path.isdir(directory):
livestreamer.load_plugins(directory)
else:
console.logger.warning("Plugin path {0} does not exist or is not "
"a directory!", directory)
def setup_args(config_files=[]):
"""Parses arguments."""
global args
arglist = sys.argv[1:]
# Load arguments from config files
for config_file in filter(os.path.isfile, config_files):
arglist.insert(0, "@" + config_file)
args = parser.parse_args(arglist)
# Force lowercase to allow case-insensitive lookup
if args.stream:
args.stream = [stream.lower() for stream in args.stream]
def setup_config_args():
config_files = []
if args.url:
with ignored(NoPluginError):
plugin = livestreamer.resolve_url(args.url)
config_files += ["{0}.{1}".format(fn, plugin.module) for fn in CONFIG_FILES]
if args.config:
# We want the config specified last to get highest priority
config_files += list(reversed(args.config))
else:
# Only load first available default config
for config_file in filter(os.path.isfile, CONFIG_FILES):
config_files.append(config_file)
break
if config_files:
setup_args(config_files)
def setup_console():
"""Console setup."""
global console
# All console related operations is handled via the ConsoleOutput class
console = ConsoleOutput(sys.stdout, livestreamer)
# Console output should be on stderr if we are outputting
# a stream to stdout.
if args.stdout or args.output == "-":
console.set_output(sys.stderr)
# We don't want log output when we are printing JSON or a command-line.
if not any(getattr(args, attr) for attr in QUIET_OPTIONS):
console.set_level(args.loglevel)
if args.quiet_player:
console.logger.warning("The option --quiet-player is deprecated since "
"version 1.4.3 as hiding player output is now "
"the default.")
if args.best_stream_default:
console.logger.warning("The option --best-stream-default is deprecated "
"since version 1.9.0, use '--default-stream best' "
"instead.")
console.json = args.json
# Handle SIGTERM just like SIGINT
signal.signal(signal.SIGTERM, signal.default_int_handler)
def setup_http_session():
"""Sets the global HTTP settings, such as proxy and headers."""
if args.http_proxy:
livestreamer.set_option("http-proxy", args.http_proxy)
if args.https_proxy:
livestreamer.set_option("https-proxy", args.https_proxy)
if args.http_cookie:
livestreamer.set_option("http-cookies", dict(args.http_cookie))
if args.http_header:
livestreamer.set_option("http-headers", dict(args.http_header))
if args.http_query_param:
livestreamer.set_option("http-query-params", dict(args.http_query_param))
if args.http_ignore_env:
livestreamer.set_option("http-trust-env", False)
if args.http_no_ssl_verify:
livestreamer.set_option("http-ssl-verify", False)
if args.http_ssl_cert:
livestreamer.set_option("http-ssl-cert", args.http_ssl_cert)
if args.http_ssl_cert_crt_key:
livestreamer.set_option("http-ssl-cert", tuple(args.http_ssl_cert_crt_key))
if args.http_timeout:
livestreamer.set_option("http-timeout", args.http_timeout)
if args.http_cookies:
console.logger.warning("The option --http-cookies is deprecated since "
"version 1.11.0, use --http-cookie instead.")
livestreamer.set_option("http-cookies", args.http_cookies)
if args.http_headers:
console.logger.warning("The option --http-headers is deprecated since "
"version 1.11.0, use --http-header instead.")
livestreamer.set_option("http-headers", args.http_headers)
if args.http_query_params:
console.logger.warning("The option --http-query-params is deprecated since "
"version 1.11.0, use --http-query-param instead.")
livestreamer.set_option("http-query-params", args.http_query_params)
def setup_plugins():
"""Loads any additional plugins."""
if os.path.isdir(PLUGINS_DIR):
load_plugins([PLUGINS_DIR])
if args.plugin_dirs:
load_plugins(args.plugin_dirs)
def setup_livestreamer():
"""Creates the Livestreamer session."""
global livestreamer
livestreamer = Livestreamer()
def setup_options():
"""Sets Livestreamer options."""
if args.hls_live_edge:
livestreamer.set_option("hls-live-edge", args.hls_live_edge)
if args.hls_segment_attempts:
livestreamer.set_option("hls-segment-attempts", args.hls_segment_attempts)
if args.hls_segment_threads:
livestreamer.set_option("hls-segment-threads", args.hls_segment_threads)
if args.hls_segment_timeout:
livestreamer.set_option("hls-segment-timeout", args.hls_segment_timeout)
if args.hls_timeout:
livestreamer.set_option("hls-timeout", args.hls_timeout)
if args.hds_live_edge:
livestreamer.set_option("hds-live-edge", args.hds_live_edge)
if args.hds_segment_attempts:
livestreamer.set_option("hds-segment-attempts", args.hds_segment_attempts)
if args.hds_segment_threads:
livestreamer.set_option("hds-segment-threads", args.hds_segment_threads)
if args.hds_segment_timeout:
livestreamer.set_option("hds-segment-timeout", args.hds_segment_timeout)
if args.hds_timeout:
livestreamer.set_option("hds-timeout", args.hds_timeout)
if args.http_stream_timeout:
livestreamer.set_option("http-stream-timeout", args.http_stream_timeout)
if args.ringbuffer_size:
livestreamer.set_option("ringbuffer-size", args.ringbuffer_size)
if args.rtmp_proxy:
livestreamer.set_option("rtmp-proxy", args.rtmp_proxy)
if args.rtmp_rtmpdump:
livestreamer.set_option("rtmp-rtmpdump", args.rtmp_rtmpdump)
if args.rtmp_timeout:
livestreamer.set_option("rtmp-timeout", args.rtmp_timeout)
if args.stream_segment_attempts:
livestreamer.set_option("stream-segment-attempts", args.stream_segment_attempts)
if args.stream_segment_threads:
livestreamer.set_option("stream-segment-threads", args.stream_segment_threads)
if args.stream_segment_timeout:
livestreamer.set_option("stream-segment-timeout", args.stream_segment_timeout)
if args.stream_timeout:
livestreamer.set_option("stream-timeout", args.stream_timeout)
livestreamer.set_option("subprocess-errorlog", args.subprocess_errorlog)
# Deprecated options
if args.hds_fragment_buffer:
console.logger.warning("The option --hds-fragment-buffer is deprecated "
"and will be removed in the future. Use "
"--ringbuffer-size instead")
def setup_plugin_options():
"""Sets Livestreamer plugin options."""
if args.twitch_cookie:
livestreamer.set_plugin_option("twitch", "cookie",
args.twitch_cookie)
if args.twitch_oauth_token:
livestreamer.set_plugin_option("twitch", "oauth_token",
args.twitch_oauth_token)
if args.ustream_password:
livestreamer.set_plugin_option("ustreamtv", "password",
args.ustream_password)
if args.crunchyroll_username:
livestreamer.set_plugin_option("crunchyroll", "username",
args.crunchyroll_username)
if args.crunchyroll_username and not args.crunchyroll_password:
crunchyroll_password = console.askpass("Enter Crunchyroll password: ")
else:
crunchyroll_password = args.crunchyroll_password
if crunchyroll_password:
livestreamer.set_plugin_option("crunchyroll", "password",
crunchyroll_password)
if args.crunchyroll_purge_credentials:
livestreamer.set_plugin_option("crunchyroll", "purge_credentials",
args.crunchyroll_purge_credentials)
if args.livestation_email:
livestreamer.set_plugin_option("livestation", "email",
args.livestation_email)
if args.livestation_password:
livestreamer.set_plugin_option("livestation", "password",
args.livestation_password)
# Deprecated options
if args.jtv_legacy_names:
console.logger.warning("The option --jtv/twitch-legacy-names is "
"deprecated and will be removed in the future.")
if args.jtv_cookie:
console.logger.warning("The option --jtv-cookie is deprecated and "
"will be removed in the future.")
if args.jtv_password:
console.logger.warning("The option --jtv-password is deprecated "
"and will be removed in the future.")
if args.gomtv_username:
console.logger.warning("The option --gomtv-username is deprecated "
"and will be removed in the future.")
if args.gomtv_password:
console.logger.warning("The option --gomtv-password is deprecated "
"and will be removed in the future.")
if args.gomtv_cookie:
console.logger.warning("The option --gomtv-cookie is deprecated "
"and will be removed in the future.")
def check_root():
if hasattr(os, "getuid"):
if os.geteuid() == 0 and not args.yes_run_as_root:
print("livestreamer is not supposed to be run as root. "
"If you really must you can do it by passing "
"--yes-run-as-root.")
sys.exit(1)
def check_version(force=False):
cache = Cache(filename="cli.json")
latest_version = cache.get("latest_version")
if force or not latest_version:
res = requests.get("https://pypi.python.org/pypi/livestreamer/json")
data = res.json()
latest_version = data.get("info").get("version")
cache.set("latest_version", latest_version, (60 * 60 * 24))
version_info_printed = cache.get("version_info_printed")
if not force and version_info_printed:
return
installed_version = StrictVersion(livestreamer.version)
latest_version = StrictVersion(latest_version)
if latest_version > installed_version:
console.logger.info("A new version of Livestreamer ({0}) is "
"available!".format(latest_version))
cache.set("version_info_printed", True, (60 * 60 * 6))
elif force:
console.logger.info("Your Livestreamer version ({0}) is up to date!",
installed_version)
if force:
sys.exit()
def main():
setup_args()
check_root()
setup_livestreamer()
setup_plugins()
setup_config_args()
setup_console()
setup_http_session()
if args.version_check or not args.no_version_check:
with ignored(Exception):
check_version(force=args.version_check)
if args.plugins:
print_plugins()
elif args.can_handle_url:
try:
livestreamer.resolve_url(args.can_handle_url)
except NoPluginError:
sys.exit(1)
else:
sys.exit(0)
elif args.url:
try:
setup_options()
setup_plugin_options()
handle_url()
except KeyboardInterrupt:
# Make sure current stream gets properly cleaned up
if stream_fd:
console.msg("Interrupted! Closing currently open stream...")
try:
stream_fd.close()
except KeyboardInterrupt:
sys.exit()
else:
console.msg("Interrupted! Exiting...")
elif args.twitch_oauth_authenticate:
authenticate_twitch_oauth()
else:
parser.print_help()
livestreamer-1.12.2/src/livestreamer_cli/constants.py 0000644 0001750 0001750 00000001541 12521217321 024100 0 ustar chrippa chrippa 0000000 0000000 import os
from livestreamer import __version__ as LIVESTREAMER_VERSION
from .compat import is_win32
DEFAULT_PLAYER_ARGUMENTS = "{filename}"
if is_win32:
APPDATA = os.environ["APPDATA"]
CONFIG_FILES = [os.path.join(APPDATA, "livestreamer", "livestreamerrc")]
PLUGINS_DIR = os.path.join(APPDATA, "livestreamer", "plugins")
else:
XDG_CONFIG_HOME = os.environ.get("XDG_CONFIG_HOME", "~/.config")
CONFIG_FILES = [
os.path.expanduser(XDG_CONFIG_HOME + "/livestreamer/config"),
os.path.expanduser("~/.livestreamerrc")
]
PLUGINS_DIR = os.path.expanduser(XDG_CONFIG_HOME + "/livestreamer/plugins")
STREAM_SYNONYMS = ["best", "worst"]
STREAM_PASSTHROUGH = ["hls", "http", "rtmp"]
__all__ = [
"CONFIG_FILES", "DEFAULT_PLAYER_ARGUMENTS", "LIVESTREAMER_VERSION",
"PLUGINS_DIR", "STREAM_SYNONYMS", "STREAM_PASSTHROUGH"
]
livestreamer-1.12.2/src/livestreamer_cli/console.py 0000644 0001750 0001750 00000003341 12521217321 023526 0 ustar chrippa chrippa 0000000 0000000 import json
import sys
from getpass import getpass
from .compat import input
from .utils import JSONEncoder
class ConsoleOutput(object):
def __init__(self, output, livestreamer, json=False):
self.livestreamer = livestreamer
self.logger = livestreamer.logger.new_module("cli")
self.json = json
self.set_output(output)
def set_level(self, level):
self.livestreamer.set_loglevel(level)
def set_output(self, output):
self.output = output
self.livestreamer.set_logoutput(output)
def ask(self, msg, *args, **kwargs):
formatted = msg.format(*args, **kwargs)
sys.stderr.write(formatted)
try:
answer = input()
except:
answer = ""
return answer.strip()
def askpass(self, msg, *args, **kwargs):
formatted = msg.format(*args, **kwargs)
return getpass(formatted)
def msg(self, msg, *args, **kwargs):
formatted = msg.format(*args, **kwargs)
formatted = "{0}\n".format(formatted)
self.output.write(formatted)
def msg_json(self, obj):
if not self.json:
return
if hasattr(obj, "__json__"):
obj = obj.__json__()
msg = json.dumps(obj, cls=JSONEncoder,
indent=2)
self.msg("{0}", msg)
if isinstance(obj, dict) and obj.get("error"):
sys.exit(1)
def exit(self, msg, *args, **kwargs):
formatted = msg.format(*args, **kwargs)
if self.json:
obj = dict(error=formatted)
self.msg_json(obj)
else:
msg = "error: {0}".format(formatted)
self.msg("{0}", msg)
sys.exit(1)
__all__ = ["ConsoleOutput"]
livestreamer-1.12.2/src/livestreamer_cli/compat.py 0000644 0001750 0001750 00000002024 12521217321 023344 0 ustar chrippa chrippa 0000000 0000000 import os
import re
import sys
is_py2 = (sys.version_info[0] == 2)
is_py3 = (sys.version_info[0] == 3)
is_win32 = os.name == "nt"
if is_py2:
input = raw_input
stdout = sys.stdout
file = file
_find_unsafe = re.compile(r"[^\w@%+=:,./-]").search
from .packages.shutil_backport import get_terminal_size
elif is_py3:
input = input
stdout = sys.stdout.buffer
from io import IOBase as file
from shutil import get_terminal_size
_find_unsafe = re.compile(r"[^\w@%+=:,./-]", re.ASCII).search
def shlex_quote(s):
"""Return a shell-escaped version of the string *s*.
Backported from Python 3.3 standard library module shlex.
"""
if not s:
return "''"
if _find_unsafe(s) is None:
return s
# use single quotes, and put single quotes into double quotes
# the string $'b is then quoted as '$'"'"'b'
return "'" + s.replace("'", "'\"'\"'") + "'"
__all__ = ["is_py2", "is_py3", "is_win32", "input", "stdout", "file",
"shlex_quote", "get_terminal_size"]
livestreamer-1.12.2/src/livestreamer_cli/argparser.py 0000644 0001750 0001750 00000061005 12521217321 024053 0 ustar chrippa chrippa 0000000 0000000 import argparse
import re
from string import printable
from textwrap import dedent
from .constants import (
LIVESTREAMER_VERSION, STREAM_PASSTHROUGH, DEFAULT_PLAYER_ARGUMENTS
)
from .utils import find_default_player
_filesize_re = re.compile("""
(?P\d+(\.\d+)?)
(?P[Kk]|[Mm])?
(?:[Bb])?
""", re.VERBOSE)
_keyvalue_re = re.compile("(?P[^=]+)\s*=\s*(?P.*)")
_printable_re = re.compile("[{0}]".format(printable))
_option_re = re.compile("""
(?P[A-z-]+) # A option name, valid characters are A to z and dash.
\s*
(?P=)? # Separating the option and the value with a equals sign is
# common, but optional.
\s*
(?P.*) # The value, anything goes.
""", re.VERBOSE)
class ArgumentParser(argparse.ArgumentParser):
def convert_arg_line_to_args(self, line):
# Strip any non-printable characters that might be in the
# beginning of the line (e.g. Unicode BOM marker).
match = _printable_re.search(line)
if not match:
return
line = line[match.start():].strip()
# Skip lines that do not start with a valid option (e.g. comments)
option = _option_re.match(line)
if not option:
return
name, value = option.group("name", "value")
if name and value:
yield "--{0}={1}".format(name, value)
elif name:
yield "--{0}".format(name)
class HelpFormatter(argparse.RawDescriptionHelpFormatter):
"""A nicer help formatter.
Help for arguments can be indented and contain new lines.
It will be de-dented and arguments in the help will be
separated by a blank line for better readability.
Originally written by Jakub Roztocil of the httpie project.
"""
def __init__(self, max_help_position=4, *args, **kwargs):
# A smaller indent for args help.
kwargs["max_help_position"] = max_help_position
argparse.RawDescriptionHelpFormatter.__init__(self, *args, **kwargs)
def _split_lines(self, text, width):
text = dedent(text).strip() + "\n\n"
return text.splitlines()
def comma_list(values):
return [val.strip() for val in values.split(",")]
def comma_list_filter(acceptable):
def func(p):
values = comma_list(p)
return list(filter(lambda v: v in acceptable, values))
return func
def num(type, min=None, max=None):
def func(value):
value = type(value)
if min is not None and not (value > min):
raise argparse.ArgumentTypeError(
"{0} value must be more than {1} but is {2}".format(
type.__name__, min, value
)
)
if max is not None and not (value <= max):
raise argparse.ArgumentTypeError(
"{0} value must be at most {1} but is {2}".format(
type.__name__, max, value
)
)
return value
func.__name__ = type.__name__
return func
def filesize(value):
match = _filesize_re.match(value)
if not match:
raise ValueError
size = float(match.group("size"))
if not size:
raise ValueError
modifier = match.group("modifier")
if modifier in ("M", "m"):
size *= 1024 * 1024
elif modifier in ("K", "k"):
size *= 1024
return num(int, min=0)(size)
def keyvalue(value):
match = _keyvalue_re.match(value)
if not match:
raise ValueError
return match.group("key", "value")
parser = ArgumentParser(
fromfile_prefix_chars="@",
formatter_class=HelpFormatter,
add_help=False,
usage="%(prog)s [OPTIONS] [URL] [STREAM]",
description=dedent("""
Livestreamer is command-line utility that extracts streams from
various services and pipes them into a video player of choice.
"""),
epilog=dedent("""
For more in-depth documention see:
http://docs.livestreamer.io/
Please report broken plugins or bugs to the issue tracker on Github:
https://github.com/chrippa/livestreamer/issues
""")
)
positional = parser.add_argument_group("Positional arguments")
positional.add_argument(
"url",
metavar="URL",
nargs="?",
help="""
A URL to attempt to extract streams from.
If it's a HTTP URL then "http://" can be omitted.
"""
)
positional.add_argument(
"stream",
metavar="STREAM",
nargs="?",
type=comma_list,
help="""
Stream to play.
Use "best" or "worst" for highest or lowest quality available.
Fallback streams can be specified by using a comma-separated list:
"720p,480p,best"
If no stream is specified and --default-stream is not used then a
list of available streams will be printed.
"""
)
general = parser.add_argument_group("General options")
general.add_argument(
"-h", "--help",
action="store_true",
help="""
Show this help message and exit.
"""
)
general.add_argument(
"-V", "--version",
action="version",
version="%(prog)s {0}".format(LIVESTREAMER_VERSION),
help="""
Show version number and exit.
"""
)
general.add_argument(
"--plugins",
action="store_true",
help="""
Print a list of all currently installed plugins.
"""
)
general.add_argument(
"--can-handle-url",
metavar="URL",
help="""
Check if Livestreamer has a plugin that can handle the specified URL.
Returns status code 1 for false and 0 for true.
Useful for external scripting.
"""
)
general.add_argument(
"--config",
action="append",
metavar="FILENAME",
help="""
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.
"""
)
general.add_argument(
"-l", "--loglevel",
metavar="LEVEL",
default="info",
help="""
Set the log message threshold.
Valid levels are: none, error, warning, info, debug
"""
)
general.add_argument(
"-Q", "--quiet",
action="store_true",
help="""
Hide all log output.
Alias for "--loglevel none".
"""
)
general.add_argument(
"-j", "--json",
action="store_true",
help="""
Output JSON representations instead of the normal text output.
Useful for external scripting.
"""
)
general.add_argument(
"--no-version-check",
action="store_true",
help="""
Do not check for new Livestreamer releases.
"""
)
general.add_argument(
"--version-check",
action="store_true",
help="""
Runs a version check and exits.
"""
)
general.add_argument(
"--yes-run-as-root",
action="store_true",
help=argparse.SUPPRESS
)
player = parser.add_argument_group("Player options")
player.add_argument(
"-p", "--player",
metavar="COMMAND",
default=find_default_player(),
help="""
Player to feed stream data to. This is a shell-like syntax to
support passing options to the player. For example:
"vlc --file-caching=5000"
To use a player that is located in a path with spaces you must
quote the path:
"'/path/with spaces/vlc' --file-caching=5000"
By default VLC will be used if it can be found in its default
location.
"""
)
player.add_argument(
"-a", "--player-args",
metavar="ARGUMENTS",
default=DEFAULT_PLAYER_ARGUMENTS,
help="""
This option allows you to customize the default arguments which
are put together with the value of --player to create a command
to execute.
This 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:
filename
This is the filename that the player will use.
It's usually "-" (stdin), but can also be a URL or a file
depending on the options used.
It's usually enough to use --player instead of this unless you
need to add arguments after the filename.
Default is "{0}".
""".format(DEFAULT_PLAYER_ARGUMENTS)
)
player.add_argument(
"-v", "--verbose-player",
action="store_true",
help="""
Allow the player to display its console output.
"""
)
player.add_argument(
"-n", "--player-fifo", "--fifo",
action="store_true",
help="""
Make the player read the stream through a named pipe instead of
the stdin pipe.
"""
)
player.add_argument(
"--player-http",
action="store_true",
help="""
Make the player read the stream through HTTP instead of
the stdin pipe.
"""
)
player.add_argument(
"--player-continuous-http",
action="store_true",
help="""
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.add_argument(
"--player-external-http",
action="store_true",
help="""
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.add_argument(
"--player-external-http-port",
metavar="PORT",
type=num(int, min=0, max=65535),
default=0,
help="""
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.add_argument(
"--player-passthrough",
metavar="TYPES",
type=comma_list_filter(STREAM_PASSTHROUGH),
default=[],
help="""
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:
- {0}
Make sure your player can handle the stream type when using this.
""".format("\n - ".join(STREAM_PASSTHROUGH))
)
player.add_argument(
"--player-no-close",
action="store_true",
help="""
By default Livestreamer 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.
"""
)
output = parser.add_argument_group("File output options")
output.add_argument(
"-o", "--output",
metavar="FILENAME",
help="""
Write stream data to FILENAME instead of playing it.
You will be prompted if the file already exists.
"""
)
output.add_argument(
"-f", "--force",
action="store_true",
help="""
When using -o, always write to file even if it already exists.
"""
)
output.add_argument(
"-O", "--stdout",
action="store_true",
help="""
Write stream data to stdout instead of playing it.
"""
)
stream = parser.add_argument_group("Stream options")
stream.add_argument(
"--default-stream",
type=comma_list,
metavar="STREAM",
help="""
Open this stream when no stream argument is specified, e.g. "best".
"""
)
stream.add_argument(
"--retry-streams",
metavar="DELAY",
type=num(float, min=0),
help="""
Will retry fetching streams until streams are found while
waiting DELAY (seconds) between each attempt.
"""
)
stream.add_argument(
"--retry-open",
metavar="ATTEMPTS",
type=num(int, min=0),
default=1,
help="""
Will try ATTEMPTS times to open the stream until giving up.
Default is 1.
"""
)
stream.add_argument(
"--stream-types", "--stream-priority",
metavar="TYPES",
type=comma_list,
help="""
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.
Default is "rtmp,hls,hds,http,akamaihd".
"""
)
stream.add_argument(
"--stream-sorting-excludes",
metavar="STREAMS",
type=comma_list,
help="""
Fine tune best/worst synonyms by excluding unwanted streams.
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"
"""
)
transport = parser.add_argument_group("Stream transport options")
transport.add_argument(
"--hds-live-edge",
type=num(float, min=0),
metavar="SECONDS",
help="""
The time live HDS streams will start from the edge of stream.
Default is 10.0.
"""
)
transport.add_argument(
"--hds-segment-attempts",
type=num(int, min=0),
metavar="ATTEMPTS",
help="""
How many attempts should be done to download each HDS segment
before giving up.
Default is 3.
"""
)
transport.add_argument(
"--hds-segment-threads",
type=num(int, max=10),
metavar="THREADS",
help="""
The size of the thread pool used to download HDS segments.
Minimum value is 1 and maximum is 10.
Default is 1.
"""
)
transport.add_argument(
"--hds-segment-timeout",
type=num(float, min=0),
metavar="TIMEOUT",
help="""
HDS segment connect and read timeout.
Default is 10.0.
"""
)
transport.add_argument(
"--hds-timeout",
type=num(float, min=0),
metavar="TIMEOUT",
help="""
Timeout for reading data from HDS streams.
Default is 60.0.
"""
)
transport.add_argument(
"--hls-live-edge",
type=num(int, min=0),
metavar="SEGMENTS",
help="""
How many segments from the end to start live HLS streams on.
The lower the value the lower latency from the source you will be,
but also increases the chance of buffering.
Default is 3.
"""
)
transport.add_argument(
"--hls-segment-attempts",
type=num(int, min=0),
metavar="ATTEMPTS",
help="""
How many attempts should be done to download each HLS segment
before giving up.
Default is 3.
"""
)
transport.add_argument(
"--hls-segment-threads",
type=num(int, max=10),
metavar="THREADS",
help="""
The size of the thread pool used to download HLS segments.
Minimum value is 1 and maximum is 10.
Default is 1.
"""
)
transport.add_argument(
"--hls-segment-timeout",
type=num(float, min=0),
metavar="TIMEOUT",
help="""
HLS segment connect and read timeout.
Default is 10.0.
""")
transport.add_argument(
"--hls-timeout",
type=num(float, min=0),
metavar="TIMEOUT",
help="""
Timeout for reading data from HLS streams.
Default is 60.0.
""")
transport.add_argument(
"--http-stream-timeout",
type=num(float, min=0),
metavar="TIMEOUT",
help="""
Timeout for reading data from HTTP streams.
Default is 60.0.
"""
)
transport.add_argument(
"--ringbuffer-size",
metavar="SIZE",
type=filesize,
help="""
The maximum size of ringbuffer. Add a M or K suffix to specify mega
or kilo bytes instead of bytes.
The ringbuffer is used as a temporary storage between the stream
and the player. This is to allows us to download the stream faster
than the player wants to read it.
The smaller the size, the higher chance of the player buffering
if there are download speed dips and the higher size the more data
we can use as a storage to catch up from speed dips.
It also allows you to temporary pause as long as the ringbuffer
doesn't get full since we continue to download the stream in the
background.
Note: A smaller size is recommended on lower end systems (such as
Raspberry Pi) when playing stream types that require some extra
processing (such as HDS) to avoid unnecessary background
processing.
Default is "16M".
""")
transport.add_argument(
"--rtmp-proxy", "--rtmpdump-proxy",
metavar="PROXY",
help="""
A SOCKS proxy that RTMP streams will use.
Example: 127.0.0.1:9050
"""
)
transport.add_argument(
"--rtmp-rtmpdump", "--rtmpdump", "-r",
metavar="FILENAME",
help="""
RTMPDump is used to access RTMP streams. You can specify the
location of the rtmpdump executable if it is not in your PATH.
Example: "/usr/local/bin/rtmpdump"
"""
)
transport.add_argument(
"--rtmp-timeout",
type=num(float, min=0),
metavar="TIMEOUT",
help="""
Timeout for reading data from RTMP streams.
Default is 60.0.
"""
)
transport.add_argument(
"--stream-segment-attempts",
type=num(int, min=0),
metavar="ATTEMPTS",
help="""
How many attempts should be done to download each segment before giving up.
This is generic option used by streams not covered by other options,
such as stream protocols specific to plugins, e.g. UStream.
Default is 3.
"""
)
transport.add_argument(
"--stream-segment-threads",
type=num(int, max=10),
metavar="THREADS",
help="""
The size of the thread pool used to download segments.
Minimum value is 1 and maximum is 10.
This is generic option used by streams not covered by other options,
such as stream protocols specific to plugins, e.g. UStream.
Default is 1.
"""
)
transport.add_argument(
"--stream-segment-timeout",
type=num(float, min=0),
metavar="TIMEOUT",
help="""
Segment connect and read timeout.
This is generic option used by streams not covered by other options,
such as stream protocols specific to plugins, e.g. UStream.
Default is 10.0.
""")
transport.add_argument(
"--stream-timeout",
type=num(float, min=0),
metavar="TIMEOUT",
help="""
Timeout for reading data from streams.
This is generic option used by streams not covered by other options,
such as stream protocols specific to plugins, e.g. UStream.
Default is 60.0.
""")
transport.add_argument(
"--stream-url",
action="store_true",
help="""
If possible, translate the stream to a URL and print it.
"""
)
transport.add_argument(
"--subprocess-cmdline", "--cmdline", "-c",
action="store_true",
help="""
Print command-line used internally to play stream.
This is only available on RTMP streams.
"""
)
transport.add_argument(
"--subprocess-errorlog", "--errorlog", "-e",
action="store_true",
help="""
Log possible errors from internal subprocesses to a temporary file.
The file will be saved in your systems temporary directory.
Useful when debugging rtmpdump related issues.
"""
)
http = parser.add_argument_group("HTTP options")
http.add_argument(
"--http-proxy",
metavar="HTTP_PROXY",
help="""
A HTTP proxy to use for all HTTP requests.
Example: http://hostname:port/
"""
)
http.add_argument(
"--https-proxy",
metavar="HTTPS_PROXY",
help="""
A HTTPS capable proxy to use for all HTTPS requests.
Example: http://hostname:port/
"""
)
http.add_argument(
"--http-cookie",
metavar="KEY=VALUE",
type=keyvalue,
action="append",
help="""
A cookie to add to each HTTP request.
Can be repeated to add multiple cookies.
"""
)
http.add_argument(
"--http-header",
metavar="KEY=VALUE",
type=keyvalue,
action="append",
help="""
A header to add to each HTTP request.
Can be repeated to add multiple headers.
"""
)
http.add_argument(
"--http-query-param",
metavar="KEY=VALUE",
type=keyvalue,
action="append",
help="""
A query parameter to add to each HTTP request.
Can be repeated to add multiple query parameters.
"""
)
http.add_argument(
"--http-ignore-env",
action="store_true",
help="""
Ignore HTTP settings set in the environment such as environment
variables (HTTP_PROXY, etc) or ~/.netrc authentication.
"""
)
http.add_argument(
"--http-no-ssl-verify",
action="store_true",
help="""
Don't attempt to verify SSL certificates.
Usually a bad idea, only use this if you know what you're doing.
"""
)
http.add_argument(
"--http-ssl-cert",
metavar="FILENAME",
help="""
SSL certificate to use.
Expects a .pem file.
"""
)
http.add_argument(
"--http-ssl-cert-crt-key",
metavar=("CRT_FILENAME", "KEY_FILENAME"),
nargs=2,
help="""
SSL certificate to use.
Expects a .crt and a .key file.
"""
)
http.add_argument(
"--http-timeout",
metavar="TIMEOUT",
type=num(float, min=0),
help="""
General timeout used by all HTTP requests except the ones covered
by other options.
Default is 20.0.
"""
)
plugin = parser.add_argument_group("Plugin options")
plugin.add_argument(
"--plugin-dirs",
metavar="DIRECTORY",
type=comma_list,
help="""
Attempts to load plugins from these directories.
Multiple directories can be used by separating them with a
semi-colon.
"""
)
plugin.add_argument(
"--twitch-oauth-token",
metavar="TOKEN",
help="""
An OAuth token to use for Twitch authentication.
Use --twitch-oauth-authenticate to create a token.
"""
)
plugin.add_argument(
"--twitch-oauth-authenticate",
action="store_true",
help="""
Open a web browser where you can grant Livestreamer access
to your Twitch account which creates a token for use with
--twitch-oauth-token.
"""
)
plugin.add_argument(
"--twitch-cookie",
metavar="COOKIES",
help="""
Twitch cookies to authenticate to allow access to subscription channels.
Example:
"_twitch_session_id=xxxxxx; persistent=xxxxx"
Note: This method is the old and clunky way of authenticating with
Twitch, using --twitch-oauth-authenticate is the recommended and
simpler way of doing it now.
"""
)
plugin.add_argument(
"--ustream-password",
metavar="PASSWORD",
help="""
A password to access password protected UStream.tv channels.
"""
)
plugin.add_argument(
"--crunchyroll-username",
metavar="USERNAME",
help="""
A Crunchyroll username to allow access to restricted streams.
"""
)
plugin.add_argument(
"--crunchyroll-password",
metavar="PASSWORD",
nargs="?",
const=True,
default=None,
help="""
A Crunchyroll password for use with --crunchyroll-username.
If left blank you will be prompted.
"""
)
plugin.add_argument(
"--crunchyroll-purge-credentials",
action="store_true",
help="""
Purge cached Crunchyroll credentials to initiate a new session
and reauthenticate.
"""
)
plugin.add_argument(
"--livestation-email",
metavar="EMAIL",
help="""
A Livestation account email to access restricted or premium
quality streams.
"""
)
plugin.add_argument(
"--livestation-password",
metavar="PASSWORD",
help="""
A Livestation account password to use with --livestation-email.
"""
)
# Deprecated options
stream.add_argument(
"--best-stream-default",
action="store_true",
help=argparse.SUPPRESS
)
player.add_argument(
"-q", "--quiet-player",
action="store_true",
help=argparse.SUPPRESS
)
transport.add_argument(
"--hds-fragment-buffer",
type=int,
metavar="fragments",
help=argparse.SUPPRESS
)
plugin.add_argument(
"--jtv-legacy-names", "--twitch-legacy-names",
action="store_true",
help=argparse.SUPPRESS
)
plugin.add_argument(
"--gomtv-cookie",
metavar="cookie",
help=argparse.SUPPRESS
)
plugin.add_argument(
"--gomtv-username",
metavar="username",
help=argparse.SUPPRESS
)
plugin.add_argument(
"--gomtv-password",
metavar="password",
nargs="?",
const=True,
default=None,
help=argparse.SUPPRESS
)
plugin.add_argument(
"--jtv-cookie",
help=argparse.SUPPRESS
)
plugin.add_argument(
"--jtv-password", "--twitch-password",
help=argparse.SUPPRESS
)
http.add_argument(
"--http-cookies",
metavar="COOKIES",
help=argparse.SUPPRESS
)
http.add_argument(
"--http-headers",
metavar="HEADERS",
help=argparse.SUPPRESS
)
http.add_argument(
"--http-query-params",
metavar="PARAMS",
help=argparse.SUPPRESS
)
__all__ = ["parser"]
livestreamer-1.12.2/src/livestreamer_cli/__init__.py 0000644 0001750 0001750 00000000000 12521217321 023610 0 ustar chrippa chrippa 0000000 0000000 livestreamer-1.12.2/src/livestreamer_cli/utils/ 0000755 0001750 0001750 00000000000 12521217500 022650 5 ustar chrippa chrippa 0000000 0000000 livestreamer-1.12.2/src/livestreamer_cli/utils/stream.py 0000644 0001750 0001750 00000002032 12521217321 024513 0 ustar chrippa chrippa 0000000 0000000 def stream_to_url(stream):
stream_type = type(stream).shortname()
if stream_type in ("hls", "http"):
url = stream.url
elif stream_type == "rtmp":
params = [stream.params.pop("rtmp", "")]
stream_params = dict(stream.params)
if "swfVfy" in stream.params:
stream_params["swfUrl"] = stream.params["swfVfy"]
stream_params["swfVfy"] = True
if "swfhash" in stream.params:
stream_params["swfVfy"] = True
stream_params.pop("swfhash", None)
stream_params.pop("swfsize", None)
for key, value in stream_params.items():
if isinstance(value, bool):
value = str(int(value))
# librtmp expects some characters to be escaped
value = value.replace("\\", "\\5c")
value = value.replace(" ", "\\20")
value = value.replace('"', "\\22")
params.append("{0}={1}".format(key, value))
url = " ".join(params)
else:
url = None
return url
livestreamer-1.12.2/src/livestreamer_cli/utils/progress.py 0000644 0001750 0001750 00000006523 12521217321 025075 0 ustar chrippa chrippa 0000000 0000000 import sys
from collections import deque
from time import time
from ..compat import is_win32, get_terminal_size
PROGRESS_FORMATS = (
"[download][{prefix}] Written {written} ({elapsed} @ {speed}/s)",
"[download] Written {written} ({elapsed} @ {speed}/s)",
"[download] {written} ({elapsed} @ {speed}/s)",
"[download] {written} ({elapsed})",
"[download] {written}"
)
def terminal_len(value):
"""Returns the length of the string it would be when displayed.
Attempts to decode the string as UTF-8 first if it's a bytestring.
"""
if isinstance(value, bytes):
value = value.decode("utf8", "ignore")
return len(value)
def print_inplace(msg):
"""Clears out the previous line and prints a new one."""
term_width = get_terminal_size().columns
spacing = term_width - terminal_len(msg)
# On windows we need one less space or we overflow the line for some reason.
if is_win32:
spacing -= 1
sys.stderr.write("\r{0}".format(msg))
sys.stderr.write(" " * max(0, spacing))
sys.stderr.flush()
def format_filesize(size):
"""Formats the file size into a human readable format."""
for suffix in ("bytes", "KB", "MB", "GB", "TB"):
if size < 1024.0:
if suffix in ("GB", "TB"):
return "{0:3.2f} {1}".format(size, suffix)
else:
return "{0:3.1f} {1}".format(size, suffix)
size /= 1024.0
def format_time(elapsed):
"""Formats elapsed seconds into a human readable format."""
hours = int(elapsed / (60 * 60))
minutes = int((elapsed % (60 * 60)) / 60)
seconds = int(elapsed % 60)
rval = ""
if hours:
rval += "{0}h".format(hours)
if elapsed > 60:
rval += "{0}m".format(minutes)
rval += "{0}s".format(seconds)
return rval
def create_status_line(**params):
"""Creates a status line with appropriate size."""
max_size = get_terminal_size().columns - 1
for fmt in PROGRESS_FORMATS:
status = fmt.format(**params)
if len(status) <= max_size:
break
return status
def progress(iterator, prefix):
"""Progress an iterator and updates a pretty status line to the terminal.
The status line contains:
- Amount of data read from the iterator
- Time elapsed
- Average speed, based on the last few seconds.
"""
prefix = (".." + prefix[-23:]) if len(prefix) > 25 else prefix
speed_updated = start = time()
speed_written = written = 0
speed_history = deque(maxlen=5)
for data in iterator:
yield data
now = time()
elapsed = now - start
written += len(data)
speed_elapsed = now - speed_updated
if speed_elapsed >= 0.5:
speed_history.appendleft((
written - speed_written,
speed_updated,
))
speed_updated = now
speed_written = written
speed_history_written = sum(h[0] for h in speed_history)
speed_history_elapsed = now - speed_history[-1][1]
speed = speed_history_written / speed_history_elapsed
status = create_status_line(
prefix=prefix,
written=format_filesize(written),
elapsed=format_time(elapsed),
speed=format_filesize(speed)
)
print_inplace(status)
livestreamer-1.12.2/src/livestreamer_cli/utils/player.py 0000644 0001750 0001750 00000002334 12521217321 024521 0 ustar chrippa chrippa 0000000 0000000 import os
import sys
from ..compat import shlex_quote
def check_paths(exes, paths):
for path in paths:
for exe in exes:
path = os.path.expanduser(os.path.join(path, exe))
if os.path.isfile(path):
return path
def find_default_player():
if "darwin" in sys.platform:
paths = os.environ.get("PATH", "").split(":")
paths += ["/Applications/VLC.app/Contents/MacOS/"]
paths += ["~/Applications/VLC.app/Contents/MacOS/"]
path = check_paths(("VLC", "vlc"), paths)
elif "win32" in sys.platform:
exename = "vlc.exe"
paths = os.environ.get("PATH", "").split(";")
path = check_paths((exename,), paths)
if not path:
subpath = "VideoLAN\\VLC\\"
envvars = ("PROGRAMFILES", "PROGRAMFILES(X86)", "PROGRAMW6432")
paths = filter(None, (os.environ.get(var) for var in envvars))
paths = (os.path.join(p, subpath) for p in paths)
path = check_paths((exename,), paths)
else:
paths = os.environ.get("PATH", "").split(":")
path = check_paths(("vlc",), paths)
if path:
# Quote command because it can contain space
return shlex_quote(path)
livestreamer-1.12.2/src/livestreamer_cli/utils/named_pipe.py 0000644 0001750 0001750 00000004175 12521217321 025333 0 ustar chrippa chrippa 0000000 0000000 import os
import tempfile
from ..compat import is_win32, is_py3
if is_win32:
from ctypes import windll, cast, c_ulong, c_void_p, byref
PIPE_ACCESS_OUTBOUND = 0x00000002
PIPE_TYPE_BYTE = 0x00000000
PIPE_READMODE_BYTE = 0x00000000
PIPE_WAIT = 0x00000000
PIPE_UNLIMITED_INSTANCES = 255
INVALID_HANDLE_VALUE = -1
class NamedPipe(object):
def __init__(self, name):
self.fifo = None
self.pipe = None
if is_win32:
self.path = os.path.join("\\\\.\\pipe", name)
self.pipe = self._create_named_pipe(self.path)
else:
self.path = os.path.join(tempfile.gettempdir(), name)
self._create_fifo(self.path)
def _create_fifo(self, name):
os.mkfifo(name, 0o660)
def _create_named_pipe(self, path):
bufsize = 8192
if is_py3:
create_named_pipe = windll.kernel32.CreateNamedPipeW
else:
create_named_pipe = windll.kernel32.CreateNamedPipeA
pipe = create_named_pipe(path, PIPE_ACCESS_OUTBOUND,
PIPE_TYPE_BYTE | PIPE_READMODE_BYTE | PIPE_WAIT,
PIPE_UNLIMITED_INSTANCES,
bufsize, bufsize,
0, None)
if pipe == INVALID_HANDLE_VALUE:
error_code = windll.kernel32.GetLastError()
raise IOError("Error code 0x{0:08X}".format(error_code))
return pipe
def open(self, mode):
if not self.pipe:
self.fifo = open(self.path, mode)
def write(self, data):
if self.pipe:
windll.kernel32.ConnectNamedPipe(self.pipe, None)
written = c_ulong(0)
windll.kernel32.WriteFile(self.pipe, cast(data, c_void_p),
len(data), byref(written),
None)
return written
else:
return self.fifo.write(data)
def close(self):
if self.pipe:
windll.kernel32.DisconnectNamedPipe(self.pipe)
else:
self.fifo.close()
os.unlink(self.path)
livestreamer-1.12.2/src/livestreamer_cli/utils/http_server.py 0000644 0001750 0001750 00000005661 12521217321 025600 0 ustar chrippa chrippa 0000000 0000000 import socket
from io import BytesIO
try:
from BaseHTTPServer import BaseHTTPRequestHandler
except ImportError:
from http.server import BaseHTTPRequestHandler
class HTTPRequest(BaseHTTPRequestHandler):
def __init__(self, request_text):
self.rfile = BytesIO(request_text)
self.raw_requestline = self.rfile.readline()
self.error_code = self.error_message = None
self.parse_request()
def send_error(self, code, message):
self.error_code = code
self.error_message = message
class HTTPServer(object):
def __init__(self):
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.conn = self.host = self.port = None
self.bound = False
@property
def addresses(self):
if self.host:
return [self.host]
addrs = set()
try:
for info in socket.getaddrinfo(socket.gethostname(), self.port,
socket.AF_INET):
addrs.add(info[4][0])
except socket.gaierror:
pass
addrs.add("127.0.0.1")
return sorted(addrs)
@property
def urls(self):
for addr in self.addresses:
yield "http://{0}:{1}/".format(addr, self.port)
@property
def url(self):
return next(self.urls, None)
def bind(self, host="127.0.0.1", port=0):
try:
self.socket.bind((host or "", port))
except socket.error as err:
raise OSError(err)
self.socket.listen(1)
self.bound = True
self.host, self.port = self.socket.getsockname()
if self.host == "0.0.0.0":
self.host = None
def open(self, timeout=30):
self.socket.settimeout(timeout)
try:
conn, addr = self.socket.accept()
conn.settimeout(None)
except socket.timeout:
raise OSError("Socket accept timed out")
try:
req_data = conn.recv(1024)
except socket.error:
raise OSError("Failed to read data from socket")
req = HTTPRequest(req_data)
if req.command not in ("GET", "HEAD"):
conn.send(b"HTTP/1.1 501 Not Implemented\r\n")
conn.close()
raise OSError("Invalid request method: {0}".format(req.command))
conn.send(b"HTTP/1.1 200 OK\r\n")
conn.send(b"Server: Livestreamer\r\n")
conn.send(b"Content-Type: video/unknown\r\n")
conn.send(b"\r\n")
# We don't want to send any data on HEAD requests.
if req.command == "HEAD":
conn.close()
raise OSError
self.conn = conn
return req
def write(self, data):
if not self.conn:
raise IOError("No connection")
self.conn.sendall(data)
def close(self, client_only=False):
if self.conn:
self.conn.close()
if not client_only:
self.socket.close()
livestreamer-1.12.2/src/livestreamer_cli/utils/__init__.py 0000644 0001750 0001750 00000001361 12521217321 024763 0 ustar chrippa chrippa 0000000 0000000 import json
from contextlib import contextmanager
from .http_server import HTTPServer
from .named_pipe import NamedPipe
from .progress import progress
from .player import find_default_player
from .stream import stream_to_url
__all__ = [
"NamedPipe", "HTTPServer", "JSONEncoder",
"find_default_player", "ignored", "progress", "stream_to_url"
]
class JSONEncoder(json.JSONEncoder):
def default(self, obj):
if hasattr(obj, "__json__"):
return obj.__json__()
elif isinstance(obj, bytes):
return obj.decode("utf8", "ignore")
else:
return json.JSONEncoder.default(self, obj)
@contextmanager
def ignored(*exceptions):
try:
yield
except exceptions:
pass
livestreamer-1.12.2/src/livestreamer_cli/packages/ 0000755 0001750 0001750 00000000000 12521217500 023266 5 ustar chrippa chrippa 0000000 0000000 livestreamer-1.12.2/src/livestreamer_cli/packages/shutil_backport.py 0000644 0001750 0001750 00000005420 12521217321 027037 0 ustar chrippa chrippa 0000000 0000000 """This is a backport of shutil.get_terminal_size from Python 3.
The original implementation is in C, but here we use the ctypes and
fcntl modules to create a pure Python version of os.get_terminal_size.
"""
import os
import struct
import sys
from collections import namedtuple
terminal_size = namedtuple("terminal_size", "columns lines")
try:
from ctypes import windll, create_string_buffer
_handles = {
0: windll.kernel32.GetStdHandle(-10),
1: windll.kernel32.GetStdHandle(-11),
2: windll.kernel32.GetStdHandle(-12),
}
def _get_terminal_size(fd):
columns = lines = 0
try:
handle = _handles[fd]
csbi = create_string_buffer(22)
res = windll.kernel32.GetConsoleScreenBufferInfo(handle, csbi)
if res:
res = struct.unpack("hhhhHhhhhhh", csbi.raw)
left, top, right, bottom = res[5:9]
columns = right - left + 1
lines = bottom - top + 1
except Exception:
pass
return columns, lines
except ImportError:
import fcntl
import termios
def _get_terminal_size(fd):
try:
res = fcntl.ioctl(fd, termios.TIOCGWINSZ, b"\x00" * 4)
lines, columns = struct.unpack("hh", res)
except Exception:
columns = lines = 0
return columns, lines
def get_terminal_size(fallback=(80, 24)):
"""Get the size of the terminal window.
For each of the two dimensions, the environment variable, COLUMNS
and LINES respectively, is checked. If the variable is defined and
the value is a positive integer, it is used.
When COLUMNS or LINES is not defined, which is the common case,
the terminal connected to sys.__stdout__ is queried
by invoking os.get_terminal_size.
If the terminal size cannot be successfully queried, either because
the system doesn't support querying, or because we are not
connected to a terminal, the value given in fallback parameter
is used. Fallback defaults to (80, 24) which is the default
size used by many terminal emulators.
The value returned is a named tuple of type os.terminal_size.
"""
# Attempt to use the environment first
try:
columns = int(os.environ["COLUMNS"])
except (KeyError, ValueError):
columns = 0
try:
lines = int(os.environ["LINES"])
except (KeyError, ValueError):
lines = 0
# Only query if necessary
if columns <= 0 or lines <= 0:
try:
columns, lines = _get_terminal_size(sys.__stdout__.fileno())
except (NameError, OSError):
pass
# Use fallback as last resort
if columns <= 0 and lines <= 0:
columns, lines = fallback
return terminal_size(columns, lines)
livestreamer-1.12.2/src/livestreamer_cli/packages/__init__.py 0000644 0001750 0001750 00000000000 12521217321 025366 0 ustar chrippa chrippa 0000000 0000000 livestreamer-1.12.2/src/livestreamer.egg-info/ 0000755 0001750 0001750 00000000000 12521217500 022353 5 ustar chrippa chrippa 0000000 0000000 livestreamer-1.12.2/src/livestreamer.egg-info/top_level.txt 0000644 0001750 0001750 00000000036 12521217500 025104 0 ustar chrippa chrippa 0000000 0000000 livestreamer
livestreamer_cli
livestreamer-1.12.2/src/livestreamer.egg-info/requires.txt 0000644 0001750 0001750 00000000023 12521217500 024746 0 ustar chrippa chrippa 0000000 0000000 requests>=1.0,<3.0
livestreamer-1.12.2/src/livestreamer.egg-info/entry_points.txt 0000644 0001750 0001750 00000000075 12521217500 025653 0 ustar chrippa chrippa 0000000 0000000 [console_scripts]
livestreamer = livestreamer_cli.main:main
livestreamer-1.12.2/src/livestreamer.egg-info/dependency_links.txt 0000644 0001750 0001750 00000000001 12521217500 026421 0 ustar chrippa chrippa 0000000 0000000
livestreamer-1.12.2/src/livestreamer.egg-info/SOURCES.txt 0000644 0001750 0001750 00000015600 12521217500 024241 0 ustar chrippa chrippa 0000000 0000000 CHANGELOG.rst
CONTRIBUTING.rst
LICENSE
LICENSE.flashmedia
LICENSE.pbs
MANIFEST.in
README.rst
requirements-docs.txt
setup.cfg
setup.py
docs/Makefile
docs/api.rst
docs/api_guide.rst
docs/changelog.rst
docs/cli.rst
docs/conf.py
docs/ext_argparse.py
docs/ext_github.py
docs/ext_releaseref.py
docs/index.rst
docs/install.rst
docs/issues.rst
docs/players.rst
docs/plugin_matrix.rst
docs/twitch_oauth.rst
docs/_static/flattr-badge.png
docs/_themes/.gitignore
docs/_themes/sphinx_rtd_theme_violet/LICENSE
docs/_themes/sphinx_rtd_theme_violet/__init__.py
docs/_themes/sphinx_rtd_theme_violet/breadcrumbs.html
docs/_themes/sphinx_rtd_theme_violet/footer.html
docs/_themes/sphinx_rtd_theme_violet/layout.html
docs/_themes/sphinx_rtd_theme_violet/layout_old.html
docs/_themes/sphinx_rtd_theme_violet/search.html
docs/_themes/sphinx_rtd_theme_violet/searchbox.html
docs/_themes/sphinx_rtd_theme_violet/theme.conf
docs/_themes/sphinx_rtd_theme_violet/versions.html
docs/_themes/sphinx_rtd_theme_violet/static/css/badge_only.css
docs/_themes/sphinx_rtd_theme_violet/static/css/theme.css
docs/_themes/sphinx_rtd_theme_violet/static/fonts/FontAwesome.otf
docs/_themes/sphinx_rtd_theme_violet/static/fonts/fontawesome-webfont.eot
docs/_themes/sphinx_rtd_theme_violet/static/fonts/fontawesome-webfont.svg
docs/_themes/sphinx_rtd_theme_violet/static/fonts/fontawesome-webfont.ttf
docs/_themes/sphinx_rtd_theme_violet/static/fonts/fontawesome-webfont.woff
docs/_themes/sphinx_rtd_theme_violet/static/js/scrollspy.js
docs/_themes/sphinx_rtd_theme_violet/static/js/theme.js
examples/gst-player.py
src/livestreamer/__init__.py
src/livestreamer/api.py
src/livestreamer/buffers.py
src/livestreamer/cache.py
src/livestreamer/compat.py
src/livestreamer/exceptions.py
src/livestreamer/logger.py
src/livestreamer/options.py
src/livestreamer/session.py
src/livestreamer/utils.py
src/livestreamer.egg-info/PKG-INFO
src/livestreamer.egg-info/SOURCES.txt
src/livestreamer.egg-info/dependency_links.txt
src/livestreamer.egg-info/entry_points.txt
src/livestreamer.egg-info/requires.txt
src/livestreamer.egg-info/top_level.txt
src/livestreamer/packages/__init__.py
src/livestreamer/packages/pbs.py
src/livestreamer/packages/flashmedia/__init__.py
src/livestreamer/packages/flashmedia/amf.py
src/livestreamer/packages/flashmedia/box.py
src/livestreamer/packages/flashmedia/compat.py
src/livestreamer/packages/flashmedia/error.py
src/livestreamer/packages/flashmedia/f4v.py
src/livestreamer/packages/flashmedia/flv.py
src/livestreamer/packages/flashmedia/ordereddict.py
src/livestreamer/packages/flashmedia/packet.py
src/livestreamer/packages/flashmedia/tag.py
src/livestreamer/packages/flashmedia/types.py
src/livestreamer/packages/flashmedia/util.py
src/livestreamer/plugin/__init__.py
src/livestreamer/plugin/plugin.py
src/livestreamer/plugin/api/__init__.py
src/livestreamer/plugin/api/http_session.py
src/livestreamer/plugin/api/mapper.py
src/livestreamer/plugin/api/support_plugin.py
src/livestreamer/plugin/api/utils.py
src/livestreamer/plugin/api/validate.py
src/livestreamer/plugins/__init__.py
src/livestreamer/plugins/afreeca.py
src/livestreamer/plugins/afreecatv.py
src/livestreamer/plugins/aftonbladet.py
src/livestreamer/plugins/alieztv.py
src/livestreamer/plugins/ard_live.py
src/livestreamer/plugins/ard_mediathek.py
src/livestreamer/plugins/artetv.py
src/livestreamer/plugins/azubutv.py
src/livestreamer/plugins/bambuser.py
src/livestreamer/plugins/beattv.py
src/livestreamer/plugins/chaturbate.py
src/livestreamer/plugins/common_jwplayer.py
src/livestreamer/plugins/common_swf.py
src/livestreamer/plugins/connectcast.py
src/livestreamer/plugins/crunchyroll.py
src/livestreamer/plugins/cybergame.py
src/livestreamer/plugins/dailymotion.py
src/livestreamer/plugins/disney_de.py
src/livestreamer/plugins/dmcloud.py
src/livestreamer/plugins/dmcloud_embed.py
src/livestreamer/plugins/dommune.py
src/livestreamer/plugins/douyutv.py
src/livestreamer/plugins/drdk.py
src/livestreamer/plugins/euronews.py
src/livestreamer/plugins/filmon.py
src/livestreamer/plugins/filmon_us.py
src/livestreamer/plugins/furstream.py
src/livestreamer/plugins/gaminglive.py
src/livestreamer/plugins/gomexp.py
src/livestreamer/plugins/goodgame.py
src/livestreamer/plugins/hitbox.py
src/livestreamer/plugins/itvplayer.py
src/livestreamer/plugins/letontv.py
src/livestreamer/plugins/livestation.py
src/livestreamer/plugins/livestream.py
src/livestreamer/plugins/media_ccc_de.py
src/livestreamer/plugins/mips.py
src/livestreamer/plugins/mlgtv.py
src/livestreamer/plugins/nhkworld.py
src/livestreamer/plugins/nos.py
src/livestreamer/plugins/npo.py
src/livestreamer/plugins/nrk.py
src/livestreamer/plugins/oldlivestream.py
src/livestreamer/plugins/periscope.py
src/livestreamer/plugins/picarto.py
src/livestreamer/plugins/rtve.py
src/livestreamer/plugins/sbsdiscovery.py
src/livestreamer/plugins/seemeplay.py
src/livestreamer/plugins/speedrunslive.py
src/livestreamer/plugins/ssh101.py
src/livestreamer/plugins/stream.py
src/livestreamer/plugins/streamingvideoprovider.py
src/livestreamer/plugins/streamlive.py
src/livestreamer/plugins/svtplay.py
src/livestreamer/plugins/tga.py
src/livestreamer/plugins/tv3cat.py
src/livestreamer/plugins/tv4play.py
src/livestreamer/plugins/tvcatchup.py
src/livestreamer/plugins/tvplayer.py
src/livestreamer/plugins/twitch.py
src/livestreamer/plugins/ustreamtv.py
src/livestreamer/plugins/vaughnlive.py
src/livestreamer/plugins/veetle.py
src/livestreamer/plugins/vgtv.py
src/livestreamer/plugins/viagame.py
src/livestreamer/plugins/viasat.py
src/livestreamer/plugins/viasat_embed.py
src/livestreamer/plugins/wattv.py
src/livestreamer/plugins/weeb.py
src/livestreamer/plugins/youtube.py
src/livestreamer/plugins/zdf_mediathek.py
src/livestreamer/stream/__init__.py
src/livestreamer/stream/akamaihd.py
src/livestreamer/stream/flvconcat.py
src/livestreamer/stream/hds.py
src/livestreamer/stream/hls.py
src/livestreamer/stream/hls_playlist.py
src/livestreamer/stream/http.py
src/livestreamer/stream/playlist.py
src/livestreamer/stream/rtmpdump.py
src/livestreamer/stream/segmented.py
src/livestreamer/stream/stream.py
src/livestreamer/stream/streamprocess.py
src/livestreamer/stream/wrappers.py
src/livestreamer_cli/__init__.py
src/livestreamer_cli/argparser.py
src/livestreamer_cli/compat.py
src/livestreamer_cli/console.py
src/livestreamer_cli/constants.py
src/livestreamer_cli/main.py
src/livestreamer_cli/output.py
src/livestreamer_cli/packages/__init__.py
src/livestreamer_cli/packages/shutil_backport.py
src/livestreamer_cli/utils/__init__.py
src/livestreamer_cli/utils/http_server.py
src/livestreamer_cli/utils/named_pipe.py
src/livestreamer_cli/utils/player.py
src/livestreamer_cli/utils/progress.py
src/livestreamer_cli/utils/stream.py
tests/__init__.py
tests/test_buffer.py
tests/test_log.py
tests/test_options.py
tests/test_plugin_api_http_session.py
tests/test_plugin_api_validate.py
tests/test_plugin_stream.py
tests/test_session.py
tests/test_stream_wrappers.py
tests/plugins/__init__.py
tests/plugins/testplugin.py
tests/plugins/testplugin_support.py livestreamer-1.12.2/src/livestreamer.egg-info/PKG-INFO 0000644 0001750 0001750 00000001575 12521217500 023460 0 ustar chrippa chrippa 0000000 0000000 Metadata-Version: 1.1
Name: livestreamer
Version: 1.12.2
Summary: Livestreamer is command-line utility that extracts streams from various services and pipes them into a video player of choice.
Home-page: http://livestreamer.io/
Author: Christopher Rosell
Author-email: chrippa@tanuki.se
License: Simplified BSD
Description: UNKNOWN
Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Console
Classifier: Operating System :: POSIX
Classifier: Operating System :: Microsoft :: Windows
Classifier: Programming Language :: Python :: 2.6
Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3.3
Classifier: Programming Language :: Python :: 3.4
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Multimedia :: Sound/Audio
Classifier: Topic :: Multimedia :: Video
Classifier: Topic :: Utilities
livestreamer-1.12.2/src/livestreamer/ 0000755 0001750 0001750 00000000000 12521217500 020661 5 ustar chrippa chrippa 0000000 0000000 livestreamer-1.12.2/src/livestreamer/utils.py 0000644 0001750 0001750 00000013600 12521217321 022374 0 ustar chrippa chrippa 0000000 0000000 import json
import re
import zlib
try:
import xml.etree.cElementTree as ET
except ImportError:
import xml.etree.ElementTree as ET
from .compat import urljoin, urlparse, parse_qsl, is_py2
from .exceptions import PluginError
def swfdecompress(data):
if data[:3] == b"CWS":
data = b"F" + data[1:8] + zlib.decompress(data[8:])
return data
def verifyjson(json, key):
if not isinstance(json, dict):
raise PluginError("JSON result is not a dict")
if not key in json:
raise PluginError("Missing '{0}' key in JSON".format(key))
return json[key]
def absolute_url(baseurl, url):
if not url.startswith("http"):
return urljoin(baseurl, url)
else:
return url
def prepend_www(url):
"""Changes google.com to www.google.com"""
parsed = urlparse(url)
if parsed.netloc.split(".")[0] != "www":
return parsed.scheme + "://www." + parsed.netloc + parsed.path
else:
return url
def parse_json(data, name="JSON", exception=PluginError, schema=None):
"""Wrapper around json.loads.
Wraps errors in custom exception with a snippet of the data in the message.
"""
try:
json_data = json.loads(data)
except ValueError as err:
snippet = repr(data)
if len(snippet) > 35:
snippet = snippet[:35] + " ..."
else:
snippet = data
raise exception("Unable to parse {0}: {1} ({2})".format(name, err, snippet))
if schema:
json_data = schema.validate(json_data, name=name, exception=exception)
return json_data
def parse_xml(data, name="XML", ignore_ns=False, exception=PluginError, schema=None):
"""Wrapper around ElementTree.fromstring with some extras.
Provides these extra features:
- Handles incorrectly encoded XML
- Allows stripping namespace information
- Wraps errors in custom exception with a snippet of the data in the message
"""
if is_py2 and isinstance(data, unicode):
data = data.encode("utf8")
if ignore_ns:
data = re.sub(" xmlns=\"(.+?)\"", "", data)
try:
tree = ET.fromstring(data)
except Exception as err:
snippet = repr(data)
if len(snippet) > 35:
snippet = snippet[:35] + " ..."
raise exception("Unable to parse {0}: {1} ({2})".format(name, err, snippet))
if schema:
tree = schema.validate(tree, name=name, exception=exception)
return tree
def parse_qsd(data, name="query string", exception=PluginError, schema=None, **params):
"""Parses a query string into a dict.
Unlike parse_qs and parse_qsl, duplicate keys are not preserved in
favor of a simpler return value.
"""
value = dict(parse_qsl(data, **params))
if schema:
value = schema.validate(value, name=name, exception=exception)
return value
def rtmpparse(url):
parse = urlparse(url)
netloc = "{hostname}:{port}".format(hostname=parse.hostname,
port=parse.port or 1935)
split = list(filter(None, parse.path.split("/")))
if len(split) > 2:
app = "/".join(split[:2])
playpath = "/".join(split[2:])
elif len(split) == 2:
app, playpath = split
else:
app = split[0]
if len(parse.query) > 0:
playpath += "?{parse.query}".format(parse=parse)
tcurl = "{scheme}://{netloc}/{app}".format(scheme=parse.scheme,
netloc=netloc,
app=app)
return (tcurl, playpath)
#####################################
# Deprecated functions, do not use. #
#####################################
import requests
def urlget(url, *args, **kwargs):
"""This function is deprecated."""
data = kwargs.pop("data", None)
exception = kwargs.pop("exception", PluginError)
method = kwargs.pop("method", "GET")
session = kwargs.pop("session", None)
timeout = kwargs.pop("timeout", 20)
if data is not None:
method = "POST"
try:
if session:
res = session.request(method, url, timeout=timeout, data=data,
*args, **kwargs)
else:
res = requests.request(method, url, timeout=timeout, data=data,
*args, **kwargs)
res.raise_for_status()
except (requests.exceptions.RequestException, IOError) as rerr:
err = exception("Unable to open URL: {url} ({err})".format(url=url,
err=rerr))
err.err = rerr
raise err
return res
urlopen = urlget
def urlresolve(url):
"""This function is deprecated."""
res = urlget(url, stream=True, allow_redirects=False)
if res.status_code == 302 and "location" in res.headers:
return res.headers["location"]
else:
return url
def res_xml(res, *args, **kw):
"""This function is deprecated."""
return parse_xml(res.text, *args, **kw)
def res_json(res, jsontype="JSON", exception=PluginError):
"""This function is deprecated."""
try:
jsondata = res.json()
except ValueError as err:
if len(res.text) > 35:
snippet = res.text[:35] + "..."
else:
snippet = res.text
raise exception("Unable to parse {0}: {1} ({2})".format(jsontype, err,
snippet))
return jsondata
import hmac
import hashlib
SWF_KEY = b"Genuine Adobe Flash Player 001"
def swfverify(url):
"""This function is deprecated."""
res = urlopen(url)
swf = swfdecompress(res.content)
h = hmac.new(SWF_KEY, swf, hashlib.sha256)
return h.hexdigest(), len(swf)
__all__ = ["urlopen", "urlget", "urlresolve", "swfdecompress", "swfverify",
"verifyjson", "absolute_url", "parse_qsd", "parse_json", "res_json",
"parse_xml", "res_xml", "rtmpparse", "prepend_www"]
livestreamer-1.12.2/src/livestreamer/session.py 0000644 0001750 0001750 00000033220 12521217321 022717 0 ustar chrippa chrippa 0000000 0000000 import imp
import pkgutil
import re
import sys
import traceback
from . import plugins, __version__
from .compat import urlparse, is_win32
from .exceptions import NoPluginError, PluginError
from .logger import Logger
from .options import Options
from .plugin import api
def print_small_exception(start_after):
type, value, traceback_ = sys.exc_info()
tb = traceback.extract_tb(traceback_)
index = 0
for i, trace in enumerate(tb):
if trace[2] == start_after:
index = i+1
break
lines = traceback.format_list(tb[index:])
lines += traceback.format_exception_only(type, value)
for line in lines:
sys.stderr.write(line)
sys.stderr.write("\n")
class Livestreamer(object):
"""A Livestreamer session is used to keep track of plugins,
options and log settings."""
def __init__(self):
self.http = api.HTTPSession()
self.options = Options({
"hds-live-edge": 10.0,
"hds-segment-attempts": 3,
"hds-segment-threads": 1,
"hds-segment-timeout": 10.0,
"hds-timeout": 60.0,
"hls-live-edge": 3,
"hls-segment-attempts": 3,
"hls-segment-threads": 1,
"hls-segment-timeout": 10.0,
"hls-timeout": 60.0,
"http-stream-timeout": 60.0,
"ringbuffer-size": 1024 * 1024 * 16, # 16 MB
"rtmp-timeout": 60.0,
"rtmp-rtmpdump": is_win32 and "rtmpdump.exe" or "rtmpdump",
"rtmp-proxy": None,
"stream-segment-attempts": 3,
"stream-segment-threads": 1,
"stream-segment-timeout": 10.0,
"stream-timeout": 60.0,
"subprocess-errorlog": False
})
self.plugins = {}
self.logger = Logger()
self.load_builtin_plugins()
def set_option(self, key, value):
"""Sets general options used by plugins and streams originating
from this session object.
:param key: key of the option
:param value: value to set the option to
**Available options**:
======================= =========================================
hds-live-edge (float) Specify the time live HDS
streams will start from the edge of
stream, default: ``10.0``
hds-segment-attempts (int) How many attempts should be done
to download each HDS segment, default: ``3``
hds-segment-threads (int) The size of the thread pool used
to download segments, default: ``1``
hds-segment-timeout (float) HDS segment connect and read
timeout, default: ``10.0``
hds-timeout (float) Timeout for reading data from
HDS streams, default: ``60.0``
hls-live-edge (int) How many segments from the end
to start live streams on, default: ``3``
hls-segment-attempts (int) How many attempts should be done
to download each HLS segment, default: ``3``
hls-segment-threads (int) The size of the thread pool used
to download segments, default: ``1``
hls-segment-timeout (float) HLS segment connect and read
timeout, default: ``10.0``
hls-timeout (float) Timeout for reading data from
HLS streams, default: ``60.0``
http-proxy (str) Specify a HTTP proxy to use for
all HTTP requests
https-proxy (str) Specify a HTTPS proxy to use for
all HTTPS requests
http-cookies (dict or str) A dict or a semi-colon (;)
delimited str of cookies to add to each
HTTP request, e.g. ``foo=bar;baz=qux``
http-headers (dict or str) A dict or semi-colon (;)
delimited str of headers to add to each
HTTP request, e.g. ``foo=bar;baz=qux``
http-query-params (dict or str) A dict or a ampersand (&)
delimited string of query parameters to
add to each HTTP request,
e.g. ``foo=bar&baz=qux``
http-trust-env (bool) Trust HTTP settings set in the
environment, such as environment
variables (HTTP_PROXY, etc) and
~/.netrc authentication
http-ssl-verify (bool) Verify SSL certificates,
default: ``True``
http-ssl-cert (str or tuple) SSL certificate to use,
can be either a .pem file (str) or a
.crt/.key pair (tuple)
http-timeout (float) General timeout used by all HTTP
requests except the ones covered by
other options, default: ``20.0``
http-stream-timeout (float) Timeout for reading data from
HTTP streams, default: ``60.0``
subprocess-errorlog (bool) Log errors from subprocesses to
a file located in the temp directory
ringbuffer-size (int) The size of the internal ring
buffer used by most stream types,
default: ``16777216`` (16MB)
rtmp-proxy (str) Specify a proxy (SOCKS) that RTMP
streams will use
rtmp-rtmpdump (str) Specify the location of the
rtmpdump executable used by RTMP streams,
e.g. ``/usr/local/bin/rtmpdump``
rtmp-timeout (float) Timeout for reading data from
RTMP streams, default: ``60.0``
stream-segment-attempts (int) How many attempts should be done
to download each segment, default: ``3``.
General option used by streams not
covered by other options.
stream-segment-threads (int) The size of the thread pool used
to download segments, default: ``1``.
General option used by streams not
covered by other options.
stream-segment-timeout (float) Segment connect and read
timeout, default: ``10.0``.
General option used by streams not
covered by other options.
stream-timeout (float) Timeout for reading data from
stream, default: ``60.0``.
General option used by streams not
covered by other options.
======================= =========================================
"""
# Backwards compatibility
if key == "rtmpdump":
key = "rtmp-rtmpdump"
elif key == "rtmpdump-proxy":
key = "rtmp-proxy"
elif key == "errorlog":
key = "subprocess-errorlog"
if key == "http-proxy":
if not re.match("^http(s)?://", value):
value = "http://" + value
self.http.proxies["http"] = value
elif key == "https-proxy":
if not re.match("^http(s)?://", value):
value = "https://" + value
self.http.proxies["https"] = value
elif key == "http-cookies":
if isinstance(value, dict):
self.http.cookies.update(value)
else:
self.http.parse_cookies(value)
elif key == "http-headers":
if isinstance(value, dict):
self.http.headers.update(value)
else:
self.http.parse_headers(value)
elif key == "http-query-params":
if isinstance(value, dict):
self.http.params.update(value)
else:
self.http.parse_query_params(value)
elif key == "http-trust-env":
self.http.trust_env = value
elif key == "http-ssl-verify":
self.http.verify = value
elif key == "http-ssl-cert":
self.http.cert = value
elif key == "http-timeout":
self.http.timeout = value
else:
self.options.set(key, value)
def get_option(self, key):
"""Returns current value of specified option.
:param key: key of the option
"""
# Backwards compatibility
if key == "rtmpdump":
key = "rtmp-rtmpdump"
elif key == "rtmpdump-proxy":
key = "rtmp-proxy"
elif key == "errorlog":
key = "subprocess-errorlog"
if key == "http-proxy":
return self.http.proxies.get("http")
elif key == "https-proxy":
return self.http.proxies.get("https")
elif key == "http-cookies":
return self.http.cookies
elif key == "http-headers":
return self.http.headers
elif key == "http-query-params":
return self.http.params
elif key == "http-trust-env":
return self.http.trust_env
elif key == "http-ssl-verify":
return self.http.verify
elif key == "http-ssl-cert":
return self.http.cert
elif key == "http-timeout":
return self.http.timeout
else:
return self.options.get(key)
def set_plugin_option(self, plugin, key, value):
"""Sets plugin specific options used by plugins originating
from this session object.
:param plugin: name of the plugin
:param key: key of the option
:param value: value to set the option to
"""
if plugin in self.plugins:
plugin = self.plugins[plugin]
plugin.set_option(key, value)
def get_plugin_option(self, plugin, key):
"""Returns current value of plugin specific option.
:param plugin: name of the plugin
:param key: key of the option
"""
if plugin in self.plugins:
plugin = self.plugins[plugin]
return plugin.get_option(key)
def set_loglevel(self, level):
"""Sets the log level used by this session.
Valid levels are: "none", "error", "warning", "info"
and "debug".
:param level: level of logging to output
"""
self.logger.set_level(level)
def set_logoutput(self, output):
"""Sets the log output used by this session.
:param output: a file-like object with a write method
"""
self.logger.set_output(output)
def resolve_url(self, url):
"""Attempts to find a plugin that can use this URL.
The default protocol (http) will be prefixed to the URL if
not specified.
Raises :exc:`NoPluginError` on failure.
:param url: a URL to match against loaded plugins
"""
parsed = urlparse(url)
if len(parsed.scheme) == 0:
url = "http://" + url
for name, plugin in self.plugins.items():
if plugin.can_handle_url(url):
obj = plugin(url)
return obj
# Attempt to handle a redirect URL
try:
res = self.http.head(url, allow_redirects=True, acceptable_status=[501])
# Fall back to GET request if server doesn't handle HEAD.
if res.status_code == 501:
res = self.http.get(url, stream=True)
if res.url != url:
return self.resolve_url(res.url)
except PluginError:
pass
raise NoPluginError
def streams(self, 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.
"""
plugin = self.resolve_url(url)
return plugin.streams(**params)
def get_plugins(self):
"""Returns the loaded plugins for the session."""
return self.plugins
def load_builtin_plugins(self):
self.load_plugins(plugins.__path__[0])
def load_plugins(self, path):
"""Attempt to load plugins from the path specified.
:param path: full path to a directory where to look for plugins
"""
for loader, name, ispkg in pkgutil.iter_modules([path]):
file, pathname, desc = imp.find_module(name, [path])
try:
self.load_plugin(name, file, pathname, desc)
except Exception:
sys.stderr.write("Failed to load plugin {0}:\n".format(name))
print_small_exception("load_plugin")
continue
def load_plugin(self, name, file, pathname, desc):
# Set the global http session for this plugin
api.http = self.http
module = imp.load_module(name, file, pathname, desc)
if hasattr(module, "__plugin__"):
module_name = getattr(module, "__name__")
plugin = getattr(module, "__plugin__")
plugin.bind(self, module_name)
self.plugins[plugin.module] = plugin
if file:
file.close()
@property
def version(self):
return __version__
__all__ = ["Livestreamer"]
livestreamer-1.12.2/src/livestreamer/options.py 0000644 0001750 0001750 00000000561 12521217321 022731 0 ustar chrippa chrippa 0000000 0000000 class Options(object):
def __init__(self, defaults=None):
if not defaults:
defaults = {}
self.defaults = defaults
self.options = defaults.copy()
def set(self, key, value):
self.options[key] = value
def get(self, key):
if key in self.options:
return self.options[key]
__all__ = ["Options"]
livestreamer-1.12.2/src/livestreamer/logger.py 0000644 0001750 0001750 00000003170 12521217321 022514 0 ustar chrippa chrippa 0000000 0000000 import sys
from threading import Lock
class Logger(object):
Levels = ["none", "error", "warning", "info", "debug"]
Format = "[{module}][{level}] {msg}\n"
def __init__(self):
self.output = sys.stdout
self.level = 0
self.lock = Lock()
def new_module(self, module):
return LoggerModule(self, module)
def set_level(self, level):
try:
index = Logger.Levels.index(level)
except ValueError:
return
self.level = index
def set_output(self, output):
self.output = output
def msg(self, module, level, msg, *args, **kwargs):
if self.level < level or level > len(Logger.Levels):
return
msg = msg.format(*args, **kwargs)
with self.lock:
self.output.write(Logger.Format.format(module=module,
level=Logger.Levels[level],
msg=msg))
if hasattr(self.output, "flush"):
self.output.flush()
class LoggerModule(object):
def __init__(self, manager, module):
self.manager = manager
self.module = module
def error(self, msg, *args, **kwargs):
self.manager.msg(self.module, 1, msg, *args, **kwargs)
def warning(self, msg, *args, **kwargs):
self.manager.msg(self.module, 2, msg, *args, **kwargs)
def info(self, msg, *args, **kwargs):
self.manager.msg(self.module, 3, msg, *args, **kwargs)
def debug(self, msg, *args, **kwargs):
self.manager.msg(self.module, 4, msg, *args, **kwargs)
__all__ = ["Logger"]
livestreamer-1.12.2/src/livestreamer/exceptions.py 0000644 0001750 0001750 00000001215 12521217321 023414 0 ustar chrippa chrippa 0000000 0000000 class LivestreamerError(Exception):
"""Any error caused by Livestreamer will be caught
with this exception."""
class PluginError(LivestreamerError):
"""Plugin related error."""
class NoStreamsError(LivestreamerError):
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(LivestreamerError):
"""Stream related error."""
__all__ = ["LivestreamerError", "PluginError", "NoPluginError",
"NoStreamsError", "StreamError"]
livestreamer-1.12.2/src/livestreamer/compat.py 0000644 0001750 0001750 00000001472 12521217321 022523 0 ustar chrippa chrippa 0000000 0000000 import os
import sys
is_py2 = (sys.version_info[0] == 2)
is_py3 = (sys.version_info[0] == 3)
is_py33 = (sys.version_info[0] == 3 and sys.version_info[1] == 3)
is_win32 = os.name == "nt"
if is_py2:
_str = str
str = unicode
range = xrange
def bytes(b, enc="ascii"):
return _str(b)
elif is_py3:
bytes = bytes
str = str
range = range
try:
from urllib.parse import (
urlparse, urlunparse, urljoin, quote, unquote, parse_qsl
)
import queue
except ImportError:
from urlparse import urlparse, urlunparse, urljoin, parse_qsl
from urllib import quote, unquote
import Queue as queue
__all__ = ["is_py2", "is_py3", "is_py33", "is_win32", "str", "bytes",
"urlparse", "urlunparse", "urljoin", "parse_qsl", "quote",
"unquote", "queue", "range"]
livestreamer-1.12.2/src/livestreamer/cache.py 0000644 0001750 0001750 00000004534 12521217321 022305 0 ustar chrippa chrippa 0000000 0000000 import json
import os
import shutil
import tempfile
from time import time
from .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, "livestreamer")
class Cache(object):
"""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:
self._cache = {}
else:
self._cache = {}
def _prune(self):
now = time()
pruned = []
for key, value in self._cache.items():
expires = value.get("expires", time())
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 (IOError, OSError):
os.remove(tempname)
def set(self, key, value, expires=60 * 60 * 24 * 7):
self._load()
self._prune()
if self.key_prefix:
key = "{0}:{1}".format(self.key_prefix, key)
expires += time()
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
__all__ = ["Cache"]
livestreamer-1.12.2/src/livestreamer/buffers.py 0000644 0001750 0001750 00000007620 12521217321 022675 0 ustar chrippa chrippa 0000000 0000000 from 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(object):
"""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:
self.event_used.wait(timeout)
# If the event is still not set it's a timeout
if not self.event_used.is_set() and self.length == 0:
raise IOError("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"]
livestreamer-1.12.2/src/livestreamer/api.py 0000644 0001750 0001750 00000000500 12521217321 022000 0 ustar chrippa chrippa 0000000 0000000 from .session import Livestreamer
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 = Livestreamer()
return session.streams(url, **params)
livestreamer-1.12.2/src/livestreamer/__init__.py 0000644 0001750 0001750 00000004151 12521217321 022774 0 ustar chrippa chrippa 0000000 0000000 # coding: utf8
"""Livestreamer extracts streams from various services.
The main compontent of Livestreamer 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 http://docs.livestreamer.io/.
"""
__title__ = "livestreamer"
__version__ = "1.12.2"
__license__ = "Simplified BSD"
__author__ = "Christopher Rosell"
__copyright__ = "Copyright 2011-2015 Christopher Rosell"
__credits__ = [
"Agustín Carrasco (@asermax)",
"Andrew Bashore (@bashtech)",
"Andy Mikhailenko (@neithere)",
"Athanasios Oikonomou (@athoik)",
"Brian Callahan (@ibara)",
"Che (@chhe)",
"Christopher Rosell (@chrippa)",
"Daniel Meißner (@meise)",
"Daniel Miranda (@danielkza)",
"Daniel Wallace (@gtmanfred)",
"David Arvelo (@darvelo)",
"Dominik Dabrowski (@doda)",
"Erik G (@tboss)",
"Eric J (@wormeyman)",
"Ethan Jones (@jonesz)",
"Gaspard Jankowiak (@gapato)",
"Jaime Marquínez Ferrándiz (@jaimeMF)",
"Jan Tore Morken (@jantore)",
"John Peterson (@john-peterson)",
"Jon Bergli Heier (@sn4kebite)",
"Joseph Glanville (@josephglanville)",
"Julian Richen (@FireDart)",
"Kacper (@kasper93)",
"Martin Panter (@vadmium)",
"Max Nordlund (@maxnordlund)",
"Michael Cheah (@cheah)",
"Moritz Blanke",
"Niall McAndrew (@niallm90)",
"Niels Kräupl (@Gamewalker)",
"Pascal Romahn (@skulblakka)",
"Sam Edwards (@dotsam)",
"Stefan Breunig (@breunigs)",
"Suhail Patel (@suhailpatel)",
"Sunaga Takahiro (@sunaga720)",
"Vitaly Evtushenko (@eltiren)",
"Warnar Boekkooi (@boekkooi)",
"@blxd",
"@btiom",
"@daslicious",
"@MasterofJOKers",
"@mammothb",
"@medina",
"@monkeyphysics",
"@nixxquality",
"@papplampe",
"@Raziel-23",
"@t0mm0",
"@ToadKing",
"@unintended",
"@wolftankk",
"@yeeeargh"
]
from .api import streams
from .exceptions import (LivestreamerError, PluginError, NoStreamsError,
NoPluginError, StreamError)
from .session import Livestreamer
livestreamer-1.12.2/src/livestreamer/stream/ 0000755 0001750 0001750 00000000000 12521217500 022154 5 ustar chrippa chrippa 0000000 0000000 livestreamer-1.12.2/src/livestreamer/stream/wrappers.py 0000644 0001750 0001750 00000005324 12521217321 024376 0 ustar chrippa chrippa 0000000 0000000 from ..buffers import Buffer, RingBuffer
from threading import Thread
import io
class StreamIOWrapper(io.IOBase):
"""Wraps file-like objects that are not inheriting from IOBase"""
def __init__(self, fd):
self.fd = fd
def read(self, size=-1):
return self.fd.read(size)
def close(self):
if hasattr(self.fd, "close"):
self.fd.close()
class StreamIOIterWrapper(io.IOBase):
"""Wraps a iterator and turn it into a file-like object"""
def __init__(self, iterator):
self.iterator = iterator
self.buffer = Buffer()
def read(self, size=-1):
if size < 0:
size = self.buffer.length
while self.buffer.length < size:
try:
chunk = next(self.iterator)
self.buffer.write(chunk)
except StopIteration:
break
return self.buffer.read(size)
def close(self):
pass
class StreamIOThreadWrapper(io.IOBase):
"""Wraps a file-like object in a thread.
Useful for getting control over read timeout where
timeout handling is missing or out of our control.
"""
class Filler(Thread):
def __init__(self, fd, buffer):
Thread.__init__(self)
self.error = None
self.fd = fd
self.buffer = buffer
self.daemon = True
self.running = False
def run(self):
self.running = True
while self.running:
try:
data = self.fd.read(8192)
except IOError as error:
self.error = error
break
if len(data) == 0:
break
self.buffer.write(data)
self.stop()
def stop(self):
self.running = False
self.buffer.close()
if hasattr(self.fd, "close"):
try:
self.fd.close()
except Exception:
pass
def __init__(self, session, fd, timeout=30):
self.buffer = RingBuffer(session.get_option("ringbuffer-size"))
self.fd = fd
self.timeout = timeout
self.filler = StreamIOThreadWrapper.Filler(self.fd, self.buffer)
self.filler.start()
def read(self, size=-1):
if self.filler.error and self.buffer.length == 0:
raise self.filler.error
return self.buffer.read(size, block=self.filler.is_alive(),
timeout=self.timeout)
def close(self):
self.filler.stop()
if self.filler.is_alive():
self.filler.join()
__all__ = ["StreamIOWrapper", "StreamIOIterWrapper", "StreamIOThreadWrapper"]
livestreamer-1.12.2/src/livestreamer/stream/streamprocess.py 0000644 0001750 0001750 00000004740 12521217321 025426 0 ustar chrippa chrippa 0000000 0000000 from .stream import Stream
from .wrappers import StreamIOThreadWrapper
from ..compat import str
from ..exceptions import StreamError
from ..packages import pbs as sh
import os
import time
import tempfile
class StreamProcessIO(StreamIOThreadWrapper):
def __init__(self, session, process, **kwargs):
self.process = process
StreamIOThreadWrapper.__init__(self, session,
process.stdout,
**kwargs)
def close(self):
try:
self.process.kill()
except Exception:
pass
StreamIOThreadWrapper.close(self)
class StreamProcess(Stream):
def __init__(self, session, params=None, timeout=60.0):
Stream.__init__(self, session)
if not params:
params = {}
self.params = params
self.errorlog = self.session.options.get("subprocess-errorlog")
self.timeout = timeout
def open(self):
cmd = self._check_cmd()
params = self.params.copy()
params["_bg"] = True
if self.errorlog:
tmpfile = tempfile.NamedTemporaryFile(prefix="livestreamer",
suffix=".err", delete=False)
params["_err"] = tmpfile
else:
params["_err"] = open(os.devnull, "wb")
with params["_err"]:
stream = cmd(**params)
# Wait 0.5 seconds to see if program exited prematurely
time.sleep(0.5)
process_alive = stream.process.returncode is None
if not process_alive:
if self.errorlog:
raise StreamError(("Error while executing subprocess, "
"error output logged to: {0}").format(tmpfile.name))
else:
raise StreamError("Error while executing subprocess")
return StreamProcessIO(self.session, stream.process,
timeout=self.timeout)
def _check_cmd(self):
try:
cmd = sh.create_command(self.cmd)
except sh.CommandNotFound as err:
raise StreamError("Unable to find {0} command".format(err))
return cmd
def cmdline(self):
cmd = self._check_cmd()
return str(cmd.bake(**self.params))
@classmethod
def is_usable(cls, cmd):
try:
cmd = sh.create_command(cmd)
except sh.CommandNotFound:
return False
return True
__all__ = ["StreamProcess"]
livestreamer-1.12.2/src/livestreamer/stream/stream.py 0000644 0001750 0001750 00000001623 12521217321 024024 0 ustar chrippa chrippa 0000000 0000000 import io
import json
class Stream(object):
__shortname__ = "stream"
"""
This is a base class that should be inherited when implementing
different stream types. Should only be created by plugins.
"""
def __init__(self, session):
self.session = session
def __repr__(self):
return ""
def __json__(self):
return dict(type=type(self).shortname())
def open(self):
"""
Attempts to open a connection to the stream.
Returns a file-like object that can be used to read the stream data.
Raises :exc:`StreamError` on failure.
"""
raise NotImplementedError
@property
def json(self):
obj = self.__json__()
return json.dumps(obj)
@classmethod
def shortname(cls):
return cls.__shortname__
class StreamIO(io.IOBase):
pass
__all__ = ["Stream", "StreamIO"]
livestreamer-1.12.2/src/livestreamer/stream/segmented.py 0000644 0001750 0001750 00000012615 12521217321 024507 0 ustar chrippa chrippa 0000000 0000000 from concurrent import futures
from threading import Thread, Event
from .stream import StreamIO
from ..buffers import RingBuffer
from ..compat import queue
class SegmentedStreamWorker(Thread):
"""The general worker thread.
This thread is responsible for queueing up segments in the
writer thread.
"""
def __init__(self, reader):
self.closed = False
self.reader = reader
self.writer = reader.writer
self.stream = reader.stream
self.session = reader.stream.session
self.logger = reader.logger
self._wait = None
Thread.__init__(self)
self.daemon = True
def close(self):
"""Shuts down the thread."""
if not self.closed:
self.logger.debug("Closing worker thread")
self.closed = True
if self._wait:
self._wait.set()
def wait(self, time):
"""Pauses the thread for a specified time.
Returns False if interrupted by another thread and True if the
time runs out normally.
"""
self._wait = Event()
return not self._wait.wait(time)
def iter_segments(self):
"""The iterator that generates segments for the worker thread.
Should be overridden by the inheriting class.
"""
return
yield
def run(self):
for segment in self.iter_segments():
self.writer.put(segment)
# End of stream, tells the writer to exit
self.writer.put(None)
self.close()
class SegmentedStreamWriter(Thread):
"""The writer thread.
This thread is responsible for fetching segments, processing them
and finally writing the data to the buffer.
"""
def __init__(self, reader, size=20, retries=None, threads=None, timeout=None):
self.closed = False
self.reader = reader
self.stream = reader.stream
self.session = reader.stream.session
self.logger = reader.logger
if not retries:
retries = self.session.options.get("stream-segment-attempts")
if not threads:
threads = self.session.options.get("stream-segment-threads")
if not timeout:
timeout = self.session.options.get("stream-segment-timeout")
self.retries = retries
self.timeout = timeout
self.executor = futures.ThreadPoolExecutor(max_workers=threads)
self.futures = queue.Queue(size)
Thread.__init__(self)
self.daemon = True
def close(self):
"""Shuts down the thread."""
if not self.closed:
self.logger.debug("Closing writer thread")
self.closed = True
self.reader.buffer.close()
self.executor.shutdown(wait=True)
def put(self, segment):
"""Adds a segment to the download pool and write queue."""
if self.closed:
return
if segment is not None:
future = self.executor.submit(self.fetch, segment,
retries=self.retries)
else:
future = None
self.queue(self.futures, (segment, future))
def queue(self, queue_, value):
"""Puts a value into a queue but aborts if this thread is closed."""
while not self.closed:
try:
queue_.put(value, block=True, timeout=1)
break
except queue.Full:
continue
def fetch(self, segment):
"""Fetches a segment.
Should be overridden by the inheriting class.
"""
pass
def write(self, segment, result):
"""Writes a segment to the buffer.
Should be overridden by the inheriting class.
"""
pass
def run(self):
while not self.closed:
try:
segment, future = self.futures.get(block=True, timeout=0.5)
except queue.Empty:
continue
# End of stream
if future is None:
break
while not self.closed:
try:
result = future.result(timeout=0.5)
except futures.TimeoutError:
continue
except futures.CancelledError:
break
if result is not None:
self.write(segment, result)
break
self.close()
class SegmentedStreamReader(StreamIO):
__worker__ = SegmentedStreamWorker
__writer__ = SegmentedStreamWriter
def __init__(self, stream, timeout=None):
StreamIO.__init__(self)
self.session = stream.session
self.stream = stream
if not timeout:
timeout = self.session.options.get("stream-timeout")
self.timeout = timeout
def open(self):
buffer_size = self.session.get_option("ringbuffer-size")
self.buffer = RingBuffer(buffer_size)
self.writer = self.__writer__(self)
self.worker = self.__worker__(self)
self.writer.start()
self.worker.start()
def close(self):
self.worker.close()
self.writer.close()
for thread in (self.worker, self.writer):
if thread.is_alive():
thread.join()
self.buffer.close()
def read(self, size):
if not self.buffer:
return b""
return self.buffer.read(size, block=self.writer.is_alive(),
timeout=self.timeout)
livestreamer-1.12.2/src/livestreamer/stream/rtmpdump.py 0000644 0001750 0001750 00000007473 12521217321 024412 0 ustar chrippa chrippa 0000000 0000000 import re
from time import sleep
from .streamprocess import StreamProcess
from ..compat import str
from ..exceptions import StreamError
from ..packages import pbs as sh
from ..utils import rtmpparse
class RTMPStream(StreamProcess):
"""RTMP stream using rtmpdump.
*Attributes:*
- :attr:`params` A :class:`dict` containing parameters passed to rtmpdump
"""
__shortname__ = "rtmp"
def __init__(self, session, params, redirect=False):
StreamProcess.__init__(self, session, params)
self.cmd = self.session.options.get("rtmp-rtmpdump")
self.timeout = self.session.options.get("rtmp-timeout")
self.redirect = redirect
self.logger = session.logger.new_module("stream.rtmp")
def __repr__(self):
return ("").format(self.params,
self.redirect)
def __json__(self):
return dict(type=RTMPStream.shortname(), params=self.params)
def open(self):
if self.session.options.get("rtmp-proxy"):
if not self._supports_param("socks"):
raise StreamError("Installed rtmpdump does not support --socks argument")
self.params["socks"] = self.session.options.get("rtmp-proxy")
if "jtv" in self.params and not self._supports_param("jtv"):
raise StreamError("Installed rtmpdump does not support --jtv argument")
if "weeb" in self.params and not self._supports_param("weeb"):
raise StreamError("Installed rtmpdump does not support --weeb argument")
if self.redirect:
self._check_redirect()
self.params["flv"] = "-"
return StreamProcess.open(self)
def _check_redirect(self, timeout=20):
cmd = self._check_cmd()
params = self.params.copy()
params["verbose"] = True
params["_bg"] = True
self.logger.debug("Attempting to find tcURL redirect")
stream = cmd(**params)
elapsed = 0
process_alive = True
while elapsed < timeout and process_alive:
stream.process.poll()
process_alive = stream.process.returncode is None
sleep(0.25)
elapsed += 0.25
if process_alive:
try:
stream.process.kill()
except Exception:
pass
stream.process.wait()
try:
stderr = stream.stderr()
except sh.ErrorReturnCode as err:
self._update_redirect(err.stderr)
def _update_redirect(self, stderr):
tcurl, redirect = None, None
stderr = str(stderr, "utf8")
m = re.search("DEBUG: Property: ", stderr)
if m:
redirect = m.group(1)
if redirect:
self.logger.debug("Found redirect tcUrl: {0}", redirect)
if "rtmp" in self.params:
tcurl, playpath = rtmpparse(self.params["rtmp"])
rtmp = "{redirect}/{playpath}".format(**locals())
self.params["rtmp"] = rtmp
if "tcUrl" in self.params:
self.params["tcUrl"] = redirect
def _supports_param(self, param):
cmd = self._check_cmd()
try:
help = cmd(help=True, _err_to_out=True)
except sh.ErrorReturnCode as err:
err = str(err.stdout, "ascii")
raise StreamError("Error while checking rtmpdump compatibility: {0}".format(err))
for line in help.splitlines():
m = re.match("^--(\w+)", line)
if not m:
continue
if m.group(1) == param:
return True
return False
@classmethod
def is_usable(cls, session):
cmd = session.options.get("rtmp-rtmpdump")
return StreamProcess.is_usable(cmd)
livestreamer-1.12.2/src/livestreamer/stream/playlist.py 0000644 0001750 0001750 00000003714 12521217321 024375 0 ustar chrippa chrippa 0000000 0000000 from .flvconcat import FLVTagConcatIO
from .stream import Stream
from ..exceptions import StreamError
__all__ = ["Playlist", "FLVPlaylist"]
class Playlist(Stream):
"""Abstract base class for playlist type streams."""
__shortname__ = "playlist"
def __init__(self, session, streams, duration=None):
Stream.__init__(self, session)
self.streams = streams
self.duration = duration
def open(self):
raise NotImplementedError
def __json__(self):
return dict(streams=self.streams, duration=self.duration,
**Stream.__json__(self))
class FLVPlaylistIO(FLVTagConcatIO):
__log_name__ = "stream.flv_playlist"
def open(self, streams):
def generator():
for stream in streams:
self.logger.debug("Opening substream: {0}", stream)
# No need for multiple ringbuffers
if hasattr(stream, "buffered"):
stream.buffered = False
try:
fd = stream.open()
except StreamError as err:
self.logger.error("Failed to open stream: {0}", err)
continue
yield fd
return FLVTagConcatIO.open(self, generator())
class FLVPlaylist(Playlist):
__shortname__ = "flv_playlist"
def __init__(self, session, streams, duration=None, tags=None,
skip_header=None, **concater_params):
Playlist.__init__(self, session, streams, duration)
if not tags:
tags = []
self.tags = tags
self.skip_header = skip_header
self.concater_params = concater_params
def open(self):
fd = FLVPlaylistIO(self.session,
tags=self.tags,
duration=self.duration,
skip_header=self.skip_header,
**self.concater_params)
fd.open(self.streams)
return fd
livestreamer-1.12.2/src/livestreamer/stream/http.py 0000644 0001750 0001750 00000004574 12521217321 023520 0 ustar chrippa chrippa 0000000 0000000 import inspect
import requests
from .stream import Stream
from .wrappers import StreamIOThreadWrapper, StreamIOIterWrapper
from ..exceptions import StreamError
def normalize_key(keyval):
key, val = keyval
key = hasattr(key, "decode") and key.decode("utf8", "ignore") or key
return key, val
def valid_args(args):
argspec = inspect.getargspec(requests.Request.__init__)
return dict(filter(lambda kv: kv[0] in argspec.args, args.items()))
class HTTPStream(Stream):
"""A HTTP stream using the requests library.
*Attributes:*
- :attr:`url` The URL to the stream, prepared by requests.
- :attr:`args` A :class:`dict` containing keyword arguments passed
to :meth:`requests.request`, such as headers and cookies.
"""
__shortname__ = "http"
def __init__(self, session_, url, buffered=True, **args):
Stream.__init__(self, session_)
self.args = dict(url=url, **args)
self.buffered = buffered
def __repr__(self):
return "".format(self.url)
def __json__(self):
method = self.args.get("method", "GET")
req = requests.Request(method=method, **valid_args(self.args))
# prepare_request is only available in requests 2.0+
if hasattr(self.session.http, "prepare_request"):
req = self.session.http.prepare_request(req)
else:
req = req.prepare()
headers = dict(map(normalize_key, req.headers.items()))
return dict(type=type(self).shortname(), url=req.url,
method=req.method, headers=headers,
body=req.body)
@property
def url(self):
method = self.args.get("method", "GET")
return requests.Request(method=method,
**valid_args(self.args)).prepare().url
def open(self):
method = self.args.get("method", "GET")
timeout = self.session.options.get("http-timeout")
res = self.session.http.request(method=method,
stream=True,
exception=StreamError,
timeout=timeout,
**self.args)
fd = StreamIOIterWrapper(res.iter_content(8192))
if self.buffered:
fd = StreamIOThreadWrapper(self.session, fd, timeout=timeout)
return fd
livestreamer-1.12.2/src/livestreamer/stream/hls_playlist.py 0000644 0001750 0001750 00000024345 12521217321 025246 0 ustar chrippa chrippa 0000000 0000000 import re
from binascii import unhexlify
from collections import namedtuple
from itertools import starmap
try:
from urlparse import urljoin
except ImportError:
from urllib.parse import urljoin
__all__ = ["load", "M3U8Parser"]
# EXT-X-BYTERANGE
ByteRange = namedtuple("ByteRange", "range offset")
# EXT-X-KEY
Key = namedtuple("Key", "method uri iv key_format key_format_versions")
# EXT-X-MAP
Map = namedtuple("Map", "uri byterange")
# EXT-X-MEDIA
Media = namedtuple("Media", "uri type group_id language name default "
"autoselect forced characteristics")
# EXT-X-START
Start = namedtuple("Start", "time_offset precise")
# EXT-X-STREAM-INF
StreamInfo = namedtuple("StreamInfo", "bandwidth program_id codecs resolution "
"audio video subtitles")
# EXT-X-I-FRAME-STREAM-INF
IFrameStreamInfo = namedtuple("IFrameStreamInfo", "bandwidth program_id "
"codecs resolution video")
Playlist = namedtuple("Playlist", "uri stream_info media is_iframe")
Resolution = namedtuple("Resolution", "width height")
Segment = namedtuple("Segment", "uri duration title key discontinuity "
"byterange date map")
ATTRIBUTE_REGEX = (r"([A-Z\-]+)=(\d+\.\d+|0x[0-9A-z]+|\d+x\d+|\d+|"
r"\"(.+?)\"|[0-9A-z\-]+)")
class M3U8(object):
def __init__(self):
self.is_endlist = False
self.is_master = False
self.allow_cache = None
self.discontinuity_sequence = None
self.iframes_only = None
self.media_sequence = None
self.playlist_type = None
self.target_duration = None
self.start = None
self.version = None
self.media = []
self.playlists = []
self.segments = []
class M3U8Parser(object):
def __init__(self, base_uri=None):
self.base_uri = base_uri
def create_stream_info(self, streaminf, cls=None):
program_id = streaminf.get("PROGRAM-ID")
if program_id:
program_id = int(program_id)
bandwidth = streaminf.get("BANDWIDTH")
if bandwidth:
bandwidth = int(bandwidth)
resolution = streaminf.get("RESOLUTION")
if resolution:
resolution = self.parse_resolution(resolution)
codecs = streaminf.get("CODECS")
if codecs:
codecs = codecs.split(",")
else:
codecs = []
if cls == IFrameStreamInfo:
return IFrameStreamInfo(bandwidth, program_id, codecs, resolution,
streaminf.get("VIDEO"))
else:
return StreamInfo(bandwidth, program_id, codecs, resolution,
streaminf.get("AUDIO"), streaminf.get("VIDEO"),
streaminf.get("SUBTITLES"))
def split_tag(self, line):
match = re.match("#(?P[\w-]+)(:(?P.+))?", line)
if match:
return match.group("tag"), match.group("value").strip()
return None, None
def parse_attributes(self, value):
def map_attribute(key, value, quoted):
return (key, quoted or value)
attr = re.findall(ATTRIBUTE_REGEX, value)
return dict(starmap(map_attribute, attr))
def parse_bool(self, value):
return value == "YES"
def parse_byterange(self, value):
match = re.match("(?P\d+)(@(?P.+))?", value)
if match:
return ByteRange(int(match.group("range")),
int(match.group("offset") or 0))
def parse_extinf(self, value):
match = re.match("(?P\d+(\.\d+)?)(,(?P.+))?", value)
if match:
return float(match.group("duration")), match.group("title")
def parse_hex(self, value):
value = value[2:]
if len(value) % 2:
value = "0" + value
return unhexlify(value)
def parse_resolution(self, value):
match = re.match("(\d+)x(\d+)", value)
if match:
width, height = int(match.group(1)), int(match.group(2))
else:
width, height = 0, 0
return Resolution(width, height)
def parse_tag(self, line, transform=None):
tag, value = self.split_tag(line)
if transform:
value = transform(value)
return value
def parse_line(self, lineno, line):
if lineno == 0 and not line.startswith("#EXTM3U"):
raise ValueError("Missing #EXTM3U header")
if not line.startswith("#"):
if self.state.pop("expect_segment", None):
byterange = self.state.pop("byterange", None)
extinf = self.state.pop("extinf", (0, None))
date = self.state.pop("date", None)
map_ = self.state.get("map")
key = self.state.get("key")
segment = Segment(self.uri(line), extinf[0],
extinf[1], key,
self.state.pop("discontinuity", False),
byterange, date, map_)
self.m3u8.segments.append(segment)
elif self.state.pop("expect_playlist", None):
streaminf = self.state.pop("streaminf", {})
stream_info = self.create_stream_info(streaminf)
playlist = Playlist(self.uri(line), stream_info, [], False)
self.m3u8.playlists.append(playlist)
elif line.startswith("#EXTINF"):
self.state["expect_segment"] = True
self.state["extinf"] = self.parse_tag(line, self.parse_extinf)
elif line.startswith("#EXT-X-BYTERANGE"):
self.state["expect_segment"] = True
self.state["byterange"] = self.parse_tag(line, self.parse_byterange)
elif line.startswith("#EXT-X-TARGETDURATION"):
self.m3u8.target_duration = self.parse_tag(line, int)
elif line.startswith("#EXT-X-MEDIA-SEQUENCE"):
self.m3u8.media_sequence = self.parse_tag(line, int)
elif line.startswith("#EXT-X-KEY"):
attr = self.parse_tag(line, self.parse_attributes)
iv = attr.get("IV")
if iv: iv = self.parse_hex(iv)
self.state["key"] = Key(attr.get("METHOD"),
self.uri(attr.get("URI")),
iv, attr.get("KEYFORMAT"),
attr.get("KEYFORMATVERSIONS"))
elif line.startswith("#EXT-X-PROGRAM-DATE-TIME"):
self.state["date"] = self.parse_tag(line)
elif line.startswith("#EXT-X-ALLOW-CACHE"):
self.m3u8.allow_cache = self.parse_tag(line, self.parse_bool)
elif line.startswith("#EXT-X-STREAM-INF"):
self.state["streaminf"] = self.parse_tag(line, self.parse_attributes)
self.state["expect_playlist"] = True
elif line.startswith("#EXT-X-PLAYLIST-TYPE"):
self.m3u8.playlist_type = self.parse_tag(line)
elif line.startswith("#EXT-X-ENDLIST"):
self.m3u8.is_endlist = True
elif line.startswith("#EXT-X-MEDIA"):
attr = self.parse_tag(line, self.parse_attributes)
media = Media(self.uri(attr.get("URI")), attr.get("TYPE"),
attr.get("GROUP-ID"), attr.get("LANGUAGE"),
attr.get("NAME"),
self.parse_bool(attr.get("DEFAULT")),
self.parse_bool(attr.get("AUTOSELECT")),
self.parse_bool(attr.get("FORCED")),
attr.get("CHARACTERISTICS"))
self.m3u8.media.append(media)
elif line.startswith("#EXT-X-DISCONTINUITY"):
self.state["discontinuity"] = True
self.state["map"] = None
elif line.startswith("#EXT-X-DISCONTINUITY-SEQUENCE"):
self.m3u8.discontinuity_sequence = self.parse_tag(line, int)
elif line.startswith("#EXT-X-I-FRAMES-ONLY"):
self.m3u8.iframes_only = True
elif line.startswith("#EXT-X-MAP"):
attr = self.parse_tag(line, self.parse_attributes)
byterange = self.parse_byterange(attr.get("BYTERANGE", ""))
self.state["map"] = Map(attr.get("URI"), byterange)
elif line.startswith("#EXT-X-I-FRAME-STREAM-INF"):
attr = self.parse_tag(line, self.parse_attributes)
streaminf = self.state.pop("streaminf", attr)
stream_info = self.create_stream_info(streaminf, IFrameStreamInfo)
playlist = Playlist(self.uri(attr.get("URI")), stream_info, [], True)
self.m3u8.playlists.append(playlist)
elif line.startswith("#EXT-X-VERSION"):
self.m3u8.version = self.parse_tag(line, int)
elif line.startswith("#EXT-X-START"):
attr = self.parse_tag(line, self.parse_attributes)
start = Start(attr.get("TIME-OFFSET"),
self.parse_bool(attr.get("PRECISE", "NO")))
self.m3u8.start = start
def parse(self, data):
self.state = {}
self.m3u8 = M3U8()
for lineno, line in enumerate(filter(bool, data.splitlines())):
self.parse_line(lineno, line)
# Associate Media entries with each Playlist
for playlist in self.m3u8.playlists:
for media_type in ("audio", "video", "subtitles"):
group_id = getattr(playlist.stream_info, media_type, None)
if group_id:
for media in filter(lambda m: m.group_id == group_id,
self.m3u8.media):
playlist.media.append(media)
self.m3u8.is_master = not not self.m3u8.playlists
return self.m3u8
def uri(self, uri):
if uri and uri.startswith("http"):
return uri
elif self.base_uri and uri:
return urljoin(self.base_uri, uri)
else:
return uri
def load(data, base_uri=None, parser=M3U8Parser):
"""Attempts to parse a M3U8 playlist from a string of data.
If specified, *base_uri* is the base URI that relative URIs will
be joined together with, otherwise relative URIs will be as is.
If specified, *parser* can be a M3U8Parser subclass to be used
to parse the data.
"""
return parser(base_uri).parse(data)
livestreamer-1.12.2/src/livestreamer/stream/hls.py 0000644 0001750 0001750 00000026543 12521217321 023327 0 ustar chrippa chrippa 0000000 0000000 from collections import defaultdict, namedtuple
try:
from Crypto.Cipher import AES
import struct
def num_to_iv(n):
return struct.pack(">8xq", n)
CAN_DECRYPT = True
except ImportError:
CAN_DECRYPT = False
from . import hls_playlist
from .http import HTTPStream
from .segmented import (SegmentedStreamReader,
SegmentedStreamWriter,
SegmentedStreamWorker)
from ..exceptions import StreamError
Sequence = namedtuple("Sequence", "num segment")
class HLSStreamWriter(SegmentedStreamWriter):
def __init__(self, reader, *args, **kwargs):
options = reader.stream.session.options
kwargs["retries"] = options.get("hls-segment-attempts")
kwargs["threads"] = options.get("hls-segment-threads")
kwargs["timeout"] = options.get("hls-segment-timeout")
SegmentedStreamWriter.__init__(self, reader, *args, **kwargs)
self.byterange_offsets = defaultdict(int)
self.key_data = None
self.key_uri = None
def create_decryptor(self, key, sequence):
if key.method != "AES-128":
raise StreamError("Unable to decrypt cipher {0}", key.method)
if not key.uri:
raise StreamError("Missing URI to decryption key")
if self.key_uri != key.uri:
res = self.session.http.get(key.uri, exception=StreamError,
**self.reader.request_params)
self.key_data = res.content
self.key_uri = key.uri
iv = key.iv or num_to_iv(sequence)
# Pad IV if needed
iv = b"\x00" * (16 - len(iv)) + iv
return AES.new(self.key_data, AES.MODE_CBC, iv)
def create_request_params(self, sequence):
request_params = dict(self.reader.request_params)
headers = request_params.pop("headers", {})
if sequence.segment.byterange:
bytes_start = self.byterange_offsets[sequence.segment.uri]
if sequence.segment.byterange.offset is not None:
bytes_start = sequence.segment.byterange.offset
bytes_len = max(sequence.segment.byterange.range - 1, 0)
bytes_end = bytes_start + bytes_len
headers["Range"] = "bytes={0}-{1}".format(bytes_start, bytes_end)
self.byterange_offsets[sequence.segment.uri] = bytes_end + 1
request_params["headers"] = headers
return request_params
def fetch(self, sequence, retries=None):
if self.closed or not retries:
return
try:
request_params = self.create_request_params(sequence)
return self.session.http.get(sequence.segment.uri,
timeout=self.timeout,
exception=StreamError,
**request_params)
except StreamError as err:
self.logger.error("Failed to open segment {0}: {1}", sequence.num, err)
return self.fetch(sequence, retries - 1)
def write(self, sequence, res, chunk_size=8192):
if sequence.segment.key and sequence.segment.key.method != "NONE":
try:
decryptor = self.create_decryptor(sequence.segment.key,
sequence.num)
except StreamError as err:
self.logger.error("Failed to create decryptor: {0}", err)
self.close()
return
# If the input data is not a multiple of 16, cut off any garbage
garbage_len = len(res.content) % 16
if garbage_len:
self.logger.debug("Cutting off {0} bytes of garbage "
"before decrypting", garbage_len)
content = decryptor.decrypt(res.content[:-(garbage_len)])
else:
content = decryptor.decrypt(res.content)
else:
content = res.content
self.reader.buffer.write(content)
self.logger.debug("Download of segment {0} complete", sequence.num)
class HLSStreamWorker(SegmentedStreamWorker):
def __init__(self, *args, **kwargs):
SegmentedStreamWorker.__init__(self, *args, **kwargs)
self.playlist_changed = False
self.playlist_end = None
self.playlist_sequence = -1
self.playlist_sequences = []
self.playlist_reload_time = 15
self.live_edge = self.session.options.get("hls-live-edge")
self.reload_playlist()
def reload_playlist(self):
if self.closed:
return
self.reader.buffer.wait_free()
self.logger.debug("Reloading playlist")
res = self.session.http.get(self.stream.url,
exception=StreamError,
**self.reader.request_params)
try:
playlist = hls_playlist.load(res.text, res.url)
except ValueError as err:
raise StreamError(err)
if playlist.is_master:
raise StreamError("Attempted to play a variant playlist, use "
"'hlsvariant://{0}' instead".format(self.stream.url))
if playlist.iframes_only:
raise StreamError("Streams containing I-frames only is not playable")
media_sequence = playlist.media_sequence or 0
sequences = [Sequence(media_sequence + i, s)
for i, s in enumerate(playlist.segments)]
if sequences:
self.process_sequences(playlist, sequences)
def process_sequences(self, playlist, sequences):
first_sequence, last_sequence = sequences[0], sequences[-1]
if first_sequence.segment.key and first_sequence.segment.key.method != "NONE":
self.logger.debug("Segments in this playlist are encrypted")
if not CAN_DECRYPT:
raise StreamError("Need pyCrypto installed to decrypt this stream")
self.playlist_changed = ([s.num for s in self.playlist_sequences] !=
[s.num for s in sequences])
self.playlist_reload_time = (playlist.target_duration or
last_sequence.segment.duration)
self.playlist_sequences = sequences
if not self.playlist_changed:
self.playlist_reload_time = max(self.playlist_reload_time / 2, 1)
if playlist.is_endlist:
self.playlist_end = last_sequence.num
if self.playlist_sequence < 0:
if self.playlist_end is None:
edge_index = -(min(len(sequences), max(int(self.live_edge), 1)))
edge_sequence = sequences[edge_index]
self.playlist_sequence = edge_sequence.num
else:
self.playlist_sequence = first_sequence.num
def valid_sequence(self, sequence):
return sequence.num >= self.playlist_sequence
def iter_segments(self):
while not self.closed:
for sequence in filter(self.valid_sequence, self.playlist_sequences):
self.logger.debug("Adding segment {0} to queue", sequence.num)
yield sequence
# End of stream
stream_end = self.playlist_end and sequence.num >= self.playlist_end
if self.closed or stream_end:
return
self.playlist_sequence = sequence.num + 1
if self.wait(self.playlist_reload_time):
try:
self.reload_playlist()
except StreamError as err:
self.logger.warning("Failed to reload playlist: {0}", err)
class HLSStreamReader(SegmentedStreamReader):
__worker__ = HLSStreamWorker
__writer__ = HLSStreamWriter
def __init__(self, stream, *args, **kwargs):
SegmentedStreamReader.__init__(self, stream, *args, **kwargs)
self.logger = stream.session.logger.new_module("stream.hls")
self.request_params = dict(stream.args)
self.timeout = stream.session.options.get("hls-timeout")
# These params are reserved for internal use
self.request_params.pop("exception", None)
self.request_params.pop("stream", None)
self.request_params.pop("timeout", None)
self.request_params.pop("url", None)
class HLSStream(HTTPStream):
"""Implementation of the Apple HTTP Live Streaming protocol
*Attributes:*
- :attr:`url` The URL to the HLS playlist.
- :attr:`args` A :class:`dict` containing keyword arguments passed
to :meth:`requests.request`, such as headers and cookies.
.. versionchanged:: 1.7.0
Added *args* attribute.
"""
__shortname__ = "hls"
def __init__(self, session_, url, **args):
HTTPStream.__init__(self, session_, url, **args)
def __repr__(self):
return "".format(self.url)
def __json__(self):
json = HTTPStream.__json__(self)
# Pretty sure HLS is GET only.
del json["method"]
del json["body"]
return json
def open(self):
reader = HLSStreamReader(self)
reader.open()
return reader
@classmethod
def parse_variant_playlist(cls, session_, url, name_key="name",
name_prefix="", check_streams=False,
**request_params):
"""Attempts to parse a variant playlist and return its streams.
:param url: The URL of the variant playlist.
:param name_key: Prefer to use this key as stream name, valid keys are:
name, pixels, bitrate.
:param name_prefix: Add this prefix to the stream names.
:param check_streams: Only allow streams that are accesible.
"""
# Backwards compatibility with "namekey" and "nameprefix" params.
name_key = request_params.pop("namekey", name_key)
name_prefix = request_params.pop("nameprefix", name_prefix)
res = session_.http.get(url, exception=IOError, **request_params)
try:
parser = hls_playlist.load(res.text, base_uri=res.url)
except ValueError as err:
raise IOError("Failed to parse playlist: {0}".format(err))
streams = {}
for playlist in filter(lambda p: not p.is_iframe, parser.playlists):
names = dict(name=None, pixels=None, bitrate=None)
for media in playlist.media:
if media.type == "VIDEO" and media.name:
names["name"] = media.name
if playlist.stream_info.resolution:
width, height = playlist.stream_info.resolution
names["pixels"] = "{0}p".format(height)
if playlist.stream_info.bandwidth:
bw = playlist.stream_info.bandwidth
if bw >= 1000:
names["bitrate"] = "{0}k".format(int(bw / 1000.0))
else:
names["bitrate"] = "{0}k".format(bw / 1000.0)
stream_name = (names.get(name_key) or names.get("name") or
names.get("pixels") or names.get("bitrate"))
if not stream_name or stream_name in streams:
continue
if check_streams:
try:
session_.http.get(playlist.uri, **request_params)
except Exception:
continue
stream = HLSStream(session_, playlist.uri, **request_params)
streams[name_prefix + stream_name] = stream
return streams
livestreamer-1.12.2/src/livestreamer/stream/hds.py 0000644 0001750 0001750 00000053246 12521217321 023317 0 ustar chrippa chrippa 0000000 0000000 from __future__ import division
import base64
import hmac
import re
import os.path
from binascii import unhexlify
from collections import namedtuple
from copy import deepcopy
from hashlib import sha256
from io import BytesIO
from math import ceil
from .flvconcat import FLVTagConcat
from .segmented import (SegmentedStreamReader,
SegmentedStreamWriter,
SegmentedStreamWorker)
from .stream import Stream
from .wrappers import StreamIOIterWrapper
from ..cache import Cache
from ..compat import parse_qsl, urljoin, urlparse, urlunparse, bytes, range
from ..exceptions import StreamError
from ..utils import absolute_url, swfdecompress
from ..packages.flashmedia import F4V, F4VError
from ..packages.flashmedia.box import Box
from ..packages.flashmedia.tag import ScriptData, Tag, TAG_TYPE_SCRIPT
# Akamai HD player verification key
# Use unhexlify() rather than bytes.fromhex() for compatibility with before
# Python 3. However, in Python 3.2 (not 3.3+), unhexlify only accepts a byte
# string.
AKAMAIHD_PV_KEY = unhexlify(
b"BD938D5EE6D9F42016F9C56577B6FDCF415FE4B184932B785AB32BCADC9BB592")
# Some streams hosted by Akamai seem to require a hdcore parameter
# to function properly.
HDCORE_VERSION = "3.1.0"
# Fragment URL format
FRAGMENT_URL = "{url}{identifier}{quality}Seg{segment}-Frag{fragment}"
Fragment = namedtuple("Fragment", "segment fragment duration url")
class HDSStreamWriter(SegmentedStreamWriter):
def __init__(self, reader, *args, **kwargs):
options = reader.stream.session.options
kwargs["retries"] = options.get("hds-segment-attempts")
kwargs["threads"] = options.get("hds-segment-threads")
kwargs["timeout"] = options.get("hds-segment-timeout")
SegmentedStreamWriter.__init__(self, reader, *args, **kwargs)
duration, tags = None, []
if self.stream.metadata:
duration = self.stream.metadata.value.get("duration")
tags = [Tag(TAG_TYPE_SCRIPT, timestamp=0,
data=self.stream.metadata)]
self.concater = FLVTagConcat(tags=tags,
duration=duration,
flatten_timestamps=True)
def fetch(self, fragment, retries=None):
if self.closed or not retries:
return
try:
return self.session.http.get(fragment.url,
stream=True,
timeout=self.timeout,
exception=StreamError,
**self.stream.request_params)
except StreamError as err:
self.logger.error("Failed to open fragment {0}-{1}: {2}",
fragment.segment, fragment.fragment, err)
return self.fetch(fragment, retries - 1)
def write(self, fragment, res, chunk_size=8192):
fd = StreamIOIterWrapper(res.iter_content(chunk_size))
self.convert_fragment(fragment, fd)
def convert_fragment(self, fragment, fd):
mdat = None
try:
f4v = F4V(fd, raw_payload=True)
# Fast forward to mdat box
for box in f4v:
if box.type == "mdat":
mdat = box.payload.data
break
except F4VError as err:
self.logger.error("Failed to parse fragment {0}-{1}: {2}",
fragment.segment, fragment.fragment, err)
return
if not mdat:
self.logger.error("No MDAT box found in fragment {0}-{1}",
fragment.segment, fragment.fragment)
return
try:
for chunk in self.concater.iter_chunks(buf=mdat, skip_header=True):
self.reader.buffer.write(chunk)
if self.closed:
break
else:
self.logger.debug("Download of fragment {0}-{1} complete",
fragment.segment, fragment.fragment)
except IOError as err:
if "Unknown tag type" in str(err):
self.logger.error("Unknown tag type found, this stream is "
"probably encrypted")
self.close()
return
self.logger.error("Error reading fragment {0}-{1}: {2}",
fragment.segment, fragment.fragment, err)
class HDSStreamWorker(SegmentedStreamWorker):
def __init__(self, *args, **kwargs):
SegmentedStreamWorker.__init__(self, *args, **kwargs)
self.bootstrap = self.stream.bootstrap
self.current_segment = -1
self.current_fragment = -1
self.first_fragment = 1
self.last_fragment = -1
self.end_fragment = None
self.bootstrap_minimal_reload_time = 2.0
self.bootstrap_reload_time = self.bootstrap_minimal_reload_time
self.invalid_fragments = set()
self.live_edge = self.session.options.get("hds-live-edge")
self.update_bootstrap()
def update_bootstrap(self):
self.logger.debug("Updating bootstrap")
if isinstance(self.bootstrap, Box):
bootstrap = self.bootstrap
else:
bootstrap = self.fetch_bootstrap(self.bootstrap)
self.live = bootstrap.payload.live
self.profile = bootstrap.payload.profile
self.timestamp = bootstrap.payload.current_media_time
self.identifier = bootstrap.payload.movie_identifier
self.time_scale = bootstrap.payload.time_scale
self.segmentruntable = bootstrap.payload.segment_run_table_entries[0]
self.fragmentruntable = bootstrap.payload.fragment_run_table_entries[0]
self.first_fragment, last_fragment = self.fragment_count()
fragment_duration = self.fragment_duration(last_fragment)
if last_fragment != self.last_fragment:
bootstrap_changed = True
self.last_fragment = last_fragment
else:
bootstrap_changed = False
if self.current_fragment < 0:
if self.live:
current_fragment = last_fragment
# Less likely to hit edge if we don't start with last fragment,
# default buffer is 10 sec.
fragment_buffer = int(ceil(self.live_edge / fragment_duration))
current_fragment = max(self.first_fragment,
current_fragment - (fragment_buffer - 1))
self.logger.debug("Live edge buffer {0} sec is {1} fragments",
self.live_edge, fragment_buffer)
# Make sure we don't have a duration set when it's a
# live stream since it will just confuse players anyway.
self.writer.concater.duration = None
else:
current_fragment = self.first_fragment
self.current_fragment = current_fragment
self.logger.debug("Current timestamp: {0}", self.timestamp / self.time_scale)
self.logger.debug("Current segment: {0}", self.current_segment)
self.logger.debug("Current fragment: {0}", self.current_fragment)
self.logger.debug("First fragment: {0}", self.first_fragment)
self.logger.debug("Last fragment: {0}", self.last_fragment)
self.logger.debug("End fragment: {0}", self.end_fragment)
self.bootstrap_reload_time = fragment_duration
if self.live and not bootstrap_changed:
self.logger.debug("Bootstrap not changed, shortening timer")
self.bootstrap_reload_time /= 2
self.bootstrap_reload_time = max(self.bootstrap_reload_time,
self.bootstrap_minimal_reload_time)
def fetch_bootstrap(self, url):
res = self.session.http.get(url,
exception=StreamError,
**self.stream.request_params)
return Box.deserialize(BytesIO(res.content))
def fragment_url(self, segment, fragment):
url = absolute_url(self.stream.baseurl, self.stream.url)
return FRAGMENT_URL.format(url=url,
segment=segment,
fragment=fragment,
identifier="",
quality="")
def fragment_count(self):
table = self.fragmentruntable.payload.fragment_run_entry_table
first_fragment, end_fragment = None, None
for i, fragmentrun in enumerate(table):
if fragmentrun.discontinuity_indicator is not None:
if fragmentrun.discontinuity_indicator == 0:
break
elif fragmentrun.discontinuity_indicator > 0:
continue
if first_fragment is None:
first_fragment = fragmentrun.first_fragment
end_fragment = fragmentrun.first_fragment
fragment_duration = (fragmentrun.first_fragment_timestamp +
fragmentrun.fragment_duration)
if self.timestamp > fragment_duration:
offset = ((self.timestamp - fragment_duration) /
fragmentrun.fragment_duration)
end_fragment += int(offset)
if first_fragment is None:
first_fragment = 1
if end_fragment is None:
end_fragment = 1
return first_fragment, end_fragment
def fragment_duration(self, fragment):
fragment_duration = 0
table = self.fragmentruntable.payload.fragment_run_entry_table
time_scale = self.fragmentruntable.payload.time_scale
for i, fragmentrun in enumerate(table):
if fragmentrun.discontinuity_indicator is not None:
self.invalid_fragments.add(fragmentrun.first_fragment)
# Check for the last fragment of the stream
if fragmentrun.discontinuity_indicator == 0:
if i > 0:
prev = table[i-1]
self.end_fragment = prev.first_fragment
break
elif fragmentrun.discontinuity_indicator > 0:
continue
if fragment >= fragmentrun.first_fragment:
fragment_duration = fragmentrun.fragment_duration / time_scale
return fragment_duration
def segment_from_fragment(self, fragment):
table = self.segmentruntable.payload.segment_run_entry_table
for segment, start, end in self.iter_segment_table(table):
if fragment >= (start + 1) and fragment <= (end + 1):
break
else:
segment = 1
return segment
def iter_segment_table(self, table):
# If the first segment in the table starts at the beginning we
# can go from there, otherwise we start from the end and use the
# total fragment count to figure out where the last segment ends.
if table[0].first_segment == 1:
prev_frag = self.first_fragment - 1
for segmentrun in table:
start = prev_frag + 1
end = prev_frag + segmentrun.fragments_per_segment
yield segmentrun.first_segment, start, end
prev_frag = end
else:
prev_frag = self.last_fragment + 1
for segmentrun in reversed(table):
start = prev_frag - segmentrun.fragments_per_segment
end = prev_frag - 1
yield segmentrun.first_segment, start, end
prev_frag = start
def valid_fragment(self, fragment):
return fragment not in self.invalid_fragments
def iter_segments(self):
while not self.closed:
fragments = range(self.current_fragment, self.last_fragment + 1)
fragments = filter(self.valid_fragment, fragments)
for fragment in fragments:
self.current_fragment = fragment + 1
self.current_segment = self.segment_from_fragment(fragment)
fragment_duration = int(self.fragment_duration(fragment) * 1000)
fragment_url = self.fragment_url(self.current_segment, fragment)
fragment = Fragment(self.current_segment, fragment,
fragment_duration, fragment_url)
self.logger.debug("Adding fragment {0}-{1} to queue",
fragment.segment, fragment.fragment)
yield fragment
# End of stream
stream_end = self.end_fragment and fragment.fragment >= self.end_fragment
if self.closed or stream_end:
return
if self.wait(self.bootstrap_reload_time):
try:
self.update_bootstrap()
except StreamError as err:
self.logger.warning("Failed to update bootstrap: {0}", err)
class HDSStreamReader(SegmentedStreamReader):
__worker__ = HDSStreamWorker
__writer__ = HDSStreamWriter
def __init__(self, stream, *args, **kwargs):
SegmentedStreamReader.__init__(self, stream, *args, **kwargs)
self.logger = stream.session.logger.new_module("stream.hds")
class HDSStream(Stream):
"""
Implements the Adobe HTTP Dynamic Streaming protocol
*Attributes:*
- :attr:`baseurl` Base URL
- :attr:`url` Base path of the stream, joined with the base URL when
fetching fragments
- :attr:`bootstrap` Either a URL pointing to the bootstrap or a
bootstrap :class:`Box` object used for initial information about
the stream
- :attr:`metadata` Either `None` or a :class:`ScriptData` object
that contains metadata about the stream, such as height, width and
bitrate
"""
__shortname__ = "hds"
def __init__(self, session, baseurl, url, bootstrap, metadata=None,
timeout=60, **request_params):
Stream.__init__(self, session)
self.baseurl = baseurl
self.url = url
self.bootstrap = bootstrap
self.metadata = metadata
self.timeout = timeout
# Deep copy request params to make it mutable
self.request_params = deepcopy(request_params)
parsed = urlparse(self.url)
if parsed.query:
params = parse_qsl(parsed.query)
if params:
if not self.request_params.get("params"):
self.request_params["params"] = {}
self.request_params["params"].update(params)
self.url = urlunparse(
(parsed.scheme, parsed.netloc, parsed.path, None, None, None)
)
def __repr__(self):
return ("").format(self.baseurl,
self.url,
self.bootstrap,
self.metadata,
self.timeout)
def __json__(self):
if isinstance(self.bootstrap, Box):
bootstrap = base64.b64encode(self.bootstrap.serialize())
else:
bootstrap = self.bootstrap
if isinstance(self.metadata, ScriptData):
metadata = self.metadata.__dict__
else:
metadata = self.metadata
return dict(type=HDSStream.shortname(), baseurl=self.baseurl,
url=self.url, bootstrap=bootstrap, metadata=metadata)
def open(self):
reader = HDSStreamReader(self)
reader.open()
return reader
@classmethod
def parse_manifest(cls, session, url, timeout=60, pvswf=None,
**request_params):
"""Parses a HDS manifest and returns its substreams.
:param url: The URL to the manifest.
:param timeout: How long to wait for data to be returned from
from the stream before raising an error.
:param pvswf: URL of player SWF for Akamai HD player verification.
"""
if not request_params:
request_params = {}
request_params["headers"] = request_params.get("headers", {})
request_params["params"] = request_params.get("params", {})
# These params are reserved for internal use
request_params.pop("exception", None)
request_params.pop("stream", None)
request_params.pop("timeout", None)
request_params.pop("url", None)
if "akamaihd" in url:
request_params["params"]["hdcore"] = HDCORE_VERSION
res = session.http.get(url, exception=IOError, **request_params)
manifest = session.http.xml(res, "manifest XML", ignore_ns=True,
exception=IOError)
parsed = urlparse(url)
baseurl = manifest.findtext("baseURL")
baseheight = manifest.findtext("height")
bootstraps = {}
streams = {}
if not baseurl:
baseurl = urljoin(url, os.path.dirname(parsed.path))
if not baseurl.endswith("/"):
baseurl += "/"
for bootstrap in manifest.findall("bootstrapInfo"):
name = bootstrap.attrib.get("id") or "_global"
url = bootstrap.attrib.get("url")
if url:
box = absolute_url(baseurl, url)
else:
data = base64.b64decode(bytes(bootstrap.text, "utf8"))
box = Box.deserialize(BytesIO(data))
bootstraps[name] = box
pvtoken = manifest.findtext("pv-2.0")
if pvtoken:
if not pvswf:
raise IOError("This manifest requires the 'pvswf' parameter "
"to verify the SWF")
params = cls._pv_params(session, pvswf, pvtoken, **request_params)
request_params["params"].update(params)
for media in manifest.findall("media"):
url = media.attrib.get("url")
bootstrapid = media.attrib.get("bootstrapInfoId", "_global")
href = media.attrib.get("href")
if url and bootstrapid:
bootstrap = bootstraps.get(bootstrapid)
if not bootstrap:
continue
bitrate = media.attrib.get("bitrate")
streamid = media.attrib.get("streamId")
height = media.attrib.get("height")
if height:
quality = height + "p"
elif bitrate:
quality = bitrate + "k"
elif streamid:
quality = streamid
elif baseheight:
quality = baseheight + "p"
else:
quality = "live"
metadata = media.findtext("metadata")
if metadata:
metadata = base64.b64decode(bytes(metadata, "utf8"))
metadata = ScriptData.deserialize(BytesIO(metadata))
else:
metadata = None
stream = HDSStream(session, baseurl, url, bootstrap,
metadata=metadata, timeout=timeout,
**request_params)
streams[quality] = stream
elif href:
url = absolute_url(baseurl, href)
child_streams = cls.parse_manifest(session, url,
timeout=timeout,
**request_params)
for name, stream in child_streams.items():
# Override stream name if bitrate is available in parent
# manifest but not the child one.
bitrate = media.attrib.get("bitrate")
if bitrate and not re.match("^(\d+)k$", name):
name = bitrate + "k"
streams[name] = stream
return streams
@classmethod
def _pv_params(cls, session, pvswf, pv, **request_params):
"""Returns any parameters needed for Akamai HD player verification.
Algorithm originally documented by KSV, source:
http://stream-recorder.com/forum/showpost.php?p=43761&postcount=13
"""
try:
data, hdntl = pv.split(";")
except ValueError:
data = pv
hdntl = ""
cache = Cache(filename="stream.json")
key = "akamaihd-player:" + pvswf
cached = cache.get(key)
request_params = deepcopy(request_params)
headers = request_params.pop("headers", {})
if cached:
headers["If-Modified-Since"] = cached["modified"]
swf = session.http.get(pvswf, headers=headers, **request_params)
if cached and swf.status_code == 304: # Server says not modified
hash = cached["hash"]
else:
# Calculate SHA-256 hash of the uncompressed SWF file, base-64
# encoded
hash = sha256()
hash.update(swfdecompress(swf.content))
hash = base64.b64encode(hash.digest()).decode("ascii")
modified = swf.headers.get("Last-Modified", "")
# Only save in cache if a valid date is given
if len(modified) < 40:
cache.set(key, dict(hash=hash, modified=modified))
msg = "st=0~exp=9999999999~acl=*~data={0}!{1}".format(data, hash)
auth = hmac.new(AKAMAIHD_PV_KEY, msg.encode("ascii"), sha256)
pvtoken = "{0}~hmac={1}".format(msg, auth.hexdigest())
# The "hdntl" parameter can be accepted as a cookie or passed in the
# query string, but the "pvtoken" parameter can only be in the query
# string
params = [("pvtoken", pvtoken)]
params.extend(parse_qsl(hdntl, keep_blank_values=True))
return params
livestreamer-1.12.2/src/livestreamer/stream/flvconcat.py 0000644 0001750 0001750 00000025035 12521217321 024513 0 ustar chrippa chrippa 0000000 0000000 from __future__ import division
from collections import namedtuple
from io import IOBase
from itertools import chain, islice
from threading import Thread
from ..buffers import RingBuffer
from ..packages.flashmedia import FLVError
from ..packages.flashmedia.tag import (AudioData, AACAudioData, VideoData,
AVCVideoData, VideoCommandFrame,
Header, ScriptData, Tag)
from ..packages.flashmedia.tag import (AAC_PACKET_TYPE_SEQUENCE_HEADER,
AVC_PACKET_TYPE_SEQUENCE_HEADER,
AUDIO_CODEC_ID_AAC,
VIDEO_CODEC_ID_AVC,
TAG_TYPE_AUDIO,
TAG_TYPE_VIDEO)
__all__ = ["extract_flv_header_tags", "FLVTagConcat", "FLVTagConcatIO"]
FLVHeaderTags = namedtuple("FLVHeaderTags", "metadata aac vc")
def iter_flv_tags(fd=None, buf=None, strict=False, skip_header=False):
if not (fd or buf):
return
offset = 0
if not skip_header:
if fd:
Header.deserialize(fd)
elif buf:
header, offset = Header.deserialize_from(buf, offset)
while fd or buf and offset < len(buf):
try:
if fd:
tag = Tag.deserialize(fd, strict=strict)
elif buf:
tag, offset = Tag.deserialize_from(buf, offset, strict=strict)
except (IOError, FLVError) as err:
if "Insufficient tag header" in str(err):
break
raise IOError(err)
yield tag
def extract_flv_header_tags(stream):
fd = stream.open()
metadata = aac_header = avc_header = None
for tag_index, tag in enumerate(iter_flv_tags(fd)):
if isinstance(tag.data, ScriptData) and tag.data.name == "onMetaData":
metadata = tag
elif (isinstance(tag.data, VideoData) and
isinstance(tag.data.data, AVCVideoData)):
if tag.data.data.type == AVC_PACKET_TYPE_SEQUENCE_HEADER:
avc_header = tag
elif (isinstance(tag.data, AudioData) and
isinstance(tag.data.data, AACAudioData)):
if tag.data.data.type == AAC_PACKET_TYPE_SEQUENCE_HEADER:
aac_header = tag
if aac_header and avc_header and metadata:
break
# Give up after 10 tags
if tag_index == 9:
break
return FLVHeaderTags(metadata, aac_header, avc_header)
class FLVTagConcat(object):
def __init__(self, duration=None, tags=[], has_video=True, has_audio=True,
flatten_timestamps=False, sync_headers=False):
self.duration = duration
self.flatten_timestamps = flatten_timestamps
self.has_audio = has_audio
self.has_video = has_video
self.sync_headers = sync_headers
self.tags = tags
if not (has_audio and has_video):
self.sync_headers = False
self.audio_header_written = False
self.flv_header_written = False
self.video_header_written = False
self.timestamps_add = {}
self.timestamps_orig = {}
self.timestamps_sub = {}
@property
def headers_written(self):
return self.audio_header_written and self.video_header_written
def verify_tag(self, tag):
if tag.filter:
raise IOError("Tag has filter flag set, probably encrypted")
# Only AAC and AVC has detectable headers
if isinstance(tag.data, AudioData) and tag.data.codec != AUDIO_CODEC_ID_AAC:
self.audio_header_written = True
if isinstance(tag.data, VideoData) and tag.data.codec != VIDEO_CODEC_ID_AVC:
self.video_header_written = True
# Make sure there is no timestamp gap between audio and video when syncing
if self.sync_headers and self.timestamps_sub and not self.headers_written:
self.timestamps_sub = {}
if isinstance(tag.data, AudioData):
if isinstance(tag.data.data, AACAudioData):
if tag.data.data.type == AAC_PACKET_TYPE_SEQUENCE_HEADER:
if self.audio_header_written:
return
self.audio_header_written = True
else:
if self.sync_headers and not self.headers_written:
return
if not self.audio_header_written:
return
else:
if self.sync_headers and not self.headers_written:
return
elif isinstance(tag.data, VideoData):
if isinstance(tag.data.data, AVCVideoData):
if tag.data.data.type == AVC_PACKET_TYPE_SEQUENCE_HEADER:
if self.video_header_written:
return
self.video_header_written = True
else:
if self.sync_headers and not self.headers_written:
return
if not self.video_header_written:
return
elif isinstance(tag.data.data, VideoCommandFrame):
return
else:
if self.sync_headers and not self.headers_written:
return
elif isinstance(tag.data, ScriptData):
if tag.data.name == "onMetaData":
if self.duration:
tag.data.value["duration"] = self.duration
elif "duration" in tag.data.value:
del tag.data.value["duration"]
else:
return False
return True
def adjust_tag_gap(self, tag):
timestamp_gap = tag.timestamp - self.timestamps_orig.get(tag.type, 0)
timestamp_sub = self.timestamps_sub.get(tag.type)
if timestamp_gap > 1000 and timestamp_sub is not None:
self.timestamps_sub[tag.type] += timestamp_gap
self.timestamps_orig[tag.type] = tag.timestamp
def adjust_tag_timestamp(self, tag):
timestamp_offset_sub = self.timestamps_sub.get(tag.type)
if timestamp_offset_sub is None and tag not in self.tags:
self.timestamps_sub[tag.type] = tag.timestamp
timestamp_offset_sub = self.timestamps_sub.get(tag.type)
timestamp_offset_add = self.timestamps_add.get(tag.type)
if timestamp_offset_add:
tag.timestamp = max(0, tag.timestamp + timestamp_offset_add)
elif timestamp_offset_sub:
tag.timestamp = max(0, tag.timestamp - timestamp_offset_sub)
def analyze_tags(self, tag_iterator):
tags = list(islice(tag_iterator, 10))
audio_tags = len(list(filter(lambda t: t.type == TAG_TYPE_AUDIO, tags)))
video_tags = len(list(filter(lambda t: t.type == TAG_TYPE_VIDEO, tags)))
self.has_audio = audio_tags > 0
self.has_video = video_tags > 0
if not (self.has_audio and self.has_video):
self.sync_headers = False
return tags
def iter_tags(self, fd=None, buf=None, skip_header=None):
if skip_header is None:
skip_header = not not self.tags
tags_iterator = filter(None, self.tags)
flv_iterator = iter_flv_tags(fd=fd, buf=buf, skip_header=skip_header)
for tag in chain(tags_iterator, flv_iterator):
yield tag
def iter_chunks(self, fd=None, buf=None, skip_header=None):
"""Reads FLV tags from fd or buf and returns them with adjusted
timestamps."""
timestamps = dict(self.timestamps_add)
tag_iterator = self.iter_tags(fd=fd, buf=buf, skip_header=skip_header)
if not self.flv_header_written:
analyzed_tags = self.analyze_tags(tag_iterator)
else:
analyzed_tags = []
for tag in chain(analyzed_tags, tag_iterator):
if not self.flv_header_written:
flv_header = Header(has_video=self.has_video,
has_audio=self.has_audio)
yield flv_header.serialize()
self.flv_header_written = True
if self.verify_tag(tag):
self.adjust_tag_gap(tag)
self.adjust_tag_timestamp(tag)
if self.duration:
norm_timestamp = tag.timestamp / 1000
if norm_timestamp > self.duration:
break
yield tag.serialize()
timestamps[tag.type] = tag.timestamp
if not self.flatten_timestamps:
self.timestamps_add = timestamps
self.tags = []
class FLVTagConcatWorker(Thread):
def __init__(self, iterator, stream):
self.error = None
self.stream = stream
self.stream_iterator = iterator
self.concater = FLVTagConcat(stream.duration, stream.tags,
**stream.concater_params)
Thread.__init__(self)
self.daemon = True
def run(self):
for fd in self.stream_iterator:
try:
chunks = self.concater.iter_chunks(
fd, skip_header=self.stream.skip_header
)
for chunk in chunks:
self.stream.buffer.write(chunk)
if not self.running:
return
except IOError as err:
self.error = err
break
self.stop()
def stop(self):
self.running = False
self.stream.buffer.close()
def start(self):
self.running = True
return Thread.start(self)
class FLVTagConcatIO(IOBase):
__worker__ = FLVTagConcatWorker
__log_name__ = "stream.flv_concat"
def __init__(self, session, duration=None, tags=[], skip_header=None,
timeout=30, **concater_params):
self.session = session
self.timeout = timeout
self.logger = session.logger.new_module(self.__log_name__)
self.concater_params = concater_params
self.duration = duration
self.skip_header = skip_header
self.tags = tags
def open(self, iterator):
self.buffer = RingBuffer(self.session.get_option("ringbuffer-size"))
self.worker = self.__worker__(iterator, self)
self.worker.start()
def close(self):
self.worker.stop()
if self.worker.is_alive():
self.worker.join()
def read(self, size=-1):
if not self.buffer:
return b""
if self.worker.error:
raise self.worker.error
return self.buffer.read(size, block=self.worker.is_alive(),
timeout=self.timeout)
livestreamer-1.12.2/src/livestreamer/stream/akamaihd.py 0000644 0001750 0001750 00000017310 12521217321 024270 0 ustar chrippa chrippa 0000000 0000000 import base64
import io
import hashlib
import hmac
import random
from .stream import Stream
from .wrappers import StreamIOThreadWrapper, StreamIOIterWrapper
from ..buffers import Buffer
from ..compat import str, bytes, urlparse
from ..exceptions import StreamError
from ..utils import swfdecompress
from ..packages.flashmedia import FLV, FLVError
from ..packages.flashmedia.tag import ScriptData
class TokenGenerator(object):
def __init__(self, stream):
self.stream = stream
def generate(self):
raise NotImplementedError
class Auth3TokenGenerator(TokenGenerator):
def generate(self):
if not self.stream.swf:
raise StreamError("A SWF URL is required to create session token")
res = self.stream.session.http.get(self.stream.swf,
exception=StreamError)
data = swfdecompress(res.content)
md5 = hashlib.md5()
md5.update(data)
data = bytes(self.stream.sessionid, "ascii") + md5.digest()
sig = hmac.new(b"foo", data, hashlib.sha1)
b64 = base64.encodestring(sig.digest())
token = str(b64, "ascii").replace("\n", "")
return token
def cache_bust_string(length):
rval = ""
for i in range(length):
rval += chr(65 + int(round(random.random() * 25)))
return rval
class AkamaiHDStreamIO(io.IOBase):
Version = "2.5.8"
FlashVersion = "LNX 11,1,102,63"
StreamURLFormat = "{host}/{streamname}"
ControlURLFormat = "{host}/control/{streamname}"
ControlData = b":)"
TokenGenerators = {
"c11e59dea648d56e864fc07a19f717b9": Auth3TokenGenerator
}
StatusComplete = 3
StatusError = 4
Errors = {
1: "Stream not found",
2: "Track not found",
3: "Seek out of bounds",
4: "Authentication failed",
5: "DVR disabled",
6: "Invalid bitrate test"
}
def __init__(self, session, url, swf=None, seek=None):
parsed = urlparse(url)
self.session = session
self.logger = self.session.logger.new_module("stream.akamaihd")
self.host = ("{scheme}://{netloc}").format(scheme=parsed.scheme, netloc=parsed.netloc)
self.streamname = parsed.path[1:]
self.swf = swf
self.seek = seek
def open(self):
self.guid = cache_bust_string(12)
self.islive = None
self.sessionid = None
self.flv = None
self.buffer = Buffer()
self.completed_handshake = False
url = self.StreamURLFormat.format(host=self.host, streamname=self.streamname)
params = self._create_params(seek=self.seek)
self.logger.debug("Opening host={host} streamname={streamname}",
host=self.host, streamname=self.streamname)
try:
res = self.session.http.get(url, stream=True, params=params)
self.fd = StreamIOIterWrapper(res.iter_content(8192))
except Exception as err:
raise StreamError(str(err))
self.handshake(self.fd)
return self
def handshake(self, fd):
try:
self.flv = FLV(fd)
except FLVError as err:
raise StreamError(str(err))
self.buffer.write(self.flv.header.serialize())
self.logger.debug("Attempting to handshake")
for i, tag in enumerate(self.flv):
if i == 10:
raise StreamError("No OnEdge metadata in FLV after 10 tags, probably not a AkamaiHD stream")
self.process_tag(tag, exception=StreamError)
if self.completed_handshake:
self.logger.debug("Handshake successful")
break
def process_tag(self, tag, exception=IOError):
if isinstance(tag.data, ScriptData) and tag.data.name == "onEdge":
self._on_edge(tag.data.value, exception=exception)
self.buffer.write(tag.serialize())
def send_token(self, token):
headers = { "x-Akamai-Streaming-SessionToken": token }
self.logger.debug("Sending new session token")
self.send_control("sendingNewToken", headers=headers,
swf=self.swf)
def send_control(self, cmd, headers=None, **params):
if not headers:
headers = {}
url = self.ControlURLFormat.format(host=self.host,
streamname=self.streamname)
headers["x-Akamai-Streaming-SessionID"] = self.sessionid
params = self._create_params(cmd=cmd, **params)
return self.session.http.post(url,
headers=headers,
params=params,
data=self.ControlData,
exception=StreamError)
def read(self, size=-1):
if not (self.flv and self.fd):
return b""
if self.buffer.length:
return self.buffer.read(size)
else:
return self.fd.read(size)
def _create_params(self, **extra):
params = dict(v=self.Version, fp=self.FlashVersion,
r=cache_bust_string(5), g=self.guid)
params.update(extra)
return params
def _generate_session_token(self, data64):
swfdata = base64.decodestring(bytes(data64, "ascii"))
md5 = hashlib.md5()
md5.update(swfdata)
hash = md5.hexdigest()
if hash in self.TokenGenerators:
generator = self.TokenGenerators[hash](self)
return generator.generate()
else:
raise StreamError(("No token generator available for hash '{0}'").format(hash))
def _on_edge(self, data, exception=IOError):
def updateattr(attr, key):
if key in data:
setattr(self, attr, data[key])
self.logger.debug("onEdge data")
for key, val in data.items():
if isinstance(val, str):
val = val[:50]
self.logger.debug(" {key}={val}",
key=key, val=val)
updateattr("islive", "isLive")
updateattr("sessionid", "session")
updateattr("status", "status")
updateattr("streamname", "streamName")
if self.status == self.StatusComplete:
self.flv = None
elif self.status == self.StatusError:
errornum = data["errorNumber"]
if errornum in self.Errors:
msg = self.Errors[errornum]
else:
msg = "Unknown error"
raise exception("onEdge error: " + msg)
if not self.completed_handshake:
if "data64" in data:
sessiontoken = self._generate_session_token(data["data64"])
else:
sessiontoken = None
self.send_token(sessiontoken)
self.completed_handshake = True
class AkamaiHDStream(Stream):
"""
Implements the AkamaiHD Adaptive Streaming protocol
*Attributes:*
- :attr:`url` URL to the stream
- :attr:`swf` URL to a SWF used by the handshake protocol
- :attr:`seek` Position to seek to when opening the stream
"""
__shortname__ = "akamaihd"
def __init__(self, session, url, swf=None, seek=None):
Stream.__init__(self, session)
self.seek = seek
self.swf = swf
self.url = url
def __repr__(self):
return ("".format(self.url, self.swf))
def __json__(self):
return dict(type=AkamaiHDStream.shortname(),
url=self.url, swf=self.swf)
def open(self):
stream = AkamaiHDStreamIO(self.session, self.url,
self.swf, self.seek)
return StreamIOThreadWrapper(self.session, stream.open())
__all__ = ["AkamaiHDStream"]
livestreamer-1.12.2/src/livestreamer/stream/__init__.py 0000644 0001750 0001750 00000000661 12521217321 024271 0 ustar chrippa chrippa 0000000 0000000 from ..exceptions import StreamError
from .stream import Stream
from .akamaihd import AkamaiHDStream
from .hds import HDSStream
from .hls import HLSStream
from .http import HTTPStream
from .rtmpdump import RTMPStream
from .streamprocess import StreamProcess
from .wrappers import StreamIOWrapper, StreamIOIterWrapper, StreamIOThreadWrapper
from .flvconcat import extract_flv_header_tags
from .playlist import Playlist, FLVPlaylist
livestreamer-1.12.2/src/livestreamer/plugins/ 0000755 0001750 0001750 00000000000 12521217500 022342 5 ustar chrippa chrippa 0000000 0000000 livestreamer-1.12.2/src/livestreamer/plugins/zdf_mediathek.py 0000644 0001750 0001750 00000005327 12521217321 025522 0 ustar chrippa chrippa 0000000 0000000 import re
from livestreamer.plugin import Plugin
from livestreamer.plugin.api import http, validate
from livestreamer.stream import HDSStream, HLSStream, RTMPStream
API_URL = "http://www.zdf.de/ZDFmediathek/xmlservice/web/beitragsDetails"
QUALITY_WEIGHTS = {
"hd": 720,
"veryhigh": 480,
"high": 240,
"med": 176,
"low": 112
}
STREAMING_TYPES = {
"h264_aac_f4f_http_f4m_http": (
"HDS", HDSStream.parse_manifest
),
"h264_aac_ts_http_m3u8_http": (
"HLS", HLSStream.parse_variant_playlist
)
}
_url_re = re.compile("""
http(s)?://(\w+\.)?zdf.de/zdfmediathek(\#)?/.+
/(live|video)
/(?P\d+)
""", re.VERBOSE | re.IGNORECASE)
_schema = validate.Schema(
validate.xml_findall("video/formitaeten/formitaet"),
[
validate.union({
"type": validate.get("basetype"),
"quality": validate.xml_findtext("quality"),
"url": validate.all(
validate.xml_findtext("url"),
validate.url()
)
})
]
)
class zdf_mediathek(Plugin):
@classmethod
def can_handle_url(cls, url):
return _url_re.match(url)
@classmethod
def stream_weight(cls, key):
weight = QUALITY_WEIGHTS.get(key)
if weight:
return weight, "zdf_mediathek"
return Plugin.stream_weight(key)
def _create_rtmp_stream(self, url):
return RTMPStream(self.session, {
"rtmp": self._get_meta_url(url),
"pageUrl": self.url,
})
def _get_meta_url(self, url):
res = http.get(url, exception=IOError)
root = http.xml(res, exception=IOError)
return root.findtext("default-stream-url")
def _get_streams(self):
match = _url_re.match(self.url)
video_id = match.group("video_id")
res = http.get(API_URL, params=dict(ak="web", id=video_id))
fmts = http.xml(res, schema=_schema)
streams = {}
for fmt in fmts:
if fmt["type"] in STREAMING_TYPES:
name, parser = STREAMING_TYPES[fmt["type"]]
try:
streams.update(parser(self.session, fmt["url"]))
except IOError as err:
self.logger.error("Failed to extract {0} streams: {1}",
name, err)
elif fmt["type"] == "h264_aac_mp4_rtmp_zdfmeta_http":
name = fmt["quality"]
try:
streams[name] = self._create_rtmp_stream(fmt["url"])
except IOError as err:
self.logger.error("Failed to extract RTMP stream '{0}': {1}",
name, err)
return streams
__plugin__ = zdf_mediathek
livestreamer-1.12.2/src/livestreamer/plugins/youtube.py 0000644 0001750 0001750 00000014174 12521217321 024420 0 ustar chrippa chrippa 0000000 0000000 import re
from livestreamer.plugin import Plugin, PluginError
from livestreamer.plugin.api import http, validate
from livestreamer.plugin.api.utils import parse_query
from livestreamer.stream import HTTPStream, HLSStream
API_KEY = "AIzaSyBDBi-4roGzWJN4du9TuDMLd_jVTcVkKz4"
API_BASE = "https://www.googleapis.com/youtube/v3"
API_SEARCH_URL = API_BASE + "/search"
API_VIDEO_INFO = "http://youtube.com/get_video_info"
HLS_HEADERS = {
"User-Agent": "Mozilla/5.0"
}
def parse_stream_map(stream_map):
if not stream_map:
return []
return [parse_query(s) for s in stream_map.split(",")]
def parse_fmt_list(formatsmap):
formats = {}
if not formatsmap:
return formats
for format in formatsmap.split(","):
s = format.split("/")
(w, h) = s[1].split("x")
formats[int(s[0])] = "{0}p".format(h)
return formats
_config_schema = validate.Schema(
{
validate.optional("fmt_list"): validate.all(
validate.text,
validate.transform(parse_fmt_list)
),
validate.optional("url_encoded_fmt_stream_map"): validate.all(
validate.text,
validate.transform(parse_stream_map),
[{
"itag": validate.all(
validate.text,
validate.transform(int)
),
"quality": validate.text,
"url": validate.url(scheme="http"),
validate.optional("s"): validate.text,
validate.optional("stereo3d"): validate.all(
validate.text,
validate.transform(int),
validate.transform(bool)
),
}]
),
validate.optional("adaptive_fmts"): validate.all(
validate.text,
validate.transform(parse_stream_map),
[{
validate.optional("s"): validate.text,
"type": validate.all(
validate.text,
validate.transform(lambda t: t.split(";")[0].split("/")),
[validate.text, validate.text]
),
"url": validate.all(
validate.url(scheme="http")
)
}]
),
validate.optional("hlsvp"): validate.text,
validate.optional("live_playback"): validate.transform(bool),
"status": validate.text
}
)
_search_schema = validate.Schema(
{
"items": [{
"id": {
"videoId": validate.text
}
}]
},
validate.get("items")
)
_channelid_re = re.compile('meta itemprop="channelId" content="([^"]+)"')
_url_re = re.compile("""
http(s)?://(\w+\.)?youtube.com
(?:
(?:
/(watch.+v=|embed/|v/)
(?P[0-9A-z_-]{11})
)
|
(?:
/(user|channel)/(?P[^/?]+)
)
)
""", re.VERBOSE)
class YouTube(Plugin):
@classmethod
def can_handle_url(self, url):
return _url_re.match(url)
@classmethod
def stream_weight(cls, stream):
match = re.match("(\w+)_3d", stream)
if match:
weight, group = Plugin.stream_weight(match.group(1))
weight -= 1
group = "youtube_3d"
else:
weight, group = Plugin.stream_weight(stream)
return weight, group
def _find_channel_video(self):
res = http.get(self.url)
match = _channelid_re.search(res.text)
if not match:
return
channel_id = match.group(1)
query = {
"channelId": channel_id,
"type": "video",
"eventType": "live",
"part": "id",
"key": API_KEY
}
res = http.get(API_SEARCH_URL, params=query)
videos = http.json(res, schema=_search_schema)
for video in videos:
video_id = video["id"]["videoId"]
return video_id
def _get_stream_info(self, url):
match = _url_re.match(url)
user = match.group("user")
if user:
video_id = self._find_channel_video()
else:
video_id = match.group("video_id")
if not video_id:
return
params = {
"video_id": video_id,
"el": "player_embedded"
}
res = http.get(API_VIDEO_INFO, params=params)
return parse_query(res.text, name="config", schema=_config_schema)
def _get_streams(self):
info = self._get_stream_info(self.url)
if not info:
return
formats = info.get("fmt_list")
streams = {}
protected = False
for stream_info in info.get("url_encoded_fmt_stream_map", []):
if stream_info.get("s"):
protected = True
continue
stream = HTTPStream(self.session, stream_info["url"])
name = formats.get(stream_info["itag"]) or stream_info["quality"]
if stream_info.get("stereo3d"):
name += "_3d"
streams[name] = stream
# Extract audio streams from the DASH format list
for stream_info in info.get("adaptive_fmts", []):
if stream_info.get("s"):
protected = True
continue
stream_type, stream_format = stream_info["type"]
if stream_type != "audio":
continue
stream = HTTPStream(self.session, stream_info["url"])
name = "audio_{0}".format(stream_format)
streams[name] = stream
hls_playlist = info.get("hlsvp")
if hls_playlist:
try:
hls_streams = HLSStream.parse_variant_playlist(
self.session, hls_playlist, headers=HLS_HEADERS, namekey="pixels"
)
streams.update(hls_streams)
except IOError as err:
self.logger.warning("Failed to extract HLS streams: {0}", err)
if not streams and protected:
raise PluginError("This plugin does not support protected videos, "
"try youtube-dl instead")
return streams
__plugin__ = YouTube
livestreamer-1.12.2/src/livestreamer/plugins/weeb.py 0000644 0001750 0001750 00000006436 12521217321 023650 0 ustar chrippa chrippa 0000000 0000000 import re
from livestreamer.plugin import Plugin, PluginError
from livestreamer.plugin.api import http, validate
from livestreamer.plugin.api.utils import parse_query
from livestreamer.stream import RTMPStream
API_URL = "http://weeb.tv/api/setPlayer"
SWF_URL = "http://static2.weeb.tv/static2/player.swf"
HEADERS = {
"Referer": SWF_URL
}
PARAMS_KEY_MAP = {
"0": "status",
"10": "rtmp",
"11": "playpath",
"13": "block_type",
"14": "block_time",
"16": "reconnect_time",
"20": "multibitrate",
"73": "token"
}
BLOCKED_MSG_FORMAT = (
"You have crossed the free viewing limit. You have been blocked for "
"{0} minutes. Try again in {1} minutes"
)
BLOCK_TYPE_VIEWING_LIMIT = 1
BLOCK_TYPE_NO_SLOTS = 11
_url_re = re.compile("http(s)?://(\w+\.)?weeb.tv/(channel|online)/(?P[^/&?]+)")
_schema = validate.Schema(
dict,
validate.map(lambda k, v: (PARAMS_KEY_MAP.get(k, k), v)),
validate.any(
{
"status": validate.transform(int),
"rtmp": validate.url(scheme="rtmp"),
"playpath": validate.text,
"multibitrate": validate.all(
validate.transform(int),
validate.transform(bool)
),
"block_type": validate.transform(int),
validate.optional("token"): validate.text,
validate.optional("block_time"): validate.text,
validate.optional("reconnect_time"): validate.text,
},
{
"status": validate.transform(int),
},
)
)
class Weeb(Plugin):
@classmethod
def can_handle_url(self, url):
return _url_re.match(url)
def _get_streams(self):
match = _url_re.match(self.url)
channel_name = match.group("channel")
form = {
"cid": channel_name,
"watchTime": 0,
"firstConnect": 1,
"ip": "NaN"
}
res = http.post(API_URL, data=form, headers=HEADERS)
params = parse_query(res.text, schema=_schema)
if params["status"] <= 0:
return
if params["block_type"] != 0:
if params["block_type"] == BLOCK_TYPE_VIEWING_LIMIT:
msg = BLOCKED_MSG_FORMAT.format(
params.get("block_time", "UNKNOWN"),
params.get("reconnect_time", "UNKNOWN")
)
raise PluginError(msg)
elif params["block_type"] == BLOCK_TYPE_NO_SLOTS:
raise PluginError("No free slots available")
else:
raise PluginError("Blocked for unknown reasons")
if "token" not in params:
raise PluginError("Server seems busy, retry again later")
streams = {}
stream_names = ["sd"]
if params["multibitrate"]:
stream_names += ["hd"]
for stream_name in stream_names:
playpath = params["playpath"]
if stream_name == "hd":
playpath += "HI"
stream = RTMPStream(self.session, {
"rtmp": "{0}/{1}".format(params["rtmp"], playpath),
"pageUrl": self.url,
"swfVfy": SWF_URL,
"weeb": params["token"],
"live": True
})
streams[stream_name] = stream
return streams
__plugin__ = Weeb
livestreamer-1.12.2/src/livestreamer/plugins/wattv.py 0000644 0001750 0001750 00000004445 12521217321 024071 0 ustar chrippa chrippa 0000000 0000000 import hashlib
import re
from livestreamer.plugin import Plugin
from livestreamer.plugin.api import http
from livestreamer.stream import HDSStream
# Got the secret from the swf with rev number location
# (tv/wat/player/media/Media.as)
TOKEN_SECRET = '9b673b13fa4682ed14c3cfa5af5310274b514c4133e9b3a81e6e3aba009l2564'
_url_re = re.compile("http(s)?://(\w+\.)?wat.tv/")
_video_id_re = re.compile("href=\"http://m.wat.tv/video/([^\"]+)", re.IGNORECASE)
class WAT(Plugin):
@classmethod
def can_handle_url(cls, url):
return _url_re.match(url)
def _get_streams(self):
res = http.get(self.url)
match = video_id = _video_id_re.search(res.text)
if not match:
return
video_id = match.group(1)
# TODO: Replace with "yield from" when dropping Python 2.
for __ in self._create_streams('web', video_id).items():
yield __
for __ in self._create_streams('webhd', video_id).items():
yield __
def _create_streams(self, type_, video_id):
url = self._generate_security_url(type_, video_id)
res = http.get(url)
return HDSStream.parse_manifest(self.session, res.text, cookies=res.cookies)
def _generate_security_url(self, type_, video_id):
token = self._generate_security_token(type_, video_id)
return ("http://www.wat.tv/get/{type_}/{video_id}?token={token}"
"&domain=www.wat.tv&refererURL=wat.tv&revision=04.00.719%0A&"
"synd=0&helios=1&context=playerWat&pub=1&country=FR"
"&sitepage=WAT%2Ftv%2Ft%2Finedit%2Ftf1%2Fparamount_pictures_"
"france&lieu=wat&playerContext=CONTEXT_WAT&getURL=1"
"&version=LNX%2014,0,0,125").format(**locals())
def _generate_security_token(self, type_, video_id):
# Get timestamp
res = http.get('http://www.wat.tv/servertime')
timestamp = int(res.text.split('|')[0])
timestamp_hex = format(timestamp, 'x').rjust(8, '0')
# Player id
player_prefix = "/{0}/{1}".format(type_, video_id)
# Create the token
data = (TOKEN_SECRET + player_prefix + timestamp_hex).encode('utf8')
token = hashlib.md5(data)
token = "{0}/{1}".format(token.hexdigest(), timestamp_hex)
return token
__plugin__ = WAT
livestreamer-1.12.2/src/livestreamer/plugins/viasat_embed.py 0000644 0001750 0001750 00000001070 12521217321 025336 0 ustar chrippa chrippa 0000000 0000000 import re
from livestreamer.plugin import Plugin
from livestreamer.plugin.api import http
_url_re = re.compile("http(s)?://(www\.)?tv(3|6|8|10)\.se")
_embed_re = re.compile('