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:
parent
419333fac3
commit
d109ce92c5
|
@ -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(" ")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
3
setup.py
3
setup.py
|
@ -40,8 +40,7 @@ setup(
|
|||
"requests",
|
||||
"requests_cache",
|
||||
"praw",
|
||||
"beautifulsoup4",
|
||||
"tabulate"
|
||||
"beautifulsoup4"
|
||||
],
|
||||
entry_points={
|
||||
"console_scripts": [
|
||||
|
|
|
@ -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")
|
||||
|
|
Reference in a new issue