Adjust all endpoints and methods to tranga/postgres-Server-V2.

Search working.
This commit is contained in:
2025-03-13 20:08:37 +01:00
parent 0402f9e6d0
commit 84520d8e18
36 changed files with 729 additions and 976 deletions

View File

@ -0,0 +1,21 @@
import React, {ReactElement, useEffect} from "react";
import {getData} from "../../App";
export default interface IAuthor {
authorId: string;
authorName: string;
}
export function AuthorElement({apiUri, authorId} : {apiUri: string, authorId: string}) : ReactElement{
let [name, setName] = React.useState<string>(authorId);
useEffect(()=> {
getData(`${apiUri}/v2/Query/Author/${authorId}`)
.then((json) => {
let ret = json as IAuthor;
setName(ret.authorName);
});
}, [])
return (<span>{name}</span>);
}

View File

@ -1,11 +1,6 @@
export default interface IBackendSettings {
"downloadLocation": string;
"workingDirectory": string;
"apiPortNumber": number;
"userAgent": string;
"bufferLibraryUpdates": boolean;
"bufferNotifications": boolean;
"version": number;
"aprilFoolsMode": boolean;
"compression": number;
"bwImages": boolean;

View File

@ -0,0 +1,10 @@
export default interface IChapter{
chapterId: string;
volumeNumber: number;
chapterNumber: string;
url: string;
title: string | undefined;
archiveFileName: string;
downloaded: boolean;
parentMangaId: string;
}

View File

@ -1,10 +0,0 @@
import IManga from "./IManga";
export default interface IChapter{
parentManga: IManga;
name: string | undefined;
volumeNumber: string;
chapterNumber: string;
url: string;
fileName: string;
}

View File

@ -0,0 +1,28 @@
export default interface IJob{
jobId: string;
parentJobId: string;
dependsOnJobIds: string[];
jobType: JobType;
recurrenceMs: number;
lastExecution: Date;
nextExecution: Date;
state: JobState;
enabled: boolean;
}
export enum JobType {
DownloadSingleChapterJob = "DownloadSingleChapterJob",
DownloadAvailableChaptersJob = "DownloadAvailableChaptersJob",
UpdateMetaDataJob = "UpdateMetaDataJob",
MoveFileOrFolderJob = "MoveFileOrFolderJob",
DownloadMangaCoverJob = "DownloadMangaCoverJob",
RetrieveChaptersJob = "RetrieveChaptersJob",
UpdateFilesDownloadedJob = "UpdateFilesDownloadedJob"
}
export enum JobState {
Waiting = "Waiting",
Running = "Running",
Completed = "Completed",
Failed = "Failed"
}

View File

@ -1,28 +0,0 @@
import IMangaConnector from "./IMangaConnector";
import IProgressToken from "./IProgressToken";
import IChapter from "./IChapter";
export default interface IJob{
progressToken: IProgressToken;
recurring: boolean;
recurrenceTime: string;
lastExecution: Date;
nextExecution: Date;
id: string;
jobType: number;
parentJobId: string | null;
mangaConnector: IMangaConnector;
mangaInternalId: string | undefined; //only on DownloadNewChapters
translatedLanguage: string | undefined; //only on DownloadNewChapters
chapter: IChapter | undefined; //only on DownloadChapter
}
export function JobTypeFromNumber(n: number): string {
switch(n) {
case 0: return "Download Chapter";
case 1: return "Download New Chapters";
case 2: return "Update Metadata";
case 3: return "Monitor";
}
return "";
}

View File

@ -0,0 +1,11 @@
export default interface ILibraryConnector {
libraryConnectorId: string;
libraryType: LibraryType;
baseUrl: string;
auth: string;
}
export enum LibraryType {
Komga = "Komga",
Kavita = "Kavita"
}

View File

@ -1,13 +0,0 @@
export default interface ILibraryConnector {
libraryType: number;
baseUrl: string;
auth: string;
}
export function GetLibraryConnectorNameFromNumber(n: number): string {
switch(n){
case 0: return "Komga";
case 1: return "Kavita";
}
return "";
}

View File

@ -0,0 +1,25 @@
import React, {ReactElement, useEffect} from "react";
import {getData} from "../../App";
import IAuthor from "./IAuthor";
export default interface ILink {
linkId: string;
linkProvider: string;
linkUrl: string;
}
export function LinkElement({apiUri, linkId} : {apiUri: string, linkId: string}) : ReactElement{
let [provider, setProvider] = React.useState<string>(linkId);
let [linkUrl, setLinkUrl] = React.useState<string>("");
useEffect(()=> {
getData(`${apiUri}/v2/Query/Link/${linkId}`)
.then((json) => {
let ret = json as ILink;
setProvider(ret.linkProvider);
setLinkUrl(ret.linkUrl);
});
}, [])
return (<a href={linkUrl}>{provider}</a>);
}

View File

@ -1,126 +1,104 @@
import IMangaConnector from "./IMangaConnector";
import KeyValuePair from "./KeyValuePair";
import Manga from "../Manga";
import React, {EventHandler, ReactElement, ReactEventHandler} from "react";
import React, {ReactElement, ReactEventHandler} from "react";
import Icon from '@mdi/react';
import { mdiTagTextOutline, mdiAccountEdit } from '@mdi/js';
import { mdiTagTextOutline, mdiAccountEdit, mdiLinkVariant } from '@mdi/js';
import MarkdownPreview from '@uiw/react-markdown-preview';
import IJob, {JobTypeFromNumber} from "./IJob";
import IJob from "./IJob";
import {AuthorElement} from "./IAuthor";
import Job from "../Job";
import ProgressBar from "@ramonak/react-progress-bar";
import {LinkElement} from "./ILink";
export default interface IManga{
"sortName": string,
"authors": string[],
"altTitles": KeyValuePair[],
"description": string,
"tags": string[],
"coverUrl": string,
"coverFileNameInCache": string,
"links": KeyValuePair[],
"year": number,
"originalLanguage": string,
"releaseStatus": number,
"folderName": string,
"publicationId": string,
"internalId": string,
"ignoreChaptersBelow": number,
"latestChapterDownloaded": number,
"latestChapterAvailable": number,
"websiteUrl": string,
"mangaConnector": IMangaConnector
mangaId: string;
connectorId: string;
name: string;
description: string;
websiteUrl: string;
year: number;
originalLanguage: string;
releaseStatus: MangaReleaseStatus;
folderName: string;
ignoreChapterBefore: number;
mangaConnectorId: string;
authorIds: string[];
tags: string[];
linkIds: string[];
altTitleIds: string[];
}
export function ReleaseStatusFromNumber(n: number): string {
switch(n) {
case 0: return "Ongoing";
case 1: return "Completed";
case 2: return "OnHiatus";
case 3: return "Cancelled";
case 4: return "Unreleased";
}
return "";
export enum MangaReleaseStatus {
Continuing = "Continuing",
Completed = "Completed",
OnHiatus = "OnHiatus",
Cancelled = "Cancelled",
Unreleased = "Unreleased",
}
export function CoverCard(apiUri: string, manga: IManga) : ReactElement {
const MangaCover : ReactEventHandler<HTMLImageElement> = (e) => {
if(e.currentTarget.src != Manga.GetMangaCoverUrl(apiUri, manga.internalId, e.currentTarget))
e.currentTarget.src = Manga.GetMangaCoverUrl(apiUri, manga.internalId, e.currentTarget);
}
return(
<div className="Manga" key={manga.internalId}>
<img src="../../media/blahaj.png" onLoad={MangaCover} alt="Manga Cover"></img>
<div className="Manga" key={manga.mangaId}>
<img src="../../media/blahaj.png" alt="Manga Cover"></img>
<div>
<p className="pill connector-name">{manga.mangaConnector.name}</p>
<div className="Manga-status" release-status={ReleaseStatusFromNumber(manga.releaseStatus)}></div>
<p className="Manga-name">{manga.sortName}</p>
<p className="pill connector-name">{manga.mangaConnectorId}</p>
<div className="Manga-status" release-status={manga.releaseStatus}></div>
<p className="Manga-name">{manga.name}</p>
</div>
</div>);
}
export function SearchResult(apiUri: string, manga: IManga, interval: Date, onJobsChanged: (internalId: string) => void) : ReactElement {
const MangaCover : ReactEventHandler<HTMLImageElement> = (e) => {
if(e.currentTarget.src != Manga.GetMangaCoverUrl(apiUri, manga.internalId, e.currentTarget))
e.currentTarget.src = Manga.GetMangaCoverUrl(apiUri, manga.internalId, e.currentTarget);
console.log(manga.mangaId);
if(e.currentTarget.src != Manga.GetMangaCoverImageUrl(apiUri, manga.mangaId, e.currentTarget))
e.currentTarget.src = Manga.GetMangaCoverImageUrl(apiUri, manga.mangaId, e.currentTarget);
}
return(
<div className="SearchResult" key={manga.internalId}>
<img src="../../media/blahaj.png" onLoad={MangaCover} alt="Manga Cover"></img>
<p className="connector-name">{manga.mangaConnector.name}</p>
<div className="Manga-status" release-status={ReleaseStatusFromNumber(manga.releaseStatus)}></div>
<p className="Manga-name"><a href={manga.websiteUrl}>{manga.sortName}<img src="../../media/link.svg"
<div className="SearchResult" key={manga.mangaId}>
<img src={Manga.GetMangaCoverImageUrl(apiUri, manga.mangaId, undefined)} alt="Manga Cover" onLoad={MangaCover}></img>
<p className="connector-name">{manga.mangaConnectorId}</p>
<div className="Manga-status" release-status={manga.releaseStatus}></div>
<p className="Manga-name"><a href={manga.websiteUrl}>{manga.name}<img src="../../media/link.svg"
alt=""/></a></p>
<div className="Manga-tags">
{manga.authors.map(author => <p className="Manga-author" key={manga.internalId + "-author-" + author}>
<Icon path={mdiAccountEdit} size={0.5}/> {author}</p>)}
{manga.tags.map(tag => <p className="Manga-tag" key={manga.internalId + "-tag-" + tag}><Icon
path={mdiTagTextOutline} size={0.5}/> {tag}</p>)}
{manga.authorIds.map(authorId =>
<p className="Manga-author" key={manga.mangaId + "-author-" + authorId} >
<Icon path={mdiAccountEdit} size={0.5} />
<AuthorElement apiUri={apiUri} authorId={authorId}></AuthorElement>
</p>)}
{manga.tags.map(tag =>
<p className="Manga-tag" key={manga.mangaId + "-tag-" + tag}>
<Icon path={mdiTagTextOutline} size={0.5}/>
{tag}
</p>)}
{manga.linkIds.map(linkId =>
<p className="Manga-link" key={manga.mangaId + "-link-" + linkId}>
<Icon path={mdiLinkVariant} size={0.5}/>
<LinkElement apiUri={apiUri} linkId={linkId}></LinkElement>
</p>)}
</div>
<MarkdownPreview className="Manga-description" source={manga.description}
style={{backgroundColor: "transparent", color: "black"}}/>
<button className="Manga-AddButton" onClick={() => {
Job.CreateJobDateInterval(apiUri, manga.internalId, "MonitorManga", interval).then(() => onJobsChanged(manga.internalId));
Job.CreateDownloadAvailableChaptersJob(apiUri, manga.mangaId, interval.getMilliseconds()).then(() => onJobsChanged(manga.mangaId));
}}>Monitor
</button>
</div>);
}
function ProgressbarStr(job: IJob): string {
return job.progressToken.timeRemaining.substring(0,job.progressToken.timeRemaining.indexOf(".")).concat(" ", ToPercentString(job.progressToken.progress));
}
function ToPercentString(n: number): string {
return n.toString().substring(2,4).concat("%");
}
export function QueueItem(apiUri: string, manga: IManga, job: IJob, triggerUpdate: () => void){
const MangaCover : ReactEventHandler<HTMLImageElement> = (e) => {
if(e.currentTarget.src != Manga.GetMangaCoverUrl(apiUri, manga.internalId, e.currentTarget))
e.currentTarget.src = Manga.GetMangaCoverUrl(apiUri, manga.internalId, e.currentTarget);
}
return (
<div className="QueueJob" key={"QueueJob-" + job.id}>
<img src="../../media/blahaj.png" onLoad={MangaCover} alt="Manga Cover"></img>
<p className="QueueJob-Name">{manga.sortName}</p>
<p className="QueueJob-JobType">{JobTypeFromNumber(job.jobType)}</p>
<p className="QueueJob-additional">{job.jobType == 0 ? `Vol.${job.chapter?.volumeNumber} Ch.${job.chapter?.chapterNumber}` : ""}</p>
{job.progressToken.state === 0
? <ProgressBar labelColor={"#000"} height={"10px"} labelAlignment={"outside"}
className="QueueJob-Progressbar" completed={job.progressToken.progress} maxCompleted={1}
customLabel={ProgressbarStr(job)}/>
: <div className="QueueJob-Progressbar"></div>}
<div className="QueueJob" key={"QueueJob-" + job.jobId}>
<img src="../../media/blahaj.png" alt="Manga Cover"></img>
<p className="QueueJob-Name">{manga.name}</p>
<p className="QueueJob-JobType">{job.jobType}</p>
<div className="QueueJob-actions">
<button className="QueueJob-Cancel"
onClick={() => Job.CancelJob(apiUri, job.id).then(triggerUpdate)}>Cancel
onClick={() => Job.StopJob(apiUri, job.jobId).then(triggerUpdate)}>Cancel
</button>
{job.parentJobId != null
? <button className="QueueJob-Cancel"
onClick={() => Job.CancelJob(apiUri, job.parentJobId!).then(triggerUpdate)}>Cancel all
onClick={() => Job.StopJob(apiUri, job.parentJobId!).then(triggerUpdate)}>Cancel all
related</button>
: <></>
}

View File

@ -0,0 +1,5 @@
export default interface IMangaAltTitle {
altTitleId: string;
language: string;
title: string;
}

View File

@ -0,0 +1,7 @@
export default interface IMangaConnector {
name: string;
supportedLanguages: string[];
iconUrl: string;
baseUris: string[];
enabled: boolean;
}

View File

@ -1,5 +0,0 @@
export default interface IMangaConnector {
SupportedLanguages: string[];
name: string;
BaseUris: string[];
}

View File

@ -0,0 +1,7 @@
export default interface INotificationConnector {
name: string;
url: string;
headers: Record<string, string>[];
httpMethod: string;
body: string;
}

View File

@ -1,8 +0,0 @@
export default interface INotificationConnector {
"notificationConnectorType": number; //see NotificationConnectorType
"endpoint": string | undefined;//only on Ntfy, Gotify
"appToken": string | undefined;//only on Gotify
"auth": string | undefined;//only on Ntfy
"topic": string | undefined;//only on Ntfy
"id": string | undefined;//only on LunaSea
}

View File

@ -1,21 +0,0 @@
export default interface IProgressToken{
cancellationRequested: boolean;
increments: number;
incrementsCompleted: number;
progress: number;
lastUpdate: Date;
executionStarted: Date;
timeRemaining: string;
state: number;
}
export function GetProgressStateFromNumber(n: number): string {
switch (n){
case 0: return "Running";
case 1: return "Complete";
case 2: return "Standby";
case 3: return "Cancelled";
case 4: return "Waiting";
}
return "";
}

View File

@ -1,4 +0,0 @@
export default interface KeyValuePair {
key: string;
value: string;
}

View File

@ -0,0 +1,9 @@
export default interface ICoverFormatRequestRecord {
size: Size;
}
export interface Size {
width: number;
height: number;
isEmpty: boolean;
}

View File

@ -0,0 +1,5 @@
export default interface IGotifyRecord {
endpoint: string;
appToken: string;
priority: number;
}

View File

@ -0,0 +1,4 @@
export default interface IModifyJobRecord {
recurrenceMs: number;
enabled: boolean;
}

View File

@ -0,0 +1,7 @@
export default interface INtfyRecord {
endpoint: string;
username: string;
password: string;
topic: string;
priority: number;
}

View File

@ -0,0 +1,3 @@
export default interface IlunaseaRecord {
id: string;
}