RenaiApp/src/main/modules/nhentai/nhentai-app-window.ts

190 lines
5.7 KiB
TypeScript

import type { WebContents } from 'electron';
import os from 'os';
import path from 'path';
import { Readable } from 'stream';
import { URL } from 'url';
import { createReadStream, remove } from 'fs-extra';
import { injectable } from 'inversify';
import { inject } from '../../core/inject';
import { SiteAppWindow } from '../app-window/site-app-window';
import { WindowClosedError } from '../app-window/window-closed-error';
import type { SessionHelperInterface } from '../session/session-helper-interface';
import type { NhentaiAppWindowInterface } from './nhentai-app-window-interface';
import {
url as nhentaiUrl,
hostname as nhentaiHostname,
paths as nhentaiPaths,
getFavoritePageUrl,
nextFavoritePageSelector,
coverLinkSelector,
downloadLinkId,
getGalleryId,
} from './nhentai-util';
const waitInterval = 2000;
@injectable()
export class NhentaiAppWindow extends SiteAppWindow implements NhentaiAppWindowInterface {
public constructor(@inject('session-helper') sessionHelper: SessionHelperInterface) {
super(sessionHelper, nhentaiUrl);
}
public async getFavorites(): Promise<NodeJS.ReadableStream> {
const release = await this.acquireLock();
try {
if (this.isClosed()) {
await this.open();
}
if (!(await this.isLoggedIn())) {
await this.login();
}
this._window?.setProgressBar(0, { mode: 'indeterminate' });
const bookUrls: string[] = [];
for await (const wc of this.getFavoritePageWebContentsGenerator()) {
bookUrls.push(
...((await wc.executeJavaScript(
`Array.from(document.querySelectorAll('${coverLinkSelector}')).map((el) => el.href)`
)) as string[])
);
}
const readable = Readable.from(
(async function* (thisArg): AsyncGenerator<Nhentai.Favorite, undefined> {
for (let i = 0; i < bookUrls.length; i++) {
const bookUrl = bookUrls[i];
yield await thisArg.getBookTorrent(bookUrl);
thisArg._window?.setProgressBar(i / bookUrls.length);
}
return;
})(this),
{
objectMode: true,
}
);
readable.once('end', () => {
this.close();
release();
});
return readable;
} catch (e) {
release();
throw e;
}
}
protected getCsp(): Session.ContentSecurityPolicy {
return {
'default-src': ['nhentai.net'],
'script-src': ['nhentai.net', "'unsafe-eval'"],
'script-src-elem': ['*.nhentai.net', "'unsafe-inline'", '*.google.com', '*.gstatic.com'],
'style-src': [
'*.nhentai.net',
"'unsafe-inline'",
'fonts.googleapis.com',
'cdnjs.cloudflare.com',
'maxcdn.bootstrapcdn.com',
'*.gstatic.com',
],
'img-src': ['*.nhentai.net', 'data:', '*.gstatic.com', '*.google.com'],
'font-src': ['fonts.gstatic.com', 'cdnjs.cloudflare.com', 'maxcdn.bootstrapcdn.com'],
'frame-src': ['*.google.com'],
'connect-src': ['nhentai.net', '*.google.com'],
'worker-src': ['*.google.com'],
};
}
protected onWillNavigate(event: Electron.Event, navigationUrl: string): void {
const url = new URL(navigationUrl);
if (url.hostname !== nhentaiHostname) {
event.preventDefault();
}
}
private async isLoggedIn(): Promise<boolean> {
if (!this._window) {
throw new WindowClosedError();
}
return this._window.webContents
.executeJavaScript(
`fetch('${
nhentaiUrl + nhentaiPaths.favorites
}', {credentials: 'include', redirect: 'manual'}).then((res) => res.status)`
)
.then((status: number) => status === HttpCode.OK);
}
/**
* @throws WindowClosedError when this._window is null
*/
private async login(): Promise<void> {
if (!this._window) {
throw new WindowClosedError();
}
await this.loadUrlSafe(nhentaiUrl + nhentaiPaths.login);
return new Promise((resolve, reject) => {
const timeout = setInterval(() => {
this.isLoggedIn()
.then((loggedIn) => {
if (loggedIn) {
clearTimeout(timeout);
resolve();
}
})
.catch((reason) => reject(reason));
}, waitInterval);
});
}
private async *getFavoritePageWebContentsGenerator(): AsyncGenerator<WebContents, undefined> {
if (!this._window) {
throw new WindowClosedError();
}
await this.loadUrlSafe(getFavoritePageUrl());
while (true) {
yield this._window.webContents;
const hasNextPage = (await this._window.webContents.executeJavaScript(
`!!document.querySelector('${nextFavoritePageSelector}')`
)) as boolean;
if (hasNextPage) {
const nextPageHref = (await this._window.webContents.executeJavaScript(
`document.querySelector('${nextFavoritePageSelector}').href`
)) as string;
await this.loadUrlSafe(nextPageHref);
} else {
break;
}
}
return;
}
private async getBookTorrent(bookUrl: string): Promise<Nhentai.Favorite> {
if (!this._window) {
throw new WindowClosedError();
}
const galleryId = getGalleryId(bookUrl);
const fileName = `${galleryId}.torrent`;
const filePath = path.resolve(os.tmpdir(), fileName);
await this.loadUrlSafe(bookUrl);
const downloadLink: string = (await this._window.webContents.executeJavaScript(
`document.getElementById('${downloadLinkId}').href`
)) as string;
await this.downloadUrlSafe(downloadLink, filePath);
const readable = createReadStream(filePath, { emitClose: true });
readable.once('close', () => {
void remove(filePath);
});
return {
name: fileName,
torrentFile: readable,
};
}
}