TikPipe 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tikpipe/__init__.py +0 -0
- tikpipe/cli.py +218 -0
- tikpipe-1.0.0.dist-info/METADATA +164 -0
- tikpipe-1.0.0.dist-info/RECORD +7 -0
- tikpipe-1.0.0.dist-info/WHEEL +5 -0
- tikpipe-1.0.0.dist-info/entry_points.txt +3 -0
- tikpipe-1.0.0.dist-info/top_level.txt +1 -0
tikpipe/__init__.py
ADDED
|
File without changes
|
tikpipe/cli.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import time
|
|
4
|
+
import json
|
|
5
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
6
|
+
from playwright.sync_api import sync_playwright
|
|
7
|
+
import yt_dlp
|
|
8
|
+
|
|
9
|
+
# Ubicación global de la sesión (para que funcione desde cualquier carpeta de Windows)
|
|
10
|
+
GLOBAL_DIR = os.path.expanduser("~/.tikpipe")
|
|
11
|
+
SESSION_FOLDER = os.path.join(GLOBAL_DIR, "perfil_navegador")
|
|
12
|
+
CONFIG_FILE = os.path.join(GLOBAL_DIR, "config.json")
|
|
13
|
+
LINKS_FILE = os.path.join(GLOBAL_DIR, "videos.txt")
|
|
14
|
+
|
|
15
|
+
os.makedirs(GLOBAL_DIR, exist_ok=True)
|
|
16
|
+
os.makedirs(SESSION_FOLDER, exist_ok=True)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def load_configuration():
|
|
20
|
+
default_config = {
|
|
21
|
+
"download_path": os.path.abspath("./TikTok_Downloads"),
|
|
22
|
+
"video_count": 10,
|
|
23
|
+
"concurrent_downloads": 6
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if not os.path.exists(CONFIG_FILE):
|
|
27
|
+
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
|
|
28
|
+
json.dump(default_config, f, indent=4)
|
|
29
|
+
return default_config
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
|
|
33
|
+
config = json.load(f)
|
|
34
|
+
path = config.get("download_path", default_config["download_path"])
|
|
35
|
+
count = config.get("video_count", default_config["video_count"])
|
|
36
|
+
concurrency = config.get("concurrent_downloads", default_config["concurrent_downloads"])
|
|
37
|
+
return {
|
|
38
|
+
"download_path": os.path.abspath(path),
|
|
39
|
+
"video_count": int(count),
|
|
40
|
+
"concurrent_downloads": int(concurrency)
|
|
41
|
+
}
|
|
42
|
+
except Exception:
|
|
43
|
+
return default_config
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def parse_arguments():
|
|
47
|
+
config = load_configuration()
|
|
48
|
+
|
|
49
|
+
# Si el usuario pide ayuda
|
|
50
|
+
if len(sys.argv) > 1 and sys.argv[1] in ("-h", "--help"):
|
|
51
|
+
print("\nTikPipe - TikTok Feed Harvester & Fast Downloader")
|
|
52
|
+
print("Usage: TikPipe [video_count] [concurrent_downloads] \"[download_path]\"")
|
|
53
|
+
print("Example: TikPipe 20 6 \"D:\\My Videos\\TikTok\"\n")
|
|
54
|
+
sys.exit(0)
|
|
55
|
+
|
|
56
|
+
# Argumento 1: Cantidad de videos
|
|
57
|
+
if len(sys.argv) >= 2:
|
|
58
|
+
try:
|
|
59
|
+
config["video_count"] = int(sys.argv[1])
|
|
60
|
+
except ValueError:
|
|
61
|
+
print(f"[Warning] Invalid video count '{sys.argv[1]}'. Using: {config['video_count']}")
|
|
62
|
+
|
|
63
|
+
# Argumento 2: Descargas simultáneas
|
|
64
|
+
if len(sys.argv) >= 3:
|
|
65
|
+
try:
|
|
66
|
+
config["concurrent_downloads"] = int(sys.argv[2])
|
|
67
|
+
except ValueError:
|
|
68
|
+
print(f"[Warning] Invalid concurrency '{sys.argv[2]}'. Using: {config['concurrent_downloads']}")
|
|
69
|
+
|
|
70
|
+
# Argumento 3: Ruta de descarga entre comillas
|
|
71
|
+
if len(sys.argv) >= 4:
|
|
72
|
+
config["download_path"] = os.path.abspath(sys.argv[3])
|
|
73
|
+
|
|
74
|
+
return config
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def download_single_video(url, ydl_opts):
|
|
78
|
+
try:
|
|
79
|
+
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
|
80
|
+
ydl.download([url])
|
|
81
|
+
return True, url
|
|
82
|
+
except Exception:
|
|
83
|
+
return False, url
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def main():
|
|
87
|
+
config = parse_arguments()
|
|
88
|
+
download_folder = config["download_path"]
|
|
89
|
+
target_count = config["video_count"]
|
|
90
|
+
concurrency = config["concurrent_downloads"]
|
|
91
|
+
|
|
92
|
+
os.makedirs(download_folder, exist_ok=True)
|
|
93
|
+
|
|
94
|
+
ydl_opts = {
|
|
95
|
+
'format': (
|
|
96
|
+
'bestvideo[vcodec^=avc]+bestaudio[acodec=mp4a.40.2]/'
|
|
97
|
+
'best[acodec=mp4a.40.2]/'
|
|
98
|
+
'bestvideo[vcodec^=avc]+bestaudio[acodec^=mp4a]/'
|
|
99
|
+
'best[vcodec^=avc]/'
|
|
100
|
+
'best'
|
|
101
|
+
),
|
|
102
|
+
'merge_output_format': 'mp4',
|
|
103
|
+
'outtmpl': os.path.join(download_folder, '%(uploader)s_%(id)s.%(ext)s'),
|
|
104
|
+
'ignoreerrors': True,
|
|
105
|
+
'nooverwrites': True,
|
|
106
|
+
'quiet': True,
|
|
107
|
+
'buffersize': 1024 * 1024 * 4,
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
intercepted_videos = set()
|
|
111
|
+
download_futures = []
|
|
112
|
+
executor = ThreadPoolExecutor(max_workers=concurrency)
|
|
113
|
+
|
|
114
|
+
if os.path.exists(LINKS_FILE):
|
|
115
|
+
with open(LINKS_FILE, "w", encoding="utf-8") as f:
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
print("\n" + "="*65)
|
|
119
|
+
print("TikPipe: STARTING PIPELINE MODE")
|
|
120
|
+
print(f"Target: {target_count} videos | Parallel Threads: {concurrency}")
|
|
121
|
+
print(f"Destination: {download_folder}")
|
|
122
|
+
print("="*65 + "\n")
|
|
123
|
+
|
|
124
|
+
with sync_playwright() as p:
|
|
125
|
+
print("Launching optimized browser...")
|
|
126
|
+
|
|
127
|
+
context = p.chromium.launch_persistent_context(
|
|
128
|
+
user_data_dir=SESSION_FOLDER,
|
|
129
|
+
channel="chrome",
|
|
130
|
+
headless=False,
|
|
131
|
+
args=[
|
|
132
|
+
"--disable-blink-features=AutomationControlled",
|
|
133
|
+
"--start-maximized"
|
|
134
|
+
],
|
|
135
|
+
ignore_default_args=["--enable-automation"],
|
|
136
|
+
no_viewport=True
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
page = context.pages[0] if context.pages else context.new_page()
|
|
140
|
+
|
|
141
|
+
def block_heavy_resources(route):
|
|
142
|
+
if route.request.resource_type in ["image", "media", "font"]:
|
|
143
|
+
route.abort()
|
|
144
|
+
else:
|
|
145
|
+
route.continue_()
|
|
146
|
+
|
|
147
|
+
page.route("**/*", block_heavy_resources)
|
|
148
|
+
|
|
149
|
+
def intercept_and_download(response):
|
|
150
|
+
url = response.url
|
|
151
|
+
if ("item_list" in url or "feed" in url) and response.status == 200:
|
|
152
|
+
try:
|
|
153
|
+
data = response.json()
|
|
154
|
+
items = data.get("itemList", [])
|
|
155
|
+
for item in items:
|
|
156
|
+
if len(intercepted_videos) >= target_count:
|
|
157
|
+
break
|
|
158
|
+
video_id = item.get("id")
|
|
159
|
+
author = item.get("author", {}).get("uniqueId")
|
|
160
|
+
if video_id and author:
|
|
161
|
+
link = f"https://www.tiktok.com/@{author}/video/{video_id}"
|
|
162
|
+
if link not in intercepted_videos:
|
|
163
|
+
intercepted_videos.add(link)
|
|
164
|
+
num = len(intercepted_videos)
|
|
165
|
+
print(f"-> [{num}/{target_count}] Intercepted and queued: @{author}")
|
|
166
|
+
|
|
167
|
+
with open(LINKS_FILE, "a", encoding="utf-8") as f:
|
|
168
|
+
f.write(f"{link}\n")
|
|
169
|
+
|
|
170
|
+
fut = executor.submit(download_single_video, link, ydl_opts)
|
|
171
|
+
download_futures.append(fut)
|
|
172
|
+
except Exception:
|
|
173
|
+
pass
|
|
174
|
+
|
|
175
|
+
page.on("response", intercept_and_download)
|
|
176
|
+
|
|
177
|
+
print("Navigating to 'For You' feed...")
|
|
178
|
+
page.goto("https://www.tiktok.com/foryou")
|
|
179
|
+
|
|
180
|
+
elapsed_seconds = 0
|
|
181
|
+
while len(intercepted_videos) < target_count and elapsed_seconds < 40:
|
|
182
|
+
if len(intercepted_videos) >= target_count:
|
|
183
|
+
break
|
|
184
|
+
page.keyboard.press("ArrowDown")
|
|
185
|
+
page.mouse.wheel(0, 500)
|
|
186
|
+
time.sleep(0.8)
|
|
187
|
+
elapsed_seconds += 0.8
|
|
188
|
+
|
|
189
|
+
print(f"\nAll {len(intercepted_videos)} videos intercepted! Closing browser...")
|
|
190
|
+
context.close()
|
|
191
|
+
|
|
192
|
+
print("\nWaiting for active downloads to finish...")
|
|
193
|
+
completed = 0
|
|
194
|
+
total = len(download_futures)
|
|
195
|
+
|
|
196
|
+
for f in as_completed(download_futures):
|
|
197
|
+
ok, link = f.result()
|
|
198
|
+
completed += 1
|
|
199
|
+
if ok:
|
|
200
|
+
print(f"[{completed}/{total}] Download finished: {link}")
|
|
201
|
+
else:
|
|
202
|
+
print(f"[{completed}/{total}] Failed: {link}")
|
|
203
|
+
|
|
204
|
+
executor.shutdown(wait=True)
|
|
205
|
+
|
|
206
|
+
if os.path.exists(LINKS_FILE):
|
|
207
|
+
with open(LINKS_FILE, "w", encoding="utf-8") as f:
|
|
208
|
+
pass
|
|
209
|
+
print(f"\n-> '{LINKS_FILE}' cleared successfully.")
|
|
210
|
+
|
|
211
|
+
print("\n" + "="*65)
|
|
212
|
+
print("TikPipe: PROCESS COMPLETED SUCCESSFULLY!")
|
|
213
|
+
print(f"Videos saved to: {download_folder}")
|
|
214
|
+
print("="*65 + "\n")
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
if __name__ == "__main__":
|
|
218
|
+
main()
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: TikPipe
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: High-performance real-time TikTok feed harvester and concurrent downloader
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: playwright
|
|
8
|
+
Requires-Dist: yt-dlp
|
|
9
|
+
Requires-Dist: curl_cffi
|
|
10
|
+
|
|
11
|
+
# TikTok Feed Harvester & Fast Downloader
|
|
12
|
+
|
|
13
|
+
A high-performance automated script for Windows designed to intercept and download videos from your TikTok **"For You" Page (FYP)** in original quality, watermark-free, utilizing your active session, and in the universally compatible **H.264 (AVC) + AAC** format.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Prerequisites
|
|
18
|
+
|
|
19
|
+
Before getting started, make sure your computer meets the following requirements:
|
|
20
|
+
|
|
21
|
+
1. **Windows 10 or 11 (64-bit)**.
|
|
22
|
+
2. **Python 3.10 or higher** (compatible with Python 3.11, 3.12, and 3.13).
|
|
23
|
+
* When installing Python, make sure to check the box that says **"Add python.exe to PATH"** on the first setup screen.
|
|
24
|
+
3. **Google Chrome** installed officially on your system.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Project Structure
|
|
29
|
+
|
|
30
|
+
Your project directory (e.g., `D:\tiktok extractor\`) should be organized as follows:
|
|
31
|
+
|
|
32
|
+
```text
|
|
33
|
+
tiktok extractor/
|
|
34
|
+
│
|
|
35
|
+
├── user.py # Main capture and download script
|
|
36
|
+
├── requirements.txt # Python dependencies list
|
|
37
|
+
├── update_dependencies.bat # 1-click dependency installer/updater
|
|
38
|
+
├── setup_login.py # One-time login helper script
|
|
39
|
+
├── config.json # Configuration file (auto-generated on first run)
|
|
40
|
+
├── videos.txt # Temporary file storing extracted URLs
|
|
41
|
+
└── perfil_navegador/ # Persistent folder storing your Chrome session
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Step 1: Install Dependencies
|
|
47
|
+
|
|
48
|
+
1. Open the project folder.
|
|
49
|
+
2. **Double-click** the file **`update_dependencies.bat`**.
|
|
50
|
+
3. A command prompt window will open and automatically execute:
|
|
51
|
+
* Upgrading the `pip` package manager.
|
|
52
|
+
* Installing `yt-dlp`, `playwright`, and `curl_cffi`.
|
|
53
|
+
* Downloading the Chromium browser binaries for Playwright.
|
|
54
|
+
4. When it completes and displays `ALL DEPENDENCIES INSTALLED AND UPDATED SUCCESSFULLY!`, press any key to close the window.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Step 2: One-Time Session Setup (Login)
|
|
59
|
+
|
|
60
|
+
To allow the program to access your personalized "For You" feed, it must use your active account. To prevent Google from blocking your login with the *"This browser or app may not be secure"* error, a native launch setup is used:
|
|
61
|
+
|
|
62
|
+
1. Create a file named **`setup_login.py`** in the project folder with the following content:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
import os
|
|
66
|
+
import subprocess
|
|
67
|
+
import sys
|
|
68
|
+
|
|
69
|
+
CARPETA_SESION = os.path.abspath("./perfil_navegador")
|
|
70
|
+
os.makedirs(CARPETA_SESION, exist_ok=True)
|
|
71
|
+
|
|
72
|
+
chrome_paths = [
|
|
73
|
+
os.path.expandvars(r"%ProgramFiles%\Google\Chrome\Application\chrome.exe"),
|
|
74
|
+
os.path.expandvars(r"%ProgramFiles(x86)%\Google\Chrome\Application\chrome.exe"),
|
|
75
|
+
os.path.expandvars(r"%LocalAppData%\Google\Chrome\Application\chrome.exe"),
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
chrome_exe = next((p for p in chrome_paths if os.path.exists(p)), None)
|
|
79
|
+
|
|
80
|
+
if not chrome_exe:
|
|
81
|
+
print("Error: Google Chrome was not found on your system.")
|
|
82
|
+
sys.exit(1)
|
|
83
|
+
|
|
84
|
+
print("Launching Google Chrome for session setup...")
|
|
85
|
+
print("1. Log in to Google and TikTok in the browser tabs that will open.")
|
|
86
|
+
print("2. Once your TikTok account is visible and active, simply CLOSE the Chrome window.")
|
|
87
|
+
|
|
88
|
+
process = subprocess.Popen([
|
|
89
|
+
chrome_exe,
|
|
90
|
+
f"--user-data-dir={CARPETA_SESION}",
|
|
91
|
+
"--no-first-run",
|
|
92
|
+
"https://accounts.google.com",
|
|
93
|
+
"https://www.tiktok.com/login"
|
|
94
|
+
])
|
|
95
|
+
process.wait()
|
|
96
|
+
print("Session successfully saved to ./perfil_navegador!")
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
2. Close all existing Google Chrome windows running on your PC.
|
|
100
|
+
3. Open a terminal in the project directory and run:
|
|
101
|
+
```cmd
|
|
102
|
+
python setup_login.py
|
|
103
|
+
```
|
|
104
|
+
4. A fresh Chrome window will launch:
|
|
105
|
+
* In the first tab, sign in to your Google account.
|
|
106
|
+
* In the second tab, click **"Continue with Google"** on TikTok (it will sign in automatically).
|
|
107
|
+
5. **Close the Chrome window using the red "X" button.**
|
|
108
|
+
6. Your session is now securely saved in `perfil_navegador`, and you will not need to sign in again.
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Step 3: Configuration (`config.json`)
|
|
113
|
+
|
|
114
|
+
The first time you run `user.py`, it will automatically create `config.json`. You can open it in **Notepad** to customize your settings:
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
{
|
|
118
|
+
"download_path": "D:\\tiktok extractor\\TikTok_Downloads",
|
|
119
|
+
"video_count": 10,
|
|
120
|
+
"concurrent_downloads": 6
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
* **`download_path`**: The folder where downloaded videos will be stored (you can set any valid directory path).
|
|
125
|
+
* **`video_count`**: The number of videos you want to harvest from your feed per execution.
|
|
126
|
+
* **`concurrent_downloads`**: How many videos are downloaded simultaneously in parallel (recommended range: 4 to 8).
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Step 4: Running the Program
|
|
131
|
+
|
|
132
|
+
Whenever you want to download videos:
|
|
133
|
+
|
|
134
|
+
1. Open your terminal in the project folder.
|
|
135
|
+
2. Run:
|
|
136
|
+
```cmd
|
|
137
|
+
python user.py
|
|
138
|
+
```
|
|
139
|
+
3. The program will:
|
|
140
|
+
* Launch Chrome in the background with your saved session.
|
|
141
|
+
* Block on-screen images and video playback to save network bandwidth.
|
|
142
|
+
* Intercept your "For You" network feed packets.
|
|
143
|
+
* **Download videos concurrently in real time** while it continues to scroll.
|
|
144
|
+
* Automatically truncate `videos.txt` upon completion and notify you in the console.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## FAQ & Troubleshooting
|
|
149
|
+
|
|
150
|
+
#### 1. Why do I see a "Database is locked" error or Chrome fails to open?
|
|
151
|
+
Make sure you do not have another Google Chrome window open using the same profile folder (`perfil_navegador`) before launching the script.
|
|
152
|
+
|
|
153
|
+
#### 2. Do the downloaded videos have watermarks?
|
|
154
|
+
No. `yt-dlp` extracts the clean, original-quality media streams directly from ByteDance/TikTok content delivery networks.
|
|
155
|
+
|
|
156
|
+
#### 3. How do I download from a specific creator profile instead of my "For You" feed?
|
|
157
|
+
In `user.py`, find the following line:
|
|
158
|
+
```python
|
|
159
|
+
page.goto("https://www.tiktok.com/foryou")
|
|
160
|
+
```
|
|
161
|
+
And replace it with the desired profile URL:
|
|
162
|
+
```python
|
|
163
|
+
page.goto("https://www.tiktok.com/@username")
|
|
164
|
+
```
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
tikpipe/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
tikpipe/cli.py,sha256=ODKosuj5Cylt3VZYSXIoW-R5NMSPq0_utkLXt72Wnbk,7688
|
|
3
|
+
tikpipe-1.0.0.dist-info/METADATA,sha256=kUduYtgcDi1kqbrpNLuL6iFpdSeM2VIgb0Vj5JRkOH4,6154
|
|
4
|
+
tikpipe-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
5
|
+
tikpipe-1.0.0.dist-info/entry_points.txt,sha256=pf2socPtNMpFhAMd7zf7sjwLvvkb64r-1nfaxqDe2x8,72
|
|
6
|
+
tikpipe-1.0.0.dist-info/top_level.txt,sha256=vHP2mq1VF2eLGF-yGVFK2h9rzgpFbffmATkF6XdozvQ,8
|
|
7
|
+
tikpipe-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tikpipe
|