├── OFDL.py ├── README.md ├── config.json ├── cookie.png ├── init.png ├── module └── snafylno.py ├── options.png ├── request.png ├── requirements.txt ├── settings.json ├── user_agent.png └── x_bc.png /OFDL.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | import json 5 | import sqlite3 6 | import requests 7 | import module.snafylno as snafylno 8 | from typing import * 9 | from queue import Queue 10 | from PyQt5 import QtGui 11 | from PyQt5 import QtCore 12 | from threading import Thread 13 | from PyQt5.QtGui import QPixmap, QCloseEvent 14 | from module.snafylno import Config 15 | from PyQt5.QtWidgets import QLabel 16 | from PyQt5.QtWidgets import QWidget 17 | from module.snafylno import Onlyfans 18 | from PyQt5.QtWidgets import QTabWidget 19 | from PyQt5.QtWidgets import QComboBox 20 | from PyQt5.QtWidgets import QCheckBox 21 | from PyQt5.QtWidgets import QLineEdit 22 | from PyQt5.QtWidgets import QPushButton 23 | from PyQt5.QtWidgets import QMessageBox 24 | from PyQt5.QtWidgets import QGridLayout 25 | from PyQt5.QtWidgets import QTreeWidget 26 | from PyQt5.QtWidgets import QApplication 27 | from PyQt5.QtWidgets import QTreeWidgetItem 28 | from PyQt5.QtWidgets import QMainWindow 29 | from PyQt5.QtCore import QObject, pyqtSlot, pyqtSignal 30 | 31 | 32 | class Settings: 33 | def __init__(self, filename: str) -> None: 34 | self.filename = filename 35 | self.config = self.load_settings(filename) 36 | 37 | def load_settings(self, filename: str) -> Dict: 38 | data = {} 39 | if os.path.isfile(filename): 40 | file = open(filename, 'r') 41 | data = json.load(file) 42 | file.close() 43 | else: 44 | data["show_avatar"] = False 45 | with open(filename, 'w') as file: 46 | json.dump(data, file) 47 | 48 | return data 49 | 50 | def save_settings(self) -> None: 51 | with open(self.filename, 'w') as file: 52 | json.dump(self.config, file) 53 | 54 | def show_avatar(self) -> bool: 55 | return self.config['show_avatar'] 56 | 57 | def set_option(self, key: str, value) -> str: 58 | self.config[key] = value 59 | self.save_settings() 60 | return self.config[key] 61 | 62 | 63 | class ConfigDlg(QWidget): 64 | def __init__(self, title: str) -> None: 65 | super().__init__() 66 | self.setWindowTitle(title) 67 | self.setGeometry(100, 100, 300, 100) 68 | self.move(350, 45) 69 | self.title = title 70 | 71 | self.edit = QLineEdit(parent = self) 72 | self.edit.move(10, 10) 73 | self.edit.resize(250, 30) 74 | 75 | self.add_node = QPushButton("Ok", parent = self) 76 | self.add_node.move(10, 50) 77 | self.add_node.clicked.connect(self._add_node) 78 | 79 | def _add_node(self) -> None: 80 | if len(self.edit.text()) > 0: 81 | config = Config('config.json') 82 | config.add_node(self.title, self.edit.text()) 83 | self.close() 84 | 85 | 86 | 87 | class OptionWindow(QWidget): 88 | def __init__(self, Onlyfans: snafylno.Onlyfans, 89 | grab_subscriptions: Callable, data_display: QtCore.pyqtBoundSignal) -> None: 90 | super().__init__() 91 | self.setWindowTitle('Options') 92 | self.setGeometry(100, 100, 250, 180) 93 | self.move(300, 45) 94 | self.Onlyfans = Onlyfans 95 | self.grab_subs = grab_subscriptions 96 | self.data_display = data_display 97 | 98 | self.settings = Settings('settings.json') 99 | 100 | self.display_avatar = QPushButton("Show Avatars: {0}".format(self.settings.show_avatar()), parent = self) 101 | self.display_avatar.move(10, 10) 102 | self.display_avatar.clicked.connect(self.change_option_avatar) 103 | 104 | self.add_user_agent = QPushButton("Add a user agent", parent = self) 105 | self.add_user_agent.move(10, 50) 106 | self.add_user_agent.clicked.connect(self.add_useragent) 107 | 108 | self.add_cookie_str = QPushButton("Add cookie", parent = self) 109 | self.add_cookie_str.move(10, 90) 110 | self.add_cookie_str.clicked.connect(self.add_cookie) 111 | 112 | self.add_cookie_str = QPushButton("Add X-BC", parent = self) 113 | self.add_cookie_str.move(10, 130) 114 | self.add_cookie_str.clicked.connect(self.add_x_bc) 115 | 116 | 117 | def check_login(self) -> None: 118 | if self.Onlyfans.user_logged_in() is True: 119 | count = self.grab_subs(self.data_display) 120 | 121 | def closeEvent(self, event) -> None: 122 | if self.Onlyfans.user_logged_in() is not True: 123 | return 124 | 125 | self.Onlyfans.load_config() 126 | thread = Thread(target = self.check_login) 127 | thread.start() 128 | 129 | def add_useragent(self) -> None: 130 | self.user_agent_dialog = ConfigDlg('user-agent') 131 | self.user_agent_dialog.show() 132 | 133 | def add_cookie(self) -> None: 134 | self.cookie_dialog = ConfigDlg('cookie') 135 | self.cookie_dialog.show() 136 | 137 | def add_x_bc(self) -> None: 138 | self.x_bc_dialog = ConfigDlg('x-bc') 139 | self.x_bc_dialog.show() 140 | 141 | 142 | def change_option_avatar(self) -> bool: 143 | current_option = self.settings.show_avatar() 144 | new_option = self.settings.set_option('show_avatar', (not current_option)) 145 | self.display_avatar.setText("Show Avatars: {0}".format(new_option)) 146 | 147 | return new_option 148 | 149 | def show_avatar(self) -> bool: 150 | return self.settings.show_avatar() 151 | 152 | 153 | 154 | 155 | class MainWindow(QWidget): 156 | data_display = QtCore.pyqtSignal(object) 157 | def __init__(self) -> None: 158 | super().__init__() 159 | self.setWindowTitle('OFDL') 160 | self.setGeometry(100, 100, 900, 500) 161 | self.move(60, 15) 162 | 163 | self.Onlyfans = Onlyfans() 164 | self.options_dialog = OptionWindow(self.Onlyfans, self.fetch_and_display_subs, self.data_display) 165 | 166 | layout = QGridLayout() 167 | self.setLayout(layout) 168 | 169 | self.tabs = QTabWidget() 170 | self.tabs.resize(600, 400) 171 | self.general_tab = QWidget() 172 | self.database_tab = QWidget() 173 | self.links_tab = QWidget() 174 | self.download_tab = QWidget() 175 | 176 | self.download_tree = QTreeWidget(parent = self.download_tab) 177 | self.download_tree.setHeaderLabels(["Model", "Path", "Filename"]) 178 | self.download_tree.resize(720, 300) 179 | self.download_tree.move(30, 20) 180 | self.download_tree.columnWidth(300) 181 | 182 | self.information_label_general = QLabel(self.general_tab) 183 | self.information_label_general.move(0, 405) 184 | self.information_label_general.resize(500, 50) 185 | self.information_label_general.setStyleSheet("color: red") 186 | 187 | self.information_label_links = QLabel(self.links_tab) 188 | self.information_label_links.move(0, 405) 189 | self.information_label_links.resize(150, 50) 190 | self.information_label_links.setStyleSheet("color: red") 191 | 192 | self.information_label_download = QLabel(self.download_tab) 193 | self.information_label_download.move(0, 405) 194 | self.information_label_download.resize(550, 50) 195 | self.information_label_download.setStyleSheet("color: red") 196 | 197 | self.information_photo_count = QLabel(self.general_tab) 198 | self.information_photo_count.move(450, 20) 199 | self.information_photo_count.resize(150, 80) 200 | 201 | self.information_video_count = QLabel(self.general_tab) 202 | self.information_video_count.move(600, 20) 203 | self.information_video_count.resize(150, 80) 204 | 205 | self.information_audio_count = QLabel(self.general_tab) 206 | self.information_audio_count.move(450, 80) 207 | self.information_audio_count.resize(150, 80) 208 | 209 | self.information_archive_count = QLabel(self.general_tab) 210 | self.information_archive_count.move(600, 80) 211 | self.information_archive_count.resize(150, 80) 212 | 213 | self.listing_type = "
pip3 install -r requirements.txt
39 |
40 | or
41 |
42 | pip3 install requests
43 | pip3 install pyqt5
44 |
45 | The main script is OFDL.py and can be run on some systems by double clicking it (usually Windows) or by going into the directory using terminal or the command line and executing:
46 |
47 | python3 OFDL.py
48 |
--------------------------------------------------------------------------------
/config.json:
--------------------------------------------------------------------------------
1 | {}
--------------------------------------------------------------------------------
/cookie.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Hashirama/OFDL/36bc39c9b2aed1f4278b4d99182d26532ed068c2/cookie.png
--------------------------------------------------------------------------------
/init.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Hashirama/OFDL/36bc39c9b2aed1f4278b4d99182d26532ed068c2/init.png
--------------------------------------------------------------------------------
/module/snafylno.py:
--------------------------------------------------------------------------------
1 | import os
2 | import json
3 | import math
4 | import time
5 | import sqlite3
6 | import requests
7 | import hashlib
8 | import datetime
9 | import threading
10 | from typing import *
11 | from queue import Queue
12 | from PyQt5 import QtCore
13 | from sqlite3 import Error
14 | from threading import Thread
15 | from PyQt5.QtCore import pyqtSignal, QObject
16 |
17 | ALL = 0b1111111
18 | MESSAGES = 0b1000000
19 | PICTURES = 0b0100000
20 | VIDEOS = 0b0010000
21 | HIGHLIGHTS = 0b0001000
22 | STORIES = 0b0000100
23 | ARCHIVED = 0b0000010
24 | AUDIO = 0b0000001
25 |
26 | def dynamic_rules():
27 | url = "https://raw.githubusercontent.com/hashypooh/dynamic_stuff/main/sign.json"
28 | r = requests.get(url)
29 | dynamic_json = json.loads(r.text)
30 | return dynamic_json
31 |
32 | dynamic_r = dynamic_rules()
33 |
34 |
35 | class Worker(Thread):
36 | def __init__(self, tasks, stop_event: threading.Event) -> None:
37 | Thread.__init__(self)
38 | self.tasks = tasks
39 | self.daemon = True
40 | self.stop_event = stop_event
41 | self.start()
42 |
43 | def run(self) -> None:
44 | while not self.stop_event.isSet():
45 | func, args, kargs = self.tasks.get()
46 | try: func(*args, **kargs)
47 | except Exception as e:
48 | print (e)
49 | self.tasks.task_done()
50 |
51 | class ThreadPool:
52 | def __init__(self, num_threads: int, stop_event: threading.Event) -> None:
53 | self.tasks = Queue(num_threads)
54 | for _ in range(num_threads):
55 | Worker(self.tasks, stop_event)
56 |
57 | def add_task(self, func, *args: Tuple, **kargs: Dict) -> None:
58 | self.tasks.put((func, args, kargs))
59 |
60 | def wait_completion(self) -> None:
61 | self.tasks.join()
62 |
63 | class Date:
64 | def __init__(self, date_time: str) -> None:
65 | self.date_time = datetime
66 | try:
67 | self.dt = datetime.datetime.fromisoformat(date_time)
68 | except ValueError:
69 | self.alt_dt = date_time
70 |
71 | def date(self) -> str:
72 | if hasattr(self, 'alt_dt'):
73 | return self.alt_dt
74 | return self.dt.strftime("%a, %d %B %Y")
75 |
76 |
77 | class Config:
78 | def __init__(self, filename: str) -> None:
79 | self.data = {}
80 | self.filename = filename
81 | try:
82 | file = open(filename, 'r')
83 | self.data = json.load(file)
84 | file.close()
85 | except FileNotFoundError:
86 | self.write_to_disk()
87 |
88 | def hash(self) -> str:
89 | if "hash" in self.data:
90 | return self.data["hash"]
91 | else: return ""
92 |
93 |
94 | def user_agent(self) -> str:
95 | if "user-agent" in self.data:
96 | return self.data["user-agent"]
97 | else: return ""
98 |
99 | def cookie(self) -> str:
100 | if "cookie" in self.data:
101 | return self.data["cookie"]
102 | else: return ""
103 |
104 | def app_token(self) -> str:
105 | if "app-token" in self.data:
106 | return self.data["app-token"]
107 | else:
108 | self.data["app-token"] = dynamic_r["app_token"]
109 | self.write_to_disk()
110 | return self.data["app-token"]
111 | return ""
112 |
113 | def x_bc(self) -> str:
114 | if "x-bc" in self.data:
115 | return self.data["x-bc"]
116 | else: return ""
117 |
118 | def add_node(self, title: str, text: str) -> None:
119 | if len(text) > 0:
120 | self.data[title] = text
121 | self.write_to_disk()
122 |
123 | def write_to_disk(self) -> None:
124 | with open(self.filename, 'w') as file:
125 | json.dump(self.data, file)
126 |
127 |
128 | def __len__(self) -> int:
129 | return len(self.data)
130 |
131 | @classmethod
132 | def create_dir(cls, dirname) -> str:
133 | if cls is None or dirname is None:
134 | return
135 | try:
136 | path = "Files/{0}".format(dirname)
137 | if not os.path.isdir(path):
138 | os.makedirs(path)
139 | except FileExistsError:
140 | pass
141 | finally:
142 | return path
143 |
144 |
145 | class MediaItem:
146 | def __init__(self, data: dict) -> None:
147 | self.data = data
148 |
149 | def download(self, path: str, total: int) -> None:
150 | tmp_path = Config.create_dir(path)
151 | if tmp_path is None:
152 | return
153 | if os.path.isfile(tmp_path + self.filename()) == False:
154 | with open(tmp_path + self.filename(), "wb") as file:
155 | response = requests.get(self.url(), stream = True)
156 | tmp = response.headers.get('content-length')
157 | if tmp is None:
158 | file.write(response.content)
159 | else:
160 | total_length = int(tmp)
161 | for data in response.iter_content(chunk_size = 4096):
162 | file.write(data)
163 |
164 |
165 | def __len__(self) -> int:
166 | return 1
167 |
168 | def media_count(self) -> int:
169 | return self.__len__()
170 |
171 | def id(self) -> int:
172 | return self.data["id"]
173 |
174 | def item_type(self) -> str:
175 | return self.data["type"]
176 |
177 | def filename(self) -> str:
178 | url = self.url().split('/')[-1].split('?')[0]
179 | return url
180 |
181 | def username(self) -> str:
182 | return self.data["username"]
183 |
184 | def user_id(self) -> int:
185 | return self.data["user_id"]
186 |
187 | def url(self) -> str:
188 | src = ""
189 | if "info" in self.data:
190 | info = self.data["info"]
191 | if info["source"] is not None:
192 | source = info["source"]
193 | if source["source"] is not None:
194 | src = source["source"]
195 | if 'files' in self.data:
196 | files = self.data["files"]
197 | if 'source' in files:
198 | source = files["source"]
199 | if 'url' in source:
200 | src = source["url"]
201 | return src
202 |
203 |
204 | def width(self) -> int:
205 | width = 0
206 | info = self.data["info"]
207 | if info["source"] is not None:
208 | source = info["source"]
209 | if source["source"] is not None:
210 | width = source["width"]
211 | return width
212 |
213 | def height(self) -> int:
214 | height = 0
215 | info = self.data["info"]
216 | if info["source"] is not None:
217 | source = info["source"]
218 | if source["source"] is not None:
219 | height = source["height"]
220 | return height
221 |
222 | def file_extension(self) -> str:
223 | file = self.url().split('.')[-1]
224 | file = file.split('?')[0]
225 | return file
226 |
227 | @classmethod
228 | def file_size(self, size: int) -> str:
229 | unit = ["KB", "MB", "GB", "TB"]
230 | count = -1
231 | if size < 1024:
232 | return str(size) + "B"
233 | else:
234 | while size >= 1024:
235 | size /= 1024
236 | count += 1
237 | return str('%.2f ' % size) + unit[count]
238 |
239 |
240 | @classmethod
241 | def media_items(cls, data: dict) -> Dict:
242 | media = {}
243 | def make(_data: dict):
244 | return cls(_data)
245 |
246 | media[data["id"]] = make(data)
247 | return media
248 |
249 | class Post:
250 | def __init__(self, data: dict) -> None:
251 | self.media = {}
252 | self.data = data
253 | self.parse_media(data)
254 |
255 | def __len__(self) -> int:
256 | return len(self.media)
257 |
258 | def username(self) -> str:
259 | if "author" in self.data:
260 | if "username" in self.data["author"]:
261 | return self.data["author"]["username"]
262 | return ""
263 |
264 | def user_id(self) -> str:
265 | if "author" in self.data:
266 | if "id" in self.data["author"]:
267 | return self.data["author"]["id"]
268 | return ""
269 |
270 | def download(self, display_data: QtCore.pyqtBoundSignal, lock: threading.Lock,
271 | conn: sqlite3.Connection, total: List[int]) -> None:
272 | data = {}
273 | for media_id, media in self.media.items():
274 | path = "{0}/{1}/{2}/".format(self.username(), type(self).__name__, media.item_type())
275 | if conn.does_exist(self.user_id(), self.id(), media.filename()) is not True:
276 | media.download(path, total)
277 | conn.insert_database(self, media)
278 | with lock:
279 | total[0] = total[0] - 1
280 |
281 | data["username"] = self.username()
282 | data["path"] = path
283 | data["filename"] = media.filename()
284 | data["total"] = total[0]
285 | display_data.emit(data)
286 |
287 |
288 | def can_view(self) -> bool:
289 | return self.data["canViewMedia"]
290 |
291 | def get_media(self) -> Dict:
292 | return self.media
293 |
294 | def parse_media(self, data: dict) -> None:
295 | if "media" in data:
296 | media = data["media"]
297 | for item in media:
298 | info = item["info"]
299 | source = info["source"]
300 | if source["source"] is None:
301 | continue
302 | else:
303 | media_items = MediaItem.media_items(item)
304 | self.media |= media_items
305 |
306 |
307 | def id(self) -> int:
308 | return self.data["id"]
309 |
310 | def posted_at(self) -> str:
311 | return Date(self.data["postedAt"]).date()
312 |
313 | def media_count(self) -> int:
314 | return len(self.media)
315 |
316 | def caption(self) -> str:
317 | return self.data["rawText"]
318 |
319 | @classmethod
320 | def post_items(cls, data: dict) -> Dict:
321 | post = {}
322 | def make(_data):
323 | return cls(_data)
324 |
325 | post[data["id"]] = make(data)
326 | return post
327 |
328 | class Archived(Post):
329 | def __init__(self, data: dict) -> None:
330 | super().__init__(data)
331 |
332 | class Story(Post):
333 | def __init__(self, data: dict) -> None:
334 | super().__init__(data)
335 |
336 | def parse_media(self, data: dict) -> None:
337 | if 'media' in data:
338 | medium = data["media"]
339 | for media in medium:
340 | media_items = MediaItem.media_items(media)
341 | self.media |= media_items
342 |
343 | def username(self):
344 | if 'username' in self.data:
345 | return self.data["username"]
346 |
347 | def caption(self) -> str:
348 | return type(self).__name__
349 |
350 | def posted_at(self) -> str:
351 | return Date(self.data["createdAt"]).date()
352 |
353 | def can_view(self) -> bool:
354 | if 'canView' in self.data:
355 | return self.data['canView']
356 | return True
357 |
358 |
359 |
360 | class Highlight(Post):
361 | def __init__(self, data: dict) -> None:
362 | super().__init__(data)
363 |
364 | def parse_media(self, data: dict) -> None:
365 | if "stories" in data:
366 | stories = data["stories"]
367 | for story in stories:
368 | if "media" in story:
369 | medium = story["media"]
370 | for media in medium:
371 | media_items = MediaItem.media_items(media)
372 | self.media |= media_items
373 |
374 | def username(self) -> str:
375 | if "username" in self.data:
376 | return self.data["username"]
377 | return ""
378 |
379 | def can_view(self) -> bool:
380 | return True
381 |
382 | def caption(self) -> str:
383 | return self.data["title"]
384 |
385 | def media_count(self) -> int:
386 | return self.data["storiesCount"]
387 |
388 | def posted_at(self) -> str:
389 | return Date(self.data["createdAt"]).date()
390 |
391 |
392 | class MessageItem(MediaItem):
393 | def __init__(self, data: dict) -> None:
394 | super().__init__(data)
395 | self.info = data["info"]
396 | self.source = self.info["source"]
397 |
398 | def download(self, display_data: QtCore.pyqtBoundSignal, lock: threading.Lock,
399 | conn: sqlite3.Connection, total: List[int]) -> None:
400 | data = {}
401 | path = "{0}/{1}/{2}/".format(self.username(), type(self).__name__, self.item_type()) #maybe none
402 | if conn.does_exist(self.user_id(), self.id(), self.filename()) is not True:
403 | super().download(path, total)
404 | conn.insert_database(self, self)
405 | with lock:
406 | total[0] = total[0] - 1
407 |
408 | data["username"] = self.username()
409 | data["path"] = path
410 | data["filename"] = self.filename()
411 | data["total"] = total[0]
412 | display_data.emit(data)
413 |
414 |
415 | def posted_at(self) -> str:
416 | return Date(self.data["createdAt"]).date()
417 |
418 | def url(self) -> str:
419 | return self.data["src"]
420 |
421 | def width(self) -> int:
422 | return self.source["width"]
423 |
424 | def height(self) -> int:
425 | return self.source["height"]
426 |
427 | def thumbnail(self) -> str:
428 | return self.data["thumb"]
429 |
430 | def can_view(self) -> bool:
431 | return self.data["canView"]
432 |
433 | def duration(self) -> int:
434 | return self.data["duration"]
435 |
436 | def caption(self) -> str:
437 | return self.data["caption"]
438 |
439 | def get_media(self) -> "MessageItem":
440 | return self
441 |
442 |
443 | class Profile:
444 | def __init__(self, data) -> None:
445 | self.data = data
446 | self.info = {}
447 | self.flags = 0
448 | self.gathered_flags = 0
449 | self.posts = {}
450 | self.error = False
451 | self.lock = threading.Lock()
452 |
453 | def __len__(self) -> int:
454 | return len(self.fetch_posts())
455 |
456 | def set_error(self) -> bool:
457 | self.error = True
458 | return self.error
459 |
460 | def error_set(self) -> bool:
461 | return self.error
462 |
463 |
464 | def download(self, stop_event, display_data: QtCore.pyqtBoundSignal,
465 | post_ids: list[int],
466 | total: List[int]) -> None:
467 | data = {}
468 | pool = ThreadPool(2, stop_event)
469 | data["info"] = "Downloading..."
470 | display_data.emit(data)
471 | for post_id in post_ids:
472 | conn = Database("onlyfans.sqlite3.db")
473 | _post = self.fetch_posts()[int(post_id)]
474 | pool.add_task(_post.download, display_data, self.lock, conn, total)
475 | pool.wait_completion()
476 | if total[0] == 0:
477 | data["info"] = "Completed..."
478 | display_data.emit(data)
479 |
480 | def fetch_posts(self) -> Dict:
481 | entire_list = {key : self.posts[key] for key in self.posts if len(self.posts[key]) > 0}
482 | result = entire_list.copy()
483 | for key, value in entire_list.items():
484 | _type = type(value).__name__
485 | flags = self.get_flag()
486 | if not (flags & MESSAGES) and _type == "MessageItem" or \
487 | not (flags & PICTURES) and _type == "Post" or \
488 | not (flags & VIDEOS) and _type == "Post" or \
489 | not (flags & HIGHLIGHTS) and _type == "Highlight" or \
490 | not (flags & STORIES) and _type == "Story" or \
491 | not (flags & ARCHIVED) and _type == "Archived" or \
492 | not (flags & AUDIO) and _type == "Audio":
493 | del result[key]
494 |
495 | return result
496 |
497 | def post_count(self) -> int:
498 | return len(self.posts)
499 |
500 | def media_count(self) -> int:
501 | total = 0
502 | posts = self.fetch_posts()
503 | for key in posts:
504 | post = posts[key]
505 | if post.can_view():
506 | total += post.media_count()
507 | return total
508 |
509 | def parse_posts(self, data: dict) -> None:
510 | posts = {}
511 | if "Highlight" in data:
512 | posts |= Highlight.post_items(data)
513 | elif "list" in data and "Message" in data:
514 | node_list = data["list"]
515 | for node in node_list:
516 | media = node["media"]
517 | for m in media:
518 | if 'canView' in m:
519 | canView = m["canView"]
520 | if canView is False:
521 | continue
522 | text = node["text"]
523 | created_at = node["createdAt"]
524 | m["createdAt"] = Date(created_at).date()
525 | m["caption"] = text
526 | m["username"] = node["fromUser"]["username"]
527 | m["user_id"] = node["fromUser"]["id"]
528 | posts |= MessageItem.media_items(m)
529 | elif "Story" in data:
530 | posts |= Story.post_items(data)
531 | elif "Archived" in data:
532 | posts |= Archived.post_items(data)
533 | elif "Post" in data:
534 | posts |= Post.post_items(data)
535 |
536 | self.posts |= posts
537 |
538 | def get_flag(self) -> int:
539 | return self.flags
540 |
541 | def put_flag(self, flag: int) -> None:
542 | self.flags = flag
543 |
544 | def set_info(self, info: dict) -> None:
545 | self.info = info
546 |
547 | def is_active(self) -> bool:
548 | return self.data["subscribedBy"] == True
549 |
550 | def username(self) -> str:
551 | return self.data["username"]
552 |
553 | def avatar(self) -> str:
554 | return self.data["avatar"]
555 |
556 | def sm_avatar(self, size: int) -> str:
557 | if self.data["avatarThumbs"] is not None:
558 | if size == 50:
559 | return self.data["avatarThumbs"]["c50"]
560 | else:
561 | return self.data["avatarThumbs"]["c144"]
562 | else:
563 | return ""
564 |
565 | def id(self) -> int:
566 | return self.data["id"]
567 |
568 | def photo_count(self) -> int:
569 | if len(self.info) > 0: return self.info["photosCount"]
570 |
571 | def videos_count(self) -> int:
572 | if len(self.info) > 0: return self.info["videosCount"]
573 |
574 | def audio_count(self) -> int:
575 | if len(self.info) > 0: return self.info["audiosCount"]
576 |
577 | def archive_count(self) -> int:
578 | if len(self.info) > 0: return self.info["archivedPostsCount"]
579 |
580 | @classmethod
581 | def profile_items(cls, data: dict) -> Dict:
582 | profiles = {}
583 | def make(_data):
584 | return cls(_data)
585 |
586 | for node in data:
587 | profiles[node["username"]] = make(node)
588 | return profiles
589 |
590 |
591 |
592 | class Onlyfans(QtCore.QObject):
593 | data_display = QtCore.pyqtSignal(object)
594 | stop_event = threading.Event()
595 | def __init__(self) -> None:
596 | QtCore.QObject.__init__(self)
597 | self.profiles = {}
598 | self.session = requests.Session()
599 | self.set_session_headers()
600 | self.logged_in = self.user_logged_in()
601 |
602 | self.base_url = "https://onlyfans.com/"
603 | self.login = "https://onlyfans.com/api2/v2/users/login"
604 | self.customer = "https://onlyfans.com/api2/v2/users/me"
605 | self.users = "https://onlyfans.com/api2/v2/users/{0}"
606 | self.message_api = "https://onlyfans.com/api2/v2/chats/{0}/messages?limit={1}&offset={2}&order=desc"
607 | self.stories_api = "https://onlyfans.com/api2/v2/users/{0}/stories?limit=100&offset={1}&order=desc"
608 | self.list_highlights = "https://onlyfans.com/api2/v2/users/{0}/stories/highlights?limit=100&offset={1}&order=desc"
609 | self.highlight = "https://onlyfans.com/api2/v2/stories/highlights/{0}"
610 | self.post_api = "https://onlyfans.com/api2/v2/users/{0}/posts?limit={1}&offset={2}&order=publish_date_desc&skip_users_dups=0"
611 | self.archived_posts = "https://onlyfans.com/api2/v2/users/{0}/posts/archived?limit=100&offset={1}&order=publish_date_desc"
612 | self.subscribe = "https://onlyfans.com/api2/v2/users/{identifier}/subscribe"
613 | self.audio = "https://onlyfans.com/api2/v2/users/{0}/posts/audios?limit=10&offset={1}&order=publish_date_desc&skip_users=all&counters=0&format=infinite"
614 | self.subscription_count = "https://onlyfans.com/api2/v2/subscriptions/count/all"
615 | self.subscriptions = "https://onlyfans.com/api2/v2/subscriptions/subscribes?offset={0}&type=all&sort=desc&field=expire_date&limit=10"
616 |
617 |
618 | def signal_stop_event(self) -> None:
619 | self.stop_event.set()
620 |
621 |
622 | def user_logged_in(self) -> bool:
623 | data = {}
624 | settings = {}
625 | additional = {}
626 | data["info"] = "Attempting to log in"
627 | self.data_display.emit(data)
628 | self.set_session_headers()
629 | self.init_url = "https://onlyfans.com/api2/v2/init"
630 | self.create_sign(self.session, self.init_url)
631 | r = self.session.get(self.init_url)
632 | if r.status_code != 200:
633 | return False
634 | json_response = json.loads(r.text)
635 | if "settings" in json_response:
636 | settings = json_response["settings"]
637 | if "upload" in json_response and "geoUploadArgs" in json_response["upload"] and \
638 | "additional" in json_response["upload"]["geoUploadArgs"]:
639 | additional = json_response["upload"]["geoUploadArgs"]["additional"]
640 |
641 | if "isAuth" in json_response:
642 | return json_response["isAuth"]
643 | elif "userLoginPrefix" in settings and "user" in additional:
644 | if len(settings["userLoginPrefix"]) > 0 and len(additional["user"]) > 0:
645 | return True
646 | return False
647 |
648 |
649 | def set_session_headers(self) -> None:
650 | self.load_config()
651 | self.session.headers = {
652 | 'User-Agent': "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36",
653 | 'Referer': 'https://onlyfans.com/',
654 | 'accept': 'application/json, text/plain, */*',
655 | 'app-token': dynamic_r["app_token"],
656 | 'accept-encoding': 'gzip, deflate, br'
657 | }
658 | if hasattr(self, 'user_agent') and hasattr(self, 'cookie') and hasattr(self, 'x_bc'):
659 | if len(self.user_agent) > 0 and len(self.cookie) > 0 and len(self.app_token) > 0 and \
660 | len(self.x_bc) > 0:
661 | self.session.headers = {
662 | 'User-Agent': self.user_agent,
663 | 'Referer': 'https://onlyfans.com/',
664 | 'accept': 'application/json, text/plain, */*',
665 | 'Cookie' : self.cookie,
666 | 'app-token': self.app_token,
667 | 'x-bc': self.x_bc,
668 | 'accept-encoding': 'gzip, deflate, br'
669 | }
670 |
671 | def download_profiles(self, user_post_ids: dict, total: List[int]) -> None:
672 | pool = ThreadPool(2, self.stop_event)
673 | profiles = self.return_all_subs()
674 | for username_key in user_post_ids:
675 | profile = profiles[username_key]
676 | data = {}
677 | data["info"] = "Starting download..."
678 | self.data_display.emit(data)
679 | pool.add_task(profile.download, self.stop_event, self.data_display,
680 | user_post_ids[username_key], total)
681 |
682 |
683 |
684 |
685 | def load_config(self) -> None:
686 | config = Config("config.json")
687 | if len(config) > 0:
688 | self.user_agent = config.user_agent()
689 | self.cookie = config.cookie()
690 | self.app_token = config.app_token()
691 | self.x_bc = config.x_bc()
692 | self.config = config
693 |
694 | def is_config_empty(self) -> int:
695 | return len(self.config) == 0
696 |
697 | @classmethod
698 | def create_sign(self, session: requests.sessions.Session, link: str) -> None:
699 | _time = str(int(round(time.time() * 1000)))
700 | index = link.find('//') + 2
701 | index = link.find('/', index)
702 | path = link[index:]
703 | msg = "\n".join([dynamic_r["static_param"], _time, path, str(0)])
704 | message = msg.encode("utf-8")
705 | _hash = hashlib.sha1(message)
706 | sha1 = _hash.hexdigest()
707 | sha1_enc = sha1.encode("ascii")
708 | checksum = (
709 | sum([sha1_enc[number] for number in dynamic_r["checksum_indexes"]])
710 | + sum(number for number in dynamic_r["checksum_constants"])
711 | )
712 | session.headers["sign"] = dynamic_r["sign_format"].format(sha1, format(abs(checksum), 'x'))
713 | session.headers["time"] = _time
714 |
715 |
716 | def get_subscriptions(self) -> int:
717 | if len(self.profiles) > 0:
718 | return len(self.profiles)
719 | global_limit = 10
720 | global_offset = 0
721 |
722 | if self.user_logged_in() is not True:
723 | print ("Login failed")
724 | return
725 |
726 | users = []
727 |
728 | while True:
729 | temp_sub = self.subscriptions.format(global_offset)
730 | self.create_sign(self.session, temp_sub)
731 | r = self.session.get(temp_sub)
732 | if len(r.text) > 0:
733 | r = json.loads(r.text)
734 | users.append(r)
735 | global_offset += 10
736 | if len(r) == 0:
737 | break
738 |
739 | for user in users:
740 | profile = Profile.profile_items(user)
741 | self.profiles |= profile
742 |
743 | count = len(self.profiles)
744 | return count
745 |
746 |
747 | def return_active_subs(self) -> Dict:
748 | active_subscriptions = {}
749 | for key in self.profiles:
750 | profile = self.profiles[key]
751 | if profile.is_active():
752 | active_subscriptions[key] = profile
753 | return active_subscriptions
754 |
755 |
756 | def return_expired_subs(self) -> Dict:
757 | expired_subscriptions = {}
758 | for key in self.profiles:
759 | profile = self.profiles[key]
760 | if not profile.is_active():
761 | expired_subscriptions[key] = profile
762 | return expired_subscriptions
763 |
764 | def return_all_subs(self) -> Dict:
765 | return self.profiles
766 |
767 | def get_user_info(self, profile) -> bool:
768 | if len(profile.info) > 0:
769 | return True
770 | link = self.users.format(profile.username())
771 |
772 | self.create_sign(self.session, link)
773 | r = self.session.get(link)
774 | json_data = json.loads(r.text)
775 |
776 | if json_data is None:
777 | return False
778 | if "error" in json_data:
779 | if profile.error_set() is False:
780 | profile.set_error()
781 | print (json_data)
782 | return False
783 |
784 | profile.set_info(json_data)
785 | return True
786 |
787 |
788 | def get_links(self, profile):
789 | total_post = profile.photo_count() + profile.videos_count()
790 | audio_count = profile.audio_count()
791 | limit = 100
792 | flag = profile.get_flag()
793 |
794 | if ((flag & PICTURES) or (flag & VIDEOS)) and \
795 | not (profile.gathered_flags & (PICTURES | VIDEOS)):
796 | offset_range = math.ceil(total_post / 100)
797 | offsets = list(range(offset_range))
798 | for offset in offsets:
799 | new_offset = offset * 100
800 | link = self.post_api.format(profile.id(), limit, new_offset)
801 | self.create_sign(self.session, link)
802 | r = self.session.get(link)
803 | if(len(r.text)) > 0:
804 | json_data = json.loads(r.text)
805 | for node in json_data:
806 | node["Post"] = True
807 | profile.parse_posts(node)
808 | profile.gathered_flags |= (PICTURES | VIDEOS)
809 |
810 | if (flag & AUDIO) and not (profile.gathered_flags & AUDIO):
811 | offset_range = math.ceil(audio_count / 10)
812 | offsets = list(range(offset_range))
813 | for offset in offsets:
814 | new_offset = offset * 10
815 | link = self.audio.format(profile.id(), new_offset)
816 | self.create_sign(self.session, link)
817 | r = self.session.get(link)
818 | json_data = json.loads(r.text)
819 | if "list" in json_data:
820 | profile.parse_posts(json_data["list"])
821 | profile.gathered_flags |= AUDIO
822 |
823 | if (flag & STORIES) and not (profile.gathered_flags & STORIES):
824 | link = self.stories_api.format(profile.id(), 0)
825 | self.create_sign(self.session, link)
826 | r = self.session.get(link)
827 | json_data = json.loads(r.text)
828 | for node in json_data:
829 | node["Story"] = True
830 | node["username"] = profile.username()
831 | profile.parse_posts(node)
832 | profile.gathered_flags |= STORIES
833 |
834 | if (flag & HIGHLIGHTS) and not (profile.gathered_flags & HIGHLIGHTS):
835 | link = self.list_highlights.format(profile.id(), 0)
836 | self.create_sign(self.session, link)
837 | r = self.session.get(link)
838 | json_data = json.loads(r.text)
839 | if 'list' in json_data:
840 | for node in json_data["list"]:
841 | highlight_id = node["id"]
842 | link = self.highlight.format(highlight_id)
843 | self.create_sign(self.session, link)
844 | r = self.session.get(link)
845 | _json_data = json.loads(r.text)
846 | _json_data["Highlight"] = True
847 | _json_data["username"] = profile.username()
848 | profile.parse_posts(_json_data)
849 | profile.gathered_flags |= HIGHLIGHTS
850 |
851 | if (flag & MESSAGES) and not (profile.gathered_flags & MESSAGES):
852 | offset = 0
853 | link = self.message_api.format(profile.id(), limit, offset)
854 | self.create_sign(self.session, link)
855 | r = self.session.get(link)
856 | json_data = json.loads(r.text)
857 | json_data["Message"] = True
858 | profile.parse_posts(json_data)
859 | if "hasMore" in json_data:
860 | hasMore = json_data["hasMore"]
861 | while hasMore:
862 | offset += limit
863 | link = self.message_api.format(profile.id(), limit, offset)
864 | self.create_sign(self.session, link)
865 | r = self.session.get(link)
866 | _json_data = json.loads(r.text)
867 | _json_data["Message"] = True
868 | if "list" in _json_data:
869 | if len(_json_data["list"]) > 0:
870 | profile.parse_posts(_json_data)
871 | hasMore = _json_data["hasMore"]
872 | profile.gathered_flags |= MESSAGES
873 |
874 | if (flag & ARCHIVED) and not (profile.gathered_flags & ARCHIVED):
875 | count = profile.archive_count()
876 | offset_range = math.ceil(count / 100)
877 | offsets = list(range(offset_range))
878 | for offset in offsets:
879 | new_offset = offset * 100
880 | link = self.archived_posts.format(profile.id(), new_offset)
881 | self.create_sign(self.session, link)
882 | r = self.session.get(link)
883 | json_data = json.loads(r.text)
884 | for node in json_data:
885 | node["Archived"] = True
886 | profile.parse_posts(node)
887 | profile.gathered_flags |= ARCHIVED
888 |
889 |
890 | class Database:
891 | def __init__(self, filename: str) -> None:
892 | self.filename = filename
893 | self.conn = self.get_database()
894 |
895 |
896 | def does_exist(self, user_id: str, post_id: str, filename: str) -> bool:
897 | try:
898 | c = self.conn.cursor()
899 | c.execute("SELECT * FROM entries where userid = ? AND id = ? AND filename = ?",
900 | (user_id, post_id, filename,))
901 | data = c.fetchall()
902 | if len(data) > 0:
903 | return True
904 | return False
905 | except:
906 | return False
907 |
908 | def insert_database(self, post: Post, file: MediaItem) -> None:
909 | id_user = post.user_id()
910 | username = post.username()
911 |
912 | url = file.url()
913 | post_id = post.id()
914 | file_name = file.filename()
915 | try:
916 | c = self.conn.cursor()
917 | c.execute('INSERT INTO entries VALUES(?,?,?,?,?)', (str(post_id), str(url), str(id_user),
918 | str(username),
919 | str(file_name)))
920 | self.conn.commit()
921 | except sqlite3.IntegrityError:
922 | pass
923 |
924 | def get_database(self) -> sqlite3.Connection:
925 | conn = None
926 | try:
927 | conn = sqlite3.connect(self.filename, check_same_thread=False)
928 | cursor = conn.cursor()
929 | cursor.execute("CREATE TABLE IF NOT EXISTS `entries`"
930 | "(`id` TEXT, `url` TEXT, `userid` TEXT, `username` TEXT, `filename` TEXT);")
931 | except Error as e:
932 | print (e)
933 | finally:
934 | return conn
935 |
936 |
937 |
--------------------------------------------------------------------------------
/options.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Hashirama/OFDL/36bc39c9b2aed1f4278b4d99182d26532ed068c2/options.png
--------------------------------------------------------------------------------
/request.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Hashirama/OFDL/36bc39c9b2aed1f4278b4d99182d26532ed068c2/request.png
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | PyQt5==5.15.7
2 | requests==2.31.0
3 |
--------------------------------------------------------------------------------
/settings.json:
--------------------------------------------------------------------------------
1 | {"show_avatar": false}
--------------------------------------------------------------------------------
/user_agent.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Hashirama/OFDL/36bc39c9b2aed1f4278b4d99182d26532ed068c2/user_agent.png
--------------------------------------------------------------------------------
/x_bc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Hashirama/OFDL/36bc39c9b2aed1f4278b4d99182d26532ed068c2/x_bc.png
--------------------------------------------------------------------------------