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:
back-to 2020-11-27 17:09:23 +01:00 committed by GitHub
parent 12ad1c71c7
commit 2f79f7d5b1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 145 additions and 0 deletions

View File

@ -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

View File

@ -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

View File

@ -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)