In [1]:
# GRAPHS ARE AT THE BOTTOM!

# WARNING: The time numbers are from https://twitchtracker.com/quin69/games and the game sizes are
# a rough estimate based on the depot (all the files that are distributed via Steam's CDN for those games)
# file size for the game's files from https://steamdb.info!
# 
# Some game size data might be innacurately scraped, but the vast majority should be correct.
In [2]:
import requests
import fake_useragent


session = requests.session()
session.headers["User-Agent"] = fake_useragent.FakeUserAgent().random
In [3]:
import bs4


response = session.get("https://twitchtracker.com/quin69/games")
parsed = bs4.BeautifulSoup(response.text, "lxml")
In [4]:
import pprint
import typing as T


# In minutes
game_to_airtime: T.Dict[str, int] = {}
for row in parsed.select("#games tbody tr"):
    columns = row.select("td")
    number, game, avg_viewers, max_viewers, followers, total_airtime, last_seen = columns

    game_string = game.get_text(separator=" ", strip=True)
    total_airtime_string = total_airtime.get_text(separator=" ", strip=True)
    game_to_airtime[game_string] = int(total_airtime_string.split(None, 1)[0])


pprint.pprint(game_to_airtime)
{'A Chair in a Room: Greenwater': 245,
 'AFFECTED: The Manor': 40,
 'Accounting': 270,
 'Amnesia: Rebirth': 255,
 'Arizona Sunshine': 140,
 'Ashes of Creation': 130,
 "Baldur's Gate 3": 665,
 'Beat Saber': 1253,
 'Besiege': 435,
 'Blade & Sorcery': 110,
 'Bless Online': 614,
 'Bloodborne': 2435,
 'Bloons TD 6': 4850,
 'Borderlands 3': 30,
 'Cuphead': 885,
 'Cyberpunk 2077': 970,
 'DOOM Eternal': 2020,
 'Dark Souls': 2903,
 'Dark Souls II: Scholar of the First Sin': 3658,
 'Dark Souls III': 5293,
 'Darkest Dungeon': 7341,
 "Demon's Souls": 2163,
 'Destiny 2': 1679,
 'Diablo': 710,
 'Diablo II: Lord of Destruction': 1761,
 'Diablo III': 56460,
 'Diablo IV': 2074,
 'Diablo Immortal': 200,
 'Divinity: Original Sin II': 2993,
 "Don't Knock Twice": 120,
 'Dreadhalls': 130,
 'Duck Season': 75,
 'Exanima': 480,
 'Fall Guys: Ultimate Knockout': 795,
 'Fallout 4': 630,
 'Final Fantasy VII Remake': 2887,
 'GeoGuessr': 70,
 'Getting Over It with Bennett Foddy': 1110,
 'God of War': 459,
 'Godfall': 215,
 'Green Hell': 1580,
 'Grim Dawn': 747,
 'Grounded': 110,
 'Hades': 3670,
 'Half-Life: Alyx': 1451,
 'Heroes of the Storm': 200,
 'Hot Dogs, Horseshoes & Hand Grenades': 75,
 'I Wanna Be The Boshy': 135,
 'IRL': 395,
 'In Your Face': 275,
 'Jump King': 90,
 'Just Chatting': 16362,
 'LOST ARK': 14125,
 'Last Epoch': 2055,
 'Limbo': 295,
 'Lineage 2: Revolution': 630,
 'Little Misfortune': 250,
 'Microsoft Flight Simulator': 120,
 'Minecraft': 9239,
 'Mortal Shell': 979,
 'Mount & Blade II: Bannerlord': 295,
 'New World': 1090,
 'Oops!!! I Slept With Your Mom': 30,
 'Papers, Please': 90,
 'Paranormal Activity: The Lost Soul': 225,
 'Path of Exile': 222300,
 'Poly Bridge 2': 170,
 'Portal': 230,
 'Portal 2': 540,
 'Risk of Rain 2': 105,
 'SUPERHOT VR': 80,
 'Sekiro: Shadows Die Twice': 3399,
 'Slay the Spire': 1795,
 'Special Events': 1521,
 'Stilt Fella': 165,
 'Subnautica': 2520,
 'Superliminal': 175,
 'Terraria': 13716,
 'The Elder Scrolls V: Skyrim': 1748,
 'The Exorcist: Legion VR': 295,
 'The Stanley Parable Mod': 200,
 'Totally Accurate Battle Simulator': 265,
 'Transference': 60,
 'Unknown': 90,
 'VRChat': 1045,
 'Valheim': 1486,
 'Warcraft III: Reforged': 115,
 'Warhammer: Chaosbane': 295,
 'Welcome to the Game II': 2460,
 'Who Wants To Be A Millionaire': 995,
 'Who Wants to Be a Millionaire?': 70,
 'Wolcen: Lords of Mayhem': 6236,
 'World of Warcraft': 353067,
 'World of Warcraft: Battle for Azeroth': 30}
In [5]:
# Have to use a webdriver because SteamDB isn't scraping friendly (CloudFlare checks)
import time
import urllib.parse

import selenium.webdriver
import selenium.common


SEARCH_URL_TEMPLATE = "https://steamdb.info/search/?a=app&q={}&type=1&category=0"
webdriver = selenium.webdriver.Chrome()


def find_steamdb_url(webdriver: selenium.webdriver.Remote, name: str) -> T.Optional[str]:
    url = SEARCH_URL_TEMPLATE.format(urllib.parse.quote_plus(name))
    webdriver.get(url)

    try:
        first_row = webdriver.find_element_by_css_selector("tr.app")
    except selenium.common.exceptions.NoSuchElementException:
        return None

    anchor = first_row.find_element_by_tag_name("a")
    return urllib.parse.urljoin(webdriver.current_url, anchor.get_attribute("href"))


def get_depot_url(steamdb_url: str) -> str:
    return steamdb_url.rstrip("/") + "/depots/"


def get_total_depot_size(webdriver: selenium.webdriver.Remote, depot_url: str) -> int:
    webdriver.get(depot_url)

    # Wait for page to load in a naive fashion
    time.sleep(3)
    total_size = 0
    for row in webdriver.find_elements_by_css_selector("#depots .table-responsive:first-of-type tbody tr"):
        columns = row.find_elements_by_tag_name("td")
        id_, name, max_size, os_, extra_info = columns
        total_size += int(max_size.get_attribute("data-sort"))

    return total_size


# Feel free to scrape it again, I don't want to
if False:
    # In bytes
    game_to_size: T.Dict[str, int] = {name: 0 for name in game_to_airtime}
    for index, (name, hours_played) in enumerate(game_to_airtime.items(), start=1):
        steamdb_url = find_steamdb_url(webdriver, name)
        if not steamdb_url:
            print(f"({index}/{len(game_to_airtime)})", name, "not found on SteamDB!")
            continue

        depot_url = get_depot_url(steamdb_url)
        depot_size = get_total_depot_size(webdriver, depot_url)
        print(f"({index}/{len(game_to_airtime)})", name, steamdb_url, depot_size)
        game_to_size[name] = depot_size
else:
    game_to_size = {
        "Path of Exile": 29589377891,
        "Diablo III": 5981923060,
        "Just Chatting": 26848396,
        "LOST ARK": 2654020720,
        "Terraria": 1847376272,
        "Minecraft": 14307586923,
        "Darkest Dungeon": 12066824156,
        "Wolcen: Lords of Mayhem": 109170195059,
        "Dark Souls III": 27630425036,
        "Bloons TD 6": 2123322021,
        "Hades": 27011131823,
        "Dark Souls II: Scholar of the First Sin": 38751409296,
        "Sekiro: Shadows Die Twice": 16108432532,
        "Divinity: Original Sin II": 97066890477,
        "Dark Souls": 7813699112,
        "Final Fantasy VII Remake": 31346367083,
        "Subnautica": 20859143166,
        "Welcome to the Game II": 5767667201,
        "Demon's Souls": 79403560322,
        "Diablo IV": 6881450057,
        "Last Epoch": 49970534162,
        "DOOM Eternal": 60026510027,
        "Slay the Spire": 2360246842,
        "Diablo II: Lord of Destruction": 10614589615,
        "The Elder Scrolls V: Skyrim": 29864781871,
        "Destiny 2": 76234637928,
        "Green Hell": 9892099952,
        "Half-Life: Alyx": 12131350186,
        "Beat Saber": 752689827,
        "Getting Over It with Bennett Foddy": 5231878019,
        "New World": 35220626108,
        "VRChat": 771268527,
        "Who Wants To Be A Millionaire": 6755493072,
        "Mortal Shell": 0,
        "Cyberpunk 2077": 117759623745,
        "Cuphead": 25009954846,
        "Fall Guys: Ultimate Knockout": 5663789458,
        "Grim Dawn": 10383299063,
        "Diablo": 333962365,
        "Baldur's Gate 3": 177857008731,
        "Lineage 2: Revolution": 8262115020,
        "Fallout 4": 53548454638,
        "Bless Online": 44348434393,
        "Portal 2": 357179213,
        "Exanima": 4429205180,
        "God of War": 1186924328,
        "Besiege": 9883393946,
        "Warhammer: Chaosbane": 58238753224,
        "Limbo": 3206400933,
        "Mount & Blade II: Bannerlord": 65992170893,
        "The Exorcist: Legion VR": 4987498428,
        "In Your Face": 329645151,
        "Accounting": 2302197925,
        "Totally Accurate Battle Simulator": 9794291046,
        "Amnesia: Rebirth": 34914707637,
        "Little Misfortune": 19764855934,
        "A Chair in a Room: Greenwater": 4162988589,
        "Portal": 357179213,
        "Paranormal Activity: The Lost Soul": 6176413889,
        "Godfall": 284533973,
        "Heroes of the Storm": 2082985997,
        "Diablo Immortal": 268337008,
        "The Stanley Parable Mod": 925170085,
        "Superliminal": 34755598440,
        "Poly Bridge 2": 1812541029,
        "Stilt Fella": 395362514,
        "Arizona Sunshine": 28572819379,
        "I Wanna Be The Boshy": 486910471,
        "Ashes of Creation": 6777024339,
        "Dreadhalls": 1013220341,
        "Microsoft Flight Simulator": 1539007973,
        "Don't Knock Twice": 2614746810,
        "Warcraft III: Reforged": 5981923060,
        "Blade & Sorcery": 8206475119,
        "Grounded": 4591337959,
        "Risk of Rain 2": 3307273090,
        "Jump King": 1782741399,
        "Unknown": 60974180387,
        "Papers, Please": 204661900,
        "SUPERHOT VR": 6628592425,
        "Duck Season": 11868750058,
        "Hot Dogs, Horseshoes & Hand Grenades": 15421675716,
        "Who Wants to Be a Millionaire?": 6755493072,
        "Transference": 11227835772,
        "AFFECTED: The Manor": 3690441537,
        "Oops!!! I Slept With Your Mom": 181920004,
        "World of Warcraft: Battle for Azeroth": 8302308185,
        "Borderlands 3": 128964722965,
        "Valheim": 2843296311,
}

print()
pprint.pprint(game_to_size)
{'A Chair in a Room: Greenwater': 4162988589,
 'AFFECTED: The Manor': 3690441537,
 'Accounting': 2302197925,
 'Amnesia: Rebirth': 34914707637,
 'Arizona Sunshine': 28572819379,
 'Ashes of Creation': 6777024339,
 "Baldur's Gate 3": 177857008731,
 'Beat Saber': 752689827,
 'Besiege': 9883393946,
 'Blade & Sorcery': 8206475119,
 'Bless Online': 44348434393,
 'Bloons TD 6': 2123322021,
 'Borderlands 3': 128964722965,
 'Cuphead': 25009954846,
 'Cyberpunk 2077': 117759623745,
 'DOOM Eternal': 60026510027,
 'Dark Souls': 7813699112,
 'Dark Souls II: Scholar of the First Sin': 38751409296,
 'Dark Souls III': 27630425036,
 'Darkest Dungeon': 12066824156,
 "Demon's Souls": 79403560322,
 'Destiny 2': 76234637928,
 'Diablo': 333962365,
 'Diablo II: Lord of Destruction': 10614589615,
 'Diablo III': 5981923060,
 'Diablo IV': 6881450057,
 'Diablo Immortal': 268337008,
 'Divinity: Original Sin II': 97066890477,
 "Don't Knock Twice": 2614746810,
 'Dreadhalls': 1013220341,
 'Duck Season': 11868750058,
 'Exanima': 4429205180,
 'Fall Guys: Ultimate Knockout': 5663789458,
 'Fallout 4': 53548454638,
 'Final Fantasy VII Remake': 31346367083,
 'Getting Over It with Bennett Foddy': 5231878019,
 'God of War': 1186924328,
 'Godfall': 284533973,
 'Green Hell': 9892099952,
 'Grim Dawn': 10383299063,
 'Grounded': 4591337959,
 'Hades': 27011131823,
 'Half-Life: Alyx': 12131350186,
 'Heroes of the Storm': 2082985997,
 'Hot Dogs, Horseshoes & Hand Grenades': 15421675716,
 'I Wanna Be The Boshy': 486910471,
 'In Your Face': 329645151,
 'Jump King': 1782741399,
 'Just Chatting': 26848396,
 'LOST ARK': 2654020720,
 'Last Epoch': 49970534162,
 'Limbo': 3206400933,
 'Lineage 2: Revolution': 8262115020,
 'Little Misfortune': 19764855934,
 'Microsoft Flight Simulator': 1539007973,
 'Minecraft': 14307586923,
 'Mortal Shell': 0,
 'Mount & Blade II: Bannerlord': 65992170893,
 'New World': 35220626108,
 'Oops!!! I Slept With Your Mom': 181920004,
 'Papers, Please': 204661900,
 'Paranormal Activity: The Lost Soul': 6176413889,
 'Path of Exile': 29589377891,
 'Poly Bridge 2': 1812541029,
 'Portal': 357179213,
 'Portal 2': 357179213,
 'Risk of Rain 2': 3307273090,
 'SUPERHOT VR': 6628592425,
 'Sekiro: Shadows Die Twice': 16108432532,
 'Slay the Spire': 2360246842,
 'Stilt Fella': 395362514,
 'Subnautica': 20859143166,
 'Superliminal': 34755598440,
 'Terraria': 1847376272,
 'The Elder Scrolls V: Skyrim': 29864781871,
 'The Exorcist: Legion VR': 4987498428,
 'The Stanley Parable Mod': 925170085,
 'Totally Accurate Battle Simulator': 9794291046,
 'Transference': 11227835772,
 'Unknown': 60974180387,
 'VRChat': 771268527,
 'Valheim': 2843296311,
 'Warcraft III: Reforged': 5981923060,
 'Warhammer: Chaosbane': 58238753224,
 'Welcome to the Game II': 5767667201,
 'Who Wants To Be A Millionaire': 6755493072,
 'Who Wants to Be a Millionaire?': 6755493072,
 'Wolcen: Lords of Mayhem': 109170195059,
 'World of Warcraft: Battle for Azeroth': 8302308185}
In [6]:
import pprint


for name, size in game_to_size.items():
    if size == 0:
        print(name)
Mortal Shell
In [7]:
MEGABYTE_IN_BYTES = 1_000_000
GIGABYTE_IN_BYTES = MEGABYTE_IN_BYTES * 1000
GAMES_NOT_ON_STEAM = ["Diablo", "Diablo II", "Diablo III", "Diablo IV", "Diablo Immortal", "Lineage 2: Revolution", "Demon's Souls", "Diablo II: Lord of Destruction"]

# No depot info for Mortal Shell. Microsoft Flight Simulator's depot size is inaccurate, because it downloads the full game
# using a special downloader, which is not distributed over Steam.
PROBLEMATIC_GAMES = ["Special Events", "IRL", "Mortal Shell", "Microsoft Flight Simulator"]


# https://eu.battle.net/support/en/article/76459
game_to_size["World of Warcraft"] = 100 * GIGABYTE_IN_BYTES

# https://www.finder.com/complete-list-playstation-4-install-sizes-460-titles
# Take average of regular/GOTY since I have no idea what he played
game_to_size["Bloodborne"] = (27.19 + 32.75) / 2 * GIGABYTE_IN_BYTES

# Hard to estimate, since it uses external resources and is a browser game... Let's say 100mb
game_to_size["GeoGuessr"] = 100 * MEGABYTE_IN_BYTES


for non_game in PROBLEMATIC_GAMES + GAMES_NOT_ON_STEAM:
    if non_game in game_to_airtime:
        del game_to_airtime[non_game]

    if non_game in game_to_size:
        del game_to_size[non_game]
In [8]:
X = [size / GIGABYTE_IN_BYTES for name, size in sorted(game_to_size.items())]
Y = [airtime / 60 for name, airtime in sorted(game_to_airtime.items())]

# Do a sanity check, seems correct
assert len(X) == len(Y)
for game, size, airtime in zip(sorted(game_to_airtime), X, Y):
    print(f"{game} ({size:.2f}Gb): Played for {airtime:.2f} hours")
A Chair in a Room: Greenwater (4.16Gb): Played for 4.08 hours
AFFECTED: The Manor (3.69Gb): Played for 0.67 hours
Accounting (2.30Gb): Played for 4.50 hours
Amnesia: Rebirth (34.91Gb): Played for 4.25 hours
Arizona Sunshine (28.57Gb): Played for 2.33 hours
Ashes of Creation (6.78Gb): Played for 2.17 hours
Baldur's Gate 3 (177.86Gb): Played for 11.08 hours
Beat Saber (0.75Gb): Played for 20.88 hours
Besiege (9.88Gb): Played for 7.25 hours
Blade & Sorcery (8.21Gb): Played for 1.83 hours
Bless Online (44.35Gb): Played for 10.23 hours
Bloodborne (29.97Gb): Played for 40.58 hours
Bloons TD 6 (2.12Gb): Played for 80.83 hours
Borderlands 3 (128.96Gb): Played for 0.50 hours
Cuphead (25.01Gb): Played for 14.75 hours
Cyberpunk 2077 (117.76Gb): Played for 16.17 hours
DOOM Eternal (60.03Gb): Played for 33.67 hours
Dark Souls (7.81Gb): Played for 48.38 hours
Dark Souls II: Scholar of the First Sin (38.75Gb): Played for 60.97 hours
Dark Souls III (27.63Gb): Played for 88.22 hours
Darkest Dungeon (12.07Gb): Played for 122.35 hours
Destiny 2 (76.23Gb): Played for 27.98 hours
Divinity: Original Sin II (97.07Gb): Played for 49.88 hours
Don't Knock Twice (2.61Gb): Played for 2.00 hours
Dreadhalls (1.01Gb): Played for 2.17 hours
Duck Season (11.87Gb): Played for 1.25 hours
Exanima (4.43Gb): Played for 8.00 hours
Fall Guys: Ultimate Knockout (5.66Gb): Played for 13.25 hours
Fallout 4 (53.55Gb): Played for 10.50 hours
Final Fantasy VII Remake (31.35Gb): Played for 48.12 hours
GeoGuessr (0.10Gb): Played for 1.17 hours
Getting Over It with Bennett Foddy (5.23Gb): Played for 18.50 hours
God of War (1.19Gb): Played for 7.65 hours
Godfall (0.28Gb): Played for 3.58 hours
Green Hell (9.89Gb): Played for 26.33 hours
Grim Dawn (10.38Gb): Played for 12.45 hours
Grounded (4.59Gb): Played for 1.83 hours
Hades (27.01Gb): Played for 61.17 hours
Half-Life: Alyx (12.13Gb): Played for 24.18 hours
Heroes of the Storm (2.08Gb): Played for 3.33 hours
Hot Dogs, Horseshoes & Hand Grenades (15.42Gb): Played for 1.25 hours
I Wanna Be The Boshy (0.49Gb): Played for 2.25 hours
In Your Face (0.33Gb): Played for 4.58 hours
Jump King (1.78Gb): Played for 1.50 hours
Just Chatting (0.03Gb): Played for 272.70 hours
LOST ARK (2.65Gb): Played for 235.42 hours
Last Epoch (49.97Gb): Played for 34.25 hours
Limbo (3.21Gb): Played for 4.92 hours
Little Misfortune (19.76Gb): Played for 4.17 hours
Minecraft (14.31Gb): Played for 153.98 hours
Mount & Blade II: Bannerlord (65.99Gb): Played for 4.92 hours
New World (35.22Gb): Played for 18.17 hours
Oops!!! I Slept With Your Mom (0.18Gb): Played for 0.50 hours
Papers, Please (0.20Gb): Played for 1.50 hours
Paranormal Activity: The Lost Soul (6.18Gb): Played for 3.75 hours
Path of Exile (29.59Gb): Played for 3705.00 hours
Poly Bridge 2 (1.81Gb): Played for 2.83 hours
Portal (0.36Gb): Played for 3.83 hours
Portal 2 (0.36Gb): Played for 9.00 hours
Risk of Rain 2 (3.31Gb): Played for 1.75 hours
SUPERHOT VR (6.63Gb): Played for 1.33 hours
Sekiro: Shadows Die Twice (16.11Gb): Played for 56.65 hours
Slay the Spire (2.36Gb): Played for 29.92 hours
Stilt Fella (0.40Gb): Played for 2.75 hours
Subnautica (20.86Gb): Played for 42.00 hours
Superliminal (34.76Gb): Played for 2.92 hours
Terraria (1.85Gb): Played for 228.60 hours
The Elder Scrolls V: Skyrim (29.86Gb): Played for 29.13 hours
The Exorcist: Legion VR (4.99Gb): Played for 4.92 hours
The Stanley Parable Mod (0.93Gb): Played for 3.33 hours
Totally Accurate Battle Simulator (9.79Gb): Played for 4.42 hours
Transference (11.23Gb): Played for 1.00 hours
Unknown (60.97Gb): Played for 1.50 hours
VRChat (0.77Gb): Played for 17.42 hours
Valheim (2.84Gb): Played for 24.77 hours
Warcraft III: Reforged (5.98Gb): Played for 1.92 hours
Warhammer: Chaosbane (58.24Gb): Played for 4.92 hours
Welcome to the Game II (5.77Gb): Played for 41.00 hours
Who Wants To Be A Millionaire (6.76Gb): Played for 16.58 hours
Who Wants to Be a Millionaire? (6.76Gb): Played for 1.17 hours
Wolcen: Lords of Mayhem (109.17Gb): Played for 103.93 hours
World of Warcraft (100.00Gb): Played for 5884.45 hours
World of Warcraft: Battle for Azeroth (8.30Gb): Played for 0.50 hours
In [9]:
import matplotlib.pyplot as plt


fig, ax = plt.subplots()
ax.set(title="Played/Size for Quin69's Stream", ylabel="Airtime in hours", xlabel="Size in Gigabytes")
ax.plot(X, Y, 'o')
plt.show()
2021-02-19T18:11:07.999519 image/svg+xml Matplotlib v3.3.4, https://matplotlib.org/
In [10]:
fig, ax = plt.subplots()


ax.set(title="Played/Size for Quin69's Stream", ylabel="Airtime in hours (log)", xlabel="Size in Gigabytes")
ax.plot(X, Y, 'o')
plt.yscale("log")

plt.savefig("plot.png")
plt.show()
2021-02-19T18:11:08.537273 image/svg+xml Matplotlib v3.3.4, https://matplotlib.org/