├── 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 = "

Active subscriptions

" 214 | 215 | self.model_label = QLabel(self.listing_type, parent=self.general_tab) 216 | self.model_label.move(10, 0) 217 | 218 | self.current_user = QLabel("", parent=self.general_tab) 219 | self.current_user.move(200, 50) 220 | self.current_user.resize(150, 20) 221 | 222 | self.tree = QTreeWidget(parent = self.general_tab) 223 | self.tree.setHeaderLabels(["Models"]) 224 | self.tree.resize(150, 200) 225 | self.tree.move(10, 20) 226 | 227 | self.cb = QComboBox(parent = self.general_tab) 228 | self.cb.move(5, 220) 229 | self.cb.addItems(["Active subscriptions", "Expired subscriptions", "All"]) 230 | self.cb.currentIndexChanged.connect(self.combo_change) 231 | 232 | self.tree.itemClicked.connect(self.onItemClicked) 233 | 234 | self.tabs.addTab(self.general_tab, "General") 235 | self.tabs.addTab(self.links_tab, "Links") 236 | self.tabs.addTab(self.download_tab, "Downloads") 237 | 238 | self.icon_label = QLabel(parent = self.general_tab) 239 | self.icon_label.move(200, 10) 240 | self.icon_label.resize(300, 300) 241 | 242 | self.all_checkbox = QCheckBox("All", parent = self.general_tab) 243 | self.all_checkbox.move(200, 250) 244 | self.all_checkbox.stateChanged.connect(self.check_change) 245 | 246 | self.message_checkbox = QCheckBox("Messages", parent = self.general_tab) 247 | self.message_checkbox.move(300, 250) 248 | self.message_checkbox.stateChanged.connect(self.check_change) 249 | 250 | self.audio_checkbox = QCheckBox("Audio", parent = self.general_tab) 251 | self.audio_checkbox.move(400, 250) 252 | self.audio_checkbox.stateChanged.connect(self.check_change) 253 | 254 | self.highlight_checkbox = QCheckBox("Highlights", parent = self.general_tab) 255 | self.highlight_checkbox.move(200, 300) 256 | self.highlight_checkbox.stateChanged.connect(self.check_change) 257 | 258 | self.story_checkbox = QCheckBox("Stories", parent = self.general_tab) 259 | self.story_checkbox.move(300, 300) 260 | self.story_checkbox.stateChanged.connect(self.check_change) 261 | 262 | self.post_checkbox = QCheckBox("Posts", parent = self.general_tab) 263 | self.post_checkbox.move(400, 300) 264 | self.post_checkbox.stateChanged.connect(self.check_change) 265 | 266 | self.archived_checkbox = QCheckBox("Archived", parent = self.general_tab) 267 | self.archived_checkbox.move(500, 300) 268 | self.archived_checkbox.stateChanged.connect(self.check_change) 269 | 270 | self.checkboxes = [self.all_checkbox, self.message_checkbox, self.audio_checkbox, 271 | self.highlight_checkbox, self.story_checkbox, 272 | self.post_checkbox, self.archived_checkbox] 273 | 274 | self.grab_links = QPushButton("Grab Links", parent = self.general_tab) 275 | self.grab_links.move(500, 400) 276 | self.grab_links.clicked.connect(self.get_links) 277 | self.grab_links.setEnabled(False) 278 | 279 | self.download_links = QPushButton("Download Files", parent = self.links_tab) 280 | self.download_links.move(100, 400) 281 | self.download_links.clicked.connect(self.download_files) 282 | self.download_links.setEnabled(False) 283 | 284 | self.options = QPushButton("Options", parent = self.general_tab) 285 | self.options.move(700, 10) 286 | self.options.clicked.connect(self.show_options) 287 | 288 | 289 | self.tree_links = QTreeWidget(parent = self.links_tab) 290 | self.tree_links.setHeaderLabels(["Model", "Type", "Caption / Text", 291 | "Date of post", "Post ID"]) 292 | self.tree_links.resize(720, 300) 293 | self.tree_links.move(30, 20) 294 | self.tree_links.columnWidth(300) 295 | 296 | self.tree_links.setColumnWidth(0, 150) 297 | self.tree_links.setColumnWidth(2, 300) 298 | self.tree_links.hideColumn(4) 299 | 300 | self.download_tree.setColumnWidth(1, 300) 301 | 302 | layout.addWidget(self.tabs, 0, 0) 303 | 304 | self.display_checkboxes(False) 305 | self.show() 306 | 307 | self.Onlyfans.load_config() 308 | self.data_display.connect(self.update_main) 309 | 310 | if self.Onlyfans.user_logged_in() is True: 311 | thread = Thread(target = self.fetch_and_display_subs, 312 | args=(self.data_display,)) 313 | thread.start() 314 | else: 315 | self.information_label_general.setText("Not logged in...") 316 | 317 | def closeEvent(self, event: QCloseEvent) -> None: 318 | self.Onlyfans.signal_stop_event() 319 | 320 | 321 | def show_options(self) -> None: 322 | self.options_dialog.show() 323 | 324 | 325 | def fetch_and_display_subs(self, data_display: QtCore.pyqtBoundSignal) -> int: 326 | data = {} 327 | data["info"] = "Fetching subscriptions ..." 328 | data_display.emit(data) 329 | self.display_checkboxes(False) 330 | count = self.Onlyfans.get_subscriptions() 331 | self.subscription_list = self.Onlyfans.return_active_subs() 332 | self.display_subscriptions(self.subscription_list, data_display) 333 | data["info"] = "Done ..." 334 | data_display.emit(data) 335 | if count > 0: 336 | self.grab_links.setEnabled(True) 337 | self.all_subscriptions = self.Onlyfans.return_all_subs() 338 | self.expired_subscriptions = self.Onlyfans.return_expired_subs() 339 | return count 340 | 341 | 342 | @QtCore.pyqtSlot(QTreeWidgetItem, int) 343 | def onItemClicked(self, it: QTreeWidgetItem, col: int) -> None: 344 | self.current_username = it.text(col) 345 | 346 | profile = self.all_subscriptions[self.current_username] 347 | if self.current_username == profile.username(): 348 | if profile.is_active(): 349 | self.display_checkboxes(True) 350 | else: 351 | self.display_checkboxes(False) 352 | self.message_checkbox.setEnabled(True) 353 | self.grab_links.setEnabled(True) 354 | 355 | if self.Onlyfans.get_user_info(profile) is False: 356 | self.display_checkboxes(False) 357 | self.switch_selections(profile.get_flag()) 358 | 359 | self.current_user.setText("

" + self.current_username + "

") 360 | self.information_photo_count.setText("

Photos: {0}".format(profile.photo_count()) + "

") 361 | self.information_video_count.setText("

Videos: {0}".format(profile.videos_count()) + "

") 362 | self.information_audio_count.setText("

Audio: {0}".format(profile.audio_count()) + "

") 363 | self.information_archive_count.setText("

Archived: {0}".format(profile.archive_count()) + "

") 364 | 365 | avatar_url = profile.sm_avatar(50) 366 | pixmap = QPixmap() 367 | if avatar_url is not None and self.options_dialog.show_avatar() is not False and avatar_url != '': 368 | r = requests.get(avatar_url) 369 | if r.status_code == 200: 370 | pixmap.loadFromData(r.content) 371 | pixmap = pixmap.scaled(96, 96, QtCore.Qt.KeepAspectRatio) 372 | self.icon_label.setPixmap(pixmap) 373 | else: 374 | self.icon_label.setPixmap(QPixmap()) 375 | 376 | def _get_links(self, data_display: QtCore.pyqtBoundSignal) -> None: 377 | data = {} 378 | profiles = self.Onlyfans.return_all_subs() 379 | 380 | for key in profiles: 381 | profile = profiles[key] 382 | if profile.get_flag() > 0: 383 | self.Onlyfans.get_links(profile) 384 | if profile.error_set() is False and profile.get_flag() > 0: 385 | data["info"] = "Collected -> {0}, still collecting...".format(profile.username()) 386 | data_display.emit(data) 387 | 388 | data["info"] = "Done ..." 389 | data_display.emit(data) 390 | 391 | self.display_collected_links(profiles, data_display) 392 | 393 | 394 | 395 | def get_links(self) -> None: 396 | if hasattr(self, 'current_username') is False: 397 | return 398 | self.information_label_general.setText("Fetching data ...") 399 | self.download_links.setEnabled(False) 400 | thread = Thread(target = self._get_links, args=(self.data_display,)) 401 | thread.start() 402 | self.grab_links.setEnabled(False) 403 | 404 | 405 | def download(self) -> None: 406 | username = '' 407 | usernames = {} 408 | total_posts = [0] 409 | root = self.tree_links.invisibleRootItem() 410 | user_count = root.childCount() 411 | profiles = self.Onlyfans.return_all_subs() 412 | self.tabs.setCurrentWidget(self.download_tab) 413 | if user_count < 1: 414 | return 415 | for i in range(user_count): 416 | post_ids = [] 417 | user = root.child(i) 418 | user_post_count = user.childCount() 419 | user_all_selected = (user.checkState(2) == QtCore.Qt.Unchecked) 420 | if user_all_selected: 421 | continue 422 | username = user.text(0) 423 | for x in range(user_post_count): 424 | user_child = user.child(x) 425 | state = (user_child.checkState(2) == QtCore.Qt.Checked) 426 | if state: 427 | item_id = user_child.text(4) 428 | post_ids.append(item_id) 429 | total_posts[0] += profiles[username].media_count() 430 | usernames[username] = post_ids 431 | 432 | profile = profiles[username] 433 | 434 | self.download_links.setEnabled(False) 435 | self.Onlyfans.data_display.connect(self.update) 436 | self.Onlyfans.download_profiles(usernames, total_posts) 437 | 438 | def update(self, data: Dict) -> None: 439 | if 'username' in data and 'path' in data and 'filename' in data: 440 | item = QTreeWidgetItem(self.download_tree) 441 | item.setText(0, data["username"]) 442 | item.setText(1, data["path"]) 443 | item.setText(2, data["filename"]) 444 | if 'info' in data: 445 | self.information_label_download.setText(data['info']) 446 | elif 'total' in data: 447 | self.information_label_download.setText("Posts left to download: {0}".format(data["total"])) 448 | 449 | def update_main(self, data: Dict) -> None: 450 | if 'info' in data: 451 | self.information_label_general.setText(data['info']) 452 | if 'display_subscriptions' in data: 453 | if 'username' in data: 454 | item = QTreeWidgetItem(self.tree) 455 | item.setText(0, data["username"]) 456 | if 'display_links' in data: 457 | profile = data["profile"] 458 | username = QTreeWidgetItem(self.tree_links) 459 | username.setText(0, profile.username()) 460 | username.setFlags(QtCore.Qt.ItemIsTristate | QtCore.Qt.ItemIsUserCheckable | QtCore.Qt.ItemIsEnabled) 461 | username.setCheckState(0, QtCore.Qt.Unchecked) 462 | 463 | fmt = "{0} has {1} media that can be downloaded".format(profile.username(), 464 | profile.media_count()) 465 | username.setToolTip(0, fmt) 466 | 467 | user_posts = data["posts"] 468 | for key in user_posts: 469 | item = QTreeWidgetItem(username) 470 | item.setText(1, type(user_posts[key]).__name__) 471 | item.setText(2, user_posts[key].caption()) 472 | item.setText(3, user_posts[key].posted_at()) 473 | item.setText(4, str(user_posts[key].id())) 474 | item.setFlags(QtCore.Qt.ItemIsTristate | QtCore.Qt.ItemIsUserCheckable | 475 | QtCore.Qt.ItemIsEnabled) 476 | item.setCheckState(2, QtCore.Qt.Checked) 477 | item.setTextAlignment(3, QtCore.Qt.AlignLeft) 478 | 479 | self.tabs.setCurrentWidget(self.links_tab) 480 | self.grab_links.setEnabled(True) 481 | root = self.tree_links.invisibleRootItem() 482 | count = root.childCount() 483 | if count > 0: 484 | self.download_links.setEnabled(True) 485 | 486 | 487 | def download_files(self) -> None: 488 | ret = QMessageBox.question(self, 'MessageBox', 489 | "Would you like to start downloading posts?", 490 | QMessageBox.No | QMessageBox.Yes) 491 | if ret == QMessageBox.No: 492 | return 493 | 494 | thread = Thread(target = self.download) 495 | thread.start() 496 | 497 | 498 | 499 | 500 | def combo_change(self, i: int) -> None: 501 | self.tree.clear() 502 | if i == 0: 503 | self.display_subscriptions(self.subscription_list, 504 | self.data_display) 505 | elif i == 1: 506 | self.display_subscriptions(self.expired_subscriptions, 507 | self.data_display) 508 | elif i == 2: 509 | self.display_subscriptions(self.all_subscriptions, 510 | self.data_display) 511 | 512 | def check_change(self, state: int) -> None: 513 | if isinstance(self.sender(), QCheckBox): 514 | check_name = self.sender().text() 515 | user = self.current_username 516 | if user in self.all_subscriptions: 517 | profile = self.all_subscriptions[user] 518 | flag = profile.get_flag() 519 | if state == QtCore.Qt.Checked: 520 | self.change_flags(user, check_name, profile, flag, True) 521 | if self.sender() == self.all_checkbox: 522 | for x in range(1, len(self.checkboxes)): 523 | self.checkboxes[x].setCheckState(QtCore.Qt.Checked) 524 | if profile.get_flag() == snafylno.ALL: 525 | self.all_checkbox.setCheckState(QtCore.Qt.Checked) 526 | elif state == QtCore.Qt.Unchecked: 527 | self.change_flags(user, check_name, profile, flag, False) 528 | if self.sender() == self.all_checkbox: 529 | for x in range(1, len(self.checkboxes)): 530 | self.checkboxes[x].setCheckState(QtCore.Qt.Unchecked) 531 | else: 532 | if self.all_checkbox.isChecked(): 533 | self.all_checkbox.setCheckState(QtCore.Qt.PartiallyChecked) 534 | if self.all_checkbox.isTristate() and profile.get_flag() == 0: 535 | self.all_checkbox.setCheckState(QtCore.Qt.Unchecked) 536 | elif state == QtCore.Qt.PartiallyChecked: 537 | if profile.get_flag() == 0: 538 | self.all_checkbox.nextCheckState() 539 | 540 | 541 | def change_flags(self, user: str, name: str, _profile: snafylno.Profile, flag: int, 542 | state: bool) -> None: 543 | if name == "All": 544 | flag = (snafylno.ALL | flag) if state is True else \ 545 | (~snafylno.ALL & flag) 546 | elif name == "Messages": 547 | flag = (snafylno.MESSAGES | flag) if state is True else \ 548 | (~snafylno.MESSAGES & flag) 549 | elif name == "Audio": 550 | flag = (snafylno.AUDIO | flag) if state is True else \ 551 | (~snafylno.AUDIO & flag) 552 | elif name == "Highlights": 553 | flag = (snafylno.HIGHLIGHTS | flag) if state is True else \ 554 | (~snafylno.HIGHLIGHTS & flag) 555 | elif name == "Stories": 556 | flag = (snafylno.STORIES | flag) if state is True else \ 557 | (~snafylno.STORIES & flag) 558 | elif name == "Posts": 559 | flag = (snafylno.PICTURES | snafylno.VIDEOS | flag) if state is True else \ 560 | ~(snafylno.PICTURES | snafylno.VIDEOS) & flag 561 | elif name == "Archived": 562 | flag = (snafylno.ARCHIVED | flag) if state is True else \ 563 | (~snafylno.ARCHIVED & flag) 564 | _profile.put_flag(flag) 565 | 566 | def switch_selections(self, flag) -> None: 567 | self.message_checkbox.setChecked(QtCore.Qt.Checked) if (flag & snafylno.MESSAGES) else \ 568 | self.message_checkbox.setChecked(QtCore.Qt.Unchecked) 569 | self.audio_checkbox.setChecked(QtCore.Qt.Checked) if (flag & snafylno.AUDIO) else \ 570 | self.audio_checkbox.setChecked(QtCore.Qt.Unchecked) 571 | self.highlight_checkbox.setChecked(QtCore.Qt.Checked) if (flag & snafylno.HIGHLIGHTS) else \ 572 | self.highlight_checkbox.setChecked(QtCore.Qt.Unchecked) 573 | self.story_checkbox.setChecked(QtCore.Qt.Checked) if (flag & snafylno.STORIES) else \ 574 | self.story_checkbox.setChecked(QtCore.Qt.Unchecked) 575 | self.post_checkbox.setChecked(QtCore.Qt.Checked) if (flag & snafylno.PICTURES or flag & snafylno.VIDEOS) else \ 576 | self.post_checkbox.setChecked(QtCore.Qt.Unchecked) 577 | self.archived_checkbox.setChecked(QtCore.Qt.Checked) if (flag & snafylno.ARCHIVED) else \ 578 | self.archived_checkbox.setChecked(QtCore.Qt.Unchecked) 579 | self.all_checkbox.setCheckState(QtCore.Qt.Checked) if ((flag & snafylno.ALL) == snafylno.ALL) else \ 580 | self.all_checkbox.setCheckState(QtCore.Qt.Unchecked) 581 | 582 | 583 | 584 | 585 | def display_collected_links(self, profiles: Dict, 586 | data_display: QtCore.pyqtBoundSignal) -> None: 587 | self.tree_links.clear() 588 | for key in profiles: 589 | profile = profiles[key] 590 | if profile.get_flag() == 0 or len(profile) < 1: 591 | continue 592 | user_posts = profile.fetch_posts() 593 | 594 | data = {} 595 | data["display_links"] = True 596 | data["profile"] = profile 597 | data["posts"] = user_posts 598 | data_display.emit(data) 599 | 600 | def display_subscriptions(self, subscriptions: Dict, 601 | data_display: QtCore.pyqtBoundSignal) -> None: 602 | for key in subscriptions: 603 | profile = subscriptions[key] 604 | username = profile.username() 605 | data = {} 606 | data["display_subscriptions"] = True 607 | data["username"] = username 608 | data_display.emit(data) 609 | 610 | 611 | def display_checkboxes(self, _bool) -> None: 612 | for checkbox in self.checkboxes: 613 | checkbox.setEnabled(_bool) 614 | 615 | 616 | 617 | 618 | def except_hook(cls, exception, traceback) -> None: 619 | sys.__excepthook__(cls, exception, traceback) 620 | 621 | 622 | if __name__ == "__main__": 623 | import sys 624 | sys.excepthook = except_hook 625 | 626 | app = QApplication(sys.argv) 627 | 628 | Main = MainWindow() 629 | 630 | 631 | sys.exit(app.exec_()) 632 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OFDL 2 | Onlyfans media downloader with graphical user interface using PyQt5. 3 | 4 | Downloads media files from *OF* (images, videos, highlights, stories) 5 | 6 | The only requirements should be requests and PyQt5 7 | 8 | Before logging in, press F12 (or inspect/inspect element and go to the network tab) to bring up the "developer tools" and then log in. You should see "init" as shown below. If not, type in the search "init" and or refresh the page. Once you've found it, click on it. 9 | 10 | 11 | 12 | Scroll down to the section "request headers" and everything you will need should be in this section (cookie, useragent, x-bc): 13 | 14 | 15 | 16 | 17 | Copy these three values: 18 | 19 | 20 | 21 | 22 | and put them into the textbox displayed after you click on the buttons pointed at in the below image: 23 | 24 | 25 | 26 | After you've added all three values, and you click the "x" button on the Options window, it should then fetch a list of your subscriptions. 27 | 28 | 29 | 30 | # Requirements 31 | 32 | Written using Python 3.9 so use 3.9 or anything above. 33 | 34 | The only two requirements/dependencies should be requests and PyQt5. 35 | 36 | There is a "requirements.txt" file that can be used to install the dependencies at the command line: 37 | 38 |
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 --------------------------------------------------------------------------------