Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v15.9.0 #176

Merged
merged 8 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ __pycache__
/env
/cache
/*.jar
log.txt
/*log.txt
/lock.file
settings.json
#/lang/English.json
Expand Down
9 changes: 9 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,14 @@
"console": "integratedTerminal",
"justMyCode": false
},
{
"name": "Run App (DEBUG+GQL+WS+Log)",
"type": "python",
"request": "launch",
"program": "main.py",
"args": ["-vvv", "--debug-gql", "--debug-ws", "--log"],
"console": "integratedTerminal",
"justMyCode": false
},
]
}
77 changes: 50 additions & 27 deletions channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ def _payload(self) -> JsonType:
]
return {"data": (b64encode(json_minify(payload).encode("utf8"))).decode("utf8")}

async def send_watch(self) -> bool:
async def send_watch(self) -> tuple[bool, bool]:
"""
Start of fix for 2024/5 API Change
"""
Expand All @@ -390,41 +390,64 @@ async def send_watch(self) -> bool:
signature: JsonType | None = response["data"]['streamPlaybackAccessToken']["signature"]
value: JsonType | None = response["data"]['streamPlaybackAccessToken']["value"]
if not signature or not value:
return False
return False, False

RequestBroadcastQualitiesURL = f"https://usher.ttvnw.net/api/channel/hls/{self._login}.m3u8?sig={signature}&token={value}"

try:
async with self._twitch.request( # Gets list of streams
async with self._twitch.request( # Gets list of m3u8 playlists
"GET", RequestBroadcastQualitiesURL
) as response1:
BroadcastQualities = await response1.text()
BroadcastQualitiesM3U = await response1.text()
except RequestException:
return False
logger.error(f"Failed to recieve list of m3u8 playlists.")
return False, False

BroadcastLowestQualityURL = BroadcastQualities.split("\n")[-1] # Just takes the last line, this could probably be handled better in the future
if not validators.url(BroadcastLowestQualityURL):
return False

try:
async with self._twitch.request( # Gets actual streams
"GET", BroadcastLowestQualityURL
) as response2:
StreamURLList = await response2.text()
except RequestException:
return False

StreamLowestQualityURL = StreamURLList.split("\n")[-2] # For whatever reason this includes a blank line at the end, this should probably be handled better in the future
if not validators.url(StreamLowestQualityURL):
return False
BroadcastQualitiesM3U = BroadcastQualitiesM3U.split("\n")
BroadcastQualitiesList = []
for i in range(int(len(BroadcastQualitiesM3U)/3)): # gets all m3u8 playlists
BroadcastQualitiesList.append(BroadcastQualitiesM3U[4+3*i])

if not all(validators.url(url) for url in BroadcastQualitiesList):
logger.error(f"Couldn't parse list of m3u8 playlists.")
return False, False

retries = -1
for BroadcastQuality in BroadcastQualitiesList:
retries = retries + 1
try:
async with self._twitch.request( # Gets actual streams
"GET", BroadcastQuality, return_error = True
) as response2:
if response2.status == 200:
StreamURLList = await response2.text()
else:
logger.log(CALL,f"Request for streams from m3u8 returned: {response2}")
continue
except RequestException:
logger.error(f"Failed to recieve list of streams.")
return False, False

StreamURL = StreamURLList.split("\n")[-2] # For whatever reason this includes a blank line at the end, this should probably be handled better in the future
if not validators.url(StreamURL):
logger.error(f"Failed to parse streamURL.")
return False, False

try:
async with self._twitch.request( # Downloads the stream
"HEAD", StreamLowestQualityURL
) as response3: # I lied, well idk, but this code doesn't listen for the actual video data
return response3.status == 200
except RequestException:
return False
try:
async with self._twitch.request( # The HEAD request is enough to advance drops
"HEAD", StreamURL, return_error = True
) as response3:
if response3.status == 200:
logger.log(CALL,f"Successfully watched after {retries} retries.")
return True, False
else:
logger.error(f"Request for stream HEAD returned: {response2}")
except RequestException:
logger.error(f"Failed to recieve list of streams.")
return False, False
await asyncio.sleep(1) # Wait a second to not spam twitch API
logger.error(f"Failed to watch all of {len(BroadcastQualitiesList)} Broadcast qualities. Can be ignored if occuring up to ~15x/hour.")
return False, True
"""
End of fix for 2024/5 API Change.
Old code below.
Expand Down
2 changes: 1 addition & 1 deletion inventory.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ def progress(self) -> float:
if self.required_minutes: # Quick fix to prevent division by zero crash
return self.current_minutes / self.required_minutes
else:
self._manager.print(f'!!!required_minutes for "{self.name}" is 0 This could be due to a subscription requirement, tracked in Issue #101!!!')
self._manager.print(f'Required_minutes for "{self.name}" from "{self.campaign.game.name}" is 0. This could be due to a subscription requirement, tracked in Issue #101. Take a look at the drop, but this can likely be ignored.')
self.preconditions_met = False
return 0

Expand Down
11 changes: 11 additions & 0 deletions patch_notes.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
## v15.9.0

18.8.2024
- Added game name to `0 required minutes` error message

27.8.2024
- Clarified that 0 required minutes error is not critical
- Fixed "Twitch is down, retrying" for my case. It seems like some might still experience the issue. Tracked in #172



## v15.8.2

13.8.2024
Expand Down
13 changes: 8 additions & 5 deletions twitch.py
Original file line number Diff line number Diff line change
Expand Up @@ -1046,11 +1046,14 @@ async def _watch_loop(self) -> NoReturn:
interval: float = WATCH_INTERVAL.total_seconds()
while True:
channel: Channel = await self.watching_channel.get()
succeeded: bool = await channel.send_watch()
succeeded, repeat_now = await channel.send_watch()
logger.log(CALL,f"returned watch, succeeded: {succeeded}, repeat_new: {repeat_now}")
if not succeeded:
# this usually means the campaign expired in the middle of mining
# or the m3u8 playlists all returned a 500 Internal server error
# NOTE: the maintenance task should switch the channel right after this happens
await self._watch_sleep(interval)
if not repeat_now:
await self._watch_sleep(interval)
continue
last_watch = time()
self._drop_update = asyncio.Future()
Expand Down Expand Up @@ -1480,7 +1483,7 @@ async def get_auth(self) -> _AuthState:

@asynccontextmanager
async def request(
self, method: str, url: URL | str, *, invalidate_after: datetime | None = None, **kwargs
self, method: str, url: URL | str, *, invalidate_after: datetime | None = None, return_error: bool = False, **kwargs
) -> abc.AsyncIterator[aiohttp.ClientResponse]:
session = await self.get_session()
method = method.upper()
Expand All @@ -1505,12 +1508,12 @@ async def request(
)
assert response is not None
logger.debug(f"Response: {response.status}: {response}")
if response.status < 500:
if response.status < 500 or return_error:
# pre-read the response to avoid getting errors outside of the context manager
raw_response = await response.read() # noqa
yield response
return
self.print(_("error", "site_down").format(seconds=round(delay)))
self.print(_("error", "site_down").format(seconds=round(delay)) + f"\nResponse: {response}" + f"\nStatus: {response.status}")
except aiohttp.ClientConnectorCertificateError: # type: ignore[unused-ignore]
# for a case where SSL verification fails
raise
Expand Down
Loading