Persepolis of open source code learning [2]

 

1. Download interface

 

1. Download and display on the main interface


MainWindow first initializes aria and starts aria2. The startup method is in download.py.

 # start aria2
        start_aria = StartAria2Thread()
        self.threadPool.append(start_aria)
        self.threadPool[0].start()
        self.threadPool[0].ARIA2RESPONDSIGNAL.connect(self.startAriaMessage)

Then define the add download link interface, there can be multiple AddLinkWindow, all stored in self.addlinkwindows_list.

There are three parameters (self, self.callBack, self.persepolis_setting) when calling. The download and add interface passes the parameters to the callBack function of the main interface through the callback function. After callBack obtains the download information, it is added to the thread pool.

new_download = DownloadLink(gid, self)
            self.threadPool.append(new_download)
            self.threadPool[len(self.threadPool) - 1].start()
            self.threadPool[len(self.threadPool) -
                            1].ARIA2NOTRESPOND.connect(self.aria2NotRespond)

Pay attention to the addLinkSpiderCallBack function of the main interface, the function call sequence is:

1. Download and add interface to obtain linkLineChanged information

2. Download the add interface and start the thread AddLinkSpiderThread to try to get the link file size, and add the thread to the main interface thread pool through the parent. And connect the signal of AddLinkSpiderThread to the addLinkSpiderCallBack function of the main thread, and add the pointer child of the download and add interface to the parameters of the slot function, so that the main interface can access the download and add interface through child.

self.parent.threadPool[len(self.parent.threadPool) - 1].ADDLINKSPIDERSIGNAL.connect(
                partial(self.parent.addLinkSpiderCallBack, child=self))

3. The AddLinkSpiderThread thread sends the result ADDLINKSPIDERSIGNAL signal to the main interface addLinkSpiderCallBack function. Note that when transmitting here, there are only dict parameters and two parameters when connecting.

self.ADDLINKSPIDERSIGNAL.emit(spider_dict)

4. The main interface addLinkSpiderCallBack function calls the download and add interface through the child to set the file name and size display.

In this way, the download link interface adds a thread to the main interface, and then the main interface thread is executed to control the update of the sub-interface. Why doesn't the download link adding interface start a thread to obtain the file size, and then change the download link interface according to the obtained result?

 

mainwindow.py:

class DownloadLink(QThread):
    ARIA2NOTRESPOND = pyqtSignal()

    def __init__(self, gid, parent):
        QThread.__init__(self)
        self.gid = gid
        self.parent = parent

    def run(self):
        # add gid of download to the active gids in temp_db
        # or update data base , if it was existed before
        try:
            self.parent.temp_db.insertInSingleTable(self.gid)
        except:
            # release lock
            self.parent.temp_db.lock = False
            dictionary = {'gid': self.gid, 'status': 'active'}
            self.parent.temp_db.updateSingleTable(dictionary)

        # if request is not successful then persepolis is checking rpc
        # connection with download.aria2Version() function
        answer = download.downloadAria(self.gid, self.parent)
        if answer == False:
            version_answer = download.aria2Version()

            if version_answer == 'did not respond':
                self.ARIA2NOTRESPOND.emit()


class MainWindow(MainWindow_Ui):
    def __init__(self, start_in_tray, persepolis_main, persepolis_setting):
        super().__init__(persepolis_setting)
        self.persepolis_setting = persepolis_setting
        self.persepolis_main = persepolis_main


        # list of threads
        self.threadPool = []

        # start aria2
        start_aria = StartAria2Thread()
        self.threadPool.append(start_aria)
        self.threadPool[0].start()
        self.threadPool[0].ARIA2RESPONDSIGNAL.connect(self.startAriaMessage)


    def addLinkButtonPressed(self, button=None):
         addlinkwindow = AddLinkWindow(self, self.callBack, self.persepolis_setting)
         self.addlinkwindows_list.append(addlinkwindow)
         self.addlinkwindows_list[len(self.addlinkwindows_list) - 1].show()

    # callback of AddLinkWindow
    def callBack(self, add_link_dictionary, download_later, category):
        # write information in data_base
        self.persepolis_db.insertInDownloadTable([dict])
        self.persepolis_db.insertInAddLinkTable([add_link_dictionary])

        # if user didn't press download_later_pushButton in add_link window
        # then create new qthread for new download!
        if not(download_later):
            new_download = DownloadLink(gid, self)
            self.threadPool.append(new_download)
            self.threadPool[len(self.threadPool) - 1].start()
            self.threadPool[len(self.threadPool) -
                            1].ARIA2NOTRESPOND.connect(self.aria2NotRespond)

            # open progress window for download.
            self.progressBarOpen(gid)

            # notify user
            # check that download scheduled or not
            if not(add_link_dictionary['start_time']):
                message = QCoreApplication.translate("mainwindow_src_ui_tr", "Download Starts")
            else:
                new_spider = SpiderThread(add_link_dictionary, self)
                self.threadPool.append(new_spider)
                self.threadPool[len(self.threadPool) - 1].start()
                self.threadPool[len(self.threadPool) - 1].SPIDERSIGNAL.connect(self.spiderUpdate)
                message = QCoreApplication.translate("mainwindow_src_ui_tr", "Download Scheduled")
            notifySend(message, '', 10000, 'no', parent=self)


    # see addlink.py file
    def addLinkSpiderCallBack(self, spider_dict, child):
        # get file_name and file_size
        file_name = spider_dict['file_name']
        file_size = spider_dict['file_size']

        if file_size:
            file_size = 'Size: ' + str(file_size)
            child.size_label.setText(file_size)

        if file_name and not(child.change_name_checkBox.isChecked()):
            child.change_name_lineEdit.setText(file_name)
            child.change_name_checkBox.setChecked(True)

 

2. Download and add interface

The download add interface AddLinkWindow initializes the first parameter self to parent, and then accesses the main interface through this parameter. The second parameter is a callback function to pass parameters to the main interface, and the third parameter passes system settings to the download. Add interface.

When the download link changes, add AddLinkSpiderThread to the threadPool of the main interface, and connect ADDLINKSPIDERSIGNAL to the addLinkSpiderCallBack of the main interface.

new_spider = AddLinkSpiderThread(dict)

self.parent.threadPool.append(new_spider)

self.parent.threadPool[len(self.parent.threadPool) - 1].ADDLINKSPIDERSIGNAL.connect(
                partial(self.parent.addLinkSpiderCallBack, child=self))

AddLinkSpiderThread obtains the file size and name information through spider.addLinkSpider, and sends it to the addLinkSpiderCallBack function of the main interface. Note that when launching here, there are only dict parameters and two parameters when connecting.

self.ADDLINKSPIDERSIGNAL.emit(spider_dict)

After pressing the OK button, pass the parameters to the main interface through the callback function call.

addlink.py:

class AddLinkSpiderThread(QThread):
    ADDLINKSPIDERSIGNAL = pyqtSignal(dict)

    def __init__(self, add_link_dictionary):
        QThread.__init__(self)
        self.add_link_dictionary = add_link_dictionary

    def run(self):
        try:
            # get file name and file size
            file_name, file_size = spider.addLinkSpider(self.add_link_dictionary)

            spider_dict = {'file_size': file_size, 'file_name': file_name}

            # emit results
            self.ADDLINKSPIDERSIGNAL.emit(spider_dict)

class AddLinkWindow(AddLinkWindow_Ui):
    def __init__(self, parent, callback, persepolis_setting, plugin_add_link_dictionary={}):
        super().__init__(persepolis_setting)
        self.callback = callback
        self.plugin_add_link_dictionary = plugin_add_link_dictionary
        self.persepolis_setting = persepolis_setting
        self.parent = parent    

        self.link_lineEdit.textChanged.connect(self.linkLineChanged)

        self.ok_pushButton.clicked.connect(partial(
             self.okButtonPressed, download_later=False))
        self.download_later_pushButton.clicked.connect(
             partial(self.okButtonPressed, download_later=True))

    # enable when link_lineEdit is not empty and find size of file.
    def linkLineChanged(self, lineEdit):
        if str(self.link_lineEdit.text()) == '':
            self.ok_pushButton.setEnabled(False)
            self.download_later_pushButton.setEnabled(False)
        else:  # find file size

            dict = {'link': str(self.link_lineEdit.text())}

            # spider is finding file size
            new_spider = AddLinkSpiderThread(dict)
            self.parent.threadPool.append(new_spider)
            self.parent.threadPool[len(self.parent.threadPool) - 1].start()
            self.parent.threadPool[len(self.parent.threadPool) - 1].ADDLINKSPIDERSIGNAL.connect(
                partial(self.parent.addLinkSpiderCallBack, child=self))

            self.ok_pushButton.setEnabled(True)
            self.download_later_pushButton.setEnabled(True)
    def okButtonPressed(self, button, download_later):
        # user submitted information by pressing ok_pushButton, so get information
        # from AddLinkWindow and return them to the mainwindow with callback!


        # save information in a dictionary(add_link_dictionary).
        self.add_link_dictionary = {'referer': referer, 'header': header, 'user_agent': user_agent, 'load_cookies': load_cookies,
                                    'out': out, 'start_time': start_time, 'end_time': end_time, 'link': link, 'ip': ip,
                                    'port': port, 'proxy_user': proxy_user, 'proxy_passwd': proxy_passwd,
                                    'download_user': download_user, 'download_passwd': download_passwd,
                                    'connections': connections, 'limit_value': limit, 'download_path': download_path}

        # get category of download
        category = str(self.add_queue_comboBox.currentText())

        del self.plugin_add_link_dictionary

        # return information to mainwindow
        self.callback(self.add_link_dictionary, download_later, category)

        # close window
        self.close()

3. Summary

1. Parameters passed between threads can be passed through callback functions, or through signals and slots.

2. Between the master and slave threads, the master thread passes self to the slave thread, and the slave thread can call the function of the master thread. The slave thread can also pass self to the main thread, and the main thread makes function calls to the slave thread

 

2. Download files

 

The service of starting aria2 is started through subprocess.Popen. The meaning of each option is described in the aria2 interface document.

subprocess Modules allow you to spawn new processes, connect their input, output, and error pipes, and get their return codes. This module intends to replace some old modules and functions os.system, os.popen*, os.spawn.

https://docs.python.org/zh-cn/3/library/subprocess.html#subprocess.Popen.communicate

https://blog.csdn.net/qq_34355232/article/details/87709418

subprocess.Popen([aria2d, '--no-conf', '--enable-rpc', '--rpc-listen-port=' + str(port), '--rpc-max-request-size=2M', '--rpc-listen-all', '--quiet=true'], stderr=subprocess.PIPE, stdout=subprocess.PIPE, stdin=subprocess.PIPE, shell=False, creationflags=NO_WINDOW)

 

Adding the download link is done through the XML-RPC remote call:

server = xmlrpc.client.ServerProxy(server_uri, allow_none=True)

The RPC interface of aria2 is introduced as follows, supporting JSON-RPC and XML-RPC.

https://aria2.github.io/manual/en/html/aria2c.html#rpc-interface

Python's XML-RPC library has a lot of introduction documents, and I found two as follows:

https://www.jianshu.com/p/9987913cf734

https://developer.51cto.com/art/201906/597963.htm

GID: aria2 manages each download through the GID index, which is a 64-bit binary number. During RPC access, it is expressed as a 16-character hexadecimal string. Usually aria2 generates GID for each download link, and the user can also specify it through the GID option.

Access aria2 via XML-RPC

aria2.addUri([secret, ]uris[, options[, position]])

Add a download link, URIS is an array of download links, option and positon are an integer, indicating the position inserted in the download queue, and 0 means the first one. If the position parameter is not provided or the position is longer than the length of the queue, the added download is at the end of the download queue. This method returns the newly registered GID downloaded.

aria2.tellStatus([secret, ]gid[, keys])

This method returns the progress of downloading the specified GID. The keys is an array of strings, specifying which items need to be queried. If the keys are empty or omitted, all items are included. Common projects gid、status、totalLength、completedLength、downloadSpeed, uploadSpeed,numSeeders、connections、dir、files。

aria2.tellActive([secret][, keys])

This method queries the status of the activated download, the query item andaria2.tellStatus类似。

aria2.removeDownloadResult([ secret ,] gid )

Remove download completed/download error/deleted download from storage according to GID, return OK if successful

aria2.remove([ secret ,] gid )

Delete the download according to the GID, if the download is in progress, stop the download first. The status of the download link changes to the removed status. Returns the GID of the deleted state.

aria2.pause([ secret ,] gid )

The download link of the specified GID is suspended, and the status of the download link becomes paused. If the download is active, the download link is placed at the top of the waiting queue. To change the state to waiting, you need to use the aria2.unpause method.

download.py

# get port from persepolis_setting
port = int(persepolis_setting.value('settings/rpc-port'))

# get aria2_path
aria2_path = persepolis_setting.value('settings/aria2_path')

# xml rpc
SERVER_URI_FORMAT = 'http://{}:{:d}/rpc'
server_uri = SERVER_URI_FORMAT.format(host, port)
server = xmlrpc.client.ServerProxy(server_uri, allow_none=True)

# start aria2 with RPC


def startAria():
    
    # in Windows
    elif os_type == OS.WINDOWS:
        if aria2_path == "" or aria2_path == None or os.path.isfile(str(aria2_path)) == False:
            cwd = sys.argv[0]
            current_directory = os.path.dirname(cwd)

            aria2d = os.path.join(current_directory, "aria2c.exe")  # aria2c.exe path
        else:
            aria2d = aria2_path

        # NO_WINDOW option avoids opening additional CMD window in MS Windows.
        NO_WINDOW = 0x08000000

        if not os.path.exists(aria2d):
            logger.sendToLog("Aria2 does not exist in the current path!", "ERROR")
            return None
        # aria2 command in windows
        subprocess.Popen([aria2d, '--no-conf', '--enable-rpc', '--rpc-listen-port=' + str(port),
                          '--rpc-max-request-size=2M', '--rpc-listen-all', '--quiet=true'],
                         stderr=subprocess.PIPE,
                         stdout=subprocess.PIPE,
                         stdin=subprocess.PIPE,
                         shell=False,
                         creationflags=NO_WINDOW)

    time.sleep(2)

    # check that starting is successful or not!
    answer = aria2Version()

    # return result
    return answer

# check aria2 release version . Persepolis uses this function to
# check that aria2 RPC connection is available or not.


def aria2Version():
    try:
        answer = server.aria2.getVersion()
    except:
        # write ERROR messages in terminal and log
        logger.sendToLog("Aria2 didn't respond!", "ERROR")
        answer = "did not respond"

    return answer

def downloadAria(gid, parent):
    # add_link_dictionary is a dictionary that contains user download request
    # information.

    # get information from data_base
    add_link_dictionary = parent.persepolis_db.searchGidInAddLinkTable(gid)

    answer = server.aria2.addUri([link], aria_dict)

Three, the database

Using sqlite3 database tutorial: https://docs.python.org/zh-cn/3/library/sqlite3.html

There are three databases TempDB in memory that store real-time data, PluginsDB stores new link data sent by browser plug-ins, and PersepolisDB is the main database that stores download information.

TempDB has two tables, single_db_table stores the GID being downloaded, and queue_db_table stores the GID information of the download queue.

PersepolisDB has four tables:

category_db_table stores type information, including the types of'All Downloads','Single Downloads' and'Scheduled Downloads'.

download_db_table stores the download status table displayed on the main interface.

addlink_db_table stores the download link added by the download add interface.

video_finder_db_table stores the download information added in the download add interface.

# This class manages TempDB
# TempDB contains gid of active downloads in every session.
class TempDB():
    def __init__(self):
        # temp_db saves in RAM
        # temp_db_connection

        self.temp_db_connection = sqlite3.connect(':memory:', check_same_thread=False)

   def createTables(self):
        # lock data base
        self.lockCursor()
        self.temp_db_cursor.execute("""CREATE TABLE IF NOT EXISTS single_db_table(

        self.temp_db_cursor.execute("""CREATE TABLE IF NOT EXISTS queue_db_table(


# persepolis main data base contains downloads information
# This class is managing persepolis.db
class PersepolisDB():
    def __init__(self):
        # persepolis.db file path
        persepolis_db_path = os.path.join(config_folder, 'persepolis.db')

        # persepolis_db_connection
        self.persepolis_db_connection = sqlite3.connect(persepolis_db_path, check_same_thread=False)

    # queues_list contains name of categories and category settings
    def createTables(self):

        # lock data base
        self.lockCursor()
        # Create category_db_table and add 'All Downloads' and 'Single Downloads' to it
        self.persepolis_db_cursor.execute("""CREATE TABLE IF NOT EXISTS category_db_table(

        # download table contains download table download items information
        self.persepolis_db_cursor.execute("""CREATE TABLE IF NOT EXISTS download_db_table(

        # addlink_db_table contains addlink window download information
        self.persepolis_db_cursor.execute("""CREATE TABLE IF NOT EXISTS addlink_db_table(


        # video_finder_db_table contains addlink window download information
        self.persepolis_db_cursor.execute("""CREATE TABLE IF NOT EXISTS video_finder_db_table(

sqlite3 The module supports two placeholders: question mark (qmark style) and named placeholder (named style).

# This is the qmark style:

cur.execute("insert into people values (?, ?)", (who, age))

# And this is the named style:

cur.execute("select * from people where name_last=:who and age=:age", {"who": who, "age": age})

The coalesce function returns the value of the first non-empty expression in its parameters, that is, the new parameter is used if the parameter is provided, and the original value is used if the new parameter is not provided.

self.temp_db_cursor.execute("""UPDATE single_db_table SET shutdown = coalesce(:shutdown, shutdown),
                                                                status = coalesce(:status, status)
                                                                WHERE gid = :gid""", dict)

MainWindow creates the CheckDownloadInfoThread thread when it is initialized, polls each download link, and returns the result to the checkDownloadInfo function of the main interface to update the download status.

        # CheckDownloadInfoThread
        check_download_info = CheckDownloadInfoThread(self)
        self.threadPool.append(check_download_info)
        self.threadPool[1].start()
        self.threadPool[1].DOWNLOAD_INFO_SIGNAL.connect(self.checkDownloadInfo)
        self.threadPool[1].RECONNECTARIASIGNAL.connect(self.reconnectAria)

 

 

Guess you like

Origin blog.csdn.net/bluewhu/article/details/104423253