forked from garrynewman/SteamScreenshotDownloader
-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathProgram.cs
More file actions
262 lines (225 loc) · 9.7 KB
/
Program.cs
File metadata and controls
262 lines (225 loc) · 9.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
// Uncomment this to only download the first 10 files (for debugging its easier to not download everything)
//#define PARTIAL_DOWNLOAD
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
// Original version credit to Garry Newman (https://github.com/garrynewman)
// Current version by Lightning2x (https://github.com/Lightning2X)
namespace SteamDownloader
{
class Program
{
static List<ulong> FILES = new List<ulong>();
static long STEAM_ID;
static string USER_AGENT => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.132 Safari/537.36";
static readonly Dictionary<string, string> MIME_TO_EXTENSION = new Dictionary<string, string>()
{
{ "image/jpeg", ".jpg" },
{ "image/png", ".png" },
{ "image/gif", ".gif" },
{ "image/webp", ".webp" },
};
static int MAX_RETRIES => 2;
static int RETRY_DELAY => 3000;
static int TASK_LIMIT => 16;
static async Task Main(string[] args)
{
Console.WriteLine($"Steam Screenshot Downloader V2.0");
Console.WriteLine();
Console.WriteLine("Please fill in your Steam64 ID. You can find it by googling steam id finder and entering your URL there. Example Steam64 ID: 76561198053864545");
Console.WriteLine("Please paste your Steam ID by right clicking or pressing ctrl V:");
STEAM_ID = ReadSteamId();
Console.WriteLine($"Downloading screenshots for Steam ID {STEAM_ID}...");
await DownloadTab("screenshots");
Console.WriteLine($"Downloading artwork for Steam ID {STEAM_ID}...");
await DownloadTab("images", "artwork");
Console.WriteLine($"All done! You can see the files in \"{Path.GetFullPath($"./{STEAM_ID}")}\"");
Console.WriteLine();
}
static async Task DownloadTab(string tab, string cosmeticName = null)
{
// Make sure the folder exists
string name = cosmeticName ?? tab;
string path = $"./{STEAM_ID}/{name}";
if (!Directory.Exists(path))
Directory.CreateDirectory(path);
// Scan all the user's pages and save the files in them to the FILES variable
await ScanPages(tab);
Console.WriteLine($"Found {FILES.Count} {name}.");
// Remove any duplicates
FILES = FILES.Distinct().ToList();
if (FILES.Count == 0)
{
Console.WriteLine($"Nothing found for tab {name}. Press any key to exit.");
Console.ReadKey();
return;
}
// Download them all
await DownloadImages(path);
FILES.Clear();
}
/// <summary>
/// Reads a SteamId from console and returns it as a long
/// </summary>
/// <returns>The Steam ID</returns>
static long ReadSteamId()
{
var read = Console.ReadLine().Trim();
if (!long.TryParse(read, out long result))
{
Console.Write("That doesn't look like a number to me, please fill in a valid SteamID:");
return ReadSteamId();
}
return result;
}
/// <summary>
/// Scans the user's steam screenshot pages
/// </summary>
/// <returns>A task that performs the page scan</returns>
static async Task ScanPages(string tab)
{
int page = 1;
while (true)
{
Console.WriteLine($"Getting Page {page} ({FILES.Count} screenshots found)");
int attempts = 0;
while (!await GetPage(page, STEAM_ID.ToString(), tab))
{
attempts++;
if (attempts >= MAX_RETRIES)
{
Console.WriteLine($"No results found in 3 attempts. Assuming this is the end and terminating scanning.");
return;
}
await Task.Delay(attempts * 1000);
}
await Task.Delay(100);
page++;
#if (PARTIAL_DOWNLOAD)
if (page >= 1)
{
break;
}
#endif
}
}
/// <summary>
/// Transforms an URL to a Fileid
/// </summary>
/// <param name="pageid">The page id</param>
/// <param name="targetAccount">The target account</param>
/// <returns>a task for performing the transformation</returns>
private static async Task<bool> GetPage(int pageid, string targetAccount, string tab)
{
// Removed the catch for now as its more clear to the user that the program is crashing this way
using (var client = new HttpClient())
{
var response = await client.GetStringAsync($"https://steamcommunity.com/profiles/{targetAccount}/{tab}?p={pageid}&browsefilter=myfiles&view=grid&privacy=30");
var matches = Regex.Matches(response, "steamcommunity\\.com/sharedfiles/filedetails/\\?id\\=([0-9]+?)\"", RegexOptions.Singleline | RegexOptions.IgnoreCase);
if (matches.Count == 0)
return false;
foreach (Match match in matches)
{
FILES.Add(ulong.Parse(match.Groups[1].Value));
}
}
return true;
}
/// <summary>
/// Downloads all files and writes images to disk
/// </summary>
/// <returns>A task for performing the image download</returns>
static async Task DownloadImages(string folder)
{
var tasks = new List<Task>();
#if PARTIAL_DOWNLOAD
// only take first 10 files if we're partially downloading for tests
FILES = FILES.Take(10).ToList();
#endif
foreach (var file in FILES)
{
var t = DownloadImage(file, folder);
tasks.Add(t);
// We hard cap the amount of tasks that can be out at any time at TASK_LIMIT
while (tasks.Count > TASK_LIMIT)
{
await Task.WhenAny(tasks);
tasks.RemoveAll(x => x.IsCompleted);
}
}
await Task.WhenAll(tasks);
}
/// <summary>
/// Downloads a file to disk
/// </summary>
/// <param name="file">The file</param>
/// <param name="retries">The amount of retries for this file</param>
/// <returns>The task performing the image download</returns>
private static async Task<bool> DownloadImage(ulong file, string folder, int retries = 0)
{
if (retries >= MAX_RETRIES)
{
return false;
}
try
{
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("user-agent", USER_AGENT);
var response = await client.GetStringAsync($"https://steamcommunity.com/sharedfiles/filedetails/?id={file}");
var matches = Regex.Matches(response, "\\<a href\\=\"(https\\://images.steamusercontent.com/ugc/([A-Z0-9/].+?))\"", RegexOptions.Singleline | RegexOptions.IgnoreCase);
if (matches.Count == 0)
{
Console.WriteLine($"ERROR - Couldn't find image link for file id: [{file}]");
return false;
}
var imageUrl = matches.First().Groups[1].Value;
Console.WriteLine($"[{file}] - downloading {imageUrl}");
var download = await client.GetAsync(imageUrl);
var fileId = GetStringBetween(imageUrl, "ugc/", "/");
var extension = GetFileExtension(download.Content.Headers.GetValues("Content-Type").First());
var data = await download.Content.ReadAsByteArrayAsync();
File.WriteAllBytes($"{folder}/{fileId}{extension}", data);
return true;
}
catch (System.Exception e)
{
Console.Error.WriteLine(e.Message);
await Task.Delay(RETRY_DELAY);
return await DownloadImage(file, folder, retries + 1);
}
}
/// <summary>
/// Gets the string between a startWord and endWord
/// </summary>
/// <param name="s">The string</param>
/// <param name="startWord">The start word to look for</param>
/// <param name="endWord">The end word to look for</param>
/// <returns>The augmented string</returns>
private static string GetStringBetween(string s, string startWord, string endWord)
{
int start = s.IndexOf(startWord) + startWord.Length;
int end = s.IndexOf(endWord, start);
return s[start..end];
}
/// <summary>
/// Gets the file extension based on mimeType
/// </summary>
/// <param name="mimeType">The mimeType</param>
/// <returns>The file extension</returns>
/// <exception cref="ArgumentException">When the mime Type is not in the dictionary</exception>
private static string GetFileExtension(string mimeType)
{
MIME_TO_EXTENSION.TryGetValue(mimeType, out string extension);
if (extension == null)
{
throw new ArgumentException($"Mimetype: {mimeType} not found in the dictionary!");
}
return extension;
}
}
}