%s
302 |303 | Version: %s 304 | (%s) 305 |
306 |
307 | Copyright © %s Pete Alexandrou
308 |
309 | Web: %s
310 |
312 | This program is free software; you can redistribute it and/or 313 | modify it under the terms of the GNU General Public License 314 | as published by the Free Software Foundation; either version 2 315 | of the License, or (at your option) any later version. 316 |
''' % (qApp.applicationName(), qApp.applicationVersion(), platform.architecture()[0], 317 | datetime.now().year, qApp.organizationDomain(), qApp.organizationDomain()) 318 | QMessageBox.about(self, 'About %s' % qApp.applicationName(), about_html) 319 | 320 | @pyqtSlot(int) 321 | def update_pagecount(self, index: int) -> None: 322 | self.dl_pagecount = int(self.dlpages_field.itemText(index)) 323 | self.scrapeWorker.maxpages = self.dl_pagecount 324 | self.progress.setMaximum(self.dl_pagecount * self.dl_pagelinks) 325 | self.settings.setValue('dl_pagecount', self.dl_pagecount) 326 | if sys.platform == 'win32': 327 | self.win_taskbar_button.progress().setMaximum(self.dl_pagecount * self.dl_pagelinks) 328 | if self.scrapeThread.isRunning(): 329 | self.scrapeThread.requestInterruption() 330 | self.start_scraping() 331 | 332 | @pyqtSlot() 333 | def show_progress(self): 334 | self.progress.show() 335 | self.taskbar.setProgress(0.0, True) 336 | if sys.platform == 'win32': 337 | self.win_taskbar_button.setWindow(self.windowHandle()) 338 | self.win_taskbar_button.progress().setRange(0, self.dl_pagecount * self.dl_pagelinks) 339 | self.win_taskbar_button.progress().setVisible(True) 340 | self.win_taskbar_button.progress().setValue(self.progress.value()) 341 | 342 | @pyqtSlot() 343 | def scrape_finished(self) -> None: 344 | self.progress.hide() 345 | self.taskbar.setProgress(0.0, False) 346 | if sys.platform == 'win32': 347 | self.win_taskbar_button.progress().setVisible(False) 348 | self.table.setSortingEnabled(True) 349 | self.filter_table(text='') 350 | 351 | @pyqtSlot(list) 352 | def add_row(self, row: list) -> None: 353 | if self.scrapeThread.isInterruptionRequested(): 354 | self.scrapeThread.terminate() 355 | else: 356 | self.cols = 0 357 | self.table.setRowCount(self.rows + 1) 358 | if self.table.cursor() != Qt.PointingHandCursor: 359 | self.table.setCursor(Qt.PointingHandCursor) 360 | for item in row: 361 | table_item = QTableWidgetItem(item) 362 | # table_item.setToolTip('%s\n\nDouble-click to view hoster links.' % row[1]) 363 | table_item.setFont(QFont('Open Sans', weight=QFont.Normal)) 364 | if self.cols == 2: 365 | if sys.platform == 'win32': 366 | table_item.setFont(QFont('Open Sans Semibold', pointSize=10)) 367 | elif sys.platform == 'darwin': 368 | table_item.setFont(QFont('Open Sans Bold', weight=QFont.Bold)) 369 | else: 370 | table_item.setFont(QFont('Open Sans', weight=QFont.DemiBold, pointSize=10)) 371 | table_item.setText(' ' + table_item.text()) 372 | elif self.cols in (0, 3): 373 | table_item.setTextAlignment(Qt.AlignCenter) 374 | self.table.setItem(self.rows, self.cols, table_item) 375 | self.update_metabar() 376 | self.cols += 1 377 | self.rows += 1 378 | 379 | @pyqtSlot(list) 380 | def add_hosters(self, links: list) -> None: 381 | self.hosters_win.show_hosters(links) 382 | 383 | @pyqtSlot(QModelIndex) 384 | def show_hosters(self, index: QModelIndex) -> None: 385 | QDesktopServices.openUrl(QUrl('http://scene-rls.net/releases/' + self.table.item(self.table.currentRow(), 1).text())) 386 | # qApp.setOverrideCursor(Qt.BusyCursor) 387 | # self.hosters_win = HosterLinks(self) 388 | # self.hosters_win.downloadLink.connect(self.download_link) 389 | # self.hosters_win.copyLink.connect(self.copy_download_link) 390 | # self.links = HostersThread(self.table.item(self.table.currentRow(), 1).text(), self.user_agent) 391 | # self.links.setHosters.connect(self.add_hosters) 392 | # self.links.noLinks.connect(self.no_links) 393 | # self.links.start() 394 | 395 | @pyqtSlot() 396 | def no_links(self) -> None: 397 | self.hosters_win.loading_progress.cancel() 398 | self.hosters_win.close() 399 | QMessageBox.warning(self, 'No Links Available', 'No links are available yet for the chosen TV show. ' + 400 | 'This is most likely due to the files still being uploaded. This is normal if the ' + 401 | 'link was published 30-45 mins ago.\n\nPlease check back again in 10-15 minutes.') 402 | 403 | @pyqtSlot(bool) 404 | def filter_faves(self, checked: bool) -> None: 405 | self.settings.setValue('faves_filter', checked) 406 | # if hasattr(self, 'scrapeWorker') and (sip.isdeleted(self.scrapeWorker) or self.scrapeWorker.complete): 407 | if not self.firstrun: 408 | self.filter_table() 409 | 410 | @pyqtSlot(str) 411 | @pyqtSlot() 412 | def filter_table(self, text: str='') -> None: 413 | filters = [] 414 | if self.favorites_button.isChecked(): 415 | filters = self.favorites 416 | self.table.sortItems(2, Qt.AscendingOrder) 417 | else: 418 | self.table.sortItems(0,Qt.DescendingOrder) 419 | if len(text): 420 | filters.append(text) 421 | if not len(filters) or not hasattr(self, 'valid_rows'): 422 | self.valid_rows = [] 423 | for search_term in filters: 424 | for item in self.table.findItems(search_term, Qt.MatchContains): 425 | self.valid_rows.append(item.row()) 426 | for row in range(0, self.table.rowCount()): 427 | if not len(filters): 428 | self.table.showRow(row) 429 | else: 430 | if row not in self.valid_rows: 431 | self.table.hideRow(row) 432 | else: 433 | self.table.showRow(row) 434 | 435 | @pyqtSlot() 436 | def clear_filters(self): 437 | if not len(self.search_field.text()): 438 | self.filter_table('') 439 | 440 | @pyqtSlot(bool) 441 | def aria2_confirmation(self, success: bool) -> None: 442 | qApp.restoreOverrideCursor() 443 | if success: 444 | if sys.platform.startswith('linux'): 445 | self.notify(title=qApp.applicationName(), msg='Your download link has been unrestricted and now ' + 446 | 'queued in Aria2 RPC Daemon', icon=self.NotifyIcon.SUCCESS) 447 | else: 448 | QMessageBox.information(self, qApp.applicationName(), 449 | 'Download link has been queued in Aria2.', QMessageBox.Ok) 450 | else: 451 | QMessageBox.critical(self, 'Aria2 RPC Daemon', 452 | 'Could not connect to Aria2 RPC Daemon. ' + 453 | 'Check your %s settings and try again.' % qApp.applicationName(), QMessageBox.Ok) 454 | 455 | @pyqtSlot(str) 456 | def download_link(self, link: str) -> None: 457 | if len(self.realdebrid_api_token) > 0 and 'real-debrid.com' not in link \ 458 | and 'rdeb.io' not in link: 459 | qApp.setOverrideCursor(Qt.BusyCursor) 460 | self.unrestrict_link(link, True) 461 | else: 462 | if self.download_manager == 'aria2': 463 | self.aria2 = Aria2Thread(settings=self.settings, link_url=link) 464 | self.aria2.aria2Confirmation.connect(self.aria2_confirmation) 465 | self.aria2.start() 466 | self.hosters_win.close() 467 | elif self.download_manager == 'pyload': 468 | self.pyload_conn = PyloadConnection(self.pyload_host, self.pyload_username, self.pyload_password) 469 | pid = self.pyload_conn.addPackage(name='TVLinker', links=[link]) 470 | qApp.restoreOverrideCursor() 471 | self.hosters_win.close() 472 | if sys.platform.startswith('linux'): 473 | self.notify(title='Download added to %s' % self.download_manager, icon=self.NotifyIcon.SUCCESS) 474 | else: 475 | QMessageBox.information(self, self.download_manager, 'Your link has been queued in %s.' 476 | % self.download_manager, QMessageBox.Ok) 477 | # open_pyload = msgbox.addButton('Open pyLoad', QMessageBox.AcceptRole) 478 | # open_pyload.clicked.connect(self.open_pyload) 479 | elif self.download_manager in ('kget', 'persepolis'): 480 | provider = self.kget_cmd if self.download_manager == 'kget' else self.persepolis_cmd 481 | cmd = '{0} "{1}"'.format(provider, link) 482 | if self.cmdexec(cmd): 483 | qApp.restoreOverrideCursor() 484 | self.hosters_win.close() 485 | if sys.platform.startswith('linux'): 486 | self.notify(title='Download added to %s' % self.download_manager, icon=self.NotifyIcon.SUCCESS) 487 | else: 488 | QMessageBox.information(self, self.download_manager, 'Your link has been queued in %s.' 489 | % self.download_manager, QMessageBox.Ok) 490 | elif self.download_manager == 'idm': 491 | cmd = '"%s" /n /d "%s"' % (self.idm_exe_path, link) 492 | if self.cmdexec(cmd): 493 | qApp.restoreOverrideCursor() 494 | self.hosters_win.close() 495 | QMessageBox.information(self, 'Internet Download Manager', 'Your link has been queued in IDM.') 496 | else: 497 | print('IDM QProcess error = %s' % self.ProcError(self.idm.error()).name) 498 | qApp.restoreOverrideCursor() 499 | self.hosters_win.close() 500 | QMessageBox.critical(self, 'Internet Download Manager', 501 | 'Could not connect to your local IDM application instance. ' + 502 | 'Please check your settings and ensure the IDM executable path is correct ' + 503 | 'according to your installation.
Error Code: %s
' 504 | % self.ProcError(self.idm.error()).name, QMessageBox.Ok) 505 | else: 506 | dlpath, _ = QFileDialog.getSaveFileName(self, 'Save File', link.split('/')[-1]) 507 | if dlpath != '': 508 | self.directdl_win = DirectDownload(parent=self) 509 | self.directdl = DownloadThread(link_url=link, dl_path=dlpath) 510 | self.directdl.dlComplete.connect(self.directdl_win.download_complete) 511 | if sys.platform.startswith('linux'): 512 | self.directdl.dlComplete.connect(lambda: self.notify(qApp.applicationName(), 513 | 'Download complete', 514 | self.NotifyIcon.SUCCESS)) 515 | else: 516 | self.directdl.dlComplete.connect(lambda: QMessageBox.information(self, qApp.applicationName(), 517 | 'Download complete', 518 | QMessageBox.Ok)) 519 | self.directdl.dlProgressTxt.connect(self.directdl_win.update_progress_label) 520 | self.directdl.dlProgress.connect(self.directdl_win.update_progress) 521 | self.directdl_win.cancelDownload.connect(self.cancel_download) 522 | self.directdl.start() 523 | self.hosters_win.close() 524 | 525 | def _init_notification_icons(self): 526 | for icon in self.NotifyIcon: 527 | icon_file = QPixmap(icon.value, 'PNG') 528 | icon_file.save(os.path.join(FixedSettings.config_path, os.path.basename(icon.value)), 'PNG', 100) 529 | 530 | def notify(self, title: str, msg: str = '', icon: Enum = None, urgency: int = 1) -> bool: 531 | icon_path = icon.value if icon is not None else self.NotifyIcon.DEFAULT.value 532 | icon_path = os.path.join(FixedSettings.config_path, os.path.basename(icon_path)) 533 | if not os.path.exists(icon_path): 534 | self._init_notification_icons() 535 | notification = notify.Notification(title, msg, icon_path) 536 | notification.set_urgency(urgency) 537 | return notification.show() 538 | 539 | def cmdexec(self, cmd: str) -> bool: 540 | self.proc = QProcess() 541 | self.proc.setProcessChannelMode(QProcess.MergedChannels) 542 | if hasattr(self.proc, 'errorOccurred'): 543 | self.proc.errorOccurred.connect(lambda error: print('Process error = %s' % self.ProcError(error).name)) 544 | if self.proc.state() == QProcess.NotRunning: 545 | self.proc.start(cmd) 546 | self.proc.waitForFinished(-1) 547 | rc = self.proc.exitStatus() == QProcess.NormalExit and self.proc.exitCode() == 0 548 | self.proc.deleteLater() 549 | return rc 550 | return False 551 | 552 | @pyqtSlot() 553 | def cancel_download(self) -> None: 554 | self.directdl.cancel_download = True 555 | self.directdl.quit() 556 | self.directdl.deleteLater() 557 | 558 | def open_pyload(self) -> None: 559 | QDesktopServices.openUrl(QUrl(self.pyload_config.host)) 560 | 561 | @pyqtSlot(str) 562 | def copy_download_link(self, link: str) -> None: 563 | if len(self.realdebrid_api_token) > 0 and 'real-debrid.com' not in link \ 564 | and 'rdeb.io' not in link: 565 | qApp.setOverrideCursor(Qt.BusyCursor) 566 | self.unrestrict_link(link, False) 567 | else: 568 | clip = qApp.clipboard() 569 | clip.setText(link) 570 | self.hosters_win.close() 571 | qApp.restoreOverrideCursor() 572 | 573 | def unrestrict_link(self, link: str, download: bool = True) -> None: 574 | caller = inspect.stack()[1].function 575 | self.realdebrid = RealDebridThread(settings=self.settings, api_url=FixedSettings.realdebrid_api_url, 576 | link_url=link, action=RealDebridThread.RealDebridAction.UNRESTRICT_LINK) 577 | self.realdebrid.errorMsg.connect(self.error_handler) 578 | if download: 579 | self.realdebrid.unrestrictedLink.connect(self.download_link) 580 | else: 581 | self.realdebrid.unrestrictedLink.connect(self.copy_download_link) 582 | self.realdebrid.start() 583 | 584 | def closeEvent(self, event: QCloseEvent) -> None: 585 | if hasattr(self, 'scrapeThread'): 586 | if not sip.isdeleted(self.scrapeThread) and self.scrapeThread.isRunning(): 587 | self.scrapeThread.requestInterruption() 588 | self.scrapeThread.quit() 589 | qApp.quit() 590 | 591 | def error_handler(self, props: list) -> None: 592 | qApp.restoreOverrideCursor() 593 | QMessageBox.critical(self, props[0], props[1], QMessageBox.Ok) 594 | 595 | @staticmethod 596 | def get_path(path: str = None, override: bool = False) -> str: 597 | if override: 598 | if getattr(sys, 'frozen', False): 599 | return os.path.join(sys._MEIPASS, path) 600 | return os.path.join(QFileInfo(__file__).absolutePath(), path) 601 | return ':assets/%s' % path 602 | 603 | @staticmethod 604 | def get_version(filename: str = '__init__.py') -> str: 605 | with open(TVLinker.get_path(filename, override=True), 'r') as initfile: 606 | for line in initfile.readlines(): 607 | m = re.match('__version__ *= *[\'](.*)[\']', line) 608 | if m: 609 | return m.group(1) 610 | 611 | def keyPressEvent(self, event: QKeyEvent) -> None: 612 | if event.key() == Qt.Key_F5: 613 | self.start_scraping() 614 | return 615 | if event.key() in {Qt.Key_Q, Qt.Key_W} and event.modifiers() == Qt.ControlModifier: 616 | qApp.quit() 617 | return 618 | 619 | class FixedSettings: 620 | applicationName = 'TVLinker' 621 | applicationVersion = TVLinker.get_version() 622 | organizationDomain = 'https://tvlinker.ozmartians.com' 623 | windowSize = QSize(1000, 785) 624 | linksPerPage = 20 625 | latest_release_url = 'https://github.com/ozmartian/tvlinker/releases/latest' 626 | realdebrid_api_url = 'https://api.real-debrid.com/rest/1.0' 627 | config_path = None 628 | 629 | @staticmethod 630 | def get_app_settings() -> QSettings: 631 | FixedSettings.config_path = QStandardPaths.writableLocation(QStandardPaths.AppConfigLocation) 632 | settings_ini = os.path.join(FixedSettings.config_path, '%s.ini' % FixedSettings.applicationName.lower()) 633 | if not os.path.exists(settings_ini): 634 | os.makedirs(FixedSettings.config_path, exist_ok=True) 635 | QFile.copy(':%s.ini' % FixedSettings.applicationName.lower(), settings_ini) 636 | QFile.setPermissions(settings_ini, QFile.ReadOwner | QFile.WriteOwner) 637 | return QSettings(settings_ini, QSettings.IniFormat) 638 | 639 | 640 | def main(): 641 | app = QApplication(sys.argv) 642 | app.setStyle(OverrideStyle()) 643 | app.setApplicationName(FixedSettings.applicationName) 644 | app.setOrganizationDomain(FixedSettings.organizationDomain) 645 | app.setApplicationVersion(FixedSettings.applicationVersion) 646 | app.setQuitOnLastWindowClosed(True) 647 | tvlinker = TVLinker(FixedSettings.get_app_settings()) 648 | sys.exit(app.exec_()) 649 | 650 | 651 | if __name__ == '__main__': 652 | main() 653 | -------------------------------------------------------------------------------- /tvlinker/assets/assets.qrc: -------------------------------------------------------------------------------- 1 |The aria2c executable binary could not ' + 35 | 'be found in your installation folders. The binary comes packaged with this ' + 36 | 'application so it is likely that it was accidentally deleted via human ' + 37 | 'intervntion or incorrect file permissions are preventing access to it.
' + 38 | 'You may either download and install aria2 manually yourself, ensuring ' +
39 | 'its installation location is globally accessible via PATH environmnt variables or ' +
40 | 'simply reinstall this application again. If the issue is not resolved then try ' +
41 | 'to download the application again incase the orignal you installed already was ' +
42 | 'corrupted/broken.', buttons=QMessageBox.Close)
43 |
44 | def __del__(self) -> None:
45 | self.proc.terminate()
46 | if not self.proc.waitForFinished(10000):
47 | self.proc.kill()
48 |
49 | @staticmethod
50 | def get_machine_code() -> str:
51 | mcode = ''
52 | if sys.platform == 'darwin':
53 | mcode = 'macOS'
54 | elif sys.platform == 'win32' and platform.machine().endswith('86'):
55 | mcode = 'win32'
56 | elif sys.platform == 'win32' and platform.machine().endswith('64'):
57 | mcode = 'win64'
58 | elif sys.platform.startswith('linux') and platform.machine().endswith('86'):
59 | mcode = 'linux32'
60 | elif sys.platform.startswith('linux') and platform.machine().endswith('64'):
61 | mcode = 'linux64'
62 | return mcode
63 |
64 | @staticmethod
65 | def setup_aria() -> bool:
66 | aria_zip = Downloader.aria_clients()[Downloader.get_machine_code()]['bin_archive']
67 | aria_install = Downloader.aria_clients()[Downloader.get_machine_code()]['target_path']
68 | if os.path.exists(aria_zip):
69 | with ZipFile(aria_zip) as archive:
70 | target_path, target_file = os.path.split(aria_install)
71 | extracted_path = archive.extract(target_file, path=target_path)
72 | if extracted_path == aria_install and os.path.exists(extracted_path):
73 | if sys.platform != 'win32':
74 | os.chmod(extracted_path, 0o755)
75 | return True
76 | return False
77 |
78 | def init_proc(self) -> None:
79 | self.proc.setProcessChannelMode(QProcess.MergedChannels)
80 | self.proc.readyRead.connect(self.console_output)
81 | self.proc.setProgram(self.aria2_cmd)
82 | self.proc.setArguments(shlex.split(self.aria2_args))
83 |
84 | def start(self) -> None:
85 | self.init_proc()
86 | self.show()
87 | self.proc.start()
88 |
89 | @pyqtSlot()
90 | def console_output(self) -> None:
91 | self.console.append(str(self.proc.readAllStandardOutput()))
92 |
93 | @pyqtSlot(QProcess.ProcessError)
94 | def cmd_error(self, error: QProcess.ProcessError) -> None:
95 | if error != QProcess.Crashed:
96 | QMessageBox.critical(self.parent, 'Error calling an external process',
97 | self.proc.errorString(), buttons=QMessageBox.Close)
98 |
99 | @staticmethod
100 | def get_path(path: str) -> str:
101 | prefix = sys._MEIPASS if getattr(sys, 'frozen', False) else QFileInfo(__file__).absolutePath()
102 | return os.path.join(prefix, path)
103 |
--------------------------------------------------------------------------------
/tvlinker/filesize.py:
--------------------------------------------------------------------------------
1 |
2 | traditional = [
3 | (1024 ** 5, 'P'),
4 | (1024 ** 4, 'T'),
5 | (1024 ** 3, 'G'),
6 | (1024 ** 2, 'M'),
7 | (1024 ** 1, 'K'),
8 | (1024 ** 0, 'B'),
9 | ]
10 |
11 | alternative = [
12 | (1024 ** 5, ' PB'),
13 | (1024 ** 4, ' TB'),
14 | (1024 ** 3, ' GB'),
15 | (1024 ** 2, ' MB'),
16 | (1024 ** 1, ' KB'),
17 | (1024 ** 0, (' byte', ' bytes')),
18 | ]
19 |
20 | verbose = [
21 | (1024 ** 5, (' petabyte', ' petabytes')),
22 | (1024 ** 4, (' terabyte', ' terabytes')),
23 | (1024 ** 3, (' gigabyte', ' gigabytes')),
24 | (1024 ** 2, (' megabyte', ' megabytes')),
25 | (1024 ** 1, (' kilobyte', ' kilobytes')),
26 | (1024 ** 0, (' byte', ' bytes')),
27 | ]
28 |
29 | iec = [
30 | (1024 ** 5, 'Pi'),
31 | (1024 ** 4, 'Ti'),
32 | (1024 ** 3, 'Gi'),
33 | (1024 ** 2, 'Mi'),
34 | (1024 ** 1, 'Ki'),
35 | (1024 ** 0, ''),
36 | ]
37 |
38 | si = [
39 | (1000 ** 5, 'P'),
40 | (1000 ** 4, 'T'),
41 | (1000 ** 3, 'G'),
42 | (1000 ** 2, 'M'),
43 | (1000 ** 1, 'K'),
44 | (1000 ** 0, 'B'),
45 | ]
46 |
47 |
48 |
49 | def size(bytes, system=traditional):
50 | """Human-readable file size.
51 |
52 | Using the traditional system, where a factor of 1024 is used::
53 |
54 | >>> size(10)
55 | '10B'
56 | >>> size(100)
57 | '100B'
58 | >>> size(1000)
59 | '1000B'
60 | >>> size(2000)
61 | '1K'
62 | >>> size(10000)
63 | '9K'
64 | >>> size(20000)
65 | '19K'
66 | >>> size(100000)
67 | '97K'
68 | >>> size(200000)
69 | '195K'
70 | >>> size(1000000)
71 | '976K'
72 | >>> size(2000000)
73 | '1M'
74 |
75 | Using the SI system, with a factor 1000::
76 |
77 | >>> size(10, system=si)
78 | '10B'
79 | >>> size(100, system=si)
80 | '100B'
81 | >>> size(1000, system=si)
82 | '1K'
83 | >>> size(2000, system=si)
84 | '2K'
85 | >>> size(10000, system=si)
86 | '10K'
87 | >>> size(20000, system=si)
88 | '20K'
89 | >>> size(100000, system=si)
90 | '100K'
91 | >>> size(200000, system=si)
92 | '200K'
93 | >>> size(1000000, system=si)
94 | '1M'
95 | >>> size(2000000, system=si)
96 | '2M'
97 |
98 | """
99 | for factor, suffix in system:
100 | if bytes >= factor:
101 | break
102 | amount = int(bytes/factor)
103 | if isinstance(suffix, tuple):
104 | singular, multiple = suffix
105 | if amount == 1:
106 | suffix = singular
107 | else:
108 | suffix = multiple
109 | return str(amount) + suffix
110 |
111 |
--------------------------------------------------------------------------------
/tvlinker/hosters.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 | import sys
5 |
6 | from bs4 import BeautifulSoup, SoupStrainer, Tag
7 |
8 | from PyQt5.QtCore import QSize, QUrl, Qt, pyqtSignal, pyqtSlot
9 | from PyQt5.QtGui import QCloseEvent, QColor, QDesktopServices, QIcon, QPalette
10 | from PyQt5.QtWidgets import (QDialog, QGraphicsDropShadowEffect, QHBoxLayout, QLabel, QMenu, QProgressBar,
11 | QProgressDialog, QPushButton, QScrollArea, QSizePolicy, QStyleFactory,
12 | QVBoxLayout, QWidget, qApp)
13 |
14 |
15 | class HosterLinks(QDialog):
16 | downloadLink = pyqtSignal(str)
17 | copyLink = pyqtSignal(str)
18 |
19 | def __init__(self, parent, f=Qt.WindowCloseButtonHint):
20 | super(HosterLinks, self).__init__(parent, f)
21 | self.parent = parent
22 | self.setObjectName('hosters')
23 | self.loading_progress = HosterProgress('Retrieving hoster links...', 0, 0, self.parent,
24 | Qt.WindowCloseButtonHint)
25 | self.layout = QVBoxLayout()
26 | self.layout.setSpacing(15)
27 | self.setLayout(self.layout)
28 | self.setWindowTitle('Hoster Links')
29 | self.setWindowModality(Qt.ApplicationModal)
30 | self.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)
31 |
32 | def show_hosters(self, links: list) -> None:
33 | self.links = links
34 | self.loading_progress.cancel()
35 | hosterswidget_layout = QVBoxLayout()
36 | for tag in self.links:
37 | title_label = QLabel(HosterLinks.bs_tag_to_string(tag.find_previous('p')), self)
38 | title_label.setTextInteractionFlags(Qt.TextBrowserInteraction)
39 | title_label.setCursor(Qt.IBeamCursor)
40 | title_label.setOpenExternalLinks(True)
41 | title_label.setAlignment(Qt.AlignCenter)
42 | title_label.setStyleSheet('QLabel { margin: 0; color: #444; font-size: 14px; padding: 8px; ' +
43 | 'border: 1px solid #C0C0C0; background-color: #FEFEFE; }')
44 | title_layout = QHBoxLayout()
45 | title_layout.setContentsMargins(0, 0, 0, 0)
46 | title_layout.setSpacing(6)
47 | title_layout.addStretch(1)
48 | bs = BeautifulSoup(HosterLinks.bs_tag_to_string(tag), 'lxml', parse_only=SoupStrainer('a'))
49 | for anchor in bs:
50 | link = anchor['href']
51 | hoster_name = HosterLinks.get_hoster_name(link)
52 | menu = QMenu(self)
53 | menu.setCursor(Qt.PointingHandCursor)
54 | menu.addAction(' COPY LINK', lambda dl=link: self.copy_link(dl), 0)
55 | menu.addAction(' OPEN LINK', lambda dl=link: self.open_link(dl), 0)
56 | menu.addAction(' DOWNLOAD', lambda dl=link: self.download_link(dl), 0)
57 | shadow = QGraphicsDropShadowEffect()
58 | shadow.setColor(Qt.gray)
59 | shadow.setBlurRadius(10)
60 | shadow.setOffset(2, 2)
61 | hoster_btn = QPushButton(self)
62 | hoster_btn.setStyle(QStyleFactory.create('Fusion'))
63 | hoster_btn.setGraphicsEffect(shadow)
64 | hoster_btn.setDefault(False)
65 | hoster_btn.setAutoDefault(False)
66 | hoster_btn.setToolTip(hoster_name)
67 | hoster_btn.setCursor(Qt.PointingHandCursor)
68 | hoster_btn.setIcon(QIcon(self.parent.get_path('images/hosters/%s.png' % hoster_name)))
69 | hoster_btn.setIconSize(QSize(100, 21))
70 | hoster_btn.setStyleSheet('''
71 | QPushButton {
72 | background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
73 | stop: 0 #FEFEFE, stop: 1 #FAFAFA);
74 | padding: 6px 0;
75 | border-radius: 0;
76 | border: 1px solid #B9B9B9;
77 | }
78 | QPushButton:hover {
79 | background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
80 | stop: 0 #B9B9B9, stop: 1 #DADBDE);
81 | }
82 | QPushButton:pressed {
83 | border: 1px inset #B9B9B9;
84 | background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,
85 | stop: 0 #DADBDE, stop: 1 #B9B9B9);
86 | }
87 | QPushButton::menu-indicator { left: -2000px; }
88 | ''')
89 | menu.setFixedWidth(118)
90 | menu.setStyleSheet('''
91 | QMenu {
92 | border-radius: 0;
93 | border: 1px solid #C0C2C3;
94 | background-color: #FAFAFA;
95 | color: #4C4C4C;
96 | }
97 | QMenu::item { text-align: center; }
98 | QMenu::item:selected, QMenu::item:hover { background-color: #6A687D; color: #FFF; }''')
99 | hoster_btn.setMenu(menu)
100 | title_layout.addWidget(hoster_btn)
101 | title_layout.addStretch(1)
102 | hoster_layout = QVBoxLayout()
103 | hoster_layout.addWidget(title_label)
104 | hoster_layout.addLayout(title_layout)
105 | hosterswidget_layout.addLayout(hoster_layout)
106 | hosterswidget_layout.addSpacing(15)
107 |
108 | stretch_layout = QHBoxLayout()
109 | stretch_layout.addStretch(1)
110 | stretch_layout.addLayout(hosterswidget_layout)
111 | stretch_layout.addStretch(1)
112 | hosters_widget = QWidget(self)
113 | hosters_widget.setLayout(stretch_layout)
114 | scrollarea = QScrollArea(self)
115 | scrollarea.setFrameShape(QScrollArea.NoFrame)
116 | scrollarea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
117 | scrollarea.setWidget(hosters_widget)
118 | scrollarea.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
119 | scrollarea.setMinimumWidth(hosters_widget.geometry().width() + 20)
120 | self.layout.addWidget(scrollarea)
121 | w = scrollarea.width() + 15
122 | h = hosters_widget.geometry().height() + 10
123 | self.setFixedSize(w if w <= 810 else 810, h if h <= 750 else 750)
124 | self.show()
125 | qApp.restoreOverrideCursor()
126 |
127 | @staticmethod
128 | def bs_tag_to_string(bstag: Tag) -> str:
129 | return ''.join(str(item) for item in bstag.contents)
130 |
131 | @staticmethod
132 | def get_hoster_name(link: str) -> str:
133 | name = QUrl(link).host().replace('www.', '').replace('.com', '').replace('.net', '') \
134 | .replace('.org', '').replace('.co', '').replace('.to', '').replace('.download', '')
135 | if name == 'businessnewscurrent.online':
136 | name = 'cloudyfiles'
137 | elif name == 'drop':
138 | name = 'dropapk'
139 | elif name == 'nitro':
140 | name = 'nitroflare'
141 | elif name == 'ul':
142 | name = 'uploaded'
143 | return name
144 |
145 | @pyqtSlot(str)
146 | def copy_link(self, link: str) -> None:
147 | self.copyLink.emit(link)
148 |
149 | @pyqtSlot(str)
150 | def open_link(self, link: str) -> None:
151 | QDesktopServices.openUrl(QUrl(link))
152 | self.close()
153 |
154 | @pyqtSlot(str)
155 | def download_link(self, link: str) -> None:
156 | self.downloadLink.emit(link)
157 |
158 | def closeEvent(self, event: QCloseEvent) -> None:
159 | self.loading_progress.cancel()
160 | self.deleteLater()
161 | qApp.restoreOverrideCursor()
162 | super(QDialog, self).closeEvent(event)
163 |
164 |
165 | class HosterProgress(QProgressDialog):
166 | def __init__(self, label: str, minval: int, maxval: int, parent: QWidget, flags: Qt.WindowFlags):
167 | super(HosterProgress, self).__init__(label, None, minval, maxval, parent, flags)
168 | self.setWindowTitle('Hoster Links')
169 | self.setMinimumWidth(485)
170 | self.setWindowModality(Qt.ApplicationModal)
171 | self.setLabelText(label)
172 | bar = QProgressBar(self)
173 | bar.setRange(minval, maxval)
174 | bar.setValue(minval)
175 | bar.setStyle(QStyleFactory.create('Fusion'))
176 | p = bar.palette()
177 | p.setColor(QPalette.Highlight, QColor(100, 44, 104, 185))
178 | bar.setPalette(p)
179 | self.setBar(bar)
180 | self.show()
181 |
--------------------------------------------------------------------------------
/tvlinker/jsfuck.py:
--------------------------------------------------------------------------------
1 | MAPPING = {
2 | 'a': '(false+"")[1]',
3 | 'b': '([]["entries"]()+"")[2]',
4 | 'c': '([]["fill"]+"")[3]',
5 | 'd': '(undefined+"")[2]',
6 | 'e': '(true+"")[3]',
7 | 'f': '(false+"")[0]',
8 | 'g': '(false+[0]+String)[20]',
9 | 'h': '(+(101))["to"+String["name"]](21)[1]',
10 | 'i': '([false]+undefined)[10]',
11 | 'j': '([]["entries"]()+"")[3]',
12 | 'k': '(+(20))["to"+String["name"]](21)',
13 | 'l': '(false+"")[2]',
14 | 'm': '(Number+"")[11]',
15 | 'n': '(undefined+"")[1]',
16 | 'o': '(true+[]["fill"])[10]',
17 | 'p': '(+(211))["to"+String["name"]](31)[1]',
18 | 'q': '(+(212))["to"+String["name"]](31)[1]',
19 | 'r': '(true+"")[1]',
20 | 's': '(false+"")[3]',
21 | 't': '(true+"")[0]',
22 | 'u': '(undefined+"")[0]',
23 | 'v': '(+(31))["to"+String["name"]](32)',
24 | 'w': '(+(32))["to"+String["name"]](33)',
25 | 'x': '(+(101))["to"+String["name"]](34)[1]',
26 | 'y': '(NaN+[Infinity])[10]',
27 | 'z': '(+(35))["to"+String["name"]](36)',
28 | 'A': '(+[]+Array)[10]',
29 | 'B': '(+[]+Boolean)[10]',
30 | 'C': 'Function("return escape")()(("")["italics"]())[2]',
31 | 'D': 'Function("return escape")()([]["fill"])["slice"]("-1")',
32 | 'E': '(RegExp+"")[12]',
33 | 'F': '(+[]+Function)[10]',
34 | 'G': '(false+Function("return Date")()())[30]',
35 | 'I': '(Infinity+"")[0]',
36 | 'M': '(true+Function("return Date")()())[30]',
37 | 'N': '(NaN+"")[0]',
38 | 'O': '(NaN+Function("return{}")())[11]',
39 | 'R': '(+[]+RegExp)[10]',
40 | 'S': '(+[]+String)[10]',
41 | 'T': '(NaN+Function("return Date")()())[30]',
42 | 'U': '(NaN+Function("return{}")()["to"+String["name"]]["call"]())[11]',
43 | ' ': '(NaN+[]["fill"])[11]',
44 | '"': '("")["fontcolor"]()[12]',
45 | '%': 'Function("return escape")()([]["fill"])[21]',
46 | '&': '("")["link"](0+")[10]',
47 | '(': '(undefined+[]["fill"])[22]',
48 | ')': '([0]+false+[]["fill"])[20]',
49 | '+': '(+(+!+[]+(!+[]+[])[!+[]+!+[]+!+[]]+[+!+[]]+[+[]]+[+[]])+[])[2]',
50 | ',': '([]["slice"]["call"](false+"")+"")[1]',
51 | '-': '(+(.+[0000000001])+"")[2]',
52 | '.': '(+(+!+[]+[+!+[]]+(!![]+[])[!+[]+!+[]+!+[]]+[!+[]+!+[]]+[+[]])+[])[+!+[]]',
53 | '/': '(false+[0])["italics"]()[10]',
54 | ':': '(RegExp()+"")[3]',
55 | ';': '("")["link"](")[14]',
56 | '<': '("")["italics"]()[0]',
57 | '=': '("")["fontcolor"]()[11]',
58 | '>': '("")["italics"]()[2]',
59 | '?': '(RegExp()+"")[2]',
60 | '[': '([]["entries"]()+"")[0]',
61 | ']': '([]["entries"]()+"")[22]',
62 | '{': '(true+[]["fill"])[20]',
63 | '}': '([]["fill"]+"")["slice"]("-1")'
64 | }
65 |
66 | SIMPLE = {
67 | 'false': '![]',
68 | 'true': '!![]',
69 | 'undefined': '[][[]]',
70 | 'NaN': '+[![]]',
71 | 'Infinity': '+(+!+[]+(!+[]+[])[!+[]+!+[]+!+[]]+[+!+[]]+[+[]]+[+[]]+[+[]])' # +"1e1000"
72 | }
73 |
74 | CONSTRUCTORS = {
75 | 'Array': '[]',
76 | 'Number': '(+[])',
77 | 'String': '([]+[])',
78 | 'Boolean': '(![])',
79 | 'Function': '[]["fill"]',
80 | 'RegExp': 'Function("return/"+false+"/")()'
81 | }
82 |
83 | def jsunfuck(jsfuckString):
84 |
85 | for key in sorted(MAPPING, key=lambda k: len(MAPPING[k]), reverse=True):
86 | if MAPPING.get(key) in jsfuckString:
87 | jsfuckString = jsfuckString.replace(MAPPING.get(key), '"{}"'.format(key))
88 |
89 | for key in sorted(SIMPLE, key=lambda k: len(SIMPLE[k]), reverse=True):
90 | if SIMPLE.get(key) in jsfuckString:
91 | jsfuckString = jsfuckString.replace(SIMPLE.get(key), '{}'.format(key))
92 |
93 | #for key in sorted(CONSTRUCTORS, key=lambda k: len(CONSTRUCTORS[k]), reverse=True):
94 | # if CONSTRUCTORS.get(key) in jsfuckString:
95 | # jsfuckString = jsfuckString.replace(CONSTRUCTORS.get(key), '{}'.format(key))
96 |
97 | return jsfuckString
98 |
--------------------------------------------------------------------------------
/tvlinker/notify.py:
--------------------------------------------------------------------------------
1 | """This is a pure-python replacement for notify-python, using python-dbus
2 | to communicate with the notifications server directly. It's compatible with
3 | Python 2 and 3, and its callbacks can work with Gtk 3 or Qt 4 applications.
4 |
5 | To use it, first call ``notify2.init('app name')``, then create and show notifications::
6 |
7 | n = notify2.Notification("Summary",
8 | "Some body text",
9 | "notification-message-im" # Icon name
10 | )
11 | n.show()
12 |
13 | To see more of what's possible, refer to docstrings of methods and objects.
14 |
15 | Based on the notifications spec at:
16 | http://developer.gnome.org/notification-spec/
17 |
18 | Porting applications from pynotify
19 | ----------------------------------
20 |
21 | There are a few differences from pynotify you should be aware of:
22 |
23 | - If you need callbacks from notifications, notify2 must know about your event
24 | loop. The simplest way is to pass 'glib' or 'qt' as the ``mainloop`` parameter
25 | to ``init``.
26 | - The methods ``attach_to_widget`` and ``attach_to_status_icon`` are not
27 | implemented. You can calculate the location you want the notification to
28 | appear and call ``Notification``.
29 | - ``set_property`` and ``get_property`` are not implemented. The summary, body
30 | and icon are accessible as attributes of a ``Notification`` instance.
31 | - Various methods that pynotify Notification instances got from gobject do not
32 | exist, or only implement part of the functionality.
33 |
34 | Several pynotify functions, especially getters and setters, are only supported
35 | for compatibility. You are encouraged to use more direct, Pythonic alternatives.
36 | """
37 |
38 | import dbus
39 |
40 | # Constants
41 | EXPIRES_DEFAULT = -1
42 | EXPIRES_NEVER = 0
43 |
44 | URGENCY_LOW = 0
45 | URGENCY_NORMAL = 1
46 | URGENCY_CRITICAL = 2
47 | urgency_levels = [URGENCY_LOW, URGENCY_NORMAL, URGENCY_CRITICAL]
48 |
49 | # Initialise the module (following pynotify's API) -----------------------------
50 |
51 | initted = False
52 | appname = ""
53 | _have_mainloop = False
54 |
55 |
56 | class UninittedError(RuntimeError):
57 | pass
58 |
59 |
60 | class UninittedDbusObj(object):
61 | def __getattr__(self, name):
62 | raise UninittedError("You must call notify2.init() before using the "
63 | "notification features.")
64 |
65 |
66 | dbus_iface = UninittedDbusObj()
67 |
68 |
69 | def init(app_name, mainloop=None):
70 | """Initialise the Dbus connection.
71 |
72 | To get callbacks from notifications, DBus must be integrated with a mainloop.
73 | There are three ways to achieve this:
74 |
75 | - Set a default mainloop (dbus.set_default_main_loop) before calling init()
76 | - Pass the mainloop parameter as a string 'glib' or 'qt' to integrate with
77 | those mainloops. (N.B. passing 'qt' currently makes that the default dbus
78 | mainloop, because that's the only way it seems to work.)
79 | - Pass the mainloop parameter a DBus compatible mainloop instance, such as
80 | dbus.mainloop.glib.DBusGMainLoop().
81 |
82 | If you only want to display notifications, without receiving information
83 | back from them, you can safely omit mainloop.
84 | """
85 | global appname, initted, dbus_iface, _have_mainloop
86 |
87 | if mainloop == 'glib':
88 | from dbus.mainloop.glib import DBusGMainLoop
89 | mainloop = DBusGMainLoop()
90 | elif mainloop == 'qt':
91 | from dbus.mainloop.qt import DBusQtMainLoop
92 | # For some reason, this only works if we make it the default mainloop
93 | # for dbus. That might make life tricky for anyone trying to juggle two
94 | # event loops, but I can't see any way round it.
95 | mainloop = DBusQtMainLoop(set_as_default=True)
96 |
97 | bus = dbus.SessionBus(mainloop=mainloop)
98 |
99 | dbus_obj = bus.get_object('org.freedesktop.Notifications',
100 | '/org/freedesktop/Notifications')
101 | dbus_iface = dbus.Interface(dbus_obj,
102 | dbus_interface='org.freedesktop.Notifications')
103 | appname = app_name
104 | initted = True
105 |
106 | if mainloop or dbus.get_default_main_loop():
107 | _have_mainloop = True
108 | dbus_iface.connect_to_signal('ActionInvoked', _action_callback)
109 | dbus_iface.connect_to_signal('NotificationClosed', _closed_callback)
110 |
111 | return True
112 |
113 |
114 | def is_initted():
115 | """Has init() been called? Only exists for compatibility with pynotify.
116 | """
117 | return initted
118 |
119 |
120 | def get_app_name():
121 | """Return appname. Only exists for compatibility with pynotify.
122 | """
123 | return appname
124 |
125 |
126 | def uninit():
127 | """Undo what init() does."""
128 | global initted, dbus_iface, _have_mainloop
129 | initted = False
130 | _have_mainloop = False
131 | dbus_iface = UninittedDbusObj()
132 |
133 |
134 | # Retrieve basic server information --------------------------------------------
135 |
136 | def get_server_caps():
137 | """Get a list of server capabilities.
138 | """
139 | return [str(x) for x in dbus_iface.GetCapabilities()]
140 |
141 |
142 | def get_server_info():
143 | """Get basic information about the server.
144 | """
145 | res = dbus_iface.GetServerInformation()
146 | return {'name': str(res[0]),
147 | 'vendor': str(res[1]),
148 | 'version': str(res[2]),
149 | 'spec-version': str(res[3]),
150 | }
151 |
152 |
153 | # Action callbacks -------------------------------------------------------------
154 |
155 | notifications_registry = {}
156 |
157 |
158 | def _action_callback(nid, action):
159 | nid, action = int(nid), str(action)
160 | n = notifications_registry[nid]
161 | n._action_callback(action)
162 |
163 |
164 | def _closed_callback(nid, reason):
165 | nid, reason = int(nid), int(reason)
166 | n = notifications_registry[nid]
167 | n._closed_callback(n)
168 | del notifications_registry[nid]
169 |
170 |
171 | def no_op(*args):
172 | """No-op function for callbacks.
173 | """
174 | pass
175 |
176 |
177 | # Controlling notifications ----------------------------------------------------
178 |
179 | class Notification(object):
180 | id = 0
181 | timeout = -1 # -1 = server default settings
182 | _closed_callback = no_op
183 |
184 | def __init__(self, summary, message='', icon=''):
185 | self.summary = summary
186 | self.message = message
187 | self.icon = icon
188 | self.hints = {}
189 | self.actions = {}
190 | self.data = {} # Any data the user wants to attach
191 |
192 | def show(self):
193 | """Ask the server to show the notification.
194 | """
195 | nid = dbus_iface.Notify(appname, # app_name (spec names)
196 | self.id, # replaces_id
197 | self.icon, # app_icon
198 | self.summary, # summary
199 | self.message, # body
200 | self._make_actions_array(), # actions
201 | self.hints, # hints
202 | self.timeout, # expire_timeout
203 | )
204 |
205 | self.id = int(nid)
206 |
207 | if _have_mainloop:
208 | notifications_registry[self.id] = self
209 | return True
210 |
211 | def update(self, summary, message="", icon=None):
212 | """Replace the summary and body of the notification, and optionally its
213 | icon. You should call show() again after this to display the updated
214 | notification.
215 | """
216 | self.summary = summary
217 | self.message = message
218 | if icon is not None:
219 | self.icon = icon
220 |
221 | def close(self):
222 | """Ask the server to close this notification.
223 | """
224 | if self.id != 0:
225 | dbus_iface.CloseNotification(self.id)
226 |
227 | def set_hint(self, key, value):
228 | """n.set_hint(key, value) <--> n.hints[key] = value
229 |
230 | Only exists for compatibility with pynotify.
231 | """
232 | self.hints[key] = value
233 |
234 | set_hint_string = set_hint_int32 = set_hint_double = set_hint
235 |
236 | def set_hint_byte(self, key, value):
237 | """Set a hint with a dbus byte value. The input value can be an
238 | integer or a bytes string of length 1.
239 | """
240 | self.hints[key] = dbus.Byte(value)
241 |
242 | def set_urgency(self, level):
243 | """Set the urgency level to one of URGENCY_LOW, URGENCY_NORMAL or
244 | URGENCY_CRITICAL.
245 | """
246 | if level not in urgency_levels:
247 | raise ValueError("Unknown urgency level specified", level)
248 | self.set_hint_byte("urgency", level)
249 |
250 | def set_category(self, category):
251 | """Set the 'category' hint for this notification.
252 | """
253 | self.hints['category'] = category
254 |
255 | def set_timeout(self, timeout):
256 | """Set the display duration in milliseconds, or one of the special
257 | values EXPIRES_DEFAULT or EXPIRES_NEVER.
258 |
259 | Only exists for compatibility with pynotify; you can simply set::
260 |
261 | n.timeout = 5000
262 | """
263 | if not isinstance(timeout, int):
264 | raise TypeError("timeout value was not int", timeout)
265 | self.timeout = timeout
266 |
267 | def get_timeout(self):
268 | """Return the timeout value for this notification.
269 |
270 | Only exists for compatibility with pynotify; you can inspect the
271 | timeout attribute directly.
272 | """
273 | return self.timeout
274 |
275 | def add_action(self, action, label, callback, user_data=None):
276 | """Add an action to the notification (if the server supports it).
277 |
278 | action : str
279 | A brief key.
280 | label : str
281 | The text displayed on the action button
282 | callback : callable
283 | A function taking at 2-3 parameters: the Notification object, the
284 | action key and (if specified) the user_data.
285 | user_data :
286 | An extra argument to pass to the callback.
287 | """
288 | self.actions[action] = (label, callback, user_data)
289 |
290 | def _make_actions_array(self):
291 | """Make the actions array to send over DBus.
292 | """
293 | arr = []
294 | for action, (label, callback, user_data) in self.actions.items():
295 | arr.append(action)
296 | arr.append(label)
297 | return arr
298 |
299 | def _action_callback(self, action):
300 | """Called when the user selects an action on the notification, to
301 | dispatch it to the relevant user-specified callback.
302 | """
303 | try:
304 | label, callback, user_data = self.actions[action]
305 | except KeyError:
306 | return
307 |
308 | if user_data is None:
309 | callback(self, action)
310 | else:
311 | callback(self, action, user_data)
312 |
313 | def connect(self, event, callback):
314 | """Set the callback for the notification closing; the only valid value
315 | for event is 'closed'. The API is compatible with pynotify.
316 | """
317 | if event != 'closed':
318 | raise ValueError("'closed' is the only valid value for event", event)
319 | self._closed_callback = callback
320 |
321 | def set_data(self, key, value):
322 | """n.set_data(key, value) <--> n.data[key] = value
323 |
324 | Only exists for compatibility with pynotify.
325 | """
326 | self.data[key] = value
327 |
328 | def get_data(self, key):
329 | """n.get_data(key) <--> n.data[key]
330 |
331 | Only exists for compatibility with pynotify.
332 | """
333 | return self.data[key]
334 |
335 | def set_icon_from_pixbuf(self, icon):
336 | """Set a custom icon from a GdkPixbuf.
337 | """
338 | struct = (
339 | icon.get_width(),
340 | icon.get_height(),
341 | icon.get_rowstride(),
342 | icon.get_has_alpha(),
343 | icon.get_bits_per_sample(),
344 | icon.get_n_channels(),
345 | dbus.ByteArray(icon.get_pixels())
346 | )
347 | self.hints['icon_data'] = struct
348 |
349 | def set_location(self, x, y):
350 | """Set the notification location as (x, y), if the server supports it.
351 | """
352 | if (not isinstance(x, int)) or (not isinstance(y, int)):
353 | raise TypeError("x and y must both be ints", (x, y))
354 | self.hints['x'] = x
355 | self.hints['y'] = y
356 |
--------------------------------------------------------------------------------
/tvlinker/progress.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 | from PyQt5.QtCore import pyqtSlot
5 | from PyQt5.QtDBus import QDBusConnection, QDBusMessage
6 | from PyQt5.QtWidgets import qApp, QWidget
7 |
8 |
9 | class TaskbarProgress(QWidget):
10 | def __init__(self, parent=None):
11 | super(TaskbarProgress, self).__init__(parent)
12 | self._desktopfile = 'application://{}.desktop'.format(qApp.applicationName().lower())
13 | self._sessionbus = QDBusConnection.sessionBus()
14 | self.clear()
15 |
16 | @pyqtSlot()
17 | def clear(self):
18 | self.setProgress(0.0, False)
19 |
20 | @pyqtSlot(float, bool)
21 | def setProgress(self, value: float, visible: bool=True):
22 | signal = QDBusMessage.createSignal('/com/canonical/unity/launcherentry/337963624',
23 | 'com.canonical.Unity.LauncherEntry', 'Update')
24 | message = signal << self._desktopfile << {'progress-visible': visible, 'progress': value}
25 | self._sessionbus.send(message)
26 |
--------------------------------------------------------------------------------
/tvlinker/pyload.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 | import json
5 | from urllib.parse import urlencode, urljoin
6 | from urllib.request import urlopen
7 |
8 |
9 | class PyloadConnection:
10 | def __init__(self, host, username, password):
11 | self.url_base = urljoin('%s' % host, 'api/')
12 | self.session = self._call('login', {'username': username, 'password': password}, False)
13 |
14 | def _call(self, name, args={}, encode=True):
15 | url = urljoin(self.url_base, name)
16 | if encode:
17 | data = {}
18 | for key, value in args.items():
19 | data[key] = json.dumps(value)
20 | else:
21 | data = args
22 | if hasattr(self, 'session'):
23 | data['session'] = self.session
24 | post = urlencode(data).encode('utf-8')
25 | return json.loads(urlopen(url, post).read().decode('utf-8'))
26 |
27 | def __getattr__(self, name):
28 | def wrapper(**kargs):
29 | return self._call(name, kargs)
30 |
31 | return wrapper
32 |
--------------------------------------------------------------------------------
/tvlinker/settings.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 | import sys
5 |
6 | from PyQt5.QtCore import pyqtSlot, QSettings, QSize, Qt
7 | from PyQt5.QtGui import QCloseEvent, QIcon, QKeyEvent, QPixmap
8 | from PyQt5.QtWidgets import (QAbstractItemView, QCheckBox, QComboBox, QDialog, QDialogButtonBox, QFormLayout,
9 | QGroupBox, QHBoxLayout, QLabel, QLineEdit, QListWidget, QListWidgetItem, QPushButton,
10 | QSizePolicy, QStackedWidget, QTabWidget, QVBoxLayout, QWidget, qApp)
11 |
12 |
13 | class Settings(QDialog):
14 | def __init__(self, parent, settings: QSettings, f=Qt.WindowCloseButtonHint):
15 | super(Settings, self).__init__(parent, f)
16 | self.parent = parent
17 | self.settings = settings
18 | self.setWindowModality(Qt.ApplicationModal)
19 | self.tab_general = GeneralTab(self.settings)
20 | self.tab_favorites = FavoritesTab(self.settings)
21 | tabs = QTabWidget()
22 | tabs.addTab(self.tab_general, 'General')
23 | tabs.addTab(self.tab_favorites, 'Favorites')
24 | button_box = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel, Qt.Horizontal, self)
25 | button_box.accepted.connect(self.save_settings)
26 | button_box.rejected.connect(self.close)
27 | layout = QVBoxLayout()
28 | layout.addWidget(tabs)
29 | layout.addWidget(button_box)
30 | self.setLayout(layout)
31 | self.setWindowTitle('%s Settings' % qApp.applicationName())
32 | self.setWindowIcon(self.parent.icon_settings)
33 |
34 | def save_settings(self) -> None:
35 | self.tab_general.save()
36 | self.tab_favorites.save()
37 | self.parent.init_settings()
38 | self.close()
39 |
40 | def keyPressEvent(self, event: QKeyEvent) -> None:
41 | if event.key() in (Qt.Key_Enter, Qt.Key_Return):
42 | return
43 | super(Settings, self).keyPressEvent(event)
44 |
45 | def closeEvent(self, event: QCloseEvent) -> None:
46 | self.tab_general.deleteLater()
47 | self.tab_favorites.deleteLater()
48 | self.deleteLater()
49 | event.accept()
50 |
51 |
52 | class GeneralTab(QWidget):
53 | def __init__(self, settings: QSettings):
54 | super(GeneralTab, self).__init__()
55 | self.settings = settings
56 |
57 | realdebrid_logo = QLabel(pixmap=QPixmap(':assets/images/realdebrid.png'))
58 | self.realdebridtoken_lineEdit = QLineEdit(self, text=self.settings.value('realdebrid_apitoken'))
59 | self.realdebridtoken_lineEdit.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed)
60 | self.realdebridtoken_lineEdit.setMinimumWidth(250)
61 | apitoken_link = 'get your API token here'
63 | realdebrid_apitoken_link = QLabel(text=apitoken_link, textFormat=Qt.RichText, openExternalLinks=True)
64 | realdebrid_formLayout = QFormLayout(labelAlignment=Qt.AlignRight)
65 | realdebrid_formLayout.addRow('API Token:', self.realdebridtoken_lineEdit)
66 | if len(self.realdebridtoken_lineEdit.text()):
67 | self.realdebridproxy_checkBox = QCheckBox('Use ShadowSocks proxy for API connections', self)
68 | self.realdebridproxy_checkBox.setChecked(self.settings.value('realdebrid_apiproxy', False, bool))
69 | self.realdebridproxy_checkBox.setCursor(Qt.PointingHandCursor)
70 | realdebrid_formLayout.addRow('', self.realdebridproxy_checkBox)
71 | else:
72 | realdebrid_formLayout.addRow('', realdebrid_apitoken_link)
73 | realdebrid_layout = QHBoxLayout()
74 | realdebrid_layout.addWidget(realdebrid_logo)
75 | realdebrid_layout.addSpacing(15)
76 | realdebrid_layout.addLayout(realdebrid_formLayout)
77 | realdebrid_group = QGroupBox()
78 | realdebrid_group.setLayout(realdebrid_layout)
79 |
80 | self.dlmanager_comboBox = QComboBox(self, editable=False, cursor=Qt.PointingHandCursor)
81 | self.dlmanager_comboBox.setAutoFillBackground(True)
82 | self.dlmanager_comboBox.addItems(('built-in', 'aria2'))
83 | if sys.platform == 'win32':
84 | self.dlmanager_comboBox.addItem('IDM')
85 | if sys.platform.startswith('linux'):
86 | self.dlmanager_comboBox.addItem('KGet')
87 | self.dlmanager_comboBox.addItems(('Persepolis', 'pyLoad'))
88 | self.dlmanager_comboBox.setCurrentIndex(self.dlmanager_comboBox.findText(
89 | str(self.settings.value('download_manager')), Qt.MatchFixedString))
90 |
91 | # self.dlpagecount_comboBox = QComboBox(self, toolTip='Default Page Count', editable=False,
92 | # cursor=Qt.PointingHandCursor)
93 | # self.dlpagecount_comboBox.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
94 | # self.dlpagecount_comboBox.addItems(('10', '20', '30', '40', '50'))
95 | # self.dlpagecount_comboBox.setCurrentIndex(self.dlpagecount_comboBox.findText(
96 | # str(self.settings.value('dl_pagecount')), Qt.MatchFixedString))
97 |
98 | minchars = 20
99 |
100 | self.dlmanager_comboBox.setMinimumContentsLength(minchars)
101 | # self.dlpagecount_comboBox.setMinimumContentsLength(minchars)
102 |
103 | general_formlayout = QFormLayout(labelAlignment=Qt.AlignRight)
104 | general_formlayout.addRow('Download Manager:', self.dlmanager_comboBox)
105 | general_layout = QHBoxLayout()
106 | general_layout.addStretch(1)
107 | general_layout.addLayout(general_formlayout)
108 | general_layout.addStretch(1)
109 |
110 | directdl_label = QLabel('No settings for built-in downloader')
111 | directdl_label.setStyleSheet('font-weight:300; text-align:center;')
112 | directdl_label.setAlignment(Qt.AlignCenter)
113 |
114 | self.aria2rpchost_lineEdit = QLineEdit(self, text=self.settings.value('aria2_rpc_host'))
115 | self.aria2rpchost_lineEdit.setPlaceholderText('http://localhost')
116 | self.aria2rpchost_lineEdit.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
117 | self.aria2rpcport_lineEdit = QLineEdit(self, text=self.settings.value('aria2_rpc_port'))
118 | self.aria2rpcport_lineEdit.setPlaceholderText('6800')
119 | self.aria2rpcport_lineEdit.setFixedWidth(100)
120 | self.aria2rpcsecret_lineEdit = QLineEdit(self, text=self.settings.value('aria2_rpc_secret'))
121 | self.aria2rpcsecret_lineEdit.setFixedWidth(100)
122 | self.aria2rpcsecret_lineEdit.setEchoMode(QLineEdit.PasswordEchoOnEdit)
123 | self.aria2rpcuser_lineEdit = QLineEdit(self, text=self.settings.value('aria2_rpc_username'))
124 | self.aria2rpcuser_lineEdit.setFixedWidth(150)
125 | self.aria2rpcpass_lineEdit = QLineEdit(self, text=self.settings.value('aria2_rpc_password'))
126 | self.aria2rpcpass_lineEdit.setFixedWidth(150)
127 | self.aria2rpcpass_lineEdit.setEchoMode(QLineEdit.PasswordEchoOnEdit)
128 | aria2_formLayout = QFormLayout(labelAlignment=Qt.AlignRight)
129 | aria2_formLayout.addRow('RPC Host:', self.aria2rpchost_lineEdit)
130 | aria2_formLayout_left = QFormLayout(labelAlignment=Qt.AlignRight)
131 | aria2_formLayout_left.addRow('RPC Port:', self.aria2rpcport_lineEdit)
132 | aria2_formLayout_left.addRow('RPC Secret:', self.aria2rpcsecret_lineEdit)
133 | aria2_formLayout_right = QFormLayout(labelAlignment=Qt.AlignRight)
134 | aria2_formLayout_right.addRow('RPC Username:', self.aria2rpcuser_lineEdit)
135 | aria2_formLayout_right.addRow('RPC Password:', self.aria2rpcpass_lineEdit)
136 | aria2_formLayout_hbox = QHBoxLayout()
137 | aria2_formLayout_hbox.addStretch(1)
138 | aria2_formLayout_hbox.addLayout(aria2_formLayout_left)
139 | aria2_formLayout_hbox.addStretch(1)
140 | aria2_formLayout_hbox.addLayout(aria2_formLayout_right)
141 | aria2_formLayout_hbox.addStretch(1)
142 | aria2_formLayout.addRow(aria2_formLayout_hbox)
143 | aria2_settings = QWidget(self)
144 | aria2_settings.setLayout(aria2_formLayout)
145 |
146 | self.dlmanagersettings_stack = QStackedWidget()
147 | self.dlmanagersettings_stack.addWidget(directdl_label)
148 | self.dlmanagersettings_stack.addWidget(aria2_settings)
149 |
150 | self.persepoliscmd_lineEdit = QLineEdit(self, text=self.settings.value('persepolis_cmd'))
151 | self.persepoliscmd_lineEdit.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
152 | persepolis_formLayout = QFormLayout(labelAlignment=Qt.AlignRight)
153 | persepolis_formLayout.addRow('persepolis command:', self.persepoliscmd_lineEdit)
154 | persepolis_settings = QWidget(self)
155 | persepolis_settings.setLayout(persepolis_formLayout)
156 |
157 | self.pyloadhost_lineEdit = QLineEdit(self, text=self.settings.value('pyload_host'))
158 | self.pyloadhost_lineEdit.setPlaceholderText('http://localhost:8000')
159 | self.pyloadhost_lineEdit.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
160 | self.pyloaduser_lineEdit = QLineEdit(self, text=self.settings.value('pyload_username'))
161 | self.pyloaduser_lineEdit.setFixedWidth(200)
162 | self.pyloadpass_lineEdit = QLineEdit(self, text=self.settings.value('pyload_password'))
163 | self.pyloadpass_lineEdit.setFixedWidth(200)
164 | pyload_formLayout = QFormLayout(labelAlignment=Qt.AlignRight)
165 | pyload_formLayout.addRow('pyLoad Host:', self.pyloadhost_lineEdit)
166 | pyload_formLayout.addRow('pyLoad Username:', self.pyloaduser_lineEdit)
167 | pyload_formLayout.addRow('pyLoad Password:', self.pyloadpass_lineEdit)
168 | pyload_settings = QWidget(self)
169 | pyload_settings.setLayout(pyload_formLayout)
170 |
171 | if sys.platform == 'win32':
172 | self.idmexepath_lineEdit = QLineEdit(self, text=self.settings.value('idm_exe_path'))
173 | self.idmexepath_lineEdit.setPlaceholderText('C:\Program Files (x86)\Internet Download Manager\IDMan.exe')
174 | self.idmexepath_lineEdit.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
175 | idm_formLayout = QFormLayout(labelAlignment=Qt.AlignRight)
176 | idm_formLayout.addRow('IDM EXE Path:', self.idmexepath_lineEdit)
177 | idm_settings = QWidget(self)
178 | idm_settings.setLayout(idm_formLayout)
179 | self.dlmanagersettings_stack.addWidget(idm_settings)
180 |
181 | if sys.platform.startswith('linux'):
182 | self.kgetcmd_lineEdit = QLineEdit(self, text=self.settings.value('kget_cmd'))
183 | self.kgetcmd_lineEdit.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
184 | kget_formLayout = QFormLayout(labelAlignment=Qt.AlignRight)
185 | kget_formLayout.addRow('kget command:', self.kgetcmd_lineEdit)
186 | kget_settings = QWidget(self)
187 | kget_settings.setLayout(kget_formLayout)
188 | self.dlmanagersettings_stack.addWidget(kget_settings)
189 |
190 | self.dlmanagersettings_stack.addWidget(persepolis_settings)
191 | self.dlmanagersettings_stack.addWidget(pyload_settings)
192 |
193 | self.dlmanagersettings_stack.setCurrentIndex(self.dlmanager_comboBox.currentIndex())
194 | self.dlmanager_comboBox.currentIndexChanged.connect(self.dlmanagersettings_stack.setCurrentIndex)
195 |
196 | general_formlayout.addRow(self.dlmanagersettings_stack)
197 | general_group = QGroupBox()
198 | general_group.setLayout(general_layout)
199 |
200 | tab_layout = QVBoxLayout()
201 | tab_layout.addStretch(1)
202 | tab_layout.addWidget(realdebrid_group)
203 | tab_layout.addWidget(general_group)
204 | tab_layout.addStretch(1)
205 |
206 | self.setLayout(tab_layout)
207 |
208 | def save(self) -> None:
209 | # self.settings.setValue('dl_pagecount', self.dlpagecount_comboBox.currentText())
210 | self.settings.setValue('realdebrid_apitoken', self.realdebridtoken_lineEdit.text())
211 | if hasattr(self, 'realdebridproxy_checkBox'):
212 | self.settings.setValue('realdebrid_apiproxy', self.realdebridproxy_checkBox.isChecked())
213 | self.settings.setValue('download_manager', self.dlmanager_comboBox.currentText().lower())
214 | if self.dlmanager_comboBox.currentText() == 'aria2':
215 | self.settings.setValue('aria2_rpc_host', self.aria2rpchost_lineEdit.text())
216 | self.settings.setValue('aria2_rpc_port', self.aria2rpcport_lineEdit.text())
217 | self.settings.setValue('aria2_rpc_secret', self.aria2rpcsecret_lineEdit.text())
218 | self.settings.setValue('aria2_rpc_username', self.aria2rpcuser_lineEdit.text())
219 | self.settings.setValue('aria2_rpc_password', self.aria2rpcpass_lineEdit.text())
220 | elif self.dlmanager_comboBox.currentText() == 'pyLoad':
221 | self.settings.setValue('pyload_host', self.pyloadhost_lineEdit.text())
222 | self.settings.setValue('pyload_username', self.pyloaduser_lineEdit.text())
223 | self.settings.setValue('pyload_password', self.pyloadpass_lineEdit.text())
224 | elif self.dlmanager_comboBox.currentText() == 'IDM':
225 | self.settings.setValue('idm_exe_path', self.idmexepath_lineEdit.text())
226 | elif self.dlmanager_comboBox.currentText() == 'KGet':
227 | self.settings.setValue('kget_cmd', self.kgetcmd_lineEdit.text())
228 | elif self.dlmanager_comboBox.currentText() == 'Persepolis':
229 | self.settings.setValue('persepolis_cmd', self.persepoliscmd_lineEdit.text())
230 |
231 |
232 | class FavoritesTab(QWidget):
233 | def __init__(self, settings: QSettings):
234 | super(FavoritesTab, self).__init__()
235 | self.settings = settings
236 | faves_formLayout = QFormLayout(labelAlignment=Qt.AlignRight)
237 | self.faves_lineEdit = QLineEdit(self)
238 | self.faves_lineEdit.returnPressed.connect(self.add_item)
239 | self.faves_lineEdit.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
240 | faves_addItemButton = QPushButton(parent=self, flat=False, cursor=Qt.PointingHandCursor, text='Add',
241 | icon=QIcon(':assets/images/plus.png'), toolTip='Add item',
242 | clicked=self.add_item)
243 | faves_addItemButton.setIconSize(QSize(12, 12))
244 | faves_deleteItemButton = QPushButton(parent=self, flat=False, cursor=Qt.PointingHandCursor, text='Delete',
245 | icon=QIcon(':assets/images/minus.png'), toolTip='Delete selected item',
246 | clicked=self.delete_items)
247 | faves_deleteItemButton.setIconSize(QSize(12, 12))
248 | faves_buttonLayout = QHBoxLayout()
249 | faves_buttonLayout.addWidget(faves_addItemButton)
250 | faves_buttonLayout.addWidget(faves_deleteItemButton)
251 | faves_formLayout.addRow('Item Label:', self.faves_lineEdit)
252 | faves_formLayout.addRow(faves_buttonLayout)
253 | faves_formLayout.addRow(self.get_notes())
254 | self.faves_listWidget = QListWidget(self)
255 | self.faves_listWidget.setSelectionMode(QAbstractItemView.ExtendedSelection)
256 | self.faves_listWidget.setSortingEnabled(True)
257 | self.add_items(self.settings.value('favorites', ''))
258 |
259 | tab_layout = QHBoxLayout()
260 | tab_layout.addLayout(faves_formLayout)
261 | tab_layout.addWidget(self.faves_listWidget)
262 |
263 | self.setLayout(tab_layout)
264 |
265 | def add_items(self, items: list) -> None:
266 | for item in items:
267 | listitem = QListWidgetItem(item, self.faves_listWidget)
268 | listitem.setFlags(listitem.flags() | Qt.ItemIsEditable)
269 | self.faves_listWidget.sortItems(Qt.AscendingOrder)
270 |
271 | @pyqtSlot()
272 | def delete_items(self) -> None:
273 | for item in self.faves_listWidget.selectedItems():
274 | deleted_item = self.faves_listWidget.takeItem(self.faves_listWidget.row(item))
275 | del deleted_item
276 |
277 | @pyqtSlot()
278 | def add_item(self) -> None:
279 | if len(self.faves_lineEdit.text()):
280 | item = QListWidgetItem(self.faves_lineEdit.text(), self.faves_listWidget)
281 | item.setFlags(item.flags() | Qt.ItemIsEditable)
282 | self.faves_listWidget.sortItems(order=Qt.AscendingOrder)
283 | self.faves_lineEdit.clear()
284 |
285 | def get_notes(self) -> QLabel:
286 | content = QLabel()
287 | content.setStyleSheet('margin:10px; border:1px solid #BABABA; padding:10px; color:#666;')
288 | content.setTextFormat(Qt.RichText)
289 | content.setWordWrap(True)
290 | content.setText('''Labels from this list will be used to filter links. Filtering is NOT case-sensitive.
291 |
Example:
the simpsons
292 | south park''')
293 | return content
294 |
295 | def save(self) -> None:
296 | if self.faves_listWidget.count():
297 | faves = []
298 | for row in range(0, self.faves_listWidget.count()):
299 | faves.append(self.faves_listWidget.item(row).text())
300 | self.settings.setValue('favorites', faves)
301 |
--------------------------------------------------------------------------------
/tvlinker/threads.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 |
4 | import os
5 | import sys
6 | import time
7 |
8 | from datetime import datetime, timedelta
9 | from tzlocal import get_localzone
10 |
11 | import pytz
12 | import requests
13 |
14 | from PyQt5.QtCore import QObject, QSettings, QThread, pyqtSignal, pyqtSlot
15 | from PyQt5.QtWidgets import QMessageBox, qApp
16 | from bs4 import BeautifulSoup
17 | from requests.exceptions import HTTPError
18 |
19 | import cloudscraper
20 |
21 | from tvlinker.filesize import alternative, size
22 |
23 | try:
24 | # noinspection PyPackageRequirements
25 | import simplejson as json
26 | except ImportError:
27 | import json
28 |
29 |
30 | class ShadowSocks:
31 | config = {
32 | 'ssocks': {
33 | 'procs': ['ss-qt5', 'sslocal'],
34 | 'proxies': {
35 | 'http': 'socks5://127.0.0.1:1080',
36 | 'https': 'socks5://127.0.0.1:1080'
37 | },
38 | },
39 | 'v2ray': {
40 | 'procs': ['v2ray'],
41 | 'proxies': {
42 | 'http': 'socks5://127.0.0.1:10808',
43 | 'https': 'socks5://127.0.0.1:10808'
44 | }
45 | }
46 | }
47 |
48 | @staticmethod
49 | def detect() -> str:
50 | if sys.platform.startswith('linux'):
51 | ptypes = ShadowSocks.config.keys()
52 | ps = os.popen('ps -Af').read()
53 | for ptype in ptypes:
54 | procs = ShadowSocks.config[ptype]['procs']
55 | for p in procs:
56 | if ps.count(p):
57 | return ptype
58 | return None
59 |
60 | @staticmethod
61 | def proxies() -> dict:
62 | proxy_type = ShadowSocks.detect()
63 | return ShadowSocks.config[proxy_type]['proxies'] if proxy_type is not None else {}
64 |
65 |
66 | class ScrapeWorker(QObject):
67 | addRow = pyqtSignal(list)
68 | workFinished = pyqtSignal()
69 |
70 | def __init__(self, source_url: str, useragent: str, maxpages: int):
71 | super(ScrapeWorker, self).__init__()
72 | self.maxpages = maxpages
73 | self.source_url = source_url
74 | self.user_agent = useragent
75 | self.scraper = cloudscraper.create_scraper()
76 | self.scraper.proxies = ShadowSocks.proxies()
77 | self.tz_format = '%b %d %Y %H:%M'
78 | self.tz_local = get_localzone()
79 | self.complete = False
80 |
81 | def scrape(self, pagenum: int) -> None:
82 | try:
83 | url = self.source_url.format(pagenum + 1)
84 | req = self.scraper.get(url)
85 | bs = BeautifulSoup(req.text, 'lxml')
86 | posts = bs('div', class_='post')
87 | for post in posts:
88 | dt_utc = datetime.strptime(post.find('div', class_='p-c p-c-time').get_text().strip(), self.tz_format)
89 | # TODO: fix hardcoded DST adjustment
90 | dt_local = dt_utc.replace(tzinfo=pytz.utc).astimezone(self.tz_local) - timedelta(hours=2)
91 | dlsize = post.find('h2').get_text().strip()
92 | table_row = [
93 | dt_local.strftime(self.tz_format),
94 | post.find('a', class_='p-title').get('href').strip(),
95 | post.find('a', class_='p-title').get_text().strip(),
96 | dlsize[dlsize.rfind('(') + 1:len(dlsize) - 1]
97 | ]
98 | self.addRow.emit(table_row)
99 | except HTTPError:
100 | sys.stderr.write(sys.exc_info()[0])
101 | # noinspection PyTypeChecker
102 | QMessageBox.critical(None, 'ERROR NOTIFICATION', sys.exc_info()[0])
103 | # self.exit()
104 |
105 | @pyqtSlot()
106 | def begin(self):
107 | for page in range(self.maxpages):
108 | if QThread.currentThread().isInterruptionRequested():
109 | return
110 | self.scrape(page)
111 | self.complete = True
112 | self.workFinished.emit()
113 |
114 |
115 | class HostersThread(QThread):
116 | setHosters = pyqtSignal(list)
117 | noLinks = pyqtSignal()
118 |
119 | def __init__(self, link_url: str, useragent: str):
120 | QThread.__init__(self)
121 | self.link_url = link_url
122 | self.user_agent = useragent
123 | self.scraper = cloudscraper.create_scraper()
124 | self.scraper.proxies = ShadowSocks.proxies()
125 |
126 | def __del__(self) -> None:
127 | self.wait()
128 |
129 | def get_hoster_links(self) -> None:
130 | try:
131 | req = self.scraper.get(self.link_url)
132 | bs = BeautifulSoup(req.text, 'lxml')
133 | links = bs.select('div.post h2[style="text-align: center;"]')
134 | self.setHosters.emit(links)
135 | except HTTPError:
136 | print(sys.exc_info()[0])
137 | # noinspection PyTypeChecker
138 | QMessageBox.critical(None, 'ERROR NOTIFICATION', sys.exc_info()[0])
139 | QThread.currentThread().quit()
140 | except IndexError:
141 | self.noLinks.emit()
142 | QThread.currentThread().quit()
143 |
144 | def run(self) -> None:
145 | self.get_hoster_links()
146 |
147 |
148 | class RealDebridThread(QThread):
149 | unrestrictedLink = pyqtSignal(str)
150 | supportedHosts = pyqtSignal(dict)
151 | hostStatus = pyqtSignal(dict)
152 | errorMsg = pyqtSignal(list)
153 |
154 | class RealDebridAction:
155 | UNRESTRICT_LINK = 0,
156 | SUPPORTED_HOSTS = 1,
157 | HOST_STATUS = 2
158 |
159 | def __init__(self,
160 | settings: QSettings,
161 | api_url: str,
162 | link_url: str,
163 | action: RealDebridAction = RealDebridAction.UNRESTRICT_LINK,
164 | check_host: str = None):
165 | QThread.__init__(self)
166 | self.api_url = api_url
167 | self.api_token = settings.value('realdebrid_apitoken')
168 | self.api_proxy = settings.value('realdebrid_apiproxy', False, bool)
169 | self.link_url = link_url
170 | self.action = action
171 | self.check_host = check_host
172 | self.proxies = ShadowSocks.proxies() if self.api_proxy else {}
173 |
174 | def __del__(self):
175 | self.wait()
176 |
177 | def post(self, endpoint: str, payload: object = None) -> dict:
178 | try:
179 | res = requests.post('{0}{1}?auth_token={2}'.format(self.api_url, endpoint, self.api_token),
180 | data=payload, proxies=self.proxies)
181 | return res.json()
182 | except HTTPError:
183 | print(sys.exc_info())
184 | self.errorMsg.emit([
185 | 'ERROR NOTIFICATION',
186 | '