Initial commit

This commit is contained in:
Deko
2023-03-03 20:38:17 +01:00
commit 61435c69e7
18 changed files with 707 additions and 0 deletions

0
app/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

123
app/discord_client.py Normal file
View File

@@ -0,0 +1,123 @@
import os
import requests
from loguru import logger
from twitch_client import StreamInformation
class DiscordClient:
_notification_msg_id: str = ""
def __init__(self):
self._webhook_url = os.environ["DISCORD_WEBHOOK_URL"]
def send_information_to_discord(
self, stream: StreamInformation, profile_image: str
) -> str:
logger.info("Sending a message with an embed to the webhook...")
streamer_url = f"https://www.twitch.tv/{stream.user_login}/"
response = requests.post(
url=f"{self._webhook_url}?wait=true",
json={
"username": "Oak Tree",
"avatar_url": "https://i.imgur.com/DBOuwjx.png",
"content": "@everyone",
"embeds": [
{
"title": stream.title,
"color": 8388863,
"timestamp": stream.started_at,
"url": streamer_url,
"author": {
"name": stream.user_name,
"url": streamer_url,
"icon_url": profile_image,
},
"image": {"url": stream.thumbnail_url},
"fields": [
{
"name": "Game",
"value": stream.game_name,
"inline": True,
},
{
"name": "Viewers",
"value": stream.viewer_count,
"inline": True,
},
],
}
],
},
)
response.raise_for_status()
self._notification_msg_id = response.json()["id"]
logger.info("Stream information sent with ping to Discord.")
return self._notification_msg_id
def update_information_on_discord(
self, stream: StreamInformation, profile_image: str
) -> None:
logger.info("Updating stream information on Discord...")
streamer_url = f"https://www.twitch.tv/{stream.user_login}/"
response = requests.patch(
url=f"{self._webhook_url}/messages/{self._notification_msg_id}",
json={
"embeds": [
{
"title": stream.title,
"color": 8388863,
"timestamp": stream.started_at,
"url": streamer_url,
"author": {
"name": f"{stream.user_name}",
"url": streamer_url,
"icon_url": profile_image,
},
"image": {"url": stream.thumbnail_url},
"fields": [
{
"name": "Game",
"value": stream.game_name,
"inline": True,
},
{
"name": "Viewers",
"value": stream.viewer_count,
"inline": True,
},
],
}
],
},
)
response.raise_for_status()
logger.info("Message embed content updated.")
def finalize_information_on_discord(
self, streamer_name, vod_url: str | None
) -> None:
logger.info("Finalizing stream information on Discord...")
if not self._notification_msg_id:
logger.info("Message ID not set, nothing to finalize.")
return
if not vod_url:
vod_url = "None available. Please contact the developer."
response = requests.patch(
url=f"{self._webhook_url}/messages/{self._notification_msg_id}",
json={
"username": "Oak Tree",
"avatar_url": "https://i.imgur.com/DBOuwjx.png",
"content": (
f"{streamer_name} stopped the stream. VOD: \n{vod_url}"
),
"embeds": [],
},
)
response.raise_for_status()
logger.info("Message updated with VOD.")

79
app/main.py Normal file
View File

@@ -0,0 +1,79 @@
import os
import time
from loguru import logger
from requests import HTTPError
from discord_client import DiscordClient
from twitch_client import TwitchClient, StreamInformation
class Main:
is_live: bool = False
_previous_stream: StreamInformation = None
def __init__(self):
self.twitch_client = TwitchClient(streamer=os.environ["STREAMER_NAME"])
self.twitch_client.update_access_token()
self.profile_image = self.twitch_client.get_streamer_profile_picture()
self.discord_client = DiscordClient()
def update_status(self):
try:
stream = self.twitch_client.get_stream()
if not stream:
if self.is_live:
logger.info("Streamer went offline.")
self.is_live = False
self.discord_client.finalize_information_on_discord(
streamer_name=self._previous_stream.user_name,
vod_url=self.twitch_client.get_vod(
user_id=self._previous_stream.user_id
),
)
return
if not self.is_live:
logger.info("Streamer went live.")
self.discord_client.send_information_to_discord(
stream=stream, profile_image=self.profile_image
)
self.is_live = True
else:
logger.info("Streamer still live.")
self.discord_client.update_information_on_discord(
stream=stream, profile_image=self.profile_image
)
self._previous_stream = stream
except HTTPError as e:
logger.exception(e)
def interrupt(self):
if not self.is_live:
return
self.is_live = False
self.discord_client.finalize_information_on_discord(
streamer_name=self._previous_stream.user_login,
vod_url=self.twitch_client.get_vod(
user_id=self._previous_stream.user_id
),
)
if __name__ == "__main__":
logger.info("Initiating main...")
start_time = time.time()
main = Main()
logger.info("Set-up looks correct, starting main loop.")
try:
while True:
main.update_status()
time.sleep(30.0 - ((time.time() - start_time) % 30.0))
except (SystemExit, KeyboardInterrupt):
logger.info("Caught wish to exit, interrupting and re-raising.")
main.interrupt()
raise

180
app/twitch_client.py Normal file
View File

@@ -0,0 +1,180 @@
import os
import random
from dataclasses import dataclass
import requests
from loguru import logger
from requests import HTTPError
@dataclass
class CachePrevent:
calls: int = 0
random_number: int = 0
five_minute_update_modulo: int = 10
def prevent_cache_on_url(self, url: str) -> str:
self.calls += 1
if self.calls % self.five_minute_update_modulo == 0:
self.random_number = random.randint(0, 999999)
return f"{url}?{self.random_number}"
@dataclass
class StreamInformation:
user_id: str
user_name: str
user_login: str
title: str
game_name: str
viewer_count: int
started_at: str
_thumbnail_url: str
@property
def thumbnail_url(self):
return (
self._thumbnail_url
.replace("{width}", "1280")
.replace("{height}", "720")
)
class TwitchClient:
_access_token: str = ""
def __init__(self, streamer: str):
self.streamer = streamer
self._client_id = os.environ["TWITCH_CLIENT_ID"]
self._client_secret = os.environ["TWITCH_CLIENT_SECRET"]
self._cache_prevent = CachePrevent()
def update_access_token(self) -> None:
logger.info("Updating twitch access token...")
response = requests.post(
url="https://id.twitch.tv/oauth2/token",
headers={"Content-Type": "application/x-www-form-url-encoded"},
params={
"client_id": self._client_id,
"client_secret": self._client_secret,
"grant_type": "client_credentials",
},
)
response.raise_for_status()
self._access_token = response.json()["access_token"]
logger.info("Updating twitch access token done.")
def _update_access_token_wrapper(self) -> bool:
try:
self.update_access_token()
except HTTPError as e:
logger.error("API call to update twitch access token failed.")
logger.exception(e)
return False
except KeyError:
logger.error("Access token was not in auth response.")
return False
return True
def get_streamer_profile_picture(
self, is_retry: bool = False
) -> str | None:
response = requests.get(
url="https://api.twitch.tv/helix/users",
headers={
"Client-Id": self._client_id,
"Authorization": f"Bearer {self._access_token}",
},
params={"login": self.streamer},
)
if response.status_code == 401:
logger.info("Getting streamer returned an auth issue.")
if is_retry:
logger.error("Auth failed twice, aborting.")
return None
if not self._update_access_token_wrapper():
return None
return self.get_streamer_profile_picture(is_retry=True)
response.raise_for_status()
return response.json()["data"][0]["profile_image_url"]
def get_stream(self, is_retry: bool = False) -> StreamInformation | None:
response = requests.get(
url="https://api.twitch.tv/helix/streams",
headers={
"Client-Id": self._client_id,
"Authorization": f"Bearer {self._access_token}",
},
params={"user_login": self.streamer},
)
if response.status_code == 401:
logger.info("Getting streams returned an auth issue.")
if is_retry:
logger.error("Auth failed twice, aborting.")
return None
if not self._update_access_token_wrapper():
return None
return self.get_stream(is_retry=True)
response.raise_for_status()
streams = response.json()["data"]
if len(streams) == 0:
return None
stream_data = streams[0]
return StreamInformation(
user_id=stream_data.get("user_id"),
user_name=stream_data.get("user_name"),
user_login=stream_data.get("user_login"),
title=stream_data.get("title"),
game_name=stream_data.get("game_name"),
viewer_count=stream_data.get("viewer_count"),
started_at=stream_data.get("started_at"),
_thumbnail_url=self._cache_prevent.prevent_cache_on_url(
url=stream_data.get("thumbnail_url"),
)
)
def get_vod(self, user_id: str, is_retry: bool = False) -> str | None:
response = requests.get(
url="https://api.twitch.tv/helix/videos",
headers={
"Client-Id": self._client_id,
"Authorization": f"Bearer {self._access_token}",
},
params={"user_id": user_id},
)
if response.status_code == 401:
logger.info("Getting vod returned an auth issue.")
if is_retry:
logger.error("Auth failed twice, aborting.")
return None
if not self._update_access_token_wrapper():
return None
return self.get_vod(user_id=user_id, is_retry=True)
response.raise_for_status()
vods = response.json()["data"]
if len(vods) == 0:
return None
vod_data = vods[0]
return vod_data.get("url")