rcdl 2.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.
rcdl/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ # __init__.py
2
+
3
+ from importlib.metadata import version
4
+
5
+ __version__ = version("rcdl")
rcdl/__main__.py ADDED
@@ -0,0 +1,23 @@
1
+ # __main__.py
2
+
3
+ import logging
4
+
5
+ from rcdl.core.config import Config, setup_logging
6
+
7
+ # setup file structure
8
+ Config.ensure_dirs()
9
+ Config.ensure_files()
10
+
11
+ # setup logging
12
+ setup_logging(Config.LOG_FILE, level=0)
13
+
14
+ logging.info("--- INIT ---")
15
+ logging.info("Logger initialized")
16
+
17
+ # init database
18
+ from rcdl.core.db import DB # noqa: E402
19
+
20
+ d = DB()
21
+ d.close()
22
+
23
+ from rcdl.interface.cli import cli # noqa: E402, F401
rcdl/core/api.py ADDED
@@ -0,0 +1,54 @@
1
+ # core/api.py
2
+
3
+ from .models import Creator
4
+
5
+
6
+ class URL:
7
+ DOMAINS_BASE_URL = {
8
+ "coomer": "https://coomer.st/api/v1/",
9
+ "kemono": "https://kemono.cr/api/v1/",
10
+ }
11
+
12
+ @staticmethod
13
+ def get_base_url(domain: str) -> str:
14
+ if domain not in URL.DOMAINS_BASE_URL:
15
+ raise KeyError(f"{domain} not in known domains urls")
16
+ return URL.DOMAINS_BASE_URL[domain]
17
+
18
+ @staticmethod
19
+ def get_post_revision(creator: Creator, post_id) -> str:
20
+ return f"{URL.get_base_url(creator.domain)}{creator.service}/user/{creator.creator_id}/post/{post_id}/revisions"
21
+
22
+ @staticmethod
23
+ def get_headers() -> dict:
24
+ return {
25
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0 Safari/537.36",
26
+ "Accept": "text/css",
27
+ }
28
+
29
+ @staticmethod
30
+ def get_url_from_file(domain: str, path_url: str):
31
+ if domain == "coomer":
32
+ return f"https://coomer.st{path_url}"
33
+ elif domain == "kemono":
34
+ return f"https://kemono.cr{path_url}"
35
+ else:
36
+ raise ValueError(
37
+ f"Domain {domain} is not an accepted value/does not exist. Please check your creators.json file"
38
+ )
39
+
40
+ @staticmethod
41
+ def add_params(url: str, params: dict):
42
+ url += "?"
43
+ for key in params:
44
+ url += f"{key}={params[key]}&"
45
+ return url[:-1]
46
+
47
+ @staticmethod
48
+ def get_creator_post_wo_param(creator: Creator) -> str:
49
+ return f"{URL.get_base_url(creator.domain)}{creator.service}/user/{creator.creator_id}/posts"
50
+
51
+ @staticmethod
52
+ def get_posts_page_url_wo_param():
53
+ domain = URL.DOMAINS_BASE_URL["coomer"]
54
+ return f"{domain}posts"
rcdl/core/config.py ADDED
@@ -0,0 +1,115 @@
1
+ # core/config.py
2
+
3
+ from pathlib import Path
4
+ import json
5
+ import logging
6
+ import os
7
+
8
+ from .file_io import write_json
9
+
10
+
11
+ class Config:
12
+ # paths
13
+ APP_NAME = "cdl"
14
+ BASE_DIR = Path.home() / "Videos/rcdl"
15
+
16
+ BASE_DIR = Path(os.environ.get("RCDL_BASE_DIR", Path.home() / "Videos/rcdl"))
17
+
18
+ CACHE_DIR = BASE_DIR / ".cache"
19
+ DB_PATH = CACHE_DIR / "cdl.db"
20
+ LOG_FILE = CACHE_DIR / "cdl.log"
21
+ FUSE_CSV_FILE = CACHE_DIR / "cdl_fuse.csv"
22
+ SETTINGS_FILE = CACHE_DIR / "settings.json"
23
+ CREATORS_FILE = CACHE_DIR / "creators.json"
24
+ DISCOVER_DIR = CACHE_DIR / "discover"
25
+
26
+ # defaults
27
+ DEFAULT_SETTINGS = {
28
+ "work_folder": str(BASE_DIR),
29
+ "yt_dlp_args": "--external-downloader aria2c",
30
+ "preset": "veryfast",
31
+ }
32
+
33
+ # default creators
34
+ DEFAULT_CREATORS = [
35
+ {"creator_id": "boixd", "service": "onlyfans", "domain": "coomer"}
36
+ ]
37
+
38
+ DEBUG = False
39
+ DRY_RUN = False
40
+
41
+ # api settings
42
+ POST_PER_PAGE = 50
43
+ DEFAULT_MAX_PAGE = 10
44
+ MAX_FAIL_COUNT = 7
45
+
46
+ @classmethod
47
+ def ensure_dirs(cls):
48
+ cls.CACHE_DIR.mkdir(parents=True, exist_ok=True)
49
+ cls.DISCOVER_DIR.mkdir(exist_ok=True)
50
+
51
+ @classmethod
52
+ def ensure_files(cls):
53
+ files = [
54
+ cls.DB_PATH,
55
+ cls.FUSE_CSV_FILE,
56
+ cls.CREATORS_FILE,
57
+ ]
58
+ for file in files:
59
+ if not file.exists():
60
+ file.touch()
61
+ logging.info("Created file %s", file)
62
+ if file == cls.CREATORS_FILE:
63
+ write_json(file, cls.DEFAULT_CREATORS)
64
+
65
+ @classmethod
66
+ def load_settings(cls):
67
+ if not cls.SETTINGS_FILE.exists():
68
+ with open(cls.SETTINGS_FILE, "w") as f:
69
+ json.dump(cls.DEFAULT_SETTINGS, f, indent=4)
70
+ return cls.DEFAULT_SETTINGS
71
+
72
+ with open(cls.SETTINGS_FILE, "r") as f:
73
+ return json.load(f)
74
+
75
+ @classmethod
76
+ def creator_folder(cls, creator_id: str) -> Path:
77
+ folder = cls.BASE_DIR / creator_id
78
+ folder.mkdir(exist_ok=True)
79
+ return folder
80
+
81
+ @classmethod
82
+ def cache_file(cls, filename: str, ext: str = ".json") -> Path:
83
+ file_name = filename + ext
84
+ file = cls.CACHE_DIR / file_name
85
+ return file
86
+
87
+ @classmethod
88
+ def set_debug(cls, debug: bool):
89
+ cls.DEBUG = debug
90
+
91
+ @classmethod
92
+ def set_dry_run(cls, dry_run: bool):
93
+ cls.DRY_RUN = dry_run
94
+
95
+
96
+ def setup_logging(log_file: Path, level: int = 0):
97
+ logger = logging.getLogger()
98
+ logger.setLevel(level)
99
+ logger.handlers.clear() # avoid double handlers if called multiple times
100
+
101
+ # file handler
102
+ file_handler = logging.FileHandler(log_file, encoding="utf-8", mode="a")
103
+ file_handler.setFormatter(
104
+ logging.Formatter(
105
+ "{asctime} - {levelname} - {message}",
106
+ style="{",
107
+ datefmt="%Y-%m-%d %H:%M:%S",
108
+ )
109
+ )
110
+ logger.addHandler(file_handler)
111
+
112
+ # console handler (stdout)
113
+ console_handler = logging.StreamHandler()
114
+ console_handler.setFormatter(logging.Formatter("{levelname}: {message}", style="{"))
115
+ logger.addHandler(console_handler)
rcdl/core/db.py ADDED
@@ -0,0 +1,262 @@
1
+ # core/db.py
2
+
3
+ import sqlite3
4
+ from typing import Optional
5
+ import logging
6
+
7
+ from .config import Config
8
+ from .models import Video, VideoStatus
9
+
10
+
11
+ class DB:
12
+ def __init__(self):
13
+ self.conn = sqlite3.connect(Config.DB_PATH)
14
+ self.conn.row_factory = sqlite3.Row
15
+ self._init_table()
16
+
17
+ def __enter__(self):
18
+ return self
19
+
20
+ def __exit__(self, exc_type, exc_value, traceback):
21
+ self.close()
22
+
23
+ def _init_table(self):
24
+ # init table for videos to DL
25
+ q = """
26
+ CREATE TABLE IF NOT EXISTS videos (
27
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
28
+ post_id TEXT,
29
+ creator_id TEXT,
30
+ service TEXT,
31
+ domain TEXT,
32
+ relative_path TEXT,
33
+ url TEXT,
34
+ part TEXT,
35
+ status TEXT DEFAULT 'not_downloaded',
36
+ fail_count INTEGER DEFAULT 0,
37
+ published TEXT,
38
+ title TEXT,
39
+ substring TEXT,
40
+ downloaded_at TEXT,
41
+ file_size REAL,
42
+ UNIQUE (service, url)
43
+ )
44
+ """
45
+ self.conn.execute(q)
46
+ self.conn.commit()
47
+
48
+ def get_video_status(self, video: Video) -> Optional[dict]:
49
+ cur = self.conn.cursor()
50
+ cur.execute(
51
+ "SELECT status, fail_count FROM videos WHERE (service, url) = (?, ?)",
52
+ (video.service, video.url),
53
+ )
54
+ row = cur.fetchone()
55
+ if row:
56
+ return {
57
+ "status": VideoStatus(row["status"]),
58
+ "fail_count": row["fail_count"],
59
+ "downloaded_at": row["downloaded_at"],
60
+ "relative_path": row["relative_path"],
61
+ }
62
+ return None
63
+
64
+ def get_videos_by_status(self, status: VideoStatus) -> list[Video]:
65
+ cur = self.conn.cursor()
66
+ cur.execute(
67
+ "SELECT * FROM videos WHERE status = ?",
68
+ (status.value,),
69
+ )
70
+ rows = cur.fetchall()
71
+ videos: list[Video] = []
72
+ for row in rows:
73
+ v = Video(
74
+ post_id=row["post_id"],
75
+ creator_id=row["creator_id"],
76
+ service=row["service"],
77
+ domain=row["domain"],
78
+ relative_path=row["relative_path"],
79
+ url=row["url"],
80
+ part=row["part"],
81
+ status=VideoStatus(row["status"]),
82
+ fail_count=row["fail_count"],
83
+ published=row["published"],
84
+ title=row["title"],
85
+ substring=row["substring"],
86
+ downloaded_at=row["downloaded_at"],
87
+ file_size=row["file_size"],
88
+ )
89
+ videos.append(v)
90
+ logging.info(f"DB request status {status.value} returned {len(videos)} results")
91
+ return videos
92
+
93
+ def get_videos_by_creator_id(self, creator_id: str):
94
+ # discover_videos
95
+ cur = self.conn.cursor()
96
+ cur.execute("SELECT * FROM videos WHERE creator_id = ?", (creator_id,))
97
+
98
+ rows = cur.fetchall()
99
+ videos: list[Video] = []
100
+ for row in rows:
101
+ v = Video(
102
+ post_id=row["post_id"],
103
+ creator_id=row["creator_id"],
104
+ service=row["service"],
105
+ domain=row["domain"],
106
+ relative_path=row["relative_path"],
107
+ url=row["url"],
108
+ part=row["part"],
109
+ status=VideoStatus(row["status"]),
110
+ fail_count=row["fail_count"],
111
+ published=row["published"],
112
+ title=row["title"],
113
+ substring=row["substring"],
114
+ downloaded_at=row["downloaded_at"],
115
+ file_size=row["file_size"],
116
+ )
117
+ videos.append(v)
118
+ return videos
119
+
120
+ def mark_not_downloaded(self, video: Video):
121
+ video.status = VideoStatus.NOT_DOWNLOADED
122
+ self._upsert_video(video)
123
+
124
+ def mark_downloaded(self, video: Video):
125
+ video.status = VideoStatus.DOWNLOADED
126
+ self._upsert_video(video)
127
+
128
+ def mark_failed(self, video: Video, fail_count: int):
129
+ video.status = VideoStatus.FAILED
130
+ video.fail_count = fail_count
131
+ self._upsert_video(video)
132
+
133
+ def mark_skipped(self, video: Video):
134
+ video.status = VideoStatus.SKIPPED
135
+ self._upsert_video(video)
136
+
137
+ def mark_ignored(self, video: Video):
138
+ video.status = VideoStatus.IGNORED
139
+ self._upsert_video(video)
140
+
141
+ def _upsert_video(self, video: Video):
142
+ q = """
143
+ INSERT INTO videos (
144
+ post_id, creator_id, service, domain, relative_path, url, part,
145
+ status, fail_count, published, title, substring,
146
+ downloaded_at, file_size
147
+ )
148
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
149
+ ON CONFLICT(service, url) DO UPDATE SET
150
+ status=excluded.status,
151
+ fail_count=excluded.fail_count,
152
+ relative_path=excluded.relative_path,
153
+ downloaded_at=excluded.downloaded_at,
154
+ file_size=excluded.file_size
155
+ """
156
+ if video.status is None:
157
+ video.status = VideoStatus.NOT_DOWNLOADED
158
+
159
+ self.conn.execute(
160
+ q,
161
+ (
162
+ video.post_id,
163
+ video.creator_id,
164
+ video.service,
165
+ video.domain,
166
+ video.relative_path,
167
+ video.url,
168
+ video.part,
169
+ video.status.value,
170
+ video.fail_count,
171
+ video.published,
172
+ video.title,
173
+ video.substring,
174
+ video.downloaded_at,
175
+ video.file_size,
176
+ ),
177
+ )
178
+ self.conn.commit()
179
+
180
+ def get_pending_videos(
181
+ self, max_fail_count: int = Config.MAX_FAIL_COUNT
182
+ ) -> list[Video]:
183
+ cur = self.conn.cursor()
184
+ cur.execute(
185
+ "SELECT * FROM videos WHERE status = ? OR (status = ? AND fail_count < ?)",
186
+ (
187
+ VideoStatus.NOT_DOWNLOADED,
188
+ VideoStatus.FAILED,
189
+ max_fail_count,
190
+ ),
191
+ )
192
+
193
+ rows = cur.fetchall()
194
+ videos: list[Video] = []
195
+
196
+ for row in rows:
197
+ videos.append(
198
+ Video(
199
+ post_id=row["post_id"],
200
+ creator_id=row["creator_id"],
201
+ service=row["service"],
202
+ domain=row["domain"],
203
+ relative_path=row["relative_path"],
204
+ url=row["url"],
205
+ part=row["part"],
206
+ status=VideoStatus(row["status"]),
207
+ fail_count=row["fail_count"],
208
+ published=row["published"],
209
+ title=row["title"],
210
+ substring=row["substring"],
211
+ downloaded_at=row["downloaded_at"],
212
+ file_size=row["file_size"],
213
+ )
214
+ )
215
+
216
+ logging.info(
217
+ "DB pending videos: %d (max_fail_count=%d)", len(videos), max_fail_count
218
+ )
219
+
220
+ return videos
221
+
222
+ def close(self):
223
+ self.conn.close()
224
+
225
+
226
+ def update_db(videos: list[Video]):
227
+ if not videos:
228
+ return
229
+
230
+ with DB() as d:
231
+ q = """
232
+ INSERT OR IGNORE INTO videos (
233
+ post_id, creator_id, service, domain, relative_path, url, part,
234
+ status, fail_count, published, title, substring,
235
+ downloaded_at, file_size
236
+ )
237
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
238
+ """
239
+
240
+ rows = []
241
+ for video in videos:
242
+ rows.append(
243
+ (
244
+ video.post_id,
245
+ video.creator_id,
246
+ video.service,
247
+ video.domain,
248
+ video.relative_path,
249
+ video.url,
250
+ video.part,
251
+ VideoStatus.NOT_DOWNLOADED.value,
252
+ 0,
253
+ video.published,
254
+ video.title,
255
+ video.substring,
256
+ None,
257
+ None,
258
+ )
259
+ )
260
+
261
+ d.conn.executemany(q, rows)
262
+ d.conn.commit()
@@ -0,0 +1,246 @@
1
+ # core/downloader.py
2
+
3
+ import logging
4
+ import os
5
+
6
+ import requests
7
+
8
+ import rcdl.core.parser as parser
9
+ from .api import URL
10
+ from .config import Config
11
+ from .models import Creator, Video
12
+ from .db import DB, update_db
13
+ from .downloader_subprocess import ytdlp_subprocess
14
+ from .file_io import write_json, load_json
15
+ from rcdl.interface.progress import ProgressPrinter
16
+
17
+
18
+ class PostsFetcher:
19
+ def __init__(
20
+ self, url: str, json_path: str, max_page: int = Config.DEFAULT_MAX_PAGE
21
+ ):
22
+ self.url = url
23
+ self.json_path = json_path
24
+
25
+ self.page = 0
26
+ self.max_page = max_page
27
+
28
+ self.status = 200
29
+
30
+ def _request_page(self, url: str) -> requests.Response:
31
+ logging.info(f"RequestEngine url {url}")
32
+ headers = URL.get_headers()
33
+ return requests.get(url, headers=headers)
34
+
35
+ def request(self, continue_on_error: bool = False, params: dict = {}):
36
+ while self.status == 200 and self.page < self.max_page:
37
+ o = self.page * Config.POST_PER_PAGE
38
+ params["o"] = o
39
+ url = URL.add_params(self.url, params)
40
+
41
+ if Config.DRY_RUN:
42
+ logging.debug(f"DRY-RUN posts fetcher {url} -> {self.json_path}")
43
+ self.page += 1
44
+ continue
45
+
46
+ response = self._request_page(url)
47
+ self.status = response.status_code
48
+
49
+ if self.status != 200:
50
+ if self.page == 0:
51
+ logging.error(f"Failed to get {url}")
52
+ else:
53
+ logging.warning(
54
+ f"Status code {self.status}. This behavior is expected."
55
+ )
56
+ if not continue_on_error:
57
+ break
58
+ else:
59
+ logging.info(f"Response Status Code: {self.status}")
60
+ if self.page > 0:
61
+ json_data = list(load_json(self.json_path))
62
+ else:
63
+ json_data = []
64
+
65
+ if "posts" in response.json():
66
+ json_data.extend(response.json()["posts"])
67
+ else:
68
+ json_data.extend(response.json())
69
+
70
+ write_json(self.json_path, json_data, mode="w")
71
+
72
+ self.page += 1
73
+
74
+
75
+ class VideoDownloader:
76
+ def __init__(self, total_dl: int = -1):
77
+ self.progress = ProgressPrinter(total_dl)
78
+
79
+ def _build_url(self, domain: str, video: Video):
80
+ return URL.get_url_from_file(domain, video.url)
81
+
82
+ def _build_output_path(self, video: Video, discover: bool = False):
83
+ if not discover:
84
+ return os.path.join(
85
+ Config.creator_folder(video.creator_id), video.relative_path
86
+ )
87
+ else:
88
+ return os.path.join(Config.DISCOVER_DIR, video.relative_path)
89
+
90
+ def _update_db_status(self, result: int, video: Video):
91
+ with DB() as d:
92
+ if result == 0:
93
+ d.mark_downloaded(video)
94
+ else:
95
+ d.mark_failed(video, video.fail_count + 1)
96
+
97
+ def _exists(self, path: str) -> bool:
98
+ if os.path.exists(path):
99
+ return True
100
+ return False
101
+
102
+ def download(
103
+ self, domain: str, video: Video, write_db: bool = True, discover: bool = False
104
+ ) -> bool:
105
+ url = self._build_url(domain, video)
106
+ path = self._build_output_path(video, discover=discover)
107
+
108
+ if Config.DRY_RUN:
109
+ logging.debug(f"DRY-RUN dl {url} -> {path}")
110
+ return True
111
+
112
+ if self._exists(path):
113
+ logging.warning("Video was already dl ! This should not happen")
114
+ self.progress.update(ignore=True) # update progress but not eta
115
+
116
+ result = ytdlp_subprocess(url, path)
117
+ if write_db:
118
+ self._update_db_status(result, video)
119
+
120
+ self.progress.update()
121
+ self.progress.display()
122
+
123
+ if result == 0:
124
+ return True
125
+ return False
126
+
127
+
128
+ def fetch_posts_by_tag(tag: str, max_page: int = Config.DEFAULT_MAX_PAGE) -> dict:
129
+ url = URL.get_posts_page_url_wo_param()
130
+ path = Config.cache_file(tag)
131
+ pf = PostsFetcher(url, str(path), max_page=max_page)
132
+ pf.request(continue_on_error=True, params={"tag": tag})
133
+
134
+ return load_json(path)
135
+
136
+
137
+ def fetch_posts_by_creator(creator: Creator) -> dict:
138
+ url = URL.get_creator_post_wo_param(creator)
139
+ path = Config.cache_file(f"{creator.creator_id}_{creator.service}")
140
+ pf = PostsFetcher(url, str(path))
141
+ pf.request()
142
+
143
+ return load_json(path)
144
+
145
+
146
+ def refresh_creators_videos(creators: list[Creator]):
147
+ for creator in creators:
148
+ logging.info(
149
+ f"CREATOR {creator.creator_id} from {creator.service} on {creator.domain}"
150
+ )
151
+
152
+ posts = fetch_posts_by_creator(creator)
153
+ logging.info(
154
+ f"Found {len(posts)} posts from creator {creator.creator_id}({creator.service})"
155
+ )
156
+ posts_with_videos = parser.filter_posts_with_videos_from_json(
157
+ str(Config.cache_file(f"{creator.creator_id}_{creator.service}"))
158
+ )
159
+ logging.info(
160
+ f"Find {len(posts_with_videos)} posts with videos from creator {creator.creator_id}({creator.service})"
161
+ )
162
+ all_videos = parser.convert_posts_to_videos(posts_with_videos)
163
+ logging.info(f"Converted {len(posts_with_videos)} to {len(all_videos)} videos")
164
+
165
+ # put all videos in db
166
+ update_db(all_videos)
167
+
168
+
169
+ def download_videos_to_be_dl():
170
+ """
171
+ Download videos of all creators in creators.json
172
+ """
173
+ with DB() as d:
174
+ videos = d.get_pending_videos()
175
+ # VideoDownloader Engine
176
+ vd = VideoDownloader(total_dl=len(videos))
177
+
178
+ for video in videos:
179
+ creator = Creator(
180
+ creator_id=video.creator_id,
181
+ service=video.service,
182
+ domain=video.domain,
183
+ status=None,
184
+ )
185
+ succesful = vd.download(creator.domain, video)
186
+ if not succesful:
187
+ logging.warning("Fail to dl vid")
188
+
189
+
190
+ def discover(tag: str, max_page: int):
191
+ discover_creators(tag, max_page)
192
+ dl_video_from_discover_creators()
193
+
194
+
195
+ def discover_creators(tag: str, max_page: int):
196
+ # download posts with searched tags
197
+ posts = fetch_posts_by_tag(tag, max_page)
198
+ logging.info(f"Find {len(posts)} post")
199
+
200
+ path = str(Config.cache_file(tag))
201
+ posts_with_videos = parser.filter_posts_with_videos_from_json(path)
202
+ logging.info(f"Find {len(posts_with_videos)} posts with videos")
203
+
204
+ creators = parser.get_creators_from_posts(posts_with_videos)
205
+
206
+ # save to csv
207
+ file = os.path.join(Config.DISCOVER_DIR, "discover.csv")
208
+ with open(file, "w") as f:
209
+ for c in creators:
210
+ line = f"{c.creator_id};{c.service};{c.domain};{'to_be_treated'}\n"
211
+ f.write(line)
212
+
213
+
214
+ def dl_video_from_discover_creators():
215
+ # load csv
216
+ file = os.path.join(Config.DISCOVER_DIR, "discover.csv")
217
+ with open(file, "r") as f:
218
+ lines = f.readlines()
219
+
220
+ creators = []
221
+ for line in lines:
222
+ line = line.replace("\n", "").strip().split(";")
223
+ creators.append(
224
+ Creator(creator_id=line[0], service=line[1], domain=line[2], status=line[3])
225
+ )
226
+
227
+ # get posts
228
+ for creator in creators:
229
+ response = requests.get(
230
+ URL.get_creator_post_wo_param(creator), headers=URL.get_headers()
231
+ )
232
+ if response.status_code != 200:
233
+ print(f"ERROR - Request {URL.get_creator_post_wo_param(creator)}")
234
+ response_posts = response.json()
235
+ posts = parser.filter_posts_with_videos_from_list(response_posts)
236
+ print(f"{len(posts)} found")
237
+ if len(posts) > 5:
238
+ posts = posts[0:5]
239
+ print("Limited posts to 5")
240
+
241
+ for post in posts:
242
+ urls = parser.extract_video_urls(post)
243
+ url = URL.get_url_from_file(creator.domain, urls[0])
244
+ filename = f"{post['user']}_{post['id']}.mp4"
245
+ filepath = os.path.join(Config.DISCOVER_DIR, filename)
246
+ ytdlp_subprocess(url, filepath)