diff options
Diffstat (limited to 'src/main/java/zajc/gogarchiver/Main.java')
-rw-r--r-- | src/main/java/zajc/gogarchiver/Main.java | 246 |
1 files changed, 246 insertions, 0 deletions
diff --git a/src/main/java/zajc/gogarchiver/Main.java b/src/main/java/zajc/gogarchiver/Main.java new file mode 100644 index 0000000..3cba37f --- /dev/null +++ b/src/main/java/zajc/gogarchiver/Main.java | |||
@@ -0,0 +1,246 @@ | |||
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; | ||
18 | |||
19 | import static java.lang.Long.MAX_VALUE; | ||
20 | import static java.lang.Math.max; | ||
21 | import static java.lang.System.*; | ||
22 | import static java.nio.file.Files.createDirectories; | ||
23 | import static java.util.Objects.requireNonNullElse; | ||
24 | import static java.util.concurrent.Executors.newFixedThreadPool; | ||
25 | import static java.util.concurrent.TimeUnit.NANOSECONDS; | ||
26 | import static java.util.stream.Collectors.toUnmodifiableSet; | ||
27 | import static java.util.stream.Stream.concat; | ||
28 | import static me.tongfei.progressbar.ProgressBarStyle.*; | ||
29 | import static picocli.CommandLine.Help.Ansi.OFF; | ||
30 | import static zajc.gogarchiver.util.Utilities.*; | ||
31 | |||
32 | import java.io.IOException; | ||
33 | import java.util.*; | ||
34 | import java.util.concurrent.*; | ||
35 | import java.util.stream.Stream; | ||
36 | |||
37 | import javax.annotation.*; | ||
38 | |||
39 | import org.eu.zajc.ef.runnable.except.all.AERunnable; | ||
40 | |||
41 | import me.tongfei.progressbar.*; | ||
42 | import picocli.CommandLine; | ||
43 | import picocli.CommandLine.*; | ||
44 | import sun.misc.Signal; // NOSONAR it's just quality of life | ||
45 | import zajc.gogarchiver.api.*; | ||
46 | import zajc.gogarchiver.exception.NotLoggedInException; | ||
47 | |||
48 | @Command(name = "gogarchiver", description = "an archival tool for GOG.com", version = "gogarchiver 1.0", | ||
49 | mixinStandardHelpOptions = true, sortSynopsis = false, sortOptions = false) | ||
50 | public class Main implements Callable<Integer> { | ||
51 | |||
52 | @Mixin private Arguments arguments; | ||
53 | |||
54 | private void run() throws Exception { | ||
55 | createDirectories(this.arguments.getOutputPath()); | ||
56 | var downloads = getDownloadList(); | ||
57 | if (downloads.isEmpty()) { | ||
58 | if (!this.arguments.isQuiet()) | ||
59 | out.println("\u001b[2KNothing to do"); | ||
60 | |||
61 | } else { | ||
62 | executeDownloads(downloads); | ||
63 | |||
64 | if (!this.arguments.isQuiet()) | ||
65 | out.println("Done"); | ||
66 | } | ||
67 | } | ||
68 | |||
69 | @SuppressWarnings({ "resource", "null" }) | ||
70 | private void executeDownloads(@Nonnull List<GameDownload> downloads) throws InterruptedException { | ||
71 | var progressTitleWidth = | ||
72 | downloads.stream().map(GameDownload::getProgressTitle).mapToInt(String::length).max().orElse(-1); | ||
73 | |||
74 | Map<GameDownload, ProgressBar> progressBars; | ||
75 | if (this.arguments.isQuiet()) { | ||
76 | progressBars = null; | ||
77 | |||
78 | } else { | ||
79 | progressBars = new HashMap<>(); | ||
80 | downloads.stream().forEachOrdered(d -> { | ||
81 | progressBars.put(d, downloadProgress(d.getProgressTitle(), progressTitleWidth)); | ||
82 | }); | ||
83 | } | ||
84 | |||
85 | var pool = newFixedThreadPool(this.arguments.getThreads()); | ||
86 | downloads.stream().forEachOrdered(d -> { | ||
87 | startDownload(d, pool, progressBars); | ||
88 | }); | ||
89 | |||
90 | pool.shutdown(); | ||
91 | pool.awaitTermination(MAX_VALUE, NANOSECONDS); | ||
92 | } | ||
93 | |||
94 | @SuppressWarnings({ "null", "resource" }) | ||
95 | private void startDownload(@Nonnull GameDownload download, @Nonnull ExecutorService service, | ||
96 | @Nullable Map<GameDownload, ProgressBar> progressBars) { | ||
97 | service.submit((AERunnable) () -> { | ||
98 | var progress = progressBars == null ? null : progressBars.get(download); | ||
99 | |||
100 | download.downloadTo(this.arguments.getOutputPath(), progress); | ||
101 | |||
102 | if (progress != null) { | ||
103 | progress.stepTo(progress.getMax()); | ||
104 | progress.refresh(); | ||
105 | progress.pause(); | ||
106 | } | ||
107 | }); | ||
108 | } | ||
109 | |||
110 | @Nonnull | ||
111 | @SuppressWarnings({ "null", "resource" }) | ||
112 | public List<GameDownload> getDownloadList() throws IOException, NotLoggedInException { | ||
113 | ForkJoinPool pool = null; | ||
114 | try (var p = this.arguments.isQuiet() ? null : createGameLoadingProgress()) { | ||
115 | if (p != null) | ||
116 | p.setExtraMessage("Loading user library"); | ||
117 | |||
118 | var ids = this.arguments.getGameIds(); | ||
119 | if (p != null) | ||
120 | p.maxHint(ids.size()); | ||
121 | |||
122 | var user = this.arguments.getUser(); | ||
123 | pool = new ForkJoinPool(ids.size() + 1); // metadata requests take a while so it doesn't hurt to parallelize | ||
124 | var games = pool.submit(() -> { // this is a hack to increase parallelStream()'s parallelism | ||
125 | return ids.parallelStream().map(user::resolveGame).filter(Objects::nonNull).peek(g -> { // NOSONAR | ||
126 | if (p != null) { | ||
127 | p.setExtraMessage(g.getTitle()); | ||
128 | p.step(); | ||
129 | } | ||
130 | }).collect(toUnmodifiableSet()); | ||
131 | }).join(); | ||
132 | |||
133 | if (p != null) { | ||
134 | p.stepTo(ids.size()); | ||
135 | p.setExtraMessage("Processing games"); | ||
136 | } | ||
137 | return createDownloadList(games, pool); | ||
138 | |||
139 | } finally { | ||
140 | if (pool != null) | ||
141 | pool.shutdown(); | ||
142 | if (!this.arguments.isQuiet()) | ||
143 | cursorUp(); | ||
144 | } | ||
145 | } | ||
146 | |||
147 | @Nonnull | ||
148 | @SuppressWarnings("null") | ||
149 | private List<GameDownload> createDownloadList(@Nonnull Set<Game> games, @Nonnull ForkJoinPool pool) { | ||
150 | var types = this.arguments.getTypes(); | ||
151 | var platforms = this.arguments.getPlatforms(); | ||
152 | var output = this.arguments.getOutputPath(); | ||
153 | |||
154 | return pool.submit(() -> { | ||
155 | return games.parallelStream().flatMap(g -> concat(Stream.of(g), g.getDlcs().stream())).filter(g -> { | ||
156 | if (g instanceof GameDlc dlc && !this.arguments.downloadDlcs()) { | ||
157 | verbose("Downloading DLCs is disabled - skipping DLC @|bold %s|@ of game @|bold %s|@", | ||
158 | dlc.getTitle(), dlc.getParent().getTitle()); | ||
159 | |||
160 | return false; | ||
161 | } else { | ||
162 | return true; | ||
163 | } | ||
164 | }).flatMap(g -> g.getDownloads().stream()).filter(d -> { | ||
165 | if (!platforms.contains(d.platform())) { | ||
166 | verbose("Downloading for @|bold %s|@ is disabled - not downloading @|bold %s|@", | ||
167 | d.platform().toString().toLowerCase(), d.getProgressTitle()); | ||
168 | return false; | ||
169 | |||
170 | } else if (!types.contains(d.type())) { | ||
171 | verbose("Downloading types of @|bold %s|@ is disabled - not downloading @|bold %s|@", | ||
172 | d.type().toString().toLowerCase(), d.getProgressTitle()); | ||
173 | return false; | ||
174 | |||
175 | } else if (output.resolve(d.path()).toFile().exists()) { | ||
176 | verbose("Not downloading @|bold %s|@ because it is already downloaded", | ||
177 | d.type().toString().toLowerCase(), d.getProgressTitle()); | ||
178 | return false; | ||
179 | |||
180 | } else { | ||
181 | return true; | ||
182 | } | ||
183 | }) | ||
184 | .sorted(Comparator.<GameDownload, String>comparing(d -> d.game().getTitle()) | ||
185 | .thenComparing(GameDownload::platform) | ||
186 | .thenComparing(d -> requireNonNullElse(d.version(), "")) | ||
187 | .thenComparing(GameDownload::type) | ||
188 | .thenComparingInt(GameDownload::part)) | ||
189 | .toList(); | ||
190 | }).join(); | ||
191 | } | ||
192 | |||
193 | @Nonnull | ||
194 | @SuppressWarnings("null") | ||
195 | public ProgressBar downloadProgress(@Nonnull String title, int titleMinWidth) { | ||
196 | return new ProgressBarBuilder().setUpdateIntervalMillis(250) | ||
197 | .setTaskName(title + ".".repeat(max(0, titleMinWidth - title.length()))) | ||
198 | .setStyle(this.arguments.getColorMode() == OFF ? UNICODE_BLOCK : COLORFUL_UNICODE_BLOCK) | ||
199 | .setInitialMax(1) | ||
200 | .setUnit(" MiB", 1024L * 1024L) | ||
201 | .build(); | ||
202 | } | ||
203 | |||
204 | @Nonnull | ||
205 | @SuppressWarnings("null") | ||
206 | private ProgressBar createGameLoadingProgress() { | ||
207 | return new ProgressBarBuilder().setUpdateIntervalMillis(250) | ||
208 | .setTaskName("Loading games") | ||
209 | .setStyle(this.arguments.getColorMode() == OFF ? UNICODE_BLOCK : COLORFUL_UNICODE_BLOCK) | ||
210 | .setInitialMax(-1) | ||
211 | .continuousUpdate() | ||
212 | .clearDisplayOnFinish() | ||
213 | .hideEta() | ||
214 | .build(); | ||
215 | } | ||
216 | |||
217 | @Override | ||
218 | public Integer call() throws Exception { | ||
219 | setVerbose(this.arguments.isVerbose()); | ||
220 | setColorMode(this.arguments.getColorMode()); | ||
221 | |||
222 | try { | ||
223 | run(); | ||
224 | } catch (NotLoggedInException e) { | ||
225 | println(""" | ||
226 | @|bold,red Invalid token.|@ Find your token by logging into GOG in your browser, \ | ||
227 | and copying the "gog-al" cookie from its developer tools."""); | ||
228 | return 1; | ||
229 | } | ||
230 | return 0; | ||
231 | } | ||
232 | |||
233 | public static void main(String[] args) { | ||
234 | Signal.handle(new Signal("INT"), s -> { | ||
235 | out.println(); | ||
236 | exit(0); | ||
237 | }); | ||
238 | |||
239 | exit(new CommandLine(new Main()).setUsageHelpAutoWidth(true) | ||
240 | .setUsageHelpLongOptionsMaxWidth(50) | ||
241 | .setCaseInsensitiveEnumValuesAllowed(true) | ||
242 | .setOverwrittenOptionsAllowed(true) | ||
243 | .execute(args)); | ||
244 | } | ||
245 | |||
246 | } | ||