├── .github └── FUNDING.yml ├── .gitignore ├── GUIDE.md ├── LICENSE ├── README.md ├── assets ├── logo.png └── sample.png └── src ├── check_updates.py ├── cookies.json ├── core ├── __init__.py ├── tiktok_api.py └── tiktok_recorder.py ├── http_utils ├── __init__.py └── http_client.py ├── main.py ├── requirements.txt ├── telegram.json ├── upload ├── __init__.py └── telegram.py └── utils ├── __init__.py ├── args_handler.py ├── custom_exceptions.py ├── dependencies.py ├── enums.py ├── logger_manager.py ├── utils.py └── video_management.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: michele0303 # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: michele0303 # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: paypal.me/michelefiorii # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # video file extensions 132 | *.mp4 133 | *.flv 134 | cookies.json 135 | telegram.json 136 | 137 | # telegram session file 138 | *.session 139 | 140 | # macOS folder metadata 141 | .DS_Store 142 | -------------------------------------------------------------------------------- /GUIDE.md: -------------------------------------------------------------------------------- 1 | # GUIDE 2 | 3 | ### How To Set Cookies 4 | 1) Go to https://www.tiktok.com/. 5 | 2) Open Inspect Element => Ctrl + Shift + I for Windows/Linux and Cmd + Option + I for macOS users. Or, right-click on the web page and choose Inspect to access the Developer tools panel. 6 | 3) Switch to the **Application** tab. 7 | 8 | ![image](https://github.com/user-attachments/assets/7a7cb64b-41fe-49ed-9d85-bc00d451b9ef) 9 | 10 | 4) **Copy** the **value** of session_ss. 11 | 12 | ![image](https://github.com/user-attachments/assets/dccd9b11-6efc-4cb9-8003-599a4fcc8957) 13 | 14 | 5) **Paste** this **value** into the cookies.json file. 15 | 16 | ![image](https://github.com/user-attachments/assets/5cd23e80-bfb0-45d4-9141-601807dc027b) 17 | 18 |
19 | 20 | ### How To Get Room_ID 21 | 1) Go to https://www.tiktok.com/@username/live 22 | 2) Open Inspect Element => Ctrl + Shift + I for Windows/Linux and Cmd + Option + I for macOS users. Or, right-click on the web page and choose Inspect to access the Developer tools panel 23 | 3) Search "room_id" with Ctrl + F 24 | 25 | ![image](https://user-images.githubusercontent.com/31160531/202849647-922d75d6-570c-43fe-a4b3-fcb795d39f92.png) 26 | 27 |
28 | 29 | ### How to Enable Upload To Telegram 30 | 1) Go to https://my.telegram.org 31 | 2) Log in with your number registered in Telegram in format "+{country code}{your_number}" 32 | 33 | ![image](https://github.com/user-attachments/assets/f591b9d2-4189-4bfe-9180-f4484625eea2) 34 | 35 | 3) Once you are authenticated, click on "API Development Tools" 36 | 37 | ![image](https://github.com/user-attachments/assets/89900d60-851e-4c6c-a20a-892dd99f7e24) 38 | 39 | 4) Please go ahead and create a new request by filling out the form below (skip if you already have one) 40 | 41 | ![image](https://github.com/user-attachments/assets/3e61e39d-81d9-4c93-ae26-c6bccf6a509c) 42 | 43 | 5) You should now have the parameter values needed to configure the telegram.json file. 44 | 45 | ![image](https://github.com/user-attachments/assets/b0a7fe9a-cb9b-413f-a5bf-2434146c63b3) 46 | 47 |
48 | 49 | ### Restricted Country 50 | 1) Italy 51 | 2) Hong Kong 52 | 3) UK 53 | 54 | 55 | ### Unrestricted Country 56 | 1) Switzerland 57 | 2) Australia 58 | 3) Austria 59 | 4) Belgium 60 | 5) Brazil 61 | 6) Bulgaria 62 | 7) Canada 63 | 8) Czech Republic 64 | 9) Denmark 65 | 10) Estonia 66 | 11) Finland 67 | 12) France 68 | 13) Germany 69 | 14) Ireland 70 | 15) Israel 71 | 16) Japan 72 | 17) Latvia 73 | 18) Luxembourg 74 | 19) Moldova 75 | 20) Netherlands 76 | 21) New Zealand 77 | 22) North Macedonia 78 | 23) Norway 79 | 24) Poland 80 | 25) Portugal 81 | 26) Romania 82 | 27) Serbia 83 | 28) Singapore 84 | 29) Slovakia 85 | 30) Spain 86 | 31) Sweden 87 | 32) USA 88 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Michele 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |

TikTok Live Recorder🎥

5 | 6 | TikTok Live Recorder is a tool for recording live streaming tiktok. 7 | 8 | ![Python](https://img.shields.io/badge/python-3670A0?style=for-the-badge&logo=python&logoColor=ffdd54) [![Licence](https://img.shields.io/github/license/Ileriayo/markdown-badges?style=for-the-badge)](./LICENSE) 9 | 10 | The TikTok Live Recorder is a tool designed to easily capture and save live streaming sessions from TikTok. It records both audio and video, allowing users to revisit and preserve engaging live content for later enjoyment and analysis. It's a valuable resource for creators, researchers, and anyone who wants to capture memorable moments from TikTok live streams. 11 | 12 | image 13 | 14 |
15 | 16 |
17 | 18 | 19 |

How To Use

20 | 21 | - [Install on Windows & Linux 💻](#install-on-windows--linux-) 22 | - [Install on Android 📱](#install-on-android-) 23 | 24 |
25 | 26 | 27 | ## Install on Windows & Linux 💻 28 | 29 | To clone and run this application, you'll need [Git](https://git-scm.com) and [Python3](https://www.python.org/downloads/) and [FFmpeg](https://www.youtube.com/watch?v=OlNWCpFdVMA) installed on your computer. From your command line: 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | ```bash 38 | # Clone this repository 39 | $ git clone https://github.com/Michele0303/tiktok-live-recorder 40 | # Go into the repository 41 | $ cd tiktok-live-recorder 42 | # Go into the source code 43 | $ cd src 44 | # Install dependencies 45 | $ pip install -r requirements.txt --break-system-packages 46 | # Run the app on windows 47 | $ python main.py -h 48 | # Run the app on linux 49 | $ python3 main.py -h 50 | ``` 51 | 52 | ## Install on Android 📱 53 | 54 | Install Termux from F-Droid: HERE - Avoid installing from Play Store to prevent potential issues. 55 | 56 | From termux command line: 57 | 58 | 59 | 60 | 61 | 62 | ```bash 63 | # Update packages 64 | $ pkg update 65 | $ pkg upgrade 66 | # Install git, python3, ffmpeg 67 | $ pkg install git python3 ffmpeg 68 | # Clone this repository 69 | $ git clone https://github.com/Michele0303/tiktok-live-recorder 70 | # Go into the repository 71 | $ cd tiktok-live-recorder 72 | # Go into the source code 73 | $ cd src 74 | # Install dependencies 75 | $ pip install -r requirements.txt --break-system-packages 76 | # Run the app 77 | $ python main.py -h 78 | ``` 79 | 80 |
81 | 82 | ## Guide 83 | 84 | - How to set cookies in cookies.json 85 | - How to get room_id 86 | - How to enable upload to telegram 87 | 88 | ## To-Do List 🔮 89 | 90 | - [x] Automatic Recording: Enable automatic recording of live TikTok sessions. 91 | - [x] Authentication: Added support for cookies-based authentication. 92 | - [x] Recording by room_id: Allow recording by providing the room ID. 93 | - [x] Recording by TikTok live URL: Enable recording by directly using the TikTok live URL. 94 | - [x] Using a Proxy to Bypass Login Restrictions: Implement the ability to use an HTTP proxy to bypass login restrictions in some countries (only to obtain the room ID). 95 | - [x] Implement a Logging System: Set up a comprehensive logging system to track activities and errors. 96 | - [x] Implement Auto-Update Feature: Create a system that automatically checks for new releases. 97 | - [x] Send Recorded Live Streams to Telegram: Enable the option to send recorded live streams directly to Telegram. 98 | - [ ] Save Chat in a File: Allow saving the chat from live streams in a file. 99 | - [ ] Support for M3U8: Add support for recording live streams via m3u8 format. 100 | - [ ] Watchlist Feature: Implement a watchlist to monitor multiple users simultaneously (while respecting TikTok's limitations). 101 | 102 | ## Legal ⚖️ 103 | 104 | This code is in no way affiliated with, authorized, maintained, sponsored or endorsed by TikTok or any of its affiliates or subsidiaries. Use at your own risk. 105 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Michele0303/tiktok-live-recorder/fa9683f177ad56edb0d51f31c407561d81bafc1c/assets/logo.png -------------------------------------------------------------------------------- /assets/sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Michele0303/tiktok-live-recorder/fa9683f177ad56edb0d51f31c407561d81bafc1c/assets/sample.png -------------------------------------------------------------------------------- /src/check_updates.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | import requests 4 | import zipfile 5 | import shutil 6 | 7 | URL = "https://raw.githubusercontent.com/Michele0303/tiktok-live-recorder/main/src/utils/enums.py" 8 | URL_REPO = "https://github.com/Michele0303/tiktok-live-recorder/archive/refs/heads/main.zip" 9 | FILE_TEMP = "enums_temp.py" 10 | FILE_NAME_UPDATE = URL_REPO.split("/")[-1] 11 | 12 | 13 | def delete_tmp_file(): 14 | try: 15 | os.remove(FILE_TEMP) 16 | except: 17 | pass 18 | 19 | def check_file(path: str) -> bool: 20 | """ 21 | Check if a file exists at the given path. 22 | 23 | Args: 24 | path (str): Path to the file. 25 | 26 | Returns: 27 | bool: True if the file exists, False otherwise. 28 | """ 29 | return Path(path).exists() 30 | 31 | 32 | def download_file(url: str, file_name: str) -> None: 33 | """ 34 | Download a file from a URL and save it locally. 35 | 36 | Args: 37 | url (str): URL to download the file from. 38 | file_name (str): Name of the file to save. 39 | """ 40 | response = requests.get(url, stream=True) 41 | 42 | if response.status_code == 200: 43 | with open(file_name, "wb") as file: 44 | for chunk in response.iter_content(1024): 45 | file.write(chunk) 46 | else: 47 | print("Error downloading the file.") 48 | 49 | 50 | def check_updates() -> bool: 51 | """ 52 | Check if there is a new version available and update if necessary. 53 | 54 | Returns: 55 | bool: True if the update was successful, False otherwise. 56 | """ 57 | download_file(URL, FILE_TEMP) 58 | 59 | if not check_file(FILE_TEMP): 60 | delete_tmp_file() 61 | print("The temporary file does not exist.") 62 | return False 63 | 64 | try: 65 | from enums_temp import Info 66 | from utils.enums import Info as InfoOld 67 | except ImportError: 68 | print("Error importing the file or missing module.") 69 | delete_tmp_file() 70 | return False 71 | 72 | if float(Info.__str__(Info.VERSION)) != float(InfoOld.__str__(InfoOld.VERSION)): 73 | print(Info.BANNER) 74 | print(f"Current version: {InfoOld.__str__(InfoOld.VERSION)}\nNew version available: {Info.__str__(Info.VERSION)}") 75 | print("\nNew features:") 76 | for feature in Info.NEW_FEATURES: 77 | print("*", feature) 78 | else: 79 | delete_tmp_file() 80 | # print("No updates available.") 81 | return False 82 | 83 | download_file(URL_REPO, FILE_NAME_UPDATE) 84 | 85 | dir_path = Path(__file__).parent 86 | temp_update_dir = dir_path / "update_temp" 87 | 88 | # Extract content from zip to a temporary update directory 89 | with zipfile.ZipFile(dir_path / FILE_NAME_UPDATE, "r") as zip_ref: 90 | zip_ref.extractall(temp_update_dir) 91 | 92 | # Find the extracted folder (it will have the name 'tiktok-live-recorder-main') 93 | extracted_folder = temp_update_dir / "tiktok-live-recorder-main" / "src" 94 | 95 | # Copy all files and folders from the extracted folder to the main directory 96 | files_to_preserve = {"check_updates.py", "cookies.json", "telegram.json"} 97 | for item in extracted_folder.iterdir(): 98 | source = item 99 | destination = dir_path / item.name 100 | 101 | # Skip overwriting the files we want to preserve 102 | if source.name in files_to_preserve: 103 | continue 104 | 105 | # If it's a file, overwrite it 106 | if source.is_file(): 107 | shutil.copy2(source, destination) 108 | # If it's a directory, copy its contents file by file 109 | elif source.is_dir(): 110 | for sub_item in source.rglob('*'): 111 | sub_destination = destination / sub_item.relative_to(source) 112 | if sub_item.is_file(): 113 | sub_destination.parent.mkdir(parents=True, exist_ok=True) 114 | shutil.copy2(sub_item, sub_destination) 115 | 116 | # Delete the temporary files and folders 117 | shutil.rmtree(temp_update_dir) 118 | try: 119 | Path(FILE_TEMP).unlink() 120 | except Exception as e: 121 | print(f"Failed to remove the temporary file {FILE_TEMP}: {e}") 122 | 123 | delete_tmp_file() 124 | 125 | try: 126 | Path(FILE_NAME_UPDATE).unlink() 127 | except Exception as e: 128 | print(f"Failed to remove the temporary file {FILE_NAME_UPDATE}: {e}") 129 | 130 | return True 131 | -------------------------------------------------------------------------------- /src/cookies.json: -------------------------------------------------------------------------------- 1 | { 2 | "sessionid_ss": "", 3 | "tt-target-idc": "useast2a" 4 | } 5 | -------------------------------------------------------------------------------- /src/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Michele0303/tiktok-live-recorder/fa9683f177ad56edb0d51f31c407561d81bafc1c/src/core/__init__.py -------------------------------------------------------------------------------- /src/core/tiktok_api.py: -------------------------------------------------------------------------------- 1 | import json 2 | import re 3 | 4 | from http_utils.http_client import HttpClient 5 | from utils.enums import StatusCode, TikTokError 6 | from utils.logger_manager import logger 7 | from utils.custom_exceptions import UserLiveException, TikTokException, \ 8 | LiveNotFound, IPBlockedByWAF 9 | 10 | 11 | class TikTokAPI: 12 | 13 | def __init__(self, proxy, cookies): 14 | self.BASE_URL = 'https://www.tiktok.com' 15 | self.WEBCAST_URL = 'https://webcast.tiktok.com' 16 | 17 | self.http_client = HttpClient(proxy, cookies).req 18 | 19 | def is_country_blacklisted(self) -> bool: 20 | """ 21 | Checks if the user is in a blacklisted country that requires login 22 | """ 23 | response = self.http_client.get( 24 | f"{self.BASE_URL}/live", 25 | allow_redirects=False 26 | ) 27 | 28 | return response.status_code == StatusCode.REDIRECT 29 | 30 | def is_room_alive(self, room_id: str) -> bool: 31 | """ 32 | Checking whether the user is live. 33 | """ 34 | if not room_id: 35 | raise UserLiveException(TikTokError.USER_NOT_CURRENTLY_LIVE) 36 | 37 | data = self.http_client.get( 38 | f"{self.WEBCAST_URL}/webcast/room/check_alive/" 39 | f"?aid=1988®ion=CH&room_ids={room_id}&user_is_login=true" 40 | ).json() 41 | 42 | if 'data' not in data or len(data['data']) == 0: 43 | return False 44 | 45 | return data['data'][0].get('alive', False) 46 | 47 | def get_user_from_room_id(self, room_id) -> str: 48 | """ 49 | Given a room_id, I get the username 50 | """ 51 | data = self.http_client.get( 52 | f"{self.WEBCAST_URL}/webcast/room/info/?aid=1988&room_id={room_id}" 53 | ).json() 54 | 55 | if 'Follow the creator to watch their LIVE' in json.dumps(data): 56 | raise UserLiveException(TikTokError.ACCOUNT_PRIVATE_FOLLOW) 57 | 58 | if 'This account is private' in data: 59 | raise UserLiveException(TikTokError.ACCOUNT_PRIVATE) 60 | 61 | display_id = data.get("data", {}).get("owner", {}).get("display_id") 62 | if display_id is None: 63 | raise TikTokException(TikTokError.USERNAME_ERROR) 64 | 65 | return display_id 66 | 67 | def get_room_and_user_from_url(self, live_url: str): 68 | """ 69 | Given a url, get user and room_id. 70 | """ 71 | response = self.http_client.get(live_url, allow_redirects=False) 72 | content = response.text 73 | 74 | if response.status_code == StatusCode.REDIRECT: 75 | raise UserLiveException(TikTokError.COUNTRY_BLACKLISTED) 76 | 77 | if response.status_code == StatusCode.MOVED: # MOBILE URL 78 | matches = re.findall("com/@(.*?)/live", content) 79 | if len(matches) < 1: 80 | raise LiveNotFound(TikTokError.INVALID_TIKTOK_LIVE_URL) 81 | 82 | user = matches[0] 83 | 84 | # https://www.tiktok.com/@/live 85 | match = re.match( 86 | r"https?://(?:www\.)?tiktok\.com/@([^/]+)/live", 87 | live_url 88 | ) 89 | if match: 90 | user = match.group(1) 91 | 92 | room_id = self.get_room_id_from_user(user) 93 | 94 | return user, room_id 95 | 96 | def get_room_id_from_user(self, user: str) -> str: 97 | """ 98 | Given a username, I get the room_id 99 | """ 100 | content = self.http_client.get( 101 | url=f'https://www.tiktok.com/@{user}/live' 102 | ).text 103 | 104 | if 'Please wait...' in content: 105 | raise IPBlockedByWAF 106 | 107 | pattern = re.compile( 108 | r'', 109 | re.DOTALL) 110 | match = pattern.search(content) 111 | 112 | if match is None: 113 | raise UserLiveException(TikTokError.ROOM_ID_ERROR) 114 | 115 | data = json.loads(match.group(1)) 116 | 117 | if 'LiveRoom' not in data and 'CurrentRoom' in data: 118 | return "" 119 | 120 | room_id = data.get('LiveRoom', {}).get('liveRoomUserInfo', {}).get( 121 | 'user', {}).get('roomId', None) 122 | 123 | if room_id is None: 124 | raise UserLiveException(TikTokError.ROOM_ID_ERROR) 125 | 126 | return room_id 127 | 128 | def get_live_url(self, room_id: str) -> str: 129 | """ 130 | Return the cdn (flv or m3u8) of the streaming 131 | """ 132 | data = self.http_client.get( 133 | f"{self.WEBCAST_URL}/webcast/room/info/?aid=1988&room_id={room_id}" 134 | ).json() 135 | 136 | if 'This account is private' in data: 137 | raise UserLiveException(TikTokError.ACCOUNT_PRIVATE) 138 | 139 | stream_url = data.get('data', {}).get('stream_url', {}) 140 | 141 | sdk_data_str = stream_url.get('live_core_sdk_data', {}).get('pull_data', {}).get('stream_data') 142 | if not sdk_data_str: 143 | logger.warning("No SDK stream data found. Falling back to legacy URLs. Consider contacting the developer to update the code.") 144 | return (stream_url.get('flv_pull_url', {}).get('FULL_HD1') or 145 | stream_url.get('flv_pull_url', {}).get('HD1') or 146 | stream_url.get('flv_pull_url', {}).get('SD2') or 147 | stream_url.get('flv_pull_url', {}).get('SD1') or 148 | stream_url.get('rtmp_pull_url', '')) 149 | 150 | # Extract stream options 151 | sdk_data = json.loads(sdk_data_str).get('data', {}) 152 | qualities = stream_url.get('live_core_sdk_data', {}).get('pull_data', {}).get('options', {}).get('qualities', []) 153 | if not qualities: 154 | logger.warning("No qualities found in the stream data. Returning None.") 155 | return None 156 | level_map = {q['sdk_key']: q['level'] for q in qualities} 157 | 158 | best_level = -1 159 | best_flv = None 160 | for sdk_key, entry in sdk_data.items(): 161 | level = level_map.get(sdk_key, -1) 162 | stream_main = entry.get('main', {}) 163 | if level > best_level: 164 | best_level = level 165 | best_flv = stream_main.get('flv') 166 | 167 | if not best_flv and data.get('status_code') == 4003110: 168 | raise UserLiveException(TikTokError.LIVE_RESTRICTION) 169 | 170 | return best_flv 171 | 172 | def download_live_stream(self, live_url: str): 173 | """ 174 | Generator che restituisce lo streaming live per un dato room_id. 175 | """ 176 | stream = self.http_client.get(live_url, stream=True) 177 | for chunk in stream.iter_content(chunk_size=4096): 178 | if not chunk: 179 | continue 180 | 181 | yield chunk 182 | -------------------------------------------------------------------------------- /src/core/tiktok_recorder.py: -------------------------------------------------------------------------------- 1 | import os 2 | import time 3 | from http.client import HTTPException 4 | 5 | from requests import RequestException 6 | 7 | from core.tiktok_api import TikTokAPI 8 | from utils.logger_manager import logger 9 | from utils.video_management import VideoManagement 10 | from upload.telegram import Telegram 11 | from utils.custom_exceptions import LiveNotFound, UserLiveException, \ 12 | TikTokException 13 | from utils.enums import Mode, Error, TimeOut, TikTokError 14 | 15 | 16 | class TikTokRecorder: 17 | 18 | def __init__( 19 | self, 20 | url, 21 | user, 22 | room_id, 23 | mode, 24 | automatic_interval, 25 | cookies, 26 | proxy, 27 | output, 28 | duration, 29 | use_telegram, 30 | ): 31 | # Setup TikTok API client 32 | self.tiktok = TikTokAPI(proxy=proxy, cookies=cookies) 33 | 34 | # TikTok Data 35 | self.url = url 36 | self.user = user 37 | self.room_id = room_id 38 | 39 | # Tool Settings 40 | self.mode = mode 41 | self.automatic_interval = automatic_interval 42 | self.duration = duration 43 | self.output = output 44 | 45 | # Upload Settings 46 | self.use_telegram = use_telegram 47 | 48 | # Check if the user's country is blacklisted 49 | self.check_country_blacklisted() 50 | 51 | # Get live information based on the provided user data 52 | if self.url: 53 | self.user, self.room_id = \ 54 | self.tiktok.get_room_and_user_from_url(self.url) 55 | 56 | if not self.user: 57 | self.user = self.tiktok.get_user_from_room_id(self.room_id) 58 | 59 | if not self.room_id: 60 | self.room_id = self.tiktok.get_room_id_from_user(self.user) 61 | 62 | logger.info(f"USERNAME: {self.user}" + ("\n" if not self.room_id else "")) 63 | logger.info(f"ROOM_ID: {self.room_id}" + ("\n" if not self.tiktok.is_room_alive(self.room_id) else "")) 64 | 65 | # If proxy is provided, set up the HTTP client without the proxy 66 | if proxy: 67 | self.tiktok = TikTokAPI(proxy=None, cookies=cookies) 68 | 69 | def run(self): 70 | """ 71 | runs the program in the selected mode. 72 | 73 | If the mode is MANUAL, it checks if the user is currently live and 74 | if so, starts recording. 75 | 76 | If the mode is AUTOMATIC, it continuously checks if the user is live 77 | and if not, waits for the specified timeout before rechecking. 78 | If the user is live, it starts recording. 79 | """ 80 | if self.mode == Mode.MANUAL: 81 | self.manual_mode() 82 | 83 | if self.mode == Mode.AUTOMATIC: 84 | self.automatic_mode() 85 | 86 | def manual_mode(self): 87 | if not self.tiktok.is_room_alive(self.room_id): 88 | raise UserLiveException( 89 | f"@{self.user}: {TikTokError.USER_NOT_CURRENTLY_LIVE}" 90 | ) 91 | 92 | self.start_recording() 93 | 94 | def automatic_mode(self): 95 | while True: 96 | try: 97 | self.room_id = self.tiktok.get_room_id_from_user(self.user) 98 | self.manual_mode() 99 | 100 | except UserLiveException as ex: 101 | logger.info(ex) 102 | logger.info(f"Waiting {self.automatic_interval} minutes before recheck\n") 103 | time.sleep(self.automatic_interval * TimeOut.ONE_MINUTE) 104 | 105 | except ConnectionError: 106 | logger.error(Error.CONNECTION_CLOSED_AUTOMATIC) 107 | time.sleep(TimeOut.CONNECTION_CLOSED * TimeOut.ONE_MINUTE) 108 | 109 | except Exception as ex: 110 | logger.error(f"Unexpected error: {ex}\n") 111 | 112 | def start_recording(self): 113 | """ 114 | Start recording live 115 | """ 116 | live_url = self.tiktok.get_live_url(self.room_id) 117 | if not live_url: 118 | raise LiveNotFound(TikTokError.RETRIEVE_LIVE_URL) 119 | 120 | current_date = time.strftime("%Y.%m.%d_%H-%M-%S", time.localtime()) 121 | 122 | if isinstance(self.output, str) and self.output != '': 123 | if not (self.output.endswith('/') or self.output.endswith('\\')): 124 | if os.name == 'nt': 125 | self.output = self.output + "\\" 126 | else: 127 | self.output = self.output + "/" 128 | 129 | output = f"{self.output if self.output else ''}TK_{self.user}_{current_date}_flv.mp4" 130 | 131 | if self.duration: 132 | logger.info(f"Started recording for {self.duration} seconds ") 133 | else: 134 | logger.info("Started recording...") 135 | 136 | buffer_size = 512 * 1024 # 512 KB buffer 137 | buffer = bytearray() 138 | 139 | logger.info("[PRESS CTRL + C ONCE TO STOP]") 140 | with open(output, "wb") as out_file: 141 | stop_recording = False 142 | while not stop_recording: 143 | try: 144 | if not self.tiktok.is_room_alive(self.room_id): 145 | logger.info("User is no longer live. Stopping recording.") 146 | break 147 | 148 | start_time = time.time() 149 | for chunk in self.tiktok.download_live_stream(live_url): 150 | buffer.extend(chunk) 151 | if len(buffer) >= buffer_size: 152 | out_file.write(buffer) 153 | buffer.clear() 154 | 155 | elapsed_time = time.time() - start_time 156 | if self.duration and elapsed_time >= self.duration: 157 | stop_recording = True 158 | break 159 | 160 | except ConnectionError: 161 | if self.mode == Mode.AUTOMATIC: 162 | logger.error(Error.CONNECTION_CLOSED_AUTOMATIC) 163 | time.sleep(TimeOut.CONNECTION_CLOSED * TimeOut.ONE_MINUTE) 164 | 165 | except (RequestException, HTTPException): 166 | time.sleep(2) 167 | 168 | except KeyboardInterrupt: 169 | logger.info("Recording stopped by user.") 170 | stop_recording = True 171 | 172 | except Exception as ex: 173 | logger.error(f"Unexpected error: {ex}\n") 174 | stop_recording = True 175 | 176 | finally: 177 | if buffer: 178 | out_file.write(buffer) 179 | buffer.clear() 180 | out_file.flush() 181 | 182 | logger.info(f"Recording finished: {output}\n") 183 | VideoManagement.convert_flv_to_mp4(output) 184 | 185 | if self.use_telegram: 186 | Telegram().upload(output.replace('_flv.mp4', '.mp4')) 187 | 188 | def check_country_blacklisted(self): 189 | is_blacklisted = self.tiktok.is_country_blacklisted() 190 | if not is_blacklisted: 191 | return False 192 | 193 | if self.room_id is None: 194 | raise TikTokException(TikTokError.COUNTRY_BLACKLISTED) 195 | 196 | if self.mode == Mode.AUTOMATIC: 197 | raise TikTokException(TikTokError.COUNTRY_BLACKLISTED_AUTO_MODE) 198 | -------------------------------------------------------------------------------- /src/http_utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Michele0303/tiktok-live-recorder/fa9683f177ad56edb0d51f31c407561d81bafc1c/src/http_utils/__init__.py -------------------------------------------------------------------------------- /src/http_utils/http_client.py: -------------------------------------------------------------------------------- 1 | import requests as req 2 | 3 | from utils.enums import StatusCode 4 | from utils.logger_manager import logger 5 | 6 | 7 | class HttpClient: 8 | 9 | def __init__(self, proxy=None, cookies=None): 10 | self.req = None 11 | self.proxy = proxy 12 | self.cookies = cookies 13 | self.configure_session() 14 | 15 | def configure_session(self) -> None: 16 | self.req = req.Session() 17 | self.req.headers.update({ 18 | "Sec-Ch-Ua": "\"Not/A)Brand\";v=\"8\", \"Chromium\";v=\"126\"", 19 | "Sec-Ch-Ua-Mobile": "?0", "Sec-Ch-Ua-Platform": "\"Linux\"", 20 | "Accept-Language": "en-US", "Upgrade-Insecure-Requests": "1", 21 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.127 Safari/537.36", 22 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", 23 | "Sec-Fetch-Site": "none", "Sec-Fetch-Mode": "navigate", 24 | "Sec-Fetch-User": "?1", "Sec-Fetch-Dest": "document", 25 | "Priority": "u=0, i", 26 | "Referer": "https://www.tiktok.com/" 27 | }) 28 | 29 | if self.cookies is not None: 30 | self.req.cookies.update(self.cookies) 31 | 32 | self.check_proxy() 33 | 34 | def check_proxy(self) -> None: 35 | if self.proxy is None: 36 | return 37 | 38 | logger.info(f"Testing {self.proxy}...") 39 | proxies = {'http': self.proxy, 'https': self.proxy} 40 | 41 | response = req.get( 42 | "https://ifconfig.me/ip", 43 | proxies=proxies, 44 | timeout=10 45 | ) 46 | 47 | if response.status_code == StatusCode.OK: 48 | self.req.proxies.update(proxies) 49 | logger.info("Proxy set up successfully") 50 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | # print banner 2 | from utils.utils import banner 3 | 4 | banner() 5 | 6 | # check and install dependencies 7 | from utils.dependencies import check_and_install_dependencies 8 | 9 | check_and_install_dependencies() 10 | 11 | from check_updates import check_updates 12 | 13 | import sys 14 | import os 15 | 16 | from utils.args_handler import validate_and_parse_args 17 | from utils.utils import read_cookies 18 | from utils.logger_manager import logger 19 | 20 | from core.tiktok_recorder import TikTokRecorder 21 | from utils.enums import TikTokError 22 | from utils.custom_exceptions import LiveNotFound, ArgsParseError, \ 23 | UserLiveException, IPBlockedByWAF, TikTokException 24 | 25 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 26 | 27 | 28 | def main(): 29 | try: 30 | args, mode = validate_and_parse_args() 31 | 32 | # check for updates 33 | if args.update_check is True: 34 | logger.info("Checking for updates...\n") 35 | if check_updates(): 36 | exit() 37 | else: 38 | logger.info("Skipped update check\n") 39 | 40 | # read cookies from file 41 | cookies = read_cookies() 42 | 43 | TikTokRecorder( 44 | url=args.url, 45 | user=args.user, 46 | room_id=args.room_id, 47 | mode=mode, 48 | automatic_interval=args.automatic_interval, 49 | cookies=cookies, 50 | proxy=args.proxy, 51 | output=args.output, 52 | duration=args.duration, 53 | use_telegram=args.telegram, 54 | ).run() 55 | 56 | except ArgsParseError as ex: 57 | logger.error(ex) 58 | 59 | except LiveNotFound as ex: 60 | logger.error(ex) 61 | 62 | except IPBlockedByWAF: 63 | logger.error(TikTokError.WAF_BLOCKED) 64 | 65 | except UserLiveException as ex: 66 | logger.error(ex) 67 | 68 | except TikTokException as ex: 69 | logger.error(ex) 70 | 71 | except Exception as ex: 72 | logger.error(ex) 73 | 74 | 75 | if __name__ == "__main__": 76 | main() 77 | -------------------------------------------------------------------------------- /src/requirements.txt: -------------------------------------------------------------------------------- 1 | argparse 2 | distro 3 | ffmpeg-python 4 | pyrogram 5 | requests 6 | -------------------------------------------------------------------------------- /src/telegram.json: -------------------------------------------------------------------------------- 1 | { 2 | "api_id": "", 3 | "api_hash": "", 4 | "bot_token": "", 5 | "chat_id": 1110107842 6 | } 7 | -------------------------------------------------------------------------------- /src/upload/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Michele0303/tiktok-live-recorder/fa9683f177ad56edb0d51f31c407561d81bafc1c/src/upload/__init__.py -------------------------------------------------------------------------------- /src/upload/telegram.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from pyrogram import Client 4 | from pyrogram.enums import ParseMode 5 | 6 | from utils.logger_manager import logger 7 | from utils.utils import read_telegram_config 8 | 9 | 10 | FREE_USER_MAX_FILE_SIZE = 2 * 1024 * 1024 * 1024 11 | PREMIUM_USER_MAX_FILE_SIZE = 4 * 1024 * 1024 * 1024 12 | 13 | 14 | class Telegram: 15 | 16 | def __init__(self): 17 | config = read_telegram_config() 18 | 19 | self.api_id = config["api_id"] 20 | self.api_hash = config["api_hash"] 21 | self.bot_token = config["bot_token"] 22 | self.chat_id = config["chat_id"] 23 | 24 | self.app = Client( 25 | 'telegram_session', 26 | api_id=self.api_id, 27 | api_hash=self.api_hash, 28 | bot_token=self.bot_token 29 | ) 30 | 31 | def upload(self, file_path: str): 32 | """ 33 | Upload a file to the bot's own chat (saved messages). 34 | """ 35 | try: 36 | self.app.start() 37 | 38 | me = self.app.get_me() 39 | is_premium = me.is_premium 40 | max_size = ( 41 | PREMIUM_USER_MAX_FILE_SIZE 42 | if is_premium else FREE_USER_MAX_FILE_SIZE 43 | ) 44 | 45 | file_size = Path(file_path).stat().st_size 46 | logger.info(f"File to upload: {Path(file_path).name} " 47 | f"({round(file_size / (1024 * 1024))} MB)") 48 | 49 | if file_size > max_size: 50 | logger.warning("The file is too large to be " 51 | "uploaded with this type of account.") 52 | return 53 | 54 | logger.info(f"Uploading video on Telegram... This may take a while depending on the file size.") 55 | self.app.send_document( 56 | chat_id=self.chat_id, 57 | document=file_path, 58 | caption=( 59 | '🎥 Video recorded via ' 60 | 'TikTok Live Recorder' 61 | ), 62 | parse_mode=ParseMode.HTML, 63 | force_document=True, 64 | ) 65 | logger.info("File successfully uploaded to Telegram.\n") 66 | 67 | except Exception as e: 68 | logger.error(f"Error during Telegram upload: {e}\n") 69 | 70 | finally: 71 | self.app.stop() 72 | -------------------------------------------------------------------------------- /src/utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Michele0303/tiktok-live-recorder/fa9683f177ad56edb0d51f31c407561d81bafc1c/src/utils/__init__.py -------------------------------------------------------------------------------- /src/utils/args_handler.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import re 3 | 4 | from utils.custom_exceptions import ArgsParseError 5 | from utils.enums import Mode, Regex 6 | 7 | 8 | def parse_args(): 9 | """ 10 | Parse command line arguments. 11 | """ 12 | parser = argparse.ArgumentParser( 13 | description="TikTok Live Recorder - A tool for recording live TikTok sessions.", 14 | formatter_class=argparse.RawTextHelpFormatter 15 | ) 16 | 17 | parser.add_argument( 18 | "-url", 19 | dest="url", 20 | help="Record a live session from the TikTok URL.", 21 | action='store' 22 | ) 23 | 24 | parser.add_argument( 25 | "-user", 26 | dest="user", 27 | help="Record a live session from the TikTok username.", 28 | action='store' 29 | ) 30 | 31 | parser.add_argument( 32 | "-room_id", 33 | dest="room_id", 34 | help="Record a live session from the TikTok room ID.", 35 | action='store' 36 | ) 37 | 38 | parser.add_argument( 39 | "-mode", 40 | dest="mode", 41 | help=( 42 | "Recording mode: (manual, automatic) [Default: manual]\n" 43 | "[manual] => Manual live recording.\n" 44 | "[automatic] => Automatic live recording when the user is live." 45 | ), 46 | default="manual", 47 | action='store' 48 | ) 49 | 50 | parser.add_argument( 51 | "-automatic_interval", 52 | dest="automatic_interval", 53 | help="Sets the interval in minutes to check if the user is live in automatic mode. [Default: 5]", 54 | type=int, 55 | default=5, 56 | action='store' 57 | ) 58 | 59 | 60 | parser.add_argument( 61 | "-proxy", 62 | dest="proxy", 63 | help=( 64 | "Use HTTP proxy to bypass login restrictions in some countries.\n" 65 | "Example: -proxy http://127.0.0.1:8080" 66 | ), 67 | action='store' 68 | ) 69 | 70 | parser.add_argument( 71 | "-output", 72 | dest="output", 73 | help=( 74 | "Specify the output directory where recordings will be saved.\n" 75 | ), 76 | action='store' 77 | ) 78 | 79 | parser.add_argument( 80 | "-duration", 81 | dest="duration", 82 | help="Specify the duration in seconds to record the live session [Default: None].", 83 | type=int, 84 | default=None, 85 | action='store' 86 | ) 87 | 88 | parser.add_argument( 89 | "-telegram", 90 | dest="telegram", 91 | action="store_true", 92 | help="Activate the option to upload the video to Telegram at the end " 93 | "of the recording.\nRequires configuring the telegram.json file", 94 | ) 95 | 96 | parser.add_argument( 97 | "-no-update-check", 98 | dest="update_check", 99 | action="store_false", 100 | help=( 101 | "Disable the check for updates before running the program. " 102 | "By default, update checking is enabled." 103 | ) 104 | ) 105 | 106 | args = parser.parse_args() 107 | 108 | return args 109 | 110 | 111 | def validate_and_parse_args(): 112 | args = parse_args() 113 | 114 | if not args.user and not args.room_id and not args.url: 115 | raise ArgsParseError("Missing URL, username, or room ID. Please provide one of these parameters.") 116 | 117 | if args.user and args.user.startswith('@'): 118 | args.user = args.user[1:] 119 | 120 | if not args.mode: 121 | raise ArgsParseError("Missing mode value. Please specify the mode (manual or automatic).") 122 | if args.mode not in ["manual", "automatic"]: 123 | raise ArgsParseError("Incorrect mode value. Choose between 'manual' and 'automatic'.") 124 | 125 | if args.url and not re.match(str(Regex.IS_TIKTOK_LIVE), args.url): 126 | raise ArgsParseError("The provided URL does not appear to be a valid TikTok live URL.") 127 | 128 | if (args.user and args.room_id) or (args.user and args.url) or (args.room_id and args.url): 129 | raise ArgsParseError("Please provide only one among username, room ID, or URL.") 130 | 131 | if (args.automatic_interval < 1): 132 | raise ArgsParseError("Incorrect automatic_interval value. Must be one minute or more.") 133 | 134 | mode = Mode.MANUAL if args.mode == "manual" else Mode.AUTOMATIC 135 | 136 | return args, mode 137 | -------------------------------------------------------------------------------- /src/utils/custom_exceptions.py: -------------------------------------------------------------------------------- 1 | from utils.enums import TikTokError 2 | 3 | 4 | class TikTokException(Exception): 5 | def __init__(self, message): 6 | super().__init__(message) 7 | 8 | 9 | class UserLiveException(Exception): 10 | def __init__(self, message): 11 | super().__init__(message) 12 | 13 | 14 | class IPBlockedByWAF(Exception): 15 | def __init__(self, message=TikTokError.WAF_BLOCKED): 16 | super().__init__(message) 17 | 18 | 19 | class LiveNotFound(Exception): 20 | pass 21 | 22 | 23 | class ArgsParseError(Exception): 24 | pass 25 | -------------------------------------------------------------------------------- /src/utils/dependencies.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import sys 3 | import platform 4 | from subprocess import SubprocessError 5 | 6 | from .logger_manager import logger 7 | 8 | 9 | def check_distro_library(): 10 | try: 11 | import distro 12 | return True 13 | except ModuleNotFoundError: 14 | logger.error("distro library is not installed") 15 | return False 16 | 17 | 18 | def install_distro_library(): 19 | try: 20 | subprocess.run( 21 | [sys.executable, "-m", "pip", "install", "distro", "--break-system-packages"], 22 | stdout=subprocess.DEVNULL, 23 | stderr=subprocess.STDOUT, 24 | check=True, 25 | ) 26 | logger.info("distro installed successfully\n") 27 | except SubprocessError as e: 28 | logger.error(f"Error: {e}") 29 | exit(1) 30 | 31 | 32 | def check_ffmpeg_binary(): 33 | try: 34 | subprocess.run( 35 | ["ffmpeg"], 36 | stdout=subprocess.DEVNULL, 37 | stderr=subprocess.STDOUT, 38 | ) 39 | return True 40 | except FileNotFoundError: 41 | logger.error("FFmpeg binary is not installed") 42 | return False 43 | 44 | 45 | def install_ffmpeg_binary(): 46 | try: 47 | logger.error('Please, install FFmpeg with this command:') 48 | if platform.system().lower() == "linux": 49 | 50 | import distro 51 | linux_family = distro.like() 52 | if linux_family == "debian": 53 | logger.info('sudo apt install ffmpeg') 54 | elif linux_family == "redhat": 55 | logger.info('sudo dnf install ffmpeg / sudo yum install ffmpeg') 56 | elif linux_family == "arch": 57 | logger.info('sudo pacman -S ffmpeg') 58 | elif linux_family == "": # Termux 59 | logger.info('pkg install ffmpeg') 60 | else: 61 | logger.info(f"Distro linux not supported (family: {linux_family})") 62 | 63 | elif platform.system().lower() == "windows": 64 | logger.info('choco install ffmpeg or follow: https://phoenixnap.com/kb/ffmpeg-windows') 65 | 66 | elif platform.system().lower() == "darwin": 67 | logger.info('brew install ffmpeg') 68 | 69 | else: 70 | logger.info(f"OS not supported: {platform}") 71 | 72 | except Exception as e: 73 | logger.error(f"Error: {e}") 74 | 75 | exit(1) 76 | 77 | 78 | def check_ffmpeg_library(): 79 | try: 80 | import ffmpeg 81 | return True 82 | except ModuleNotFoundError: 83 | logger.error("ffmpeg-python library is not installed") 84 | return False 85 | 86 | 87 | def install_ffmpeg_library(): 88 | try: 89 | subprocess.run( 90 | [sys.executable, "-m", "pip", "install", "ffmpeg-python", "--break-system-packages"], 91 | stdout=subprocess.DEVNULL, 92 | stderr=subprocess.STDOUT, 93 | check=True, 94 | ) 95 | logger.info("ffmpeg-python installed successfully\n") 96 | except SubprocessError as e: 97 | logger.error(f"Error: {e}") 98 | exit(1) 99 | 100 | 101 | def check_argparse_library(): 102 | try: 103 | import argparse 104 | return True 105 | except ModuleNotFoundError: 106 | logger.error("argparse library is not installed") 107 | return False 108 | 109 | 110 | def install_argparse_library(): 111 | try: 112 | subprocess.run( 113 | [sys.executable, "-m", "pip", "install", "argparse", "--break-system-packages"], 114 | stdout=subprocess.DEVNULL, 115 | stderr=subprocess.STDOUT, 116 | check=True, 117 | ) 118 | logger.info("argparse installed successfully\n") 119 | except SubprocessError as e: 120 | logger.error(f"Error: {e}") 121 | exit(1) 122 | 123 | 124 | def check_requests_library(): 125 | try: 126 | import requests 127 | return True 128 | except ModuleNotFoundError: 129 | logger.error("requests library is not installed") 130 | return False 131 | 132 | 133 | def check_pyrogram_library(): 134 | try: 135 | import pyrogram 136 | return True 137 | except ModuleNotFoundError: 138 | logger.error("pyrogram library is not installed") 139 | return False 140 | 141 | 142 | def install_pyrogram_library(): 143 | try: 144 | subprocess.run( 145 | [sys.executable, "-m", "pip", "install", "pyrogram", "--break-system-packages"], 146 | stdout=subprocess.DEVNULL, 147 | stderr=subprocess.STDOUT, 148 | check=True, 149 | ) 150 | logger.info("pyrogram installed successfully\n") 151 | except SubprocessError as e: 152 | logger.error(f"Error: {e}") 153 | exit(1) 154 | 155 | 156 | def install_requests_library(): 157 | try: 158 | subprocess.run( 159 | [sys.executable, "-m", "pip", "install", "requests", "--break-system-packages"], 160 | stdout=subprocess.DEVNULL, 161 | stderr=subprocess.STDOUT, 162 | check=True, 163 | ) 164 | logger.info("requests installed successfully\n") 165 | except SubprocessError as e: 166 | logger.error(f"Error: {e}") 167 | exit(1) 168 | 169 | 170 | def check_and_install_dependencies(): 171 | logger.info("Checking and Installing dependencies...\n") 172 | 173 | if not check_distro_library(): 174 | install_distro_library() 175 | 176 | if not check_ffmpeg_library(): 177 | install_ffmpeg_library() 178 | 179 | if not check_argparse_library(): 180 | install_argparse_library() 181 | 182 | if not check_requests_library(): 183 | install_requests_library() 184 | 185 | if not check_pyrogram_library(): 186 | install_pyrogram_library() 187 | 188 | if not check_ffmpeg_binary(): 189 | install_ffmpeg_binary() 190 | -------------------------------------------------------------------------------- /src/utils/enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, IntEnum 2 | 3 | 4 | class Regex(Enum): 5 | 6 | def __str__(self): 7 | return str(self.value) 8 | 9 | IS_TIKTOK_LIVE = r".*www\.tiktok\.com.*|.*vm\.tiktok\.com.*" 10 | 11 | 12 | class TimeOut(IntEnum): 13 | """ 14 | Enumeration that defines timeout values. 15 | """ 16 | 17 | def __mul__(self, operator): 18 | return self.value * operator 19 | 20 | ONE_MINUTE = 60 21 | AUTOMATIC_MODE = 5 22 | CONNECTION_CLOSED = 2 23 | 24 | 25 | class StatusCode(IntEnum): 26 | OK = 200 27 | REDIRECT = 302 28 | MOVED = 301 29 | 30 | 31 | class Mode(IntEnum): 32 | """ 33 | Enumeration that represents the recording modes. 34 | """ 35 | MANUAL = 0 36 | AUTOMATIC = 1 37 | 38 | 39 | class Error(Enum): 40 | """ 41 | Enumeration that contains possible errors while using TikTok-Live-Recorder. 42 | """ 43 | 44 | def __str__(self): 45 | return str(self.value) 46 | 47 | 48 | 49 | CONNECTION_CLOSED = "Connection broken by the server." 50 | CONNECTION_CLOSED_AUTOMATIC = f"{CONNECTION_CLOSED}. Try again after delay of {TimeOut.CONNECTION_CLOSED} minutes" 51 | 52 | 53 | class TikTokError(Enum): 54 | """ 55 | Enumeration that contains possible errors of TikTok 56 | """ 57 | 58 | def __str__(self): 59 | return str(self.value) 60 | 61 | COUNTRY_BLACKLISTED = 'Captcha required or country blocked. ' \ 62 | 'Use a VPN, room_id, or authenticate with cookies.\n' \ 63 | 'How to set cookies: https://github.com/Michele0303/tiktok-live-recorder/blob/main/GUIDE.md#how-to-set-cookies\n' \ 64 | 'How to get room_id: https://github.com/Michele0303/TikTok-Live-Recorder/blob/main/GUIDE.md#how-to-get-room_id\n' 65 | 66 | COUNTRY_BLACKLISTED_AUTO_MODE = \ 67 | 'Automatic mode is available only in unblocked countries. ' \ 68 | 'Use a VPN or authenticate with cookies.\n' \ 69 | 'How to set cookies: https://github.com/Michele0303/tiktok-live-recorder/blob/main/GUIDE.md#how-to-set-cookies\n' 70 | 71 | ACCOUNT_PRIVATE = 'Account is private, login required. ' \ 72 | 'Please add your cookies to cookies.json ' \ 73 | 'https://github.com/Michele0303/tiktok-live-recorder/blob/main/GUIDE.md#how-to-set-cookies' 74 | 75 | ACCOUNT_PRIVATE_FOLLOW = 'This account is private. Follow the creator to access their LIVE.' 76 | 77 | LIVE_RESTRICTION = 'Live is private, login required. ' \ 78 | 'Please add your cookies to cookies.json' \ 79 | 'https://github.com/Michele0303/tiktok-live-recorder/blob/main/GUIDE.md#how-to-set-cookies' 80 | 81 | USERNAME_ERROR = 'Username / RoomID not found or the user has never been in live.' 82 | 83 | ROOM_ID_ERROR = 'Error extracting RoomID' 84 | 85 | USER_NEVER_BEEN_LIVE = "The user has never hosted a live stream on TikTok." 86 | 87 | USER_NOT_CURRENTLY_LIVE = "The user is not hosting a live stream at the moment." 88 | 89 | RETRIEVE_LIVE_URL = 'Unable to retrieve live streaming url. Please try again later.' 90 | 91 | INVALID_TIKTOK_LIVE_URL = 'The provided URL is not a valid TikTok live stream.' 92 | 93 | WAF_BLOCKED = 'Your IP is blocked by TikTok WAF. Please change your IP address.' 94 | 95 | 96 | 97 | class Info(Enum): 98 | """ 99 | Enumeration that defines the version number and the banner message. 100 | """ 101 | 102 | def __str__(self): 103 | return str(self.value) 104 | 105 | def __iter__(self): 106 | return iter(self.value) 107 | 108 | NEW_FEATURES = [ 109 | "Bug fixes", 110 | ] 111 | 112 | VERSION = 6.4 113 | BANNER = fr""" 114 | 115 | _____ _ _ _____ _ _ _ ___ _ 116 | |_ _(_) |_|_ _|__| |__ | | (_)_ _____ | _ \___ __ ___ _ _ __| |___ _ _ 117 | | | | | / / | |/ _ \ / / | |__| \ V / -_) | / -_) _/ _ \ '_/ _` / -_) '_| 118 | |_| |_|_\_\ |_|\___/_\_\ |____|_|\_/\___| |_|_\___\__\___/_| \__,_\___|_| 119 | 120 | V{VERSION} 121 | """ 122 | -------------------------------------------------------------------------------- /src/utils/logger_manager.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | class MaxLevelFilter(logging.Filter): 4 | """ 5 | Filter that only allows log records up to a specified maximum level. 6 | """ 7 | def __init__(self, max_level): 8 | super().__init__() 9 | self.max_level = max_level 10 | 11 | def filter(self, record): 12 | # Only accept records whose level number is <= self.max_level 13 | return record.levelno <= self.max_level 14 | 15 | class LoggerManager: 16 | 17 | _instance = None # Singleton instance 18 | 19 | def __new__(cls): 20 | if cls._instance is None: 21 | cls._instance = super(LoggerManager, cls).__new__(cls) 22 | cls._instance.logger = None 23 | cls._instance.setup_logger() 24 | return cls._instance 25 | 26 | def setup_logger(self): 27 | if self.logger is None: 28 | self.logger = logging.getLogger('logger') 29 | self.logger.setLevel(logging.INFO) 30 | 31 | # 1) INFO handler 32 | info_handler = logging.StreamHandler() 33 | info_handler.setLevel(logging.INFO) 34 | info_format = '[*] %(asctime)s - %(message)s' 35 | info_datefmt = '%Y-%m-%d %H:%M:%S' 36 | info_formatter = logging.Formatter(info_format, info_datefmt) 37 | info_handler.setFormatter(info_formatter) 38 | 39 | # Add a filter to exclude ERROR level (and above) messages 40 | info_handler.addFilter(MaxLevelFilter(logging.INFO)) 41 | 42 | self.logger.addHandler(info_handler) 43 | 44 | # 2) ERROR handler 45 | error_handler = logging.StreamHandler() 46 | error_handler.setLevel(logging.ERROR) 47 | error_format = '[!] %(asctime)s - %(message)s' 48 | error_datefmt = '%Y-%m-%d %H:%M:%S' 49 | error_formatter = logging.Formatter(error_format, error_datefmt) 50 | error_handler.setFormatter(error_formatter) 51 | 52 | self.logger.addHandler(error_handler) 53 | 54 | def info(self, message): 55 | """ 56 | Log an INFO-level message. 57 | """ 58 | self.logger.info(message) 59 | 60 | def error(self, message): 61 | """ 62 | Log an ERROR-level message. 63 | """ 64 | self.logger.error(message) 65 | 66 | 67 | logger = LoggerManager().logger 68 | -------------------------------------------------------------------------------- /src/utils/utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | 4 | from utils.enums import Info 5 | 6 | 7 | def banner() -> None: 8 | """ 9 | Prints a banner with the name of the tool and its version number. 10 | """ 11 | print(Info.BANNER) 12 | 13 | 14 | def read_cookies(): 15 | """ 16 | Loads the config file and returns it. 17 | """ 18 | script_dir = os.path.dirname(os.path.abspath(__file__)) 19 | config_path = os.path.join(script_dir, "..", "cookies.json") 20 | with open(config_path, "r") as f: 21 | return json.load(f) 22 | 23 | 24 | def read_telegram_config(): 25 | """ 26 | Loads the telegram config file and returns it. 27 | """ 28 | script_dir = os.path.dirname(os.path.abspath(__file__)) 29 | config_path = os.path.join(script_dir, "..", "telegram.json") 30 | with open(config_path, "r") as f: 31 | return json.load(f) 32 | -------------------------------------------------------------------------------- /src/utils/video_management.py: -------------------------------------------------------------------------------- 1 | import os 2 | import ffmpeg 3 | 4 | from utils.logger_manager import logger 5 | 6 | 7 | class VideoManagement: 8 | 9 | @staticmethod 10 | def convert_flv_to_mp4(file): 11 | """ 12 | Convert the video from flv format to mp4 format 13 | """ 14 | logger.info("Converting {} to MP4 format...".format(file)) 15 | 16 | try: 17 | ffmpeg.input(file).output( 18 | file.replace('_flv.mp4', '.mp4'), 19 | c='copy', 20 | y='-y', 21 | ).run(quiet=True) 22 | except ffmpeg.Error as e: 23 | logger.error(f"ffmpeg error: {e.stderr.decode() if hasattr(e, 'stderr') else str(e)}") 24 | 25 | os.remove(file) 26 | 27 | logger.info("Finished converting {}\n".format(file)) 28 | --------------------------------------------------------------------------------