class Network(): """ Controls data requests. """ def __init__(self): self.log = LoggerManager().get_logger("Network") self.stopping_thread = False def stop_thread(self): """ Data requests can't be stopped immediatelly, so this method warns the Network instance to stop as soon as possible. """ self.stopping_thread = True def get_data(self, url, is_binary=False): """ Requests data, be it the html/rss of a site or a torrent file. :type url: str :param url: Link to the data to be downloaded. :type is_binary: bool :param is_binary: If the data is binary (torrent files) or not. :rtype: str :return: the data requested """ if url == "": self.log.error("No link has been received (aka empty URL)!") return None resp = "" if url.find("://") == -1: url = "http://" + url self.log.debug("link: %s" % url) timeout = True tries = 0 while timeout and not self.stopping_thread: try: if url.startswith("https"): response = requests.get(url, timeout=10, verify=constant.CACERT_PATH) else: response = requests.get(url, timeout=10) timeout = False if is_binary: resp = response.content else: resp = response.text #resp = HTMLParser().unescape(response.text) except (ReadTimeout, ConnectTimeout): tries += 1 if tries == 3: self.log.warning( "Retried 3 times... Will try again later.") return "" self.log.debug("Retrying (%i)" % tries) except Exception as error: self.log.print_traceback(error, self.log.error) return resp def download_torrent(self, url, file_name, torrent_path, anime_folder): """ Requests download of the torrent file, saves it, and opens it with torrent application. :type url: str :param url: Link to the torrent file. :type file_name: str :param file_name: The name with which the torrent file will be saved. :type torrent_path: str :param torrent_path: The folder where the torrent file will be saved. :type anime_folder: str :param anime_folder: The folder where the torrent application should save the episode (uTorrent only). :rtype: bool :return: if the torrent was sent to the torrent application or not. """ if os.path.isdir(torrent_path): try: data = self.get_data(url, is_binary=True) if self.__is_torrent(data): self.__save_torrent(data, file_name, torrent_path, anime_folder) return True return False except Exception as error: raise error else: self.log.error( "The .torrent was NOT downloaded (does the specified folder (%s) exists?)" % torrent_path) return False @staticmethod def __is_torrent(file_data): """ Makes sure the file downloaded is indeed a ".torrent". :param file_data: The file. :rtype: bool :return: If it's a torrent file or not. """ # seems like torrent files start with this. So yeah, it's POG, but for now it's working ^^ return str(file_data).find("d8:announce") == 0 def __save_torrent(self, data, file_name, torrent_path, anime_folder=""): """ Saves the torrent file and opens it with the torrent application selected. :param data: The file data. :type file_name: str or unicode :param file_name: The name with which the torrent file will be saved. :type torrent_path: str :param torrent_path: The folder where the torrent file will be saved. :type anime_folder: str :param anime_folder: The folder where the torrent application should save the episode (uTorrent only). """ if os.path.isdir(torrent_path): title = strings.remove_special_chars(file_name) torrent_file_path = "%s\\%s.torrent" % (torrent_path, title) with open(torrent_file_path, "wb") as torrent: torrent.write(data) self.log.info(".torrent saved") try: application_fullpath = torrent_application.fullpath() self.log.debug("Opening '%s' with '%s'" % (torrent_file_path, application_fullpath)) if torrent_application.is_utorrent() and os.path.isdir( anime_folder): self.log.debug("Using uTorrent, save in folder '%s'" % strings.escape_unicode(anime_folder)) params = ['/DIRECTORY', anime_folder, torrent_file_path] else: params = [torrent_file_path] # http://stackoverflow.com/questions/1910275/unicode-filenames-on-windows-with-python-subprocess-popen # TLDR: Python 2.X's subprocess.Popen doesn't work well with unicode QtCore.QProcess().startDetached(application_fullpath, params) self.log.info(".torrent opened") except Exception as error: self.log.error("Error opening torrent application") self.log.print_traceback(error, self.log.error) raise error else: self.log.error( "The .torrent was NOT saved. Apparently the specified folder (%s) does NOT exist." % torrent_path)
class Main(QtCore.QObject): """ Main class, instantiated when the application starts. Creates main window/system tray icon, and stops the user from opnening more than one instance of the application. It also starts/stops the downloader when the user requests or during auto-start on Windows startup. """ def __init__(self): QtCore.QObject.__init__(self) # Make sure the required folders/files exist if not os.path.isdir(constant.DATA_PATH): os.makedirs(constant.DATA_PATH) self.log = LoggerManager().get_logger("MAIN") try: if not os.path.isfile(constant.DB_PATH): shutil.copyfile("dbTemplate.db", constant.DB_PATH) except (shutil.Error, IOError) as error: self.log.print_traceback(error, self.log.critical) sys.exit(1) try: if not os.path.isdir(constant.DEFAULT_TORRENTS_PATH): os.makedirs(constant.DEFAULT_TORRENTS_PATH) except Exception as error: self.log.print_traceback(error, self.log.critical) sys.exit(1) self.app = QtSingleApplication(constant.GUID, sys.argv) self.log.info("---STARTING APPLICATION---") if self.app.isRunning(): self.log.warning( "---The launch of another instance of this application will be cancelled---" ) self.app.sendMessage() sys.exit(0) self.app.messageReceived.connect(self.another_instance_opened) self.app.setQuitOnLastWindowClosed(False) self.window = None self.tray_icon = None self.thread = None self.downloader = None self.timer = None self.downloader_is_running = False self.downloader_is_restarting = False self.downloader_is_stopping = False try: self.window = WindowMain(self) self.tray_icon = SystemTrayIcon(self, self.window) self.tray_icon.show() show_gui = "-nogui" not in sys.argv if show_gui: if self.downloader_is_running: self.window.downloader_started() else: self.window.downloader_stopped() self.window.show() elif not self.window.is_visible(): self.log.info("STARTING DOWNLOADER") self.start_downloader() self.app.exec_() except Exception as unforeseenError: self.log.critical("UNFORESEEN ERROR") self.log.print_traceback(unforeseenError, self.log.critical) if self.tray_icon is not None: self.show_tray_message("Unforeseen error occurred...") exit() def quit(self): """ Finishes the application gracefully - at least tries to, teehee (^_^;) """ if self.tray_icon is not None: self.tray_icon.hide() self.tray_icon.deleteLater() if self.timer is not None: self.timer.stop() if self.thread is not None and self.thread.isRunning(): self.stop_downloader() #self.app.closeAllWindows() self.app.quit() def another_instance_opened(self, _): """ Called when the user tries to open another instance of the application. Instead of allowing it, will open the current one to avoid any errors. :type _: QtCore.QString :param _: message received, see class QtSingleApplication below. """ self.window.show() def start_downloader(self): """ Starts the downloader in a thread. """ # Don't know how to reproduce, but in some really rare cases the downloader might start without the user requesting it. # These logs try to collect information that might help pinpoint what causes that. # Actually, it's been so long since the last time this error was observed that I don't know if it still happens # or if whatever caused it was fixed... self.log.debug("stack ([1][3]):") i = 0 for item in inspect.stack(): self.log.debug("[" + str(i) + "]= " + str(item)) i += 1 self.log.debug("downloader_is_running: " + str(self.downloader_is_running)) self.log.debug("downloader_is_restarting: " + str(self.downloader_is_restarting)) self.log.debug("downloader_is_stopping: " + str(self.downloader_is_stopping)) if not self.downloader_is_stopping: if self.downloader_is_restarting: self.log.info("RESTARTING DOWNLOADER THREAD") self.downloader_is_restarting = False else: self.log.info("STARTING DOWNLOADER THREAD") self.window.downloader_starting() self.thread = QtCore.QThread(self) self.downloader = Downloader() self.downloader.moveToThread(self.thread) self.downloader.running.connect(self.downloader_started) self.downloader.finish.connect(self.thread.quit) self.downloader.restart.connect(self.restart_downloader) self.downloader.showMessage.connect(self.show_tray_message) self.downloader.update_ui.connect(self.update_ui) # noinspection PyUnresolvedReferences self.thread.started.connect( self.downloader.execute_once ) # PyCharm doesn't recognize started.connect()... # noinspection PyUnresolvedReferences self.thread.finished.connect( self.downloader_stopped ) # PyCharm doesn't recognize finished.connect()... self.thread.start() else: self.downloader_is_stopping = False self.downloader_is_restarting = False def stop_downloader(self): """ Stops the downloader (¬_¬) """ self.log.info("TERMINATING DOWNLOADER THREAD") self.window.downloader_stopping() self.downloader_is_stopping = True self.downloader_is_restarting = False if self.thread.isRunning(): self.downloader.stop_thread() thread_stopped_gracefully = self.thread.wait(300) if self.thread.isRunning(): thread_stopped_gracefully = self.thread.quit() self.log.info("THREAD STOPPED CORRECTLY: %s" % thread_stopped_gracefully) if not thread_stopped_gracefully: self.thread.terminate() else: self.downloader_stopped() try: self.timer.stop() except AttributeError: pass # Happens when the downloader is interrupted before being able to fully execute at least once. def restart_downloader(self): """ Finishes the current downloader thread and starts a timer. When the timer times out a new downloader thread is created. """ self.downloader_is_restarting = True self.thread.quit() self.log.info("THREAD FINISHED CORRECTLY: %s" % self.thread.wait(300)) self.timer = QtCore.QTimer() # noinspection PyUnresolvedReferences self.timer.timeout.connect( self.start_downloader ) # PyCharm doesn't recognize timeout.connect()... self.timer.setSingleShot(True) self.timer.start(db.DBManager().get_config().sleep_time * 1000) @QtCore.pyqtSlot() def downloader_started(self): """ Downloader thread started correctly; notifies the user. """ self.downloader_is_running = True self.window.downloader_started() @QtCore.pyqtSlot() def downloader_stopped(self): """ Downloader thread stopped correctly; notifies the user. """ if not self.downloader_is_restarting: self.downloader_is_running = False self.downloader_is_stopping = False self.downloader_is_restarting = False self.window.downloader_stopped() @QtCore.pyqtSlot(str) def show_tray_message(self, message): """ Uses the system tray icon to notify the user about something. :type message: str :param message: Message to be shown to the user. """ # TODO: Would it be better if this were moved to manager.system_tray_icon? self.tray_icon.showMessage(constant.TRAY_MESSAGE_TITLE, message, QtGui.QSystemTrayIcon.Information, 5000) @QtCore.pyqtSlot(str) def update_ui(self, message): """ Updates the anime table in the main window. Also, shows a message to the user using the system tray icon. :type message: str :param message: Message to be shown to the user. """ if self.window is not None: self.window.update_anime_table() self.show_tray_message(message)