diff options
Diffstat (limited to 'src/main/java/zajc/gogarchiver/api/User.java')
-rw-r--r-- | src/main/java/zajc/gogarchiver/api/User.java | 168 |
1 files changed, 168 insertions, 0 deletions
diff --git a/src/main/java/zajc/gogarchiver/api/User.java b/src/main/java/zajc/gogarchiver/api/User.java new file mode 100644 index 0000000..114cb26 --- /dev/null +++ b/src/main/java/zajc/gogarchiver/api/User.java | |||
@@ -0,0 +1,168 @@ | |||
1 | //SPDX-License-Identifier: GPL-3.0 | ||
2 | /* | ||
3 | * gogarchiver-ng, an archival tool for GOG.com | ||
4 | * Copyright (C) 2024 Marko Zajc | ||
5 | * | ||
6 | * This program is free software: you can redistribute it and/or modify it under the | ||
7 | * terms of the GNU General Public License as published by the Free Software | ||
8 | * Foundation, version 3. | ||
9 | * | ||
10 | * This program is distributed in the hope that it will be useful, but WITHOUT ANY | ||
11 | * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A | ||
12 | * PARTICULAR PURPOSE. See the GNU General Public License for more details. | ||
13 | * | ||
14 | * You should have received a copy of the GNU General Public License along with this | ||
15 | * program. If not, see <https://www.gnu.org/licenses/>. | ||
16 | */ | ||
17 | package zajc.gogarchiver.api; | ||
18 | |||
19 | import static java.io.File.separatorChar; | ||
20 | import static java.nio.file.Files.createDirectories; | ||
21 | import static java.util.stream.Collectors.toUnmodifiableSet; | ||
22 | import static zajc.gogarchiver.api.GameDownload.Platform.LINUX; | ||
23 | import static zajc.gogarchiver.util.Utilities.*; | ||
24 | |||
25 | import java.io.*; | ||
26 | import java.nio.file.Path; | ||
27 | import java.util.*; | ||
28 | import java.util.concurrent.ConcurrentHashMap; | ||
29 | |||
30 | import javax.annotation.*; | ||
31 | |||
32 | import kong.unirest.*; | ||
33 | import kong.unirest.json.JSONObject; | ||
34 | import me.tongfei.progressbar.ProgressBar; | ||
35 | import zajc.gogarchiver.exception.NotLoggedInException; | ||
36 | import zajc.gogarchiver.util.LazyValue; | ||
37 | |||
38 | public class User { | ||
39 | |||
40 | private static final UnirestInstance UNIREST_NO_REDIRECT = new UnirestInstance(new Config().followRedirects(false)); | ||
41 | |||
42 | private static final String URL_USER = "https://www.gog.com/userData.json"; | ||
43 | private static final String URL_LIBRARY = "https://menu.gog.com/v1/account/licences"; | ||
44 | private static final String URL_GAME_DETAILS = "https://www.gog.com/account/gameDetails/%s.json"; | ||
45 | |||
46 | @Nonnull private final String token; | ||
47 | @Nonnull private final LazyValue<Set<String>> libraryIds = new LazyValue<>(); | ||
48 | @Nonnull private final LazyValue<JSONObject> userData = new LazyValue<>(); | ||
49 | @Nonnull private final Map<String, Game> games = new ConcurrentHashMap<>(); | ||
50 | |||
51 | public User(@Nonnull String token) throws NotLoggedInException { | ||
52 | this.token = token; | ||
53 | |||
54 | if (!isLoggedIn()) | ||
55 | throw new NotLoggedInException(); | ||
56 | } | ||
57 | |||
58 | private boolean isLoggedIn() { | ||
59 | return getUserData().getBoolean("isLoggedIn"); | ||
60 | } | ||
61 | |||
62 | @Nonnull | ||
63 | @SuppressWarnings("null") | ||
64 | public String getUsername() { | ||
65 | return getUserData().getString("username"); | ||
66 | } | ||
67 | |||
68 | @Nonnull | ||
69 | @SuppressWarnings("null") | ||
70 | private JSONObject getUserData() { | ||
71 | return this.userData.get(() -> getJson(URL_USER).getObject()); | ||
72 | } | ||
73 | |||
74 | @Nonnull | ||
75 | @SuppressWarnings("null") | ||
76 | public Set<String> getLibraryIds() { | ||
77 | return this.libraryIds | ||
78 | .get(() -> stream(getJson(URL_LIBRARY).getArray()).map(Object::toString).collect(toUnmodifiableSet())); | ||
79 | } | ||
80 | |||
81 | @Nullable | ||
82 | public Game resolveGame(@Nonnull String id) { | ||
83 | return this.games.computeIfAbsent(id, this::resolveGameDirectly); | ||
84 | } | ||
85 | |||
86 | @Nullable | ||
87 | @SuppressWarnings("null") | ||
88 | public Game resolveGameDirectly(@Nonnull String id) { | ||
89 | if (!getLibraryIds().contains(id)) { | ||
90 | warn("User @|bold %s|@ does not own game @|bold %s|@.", getUsername(), id); | ||
91 | return null; | ||
92 | } | ||
93 | |||
94 | var json = getJson(URL_GAME_DETAILS.formatted(id)); | ||
95 | if (json.isArray() && json.getArray().isEmpty()) // is a dlc | ||
96 | return null; | ||
97 | else | ||
98 | return Game.fromJson(this, json.getObject(), id); | ||
99 | } | ||
100 | |||
101 | @Nonnull | ||
102 | @SuppressWarnings("null") | ||
103 | public JsonNode getJson(@Nonnull String url) { | ||
104 | return checkResponse(url, get(url).asJson()).getBody(); | ||
105 | } | ||
106 | |||
107 | @SuppressWarnings("resource") | ||
108 | public void downloadTo(@Nonnull GameDownload download, @Nonnull Path output, | ||
109 | @Nullable ProgressBar monitor) throws IOException { | ||
110 | var parent = output.getParent(); | ||
111 | if (parent != null) | ||
112 | createDirectories(parent); | ||
113 | |||
114 | var temp = new File((parent == null ? "" : parent.toString() + separatorChar) + '.' + | ||
115 | output.getFileName().toString() + | ||
116 | ".part"); | ||
117 | temp.deleteOnExit(); // NOSONAR it's good enough | ||
118 | |||
119 | var req = get(download.url()); | ||
120 | if (monitor != null) { | ||
121 | req.downloadMonitor((_1, _2, downloaded, total) -> { | ||
122 | if (monitor.getMax() == 1) | ||
123 | monitor.maxHint(total); | ||
124 | monitor.stepTo(downloaded); | ||
125 | }); | ||
126 | } | ||
127 | |||
128 | var outputFile = checkResponse(download.originalUrl(), req.asFile(temp.getPath())).getBody(); | ||
129 | if (download.platform() == LINUX) | ||
130 | outputFile.setExecutable(true, false); // NOSONAR doesn't matter much | ||
131 | |||
132 | if (!outputFile.renameTo(output.toFile())) | ||
133 | throw new IOException("Couldn't rename the part file"); | ||
134 | } | ||
135 | |||
136 | @Nonnull | ||
137 | @SuppressWarnings("null") | ||
138 | public String resolveUrl(@Nonnull GameDownload download) { | ||
139 | var location = download.originalUrl(); | ||
140 | try (var unirest = Unirest.spawnInstance()) { | ||
141 | unirest.config().followRedirects(false); | ||
142 | |||
143 | for (int i = 0; i < 10; i++) { | ||
144 | var newLocation = UNIREST_NO_REDIRECT.get(location) | ||
145 | .cookie("gog-al", this.token) | ||
146 | .asEmpty() | ||
147 | .getHeaders() | ||
148 | .all() | ||
149 | .stream() | ||
150 | .filter(h -> h.getName().equalsIgnoreCase("location")) | ||
151 | .findFirst() | ||
152 | .map(Header::getValue); | ||
153 | |||
154 | if (newLocation.isPresent()) | ||
155 | location = newLocation.get(); | ||
156 | else | ||
157 | return location; | ||
158 | } | ||
159 | } | ||
160 | |||
161 | throw new RuntimeException("Encountered a redirect loop on " + download.originalUrl()); | ||
162 | } | ||
163 | |||
164 | public GetRequest get(@Nonnull String url) { | ||
165 | return Unirest.get(url).cookie("gog-al", this.token); | ||
166 | } | ||
167 | |||
168 | } | ||