Skip to content

Update Stream string representation and refactor to_url + JSON data #4521

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions src/streamlink/plugin/api/http_session.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import ssl
import time
from typing import Any, Callable, List, Pattern, Tuple
from typing import Any, Callable, Dict, List, Pattern, Tuple

import requests.adapters
import urllib3
from requests import Session
from requests import PreparedRequest, Request, Session

from streamlink.exceptions import PluginError
from streamlink.packages.requests_file import FileAdapter
Expand Down Expand Up @@ -96,6 +96,10 @@ def _parse_keyvalue_list(val):
continue


# requests.Request.__init__ keywords, except for "hooks"
_VALID_REQUEST_ARGS = "method", "url", "headers", "files", "data", "params", "auth", "cookies", "json"


class HTTPSession(Session):
def __init__(self):
super().__init__()
Expand Down Expand Up @@ -168,6 +172,18 @@ def resolve_url(self, url):
"""Resolves any redirects and returns the final URL."""
return self.get(url, stream=True).url

@staticmethod
def valid_request_args(**req_keywords) -> Dict:
return {k: v for k, v in req_keywords.items() if k in _VALID_REQUEST_ARGS}

def prepare_new_request(self, **req_keywords) -> PreparedRequest:
valid_args = self.valid_request_args(**req_keywords)
valid_args.setdefault("method", "GET")
request = Request(**valid_args)

# prepare request with the session context, which might add params, headers, cookies, etc.
return self.prepare_request(request)

def request(self, method, url, *args, **kwargs):
acceptable_status = kwargs.pop("acceptable_status", [])
exception = kwargs.pop("exception", PluginError)
Expand Down
2 changes: 1 addition & 1 deletion src/streamlink/plugins/filmon.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ def to_url(self):
url = self.url
expires = self.watch_timeout - time.time()
if expires < 0:
raise TypeError("Stream has expired and cannot be converted to a URL")
raise TypeError("Stream has expired and cannot be translated to a URL")
return url


Expand Down
4 changes: 2 additions & 2 deletions src/streamlink/plugins/twitcasting.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,8 @@ def __init__(self, session, url):
super().__init__(session)
self.url = url

def __repr__(self):
return f"<TwitCastingStream({self.url!r})>"
def to_url(self):
return self.url

def open(self):
reader = TwitCastingReader(self)
Expand Down
5 changes: 1 addition & 4 deletions src/streamlink/plugins/twitch.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@
from typing import List, NamedTuple, Optional
from urllib.parse import urlparse

import requests

from streamlink.exceptions import NoStreamsError, PluginError
from streamlink.plugin import Plugin, PluginArgument, PluginArguments, pluginmatcher
from streamlink.plugin.api import validate
Expand Down Expand Up @@ -190,8 +188,7 @@ def _create_url(self, endpoint, **extra_params):
}
params.update(extra_params)

req = requests.Request("GET", url, params=params)
req = self.session.http.prepare_request(req)
req = self.session.http.prepare_new_request(url=url, params=params)

return req.url

Expand Down
34 changes: 20 additions & 14 deletions src/streamlink/stream/dash.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,9 @@
from typing import Dict, Optional
from urllib.parse import urlparse, urlunparse

import requests

from streamlink import PluginError, StreamError
from streamlink.stream.dash_manifest import MPD, Representation, freeze_timeline, sleep_until, sleeper, utc
from streamlink.stream.ffmpegmux import FFMPEGMuxer
from streamlink.stream.http import normalize_key, valid_args
from streamlink.stream.segmented import SegmentedStreamReader, SegmentedStreamWorker, SegmentedStreamWriter
from streamlink.stream.stream import Stream
from streamlink.utils.l10n import Language
Expand Down Expand Up @@ -173,11 +170,26 @@ def __init__(
self.args = args

def __json__(self):
req = requests.Request(method="GET", url=self.mpd.url, **valid_args(self.args))
req = req.prepare()
json = dict(type=self.shortname())

if self.mpd.url:
args = self.args.copy()
args.update(url=self.mpd.url)
req = self.session.http.prepare_new_request(**args)
json.update(
# the MPD URL has already been prepared by the initial request in `parse_manifest`
url=self.mpd.url,
headers=dict(req.headers),
)

return json

headers = dict(map(normalize_key, req.headers.items()))
return dict(type=type(self).shortname(), url=req.url, headers=headers)
def to_url(self):
if self.mpd.url is None:
return super().to_url()

# the MPD URL has already been prepared by the initial request in `parse_manifest`
return self.mpd.url

@classmethod
def parse_manifest(
Expand All @@ -197,7 +209,7 @@ def parse_manifest(
if url_or_manifest.startswith('<?xml'):
mpd = MPD(parse_xml(url_or_manifest, ignore_ns=True))
else:
res = session.http.get(url_or_manifest, **args)
res = session.http.get(url_or_manifest, **session.http.valid_request_args(**args))
url = res.url

urlp = list(urlparse(url))
Expand Down Expand Up @@ -300,9 +312,3 @@ def open(self):
return video
elif self.audio_representation:
return audio

def to_url(self):
return self.mpd.url

def to_manifest_url(self):
return self.mpd.url
14 changes: 14 additions & 0 deletions src/streamlink/stream/file.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,19 @@ def __init__(self, session, path=None, fileobj=None):
if not self.path and not self.fileobj:
raise ValueError("path or fileobj must be set")

def __json__(self):
json = super().__json__()

if self.path:
json["path"] = self.path

return json

def to_url(self):
if self.path is None:
return super().to_url()

return self.path

def open(self):
return self.fileobj or open(self.path)
16 changes: 11 additions & 5 deletions src/streamlink/stream/hls.py
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,9 @@ def __init__(
self.url_master = url_master

def to_manifest_url(self):
if self.url_master is None:
return super().to_manifest_url()

return self.url_master


Expand Down Expand Up @@ -527,14 +530,11 @@ def __init__(
self.start_offset = start_offset
self.duration = duration

def __repr__(self):
return f"<HLSStream({self.url!r}, {self.url_master!r})>"

def __json__(self):
json = super().__json__()

if self.url_master:
json["master"] = self.url_master
json["master"] = self.to_manifest_url()

# Pretty sure HLS is GET only.
del json["method"]
Expand All @@ -543,7 +543,13 @@ def __json__(self):
return json

def to_manifest_url(self):
return self.url_master
if self.url_master is None:
return super().to_manifest_url()

args = self.args.copy()
args.update(url=self.url_master)

return self.session.http.prepare_new_request(**args).url

def open(self):
reader = self.__reader__(self)
Expand Down
53 changes: 15 additions & 38 deletions src/streamlink/stream/http.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,10 @@
from typing import Dict

import requests

from streamlink.exceptions import StreamError
from streamlink.stream.stream import Stream
from streamlink.stream.wrappers import StreamIOIterWrapper, StreamIOThreadWrapper


VALID_ARGS = ("method", "url", "params", "headers", "cookies", "auth", "data", "json", "files")


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):
return {k: v for k, v in args.items() if k in VALID_ARGS}


class HTTPStream(Stream):
"""
An HTTP stream using the :mod:`requests` library.
Expand Down Expand Up @@ -49,48 +33,41 @@ def __init__(
self.args = dict(url=url, **args)
self.buffered = buffered

def __repr__(self):
return "<HTTPStream({0!r})>".format(self.url)

def __json__(self):
args = self.args.copy()
method = args.pop("method", "GET")
req = requests.Request(method=method, **valid_args(args))
req = self.session.http.prepare_request(req)

headers = dict(map(normalize_key, req.headers.items()))
req = self.session.http.prepare_new_request(**self.args)

return dict(
type=self.shortname(),
method=req.method,
url=req.url,
headers=dict(req.headers),
body=req.body,
)

return dict(type=type(self).shortname(), url=req.url,
method=req.method, headers=headers,
body=req.body)
def to_url(self):
return self.url

@property
def url(self) -> str:
"""
The URL to the stream, prepared by :mod:`requests` with parameters read from :attr:`args`.
"""

args = self.args.copy()
method = args.pop("method", "GET")
return requests.Request(method=method, **valid_args(args)).prepare().url
return self.session.http.prepare_new_request(**self.args).url

def open(self):
args = self.args.copy()
method = args.pop("method", "GET")
reqargs = self.session.http.valid_request_args(**self.args)
reqargs.setdefault("method", "GET")
timeout = self.session.options.get("stream-timeout")
res = self.session.http.request(
method=method,
stream=True,
exception=StreamError,
timeout=timeout,
**valid_args(args)
**reqargs,
)

fd = StreamIOIterWrapper(res.iter_content(8192))
if self.buffered:
fd = StreamIOThreadWrapper(self.session, fd, timeout=timeout)

return fd

def to_url(self):
return self.url
41 changes: 24 additions & 17 deletions src/streamlink/stream/stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ class Stream:

__shortname__ = "stream"

@classmethod
def shortname(cls):
return cls.__shortname__

def __init__(self, session):
"""
:param streamlink.Streamlink session: Streamlink session instance
Expand All @@ -21,10 +25,28 @@ def __init__(self, session):
self.session = session

def __repr__(self):
return "<Stream()>"
params = [repr(self.shortname())]
for method in self.to_url, self.to_manifest_url:
try:
params.append(repr(method()))
except TypeError:
pass

return f"<{self.__class__.__name__} [{', '.join(params)}]>"

def __json__(self):
return dict(type=type(self).shortname())
return dict(type=self.shortname())

@property
def json(self):
obj = self.__json__()
return json.dumps(obj)

def to_url(self):
raise TypeError(f"<{self.__class__.__name__} [{self.shortname()}]> cannot be translated to a URL")

def to_manifest_url(self):
raise TypeError(f"<{self.__class__.__name__} [{self.shortname()}]> cannot be translated to a manifest URL")

def open(self) -> "StreamIO":
"""
Expand All @@ -36,21 +58,6 @@ def open(self) -> "StreamIO":

raise NotImplementedError

@property
def json(self):
obj = self.__json__()
return json.dumps(obj)

@classmethod
def shortname(cls):
return cls.__shortname__

def to_url(self):
raise TypeError("{0} cannot be converted to a URL".format(self.shortname()))

def to_manifest_url(self):
raise TypeError("{0} cannot be converted to a URL".format(self.shortname()))


class StreamIO(io.IOBase):
pass
Expand Down
11 changes: 8 additions & 3 deletions src/streamlink_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
from streamlink_cli.console import ConsoleOutput, ConsoleUserInputRequester
from streamlink_cli.constants import CONFIG_FILES, DEFAULT_STREAM_METADATA, LOG_DIR, PLUGIN_DIRS, STREAM_SYNONYMS
from streamlink_cli.output import FileOutput, Output, PlayerOutput
from streamlink_cli.utils import Formatter, HTTPServer, datetime, ignored, progress, stream_to_url
from streamlink_cli.utils import Formatter, HTTPServer, datetime, ignored, progress

ACCEPTABLE_ERRNO = (errno.EPIPE, errno.EINVAL, errno.ECONNRESET)
try:
Expand Down Expand Up @@ -257,11 +257,16 @@ def output_stream_passthrough(stream, formatter: Formatter):
"""Prepares a filename to be passed to the player."""
global output

filename = f'"{stream_to_url(stream)}"'
try:
url = stream.to_url()
except TypeError:
console.exit("The stream specified cannot be translated to a URL")
return False

output = PlayerOutput(
args.player,
args=args.player_args,
filename=filename,
filename=f'"{url}"',
call=True,
quiet=not args.verbose_player,
title=formatter.title(args.title, defaults=DEFAULT_STREAM_METADATA) if args.title else args.url
Expand Down
Loading