aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/zajc/gogarchiver/Main.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/zajc/gogarchiver/Main.java')
-rw-r--r--src/main/java/zajc/gogarchiver/Main.java246
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 */
17package zajc.gogarchiver;
18
19import static java.lang.Long.MAX_VALUE;
20import static java.lang.Math.max;
21import static java.lang.System.*;
22import static java.nio.file.Files.createDirectories;
23import static java.util.Objects.requireNonNullElse;
24import static java.util.concurrent.Executors.newFixedThreadPool;
25import static java.util.concurrent.TimeUnit.NANOSECONDS;
26import static java.util.stream.Collectors.toUnmodifiableSet;
27import static java.util.stream.Stream.concat;
28import static me.tongfei.progressbar.ProgressBarStyle.*;
29import static picocli.CommandLine.Help.Ansi.OFF;
30import static zajc.gogarchiver.util.Utilities.*;
31
32import java.io.IOException;
33import java.util.*;
34import java.util.concurrent.*;
35import java.util.stream.Stream;
36
37import javax.annotation.*;
38
39import org.eu.zajc.ef.runnable.except.all.AERunnable;
40
41import me.tongfei.progressbar.*;
42import picocli.CommandLine;
43import picocli.CommandLine.*;
44import sun.misc.Signal; // NOSONAR it's just quality of life
45import zajc.gogarchiver.api.*;
46import 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)
50public 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}