diff --git a/.github/workflows/deploy-docker.yml b/.github/workflows/deploy-docker.yml index 38bcdbe..8eba255 100644 --- a/.github/workflows/deploy-docker.yml +++ b/.github/workflows/deploy-docker.yml @@ -22,7 +22,7 @@ jobs: uses: docker/setup-buildx-action@v3.3.0 - name: Login to DockerHub - uses: docker/login-action@v3.1.0 + uses: docker/login-action@v3.2.0 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_TOKEN }} @@ -38,7 +38,7 @@ jobs: - name: Build and push AMD64, ARM64, ARMv7 id: docker_build - uses: docker/build-push-action@v5.3.0 + uses: docker/build-push-action@v5.4.0 with: context: . push: true diff --git a/README.md b/README.md index dc4ae5e..7223cfb 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,10 @@ If you want to offer me a coffee, I would be grateful! ❤️ | | | |-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------| +|Donate BTC|`bc1qq49mvgda2zw4f9kta0a85xztwuxewqwac5eckd` _(BTC)_| |Donate DOGE|`DAKzncwKkpfPCm1xVU7u2pConpXwX7HS3D` _(DOGE)_| +|Donate via DonationAlerts|https://www.donationalerts.com/r/rdavydov| +|Donate via Boosty|https://boosty.to/rdavydov/donate| If you have any issues or you want to contribute, you are welcome! But please read the [CONTRIBUTING.md](https://github.com/rdavydov/Twitch-Channel-Points-Miner-v2/blob/master/CONTRIBUTING.md) file. diff --git a/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py b/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py index 8162568..f7e415c 100644 --- a/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py +++ b/TwitchChannelPointsMiner/TwitchChannelPointsMiner.py @@ -190,6 +190,7 @@ class TwitchChannelPointsMiner: if Settings.enable_analytics is True: from TwitchChannelPointsMiner.classes.AnalyticsServer import AnalyticsServer + days_ago = days_ago if days_ago <= 365*15 else 365*15 http_server = AnalyticsServer( host=host, port=port, refresh=refresh, days_ago=days_ago, username=self.username ) diff --git a/TwitchChannelPointsMiner/__init__.py b/TwitchChannelPointsMiner/__init__.py index b6df4f8..eb5f952 100644 --- a/TwitchChannelPointsMiner/__init__.py +++ b/TwitchChannelPointsMiner/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -__version__ = "1.9.5" +__version__ = "1.9.7" from .TwitchChannelPointsMiner import TwitchChannelPointsMiner __all__ = [ diff --git a/TwitchChannelPointsMiner/classes/Twitch.py b/TwitchChannelPointsMiner/classes/Twitch.py index f9661f9..ea12537 100644 --- a/TwitchChannelPointsMiner/classes/Twitch.py +++ b/TwitchChannelPointsMiner/classes/Twitch.py @@ -11,14 +11,14 @@ import random import re import string import time -# from datetime import datetime +# import json +import requests +import validators from pathlib import Path from secrets import choice, token_hex - -# import json +# from urllib.parse import quote # from base64 import urlsafe_b64decode - -import requests +# from datetime import datetime from TwitchChannelPointsMiner.classes.entities.Campaign import Campaign from TwitchChannelPointsMiner.classes.entities.Drop import Drop @@ -79,7 +79,7 @@ class Twitch(object): self.client_session = token_hex(16) self.client_version = CLIENT_VERSION self.twilight_build_id_pattern = re.compile( - r"window\.__twilightBuildID=\"([0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-4[0-9A-Fa-f]{3}-[89ABab][0-9A-Fa-f]{3}-[0-9A-Fa-f]{12})\";" + r'window\.__twilightBuildID\s*=\s*"([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})"' ) def login(self): @@ -433,7 +433,8 @@ class Twitch(object): ) > 30 ) - and streamers[index].stream.minute_watched < 7 # fix #425 + # fix #425 + and streamers[index].stream.minute_watched < 7 ): streamers_watching.append(index) if len(streamers_watching) == 2: @@ -467,14 +468,87 @@ class Twitch(object): streamers_watching = streamers_watching[:2] for index in streamers_watching: - next_iteration = time.time() + 60 / len(streamers_watching) + # next_iteration = time.time() + 60 / len(streamers_watching) + next_iteration = time.time() + 20 / len(streamers_watching) try: + #################################### + # Start of fix for 2024/5 API Change + # Create the JSON data for the GraphQL request + json_data = copy.deepcopy( + GQLOperations.PlaybackAccessToken) + json_data["variables"] = { + "login": streamers[index].username, + "isLive": True, + "isVod": False, + "vodID": "", + # "playerType": "site" + "playerType": "picture-by-picture" + } + + # Get signature and value using the post_gql_request method + responsePlaybackAccessToken = self.post_gql_request( + json_data) + logger.debug( + f"Sent PlaybackAccessToken request for {streamers[index]}" + ) + signature = responsePlaybackAccessToken["data"]['streamPlaybackAccessToken']["signature"] + value = responsePlaybackAccessToken["data"]['streamPlaybackAccessToken']["value"] + if not signature or not value: + continue + + # encoded_value = quote(json.dumps(value)) + + # Construct the URL for the broadcast qualities + RequestBroadcastQualitiesURL = f"https://usher.ttvnw.net/api/channel/hls/{streamers[index].username}.m3u8?sig={signature}&token={value}" + + # Get list of video qualities + responseBroadcastQualities = requests.get(RequestBroadcastQualitiesURL, headers={ + "User-Agent": self.user_agent}, timeout=20) # timeout=60 + logger.debug( + f"Send RequestBroadcastQualitiesURL request for {streamers[index]} - Status code: {responseBroadcastQualities.status_code}" + ) + if responseBroadcastQualities.status_code != 200: + continue + BroadcastQualities = responseBroadcastQualities.text + + # Just takes the last line, which should be the URL for the lowest quality + BroadcastLowestQualityURL = BroadcastQualities.split( + "\n")[-1] + if not validators.url(BroadcastLowestQualityURL): + continue + + # Get list of video URLs + responseStreamURLList = requests.get(BroadcastLowestQualityURL, headers={ + "User-Agent": self.user_agent}, timeout=20) # timeout=60 + logger.debug( + f"Send BroadcastLowestQualityURL request for {streamers[index]} - Status code: {responseStreamURLList.status_code}" + ) + if responseStreamURLList.status_code != 200: + continue + StreamURLList = responseStreamURLList.text + + # Just takes the last line, which should be the URL for the lowest quality + StreamLowestQualityURL = StreamURLList.split("\n")[-2] + if not validators.url(StreamLowestQualityURL): + continue + + # Perform a HEAD request to simulate watching the stream + responseStreamLowestQualityURL = requests.head(StreamLowestQualityURL, headers={ + "User-Agent": self.user_agent}, timeout=20) # timeout=60 + logger.debug( + f"Send StreamLowestQualityURL request for {streamers[index]} - Status code: {responseStreamLowestQualityURL.status_code}" + ) + if responseStreamLowestQualityURL.status_code != 200: + continue + # End of fix for 2024/5 API Change + ################################## response = requests.post( streamers[index].stream.spade_url, data=streamers[index].stream.encode_payload(), headers={"User-Agent": self.user_agent}, - timeout=60, + # timeout=60, + timeout=20, ) logger.debug( f"Send minute watched request for {streamers[index]} - Status code: {response.status_code}" @@ -543,7 +617,8 @@ class Twitch(object): ) if streamers_watching == []: - self.__chuncked_sleep(60, chunk_size=chunk_size) + # self.__chuncked_sleep(60, chunk_size=chunk_size) + self.__chuncked_sleep(20, chunk_size=chunk_size) except Exception: logger.error( "Exception raised in send minute watched", exc_info=True) diff --git a/TwitchChannelPointsMiner/constants.py b/TwitchChannelPointsMiner/constants.py index 93d7e14..c73ad4f 100644 --- a/TwitchChannelPointsMiner/constants.py +++ b/TwitchChannelPointsMiner/constants.py @@ -1,10 +1,13 @@ # Twitch endpoints -URL = "https://www.twitch.tv" +URL = "https://www.twitch.tv" # Browser, Apps +# URL = "https://m.twitch.tv" # Mobile Browser +# URL = "https://android.tv.twitch.tv" # TV IRC = "irc.chat.twitch.tv" IRC_PORT = 6667 WEBSOCKET = "wss://pubsub-edge.twitch.tv/v1" CLIENT_ID = "ue6666qo983tsx6so1t0vnawi233wa" # TV # CLIENT_ID = "kimne78kx3ncx6brgo4mv6wki5h1ko" # Browser +# CLIENT_ID = "r8s4dac0uhzifbpu9sjdiwzctle17ff" # Mobile Browser # CLIENT_ID = "kd1unb4b3q4t58fwlpcbzcbnm76a8fp" # Android App # CLIENT_ID = "851cqzxpb9bqu9z6galo155du" # iOS App DROP_ID = "c2542d6d-cd10-4532-919b-3d19f30a768b" @@ -46,6 +49,15 @@ class GQLOperations: } }, } + PlaybackAccessToken = { + "operationName": "PlaybackAccessToken", + "extensions": { + "persistedQuery": { + "version": 1, + "sha256Hash": "3093517e37e4f4cb48906155bcd894150aef92617939236d2508f3375ab732ce", + } + }, + } VideoPlayerStreamInfoOverlayChannel = { "operationName": "VideoPlayerStreamInfoOverlayChannel", "extensions": { diff --git a/requirements.txt b/requirements.txt index 51a1e42..eda41da 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,4 @@ flask irc pandas pytz +validators