Add extract media url service to media extractor (#100780)

* Exclude manifest files from youtube media extraction

* Add media_extractor service to extract media

* Fix snapshot

* Run ytdlp async

* Add icon

* Fix

* Fix
This commit is contained in:
Joost Lekkerkerker 2024-04-16 16:13:03 +02:00 committed by GitHub
parent 7cd0fe3c5f
commit e9894f8e91
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 2045 additions and 7 deletions

View file

@ -17,18 +17,29 @@ from homeassistant.components.media_player import (
SERVICE_PLAY_MEDIA, SERVICE_PLAY_MEDIA,
) )
from homeassistant.const import ATTR_ENTITY_ID from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.core import (
HomeAssistant,
ServiceCall,
ServiceResponse,
SupportsResponse,
)
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from .const import (
ATTR_FORMAT_QUERY,
ATTR_URL,
DEFAULT_STREAM_QUERY,
DOMAIN,
SERVICE_EXTRACT_MEDIA_URL,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_CUSTOMIZE_ENTITIES = "customize" CONF_CUSTOMIZE_ENTITIES = "customize"
CONF_DEFAULT_STREAM_QUERY = "default_query" CONF_DEFAULT_STREAM_QUERY = "default_query"
DEFAULT_STREAM_QUERY = "best"
DOMAIN = "media_extractor"
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
{ {
DOMAIN: vol.Schema( DOMAIN: vol.Schema(
@ -47,10 +58,62 @@ CONFIG_SCHEMA = vol.Schema(
def setup(hass: HomeAssistant, config: ConfigType) -> bool: def setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the media extractor service.""" """Set up the media extractor service."""
async def extract_media_url(call: ServiceCall) -> ServiceResponse:
"""Extract media url."""
youtube_dl = YoutubeDL(
{"quiet": True, "logger": _LOGGER, "format": call.data[ATTR_FORMAT_QUERY]}
)
def extract_info() -> dict[str, Any]:
return cast(
dict[str, Any],
youtube_dl.extract_info(
call.data[ATTR_URL], download=False, process=False
),
)
result = await hass.async_add_executor_job(extract_info)
if "entries" in result:
_LOGGER.warning("Playlists are not supported, looking for the first video")
entries = list(result["entries"])
if entries:
selected_media = entries[0]
else:
raise HomeAssistantError("Playlist is empty")
else:
selected_media = result
if "formats" in selected_media:
if selected_media["extractor"] == "youtube":
url = get_best_stream_youtube(selected_media["formats"])
else:
url = get_best_stream(selected_media["formats"])
else:
url = cast(str, selected_media["url"])
return {"url": url}
def play_media(call: ServiceCall) -> None: def play_media(call: ServiceCall) -> None:
"""Get stream URL and send it to the play_media service.""" """Get stream URL and send it to the play_media service."""
MediaExtractor(hass, config[DOMAIN], call.data).extract_and_send() MediaExtractor(hass, config[DOMAIN], call.data).extract_and_send()
default_format_query = config.get(DOMAIN, {}).get(
CONF_DEFAULT_STREAM_QUERY, DEFAULT_STREAM_QUERY
)
hass.services.async_register(
DOMAIN,
SERVICE_EXTRACT_MEDIA_URL,
extract_media_url,
schema=vol.Schema(
{
vol.Required(ATTR_URL): cv.string,
vol.Optional(
ATTR_FORMAT_QUERY, default=default_format_query
): cv.string,
}
),
supports_response=SupportsResponse.ONLY,
)
hass.services.register( hass.services.register(
DOMAIN, DOMAIN,
SERVICE_PLAY_MEDIA, SERVICE_PLAY_MEDIA,

View file

@ -0,0 +1,9 @@
"""Constants for media_extractor."""
DEFAULT_STREAM_QUERY = "best"
DOMAIN = "media_extractor"
ATTR_URL = "url"
ATTR_FORMAT_QUERY = "format_query"
SERVICE_EXTRACT_MEDIA_URL = "extract_media_url"

View file

@ -1,5 +1,6 @@
{ {
"services": { "services": {
"play_media": "mdi:play" "play_media": "mdi:play",
"extract_media_url": "mdi:link"
} }
} }

View file

@ -19,3 +19,14 @@ play_media:
- "MUSIC" - "MUSIC"
- "TVSHOW" - "TVSHOW"
- "VIDEO" - "VIDEO"
extract_media_url:
fields:
url:
required: true
example: "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
selector:
text:
format_query:
example: "best"
selector:
text:

View file

@ -13,6 +13,20 @@
"description": "The type of the content to play. Must be one of MUSIC, TVSHOW, VIDEO, EPISODE, CHANNEL or PLAYLIST MUSIC." "description": "The type of the content to play. Must be one of MUSIC, TVSHOW, VIDEO, EPISODE, CHANNEL or PLAYLIST MUSIC."
} }
} }
},
"extract_media_url": {
"name": "Get Media URL",
"description": "Extract media url from a service.",
"fields": {
"url": {
"name": "Media URL",
"description": "URL where the media can be found."
},
"format_query": {
"name": "Format query",
"description": "Youtube-dl query to select the quality of the result."
}
}
} }
} }
} }

View file

@ -36,9 +36,13 @@ class MockYoutubeDL:
"""Initialize mock object for YoutubeDL.""" """Initialize mock object for YoutubeDL."""
self.params = params self.params = params
def extract_info(self, url: str, *, process: bool = False) -> dict[str, Any]: def extract_info(
self, url: str, *, download: bool = True, process: bool = False
) -> dict[str, Any]:
"""Return info.""" """Return info."""
self._fixture = _get_base_fixture(url) self._fixture = _get_base_fixture(url)
if not download:
return load_json_object_fixture(f"media_extractor/{self._fixture}.json")
return load_json_object_fixture(f"media_extractor/{self._fixture}_info.json") return load_json_object_fixture(f"media_extractor/{self._fixture}_info.json")
def process_ie_result( def process_ie_result(

View file

@ -0,0 +1,87 @@
{
"id": "223644256",
"uploader": "BRUTTOBAND",
"uploader_id": "111488150",
"uploader_url": "https://soundcloud.com/bruttoband",
"timestamp": 1442140228,
"title": "BRUTTO - \u0420\u043e\u0434\u043d\u044b \u043a\u0440\u0430\u0439",
"description": "",
"thumbnails": [
{
"id": "mini",
"url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-mini.jpg",
"width": 16,
"height": 16
},
{
"id": "tiny",
"url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-tiny.jpg",
"width": 20,
"height": 20
},
{
"id": "small",
"url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-small.jpg",
"width": 32,
"height": 32
},
{
"id": "badge",
"url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-badge.jpg",
"width": 47,
"height": 47
},
{
"id": "t67x67",
"url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-t67x67.jpg",
"width": 67,
"height": 67
},
{
"id": "large",
"url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-large.jpg",
"width": 100,
"height": 100
},
{
"id": "t300x300",
"url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-t300x300.jpg",
"width": 300,
"height": 300
},
{
"id": "crop",
"url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-crop.jpg",
"width": 400,
"height": 400
},
{
"id": "t500x500",
"url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-t500x500.jpg",
"width": 500,
"height": 500
},
{
"id": "original",
"url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-original.jpg",
"preference": 10
}
],
"duration": 229.089,
"webpage_url": "https://soundcloud.com/bruttoband/brutto-11",
"license": "all-rights-reserved",
"view_count": 291779,
"like_count": 3347,
"comment_count": 14,
"repost_count": 59,
"genre": "Brutto",
"url": "https://cf-media.sndcdn.com/50remGX1OqRY.128.mp3?Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiKjovL2NmLW1lZGlhLnNuZGNkbi5jb20vNTByZW1HWDFPcVJZLjEyOC5tcDMqIiwiQ29uZGl0aW9uIjp7IkRhdGVMZXNzVGhhbiI6eyJBV1M6RXBvY2hUaW1lIjoxNjk0Nzk4NTkzfX19XX0_&Signature=flALJvEBnzS0ZOOhf0-07Ap~NURw2Gn2OqkeKKTTMX5HRGJw9eXFay79tcC4GsMMXWUgWoCx-n3yelpyilE2MOEIufBNUbjqRfMSJaX5YhYxjQdoDYuiU~gqBzJyPw9pKzr6P8~5HNKL3Idr0CNhUzdV6FQLaUPKMMibq9ghV833mUmdyvdk1~GZBc8MOg9GrTdcigGgpPzd-vrIMICMvFzFnwBOeOotxX2Vfqf9~wVekBKGlvB9A~7TlZ71lv9Fl9u4m8rse9E-mByweVc1M784ehJV3~tRPjuF~FXXWKP8x0nGJmoq7RAnG7iFIt~fQFmsfOq2o~PG7dHMRPh7hw__&Key-Pair-Id=APKAI6TU7MMXM5DG6EPQ",
"ext": "mp3",
"original_url": "https://soundcloud.com/bruttoband/brutto-11",
"webpage_url_basename": "brutto-11",
"webpage_url_domain": "soundcloud.com",
"extractor": "soundcloud",
"extractor_key": "Soundcloud",
"heatmap": [],
"automatic_captions": {}
}

View file

@ -0,0 +1,114 @@
{
"id": "223644255",
"uploader": "BRUTTOBAND",
"uploader_id": "111488150",
"uploader_url": "https://soundcloud.com/bruttoband",
"timestamp": 1442140228,
"title": "BRUTTO - \u0420\u043e\u0434\u043d\u044b \u043a\u0440\u0430\u0439",
"description": "",
"thumbnails": [
{
"id": "mini",
"url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-mini.jpg",
"width": 16,
"height": 16
},
{
"id": "tiny",
"url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-tiny.jpg",
"width": 20,
"height": 20
},
{
"id": "small",
"url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-small.jpg",
"width": 32,
"height": 32
},
{
"id": "badge",
"url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-badge.jpg",
"width": 47,
"height": 47
},
{
"id": "t67x67",
"url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-t67x67.jpg",
"width": 67,
"height": 67
},
{
"id": "large",
"url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-large.jpg",
"width": 100,
"height": 100
},
{
"id": "t300x300",
"url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-t300x300.jpg",
"width": 300,
"height": 300
},
{
"id": "crop",
"url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-crop.jpg",
"width": 400,
"height": 400
},
{
"id": "t500x500",
"url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-t500x500.jpg",
"width": 500,
"height": 500
},
{
"id": "original",
"url": "https://i1.sndcdn.com/artworks-000129385232-ysbisn-original.jpg",
"preference": 10
}
],
"duration": 229.089,
"webpage_url": "https://soundcloud.com/bruttoband/brutto-11",
"license": "all-rights-reserved",
"view_count": 291779,
"like_count": 3347,
"comment_count": 14,
"repost_count": 59,
"genre": "Brutto",
"formats": [
{
"url": "https://cf-hls-media.sndcdn.com/playlist/50remGX1OqRY.128.mp3/playlist.m3u8?Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiKjovL2NmLWhscy1tZWRpYS5zbmRjZG4uY29tL3BsYXlsaXN0LzUwcmVtR1gxT3FSWS4xMjgubXAzL3BsYXlsaXN0Lm0zdTgqIiwiQ29uZGl0aW9uIjp7IkRhdGVMZXNzVGhhbiI6eyJBV1M6RXBvY2hUaW1lIjoxNjk1NTAzOTYzfX19XX0_&Signature=eNeIoSTgRZL89YBJYXpmRg0AVGk3M0gV4E4rYPYbFw6pTePHO4o8Mv6HwdK85FOMsaUHZvYgzc35uWPhAr1SUqqjnm--xwN8VUrDkCPgdv97Vrs9qJ9QElHKnlWhK2-BDs3Y7sDcAurA00L2uReB-vjI-4K65WBApYBTaUGnOACimoVAOWHmtigO0Ap5DxlEh7fqqwi88enEvVDE-98v5uX9FcV9lq9AfVwEtfqbPsjVJyh6WbWAB3PJDJElvV13RgKmzVvbFluLElYlDud9WMsHjztdWhdaRzGOj1AfcQcwkQbQlBRiAKMtqrRlzAAXnBfLvMF3DOvdYWeCwJeCXA__&Key-Pair-Id=APKAI6TU7MMXM5DG6EPQ",
"ext": "mp3",
"abr": 128,
"format_id": "hls_mp3_128",
"protocol": "m3u8_native",
"preference": null,
"vcodec": "none"
},
{
"url": "https://cf-media.sndcdn.com/50remGX1OqRY.128.mp3?Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiKjovL2NmLW1lZGlhLnNuZGNkbi5jb20vNTByZW1HWDFPcVJZLjEyOC5tcDMqIiwiQ29uZGl0aW9uIjp7IkRhdGVMZXNzVGhhbiI6eyJBV1M6RXBvY2hUaW1lIjoxNjk1NTAzOTYzfX19XX0_&Signature=JAG~zJ~2NOWOgiuHLCSYWwdjUVuYWR2fBvmxPGSnLzMgX2xqu5~WfOk-gOyRUbHhnKnybUbP70cr6~t~Qx0KEU5mwIy2H0YhOXDHFX5RJVQlj1iCVuko-hAFJc7RtZuKTP5oCWOM-R2a6HfYN88YAIqgwWbGvTKin1CAgHaICeoM2p5O50n-kp05KgCw3RKcRutkYT-RVcWkmXtY4D4Jtw~LuBERDNyErseTHzmruDCkaYkVNlTcaIdgygQjgxVlgZiIRj-p0vRNO0qv5Bc0LfNMBzYm9fTAr86c~TzxyvQRhwHOPYp-DCXcs1K6i9x4BVvHWLOSHr0Dhd3X4fe5kw__&Key-Pair-Id=APKAI6TU7MMXM5DG6EPQ",
"ext": "mp3",
"abr": 128,
"format_id": "http_mp3_128",
"protocol": "http",
"preference": null,
"vcodec": "none"
},
{
"url": "https://cf-hls-opus-media.sndcdn.com/playlist/50remGX1OqRY.64.opus/playlist.m3u8?Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiKjovL2NmLWhscy1vcHVzLW1lZGlhLnNuZGNkbi5jb20vcGxheWxpc3QvNTByZW1HWDFPcVJZLjY0Lm9wdXMvcGxheWxpc3QubTN1OCoiLCJDb25kaXRpb24iOnsiRGF0ZUxlc3NUaGFuIjp7IkFXUzpFcG9jaFRpbWUiOjE2OTU1MDM5NjR9fX1dfQ__&Signature=HqyeKoK4gx1Bd3jId5v1-9ltDY2SN7fzGp6M7tW3kWluv8Ns0SgANPKG3~Tzo8hUrQzAYVvwMbO2F75b6NBeBr4xx0SRCeKotZdArzBFT4Wtrz4HtEddLyPjp12vWYCDuOshd1sTjrvaBAd9TAFTZuwyal1OKpLMsqK0QN-KFH-5GsmLDZTPWcoVkMqC7XBmNWTq0G1mtVeP57TN~9T7qEYqRLFIDXdm2HKSPSR4BB2gjaZUK22pBUPSGVr-ziBSpNVHvNISy2QdObjS5zjuAe8bl-npQ3PlFatYECDI3Gc~wjCNIJPdTpCbHUsX36SSR4dnKlgW1nYGx~eED7dppA__&Key-Pair-Id=APKAI6TU7MMXM5DG6EPQ",
"ext": "opus",
"abr": 64,
"format_id": "hls_opus_64",
"protocol": "m3u8_native",
"preference": null,
"vcodec": "none"
}
],
"original_url": "https://soundcloud.com/bruttoband/brutto-11",
"webpage_url_basename": "brutto-11",
"webpage_url_domain": "soundcloud.com",
"extractor": "soundcloud",
"extractor_key": "Soundcloud",
"heatmap": [],
"automatic_captions": {}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,49 @@
{
"id": "PLZ4DbyIWUwCq4V8bIEa8jm2ozHZVuREJO",
"title": "Very important videos",
"availability": "public",
"channel_follower_count": null,
"description": "Not original",
"tags": [],
"thumbnails": [
{
"url": "https://i.ytimg.com/vi/q6EoRBvdVPQ/hqdefault.jpg?sqp=-oaymwEwCKgBEF5IWvKriqkDIwgBFQAAiEIYAfABAfgB3gOAAugCigIMCAAQARg8IGUoPzAP&rs=AOn4CLDBCH5IQ0obogxXhAzIH8pE0d7r1Q",
"height": 94,
"width": 168
},
{
"url": "https://i.ytimg.com/vi/q6EoRBvdVPQ/hqdefault.jpg?sqp=-oaymwEwCMQBEG5IWvKriqkDIwgBFQAAiEIYAfABAfgB3gOAAugCigIMCAAQARg8IGUoPzAP&rs=AOn4CLAybhgn-CoPMjBE-0VfBDqvy0jyOQ",
"height": 110,
"width": 196
},
{
"url": "https://i.ytimg.com/vi/q6EoRBvdVPQ/hqdefault.jpg?sqp=-oaymwExCPYBEIoBSFryq4qpAyMIARUAAIhCGAHwAQH4Ad4DgALoAooCDAgAEAEYPCBlKD8wDw==&rs=AOn4CLDutIdjr5zTE9G78eWf83-mGXYnUA",
"height": 138,
"width": 246
},
{
"url": "https://i.ytimg.com/vi/q6EoRBvdVPQ/hqdefault.jpg?sqp=-oaymwExCNACELwBSFryq4qpAyMIARUAAIhCGAHwAQH4Ad4DgALoAooCDAgAEAEYPCBlKD8wDw==&rs=AOn4CLD2884fHuvAv8ysHA48LD3uArB6bA",
"height": 188,
"width": 336
}
],
"modified_date": "20230813",
"view_count": 5770834,
"playlist_count": 3,
"channel": "ZulTarx",
"channel_id": "UChOLuQpsxxmJiJUeSU2tSTw",
"uploader_id": "@Armand314",
"uploader": "ZulTarx",
"channel_url": "https://www.youtube.com/channel/UChOLuQpsxxmJiJUeSU2tSTw",
"uploader_url": "https://www.youtube.com/@Armand314",
"_type": "playlist",
"entries": [],
"extractor_key": "YoutubeTab",
"extractor": "youtube:tab",
"webpage_url": "https://www.youtube.com/playlist?list=PLZ4DbyIWUwCq4V8bIEa8jm2ozHZVuREJO",
"original_url": "https://www.youtube.com/playlist?list=PLZ4DbyIWUwCq4V8bIEa8jm2ozHZVuREJO",
"webpage_url_basename": "playlist",
"webpage_url_domain": "youtube.com",
"heatmap": [],
"automatic_captions": {}
}

View file

@ -0,0 +1,179 @@
{
"id": "PLZ4DbyIWUwCq4V8bIEa8jm2ozHZVuREJP",
"title": "Very important videos",
"availability": "public",
"channel_follower_count": null,
"description": "Not original",
"tags": [],
"thumbnails": [
{
"url": "https://i.ytimg.com/vi/q6EoRBvdVPQ/hqdefault.jpg?sqp=-oaymwEwCKgBEF5IWvKriqkDIwgBFQAAiEIYAfABAfgB3gOAAugCigIMCAAQARg8IGUoPzAP&rs=AOn4CLDBCH5IQ0obogxXhAzIH8pE0d7r1Q",
"height": 94,
"width": 168
},
{
"url": "https://i.ytimg.com/vi/q6EoRBvdVPQ/hqdefault.jpg?sqp=-oaymwEwCMQBEG5IWvKriqkDIwgBFQAAiEIYAfABAfgB3gOAAugCigIMCAAQARg8IGUoPzAP&rs=AOn4CLAybhgn-CoPMjBE-0VfBDqvy0jyOQ",
"height": 110,
"width": 196
},
{
"url": "https://i.ytimg.com/vi/q6EoRBvdVPQ/hqdefault.jpg?sqp=-oaymwExCPYBEIoBSFryq4qpAyMIARUAAIhCGAHwAQH4Ad4DgALoAooCDAgAEAEYPCBlKD8wDw==&rs=AOn4CLDutIdjr5zTE9G78eWf83-mGXYnUA",
"height": 138,
"width": 246
},
{
"url": "https://i.ytimg.com/vi/q6EoRBvdVPQ/hqdefault.jpg?sqp=-oaymwExCNACELwBSFryq4qpAyMIARUAAIhCGAHwAQH4Ad4DgALoAooCDAgAEAEYPCBlKD8wDw==&rs=AOn4CLD2884fHuvAv8ysHA48LD3uArB6bA",
"height": 188,
"width": 336
}
],
"modified_date": "20230813",
"view_count": 5770834,
"playlist_count": 3,
"channel": "ZulTarx",
"channel_id": "UChOLuQpsxxmJiJUeSU2tSTw",
"uploader_id": "@Armand314",
"uploader": "ZulTarx",
"channel_url": "https://www.youtube.com/channel/UChOLuQpsxxmJiJUeSU2tSTw",
"uploader_url": "https://www.youtube.com/@Armand314",
"_type": "playlist",
"entries": [
{
"_type": "url",
"ie_key": "Youtube",
"id": "q6EoRBvdVPQ",
"url": "https://www.youtube.com/watch?v=q6EoRBvdVPQ",
"title": "Yee",
"description": null,
"duration": 10,
"channel_id": "UC-fD_qwTEQQ1L-MUWx_mNvg",
"channel": "revergo",
"channel_url": "https://www.youtube.com/channel/UC-fD_qwTEQQ1L-MUWx_mNvg",
"uploader": "revergo",
"uploader_id": "@revergo",
"uploader_url": "https://www.youtube.com/@revergo",
"thumbnails": [
{
"url": "https://i.ytimg.com/vi/q6EoRBvdVPQ/hqdefault.jpg?sqp=-oaymwE1CKgBEF5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AHeA4AC6AKKAgwIABABGDwgZSg_MA8=&rs=AOn4CLAJYg16HMBdEsv9lYBJyNqA5G3anQ",
"height": 94,
"width": 168
},
{
"url": "https://i.ytimg.com/vi/q6EoRBvdVPQ/hqdefault.jpg?sqp=-oaymwE1CMQBEG5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AHeA4AC6AKKAgwIABABGDwgZSg_MA8=&rs=AOn4CLAgCNP9UuQas-D59hHHM-RqkUvA6g",
"height": 110,
"width": 196
},
{
"url": "https://i.ytimg.com/vi/q6EoRBvdVPQ/hqdefault.jpg?sqp=-oaymwE2CPYBEIoBSFXyq4qpAygIARUAAIhCGAFwAcABBvABAfgB3gOAAugCigIMCAAQARg8IGUoPzAP&rs=AOn4CLCTWaY5897XxhcpRyVtGQQNuMHfTg",
"height": 138,
"width": 246
},
{
"url": "https://i.ytimg.com/vi/q6EoRBvdVPQ/hqdefault.jpg?sqp=-oaymwE2CNACELwBSFXyq4qpAygIARUAAIhCGAFwAcABBvABAfgB3gOAAugCigIMCAAQARg8IGUoPzAP&rs=AOn4CLCeS6NC75yTYvyP4DsehZ3oXNuxMQ",
"height": 188,
"width": 336
}
],
"timestamp": null,
"release_timestamp": null,
"availability": null,
"view_count": 96000000,
"live_status": null,
"channel_is_verified": null
},
{
"_type": "url",
"ie_key": "Youtube",
"id": "8YWl7tDGUPA",
"url": "https://www.youtube.com/watch?v=8YWl7tDGUPA",
"title": "color red",
"description": null,
"duration": 17,
"channel_id": "UCbYMTn6xKV0IKshL4pRCV3g",
"channel": "Alex Jimenez",
"channel_url": "https://www.youtube.com/channel/UCbYMTn6xKV0IKshL4pRCV3g",
"uploader": "Alex Jimenez",
"uploader_id": "@alexjimenez1237",
"uploader_url": "https://www.youtube.com/@alexjimenez1237",
"thumbnails": [
{
"url": "https://i.ytimg.com/vi/8YWl7tDGUPA/hqdefault.jpg?sqp=-oaymwE1CKgBEF5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AG2BIACwAKKAgwIABABGGUgXShUMA8=&rs=AOn4CLBqzngIx-4i_HFvqloetUfeN8yrYw",
"height": 94,
"width": 168
},
{
"url": "https://i.ytimg.com/vi/8YWl7tDGUPA/hqdefault.jpg?sqp=-oaymwE1CMQBEG5IVfKriqkDKAgBFQAAiEIYAXABwAEG8AEB-AG2BIACwAKKAgwIABABGGUgXShUMA8=&rs=AOn4CLB7mWPQmdL2QBLxTHhrgbFj2jFaCg",
"height": 110,
"width": 196
},
{
"url": "https://i.ytimg.com/vi/8YWl7tDGUPA/hqdefault.jpg?sqp=-oaymwE2CPYBEIoBSFXyq4qpAygIARUAAIhCGAFwAcABBvABAfgBtgSAAsACigIMCAAQARhlIF0oVDAP&rs=AOn4CLA9YAIO3g_DnClsuc5LjMQn4O9ZQQ",
"height": 138,
"width": 246
},
{
"url": "https://i.ytimg.com/vi/8YWl7tDGUPA/hqdefault.jpg?sqp=-oaymwE2CNACELwBSFXyq4qpAygIARUAAIhCGAFwAcABBvABAfgBtgSAAsACigIMCAAQARhlIF0oVDAP&rs=AOn4CLDPHY6aG08hlTJMlc-LJt9ywtpWEg",
"height": 188,
"width": 336
}
],
"timestamp": null,
"release_timestamp": null,
"availability": null,
"view_count": 30000000,
"live_status": null,
"channel_is_verified": null
},
{
"_type": "url",
"ie_key": "Youtube",
"id": "6bnanI9jXps",
"url": "https://www.youtube.com/watch?v=6bnanI9jXps",
"title": "Terrible Mall Commercial",
"description": null,
"duration": 31,
"channel_id": "UCLmnB20wsih9F5N0o5K0tig",
"channel": "quantim",
"channel_url": "https://www.youtube.com/channel/UCLmnB20wsih9F5N0o5K0tig",
"uploader": "quantim",
"uploader_id": "@Potatoflesh",
"uploader_url": "https://www.youtube.com/@Potatoflesh",
"thumbnails": [
{
"url": "https://i.ytimg.com/vi/6bnanI9jXps/hqdefault.jpg?sqp=-oaymwEbCKgBEF5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLAsyI0ZJA9STG8vlSdRkKk55ls5Dg",
"height": 94,
"width": 168
},
{
"url": "https://i.ytimg.com/vi/6bnanI9jXps/hqdefault.jpg?sqp=-oaymwEbCMQBEG5IVfKriqkDDggBFQAAiEIYAXABwAEG&rs=AOn4CLD2bZ9S8AB4UGsZlx_8TjBoL72enA",
"height": 110,
"width": 196
},
{
"url": "https://i.ytimg.com/vi/6bnanI9jXps/hqdefault.jpg?sqp=-oaymwEcCPYBEIoBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLCKNlgvl_7lKoFq8vyDYZRtTs4woA",
"height": 138,
"width": 246
},
{
"url": "https://i.ytimg.com/vi/6bnanI9jXps/hqdefault.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg==&rs=AOn4CLBeZv8F8IyICmKD9qjo9pTMJmM8ug",
"height": 188,
"width": 336
}
],
"timestamp": null,
"release_timestamp": null,
"availability": null,
"view_count": 26000000,
"live_status": null,
"channel_is_verified": null
}
],
"extractor_key": "YoutubeTab",
"extractor": "youtube:tab",
"webpage_url": "https://www.youtube.com/playlist?list=PLZ4DbyIWUwCq4V8bIEa8jm2ozHZVuREJP",
"original_url": "https://www.youtube.com/playlist?list=PLZ4DbyIWUwCq4V8bIEa8jm2ozHZVuREJP",
"webpage_url_basename": "playlist",
"webpage_url_domain": "youtube.com",
"heatmap": [],
"automatic_captions": {}
}

View file

@ -1,4 +1,24 @@
# serializer version: 1 # serializer version: 1
# name: test_extract_media_service[https://soundcloud.com/bruttoband/brutto-11]
dict({
'url': 'https://cf-hls-opus-media.sndcdn.com/playlist/50remGX1OqRY.64.opus/playlist.m3u8?Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiKjovL2NmLWhscy1vcHVzLW1lZGlhLnNuZGNkbi5jb20vcGxheWxpc3QvNTByZW1HWDFPcVJZLjY0Lm9wdXMvcGxheWxpc3QubTN1OCoiLCJDb25kaXRpb24iOnsiRGF0ZUxlc3NUaGFuIjp7IkFXUzpFcG9jaFRpbWUiOjE2OTU1MDM5NjR9fX1dfQ__&Signature=HqyeKoK4gx1Bd3jId5v1-9ltDY2SN7fzGp6M7tW3kWluv8Ns0SgANPKG3~Tzo8hUrQzAYVvwMbO2F75b6NBeBr4xx0SRCeKotZdArzBFT4Wtrz4HtEddLyPjp12vWYCDuOshd1sTjrvaBAd9TAFTZuwyal1OKpLMsqK0QN-KFH-5GsmLDZTPWcoVkMqC7XBmNWTq0G1mtVeP57TN~9T7qEYqRLFIDXdm2HKSPSR4BB2gjaZUK22pBUPSGVr-ziBSpNVHvNISy2QdObjS5zjuAe8bl-npQ3PlFatYECDI3Gc~wjCNIJPdTpCbHUsX36SSR4dnKlgW1nYGx~eED7dppA__&Key-Pair-Id=APKAI6TU7MMXM5DG6EPQ',
})
# ---
# name: test_extract_media_service[https://test.com/abc]
dict({
'url': 'https://cf-media.sndcdn.com/50remGX1OqRY.128.mp3?Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiKjovL2NmLW1lZGlhLnNuZGNkbi5jb20vNTByZW1HWDFPcVJZLjEyOC5tcDMqIiwiQ29uZGl0aW9uIjp7IkRhdGVMZXNzVGhhbiI6eyJBV1M6RXBvY2hUaW1lIjoxNjk0Nzk4NTkzfX19XX0_&Signature=flALJvEBnzS0ZOOhf0-07Ap~NURw2Gn2OqkeKKTTMX5HRGJw9eXFay79tcC4GsMMXWUgWoCx-n3yelpyilE2MOEIufBNUbjqRfMSJaX5YhYxjQdoDYuiU~gqBzJyPw9pKzr6P8~5HNKL3Idr0CNhUzdV6FQLaUPKMMibq9ghV833mUmdyvdk1~GZBc8MOg9GrTdcigGgpPzd-vrIMICMvFzFnwBOeOotxX2Vfqf9~wVekBKGlvB9A~7TlZ71lv9Fl9u4m8rse9E-mByweVc1M784ehJV3~tRPjuF~FXXWKP8x0nGJmoq7RAnG7iFIt~fQFmsfOq2o~PG7dHMRPh7hw__&Key-Pair-Id=APKAI6TU7MMXM5DG6EPQ',
})
# ---
# name: test_extract_media_service[https://www.youtube.com/playlist?list=PLZ4DbyIWUwCq4V8bIEa8jm2ozHZVuREJP]
dict({
'url': 'https://www.youtube.com/watch?v=q6EoRBvdVPQ',
})
# ---
# name: test_extract_media_service[https://www.youtube.com/watch?v=dQw4w9WgXcQ]
dict({
'url': 'https://rr2---sn-5hnekn7k.googlevideo.com/videoplayback?expire=1695524274&ei=UlEPZeSINMKn6dsPt6mr8Ac&ip=45.93.75.130&id=o-AA7xSUDqvzXPAn6m_Jd8d0jhyk4er7c5zjbyZCHyREaQ&itag=22&source=youtube&requiressl=yes&mh=7c&mm=31%2C29&mn=sn-5hnekn7k%2Csn-5hne6nzy&ms=au%2Crdu&mv=m&mvi=2&pl=22&initcwndbps=1311250&spc=UWF9f6JvGMIlJJFOWUL6L4bH2pl28C4&vprv=1&svpuc=1&mime=video%2Fmp4&cnr=14&ratebypass=yes&dur=212.091&lmt=1694045086815467&mt=1695502401&fvip=3&fexp=24007246&beids=24350018&c=ANDROID&txp=4532434&sparams=expire%2Cei%2Cip%2Cid%2Citag%2Csource%2Crequiressl%2Cspc%2Cvprv%2Csvpuc%2Cmime%2Ccnr%2Cratebypass%2Cdur%2Clmt&sig=AOq0QJ8wRAIgHVwajP0J2fiJ1ERoAonpxghXGzDmEXh3rvJ399UEMWECIFdBjiVUOk7QdiFBxQ4QqojJd8p_PfL25TV_8TBrp_Kb&lsparams=mh%2Cmm%2Cmn%2Cms%2Cmv%2Cmvi%2Cpl%2Cinitcwndbps&lsig=AG3C_xAwRgIhANJRDHqeYwWk4W8XXGlTNmYA3OOiuzK3PEWtVpK4NimlAiEAwK0nj8vGtcP0IJMZVBGh8SC5KRFzPfx_yJn41ZOUGyQ%3D',
})
# ---
# name: test_no_target_entity # name: test_no_target_entity
ReadOnlyDict({ ReadOnlyDict({
'device_id': list([ 'device_id': list([

View file

@ -9,9 +9,14 @@ import pytest
from syrupy import SnapshotAssertion from syrupy import SnapshotAssertion
from yt_dlp import DownloadError from yt_dlp import DownloadError
from homeassistant.components.media_extractor import DOMAIN from homeassistant.components.media_extractor.const import (
ATTR_URL,
DOMAIN,
SERVICE_EXTRACT_MEDIA_URL,
)
from homeassistant.components.media_player import SERVICE_PLAY_MEDIA from homeassistant.components.media_player import SERVICE_PLAY_MEDIA
from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import HomeAssistantError
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from tests.common import load_json_object_fixture from tests.common import load_json_object_fixture
@ -30,6 +35,58 @@ async def test_play_media_service_is_registered(hass: HomeAssistant) -> None:
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.services.has_service(DOMAIN, SERVICE_PLAY_MEDIA) assert hass.services.has_service(DOMAIN, SERVICE_PLAY_MEDIA)
assert hass.services.has_service(DOMAIN, SERVICE_EXTRACT_MEDIA_URL)
@pytest.mark.parametrize(
"url",
[
YOUTUBE_VIDEO,
SOUNDCLOUD_TRACK,
NO_FORMATS_RESPONSE,
YOUTUBE_PLAYLIST,
],
)
async def test_extract_media_service(
hass: HomeAssistant,
mock_youtube_dl: MockYoutubeDL,
snapshot: SnapshotAssertion,
empty_media_extractor_config: dict[str, Any],
url: str,
) -> None:
"""Test play media service is registered."""
await async_setup_component(hass, DOMAIN, empty_media_extractor_config)
await hass.async_block_till_done()
assert (
await hass.services.async_call(
DOMAIN,
SERVICE_EXTRACT_MEDIA_URL,
{ATTR_URL: url},
blocking=True,
return_response=True,
)
== snapshot
)
async def test_extracting_playlist_no_entries(
hass: HomeAssistant,
mock_youtube_dl: MockYoutubeDL,
empty_media_extractor_config: dict[str, Any],
) -> None:
"""Test extracting a playlist without entries."""
await async_setup_component(hass, DOMAIN, empty_media_extractor_config)
await hass.async_block_till_done()
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
DOMAIN,
SERVICE_EXTRACT_MEDIA_URL,
{ATTR_URL: YOUTUBE_EMPTY_PLAYLIST},
blocking=True,
return_response=True,
)
@pytest.mark.parametrize( @pytest.mark.parametrize(