├── 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 | [](https://discord.gg/JSGpedK)
22 | [](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 |
--------------------------------------------------------------------------------