Ejemplo n.º 1
0
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)
Ejemplo n.º 2
0
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)