├── screens ├── sultans-tab.png ├── ui-screen.png └── feather-chords.png ├── settings.cfg ├── .gitignore ├── requirements.txt ├── README.md ├── ui ├── results.ui └── search.ui └── src ├── utils.py └── tab_scraper.py /screens/sultans-tab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanfhear/tab-scraper/HEAD/screens/sultans-tab.png -------------------------------------------------------------------------------- /screens/ui-screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanfhear/tab-scraper/HEAD/screens/ui-screen.png -------------------------------------------------------------------------------- /screens/feather-chords.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seanfhear/tab-scraper/HEAD/screens/feather-chords.png -------------------------------------------------------------------------------- /settings.cfg: -------------------------------------------------------------------------------- 1 | [MAIN] 2 | 3 | # the full path to the root directory you want to store all tabs in 4 | # e.g. = C:\Users\yourname\Music\Tabs\ 5 | # make sure it ends with a slash 6 | destination_root = . 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | *.pyc 3 | vvenv/ 4 | *.json 5 | *.log 6 | dist/ 7 | src/geckodriver 8 | src/ui.py 9 | *.spec 10 | build/ 11 | *.zip 12 | *.sh 13 | *.bat 14 | *.exe 15 | src/settings.cfg 16 | tab_scraper/ 17 | release_notes.txt 18 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | cairocffi==1.0.2 2 | CairoSVG==2.5.1 3 | certifi==2019.3.9 4 | cffi==1.12.3 5 | chardet==3.0.4 6 | cssselect2==0.2.1 7 | defusedxml==0.6.0 8 | html5lib==1.0.1 9 | idna==2.8 10 | Pillow==9.0.0 11 | pycparser==2.19 12 | Pyphen==0.9.5 13 | PyQt5==5.12.1 14 | PyQt5-sip==4.19.15 15 | requests==2.21.0 16 | selenium==3.141.0 17 | six==1.12.0 18 | soupsieve==1.9.1 19 | tinycss2==1.0.2 20 | urllib3==1.26.5 21 | webencodings==0.5.1 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tab-scraper 2 | An interface for downloading guitar tabs from Ultimate Guitar. 3 | 4 | ![ui-image](screens/ui-screen.png) 5 | 6 | Get screenshots of Guitar Chords, Tabs, Bass Tabs and Ukulele Chords with no clutter. 7 | 8 | Chords | Tab 9 | :------:|:------| 10 | ![chords](screens/feather-chords.png) | ![tab](screens/sultans-tab.png) 11 | 12 | You can also download GuitarPro and PowerTab files.
13 | All files are sorted into directories for quick and easy access. 14 | 15 | ### Prerequisites 16 | 17 | 1. Firefox Web Browser 18 | 19 | ### Running the Program 20 | 21 | #### Executable 22 | 23 | 1. Download an executable version [here](https://github.com/Sean-Hassett/tab-scraper/releases). 24 | 2. Follow instructions in the readme.txt provided. 25 | 26 | #### Command Line 27 | 28 | 1. Open settings.cfg and enter in the root directory where you would like all tabs to be stored e.g. username/Music/Tabs/ 29 | 2. Download [Geckodriver](https://github.com/mozilla/geckodriver/releases) and put the geckodriver executable into the src directory. 30 | 3. Run `pip install -r requirements.txt` 31 | 4. run `python tab_scraper.py` from src directory. 32 | 33 | ### Built With 34 | 35 | - Python 3 36 | 37 | - [PyQT5](https://pypi.org/project/PyQt5/) 38 | 39 | - [Selenium](https://selenium-python.readthedocs.io/) 40 | 41 | - [Geckodriver](https://github.com/mozilla/geckodriver/releases) 42 | -------------------------------------------------------------------------------- /ui/results.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Form 4 | 5 | 6 | 7 | 0 8 | 0 9 | 962 10 | 537 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 19 | 230 20 | 80 21 | 651 22 | 241 23 | 24 | 25 | 26 | true 27 | 28 | 29 | QAbstractItemView::SingleSelection 30 | 31 | 32 | QAbstractItemView::SelectRows 33 | 34 | 35 | QAbstractItemView::ScrollPerPixel 36 | 37 | 38 | QAbstractItemView::ScrollPerPixel 39 | 40 | 41 | false 42 | 43 | 44 | true 45 | 46 | 47 | 100 48 | 49 | 50 | 99 51 | 52 | 53 | 54 | 1 55 | 56 | 57 | 58 | 59 | Type 60 | 61 | 62 | 63 | 64 | Artist 65 | 66 | 67 | 68 | 69 | Title 70 | 71 | 72 | 73 | 74 | Rating 75 | 76 | 77 | 78 | 79 | Votes 80 | 81 | 82 | 83 | 84 | Chords 85 | 86 | 87 | 88 | 89 | Oasis 90 | 91 | 92 | 93 | 94 | Wonderwall 95 | 96 | 97 | 98 | 99 | 4.84 100 | 101 | 102 | 103 | 104 | 3,277 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /ui/search.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 200 10 | 420 11 | 12 | 13 | 14 | 15 | 0 16 | 0 17 | 18 | 19 | 20 | 21 | 200 22 | 420 23 | 24 | 25 | 26 | 27 | 200 28 | 420 29 | 30 | 31 | 32 | 33 | 12 34 | 35 | 36 | 37 | MainWindow 38 | 39 | 40 | QTabWidget::Rounded 41 | 42 | 43 | 44 | 45 | 46 | 25 47 | 25 48 | 150 49 | 30 50 | 51 | 52 | 53 | 54 | 12 55 | 56 | 57 | 58 | false 59 | 60 | 61 | 62 | 63 | 64 | Artist 65 | 66 | 67 | 68 | 69 | 70 | 25 71 | 70 72 | 150 73 | 30 74 | 75 | 76 | 77 | 78 | 12 79 | 80 | 81 | 82 | Song Title 83 | 84 | 85 | 86 | 87 | 88 | 25 89 | 125 90 | 150 91 | 20 92 | 93 | 94 | 95 | 96 | 12 97 | 98 | 99 | 100 | Chords 101 | 102 | 103 | 104 | 105 | 106 | 25 107 | 160 108 | 150 109 | 20 110 | 111 | 112 | 113 | 114 | 12 115 | 116 | 117 | 118 | Tab 119 | 120 | 121 | 122 | 123 | 124 | 25 125 | 195 126 | 150 127 | 20 128 | 129 | 130 | 131 | 132 | 12 133 | 134 | 135 | 136 | GuitarPro 137 | 138 | 139 | 140 | 141 | 142 | 25 143 | 345 144 | 150 145 | 30 146 | 147 | 148 | 149 | 150 | 12 151 | 152 | 153 | 154 | Search 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 25 164 | 230 165 | 150 166 | 20 167 | 168 | 169 | 170 | 171 | 12 172 | 173 | 174 | 175 | Bass 176 | 177 | 178 | 179 | 180 | 181 | 25 182 | 265 183 | 150 184 | 20 185 | 186 | 187 | 188 | 189 | 12 190 | 191 | 192 | 193 | Ukulele 194 | 195 | 196 | 197 | 198 | 199 | 25 200 | 300 201 | 150 202 | 20 203 | 204 | 205 | 206 | 207 | 12 208 | 209 | 210 | 211 | Power 212 | 213 | 214 | 215 | 216 | 217 | 218 | 0 219 | 0 220 | 200 221 | 27 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | -------------------------------------------------------------------------------- /src/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import sys 4 | import json 5 | import requests 6 | from time import sleep 7 | from selenium import webdriver 8 | from selenium.webdriver.firefox.options import Options 9 | from selenium.webdriver.firefox.firefox_profile import FirefoxProfile 10 | from selenium.common.exceptions import NoSuchElementException 11 | from configparser import ConfigParser 12 | 13 | 14 | SEARCH_URL = "https://www.ultimate-guitar.com/search.php?page={}&search_type=title&value={}" 15 | RESULTS_PATTERN = "\"results\":(\[.*?\]),\"pagination\"" 16 | RESULTS_COUNT_PATTERN = "\"tabs\",\"results_count\":([0-9]+?),\"results\"" 17 | DOWNLOAD_TIMEOUT = 15 18 | 19 | 20 | def search_tabs(search_string, types): 21 | page = 1 22 | # get first page of results 23 | response = requests.get(SEARCH_URL.format(page, search_string)) 24 | # count is the number of results, used to know how many pages to search 25 | count = 0 26 | try: 27 | # isolate results from page using regex 28 | response_body = response.content.decode() 29 | results = re.search(RESULTS_PATTERN, response_body).group(1) 30 | count = int(re.search(RESULTS_COUNT_PATTERN, response_body).group(1)) 31 | except AttributeError: 32 | results = '' 33 | response_data = json.loads(results) 34 | 35 | ret = [] 36 | while count > 0: 37 | for item in response_data: 38 | try: 39 | # Get every result that has a desired type 40 | if item["type"] in types: 41 | ret.append((item["type"], item["artist_name"], item["song_name"], 42 | str(round(float(item["rating"]), 1)), str(item["votes"]), 43 | item["tab_url"], str(item["version"]))) 44 | except KeyError: 45 | # key error on "official" tabs, not interested in these tabs 46 | '' 47 | count -= 1 48 | if count > 0: 49 | page += 1 50 | response = requests.get(SEARCH_URL.format(page, search_string)) 51 | try: 52 | # isolate results from page using regex 53 | response_body = response.content.decode() 54 | results = re.search(RESULTS_PATTERN, response_body).group(1) 55 | except AttributeError: 56 | results = '' 57 | response_data = json.loads(results) 58 | return ret 59 | 60 | 61 | def download_tab(url, tab_type, artist, title, version): 62 | if getattr(sys, 'frozen', False): 63 | # If the application is run as a bundle, the pyInstaller bootloader 64 | # extends the sys module by a flag frozen=True 65 | application_path = os.path.dirname(sys.executable) 66 | else: 67 | application_path = os.path.dirname(os.path.abspath(os.path.splitext(__file__)[0])) 68 | settings_file = os.path.join(application_path, "settings.cfg") 69 | 70 | config = ConfigParser() 71 | config.read(settings_file) 72 | cfg = config['MAIN'] 73 | 74 | # get path to gecko executable by joining the application path with 'geckodriver' and the .exe file extension 75 | # if the executed tab_scraper is an exe 76 | gecko_path = (os.path.join(application_path, "geckodriver", 77 | ".exe" if os.path.splitext(__file__)[1] == ".exe" else ""))[:-1] 78 | 79 | # create destination directory if it doesn't exist 80 | destination_root = cfg['destination_root'] 81 | destination = os.path.join(destination_root, tab_type, artist) 82 | os.makedirs(destination, exist_ok=True) 83 | 84 | options = Options() 85 | options.headless = True 86 | 87 | driver = webdriver.Firefox(options=options, executable_path=gecko_path) 88 | driver.get(url) 89 | 90 | # clear the privacy policy message 91 | try: 92 | popup_btn = driver.find_element_by_xpath('//button[text()="Got it, thanks!"]') 93 | popup_btn.click() 94 | except NoSuchElementException: 95 | pass 96 | 97 | # clear the official tab ad 98 | try: 99 | popup_btn = driver.find_element_by_xpath('//div[contains(@class, "ai-ah")]//button') 100 | popup_btn.click() 101 | except NoSuchElementException: 102 | pass 103 | 104 | # hide the autoscroll tool 105 | try: 106 | autoscroll = driver.find_element_by_xpath('//span[text()="Autoscroll"]/parent::button/parent::div/parent::section') 107 | driver.execute_script("arguments[0].setAttribute('style', 'display: none')", autoscroll) 108 | except NoSuchElementException: 109 | pass 110 | 111 | tab = driver.find_element_by_tag_name("pre") 112 | filename = os.path.join(destination, (title + " (Ver " + version + ")" + ".png")) 113 | tab.screenshot(filename) 114 | 115 | driver.quit() 116 | 117 | 118 | def download_file(url, tab_type, artist): 119 | if getattr(sys, 'frozen', False): 120 | # If the application is run as a bundle, the pyInstaller bootloader 121 | # extends the sys module by a flag frozen=True 122 | application_path = os.path.dirname(sys.executable) 123 | else: 124 | application_path = os.path.dirname(os.path.abspath(os.path.splitext(__file__)[0])) 125 | settings_file = os.path.join(application_path, "settings.cfg") 126 | config = ConfigParser() 127 | config.read(settings_file) 128 | cfg = config['MAIN'] 129 | 130 | # get path to gecko executable by joining the application path with 'geckodriver' and the .exe file extension 131 | # if the executed tab_scraper is an exe 132 | gecko_path = (os.path.join(application_path, "geckodriver", 133 | ".exe" if os.path.splitext(__file__)[1] == ".exe" else ""))[:-1] 134 | 135 | # create destination directory if it doesn't exist 136 | destination_root = cfg['destination_root'] 137 | destination = os.path.join(destination_root, tab_type, artist) 138 | os.makedirs(destination, exist_ok=True) 139 | 140 | # count how many files are in the destination already 141 | nFiles = len(os.listdir(destination)) 142 | 143 | options = Options() 144 | options.headless = True 145 | 146 | profile = FirefoxProfile() 147 | profile.set_preference("browser.download.folderList", 2) 148 | profile.set_preference("browser.download.manager.showWhenStarting", False) 149 | profile.set_preference("browser.download.dir", destination) 150 | profile.set_preference("browser.helperApps.neverAsk.saveToDisk", "application/octet-stream") 151 | 152 | driver = webdriver.Firefox(options=options, firefox_profile=profile, executable_path=gecko_path) 153 | driver.get(url) 154 | button = driver.find_element_by_xpath('//button/span[text()="DOWNLOAD Guitar Pro TAB" ' 155 | 'or text()="DOWNLOAD Power TAB"]') 156 | driver.execute_script("arguments[0].click();", button) 157 | 158 | # kill firefox process after download completes or a timeout is reached 159 | downloading = True 160 | timeout = DOWNLOAD_TIMEOUT 161 | while downloading and timeout > 0: 162 | sleep(0.5) 163 | if len(os.listdir(destination)) > nFiles: 164 | downloading = False 165 | timeout -= 0.5 166 | driver.quit() 167 | -------------------------------------------------------------------------------- /src/tab_scraper.py: -------------------------------------------------------------------------------- 1 | from PyQt5 import QtCore, QtGui, QtWidgets 2 | from src import utils 3 | import sys 4 | import os 5 | from configparser import ConfigParser 6 | 7 | VERSION = 'v.0.1.2' 8 | TOTAL_WIDTH = 1000 9 | SEARCH_WIDTH = 200 10 | SEARCH_ELEMENT_WIDTH = 150 11 | TEXT_BOX_HEIGHT = 30 12 | CHECK_BOX_HEIGHT = 20 13 | BUTTON_HEIGHT = 30 14 | BUTTON_WIDTH = 150 15 | DIRECTORY_BUTTON_WIDTH = 30 16 | OFFSET = 25 17 | CHECK_BOX_OFFSET = 15 18 | CHECK_BOX_NAMES = ["{}Chords", "{}Tab", "{}GuitarPro", "{}PowerTab", "{}Bass", "{}Ukulele"] 19 | TYPES_DICT = {"Chords": "Chords", 20 | "Tab": "Tabs", 21 | "GuitarPro": "Pro", 22 | "PowerTab": "Power", 23 | "Bass": "Bass Tabs", 24 | "Ukulele": "Ukulele Chords"} 25 | TABLE_COLUMNS = ["Type", "Artist", "Title", "Rating", "Votes"] 26 | 27 | 28 | class MainWindow(object): 29 | def setup_ui(self, search_window): 30 | self.status = '' 31 | self.results = [] 32 | font = QtGui.QFont() 33 | font.setPointSize(12) 34 | 35 | window_height = ((OFFSET * 2 + TEXT_BOX_HEIGHT) + 36 | ((CHECK_BOX_HEIGHT + CHECK_BOX_OFFSET) * len(CHECK_BOX_NAMES) - CHECK_BOX_OFFSET) + 37 | (OFFSET * 2 + BUTTON_HEIGHT)) * 2 38 | 39 | search_window.setObjectName("SearchWindow") 40 | search_window.setWindowTitle("Tab Scraper " + VERSION) 41 | search_window.setMinimumSize(QtCore.QSize(TOTAL_WIDTH, window_height)) 42 | search_window.setMaximumSize(QtCore.QSize(TOTAL_WIDTH, window_height)) 43 | search_window.setFont(font) 44 | search_window.setTabShape(QtWidgets.QTabWidget.Rounded) 45 | 46 | self.central_widget = QtWidgets.QWidget(search_window) 47 | self.central_widget.setObjectName("centralwidget") 48 | 49 | self.check_boxes = [" "] * len(CHECK_BOX_NAMES) 50 | for i, name in enumerate(CHECK_BOX_NAMES): 51 | self.check_boxes[i] = QtWidgets.QCheckBox(self.central_widget) 52 | self.check_boxes[i].setGeometry(QtCore.QRect(OFFSET, 53 | ((OFFSET * 2 + TEXT_BOX_HEIGHT) + 54 | (CHECK_BOX_HEIGHT + CHECK_BOX_OFFSET) * i), 55 | SEARCH_ELEMENT_WIDTH, 56 | CHECK_BOX_HEIGHT)) 57 | self.check_boxes[i].setObjectName(name.format("checkBox")) 58 | self.check_boxes[i].setText(name.format("")) 59 | 60 | self.search_button = QtWidgets.QPushButton(self.central_widget) 61 | self.search_button.setGeometry(QtCore.QRect(OFFSET, 62 | ((OFFSET * 2 + TEXT_BOX_HEIGHT) + 63 | (CHECK_BOX_HEIGHT + CHECK_BOX_OFFSET) * 64 | len(CHECK_BOX_NAMES) - CHECK_BOX_OFFSET + OFFSET), 65 | BUTTON_WIDTH, 66 | BUTTON_HEIGHT)) 67 | self.search_button.setObjectName("searchButton") 68 | self.search_button.setText("Search") 69 | self.search_button.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) 70 | self.search_button.clicked.connect(self.search_tabs) 71 | 72 | self.download_button = QtWidgets.QPushButton(self.central_widget) 73 | self.download_button.setGeometry(QtCore.QRect(OFFSET, 74 | ((OFFSET * 2 + TEXT_BOX_HEIGHT) + 75 | (CHECK_BOX_HEIGHT + CHECK_BOX_OFFSET) * 76 | len(CHECK_BOX_NAMES) - CHECK_BOX_OFFSET + OFFSET + 77 | (OFFSET + BUTTON_HEIGHT)), 78 | BUTTON_WIDTH - DIRECTORY_BUTTON_WIDTH, 79 | BUTTON_HEIGHT)) 80 | self.download_button.setObjectName("downloadButton") 81 | self.download_button.setText("Download") 82 | self.download_button.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) 83 | self.download_button.clicked.connect(self.download_tab) 84 | 85 | self.set_directory_button = QtWidgets.QPushButton(self.central_widget) 86 | self.set_directory_button.setGeometry(QtCore.QRect(OFFSET + (BUTTON_WIDTH - DIRECTORY_BUTTON_WIDTH), 87 | ((OFFSET * 2 + TEXT_BOX_HEIGHT) + 88 | (CHECK_BOX_HEIGHT + CHECK_BOX_OFFSET) * 89 | len(CHECK_BOX_NAMES) - CHECK_BOX_OFFSET + OFFSET + 90 | (OFFSET + BUTTON_HEIGHT)), 91 | DIRECTORY_BUTTON_WIDTH, 92 | BUTTON_HEIGHT)) 93 | self.set_directory_button.setObjectName("downloadButton") 94 | self.set_directory_button.setText("...") 95 | self.set_directory_button.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) 96 | self.set_directory_button.clicked.connect(self.set_download_location) 97 | 98 | self.status_message = QtWidgets.QLabel(self.central_widget) 99 | self.status_message.setGeometry(QtCore.QRect(OFFSET, 100 | ((OFFSET * 2 + TEXT_BOX_HEIGHT) + 101 | (CHECK_BOX_HEIGHT + CHECK_BOX_OFFSET) * 102 | len(CHECK_BOX_NAMES) - CHECK_BOX_OFFSET + OFFSET + 103 | (OFFSET + BUTTON_HEIGHT) * 2), 104 | BUTTON_WIDTH, 105 | BUTTON_HEIGHT)) 106 | status_font = QtGui.QFont() 107 | status_font.setPointSize(10) 108 | self.status_message.setFont(status_font) 109 | self.status_message.setText(self.status) 110 | 111 | 112 | self.search_input = QtWidgets.QLineEdit(self.central_widget) 113 | self.search_input.setGeometry(QtCore.QRect(OFFSET, OFFSET, SEARCH_ELEMENT_WIDTH, TEXT_BOX_HEIGHT)) 114 | self.search_input.setObjectName("lineEdit") 115 | self.search_input.returnPressed.connect(self.search_button.click) 116 | self.search_input.setPlaceholderText("Search query...") 117 | 118 | self.tableWidget = QtWidgets.QTableWidget(self.central_widget) 119 | self.tableWidget.setGeometry(QtCore.QRect(SEARCH_WIDTH, OFFSET, TOTAL_WIDTH - SEARCH_WIDTH - OFFSET, 120 | window_height - OFFSET * 2)) 121 | self.tableWidget.setSortingEnabled(False) 122 | self.tableWidget.setAlternatingRowColors(True) 123 | self.tableWidget.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection) 124 | self.tableWidget.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) 125 | self.tableWidget.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) 126 | self.tableWidget.setHorizontalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) 127 | self.tableWidget.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) 128 | 129 | self.tableWidget.setShowGrid(False) 130 | self.tableWidget.setObjectName("tableWidget") 131 | self.tableWidget.setColumnCount(len(TABLE_COLUMNS)) 132 | self.tableWidget.setRowCount(len(self.results)) 133 | 134 | for i, header in enumerate(TABLE_COLUMNS): 135 | item = QtWidgets.QTableWidgetItem() 136 | item.setText(header) 137 | self.tableWidget.setHorizontalHeaderItem(i, item) 138 | 139 | header = self.tableWidget.horizontalHeader() 140 | for i in range(len(TABLE_COLUMNS)): 141 | if TABLE_COLUMNS[i] == "Title": 142 | header.setSectionResizeMode(i, QtWidgets.QHeaderView.Stretch) 143 | else: 144 | header.setSectionResizeMode(i, QtWidgets.QHeaderView.ResizeToContents) 145 | 146 | self.tableWidget.horizontalHeader().setDefaultSectionSize(100) 147 | self.tableWidget.horizontalHeader().setMinimumSectionSize(100) 148 | 149 | QtCore.QMetaObject.connectSlotsByName(search_window) 150 | search_window.setCentralWidget(self.central_widget) 151 | 152 | def search_tabs(self): 153 | types = [] 154 | for check_box in self.check_boxes: 155 | if check_box.isChecked(): 156 | types.append(TYPES_DICT[check_box.text()]) 157 | search_string = "%20".join(self.search_input.text().split()) 158 | if len(search_string) > 0: 159 | self.results = utils.search_tabs(search_string, types) 160 | self.update_table() 161 | 162 | def update_table(self): 163 | self.tableWidget.setRowCount(len(self.results)) 164 | for i in range(len(self.results)): 165 | for j in range(len(TABLE_COLUMNS)): 166 | item = QtWidgets.QTableWidgetItem() 167 | item.setText(self.results[i][j]) 168 | self.tableWidget.setItem(i, j, item) 169 | 170 | def download_tab(self): 171 | self.status_message.setText('Downloading...') 172 | self.status_message.repaint() 173 | if len(self.results) > 0: 174 | row = self.results[self.tableWidget.currentRow()] 175 | url = row[-2] 176 | is_file = False 177 | if row[0] == "Pro" or row[0] == "Power": 178 | is_file = True 179 | 180 | if is_file: 181 | utils.download_file(url, row[0], row[1].replace("/", "")) 182 | else: 183 | utils.download_tab(url, row[0], row[1].replace("/", ""), row[2], row[6]) 184 | self.status_message.setText('Download finished') 185 | #self.central_widget.update() 186 | 187 | def set_download_location(self): 188 | dialog = QtWidgets.QFileDialog() 189 | folder_path = dialog.getExistingDirectory(None, "Select Folder") 190 | 191 | if getattr(sys, 'frozen', False): 192 | # If the application is run as a bundle, the pyInstaller bootloader 193 | # extends the sys module by a flag frozen=True 194 | application_path = os.path.dirname(sys.executable) 195 | else: 196 | application_path = os.path.dirname(os.path.abspath(os.path.splitext(__file__)[0])) 197 | settings_file = os.path.join(application_path, "settings.cfg") 198 | 199 | config = ConfigParser() 200 | config.read(settings_file) 201 | 202 | config.set('MAIN', 'destination_root', folder_path) 203 | with open(settings_file, 'w+') as f: 204 | config.write(f) 205 | 206 | 207 | 208 | 209 | if __name__ == "__main__": 210 | app = QtWidgets.QApplication(sys.argv) 211 | SearchWindow = QtWidgets.QMainWindow() 212 | ui = MainWindow() 213 | ui.setup_ui(SearchWindow) 214 | SearchWindow.show() 215 | sys.exit(app.exec_()) 216 | --------------------------------------------------------------------------------