├── modules ├── __init__.py ├── defines.py ├── addaddondlg.py ├── preferences.py ├── waitdlg.py └── application.py ├── .gitignore ├── media ├── icon.ico ├── icon.png ├── icon.xcf └── lcurse.desktop ├── translations ├── de_DE.qm ├── fr_FR.qm ├── de_DE.ts └── fr_FR.ts ├── Makefile ├── lcurse.pro ├── Pipfile ├── setup.py ├── README.md ├── LICENSE ├── lcurse └── console.py /modules/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.pyc 3 | *.xcf 4 | .pydevproject 5 | -------------------------------------------------------------------------------- /media/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ephraim/lcurse/HEAD/media/icon.ico -------------------------------------------------------------------------------- /media/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ephraim/lcurse/HEAD/media/icon.png -------------------------------------------------------------------------------- /media/icon.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ephraim/lcurse/HEAD/media/icon.xcf -------------------------------------------------------------------------------- /translations/de_DE.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ephraim/lcurse/HEAD/translations/de_DE.qm -------------------------------------------------------------------------------- /translations/fr_FR.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ephraim/lcurse/HEAD/translations/fr_FR.qm -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | update-translations: 2 | pylupdate4 lcurse.pro 3 | 4 | release-translations: 5 | lrelease-qt4 lcurse.pro 6 | -------------------------------------------------------------------------------- /lcurse.pro: -------------------------------------------------------------------------------- 1 | SOURCES += modules/application.py \ 2 | modules/preferences.py \ 3 | modules/waitdlg.py \ 4 | modules/addaddondlg.py 5 | TRANSLATIONS += translations/de_DE.ts \ 6 | translations/fr_FR.ts 7 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.python.org/simple" 3 | verify_ssl = true 4 | 5 | [requires] 6 | python_version = '3.6' 7 | 8 | [packages] 9 | beautifulsoup4 = '>4.5.3' 10 | cfscrape = '*' 11 | lxml = '>3.6' 12 | PyQt5 = '>5.7' 13 | -------------------------------------------------------------------------------- /media/lcurse.desktop: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env xdg-open 2 | [Desktop Entry] 3 | Name=lcurse 4 | GenericName=WoW Addon Manager 5 | Comment=Python script to have a "curse" compatible client for linux 6 | Exec=lcurse %u 7 | Type=Application 8 | Icon=lcurse 9 | Categories=Game;RolePlaying; 10 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from distutils.core import setup 4 | 5 | setup( 6 | name='lcurse', 7 | version='2.0.0', 8 | description='A Curse compatible client for Linux.', 9 | url='https://github.com/ephraim/lcurse', 10 | packages=['modules'], 11 | scripts=['lcurse', 'console.py'], 12 | license='Unlicense', 13 | ) 14 | -------------------------------------------------------------------------------- /modules/defines.py: -------------------------------------------------------------------------------- 1 | from PyQt5 import Qt 2 | 3 | WOW_FOLDER_KEY = "Preferences/wowfolder" 4 | WOW_TOC_KEY = "Preferences/CurrentToc" 5 | WOW_FOLDER_DEFAULT = "{}/.wine/drive_c/Program Files (x86)/World of Warcraft".format(Qt.QDir.homePath()) 6 | WOW_VERSION_DEFAULT = "retail" 7 | 8 | LCURSE_FOLDER = "{}/.lcurse".format(Qt.QDir.homePath()) 9 | LCURSE_ADDONS = LCURSE_FOLDER + "/addons.json" 10 | LCURSE_ADDONS_BASE = LCURSE_FOLDER + "/addons_{}.json" 11 | LCURSE_ADDON_CATALOG = LCURSE_FOLDER + "/addon-catalog.json" 12 | LCURSE_ADDON_TOCS_CACHE = LCURSE_FOLDER + "/tocs.json" 13 | 14 | LCURSE_MAXTHREADS_KEY = "Preferences/maxthreads" 15 | LCURSE_MAXTHREADS_DEFAULT = 50 16 | LCURSE_DBVERSION = 1 17 | TOC = "70100" 18 | # with open(toc, encoding="utf8", errors='replace') as f: 19 | # line = f.readline() 20 | # while line != "": 21 | # print(line) 22 | # sys.exit(42) 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | lcurse 2 | ====== 3 | 4 | Python script to have a "curse" compatible client for linux 5 | 6 | 7 | lcurse nowadays supports git repositories too. 8 | As git repos aren't structured the same, you will most probably need to create an link via "ln -s source destination" inside the wow/Interface/Addons folder. 9 | But at least the update is then done via the usuall lcurse way. 10 | 11 | ### Requirements 12 | * python 3.6 13 | * pipenv 14 | * PyQt5 15 | * bs4 16 | * cfscrape 17 | * lxml 18 | 19 | All requirements can be installed with: 20 | ```bash 21 | pipenv install 22 | ``` 23 | 24 | ## Running 25 | 26 | Simply: 27 | ```bash 28 | pipenv run ./lcurse 29 | ``` 30 | 31 | ### Unattended mode 32 | 33 | You may also run `lcurse` in "unattended mode" once you have set it up. This 34 | will update all your addons and then exit. Use 35 | ```bash 36 | pipenv run ./lcurse --auto-update 37 | ``` 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /modules/addaddondlg.py: -------------------------------------------------------------------------------- 1 | from PyQt5 import Qt 2 | 3 | 4 | class AddAddonDlg(Qt.QDialog): 5 | def __init__(self, parent, availableAddons): 6 | super(AddAddonDlg, self).__init__(parent) 7 | box = Qt.QVBoxLayout(self) 8 | box.addWidget(Qt.QLabel(self.tr("Type name or url of the addon you want to add:"), self)) 9 | self.input = Qt.QLineEdit(self) 10 | box.addWidget(self.input) 11 | btnBox = Qt.QDialogButtonBox(Qt.QDialogButtonBox.Ok | Qt.QDialogButtonBox.Cancel) 12 | btnBox.accepted.connect(self.accept) 13 | btnBox.rejected.connect(self.reject) 14 | box.addWidget(btnBox) 15 | self.show() 16 | if availableAddons: 17 | self.completer = Qt.QCompleter([addon[0] for addon in availableAddons], self) 18 | self.completer.setFilterMode(Qt.Qt.MatchContains) 19 | self.completer.setCaseSensitivity(Qt.Qt.CaseInsensitive) 20 | self.input.setCompleter(self.completer) 21 | else: 22 | Qt.QMessageBox.information(self, self.tr("No addon catalog data"), self.tr( 23 | "You haven't updated the available addons catalog, " 24 | "so you need to insert a URL for the addon you want to add.")) 25 | 26 | def getText(self): 27 | return self.input.text() 28 | -------------------------------------------------------------------------------- /lcurse: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import sys 5 | import signal 6 | import os 7 | 8 | from PyQt5 import Qt 9 | 10 | rootDir = os.path.dirname(os.path.realpath(__file__)) 11 | modulesDir = "{}/modules".format(rootDir) 12 | appTranslationDir = "{}/translations".format(rootDir) 13 | 14 | sys.path.insert(0, modulesDir) 15 | 16 | from modules import defines 17 | 18 | app = None 19 | ret = 42 20 | translations = [] 21 | 22 | 23 | def loadTranslators(): 24 | localeName = Qt.QLocale.system().name() 25 | 26 | qttranslator = Qt.QTranslator() 27 | qttranslator.load("qt_" + localeName, Qt.QLibraryInfo.location(Qt.QLibraryInfo.TranslationsPath)) 28 | translations.append(qttranslator) 29 | 30 | appTranslationFile = "{}/{}.qm".format(appTranslationDir, localeName) 31 | if os.path.exists(appTranslationFile): 32 | apptranslator = Qt.QTranslator() 33 | apptranslator.load(appTranslationFile) 34 | translations.append(apptranslator) 35 | elif localeName[:2] != 'en': 36 | print("WARNING: gui translation file could not be found: {}".format(appTranslationFile)) 37 | 38 | for translator in translations: 39 | app.installTranslator(translator) 40 | 41 | 42 | if __name__ == "__main__": 43 | Qt.QCoreApplication.setApplicationName("lcurse") 44 | Qt.QCoreApplication.setOrganizationName("None-Inc.") 45 | app = Qt.QApplication(sys.argv) 46 | loadTranslators() 47 | 48 | from modules import application 49 | 50 | mainWidget = application.MainWidget() 51 | try: 52 | mainWidget.show() 53 | if len(sys.argv) > 1 and sys.argv[1] == "--auto-update": 54 | mainWidget.hide() 55 | mainWidget.updateAddons() 56 | ret = 0 57 | else: 58 | ret = app.exec_() 59 | mainWidget.saveAddons() 60 | except Exception as e: 61 | print(str(e)) 62 | 63 | sys.exit(ret) 64 | -------------------------------------------------------------------------------- /console.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import json 4 | from PyQt5 import Qt 5 | 6 | rootDir = os.path.dirname(os.path.realpath(__file__)) 7 | modulesDir = "{}/modules".format(rootDir) 8 | appTranslationFile = "{}/translations/{}.qm".format(rootDir, Qt.QLocale.system().name()) 9 | 10 | sys.path.insert(0, modulesDir) 11 | 12 | from modules import waitdlg, defines 13 | from _thread import start_new_thread 14 | 15 | def loadAddons(addonsFile): 16 | addons = None 17 | if os.path.exists(addonsFile): 18 | with open(addonsFile) as f: 19 | addons = json.load(f) 20 | return addons 21 | 22 | def saveAddons(addonsFile, addons): 23 | with open(addonsFile, "w") as f: 24 | json.dump(addons, f) 25 | 26 | class CheckConsole(Qt.QApplication): 27 | def __init__(self, argv, addons): 28 | super(Qt.QApplication, self).__init__(argv) 29 | settings = Qt.QSettings() 30 | self.maxThreads = int(settings.value(defines.LCURSE_MAXTHREADS_KEY, defines.LCURSE_MAXTHREADS_DEFAULT)) 31 | self.sem = Qt.QSemaphore(self.maxThreads) 32 | self.addons = addons 33 | 34 | @Qt.pyqtSlot(Qt.QVariant, bool) 35 | def onUpdateFinished(self, addon, result): 36 | self.sem.release() 37 | self.threadsCount -= 1 38 | print("Addon '{}' updated: {}".format(addon[1], result and "successfully" or "failed")) 39 | if result: 40 | # addon[0] == idx, addon[5] == data from check, addon[5][0] new version 41 | self.addons[addon[0]]["version"] = addon[5][0] # replace old version with new version in addon 42 | if self.threadsCount <= 0: 43 | saveAddons(os.path.expanduser(defines.LCURSE_ADDONS), self.addons) 44 | self.quit() 45 | 46 | @Qt.pyqtSlot(Qt.QVariant, bool, Qt.QVariant) 47 | def onCheckFinished(self, addon, needsUpdate, updateData): 48 | self.sem.release() 49 | if not needsUpdate: 50 | print("Addon '{}' is up to date.".format(addon[1])) 51 | self.threadsCount -= 1 52 | else: 53 | self.sem.acquire() 54 | print("Addon '{}' needs update. New Version: {}".format(addon[1], updateData[0])) 55 | addon.append(updateData) 56 | thread = waitdlg.UpdateWorker(addon) 57 | thread.updateFinished.connect(self.onUpdateFinished) 58 | thread.start() 59 | self.threads.append(thread) 60 | 61 | if self.threadsCount <= 0: 62 | saveAddons(os.path.expanduser(defines.LCURSE_ADDONS), self.addons) 63 | self.quit() 64 | 65 | def startWorkerThreads(self): 66 | self.threads = [] 67 | self.threadsCount = len(self.addons) 68 | i = 0 69 | for i in range(len(self.addons)): 70 | addon = self.addons[i] 71 | self.sem.acquire() 72 | thread = waitdlg.CheckWorker([i, addon["name"], addon["uri"], addon["version"], addon["allowbeta"]]) 73 | thread.checkFinished.connect(self.onCheckFinished) 74 | thread.start() 75 | self.threads.append(thread) 76 | 77 | def exec_(self): 78 | print("checking all addons. please wait ...") 79 | start_new_thread(self.startWorkerThreads, ()) 80 | return super(Qt.QApplication, self).exec_() 81 | 82 | Qt.QCoreApplication.setApplicationName("lcurse") 83 | Qt.QCoreApplication.setOrganizationName("None-Inc.") 84 | 85 | check = CheckConsole(sys.argv, loadAddons(os.path.expanduser(defines.LCURSE_ADDONS))) 86 | ret = check.exec_() 87 | print("Done. Bye Bye!") 88 | sys.exit(ret) 89 | -------------------------------------------------------------------------------- /modules/preferences.py: -------------------------------------------------------------------------------- 1 | from PyQt5 import Qt 2 | from modules import defines 3 | 4 | 5 | class PreferencesDlg(Qt.QDialog): 6 | def __init__(self, parent): 7 | super(PreferencesDlg, self).__init__(parent) 8 | self.settings = Qt.QSettings() 9 | 10 | layout = Qt.QVBoxLayout(self) 11 | 12 | layout.addWidget(Qt.QLabel(self.tr("WoW Install Folder:"), self)) 13 | folderlayout = Qt.QHBoxLayout() 14 | self.wowInstallFolder = Qt.QLineEdit(self.getWowFolder(), self) 15 | folderlayout.addWidget(self.wowInstallFolder) 16 | btn = Qt.QPushButton(self.tr("..."), self) 17 | btn.clicked.connect(self.browseForWoWFolder) 18 | folderlayout.addWidget(btn) 19 | layout.addLayout(folderlayout) 20 | 21 | layout.addWidget(Qt.QLabel(self.tr("Max. concurrent Threads:"), self)) 22 | self.maxthreads = Qt.QSpinBox(self) 23 | self.maxthreads.setMinimum(1) 24 | self.maxthreads.setMaximum(1000) 25 | self.maxthreads.setValue(self.getMaxThreads()) 26 | layout.addWidget(self.maxthreads) 27 | 28 | layout.addWidget(Qt.QLabel(self.tr("Current TOC Number:"), self)) 29 | self.currenttoc = Qt.QLineEdit(str(self.getTocVersion()),self) 30 | layout.addWidget(self.currenttoc) 31 | 32 | bottom = Qt.QHBoxLayout() 33 | bottom.addSpacing(100) 34 | btn = Qt.QPushButton(self.tr("Save"), self) 35 | btn.clicked.connect(self.accept) 36 | btn.setDefault(True) 37 | bottom.addWidget(btn) 38 | btn = Qt.QPushButton(self.tr("Cancel"), self) 39 | btn.clicked.connect(self.reject) 40 | bottom.addWidget(btn) 41 | layout.addSpacing(100) 42 | layout.addLayout(bottom) 43 | self.setLayout(layout) 44 | 45 | def browseForWoWFolder(self): 46 | selectedDir = Qt.QFileDialog.getExistingDirectory(self, 47 | self.tr("Select Wow Install Folder"), 48 | self.wowInstallFolder.text(), 49 | Qt.QFileDialog.ShowDirsOnly | 50 | Qt.QFileDialog.DontResolveSymlinks) 51 | 52 | if selectedDir: 53 | directory = Qt.QDir("{}/_retail_/Interface/AddOns".format(selectedDir)) 54 | if directory.exists(): 55 | self.wowInstallFolder.setText(selectedDir) 56 | else: 57 | Qt.QMessageBox.warning(self, self.tr("Not Wow-Folder"), self.tr( 58 | "The selected folder is not a WoW installation directory.\nPlease select the WoW folder")) 59 | 60 | def getMaxThreads(self): 61 | return int(self.settings.value(defines.LCURSE_MAXTHREADS_KEY, defines.LCURSE_MAXTHREADS_DEFAULT)) 62 | 63 | def setMaxThreads(self, newMaxThreads): 64 | return self.settings.setValue(defines.LCURSE_MAXTHREADS_KEY, int(newMaxThreads)) 65 | 66 | def getWowFolder(self): 67 | return self.settings.value(defines.WOW_FOLDER_KEY, defines.WOW_FOLDER_DEFAULT) 68 | 69 | def setWowFolder(self, newfolder): 70 | return self.settings.setValue(defines.WOW_FOLDER_KEY, newfolder) 71 | 72 | def getTocVersion(self): 73 | return self.settings.value(defines.WOW_TOC_KEY,70200) 74 | 75 | def setTocVersion(self,newtoc): 76 | return self.settings.setValue(defines.WOW_TOC_KEY,int(newtoc)) 77 | 78 | def accept(self): 79 | self.setWowFolder(self.wowInstallFolder.text()) 80 | self.setMaxThreads(self.maxthreads.value()) 81 | self.setTocVersion(self.currenttoc.text()) 82 | super(PreferencesDlg, self).accept() 83 | -------------------------------------------------------------------------------- /modules/waitdlg.py: -------------------------------------------------------------------------------- 1 | from PyQt5 import Qt 2 | from bs4 import BeautifulSoup 3 | import urllib.parse 4 | import cfscrape 5 | from requests.exceptions import RequestException 6 | import zipfile 7 | from modules import defines 8 | import os 9 | import re 10 | import time 11 | import tempfile 12 | from _thread import start_new_thread 13 | from threading import Lock 14 | from subprocess import check_output, check_call 15 | import hashlib 16 | import json 17 | 18 | scraper = cfscrape.create_scraper() 19 | 20 | # Debug helper: caches html page to not hammer server while testing/debugging/coding 21 | class CachedResponse: 22 | content = "" 23 | def __init__(self,data): 24 | self.content=data 25 | 26 | def read(self): 27 | return self.content 28 | 29 | # Debug helper: caches html page to not hammer server while testing/debugging/coding 30 | class CacheDecorator(object): 31 | cachePrefix = '/tmp/urlcache_' 32 | def __init__(self,fun): 33 | self.fun=fun 34 | 35 | def __call__(self, url): 36 | md5 = hashlib.md5() 37 | md5.update(bytes(url,'utf8')) 38 | hash=md5.hexdigest() 39 | try: 40 | return self.ReadFromCache(hash) 41 | except: 42 | response = self.fun(url) 43 | f = open(self.cachePrefix + hash, "w") 44 | f.write(response.text) 45 | f.close() 46 | return response 47 | 48 | def ReadFromCache(self, hash): 49 | return CachedResponse(open(self.cachePrefix + hash,'r').read()) 50 | 51 | # Enable CacheDecorator in order to cache html pages retrieved from curse 52 | # WARNING only for html parsing, disable when you are testing downloading zips 53 | #@CacheDecorator 54 | def OpenWithRetry(url): 55 | count = 0 56 | maxcount = 5 57 | 58 | # Retry 5 times 59 | while count < maxcount: 60 | try: 61 | response = scraper.get(urllib.parse.urlparse(urllib.parse.quote(url, ':/?=')).geturl()) 62 | 63 | return response 64 | 65 | except Exception as e: 66 | print(self.tr("Could not open '{}', retrying... ({})").format(url, count)) 67 | 68 | count = count + 1 69 | time.sleep(1) 70 | 71 | if count >= maxcount: 72 | raise 73 | 74 | 75 | class CheckDlg(Qt.QDialog): 76 | checkFinished = Qt.pyqtSignal(Qt.QVariant, bool, Qt.QVariant) 77 | closeSignal = Qt.pyqtSignal() 78 | 79 | def __init__(self, parent, wowVersion, addons): 80 | super(CheckDlg, self).__init__(parent) 81 | settings = Qt.QSettings() 82 | layout = Qt.QVBoxLayout(self) 83 | if len(addons) == 1: 84 | layout.addWidget(Qt.QLabel(self.tr("Verifying if the addon needs an update..."))) 85 | else: 86 | layout.addWidget(Qt.QLabel(self.tr("Verifying which addon needs an update..."))) 87 | self.progress = Qt.QProgressBar(self) 88 | self.progress.setRange(0, len(addons)) 89 | self.progress.setValue(0) 90 | self.progress.setFormat("%v / %m | %p%") 91 | layout.addWidget(self.progress) 92 | cancelBox = Qt.QHBoxLayout() 93 | cancelBox.addStretch() 94 | self.cancelButton = Qt.QPushButton(self.tr("Cancel"), self) 95 | self.cancelButton.clicked.connect(self.onCancel) 96 | cancelBox.addWidget(self.cancelButton) 97 | cancelBox.addStretch() 98 | layout.addLayout(cancelBox) 99 | self.wowVersion = wowVersion 100 | self.addons = addons 101 | self.maxThreads = int(settings.value(defines.LCURSE_MAXTHREADS_KEY, defines.LCURSE_MAXTHREADS_DEFAULT)) 102 | self.sem = Qt.QSemaphore(self.maxThreads) 103 | 104 | # safe to use without a mutex because reading and writing are independent of each other, 105 | # and GIL will make these atomic operations. 106 | self.cancelled = False 107 | 108 | # protected with self.progressMutex 109 | self.progressMutex = Lock() 110 | self.progressOrAborted = 0 111 | 112 | self.closeSignal.connect(self.close) 113 | 114 | def closeEvent(self, event): 115 | with self.progressMutex: 116 | if self.progressOrAborted < self.progress.maximum(): 117 | # if we aren't ready to close, the user pressed the close button - set the cancel flag so we can stop 118 | self.cancelled = True 119 | event.ignore() 120 | 121 | def startWorkerThreads(self): 122 | self.threads = [] 123 | for addon in self.addons: 124 | self.sem.acquire() 125 | if not self.cancelled: 126 | thread = CheckWorker(self.wowVersion, addon) 127 | thread.checkFinished.connect(self.onCheckFinished) 128 | thread.start() 129 | self.threads.append(thread) 130 | else: 131 | self.onCancelOrFinish(False) 132 | 133 | def exec_(self): 134 | start_new_thread(self.startWorkerThreads, ()) 135 | super(CheckDlg, self).exec_() 136 | 137 | def onCancelOrFinish(self, updateProgress): 138 | self.sem.release() 139 | shouldClose = False 140 | if updateProgress: 141 | self.progress.setValue(self.progress.value() + 1) 142 | with self.progressMutex: 143 | self.progressOrAborted += 1 144 | if self.progressOrAborted == self.progress.maximum(): 145 | shouldClose = True 146 | if shouldClose: 147 | # emit this as a signal so that it will be processed on the main thread. 148 | # Otherwise, this will try to do cleanup from a worker thread, which is a /bad/ idea. 149 | self.closeSignal.emit() 150 | 151 | @Qt.pyqtSlot(Qt.QVariant, bool, Qt.QVariant) 152 | def onCheckFinished(self, addon, needsUpdate, updateData): 153 | self.checkFinished.emit(addon, needsUpdate, updateData) 154 | self.onCancelOrFinish(True) 155 | 156 | def onCancel(self): 157 | self.cancelled = True 158 | 159 | 160 | class CheckWorker(Qt.QThread): 161 | checkFinished = Qt.pyqtSignal(Qt.QVariant, bool, Qt.QVariant) 162 | 163 | def __init__(self, wowVersion, addon): 164 | super(CheckWorker, self).__init__() 165 | self.wowVersion = wowVersion 166 | self.addon = addon 167 | 168 | def needsUpdateGit(self): 169 | try: 170 | settings = Qt.QSettings() 171 | dest = "{}/_{}_/Interface/AddOns/{}".format( 172 | settings.value(defines.WOW_FOLDER_KEY, defines.WOW_FOLDER_DEFAULT), 173 | self.wowVersion, 174 | os.path.basename(str(self.addon[2])[:-4])) 175 | originCurrent = str(check_output(["git", "ls-remote", str(self.addon[2]), "HEAD"]), "utf-8").split()[0] 176 | localCurrent = self.addon[3] 177 | if localCurrent != originCurrent: 178 | return (True, (originCurrent, "")) 179 | return (False, ("", "")) 180 | except Exception as e: 181 | print(self.tr("Git Update Exception"),e) 182 | return (False, None) 183 | 184 | def needsUpdateCurse(self): 185 | try: 186 | pattern = re.compile("-nolib$") 187 | url = self.addon[2] + '/files' 188 | response = OpenWithRetry(url) 189 | html = response.content 190 | soup = BeautifulSoup(html, "lxml") 191 | beta=self.addon[4] 192 | lis = soup.findAll("tr") 193 | if lis: 194 | isOk=False 195 | versionIdx = 1 196 | if self.wowVersion == 'classic': 197 | while versionIdx < len(lis): 198 | version = tuple(lis[versionIdx].findAll('td')[4].stripped_strings) 199 | if int(version[0][0]) == 1 or len(version) > 1 and int(version[0][0]) > 7 and version[1][0] == '+': 200 | isOk = beta or lis[versionIdx].td.div.span.string=='R' 201 | if isOk: 202 | break 203 | versionIdx=versionIdx+1 204 | if not isOk: 205 | versionIdx = 1 206 | while versionIdx < len(lis): 207 | version = tuple(lis[versionIdx].findAll('td')[4].stripped_strings) 208 | if int(version[0][0]) > 1 or len(version) > 1 and version[1][0] == '+': 209 | isOk = beta or lis[versionIdx].td.div.span.string=='R' 210 | if isOk: 211 | break 212 | versionIdx=versionIdx+1 213 | row=lis[versionIdx] 214 | elem = row.find("a",attrs={"data-action":"file-link"}) 215 | version=elem.string 216 | if str(self.addon[3]) != version: 217 | addonid = elem.attrs['href'].split('/')[-1] 218 | addonname = elem.attrs['href'].split('/')[-3] 219 | downloadLink = "https://www.curseforge.com/wow/addons/" + addonname + "/download/" + addonid + "/file" 220 | return (True, (version, downloadLink)) 221 | return (False, ("", "")) 222 | except RequestException as e: 223 | print(self.tr("Curse Update Exception"),e) 224 | except Exception as e: 225 | print(e) 226 | return (False, None) 227 | 228 | def run(self): 229 | result = None; 230 | if "curseforge.com" in self.addon[2]: 231 | result = self.needsUpdateCurse() 232 | elif self.addon[2].endswith(".git"): 233 | result = self.needsUpdateGit() 234 | 235 | if result: 236 | self.checkFinished.emit(self.addon, result[0], result[1]) 237 | else: 238 | self.checkFinished.emit(self.addon, False, False) 239 | 240 | 241 | class UpdateDlg(Qt.QDialog): 242 | updateFinished = Qt.pyqtSignal(Qt.QVariant, bool) 243 | 244 | def __init__(self, parent, wowVersion, addons): 245 | super(UpdateDlg, self).__init__(parent) 246 | settings = Qt.QSettings() 247 | layout = Qt.QVBoxLayout(self) 248 | if len(addons) == 1: 249 | layout.addWidget(Qt.QLabel(self.tr("Updating the addon..."))) 250 | else: 251 | layout.addWidget(Qt.QLabel(self.tr("Updating the addons..."))) 252 | self.progress = Qt.QProgressBar(self) 253 | self.progress.setRange(0, len(addons)) 254 | self.progress.setValue(0) 255 | self.progress.setFormat("%v / %m | %p%") 256 | self.wowVersion = wowVersion 257 | layout.addWidget(self.progress) 258 | self.addons = addons 259 | self.maxThreads = int(settings.value(defines.LCURSE_MAXTHREADS_KEY, defines.LCURSE_MAXTHREADS_DEFAULT)) 260 | self.sem = Qt.QSemaphore(self.maxThreads) 261 | 262 | def startWorkerThreads(self): 263 | self.threads = [] 264 | for addon in self.addons: 265 | self.sem.acquire() 266 | thread = UpdateWorker(self.wowVersion, addon) 267 | thread.updateFinished.connect(self.onUpdateFinished) 268 | thread.start() 269 | self.threads.append(thread) 270 | 271 | def exec_(self): 272 | start_new_thread(self.startWorkerThreads, ()) 273 | super(UpdateDlg, self).exec_() 274 | 275 | @Qt.pyqtSlot(Qt.QVariant, bool) 276 | def onUpdateFinished(self, addon, result): 277 | self.sem.release() 278 | value = self.progress.value() + 1 279 | self.progress.setValue(value) 280 | self.updateFinished.emit(addon, result) 281 | if value == self.progress.maximum(): 282 | self.close() 283 | 284 | 285 | class UpdateWorker(Qt.QThread): 286 | updateFinished = Qt.pyqtSignal(Qt.QVariant, bool) 287 | 288 | def __init__(self, wowVersion, addon): 289 | super(UpdateWorker, self).__init__() 290 | self.wowVersion = wowVersion 291 | self.addon = addon 292 | 293 | def doUpdateGit(self): 294 | try: 295 | settings = Qt.QSettings() 296 | dest = "{}/_{}_/Interface/AddOns".format(settings.value(defines.WOW_FOLDER_KEY, defines.WOW_FOLDER_DEFAULT), self.wowVersion) 297 | destAddon = "{}/{}".format(dest, os.path.basename(str(self.addon[2]))[:-4]) 298 | if not os.path.exists(destAddon): 299 | os.chdir(dest) 300 | check_call(["git", "clone", self.addon[2]]) 301 | else: 302 | os.chdir(destAddon) 303 | check_call(["git", "pull"]) 304 | toc = None 305 | for filename in os.listdir(destAddon): 306 | if filename[-4:] == '.toc': 307 | toc = '{}/{}'.format(destAddon, filename) 308 | break 309 | return True, toc 310 | except Exception as e: 311 | print("DoGitUpdate",e) 312 | return False 313 | 314 | def doUpdateCurse(self): 315 | try: 316 | settings = Qt.QSettings() 317 | response = OpenWithRetry(self.addon[5][1]) 318 | dest = "{}/_{}_/Interface/AddOns/".format(settings.value(defines.WOW_FOLDER_KEY, defines.WOW_FOLDER_DEFAULT), self.wowVersion) 319 | 320 | with tempfile.NamedTemporaryFile('w+b') as zipped: 321 | zipped.write(response.content) 322 | zipped.seek(0) 323 | with zipfile.ZipFile(zipped, 'r') as z: 324 | r=re.compile(".*\.toc$") 325 | r2=re.compile("[\\/]") 326 | tocs=filter(r.match,z.namelist()) 327 | for nome in list(tocs): 328 | t=r2.split(nome) 329 | if len(t) == 2: 330 | break 331 | toc="{}/_{}_/Interface/AddOns/{}".format(settings.value(defines.WOW_FOLDER_KEY, defines.WOW_FOLDER_DEFAULT), self.wowVersion, nome) 332 | z.extractall(dest) 333 | return True, toc 334 | except Exception as e: 335 | print("DoCurseUpdate",e) 336 | raise e 337 | return False 338 | 339 | def run(self): 340 | if "curseforge.com" in self.addon[2]: 341 | result,toc = self.doUpdateCurse() 342 | elif self.addon[2].endswith(".git"): 343 | result,toc = self.doUpdateGit() 344 | else: 345 | result=False 346 | toc="n/a" 347 | self.updateFinished.emit(self.addon + (toc,), result) 348 | 349 | 350 | class UpdateCatalogDlg(Qt.QDialog): 351 | updateCatalogFinished = Qt.pyqtSignal(Qt.QVariant) 352 | 353 | def __init__(self, parent): 354 | super(UpdateCatalogDlg, self).__init__(parent) 355 | layout = Qt.QVBoxLayout(self) 356 | layout.addWidget(Qt.QLabel(self.tr("Updating list of available addons..."))) 357 | self.progress = Qt.QProgressBar(self) 358 | self.progress.setRange(0, 0) 359 | self.progress.setValue(0) 360 | layout.addWidget(self.progress) 361 | 362 | def exec_(self): 363 | self.thread = UpdateCatalogWorker() 364 | self.thread.updateCatalogFinished.connect(self.onUpdateCatalogFinished) 365 | self.thread.retrievedLastpage.connect(self.setMaxProgress) 366 | self.thread.progress.connect(self.onProgress) 367 | self.thread.start() 368 | super(UpdateCatalogDlg, self).exec_() 369 | 370 | @Qt.pyqtSlot(int) 371 | def setMaxProgress(self, maxval): 372 | self.progress.setRange(0, maxval) 373 | 374 | @Qt.pyqtSlot(int) 375 | def onProgress(self, foundAddons): 376 | value = self.progress.value() + 1 377 | self.progress.setValue(value) 378 | self.progress.setFormat(self.tr("%p% - found addons: {}").format(foundAddons)) 379 | 380 | @Qt.pyqtSlot(Qt.QVariant) 381 | def onUpdateCatalogFinished(self, addons): 382 | self.updateCatalogFinished.emit(addons) 383 | self.close() 384 | 385 | 386 | class UpdateCatalogWorker(Qt.QThread): 387 | updateCatalogFinished = Qt.pyqtSignal(Qt.QVariant) 388 | retrievedLastpage = Qt.pyqtSignal(int) 389 | progress = Qt.pyqtSignal(int) 390 | 391 | def __init__(self): 392 | super(UpdateCatalogWorker, self).__init__() 393 | settings = Qt.QSettings() 394 | self.addons = [] 395 | self.addonsMutex = Qt.QMutex() 396 | self.maxThreads = int(settings.value(defines.LCURSE_MAXTHREADS_KEY, defines.LCURSE_MAXTHREADS_DEFAULT)) 397 | self.sem = Qt.QSemaphore(self.maxThreads) 398 | self.lastpage = 1 399 | 400 | def retrievePartialListOfAddons(self, page): 401 | response = OpenWithRetry("https://www.curseforge.com/wow/addons?page={}".format(page)) 402 | soup = BeautifulSoup(response.content, "lxml") 403 | # Curse returns a soft-500 404 | if soup.find_all("h2", string="Error"): 405 | print(self.tr("Server-side error while getting addon list.")) 406 | 407 | lastpage = 1 408 | if page == 1: 409 | pager = soup.select("a.pagination-item span") 410 | if pager: 411 | lastpage = int(pager[len(pager) - 1].contents[0]) 412 | 413 | projects = soup.select("div.project-listing-row") 414 | self.addonsMutex.lock() 415 | for project in projects: 416 | links=project.select("a.button--hollow") 417 | texts=project.select("a h3") 418 | for text in texts: 419 | nome=text.string.replace('\\r','').replace('\\n','').strip() 420 | break 421 | for link in links: 422 | href=link.get("href", link.get("data-normal-href")).replace("/woW/", "/wow/").replace("/download",'') 423 | break 424 | self.addons.append([nome, "https://www.curseforge.com{}".format(href)]) 425 | self.progress.emit(len(self.addons)) 426 | self.addonsMutex.unlock() 427 | 428 | self.sem.release() 429 | 430 | return lastpage 431 | 432 | def retrieveListOfAddons(self): 433 | page = 1 434 | lastpage = 1 435 | self.sem.acquire() 436 | lastpage = self.retrievePartialListOfAddons(page) 437 | page += 1 438 | self.retrievedLastpage.emit(lastpage) 439 | 440 | while page <= lastpage: 441 | self.sem.acquire() 442 | start_new_thread(self.retrievePartialListOfAddons, (page,)) 443 | page += 1 444 | 445 | def run(self): 446 | self.retrieveListOfAddons() 447 | 448 | # wait until all worker are done 449 | self.sem.acquire(self.maxThreads) 450 | 451 | self.updateCatalogFinished.emit(self.addons) 452 | -------------------------------------------------------------------------------- /translations/de_DE.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | AddAddonDlg 5 | 6 | 7 | Type name or url of the addon you want to add: 8 | Gib den Namen oder die URL des Addons ein, welches du installieren möchtest: 9 | 10 | 11 | 12 | No addon catalog data 13 | Keine Addonkatalog Daten 14 | 15 | 16 | 17 | You haven't updated the available addons catalog, so you need to insert a URL for the addon you want to add. 18 | Du hast den Katalog von verfügbaren Addons noch nie upgedatet. Deswegen musst du die URL des Addons angeben, welches du installieren willst. 19 | 20 | 21 | 22 | CacheDecorator 23 | 24 | 25 | Could not open '{}', retrying... ({}) 26 | Konnte '{}' nicht offnen, wird wiederholt... ({}) 27 | 28 | 29 | 30 | CheckDlg 31 | 32 | 33 | Verifying which addon needs an update... 34 | Überprüfe welches Addon ein Update benötigt... 35 | 36 | 37 | 38 | Verifying if the addon needs an update... 39 | Überprüfe ob das Addon ein Update benötigt... 40 | 41 | 42 | 43 | Cancel 44 | Abbrechen 45 | 46 | 47 | 48 | CheckWorker 49 | 50 | 51 | Git Update Exception 52 | Git-Aktualisierungsfehler 53 | 54 | 55 | 56 | Curse Update Exception 57 | Curse-Aktualisierungsfehler 58 | 59 | 60 | 61 | Grid 62 | 63 | 64 | multiple addons 65 | mehrere Addons 66 | 67 | 68 | 69 | Context menu for {} 70 | Kontextmenü für 71 | 72 | 73 | 74 | Update addon 75 | Aktuelles Addon updaten 76 | 77 | 78 | 79 | Update currently selected addons if needed 80 | Update das aktuell selektierte Addon wenn eine neuere Version verfügbar ist 81 | 82 | 83 | 84 | Force update addon 85 | Addon update forcieren 86 | 87 | 88 | 89 | Unconditionally update currently selected addons 90 | Update das aktuell selektierte Addon unbedingt 91 | 92 | 93 | 94 | Remove addon from list 95 | Addon aus Liste entfernen 96 | 97 | 98 | 99 | Leave all files unaltered, useful for subaddons 100 | Lasse alle Dateien unverändert, nützlich für Sub-Addons 101 | 102 | 103 | 104 | MainWidget 105 | 106 | 107 | Load addons 108 | Addons Laden 109 | 110 | 111 | 112 | Re/Load your addon configuration 113 | Lade deine Addon Konfiguration 114 | 115 | 116 | 117 | Save addons 118 | Addons Speichern 119 | 120 | 121 | 122 | Save your addon configuration 123 | Speichere deine Addons Konfiguration 124 | 125 | 126 | 127 | Preferences 128 | Einstellungen 129 | 130 | 131 | 132 | Change preferences, like WoW install folder 133 | Ändere Einstellungen, wie z.B. WoW Installations Verzeichnis 134 | 135 | 136 | 137 | Exit 138 | Beenden 139 | 140 | 141 | 142 | Exit application 143 | Beende die Anwendung 144 | 145 | 146 | 147 | Addons 148 | Addons 149 | 150 | 151 | 152 | Check all addons 153 | Alle Addons überprüfen 154 | 155 | 156 | 157 | Check all addons for new version 158 | Überprüfe alle Addons auf verfügbare neuere Version 159 | 160 | 161 | 162 | Check addon 163 | Aktuelles Addon überprüfen 164 | 165 | 166 | 167 | Check currently selected addon for new version 168 | Überprüfe das aktuell selektierte Addon ob eine neue Version verfügbar ist 169 | 170 | 171 | 172 | Update all addons 173 | Alle Addons updaten 174 | 175 | 176 | 177 | Update all addons which need an update 178 | Update alle Addons, für die ein Update zur Verfügung steht 179 | 180 | 181 | 182 | Update addon 183 | Aktuelles Addon updaten 184 | 185 | 186 | 187 | Update currently selected addons if needed 188 | Update das aktuell selektierte Addon wenn eine neuere Version verfügbar ist 189 | 190 | 191 | 192 | Add addon 193 | Addon hinzufügen 194 | 195 | 196 | 197 | Add a new addon 198 | Füge ein neues Addon hinzu 199 | 200 | 201 | 202 | Remove addon 203 | Addon entfernen 204 | 205 | 206 | 207 | Remove currently selected addon 208 | Entfernt das aktuell selektierte Addon 209 | 210 | 211 | 212 | Remove selected addon 213 | Entferne das selektierte Addon 214 | 215 | 216 | 217 | General 218 | Allgemein 219 | 220 | 221 | 222 | Import addons 223 | Addons importieren 224 | 225 | 226 | 227 | Import addons from WoW installation 228 | Importiere Addons aus der WoW Installation 229 | 230 | 231 | 232 | Ready 233 | Fertig 234 | 235 | 236 | 237 | Update catalog 238 | Katalog updaten 239 | 240 | 241 | 242 | Retrieve a list of available addons 243 | Erhalte eine Liste der verfügbaren Addons 244 | 245 | 246 | 247 | Catalog 248 | Katalog 249 | 250 | 251 | 252 | lcurse-folder not a folder 253 | lcurse-Ordner ist kein Ordner 254 | 255 | 256 | 257 | There is an entry ".lcurse" in your home directory which is neither a folder nor a link to a folder. Exiting! 258 | Es gibt einen ".lcurse" Eintrag in deinem Home Verzeichnis, der weder ein Verzeichnis noch ein Link auf ein Verzeichnis ist. lCurse wird beendet! 259 | 260 | 261 | 262 | Do you really want to remove the following addon? 263 | {} 264 | Willst du wirklich das folgende Addon entfernen? 265 | {} 266 | 267 | 268 | 269 | Force update addon 270 | Addon update forcieren 271 | 272 | 273 | 274 | Force update of currently selected addon 275 | Forciert das Update des aktuell selektierte Addon 276 | 277 | 278 | 279 | No addons matching "{}" found. 280 | The addon might already be removed, or could be going under a different name. 281 | Manual deletion may be required. 282 | Es konnte kein Addon gefunden werden, dass "{}" entspricht. 283 | Kann sein, dass das Addon schon entfernt wurde oder es kann unter einem anderen Namen installiert sein. 284 | Eine manuelle Entfernung könnte nötig sein. 285 | 286 | 287 | 288 | Remove the following addons as well? 289 | {} 290 | Sollen die folgenden Addons auch entfernt werden? 291 | {} 292 | 293 | 294 | 295 | Error messages 296 | Fehlermeldungen 297 | 298 | 299 | 300 | Remove selected addon information 301 | Ausgewählte Addon-Informationen entfernen 302 | 303 | 304 | 305 | Clear specific addon information 306 | Bestimmte Addon-Informationen löschen 307 | 308 | 309 | 310 | Remove addon from list 311 | Addon aus Liste entfernen 312 | 313 | 314 | 315 | Leave all files unaltered, useful for subaddons 316 | Lasse alle Dateien unverändert, nützlich für Sub-Addons 317 | 318 | 319 | 320 | WoW Version 321 | WoW-Version 322 | 323 | 324 | 325 | not enough information found for addon {} 326 | (version={},name={},toc={}) 327 | 328 | Zu wenig Informationen verfügbar für Addon {} 329 | (version={},name={},toc={}) 330 | 331 | 332 | 333 | Warning, old database, will convert 334 | Warnung, alte Datenbank, wird konvertiert 335 | 336 | 337 | 338 | Saving addons to {} 339 | Addons werden gespeichert in {} 340 | 341 | 342 | 343 | can't handle: {} 344 | Nicht unterstützt: {} 345 | 346 | 347 | 348 | retrieving addon information 349 | Addon-Informationen werden erhaltet 350 | 351 | 352 | 353 | www.curseforge.com layout has changed. 354 | www.curseforge.com-Layout hat sich geändert 355 | 356 | 357 | 358 | Db version is {} vs {} 359 | Db-Version ist {} vs {} 360 | 361 | 362 | 363 | Database update! 364 | Datenbank-Update! 365 | 366 | 367 | 368 | Current Row: {0:d} 369 | Aktuelle Reihe: {0:d} 370 | 371 | 372 | 373 | retrieved list of addons: {} 374 | Erhaltete Addons: {} 375 | 376 | 377 | 378 | Name 379 | Name 380 | 381 | 382 | 383 | Url 384 | Url 385 | 386 | 387 | 388 | Version 389 | Version 390 | 391 | 392 | 393 | TOC 394 | TOC 395 | 396 | 397 | 398 | Allow Beta 399 | Beta zulassen 400 | 401 | 402 | 403 | PreferencesDlg 404 | 405 | 406 | WoW Install Folder: 407 | WoW Installations Verzeichnis: 408 | 409 | 410 | 411 | ... 412 | ... 413 | 414 | 415 | 416 | Save 417 | Speichern 418 | 419 | 420 | 421 | Cancel 422 | Abbrechen 423 | 424 | 425 | 426 | Select Wow Install Folder 427 | Wählen dein WoW Installations Verzeichnis aus 428 | 429 | 430 | 431 | Not Wow-Folder 432 | Kein WoW Verzeichnis 433 | 434 | 435 | 436 | The selected folder is not a WoW installation directory. 437 | Please select the WoW folder 438 | Das ausgewählte Verzeichnis ist kein WoW Installations Verzeichnis. 439 | Bitte wähle das WoW Verzeichnis aus 440 | 441 | 442 | 443 | Max. concurrent Threads: 444 | Max. Anzahl gleichzeitiger Threads: 445 | 446 | 447 | 448 | Current TOC Number: 449 | Aktuelle TOC-Nummer 450 | 451 | 452 | 453 | UpdateCatalogDlg 454 | 455 | 456 | Updating list of available addons... 457 | Die Liste der verfügbaren Addons wird upgedatet... 458 | 459 | 460 | 461 | %p% - found addons: {} 462 | %p% - gefundene Addons: {} 463 | 464 | 465 | 466 | UpdateCatalogWorker 467 | 468 | 469 | Server-side error while getting addon list. 470 | Serverseitiger Fehler beim erhalten der Addon-Liste. 471 | 472 | 473 | 474 | UpdateDlg 475 | 476 | 477 | Updating the addons... 478 | Die Addons wird upgedatet... 479 | 480 | 481 | 482 | Updating the addon... 483 | Das Addon wird upgedatet... 484 | 485 | 486 | 487 | -------------------------------------------------------------------------------- /translations/fr_FR.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | AddAddonDlg 5 | 6 | 7 | Type name or url of the addon you want to add: 8 | Donnez le nom ou l'URL de l'addon que vous souhaitez ajouter : 9 | 10 | 11 | 12 | No addon catalog data 13 | Catalogue des addons absent 14 | 15 | 16 | 17 | You haven't updated the available addons catalog, so you need to insert a URL for the addon you want to add. 18 | Vous n'avez pas mis à jour le catalogue des addons disponibles, donnez l'URL de l'addon que vous souhaitez ajouter. 19 | 20 | 21 | 22 | CacheDecorator 23 | 24 | 25 | Could not open '{}', retrying... ({}) 26 | Ne peut ouvrir '{}', réessayé... ({}) 27 | 28 | 29 | 30 | CheckDlg 31 | 32 | 33 | Verifying which addon needs an update... 34 | Vérifiez si ces addons nécessitent une mise à jour... 35 | 36 | 37 | 38 | Verifying if the addon needs an update... 39 | Vérifiez si cet addon nécessite une mise à jour... 40 | 41 | 42 | 43 | Cancel 44 | Annuler 45 | 46 | 47 | 48 | CheckWorker 49 | 50 | 51 | Git Update Exception 52 | Exception de mise à jour de Git 53 | 54 | 55 | 56 | Curse Update Exception 57 | Exception de mise à jour de Curse 58 | 59 | 60 | 61 | Grid 62 | 63 | 64 | multiple addons 65 | plusieurs addons 66 | 67 | 68 | 69 | Context menu for {} 70 | Menu contextuel de {} 71 | 72 | 73 | 74 | Update addon 75 | Mise à jour addon 76 | 77 | 78 | 79 | Update currently selected addons if needed 80 | Mise à jour des addons sélectionnés si nécessaire 81 | 82 | 83 | 84 | Force update addon 85 | Forcer la mise à jour addon 86 | 87 | 88 | 89 | Unconditionally update currently selected addons 90 | Mise à jour forcée des addons sélectionnés 91 | 92 | 93 | 94 | Remove addon from list 95 | Supprimer addon de la liste 96 | 97 | 98 | 99 | Leave all files unaltered, useful for subaddons 100 | Laisser tous les fichiers inchangés, utile pour les sous-addons 101 | 102 | 103 | 104 | MainWidget 105 | 106 | 107 | Load addons 108 | Importer les addons déjà présent 109 | 110 | 111 | 112 | Re/Load your addon configuration 113 | Re/Charger votre configuration des addons 114 | 115 | 116 | 117 | Save addons 118 | Sauver les addons 119 | 120 | 121 | 122 | Save your addon configuration 123 | Sauver votre configuration des addons 124 | 125 | 126 | 127 | Preferences 128 | Préférences 129 | 130 | 131 | 132 | Change preferences, like WoW install folder 133 | Modifier les préférences, dossier d'installation WoW par exemple 134 | 135 | 136 | 137 | Exit 138 | Terminer 139 | 140 | 141 | 142 | Exit application 143 | Sortir de l'application 144 | 145 | 146 | 147 | Addons 148 | Addons 149 | 150 | 151 | 152 | Check all addons 153 | Vérifier tous les addons 154 | 155 | 156 | 157 | Check all addons for new version 158 | Vérifier tous les addons pour nouvelle version 159 | 160 | 161 | 162 | Check addon 163 | Vérifier addon 164 | 165 | 166 | 167 | Check currently selected addon for new version 168 | Vérifier addon sélectionné pour nouvelle version 169 | 170 | 171 | 172 | Update all addons 173 | Mise à jour de tous les addons 174 | 175 | 176 | 177 | Update all addons which need an update 178 | Mise à jour de tous les addons, si nécessaire 179 | 180 | 181 | 182 | Update addon 183 | Mise à jour addon 184 | 185 | 186 | 187 | Update currently selected addons if needed 188 | Mise à jour des addons sélectionnés, si nécessaire 189 | 190 | 191 | 192 | Add addon 193 | Ajouter addon 194 | 195 | 196 | 197 | Add a new addon 198 | Ajouter nouvel addon 199 | 200 | 201 | 202 | Remove addon 203 | Supprimer addon 204 | 205 | 206 | 207 | Remove currently selected addon 208 | Supprimer addon sélectionné 209 | 210 | 211 | 212 | Remove selected addon 213 | Supprimer addon sélectionné 214 | 215 | 216 | 217 | General 218 | Général 219 | 220 | 221 | 222 | Import addons 223 | Importer les addons déjà présent 224 | 225 | 226 | 227 | Import addons from WoW installation 228 | Importer les addons déjà présent à partir du dossier d'installation de WoW 229 | 230 | 231 | 232 | Ready 233 | Prêt 234 | 235 | 236 | 237 | Update catalog 238 | Mise à jour catalogue 239 | 240 | 241 | 242 | Retrieve a list of available addons 243 | Extraire une liste des addons disponibles 244 | 245 | 246 | 247 | Catalog 248 | Catalogue 249 | 250 | 251 | 252 | lcurse-folder not a folder 253 | dossier lcurse n'est pas un dossier 254 | 255 | 256 | 257 | There is an entry ".lcurse" in your home directory which is neither a folder nor a link to a folder. Exiting! 258 | Il y a une entrée ".lcurse" dans le répertoire personnel qui n'est ni un dossier ni un lien vers un dossier. Sortir ! 259 | 260 | 261 | 262 | Do you really want to remove the following addon? 263 | {} 264 | Souhaitez-vous vraiment supprimer cet addon ? 265 | {} 266 | 267 | 268 | 269 | Force update addon 270 | Forcer la mise à jour addon 271 | 272 | 273 | 274 | Force update of currently selected addon 275 | Forcer mise à jour addon sélectionné 276 | 277 | 278 | 279 | No addons matching "{}" found. 280 | The addon might already be removed, or could be going under a different name. 281 | Manual deletion may be required. 282 | Aucun addons trouvés qui correspondent à "{}". 283 | Addon peut-être déjà supprimé, ou sous un autre nom. 284 | Une suppression manuelle peut être requise. 285 | 286 | 287 | 288 | Remove the following addons as well? 289 | {} 290 | Supprimez également les addons suivants ? 291 | {} 292 | 293 | 294 | 295 | Error messages 296 | Messages d'erreur 297 | 298 | 299 | 300 | Remove selected addon information 301 | Supprimer les informations sélectionnées de l'addon 302 | 303 | 304 | 305 | Clear specific addon information 306 | Effacer les informations specifiques de l'addon 307 | 308 | 309 | 310 | Remove addon from list 311 | Supprimer addon de la liste 312 | 313 | 314 | 315 | Leave all files unaltered, useful for subaddons 316 | Laisser tous les fichiers inchangés, utile pour les sous-addons 317 | 318 | 319 | 320 | WoW Version 321 | Version WoW 322 | 323 | 324 | 325 | not enough information found for addon {} 326 | (version={},name={},toc={}) 327 | 328 | Informations insuffisantes concernant l'addon {} 329 | (version={},name={},toc={}) 330 | 331 | 332 | 333 | Warning, old database, will convert 334 | Attention, ancienne base de données, va être convertie 335 | 336 | 337 | 338 | Saving addons to {} 339 | Sauvegarde des addons vers {} 340 | 341 | 342 | 343 | can't handle: {} 344 | ne supporte pas : {} 345 | 346 | 347 | 348 | retrieving addon information 349 | récupération des informations de l'addon 350 | 351 | 352 | 353 | www.curseforge.com layout has changed. 354 | la structure de www.curseforge.com a été modifiée. 355 | 356 | 357 | 358 | Db version is {} vs {} 359 | Version de la Base {} versus {} 360 | 361 | 362 | 363 | Database update! 364 | Mise à jour Base de Données ! 365 | 366 | 367 | 368 | Current Row: {0:d} 369 | Rangée actuelle: {0:d} 370 | 371 | 372 | 373 | retrieved list of addons: {} 374 | nombre addons référencés : {} 375 | 376 | 377 | 378 | Name 379 | Nom 380 | 381 | 382 | 383 | Url 384 | Url 385 | 386 | 387 | 388 | Version 389 | Version 390 | 391 | 392 | 393 | TOC 394 | TOC 395 | 396 | 397 | 398 | Allow Beta 399 | Bêta permis 400 | 401 | 402 | 403 | PreferencesDlg 404 | 405 | 406 | WoW Install Folder: 407 | Dossier d'installation WoW : 408 | 409 | 410 | 411 | ... 412 | ... 413 | 414 | 415 | 416 | Save 417 | Sauvegarder 418 | 419 | 420 | 421 | Cancel 422 | Annuler 423 | 424 | 425 | 426 | Select Wow Install Folder 427 | Sélectionner le répertoire d'installation WoW 428 | 429 | 430 | 431 | Not Wow-Folder 432 | Pas un dossier d'installation WoW 433 | 434 | 435 | 436 | The selected folder is not a WoW installation directory. 437 | Please select the WoW folder 438 | Le dossier sélectionné n'est pas un dossier d'installation WoW. 439 | Veuillez sélectionner le dossier WoW 440 | 441 | 442 | 443 | Max. concurrent Threads: 444 | Threads (fils) simultanés maximum : 445 | 446 | 447 | 448 | Current TOC Number: 449 | Numéro TOC actuel : 450 | 451 | 452 | 453 | UpdateCatalogDlg 454 | 455 | 456 | Updating list of available addons... 457 | Mise à jour de la liste des addons disponibles... 458 | 459 | 460 | 461 | %p% - found addons: {} 462 | %p% - addons trouvés : {} 463 | 464 | 465 | 466 | UpdateCatalogWorker 467 | 468 | 469 | Server-side error while getting addon list. 470 | Erreur coté serveur pendant la création de la liste des addons. 471 | 472 | 473 | 474 | UpdateDlg 475 | 476 | 477 | Updating the addons... 478 | Mise à jour des addons... 479 | 480 | 481 | 482 | Updating the addon... 483 | Mise à jour addon... 484 | 485 | 486 | 487 | -------------------------------------------------------------------------------- /modules/application.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import sys 5 | import json 6 | import os 7 | import re 8 | from shutil import rmtree 9 | import urllib 10 | from urllib.parse import urlparse, quote as urlquote 11 | from urllib.request import build_opener, HTTPCookieProcessor, HTTPError 12 | from http import cookiejar 13 | from bs4 import BeautifulSoup 14 | 15 | from PyQt5 import Qt 16 | 17 | from modules import preferences 18 | from modules import addaddondlg 19 | from modules import waitdlg 20 | from modules import defines 21 | #from PyQt4.uic.Compiler.qtproxies import QtGui 22 | from PyQt5 import QtGui 23 | 24 | opener = build_opener(HTTPCookieProcessor(cookiejar.CookieJar())) 25 | # default User-Agent ('Python-urllib/2.6') will *not* work 26 | opener.addheaders = [('User-Agent', 'Mozilla/5.0'), ] 27 | 28 | class Grid(Qt.QTableWidget): 29 | def __init__(self, parent=None): 30 | self.parent = parent 31 | super().__init__(parent.mainWidget) 32 | self.setSelectionBehavior(Qt.QAbstractItemView.SelectRows) 33 | 34 | def contextMenuEvent(self,event): 35 | self.menu = Qt.QMenu(self) 36 | rows=self.currentRows() 37 | if len(rows) == 1: 38 | name = self.item(rows[0], 0).text() 39 | else: 40 | name = self.tr("multiple addons") 41 | self.menu.addAction(self.tr("Context menu for {}").format(name)) 42 | actionUpdate = Qt.QAction(self.tr("Update addon"), self) 43 | actionUpdate.setStatusTip(self.tr("Update currently selected addons if needed")) 44 | actionUpdate.triggered.connect(self.parent.updateAddons) 45 | self.menu.addAction(actionUpdate) 46 | actionForceUpdate = Qt.QAction(self.tr("Force update addon"), self) 47 | actionForceUpdate.setStatusTip(self.tr("Unconditionally update currently selected addons")) 48 | actionForceUpdate.triggered.connect(self.parent.forceUpdateAddon) 49 | self.menu.addAction(actionForceUpdate) 50 | actionRemovefromlist = Qt.QAction(self.tr("Remove addon from list"),self) 51 | actionRemovefromlist.setStatusTip(self.tr("Leave all files unaltered, useful for subaddons")) 52 | actionRemovefromlist.triggered.connect(self.parent.removeFromList) 53 | self.menu.addAction(actionRemovefromlist) 54 | self.menu.popup(Qt.QCursor.pos()) 55 | 56 | def currentRows(self): 57 | rows = [] 58 | for row in self.selectionModel().selectedRows(): 59 | rows.append(row.row()) 60 | rows.sort() 61 | return rows 62 | 63 | 64 | class MainWidget(Qt.QMainWindow): 65 | def __init__(self): 66 | super(MainWidget, self).__init__() 67 | self.ensureLCurseFolder() 68 | self.setActiveWowVersion(defines.WOW_VERSION_DEFAULT) 69 | self.addWidgets() 70 | 71 | self.addons = [] 72 | self.loadAddons() 73 | 74 | self.availableAddons = [] 75 | self.loadAddonCatalog() 76 | 77 | def getWowToc(self): 78 | settings = Qt.QSettings() 79 | try: 80 | buildinfo="{}/.build.info".format(settings.value(defines.WOW_FOLDER_KEY, defines.WOW_FOLDER_DEFAULT)) 81 | with open(buildinfo, encoding="utf8", errors='replace') as f: 82 | line = f.readline() 83 | if self.wowVersion == 'retail': 84 | wowVersion = 'wow' 85 | else: 86 | wowVersion = 'wow_{}'.format(self.wowVersion) 87 | while True: 88 | line = f.readline() 89 | if line: 90 | line = line.strip().split('|') 91 | if line[13] == wowVersion: 92 | version = line[12] 93 | break 94 | else: 95 | break 96 | f.close() 97 | v=version.split('.') 98 | if self.wowVersion == 'classic': 99 | return str(int(v[0])*10000 + int(v[1])*100 + int(v[2])) 100 | else: 101 | return str(int(v[0])*10000 + int(v[1])*100) 102 | except Exception as e: 103 | return settings.value(defines.WOW_TOC_KEY,defines.TOC) 104 | print(self.tr("Error messages"),e) 105 | 106 | def addWidgets(self): 107 | self.mainWidget = Qt.QWidget(self) 108 | box = Qt.QVBoxLayout(self.mainWidget) 109 | 110 | menubar = self.menuBar() 111 | 112 | actionLoad = Qt.QAction(self.tr("Load addons"), self) 113 | actionLoad.setShortcut("Ctrl+L") 114 | actionLoad.setStatusTip(self.tr("Re/Load your addon configuration")) 115 | actionLoad.triggered.connect(self.loadAddons) 116 | 117 | actionSave = Qt.QAction(self.tr("Save addons"), self) 118 | actionSave.setShortcut("Ctrl+S") 119 | actionSave.setStatusTip(self.tr("Save your addon configuration")) 120 | actionSave.triggered.connect(self.saveAddons) 121 | 122 | actionImport = Qt.QAction(self.tr("Import addons"), self) 123 | actionImport.setStatusTip(self.tr("Import addons from WoW installation")) 124 | actionImport.triggered.connect(self.importAddons) 125 | 126 | actionPrefs = Qt.QAction(self.tr("Preferences"), self) 127 | actionPrefs.setShortcut("Ctrl+P") 128 | actionPrefs.setStatusTip(self.tr("Change preferences, like WoW install folder")) 129 | actionPrefs.triggered.connect(self.openPreferences) 130 | 131 | actionExit = Qt.QAction(self.tr("Exit"), self) 132 | actionExit.setShortcuts(Qt.QKeySequence.Quit) 133 | actionExit.setStatusTip(self.tr("Exit application")) 134 | actionExit.triggered.connect(self.close) 135 | 136 | actionClearCell = Qt.QAction(self.tr("Remove selected addon information"),self) 137 | actionClearCell.setShortcut("Backspace") 138 | actionClearCell.setStatusTip(self.tr("Clear specific addon information")) 139 | actionClearCell.triggered.connect(self.clearCell) 140 | 141 | menuFile = menubar.addMenu(self.tr("General")) 142 | menuFile.addAction(actionLoad) 143 | menuFile.addAction(actionSave) 144 | menuFile.addAction(actionImport) 145 | menuFile.addSeparator() 146 | menuFile.addAction(actionPrefs) 147 | menuFile.addSeparator() 148 | menuFile.addAction(actionExit) 149 | self.addAction(actionLoad) 150 | self.addAction(actionSave) 151 | self.addAction(actionPrefs) 152 | self.addAction(actionExit) 153 | self.addAction(actionClearCell) 154 | 155 | actionCheckAll = Qt.QAction(self.tr("Check all addons"), self) 156 | actionCheckAll.setShortcut('Ctrl+Shift+A') 157 | actionCheckAll.setStatusTip(self.tr("Check all addons for new version")) 158 | actionCheckAll.triggered.connect(self.checkAllAddonsForUpdate) 159 | 160 | actionCheck = Qt.QAction(self.tr("Check addon"), self) 161 | actionCheck.setShortcut('Ctrl+A') 162 | actionCheck.setStatusTip(self.tr("Check currently selected addon for new version")) 163 | actionCheck.triggered.connect(self.checkAddonsForUpdate) 164 | 165 | actionUpdateAll = Qt.QAction(self.tr("Update all addons"), self) 166 | actionUpdateAll.setShortcut("Ctrl+Shift+U") 167 | actionUpdateAll.setStatusTip(self.tr("Update all addons which need an update")) 168 | actionUpdateAll.triggered.connect(self.updateAllAddons) 169 | 170 | actionUpdate = Qt.QAction(self.tr("Update addon"), self) 171 | actionUpdate.setShortcut("Ctrl+U") 172 | actionUpdate.setStatusTip(self.tr("Update currently selected addons if needed")) 173 | actionUpdate.triggered.connect(self.updateAddons) 174 | 175 | actionRemovefromlist = Qt.QAction(self.tr("Remove addon from list"),self) 176 | actionRemovefromlist.setShortcut(Qt.QKeySequence.Delete) 177 | actionRemovefromlist.setStatusTip(self.tr("Leave all files unaltered, useful for subaddons")) 178 | actionRemovefromlist.triggered.connect(self.removeFromList) 179 | 180 | actionAdd = Qt.QAction(self.tr("Add addon"), self) 181 | actionAdd.setStatusTip(self.tr("Add a new addon")) 182 | actionAdd.triggered.connect(self.addAddon) 183 | 184 | actionRemove = Qt.QAction(self.tr("Remove addon"), self) 185 | actionRemove.setShortcut("Shift+Del") 186 | actionRemove.setStatusTip(self.tr("Remove currently selected addon")) 187 | actionRemove.triggered.connect(self.removeAddon) 188 | 189 | actionForceUpdate = Qt.QAction(self.tr("Force update addon"), self) 190 | actionForceUpdate.setShortcut("Ctrl+F") 191 | actionForceUpdate.setStatusTip(self.tr("Force update of currently selected addon")) 192 | actionForceUpdate.triggered.connect(self.forceUpdateAddon) 193 | 194 | menuAddons = menubar.addMenu(self.tr("Addons")) 195 | menuAddons.addAction(actionCheckAll) 196 | menuAddons.addAction(actionCheck) 197 | menuAddons.addSeparator() 198 | menuAddons.addAction(actionUpdateAll) 199 | menuAddons.addAction(actionUpdate) 200 | menuAddons.addAction(actionRemovefromlist) 201 | menuAddons.addSeparator() 202 | menuAddons.addAction(actionAdd) 203 | menuAddons.addAction(actionRemove) 204 | menuAddons.addAction(actionForceUpdate) 205 | toolbar = self.addToolBar(self.tr("Addons")) 206 | toolbar.addAction(actionUpdateAll) 207 | toolbar.addAction(actionAdd) 208 | self.addAction(actionCheckAll) 209 | self.addAction(actionCheck) 210 | self.addAction(actionUpdateAll) 211 | self.addAction(actionUpdate) 212 | self.addAction(actionForceUpdate) 213 | 214 | actionCatalogUpdate = Qt.QAction(self.tr("Update catalog"), self) 215 | actionCatalogUpdate.setStatusTip(self.tr("Retrieve a list of available addons")) 216 | actionCatalogUpdate.triggered.connect(self.updateCatalog) 217 | menuCatalog = menubar.addMenu(self.tr("Catalog")) 218 | menuCatalog.addAction(actionCatalogUpdate) 219 | toolbar = self.addToolBar(self.tr("Catalog")) 220 | toolbar.addAction(actionCatalogUpdate) 221 | 222 | wowVersions = self.getWowVersions() 223 | if len(wowVersions) > 1: 224 | wowVersionSelector = Qt.QComboBox() 225 | wowVersionSelector.addItems(wowVersions) 226 | wowVersionSelector.currentTextChanged.connect(self.setActiveWowVersion) 227 | toolbar = self.addToolBar(self.tr("WoW Version")) 228 | toolbar.addWidget(wowVersionSelector) 229 | 230 | self.addonList = Grid(self) 231 | 232 | self.addonList.setColumnCount(5) 233 | self.addonList.setHorizontalHeaderLabels([self.tr("Name"), self.tr("Url"), self.tr("Version"), self.tr("TOC"), self.tr("Allow Beta")]) 234 | 235 | self.resize(1070, 815) 236 | screen = Qt.QDesktopWidget().screenGeometry() 237 | size = self.geometry() 238 | self.move((screen.width() - size.width()) / 2, (screen.height() - size.height()) / 5) 239 | 240 | box.addWidget(self.addonList) 241 | self.statusBar().showMessage(self.tr("Ready")) 242 | self.setCentralWidget(self.mainWidget) 243 | self.show() 244 | 245 | # def resizeEvent(self, event): 246 | # print(self.geometry()) 247 | 248 | def getWowVersions(self): 249 | wowVersions = ["retail"] 250 | settings = Qt.QSettings() 251 | for wowVersion in ("classic", "ptr"): 252 | directory = "{}/_{}_/Interface/AddOns".format(str(settings.value(defines.WOW_FOLDER_KEY, defines.WOW_FOLDER_DEFAULT)), wowVersion) 253 | if os.path.exists(directory): 254 | wowVersions.append(wowVersion) 255 | return wowVersions 256 | 257 | def setActiveWowVersion(self, text): 258 | if not hasattr(self, 'wowVersion') or self.wowVersion != text: 259 | if hasattr(self, 'addons'): 260 | self.saveAddons() 261 | self.wowVersion = text 262 | defines.TOC=self.getWowToc() 263 | self.setWindowTitle('WoW!Curse ({}: {})'.format(self.wowVersion, defines.TOC)) 264 | if text == defines.WOW_VERSION_DEFAULT: 265 | self.addonsFile = os.path.expanduser(defines.LCURSE_ADDONS) 266 | else: 267 | self.addonsFile = os.path.expanduser(defines.LCURSE_ADDONS_BASE.format(text)) 268 | if hasattr(self, 'addons'): 269 | self.loadAddons() 270 | 271 | def ensureLCurseFolder(self): 272 | if not os.path.exists(defines.LCURSE_FOLDER): 273 | os.mkdir(defines.LCURSE_FOLDER) 274 | elif not os.path.isdir(defines.LCURSE_FOLDER): 275 | e = self.tr( 276 | "There is an entry \".lcurse\" in your home directory which is neither a folder nor a link to a folder." 277 | " Exiting!") 278 | Qt.QMessageBox.critical(None, self.tr("lcurse-folder not a folder"), e) 279 | print(e) 280 | raise 281 | 282 | def sizeHint(self): 283 | width = self.addonList.sizeHintForColumn(0) + self.addonList.sizeHintForColumn( 284 | 1) + self.addonList.sizeHintForColumn(2) + self.addonList.sizeHintForColumn(3) + 120 285 | size = Qt.QSize(width, 815) 286 | return size 287 | 288 | def adjustSize(self): 289 | self.resize(self.sizeHint()) 290 | 291 | def removeStupidStuff(self, s): 292 | s = re.sub(r"\|r", "", s) 293 | s = re.sub(r"\|c.{8}", "", s) 294 | s = re.sub(r"\[|\]", "", s) 295 | return s 296 | 297 | def extractAddonMetadataFromTOC(self, toc): 298 | (name, uri, version, curseId, tocversion) = ("", "", "", "", "") 299 | title_re = re.compile(r"^## *Title: *(.*)") 300 | title2_re = re.compile(r"^## *Title.....: *(.*)") 301 | curse_title_re = re.compile(r"^## *X-Curse-Project-Name: *(.*)") 302 | curse_version_re = re.compile(r"^## *X-Curse-Packaged-Version: *(.*)") 303 | version_re = re.compile(r"^## *Version: *(.*)$") 304 | curse_re = re.compile(r"^## *X-Curse-Project-ID: *(.*)") 305 | tocversion_re = re.compile(r"^## *Interface: *(\d*)") 306 | with open(toc, encoding="utf-8-sig", errors='replace') as f: 307 | line = f.readline() 308 | while line != "": 309 | line = line.strip() 310 | m = curse_title_re.match(line) 311 | if m: 312 | name = m.group(1).strip() 313 | line = f.readline() 314 | continue 315 | if name == "": 316 | m = title_re.match(line) 317 | if m: 318 | name = m.group(1).strip() 319 | line = f.readline() 320 | continue 321 | if name == "": 322 | m = title2_re.match(line) 323 | if m: 324 | name = m.group(1).strip() 325 | line = f.readline() 326 | continue 327 | m = curse_version_re.match(line) 328 | if m: 329 | version = m.group(1).strip() 330 | line = f.readline() 331 | continue 332 | if version == "": 333 | m = version_re.match(line) 334 | if m: 335 | version = m.group(1).strip() 336 | line = f.readline() 337 | continue 338 | m = curse_re.match(line) 339 | if m: 340 | curseId = m.group(1).strip() 341 | line = f.readline() 342 | continue 343 | m = tocversion_re.match(line) 344 | if m: 345 | tocversion = m.group(1).strip() 346 | line = f.readline() 347 | continue 348 | line = f.readline() 349 | if not version: 350 | version="n/a" 351 | if not name: 352 | print(self.tr("not enough information found for addon {}\n(version={},name={},toc={})\n").format(toc,version,name,tocversion)) 353 | name = self.removeStupidStuff(name) 354 | curseId = self.removeStupidStuff(curseId) 355 | 356 | uri = "https://www.curseforge.com/wow/addons/{}".format(name.lower().replace(" ", "-")) 357 | if curseId: 358 | if curseId.isdigit(): 359 | uri = "https://www.curseforge.com/projects/{}".format(curseId) 360 | else: 361 | uri = "https://www.curseforge.com/wow/addons/{}".format(curseId) 362 | 363 | #return ["", "", "", ""] 364 | 365 | return [name, uri, version, tocversion] 366 | 367 | def importAddons(self): 368 | settings = Qt.QSettings() 369 | parent = "{}/_{}_/Interface/AddOns".format(str(settings.value(defines.WOW_FOLDER_KEY, defines.WOW_FOLDER_DEFAULT)), self.wowVersion) 370 | contents = os.listdir(parent) 371 | for item in contents: 372 | itemDir = "{}/{}".format(parent, item) 373 | if os.path.isdir(itemDir) and not item.lower().startswith("blizzard_"): 374 | toc = "{}/{}.toc".format(itemDir, item) 375 | if os.path.exists(toc): 376 | tmp = self.extractAddonMetadataFromTOC(toc) 377 | if tmp[0] == "": 378 | continue 379 | (name, uri, version, tocVersion) = tmp 380 | addons = self.addonList.findItems(name, Qt.Qt.MatchExactly) 381 | if not addons: 382 | self.insertAddon(name, uri, version, tocVersion, False) 383 | elif tocVersion: 384 | for addon in addons: 385 | self.addonList.item(addon.row(), 3).setText(tocVersion) 386 | self.addonList.resizeColumnsToContents() 387 | self.saveAddons() 388 | 389 | def openPreferences(self): 390 | pref = preferences.PreferencesDlg(self) 391 | pref.exec_() 392 | 393 | def insertAddon(self, name, uri, version, tocVersion, allowBeta): 394 | self.addonList.setSortingEnabled(False) 395 | row = self.addonList.rowCount() 396 | self.addonList.insertRow(row) 397 | self.addonList.setItem(row, 0, Qt.QTableWidgetItem(name)) 398 | self.addonList.setItem(row, 1, Qt.QTableWidgetItem(uri)) 399 | self.addonList.setItem(row, 2, Qt.QTableWidgetItem(version)) 400 | self.addonList.setItem(row, 3, Qt.QTableWidgetItem(tocVersion)) 401 | allowBetaItem = Qt.QTableWidgetItem() 402 | allowBetaItem.setCheckState(Qt.Qt.Checked if allowBeta else Qt.Qt.Unchecked) 403 | self.addonList.setItem(row, 4, allowBetaItem) 404 | self.setRowColor(row, Qt.Qt.cyan) 405 | self.addonList.setSortingEnabled(True) 406 | 407 | def loadAddonCatalog(self): 408 | if os.path.exists(defines.LCURSE_ADDON_CATALOG): 409 | with open(defines.LCURSE_ADDON_CATALOG) as c: 410 | self.availableAddons = json.load(c) 411 | 412 | def loadAddons(self): 413 | self.addonList.setSortingEnabled(False) 414 | self.addonList.clearContents() 415 | addons = None 416 | if os.path.exists(self.addonsFile): 417 | with open(self.addonsFile) as f: 418 | data = json.load(f) 419 | try: 420 | dbversion = data['dbversion'] 421 | addons = data['addons'] 422 | except: 423 | print(self.tr("Warning, old database, will convert")) 424 | dbversion = 0 425 | addons = data 426 | if not addons: 427 | self.addonList.setRowCount(0) 428 | return 429 | self.addonList.setRowCount(len(addons)) 430 | tocs=self.updateDatabaseFormat(dbversion) 431 | for (row, addon) in enumerate(addons): 432 | url = urllib.parse.urlparse(addon["uri"]) 433 | if url.netloc == "mods.curse.com" or url.netloc == "www.curse.com" or "wowace" in url.netloc: 434 | path=url.path.replace("/addons",'').replace('/wow','') 435 | addon["uri"]=url.scheme + '://www.curseforge.com/wow/addons' +path 436 | self.addonList.setItem(row, 0, Qt.QTableWidgetItem(addon["name"])) 437 | self.addonList.setItem(row, 1, Qt.QTableWidgetItem(addon["uri"])) 438 | self.addonList.setItem(row, 2, Qt.QTableWidgetItem(addon["version"])) 439 | try: 440 | if addon["toc"] == "": 441 | addon["toc"] = "n/a" 442 | self.addonList.setItem(row, 3, Qt.QTableWidgetItem(addon["toc"])) 443 | 444 | except Exception as e: 445 | addon["toc"] = "n/a" 446 | if addon["toc"] == "n/a": 447 | try: 448 | self.addonList.setItem(row, 3, Qt.QTableWidgetItem(tocs[addon["name"]]["toc"])) 449 | except Exception as e: 450 | self.addonList.setItem(row, 3, Qt.QTableWidgetItem("n/a")) 451 | toc=self.addonList.item(row, 3).text() 452 | if (toc=="n/a"): 453 | self.addonList.item(row, 3).setForeground(Qt.Qt.black) 454 | elif ( toc == defines.TOC): 455 | self.addonList.item(row, 3).setForeground(Qt.Qt.blue) 456 | else: 457 | self.addonList.item(row, 3).setForeground(Qt.Qt.red) 458 | allowBeta = addon.get("allowbeta", False) 459 | allowBetaItem = Qt.QTableWidgetItem() 460 | allowBetaItem.setCheckState(Qt.Qt.Checked if allowBeta else Qt.Qt.Unchecked) 461 | self.addonList.setItem(row, 4, allowBetaItem) 462 | self.addonList.resizeColumnsToContents() 463 | self.adjustSize() 464 | self.addonList.setSortingEnabled(True) 465 | 466 | def saveAddons(self): 467 | print(self.tr("Saving addons to {}").format(self.addonsFile)) 468 | addons = [] 469 | sortSection = self.addonList.horizontalHeader().sortIndicatorSection() 470 | sortOrder = self.addonList.horizontalHeader().sortIndicatorOrder() 471 | self.addonList.sortItems(0) 472 | for row in range(self.addonList.rowCount()): 473 | addons.append(dict( 474 | name=str(self.addonList.item(row, 0).text()), 475 | uri=str(self.addonList.item(row, 1).text()), 476 | version=str(self.addonList.item(row, 2).text()), 477 | toc=str(self.addonList.item(row,3).text()), 478 | allowbeta=bool(self.addonList.item(row, 4).checkState() == Qt.Qt.Checked) 479 | )) 480 | self.addonList.sortItems(sortSection, sortOrder) 481 | data={} 482 | data['addons'] = addons 483 | data['dbversion'] = defines.LCURSE_DBVERSION 484 | with open(self.addonsFile, "w") as f: 485 | json.dump(data, f,indent=1) 486 | 487 | def addAddon(self): 488 | url = None 489 | addAddonDlg = addaddondlg.AddAddonDlg(self, self.availableAddons) 490 | result = addAddonDlg.exec_() 491 | if result != Qt.QDialog.Accepted: 492 | return 493 | nameOrUrl = addAddonDlg.getText() 494 | name = nameOrUrl 495 | try: 496 | for item in self.availableAddons: 497 | if item[0] == name: 498 | url = item[1] 499 | except IndexError: 500 | print(self.tr("can't handle: {}").format(name)) 501 | name = "" 502 | 503 | if url == None: 504 | url = str(nameOrUrl) 505 | if "curseforge.com" in url: 506 | try: 507 | print(self.tr("retrieving addon information")) 508 | response = opener.open(urlparse(urlquote(url, ':/')).geturl()) 509 | soup = BeautifulSoup(response.read(), "lxml") 510 | try: 511 | captions = soup.select("h2.name") 512 | name = captions[0].string 513 | except: 514 | print(self.tr("www.curseforge.com layout has changed.")) 515 | pass 516 | except HTTPError as e: 517 | print(e) 518 | elif url.endswith(".git"): 519 | name = os.path.basename(url)[:-4] 520 | 521 | if name: 522 | self.insertAddon(name, url, "", "", False) 523 | 524 | def updateDatabaseFormat(self,oldVersion): 525 | if oldVersion != defines.LCURSE_DBVERSION: 526 | print(self.tr("Db version is {} vs {}").format(oldVersion, defines.LCURSE_DBVERSION)) 527 | if oldVersion >= defines.LCURSE_DBVERSION: 528 | return {} 529 | print(self.tr("Database update!")) 530 | settings = Qt.QSettings() 531 | parent = "{}/_{}_/Interface/AddOns".format(str(settings.value(defines.WOW_FOLDER_KEY, defines.WOW_FOLDER_DEFAULT)), self.wowVersion1) 532 | contents = os.listdir(parent) 533 | contents.sort() 534 | tocversions={} 535 | for item in contents: 536 | itemDir = "{}/{}".format(parent, item) 537 | if os.path.isdir(itemDir) and not item.lower().startswith("blizzard_"): 538 | toc = "{}/{}.toc".format(itemDir, item) 539 | if os.path.exists(toc): 540 | tmp = self.extractAddonMetadataFromTOC(toc) 541 | name=self.removeStupidStuff(tmp[0]) 542 | tocversions[tmp[0]]={"folder":item,"toc":tmp[3]} 543 | with open(defines.LCURSE_ADDON_TOCS_CACHE, "w") as f: 544 | json.dump(tocversions, f,indent=1) 545 | return tocversions 546 | 547 | def removeFromList(self): 548 | rows = self.addonList.currentRows() 549 | rows.reverse() 550 | for row in rows: 551 | self.addonList.removeRow(row) 552 | self.saveAddons() 553 | 554 | def clearCell(self): 555 | cell = self.addonList.currentItem() 556 | cell.setText("") 557 | 558 | def removeAddon(self): 559 | row = self.addonList.currentRow() 560 | print(self.tr("Current Row: {0:d}").format(row)) 561 | answer = Qt.QMessageBox.question(self, self.tr("Remove selected addon"), 562 | str(self.tr("Do you really want to remove the following addon?\n{}")).format( 563 | str(self.addonList.item(row, 0).text())), 564 | Qt.QMessageBox.Yes, Qt.QMessageBox.No) 565 | if answer != Qt.QMessageBox.Yes: 566 | return 567 | settings = Qt.QSettings() 568 | parent = "{}/_{}_/Interface/AddOns".format(str(settings.value(defines.WOW_FOLDER_KEY, defines.WOW_FOLDER_DEFAULT)), self.wowVersion) 569 | contents = os.listdir(parent) 570 | addonName = str(self.addonList.item(row, 0).text()) 571 | deleted = False 572 | deleted_addons = [] 573 | potential_deletions = [] 574 | for item in contents: 575 | itemDir = "{}/{}".format(parent, item) 576 | if os.path.isdir(itemDir) and not item.lower().startswith("blizzard_"): 577 | toc = "{}/{}.toc".format(itemDir, item) 578 | if os.path.exists(toc): 579 | tmp = self.extractAddonMetadataFromTOC(toc) 580 | if tmp[0] == addonName: 581 | rmtree(itemDir) 582 | deleted_addons.append(item) 583 | deleted = True 584 | 585 | self.addonList.removeRow(row) 586 | 587 | if not deleted: 588 | Qt.QMessageBox.question(self, "No addons removed", 589 | str(self.tr("No addons matching \"{}\" found.\nThe addon might already be removed, or could be going under a different name.\nManual deletion may be required.")).format(addonName), 590 | Qt.QMessageBox.Ok) 591 | else: 592 | potential = False 593 | for item in contents: 594 | itemDir = "{}/{}".format(parent, item) 595 | if os.path.isdir(itemDir) and not item.lower().startswith("blizzard_"): 596 | toc = "{}/{}.toc".format(itemDir, item) 597 | if os.path.exists(toc): 598 | tmp = self.extractAddonMetadataFromTOC(toc) 599 | for d in deleted_addons: 600 | deletions = list(filter(None, re.split("[_, \-!?:]+", d))) 601 | for word in deletions: 602 | if re.search(word, tmp[0]): 603 | potential_deletions.append(item) 604 | potential = True 605 | break 606 | if potential: 607 | break 608 | if potential: 609 | to_delete = '\n'.join(potential_deletions) 610 | removal = Qt.QMessageBox.question(self, "Potential deletion candidates found", 611 | str(self.tr("Remove the following addons as well?\n{}")).format(to_delete), 612 | Qt.QMessageBox.Yes, Qt.QMessageBox.No) 613 | if removal == Qt.QMessageBox.Yes: 614 | for p in potential_deletions: 615 | all_rows = self.addonList.rowCount() 616 | for n in range(0, all_rows): 617 | name = str(self.addonList.item(n, 0).text()) 618 | if p == name: 619 | self.addonList.removeRow(n) 620 | break 621 | rmtree("{}/{}".format(parent, p)) 622 | 623 | self.saveAddons() 624 | 625 | def setRowColor(self, row, color): 626 | self.addonList.item(row, 0).setBackground(color) 627 | self.addonList.item(row, 1).setBackground(color) 628 | self.addonList.item(row, 2).setBackground(color) 629 | 630 | def onCheckFinished(self, addon, result, data): 631 | if result: 632 | self.setRowColor(addon[0], Qt.Qt.yellow) 633 | self.addonList.item(addon[0], 0).setData(Qt.Qt.UserRole, data) 634 | elif data is None: 635 | self.setRowColor(addon[0], Qt.Qt.red) 636 | else: 637 | self.setRowColor(addon[0], Qt.Qt.white) 638 | 639 | def checkAddonsForUpdate(self, *args, rows=None): 640 | if rows == None: 641 | rows = self.addonList.currentRows() 642 | 643 | addons = [] 644 | for row in rows: 645 | name = self.addonList.item(row, 0).text() 646 | uri = self.addonList.item(row, 1).text() 647 | version = self.addonList.item(row, 2).text() 648 | allowBeta = bool(self.addonList.item(row, 4).checkState() == Qt.Qt.Checked) 649 | addons.append((row, name, uri, version, allowBeta)) 650 | 651 | checkDlg = waitdlg.CheckDlg(self, self.wowVersion, addons) 652 | checkDlg.checkFinished.connect(self.onCheckFinished) 653 | checkDlg.exec_() 654 | 655 | def checkAllAddonsForUpdate(self): 656 | self.checkAddonsForUpdate(rows=range(self.addonList.rowCount())) 657 | 658 | def onUpdateFinished(self, addon, result): 659 | if result: 660 | tmp=None 661 | toc=str(addon[6]) 662 | if os.path.exists(toc): 663 | tmp = self.extractAddonMetadataFromTOC(toc) 664 | data = self.addonList.item(addon[0], 0).data(Qt.Qt.UserRole) 665 | self.addonList.item(addon[0], 2).setText(data[0]) 666 | if tmp: 667 | self.addonList.item(addon[0], 3).setText(tmp[3]) 668 | if (tmp and tmp[3] < defines.TOC): 669 | self.addonList.item(addon[0], 3).setForeground(Qt.Qt.red) 670 | else: 671 | self.addonList.item(addon[0], 3).setForeground(Qt.Qt.blue) 672 | self.addonList.item(addon[0], 0).setData(Qt.Qt.UserRole, None) 673 | self.setRowColor(addon[0], Qt.Qt.green) 674 | 675 | def forceUpdateAddon(self, *args, rows=None): 676 | if rows == None: 677 | rows = self.addonList.currentRows() 678 | 679 | for row in rows: 680 | self.addonList.item(row, 2).setText("") 681 | 682 | self.updateAddons(rows=rows) 683 | 684 | def updateAddons(self, *args, rows=None): 685 | self.checkAddonsForUpdate(rows=rows) 686 | addons = [] 687 | 688 | if rows == None: 689 | rows = self.addonList.currentRows() 690 | 691 | for row in rows: 692 | data = self.addonList.item(row, 0).data(Qt.Qt.UserRole) 693 | if data: 694 | name = self.addonList.item(row, 0).text() 695 | uri = self.addonList.item(row, 1).text() 696 | version = self.addonList.item(row, 2).text() 697 | allowBeta = bool(self.addonList.item(row, 4).checkState() == Qt.Qt.Checked) 698 | addons.append((row, name, uri, version, allowBeta, data)) 699 | 700 | if addons: 701 | updateDlg = waitdlg.UpdateDlg(self, self.wowVersion, addons) 702 | updateDlg.updateFinished.connect(self.onUpdateFinished) 703 | updateDlg.exec_() 704 | self.saveAddons() 705 | 706 | def updateAllAddons(self): 707 | self.updateAddons(rows=range(self.addonList.rowCount())) 708 | 709 | def onUpdateCatalogFinished(self, addons): 710 | print(self.tr("retrieved list of addons: {}").format(len(addons))) 711 | self.availableAddons = addons 712 | with open(defines.LCURSE_ADDON_CATALOG, "w") as c: 713 | json.dump(self.availableAddons, c) 714 | 715 | def updateCatalog(self): 716 | updateCatalogDlg = waitdlg.UpdateCatalogDlg(self) 717 | updateCatalogDlg.updateCatalogFinished.connect(self.onUpdateCatalogFinished) 718 | updateCatalogDlg.exec_() 719 | 720 | def start(self): 721 | return self.exec_() 722 | 723 | 724 | if __name__ == "__main__": 725 | sys.exit(42) 726 | --------------------------------------------------------------------------------