Remove processing of tags;

- Removes all logic related to stream tags processing
- Implements a new method of fetching live non-ACL channels, with drops enabled
- Minor logic cleanup changes
This commit is contained in:
DevilXD committed 2023-10-15 12:06:58 +02:00
1 parent 67cfc6603d
commit 5e862f9543
4 files changed
+54 -22

No files matched your search

+10 -10
View File
@@ -9,7 +9,7 @@ from typing import Any, SupportsInt, TYPE_CHECKING
from utils import invalidate_cache, json_minify, Game from utils import invalidate_cache, json_minify, Game
from exceptions import MinerException, RequestException from exceptions import MinerException, RequestException
from constants import BASE_URL, GQL_OPERATIONS, ONLINE_DELAY, DROPS_ENABLED_TAG, URLType from constants import BASE_URL, GQL_OPERATIONS, ONLINE_DELAY, URLType
if TYPE_CHECKING: if TYPE_CHECKING:
from twitch import Twitch from twitch import Twitch
@@ -31,12 +31,11 @@ class Stream:
game: JsonType | None, game: JsonType | None,
viewers: int, viewers: int,
title: str, title: str,
tags: list[JsonType],
): ):
self.channel: Channel = channel self.channel: Channel = channel
self.broadcast_id = int(id) self.broadcast_id = int(id)
self.viewers: int = viewers self.viewers: int = viewers
self.drops_enabled: bool = any(t["id"] == DROPS_ENABLED_TAG for t in tags) self.drops_enabled: bool = False
self.game: Game | None = Game(game) if game else None self.game: Game | None = Game(game) if game else None
self.title: str = title self.title: str = title
@@ -50,21 +49,20 @@ class Stream:
game=settings["game"], game=settings["game"],
viewers=stream["viewersCount"], viewers=stream["viewersCount"],
title=settings["title"], title=settings["title"],
tags=stream["tags"] or [],
) )
@classmethod @classmethod
def from_directory(cls, channel: Channel, data: JsonType) -> Stream: def from_directory(
cls, channel: Channel, data: JsonType, *, drops_enabled: bool = False
) -> Stream:
self = cls( self = cls(
channel, channel,
id=data["id"], id=data["id"],
game=data["game"], # has to be there since we searched with it game=data["game"], # has to be there since we searched with it
viewers=data["viewersCount"], viewers=data["viewersCount"],
title=data["title"], title=data["title"],
tags=[],
) )
# HACK: we assume this is always the case for directory streams, since we search by the tag self.drops_enabled = drops_enabled
# self.drops_enabled = True
return self return self
def __eq__(self, other: object) -> bool: def __eq__(self, other: object) -> bool:
@@ -109,12 +107,14 @@ class Channel:
) )
@classmethod @classmethod
def from_directory(cls, twitch: Twitch, data: JsonType) -> Channel: def from_directory(
cls, twitch: Twitch, data: JsonType, *, drops_enabled: bool = False
) -> Channel:
channel = data["broadcaster"] channel = data["broadcaster"]
self = cls( self = cls(
twitch, id=channel["id"], login=channel["login"], display_name=channel["displayName"] twitch, id=channel["id"], login=channel["login"], display_name=channel["displayName"]
) )
self._stream = Stream.from_directory(self, data) self._stream = Stream.from_directory(self, data, drops_enabled=drops_enabled)
return self return self
@classmethod @classmethod
+31 -5
View File
@@ -48,6 +48,30 @@ def _resource_path(relative_path: Path | str) -> Path:
return base_path.joinpath(relative_path) return base_path.joinpath(relative_path)
def _merge_vars(base_vars: JsonType, vars: JsonType) -> None:
# NOTE: This modifies base in place
for k, v in vars.items():
if k not in base_vars:
base_vars[k] = v
elif isinstance(v, dict):
if isinstance(base_vars[k], dict):
_merge_vars(base_vars[k], v)
elif base_vars[k] is Ellipsis:
# unspecified base, use the passed in var
base_vars[k] = v
else:
raise RuntimeError(f"Var is a dict, base is not: '{k}'")
elif isinstance(base_vars[k], dict):
raise RuntimeError(f"Base is a dict, var is not: '{k}'")
else:
# simple overwrite
base_vars[k] = v
# ensure none of the vars are ellipsis (unset value)
for k, v in base_vars.items():
if v is Ellipsis:
raise RuntimeError(f"Unspecified variable: '{k}'")
# Base Paths # Base Paths
# NOTE: pyinstaller will set this to its own executable when building, # NOTE: pyinstaller will set this to its own executable when building,
# detect this to use __file__ and main.py redirection instead # detect this to use __file__ and main.py redirection instead
@@ -88,7 +112,6 @@ PING_TIMEOUT = timedelta(seconds=10)
ONLINE_DELAY = timedelta(seconds=120) ONLINE_DELAY = timedelta(seconds=120)
WATCH_INTERVAL = timedelta(seconds=59) WATCH_INTERVAL = timedelta(seconds=59)
# Strings # Strings
DROPS_ENABLED_TAG = "c2542d6d-cd10-4532-919b-3d19f30a768b"
WINDOW_TITLE = f"Twitch Drops Miner v{__version__} (by DevilXD)" WINDOW_TITLE = f"Twitch Drops Miner v{__version__} (by DevilXD)"
# Logging # Logging
FILE_FORMATTER = logging.Formatter( FILE_FORMATTER = logging.Formatter(
@@ -202,7 +225,7 @@ class GQLOperation(JsonType):
modified = deepcopy(self) modified = deepcopy(self)
if "variables" in self: if "variables" in self:
existing_variables: JsonType = modified["variables"] existing_variables: JsonType = modified["variables"]
existing_variables.update(variables) _merge_vars(existing_variables, variables)
else: else:
modified["variables"] = variables modified["variables"] = variables
return modified return modified
@@ -284,15 +307,18 @@ GQL_OPERATIONS: dict[str, GQLOperation] = {
# returns live channels for a particular game # returns live channels for a particular game
"GameDirectory": GQLOperation( "GameDirectory": GQLOperation(
"DirectoryPage_Game", "DirectoryPage_Game",
"df4bb6cc45055237bfaf3ead608bbafb79815c7100b6ee126719fac3762ddf8b", "3c9a94ee095c735e43ed3ad6ce6d4cbd03c4c6f754b31de54993e0d48fd54e30",
variables={ variables={
"limit": ..., # limit of channels returned "limit": ..., # limit of channels returned
"name": ..., # game name "slug": ..., # game slug
"imageWidth": 50,
"options": { "options": {
"broadcasterLanguages": [],
"freeformTags": None,
"includeRestricted": ["SUB_ONLY_LIVE"], "includeRestricted": ["SUB_ONLY_LIVE"],
"recommendationsContext": {"platform": "web"}, "recommendationsContext": {"platform": "web"},
"sort": "RELEVANCE", "sort": "RELEVANCE",
"tags": [], # list of tag IDs "tags": [],
"requestID": "JIRA-VXP-2397", "requestID": "JIRA-VXP-2397",
}, },
"sortTypeIsRecency": False, "sortTypeIsRecency": False,
+5 -7
View File
@@ -65,7 +65,6 @@ from constants import (
GQL_OPERATIONS, GQL_OPERATIONS,
MAX_CHANNELS, MAX_CHANNELS,
WATCH_INTERVAL, WATCH_INTERVAL,
# DROPS_ENABLED_TAG,
State, State,
ClientType, ClientType,
WebsocketTopic, WebsocketTopic,
@@ -1537,7 +1536,7 @@ class Twitch:
if ( if (
"message" in error_dict "message" in error_dict
and error_dict["message"] in ( and error_dict["message"] in (
"service error", # "service error",
"service unavailable", "service unavailable",
"service timeout", "service timeout",
"context deadline exceeded", "context deadline exceeded",
@@ -1558,8 +1557,7 @@ class Twitch:
merged = {} merged = {}
for key in set(chain(primary_data.keys(), secondary_data.keys())): for key in set(chain(primary_data.keys(), secondary_data.keys())):
in_primary = key in primary_data in_primary = key in primary_data
in_secondary = key in secondary_data if in_primary and key in secondary_data:
if in_primary and in_secondary:
vp = primary_data[key] vp = primary_data[key]
vs = secondary_data[key] vs = secondary_data[key]
if not isinstance(vp, type(vs)) or not isinstance(vs, type(vp)): if not isinstance(vp, type(vs)) or not isinstance(vs, type(vp)):
@@ -1693,15 +1691,15 @@ class Twitch:
response = await self.gql_request( response = await self.gql_request(
GQL_OPERATIONS["GameDirectory"].with_variables({ GQL_OPERATIONS["GameDirectory"].with_variables({
"limit": limit, "limit": limit,
"name": game.name, "slug": game.slug,
"options": { "options": {
"includeRestricted": ["SUB_ONLY_LIVE"], "includeRestricted": ["SUB_ONLY_LIVE"],
# "tags": [DROPS_ENABLED_TAG], "systemFilters": ["DROPS_ENABLED"],
}, },
}) })
) )
return [ return [
Channel.from_directory(self, stream_channel_data["node"]) Channel.from_directory(self, stream_channel_data["node"], drops_enabled=True)
for stream_channel_data in response["data"]["game"]["streams"]["edges"] for stream_channel_data in response["data"]["game"]["streams"]["edges"]
] ]
+8
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
import io import io
import os import os
import re
import sys import sys
import json import json
import random import random
@@ -402,3 +403,10 @@ class Game:
def __hash__(self) -> int: def __hash__(self) -> int:
return self.id return self.id
@property
def slug(self) -> str:
"""
Converts the game name into a slug, useable for the GQL API.
"""
return re.sub(r'(?![- ])\W', '', self.name.lower().replace(' ', '-'))