188 lines
5.4 KiB
TypeScript
188 lines
5.4 KiB
TypeScript
import { 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 { ISessionHelper } from '../session/i-session-helper';
|
|
import { INhentaiAppWindow } from './i-nhentai-app-window';
|
|
import { IFavorite } from './nhentai';
|
|
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 INhentaiAppWindow {
|
|
public constructor(@inject('session-helper') sessionHelper: ISessionHelper) {
|
|
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();
|
|
}
|
|
|
|
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<IFavorite, undefined> {
|
|
for (const bookUrl of bookUrls) {
|
|
yield await thisArg.getBookTorrent(bookUrl);
|
|
}
|
|
return;
|
|
})(this),
|
|
{
|
|
objectMode: true,
|
|
}
|
|
);
|
|
readable.once('end', () => {
|
|
this.close();
|
|
release();
|
|
});
|
|
|
|
return readable;
|
|
} catch (e) {
|
|
release();
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
protected getCsp(): IContentSecurityPolicy {
|
|
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<IFavorite> {
|
|
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,
|
|
};
|
|
}
|
|
}
|