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 requests_cache
from tabulate import tabulate
from dailyreleases import config, __version__
from dailyreleases import config, __version__, util
from dailyreleases.gog import GOG
from dailyreleases.predb import Predb
from dailyreleases.reddit import Reddit
@ -148,16 +147,15 @@ class DailyReleasesBot(object):
else:
platform = "Windows"
# Find type (games/dlc/updates)
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!
# Find type (game/dlc/update)
# 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"
if re.search("(?<!incl[._-])dlc", # 'Incl.DLC' isn't a DLC-release
rls_name, flags=re.IGNORECASE):
rls_type = "DLC"
if re.search("update|v[0-9]|addon|Crack[._-]?fix|DIR[._-]?FIX|build[._-]?[0-9]+",
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("Tags: %s. Highlights: %s", tags, highlights)
@ -170,10 +168,9 @@ class DailyReleasesBot(object):
logger.info("Skipping %s: no store link (probably software)", dirname)
return
# Sadly, Steam is the de facto pc game store. Therefore, (almost) all the popular games are published there.
# For this reason, game popularity and score is -1 be default; it is updated if the game exists on Steam.
popularity = -1
review_score = -1
# Game score and number of reviews is -1 by default; it is updated if the game exists on Steam
score = -1
num_reviews = -1
# 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.
@ -188,11 +185,11 @@ class DailyReleasesBot(object):
# Set game name to package name (e.g. 'Fallout New Vegas Ultimate' instead of 'Fallout New Vegas')
game_name = steam_packagedetails["name"]
# Find the "base game" of the package (the most popular app in the package)
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_basegame_appdetails = max(steam_package_appdetails,
key=lambda app: self.steam.popularity(app["steam_appid"]))
# 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_apps_appdetails = [self.steam.appdetails(appid) for appid in steam_package_appids]
steam_package_basegame_appdetails = max(steam_package_apps_appdetails,
key=lambda app: self.steam.reviews(app["steam_appid"]).number)
# Use the base game of the package as the basis for further computation
steam_appdetails = steam_package_basegame_appdetails
@ -203,14 +200,13 @@ class DailyReleasesBot(object):
steam_appdetails = self.steam.appdetails(steam_appid)
game_name = steam_appdetails["name"]
# Now that we have chosen a single Steam game to represent the release, use it to improve the information
popularity = self.steam.popularity(steam_appid)
review_score = self.steam.review_score(steam_appid)
# Now that we have a single Steam game to represent the release, use it to improve the information
score, num_reviews = self.steam.reviews(steam_appid)
# 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,
# 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"
# 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,
"platform": platform,
"store_links": store_links,
"popularity": popularity,
"review_score": review_score,
"score": score,
"num_reviews": num_reviews,
"tags": tags,
"highlights": highlights
}
@ -299,41 +295,53 @@ class DailyReleasesBot(object):
post.append(f"# {platform_name}")
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
if not type_releases:
continue
# The sub-tables for updates will use the entire rls_name as the name, while games and dlcs will show
# tags and highlights, as well as the actual game_name
def row(r):
if type_name == "Updates":
name = "[{}]({})".format(r["rls_name"], r["nfo_link"])
else:
tags = " ({})".format(" ".join(r["tags"])) if r["tags"] else ""
highlights = " **- {}**".format(", ".join(r["highlights"])) if r["highlights"] else ""
name = "[{}{}]({}){}".format(r["game_name"], tags, r["nfo_link"], highlights)
# Releases in the 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 as well.
# The popularity of a game is defined by the number of reviews it has on Steam. The popularity of the
# release itself extends this definition, but ranks RIPs lower than non-RIPS.
def popularity(rls):
is_rip = "RIP" in [tag.upper() for tag in rls["tags"]]
return rls["num_reviews"], not is_rip
if r["review_score"] != -1:
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)
group_order = defaultdict(lambda: (-1, False))
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)
for rls in sorted(type_releases,
key=lambda r: (group_popularity[r["group"]], r["group"], r["popularity"]),
reverse=True)]
post.append(tabulate(table, headers=(type_name, "Group", "Reviews", "Stores"), tablefmt="pipe"))
sorted_releases = sorted(type_releases,
key=lambda r: (group_order[r["group"]],
r["group"], # ensure grouping if two groups share group_order
popularity(r)),
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("&nbsp;")

View file

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

View file

@ -1,6 +1,31 @@
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):
"""
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_cache",
"praw",
"beautifulsoup4",
"tabulate"
"beautifulsoup4"
],
entry_points={
"console_scripts": [

View file

@ -14,7 +14,7 @@ class ParseDirnameTestCase(unittest.TestCase):
self.assertEqual("Aztez", p["rls_name"])
self.assertEqual("Aztez", p["game_name"])
self.assertEqual("Windows", p["platform"])
self.assertEqual("Games", p["type"])
self.assertEqual("Game", p["type"])
self.assertEqual("DARKSiDERS", p["group"])
self.assertIn("store.steampowered.com/app/244750", p["store_links"]["Steam"])
self.assertEqual([], p["tags"])
@ -27,7 +27,7 @@ class ParseDirnameTestCase(unittest.TestCase):
def test_update(self):
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):
p = self.bot.parse_dirname("Death.Coming.PROPER-SiMPLEX")
@ -36,17 +36,17 @@ class ParseDirnameTestCase(unittest.TestCase):
def test_macos_release(self):
p = self.bot.parse_dirname("The_Fall_Part_2_Unbound_MacOS-Razor1911")
self.assertEqual("Mac OSX", p["platform"])
self.assertEqual("Games", p["type"])
self.assertEqual("Game", p["type"])
def test_macosx_update(self):
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("Updates", p["type"])
self.assertEqual("Update", p["type"])
def test_linux_release(self):
p = self.bot.parse_dirname("Sphinx_And_The_Cursed_Mummy_Linux-Razor1911")
self.assertEqual("Linux", p["platform"])
self.assertEqual("Games", p["type"])
self.assertEqual("Game", p["type"])
def test_dlc_explicit(self):
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):
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):
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")
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):
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.assertEqual(-1, p["popularity"])
self.assertEqual(-1, p["score"])
self.assertEqual(-1, p["num_reviews"])
def test_gog_exclusive(self):
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.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")
self.assertEqual(-1, p["popularity"])
self.assertEqual(-1, p["score"])
def test_tags(self):
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):
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("Updates", p["type"])
self.assertEqual("Update", p["type"])
self.assertIn("store.steampowered.com/sub/202103", p["store_links"]["Steam"])
def test_steam_package_with_dlc_first(self):
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("Games", p["type"])
self.assertEqual("Game", p["type"])
self.assertIn("store.steampowered.com/sub/124923", p["store_links"]["Steam"])
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", p["game_name"])
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"])
def test_denuvo_eula(self):
@ -129,7 +130,7 @@ class ParseDirnameTestCase(unittest.TestCase):
def test_build_is_update(self):
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):
p = self.bot.parse_dirname("Zoo.Tycoon.Ultimate.Animal.Collection.READNFO-CODEX")