mirror of https://github.com/streamlink/streamlink
plugins.pluto: new plugin for https://pluto.tv/ (#3363)
- Livestreams will break during commercials, without the `self.session.set_option('ffmpeg-fout', 'mpegts')` option enabled. - VODs might work without it, but I did not test it. closes https://github.com/streamlink/streamlink/issues/854 closes https://github.com/streamlink/streamlink/pull/2747 closes https://github.com/streamlink/streamlink/issues/3156 Co-authored-by: calculon-jr <54852718+calculon-jr@users.noreply.github.com>
This commit is contained in:
parent
12ad1c71c7
commit
2f79f7d5b1
|
@ -139,6 +139,7 @@ piczel piczel.tv Yes No
|
|||
pixiv sketch.pixiv.net Yes --
|
||||
playtv - playtv.fr Yes -- Streams may be geo-restricted to France.
|
||||
- play.tv
|
||||
pluto pluto.tv Yes Yes
|
||||
pluzz - france.tv Yes Yes Streams may be geo-restricted to France, Andorra and Monaco.
|
||||
- ludo.fr
|
||||
- zouzous.fr
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
import logging
|
||||
import re
|
||||
from uuid import uuid4
|
||||
|
||||
from streamlink.plugin import Plugin
|
||||
from streamlink.plugin.api import validate
|
||||
from streamlink.stream import HLSStream
|
||||
from streamlink.stream.ffmpegmux import MuxedStream
|
||||
from streamlink.utils.url import update_qsd
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Pluto(Plugin):
|
||||
_re_url = re.compile(r'''^https?://(?:www\.)?pluto\.tv/(?:
|
||||
live-tv/(?P<slug_live>[^/?]+)/?$
|
||||
|
|
||||
on-demand/series/(?P<slug_series>[^/]+)/season/\d+/episode/(?P<slug_episode>[^/]+)$
|
||||
|
|
||||
on-demand/movies/(?P<slug_movies>[^/]+)$
|
||||
)''', re.VERBOSE)
|
||||
|
||||
title = None
|
||||
|
||||
@classmethod
|
||||
def can_handle_url(cls, url):
|
||||
return cls._re_url.match(url) is not None
|
||||
|
||||
def get_title(self):
|
||||
return self.title
|
||||
|
||||
def _schema_media(self, slug):
|
||||
return validate.Schema(
|
||||
[{
|
||||
'name': str,
|
||||
'slug': str,
|
||||
'stitched': {
|
||||
'urls': [
|
||||
{
|
||||
'type': str,
|
||||
'url': validate.url(),
|
||||
}
|
||||
]
|
||||
}
|
||||
}],
|
||||
validate.filter(lambda k: k['slug'].lower() == slug.lower()),
|
||||
validate.get(0),
|
||||
)
|
||||
|
||||
def _get_streams(self):
|
||||
data = None
|
||||
|
||||
m = self._re_url.match(self.url).groupdict()
|
||||
if m['slug_live']:
|
||||
res = self.session.http.get('https://api.pluto.tv/v2/channels')
|
||||
data = self.session.http.json(res,
|
||||
schema=self._schema_media(m['slug_live']))
|
||||
elif m['slug_series'] and m['slug_episode']:
|
||||
res = self.session.http.get(f'http://api.pluto.tv/v3/vod/slugs/{m["slug_series"]}')
|
||||
data = self.session.http.json(
|
||||
res,
|
||||
schema=validate.Schema(
|
||||
{'seasons': validate.all(
|
||||
[{'episodes': self._schema_media(m['slug_episode'])}],
|
||||
validate.filter(lambda k: k['episodes'] is not None))},
|
||||
validate.get('seasons'),
|
||||
validate.get(0),
|
||||
validate.any(None, validate.get('episodes'))
|
||||
),
|
||||
)
|
||||
elif m['slug_movies']:
|
||||
res = self.session.http.get('https://api.pluto.tv/v3/vod/categories',
|
||||
params={'includeItems': 'true', 'deviceType': 'web'})
|
||||
data = self.session.http.json(
|
||||
res,
|
||||
schema=validate.Schema(
|
||||
{'categories': validate.all(
|
||||
[{'items': self._schema_media(m['slug_movies'])}],
|
||||
validate.filter(lambda k: k['items'] is not None))},
|
||||
validate.get('categories'),
|
||||
validate.get(0),
|
||||
validate.any(None, validate.get('items')),
|
||||
),
|
||||
)
|
||||
|
||||
log.trace(f'{data!r}')
|
||||
if data is None:
|
||||
return
|
||||
|
||||
self.title = data['name']
|
||||
stream_url_no_sid = data['stitched']['urls'][0]['url']
|
||||
device_id = str(uuid4())
|
||||
stream_url = update_qsd(stream_url_no_sid, {
|
||||
'deviceId': device_id,
|
||||
'sid': device_id,
|
||||
'deviceType': 'web',
|
||||
'deviceMake': 'Firefox',
|
||||
'deviceModel': 'Firefox',
|
||||
'appName': 'web',
|
||||
})
|
||||
|
||||
self.session.set_option('ffmpeg-fout', 'mpegts')
|
||||
for q, s in HLSStream.parse_variant_playlist(self.session, stream_url).items():
|
||||
yield q, MuxedStream(self.session, s)
|
||||
|
||||
|
||||
__plugin__ = Pluto
|
|
@ -0,0 +1,37 @@
|
|||
import unittest
|
||||
|
||||
from streamlink.plugins.pluto import Pluto
|
||||
|
||||
|
||||
class TestPluginPluto(unittest.TestCase):
|
||||
def test_can_handle_url(self):
|
||||
should_match = [
|
||||
'http://www.pluto.tv/live-tv/channel-lineup',
|
||||
'http://pluto.tv/live-tv/channel',
|
||||
'http://pluto.tv/live-tv/channel/',
|
||||
'https://pluto.tv/live-tv/red-bull-tv-2',
|
||||
'https://pluto.tv/live-tv/4k-tv',
|
||||
'http://www.pluto.tv/on-demand/series/leverage/season/1/episode/the-nigerian-job-2009-1-1',
|
||||
'http://pluto.tv/on-demand/series/fear-factor-usa-(lf)/season/5/episode/underwater-safe-bob-car-ramp-2004-5-3',
|
||||
'https://www.pluto.tv/on-demand/movies/dr.-no-1963-1-1',
|
||||
'http://pluto.tv/on-demand/movies/the-last-dragon-(1985)-1-1',
|
||||
]
|
||||
for url in should_match:
|
||||
self.assertTrue(Pluto.can_handle_url(url), url)
|
||||
|
||||
def test_can_handle_url_negative(self):
|
||||
should_not_match = [
|
||||
'https://fake.pluto.tv/live-tv/hello',
|
||||
'http://www.pluto.tv/live-tv/channel-lineup/extra',
|
||||
'https://www.pluto.tv/live-tv',
|
||||
'https://pluto.tv/live-tv',
|
||||
'https://www.pluto.com/live-tv/swag',
|
||||
'https://youtube.com/live-tv/swag.html',
|
||||
'http://pluto.tv/movies/dr.-no-1963-1-1',
|
||||
'http://pluto.tv/on-demand/movies/dr.-no-1/963-1-1',
|
||||
'http://pluto.tv/on-demand/series/dr.-no-1963-1-1',
|
||||
'http://pluto.tv/on-demand/movies/leverage/season/1/episode/the-nigerian-job-2009-1-1',
|
||||
'http://pluto.tv/on-demand/fear-factor-usa-(lf)/season/5/episode/underwater-safe-bob-car-ramp-2004-5-3',
|
||||
]
|
||||
for url in should_not_match:
|
||||
self.assertFalse(Pluto.can_handle_url(url), url)
|
Loading…
Reference in New Issue