-
Notifications
You must be signed in to change notification settings - Fork 10
/
Copy pathapp.ts
136 lines (117 loc) · 5.49 KB
/
app.ts
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
/* istanbul ignore file: Covered by E2E */
import { GuiSink } from '@lib/logging/gui-sink';
import { LOGGER } from '@lib/logging/logger';
import { EditNote } from '@lib/MB/edit-note';
import { getURLsForRelease } from '@lib/MB/urls';
import { enumerate } from '@lib/util/array';
import { assertHasValue } from '@lib/util/assert';
import { pFinally } from '@lib/util/async';
import { qs } from '@lib/util/dom';
import { ObservableSemaphore } from '@lib/util/observable';
import type { BareCoverArt, QueuedImageBatch } from './types';
import { ImageFetcher } from './fetch';
import { fillEditNote } from './form';
import { getProvider } from './providers';
import { SeedParameters } from './seeding/parameters';
import { InputForm } from './ui/main';
export class App {
private readonly note: EditNote;
private readonly fetcher: ImageFetcher;
private readonly ui: InputForm;
private readonly urlsInProgress: Set<string>;
private readonly loggingSink = new GuiSink();
private readonly fetchingSema: ObservableSemaphore;
public onlyFront = false;
public constructor() {
this.note = EditNote.withFooterFromGMInfo();
this.urlsInProgress = new Set();
// Set up logging banner
LOGGER.addSink(this.loggingSink);
qs('.add-files').insertAdjacentElement('afterend', this.loggingSink.rootElement);
this.fetchingSema = new ObservableSemaphore({
// Need to use lambdas here to access the original `this`.
onAcquired: (): void => {
this.ui.disableSubmissions();
},
onReleased: (): void => {
this.ui.enableSubmissions();
},
});
this.ui = new InputForm(this);
this.fetcher = new ImageFetcher(this.ui);
}
public async processURLs(urls: URL[]): Promise<void> {
return this._processURLs(urls.map((url) => ({ url })));
}
public clearLogLater(): void {
this.loggingSink.clearAllLater();
}
private async _processURLs(coverArts: readonly BareCoverArt[], origin?: string): Promise<void> {
// Run the fetcher in a section during which submitting the edit form
// will be blocked. This is to prevent users from submitting the edits
// while we're still adding images. We run the whole loop in the section
// to prevent toggling the button in between two URLs.
const batches = await this.fetchingSema.runInSection(async () => {
const fetchedBatches: QueuedImageBatch[] = [];
for (const [coverArt, index] of enumerate(coverArts)) {
// Don't process a URL if we're already doing so, e.g. a user
// clicked a button that was already processing via a seed param.
if (this.urlsInProgress.has(coverArt.url.href)) {
continue;
}
this.urlsInProgress.add(coverArt.url.href);
if (coverArts.length > 1) {
LOGGER.info(`Fetching ${coverArt.url} (${index + 1}/${coverArts.length})`);
} else {
// Don't specify progress if there's just one image to process.
LOGGER.info(`Fetching ${coverArt.url}`);
}
try {
const fetchResult = await this.fetcher.fetchImages(coverArt, this.onlyFront);
fetchedBatches.push(fetchResult);
} catch (error) {
LOGGER.error('Failed to fetch or enqueue images', error);
}
this.urlsInProgress.delete(coverArt.url.href);
}
return fetchedBatches;
});
fillEditNote(batches, origin ?? '', this.note);
const totalNumberImages = batches.reduce((accumulator, batch) => accumulator + batch.images.length, 0);
if (totalNumberImages > 0) {
LOGGER.success(`Successfully added ${totalNumberImages} image(s)`);
}
}
public async processSeedingParameters(): Promise<void> {
const parameters = SeedParameters.decode(new URLSearchParams(document.location.search));
await this._processURLs(parameters.images, parameters.origin);
this.clearLogLater();
}
public async addImportButtons(): Promise<void> {
const mbid = window.location.href.match(/musicbrainz\.org\/release\/([a-f\d-]+)\//)?.[1];
assertHasValue(mbid);
const attachedURLs = await getURLsForRelease(mbid, {
excludeEnded: true,
excludeDuplicates: true,
});
const supportedURLs = attachedURLs.filter((url) => getProvider(url)?.allowButtons);
if (supportedURLs.length === 0) return;
// Helper to ensure we don't silently ignore promise rejections in
// `this.processURL`, as the callback given to `ui.addImportButton`
// expects a synchronous function.
// eslint-disable-next-line unicorn/consistent-function-scoping -- Requires access to `this`.
const syncProcessURL = (url: URL): void => {
void pFinally(
this.processURLs([url])
.catch((error) => {
LOGGER.error(`Failed to process URL ${url.href}`, error);
}),
this.clearLogLater.bind(this));
};
await Promise.all(supportedURLs.map((url) => {
const provider = getProvider(url);
assertHasValue(provider);
return this.ui.addImportButton(syncProcessURL.bind(this, url), url.href, provider);
}));
}
}