mirror of https://github.com/streamlink/streamlink
247 lines
8.2 KiB
Python
247 lines
8.2 KiB
Python
"""
|
|
$description Live local TV channels and video on-demand service. OTT service from FilmOn.
|
|
$url filmon.com
|
|
$type live, vod
|
|
$notes Some VODs are mp4 which may not stream, use -o to download
|
|
"""
|
|
|
|
import logging
|
|
import re
|
|
import time
|
|
from typing import Iterator, List, Tuple
|
|
from urllib.parse import urlparse, urlunparse
|
|
|
|
from streamlink.exceptions import PluginError, StreamError
|
|
from streamlink.plugin import Plugin, pluginmatcher
|
|
from streamlink.plugin.api import validate
|
|
from streamlink.plugin.api.http_session import TLSSecLevel1Adapter
|
|
from streamlink.stream.hls import HLSStream, HLSStreamReader, HLSStreamWorker
|
|
from streamlink.stream.http import HTTPStream
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
_StreamData = Tuple[str, str, int]
|
|
|
|
|
|
class FilmOnHLSStreamWorker(HLSStreamWorker):
|
|
def _fetch_playlist(self):
|
|
try:
|
|
return super()._fetch_playlist()
|
|
except StreamError as err:
|
|
# noinspection PyUnresolvedReferences
|
|
if err.err.response.status_code in (403, 502):
|
|
self.stream.watch_timeout = 0
|
|
self.playlist_reload_time = 0
|
|
log.debug(f"Force-reloading the channel playlist on error: {err}")
|
|
raise err
|
|
|
|
|
|
class FilmOnHLSStreamReader(HLSStreamReader):
|
|
__worker__ = FilmOnHLSStreamWorker
|
|
|
|
|
|
class FilmOnHLS(HLSStream):
|
|
__shortname__ = "hls-filmon"
|
|
__reader__ = FilmOnHLSStreamReader
|
|
|
|
def __init__(self, session_, url: str, api: "FilmOnAPI", channel=None, vod_id=None, quality="high", **args):
|
|
if channel is None and vod_id is None:
|
|
raise PluginError("Channel or vod_id must be set")
|
|
|
|
super().__init__(session_, url, **args)
|
|
self.api = api
|
|
self.channel = channel
|
|
self.vod_id = vod_id
|
|
self.quality = quality
|
|
self._url = url
|
|
self.watch_timeout = 0.0
|
|
self._first_netloc = ""
|
|
|
|
def _get_stream_data(self) -> Iterator[_StreamData]:
|
|
if self.channel:
|
|
log.debug(f"Reloading FilmOn channel playlist: {self.channel}")
|
|
yield from self.api.channel(self.channel)
|
|
elif self.vod_id:
|
|
log.debug(f"Reloading FilmOn VOD playlist: {self.vod_id}")
|
|
yield from self.api.vod(self.vod_id)
|
|
|
|
@property
|
|
def url(self) -> str:
|
|
if time.time() <= self.watch_timeout:
|
|
return self._url
|
|
|
|
# If the watch timeout has passed then refresh the playlist from the API
|
|
for quality, url, timeout in self._get_stream_data():
|
|
if quality == self.quality:
|
|
self.watch_timeout = time.time() + timeout
|
|
|
|
if not self.channel:
|
|
self._url = url
|
|
else:
|
|
parsed = urlparse(url)
|
|
if not self._first_netloc:
|
|
# save the first netloc
|
|
self._first_netloc = parsed.netloc
|
|
# always use the first saved netloc
|
|
self._url = parsed._replace(netloc=self._first_netloc).geturl()
|
|
|
|
return self._url
|
|
|
|
raise TypeError("Stream has expired and cannot be translated to a URL")
|
|
|
|
|
|
class FilmOnAPI:
|
|
channel_url = "https://www.filmon.com/ajax/getChannelInfo"
|
|
vod_url = "https://vms-admin.filmon.com/api/video/movie?id={0}"
|
|
|
|
ATTEMPTS = 5
|
|
TIMEOUT = 0.75
|
|
|
|
stream_schema = validate.all(
|
|
{
|
|
"quality": str,
|
|
"url": validate.url(),
|
|
"watch-timeout": int,
|
|
},
|
|
validate.union_get("quality", "url", "watch-timeout"),
|
|
)
|
|
|
|
def __init__(self, session):
|
|
self.session = session
|
|
|
|
def channel(self, channel) -> List[_StreamData]:
|
|
num = 1
|
|
while True:
|
|
# retry for 50X errors or validation errors at the same time
|
|
try:
|
|
return self.session.http.post(
|
|
self.channel_url,
|
|
data={
|
|
"channel_id": channel,
|
|
"quality": "low",
|
|
},
|
|
headers={"X-Requested-With": "XMLHttpRequest"},
|
|
schema=validate.Schema(
|
|
validate.parse_json(),
|
|
{"streams": [self.stream_schema]},
|
|
validate.get("streams"),
|
|
),
|
|
)
|
|
except PluginError:
|
|
log.debug(f"Received invalid or non-JSON data, attempt {num}/{self.ATTEMPTS}")
|
|
if num >= self.ATTEMPTS:
|
|
raise
|
|
num = num + 1
|
|
time.sleep(self.TIMEOUT)
|
|
|
|
def vod(self, vod_id) -> List[_StreamData]:
|
|
return self.session.http.get(
|
|
self.vod_url.format(vod_id),
|
|
schema=validate.Schema(
|
|
validate.parse_json(),
|
|
{"response": {"streams": {str: self.stream_schema}}},
|
|
validate.get(("response", "streams")),
|
|
validate.transform(lambda d: d.values()),
|
|
),
|
|
)
|
|
|
|
|
|
@pluginmatcher(re.compile(r"""
|
|
https?://(?:www\.)?filmon\.(?:tv|com)/
|
|
(?:
|
|
(?:
|
|
index/popout\?
|
|
|
|
|
(?:tv/)?channel/(?:export\?)?
|
|
|
|
|
tv/(?!channel/)
|
|
|
|
|
channel/
|
|
|
|
|
(?P<is_group>group/)
|
|
)(?:channel_id=)?(?P<channel>[-_\w]+)
|
|
|
|
|
vod/view/(?P<vod_id>[^/?&]+)
|
|
)
|
|
""", re.VERBOSE))
|
|
class Filmon(Plugin):
|
|
quality_weights = {
|
|
"high": 720,
|
|
"low": 480,
|
|
}
|
|
|
|
TIME_CHANNEL = 60 * 60 * 24 * 365
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
parsed = urlparse(self.url)
|
|
if parsed.path.startswith("/channel/"):
|
|
self.url = urlunparse(parsed._replace(path=parsed.path.replace("/channel/", "/tv/")))
|
|
self.api = FilmOnAPI(self.session)
|
|
|
|
adapter = TLSSecLevel1Adapter()
|
|
self.session.http.mount("https://filmon.com", adapter)
|
|
self.session.http.mount("https://www.filmon.com", adapter)
|
|
self.session.http.mount("https://vms-admin.filmon.com/", adapter)
|
|
|
|
self.session.options.set("hls-playlist-reload-time", "segment")
|
|
|
|
@classmethod
|
|
def stream_weight(cls, key):
|
|
weight = cls.quality_weights.get(key)
|
|
if weight:
|
|
return weight, "filmon"
|
|
|
|
return super().stream_weight(key)
|
|
|
|
def _get_streams(self):
|
|
channel = self.match.group("channel")
|
|
vod_id = self.match.group("vod_id")
|
|
is_group = self.match.group("is_group")
|
|
|
|
# get cookies
|
|
self.session.http.get(self.url)
|
|
|
|
if vod_id:
|
|
for quality, url, _timeout in self.api.vod(vod_id):
|
|
if url.endswith(".m3u8"):
|
|
streams = HLSStream.parse_variant_playlist(self.session, url)
|
|
if streams:
|
|
yield from streams.items()
|
|
return
|
|
yield quality, HLSStream(self.session, url)
|
|
elif url.endswith(".mp4"):
|
|
yield quality, HTTPStream(self.session, url)
|
|
else:
|
|
if not channel or channel.isdigit():
|
|
_id = channel
|
|
else:
|
|
_id = self.cache.get(channel)
|
|
if _id is not None:
|
|
log.debug(f"Found cached channel ID: {_id}")
|
|
else:
|
|
_id = self.session.http.get(self.url, schema=validate.Schema(
|
|
re.compile(r"""channel_id\s*=\s*(?P<q>['"]?)(?P<value>\d+)(?P=q)"""),
|
|
validate.any(None, validate.get("value")),
|
|
))
|
|
log.debug(f"Found channel ID: {_id}")
|
|
# do not cache a group url
|
|
if _id and not is_group:
|
|
self.cache.set(channel, _id, expires=self.TIME_CHANNEL)
|
|
|
|
if _id is None:
|
|
raise PluginError(f"Unable to find channel ID: {channel}")
|
|
|
|
try:
|
|
for quality, url, _timeout in self.api.channel(_id):
|
|
yield quality, FilmOnHLS(self.session, url, self.api, channel=_id, quality=quality)
|
|
except Exception:
|
|
if channel and not channel.isdigit():
|
|
self.cache.set(channel, None, expires=0)
|
|
log.debug(f"Reset cached channel: {channel}")
|
|
raise
|
|
|
|
|
|
__plugin__ = Filmon
|