def _clearFilters(self): self._platforms.clearSelection() self._regions.clearSelection() self._genres.clearSelection() self._years.clearSelection() self._item.setCheckState(Qt.PartiallyChecked) self._box.setCheckState(Qt.PartiallyChecked) self._manual.setCheckState(Qt.PartiallyChecked) logger.info("Cleared all filters.")
def _tryAlternatives(title: str, platform: str): # Occasionally the title has a trailing "-", "_", "__", or very rarely "___" in the url pTitle = _parseTitle(title) testurl = [_platforms[platform], pTitle, "release-info"] for c in ["-", "_", "__", "___"]: testurl[1] = pTitle + c # Add either '-', '_', or '__' to string logger.info(f"Trying with url: {_baseURL + '/'.join(testurl)}") res = requests.get(_baseURL + "/".join(testurl)) # Try alternative URL try: res.raise_for_status() except requests.exceptions.HTTPError: # Not a valid page logger.info("Not a valid page.") continue # Parse the html and get title and platform strings soup = bs4.BeautifulSoup(res.text, 'html.parser') te = soup.select(_titleCSS) pf = soup.select(_platformCSS) if len(te) == 0 or len(pf) == 0: continue # Check if title and platform match if te[0].text.strip().lower() == title.lower() and pf[0].text.strip().lower() == platform.lower(): logger.info(f"Found matching title at: {_baseURL + '/'.join(testurl)}") return res, _baseURL + "/".join(testurl) else: continue return None, ""
def _trySuggestions(platform: str, region: int, soup: bs4.BeautifulSoup) -> bs4.BeautifulSoup: """ Goes through the list of games and tries to find one that matches the platform :param platform: The platform we're looking for :param region: Which region. 0: NTSC (JP), 1: NTSC (NA), 2: PAL :param soup: BeautifulSoup object :return: BeautifulSoup object for new page if found, else a NoneType BeautifulSoup object """ logger.info("Couldn't find game at url. Trying alternatives...") titleUrlRegex = re.compile(r'href=\".*?\"') titles = soup.find_all("td", {"class": "title"}) consoles = soup.find_all("td", {"class": "console"}) url = "" for title, console in zip(titles, consoles): if console.text.lower().replace(" ", "-") == _platforms[platform][region]: url = titleUrlRegex.findall(title.decode()).pop()[5:].strip('"') break if len(url) > 0: logger.info(f"New url found: {url}") res = requests.get(url) soup = bs4.BeautifulSoup(res.text, "html.parser") return soup logger.info("Couldn't find title in alternate urls.") return soup.clear()
def main(): gcmDir = getScriptDir() dbPath = gcmDir + "/data/db/collection.db" logger.info(f"Program directory set to {gcmDir}.") # Make sure we have everything if not os.path.exists(gcmDir + "/data"): logger.critical( "Data directory doesn't exist. Creating '/data/db' and '/data/vgdb'..." ) os.makedirs(gcmDir + "/data/db") os.mkdir(gcmDir + "/data/vgdb") createDB(dbPath) if not os.path.exists(gcmDir + "/data/db"): logger.critical("'/data/db' doest't exist. Creating...") os.mkdir(gcmDir + "/data/db") createDB(dbPath) if not os.path.exists(gcmDir + "/data/vgdb"): os.mkdir(gcmDir + "/data/vgdb") if not os.path.exists(dbPath): logger.critical("No database was found.") createDB(dbPath) createWindow(dbPath)
def getMobyRelease(name: str, platform: str, region: str, country: str = ""): """ Finds a specific release for a game on MobyGames.com :param name: The name of the game :param platform: The game's platform :param region: The game's region (NTSC (JP), NTSC (NA), PAL accepted) :param country: Optionally specify a specific country :return: Dictionary of the release info """ logger.info(f"Find release info for '{name}' on '{region} {platform}' for country '{country}'" if country != "" else f"Find release info for '{name}' on '{region} {platform}'.") releaseInfo = {"publisher": "", "developer": "", "platforms": "", "genre": "", "code": "", "year": ""} regionDict = {"NTSC (JP)": ("Japan", "Worldwide"), "NTSC (NA)": ("United States", "Canada", "Worldwide"), "PAL": ("United Kingdom", "Ireland", "Germany", "France", "Italy", "Austria", "Belgium", "The Netherlands", "Portugal", "Spain", "Switzerland", "Russia", "Sweden", "Denmark", "Norway", "Finland", "Australia, New Zealand", "Worldwide")} if region in ("PAL A", "PAL B"): region = "PAL" elif region not in ("PAL", "NTSC (NA)", "NTSC (JP)"): # Catch all for other non-valid regions region = "NTSC (NA)" regionValue = regionDict[region] info = getMobyInfo(name, platform) if info["title"] == "": # No data found, return empty values logger.error("Release info not found.") return releaseInfo publisher = info["publisher"] developer = info["developer"] platforms = info["platforms"] genre = info["genre"] covers = info["covers"] if "covers" in info.keys() else "" yearFormat = re.compile(r"\d{4}") skipCode = False code = "" year = "" # Try to get product code, and also year since it might be different between releases correctRelease = "" for release in info["releases"].keys(): # Optionally check the specific country's release, but only if it makes sense # (e.g. don't check for Norway if region == NTSC (JP) if country != "" and country in regionValue and country in release: correctRelease = release else: if region == "PAL" and "United Kingdom" in release: # Make UK release default for PAL correctRelease = release else: if country == "" and correctRelease == "": # UK not found, or region isn't PAL, try to find another release for r in release: if r in regionValue or r == regionValue: correctRelease = release break elif country != "" and correctRelease == "": continue if correctRelease != "": break if correctRelease == "": correctRelease = list(info['releases'].keys())[0] skipCode = True logger.warning(f"Couldn't find correct release for given region. Defaulting to the first one {correctRelease}. " "This also means we skip checking the product code. Please enter it manually.") details = info['releases'][correctRelease] for d in details: if d[0] in ("Company Code", "Nintendo Media PN", "Sony PN") and not skipCode: code = d[1] elif d[0] == "Release Date": year = yearFormat.findall(d[1])[0] # Try with EAN-13 or UPC-A for the code as a fallback if code == "" and not skipCode: logger.warning("Couldn't find product code. Trying with barcode instead.") for d in info["releases"][correctRelease]: if d[0] in ("EAN-13", "UPC-A"): code = d[0] + ": " + d[1] break if code == "": logger.warning("Can't find barcode either.") # Find the release's cover image if len(covers) > 0 and str(list(covers.values())[0]).find("/shots/") == -1: # We have a cover image, determine region res = None for cover in covers: countries = cover.split(" , ") for country in countries: if region == "PAL" and country.strip() == "United Kingdom": # Default to UK for PAL region res = requests.get(covers[cover]) break elif country.strip() in regionValue: res = requests.get(covers[cover]) break if res is not None: break if res is None: # Correct region not found, select the first one. logger.warning("Couldn't find correct cover for the region. Defaulting to the first image.") res = requests.get(list(covers.values())[0]) imgCSS = ".img-responsive" imgURLReg = re.compile(r'src=\".*?\"') soup = bs4.BeautifulSoup(res.text, "html.parser") imgURL = "https://www.mobygames.com" + imgURLReg.findall(str(soup.select(imgCSS))).pop().split('=')[1].strip('"') logger.info(f"Found cover image at: {imgURL}") elif len(covers) > 0 and str(list(covers.values())[0]).find("/shots/") != -1: # Cover image wasn't found but we have a screen shot imgURL = list(covers.values())[0] logger.warning("A proper cover image wasn't found. But found a screen shot.") logger.info(f"Screenshot url: {imgURL}") else: # No image found logger.warning("Couldn't find a cover image.") imgURL = "" releaseInfo = {"publisher": publisher, "developer": developer, "platforms": platforms, "genre": genre, "image": imgURL, "code": code, "year": year} logger.info("Release info found.") return releaseInfo
def getMobyInfo(title: str, platform: str) -> dict: """Takes a game name and its platform, and returns a dictionary with the game's information from MobyGames.com. :param title: Title of the game :param platform: The game's platform :return: Dictionary of the game's info """ logger.info(f"Getting info for '{title}' for '{platform}'...") mobyCSSData = { "title": "html body div#wrapper div.container div#main.row div.col-md-12.col-lg-12 div.rightPanelHeader " "h1.niceHeaderTitle a", "publisher": "#coreGameRelease > div:nth-child(2) > a:nth-child(1)", "developer": "#coreGameRelease > div:nth-child(4) > a:nth-child(1)", "release": "#coreGameRelease > div:nth-child(6) > a:nth-child(1)", "platforms": "#coreGameRelease > div:nth-child(8)", "genre": "#coreGameGenre > div:nth-child(1) > div:nth-child(2)", } # Gameplay type. If found, used instead of Genre since it's usually more # representative (e.g. "Platform" instead of "Action") gameplayCSS = ["#coreGameGenre > div:nth-child(1) > div:nth-child(3)", "#coreGameGenre > div:nth-child(1) > div:nth-child(4)", "#coreGameGenre > div:nth-child(1) > div:nth-child(5)", "#coreGameGenre > div:nth-child(1) > div:nth-child(6)", "#coreGameGenre > div:nth-child(1) > div:nth-child(7)", "#coreGameGenre > div:nth-child(1) > div:nth-child(8)", "#coreGameGenre > div:nth-child(1) > div:nth-child(9)", "#coreGameGenre > div:nth-child(1) > div:nth-child(10)", "#coreGameGenre > div:nth-child(1) > div:nth-child(11)", "#coreGameGenre > div:nth-child(1) > div:nth-child(12)", "#coreGameGenre > div:nth-child(2) > div:nth-child(3)", "#coreGameGenre > div:nth-child(2) > div:nth-child(4)", "#coreGameGenre > div:nth-child(2) > div:nth-child(5)", "#coreGameGenre > div:nth-child(2) > div:nth-child(6)", "#coreGameGenre > div:nth-child(2) > div:nth-child(7)", "#coreGameGenre > div:nth-child(2) > div:nth-child(8)", "#coreGameGenre > div:nth-child(2) > div:nth-child(9)", "#coreGameGenre > div:nth-child(2) > div:nth-child(10)", "#coreGameGenre > div:nth-child(2) > div:nth-child(11)", "#coreGameGenre > div:nth-child(2) > div:nth-child(12)"] if platform.lower() == "game & watch": title = "Game & Watch Wide Screen: " + title # TODO: Need to figure out something better for each variety pTitle = _parseTitle(title) pPlatform = _parsePlatform(platform) logger.info(f"Parsed title to '{pTitle}'.") logger.info(f"Parsed platform to '{pPlatform}'") # Get data if pPlatform not in _platforms.keys(): # Platform not supported logger.error(f"Platform '{platform}' not supported.") return {x: "" for x in mobyCSSData.keys()} fullURL = _baseURL + "/".join((_platforms[pPlatform], pTitle, "release-info")) logger.info(f"Full url to mobygames: {fullURL}") try: res = requests.get(fullURL) except socket.gaierror: # Most likely no internet connection logger.error("Can't establish connection.") return {x: "" for x in mobyCSSData.keys()} try: res.raise_for_status() soup = bs4.BeautifulSoup(res.text, 'html.parser') except requests.exceptions.HTTPError: # Try the suggested results on the 404 page logger.info("Title not immediately found. Trying the suggestions.") res, title, fullURL = _trySuggestions(title, pPlatform) if res is None: # Couldn't find anything. Return empty values logger.error("Title not found.") return {x: "" for x in mobyCSSData.keys()} soup = bs4.BeautifulSoup(res.text, 'html.parser') # Extract data for key in mobyCSSData.keys(): pf = soup.select(_platformCSS) pf = pf[0].text.strip() if len(pf) > 0 else "" if pf.lower() != pPlatform.lower(): # Try some alternative URLs logger.info("Platform mismatch. Trying some alternative urls.") res, fullURL = _tryAlternatives(title, pPlatform) if res is None: # Nothing was found. logger.error("Title not found.") return {x: "" for x in mobyCSSData.keys()} soup = bs4.BeautifulSoup(res.text, 'html.parser') try: value = soup.select(mobyCSSData[key]) if key == "platforms": # Make sure we don't include the '| Combined View' text mobyCSSData[key] = ucd.normalize("NFKD", value[0].text.split("|", 1)[0].strip()) # Also make sure to insert the platform we're looking for platforms = mobyCSSData[key].split(", ") if pPlatform not in platforms: platforms.append(pPlatform) mobyCSSData[key] = ", ".join(sorted(platforms, key=str.lower)) elif key == "genre": # Try finding the "Gameplay" category since it's more accurate but not always available gameplay = "" for i in range(len(gameplayCSS)): temp = soup.select(gameplayCSS[i]) if len(temp) > 0: if temp[0].text.strip() == "Gameplay": # Gameplay type is under this header gameplay = soup.select(gameplayCSS[i + 1]) break # "Arcade" and "Puzzle elements" by themselves are about as useful as "Action" if len(gameplay) > 0 and gameplay[0].text.strip() != "Arcade" and\ ucd.normalize("NFKD", gameplay[0].text.strip()) != "Puzzle elements" and\ ucd.normalize("NFKD", gameplay[0].text.strip()) != "Arcade, Puzzle elements": # Save gameplay types without "Arcade", since it's pretty useless. mobyCSSData[key] = str(ucd.normalize("NFKD", gameplay[0].text.strip())).replace("Arcade, ", "") else: # Default back to normal Genre mobyCSSData[key] = ucd.normalize("NFKD", value[0].text.strip()) else: mobyCSSData[key] = ucd.normalize("NFKD", value[0].text.strip()) except IndexError: # Not all games have all data. Just add an empty string instead. if key == "genre": # If there's an ESRB rating it takes the place of the normal genre position altGenreCSS = "#coreGameGenre > div:nth-child(2) > div:nth-child(4)" try: value = soup.select(altGenreCSS) mobyCSSData[key] = ucd.normalize("NFKD", value[0].text.strip()) except IndexError: # Still nothing logger.info(f"No data for value: '{key}'") mobyCSSData[key] = "" else: logger.info(f"No data for value: '{key}'") mobyCSSData[key] = "" # Get release info releases = {} release = "" info = soup.find_all("div", {"class": "floatholder relInfo"}) for i in info: titles = i.find_all("div", {"class": "relInfoTitle"}) details = i.find_all("div", {"class": "relInfoDetails"}) for title, detail in zip(titles, details): if title.text.strip() in ("Country", "Countries"): # Make the country name the dict key temprelease = detail.text.split(",") temprelease = [x.strip() for x in temprelease] release = tuple(temprelease) releases[release] = [] else: # Add the rest of the info to the country name key releases[release].append([ucd.normalize("NFKD", title.text.strip()), ucd.normalize("NFKD", detail.text.strip())]) mobyCSSData["releases"] = releases # Get cover image imgurlReg = re.compile(r'href=\".*?\"') coverURL = fullURL.replace("release-info", "cover-art") coverRes = requests.get(coverURL) coverSoup = bs4.BeautifulSoup(coverRes.text, "html.parser") coverReleases = coverSoup.find_all("table", {"summary": "Description of Covers"}) coverMedia = coverSoup.find_all("div", {"class": "thumbnail"}) if len(coverMedia) == 0: # No covers found, default to title screen shot imgurlReg = re.compile(r'src=\".*?\"') image = soup.find_all("div", {"id": "coreGameCover"}) temp = [] for i in image.pop().decode(): temp.append(i) tmpsrc = imgurlReg.findall("".join(temp)) if len(tmpsrc) > 0: imgsrc = tmpsrc.pop().split('=')[1].strip('"') # Find 'src=' part, then split at '=' mobyCSSData["covers"] = {"United States": "https://www.mobygames.com" + imgsrc} else: logger.warning("No cover image found.") else: # Find the "Front Cover" URLs coverURLs = [] for media in coverMedia: covers = media.find_all("a", {"class": "thumbnail-cover"}) for cover in covers: if str(cover).find("Front Cover") != -1: # Find 'href=' part, split at '=', and strip away '"' on the right side coverURLs.append(imgurlReg.findall(str(cover)).pop().split('=')[1].strip('"')) covers = {} for release, url in zip(coverReleases, coverURLs): rel = release.find_all("td") for j, r in enumerate(rel): # Find index of countries list # if len(rel) > 9: # continue # Skips player's choice releases etc (anything that has a "Package Comments" section) if r.text in ("Country", "Countries"): # Country as key, url as value. When several countries the last ones are separated with " and ". covers[rel[j + 2].text.replace(" and ", " , ")] = url break mobyCSSData["covers"] = covers logger.info("Title info found.") return mobyCSSData
def _trySuggestions(title: str, platform: str): # Checks if the suggested URLs match pTitle = _parseTitle(title) res = requests.get(_baseURL + "/".join((_platforms[platform], pTitle, "release-info"))) suggestionsCSS = ".col-md-12 > div:nth-child(3) > ul:nth-child(2)" # List of URLs alternativeTitlesCSS = [".col-md-8 > ul:nth-child(17)", ".col-md-8 > ul:nth-child(18)", ".col-md-8 > ul:nth-child(19)", # List of alternative titles ".col-md-8 > ul:nth-child(20)", # Not all of them might be valid, ".col-md-8 > ul:nth-child(21)", # but I've seen 17, 19, 20, 25, 28, 31, 39, and 42 ".col-md-8 > ul:nth-child(22)", # being used ".col-md-8 > ul:nth-child(23)", ".col-md-8 > ul:nth-child(24)", ".col-md-8 > ul:nth-child(25)", ".col-md-8 > ul:nth-child(26)", ".col-md-8 > ul:nth-child(27)", ".col-md-8 > ul:nth-child(28)", ".col-md-8 > ul:nth-child(29)", ".col-md-8 > ul:nth-child(30)", ".col-md-8 > ul:nth-child(31)", ".col-md-8 > ul:nth-child(32)", ".col-md-8 > ul:nth-child(33)", ".col-md-8 > ul:nth-child(34)", ".col-md-8 > ul:nth-child(35)", ".col-md-8 > ul:nth-child(36)", ".col-md-8 > ul:nth-child(37)", ".col-md-8 > ul:nth-child(38)", ".col-md-8 > ul:nth-child(39)", ".col-md-8 > ul:nth-child(40)", ".col-md-8 > ul:nth-child(41)", ".col-md-8 > ul:nth-child(42)"] # Find new url url = re.compile(r'".*"') # URL is located within quotation marks soup = bs4.BeautifulSoup(res.text, 'html.parser') res = soup.select(suggestionsCSS) if len(res) > 0: suggestionURLs = [u.strip('"') for u in url.findall(res.pop().decode())] else: # No suggestions found logger.warning("Couldn't find any suggestions.") return None, title, "" # Try each suggestion for suggestion in suggestionURLs: # The suggestions all use the Combined View. Insert the platform into url temp = suggestion.split("/") temp.insert(4, _platforms[platform]) temp.append("release-info") newurl = "/".join(temp) logger.info(f"Trying with url: {newurl}") # Get the platform and title strings res = requests.get(newurl) soup = bs4.BeautifulSoup(res.text, "html.parser") te = soup.select(_titleCSS) pf = soup.select(_platformCSS) if len(te) == 0 or len(pf) == 0: # This shouldn't happen but who knows continue if pf[0].text.strip() != platform: logger.info("Not the correct platform.") continue # Not the right platform, abort newtitle = te[0].text.strip() # Sometimes ō is transliterated as ou or oo, and ū as uu if newtitle.lower() == title.lower() or \ newtitle.lower() == title.replace("ō", "ou").lower() or \ newtitle.lower() == title.replace("ou", "ō").lower() or \ newtitle.lower() == title.replace("ō", "oo").lower() or \ newtitle.lower() == title.replace("oo", "ō").lower() or \ newtitle.lower() == title.replace("ū", "uu").lower() or \ newtitle.lower() == title.replace("uu", "ū").lower(): logger.info("Found match at url.") return res, title, newurl else: # Try removing any weird characters from the sides of the game name: t = title.rstrip("\\-%$£@") t = t.lstrip("\\-%$£@") if newtitle.lower() == t.lower() or \ newtitle.lower() == t.replace("ou", "ō").lower() or \ newtitle.lower() == t.replace("ō", "oo").lower() or \ newtitle.lower() == t.replace("oo", "ō").lower() or \ newtitle.lower() == t.replace("ū", "uu").lower() or \ newtitle.lower() == t.replace("uu", "ū").lower(): logger.info(f"Found match at url with title '{t}'.") return res, t, newurl else: # Check the alternative titles (Japanese games often have different titles for example) logger.info(f"Platform matches, but not title ({newtitle}). Trying to find it in 'Alternate Titles'.") alturl = newurl.split("/") alturl = "/".join(alturl[:-1]) # Remove the 'release-info' part. Alt titles are on the main page. altres = requests.get(alturl) soup = bs4.BeautifulSoup(altres.text, "html.parser") temp = [] for alt in alternativeTitlesCSS: # Try to find the alt titles temp = soup.select(alt) if len(temp) > 0: break if len(temp) == 0: # Still nothing, give up logger.info("No alternative titles found on page.") continue altTitles = [t.strip('"') for t in url.findall(temp[0].text)] # Not URLs but regex rule is the same for alt in altTitles: logger.info(f"Found alternative title: '{alt}'.") if alt.lower() == title.lower(): logger.info("Found match at url.") return res, title, newurl elif alt.lower() == t.lower(): logger.info(f"Found match at url with title '{t}'.") return res, t, newurl # Sometimes ō is transliterated as ou or oo, and ū as uu elif alt.lower() == title.replace("ō", "ou").lower() or \ alt.lower() == title.replace("ou", "ō").lower() or \ alt.lower() == title.replace("ō", "oo").lower() or \ alt.lower() == title.replace("oo", "ō").lower() or \ alt.lower() == title.replace("ū", "uu").lower() or \ alt.lower() == title.replace("uu", "ū").lower(): logger.info(f"Found matching alternative title'.") return res, title, newurl elif alt.lower() == t.replace("ō", "ou").lower() or \ alt.lower() == t.replace("ou", "ō").lower() or \ alt.lower() == t.replace("ō", "oo").lower() or \ alt.lower() == t.replace("oo", "ō").lower() or \ alt.lower() == t.replace("ū", "uu").lower() or \ alt.lower() == t.replace("uu", "ū").lower(): logger.info(f"Found matching alternative title, with title '{t}'") return res, t, newurl else: continue logger.info("Suggestions doesn't match the title.") return None, title, ""
def getPriceData(title: str, platform: str, region: str, currency="USD") -> dict: """ Tries to look up pricing info for a game on Pricecharting.com :param title: The title of the game we're looking for :param platform: Which platform the game is on :param region: Which regional release we're looking for :param currency: Which currency we want back. Possible options are: USD, AUD, BRL, CAD, EUR, GBP, and MXN :return: A dictionary with the game's current average prices (loose, cib, and new) """ logger.info(f"Looking up price for '{title}' {region} on {platform}...") priceInfo = {"loose": "#used_price > span:nth-child(1)", "cib": "#complete_price > span:nth-child(1)", "new": "#new_price > span:nth-child(1)"} regions = {"NTSC (JP)": 0, "NTSC (NA)": 1, "PAL": 2} rates = {"USD": 1.0, "AUD": 0.0, "BRL": 0.0, "CAD": 0.0, "EUR": 0.0, "GBP": 0.0, "MXN": 0.0} sign = {"USD": "$", "AUD": "AUD ", "BRL": "R$", "CAD": "CAD ", "EUR": "€", "GBP": "£", "MXN": "Mex$"} ratesRegex = re.compile(r'("\w{3}":\d\.\d.*.)') pTitle = _parseTitle(title) pPlatform = _parsePlatform(platform) logger.info(f"Title parsed to '{pTitle}'.") logger.info(f"Platform parsed to '{pPlatform}'.") # Sanity check if region in ("PAL A", "PAL B"): logger.info(f"Changing {region} to 'PAL'...") region = "PAL" elif region not in ("NTSC (JP)", "NTSC (NA)", "PAL"): logger.info(f"Changing {region} to 'NTSC (NA)'") region = "NTSC (NA)" if pPlatform not in _platforms.keys(): # Platform not supported logger.error("Platform is not supported currently.") return {x: "N/A" for x in priceInfo.keys()} fullURL = _baseURL + "/".join((_platforms[pPlatform][regions[region]], pTitle)) logger.info(f"Full url to price info is: {fullURL}") # Error handling try: res = requests.get(fullURL) except socket.gaierror: # Most likely no internet connection logger.error("Couldn't establish connection.") return {x: "N/A" for x in priceInfo.keys()} try: res.raise_for_status() soup = bs4.BeautifulSoup(res.text, "html.parser") except requests.exceptions.HTTPError: # Not found logger.error("Title not found.") return {x: "N/A" for x in priceInfo.keys()} if len(soup.select("#product_name > a:nth-child(1)")) == 0: # Didn't find the right page, try suggestions soup = _trySuggestions(pPlatform, regions[region], soup) if soup is None: # Still couldn't find anything logger.error("Title not found.") return {x: "N/A" for x in priceInfo.keys()} # Get currency rates currentRates = ratesRegex.findall(res.text)[0].split(",") for c in currentRates: cur, rate = c.split(":") rates[cur.strip('"')] = float(rate.rstrip("};")) for key, val in priceInfo.items(): if len(soup.select(val)) == 0: # Maybe not right page? priceInfo[key] = "N/A" continue price = ucd.normalize("NFKD", soup.select(val)[0].text.strip()).lstrip("$") if price == "N/A": # No price found priceInfo[key] = "N/A" continue getcontext().prec = len(price) - 1 # Dynamically set precision to 2 decimal points priceInfo[key] = sign[currency] + str(Decimal(price) * Decimal(rates[currency])) logger.info("Price data found.") return priceInfo
def __init__(self, dbpath): super(MainWindow, self).__init__() # 'Add to collection' window self.addWindow = None # 'Import games' window self.importWindow = None # Side panel self.sidePanel = SidePanel() # Tables and their databases db = QSqlDatabase.addDatabase("QSQLITE") db.setDatabaseName(dbpath) if not db.open(): logger.critical(f"Couldn't open database: {db.lastError().text()}") QMessageBox.critical(None, "Database Error", db.lastError().text()) self.gamesTableView = Table("games", db) self.gamesTableView.doubleClick.connect(self.sidePanel.showDetails) self.consolesTableView = Table("consoles", db) self.consolesTableView.doubleClick.connect(self.sidePanel.showDetails) self.accessoriesTableView = Table("accessories", db) self.accessoriesTableView.doubleClick.connect(self.sidePanel.showDetails) self.tableViewList = [self.gamesTableView, self.consolesTableView, self.accessoriesTableView] self.allPlatforms = set() self.allRegions = set() self.allGenres = set() self.allYears = set() for table in self.tableViewList: for row in table.ownedItems(): self.allPlatforms.add(row["platform"]) self.allRegions.add(row["region"]) self.allYears.add(row["year"]) # Split multi-genre entries for genre in row["genre"].split(", "): self.allGenres.add(genre) self.filterDock = FilterDock(sorted(self.allPlatforms, key=str.lower), sorted(self.allRegions, key=str.lower), sorted(self.allGenres, key=str.lower), sorted(self.allYears, key=str.lower)) # Overview tab self.overview = Overview(self.tableViewList) # Randomizer tab self.randomizer = Randomizer(self.gamesTableView.ownedItems(), sorted(self.allPlatforms, key=str.lower), sorted(self.allGenres, key=str.lower)) self.randomizer.consoleList.itemClicked.connect(self.updateStatusbar) self.randomizer.genreList.itemClicked.connect(self.updateStatusbar) self.randomizer.genreMatchExclusiveCB.stateChanged.connect(self.updateStatusbar) self.randomizer.btnAll.clicked.connect(self.updateStatusbar) self.randomizer.btnNone.clicked.connect(self.updateStatusbar) ## MainWindow layout # Widgets self.centralWidget = QWidget() self.setCentralWidget(self.centralWidget) self.tab = QTabWidget() self.toolbar = self.addToolBar("Exit") self.toolbar.addAction(self.buttonActions("exit")) self.toolbar.addAction(self.buttonActions("add")) self.toolbar.addAction(self.buttonActions("import")) self.fileMenu = self.menuBar().addMenu(self.tr("&File")) self.fileMenu.addAction(self.buttonActions("add")) self.fileMenu.addAction(self.buttonActions("export")) self.fileMenu.addAction(self.buttonActions("import")) self.fileMenu.addAction(self.buttonActions("steam")) self.fileMenu.addAction(self.buttonActions("fetch")) self.fileMenu.insertSeparator(self.buttonActions("exit")) self.fileMenu.addAction(self.buttonActions("exit")) self.viewMenu = self.menuBar().addMenu(self.tr("&View")) self.viewMenu.addAction(self.buttonActions("owned")) self.viewMenu.addAction(self.buttonActions("delnotowned")) self.viewMenu.addAction(self.buttonActions("value")) self.helpMenu = self.menuBar().addMenu(self.tr("&Help")) self.helpMenu.addAction(self.buttonActions("about")) self.statusProgressBar = QProgressBar() self.statusProgressBar.setMaximumSize(100, 15) self.statusProgressBar.setRange(0, 0) self.statusProgressBar.setVisible(False) self.statusBar().addPermanentWidget(self.statusProgressBar) # Search stuff self.searchLabel = QLabel("Search") self.searchLabel.setVisible(False) self.searchBox = QLineEdit() self.searchBox.setVisible(False) self.searchBox.setClearButtonEnabled(True) # self.searchBox.textChanged.connect(self.search) self.searchBox.returnPressed.connect(self.search) self.searchBtn = QPushButton("Search") self.searchBtn.clicked.connect(self.search) self.searchBtn.setVisible(False) self.filterBtn = QPushButton("Filter") self.filterBtn.clicked.connect(self.filterDock.toggleVisibility) self.filterBtn.setVisible(False) # Tab layout. self.tab.addTab(self.overview.widget, "Overview") self.tab.addTab(self.gamesTableView, "Games") self.tab.addTab(self.consolesTableView, "Consoles") self.tab.addTab(self.accessoriesTableView, "Accessories") self.tab.addTab(self.randomizer.widget, "Randomizer") self.tab.currentChanged.connect(self.search) self.tab.currentChanged.connect(self.sidePanel.hideDetails) # Connect sidePanel's saved signal to corresponding table's updateData() # TODO: Update the sets of platforms and genres properly self.sidePanel.saved.connect(self.tableViewList[self.tab.currentIndex()].updateData) self.sidePanel.saved.connect(lambda: self.randomizer.updateLists(self.gamesTableView.ownedItems(), sorted(self.allPlatforms, key=str.lower), sorted(self.allGenres, key=str.lower))) # Main layout self.tabHbox = QHBoxLayout() self.tabHbox.addWidget(self.tab, 1) self.tabHbox.addWidget(self.sidePanel, 1) self.advSearchHbox = QHBoxLayout() self.advSearchHbox.addWidget(self.filterDock, 0) self.searchHbox = QHBoxLayout() self.searchHbox.addWidget(self.searchLabel, 0) self.searchHbox.addWidget(self.searchBox, 1) self.searchHbox.addWidget(self.filterBtn, 0) self.searchHbox.addWidget(self.searchBtn, 0) self.mainLayout = QVBoxLayout() self.mainLayout.addLayout(self.tabHbox, 1) self.mainLayout.addLayout(self.advSearchHbox, 0) self.mainLayout.addLayout(self.searchHbox, 0) self.centralWidget.setLayout(self.mainLayout) # Make sure screen geometry is big enough. Otherwise set window to maximized. gSize = QApplication.desktop().availableGeometry() if gSize.width() <= 1280 or gSize.height() <= 768: logger.info("Screen geometry smaller than 1280x768. Setting window to maximized mode.") self.showMaximized() else: self.resize(1280, 768) self.center() self.setWindowTitle(f"Game Collection Manager v{_VERSION}") self.statusBar().showMessage("")
def updateYears(self, years): self._years.clear() self._years.addItems(years) logger.info("Updated years list.")
def updateGenres(self, genres): self._genres.clear() self._genres.addItems(genres) logger.info("Updated genres list.")
def updateRegions(self, regions): self._regions.clear() self._regions.addItems(regions) logger.info("Updated regions list.")
def updatePlatforms(self, platforms): self._platforms.clear() self._platforms.addItems(platforms) logger.info("Updated platforms list.")
def main(): gcmDir = getScriptDir() dbPath = gcmDir + "/data/db/collection.db" logger.info(f"Program directory set to {gcmDir}.") # Make sure we have everything if not os.path.exists(gcmDir + "/data"): logger.critical( "Data directory doesn't exist. Creating '/data/db' and '/data/vgdb'..." ) os.makedirs(gcmDir + "/data/db") os.mkdir(gcmDir + "/data/vgdb") createDB(dbPath) if not os.path.exists(gcmDir + "/data/db"): logger.critical("'/data/db' doest't exist. Creating...") os.mkdir(gcmDir + "/data/db") createDB(dbPath) if not os.path.exists(gcmDir + "/data/vgdb"): os.mkdir(gcmDir + "/data/vgdb") if not os.path.exists(dbPath): logger.critical("No database was found.") createDB(dbPath) createWindow(dbPath) if __name__ == "__main__": logger.info("Starting up...") main()