├── README.md ├── aiosteamsearch.py ├── examples ├── top_games_example.py └── user_example.py └── steamsearch.py /README.md: -------------------------------------------------------------------------------- 1 | # SteamSearch 2 | #####A simple module to interface with Steam 3 | 4 | ## Files 5 | [steamsearch](https://github.com/billy-yoyo/steamsearch/blob/master/steamsearch.py) is the non-async library, it's dependencies are `requests` and `bs4` ([BeautifulSoup4](https://pypi.python.org/pypi/beautifulsoup4)) 6 | 7 | [aiosteamsearch](https://github.com/billy-yoyo/steamsearch/blob/master/aiosteamsearch.py) is the async library, it's dependencies are `aiohttp` and `bs4` ([BeautifulSoup4](https://pypi.python.org/pypi/beautifulsoup4)) 8 | 9 | Look at "[examples/](https://github.com/billy-yoyo/steamsearch/tree/master/examples)" for some simple examples of what you can do with the module. 10 | 11 | ## 12 | 13 | ####SteamSearch is used to create the following projects: 14 | 15 | ---- 16 | # SteamBot 17 | ### A bot for everything to do with Steam 18 | Designed with ease of use in mind, [SteamBot](https://bots.discord.pw/bots/205653475298639872) doesn't require any set up of vanity urls or making your profile public, just invite it to your server then you and your members are all ready to go. 19 | 20 | ##### Developed by [Billyoyo](https://github.com/billy-yoyo) 21 | [![discord](https://discordapp.com/api/guilds/209743049327116309/embed.png)](https://discord.gg/JSGpedK) 22 | [![python](https://img.shields.io/badge/python-3.4-blue.svg)](https://www.python.org/) 23 | -------------------------------------------------------------------------------- /aiosteamsearch.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2016-2017 billyoyo 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | 26 | import asyncio 27 | import aiohttp 28 | import operator 29 | import json 30 | import math 31 | import re 32 | from urllib import parse 33 | from bs4 import BeautifulSoup 34 | 35 | # used to map currency symbols to currency codes 36 | CURRENCY_MAP = { 37 | "lek": "ALL", 38 | "$": "USD", 39 | "ман": "AZN", 40 | "p.": "BYR", 41 | "BZ$": "BZD", 42 | "$b": "BOB", 43 | "KM": "BAM", 44 | "P": "BWP", 45 | "лв": "BGN", 46 | "R$": "BRL", 47 | "¥": "JPY", 48 | "₡": "CRC", 49 | "kn": "HRK", 50 | "₱": "CUP", 51 | "Kč": "CZK", 52 | "kr": "DKK", 53 | "RD$": "DOP", 54 | "£": "GBP", 55 | "€": "EUR", 56 | "¢": "GHS", 57 | "Q": "GTQ", 58 | "L": "HNL", 59 | "Ft": "HUF", 60 | "Rp": "IDR", 61 | "₪": "ILS", 62 | "J$": "JMD", 63 | "₩": "KRW", 64 | "₭": "LAK", 65 | "ден": "MKD", 66 | "RM": "MYR", 67 | "Rs": "MUR", 68 | "руб": "RUB" 69 | } 70 | 71 | # list of country codes 72 | COUNTRY_CODES = ['af','ax','al','dz','as','ad','ao','ai','aq','ag','ar','am','aw','au','at','az','bs','bh','bd','bb', 73 | 'by','be','bz','bj','bm','bt','bo','ba','bw','bv','br','io','bn','bg','bf','bi','kh','cm','ca','cv', 74 | 'ky','cf','td','cl','cn','cx','cc','co','km','cg','cd','ck','cr','ci','hr','cu','cy','cz','dk','dj', 75 | 'dm','do','ec','eg','sv','gq','er','ee','et','fk','fo','fj','fi','fr','gf','pf','tf','ga','gm','ge', 76 | 'de','gh','gi','gr','gl','gd','gp','gu','gt','gg','gn','gw','gy','ht','hm','va','hn','hk','hu','is', 77 | 'in','id','ir','iq','ie','im','il','it','jm','jp','je','jo','kz','ke','ki','kp','kr','kw','kg','la', 78 | 'lv','lb','ls','lr','ly','li','lt','lu','mo','mk','mg','mw','my','mv','ml','mt','mh','mq','mr','mu', 79 | 'yt','mx','fm','md','mc','mn','ms','ma','mz','mm','na','nr','np','nl','an','nc','nz','ni','ne','ng', 80 | 'nu','nf','mp','no','om','pk','pw','ps','pa','pg','py','pe','ph','pn','pl','pt','pr','qa','re','ro', 81 | 'ru','rw','an','da','rw','sh','kn','lc','pm','vc','ws','sm','st','sa','sn','cs','sc','sl','sg','sk', 82 | 'si','sb','so','za','gs','es','lk','sd','sr','sj','sz','se','ch','sy','tw','tj','tz','th','tl','tg', 83 | 'tk','to','tt','tn','tr','tm','tc','tv','ug','ua','ae','gb','us','um','uy','uz','vu','ve','vn','vg', 84 | 'vi','wf','eh','ye','zm','zw'] 85 | 86 | # list of currencies I can convert to 87 | VALID_CURRENCIES = ['AED', 'AFN', 'ALL', 'AMD', 'ANG', 'AOA', 'ARS', 'AUD', 'AWG', 'AZN', 'BAM', 'BBD', 'BDT', 'BGN', 88 | 'BHD', 'BIF', 'BMD', 'BND', 'BOB', 'BRL', 'BSD', 'BTC', 'BTN', 'BWP', 'BYN', 'BYR', 'BZD', 'CAD', 89 | 'CDF', 'CHF', 'CLF', 'CLP', 'CNY', 'COP', 'CRC', 'CUC', 'CUP', 'CVE', 'CZK', 'DJF', 'DKK', 'DOP', 90 | 'DZD', 'EEK', 'EGP', 'ERN', 'ETB', 'EUR', 'FJD', 'FKP', 'GBP', 'GEL', 'GGP', 'GHS', 'GIP', 'GMD', 91 | 'GNF', 'GTQ', 'GYD', 'HKD', 'HNL', 'HRK', 'HTG', 'HUF', 'IDR', 'ILS', 'IMP', 'INR', 'IQD', 'IRR', 92 | 'ISK', 'JEP', 'JMD', 'JOD', 'JPY', 'KES', 'KGS', 'KHR', 'KMF', 'KPW', 'KRW', 'KWD', 'KYD', 'KZT', 93 | 'LAK', 'LBP', 'LKR', 'LRD', 'LSL', 'LTL', 'LVL', 'LYD', 'MAD', 'MDL', 'MGA', 'MKD', 'MMK', 'MNT', 94 | 'MOP', 'MRO', 'MTL', 'MUR', 'MVR', 'MWK', 'MXN', 'MYR', 'MZN', 'NAD', 'NGN', 'NIO', 'NOK', 'NPR', 95 | 'NZD', 'OMR', 'PAB', 'PEN', 'PGK', 'PHP', 'PKR', 'PLN', 'PYG', 'QAR', 'RON', 'RSD', 'RUB', 'RWF', 96 | 'SAR', 'SBD', 'SCR', 'SDG', 'SEK', 'SGD', 'SHP', 'SLL', 'SOS', 'SRD', 'STD', 'SVC', 'SYP', 'SZL', 97 | 'THB', 'TJS', 'TMT', 'TND', 'TOP', 'TRY', 'TTD', 'TWD', 'TZS', 'UAH', 'UGX', 'USD', 'UYU', 'UZS', 98 | 'VEF', 'VND', 'VUV', 'WST', 'XAF', 'XAG', 'XAU', 'XCD', 'XDR', 'XOF', 'XPD', 'XPF', 'XPT', 'YER', 99 | 'ZAR', 'ZMK', 'ZMW', 'BCN', 'BTS', 'DASH', 'DOGE', 'EAC', 'EMC', 'ETH', 'FCT', 'FTC', 'LD', 'LTC', 100 | 'NMC', 'NVC', 'NXT', 'PPC', 'STR', 'VTC', 'XCP', 'XEM', 'XMR', 'XPM', 'XRP', 'VEF_BLKMKT', 101 | 'VEF_SIMADI'] 102 | 103 | STEAM_KEY = "" # contains your Steam API key (set using set_key) 104 | STEAM_CACHE = True # whether or not steamsearch should cache some results which generally aren't going to change 105 | STEAM_SESSION = "" # your Steam Session for SteamCommunityAjax 106 | STEAM_PRINTING = False # whether or not steamsearch will occasionally print warnings 107 | 108 | 109 | def set_key(key, session, cache=True, printing=False): 110 | """Used to initiate your key + session strings, also to enable/disable caching 111 | 112 | Args: 113 | key (str): Your Steam API key 114 | session (str): Your SteamCommunityAjax session, this basically just needs to be any string containing only a-z, A-Z or 0-9 115 | cache (bool, optional): True to enable caching 116 | """ 117 | global STEAM_KEY, STEAM_CACHE, STEAM_SESSION, STEAM_PRINTING 118 | STEAM_KEY = key 119 | STEAM_SESSION = session 120 | STEAM_CACHE = cache 121 | STEAM_PRINTING = printing 122 | 123 | 124 | def count_cache(): 125 | """Counts the amount of cached results 126 | 127 | Returns: 128 | the number of cached results (int) 129 | """ 130 | return len(gameid_cache) + len(item_name_cache) + len(userid_cache) 131 | 132 | 133 | def clear_cache(): 134 | """Clears all of the cached results 135 | 136 | Returns: 137 | the number of results cleared 138 | """ 139 | global gameid_cache, item_name_cache, userid_cache 140 | items = count_cache() 141 | gameid_cache = {} 142 | item_name_cache = {} 143 | userid_cache = {} 144 | return items 145 | 146 | 147 | class SteamKeyNotSet(Exception): 148 | """Exception raised if STEAM_KEY is used before it was set""" 149 | pass 150 | 151 | 152 | class SteamSessionNotSet(Exception): 153 | """Exception raised if STEAM_SESSION is used before it was set""" 154 | pass 155 | 156 | 157 | def _check_key_set(): 158 | """Internal method to ensure STEAM_KEY has been set before attempting to use it""" 159 | if not isinstance(STEAM_KEY, str) or STEAM_KEY == "": 160 | raise SteamKeyNotSet 161 | 162 | 163 | def _check_session_set(): 164 | """Internal method to ensure STEAM_SESSION has been set before attempting to use it""" 165 | if not isinstance(STEAM_KEY, str) or STEAM_SESSION == "": 166 | raise SteamSessionNotSet 167 | 168 | async def exchange(amount, from_curr, to_curr, timeout=10): 169 | """Converts an amount of money from one currency to another 170 | 171 | Args: 172 | amount (float): The amount of money you want to convert 173 | from_curr (str): The currency you want to convert from, 174 | either country symbol (e.g USD) or currency smybol (e.g. £) 175 | to_curr (str): The currency you want to convert to, same format as from_curr 176 | timeout (int, optional): The time in seconds aiohttp will take to timeout the request 177 | Returns: 178 | float: the converted amount of money to 2 d.p., or the original amount of the conversion failed. 179 | """ 180 | try: 181 | async with aiohttp.ClientSession(timeout=timeout) as session: 182 | resp = await session.get("https://api.fixer.io/latest?symbols=" + from_curr + "," + to_curr, timeout=timeout) 183 | data = await resp.json() 184 | if "rates" in data: 185 | return int((amount / data["rates"][from_curr]) * data["rates"][to_curr] * 100)/100 186 | except: 187 | return amount 188 | 189 | 190 | def is_integer(x): 191 | try: 192 | int(x) 193 | return True 194 | except: 195 | return False 196 | 197 | 198 | # link, id, image, title, released, review, reviewLong, discount, price, discountPrice, 199 | class GamePageResult: 200 | def __init__(self, link, id, soup): 201 | self.title = "???" 202 | titlesoup = soup.find("div", {"class": "apphub_AppName"}) 203 | if titlesoup is not None: 204 | self.title = titlesoup.get_text() 205 | 206 | self.link = link 207 | self.id = id 208 | 209 | imgsoup = soup.find("img", {"class": "game_header_image_full"}) 210 | self.image = "???" 211 | if imgsoup is not None: 212 | self.image = imgsoup.get("src") 213 | 214 | self.released = "???" 215 | releasesoup = soup.find("div", {"class": "release_date"}) 216 | if releasesoup is not None: 217 | releasesoup = soup.find("span", {"class": "date"}) 218 | if releasesoup is not None: 219 | self.released = releasesoup.get_text() 220 | 221 | self.review = "???" 222 | reviewsoup = soup.find("span", {"class": "game_review_summary"}) 223 | if reviewsoup is not None: 224 | self.review = reviewsoup.get_text().replace("\n", "").replace("\r", "").replace("\t", "").replace("(", "").replace(")", "").replace("-", "").strip() 225 | 226 | self.reviewLong = "???" 227 | reviewsoup = soup.find_all("span", {"class": "responsive_reviewdesc"}) 228 | if reviewsoup is not None and len(reviewsoup) >= 2: 229 | self.reviewLong = reviewsoup[1].get_text().replace("\n", "").replace("\r", "").replace("\t", "").replace("(", "").replace(")", "").replace("-", "").strip() 230 | 231 | self.discount = "" 232 | discountsoup = soup.find("div", {"class": "discount_pct"}) 233 | if discountsoup is not None: 234 | self.discount = discountsoup.get_text().replace(" ", "").replace("\n", "").replace("\r", "").replace("\t", "").replace("(", "").replace(")", "") 235 | 236 | if self.discount == "": 237 | self.price = "???" 238 | self.discountPrice = "???" 239 | pricesoup = soup.find("div", {"class": "game_purchase_price"}) 240 | if pricesoup is not None: 241 | self.price = pricesoup.get_text().replace(" ", "").replace("\n", "").replace("\r", "").replace("\t", "").replace("(", "").replace(")", "").replace("-", "") 242 | else: 243 | self.price = "???" 244 | self.discountPrice = "???" 245 | pricesoup = soup.find("div", {"class": "discount_original_price"}) 246 | if pricesoup is not None: 247 | self.price = pricesoup.get_text().replace(" ", "").replace("\n", "").replace("\r", "").replace("\t", "").replace("(", "").replace(")", "").replace("-", "") 248 | 249 | pricesoup = soup.find("div", {"class": "discount_final_price"}) 250 | if pricesoup is not None: 251 | self.discountPrice = pricesoup.get_text().replace(" ", "").replace("\n", "").replace("\r", "").replace("\t", "").replace("(", "").replace(")", "").replace("-", "") 252 | 253 | async def update_price(self, currency, currency_symbol): 254 | """Attempts to convert the price to GBP 255 | 256 | Args: 257 | currency (str): The currency code (e.g USD or GBP) to convert the price to 258 | currency_symbol (str): The currency symbol to add to the start of the price 259 | """ 260 | if currency != "GBP": 261 | try: 262 | if self.price != "???" and self.price != "" and self.price != "Free to Play": 263 | rawprice = await exchange(float(self.price[1:]), "GBP", currency) 264 | self.price = currency_symbol + str(rawprice) 265 | except: 266 | if STEAM_PRINTING: 267 | print("failed to convert currency (GBP)") 268 | 269 | def __str__(self): 270 | return self.title 271 | 272 | class GameResult: 273 | """Class containing information about a game search result""" 274 | def __init__(self, soup): 275 | """ 276 | 277 | Args: 278 | soup (BeautifulSoup): soup from game search page 279 | """ 280 | self.link = soup.get("href") 281 | linkspl = self.link.split("/") 282 | self.link = "/".join(linkspl[:5]) 283 | self.id = linkspl[4] 284 | 285 | 286 | self.image = None #"https://cdn.edgecast.steamstatic.com/steam/apps/%s/capsule_184x69.jpg" % self.id 287 | imgsoup = soup.find("img") 288 | if imgsoup is not None: 289 | self.image = imgsoup.get("src") 290 | 291 | self.title = "???" 292 | titlesoup = soup.find("span", {"class": "title"}) 293 | if titlesoup is not None: 294 | self.title = titlesoup.get_text() 295 | 296 | self.released = "???" 297 | releasesoup = soup.find("div", {"class": "col search_released responsive_secondrow"}) 298 | if releasesoup is not None: 299 | self.released = releasesoup.get_text() 300 | 301 | self.review = "???" 302 | self.reviewLong = "???" 303 | reviewsoup = soup.findAll("span") 304 | for span in reviewsoup: 305 | cls = span.get("class") 306 | if cls is not None and "search_review_summary" in cls: 307 | reviewRaw = span.get("data-tooltip-html").split("
") 308 | self.review = reviewRaw[0] 309 | self.reviewLong = reviewRaw[1] 310 | break 311 | 312 | self.discount = "" 313 | discountsoup = soup.find("div", {"class": "col search_discount responsive_secondrow"}) 314 | if discountsoup is not None: 315 | span = discountsoup.find("span") 316 | if span is not None: 317 | self.discount = span.get_text().replace(" ", "").replace("\n", "").replace("\r", "").replace("\t", "") 318 | 319 | self.price = "???" 320 | self.discountPrice = "???" 321 | 322 | if self.discount == "": 323 | pricesoup = soup.find("div", {"class": "col search_price responsive_secondrow"}) 324 | self.price = pricesoup.get_text().replace(" ", "").replace("\n", "").replace("\t", "").replace("\r", "") 325 | else: 326 | pricesoup = soup.find("div", {"class": "col search_price discounted responsive_secondrow"}) 327 | span = pricesoup.find("span") 328 | if span is not None: 329 | self.price = span.get_text().replace(" ", "").replace("\n", "").replace("\t", "").replace("\r", "").replace("", "").replace("", "") 330 | self.discountPrice = pricesoup.get_text().replace(" ", "").replace("\n", "").replace("\t", "").replace("\r", "").replace(self.price, "") 331 | 332 | if self.price.lower() == "freetoplay": 333 | self.price = "Free to Play" 334 | 335 | #def set_image_size(self, width, height): 336 | # if self.image is not None: 337 | # self.image = re.sub("width=[0-9]+", "width=%s" % width, self.image) 338 | # self.image = re.sub("height=[0-9]+", "height=%s" % height, self.image) 339 | 340 | def get_price_text(self): 341 | if self.discount == "": 342 | return self.price 343 | else: 344 | return self.discountPrice + " (" + self.discount + ")" 345 | 346 | async def update_price(self, currency, currency_symbol): 347 | """Attempts to convert the price to GBP 348 | 349 | Args: 350 | currency (str): The currency code (e.g USD or GBP) to convert the price to 351 | currency_symbol (str): The currency symbol to add to the start of the price 352 | """ 353 | if currency != "GBP": 354 | try: 355 | if self.price != "???" and self.price != "" and self.price != "Free to Play": 356 | rawprice = await exchange(float(self.price[1:]), "GBP", currency) 357 | self.price = currency_symbol + str(rawprice) 358 | except: 359 | if STEAM_PRINTING: 360 | print("failed to convert currency (GBP)") 361 | 362 | def __str__(self): 363 | return self.title 364 | 365 | 366 | 367 | class CategoryResult: 368 | def __init__(self, soup): 369 | 370 | self.link = "/".join(soup.get("href").split("/")[:-1]) or "???" 371 | self.id = soup.get("data-ds-appid") or "???" 372 | 373 | name_soup = soup.find("span", {"class": "title"}) 374 | self.title = name_soup.get_text() if name_soup is not None else "???" 375 | 376 | img_soup = soup.find("img") 377 | self.img = img_soup.get("src") if img_soup is not None else "???" 378 | self.img = self.img or "???" 379 | 380 | discount_soup = soup.find("div", {"class": "search_discount"}) 381 | self.discount = discount_soup.get_text().strip() if discount_soup is not None else "???" 382 | 383 | price_soup = soup.find("div", {"class": "search_price"}) 384 | if price_soup is not None: 385 | price_text_raw = "".join([x for x in price_soup.get_text().split() if x != ""]) 386 | 387 | discount_price_soup = price_soup.find("span") 388 | if discount_price_soup is not None: 389 | self.price = discount_price_soup.get_text().strip() 390 | self.discount_price = price_text_raw.replace(self.price, "") 391 | else: 392 | self.price = price_text_raw 393 | self.discount_price = "???" 394 | else: 395 | self.price = "???" 396 | 397 | if self.price.replace(" ", "").lower() == "freetoplay": 398 | self.price = "free to play" 399 | elif self.price == "": 400 | self.price = "???" 401 | 402 | def get_price_text(self): 403 | if self.discount == "???": 404 | return self.price 405 | elif self.discount == "": 406 | return self.price 407 | else: 408 | return self.discount_price + " (" + self.discount + ")" 409 | 410 | 411 | class NewCategoryResult: 412 | def __init__(self, soup): 413 | self.link = "/".join(soup.get("href").split("/")[:-1]) or "???" 414 | self.id = soup.get("data-ds-appid") or "???" 415 | 416 | name_soup = soup.find("div", {"class": "tab_item_name"}) 417 | self.title = "???" 418 | if name_soup is not None: 419 | self.title = name_soup.get_text() 420 | 421 | img_soup = soup.find("img") 422 | self.img = "???" 423 | if img_soup is not None: 424 | self.img = img_soup.get("src") or "???" 425 | 426 | self.discount = "???" 427 | self.price = "???" 428 | self.discount_price = "???" 429 | 430 | pricesoup = soup.find("div", {"class": "discount_block"}) 431 | if pricesoup is not None: 432 | discount = pricesoup.find("div", {"class": "discount_pct"}) 433 | if discount is not None: 434 | self.discount = discount.get_text() 435 | dpsoup = pricesoup.find("div", {"class": "discount_prices"}) 436 | if dpsoup is not None: 437 | if self.discount == "???": 438 | price = dpsoup.find("div", {"class": "discount_final_price"}) 439 | self.price = price.get_text() 440 | else: 441 | price = dpsoup.find("div", {"class": "discount_original_price"}) 442 | self.price = price.get_text() 443 | discountprice = dpsoup.find("div", {"class": "discount_final_price"}) 444 | self.discount_price = discountprice.get_text() 445 | 446 | if self.price.lower() == "freetoplay": 447 | self.price = "Free to Play" 448 | 449 | def get_price_text(self): 450 | if self.discount == "???": 451 | return self.price 452 | elif self.discount == "": 453 | return self.price 454 | else: 455 | return self.discount_price + " (" + self.discount + ")" 456 | 457 | 458 | class TopResult: 459 | """Class containing information about the games on the front of the store (new releases, specials etc.)""" 460 | def __init__(self, soup): 461 | """ 462 | 463 | Args: 464 | soup (BeautifulSoup): Soup for the section of the store page containing the game information 465 | """ 466 | self.link = "???" 467 | 468 | linksoup = soup.find("a", {"class": "tab_item_overlay"}) 469 | if linksoup is not None: 470 | self.link = linksoup.get("href") 471 | if self.link is None: 472 | self.link = "???" 473 | 474 | self.image = "???" 475 | imagesoup = soup.find("div", {"class": "tab_item_cap"}) 476 | if imagesoup is not None: 477 | img = imagesoup.get("img") 478 | if img is not None: 479 | self.image = img.get("src") 480 | 481 | self.discount = "" 482 | self.price = "" 483 | self.discountPrice = "???" 484 | 485 | pricesoup = soup.find("div", {"class": "discount_block"}) 486 | if pricesoup is not None: 487 | discount = pricesoup.find("div", {"class": "discount_pct"}) 488 | if discount is not None: 489 | self.discount = discount.get_text() 490 | dpsoup = pricesoup.find("div", {"class": "discount_prices"}) 491 | if dpsoup is not None: 492 | if self.discount == "": 493 | price = dpsoup.find("div", {"class": "discount_final_price"}) 494 | self.price = price.get_text() 495 | else: 496 | price = dpsoup.find("div", {"class": "discount_original_price"}) 497 | self.price = price.get_text() 498 | discountprice = dpsoup.find("div", {"class": "discount_final_price"}) 499 | self.discountPrice = discountprice.get_text() 500 | 501 | if self.price.lower() == "freetoplay": 502 | self.price = "Free to Play" 503 | 504 | titlesoup = soup.find("div", {"class": "tab_item_content"}) 505 | if titlesoup is not None: 506 | title = soup.find("div", {"class": "tab_item_name"}) 507 | self.title = title.get_text() 508 | 509 | self.review = "???" 510 | self.reviewLong = "???" 511 | self.released = "???" 512 | 513 | def get_price_text(self): 514 | if self.discount == "": 515 | return self.price 516 | else: 517 | return self.discountPrice + " (" + self.discount + ")" 518 | 519 | async def update_price(self, currency, currency_symbol): 520 | """Attempts to convert the price to GBP 521 | 522 | Args: 523 | currency (str): The currency code (e.g USD or GBP) to convert the price to 524 | currency_symbol (str): The currency symbol to add to the start of the price 525 | """ 526 | if currency != "GBP": 527 | try: 528 | if self.price != "???" and self.price != "" and self.price != "Free to Play": 529 | rawprice = await exchange(float(self.price[1:]), "GBP", currency) 530 | self.price = currency_symbol + str(rawprice) 531 | 532 | if self.discountPrice != "???" and self.price != "": 533 | rawdiscountprice = await exchange(float(self.discountPrice[1:]), "GBP", currency) 534 | self.discountPrice = currency_symbol + str(rawdiscountprice) 535 | except: 536 | if STEAM_PRINTING: 537 | print("failed to convert currency (GBP)") 538 | 539 | def __str__(self): 540 | return self.title 541 | 542 | 543 | class SteamSaleResult(TopResult): 544 | def __init__(self, soup): 545 | self.link = "/".join(soup.get("href").split("/")[:-1]) 546 | self.id = soup.get("data-ds-appid") 547 | 548 | self.image = "???" 549 | imagesoup = soup.find("img", {"class": "sale_capsule_image"}) 550 | if imagesoup is not None: 551 | self.image = imagesoup.get("src") 552 | 553 | self.discount = "" 554 | self.price = "" 555 | self.discountPrice = "???" 556 | 557 | pricesoup = soup.find("div", {"class": "discount_block"}) 558 | if pricesoup is not None: 559 | discount = pricesoup.find("div", {"class": "discount_pct"}) 560 | if discount is not None: 561 | self.discount = discount.get_text() 562 | dpsoup = pricesoup.find("div", {"class": "discount_prices"}) 563 | if dpsoup is not None: 564 | if self.discount == "": 565 | price = dpsoup.find("div", {"class": "discount_final_price"}) 566 | self.price = price.get_text() 567 | else: 568 | price = dpsoup.find("div", {"class": "discount_original_price"}) 569 | self.price = price.get_text() 570 | discountprice = dpsoup.find("div", {"class": "discount_final_price"}) 571 | self.discountPrice = discountprice.get_text() 572 | 573 | if self.price.lower() == "freetoplay": 574 | self.price = "Free to Play" 575 | 576 | self.title = "???" 577 | 578 | self.review = "???" 579 | self.reviewLong = "???" 580 | self.released = "???" 581 | 582 | async def get_title(self, cc="gb", timeout=10): 583 | async with aiohttp.ClientSession() as session: 584 | resp = await session.get("https://store.steampowered.com/api/appdetails/?appids=" + self.id, timeout=timeout) 585 | data = await resp.json() 586 | 587 | self.title = parse.unquote(data[self.id]["data"]["name"]) 588 | 589 | class UserResult: 590 | """Class containing information about a specific user""" 591 | def __init__(self, data): 592 | """ 593 | 594 | Args: 595 | data (dict): part of the JSON returned by the Steam API 596 | """ 597 | self.id = data.get("steamid", "???") 598 | self.name = data.get("personaname", "???") 599 | self.visibilityState = str(data.get("communityvisibilitystate", "???")) 600 | self.profileStage = str(data.get("profilestate", "???")) 601 | self.lastLogoff = str(data.get("lastlogoff", "???")) 602 | self.url = data.get("profileurl", "???") 603 | self.avatar = data.get("avatar", "???") 604 | self.avatarMedium = data.get("avatarmedium", "???") 605 | self.avatarFull = data.get("avatarfull", "???") 606 | self.personaState = data.get("personastate", "???") 607 | self.realName = data.get("realname", "???") 608 | self.clan = data.get("primaryclanid", "???") 609 | self.created = str(data.get("timecreated", "???")) 610 | self.country = data.get("loccountrycode", "???") 611 | 612 | 613 | class UserGame: 614 | """Class containing information about user's playtime on a specific game""" 615 | def __init__(self, data): 616 | """ 617 | 618 | Args: 619 | data (dict): part of the JSON returned by the Steam API 620 | """ 621 | self.id = str(data.get("appid", "???")) 622 | self.name = data.get("name", "???") 623 | self.playtime_2weeks = str(data.get("playtime_2weeks", "???")) # IN MINUTES 624 | self.playtime_forever = str(data.get("playtime_forever", "???")) # IN MINUTES 625 | self.playtime_forever_int = 0 626 | if self.playtime_forever != "???": 627 | self.playtime_forever_int = int(self.playtime_forever) 628 | self.icon = data.get("img_icon_url", "???") 629 | self.logo = data.get("img_logo_url", "???") 630 | 631 | 632 | def format_playtime(self, playtime): 633 | """Formats the playtime in to hours 634 | 635 | Args: 636 | playtime (str | int): must be something representing an integer, playtime in minutes 637 | Returns: 638 | str: the formatted playtime in to Hours to 2 d.p. 639 | """ 640 | if playtime != "???": 641 | return str(int(int(playtime) / 6)/10) 642 | else: 643 | return playtime 644 | 645 | def get_playtime_string(self, start="%s hours on record", end=" (%s hours in the last 2 weeks)"): 646 | """Converts the object to single line format 647 | 648 | Returns: 649 | A string representing this object 650 | """ 651 | if self.playtime_2weeks != "???": 652 | start += end 653 | return start % (self.format_playtime(self.playtime_forever), self.format_playtime(self.playtime_2weeks)) 654 | else: 655 | return start % self.format_playtime(self.playtime_forever) 656 | 657 | 658 | class UserLibrary: 659 | """Class containing information about a set of games in the users library""" 660 | def __init__(self, data): 661 | self.count = data.get("game_count", "???") 662 | self.games = {} 663 | for game in data.get("games", []): 664 | ugame = UserGame(game) 665 | self.games[ugame.id] = ugame 666 | 667 | def get_game_list(self, limit=10, start="%s hours on record", end=" (%s hours in the last 2 weeks)"): 668 | """Converts the game list to a list of singe line formatted strings 669 | 670 | Args: 671 | limit (int): how many of the games to get, in decreasing order of total playtime 672 | Returns: 673 | a list of strings representing the user's most played games 674 | """ 675 | results = sorted(self.games.values(), key=operator.attrgetter('playtime_forever_int'))[-1:-(limit+1):-1] 676 | pairs = [("", "")] * len(results) 677 | longest_name = 0 678 | for i, result in enumerate(results): 679 | pairs[i] = (result.name, result.get_playtime_string(start=start, end=end)) 680 | if len(result.name) > longest_name: 681 | longest_name = len(result.name) 682 | final = [""] * len(results) 683 | longest_name += 3 684 | max_i_len = len(str(len(pairs))) 685 | for i, pair in enumerate(pairs): 686 | final[i] = " " * (max_i_len - len(str(i+1))) + str(i+1) + ". " + pair[0] + " " * (longest_name - len(pair[0])) + pair[1] 687 | return final 688 | 689 | 690 | class UserAchievement: 691 | """Class containing information about a user's specific achievement for a specific game""" 692 | def __init__(self, data): 693 | """ 694 | 695 | Args: 696 | data is part of the JSON returned by the Steam API 697 | """ 698 | self.apiname = data.get("apiname", "???") 699 | self.displayname = self.apiname 700 | for letter in "ABCDEFGHIJKLMNOPQRSTUVWXYZ": 701 | self.displayname = self.displayname.replace(letter, " " + letter) 702 | if self.displayname[0] == " ": 703 | self.displayname = self.displayname[1:] 704 | self.achieved = bool(data.get("achieved", False)) 705 | self.name = data.get("name", "???") 706 | self.description = data.get("description", "???") 707 | 708 | 709 | #self.id = "???" 710 | 711 | def line_format(self): 712 | """Format the achievement in to a single line""" 713 | return ("✅" if self.achieved else "❎") + " " + (self.name if self.name != "???" else self.apiname) 714 | 715 | 716 | class UserAchievements: 717 | """Class containing information about a user's achievements for a specific game""" 718 | def __init__(self, gameid, gamename, data): 719 | """ 720 | 721 | Args: 722 | gameid (str): the appid of the game these achievements are for 723 | gamename (str): the gamename of the game these achievements are for 724 | data (dict): part of the JSON returned by the Steam API 725 | """ 726 | self.gameid = gameid 727 | self.game = gamename 728 | self.achievements = sorted([UserAchievement(x) for x in data], key=operator.attrgetter("apiname")) 729 | 730 | def get(self, name): 731 | """Get an achievement matching 'name' 732 | 733 | Args: 734 | name (str): the name of the achievement you want to find, NOT FUZZY 735 | Returns: 736 | UserAchievement: the user achievement found, None if no achievement with that name found 737 | """ 738 | name = name.lower().replace(" ", "").replace("-", "") 739 | for achiev in self.achievements: 740 | if achiev.apiname.lower() == name: 741 | return achiev 742 | return None 743 | 744 | def lines_format(self): 745 | """Return a list of all the achievements in line order 746 | 747 | Returns: 748 | list[str]: a list of the line formats""" 749 | return [x.line_format() for x in self.achievements] 750 | 751 | 752 | class GlobalAchievement: 753 | """Class containing information about a specific achievement for a specific game""" 754 | def __init__(self, soup): 755 | """ 756 | 757 | Args: 758 | soup (BeautifulSoup): part of the soup found on the achievements page 759 | """ 760 | textSoup = soup.find("div", {"class": "achieveTxt"}) 761 | if textSoup is not None: 762 | name = textSoup.find("h3") 763 | desc = textSoup.find("h5") 764 | 765 | if name is not None: 766 | self.name = name.get_text() 767 | else: 768 | self.name = "???" 769 | 770 | if desc is not None: 771 | self.desc = desc.get_text() 772 | else: 773 | self.desc = "???" 774 | else: 775 | self.name = "???" 776 | self.desc = "???" 777 | 778 | self.apiname = self.name.replace(" ", "").replace("-", "") 779 | percentSoup = soup.find("div", {"class": "achievePercent"}) 780 | if percentSoup is not None: 781 | self.percent = percentSoup.get_text() 782 | else: 783 | self.percent = "??%" 784 | 785 | imgSoup = soup.find("div", {"class": "achieveImgHolder"}) 786 | if imgSoup is not None: 787 | imgSoup = imgSoup.find("img") 788 | if imgSoup is not None: 789 | self.img = imgSoup.get("src") 790 | else: 791 | self.img = "???" 792 | else: 793 | self.img = "???" 794 | 795 | 796 | class GlobalAchievements: 797 | """Contains information about all the achievements for a specific game""" 798 | def __init__(self, soup): 799 | """ 800 | 801 | Args: 802 | soup (BeautifulSoup): part of the soup found on the achievements page 803 | """ 804 | rows = soup.find_all("div", {"class": "achieveRow"}) 805 | self.achievements = sorted([GlobalAchievement(x) for x in rows], key=operator.attrgetter("apiname")) 806 | 807 | def get(self, name): 808 | """Get an achievement matching 'name' 809 | 810 | Args: 811 | name (str): the name of the achievement you want to find, NOT FUZZY 812 | Returns: 813 | GlobalAchievement: the user achievement found, None if no achievement with that name found 814 | """ 815 | name = name.lower() 816 | for achiev in self.achievements: 817 | if achiev.apiname.lower() == name: 818 | return achiev 819 | return None 820 | 821 | 822 | class UserWishlistGame: 823 | def __init__(self, game): 824 | self.name = game[0] 825 | self.link = game[1] 826 | self.price = game[2] 827 | 828 | self.discount_price = None 829 | self.discount_percent = None 830 | 831 | if len(game) > 3: 832 | self.discount_price = game[3] 833 | self.discount_percent = game[4] 834 | 835 | 836 | class UserWishlist: 837 | def __init__(self, games): 838 | self.games = [UserWishlistGame(game) for game in games] 839 | 840 | 841 | class SteamGame: 842 | 843 | def __init__(self, **data): 844 | self.id = data.pop("id", "???") 845 | self.title = data.pop("name", "???") 846 | self.type = data.pop("type", "???") 847 | self.headline = data.pop("headline", "???") 848 | 849 | self.small_image = data.pop("small_capsule_image", "???") 850 | self.large_image = data.pop("large_capsule_image", "???") 851 | self.header_image = data.pop("header_image", "???") 852 | 853 | self.linux = data.pop("linux_available", False) 854 | self.mac = data.pop("mac_available", False) 855 | self.windows = data.pop("windows_available", False) 856 | self.controller = data.pop("controller_support", False) 857 | self.streaming_video = data.pop("streamingvideo_available", False) 858 | 859 | self.discounted = data.pop("discounted", False) 860 | self.original_price = data.pop("original_price", 0) 861 | self.price = data.pop("final_price", 0) 862 | self.discount_expiration = data.pop("discount_expiration", "???") 863 | self.discount_percent = data.pop("discount_percent", 0) 864 | self.currency = data.pop("currency", "???") 865 | 866 | 867 | def get_price_text(self): 868 | if self.discounted: 869 | return str(self.price/100) 870 | else: 871 | return str(self.price/100) + " (-" + str(self.discount_percent) + "%)" 872 | 873 | 874 | class ItemResult: 875 | """Class containing information about an item on the steam market""" 876 | def __init__(self, soup): 877 | """ 878 | 879 | Args: 880 | soup (BeautifulSoup): the soup of the item's store page 881 | """ 882 | price = soup.find("span", {"class": "market_listing_price_with_publisher_fee_only"}) 883 | self.price = "???" 884 | self.game = "???" 885 | if price is not None: 886 | rawprice = price.get_text().replace("\n", "").replace("\t", "").replace("\r", "") 887 | before = "" 888 | after = "" 889 | while len(rawprice) > 0 and rawprice[0] not in "0123456789.,": 890 | before += rawprice[0] 891 | rawprice = rawprice[1:] 892 | while len(rawprice) > 0 and rawprice[-1] not in "0123456789.,": 893 | after = rawprice[-1] + after 894 | rawprice = rawprice[:-1] 895 | before = before.replace(" ", "") 896 | after = after.replace(" ", "") 897 | 898 | currency = after 899 | if before in CURRENCY_MAP: 900 | currency = CURRENCY_MAP[before] 901 | elif after in CURRENCY_MAP: 902 | currency = CURRENCY_MAP[after] 903 | elif STEAM_PRINTING: 904 | print("no currency matching `" + before + "` or `" + after + "`") 905 | 906 | self.price = rawprice 907 | self.currency = currency 908 | elif STEAM_PRINTING: 909 | print("failed to find price") 910 | 911 | text = str(soup) 912 | self.icon = "???" 913 | iconindex = text.find('"icon_url":') 914 | if iconindex > 0: 915 | iconurl = text[iconindex+len('"icon_url":'):text.find(',', iconindex)].replace(" ", "").replace('"', "") 916 | self.icon = "https://steamcommunity-a.akamaihd.net/economy/image/" + iconurl 917 | elif STEAM_PRINTING: 918 | print("failed to find icon") 919 | 920 | index = text.find("var g_rgAssets") 921 | nindex = text.find("\n", index) 922 | jsontext = text[index:nindex] 923 | while jsontext[0] != "{" and jsontext[0] != "[": 924 | jsontext = jsontext[1:] 925 | while jsontext[-1] != "}" and jsontext[-1] != "]": 926 | jsontext = jsontext[:-1] 927 | 928 | 929 | try: 930 | data = json.loads(jsontext) 931 | raw = {} 932 | for k1 in data: 933 | for k2 in data[k1]: 934 | for k3 in data[k1][k2]: 935 | if "tradable" in data[k1][k2][k3] and data[k1][k2][k3]["tradable"] == 1: 936 | raw = data[k1][k2][k3] 937 | break 938 | 939 | self.actions = raw.get("actions", []) 940 | self.name = raw.get("name", "???") 941 | self.gameIcon = raw.get("app_icon", "???") 942 | self.icon = "https://steamcommunity-a.akamaihd.net/economy/image/" + raw.get("icon_url", "???") 943 | self.type = raw.get("type", "???") 944 | self.desc = [BeautifulSoup(x.get("value", ""), "html.parser").get_text() for x in raw.get("descriptions", [])] 945 | except: 946 | self.actions = [] 947 | self.name = "???" 948 | self.gameIcon = "???" 949 | self.icon_url = "???" 950 | self.type = "???" 951 | self.desc = "" 952 | if STEAM_PRINTING: 953 | print("failed to load market data") 954 | 955 | async def update_price(self, currency, currency_symbol): 956 | """Attempts to convert the price to GBP 957 | 958 | Args: 959 | currency (str): The currency code (e.g USD or GBP) to convert the price to 960 | currency_symbol (str): The currency symbol to add to the start of the price 961 | """ 962 | try: 963 | rawprice = await exchange(float(self.price.replace(",", ".")), self.currency, currency) 964 | self.price = currency_symbol + str(rawprice) 965 | except: 966 | if STEAM_PRINTING: 967 | print("failed to convert currency (" + self.currency + ")") 968 | 969 | 970 | async def check_game_sales(checks, old, optional_test=None, timeout=120): 971 | """ 972 | 973 | :param checks: a list of tuples (gameid, percent, cc, other...) 974 | :param old: a dict of games found last time {gameid: percent} 975 | :return: a list of tuples (gameid, check_percent, old_percent, price_overview, name, other...) 976 | """ 977 | async with aiohttp.ClientSession() as session: 978 | cached = optional_test or {} 979 | print("useing optional test: %s" % cached) 980 | results, new_old = [], {} 981 | 982 | print("using checks: %s" % str(checks)) 983 | 984 | for check in checks: 985 | try: 986 | if check[0] not in cached: 987 | resp = await session.get("https://store.steampowered.com/api/appdetails/?appids=" + check[0] + "&cc=" + check[2], timeout=timeout) 988 | json = await resp.json() 989 | 990 | if not isinstance(json, dict): 991 | print("failed to find percent for %s" % check[0]) 992 | continue 993 | 994 | if json[check[0]]["success"]: 995 | if "price_overview" not in json[check[0]]["data"]: 996 | cached[check[0]] = None 997 | continue 998 | price_overview = json[check[0]]["data"]["price_overview"] 999 | cached[check[0]] = (price_overview, json[check[0]]["data"]["name"]) 1000 | else: 1001 | cached[check[0]] = None 1002 | 1003 | resp.close() 1004 | 1005 | if cached[check[0]] is not None: 1006 | result = cached[check[0]] 1007 | print(result) 1008 | old_percent = float(old.get(check[0], 0)) 1009 | new_percent = float(result[0]["discount_percent"]) 1010 | required_percent = float(check[1]) 1011 | if new_percent >= required_percent and new_percent != old_percent: 1012 | results.append([check[0], float(check[1]), old_percent, result[0], result[1]] + list(check[3:])) 1013 | except: 1014 | print("[WARNING] failed to process check %s" % check) 1015 | pass 1016 | for gameid in cached: 1017 | if cached[gameid] is not None: 1018 | new_old[gameid] = float(cached[gameid][0]["discount_percent"]) 1019 | else: 1020 | new_old[gameid] = 0 1021 | return results, new_old 1022 | 1023 | 1024 | async def is_valid_game_id(appid, timeout=10): 1025 | if not isinstance(appid, str): 1026 | return False 1027 | async with aiohttp.ClientSession() as session: 1028 | resp = await session.get( "https://store.steampowered.com/api/appdetails/?appids=" + appid, timeout=timeout) 1029 | json = await resp.json() 1030 | 1031 | return json[appid]["success"] 1032 | 1033 | 1034 | async def get_game_name_by_id(appid, timeout=10): 1035 | async with aiohttp.ClientSession() as session: 1036 | resp = await session.get("https://store.steampowered.com/api/appdetails/?appids=" + appid, timeout=timeout) 1037 | data = await resp.json() 1038 | 1039 | return parse.unquote(data[appid]["data"]["name"]) 1040 | 1041 | async def get_game_by_id(appid, timeout=10, cc="gb"): 1042 | async with aiohttp.ClientSession() as session: 1043 | resp = await session.get("https://store.steampowered.com/app/" + appid + "/?cc=" + cc, timeout=timeout) 1044 | text = await resp.read() 1045 | soup = BeautifulSoup(text, "html.parser") 1046 | 1047 | return GamePageResult("https://store.steampowered.com/app/" + appid, appid, soup) 1048 | 1049 | async def get_recommendations(appid, timeout=10): 1050 | appid = str(appid) 1051 | similar = [] 1052 | async with aiohttp.ClientSession() as session: 1053 | resp = await session.get("https://store.steampowered.com/recommended/morelike/app/" + appid, timeout=timeout) 1054 | text = await resp.text() 1055 | print(text) 1056 | 1057 | soup = BeautifulSoup(text, "html.parser") 1058 | 1059 | 1060 | items = soup.find_all("div", {"class": "similar_grid_item"}) 1061 | print("found %s items" % len(items)) 1062 | for item in items: 1063 | subsoup = item.find("div", {"class": "similar_grid_capsule"}) 1064 | if subsoup is not None: 1065 | similar_id = subsoup.get("data-ds-appid") 1066 | if similar_id is not None: 1067 | similar.append(similar_id) 1068 | else: 1069 | print("failed to find appid") 1070 | else: 1071 | print("failed to get item") 1072 | return similar 1073 | 1074 | async def get_user_level(userid, timeout=10, be_specific=False): 1075 | if not is_integer(userid): 1076 | userid = await search_for_userid(userid, timeout=timeout, be_specific=be_specific) 1077 | async with aiohttp.ClientSession() as session: 1078 | resp = await session.get("https://api.steampowered.com/IPlayerService/GetSteamLevel/v1/?key=%s&steamid=%s" % (STEAM_KEY, userid), timeout=timeout) 1079 | data = await resp.json() 1080 | 1081 | if "response" in data: 1082 | return data["response"].get("player_level") 1083 | return None 1084 | 1085 | async def get_games(term, timeout=10, limit=-1, cc="gb"): 1086 | """Search for a game on steam 1087 | 1088 | Args: 1089 | term (str): the game you want to search for 1090 | timeout (int, optional): how long aiohttp should wait before raising a timeout error 1091 | limit (int, optional): how many results you want to return, 0 or less means every result 1092 | Returns: 1093 | a list of GameResult objects containing the results 1094 | """ 1095 | async with aiohttp.ClientSession() as session: 1096 | resp = await session.get("https://store.steampowered.com/search/?term=" + parse.quote(term) + "&cc=" + cc, timeout=timeout) 1097 | text = await resp.read() 1098 | soup = BeautifulSoup(text, "html.parser") 1099 | 1100 | subsoup = soup.findAll("div", {"id": "search_result_container"})[0] 1101 | rawResults = subsoup.findAll("a") 1102 | results = [] 1103 | n = 0 1104 | for x in rawResults: 1105 | if n >= limit > 0: 1106 | break 1107 | n += 1 1108 | cls = x.get("class") 1109 | if cls is not None and "search_result_row" in cls: 1110 | gr = GameResult(x) 1111 | #await gr.update_price(currency, currency_symbol) 1112 | results.append(gr) 1113 | return results 1114 | 1115 | 1116 | async def category_search(link, timeout=10, limit=-1, cc="gb"): 1117 | async with aiohttp.ClientSession() as session: 1118 | resp = await session.get("https://store.steampowered.com/" + link + "&cc=" + cc, timeout=timeout) 1119 | text = await resp.read() 1120 | soup = BeautifulSoup(text, "html.parser") 1121 | 1122 | results = [] 1123 | soups = soup.find_all("a", {"class": "search_result_row"}) 1124 | for subsoup in soups: 1125 | results.append(CategoryResult(subsoup)) 1126 | if 0 < limit <= len(results): 1127 | break 1128 | return results 1129 | 1130 | async def top_search(*args, **kwargs): 1131 | result = await category_search("search/?filter=topsellers", *args, **kwargs) 1132 | return result 1133 | 1134 | async def upcoming_search(*args, **kwargs): 1135 | result = await category_search("search/?filter=comingsoon", *args, **kwargs) 1136 | return result 1137 | 1138 | async def specials_search(*args, **kwargs): 1139 | result = await category_search("search/?specials=1", *args, **kwargs) 1140 | return result 1141 | 1142 | async def new_search(timeout=10, limit=-1, cc="gb"): 1143 | async with aiohttp.ClientSession() as session: 1144 | resp = await session.get("https://store.steampowered.com/explore/new/?cc=%s" % cc, timeout=timeout) 1145 | text = await resp.read() 1146 | soup = BeautifulSoup(text, "html.parser") 1147 | 1148 | results = [] 1149 | subsoups = soup.find_all("a", {"class": "tab_item"}) 1150 | for subsoup in subsoups: 1151 | results.append(NewCategoryResult(subsoup)) 1152 | if 0 < limit <= len(results): 1153 | break 1154 | 1155 | return results 1156 | 1157 | async def new_specials(timeout=10, limit=-1, cc="gb"): 1158 | """Search for a game on steam 1159 | 1160 | Args: 1161 | term (str): the game you want to search for 1162 | timeout (int, optional): how long aiohttp should wait before raising a timeout error 1163 | limit (int, optional): how many results you want to return, 0 or less means every result 1164 | Returns: 1165 | a list of GameResult objects containing the results 1166 | """ 1167 | async with aiohttp.ClientSession() as session: 1168 | resp = await session.get("https://store.steampowered.com/search/?specials=1&cc=" + cc, timeout=timeout) 1169 | text = await resp.read() 1170 | soup = BeautifulSoup(text, "html.parser") 1171 | 1172 | subsoup = soup.findAll("div", {"id": "search_result_container"})[0] 1173 | rawResults = subsoup.findAll("a") 1174 | results = [] 1175 | n = 0 1176 | for x in rawResults: 1177 | if n >= limit > 0: 1178 | break 1179 | n += 1 1180 | cls = x.get("class") 1181 | if cls is not None and "search_result_row" in cls: 1182 | gr = GameResult(x) 1183 | #await gr.update_price(currency, currency_symbol) 1184 | results.append(gr) 1185 | return results 1186 | 1187 | 1188 | 1189 | async def top_sellers(timeout=60, limit=-1, cc="gb"): 1190 | """gets the top sellers on the front page of the store 1191 | 1192 | Args: 1193 | timeout (int, optional): how long aiohttp should wait before throwing a timeout error 1194 | limit (int, optional): how many results it should return, 0 or less returns every result found 1195 | Returns: 1196 | a list of TopResult objects""" 1197 | async with aiohttp.ClientSession() as session: 1198 | resp = await session.get("https://store.steampowered.com/?cc=" + cc, timeout=timeout) 1199 | text = await resp.read() 1200 | soup = BeautifulSoup(text, "html.parser") 1201 | 1202 | subsoup = soup.find("div", {"id": "tab_topsellers_content"}) 1203 | rawResults = subsoup.findAll("a", recursive=False) 1204 | results = [] 1205 | n = 0 1206 | for x in rawResults: 1207 | if n >= limit > 0: 1208 | break 1209 | 1210 | cls = x.get("class") 1211 | if cls is not None and "tab_item" in cls: 1212 | #if cls is not None and "sale_capsule" in cls: 1213 | tr = TopResult(x) 1214 | results.append(tr) 1215 | n += 1 1216 | #try: 1217 | # tr = SteamSaleResult(x) 1218 | # await tr.get_title(cc=cc, timeout=timeout) 1219 | # #await tr.update_price(currency, currency_symbol) 1220 | # results.append(tr) 1221 | # n += 1 1222 | #except: 1223 | # print("WARNING: failed to create result") 1224 | return results 1225 | 1226 | 1227 | async def new_releases(timeout=10, limit=-1, cc="gb"): 1228 | """gets the new releases on the front page of the store 1229 | 1230 | Args: 1231 | timeout (int, optional): how long aiohttp should wait before throwing a timeout error 1232 | limit (int, optional): how many results it should return, 0 or less returns every result found 1233 | Returns: 1234 | a list of TopResult objects""" 1235 | async with aiohttp.ClientSession() as session: 1236 | resp = await session.get("https://store.steampowered.com/?cc=" + cc, timeout=timeout) 1237 | text = await resp.read() 1238 | soup = BeautifulSoup(text, "html.parser") 1239 | 1240 | subsoup = soup.find("div", {"id": "tab_newreleases_content"}) 1241 | rawResults = subsoup.findAll("a", recursive=False) 1242 | results = [] 1243 | n = 0 1244 | for x in rawResults: 1245 | if n >= limit > 0: 1246 | break 1247 | cls = x.get("class") 1248 | 1249 | if cls is not None and "tab_item" in cls: 1250 | #if cls is not None and "sale_capsule" in cls: 1251 | tr = TopResult(x) 1252 | results.append(tr) 1253 | n += 1 1254 | #try: 1255 | # tr = SteamSaleResult(x) 1256 | # await tr.get_title(cc=cc, timeout=timeout) 1257 | # #await tr.update_price(currency, currency_symbol) 1258 | # results.append(tr) 1259 | # n += 1 1260 | #except: 1261 | # print("WARNING: failed to create result") 1262 | return results 1263 | 1264 | 1265 | async def upcoming(timeout=10, limit=-1, cc="gb"): 1266 | """gets the upcoming games on the front page of the store 1267 | 1268 | Args: 1269 | timeout (int, optional): how long aiohttp should wait before throwing a timeout error 1270 | limit (int, optional): how many results it should return, 0 or less returns every result found 1271 | Returns: 1272 | a list of TopResult objects""" 1273 | async with aiohttp.ClientSession() as session: 1274 | resp = await session.get("https://store.steampowered.com/?cc=" + cc, timeout=timeout) 1275 | text = await resp.read() 1276 | soup = BeautifulSoup(text, "html.parser") 1277 | 1278 | subsoup = soup.find("div", {"id": "tab_upcoming_content"}) 1279 | rawResults = subsoup.findAll("a", recursive=False) 1280 | results = [] 1281 | n = 0 1282 | for x in rawResults: 1283 | if n >= limit > 0: 1284 | break 1285 | cls = x.get("class") 1286 | if cls is not None and "tab_item" in cls: 1287 | #if cls is not None and "sale_capsule" in cls: 1288 | tr = TopResult(x) 1289 | results.append(tr) 1290 | n += 1 1291 | #try: 1292 | # tr = SteamSaleResult(x) 1293 | # await tr.get_title(cc=cc, timeout=timeout) 1294 | # #await tr.update_price(currency, currency_symbol) 1295 | # results.append(tr) 1296 | # n += 1 1297 | #except: 1298 | # print("WARNING: failed to create result") 1299 | return results 1300 | 1301 | 1302 | async def specials(timeout=10, limit=-1, cc="gb"): 1303 | """gets the specials on the front page of the store 1304 | 1305 | Args: 1306 | timeout (int, optional): how long aiohttp should wait before throwing a timeout error 1307 | limit (int, optional): how many results it should return, 0 or less returns every result found 1308 | Returns: 1309 | a list of TopResult objects""" 1310 | async with aiohttp.ClientSession() as session: 1311 | resp = await session.get("https://store.steampowered.com/?cc=" + cc, timeout=timeout) 1312 | text = await resp.read() 1313 | soup = BeautifulSoup(text, "html.parser") 1314 | 1315 | subsoup = soup.find("div", {"id": "tab_specials_content"}) 1316 | rawResults = subsoup.findAll("a", recursive=False) 1317 | results = [] 1318 | n = 0 1319 | for x in rawResults: 1320 | if n >= limit > 0: 1321 | break 1322 | cls = x.get("class") 1323 | if cls is not None and "tab_item" in cls: 1324 | tr = TopResult(x) 1325 | #await tr.update_price(currency, currency_symbol) 1326 | results.append(tr) 1327 | n += 1 1328 | return results 1329 | 1330 | 1331 | async def get_user(steamid, timeout=10, be_specific=False): 1332 | """Gets some information about a specific steamid 1333 | 1334 | Args: 1335 | steamid (str): The user's steamid 1336 | timeout (int, optional): The amount of time before aiohttp raises a timeout error 1337 | Returns: 1338 | a UserResult object 1339 | """ 1340 | if not is_integer(steamid): 1341 | steamid = await search_for_userid(steamid, be_specific=be_specific) 1342 | if steamid is not None: 1343 | _check_key_set() 1344 | async with aiohttp.ClientSession() as session: 1345 | resp = await session.get("https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key=" + STEAM_KEY + "&steamids=" + steamid, timeout=timeout) 1346 | data = await resp.json() 1347 | 1348 | if "response" in data and "players" in data["response"] and len(data["response"]["players"]) > 0: 1349 | player = data["response"]["players"][0] 1350 | return UserResult(player) 1351 | return None 1352 | 1353 | 1354 | async def get_user_library(steamid, timeout=10, be_specific=False): 1355 | """Gets a list of all the games a user owns 1356 | 1357 | Args: 1358 | steamid (str): The user's steamid 1359 | timeout (int, optional): The amount of time before aiohttp raises a timeout error 1360 | Returns: 1361 | a UserLibrary object 1362 | """ 1363 | if not is_integer(steamid): 1364 | steamid = await search_for_userid(steamid, be_specific=be_specific) 1365 | if steamid is not None: 1366 | _check_key_set() 1367 | async with aiohttp.ClientSession() as session: 1368 | resp = await session.get("https://api.steampowered.com/IPlayerService/GetOwnedGames/v0001/?key=" + STEAM_KEY + "&steamid=" + steamid + "&format=json&include_appinfo=1&include_played_free_games=1", timeout=timeout) 1369 | data = await resp.json() 1370 | 1371 | if "response" in data: 1372 | player = data["response"] 1373 | return UserLibrary(player) 1374 | return None 1375 | 1376 | 1377 | userid_cache = {} # caches search terms to steamids 1378 | 1379 | 1380 | async def get_user_id(name, timeout=10): 1381 | """Resolves a username to a steamid, however is limited to ONLY vanity URL's. search_user_id is recommended 1382 | 1383 | Args: 1384 | name (str): The name of the user to find the steamid of 1385 | timeout (int, optional): The amount of time before aiohttp raises a timeout error 1386 | Returns: 1387 | either None or a steamid (str) if a vanity url matching that name is found 1388 | """ 1389 | if name in userid_cache: 1390 | return userid_cache[name] 1391 | else: 1392 | _check_key_set() 1393 | async with aiohttp.ClientSession() as session: 1394 | resp = await session.get("https://api.steampowered.com/ISteamUser/ResolveVanityURL/v0001/?key=" + STEAM_KEY + "&vanityurl=" + parse.quote(name), timeout=timeout) 1395 | data = await resp.json() 1396 | 1397 | if "response" in data and "success" in data["response"] and data["response"]["success"] == 1: 1398 | id = data["response"]["steamid"] 1399 | if STEAM_CACHE: 1400 | userid_cache[name] = id 1401 | return id 1402 | return None 1403 | 1404 | 1405 | async def search_for_userid(username, timeout=10, be_specific=False): 1406 | """Searches for a steamid based on a username, not using vanity URLs 1407 | 1408 | Args: 1409 | username (str): the username of the user you're searching for 1410 | timeout (int, optional): the amount of time before aiohttp throws a timeout error 1411 | Returns: 1412 | A steamid (str) 1413 | """ 1414 | if username in userid_cache: 1415 | return userid_cache[username] 1416 | else: 1417 | if be_specific: 1418 | uid = await get_user_id(username, timeout=timeout) 1419 | return uid 1420 | else: 1421 | links = await search_for_users(username, limit=1, timeout=timeout) 1422 | if len(links) > 0: 1423 | uid = await extract_id_from_url(links[0][0], timeout=timeout) 1424 | return uid 1425 | else: 1426 | uid = await get_user_id(username, timeout=timeout) 1427 | return uid 1428 | 1429 | 1430 | async def search_for_users(username, limit=1, timeout=10): 1431 | """Searches for basic information about users 1432 | 1433 | Args: 1434 | username (str): the username of the user you're searching for 1435 | timeout (int, optional): the amount of time before aiohttp throws a timeout error 1436 | limit (int, optional): the amount of user results to return, 0 or less for all of them 1437 | Returns: 1438 | a list of tuples containing (steam_profile_url (str), steam_user_name (str)) 1439 | """ 1440 | _check_session_set() 1441 | async with aiohttp.ClientSession() as session: 1442 | resp = await session.get("https://steamcommunity.com/search/SearchCommunityAjax?text=" + parse.quote(username) + "&filter=users&sessionid=" + STEAM_SESSION + "&page=1", headers={"Cookie": "sessionid=" + STEAM_SESSION}, timeout=timeout) 1443 | data = await resp.json() 1444 | soup = BeautifulSoup(data["html"], "html.parser") 1445 | stuff = soup.find_all("a", {"class": "searchPersonaName"}) 1446 | links = [] 1447 | for thing in stuff: 1448 | try: 1449 | links.append((thing.get("href"), thing.get_text())) 1450 | if len(links) >= limit > 0: 1451 | return links 1452 | except: 1453 | pass 1454 | return links 1455 | 1456 | 1457 | async def extract_id_from_url(url, timeout=10): 1458 | """Extracts a steamid from a steam user's profile URL, or finds it based on a vanity URL 1459 | 1460 | Args: 1461 | url (str): The url of the user's profile 1462 | timeout (int, optional): The amount of time before aiohttp raises a timeout error 1463 | Returns: 1464 | the steamid of the user (str) or None if no steamid could be extracted 1465 | """ 1466 | if url.startswith("https://steamcommunity.com/profiles/"): 1467 | return url[len("https://steamcommunity.com/profiles/"):] 1468 | elif url.startswith("https://steamcommunity.com/id/"): 1469 | vanityname = url[len("https://steamcommunity.com/id/"):] 1470 | id = await get_user_id(vanityname, timeout=timeout) 1471 | return id 1472 | 1473 | 1474 | async def get_item(appid, item_name, timeout=10, currency="GBP", currency_symbol="£"): 1475 | """Gets information about an item from the market 1476 | 1477 | Args: 1478 | appid (str): The appid of the game the item belongs to, or the name if you don't know the ID 1479 | item_name (str): The item you're searching for 1480 | timeout (int, optional): The amount of time before aiohttp raises a timeout error 1481 | currency (str, optional): The currency to convert the item's price to (default GBP) 1482 | currency_symbol (str, optional): the currency symbol to use for the item's price (default £) 1483 | Returns: 1484 | an ItemResult object 1485 | """ 1486 | if not is_integer(appid): 1487 | appdata = await get_app(appid, timeout) 1488 | appid = appdata[0] 1489 | if appid is not None: 1490 | item_name = await get_item_name(item_name, appid, timeout=timeout) 1491 | if item_name is not None: 1492 | async with aiohttp.ClientSession() as session: 1493 | resp = await session.get("https://steamcommunity.com/market/listings/" + appid + "/" + parse.quote(item_name), timeout=timeout) 1494 | text = await resp.text() 1495 | soup = BeautifulSoup(text, "html.parser") 1496 | 1497 | result = ItemResult(soup) 1498 | await result.update_price(currency, currency_symbol) 1499 | return result 1500 | 1501 | 1502 | gameid_cache = {} # caches search terms to (appid, appname) tuples 1503 | 1504 | 1505 | async def get_app(name, timeout=10): 1506 | """Gets an appid based off of the app name 1507 | 1508 | Args: 1509 | name (str): the name of the app (game) 1510 | timeout (int, optional): the amount of time before aiohttp raises a timeout error 1511 | Returns: 1512 | A tuple containing (appid (str), apptitle (str)) 1513 | """ 1514 | if name in gameid_cache: 1515 | return gameid_cache[name] 1516 | else: 1517 | dat = await get_games(name, limit=1, timeout=timeout) 1518 | if len(dat) > 0: 1519 | if STEAM_CACHE: 1520 | gameid_cache[name] = (dat[0].id, dat[0].title) 1521 | return dat[0].id, dat[0].title 1522 | else: 1523 | return None, None 1524 | 1525 | 1526 | item_name_cache = {} # caches search terms to item url names 1527 | 1528 | 1529 | async def get_item_name(name, appid, timeout=10): 1530 | """Finds an item's name required for the URL of it's store page 1531 | 1532 | Args: 1533 | name (str): The name of the item you're searching for 1534 | appid (str): The appid of the game the item belongs to 1535 | timeout (int, optional): The amount of time before aiohttp raises a timeout error 1536 | Returns: 1537 | the item name (str) or None if no item could be found 1538 | """ 1539 | cache_name = appid + "::" + name 1540 | if cache_name in item_name_cache: 1541 | return item_name_cache[cache_name] 1542 | else: 1543 | async with aiohttp.ClientSession() as session: 1544 | if appid != "": 1545 | resp = await session.get("https://steamcommunity.com/market/search?appid=" + appid + "&q=" + parse.quote(name), timeout=timeout) 1546 | else: 1547 | resp = await session.get("https://steamcommunity.com/market/search?q=" + parse.quote(name), timeout=timeout) 1548 | text = await resp.text() 1549 | soup = BeautifulSoup(text, "html.parser") 1550 | 1551 | namesoup = soup.find("span", {"class": "market_listing_item_name"}) 1552 | if namesoup is not None: 1553 | item_name = namesoup.get_text() 1554 | if STEAM_CACHE: 1555 | item_name_cache[cache_name] = item_name 1556 | return item_name 1557 | return None 1558 | 1559 | 1560 | async def get_wishlist(userid, cc="gb", timeout=10, discount_only=True, be_specific=False): 1561 | if not is_integer(userid): 1562 | userid = await search_for_userid(userid, be_specific=be_specific) 1563 | if userid is not None: 1564 | print(userid) 1565 | async with aiohttp.ClientSession() as session: 1566 | URL = "https://store.steampowered.com/wishlist/profiles/" + userid + "/?cc=" + cc 1567 | print(URL) 1568 | resp = await session.get("https://store.steampowered.com/wishlist/profiles/" + userid + "/wishlistdata/?cc=" + cc, timeout=timeout) 1569 | data = await resp.json() 1570 | 1571 | games = [] 1572 | 1573 | for appid in data: 1574 | game = data[appid] 1575 | name = game.get("name", "???") 1576 | link = "https://store.steampowered.com/app/%s/" % appid 1577 | price = "???" 1578 | subs = game.get("subs", []) 1579 | if len(subs) > 0: 1580 | html = None 1581 | discounted = False 1582 | for sub in subs: 1583 | if "discount_block" in sub: 1584 | html = sub["discount_block"] 1585 | discounted = sub.get("discount_pct", 0) > 0 1586 | break 1587 | 1588 | if html is not None: 1589 | soup = BeautifulSoup(html, "html.parser") 1590 | price_soup = soup.find("div", {"class": "discount_final_price"}) 1591 | if price_soup is not None: 1592 | price = price_soup.get_text() 1593 | 1594 | if discounted: 1595 | original_price = "???" 1596 | original_price_soup = soup.find("div", {"class": "discount_original_price"}) 1597 | if original_price_soup is not None: 1598 | original_price = original_price_soup.get_text() 1599 | 1600 | discount_percent = "??%" 1601 | discount_percent_soup = soup.find("div", {"class": "discount_pct"}) 1602 | if discount_percent_soup is not None: 1603 | discount_percent = discount_percent_soup.get_text() 1604 | 1605 | games.append((name, link, original_price, price, discount_percent)) 1606 | continue 1607 | 1608 | if not discount_only: 1609 | games.append((name, link, price)) 1610 | 1611 | return UserWishlist(games) 1612 | 1613 | 1614 | async def get_screenshots(username, timeout=10, limit=-1): 1615 | """Searches for the most recent (public) screenshots a user has uploaded, 1616 | 1617 | Args: 1618 | username (str): The name of the user you're finding screenshots for 1619 | timeout (int, optional): The amount of time before aiohttp raises a timeout error 1620 | limit (intm optional): The amount of screenshots to find, 0 or less for all of them 1621 | Returns: 1622 | a list of URLs (strings) linking to the screenshots 1623 | """ 1624 | ulinks = await search_for_users(username, limit=1) 1625 | if len(ulinks) > 0: 1626 | async with aiohttp.ClientSession() as session: 1627 | resp = await session.get(ulinks[0][0] + "/screenshots/", timeout=timeout) 1628 | text = await resp.text() 1629 | soup = BeautifulSoup(text, "html.parser") 1630 | 1631 | links = [] 1632 | screensoups = soup.find_all("a", {"class": "profile_media_item"}) 1633 | for ssoup in screensoups: 1634 | imgsoup = ssoup.find("img") 1635 | if imgsoup is not None: 1636 | links.append(imgsoup.get("src")) 1637 | if len(links) >= limit > 0: 1638 | break 1639 | return links 1640 | else: 1641 | return None 1642 | 1643 | 1644 | async def top_game_playercounts(limit=10, timeout=10): 1645 | """Gets the top games on steam right now by player count 1646 | 1647 | Args: 1648 | timeout (int, optional): The amount of time before aiohttp raises a timeout error 1649 | limit (int, optional): The amount of playercounts to return, 0 or less for all found 1650 | Returns: 1651 | A list of tuples in the format (current_players (str), peak_players (str), game_name (str), game_link (str)) 1652 | """ 1653 | async with aiohttp.ClientSession() as session: 1654 | resp = await session.get("https://store.steampowered.com/stats", timeout=timeout) 1655 | text = await resp.text() 1656 | soup = BeautifulSoup(text, "html.parser") 1657 | 1658 | stats = [] 1659 | ssoups = soup.find_all("tr", {"class": "player_count_row"}) 1660 | for subsoup in ssoups: 1661 | linksoup = subsoup.find("a", {"class": "gameLink"}) 1662 | name = linksoup.get_text() 1663 | link = linksoup.get("href") 1664 | stuff = subsoup.find_all("span", {"class": "currentServers"}) 1665 | if len(stuff) > 0: 1666 | current_players = stuff[0].get_text() 1667 | peak_players = stuff[1].get_text() 1668 | stats.append((current_players, peak_players, name, link)) 1669 | if len(stats) >= limit > 0: 1670 | break 1671 | return stats 1672 | 1673 | async def get_playercount(appid, timeout=10): 1674 | async with aiohttp.ClientSession() as session: 1675 | resp = await session.get("https://api.steampowered.com/ISteamUserStats/GetNumberOfCurrentPlayers/v1/?key=%s&format=json&appid=%s" % (STEAM_KEY, appid), timeout=timeout) 1676 | data = await resp.json() 1677 | 1678 | if "response" in data: 1679 | return data["response"].get("player_count") 1680 | 1681 | async def search_for_playercount(appid, timeout=10, be_specific=False): 1682 | if not be_specific: 1683 | appid, appname = await get_app(appid) 1684 | else: 1685 | appname = appid 1686 | 1687 | async with aiohttp.ClientSession() as session: 1688 | resp = await session.get("https://store.steampowered.com/stats", timeout=timeout) 1689 | text = await resp.text() 1690 | soup = BeautifulSoup(text, "html.parser") 1691 | 1692 | number = 0 1693 | ssoups = soup.find_all("tr", {"class": "player_count_row"}) 1694 | for subsoup in ssoups: 1695 | number += 1 1696 | linksoup = subsoup.find("a", {"class": "gameLink"}) 1697 | name = linksoup.get_text() 1698 | link = linksoup.get("href") 1699 | if link.split("/")[-2] == appid: 1700 | stuff = subsoup.find_all("span", {"class": "currentServers"}) 1701 | if len(stuff) > 0: 1702 | current_players = stuff[0].get_text() 1703 | peak_players = stuff[1].get_text() 1704 | return (name, current_players, peak_players, number, link) 1705 | 1706 | if appid is None: 1707 | return None 1708 | 1709 | current_players = await get_playercount(appid, timeout=timeout) 1710 | if current_players is not None: 1711 | return (appname, current_players, "???", "???", "https://store.steampowered.com/app/%s/" % appid) 1712 | 1713 | 1714 | 1715 | async def steam_user_data(timeout=10): 1716 | """Gets information about the amount of users on steam over the past 48 hours 1717 | 1718 | Args: 1719 | timeout (int, optional): The amount of time before aiohttp raises a timeout error 1720 | Returns: 1721 | A tuple containing (min_users (int), max_users (int), current_users (int))""" 1722 | async with aiohttp.ClientSession() as session: 1723 | resp = await session.get("https://store.steampowered.com/stats/userdata.json", timeout=timeout) 1724 | data = await resp.json() 1725 | data = data[0]["data"] 1726 | 1727 | min_users = -1 1728 | max_users = -1 1729 | for pair in data: 1730 | if min_users == -1 or pair[1] < min_users: 1731 | min_users = pair[1] 1732 | if max_users == -1 or pair[1] > max_users: 1733 | max_users = pair[1] 1734 | return min_users, max_users, data[-1][1] 1735 | 1736 | 1737 | 1738 | async def get_user_achievements(username, gameid, timeout=10, be_specific=False): 1739 | """Gets information about a specific user's achievements for a specific game 1740 | 1741 | Args: 1742 | username (str): the id or name of the user you want the achievements for 1743 | gameid (str): the id or name of the game you want the achievements for 1744 | timeout (int): the amount of time before aiohttp raises a timeout error 1745 | Returns: 1746 | UserAchievement: the user achievements found""" 1747 | if not is_integer(username): 1748 | username = await search_for_userid(username, timeout=timeout, be_specific=be_specific) 1749 | if not is_integer(gameid): 1750 | gameid, gamename = await get_app(gameid, timeout=timeout) 1751 | else: 1752 | gamename = "???" 1753 | _check_key_set() 1754 | if username is not None and gameid is not None: 1755 | async with aiohttp.ClientSession() as session: 1756 | resp = await session.get("https://api.steampowered.com/ISteamUserStats/GetPlayerAchievements/v0001/?appid=" + gameid + "&key=" + STEAM_KEY + "&steamid=" + username, timeout=timeout) 1757 | data = await resp.json() 1758 | if "playerstats" in data and "achievements" in data["playerstats"]: 1759 | return UserAchievements(gameid, gamename, data["playerstats"]["achievements"]) 1760 | 1761 | 1762 | async def get_global_achievements(gameid, timeout=10): 1763 | """Gets information about a game's global achievement stats (name, description, percent completed) 1764 | 1765 | Args: 1766 | gameid (str): the id or name of the game you want the achievements for 1767 | timeout (int, optional): the amount of time before aiohttp raises a timeout error 1768 | Returns: 1769 | GlobalAchievements: the global achievements found 1770 | """ 1771 | if not is_integer(gameid): 1772 | gameid, gamename = await get_app(gameid, timeout=timeout) 1773 | if gameid is not None: 1774 | async with aiohttp.ClientSession() as session: 1775 | resp = await session.get("https://steamcommunity.com/stats/" + gameid + "/achievements/", timeout=timeout) 1776 | text = await resp.text() 1777 | soup = BeautifulSoup(text, "html.parser") 1778 | 1779 | return GlobalAchievements(soup) 1780 | 1781 | 1782 | async def count_user_removed(username, timeout=10, be_specific=False): 1783 | if not is_integer(username): 1784 | username = await search_for_userid(username, be_specific=be_specific) 1785 | 1786 | if username is None: 1787 | return 1788 | 1789 | async with aiohttp.ClientSession() as session: 1790 | resp = await session.get( 1791 | 'https://removed.timekillerz.eu/content/steambot-server.php?steamid=' + parse.quote(username), 1792 | timeout=timeout 1793 | ) 1794 | 1795 | _data = await resp.json() 1796 | data = _data['response'] 1797 | 1798 | try: 1799 | return data['removed_count'], data['game_count'], data['total_removed_count'], data['players'][0]['personaname'] 1800 | except KeyError: 1801 | return # don't want to propagate the error 1802 | 1803 | 1804 | def convert_to_table(items, columns, seperator="|", spacing=1): 1805 | """Utility function to convert a list of times in to a neat table, with the given columns 1806 | 1807 | Args: 1808 | items (list[str]): the strings you want to put in the table 1809 | columns (int): the amount of columns you want in the table 1810 | seperator (str, optional): what to seperate the columns by, default is | 1811 | spacing (int, optional): how many spaces to put either side of the seperator, default is 1 1812 | Returns: 1813 | list[str]: the rows of the table 1814 | """ 1815 | max_sizes = [0] * columns 1816 | for i, item in enumerate(items): 1817 | column = i % columns 1818 | if len(item) > max_sizes[column]: 1819 | max_sizes[column] = len(item) 1820 | 1821 | spacing = " " * spacing 1822 | lines = [""] * math.ceil(len(items) / columns) 1823 | for i in range(0, len(items), columns): 1824 | line = "" 1825 | maxc = min(len(items), i+columns) 1826 | for j in range(i, maxc): 1827 | c = j - i 1828 | line += items[j] + " " * (max_sizes[c] - len(items[j])) 1829 | if c < maxc - 1: 1830 | line += spacing + seperator + spacing 1831 | lines.append(line) 1832 | return lines 1833 | -------------------------------------------------------------------------------- /examples/top_games_example.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is example shows how to use steamsearch to get the top games in different categories 3 | """ 4 | 5 | import steamsearch 6 | 7 | steamsearch.set_key("STEAM-API-KEY", "anotherSession", cache=True) 8 | 9 | popular = steamsearch.top_sellers(limit=5) # type: list[steamsearch.TopResult] 10 | print(" --- TOP SELLERS --- ") 11 | for result in popular: 12 | print(result.title) 13 | 14 | print("") 15 | 16 | most_played = steamsearch.top_game_playercounts(limit=5) 17 | print(" --- TOP PLAYERCOUNTS --- ") 18 | for result in most_played: # result is a tuple (current_playercount, peak_playercount, game, link) 19 | print("{0[2]} : {0[3]}".format(result)) -------------------------------------------------------------------------------- /examples/user_example.py: -------------------------------------------------------------------------------- 1 | """ 2 | This example shows how to use steamsearch to get information about a user and some information about what games they own. 3 | """ 4 | 5 | import steamsearch 6 | 7 | steamsearch.set_key("STEAM-API-KEY", "exampleSession", cache=True) 8 | 9 | user = steamsearch.get_user("billyoyo") # type: steamsearch.UserResult 10 | print("{0.id} :: {0.name}".format(user)) # print the user's ID and name 11 | 12 | library = steamsearch.get_user_library(user.id) # type: steamsearch.UserLibrary 13 | 14 | gameid, gamename = steamsearch.get_app("civilisation 5") # returns the gameid and gamename of civilisation 5 15 | game = library.games.get(gameid, None) # type: steamsearch.UserGame 16 | print("{0} has {1} on {2}".format(user.name, game.get_playtime_string(), game.name)) # print information about playtime 17 | 18 | top_games = library.get_top_games(limit=10) # type: list[steamsearch.UserGame] 19 | print(" --- TOP 10 GAMES --- ") 20 | for game in top_games: 21 | print(game.name) # prints the game's name -------------------------------------------------------------------------------- /steamsearch.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2016-2017 billyoyo 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | import requests 26 | import operator 27 | import json 28 | from urllib import parse 29 | from bs4 import BeautifulSoup 30 | 31 | # used to map currency symbols to currency codes 32 | CURRENCY_MAP = { 33 | "lek": "ALL", 34 | "$": "USD", 35 | "ман": "AZN", 36 | "p.": "BYR", 37 | "BZ$": "BZD", 38 | "$b": "BOB", 39 | "KM": "BAM", 40 | "P": "BWP", 41 | "лв": "BGN", 42 | "R$": "BRL", 43 | "¥": "JPY", 44 | "₡": "CRC", 45 | "kn": "HRK", 46 | "₱": "CUP", 47 | "Kč": "CZK", 48 | "kr": "DKK", 49 | "RD$": "DOP", 50 | "£": "GBP", 51 | "€": "EUR", 52 | "¢": "GHS", 53 | "Q": "GTQ", 54 | "L": "HNL", 55 | "Ft": "HUF", 56 | "Rp": "IDR", 57 | "₪": "ILS", 58 | "J$": "JMD", 59 | "₩": "KRW", 60 | "₭": "LAK", 61 | "ден": "MKD", 62 | "RM": "MYR", 63 | "Rs": "MUR", 64 | "руб": "RUB" 65 | } 66 | 67 | STEAM_KEY = "" # contains your Steam API key (set using set_key) 68 | STEAM_CACHE = True # whether or not steamsearch should cache some results which generally aren't going to change 69 | STEAM_SESSION = "" # your Steam Session for SteamCommunityAjax 70 | STEAM_PRINTING = False # whether or not steamsearch will occasionally print warnings 71 | 72 | 73 | def set_key(key, session, cache=True, printing=False): 74 | """Used to initiate your key + session strings, also to enable/disable caching 75 | 76 | Args: 77 | key (str): Your Steam API key 78 | session (str): Your SteamCommunityAjax session, this basically just needs to be any string containing only a-z, A-Z or 0-9 79 | cache (bool, optional): True to enable caching 80 | """ 81 | global STEAM_KEY, STEAM_CACHE, STEAM_SESSION, STEAM_PRINTING 82 | STEAM_KEY = key 83 | STEAM_SESSION = session 84 | STEAM_CACHE = cache 85 | STEAM_PRINTING = printing 86 | 87 | 88 | def count_cache(): 89 | """Counts the amount of cached results 90 | 91 | Returns: 92 | the number of cached results (int) 93 | """ 94 | return len(gameid_cache) + len(item_name_cache) + len(userid_cache) 95 | 96 | 97 | def clear_cache(): 98 | """Clears all of the cached results 99 | 100 | Returns: 101 | the number of results cleared 102 | """ 103 | global gameid_cache, item_name_cache, userid_cache 104 | items = count_cache() 105 | gameid_cache = {} 106 | item_name_cache = {} 107 | userid_cache = {} 108 | return items 109 | 110 | 111 | class SteamKeyNotSet(Exception): 112 | """Exception raised if STEAM_KEY is used before it was set""" 113 | pass 114 | 115 | 116 | class SteamSessionNotSet(Exception): 117 | """Exception raised if STEAM_SESSION is used before it was set""" 118 | pass 119 | 120 | 121 | def _check_key_set(): 122 | """Internal method to ensure STEAM_KEY has been set before attempting to use it""" 123 | if not isinstance(STEAM_KEY, str) or STEAM_KEY == "": 124 | raise SteamKeyNotSet 125 | 126 | 127 | def _check_session_set(): 128 | """Internal method to ensure STEAM_SESSION has been set before attempting to use it""" 129 | if not isinstance(STEAM_KEY, str) or STEAM_SESSION == "": 130 | raise SteamSessionNotSet 131 | 132 | 133 | def exchange(amount, from_curr, to_curr, timeout=10): 134 | """Converts an amount of money from one currency to another 135 | 136 | Args: 137 | amount (float): The amount of money you want to convert 138 | from_curr (str): The currency you want to convert from, 139 | either country symbol (e.g USD) or currency smybol (e.g. £) 140 | to_curr (str): The currency you want to convert to, same format as from_curr 141 | timeout (int, optional): The time in seconds aiohttp will take to timeout the request 142 | Returns: 143 | float: the converted amount of money to 2 d.p., or the original amount of the conversion failed. 144 | """ 145 | try: 146 | resp = requests.get("http://api.fixer.io/latest?symbols=" + from_curr + "," + to_curr, timeout=timeout) 147 | data = resp.json() 148 | if "rates" in data: 149 | return int((amount / data["rates"][from_curr]) * data["rates"][to_curr] * 100)/100 150 | except: 151 | return amount 152 | 153 | 154 | def is_integer(x): 155 | try: 156 | int(x) 157 | return True 158 | except: 159 | return False 160 | 161 | 162 | class GameResult: 163 | """Class containing information about a game search result""" 164 | def __init__(self, soup): 165 | """ 166 | 167 | Args: 168 | soup (BeautifulSoup): soup from game search page 169 | """ 170 | self.link = soup.get("href") 171 | linkspl = self.link.split("/") 172 | self.id = linkspl[4] 173 | 174 | self.image = "" 175 | imgsoup = soup.find("img") 176 | if imgsoup is not None: 177 | self.image = imgsoup.get("src") 178 | 179 | self.title = "???" 180 | titlesoup = soup.find("span", {"class": "title"}) 181 | if titlesoup is not None: 182 | self.title = titlesoup.get_text() 183 | 184 | self.released = "???" 185 | releasesoup = soup.find("div", {"class": "col search_released responsive_secondrow"}) 186 | if releasesoup is not None: 187 | self.released = releasesoup.get_text() 188 | 189 | self.review = "???" 190 | self.reviewLong = "???" 191 | reviewsoup = soup.findAll("span") 192 | for span in reviewsoup: 193 | cls = span.get("class") 194 | if cls is not None and "search_review_summary" in cls: 195 | review_raw = span.get("data-store-tooltip").split("
") 196 | self.review = review_raw[0] 197 | self.reviewLong = review_raw[1] 198 | break 199 | 200 | self.discount = "" 201 | discountsoup = soup.find("div", {"class": "col search_discount responsive_secondrow"}) 202 | if discountsoup is not None: 203 | span = discountsoup.find("span") 204 | if span is not None: 205 | self.discount = span.get_text().replace(" ", "").replace("\n", "").replace("\r", "").replace("\t", "") 206 | 207 | self.price = "???" 208 | self.discountPrice = "???" 209 | 210 | if self.discount == "": 211 | pricesoup = soup.find("div", {"class": "col search_price responsive_secondrow"}) 212 | self.price = pricesoup.get_text().replace(" ", "").replace("\n", "").replace("\t", "").replace("\r", "") 213 | else: 214 | pricesoup = soup.find("div", {"class": "col search_price discounted responsive_secondrow"}) 215 | span = pricesoup.find("span") 216 | if span is not None: 217 | self.price = span.get_text().replace(" ", "").replace("\n", "").replace("\t", "").replace("\r", "").replace("", "").replace("", "") 218 | self.discountPrice = pricesoup.get_text().replace(" ", "").replace("\n", "").replace("\t", "").replace("\r", "").replace(self.price, "") 219 | 220 | if self.price.lower() == "freetoplay": 221 | self.price = "Free to Play" 222 | 223 | def __str__(self): 224 | return self.title 225 | 226 | 227 | class TopResult: 228 | """Class containing information about the games on the front of the store (new releases, specials etc.)""" 229 | def __init__(self, soup): 230 | """ 231 | 232 | Args: 233 | soup (BeautifulSoup): Soup for the section of the store page containing the game information 234 | """ 235 | self.link = "???" 236 | 237 | linksoup = soup.find("a", {"class": "tab_item_overlay"}) 238 | if linksoup is not None: 239 | self.link = linksoup.get("href") 240 | if self.link is None: 241 | self.link = "???" 242 | 243 | self.image = "???" 244 | imagesoup = soup.find("div", {"class": "tab_item_cap"}) 245 | if imagesoup is not None: 246 | img = imagesoup.get("img") 247 | if img is not None: 248 | self.image = img.get("src") 249 | 250 | self.discount = "" 251 | self.price = "" 252 | self.discountPrice = "???" 253 | 254 | pricesoup = soup.find("div", {"class": "discount_block"}) 255 | if pricesoup is not None: 256 | discount = pricesoup.find("div", {"class": "discount_pct"}) 257 | if discount is not None: 258 | self.discount = discount.get_text() 259 | dpsoup = pricesoup.find("div", {"class": "discount_prices"}) 260 | if dpsoup is not None: 261 | if self.discount == "": 262 | price = dpsoup.find("div", {"class": "discount_final_price"}) 263 | self.price = price.get_text() 264 | else: 265 | price = dpsoup.find("div", {"class": "discount_original_price"}) 266 | self.price = price.get_text() 267 | discountprice = dpsoup.find("div", {"class": "discount_final_price"}) 268 | self.discountPrice = discountprice.get_text() 269 | 270 | if self.price.lower() == "freetoplay": 271 | self.price = "Free to Play" 272 | 273 | titlesoup = soup.find("div", {"class": "tab_item_content"}) 274 | if titlesoup is not None: 275 | title = soup.find("div", {"class": "tab_item_name"}) 276 | self.title = title.get_text() 277 | 278 | self.review = "???" 279 | self.reviewLong = "???" 280 | self.released = "???" 281 | 282 | def get_price_text(self): 283 | if self.discount == "": 284 | return self.price 285 | else: 286 | return self.discountPrice + " (" + self.discount + ")" 287 | 288 | def __str__(self): 289 | return self.title 290 | 291 | 292 | class UserResult: 293 | """Class containing information about a specific user""" 294 | def __init__(self, data): 295 | """ 296 | 297 | Args: 298 | data (dict): part of the JSON returned by the Steam API 299 | """ 300 | self.id = data.get("steamid", "???") 301 | self.name = data.get("personaname", "???") 302 | self.visibilityState = str(data.get("communityvisibilitystate", "???")) 303 | self.profileStage = str(data.get("profilestate", "???")) 304 | self.lastLogoff = str(data.get("lastlogoff", "???")) 305 | self.url = data.get("profileurl", "???") 306 | self.avatar = data.get("avatar", "???") 307 | self.avatarMedium = data.get("avatarmedium", "???") 308 | self.avatarFull = data.get("avatarfull", "???") 309 | self.personaState = data.get("personastate", "???") 310 | self.realName = data.get("realname", "???") 311 | self.clan = data.get("primaryclanid", "???") 312 | self.created = str(data.get("timecreated", "???")) 313 | self.country = data.get("loccountrycode", "???") 314 | 315 | 316 | class UserGame: 317 | """Class containing information about user's playtime on a specific game""" 318 | def __init__(self, data): 319 | """ 320 | 321 | Args: 322 | data (dict): part of the JSON returned by the Steam API 323 | """ 324 | self.id = str(data.get("appid", "???")) 325 | self.name = data.get("name", "???") 326 | self.playtime_2weeks = str(data.get("playtime_2weeks", "???")) # IN MINUTES 327 | self.playtime_forever = str(data.get("playtime_forever", "???")) # IN MINUTES 328 | self.playtime_forever_int = 0 329 | if self.playtime_forever != "???": 330 | self.playtime_forever_int = int(self.playtime_forever) 331 | self.icon = data.get("img_icon_url", "???") 332 | self.logo = data.get("img_logo_url", "???") 333 | 334 | 335 | def format_playtime(self, playtime): 336 | """Formats the playtime in to hours 337 | 338 | Args: 339 | playtime (str | int): must be something representing an integer, playtime in minutes 340 | Returns: 341 | str: the formatted playtime in to Hours to 2 d.p. 342 | """ 343 | if playtime != "???": 344 | return str(int(int(playtime) / 6)/10) 345 | else: 346 | return playtime 347 | 348 | def single_line_format(self): 349 | """Converts the object to single line format 350 | 351 | Returns: 352 | A string representing this object 353 | """ 354 | if self.playtime_2weeks != "???": 355 | return self.format_playtime(self.playtime_forever) + " hours on record (" + self.format_playtime(self.playtime_2weeks) + " hours in the last 2 weeks)" 356 | else: 357 | return self.format_playtime(self.playtime_forever) + " hours on record" 358 | 359 | 360 | class UserLibrary: 361 | """Class containing information about a set of games in the users library""" 362 | def __init__(self, data): 363 | self.count = data.get("game_count", "???") 364 | self.games = {} 365 | for game in data.get("games", []): 366 | ugame = UserGame(game) 367 | self.games[ugame.id] = ugame 368 | 369 | def get_game_list(self, limit=10): 370 | """Converts the game list to a list of singe line formatted strings 371 | 372 | Args: 373 | limit (int): how many of the games to get, in decreasing order of total playtime 374 | Returns: 375 | a list of strings representing the user's most played games 376 | """ 377 | results = sorted(self.games.values(), key=operator.attrgetter('playtime_forever_int'))[-1:-(limit+1):-1] 378 | pairs = [("", "")] * len(results) 379 | longest_name = 0 380 | for i, result in enumerate(results): 381 | pairs[i] = (result.name, result.single_line_format()) 382 | if len(result.name) > longest_name: 383 | longest_name = len(result.name) 384 | final = [""] * len(results) 385 | longest_name += 3 386 | for i, pair in enumerate(pairs): 387 | final[i] = pair[0] + " " * (longest_name - len(pair[0])) + pair[1] 388 | return final 389 | 390 | 391 | class ItemResult: 392 | """Class containing information about an item on the steam market""" 393 | def __init__(self, soup): 394 | """ 395 | 396 | Args: 397 | soup (BeautifulSoup): the soup of the item's store page 398 | """ 399 | price = soup.find("span", {"class": "market_listing_price_with_publisher_fee_only"}) 400 | self.price = "???" 401 | self.game = "???" 402 | if price is not None: 403 | rawprice = price.get_text().replace("\n", "").replace("\t", "").replace("\r", "") 404 | before = "" 405 | after = "" 406 | while len(rawprice) > 0 and rawprice[0] not in "0123456789.,": 407 | before += rawprice[0] 408 | rawprice = rawprice[1:] 409 | while len(rawprice) > 0 and rawprice[-1] not in "0123456789.,": 410 | after = rawprice[-1] + after 411 | rawprice = rawprice[:-1] 412 | before = before.replace(" ", "") 413 | after = after.replace(" ", "") 414 | 415 | currency = after 416 | if before in CURRENCY_MAP: 417 | currency = CURRENCY_MAP[before] 418 | elif after in CURRENCY_MAP: 419 | currency = CURRENCY_MAP[after] 420 | elif STEAM_PRINTING: 421 | print("no currency matching `" + before + "` or `" + after + "`") 422 | 423 | self.price = rawprice 424 | self.currency = currency 425 | elif STEAM_PRINTING: 426 | print("failed to find price") 427 | 428 | text = str(soup) 429 | self.icon = "???" 430 | iconindex = text.find('"icon_url":') 431 | if iconindex > 0: 432 | iconurl = text[iconindex+len('"icon_url":'):text.find(',', iconindex)].replace(" ", "").replace('"', "") 433 | self.icon = "http://steamcommunity-a.akamaihd.net/economy/image/" + iconurl 434 | elif STEAM_PRINTING: 435 | print("failed to find icon") 436 | 437 | index = text.find("var g_rgAssets") 438 | nindex = text.find("\n", index) 439 | jsontext = text[index:nindex] 440 | while jsontext[0] != "{" and jsontext[0] != "[": 441 | jsontext = jsontext[1:] 442 | while jsontext[-1] != "}" and jsontext[-1] != "]": 443 | jsontext = jsontext[:-1] 444 | 445 | try: 446 | data = json.loads(jsontext) 447 | raw = {} 448 | for k1 in data: 449 | for k2 in data[k1]: 450 | for k3 in data[k1][k2]: 451 | if "tradable" in data[k1][k2][k3] and data[k1][k2][k3]["tradable"] == 1: 452 | raw = data[k1][k2][k3] 453 | break 454 | 455 | self.actions = raw.get("actions", []) 456 | self.name = raw.get("name", "???") 457 | self.gameIcon = raw.get("app_icon", "???") 458 | self.icon = "http://steamcommunity-a.akamaihd.net/economy/image/" + raw.get("icon_url", "???") 459 | self.type = raw.get("type", "???") 460 | self.desc = [BeautifulSoup(x.get("value", ""), "html.parser").get_text() for x in raw.get("descriptions", [])] 461 | except: 462 | self.actions = [] 463 | self.name = "???" 464 | self.gameIcon = "???" 465 | self.icon_url = "???" 466 | self.type = "???" 467 | self.desc = "" 468 | if STEAM_PRINTING: 469 | print("failed to load market data") 470 | 471 | def update_price(self): 472 | """Attempts to convert the price to GBP""" 473 | try: 474 | rawprice = exchange(float(self.price.replace(",", ".")), self.currency, "GBP") 475 | self.price = "£" + str(rawprice) 476 | except: 477 | if STEAM_PRINTING: 478 | print("failed to convert currency (" + self.currency + ")") 479 | 480 | 481 | def get_games(term, timeout=10, limit=-1): 482 | """Search for a game on steam 483 | 484 | Args: 485 | term (str): the game you want to search for 486 | timeout (int, optional): how long aiohttp should wait before raising a timeout error 487 | limit (int, optional): how many results you want to return, 0 or less means every result 488 | Returns: 489 | a list of GameResult objects containing the results 490 | """ 491 | resp = requests.get("http://store.steampowered.com/search/?term=" + parse.quote(term), timeout=timeout) 492 | text = resp.text 493 | soup = BeautifulSoup(text, "html.parser") 494 | 495 | subsoup = soup.findAll("div", {"id": "search_result_container"})[0] 496 | raw_results = subsoup.findAll("a") 497 | results = [] 498 | n = 0 499 | for x in raw_results: 500 | if n >= limit > 0: 501 | break 502 | n += 1 503 | cls = x.get("class") 504 | if cls is not None and "search_result_row" in cls: 505 | results.append(GameResult(x)) 506 | return results 507 | 508 | 509 | def top_sellers(timeout=10, limit=-1): 510 | """gets the top sellers on the front page of the store 511 | 512 | Args: 513 | timeout (int, optional): how long aiohttp should wait before throwing a timeout error 514 | limit (int, optional): how many results it should return, 0 or less returns every result found 515 | Returns: 516 | a list of TopResult objects""" 517 | resp = requests.get("http://store.steampowered.com/", timeout=timeout) 518 | text = resp.text 519 | soup = BeautifulSoup(text, "html.parser") 520 | 521 | subsoup = soup.find("div", {"id": "tab_topsellers_content"}) 522 | raw_results = subsoup.findAll("div", recursive=False) 523 | results = [] 524 | n = 0 525 | for x in raw_results: 526 | if n >= limit > 0: 527 | break 528 | n += 1 529 | cls = x.get("class") 530 | if cls is not None and "tab_item" in cls: 531 | results.append(TopResult(x)) 532 | return results 533 | 534 | 535 | def new_releases(timeout=10, limit=-1): 536 | """gets the new releases on the front page of the store 537 | 538 | Args: 539 | timeout (int, optional): how long aiohttp should wait before throwing a timeout error 540 | limit (int, optional): how many results it should return, 0 or less returns every result found 541 | Returns: 542 | a list of TopResult objects""" 543 | resp = requests.get("http://store.steampowered.com/", timeout=timeout) 544 | text = resp.text 545 | soup = BeautifulSoup(text, "html.parser") 546 | 547 | subsoup = soup.find("div", {"id": "tab_newreleases_content"}) 548 | raw_results = subsoup.findAll("div", recursive=False) 549 | results = [] 550 | n = 0 551 | for x in raw_results: 552 | if n >= limit > 0: 553 | break 554 | n += 1 555 | cls = x.get("class") 556 | if cls is not None and "tab_item" in cls: 557 | results.append(TopResult(x)) 558 | return results 559 | 560 | 561 | def upcoming(timeout=10, limit=-1): 562 | """gets the upcoming games on the front page of the store 563 | 564 | Args: 565 | timeout (int, optional): how long aiohttp should wait before throwing a timeout error 566 | limit (int, optional): how many results it should return, 0 or less returns every result found 567 | Returns: 568 | a list of TopResult objects""" 569 | resp = requests.get("http://store.steampowered.com/", timeout=timeout) 570 | text = resp.text 571 | soup = BeautifulSoup(text, "html.parser") 572 | 573 | subsoup = soup.find("div", {"id": "tab_upcoming_content"}) 574 | raw_results = subsoup.findAll("div", recursive=False) 575 | results = [] 576 | n = 0 577 | for x in raw_results: 578 | if n >= limit > 0: 579 | break 580 | n += 1 581 | cls = x.get("class") 582 | if cls is not None and "tab_item" in cls: 583 | results.append(TopResult(x)) 584 | return results 585 | 586 | 587 | def specials(timeout=10, limit=-1): 588 | """gets the specials on the front page of the store 589 | 590 | Args: 591 | timeout (int, optional): how long aiohttp should wait before throwing a timeout error 592 | limit (int, optional): how many results it should return, 0 or less returns every result found 593 | Returns: 594 | a list of TopResult objects""" 595 | resp = requests.get("http://store.steampowered.com/", timeout=timeout) 596 | text = resp.text 597 | soup = BeautifulSoup(text, "html.parser") 598 | 599 | subsoup = soup.find("div", {"id": "tab_specials_content"}) 600 | raw_results = subsoup.findAll("div", recursive=False) 601 | results = [] 602 | n = 0 603 | for x in raw_results: 604 | if n >= limit > 0: 605 | break 606 | n += 1 607 | cls = x.get("class") 608 | if cls is not None and "tab_item" in cls: 609 | results.append(TopResult(x)) 610 | return results 611 | 612 | 613 | def get_user(steamid, timeout=10): 614 | """Gets some information about a specific steamid 615 | 616 | Args: 617 | steamid (str): The user's steamid 618 | timeout (int, optional): The amount of time before aiohttp raises a timeout error 619 | Returns: 620 | a UserResult object 621 | """ 622 | 623 | if not is_integer(steamid): 624 | steamid = search_for_userid(steamid) 625 | if steamid is not None: 626 | check_key_set() 627 | resp = requests.get("http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key=" + STEAM_KEY + "&steamids=" + steamid, timeout=timeout) 628 | data = resp.json() 629 | 630 | if "response" in data and "players" in data["response"] and len(data["response"]["players"]) > 0: 631 | player = data["response"]["players"][0] 632 | return UserResult(player) 633 | return None 634 | 635 | 636 | def get_user_library(steamid, timeout=10): 637 | """Gets a list of all the games a user owns 638 | 639 | Args: 640 | steamid (str): The user's steamid 641 | timeout (int, optional): The amount of time before aiohttp raises a timeout error 642 | Returns: 643 | a UserLibrary object 644 | """ 645 | if not is_integer(steamid): 646 | steamid = search_for_userid(steamid) 647 | if steamid is not None: 648 | check_key_set() 649 | resp = requests.get("http://api.steampowered.com/IPlayerService/GetOwnedGames/v0001/?key=" + STEAM_KEY + "&steamid=" + steamid + "&format=json&include_appinfo=1&include_played_free_games=1", timeout=timeout) 650 | data = resp.json() 651 | 652 | if "response" in data: 653 | player = data["response"] 654 | return UserLibrary(player) 655 | return None 656 | 657 | 658 | userid_cache = {} # caches search terms to steamids 659 | 660 | 661 | def get_user_id(name, timeout=10): 662 | """Resolves a username to a steamid, however is limited to ONLY vanity URL's. search_user_id is recommended 663 | 664 | Args: 665 | name (str): The name of the user to find the steamid of 666 | timeout (int, optional): The amount of time before aiohttp raises a timeout error 667 | Returns: 668 | either None or a steamid (str) if a vanity url matching that name is found 669 | """ 670 | if name in userid_cache: 671 | return userid_cache[name] 672 | else: 673 | check_key_set() 674 | resp = requests.get("http://api.steampowered.com/ISteamUser/ResolveVanityURL/v0001/?key=" + STEAM_KEY + "&vanityurl=" + parse.quote(name), timeout=timeout) 675 | data = resp.json() 676 | 677 | if "response" in data and "success" in data["response"] and data["response"]["success"] == 1: 678 | steamid = data["response"]["steamid"] 679 | if STEAM_CACHE: 680 | userid_cache[name] = steamid 681 | return steamid 682 | return None 683 | 684 | 685 | def search_for_userid(username, timeout=10): 686 | """Searches for a steamid based on a username, not using vanity URLs 687 | 688 | Args: 689 | username (str): the username of the user you're searching for 690 | timeout (int, optional): the amount of time before aiohttp throws a timeout error 691 | Returns: 692 | A steamid (str) 693 | """ 694 | if username in userid_cache: 695 | return userid_cache[username] 696 | else: 697 | links = search_for_users(username, limit=1, timeout=timeout) 698 | uid = extract_id_from_url(links[0][0], timeout=timeout) 699 | return uid 700 | 701 | 702 | def search_for_users(username, limit=1, timeout=10): 703 | """Searches for basic information about users 704 | 705 | Args: 706 | username (str): the username of the user you're searching for 707 | timeout (int, optional): the amount of time before aiohttp throws a timeout error 708 | limit (int, optional): the amount of user results to return, 0 or less for all of them 709 | Returns: 710 | a list of tuples containing (steam_profile_url (str), steam_user_name (str)) 711 | """ 712 | check_session_set() 713 | resp = requests.get("http://steamcommunity.com/search/SearchCommunityAjax?text=" + parse.quote(username) + "&filter=users&sessionid=" + STEAM_SESSION + "&page=1", headers={"Cookie": "sessionid=" + STEAM_SESSION}, timeout=timeout) 714 | data = resp.json() 715 | soup = BeautifulSoup(data["html"], "html.parser") 716 | stuff = soup.find_all("a", {"class": "searchPersonaName"}) 717 | links = [] 718 | for thing in stuff: 719 | try: 720 | links.append((thing.get("href"), thing.get_text())) 721 | if len(links) >= limit > 0: 722 | return links 723 | except: 724 | pass 725 | return links 726 | 727 | 728 | def extract_id_from_url(url, timeout=10): 729 | """Extracts a steamid from a steam user's profile URL, or finds it based on a vanity URL 730 | 731 | Args: 732 | url (str): The url of the user's profile 733 | timeout (int, optional): The amount of time before aiohttp raises a timeout error 734 | Returns: 735 | the steamid of the user (str) or None if no steamid could be extracted 736 | """ 737 | if url.startswith("http://steamcommunity.com/profiles/"): 738 | return url[len("http://steamcommunity.com/profiles/"):] 739 | elif url.startswith("http://steamcommunity.com/id/"): 740 | vanityname = url[len("http://steamcommunity.com/id/"):] 741 | steamid = get_user_id(vanityname, timeout=timeout) 742 | return steamid 743 | 744 | 745 | def get_item(appid, item_name, timeout=10): 746 | """Gets information about an item from the market 747 | 748 | Args: 749 | appid (str): The appid of the game the item belongs to, or the name if you don't know the ID 750 | item_name (str): The item you're searching for 751 | timeout (int, optional): The amount of time before aiohttp raises a timeout error 752 | Returns: 753 | an ItemResult object 754 | """ 755 | if not is_integer(appid): 756 | appdata = get_app(appid, timeout) 757 | appid = appdata[0] 758 | item_name = get_item_name(item_name, appid, timeout=timeout) 759 | if item_name is not None and appid is not None: 760 | resp = requests.get("http://steamcommunity.com/market/listings/" + appid + "/" + parse.quote(item_name)) 761 | text = resp.text 762 | soup = BeautifulSoup(text, "html.parser") 763 | 764 | result = ItemResult(soup) 765 | result.update_price() 766 | return result 767 | 768 | 769 | gameid_cache = {} # caches search terms to (appid, appname) tuples 770 | 771 | 772 | def get_app(name, timeout=10): 773 | """Gets an appid based off of the app name 774 | 775 | Args: 776 | name (str): the name of the app (game) 777 | timeout (int, optional): the amount of time before aiohttp raises a timeout error 778 | Returns: 779 | A tuple containing (appid (str), apptitle (str)) 780 | """ 781 | if name in gameid_cache: 782 | return gameid_cache[name] 783 | else: 784 | dat = get_games(name, limit=1, timeout=timeout) 785 | if STEAM_CACHE: 786 | gameid_cache[name] = (dat[0].id, dat[0].title) 787 | return dat[0].id, dat[0].title 788 | 789 | 790 | item_name_cache = {} # caches search terms to item url names 791 | 792 | 793 | def get_item_name(name, appid, timeout=10): 794 | """Finds an item's name required for the URL of it's store page 795 | 796 | Args: 797 | name (str): The name of the item you're searching for 798 | appid (str): The appid of the game the item belongs to 799 | timeout (int, optional): The amount of time before aiohttp raises a timeout error 800 | Returns: 801 | the item name (str) or None if no item could be found 802 | """ 803 | cache_name = appid + "::" + name 804 | if cache_name in item_name_cache: 805 | return item_name_cache[cache_name] 806 | else: 807 | if appid != "": 808 | resp = requests.get("http://steamcommunity.com/market/search?appid=" + appid + "&q=" + parse.quote(name), timeout=timeout) 809 | else: 810 | resp = requests.get("http://steamcommunity.com/market/search?q=" + parse.quote(name), timeout=timeout) 811 | text = resp.text 812 | soup = BeautifulSoup(text, "html.parser") 813 | 814 | namesoup = soup.find("span", {"class": "market_listing_item_name"}) 815 | if namesoup is not None: 816 | item_name = namesoup.get_text() 817 | if STEAM_CACHE: 818 | item_name_cache[cache_name] = item_name 819 | return item_name 820 | return None 821 | 822 | 823 | def get_screenshots(username, timeout=10, limit=-1): 824 | """Searches for the most recent (public) screenshots a user has uploaded, 825 | 826 | Args: 827 | username (str): The name of the user you're finding screenshots for 828 | timeout (int, optional): The amount of time before aiohttp raises a timeout error 829 | limit (intm optional): The amount of screenshots to find, 0 or less for all of them 830 | Returns: 831 | a list of URLs (strings) linking to the screenshots 832 | """ 833 | ulinks = search_for_users(username, limit=1) 834 | if len(ulinks) > 0: 835 | resp = requests.get(ulinks[0][0] + "/screenshots/", timeout=timeout) 836 | text = resp.text 837 | soup = BeautifulSoup(text, "html.parser") 838 | 839 | links = [] 840 | screensoups = soup.find_all("a", {"class": "profile_media_item"}) 841 | for ssoup in screensoups: 842 | imgsoup = ssoup.find("img") 843 | if imgsoup is not None: 844 | links.append(imgsoup.get("src")) 845 | if len(links) >= limit > 0: 846 | break 847 | return links 848 | else: 849 | return None 850 | 851 | 852 | def top_game_playercounts(limit=10, timeout=10): 853 | """Gets the top games on steam right now by player count 854 | 855 | Args: 856 | timeout (int, optional): The amount of time before aiohttp raises a timeout error 857 | limit (int, optional): The amount of playercounts to return, 0 or less for all found 858 | Returns: 859 | A list of tuples in the format (current_players (str), peak_players (str), game_name (str), game_link (str)) 860 | """ 861 | resp = requests.get("http://store.steampowered.com/stats", timeout=timeout) 862 | text = resp.text 863 | soup = BeautifulSoup(text, "html.parser") 864 | 865 | stats = [] 866 | ssoups = soup.find_all("tr", {"class": "player_count_row"}) 867 | for subsoup in ssoups: 868 | linksoup = subsoup.find("a", {"class": "gameLink"}) 869 | name = linksoup.get_text() 870 | link = linksoup.get("href") 871 | stuff = subsoup.find_all("span", {"class": "currentServers"}) 872 | if len(stuff) > 0: 873 | current_players = stuff[0].get_text() 874 | peak_players = stuff[1].get_text() 875 | stats.append((current_players, peak_players, name, link)) 876 | if len(stats) >= limit > 0: 877 | break 878 | return stats 879 | 880 | 881 | def steam_user_data(timeout=10): 882 | """Gets information about the amount of users on steam over the past 48 hours 883 | 884 | Args: 885 | timeout (int, optional): The amount of time before aiohttp raises a timeout error 886 | Returns: 887 | A tuple containing (min_users (int), max_users (int), current_users (int))""" 888 | resp = requests.get("http://store.steampowered.com/stats/userdata.json", timeout=timeout) 889 | data = resp.json() 890 | data = data[0]["data"] 891 | 892 | min_users = -1 893 | max_users = -1 894 | for pair in data: 895 | if min_users == -1 or pair[1] < min_users: 896 | min_users = pair[1] 897 | if max_users == -1 or pair[1] > max_users: 898 | max_users = pair[1] 899 | return min_users, max_users, data[-1][1] 900 | 901 | 902 | --------------------------------------------------------------------------------