This commit is contained in:
death-claw
2023-11-14 00:38:43 +01:00
committed by GitHub
parent f0c2de829a
commit 6376576e9e
260 changed files with 5866 additions and 5207 deletions
+4 -2
View File
@@ -2,10 +2,12 @@
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>me.mnlr</groupId>
<groupId>me.vripper</groupId>
<artifactId>vripper-web-ui</artifactId>
<version>4.4.0</version>
<version>5.0.0</version>
<name>vripper-web-ui</name>
<build>
<resources>
<resource>
@@ -0,0 +1,3 @@
export class DownloadSpeed {
constructor(public speed: number) {}
}
@@ -0,0 +1,3 @@
export class ErrorCount {
constructor(public count: number) {}
}
@@ -1,9 +0,0 @@
export class GlobalState {
constructor(
public running: number,
public remaining: number,
public error: number,
public loggedUser: string,
public downloadSpeed: string
) {}
}
+3 -3
View File
@@ -1,10 +1,10 @@
export class Image {
constructor(
public postId: string,
public postId: number,
public url: string,
public status: string,
public index: number,
public current: number,
public total: number
public downloaded: number,
public size: number
) {}
}
@@ -4,7 +4,7 @@ export class PostItem {
public title: string,
public url: string,
public hosts: [{ first: string; second: number }],
public postId: string,
public threadId: string
public postId: number,
public threadId: number
) {}
}
+3 -2
View File
@@ -1,6 +1,6 @@
export class Post {
constructor(
public postId: string,
public postId: number,
public postTitle: string,
public status: string,
public url: string,
@@ -9,6 +9,7 @@ export class Post {
public hosts: string[],
public addedOn: string,
public rank: number,
public downloadDirectory: string
public downloadDirectory: string,
public downloaded: number
) {}
}
@@ -0,0 +1,3 @@
export class QueueState {
constructor(public running: number, public remaining: number) {}
}
@@ -1,20 +1,19 @@
export interface Settings {
maxEventLog: number;
connectionSettings: ConnectionSettings;
downloadSettings: DownloadSettings;
viperSettings: ViperSettings;
systemSettings: SystemSettings;
}
export interface ConnectionSettings {
maxThreads: number;
maxTotalThreads: number;
maxConcurrentPerHost: number;
maxGlobalConcurrent: number;
timeout: number;
maxAttempts: number;
}
export interface DownloadSettings {
downloadPath: string;
tempPath: string;
autoStart: boolean;
autoQueueThreshold: number;
forceOrder: boolean;
@@ -31,3 +30,9 @@ export interface ViperSettings {
thanks: boolean;
proxy: string;
}
export interface SystemSettings {
tempPath: string;
cachePath: string;
maxEventLog: number;
}
@@ -1,7 +1,8 @@
export class Thread {
constructor(
public link: string,
public threadId: string,
public title: string,
public threadId: number,
public total: number
) {}
}
@@ -40,6 +40,7 @@ import {
import { ProgressCellComponent } from '../progress-cell/progress-cell.component';
import { Image } from '../domain/image.model';
import { ImageDialogData, ImagesComponent } from '../images/images.component';
import { formatBytes } from '../utils/utils';
@Component({
selector: 'app-download-table',
@@ -130,8 +131,13 @@ export class DownloadTableComponent implements OnDestroy {
{
headerName: 'Total',
tooltipValueGetter: params =>
`${params.data?.done}/${params.data?.total}`,
valueGetter: params => `${params.data?.done}/${params.data?.total}`,
`${params.data?.done}/${params.data?.total} (${formatBytes(
params.data?.downloaded
)})`,
valueGetter: params =>
`${params.data?.done}/${params.data?.total} (${formatBytes(
params.data?.downloaded
)})`,
},
{
headerName: 'Hosts',
@@ -253,33 +259,38 @@ export class DownloadTableComponent implements OnDestroy {
private connect() {
this.subscriptions.push(
this.applicationEndpoint.posts$.subscribe((e: Post[]) => {
const toAdd: Post[] = [];
const toUpdate: Post[] = [];
e.forEach(v => {
if (this.agGrid.api.getRowNode(v.postId) == null) {
toAdd.push(v);
} else {
toUpdate.push(v);
}
});
this.agGrid.api.applyTransaction({ update: toUpdate, add: toAdd });
this.applicationEndpoint.newPosts$.subscribe((newPosts: Post[]) => {
this.agGrid.api.applyTransaction({ add: newPosts });
})
);
this.subscriptions.push(
this.applicationEndpoint.postsRemove$.subscribe((e: string[]) => {
this.applicationEndpoint.deletedPosts$.subscribe((e: string[]) => {
const toRemove: string[] = [];
e.forEach(v => {
const rowNode: IRowNode | undefined = this.agGrid.api.getRowNode(v);
if (rowNode != null) {
toRemove.push(rowNode.data);
}
return;
});
this.agGrid.api.applyTransaction({ remove: toRemove });
})
);
this.subscriptions.push(
this.applicationEndpoint.updatedPosts$.subscribe((e: Post[]) => {
const toUpdate: Post[] = [];
e.forEach(v => {
const rowNode: IRowNode | undefined = this.agGrid.api.getRowNode(
String(v.postId)
);
if (rowNode != null) {
toUpdate.push(v);
}
});
this.agGrid.api.applyTransaction({ update: toUpdate });
})
);
}
private disconnect() {
@@ -9,9 +9,10 @@ import { Image } from '../domain/image.model';
import { MatButtonModule } from '@angular/material/button';
import { ValueGetterParams } from 'ag-grid-community/dist/lib/entities/colDef';
import { ProgressCellComponent } from '../progress-cell/progress-cell.component';
import { ITooltipParams } from 'ag-grid-community/dist/lib/rendering/tooltipComponent';
export interface ImageDialogData {
postId: string;
postId: number;
}
@Component({
@@ -24,7 +25,7 @@ export interface ImageDialogData {
export class ImagesComponent implements OnDestroy {
@ViewChild('agGrid') agGrid!: AgGridAngular;
gridOptions: GridOptions;
gridOptions: GridOptions<Image>;
subscriptions: Subscription[] = [];
constructor(
@@ -48,21 +49,21 @@ export class ImagesComponent implements OnDestroy {
{
headerName: 'Progress',
field: 'progress',
tooltipValueGetter: params => {
tooltipValueGetter: (params: ITooltipParams<Image>) => {
if (!params.data) {
return 0;
}
return params.data.current === 0 || params.data.total === 0
return params.data.downloaded === 0 || params.data.size === 0
? 0
: (params.data.current / params.data.total) * 100;
: (params.data.downloaded / params.data.size) * 100;
},
valueGetter: (params: ValueGetterParams<Image>) => {
if (!params.data) {
return 0;
}
return params.data.current === 0 || params.data.total === 0
return params.data.downloaded === 0 || params.data.size === 0
? 0
: (params.data.current / params.data.total) * 100;
: (params.data.downloaded / params.data.size) * 100;
},
cellRenderer: ProgressCellComponent,
},
@@ -101,17 +101,20 @@ export class LogTableComponent implements OnInit, OnDestroy {
private connect() {
this.subscriptions.push(
this.applicationEndpoint.logs$.subscribe((e: Log[]) => {
const toAdd: Log[] = [];
this.applicationEndpoint.newLogs$.subscribe((e: Log[]) => {
this.agGrid.api.applyTransaction({ add: e });
})
);
this.subscriptions.push(
this.applicationEndpoint.updatedLogs$.subscribe((e: Log[]) => {
const toUpdate: Log[] = [];
e.forEach(v => {
if (this.agGrid.api.getRowNode(v.id.toString()) == null) {
toAdd.push(v);
} else {
if (this.agGrid.api.getRowNode(v.id.toString()) != null) {
toUpdate.push(v);
}
});
this.agGrid.api.applyTransaction({ update: toUpdate, add: toAdd });
this.agGrid.api.applyTransaction({ update: toUpdate });
})
);
@@ -125,7 +128,6 @@ export class LogTableComponent implements OnInit, OnDestroy {
if (rowNode != null) {
toRemove.push(rowNode.data);
}
return;
});
this.agGrid.api.applyTransaction({ remove: toRemove });
})
@@ -0,0 +1,12 @@
import { Pipe, PipeTransform } from '@angular/core';
import { formatBytes } from '../utils/utils';
@Pipe({
standalone: true,
name: 'downloadSpeed',
})
export class DownloadSpeedPipe implements PipeTransform {
transform(value: number): string {
return formatBytes(value);
}
}
@@ -10,27 +10,42 @@ import { Image } from '../domain/image.model';
import { HttpClient } from '@angular/common/http';
import { PostItem } from '../domain/post-item.model';
import { Settings } from '../domain/settings.model';
import { GlobalState } from '../domain/global-state.model';
import { QueueState } from '../domain/queue-state.model';
import { DownloadSpeed } from '../domain/download.speed';
import { ErrorCount } from '../domain/error.count';
@Injectable({
providedIn: 'root',
})
export class ApplicationEndpointService {
private rxStomp!: RxStomp;
private posts!: Observable<Post[]>;
private globalState!: Observable<GlobalState>;
connectionState = signal(RxStompState.CLOSED);
get posts$(): Observable<Post[]> {
return this.posts;
constructor(
@Inject(WS_BASE_URL) private wsBaseUrl: string,
@Inject(BASE_URL) private baseUrl: string,
private httpClient: HttpClient
) {
this.init();
}
private postsRemove!: Observable<string[]>;
private rxStomp!: RxStomp;
get postsRemove$(): Observable<string[]> {
return this.postsRemove;
connectionState = signal(RxStompState.CLOSED);
private newPosts!: Observable<Post[]>;
get newPosts$(): Observable<Post[]> {
return this.newPosts;
}
private updatedPosts!: Observable<Post[]>;
get updatedPosts$(): Observable<Post[]> {
return this.updatedPosts;
}
private deletedPosts!: Observable<string[]>;
get deletedPosts$(): Observable<string[]> {
return this.deletedPosts;
}
private threads!: Observable<Thread[]>;
@@ -51,10 +66,16 @@ export class ApplicationEndpointService {
return this.threadRemoveAll;
}
private logs!: Observable<Log[]>;
private newLogs!: Observable<Log[]>;
get logs$(): Observable<Log[]> {
return this.logs;
get newLogs$(): Observable<Log[]> {
return this.newLogs;
}
private updatedLogs!: Observable<Log[]>;
get updatedLogs$(): Observable<Log[]> {
return this.updatedLogs;
}
private logsRemove!: Observable<number[]>;
@@ -63,7 +84,7 @@ export class ApplicationEndpointService {
return this.logsRemove;
}
postDetails$(postId: string): Observable<Image[]> {
postDetails$(postId: number): Observable<Image[]> {
return this.rxStomp.watch('/topic/images/' + postId).pipe(
map(e => {
return JSON.parse(e.body).map((element: any) => {
@@ -72,8 +93,8 @@ export class ApplicationEndpointService {
element.url,
element.status,
element.index + 1,
element.current,
element.total
element.downloaded,
element.size
);
});
}),
@@ -81,14 +102,6 @@ export class ApplicationEndpointService {
);
}
constructor(
@Inject(WS_BASE_URL) private wsBaseUrl: string,
@Inject(BASE_URL) private baseUrl: string,
private httpClient: HttpClient
) {
this.init();
}
init() {
this.connect();
this.prepareTopics();
@@ -98,8 +111,28 @@ export class ApplicationEndpointService {
this.rxStomp.deactivate();
}
get globalState$(): Observable<GlobalState> {
return this.globalState;
private queueState!: Observable<QueueState>;
get queueState$(): Observable<QueueState> {
return this.queueState;
}
private downloadSpeed!: Observable<DownloadSpeed>;
get downloadSpeed$(): Observable<DownloadSpeed> {
return this.downloadSpeed;
}
private vgUsername!: Observable<string>;
get vgUsername$(): Observable<string> {
return this.vgUsername;
}
private errorCount!: Observable<ErrorCount>;
get errorCount$(): Observable<ErrorCount> {
return this.errorCount;
}
connect() {
@@ -115,7 +148,28 @@ export class ApplicationEndpointService {
prepareTopics() {
this.rxStomp.connectionState$.subscribe(e => this.connectionState.set(e));
this.postsRemove = this.rxStomp.watch('/topic/posts/deleted').pipe(
this.downloadSpeed = this.rxStomp.watch('/topic/download-speed').pipe(
map(e => {
return JSON.parse(e.body);
}),
share()
);
this.vgUsername = this.rxStomp.watch('/topic/vg-username').pipe(
map(e => {
return e.body;
}),
share()
);
this.errorCount = this.rxStomp.watch('/topic/error-count').pipe(
map(e => {
return JSON.parse(e.body);
}),
share()
);
this.deletedPosts = this.rxStomp.watch('/topic/posts/deleted').pipe(
map(e => {
return JSON.parse(e.body);
}),
@@ -143,15 +197,15 @@ export class ApplicationEndpointService {
share()
);
this.globalState = this.rxStomp.watch('/topic/state').pipe(
this.queueState = this.rxStomp.watch('/topic/queue-state').pipe(
map(e => {
const state: GlobalState = JSON.parse(e.body);
const state: QueueState = JSON.parse(e.body);
return state;
}),
share()
);
this.posts = this.rxStomp.watch('/topic/posts').pipe(
this.newPosts = this.rxStomp.watch('/topic/posts/new').pipe(
map(e => {
// const posts: Array<Post> = [];
return JSON.parse(e.body).map((element: any) => {
@@ -165,15 +219,52 @@ export class ApplicationEndpointService {
element.hosts,
element.addedOn,
element.rank + 1,
element.downloadDirectory
element.downloadDirectory,
element.downloaded
);
});
// return posts;
}),
share()
);
this.logs = this.rxStomp.watch('/topic/logs').pipe(
this.updatedPosts = this.rxStomp.watch('/topic/posts/updated').pipe(
map(e => {
// const posts: Array<Post> = [];
return JSON.parse(e.body).map((element: any) => {
return new Post(
element.postId,
element.postTitle,
element.status,
element.url,
element.done,
element.total,
element.hosts,
element.addedOn,
element.rank + 1,
element.downloadDirectory,
element.downloaded
);
});
}),
share()
);
this.newLogs = this.rxStomp.watch('/topic/logs/new').pipe(
map(e => {
return JSON.parse(e.body).map((element: any) => {
return new Log(
element.id,
element.type,
element.status,
element.time,
element.message
);
});
}),
share()
);
this.updatedLogs = this.rxStomp.watch('/topic/logs/updated').pipe(
map(e => {
return JSON.parse(e.body).map((element: any) => {
return new Log(
@@ -191,7 +282,12 @@ export class ApplicationEndpointService {
this.threads = this.rxStomp.watch('/topic/threads').pipe(
map(e => {
return JSON.parse(e.body).map((element: any) => {
return new Thread(element.link, element.threadId, element.total);
return new Thread(
element.link,
element.title,
element.threadId,
element.total
);
});
}),
share()
@@ -212,7 +308,7 @@ export class ApplicationEndpointService {
);
}
getThreadPosts(threadId: string) {
getThreadPosts(threadId: number) {
return this.httpClient
.get<PostItem[]>(this.baseUrl + `/api/grab/${threadId}`)
.pipe(map(v => v.map(p => ({ ...p, hosts: p.hosts }))));
@@ -22,10 +22,6 @@
name="downloadPath"
required />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Temporary Path</mat-label>
<input formControlName="tempPath" matInput name="tempPath" required />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label
@@ -132,22 +128,22 @@
<mat-form-field appearance="outline">
<mat-label>Global concurrent downloads</mat-label>
<input
formControlName="maxTotalThreads"
formControlName='maxConcurrentPerHost'
matInput
max="12"
min="0"
name="maxTotalThreads"
name='maxConcurrentPerHost'
required
type="number" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>Concurrent downloads per host</mat-label>
<input
formControlName="maxThreads"
formControlName='maxGlobalConcurrent'
matInput
max="4"
min="1"
name="maxThreads"
name='maxGlobalConcurrent'
required
type="number" />
</mat-form-field>
@@ -233,9 +229,9 @@
</div>
</form>
</mat-tab>
<mat-tab label="Event Log">
<mat-tab label='System'>
<form
[formGroup]="eventLogSettingsForm"
[formGroup]='systemSettingsForm'
autocomplete="off"
style="
display: flex;
@@ -245,7 +241,19 @@
padding-right: 20px;
">
<mat-form-field appearance="outline">
<mat-label>Maximum records</mat-label>
<mat-label>Temporary Path</mat-label>
<input formControlName='tempPath' matInput name='tempPath' required />
</mat-form-field>
<mat-form-field appearance='outline'>
<mat-label>Cache Path</mat-label>
<input
formControlName='cachePath'
matInput
name='cachePath'
required />
</mat-form-field>
<mat-form-field appearance='outline'>
<mat-label>Maximum log entries</mat-label>
<input
formControlName="maxEventLog"
matInput
@@ -21,6 +21,7 @@ import {
ConnectionSettings,
DownloadSettings,
Settings,
SystemSettings,
ViperSettings,
} from '../domain/settings.model';
import { ApplicationEndpointService } from '../services/application-endpoint.service';
@@ -62,7 +63,6 @@ export class SettingsComponent {
downloadSettingsForm = new FormGroup({
downloadPath: new FormControl(''),
tempPath: new FormControl(''),
autoStart: new FormControl(false),
autoQueueThreshold: new FormControl(0),
forceOrder: new FormControl(false),
@@ -73,13 +73,15 @@ export class SettingsComponent {
});
connectionSettingsForm = new FormGroup({
maxThreads: new FormControl(0),
maxTotalThreads: new FormControl(0),
maxConcurrentPerHost: new FormControl(0),
maxGlobalConcurrent: new FormControl(0),
timeout: new FormControl(0),
maxAttempts: new FormControl(0),
});
eventLogSettingsForm = new FormGroup({
systemSettingsForm = new FormGroup({
tempPath: new FormControl(''),
cachePath: new FormControl(''),
maxEventLog: new FormControl(0),
});
@@ -91,7 +93,7 @@ export class SettingsComponent {
this.viperGirlsSettingsForm.reset(data.viperSettings);
this.downloadSettingsForm.reset(data.downloadSettings);
this.connectionSettingsForm.reset(data.connectionSettings);
this.eventLogSettingsForm.reset(data);
this.systemSettingsForm.reset(data.systemSettings);
}
save = () => {
@@ -106,7 +108,9 @@ export class SettingsComponent {
downloadSettings: {
...(this.downloadSettingsForm.value as DownloadSettings),
},
...this.eventLogSettingsForm.value,
systemSettings: {
...(this.systemSettingsForm.value as SystemSettings),
},
} as Settings)
.subscribe(() => this.dialogRef.close());
};
@@ -1,16 +1,22 @@
<ng-container *ngIf="globalState$ | async as state">
<div class="status-bar" style="display: flex">
<span style="padding-left: 10px; flex-grow: 1">
<ng-container *ngIf="state.loggedUser.length > 0"
>Logged in as: {{ state.loggedUser }}</ng-container
>
</span>
<span>{{ state.downloadSpeed || '0' + '/s' }}</span>
<div class='status-bar' style='display: flex'>
<span style='padding-left: 10px; flex-grow: 1'>
<ng-container *ngIf='vgUsername$ | async as vgUsername'>
<span>{{
vgUsername.length > 0 ? 'Logged in as: ' + vgUsername : ''
}}</span>
</ng-container>
</span>
<span *ngIf='downloadSpeed$ | async as downloadSpeed'>{{
(downloadSpeed.speed | downloadSpeed) + '/s'
}}</span>
<mat-divider [vertical]='true' style='height: 20px'></mat-divider>
<ng-container *ngIf='queueState$ | async as queueState'>
<span>Downloading: {{ queueState.running }}</span>
<mat-divider [vertical]="true" style="height: 20px"></mat-divider>
<span>Downloading: {{ state.running }}</span>
<span>Pending: {{ queueState.remaining }}</span>
<mat-divider [vertical]="true" style="height: 20px"></mat-divider>
<span>Pending: {{ state.remaining }}</span>
<mat-divider [vertical]="true" style="height: 20px"></mat-divider>
<span style="padding-right: 10px">Error: {{ state.error }}</span>
</div>
</ng-container>
</ng-container>
<span *ngIf='errorCount$ | async as errorCount' style='padding-right: 10px'
>Error: {{ errorCount.count }}</span
>
</div>
@@ -2,16 +2,20 @@ import { Component } from '@angular/core';
import { CommonModule, NgIf } from '@angular/common';
import { ApplicationEndpointService } from '../services/application-endpoint.service';
import { MatDividerModule } from '@angular/material/divider';
import { DownloadSpeedPipe } from '../pipes/download-speed.pipe';
@Component({
selector: 'app-status-bar',
standalone: true,
imports: [CommonModule, NgIf, MatDividerModule],
imports: [CommonModule, NgIf, MatDividerModule, DownloadSpeedPipe],
templateUrl: './status-bar.component.html',
styleUrls: ['./status-bar.component.scss'],
})
export class StatusBarComponent {
constructor(private applicationEndpoint: ApplicationEndpointService) {}
globalState$ = this.applicationEndpoint.globalState$;
queueState$ = this.applicationEndpoint.queueState$;
downloadSpeed$ = this.applicationEndpoint.downloadSpeed$;
vgUsername$ = this.applicationEndpoint.vgUsername$;
errorCount$ = this.applicationEndpoint.errorCount$;
}
@@ -18,7 +18,7 @@ import { PostItem } from '../domain/post-item.model';
import { ValueGetterParams } from 'ag-grid-community/dist/lib/entities/colDef';
export interface ThreadDialogData {
threadId: string;
threadId: number;
}
@Component({
@@ -3,9 +3,7 @@ import {
Component,
ComponentRef,
EventEmitter,
Input,
OnDestroy,
OnInit,
Output,
ViewChild,
} from '@angular/core';
@@ -18,7 +16,7 @@ import {
RowDataUpdatedEvent,
RowDoubleClickedEvent,
} from 'ag-grid-community';
import { filter, fromEvent, merge, Observable, Subscription, take } from 'rxjs';
import { fromEvent, merge, Subscription, take } from 'rxjs';
import { ApplicationEndpointService } from '../services/application-endpoint.service';
import { Thread } from '../domain/thread.model';
import { ComponentPortal, PortalModule } from '@angular/cdk/portal';
@@ -73,6 +71,12 @@ export class ThreadTableComponent implements OnDestroy {
) {
this.gridOptions = <GridOptions>{
columnDefs: [
{
headerName: 'Title',
field: 'title',
tooltipField: 'title',
flex: 1,
},
{
headerName: 'Url',
field: 'link',
@@ -184,7 +188,7 @@ export class ThreadTableComponent implements OnDestroy {
const toAdd: Thread[] = [];
const toUpdate: Thread[] = [];
e.forEach(v => {
if (this.agGrid.api.getRowNode(v.threadId) == null) {
if (this.agGrid.api.getRowNode(String(v.threadId)) == null) {
toAdd.push(v);
} else {
toUpdate.push(v);
+21
View File
@@ -0,0 +1,21 @@
export function formatBytes(bytes: number, decimals = 2) {
if (!+bytes) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = [
'Bytes',
'KiB',
'MiB',
'GiB',
'TiB',
'PiB',
'EiB',
'ZiB',
'YiB',
];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
}