1
0
Fork 0

Sort RIPs after non-RIPs.

Uglify output table per YouSmellFunky's request. Removes dependency on tabulate.
Rename Games->Game, Updates->Update in table header.
Abbreviate number of reviews using SI prefixes.
This commit is contained in:
Casper V. Kristensen 2018-08-03 03:30:17 +02:00
parent 419333fac3
commit d109ce92c5
Signed by: caspervk
GPG key ID: B1156723DB3BDDA8
5 changed files with 112 additions and 75 deletions

View file

@ -10,9 +10,8 @@ from datetime import datetime, timedelta
import prawcore import prawcore
import requests_cache import requests_cache
from tabulate import tabulate
from dailyreleases import config, __version__ from dailyreleases import config, __version__, util
from dailyreleases.gog import GOG from dailyreleases.gog import GOG
from dailyreleases.predb import Predb from dailyreleases.predb import Predb
from dailyreleases.reddit import Reddit from dailyreleases.reddit import Reddit
@ -148,16 +147,15 @@ class DailyReleasesBot(object):
else: else:
platform = "Windows" platform = "Windows"
# Find type (games/dlc/updates) # Find type (game/dlc/update)
rls_type = "Games" # Order of the if-statements is important: Update trumps DLC because an update to a DLC is an update, not a DLC!
rls_type = "Game"
# Order of the if-statements is important: update trumps dlc because an update to a DLC is an update, not a dlc!
if re.search("(?<!incl[._-])dlc", # 'Incl.DLC' isn't a DLC-release if re.search("(?<!incl[._-])dlc", # 'Incl.DLC' isn't a DLC-release
rls_name, flags=re.IGNORECASE): rls_name, flags=re.IGNORECASE):
rls_type = "DLC" rls_type = "DLC"
if re.search("update|v[0-9]|addon|Crack[._-]?fix|DIR[._-]?FIX|build[._-]?[0-9]+", if re.search("update|v[0-9]|addon|Crack[._-]?fix|DIR[._-]?FIX|build[._-]?[0-9]+",
rls_name, flags=re.IGNORECASE): rls_name, flags=re.IGNORECASE):
rls_type = "Updates" rls_type = "Update"
logger.info("Offline: %s %s : %s - %s", platform, rls_type, game_name, group) logger.info("Offline: %s %s : %s - %s", platform, rls_type, game_name, group)
logger.info("Tags: %s. Highlights: %s", tags, highlights) logger.info("Tags: %s. Highlights: %s", tags, highlights)
@ -170,10 +168,9 @@ class DailyReleasesBot(object):
logger.info("Skipping %s: no store link (probably software)", dirname) logger.info("Skipping %s: no store link (probably software)", dirname)
return return
# Sadly, Steam is the de facto pc game store. Therefore, (almost) all the popular games are published there. # Game score and number of reviews is -1 by default; it is updated if the game exists on Steam
# For this reason, game popularity and score is -1 be default; it is updated if the game exists on Steam. score = -1
popularity = -1 num_reviews = -1
review_score = -1
# If one of the store links we found is to Steam, use their API to get (better) information about the game. # If one of the store links we found is to Steam, use their API to get (better) information about the game.
# Note: Doesn't apply to Steam bundles, as Steam has no public API for those. # Note: Doesn't apply to Steam bundles, as Steam has no public API for those.
@ -188,11 +185,11 @@ class DailyReleasesBot(object):
# Set game name to package name (e.g. 'Fallout New Vegas Ultimate' instead of 'Fallout New Vegas') # Set game name to package name (e.g. 'Fallout New Vegas Ultimate' instead of 'Fallout New Vegas')
game_name = steam_packagedetails["name"] game_name = steam_packagedetails["name"]
# Find the "base game" of the package (the most popular app in the package) # Find "base game" of the package; the most popular app (i.e. the one with the most reviews)
steam_package_appids = {str(app["id"]) for app in steam_packagedetails["apps"]} steam_package_appids = [str(app["id"]) for app in steam_packagedetails["apps"]]
steam_package_appdetails = [self.steam.appdetails(appid) for appid in steam_package_appids] steam_package_apps_appdetails = [self.steam.appdetails(appid) for appid in steam_package_appids]
steam_package_basegame_appdetails = max(steam_package_appdetails, steam_package_basegame_appdetails = max(steam_package_apps_appdetails,
key=lambda app: self.steam.popularity(app["steam_appid"])) key=lambda app: self.steam.reviews(app["steam_appid"]).number)
# Use the base game of the package as the basis for further computation # Use the base game of the package as the basis for further computation
steam_appdetails = steam_package_basegame_appdetails steam_appdetails = steam_package_basegame_appdetails
@ -203,14 +200,13 @@ class DailyReleasesBot(object):
steam_appdetails = self.steam.appdetails(steam_appid) steam_appdetails = self.steam.appdetails(steam_appid)
game_name = steam_appdetails["name"] game_name = steam_appdetails["name"]
# Now that we have chosen a single Steam game to represent the release, use it to improve the information # Now that we have a single Steam game to represent the release, use it to improve the information
popularity = self.steam.popularity(steam_appid) score, num_reviews = self.steam.reviews(steam_appid)
review_score = self.steam.review_score(steam_appid)
# DLC releases doesn't always contain the word "dlc" (e.g. 'Fallout New Vegas: Dead Money'), so some DLCs # DLC releases doesn't always contain the word "dlc" (e.g. 'Fallout New Vegas: Dead Money'), so some DLCs
# get mislabeled as games during offline parsing. We can use the Steam API to get the correct release type, # get mislabeled as games during offline parsing. We can use the Steam API to get the correct release type,
# but if the release was already deemed an update, keep it as such, because an update to a DLC is an update. # but if the release was already deemed an update, keep it as such, because an update to a DLC is an update.
if rls_type != "Updates" and steam_appdetails["type"] == "dlc": if steam_appdetails["type"] == "dlc" and rls_type != "Update":
rls_type = "DLC" rls_type = "DLC"
# If Steam links to a 3rd-party EULAs, check it for the word "denuvo" and add a highlight if it occurs # If Steam links to a 3rd-party EULAs, check it for the word "denuvo" and add a highlight if it occurs
@ -231,8 +227,8 @@ class DailyReleasesBot(object):
"type": rls_type, "type": rls_type,
"platform": platform, "platform": platform,
"store_links": store_links, "store_links": store_links,
"popularity": popularity, "score": score,
"review_score": review_score, "num_reviews": num_reviews,
"tags": tags, "tags": tags,
"highlights": highlights "highlights": highlights
} }
@ -299,41 +295,53 @@ class DailyReleasesBot(object):
post.append(f"# {platform_name}") post.append(f"# {platform_name}")
for type_name, type_releases in sorted(platform_releases.items(), for type_name, type_releases in sorted(platform_releases.items(),
key=lambda n: ("Games", "Updates", "DLC").index(n[0])): key=lambda n: ("Game", "Update", "DLC").index(n[0])):
# Skip release type if there are no releases for it # Skip release type if there are no releases for it
if not type_releases: if not type_releases:
continue continue
# The sub-tables for updates will use the entire rls_name as the name, while games and dlcs will show # Releases in the tables are grouped by release group, and the groups are ordered according to the most
# tags and highlights, as well as the actual game_name # popular game within the group. Games are sorted by popularity internally in the groups as well.
def row(r): # The popularity of a game is defined by the number of reviews it has on Steam. The popularity of the
if type_name == "Updates": # release itself extends this definition, but ranks RIPs lower than non-RIPS.
name = "[{}]({})".format(r["rls_name"], r["nfo_link"]) def popularity(rls):
else: is_rip = "RIP" in [tag.upper() for tag in rls["tags"]]
tags = " ({})".format(" ".join(r["tags"])) if r["tags"] else "" return rls["num_reviews"], not is_rip
highlights = " **- {}**".format(", ".join(r["highlights"])) if r["highlights"] else ""
name = "[{}{}]({}){}".format(r["game_name"], tags, r["nfo_link"], highlights)
if r["review_score"] != -1: group_order = defaultdict(lambda: (-1, False))
reviews = "{:.0%} ^^\({}\)".format(r["review_score"], r["popularity"])
else:
reviews = "-"
stores = ", ".join(f"[{name}]({link})" for name, link in r["store_links"].items())
return name, r["group"], reviews, stores
# Releases in the sub-tables are grouped by release group, and the groups are ordered according to the
# most popular game within the group. Games are sorted by popularity internally in the groups.
group_popularity = defaultdict(int)
for rls in type_releases: for rls in type_releases:
group_popularity[rls["group"]] = max(group_popularity[rls["group"]], rls["popularity"]) group = rls["group"]
group_order[group] = max(group_order[group], popularity(rls))
table = [row(rls) sorted_releases = sorted(type_releases,
for rls in sorted(type_releases, key=lambda r: (group_order[r["group"]],
key=lambda r: (group_popularity[r["group"]], r["group"], r["popularity"]), r["group"], # ensure grouping if two groups share group_order
reverse=True)] popularity(r)),
post.append(tabulate(table, headers=(type_name, "Group", "Reviews", "Stores"), tablefmt="pipe")) reverse=True)
# The rows in tables containing updates will use the full rls_name as the name, while tables containing
# game and DLC releases will show tags and highlights, as well as the actual stylized game_name.
def row(rls):
if type_name == "Update":
name = "[{}]({})".format(rls["rls_name"], rls["nfo_link"])
else:
tags = " ({})".format(" ".join(rls["tags"])) if rls["tags"] else ""
highlights = " **- {}**".format(", ".join(rls["highlights"])) if rls["highlights"] else ""
name = "[{}{}]({}){}".format(rls["game_name"], tags, rls["nfo_link"], highlights)
if rls["score"] == -1:
reviews = "-"
else:
num_reviews_humanized = util.humanize(rls["num_reviews"], precision=1, prefix="dec", suffix="")
reviews = "{:.0%} ({})".format(rls["score"], num_reviews_humanized)
stores = ", ".join(f"[{name}]({link})" for name, link in rls["store_links"].items())
return name, rls["group"], stores, reviews
post.append(f"| {type_name} | Group | Store | Score (Reviews) |")
post.append("|:-|:-|:-|:-|")
post.extend("| {} | {} | {} | {} |".format(*row(rls)) for rls in sorted_releases)
post.append("") post.append("")
post.append("&nbsp;") post.append("&nbsp;")

View file

@ -1,4 +1,7 @@
import logging import logging
from collections import namedtuple
from dailyreleases import util
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -33,14 +36,15 @@ class Steam(object):
r = self.cache.get(f"https://store.steampowered.com/appreviews/{appid}", params=payload) r = self.cache.get(f"https://store.steampowered.com/appreviews/{appid}", params=payload)
return r.json()["query_summary"] return r.json()["query_summary"]
def review_score(self, appid): def reviews(self, appid):
app_review = self.appreviews(appid) app_review = self.appreviews(appid)
if app_review["total_reviews"] == 0: Reviews = namedtuple("Reviews", ("score", "number"))
return -1
return app_review["total_positive"] / app_review["total_reviews"]
def popularity(self, appid): if app_review["total_reviews"] == 0:
return self.appreviews(appid)["total_reviews"] return Reviews(-1, -1)
positive = app_review["total_positive"] / app_review["total_reviews"]
return Reviews(positive, app_review["total_reviews"])
def eula(self, appid): def eula(self, appid):
return self.cache.get(f"https://store.steampowered.com//eula/{appid}_eula_0").text return self.cache.get(f"https://store.steampowered.com//eula/{appid}_eula_0").text

View file

@ -1,6 +1,31 @@
import difflib import difflib
def humanize(n, precision=2, prefix="bin", suffix="B") -> str:
"""
Return a humanized string representation of a number (of bytes).
Adapted from Doug Latornell - http://code.activestate.com/recipes/577081/
"""
abbrevs = {
"dec": [
(1000 ** 5, 'P' + suffix),
(1000 ** 4, 'T' + suffix),
(1000 ** 3, 'G' + suffix),
(1000 ** 2, 'M' + suffix),
(1000 ** 1, 'k' + suffix)
],
"bin": [
(1 << 50, 'Pi' + suffix),
(1 << 40, 'Ti' + suffix),
(1 << 30, 'Gi' + suffix),
(1 << 20, 'Mi' + suffix),
(1 << 10, 'ki' + suffix)
]
}
factor, suffix = next(((f, s) for f, s in abbrevs[prefix] if n >= f), (1, suffix))
return "{1:.{0}f}".format(precision, n / factor).rstrip("0").rstrip(".") + suffix
def case_insensitive_close_matches(word, possibilities, n=3, cutoff=0.6): def case_insensitive_close_matches(word, possibilities, n=3, cutoff=0.6):
""" """
Python's difflib.get_close_matches does case sensitive sequence matching, this function decorates the library Python's difflib.get_close_matches does case sensitive sequence matching, this function decorates the library

View file

@ -40,8 +40,7 @@ setup(
"requests", "requests",
"requests_cache", "requests_cache",
"praw", "praw",
"beautifulsoup4", "beautifulsoup4"
"tabulate"
], ],
entry_points={ entry_points={
"console_scripts": [ "console_scripts": [

View file

@ -14,7 +14,7 @@ class ParseDirnameTestCase(unittest.TestCase):
self.assertEqual("Aztez", p["rls_name"]) self.assertEqual("Aztez", p["rls_name"])
self.assertEqual("Aztez", p["game_name"]) self.assertEqual("Aztez", p["game_name"])
self.assertEqual("Windows", p["platform"]) self.assertEqual("Windows", p["platform"])
self.assertEqual("Games", p["type"]) self.assertEqual("Game", p["type"])
self.assertEqual("DARKSiDERS", p["group"]) self.assertEqual("DARKSiDERS", p["group"])
self.assertIn("store.steampowered.com/app/244750", p["store_links"]["Steam"]) self.assertIn("store.steampowered.com/app/244750", p["store_links"]["Steam"])
self.assertEqual([], p["tags"]) self.assertEqual([], p["tags"])
@ -27,7 +27,7 @@ class ParseDirnameTestCase(unittest.TestCase):
def test_update(self): def test_update(self):
p = self.bot.parse_dirname("Car.Mechanic.Simulator.2018.Plymouth.Update.v1.5.1.Hotfix-PLAZA") p = self.bot.parse_dirname("Car.Mechanic.Simulator.2018.Plymouth.Update.v1.5.1.Hotfix-PLAZA")
self.assertEqual("Updates", p["type"]) self.assertEqual("Update", p["type"])
def test_proper_highlight(self): def test_proper_highlight(self):
p = self.bot.parse_dirname("Death.Coming.PROPER-SiMPLEX") p = self.bot.parse_dirname("Death.Coming.PROPER-SiMPLEX")
@ -36,17 +36,17 @@ class ParseDirnameTestCase(unittest.TestCase):
def test_macos_release(self): def test_macos_release(self):
p = self.bot.parse_dirname("The_Fall_Part_2_Unbound_MacOS-Razor1911") p = self.bot.parse_dirname("The_Fall_Part_2_Unbound_MacOS-Razor1911")
self.assertEqual("Mac OSX", p["platform"]) self.assertEqual("Mac OSX", p["platform"])
self.assertEqual("Games", p["type"]) self.assertEqual("Game", p["type"])
def test_macosx_update(self): def test_macosx_update(self):
p = self.bot.parse_dirname("Man_O_War_Corsair_Warhammer_Naval_Battles_v1.3.2_MacOSX-Razor1911") p = self.bot.parse_dirname("Man_O_War_Corsair_Warhammer_Naval_Battles_v1.3.2_MacOSX-Razor1911")
self.assertEqual("Mac OSX", p["platform"]) self.assertEqual("Mac OSX", p["platform"])
self.assertEqual("Updates", p["type"]) self.assertEqual("Update", p["type"])
def test_linux_release(self): def test_linux_release(self):
p = self.bot.parse_dirname("Sphinx_And_The_Cursed_Mummy_Linux-Razor1911") p = self.bot.parse_dirname("Sphinx_And_The_Cursed_Mummy_Linux-Razor1911")
self.assertEqual("Linux", p["platform"]) self.assertEqual("Linux", p["platform"])
self.assertEqual("Games", p["type"]) self.assertEqual("Game", p["type"])
def test_dlc_explicit(self): def test_dlc_explicit(self):
p = self.bot.parse_dirname("Fallout.4.Far.Harbor.DLC-CODEX") p = self.bot.parse_dirname("Fallout.4.Far.Harbor.DLC-CODEX")
@ -59,30 +59,31 @@ class ParseDirnameTestCase(unittest.TestCase):
def test_incl_dlc_update(self): def test_incl_dlc_update(self):
p = self.bot.parse_dirname("Wolfenstein.II.The.New.Colossus.Update.5.incl.DLC-CODEX") p = self.bot.parse_dirname("Wolfenstein.II.The.New.Colossus.Update.5.incl.DLC-CODEX")
self.assertEqual("Updates", p["type"]) self.assertEqual("Update", p["type"])
def test_incl_dlc_release(self): def test_incl_dlc_release(self):
p = self.bot.parse_dirname("Mutiny.Incl.DLC-DARKSiDERS") p = self.bot.parse_dirname("Mutiny.Incl.DLC-DARKSiDERS")
self.assertEqual("Games", p["type"]) self.assertEqual("Game", p["type"])
def test_popularity_steam(self): def test_score_steam(self):
p1 = self.bot.parse_dirname("BioShock_Infinite-FLT") p1 = self.bot.parse_dirname("BioShock_Infinite-FLT")
p2 = self.bot.parse_dirname("Duke.Nukem.Forever.Complete-PLAZA") p2 = self.bot.parse_dirname("Duke.Nukem.Forever.Complete-PLAZA")
self.assertGreater(p1["popularity"], p2["popularity"]) self.assertGreater(p1["score"], p2["score"])
def test_non_steam(self): def test_non_steam(self):
p = self.bot.parse_dirname("Battlefield.1.REPACK-CPY") p = self.bot.parse_dirname("Battlefield.1.REPACK-CPY")
self.assertIn("www.origin.com/usa/en-us/store/battlefield/battlefield-1", p["store_links"]["Origin"]) self.assertIn("www.origin.com/usa/en-us/store/battlefield/battlefield-1", p["store_links"]["Origin"])
self.assertEqual(-1, p["popularity"]) self.assertEqual(-1, p["score"])
self.assertEqual(-1, p["num_reviews"])
def test_gog_exclusive(self): def test_gog_exclusive(self):
p = self.bot.parse_dirname("Dungeons.and.Dragons.Dragonshard.v2.0.0.10.Multilingual-DELiGHT") p = self.bot.parse_dirname("Dungeons.and.Dragons.Dragonshard.v2.0.0.10.Multilingual-DELiGHT")
self.assertIn("gog.com/game/dungeons_dragons_dragonshard", p["store_links"]["GOG"]) self.assertIn("gog.com/game/dungeons_dragons_dragonshard", p["store_links"]["GOG"])
self.assertEqual(-1, p["popularity"]) self.assertEqual(-1, p["score"])
def test_popularity_non_steam(self): def test_score_non_steam(self):
p = self.bot.parse_dirname("Ode.RIP.MULTI12-SiMPLEX") p = self.bot.parse_dirname("Ode.RIP.MULTI12-SiMPLEX")
self.assertEqual(-1, p["popularity"]) self.assertEqual(-1, p["score"])
def test_tags(self): def test_tags(self):
p = self.bot.parse_dirname("Teenage.Mutant.Ninja.Turtles.Portal.Power.RIP.MULTI8-SiMPLEX") p = self.bot.parse_dirname("Teenage.Mutant.Ninja.Turtles.Portal.Power.RIP.MULTI8-SiMPLEX")
@ -95,13 +96,13 @@ class ParseDirnameTestCase(unittest.TestCase):
def test_steam_package(self): def test_steam_package(self):
p = self.bot.parse_dirname("Farming.Simulator.17.Platinum.Edition.Update.v1.5.3-BAT") p = self.bot.parse_dirname("Farming.Simulator.17.Platinum.Edition.Update.v1.5.3-BAT")
self.assertEqual("Farming Simulator 17 - Platinum Edition", p["game_name"]) self.assertEqual("Farming Simulator 17 - Platinum Edition", p["game_name"])
self.assertEqual("Updates", p["type"]) self.assertEqual("Update", p["type"])
self.assertIn("store.steampowered.com/sub/202103", p["store_links"]["Steam"]) self.assertIn("store.steampowered.com/sub/202103", p["store_links"]["Steam"])
def test_steam_package_with_dlc_first(self): def test_steam_package_with_dlc_first(self):
p = self.bot.parse_dirname("The.Witcher.3.Wild.Hunt.Game.of.The.Year.Edition-RELOADED") p = self.bot.parse_dirname("The.Witcher.3.Wild.Hunt.Game.of.The.Year.Edition-RELOADED")
self.assertEqual("The Witcher 3: Wild Hunt - Game of the Year Edition", p["game_name"]) self.assertEqual("The Witcher 3: Wild Hunt - Game of the Year Edition", p["game_name"])
self.assertEqual("Games", p["type"]) self.assertEqual("Game", p["type"])
self.assertIn("store.steampowered.com/sub/124923", p["store_links"]["Steam"]) self.assertIn("store.steampowered.com/sub/124923", p["store_links"]["Steam"])
def test_steam_bundle(self): def test_steam_bundle(self):
@ -109,7 +110,7 @@ class ParseDirnameTestCase(unittest.TestCase):
self.assertEqual("Valve.Complete.Pack-FAKE", p["dirname"]) self.assertEqual("Valve.Complete.Pack-FAKE", p["dirname"])
self.assertEqual("Valve Complete Pack", p["game_name"]) self.assertEqual("Valve Complete Pack", p["game_name"])
self.assertEqual("Windows", p["platform"]) self.assertEqual("Windows", p["platform"])
self.assertEqual("Games", p["type"]) self.assertEqual("Game", p["type"])
self.assertIn("store.steampowered.com/bundle/232", p["store_links"]["Steam"]) self.assertIn("store.steampowered.com/bundle/232", p["store_links"]["Steam"])
def test_denuvo_eula(self): def test_denuvo_eula(self):
@ -129,7 +130,7 @@ class ParseDirnameTestCase(unittest.TestCase):
def test_build_is_update(self): def test_build_is_update(self):
p = self.bot.parse_dirname("DUSK.Episode.1.Build.2.6-SKIDROW") p = self.bot.parse_dirname("DUSK.Episode.1.Build.2.6-SKIDROW")
self.assertEqual("Updates", p["type"]) self.assertEqual("Update", p["type"])
def test_readnfo_microsoft_store(self): def test_readnfo_microsoft_store(self):
p = self.bot.parse_dirname("Zoo.Tycoon.Ultimate.Animal.Collection.READNFO-CODEX") p = self.bot.parse_dirname("Zoo.Tycoon.Ultimate.Animal.Collection.READNFO-CODEX")