From 4d7e3cc4104ef782110acfd439fa877b15407beb Mon Sep 17 00:00:00 2001 From: Jonny Bergdahl Date: Fri, 8 Mar 2024 09:08:43 +0100 Subject: [PATCH 1/4] Add new Consent cookie Add debug logging Code cleanup --- custom_components/youtube/manifest.json | 2 +- custom_components/youtube/sensor.py | 96 +++++++++++++------------ 2 files changed, 53 insertions(+), 45 deletions(-) diff --git a/custom_components/youtube/manifest.json b/custom_components/youtube/manifest.json index b3470cf..bc295d3 100644 --- a/custom_components/youtube/manifest.json +++ b/custom_components/youtube/manifest.json @@ -1,7 +1,7 @@ { "domain": "youtube", "name": "Youtube Sensor", - "version": "0.0.0", + "version": "0.0.1", "documentation": "https://github.com/custom-components/youtube", "dependencies": [], "codeowners": ["@pinkywafer"], diff --git a/custom_components/youtube/sensor.py b/custom_components/youtube/sensor.py index 74af357..1029c3c 100644 --- a/custom_components/youtube/sensor.py +++ b/custom_components/youtube/sensor.py @@ -17,11 +17,10 @@ import html CONF_CHANNEL_ID = 'channel_id' - ICON = 'mdi:youtube' - BASE_URL = 'https://www.youtube.com/feeds/videos.xml?channel_id={}' CHANNEL_LIVE_URL = 'https://www.youtube.com/channel/{}' +COOKIES = {"SOCS": "CAESEwgDEgk0ODE3Nzk3MjQaAmVuIAEaBgiA_LyaBg"} PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_CHANNEL_ID): cv.string, @@ -34,19 +33,22 @@ async def async_setup_platform( hass, config, async_add_entities, discovery_info=None): # pylint: disable=unused-argument """Setup sensor platform.""" channel_id = config['channel_id'] + _LOGGER.debug(f'Setting up {channel_id}') session = async_create_clientsession(hass) try: url = BASE_URL.format(channel_id) async with async_timeout.timeout(10): response = await session.get(url) info = await response.text() - name = info.split('')[1].split('</')[0] + name = info.split('<title>')[1].split('</')[0] except Exception as error: # pylint: disable=broad-except - _LOGGER.debug('Unable to set up - %s', error) + _LOGGER.error(f'Unable to set up {channel_id} - {error}') name = None if name is not None: - async_add_entities([YoutubeSensor(channel_id, name, session)], True) + sensor = YoutubeSensor(channel_id, name, session) + async_add_entities([sensor], True) + class YoutubeSensor(Entity): """YouTube Sensor class""" @@ -70,7 +72,7 @@ def __init__(self, channel_id, name, session): async def async_update(self): """Update sensor.""" - _LOGGER.debug('%s - Running update', self._name) + _LOGGER.debug(f'{self._name} - Running update') try: url = BASE_URL.format(self.channel_id) async with async_timeout.timeout(10): @@ -83,9 +85,9 @@ async def async_update(self): title = info.split('<title>')[2].split('</')[0] url = info.split('<link rel="alternate" href="')[2].split('"/>')[0] if self.live or url != self.url: - self.stream, self.live, self.stream_start = await is_live(url, self._name, self.hass, self.session) + self.stream, self.live, self.stream_start = await self.is_live(url) else: - _LOGGER.debug('%s - Skipping live check', self._name) + _LOGGER.debug(f'{self._name} - Skipping live check') self.url = url self.content_id = url.split('?v=')[1] self.published = info.split('<published>')[2].split('</')[0] @@ -95,10 +97,10 @@ async def async_update(self): self._image = thumbnail_url self.stars = info.split('<media:starRating count="')[1].split('"')[0] self.views = info.split('<media:statistics views="')[1].split('"')[0] - url = CHANNEL_LIVE_URL.format(self.channel_id) - self.channel_live, self.channel_image = await is_channel_live(url, self.name, self.hass, self.session) + + self.channel_live, self.channel_image = await self.is_channel_live() except Exception as error: # pylint: disable=broad-except - _LOGGER.debug('%s - Could not update - %s', self._name, error) + _LOGGER.debug(f'{self._name} - Could not update - {error}') @property def name(self): @@ -139,37 +141,43 @@ def extra_state_attributes(self): 'channel_is_live': self.channel_live, 'channel_image': self.channel_image} -async def is_live(url, name, hass, session): - """Return bool if video is stream and bool if video is live""" - live = False - stream = False - start = None - try: - async with async_timeout.timeout(10): - response = await session.get(url, cookies=dict(CONSENT="YES+cb")) - info = await response.text() - if 'isLiveBroadcast' in info: - stream = True - start = parse(info.split('startDate" content="')[1].split('"')[0]) - if 'endDate' not in info: + async def is_live(self, url): + """Return bool if video is stream and bool if video is live""" + live = False + stream = False + start = None + try: + async with async_timeout.timeout(10): + response = await self.session.get(url) + html = await response.text() + if 'isLiveBroadcast' in html: + stream = True + start = parse(html.split('startDate" content="')[1].split('"')[0]) + if 'endDate' not in html: + live = True + _LOGGER.debug(f'{self._name} - Latest Video is live') + except Exception as error: # pylint: disable=broad-except + _LOGGER.debug(f'{self._name} - is_live(): Error {error}') + return stream, live, start + + async def is_channel_live(self): + """Return bool if channel is live""" + live = False + channel_image = None + url = CHANNEL_LIVE_URL.format(self.channel_id) + try: + _LOGGER.debug("GET %s: %s", self._name, url) + async with async_timeout.timeout(10): + response = await self.session.get(url, cookies = COOKIES) + html = await response.text() + if '{"iconType":"LIVE"}' in html: live = True - _LOGGER.debug('%s - Latest Video is live', name) - except Exception as error: # pylint: disable=broad-except - _LOGGER.debug('%s - Could not update - %s', name, error) - return stream, live, start - -async def is_channel_live(url, name, hass, session): - """Return bool if channel is live""" - live = False - try: - async with async_timeout.timeout(10): - response = await session.get(url, cookies=dict(CONSENT="YES+cb")) - info = await response.text() - if '{"iconType":"LIVE"}' in info: - live = True - _LOGGER.debug('%s - Channel is live', name) - regex = r"\"width\":48,\"height\":48},{\"url\":\"(.*?)\",\"width\":88,\"height\":88},{\"url\":" - channel_image = re.findall(regex, info, re.MULTILINE)[0].replace("=s88-c-k-c0x00ffffff-no-rj", "") - except Exception as error: # pylint: disable=broad-except - _LOGGER.debug('%s - Could not update - %s', name, error) - return live, channel_image + _LOGGER.debug(f'{self._name} - Channel is live') + regex = r"\"width\":48,\"height\":48},{\"url\":\"(.*?)\",\"width\":88,\"height\":88},{\"url\":" + found = re.findall(regex, html, re.MULTILINE) + if found: + channel_image = found[0] + channel_image = channel_image.replace("=s88-c-k-c0x00ffffff-no-rj", "") + except Exception as error: # pylint: disable=broad-except + _LOGGER.debug(f'{self._name} - is_channel_live(): Error {error}') + return live, channel_image From c6238ec760440294e56767156ed139ffa45c886e Mon Sep 17 00:00:00 2001 From: Jonny Bergdahl <bergdahl@users.noreply.github.com> Date: Fri, 8 Mar 2024 10:51:16 +0100 Subject: [PATCH 2/4] Added support for @ChannelName style channel id's. --- custom_components/youtube/sensor.py | 32 +++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/custom_components/youtube/sensor.py b/custom_components/youtube/sensor.py index 1029c3c..f64816a 100644 --- a/custom_components/youtube/sensor.py +++ b/custom_components/youtube/sensor.py @@ -18,7 +18,8 @@ CONF_CHANNEL_ID = 'channel_id' ICON = 'mdi:youtube' -BASE_URL = 'https://www.youtube.com/feeds/videos.xml?channel_id={}' +CHANNEL_URL = "https://www.youtube.com/{}" +RSS_URL = 'https://www.youtube.com/feeds/videos.xml?channel_id={}' CHANNEL_LIVE_URL = 'https://www.youtube.com/channel/{}' COOKIES = {"SOCS": "CAESEwgDEgk0ODE3Nzk3MjQaAmVuIAEaBgiA_LyaBg"} @@ -35,8 +36,10 @@ async def async_setup_platform( channel_id = config['channel_id'] _LOGGER.debug(f'Setting up {channel_id}') session = async_create_clientsession(hass) + if channel_id.startswith('@'): + channel_id = await get_channel_id(session, channel_id) try: - url = BASE_URL.format(channel_id) + url = RSS_URL.format(channel_id) async with async_timeout.timeout(10): response = await session.get(url) info = await response.text() @@ -50,8 +53,29 @@ async def async_setup_platform( async_add_entities([sensor], True) +async def get_channel_id(session, user_name): + channel_id = None + url = CHANNEL_URL.format(user_name) + _LOGGER.debug("Trying %s", url) + try: + async with async_timeout.timeout(10): + response = await session.get(url, cookies=COOKIES) + html = await response.text() + regex = r"<link rel=\"alternate\" type=\"application/rss\+xml\" title=\"RSS\" href=\"(.*?)\">" + found = re.findall(regex, html, re.MULTILINE) + if found: + strings = found[0].split("=") + channel_id = strings[1] + except Exception as error: # pylint: disable=broad-except + _LOGGER.debug(f'{user_name} - get_channel_id(): Error {error}') + + _LOGGER.debug("Channel id for name %s: %s", user_name, channel_id) + return channel_id + + class YoutubeSensor(Entity): """YouTube Sensor class""" + def __init__(self, channel_id, name, session): self._state = None self.session = session @@ -74,7 +98,7 @@ async def async_update(self): """Update sensor.""" _LOGGER.debug(f'{self._name} - Running update') try: - url = BASE_URL.format(self.channel_id) + url = RSS_URL.format(self.channel_id) async with async_timeout.timeout(10): response = await self.session.get(url) info = await response.text() @@ -168,7 +192,7 @@ async def is_channel_live(self): try: _LOGGER.debug("GET %s: %s", self._name, url) async with async_timeout.timeout(10): - response = await self.session.get(url, cookies = COOKIES) + response = await self.session.get(url, cookies=COOKIES) html = await response.text() if '{"iconType":"LIVE"}' in html: live = True From d3550a8627d828927abccad2e818865062e1306d Mon Sep 17 00:00:00 2001 From: Jonny Bergdahl <bergdahl@users.noreply.github.com> Date: Fri, 8 Mar 2024 14:22:58 +0100 Subject: [PATCH 3/4] Removed async_timeout after consulting with @joostlek --- custom_components/youtube/sensor.py | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/custom_components/youtube/sensor.py b/custom_components/youtube/sensor.py index f64816a..e0ef959 100644 --- a/custom_components/youtube/sensor.py +++ b/custom_components/youtube/sensor.py @@ -6,7 +6,6 @@ """ import logging -import async_timeout import voluptuous as vol import homeassistant.helpers.config_validation as cv from homeassistant.helpers.aiohttp_client import async_create_clientsession @@ -40,10 +39,9 @@ async def async_setup_platform( channel_id = await get_channel_id(session, channel_id) try: url = RSS_URL.format(channel_id) - async with async_timeout.timeout(10): - response = await session.get(url) - info = await response.text() - name = info.split('<title>')[1].split('</')[0] + response = await session.get(url) + info = await response.text() + name = info.split('<title>')[1].split('</')[0] except Exception as error: # pylint: disable=broad-except _LOGGER.error(f'Unable to set up {channel_id} - {error}') name = None @@ -58,9 +56,8 @@ async def get_channel_id(session, user_name): url = CHANNEL_URL.format(user_name) _LOGGER.debug("Trying %s", url) try: - async with async_timeout.timeout(10): - response = await session.get(url, cookies=COOKIES) - html = await response.text() + response = await session.get(url, cookies=COOKIES) + html = await response.text() regex = r"<link rel=\"alternate\" type=\"application/rss\+xml\" title=\"RSS\" href=\"(.*?)\">" found = re.findall(regex, html, re.MULTILINE) if found: @@ -99,9 +96,8 @@ async def async_update(self): _LOGGER.debug(f'{self._name} - Running update') try: url = RSS_URL.format(self.channel_id) - async with async_timeout.timeout(10): - response = await self.session.get(url) - info = await response.text() + response = await self.session.get(url) + info = await response.text() exp = parse(response.headers['Expires']) if exp < self.expiry: return @@ -171,9 +167,8 @@ async def is_live(self, url): stream = False start = None try: - async with async_timeout.timeout(10): - response = await self.session.get(url) - html = await response.text() + response = await self.session.get(url) + html = await response.text() if 'isLiveBroadcast' in html: stream = True start = parse(html.split('startDate" content="')[1].split('"')[0]) @@ -191,9 +186,8 @@ async def is_channel_live(self): url = CHANNEL_LIVE_URL.format(self.channel_id) try: _LOGGER.debug("GET %s: %s", self._name, url) - async with async_timeout.timeout(10): - response = await self.session.get(url, cookies=COOKIES) - html = await response.text() + response = await self.session.get(url, cookies=COOKIES) + html = await response.text() if '{"iconType":"LIVE"}' in html: live = True _LOGGER.debug(f'{self._name} - Channel is live') From c652261552af75145e9c348279abd720dca9cadb Mon Sep 17 00:00:00 2001 From: Jonny Bergdahl <bergdahl@users.noreply.github.com> Date: Wed, 13 Mar 2024 20:45:26 +0100 Subject: [PATCH 4/4] Update README for channel name --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 02308af..787449c 100644 --- a/README.md +++ b/README.md @@ -27,12 +27,22 @@ To get started put all the files from`/custom_components/youtube/` here: ## Example configuration.yaml +Using the old style channel id. + ```yaml sensor: platform: youtube channel_id: UCZ2Ku6wrhdYDHCaBzLaA3bw ``` +Or using new style channel name (_Note: You need to enclose the channel name in quotes!_) + +```yaml +sensor: + platform: youtube + channel_id: '@frenck' +``` + ## Configuration variables key | type | description