diff --git a/.gitignore b/.gitignore index 2c722a1..8e7bba9 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ dist/* */__pycache__/* .env __pycache__/* +cookies.json +.venv/* \ No newline at end of file diff --git a/README.md b/README.md index 64408c7..f6519fb 100644 --- a/README.md +++ b/README.md @@ -7,16 +7,27 @@ This application is a simple tool that generates a TikTok Live Stream Key for OB ## Features -- Generate TikTok Live Stream Key +- Generate TikTok Live Stream Key using Streamlabs API ## Requirements - Streamlabs TikTok LIVE access. You can request access [here](https://tiktok.com/falcon/live_g/live_access_pc_apply/result/index.html?id=GL6399433079641606942&lang=en-US) -- Streamlabs account - TikTok account -- Streamlabs installed on your computer and you are logged in with your TikTok account in Streamlabs +- Streamlabs installed on your computer and you are logged in with your TikTok account in Streamlabs (optional) ## Download -- Download the latest release from [here](https://github.com/Loukious/StreamLabsTikTokStreamKeyGenerator/releases/latest) +- Download the latest release from [here](../../releases/latest) + +## Usage +1. Run the application. +2. click on the "Load token" button. +3. + 1. If you have Streamlabs installed on your computer and you are logged in with your TikTok account in Streamlabs, the application will automatically load the token. + 2. If you don't have Streamlabs installed on your computer or you are not logged in with your TikTok account in Streamlabs, you will need to login with your TikTok account in the browser that will open. + +4. Select stream title and category. +5. Click on "Save Config" button to save the token, title and category. +6. Click on the "Go Live" button. + ## Screenshots @@ -26,8 +37,13 @@ This application is a simple tool that generates a TikTok Live Stream Key for OB The script will output: - **Base stream URL:** The URL needed to connect to the TikTok live stream. -- **Stream key for OBS Studio integration:** Stream key that that you can use in OBS Studio to stream to TikTok. +- **Stream key for OBS Studio (or any other streaming app):** Stream key that that you can use in OBS Studio to stream to TikTok. ## FAQ +### I'm getting a `Maximum number of attempts reached. Try again later.` error. What should I do? +This error sometimes occurs when TikTok detects selenium. You can use this [extension](https://chromewebstore.google.com/detail/export-cookie-json-file-f/nmckokihipjgplolmcmjakknndddifde) to export your cookies and import them into the script. +1. Start by installing the above extension in your browser. +2. Log into TikTok in the browser (if not already logged in), then export the cookies using the extension (while being on TikTok's website). +3. After that, place the file in the same directory as the script and rename it to `cookies.json` then start the app. ### Do I need to have 1k followers to get Streamlabs TikTok LIVE access? No, you can request access even if you have less than 1k followers. \ No newline at end of file diff --git a/Stream.py b/Stream.py new file mode 100644 index 0000000..cd46f4d --- /dev/null +++ b/Stream.py @@ -0,0 +1,39 @@ + + +import requests + + +class Stream: + def __init__(self, token): + self.s = requests.session() + self.s.headers.update({ + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) StreamlabsDesktop/1.16.7 Chrome/114.0.5735.289 Electron/25.9.3 Safari/537.36", + "authorization": f"Bearer {token}" + }) + + def search(self, game): + if not game: + return [] + url = f"https://streamlabs.com/api/v5/slobs/tiktok/info?category={game}" + info = self.s.get(url).json() + info["categories"].append({"full_name": "Other", "game_mask_id": ""}) + return info["categories"] + + def start(self, title, category): + url = "https://streamlabs.com/api/v5/slobs/tiktok/stream/start" + files=( + ('title', (None, title)), + ('device_platform', (None, 'win32')), + ('category', (None, category)), + ) + response = self.s.post(url, files=files).json() + try: + self.id = response["id"] + return response["rtmp"], response["key"] + except KeyError: + return None, None + + def end(self): + url = f"https://streamlabs.com/api/v5/slobs/tiktok/stream/{self.id}/end" + response = self.s.post(url).json() + return response["success"] \ No newline at end of file diff --git a/StreamLabsTikTokStreamKeyGenerator.py b/StreamLabsTikTokStreamKeyGenerator.py index 2889178..aa10844 100644 --- a/StreamLabsTikTokStreamKeyGenerator.py +++ b/StreamLabsTikTokStreamKeyGenerator.py @@ -3,59 +3,37 @@ import tkinter as tk from tkinter import messagebox import webbrowser -import requests - - -class Stream: - def __init__(self, token): - self.s = requests.session() - self.s.headers.update({ - "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) StreamlabsDesktop/1.16.7 Chrome/114.0.5735.289 Electron/25.9.3 Safari/537.36", - "authorization": f"Bearer {token}" - }) - - def search(self, game): - if not game: - return [] - url = f"https://streamlabs.com/api/v5/slobs/tiktok/info?category={game}" - info = self.s.get(url).json() - info["categories"].append({"full_name": "Other", "game_mask_id": ""}) - return info["categories"] - - def start(self, title, category): - url = "https://streamlabs.com/api/v5/slobs/tiktok/stream/start" - files=( - ('title', (None, title)), - ('device_platform', (None, 'win32')), - ('category', (None, category)), - ) - response = self.s.post(url, files=files).json() - try: - self.id = response["id"] - return response["rtmp"], response["key"] - except KeyError: - return None, None +from Stream import Stream +from TokenRetriever import TokenRetriever - def end(self): - url = f"https://streamlabs.com/api/v5/slobs/tiktok/stream/{self.id}/end" - response = self.s.post(url).json() - return response["success"] def load_config(): """Load entry values from a JSON file.""" try: with open("config.json", "r") as file: data = json.load(file) + + token_entry.delete(0, tk.END) + token_entry.insert(0, data.get("token", "")) stream_title_entry.config(state=tk.NORMAL) stream_title_entry.delete(0, tk.END) stream_title_entry.insert(0, data.get("title", "")) - stream_title_entry.config(state=tk.DISABLED) + game_category_entry.config(state=tk.NORMAL) game_category_entry.delete(0, tk.END) game_category_entry.insert(0, data.get("game", "")) - game_category_entry.config(state=tk.DISABLED) + + + if token_entry.get(): + global stream + stream = Stream(token_entry.get()) + go_live_button.config(state=tk.NORMAL) + else: + stream_title_entry.config(state=tk.DISABLED) + game_category_entry.config(state=tk.DISABLED) + if stream: fetch_game_mask_id(data.get("game", "")) @@ -75,7 +53,8 @@ def save_config(): """Save entry values to a JSON file.""" data = { "title": stream_title_entry.get(), - "game": game_category_entry.get() + "game": game_category_entry.get(), + "token": token_entry.get() } with open("config.json", "w") as file: json.dump(data, file) @@ -117,13 +96,31 @@ def load_token(): except Exception as e: messagebox.showerror("Error", f"Error reading file {file}: {e}") - messagebox.showinfo("API Token", "No API Token found. Make sure you have logged into Streamlabs with your TikTok account.") + messagebox.showinfo("API Token", "No API Token found locally. A webpage will now open to allow you to login into your TikTok account.") return None +def fetch_online_token(): + retriever = TokenRetriever() + token = retriever.retrieve_token() + if token: + token_entry.delete(0, tk.END) + token_entry.insert(0, token) + token_entry.config(show='*') + global stream + stream = Stream(token) + stream_title_entry.config(state=tk.NORMAL) + game_category_entry.config(state=tk.NORMAL) + go_live_button.config(state=tk.NORMAL) + fetch_game_mask_id(game_category_entry.get()) + else: + messagebox.showerror("Error", "Failed to obtain token online.") + def populate_token(): global stream token = load_token() - if token: + if not token: + fetch_online_token() # If no local token, try fetching online + else: token_entry.delete(0, tk.END) token_entry.insert(0, token) token_entry.config(show='*') @@ -143,10 +140,7 @@ def toggle_token_visibility(): toggle_button.config(text='Hide Token') def go_live(): - game_mask_id = getattr(game_category_entry, 'game_mask_id', None) - if not game_mask_id: - messagebox.showerror("Error", "Game category is invalid or not selected.") - return + game_mask_id = getattr(game_category_entry, 'game_mask_id', "") stream_url, stream_key = stream.start(stream_title_entry.get(), game_mask_id) @@ -351,5 +345,4 @@ def on_motion(event): if __name__ == "__main__": stream = None load_config() - populate_token() root.mainloop() diff --git a/TokenRetriever.py b/TokenRetriever.py new file mode 100644 index 0000000..e5e0755 --- /dev/null +++ b/TokenRetriever.py @@ -0,0 +1,88 @@ +import gzip +import seleniumwire.undetected_chromedriver as uc +import json +import requests +from urllib.parse import urlparse, parse_qs +import hashlib +import os +import base64 + +class TokenRetriever: + CLIENT_KEY = "awdjaq9ide8ofrtz" + REDIRECT_URI = "https://streamlabs.com/tiktok/auth" + STATE = "" + SCOPE = "user.info.basic,live.room.info,live.room.manage,user.info.profile,user.info.stats" + STREAMLABS_API_URL = "https://streamlabs.com/api/v5/auth/data" + + def __init__(self, cookies_file='cookies.json'): + self.code_verifier = self.generate_code_verifier() + self.code_challenge = self.generate_code_challenge(self.code_verifier) + self.streamlabs_auth_url = ( + f"https://streamlabs.com/m/login?force_verify=1&external=mobile&skip_splash=1&tiktok&code_challenge={self.code_challenge}" + ) + self.cookies_file = cookies_file + self.driver = None + + @staticmethod + def generate_code_verifier(): + return base64.urlsafe_b64encode(os.urandom(64)).decode('utf-8').rstrip('=') + + @staticmethod + def generate_code_challenge(code_verifier): + sha256 = hashlib.sha256() + sha256.update(code_verifier.encode('utf-8')) + return base64.urlsafe_b64encode(sha256.digest()).decode('utf-8').rstrip('=') + + def load_cookies(self, driver): + if os.path.exists(self.cookies_file): + with open(self.cookies_file, 'r') as f: + cookies = json.load(f) + for cookie in cookies: + driver.add_cookie(cookie) + + def retrieve_token(self): + self.driver = uc.Chrome() + self.driver.get("https://www.tiktok.com") # Load a page first before setting cookies + self.load_cookies(self.driver) + self.driver.get(self.streamlabs_auth_url) + + try: + request = self.driver.wait_for_request('https://www.tiktok.com/passport/open/web/auth/v2/', timeout=600) + if request: + decompressed_body = gzip.decompress(request.response.body) + response_body = decompressed_body.decode('utf-8') + data = json.loads(response_body) + + redirect_url = data.get('redirect_url') + if redirect_url: + with requests.session() as s: + s.get(redirect_url) + parsed_url = urlparse(redirect_url) + auth_code = parse_qs(parsed_url.query).get('code', [None])[0] + else: + print("No redirect_url found in the response.") + return None + else: + print("No request intercepted or timeout reached.") + return None + + finally: + try: + self.driver.close() + except Exception as e: + print(f"Error closing browser: {e}") + + if auth_code: + try: + import time + token_request_url = f"{self.STREAMLABS_API_URL}?code_verifier={self.code_verifier}&code={auth_code}" + time.sleep(3) # Wait a few seconds before sending the request + response = requests.get(token_request_url).json() + if response["success"]: + return response["data"]["oauth_token"] + except: + print("Failed to obtain token.") + return None + + print("Failed to obtain authorization code.") + return None diff --git a/requirements.txt b/requirements.txt index 4f0f2c7..b4c4dec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,6 @@ requests>=2.31.0 pyinstaller>=6.6.0 -setuptools>=69.5.1 \ No newline at end of file +setuptools>=69.5.1 +selenium-wire>=5.1.0 +undetected_chromedriver>=3.5.5 +blinker==1.7.0 \ No newline at end of file