Compare commits

..

14 Commits

33 changed files with 4634 additions and 37 deletions

1
.gitignore vendored
View File

@@ -4,3 +4,4 @@ Server/.vs
Server/bin
Server/obj
Server/Bibblan.csproj.user
Server/appsettings.user.json

View File

@@ -6,6 +6,7 @@
<meta name="theme-color" content="#000000" />
<link rel="shortcut icon" type="image/ico" href="/src/assets/favicon.ico" />
<title>Solid App</title>
<link href="/src/styles/styles.css" rel="stylesheet" />
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View File

@@ -1,4 +1,4 @@
import { render } from "solid-js/web";
import App from "./src/app";
import App from "./src/App";
render(() => <App />, document.getElementById("root")!);

1876
Frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -3,18 +3,23 @@
"version": "1.0.0",
"main": "index.ts",
"license": "MIT",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build"
},
"devDependencies": {
"@types/node": "^24.3.1",
"sass-embedded": "^1.92.0",
"terser": "^5.44.0",
"typescript": "^5.9.2",
"vite": "^7.1.4",
"vite-plugin-solid": "^2.11.8"
},
"dependencies": {
"@solidjs/router": "^0.15.3",
"bootstrap": "^5.3.8",
"solid-bootstrap": "^1.0.21",
"solid-js": "^1.9.9"
}
}

View File

@@ -1,43 +1,61 @@
import { Router, Route } from "@solidjs/router";
import type { Component } from "solid-js";
import { createSignal, For, onMount } from "solid-js";
interface Weather {
date: string;
temperatureC: number;
temperatureF: number;
summary: string;
}
import Home from "./components/Home";
import BookList from "./components/BookList";
import AuthorList from "./components/AuthorList";
import SeriesList from "./components/SeriesList";
import BookDetail from "./components/BookDetail";
const App: Component = () => {
const [weather, setWeather] = createSignal<Weather[]>([]);
onMount(() => {
fetch("/api/weatherforecast")
.then((r) => r.json())
.then(setWeather);
});
return (
<div>
<header>
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<a
href="https://github.com/solidjs/solid"
target="_blank"
rel="noopener noreferrer"
>
Learn Solid
</a>
<nav class="navbar navbar-expand-lg bg-body-tertiary">
<div class="container">
<a class="navbar-brand" href="#">
Navbar
</a>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarNavAltMarkup"
aria-controls="navbarNavAltMarkup"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNavAltMarkup">
<div class="navbar-nav">
<a class="nav-link active" href="/">
Home
</a>
<a class="nav-link" href="/books">
Books
</a>
<a class="nav-link" href="/authors">
Authors
</a>
<a class="nav-link" href="/series">
Series
</a>
</div>
</div>
</div>
</nav>
</header>
<For each={weather()}>
{(item) => (
<li>
{item.date} - {item.temperatureC}°C / {item.temperatureF}°F -{" "}
{item.summary}
</li>
)}
</For>
<main class="container">
<Router>
<Route path="/books/author/:authorid" component={BookList} />
<Route path="/books/:id" component={BookDetail} />
<Route path="/books" component={BookList} />
<Route path="/authors" component={AuthorList} />
<Route path="/series" component={SeriesList} />
<Route path="/" component={Home} />
</Router>
</main>
</div>
);
};

View File

@@ -0,0 +1,32 @@
import { Show, type Component } from "solid-js";
import { Author } from "../types/types";
import { Card } from "solid-bootstrap";
const AuthorCard: Component<{ author: Author }> = (props: {
author: Author;
}) => {
return (
<Card class="book-card col-lg-2 col-md-3 col-sm-4">
<div class="card-img-top missing">?</div>
<div
class="card-img-top author"
style={{
"background-image":
'url("/api/bibblan/authorcover/' +
encodeURIComponent(props.author.id) +
'")',
}}
></div>
<Card.Body>
<Card.Title>{props.author.name}</Card.Title>
<Card.Subtitle>
<a href={`/books/author/${props.author.id}`}>
{props.author.bookCount} books
</a>
</Card.Subtitle>
</Card.Body>
</Card>
);
};
export default AuthorCard;

View File

@@ -0,0 +1,35 @@
import { createSignal, onMount, For, type Component } from "solid-js";
import AuthorCard from "./AuthorCard";
import BibblanService from "../services/bibblanservice";
import { Author } from "../types/types";
const BookList: Component = () => {
const [authors, setAuthors] = createSignal<Author[]>([]);
const [query, setQuery] = createSignal("");
const update = (query: string) => {
setQuery(query);
BibblanService.getAuthors(query).then(setAuthors);
};
onMount(() => {
BibblanService.getAuthors().then(setAuthors);
});
return (
<div>
<h1>Authors!</h1>
<input
type="text"
class="form-control mb-3"
placeholder="Search..."
onInput={(e) => update(e.currentTarget.value)}
/>
<div class="book-grid d-flex flex-wrap justify-content-between gap-3 p-3">
<For each={authors()}>{(item) => <AuthorCard author={item} />}</For>
</div>
</div>
);
};
export default BookList;

View File

@@ -0,0 +1,38 @@
import { Show, createSignal, type Component } from "solid-js";
import { Book } from "../types/types";
import { Card, Modal } from "solid-bootstrap";
const BookCard: Component<{ book: Book }> = (props: { book: Book }) => {
const [show, setShow] = createSignal(false);
function handleClick(): void {
setShow(true);
}
return (
<Card class="book-card col-lg-2 col-md-3 col-sm-4">
<div class="card-img-top missing">?</div>
<Show when={true || props.book.hasCover}>
<div
class="card-img-top"
style={{
"background-image":
'url("/api/calibre/cover?path=' +
encodeURIComponent(props.book.path) +
'")',
}}
></div>
</Show>
<Card.Body>
<Card.Title>{props.book.title}</Card.Title>
<Card.Subtitle>{props.book.author}</Card.Subtitle>
<Card.Text>
Series: {props.book.seriesName} {props.book.seriesNumber}
</Card.Text>
<a href={`/books/${props.book.id}`} class="stretched-link"></a>
</Card.Body>
</Card>
);
};
export default BookCard;

View File

@@ -0,0 +1,31 @@
import { createSignal, onMount, For, Show, type Component } from "solid-js";
import { useParams } from "@solidjs/router";
import BibblanService from "../services/bibblanservice";
import { type BookDetail as BD } from "../types/types";
const BookDetail: Component = () => {
const [detail, setDetail] = createSignal<BD>({} as BD);
const params = useParams();
onMount(() => {
BibblanService.getBook(params.id).then((b) => {
console.log("detail", b);
setDetail(b);
});
});
return (
<>
<h1>Book detail - {detail()?.book?.title}</h1>
<Show when={detail()?.book?.id !== undefined}>
<br />
<pre>
<code>{JSON.stringify(detail(), null, 2)}</code>
</pre>
<br />
</Show>
</>
);
};
export default BookDetail;

View File

@@ -0,0 +1,39 @@
import { createSignal, onMount, For, Show, type Component } from "solid-js";
import { useParams } from "@solidjs/router";
import BookCard from "./BookCard";
import BibblanService from "../services/bibblanservice";
import { Book } from "../types/types";
const BookList: Component = () => {
const [books, setBooks] = createSignal<Book[]>([]);
const [query, setQuery] = createSignal("");
const params = useParams();
const update = (query: string) => {
setQuery(query);
BibblanService.getBooks(params.authorid, query).then(setBooks);
};
onMount(() => {
BibblanService.getBooks(params.authorid).then(setBooks);
});
return (
<div>
<h1>Books!</h1>
<Show when={!params.authorid}>
<input
type="text"
class="form-control mb-3"
placeholder="Search..."
onInput={(e) => update(e.currentTarget.value)}
/>
</Show>
<div class="book-grid d-flex flex-wrap justify-content-between gap-3 p-3">
<For each={books()}>{(item) => <BookCard book={item} />}</For>
</div>
</div>
);
};
export default BookList;

View File

@@ -0,0 +1,6 @@
import { type Component } from "solid-js";
const Home: Component = () => {
return <h1>Home!</h1>;
};
export default Home;

View File

@@ -0,0 +1,139 @@
import { createSignal, onMount, For, type Component, Show } from "solid-js";
import BookCard from "./BookCard";
import BibblanService from "../services/bibblanservice";
import { Series, ListBook, Book } from "../types/types";
import { OverlayTrigger, Popover, Button } from "solid-bootstrap";
const SeriesList: Component = () => {
const [series, setSeries] = createSignal<Series[]>([]);
const [query, setQuery] = createSignal("");
const update = (query: string) => {
setQuery(query);
BibblanService.getSeries(query).then(setSeries);
};
onMount(() => {
update(query());
});
const distinctAuthors = (
books: ListBook[]
): { id: number; name: string }[] => {
return [
...new Set(
books.map((b) => JSON.stringify({ id: b.authorId, name: b.authorName }))
),
].map((x) => JSON.parse(x)) as { id: number; name: string }[];
};
const minDate = (books: ListBook[]): string => {
if (books.length === 0) return "";
const darr = books.map((b) => new Date(b.pubDate));
const d = darr.reduce((min, b) => (b < min ? b : min), darr[0]);
return d?.toISOString().substr(0, 10) ?? "";
};
const maxDate = (books: ListBook[]): string => {
if (books.length === 0) return "";
const darr = books.map((b) => new Date(b.pubDate));
const d = darr.reduce((max, b) => (b > max ? b : max), darr[0]);
return d?.toISOString().substr(0, 10) ?? "";
};
function extendBook(item: ListBook, seriesName: string): Book {
return {
...item,
author: item.authorName,
comments: "",
language: "",
path: item.path,
hasCover: item.hasCover,
formats: [],
seriesName,
seriesNumber: item.seriesIndex,
};
}
function orderBySeriesIndex(books: ListBook[]): ListBook[] {
return [...books].sort((a, b) => {
const indexA = a.seriesIndex ?? 0;
const indexB = b.seriesIndex ?? 0;
return indexA - indexB;
});
}
return (
<div>
<h1>Series!</h1>
<input
type="text"
class="form-control mb-3"
placeholder="Search..."
onInput={(e) => update(e.currentTarget.value)}
/>
<table class="table p-3 series-table">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Author</th>
<th>Book Count</th>
<th>Published</th>
<th></th>
</tr>
</thead>
<tbody>
<For each={series()}>
{(item) => (
<>
<tr>
<td>{item.id}</td>
<td>{item.name}</td>
<td>
<For each={distinctAuthors(item.books)}>
{(author) => (
<a href={`/books/author/${author.id}`}>{author.name}</a>
)}
</For>
</td>
<td>{item.books.length}</td>
<td>
{minDate(item.books) ?? "N/A"} -{" "}
{maxDate(item.books) ?? "N/A"}
</td>
<td>
<OverlayTrigger
trigger="click"
offset={[0, 8]}
placement="left"
overlay={
<Popover id="popover-series" class="shadow">
<Popover.Header as="h3">{item.name}</Popover.Header>
<Popover.Body>
<div class="book-grid d-flex flex-wrap justify-content-between gap-3 p-3">
<For each={orderBySeriesIndex(item.books)}>
{(b) => (
<BookCard book={extendBook(b, item.name)} />
)}
</For>
</div>
</Popover.Body>
</Popover>
}
>
<Button variant="secondary">Books</Button>
</OverlayTrigger>
</td>
</tr>
</>
)}
</For>
</tbody>
</table>
</div>
);
};
export default SeriesList;

View File

@@ -0,0 +1,47 @@
import { Book, Author, Series, BookDetail } from "../types/types";
const BibblanService = {
getBooks: async (
authorid: string | undefined = undefined,
query: string | undefined = undefined
): Promise<Book[]> => {
let url = "/api/bibblan/books";
if (authorid != undefined) {
url += `/author/${authorid}`;
} else if (query != undefined && query.length > 0) {
url += `?query=${encodeURIComponent(query)}`;
}
const response = await fetch(url);
return response.json();
},
getBook: async (id: string): Promise<BookDetail> => {
let url = "/api/bibblan/books/" + id;
const response = await fetch(url);
return response.json();
},
getAuthors: async (
query: string | undefined = undefined
): Promise<Author[]> => {
let url = "/api/bibblan/authors";
if (query != undefined && query.length > 0) {
url += `?query=${encodeURIComponent(query)}`;
}
const response = await fetch(url);
return response.json();
},
getSeries: async (
query: string | undefined = undefined
): Promise<Series[]> => {
let url = "/api/bibblan/series";
if (query != undefined && query.length > 0) {
url += `?query=${encodeURIComponent(query)}`;
}
const response = await fetch(url);
return response.json();
},
};
export default BibblanService;

View File

@@ -0,0 +1,10 @@
import { Book } from "../types/types";
const CalibreService = {
getBooks: async (): Promise<Book[]> => {
const response = await fetch("/api/calibre/books");
return response.json();
},
};
export default CalibreService;

View File

@@ -0,0 +1,32 @@
@import "../../node_modules/bootstrap/scss/bootstrap.scss";
.book-card {
.card-img-top {
padding: 1rem;
aspect-ratio: 30 / 47;
background-size: contain;
width: calc(100% - 1.6rem);
margin-left: 0.8rem;
margin-top: 0.8rem;
background-repeat: no-repeat;
background-position: 0 0;
z-index: 1;
&.missing {
position: absolute;
display: block;
font-size: 7rem;
font-weight: 900;
color: #ccc;
text-align: center;
opacity: 0.8;
z-index: 0;
}
/* &.author { } */
}
}
#popover-series {
--bs-popover-max-width: 80%;
}

View File

@@ -0,0 +1,86 @@
export interface Book {
id: number;
title: string;
authorId: number;
author: string;
comments: string;
language: string;
path: string;
hasCover: boolean;
formats: Array<{ id: number; format: string; fileName: string }>;
seriesName: string;
seriesNumber: number;
}
export interface Author {
id: number;
name: string;
bookCount: number;
}
export interface ListBook {
id: number;
title: string;
authorId: number;
authorName: string;
path: string;
hasCover: boolean;
pubDate: Date;
seriesIndex: number;
}
export interface Series {
id: number;
name: string;
books: Array<ListBook>;
}
export interface Publisher {
id: number;
name: string;
sort: string;
}
export interface Tag {
id: number;
name: string;
}
export interface Rating {
id: number;
book: number;
rating: number;
}
export interface Language {
id: number;
book: number;
langCode: number;
itemOrder: number;
}
export interface BookComment {
id: number;
book: number;
text: string;
}
export interface BookData {
id: number;
book: number;
format: string;
uncompressedSize: number;
name: string;
}
export interface BookDetail {
book: Book;
authors: Author[];
publishers: Publisher[];
language: Language[];
ratings: any[];
series: Author[];
tags: Tag[];
comments: BookComment[];
data: BookData[];
}

View File

@@ -138,6 +138,11 @@
dependencies:
"@babel/helper-plugin-utils" "^7.27.1"
"@babel/runtime@^7.8.7":
version "7.28.4"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.4.tgz#a70226016fabe25c5783b2f22d3e1c9bc5ca3326"
integrity sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==
"@babel/template@^7.27.2":
version "7.27.2"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.2.tgz#fa78ceed3c4e7b63ebf6cb39e5852fca45f6809d"
@@ -168,6 +173,11 @@
"@babel/helper-string-parser" "^7.27.1"
"@babel/helper-validator-identifier" "^7.27.1"
"@bufbuild/protobuf@^2.5.0":
version "2.7.0"
resolved "https://registry.yarnpkg.com/@bufbuild/protobuf/-/protobuf-2.7.0.tgz#8944a4575abdc222839f93e90c861a67f1981787"
integrity sha512-qn6tAIZEw5i/wiESBF4nQxZkl86aY4KoO0IkUa2Lh+rya64oTOdJQFlZuMwI1Qz9VBJQrQC4QlSA2DNek5gCOA==
"@esbuild/aix-ppc64@0.25.9":
version "0.25.9"
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz#bef96351f16520055c947aba28802eede3c9e9a9"
@@ -332,6 +342,100 @@
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
"@parcel/watcher-android-arm64@2.5.1":
version "2.5.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz#507f836d7e2042f798c7d07ad19c3546f9848ac1"
integrity sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==
"@parcel/watcher-darwin-arm64@2.5.1":
version "2.5.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz#3d26dce38de6590ef79c47ec2c55793c06ad4f67"
integrity sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==
"@parcel/watcher-darwin-x64@2.5.1":
version "2.5.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz#99f3af3869069ccf774e4ddfccf7e64fd2311ef8"
integrity sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==
"@parcel/watcher-freebsd-x64@2.5.1":
version "2.5.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz#14d6857741a9f51dfe51d5b08b7c8afdbc73ad9b"
integrity sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==
"@parcel/watcher-linux-arm-glibc@2.5.1":
version "2.5.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz#43c3246d6892381db473bb4f663229ad20b609a1"
integrity sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==
"@parcel/watcher-linux-arm-musl@2.5.1":
version "2.5.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz#663750f7090bb6278d2210de643eb8a3f780d08e"
integrity sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==
"@parcel/watcher-linux-arm64-glibc@2.5.1":
version "2.5.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz#ba60e1f56977f7e47cd7e31ad65d15fdcbd07e30"
integrity sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==
"@parcel/watcher-linux-arm64-musl@2.5.1":
version "2.5.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz#f7fbcdff2f04c526f96eac01f97419a6a99855d2"
integrity sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==
"@parcel/watcher-linux-x64-glibc@2.5.1":
version "2.5.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz#4d2ea0f633eb1917d83d483392ce6181b6a92e4e"
integrity sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==
"@parcel/watcher-linux-x64-musl@2.5.1":
version "2.5.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz#277b346b05db54f55657301dd77bdf99d63606ee"
integrity sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==
"@parcel/watcher-win32-arm64@2.5.1":
version "2.5.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz#7e9e02a26784d47503de1d10e8eab6cceb524243"
integrity sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==
"@parcel/watcher-win32-ia32@2.5.1":
version "2.5.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz#2d0f94fa59a873cdc584bf7f6b1dc628ddf976e6"
integrity sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==
"@parcel/watcher-win32-x64@2.5.1":
version "2.5.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz#ae52693259664ba6f2228fa61d7ee44b64ea0947"
integrity sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==
"@parcel/watcher@^2.4.1":
version "2.5.1"
resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.5.1.tgz#342507a9cfaaf172479a882309def1e991fb1200"
integrity sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==
dependencies:
detect-libc "^1.0.3"
is-glob "^4.0.3"
micromatch "^4.0.5"
node-addon-api "^7.0.0"
optionalDependencies:
"@parcel/watcher-android-arm64" "2.5.1"
"@parcel/watcher-darwin-arm64" "2.5.1"
"@parcel/watcher-darwin-x64" "2.5.1"
"@parcel/watcher-freebsd-x64" "2.5.1"
"@parcel/watcher-linux-arm-glibc" "2.5.1"
"@parcel/watcher-linux-arm-musl" "2.5.1"
"@parcel/watcher-linux-arm64-glibc" "2.5.1"
"@parcel/watcher-linux-arm64-musl" "2.5.1"
"@parcel/watcher-linux-x64-glibc" "2.5.1"
"@parcel/watcher-linux-x64-musl" "2.5.1"
"@parcel/watcher-win32-arm64" "2.5.1"
"@parcel/watcher-win32-ia32" "2.5.1"
"@parcel/watcher-win32-x64" "2.5.1"
"@popperjs/core@^2.10.1":
version "2.11.8"
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f"
integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
"@rollup/rollup-android-arm-eabi@4.50.0":
version "4.50.0"
resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.0.tgz#939c1be9625d428d8513e4ab60d406fe8db23718"
@@ -437,6 +541,11 @@
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.50.0.tgz#d81efe6a12060c7feddf9805e2a94c3ab0679f48"
integrity sha512-xMmiWRR8sp72Zqwjgtf3QbZfF1wdh8X2ABu3EaozvZcyHJeU0r+XAnXdKgs4cCAp6ORoYoCygipYP1mjmbjrsg==
"@solidjs/router@^0.15.3":
version "0.15.3"
resolved "https://registry.yarnpkg.com/@solidjs/router/-/router-0.15.3.tgz#2c5e7aa637980ab7fce956aedc8cd20614163f2a"
integrity sha512-iEbW8UKok2Oio7o6Y4VTzLj+KFCmQPGEpm1fS3xixwFBdclFVBvaQVeibl1jys4cujfAK5Kn6+uG2uBm3lxOMw==
"@types/babel__core@^7.20.4":
version "7.20.5"
resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017"
@@ -506,6 +615,18 @@ babel-preset-solid@^1.8.4:
dependencies:
babel-plugin-jsx-dom-expressions "^0.40.1"
bootstrap@^5.3.8:
version "5.3.8"
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.3.8.tgz#6401a10057a22752d21f4e19055508980656aeed"
integrity sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==
braces@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789"
integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
dependencies:
fill-range "^7.1.1"
browserslist@^4.24.0:
version "4.25.4"
resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.25.4.tgz#ebdd0e1d1cf3911834bab3a6cd7b917d9babf5af"
@@ -516,6 +637,11 @@ browserslist@^4.24.0:
node-releases "^2.0.19"
update-browserslist-db "^1.1.3"
buffer-builder@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/buffer-builder/-/buffer-builder-0.2.0.tgz#3322cd307d8296dab1f604618593b261a3fade8f"
integrity sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==
buffer-from@^1.0.0:
version "1.1.2"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5"
@@ -526,6 +652,18 @@ caniuse-lite@^1.0.30001737:
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001739.tgz#b34ce2d56bfc22f4352b2af0144102d623a124f4"
integrity sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA==
chokidar@^4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30"
integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==
dependencies:
readdirp "^4.0.1"
colorjs.io@^0.5.0:
version "0.5.2"
resolved "https://registry.yarnpkg.com/colorjs.io/-/colorjs.io-0.5.2.tgz#63b20139b007591ebc3359932bef84628eb3fcef"
integrity sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==
commander@^2.20.0:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
@@ -536,7 +674,7 @@ convert-source-map@^2.0.0:
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a"
integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==
csstype@^3.1.0:
csstype@^3.0.2, csstype@^3.1.0:
version "3.1.3"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
@@ -548,6 +686,19 @@ debug@^4.1.0, debug@^4.3.1:
dependencies:
ms "^2.1.3"
detect-libc@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"
integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==
dom-helpers@^5.2.0:
version "5.2.1"
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902"
integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==
dependencies:
"@babel/runtime" "^7.8.7"
csstype "^3.0.2"
electron-to-chromium@^1.5.211:
version "1.5.214"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.214.tgz#f7bbdc0796124292d4b8a34a49e968c5e6430763"
@@ -600,6 +751,13 @@ fdir@^6.4.4, fdir@^6.5.0:
resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350"
integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==
fill-range@^7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292"
integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
dependencies:
to-regex-range "^5.0.1"
fsevents@~2.3.2, fsevents@~2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
@@ -610,11 +768,38 @@ gensync@^1.0.0-beta.2:
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
has-flag@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
html-entities@2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.3.3.tgz#117d7626bece327fc8baace8868fa6f5ef856e46"
integrity sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==
immutable@^5.0.2:
version "5.1.3"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-5.1.3.tgz#e6486694c8b76c37c063cca92399fa64098634d4"
integrity sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==
is-extglob@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
is-glob@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084"
integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
dependencies:
is-extglob "^2.1.1"
is-number@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b"
integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
is-what@^4.1.8:
version "4.1.16"
resolved "https://registry.yarnpkg.com/is-what/-/is-what-4.1.16.tgz#1ad860a19da8b4895ad5495da3182ce2acdd7a6f"
@@ -649,6 +834,14 @@ merge-anything@^5.1.7:
dependencies:
is-what "^4.1.8"
micromatch@^4.0.5:
version "4.0.8"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202"
integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
dependencies:
braces "^3.0.3"
picomatch "^2.3.1"
ms@^2.1.3:
version "2.1.3"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
@@ -659,6 +852,11 @@ nanoid@^3.3.11:
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b"
integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==
node-addon-api@^7.0.0:
version "7.1.1"
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558"
integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==
node-releases@^2.0.19:
version "2.0.19"
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314"
@@ -676,6 +874,11 @@ picocolors@^1.1.1:
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b"
integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
picomatch@^2.3.1:
version "2.3.1"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
picomatch@^4.0.2, picomatch@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.3.tgz#796c76136d1eead715db1e7bad785dedd695a042"
@@ -690,6 +893,11 @@ postcss@^8.5.6:
picocolors "^1.1.1"
source-map-js "^1.2.1"
readdirp@^4.0.1:
version "4.1.2"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d"
integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==
rollup@^4.43.0:
version "4.50.0"
resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.50.0.tgz#6f237f598b7163ede33ce827af8534c929aaa186"
@@ -720,6 +928,151 @@ rollup@^4.43.0:
"@rollup/rollup-win32-x64-msvc" "4.50.0"
fsevents "~2.3.2"
rxjs@^7.4.0:
version "7.8.2"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.2.tgz#955bc473ed8af11a002a2be52071bf475638607b"
integrity sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==
dependencies:
tslib "^2.1.0"
sass-embedded-all-unknown@1.92.0:
version "1.92.0"
resolved "https://registry.yarnpkg.com/sass-embedded-all-unknown/-/sass-embedded-all-unknown-1.92.0.tgz#73b87468f0c3083c0a181e677730c1df891f3bce"
integrity sha512-0VcRBilndf8Iot7zfKKEYH7Ig4JBRjltf7Ba9dNL6wtv0m1a36cm8FgZFofrXtDjUgVTV/aEH/Xw4zBUs6vFYA==
dependencies:
sass "1.92.0"
sass-embedded-android-arm64@1.92.0:
version "1.92.0"
resolved "https://registry.yarnpkg.com/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.92.0.tgz#31bb0e18f9d00da2dbc4ed50625ad5cc8b33777f"
integrity sha512-m0JY0QyskN77AFpA8FKxqXZNWSzrPvKOvZqOu1DwEEipyHuSdiAVFDHZ6EpI3aABxCXE3jgkP+Ij2mb4hiLxFw==
sass-embedded-android-arm@1.92.0:
version "1.92.0"
resolved "https://registry.yarnpkg.com/sass-embedded-android-arm/-/sass-embedded-android-arm-1.92.0.tgz#db635e9ca83b9b98402d7bf7f144484d59282a72"
integrity sha512-0NH0zElKL5gLdNcWFzYX/bqtpoFq5ogcU+4vLdmpBXA9Zl5NFPXAPRA6K8pgjQNWpnV7bG05JSIVPuNKZ60Ptg==
sass-embedded-android-riscv64@1.92.0:
version "1.92.0"
resolved "https://registry.yarnpkg.com/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.92.0.tgz#e61f1ba5373b41231ce728ab64901494f7ca8d9a"
integrity sha512-wfVRf2PFR15vCgJE3SWLZQRo+98xm7vKvpHiaPU4satutwMKC8yXxDvsc7hFBqQYBtqHNK5ap5dZSXlgdGGZrA==
sass-embedded-android-x64@1.92.0:
version "1.92.0"
resolved "https://registry.yarnpkg.com/sass-embedded-android-x64/-/sass-embedded-android-x64-1.92.0.tgz#5602f9dd439022c1257f156cdb589d9731400a85"
integrity sha512-bc1c3OMdfrYoiIzVzsMI3KBnIa4mEumg7jHzonkDUIUWrfUNsbzlO+UXX1CcRyfaOIqyKNNZLVvBCz8EwmUsbQ==
sass-embedded-darwin-arm64@1.92.0:
version "1.92.0"
resolved "https://registry.yarnpkg.com/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.92.0.tgz#f06c2550be25890fc1e9ead9fed908cff0709c36"
integrity sha512-vZh1WCL2QlQyTlAD8snmC8W90XBZI/125o15bfKkGbUzV58dkZJf413hk6JVQS2+a0lZT4GxvrlGH1fSaSNTug==
sass-embedded-darwin-x64@1.92.0:
version "1.92.0"
resolved "https://registry.yarnpkg.com/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.92.0.tgz#e2382ff4cd2a38aa13d9d7e0fd4aaa68d1df7433"
integrity sha512-4vJsXpOQAgU+KrrW/3POvhnfkG9iJ4gU8ujeEDXqCSSY2M+5B/j0S6iXB7nu3Z9MfmCl8V4B6xyeB0EWE5Ul0g==
sass-embedded-linux-arm64@1.92.0:
version "1.92.0"
resolved "https://registry.yarnpkg.com/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.92.0.tgz#17022911a8bcc4ede6c60a90e04fdb48a0fdce83"
integrity sha512-HH4LNY1svM2Lv6NCxqOQca42hzG/o55ON9X3T0R18Rl9kVb3y5qiJpdrHh7sSlZWF4qhHYbRc9BIc+Tw142oog==
sass-embedded-linux-arm@1.92.0:
version "1.92.0"
resolved "https://registry.yarnpkg.com/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.92.0.tgz#96b2fbed0165f788a0973dca0d04145212549882"
integrity sha512-HMjTDjIT8bHwAVd0c8r8QvGxGZwJg3H06/Y5ZiaiwVGiZtYS9jfef6LnrPw8LY5cOG8wm9RZ9AgVqvkL7E2jBQ==
sass-embedded-linux-musl-arm64@1.92.0:
version "1.92.0"
resolved "https://registry.yarnpkg.com/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.92.0.tgz#714e6029f873dac5fce788e6ce0561dc82f25e95"
integrity sha512-xYzZDmcPb3BsaD6qlRTqZqtyMOZfGCSKJBZYj2ZRJiKDDr1sqPSIqKx6G8jc1wJAVdvoNp5tzENnCfY7NRkxNA==
sass-embedded-linux-musl-arm@1.92.0:
version "1.92.0"
resolved "https://registry.yarnpkg.com/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.92.0.tgz#5bc5ff27414f5d8242fdf7681ba9ab73b5098c68"
integrity sha512-qJDCXm379yRT9+8wKSi6nHFCOODTmD6XmE8rqmMozKo6kvCM+Y3sAMlHrT/0+pfzlGh1JSamkoYIo/ODn+LRVA==
sass-embedded-linux-musl-riscv64@1.92.0:
version "1.92.0"
resolved "https://registry.yarnpkg.com/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.92.0.tgz#c4ce07f74b14c32a1f886a49b9e5b0e1fd6371ec"
integrity sha512-ZD3a6c7YvAjp1lEkKyaQpHc5EuetQ0RU3YoTfjwHiyWwezsuJHZc4hkS7SXWbZNEvi7tc2U1bdt4nSdx9c5Qxw==
sass-embedded-linux-musl-x64@1.92.0:
version "1.92.0"
resolved "https://registry.yarnpkg.com/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.92.0.tgz#967cbe4280edb2e50b1991c2b75f31143f9ce006"
integrity sha512-ShivGoEKmpyL57hQB9K+EMEOWOo+LuwH5eIM2T0sRIHW5n28IS6h12R3WEJVf+PYtSi9FKWazy7kzeLefya6fQ==
sass-embedded-linux-riscv64@1.92.0:
version "1.92.0"
resolved "https://registry.yarnpkg.com/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.92.0.tgz#3f584bf3038d5d5ef931ab124cca2e4ac51364fc"
integrity sha512-CTZF8rMYBS4JsGGFMUwdPExq6DxhONXQv9omKpVmuleRw52Isx37GaMTQg5zSxunS6QfwqCyUysjWXTLe8khHA==
sass-embedded-linux-x64@1.92.0:
version "1.92.0"
resolved "https://registry.yarnpkg.com/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.92.0.tgz#1a4b70ab5f71045c1ca77c59489f0371e6bc47c0"
integrity sha512-jAY4tzhSUUDUYSl0m+GQub/ZpVk00Pn4ybHeUICAYSQj043A9rkag+LSKDGCvC/0MptMM+/HkIDAC06tRY4PeQ==
sass-embedded-unknown-all@1.92.0:
version "1.92.0"
resolved "https://registry.yarnpkg.com/sass-embedded-unknown-all/-/sass-embedded-unknown-all-1.92.0.tgz#3400c5ec2f6d7b4d488b09f6f7f4337736fb6cdb"
integrity sha512-s0UF1jquqhrxg0dl/0E+L5tCH1zv1ueF+m3VgJukDkDSTW+nb7wpCGcm8csGoSXnP8+dq53jtUXVtt8sPLr8ZQ==
dependencies:
sass "1.92.0"
sass-embedded-win32-arm64@1.92.0:
version "1.92.0"
resolved "https://registry.yarnpkg.com/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.92.0.tgz#92bef1e92023f6023bfcf87fbf126e354b91f317"
integrity sha512-K8x+q2W0VyGPBtO3b0AlpecGOk47ce2FkEX0WD1gEexpbRCytQ+udDACHQGXpwWYPgSIT9ky0IASzDVT1fjMcw==
sass-embedded-win32-x64@1.92.0:
version "1.92.0"
resolved "https://registry.yarnpkg.com/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.92.0.tgz#71fb693e1e8f2a976f882dcf9fad3c7b6879a0ed"
integrity sha512-b0051n7EwSvH580u8LjsCAj2US1F59FY6/GbWJWlE2bidzY86/f8ovl4LsGY/uM3lNzWQlA/0BGmdAm44d/qJg==
sass-embedded@^1.92.0:
version "1.92.0"
resolved "https://registry.yarnpkg.com/sass-embedded/-/sass-embedded-1.92.0.tgz#6b1ba02343c253e66a7545e34076751a5f9d2f2c"
integrity sha512-daqnoAA+AmXvcL1fvJRMd4RDPZM2s27qYxb51c5TYc1B1Zugu0gVGyA5leoXQJEzo6sDTQ95J8X0yFcdBNGNtw==
dependencies:
"@bufbuild/protobuf" "^2.5.0"
buffer-builder "^0.2.0"
colorjs.io "^0.5.0"
immutable "^5.0.2"
rxjs "^7.4.0"
supports-color "^8.1.1"
sync-child-process "^1.0.2"
varint "^6.0.0"
optionalDependencies:
sass-embedded-all-unknown "1.92.0"
sass-embedded-android-arm "1.92.0"
sass-embedded-android-arm64 "1.92.0"
sass-embedded-android-riscv64 "1.92.0"
sass-embedded-android-x64 "1.92.0"
sass-embedded-darwin-arm64 "1.92.0"
sass-embedded-darwin-x64 "1.92.0"
sass-embedded-linux-arm "1.92.0"
sass-embedded-linux-arm64 "1.92.0"
sass-embedded-linux-musl-arm "1.92.0"
sass-embedded-linux-musl-arm64 "1.92.0"
sass-embedded-linux-musl-riscv64 "1.92.0"
sass-embedded-linux-musl-x64 "1.92.0"
sass-embedded-linux-riscv64 "1.92.0"
sass-embedded-linux-x64 "1.92.0"
sass-embedded-unknown-all "1.92.0"
sass-embedded-win32-arm64 "1.92.0"
sass-embedded-win32-x64 "1.92.0"
sass@1.92.0:
version "1.92.0"
resolved "https://registry.yarnpkg.com/sass/-/sass-1.92.0.tgz#02d9ae21ce1763def2cd461449aac2eb56364796"
integrity sha512-KDNI0BxgIRDAfJgzNm5wuy+4yOCIZyrUbjSpiU/JItfih+KGXAVefKL53MTml054MmBA3DDKIBMSI/7XLxZJ3A==
dependencies:
chokidar "^4.0.0"
immutable "^5.0.2"
source-map-js ">=0.6.2 <2.0.0"
optionalDependencies:
"@parcel/watcher" "^2.4.1"
semver@^6.3.1:
version "6.3.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
@@ -735,6 +1088,24 @@ seroval@~1.3.0:
resolved "https://registry.yarnpkg.com/seroval/-/seroval-1.3.2.tgz#7e5be0dc1a3de020800ef013ceae3a313f20eca7"
integrity sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==
solid-bootstrap-core@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/solid-bootstrap-core/-/solid-bootstrap-core-2.0.0.tgz#5e9bd86bd1c21fe6d3b019542c324f50cfda12df"
integrity sha512-tw1me1iEvI+UzYRL2Gs53MP51WVwx785CU+6KQVGhaLESw3OoayeFLhe1CvUYb7kuskjtNGyAorZMdMwBJQIBA==
dependencies:
"@popperjs/core" "^2.10.1"
dom-helpers "^5.2.0"
solid-react-transition "^1.0.8"
solid-bootstrap@^1.0.21:
version "1.0.21"
resolved "https://registry.yarnpkg.com/solid-bootstrap/-/solid-bootstrap-1.0.21.tgz#9183ba07e52f88db448710a1fa953320983d3d02"
integrity sha512-B1+1qsdqrdQhl4xgmgrFRafxOM3xrnafxrC1scbOTYYJI2ido1XfoxtYyJ72W1eSHxYjm8eRGXdGTGdBJsGPpg==
dependencies:
dom-helpers "^5.2.0"
solid-bootstrap-core "^2.0.0"
solid-react-transition "^1.0.11"
solid-js@^1.9.9:
version "1.9.9"
resolved "https://registry.yarnpkg.com/solid-js/-/solid-js-1.9.9.tgz#7c61402a969a246973ac55ff78a06210eabc097c"
@@ -744,6 +1115,11 @@ solid-js@^1.9.9:
seroval "~1.3.0"
seroval-plugins "~1.3.0"
solid-react-transition@^1.0.11, solid-react-transition@^1.0.8:
version "1.0.11"
resolved "https://registry.yarnpkg.com/solid-react-transition/-/solid-react-transition-1.0.11.tgz#10c044b800674cadb2a3fb33af8a72f87bc8e9c3"
integrity sha512-YMT7z6sOupCicDtX19156vbVOm3vCIgjVhPTybR9gLKiIPrDB2NDVqnQk4kpNCDZTOwSjLTOyUQw0xJnXgDg2A==
solid-refresh@^0.6.3:
version "0.6.3"
resolved "https://registry.yarnpkg.com/solid-refresh/-/solid-refresh-0.6.3.tgz#d23ef80f04e177619c9234a809c573cb16360627"
@@ -753,7 +1129,7 @@ solid-refresh@^0.6.3:
"@babel/helper-module-imports" "^7.22.15"
"@babel/types" "^7.23.6"
source-map-js@^1.2.1:
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
@@ -771,6 +1147,25 @@ source-map@^0.6.0:
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
supports-color@^8.1.1:
version "8.1.1"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c"
integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==
dependencies:
has-flag "^4.0.0"
sync-child-process@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/sync-child-process/-/sync-child-process-1.0.2.tgz#45e7c72e756d1243e80b547ea2e17957ab9e367f"
integrity sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==
dependencies:
sync-message-port "^1.0.0"
sync-message-port@^1.0.0:
version "1.1.3"
resolved "https://registry.yarnpkg.com/sync-message-port/-/sync-message-port-1.1.3.tgz#6055c565ee8c81d2f9ee5aae7db757e6d9088c0c"
integrity sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg==
terser@^5.44.0:
version "5.44.0"
resolved "https://registry.yarnpkg.com/terser/-/terser-5.44.0.tgz#ebefb8e5b8579d93111bfdfc39d2cf63879f4a82"
@@ -789,6 +1184,18 @@ tinyglobby@^0.2.14:
fdir "^6.4.4"
picomatch "^4.0.2"
to-regex-range@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4"
integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
dependencies:
is-number "^7.0.0"
tslib@^2.1.0:
version "2.8.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f"
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
typescript@^5.9.2:
version "5.9.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.2.tgz#d93450cddec5154a2d5cabe3b8102b83316fb2a6"
@@ -812,6 +1219,11 @@ validate-html-nesting@^1.2.1:
resolved "https://registry.yarnpkg.com/validate-html-nesting/-/validate-html-nesting-1.2.3.tgz#22edb5c77de247f9b2b20f6becee3d8c614f24cb"
integrity sha512-kdkWdCl6eCeLlRShJKbjVOU2kFKxMF8Ghu50n+crEoyx+VKm3FxAxF9z4DCy6+bbTOqNW0+jcIYRnjoIRzigRw==
varint@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/varint/-/varint-6.0.0.tgz#9881eb0ce8feaea6512439d19ddf84bf551661d0"
integrity sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==
vite-plugin-solid@^2.11.8:
version "2.11.8"
resolved "https://registry.yarnpkg.com/vite-plugin-solid/-/vite-plugin-solid-2.11.8.tgz#db3d9a9f0b8c82f7fcacf8c5ebea4a3136bf107d"

View File

@@ -10,10 +10,24 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.66" />
<PackageReference Include="GraphQL.Client" Version="6.1.0" />
<PackageReference Include="GraphQL.Client.Serializer.Newtonsoft" Version="6.1.0" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.6" />
<PackageReference Include="Microsoft.AspNetCore.SpaProxy">
<Version>9.*-*</Version>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.8" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
</ItemGroup>
<ItemGroup>
<Content Update="appsettings.user.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,26 @@
using Bibblan.Models;
using Microsoft.Extensions.Options;
using System.Net.Http.Headers;
namespace Bibblan.Business.Clients
{
public class HardcoverAuthenticationHandler : DelegatingHandler
{
public BibblanOptions settings;
public HardcoverAuthenticationHandler(IOptions<BibblanOptions> options) : base()
{
settings = options.Value;
InnerHandler = new HttpClientHandler();
}
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", settings.HardcoverApiToken);
return await base.SendAsync(request, cancellationToken);
}
}
}

View File

@@ -0,0 +1,154 @@
using GraphQL;
using GraphQL.Client.Abstractions;
using static Microsoft.EntityFrameworkCore.DbLoggerCategory;
namespace Bibblan.Business.Clients
{
public class Publisher
{
public string Name { get; set; }
}
public class Image
{
public string Url { get; set; }
}
public class Author
{
public long Id { get; set; }
public string Name { get; set; }
public string Name_Personal { get; set; }
public Image Image { get; set; }
public string Bio { get; set; }
public int Book_Count { get; set; }
public DateTime? Born_Date { get; set; }
public DateTime? Death_Date { get; set; }
public int? Gender_Id{ get; set; }
public Dictionary<string, List<string>> Identifiers { get; set; }
public string State { get; set; }
}
public class Contributor
{
public Author Author { get; set; }
}
public class Edition
{
public long Id { get; set; }
public string Title { get; set; }
public string Edition_Format { get; set; }
public int? Pages { get; set; }
public DateTime? Release_Date { get; set; }
public string Isbn_10 { get; set; }
public string Isbn_13 { get; set; }
public Publisher Publisher { get; set; }
public List<Contributor> Contributions { get; set; }
public Image Image { get; set; }
}
public class EditionsCollectionType
{
public List<Edition> Editions { get; set; }
}
public class AuthorsCollectionType
{
public List<Author> Authors { get; set; }
}
public class HardcoverClient
{
private readonly IGraphQLClient _client;
public HardcoverClient(IGraphQLClient client)
{
_client = client;
}
public async Task<List<Edition>> LookupBook(string title, string? author = null)
{
var query = new GraphQLRequest
{
Query = @"
query ($title: String!, $author: String) {
editions(
where: {title: {_eq: $title}, _and: {contributions: {author: {name: {_eq: $author}}}}}
) {
id
title
edition_format
pages
release_date
isbn_10
isbn_13
publisher {
name
}
contributions {
author {
id
name
name_personal
image {
url
}
bio
books_count
born_date
death_date
gender_id
identifiers
state
}
}
image {
url
}
}
}",
Variables = new
{
title,
author
}
};
var response = await _client.SendQueryAsync<EditionsCollectionType>(query);
return response.Data.Editions;
}
public async Task<List<Author>> LookupAuthor(string name)
{
//order_by: {books_count: desc}
//limit: 1
var query = new GraphQLRequest
{
Query = @"query ($name: String){
authors(where: {name: {_eq: $name}}) {
id
name
name_personal
image {
url
}
bio
books_count
born_date
death_date
gender_id
identifiers
state
}
}",
Variables = new
{
name
}
};
var response = await _client.SendQueryAsync<AuthorsCollectionType>(query);
return response.Data.Authors;
}
}
}

View File

@@ -0,0 +1,57 @@
using Bibblan.Models;
using Bibblan.ViewModels;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.Extensions.Options;
namespace Bibblan.Business.Services
{
public class CalibreService
{
private readonly BibblanOptions _options;
SqliteCalibreContext _context;
public CalibreService(SqliteCalibreContext context, IOptions<BibblanOptions> options)
{
_options = options.Value;
_context = context;
}
public byte[] Cover(string path)
{
var fullPath = Path.Combine(_options.CalibreRoot, path, "cover.jpg");
if (!File.Exists(fullPath))
return null;
return File.ReadAllBytes(fullPath);
}
public IQueryable<BookVm> GetAllBooks()
{
return from b in _context.Books
join ba in _context.BooksAuthorsLink on b.Id equals ba.Book
join a in _context.Authors on ba.Author equals a.Id
join comment in _context.Comments on b.Id equals comment.Book into comments
from comment in comments.DefaultIfEmpty()
join bl in _context.BooksLanguagesLink on b.Id equals bl.Book
join l in _context.Languages on bl.LangCode equals l.Id
join bs in _context.BooksSeriesLink on b.Id equals bs.Book into book_series_link
from bs in book_series_link.DefaultIfEmpty()
join s in _context.Series on bs.Series equals s.Id into series
from s in series.DefaultIfEmpty()
orderby b.Title
select new BookVm
{
Id = b.Id,
Title = b.Title,
AuthorId = a.Id,
Author = a.Sort,
Comments = comment.Text,
Language = l.LangCode,
Path = b.Path,
HasCover = b.HasCover,// == "1",
Formats = (from d in _context.Data where d.Book == b.Id orderby d.Format select new DataVm { Id = d.Id, Format = d.Format, FileName = d.Name + "." + d.Format.ToLower() }).ToList(),
SeriesName = s.Name,
SeriesNumber = b.SeriesIndex
};
}
}
}

View File

@@ -0,0 +1,159 @@
using Bibblan.Models;
using Bibblan.ViewModels;
using Dapper;
using Microsoft.Extensions.Options;
using Npgsql;
namespace Bibblan.Business.Services
{
public class BookFilter
{
public int? Id;
public int? Author;
public string? Query;
}
public class DatabaseService(IOptions<BibblanOptions> options)
{
readonly BibblanOptions settings = options.Value;
public List<Books> GetBooks(int count, BookFilter filter = null)
{
var conn = new NpgsqlConnection(settings.BibblanConnection);
var query = "select * from books";
object parameters = null;
if (filter != null)
{
query += " where ";
if(filter.Author != null)
{
query += $"id in (select book from books_authors_link where author = {filter.Author})";
}
else if(filter.Id != null)
{
query += $"id = {filter.Id}";
}
else if(!String.IsNullOrWhiteSpace(query))
{
filter.Query = filter.Query.ToLowerInvariant();
query += $"lower(title) like @query or lower(author_sort) like @query";
parameters = new { query = "%" + filter.Query + "%" };
}
}
return conn.Query<Books>(query, parameters).Take(count).ToList();
}
public BookDetailVm GetBookDetails(int id)
{
var query = @"select * from books where id = @book;
select * from authors where id in (select author from books_authors_link where book = @book);
select * from books_languages_link where book = @book;
select * from publishers where id in (select publisher from books_publishers_link where book = @book);
select * from books_ratings_link where book = @book;
select * from series where id in (select series from books_series_link where book = @book);
select * from tags where id in (select tag from books_tags_link where book = @book);
select * from comments where book = @book;
select * from data where book = @book;";
using (var conn = new NpgsqlConnection(settings.BibblanConnection))
{
var results = conn.QueryMultiple(query, new { book = id });
var book = results.ReadFirst<Books>();
var authors = results.Read<Authors>();
var lang = results.Read<BooksLanguagesLink>();
var publishers = results.Read<Publishers>();
var ratings = results.Read<BooksRatingsLink>();
var series = results.Read<Series>();
var tags = results.Read<Tags>();
var comments = results.Read<Comments>();
var data = results.Read<Data>();
return new BookDetailVm
{
Book = book,
Authors = authors,
Language = lang,
Publishers = publishers,
Ratings = ratings,
Series = series,
Tags = tags,
Comments = comments,
Data = data
};
}
}
public IEnumerable<AuthorVm> GetAuthors(int count, BookFilter filter = null)
{
var query = "select a.id, a.name, count(bal.*) as bookcount\r\nfrom authors a\r\nleft join books_authors_link bal on a.id = bal.author\r\ngroup by a.id , a.name ";
object parameters = null;
if (!String.IsNullOrWhiteSpace(filter?.Query))
{
filter.Query = filter.Query.ToLowerInvariant();
query += $" having lower(a.name) like @query";
parameters = new { query = "%" + filter.Query + "%" };
}
var conn = new NpgsqlConnection(settings.BibblanConnection);
return conn.Query<AuthorVm>(query, parameters).Take(count).ToList();
}
internal List<SeriesVm> GetSeries(int count, BookFilter? filter)
{
var query = @"select s.id as seriesid, s.name as seriesname, b.*, a.*
from series s
inner join books_series_link bsl on s.id = bsl.series
inner join books b on bsl.book = b.id
inner join books_authors_link bal on b.id = bal.book
inner join authors a on bal.author = a.id";
object parameters = null;
if (!String.IsNullOrWhiteSpace(filter?.Query))
{
filter.Query = filter.Query?.ToLowerInvariant().Trim() ?? "";
query += $" where lower(s.name) like @query";
parameters = new { query = "%" + filter.Query + "%" };
}
var conn = new NpgsqlConnection(settings.BibblanConnection);
var lookup = new Dictionary<long, SeriesVm>();
conn.Query<SeriesVm, Books, Authors, SeriesVm>(query, (s,b,a) =>
{
if (!lookup.TryGetValue(s.Id, out SeriesVm svm))
{
lookup.Add(s.Id, svm = s);
}
svm.Books.Add(new ListBook
{
AuthorId = a.Id,
AuthorName = a.Name,
Id = b.Id,
PubDate = b.Pubdate,
HasCover = b.HasCover,
Path = b.Path,
SeriesIndex = b.SeriesIndex,
Title = b.Title
});
return svm;
}, splitOn: "id", param: parameters);
return lookup.Values.Take(count).ToList();
}
internal List<Tag> GetTags(BookFilter? filter)
{
var query = @"select t.id, t.name, count(btl.*) as bookcount from tags t
inner join books_tags_link btl on t.id = btl.tag
group by t.id, t.name
order by bookcount desc";
object parameters = null;
if (!String.IsNullOrWhiteSpace(filter?.Query))
{
filter.Query = filter.Query.ToLowerInvariant();
query += $" having lower(name) like @query";
parameters = new { query = "%" + filter.Query + "%" };
}
var conn = new NpgsqlConnection(settings.BibblanConnection);
return conn.Query<Tag>(query,parameters).ToList();
}
}
}

View File

@@ -0,0 +1,126 @@
using Bibblan.Business.Clients;
using Bibblan.Business.Services;
using GraphQL;
using GraphQL.Client.Abstractions;
using Microsoft.AspNetCore.Mvc;
namespace Bibblan.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class BibblanController : ControllerBase
{
DatabaseService _db;
CalibreService _calibre;
HardcoverClient _hardCover;
public BibblanController(DatabaseService databaseService, CalibreService calibre, HardcoverClient hcclient)
{
_db = databaseService;
_calibre = calibre;
_hardCover = hcclient;
}
[HttpGet("cover")]
public IActionResult GetCover(string path)
{
//TODO: Bör kanske inte gå direkt mot calibres filer..
var bytes = _calibre.Cover(path);
if (bytes == null)
return NotFound();
return File(bytes, "image/jpeg", true);
}
[HttpGet("books")]
public IActionResult GetBooks(string query = null)
{
BookFilter? filter = query != null ? new BookFilter
{
Query = query
} : null;
var books = _db.GetBooks(100,filter).ToList();
return Ok(books);
}
[HttpGet("books/{id}")]
public IActionResult GetBook(int id)
{
var book = _db.GetBookDetails(id);
return Ok(book);
}
[HttpGet("authors")]
public IActionResult GetAuthors(string query = null)
{
BookFilter? filter = query != null ? new BookFilter
{
Query = query
} : null;
var authors = _db.GetAuthors(100, filter).ToList();
return Ok(authors);
}
[HttpGet("books/author/{authorid}")]
public IActionResult GetBooksByAuthor(int authorid)
{
var authors = _db.GetBooks(100, new BookFilter
{
Author = authorid
}).ToList();
return Ok(authors);
}
[HttpGet("authorcover/{authorid}")]
public IActionResult GetAuthorCover(int authorid)
{
//TODO: fixa vid tillfälle
return Ok(new { desc = "picture of banana goes here"});
}
[HttpGet("series")]
public IActionResult GetSeries(string query = null)
{
BookFilter? filter = query != null ? new BookFilter
{
Query = query
} : null;
var series = _db.GetSeries(100, filter).ToList();
return Ok(series);
}
[HttpGet("tags")]
public IActionResult GetTags(string query = null)
{
BookFilter? filter = query != null ? new BookFilter
{
Query = query
} : null;
var tags = _db.GetTags(filter).ToList();
return Ok(tags);
}
//test
[HttpGet("lookupbook")]
public async Task<IActionResult> LookupBook(string title, string author)
{
var result = await _hardCover.LookupBook(title, author);
return Ok(result);
}
[HttpGet("lookupauthor")]
public async Task<IActionResult> LookupAuthor(string name)
{
var result = await _hardCover.LookupAuthor(name);
return Ok(result);
}
}
}

View File

@@ -0,0 +1,313 @@
using Bibblan.Business.Services;
using Bibblan.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;
namespace Bibblan.Controllers
{
[ApiController]
[Route("api/[controller]")]
public class CalibreController : ControllerBase
{
CalibreService _service;
PostgresCalibreContext _postgresContext;
SqliteCalibreContext _sqliteContext;
public CalibreController(CalibreService service, SqliteCalibreContext sqliteContext, PostgresCalibreContext postgresContext) {
_service = service;
_postgresContext = postgresContext;
_sqliteContext = sqliteContext;
}
[HttpGet("books")]
public IActionResult GetBooks()
{
var books = _service.GetAllBooks().Take(100).ToList();
return Ok(books);
}
[HttpGet("cover")]
public IActionResult GetCover(string path)
{
var bytes = _service.Cover(path);
if (bytes == null)
return NotFound();
return File(bytes, "image/jpeg", true);
}
[HttpGet("dump")]
public IActionResult Dump()
{
//var e = _postgresContext.Database.EnsureCreated();
//var script = _postgresContext.Database.GenerateCreateScript();
// _postgresContext.Database.ExecuteSqlRaw(script);
var books = 1;// DumpBooks();
var auhtors = 1;// DumpAuthors();
var comments = 1;// DumpComments();
var data = 1;// DumpData();
var identifiers = 1;// DumpIdentifiers();
var languages = 1;// DumpLanguages();
var publishers = 1;// DumpPublishers();
var ratings = 1;// DumpRatings();
var series = 1;// DumpSeries();
var tags = 1;// DumpTags();
var ba = 1;// Dumpa(_postgresContext, _sqliteContext.BooksAuthorsLink.OrderBy(x => x.Id), _postgresContext.BooksAuthorsLink);
var bl = 1;// Dumpa(_postgresContext, _sqliteContext.BooksLanguagesLink.OrderBy(x => x.Id), _postgresContext.BooksLanguagesLink);
var bp = 1;// Dumpa(_postgresContext, _sqliteContext.BooksPublishersLink.OrderBy(x => x.Id), _postgresContext.BooksPublishersLink);
var br = 1;// Dumpa(_postgresContext, _sqliteContext.BooksRatingsLink.OrderBy(x => x.Id), _postgresContext.BooksRatingsLink);
var bs = 1;// Dumpa(_postgresContext, _sqliteContext.BooksSeriesLink.OrderBy(x => x.Id), _postgresContext.BooksSeriesLink);
var bt = 1;// Dumpa(_postgresContext, _sqliteContext.BooksTagsLink.OrderBy(x => x.Id), _postgresContext.BooksTagsLink);
return Ok(new
{
books,
auhtors,
comments,
data,
identifiers,
languages,
publishers,
ratings,
series,
tags,
links = new
{
ba,
bl,
bp,
br,
bs,
bt
}
});
}
private int Dumpa<T>(DbContext toContext, IQueryable<T> from, DbSet<T> to)
where T : class
{
var current = 0;
int step = 100;
var done = false;
while (!done)
{
var records = from.Skip(current).Take(step);
done = records.Count() == 0;
foreach (var record in records)
{
to.Add(record);
}
toContext.SaveChanges();
current += step;
}
return current;
}
private int DumpBooks() {
var current = 0;
int step = 100;
var done = false;
while (!done)
{
var books = _sqliteContext.Books.OrderBy(b => b.Id).Skip(current).Take(step);
done = books.Count() == 0;
foreach (var book in books)
{
_postgresContext.Books.Add(book);
}
_postgresContext.SaveChanges();
current += step;
}
return current;
}
private int DumpAuthors()
{
var current = 0;
int step = 100;
var done = false;
while (!done)
{
var authors = _sqliteContext.Authors.OrderBy(b => b.Id).Skip(current).Take(step);
done = authors.Count() == 0;
foreach (var author in authors)
{
_postgresContext.Authors.Add(author);
}
_postgresContext.SaveChanges();
current += step;
}
return current;
}
private int DumpComments()
{
var current = 0;
int step = 100;
var done = false;
while (!done)
{
var comments = _sqliteContext.Comments.OrderBy(b => b.Id).Skip(current).Take(step);
done = comments.Count() == 0;
foreach (var comment in comments)
{
_postgresContext.Comments.Add(comment);
}
_postgresContext.SaveChanges();
current += step;
}
return current;
}
private int DumpData()
{
var current = 0;
int step = 100;
var done = false;
while (!done)
{
var data = _sqliteContext.Data.OrderBy(b => b.Id).Skip(current).Take(step);
done = data.Count() == 0;
foreach (var d in data)
{
_postgresContext.Data.Add(d);
}
_postgresContext.SaveChanges();
current += step;
}
return current;
}
private int DumpIdentifiers()
{
var current = 0;
int step = 100;
var done = false;
while (!done)
{
var identifiers = _sqliteContext.Identifiers.OrderBy(b => b.Id).Skip(current).Take(step);
done = identifiers.Count() == 0;
foreach (var id in identifiers)
{
_postgresContext.Identifiers.Add(id);
}
_postgresContext.SaveChanges();
current += step;
}
return current;
}
private int DumpLanguages()
{
var current = 0;
int step = 100;
var done = false;
while (!done)
{
var languages = _sqliteContext.Languages.OrderBy(b => b.Id).Skip(current).Take(step);
done = languages.Count() == 0;
foreach (var lang in languages)
{
_postgresContext.Languages.Add(lang);
}
_postgresContext.SaveChanges();
current += step;
}
return current;
}
private int DumpPublishers()
{
var current = 0;
int step = 100;
var done = false;
while (!done)
{
var publishers = _sqliteContext.Publishers.OrderBy(b => b.Id).Skip(current).Take(step);
done = publishers.Count() == 0;
foreach (var pub in publishers)
{
if(pub.Sort == null)
{
pub.Sort = pub.Name;
}
_postgresContext.Publishers.Add(pub);
}
_postgresContext.SaveChanges();
current += step;
}
return current;
}
private int DumpRatings()
{
var current = 0;
int step = 100;
var done = false;
while (!done)
{
var ratings = _sqliteContext.Ratings.OrderBy(b => b.Id).Skip(current).Take(step);
done = ratings.Count() == 0;
foreach (var rating in ratings)
{
_postgresContext.Ratings.Add(rating);
}
_postgresContext.SaveChanges();
current += step;
}
return current;
}
private int DumpSeries()
{
var current = 0;
int step = 100;
var done = false;
while (!done)
{
var series = _sqliteContext.Series.OrderBy(b => b.Id).Skip(current).Take(step);
done = series.Count() == 0;
foreach (var ser in series)
{
_postgresContext.Series.Add(ser);
}
_postgresContext.SaveChanges();
current += step;
}
return current;
}
private int DumpTags()
{
var current = 0;
int step = 100;
var done = false;
while (!done)
{
var tags = _sqliteContext.Tags.OrderBy(b => b.Id).Skip(current).Take(step);
done = tags.Count() == 0;
foreach (var tag in tags)
{
_postgresContext.Tags.Add(tag);
}
_postgresContext.SaveChanges();
current += step;
}
return current;
}
}
}

View File

@@ -0,0 +1,13 @@
namespace Bibblan.Models
{
public class BibblanOptions
{
public const string Bibblan = "Bibblan";
public string CalibreDb { get; set; } = String.Empty;
public string CalibreRoot { get; set; } = String.Empty;
public string BibblanConnection { get; set; } = String.Empty;
public string HardcoverApiToken { get; set; } = string.Empty;
public string HardcoverApiUrl { get; set; } = string.Empty;
}
}

View File

@@ -0,0 +1,781 @@
using Microsoft.EntityFrameworkCore;
namespace Bibblan.Models
{
public partial class Authors
{
public long Id { get; set; }
public string Name { get; set; }
public string Sort { get; set; }
public string Link { get; set; }
}
public partial class Books
{
public long Id { get; set; }
public string Title { get; set; }
public string Sort { get; set; }
public DateTime Timestamp { get; set; }
public DateTime Pubdate { get; set; }
public double SeriesIndex { get; set; }
public string AuthorSort { get; set; }
public string Isbn { get; set; }
public string Lccn { get; set; }
public string Path { get; set; }
public long Flags { get; set; }
public string Uuid { get; set; }
public bool HasCover { get; set; }
public DateTime LastModified { get; set; }
}
public partial class BooksAuthorsLink
{
public long Id { get; set; }
public long Book { get; set; }
public long Author { get; set; }
}
public partial class BooksLanguagesLink
{
public long Id { get; set; }
public long Book { get; set; }
public long LangCode { get; set; }
public long ItemOrder { get; set; }
}
public partial class BooksPluginData
{
public long Id { get; set; }
public long Book { get; set; }
public string Name { get; set; }
public string Val { get; set; }
}
public partial class BooksPublishersLink
{
public long Id { get; set; }
public long Book { get; set; }
public long Publisher { get; set; }
}
public partial class BooksRatingsLink
{
public long Id { get; set; }
public long Book { get; set; }
public long Rating { get; set; }
}
public partial class BooksSeriesLink
{
public long Id { get; set; }
public long Book { get; set; }
public long Series { get; set; }
}
public partial class BooksTagsLink
{
public long Id { get; set; }
public long Book { get; set; }
public long Tag { get; set; }
}
public partial class Comments
{
public long Id { get; set; }
public long Book { get; set; }
public string Text { get; set; }
}
public partial class ConversionOptions
{
public long Id { get; set; }
public string Format { get; set; }
public long? Book { get; set; }
public byte[] Data { get; set; }
}
public partial class CustomColumns
{
public long Id { get; set; }
public string Label { get; set; }
public string Name { get; set; }
public string Datatype { get; set; }
public bool MarkForDelete { get; set; }
public bool Editable { get; set; }
public string Display { get; set; }
public bool IsMultiple { get; set; }
public bool Normalized { get; set; }
}
public partial class Data
{
public long Id { get; set; }
public long Book { get; set; }
public string Format { get; set; }
public long UncompressedSize { get; set; }
public string Name { get; set; }
}
public partial class Feeds
{
public long Id { get; set; }
public string Title { get; set; }
public string Script { get; set; }
}
public partial class Identifiers
{
public long Id { get; set; }
public long Book { get; set; }
public string Type { get; set; }
public string Val { get; set; }
}
public partial class Languages
{
public long Id { get; set; }
public string LangCode { get; set; }
}
public partial class LibraryId
{
public long Id { get; set; }
public string Uuid { get; set; }
}
public partial class MetadataDirtied
{
public long Id { get; set; }
public long Book { get; set; }
}
public partial class Preferences
{
public long Id { get; set; }
public string Key { get; set; }
public string Val { get; set; }
}
public partial class Publishers
{
public long Id { get; set; }
public string Name { get; set; }
public string? Sort { get; set; }
}
public partial class Ratings
{
public long Id { get; set; }
public long? Rating { get; set; }
}
public partial class Series
{
public long Id { get; set; }
public string Name { get; set; }
public string Sort { get; set; }
}
public partial class Tags
{
public long Id { get; set; }
public string Name { get; set; }
}
public partial class BaseContext : DbContext
{
public virtual DbSet<Authors> Authors { get; set; }
public virtual DbSet<Books> Books { get; set; }
public virtual DbSet<BooksAuthorsLink> BooksAuthorsLink { get; set; }
public virtual DbSet<BooksLanguagesLink> BooksLanguagesLink { get; set; }
public virtual DbSet<BooksPluginData> BooksPluginData { get; set; }
public virtual DbSet<BooksPublishersLink> BooksPublishersLink { get; set; }
public virtual DbSet<BooksRatingsLink> BooksRatingsLink { get; set; }
public virtual DbSet<BooksSeriesLink> BooksSeriesLink { get; set; }
public virtual DbSet<BooksTagsLink> BooksTagsLink { get; set; }
public virtual DbSet<Comments> Comments { get; set; }
public virtual DbSet<ConversionOptions> ConversionOptions { get; set; }
public virtual DbSet<CustomColumns> CustomColumns { get; set; }
public virtual DbSet<Data> Data { get; set; }
public virtual DbSet<Feeds> Feeds { get; set; }
public virtual DbSet<Identifiers> Identifiers { get; set; }
public virtual DbSet<Languages> Languages { get; set; }
public virtual DbSet<LibraryId> LibraryId { get; set; }
public virtual DbSet<MetadataDirtied> MetadataDirtied { get; set; }
public virtual DbSet<Preferences> Preferences { get; set; }
public virtual DbSet<Publishers> Publishers { get; set; }
public virtual DbSet<Ratings> Ratings { get; set; }
public virtual DbSet<Series> Series { get; set; }
public virtual DbSet<Tags> Tags { get; set; }
public BaseContext(DbContextOptions options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Authors>(entity =>
{
entity.ToTable("authors");
entity.HasIndex(e => e.Name)
.IsUnique();
entity.Property(e => e.Id)
.HasColumnName("id")
.ValueGeneratedNever();
entity.Property(e => e.Link)
.IsRequired()
.HasColumnName("link")
.HasDefaultValueSql("\"\"");
entity.Property(e => e.Name)
.IsRequired()
.HasColumnName("name");
entity.Property(e => e.Sort).HasColumnName("sort");
});
modelBuilder.Entity<Books>(entity =>
{
entity.ToTable("books");
entity.HasIndex(e => e.AuthorSort)
.HasDatabaseName("authors_idx");
entity.HasIndex(e => e.Sort)
.HasDatabaseName("books_idx");
entity.Property(e => e.Id)
.HasColumnName("id")
.ValueGeneratedNever();
entity.Property(e => e.AuthorSort).HasColumnName("author_sort");
entity.Property(e => e.Flags)
.HasColumnName("flags")
.HasDefaultValueSql("1");
entity.Property(e => e.HasCover)
.HasColumnName("has_cover")
.HasColumnType("BOOL")
.HasDefaultValueSql("0");
entity.Property(e => e.Isbn)
.HasColumnName("isbn")
.HasDefaultValueSql("\"\"");
entity.Property(e => e.LastModified)
.IsRequired()
.HasColumnName("last_modified")
.HasColumnType("TIMESTAMP")
.HasDefaultValueSql("\"2000-01-01 00:00:00+00:00\"");
entity.Property(e => e.Lccn)
.HasColumnName("lccn")
.HasDefaultValueSql("\"\"");
entity.Property(e => e.Path)
.IsRequired()
.HasColumnName("path")
.HasDefaultValueSql("\"\"");
entity.Property(e => e.Pubdate)
.HasColumnName("pubdate")
.HasColumnType("TIMESTAMP")
.HasDefaultValueSql("CURRENT_TIMESTAMP");
entity.Property(e => e.SeriesIndex)
.HasColumnName("series_index")
.HasDefaultValueSql("1.0");
entity.Property(e => e.Sort).HasColumnName("sort");
entity.Property(e => e.Timestamp)
.HasColumnName("timestamp")
.HasColumnType("TIMESTAMP")
.HasDefaultValueSql("CURRENT_TIMESTAMP");
entity.Property(e => e.Title)
.IsRequired()
.HasColumnName("title")
.HasDefaultValueSql("'Unknown'");
entity.Property(e => e.Uuid).HasColumnName("uuid");
});
modelBuilder.Entity<BooksAuthorsLink>(entity =>
{
entity.ToTable("books_authors_link");
entity.HasIndex(e => e.Author)
.HasDatabaseName("books_authors_link_aidx");
entity.HasIndex(e => e.Book)
.HasDatabaseName("books_authors_link_bidx");
entity.HasIndex(e => new { e.Book, e.Author })
.IsUnique();
entity.Property(e => e.Id)
.HasColumnName("id")
.ValueGeneratedNever();
entity.Property(e => e.Author).HasColumnName("author");
entity.Property(e => e.Book).HasColumnName("book");
});
modelBuilder.Entity<BooksLanguagesLink>(entity =>
{
entity.ToTable("books_languages_link");
entity.HasIndex(e => e.Book)
.HasDatabaseName("books_languages_link_bidx");
entity.HasIndex(e => e.LangCode)
.HasDatabaseName("books_languages_link_aidx");
entity.HasIndex(e => new { e.Book, e.LangCode })
.IsUnique();
entity.Property(e => e.Id)
.HasColumnName("id")
.ValueGeneratedNever();
entity.Property(e => e.Book).HasColumnName("book");
entity.Property(e => e.ItemOrder)
.HasColumnName("item_order")
.HasDefaultValueSql("0");
entity.Property(e => e.LangCode).HasColumnName("lang_code");
});
modelBuilder.Entity<BooksPluginData>(entity =>
{
entity.ToTable("books_plugin_data");
entity.HasIndex(e => new { e.Book, e.Name })
.IsUnique();
entity.Property(e => e.Id)
.HasColumnName("id")
.ValueGeneratedNever();
entity.Property(e => e.Book).HasColumnName("book");
entity.Property(e => e.Name)
.IsRequired()
.HasColumnName("name");
entity.Property(e => e.Val)
.IsRequired()
.HasColumnName("val");
});
modelBuilder.Entity<BooksPublishersLink>(entity =>
{
entity.ToTable("books_publishers_link");
entity.HasIndex(e => e.Book)
.HasDatabaseName("books_publishers_link_bidx");
entity.HasIndex(e => e.Publisher)
.HasDatabaseName("books_publishers_link_aidx");
entity.Property(e => e.Id)
.HasColumnName("id")
.ValueGeneratedNever();
entity.Property(e => e.Book).HasColumnName("book");
entity.Property(e => e.Publisher).HasColumnName("publisher");
});
modelBuilder.Entity<BooksRatingsLink>(entity =>
{
entity.ToTable("books_ratings_link");
entity.HasIndex(e => e.Book)
.HasDatabaseName("books_ratings_link_bidx");
entity.HasIndex(e => e.Rating)
.HasDatabaseName("books_ratings_link_aidx");
entity.HasIndex(e => new { e.Book, e.Rating })
.IsUnique();
entity.Property(e => e.Id)
.HasColumnName("id")
.ValueGeneratedNever();
entity.Property(e => e.Book).HasColumnName("book");
entity.Property(e => e.Rating).HasColumnName("rating");
});
modelBuilder.Entity<BooksSeriesLink>(entity =>
{
entity.ToTable("books_series_link");
entity.HasIndex(e => e.Book)
.HasDatabaseName("books_series_link_bidx");
entity.HasIndex(e => e.Series)
.HasDatabaseName("books_series_link_aidx");
entity.Property(e => e.Id)
.HasColumnName("id")
.ValueGeneratedNever();
entity.Property(e => e.Book).HasColumnName("book");
entity.Property(e => e.Series).HasColumnName("series");
});
modelBuilder.Entity<BooksTagsLink>(entity =>
{
entity.ToTable("books_tags_link");
entity.HasIndex(e => e.Book)
.HasDatabaseName("books_tags_link_bidx");
entity.HasIndex(e => e.Tag)
.HasDatabaseName("books_tags_link_aidx");
entity.HasIndex(e => new { e.Book, e.Tag })
.IsUnique();
entity.Property(e => e.Id)
.HasColumnName("id")
.ValueGeneratedNever();
entity.Property(e => e.Book).HasColumnName("book");
entity.Property(e => e.Tag).HasColumnName("tag");
});
modelBuilder.Entity<Comments>(entity =>
{
entity.ToTable("comments");
entity.HasIndex(e => e.Book)
.HasDatabaseName("comments_idx");
entity.Property(e => e.Id)
.HasColumnName("id")
.ValueGeneratedNever();
entity.Property(e => e.Book).HasColumnName("book");
entity.Property(e => e.Text)
.IsRequired()
.HasColumnName("text");
});
modelBuilder.Entity<ConversionOptions>(entity =>
{
entity.ToTable("conversion_options");
entity.HasIndex(e => e.Book)
.HasDatabaseName("conversion_options_idx_b");
entity.HasIndex(e => e.Format)
.HasDatabaseName("conversion_options_idx_a");
entity.HasIndex(e => new { e.Format, e.Book })
.IsUnique();
entity.Property(e => e.Id)
.HasColumnName("id")
.ValueGeneratedNever();
entity.Property(e => e.Book).HasColumnName("book");
entity.Property(e => e.Data)
.IsRequired()
.HasColumnName("data");
entity.Property(e => e.Format)
.IsRequired()
.HasColumnName("format");
});
modelBuilder.Entity<CustomColumns>(entity =>
{
entity.ToTable("custom_columns");
entity.HasIndex(e => e.Label)
.HasDatabaseName("custom_columns_idx");
entity.Property(e => e.Id)
.HasColumnName("id")
.ValueGeneratedNever();
entity.Property(e => e.Datatype)
.IsRequired()
.HasColumnName("datatype");
entity.Property(e => e.Display)
.IsRequired()
.HasColumnName("display")
.HasDefaultValueSql("\"{}\"");
entity.Property(e => e.Editable)
.IsRequired()
.HasColumnName("editable")
.HasColumnType("BOOL")
.HasDefaultValueSql("1");
entity.Property(e => e.IsMultiple)
.IsRequired()
.HasColumnName("is_multiple")
.HasColumnType("BOOL")
.HasDefaultValueSql("0");
entity.Property(e => e.Label)
.IsRequired()
.HasColumnName("label");
entity.Property(e => e.MarkForDelete)
.IsRequired()
.HasColumnName("mark_for_delete")
.HasColumnType("BOOL")
.HasDefaultValueSql("0");
entity.Property(e => e.Name)
.IsRequired()
.HasColumnName("name");
entity.Property(e => e.Normalized)
.IsRequired()
.HasColumnName("normalized")
.HasColumnType("BOOL");
});
modelBuilder.Entity<Data>(entity =>
{
entity.ToTable("data");
entity.HasIndex(e => e.Book)
.HasDatabaseName("data_idx");
entity.HasIndex(e => e.Format)
.HasDatabaseName("formats_idx");
entity.HasIndex(e => new { e.Book, e.Format })
.IsUnique();
entity.Property(e => e.Id)
.HasColumnName("id")
.ValueGeneratedNever();
entity.Property(e => e.Book).HasColumnName("book");
entity.Property(e => e.Format)
.IsRequired()
.HasColumnName("format");
entity.Property(e => e.Name)
.IsRequired()
.HasColumnName("name");
entity.Property(e => e.UncompressedSize).HasColumnName("uncompressed_size");
});
modelBuilder.Entity<Feeds>(entity =>
{
entity.ToTable("feeds");
entity.HasIndex(e => e.Title)
.IsUnique();
entity.Property(e => e.Id)
.HasColumnName("id")
.ValueGeneratedNever();
entity.Property(e => e.Script)
.IsRequired()
.HasColumnName("script");
entity.Property(e => e.Title)
.IsRequired()
.HasColumnName("title");
});
modelBuilder.Entity<Identifiers>(entity =>
{
entity.ToTable("identifiers");
entity.HasIndex(e => new { e.Book, e.Type })
.IsUnique();
entity.Property(e => e.Id)
.HasColumnName("id")
.ValueGeneratedNever();
entity.Property(e => e.Book).HasColumnName("book");
entity.Property(e => e.Type)
.IsRequired()
.HasColumnName("type")
.HasDefaultValueSql("\"isbn\"");
entity.Property(e => e.Val)
.IsRequired()
.HasColumnName("val");
});
modelBuilder.Entity<Languages>(entity =>
{
entity.ToTable("languages");
entity.HasIndex(e => e.LangCode)
.HasDatabaseName("languages_idx");
entity.Property(e => e.Id)
.HasColumnName("id")
.ValueGeneratedNever();
entity.Property(e => e.LangCode)
.IsRequired()
.HasColumnName("lang_code");
});
modelBuilder.Entity<LibraryId>(entity =>
{
entity.ToTable("library_id");
entity.HasIndex(e => e.Uuid)
.IsUnique();
entity.Property(e => e.Id)
.HasColumnName("id")
.ValueGeneratedNever();
entity.Property(e => e.Uuid)
.IsRequired()
.HasColumnName("uuid");
});
modelBuilder.Entity<MetadataDirtied>(entity =>
{
entity.ToTable("metadata_dirtied");
entity.HasIndex(e => e.Book)
.IsUnique();
entity.Property(e => e.Id)
.HasColumnName("id")
.ValueGeneratedNever();
entity.Property(e => e.Book).HasColumnName("book");
});
modelBuilder.Entity<Preferences>(entity =>
{
entity.ToTable("preferences");
entity.HasIndex(e => e.Key)
.IsUnique();
entity.Property(e => e.Id)
.HasColumnName("id")
.ValueGeneratedNever();
entity.Property(e => e.Key)
.IsRequired()
.HasColumnName("key");
entity.Property(e => e.Val)
.IsRequired()
.HasColumnName("val");
});
modelBuilder.Entity<Publishers>(entity =>
{
entity.ToTable("publishers");
entity.HasIndex(e => e.Name)
.HasDatabaseName("publishers_idx");
entity.Property(e => e.Id)
.HasColumnName("id")
.ValueGeneratedNever();
entity.Property(e => e.Name)
.IsRequired()
.HasColumnName("name");
entity.Property(e => e.Sort).HasColumnName("sort");
});
modelBuilder.Entity<Ratings>(entity =>
{
entity.ToTable("ratings");
entity.HasIndex(e => e.Rating)
.IsUnique();
entity.Property(e => e.Id)
.HasColumnName("id")
.ValueGeneratedNever();
entity.Property(e => e.Rating).HasColumnName("rating");
});
modelBuilder.Entity<Series>(entity =>
{
entity.ToTable("series");
entity.HasIndex(e => e.Name)
.HasDatabaseName("series_idx");
entity.Property(e => e.Id)
.HasColumnName("id")
.ValueGeneratedNever();
entity.Property(e => e.Name)
.IsRequired()
.HasColumnName("name");
entity.Property(e => e.Sort).HasColumnName("sort");
});
modelBuilder.Entity<Tags>(entity =>
{
entity.ToTable("tags");
entity.HasIndex(e => e.Name)
.HasDatabaseName("tags_idx");
entity.Property(e => e.Id)
.HasColumnName("id")
.ValueGeneratedNever();
entity.Property(e => e.Name)
.IsRequired()
.HasColumnName("name");
});
}
}
public partial class SqliteCalibreContext : BaseContext
{
public SqliteCalibreContext(DbContextOptions<SqliteCalibreContext> options) : base(options)
{
}
}
public partial class PostgresCalibreContext : BaseContext
{
public PostgresCalibreContext(DbContextOptions<PostgresCalibreContext> options) : base(options)
{
}
}
}

View File

@@ -1,13 +1,60 @@
var builder = WebApplication.CreateBuilder(args);
using Bibblan.Business.Clients;
using Bibblan.Business.Services;
using Bibblan.Models;
using Bibblan.ViewModels;
using Dapper;
using GraphQL.Client.Abstractions;
using GraphQL.Client.Http;
using GraphQL.Client.Serializer.Newtonsoft;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using System.ComponentModel.DataAnnotations.Schema;
using System.Net.Http;
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddJsonFile($"appsettings.user.json", true, true);
var configSection = builder.Configuration.GetSection(BibblanOptions.Bibblan);
BibblanOptions config = new();
configSection.Bind(config);
// Add services to the container.
builder.Services.Configure<BibblanOptions>(configSection);
builder.Services.AddScoped<CalibreService, CalibreService>();
builder.Services.AddScoped<DatabaseService, DatabaseService>();
builder.Services.AddControllers();
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();
//hardcover registration
builder.Services.AddTransient<HardcoverAuthenticationHandler>();//m<>ste vara transient
builder.Services.AddScoped<IGraphQLClient>(s => new GraphQLHttpClient(new GraphQLHttpClientOptions
{
HttpMessageHandler = s.GetRequiredService<HardcoverAuthenticationHandler>(),
EndPoint = new Uri(config.HardcoverApiUrl),
}, new NewtonsoftJsonSerializer()));
builder.Services.AddScoped<HardcoverClient>();
//databases
builder.Services.AddDbContext<SqliteCalibreContext>(options => options.UseSqlite($"Data Source={config.CalibreDb};Mode=ReadOnly;"));
builder.Services.AddDbContext<PostgresCalibreContext>(options => options.UseNpgsql(config.BibblanConnection));
var app = builder.Build();
Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;
Dapper.SqlMapper.SetTypeMap(
typeof(SeriesVm),
new CustomPropertyTypeMap(
typeof(SeriesVm),
(type, columnName) =>
type.GetProperties().FirstOrDefault(prop =>
prop.GetCustomAttributes(false)
.OfType<ColumnAttribute>()
.Any(attr => attr.Name == columnName))));
app.UseDefaultFiles();
app.MapStaticAssets();

View File

@@ -0,0 +1,9 @@
namespace Bibblan.ViewModels
{
public class AuthorVm
{
public long Id { get; set; }
public string Name { get; set; }
public int BookCount { get; set; }
}
}

View File

@@ -0,0 +1,40 @@
using Bibblan.Models;
namespace Bibblan.ViewModels
{
public class BookDetailVm
{
public Books Book { get; internal set; }
public IEnumerable<Authors> Authors { get; internal set; }
public IEnumerable<Publishers> Publishers { get; internal set; }
public IEnumerable<BooksLanguagesLink> Language { get; internal set; }
public IEnumerable<BooksRatingsLink> Ratings { get; internal set; }
public IEnumerable<Series> Series { get; internal set; }
public IEnumerable<Tags> Tags { get; internal set; }
public IEnumerable<Comments> Comments { get; internal set; }
public IEnumerable<Data> Data { get; internal set; }
}
public class BookVm
{
public long Id { get; set; }
public string Title { get; set; }
public string Author { get; set; }
public long AuthorId { get; set; }
public string Comments { get; set; }
public string Language { get; set; }
public string Path { get; set; }
public List<DataVm> Formats { get; set; }
public bool HasCover { get; set; }
public string SeriesName { get; set; }
public double SeriesNumber { get; set; }
public string TitleAndSeries
{
get
{
return Title + (!string.IsNullOrWhiteSpace(SeriesName) ? $" ({SeriesName + (SeriesNumber > 0 ? $" {SeriesNumber:##}" : string.Empty)})" : string.Empty);
}
}
}
}

View File

@@ -0,0 +1,9 @@
namespace Bibblan.ViewModels
{
public class DataVm
{
public long Id { get; set; }
public string Format { get; set; }
public string FileName { get; set; }
}
}

View File

@@ -0,0 +1,34 @@
using System.ComponentModel.DataAnnotations.Schema;
namespace Bibblan.ViewModels
{
public class Tag
{
public long Id { get; set; }
public string Name { get; set; }
public int BookCount { get; set; }
}
public class ListBook
{
public long Id { get; set; }
public string Title { get; set; }
public DateTime PubDate { get; set; }
public double SeriesIndex { get; set; }
public long AuthorId { get; set; }
public string AuthorName { get; set; }
public bool HasCover { get; set; }
public string Path { get; set; }
}
public class SeriesVm
{
[Column("seriesid")]
public long Id { get; set; }
[Column("seriesname")]
public string Name { get; set; }
public List<ListBook> Books { get; set; } = new();
}
}

View File

@@ -5,5 +5,12 @@
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
"AllowedHosts": "*",
"Bibblan": {
"CalibreDb": "metadata.db",
"CalibreRoot": "c:\\my_books\\",
"BibblanConnection": "Server=localhost;Port=5432;Database=bibblan;User Id=bibblanuser;Password=1234567;",
"HardcoverApiUrl": "https://api.hardcover.app/v1/graphql",
"HardcoverApiToken": ""
}
}