scmlewis commited on
Commit
46966d1
·
verified ·
1 Parent(s): 232ae58

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +75 -340
app.py CHANGED
@@ -1,22 +1,17 @@
1
  import streamlit as st
2
  import requests
3
  from time import sleep
4
- import datetime
5
- from typing import List, Dict, Optional
6
 
7
- # Constants
8
  STEAM_STORE_URL = "https://store.steampowered.com/app/{}"
9
  STEAM_CDN_COVER = "https://cdn.cloudflare.steamstatic.com/steam/apps/{}/capsule_184x69.jpg"
10
  FALLBACK_COVER = "https://cdn.cloudflare.steamstatic.com/steam/store_capsule.jpg"
11
 
12
- # Page config
13
- st.set_page_config(
14
- page_title="Steam Top 20 Dashboard",
15
- layout="wide",
16
- initial_sidebar_state="collapsed"
17
- )
18
 
19
- # Enhanced CSS with view toggle and auto-refresh
 
 
 
20
  st.markdown("""
21
  <style>
22
  html, body, .main, .block-container {
@@ -24,11 +19,6 @@ st.markdown("""
24
  background: linear-gradient(135deg, #141e30 0%, #243b55 100%) !important;
25
  color: #e2eeff !important;
26
  }
27
-
28
- .stApp > header {
29
- background-color: transparent;
30
- }
31
-
32
  .modern-header {
33
  margin-top: 26px;
34
  font-size: 2.4rem;
@@ -39,7 +29,6 @@ st.markdown("""
39
  letter-spacing: 1px;
40
  text-align: center;
41
  }
42
-
43
  .modern-subtitle {
44
  color: #b5e3f1;
45
  font-size: 1.05rem;
@@ -47,192 +36,48 @@ st.markdown("""
47
  margin-bottom: 21px;
48
  text-align: center;
49
  }
50
-
51
- .controls-container {
52
- background: rgba(35, 47, 66, 0.6);
53
- padding: 15px 20px;
54
- border-radius: 10px;
55
- border: 1px solid #364f6b;
56
- margin-bottom: 20px;
57
- }
58
-
59
- .status-indicator {
60
- display: inline-flex;
61
- align-items: center;
62
- gap: 8px;
63
- color: #b5e3f1;
64
- font-size: 14px;
65
- padding: 8px 12px;
66
- background: rgba(54, 79, 107, 0.4);
67
- border-radius: 6px;
68
- }
69
-
70
- .update-dot {
71
- width: 8px;
72
- height: 8px;
73
- background: #4ade80;
74
- border-radius: 50%;
75
- animation: pulse 2s ease-in-out infinite;
76
- }
77
-
78
- @keyframes pulse {
79
- 0%, 100% { opacity: 1; }
80
- 50% { opacity: 0.4; }
81
- }
82
-
83
- .game-card-grid {
84
- background: linear-gradient(135deg, #232f42 70%, #2e3f54 100%);
85
- border: 1px solid #364f6b;
86
- border-radius: 10px;
87
- padding: 15px;
88
- margin-bottom: 15px;
89
- text-align: center;
90
- transition: all 0.3s ease;
91
- min-height: 280px;
92
- display: flex;
93
- flex-direction: column;
94
- align-items: center;
95
- justify-content: center;
96
- }
97
-
98
- .game-card-grid:hover {
99
- transform: translateY(-3px);
100
- border-color: #48c6ef;
101
- box-shadow: 0 5px 20px rgba(72, 198, 239, 0.3);
102
- }
103
-
104
- .game-card-grid img {
105
- border-radius: 7px;
106
- box-shadow: 0 3px 10px rgba(0,0,0,0.4);
107
- margin-bottom: 12px;
108
- max-width: 150px;
109
- width: 100%;
110
- }
111
-
112
- .game-card-list {
113
- background: linear-gradient(135deg, #232f42 70%, #2e3f54 100%);
114
- border: 1px solid #364f6b;
115
- border-radius: 10px;
116
- padding: 15px;
117
- margin-bottom: 10px;
118
- transition: all 0.3s ease;
119
- display: flex;
120
- align-items: center;
121
- gap: 15px;
122
- }
123
-
124
- .game-card-list:hover {
125
- transform: translateX(5px);
126
- border-color: #48c6ef;
127
- box-shadow: 0 3px 15px rgba(72, 198, 239, 0.2);
128
- }
129
-
130
- .game-card-list img {
131
- border-radius: 7px;
132
- box-shadow: 0 3px 10px rgba(0,0,0,0.4);
133
- max-width: 120px;
134
- width: 100%;
135
- }
136
-
137
- .game-title {
138
- color: #69e2ff;
139
- font-weight: 500;
140
- font-size: 15px;
141
- margin-bottom: 8px;
142
- line-height: 1.3;
143
- }
144
-
145
- .game-detail {
146
- font-size: 12px;
147
- color: #d7e5f7;
148
- margin-bottom: 3px;
149
- }
150
-
151
- .sale-badge {
152
- color: #7bffc9;
153
- font-weight: 500;
154
- font-size: 13px;
155
- }
156
-
157
- .metacritic {
158
- color: #ffe761;
159
- font-size: 13px;
160
- font-weight: 600;
161
- }
162
-
163
- .current-players {
164
- color: #a1eafc;
165
- font-size: 12.5px;
166
- font-weight: 600;
167
- }
168
-
169
- .list-cover {
170
- flex-shrink: 0;
171
- }
172
-
173
- .list-info {
174
- flex: 1;
175
- display: grid;
176
- grid-template-columns: 2fr 1fr 1fr 1fr;
177
- gap: 15px;
178
- align-items: center;
179
- }
180
-
181
- .list-info a {
182
- text-decoration: none;
183
- color: inherit;
184
- }
185
-
186
- @media (max-width: 768px) {
187
- .list-info {
188
- grid-template-columns: 1fr;
189
- gap: 8px;
190
- }
191
- }
192
  </style>
193
  """, unsafe_allow_html=True)
194
 
195
- @st.cache_data(ttl=300)
196
- def get_top_games(limit: int = 20) -> List[int]:
197
- """Fetch top games from Steam API"""
198
- try:
199
- url = "https://api.steampowered.com/ISteamChartsService/GetMostPlayedGames/v1/"
200
- response = requests.get(url, timeout=10)
201
- response.raise_for_status()
202
- data = response.json()
203
- ranks = data["response"]["ranks"][:limit]
204
- return [g["appid"] for g in ranks]
205
- except Exception as e:
206
- st.error(f"Error fetching top games: {e}")
207
- return []
208
 
209
- def resolve_cover_url(appid: int) -> str:
210
- """Check if cover image exists, return fallback if not"""
 
 
 
 
 
 
 
 
 
 
 
 
 
211
  test_url = f"https://cdn.cloudflare.steamstatic.com/steam/apps/{appid}/capsule_184x69.jpg"
 
212
  try:
213
- resp = requests.head(test_url, timeout=3)
214
- if resp.status_code == 200:
215
  return test_url
216
  except Exception:
217
  pass
218
- return FALLBACK_COVER
 
219
 
220
- @st.cache_data(ttl=300)
221
- def get_game_details(appid: int) -> Optional[Dict]:
222
- """Fetch detailed game information"""
223
  url = f"https://store.steampowered.com/api/appdetails?appids={appid}&cc=us&l=en"
224
  try:
225
- r = requests.get(url, timeout=10)
226
  r.raise_for_status()
227
  item = r.json().get(str(appid), {})
228
  if not item.get("success"):
229
  return None
230
-
231
  info = item["data"]
232
  price = info.get("price_overview", {})
233
  metacritic = info.get("metacritic", {})
234
  cover_url = resolve_cover_url(appid)
235
-
236
  return {
237
  "appid": appid,
238
  "name": info.get("name", "Unknown"),
@@ -244,183 +89,73 @@ def get_game_details(appid: int) -> Optional[Dict]:
244
  "box_art": cover_url,
245
  "steam_link": STEAM_STORE_URL.format(appid),
246
  }
247
- except Exception as e:
248
  return None
249
 
250
- @st.cache_data(ttl=180)
251
- def get_current_player_count(appid: int) -> int:
252
- """Fetch current player count"""
253
  url = f"https://api.steampowered.com/ISteamUserStats/GetNumberOfCurrentPlayers/v1/?appid={appid}"
254
  try:
255
- r = requests.get(url, timeout=5)
256
  data = r.json()
257
  return data.get("response", {}).get("player_count", 0)
258
  except Exception:
259
  return 0
260
 
261
- def format_card_grid(game: Dict) -> str:
262
- """Format game card for grid view"""
263
- discount_html = ""
264
- if game["on_sale"]:
265
- discount_html = f'<div class="sale-badge">-{game["discount"]}% OFF</div>'
266
 
267
- metacritic_text = f"Metacritic: {game['metacritic']}" if game["metacritic"] else "Metacritic: N/A"
268
-
269
- return f'''
270
- <a href="{game["steam_link"]}" target="_blank" style="text-decoration:none; color:inherit;">
271
- <div class="game-card-grid">
272
- <img src="{game["box_art"]}" alt="{game["name"]}" />
273
- <div class="game-title">{game["name"]}</div>
274
- <div class="game-detail">Release: {game["release_date"]}</div>
275
- {discount_html}
276
- <div class="game-detail">Price: {game["price"]}</div>
277
- <div class="metacritic">{metacritic_text}</div>
278
- <div class="current-players">Current Players: <b>{game["current_players"]:,}</b></div>
279
- </div>
280
- </a>
281
- '''
282
-
283
- def format_card_list(game: Dict) -> str:
284
- """Format game card for list view"""
285
  discount_html = ""
 
286
  if game["on_sale"]:
287
- discount_html = f'<div class="sale-badge">-{game["discount"]}% OFF</div>'
 
288
 
289
- metacritic_text = f"MC: {game['metacritic']}" if game["metacritic"] else "MC: N/A"
290
 
 
291
  return f'''
292
- <div class="game-card-list">
293
- <div class="list-cover">
294
- <img src="{game["box_art"]}" alt="{game["name"]}" />
295
- </div>
296
- <div class="list-info">
297
- <div>
298
- <a href="{game["steam_link"]}" target="_blank">
299
- <div class="game-title">{game["name"]}</div>
300
- </a>
301
- <div class="game-detail">Release: {game["release_date"]}</div>
302
- </div>
303
- <div>
304
- {discount_html}
305
- <div class="game-detail">Price: {game["price"]}</div>
306
- </div>
307
- <div>
308
- <div class="metacritic">{metacritic_text}</div>
309
- </div>
310
- <div>
311
- <div class="current-players"><b>{game["current_players"]:,}</b> players</div>
312
  </div>
313
- </div>
314
  </div>
315
  '''
316
 
 
317
  def main():
318
- # Header
319
- st.markdown('<div class="modern-header">Steam Top 20: Live Trending Games</div>', unsafe_allow_html=True)
320
- st.markdown('<div class="modern-subtitle">Discover what the world is playing right now.<br>This dashboard shows live player counts, deals, and essential info—always fresh from official Steam data.</div>', unsafe_allow_html=True)
321
-
322
- # Initialize session state
323
- if 'view_mode' not in st.session_state:
324
- st.session_state.view_mode = 'grid'
325
- if 'auto_refresh' not in st.session_state:
326
- st.session_state.auto_refresh = False
327
- if 'last_update' not in st.session_state:
328
- st.session_state.last_update = None
329
-
330
- # Create three columns for controls
331
- col1, col2, col3 = st.columns([2, 3, 2])
332
-
333
- with col1:
334
- st.write("**View Mode:**")
335
- view_mode = st.radio(
336
- "View",
337
- options=['grid', 'list'],
338
- index=0 if st.session_state.view_mode == 'grid' else 1,
339
- horizontal=True,
340
- label_visibility='collapsed'
341
- )
342
- st.session_state.view_mode = view_mode
343
-
344
- with col2:
345
- # Last updated status
346
- if st.session_state.last_update:
347
- time_diff = datetime.datetime.now() - st.session_state.last_update
348
- if time_diff.total_seconds() < 60:
349
- time_text = "Just now"
350
- elif time_diff.total_seconds() < 3600:
351
- mins = int(time_diff.total_seconds() // 60)
352
- time_text = f"{mins} minute{'s' if mins > 1 else ''} ago"
353
- else:
354
- hours = int(time_diff.total_seconds() // 3600)
355
- time_text = f"{hours} hour{'s' if hours > 1 else ''} ago"
356
- else:
357
- time_text = "Loading..."
358
-
359
- st.markdown(f'''
360
- <div class="status-indicator">
361
- <div class="update-dot"></div>
362
- Last updated: {time_text}
363
- </div>
364
- ''', unsafe_allow_html=True)
365
-
366
- with col3:
367
- st.write("**Auto-refresh (5min):**")
368
- auto_refresh = st.checkbox(
369
- "Enable auto-refresh",
370
- value=st.session_state.auto_refresh,
371
- label_visibility='collapsed'
372
- )
373
- st.session_state.auto_refresh = auto_refresh
374
-
375
- if st.button("🔄 Refresh Now", use_container_width=True):
376
- st.cache_data.clear()
377
- st.rerun()
378
-
379
- st.markdown("---")
380
-
381
- # Auto-refresh logic
382
- if st.session_state.auto_refresh:
383
- st.info("⏱️ Auto-refresh enabled - Page will update every 5 minutes")
384
-
385
- # Fetch and display games
386
- with st.spinner("Fetching top games..."):
387
- appids = get_top_games(20)
388
-
389
- if not appids:
390
- st.error("Failed to fetch games. Please try again later.")
391
- return
392
-
393
- games = []
394
- progress_bar = st.progress(0, text="Loading game details...")
395
-
396
- for idx, appid in enumerate(appids):
397
- details = get_game_details(appid)
398
- if details:
399
- details["current_players"] = get_current_player_count(appid)
400
- games.append(details)
401
-
402
- progress_bar.progress(
403
- (idx + 1) / len(appids),
404
- text=f"Loading game details... ({idx + 1}/{len(appids)})"
405
- )
406
- sleep(0.1)
407
-
408
- progress_bar.empty()
409
- st.session_state.last_update = datetime.datetime.now()
410
-
411
- # Display games based on view mode
412
- if st.session_state.view_mode == 'grid':
413
- cols = st.columns(4)
414
- for idx, game in enumerate(games):
415
- with cols[idx % 4]:
416
- st.markdown(format_card_grid(game), unsafe_allow_html=True)
417
- else:
418
- for game in games:
419
- st.markdown(format_card_list(game), unsafe_allow_html=True)
420
-
421
- # Footer info
422
- st.markdown("---")
423
- st.markdown(f"**Total games loaded:** {len(games)} | **Data source:** Steam Public APIs")
424
 
425
  if __name__ == "__main__":
426
- main()
 
1
  import streamlit as st
2
  import requests
3
  from time import sleep
 
 
4
 
5
+
6
  STEAM_STORE_URL = "https://store.steampowered.com/app/{}"
7
  STEAM_CDN_COVER = "https://cdn.cloudflare.steamstatic.com/steam/apps/{}/capsule_184x69.jpg"
8
  FALLBACK_COVER = "https://cdn.cloudflare.steamstatic.com/steam/store_capsule.jpg"
9
 
 
 
 
 
 
 
10
 
11
+ st.set_page_config(layout="wide")
12
+
13
+
14
+ # Modern CSS for small cards, centered layout, left-aligned text block
15
  st.markdown("""
16
  <style>
17
  html, body, .main, .block-container {
 
19
  background: linear-gradient(135deg, #141e30 0%, #243b55 100%) !important;
20
  color: #e2eeff !important;
21
  }
 
 
 
 
 
22
  .modern-header {
23
  margin-top: 26px;
24
  font-size: 2.4rem;
 
29
  letter-spacing: 1px;
30
  text-align: center;
31
  }
 
32
  .modern-subtitle {
33
  color: #b5e3f1;
34
  font-size: 1.05rem;
 
36
  margin-bottom: 21px;
37
  text-align: center;
38
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  </style>
40
  """, unsafe_allow_html=True)
41
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
 
43
+ st.markdown('<div class="modern-header">Steam Top 20: Live Trending Games</div>', unsafe_allow_html=True)
44
+ st.markdown('<div class="modern-subtitle">Discover what the world is playing right now.<br>This dashboard shows live player counts, deals, and essential info—always fresh from official Steam data.</div>', unsafe_allow_html=True)
45
+
46
+
47
+ @st.cache_data
48
+ def get_top_games(limit=20):
49
+ url = "https://api.steampowered.com/ISteamChartsService/GetMostPlayedGames/v1/"
50
+ response = requests.get(url)
51
+ response.raise_for_status()
52
+ data = response.json()
53
+ ranks = data["response"]["ranks"][:limit]
54
+ return [g["appid"] for g in ranks]
55
+
56
+
57
+ def resolve_cover_url(appid):
58
  test_url = f"https://cdn.cloudflare.steamstatic.com/steam/apps/{appid}/capsule_184x69.jpg"
59
+ fallback_url = FALLBACK_COVER
60
  try:
61
+ resp = requests.get(test_url, timeout=2.5)
62
+ if resp.status_code == 200 and (resp.content and not resp.url.endswith("trans.gif")):
63
  return test_url
64
  except Exception:
65
  pass
66
+ return fallback_url
67
+
68
 
69
+ def get_game_details(appid):
 
 
70
  url = f"https://store.steampowered.com/api/appdetails?appids={appid}&cc=us&l=en"
71
  try:
72
+ r = requests.get(url)
73
  r.raise_for_status()
74
  item = r.json().get(str(appid), {})
75
  if not item.get("success"):
76
  return None
 
77
  info = item["data"]
78
  price = info.get("price_overview", {})
79
  metacritic = info.get("metacritic", {})
80
  cover_url = resolve_cover_url(appid)
 
81
  return {
82
  "appid": appid,
83
  "name": info.get("name", "Unknown"),
 
89
  "box_art": cover_url,
90
  "steam_link": STEAM_STORE_URL.format(appid),
91
  }
92
+ except Exception:
93
  return None
94
 
95
+
96
+ def get_current_player_count(appid):
 
97
  url = f"https://api.steampowered.com/ISteamUserStats/GetNumberOfCurrentPlayers/v1/?appid={appid}"
98
  try:
99
+ r = requests.get(url)
100
  data = r.json()
101
  return data.get("response", {}).get("player_count", 0)
102
  except Exception:
103
  return 0
104
 
 
 
 
 
 
105
 
106
+ def format_card(game):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  discount_html = ""
108
+ price_html = f'<div style="font-size:13px;">Price: {game["price"]}</div>'
109
  if game["on_sale"]:
110
+ discount_html = f'<div style="color:#7bffc9; font-weight:500; margin-bottom:2px; font-size:13px;">-{game["discount"]}% OFF</div>'
111
+ price_html = f'{discount_html}{price_html}'
112
 
 
113
 
114
+ # Center image and text block, but text within that block is always left-aligned
115
  return f'''
116
+ <div style="
117
+ background: linear-gradient(135deg, #232f42 70%, #2e3f54 100%);
118
+ border:1px solid #364f6b;
119
+ border-radius:10px;
120
+ padding:10px 10px 13px 10px;
121
+ margin-bottom:13px;
122
+ min-height:238px;
123
+ display:flex;
124
+ flex-direction:column;
125
+ align-items:center;
126
+ justify-content:center;">
127
+ <a href="{game["steam_link"]}" target="_blank" style="text-decoration:none;">
128
+ <img src="{game["box_art"]}" width="130" style="border-radius:7px; box-shadow:0 3px 10px -4px #000; margin:0 auto 8px auto; display:block;" />
129
+ <div style="width:165px; max-width:90vw; display:flex; flex-direction:column; align-items:flex-start; justify-content:flex-start;">
130
+ <h3 style="margin:7px 0 3px 0; color:#69e2ff; font-family: 'Segoe UI', 'Roboto', sans-serif; font-weight:500; font-size:15px; line-height:1.13; text-align:left;">{game["name"]}</h3>
131
+ <div style="font-size:12px; color:#d7e5f7; margin-bottom:2px; text-align:left;">Release: {game["release_date"]}</div>
132
+ {price_html}
133
+ <div style="color:#ffe761; font-size:13px; font-weight:600; margin-bottom:2px; text-align:left;">{"Metacritic: "+str(game["metacritic"]) if game["metacritic"] else "Metacritic: N/A"}</div>
134
+ <div style="margin-top:2px; font-size:12.5px; color:#a1eafc; text-align:left;">Current Players: <b>{game["current_players"]:,}</b></div>
 
135
  </div>
136
+ </a>
137
  </div>
138
  '''
139
 
140
+
141
  def main():
142
+ appids = get_top_games(20)
143
+ games = []
144
+ progress_bar = st.progress(0)
145
+ for idx, aid in enumerate(appids):
146
+ details = get_game_details(aid)
147
+ if details:
148
+ details["current_players"] = get_current_player_count(aid)
149
+ games.append(details)
150
+ progress_bar.progress((idx + 1) / len(appids))
151
+ sleep(0.1)
152
+
153
+
154
+ columns = st.columns(4)
155
+ for idx, game in enumerate(games):
156
+ with columns[idx % 4]:
157
+ st.markdown(format_card(game), unsafe_allow_html=True)
158
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
 
160
  if __name__ == "__main__":
161
+ main()