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 |
--------------------------------------------------------------------------------