Play first item in m3u and pls playlists when casting (#70047)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
This commit is contained in:
parent
0959ee4353
commit
ce302f4540
20 changed files with 484 additions and 5 deletions
|
@ -1,13 +1,25 @@
|
||||||
"""Helpers to deal with Cast devices."""
|
"""Helpers to deal with Cast devices."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import configparser
|
||||||
|
from dataclasses import dataclass
|
||||||
|
import logging
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
import attr
|
import attr
|
||||||
from pychromecast import dial
|
from pychromecast import dial
|
||||||
from pychromecast.const import CAST_TYPE_GROUP
|
from pychromecast.const import CAST_TYPE_GROUP
|
||||||
from pychromecast.models import CastInfo
|
from pychromecast.models import CastInfo
|
||||||
|
|
||||||
|
from homeassistant.helpers import aiohttp_client
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
_PLS_SECTION_PLAYLIST = "playlist"
|
||||||
|
|
||||||
|
|
||||||
@attr.s(slots=True, frozen=True)
|
@attr.s(slots=True, frozen=True)
|
||||||
class ChromecastInfo:
|
class ChromecastInfo:
|
||||||
|
@ -155,3 +167,143 @@ class CastStatusListener:
|
||||||
else:
|
else:
|
||||||
self._mz_mgr.deregister_listener(self._uuid, self)
|
self._mz_mgr.deregister_listener(self._uuid, self)
|
||||||
self._valid = False
|
self._valid = False
|
||||||
|
|
||||||
|
|
||||||
|
class PlaylistError(Exception):
|
||||||
|
"""Exception wrapper for pls and m3u helpers."""
|
||||||
|
|
||||||
|
|
||||||
|
class PlaylistSupported(PlaylistError):
|
||||||
|
"""The playlist is supported by cast devices and should not be parsed."""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PlaylistItem:
|
||||||
|
"""Playlist item."""
|
||||||
|
|
||||||
|
length: str | None
|
||||||
|
title: str | None
|
||||||
|
url: str
|
||||||
|
|
||||||
|
|
||||||
|
def _is_url(url):
|
||||||
|
"""Validate the URL can be parsed and at least has scheme + netloc."""
|
||||||
|
result = urlparse(url)
|
||||||
|
return all([result.scheme, result.netloc])
|
||||||
|
|
||||||
|
|
||||||
|
async def _fetch_playlist(hass, url):
|
||||||
|
"""Fetch a playlist from the given url."""
|
||||||
|
try:
|
||||||
|
session = aiohttp_client.async_get_clientsession(hass, verify_ssl=False)
|
||||||
|
async with session.get(url, timeout=5) as resp:
|
||||||
|
charset = resp.charset or "utf-8"
|
||||||
|
try:
|
||||||
|
playlist_data = (await resp.content.read(64 * 1024)).decode(charset)
|
||||||
|
except ValueError as err:
|
||||||
|
raise PlaylistError(f"Could not decode playlist {url}") from err
|
||||||
|
except asyncio.TimeoutError as err:
|
||||||
|
raise PlaylistError(f"Timeout while fetching playlist {url}") from err
|
||||||
|
except aiohttp.client_exceptions.ClientError as err:
|
||||||
|
raise PlaylistError(f"Error while fetching playlist {url}") from err
|
||||||
|
|
||||||
|
return playlist_data
|
||||||
|
|
||||||
|
|
||||||
|
async def parse_m3u(hass, url):
|
||||||
|
"""Very simple m3u parser.
|
||||||
|
|
||||||
|
Based on https://github.com/dvndrsn/M3uParser/blob/master/m3uparser.py
|
||||||
|
"""
|
||||||
|
m3u_data = await _fetch_playlist(hass, url)
|
||||||
|
m3u_lines = m3u_data.splitlines()
|
||||||
|
|
||||||
|
playlist = []
|
||||||
|
|
||||||
|
length = None
|
||||||
|
title = None
|
||||||
|
|
||||||
|
for line in m3u_lines:
|
||||||
|
line = line.strip()
|
||||||
|
if line.startswith("#EXTINF:"):
|
||||||
|
# Get length and title from #EXTINF line
|
||||||
|
info = line.split("#EXTINF:")[1].split(",", 1)
|
||||||
|
if len(info) != 2:
|
||||||
|
_LOGGER.warning("Ignoring invalid extinf %s in playlist %s", line, url)
|
||||||
|
continue
|
||||||
|
length = info[0].split(" ", 1)
|
||||||
|
title = info[1].strip()
|
||||||
|
elif line.startswith("#EXT-X-VERSION:"):
|
||||||
|
# HLS stream, supported by cast devices
|
||||||
|
raise PlaylistSupported("HLS")
|
||||||
|
elif line.startswith("#"):
|
||||||
|
# Ignore other extensions
|
||||||
|
continue
|
||||||
|
elif len(line) != 0:
|
||||||
|
# Get song path from all other, non-blank lines
|
||||||
|
if not _is_url(line):
|
||||||
|
raise PlaylistError(f"Invalid item {line} in playlist {url}")
|
||||||
|
playlist.append(PlaylistItem(length=length, title=title, url=line))
|
||||||
|
# reset the song variables so it doesn't use the same EXTINF more than once
|
||||||
|
length = None
|
||||||
|
title = None
|
||||||
|
|
||||||
|
return playlist
|
||||||
|
|
||||||
|
|
||||||
|
async def parse_pls(hass, url):
|
||||||
|
"""Very simple pls parser.
|
||||||
|
|
||||||
|
Based on https://github.com/mariob/plsparser/blob/master/src/plsparser.py
|
||||||
|
"""
|
||||||
|
pls_data = await _fetch_playlist(hass, url)
|
||||||
|
|
||||||
|
pls_parser = configparser.ConfigParser()
|
||||||
|
try:
|
||||||
|
pls_parser.read_string(pls_data, url)
|
||||||
|
except configparser.Error as err:
|
||||||
|
raise PlaylistError(f"Can't parse playlist {url}") from err
|
||||||
|
|
||||||
|
if (
|
||||||
|
_PLS_SECTION_PLAYLIST not in pls_parser
|
||||||
|
or pls_parser[_PLS_SECTION_PLAYLIST].getint("Version") != 2
|
||||||
|
):
|
||||||
|
raise PlaylistError(f"Invalid playlist {url}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
num_entries = pls_parser.getint(_PLS_SECTION_PLAYLIST, "NumberOfEntries")
|
||||||
|
except (configparser.NoOptionError, ValueError) as err:
|
||||||
|
raise PlaylistError(f"Invalid NumberOfEntries in playlist {url}") from err
|
||||||
|
|
||||||
|
playlist_section = pls_parser[_PLS_SECTION_PLAYLIST]
|
||||||
|
|
||||||
|
playlist = []
|
||||||
|
for entry in range(1, num_entries + 1):
|
||||||
|
file_option = f"File{entry}"
|
||||||
|
if file_option not in playlist_section:
|
||||||
|
_LOGGER.warning("Missing %s in pls from %s", file_option, url)
|
||||||
|
continue
|
||||||
|
item_url = playlist_section[file_option]
|
||||||
|
if not _is_url(item_url):
|
||||||
|
raise PlaylistError(f"Invalid item {item_url} in playlist {url}")
|
||||||
|
playlist.append(
|
||||||
|
PlaylistItem(
|
||||||
|
length=playlist_section.get(f"Length{entry}"),
|
||||||
|
title=playlist_section.get(f"Title{entry}"),
|
||||||
|
url=item_url,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return playlist
|
||||||
|
|
||||||
|
|
||||||
|
async def parse_playlist(hass, url):
|
||||||
|
"""Parse an m3u or pls playlist."""
|
||||||
|
if url.endswith(".m3u") or url.endswith(".m3u8"):
|
||||||
|
playlist = await parse_m3u(hass, url)
|
||||||
|
else:
|
||||||
|
playlist = await parse_pls(hass, url)
|
||||||
|
|
||||||
|
if not playlist:
|
||||||
|
raise PlaylistError(f"Empty playlist {url}")
|
||||||
|
|
||||||
|
return playlist
|
||||||
|
|
|
@ -64,7 +64,14 @@ from .const import (
|
||||||
SIGNAL_HASS_CAST_SHOW_VIEW,
|
SIGNAL_HASS_CAST_SHOW_VIEW,
|
||||||
)
|
)
|
||||||
from .discovery import setup_internal_discovery
|
from .discovery import setup_internal_discovery
|
||||||
from .helpers import CastStatusListener, ChromecastInfo, ChromeCastZeroconf
|
from .helpers import (
|
||||||
|
CastStatusListener,
|
||||||
|
ChromecastInfo,
|
||||||
|
ChromeCastZeroconf,
|
||||||
|
PlaylistError,
|
||||||
|
PlaylistSupported,
|
||||||
|
parse_playlist,
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -582,14 +589,13 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
|
||||||
media_id = sourced_media.url
|
media_id = sourced_media.url
|
||||||
|
|
||||||
extra = kwargs.get(ATTR_MEDIA_EXTRA, {})
|
extra = kwargs.get(ATTR_MEDIA_EXTRA, {})
|
||||||
metadata = extra.get("metadata")
|
|
||||||
|
|
||||||
# Handle media supported by a known cast app
|
# Handle media supported by a known cast app
|
||||||
if media_type == CAST_DOMAIN:
|
if media_type == CAST_DOMAIN:
|
||||||
try:
|
try:
|
||||||
app_data = json.loads(media_id)
|
app_data = json.loads(media_id)
|
||||||
if metadata is not None:
|
if metadata := extra.get("metadata"):
|
||||||
app_data["metadata"] = extra.get("metadata")
|
app_data["metadata"] = metadata
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
_LOGGER.error("Invalid JSON in media_content_id")
|
_LOGGER.error("Invalid JSON in media_content_id")
|
||||||
raise
|
raise
|
||||||
|
@ -640,9 +646,51 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
|
||||||
"hlsVideoSegmentFormat": "fmp4",
|
"hlsVideoSegmentFormat": "fmp4",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
elif (
|
||||||
|
media_id.endswith(".m3u")
|
||||||
|
or media_id.endswith(".m3u8")
|
||||||
|
or media_id.endswith(".pls")
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
playlist = await parse_playlist(self.hass, media_id)
|
||||||
|
_LOGGER.debug(
|
||||||
|
"[%s %s] Playing item %s from playlist %s",
|
||||||
|
self.entity_id,
|
||||||
|
self._cast_info.friendly_name,
|
||||||
|
playlist[0].url,
|
||||||
|
media_id,
|
||||||
|
)
|
||||||
|
media_id = playlist[0].url
|
||||||
|
if title := playlist[0].title:
|
||||||
|
extra = {
|
||||||
|
**extra,
|
||||||
|
"metadata": {"title": title},
|
||||||
|
}
|
||||||
|
except PlaylistSupported as err:
|
||||||
|
_LOGGER.debug(
|
||||||
|
"[%s %s] Playlist %s is supported: %s",
|
||||||
|
self.entity_id,
|
||||||
|
self._cast_info.friendly_name,
|
||||||
|
media_id,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
except PlaylistError as err:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"[%s %s] Failed to parse playlist %s: %s",
|
||||||
|
self.entity_id,
|
||||||
|
self._cast_info.friendly_name,
|
||||||
|
media_id,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
|
||||||
# Default to play with the default media receiver
|
# Default to play with the default media receiver
|
||||||
app_data = {"media_id": media_id, "media_type": media_type, **extra}
|
app_data = {"media_id": media_id, "media_type": media_type, **extra}
|
||||||
|
_LOGGER.debug(
|
||||||
|
"[%s %s] Playing %s with default_media_receiver",
|
||||||
|
self.entity_id,
|
||||||
|
self._cast_info.friendly_name,
|
||||||
|
app_data,
|
||||||
|
)
|
||||||
await self.hass.async_add_executor_job(
|
await self.hass.async_add_executor_job(
|
||||||
quick_play, self._chromecast, "default_media_receiver", app_data
|
quick_play, self._chromecast, "default_media_receiver", app_data
|
||||||
)
|
)
|
||||||
|
|
9
tests/components/cast/fixtures/164-hi-aac.pls
Normal file
9
tests/components/cast/fixtures/164-hi-aac.pls
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
[playlist]
|
||||||
|
|
||||||
|
NumberOfEntries=1
|
||||||
|
|
||||||
|
File1=https://http-live.sr.se/p3-aac-192
|
||||||
|
Title1=Sveriges Radio
|
||||||
|
Length1=-1
|
||||||
|
|
||||||
|
Version=2
|
0
tests/components/cast/fixtures/164-hi-aac_invalid.pls
Normal file
0
tests/components/cast/fixtures/164-hi-aac_invalid.pls
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
[playlist]
|
||||||
|
|
||||||
|
NumberOfEntries=many
|
||||||
|
|
||||||
|
File1=https://http-live.sr.se/p3-aac-192
|
||||||
|
Title1=Sveriges Radio
|
||||||
|
Length1=-1
|
||||||
|
|
||||||
|
Version=2
|
|
@ -0,0 +1,9 @@
|
||||||
|
[playlist]
|
||||||
|
|
||||||
|
NumberOfEntries=1
|
||||||
|
|
||||||
|
File1=http-live.sr.se/p3-aac-192
|
||||||
|
Title1=Sveriges Radio
|
||||||
|
Length1=-1
|
||||||
|
|
||||||
|
Version=2
|
|
@ -0,0 +1,9 @@
|
||||||
|
[playlist]
|
||||||
|
|
||||||
|
NumberOfEntries=1
|
||||||
|
|
||||||
|
File1=https://http-live.sr.se/p3-aac-192
|
||||||
|
Title1=Sveriges Radio
|
||||||
|
Length1=-1
|
||||||
|
|
||||||
|
Version=3
|
|
@ -0,0 +1,8 @@
|
||||||
|
[playlist]
|
||||||
|
|
||||||
|
NumberOfEntries=1
|
||||||
|
|
||||||
|
Title1=Sveriges Radio
|
||||||
|
Length1=-1
|
||||||
|
|
||||||
|
Version=2
|
7
tests/components/cast/fixtures/164-hi-aac_no_entries.pls
Normal file
7
tests/components/cast/fixtures/164-hi-aac_no_entries.pls
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
[playlist]
|
||||||
|
|
||||||
|
File1=https://http-live.sr.se/p3-aac-192
|
||||||
|
Title1=Sveriges Radio
|
||||||
|
Length1=-1
|
||||||
|
|
||||||
|
Version=2
|
|
@ -0,0 +1,7 @@
|
||||||
|
NumberOfEntries=1
|
||||||
|
|
||||||
|
File1=https://http-live.sr.se/p3-aac-192
|
||||||
|
Title1=Sveriges Radio
|
||||||
|
Length1=-1
|
||||||
|
|
||||||
|
Version=2
|
7
tests/components/cast/fixtures/164-hi-aac_no_version.pls
Normal file
7
tests/components/cast/fixtures/164-hi-aac_no_version.pls
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
[playlist]
|
||||||
|
|
||||||
|
NumberOfEntries=1
|
||||||
|
|
||||||
|
File1=https://http-live.sr.se/p3-aac-192
|
||||||
|
Title1=Sveriges Radio
|
||||||
|
Length1=-1
|
4
tests/components/cast/fixtures/209-hi-mp3.m3u
Normal file
4
tests/components/cast/fixtures/209-hi-mp3.m3u
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
#EXTM3U
|
||||||
|
|
||||||
|
#EXTINF:-1,Sveriges Radio
|
||||||
|
https://http-live.sr.se/p4norrbotten-mp3-192
|
4
tests/components/cast/fixtures/209-hi-mp3_bad_extinf.m3u
Normal file
4
tests/components/cast/fixtures/209-hi-mp3_bad_extinf.m3u
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
#EXTM3U
|
||||||
|
|
||||||
|
#EXTINF:invalid
|
||||||
|
https://http-live.sr.se/p4norrbotten-mp3-192
|
4
tests/components/cast/fixtures/209-hi-mp3_bad_url.m3u
Normal file
4
tests/components/cast/fixtures/209-hi-mp3_bad_url.m3u
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
#EXTM3U
|
||||||
|
|
||||||
|
#EXTINF:-1,Sveriges Radio
|
||||||
|
p4norrbotten-mp3-192
|
3
tests/components/cast/fixtures/209-hi-mp3_no_extinf.m3u
Normal file
3
tests/components/cast/fixtures/209-hi-mp3_no_extinf.m3u
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
#EXTM3U
|
||||||
|
|
||||||
|
https://http-live.sr.se/p4norrbotten-mp3-192
|
4
tests/components/cast/fixtures/bbc_radio_fourfm.m3u8
Normal file
4
tests/components/cast/fixtures/bbc_radio_fourfm.m3u8
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
#EXTM3U
|
||||||
|
#EXT-X-VERSION:3
|
||||||
|
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=101760,CODECS="mp4a.40.5"
|
||||||
|
http://as-hls-ww-live.akamaized.net/pool_904/live/ww/bbc_radio_fourfm/bbc_radio_fourfm.isml/bbc_radio_fourfm-audio%3d96000.norewind.m3u8
|
1
tests/components/cast/fixtures/empty.m3u
Normal file
1
tests/components/cast/fixtures/empty.m3u
Normal file
|
@ -0,0 +1 @@
|
||||||
|
|
114
tests/components/cast/test_helpers.py
Normal file
114
tests/components/cast/test_helpers.py
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
"""Tests for the Cast integration helpers."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from aiohttp import client_exceptions
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.cast.helpers import (
|
||||||
|
PlaylistError,
|
||||||
|
PlaylistItem,
|
||||||
|
PlaylistSupported,
|
||||||
|
parse_playlist,
|
||||||
|
)
|
||||||
|
|
||||||
|
from tests.common import load_fixture
|
||||||
|
|
||||||
|
|
||||||
|
async def test_hls_playlist_supported(hass, aioclient_mock):
|
||||||
|
"""Test playlist parsing of HLS playlist."""
|
||||||
|
url = "http://a.files.bbci.co.uk/media/live/manifesto/audio/simulcast/hls/nonuk/sbr_low/ak/bbc_radio_fourfm.m3u8"
|
||||||
|
aioclient_mock.get(url, text=load_fixture("bbc_radio_fourfm.m3u8", "cast"))
|
||||||
|
with pytest.raises(PlaylistSupported):
|
||||||
|
await parse_playlist(hass, url)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"url,fixture,expected_playlist",
|
||||||
|
(
|
||||||
|
(
|
||||||
|
"https://sverigesradio.se/topsy/direkt/209-hi-mp3.m3u",
|
||||||
|
"209-hi-mp3.m3u",
|
||||||
|
[
|
||||||
|
PlaylistItem(
|
||||||
|
length=["-1"],
|
||||||
|
title="Sveriges Radio",
|
||||||
|
url="https://http-live.sr.se/p4norrbotten-mp3-192",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"https://sverigesradio.se/topsy/direkt/209-hi-mp3.m3u",
|
||||||
|
"209-hi-mp3_bad_extinf.m3u",
|
||||||
|
[
|
||||||
|
PlaylistItem(
|
||||||
|
length=None,
|
||||||
|
title=None,
|
||||||
|
url="https://http-live.sr.se/p4norrbotten-mp3-192",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"https://sverigesradio.se/topsy/direkt/209-hi-mp3.m3u",
|
||||||
|
"209-hi-mp3_no_extinf.m3u",
|
||||||
|
[
|
||||||
|
PlaylistItem(
|
||||||
|
length=None,
|
||||||
|
title=None,
|
||||||
|
url="https://http-live.sr.se/p4norrbotten-mp3-192",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"http://sverigesradio.se/topsy/direkt/164-hi-aac.pls",
|
||||||
|
"164-hi-aac.pls",
|
||||||
|
[
|
||||||
|
PlaylistItem(
|
||||||
|
length="-1",
|
||||||
|
title="Sveriges Radio",
|
||||||
|
url="https://http-live.sr.se/p3-aac-192",
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
async def test_parse_playlist(hass, aioclient_mock, url, fixture, expected_playlist):
|
||||||
|
"""Test playlist parsing of HLS playlist."""
|
||||||
|
aioclient_mock.get(url, text=load_fixture(fixture, "cast"))
|
||||||
|
playlist = await parse_playlist(hass, url)
|
||||||
|
assert expected_playlist == playlist
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"url,fixture",
|
||||||
|
(
|
||||||
|
("http://sverigesradio.se/164-hi-aac.pls", "164-hi-aac_invalid_entries.pls"),
|
||||||
|
("http://sverigesradio.se/164-hi-aac.pls", "164-hi-aac_invalid_file.pls"),
|
||||||
|
("http://sverigesradio.se/164-hi-aac.pls", "164-hi-aac_invalid_version.pls"),
|
||||||
|
("http://sverigesradio.se/164-hi-aac.pls", "164-hi-aac_invalid.pls"),
|
||||||
|
("http://sverigesradio.se/164-hi-aac.pls", "164-hi-aac_missing_file.pls"),
|
||||||
|
("http://sverigesradio.se/164-hi-aac.pls", "164-hi-aac_no_entries.pls"),
|
||||||
|
("http://sverigesradio.se/164-hi-aac.pls", "164-hi-aac_no_playlist.pls"),
|
||||||
|
("http://sverigesradio.se/164-hi-aac.pls", "164-hi-aac_no_version.pls"),
|
||||||
|
("https://sverigesradio.se/209-hi-mp3.m3u", "209-hi-mp3_bad_url.m3u"),
|
||||||
|
("https://sverigesradio.se/209-hi-mp3.m3u", "empty.m3u"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
async def test_parse_bad_playlist(hass, aioclient_mock, url, fixture):
|
||||||
|
"""Test playlist parsing of HLS playlist."""
|
||||||
|
aioclient_mock.get(url, text=load_fixture(fixture, "cast"))
|
||||||
|
with pytest.raises(PlaylistError):
|
||||||
|
await parse_playlist(hass, url)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"url,exc",
|
||||||
|
(
|
||||||
|
("http://sverigesradio.se/164-hi-aac.pls", asyncio.TimeoutError),
|
||||||
|
("http://sverigesradio.se/164-hi-aac.pls", client_exceptions.ClientError),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
async def test_parse_http_error(hass, aioclient_mock, url, exc):
|
||||||
|
"""Test playlist parsing of HLS playlist when aioclient raises."""
|
||||||
|
aioclient_mock.get(url, text="", exc=exc)
|
||||||
|
with pytest.raises(PlaylistError):
|
||||||
|
await parse_playlist(hass, url)
|
|
@ -44,7 +44,12 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er,
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
from tests.common import MockConfigEntry, assert_setup_component, mock_platform
|
from tests.common import (
|
||||||
|
MockConfigEntry,
|
||||||
|
assert_setup_component,
|
||||||
|
load_fixture,
|
||||||
|
mock_platform,
|
||||||
|
)
|
||||||
from tests.components.media_player import common
|
from tests.components.media_player import common
|
||||||
|
|
||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
|
@ -1108,6 +1113,80 @@ async def test_entity_play_media_sign_URL(hass: HomeAssistant, quick_play_mock):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"url,fixture,playlist_item",
|
||||||
|
(
|
||||||
|
# Test title is extracted from m3u playlist
|
||||||
|
(
|
||||||
|
"https://sverigesradio.se/topsy/direkt/209-hi-mp3.m3u",
|
||||||
|
"209-hi-mp3.m3u",
|
||||||
|
{
|
||||||
|
"media_id": "https://http-live.sr.se/p4norrbotten-mp3-192",
|
||||||
|
"media_type": "audio",
|
||||||
|
"metadata": {"title": "Sveriges Radio"},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
# Test title is extracted from pls playlist
|
||||||
|
(
|
||||||
|
"http://sverigesradio.se/topsy/direkt/164-hi-aac.pls",
|
||||||
|
"164-hi-aac.pls",
|
||||||
|
{
|
||||||
|
"media_id": "https://http-live.sr.se/p3-aac-192",
|
||||||
|
"media_type": "audio",
|
||||||
|
"metadata": {"title": "Sveriges Radio"},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
# Test HLS playlist is forwarded to the device
|
||||||
|
(
|
||||||
|
"http://a.files.bbci.co.uk/media/live/manifesto/audio/simulcast/hls/nonuk/sbr_low/ak/bbc_radio_fourfm.m3u8",
|
||||||
|
"bbc_radio_fourfm.m3u8",
|
||||||
|
{
|
||||||
|
"media_id": "http://a.files.bbci.co.uk/media/live/manifesto/audio/simulcast/hls/nonuk/sbr_low/ak/bbc_radio_fourfm.m3u8",
|
||||||
|
"media_type": "audio",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
# Test bad playlist is forwarded to the device
|
||||||
|
(
|
||||||
|
"https://sverigesradio.se/209-hi-mp3.m3u",
|
||||||
|
"209-hi-mp3_bad_url.m3u",
|
||||||
|
{
|
||||||
|
"media_id": "https://sverigesradio.se/209-hi-mp3.m3u",
|
||||||
|
"media_type": "audio",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
async def test_entity_play_media_playlist(
|
||||||
|
hass: HomeAssistant, aioclient_mock, quick_play_mock, url, fixture, playlist_item
|
||||||
|
):
|
||||||
|
"""Test playing media."""
|
||||||
|
entity_id = "media_player.speaker"
|
||||||
|
aioclient_mock.get(url, text=load_fixture(fixture, "cast"))
|
||||||
|
|
||||||
|
await async_process_ha_core_config(
|
||||||
|
hass,
|
||||||
|
{"internal_url": "http://example.com:8123"},
|
||||||
|
)
|
||||||
|
|
||||||
|
info = get_fake_chromecast_info()
|
||||||
|
|
||||||
|
chromecast, _ = await async_setup_media_player_cast(hass, info)
|
||||||
|
_, conn_status_cb, _ = get_status_callbacks(chromecast)
|
||||||
|
|
||||||
|
connection_status = MagicMock()
|
||||||
|
connection_status.status = "CONNECTED"
|
||||||
|
conn_status_cb(connection_status)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# Play_media
|
||||||
|
await common.async_play_media(hass, "audio", url, entity_id)
|
||||||
|
quick_play_mock.assert_called_once_with(
|
||||||
|
chromecast,
|
||||||
|
"default_media_receiver",
|
||||||
|
playlist_item,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def test_entity_media_content_type(hass: HomeAssistant):
|
async def test_entity_media_content_type(hass: HomeAssistant):
|
||||||
"""Test various content types."""
|
"""Test various content types."""
|
||||||
entity_id = "media_player.speaker"
|
entity_id = "media_player.speaker"
|
||||||
|
|
|
@ -175,6 +175,7 @@ class AiohttpClientMockResponse:
|
||||||
if response is None:
|
if response is None:
|
||||||
response = b""
|
response = b""
|
||||||
|
|
||||||
|
self.charset = "utf-8"
|
||||||
self.method = method
|
self.method = method
|
||||||
self._url = url
|
self._url = url
|
||||||
self.status = status
|
self.status = status
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue